@3minapi/cli 0.0.0-stage → 0.22.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 (100) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +88 -2
  3. package/dist/bin.js +54 -0
  4. package/dist/cli.js +241 -0
  5. package/dist/commands/auth.js +573 -0
  6. package/dist/commands/common.js +48 -0
  7. package/dist/commands/dashboard.js +298 -0
  8. package/dist/commands/delete.js +167 -0
  9. package/dist/commands/doctor.js +326 -0
  10. package/dist/commands/guide.js +84 -0
  11. package/dist/commands/index.js +28 -0
  12. package/dist/commands/init.js +248 -0
  13. package/dist/commands/logs.js +154 -0
  14. package/dist/commands/new.js +109 -0
  15. package/dist/commands/paths.js +64 -0
  16. package/dist/commands/reload.js +32 -0
  17. package/dist/commands/results.js +445 -0
  18. package/dist/commands/start.js +443 -0
  19. package/dist/commands/stop.js +68 -0
  20. package/dist/commands/tasks.js +93 -0
  21. package/dist/commands/worker.js +178 -0
  22. package/dist/core/api.js +132 -0
  23. package/dist/core/config.js +188 -0
  24. package/dist/core/control-db.js +462 -0
  25. package/dist/core/credentials.js +75 -0
  26. package/dist/core/domain/constants.js +121 -0
  27. package/dist/core/domain/contract.js +102 -0
  28. package/dist/core/domain/errors.js +57 -0
  29. package/dist/core/domain/index.js +3 -0
  30. package/dist/core/domain/types.js +1 -0
  31. package/dist/core/error-report.js +103 -0
  32. package/dist/core/host/detach.js +49 -0
  33. package/dist/core/host/live-connection.js +270 -0
  34. package/dist/core/host/live-receiver.js +264 -0
  35. package/dist/core/host/module.js +1 -0
  36. package/dist/core/host/poller.js +113 -0
  37. package/dist/core/host/pull-step.js +54 -0
  38. package/dist/core/host/receiver.js +1 -0
  39. package/dist/core/host/reload.js +34 -0
  40. package/dist/core/host/runner-lock.js +245 -0
  41. package/dist/core/host/session.js +84 -0
  42. package/dist/core/host/usage.js +20 -0
  43. package/dist/core/log-read.js +59 -0
  44. package/dist/core/logger.js +206 -0
  45. package/dist/core/names.js +27 -0
  46. package/dist/core/network.js +10 -0
  47. package/dist/core/open-browser.js +26 -0
  48. package/dist/core/output.js +68 -0
  49. package/dist/core/paths.js +26 -0
  50. package/dist/core/project.js +85 -0
  51. package/dist/core/prompt.js +140 -0
  52. package/dist/core/redact.js +45 -0
  53. package/dist/core/secure-fs.js +50 -0
  54. package/dist/core/text-file.js +12 -0
  55. package/dist/core/text.js +27 -0
  56. package/dist/core/time.js +84 -0
  57. package/dist/core/update-notice.js +142 -0
  58. package/dist/core/version.js +16 -0
  59. package/dist/core/warnings.js +10 -0
  60. package/dist/help.js +144 -0
  61. package/dist/modules/dashboard/assets.js +411 -0
  62. package/dist/modules/dashboard/data.js +565 -0
  63. package/dist/modules/dashboard/guard.js +75 -0
  64. package/dist/modules/dashboard/lock.js +93 -0
  65. package/dist/modules/dashboard/server.js +295 -0
  66. package/dist/modules/dashboard/views/html.js +114 -0
  67. package/dist/modules/dashboard/views/layout.js +59 -0
  68. package/dist/modules/dashboard/views/logs.js +122 -0
  69. package/dist/modules/dashboard/views/overview.js +316 -0
  70. package/dist/modules/dashboard/views/runs.js +486 -0
  71. package/dist/modules/dashboard/views/workers.js +157 -0
  72. package/dist/modules/workers/builtin/host-identity.js +59 -0
  73. package/dist/modules/workers/builtin/results-worker.js +37 -0
  74. package/dist/modules/workers/local-server.js +244 -0
  75. package/dist/modules/workers/module.js +98 -0
  76. package/dist/modules/workers/reload-result.js +16 -0
  77. package/dist/modules/workers/repeat-scheduler.js +165 -0
  78. package/dist/modules/workers/results/missing-paths.js +66 -0
  79. package/dist/modules/workers/results/query.js +175 -0
  80. package/dist/modules/workers/results/reader-input.js +134 -0
  81. package/dist/modules/workers/results/reader.js +430 -0
  82. package/dist/modules/workers/results/results-child.js +64 -0
  83. package/dist/modules/workers/results/shape.js +212 -0
  84. package/dist/modules/workers/results/store.js +336 -0
  85. package/dist/modules/workers/results/substitute.js +30 -0
  86. package/dist/modules/workers/runtime/env.js +43 -0
  87. package/dist/modules/workers/runtime/error-chain.js +109 -0
  88. package/dist/modules/workers/runtime/executor.js +289 -0
  89. package/dist/modules/workers/runtime/inspect.js +53 -0
  90. package/dist/modules/workers/runtime/protocol.js +1 -0
  91. package/dist/modules/workers/runtime/resolve.js +30 -0
  92. package/dist/modules/workers/runtime/schema.js +85 -0
  93. package/dist/modules/workers/runtime/thread.js +117 -0
  94. package/dist/modules/workers/start.js +265 -0
  95. package/dist/modules/workers/sync.js +241 -0
  96. package/dist/modules/workers/task-runner.js +892 -0
  97. package/dist/modules/workers/template.js +100 -0
  98. package/dist/modules/workers/worker-set.js +122 -0
  99. package/dist/modules/workers/workers.js +66 -0
  100. package/package.json +48 -5
@@ -0,0 +1,109 @@
1
+ const MAX_DEPTH = 5;
2
+ const MAX_AGGREGATE_ITEMS = 3;
3
+ const NON_ERROR_MAX_CHARS = 200;
4
+ const CHAIN = ' ← ';
5
+ export function errorMessage(err) {
6
+ if (!(err instanceof Error))
7
+ return String(err);
8
+ const head = err.message || err.name;
9
+ const inner = describeCauses(err, 1, new Set([err]));
10
+ return join(head, inner);
11
+ }
12
+ export function errorStack(err) {
13
+ if (!(err instanceof Error))
14
+ return undefined;
15
+ const lines = [err.stack ?? errorMessage(err)];
16
+ const seen = new Set([err]);
17
+ const walk = (e, depth) => {
18
+ if (depth > MAX_DEPTH)
19
+ return;
20
+ for (const c of causesOf(e)) {
21
+ if (seen.has(c))
22
+ continue;
23
+ if (typeof c === 'object' && c !== null)
24
+ seen.add(c);
25
+ lines.push(`Caused by: ${c instanceof Error ? (c.stack ?? c.message) : nonError(c)}`);
26
+ if (c instanceof Error)
27
+ walk(c, depth + 1);
28
+ }
29
+ };
30
+ walk(err, 1);
31
+ return lines.join('\n');
32
+ }
33
+ export function withoutHome(message, home) {
34
+ if (home.length < 3)
35
+ return message;
36
+ const forms = [...new Set([home, home.replaceAll('\\', '/')])];
37
+ const pattern = forms.map((f) => f.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|');
38
+ return message.replace(new RegExp(`(?:${pattern})(?=[\\\\/'"\`\\s:),;]|$)`, 'g'), '~');
39
+ }
40
+ function causesOf(e) {
41
+ const list = [];
42
+ if (Array.isArray(e.errors))
43
+ list.push(...e.errors);
44
+ if ('cause' in e && e.cause !== undefined)
45
+ list.push(e.cause);
46
+ return list;
47
+ }
48
+ function describeCauses(e, depth, seen) {
49
+ if (depth > MAX_DEPTH)
50
+ return '';
51
+ const parts = [];
52
+ if (Array.isArray(e.errors) && e.errors.length > 0) {
53
+ const items = e.errors
54
+ .slice(0, MAX_AGGREGATE_ITEMS)
55
+ .map((x) => describe(x, depth, seen))
56
+ .filter((s) => s !== '');
57
+ const more = e.errors.length - MAX_AGGREGATE_ITEMS;
58
+ if (more > 0)
59
+ items.push(`and ${more} more`);
60
+ if (items.length > 0)
61
+ parts.push(items.join('; '));
62
+ }
63
+ if ('cause' in e && e.cause !== undefined) {
64
+ const c = describe(e.cause, depth, seen);
65
+ if (c !== '')
66
+ parts.push(c);
67
+ }
68
+ return parts.join(CHAIN);
69
+ }
70
+ function describe(value, depth, seen) {
71
+ if (typeof value === 'object' && value !== null) {
72
+ if (seen.has(value))
73
+ return '';
74
+ seen.add(value);
75
+ }
76
+ if (!(value instanceof Error))
77
+ return nonError(value);
78
+ const e = value;
79
+ const aggregate = Array.isArray(e.errors) && e.errors.length > 0;
80
+ const message = e.message || (aggregate ? '' : e.name);
81
+ const code = typeof e.code === 'string' && !message.includes(e.code) ? e.code : null;
82
+ const head = aggregate && !message ? '' : code ? `${message} (${code})` : message;
83
+ return join(head, describeCauses(e, depth + 1, seen));
84
+ }
85
+ function join(head, inner) {
86
+ if (!head)
87
+ return inner;
88
+ if (!inner)
89
+ return head;
90
+ if (inner === head || inner.startsWith(`${head}${CHAIN}`))
91
+ return inner;
92
+ return `${head}${CHAIN}${inner}`;
93
+ }
94
+ function nonError(value) {
95
+ let text;
96
+ if (typeof value === 'string')
97
+ text = value;
98
+ else if (value === null || typeof value !== 'object')
99
+ text = String(value);
100
+ else {
101
+ try {
102
+ text = JSON.stringify(value) ?? String(value);
103
+ }
104
+ catch {
105
+ text = Object.prototype.toString.call(value);
106
+ }
107
+ }
108
+ return text.length > NON_ERROR_MAX_CHARS ? `${text.slice(0, NON_ERROR_MAX_CHARS)}…` : text;
109
+ }
@@ -0,0 +1,289 @@
1
+ import { getDefaultAutoSelectFamilyAttemptTimeout } from 'node:net';
2
+ import { homedir } from 'node:os';
3
+ import { createInterface } from 'node:readline';
4
+ import { Worker } from 'node:worker_threads';
5
+ import { CANCEL_GRACE_MS, DEFAULT_WORKER_TIMEOUT_MS, ERROR_MESSAGE_MAX_CHARS, STORED_ERROR_MAX_CHARS, TERMINATE_WAIT_MS, WORKER_TIMEOUT_MAX_MS } from "../../../core/domain/index.js";
6
+ import { redactAndTruncate } from "../../../core/redact.js";
7
+ import { errorMessage, errorStack, withoutHome } from "./error-chain.js";
8
+ import { resolveWorkerFile } from "./resolve.js";
9
+ export const THREAD_URL = new URL(import.meta.url.endsWith('.ts') ? './thread.ts' : './thread.js', import.meta.url);
10
+ export class Executor {
11
+ opts;
12
+ active = new Map();
13
+ queue = [];
14
+ stopping = new Set();
15
+ halted = null;
16
+ constructor(opts) {
17
+ this.opts = opts;
18
+ }
19
+ get size() {
20
+ return this.active.size + this.queue.length;
21
+ }
22
+ get concurrency() {
23
+ return this.opts.concurrency;
24
+ }
25
+ get free() {
26
+ return Math.max(0, this.opts.concurrency - this.size);
27
+ }
28
+ has(id) {
29
+ return this.active.has(id) || this.queue.some((q) => q.job.id === id);
30
+ }
31
+ run(job, hooks = {}) {
32
+ return new Promise((resolve) => {
33
+ if (this.halted !== null)
34
+ return resolve({ status: 'cancelled', reason: this.halted });
35
+ const entry = { job, hooks, resolve };
36
+ if (this.active.size < this.opts.concurrency)
37
+ this.start(entry);
38
+ else
39
+ this.queue.push(entry);
40
+ });
41
+ }
42
+ cancel(id, reason = 'cancelled') {
43
+ const i = this.queue.findIndex((q) => q.job.id === id);
44
+ if (i !== -1) {
45
+ const [q] = this.queue.splice(i, 1);
46
+ q.resolve({ status: 'cancelled', reason });
47
+ return true;
48
+ }
49
+ const a = this.active.get(id);
50
+ if (!a)
51
+ return false;
52
+ a.cancel(reason);
53
+ return true;
54
+ }
55
+ async cancelAll(reason) {
56
+ this.halted ??= reason;
57
+ this.drainQueue(reason);
58
+ for (const a of [...this.active.values()])
59
+ a.cancel(reason);
60
+ await this.idle();
61
+ }
62
+ async haltAll(reason) {
63
+ this.halted ??= reason;
64
+ this.drainQueue(reason);
65
+ for (const a of [...this.active.values()])
66
+ a.kill(reason);
67
+ await this.idle();
68
+ }
69
+ async idle() {
70
+ while (this.active.size > 0 || this.queue.length > 0 || this.stopping.size > 0) {
71
+ await Promise.race([new Promise((r) => setTimeout(r, 20)), ...this.stopping]);
72
+ }
73
+ }
74
+ drainQueue(reason) {
75
+ for (const q of this.queue.splice(0))
76
+ q.resolve({ status: 'cancelled', reason });
77
+ }
78
+ next() {
79
+ while (this.halted === null &&
80
+ this.queue.length > 0 &&
81
+ this.active.size < this.opts.concurrency) {
82
+ this.start(this.queue.shift());
83
+ }
84
+ }
85
+ start({ job, hooks, resolve }) {
86
+ const builtin = this.opts.builtin?.(job.worker) ?? null;
87
+ if (builtin) {
88
+ this.started(job, hooks);
89
+ return this.startBuiltin(job, builtin, resolve);
90
+ }
91
+ const found = resolveWorkerFile(this.opts.workersDir, job.worker);
92
+ if (!found.ok) {
93
+ resolve({ status: 'failed', reason: found.reason, message: found.message });
94
+ return;
95
+ }
96
+ const log = (message, level) => {
97
+ hooks.onLog?.(message, level);
98
+ this.opts.logger?.worker(job.worker, level, message, {
99
+ task_id: job.id,
100
+ ...(job.run !== undefined ? { run_no: job.run } : {})
101
+ });
102
+ };
103
+ let env;
104
+ try {
105
+ env = typeof this.opts.env === 'function' ? this.opts.env() : this.opts.env;
106
+ }
107
+ catch (err) {
108
+ resolve({ status: 'failed', reason: 'invalid_worker', message: err.message });
109
+ return;
110
+ }
111
+ const workerData = {
112
+ file: found.file,
113
+ input: job.input,
114
+ connectAttemptTimeoutMs: getDefaultAutoSelectFamilyAttemptTimeout()
115
+ };
116
+ this.started(job, hooks);
117
+ const thread = new Worker(THREAD_URL, {
118
+ workerData,
119
+ env,
120
+ execArgv: [],
121
+ stdout: true,
122
+ stderr: true,
123
+ name: `3minapi:${job.worker}`
124
+ });
125
+ pipeLines(thread.stdout, (line) => log(line, 'info'));
126
+ pipeLines(thread.stderr, (line) => log(line, 'warn'));
127
+ const startedAt = Date.now();
128
+ let settled = false;
129
+ let cancelReason = null;
130
+ let timer;
131
+ let grace;
132
+ const settle = (outcome) => {
133
+ if (settled)
134
+ return;
135
+ settled = true;
136
+ if (outcome.status === 'failed') {
137
+ outcome = {
138
+ ...outcome,
139
+ message: redactAndTruncate(outcome.message, ERROR_MESSAGE_MAX_CHARS),
140
+ detail: redactAndTruncate(outcome.message, STORED_ERROR_MAX_CHARS)
141
+ };
142
+ }
143
+ clearTimeout(timer);
144
+ clearTimeout(grace);
145
+ this.active.delete(job.id);
146
+ this.stopThread(thread, job);
147
+ resolve(cancelReason !== null ? { status: 'cancelled', reason: cancelReason } : outcome);
148
+ this.next();
149
+ };
150
+ const arm = (ms) => {
151
+ clearTimeout(timer);
152
+ timer = setTimeout(() => settle({
153
+ status: 'failed',
154
+ reason: 'timeout',
155
+ message: `did not finish within ${ms} ms`
156
+ }), Math.max(0, ms - (Date.now() - startedAt)));
157
+ };
158
+ arm(DEFAULT_WORKER_TIMEOUT_MS);
159
+ thread.on('message', (msg) => {
160
+ switch (msg.type) {
161
+ case 'loaded':
162
+ if (msg.timeoutMs !== null)
163
+ arm(Math.min(msg.timeoutMs, WORKER_TIMEOUT_MAX_MS));
164
+ return;
165
+ case 'log':
166
+ log(msg.message, 'info');
167
+ return;
168
+ case 'report': {
169
+ const ack = () => {
170
+ if (!settled)
171
+ post(thread, { type: 'report-ack', id: msg.id });
172
+ };
173
+ Promise.resolve(hooks.onReport?.(msg.message)).then(ack, ack);
174
+ return;
175
+ }
176
+ case 'done':
177
+ return settle({ status: 'completed', resultJson: msg.json });
178
+ case 'fail':
179
+ if (msg.stack)
180
+ log(msg.stack, 'error');
181
+ return settle({ status: 'failed', reason: msg.reason, message: msg.message });
182
+ }
183
+ });
184
+ thread.on('error', (err) => {
185
+ const stack = errorStack(err);
186
+ if (stack)
187
+ log(stack, 'error');
188
+ settle({
189
+ status: 'failed',
190
+ reason: 'handler_error',
191
+ message: `uncaught error: ${withoutHome(errorMessage(err), homedir())}`
192
+ });
193
+ });
194
+ thread.on('exit', (code) => settle({
195
+ status: 'failed',
196
+ reason: 'handler_error',
197
+ message: `the worker exited before returning a result (exit code ${code})`
198
+ }));
199
+ this.active.set(job.id, {
200
+ cancel: (reason) => {
201
+ if (settled || cancelReason !== null)
202
+ return;
203
+ cancelReason = reason;
204
+ post(thread, { type: 'cancel' });
205
+ grace = setTimeout(() => settle({ status: 'cancelled', reason }), this.opts.cancelGraceMs ?? CANCEL_GRACE_MS);
206
+ },
207
+ kill: (reason) => {
208
+ cancelReason ??= reason;
209
+ settle({ status: 'cancelled', reason });
210
+ }
211
+ });
212
+ }
213
+ started(job, hooks) {
214
+ try {
215
+ hooks.onStart?.();
216
+ }
217
+ catch (err) {
218
+ this.opts.logger?.warn('task start hook failed', {
219
+ task_id: job.id,
220
+ message: err?.message ?? String(err)
221
+ });
222
+ }
223
+ }
224
+ startBuiltin(job, run, resolve) {
225
+ const controller = new AbortController();
226
+ let settled = false;
227
+ let cancelReason = null;
228
+ const settle = (outcome) => {
229
+ if (settled)
230
+ return;
231
+ settled = true;
232
+ if (outcome.status === 'failed') {
233
+ outcome = {
234
+ ...outcome,
235
+ message: redactAndTruncate(outcome.message, ERROR_MESSAGE_MAX_CHARS),
236
+ detail: redactAndTruncate(outcome.message, STORED_ERROR_MAX_CHARS)
237
+ };
238
+ }
239
+ this.active.delete(job.id);
240
+ resolve(cancelReason !== null ? { status: 'cancelled', reason: cancelReason } : outcome);
241
+ this.next();
242
+ };
243
+ const stop = (reason) => {
244
+ if (settled)
245
+ return;
246
+ cancelReason ??= reason;
247
+ controller.abort();
248
+ settle({ status: 'cancelled', reason });
249
+ };
250
+ this.active.set(job.id, { cancel: stop, kill: stop });
251
+ run(job.input, controller.signal).then(settle, (err) => settle({
252
+ status: 'failed',
253
+ reason: 'handler_error',
254
+ message: `the built-in worker failed: ${err?.message ?? String(err)}`
255
+ }));
256
+ }
257
+ stopThread(thread, job) {
258
+ const wait = this.opts.terminateWaitMs ?? TERMINATE_WAIT_MS;
259
+ let timeout;
260
+ const done = Promise.race([
261
+ thread.terminate().then(() => true),
262
+ new Promise((r) => {
263
+ timeout = setTimeout(() => r(false), wait);
264
+ })
265
+ ]).then((stopped) => {
266
+ clearTimeout(timeout);
267
+ if (!stopped) {
268
+ this.opts.logger?.warn('worker thread did not stop; it may be blocked in native or synchronous code', {
269
+ worker: job.worker,
270
+ task_id: job.id
271
+ });
272
+ }
273
+ });
274
+ this.stopping.add(done);
275
+ void done.finally(() => this.stopping.delete(done));
276
+ }
277
+ }
278
+ function post(thread, msg) {
279
+ try {
280
+ thread.postMessage(msg);
281
+ }
282
+ catch {
283
+ }
284
+ }
285
+ function pipeLines(stream, onLine) {
286
+ if (!stream)
287
+ return;
288
+ createInterface({ input: stream, crlfDelay: Infinity }).on('line', onLine);
289
+ }
@@ -0,0 +1,53 @@
1
+ import { getDefaultAutoSelectFamilyAttemptTimeout } from 'node:net';
2
+ import { Worker } from 'node:worker_threads';
3
+ import { ERROR_MESSAGE_MAX_CHARS } from "../../../core/domain/index.js";
4
+ import { redactAndTruncate } from "../../../core/redact.js";
5
+ import { THREAD_URL } from "./executor.js";
6
+ export const INSPECT_TIMEOUT_MS = 10_000;
7
+ export function inspectWorker(file, env, timeoutMs = INSPECT_TIMEOUT_MS) {
8
+ return new Promise((resolve) => {
9
+ const workerData = {
10
+ file,
11
+ input: null,
12
+ inspect: true,
13
+ connectAttemptTimeoutMs: getDefaultAutoSelectFamilyAttemptTimeout()
14
+ };
15
+ const thread = new Worker(THREAD_URL, {
16
+ workerData,
17
+ env,
18
+ execArgv: [],
19
+ stdout: true,
20
+ stderr: true
21
+ });
22
+ thread.stdout.resume();
23
+ thread.stderr.resume();
24
+ let settled = false;
25
+ const settle = (result) => {
26
+ if (settled)
27
+ return;
28
+ settled = true;
29
+ clearTimeout(timer);
30
+ void thread.terminate();
31
+ resolve(result.ok
32
+ ? result
33
+ : { ...result, message: redactAndTruncate(result.message, ERROR_MESSAGE_MAX_CHARS) });
34
+ };
35
+ const timer = setTimeout(() => settle({
36
+ ok: false,
37
+ reason: 'invalid_worker',
38
+ message: `loading did not finish within ${timeoutMs} ms`
39
+ }), timeoutMs);
40
+ thread.on('message', (msg) => {
41
+ if (msg.type === 'meta')
42
+ settle({ ok: true, meta: msg.meta });
43
+ else if (msg.type === 'fail')
44
+ settle({ ok: false, reason: msg.reason, message: msg.message });
45
+ });
46
+ thread.on('error', (err) => settle({ ok: false, reason: 'invalid_worker', message: `cannot load: ${err.message}` }));
47
+ thread.on('exit', (code) => settle({
48
+ ok: false,
49
+ reason: 'invalid_worker',
50
+ message: `exited while loading (exit code ${code})`
51
+ }));
52
+ });
53
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,30 @@
1
+ import { existsSync } from 'node:fs';
2
+ import path from 'node:path';
3
+ import { WORKER_EXTENSIONS } from "../../../core/domain/index.js";
4
+ import { checkWorkerName, invalidNameMessage } from "../../../core/names.js";
5
+ export function resolveWorkerFile(workersDir, name) {
6
+ const check = checkWorkerName(name);
7
+ if (!check.ok) {
8
+ return {
9
+ ok: false,
10
+ reason: 'invalid_worker',
11
+ message: invalidNameMessage(name, check.reason)
12
+ };
13
+ }
14
+ const found = WORKER_EXTENSIONS.map((ext) => path.join(workersDir, name + ext)).filter((f) => existsSync(f));
15
+ if (found.length === 0) {
16
+ return {
17
+ ok: false,
18
+ reason: 'worker_not_found',
19
+ message: `no worker file for "${name}" in the ${path.basename(workersDir)} folder (expected ${name}${WORKER_EXTENSIONS.join(` / ${name}`)})`
20
+ };
21
+ }
22
+ if (found.length > 1) {
23
+ return {
24
+ ok: false,
25
+ reason: 'invalid_worker',
26
+ message: `more than one file for worker "${name}": ${found.map((f) => path.basename(f)).join(', ')}. Keep only one.`
27
+ };
28
+ }
29
+ return { ok: true, file: found[0] };
30
+ }
@@ -0,0 +1,85 @@
1
+ export function validate(schema, value, at = 'input') {
2
+ if (!isObject(schema))
3
+ return null;
4
+ const s = schema;
5
+ if (s.type !== undefined) {
6
+ const types = Array.isArray(s.type) ? s.type : [s.type];
7
+ if (!types.some((t) => matchesType(t, value))) {
8
+ return `${at}: expected ${types.join(' or ')}, got ${typeOf(value)}`;
9
+ }
10
+ }
11
+ if (Array.isArray(s.enum) && !s.enum.some((option) => deepEqual(option, value))) {
12
+ return `${at}: must be one of ${s.enum.map((v) => JSON.stringify(v)).join(', ')}`;
13
+ }
14
+ if (isObject(value)) {
15
+ const obj = value;
16
+ if (Array.isArray(s.required)) {
17
+ for (const key of s.required) {
18
+ if (typeof key === 'string' && !(key in obj))
19
+ return `${at}.${key}: required`;
20
+ }
21
+ }
22
+ if (isObject(s.properties)) {
23
+ for (const [key, sub] of Object.entries(s.properties)) {
24
+ if (!(key in obj))
25
+ continue;
26
+ const reason = validate(sub, obj[key], `${at}.${key}`);
27
+ if (reason)
28
+ return reason;
29
+ }
30
+ }
31
+ }
32
+ if (Array.isArray(value) && isObject(s.items)) {
33
+ for (let i = 0; i < value.length; i++) {
34
+ const reason = validate(s.items, value[i], `${at}[${i}]`);
35
+ if (reason)
36
+ return reason;
37
+ }
38
+ }
39
+ return null;
40
+ }
41
+ function matchesType(type, value) {
42
+ switch (type) {
43
+ case 'object':
44
+ return isObject(value);
45
+ case 'array':
46
+ return Array.isArray(value);
47
+ case 'string':
48
+ return typeof value === 'string';
49
+ case 'number':
50
+ return typeof value === 'number' && Number.isFinite(value);
51
+ case 'integer':
52
+ return Number.isInteger(value);
53
+ case 'boolean':
54
+ return typeof value === 'boolean';
55
+ case 'null':
56
+ return value === null;
57
+ default:
58
+ return true;
59
+ }
60
+ }
61
+ function typeOf(value) {
62
+ if (value === null)
63
+ return 'null';
64
+ if (Array.isArray(value))
65
+ return 'array';
66
+ if (Number.isInteger(value))
67
+ return 'integer';
68
+ return typeof value;
69
+ }
70
+ function isObject(value) {
71
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
72
+ }
73
+ function deepEqual(a, b) {
74
+ if (a === b)
75
+ return true;
76
+ if (typeof a !== 'object' || typeof b !== 'object' || a === null || b === null)
77
+ return false;
78
+ if (Array.isArray(a) !== Array.isArray(b))
79
+ return false;
80
+ const ka = Object.keys(a);
81
+ const kb = Object.keys(b);
82
+ if (ka.length !== kb.length)
83
+ return false;
84
+ return ka.every((k) => deepEqual(a[k], b[k]));
85
+ }
@@ -0,0 +1,117 @@
1
+ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
2
+ if (typeof path === "string" && /^\.\.?\//.test(path)) {
3
+ return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
4
+ return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
5
+ });
6
+ }
7
+ return path;
8
+ };
9
+ import { realpathSync } from 'node:fs';
10
+ import { setDefaultAutoSelectFamilyAttemptTimeout } from 'node:net';
11
+ import { homedir } from 'node:os';
12
+ import path from 'node:path';
13
+ import { parentPort, workerData } from 'node:worker_threads';
14
+ import { pathToFileURL } from 'node:url';
15
+ import { errorMessage, errorStack, withoutHome } from "./error-chain.js";
16
+ import { validate } from "./schema.js";
17
+ const port = parentPort;
18
+ const { file, input, inspect, connectAttemptTimeoutMs } = workerData;
19
+ if (connectAttemptTimeoutMs !== undefined)
20
+ setDefaultAutoSelectFamilyAttemptTimeout(connectAttemptTimeoutMs);
21
+ const controller = new AbortController();
22
+ const pendingReports = new Map();
23
+ let nextReportId = 1;
24
+ function post(msg) {
25
+ port.postMessage(msg);
26
+ }
27
+ port.on('message', (msg) => {
28
+ if (msg.type === 'cancel')
29
+ controller.abort(new Error('task cancelled'));
30
+ else if (msg.type === 'report-ack') {
31
+ pendingReports.get(msg.id)?.();
32
+ pendingReports.delete(msg.id);
33
+ }
34
+ });
35
+ const ctx = Object.freeze({
36
+ log(message) {
37
+ post({ type: 'log', message: String(message) });
38
+ },
39
+ report(message) {
40
+ const id = nextReportId++;
41
+ return new Promise((resolve) => {
42
+ pendingReports.set(id, resolve);
43
+ post({ type: 'report', id, message: String(message) });
44
+ });
45
+ },
46
+ signal: controller.signal,
47
+ env: process.env
48
+ });
49
+ function withoutPath(message, file) {
50
+ let real = file;
51
+ try {
52
+ real = realpathSync(file);
53
+ }
54
+ catch {
55
+ }
56
+ const forms = [...new Set([real, file].flatMap((p) => [pathToFileURL(p).href, p]))].sort((a, b) => b.length - a.length);
57
+ let out = message;
58
+ for (const form of forms)
59
+ out = out.split(form).join(path.basename(file));
60
+ return out;
61
+ }
62
+ function fail(reason, message, stack) {
63
+ post({ type: 'fail', reason, message, stack });
64
+ }
65
+ async function main() {
66
+ let mod;
67
+ try {
68
+ mod = (await import(__rewriteRelativeImportExtension(pathToFileURL(file).href)));
69
+ }
70
+ catch (err) {
71
+ const e = err;
72
+ if (e?.code === 'ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX' ||
73
+ e?.code === 'ERR_INVALID_TYPESCRIPT_SYNTAX') {
74
+ return fail('unsupported_syntax', `${e.message.split('\n')[0]} — this TypeScript syntax cannot run without a build step. ` +
75
+ 'Use plain type annotations: replace enums with const objects and avoid namespaces and constructor parameter properties, or write the worker in JavaScript.');
76
+ }
77
+ return fail('invalid_worker', `cannot load ${path.basename(file)}: ${withoutPath(e?.message ?? String(err), file)}`);
78
+ }
79
+ const handler = mod.handler;
80
+ if (typeof handler !== 'function') {
81
+ return fail('invalid_worker', `${path.basename(file)} must export an async function named "handler"`);
82
+ }
83
+ const raw = mod.timeoutMs;
84
+ const timeoutMs = typeof raw === 'number' && Number.isFinite(raw) && raw > 0 ? raw : null;
85
+ if (inspect) {
86
+ const description = mod.description;
87
+ return post({
88
+ type: 'meta',
89
+ meta: {
90
+ description: typeof description === 'string' ? description : null,
91
+ timeoutMs,
92
+ hasInputSchema: typeof mod.inputSchema === 'object' && mod.inputSchema !== null
93
+ }
94
+ });
95
+ }
96
+ post({ type: 'loaded', timeoutMs });
97
+ const invalid = validate(mod.inputSchema, input);
98
+ if (invalid)
99
+ return fail('invalid_input', `input does not match the worker's inputSchema — ${invalid}`);
100
+ let result;
101
+ try {
102
+ result = await handler(input, ctx);
103
+ }
104
+ catch (err) {
105
+ const message = withoutHome(withoutPath(errorMessage(err), file), homedir());
106
+ return fail('handler_error', message, errorStack(err));
107
+ }
108
+ let json;
109
+ try {
110
+ json = JSON.stringify(result ?? null);
111
+ }
112
+ catch (err) {
113
+ return fail('handler_error', `the handler's return value is not JSON-serializable: ${err.message}`);
114
+ }
115
+ post({ type: 'done', json: json ?? 'null' });
116
+ }
117
+ await main();