@skhema/cli 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -31,7 +31,7 @@ SKHEMA_API_KEY=sk_live_… npx @skhema/cli init --yes --json
31
31
  future commands (never overwriting a different stored key without `--force`).
32
32
  - **Installs skills** into every detected agent platform (idempotent — reruns
33
33
  report `unchanged`).
34
- - **Registers the MCP server** (`https://mcp-server.skhema.com/mcp`, Streamable
34
+ - **Registers the MCP server** (`https://mcp.skhema.com/mcp`, Streamable
35
35
  HTTP) in detected clients by safely merging a dedicated config file and writing
36
36
  a `.bak` backup — **or printing copy-paste instructions** when a client's config
37
37
  isn't cleanly writable. `--yes` registers non-interactively; without it, `init`
package/dist/cli.js CHANGED
@@ -1,8 +1,9 @@
1
+ import { CommanderError } from 'commander';
1
2
  import { createRequire } from 'module';
2
3
  import { isApprovedContributor } from './lib/auth/entitlements.js';
3
4
  import { getStoredCredentials } from './lib/auth/token-store.js';
4
5
  import { outputError } from './lib/output.js';
5
- import { buildProgram } from './program.js';
6
+ import { buildProgram, resolveCommanderExitCode } from './program.js';
6
7
  const require = createRequire(import.meta.url);
7
8
  const pkg = require('../package.json');
8
9
  const program = buildProgram(pkg.version);
@@ -35,6 +36,17 @@ async function main() {
35
36
  }
36
37
  }
37
38
  }
38
- await program.parseAsync();
39
+ try {
40
+ await program.parseAsync();
41
+ }
42
+ catch (err) {
43
+ // With exitOverride set, commander throws instead of exiting for every
44
+ // parser-level outcome (usage error, --help, --version). Map those onto the
45
+ // ExitCode contract; anything else is a real failure and should propagate.
46
+ if (err instanceof CommanderError) {
47
+ process.exit(resolveCommanderExitCode(err));
48
+ }
49
+ throw err;
50
+ }
39
51
  }
40
52
  main();
@@ -1,7 +1,8 @@
1
1
  import { Command } from 'commander';
2
2
  /**
3
- * Element commands — the strategic building blocks in a workspace. All are
4
- * workspace-scoped via `--workspace` (falling back to the default workspace).
3
+ * Element commands — the strategic building blocks in a workspace. Most are
4
+ * workspace-scoped via `--workspace` (falling back to the default workspace);
5
+ * `types` is a local, network-free lookup of the method vocabulary.
5
6
  */
6
7
  export declare function registerElementCommands(program: Command): void;
7
8
  //# sourceMappingURL=element.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"element.d.ts","sourceRoot":"","sources":["../../src/commands/element.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AAkBnC;;;GAGG;AACH,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CA8S9D"}
1
+ {"version":3,"file":"element.d.ts","sourceRoot":"","sources":["../../src/commands/element.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AA2EnC;;;;GAIG;AACH,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CA2T9D"}
@@ -1,3 +1,4 @@
1
+ import { getElementDefinition, getElementsForComponent, SKHEMA_MAPPING, } from '@skhema/method';
1
2
  import chalk from 'chalk';
2
3
  import { resolveClient } from '../lib/api/client.js';
3
4
  import { asRecord, buildBody, coerceList, readPayloadFile, resolveWorkspaceId, } from '../lib/api/command-helpers.js';
@@ -8,9 +9,47 @@ function summarize(content) {
8
9
  const text = typeof content === 'string' ? content : '';
9
10
  return text.length > 48 ? `${text.slice(0, 47)}…` : text || '—';
10
11
  }
12
+ /** Human-friendly rendering of an expected mood (e.g. `noun_phrase` → `noun phrase`). */
13
+ function formatMood(mood) {
14
+ return mood ? mood.replace(/_/g, ' ') : '—';
15
+ }
16
+ /**
17
+ * The component → allowed-element-type mapping, read straight from
18
+ * `@skhema/method` (SKHEMA_MAPPING) in canonical component-flow order. This is
19
+ * the same vocabulary the backend enforces, so agents can discover the
20
+ * allow-list locally instead of probing it via `bad_request` errors.
21
+ */
22
+ function buildElementTypeMapping() {
23
+ return SKHEMA_MAPPING.componentFlow.map((component) => ({
24
+ component: component.value,
25
+ label: component.label,
26
+ acronym: component.acronym,
27
+ elementTypes: getElementsForComponent(component.value).map((element) => ({
28
+ type: element.value,
29
+ label: element.label,
30
+ acronym: element.acronym,
31
+ mood: getElementDefinition(element.value)?.expectedMood ?? null,
32
+ })),
33
+ }));
34
+ }
35
+ /** Render the mapping as a per-component section with an element-type table. */
36
+ function renderElementTypeMapping(mapping) {
37
+ logHeader('Element types by component');
38
+ for (const component of mapping) {
39
+ log('');
40
+ log(chalk.bold(`${component.label} ${chalk.dim(`(${component.acronym})`)}`));
41
+ logTable(['acronym', 'type', 'label', 'mood'], component.elementTypes.map((element) => [
42
+ element.acronym,
43
+ element.type,
44
+ element.label,
45
+ formatMood(element.mood),
46
+ ]));
47
+ }
48
+ }
11
49
  /**
12
- * Element commands — the strategic building blocks in a workspace. All are
13
- * workspace-scoped via `--workspace` (falling back to the default workspace).
50
+ * Element commands — the strategic building blocks in a workspace. Most are
51
+ * workspace-scoped via `--workspace` (falling back to the default workspace);
52
+ * `types` is a local, network-free lookup of the method vocabulary.
14
53
  */
15
54
  export function registerElementCommands(program) {
16
55
  const element = program.command('element').description('Element commands');
@@ -200,4 +239,15 @@ export function registerElementCommands(program) {
200
239
  return data;
201
240
  });
202
241
  });
242
+ element
243
+ .command('types')
244
+ .description('List element types allowed in each component (method vocabulary, no network)')
245
+ .action(async () => {
246
+ await runCommand('element types', async () => {
247
+ const mapping = buildElementTypeMapping();
248
+ if (!getGlobalOptions().json)
249
+ renderElementTypeMapping(mapping);
250
+ return { components: mapping };
251
+ });
252
+ });
203
253
  }
@@ -1 +1 @@
1
- {"version":3,"file":"skills.d.ts","sourceRoot":"","sources":["../../src/commands/skills.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AAenC,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAgO7D"}
1
+ {"version":3,"file":"skills.d.ts","sourceRoot":"","sources":["../../src/commands/skills.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AAwCnC,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CA6U7D"}
@@ -1,10 +1,28 @@
1
+ import readline from 'readline';
1
2
  import chalk from 'chalk';
2
3
  import ora from 'ora';
3
4
  import { getGlobalOptions, log, logHeader, logTable, outputError, outputSuccess, } from '../lib/output.js';
4
5
  import { listBundledSkills, readBundledSkill } from '../lib/skills/bundled.js';
5
- import { installSkills, updateSkills } from '../lib/skills/installer.js';
6
+ import { installSkills, pruneSkills, updateSkills, } from '../lib/skills/installer.js';
6
7
  import { readLock } from '../lib/skills/lock.js';
7
8
  import { detectPlatforms } from '../lib/skills/platform-detect.js';
9
+ function interactiveTerminal() {
10
+ return (Boolean(process.stdin.isTTY && process.stdout.isTTY) &&
11
+ !getGlobalOptions().json);
12
+ }
13
+ async function confirm(question) {
14
+ const rl = readline.createInterface({
15
+ input: process.stdin,
16
+ output: process.stdout,
17
+ });
18
+ const answer = await new Promise((resolve) => {
19
+ rl.question(`${question} [y/N]: `, (value) => {
20
+ rl.close();
21
+ resolve(value.trim().toLowerCase());
22
+ });
23
+ });
24
+ return answer === 'y' || answer === 'yes';
25
+ }
8
26
  export function registerSkillsCommands(program) {
9
27
  const skills = program.command('skills').description('AI skills management');
10
28
  skills
@@ -171,4 +189,83 @@ export function registerSkillsCommands(program) {
171
189
  process.exit(1);
172
190
  }
173
191
  });
192
+ skills
193
+ .command('prune')
194
+ .description('Remove installed skills that are no longer bundled with this CLI (retired or renamed)')
195
+ .option('--platform <id>', 'Prune a specific platform only')
196
+ .option('--yes', 'Actually remove; without it, only preview what would go')
197
+ .action(async (options) => {
198
+ const opts = getGlobalOptions();
199
+ const spinner = opts.json || opts.quiet
200
+ ? null
201
+ : ora('Detecting agent platforms...').start();
202
+ try {
203
+ const platforms = detectPlatforms().filter((p) => p.detected);
204
+ const targets = options.platform
205
+ ? platforms.filter((p) => p.id === options.platform)
206
+ : platforms;
207
+ if (targets.length === 0) {
208
+ spinner?.warn(options.platform
209
+ ? `Platform "${options.platform}" not detected.`
210
+ : 'No agent platforms detected.');
211
+ if (opts.json)
212
+ outputSuccess([], 'skills prune');
213
+ return;
214
+ }
215
+ // Preview first — this never writes.
216
+ const preview = await pruneSkills(targets, { apply: false });
217
+ if (preview.length === 0) {
218
+ spinner?.succeed('No retired skills to prune. Everything is current.');
219
+ if (opts.json)
220
+ outputSuccess([], 'skills prune');
221
+ return;
222
+ }
223
+ spinner?.info(`Found ${preview.length} retired skill install(s) across ${new Set(preview.map((r) => r.platform.id)).size} platform(s).`);
224
+ // Mutate only with --yes, or after an interactive confirmation.
225
+ let apply = Boolean(options.yes);
226
+ if (!apply) {
227
+ if (interactiveTerminal()) {
228
+ logHeader('Would remove');
229
+ logTable(['Platform', 'Skill', 'Path'], preview.map((r) => [
230
+ r.platform.name,
231
+ r.skill,
232
+ chalk.dim(r.targetPath),
233
+ ]));
234
+ apply = await confirm('Remove these skills?');
235
+ }
236
+ }
237
+ if (!apply) {
238
+ const results = preview;
239
+ if (opts.json) {
240
+ outputSuccess(results, 'skills prune');
241
+ return;
242
+ }
243
+ logHeader('Prune Preview (dry run — pass --yes to remove)');
244
+ logTable(['Platform', 'Skill', 'Path'], results.map((r) => [
245
+ r.platform.name,
246
+ r.skill,
247
+ chalk.dim(r.targetPath),
248
+ ]));
249
+ log('');
250
+ log(chalk.dim('Re-run with --yes to remove these skills.'));
251
+ return;
252
+ }
253
+ const results = await pruneSkills(targets, { apply: true });
254
+ if (opts.json) {
255
+ outputSuccess(results, 'skills prune');
256
+ return;
257
+ }
258
+ logHeader('Prune Results');
259
+ logTable(['Platform', 'Skill', 'Status'], results.map((r) => [
260
+ r.platform.name,
261
+ r.skill,
262
+ chalk.red(r.action),
263
+ ]));
264
+ }
265
+ catch (error) {
266
+ spinner?.fail('Prune failed');
267
+ outputError(error instanceof Error ? error.message : 'Unknown error', 'skills prune');
268
+ process.exit(1);
269
+ }
270
+ });
174
271
  }
@@ -1 +1 @@
1
- {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/lib/config.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,aAAa,sBAAsB,CAAA;AAChD,eAAO,MAAM,aAAa,QACgC,CAAA;AAC1D,eAAO,MAAM,YAAY,yBAAyB,CAAA;AAClD,eAAO,MAAM,oBAAoB,IAAI,CAAA;AACrC,eAAO,MAAM,kBAAkB,MAAM,CAAA;AACrC,eAAO,MAAM,aAAa,SAAU,CAAA;AACpC,eAAO,MAAM,gBAAgB,mBAAmB,CAAA;AAChD,eAAO,MAAM,gBAAgB,YAAY,CAAA;AAGzC,eAAO,MAAM,wBAAwB,YAAY,CAAA;AACjD,eAAO,MAAM,eAAe,YAAY,CAAA;AACxC,eAAO,MAAM,gBAAgB,qBAAqB,CAAA;AAClD,eAAO,MAAM,YAAY,iBAAiB,CAAA;AAC1C,eAAO,MAAM,WAAW,gBAAgB,CAAA;AACxC,eAAO,MAAM,eAAe,4BAA4B,CAAA;AAGxD,eAAO,MAAM,YAAY,QAC+B,CAAA;AAGxD,eAAO,MAAM,eAAe,mBAAmB,CAAA;AAO/C,eAAO,MAAM,cAAc,QACwC,CAAA;AAMnE,eAAO,MAAM,YAAY,QACsD,CAAA;AAC/E,eAAO,MAAM,wBAAwB,QAEa,CAAA"}
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/lib/config.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,aAAa,sBAAsB,CAAA;AAChD,eAAO,MAAM,aAAa,QACgC,CAAA;AAC1D,eAAO,MAAM,YAAY,yBAAyB,CAAA;AAClD,eAAO,MAAM,oBAAoB,IAAI,CAAA;AACrC,eAAO,MAAM,kBAAkB,MAAM,CAAA;AACrC,eAAO,MAAM,aAAa,SAAU,CAAA;AACpC,eAAO,MAAM,gBAAgB,mBAAmB,CAAA;AAChD,eAAO,MAAM,gBAAgB,YAAY,CAAA;AAGzC,eAAO,MAAM,wBAAwB,YAAY,CAAA;AACjD,eAAO,MAAM,eAAe,YAAY,CAAA;AACxC,eAAO,MAAM,gBAAgB,qBAAqB,CAAA;AAClD,eAAO,MAAM,YAAY,iBAAiB,CAAA;AAC1C,eAAO,MAAM,WAAW,gBAAgB,CAAA;AACxC,eAAO,MAAM,eAAe,4BAA4B,CAAA;AAGxD,eAAO,MAAM,YAAY,QAC+B,CAAA;AAGxD,eAAO,MAAM,eAAe,mBAAmB,CAAA;AAQ/C,eAAO,MAAM,cAAc,QACiC,CAAA;AAM5D,eAAO,MAAM,YAAY,QACsD,CAAA;AAC/E,eAAO,MAAM,wBAAwB,QAEa,CAAA"}
@@ -19,11 +19,12 @@ export const API_BASE_URL = process.env.SKHEMA_API_URL ?? 'https://api.skhema.co
19
19
  // Environment variable an unattended CI/agent run sets to supply an API key.
20
20
  export const API_KEY_ENV_VAR = 'SKHEMA_API_KEY';
21
21
  // Public Skhema MCP server endpoint (Streamable HTTP transport). This is the
22
- // protocol endpoint AI clients connect to — distinct from mcp.skhema.com (the
23
- // management dashboard). Auth is per-client browser OAuth on first use, so the
24
- // CLI only registers the URL; it never injects a credential here.
22
+ // protocol endpoint AI clients connect to — mcp.skhema.com IS the MCP server.
23
+ // (The human-facing management UI now lives at app.skhema.com/mcp.) Auth is
24
+ // per-client browser OAuth on first use, so the CLI only registers the URL; it
25
+ // never injects a credential here.
25
26
  // Override via env for staging/preview environments.
26
- export const MCP_SERVER_URL = process.env.SKHEMA_MCP_URL ?? 'https://mcp-server.skhema.com/mcp';
27
+ export const MCP_SERVER_URL = process.env.SKHEMA_MCP_URL ?? 'https://mcp.skhema.com/mcp';
27
28
  // Supabase project configuration.
28
29
  // Publishable keys are safe to embed in client-side code (they are the new
29
30
  // replacement for the legacy anon key and rely on RLS + server-side auth
@@ -1,4 +1,17 @@
1
- import type { DetectedPlatform, InstallResult } from './types.js';
1
+ import type { DetectedPlatform, InstallResult, PruneResult } from './types.js';
2
2
  export declare function installSkills(platforms: DetectedPlatform[]): Promise<InstallResult[]>;
3
3
  export declare function updateSkills(platforms: DetectedPlatform[]): Promise<InstallResult[]>;
4
+ /**
5
+ * Find skills we previously installed that are no longer bundled with this CLI
6
+ * (retired or renamed) and remove them from each platform.
7
+ *
8
+ * Attribution is anchored on the per-platform lock file: only skills recorded
9
+ * there by our own installer are candidates, so a skill authored by the user or
10
+ * another tool that happens to share the skills directory is never touched. With
11
+ * `apply: false` (the default), nothing is written — the returned list is a
12
+ * preview of what removal would do.
13
+ */
14
+ export declare function pruneSkills(platforms: DetectedPlatform[], options?: {
15
+ apply: boolean;
16
+ }): Promise<PruneResult[]>;
4
17
  //# sourceMappingURL=installer.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"installer.d.ts","sourceRoot":"","sources":["../../../src/lib/skills/installer.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,gBAAgB,EAAE,aAAa,EAAE,MAAM,YAAY,CAAA;AA+DjE,wBAAsB,aAAa,CACjC,SAAS,EAAE,gBAAgB,EAAE,GAC5B,OAAO,CAAC,aAAa,EAAE,CAAC,CA0C1B;AAED,wBAAsB,YAAY,CAChC,SAAS,EAAE,gBAAgB,EAAE,GAC5B,OAAO,CAAC,aAAa,EAAE,CAAC,CAG1B"}
1
+ {"version":3,"file":"installer.d.ts","sourceRoot":"","sources":["../../../src/lib/skills/installer.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,gBAAgB,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,YAAY,CAAA;AA+D9E,wBAAsB,aAAa,CACjC,SAAS,EAAE,gBAAgB,EAAE,GAC5B,OAAO,CAAC,aAAa,EAAE,CAAC,CA0C1B;AAED,wBAAsB,YAAY,CAChC,SAAS,EAAE,gBAAgB,EAAE,GAC5B,OAAO,CAAC,aAAa,EAAE,CAAC,CAG1B;AAED;;;;;;;;;GASG;AACH,wBAAsB,WAAW,CAC/B,SAAS,EAAE,gBAAgB,EAAE,EAC7B,OAAO,GAAE;IAAE,KAAK,EAAE,OAAO,CAAA;CAAqB,GAC7C,OAAO,CAAC,WAAW,EAAE,CAAC,CAmCxB"}
@@ -83,3 +83,43 @@ export async function updateSkills(platforms) {
83
83
  // Update uses the same logic as install — content comparison handles skip/update
84
84
  return installSkills(platforms);
85
85
  }
86
+ /**
87
+ * Find skills we previously installed that are no longer bundled with this CLI
88
+ * (retired or renamed) and remove them from each platform.
89
+ *
90
+ * Attribution is anchored on the per-platform lock file: only skills recorded
91
+ * there by our own installer are candidates, so a skill authored by the user or
92
+ * another tool that happens to share the skills directory is never touched. With
93
+ * `apply: false` (the default), nothing is written — the returned list is a
94
+ * preview of what removal would do.
95
+ */
96
+ export async function pruneSkills(platforms, options = { apply: false }) {
97
+ const bundledNames = new Set(getBundledSkills().map((skill) => skill.name));
98
+ const results = [];
99
+ for (const platform of platforms) {
100
+ const lock = readLock(platform);
101
+ const retired = Object.keys(lock.skills)
102
+ .filter((name) => !bundledNames.has(name))
103
+ .sort((a, b) => a.localeCompare(b));
104
+ for (const name of retired) {
105
+ const targetPath = path.join(platform.skillsDir, name);
106
+ if (options.apply) {
107
+ // Remove only the directory our lock attributes to this skill.
108
+ if (fs.existsSync(targetPath)) {
109
+ fs.rmSync(targetPath, { recursive: true, force: true });
110
+ }
111
+ delete lock.skills[name];
112
+ }
113
+ results.push({
114
+ platform,
115
+ skill: name,
116
+ action: options.apply ? 'removed' : 'would-remove',
117
+ targetPath,
118
+ });
119
+ }
120
+ if (options.apply && retired.length > 0) {
121
+ writeLock(platform, lock);
122
+ }
123
+ }
124
+ return results;
125
+ }
@@ -17,6 +17,12 @@ export interface InstallResult {
17
17
  action: 'installed' | 'updated' | 'skipped';
18
18
  targetPath: string;
19
19
  }
20
+ export interface PruneResult {
21
+ platform: DetectedPlatform;
22
+ skill: string;
23
+ action: 'removed' | 'would-remove';
24
+ targetPath: string;
25
+ }
20
26
  export interface SkillLock {
21
27
  version: string;
22
28
  installedAt: string;
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/lib/skills/types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,gBAAgB;IAC/B,EAAE,EAAE,MAAM,CAAA;IACV,IAAI,EAAE,MAAM,CAAA;IACZ,SAAS,EAAE,MAAM,CAAA;IACjB,SAAS,EAAE,MAAM,CAAA;IACjB,QAAQ,EAAE,OAAO,CAAA;CAClB;AAED,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAA;IACZ,WAAW,EAAE,MAAM,CAAA;IACnB,OAAO,EAAE,MAAM,CAAA;IACf,KAAK,EAAE,MAAM,EAAE,CAAA;CAChB;AAED,MAAM,WAAW,aAAa;IAC5B,QAAQ,EAAE,gBAAgB,CAAA;IAC1B,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,EAAE,WAAW,GAAG,SAAS,GAAG,SAAS,CAAA;IAC3C,UAAU,EAAE,MAAM,CAAA;CACnB;AAED,MAAM,WAAW,SAAS;IACxB,OAAO,EAAE,MAAM,CAAA;IACf,WAAW,EAAE,MAAM,CAAA;IACnB,MAAM,EAAE,MAAM,CACZ,MAAM,EACN;QACE,OAAO,EAAE,MAAM,CAAA;QACf,QAAQ,EAAE,MAAM,CAAA;QAChB,WAAW,EAAE,MAAM,CAAA;KACpB,CACF,CAAA;CACF"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/lib/skills/types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,gBAAgB;IAC/B,EAAE,EAAE,MAAM,CAAA;IACV,IAAI,EAAE,MAAM,CAAA;IACZ,SAAS,EAAE,MAAM,CAAA;IACjB,SAAS,EAAE,MAAM,CAAA;IACjB,QAAQ,EAAE,OAAO,CAAA;CAClB;AAED,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAA;IACZ,WAAW,EAAE,MAAM,CAAA;IACnB,OAAO,EAAE,MAAM,CAAA;IACf,KAAK,EAAE,MAAM,EAAE,CAAA;CAChB;AAED,MAAM,WAAW,aAAa;IAC5B,QAAQ,EAAE,gBAAgB,CAAA;IAC1B,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,EAAE,WAAW,GAAG,SAAS,GAAG,SAAS,CAAA;IAC3C,UAAU,EAAE,MAAM,CAAA;CACnB;AAED,MAAM,WAAW,WAAW;IAC1B,QAAQ,EAAE,gBAAgB,CAAA;IAC1B,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,EAAE,SAAS,GAAG,cAAc,CAAA;IAClC,UAAU,EAAE,MAAM,CAAA;CACnB;AAED,MAAM,WAAW,SAAS;IACxB,OAAO,EAAE,MAAM,CAAA;IACf,WAAW,EAAE,MAAM,CAAA;IACnB,MAAM,EAAE,MAAM,CACZ,MAAM,EACN;QACE,OAAO,EAAE,MAAM,CAAA;QACf,QAAQ,EAAE,MAAM,CAAA;QAChB,WAAW,EAAE,MAAM,CAAA;KACpB,CACF,CAAA;CACF"}
package/dist/program.d.ts CHANGED
@@ -1,4 +1,16 @@
1
- import { Command } from 'commander';
1
+ import { Command, CommanderError } from 'commander';
2
+ /**
3
+ * Resolve the process exit code for a parser-level {@link CommanderError}.
4
+ *
5
+ * Commander's own defaults are inconsistent (unknown option → 1, unknown
6
+ * command silently swallowed → 0, missing subcommand → 1). We collapse them
7
+ * onto the CLI's stable {@link ExitCode} contract: printing help or the version
8
+ * is an informational success (`0`), and every other parser failure — unknown
9
+ * option, unknown/missing command, missing or excess argument — is a usage
10
+ * error ({@link ExitCode.Usage}). Commander tags the informational cases with
11
+ * `exitCode === 0`, which is the only discriminator we need.
12
+ */
13
+ export declare function resolveCommanderExitCode(err: CommanderError): number;
2
14
  /**
3
15
  * Assemble the full skhema CLI program. The runtime entry (cli.ts) and the
4
16
  * docs reference generator both build from here, so every registered command
@@ -1 +1 @@
1
- {"version":3,"file":"program.d.ts","sourceRoot":"","sources":["../src/program.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AAqBnC;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAiDrD"}
1
+ {"version":3,"file":"program.d.ts","sourceRoot":"","sources":["../src/program.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,cAAc,EAAE,MAAM,WAAW,CAAA;AAsBnD;;;;;;;;;;GAUG;AACH,wBAAgB,wBAAwB,CAAC,GAAG,EAAE,cAAc,GAAG,MAAM,CAEpE;AAED;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAqErD"}
package/dist/program.js CHANGED
@@ -16,8 +16,23 @@ import { registerStrategyCommands } from './commands/strategy.js';
16
16
  import { registerWebhookCommands } from './commands/webhook.js';
17
17
  import { registerWorkspaceCommands } from './commands/workspace.js';
18
18
  import { setApiKeyFlag } from './lib/api/credential-context.js';
19
+ import { ExitCode } from './lib/api/run-command.js';
19
20
  import { showBanner } from './lib/banner.js';
20
21
  import { setGlobalOptions } from './lib/output.js';
22
+ /**
23
+ * Resolve the process exit code for a parser-level {@link CommanderError}.
24
+ *
25
+ * Commander's own defaults are inconsistent (unknown option → 1, unknown
26
+ * command silently swallowed → 0, missing subcommand → 1). We collapse them
27
+ * onto the CLI's stable {@link ExitCode} contract: printing help or the version
28
+ * is an informational success (`0`), and every other parser failure — unknown
29
+ * option, unknown/missing command, missing or excess argument — is a usage
30
+ * error ({@link ExitCode.Usage}). Commander tags the informational cases with
31
+ * `exitCode === 0`, which is the only discriminator we need.
32
+ */
33
+ export function resolveCommanderExitCode(err) {
34
+ return err.exitCode === 0 ? ExitCode.Ok : ExitCode.Usage;
35
+ }
21
36
  /**
22
37
  * Assemble the full skhema CLI program. The runtime entry (cli.ts) and the
23
38
  * docs reference generator both build from here, so every registered command
@@ -25,6 +40,15 @@ import { setGlobalOptions } from './lib/output.js';
25
40
  */
26
41
  export function buildProgram(version) {
27
42
  const program = new Command();
43
+ // Throw a CommanderError instead of calling process.exit directly, so the
44
+ // entry point can map every parser-level failure onto the ExitCode taxonomy.
45
+ // Set before registering subcommands: commander copies the exit callback into
46
+ // each child at `.command()` time, so this one call covers the whole tree.
47
+ program.exitOverride();
48
+ // The default `.action()` below (banner on bare invocation) otherwise
49
+ // suppresses commander's implicit `help` command, so `skhema help [command]`
50
+ // would fall through to the unknown-command guard. Re-enable it explicitly.
51
+ program.helpCommand(true);
28
52
  program
29
53
  .name('skhema')
30
54
  .description('Skhema CLI — drive the Skhema Public API from your terminal, CI, or agent')
@@ -43,7 +67,16 @@ export function buildProgram(version) {
43
67
  setApiKeyFlag(opts.apiKey);
44
68
  })
45
69
  .action(() => {
46
- // Show banner when run with no subcommand
70
+ // A bare `skhema` invocation shows the banner. Any leftover operand means
71
+ // an unknown command was given: commander routes it here (the default
72
+ // action) instead of erroring, so we must reject it ourselves — otherwise
73
+ // the banner prints and the process exits 0, hiding the mistake.
74
+ if (program.args.length > 0) {
75
+ program.error(`unknown command '${program.args[0]}'`, {
76
+ code: 'commander.unknownCommand',
77
+ exitCode: ExitCode.Usage,
78
+ });
79
+ }
47
80
  showBanner(version);
48
81
  program.help();
49
82
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skhema/cli",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "Skhema CLI - Authentication and AI skills management for agent platforms",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -42,7 +42,7 @@
42
42
  },
43
43
  "dependencies": {
44
44
  "@skhema/agent-sdk": "0.1.4",
45
- "@skhema/method": "0.3.0",
45
+ "@skhema/method": "0.4.0",
46
46
  "@skhema/sdk": "0.2.4",
47
47
  "chalk": "^5.3.0",
48
48
  "commander": "^12.0.0",