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