@miakapp/cli 4.0.0-alpha.3 → 4.0.0-alpha.5

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 (62) hide show
  1. package/assets/agent-guide.md +5 -5
  2. package/dist/agent-pack.d.ts +86 -0
  3. package/dist/agent-pack.d.ts.map +1 -0
  4. package/dist/agent-pack.js +202 -0
  5. package/dist/agent-pack.js.map +1 -0
  6. package/dist/artifact.d.ts +32 -0
  7. package/dist/artifact.d.ts.map +1 -0
  8. package/dist/artifact.js +328 -0
  9. package/dist/artifact.js.map +1 -0
  10. package/dist/control-plane.d.ts +37 -0
  11. package/dist/control-plane.d.ts.map +1 -0
  12. package/dist/control-plane.js +127 -0
  13. package/dist/control-plane.js.map +1 -0
  14. package/dist/discovery.d.ts +95 -0
  15. package/dist/discovery.d.ts.map +1 -0
  16. package/dist/discovery.js +389 -0
  17. package/dist/discovery.js.map +1 -0
  18. package/dist/errors.d.ts +37 -0
  19. package/dist/errors.d.ts.map +1 -0
  20. package/dist/errors.js +56 -0
  21. package/dist/errors.js.map +1 -0
  22. package/dist/internal/http.d.ts +35 -0
  23. package/dist/internal/http.d.ts.map +1 -0
  24. package/dist/internal/http.js +125 -0
  25. package/dist/internal/http.js.map +1 -0
  26. package/dist/internal/json.d.ts +15 -0
  27. package/dist/internal/json.d.ts.map +1 -0
  28. package/dist/internal/json.js +227 -0
  29. package/dist/internal/json.js.map +1 -0
  30. package/dist/internal/names.d.ts +45 -0
  31. package/dist/internal/names.d.ts.map +1 -0
  32. package/dist/internal/names.js +98 -0
  33. package/dist/internal/names.js.map +1 -0
  34. package/dist/internal/requirements.d.ts +10 -0
  35. package/dist/internal/requirements.d.ts.map +1 -0
  36. package/dist/internal/requirements.js +50 -0
  37. package/dist/internal/requirements.js.map +1 -0
  38. package/dist/internal/yaml.d.ts +26 -0
  39. package/dist/internal/yaml.d.ts.map +1 -0
  40. package/dist/internal/yaml.js +184 -0
  41. package/dist/internal/yaml.js.map +1 -0
  42. package/dist/main.d.ts +88 -0
  43. package/dist/main.d.ts.map +1 -0
  44. package/dist/main.js +698 -0
  45. package/dist/main.js.map +1 -0
  46. package/dist/mcp.d.ts +93 -0
  47. package/dist/mcp.d.ts.map +1 -0
  48. package/dist/mcp.js +539 -0
  49. package/dist/mcp.js.map +1 -0
  50. package/dist/project.d.ts +25 -0
  51. package/dist/project.d.ts.map +1 -0
  52. package/dist/project.js +90 -0
  53. package/dist/project.js.map +1 -0
  54. package/dist/publication.d.ts +95 -0
  55. package/dist/publication.d.ts.map +1 -0
  56. package/dist/publication.js +408 -0
  57. package/dist/publication.js.map +1 -0
  58. package/dist/version.d.ts +15 -0
  59. package/dist/version.d.ts.map +1 -0
  60. package/dist/version.js +15 -0
  61. package/dist/version.js.map +1 -0
  62. package/package.json +2 -1
package/dist/main.js ADDED
@@ -0,0 +1,698 @@
1
+ /**
2
+ * Agent-first command surface.
3
+ *
4
+ * The CLI builds, validates, publishes and rolls back one home component. Git
5
+ * stays the user's: this tool never writes history, never rewrites sources it
6
+ * did not generate and never invents a control-plane endpoint. Every command is
7
+ * a pure function of the project file, the artifact bytes on disk and the
8
+ * arguments it was given.
9
+ *
10
+ * Two properties matter more than ergonomics here, because the usual caller is
11
+ * a coding agent rather than a person:
12
+ *
13
+ * - one stable exit code and one stable failure kind per outcome (see
14
+ * {@link EXIT_CODE}), so a wrapper decides without parsing prose;
15
+ * - `--json`, which prints exactly one closed object on stdout.
16
+ */
17
+ import { installPack } from './agent-pack.js';
18
+ import { CLI_VERSION } from './version.js';
19
+ import { prepareArtifact } from './artifact.js';
20
+ import { exchangePublisherToken, fetchDiscovery } from './control-plane.js';
21
+ import { discoverFlows, inventoryJson } from './discovery.js';
22
+ import { CliError, EXIT_CODE, artifactError, authorizationError, projectError, usageError, } from './errors.js';
23
+ import { isDigest, isRelease, REQUIREMENT_KINDS } from './internal/names.js';
24
+ import { PROJECT_FILE, PROJECT_SCHEMA, findProjectFile, parseProject } from './project.js';
25
+ import { activateRelease, publish, readRelease, readUpload, } from './publication.js';
26
+ export { CLI_VERSION };
27
+ /**
28
+ * The Home Key is read from the environment only. A secret passed as an
29
+ * argument would land in shell history, in a process listing and in most CI
30
+ * logs, so no command accepts one.
31
+ */
32
+ export const HOME_KEY_VARIABLE = 'MIAKAPP_HOME_KEY';
33
+ const USAGE = `miakapp ${CLI_VERSION} — build, publish and roll back a Miakapp home component
34
+
35
+ Usage
36
+ miakapp <command> [options]
37
+
38
+ Commands
39
+ docs start Print the complete agent guide bundled with this CLI
40
+ init Write ${PROJECT_FILE} in the current directory
41
+ agent-pack Install the guide and the MCP wiring into a repository
42
+ discover Inventory an existing Node-RED installation offline
43
+ check Validate the project and the artifact offline
44
+ publish Upload, finalize and activate the built artifact
45
+ activate Activate an already finalized digest at a new generation
46
+ rollback Alias of activate, for returning to a known-good digest
47
+ release <sha256> Read one finalized release record
48
+ upload <uploadId> Read one upload status, to reconcile a lost request
49
+ mcp Serve these commands over MCP on stdio
50
+ help Print this text
51
+ version Print the CLI version
52
+
53
+ Common options
54
+ --json Print one machine-readable object on stdout
55
+ --project <dir> Start the ${PROJECT_FILE} search here (default: cwd)
56
+
57
+ publish options
58
+ --expected-generation <n> Generation the pointer is expected to hold (required)
59
+ --generation <n> Generation to publish (default: expected + 1)
60
+ --release <name> Override component.release from ${PROJECT_FILE}
61
+
62
+ activate / rollback options
63
+ --sha256 <digest> Finalized artifact digest (required)
64
+ --expected-generation <n> Generation the pointer is expected to hold (required)
65
+ --generation <n> Generation to publish (default: expected + 1)
66
+
67
+ discover options
68
+ --flows <path> Node-RED flows export to read (required)
69
+
70
+ mcp options
71
+ (none) Reads JSON-RPC on stdin, writes it on stdout. Every
72
+ command above becomes one tool; publish, activate
73
+ and rollback additionally require confirm: true.
74
+
75
+ agent-pack options
76
+ --dir <path> Repository to install into (default: cwd)
77
+
78
+ init options
79
+ --home <homeId> Home ID to write into ${PROJECT_FILE} (required)
80
+ --control-plane <https url> Control-plane issuer (required)
81
+ --artifact <path> Built artifact path (default: dist/component.js)
82
+ --release <name> Initial release name (default: 0.1.0)
83
+
84
+ Environment
85
+ ${HOME_KEY_VARIABLE} Home Key with the components:publish scope. Required by
86
+ publish, activate, rollback, release and upload. It is never
87
+ accepted as an argument and never printed.
88
+
89
+ Exit codes
90
+ 0 success 1 usage 2 project 3 artifact
91
+ 4 authorization 5 contract 6 conflict 7 unknown outcome
92
+ `;
93
+ const GLOBAL_FLAGS = ['json'];
94
+ const GLOBAL_OPTIONS = ['project'];
95
+ /** Exported so the MCP surface can be proved to expose every option, and no other. */
96
+ export const COMMAND_OPTIONS = {
97
+ docs: [],
98
+ init: ['home', 'control-plane', 'artifact', 'release'],
99
+ 'agent-pack': ['dir'],
100
+ discover: ['flows'],
101
+ check: [],
102
+ publish: ['expected-generation', 'generation', 'release'],
103
+ activate: ['sha256', 'expected-generation', 'generation'],
104
+ rollback: ['sha256', 'expected-generation', 'generation'],
105
+ release: [],
106
+ upload: [],
107
+ mcp: [],
108
+ help: [],
109
+ version: [],
110
+ };
111
+ /**
112
+ * Parses `--name value`, `--name=value` and bare flags.
113
+ *
114
+ * Options are a closed set per command: an unrecognized one is a usage error
115
+ * rather than a silently ignored argument, so a mistyped flag can never turn a
116
+ * publication into a different publication.
117
+ */
118
+ export function parseArguments(argv) {
119
+ const first = argv[0];
120
+ if (first === undefined || first === '--help' || first === '-h') {
121
+ return { command: 'help', options: new Map(), flags: new Set(), positional: [] };
122
+ }
123
+ if (first === '--version' || first === '-v') {
124
+ return { command: 'version', options: new Map(), flags: new Set(), positional: [] };
125
+ }
126
+ if (first.startsWith('-'))
127
+ throw usageError(`Expected a command, received ${first}`);
128
+ const command = first;
129
+ const allowed = COMMAND_OPTIONS[command];
130
+ if (allowed === undefined) {
131
+ throw usageError(`Unknown command: ${command}`, 'Run miakapp help for the command list.');
132
+ }
133
+ const options = new Map();
134
+ const flags = new Set();
135
+ const positional = [];
136
+ for (let index = 1; index < argv.length; index += 1) {
137
+ const argument = argv[index];
138
+ if (!argument.startsWith('--')) {
139
+ positional.push(argument);
140
+ continue;
141
+ }
142
+ const separator = argument.indexOf('=');
143
+ const name = separator === -1 ? argument.slice(2) : argument.slice(2, separator);
144
+ if (name === '')
145
+ throw usageError('Encountered a bare -- separator');
146
+ if (GLOBAL_FLAGS.includes(name)) {
147
+ if (separator !== -1)
148
+ throw usageError(`--${name} does not take a value`);
149
+ flags.add(name);
150
+ continue;
151
+ }
152
+ const known = GLOBAL_OPTIONS.includes(name) || allowed.includes(name);
153
+ if (!known) {
154
+ throw usageError(`Unknown option --${name} for ${command}`, 'Run miakapp help for the options this command accepts.');
155
+ }
156
+ if (options.has(name))
157
+ throw usageError(`--${name} was given twice`);
158
+ if (separator !== -1) {
159
+ options.set(name, argument.slice(separator + 1));
160
+ continue;
161
+ }
162
+ const value = argv[index + 1];
163
+ if (value === undefined || value.startsWith('--')) {
164
+ throw usageError(`--${name} requires a value`);
165
+ }
166
+ options.set(name, value);
167
+ index += 1;
168
+ }
169
+ return { command, options, flags, positional };
170
+ }
171
+ function requiredOption(invocation, name) {
172
+ const value = invocation.options.get(name);
173
+ if (value === undefined || value === '')
174
+ throw usageError(`--${name} is required`);
175
+ return value;
176
+ }
177
+ function generationOption(invocation, name) {
178
+ const raw = requiredOption(invocation, name);
179
+ if (!/^(0|[1-9][0-9]*)$/.test(raw)) {
180
+ throw usageError(`--${name} must be a decimal non-negative integer, received ${raw}`);
181
+ }
182
+ const value = Number(raw);
183
+ if (!Number.isSafeInteger(value))
184
+ throw usageError(`--${name} is above the safe integer range`);
185
+ return value;
186
+ }
187
+ /** `--generation` defaults to one above the expected generation, never higher. */
188
+ function generationPair(invocation) {
189
+ const expectedGeneration = generationOption(invocation, 'expected-generation');
190
+ const generation = invocation.options.has('generation')
191
+ ? generationOption(invocation, 'generation')
192
+ : expectedGeneration + 1;
193
+ if (generation <= expectedGeneration) {
194
+ throw usageError('--generation must be strictly above --expected-generation');
195
+ }
196
+ return { expectedGeneration, generation };
197
+ }
198
+ function digestOption(invocation, name) {
199
+ const value = requiredOption(invocation, name);
200
+ if (!isDigest(value)) {
201
+ throw usageError(`--${name} must be a SHA-256 digest as 43 base64url characters`);
202
+ }
203
+ return value;
204
+ }
205
+ function homeKey(host) {
206
+ const value = host.env(HOME_KEY_VARIABLE);
207
+ if (value === undefined || value === '') {
208
+ throw authorizationError(`${HOME_KEY_VARIABLE} is not set`, `Export a Home Key holding components:publish as ${HOME_KEY_VARIABLE}. `
209
+ + 'It is never accepted as a command-line argument.');
210
+ }
211
+ return value;
212
+ }
213
+ async function nodeFileSystem() {
214
+ const fs = await import('node:fs/promises');
215
+ return {
216
+ async read(path) {
217
+ return new Uint8Array(await fs.readFile(path));
218
+ },
219
+ async write(path, bytes) {
220
+ await fs.writeFile(path, bytes, { flag: 'wx' });
221
+ },
222
+ async replace(path, bytes) {
223
+ await fs.writeFile(path, bytes);
224
+ },
225
+ async makeDirectory(path) {
226
+ await fs.mkdir(path, { recursive: true });
227
+ },
228
+ async exists(path) {
229
+ try {
230
+ await fs.access(path);
231
+ return true;
232
+ }
233
+ catch {
234
+ return false;
235
+ }
236
+ },
237
+ };
238
+ }
239
+ async function files(host) {
240
+ return host.files ?? await nodeFileSystem();
241
+ }
242
+ async function loadProject(host, invocation) {
243
+ const filesystem = await files(host);
244
+ const start = invocation.options.get('project') ?? host.cwd();
245
+ const path = await findProjectFile(start, (candidate) => filesystem.exists(candidate));
246
+ const root = path.slice(0, path.length - PROJECT_FILE.length - 1);
247
+ let source;
248
+ try {
249
+ source = new TextDecoder('utf-8', { fatal: true }).decode(await filesystem.read(path));
250
+ }
251
+ catch {
252
+ throw projectError(`${path} is not readable as UTF-8 text`);
253
+ }
254
+ return parseProject(root, source);
255
+ }
256
+ async function loadArtifact(host, project) {
257
+ const filesystem = await files(host);
258
+ if (!await filesystem.exists(project.artifactPath)) {
259
+ throw artifactError(`No artifact at ${project.artifactPath}`, 'Build the component before publishing; the CLI never bundles sources itself.');
260
+ }
261
+ return prepareArtifact(await filesystem.read(project.artifactPath));
262
+ }
263
+ async function publicationTarget(host, project) {
264
+ const key = homeKey(host);
265
+ const options = host.fetch === undefined ? {} : { fetch: host.fetch };
266
+ const discovery = await fetchDiscovery({ issuer: project.issuer, ...options });
267
+ const token = await exchangePublisherToken(discovery, key, options);
268
+ return { issuer: discovery.issuer, homeId: project.homeId, token: token.accessToken, ...options };
269
+ }
270
+ function requirementFields(requires) {
271
+ return REQUIREMENT_KINDS.map((kind) => [`requires.${kind}`, requires[kind]]);
272
+ }
273
+ function pointerResult(summary, pointer) {
274
+ return {
275
+ summary,
276
+ fields: [
277
+ ['home', pointer.homeId],
278
+ ['generation', pointer.generation],
279
+ ['release', pointer.release],
280
+ ['sha256', pointer.sha256],
281
+ ['size', pointer.size],
282
+ ['url', pointer.url],
283
+ ...requirementFields(pointer.requires),
284
+ ],
285
+ json: {
286
+ home_id: pointer.homeId,
287
+ generation: pointer.generation,
288
+ release: pointer.release,
289
+ abi: pointer.abi,
290
+ url: pointer.url,
291
+ sha256: pointer.sha256,
292
+ size: pointer.size,
293
+ requires: pointer.requires,
294
+ },
295
+ };
296
+ }
297
+ function projectTemplate(fields) {
298
+ return `schema: ${PROJECT_SCHEMA}
299
+ home: ${fields.home}
300
+ control_plane: ${fields.controlPlane}
301
+
302
+ component:
303
+ artifact: ${fields.artifact}
304
+ release: ${fields.release}
305
+ requires:
306
+ state_read: []
307
+ event_subscribe: []
308
+ event_publish: []
309
+ call: []
310
+ presentation: []
311
+ `;
312
+ }
313
+ async function runInit(host, invocation) {
314
+ const filesystem = await files(host);
315
+ const root = invocation.options.get('project') ?? host.cwd();
316
+ const path = `${root}/${PROJECT_FILE}`;
317
+ if (await filesystem.exists(path)) {
318
+ throw projectError(`${path} already exists`, 'The CLI never overwrites a project file; edit it or remove it first.');
319
+ }
320
+ const release = invocation.options.get('release') ?? '0.1.0';
321
+ if (!isRelease(release)) {
322
+ throw usageError('--release must be 1..64 UTF-8 bytes without control characters');
323
+ }
324
+ const source = projectTemplate({
325
+ home: requiredOption(invocation, 'home'),
326
+ controlPlane: requiredOption(invocation, 'control-plane'),
327
+ artifact: invocation.options.get('artifact') ?? 'dist/component.js',
328
+ release,
329
+ });
330
+ // Parsed before it is written, so init can never emit a file check rejects.
331
+ parseProject(root, source);
332
+ await filesystem.write(path, new TextEncoder().encode(source));
333
+ return {
334
+ summary: `Wrote ${path}`,
335
+ fields: [['project', path]],
336
+ json: { project: path, schema: PROJECT_SCHEMA },
337
+ };
338
+ }
339
+ /**
340
+ * Absolute path of the guide shipped with this package.
341
+ *
342
+ * Resolved from this module rather than from the working directory: the pack is
343
+ * installed into someone else's repository, and the guide has to come from the
344
+ * installed CLI wherever that repository happens to be.
345
+ *
346
+ * Exported so a test reads the same path the command does. A test that seeded a
347
+ * fixture at a path the command never opens would prove nothing.
348
+ */
349
+ export async function guideAssetPath() {
350
+ const { fileURLToPath } = await import('node:url');
351
+ return fileURLToPath(new URL('../assets/agent-guide.md', import.meta.url));
352
+ }
353
+ /**
354
+ * Gives an agent its complete starting contract without requiring a repository,
355
+ * a network request or an MCP client. The onboarding page can therefore hand a
356
+ * person one stable command and the installed CLI remains the source of truth.
357
+ */
358
+ async function runDocs(host, invocation) {
359
+ if (invocation.positional.length !== 1 || invocation.positional[0] !== 'start') {
360
+ throw usageError('docs requires the topic start', 'Run miakapp docs start to print the complete agent guide.');
361
+ }
362
+ const path = await guideAssetPath();
363
+ let guide;
364
+ try {
365
+ const filesystem = await files(host);
366
+ guide = new TextDecoder('utf-8', { fatal: true }).decode(await filesystem.read(path));
367
+ }
368
+ catch {
369
+ throw projectError(`The packaged guide is missing or unreadable at ${path}`, 'Reinstall @miakapp/cli: the guide is bundled with the package and opens no network connection.');
370
+ }
371
+ return {
372
+ summary: 'Miakapp agent guide',
373
+ fields: [],
374
+ json: { topic: 'start', guide },
375
+ text: guide.endsWith('\n') ? guide : `${guide}\n`,
376
+ };
377
+ }
378
+ /**
379
+ * Installs the agent pack. Like `discover`, it loads no project file: the
380
+ * repository it prepares is usually one that has no V4 project yet.
381
+ */
382
+ async function runAgentPack(host, invocation) {
383
+ const filesystem = await files(host);
384
+ const root = invocation.options.get('dir') ?? host.cwd();
385
+ if (!await filesystem.exists(root)) {
386
+ throw projectError(`No directory at ${root}`, 'Point --dir at the repository to install into, or run the command inside it.');
387
+ }
388
+ const guidePath = await guideAssetPath();
389
+ let guide;
390
+ try {
391
+ guide = new TextDecoder('utf-8', { fatal: true }).decode(await filesystem.read(guidePath));
392
+ }
393
+ catch {
394
+ throw projectError(`The packaged guide is missing or unreadable at ${guidePath}`, 'Reinstall @miakapp/cli: the pack copies the guide out of the package, never off the network.');
395
+ }
396
+ const result = await installPack(filesystem, root, guide);
397
+ const changed = result.files.filter((entry) => entry.action !== 'unchanged').length;
398
+ return {
399
+ summary: changed === 0
400
+ ? `The pack in ${root} is already current`
401
+ : `Installed the agent pack in ${root}`,
402
+ fields: result.files.map((entry) => [entry.path, entry.action]),
403
+ json: {
404
+ root: result.root,
405
+ changed,
406
+ files: result.files.map((entry) => ({ path: entry.path, action: entry.action })),
407
+ },
408
+ };
409
+ }
410
+ /**
411
+ * Reads an existing installation. `discover` never loads the project file: an
412
+ * agent runs it on a house that has no V4 project yet, which is the whole point
413
+ * of the command.
414
+ */
415
+ async function runDiscover(host, invocation) {
416
+ const path = requiredOption(invocation, 'flows');
417
+ const filesystem = await files(host);
418
+ if (!await filesystem.exists(path)) {
419
+ throw projectError(`No flows export at ${path}`, 'Point --flows at the Node-RED flows.json, or at an Export > All flows download.');
420
+ }
421
+ const inventory = discoverFlows(await filesystem.read(path));
422
+ return {
423
+ summary: discoverSummary(inventory),
424
+ fields: discoverFields(inventory),
425
+ json: inventoryJson(inventory),
426
+ };
427
+ }
428
+ function counted(count, singular, plural) {
429
+ return `${count} ${count === 1 ? singular : plural}`;
430
+ }
431
+ function discoverSummary(inventory) {
432
+ const critical = inventory.findings.filter((item) => item.severity === 'critical').length;
433
+ const census = [
434
+ counted(inventory.nodeCount, 'node', 'nodes'),
435
+ counted(inventory.flows.length, 'flow', 'flows'),
436
+ counted(inventory.brokers.length, 'broker', 'brokers'),
437
+ counted(inventory.state.length, 'state path', 'state paths'),
438
+ counted(inventory.actions.length, 'action', 'actions'),
439
+ ].join(', ');
440
+ return critical === 0
441
+ ? census
442
+ : `${census} — ${counted(critical, 'finding', 'findings')} to settle before migrating`;
443
+ }
444
+ function discoverFields(inventory) {
445
+ const fields = [];
446
+ for (const home of inventory.homes) {
447
+ fields.push([`home.${home.homeId}`, `coordinator ${home.coordinatorId}`]);
448
+ }
449
+ for (const broker of inventory.brokers) {
450
+ const address = broker.port === undefined ? broker.host : `${broker.host}:${broker.port}`;
451
+ fields.push([
452
+ `broker.${broker.name === '' ? broker.id : broker.name}`,
453
+ `${address} tls=${broker.tls} in=${broker.subscribes.length} out=${broker.publishes.length}`,
454
+ ]);
455
+ }
456
+ for (const tab of inventory.flows) {
457
+ fields.push([`flow.${tab.label === '' ? tab.id : tab.label}`, `${tab.nodeCount} nodes`]);
458
+ }
459
+ if (inventory.state.length > 0) {
460
+ fields.push(['state', inventory.state.map((entry) => entry.path)]);
461
+ }
462
+ if (inventory.actions.length > 0) {
463
+ fields.push(['actions', inventory.actions.map((entry) => entry.inputId)]);
464
+ }
465
+ if (inventory.unmodelled.length > 0) {
466
+ fields.push(['unmodelled', inventory.unmodelled.map((entry) => `${entry.type}×${entry.count}`)]);
467
+ }
468
+ for (const item of inventory.findings) {
469
+ fields.push([item.severity, item.detail]);
470
+ }
471
+ return fields;
472
+ }
473
+ async function runCheck(host, invocation) {
474
+ const project = await loadProject(host, invocation);
475
+ const artifact = await loadArtifact(host, project);
476
+ const filesystem = await files(host);
477
+ const coordinator = project.coordinatorEntry;
478
+ if (coordinator !== undefined && !await filesystem.exists(coordinator)) {
479
+ throw projectError(`coordinator.entry does not exist: ${coordinator}`);
480
+ }
481
+ return {
482
+ summary: `${project.homeId} release ${project.release} is publishable`,
483
+ fields: [
484
+ ['home', project.homeId],
485
+ ['control_plane', project.issuer],
486
+ ['release', project.release],
487
+ ['artifact', project.artifactPath],
488
+ ['sha256', artifact.sha256],
489
+ ['size', artifact.size],
490
+ ['tokens', artifact.tokens],
491
+ ...requirementFields(project.requires),
492
+ ],
493
+ json: {
494
+ home_id: project.homeId,
495
+ control_plane: project.issuer,
496
+ release: project.release,
497
+ artifact: project.artifactPath,
498
+ sha256: artifact.sha256,
499
+ size: artifact.size,
500
+ tokens: artifact.tokens,
501
+ requires: project.requires,
502
+ },
503
+ };
504
+ }
505
+ async function runPublish(host, invocation) {
506
+ const { expectedGeneration, generation } = generationPair(invocation);
507
+ const project = await loadProject(host, invocation);
508
+ const release = invocation.options.get('release') ?? project.release;
509
+ if (!isRelease(release)) {
510
+ throw usageError('--release must be 1..64 UTF-8 bytes without control characters');
511
+ }
512
+ const artifact = await loadArtifact(host, project);
513
+ const target = await publicationTarget(host, project);
514
+ const { pointer } = await publish(target, artifact, {
515
+ release,
516
+ requires: project.requires,
517
+ expectedGeneration,
518
+ generation,
519
+ });
520
+ return pointerResult(`Published ${release} as generation ${pointer.generation}`, pointer);
521
+ }
522
+ async function runActivate(host, invocation) {
523
+ const sha256 = digestOption(invocation, 'sha256');
524
+ const { expectedGeneration, generation } = generationPair(invocation);
525
+ const project = await loadProject(host, invocation);
526
+ const target = await publicationTarget(host, project);
527
+ // Activation is checked against a readable finalized record first, so a typo
528
+ // in a digest fails as an artifact error instead of spending a CAS attempt.
529
+ const existing = await readRelease(target, sha256);
530
+ if (existing === undefined) {
531
+ throw artifactError(`No finalized release for ${sha256}`, 'Activate only a digest this home has already published.');
532
+ }
533
+ const pointer = await activateRelease(target, { sha256, expectedGeneration, generation });
534
+ return pointerResult(`Activated ${existing.release} as generation ${pointer.generation}`, pointer);
535
+ }
536
+ async function runRelease(host, invocation) {
537
+ const sha256 = invocation.positional[0];
538
+ if (sha256 === undefined)
539
+ throw usageError('release requires one sha256 argument');
540
+ if (!isDigest(sha256)) {
541
+ throw usageError('The release digest must be 43 base64url characters');
542
+ }
543
+ const project = await loadProject(host, invocation);
544
+ const target = await publicationTarget(host, project);
545
+ const record = await readRelease(target, sha256);
546
+ if (record === undefined) {
547
+ throw artifactError(`No finalized release for ${sha256}`);
548
+ }
549
+ return {
550
+ summary: `Release ${record.release} finalized at ${record.finalizedAt}`,
551
+ fields: [
552
+ ['release', record.release],
553
+ ['sha256', record.sha256],
554
+ ['size', record.size],
555
+ ['finalized_at', record.finalizedAt],
556
+ ...requirementFields(record.requires),
557
+ ],
558
+ json: {
559
+ release: record.release,
560
+ abi: record.abi,
561
+ sha256: record.sha256,
562
+ size: record.size,
563
+ requires: record.requires,
564
+ finalized_at: record.finalizedAt,
565
+ },
566
+ };
567
+ }
568
+ async function runUpload(host, invocation) {
569
+ const uploadId = invocation.positional[0];
570
+ if (uploadId === undefined)
571
+ throw usageError('upload requires one uploadId argument');
572
+ const project = await loadProject(host, invocation);
573
+ const target = await publicationTarget(host, project);
574
+ const state = await readUpload(target, uploadId);
575
+ return {
576
+ summary: `Upload ${state.uploadId} is ${state.status}`,
577
+ fields: [
578
+ ['upload_id', state.uploadId],
579
+ ['status', state.status],
580
+ ['release', state.release],
581
+ ['sha256', state.sha256],
582
+ ['size', state.size],
583
+ ['expires_at', state.expiresAt],
584
+ ],
585
+ json: {
586
+ upload_id: state.uploadId,
587
+ status: state.status,
588
+ release: state.release,
589
+ abi: state.abi,
590
+ sha256: state.sha256,
591
+ size: state.size,
592
+ requires: state.requires,
593
+ expires_at: state.expiresAt,
594
+ },
595
+ };
596
+ }
597
+ /**
598
+ * Runs one parsed invocation.
599
+ *
600
+ * Exported for `mcp`, which reaches the same commands without a process: a
601
+ * tool call and a command line must not be able to diverge.
602
+ */
603
+ export async function dispatch(host, invocation) {
604
+ switch (invocation.command) {
605
+ case 'docs':
606
+ return await runDocs(host, invocation);
607
+ case 'init':
608
+ return await runInit(host, invocation);
609
+ case 'agent-pack':
610
+ return await runAgentPack(host, invocation);
611
+ case 'discover':
612
+ return await runDiscover(host, invocation);
613
+ case 'check':
614
+ return await runCheck(host, invocation);
615
+ case 'publish':
616
+ return await runPublish(host, invocation);
617
+ case 'activate':
618
+ case 'rollback':
619
+ return await runActivate(host, invocation);
620
+ case 'release':
621
+ return await runRelease(host, invocation);
622
+ case 'upload':
623
+ return await runUpload(host, invocation);
624
+ default:
625
+ throw usageError(`Unknown command: ${invocation.command}`);
626
+ }
627
+ }
628
+ function renderText(result) {
629
+ if (result.text !== undefined)
630
+ return result.text;
631
+ const lines = [result.summary];
632
+ for (const [key, value] of result.fields) {
633
+ lines.push(` ${key}: ${Array.isArray(value) ? `[${value.join(', ')}]` : String(value)}`);
634
+ }
635
+ return `${lines.join('\n')}\n`;
636
+ }
637
+ function renderFailure(error, json) {
638
+ if (json) {
639
+ return `${JSON.stringify({
640
+ ok: false,
641
+ kind: error.kind,
642
+ exit_code: error.exitCode,
643
+ message: error.message,
644
+ ...(error.remedy === undefined ? {} : { remedy: error.remedy }),
645
+ })}\n`;
646
+ }
647
+ const remedy = error.remedy === undefined ? '' : `\n ${error.remedy}`;
648
+ return `miakapp: ${error.kind}: ${error.message}${remedy}\n`;
649
+ }
650
+ /**
651
+ * Runs one invocation and returns its exit code.
652
+ *
653
+ * Nothing throws out of this function: an unexpected error becomes
654
+ * `unknown_outcome`, because a CLI that crashed mid-publication cannot claim
655
+ * the control plane was left untouched.
656
+ */
657
+ export async function run(argv, host) {
658
+ let json = false;
659
+ try {
660
+ const invocation = parseArguments(argv);
661
+ json = invocation.flags.has('json');
662
+ if (invocation.command === 'help') {
663
+ host.write(json ? `${JSON.stringify({ ok: true, usage: USAGE })}\n` : USAGE);
664
+ return EXIT_CODE.success;
665
+ }
666
+ if (invocation.command === 'version') {
667
+ host.write(json ? `${JSON.stringify({ ok: true, version: CLI_VERSION })}\n` : `${CLI_VERSION}\n`);
668
+ return EXIT_CODE.success;
669
+ }
670
+ if (invocation.command === 'mcp') {
671
+ if (json)
672
+ throw usageError('mcp does not take --json; the protocol is already JSON-RPC');
673
+ const input = host.input;
674
+ if (input === undefined) {
675
+ throw usageError('mcp needs a request stream on stdin', 'An MCP client starts this command as a subprocess and speaks JSON-RPC over the pipe.');
676
+ }
677
+ // Imported here, not at the top: mcp.ts is built on this module, and the
678
+ // other commands must not pay for a protocol they never speak.
679
+ const { serve } = await import('./mcp.js');
680
+ return await serve(host, input);
681
+ }
682
+ const result = await dispatch(host, invocation);
683
+ host.write(json
684
+ ? `${JSON.stringify({ ok: true, command: invocation.command, ...result.json })}\n`
685
+ : renderText(result));
686
+ return EXIT_CODE.success;
687
+ }
688
+ catch (error) {
689
+ if (error instanceof CliError) {
690
+ host.writeError(renderFailure(error, json));
691
+ return error.exitCode;
692
+ }
693
+ const message = error instanceof Error ? error.message : 'Unrecognized failure';
694
+ host.writeError(renderFailure(new CliError('unknown_outcome', `The command ended in an unhandled failure: ${message}`, 'Reconcile with miakapp release or miakapp upload before publishing again.'), json));
695
+ return EXIT_CODE.unknown_outcome;
696
+ }
697
+ }
698
+ //# sourceMappingURL=main.js.map