@tetherto/mdk-cli 0.0.0 → 0.12.1
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/LICENSE +202 -0
- package/README.md +447 -0
- package/dist/commands/add.js +61 -0
- package/dist/commands/add.js.map +1 -0
- package/dist/commands/agent.js +42 -0
- package/dist/commands/agent.js.map +1 -0
- package/dist/commands/create.js +261 -0
- package/dist/commands/create.js.map +1 -0
- package/dist/commands/inspect.js +171 -0
- package/dist/commands/inspect.js.map +1 -0
- package/dist/commands/meta.js +12 -0
- package/dist/commands/meta.js.map +1 -0
- package/dist/commands/onboard.js +447 -0
- package/dist/commands/onboard.js.map +1 -0
- package/dist/commands/run.js +203 -0
- package/dist/commands/run.js.map +1 -0
- package/dist/commands/validate.js +125 -0
- package/dist/commands/validate.js.map +1 -0
- package/dist/index.js +9 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/add-package.js +148 -0
- package/dist/lib/add-package.js.map +1 -0
- package/dist/lib/app-package.js +182 -0
- package/dist/lib/app-package.js.map +1 -0
- package/dist/lib/app-scaffold.js +221 -0
- package/dist/lib/app-scaffold.js.map +1 -0
- package/dist/lib/app-ui-check.js +178 -0
- package/dist/lib/app-ui-check.js.map +1 -0
- package/dist/lib/app-ui-runner.js +122 -0
- package/dist/lib/app-ui-runner.js.map +1 -0
- package/dist/lib/catalog.js +69 -0
- package/dist/lib/catalog.js.map +1 -0
- package/dist/lib/checkout.js +14 -0
- package/dist/lib/checkout.js.map +1 -0
- package/dist/lib/dashboard-apps.js +71 -0
- package/dist/lib/dashboard-apps.js.map +1 -0
- package/dist/lib/dashboard.js +318 -0
- package/dist/lib/dashboard.js.map +1 -0
- package/dist/lib/detect.js +38 -0
- package/dist/lib/detect.js.map +1 -0
- package/dist/lib/env-file.js +57 -0
- package/dist/lib/env-file.js.map +1 -0
- package/dist/lib/fetch-template.js +33 -0
- package/dist/lib/fetch-template.js.map +1 -0
- package/dist/lib/help.js +22 -0
- package/dist/lib/help.js.map +1 -0
- package/dist/lib/names.js +36 -0
- package/dist/lib/names.js.map +1 -0
- package/dist/lib/npm.js +113 -0
- package/dist/lib/npm.js.map +1 -0
- package/dist/lib/package-source.js +76 -0
- package/dist/lib/package-source.js.map +1 -0
- package/dist/lib/pkg.js +5 -0
- package/dist/lib/pkg.js.map +1 -0
- package/dist/lib/plugin-setup.js +204 -0
- package/dist/lib/plugin-setup.js.map +1 -0
- package/dist/lib/ports.js +31 -0
- package/dist/lib/ports.js.map +1 -0
- package/dist/lib/project.js +297 -0
- package/dist/lib/project.js.map +1 -0
- package/dist/lib/run-lock.js +188 -0
- package/dist/lib/run-lock.js.map +1 -0
- package/dist/lib/runtime.js +683 -0
- package/dist/lib/runtime.js.map +1 -0
- package/dist/lib/scaffold.js +82 -0
- package/dist/lib/scaffold.js.map +1 -0
- package/dist/lib/shutdown.js +53 -0
- package/dist/lib/shutdown.js.map +1 -0
- package/dist/lib/skill.js +38 -0
- package/dist/lib/skill.js.map +1 -0
- package/dist/lib/spec.js +375 -0
- package/dist/lib/spec.js.map +1 -0
- package/dist/lib/status.js +245 -0
- package/dist/lib/status.js.map +1 -0
- package/dist/lib/theme.js +50 -0
- package/dist/lib/theme.js.map +1 -0
- package/dist/lib/worker-package.js +74 -0
- package/dist/lib/worker-package.js.map +1 -0
- package/dist/lib/worker-scaffold.js +122 -0
- package/dist/lib/worker-scaffold.js.map +1 -0
- package/dist/lib/worker-smoke.js +243 -0
- package/dist/lib/worker-smoke.js.map +1 -0
- package/dist/program.js +58 -0
- package/dist/program.js.map +1 -0
- package/package.json +59 -1
- package/templates/app/README.md +66 -0
- package/templates/app/mdk-app.yaml +10 -0
- package/templates/app/package.json +17 -0
- package/templates/app/plugin/controllers/summary.js +36 -0
- package/templates/app/plugin/lib/client.js +11 -0
- package/templates/app/plugin/mdk-plugin.json +16 -0
- package/templates/app-ui/tsconfig.json +29 -0
- package/templates/app-ui/ui-src/components/summary-panel.tsx +24 -0
- package/templates/app-ui/ui-src/data/summary.ts +18 -0
- package/templates/app-ui/ui-src/hooks/use-summary.ts +54 -0
- package/templates/app-ui/ui-src/index.tsx +15 -0
- package/templates/app-ui/ui-src/pages/overview-page.tsx +18 -0
- package/templates/app-ui/ui-src/styles.css +13 -0
- package/templates/app-ui/ui-src/styles.d.ts +3 -0
- package/templates/app-ui/ui-src/vite.config.ts +26 -0
- package/templates/worker/README.md +93 -0
- package/templates/worker/mdk-contract.json +26 -0
- package/templates/worker/mock/server.js +93 -0
- package/templates/worker/package.json +23 -0
- package/templates/worker/smoke.config.js +31 -0
- package/templates/worker/src/client.js +47 -0
- package/templates/worker/src/commands/reboot.js +5 -0
- package/templates/worker/src/commands/set-power-mode.js +10 -0
- package/templates/worker/src/telemetry/hashrate-rt.js +5 -0
- package/templates/worker/src/telemetry/power-mode.js +5 -0
- package/templates/worker/src/telemetry/power.js +5 -0
- package/templates/worker/src/telemetry/temperature.js +5 -0
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import { Option } from 'commander';
|
|
2
|
+
import { resolve } from 'node:path';
|
|
3
|
+
import { examples } from '../lib/help.js';
|
|
4
|
+
import { StackSpecError, loadStackSpec } from '../lib/spec.js';
|
|
5
|
+
import { assertGatewayPortFree, declaredMockPorts, runApp, runGateway, runKernel, runMcp, runWorker, superviseApp, } from '../lib/runtime.js';
|
|
6
|
+
import { findDashboardDir, runDashboard } from '../lib/dashboard.js';
|
|
7
|
+
import { describeDashboardApps } from '../lib/dashboard-apps.js';
|
|
8
|
+
import { installShutdown } from '../lib/shutdown.js';
|
|
9
|
+
import { acquireRunLocks } from '../lib/run-lock.js';
|
|
10
|
+
import { theme } from '../lib/theme.js';
|
|
11
|
+
const TARGETS = ['all', 'kernel', 'gateway', 'mcp', 'worker', 'app', 'dashboard'];
|
|
12
|
+
/** Targets that take a `[name]`; the rest refuse one rather than ignore it. */
|
|
13
|
+
const NAMED_TARGETS = ['worker', 'app'];
|
|
14
|
+
function die(message) {
|
|
15
|
+
process.stderr.write(`${theme.warn('error')}: ${message}\n`);
|
|
16
|
+
process.exit(1);
|
|
17
|
+
}
|
|
18
|
+
/** Loads + validates mdk.yaml, converting a spec error into a clean exit. */
|
|
19
|
+
function loadSpecOrDie(dir) {
|
|
20
|
+
try {
|
|
21
|
+
return loadStackSpec(dir);
|
|
22
|
+
}
|
|
23
|
+
catch (error) {
|
|
24
|
+
if (error instanceof StackSpecError)
|
|
25
|
+
die(error.message);
|
|
26
|
+
throw error;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Boots Kernel + Workers + Gateway together in this process (target `all`).
|
|
31
|
+
* Workers boot before the gateway so they register with the in-process Kernel
|
|
32
|
+
* before the gateway resolves its client; the gateway boots last since it
|
|
33
|
+
* `chdir`s into its own state root. Each app runs as a supervised child
|
|
34
|
+
* process, started before the gateway.
|
|
35
|
+
*/
|
|
36
|
+
async function runAll(dir, spec) {
|
|
37
|
+
// Checked up front: the gateway boots last, and booting a Kernel and every
|
|
38
|
+
// worker only to die on a taken port would leave the user to clean up state
|
|
39
|
+
// for a failure that was knowable in the first millisecond.
|
|
40
|
+
await assertGatewayPortFree(spec.spec.gateway.port);
|
|
41
|
+
const kernel = await runKernel(dir);
|
|
42
|
+
// Each worker avoids the ports its not-yet-booted neighbours asked for; the
|
|
43
|
+
// ones already booted are detected as bound, so this covers the whole set.
|
|
44
|
+
const declared = spec.spec.workers;
|
|
45
|
+
const workers = [];
|
|
46
|
+
for (const [i, w] of declared.entries()) {
|
|
47
|
+
const reservedPorts = new Set(declared.slice(i + 1).flatMap(declaredMockPorts));
|
|
48
|
+
const handle = await runWorker(dir, w, kernel, { reservedPorts });
|
|
49
|
+
workers.push({ label: `worker ${w.name}`, stop: () => handle.stop() });
|
|
50
|
+
}
|
|
51
|
+
const apps = [];
|
|
52
|
+
for (const a of spec.spec.apps) {
|
|
53
|
+
const handle = await superviseApp(dir, a, kernel);
|
|
54
|
+
apps.push({ label: `app ${a.name}`, stop: () => handle.stop() });
|
|
55
|
+
}
|
|
56
|
+
const gateway = await runGateway(dir, spec, kernel);
|
|
57
|
+
process.stderr.write(`\n${theme.ok('Stack running.')} ${theme.muted('Press Ctrl+C to stop.')}\n`);
|
|
58
|
+
// Reverse boot order: stop serving requests, then the apps and workers behind
|
|
59
|
+
// them, then the Kernel they were registered with.
|
|
60
|
+
return [
|
|
61
|
+
{ label: 'gateway', stop: () => stopGateway(gateway) },
|
|
62
|
+
...apps,
|
|
63
|
+
...workers,
|
|
64
|
+
{ label: 'kernel', stop: () => kernel.stop() },
|
|
65
|
+
];
|
|
66
|
+
}
|
|
67
|
+
/** Promisifies the Gateway's callback-style `stop`. */
|
|
68
|
+
function stopGateway(gateway) {
|
|
69
|
+
return new Promise((resolve) => {
|
|
70
|
+
try {
|
|
71
|
+
gateway.stop(() => resolve());
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
resolve();
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* The components a target runs in this process. The Gateway and the MCP server
|
|
80
|
+
* are not listed: their port check already refuses a second copy.
|
|
81
|
+
*/
|
|
82
|
+
function lockedComponents(target, name, spec) {
|
|
83
|
+
switch (target) {
|
|
84
|
+
case 'all':
|
|
85
|
+
return [
|
|
86
|
+
{ kind: 'kernel' },
|
|
87
|
+
...spec.spec.workers.map((w) => ({ kind: 'worker', name: w.name })),
|
|
88
|
+
...spec.spec.apps.map((a) => ({ kind: 'app', name: a.name })),
|
|
89
|
+
];
|
|
90
|
+
case 'kernel':
|
|
91
|
+
return [{ kind: 'kernel' }];
|
|
92
|
+
case 'worker':
|
|
93
|
+
case 'app': {
|
|
94
|
+
// Only a declared name: an unknown one is refused below, before it can
|
|
95
|
+
// name a lock file.
|
|
96
|
+
const declared = target === 'worker' ? spec.spec.workers : spec.spec.apps;
|
|
97
|
+
return declared.some((c) => c.name === name) ? [{ kind: target, name }] : [];
|
|
98
|
+
}
|
|
99
|
+
default:
|
|
100
|
+
return [];
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
async function runTarget(target, name, dir, spec, opts = {}) {
|
|
104
|
+
try {
|
|
105
|
+
acquireRunLocks(dir, lockedComponents(target, name, spec));
|
|
106
|
+
switch (target) {
|
|
107
|
+
case 'all':
|
|
108
|
+
return await runAll(dir, spec);
|
|
109
|
+
case 'kernel': {
|
|
110
|
+
const kernel = await runKernel(dir);
|
|
111
|
+
process.stderr.write(`\n${theme.ok('Kernel running.')} ${theme.muted('Press Ctrl+C to stop.')}\n`);
|
|
112
|
+
return [{ label: 'kernel', stop: () => kernel.stop() }];
|
|
113
|
+
}
|
|
114
|
+
case 'gateway': {
|
|
115
|
+
const gateway = await runGateway(dir, spec);
|
|
116
|
+
process.stderr.write(`\n${theme.ok('Gateway running.')} ${theme.muted('Press Ctrl+C to stop.')}\n`);
|
|
117
|
+
return [{ label: 'gateway', stop: () => stopGateway(gateway) }];
|
|
118
|
+
}
|
|
119
|
+
case 'mcp': {
|
|
120
|
+
const mcp = await runMcp(dir, spec, undefined, { allowWrite: opts.allowWrite });
|
|
121
|
+
process.stderr.write(`\n${theme.ok('MCP server running.')} ${theme.muted('Press Ctrl+C to stop.')}\n`);
|
|
122
|
+
return [{ label: 'mcp', stop: () => mcp.stop() }];
|
|
123
|
+
}
|
|
124
|
+
case 'worker': {
|
|
125
|
+
if (!name)
|
|
126
|
+
die('`mdk run worker` requires a worker <name> (see spec.workers in mdk.yaml).');
|
|
127
|
+
const worker = spec.spec.workers.find((w) => w.name === name);
|
|
128
|
+
if (!worker) {
|
|
129
|
+
const known = spec.spec.workers.map((w) => w.name).join(', ') || '(none)';
|
|
130
|
+
die(`No worker named "${name}" in mdk.yaml. Known workers: ${known}.`);
|
|
131
|
+
}
|
|
132
|
+
const handle = await runWorker(dir, worker);
|
|
133
|
+
process.stderr.write(`\n${theme.ok('Worker running.')} ${theme.muted('Press Ctrl+C to stop.')}\n`);
|
|
134
|
+
return [{ label: `worker ${worker.name}`, stop: () => handle.stop() }];
|
|
135
|
+
}
|
|
136
|
+
case 'app': {
|
|
137
|
+
if (!name)
|
|
138
|
+
die('`mdk run app` requires an app <name> (see spec.apps in mdk.yaml).');
|
|
139
|
+
const app = spec.spec.apps.find((a) => a.name === name);
|
|
140
|
+
if (!app) {
|
|
141
|
+
const known = spec.spec.apps.map((a) => a.name).join(', ') || '(none)';
|
|
142
|
+
die(`No app named "${name}" in mdk.yaml. Known apps: ${known}.`);
|
|
143
|
+
}
|
|
144
|
+
const handle = await runApp(dir, app);
|
|
145
|
+
process.stderr.write(`\n${theme.ok('App running.')} ${theme.muted('Press Ctrl+C to stop.')}\n`);
|
|
146
|
+
return [{ label: `app ${app.name}`, stop: () => handle.stop() }];
|
|
147
|
+
}
|
|
148
|
+
case 'dashboard': {
|
|
149
|
+
const dashboardDir = findDashboardDir(dir);
|
|
150
|
+
if (!dashboardDir) {
|
|
151
|
+
die(`No dashboard app found under ${resolve(dir)}. Scaffold one first: ${theme.cmd('mdk create dashboard')}`);
|
|
152
|
+
}
|
|
153
|
+
// Installed apps are found here, from mdk.yaml, and handed to the
|
|
154
|
+
// dashboard: it serves their UIs itself, so no Gateway is involved.
|
|
155
|
+
const apps = describeDashboardApps(dir, spec.spec.apps ?? []);
|
|
156
|
+
if (apps.length) {
|
|
157
|
+
process.stderr.write(`${theme.muted('Apps:')} ${apps.map((a) => `${a.displayName} (${a.name})`).join(', ')}\n`);
|
|
158
|
+
}
|
|
159
|
+
const handle = await runDashboard(dashboardDir, apps);
|
|
160
|
+
process.stderr.write(`\n${theme.ok('Dashboard running.')} ${theme.muted('Press Ctrl+C to stop.')}\n`);
|
|
161
|
+
return [{ label: 'dashboard', stop: () => handle.stop() }];
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
catch (error) {
|
|
166
|
+
die(error instanceof Error ? error.message : String(error));
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
// Group C — Run & manage
|
|
170
|
+
export function registerRun(program) {
|
|
171
|
+
program
|
|
172
|
+
.command('run')
|
|
173
|
+
.argument('[target]', `What to start: ${TARGETS.join(' | ')} (default: all)`)
|
|
174
|
+
.argument('[name]', 'The worker or app to start (worker and app targets only)')
|
|
175
|
+
.summary('Start the stack, or one component of it')
|
|
176
|
+
.description('Start the Kernel, workers, app runtimes and Gateway from mdk.yaml (target all), or one component. ' +
|
|
177
|
+
'mcp and dashboard are never part of all; start them on their own. Runs in the foreground; Ctrl+C stops it.')
|
|
178
|
+
.option('-d, --dir <path>', 'Project directory containing mdk.yaml', '.')
|
|
179
|
+
.addOption(new Option('--detach', 'Run in the background (not implemented yet)').default(false).hideHelp())
|
|
180
|
+
.option('--allow-write', 'mcp: expose write tools (hidden by default; or set spec.mcp.allowWrite)', false)
|
|
181
|
+
.addHelpText('after', examples('mdk run', 'mdk run kernel', 'mdk run worker rig', 'mdk run app ops-center', 'mdk run gateway --dir ./my-stack'))
|
|
182
|
+
.action(async (target, name, opts) => {
|
|
183
|
+
if (opts.detach) {
|
|
184
|
+
die('`--detach` is not implemented yet. Run in the foreground (omit --detach).');
|
|
185
|
+
}
|
|
186
|
+
if (target && !TARGETS.includes(target)) {
|
|
187
|
+
die(`Unknown target "${target}". Use one of: ${TARGETS.join(' | ')}.`);
|
|
188
|
+
}
|
|
189
|
+
const resolvedTarget = target ?? 'all';
|
|
190
|
+
if (name !== undefined && !NAMED_TARGETS.includes(resolvedTarget)) {
|
|
191
|
+
die(`\`mdk run ${resolvedTarget}\` takes no name (got "${name}"); only worker and app do.`);
|
|
192
|
+
}
|
|
193
|
+
const spec = loadSpecOrDie(opts.dir);
|
|
194
|
+
// No explicit target: boot everything together. Components can always be
|
|
195
|
+
// run separately instead — `mdk run kernel` / `gateway` / `worker <name>`
|
|
196
|
+
// — that choice is just which command(s) you type, not a spec setting.
|
|
197
|
+
const running = await runTarget(resolvedTarget, name, opts.dir, spec, { allowWrite: opts.allowWrite });
|
|
198
|
+
// Registered only once everything is up: a boot that fails exits on its
|
|
199
|
+
// own, and until then the default signal behavior is the right one.
|
|
200
|
+
installShutdown(running);
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
//# sourceMappingURL=run.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"run.js","sourceRoot":"","sources":["../../src/commands/run.ts"],"names":[],"mappings":"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","sourcesContent":["import { Option, type Command } from 'commander';\nimport { resolve } from 'node:path';\nimport { examples } from '../lib/help.js';\nimport { StackSpecError, loadStackSpec } from '../lib/spec.js';\nimport type { StackSpec } from '../lib/spec.js';\nimport {\n assertGatewayPortFree,\n declaredMockPorts,\n runApp,\n runGateway,\n runKernel,\n runMcp,\n runWorker,\n superviseApp,\n type GatewayHandle,\n} from '../lib/runtime.js';\nimport { findDashboardDir, runDashboard } from '../lib/dashboard.js';\nimport { describeDashboardApps } from '../lib/dashboard-apps.js';\nimport { installShutdown, type Stoppable } from '../lib/shutdown.js';\nimport { acquireRunLocks, type RunComponent } from '../lib/run-lock.js';\nimport { theme } from '../lib/theme.js';\n\ntype RunTarget = 'all' | 'kernel' | 'gateway' | 'mcp' | 'worker' | 'app' | 'dashboard';\nconst TARGETS: RunTarget[] = ['all', 'kernel', 'gateway', 'mcp', 'worker', 'app', 'dashboard'];\n/** Targets that take a `[name]`; the rest refuse one rather than ignore it. */\nconst NAMED_TARGETS: RunTarget[] = ['worker', 'app'];\n\ninterface RunOptions {\n dir: string;\n detach?: boolean;\n allowWrite?: boolean;\n}\n\nfunction die(message: string): never {\n process.stderr.write(`${theme.warn('error')}: ${message}\\n`);\n process.exit(1);\n}\n\n/** Loads + validates mdk.yaml, converting a spec error into a clean exit. */\nfunction loadSpecOrDie(dir: string): StackSpec {\n try {\n return loadStackSpec(dir);\n } catch (error) {\n if (error instanceof StackSpecError) die(error.message);\n throw error;\n }\n}\n\n/**\n * Boots Kernel + Workers + Gateway together in this process (target `all`).\n * Workers boot before the gateway so they register with the in-process Kernel\n * before the gateway resolves its client; the gateway boots last since it\n * `chdir`s into its own state root. Each app runs as a supervised child\n * process, started before the gateway.\n */\nasync function runAll(dir: string, spec: StackSpec): Promise<Stoppable[]> {\n // Checked up front: the gateway boots last, and booting a Kernel and every\n // worker only to die on a taken port would leave the user to clean up state\n // for a failure that was knowable in the first millisecond.\n await assertGatewayPortFree(spec.spec.gateway.port);\n\n const kernel = await runKernel(dir);\n\n // Each worker avoids the ports its not-yet-booted neighbours asked for; the\n // ones already booted are detected as bound, so this covers the whole set.\n const declared = spec.spec.workers;\n const workers: Stoppable[] = [];\n for (const [i, w] of declared.entries()) {\n const reservedPorts = new Set(declared.slice(i + 1).flatMap(declaredMockPorts));\n const handle = await runWorker(dir, w, kernel, { reservedPorts });\n workers.push({ label: `worker ${w.name}`, stop: () => handle.stop() });\n }\n\n const apps: Stoppable[] = [];\n for (const a of spec.spec.apps) {\n const handle = await superviseApp(dir, a, kernel);\n apps.push({ label: `app ${a.name}`, stop: () => handle.stop() });\n }\n\n const gateway = await runGateway(dir, spec, kernel);\n process.stderr.write(`\\n${theme.ok('Stack running.')} ${theme.muted('Press Ctrl+C to stop.')}\\n`);\n\n // Reverse boot order: stop serving requests, then the apps and workers behind\n // them, then the Kernel they were registered with.\n return [\n { label: 'gateway', stop: () => stopGateway(gateway) },\n ...apps,\n ...workers,\n { label: 'kernel', stop: () => kernel.stop() },\n ];\n}\n\n/** Promisifies the Gateway's callback-style `stop`. */\nfunction stopGateway(gateway: GatewayHandle): Promise<void> {\n return new Promise((resolve) => {\n try {\n gateway.stop(() => resolve());\n } catch {\n resolve();\n }\n });\n}\n\n/**\n * The components a target runs in this process. The Gateway and the MCP server\n * are not listed: their port check already refuses a second copy.\n */\nfunction lockedComponents(target: RunTarget, name: string | undefined, spec: StackSpec): RunComponent[] {\n switch (target) {\n case 'all':\n return [\n { kind: 'kernel' },\n ...spec.spec.workers.map((w): RunComponent => ({ kind: 'worker', name: w.name })),\n ...spec.spec.apps.map((a): RunComponent => ({ kind: 'app', name: a.name })),\n ];\n case 'kernel':\n return [{ kind: 'kernel' }];\n case 'worker':\n case 'app': {\n // Only a declared name: an unknown one is refused below, before it can\n // name a lock file.\n const declared = target === 'worker' ? spec.spec.workers : spec.spec.apps;\n return declared.some((c) => c.name === name) ? [{ kind: target, name }] : [];\n }\n default:\n return [];\n }\n}\n\nasync function runTarget(\n target: RunTarget,\n name: string | undefined,\n dir: string,\n spec: StackSpec,\n opts: { allowWrite?: boolean } = {},\n): Promise<Stoppable[]> {\n try {\n acquireRunLocks(dir, lockedComponents(target, name, spec));\n switch (target) {\n case 'all':\n return await runAll(dir, spec);\n\n case 'kernel': {\n const kernel = await runKernel(dir);\n process.stderr.write(`\\n${theme.ok('Kernel running.')} ${theme.muted('Press Ctrl+C to stop.')}\\n`);\n return [{ label: 'kernel', stop: () => kernel.stop() }];\n }\n\n case 'gateway': {\n const gateway = await runGateway(dir, spec);\n process.stderr.write(`\\n${theme.ok('Gateway running.')} ${theme.muted('Press Ctrl+C to stop.')}\\n`);\n return [{ label: 'gateway', stop: () => stopGateway(gateway) }];\n }\n\n case 'mcp': {\n const mcp = await runMcp(dir, spec, undefined, { allowWrite: opts.allowWrite });\n process.stderr.write(`\\n${theme.ok('MCP server running.')} ${theme.muted('Press Ctrl+C to stop.')}\\n`);\n return [{ label: 'mcp', stop: () => mcp.stop() }];\n }\n\n case 'worker': {\n if (!name) die('`mdk run worker` requires a worker <name> (see spec.workers in mdk.yaml).');\n const worker = spec.spec.workers.find((w) => w.name === name);\n if (!worker) {\n const known = spec.spec.workers.map((w) => w.name).join(', ') || '(none)';\n die(`No worker named \"${name}\" in mdk.yaml. Known workers: ${known}.`);\n }\n const handle = await runWorker(dir, worker);\n process.stderr.write(`\\n${theme.ok('Worker running.')} ${theme.muted('Press Ctrl+C to stop.')}\\n`);\n return [{ label: `worker ${worker.name}`, stop: () => handle.stop() }];\n }\n\n case 'app': {\n if (!name) die('`mdk run app` requires an app <name> (see spec.apps in mdk.yaml).');\n const app = spec.spec.apps.find((a) => a.name === name);\n if (!app) {\n const known = spec.spec.apps.map((a) => a.name).join(', ') || '(none)';\n die(`No app named \"${name}\" in mdk.yaml. Known apps: ${known}.`);\n }\n const handle = await runApp(dir, app);\n process.stderr.write(`\\n${theme.ok('App running.')} ${theme.muted('Press Ctrl+C to stop.')}\\n`);\n return [{ label: `app ${app.name}`, stop: () => handle.stop() }];\n }\n\n case 'dashboard': {\n const dashboardDir = findDashboardDir(dir);\n if (!dashboardDir) {\n die(\n `No dashboard app found under ${resolve(dir)}. Scaffold one first: ${theme.cmd('mdk create dashboard')}`,\n );\n }\n // Installed apps are found here, from mdk.yaml, and handed to the\n // dashboard: it serves their UIs itself, so no Gateway is involved.\n const apps = describeDashboardApps(dir, spec.spec.apps ?? []);\n if (apps.length) {\n process.stderr.write(`${theme.muted('Apps:')} ${apps.map((a) => `${a.displayName} (${a.name})`).join(', ')}\\n`);\n }\n const handle = await runDashboard(dashboardDir, apps);\n process.stderr.write(\n `\\n${theme.ok('Dashboard running.')} ${theme.muted('Press Ctrl+C to stop.')}\\n`,\n );\n return [{ label: 'dashboard', stop: () => handle.stop() }];\n }\n }\n } catch (error) {\n die(error instanceof Error ? error.message : String(error));\n }\n}\n\n// Group C — Run & manage\nexport function registerRun(program: Command): void {\n program\n .command('run')\n .argument('[target]', `What to start: ${TARGETS.join(' | ')} (default: all)`)\n .argument('[name]', 'The worker or app to start (worker and app targets only)')\n .summary('Start the stack, or one component of it')\n .description(\n 'Start the Kernel, workers, app runtimes and Gateway from mdk.yaml (target all), or one component. ' +\n 'mcp and dashboard are never part of all; start them on their own. Runs in the foreground; Ctrl+C stops it.',\n )\n .option('-d, --dir <path>', 'Project directory containing mdk.yaml', '.')\n .addOption(new Option('--detach', 'Run in the background (not implemented yet)').default(false).hideHelp())\n .option('--allow-write', 'mcp: expose write tools (hidden by default; or set spec.mcp.allowWrite)', false)\n .addHelpText(\n 'after',\n examples('mdk run', 'mdk run kernel', 'mdk run worker rig', 'mdk run app ops-center', 'mdk run gateway --dir ./my-stack'),\n )\n .action(async (target: string | undefined, name: string | undefined, opts: RunOptions) => {\n if (opts.detach) {\n die('`--detach` is not implemented yet. Run in the foreground (omit --detach).');\n }\n\n if (target && !TARGETS.includes(target as RunTarget)) {\n die(`Unknown target \"${target}\". Use one of: ${TARGETS.join(' | ')}.`);\n }\n\n const resolvedTarget = (target as RunTarget | undefined) ?? 'all';\n if (name !== undefined && !NAMED_TARGETS.includes(resolvedTarget)) {\n die(`\\`mdk run ${resolvedTarget}\\` takes no name (got \"${name}\"); only worker and app do.`);\n }\n\n const spec = loadSpecOrDie(opts.dir);\n\n // No explicit target: boot everything together. Components can always be\n // run separately instead — `mdk run kernel` / `gateway` / `worker <name>`\n // — that choice is just which command(s) you type, not a spec setting.\n const running = await runTarget(resolvedTarget, name, opts.dir, spec, { allowWrite: opts.allowWrite });\n\n // Registered only once everything is up: a boot that fails exits on its\n // own, and until then the default signal behavior is the right one.\n installShutdown(running);\n });\n}\n"]}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { resolve } from 'node:path';
|
|
2
|
+
import { validateApp } from '../lib/app-package.js';
|
|
3
|
+
import { resolvePackageDir } from '../lib/runtime.js';
|
|
4
|
+
import { loadStackSpec, STACK_FILE } from '../lib/spec.js';
|
|
5
|
+
import { validateWorker } from '../lib/worker-package.js';
|
|
6
|
+
import { runWorkerSmoke } from '../lib/worker-smoke.js';
|
|
7
|
+
import { examples } from '../lib/help.js';
|
|
8
|
+
import { theme, tick } from '../lib/theme.js';
|
|
9
|
+
const count = (n, noun, plural = `${noun}s`) => `${n} ${n === 1 ? noun : plural}`;
|
|
10
|
+
/**
|
|
11
|
+
* The package directory of the app or worker `mdk.yaml` declares as `name`
|
|
12
|
+
* (`spec.apps[]` / `spec.workers[]`), resolved as `mdk run` resolves it.
|
|
13
|
+
*/
|
|
14
|
+
function resolveDeclared(kind, name, projectDir) {
|
|
15
|
+
const section = kind === 'app' ? 'apps' : 'workers';
|
|
16
|
+
const entries = loadStackSpec(projectDir).spec[section];
|
|
17
|
+
const entry = entries.find((e) => e.name === name);
|
|
18
|
+
if (!entry) {
|
|
19
|
+
throw new Error(`No ${kind} named "${name}" in ${STACK_FILE} (spec.${section}: ${entries.map((e) => e.name).join(', ') || 'none'}). ` +
|
|
20
|
+
`Add it with \`mdk create ${kind}\` or \`mdk add ${kind}\`.`);
|
|
21
|
+
}
|
|
22
|
+
return resolvePackageDir(projectDir, entry.package);
|
|
23
|
+
}
|
|
24
|
+
function check(kind, dir) {
|
|
25
|
+
if (kind === 'app') {
|
|
26
|
+
// The author's own package: also load the UI to check its exports.
|
|
27
|
+
const { errors, warnings, manifest, routes, ui } = validateApp(dir, { runUiContract: true });
|
|
28
|
+
return {
|
|
29
|
+
errors,
|
|
30
|
+
warnings,
|
|
31
|
+
summary: () => `${theme.label(manifest.displayName)} ${theme.muted(`(${manifest.id} ${manifest.version})`)} is a valid app: ` +
|
|
32
|
+
`${count(routes, 'route')}, ${count(manifest.permissions?.length ?? 0, 'permission rule')}${ui ? ', UI' : ''}`,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
const { errors, name, version, telemetry, commands } = validateWorker(dir);
|
|
36
|
+
return {
|
|
37
|
+
errors,
|
|
38
|
+
name,
|
|
39
|
+
summary: () => `${theme.label(name)} ${theme.muted(`(${version})`)} is a valid worker: ` +
|
|
40
|
+
`${count(telemetry, 'telemetry entry', 'telemetry entries')}, ${count(commands, 'command')}`,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
/** Runs one validation and prints it; sets the exit code to 1 when the package is not valid. */
|
|
44
|
+
export async function runValidate(kind, name, opts = {}) {
|
|
45
|
+
let dir = null;
|
|
46
|
+
let outcome;
|
|
47
|
+
try {
|
|
48
|
+
// --path checks the package directory itself — the partner-CI mode, where
|
|
49
|
+
// the repo is the package and there is no mdk.yaml to declare it.
|
|
50
|
+
if (opts.path && name)
|
|
51
|
+
throw new Error(`Pass a ${STACK_FILE} name or --path, not both.`);
|
|
52
|
+
if (!opts.path && !name)
|
|
53
|
+
throw new Error(`Pass the ${kind}'s name in ${STACK_FILE}, or --path <dir> to check a package directory.`);
|
|
54
|
+
dir = opts.path ? resolve(opts.path) : resolveDeclared(kind, name, opts.dir ?? '.');
|
|
55
|
+
outcome = check(kind, dir);
|
|
56
|
+
// Behavioural checks only on a statically valid package — a partner fixes
|
|
57
|
+
// contract problems before behaviour, and smoke needs a loadable contract.
|
|
58
|
+
if (kind === 'worker' && opts.smoke && outcome.errors.length === 0) {
|
|
59
|
+
const smoke = await runWorkerSmoke(dir, { config: opts.smokeConfig, timeoutMs: Number(opts.timeout) || undefined });
|
|
60
|
+
outcome.errors.push(...smoke.failures);
|
|
61
|
+
outcome.warnings = [...(outcome.warnings ?? []), ...smoke.warnings];
|
|
62
|
+
const summary = outcome.summary;
|
|
63
|
+
outcome.summary = () => `${summary()}; ${count(smoke.passed, 'smoke check')} passed`;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
outcome = { errors: [error instanceof Error ? error.message : String(error)], summary: () => '' };
|
|
68
|
+
}
|
|
69
|
+
const { errors, summary } = outcome;
|
|
70
|
+
const warnings = outcome.warnings ?? [];
|
|
71
|
+
if (opts.json) {
|
|
72
|
+
const reported = name ?? outcome.name ?? null;
|
|
73
|
+
process.stdout.write(`${JSON.stringify({ ok: errors.length === 0, kind, name: reported, path: dir, errors, warnings })}\n`);
|
|
74
|
+
if (errors.length)
|
|
75
|
+
process.exitCode = 1;
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
for (const warning of warnings)
|
|
79
|
+
process.stderr.write(`${theme.warn('warning')}: ${warning}\n`);
|
|
80
|
+
if (errors.length) {
|
|
81
|
+
for (const error of errors)
|
|
82
|
+
process.stderr.write(`${theme.warn('error')}: ${error}\n`);
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
process.stdout.write(`${tick} ${summary()}\n`);
|
|
86
|
+
}
|
|
87
|
+
if (errors.length)
|
|
88
|
+
process.exitCode = 1;
|
|
89
|
+
}
|
|
90
|
+
// Group B — Validate
|
|
91
|
+
export function registerValidate(program) {
|
|
92
|
+
const validate = program
|
|
93
|
+
.command('validate')
|
|
94
|
+
.usage('<command> [options]')
|
|
95
|
+
.summary('Check an app or worker package')
|
|
96
|
+
.description('Check an app or worker package and list every problem. Exits 1 if it is not valid.')
|
|
97
|
+
.addHelpText('after', examples('mdk validate app ops-center', 'mdk validate worker rig --json'));
|
|
98
|
+
const name = (section) => `Its name in ${STACK_FILE} (${section})`;
|
|
99
|
+
validate
|
|
100
|
+
.command('app')
|
|
101
|
+
.argument('<name>', name('spec.apps'))
|
|
102
|
+
.summary('Check an app package')
|
|
103
|
+
.description('Check the package of an app mdk.yaml declares: mdk-app.yaml, its version against package.json, the permission rules, ' +
|
|
104
|
+
'the route table, including that every handler file exists, and the UI build with its contract exports.')
|
|
105
|
+
.option('--dir <path>', 'Project directory (the one holding mdk.yaml)', '.')
|
|
106
|
+
.option('--json', 'Print the result as JSON: { ok, kind, name, path, errors, warnings }', false)
|
|
107
|
+
.addHelpText('after', examples('mdk validate app ops-center', 'mdk validate app ops-center --json'))
|
|
108
|
+
.action((app, opts) => runValidate('app', app, opts));
|
|
109
|
+
validate
|
|
110
|
+
.command('worker')
|
|
111
|
+
.argument('[name]', name('spec.workers'))
|
|
112
|
+
.summary('Check a worker package')
|
|
113
|
+
.description('Check a worker package: package.json and its mdk-contract.json Worker Plugin contract, validated against ' +
|
|
114
|
+
'mdk-contract.schema.json and its semantic rules — unique names, min/max on number params, every handler file ' +
|
|
115
|
+
'on disk (handlers are not run). Name a worker mdk.yaml declares, or check any package directory with --path.')
|
|
116
|
+
.option('--dir <path>', 'Project directory (the one holding mdk.yaml)', '.')
|
|
117
|
+
.option('--path <dir>', 'Check this package directory instead of a name declared in mdk.yaml')
|
|
118
|
+
.option('--smoke', 'Also run behavioural checks against the mock device smoke.config.js sets up', false)
|
|
119
|
+
.option('--smoke-config <json|path>', 'Device spec for --smoke: inline JSON, a .json file, or a module with setup()')
|
|
120
|
+
.option('--timeout <ms>', 'Deadline per smoke handler call', '10000')
|
|
121
|
+
.option('--json', 'Print the result as JSON: { ok, kind, name, path, errors, warnings }', false)
|
|
122
|
+
.addHelpText('after', examples('mdk validate worker rig', 'mdk validate worker rig --json', 'mdk validate worker --path . --smoke --json'))
|
|
123
|
+
.action((worker, opts) => runValidate('worker', worker, opts));
|
|
124
|
+
}
|
|
125
|
+
//# sourceMappingURL=validate.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validate.js","sourceRoot":"","sources":["../../src/commands/validate.ts"],"names":[],"mappings":"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","sourcesContent":["import type { Command } from 'commander';\nimport { resolve } from 'node:path';\nimport { validateApp } from '../lib/app-package.js';\nimport { resolvePackageDir } from '../lib/runtime.js';\nimport { loadStackSpec, STACK_FILE } from '../lib/spec.js';\nimport { validateWorker } from '../lib/worker-package.js';\nimport { runWorkerSmoke } from '../lib/worker-smoke.js';\nimport { examples } from '../lib/help.js';\nimport { theme, tick } from '../lib/theme.js';\n\nconst count = (n: number, noun: string, plural = `${noun}s`): string => `${n} ${n === 1 ? noun : plural}`;\n\ntype Kind = 'app' | 'worker';\n\n/**\n * The package directory of the app or worker `mdk.yaml` declares as `name`\n * (`spec.apps[]` / `spec.workers[]`), resolved as `mdk run` resolves it.\n */\nfunction resolveDeclared(kind: Kind, name: string, projectDir: string): string {\n const section = kind === 'app' ? 'apps' : 'workers';\n const entries: Array<{ name: string; package: string }> = loadStackSpec(projectDir).spec[section];\n const entry = entries.find((e) => e.name === name);\n if (!entry) {\n throw new Error(\n `No ${kind} named \"${name}\" in ${STACK_FILE} (spec.${section}: ${entries.map((e) => e.name).join(', ') || 'none'}). ` +\n `Add it with \\`mdk create ${kind}\\` or \\`mdk add ${kind}\\`.`,\n );\n }\n return resolvePackageDir(projectDir, entry.package);\n}\n\n/** The problems found, and how to describe the package when there are none. */\ninterface Outcome {\n errors: string[];\n /** Problems that do not fail the check (a stale or empty UI build). */\n warnings?: string[];\n /** The package's own name, for a package checked by --path rather than by mdk.yaml name. */\n name?: string;\n summary: () => string;\n}\n\nfunction check(kind: Kind, dir: string): Outcome {\n if (kind === 'app') {\n // The author's own package: also load the UI to check its exports.\n const { errors, warnings, manifest, routes, ui } = validateApp(dir, { runUiContract: true });\n return {\n errors,\n warnings,\n summary: () =>\n `${theme.label(manifest.displayName)} ${theme.muted(`(${manifest.id} ${manifest.version})`)} is a valid app: ` +\n `${count(routes, 'route')}, ${count(manifest.permissions?.length ?? 0, 'permission rule')}${ui ? ', UI' : ''}`,\n };\n }\n const { errors, name, version, telemetry, commands } = validateWorker(dir);\n return {\n errors,\n name,\n summary: () =>\n `${theme.label(name!)} ${theme.muted(`(${version})`)} is a valid worker: ` +\n `${count(telemetry, 'telemetry entry', 'telemetry entries')}, ${count(commands, 'command')}`,\n };\n}\n\ninterface ValidateOpts {\n json?: boolean;\n dir?: string;\n path?: string;\n smoke?: boolean;\n smokeConfig?: string;\n timeout?: string;\n}\n\n/** Runs one validation and prints it; sets the exit code to 1 when the package is not valid. */\nexport async function runValidate(kind: Kind, name: string | undefined, opts: ValidateOpts = {}): Promise<void> {\n let dir: string | null = null;\n let outcome: Outcome;\n try {\n // --path checks the package directory itself — the partner-CI mode, where\n // the repo is the package and there is no mdk.yaml to declare it.\n if (opts.path && name) throw new Error(`Pass a ${STACK_FILE} name or --path, not both.`);\n if (!opts.path && !name) throw new Error(`Pass the ${kind}'s name in ${STACK_FILE}, or --path <dir> to check a package directory.`);\n dir = opts.path ? resolve(opts.path) : resolveDeclared(kind, name!, opts.dir ?? '.');\n outcome = check(kind, dir);\n // Behavioural checks only on a statically valid package — a partner fixes\n // contract problems before behaviour, and smoke needs a loadable contract.\n if (kind === 'worker' && opts.smoke && outcome.errors.length === 0) {\n const smoke = await runWorkerSmoke(dir, { config: opts.smokeConfig, timeoutMs: Number(opts.timeout) || undefined });\n outcome.errors.push(...smoke.failures);\n outcome.warnings = [...(outcome.warnings ?? []), ...smoke.warnings];\n const summary = outcome.summary;\n outcome.summary = () => `${summary()}; ${count(smoke.passed, 'smoke check')} passed`;\n }\n } catch (error) {\n outcome = { errors: [error instanceof Error ? error.message : String(error)], summary: () => '' };\n }\n const { errors, summary } = outcome;\n const warnings = outcome.warnings ?? [];\n if (opts.json) {\n const reported = name ?? outcome.name ?? null;\n process.stdout.write(`${JSON.stringify({ ok: errors.length === 0, kind, name: reported, path: dir, errors, warnings })}\\n`);\n if (errors.length) process.exitCode = 1;\n return;\n }\n for (const warning of warnings) process.stderr.write(`${theme.warn('warning')}: ${warning}\\n`);\n if (errors.length) {\n for (const error of errors) process.stderr.write(`${theme.warn('error')}: ${error}\\n`);\n } else {\n process.stdout.write(`${tick} ${summary()}\\n`);\n }\n if (errors.length) process.exitCode = 1;\n}\n\n// Group B — Validate\nexport function registerValidate(program: Command): void {\n const validate = program\n .command('validate')\n .usage('<command> [options]')\n .summary('Check an app or worker package')\n .description('Check an app or worker package and list every problem. Exits 1 if it is not valid.')\n .addHelpText('after', examples('mdk validate app ops-center', 'mdk validate worker rig --json'));\n\n const name = (section: string): string => `Its name in ${STACK_FILE} (${section})`;\n\n validate\n .command('app')\n .argument('<name>', name('spec.apps'))\n .summary('Check an app package')\n .description(\n 'Check the package of an app mdk.yaml declares: mdk-app.yaml, its version against package.json, the permission rules, ' +\n 'the route table, including that every handler file exists, and the UI build with its contract exports.',\n )\n .option('--dir <path>', 'Project directory (the one holding mdk.yaml)', '.')\n .option('--json', 'Print the result as JSON: { ok, kind, name, path, errors, warnings }', false)\n .addHelpText('after', examples('mdk validate app ops-center', 'mdk validate app ops-center --json'))\n .action((app: string, opts: { json: boolean; dir: string }) => runValidate('app', app, opts));\n\n validate\n .command('worker')\n .argument('[name]', name('spec.workers'))\n .summary('Check a worker package')\n .description(\n 'Check a worker package: package.json and its mdk-contract.json Worker Plugin contract, validated against ' +\n 'mdk-contract.schema.json and its semantic rules — unique names, min/max on number params, every handler file ' +\n 'on disk (handlers are not run). Name a worker mdk.yaml declares, or check any package directory with --path.',\n )\n .option('--dir <path>', 'Project directory (the one holding mdk.yaml)', '.')\n .option('--path <dir>', 'Check this package directory instead of a name declared in mdk.yaml')\n .option('--smoke', 'Also run behavioural checks against the mock device smoke.config.js sets up', false)\n .option('--smoke-config <json|path>', 'Device spec for --smoke: inline JSON, a .json file, or a module with setup()')\n .option('--timeout <ms>', 'Deadline per smoke handler call', '10000')\n .option('--json', 'Print the result as JSON: { ok, kind, name, path, errors, warnings }', false)\n .addHelpText(\n 'after',\n examples(\n 'mdk validate worker rig',\n 'mdk validate worker rig --json',\n 'mdk validate worker --path . --smoke --json',\n ),\n )\n .action((worker: string | undefined, opts: ValidateOpts) => runValidate('worker', worker, opts));\n}\n"]}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { buildProgram } from './program.js';
|
|
3
|
+
const program = buildProgram();
|
|
4
|
+
program.parseAsync(process.argv).catch((error) => {
|
|
5
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
6
|
+
process.stderr.write(`${message}\n`);
|
|
7
|
+
process.exit(1);
|
|
8
|
+
});
|
|
9
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAE5C,MAAM,OAAO,GAAG,YAAY,EAAE,CAAC;AAE/B,OAAO,CAAC,UAAU,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,KAAK,CAAC,CAAC,KAAc,EAAE,EAAE;IACxD,MAAM,OAAO,GAAG,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;IACvE,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,OAAO,IAAI,CAAC,CAAC;IACrC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC","sourcesContent":["#!/usr/bin/env node\nimport { buildProgram } from './program.js';\n\nconst program = buildProgram();\n\nprogram.parseAsync(process.argv).catch((error: unknown) => {\n const message = error instanceof Error ? error.message : String(error);\n process.stderr.write(`${message}\\n`);\n process.exit(1);\n});\n"]}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { existsSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { join, resolve } from 'node:path';
|
|
3
|
+
import { parse as parseYaml } from 'yaml';
|
|
4
|
+
import { validateApp } from './app-package.js';
|
|
5
|
+
import { addAppToStackFile } from './app-scaffold.js';
|
|
6
|
+
import { installDeps, installPackages } from './npm.js';
|
|
7
|
+
import { parseSource } from './package-source.js';
|
|
8
|
+
import { ensureProjectManifest, readStackName } from './project.js';
|
|
9
|
+
import { resolveProjectPackageDir } from './runtime.js';
|
|
10
|
+
import { shortName, toAppName } from './spec.js';
|
|
11
|
+
import { addWorkerToStackFile } from './worker-scaffold.js';
|
|
12
|
+
import { validateWorker } from './worker-package.js';
|
|
13
|
+
const fail = (message, extra = {}) => ({
|
|
14
|
+
ok: false,
|
|
15
|
+
message,
|
|
16
|
+
...extra,
|
|
17
|
+
});
|
|
18
|
+
function readDependencies(dir) {
|
|
19
|
+
try {
|
|
20
|
+
const pkg = JSON.parse(readFileSync(join(dir, 'package.json'), 'utf8'));
|
|
21
|
+
return pkg.dependencies ?? {};
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return {};
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
/** The dependency npm just added: new, or changed, or (for a re-add) pointing at the spec we asked for. */
|
|
28
|
+
function installedName(before, after, spec) {
|
|
29
|
+
const changed = Object.keys(after).filter((k) => after[k] !== before[k]);
|
|
30
|
+
if (changed.length === 1)
|
|
31
|
+
return changed[0];
|
|
32
|
+
return Object.keys(after).find((k) => after[k] === spec || after[k]?.includes(spec.replace(/^github:/, '')));
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* A third-party package's install scripts would run before it is validated, and
|
|
36
|
+
* a range would let the next install pick a version nobody checked.
|
|
37
|
+
*/
|
|
38
|
+
const ADD_INSTALL_ARGS = ['--ignore-scripts', '--save-exact'];
|
|
39
|
+
const MANIFESTS = ['package.json', 'package-lock.json'];
|
|
40
|
+
function snapshot(dir) {
|
|
41
|
+
return MANIFESTS.map((name) => {
|
|
42
|
+
const file = join(dir, name);
|
|
43
|
+
return [file, existsSync(file) ? readFileSync(file, 'utf8') : null];
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Puts package.json and the lockfile back as they were and resyncs node_modules,
|
|
48
|
+
* so a refused package neither stays installed nor replaces the version an app
|
|
49
|
+
* already listed in mdk.yaml runs.
|
|
50
|
+
*/
|
|
51
|
+
function restore(dir, saved) {
|
|
52
|
+
for (const [file, content] of saved) {
|
|
53
|
+
if (content === null)
|
|
54
|
+
rmSync(file, { force: true });
|
|
55
|
+
else
|
|
56
|
+
writeFileSync(file, content, 'utf8');
|
|
57
|
+
}
|
|
58
|
+
installDeps(dir, true, ['--ignore-scripts']);
|
|
59
|
+
}
|
|
60
|
+
/** The name `spec.apps` already runs `reference` under, if it lists it. */
|
|
61
|
+
function listedAppName(dir, reference) {
|
|
62
|
+
try {
|
|
63
|
+
const doc = parseYaml(readFileSync(resolve(dir, 'mdk.yaml'), 'utf8'));
|
|
64
|
+
return doc.spec?.apps?.find((a) => a.package === reference)?.name;
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
return undefined;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* `mdk add app|worker <source>`: installs the package from npm or GitHub, checks it is a
|
|
72
|
+
* valid app or worker, and registers it in `mdk.yaml`. `package.json` is updated by npm
|
|
73
|
+
* itself, and restored if the package is refused. `mdk.yaml` is only touched once the
|
|
74
|
+
* package validates.
|
|
75
|
+
*/
|
|
76
|
+
export function addPackage(opts) {
|
|
77
|
+
const stackName = readStackName(opts.dir);
|
|
78
|
+
if (!stackName) {
|
|
79
|
+
return fail(`No mdk.yaml in ${opts.dir}. Run \`mdk onboard\` first, or pass --dir <project>.`);
|
|
80
|
+
}
|
|
81
|
+
let source;
|
|
82
|
+
try {
|
|
83
|
+
source = parseSource(opts.source);
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
return fail(error instanceof Error ? error.message : String(error));
|
|
87
|
+
}
|
|
88
|
+
const install = opts.install !== false;
|
|
89
|
+
if (!install && source.kind === 'github') {
|
|
90
|
+
return fail('A GitHub source has to be installed to learn its package name; drop --no-install.');
|
|
91
|
+
}
|
|
92
|
+
try {
|
|
93
|
+
ensureProjectManifest(opts.dir, stackName);
|
|
94
|
+
}
|
|
95
|
+
catch (error) {
|
|
96
|
+
return fail(`Could not prepare package.json: ${error instanceof Error ? error.message : String(error)}`);
|
|
97
|
+
}
|
|
98
|
+
const before = readDependencies(opts.dir);
|
|
99
|
+
const saved = install ? snapshot(opts.dir) : null;
|
|
100
|
+
const refuse = (message, extra = {}) => {
|
|
101
|
+
if (saved)
|
|
102
|
+
restore(opts.dir, saved);
|
|
103
|
+
return fail(saved ? `${message} package.json was restored.` : message, extra);
|
|
104
|
+
};
|
|
105
|
+
const result = installPackages(opts.dir, [source.installSpec], install, ADD_INSTALL_ARGS);
|
|
106
|
+
if (!result.ok)
|
|
107
|
+
return refuse(result.message ?? 'npm install failed.');
|
|
108
|
+
const packageName = source.packageName ?? installedName(before, readDependencies(opts.dir), source.installSpec);
|
|
109
|
+
if (!packageName) {
|
|
110
|
+
return refuse(`Installed ${source.installSpec} but could not tell which package it added.`);
|
|
111
|
+
}
|
|
112
|
+
const reference = source.subpath ? `${packageName}/${source.subpath}` : packageName;
|
|
113
|
+
let packageDir;
|
|
114
|
+
try {
|
|
115
|
+
packageDir = resolveProjectPackageDir(opts.dir, reference);
|
|
116
|
+
}
|
|
117
|
+
catch (error) {
|
|
118
|
+
return refuse(error instanceof Error ? error.message : String(error), { packageName });
|
|
119
|
+
}
|
|
120
|
+
const errors = opts.kind === 'app' ? validateApp(packageDir).errors : validateWorker(packageDir).errors;
|
|
121
|
+
if (errors.length) {
|
|
122
|
+
return refuse(`${reference} is not a valid ${opts.kind}; mdk.yaml was not changed.`, {
|
|
123
|
+
errors,
|
|
124
|
+
packageName,
|
|
125
|
+
reference,
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
const instanceName = toAppName(opts.name?.trim() || shortName(source.subpath ?? packageName));
|
|
129
|
+
if (opts.kind === 'app') {
|
|
130
|
+
// A re-add without --name updates the app already running this package;
|
|
131
|
+
// another --name is a second app.
|
|
132
|
+
const listed = opts.name ? undefined : listedAppName(opts.dir, reference);
|
|
133
|
+
const stackFile = addAppToStackFile(opts.dir, reference, listed ?? instanceName);
|
|
134
|
+
if (stackFile === 'name-taken') {
|
|
135
|
+
return refuse(`spec.apps already has an app named "${instanceName}" with another package; mdk.yaml was not changed. ` +
|
|
136
|
+
'Pick another name with --name.', { packageName, reference, instanceName, stackFile });
|
|
137
|
+
}
|
|
138
|
+
return { ok: true, packageName, reference, instanceName: listed ?? instanceName, stackFile, installed: install };
|
|
139
|
+
}
|
|
140
|
+
const mock = existsSync(join(packageDir, 'mock', 'server.js'));
|
|
141
|
+
const { update } = addWorkerToStackFile(opts.dir, instanceName, reference, { mock });
|
|
142
|
+
if (update === 'name-taken') {
|
|
143
|
+
return refuse(`spec.workers already has a worker named "${instanceName}" with another package; mdk.yaml was not changed. ` +
|
|
144
|
+
'Pick another name with --name.', { packageName, reference, instanceName, stackFile: update });
|
|
145
|
+
}
|
|
146
|
+
return { ok: true, packageName, reference, instanceName, stackFile: update, needsDeviceOpts: !mock, installed: install };
|
|
147
|
+
}
|
|
148
|
+
//# sourceMappingURL=add-package.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"add-package.js","sourceRoot":"","sources":["../../src/lib/add-package.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAC1E,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,EAAE,KAAK,IAAI,SAAS,EAAE,MAAM,MAAM,CAAC;AAC1C,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAC/C,OAAO,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AACtD,OAAO,EAAE,WAAW,EAAE,eAAe,EAAE,MAAM,UAAU,CAAC;AACxD,OAAO,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAClD,OAAO,EAAE,qBAAqB,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AACpE,OAAO,EAAE,wBAAwB,EAAE,MAAM,cAAc,CAAC;AACxD,OAAO,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAEjD,OAAO,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AAC5D,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAgCrD,MAAM,IAAI,GAAG,CAAC,OAAe,EAAE,KAAK,GAA8B,EAAE,EAAoB,EAAE,CAAC,CAAC;IAC1F,EAAE,EAAE,KAAK;IACT,OAAO;IACP,GAAG,KAAK;CACT,CAAC,CAAC;AAEH,SAAS,gBAAgB,CAAC,GAAW;IACnC,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,IAAI,CAAC,GAAG,EAAE,cAAc,CAAC,EAAE,MAAM,CAAC,CAErE,CAAC;QACF,OAAO,GAAG,CAAC,YAAY,IAAI,EAAE,CAAC;IAChC,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,CAAC;IACZ,CAAC;AACH,CAAC;AAED,2GAA2G;AAC3G,SAAS,aAAa,CAAC,MAA8B,EAAE,KAA6B,EAAE,IAAY;IAChG,MAAM,OAAO,GAAG,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,KAAK,CAAC,CAAC,CAAC,KAAK,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC;IACzE,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,OAAO,CAAC,CAAC,CAAC,CAAC;IAC5C,OAAO,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,KAAK,CAAC,CAAC,CAAC,KAAK,IAAI,IAAI,KAAK,CAAC,CAAC,CAAC,EAAE,QAAQ,CAAC,IAAI,CAAC,OAAO,CAAC,UAAU,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC;AAC/G,CAAC;AAED;;;GAGG;AACH,MAAM,gBAAgB,GAAG,CAAC,kBAAkB,EAAE,cAAc,CAAC,CAAC;AAE9D,MAAM,SAAS,GAAG,CAAC,cAAc,EAAE,mBAAmB,CAAC,CAAC;AAIxD,SAAS,QAAQ,CAAC,GAAW;IAC3B,OAAO,SAAS,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE;QAC5B,MAAM,IAAI,GAAG,IAAI,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;QAC7B,OAAO,CAAC,IAAI,EAAE,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,YAAY,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;IACtE,CAAC,CAAC,CAAC;AACL,CAAC;AAED;;;;GAIG;AACH,SAAS,OAAO,CAAC,GAAW,EAAE,KAAe;IAC3C,KAAK,MAAM,CAAC,IAAI,EAAE,OAAO,CAAC,IAAI,KAAK,EAAE,CAAC;QACpC,IAAI,OAAO,KAAK,IAAI;YAAE,MAAM,CAAC,IAAI,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;;YAC/C,aAAa,CAAC,IAAI,EAAE,OAAO,EAAE,MAAM,CAAC,CAAC;IAC5C,CAAC;IACD,WAAW,CAAC,GAAG,EAAE,IAAI,EAAE,CAAC,kBAAkB,CAAC,CAAC,CAAC;AAC/C,CAAC;AAED,2EAA2E;AAC3E,SAAS,aAAa,CAAC,GAAW,EAAE,SAAiB;IACnD,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,SAAS,CAAC,YAAY,CAAC,OAAO,CAAC,GAAG,EAAE,UAAU,CAAC,EAAE,MAAM,CAAC,CAEnE,CAAC;QACF,OAAO,GAAG,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,KAAK,SAAS,CAAC,EAAE,IAAI,CAAC;IACpE,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,SAAS,CAAC;IACnB,CAAC;AACH,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,UAAU,CAAC,IAAuB;IAChD,MAAM,SAAS,GAAG,aAAa,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IAC1C,IAAI,CAAC,SAAS,EAAE,CAAC;QACf,OAAO,IAAI,CAAC,kBAAkB,IAAI,CAAC,GAAG,uDAAuD,CAAC,CAAC;IACjG,CAAC;IAED,IAAI,MAAM,CAAC;IACX,IAAI,CAAC;QACH,MAAM,GAAG,WAAW,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IACpC,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,OAAO,IAAI,CAAC,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;IACtE,CAAC;IAED,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,KAAK,KAAK,CAAC;IACvC,IAAI,CAAC,OAAO,IAAI,MAAM,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;QACzC,OAAO,IAAI,CAAC,mFAAmF,CAAC,CAAC;IACnG,CAAC;IAED,IAAI,CAAC;QACH,qBAAqB,CAAC,IAAI,CAAC,GAAG,EAAE,SAAS,CAAC,CAAC;IAC7C,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,OAAO,IAAI,CAAC,mCAAmC,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;IAC3G,CAAC;IAED,MAAM,MAAM,GAAG,gBAAgB,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IAC1C,MAAM,KAAK,GAAG,OAAO,CAAC,CAAC,CAAC,QAAQ,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;IAClD,MAAM,MAAM,GAAG,CAAC,OAAe,EAAE,KAAK,GAA8B,EAAE,EAAoB,EAAE;QAC1F,IAAI,KAAK;YAAE,OAAO,CAAC,IAAI,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;QACpC,OAAO,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,GAAG,OAAO,6BAA6B,CAAC,CAAC,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;IAChF,CAAC,CAAC;IAEF,MAAM,MAAM,GAAG,eAAe,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,MAAM,CAAC,WAAW,CAAC,EAAE,OAAO,EAAE,gBAAgB,CAAC,CAAC;IAC1F,IAAI,CAAC,MAAM,CAAC,EAAE;QAAE,OAAO,MAAM,CAAC,MAAM,CAAC,OAAO,IAAI,qBAAqB,CAAC,CAAC;IAEvE,MAAM,WAAW,GAAG,MAAM,CAAC,WAAW,IAAI,aAAa,CAAC,MAAM,EAAE,gBAAgB,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC,WAAW,CAAC,CAAC;IAChH,IAAI,CAAC,WAAW,EAAE,CAAC;QACjB,OAAO,MAAM,CAAC,aAAa,MAAM,CAAC,WAAW,6CAA6C,CAAC,CAAC;IAC9F,CAAC;IACD,MAAM,SAAS,GAAG,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,WAAW,IAAI,MAAM,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,WAAW,CAAC;IAEpF,IAAI,UAAkB,CAAC;IACvB,IAAI,CAAC;QACH,UAAU,GAAG,wBAAwB,CAAC,IAAI,CAAC,GAAG,EAAE,SAAS,CAAC,CAAC;IAC7D,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,OAAO,MAAM,CAAC,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,EAAE,WAAW,EAAE,CAAC,CAAC;IACzF,CAAC;IAED,MAAM,MAAM,GAAG,IAAI,CAAC,IAAI,KAAK,KAAK,CAAC,CAAC,CAAC,WAAW,CAAC,UAAU,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,cAAc,CAAC,UAAU,CAAC,CAAC,MAAM,CAAC;IACxG,IAAI,MAAM,CAAC,MAAM,EAAE,CAAC;QAClB,OAAO,MAAM,CAAC,GAAG,SAAS,mBAAmB,IAAI,CAAC,IAAI,6BAA6B,EAAE;YACnF,MAAM;YACN,WAAW;YACX,SAAS;SACV,CAAC,CAAC;IACL,CAAC;IAED,MAAM,YAAY,GAAG,SAAS,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,SAAS,CAAC,MAAM,CAAC,OAAO,IAAI,WAAW,CAAC,CAAC,CAAC;IAC9F,IAAI,IAAI,CAAC,IAAI,KAAK,KAAK,EAAE,CAAC;QACxB,wEAAwE;QACxE,kCAAkC;QAClC,MAAM,MAAM,GAAG,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,aAAa,CAAC,IAAI,CAAC,GAAG,EAAE,SAAS,CAAC,CAAC;QAC1E,MAAM,SAAS,GAAG,iBAAiB,CAAC,IAAI,CAAC,GAAG,EAAE,SAAS,EAAE,MAAM,IAAI,YAAY,CAAC,CAAC;QACjF,IAAI,SAAS,KAAK,YAAY,EAAE,CAAC;YAC/B,OAAO,MAAM,CACX,uCAAuC,YAAY,oDAAoD;gBACrG,gCAAgC,EAClC,EAAE,WAAW,EAAE,SAAS,EAAE,YAAY,EAAE,SAAS,EAAE,CACpD,CAAC;QACJ,CAAC;QACD,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,WAAW,EAAE,SAAS,EAAE,YAAY,EAAE,MAAM,IAAI,YAAY,EAAE,SAAS,EAAE,SAAS,EAAE,OAAO,EAAE,CAAC;IACnH,CAAC;IAED,MAAM,IAAI,GAAG,UAAU,CAAC,IAAI,CAAC,UAAU,EAAE,MAAM,EAAE,WAAW,CAAC,CAAC,CAAC;IAC/D,MAAM,EAAE,MAAM,EAAE,GAAG,oBAAoB,CAAC,IAAI,CAAC,GAAG,EAAE,YAAY,EAAE,SAAS,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC;IACrF,IAAI,MAAM,KAAK,YAAY,EAAE,CAAC;QAC5B,OAAO,MAAM,CACX,4CAA4C,YAAY,oDAAoD;YAC1G,gCAAgC,EAClC,EAAE,WAAW,EAAE,SAAS,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,EAAE,CAC5D,CAAC;IACJ,CAAC;IACD,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,WAAW,EAAE,SAAS,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,EAAE,eAAe,EAAE,CAAC,IAAI,EAAE,SAAS,EAAE,OAAO,EAAE,CAAC;AAC3H,CAAC","sourcesContent":["import { existsSync, readFileSync, rmSync, writeFileSync } from 'node:fs';\nimport { join, resolve } from 'node:path';\nimport { parse as parseYaml } from 'yaml';\nimport { validateApp } from './app-package.js';\nimport { addAppToStackFile } from './app-scaffold.js';\nimport { installDeps, installPackages } from './npm.js';\nimport { parseSource } from './package-source.js';\nimport { ensureProjectManifest, readStackName } from './project.js';\nimport { resolveProjectPackageDir } from './runtime.js';\nimport { shortName, toAppName } from './spec.js';\nimport type { StackFileUpdate } from './scaffold.js';\nimport { addWorkerToStackFile } from './worker-scaffold.js';\nimport { validateWorker } from './worker-package.js';\n\nexport type PackageKind = 'app' | 'worker';\n\nexport interface AddPackageOptions {\n kind: PackageKind;\n /** npm spec or GitHub repo, see `parseSource`. */\n source: string;\n /** Project directory holding `mdk.yaml`. */\n dir: string;\n /** Instance name in `mdk.yaml` (default: derived from the package name). */\n name?: string;\n /** Run `npm install` (default: true). A GitHub source needs it to learn the package name. */\n install?: boolean;\n}\n\nexport interface AddPackageResult {\n ok: boolean;\n message?: string;\n /** Validation errors that stopped the add (package.json is restored). */\n errors?: string[];\n packageName?: string;\n /** npm ran, so package.json lists the dependency. */\n installed?: boolean;\n /** What `mdk.yaml` references: the package name, plus the subpath if any. */\n reference?: string;\n instanceName?: string;\n stackFile?: StackFileUpdate;\n /** A worker without a bundled mock needs its `devices[].opts` filled in. */\n needsDeviceOpts?: boolean;\n}\n\nconst fail = (message: string, extra: Partial<AddPackageResult> = {}): AddPackageResult => ({\n ok: false,\n message,\n ...extra,\n});\n\nfunction readDependencies(dir: string): Record<string, string> {\n try {\n const pkg = JSON.parse(readFileSync(join(dir, 'package.json'), 'utf8')) as {\n dependencies?: Record<string, string>;\n };\n return pkg.dependencies ?? {};\n } catch {\n return {};\n }\n}\n\n/** The dependency npm just added: new, or changed, or (for a re-add) pointing at the spec we asked for. */\nfunction installedName(before: Record<string, string>, after: Record<string, string>, spec: string): string | undefined {\n const changed = Object.keys(after).filter((k) => after[k] !== before[k]);\n if (changed.length === 1) return changed[0];\n return Object.keys(after).find((k) => after[k] === spec || after[k]?.includes(spec.replace(/^github:/, '')));\n}\n\n/**\n * A third-party package's install scripts would run before it is validated, and\n * a range would let the next install pick a version nobody checked.\n */\nconst ADD_INSTALL_ARGS = ['--ignore-scripts', '--save-exact'];\n\nconst MANIFESTS = ['package.json', 'package-lock.json'];\n\ntype Snapshot = Array<[string, string | null]>;\n\nfunction snapshot(dir: string): Snapshot {\n return MANIFESTS.map((name) => {\n const file = join(dir, name);\n return [file, existsSync(file) ? readFileSync(file, 'utf8') : null];\n });\n}\n\n/**\n * Puts package.json and the lockfile back as they were and resyncs node_modules,\n * so a refused package neither stays installed nor replaces the version an app\n * already listed in mdk.yaml runs.\n */\nfunction restore(dir: string, saved: Snapshot): void {\n for (const [file, content] of saved) {\n if (content === null) rmSync(file, { force: true });\n else writeFileSync(file, content, 'utf8');\n }\n installDeps(dir, true, ['--ignore-scripts']);\n}\n\n/** The name `spec.apps` already runs `reference` under, if it lists it. */\nfunction listedAppName(dir: string, reference: string): string | undefined {\n try {\n const doc = parseYaml(readFileSync(resolve(dir, 'mdk.yaml'), 'utf8')) as {\n spec?: { apps?: Array<{ name?: string; package?: string }> };\n };\n return doc.spec?.apps?.find((a) => a.package === reference)?.name;\n } catch {\n return undefined;\n }\n}\n\n/**\n * `mdk add app|worker <source>`: installs the package from npm or GitHub, checks it is a\n * valid app or worker, and registers it in `mdk.yaml`. `package.json` is updated by npm\n * itself, and restored if the package is refused. `mdk.yaml` is only touched once the\n * package validates.\n */\nexport function addPackage(opts: AddPackageOptions): AddPackageResult {\n const stackName = readStackName(opts.dir);\n if (!stackName) {\n return fail(`No mdk.yaml in ${opts.dir}. Run \\`mdk onboard\\` first, or pass --dir <project>.`);\n }\n\n let source;\n try {\n source = parseSource(opts.source);\n } catch (error) {\n return fail(error instanceof Error ? error.message : String(error));\n }\n\n const install = opts.install !== false;\n if (!install && source.kind === 'github') {\n return fail('A GitHub source has to be installed to learn its package name; drop --no-install.');\n }\n\n try {\n ensureProjectManifest(opts.dir, stackName);\n } catch (error) {\n return fail(`Could not prepare package.json: ${error instanceof Error ? error.message : String(error)}`);\n }\n\n const before = readDependencies(opts.dir);\n const saved = install ? snapshot(opts.dir) : null;\n const refuse = (message: string, extra: Partial<AddPackageResult> = {}): AddPackageResult => {\n if (saved) restore(opts.dir, saved);\n return fail(saved ? `${message} package.json was restored.` : message, extra);\n };\n\n const result = installPackages(opts.dir, [source.installSpec], install, ADD_INSTALL_ARGS);\n if (!result.ok) return refuse(result.message ?? 'npm install failed.');\n\n const packageName = source.packageName ?? installedName(before, readDependencies(opts.dir), source.installSpec);\n if (!packageName) {\n return refuse(`Installed ${source.installSpec} but could not tell which package it added.`);\n }\n const reference = source.subpath ? `${packageName}/${source.subpath}` : packageName;\n\n let packageDir: string;\n try {\n packageDir = resolveProjectPackageDir(opts.dir, reference);\n } catch (error) {\n return refuse(error instanceof Error ? error.message : String(error), { packageName });\n }\n\n const errors = opts.kind === 'app' ? validateApp(packageDir).errors : validateWorker(packageDir).errors;\n if (errors.length) {\n return refuse(`${reference} is not a valid ${opts.kind}; mdk.yaml was not changed.`, {\n errors,\n packageName,\n reference,\n });\n }\n\n const instanceName = toAppName(opts.name?.trim() || shortName(source.subpath ?? packageName));\n if (opts.kind === 'app') {\n // A re-add without --name updates the app already running this package;\n // another --name is a second app.\n const listed = opts.name ? undefined : listedAppName(opts.dir, reference);\n const stackFile = addAppToStackFile(opts.dir, reference, listed ?? instanceName);\n if (stackFile === 'name-taken') {\n return refuse(\n `spec.apps already has an app named \"${instanceName}\" with another package; mdk.yaml was not changed. ` +\n 'Pick another name with --name.',\n { packageName, reference, instanceName, stackFile },\n );\n }\n return { ok: true, packageName, reference, instanceName: listed ?? instanceName, stackFile, installed: install };\n }\n\n const mock = existsSync(join(packageDir, 'mock', 'server.js'));\n const { update } = addWorkerToStackFile(opts.dir, instanceName, reference, { mock });\n if (update === 'name-taken') {\n return refuse(\n `spec.workers already has a worker named \"${instanceName}\" with another package; mdk.yaml was not changed. ` +\n 'Pick another name with --name.',\n { packageName, reference, instanceName, stackFile: update },\n );\n }\n return { ok: true, packageName, reference, instanceName, stackFile: update, needsDeviceOpts: !mock, installed: install };\n}\n"]}
|