@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,243 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import { createRequire } from 'node:module';
3
+ import { isAbsolute, join, resolve } from 'node:path';
4
+ const require = createRequire(import.meta.url);
5
+ const TYPE_OK = {
6
+ number: (v) => typeof v === 'number' && Number.isFinite(v),
7
+ string: (v) => typeof v === 'string',
8
+ boolean: (v) => typeof v === 'boolean',
9
+ array: (v) => Array.isArray(v),
10
+ object: (v) => typeof v === 'object' && v !== null && !Array.isArray(v),
11
+ };
12
+ function withDeadline(run, ms, label) {
13
+ return new Promise((resolvePromise, rejectPromise) => {
14
+ // Deliberately not unref'd: against a hung handler this timer is the only
15
+ // handle on the event loop — unref it and node exits 0 before it fires.
16
+ const timer = setTimeout(() => rejectPromise(new Error(`ERR_SMOKE_TIMEOUT: ${label} did not settle within ${ms}ms`)), ms);
17
+ Promise.resolve()
18
+ .then(run)
19
+ .then(resolvePromise, rejectPromise)
20
+ .finally(() => clearTimeout(timer));
21
+ });
22
+ }
23
+ /** A value the runtime's param gate accepts for `p`, for params a probe is not about. */
24
+ function gateValue(p) {
25
+ if (Array.isArray(p.enum) && p.enum.length)
26
+ return p.enum[0];
27
+ switch (p.type) {
28
+ case 'number':
29
+ return typeof p.min === 'number' ? p.min : typeof p.max === 'number' ? Math.min(p.max, 0) : 0;
30
+ case 'boolean':
31
+ return false;
32
+ case 'string':
33
+ return '';
34
+ default:
35
+ return {};
36
+ }
37
+ }
38
+ /**
39
+ * A param set for `cmd` that passes its gate: the smoke config's sample when
40
+ * it has one, every other required param filled with a valid value. The gate
41
+ * checks params in declaration order and `required` before range, so a probe
42
+ * that sends only the param under test fails on the first missing required
43
+ * param instead of the bound it means to test. Optional params stay absent —
44
+ * the gate skips them, and a filler value ('' / {} / false) could make the
45
+ * handler reject for its own reasons, passing the self-rejection check for
46
+ * the wrong one.
47
+ */
48
+ function baselineParams(cmd, sample) {
49
+ const filled = Object.fromEntries((cmd.params ?? []).filter((p) => p.required === true).map((p) => [p.name, gateValue(p)]));
50
+ return { ...filled, ...(sample ?? {}) };
51
+ }
52
+ async function resolveSpec(workerDir, config) {
53
+ const requireFromWorker = createRequire(join(workerDir, 'noop.js'));
54
+ if (config) {
55
+ if (config.trim().startsWith('{'))
56
+ return { config: JSON.parse(config) };
57
+ const file = isAbsolute(config) ? config : resolve(config);
58
+ if (file.endsWith('.json'))
59
+ return { config: JSON.parse(readFileSync(file, 'utf8')) };
60
+ const mod = requireFromWorker(file);
61
+ return mod.setup ? mod.setup() : { config: mod };
62
+ }
63
+ const smokeConfig = join(workerDir, 'smoke.config.js');
64
+ if (!existsSync(smokeConfig)) {
65
+ throw new Error(`ERR_SMOKE_NO_CONFIG: no --smoke-config given and no ${smokeConfig}`);
66
+ }
67
+ return requireFromWorker(smokeConfig).setup();
68
+ }
69
+ /**
70
+ * Behavioural checks against the worker's own mock device — no Kernel, no DHT.
71
+ * Live pass: every declared telemetry handler returns its declared type, the
72
+ * runtime's param gate rejects out-of-range values, every smoke.config sample
73
+ * command executes, and a handler called with an out-of-range param rejects
74
+ * with an ERR_* of its own (the worker is reachable on its own RPC key, so it
75
+ * must not rely on the Kernel's gate). Stopped pass, after `teardown()`: every
76
+ * handler rejects with an ERR_* within the deadline — no hang, no crash, no
77
+ * stale value. Two exceptions: a `health` channel may instead report a non-OK
78
+ * status (its job is to say the device is down), and the smoke config's
79
+ * `offlineTelemetry` channels must return their declared type. Every handler
80
+ * call is bounded by `timeoutMs`.
81
+ */
82
+ export async function runWorkerSmoke(workerDir, opts = {}) {
83
+ const timeoutMs = opts.timeoutMs ?? 10_000;
84
+ const outcome = { passed: 0, failures: [], warnings: [] };
85
+ const check = (ok, label, detail) => {
86
+ if (ok)
87
+ outcome.passed++;
88
+ else
89
+ outcome.failures.push(`smoke: ${label}${detail ? ` — ${detail}` : ''}`);
90
+ };
91
+ const runtime = require('@tetherto/mdk-worker');
92
+ const pluginDir = existsSync(join(workerDir, 'plugin', 'mdk-contract.json')) ? join(workerDir, 'plugin') : workerDir;
93
+ const { contract, entries, dir } = runtime.loadContract(pluginDir);
94
+ const spec = await resolveSpec(workerDir, opts.config);
95
+ // setup() has started the mock: stop it however the checks end, or its open
96
+ // socket keeps the process alive after a handler fails to load.
97
+ let stopped = false;
98
+ const stop = async () => {
99
+ if (stopped || !spec.teardown)
100
+ return;
101
+ stopped = true;
102
+ await withDeadline(() => spec.teardown(), timeoutMs, 'teardown()');
103
+ };
104
+ try {
105
+ const instance = runtime.createInstance({
106
+ dir,
107
+ entries,
108
+ device: {
109
+ id: 'smoke-0',
110
+ opts: Object.freeze({ ...(spec.config ?? {}) }),
111
+ env: Object.freeze({ ...(spec.env ?? {}) }),
112
+ config: Object.freeze({ ...(spec.pluginConfig ?? {}) }),
113
+ workerId: 'smoke',
114
+ logger: () => { },
115
+ },
116
+ });
117
+ const telemetry = contract.capabilities.telemetry ?? [];
118
+ const commands = contract.capabilities.commands ?? [];
119
+ for (const entry of telemetry) {
120
+ const label = `telemetry '${entry.name}' returns a ${entry.type}`;
121
+ const typeOk = TYPE_OK[entry.type ?? ''] ?? ((v) => v !== undefined);
122
+ try {
123
+ const value = await withDeadline(() => instance.telemetry.get(entry.name)({}), timeoutMs, label);
124
+ check(value !== undefined && typeOk(value), label, value === undefined ? 'returned undefined' : `returned ${JSON.stringify(value)}`);
125
+ }
126
+ catch (error) {
127
+ check(false, label, message(error));
128
+ }
129
+ }
130
+ for (const cmd of commands) {
131
+ const base = baselineParams(cmd, spec.commands?.[cmd.name]);
132
+ for (const p of cmd.params ?? []) {
133
+ if (p.type !== 'number' || typeof p.min !== 'number' || typeof p.max !== 'number')
134
+ continue;
135
+ const below = runtime.validateParams(cmd.params, { ...base, [p.name]: p.min - 1 });
136
+ check(below !== null && below.startsWith('ERR_PARAM_RANGE'), `param gate rejects '${cmd.name}' ${p.name}=${p.min - 1}`, below ?? 'accepted');
137
+ const above = runtime.validateParams(cmd.params, { ...base, [p.name]: p.max + 1 });
138
+ check(above !== null && above.startsWith('ERR_PARAM_RANGE'), `param gate rejects '${cmd.name}' ${p.name}=${p.max + 1}`, above ?? 'accepted');
139
+ const atMin = runtime.validateParams(cmd.params, { ...base, [p.name]: p.min });
140
+ const atMax = runtime.validateParams(cmd.params, { ...base, [p.name]: p.max });
141
+ check(atMin === null && atMax === null, `param gate accepts '${cmd.name}' ${p.name} at [${p.min}, ${p.max}]`, atMin ?? atMax ?? undefined);
142
+ }
143
+ }
144
+ for (const [name, params] of Object.entries(spec.commands ?? {})) {
145
+ const label = `command '${name}' executes with sample params`;
146
+ const cmd = commands.find((c) => c.name === name);
147
+ const handler = instance.commands.get(name);
148
+ if (!cmd || !handler) {
149
+ check(false, label, 'not declared in contract');
150
+ continue;
151
+ }
152
+ const gate = runtime.validateParams(cmd.params, params);
153
+ if (gate !== null) {
154
+ check(false, label, gate);
155
+ continue;
156
+ }
157
+ try {
158
+ await withDeadline(() => handler(params), timeoutMs, label);
159
+ check(true, label);
160
+ }
161
+ catch (error) {
162
+ check(false, label, message(error));
163
+ }
164
+ }
165
+ // The worker is reachable on its own RPC key, so each handler must reject
166
+ // bad input itself, not lean on the Kernel's gate.
167
+ for (const cmd of commands) {
168
+ const p = (cmd.params ?? []).find((q) => q.type === 'number' && typeof q.min === 'number');
169
+ if (!p)
170
+ continue;
171
+ const label = `command '${cmd.name}' itself rejects ${p.name}=${p.min - 1} with an ERR_*`;
172
+ // Otherwise valid params, so the handler rejects the bound, not a missing param.
173
+ const params = { ...baselineParams(cmd, spec.commands?.[cmd.name]), [p.name]: p.min - 1 };
174
+ await expectErr(() => instance.commands.get(cmd.name)(params), timeoutMs, label, check);
175
+ }
176
+ if (!spec.teardown) {
177
+ outcome.warnings.push('smoke: no teardown() in the smoke config — device-stopped behaviour not checked');
178
+ return outcome;
179
+ }
180
+ await stop();
181
+ const offline = new Set(spec.offlineTelemetry ?? []);
182
+ for (const name of offline) {
183
+ if (!telemetry.some((entry) => entry.name === name))
184
+ check(false, `offlineTelemetry '${name}' is a declared telemetry channel`);
185
+ }
186
+ for (const entry of telemetry) {
187
+ const read = () => instance.telemetry.get(entry.name)({});
188
+ if (offline.has(entry.name)) {
189
+ const label = `telemetry '${entry.name}' still returns a ${entry.type} from the worker's store when the device is stopped`;
190
+ const typeOk = TYPE_OK[entry.type ?? ''] ?? ((v) => v !== undefined);
191
+ try {
192
+ const value = await withDeadline(read, timeoutMs, label);
193
+ check(value !== undefined && typeOk(value), label, `returned ${JSON.stringify(value)}`);
194
+ }
195
+ catch (error) {
196
+ check(false, label, message(error));
197
+ }
198
+ }
199
+ else if (entry.name === 'health') {
200
+ const label = `telemetry 'health' reports a non-OK status or fails with an ERR_* when the device is stopped`;
201
+ await expectErr(read, timeoutMs, label, check, (value) => isNonOkHealth(value));
202
+ }
203
+ else {
204
+ const label = `telemetry '${entry.name}' fails with an ERR_* when the device is stopped`;
205
+ await expectErr(read, timeoutMs, label, check);
206
+ }
207
+ }
208
+ for (const [name, params] of Object.entries(spec.commands ?? {})) {
209
+ const handler = instance.commands.get(name);
210
+ if (!handler)
211
+ continue;
212
+ const label = `command '${name}' fails with an ERR_* when the device is stopped`;
213
+ await expectErr(() => handler(params), timeoutMs, label, check);
214
+ }
215
+ return outcome;
216
+ }
217
+ finally {
218
+ await stop().catch(() => { });
219
+ }
220
+ }
221
+ async function expectErr(run, timeoutMs, label, check,
222
+ /** A resolved value that also counts as failing correctly. */
223
+ acceptResolved) {
224
+ try {
225
+ const value = await withDeadline(run, timeoutMs, label);
226
+ check(acceptResolved?.(value) ?? false, label, `resolved with ${JSON.stringify(value)}`);
227
+ }
228
+ catch (error) {
229
+ const text = message(error);
230
+ check(/(^|: )ERR_[A-Z_]+/.test(text) && !text.startsWith('ERR_SMOKE_TIMEOUT'), label, text);
231
+ }
232
+ }
233
+ /** A health verdict that says the device is not fine: `{ status }` with any value but OK. */
234
+ function isNonOkHealth(value) {
235
+ if (typeof value !== 'object' || value === null)
236
+ return false;
237
+ const { status } = value;
238
+ return typeof status === 'string' && status !== '' && status !== 'OK';
239
+ }
240
+ function message(error) {
241
+ return error instanceof Error ? error.message : String(error);
242
+ }
243
+ //# sourceMappingURL=worker-smoke.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"worker-smoke.js","sourceRoot":"","sources":["../../src/lib/worker-smoke.ts"],"names":[],"mappings":"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","sourcesContent":["import { existsSync, readFileSync } from 'node:fs';\nimport { createRequire } from 'node:module';\nimport { isAbsolute, join, resolve } from 'node:path';\n\nconst require = createRequire(import.meta.url);\n\nconst TYPE_OK: Record<string, (v: unknown) => boolean> = {\n number: (v) => typeof v === 'number' && Number.isFinite(v),\n string: (v) => typeof v === 'string',\n boolean: (v) => typeof v === 'boolean',\n array: (v) => Array.isArray(v),\n object: (v) => typeof v === 'object' && v !== null && !Array.isArray(v),\n};\n\ninterface ContractParam {\n name: string;\n type?: string;\n required?: boolean;\n enum?: unknown[];\n min?: number;\n max?: number;\n}\n\ninterface ContractEntry {\n name: string;\n type?: string;\n params?: ContractParam[];\n}\n\ninterface MdkWorkerRuntime {\n loadContract(dir: string): {\n dir: string;\n contract: {\n metadata: { brand?: string; deviceFamily?: string };\n capabilities: { telemetry?: ContractEntry[]; commands?: ContractEntry[] };\n };\n entries: { telemetry: Map<string, string>; commands: Map<string, string> };\n };\n createInstance(opts: {\n dir: string;\n entries: { telemetry: Map<string, string>; commands: Map<string, string> };\n device: Record<string, unknown>;\n }): { telemetry: Map<string, (params: object) => unknown>; commands: Map<string, (params: object) => unknown> };\n validateParams(declared: ContractParam[] | undefined, params: object): string | null;\n}\n\n/** What smoke.config.js `setup()` resolves to: the mock device and how to reach it. */\ninterface SmokeSpec {\n config?: Record<string, unknown>;\n env?: Record<string, unknown>;\n pluginConfig?: Record<string, unknown>;\n /** Sample params per command name, executed against the mock. */\n commands?: Record<string, object>;\n /** Stops the mock device; its presence enables the device-stopped failure pass. */\n teardown?: () => unknown;\n /**\n * Telemetry served from the worker's own store rather than the device (a\n * `history` log, say), which keeps answering once it stops. These must still\n * settle and return their declared type instead of failing with an ERR_*.\n */\n offlineTelemetry?: string[];\n}\n\nexport interface SmokeOutcome {\n /** Checks that passed. */\n passed: number;\n /** One line per failed check. */\n failures: string[];\n /** Passes that could not run (no teardown to stop the mock with). */\n warnings: string[];\n}\n\nfunction withDeadline<T>(run: () => T | Promise<T>, ms: number, label: string): Promise<T> {\n return new Promise<T>((resolvePromise, rejectPromise) => {\n // Deliberately not unref'd: against a hung handler this timer is the only\n // handle on the event loop — unref it and node exits 0 before it fires.\n const timer = setTimeout(() => rejectPromise(new Error(`ERR_SMOKE_TIMEOUT: ${label} did not settle within ${ms}ms`)), ms);\n Promise.resolve()\n .then(run)\n .then(resolvePromise, rejectPromise)\n .finally(() => clearTimeout(timer));\n });\n}\n\n/** A value the runtime's param gate accepts for `p`, for params a probe is not about. */\nfunction gateValue(p: ContractParam): unknown {\n if (Array.isArray(p.enum) && p.enum.length) return p.enum[0];\n switch (p.type) {\n case 'number':\n return typeof p.min === 'number' ? p.min : typeof p.max === 'number' ? Math.min(p.max, 0) : 0;\n case 'boolean':\n return false;\n case 'string':\n return '';\n default:\n return {};\n }\n}\n\n/**\n * A param set for `cmd` that passes its gate: the smoke config's sample when\n * it has one, every other required param filled with a valid value. The gate\n * checks params in declaration order and `required` before range, so a probe\n * that sends only the param under test fails on the first missing required\n * param instead of the bound it means to test. Optional params stay absent —\n * the gate skips them, and a filler value ('' / {} / false) could make the\n * handler reject for its own reasons, passing the self-rejection check for\n * the wrong one.\n */\nfunction baselineParams(cmd: ContractEntry, sample: object | undefined): Record<string, unknown> {\n const filled = Object.fromEntries((cmd.params ?? []).filter((p) => p.required === true).map((p) => [p.name, gateValue(p)]));\n return { ...filled, ...(sample ?? {}) };\n}\n\nasync function resolveSpec(workerDir: string, config: string | undefined): Promise<SmokeSpec> {\n const requireFromWorker = createRequire(join(workerDir, 'noop.js'));\n if (config) {\n if (config.trim().startsWith('{')) return { config: JSON.parse(config) };\n const file = isAbsolute(config) ? config : resolve(config);\n if (file.endsWith('.json')) return { config: JSON.parse(readFileSync(file, 'utf8')) };\n const mod = requireFromWorker(file);\n return mod.setup ? mod.setup() : { config: mod };\n }\n const smokeConfig = join(workerDir, 'smoke.config.js');\n if (!existsSync(smokeConfig)) {\n throw new Error(`ERR_SMOKE_NO_CONFIG: no --smoke-config given and no ${smokeConfig}`);\n }\n return requireFromWorker(smokeConfig).setup();\n}\n\n/**\n * Behavioural checks against the worker's own mock device — no Kernel, no DHT.\n * Live pass: every declared telemetry handler returns its declared type, the\n * runtime's param gate rejects out-of-range values, every smoke.config sample\n * command executes, and a handler called with an out-of-range param rejects\n * with an ERR_* of its own (the worker is reachable on its own RPC key, so it\n * must not rely on the Kernel's gate). Stopped pass, after `teardown()`: every\n * handler rejects with an ERR_* within the deadline — no hang, no crash, no\n * stale value. Two exceptions: a `health` channel may instead report a non-OK\n * status (its job is to say the device is down), and the smoke config's\n * `offlineTelemetry` channels must return their declared type. Every handler\n * call is bounded by `timeoutMs`.\n */\nexport async function runWorkerSmoke(workerDir: string, opts: { config?: string; timeoutMs?: number } = {}): Promise<SmokeOutcome> {\n const timeoutMs = opts.timeoutMs ?? 10_000;\n const outcome: SmokeOutcome = { passed: 0, failures: [], warnings: [] };\n const check = (ok: boolean, label: string, detail?: string): void => {\n if (ok) outcome.passed++;\n else outcome.failures.push(`smoke: ${label}${detail ? ` — ${detail}` : ''}`);\n };\n\n const runtime = require('@tetherto/mdk-worker') as MdkWorkerRuntime;\n const pluginDir = existsSync(join(workerDir, 'plugin', 'mdk-contract.json')) ? join(workerDir, 'plugin') : workerDir;\n const { contract, entries, dir } = runtime.loadContract(pluginDir);\n const spec = await resolveSpec(workerDir, opts.config);\n\n // setup() has started the mock: stop it however the checks end, or its open\n // socket keeps the process alive after a handler fails to load.\n let stopped = false;\n const stop = async (): Promise<void> => {\n if (stopped || !spec.teardown) return;\n stopped = true;\n await withDeadline(() => spec.teardown!(), timeoutMs, 'teardown()');\n };\n\n try {\n const instance = runtime.createInstance({\n dir,\n entries,\n device: {\n id: 'smoke-0',\n opts: Object.freeze({ ...(spec.config ?? {}) }),\n env: Object.freeze({ ...(spec.env ?? {}) }),\n config: Object.freeze({ ...(spec.pluginConfig ?? {}) }),\n workerId: 'smoke',\n logger: () => {},\n },\n });\n\n const telemetry = contract.capabilities.telemetry ?? [];\n const commands = contract.capabilities.commands ?? [];\n\n for (const entry of telemetry) {\n const label = `telemetry '${entry.name}' returns a ${entry.type}`;\n const typeOk = TYPE_OK[entry.type ?? ''] ?? ((v: unknown) => v !== undefined);\n try {\n const value = await withDeadline(() => instance.telemetry.get(entry.name)!({}), timeoutMs, label);\n check(value !== undefined && typeOk(value), label, value === undefined ? 'returned undefined' : `returned ${JSON.stringify(value)}`);\n } catch (error) {\n check(false, label, message(error));\n }\n }\n\n for (const cmd of commands) {\n const base = baselineParams(cmd, spec.commands?.[cmd.name]);\n for (const p of cmd.params ?? []) {\n if (p.type !== 'number' || typeof p.min !== 'number' || typeof p.max !== 'number') continue;\n const below = runtime.validateParams(cmd.params, { ...base, [p.name]: p.min - 1 });\n check(below !== null && below.startsWith('ERR_PARAM_RANGE'), `param gate rejects '${cmd.name}' ${p.name}=${p.min - 1}`, below ?? 'accepted');\n const above = runtime.validateParams(cmd.params, { ...base, [p.name]: p.max + 1 });\n check(above !== null && above.startsWith('ERR_PARAM_RANGE'), `param gate rejects '${cmd.name}' ${p.name}=${p.max + 1}`, above ?? 'accepted');\n const atMin = runtime.validateParams(cmd.params, { ...base, [p.name]: p.min });\n const atMax = runtime.validateParams(cmd.params, { ...base, [p.name]: p.max });\n check(atMin === null && atMax === null, `param gate accepts '${cmd.name}' ${p.name} at [${p.min}, ${p.max}]`, atMin ?? atMax ?? undefined);\n }\n }\n\n for (const [name, params] of Object.entries(spec.commands ?? {})) {\n const label = `command '${name}' executes with sample params`;\n const cmd = commands.find((c) => c.name === name);\n const handler = instance.commands.get(name);\n if (!cmd || !handler) {\n check(false, label, 'not declared in contract');\n continue;\n }\n const gate = runtime.validateParams(cmd.params, params);\n if (gate !== null) {\n check(false, label, gate);\n continue;\n }\n try {\n await withDeadline(() => handler(params), timeoutMs, label);\n check(true, label);\n } catch (error) {\n check(false, label, message(error));\n }\n }\n\n // The worker is reachable on its own RPC key, so each handler must reject\n // bad input itself, not lean on the Kernel's gate.\n for (const cmd of commands) {\n const p = (cmd.params ?? []).find((q) => q.type === 'number' && typeof q.min === 'number');\n if (!p) continue;\n const label = `command '${cmd.name}' itself rejects ${p.name}=${p.min! - 1} with an ERR_*`;\n // Otherwise valid params, so the handler rejects the bound, not a missing param.\n const params = { ...baselineParams(cmd, spec.commands?.[cmd.name]), [p.name]: p.min! - 1 };\n await expectErr(() => instance.commands.get(cmd.name)!(params), timeoutMs, label, check);\n }\n\n if (!spec.teardown) {\n outcome.warnings.push('smoke: no teardown() in the smoke config — device-stopped behaviour not checked');\n return outcome;\n }\n await stop();\n\n const offline = new Set(spec.offlineTelemetry ?? []);\n for (const name of offline) {\n if (!telemetry.some((entry) => entry.name === name)) check(false, `offlineTelemetry '${name}' is a declared telemetry channel`);\n }\n for (const entry of telemetry) {\n const read = (): unknown => instance.telemetry.get(entry.name)!({});\n if (offline.has(entry.name)) {\n const label = `telemetry '${entry.name}' still returns a ${entry.type} from the worker's store when the device is stopped`;\n const typeOk = TYPE_OK[entry.type ?? ''] ?? ((v: unknown) => v !== undefined);\n try {\n const value = await withDeadline(read, timeoutMs, label);\n check(value !== undefined && typeOk(value), label, `returned ${JSON.stringify(value)}`);\n } catch (error) {\n check(false, label, message(error));\n }\n } else if (entry.name === 'health') {\n const label = `telemetry 'health' reports a non-OK status or fails with an ERR_* when the device is stopped`;\n await expectErr(read, timeoutMs, label, check, (value) => isNonOkHealth(value));\n } else {\n const label = `telemetry '${entry.name}' fails with an ERR_* when the device is stopped`;\n await expectErr(read, timeoutMs, label, check);\n }\n }\n for (const [name, params] of Object.entries(spec.commands ?? {})) {\n const handler = instance.commands.get(name);\n if (!handler) continue;\n const label = `command '${name}' fails with an ERR_* when the device is stopped`;\n await expectErr(() => handler(params), timeoutMs, label, check);\n }\n\n return outcome;\n } finally {\n await stop().catch(() => {});\n }\n}\n\nasync function expectErr(\n run: () => unknown,\n timeoutMs: number,\n label: string,\n check: (ok: boolean, label: string, detail?: string) => void,\n /** A resolved value that also counts as failing correctly. */\n acceptResolved?: (value: unknown) => boolean,\n): Promise<void> {\n try {\n const value = await withDeadline(run, timeoutMs, label);\n check(acceptResolved?.(value) ?? false, label, `resolved with ${JSON.stringify(value)}`);\n } catch (error) {\n const text = message(error);\n check(/(^|: )ERR_[A-Z_]+/.test(text) && !text.startsWith('ERR_SMOKE_TIMEOUT'), label, text);\n }\n}\n\n/** A health verdict that says the device is not fine: `{ status }` with any value but OK. */\nfunction isNonOkHealth(value: unknown): boolean {\n if (typeof value !== 'object' || value === null) return false;\n const { status } = value as { status?: unknown };\n return typeof status === 'string' && status !== '' && status !== 'OK';\n}\n\nfunction message(error: unknown): string {\n return error instanceof Error ? error.message : String(error);\n}\n"]}
@@ -0,0 +1,58 @@
1
+ import { Command } from 'commander';
2
+ import { pkg } from './lib/pkg.js';
3
+ import { configureHelp, examples } from './lib/help.js';
4
+ import { registerOnboard } from './commands/onboard.js';
5
+ import { registerCreate } from './commands/create.js';
6
+ import { registerAdd } from './commands/add.js';
7
+ import { registerValidate } from './commands/validate.js';
8
+ import { registerRun } from './commands/run.js';
9
+ import { registerStatus } from './commands/inspect.js';
10
+ import { registerSkill } from './commands/agent.js';
11
+ import { registerVersion } from './commands/meta.js';
12
+ /** The help heading each top-level command is listed under. */
13
+ const HELP_GROUPS = {
14
+ onboard: 'Set up:',
15
+ create: 'Build:',
16
+ add: 'Build:',
17
+ validate: 'Build:',
18
+ run: 'Run:',
19
+ status: 'Run:',
20
+ skill: 'Other:',
21
+ version: 'Other:',
22
+ };
23
+ /**
24
+ * Builds the full `mdk` command tree. Every command reads `mdk <verb> [noun]`.
25
+ *
26
+ * `onboard`, `create`, `add`, `validate`, `run`, `status`, `skill add` and
27
+ * `version` are implemented.
28
+ */
29
+ export function buildProgram() {
30
+ const program = configureHelp(new Command());
31
+ program
32
+ .name('mdk')
33
+ .usage('<command> [options]')
34
+ .description('Build, run and check an MDK stack: workers, apps, the Kernel and the Gateway.')
35
+ .version(pkg.version, '--version', 'Print the CLI version')
36
+ .option('-o, --output <fmt>', 'Output format for data: table | json | yaml', 'table')
37
+ .option('-v, --verbose', 'Increase log detail', false)
38
+ .option('--debug', 'Print stack traces for unexpected failures', false)
39
+ .showHelpAfterError();
40
+ registerOnboard(program);
41
+ registerCreate(program);
42
+ registerAdd(program);
43
+ registerValidate(program);
44
+ registerRun(program);
45
+ registerStatus(program);
46
+ registerSkill(program);
47
+ registerVersion(program);
48
+ for (const command of program.commands) {
49
+ const group = HELP_GROUPS[command.name()];
50
+ if (group)
51
+ command.helpGroup(group);
52
+ }
53
+ // Its own instance: the one the subcommands inherited stays under their `Commands:`.
54
+ program.addHelpCommand(new Command('help').argument('[command]').description('Show help for a command').helpGroup('Other:'));
55
+ program.addHelpText('after', examples('mdk onboard # new project: writes mdk.yaml', 'mdk create app ops-center # scaffold an app into apps/ops-center', 'mdk add worker @your-org/mdk-worker-rig # install a worker from npm', 'mdk validate app ops-center # check it before publishing', 'mdk run # start the Kernel, workers, apps and Gateway') + '\nRun `mdk <command> --help` for the options of a command.\n');
56
+ return program;
57
+ }
58
+ //# sourceMappingURL=program.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"program.js","sourceRoot":"","sources":["../src/program.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,GAAG,EAAE,MAAM,cAAc,CAAC;AACnC,OAAO,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AACxD,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AACxD,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AACtD,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAC1D,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AACvD,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACpD,OAAO,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AAErD,+DAA+D;AAC/D,MAAM,WAAW,GAA2B;IAC1C,OAAO,EAAE,SAAS;IAClB,MAAM,EAAE,QAAQ;IAChB,GAAG,EAAE,QAAQ;IACb,QAAQ,EAAE,QAAQ;IAClB,GAAG,EAAE,MAAM;IACX,MAAM,EAAE,MAAM;IACd,KAAK,EAAE,QAAQ;IACf,OAAO,EAAE,QAAQ;CAClB,CAAC;AAEF;;;;;GAKG;AACH,MAAM,UAAU,YAAY;IAC1B,MAAM,OAAO,GAAG,aAAa,CAAC,IAAI,OAAO,EAAE,CAAC,CAAC;IAE7C,OAAO;SACJ,IAAI,CAAC,KAAK,CAAC;SACX,KAAK,CAAC,qBAAqB,CAAC;SAC5B,WAAW,CAAC,+EAA+E,CAAC;SAC5F,OAAO,CAAC,GAAG,CAAC,OAAO,EAAE,WAAW,EAAE,uBAAuB,CAAC;SAC1D,MAAM,CAAC,oBAAoB,EAAE,6CAA6C,EAAE,OAAO,CAAC;SACpF,MAAM,CAAC,eAAe,EAAE,qBAAqB,EAAE,KAAK,CAAC;SACrD,MAAM,CAAC,SAAS,EAAE,4CAA4C,EAAE,KAAK,CAAC;SACtE,kBAAkB,EAAE,CAAC;IAExB,eAAe,CAAC,OAAO,CAAC,CAAC;IACzB,cAAc,CAAC,OAAO,CAAC,CAAC;IACxB,WAAW,CAAC,OAAO,CAAC,CAAC;IACrB,gBAAgB,CAAC,OAAO,CAAC,CAAC;IAC1B,WAAW,CAAC,OAAO,CAAC,CAAC;IACrB,cAAc,CAAC,OAAO,CAAC,CAAC;IACxB,aAAa,CAAC,OAAO,CAAC,CAAC;IACvB,eAAe,CAAC,OAAO,CAAC,CAAC;IAEzB,KAAK,MAAM,OAAO,IAAI,OAAO,CAAC,QAAQ,EAAE,CAAC;QACvC,MAAM,KAAK,GAAG,WAAW,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC,CAAC;QAC1C,IAAI,KAAK;YAAE,OAAO,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC;IACtC,CAAC;IACD,qFAAqF;IACrF,OAAO,CAAC,cAAc,CAAC,IAAI,OAAO,CAAC,MAAM,CAAC,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC,WAAW,CAAC,yBAAyB,CAAC,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAC,CAAC;IAE7H,OAAO,CAAC,WAAW,CACjB,OAAO,EACP,QAAQ,CACN,yEAAyE,EACzE,iFAAiF,EACjF,0EAA0E,EAC1E,uEAAuE,EACvE,wFAAwF,CACzF,GAAG,8DAA8D,CACnE,CAAC;IAEF,OAAO,OAAO,CAAC;AACjB,CAAC","sourcesContent":["import { Command } from 'commander';\nimport { pkg } from './lib/pkg.js';\nimport { configureHelp, examples } from './lib/help.js';\nimport { registerOnboard } from './commands/onboard.js';\nimport { registerCreate } from './commands/create.js';\nimport { registerAdd } from './commands/add.js';\nimport { registerValidate } from './commands/validate.js';\nimport { registerRun } from './commands/run.js';\nimport { registerStatus } from './commands/inspect.js';\nimport { registerSkill } from './commands/agent.js';\nimport { registerVersion } from './commands/meta.js';\n\n/** The help heading each top-level command is listed under. */\nconst HELP_GROUPS: Record<string, string> = {\n onboard: 'Set up:',\n create: 'Build:',\n add: 'Build:',\n validate: 'Build:',\n run: 'Run:',\n status: 'Run:',\n skill: 'Other:',\n version: 'Other:',\n};\n\n/**\n * Builds the full `mdk` command tree. Every command reads `mdk <verb> [noun]`.\n *\n * `onboard`, `create`, `add`, `validate`, `run`, `status`, `skill add` and\n * `version` are implemented.\n */\nexport function buildProgram(): Command {\n const program = configureHelp(new Command());\n\n program\n .name('mdk')\n .usage('<command> [options]')\n .description('Build, run and check an MDK stack: workers, apps, the Kernel and the Gateway.')\n .version(pkg.version, '--version', 'Print the CLI version')\n .option('-o, --output <fmt>', 'Output format for data: table | json | yaml', 'table')\n .option('-v, --verbose', 'Increase log detail', false)\n .option('--debug', 'Print stack traces for unexpected failures', false)\n .showHelpAfterError();\n\n registerOnboard(program);\n registerCreate(program);\n registerAdd(program);\n registerValidate(program);\n registerRun(program);\n registerStatus(program);\n registerSkill(program);\n registerVersion(program);\n\n for (const command of program.commands) {\n const group = HELP_GROUPS[command.name()];\n if (group) command.helpGroup(group);\n }\n // Its own instance: the one the subcommands inherited stays under their `Commands:`.\n program.addHelpCommand(new Command('help').argument('[command]').description('Show help for a command').helpGroup('Other:'));\n\n program.addHelpText(\n 'after',\n examples(\n 'mdk onboard # new project: writes mdk.yaml',\n 'mdk create app ops-center # scaffold an app into apps/ops-center',\n 'mdk add worker @your-org/mdk-worker-rig # install a worker from npm',\n 'mdk validate app ops-center # check it before publishing',\n 'mdk run # start the Kernel, workers, apps and Gateway',\n ) + '\\nRun `mdk <command> --help` for the options of a command.\\n',\n );\n\n return program;\n}\n"]}
package/package.json CHANGED
@@ -1 +1,59 @@
1
- {"name":"@tetherto/mdk-cli","version":"0.0.0"}
1
+ {
2
+ "name": "@tetherto/mdk-cli",
3
+ "version": "0.12.1",
4
+ "description": "MDK command-line tool: onboard, scaffold, run and inspect an MDK stack.",
5
+ "type": "module",
6
+ "bin": {
7
+ "mdk": "dist/index.js"
8
+ },
9
+ "files": [
10
+ "dist",
11
+ "templates"
12
+ ],
13
+ "engines": {
14
+ "node": ">=24"
15
+ },
16
+ "scripts": {
17
+ "build": "tsc -p tsconfig.json",
18
+ "dev": "tsx src/index.ts",
19
+ "start": "node dist/index.js",
20
+ "clean": "rm -rf dist",
21
+ "test": "vitest run",
22
+ "test:watch": "vitest",
23
+ "test:coverage": "vitest run --coverage",
24
+ "test:e2e": "vitest run --config vitest.e2e.config.ts",
25
+ "test:e2e:stack": "vitest run --config vitest.e2e.config.ts tests/e2e/stack.e2e.ts tests/e2e/split.e2e.ts",
26
+ "test:e2e:scaffold": "vitest run --config vitest.e2e.config.ts tests/e2e/scaffold.e2e.ts",
27
+ "prepack": "npm run clean && npm run build"
28
+ },
29
+ "license": "Apache-2.0",
30
+ "dependencies": {
31
+ "@clack/prompts": "^1.7.0",
32
+ "@tetherto/mdk-app-runtime": "^0.12.1",
33
+ "@tetherto/mdk-client": "^0.12.1",
34
+ "@tetherto/mdk-core": "^0.12.1",
35
+ "@tetherto/mdk-gateway": "^0.12.1",
36
+ "@tetherto/mdk-mcp": "^0.12.1",
37
+ "@tetherto/mdk-skill": "^0.12.1",
38
+ "@tetherto/mdk-worker": "^0.12.1",
39
+ "commander": "^15.0.0",
40
+ "degit": "^3.6.6",
41
+ "picocolors": "^1.1.1",
42
+ "yaml": "^2.9.0"
43
+ },
44
+ "devDependencies": {
45
+ "@types/node": "^26.1.1",
46
+ "@vitest/coverage-v8": "^4.1.10",
47
+ "tsx": "^4.23.1",
48
+ "typescript": "^7.0.2",
49
+ "vitest": "^4.1.10"
50
+ },
51
+ "repository": {
52
+ "type": "git",
53
+ "url": "git+https://github.com/tetherto/mdk.git",
54
+ "directory": "packages/cli"
55
+ },
56
+ "publishConfig": {
57
+ "access": "public"
58
+ }
59
+ }
@@ -0,0 +1,66 @@
1
+ # __PLUGIN_NAME__
2
+
3
+ An MDK **app** — HTTP endpoints that aggregate data from your workers'
4
+ devices over the MDK protocol. An app is an app manifest ([`mdk-app.yaml`](./mdk-app.yaml))
5
+ plus its plugin: a route manifest ([`plugin/mdk-plugin.json`](./plugin/mdk-plugin.json)) and one controller module per route;
6
+ `mdk run` runs it in its own process and the Gateway forwards each route to it. There is no root export.
7
+
8
+ ## Layout
9
+
10
+ ```
11
+ mdk-app.yaml # app manifest: id, version, displayName, permissions, config defaults
12
+ plugin/mdk-plugin.json # route manifest: id, handler, http.method + http.path
13
+ plugin/lib/client.js # ambient MDK client from @tetherto/mdk-gateway/plugin
14
+ plugin/controllers/*.js # one module per route: async (req) => result
15
+ ```
16
+
17
+ Each controller receives `req` (`{ params, query, body, headers }`). Talk to
18
+ the Kernel through the plugin-owned client in `plugin/lib/client.js`, which is built
19
+ from the ambient import override:
20
+
21
+ ```js
22
+ const { config } = require('@tetherto/mdk-gateway/plugin')
23
+ const { createMdkClient } = require('@tetherto/mdk-client')
24
+ module.exports = createMdkClient(config)
25
+ ```
26
+
27
+ The app runtime freezes a per-plugin context onto
28
+ `@tetherto/mdk-gateway/plugin` (same pattern as `backend/apps/agent`), so
29
+ `config` already includes `kernelKey` / `kernelBootstrap` plus the `config`
30
+ defaults in `mdk-app.yaml`, overridden by this app's `spec.apps[].config`
31
+ block. Controllers do **not** take `mdkClient` from a `services` parameter.
32
+
33
+ The return value is sent as JSON (HTTP 200), so return plain data: a `Buffer` would arrive as its JSON form. Throw
34
+ `Error('ERR_...')` to return a 400.
35
+
36
+ Mark a route that only reads with `"safety": "read-only"`: `mdk run mcp` turns routes into agent tools and
37
+ withholds every other one as a write unless it is started with `--allow-write`. Mark a route that changes
38
+ devices with `"safety": "write"`.
39
+
40
+ Routes are open by default. Add `"auth": true` to a route to require a caller
41
+ identity: the Gateway then refuses it (401) unless an auth verifier is configured
42
+ and accepts the request.
43
+
44
+ ## Run it
45
+
46
+ This app is registered in your stack's `mdk.yaml` under `spec.apps` by
47
+ **package name** and resolved from `node_modules` (a `file:` dependency in the project's `package.json` links it):
48
+
49
+ ```yaml
50
+ spec:
51
+ apps:
52
+ - name: __PLUGIN_APP__
53
+ package: __PACKAGE_NAME__
54
+ config: {}
55
+ ```
56
+
57
+ Then boot the stack:
58
+
59
+ ```bash
60
+ mdk run # Kernel, workers, this app and the Gateway together
61
+ # or run the app on its own next to `mdk run kernel` and `mdk run gateway`:
62
+ mdk run app __PLUGIN_APP__
63
+ ```
64
+
65
+ Call it: `GET http://localhost:<gatewayPort>/apps/__PLUGIN_APP__/api/__PLUGIN_NAME__/summary`
66
+ (set `mount` on the app entry to serve it under another prefix).
@@ -0,0 +1,10 @@
1
+ # The app manifest `mdk run` reads; `plugin/mdk-plugin.json` holds the route table.
2
+ id: com.example.__PLUGIN_APP__
3
+ version: 0.1.0
4
+ displayName: "__PLUGIN_NAME__"
5
+ # What the app asks the operator to approve, e.g.
6
+ # - resources: [workers, devices]
7
+ # verbs: [list, read]
8
+ permissions: []
9
+ # Defaults for the app's settings, overridden by its `config` in mdk.yaml.
10
+ config: {}
@@ -0,0 +1,17 @@
1
+ {
2
+ "name": "__PLUGIN_NAME__",
3
+ "version": "0.12.1",
4
+ "private": true,
5
+ "description": "MDK app (mdk-app.yaml + plugin/ route manifest and controllers). Run by `mdk run` behind the Gateway — it serves HTTP endpoints that aggregate worker telemetry over the MDK protocol.",
6
+ "keywords": [
7
+ "mdk",
8
+ "mdk-app"
9
+ ],
10
+ "license": "Apache-2.0",
11
+ "engines": {
12
+ "node": ">=24"
13
+ },
14
+ "dependencies": {
15
+ "@tetherto/mdk-client": "^0.12.1"
16
+ }
17
+ }
@@ -0,0 +1,36 @@
1
+ 'use strict'
2
+
3
+ const mdkClient = require('../lib/client')
4
+
5
+ // A read-only aggregation endpoint: it asks the Kernel (through the plugin's
6
+ // ambient MDK client) for the fleet and returns a small summary. Replace this
7
+ // with your own aggregation, grounded in the telemetry channels your workers
8
+ // expose.
9
+ //
10
+ // Contract: `async (req) => result`. The return value is sent as JSON
11
+ // (HTTP 200). Throw `Error('ERR_...')` to return a 400.
12
+ module.exports = async function summary (req) {
13
+ // createMdkClient fails per request (ERR_MDK_CLIENT_UNAVAILABLE) when the
14
+ // Gateway has no Kernel key / connection — report that instead of failing
15
+ // so the route stays callable during boot.
16
+ let workers
17
+ try {
18
+ const resp = await mdkClient.listWorkers()
19
+ workers = (resp && resp.workers) || []
20
+ } catch (err) {
21
+ if (err && err.code === 'ERR_MDK_CLIENT_UNAVAILABLE') {
22
+ return { ok: true, kernelConnected: false, workerCount: 0, deviceCount: 0, workers: [] }
23
+ }
24
+ throw err
25
+ }
26
+
27
+ const deviceCount = workers.reduce((n, w) => n + ((w.deviceIds && w.deviceIds.length) || 0), 0)
28
+
29
+ return {
30
+ ok: true,
31
+ kernelConnected: true,
32
+ workerCount: workers.length,
33
+ deviceCount,
34
+ workers: workers.map((w) => ({ workerId: w.workerId, devices: (w.deviceIds || []).length }))
35
+ }
36
+ }
@@ -0,0 +1,11 @@
1
+ 'use strict'
2
+
3
+ // Plugin-owned MDK client, built from the ambient gateway context. The loader
4
+ // overrides `require('@tetherto/mdk-gateway/plugin')` per plugin (same pattern
5
+ // as backend/apps/agent) so `config` carries this plugin's merged settings
6
+ // including kernelKey / kernelBootstrap. Controllers import this module —
7
+ // they never take mdkClient from a services bag.
8
+ const { config } = require('@tetherto/mdk-gateway/plugin')
9
+ const { createMdkClient } = require('@tetherto/mdk-client')
10
+
11
+ module.exports = createMdkClient(config)
@@ -0,0 +1,16 @@
1
+ {
2
+ "name": "__PLUGIN_NAME__",
3
+ "version": "0.1.0",
4
+ "description": "MDK app plugin — HTTP endpoints that aggregate worker telemetry.",
5
+ "routes": [
6
+ {
7
+ "id": "__PLUGIN_NAME__.summary",
8
+ "handler": "./controllers/summary.js",
9
+ "safety": "read-only",
10
+ "http": {
11
+ "method": "GET",
12
+ "path": "/api/__PLUGIN_NAME__/summary"
13
+ }
14
+ }
15
+ ]
16
+ }
@@ -0,0 +1,29 @@
1
+ // Editor-only: lets you edit the starter UI in an MDK checkout without errors.
2
+ // `mdk create app` copies only ui-src/, and writes the app's own tsconfig there.
3
+ // The MDK packages resolve to this checkout's built ui/packages (npm run build in ui/).
4
+ {
5
+ "compilerOptions": {
6
+ "target": "ES2022",
7
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
8
+ "module": "ESNext",
9
+ "moduleResolution": "bundler",
10
+ "jsx": "react-jsx",
11
+ "strict": true,
12
+ "noUncheckedIndexedAccess": true,
13
+ "skipLibCheck": true,
14
+ "noEmit": true,
15
+ "isolatedModules": true,
16
+ "typeRoots": ["../../../../ui/node_modules/@types"],
17
+ "paths": {
18
+ "@tetherto/mdk-ui-foundation": ["../../../../ui/packages/ui-foundation/dist/index.d.ts"],
19
+ "@tetherto/mdk-ui-foundation/app-shared-modules": ["../../../../ui/packages/ui-foundation/dist/apps/shared-modules.d.ts"],
20
+ "@tetherto/mdk-react-adapter": ["../../../../ui/packages/react-adapter/dist/index.d.ts"],
21
+ "@tetherto/mdk-react-devkit": ["../../../../ui/packages/react-devkit/dist/index.d.ts"],
22
+ "react": ["../../../../ui/node_modules/@types/react"],
23
+ "react/*": ["../../../../ui/node_modules/@types/react/*"],
24
+ "vite": ["../../../../ui/node_modules/vite"],
25
+ "@vitejs/plugin-react": ["../../../../ui/node_modules/@vitejs/plugin-react/dist/index.d.ts"]
26
+ }
27
+ },
28
+ "include": ["ui-src"]
29
+ }