feat: implement local-only command checkers for cli and mcp (#1426)

This commit is contained in:
Ralph Khreish
2025-11-19 22:08:04 +01:00
committed by GitHub
parent 99d9179522
commit 4049f34d5a
57 changed files with 2676 additions and 482 deletions

View File

@@ -82,6 +82,12 @@ export type {
} from './modules/auth/types.js';
export { AuthenticationError } from './modules/auth/types.js';
// Auth constants
export {
LOCAL_ONLY_COMMANDS,
type LocalOnlyCommand
} from './modules/auth/index.js';
// Brief types
export type { Brief } from './modules/briefs/types.js';
export type { TagWithStats } from './modules/briefs/services/brief-service.js';

View File

@@ -16,6 +16,7 @@ import type {
OAuthFlowOptions,
UserContext
} from './types.js';
import { checkAuthBlock, type AuthBlockResult } from './command.guard.js';
/**
* Display information for storage context
@@ -225,6 +226,41 @@ export class AuthDomain {
return `${baseUrl}/home/${context.orgSlug}/briefs/create`;
}
// ========== Command Guards ==========
/**
* Check if a local-only command should be blocked when using API storage
*
* Local-only commands (like dependency management) are blocked when authenticated
* with Hamster and using API storage, since Hamster manages these features remotely.
*
* @param commandName - Name of the command to check
* @param storageType - Current storage type being used
* @returns Guard result with blocking decision and context
*
* @example
* ```ts
* const result = await tmCore.auth.guardCommand('add-dependency', tmCore.tasks.getStorageType());
* if (result.isBlocked) {
* console.log(`Command blocked: ${result.briefName}`);
* }
* ```
*/
async guardCommand(
commandName: string,
storageType: StorageType
): Promise<AuthBlockResult> {
const hasValidSession = await this.hasValidSession();
const context = this.getContext();
return checkAuthBlock({
hasValidSession,
briefName: context?.briefName,
storageType,
commandName
});
}
/**
* Get web app base URL from environment configuration
* @private

View File

@@ -0,0 +1,77 @@
/**
* @fileoverview Command guard - Core logic for blocking local-only commands
* Pure business logic - no presentation layer concerns
*/
import type { StorageType } from '../../common/types/index.js';
import { LOCAL_ONLY_COMMANDS, type LocalOnlyCommand } from './constants.js';
/**
* Result from checking if a command should be blocked
*/
export interface AuthBlockResult {
/** Whether the command should be blocked */
isBlocked: boolean;
/** Brief name if authenticated with Hamster */
briefName?: string;
/** Command name that was checked */
commandName: string;
}
/**
* Check if a command is local-only
*/
export function isLocalOnlyCommand(
commandName: string
): commandName is LocalOnlyCommand {
return LOCAL_ONLY_COMMANDS.includes(commandName as LocalOnlyCommand);
}
/**
* Parameters for auth block check
*/
export interface AuthBlockParams {
/** Whether user has a valid auth session */
hasValidSession: boolean;
/** Brief name from auth context */
briefName?: string;
/** Current storage type being used */
storageType: StorageType;
/** Command name to check */
commandName: string;
}
/**
* Check if a command should be blocked because user is authenticated with Hamster
*
* This is pure business logic with dependency injection - returns data only, no display/formatting
* Presentation layers (CLI, MCP) should format the response appropriately
*
* @param params - Auth block parameters
* @returns AuthBlockResult with blocking decision and context
*/
export function checkAuthBlock(params: AuthBlockParams): AuthBlockResult {
const { hasValidSession, briefName, storageType, commandName } = params;
// Only check auth for local-only commands
if (!isLocalOnlyCommand(commandName)) {
return { isBlocked: false, commandName };
}
// Not authenticated - command is allowed
if (!hasValidSession) {
return { isBlocked: false, commandName };
}
// Authenticated but using file storage - command is allowed
if (storageType !== 'api') {
return { isBlocked: false, commandName };
}
// User is authenticated AND using API storage - block the command
return {
isBlocked: true,
briefName: briefName || 'remote brief',
commandName
};
}

View File

@@ -0,0 +1,18 @@
/**
* @fileoverview Auth module constants
*/
/**
* Commands that are only available for local file storage
* These commands are blocked when using Hamster (API storage)
*/
export const LOCAL_ONLY_COMMANDS = [
'add-dependency',
'remove-dependency',
'validate-dependencies',
'fix-dependencies',
'clear-subtasks',
'models'
] as const;
export type LocalOnlyCommand = (typeof LOCAL_ONLY_COMMANDS)[number];

View File

@@ -26,3 +26,9 @@ export {
DEFAULT_AUTH_CONFIG,
getAuthConfig
} from './config.js';
// Command guard types and utilities
export { isLocalOnlyCommand, type AuthBlockResult } from './command.guard.js';
// Auth constants
export { LOCAL_ONLY_COMMANDS, type LocalOnlyCommand } from './constants.js';