@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.
Files changed (112) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +447 -0
  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 +59 -1
  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,178 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import { copyFileSync, existsSync, mkdtempSync, readdirSync, readFileSync, rmSync, statSync } from 'node:fs';
3
+ import { tmpdir } from 'node:os';
4
+ import { dirname, join, relative, sep } from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+ const __dirname = dirname(fileURLToPath(import.meta.url));
7
+ /**
8
+ * The bare imports a dashboard lends to the app UIs it loads (import map), so
9
+ * the only ones an app's built `ui/` may leave unresolved. A copy of
10
+ * `MDK_APP_SHARED_MODULES` in `@tetherto/mdk-ui-foundation` — the CLI does not
11
+ * depend on the UI packages; a test fails if the two drift.
12
+ */
13
+ export const APP_UI_SHARED_MODULES = [
14
+ 'react',
15
+ 'react/jsx-runtime',
16
+ 'react-dom',
17
+ 'react-router',
18
+ '@tanstack/react-query',
19
+ '@tetherto/mdk-ui-foundation',
20
+ '@tetherto/mdk-react-adapter',
21
+ '@tetherto/mdk-react-devkit',
22
+ ];
23
+ const SHARED = new Set(APP_UI_SHARED_MODULES);
24
+ // Static `import … from '…'`, `import '…'` and `export … from '…'`, at the
25
+ // start of a statement, and dynamic `import('…')`. Built ES modules (what
26
+ // `ui/` holds) write them this plainly; syntax is checked by Node separately.
27
+ const STATIC_IMPORT = /(?:^|[;\n}])\s*(import|export)\s*(?:([\w$\s{},*]*?)\s*from\s*)?(['"])([^'"\n]+)\3/g;
28
+ const DYNAMIC_IMPORT = /\bimport\s*\(\s*(['"])([^'"\n]+)\1\s*\)/g;
29
+ // React 19 keeps its internals under this key in every build of `react` and
30
+ // `react-dom`: finding it in ui/ means a copy of React was bundled in.
31
+ const REACT_INTERNALS = '__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE';
32
+ // Edits within this window of the build are taken as part of it: a fresh clone
33
+ // or copy writes ui/ and ui-src/ moments apart, in any order.
34
+ const STALE_TOLERANCE_MS = 2_000;
35
+ // How long the contract check may run the app's top-level code before giving up.
36
+ const CONTRACT_TIMEOUT_MS = 15_000;
37
+ const isBare = (specifier) => !specifier.startsWith('.') && !specifier.startsWith('/') && !/^[a-z][a-z0-9+.-]*:/i.test(specifier);
38
+ /** The names a statement's clause imports: `{ a, b as c }` → a, b; `x` → default. */
39
+ function importedNames(kind, clause) {
40
+ if (!clause)
41
+ return [];
42
+ const names = [];
43
+ const braces = clause.match(/\{([^}]*)\}/);
44
+ if (braces) {
45
+ for (const part of braces[1].split(',')) {
46
+ const name = part.trim().split(/\s+as\s+/)[0].trim();
47
+ if (name)
48
+ names.push(name);
49
+ }
50
+ }
51
+ // `import x from`, `import x, { … } from` — a default import; `* as ns` is not.
52
+ const head = clause.replace(/\{[^}]*\}/, '').replace(/,/g, ' ').trim();
53
+ if (kind === 'import' && head && !head.startsWith('*'))
54
+ names.push('default');
55
+ return names;
56
+ }
57
+ /** Every bare import in `source`, with the names imported from each. */
58
+ export function scanImports(source) {
59
+ const found = new Map();
60
+ const add = (specifier, names) => {
61
+ if (!isBare(specifier))
62
+ return;
63
+ found.set(specifier, [...(found.get(specifier) ?? []), ...names]);
64
+ };
65
+ for (const m of source.matchAll(STATIC_IMPORT))
66
+ add(m[4], importedNames(m[1], m[2]));
67
+ for (const m of source.matchAll(DYNAMIC_IMPORT))
68
+ add(m[2], []);
69
+ return found;
70
+ }
71
+ /** Files under `dir`, skipping `node_modules`. */
72
+ function listFiles(dir) {
73
+ return readdirSync(dir, { withFileTypes: true }).flatMap((entry) => {
74
+ if (entry.name === 'node_modules')
75
+ return [];
76
+ const full = join(dir, entry.name);
77
+ if (entry.isDirectory())
78
+ return listFiles(full);
79
+ return entry.isFile() ? [full] : [];
80
+ });
81
+ }
82
+ /** Node's own parser, without running the code: `node --check` on a `.mjs` copy. */
83
+ function syntaxError(file, label) {
84
+ const work = mkdtempSync(join(tmpdir(), 'mdk-ui-check-'));
85
+ try {
86
+ const copy = join(work, 'module.mjs');
87
+ copyFileSync(file, copy);
88
+ const res = spawnSync(process.execPath, ['--check', copy], { encoding: 'utf8' });
89
+ if (res.status === 0)
90
+ return undefined;
91
+ const detail = (res.stderr || '').split('\n').find((line) => /Error/.test(line))?.trim();
92
+ return `${label} is not a valid JavaScript module${detail ? `: ${detail}` : ''}`;
93
+ }
94
+ finally {
95
+ rmSync(work, { recursive: true, force: true });
96
+ }
97
+ }
98
+ function runnerPath() {
99
+ const js = join(__dirname, 'app-ui-runner.js');
100
+ return existsSync(js) ? js : join(__dirname, 'app-ui-runner.ts');
101
+ }
102
+ /**
103
+ * Checks an app's built UI (`<dir>/ui/`) the way the dashboard will use it:
104
+ *
105
+ * - every `.js` file parses as an ES module;
106
+ * - it does not bundle its own React;
107
+ * - its bare imports are only the modules the dashboard provides
108
+ * ({@link APP_UI_SHARED_MODULES}) — anything else fails to resolve in the
109
+ * browser, and a missing `react` import usually means React was bundled in;
110
+ * - with `runContract`, `ui/index.js` is loaded in a child Node process with
111
+ * those imports stubbed, and its default export checked against the app UI
112
+ * contract (id matching `mdk-app.yaml`, displayName, routes, panels). This
113
+ * runs the app's top-level code, so it is for the author's own package
114
+ * (`mdk validate app`), not for installing someone else's;
115
+ * - `ui-src/` edited after the last build is a warning.
116
+ */
117
+ export function checkAppUi(dir, opts = {}) {
118
+ const errors = [];
119
+ const warnings = [];
120
+ const uiDir = join(dir, 'ui');
121
+ const entry = join(uiDir, 'index.js');
122
+ if (!existsSync(entry))
123
+ return { errors: ['ui/index.js: not found'], warnings };
124
+ const imports = new Map();
125
+ for (const file of listFiles(uiDir).filter((f) => f.endsWith('.js') || f.endsWith('.mjs'))) {
126
+ const label = relative(dir, file).split(sep).join('/');
127
+ const bad = syntaxError(file, label);
128
+ if (bad) {
129
+ errors.push(bad);
130
+ continue;
131
+ }
132
+ const source = readFileSync(file, 'utf8');
133
+ if (source.includes(REACT_INTERNALS)) {
134
+ errors.push(`${label} bundles its own copy of React — leave the shared modules external in the UI build ` +
135
+ '(rollupOptions.external: MDK_APP_SHARED_MODULES), or hooks break inside the dashboard');
136
+ }
137
+ for (const [specifier, names] of scanImports(source)) {
138
+ if (!SHARED.has(specifier)) {
139
+ errors.push(`${label} imports "${specifier}", which the dashboard does not provide — bundle it, ` +
140
+ `or import only: ${APP_UI_SHARED_MODULES.join(', ')}`);
141
+ }
142
+ imports.set(specifier, [...(imports.get(specifier) ?? []), ...names]);
143
+ }
144
+ }
145
+ // A ui-src/ newer than the build means the published UI would be stale.
146
+ const src = join(dir, 'ui-src');
147
+ if (existsSync(src)) {
148
+ const built = statSync(entry).mtimeMs;
149
+ const newer = listFiles(src).find((file) => statSync(file).mtimeMs > built + STALE_TOLERANCE_MS);
150
+ if (newer) {
151
+ warnings.push(`${relative(dir, newer).split(sep).join('/')} changed after ui/ was built — run \`npm run build:ui\``);
152
+ }
153
+ }
154
+ if (opts.runContract && opts.id && !errors.length) {
155
+ const res = spawnSync(process.execPath, [runnerPath()], {
156
+ encoding: 'utf8',
157
+ timeout: CONTRACT_TIMEOUT_MS,
158
+ env: {
159
+ ...process.env,
160
+ MDK_UI_ENTRY: entry,
161
+ MDK_UI_ID: opts.id,
162
+ MDK_UI_IMPORTS: JSON.stringify(Object.fromEntries(imports)),
163
+ },
164
+ });
165
+ const line = (res.stdout || '').trim().split('\n').at(-1);
166
+ try {
167
+ const result = JSON.parse(line ?? '');
168
+ errors.push(...result.errors);
169
+ warnings.push(...result.warnings);
170
+ }
171
+ catch {
172
+ const reason = res.error?.message ?? ((res.stderr || '').trim().split('\n')[0] || `exit code ${res.status}`);
173
+ warnings.push(`could not check the UI contract (${reason}); the dashboard checks it when it loads the app`);
174
+ }
175
+ }
176
+ return { errors, warnings };
177
+ }
178
+ //# sourceMappingURL=app-ui-check.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"app-ui-check.js","sourceRoot":"","sources":["../../src/lib/app-ui-check.ts"],"names":[],"mappings":"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","sourcesContent":["import { spawnSync } from 'node:child_process';\nimport { copyFileSync, existsSync, mkdtempSync, readdirSync, readFileSync, rmSync, statSync } from 'node:fs';\nimport { tmpdir } from 'node:os';\nimport { dirname, join, relative, sep } from 'node:path';\nimport { fileURLToPath } from 'node:url';\n\nconst __dirname = dirname(fileURLToPath(import.meta.url));\n\n/**\n * The bare imports a dashboard lends to the app UIs it loads (import map), so\n * the only ones an app's built `ui/` may leave unresolved. A copy of\n * `MDK_APP_SHARED_MODULES` in `@tetherto/mdk-ui-foundation` — the CLI does not\n * depend on the UI packages; a test fails if the two drift.\n */\nexport const APP_UI_SHARED_MODULES = [\n 'react',\n 'react/jsx-runtime',\n 'react-dom',\n 'react-router',\n '@tanstack/react-query',\n '@tetherto/mdk-ui-foundation',\n '@tetherto/mdk-react-adapter',\n '@tetherto/mdk-react-devkit',\n] as const;\n\nconst SHARED = new Set<string>(APP_UI_SHARED_MODULES);\n\nexport interface AppUiCheck {\n errors: string[];\n warnings: string[];\n}\n\n// Static `import … from '…'`, `import '…'` and `export … from '…'`, at the\n// start of a statement, and dynamic `import('…')`. Built ES modules (what\n// `ui/` holds) write them this plainly; syntax is checked by Node separately.\nconst STATIC_IMPORT = /(?:^|[;\\n}])\\s*(import|export)\\s*(?:([\\w$\\s{},*]*?)\\s*from\\s*)?(['\"])([^'\"\\n]+)\\3/g;\nconst DYNAMIC_IMPORT = /\\bimport\\s*\\(\\s*(['\"])([^'\"\\n]+)\\1\\s*\\)/g;\n\n// React 19 keeps its internals under this key in every build of `react` and\n// `react-dom`: finding it in ui/ means a copy of React was bundled in.\nconst REACT_INTERNALS = '__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE';\n\n// Edits within this window of the build are taken as part of it: a fresh clone\n// or copy writes ui/ and ui-src/ moments apart, in any order.\nconst STALE_TOLERANCE_MS = 2_000;\n\n// How long the contract check may run the app's top-level code before giving up.\nconst CONTRACT_TIMEOUT_MS = 15_000;\n\nconst isBare = (specifier: string): boolean =>\n !specifier.startsWith('.') && !specifier.startsWith('/') && !/^[a-z][a-z0-9+.-]*:/i.test(specifier);\n\n/** The names a statement's clause imports: `{ a, b as c }` → a, b; `x` → default. */\nfunction importedNames(kind: string, clause: string | undefined): string[] {\n if (!clause) return [];\n const names: string[] = [];\n const braces = clause.match(/\\{([^}]*)\\}/);\n if (braces) {\n for (const part of braces[1]!.split(',')) {\n const name = part.trim().split(/\\s+as\\s+/)[0]!.trim();\n if (name) names.push(name);\n }\n }\n // `import x from`, `import x, { … } from` — a default import; `* as ns` is not.\n const head = clause.replace(/\\{[^}]*\\}/, '').replace(/,/g, ' ').trim();\n if (kind === 'import' && head && !head.startsWith('*')) names.push('default');\n return names;\n}\n\n/** Every bare import in `source`, with the names imported from each. */\nexport function scanImports(source: string): Map<string, string[]> {\n const found = new Map<string, string[]>();\n const add = (specifier: string, names: string[]): void => {\n if (!isBare(specifier)) return;\n found.set(specifier, [...(found.get(specifier) ?? []), ...names]);\n };\n for (const m of source.matchAll(STATIC_IMPORT)) add(m[4]!, importedNames(m[1]!, m[2]));\n for (const m of source.matchAll(DYNAMIC_IMPORT)) add(m[2]!, []);\n return found;\n}\n\n/** Files under `dir`, skipping `node_modules`. */\nfunction listFiles(dir: string): string[] {\n return readdirSync(dir, { withFileTypes: true }).flatMap((entry) => {\n if (entry.name === 'node_modules') return [];\n const full = join(dir, entry.name);\n if (entry.isDirectory()) return listFiles(full);\n return entry.isFile() ? [full] : [];\n });\n}\n\n/** Node's own parser, without running the code: `node --check` on a `.mjs` copy. */\nfunction syntaxError(file: string, label: string): string | undefined {\n const work = mkdtempSync(join(tmpdir(), 'mdk-ui-check-'));\n try {\n const copy = join(work, 'module.mjs');\n copyFileSync(file, copy);\n const res = spawnSync(process.execPath, ['--check', copy], { encoding: 'utf8' });\n if (res.status === 0) return undefined;\n const detail = (res.stderr || '').split('\\n').find((line) => /Error/.test(line))?.trim();\n return `${label} is not a valid JavaScript module${detail ? `: ${detail}` : ''}`;\n } finally {\n rmSync(work, { recursive: true, force: true });\n }\n}\n\nfunction runnerPath(): string {\n const js = join(__dirname, 'app-ui-runner.js');\n return existsSync(js) ? js : join(__dirname, 'app-ui-runner.ts');\n}\n\n/**\n * Checks an app's built UI (`<dir>/ui/`) the way the dashboard will use it:\n *\n * - every `.js` file parses as an ES module;\n * - it does not bundle its own React;\n * - its bare imports are only the modules the dashboard provides\n * ({@link APP_UI_SHARED_MODULES}) — anything else fails to resolve in the\n * browser, and a missing `react` import usually means React was bundled in;\n * - with `runContract`, `ui/index.js` is loaded in a child Node process with\n * those imports stubbed, and its default export checked against the app UI\n * contract (id matching `mdk-app.yaml`, displayName, routes, panels). This\n * runs the app's top-level code, so it is for the author's own package\n * (`mdk validate app`), not for installing someone else's;\n * - `ui-src/` edited after the last build is a warning.\n */\nexport function checkAppUi(dir: string, opts: { id?: string; runContract?: boolean } = {}): AppUiCheck {\n const errors: string[] = [];\n const warnings: string[] = [];\n const uiDir = join(dir, 'ui');\n const entry = join(uiDir, 'index.js');\n if (!existsSync(entry)) return { errors: ['ui/index.js: not found'], warnings };\n\n const imports = new Map<string, string[]>();\n for (const file of listFiles(uiDir).filter((f) => f.endsWith('.js') || f.endsWith('.mjs'))) {\n const label = relative(dir, file).split(sep).join('/');\n const bad = syntaxError(file, label);\n if (bad) {\n errors.push(bad);\n continue;\n }\n const source = readFileSync(file, 'utf8');\n if (source.includes(REACT_INTERNALS)) {\n errors.push(\n `${label} bundles its own copy of React — leave the shared modules external in the UI build ` +\n '(rollupOptions.external: MDK_APP_SHARED_MODULES), or hooks break inside the dashboard',\n );\n }\n for (const [specifier, names] of scanImports(source)) {\n if (!SHARED.has(specifier)) {\n errors.push(\n `${label} imports \"${specifier}\", which the dashboard does not provide — bundle it, ` +\n `or import only: ${APP_UI_SHARED_MODULES.join(', ')}`,\n );\n }\n imports.set(specifier, [...(imports.get(specifier) ?? []), ...names]);\n }\n }\n\n // A ui-src/ newer than the build means the published UI would be stale.\n const src = join(dir, 'ui-src');\n if (existsSync(src)) {\n const built = statSync(entry).mtimeMs;\n const newer = listFiles(src).find((file) => statSync(file).mtimeMs > built + STALE_TOLERANCE_MS);\n if (newer) {\n warnings.push(`${relative(dir, newer).split(sep).join('/')} changed after ui/ was built — run \\`npm run build:ui\\``);\n }\n }\n\n if (opts.runContract && opts.id && !errors.length) {\n const res = spawnSync(process.execPath, [runnerPath()], {\n encoding: 'utf8',\n timeout: CONTRACT_TIMEOUT_MS,\n env: {\n ...process.env,\n MDK_UI_ENTRY: entry,\n MDK_UI_ID: opts.id,\n MDK_UI_IMPORTS: JSON.stringify(Object.fromEntries(imports)),\n },\n });\n const line = (res.stdout || '').trim().split('\\n').at(-1);\n try {\n const result = JSON.parse(line ?? '') as AppUiCheck;\n errors.push(...result.errors);\n warnings.push(...result.warnings);\n } catch {\n const reason = res.error?.message ?? ((res.stderr || '').trim().split('\\n')[0] || `exit code ${res.status}`);\n warnings.push(`could not check the UI contract (${reason}); the dashboard checks it when it loads the app`);\n }\n }\n\n return { errors, warnings };\n}\n"]}
@@ -0,0 +1,122 @@
1
+ /**
2
+ * Child process for `checkAppUi`'s contract check: imports an app's built
3
+ * `ui/index.js` with every bare import replaced by a stub, then checks its
4
+ * default export against the app UI contract the dashboard enforces
5
+ * (`checkAppUiModule` in @tetherto/mdk-react-adapter).
6
+ *
7
+ * Runs in its own process so the app's top-level code never touches the CLI's
8
+ * module graph. It has no imports of its own beyond `node:`, so it also runs
9
+ * as `.ts` (tests) under Node's type stripping.
10
+ *
11
+ * Input (env): MDK_UI_ENTRY (absolute path), MDK_UI_ID (expected id),
12
+ * MDK_UI_IMPORTS (JSON: specifier → the names imported from it).
13
+ * Output (stdout, one JSON line): { errors: string[], warnings: string[] }.
14
+ */
15
+ import { registerHooks } from 'node:module';
16
+ import { pathToFileURL } from 'node:url';
17
+ const STUB_PREFIX = 'mdk-ui-stub:';
18
+ // Exports whose real value an app's default export is built from, so a stub
19
+ // would fail the contract check: kept as they are in @tetherto/mdk-react-adapter
20
+ // (a test runs the same modules through both checkers).
21
+ const REAL_EXPORTS = {
22
+ defineMdkApp: 'export const defineMdkApp = (ui) => ui;',
23
+ MDK_APP_PANEL_SLOT: "export const MDK_APP_PANEL_SLOT = Object.freeze({ HOME: 'home' });",
24
+ };
25
+ const IDENTIFIER = /^[A-Za-z_$][\w$]*$/;
26
+ const entry = process.env.MDK_UI_ENTRY ?? '';
27
+ const expectedId = process.env.MDK_UI_ID ?? '';
28
+ const imports = JSON.parse(process.env.MDK_UI_IMPORTS ?? '{}');
29
+ const isBare = (specifier) => !specifier.startsWith('.') && !specifier.startsWith('/') && !/^[a-z][a-z0-9+.-]*:/i.test(specifier);
30
+ // A value that survives anything top-level code does with an import it got from
31
+ // the dashboard: call it, construct it, read any property of it.
32
+ const stubSource = (specifier) => {
33
+ const names = imports[specifier] ?? [];
34
+ const lines = [
35
+ 'const stub = new Proxy(function () {}, {',
36
+ ' get: (_t, key) => (key === Symbol.toPrimitive ? () => "" : stub),',
37
+ ' apply: () => stub,',
38
+ ' construct: () => stub,',
39
+ '});',
40
+ 'export default stub;',
41
+ ];
42
+ for (const name of new Set(names)) {
43
+ if (name === 'default' || !IDENTIFIER.test(name))
44
+ continue;
45
+ lines.push(REAL_EXPORTS[name] ?? `export const ${name} = stub;`);
46
+ }
47
+ return lines.join('\n');
48
+ };
49
+ registerHooks({
50
+ resolve(specifier, context, nextResolve) {
51
+ if (isBare(specifier))
52
+ return { url: `${STUB_PREFIX}${specifier}`, shortCircuit: true };
53
+ return nextResolve(specifier, context);
54
+ },
55
+ load(url, context, nextLoad) {
56
+ if (url.startsWith(STUB_PREFIX)) {
57
+ return { format: 'module', source: stubSource(url.slice(STUB_PREFIX.length)), shortCircuit: true };
58
+ }
59
+ return nextLoad(url, context);
60
+ },
61
+ });
62
+ const isRecord = (value) => value !== null && typeof value === 'object';
63
+ const isComponent = (value) => typeof value === 'function' || (isRecord(value) && '$$typeof' in value);
64
+ const errors = [];
65
+ const warnings = [];
66
+ function checkContract(ui) {
67
+ if (!isRecord(ui)) {
68
+ errors.push('ui/index.js must default-export the app UI object (defineMdkApp({ ... }))');
69
+ return;
70
+ }
71
+ if (ui.id !== expectedId) {
72
+ errors.push(`ui/index.js declares id "${String(ui.id)}", but mdk-app.yaml says "${expectedId}" — the dashboard refuses a mismatch`);
73
+ }
74
+ if (typeof ui.displayName !== 'string' || !ui.displayName)
75
+ errors.push('ui/index.js must export a non-empty `displayName`');
76
+ if (!Array.isArray(ui.routes))
77
+ errors.push('ui/index.js must export `routes` as an array');
78
+ else {
79
+ ui.routes.forEach((route, i) => {
80
+ if (!isRecord(route) || typeof route.path !== 'string')
81
+ errors.push(`ui/index.js routes[${i}] needs a string \`path\``);
82
+ else if (!isComponent(route.component))
83
+ errors.push(`ui/index.js routes[${i}] ("${route.path}") needs a React \`component\``);
84
+ });
85
+ }
86
+ // Valid, but invisible: no sidebar entry and nothing on Home — usually the
87
+ // placeholder `mdk create app` writes before the UI is built.
88
+ if (Array.isArray(ui.routes) && Array.isArray(ui.panels) && !ui.routes.length && !ui.panels.length) {
89
+ warnings.push('ui/index.js exports no pages and no panels, so the app will not show in the dashboard — build the UI with `npm run build:ui`');
90
+ }
91
+ if (!Array.isArray(ui.panels))
92
+ errors.push('ui/index.js must export `panels` as an array');
93
+ else {
94
+ const ids = new Set();
95
+ ui.panels.forEach((panel, i) => {
96
+ if (!isRecord(panel) || typeof panel.id !== 'string' || !panel.id) {
97
+ errors.push(`ui/index.js panels[${i}] needs a string \`id\``);
98
+ return;
99
+ }
100
+ if (ids.has(panel.id))
101
+ errors.push(`ui/index.js has two panels with id "${panel.id}"`);
102
+ ids.add(panel.id);
103
+ if (typeof panel.slot !== 'string' || !panel.slot)
104
+ errors.push(`ui/index.js panel "${panel.id}" needs a string \`slot\``);
105
+ if (!isComponent(panel.component))
106
+ errors.push(`ui/index.js panel "${panel.id}" needs a React \`component\``);
107
+ });
108
+ }
109
+ }
110
+ try {
111
+ const mod = (await import(pathToFileURL(entry).href));
112
+ checkContract(mod.default);
113
+ }
114
+ catch (error) {
115
+ const message = error instanceof Error ? error.message : String(error);
116
+ // Syntax is checked before this runs; anything thrown here is the app's
117
+ // top-level code meeting a stub or a browser-only global. The dashboard still
118
+ // checks the contract when it loads the app, so this is not fatal.
119
+ warnings.push(`could not check the UI contract in Node (${message.split('\n')[0]}); the dashboard checks it when it loads the app`);
120
+ }
121
+ process.stdout.write(`${JSON.stringify({ errors, warnings })}\n`);
122
+ //# sourceMappingURL=app-ui-runner.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"app-ui-runner.js","sourceRoot":"","sources":["../../src/lib/app-ui-runner.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Child process for `checkAppUi`'s contract check: imports an app's built\n * `ui/index.js` with every bare import replaced by a stub, then checks its\n * default export against the app UI contract the dashboard enforces\n * (`checkAppUiModule` in @tetherto/mdk-react-adapter).\n *\n * Runs in its own process so the app's top-level code never touches the CLI's\n * module graph. It has no imports of its own beyond `node:`, so it also runs\n * as `.ts` (tests) under Node's type stripping.\n *\n * Input (env): MDK_UI_ENTRY (absolute path), MDK_UI_ID (expected id),\n * MDK_UI_IMPORTS (JSON: specifier → the names imported from it).\n * Output (stdout, one JSON line): { errors: string[], warnings: string[] }.\n */\nimport { registerHooks } from 'node:module';\nimport { pathToFileURL } from 'node:url';\n\nconst STUB_PREFIX = 'mdk-ui-stub:';\n\n// Exports whose real value an app's default export is built from, so a stub\n// would fail the contract check: kept as they are in @tetherto/mdk-react-adapter\n// (a test runs the same modules through both checkers).\nconst REAL_EXPORTS: Record<string, string> = {\n defineMdkApp: 'export const defineMdkApp = (ui) => ui;',\n MDK_APP_PANEL_SLOT: \"export const MDK_APP_PANEL_SLOT = Object.freeze({ HOME: 'home' });\",\n};\nconst IDENTIFIER = /^[A-Za-z_$][\\w$]*$/;\n\nconst entry = process.env.MDK_UI_ENTRY ?? '';\nconst expectedId = process.env.MDK_UI_ID ?? '';\nconst imports = JSON.parse(process.env.MDK_UI_IMPORTS ?? '{}') as Record<string, string[]>;\n\nconst isBare = (specifier: string): boolean =>\n !specifier.startsWith('.') && !specifier.startsWith('/') && !/^[a-z][a-z0-9+.-]*:/i.test(specifier);\n\n// A value that survives anything top-level code does with an import it got from\n// the dashboard: call it, construct it, read any property of it.\nconst stubSource = (specifier: string): string => {\n const names = imports[specifier] ?? [];\n const lines = [\n 'const stub = new Proxy(function () {}, {',\n ' get: (_t, key) => (key === Symbol.toPrimitive ? () => \"\" : stub),',\n ' apply: () => stub,',\n ' construct: () => stub,',\n '});',\n 'export default stub;',\n ];\n for (const name of new Set(names)) {\n if (name === 'default' || !IDENTIFIER.test(name)) continue;\n lines.push(REAL_EXPORTS[name] ?? `export const ${name} = stub;`);\n }\n return lines.join('\\n');\n};\n\nregisterHooks({\n resolve(specifier, context, nextResolve) {\n if (isBare(specifier)) return { url: `${STUB_PREFIX}${specifier}`, shortCircuit: true };\n return nextResolve(specifier, context);\n },\n load(url, context, nextLoad) {\n if (url.startsWith(STUB_PREFIX)) {\n return { format: 'module', source: stubSource(url.slice(STUB_PREFIX.length)), shortCircuit: true };\n }\n return nextLoad(url, context);\n },\n});\n\nconst isRecord = (value: unknown): value is Record<string, unknown> => value !== null && typeof value === 'object';\nconst isComponent = (value: unknown): boolean => typeof value === 'function' || (isRecord(value) && '$$typeof' in value);\n\nconst errors: string[] = [];\nconst warnings: string[] = [];\n\nfunction checkContract(ui: unknown): void {\n if (!isRecord(ui)) {\n errors.push('ui/index.js must default-export the app UI object (defineMdkApp({ ... }))');\n return;\n }\n if (ui.id !== expectedId) {\n errors.push(`ui/index.js declares id \"${String(ui.id)}\", but mdk-app.yaml says \"${expectedId}\" — the dashboard refuses a mismatch`);\n }\n if (typeof ui.displayName !== 'string' || !ui.displayName) errors.push('ui/index.js must export a non-empty `displayName`');\n\n if (!Array.isArray(ui.routes)) errors.push('ui/index.js must export `routes` as an array');\n else {\n ui.routes.forEach((route: unknown, i) => {\n if (!isRecord(route) || typeof route.path !== 'string') errors.push(`ui/index.js routes[${i}] needs a string \\`path\\``);\n else if (!isComponent(route.component)) errors.push(`ui/index.js routes[${i}] (\"${route.path}\") needs a React \\`component\\``);\n });\n }\n\n // Valid, but invisible: no sidebar entry and nothing on Home — usually the\n // placeholder `mdk create app` writes before the UI is built.\n if (Array.isArray(ui.routes) && Array.isArray(ui.panels) && !ui.routes.length && !ui.panels.length) {\n warnings.push('ui/index.js exports no pages and no panels, so the app will not show in the dashboard — build the UI with `npm run build:ui`');\n }\n\n if (!Array.isArray(ui.panels)) errors.push('ui/index.js must export `panels` as an array');\n else {\n const ids = new Set<string>();\n ui.panels.forEach((panel: unknown, i) => {\n if (!isRecord(panel) || typeof panel.id !== 'string' || !panel.id) {\n errors.push(`ui/index.js panels[${i}] needs a string \\`id\\``);\n return;\n }\n if (ids.has(panel.id)) errors.push(`ui/index.js has two panels with id \"${panel.id}\"`);\n ids.add(panel.id);\n if (typeof panel.slot !== 'string' || !panel.slot) errors.push(`ui/index.js panel \"${panel.id}\" needs a string \\`slot\\``);\n if (!isComponent(panel.component)) errors.push(`ui/index.js panel \"${panel.id}\" needs a React \\`component\\``);\n });\n }\n}\n\ntry {\n const mod = (await import(pathToFileURL(entry).href)) as { default?: unknown };\n checkContract(mod.default);\n} catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n // Syntax is checked before this runs; anything thrown here is the app's\n // top-level code meeting a stub or a browser-only global. The dashboard still\n // checks the contract when it loads the app, so this is not fatal.\n warnings.push(`could not check the UI contract in Node (${message.split('\\n')[0]}); the dashboard checks it when it loads the app`);\n}\n\nprocess.stdout.write(`${JSON.stringify({ errors, warnings })}\\n`);\n"]}
@@ -0,0 +1,69 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { fileURLToPath } from 'node:url';
3
+ import { dirname, join, resolve } from 'node:path';
4
+ import { checkoutRoot } from './checkout.js';
5
+ const __dirname = dirname(fileURLToPath(import.meta.url));
6
+ /** Package root of the CLI itself — the anchor for finding the MDK checkout. */
7
+ const CLI_ROOT = resolve(__dirname, '..', '..');
8
+ /**
9
+ * Worker plugins offered by `mdk onboard`. Empty today — the bundled demo
10
+ * worker was removed from the monorepo. When an entry is added, a `repoPath`
11
+ * makes it a checkout-linked package; without one it is installed from the
12
+ * registry (and its device config would need to be filled in by hand).
13
+ */
14
+ export const WORKER_CATALOG = [];
15
+ /**
16
+ * Apps offered by `mdk onboard`. Every entry today has a `repoPath`
17
+ * and is real/runnable from the MDK checkout — the field stays optional for a
18
+ * future entry that isn't (selecting one without a `repoPath` would still
19
+ * write a valid spec entry, but its install would 404).
20
+ */
21
+ export const APP_CATALOG = [
22
+ {
23
+ value: '@tetherto/mdk-app-agent',
24
+ label: 'mdk-app-agent',
25
+ hint: 'auth-gated operator agent chat (sessions, SSE, approvals) — bundled',
26
+ repoPath: join('backend', 'apps', 'agent'),
27
+ // The UI's agent transport (ui/packages/ui-agent) calls /agent/... at the Gateway root.
28
+ mount: '/',
29
+ },
30
+ {
31
+ value: '@tetherto/mdk-app-demo',
32
+ label: 'mdk-app-demo',
33
+ hint: 'fleet summary + history across your hashrate_rt miners — bundled',
34
+ repoPath: join('backend', 'apps', 'demo'),
35
+ },
36
+ ];
37
+ export function findCatalogWorker(pkg) {
38
+ return WORKER_CATALOG.find((entry) => entry.value === pkg);
39
+ }
40
+ export function findCatalogApp(pkg) {
41
+ return APP_CATALOG.find((entry) => entry.value === pkg);
42
+ }
43
+ /**
44
+ * Absolute path to `repoPath` in the MDK checkout the CLI runs from (only the
45
+ * `<root>/packages/cli` layout counts, see `checkoutRoot`). Returns null when
46
+ * the CLI is installed standalone from npm, where there is no checkout to point at.
47
+ */
48
+ function findInCheckout(repoPath) {
49
+ const root = checkoutRoot(CLI_ROOT);
50
+ if (!root)
51
+ return null;
52
+ const candidate = join(root, repoPath);
53
+ return existsSync(join(candidate, 'package.json')) ? candidate : null;
54
+ }
55
+ /**
56
+ * Decides how a selected catalog entry (worker or app) should be
57
+ * installed. A bundled package is path-backed with a relative `file:` dep to
58
+ * its checkout directory; everything else is installed from the registry.
59
+ */
60
+ export function resolveCatalogPackage(entry) {
61
+ const packageName = entry.value;
62
+ if (!entry.repoPath)
63
+ return { packageName, bundled: false };
64
+ const dir = findInCheckout(entry.repoPath);
65
+ if (dir)
66
+ return { packageName, bundled: true, checkoutDir: dir };
67
+ return { packageName, bundled: false, unavailable: true };
68
+ }
69
+ //# sourceMappingURL=catalog.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"catalog.js","sourceRoot":"","sources":["../../src/lib/catalog.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AACzC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACnD,OAAO,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAE7C,MAAM,SAAS,GAAG,OAAO,CAAC,aAAa,CAAC,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;AAE1D,gFAAgF;AAChF,MAAM,QAAQ,GAAG,OAAO,CAAC,SAAS,EAAE,IAAI,EAAE,IAAI,CAAC,CAAC;AA8ChD;;;;;GAKG;AACH,MAAM,CAAC,MAAM,cAAc,GAAoB,EAAE,CAAC;AAElD;;;;;GAKG;AACH,MAAM,CAAC,MAAM,WAAW,GAAiB;IACvC;QACE,KAAK,EAAE,yBAAyB;QAChC,KAAK,EAAE,eAAe;QACtB,IAAI,EAAE,qEAAqE;QAC3E,QAAQ,EAAE,IAAI,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,CAAC;QAC1C,wFAAwF;QACxF,KAAK,EAAE,GAAG;KACX;IACD;QACE,KAAK,EAAE,wBAAwB;QAC/B,KAAK,EAAE,cAAc;QACrB,IAAI,EAAE,kEAAkE;QACxE,QAAQ,EAAE,IAAI,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,CAAC;KAC1C;CACF,CAAC;AAEF,MAAM,UAAU,iBAAiB,CAAC,GAAW;IAC3C,OAAO,cAAc,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,KAAK,KAAK,GAAG,CAAC,CAAC;AAC7D,CAAC;AAED,MAAM,UAAU,cAAc,CAAC,GAAW;IACxC,OAAO,WAAW,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,KAAK,KAAK,GAAG,CAAC,CAAC;AAC1D,CAAC;AAED;;;;GAIG;AACH,SAAS,cAAc,CAAC,QAAgB;IACtC,MAAM,IAAI,GAAG,YAAY,CAAC,QAAQ,CAAC,CAAC;IACpC,IAAI,CAAC,IAAI;QAAE,OAAO,IAAI,CAAC;IACvB,MAAM,SAAS,GAAG,IAAI,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC;IACvC,OAAO,UAAU,CAAC,IAAI,CAAC,SAAS,EAAE,cAAc,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC;AACxE,CAAC;AAaD;;;;GAIG;AACH,MAAM,UAAU,qBAAqB,CAAC,KAAmB;IACvD,MAAM,WAAW,GAAG,KAAK,CAAC,KAAK,CAAC;IAChC,IAAI,CAAC,KAAK,CAAC,QAAQ;QAAE,OAAO,EAAE,WAAW,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC;IAE5D,MAAM,GAAG,GAAG,cAAc,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;IAC3C,IAAI,GAAG;QAAE,OAAO,EAAE,WAAW,EAAE,OAAO,EAAE,IAAI,EAAE,WAAW,EAAE,GAAG,EAAE,CAAC;IACjE,OAAO,EAAE,WAAW,EAAE,OAAO,EAAE,KAAK,EAAE,WAAW,EAAE,IAAI,EAAE,CAAC;AAC5D,CAAC","sourcesContent":["import { existsSync } from 'node:fs';\nimport { fileURLToPath } from 'node:url';\nimport { dirname, join, resolve } from 'node:path';\nimport { checkoutRoot } from './checkout.js';\n\nconst __dirname = dirname(fileURLToPath(import.meta.url));\n\n/** Package root of the CLI itself — the anchor for finding the MDK checkout. */\nconst CLI_ROOT = resolve(__dirname, '..', '..');\n\nexport interface CatalogEntry {\n /** npm package name — what `mdk.yaml` references when it resolves from the registry. */\n value: string;\n label: string;\n hint: string;\n /**\n * Location inside the MDK checkout, for a package that ships with MDK but is\n * not on npm yet. When the CLI runs from the checkout, the project depends on\n * it via `addFileDependency`: a relative `file:` link to the checkout\n * path (whether that path sits inside the project folder or elsewhere in the\n * monorepo) — never by appending the path to `workspaces`, and never via bare\n * `*` (npm would treat that as a registry range outside a parent workspace).\n * `npm install` links it into `node_modules/<package>`, exactly as it would a\n * published package. No project folder is created for it: `workers/*` and\n * `apps/*` are reserved for packages the user actually owns\n * (`mdk create ...`).\n *\n * Linking (rather than copying) also keeps in-checkout relative resolution\n * working: Node resolves a symlinked package from its real location, so a\n * device mock finds its shared framework and an app finds the deps\n * in its own node_modules.\n */\n repoPath?: string;\n}\n\nexport interface CatalogApp extends CatalogEntry {\n /** `spec.apps[].mount` to write, when the app's clients call it somewhere other than `/apps/<name>`. */\n mount?: string;\n}\n\nexport interface CatalogWorker extends CatalogEntry {\n /** Worker-level config the plugin reads, beyond its device list. */\n config?: Record<string, unknown>;\n /**\n * Per-device opts beyond the `host`/`port`/`serial` every seed device gets.\n *\n * One object is handed to both the plugin's `connect()` and the device mock,\n * so it has to satisfy both at once.\n */\n deviceOpts?: Record<string, unknown>;\n /** Boot a device simulator from `<pkg>/mock/server.js`, so no hardware is needed. */\n mock?: boolean;\n}\n\n/**\n * Worker plugins offered by `mdk onboard`. Empty today — the bundled demo\n * worker was removed from the monorepo. When an entry is added, a `repoPath`\n * makes it a checkout-linked package; without one it is installed from the\n * registry (and its device config would need to be filled in by hand).\n */\nexport const WORKER_CATALOG: CatalogWorker[] = [];\n\n/**\n * Apps offered by `mdk onboard`. Every entry today has a `repoPath`\n * and is real/runnable from the MDK checkout — the field stays optional for a\n * future entry that isn't (selecting one without a `repoPath` would still\n * write a valid spec entry, but its install would 404).\n */\nexport const APP_CATALOG: CatalogApp[] = [\n {\n value: '@tetherto/mdk-app-agent',\n label: 'mdk-app-agent',\n hint: 'auth-gated operator agent chat (sessions, SSE, approvals) — bundled',\n repoPath: join('backend', 'apps', 'agent'),\n // The UI's agent transport (ui/packages/ui-agent) calls /agent/... at the Gateway root.\n mount: '/',\n },\n {\n value: '@tetherto/mdk-app-demo',\n label: 'mdk-app-demo',\n hint: 'fleet summary + history across your hashrate_rt miners — bundled',\n repoPath: join('backend', 'apps', 'demo'),\n },\n];\n\nexport function findCatalogWorker(pkg: string): CatalogWorker | undefined {\n return WORKER_CATALOG.find((entry) => entry.value === pkg);\n}\n\nexport function findCatalogApp(pkg: string): CatalogApp | undefined {\n return APP_CATALOG.find((entry) => entry.value === pkg);\n}\n\n/**\n * Absolute path to `repoPath` in the MDK checkout the CLI runs from (only the\n * `<root>/packages/cli` layout counts, see `checkoutRoot`). Returns null when\n * the CLI is installed standalone from npm, where there is no checkout to point at.\n */\nfunction findInCheckout(repoPath: string): string | null {\n const root = checkoutRoot(CLI_ROOT);\n if (!root) return null;\n const candidate = join(root, repoPath);\n return existsSync(join(candidate, 'package.json')) ? candidate : null;\n}\n\nexport interface ResolvedCatalogPackage {\n /** npm package name — what `mdk.yaml` references and what lands in node_modules. */\n packageName: string;\n /** True when the package is linked from the checkout rather than the registry. */\n bundled: boolean;\n /** Set when the entry wanted the checkout but the CLI is running standalone. */\n unavailable?: boolean;\n /** Absolute checkout path — present when bundled and resolvable. */\n checkoutDir?: string;\n}\n\n/**\n * Decides how a selected catalog entry (worker or app) should be\n * installed. A bundled package is path-backed with a relative `file:` dep to\n * its checkout directory; everything else is installed from the registry.\n */\nexport function resolveCatalogPackage(entry: CatalogEntry): ResolvedCatalogPackage {\n const packageName = entry.value;\n if (!entry.repoPath) return { packageName, bundled: false };\n\n const dir = findInCheckout(entry.repoPath);\n if (dir) return { packageName, bundled: true, checkoutDir: dir };\n return { packageName, bundled: false, unavailable: true };\n}\n"]}
@@ -0,0 +1,14 @@
1
+ import { basename, dirname } from 'node:path';
2
+ /**
3
+ * Root of the MDK monorepo checkout the CLI runs from, or null for a published
4
+ * install. Only the known `<root>/packages/cli` layout is accepted — never a walk
5
+ * up the tree, which would let any ancestor directory (a shared `/tmp`, a home
6
+ * directory) plant the templates or packages the CLI then copies and installs.
7
+ */
8
+ export function checkoutRoot(cliRoot) {
9
+ const packagesDir = dirname(cliRoot);
10
+ if (basename(cliRoot) !== 'cli' || basename(packagesDir) !== 'packages')
11
+ return null;
12
+ return dirname(packagesDir);
13
+ }
14
+ //# sourceMappingURL=checkout.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"checkout.js","sourceRoot":"","sources":["../../src/lib/checkout.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAE9C;;;;;GAKG;AACH,MAAM,UAAU,YAAY,CAAC,OAAe;IAC1C,MAAM,WAAW,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IACrC,IAAI,QAAQ,CAAC,OAAO,CAAC,KAAK,KAAK,IAAI,QAAQ,CAAC,WAAW,CAAC,KAAK,UAAU;QAAE,OAAO,IAAI,CAAC;IACrF,OAAO,OAAO,CAAC,WAAW,CAAC,CAAC;AAC9B,CAAC","sourcesContent":["import { basename, dirname } from 'node:path';\n\n/**\n * Root of the MDK monorepo checkout the CLI runs from, or null for a published\n * install. Only the known `<root>/packages/cli` layout is accepted — never a walk\n * up the tree, which would let any ancestor directory (a shared `/tmp`, a home\n * directory) plant the templates or packages the CLI then copies and installs.\n */\nexport function checkoutRoot(cliRoot: string): string | null {\n const packagesDir = dirname(cliRoot);\n if (basename(cliRoot) !== 'cli' || basename(packagesDir) !== 'packages') return null;\n return dirname(packagesDir);\n}\n"]}
@@ -0,0 +1,71 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { parse as parseYaml } from 'yaml';
4
+ import { APP_MANIFEST } from './app-package.js';
5
+ import { resolvePackageDir } from './runtime.js';
6
+ /**
7
+ * Environment variable `mdk run dashboard` hands the installed apps to the
8
+ * dashboard's dev server in: a JSON array of {@link DashboardApp}. The
9
+ * dashboard serves each app's built `ui/` and lists the apps to the browser
10
+ * itself, so finding them needs no Gateway.
11
+ */
12
+ export const DASHBOARD_APPS_ENV = 'MDK_DASHBOARD_APPS';
13
+ /**
14
+ * Where the Gateway serves an app's own routes: its `mount`, or `/apps/<name>`
15
+ * (startGateway's default). `mount: /` puts them at the root, so the prefix is
16
+ * empty.
17
+ */
18
+ export function appApiBase(app) {
19
+ const mount = app.mount ?? `/apps/${app.name}`;
20
+ return mount === '/' ? '' : mount.replace(/\/+$/, '');
21
+ }
22
+ /**
23
+ * Resolves each `spec.apps[]` package to its directory, as `mdk run` does
24
+ * (`resolvePackageDir`); it must hold an `mdk-app.yaml`, or it is not an app.
25
+ */
26
+ export function resolveAppDirs(projectDir, apps) {
27
+ return apps.map((app) => {
28
+ let dir;
29
+ try {
30
+ dir = resolvePackageDir(projectDir, app.package);
31
+ }
32
+ catch (error) {
33
+ throw new Error(`App "${app.name}": ${error instanceof Error ? error.message : String(error)}`);
34
+ }
35
+ if (!existsSync(join(dir, APP_MANIFEST))) {
36
+ throw new Error(`App "${app.name}": ${dir} has no ${APP_MANIFEST} — "${app.package}" is not an MDK app package.`);
37
+ }
38
+ return { name: app.name, dir };
39
+ });
40
+ }
41
+ /**
42
+ * The installed apps of a stack, read from `mdk.yaml` and each app's
43
+ * `mdk-app.yaml`, for the dashboard to load. Throws naming the app when a
44
+ * package is missing or its manifest lacks what the dashboard shows.
45
+ */
46
+ export function describeDashboardApps(projectDir, apps) {
47
+ return resolveAppDirs(projectDir, apps).map(({ name, dir }, i) => {
48
+ let manifest;
49
+ try {
50
+ manifest = parseYaml(readFileSync(join(dir, APP_MANIFEST), 'utf8'));
51
+ }
52
+ catch (error) {
53
+ throw new Error(`App "${name}": ${APP_MANIFEST}: ${error instanceof Error ? error.message : String(error)}`);
54
+ }
55
+ const fields = (manifest ?? {});
56
+ for (const field of ['id', 'version', 'displayName']) {
57
+ if (typeof fields[field] !== 'string' || !fields[field]) {
58
+ throw new Error(`App "${name}": ${APP_MANIFEST} is missing "${field}" — check it with \`mdk validate app ${name}\`.`);
59
+ }
60
+ }
61
+ return {
62
+ name,
63
+ id: fields.id,
64
+ version: fields.version,
65
+ displayName: fields.displayName,
66
+ apiBase: appApiBase(apps[i]),
67
+ dir,
68
+ };
69
+ });
70
+ }
71
+ //# sourceMappingURL=dashboard-apps.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dashboard-apps.js","sourceRoot":"","sources":["../../src/lib/dashboard-apps.ts"],"names":[],"mappings":"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","sourcesContent":["import { existsSync, readFileSync } from 'node:fs';\nimport { join } from 'node:path';\nimport { parse as parseYaml } from 'yaml';\nimport { APP_MANIFEST } from './app-package.js';\nimport { resolvePackageDir } from './runtime.js';\nimport type { StackSpec } from './spec.js';\n\n/**\n * Environment variable `mdk run dashboard` hands the installed apps to the\n * dashboard's dev server in: a JSON array of {@link DashboardApp}. The\n * dashboard serves each app's built `ui/` and lists the apps to the browser\n * itself, so finding them needs no Gateway.\n */\nexport const DASHBOARD_APPS_ENV = 'MDK_DASHBOARD_APPS';\n\n/** One installed app, as the dashboard needs it to load the app's UI. */\nexport interface DashboardApp {\n /** Installed name (`spec.apps[].name`). */\n name: string;\n /** Reverse-DNS id from the app's `mdk-app.yaml`. */\n id: string;\n version: string;\n displayName: string;\n /** Where the app's own routes are reached through the Gateway: its `mount`, default `/apps/<name>`. */\n apiBase: string;\n /** The app package's directory; its built UI is `<dir>/ui/`. */\n dir: string;\n}\n\ntype AppEntry = NonNullable<StackSpec['spec']['apps']>[number];\n\n/**\n * Where the Gateway serves an app's own routes: its `mount`, or `/apps/<name>`\n * (startGateway's default). `mount: /` puts them at the root, so the prefix is\n * empty.\n */\nexport function appApiBase(app: Pick<AppEntry, 'name' | 'mount'>): string {\n const mount = app.mount ?? `/apps/${app.name}`;\n return mount === '/' ? '' : mount.replace(/\\/+$/, '');\n}\n\n/**\n * Resolves each `spec.apps[]` package to its directory, as `mdk run` does\n * (`resolvePackageDir`); it must hold an `mdk-app.yaml`, or it is not an app.\n */\nexport function resolveAppDirs(projectDir: string, apps: AppEntry[]): Array<{ name: string; dir: string }> {\n return apps.map((app) => {\n let dir: string;\n try {\n dir = resolvePackageDir(projectDir, app.package);\n } catch (error) {\n throw new Error(`App \"${app.name}\": ${error instanceof Error ? error.message : String(error)}`);\n }\n if (!existsSync(join(dir, APP_MANIFEST))) {\n throw new Error(`App \"${app.name}\": ${dir} has no ${APP_MANIFEST} — \"${app.package}\" is not an MDK app package.`);\n }\n return { name: app.name, dir };\n });\n}\n\n/**\n * The installed apps of a stack, read from `mdk.yaml` and each app's\n * `mdk-app.yaml`, for the dashboard to load. Throws naming the app when a\n * package is missing or its manifest lacks what the dashboard shows.\n */\nexport function describeDashboardApps(projectDir: string, apps: AppEntry[]): DashboardApp[] {\n return resolveAppDirs(projectDir, apps).map(({ name, dir }, i) => {\n let manifest: unknown;\n try {\n manifest = parseYaml(readFileSync(join(dir, APP_MANIFEST), 'utf8'));\n } catch (error) {\n throw new Error(`App \"${name}\": ${APP_MANIFEST}: ${error instanceof Error ? error.message : String(error)}`);\n }\n const fields = (manifest ?? {}) as Record<string, unknown>;\n for (const field of ['id', 'version', 'displayName']) {\n if (typeof fields[field] !== 'string' || !fields[field]) {\n throw new Error(`App \"${name}\": ${APP_MANIFEST} is missing \"${field}\" — check it with \\`mdk validate app ${name}\\`.`);\n }\n }\n return {\n name,\n id: fields.id as string,\n version: fields.version as string,\n displayName: fields.displayName as string,\n apiBase: appApiBase(apps[i]!),\n dir,\n };\n });\n}\n"]}