Skip to content

Commit 0cd0631

Browse files
committed
fix: parsing of flags with = signs
1 parent b924bb4 commit 0cd0631

5 files changed

Lines changed: 124 additions & 105 deletions

File tree

.claude/settings.json

Lines changed: 0 additions & 15 deletions
This file was deleted.

.mcp.json

Lines changed: 0 additions & 10 deletions
This file was deleted.

CLAUDE.md

Lines changed: 0 additions & 72 deletions
This file was deleted.

packages/cli/src/cli.ts

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,29 @@ import { startVisualizationTool } from './visualization-launcher.js';
4848
import { generateConfig, GeneratorRegistry } from './config-generator.js';
4949
import { generateSkill, SkillGeneratorRegistry } from './skill-generator.js';
5050

51+
/**
52+
* Parse a named flag from an args array, supporting both space-separated and
53+
* equals-sign notation:
54+
* --flag value → returns "value"
55+
* --flag=value → returns "value"
56+
*
57+
* Returns `undefined` when the flag is not present.
58+
*/
59+
export function parseFlag(args: string[], flag: string): string | undefined {
60+
for (let i = 0; i < args.length; i++) {
61+
const arg = args[i];
62+
// --flag=value notation
63+
if (arg.startsWith(`${flag}=`)) {
64+
return arg.slice(flag.length + 1);
65+
}
66+
// --flag value notation
67+
if (arg === flag && i + 1 < args.length) {
68+
return args[i + 1];
69+
}
70+
}
71+
return undefined;
72+
}
73+
5174
/**
5275
* Parse command line arguments and handle CLI commands
5376
*/
@@ -70,9 +93,7 @@ async function parseCliArgs(): Promise<{ shouldExit: boolean }> {
7093
handleSetupList();
7194
return { shouldExit: true };
7295
} else if (subcommand) {
73-
// Parse --mode flag
74-
const modeIndex = args.findIndex(arg => arg === '--mode');
75-
const mode = modeIndex !== -1 ? args[modeIndex + 1] : 'skill';
96+
const mode = parseFlag(args, '--mode') ?? 'skill';
7697
if (mode !== 'skill' && mode !== 'config') {
7798
console.error('❌ Error: --mode must be "skill" or "config"');
7899
process.exit(1);
@@ -119,10 +140,7 @@ async function parseCliArgs(): Promise<{ shouldExit: boolean }> {
119140
handleCrowdList();
120141
return { shouldExit: true };
121142
} else if (subcommand === 'copy') {
122-
// Check for --output-dir flag
123-
const outputDirIndex = args.findIndex(arg => arg === '--output-dir');
124-
const outputDir =
125-
outputDirIndex !== -1 ? args[outputDirIndex + 1] : undefined;
143+
const outputDir = parseFlag(args, '--output-dir');
126144
handleCrowdCopy(outputDir);
127145
return { shouldExit: true };
128146
} else {

packages/cli/test/cli.test.ts

Lines changed: 99 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
2+
import { parseFlag } from '../src/cli.js';
23

34
// Use vi.hoisted for proper test isolation
45
const mocks = vi.hoisted(() => ({
@@ -22,6 +23,9 @@ const mocks = vi.hoisted(() => ({
2223
'opencode',
2324
'copilot',
2425
]),
26+
exists: vi.fn((name: string) =>
27+
['kiro', 'claude', 'gemini', 'opencode', 'copilot'].includes(name)
28+
),
2529
getHelpText: vi.fn(
2630
() =>
2731
` kiro Generate .amazonq/cli-agents/vibe.json (Kiro/Amazon Q)
@@ -31,6 +35,20 @@ const mocks = vi.hoisted(() => ({
3135
copilot Generate .vscode/mcp.json, .github/agents/Vibe.agent.md`
3236
),
3337
},
38+
SkillGeneratorRegistry: {
39+
getGeneratorNames: vi.fn(() => [
40+
'claude',
41+
'gemini',
42+
'opencode',
43+
'copilot',
44+
'kiro',
45+
]),
46+
exists: vi.fn((name: string) =>
47+
['claude', 'gemini', 'opencode', 'copilot', 'kiro'].includes(name)
48+
),
49+
getHelpText: vi.fn(() => ''),
50+
},
51+
generateSkill: vi.fn(),
3452
}));
3553

3654
vi.mock('@codemcp/workflows-core', () => ({
@@ -64,6 +82,11 @@ vi.mock('../src/config-generator.js', () => ({
6482
GeneratorRegistry: mocks.GeneratorRegistry,
6583
}));
6684

85+
vi.mock('../src/skill-generator.js', () => ({
86+
generateSkill: mocks.generateSkill,
87+
SkillGeneratorRegistry: mocks.SkillGeneratorRegistry,
88+
}));
89+
6790
describe('CLI', () => {
6891
let originalArgv: string[];
6992
let originalExit: typeof process.exit;
@@ -142,8 +165,9 @@ describe('CLI', () => {
142165
kill: vi.fn(),
143166
} as unknown);
144167

145-
// Mock generateConfig to prevent actual file generation
168+
// Mock generateConfig and generateSkill to prevent actual file generation
146169
mocks.generateConfig.mockResolvedValue(undefined);
170+
mocks.generateSkill.mockResolvedValue(undefined);
147171

148172
// Import from source
149173
const module = await import('../src/cli.js');
@@ -385,6 +409,48 @@ describe('CLI', () => {
385409
});
386410
});
387411

412+
describe('Setup Command --mode flag', () => {
413+
it('should call generateConfig when --mode config is passed (space notation)', async () => {
414+
process.argv = ['node', 'cli.js', 'setup', 'claude', '--mode', 'config'];
415+
416+
await runCli();
417+
418+
expect(mocks.generateConfig).toHaveBeenCalledWith('claude', '/mock/cwd');
419+
});
420+
421+
it('should call generateConfig when --mode=config is passed (equals notation)', async () => {
422+
process.argv = ['node', 'cli.js', 'setup', 'claude', '--mode=config'];
423+
424+
await runCli();
425+
426+
expect(mocks.generateConfig).toHaveBeenCalledWith('claude', '/mock/cwd');
427+
});
428+
429+
it('should show error for invalid --mode value', async () => {
430+
process.argv = ['node', 'cli.js', 'setup', 'claude', '--mode=invalid'];
431+
432+
processExitSpy.mockImplementation(() => undefined as never);
433+
434+
await runCli();
435+
436+
expect(consoleErrorSpy).toHaveBeenCalledWith(
437+
expect.stringContaining('--mode must be "skill" or "config"')
438+
);
439+
});
440+
441+
it('should show error for invalid --mode= value (equals notation)', async () => {
442+
process.argv = ['node', 'cli.js', 'setup', 'claude', '--mode='];
443+
444+
processExitSpy.mockImplementation(() => undefined as never);
445+
446+
await runCli();
447+
448+
expect(consoleErrorSpy).toHaveBeenCalledWith(
449+
expect.stringContaining('--mode must be "skill" or "config"')
450+
);
451+
});
452+
});
453+
388454
describe('Unknown Arguments', () => {
389455
it('should show error for unknown command', () => {
390456
process.argv = ['node', 'cli.js', '--unknown-flag'];
@@ -401,6 +467,38 @@ describe('CLI', () => {
401467
});
402468
});
403469

470+
describe('parseFlag helper', () => {
471+
it('returns value for --flag value notation', () => {
472+
expect(parseFlag(['--mode', 'config'], '--mode')).toBe('config');
473+
});
474+
475+
it('returns value for --flag=value notation', () => {
476+
expect(parseFlag(['--mode=config'], '--mode')).toBe('config');
477+
});
478+
479+
it('returns undefined when flag is absent', () => {
480+
expect(parseFlag(['setup', 'claude'], '--mode')).toBeUndefined();
481+
});
482+
483+
it('returns undefined when --flag appears last with no following value', () => {
484+
expect(parseFlag(['--mode'], '--mode')).toBeUndefined();
485+
});
486+
487+
it('handles empty string value from --flag= notation', () => {
488+
expect(parseFlag(['--flag='], '--flag')).toBe('');
489+
});
490+
491+
it('does not confuse prefix-matching flags (--mode vs --mode-extra)', () => {
492+
expect(parseFlag(['--mode-extra=config'], '--mode')).toBeUndefined();
493+
});
494+
495+
it('returns first match when flag appears multiple times', () => {
496+
expect(parseFlag(['--mode=skill', '--mode=config'], '--mode')).toBe(
497+
'skill'
498+
);
499+
});
500+
});
501+
404502
describe('Default Behavior', () => {
405503
it('should start visualization tool by default', () => {
406504
process.argv = ['node', 'cli.js'];

0 commit comments

Comments
 (0)