@tetherto/mdk-cli 0.0.0-stage → 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.
Files changed (112) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +446 -2
  3. package/dist/commands/add.js +61 -0
  4. package/dist/commands/add.js.map +1 -0
  5. package/dist/commands/agent.js +42 -0
  6. package/dist/commands/agent.js.map +1 -0
  7. package/dist/commands/create.js +261 -0
  8. package/dist/commands/create.js.map +1 -0
  9. package/dist/commands/inspect.js +171 -0
  10. package/dist/commands/inspect.js.map +1 -0
  11. package/dist/commands/meta.js +12 -0
  12. package/dist/commands/meta.js.map +1 -0
  13. package/dist/commands/onboard.js +447 -0
  14. package/dist/commands/onboard.js.map +1 -0
  15. package/dist/commands/run.js +203 -0
  16. package/dist/commands/run.js.map +1 -0
  17. package/dist/commands/validate.js +125 -0
  18. package/dist/commands/validate.js.map +1 -0
  19. package/dist/index.js +9 -0
  20. package/dist/index.js.map +1 -0
  21. package/dist/lib/add-package.js +148 -0
  22. package/dist/lib/add-package.js.map +1 -0
  23. package/dist/lib/app-package.js +182 -0
  24. package/dist/lib/app-package.js.map +1 -0
  25. package/dist/lib/app-scaffold.js +221 -0
  26. package/dist/lib/app-scaffold.js.map +1 -0
  27. package/dist/lib/app-ui-check.js +178 -0
  28. package/dist/lib/app-ui-check.js.map +1 -0
  29. package/dist/lib/app-ui-runner.js +122 -0
  30. package/dist/lib/app-ui-runner.js.map +1 -0
  31. package/dist/lib/catalog.js +69 -0
  32. package/dist/lib/catalog.js.map +1 -0
  33. package/dist/lib/checkout.js +14 -0
  34. package/dist/lib/checkout.js.map +1 -0
  35. package/dist/lib/dashboard-apps.js +71 -0
  36. package/dist/lib/dashboard-apps.js.map +1 -0
  37. package/dist/lib/dashboard.js +318 -0
  38. package/dist/lib/dashboard.js.map +1 -0
  39. package/dist/lib/detect.js +38 -0
  40. package/dist/lib/detect.js.map +1 -0
  41. package/dist/lib/env-file.js +57 -0
  42. package/dist/lib/env-file.js.map +1 -0
  43. package/dist/lib/fetch-template.js +33 -0
  44. package/dist/lib/fetch-template.js.map +1 -0
  45. package/dist/lib/help.js +22 -0
  46. package/dist/lib/help.js.map +1 -0
  47. package/dist/lib/names.js +36 -0
  48. package/dist/lib/names.js.map +1 -0
  49. package/dist/lib/npm.js +113 -0
  50. package/dist/lib/npm.js.map +1 -0
  51. package/dist/lib/package-source.js +76 -0
  52. package/dist/lib/package-source.js.map +1 -0
  53. package/dist/lib/pkg.js +5 -0
  54. package/dist/lib/pkg.js.map +1 -0
  55. package/dist/lib/plugin-setup.js +204 -0
  56. package/dist/lib/plugin-setup.js.map +1 -0
  57. package/dist/lib/ports.js +31 -0
  58. package/dist/lib/ports.js.map +1 -0
  59. package/dist/lib/project.js +297 -0
  60. package/dist/lib/project.js.map +1 -0
  61. package/dist/lib/run-lock.js +188 -0
  62. package/dist/lib/run-lock.js.map +1 -0
  63. package/dist/lib/runtime.js +683 -0
  64. package/dist/lib/runtime.js.map +1 -0
  65. package/dist/lib/scaffold.js +82 -0
  66. package/dist/lib/scaffold.js.map +1 -0
  67. package/dist/lib/shutdown.js +53 -0
  68. package/dist/lib/shutdown.js.map +1 -0
  69. package/dist/lib/skill.js +38 -0
  70. package/dist/lib/skill.js.map +1 -0
  71. package/dist/lib/spec.js +375 -0
  72. package/dist/lib/spec.js.map +1 -0
  73. package/dist/lib/status.js +245 -0
  74. package/dist/lib/status.js.map +1 -0
  75. package/dist/lib/theme.js +50 -0
  76. package/dist/lib/theme.js.map +1 -0
  77. package/dist/lib/worker-package.js +74 -0
  78. package/dist/lib/worker-package.js.map +1 -0
  79. package/dist/lib/worker-scaffold.js +122 -0
  80. package/dist/lib/worker-scaffold.js.map +1 -0
  81. package/dist/lib/worker-smoke.js +243 -0
  82. package/dist/lib/worker-smoke.js.map +1 -0
  83. package/dist/program.js +58 -0
  84. package/dist/program.js.map +1 -0
  85. package/package.json +57 -4
  86. package/templates/app/README.md +66 -0
  87. package/templates/app/mdk-app.yaml +10 -0
  88. package/templates/app/package.json +17 -0
  89. package/templates/app/plugin/controllers/summary.js +36 -0
  90. package/templates/app/plugin/lib/client.js +11 -0
  91. package/templates/app/plugin/mdk-plugin.json +16 -0
  92. package/templates/app-ui/tsconfig.json +29 -0
  93. package/templates/app-ui/ui-src/components/summary-panel.tsx +24 -0
  94. package/templates/app-ui/ui-src/data/summary.ts +18 -0
  95. package/templates/app-ui/ui-src/hooks/use-summary.ts +54 -0
  96. package/templates/app-ui/ui-src/index.tsx +15 -0
  97. package/templates/app-ui/ui-src/pages/overview-page.tsx +18 -0
  98. package/templates/app-ui/ui-src/styles.css +13 -0
  99. package/templates/app-ui/ui-src/styles.d.ts +3 -0
  100. package/templates/app-ui/ui-src/vite.config.ts +26 -0
  101. package/templates/worker/README.md +93 -0
  102. package/templates/worker/mdk-contract.json +26 -0
  103. package/templates/worker/mock/server.js +93 -0
  104. package/templates/worker/package.json +23 -0
  105. package/templates/worker/smoke.config.js +31 -0
  106. package/templates/worker/src/client.js +47 -0
  107. package/templates/worker/src/commands/reboot.js +5 -0
  108. package/templates/worker/src/commands/set-power-mode.js +10 -0
  109. package/templates/worker/src/telemetry/hashrate-rt.js +5 -0
  110. package/templates/worker/src/telemetry/power-mode.js +5 -0
  111. package/templates/worker/src/telemetry/power.js +5 -0
  112. package/templates/worker/src/telemetry/temperature.js +5 -0
@@ -0,0 +1,297 @@
1
+ import { appendFileSync, existsSync, readFileSync, writeFileSync } from 'node:fs';
2
+ import { basename, isAbsolute, join, relative, resolve, sep } from 'node:path';
3
+ import { parse as parseYaml } from 'yaml';
4
+ /**
5
+ * Project scaffolding shared by `mdk onboard` and `mdk create` — the files that
6
+ * describe the *project* rather than any one component.
7
+ *
8
+ * The emitted layout is role-grouped, mirroring MDK's own component model, so a
9
+ * path in `mdk.yaml` says what it is:
10
+ *
11
+ * mdk.yaml the stack spec
12
+ * package.json private root manifest (no `workspaces` key — path-backed
13
+ * packages link via relative `file:` deps so `npm install`
14
+ * works inside the project directory)
15
+ * workers/<name>/ worker plugins (mdk create worker)
16
+ * apps/<name>/ apps (mdk create app)
17
+ * apps/dashboard/ the UI dashboard (mdk create dashboard)
18
+ * .mdk/ runtime state written by `mdk run` (gitignored)
19
+ */
20
+ /** Component directories, relative to the project root. */
21
+ export const DIRS = {
22
+ workers: 'workers',
23
+ apps: 'apps',
24
+ /** Canonical dashboard location — a stable path, unlike the app's package name. */
25
+ dashboard: join('apps', 'dashboard'),
26
+ };
27
+ /**
28
+ * Historical workspace globs (`workers/*`, `plugins/*`). Kept for reading
29
+ * pre-existing manifests that still declare them (`isWorkspaceMember`); new
30
+ * projects never write a `workspaces` key — local scaffolds use `file:` deps.
31
+ */
32
+ export const WORKSPACES = [`${DIRS.workers}/*`, 'plugins/*'];
33
+ /** Coerces a stack name into a valid npm package name. */
34
+ function npmName(name) {
35
+ const slug = name
36
+ .trim()
37
+ .toLowerCase()
38
+ .replace(/[^a-z0-9._-]+/g, '-')
39
+ .replace(/^[._-]+|[-._]+$/g, '');
40
+ return slug || 'mdk-stack';
41
+ }
42
+ /**
43
+ * Ensures the project has a private root `package.json`. Creates one if absent
44
+ * (no `workspaces` key — local packages are linked via `file:` dependencies).
45
+ * An existing manifest is never modified (name, deps, scripts, or workspaces).
46
+ */
47
+ export function ensureProjectManifest(targetDir, stackName) {
48
+ const path = join(targetDir, 'package.json');
49
+ if (!existsSync(path)) {
50
+ const manifest = {
51
+ name: npmName(stackName),
52
+ version: '0.1.0',
53
+ private: true,
54
+ scripts: { dev: 'mdk run' },
55
+ };
56
+ writeFileSync(path, `${JSON.stringify(manifest, null, 2)}\n`, 'utf8');
57
+ return 'created';
58
+ }
59
+ // Existing manifest is the user's — never overwrite it, and never inject workspaces.
60
+ return 'present';
61
+ }
62
+ /** The project's declared workspace globs (empty when it is not a workspace root). */
63
+ function readWorkspaces(targetDir) {
64
+ const path = join(targetDir, 'package.json');
65
+ if (!existsSync(path))
66
+ return [];
67
+ try {
68
+ const pkg = JSON.parse(readFileSync(path, 'utf8'));
69
+ return Array.isArray(pkg.workspaces) ? pkg.workspaces.filter((w) => typeof w === 'string') : [];
70
+ }
71
+ catch {
72
+ return [];
73
+ }
74
+ }
75
+ /**
76
+ * True when `packageDir` is covered by one of the project's workspace globs —
77
+ * i.e. a root `npm install` would actually install it. Only such components may
78
+ * be installed from the root; anything else (the dashboard, which is
79
+ * intentionally outside the workspaces) has to install in its own directory.
80
+ */
81
+ export function isWorkspaceMember(targetDir, packageDir) {
82
+ const globs = readWorkspaces(targetDir);
83
+ if (!globs.length)
84
+ return false;
85
+ const rel = relative(resolve(targetDir), resolve(packageDir));
86
+ if (!rel || rel.startsWith('..'))
87
+ return false;
88
+ const segments = rel.split(sep);
89
+ return globs.some((glob) => {
90
+ const parts = glob.split('/');
91
+ return (parts.length === segments.length && parts.every((p, i) => p === '*' || p === segments[i]));
92
+ });
93
+ }
94
+ /**
95
+ * True when the project root already declares a `file:` dependency — in
96
+ * either `dependencies` or `devDependencies` — that points at `packageDir`, so
97
+ * `npm install` must run at the project root to create the symlink the
98
+ * runtime resolves from `node_modules`.
99
+ */
100
+ export function isFileLinkedFromProject(targetDir, packageDir) {
101
+ const path = join(targetDir, 'package.json');
102
+ if (!existsSync(path))
103
+ return false;
104
+ try {
105
+ const pkg = JSON.parse(readFileSync(path, 'utf8'));
106
+ const deps = { ...pkg.devDependencies, ...pkg.dependencies };
107
+ const absPackage = resolve(packageDir);
108
+ return Object.values(deps).some((spec) => {
109
+ if (!spec.startsWith('file:'))
110
+ return false;
111
+ return resolve(targetDir, spec.slice('file:'.length)) === absPackage;
112
+ });
113
+ }
114
+ catch {
115
+ return false;
116
+ }
117
+ }
118
+ /** Sets one `dependencies` entry in the project root manifest, if not already set. */
119
+ function setDependency(targetDir, packageName, versionSpec) {
120
+ const path = join(targetDir, 'package.json');
121
+ if (!existsSync(path))
122
+ throw new Error('no package.json');
123
+ const pkg = JSON.parse(readFileSync(path, 'utf8'));
124
+ if (!pkg.dependencies)
125
+ pkg.dependencies = {};
126
+ if (pkg.dependencies[packageName] === versionSpec)
127
+ return;
128
+ pkg.dependencies[packageName] = versionSpec;
129
+ writeFileSync(path, `${JSON.stringify(pkg, null, 2)}\n`, 'utf8');
130
+ }
131
+ /**
132
+ * Declares a `file:`-linked dependency pointing at `absPath`, written relative
133
+ * to `targetDir`. Works for packages inside the project folder and for bundled
134
+ * monorepo catalog checkouts outside it — never appends to a `workspaces`
135
+ * array, and never writes bare `*` (that hits the registry when the project is
136
+ * not a monorepo workspace member). `npm install` resolves the relative
137
+ * `file:` specifier against the project root and symlinks it into
138
+ * `node_modules/<packageName>`.
139
+ */
140
+ export function addFileDependency(targetDir, packageName, absPath) {
141
+ const target = resolve(absPath);
142
+ const rel = relative(resolve(targetDir), target);
143
+ // `path.relative()` returns an absolute path when `targetDir` and `absPath`
144
+ // are on different drives (Windows only) — fall back to the absolute path
145
+ // rather than writing a `file:./C:/...` spec, which npm resolves under the
146
+ // project dir and 404s.
147
+ const posix = (isAbsolute(rel) ? target : rel).split(sep).join('/');
148
+ const spec = isAbsolute(rel) || posix.startsWith('.') ? posix : `./${posix}`;
149
+ setDependency(targetDir, packageName, `file:${spec}`);
150
+ }
151
+ /**
152
+ * The stack name from `<dir>/mdk.yaml` (falling back to the directory name), or
153
+ * null when there is no spec — i.e. when the directory is not an MDK project and
154
+ * nothing should be scaffolded into it.
155
+ */
156
+ export function readStackName(targetDir) {
157
+ const specPath = join(targetDir, 'mdk.yaml');
158
+ if (!existsSync(specPath))
159
+ return null;
160
+ try {
161
+ const doc = parseYaml(readFileSync(specPath, 'utf8'));
162
+ const name = doc?.metadata?.name?.trim();
163
+ if (name)
164
+ return name;
165
+ }
166
+ catch {
167
+ /* fall through to the directory name */
168
+ }
169
+ return basename(resolve(targetDir));
170
+ }
171
+ const GITIGNORE_HEADER = '# MDK';
172
+ const GITIGNORE_BLOCK = [
173
+ `${GITIGNORE_HEADER} runtime state — kernel/gateway stores, worker DBs, discovery keys`,
174
+ '.mdk/',
175
+ '',
176
+ '# Dependencies & build output',
177
+ 'node_modules/',
178
+ 'dist/',
179
+ 'coverage/',
180
+ '.vite/',
181
+ '*.tsbuildinfo',
182
+ '',
183
+ '# Local environment (commit .env.example instead)',
184
+ '.env',
185
+ '.env.local',
186
+ '*.log',
187
+ '',
188
+ ].join('\n');
189
+ /**
190
+ * Ensures the project has a `.gitignore` covering MDK runtime state and the
191
+ * build output of the components it scaffolds. Creates one if absent; if a
192
+ * `.gitignore` already exists without the MDK block, appends it (never clobbers
193
+ * user content).
194
+ */
195
+ export function ensureProjectGitignore(targetDir) {
196
+ const path = join(targetDir, '.gitignore');
197
+ if (!existsSync(path)) {
198
+ writeFileSync(path, GITIGNORE_BLOCK, 'utf8');
199
+ return 'created';
200
+ }
201
+ const current = readFileSync(path, 'utf8');
202
+ if (current.includes(GITIGNORE_HEADER) || current.includes('.mdk/')) {
203
+ // `.env` holds secret plugin-setup answers, so it must be ignored even when
204
+ // an older or hand-edited MDK block left it out.
205
+ if (/^\/?\.env\s*$/m.test(current))
206
+ return 'present';
207
+ appendFileSync(path, `${current.endsWith('\n') ? '' : '\n'}.env\n`, 'utf8');
208
+ return 'updated';
209
+ }
210
+ appendFileSync(path, `${current.endsWith('\n') ? '' : '\n'}\n${GITIGNORE_BLOCK}`, 'utf8');
211
+ return 'updated';
212
+ }
213
+ function runSection({ workerNames }) {
214
+ const workers = workerNames.length
215
+ ? workerNames.map((name) => `mdk run worker ${name}`)
216
+ : ['mdk run worker <name>'];
217
+ return [
218
+ '```bash',
219
+ 'npm install # once, links workers/ and apps/',
220
+ 'mdk run # boots Kernel, workers, apps and Gateway together',
221
+ '```',
222
+ '',
223
+ 'Or run each component in its own terminal:',
224
+ '',
225
+ '```bash',
226
+ 'mdk run kernel',
227
+ 'mdk run gateway',
228
+ ...workers,
229
+ '```',
230
+ ].join('\n');
231
+ }
232
+ /**
233
+ * Writes a project README describing the emitted layout and how to run the
234
+ * stack. Never overwrites an existing README.
235
+ */
236
+ export function ensureProjectReadme(targetDir, opts) {
237
+ const path = join(targetDir, 'README.md');
238
+ if (existsSync(path))
239
+ return 'present';
240
+ const dashboard = opts.dashboardDir
241
+ ? [
242
+ '## Dashboard',
243
+ '',
244
+ '```bash',
245
+ `cd ${opts.dashboardDir}`,
246
+ 'npm install',
247
+ 'npm run dev',
248
+ '```',
249
+ '',
250
+ 'It proxies `/auth`, `/api` and `/pub` to the Gateway, so start the stack first.',
251
+ '',
252
+ '',
253
+ ].join('\n')
254
+ : '';
255
+ const content = `# ${opts.stackName}
256
+
257
+ An MDK stack — a Kernel, a Gateway and its Workers — described declaratively in
258
+ \`mdk.yaml\`. Edit that file to change ports, apps, workers or the
259
+ devices each worker manages.
260
+
261
+ ## Layout
262
+
263
+ \`\`\`
264
+ mdk.yaml the stack spec: ports, apps, workers, devices
265
+ workers/<name>/ worker plugins — device contract, handlers and a mock device
266
+ apps/<name>/ apps — HTTP aggregation endpoints served through the Gateway
267
+ apps/dashboard/ the UI dashboard (Vite app)
268
+ .mdk/ runtime state written by \`mdk run\` — disposable, gitignored
269
+ \`\`\`
270
+
271
+ Workers, apps and bundled monorepo catalog packages are linked with relative
272
+ \`file:\` dependencies (no \`workspaces\` key). One \`npm install\` at the project
273
+ root wires them up so \`mdk run\` can resolve them. Component directories
274
+ appear as you add components.
275
+
276
+ ## Run it
277
+
278
+ ${runSection(opts)}
279
+
280
+ ## Add components
281
+
282
+ \`\`\`bash
283
+ mdk create worker <name> # scaffolds workers/<name> and registers it in mdk.yaml
284
+ mdk create app <name> # scaffolds apps/<name> and registers it in mdk.yaml
285
+ mdk create dashboard # scaffolds apps/dashboard
286
+ \`\`\`
287
+
288
+ ${dashboard}## Runtime state
289
+
290
+ \`.mdk/\` holds the Kernel and Gateway stores, each worker's database and identity
291
+ keys, and the local discovery keys. Delete it to reset the stack — every
292
+ component then comes back with a new identity; it is never committed.
293
+ `;
294
+ writeFileSync(path, content, 'utf8');
295
+ return 'created';
296
+ }
297
+ //# sourceMappingURL=project.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"project.js","sourceRoot":"","sources":["../../src/lib/project.ts"],"names":[],"mappings":"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","sourcesContent":["import { appendFileSync, existsSync, readFileSync, writeFileSync } from 'node:fs';\nimport { basename, isAbsolute, join, relative, resolve, sep } from 'node:path';\nimport { parse as parseYaml } from 'yaml';\n\n/**\n * Project scaffolding shared by `mdk onboard` and `mdk create` — the files that\n * describe the *project* rather than any one component.\n *\n * The emitted layout is role-grouped, mirroring MDK's own component model, so a\n * path in `mdk.yaml` says what it is:\n *\n * mdk.yaml the stack spec\n * package.json private root manifest (no `workspaces` key — path-backed\n * packages link via relative `file:` deps so `npm install`\n * works inside the project directory)\n * workers/<name>/ worker plugins (mdk create worker)\n * apps/<name>/ apps (mdk create app)\n * apps/dashboard/ the UI dashboard (mdk create dashboard)\n * .mdk/ runtime state written by `mdk run` (gitignored)\n */\n\n/** Component directories, relative to the project root. */\nexport const DIRS = {\n workers: 'workers',\n apps: 'apps',\n /** Canonical dashboard location — a stable path, unlike the app's package name. */\n dashboard: join('apps', 'dashboard'),\n} as const;\n\n/**\n * Historical workspace globs (`workers/*`, `plugins/*`). Kept for reading\n * pre-existing manifests that still declare them (`isWorkspaceMember`); new\n * projects never write a `workspaces` key — local scaffolds use `file:` deps.\n */\nexport const WORKSPACES = [`${DIRS.workers}/*`, 'plugins/*'];\n\nexport type FileAction = 'created' | 'updated' | 'present';\n\n/** Coerces a stack name into a valid npm package name. */\nfunction npmName(name: string): string {\n const slug = name\n .trim()\n .toLowerCase()\n .replace(/[^a-z0-9._-]+/g, '-')\n .replace(/^[._-]+|[-._]+$/g, '');\n return slug || 'mdk-stack';\n}\n\n/**\n * Ensures the project has a private root `package.json`. Creates one if absent\n * (no `workspaces` key — local packages are linked via `file:` dependencies).\n * An existing manifest is never modified (name, deps, scripts, or workspaces).\n */\nexport function ensureProjectManifest(targetDir: string, stackName: string): FileAction {\n const path = join(targetDir, 'package.json');\n\n if (!existsSync(path)) {\n const manifest = {\n name: npmName(stackName),\n version: '0.1.0',\n private: true,\n scripts: { dev: 'mdk run' },\n };\n writeFileSync(path, `${JSON.stringify(manifest, null, 2)}\\n`, 'utf8');\n return 'created';\n }\n\n // Existing manifest is the user's — never overwrite it, and never inject workspaces.\n return 'present';\n}\n\n/** The project's declared workspace globs (empty when it is not a workspace root). */\nfunction readWorkspaces(targetDir: string): string[] {\n const path = join(targetDir, 'package.json');\n if (!existsSync(path)) return [];\n try {\n const pkg = JSON.parse(readFileSync(path, 'utf8')) as { workspaces?: unknown };\n return Array.isArray(pkg.workspaces) ? pkg.workspaces.filter((w) => typeof w === 'string') : [];\n } catch {\n return [];\n }\n}\n\n/**\n * True when `packageDir` is covered by one of the project's workspace globs —\n * i.e. a root `npm install` would actually install it. Only such components may\n * be installed from the root; anything else (the dashboard, which is\n * intentionally outside the workspaces) has to install in its own directory.\n */\nexport function isWorkspaceMember(targetDir: string, packageDir: string): boolean {\n const globs = readWorkspaces(targetDir);\n if (!globs.length) return false;\n\n const rel = relative(resolve(targetDir), resolve(packageDir));\n if (!rel || rel.startsWith('..')) return false;\n const segments = rel.split(sep);\n\n return globs.some((glob) => {\n const parts = glob.split('/');\n return (\n parts.length === segments.length && parts.every((p, i) => p === '*' || p === segments[i])\n );\n });\n}\n\n/**\n * True when the project root already declares a `file:` dependency — in\n * either `dependencies` or `devDependencies` — that points at `packageDir`, so\n * `npm install` must run at the project root to create the symlink the\n * runtime resolves from `node_modules`.\n */\nexport function isFileLinkedFromProject(targetDir: string, packageDir: string): boolean {\n const path = join(targetDir, 'package.json');\n if (!existsSync(path)) return false;\n try {\n const pkg = JSON.parse(readFileSync(path, 'utf8')) as {\n dependencies?: Record<string, string>;\n devDependencies?: Record<string, string>;\n };\n const deps = { ...pkg.devDependencies, ...pkg.dependencies };\n const absPackage = resolve(packageDir);\n return Object.values(deps).some((spec) => {\n if (!spec.startsWith('file:')) return false;\n return resolve(targetDir, spec.slice('file:'.length)) === absPackage;\n });\n } catch {\n return false;\n }\n}\n\n/** Sets one `dependencies` entry in the project root manifest, if not already set. */\nfunction setDependency(targetDir: string, packageName: string, versionSpec: string): void {\n const path = join(targetDir, 'package.json');\n if (!existsSync(path)) throw new Error('no package.json');\n\n const pkg = JSON.parse(readFileSync(path, 'utf8')) as {\n dependencies?: Record<string, string>;\n };\n if (!pkg.dependencies) pkg.dependencies = {};\n if (pkg.dependencies[packageName] === versionSpec) return;\n pkg.dependencies[packageName] = versionSpec;\n writeFileSync(path, `${JSON.stringify(pkg, null, 2)}\\n`, 'utf8');\n}\n\n/**\n * Declares a `file:`-linked dependency pointing at `absPath`, written relative\n * to `targetDir`. Works for packages inside the project folder and for bundled\n * monorepo catalog checkouts outside it — never appends to a `workspaces`\n * array, and never writes bare `*` (that hits the registry when the project is\n * not a monorepo workspace member). `npm install` resolves the relative\n * `file:` specifier against the project root and symlinks it into\n * `node_modules/<packageName>`.\n */\nexport function addFileDependency(targetDir: string, packageName: string, absPath: string): void {\n const target = resolve(absPath);\n const rel = relative(resolve(targetDir), target);\n // `path.relative()` returns an absolute path when `targetDir` and `absPath`\n // are on different drives (Windows only) — fall back to the absolute path\n // rather than writing a `file:./C:/...` spec, which npm resolves under the\n // project dir and 404s.\n const posix = (isAbsolute(rel) ? target : rel).split(sep).join('/');\n const spec = isAbsolute(rel) || posix.startsWith('.') ? posix : `./${posix}`;\n setDependency(targetDir, packageName, `file:${spec}`);\n}\n\n/**\n * The stack name from `<dir>/mdk.yaml` (falling back to the directory name), or\n * null when there is no spec — i.e. when the directory is not an MDK project and\n * nothing should be scaffolded into it.\n */\nexport function readStackName(targetDir: string): string | null {\n const specPath = join(targetDir, 'mdk.yaml');\n if (!existsSync(specPath)) return null;\n try {\n const doc = parseYaml(readFileSync(specPath, 'utf8')) as { metadata?: { name?: string } };\n const name = doc?.metadata?.name?.trim();\n if (name) return name;\n } catch {\n /* fall through to the directory name */\n }\n return basename(resolve(targetDir));\n}\n\nconst GITIGNORE_HEADER = '# MDK';\nconst GITIGNORE_BLOCK = [\n `${GITIGNORE_HEADER} runtime state — kernel/gateway stores, worker DBs, discovery keys`,\n '.mdk/',\n '',\n '# Dependencies & build output',\n 'node_modules/',\n 'dist/',\n 'coverage/',\n '.vite/',\n '*.tsbuildinfo',\n '',\n '# Local environment (commit .env.example instead)',\n '.env',\n '.env.local',\n '*.log',\n '',\n].join('\\n');\n\n/**\n * Ensures the project has a `.gitignore` covering MDK runtime state and the\n * build output of the components it scaffolds. Creates one if absent; if a\n * `.gitignore` already exists without the MDK block, appends it (never clobbers\n * user content).\n */\nexport function ensureProjectGitignore(targetDir: string): FileAction {\n const path = join(targetDir, '.gitignore');\n if (!existsSync(path)) {\n writeFileSync(path, GITIGNORE_BLOCK, 'utf8');\n return 'created';\n }\n const current = readFileSync(path, 'utf8');\n if (current.includes(GITIGNORE_HEADER) || current.includes('.mdk/')) {\n // `.env` holds secret plugin-setup answers, so it must be ignored even when\n // an older or hand-edited MDK block left it out.\n if (/^\\/?\\.env\\s*$/m.test(current)) return 'present';\n appendFileSync(path, `${current.endsWith('\\n') ? '' : '\\n'}.env\\n`, 'utf8');\n return 'updated';\n }\n appendFileSync(path, `${current.endsWith('\\n') ? '' : '\\n'}\\n${GITIGNORE_BLOCK}`, 'utf8');\n return 'updated';\n}\n\nexport interface ReadmeOptions {\n stackName: string;\n /** Worker names from the spec, for the per-component run commands. */\n workerNames: string[];\n /** Dashboard path relative to the project root, when one was scaffolded. */\n dashboardDir?: string;\n}\n\nfunction runSection({ workerNames }: ReadmeOptions): string {\n const workers = workerNames.length\n ? workerNames.map((name) => `mdk run worker ${name}`)\n : ['mdk run worker <name>'];\n return [\n '```bash',\n 'npm install # once, links workers/ and apps/',\n 'mdk run # boots Kernel, workers, apps and Gateway together',\n '```',\n '',\n 'Or run each component in its own terminal:',\n '',\n '```bash',\n 'mdk run kernel',\n 'mdk run gateway',\n ...workers,\n '```',\n ].join('\\n');\n}\n\n/**\n * Writes a project README describing the emitted layout and how to run the\n * stack. Never overwrites an existing README.\n */\nexport function ensureProjectReadme(targetDir: string, opts: ReadmeOptions): FileAction {\n const path = join(targetDir, 'README.md');\n if (existsSync(path)) return 'present';\n\n const dashboard = opts.dashboardDir\n ? [\n '## Dashboard',\n '',\n '```bash',\n `cd ${opts.dashboardDir}`,\n 'npm install',\n 'npm run dev',\n '```',\n '',\n 'It proxies `/auth`, `/api` and `/pub` to the Gateway, so start the stack first.',\n '',\n '',\n ].join('\\n')\n : '';\n\n const content = `# ${opts.stackName}\n\nAn MDK stack — a Kernel, a Gateway and its Workers — described declaratively in\n\\`mdk.yaml\\`. Edit that file to change ports, apps, workers or the\ndevices each worker manages.\n\n## Layout\n\n\\`\\`\\`\nmdk.yaml the stack spec: ports, apps, workers, devices\nworkers/<name>/ worker plugins — device contract, handlers and a mock device\napps/<name>/ apps — HTTP aggregation endpoints served through the Gateway\napps/dashboard/ the UI dashboard (Vite app)\n.mdk/ runtime state written by \\`mdk run\\` — disposable, gitignored\n\\`\\`\\`\n\nWorkers, apps and bundled monorepo catalog packages are linked with relative\n\\`file:\\` dependencies (no \\`workspaces\\` key). One \\`npm install\\` at the project\nroot wires them up so \\`mdk run\\` can resolve them. Component directories\nappear as you add components.\n\n## Run it\n\n${runSection(opts)}\n\n## Add components\n\n\\`\\`\\`bash\nmdk create worker <name> # scaffolds workers/<name> and registers it in mdk.yaml\nmdk create app <name> # scaffolds apps/<name> and registers it in mdk.yaml\nmdk create dashboard # scaffolds apps/dashboard\n\\`\\`\\`\n\n${dashboard}## Runtime state\n\n\\`.mdk/\\` holds the Kernel and Gateway stores, each worker's database and identity\nkeys, and the local discovery keys. Delete it to reset the stack — every\ncomponent then comes back with a new identity; it is never committed.\n`;\n\n writeFileSync(path, content, 'utf8');\n return 'created';\n}\n"]}
@@ -0,0 +1,188 @@
1
+ import { mkdirSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs';
2
+ import { dirname, join, resolve } from 'node:path';
3
+ /** Absolute path of a component's lock file. */
4
+ export function runLockFile(projectDir, component) {
5
+ if (component.name !== undefined && !/^[a-z0-9][a-z0-9._-]*$/.test(component.name)) {
6
+ throw new Error(`Invalid ${component.kind} name "${component.name}".`);
7
+ }
8
+ const file = component.name ? `${component.kind}-${component.name}.pid` : `${component.kind}.pid`;
9
+ return join(resolve(projectDir), '.mdk', 'run', file);
10
+ }
11
+ /**
12
+ * Where `mdk run` records the pid of a supervised app's runtime process. The run
13
+ * lock names the supervisor, which outlives a child that crashed and is waiting
14
+ * to restart; this file only exists while a child is up.
15
+ */
16
+ export function appChildFile(projectDir, name) {
17
+ return runLockFile(projectDir, { kind: 'app', name }).replace(/\.pid$/, '.child');
18
+ }
19
+ /** The pid in an app's child file: undefined without one (the app runs in-process). */
20
+ export function appChildPid(projectDir, name) {
21
+ try {
22
+ return Number(readFileSync(appChildFile(projectDir, name), 'utf8').trim());
23
+ }
24
+ catch {
25
+ return undefined;
26
+ }
27
+ }
28
+ function label(component) {
29
+ return component.name ? `${component.kind} "${component.name}"` : `The ${component.kind}`;
30
+ }
31
+ /** True when `pid` is a live process (EPERM still means it exists). */
32
+ export function isAlive(pid) {
33
+ if (!Number.isInteger(pid) || pid <= 0)
34
+ return false;
35
+ try {
36
+ process.kill(pid, 0);
37
+ return true;
38
+ }
39
+ catch (error) {
40
+ return error.code === 'EPERM';
41
+ }
42
+ }
43
+ /** The live pid holding a component's lock, or null when it is free or stale. */
44
+ export function lockHolder(projectDir, component) {
45
+ let pid;
46
+ try {
47
+ pid = Number(readFileSync(runLockFile(projectDir, component), 'utf8').trim());
48
+ }
49
+ catch {
50
+ return null;
51
+ }
52
+ return isAlive(pid) ? pid : null;
53
+ }
54
+ function capitalize(text) {
55
+ return text[0].toUpperCase() + text.slice(1);
56
+ }
57
+ /** Lock files this process owns; removed when it exits. */
58
+ const owned = new Set();
59
+ let exitHookInstalled = false;
60
+ function releaseFile(file) {
61
+ owned.delete(file);
62
+ try {
63
+ if (readFileSync(file, 'utf8').trim() === String(process.pid))
64
+ unlinkSync(file);
65
+ }
66
+ catch {
67
+ /* already gone */
68
+ }
69
+ }
70
+ function releaseAll() {
71
+ for (const file of [...owned])
72
+ releaseFile(file);
73
+ }
74
+ function alreadyRunning(component, file, holder) {
75
+ return new Error(`${capitalize(label(component))} is already running for this project (pid ${holder}).\n` +
76
+ `Stop it first, or remove ${file} if that process is not an MDK component.`);
77
+ }
78
+ const isEexist = (error) => error.code === 'EEXIST';
79
+ /** Blocks the thread for `ms` (lock acquisition is synchronous). */
80
+ function sleepSync(ms) {
81
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
82
+ }
83
+ const TAKEOVER_TRIES = 100;
84
+ const TAKEOVER_WAIT_MS = 20;
85
+ /**
86
+ * Replaces a stale lock under a `<lock>.takeover` guard, so two processes that
87
+ * both found it stale cannot both pass the check and one delete the lock the
88
+ * other just took. A guard whose process is gone is cleared.
89
+ */
90
+ function takeOver(projectDir, component, file) {
91
+ const guard = `${file}.takeover`;
92
+ for (let tries = 0;; tries++) {
93
+ try {
94
+ writeFileSync(guard, String(process.pid), { flag: 'wx' });
95
+ break;
96
+ }
97
+ catch (error) {
98
+ if (!isEexist(error))
99
+ throw error;
100
+ let guardPid = 0;
101
+ try {
102
+ guardPid = Number(readFileSync(guard, 'utf8').trim());
103
+ }
104
+ catch {
105
+ continue; // released meanwhile
106
+ }
107
+ if (!isAlive(guardPid)) {
108
+ try {
109
+ unlinkSync(guard);
110
+ }
111
+ catch {
112
+ /* cleared by another process */
113
+ }
114
+ continue;
115
+ }
116
+ if (tries >= TAKEOVER_TRIES) {
117
+ throw new Error(`${capitalize(label(component))} is being started by another process (pid ${guardPid}).`);
118
+ }
119
+ sleepSync(TAKEOVER_WAIT_MS);
120
+ }
121
+ }
122
+ try {
123
+ const holder = lockHolder(projectDir, component);
124
+ if (holder !== null && holder !== process.pid)
125
+ throw alreadyRunning(component, file, holder);
126
+ try {
127
+ unlinkSync(file);
128
+ }
129
+ catch {
130
+ /* already gone */
131
+ }
132
+ try {
133
+ writeFileSync(file, String(process.pid), { flag: 'wx' });
134
+ }
135
+ catch (error) {
136
+ // A process that found no lock at all took it first.
137
+ if (!isEexist(error))
138
+ throw error;
139
+ throw alreadyRunning(component, file, lockHolder(projectDir, component) ?? 0);
140
+ }
141
+ }
142
+ finally {
143
+ try {
144
+ unlinkSync(guard);
145
+ }
146
+ catch {
147
+ /* already gone */
148
+ }
149
+ }
150
+ }
151
+ /**
152
+ * Takes the run locks for every component this process is about to start, or
153
+ * throws naming the one already running. All or nothing: on a conflict the
154
+ * locks taken so far are released. The locks are dropped when the process exits.
155
+ */
156
+ export function acquireRunLocks(projectDir, components) {
157
+ const taken = [];
158
+ const release = () => {
159
+ for (const file of taken.splice(0))
160
+ releaseFile(file);
161
+ };
162
+ try {
163
+ for (const component of components) {
164
+ const file = runLockFile(projectDir, component);
165
+ mkdirSync(dirname(file), { recursive: true });
166
+ try {
167
+ writeFileSync(file, String(process.pid), { flag: 'wx' });
168
+ }
169
+ catch (error) {
170
+ if (!isEexist(error))
171
+ throw error;
172
+ takeOver(projectDir, component, file);
173
+ }
174
+ taken.push(file);
175
+ owned.add(file);
176
+ }
177
+ }
178
+ catch (error) {
179
+ release();
180
+ throw error;
181
+ }
182
+ if (!exitHookInstalled) {
183
+ exitHookInstalled = true;
184
+ process.once('exit', releaseAll);
185
+ }
186
+ return { release };
187
+ }
188
+ //# sourceMappingURL=run-lock.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"run-lock.js","sourceRoot":"","sources":["../../src/lib/run-lock.ts"],"names":[],"mappings":"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","sourcesContent":["import { mkdirSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs';\nimport { dirname, join, resolve } from 'node:path';\n\n/**\n * One running copy per component and project. Two copies of a worker or app share\n * its identity keys and store, so the second one takes over the first one's DHT\n * key and the first goes unreachable once either stops; two Kernels fight over\n * the store lock. A `.mdk/run/<component>.pid` file names the process that owns\n * the component; a stale file (its process gone) is taken over.\n */\n\n/** A component that holds a run lock: `kernel`, `worker <name>` or `app <name>`. */\nexport interface RunComponent {\n kind: 'kernel' | 'worker' | 'app';\n name?: string;\n}\n\n/** Absolute path of a component's lock file. */\nexport function runLockFile(projectDir: string, component: RunComponent): string {\n if (component.name !== undefined && !/^[a-z0-9][a-z0-9._-]*$/.test(component.name)) {\n throw new Error(`Invalid ${component.kind} name \"${component.name}\".`);\n }\n const file = component.name ? `${component.kind}-${component.name}.pid` : `${component.kind}.pid`;\n return join(resolve(projectDir), '.mdk', 'run', file);\n}\n\n/**\n * Where `mdk run` records the pid of a supervised app's runtime process. The run\n * lock names the supervisor, which outlives a child that crashed and is waiting\n * to restart; this file only exists while a child is up.\n */\nexport function appChildFile(projectDir: string, name: string): string {\n return runLockFile(projectDir, { kind: 'app', name }).replace(/\\.pid$/, '.child');\n}\n\n/** The pid in an app's child file: undefined without one (the app runs in-process). */\nexport function appChildPid(projectDir: string, name: string): number | undefined {\n try {\n return Number(readFileSync(appChildFile(projectDir, name), 'utf8').trim());\n } catch {\n return undefined;\n }\n}\n\nfunction label(component: RunComponent): string {\n return component.name ? `${component.kind} \"${component.name}\"` : `The ${component.kind}`;\n}\n\n/** True when `pid` is a live process (EPERM still means it exists). */\nexport function isAlive(pid: number): boolean {\n if (!Number.isInteger(pid) || pid <= 0) return false;\n try {\n process.kill(pid, 0);\n return true;\n } catch (error) {\n return (error as NodeJS.ErrnoException).code === 'EPERM';\n }\n}\n\n/** The live pid holding a component's lock, or null when it is free or stale. */\nexport function lockHolder(projectDir: string, component: RunComponent): number | null {\n let pid: number;\n try {\n pid = Number(readFileSync(runLockFile(projectDir, component), 'utf8').trim());\n } catch {\n return null;\n }\n return isAlive(pid) ? pid : null;\n}\n\nfunction capitalize(text: string): string {\n return text[0]!.toUpperCase() + text.slice(1);\n}\n\n/** Lock files this process owns; removed when it exits. */\nconst owned = new Set<string>();\nlet exitHookInstalled = false;\n\nfunction releaseFile(file: string): void {\n owned.delete(file);\n try {\n if (readFileSync(file, 'utf8').trim() === String(process.pid)) unlinkSync(file);\n } catch {\n /* already gone */\n }\n}\n\nfunction releaseAll(): void {\n for (const file of [...owned]) releaseFile(file);\n}\n\nfunction alreadyRunning(component: RunComponent, file: string, holder: number): Error {\n return new Error(\n `${capitalize(label(component))} is already running for this project (pid ${holder}).\\n` +\n `Stop it first, or remove ${file} if that process is not an MDK component.`,\n );\n}\n\nconst isEexist = (error: unknown): boolean => (error as NodeJS.ErrnoException).code === 'EEXIST';\n\n/** Blocks the thread for `ms` (lock acquisition is synchronous). */\nfunction sleepSync(ms: number): void {\n Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);\n}\n\nconst TAKEOVER_TRIES = 100;\nconst TAKEOVER_WAIT_MS = 20;\n\n/**\n * Replaces a stale lock under a `<lock>.takeover` guard, so two processes that\n * both found it stale cannot both pass the check and one delete the lock the\n * other just took. A guard whose process is gone is cleared.\n */\nfunction takeOver(projectDir: string, component: RunComponent, file: string): void {\n const guard = `${file}.takeover`;\n for (let tries = 0; ; tries++) {\n try {\n writeFileSync(guard, String(process.pid), { flag: 'wx' });\n break;\n } catch (error) {\n if (!isEexist(error)) throw error;\n let guardPid = 0;\n try {\n guardPid = Number(readFileSync(guard, 'utf8').trim());\n } catch {\n continue; // released meanwhile\n }\n if (!isAlive(guardPid)) {\n try {\n unlinkSync(guard);\n } catch {\n /* cleared by another process */\n }\n continue;\n }\n if (tries >= TAKEOVER_TRIES) {\n throw new Error(`${capitalize(label(component))} is being started by another process (pid ${guardPid}).`);\n }\n sleepSync(TAKEOVER_WAIT_MS);\n }\n }\n\n try {\n const holder = lockHolder(projectDir, component);\n if (holder !== null && holder !== process.pid) throw alreadyRunning(component, file, holder);\n try {\n unlinkSync(file);\n } catch {\n /* already gone */\n }\n try {\n writeFileSync(file, String(process.pid), { flag: 'wx' });\n } catch (error) {\n // A process that found no lock at all took it first.\n if (!isEexist(error)) throw error;\n throw alreadyRunning(component, file, lockHolder(projectDir, component) ?? 0);\n }\n } finally {\n try {\n unlinkSync(guard);\n } catch {\n /* already gone */\n }\n }\n}\n\n/**\n * Takes the run locks for every component this process is about to start, or\n * throws naming the one already running. All or nothing: on a conflict the\n * locks taken so far are released. The locks are dropped when the process exits.\n */\nexport function acquireRunLocks(projectDir: string, components: RunComponent[]): { release(): void } {\n const taken: string[] = [];\n const release = (): void => {\n for (const file of taken.splice(0)) releaseFile(file);\n };\n\n try {\n for (const component of components) {\n const file = runLockFile(projectDir, component);\n mkdirSync(dirname(file), { recursive: true });\n try {\n writeFileSync(file, String(process.pid), { flag: 'wx' });\n } catch (error) {\n if (!isEexist(error)) throw error;\n takeOver(projectDir, component, file);\n }\n taken.push(file);\n owned.add(file);\n }\n } catch (error) {\n release();\n throw error;\n }\n\n if (!exitHookInstalled) {\n exitHookInstalled = true;\n process.once('exit', releaseAll);\n }\n return { release };\n}\n"]}