@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
package/dist/lib/spec.js
ADDED
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { isAbsolute, resolve } from 'node:path';
|
|
3
|
+
import { parse as parseYaml, stringify as stringifyYaml } from 'yaml';
|
|
4
|
+
import { findCatalogApp, findCatalogWorker } from './catalog.js';
|
|
5
|
+
import { DEFAULT_HOST } from './ports.js';
|
|
6
|
+
import { readProjectEnv } from './env-file.js';
|
|
7
|
+
import { NAME_RE } from './names.js';
|
|
8
|
+
const APP_NAME_PATTERN = /^[a-z][a-z0-9-]*$/;
|
|
9
|
+
/**
|
|
10
|
+
* The Gateway's own rule for `mount` (startGateway in @tetherto/mdk-core): a
|
|
11
|
+
* leading "/" and literal segments (no `:param`, `*`, `.` or `..`). Checked
|
|
12
|
+
* here so a bad mount fails before anything boots, not in the Gateway last.
|
|
13
|
+
*/
|
|
14
|
+
function isValidMount(mount) {
|
|
15
|
+
return mount.startsWith('/') && mount.split('/').filter(Boolean).every((s) => /^(?!\.\.?$)[A-Za-z0-9._~-]+$/.test(s));
|
|
16
|
+
}
|
|
17
|
+
/** Coerces a package or folder name into a valid app name. */
|
|
18
|
+
export function toAppName(raw) {
|
|
19
|
+
const slug = raw.toLowerCase().replace(/[^a-z0-9-]+/g, '-').replace(/^-+|-+$/g, '');
|
|
20
|
+
return /^[a-z]/.test(slug) ? slug : `app-${slug}`;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Whether a spec's `package` names a directory on disk rather than an npm
|
|
24
|
+
* package — relative or absolute either way. Scaffolded components are written
|
|
25
|
+
* as paths, so they are resolved from disk and never handed to `npm install`.
|
|
26
|
+
*/
|
|
27
|
+
export function isLocalPackagePath(pkg) {
|
|
28
|
+
return pkg.startsWith('.') || isAbsolute(pkg);
|
|
29
|
+
}
|
|
30
|
+
const ENV_REF_RE = /\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g;
|
|
31
|
+
/**
|
|
32
|
+
* Resolves one string from `mdk.yaml` (`spec.workers[].env`, and every string in
|
|
33
|
+
* `spec.gateway.config` / `spec.apps[].config`): `${NAME}` is
|
|
34
|
+
* replaced with `process.env.NAME`, else the project's `.env`, falling back to
|
|
35
|
+
* the literal text when the variable is unset — enough to keep secrets out of
|
|
36
|
+
* mdk.yaml without a templating engine.
|
|
37
|
+
*/
|
|
38
|
+
function resolveEnvValue(value, vars) {
|
|
39
|
+
return value.replace(ENV_REF_RE, (match, name) => vars[name] ?? match);
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* The first `${NAME}` that `resolveEnvValue` left in `value` because NAME was
|
|
43
|
+
* unset, or undefined. For a secret, where keeping the literal text would turn
|
|
44
|
+
* a missing variable into a guessable value.
|
|
45
|
+
*/
|
|
46
|
+
export function unresolvedEnvRef(value) {
|
|
47
|
+
return value.match(new RegExp(ENV_REF_RE.source))?.[1];
|
|
48
|
+
}
|
|
49
|
+
/** `resolveEnvValue` applied to every string inside a config block. */
|
|
50
|
+
function resolveEnvDeep(value, vars) {
|
|
51
|
+
if (typeof value === 'string')
|
|
52
|
+
return resolveEnvValue(value, vars);
|
|
53
|
+
if (Array.isArray(value))
|
|
54
|
+
return value.map((item) => resolveEnvDeep(item, vars));
|
|
55
|
+
if (value !== null && typeof value === 'object') {
|
|
56
|
+
return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, resolveEnvDeep(item, vars)]));
|
|
57
|
+
}
|
|
58
|
+
return value;
|
|
59
|
+
}
|
|
60
|
+
export function shortName(pkg) {
|
|
61
|
+
const base = pkg.split('/').pop() ?? pkg;
|
|
62
|
+
return base.replace(/^mdk-(worker-|plugin-|app-)?/, '') || base;
|
|
63
|
+
}
|
|
64
|
+
/** First port a seeded mock device server is offered. */
|
|
65
|
+
export const MOCK_PORT_BASE = 18080;
|
|
66
|
+
/** Stack-unique id for a worker's seed device (worker names are unique). */
|
|
67
|
+
export function deviceId(workerName) {
|
|
68
|
+
return `${workerName}-0`;
|
|
69
|
+
}
|
|
70
|
+
/** Matching serial for the seed device, in the shape device firmware reports. */
|
|
71
|
+
export function deviceSerial(workerName) {
|
|
72
|
+
return deviceId(workerName).toUpperCase();
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Config block for one selected worker.
|
|
76
|
+
*
|
|
77
|
+
* Worker config is plugin-defined and otherwise opaque to the CLI, so this can
|
|
78
|
+
* only be filled in for catalog entries whose device shape we know. Those get a
|
|
79
|
+
* seed device and `mock: true`, which is the whole difference between a spec
|
|
80
|
+
* that boots and one the user has to finish by hand.
|
|
81
|
+
*/
|
|
82
|
+
function workerConfig(entry, name, mockPort) {
|
|
83
|
+
// Nothing known about this package's devices — leave an editable starter value
|
|
84
|
+
// and let `mdk run` name what is still missing.
|
|
85
|
+
if (!entry?.deviceOpts && !entry?.mock)
|
|
86
|
+
return { pollIntervalMs: 2000 };
|
|
87
|
+
return {
|
|
88
|
+
...(entry.config ?? {}),
|
|
89
|
+
...(entry.mock ? { mock: true } : {}),
|
|
90
|
+
devices: [
|
|
91
|
+
{
|
|
92
|
+
id: deviceId(name),
|
|
93
|
+
opts: {
|
|
94
|
+
host: DEFAULT_HOST,
|
|
95
|
+
port: mockPort,
|
|
96
|
+
serial: deviceSerial(name),
|
|
97
|
+
...entry.deviceOpts,
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
],
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
function pluginConfig(pkg) {
|
|
104
|
+
if (pkg.includes('summary'))
|
|
105
|
+
return { refreshIntervalMs: 5000 };
|
|
106
|
+
if (pkg.includes('alerts'))
|
|
107
|
+
return { evaluateIntervalMs: 10000 };
|
|
108
|
+
return {};
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Builds the declarative stack spec (`mdk.yaml`) purely from the onboarding
|
|
112
|
+
* answers. One worker instance is created per selected worker plugin, and each
|
|
113
|
+
* selected app is added under `spec.apps`.
|
|
114
|
+
*/
|
|
115
|
+
export function buildStackSpec(answers) {
|
|
116
|
+
const workers = answers.workerPackages.map((pkg, i) => {
|
|
117
|
+
const entry = findCatalogWorker(pkg);
|
|
118
|
+
const name = `${shortName(pkg)}-a`;
|
|
119
|
+
return {
|
|
120
|
+
name,
|
|
121
|
+
// Always the package name: a bundled worker is `file:`-linked into the
|
|
122
|
+
// project's node_modules under this name (see addFileDependency), so the
|
|
123
|
+
// spec stays portable and does not hard-code anyone's checkout path.
|
|
124
|
+
package: pkg,
|
|
125
|
+
port: answers.workerBasePort + i * 2,
|
|
126
|
+
// One mock port per worker, so selecting several cannot collide.
|
|
127
|
+
config: workerConfig(entry, name, MOCK_PORT_BASE + i),
|
|
128
|
+
};
|
|
129
|
+
});
|
|
130
|
+
return {
|
|
131
|
+
apiVersion: 'mdk/v1',
|
|
132
|
+
kind: 'Stack',
|
|
133
|
+
metadata: { name: answers.stackName },
|
|
134
|
+
spec: {
|
|
135
|
+
kernel: { port: answers.kernelPort },
|
|
136
|
+
gateway: {
|
|
137
|
+
port: answers.gatewayPort,
|
|
138
|
+
config: answers.gatewayConfig ?? {},
|
|
139
|
+
},
|
|
140
|
+
// Onboarding never asks about MCP settings; `loadStackSpec` defaults
|
|
141
|
+
// this the same way when the section is absent from mdk.yaml entirely.
|
|
142
|
+
mcp: {},
|
|
143
|
+
workers,
|
|
144
|
+
apps: answers.appPackages.map((pkg) => {
|
|
145
|
+
const mount = findCatalogApp(pkg)?.mount;
|
|
146
|
+
return {
|
|
147
|
+
name: toAppName(shortName(pkg)),
|
|
148
|
+
package: pkg,
|
|
149
|
+
config: answers.pluginConfigs?.[pkg] ?? pluginConfig(pkg),
|
|
150
|
+
...(mount !== undefined ? { mount } : {}),
|
|
151
|
+
};
|
|
152
|
+
}),
|
|
153
|
+
},
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
/** File name of the declarative stack spec, relative to a project directory. */
|
|
157
|
+
export const STACK_FILE = 'mdk.yaml';
|
|
158
|
+
class StackSpecError extends Error {
|
|
159
|
+
}
|
|
160
|
+
function fail(message) {
|
|
161
|
+
throw new StackSpecError(message);
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Rejects worker names and device ids that repeat across the stack.
|
|
165
|
+
*
|
|
166
|
+
* The Kernel registers both globally, so a duplicate does not error — the second
|
|
167
|
+
* registration is simply dropped, and the stack comes up looking fine while a
|
|
168
|
+
* worker is missing from the fleet. Catching it here turns a silent, confusing
|
|
169
|
+
* runtime failure into a spec error that names both offenders.
|
|
170
|
+
*/
|
|
171
|
+
function assertUniqueIdentities(file, workers) {
|
|
172
|
+
const workerNames = new Set();
|
|
173
|
+
const deviceOwners = new Map();
|
|
174
|
+
for (const worker of workers) {
|
|
175
|
+
if (workerNames.has(worker.name)) {
|
|
176
|
+
fail(`${file}: duplicate worker name "${worker.name}" in \`spec.workers\`.`);
|
|
177
|
+
}
|
|
178
|
+
workerNames.add(worker.name);
|
|
179
|
+
const devices = worker.config.devices;
|
|
180
|
+
if (!Array.isArray(devices))
|
|
181
|
+
continue;
|
|
182
|
+
for (const device of devices) {
|
|
183
|
+
const id = device?.id;
|
|
184
|
+
if (typeof id !== 'string' || !id)
|
|
185
|
+
continue;
|
|
186
|
+
const owner = deviceOwners.get(id);
|
|
187
|
+
if (owner) {
|
|
188
|
+
fail(`${file}: duplicate device id "${id}" — used by workers "${owner}" and "${worker.name}".\n` +
|
|
189
|
+
'Device ids must be unique across the stack, or only one of them will register.');
|
|
190
|
+
}
|
|
191
|
+
deviceOwners.set(id, worker.name);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Reads and validates `<projectDir>/mdk.yaml` into a `StackSpec`.
|
|
197
|
+
*
|
|
198
|
+
* Throws a `StackSpecError` with an actionable message (pointing at
|
|
199
|
+
* `mdk onboard`) when the file is missing, unparseable, or structurally
|
|
200
|
+
* invalid. Only the fields `mdk run` relies on are validated here; plugin/worker
|
|
201
|
+
* `config` blocks stay opaque (plugin-defined).
|
|
202
|
+
*/
|
|
203
|
+
export function loadStackSpec(projectDir) {
|
|
204
|
+
const file = resolve(projectDir, STACK_FILE);
|
|
205
|
+
if (!existsSync(file)) {
|
|
206
|
+
fail(`No ${STACK_FILE} found in ${resolve(projectDir)}.\nRun \`mdk onboard\` to create one.`);
|
|
207
|
+
}
|
|
208
|
+
let raw;
|
|
209
|
+
try {
|
|
210
|
+
raw = parseYaml(readFileSync(file, 'utf8'));
|
|
211
|
+
}
|
|
212
|
+
catch (error) {
|
|
213
|
+
fail(`${file} is not valid YAML: ${error instanceof Error ? error.message : String(error)}`);
|
|
214
|
+
}
|
|
215
|
+
if (raw === null || typeof raw !== 'object') {
|
|
216
|
+
fail(`${file} is empty or not a mapping. Run \`mdk onboard\` to recreate it.`);
|
|
217
|
+
}
|
|
218
|
+
const doc = raw;
|
|
219
|
+
if (doc.kind !== 'Stack') {
|
|
220
|
+
fail(`${file}: expected \`kind: Stack\`, got ${JSON.stringify(doc.kind ?? null)}.`);
|
|
221
|
+
}
|
|
222
|
+
if (typeof doc.apiVersion !== 'string' || !doc.apiVersion) {
|
|
223
|
+
fail(`${file}: missing \`apiVersion\`.`);
|
|
224
|
+
}
|
|
225
|
+
const spec = doc.spec;
|
|
226
|
+
if (spec === null || typeof spec !== 'object') {
|
|
227
|
+
fail(`${file}: missing \`spec\` mapping.`);
|
|
228
|
+
}
|
|
229
|
+
const s = spec;
|
|
230
|
+
const gateway = s.gateway;
|
|
231
|
+
if (gateway === null || typeof gateway !== 'object') {
|
|
232
|
+
fail(`${file}: missing \`spec.gateway\` mapping.`);
|
|
233
|
+
}
|
|
234
|
+
const g = gateway;
|
|
235
|
+
if (typeof g.port !== 'number') {
|
|
236
|
+
fail(`${file}: \`spec.gateway.port\` must be a number.`);
|
|
237
|
+
}
|
|
238
|
+
if (g.config != null && (typeof g.config !== 'object' || Array.isArray(g.config))) {
|
|
239
|
+
fail(`${file}: \`spec.gateway.config\` must be a mapping.`);
|
|
240
|
+
}
|
|
241
|
+
// The process environment wins over the project's `.env`, as with dotenv.
|
|
242
|
+
const vars = { ...readProjectEnv(resolve(projectDir)), ...process.env };
|
|
243
|
+
const gatewayConfig = resolveEnvDeep(g.config ?? {}, vars);
|
|
244
|
+
const metadata = (doc.metadata ?? {});
|
|
245
|
+
const name = typeof metadata.name === 'string' ? metadata.name : 'mdk-stack';
|
|
246
|
+
if (g.plugins != null)
|
|
247
|
+
fail(unsupportedPluginsMessage(file, g.plugins));
|
|
248
|
+
const kernel = (s.kernel ?? {});
|
|
249
|
+
if (s.mcp != null && (typeof s.mcp !== 'object' || Array.isArray(s.mcp))) {
|
|
250
|
+
fail(`${file}: \`spec.mcp\` must be a mapping.`);
|
|
251
|
+
}
|
|
252
|
+
const rawMcp = (s.mcp ?? {});
|
|
253
|
+
if (rawMcp.port != null && typeof rawMcp.port !== 'number') {
|
|
254
|
+
fail(`${file}: \`spec.mcp.port\` must be a number.`);
|
|
255
|
+
}
|
|
256
|
+
const mcp = typeof rawMcp.port === 'number' ? { port: rawMcp.port } : {};
|
|
257
|
+
if (rawMcp.allowWrite != null) {
|
|
258
|
+
if (typeof rawMcp.allowWrite !== 'boolean')
|
|
259
|
+
fail(`${file}: \`spec.mcp.allowWrite\` must be a boolean.`);
|
|
260
|
+
mcp.allowWrite = rawMcp.allowWrite;
|
|
261
|
+
}
|
|
262
|
+
if (rawMcp.allowTools != null) {
|
|
263
|
+
if (!Array.isArray(rawMcp.allowTools) || !rawMcp.allowTools.every((t) => typeof t === 'string' && t)) {
|
|
264
|
+
fail(`${file}: \`spec.mcp.allowTools\` must be a list of tool ids.`);
|
|
265
|
+
}
|
|
266
|
+
mcp.allowTools = rawMcp.allowTools;
|
|
267
|
+
}
|
|
268
|
+
if (rawMcp.token != null) {
|
|
269
|
+
if (typeof rawMcp.token !== 'string' || !rawMcp.token)
|
|
270
|
+
fail(`${file}: \`spec.mcp.token\` must be a non-empty string.`);
|
|
271
|
+
mcp.token = resolveEnvValue(rawMcp.token, vars);
|
|
272
|
+
}
|
|
273
|
+
if (rawMcp.identity != null) {
|
|
274
|
+
if (typeof rawMcp.identity !== 'object' || Array.isArray(rawMcp.identity)) {
|
|
275
|
+
fail(`${file}: \`spec.mcp.identity\` must be a mapping.`);
|
|
276
|
+
}
|
|
277
|
+
mcp.identity = resolveEnvDeep(rawMcp.identity, vars);
|
|
278
|
+
}
|
|
279
|
+
const workers = Array.isArray(s.workers)
|
|
280
|
+
? s.workers.map((w, i) => {
|
|
281
|
+
if (typeof w?.name !== 'string' || !w.name) {
|
|
282
|
+
fail(`${file}: \`spec.workers[${i}].name\` must be a non-empty string.`);
|
|
283
|
+
}
|
|
284
|
+
// The name becomes a state directory under `.mdk/` (and an identity
|
|
285
|
+
// dir), so it must be a single safe path segment.
|
|
286
|
+
if (!NAME_RE.test(w.name)) {
|
|
287
|
+
fail(`${file}: \`spec.workers[${i}].name\` "${w.name}" is invalid. ` +
|
|
288
|
+
"Use lowercase letters, digits, '.', '_' or '-'.");
|
|
289
|
+
}
|
|
290
|
+
if (typeof w?.package !== 'string' || !w.package) {
|
|
291
|
+
fail(`${file}: \`spec.workers[${i}].package\` must be a non-empty string.`);
|
|
292
|
+
}
|
|
293
|
+
const rawEnv = w.env;
|
|
294
|
+
const env = {};
|
|
295
|
+
if (rawEnv && typeof rawEnv === 'object') {
|
|
296
|
+
for (const [key, value] of Object.entries(rawEnv)) {
|
|
297
|
+
if (typeof value === 'string')
|
|
298
|
+
env[key] = resolveEnvValue(value, vars);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
return {
|
|
302
|
+
name: w.name,
|
|
303
|
+
package: w.package,
|
|
304
|
+
port: typeof w.port === 'number' ? w.port : 0,
|
|
305
|
+
config: (w.config ?? {}),
|
|
306
|
+
env,
|
|
307
|
+
};
|
|
308
|
+
})
|
|
309
|
+
: [];
|
|
310
|
+
assertUniqueIdentities(file, workers);
|
|
311
|
+
if (s.apps != null && !Array.isArray(s.apps)) {
|
|
312
|
+
fail(`${file}: \`spec.apps\` must be a list.`);
|
|
313
|
+
}
|
|
314
|
+
const appNames = new Set();
|
|
315
|
+
const apps = (s.apps ?? []).map((a, i) => {
|
|
316
|
+
const at = `\`spec.apps[${i}]`;
|
|
317
|
+
if (typeof a?.name !== 'string' || !APP_NAME_PATTERN.test(a.name)) {
|
|
318
|
+
fail(`${file}: ${at}.name\` must be lowercase letters, digits and dashes, starting with a letter.`);
|
|
319
|
+
}
|
|
320
|
+
if (appNames.has(a.name))
|
|
321
|
+
fail(`${file}: duplicate app name "${a.name}" in \`spec.apps\`.`);
|
|
322
|
+
appNames.add(a.name);
|
|
323
|
+
if (typeof a.package !== 'string' || !a.package) {
|
|
324
|
+
fail(`${file}: ${at}.package\` must be a non-empty string.`);
|
|
325
|
+
}
|
|
326
|
+
if (a.config != null && (typeof a.config !== 'object' || Array.isArray(a.config))) {
|
|
327
|
+
fail(`${file}: ${at}.config\` must be a mapping.`);
|
|
328
|
+
}
|
|
329
|
+
if (a.mount != null && (typeof a.mount !== 'string' || !isValidMount(a.mount))) {
|
|
330
|
+
fail(`${file}: ${at}.mount\` must be a path of literal segments starting with "/", e.g. / or /ops.`);
|
|
331
|
+
}
|
|
332
|
+
return {
|
|
333
|
+
name: a.name,
|
|
334
|
+
package: a.package,
|
|
335
|
+
config: resolveEnvDeep(a.config ?? {}, vars),
|
|
336
|
+
...(typeof a.mount === 'string' ? { mount: a.mount } : {}),
|
|
337
|
+
};
|
|
338
|
+
});
|
|
339
|
+
return {
|
|
340
|
+
apiVersion: doc.apiVersion,
|
|
341
|
+
kind: 'Stack',
|
|
342
|
+
metadata: { name },
|
|
343
|
+
spec: {
|
|
344
|
+
kernel: { port: typeof kernel.port === 'number' ? kernel.port : 0 },
|
|
345
|
+
gateway: { port: g.port, config: gatewayConfig },
|
|
346
|
+
mcp,
|
|
347
|
+
workers,
|
|
348
|
+
apps,
|
|
349
|
+
},
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
/**
|
|
353
|
+
* The error for a spec with `spec.gateway.plugins`: names the `spec.apps`
|
|
354
|
+
* entries for the packages it lists, ready to paste.
|
|
355
|
+
*/
|
|
356
|
+
function unsupportedPluginsMessage(file, plugins) {
|
|
357
|
+
const apps = (Array.isArray(plugins) ? plugins : [])
|
|
358
|
+
.filter((p) => typeof p?.package === 'string' && p.package !== '')
|
|
359
|
+
.map((p) => {
|
|
360
|
+
const pkg = p.package;
|
|
361
|
+
const config = p.config && typeof p.config === 'object' && !Array.isArray(p.config) ? p.config : undefined;
|
|
362
|
+
return { name: toAppName(shortName(pkg)), package: pkg, ...(config ? { config } : {}) };
|
|
363
|
+
});
|
|
364
|
+
const lines = [
|
|
365
|
+
`${file}: \`spec.gateway.plugins\` is not supported. Apps are declared under \`spec.apps\`; each runs in its own process behind the Gateway.`,
|
|
366
|
+
];
|
|
367
|
+
if (apps.length) {
|
|
368
|
+
const snippet = stringifyYaml({ apps }).trimEnd().split('\n').map((line) => ` ${line}`).join('\n');
|
|
369
|
+
lines.push('For the packages listed there, under `spec`:', '', snippet, '');
|
|
370
|
+
}
|
|
371
|
+
lines.push('Each package also needs an mdk-app.yaml; once it is listed, check it with `mdk validate app <name>`. Add `mount: /` to an app whose', 'clients call its routes at the Gateway root (the bundled agent, for one); otherwise they are served under /apps/<name>.');
|
|
372
|
+
return lines.join('\n');
|
|
373
|
+
}
|
|
374
|
+
export { StackSpecError };
|
|
375
|
+
//# sourceMappingURL=spec.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"spec.js","sourceRoot":"","sources":["../../src/lib/spec.ts"],"names":[],"mappings":"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","sourcesContent":["import { existsSync, readFileSync } from 'node:fs';\nimport { isAbsolute, resolve } from 'node:path';\nimport { parse as parseYaml, stringify as stringifyYaml } from 'yaml';\nimport { findCatalogApp, findCatalogWorker, type CatalogWorker } from './catalog.js';\nimport { DEFAULT_HOST } from './ports.js';\nimport { readProjectEnv } from './env-file.js';\nimport { NAME_RE } from './names.js';\n\nexport interface StackAnswers {\n stackName: string;\n gatewayPort: number;\n kernelPort: number;\n workerBasePort: number;\n workerPackages: string[];\n appPackages: string[];\n /** Gateway-level settings destined for `spec.gateway.config` (see plugin-setup.ts). */\n gatewayConfig?: Record<string, unknown>;\n /** Per-app setup answers, keyed by package, destined for `spec.apps[].config`. */\n pluginConfigs?: Record<string, Record<string, unknown>>;\n}\n\nexport interface WorkerInstance {\n name: string;\n package: string;\n port: number;\n config: Record<string, unknown>;\n /** Resolved by `loadStackSpec` from `spec.workers[].env` — see `resolveEnvValue`. */\n env?: Record<string, string>;\n}\n\n/** One `spec.apps[]` entry: a package with an `mdk-app.yaml`, run as its own runtime behind the Gateway. */\nexport interface AppInstance {\n /** Matches `APP_NAME_PATTERN`; the Gateway serves the app under `/apps/<name>` unless `mount` is set. */\n name: string;\n package: string;\n config: Record<string, unknown>;\n mount?: string;\n}\n\nconst APP_NAME_PATTERN = /^[a-z][a-z0-9-]*$/;\n\n/**\n * The Gateway's own rule for `mount` (startGateway in @tetherto/mdk-core): a\n * leading \"/\" and literal segments (no `:param`, `*`, `.` or `..`). Checked\n * here so a bad mount fails before anything boots, not in the Gateway last.\n */\nfunction isValidMount(mount: string): boolean {\n return mount.startsWith('/') && mount.split('/').filter(Boolean).every((s) => /^(?!\\.\\.?$)[A-Za-z0-9._~-]+$/.test(s));\n}\n\n/** Coerces a package or folder name into a valid app name. */\nexport function toAppName(raw: string): string {\n const slug = raw.toLowerCase().replace(/[^a-z0-9-]+/g, '-').replace(/^-+|-+$/g, '');\n return /^[a-z]/.test(slug) ? slug : `app-${slug}`;\n}\n\nexport interface McpSpec {\n port?: number;\n allowWrite?: boolean;\n allowTools?: string[];\n token?: string;\n identity?: Record<string, unknown>;\n}\n\nexport interface StackSpec {\n apiVersion: string;\n kind: string;\n metadata: { name: string };\n spec: {\n kernel: { port: number };\n gateway: {\n port: number;\n /** Merged into the gateway's `common.json` — gateway-wide settings only;\n * an app's own settings belong in its `spec.apps[].config` block. */\n config: Record<string, unknown>;\n };\n /**\n * Optional MCP server settings; `port` defaults to `gateway.port + 100` when\n * the CLI boots it. Write tools stay hidden unless `allowWrite` (or the\n * `--allow-write` flag) is set or they are named in `allowTools`; `auth: true`\n * Gateway routes need an `identity`. `token` (use a `${VAR}` placeholder)\n * requires `Authorization: Bearer <token>` on /mcp.\n */\n mcp: McpSpec;\n workers: WorkerInstance[];\n apps: AppInstance[];\n };\n}\n\n/**\n * Whether a spec's `package` names a directory on disk rather than an npm\n * package — relative or absolute either way. Scaffolded components are written\n * as paths, so they are resolved from disk and never handed to `npm install`.\n */\nexport function isLocalPackagePath(pkg: string): boolean {\n return pkg.startsWith('.') || isAbsolute(pkg);\n}\n\nconst ENV_REF_RE = /\\$\\{([A-Za-z_][A-Za-z0-9_]*)\\}/g;\n\n/**\n * Resolves one string from `mdk.yaml` (`spec.workers[].env`, and every string in\n * `spec.gateway.config` / `spec.apps[].config`): `${NAME}` is\n * replaced with `process.env.NAME`, else the project's `.env`, falling back to\n * the literal text when the variable is unset — enough to keep secrets out of\n * mdk.yaml without a templating engine.\n */\nfunction resolveEnvValue(value: string, vars: Record<string, string | undefined>): string {\n return value.replace(ENV_REF_RE, (match, name: string) => vars[name] ?? match);\n}\n\n/**\n * The first `${NAME}` that `resolveEnvValue` left in `value` because NAME was\n * unset, or undefined. For a secret, where keeping the literal text would turn\n * a missing variable into a guessable value.\n */\nexport function unresolvedEnvRef(value: string): string | undefined {\n return value.match(new RegExp(ENV_REF_RE.source))?.[1];\n}\n\n/** `resolveEnvValue` applied to every string inside a config block. */\nfunction resolveEnvDeep(value: unknown, vars: Record<string, string | undefined>): unknown {\n if (typeof value === 'string') return resolveEnvValue(value, vars);\n if (Array.isArray(value)) return value.map((item) => resolveEnvDeep(item, vars));\n if (value !== null && typeof value === 'object') {\n return Object.fromEntries(\n Object.entries(value).map(([key, item]) => [key, resolveEnvDeep(item, vars)]),\n );\n }\n return value;\n}\n\nexport function shortName(pkg: string): string {\n const base = pkg.split('/').pop() ?? pkg;\n return base.replace(/^mdk-(worker-|plugin-|app-)?/, '') || base;\n}\n\n/** First port a seeded mock device server is offered. */\nexport const MOCK_PORT_BASE = 18080;\n\n/** Stack-unique id for a worker's seed device (worker names are unique). */\nexport function deviceId(workerName: string): string {\n return `${workerName}-0`;\n}\n\n/** Matching serial for the seed device, in the shape device firmware reports. */\nexport function deviceSerial(workerName: string): string {\n return deviceId(workerName).toUpperCase();\n}\n\n/**\n * Config block for one selected worker.\n *\n * Worker config is plugin-defined and otherwise opaque to the CLI, so this can\n * only be filled in for catalog entries whose device shape we know. Those get a\n * seed device and `mock: true`, which is the whole difference between a spec\n * that boots and one the user has to finish by hand.\n */\nfunction workerConfig(\n entry: CatalogWorker | undefined,\n name: string,\n mockPort: number,\n): Record<string, unknown> {\n // Nothing known about this package's devices — leave an editable starter value\n // and let `mdk run` name what is still missing.\n if (!entry?.deviceOpts && !entry?.mock) return { pollIntervalMs: 2000 };\n\n return {\n ...(entry.config ?? {}),\n ...(entry.mock ? { mock: true } : {}),\n devices: [\n {\n id: deviceId(name),\n opts: {\n host: DEFAULT_HOST,\n port: mockPort,\n serial: deviceSerial(name),\n ...entry.deviceOpts,\n },\n },\n ],\n };\n}\n\nfunction pluginConfig(pkg: string): Record<string, unknown> {\n if (pkg.includes('summary')) return { refreshIntervalMs: 5000 };\n if (pkg.includes('alerts')) return { evaluateIntervalMs: 10000 };\n return {};\n}\n\n/**\n * Builds the declarative stack spec (`mdk.yaml`) purely from the onboarding\n * answers. One worker instance is created per selected worker plugin, and each\n * selected app is added under `spec.apps`.\n */\nexport function buildStackSpec(answers: StackAnswers): StackSpec {\n const workers: WorkerInstance[] = answers.workerPackages.map((pkg, i) => {\n const entry = findCatalogWorker(pkg);\n const name = `${shortName(pkg)}-a`;\n return {\n name,\n // Always the package name: a bundled worker is `file:`-linked into the\n // project's node_modules under this name (see addFileDependency), so the\n // spec stays portable and does not hard-code anyone's checkout path.\n package: pkg,\n port: answers.workerBasePort + i * 2,\n // One mock port per worker, so selecting several cannot collide.\n config: workerConfig(entry, name, MOCK_PORT_BASE + i),\n };\n });\n\n return {\n apiVersion: 'mdk/v1',\n kind: 'Stack',\n metadata: { name: answers.stackName },\n spec: {\n kernel: { port: answers.kernelPort },\n gateway: {\n port: answers.gatewayPort,\n config: answers.gatewayConfig ?? {},\n },\n // Onboarding never asks about MCP settings; `loadStackSpec` defaults\n // this the same way when the section is absent from mdk.yaml entirely.\n mcp: {},\n workers,\n apps: answers.appPackages.map((pkg) => {\n const mount = findCatalogApp(pkg)?.mount;\n return {\n name: toAppName(shortName(pkg)),\n package: pkg,\n config: answers.pluginConfigs?.[pkg] ?? pluginConfig(pkg),\n ...(mount !== undefined ? { mount } : {}),\n };\n }),\n },\n };\n}\n\n/** File name of the declarative stack spec, relative to a project directory. */\nexport const STACK_FILE = 'mdk.yaml';\n\nclass StackSpecError extends Error {}\n\nfunction fail(message: string): never {\n throw new StackSpecError(message);\n}\n\n/**\n * Rejects worker names and device ids that repeat across the stack.\n *\n * The Kernel registers both globally, so a duplicate does not error — the second\n * registration is simply dropped, and the stack comes up looking fine while a\n * worker is missing from the fleet. Catching it here turns a silent, confusing\n * runtime failure into a spec error that names both offenders.\n */\nfunction assertUniqueIdentities(file: string, workers: WorkerInstance[]): void {\n const workerNames = new Set<string>();\n const deviceOwners = new Map<string, string>();\n\n for (const worker of workers) {\n if (workerNames.has(worker.name)) {\n fail(`${file}: duplicate worker name \"${worker.name}\" in \\`spec.workers\\`.`);\n }\n workerNames.add(worker.name);\n\n const devices = (worker.config as { devices?: Array<{ id?: unknown }> }).devices;\n if (!Array.isArray(devices)) continue;\n\n for (const device of devices) {\n const id = device?.id;\n if (typeof id !== 'string' || !id) continue;\n\n const owner = deviceOwners.get(id);\n if (owner) {\n fail(\n `${file}: duplicate device id \"${id}\" — used by workers \"${owner}\" and \"${worker.name}\".\\n` +\n 'Device ids must be unique across the stack, or only one of them will register.',\n );\n }\n deviceOwners.set(id, worker.name);\n }\n }\n}\n\n/**\n * Reads and validates `<projectDir>/mdk.yaml` into a `StackSpec`.\n *\n * Throws a `StackSpecError` with an actionable message (pointing at\n * `mdk onboard`) when the file is missing, unparseable, or structurally\n * invalid. Only the fields `mdk run` relies on are validated here; plugin/worker\n * `config` blocks stay opaque (plugin-defined).\n */\nexport function loadStackSpec(projectDir: string): StackSpec {\n const file = resolve(projectDir, STACK_FILE);\n if (!existsSync(file)) {\n fail(`No ${STACK_FILE} found in ${resolve(projectDir)}.\\nRun \\`mdk onboard\\` to create one.`);\n }\n\n let raw: unknown;\n try {\n raw = parseYaml(readFileSync(file, 'utf8'));\n } catch (error) {\n fail(`${file} is not valid YAML: ${error instanceof Error ? error.message : String(error)}`);\n }\n\n if (raw === null || typeof raw !== 'object') {\n fail(`${file} is empty or not a mapping. Run \\`mdk onboard\\` to recreate it.`);\n }\n const doc = raw as Record<string, unknown>;\n\n if (doc.kind !== 'Stack') {\n fail(`${file}: expected \\`kind: Stack\\`, got ${JSON.stringify(doc.kind ?? null)}.`);\n }\n if (typeof doc.apiVersion !== 'string' || !doc.apiVersion) {\n fail(`${file}: missing \\`apiVersion\\`.`);\n }\n\n const spec = doc.spec;\n if (spec === null || typeof spec !== 'object') {\n fail(`${file}: missing \\`spec\\` mapping.`);\n }\n const s = spec as Record<string, unknown>;\n\n const gateway = s.gateway;\n if (gateway === null || typeof gateway !== 'object') {\n fail(`${file}: missing \\`spec.gateway\\` mapping.`);\n }\n const g = gateway as Record<string, unknown>;\n if (typeof g.port !== 'number') {\n fail(`${file}: \\`spec.gateway.port\\` must be a number.`);\n }\n\n if (g.config != null && (typeof g.config !== 'object' || Array.isArray(g.config))) {\n fail(`${file}: \\`spec.gateway.config\\` must be a mapping.`);\n }\n // The process environment wins over the project's `.env`, as with dotenv.\n const vars: Record<string, string | undefined> = { ...readProjectEnv(resolve(projectDir)), ...process.env };\n const gatewayConfig = resolveEnvDeep(g.config ?? {}, vars) as Record<string, unknown>;\n\n const metadata = (doc.metadata ?? {}) as Record<string, unknown>;\n const name = typeof metadata.name === 'string' ? metadata.name : 'mdk-stack';\n\n if (g.plugins != null) fail(unsupportedPluginsMessage(file, g.plugins));\n\n const kernel = (s.kernel ?? {}) as Record<string, unknown>;\n\n if (s.mcp != null && (typeof s.mcp !== 'object' || Array.isArray(s.mcp))) {\n fail(`${file}: \\`spec.mcp\\` must be a mapping.`);\n }\n const rawMcp = (s.mcp ?? {}) as Record<string, unknown>;\n if (rawMcp.port != null && typeof rawMcp.port !== 'number') {\n fail(`${file}: \\`spec.mcp.port\\` must be a number.`);\n }\n const mcp: McpSpec = typeof rawMcp.port === 'number' ? { port: rawMcp.port } : {};\n if (rawMcp.allowWrite != null) {\n if (typeof rawMcp.allowWrite !== 'boolean') fail(`${file}: \\`spec.mcp.allowWrite\\` must be a boolean.`);\n mcp.allowWrite = rawMcp.allowWrite;\n }\n if (rawMcp.allowTools != null) {\n if (!Array.isArray(rawMcp.allowTools) || !rawMcp.allowTools.every((t) => typeof t === 'string' && t)) {\n fail(`${file}: \\`spec.mcp.allowTools\\` must be a list of tool ids.`);\n }\n mcp.allowTools = rawMcp.allowTools as string[];\n }\n if (rawMcp.token != null) {\n if (typeof rawMcp.token !== 'string' || !rawMcp.token) fail(`${file}: \\`spec.mcp.token\\` must be a non-empty string.`);\n mcp.token = resolveEnvValue(rawMcp.token, vars);\n }\n if (rawMcp.identity != null) {\n if (typeof rawMcp.identity !== 'object' || Array.isArray(rawMcp.identity)) {\n fail(`${file}: \\`spec.mcp.identity\\` must be a mapping.`);\n }\n mcp.identity = resolveEnvDeep(rawMcp.identity, vars) as Record<string, unknown>;\n }\n\n const workers = Array.isArray(s.workers)\n ? (s.workers as Array<Record<string, unknown>>).map((w, i) => {\n if (typeof w?.name !== 'string' || !w.name) {\n fail(`${file}: \\`spec.workers[${i}].name\\` must be a non-empty string.`);\n }\n // The name becomes a state directory under `.mdk/` (and an identity\n // dir), so it must be a single safe path segment.\n if (!NAME_RE.test(w.name as string)) {\n fail(\n `${file}: \\`spec.workers[${i}].name\\` \"${w.name as string}\" is invalid. ` +\n \"Use lowercase letters, digits, '.', '_' or '-'.\",\n );\n }\n if (typeof w?.package !== 'string' || !w.package) {\n fail(`${file}: \\`spec.workers[${i}].package\\` must be a non-empty string.`);\n }\n const rawEnv = w.env as Record<string, unknown> | undefined;\n const env: Record<string, string> = {};\n if (rawEnv && typeof rawEnv === 'object') {\n for (const [key, value] of Object.entries(rawEnv)) {\n if (typeof value === 'string') env[key] = resolveEnvValue(value, vars);\n }\n }\n return {\n name: w.name as string,\n package: w.package as string,\n port: typeof w.port === 'number' ? (w.port as number) : 0,\n config: (w.config ?? {}) as Record<string, unknown>,\n env,\n };\n })\n : [];\n\n assertUniqueIdentities(file, workers);\n\n if (s.apps != null && !Array.isArray(s.apps)) {\n fail(`${file}: \\`spec.apps\\` must be a list.`);\n }\n const appNames = new Set<string>();\n const apps = ((s.apps ?? []) as Array<Record<string, unknown>>).map((a, i): AppInstance => {\n const at = `\\`spec.apps[${i}]`;\n if (typeof a?.name !== 'string' || !APP_NAME_PATTERN.test(a.name)) {\n fail(`${file}: ${at}.name\\` must be lowercase letters, digits and dashes, starting with a letter.`);\n }\n if (appNames.has(a.name)) fail(`${file}: duplicate app name \"${a.name}\" in \\`spec.apps\\`.`);\n appNames.add(a.name);\n if (typeof a.package !== 'string' || !a.package) {\n fail(`${file}: ${at}.package\\` must be a non-empty string.`);\n }\n if (a.config != null && (typeof a.config !== 'object' || Array.isArray(a.config))) {\n fail(`${file}: ${at}.config\\` must be a mapping.`);\n }\n if (a.mount != null && (typeof a.mount !== 'string' || !isValidMount(a.mount))) {\n fail(`${file}: ${at}.mount\\` must be a path of literal segments starting with \"/\", e.g. / or /ops.`);\n }\n return {\n name: a.name,\n package: a.package,\n config: resolveEnvDeep(a.config ?? {}, vars) as Record<string, unknown>,\n ...(typeof a.mount === 'string' ? { mount: a.mount } : {}),\n };\n });\n\n return {\n apiVersion: doc.apiVersion,\n kind: 'Stack',\n metadata: { name },\n spec: {\n kernel: { port: typeof kernel.port === 'number' ? (kernel.port as number) : 0 },\n gateway: { port: g.port, config: gatewayConfig },\n mcp,\n workers,\n apps,\n },\n };\n}\n\n/**\n * The error for a spec with `spec.gateway.plugins`: names the `spec.apps`\n * entries for the packages it lists, ready to paste.\n */\nfunction unsupportedPluginsMessage(file: string, plugins: unknown): string {\n const apps = (Array.isArray(plugins) ? plugins : [])\n .filter((p): p is Record<string, unknown> => typeof p?.package === 'string' && p.package !== '')\n .map((p) => {\n const pkg = p.package as string;\n const config = p.config && typeof p.config === 'object' && !Array.isArray(p.config) ? p.config : undefined;\n return { name: toAppName(shortName(pkg)), package: pkg, ...(config ? { config } : {}) };\n });\n const lines = [\n `${file}: \\`spec.gateway.plugins\\` is not supported. Apps are declared under \\`spec.apps\\`; each runs in its own process behind the Gateway.`,\n ];\n if (apps.length) {\n const snippet = stringifyYaml({ apps }).trimEnd().split('\\n').map((line) => ` ${line}`).join('\\n');\n lines.push('For the packages listed there, under `spec`:', '', snippet, '');\n }\n lines.push(\n 'Each package also needs an mdk-app.yaml; once it is listed, check it with `mdk validate app <name>`. Add `mount: /` to an app whose',\n 'clients call its routes at the Gateway root (the bundled agent, for one); otherwise they are served under /apps/<name>.',\n );\n return lines.join('\\n');\n}\n\nexport { StackSpecError };\n"]}
|