@miakapp/cli 4.0.0-alpha.4 → 4.0.0-alpha.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +99 -32
- package/assets/agent-guide.md +198 -27
- package/bin/miakapp.js +5 -1
- package/dist/agent-pack.d.ts.map +1 -1
- package/dist/agent-pack.js +12 -5
- package/dist/agent-pack.js.map +1 -1
- package/dist/contexts.d.ts +59 -0
- package/dist/contexts.d.ts.map +1 -0
- package/dist/contexts.js +388 -0
- package/dist/contexts.js.map +1 -0
- package/dist/control-plane.d.ts +14 -0
- package/dist/control-plane.d.ts.map +1 -1
- package/dist/control-plane.js +33 -1
- package/dist/control-plane.js.map +1 -1
- package/dist/internal/names.d.ts +5 -0
- package/dist/internal/names.d.ts.map +1 -1
- package/dist/internal/names.js +6 -0
- package/dist/internal/names.js.map +1 -1
- package/dist/main.d.ts +16 -3
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +555 -49
- package/dist/main.js.map +1 -1
- package/dist/mcp.d.ts +7 -1
- package/dist/mcp.d.ts.map +1 -1
- package/dist/mcp.js +167 -26
- package/dist/mcp.js.map +1 -1
- package/dist/pairing.d.ts +27 -0
- package/dist/pairing.d.ts.map +1 -0
- package/dist/pairing.js +148 -0
- package/dist/pairing.js.map +1 -0
- package/dist/project.d.ts +3 -1
- package/dist/project.d.ts.map +1 -1
- package/dist/project.js +33 -6
- package/dist/project.js.map +1 -1
- package/dist/publication.d.ts +20 -5
- package/dist/publication.d.ts.map +1 -1
- package/dist/publication.js +58 -16
- package/dist/publication.js.map +1 -1
- package/dist/terminal.d.ts +10 -0
- package/dist/terminal.d.ts.map +1 -0
- package/dist/terminal.js +70 -0
- package/dist/terminal.js.map +1 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/dist/main.js
CHANGED
|
@@ -14,22 +14,28 @@
|
|
|
14
14
|
* {@link EXIT_CODE}), so a wrapper decides without parsing prose;
|
|
15
15
|
* - `--json`, which prints exactly one closed object on stdout.
|
|
16
16
|
*/
|
|
17
|
+
import { homedir, hostname } from 'node:os';
|
|
17
18
|
import { installPack } from './agent-pack.js';
|
|
18
19
|
import { CLI_VERSION } from './version.js';
|
|
19
20
|
import { prepareArtifact } from './artifact.js';
|
|
20
|
-
import { exchangePublisherToken, fetchDiscovery } from './control-plane.js';
|
|
21
|
+
import { exchangePublisherToken, fetchDiscovery, homeKeyId, homeUrl } from './control-plane.js';
|
|
22
|
+
import { configDirectory, contextNameError, isContextName, mutateContexts, readContexts, readCredential, } from './contexts.js';
|
|
21
23
|
import { discoverFlows, inventoryJson } from './discovery.js';
|
|
22
|
-
import { CliError, EXIT_CODE, artifactError, authorizationError, projectError, usageError, } from './errors.js';
|
|
24
|
+
import { CliError, EXIT_CODE, artifactError, authorizationError, contractError, projectError, usageError, } from './errors.js';
|
|
23
25
|
import { isDigest, isRelease, REQUIREMENT_KINDS } from './internal/names.js';
|
|
26
|
+
import { DEFAULT_ISSUER, PAIRING_PAGE, normalizeCode, pairingIssuer, redeemPairingCode, validateLabel, } from './pairing.js';
|
|
24
27
|
import { PROJECT_FILE, PROJECT_SCHEMA, findProjectFile, parseProject } from './project.js';
|
|
25
|
-
import { activateRelease, publish, readRelease, readUpload, } from './publication.js';
|
|
28
|
+
import { activateRelease, publish, readPointer, readRelease, readUpload, } from './publication.js';
|
|
26
29
|
export { CLI_VERSION };
|
|
27
30
|
/**
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
* logs, so no command
|
|
31
|
+
* A Home Key comes from a paired context in `~/.miakapp` or, for CI and
|
|
32
|
+
* compatibility, from this variable. A secret passed as an argument would land
|
|
33
|
+
* in shell history, in a process listing and in most CI logs, so no command
|
|
34
|
+
* accepts one.
|
|
31
35
|
*/
|
|
32
36
|
export const HOME_KEY_VARIABLE = 'MIAKAPP_HOME_KEY';
|
|
37
|
+
/** Selects a stored context for one invocation, like `--context`. */
|
|
38
|
+
export const CONTEXT_VARIABLE = 'MIAKAPP_CONTEXT';
|
|
33
39
|
const USAGE = `miakapp ${CLI_VERSION} — build, publish and roll back a Miakapp home component
|
|
34
40
|
|
|
35
41
|
Usage
|
|
@@ -37,10 +43,16 @@ Usage
|
|
|
37
43
|
|
|
38
44
|
Commands
|
|
39
45
|
docs start Print the complete agent guide bundled with this CLI
|
|
46
|
+
pair Redeem a one-time pairing code into a stored context
|
|
47
|
+
context list List stored contexts (never prints a key)
|
|
48
|
+
context show [name] Show one context, the current one by default
|
|
49
|
+
context use <name> Make a context current
|
|
50
|
+
context remove <name> Delete a context and its stored key
|
|
40
51
|
init Write ${PROJECT_FILE} in the current directory
|
|
41
52
|
agent-pack Install the guide and the MCP wiring into a repository
|
|
42
53
|
discover Inventory an existing Node-RED installation offline
|
|
43
54
|
check Validate the project and the artifact offline
|
|
55
|
+
status Read the live generation and release of the home
|
|
44
56
|
publish Upload, finalize and activate the built artifact
|
|
45
57
|
activate Activate an already finalized digest at a new generation
|
|
46
58
|
rollback Alias of activate, for returning to a known-good digest
|
|
@@ -53,15 +65,25 @@ Commands
|
|
|
53
65
|
Common options
|
|
54
66
|
--json Print one machine-readable object on stdout
|
|
55
67
|
--project <dir> Start the ${PROJECT_FILE} search here (default: cwd)
|
|
68
|
+
--context <name> Use this stored context (status, publish, activate,
|
|
69
|
+
rollback, release, upload, init)
|
|
70
|
+
|
|
71
|
+
pair options
|
|
72
|
+
(code) Read from stdin: hidden prompt on a terminal, first
|
|
73
|
+
line of a pipe otherwise. Preferred: never echoed.
|
|
74
|
+
--code <code> Pass the code explicitly (lands in shell history)
|
|
75
|
+
--issuer <https url> Control plane to redeem at (default: ${DEFAULT_ISSUER})
|
|
76
|
+
--label <label> Key label shown to the owner (default: miakapp-cli@host)
|
|
77
|
+
--name <context> Context name (default: the paired home ID)
|
|
56
78
|
|
|
57
79
|
publish options
|
|
58
|
-
--expected-generation <n> Generation the pointer
|
|
80
|
+
--expected-generation <n> Generation the pointer holds (default: read live)
|
|
59
81
|
--generation <n> Generation to publish (default: expected + 1)
|
|
60
82
|
--release <name> Override component.release from ${PROJECT_FILE}
|
|
61
83
|
|
|
62
84
|
activate / rollback options
|
|
63
85
|
--sha256 <digest> Finalized artifact digest (required)
|
|
64
|
-
--expected-generation <n> Generation the pointer
|
|
86
|
+
--expected-generation <n> Generation the pointer holds (default: read live)
|
|
65
87
|
--generation <n> Generation to publish (default: expected + 1)
|
|
66
88
|
|
|
67
89
|
discover options
|
|
@@ -76,15 +98,22 @@ agent-pack options
|
|
|
76
98
|
--dir <path> Repository to install into (default: cwd)
|
|
77
99
|
|
|
78
100
|
init options
|
|
79
|
-
--home <homeId> Home ID
|
|
80
|
-
--control-plane <https url> Control-plane issuer (
|
|
101
|
+
--home <homeId> Home ID (default: from the selected context)
|
|
102
|
+
--control-plane <https url> Control-plane issuer (default: from the context)
|
|
81
103
|
--artifact <path> Built artifact path (default: dist/component.js)
|
|
82
104
|
--release <name> Initial release name (default: 0.1.0)
|
|
83
105
|
|
|
106
|
+
Credentials, highest precedence first
|
|
107
|
+
1. --context <name> a context stored by miakapp pair
|
|
108
|
+
2. ${CONTEXT_VARIABLE}=<name> the same, from the environment
|
|
109
|
+
3. ${HOME_KEY_VARIABLE} a raw Home Key, for CI and compatibility
|
|
110
|
+
4. the stored context whose home and issuer match ${PROJECT_FILE}
|
|
111
|
+
(the current context wins a tie)
|
|
112
|
+
A stored context whose home or issuer differs from ${PROJECT_FILE} is refused,
|
|
113
|
+
so a key can never publish into another home. Keys are never printed.
|
|
114
|
+
|
|
84
115
|
Environment
|
|
85
|
-
|
|
86
|
-
publish, activate, rollback, release and upload. It is never
|
|
87
|
-
accepted as an argument and never printed.
|
|
116
|
+
MIAKAPP_CONFIG_DIR Absolute directory replacing ~/.miakapp
|
|
88
117
|
|
|
89
118
|
Exit codes
|
|
90
119
|
0 success 1 usage 2 project 3 artifact
|
|
@@ -95,15 +124,18 @@ const GLOBAL_OPTIONS = ['project'];
|
|
|
95
124
|
/** Exported so the MCP surface can be proved to expose every option, and no other. */
|
|
96
125
|
export const COMMAND_OPTIONS = {
|
|
97
126
|
docs: [],
|
|
98
|
-
|
|
127
|
+
pair: ['code', 'issuer', 'label', 'name'],
|
|
128
|
+
context: [],
|
|
129
|
+
init: ['home', 'control-plane', 'artifact', 'release', 'context', 'kind'],
|
|
99
130
|
'agent-pack': ['dir'],
|
|
100
131
|
discover: ['flows'],
|
|
101
132
|
check: [],
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
133
|
+
status: ['context'],
|
|
134
|
+
publish: ['expected-generation', 'generation', 'release', 'context'],
|
|
135
|
+
activate: ['sha256', 'expected-generation', 'generation', 'context'],
|
|
136
|
+
rollback: ['sha256', 'expected-generation', 'generation', 'context'],
|
|
137
|
+
release: ['context'],
|
|
138
|
+
upload: ['context'],
|
|
107
139
|
mcp: [],
|
|
108
140
|
help: [],
|
|
109
141
|
version: [],
|
|
@@ -184,17 +216,34 @@ function generationOption(invocation, name) {
|
|
|
184
216
|
throw usageError(`--${name} is above the safe integer range`);
|
|
185
217
|
return value;
|
|
186
218
|
}
|
|
187
|
-
/**
|
|
188
|
-
function
|
|
189
|
-
const expectedGeneration =
|
|
219
|
+
/** Validated before any network use, so a typo never costs a request. */
|
|
220
|
+
function generationPlan(invocation) {
|
|
221
|
+
const expectedGeneration = invocation.options.has('expected-generation')
|
|
222
|
+
? generationOption(invocation, 'expected-generation')
|
|
223
|
+
: undefined;
|
|
190
224
|
const generation = invocation.options.has('generation')
|
|
191
225
|
? generationOption(invocation, 'generation')
|
|
192
|
-
:
|
|
193
|
-
if (generation
|
|
226
|
+
: undefined;
|
|
227
|
+
if (expectedGeneration !== undefined && generation !== undefined
|
|
228
|
+
&& generation <= expectedGeneration) {
|
|
194
229
|
throw usageError('--generation must be strictly above --expected-generation');
|
|
195
230
|
}
|
|
196
231
|
return { expectedGeneration, generation };
|
|
197
232
|
}
|
|
233
|
+
/**
|
|
234
|
+
* Resolves the compare-and-set pair. Without `--expected-generation` the live
|
|
235
|
+
* generation is read first. That read takes no lock: the activation is still a
|
|
236
|
+
* compare-and-set, so a publication that lands in between fails with
|
|
237
|
+
* `conflict` rather than being overwritten.
|
|
238
|
+
*/
|
|
239
|
+
async function resolveGenerations(target, plan) {
|
|
240
|
+
const expectedGeneration = plan.expectedGeneration ?? (await readPointer(target)).generation;
|
|
241
|
+
const generation = plan.generation ?? expectedGeneration + 1;
|
|
242
|
+
if (generation <= expectedGeneration) {
|
|
243
|
+
throw usageError(`--generation must be strictly above the live generation ${expectedGeneration}`);
|
|
244
|
+
}
|
|
245
|
+
return { expectedGeneration, generation };
|
|
246
|
+
}
|
|
198
247
|
function digestOption(invocation, name) {
|
|
199
248
|
const value = requiredOption(invocation, name);
|
|
200
249
|
if (!isDigest(value)) {
|
|
@@ -202,13 +251,95 @@ function digestOption(invocation, name) {
|
|
|
202
251
|
}
|
|
203
252
|
return value;
|
|
204
253
|
}
|
|
205
|
-
function
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
254
|
+
function nonEmpty(value) {
|
|
255
|
+
return value === undefined || value === '' ? undefined : value;
|
|
256
|
+
}
|
|
257
|
+
function storeDirectory(host) {
|
|
258
|
+
return configDirectory((name) => host.env(name), host.homeDirectory?.() ?? homedir());
|
|
259
|
+
}
|
|
260
|
+
/** The context named for this invocation, by flag first and then environment. */
|
|
261
|
+
function explicitContext(host, invocation) {
|
|
262
|
+
const name = invocation.options.get('context') ?? nonEmpty(host.env(CONTEXT_VARIABLE));
|
|
263
|
+
if (name !== undefined && !isContextName(name))
|
|
264
|
+
contextNameError(name);
|
|
265
|
+
return name;
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* The wrong-home guard. The project file says where a publication goes; a
|
|
269
|
+
* context says which home its key opens. When the two disagree the command
|
|
270
|
+
* stops, because the control plane would accept a valid key for home A
|
|
271
|
+
* publishing into home A, while the person meant home B.
|
|
272
|
+
*/
|
|
273
|
+
function requireSameHome(context, project, origin) {
|
|
274
|
+
if (context.homeId === project.homeId && context.issuer === project.issuer)
|
|
275
|
+
return;
|
|
276
|
+
throw authorizationError(`${origin} is for home ${context.homeId} at ${context.issuer}, but ${PROJECT_FILE} targets `
|
|
277
|
+
+ `home ${project.homeId} at ${project.issuer}`, 'Select the context paired with this home (miakapp context list), or pair it with '
|
|
278
|
+
+ 'miakapp pair. Nothing was sent.');
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* Picks the Home Key for one invocation. Precedence, highest first:
|
|
282
|
+
*
|
|
283
|
+
* 1. `--context <name>`, then `MIAKAPP_CONTEXT`: an explicit stored context;
|
|
284
|
+
* 2. `MIAKAPP_HOME_KEY`: a raw key, for CI and compatibility;
|
|
285
|
+
* 3. the stored context paired with the project's home and issuer, the
|
|
286
|
+
* current context breaking a tie between several keys for that one home.
|
|
287
|
+
*
|
|
288
|
+
* A stored context is used only for the home it was paired with, and a raw
|
|
289
|
+
* key known to belong to another stored home is refused the same way.
|
|
290
|
+
*/
|
|
291
|
+
async function resolveCredential(host, invocation, project) {
|
|
292
|
+
const directory = storeDirectory(host);
|
|
293
|
+
const explicit = explicitContext(host, invocation);
|
|
294
|
+
if (explicit !== undefined) {
|
|
295
|
+
const { context, homeKey } = await readCredential(directory, explicit);
|
|
296
|
+
requireSameHome(context, project, `Context ${context.name}`);
|
|
297
|
+
return { homeKey, source: 'context', context };
|
|
210
298
|
}
|
|
211
|
-
|
|
299
|
+
const environmentKey = nonEmpty(host.env(HOME_KEY_VARIABLE));
|
|
300
|
+
if (environmentKey !== undefined) {
|
|
301
|
+
const keyId = homeKeyId(environmentKey);
|
|
302
|
+
let known = [];
|
|
303
|
+
try {
|
|
304
|
+
known = (await readContexts(directory)).contexts.values();
|
|
305
|
+
}
|
|
306
|
+
catch {
|
|
307
|
+
// The guard is best effort here: a CI runner has no store, and a broken
|
|
308
|
+
// one must not block a key the caller supplied explicitly.
|
|
309
|
+
}
|
|
310
|
+
for (const context of known) {
|
|
311
|
+
if (context.keyId === keyId) {
|
|
312
|
+
requireSameHome(context, project, `${HOME_KEY_VARIABLE} (paired as context ${context.name})`);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
return { homeKey: environmentKey, source: 'environment', context: undefined };
|
|
316
|
+
}
|
|
317
|
+
const state = await readContexts(directory);
|
|
318
|
+
const matching = [...state.contexts.values()]
|
|
319
|
+
.filter((context) => context.homeId === project.homeId && context.issuer === project.issuer)
|
|
320
|
+
.sort((a, b) => (a.createdAt < b.createdAt ? 1 : -1));
|
|
321
|
+
const chosen = matching.find((context) => context.name === state.current) ?? matching[0];
|
|
322
|
+
if (chosen === undefined) {
|
|
323
|
+
const others = [...state.contexts.keys()];
|
|
324
|
+
throw authorizationError(`No credential for home ${project.homeId} at ${project.issuer}`, 'Pair this home: ask the owner to open '
|
|
325
|
+
+ `${PAIRING_PAGE}, choose it, confirm access and send you the code, then run `
|
|
326
|
+
+ `miakapp pair. ${HOME_KEY_VARIABLE} also works, for CI.`
|
|
327
|
+
+ (others.length === 0 ? '' : ` Stored contexts are for other homes: ${others.join(', ')}.`));
|
|
328
|
+
}
|
|
329
|
+
const { homeKey } = await readCredential(directory, chosen.name);
|
|
330
|
+
return { homeKey, source: 'context', context: chosen };
|
|
331
|
+
}
|
|
332
|
+
function credentialJson(credential) {
|
|
333
|
+
return {
|
|
334
|
+
credential_source: credential.source,
|
|
335
|
+
context: credential.context?.name ?? null,
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
function credentialFields(credential) {
|
|
339
|
+
return [[
|
|
340
|
+
'credential',
|
|
341
|
+
credential.context === undefined ? HOME_KEY_VARIABLE : `context ${credential.context.name}`,
|
|
342
|
+
]];
|
|
212
343
|
}
|
|
213
344
|
async function nodeFileSystem() {
|
|
214
345
|
const fs = await import('node:fs/promises');
|
|
@@ -260,30 +391,47 @@ async function loadArtifact(host, project) {
|
|
|
260
391
|
}
|
|
261
392
|
return prepareArtifact(await filesystem.read(project.artifactPath));
|
|
262
393
|
}
|
|
263
|
-
async function publicationTarget(host, project) {
|
|
264
|
-
const
|
|
394
|
+
async function publicationTarget(host, invocation, project) {
|
|
395
|
+
const credential = await resolveCredential(host, invocation, project);
|
|
265
396
|
const options = host.fetch === undefined ? {} : { fetch: host.fetch };
|
|
266
397
|
const discovery = await fetchDiscovery({ issuer: project.issuer, ...options });
|
|
267
|
-
const token = await exchangePublisherToken(discovery,
|
|
268
|
-
return {
|
|
398
|
+
const token = await exchangePublisherToken(discovery, credential.homeKey, options);
|
|
399
|
+
return {
|
|
400
|
+
target: { issuer: discovery.issuer, homeId: project.homeId, token: token.accessToken, ...options },
|
|
401
|
+
credential,
|
|
402
|
+
homeUrl: homeUrl(discovery, project.homeId),
|
|
403
|
+
};
|
|
269
404
|
}
|
|
270
405
|
function requirementFields(requires) {
|
|
271
406
|
return REQUIREMENT_KINDS.map((kind) => [`requires.${kind}`, requires[kind]]);
|
|
272
407
|
}
|
|
273
|
-
|
|
408
|
+
/**
|
|
409
|
+
* `home_url` is the link residents open — the trusted Miakapp shell for this
|
|
410
|
+
* home, from discovery — and the only link to hand a person. `url` stays the
|
|
411
|
+
* raw artifact the shell verifies and runs; it is printed as `artifact_url` so
|
|
412
|
+
* it is never mistaken for a page.
|
|
413
|
+
*/
|
|
414
|
+
function pointerResult(summary, pointer, credential, expectedGeneration, residentUrl) {
|
|
274
415
|
return {
|
|
275
416
|
summary,
|
|
276
417
|
fields: [
|
|
418
|
+
...credentialFields(credential),
|
|
419
|
+
...(expectedGeneration === undefined
|
|
420
|
+
? []
|
|
421
|
+
: [['expected_generation', expectedGeneration]]),
|
|
277
422
|
['home', pointer.homeId],
|
|
423
|
+
['home_url', residentUrl ?? 'not advertised by this control plane'],
|
|
424
|
+
['abi', pointer.abi],
|
|
278
425
|
['generation', pointer.generation],
|
|
279
426
|
['release', pointer.release],
|
|
280
427
|
['sha256', pointer.sha256],
|
|
281
428
|
['size', pointer.size],
|
|
282
|
-
['
|
|
429
|
+
['artifact_url', pointer.url],
|
|
283
430
|
...requirementFields(pointer.requires),
|
|
284
431
|
],
|
|
285
432
|
json: {
|
|
286
433
|
home_id: pointer.homeId,
|
|
434
|
+
home_url: residentUrl,
|
|
287
435
|
generation: pointer.generation,
|
|
288
436
|
release: pointer.release,
|
|
289
437
|
abi: pointer.abi,
|
|
@@ -291,10 +439,29 @@ function pointerResult(summary, pointer) {
|
|
|
291
439
|
sha256: pointer.sha256,
|
|
292
440
|
size: pointer.size,
|
|
293
441
|
requires: pointer.requires,
|
|
442
|
+
...(expectedGeneration === undefined ? {} : { expected_generation: expectedGeneration }),
|
|
443
|
+
...credentialJson(credential),
|
|
294
444
|
},
|
|
295
445
|
};
|
|
296
446
|
}
|
|
297
447
|
function projectTemplate(fields) {
|
|
448
|
+
if (fields.kind === 'app') {
|
|
449
|
+
return `schema: ${PROJECT_SCHEMA}
|
|
450
|
+
home: ${fields.home}
|
|
451
|
+
control_plane: ${fields.controlPlane}
|
|
452
|
+
|
|
453
|
+
# A whole-house application: one self-contained classic-script (IIFE) bundle
|
|
454
|
+
# that draws its own interface in Miakapp's isolated frame. Every path read and
|
|
455
|
+
# every function called must be declared; the coordinator still decides, per
|
|
456
|
+
# resident, what each one may see and do.
|
|
457
|
+
app:
|
|
458
|
+
artifact: ${fields.artifact}
|
|
459
|
+
release: ${fields.release}
|
|
460
|
+
requires:
|
|
461
|
+
state_read: []
|
|
462
|
+
call: []
|
|
463
|
+
`;
|
|
464
|
+
}
|
|
298
465
|
return `schema: ${PROJECT_SCHEMA}
|
|
299
466
|
home: ${fields.home}
|
|
300
467
|
control_plane: ${fields.controlPlane}
|
|
@@ -321,19 +488,72 @@ async function runInit(host, invocation) {
|
|
|
321
488
|
if (!isRelease(release)) {
|
|
322
489
|
throw usageError('--release must be 1..64 UTF-8 bytes without control characters');
|
|
323
490
|
}
|
|
491
|
+
const kind = invocation.options.get('kind') ?? 'app';
|
|
492
|
+
if (kind !== 'app' && kind !== 'component') {
|
|
493
|
+
throw usageError('--kind must be app (whole-house application, default) or component');
|
|
494
|
+
}
|
|
495
|
+
const target = await initTarget(host, invocation);
|
|
324
496
|
const source = projectTemplate({
|
|
325
|
-
home:
|
|
326
|
-
controlPlane:
|
|
327
|
-
artifact: invocation.options.get('artifact') ?? 'dist/component.js',
|
|
497
|
+
home: target.home,
|
|
498
|
+
controlPlane: target.controlPlane,
|
|
499
|
+
artifact: invocation.options.get('artifact') ?? (kind === 'app' ? 'dist/app.js' : 'dist/component.js'),
|
|
328
500
|
release,
|
|
501
|
+
kind,
|
|
329
502
|
});
|
|
330
503
|
// Parsed before it is written, so init can never emit a file check rejects.
|
|
331
504
|
parseProject(root, source);
|
|
332
505
|
await filesystem.write(path, new TextEncoder().encode(source));
|
|
333
506
|
return {
|
|
334
507
|
summary: `Wrote ${path}`,
|
|
335
|
-
fields: [
|
|
336
|
-
|
|
508
|
+
fields: [
|
|
509
|
+
['project', path],
|
|
510
|
+
['home', target.home],
|
|
511
|
+
['control_plane', target.controlPlane],
|
|
512
|
+
...(target.context === undefined ? [] : [['context', target.context]]),
|
|
513
|
+
],
|
|
514
|
+
json: {
|
|
515
|
+
project: path,
|
|
516
|
+
schema: PROJECT_SCHEMA,
|
|
517
|
+
kind,
|
|
518
|
+
home_id: target.home,
|
|
519
|
+
control_plane: target.controlPlane,
|
|
520
|
+
context: target.context ?? null,
|
|
521
|
+
},
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
/**
|
|
525
|
+
* `--home` and `--control-plane` default to the selected context — the one
|
|
526
|
+
* named by `--context` or `MIAKAPP_CONTEXT`, else the current one — so the step
|
|
527
|
+
* after `miakapp pair` needs no identifier typed by hand. An explicit value
|
|
528
|
+
* that contradicts an explicitly selected context is refused rather than
|
|
529
|
+
* silently preferred.
|
|
530
|
+
*/
|
|
531
|
+
async function initTarget(host, invocation) {
|
|
532
|
+
const home = invocation.options.get('home');
|
|
533
|
+
const controlPlane = invocation.options.get('control-plane');
|
|
534
|
+
const explicit = explicitContext(host, invocation);
|
|
535
|
+
if (explicit === undefined && home !== undefined && controlPlane !== undefined) {
|
|
536
|
+
return { home, controlPlane, context: undefined };
|
|
537
|
+
}
|
|
538
|
+
const state = await readContexts(storeDirectory(host));
|
|
539
|
+
const name = explicit ?? state.current ?? undefined;
|
|
540
|
+
const context = name === undefined ? undefined : state.contexts.get(name);
|
|
541
|
+
if (explicit !== undefined && context === undefined) {
|
|
542
|
+
throw usageError(`No context named ${explicit}`, 'Run miakapp context list.');
|
|
543
|
+
}
|
|
544
|
+
if (context === undefined) {
|
|
545
|
+
throw usageError(`--${home === undefined ? 'home' : 'control-plane'} is required when no context is selected`, 'Pair the home first with miakapp pair, or pass both --home and --control-plane.');
|
|
546
|
+
}
|
|
547
|
+
if (explicit !== undefined
|
|
548
|
+
&& ((home !== undefined && home !== context.homeId)
|
|
549
|
+
|| (controlPlane !== undefined && controlPlane !== context.issuer))) {
|
|
550
|
+
throw usageError(`Context ${context.name} is for home ${context.homeId} at ${context.issuer}, which `
|
|
551
|
+
+ 'contradicts --home or --control-plane');
|
|
552
|
+
}
|
|
553
|
+
return {
|
|
554
|
+
home: home ?? context.homeId,
|
|
555
|
+
controlPlane: controlPlane ?? context.issuer,
|
|
556
|
+
context: context.name,
|
|
337
557
|
};
|
|
338
558
|
}
|
|
339
559
|
/**
|
|
@@ -483,6 +703,7 @@ async function runCheck(host, invocation) {
|
|
|
483
703
|
fields: [
|
|
484
704
|
['home', project.homeId],
|
|
485
705
|
['control_plane', project.issuer],
|
|
706
|
+
['abi', project.abi],
|
|
486
707
|
['release', project.release],
|
|
487
708
|
['artifact', project.artifactPath],
|
|
488
709
|
['sha256', artifact.sha256],
|
|
@@ -493,6 +714,7 @@ async function runCheck(host, invocation) {
|
|
|
493
714
|
json: {
|
|
494
715
|
home_id: project.homeId,
|
|
495
716
|
control_plane: project.issuer,
|
|
717
|
+
abi: project.abi,
|
|
496
718
|
release: project.release,
|
|
497
719
|
artifact: project.artifactPath,
|
|
498
720
|
sha256: artifact.sha256,
|
|
@@ -503,35 +725,38 @@ async function runCheck(host, invocation) {
|
|
|
503
725
|
};
|
|
504
726
|
}
|
|
505
727
|
async function runPublish(host, invocation) {
|
|
506
|
-
const
|
|
728
|
+
const plan = generationPlan(invocation);
|
|
507
729
|
const project = await loadProject(host, invocation);
|
|
508
730
|
const release = invocation.options.get('release') ?? project.release;
|
|
509
731
|
if (!isRelease(release)) {
|
|
510
732
|
throw usageError('--release must be 1..64 UTF-8 bytes without control characters');
|
|
511
733
|
}
|
|
512
734
|
const artifact = await loadArtifact(host, project);
|
|
513
|
-
const target = await publicationTarget(host, project);
|
|
735
|
+
const { target, credential, homeUrl: residentUrl } = await publicationTarget(host, invocation, project);
|
|
736
|
+
const { expectedGeneration, generation } = await resolveGenerations(target, plan);
|
|
514
737
|
const { pointer } = await publish(target, artifact, {
|
|
515
738
|
release,
|
|
739
|
+
abi: project.abi,
|
|
516
740
|
requires: project.requires,
|
|
517
741
|
expectedGeneration,
|
|
518
742
|
generation,
|
|
519
743
|
});
|
|
520
|
-
return pointerResult(`Published ${release} as generation ${pointer.generation}`, pointer);
|
|
744
|
+
return pointerResult(`Published ${release} as generation ${pointer.generation}`, pointer, credential, expectedGeneration, residentUrl);
|
|
521
745
|
}
|
|
522
746
|
async function runActivate(host, invocation) {
|
|
523
747
|
const sha256 = digestOption(invocation, 'sha256');
|
|
524
|
-
const
|
|
748
|
+
const plan = generationPlan(invocation);
|
|
525
749
|
const project = await loadProject(host, invocation);
|
|
526
|
-
const target = await publicationTarget(host, project);
|
|
750
|
+
const { target, credential, homeUrl: residentUrl } = await publicationTarget(host, invocation, project);
|
|
527
751
|
// Activation is checked against a readable finalized record first, so a typo
|
|
528
752
|
// in a digest fails as an artifact error instead of spending a CAS attempt.
|
|
529
753
|
const existing = await readRelease(target, sha256);
|
|
530
754
|
if (existing === undefined) {
|
|
531
755
|
throw artifactError(`No finalized release for ${sha256}`, 'Activate only a digest this home has already published.');
|
|
532
756
|
}
|
|
757
|
+
const { expectedGeneration, generation } = await resolveGenerations(target, plan);
|
|
533
758
|
const pointer = await activateRelease(target, { sha256, expectedGeneration, generation });
|
|
534
|
-
return pointerResult(`Activated ${existing.release} as generation ${pointer.generation}`, pointer);
|
|
759
|
+
return pointerResult(`Activated ${existing.release} as generation ${pointer.generation}`, pointer, credential, expectedGeneration, residentUrl);
|
|
535
760
|
}
|
|
536
761
|
async function runRelease(host, invocation) {
|
|
537
762
|
const sha256 = invocation.positional[0];
|
|
@@ -541,7 +766,7 @@ async function runRelease(host, invocation) {
|
|
|
541
766
|
throw usageError('The release digest must be 43 base64url characters');
|
|
542
767
|
}
|
|
543
768
|
const project = await loadProject(host, invocation);
|
|
544
|
-
const target = await publicationTarget(host, project);
|
|
769
|
+
const { target } = await publicationTarget(host, invocation, project);
|
|
545
770
|
const record = await readRelease(target, sha256);
|
|
546
771
|
if (record === undefined) {
|
|
547
772
|
throw artifactError(`No finalized release for ${sha256}`);
|
|
@@ -570,7 +795,7 @@ async function runUpload(host, invocation) {
|
|
|
570
795
|
if (uploadId === undefined)
|
|
571
796
|
throw usageError('upload requires one uploadId argument');
|
|
572
797
|
const project = await loadProject(host, invocation);
|
|
573
|
-
const target = await publicationTarget(host, project);
|
|
798
|
+
const { target } = await publicationTarget(host, invocation, project);
|
|
574
799
|
const state = await readUpload(target, uploadId);
|
|
575
800
|
return {
|
|
576
801
|
summary: `Upload ${state.uploadId} is ${state.status}`,
|
|
@@ -594,6 +819,281 @@ async function runUpload(host, invocation) {
|
|
|
594
819
|
},
|
|
595
820
|
};
|
|
596
821
|
}
|
|
822
|
+
async function runStatus(host, invocation) {
|
|
823
|
+
const project = await loadProject(host, invocation);
|
|
824
|
+
const { target, credential, homeUrl: residentUrl } = await publicationTarget(host, invocation, project);
|
|
825
|
+
const { generation, pointer } = await readPointer(target);
|
|
826
|
+
if (pointer === null) {
|
|
827
|
+
return {
|
|
828
|
+
summary: `${project.homeId} has never activated an interface (generation 0)`,
|
|
829
|
+
fields: [
|
|
830
|
+
['home', project.homeId],
|
|
831
|
+
['home_url', residentUrl ?? 'not advertised by this control plane'],
|
|
832
|
+
['generation', 0],
|
|
833
|
+
...credentialFields(credential),
|
|
834
|
+
],
|
|
835
|
+
json: {
|
|
836
|
+
home_id: project.homeId,
|
|
837
|
+
home_url: residentUrl,
|
|
838
|
+
generation,
|
|
839
|
+
active: false,
|
|
840
|
+
...credentialJson(credential),
|
|
841
|
+
},
|
|
842
|
+
};
|
|
843
|
+
}
|
|
844
|
+
const result = pointerResult(`${project.homeId} runs ${pointer.release} at generation ${generation}`, pointer, credential, undefined, residentUrl);
|
|
845
|
+
return { ...result, json: { ...result.json, active: true } };
|
|
846
|
+
}
|
|
847
|
+
function defaultLabel(host) {
|
|
848
|
+
const machine = (host.hostname?.() ?? hostname()).replace(/[^A-Za-z0-9._-]/g, '-');
|
|
849
|
+
return `miakapp-cli@${machine === '' ? 'unknown' : machine}`.slice(0, 64);
|
|
850
|
+
}
|
|
851
|
+
/** A free context name: the base itself, else base-2, base-3, ... */
|
|
852
|
+
function freeContextName(taken, base) {
|
|
853
|
+
if (!taken.has(base))
|
|
854
|
+
return base;
|
|
855
|
+
for (let suffix = 2;; suffix += 1) {
|
|
856
|
+
const candidate = `${base.slice(0, 62 - String(suffix).length)}-${suffix}`;
|
|
857
|
+
if (!taken.has(candidate))
|
|
858
|
+
return candidate;
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
async function pairingCode(host, invocation) {
|
|
862
|
+
const explicit = invocation.options.get('code');
|
|
863
|
+
if (explicit !== undefined)
|
|
864
|
+
return explicit;
|
|
865
|
+
if (host.readSecret === undefined) {
|
|
866
|
+
throw usageError('pair needs the one-time code', 'Pipe it on stdin (printf %s "$CODE" | miakapp pair) or run miakapp pair in a terminal to '
|
|
867
|
+
+ 'be prompted without echo.');
|
|
868
|
+
}
|
|
869
|
+
return await host.readSecret('Pairing code (hidden): ');
|
|
870
|
+
}
|
|
871
|
+
/**
|
|
872
|
+
* Trades a one-time pairing code for a fresh Home Key and stores it as a new
|
|
873
|
+
* context. Every existing context is kept. The new context becomes current.
|
|
874
|
+
*
|
|
875
|
+
* Everything that can fail without spending the code — the issuer, the label,
|
|
876
|
+
* the name, the readability of the store and the issuer's discovery document —
|
|
877
|
+
* is checked before the code is sent, because a code works exactly once.
|
|
878
|
+
*/
|
|
879
|
+
async function runPair(host, invocation) {
|
|
880
|
+
const issuer = pairingIssuer(invocation.options.get('issuer') ?? DEFAULT_ISSUER);
|
|
881
|
+
const label = validateLabel(invocation.options.get('label') ?? defaultLabel(host));
|
|
882
|
+
const requestedName = invocation.options.get('name');
|
|
883
|
+
if (requestedName !== undefined && !isContextName(requestedName))
|
|
884
|
+
contextNameError(requestedName);
|
|
885
|
+
const directory = storeDirectory(host);
|
|
886
|
+
const before = await readContexts(directory);
|
|
887
|
+
if (requestedName !== undefined && before.contexts.has(requestedName)) {
|
|
888
|
+
throw usageError(`A context named ${requestedName} already exists`, `Choose another --name, or remove it first with miakapp context remove ${requestedName}. `
|
|
889
|
+
+ 'The code was not used.');
|
|
890
|
+
}
|
|
891
|
+
const code = normalizeCode(await pairingCode(host, invocation));
|
|
892
|
+
const options = host.fetch === undefined ? {} : { fetch: host.fetch };
|
|
893
|
+
let discovery;
|
|
894
|
+
try {
|
|
895
|
+
discovery = await fetchDiscovery({ issuer, ...options });
|
|
896
|
+
}
|
|
897
|
+
catch (error) {
|
|
898
|
+
const reason = error instanceof Error ? error.message : 'unknown failure';
|
|
899
|
+
throw contractError(`${issuer} did not prove it is a Miakapp control plane: ${reason}`, 'The code was not sent and is still valid until it expires. Check --issuer and the '
|
|
900
|
+
+ 'network, then run pair again with the same code.');
|
|
901
|
+
}
|
|
902
|
+
const paired = await redeemPairingCode({ issuer, code, label, ...options });
|
|
903
|
+
const createdAt = new Date().toISOString();
|
|
904
|
+
let stored;
|
|
905
|
+
try {
|
|
906
|
+
stored = await mutateContexts(directory, (state) => {
|
|
907
|
+
const previous = [...state.contexts.values()]
|
|
908
|
+
.filter((context) => context.homeId === paired.homeId && context.issuer === paired.issuer);
|
|
909
|
+
const name = freeContextName(state.contexts, requestedName ?? paired.homeId);
|
|
910
|
+
state.contexts.set(name, {
|
|
911
|
+
name,
|
|
912
|
+
issuer: paired.issuer,
|
|
913
|
+
homeId: paired.homeId,
|
|
914
|
+
keyId: paired.keyId,
|
|
915
|
+
label,
|
|
916
|
+
createdAt,
|
|
917
|
+
});
|
|
918
|
+
state.keys.set(name, paired.homeKey);
|
|
919
|
+
state.current = name;
|
|
920
|
+
return { name, previous };
|
|
921
|
+
});
|
|
922
|
+
}
|
|
923
|
+
catch (error) {
|
|
924
|
+
const reason = error instanceof Error ? error.message : 'unknown failure';
|
|
925
|
+
throw authorizationError(`Home Key ${paired.keyId} for ${paired.homeId} was issued but could not be stored in `
|
|
926
|
+
+ `${directory}: ${reason}`, `Ask the owner to revoke the key labelled ${JSON.stringify(label)}, fix the directory, then pair again.`);
|
|
927
|
+
}
|
|
928
|
+
// The key is stored; proving it can obtain a publication token tells the
|
|
929
|
+
// agent now, rather than at its first publish, whether the grant is usable.
|
|
930
|
+
let publishAccess = 'verified';
|
|
931
|
+
let publishAccessDetail;
|
|
932
|
+
try {
|
|
933
|
+
await exchangePublisherToken(discovery, paired.homeKey, options);
|
|
934
|
+
}
|
|
935
|
+
catch (error) {
|
|
936
|
+
publishAccess = 'refused';
|
|
937
|
+
publishAccessDetail = error instanceof Error ? error.message : 'unknown failure';
|
|
938
|
+
}
|
|
939
|
+
const previous = stored.previous.map((context) => context.name);
|
|
940
|
+
return {
|
|
941
|
+
summary: `Paired ${paired.homeId} as context ${stored.name}`
|
|
942
|
+
+ (publishAccess === 'verified' ? '' : ' (the key could not obtain a publication token)'),
|
|
943
|
+
fields: [
|
|
944
|
+
['context', stored.name],
|
|
945
|
+
['home', paired.homeId],
|
|
946
|
+
['issuer', paired.issuer],
|
|
947
|
+
['key_id', paired.keyId],
|
|
948
|
+
['label', label],
|
|
949
|
+
['current', 'yes'],
|
|
950
|
+
['publish_access', publishAccessDetail === undefined ? publishAccess : `${publishAccess}: ${publishAccessDetail}`],
|
|
951
|
+
['config', directory],
|
|
952
|
+
...(previous.length === 0 ? [] : [['earlier_contexts_for_this_home', previous]]),
|
|
953
|
+
],
|
|
954
|
+
json: {
|
|
955
|
+
context: stored.name,
|
|
956
|
+
home_id: paired.homeId,
|
|
957
|
+
issuer: paired.issuer,
|
|
958
|
+
key_id: paired.keyId,
|
|
959
|
+
label,
|
|
960
|
+
current: true,
|
|
961
|
+
publish_access: publishAccess,
|
|
962
|
+
...(publishAccessDetail === undefined ? {} : { publish_access_detail: publishAccessDetail }),
|
|
963
|
+
config_directory: directory,
|
|
964
|
+
earlier_contexts_for_this_home: previous,
|
|
965
|
+
},
|
|
966
|
+
};
|
|
967
|
+
}
|
|
968
|
+
function contextJson(context, current, credential) {
|
|
969
|
+
return {
|
|
970
|
+
name: context.name,
|
|
971
|
+
home_id: context.homeId,
|
|
972
|
+
issuer: context.issuer,
|
|
973
|
+
key_id: context.keyId,
|
|
974
|
+
label: context.label,
|
|
975
|
+
created_at: context.createdAt,
|
|
976
|
+
current: context.name === current,
|
|
977
|
+
credential: credential ?? 'missing',
|
|
978
|
+
};
|
|
979
|
+
}
|
|
980
|
+
/**
|
|
981
|
+
* `context list | show [name] | use <name> | remove <name>`.
|
|
982
|
+
*
|
|
983
|
+
* Nothing here prints a Home Key: `credential` reports only whether the stored
|
|
984
|
+
* key is present and is the key the context was paired with.
|
|
985
|
+
*/
|
|
986
|
+
async function runContext(host, invocation) {
|
|
987
|
+
const [action, name, extra] = invocation.positional;
|
|
988
|
+
const directory = storeDirectory(host);
|
|
989
|
+
if (extra !== undefined)
|
|
990
|
+
throw usageError('context takes at most one name');
|
|
991
|
+
const named = (verb) => {
|
|
992
|
+
if (name === undefined)
|
|
993
|
+
throw usageError(`context ${verb} requires a context name`);
|
|
994
|
+
if (!isContextName(name))
|
|
995
|
+
contextNameError(name);
|
|
996
|
+
return name;
|
|
997
|
+
};
|
|
998
|
+
switch (action) {
|
|
999
|
+
case 'list': {
|
|
1000
|
+
if (name !== undefined)
|
|
1001
|
+
throw usageError('context list takes no name');
|
|
1002
|
+
const state = await readContexts(directory);
|
|
1003
|
+
const contexts = [...state.contexts.values()];
|
|
1004
|
+
return {
|
|
1005
|
+
summary: contexts.length === 0
|
|
1006
|
+
? `No context in ${directory}. Run miakapp pair to add one.`
|
|
1007
|
+
: `${counted(contexts.length, 'context', 'contexts')} in ${directory}`,
|
|
1008
|
+
fields: contexts.map((context) => [
|
|
1009
|
+
`${context.name === state.current ? '* ' : ''}${context.name}`,
|
|
1010
|
+
`home ${context.homeId} at ${context.issuer}, key ${context.keyId} `
|
|
1011
|
+
+ `(${state.credentials.get(context.name) ?? 'missing'})`,
|
|
1012
|
+
]),
|
|
1013
|
+
json: {
|
|
1014
|
+
config_directory: directory,
|
|
1015
|
+
current_context: state.current,
|
|
1016
|
+
contexts: contexts.map((context) => contextJson(context, state.current, state.credentials.get(context.name))),
|
|
1017
|
+
},
|
|
1018
|
+
};
|
|
1019
|
+
}
|
|
1020
|
+
case 'show': {
|
|
1021
|
+
const state = await readContexts(directory);
|
|
1022
|
+
const selected = name ?? state.current ?? undefined;
|
|
1023
|
+
if (selected === undefined) {
|
|
1024
|
+
throw usageError('No current context', 'Name one: miakapp context show <name>, or pair with miakapp pair.');
|
|
1025
|
+
}
|
|
1026
|
+
if (!isContextName(selected))
|
|
1027
|
+
contextNameError(selected);
|
|
1028
|
+
const context = state.contexts.get(selected);
|
|
1029
|
+
if (context === undefined) {
|
|
1030
|
+
throw usageError(`No context named ${selected}`, 'Run miakapp context list.');
|
|
1031
|
+
}
|
|
1032
|
+
const json = contextJson(context, state.current, state.credentials.get(context.name));
|
|
1033
|
+
return {
|
|
1034
|
+
summary: `Context ${context.name}${json['current'] === true ? ' (current)' : ''}`,
|
|
1035
|
+
fields: [
|
|
1036
|
+
['home', context.homeId],
|
|
1037
|
+
['issuer', context.issuer],
|
|
1038
|
+
['key_id', context.keyId],
|
|
1039
|
+
['label', context.label],
|
|
1040
|
+
['created_at', context.createdAt],
|
|
1041
|
+
['credential', `${String(json['credential'])} (never printed)`],
|
|
1042
|
+
['config', directory],
|
|
1043
|
+
],
|
|
1044
|
+
json: { ...json, config_directory: directory },
|
|
1045
|
+
};
|
|
1046
|
+
}
|
|
1047
|
+
case 'use': {
|
|
1048
|
+
const selected = named('use');
|
|
1049
|
+
const context = await mutateContexts(directory, (state) => {
|
|
1050
|
+
const found = state.contexts.get(selected);
|
|
1051
|
+
if (found === undefined) {
|
|
1052
|
+
throw usageError(`No context named ${selected}`, 'Run miakapp context list.');
|
|
1053
|
+
}
|
|
1054
|
+
state.current = selected;
|
|
1055
|
+
return found;
|
|
1056
|
+
});
|
|
1057
|
+
return {
|
|
1058
|
+
summary: `Current context is now ${selected}`,
|
|
1059
|
+
fields: [['home', context.homeId], ['issuer', context.issuer]],
|
|
1060
|
+
json: { current_context: selected, home_id: context.homeId, issuer: context.issuer },
|
|
1061
|
+
};
|
|
1062
|
+
}
|
|
1063
|
+
case 'remove': {
|
|
1064
|
+
const selected = named('remove');
|
|
1065
|
+
const result = await mutateContexts(directory, (state) => {
|
|
1066
|
+
const found = state.contexts.get(selected);
|
|
1067
|
+
if (found === undefined) {
|
|
1068
|
+
throw usageError(`No context named ${selected}`, 'Run miakapp context list.');
|
|
1069
|
+
}
|
|
1070
|
+
state.contexts.delete(selected);
|
|
1071
|
+
state.keys.delete(selected);
|
|
1072
|
+
const wasCurrent = state.current === selected;
|
|
1073
|
+
if (wasCurrent)
|
|
1074
|
+
state.current = null;
|
|
1075
|
+
return { context: found, wasCurrent };
|
|
1076
|
+
});
|
|
1077
|
+
return {
|
|
1078
|
+
summary: `Removed context ${selected} and its stored key`,
|
|
1079
|
+
fields: [
|
|
1080
|
+
['home', result.context.homeId],
|
|
1081
|
+
['key_id', result.context.keyId],
|
|
1082
|
+
['still_valid_on_server', 'yes: ask the owner to revoke it if it is no longer needed'],
|
|
1083
|
+
],
|
|
1084
|
+
json: {
|
|
1085
|
+
removed: selected,
|
|
1086
|
+
home_id: result.context.homeId,
|
|
1087
|
+
key_id: result.context.keyId,
|
|
1088
|
+
was_current: result.wasCurrent,
|
|
1089
|
+
revoked: false,
|
|
1090
|
+
},
|
|
1091
|
+
};
|
|
1092
|
+
}
|
|
1093
|
+
default:
|
|
1094
|
+
throw usageError(action === undefined ? 'context requires an action' : `Unknown context action: ${action}`, 'Use context list, context show [name], context use <name> or context remove <name>.');
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
597
1097
|
/**
|
|
598
1098
|
* Runs one parsed invocation.
|
|
599
1099
|
*
|
|
@@ -604,6 +1104,12 @@ export async function dispatch(host, invocation) {
|
|
|
604
1104
|
switch (invocation.command) {
|
|
605
1105
|
case 'docs':
|
|
606
1106
|
return await runDocs(host, invocation);
|
|
1107
|
+
case 'pair':
|
|
1108
|
+
return await runPair(host, invocation);
|
|
1109
|
+
case 'context':
|
|
1110
|
+
return await runContext(host, invocation);
|
|
1111
|
+
case 'status':
|
|
1112
|
+
return await runStatus(host, invocation);
|
|
607
1113
|
case 'init':
|
|
608
1114
|
return await runInit(host, invocation);
|
|
609
1115
|
case 'agent-pack':
|