Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 8 additions & 6 deletions src/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
49 changes: 49 additions & 0 deletions test/cli.test.js
Original file line number Diff line number Diff line change
@@ -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 });
Expand All @@ -17,18 +20,25 @@ 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']);
});

test('--config consumes its value (not mistaken for the positional)', () => {
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', () => {
Expand All @@ -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-'));
Expand Down