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