@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,245 @@
|
|
|
1
|
+
import { createRequire } from 'node:module';
|
|
2
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
3
|
+
import { join, resolve } from 'node:path';
|
|
4
|
+
import { detectEnvironment, NODE_MIN_MAJOR } from './detect.js';
|
|
5
|
+
import { StackSpecError, loadStackSpec, STACK_FILE } from './spec.js';
|
|
6
|
+
import { kernelKeyFile, localDiscovery, mdkDir, resolvePackageDir, trustedKeysDir } from './runtime.js';
|
|
7
|
+
import { appChildPid, isAlive, lockHolder } from './run-lock.js';
|
|
8
|
+
const require = createRequire(import.meta.url);
|
|
9
|
+
function mdkClientModule() {
|
|
10
|
+
return require('@tetherto/mdk-client');
|
|
11
|
+
}
|
|
12
|
+
const NODE_REQUIRED = `>=${NODE_MIN_MAJOR}`;
|
|
13
|
+
const GATEWAY_TIMEOUT_MS = 1500;
|
|
14
|
+
const KERNEL_TIMEOUT_MS = 3000;
|
|
15
|
+
/** True when `pkg` resolves the way `mdk run` resolves it (subpaths and local paths included). */
|
|
16
|
+
function isResolvable(projectDir, pkg) {
|
|
17
|
+
try {
|
|
18
|
+
resolvePackageDir(projectDir, pkg);
|
|
19
|
+
return true;
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Probes the Gateway over HTTP.
|
|
27
|
+
*
|
|
28
|
+
* A Gateway serves only the routes `mdk.yaml` declares and has no built-in route
|
|
29
|
+
* (no `/echo`), so no one path is there on every stack. Any answer below 500
|
|
30
|
+
* from `/`, a 404 included, means the Gateway is serving.
|
|
31
|
+
*/
|
|
32
|
+
async function probeGateway(port) {
|
|
33
|
+
const url = `http://127.0.0.1:${port}`;
|
|
34
|
+
try {
|
|
35
|
+
const res = await fetch(`${url}/`, {
|
|
36
|
+
signal: AbortSignal.timeout(GATEWAY_TIMEOUT_MS),
|
|
37
|
+
});
|
|
38
|
+
if (res.status >= 500)
|
|
39
|
+
return { state: 'down', url, error: `HTTP ${res.status}` };
|
|
40
|
+
return { state: 'up', url, error: null };
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
44
|
+
// A refused connection is the normal "not running" case, not a fault.
|
|
45
|
+
return { state: 'down', url, error: /ECONNREFUSED|fetch failed/i.test(message) ? null : message };
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Probes the Kernel over HRPC using the published key file, returning its live
|
|
50
|
+
* worker registry. The client owns its own DHT node, so it is always closed.
|
|
51
|
+
*/
|
|
52
|
+
async function probeKernel(projectDir) {
|
|
53
|
+
const keyFile = kernelKeyFile(projectDir);
|
|
54
|
+
if (!existsSync(keyFile)) {
|
|
55
|
+
return {
|
|
56
|
+
kernel: { state: 'down', keyFile, key: null, error: null },
|
|
57
|
+
status: null,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
let key;
|
|
61
|
+
try {
|
|
62
|
+
key = readFileSync(keyFile, 'utf8').trim();
|
|
63
|
+
}
|
|
64
|
+
catch (error) {
|
|
65
|
+
return {
|
|
66
|
+
kernel: {
|
|
67
|
+
state: 'down',
|
|
68
|
+
keyFile,
|
|
69
|
+
key: null,
|
|
70
|
+
error: error instanceof Error ? error.message : String(error),
|
|
71
|
+
},
|
|
72
|
+
status: null,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
let client;
|
|
76
|
+
try {
|
|
77
|
+
const discovery = localDiscovery();
|
|
78
|
+
const kernelKeyPair = discovery.callerKeyPair(join(mdkDir(projectDir), 'cli', 'identity'));
|
|
79
|
+
discovery.publishTrustedKey(trustedKeysDir(projectDir), 'cli', kernelKeyPair.publicKey);
|
|
80
|
+
client = mdkClientModule().createMdkClient({ kernelKey: key, kernelKeyPair });
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
return {
|
|
84
|
+
kernel: { state: 'down', keyFile, key, error: error instanceof Error ? error.message : String(error) },
|
|
85
|
+
status: null,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
try {
|
|
89
|
+
// A newly published key is admitted once the Kernel's trusted-keys watcher sees it.
|
|
90
|
+
await client.connect({ warmup: true });
|
|
91
|
+
const status = await client.getStatus({ retries: 1, timeoutMs: KERNEL_TIMEOUT_MS });
|
|
92
|
+
return { kernel: { state: 'up', keyFile, key, error: null }, status };
|
|
93
|
+
}
|
|
94
|
+
catch (error) {
|
|
95
|
+
return {
|
|
96
|
+
kernel: {
|
|
97
|
+
state: 'down',
|
|
98
|
+
keyFile,
|
|
99
|
+
key,
|
|
100
|
+
error: error instanceof Error ? error.message : String(error),
|
|
101
|
+
},
|
|
102
|
+
status: null,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
finally {
|
|
106
|
+
await client.close().catch(() => { });
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
/** Merges the workers declared in the spec with the Kernel's live registry. */
|
|
110
|
+
function buildWorkers(spec, status) {
|
|
111
|
+
const declared = spec ? spec.spec.workers.map((w) => w.name) : [];
|
|
112
|
+
const registered = new Map((status?.workers ?? []).map((w) => [w.workerId, w]));
|
|
113
|
+
const items = declared.map((name) => {
|
|
114
|
+
const live = registered.get(name);
|
|
115
|
+
return {
|
|
116
|
+
name,
|
|
117
|
+
declared: true,
|
|
118
|
+
registered: live != null,
|
|
119
|
+
state: live?.state ?? null,
|
|
120
|
+
health: live?.healthState ?? null,
|
|
121
|
+
devices: live?.deviceCount ?? live?.deviceIds?.length ?? 0,
|
|
122
|
+
};
|
|
123
|
+
});
|
|
124
|
+
// Workers the Kernel knows about that the spec does not — surfaced rather than
|
|
125
|
+
// hidden, since they are real processes serving traffic.
|
|
126
|
+
for (const [workerId, live] of registered) {
|
|
127
|
+
if (declared.includes(workerId))
|
|
128
|
+
continue;
|
|
129
|
+
items.push({
|
|
130
|
+
name: workerId,
|
|
131
|
+
declared: false,
|
|
132
|
+
registered: true,
|
|
133
|
+
state: live.state ?? null,
|
|
134
|
+
health: live.healthState ?? null,
|
|
135
|
+
devices: live.deviceCount ?? live.deviceIds?.length ?? 0,
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
return {
|
|
139
|
+
declared: declared.length,
|
|
140
|
+
registered: registered.size,
|
|
141
|
+
totalDevices: status?.totalDevices ?? 0,
|
|
142
|
+
items,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
function installedVersion(dir, pkg) {
|
|
146
|
+
try {
|
|
147
|
+
const manifest = JSON.parse(readFileSync(join(resolvePackageDir(dir, pkg), 'package.json'), 'utf8'));
|
|
148
|
+
return typeof manifest.version === 'string' ? manifest.version : null;
|
|
149
|
+
}
|
|
150
|
+
catch {
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
function buildApps(dir, spec) {
|
|
155
|
+
const items = (spec?.spec.apps ?? []).map((a) => {
|
|
156
|
+
const holder = lockHolder(dir, { kind: 'app', name: a.name });
|
|
157
|
+
const child = holder === null ? undefined : appChildPid(dir, a.name);
|
|
158
|
+
const pid = child === undefined ? holder : isAlive(child) ? child : null;
|
|
159
|
+
return {
|
|
160
|
+
name: a.name,
|
|
161
|
+
package: a.package,
|
|
162
|
+
version: installedVersion(dir, a.package),
|
|
163
|
+
mount: a.mount ?? `/apps/${a.name}`,
|
|
164
|
+
running: pid !== null,
|
|
165
|
+
pid,
|
|
166
|
+
};
|
|
167
|
+
});
|
|
168
|
+
return { declared: items.length, running: items.filter((a) => a.running).length, items };
|
|
169
|
+
}
|
|
170
|
+
/** A worker counts as serving when the Kernel has it READY and not DEAD/SICK. */
|
|
171
|
+
function workerServing(w) {
|
|
172
|
+
if (!w.registered)
|
|
173
|
+
return false;
|
|
174
|
+
if (w.state && w.state !== 'READY')
|
|
175
|
+
return false;
|
|
176
|
+
return !(w.health === 'DEAD' || w.health === 'SICK');
|
|
177
|
+
}
|
|
178
|
+
/** Collects the full status report for a project directory. */
|
|
179
|
+
export async function collectStatus(projectDir) {
|
|
180
|
+
const dir = resolve(projectDir);
|
|
181
|
+
const env = detectEnvironment(dir);
|
|
182
|
+
// Spec ------------------------------------------------------------------
|
|
183
|
+
const specPath = resolve(dir, STACK_FILE);
|
|
184
|
+
let spec = null;
|
|
185
|
+
let specError = null;
|
|
186
|
+
try {
|
|
187
|
+
spec = loadStackSpec(dir);
|
|
188
|
+
}
|
|
189
|
+
catch (error) {
|
|
190
|
+
if (error instanceof StackSpecError)
|
|
191
|
+
specError = error.message;
|
|
192
|
+
else
|
|
193
|
+
throw error;
|
|
194
|
+
}
|
|
195
|
+
// Declared packages must resolve the way the runtime resolves them, or
|
|
196
|
+
// `mdk run` fails at boot — a precondition worth reporting up front.
|
|
197
|
+
const packages = spec
|
|
198
|
+
? [
|
|
199
|
+
...spec.spec.workers.map((w) => w.package),
|
|
200
|
+
...spec.spec.apps.map((a) => a.package),
|
|
201
|
+
]
|
|
202
|
+
: [];
|
|
203
|
+
const missing = packages.filter((p) => !isResolvable(dir, p));
|
|
204
|
+
const environment = {
|
|
205
|
+
node: { version: env.nodeVersion, ok: env.nodeOk, required: NODE_REQUIRED },
|
|
206
|
+
packageManager: env.packageManager,
|
|
207
|
+
git: env.git,
|
|
208
|
+
spec: {
|
|
209
|
+
found: existsSync(specPath),
|
|
210
|
+
path: specPath,
|
|
211
|
+
valid: spec != null,
|
|
212
|
+
error: specError,
|
|
213
|
+
stack: spec?.metadata.name ?? null,
|
|
214
|
+
},
|
|
215
|
+
dependencies: { ok: missing.length === 0, checked: packages.length, missing },
|
|
216
|
+
};
|
|
217
|
+
// Stack -----------------------------------------------------------------
|
|
218
|
+
const [{ kernel, status }, gateway] = await Promise.all([
|
|
219
|
+
probeKernel(dir),
|
|
220
|
+
spec
|
|
221
|
+
? probeGateway(spec.spec.gateway.port)
|
|
222
|
+
: Promise.resolve({
|
|
223
|
+
state: 'down',
|
|
224
|
+
url: null,
|
|
225
|
+
error: `no ${STACK_FILE} — gateway port unknown`,
|
|
226
|
+
}),
|
|
227
|
+
]);
|
|
228
|
+
const workers = buildWorkers(spec, status);
|
|
229
|
+
const apps = buildApps(dir, spec);
|
|
230
|
+
const stack = { kernel, gateway, workers, apps };
|
|
231
|
+
// Aggregate -------------------------------------------------------------
|
|
232
|
+
const envOk = environment.node.ok && environment.spec.valid && environment.dependencies.ok;
|
|
233
|
+
let health;
|
|
234
|
+
if (kernel.state === 'down') {
|
|
235
|
+
health = 'down';
|
|
236
|
+
}
|
|
237
|
+
else if (gateway.state === 'down' || !workers.items.every(workerServing) || apps.running < apps.declared) {
|
|
238
|
+
health = 'degraded';
|
|
239
|
+
}
|
|
240
|
+
else {
|
|
241
|
+
health = 'healthy';
|
|
242
|
+
}
|
|
243
|
+
return { project: dir, environment, stack, health, ok: envOk && health === 'healthy' };
|
|
244
|
+
}
|
|
245
|
+
//# sourceMappingURL=status.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"status.js","sourceRoot":"","sources":["../../src/lib/status.ts"],"names":[],"mappings":"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","sourcesContent":["import { createRequire } from 'node:module';\nimport { existsSync, readFileSync } from 'node:fs';\nimport { join, resolve } from 'node:path';\nimport { detectEnvironment, NODE_MIN_MAJOR } from './detect.js';\nimport { StackSpecError, loadStackSpec, STACK_FILE, type StackSpec } from './spec.js';\nimport { kernelKeyFile, localDiscovery, mdkDir, resolvePackageDir, trustedKeysDir, type CallerKeyPair } from './runtime.js';\nimport { appChildPid, isAlive, lockHolder } from './run-lock.js';\n\nconst require = createRequire(import.meta.url);\n\n/**\n * Read-only state collection for `mdk status`. Nothing here starts, stops or\n * repairs anything: it inspects the environment, validates the spec, and probes\n * the running stack.\n *\n * The one write: the Kernel admits only known callers, so the Kernel probe dials\n * with a persistent CLI identity it publishes to the trusted-keys dir.\n *\n * An app runtime accepts calls from the Gateway's key only, so it cannot be\n * probed from here; an app counts as running while a live process holds its\n * run lock (see run-lock.ts).\n *\n * Liveness is deliberately probe-based, never file-based. Both `.mdk/kernel.key`\n * and `.mdk/keys/*.key` survive a shutdown by design (the keys are stable across\n * restarts), so their presence proves only that a component once ran. The Kernel\n * is therefore probed over HRPC and the Gateway over HTTP.\n */\n\n// --- @tetherto/mdk-client (typeless CommonJS, lazily required) --------------\n\ninterface RegisteredWorker {\n workerId: string;\n state?: string;\n healthState?: string;\n deviceIds?: string[];\n deviceCount?: number;\n}\ninterface KernelStatus {\n workers: RegisteredWorker[];\n totalDevices: number;\n}\ninterface MdkClient {\n connect(opts?: Record<string, unknown>): Promise<void>;\n close(): Promise<void>;\n getStatus(opts?: Record<string, unknown>): Promise<KernelStatus>;\n}\ninterface MdkClientModule {\n /** Reads the Kernel key from `config.kernelKey`; without one every call fails with ERR_MDK_CLIENT_UNAVAILABLE. */\n createMdkClient(config: { kernelKey: string; kernelKeyPair?: CallerKeyPair }): MdkClient;\n}\n\nfunction mdkClientModule(): MdkClientModule {\n return require('@tetherto/mdk-client') as MdkClientModule;\n}\n\n// --- report shape (the `-o json|yaml` contract) ----------------------------\n\nexport type Health = 'healthy' | 'degraded' | 'down';\nexport type ComponentState = 'up' | 'down';\n\nexport interface EnvironmentReport {\n node: { version: string; ok: boolean; required: string };\n packageManager: string;\n git: boolean;\n spec: {\n found: boolean;\n path: string;\n valid: boolean;\n error: string | null;\n stack: string | null;\n };\n dependencies: { ok: boolean; checked: number; missing: string[] };\n}\n\nexport interface WorkerReport {\n name: string;\n /** Listed in `spec.workers` (false = registered with the Kernel but not in the spec). */\n declared: boolean;\n /** Present in the Kernel's live worker registry. */\n registered: boolean;\n state: string | null;\n health: string | null;\n devices: number;\n}\n\nexport interface AppReport {\n name: string;\n package: string;\n /** The installed package's version, or null when it is not installed. */\n version: string | null;\n /** Where the Gateway serves it. */\n mount: string;\n running: boolean;\n /** The app's runtime process: the child `mdk run` supervises, or the `mdk run app <name>` process. */\n pid: number | null;\n}\n\nexport interface StackReport {\n kernel: { state: ComponentState; keyFile: string; key: string | null; error: string | null };\n gateway: { state: ComponentState; url: string | null; error: string | null };\n workers: {\n declared: number;\n registered: number;\n totalDevices: number;\n items: WorkerReport[];\n };\n apps: {\n declared: number;\n running: number;\n items: AppReport[];\n };\n}\n\nexport interface StatusReport {\n project: string;\n environment: EnvironmentReport;\n stack: StackReport;\n health: Health;\n ok: boolean;\n}\n\nconst NODE_REQUIRED = `>=${NODE_MIN_MAJOR}`;\nconst GATEWAY_TIMEOUT_MS = 1500;\nconst KERNEL_TIMEOUT_MS = 3000;\n\n/** True when `pkg` resolves the way `mdk run` resolves it (subpaths and local paths included). */\nfunction isResolvable(projectDir: string, pkg: string): boolean {\n try {\n resolvePackageDir(projectDir, pkg);\n return true;\n } catch {\n return false;\n }\n}\n\n/**\n * Probes the Gateway over HTTP.\n *\n * A Gateway serves only the routes `mdk.yaml` declares and has no built-in route\n * (no `/echo`), so no one path is there on every stack. Any answer below 500\n * from `/`, a 404 included, means the Gateway is serving.\n */\nasync function probeGateway(port: number): Promise<{ state: ComponentState; url: string; error: string | null }> {\n const url = `http://127.0.0.1:${port}`;\n try {\n const res = await fetch(`${url}/`, {\n signal: AbortSignal.timeout(GATEWAY_TIMEOUT_MS),\n });\n if (res.status >= 500) return { state: 'down', url, error: `HTTP ${res.status}` };\n return { state: 'up', url, error: null };\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n // A refused connection is the normal \"not running\" case, not a fault.\n return { state: 'down', url, error: /ECONNREFUSED|fetch failed/i.test(message) ? null : message };\n }\n}\n\n/**\n * Probes the Kernel over HRPC using the published key file, returning its live\n * worker registry. The client owns its own DHT node, so it is always closed.\n */\nasync function probeKernel(\n projectDir: string,\n): Promise<{ kernel: StackReport['kernel']; status: KernelStatus | null }> {\n const keyFile = kernelKeyFile(projectDir);\n if (!existsSync(keyFile)) {\n return {\n kernel: { state: 'down', keyFile, key: null, error: null },\n status: null,\n };\n }\n\n let key: string;\n try {\n key = readFileSync(keyFile, 'utf8').trim();\n } catch (error) {\n return {\n kernel: {\n state: 'down',\n keyFile,\n key: null,\n error: error instanceof Error ? error.message : String(error),\n },\n status: null,\n };\n }\n\n let client: MdkClient;\n try {\n const discovery = localDiscovery();\n const kernelKeyPair = discovery.callerKeyPair(join(mdkDir(projectDir), 'cli', 'identity'));\n discovery.publishTrustedKey(trustedKeysDir(projectDir), 'cli', kernelKeyPair.publicKey);\n client = mdkClientModule().createMdkClient({ kernelKey: key, kernelKeyPair });\n } catch (error) {\n return {\n kernel: { state: 'down', keyFile, key, error: error instanceof Error ? error.message : String(error) },\n status: null,\n };\n }\n try {\n // A newly published key is admitted once the Kernel's trusted-keys watcher sees it.\n await client.connect({ warmup: true });\n const status = await client.getStatus({ retries: 1, timeoutMs: KERNEL_TIMEOUT_MS });\n return { kernel: { state: 'up', keyFile, key, error: null }, status };\n } catch (error) {\n return {\n kernel: {\n state: 'down',\n keyFile,\n key,\n error: error instanceof Error ? error.message : String(error),\n },\n status: null,\n };\n } finally {\n await client.close().catch(() => {});\n }\n}\n\n/** Merges the workers declared in the spec with the Kernel's live registry. */\nfunction buildWorkers(spec: StackSpec | null, status: KernelStatus | null): StackReport['workers'] {\n const declared = spec ? spec.spec.workers.map((w) => w.name) : [];\n const registered = new Map((status?.workers ?? []).map((w) => [w.workerId, w]));\n\n const items: WorkerReport[] = declared.map((name) => {\n const live = registered.get(name);\n return {\n name,\n declared: true,\n registered: live != null,\n state: live?.state ?? null,\n health: live?.healthState ?? null,\n devices: live?.deviceCount ?? live?.deviceIds?.length ?? 0,\n };\n });\n\n // Workers the Kernel knows about that the spec does not — surfaced rather than\n // hidden, since they are real processes serving traffic.\n for (const [workerId, live] of registered) {\n if (declared.includes(workerId)) continue;\n items.push({\n name: workerId,\n declared: false,\n registered: true,\n state: live.state ?? null,\n health: live.healthState ?? null,\n devices: live.deviceCount ?? live.deviceIds?.length ?? 0,\n });\n }\n\n return {\n declared: declared.length,\n registered: registered.size,\n totalDevices: status?.totalDevices ?? 0,\n items,\n };\n}\n\nfunction installedVersion(dir: string, pkg: string): string | null {\n try {\n const manifest = JSON.parse(readFileSync(join(resolvePackageDir(dir, pkg), 'package.json'), 'utf8')) as {\n version?: unknown;\n };\n return typeof manifest.version === 'string' ? manifest.version : null;\n } catch {\n return null;\n }\n}\n\nfunction buildApps(dir: string, spec: StackSpec | null): StackReport['apps'] {\n const items = (spec?.spec.apps ?? []).map((a): AppReport => {\n const holder = lockHolder(dir, { kind: 'app', name: a.name });\n const child = holder === null ? undefined : appChildPid(dir, a.name);\n const pid = child === undefined ? holder : isAlive(child) ? child : null;\n return {\n name: a.name,\n package: a.package,\n version: installedVersion(dir, a.package),\n mount: a.mount ?? `/apps/${a.name}`,\n running: pid !== null,\n pid,\n };\n });\n return { declared: items.length, running: items.filter((a) => a.running).length, items };\n}\n\n/** A worker counts as serving when the Kernel has it READY and not DEAD/SICK. */\nfunction workerServing(w: WorkerReport): boolean {\n if (!w.registered) return false;\n if (w.state && w.state !== 'READY') return false;\n return !(w.health === 'DEAD' || w.health === 'SICK');\n}\n\n/** Collects the full status report for a project directory. */\nexport async function collectStatus(projectDir: string): Promise<StatusReport> {\n const dir = resolve(projectDir);\n const env = detectEnvironment(dir);\n\n // Spec ------------------------------------------------------------------\n const specPath = resolve(dir, STACK_FILE);\n let spec: StackSpec | null = null;\n let specError: string | null = null;\n try {\n spec = loadStackSpec(dir);\n } catch (error) {\n if (error instanceof StackSpecError) specError = error.message;\n else throw error;\n }\n\n // Declared packages must resolve the way the runtime resolves them, or\n // `mdk run` fails at boot — a precondition worth reporting up front.\n const packages = spec\n ? [\n ...spec.spec.workers.map((w) => w.package),\n ...spec.spec.apps.map((a) => a.package),\n ]\n : [];\n const missing = packages.filter((p) => !isResolvable(dir, p));\n\n const environment: EnvironmentReport = {\n node: { version: env.nodeVersion, ok: env.nodeOk, required: NODE_REQUIRED },\n packageManager: env.packageManager,\n git: env.git,\n spec: {\n found: existsSync(specPath),\n path: specPath,\n valid: spec != null,\n error: specError,\n stack: spec?.metadata.name ?? null,\n },\n dependencies: { ok: missing.length === 0, checked: packages.length, missing },\n };\n\n // Stack -----------------------------------------------------------------\n const [{ kernel, status }, gateway] = await Promise.all([\n probeKernel(dir),\n spec\n ? probeGateway(spec.spec.gateway.port)\n : Promise.resolve({\n state: 'down' as ComponentState,\n url: null,\n error: `no ${STACK_FILE} — gateway port unknown`,\n }),\n ]);\n\n const workers = buildWorkers(spec, status);\n const apps = buildApps(dir, spec);\n const stack: StackReport = { kernel, gateway, workers, apps };\n\n // Aggregate -------------------------------------------------------------\n const envOk = environment.node.ok && environment.spec.valid && environment.dependencies.ok;\n let health: Health;\n if (kernel.state === 'down') {\n health = 'down';\n } else if (gateway.state === 'down' || !workers.items.every(workerServing) || apps.running < apps.declared) {\n health = 'degraded';\n } else {\n health = 'healthy';\n }\n\n return { project: dir, environment, stack, health, ok: envOk && health === 'healthy' };\n}\n"]}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import pc from 'picocolors';
|
|
2
|
+
// Small palette so the CLI has a consistent, colorful look (OpenClaw-style).
|
|
3
|
+
export const theme = {
|
|
4
|
+
brand: (s) => pc.cyan(pc.bold(s)),
|
|
5
|
+
accent: (s) => pc.magenta(s),
|
|
6
|
+
label: (s) => pc.bold(pc.white(s)),
|
|
7
|
+
ok: (s) => pc.green(s),
|
|
8
|
+
warn: (s) => pc.yellow(s),
|
|
9
|
+
muted: (s) => pc.dim(s),
|
|
10
|
+
key: (s) => pc.cyan(s),
|
|
11
|
+
value: (s) => pc.white(s),
|
|
12
|
+
cmd: (s) => pc.green(s),
|
|
13
|
+
};
|
|
14
|
+
// MDK brand orange (256-color 208), matching the logo. Falls back to plain
|
|
15
|
+
// text when colors are unsupported (e.g. NO_COLOR or a non-TTY pipe).
|
|
16
|
+
const orange = (s) => pc.isColorSupported ? `\x1b[38;5;208m${s}\x1b[39m` : s;
|
|
17
|
+
// A little badge used in intros, e.g. " MDK " on a colored background.
|
|
18
|
+
export function badge(label) {
|
|
19
|
+
return pc.bgCyan(pc.black(pc.bold(` ${label} `)));
|
|
20
|
+
}
|
|
21
|
+
const MDK_ART = [
|
|
22
|
+
'███╗ ███╗██████╗ ██╗ ██╗',
|
|
23
|
+
'████╗ ████║██╔══██╗██║ ██╔╝',
|
|
24
|
+
'██╔████╔██║██║ ██║█████╔╝ ',
|
|
25
|
+
'██║╚██╔╝██║██║ ██║██╔═██╗ ',
|
|
26
|
+
'██║ ╚═╝ ██║██████╔╝██║ ██╗',
|
|
27
|
+
'╚═╝ ╚═╝╚═════╝ ╚═╝ ╚═╝',
|
|
28
|
+
];
|
|
29
|
+
/** Big orange "MDK" logo with a "tether." tagline, for the onboarding header. */
|
|
30
|
+
export function banner() {
|
|
31
|
+
const art = MDK_ART.map((line) => orange(pc.bold(line))).join('\n');
|
|
32
|
+
const tagline = `${' '.repeat(19)}${orange('tether.')}`;
|
|
33
|
+
return `\n${art}\n${tagline}\n`;
|
|
34
|
+
}
|
|
35
|
+
/** Aligned key/value block (keys padded to a common width, then colored). */
|
|
36
|
+
export function kvBlock(rows) {
|
|
37
|
+
const width = Math.max(0, ...rows.map(([k]) => k.length));
|
|
38
|
+
return rows.map(([k, v]) => `${theme.key(k.padEnd(width))} ${v}`).join('\n');
|
|
39
|
+
}
|
|
40
|
+
/** Aligned command list; a comment (rendered as `# ...`) is optional. */
|
|
41
|
+
export function cmdBlock(rows) {
|
|
42
|
+
const width = Math.max(0, ...rows.map(([c]) => c.length));
|
|
43
|
+
return rows
|
|
44
|
+
.map(([c, comment]) => comment ? `${theme.cmd(c.padEnd(width))} ${theme.muted(`# ${comment}`)}` : theme.cmd(c))
|
|
45
|
+
.join('\n');
|
|
46
|
+
}
|
|
47
|
+
export const tick = pc.green('✔');
|
|
48
|
+
export const dot = pc.dim('•');
|
|
49
|
+
export const arrow = pc.dim('→');
|
|
50
|
+
//# sourceMappingURL=theme.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"theme.js","sourceRoot":"","sources":["../../src/lib/theme.ts"],"names":[],"mappings":"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","sourcesContent":["import pc from 'picocolors';\n\n// Small palette so the CLI has a consistent, colorful look (OpenClaw-style).\nexport const theme = {\n brand: (s: string) => pc.cyan(pc.bold(s)),\n accent: (s: string) => pc.magenta(s),\n label: (s: string) => pc.bold(pc.white(s)),\n ok: (s: string) => pc.green(s),\n warn: (s: string) => pc.yellow(s),\n muted: (s: string) => pc.dim(s),\n key: (s: string) => pc.cyan(s),\n value: (s: string) => pc.white(s),\n cmd: (s: string) => pc.green(s),\n};\n\n// MDK brand orange (256-color 208), matching the logo. Falls back to plain\n// text when colors are unsupported (e.g. NO_COLOR or a non-TTY pipe).\nconst orange = (s: string): string =>\n pc.isColorSupported ? `\\x1b[38;5;208m${s}\\x1b[39m` : s;\n\n// A little badge used in intros, e.g. \" MDK \" on a colored background.\nexport function badge(label: string): string {\n return pc.bgCyan(pc.black(pc.bold(` ${label} `)));\n}\n\nconst MDK_ART = [\n '███╗ ███╗██████╗ ██╗ ██╗',\n '████╗ ████║██╔══██╗██║ ██╔╝',\n '██╔████╔██║██║ ██║█████╔╝ ',\n '██║╚██╔╝██║██║ ██║██╔═██╗ ',\n '██║ ╚═╝ ██║██████╔╝██║ ██╗',\n '╚═╝ ╚═╝╚═════╝ ╚═╝ ╚═╝',\n];\n\n/** Big orange \"MDK\" logo with a \"tether.\" tagline, for the onboarding header. */\nexport function banner(): string {\n const art = MDK_ART.map((line) => orange(pc.bold(line))).join('\\n');\n const tagline = `${' '.repeat(19)}${orange('tether.')}`;\n return `\\n${art}\\n${tagline}\\n`;\n}\n\n/** Aligned key/value block (keys padded to a common width, then colored). */\nexport function kvBlock(rows: Array<[string, string]>): string {\n const width = Math.max(0, ...rows.map(([k]) => k.length));\n return rows.map(([k, v]) => `${theme.key(k.padEnd(width))} ${v}`).join('\\n');\n}\n\n/** Aligned command list; a comment (rendered as `# ...`) is optional. */\nexport function cmdBlock(rows: Array<[string, string?]>): string {\n const width = Math.max(0, ...rows.map(([c]) => c.length));\n return rows\n .map(([c, comment]) =>\n comment ? `${theme.cmd(c.padEnd(width))} ${theme.muted(`# ${comment}`)}` : theme.cmd(c),\n )\n .join('\\n');\n}\n\nexport const tick = pc.green('✔');\nexport const dot = pc.dim('•');\nexport const arrow = pc.dim('→');\n"]}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { existsSync, readFileSync, statSync } from 'node:fs';
|
|
2
|
+
import { createRequire } from 'node:module';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { isMapping, message, readFile, syntaxError } from './app-package.js';
|
|
5
|
+
const require = createRequire(import.meta.url);
|
|
6
|
+
export const WORKER_CONTRACT = 'mdk-contract.json';
|
|
7
|
+
/**
|
|
8
|
+
* Checks a worker package before it is run or published: a readable package.json with a
|
|
9
|
+
* name and version, and a contract-first Worker Plugin — `mdk-contract.json` at the root,
|
|
10
|
+
* validated against the runtime's mdk-contract.schema.json plus its semantic rules
|
|
11
|
+
* (unique names, number-param bounds, handler files on disk) and by the Worker runtime's
|
|
12
|
+
* own loader, with at least one entry, command params as lists, and every handler
|
|
13
|
+
* compiling. None of them is executed.
|
|
14
|
+
*/
|
|
15
|
+
export function validateWorker(dir) {
|
|
16
|
+
const errors = [];
|
|
17
|
+
const result = { errors, telemetry: 0, commands: 0 };
|
|
18
|
+
if (!existsSync(dir) || !statSync(dir).isDirectory()) {
|
|
19
|
+
errors.push(`${dir}: not a directory`);
|
|
20
|
+
return result;
|
|
21
|
+
}
|
|
22
|
+
const pkg = readFile(dir, 'package.json', JSON.parse, errors);
|
|
23
|
+
if (isMapping(pkg)) {
|
|
24
|
+
for (const key of ['name', 'version']) {
|
|
25
|
+
if (typeof pkg[key] === 'string' && pkg[key])
|
|
26
|
+
result[key] = pkg[key];
|
|
27
|
+
else
|
|
28
|
+
errors.push(`package.json: "${key}" must be a non-empty string`);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
if (!existsSync(join(dir, WORKER_CONTRACT))) {
|
|
32
|
+
errors.push(`${WORKER_CONTRACT}: not found (a Worker Plugin has its contract at the package root)`);
|
|
33
|
+
return result;
|
|
34
|
+
}
|
|
35
|
+
const loader = require('@tetherto/mdk-worker');
|
|
36
|
+
// Schema + semantic conformance first: it reports every problem at once
|
|
37
|
+
// (ERR_CONTRACT_*), where the loader below stops at the first. Unparseable
|
|
38
|
+
// JSON is left to the loader so it keeps its ERR_CONTRACT_INVALID_JSON shape.
|
|
39
|
+
let contractJson;
|
|
40
|
+
try {
|
|
41
|
+
contractJson = JSON.parse(readFileSync(join(dir, WORKER_CONTRACT), 'utf8'));
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
contractJson = undefined;
|
|
45
|
+
}
|
|
46
|
+
if (contractJson !== undefined) {
|
|
47
|
+
errors.push(...loader.validateContract(contractJson, { dir }).errors.map((e) => `${WORKER_CONTRACT}: ${e}`));
|
|
48
|
+
}
|
|
49
|
+
try {
|
|
50
|
+
const { contract, entries } = loader.loadContract(dir);
|
|
51
|
+
result.telemetry = entries.telemetry.size;
|
|
52
|
+
result.commands = entries.commands.size;
|
|
53
|
+
if (result.telemetry + result.commands === 0) {
|
|
54
|
+
errors.push(`${WORKER_CONTRACT}: declares no telemetry and no commands`);
|
|
55
|
+
}
|
|
56
|
+
for (const command of contract.capabilities?.commands ?? []) {
|
|
57
|
+
if (command.params !== undefined && !Array.isArray(command.params)) {
|
|
58
|
+
errors.push(`${WORKER_CONTRACT}: commands.${command.name}.params must be a list`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
for (const section of ['telemetry', 'commands']) {
|
|
62
|
+
for (const [name, file] of entries[section]) {
|
|
63
|
+
const syntax = syntaxError(file);
|
|
64
|
+
if (syntax)
|
|
65
|
+
errors.push(`${WORKER_CONTRACT}: ${section}.${name} handler does not compile: ${syntax}`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
catch (error) {
|
|
70
|
+
errors.push(`${WORKER_CONTRACT}: ${message(error)}`);
|
|
71
|
+
}
|
|
72
|
+
return result;
|
|
73
|
+
}
|
|
74
|
+
//# sourceMappingURL=worker-package.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"worker-package.js","sourceRoot":"","sources":["../../src/lib/worker-package.ts"],"names":[],"mappings":"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","sourcesContent":["import { existsSync, readFileSync, statSync } from 'node:fs';\nimport { createRequire } from 'node:module';\nimport { join } from 'node:path';\nimport { isMapping, message, readFile, syntaxError } from './app-package.js';\n\nconst require = createRequire(import.meta.url);\n\nexport const WORKER_CONTRACT = 'mdk-contract.json';\n\nexport interface WorkerValidation {\n errors: string[];\n /** The package.json `name` / `version`, when readable. */\n name?: string;\n version?: string;\n telemetry: number;\n commands: number;\n}\n\ninterface MdkWorkerLoader {\n loadContract(dir: string): {\n contract: { capabilities?: { commands?: Array<{ name?: string; params?: unknown }> } };\n entries: { telemetry: Map<string, string>; commands: Map<string, string> };\n };\n validateContract(contract: unknown, opts?: { dir?: string; semantic?: boolean }): { ok: boolean; errors: string[] };\n}\n\n/**\n * Checks a worker package before it is run or published: a readable package.json with a\n * name and version, and a contract-first Worker Plugin — `mdk-contract.json` at the root,\n * validated against the runtime's mdk-contract.schema.json plus its semantic rules\n * (unique names, number-param bounds, handler files on disk) and by the Worker runtime's\n * own loader, with at least one entry, command params as lists, and every handler\n * compiling. None of them is executed.\n */\nexport function validateWorker(dir: string): WorkerValidation {\n const errors: string[] = [];\n const result: WorkerValidation = { errors, telemetry: 0, commands: 0 };\n if (!existsSync(dir) || !statSync(dir).isDirectory()) {\n errors.push(`${dir}: not a directory`);\n return result;\n }\n\n const pkg = readFile(dir, 'package.json', JSON.parse, errors);\n if (isMapping(pkg)) {\n for (const key of ['name', 'version'] as const) {\n if (typeof pkg[key] === 'string' && pkg[key]) result[key] = pkg[key];\n else errors.push(`package.json: \"${key}\" must be a non-empty string`);\n }\n }\n\n if (!existsSync(join(dir, WORKER_CONTRACT))) {\n errors.push(`${WORKER_CONTRACT}: not found (a Worker Plugin has its contract at the package root)`);\n return result;\n }\n const loader = require('@tetherto/mdk-worker') as MdkWorkerLoader;\n\n // Schema + semantic conformance first: it reports every problem at once\n // (ERR_CONTRACT_*), where the loader below stops at the first. Unparseable\n // JSON is left to the loader so it keeps its ERR_CONTRACT_INVALID_JSON shape.\n let contractJson: unknown;\n try {\n contractJson = JSON.parse(readFileSync(join(dir, WORKER_CONTRACT), 'utf8'));\n } catch {\n contractJson = undefined;\n }\n if (contractJson !== undefined) {\n errors.push(...loader.validateContract(contractJson, { dir }).errors.map((e) => `${WORKER_CONTRACT}: ${e}`));\n }\n\n try {\n const { contract, entries } = loader.loadContract(dir);\n result.telemetry = entries.telemetry.size;\n result.commands = entries.commands.size;\n if (result.telemetry + result.commands === 0) {\n errors.push(`${WORKER_CONTRACT}: declares no telemetry and no commands`);\n }\n for (const command of contract.capabilities?.commands ?? []) {\n if (command.params !== undefined && !Array.isArray(command.params)) {\n errors.push(`${WORKER_CONTRACT}: commands.${command.name}.params must be a list`);\n }\n }\n for (const section of ['telemetry', 'commands'] as const) {\n for (const [name, file] of entries[section]) {\n const syntax = syntaxError(file);\n if (syntax) errors.push(`${WORKER_CONTRACT}: ${section}.${name} handler does not compile: ${syntax}`);\n }\n }\n } catch (error) {\n errors.push(`${WORKER_CONTRACT}: ${message(error)}`);\n }\n return result;\n}\n"]}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { existsSync, mkdirSync } from 'node:fs';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
3
|
+
import { dirname, join, resolve } from 'node:path';
|
|
4
|
+
import { installScaffold } from './npm.js';
|
|
5
|
+
import { DIRS } from './project.js';
|
|
6
|
+
import { appendToStackList, copyTemplate, linkIntoProject, updateJson } from './scaffold.js';
|
|
7
|
+
import { deviceId, deviceSerial, MOCK_PORT_BASE } from './spec.js';
|
|
8
|
+
import { NAME_RE, parseOrg } from './names.js';
|
|
9
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
10
|
+
/** Placeholder baked into `templates/worker/**`, replaced at scaffold time. */
|
|
11
|
+
const NAME_TOKEN = '__WORKER_NAME__';
|
|
12
|
+
/**
|
|
13
|
+
* Absolute path to the bundled worker template (`templates/worker`). Resolves the same from
|
|
14
|
+
* `dist/lib` (built) and `src/lib` (tsx dev) since both sit one level under the
|
|
15
|
+
* package root.
|
|
16
|
+
*/
|
|
17
|
+
function templateDir() {
|
|
18
|
+
return resolve(__dirname, '..', '..', 'templates', 'worker');
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Lowest mock port from `MOCK_PORT_BASE` up that no device in this project's
|
|
22
|
+
* spec already claims, so scaffolding a second worker cannot collide with the
|
|
23
|
+
* first. Only covers *this* project — a port taken by another stack on the
|
|
24
|
+
* machine is resolved at boot (see `resolveMockPorts` in runtime.ts).
|
|
25
|
+
*/
|
|
26
|
+
function nextMockPort(doc) {
|
|
27
|
+
const spec = doc.toJS();
|
|
28
|
+
const used = new Set();
|
|
29
|
+
for (const worker of spec?.spec?.workers ?? []) {
|
|
30
|
+
for (const device of worker?.config?.devices ?? []) {
|
|
31
|
+
const port = device?.opts?.port;
|
|
32
|
+
if (typeof port === 'number')
|
|
33
|
+
used.add(port);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
let port = MOCK_PORT_BASE;
|
|
37
|
+
while (used.has(port))
|
|
38
|
+
port++;
|
|
39
|
+
return port;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Appends a worker to `spec.workers` in `<parentDir>/mdk.yaml`. With `mock` (the
|
|
43
|
+
* default) it seeds a `mock: true` config with one sample device on an unclaimed port
|
|
44
|
+
* so the entry is runnable immediately; without, the device options are the
|
|
45
|
+
* operator's to fill in. No-ops (`'exists'`) when this worker is listed already;
|
|
46
|
+
* `'name-taken'` when the name belongs to a worker running another package.
|
|
47
|
+
*/
|
|
48
|
+
export function addWorkerToStackFile(parentDir, name, relPackage, { mock = true } = {}) {
|
|
49
|
+
let port = MOCK_PORT_BASE;
|
|
50
|
+
const update = appendToStackList(parentDir, 'workers', name, (entry) => entry.get('name') === name && entry.get('package') === relPackage, (doc) => {
|
|
51
|
+
port = nextMockPort(doc);
|
|
52
|
+
// Flow-style opts to match the compact form the scaffold documents. The
|
|
53
|
+
// device identity is derived from the (project-unique) worker name: the
|
|
54
|
+
// Kernel registers device ids globally, so a shared `dev-0` would leave the
|
|
55
|
+
// second worker silently unregistered.
|
|
56
|
+
const opts = doc.createNode(mock ? { host: '127.0.0.1', port, serial: deviceSerial(name) } : {});
|
|
57
|
+
opts.flow = true;
|
|
58
|
+
const worker = doc.createNode({
|
|
59
|
+
name,
|
|
60
|
+
package: relPackage,
|
|
61
|
+
config: { ...(mock ? { mock: true } : {}), devices: [{ id: deviceId(name) }] },
|
|
62
|
+
});
|
|
63
|
+
worker.setIn(['config', 'devices', 0, 'opts'], opts);
|
|
64
|
+
return worker;
|
|
65
|
+
});
|
|
66
|
+
return { update, port };
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Scaffolds a new Worker Plugin package from the bundled worker template
|
|
70
|
+
* into `<parentDir>/workers/<name>` — the local-path layout `mdk run worker`
|
|
71
|
+
* expects. Substitutes the name token and sets the package.json `name`.
|
|
72
|
+
*/
|
|
73
|
+
export function createWorker(opts) {
|
|
74
|
+
const name = opts.name.trim();
|
|
75
|
+
if (!NAME_RE.test(name)) {
|
|
76
|
+
return {
|
|
77
|
+
ok: false,
|
|
78
|
+
message: `Invalid worker name "${opts.name}". Use lowercase letters, digits, '.', '_' or '-'.`,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
const org = parseOrg(opts.org);
|
|
82
|
+
if (!org.ok)
|
|
83
|
+
return { ok: false, message: org.message };
|
|
84
|
+
const src = templateDir();
|
|
85
|
+
if (!existsSync(src)) {
|
|
86
|
+
return { ok: false, message: `Worker template not found at ${src}.` };
|
|
87
|
+
}
|
|
88
|
+
const workerPath = resolve(opts.parentDir, DIRS.workers, name);
|
|
89
|
+
if (existsSync(workerPath) && !opts.force) {
|
|
90
|
+
return { ok: false, message: `${workerPath} already exists (use --force to overwrite).` };
|
|
91
|
+
}
|
|
92
|
+
try {
|
|
93
|
+
mkdirSync(resolve(opts.parentDir, DIRS.workers), { recursive: true });
|
|
94
|
+
copyTemplate(src, workerPath, { [NAME_TOKEN]: name });
|
|
95
|
+
const packageName = org.org ? `@${org.org}/${name}` : name;
|
|
96
|
+
updateJson(join(workerPath, 'package.json'), { name: packageName });
|
|
97
|
+
const linkWarning = linkIntoProject(opts.parentDir, packageName, workerPath);
|
|
98
|
+
const install = installScaffold(opts.parentDir, workerPath, opts.install !== false);
|
|
99
|
+
const relPackage = `./${DIRS.workers}/${name}`;
|
|
100
|
+
const stackFile = opts.updateStackFile === false
|
|
101
|
+
? undefined
|
|
102
|
+
: addWorkerToStackFile(opts.parentDir, name, relPackage);
|
|
103
|
+
return {
|
|
104
|
+
ok: true,
|
|
105
|
+
workerPath,
|
|
106
|
+
relPackage,
|
|
107
|
+
packageName,
|
|
108
|
+
installWarning: linkWarning ?? (install.ok ? undefined : install.message),
|
|
109
|
+
installDir: install.dir,
|
|
110
|
+
stackFile: stackFile?.update,
|
|
111
|
+
mockPort: stackFile?.port ?? MOCK_PORT_BASE,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
catch (error) {
|
|
115
|
+
return {
|
|
116
|
+
ok: false,
|
|
117
|
+
message: 'Could not scaffold the worker.',
|
|
118
|
+
detail: error instanceof Error ? error.message : String(error),
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
//# sourceMappingURL=worker-scaffold.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"worker-scaffold.js","sourceRoot":"","sources":["../../src/lib/worker-scaffold.ts"],"names":[],"mappings":"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","sourcesContent":["import { existsSync, mkdirSync } from 'node:fs';\nimport { fileURLToPath } from 'node:url';\nimport { dirname, join, resolve } from 'node:path';\nimport type { Document, YAMLMap } from 'yaml';\nimport { installScaffold } from './npm.js';\nimport { DIRS } from './project.js';\nimport { appendToStackList, copyTemplate, linkIntoProject, updateJson, type StackFileUpdate } from './scaffold.js';\nimport { deviceId, deviceSerial, MOCK_PORT_BASE } from './spec.js';\nimport { NAME_RE, parseOrg } from './names.js';\n\nconst __dirname = dirname(fileURLToPath(import.meta.url));\n\n/** Placeholder baked into `templates/worker/**`, replaced at scaffold time. */\nconst NAME_TOKEN = '__WORKER_NAME__';\n\n/**\n * Absolute path to the bundled worker template (`templates/worker`). Resolves the same from\n * `dist/lib` (built) and `src/lib` (tsx dev) since both sit one level under the\n * package root.\n */\nfunction templateDir(): string {\n return resolve(__dirname, '..', '..', 'templates', 'worker');\n}\n\nexport interface WorkerScaffoldOptions {\n /** Worker name — used for the folder, the mdk.yaml entry, and the package name. */\n name: string;\n /** Project directory; the worker is created under `<parentDir>/workers/<name>`. */\n parentDir: string;\n /** npm scope/org for the generated package name (e.g. `demo` → `@demo/<name>`). */\n org?: string;\n /** Overwrite the target if it already exists (default: false). */\n force?: boolean;\n /** Run `npm install` in the scaffolded worker (default: true). */\n install?: boolean;\n /** Add the worker entry to `<parentDir>/mdk.yaml` under `spec.workers` (default: true). */\n updateStackFile?: boolean;\n}\n\nexport interface WorkerScaffoldResult {\n ok: boolean;\n message?: string;\n detail?: string;\n /** Absolute path of the scaffolded worker. */\n workerPath?: string;\n /** Local path to reference in mdk.yaml (`./workers/<name>`). */\n relPackage?: string;\n /** Resolved package.json `name`. */\n packageName?: string;\n /** Warning surfaced when `npm install` could not complete (scaffold still ok). */\n installWarning?: string;\n /** Directory `npm install` ran in (the workspace root for a linked worker). */\n installDir?: string;\n /** How the `mdk.yaml` update went (so the caller can guide the user). */\n stackFile?: StackFileUpdate;\n /** Mock port the seed device was given — so a printed snippet matches the file. */\n mockPort?: number;\n}\n\n/**\n * Lowest mock port from `MOCK_PORT_BASE` up that no device in this project's\n * spec already claims, so scaffolding a second worker cannot collide with the\n * first. Only covers *this* project — a port taken by another stack on the\n * machine is resolved at boot (see `resolveMockPorts` in runtime.ts).\n */\nfunction nextMockPort(doc: Document): number {\n const spec = doc.toJS() as {\n spec?: { workers?: Array<{ config?: { devices?: Array<{ opts?: { port?: unknown } }> } }> };\n };\n\n const used = new Set<number>();\n for (const worker of spec?.spec?.workers ?? []) {\n for (const device of worker?.config?.devices ?? []) {\n const port = device?.opts?.port;\n if (typeof port === 'number') used.add(port);\n }\n }\n\n let port = MOCK_PORT_BASE;\n while (used.has(port)) port++;\n return port;\n}\n\n/** Outcome of the `mdk.yaml` edit, plus the mock port the entry was given. */\nexport interface StackFileResult {\n update: StackFileUpdate;\n port: number;\n}\n\n/**\n * Appends a worker to `spec.workers` in `<parentDir>/mdk.yaml`. With `mock` (the\n * default) it seeds a `mock: true` config with one sample device on an unclaimed port\n * so the entry is runnable immediately; without, the device options are the\n * operator's to fill in. No-ops (`'exists'`) when this worker is listed already;\n * `'name-taken'` when the name belongs to a worker running another package.\n */\nexport function addWorkerToStackFile(\n parentDir: string,\n name: string,\n relPackage: string,\n { mock = true }: { mock?: boolean } = {},\n): StackFileResult {\n let port = MOCK_PORT_BASE;\n const update = appendToStackList(\n parentDir,\n 'workers',\n name,\n (entry) => entry.get('name') === name && entry.get('package') === relPackage,\n (doc) => {\n port = nextMockPort(doc);\n // Flow-style opts to match the compact form the scaffold documents. The\n // device identity is derived from the (project-unique) worker name: the\n // Kernel registers device ids globally, so a shared `dev-0` would leave the\n // second worker silently unregistered.\n const opts = doc.createNode(mock ? { host: '127.0.0.1', port, serial: deviceSerial(name) } : {});\n (opts as unknown as { flow: boolean }).flow = true;\n const worker = doc.createNode({\n name,\n package: relPackage,\n config: { ...(mock ? { mock: true } : {}), devices: [{ id: deviceId(name) }] },\n }) as YAMLMap;\n worker.setIn(['config', 'devices', 0, 'opts'], opts);\n return worker;\n },\n );\n return { update, port };\n}\n\n/**\n * Scaffolds a new Worker Plugin package from the bundled worker template\n * into `<parentDir>/workers/<name>` — the local-path layout `mdk run worker`\n * expects. Substitutes the name token and sets the package.json `name`.\n */\nexport function createWorker(opts: WorkerScaffoldOptions): WorkerScaffoldResult {\n const name = opts.name.trim();\n if (!NAME_RE.test(name)) {\n return {\n ok: false,\n message: `Invalid worker name \"${opts.name}\". Use lowercase letters, digits, '.', '_' or '-'.`,\n };\n }\n\n const org = parseOrg(opts.org);\n if (!org.ok) return { ok: false, message: org.message };\n\n const src = templateDir();\n if (!existsSync(src)) {\n return { ok: false, message: `Worker template not found at ${src}.` };\n }\n\n const workerPath = resolve(opts.parentDir, DIRS.workers, name);\n if (existsSync(workerPath) && !opts.force) {\n return { ok: false, message: `${workerPath} already exists (use --force to overwrite).` };\n }\n\n try {\n mkdirSync(resolve(opts.parentDir, DIRS.workers), { recursive: true });\n copyTemplate(src, workerPath, { [NAME_TOKEN]: name });\n\n const packageName = org.org ? `@${org.org}/${name}` : name;\n updateJson(join(workerPath, 'package.json'), { name: packageName });\n\n const linkWarning = linkIntoProject(opts.parentDir, packageName, workerPath);\n const install = installScaffold(opts.parentDir, workerPath, opts.install !== false);\n\n const relPackage = `./${DIRS.workers}/${name}`;\n const stackFile =\n opts.updateStackFile === false\n ? undefined\n : addWorkerToStackFile(opts.parentDir, name, relPackage);\n\n return {\n ok: true,\n workerPath,\n relPackage,\n packageName,\n installWarning: linkWarning ?? (install.ok ? undefined : install.message),\n installDir: install.dir,\n stackFile: stackFile?.update,\n mockPort: stackFile?.port ?? MOCK_PORT_BASE,\n };\n } catch (error) {\n return {\n ok: false,\n message: 'Could not scaffold the worker.',\n detail: error instanceof Error ? error.message : String(error),\n };\n }\n}\n"]}
|