@malmhq/intermesh-cli 0.1.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.
Files changed (95) hide show
  1. package/LICENSE +198 -0
  2. package/README.md +77 -0
  3. package/dist/generation/.opencode/AGENTS.md +38 -0
  4. package/dist/generation/.opencode/agents/generation-builder.md +9 -0
  5. package/dist/generation/.opencode/agents/generation-fidelity.md +9 -0
  6. package/dist/generation/.opencode/agents/generation-source.md +13 -0
  7. package/dist/generation/.opencode/prompts/analyze-capabilities.md +61 -0
  8. package/dist/generation/.opencode/prompts/analyze-source.md +41 -0
  9. package/dist/generation/.opencode/prompts/generate.md +9 -0
  10. package/dist/generation/.opencode/prompts/repair-capability-proposal.md +5 -0
  11. package/dist/generation/.opencode/skills/generation-building/SKILL.md +32 -0
  12. package/dist/generation/.opencode/skills/generation-package-review/SKILL.md +36 -0
  13. package/dist/generation/.opencode/skills/generation-source-analysis/SKILL.md +36 -0
  14. package/dist/generation/AGENTS.md +38 -0
  15. package/dist/generation/agent-skill-installer/index.ts +4 -0
  16. package/dist/generation/agent-skill-installer/install.ts +503 -0
  17. package/dist/generation/agent-skill-installer/lock.ts +128 -0
  18. package/dist/generation/agent-skill-installer/manifest.ts +271 -0
  19. package/dist/generation/agent-skill-installer/targets.ts +169 -0
  20. package/dist/generation/runtime-entrypoint.js +39980 -0
  21. package/dist/generation/scripts/check-package.mjs +45 -0
  22. package/dist/generation/scripts/installed-help.mjs +98 -0
  23. package/dist/generation/scripts/lead-input.mjs +584 -0
  24. package/dist/generation/scripts/prepare-opencode.mjs +33 -0
  25. package/dist/generation/scripts/prepared-work.mjs +197 -0
  26. package/dist/generation/scripts/repair-loop.mjs +519 -0
  27. package/dist/generation/scripts/run-reviewers.mjs +306 -0
  28. package/dist/generation/scripts/runtime-capture.mjs +119 -0
  29. package/dist/generation/scripts/stage-timings.mjs +53 -0
  30. package/dist/generation/scripts/validate-source-brief.mjs +220 -0
  31. package/dist/generation/scripts/verify-package.mjs +365 -0
  32. package/dist/generation/templates/npm-cli/CONTRIBUTING.md +20 -0
  33. package/dist/generation/templates/npm-cli/GENERATION-PATTERNS.md +143 -0
  34. package/dist/generation/templates/npm-cli/README.md +67 -0
  35. package/dist/generation/templates/npm-cli/capabilities.json +1 -0
  36. package/dist/generation/templates/npm-cli/mock/routes.ts +3 -0
  37. package/dist/generation/templates/npm-cli/mock/server.ts +15 -0
  38. package/dist/generation/templates/npm-cli/mock/types.ts +7 -0
  39. package/dist/generation/templates/npm-cli/package.json +36 -0
  40. package/dist/generation/templates/npm-cli/pnpm-lock.yaml +495 -0
  41. package/dist/generation/templates/npm-cli/pnpm-workspace.yaml +2 -0
  42. package/dist/generation/templates/npm-cli/skills/customer-cli-management/SKILL.md +22 -0
  43. package/dist/generation/templates/npm-cli/skills/customer-workflow/SKILL.md +22 -0
  44. package/dist/generation/templates/npm-cli/src/auth/commands.ts +77 -0
  45. package/dist/generation/templates/npm-cli/src/auth/credentials.ts +66 -0
  46. package/dist/generation/templates/npm-cli/src/auth/prompt.ts +27 -0
  47. package/dist/generation/templates/npm-cli/src/cli.ts +53 -0
  48. package/dist/generation/templates/npm-cli/src/commands.ts +6 -0
  49. package/dist/generation/templates/npm-cli/src/customer.ts +16 -0
  50. package/dist/generation/templates/npm-cli/src/index.ts +11 -0
  51. package/dist/generation/templates/npm-cli/src/lifecycle/commands.ts +344 -0
  52. package/dist/generation/templates/npm-cli/src/lifecycle/config.ts +32 -0
  53. package/dist/generation/templates/npm-cli/src/shared/dry-run.ts +32 -0
  54. package/dist/generation/templates/npm-cli/src/shared/errors.ts +162 -0
  55. package/dist/generation/templates/npm-cli/src/shared/files.ts +45 -0
  56. package/dist/generation/templates/npm-cli/src/shared/http.ts +125 -0
  57. package/dist/generation/templates/npm-cli/src/shared/output.ts +33 -0
  58. package/dist/generation/templates/npm-cli/test/auth.test.ts +113 -0
  59. package/dist/generation/templates/npm-cli/test/fixture-cli.ts +51 -0
  60. package/dist/generation/templates/npm-cli/test/foundation.test.ts +316 -0
  61. package/dist/generation/templates/npm-cli/test/helpers.ts +34 -0
  62. package/dist/generation/templates/npm-cli/test/mock-lifecycle.test.ts +86 -0
  63. package/dist/generation/templates/npm-cli/test/run-with-mock.ts +142 -0
  64. package/dist/generation/templates/npm-cli/tsconfig.build.json +5 -0
  65. package/dist/generation/templates/npm-cli/tsconfig.json +11 -0
  66. package/dist/generation/templates/opencode-bootstrap/package-lock.json +402 -0
  67. package/dist/generation/templates/opencode-bootstrap/package.json +7 -0
  68. package/dist/index.js +51965 -0
  69. package/dist/runtime/candidate-inspection/inspect.Dockerfile +4 -0
  70. package/dist/runtime/candidate-inspection/install.Dockerfile +5 -0
  71. package/dist/runtime/candidate-inspection/registry-proxy.mjs +37 -0
  72. package/dist/runtime/harbor/bootstrap/package-lock.json +402 -0
  73. package/dist/runtime/harbor/bootstrap/package.json +7 -0
  74. package/dist/runtime/harbor/evaluation.Dockerfile +31 -0
  75. package/dist/runtime/harbor/harbor_launcher.py +62 -0
  76. package/dist/runtime/harbor/intermesh_opencode.py +44 -0
  77. package/dist/runtime/harbor/model-forwarder.mjs +52 -0
  78. package/dist/runtime/live-task.md +13 -0
  79. package/package.json +44 -0
  80. package/skills/inter-cli-management/SKILL.md +38 -0
  81. package/skills/inter-cli-management/evals/evals.json +40 -0
  82. package/skills/inter-context/SKILL.md +72 -0
  83. package/skills/inter-context/evals/cases.json +82 -0
  84. package/skills/inter-eval-authoring/SKILL.md +70 -0
  85. package/skills/inter-eval-authoring/evals/evals.json +69 -0
  86. package/skills/inter-eval-authoring/evals/trigger-queries.json +34 -0
  87. package/skills/inter-eval-authoring/references/authoring-guide.md +70 -0
  88. package/skills/inter-eval-authoring/references/eval-plan.example.json +92 -0
  89. package/skills/inter-eval-authoring/references/eval-plan.schema.json +304 -0
  90. package/skills/inter-evaluation/SKILL.md +51 -0
  91. package/skills/inter-evaluation/evals/evals.json +49 -0
  92. package/skills/inter-evaluation/evals/trigger-queries.json +30 -0
  93. package/skills/inter-generation/SKILL.md +42 -0
  94. package/skills/inter-generation/evals/evals.json +81 -0
  95. package/skills/inter-generation/evals/trigger-queries.json +58 -0
@@ -0,0 +1,27 @@
1
+ import { createInterface } from 'node:readline/promises';
2
+ import { Writable, type Readable } from 'node:stream';
3
+ import { CliError, cliErrors } from '../shared/errors.js';
4
+
5
+ export async function readHiddenToken(
6
+ prompt: string,
7
+ input: Readable = process.stdin,
8
+ output: Writable = process.stderr,
9
+ ): Promise<string> {
10
+ const terminal = Boolean('isTTY' in input && input.isTTY);
11
+ if (!terminal)
12
+ throw new CliError(
13
+ cliErrors.invalidInput(
14
+ 'Interactive token entry requires a terminal. Use the configured token environment variable.',
15
+ ),
16
+ );
17
+ const muted = new Writable({ write: (_chunk, _encoding, done) => done() });
18
+ output.write(`${prompt}: `);
19
+ const lines = createInterface({ input, output: terminal ? muted : output, terminal });
20
+ try {
21
+ const token = (await lines.question('')).trim();
22
+ output.write('\n');
23
+ return token;
24
+ } finally {
25
+ lines.close();
26
+ }
27
+ }
@@ -0,0 +1,53 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { Command, CommanderError, Option } from 'commander';
3
+ import { CliError, cliErrors } from './shared/errors.js';
4
+ import { printError, printSuccess } from './shared/output.js';
5
+ import { dryRun, DryRunComplete } from './shared/dry-run.js';
6
+
7
+ export function createProgram(): Command {
8
+ const pkg = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
9
+ return new Command()
10
+ .name(Object.keys(pkg.bin)[0])
11
+ .description(pkg.description)
12
+ .version(pkg.version)
13
+ .option('--json', 'Emit stable JSON without interactive prompts')
14
+ .option('--dry-run', 'Validate and preview without API calls or changes')
15
+ .hook('preAction', (_root, command) =>
16
+ dryRun.enterWith(Boolean(command.optsWithGlobals().dryRun)),
17
+ )
18
+ .addOption(new Option('--base-url <url>', 'Loopback mock URL for tests only').hideHelp())
19
+ .allowExcessArguments(false)
20
+ .exitOverride()
21
+ .configureOutput({
22
+ writeErr: () => {},
23
+ writeOut: (text) => {
24
+ if (process.argv.includes('--json') || !process.stdin.isTTY || !process.stdout.isTTY)
25
+ printSuccess({ help: text }, 'help');
26
+ else process.stdout.write(text);
27
+ },
28
+ })
29
+ .showSuggestionAfterError(false);
30
+ }
31
+
32
+ export async function runCli(program: Command, argv: string[]): Promise<void> {
33
+ try {
34
+ if (argv.length === 0) program.outputHelp();
35
+ else await dryRun.run(false, () => program.parseAsync(argv, { from: 'user' }));
36
+ } catch (error) {
37
+ if (error instanceof DryRunComplete) return;
38
+ if (error instanceof CommanderError) {
39
+ if (error.exitCode === 0) return;
40
+ printError(
41
+ new CliError(
42
+ error.code === 'commander.unknownCommand'
43
+ ? cliErrors.unknownCommand
44
+ : cliErrors.invalidInput(
45
+ 'Invalid command input; see --help for supported commands and options.',
46
+ ),
47
+ ),
48
+ );
49
+ } else {
50
+ printError(error instanceof CliError ? error : new CliError(cliErrors.internal));
51
+ }
52
+ }
53
+ }
@@ -0,0 +1,6 @@
1
+ import type { Command } from 'commander';
2
+
3
+ export function registerCommands(program: Command): void {
4
+ // Integration registers customer command families here.
5
+ void program;
6
+ }
@@ -0,0 +1,16 @@
1
+ export const customer = {
2
+ integration_id: 'customer-api',
3
+ package: {
4
+ name: 'customer-cli-starter',
5
+ binary: 'customer-cli',
6
+ display_name: 'Customer CLI',
7
+ },
8
+ api: {
9
+ base_url: 'https://api.example.com',
10
+ auth: {
11
+ type: 'bearer_token',
12
+ environment_variable: 'CUSTOMER_API_TOKEN',
13
+ login_prompt: 'Paste your API token',
14
+ },
15
+ },
16
+ } as const;
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env node
2
+ import { createProgram, runCli } from './cli.js';
3
+ import { registerAuthCommands } from './auth/commands.js';
4
+ import { registerCommands } from './commands.js';
5
+ import { registerLifecycleCommands } from './lifecycle/commands.js';
6
+
7
+ const program = createProgram();
8
+ registerAuthCommands(program);
9
+ registerLifecycleCommands(program);
10
+ registerCommands(program);
11
+ await runCli(program, process.argv.slice(2));
@@ -0,0 +1,344 @@
1
+ import { execFile } from 'node:child_process';
2
+ import { homedir } from 'node:os';
3
+ import { resolve } from 'node:path';
4
+ import { createInterface } from 'node:readline/promises';
5
+ import { promisify } from 'node:util';
6
+ import type { Command } from 'commander';
7
+ import { login } from '../auth/commands.js';
8
+ import { credentialStatus, keyringStore } from '../auth/credentials.js';
9
+ import { readHiddenToken } from '../auth/prompt.js';
10
+ import { customer } from '../customer.js';
11
+ import { dryRun } from '../shared/dry-run.js';
12
+ import { CliError, type ErrorDetails } from '../shared/errors.js';
13
+ import { printSuccess } from '../shared/output.js';
14
+ import {
15
+ CompatibilitySkillError,
16
+ inspectCompatibilityLinks,
17
+ inspectDiscoveryCopies,
18
+ PartialSkillInstallationError,
19
+ resolveDestinations,
20
+ SkillInstallerError,
21
+ synchronizeSkillSuite,
22
+ type InstallScope,
23
+ type SkillDestination,
24
+ } from './installer/index.js';
25
+ import { bundledSuite, packageIdentity } from './config.js';
26
+
27
+ const execute = promisify(execFile);
28
+ const sharedAgents = ['codex', 'claude', 'opencode', 'cursor'];
29
+ type LifecycleSelection = {
30
+ json: boolean;
31
+ scope?: InstallScope;
32
+ projectDir?: string;
33
+ };
34
+
35
+ function lifecycleError(error: SkillInstallerError): CliError {
36
+ const details: ErrorDetails | undefined =
37
+ error instanceof PartialSkillInstallationError
38
+ ? {
39
+ destinations: error.receipt.destinations.map((destination) => ({
40
+ agents: destination.agents,
41
+ scope: destination.scope,
42
+ location: destination.location,
43
+ status: destination.status,
44
+ ...(destination.code ? { code: destination.code } : {}),
45
+ })),
46
+ }
47
+ : error instanceof CompatibilitySkillError
48
+ ? { destinations: [error.destination] }
49
+ : undefined;
50
+ return new CliError({
51
+ code: error.code,
52
+ message: error.message,
53
+ hint: error.hint,
54
+ ...(details ? { details } : {}),
55
+ });
56
+ }
57
+
58
+ function parseScope(value: string | undefined): InstallScope | undefined {
59
+ if (value === undefined) return undefined;
60
+ if (value !== 'project' && value !== 'global')
61
+ throw new CliError({
62
+ code: 'INVALID_INPUT',
63
+ message: `Unsupported scope: ${value}`,
64
+ hint: 'Choose project or global.',
65
+ exitCode: 2,
66
+ });
67
+ return value;
68
+ }
69
+
70
+ async function completeSelection(input: LifecycleSelection): Promise<LifecycleSelection> {
71
+ if (input.scope && (input.scope !== 'project' || input.projectDir)) return input;
72
+ if (input.json || !process.stdin.isTTY || !process.stdout.isTTY)
73
+ throw new CliError({
74
+ code: 'INVALID_INPUT',
75
+ message: 'Setup requires --scope, plus --project-dir for project scope.',
76
+ hint: 'Run setup --help for valid inputs.',
77
+ exitCode: 2,
78
+ });
79
+ const prompt = createInterface({ input: process.stdin, output: process.stderr });
80
+ try {
81
+ const chosenScope =
82
+ input.scope ?? parseScope((await prompt.question('Scope [project]: ')).trim() || 'project');
83
+ const projectDir =
84
+ chosenScope === 'project'
85
+ ? (input.projectDir ??
86
+ ((await prompt.question(`Project directory [${process.cwd()}]: `)).trim() ||
87
+ process.cwd()))
88
+ : undefined;
89
+ return { ...input, scope: chosenScope, projectDir };
90
+ } finally {
91
+ prompt.close();
92
+ }
93
+ }
94
+
95
+ async function authentication() {
96
+ try {
97
+ const value = await credentialStatus(keyringStore());
98
+ return value.configured
99
+ ? { status: 'connected' as const }
100
+ : { status: 'missing' as const, hint: `Run ${(await packageIdentity()).binary} login.` };
101
+ } catch {
102
+ return {
103
+ status: 'unavailable' as const,
104
+ hint: 'Check access to the operating system credential store and try again.',
105
+ };
106
+ }
107
+ }
108
+
109
+ async function offerLogin(state: Awaited<ReturnType<typeof authentication>>) {
110
+ if (state.status !== 'missing' || !process.stdin.isTTY || !process.stdout.isTTY) return state;
111
+ const prompt = createInterface({ input: process.stdin, output: process.stderr });
112
+ try {
113
+ const answer = (await prompt.question(`Run ${customer.package.binary} login now? [Y/n]: `))
114
+ .trim()
115
+ .toLowerCase();
116
+ if (answer === 'n' || answer === 'no') return state;
117
+ } finally {
118
+ prompt.close();
119
+ }
120
+ await login(keyringStore(), () => readHiddenToken(customer.api.auth.login_prompt));
121
+ return { status: 'connected' as const };
122
+ }
123
+
124
+ async function setup(input: { json: boolean; scope?: InstallScope; projectDir?: string }) {
125
+ input = await completeSelection(input);
126
+ if (!input.scope) throw new TypeError('Setup selection is incomplete');
127
+ const suite = await bundledSuite();
128
+ const projectDir = resolve(input.projectDir ?? process.cwd());
129
+ const copies = await inspectDiscoveryCopies({
130
+ suite,
131
+ projectDir,
132
+ homeDir: homedir(),
133
+ });
134
+ const conflicts = copies.filter(
135
+ (copy) => copy.state === 'unmanaged' || copy.state === 'different_owner',
136
+ );
137
+ if (conflicts.length > 0)
138
+ throw new CliError({
139
+ code: 'SKILL_CONFLICT',
140
+ message: 'A same-named Skill is not managed by this CLI.',
141
+ hint: 'Remove the conflicting Skill or choose another destination.',
142
+ details: {
143
+ destinations: conflicts.map(({ agent, scope, location, skill, state }) => ({
144
+ agent,
145
+ scope,
146
+ location,
147
+ skill,
148
+ state,
149
+ })),
150
+ },
151
+ });
152
+ const destinations = new Map<string, SkillDestination>();
153
+ for (const destination of [
154
+ ...resolveDestinations({
155
+ scope: input.scope,
156
+ projectDir,
157
+ homeDir: homedir(),
158
+ }),
159
+ ])
160
+ destinations.set(destination.root, destination);
161
+ try {
162
+ const compatibility = await inspectCompatibilityLinks(suite, [...destinations.values()]);
163
+ if (dryRun.getStore())
164
+ return {
165
+ dry_run: true,
166
+ effect: 'Install managed Skills',
167
+ summary: 'Would install managed Skills in the shared Agent Skills directory.',
168
+ checks: ['Installation scope', 'Skill conflicts', 'Compatibility links'],
169
+ not_checked: ['Destination write permissions', 'Credential validity'],
170
+ scope: input.scope,
171
+ project_dir: input.projectDir,
172
+ destinations: [
173
+ ...destinations.values(),
174
+ ...compatibility.map(({ agents, scope, location, actions }) => ({
175
+ agents,
176
+ scope,
177
+ location,
178
+ actions: actions.map(({ action, name }) => ({ action, skill: name })),
179
+ })),
180
+ ],
181
+ };
182
+ const receipt = await synchronizeSkillSuite(suite, [...destinations.values()]);
183
+ let auth = await authentication();
184
+ if (!input.json) auth = await offerLogin(auth);
185
+ return {
186
+ ready: auth.status === 'connected',
187
+ skills: {
188
+ status: 'installed' as const,
189
+ package_version: suite.version,
190
+ destinations: receipt.destinations.map((destination) => ({
191
+ ...destination,
192
+ skill_hashes: suite.skills,
193
+ })),
194
+ },
195
+ authentication: auth,
196
+ };
197
+ } catch (error) {
198
+ if (error instanceof SkillInstallerError) throw lifecycleError(error);
199
+ throw error;
200
+ }
201
+ }
202
+
203
+ async function status(projectDir = process.cwd()) {
204
+ const suite = await bundledSuite();
205
+ const copies = await inspectDiscoveryCopies({
206
+ suite,
207
+ projectDir: resolve(projectDir),
208
+ homeDir: homedir(),
209
+ });
210
+ const uniqueCopies = [
211
+ ...new Map(copies.map((copy) => [`${copy.root}:${copy.skill}`, copy])).values(),
212
+ ];
213
+ const conflicts = uniqueCopies.some(
214
+ (copy) => copy.state === 'unmanaged' || copy.state === 'different_owner',
215
+ );
216
+ const outdated = uniqueCopies.some(
217
+ (copy) => copy.state === 'outdated' || copy.state === 'managed_modified',
218
+ );
219
+ const rootsBySkill = new Map<string, Set<string>>();
220
+ for (const copy of uniqueCopies) {
221
+ const roots = rootsBySkill.get(copy.skill) ?? new Set<string>();
222
+ roots.add(copy.root);
223
+ rootsBySkill.set(copy.skill, roots);
224
+ }
225
+ const shadowed = [...rootsBySkill.values()].some((roots) => roots.size > 1);
226
+ const present = new Set(uniqueCopies.map((copy) => copy.skill));
227
+ const expected = Object.keys(suite.skills);
228
+ const scopes = [...new Set(uniqueCopies.map((copy) => copy.scope))];
229
+ let compatibilityMissing = false;
230
+ let compatibilityConflict = false;
231
+ for (const scope of scopes) {
232
+ try {
233
+ const plans = await inspectCompatibilityLinks(
234
+ suite,
235
+ resolveDestinations({ scope, projectDir: resolve(projectDir), homeDir: homedir() }),
236
+ );
237
+ compatibilityMissing ||= plans.some((plan) =>
238
+ plan.actions.some((action) => action.action !== 'reuse'),
239
+ );
240
+ } catch {
241
+ compatibilityConflict = true;
242
+ }
243
+ }
244
+ const skillStatus =
245
+ conflicts || compatibilityConflict
246
+ ? 'conflicting'
247
+ : shadowed
248
+ ? 'shadowed'
249
+ : outdated
250
+ ? 'outdated'
251
+ : present.size === 0
252
+ ? 'missing'
253
+ : expected.every((name) => present.has(name))
254
+ ? compatibilityMissing
255
+ ? 'partial'
256
+ : 'installed'
257
+ : 'partial';
258
+ const auth = await authentication();
259
+ return {
260
+ ready: skillStatus === 'installed' && auth.status === 'connected',
261
+ skills: {
262
+ status: skillStatus,
263
+ package_version: suite.version,
264
+ destinations: uniqueCopies.map(({ scope, location }) => ({
265
+ agents: sharedAgents,
266
+ scope,
267
+ location,
268
+ skill_hashes: suite.skills,
269
+ })),
270
+ },
271
+ authentication: auth,
272
+ };
273
+ }
274
+
275
+ async function update(input: { json: boolean; scope?: InstallScope; projectDir?: string }) {
276
+ input = await completeSelection(input);
277
+ if (!input.scope) throw new TypeError('Update selection is incomplete');
278
+ if (dryRun.getStore())
279
+ return {
280
+ dry_run: true,
281
+ effect: 'Update npm package and managed Skills',
282
+ summary: 'Would update the npm package and shared managed Skills.',
283
+ checks: ['Installation scope'],
284
+ not_checked: ['Registry availability', 'Destination write permissions'],
285
+ scope: input.scope,
286
+ project_dir: input.projectDir,
287
+ };
288
+ const previous = await packageIdentity();
289
+ try {
290
+ await execute('npm', ['install', '--global', `${previous.name}@latest`], {
291
+ encoding: 'utf8',
292
+ timeout: 120_000,
293
+ });
294
+ } catch {
295
+ throw new CliError({
296
+ code: 'UPDATE_FAILED',
297
+ message: 'The npm package update failed.',
298
+ hint: `Check npm access, then retry ${previous.binary} update.`,
299
+ });
300
+ }
301
+ const current = await packageIdentity();
302
+ const result = await setup({ ...input, json: true });
303
+ return { previous_version: previous.version, version: current.version, ...result };
304
+ }
305
+
306
+ function lifecycleOptions(command: Command): Command {
307
+ return command
308
+ .option('--scope <scope>', 'Installation scope: project or global')
309
+ .option('--project-dir <path>', 'Project directory for project scope');
310
+ }
311
+
312
+ export function registerLifecycleCommands(program: Command): void {
313
+ lifecycleOptions(program.command('setup').description("Install this CLI's Agent Skills")).action(
314
+ async (options: { scope?: string; projectDir?: string }) => {
315
+ printSuccess(
316
+ await setup({
317
+ json: Boolean(program.opts().json || program.opts().dryRun),
318
+ scope: parseScope(options.scope),
319
+ projectDir: options.projectDir,
320
+ }),
321
+ 'setup',
322
+ );
323
+ },
324
+ );
325
+ program
326
+ .command('status')
327
+ .description('Show Skill installation and authentication status')
328
+ .option('--project-dir <path>', 'Project directory to inspect')
329
+ .action(async (options: { projectDir?: string }) =>
330
+ printSuccess(await status(options.projectDir), 'status'),
331
+ );
332
+ lifecycleOptions(
333
+ program.command('update').description('Update the npm CLI and managed Skills'),
334
+ ).action(async (options: { scope?: string; projectDir?: string }) => {
335
+ printSuccess(
336
+ await update({
337
+ json: Boolean(program.opts().json || program.opts().dryRun),
338
+ scope: parseScope(options.scope),
339
+ projectDir: options.projectDir,
340
+ }),
341
+ 'update',
342
+ );
343
+ });
344
+ }
@@ -0,0 +1,32 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { fileURLToPath } from 'node:url';
3
+ import { inspectBundledSuite } from './installer/index.js';
4
+
5
+ function isRecord(value: unknown): value is Record<string, unknown> {
6
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
7
+ }
8
+
9
+ export async function packageIdentity(): Promise<{
10
+ name: string;
11
+ binary: string;
12
+ version: string;
13
+ }> {
14
+ const value: unknown = JSON.parse(
15
+ await readFile(new URL('../../package.json', import.meta.url), 'utf8'),
16
+ );
17
+ if (!isRecord(value) || typeof value.name !== 'string' || typeof value.version !== 'string')
18
+ throw new TypeError('CLI package identity is invalid');
19
+ if (!isRecord(value.bin) || Object.keys(value.bin).length !== 1)
20
+ throw new TypeError('CLI package binary is invalid');
21
+ return { name: value.name, binary: Object.keys(value.bin)[0], version: value.version };
22
+ }
23
+
24
+ export async function bundledSuite() {
25
+ const identity = await packageIdentity();
26
+ return inspectBundledSuite({
27
+ sourceRoot: fileURLToPath(new URL('../../skills', import.meta.url)),
28
+ packageName: identity.name,
29
+ binary: identity.binary,
30
+ version: identity.version,
31
+ });
32
+ }
@@ -0,0 +1,32 @@
1
+ import { AsyncLocalStorage } from 'node:async_hooks';
2
+ import { printSuccess } from './output.js';
3
+
4
+ export const dryRun = new AsyncLocalStorage<boolean>();
5
+ export class DryRunComplete extends Error {}
6
+
7
+ export interface WorkflowPreview {
8
+ summary: string;
9
+ steps: Array<{ summary: string; depends_on_response?: boolean }>;
10
+ }
11
+
12
+ export function previewWorkflow(workflow: WorkflowPreview, command: string): void {
13
+ if (!dryRun.getStore()) return;
14
+ printSuccess(
15
+ {
16
+ dry_run: true,
17
+ summary: workflow.summary,
18
+ steps: workflow.steps.map((step) => ({
19
+ summary: step.summary,
20
+ status: step.depends_on_response ? 'resolved_during_execution' : 'planned',
21
+ })),
22
+ checks: ['Local command validation completed'],
23
+ not_checked: [
24
+ 'API permissions and server state',
25
+ 'Response-dependent values',
26
+ 'Execution results',
27
+ ],
28
+ },
29
+ command,
30
+ );
31
+ throw new DryRunComplete();
32
+ }
@@ -0,0 +1,162 @@
1
+ type JsonValue = boolean | number | string | null | JsonValue[] | { [key: string]: JsonValue };
2
+ export type ErrorDetails = Readonly<Record<string, JsonValue>>;
3
+
4
+ type ErrorCode =
5
+ | 'UNKNOWN_COMMAND'
6
+ | 'INVALID_INPUT'
7
+ | 'INVALID_CONFIG'
8
+ | 'AUTH_REQUIRED'
9
+ | 'AUTH_FAILED'
10
+ | 'CREDENTIAL_STORE_UNAVAILABLE'
11
+ | 'UPSTREAM_ERROR'
12
+ | 'REQUEST_TIMEOUT'
13
+ | 'REQUEST_FAILED'
14
+ | 'FILE_WRITE_FAILED'
15
+ | 'SKILL_CONFLICT'
16
+ | 'SKILL_PATH_UNSAFE'
17
+ | 'SKILL_INSTALLATION_BUSY'
18
+ | 'SKILL_INSTALLATION_FAILED'
19
+ | 'UPDATE_FAILED';
20
+
21
+ export interface ErrorDefinition {
22
+ code: ErrorCode;
23
+ message: string;
24
+ hint?: string;
25
+ details?: ErrorDetails;
26
+ retryable?: boolean;
27
+ exitCode?: number;
28
+ }
29
+
30
+ export const cliErrors = {
31
+ unknownCommand: {
32
+ code: 'UNKNOWN_COMMAND',
33
+ message: 'Invalid command input; see --help for supported commands and options.',
34
+ hint: 'Run the CLI with --help to see available commands.',
35
+ details: { reason: 'unknown_command' },
36
+ exitCode: 2,
37
+ },
38
+ invalidInput: (message: string): ErrorDefinition => ({
39
+ code: 'INVALID_INPUT',
40
+ message,
41
+ hint: 'Run the command with --help to see valid inputs.',
42
+ details: { reason: 'invalid_arguments' },
43
+ exitCode: 2,
44
+ }),
45
+ invalidLoopbackUrl: {
46
+ code: 'INVALID_CONFIG',
47
+ message: 'Provide a valid loopback --base-url.',
48
+ hint: 'Use an HTTP or HTTPS URL on localhost, 127.0.0.1, or ::1.',
49
+ details: { reason: 'invalid_loopback_url' },
50
+ exitCode: 2,
51
+ },
52
+ loopbackUrlNotPermitted: {
53
+ code: 'INVALID_CONFIG',
54
+ message: 'Only credential-free loopback mock URLs are supported.',
55
+ hint: 'Remove credentials and use localhost, 127.0.0.1, or ::1.',
56
+ details: { reason: 'loopback_url_not_permitted' },
57
+ exitCode: 2,
58
+ },
59
+ customerApiConfigurationInvalid: {
60
+ code: 'INVALID_CONFIG',
61
+ message: 'Customer API configuration is invalid.',
62
+ hint: 'Regenerate the CLI from a credential-free HTTPS API URL.',
63
+ details: { reason: 'customer_api_url_invalid' },
64
+ exitCode: 2,
65
+ },
66
+ operationPathInvalid: {
67
+ code: 'INVALID_CONFIG',
68
+ message: 'Operation path is invalid.',
69
+ hint: 'Regenerate the CLI from a valid API operation path.',
70
+ details: { reason: 'operation_path_invalid' },
71
+ exitCode: 2,
72
+ },
73
+ operationOriginMismatch: {
74
+ code: 'INVALID_CONFIG',
75
+ message: 'Operation must use the configured API origin.',
76
+ hint: 'Regenerate the CLI and verify the configured API origin.',
77
+ details: { reason: 'operation_origin_mismatch' },
78
+ exitCode: 2,
79
+ },
80
+ invalidToken: {
81
+ code: 'INVALID_INPUT',
82
+ message: 'API token is invalid.',
83
+ hint: 'Paste a non-empty API token without line breaks.',
84
+ details: { reason: 'invalid_token' },
85
+ exitCode: 2,
86
+ },
87
+ credentialStoreUnavailable: {
88
+ code: 'CREDENTIAL_STORE_UNAVAILABLE',
89
+ message: 'The operating system credential store is unavailable.',
90
+ hint: 'Check access to the system credential store and try again.',
91
+ exitCode: 2,
92
+ },
93
+ authRequired: (binary: string): ErrorDefinition => ({
94
+ code: 'AUTH_REQUIRED',
95
+ message: `Run ${binary} login before using this command.`,
96
+ hint: `Run ${binary} login or provide the configured token environment variable.`,
97
+ exitCode: 2,
98
+ }),
99
+ authFailed: {
100
+ code: 'AUTH_FAILED',
101
+ message: 'API authentication failed.',
102
+ hint: 'Check or replace the configured API token, then retry.',
103
+ },
104
+ redirectNotAllowed: {
105
+ code: 'REQUEST_FAILED',
106
+ message: 'Redirects are not allowed.',
107
+ hint: 'Verify the configured API URL and operation path.',
108
+ details: { reason: 'redirect_rejected' },
109
+ },
110
+ upstream: (status: number): ErrorDefinition => ({
111
+ code: 'UPSTREAM_ERROR',
112
+ message: `API returned HTTP ${String(status)}.`,
113
+ hint: 'Check the API status and request inputs, then retry when appropriate.',
114
+ details: { status },
115
+ }),
116
+ timeout: {
117
+ code: 'REQUEST_TIMEOUT',
118
+ message: 'Request timed out.',
119
+ hint: 'Retry the command. If it continues to time out, check the API status.',
120
+ retryable: true,
121
+ },
122
+ requestFailed: {
123
+ code: 'REQUEST_FAILED',
124
+ message: 'Could not reach the customer API.',
125
+ hint: 'Check network access and the customer API status, then retry.',
126
+ retryable: true,
127
+ },
128
+ invalidJson: {
129
+ code: 'REQUEST_FAILED',
130
+ message: 'API returned invalid JSON.',
131
+ hint: 'Check that the selected operation returns JSON and retry.',
132
+ details: { reason: 'invalid_json' },
133
+ },
134
+ internal: {
135
+ code: 'REQUEST_FAILED',
136
+ message: 'Request could not be completed.',
137
+ hint: 'Retry the command. If it fails again, inspect the API status.',
138
+ },
139
+ fileWriteFailed: {
140
+ code: 'FILE_WRITE_FAILED',
141
+ message: 'Cannot write output; choose a new, writable file path.',
142
+ hint: 'Choose a path that does not exist in a writable directory.',
143
+ },
144
+ } as const;
145
+
146
+ export class CliError extends Error {
147
+ readonly code: ErrorCode;
148
+ readonly hint: string | undefined;
149
+ readonly details: ErrorDetails | undefined;
150
+ readonly retryable: boolean;
151
+ readonly exitCode: number;
152
+
153
+ constructor(definition: ErrorDefinition) {
154
+ super(definition.message);
155
+ this.name = 'CliError';
156
+ this.code = definition.code;
157
+ this.hint = definition.hint ?? 'Run the command with --help to check the required inputs.';
158
+ this.details = definition.details;
159
+ this.retryable = definition.retryable ?? false;
160
+ this.exitCode = definition.exitCode ?? 1;
161
+ }
162
+ }