@tetherto/mdk-cli 0.0.0-stage → 0.12.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (112) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +446 -2
  3. package/dist/commands/add.js +61 -0
  4. package/dist/commands/add.js.map +1 -0
  5. package/dist/commands/agent.js +42 -0
  6. package/dist/commands/agent.js.map +1 -0
  7. package/dist/commands/create.js +261 -0
  8. package/dist/commands/create.js.map +1 -0
  9. package/dist/commands/inspect.js +171 -0
  10. package/dist/commands/inspect.js.map +1 -0
  11. package/dist/commands/meta.js +12 -0
  12. package/dist/commands/meta.js.map +1 -0
  13. package/dist/commands/onboard.js +447 -0
  14. package/dist/commands/onboard.js.map +1 -0
  15. package/dist/commands/run.js +203 -0
  16. package/dist/commands/run.js.map +1 -0
  17. package/dist/commands/validate.js +125 -0
  18. package/dist/commands/validate.js.map +1 -0
  19. package/dist/index.js +9 -0
  20. package/dist/index.js.map +1 -0
  21. package/dist/lib/add-package.js +148 -0
  22. package/dist/lib/add-package.js.map +1 -0
  23. package/dist/lib/app-package.js +182 -0
  24. package/dist/lib/app-package.js.map +1 -0
  25. package/dist/lib/app-scaffold.js +221 -0
  26. package/dist/lib/app-scaffold.js.map +1 -0
  27. package/dist/lib/app-ui-check.js +178 -0
  28. package/dist/lib/app-ui-check.js.map +1 -0
  29. package/dist/lib/app-ui-runner.js +122 -0
  30. package/dist/lib/app-ui-runner.js.map +1 -0
  31. package/dist/lib/catalog.js +69 -0
  32. package/dist/lib/catalog.js.map +1 -0
  33. package/dist/lib/checkout.js +14 -0
  34. package/dist/lib/checkout.js.map +1 -0
  35. package/dist/lib/dashboard-apps.js +71 -0
  36. package/dist/lib/dashboard-apps.js.map +1 -0
  37. package/dist/lib/dashboard.js +318 -0
  38. package/dist/lib/dashboard.js.map +1 -0
  39. package/dist/lib/detect.js +38 -0
  40. package/dist/lib/detect.js.map +1 -0
  41. package/dist/lib/env-file.js +57 -0
  42. package/dist/lib/env-file.js.map +1 -0
  43. package/dist/lib/fetch-template.js +33 -0
  44. package/dist/lib/fetch-template.js.map +1 -0
  45. package/dist/lib/help.js +22 -0
  46. package/dist/lib/help.js.map +1 -0
  47. package/dist/lib/names.js +36 -0
  48. package/dist/lib/names.js.map +1 -0
  49. package/dist/lib/npm.js +113 -0
  50. package/dist/lib/npm.js.map +1 -0
  51. package/dist/lib/package-source.js +76 -0
  52. package/dist/lib/package-source.js.map +1 -0
  53. package/dist/lib/pkg.js +5 -0
  54. package/dist/lib/pkg.js.map +1 -0
  55. package/dist/lib/plugin-setup.js +204 -0
  56. package/dist/lib/plugin-setup.js.map +1 -0
  57. package/dist/lib/ports.js +31 -0
  58. package/dist/lib/ports.js.map +1 -0
  59. package/dist/lib/project.js +297 -0
  60. package/dist/lib/project.js.map +1 -0
  61. package/dist/lib/run-lock.js +188 -0
  62. package/dist/lib/run-lock.js.map +1 -0
  63. package/dist/lib/runtime.js +683 -0
  64. package/dist/lib/runtime.js.map +1 -0
  65. package/dist/lib/scaffold.js +82 -0
  66. package/dist/lib/scaffold.js.map +1 -0
  67. package/dist/lib/shutdown.js +53 -0
  68. package/dist/lib/shutdown.js.map +1 -0
  69. package/dist/lib/skill.js +38 -0
  70. package/dist/lib/skill.js.map +1 -0
  71. package/dist/lib/spec.js +375 -0
  72. package/dist/lib/spec.js.map +1 -0
  73. package/dist/lib/status.js +245 -0
  74. package/dist/lib/status.js.map +1 -0
  75. package/dist/lib/theme.js +50 -0
  76. package/dist/lib/theme.js.map +1 -0
  77. package/dist/lib/worker-package.js +74 -0
  78. package/dist/lib/worker-package.js.map +1 -0
  79. package/dist/lib/worker-scaffold.js +122 -0
  80. package/dist/lib/worker-scaffold.js.map +1 -0
  81. package/dist/lib/worker-smoke.js +243 -0
  82. package/dist/lib/worker-smoke.js.map +1 -0
  83. package/dist/program.js +58 -0
  84. package/dist/program.js.map +1 -0
  85. package/package.json +57 -4
  86. package/templates/app/README.md +66 -0
  87. package/templates/app/mdk-app.yaml +10 -0
  88. package/templates/app/package.json +17 -0
  89. package/templates/app/plugin/controllers/summary.js +36 -0
  90. package/templates/app/plugin/lib/client.js +11 -0
  91. package/templates/app/plugin/mdk-plugin.json +16 -0
  92. package/templates/app-ui/tsconfig.json +29 -0
  93. package/templates/app-ui/ui-src/components/summary-panel.tsx +24 -0
  94. package/templates/app-ui/ui-src/data/summary.ts +18 -0
  95. package/templates/app-ui/ui-src/hooks/use-summary.ts +54 -0
  96. package/templates/app-ui/ui-src/index.tsx +15 -0
  97. package/templates/app-ui/ui-src/pages/overview-page.tsx +18 -0
  98. package/templates/app-ui/ui-src/styles.css +13 -0
  99. package/templates/app-ui/ui-src/styles.d.ts +3 -0
  100. package/templates/app-ui/ui-src/vite.config.ts +26 -0
  101. package/templates/worker/README.md +93 -0
  102. package/templates/worker/mdk-contract.json +26 -0
  103. package/templates/worker/mock/server.js +93 -0
  104. package/templates/worker/package.json +23 -0
  105. package/templates/worker/smoke.config.js +31 -0
  106. package/templates/worker/src/client.js +47 -0
  107. package/templates/worker/src/commands/reboot.js +5 -0
  108. package/templates/worker/src/commands/set-power-mode.js +10 -0
  109. package/templates/worker/src/telemetry/hashrate-rt.js +5 -0
  110. package/templates/worker/src/telemetry/power-mode.js +5 -0
  111. package/templates/worker/src/telemetry/power.js +5 -0
  112. package/templates/worker/src/telemetry/temperature.js +5 -0
@@ -0,0 +1,24 @@
1
+ // Component layer (@tetherto/mdk-react-devkit): renders what the hook shaped.
2
+ // No fetching, no data logic.
3
+ import { EmptyState, ErrorCard, Loader, SingleStatCard } from '@tetherto/mdk-react-devkit'
4
+
5
+ import type { SummaryView } from '../hooks/use-summary'
6
+
7
+ export const SummaryPanel = ({ view }: { view: SummaryView }) => {
8
+ switch (view.status) {
9
+ case 'loading':
10
+ return <Loader />
11
+ case 'error':
12
+ return <ErrorCard title="Could not load the summary" error={view.message} />
13
+ case 'no-kernel':
14
+ return <EmptyState description="The backend is up, but not connected to a Kernel yet." size="sm" />
15
+ case 'ready':
16
+ return (
17
+ <div className="__APP_CSS__-summary">
18
+ {view.stats.map((stat) => (
19
+ <SingleStatCard key={stat.label} name={stat.label} value={stat.value} />
20
+ ))}
21
+ </div>
22
+ )
23
+ }
24
+ }
@@ -0,0 +1,18 @@
1
+ // Data layer (@tetherto/mdk-ui-foundation): the contract with the app's own
2
+ // backend — the response type, its query key and the request. No React here.
3
+ import { mdkFetch } from '@tetherto/mdk-ui-foundation'
4
+
5
+ /** What the app's `summary` route returns (`plugin/controllers/summary.js`, `GET /api/<name>/summary`). */
6
+ export type Summary = {
7
+ ok: boolean
8
+ kernelConnected: boolean
9
+ workerCount: number
10
+ deviceCount: number
11
+ workers: Array<{ workerId: string; devices: number }>
12
+ }
13
+
14
+ /** Query key for the summary at one URL. */
15
+ export const summaryQueryKey = (url: string) => ['__APP_NAME__', 'summary', url] as const
16
+
17
+ /** GET the summary, with the signed-in operator's token. */
18
+ export const fetchSummary = (url: string): Promise<Summary> => mdkFetch<Summary>(url)
@@ -0,0 +1,54 @@
1
+ // Hook layer (@tetherto/mdk-react-adapter): fetches the summary and shapes it
2
+ // into exactly what the component renders, including loading and error states.
3
+ import { MdkFetchError } from '@tetherto/mdk-ui-foundation'
4
+ import { useMdkApp, useQuery } from '@tetherto/mdk-react-adapter'
5
+
6
+ import { fetchSummary, summaryQueryKey } from '../data/summary'
7
+
8
+ export type SummaryView =
9
+ | { status: 'loading' }
10
+ | { status: 'error'; message: string }
11
+ | { status: 'no-kernel' }
12
+ | { status: 'ready'; stats: Array<{ label: string; value: number }> }
13
+
14
+ // The Gateway answered, but nothing serves this route: the app's runtime is
15
+ // not running, or the route is not in plugin/mdk-plugin.json.
16
+ const NOT_SERVED = 404
17
+ // The Gateway or the app's runtime did not answer (a dev proxy reports a
18
+ // stopped Gateway as 500).
19
+ const UNREACHABLE = new Set([500, 502, 503, 504])
20
+
21
+ const describeError = (error: unknown, url: string): string => {
22
+ if (error instanceof MdkFetchError && error.status === NOT_SERVED) {
23
+ return `Nothing serves ${url} yet (HTTP 404). The Gateway is up, but this app's backend runtime is not running or does not have this route.`
24
+ }
25
+ if (error instanceof MdkFetchError && UNREACHABLE.has(error.status)) {
26
+ return `The app's backend is not reachable (HTTP ${error.status}). Start the stack with \`mdk run\`.`
27
+ }
28
+ return error instanceof Error ? error.message : String(error)
29
+ }
30
+
31
+ /** The app's summary, ready to render. */
32
+ export const useSummary = (): SummaryView => {
33
+ // The app's routes live under the name the operator installed it as, so the
34
+ // URL comes from useMdkApp() rather than being hardcoded.
35
+ const { apiUrl } = useMdkApp()
36
+ const url = apiUrl('/api/__APP_NAME__/summary')
37
+
38
+ const query = useQuery({
39
+ queryKey: summaryQueryKey(url),
40
+ queryFn: () => fetchSummary(url),
41
+ retry: false,
42
+ })
43
+
44
+ if (query.isPending) return { status: 'loading' }
45
+ if (query.isError) return { status: 'error', message: describeError(query.error, url) }
46
+ if (!query.data.kernelConnected) return { status: 'no-kernel' }
47
+ return {
48
+ status: 'ready',
49
+ stats: [
50
+ { label: 'Workers', value: query.data.workerCount },
51
+ { label: 'Devices', value: query.data.deviceCount },
52
+ ],
53
+ }
54
+ }
@@ -0,0 +1,15 @@
1
+ // The app's UI entry, built to ../ui/index.js (`npm run build:ui` in the app). The dashboard
2
+ // imports it and mounts what it exports: pages under /app/<name>/ and panels on
3
+ // its own pages.
4
+ import { defineMdkApp, MDK_APP_PANEL_SLOT } from '@tetherto/mdk-react-adapter'
5
+
6
+ import { OverviewPage, SummaryHomePanel } from './pages/overview-page'
7
+ import './styles.css'
8
+
9
+ export default defineMdkApp({
10
+ // Must equal `id` in mdk-app.yaml: the dashboard refuses a mismatch.
11
+ id: '__APP_ID__',
12
+ displayName: '__APP_DISPLAY_NAME__',
13
+ routes: [{ path: '', component: OverviewPage }],
14
+ panels: [{ id: 'summary', slot: MDK_APP_PANEL_SLOT.HOME, component: SummaryHomePanel }],
15
+ })
@@ -0,0 +1,18 @@
1
+ // Page: thin glue — read the hook, hand its output to the component.
2
+ import { Typography } from '@tetherto/mdk-react-devkit'
3
+
4
+ import { SummaryPanel } from '../components/summary-panel'
5
+ import { useSummary } from '../hooks/use-summary'
6
+
7
+ export const OverviewPage = () => (
8
+ <section className="__APP_CSS__-page">
9
+ <Typography variant="heading1">__APP_DISPLAY_NAME__</Typography>
10
+ <Typography variant="secondary">
11
+ Your app&apos;s starter page. Edit ui-src/ and rebuild with npm run build:ui.
12
+ </Typography>
13
+ <SummaryPanel view={useSummary()} />
14
+ </section>
15
+ )
16
+
17
+ /** The same summary as a card on the dashboard's Home. */
18
+ export const SummaryHomePanel = () => <SummaryPanel view={useSummary()} />
@@ -0,0 +1,13 @@
1
+ /* Built to ui/style.css; the dashboard adds it to the page with the app. */
2
+ .__APP_CSS__-page {
3
+ display: flex;
4
+ flex-direction: column;
5
+ gap: 16px;
6
+ padding: 24px;
7
+ }
8
+
9
+ .__APP_CSS__-summary {
10
+ display: flex;
11
+ flex-wrap: wrap;
12
+ gap: 16px;
13
+ }
@@ -0,0 +1,3 @@
1
+ // `index.tsx` imports its stylesheet for the side effect; Vite bundles the CSS.
2
+ // TypeScript 7 checks side-effect imports, so it needs to know they resolve.
3
+ declare module '*.css'
@@ -0,0 +1,26 @@
1
+ // Builds index.tsx to ../ui/index.js (+ ../ui/style.css) for the MDK dashboard.
2
+ // Run from the app root with `npm run build:ui`.
3
+ //
4
+ // React and the MDK packages stay external: the dashboard lends the app its own
5
+ // instances through an import map, so the app ships none of them (a second
6
+ // React would break hooks).
7
+ import { MDK_APP_SHARED_MODULES } from '@tetherto/mdk-ui-foundation/app-shared-modules'
8
+ import react from '@vitejs/plugin-react'
9
+ import { defineConfig } from 'vite'
10
+
11
+ export default defineConfig({
12
+ plugins: [react()],
13
+ define: { 'process.env.NODE_ENV': JSON.stringify('production') },
14
+ build: {
15
+ // The app package ships ui/, not this source: build one level up.
16
+ outDir: '../ui',
17
+ emptyOutDir: true,
18
+ lib: {
19
+ entry: 'index.tsx',
20
+ formats: ['es'],
21
+ fileName: () => 'index.js',
22
+ cssFileName: 'style',
23
+ },
24
+ rollupOptions: { external: [...MDK_APP_SHARED_MODULES] },
25
+ },
26
+ })
@@ -0,0 +1,93 @@
1
+ # __WORKER_NAME__
2
+
3
+ An MDK **Worker Plugin** — the code that translates between MDK and a family of
4
+ physical devices. It ships only an [`mdk-contract.json`](./mdk-contract.json) (its capability manifest)
5
+ plus [`src/`](./src/) handler files, a device "vendor SDK" client, and a mock device
6
+ server. It exports no module of its own; it is **directory-loaded** by
7
+ `WorkerRuntimeV2`, which the MDK CLI constructs (`mdk run worker <name>`) — the
8
+ package never constructs or depends on the runtime itself.
9
+
10
+ This scaffold is a working example that talks to a hypothetical device over a
11
+ tiny HTTP JSON API. Replace the device client, mock, contract, and handlers with
12
+ your own device's protocol.
13
+
14
+ ## Layout
15
+
16
+ ```
17
+ mdk-contract.json # capability manifest (telemetry channels + commands)
18
+ src/client.js # your device "vendor SDK" — binds to this device once, per instance
19
+ src/telemetry/*.js # telemetry handlers (metrics read live from the device)
20
+ src/commands/*.js # command handlers (reboot, set-power-mode, ...)
21
+ mock/server.js # a standalone device simulator (createServer)
22
+ smoke.config.js # the device spec `mdk validate worker --smoke` checks against
23
+ ```
24
+
25
+ Handlers are plain `(params)` functions — they call `require('../client')` for
26
+ their device's client, which itself binds once (at `require` time) to the
27
+ ambient `@tetherto/mdk-worker/device` for this instance's connection opts.
28
+ There is one such module registry per configured device, so each handler
29
+ always talks to its own device.
30
+
31
+ ## Run it
32
+
33
+ Add the worker to your stack's `mdk.yaml` by **local path** and describe its
34
+ seed devices under `config`:
35
+
36
+ ```yaml
37
+ spec:
38
+ workers:
39
+ - name: __WORKER_NAME__
40
+ package: ./workers/__WORKER_NAME__
41
+ config:
42
+ mock: true # start mock/server.js per device (dev only)
43
+ devices:
44
+ - id: dev-0
45
+ opts: { host: 127.0.0.1, port: 18080, serial: DEV-0 }
46
+ ```
47
+
48
+ Then:
49
+
50
+ ```bash
51
+ mdk run worker __WORKER_NAME__ # boots the worker (+ its mocks with mock: true)
52
+ ```
53
+
54
+ With `mock: true` the CLI starts one simulator from [`mock/server.js`](./mock/server.js) per device
55
+ so the worker talks to a simulator instead of hardware. Drop `mock` (or set it
56
+ `false`) and point `opts.host`/`opts.port` at real devices for production.
57
+
58
+ ## Check it
59
+
60
+ ```bash
61
+ mdk validate worker __WORKER_NAME__ # the contract and handler files, without running them
62
+ mdk validate worker --path . --smoke # also runs every handler against the simulator
63
+ ```
64
+
65
+ `--smoke` starts the simulator through [`smoke.config.js`](./smoke.config.js), calls every telemetry
66
+ handler and each sample command it lists, then stops the simulator and expects every handler to fail
67
+ with an `ERR_*` error code. Add a sample to `commands` for each command you add, unless it interrupts
68
+ the device.
69
+
70
+ ## Builtin per-device health check
71
+
72
+ `WorkerRuntimeV2` answers a `telemetry.pull` query of `type: 'health'` for any
73
+ device automatically — no contract or handler changes needed. It works by
74
+ registering `health` as a plain handler in the same table any contract-declared
75
+ telemetry channel lives in, so it is dispatched with zero special-casing, the
76
+ same way `status`/`hashrate_rt`/etc. would be. That means it behaves exactly
77
+ like any other named channel:
78
+
79
+ - Requires a `deviceId` — a query with no `deviceId` returns
80
+ `{ error: 'ERR_DEVICE_ID_REQUIRED: health' }`, same as any other named channel.
81
+ - The response comes back in the standard named-channel shape,
82
+ `{ name: 'health', value: { status, id, opts, env, config, workerId } }` —
83
+ that device's own ambient config, useful for verifying that Gateway → Kernel
84
+ → Worker routing reaches the right device instance (e.g. a query addressed
85
+ to `dev-3` always comes back with `dev-3`'s own `opts`, never a sibling
86
+ device's).
87
+ - It also shows up as a `health` key inside the default `type: 'metrics'`
88
+ aggregate for every device, since that aggregate iterates the very same
89
+ handler table.
90
+
91
+ If your own contract declares a `health` telemetry channel, that one is used
92
+ instead — the builtin never overrides a plugin-declared handler of the same
93
+ name.
@@ -0,0 +1,26 @@
1
+ {
2
+ "metadata": {
3
+ "provider": "demo",
4
+ "deviceFamily": "device",
5
+ "brand": "DEMO",
6
+ "modelsSupported": ["DEMO_V1"],
7
+ "overview": "Template Worker Plugin — talks to a device's HTTP JSON API over src/client.js. Replace the client, mock, contract and handlers with your own device's protocol."
8
+ },
9
+ "capabilities": {
10
+ "telemetry": [
11
+ { "name": "hashrate_rt", "unit": "TH/s", "type": "number", "handler": "src/telemetry/hashrate-rt.js", "description": "Real-time throughput from the device's summary endpoint." },
12
+ { "name": "power", "unit": "W", "type": "number", "handler": "src/telemetry/power.js", "description": "Current power draw." },
13
+ { "name": "temperature", "unit": "C", "type": "number", "handler": "src/telemetry/temperature.js", "description": "Device temperature." },
14
+ { "name": "power_mode", "type": "string", "handler": "src/telemetry/power-mode.js", "description": "Active power mode: eco, normal or high." }
15
+ ],
16
+ "commands": [
17
+ { "name": "reboot", "handler": "src/commands/reboot.js", "description": "Restarts the device controller.", "constraints": "Do not call more than once per 5 minutes.", "params": [] },
18
+ { "name": "setPowerMode", "handler": "src/commands/set-power-mode.js", "description": "Changes the power mode: eco, normal or high (low is accepted as an alias for eco).", "params": [{ "name": "mode", "type": "string" }] }
19
+ ],
20
+ "health": { "supportedStates": ["OK", "OFFLINE"] },
21
+ "errors": {
22
+ "ERR_BAD_POWER_MODE": "The device rejected the requested power mode.",
23
+ "ERR_DEVICE_CALL_FAILED": "The device's HTTP API call failed or returned an error."
24
+ }
25
+ }
26
+ }
@@ -0,0 +1,93 @@
1
+ 'use strict'
2
+
3
+ const http = require('http')
4
+ const debug = require('debug')('mdk:worker:__WORKER_NAME__:mock')
5
+
6
+ // A standalone device simulator: a small HTTP JSON API (GET /api/v3/summary,
7
+ // POST /api/v3/command) matching src/client.js. It knows nothing about MDK —
8
+ // it is the device-side surface the Worker Plugin translates against, the way
9
+ // a real device on the LAN would be. Replace with your own device's protocol.
10
+
11
+ const POWER_MODES = {
12
+ eco: { power: 0.8, hashrate: 0.85 },
13
+ normal: { power: 1, hashrate: 1 },
14
+ high: { power: 1.15, hashrate: 1.1 }
15
+ }
16
+
17
+ function jitter (value) {
18
+ return Math.round(value * (0.99 + Math.random() * 0.02) * 100) / 100
19
+ }
20
+
21
+ function createServer ({ host, port, serial, hashrateThs, powerW }) {
22
+ const state = {
23
+ serial: serial || 'DEV-0000',
24
+ baseHashrateThs: hashrateThs || 180,
25
+ basePowerW: powerW || 3400,
26
+ powerMode: 'normal',
27
+ bootTime: Date.now()
28
+ }
29
+
30
+ const summary = () => {
31
+ const scale = POWER_MODES[state.powerMode]
32
+ return {
33
+ serial: state.serial,
34
+ model: 'DEMO_V1',
35
+ firmware: 'v1.0.0',
36
+ uptime_s: Math.floor((Date.now() - state.bootTime) / 1000),
37
+ power_mode: state.powerMode,
38
+ hashrate_ths: jitter(state.baseHashrateThs * scale.hashrate),
39
+ power_w: jitter(state.basePowerW * scale.power),
40
+ board_temp_c: jitter(62 + (scale.power - 1) * 20)
41
+ }
42
+ }
43
+
44
+ const command = (body) => {
45
+ switch (body.cmd) {
46
+ case 'reboot':
47
+ state.bootTime = Date.now()
48
+ return { ok: true, rebooting: true }
49
+ case 'set-power-mode':
50
+ if (!POWER_MODES[body.mode]) throw new Error(`ERR_BAD_POWER_MODE: ${body.mode}`)
51
+ state.powerMode = body.mode
52
+ return { ok: true, power_mode: state.powerMode }
53
+ default:
54
+ throw new Error(`ERR_UNKNOWN_CMD: ${body.cmd}`)
55
+ }
56
+ }
57
+
58
+ const server = http.createServer((req, res) => {
59
+ const reply = (code, body) => {
60
+ res.writeHead(code, { 'content-type': 'application/json' })
61
+ res.end(JSON.stringify(body))
62
+ }
63
+
64
+ if (req.method === 'GET' && req.url === '/api/v3/summary') return reply(200, summary())
65
+
66
+ if (req.method === 'POST' && req.url === '/api/v3/command') {
67
+ let buf = ''
68
+ req.on('data', (chunk) => { buf += chunk })
69
+ req.on('end', () => {
70
+ try {
71
+ reply(200, command(JSON.parse(buf || '{}')))
72
+ } catch (err) {
73
+ reply(400, { ok: false, error: err.message })
74
+ }
75
+ })
76
+ return
77
+ }
78
+
79
+ reply(404, { ok: false, error: 'ERR_NOT_FOUND' })
80
+ })
81
+
82
+ server.listen(port, host || '127.0.0.1', () => {
83
+ debug('device mock %s listening on %s:%d', state.serial, host, port)
84
+ })
85
+
86
+ return {
87
+ server,
88
+ state,
89
+ exit () { server.close() }
90
+ }
91
+ }
92
+
93
+ module.exports = { createServer }
@@ -0,0 +1,23 @@
1
+ {
2
+ "name": "__WORKER_NAME__",
3
+ "version": "0.12.1",
4
+ "description": "MDK Worker Plugin — an mdk-contract.json plus src/ handler files, loaded from this directory and instantiated per device by WorkerRuntimeV2 (see `mdk run worker`). It exports no module of its own.",
5
+ "keywords": [
6
+ "mdk",
7
+ "mdk-worker"
8
+ ],
9
+ "license": "Apache-2.0",
10
+ "engines": {
11
+ "node": ">=24"
12
+ },
13
+ "scripts": {
14
+ "mock": "node -e \"require('./mock/server').createServer({ host: '127.0.0.1', port: 18080, serial: 'DEV-0' })\""
15
+ },
16
+ "dependencies": {
17
+ "@tetherto/mdk-worker": "^0.12.1",
18
+ "debug": "4.4.1"
19
+ },
20
+ "devDependencies": {
21
+ "brittle": "3.18.0"
22
+ }
23
+ }
@@ -0,0 +1,31 @@
1
+ 'use strict'
2
+
3
+ // Device spec for `mdk validate worker --path . --smoke`: setup() starts the
4
+ // simulator from mock/server.js and returns how to reach it, one sample param
5
+ // set per command to execute, and teardown() to stop it. After teardown() the
6
+ // smoke run checks that every handler fails with an ERR_* instead of hanging
7
+ // or returning a stale value.
8
+ //
9
+ // reboot is left out of `commands` on purpose: nobody watches the device
10
+ // during an automated smoke run, so keep commands that interrupt it out.
11
+ // A telemetry channel served from the worker's own store (a `history` log,
12
+ // say) keeps answering after teardown(); list it as
13
+ // `offlineTelemetry: ['history']` next to `teardown`.
14
+
15
+ const { once } = require('node:events')
16
+ const { createServer } = require('./mock/server')
17
+
18
+ module.exports = {
19
+ async setup () {
20
+ const mock = createServer({ host: '127.0.0.1', port: 0, serial: 'SMOKE-0' })
21
+ await once(mock.server, 'listening')
22
+
23
+ return {
24
+ config: { host: '127.0.0.1', port: mock.server.address().port },
25
+ commands: {
26
+ setPowerMode: { mode: 'normal' }
27
+ },
28
+ teardown: () => mock.exit()
29
+ }
30
+ }
31
+ }
@@ -0,0 +1,47 @@
1
+ 'use strict'
2
+
3
+ const { opts, env, config, logger } = require('@tetherto/mdk-worker/device')
4
+
5
+ // The "vendor SDK" for your device's own HTTP JSON API. Plain device I/O — no
6
+ // MDK concepts, no base classes. This is all a plugin author writes to talk
7
+ // to their own device; replace it with your real protocol.
8
+ //
9
+ // The runtime loads this module fresh into a private registry per device, so
10
+ // binding directly to the ambient device module here — rather than exporting
11
+ // a `createClient(opts)` factory for a caller to invoke — already yields one
12
+ // client per device with no extra plumbing.
13
+ // Log only non-secret fields and key names: `env` carries credentials such as
14
+ // DEVICE_TOKEN, and `opts`/`config` may too — never log their values.
15
+ logger('config received: host=%s port=%s env keys=%o config keys=%o',
16
+ opts.host, opts.port, Object.keys(env || {}), Object.keys(config || {}))
17
+
18
+ const base = `http://${opts.host || '127.0.0.1'}:${opts.port}`
19
+ const auth = env.DEVICE_TOKEN ? { authorization: `Bearer ${env.DEVICE_TOKEN}` } : {}
20
+
21
+ const call = async (path, callOpts) => {
22
+ let res, body
23
+ try {
24
+ res = await fetch(base + path, { ...callOpts, headers: { ...auth, ...(callOpts && callOpts.headers) } })
25
+ body = await res.json()
26
+ } catch (err) {
27
+ // An unreachable device or a non-JSON reply: surface it as this plugin's
28
+ // own error code, not fetch's bare "fetch failed".
29
+ throw new Error(`ERR_DEVICE_CALL_FAILED: ${(err.cause && err.cause.code) || err.message}`)
30
+ }
31
+ if (!res.ok || body.ok === false) {
32
+ throw new Error(body.error || `ERR_DEVICE_CALL_FAILED: ${res.status}`)
33
+ }
34
+ return body
35
+ }
36
+
37
+ const command = (cmd, args) => call('/api/v3/command', {
38
+ method: 'POST',
39
+ headers: { 'content-type': 'application/json' },
40
+ body: JSON.stringify({ cmd, ...args })
41
+ })
42
+
43
+ module.exports = {
44
+ getSummary: () => call('/api/v3/summary'),
45
+ reboot: () => command('reboot'),
46
+ setPowerMode: (mode) => command('set-power-mode', { mode })
47
+ }
@@ -0,0 +1,5 @@
1
+ 'use strict'
2
+
3
+ const client = require('../client')
4
+
5
+ module.exports = async () => client.reboot()
@@ -0,0 +1,10 @@
1
+ 'use strict'
2
+
3
+ const client = require('../client')
4
+
5
+ module.exports = async (params) => {
6
+ // Example vendor-agnostic-to-firmware translation: this device's API calls
7
+ // low "eco". Adjust to your own device's vocabulary.
8
+ const mode = params.mode === 'low' ? 'eco' : params.mode
9
+ return client.setPowerMode(mode)
10
+ }
@@ -0,0 +1,5 @@
1
+ 'use strict'
2
+
3
+ const client = require('../client')
4
+
5
+ module.exports = async () => (await client.getSummary()).hashrate_ths
@@ -0,0 +1,5 @@
1
+ 'use strict'
2
+
3
+ const client = require('../client')
4
+
5
+ module.exports = async () => (await client.getSummary()).power_mode
@@ -0,0 +1,5 @@
1
+ 'use strict'
2
+
3
+ const client = require('../client')
4
+
5
+ module.exports = async () => (await client.getSummary()).power_w
@@ -0,0 +1,5 @@
1
+ 'use strict'
2
+
3
+ const client = require('../client')
4
+
5
+ module.exports = async () => (await client.getSummary()).board_temp_c