diff --git a/src/cli.js b/src/cli.js index 4926ecb..d0f26d8 100644 --- a/src/cli.js +++ b/src/cli.js @@ -54,22 +54,24 @@ function parseArgs(argv) { }; for (let i = 0; i < argv.length; i++) { const a = argv[i]; - if (a === '--scene') { - const value = argv[++i]; + if (a === '--scene' || a.startsWith('--scene=')) { + const inline = a.startsWith('--scene='); + const value = inline ? a.slice('--scene='.length) : argv[++i]; if (!value || value.startsWith('-')) { opts.errors.push('--scene requires a scene name'); - if (value && value.startsWith('-')) i--; + if (!inline && value && value.startsWith('-')) i--; } else { const scenes = value.split(',').filter(Boolean); if (scenes.length) opts.scenes.push(...scenes); else opts.errors.push('--scene requires a scene name'); } } - else if (a === '--config') { - const value = argv[++i]; + else if (a === '--config' || a.startsWith('--config=')) { + const inline = a.startsWith('--config='); + const value = inline ? a.slice('--config='.length) : argv[++i]; if (!value || value.startsWith('-')) { opts.errors.push('--config requires a config path'); - if (value && value.startsWith('-')) i--; + if (!inline && value && value.startsWith('-')) i--; } else { opts.config = value; } diff --git a/test/cli.test.js b/test/cli.test.js index 856e520..a7795d8 100644 --- a/test/cli.test.js +++ b/test/cli.test.js @@ -1,8 +1,11 @@ const fs = require('fs'); const os = require('os'); const path = require('path'); +const { spawnSync } = require('child_process'); const { parseArgs, resolveConfigPath, USAGE } = require('../src/cli'); +const BIN = path.resolve(__dirname, '..', 'bin', 'shotkit.js'); + describe('parseArgs', () => { test('defaults', () => { expect(parseArgs([])).toMatchObject({ scenes: [], errors: [], json: false, noVideo: false, help: false, path: null }); @@ -17,11 +20,14 @@ describe('parseArgs', () => { test('--scene accepts comma lists and repeats', () => { expect(parseArgs(['--scene', 'a,b', '--scene', 'c']).scenes).toEqual(['a', 'b', 'c']); + expect(parseArgs(['--scene=a,b']).scenes).toEqual(['a', 'b']); }); test('usage errors are explicit for missing values and unknown options', () => { expect(parseArgs(['--scene']).errors).toEqual(['--scene requires a scene name']); + expect(parseArgs(['--scene=']).errors).toEqual(['--scene requires a scene name']); expect(parseArgs(['--config', '--json']).errors).toEqual(['--config requires a config path']); + expect(parseArgs(['--config=']).errors).toEqual(['--config requires a config path']); expect(parseArgs(['--wat']).errors).toEqual(['unknown option: --wat']); }); @@ -29,6 +35,10 @@ describe('parseArgs', () => { const o = parseArgs(['--config', 'x.js', 'repo']); expect(o.config).toBe('x.js'); expect(o.path).toBe('repo'); + + const inline = parseArgs(['--config=x.js', 'repo']); + expect(inline.config).toBe('x.js'); + expect(inline.path).toBe('repo'); }); test('only one positional path is accepted', () => { @@ -43,6 +53,45 @@ describe('parseArgs', () => { }); }); +describe('shotkit CLI usage errors', () => { + function run(args) { + return spawnSync(process.execPath, [BIN, ...args], { + cwd: os.tmpdir(), + encoding: 'utf8', + }); + } + + test('unknown flags and flag typos fail before config resolution', () => { + const res = run(['--no-buid']); + + expect(res.status).toBe(2); + expect(res.stdout).toBe(''); + expect(res.stderr).toContain('unknown option: --no-buid'); + expect(res.stderr).toContain('Usage: shotkit'); + }); + + test('missing option values fail with usage code 2', () => { + const res = run(['--scene']); + + expect(res.status).toBe(2); + expect(res.stdout).toBe(''); + expect(res.stderr).toContain('--scene requires a scene name'); + expect(res.stderr).toContain('Usage: shotkit'); + }); + + test('--json usage errors keep stdout empty and write JSON to stderr', () => { + const res = run(['--config', '--json']); + + expect(res.status).toBe(2); + expect(res.stdout).toBe(''); + expect(JSON.parse(res.stderr)).toEqual({ + ok: false, + error: '--config requires a config path', + code: 2, + }); + }); +}); + describe('resolveConfigPath', () => { test('prefers shotkit.config.js, falls back to store.config.js, else null', () => { const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'sk-cli-'));