@skhema/cli 0.4.2 → 0.4.4

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/dist/cli.js CHANGED
@@ -1,63 +1,11 @@
1
- import { Command } from 'commander';
2
1
  import { createRequire } from 'module';
3
- import { registerAgentCommands } from './commands/agent.js';
4
- import { registerApiCommands } from './commands/api.js';
5
- import { registerAuthCommands } from './commands/auth.js';
6
- import { registerComponentCommands } from './commands/component.js';
7
- import { registerContributeCommands } from './commands/contribute.js';
8
- import { registerElementCommands } from './commands/element.js';
9
- import { registerWorkspaceExportCommand } from './commands/export.js';
10
- import { registerInitCommand } from './commands/init.js';
11
- import { registerLinkCommands } from './commands/link.js';
12
- import { registerResourceCommands } from './commands/resource.js';
13
- import { registerSkillsCommands } from './commands/skills.js';
14
- import { registerStrategyCommands } from './commands/strategy.js';
15
- import { registerWebhookCommands } from './commands/webhook.js';
16
- import { registerWorkspaceCommands } from './commands/workspace.js';
17
- import { setApiKeyFlag } from './lib/api/credential-context.js';
18
2
  import { isApprovedContributor } from './lib/auth/entitlements.js';
19
3
  import { getStoredCredentials } from './lib/auth/token-store.js';
20
- import { showBanner } from './lib/banner.js';
21
- import { outputError, setGlobalOptions } from './lib/output.js';
4
+ import { outputError } from './lib/output.js';
5
+ import { buildProgram } from './program.js';
22
6
  const require = createRequire(import.meta.url);
23
7
  const pkg = require('../package.json');
24
- const program = new Command();
25
- program
26
- .name('skhema')
27
- .description('Skhema CLI — drive the Skhema Public API from your terminal, CI, or agent')
28
- .version(pkg.version)
29
- .option('--json', 'Output structured JSON')
30
- .option('--verbose', 'Verbose output')
31
- .option('--quiet', 'Suppress non-essential output')
32
- .option('--api-key <key>', 'Use this API key for this invocation (highest credential precedence)')
33
- .hook('preAction', (thisCommand) => {
34
- const opts = thisCommand.opts();
35
- setGlobalOptions({
36
- json: opts.json ?? false,
37
- verbose: opts.verbose ?? false,
38
- quiet: opts.quiet ?? false,
39
- });
40
- setApiKeyFlag(opts.apiKey);
41
- })
42
- .action(() => {
43
- // Show banner when run with no subcommand
44
- showBanner(pkg.version);
45
- program.help();
46
- });
47
- registerInitCommand(program);
48
- registerAuthCommands(program);
49
- registerWorkspaceCommands(program);
50
- registerElementCommands(program);
51
- registerLinkCommands(program);
52
- registerWorkspaceExportCommand(program);
53
- registerComponentCommands(program);
54
- registerStrategyCommands(program);
55
- registerResourceCommands(program);
56
- registerWebhookCommands(program);
57
- registerAgentCommands(program);
58
- registerApiCommands(program);
59
- registerSkillsCommands(program);
60
- registerContributeCommands(program);
8
+ const program = buildProgram(pkg.version);
61
9
  async function main() {
62
10
  // Gate all contribute invocations (including --help) before Commander parses.
63
11
  // Commander handles --help internally before preAction fires, so we check early.
@@ -0,0 +1,10 @@
1
+ import { Command } from 'commander';
2
+ /**
3
+ * Document commands — workspace-authored narrative artifacts (board updates,
4
+ * team briefs, decision briefs, custom docs). All are workspace-scoped via
5
+ * `--workspace` (falling back to the default set by `skhema workspace use`).
6
+ * The routes carry the workspace id in the path, so — unlike `workspace list`
7
+ * — no organizationId is forwarded; the gateway authorizes via workspace access.
8
+ */
9
+ export declare function registerDocumentCommands(program: Command): void;
10
+ //# sourceMappingURL=document.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"document.d.ts","sourceRoot":"","sources":["../../src/commands/document.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AAqBnC;;;;;;GAMG;AACH,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAgK/D"}
@@ -0,0 +1,122 @@
1
+ import chalk from 'chalk';
2
+ import { resolveClient } from '../lib/api/client.js';
3
+ import { asRecord, buildBody, coerceList, readPayloadFile, resolveWorkspaceId, } from '../lib/api/command-helpers.js';
4
+ import { runCommand, UsageError } from '../lib/api/run-command.js';
5
+ import { getGlobalOptions, log, logHeader, logTable } from '../lib/output.js';
6
+ /** Print a single document as a label block (human output only). */
7
+ function printDocument(document) {
8
+ const d = asRecord(document);
9
+ logHeader(String(d.title ?? d.id ?? 'Document'));
10
+ log(` Id: ${chalk.cyan(String(d.id ?? '—'))}`);
11
+ if (d.docType)
12
+ log(` Type: ${String(d.docType)}`);
13
+ if (d.updatedAt)
14
+ log(` Updated: ${String(d.updatedAt)}`);
15
+ }
16
+ /**
17
+ * Document commands — workspace-authored narrative artifacts (board updates,
18
+ * team briefs, decision briefs, custom docs). All are workspace-scoped via
19
+ * `--workspace` (falling back to the default set by `skhema workspace use`).
20
+ * The routes carry the workspace id in the path, so — unlike `workspace list`
21
+ * — no organizationId is forwarded; the gateway authorizes via workspace access.
22
+ */
23
+ export function registerDocumentCommands(program) {
24
+ const document = program.command('document').description('Document commands');
25
+ const withWorkspace = (cmd) => cmd.option('--workspace <id>', 'Workspace id (defaults to `skhema workspace use`)');
26
+ withWorkspace(document.command('list').description('List documents')).action(async (options) => {
27
+ await runCommand('document list', async () => {
28
+ const workspaceId = resolveWorkspaceId(options.workspace);
29
+ const { client } = await resolveClient();
30
+ const data = await client.documents.list(workspaceId);
31
+ const rows = coerceList(data, 'documents');
32
+ if (!getGlobalOptions().json) {
33
+ logHeader(`Documents (${rows.length})`);
34
+ if (rows.length === 0)
35
+ log(chalk.dim(' No documents.'));
36
+ else
37
+ logTable(['Title', 'Type', 'Updated', 'Id'], rows.map((r) => {
38
+ const d = asRecord(r);
39
+ return [
40
+ String(d.title ?? '—'),
41
+ String(d.docType ?? '—'),
42
+ String(d.updatedAt ?? '—'),
43
+ String(d.id ?? '—'),
44
+ ];
45
+ }));
46
+ }
47
+ return data;
48
+ });
49
+ });
50
+ withWorkspace(document.command('get <document-id>').description('Get a document by id')).action(async (documentId, options) => {
51
+ await runCommand('document get', async () => {
52
+ const workspaceId = resolveWorkspaceId(options.workspace);
53
+ const { client } = await resolveClient();
54
+ const data = await client.documents.get(workspaceId, documentId);
55
+ if (!getGlobalOptions().json)
56
+ printDocument(asRecord(data).document ?? data);
57
+ return data;
58
+ });
59
+ });
60
+ withWorkspace(document
61
+ .command('create')
62
+ .description('Create a document')
63
+ .option('--title <title>', 'Document title')
64
+ .option('--doc-type <type>', 'Document type (board_update | team_brief | decision_brief | custom)')
65
+ .option('--content <content>', 'Document content')
66
+ .option('--file <path>', 'JSON payload file for the full body ("-" for stdin)')).action(async (options) => {
67
+ await runCommand('document create', async () => {
68
+ const workspaceId = resolveWorkspaceId(options.workspace);
69
+ const body = buildBody(readPayloadFile(options.file), {
70
+ title: options.title,
71
+ docType: options.docType,
72
+ content: options.content,
73
+ });
74
+ if (Object.keys(body).length === 0) {
75
+ throw new UsageError('Nothing to create. Pass document fields as flags or a --file payload.');
76
+ }
77
+ const { client } = await resolveClient();
78
+ const data = await client.documents.create(workspaceId, body);
79
+ if (!getGlobalOptions().json) {
80
+ log(chalk.green('Document created.'));
81
+ printDocument(asRecord(data).document ?? data);
82
+ }
83
+ return data;
84
+ });
85
+ });
86
+ withWorkspace(document
87
+ .command('update <document-id>')
88
+ .description('Update a document')
89
+ .option('--title <title>', 'Document title')
90
+ .option('--doc-type <type>', 'Document type (board_update | team_brief | decision_brief | custom)')
91
+ .option('--content <content>', 'Document content')
92
+ .option('--file <path>', 'JSON payload file for the full body ("-" for stdin)')).action(async (documentId, options) => {
93
+ await runCommand('document update', async () => {
94
+ const workspaceId = resolveWorkspaceId(options.workspace);
95
+ const body = buildBody(readPayloadFile(options.file), {
96
+ title: options.title,
97
+ docType: options.docType,
98
+ content: options.content,
99
+ });
100
+ if (Object.keys(body).length === 0) {
101
+ throw new UsageError('Nothing to update. Pass document fields as flags or a --file payload.');
102
+ }
103
+ const { client } = await resolveClient();
104
+ const data = await client.documents.update(workspaceId, documentId, body);
105
+ if (!getGlobalOptions().json) {
106
+ log(chalk.green('Document updated.'));
107
+ printDocument(asRecord(data).document ?? data);
108
+ }
109
+ return data;
110
+ });
111
+ });
112
+ withWorkspace(document.command('delete <document-id>').description('Delete a document')).action(async (documentId, options) => {
113
+ await runCommand('document delete', async () => {
114
+ const workspaceId = resolveWorkspaceId(options.workspace);
115
+ const { client } = await resolveClient();
116
+ const data = await client.documents.delete(workspaceId, documentId);
117
+ if (!getGlobalOptions().json)
118
+ log(chalk.green(`Deleted document ${documentId}.`));
119
+ return data;
120
+ });
121
+ });
122
+ }
@@ -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;AAanC,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CA4I7D"}
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,7 +1,9 @@
1
1
  import chalk from 'chalk';
2
2
  import ora from 'ora';
3
3
  import { getGlobalOptions, log, logHeader, logTable, outputError, outputSuccess, } from '../lib/output.js';
4
+ import { listBundledSkills, readBundledSkill } from '../lib/skills/bundled.js';
4
5
  import { installSkills, updateSkills } from '../lib/skills/installer.js';
6
+ import { readLock } from '../lib/skills/lock.js';
5
7
  import { detectPlatforms } from '../lib/skills/platform-detect.js';
6
8
  export function registerSkillsCommands(program) {
7
9
  const skills = program.command('skills').description('AI skills management');
@@ -57,12 +59,71 @@ export function registerSkillsCommands(program) {
57
59
  });
58
60
  skills
59
61
  .command('list')
60
- .description('Show detected platforms and installed skills')
62
+ .description('List the Skhema skills bundled with this CLI')
63
+ .action(async () => {
64
+ try {
65
+ const bundled = listBundledSkills();
66
+ const detected = detectPlatforms().filter((p) => p.detected);
67
+ const withInstalls = bundled.map((skill) => ({
68
+ name: skill.name,
69
+ description: skill.description,
70
+ installedOn: detected
71
+ .filter((p) => readLock(p).skills[skill.name] !== undefined)
72
+ .map((p) => p.id),
73
+ }));
74
+ if (getGlobalOptions().json) {
75
+ outputSuccess({ skills: withInstalls, detectedPlatforms: detected.length }, 'skills list');
76
+ return;
77
+ }
78
+ logHeader(`Skhema Skills (${withInstalls.length})`);
79
+ const truncate = (text, max) => text.length > max ? `${text.slice(0, max - 1)}…` : text;
80
+ logTable(['Name', 'Description', 'Installed'], withInstalls.map((skill) => [
81
+ skill.name,
82
+ truncate(skill.description, 60),
83
+ skill.installedOn.length === 0
84
+ ? chalk.dim('not installed')
85
+ : `${skill.installedOn.length}/${detected.length} platforms`,
86
+ ]));
87
+ log('');
88
+ log(chalk.dim('Run `skhema skills show <name>` to read a skill, `skhema skills platforms` for install locations.'));
89
+ }
90
+ catch (error) {
91
+ outputError(error instanceof Error ? error.message : 'Unknown error', 'skills list');
92
+ process.exit(1);
93
+ }
94
+ });
95
+ skills
96
+ .command('show <name>')
97
+ .description('Print a bundled skill (frontmatter and documentation)')
98
+ .action(async (name) => {
99
+ try {
100
+ const skill = readBundledSkill(name);
101
+ if (!skill) {
102
+ const available = listBundledSkills()
103
+ .map((s) => s.name)
104
+ .join(', ');
105
+ outputError(`Skill "${name}" is not bundled with this CLI. Available: ${available}`, 'skills show');
106
+ process.exit(1);
107
+ }
108
+ if (getGlobalOptions().json) {
109
+ outputSuccess(skill, 'skills show');
110
+ return;
111
+ }
112
+ log(skill.content);
113
+ }
114
+ catch (error) {
115
+ outputError(error instanceof Error ? error.message : 'Unknown error', 'skills show');
116
+ process.exit(1);
117
+ }
118
+ });
119
+ skills
120
+ .command('platforms')
121
+ .description('Show detected agent platforms and their skills directories')
61
122
  .action(async () => {
62
123
  try {
63
124
  const platforms = detectPlatforms();
64
125
  if (getGlobalOptions().json) {
65
- outputSuccess(platforms, 'skills list');
126
+ outputSuccess(platforms, 'skills platforms');
66
127
  return;
67
128
  }
68
129
  logHeader('Agent Platforms');
@@ -74,7 +135,7 @@ export function registerSkillsCommands(program) {
74
135
  logTable(['Platform', 'Status', 'Skills Directory'], rows);
75
136
  }
76
137
  catch (error) {
77
- outputError(error instanceof Error ? error.message : 'Unknown error', 'skills list');
138
+ outputError(error instanceof Error ? error.message : 'Unknown error', 'skills platforms');
78
139
  process.exit(1);
79
140
  }
80
141
  });
@@ -1 +1 @@
1
- {"version":3,"file":"command-helpers.d.ts","sourceRoot":"","sources":["../../../src/lib/api/command-helpers.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAA;AAKzC;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,CAQxD;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,WAAW,GAAG,SAAS,CAsB1E;AAED;;;;GAIG;AACH,wBAAgB,SAAS,CACvB,IAAI,EAAE,WAAW,GAAG,SAAS,EAC7B,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC9B,WAAW,CAMb;AAED,4EAA4E;AAC5E,wBAAgB,QAAQ,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAIhE;AAED;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,OAAO,EAAE,GAAG,CAAC,EAAE,MAAM,GAAG,OAAO,EAAE,CAQlE"}
1
+ {"version":3,"file":"command-helpers.d.ts","sourceRoot":"","sources":["../../../src/lib/api/command-helpers.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAA;AAKzC;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,CAQxD;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,WAAW,GAAG,SAAS,CAuB1E;AAED;;;;GAIG;AACH,wBAAgB,SAAS,CACvB,IAAI,EAAE,WAAW,GAAG,SAAS,EAC7B,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC9B,WAAW,CAMb;AAED,4EAA4E;AAC5E,wBAAgB,QAAQ,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAIhE;AAED;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,OAAO,EAAE,GAAG,CAAC,EAAE,MAAM,GAAG,OAAO,EAAE,CAQlE"}
@@ -29,7 +29,8 @@ export function readPayloadFile(filePath) {
29
29
  throw new UsageError(`Could not read payload file "${filePath}": ${err instanceof Error ? err.message : String(err)}`);
30
30
  }
31
31
  try {
32
- const parsed = JSON.parse(raw);
32
+ // Strip a leading UTF-8 BOM so a byte-identical-looking file parses.
33
+ const parsed = JSON.parse(raw.replace(/^\uFEFF/, ''));
33
34
  if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
34
35
  throw new Error('expected a JSON object');
35
36
  }
@@ -1 +1 @@
1
- {"version":3,"file":"passthrough.d.ts","sourceRoot":"","sources":["../../../src/lib/api/passthrough.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AAW7C;;;GAGG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,UAAU,CAQvD;AAED;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM,CAyBrE;AAED;;;;;;;;GAQG;AACH,wBAAsB,eAAe,CACnC,GAAG,EAAE,MAAM,GAAG,SAAS,EACvB,EAAE,EAAE;IACF,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAA;IAC3C,SAAS,EAAE,MAAM,OAAO,CAAC,MAAM,CAAC,CAAA;CACjC,GACA,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,CAAC,CA+B9C;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAC7B,GAAG,EAAE,MAAM,EAAE,GAAG,SAAS,GACxB,KAAK,CAAC,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CASzB"}
1
+ {"version":3,"file":"passthrough.d.ts","sourceRoot":"","sources":["../../../src/lib/api/passthrough.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AAW7C;;;GAGG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,UAAU,CAQvD;AAED;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM,CAyBrE;AAED;;;;;;;;GAQG;AACH,wBAAsB,eAAe,CACnC,GAAG,EAAE,MAAM,GAAG,SAAS,EACvB,EAAE,EAAE;IACF,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAA;IAC3C,SAAS,EAAE,MAAM,OAAO,CAAC,MAAM,CAAC,CAAA;CACjC,GACA,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,CAAC,CA2C9C;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAC7B,GAAG,EAAE,MAAM,EAAE,GAAG,SAAS,GACxB,KAAK,CAAC,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CASzB"}
@@ -79,10 +79,19 @@ export async function resolveDataBody(raw, io) {
79
79
  }
80
80
  let parsed;
81
81
  try {
82
- parsed = JSON.parse(text);
82
+ // Strip a leading UTF-8 BOM (editors and some `echo`/redirect toolchains
83
+ // prepend one); JSON.parse chokes on it, which surfaced as the opaque
84
+ // "--data is not valid JSON" even for a well-formed @file. Mirrors curl.
85
+ parsed = JSON.parse(text.replace(/^\uFEFF/, ''));
83
86
  }
84
- catch {
85
- throw new UsageError('--data is not valid JSON.');
87
+ catch (err) {
88
+ // Name the source so a failed @file is not mistaken for the literal form.
89
+ const label = raw === '-'
90
+ ? 'from stdin'
91
+ : raw.startsWith('@')
92
+ ? `file "${raw.slice(1)}"`
93
+ : 'value';
94
+ throw new UsageError(`--data ${label} is not valid JSON: ${err instanceof Error ? err.message : String(err)}`);
86
95
  }
87
96
  if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
88
97
  throw new UsageError('--data must be a JSON object.');
@@ -1 +1 @@
1
- {"version":3,"file":"payload.d.ts","sourceRoot":"","sources":["../../../src/lib/api/payload.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAA;AAI9C,8DAA8D;AAC9D,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAatD;AAED,4EAA4E;AAC5E,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,GAAG,WAAW,CAczD;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,OAAO,EAAE;IACnC,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,MAAM,CAAC,EAAE,WAAW,CAAA;CACrB,GAAG,WAAW,CAad"}
1
+ {"version":3,"file":"payload.d.ts","sourceRoot":"","sources":["../../../src/lib/api/payload.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAA;AAI9C,8DAA8D;AAC9D,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAatD;AAED,4EAA4E;AAC5E,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,GAAG,WAAW,CAezD;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,OAAO,EAAE;IACnC,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,MAAM,CAAC,EAAE,WAAW,CAAA;CACrB,GAAG,WAAW,CAad"}
@@ -22,7 +22,8 @@ export function parseJsonFile(source) {
22
22
  const raw = readFileOrStdin(source);
23
23
  let parsed;
24
24
  try {
25
- parsed = JSON.parse(raw);
25
+ // Strip a leading UTF-8 BOM so a byte-identical-looking file parses.
26
+ parsed = JSON.parse(raw.replace(/^\uFEFF/, ''));
26
27
  }
27
28
  catch {
28
29
  throw new UsageError(`Payload in ${source === '-' ? 'stdin' : source} is not valid JSON.`);
@@ -0,0 +1,2 @@
1
+ export declare function generateCliReference(): string;
2
+ //# sourceMappingURL=reference-generator.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reference-generator.d.ts","sourceRoot":"","sources":["../../../src/lib/docs/reference-generator.ts"],"names":[],"mappings":"AA6IA,wBAAgB,oBAAoB,IAAI,MAAM,CAyE7C"}
@@ -0,0 +1,185 @@
1
+ import { buildProgram } from '../../program.js';
2
+ import { ExitCode } from '../api/run-command.js';
3
+ /**
4
+ * Generate the public CLI reference as MDX, by walking the same commander
5
+ * program tree the runtime executes (src/program.ts). Deterministic and
6
+ * version-independent: the output changes only when commands, arguments,
7
+ * options, or exit codes change — never on a plain version bump.
8
+ *
9
+ * The committed copy lives at docs/cli-reference.mdx and is drift-tested
10
+ * (src/__tests__/cli-reference-drift.test.ts). The deployed copy is
11
+ * skhema-web content/docs/api/cli.mdx — sync it when this file changes.
12
+ */
13
+ /** Human meanings for the stable exit-code contract (run-command.ts). */
14
+ const EXIT_CODE_MEANINGS = {
15
+ Ok: 'Success',
16
+ Error: 'Generic or API error (non-specific 4xx/5xx, unexpected failure)',
17
+ Usage: 'Usage error — bad or missing arguments, caught before any API call',
18
+ AuthRequired: 'No usable credential — authenticate first',
19
+ Denied: 'Permission or plan denied (HTTP 403 or 402)',
20
+ RateLimited: 'Rate limited (HTTP 429)',
21
+ };
22
+ /** Escape plain text for MDX prose and table cells. */
23
+ function esc(text) {
24
+ return text
25
+ .replace(/\\/g, '\\\\')
26
+ .replace(/</g, '\\<')
27
+ .replace(/\{/g, '\\{')
28
+ .replace(/\}/g, '\\}')
29
+ .replace(/\|/g, '\\|')
30
+ .replace(/\n/g, ' ');
31
+ }
32
+ /**
33
+ * Escape a string destined for an inline-code span INSIDE a GFM table cell.
34
+ * Table cell-splitting on `|` happens before inline-code parsing, so a pipe
35
+ * inside backticks still breaks the row (and can leak `<` into JSX position —
36
+ * this took down /docs/api/cli via `--data <json|@file|->`). GFM honours
37
+ * `\|` everywhere in a table row, including within code spans.
38
+ */
39
+ function escCode(text) {
40
+ return text.replace(/\|/g, '\\|');
41
+ }
42
+ /** Render an argument the way commander's help does: <required> / [optional]. */
43
+ function argName(arg) {
44
+ const name = arg.name() + (arg.variadic ? '...' : '');
45
+ return arg.required ? `<${name}>` : `[${name}]`;
46
+ }
47
+ /** Full invocation path from the root program down to this command. */
48
+ function commandPath(cmd) {
49
+ const parts = [];
50
+ let current = cmd;
51
+ while (current) {
52
+ parts.unshift(current.name());
53
+ current = current.parent;
54
+ }
55
+ return parts.join(' ');
56
+ }
57
+ function usageLine(cmd) {
58
+ const pieces = [commandPath(cmd)];
59
+ const args = cmd.registeredArguments.map(argName);
60
+ if (args.length > 0)
61
+ pieces.push(...args);
62
+ const visibleOptions = cmd.options.filter((option) => !option.hidden);
63
+ if (visibleOptions.length > 0)
64
+ pieces.push('[options]');
65
+ if (cmd.commands.length > 0)
66
+ pieces.push('<command>');
67
+ return pieces.join(' ');
68
+ }
69
+ function renderDefault(option) {
70
+ if (option.defaultValue === undefined)
71
+ return '';
72
+ return `\`${JSON.stringify(option.defaultValue)}\``;
73
+ }
74
+ function renderOptionsTable(cmd) {
75
+ const visible = cmd.options.filter((option) => !option.hidden);
76
+ if (visible.length === 0)
77
+ return [];
78
+ const hasDefaults = visible.some((option) => option.defaultValue !== undefined);
79
+ const lines = [];
80
+ if (hasDefaults) {
81
+ lines.push('| Option | Description | Default |');
82
+ lines.push('| --- | --- | --- |');
83
+ for (const option of visible) {
84
+ lines.push(`| \`${escCode(option.flags)}\` | ${esc(option.description ?? '')} | ${renderDefault(option)} |`);
85
+ }
86
+ }
87
+ else {
88
+ lines.push('| Option | Description |');
89
+ lines.push('| --- | --- |');
90
+ for (const option of visible) {
91
+ lines.push(`| \`${escCode(option.flags)}\` | ${esc(option.description ?? '')} |`);
92
+ }
93
+ }
94
+ lines.push('');
95
+ return lines;
96
+ }
97
+ function renderArgumentsTable(cmd) {
98
+ const args = cmd.registeredArguments.filter((arg) => arg.description);
99
+ if (args.length === 0)
100
+ return [];
101
+ const lines = ['| Argument | Description |', '| --- | --- |'];
102
+ for (const arg of args) {
103
+ lines.push(`| \`${argName(arg)}\` | ${esc(arg.description)} |`);
104
+ }
105
+ lines.push('');
106
+ return lines;
107
+ }
108
+ function renderCommand(cmd, depth) {
109
+ if (cmd.name() === 'help')
110
+ return [];
111
+ const lines = [];
112
+ const heading = '#'.repeat(Math.min(depth + 2, 6));
113
+ lines.push(`${heading} \`${commandPath(cmd)}\``);
114
+ lines.push('');
115
+ lines.push('```sh');
116
+ lines.push(usageLine(cmd));
117
+ lines.push('```');
118
+ lines.push('');
119
+ const description = cmd.description();
120
+ if (description) {
121
+ lines.push(esc(description));
122
+ lines.push('');
123
+ }
124
+ lines.push(...renderArgumentsTable(cmd));
125
+ lines.push(...renderOptionsTable(cmd));
126
+ for (const sub of cmd.commands) {
127
+ lines.push(...renderCommand(sub, depth + 1));
128
+ }
129
+ return lines;
130
+ }
131
+ export function generateCliReference() {
132
+ const program = buildProgram('0.0.0-reference');
133
+ const lines = [];
134
+ lines.push('---');
135
+ lines.push('title: CLI Reference');
136
+ lines.push('description: Every skhema command — usage, arguments, options, and the exit-code contract. Generated from the CLI itself.');
137
+ lines.push('---');
138
+ lines.push('');
139
+ lines.push('{/* AUTO-GENERATED from the @skhema/cli command tree — do not edit by hand.');
140
+ lines.push(' Regenerate with `npm run generate:cli-reference` in skhema-packages/skhema-cli. */}');
141
+ lines.push('');
142
+ lines.push('This page is generated from the CLI itself, so it always matches the installed command surface. For a guided tour, start with [Using the CLI](/docs/guides/using-the-cli).');
143
+ lines.push('');
144
+ lines.push('```sh');
145
+ lines.push('npm install -g @skhema/cli');
146
+ lines.push('```');
147
+ lines.push('');
148
+ lines.push('## `skhema`');
149
+ lines.push('');
150
+ lines.push('```sh');
151
+ lines.push('skhema [options] <command>');
152
+ lines.push('```');
153
+ lines.push('');
154
+ lines.push(esc(program.description()));
155
+ lines.push('');
156
+ lines.push('### Global options');
157
+ lines.push('');
158
+ lines.push('Available on every command.');
159
+ lines.push('');
160
+ lines.push('| Option | Description |');
161
+ lines.push('| --- | --- |');
162
+ for (const option of program.options.filter((o) => !o.hidden)) {
163
+ lines.push(`| \`${escCode(option.flags)}\` | ${esc(option.description ?? '')} |`);
164
+ }
165
+ lines.push('');
166
+ lines.push('### Exit codes');
167
+ lines.push('');
168
+ lines.push('Exit codes are a stable contract — scripts, CI steps, and agents can branch on them.');
169
+ lines.push('');
170
+ lines.push('| Code | Meaning |');
171
+ lines.push('| --- | --- |');
172
+ for (const [key, meaning] of Object.entries(EXIT_CODE_MEANINGS)) {
173
+ lines.push(`| \`${ExitCode[key]}\` | ${meaning} |`);
174
+ }
175
+ lines.push('');
176
+ lines.push('## Commands');
177
+ lines.push('');
178
+ for (const cmd of program.commands) {
179
+ lines.push(...renderCommand(cmd, 1));
180
+ }
181
+ return (lines
182
+ .join('\n')
183
+ .replace(/\n{3,}/g, '\n\n')
184
+ .trimEnd() + '\n');
185
+ }
@@ -0,0 +1,16 @@
1
+ /** A skill shipped inside the @skhema/cli package (skills/, synced at build). */
2
+ export interface BundledSkill {
3
+ name: string;
4
+ description: string;
5
+ dir: string;
6
+ }
7
+ export declare function getBundledSkillsDir(): string;
8
+ /** Enumerate the bundled skills with their frontmatter metadata. */
9
+ export declare function listBundledSkills(): BundledSkill[];
10
+ /** Read a bundled skill's full SKILL.md, or null if it is not bundled. */
11
+ export declare function readBundledSkill(name: string): {
12
+ name: string;
13
+ description: string;
14
+ content: string;
15
+ } | null;
16
+ //# sourceMappingURL=bundled.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bundled.d.ts","sourceRoot":"","sources":["../../../src/lib/skills/bundled.ts"],"names":[],"mappings":"AAOA,iFAAiF;AACjF,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAA;IACZ,WAAW,EAAE,MAAM,CAAA;IACnB,GAAG,EAAE,MAAM,CAAA;CACZ;AAED,wBAAgB,mBAAmB,IAAI,MAAM,CAG5C;AA6BD,oEAAoE;AACpE,wBAAgB,iBAAiB,IAAI,YAAY,EAAE,CAmBlD;AAED,0EAA0E;AAC1E,wBAAgB,gBAAgB,CAC9B,IAAI,EAAE,MAAM,GACX;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAM/D"}
@@ -0,0 +1,66 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { fileURLToPath } from 'url';
4
+ const __filename = fileURLToPath(import.meta.url);
5
+ const __dirname = path.dirname(__filename);
6
+ export function getBundledSkillsDir() {
7
+ // Navigate from dist/lib/skills/bundled.js (or src/… under tsx) to skills/
8
+ return path.resolve(__dirname, '..', '..', '..', 'skills');
9
+ }
10
+ /**
11
+ * Parse the SKILL.md frontmatter block. The bundled skills use simple
12
+ * single-line `key: value` pairs between `---` fences.
13
+ */
14
+ function parseFrontmatter(content) {
15
+ const result = {};
16
+ const lines = content.split('\n');
17
+ if (lines[0]?.trim() !== '---')
18
+ return result;
19
+ for (let i = 1; i < lines.length; i++) {
20
+ const line = lines[i];
21
+ if (line.trim() === '---')
22
+ break;
23
+ const colon = line.indexOf(':');
24
+ if (colon === -1)
25
+ continue;
26
+ let value = line.slice(colon + 1).trim();
27
+ // Unwrap YAML-style quoting ("…" or '…') around single-line values.
28
+ if (value.length >= 2 &&
29
+ ((value.startsWith('"') && value.endsWith('"')) ||
30
+ (value.startsWith("'") && value.endsWith("'")))) {
31
+ value = value.slice(1, -1);
32
+ }
33
+ result[line.slice(0, colon).trim()] = value;
34
+ }
35
+ return result;
36
+ }
37
+ /** Enumerate the bundled skills with their frontmatter metadata. */
38
+ export function listBundledSkills() {
39
+ const skillsDir = getBundledSkillsDir();
40
+ if (!fs.existsSync(skillsDir))
41
+ return [];
42
+ return fs
43
+ .readdirSync(skillsDir, { withFileTypes: true })
44
+ .filter((entry) => entry.isDirectory())
45
+ .map((entry) => {
46
+ const dir = path.join(skillsDir, entry.name);
47
+ const skillFile = path.join(dir, 'SKILL.md');
48
+ let description = '';
49
+ if (fs.existsSync(skillFile)) {
50
+ const meta = parseFrontmatter(fs.readFileSync(skillFile, 'utf-8'));
51
+ description = meta.description ?? '';
52
+ }
53
+ return { name: entry.name, description, dir };
54
+ })
55
+ .filter((skill) => fs.existsSync(path.join(skill.dir, 'SKILL.md')))
56
+ .sort((a, b) => a.name.localeCompare(b.name));
57
+ }
58
+ /** Read a bundled skill's full SKILL.md, or null if it is not bundled. */
59
+ export function readBundledSkill(name) {
60
+ const skillFile = path.join(getBundledSkillsDir(), name, 'SKILL.md');
61
+ if (!fs.existsSync(skillFile))
62
+ return null;
63
+ const content = fs.readFileSync(skillFile, 'utf-8');
64
+ const meta = parseFrontmatter(content);
65
+ return { name, description: meta.description ?? '', content };
66
+ }
@@ -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;AAgFjE,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,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,28 +1,13 @@
1
1
  import crypto from 'crypto';
2
2
  import fs from 'fs';
3
3
  import path from 'path';
4
- import { fileURLToPath } from 'url';
4
+ import { listBundledSkills } from './bundled.js';
5
5
  import { readLock, writeLock } from './lock.js';
6
- const __filename = fileURLToPath(import.meta.url);
7
- const __dirname = path.dirname(__filename);
8
- function getBundledSkillsDir() {
9
- // Navigate from dist/lib/skills/installer.js to skills/
10
- return path.resolve(__dirname, '..', '..', '..', 'skills');
11
- }
12
6
  function getFileChecksum(content) {
13
7
  return crypto.createHash('sha256').update(content).digest('hex').slice(0, 16);
14
8
  }
15
9
  function getBundledSkills() {
16
- const skillsDir = getBundledSkillsDir();
17
- if (!fs.existsSync(skillsDir))
18
- return [];
19
- return fs
20
- .readdirSync(skillsDir, { withFileTypes: true })
21
- .filter((entry) => entry.isDirectory())
22
- .map((entry) => ({
23
- name: entry.name,
24
- dir: path.join(skillsDir, entry.name),
25
- }));
10
+ return listBundledSkills();
26
11
  }
27
12
  function copySkillToPlatform(skillDir, skillName, platform) {
28
13
  const targetDir = path.join(platform.skillsDir, skillName);
@@ -0,0 +1,8 @@
1
+ import { Command } from 'commander';
2
+ /**
3
+ * Assemble the full skhema CLI program. The runtime entry (cli.ts) and the
4
+ * docs reference generator both build from here, so every registered command
5
+ * is — structurally — part of the generated reference.
6
+ */
7
+ export declare function buildProgram(version: string): Command;
8
+ //# sourceMappingURL=program.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"program.d.ts","sourceRoot":"","sources":["../src/program.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AAoBnC;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAgDrD"}
@@ -0,0 +1,65 @@
1
+ import { Command } from 'commander';
2
+ import { registerAgentCommands } from './commands/agent.js';
3
+ import { registerApiCommands } from './commands/api.js';
4
+ import { registerAuthCommands } from './commands/auth.js';
5
+ import { registerComponentCommands } from './commands/component.js';
6
+ import { registerContributeCommands } from './commands/contribute.js';
7
+ import { registerDocumentCommands } from './commands/document.js';
8
+ import { registerElementCommands } from './commands/element.js';
9
+ import { registerWorkspaceExportCommand } from './commands/export.js';
10
+ import { registerInitCommand } from './commands/init.js';
11
+ import { registerLinkCommands } from './commands/link.js';
12
+ import { registerResourceCommands } from './commands/resource.js';
13
+ import { registerSkillsCommands } from './commands/skills.js';
14
+ import { registerStrategyCommands } from './commands/strategy.js';
15
+ import { registerWebhookCommands } from './commands/webhook.js';
16
+ import { registerWorkspaceCommands } from './commands/workspace.js';
17
+ import { setApiKeyFlag } from './lib/api/credential-context.js';
18
+ import { showBanner } from './lib/banner.js';
19
+ import { setGlobalOptions } from './lib/output.js';
20
+ /**
21
+ * Assemble the full skhema CLI program. The runtime entry (cli.ts) and the
22
+ * docs reference generator both build from here, so every registered command
23
+ * is — structurally — part of the generated reference.
24
+ */
25
+ export function buildProgram(version) {
26
+ const program = new Command();
27
+ program
28
+ .name('skhema')
29
+ .description('Skhema CLI — drive the Skhema Public API from your terminal, CI, or agent')
30
+ .version(version)
31
+ .option('--json', 'Output structured JSON')
32
+ .option('--verbose', 'Verbose output')
33
+ .option('--quiet', 'Suppress non-essential output')
34
+ .option('--api-key <key>', 'Use this API key for this invocation (highest credential precedence)')
35
+ .hook('preAction', (thisCommand) => {
36
+ const opts = thisCommand.opts();
37
+ setGlobalOptions({
38
+ json: opts.json ?? false,
39
+ verbose: opts.verbose ?? false,
40
+ quiet: opts.quiet ?? false,
41
+ });
42
+ setApiKeyFlag(opts.apiKey);
43
+ })
44
+ .action(() => {
45
+ // Show banner when run with no subcommand
46
+ showBanner(version);
47
+ program.help();
48
+ });
49
+ registerInitCommand(program);
50
+ registerAuthCommands(program);
51
+ registerWorkspaceCommands(program);
52
+ registerElementCommands(program);
53
+ registerLinkCommands(program);
54
+ registerWorkspaceExportCommand(program);
55
+ registerComponentCommands(program);
56
+ registerStrategyCommands(program);
57
+ registerDocumentCommands(program);
58
+ registerResourceCommands(program);
59
+ registerWebhookCommands(program);
60
+ registerAgentCommands(program);
61
+ registerApiCommands(program);
62
+ registerSkillsCommands(program);
63
+ registerContributeCommands(program);
64
+ return program;
65
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skhema/cli",
3
- "version": "0.4.2",
3
+ "version": "0.4.4",
4
4
  "description": "Skhema CLI - Authentication and AI skills management for agent platforms",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -37,23 +37,26 @@
37
37
  "check": "npm run typecheck && npm run lint && npm run format:check",
38
38
  "check:fix": "npm run format && npm run lint:fix",
39
39
  "prepublishOnly": "npm run build",
40
- "postinstall": "node dist/postinstall.js || true"
40
+ "postinstall": "node dist/postinstall.js || true",
41
+ "generate:cli-reference": "tsx scripts/generate-cli-reference.ts"
41
42
  },
42
43
  "dependencies": {
43
- "chalk": "^5.3.0",
44
- "commander": "^12.0.0",
45
- "ora": "^8.0.0",
46
44
  "@skhema/agent-sdk": "0.1.4",
47
45
  "@skhema/method": "0.3.0",
48
- "@skhema/sdk": "0.2.1"
46
+ "@skhema/sdk": "0.2.2",
47
+ "chalk": "^5.3.0",
48
+ "commander": "^12.0.0",
49
+ "ora": "^8.0.0"
49
50
  },
50
51
  "devDependencies": {
52
+ "@mdx-js/mdx": "3.1.1",
51
53
  "@skhema/linter": "2.2.0",
52
54
  "@skhema/prettier-config": "1.0.0",
53
55
  "@types/node": "^22.0.0",
54
56
  "eslint": "^9.0.0",
55
57
  "prettier": "^3.0.0",
56
58
  "prettier-plugin-organize-imports": "^4.3.0",
59
+ "remark-gfm": "4.0.1",
57
60
  "tsx": "^4.7.0",
58
61
  "typescript": "^5.0.0",
59
62
  "vitest": "^4.0.0"