@nmakarov/cli-toolkit 0.16.0 → 0.21.0

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 (49) hide show
  1. package/README.md +9 -0
  2. package/dist/cli-runner.cjs +5335 -0
  3. package/dist/cli-runner.cjs.map +1 -0
  4. package/dist/cli-runner.js +5318 -0
  5. package/dist/cli-runner.js.map +1 -0
  6. package/dist/db.cjs +1 -1
  7. package/dist/db.cjs.map +1 -1
  8. package/dist/db.js +1 -1
  9. package/dist/db.js.map +1 -1
  10. package/dist/filedatabase.cjs +94 -13
  11. package/dist/filedatabase.cjs.map +1 -1
  12. package/dist/filedatabase.js +94 -13
  13. package/dist/filedatabase.js.map +1 -1
  14. package/dist/http-client2.cjs +1375 -11
  15. package/dist/http-client2.cjs.map +1 -1
  16. package/dist/http-client2.js +1365 -11
  17. package/dist/http-client2.js.map +1 -1
  18. package/dist/index.cjs +1639 -30
  19. package/dist/index.cjs.map +1 -1
  20. package/dist/index.js +1608 -29
  21. package/dist/index.js.map +1 -1
  22. package/dist/init.cjs +25 -7
  23. package/dist/init.cjs.map +1 -1
  24. package/dist/init.js +25 -7
  25. package/dist/init.js.map +1 -1
  26. package/dist/logger.cjs +9 -6
  27. package/dist/logger.cjs.map +1 -1
  28. package/dist/logger.js +9 -6
  29. package/dist/logger.js.map +1 -1
  30. package/dist/mock-server.cjs +192 -343
  31. package/dist/mock-server.cjs.map +1 -1
  32. package/dist/mock-server.js +190 -343
  33. package/dist/mock-server.js.map +1 -1
  34. package/dist/params.cjs +16 -1
  35. package/dist/params.cjs.map +1 -1
  36. package/dist/params.js +16 -1
  37. package/dist/params.js.map +1 -1
  38. package/dist/tasks.cjs +2666 -0
  39. package/dist/tasks.cjs.map +1 -0
  40. package/dist/tasks.js +2600 -0
  41. package/dist/tasks.js.map +1 -0
  42. package/dist/utils.cjs +15 -2
  43. package/dist/utils.cjs.map +1 -1
  44. package/dist/utils.js +12 -1
  45. package/dist/utils.js.map +1 -1
  46. package/package.json +20 -5
  47. package/scripts/ssm/parse-cli.ts +35 -0
  48. package/scripts/ssm/ssm-admin.ts +151 -0
  49. package/scripts/ssm/ssm-pull.ts +150 -0
package/dist/tasks.cjs ADDED
@@ -0,0 +1,2666 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/tasks.ts
31
+ var tasks_exports = {};
32
+ __export(tasks_exports, {
33
+ TaskMaster: () => TaskMaster,
34
+ TaskPing: () => TaskPing,
35
+ TaskSampleProcess: () => TaskSampleProcess,
36
+ TaskShellCommand: () => TaskShellCommand,
37
+ TaskStopRunner: () => TaskStopRunner,
38
+ TaskSumAB: () => TaskSumAB,
39
+ TaskSystemInfo: () => TaskSystemInfo,
40
+ TasksManager: () => TasksManager,
41
+ TasksRegistry: () => TasksRegistry,
42
+ appendTaskIpcLog: () => appendTaskIpcLog,
43
+ defaultTasksRegistry: () => defaultTasksRegistry,
44
+ enqueueStopTask: () => enqueueStopTask,
45
+ enqueueTask: () => enqueueTask,
46
+ ensureTaskTables: () => ensureTaskTables,
47
+ listAliveRunnerHeartbeats: () => listServicesRegistry,
48
+ listServicesRegistry: () => listServicesRegistry,
49
+ queueToTableNames: () => queueToTableNames,
50
+ registerInServicesRegistry: () => registerInServicesRegistry,
51
+ registerRunnerHeartbeat: () => registerInServicesRegistry,
52
+ runNodeTaskScript: () => runNodeTaskScript,
53
+ runTasksLoop: () => runTasksLoop,
54
+ runnerHeartbeatsTable: () => servicesRegistryTable,
55
+ servicesRegistryTable: () => servicesRegistryTable,
56
+ touchRunnerHeartbeat: () => touchServicesRegistry,
57
+ touchServicesRegistry: () => touchServicesRegistry,
58
+ unregisterRunnerHeartbeat: () => unregisterServicesRegistry,
59
+ unregisterServicesRegistry: () => unregisterServicesRegistry,
60
+ updateServicesRegistryMetadata: () => updateServicesRegistryMetadata,
61
+ updateTaskProgress: () => updateTaskProgress,
62
+ waitForTaskResult: () => waitForTaskResult
63
+ });
64
+ module.exports = __toCommonJS(tasks_exports);
65
+
66
+ // src/utils/date-utils.ts
67
+ function isTimestampFolder(folderName) {
68
+ const isoRegex = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(Z|\.\d{3}Z)$/;
69
+ if (!isoRegex.test(folderName)) {
70
+ return false;
71
+ }
72
+ const date = new Date(folderName);
73
+ return !isNaN(date.getTime()) && date.getTime() > 0;
74
+ }
75
+
76
+ // src/utils/fs-utils.ts
77
+ var import_fs = __toESM(require("fs"), 1);
78
+ var import_path = __toESM(require("path"), 1);
79
+ async function ensurePath(...pathParts) {
80
+ const fullPath = import_path.default.resolve(...pathParts);
81
+ if (!import_fs.default.existsSync(fullPath)) {
82
+ await import_fs.default.promises.mkdir(fullPath, { recursive: true });
83
+ }
84
+ return fullPath;
85
+ }
86
+ function getFileExtension(dataType) {
87
+ switch (dataType) {
88
+ case "json-array":
89
+ case "json-object":
90
+ return "json";
91
+ case "text":
92
+ return "txt";
93
+ case "xml":
94
+ return "xml";
95
+ default:
96
+ return "json";
97
+ }
98
+ }
99
+
100
+ // src/utils/os-utils.ts
101
+ var import_fs2 = __toESM(require("fs"), 1);
102
+ var import_path2 = __toESM(require("path"), 1);
103
+ var import_child_process = require("child_process");
104
+ function getFreeDiskSpace(targetPath) {
105
+ try {
106
+ let pathToCheck = targetPath;
107
+ if (!import_fs2.default.existsSync(targetPath)) {
108
+ const parentDir = import_path2.default.dirname(targetPath);
109
+ if (import_fs2.default.existsSync(parentDir)) {
110
+ pathToCheck = parentDir;
111
+ } else {
112
+ pathToCheck = process.platform === "win32" ? "C:\\" : "/";
113
+ }
114
+ }
115
+ if (process.platform === "win32") {
116
+ return null;
117
+ } else {
118
+ const stdout = (0, import_child_process.execSync)(`df -k "${pathToCheck}"`, { encoding: "utf8" });
119
+ const lines = stdout.trim().split("\n");
120
+ const parts = lines[1].split(/\s+/);
121
+ const freeKb = parseInt(parts[3], 10);
122
+ return freeKb * 1024;
123
+ }
124
+ } catch (error) {
125
+ return null;
126
+ }
127
+ }
128
+
129
+ // src/utils/format-utils.ts
130
+ function bytesToHumanReadable(bytes) {
131
+ if (bytes === 0) return "0 B";
132
+ const k = 1024;
133
+ const sizes = ["B", "KB", "MB", "GB", "TB", "PB"];
134
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
135
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
136
+ }
137
+
138
+ // src/utils/core-utils.ts
139
+ function sleepMs(ms) {
140
+ return new Promise((resolve) => setTimeout(resolve, ms));
141
+ }
142
+ function toJsonColumn(value) {
143
+ if (value === void 0 || value === null) return null;
144
+ return JSON.stringify(value);
145
+ }
146
+
147
+ // src/tasks/servicesRegistry.ts
148
+ var import_node_crypto2 = require("crypto");
149
+ var import_promises = require("fs/promises");
150
+ var import_node_os = __toESM(require("os"), 1);
151
+ var import_node_path = __toESM(require("path"), 1);
152
+
153
+ // src/tasks/taskUtils.ts
154
+ var import_node_crypto = require("crypto");
155
+ function getDb(context) {
156
+ const db = context.db;
157
+ if (!db) {
158
+ throw new Error("Tasks component requires context.db. Initialize DB first and attach to context.");
159
+ }
160
+ return db;
161
+ }
162
+ function queueToTableNames(queue) {
163
+ if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(queue)) {
164
+ throw new Error(`Invalid queue name "${queue}". Use letters, numbers, underscore only.`);
165
+ }
166
+ return {
167
+ tasksTable: queue,
168
+ historyTable: `${queue}_history`
169
+ };
170
+ }
171
+ function servicesRegistryTable(queue) {
172
+ if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(queue)) {
173
+ throw new Error(`Invalid queue name "${queue}". Use letters, numbers, underscore only.`);
174
+ }
175
+ return `${queue}_services_registry`;
176
+ }
177
+ async function ensureTaskTables(context, options = {}) {
178
+ const queue = options.queue ?? "tasks";
179
+ const recreate = options.recreate ?? false;
180
+ const db = getDb(context);
181
+ const { tasksTable, historyTable } = queueToTableNames(queue);
182
+ const needsTasks = recreate ? true : !await db.tableExists(tasksTable);
183
+ const needsHistory = recreate ? true : !await db.tableExists(historyTable);
184
+ if (recreate) {
185
+ await db.schema.dropTableIfExists(historyTable);
186
+ await db.schema.dropTableIfExists(tasksTable);
187
+ }
188
+ if (needsTasks) {
189
+ await db.raw(`CREATE EXTENSION IF NOT EXISTS "uuid-ossp"`);
190
+ await db.schema.createTable(tasksTable, (t) => {
191
+ t.uuid("id").primary().defaultTo(db.raw("uuid_generate_v4()"));
192
+ t.timestamp("created_at").notNullable().defaultTo(db.fn.now());
193
+ t.timestamp("started_at");
194
+ t.timestamp("completed_at");
195
+ t.integer("priority").notNullable().defaultTo(0);
196
+ t.text("schedule");
197
+ t.timestamp("past_due").defaultTo(null);
198
+ t.text("target").notNullable();
199
+ t.text("task").notNullable();
200
+ t.json("params");
201
+ t.text("opid");
202
+ t.timestamp("paused_at").defaultTo(null);
203
+ t.text("progress");
204
+ t.boolean("success");
205
+ t.json("results");
206
+ });
207
+ await db.schema.alterTable(tasksTable, (t) => {
208
+ t.index(["target", "started_at", "created_at"], `${tasksTable}_target_started_created_idx`);
209
+ t.index(["target", "past_due", "priority", "created_at"], `${tasksTable}_target_past_due_priority_created_idx`);
210
+ t.index(["target", "task"], `${tasksTable}_target_task_idx`);
211
+ });
212
+ }
213
+ if (needsHistory) {
214
+ await db.schema.createTable(historyTable, (t) => {
215
+ t.uuid("id").notNullable();
216
+ t.timestamp("created_at").notNullable();
217
+ t.timestamp("started_at");
218
+ t.timestamp("completed_at");
219
+ t.integer("priority").notNullable().defaultTo(0);
220
+ t.text("schedule");
221
+ t.timestamp("past_due").defaultTo(null);
222
+ t.text("target").notNullable();
223
+ t.text("task").notNullable();
224
+ t.json("params");
225
+ t.text("opid");
226
+ t.text("progress");
227
+ t.boolean("success");
228
+ t.json("results");
229
+ });
230
+ await db.schema.alterTable(historyTable, (t) => {
231
+ t.index(["target", "created_at"], `${historyTable}_target_created_idx`);
232
+ t.index(["task", "created_at"], `${historyTable}_task_created_idx`);
233
+ });
234
+ }
235
+ const registryTable = servicesRegistryTable(queue);
236
+ const needsRegistry = !await db.tableExists(registryTable);
237
+ if (needsRegistry) {
238
+ await db.schema.createTable(registryTable, (t) => {
239
+ t.uuid("id").primary().defaultTo(db.raw("uuid_generate_v4()"));
240
+ t.uuid("instance_id").notNullable().unique();
241
+ t.text("queue").notNullable();
242
+ t.text("service_group").notNullable();
243
+ t.text("service_name").notNullable();
244
+ t.text("target").notNullable();
245
+ t.text("hostname");
246
+ t.integer("pid");
247
+ t.json("metadata");
248
+ t.timestamp("created_at").notNullable().defaultTo(db.fn.now());
249
+ t.timestamp("last_seen_at").notNullable().defaultTo(db.fn.now());
250
+ t.unique(["queue", "service_name"], `${registryTable}_queue_service_name_uniq`);
251
+ t.index(["queue", "service_group", "last_seen_at"], `${registryTable}_queue_group_seen_idx`);
252
+ t.index(["queue", "last_seen_at"], `${registryTable}_queue_seen_idx`);
253
+ });
254
+ }
255
+ }
256
+ async function enqueueTask(context, options) {
257
+ const db = getDb(context);
258
+ const queue = options.queue ?? "tasks";
259
+ const { tasksTable } = queueToTableNames(queue);
260
+ const id = (0, import_node_crypto.randomUUID)();
261
+ await db(tasksTable).insert({
262
+ id,
263
+ target: options.target,
264
+ task: options.task,
265
+ params: toJsonColumn(options.params ?? null),
266
+ opid: options.opid ?? null,
267
+ priority: options.priority ?? 0,
268
+ schedule: options.schedule ?? null
269
+ });
270
+ return id;
271
+ }
272
+ async function updateTaskProgress(context, tasksTable, taskId, progress) {
273
+ const db = getDb(context);
274
+ await db(tasksTable).where({ id: taskId }).update({
275
+ progress: typeof progress === "string" ? progress : JSON.stringify(progress)
276
+ });
277
+ }
278
+
279
+ // src/tasks/servicesRegistry.ts
280
+ function getDb2(context) {
281
+ const db = context.db;
282
+ if (!db) {
283
+ throw new Error("Services registry requires context.db");
284
+ }
285
+ return db;
286
+ }
287
+ function parseMetadataColumn(value) {
288
+ if (!value) return {};
289
+ if (typeof value === "object" && !Array.isArray(value)) return value;
290
+ if (typeof value === "string") {
291
+ try {
292
+ const p = JSON.parse(value);
293
+ return p && typeof p === "object" && !Array.isArray(p) ? p : {};
294
+ } catch {
295
+ return {};
296
+ }
297
+ }
298
+ return {};
299
+ }
300
+ var DEFAULT_GROUP_MAX_INSTANCES = {
301
+ intake: 1,
302
+ harvest: 1,
303
+ loader: 0,
304
+ photos: 0,
305
+ photosprocessor: 0,
306
+ ingest: 0
307
+ };
308
+ function sanitizeNamePart(raw) {
309
+ const s = String(raw || "").trim().replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
310
+ return s.slice(0, 80) || "runner";
311
+ }
312
+ function identityFilePath(identityDir, queue, serviceGroup) {
313
+ const safeQ = sanitizeNamePart(queue);
314
+ const safeG = sanitizeNamePart(serviceGroup);
315
+ return import_node_path.default.join(identityDir, `${safeQ}_${safeG}.json`);
316
+ }
317
+ async function readIdentityFile(filePath) {
318
+ try {
319
+ const text = await (0, import_promises.readFile)(filePath, "utf8");
320
+ const parsed = JSON.parse(text);
321
+ return parsed && typeof parsed === "object" ? parsed : {};
322
+ } catch {
323
+ return {};
324
+ }
325
+ }
326
+ async function writeIdentityFile(filePath, data) {
327
+ await (0, import_promises.mkdir)(import_node_path.default.dirname(filePath), { recursive: true });
328
+ await (0, import_promises.writeFile)(filePath, `${JSON.stringify(data, null, 2)}
329
+ `, "utf8");
330
+ }
331
+ function resolveMaxInstances(serviceGroup, override) {
332
+ if (override !== void 0 && Number.isFinite(override)) {
333
+ return Math.max(0, Math.floor(Number(override)));
334
+ }
335
+ const g = serviceGroup.trim().toLowerCase();
336
+ return DEFAULT_GROUP_MAX_INSTANCES[g] ?? 0;
337
+ }
338
+ async function countAliveInGroup(db, registryTable, queue, serviceGroup, staleMs, excludeInstanceId) {
339
+ const cutoff = new Date(Date.now() - staleMs);
340
+ let q = db(registryTable).where({ queue, service_group: serviceGroup }).where("last_seen_at", ">", cutoff);
341
+ if (excludeInstanceId) {
342
+ q = q.whereNot("instance_id", excludeInstanceId);
343
+ }
344
+ const row = await q.count("id as count").first();
345
+ return Number(row?.count ?? 0);
346
+ }
347
+ function isUniqueViolation(error) {
348
+ const code = error?.code ?? error?.errno;
349
+ return code === "23505" || String(error?.message || "").includes("duplicate key");
350
+ }
351
+ async function registerInServicesRegistry(context, options) {
352
+ const db = getDb2(context);
353
+ const registryTable = servicesRegistryTable(options.queue);
354
+ const serviceGroup = options.serviceGroup.trim();
355
+ if (!serviceGroup) {
356
+ throw new Error("registerInServicesRegistry: serviceGroup is required");
357
+ }
358
+ const identityPath = identityFilePath(options.identityDir, options.queue, serviceGroup);
359
+ let identity = await readIdentityFile(identityPath);
360
+ let instanceId = typeof identity.instanceId === "string" && identity.instanceId.trim() ? identity.instanceId.trim() : (0, import_node_crypto2.randomUUID)();
361
+ identity.instanceId = instanceId;
362
+ await writeIdentityFile(identityPath, identity);
363
+ const hostname = import_node_os.default.hostname();
364
+ const pid = typeof process.pid === "number" ? process.pid : null;
365
+ const meta = toJsonColumn(options.metadata ?? null);
366
+ const existing = await db(registryTable).where({ instance_id: instanceId }).first();
367
+ if (existing) {
368
+ await db(registryTable).where({ instance_id: instanceId }).update({
369
+ target: options.target,
370
+ hostname,
371
+ pid,
372
+ metadata: meta,
373
+ last_seen_at: db.fn.now()
374
+ });
375
+ const serviceName = String(existing.service_name);
376
+ identity.serviceName = serviceName;
377
+ await writeIdentityFile(identityPath, identity);
378
+ const reg = {
379
+ instanceId,
380
+ serviceName,
381
+ serviceGroup,
382
+ queue: options.queue,
383
+ target: options.target,
384
+ rowId: String(existing.id)
385
+ };
386
+ context.servicesRegistry = reg;
387
+ context.runnerHeartbeat = reg;
388
+ context.logger.info?.(
389
+ `[services-registry] resumed instance_id=${instanceId} name=${serviceName} group=${serviceGroup} queue=${options.queue}`
390
+ );
391
+ return {
392
+ instanceId,
393
+ serviceName,
394
+ serviceGroup,
395
+ queue: options.queue,
396
+ target: options.target,
397
+ rowId: String(existing.id),
398
+ registryTable
399
+ };
400
+ }
401
+ const maxAllowed = resolveMaxInstances(serviceGroup, options.groupMaxInstances);
402
+ const aliveOthers = await countAliveInGroup(
403
+ db,
404
+ registryTable,
405
+ options.queue,
406
+ serviceGroup,
407
+ options.staleMs,
408
+ instanceId
409
+ );
410
+ if (maxAllowed > 0 && aliveOthers >= maxAllowed) {
411
+ const msg = `[services-registry] group limit reached for "${serviceGroup}": ${aliveOthers} alive (max ${maxAllowed}, queue=${options.queue}).`;
412
+ if (options.enforceMaxInstances) {
413
+ throw new Error(msg);
414
+ }
415
+ context.logger.warn?.(`${msg} Starting anyway (runnerEnforceMaxInstances=false).`);
416
+ }
417
+ const explicitName = options.serviceName?.trim();
418
+ const fromFile = typeof identity.serviceName === "string" ? identity.serviceName.trim() : "";
419
+ const hostBase = sanitizeNamePart(hostname);
420
+ const groupBase = sanitizeNamePart(serviceGroup);
421
+ const baseCandidates = [];
422
+ if (explicitName) baseCandidates.push(sanitizeNamePart(explicitName));
423
+ if (fromFile) baseCandidates.push(sanitizeNamePart(fromFile));
424
+ baseCandidates.push(`${groupBase}-${hostBase}`);
425
+ baseCandidates.push(groupBase);
426
+ function* eachServiceNameCandidate(bases) {
427
+ const seen = /* @__PURE__ */ new Set();
428
+ for (const rawBase of bases) {
429
+ const base = sanitizeNamePart(rawBase);
430
+ if (!base) continue;
431
+ const seq = [base];
432
+ for (let n = 2; n <= 500; n++) seq.push(`${base}-${n}`);
433
+ for (const c of seq) {
434
+ if (seen.has(c)) continue;
435
+ seen.add(c);
436
+ yield c;
437
+ }
438
+ }
439
+ }
440
+ let inserted;
441
+ for (const candidate of eachServiceNameCandidate(baseCandidates)) {
442
+ try {
443
+ const rows = await db(registryTable).insert({
444
+ instance_id: instanceId,
445
+ queue: options.queue,
446
+ service_group: serviceGroup,
447
+ service_name: candidate,
448
+ target: options.target,
449
+ hostname,
450
+ pid,
451
+ metadata: meta,
452
+ last_seen_at: db.fn.now()
453
+ }).returning(["id", "service_name"]);
454
+ const row = Array.isArray(rows) ? rows[0] : rows;
455
+ if (row) {
456
+ inserted = { id: String(row.id), service_name: String(row.service_name) };
457
+ break;
458
+ }
459
+ } catch (error) {
460
+ if (!isUniqueViolation(error)) {
461
+ throw error;
462
+ }
463
+ }
464
+ }
465
+ if (!inserted) {
466
+ throw new Error(
467
+ `[services-registry] could not allocate a unique service_name for group=${serviceGroup} queue=${options.queue} (too many collisions).`
468
+ );
469
+ }
470
+ identity.serviceName = inserted.service_name;
471
+ await writeIdentityFile(identityPath, identity);
472
+ const regNew = {
473
+ instanceId,
474
+ serviceName: inserted.service_name,
475
+ serviceGroup,
476
+ queue: options.queue,
477
+ target: options.target,
478
+ rowId: inserted.id
479
+ };
480
+ context.servicesRegistry = regNew;
481
+ context.runnerHeartbeat = regNew;
482
+ context.logger.info?.(
483
+ `[services-registry] registered instance_id=${instanceId} name=${inserted.service_name} group=${serviceGroup} queue=${options.queue} target=${options.target}`
484
+ );
485
+ return {
486
+ instanceId,
487
+ serviceName: inserted.service_name,
488
+ serviceGroup,
489
+ queue: options.queue,
490
+ target: options.target,
491
+ rowId: inserted.id,
492
+ registryTable
493
+ };
494
+ }
495
+ async function touchServicesRegistry(context, registration) {
496
+ const db = getDb2(context);
497
+ const hostname = import_node_os.default.hostname();
498
+ const pid = typeof process.pid === "number" ? process.pid : null;
499
+ await db(registration.registryTable).where({ instance_id: registration.instanceId }).update({
500
+ last_seen_at: db.fn.now(),
501
+ hostname,
502
+ pid
503
+ });
504
+ }
505
+ async function updateServicesRegistryMetadata(context, registration, patch) {
506
+ const db = getDb2(context);
507
+ const row = await db(registration.registryTable).where({ instance_id: registration.instanceId }).first();
508
+ const prev = parseMetadataColumn(row?.metadata);
509
+ const merged = { ...prev, ...patch };
510
+ await db(registration.registryTable).where({ instance_id: registration.instanceId }).update({
511
+ metadata: toJsonColumn(merged),
512
+ last_seen_at: db.fn.now()
513
+ });
514
+ context.logger.info?.(`[services-registry] metadata updated for ${registration.serviceName}`);
515
+ }
516
+ async function unregisterServicesRegistry(context, registration) {
517
+ const db = getDb2(context);
518
+ await db(registration.registryTable).where({ instance_id: registration.instanceId }).delete();
519
+ context.logger.info?.(`[services-registry] unregistered name=${registration.serviceName} instance_id=${registration.instanceId}`);
520
+ }
521
+ async function listServicesRegistry(context, options = { queue: "tasks" }) {
522
+ const db = getDb2(context);
523
+ const staleMs = options.staleMs ?? 6e4;
524
+ const cutoff = new Date(Date.now() - staleMs);
525
+ const table = servicesRegistryTable(options.queue);
526
+ let q = db(table).where("last_seen_at", ">", cutoff).orderBy([{ column: "service_group", order: "asc" }, { column: "service_name", order: "asc" }]);
527
+ if (options.serviceGroup?.trim()) {
528
+ q = q.where({ service_group: options.serviceGroup.trim() });
529
+ }
530
+ return await q;
531
+ }
532
+
533
+ // src/filedatabase/index.ts
534
+ var import_fs3 = __toESM(require("fs"), 1);
535
+ var import_path3 = __toESM(require("path"), 1);
536
+
537
+ // src/filedatabase/serializers.ts
538
+ function detectDataType(data) {
539
+ if (Array.isArray(data)) {
540
+ return "json-array";
541
+ } else if (typeof data === "object" && data !== null) {
542
+ return "json-object";
543
+ } else if (typeof data === "string") {
544
+ const trimmed = data.trim();
545
+ if (trimmed.startsWith("<?xml") || trimmed.startsWith("<")) {
546
+ return "xml";
547
+ }
548
+ return "text";
549
+ } else {
550
+ return "text";
551
+ }
552
+ }
553
+ function serializeData(data) {
554
+ const dataType = detectDataType(data);
555
+ if (dataType === "json-array" || dataType === "json-object") {
556
+ return JSON.stringify(data, null, 4);
557
+ } else {
558
+ return String(data);
559
+ }
560
+ }
561
+ function deserializeData(rawData, dataType) {
562
+ if (dataType === "json-array" || dataType === "json-object") {
563
+ return JSON.parse(rawData);
564
+ } else {
565
+ return rawData;
566
+ }
567
+ }
568
+
569
+ // src/errors.ts
570
+ var FrameworkError = class extends Error {
571
+ constructor(message) {
572
+ super(message);
573
+ this.name = "FrameworkError";
574
+ }
575
+ };
576
+ var ParamError = class extends FrameworkError {
577
+ constructor(message) {
578
+ super(message);
579
+ this.name = "ParamError";
580
+ }
581
+ };
582
+ var FileDatabaseError = class extends FrameworkError {
583
+ constructor(message) {
584
+ super(message);
585
+ this.name = "FileDatabaseError";
586
+ }
587
+ };
588
+
589
+ // src/filedatabase/index.ts
590
+ var FileDatabase = class _FileDatabase {
591
+ basePath;
592
+ namespace;
593
+ tableName = null;
594
+ versioned;
595
+ maxVersions;
596
+ pageSize;
597
+ useMetadata;
598
+ freeSpaceThreshold;
599
+ logger;
600
+ // Current operation state
601
+ currentVersion = null;
602
+ currentVersionFolder = null;
603
+ currentFileNumber = 0;
604
+ currentRecord = 0;
605
+ hasReadFirstPage = false;
606
+ lastFileData = null;
607
+ metadata;
608
+ // Synopsis calculation functions
609
+ fileSynopsisFunction = null;
610
+ versionSynopsisFunction = null;
611
+ /**
612
+ * Constructor - accepts context as first parameter (new pattern)
613
+ * or config object (legacy pattern for backward compatibility)
614
+ */
615
+ constructor(contextOrConfig, options) {
616
+ let config;
617
+ if (contextOrConfig && typeof contextOrConfig === "object" && "params" in contextOrConfig) {
618
+ const context = contextOrConfig;
619
+ const opts = options || {};
620
+ const defs = {
621
+ basePath: "string default ./data",
622
+ namespace: "string default default",
623
+ tableName: "string",
624
+ maxVersions: "number default 5",
625
+ pageSize: "number default 5000"
626
+ };
627
+ const discovered = context.params.getAllForModule(defs);
628
+ config = { ...discovered, ...opts, logger: context.logger };
629
+ } else {
630
+ config = contextOrConfig;
631
+ }
632
+ if (!config.basePath) {
633
+ throw new ParamError("[FileDatabase] basePath is required");
634
+ }
635
+ this.basePath = config.basePath;
636
+ this.namespace = config.namespace || "default";
637
+ this.tableName = config.tableName || null;
638
+ this.versioned = config.versioned ?? true;
639
+ this.maxVersions = config.maxVersions || 5;
640
+ this.pageSize = config.pageSize || 5e3;
641
+ this.useMetadata = config.useMetadata !== false;
642
+ this.freeSpaceThreshold = config.freeSpaceThreshold || 100 * 1024 * 1024;
643
+ this.logger = config.logger || console;
644
+ this.metadata = this.getDefaultMetadata();
645
+ }
646
+ /**
647
+ * Initialize FileDatabase from context and options.
648
+ * Params are read via getAllForModule("filedatabase", defs) for --showUsedParams grouping.
649
+ */
650
+ static init(context, options) {
651
+ return new _FileDatabase(context, options ?? {});
652
+ }
653
+ /**
654
+ * Get default metadata structure
655
+ */
656
+ getDefaultMetadata() {
657
+ return {
658
+ version: this.currentVersion || null,
659
+ files: [],
660
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
661
+ modifiedAt: (/* @__PURE__ */ new Date()).toISOString(),
662
+ totalRecords: 0,
663
+ synopsis: null,
664
+ dataType: null
665
+ };
666
+ }
667
+ /**
668
+ * Get the destination path (basePath/namespace/tableName[/version])
669
+ */
670
+ getDestinationPath(version) {
671
+ const errors = ["basePath", "namespace", "tableName"].filter((prop) => !this[prop]).map((prop) => `${prop} is not set`);
672
+ if (errors.length) {
673
+ throw new FileDatabaseError(`[FileDatabase] ${errors.join("; ")}`);
674
+ }
675
+ let parts = [this.basePath, this.namespace];
676
+ if (this.tableName) {
677
+ parts.push(...this.tableName.split("/"));
678
+ }
679
+ if (this.versioned && version) {
680
+ parts.push(version);
681
+ }
682
+ return import_path3.default.resolve(...parts);
683
+ }
684
+ /**
685
+ * Set current version and version folder
686
+ */
687
+ async setCurrentVersion(version) {
688
+ this.currentVersion = version;
689
+ this.currentVersionFolder = await ensurePath(this.getDestinationPath(), version);
690
+ }
691
+ /**
692
+ * Create a new version folder with comprehensive timestamp logic
693
+ * Only works in versioned mode
694
+ */
695
+ async makeNewVersion() {
696
+ if (!this.versioned) {
697
+ throw new FileDatabaseError("makeNewVersion() only works in versioned mode");
698
+ }
699
+ this.metadata = this.getDefaultMetadata();
700
+ const existingVersions = await this.getVersions();
701
+ let versionName;
702
+ if (existingVersions.length > 0) {
703
+ const maxTimestamp = existingVersions.reduce((max, version) => {
704
+ const versionDate = new Date(version.replace("Z", ""));
705
+ const maxDate2 = new Date(max.replace("Z", ""));
706
+ return versionDate > maxDate2 ? version : max;
707
+ });
708
+ const maxDate = new Date(maxTimestamp.replace("Z", ""));
709
+ const nextDate = new Date(maxDate.getTime() + 1e3);
710
+ versionName = nextDate.toISOString().split(".")[0] + "Z";
711
+ } else {
712
+ const now = /* @__PURE__ */ new Date();
713
+ versionName = now.toISOString().split(".")[0] + "Z";
714
+ }
715
+ await this.setCurrentVersion(versionName);
716
+ this.currentFileNumber = 0;
717
+ const versions = await this.getVersions();
718
+ while (versions.length > this.maxVersions) {
719
+ const versionToDelete = import_path3.default.resolve(this.getDestinationPath(), versions.shift());
720
+ this.logger.silly?.(`[FileDatabase] Deleting old version: ${versionToDelete}`);
721
+ await import_fs3.default.promises.rm(versionToDelete, { recursive: true, force: true });
722
+ }
723
+ return versionName;
724
+ }
725
+ /**
726
+ * Get list of all versions (sorted chronologically)
727
+ * Only works in versioned mode
728
+ */
729
+ async getVersions() {
730
+ if (!this.versioned) {
731
+ return [];
732
+ }
733
+ const destPath = this.getDestinationPath();
734
+ try {
735
+ await ensurePath(destPath);
736
+ const items = await import_fs3.default.promises.readdir(destPath);
737
+ const versions = items.filter((item) => {
738
+ const itemPath = import_path3.default.join(destPath, item);
739
+ const stat = import_fs3.default.statSync(itemPath);
740
+ return stat.isDirectory() && isTimestampFolder(item);
741
+ });
742
+ return versions.sort();
743
+ } catch (error) {
744
+ return [];
745
+ }
746
+ }
747
+ /**
748
+ * Get the latest version (most recent timestamp)
749
+ * Only works in versioned mode
750
+ * @returns Latest version string or null if no versions
751
+ */
752
+ async getLatestVersion() {
753
+ if (!this.versioned) {
754
+ throw new FileDatabaseError("getLatestVersion() only works in versioned mode");
755
+ }
756
+ const versions = await this.getVersions();
757
+ if (versions.length === 0) {
758
+ return null;
759
+ }
760
+ return versions[versions.length - 1];
761
+ }
762
+ /**
763
+ * Check if any data exists in this table
764
+ * Works for both versioned and non-versioned modes
765
+ * @returns true if data exists
766
+ */
767
+ async hasData() {
768
+ const tablePath = this.getDestinationPath();
769
+ if (!import_fs3.default.existsSync(tablePath)) {
770
+ return false;
771
+ }
772
+ if (this.versioned) {
773
+ const versions = await this.getVersions();
774
+ return versions.length > 0;
775
+ } else {
776
+ const items = await import_fs3.default.promises.readdir(tablePath);
777
+ return items.some(
778
+ (item) => item === "metadata.json" || item.match(/^\d{6}\.(json|txt|xml)$/) || item.endsWith(".json")
779
+ );
780
+ }
781
+ }
782
+ /**
783
+ * Auto-detect the data format in this table
784
+ * Used when reading existing data
785
+ * @returns Format detection result
786
+ */
787
+ async detectDataFormat() {
788
+ const tablePath = this.getDestinationPath();
789
+ if (!import_fs3.default.existsSync(tablePath)) {
790
+ return { versioned: false, hasMetadata: false, dataType: null };
791
+ }
792
+ const items = await import_fs3.default.promises.readdir(tablePath);
793
+ if (items.includes("metadata.json")) {
794
+ const metadata = JSON.parse(
795
+ await import_fs3.default.promises.readFile(import_path3.default.join(tablePath, "metadata.json"), "utf8")
796
+ );
797
+ return {
798
+ versioned: false,
799
+ hasMetadata: true,
800
+ dataType: metadata.dataType || null
801
+ };
802
+ }
803
+ const versionFolders = items.filter((item) => {
804
+ const itemPath = import_path3.default.join(tablePath, item);
805
+ const stat = import_fs3.default.statSync(itemPath);
806
+ return stat.isDirectory() && isTimestampFolder(item);
807
+ });
808
+ if (versionFolders.length > 0) {
809
+ const latestVersion = versionFolders.sort().pop();
810
+ const versionMetadataPath = import_path3.default.join(tablePath, latestVersion, "metadata.json");
811
+ return {
812
+ versioned: true,
813
+ hasMetadata: import_fs3.default.existsSync(versionMetadataPath),
814
+ dataType: null
815
+ };
816
+ }
817
+ const dataFiles = items.filter((f) => f.match(/^\d{6}\.(json|txt|xml)$/));
818
+ if (dataFiles.length > 0) {
819
+ return {
820
+ versioned: false,
821
+ hasMetadata: false,
822
+ dataType: null
823
+ };
824
+ }
825
+ return { versioned: false, hasMetadata: false, dataType: null };
826
+ }
827
+ /**
828
+ * Load metadata from JSON file
829
+ */
830
+ async loadMetadataJson(version) {
831
+ const metadataFile = import_path3.default.join(this.getDestinationPath(), version, "metadata.json");
832
+ if (import_fs3.default.existsSync(metadataFile)) {
833
+ try {
834
+ const rawData = await import_fs3.default.promises.readFile(metadataFile, "utf8");
835
+ return JSON.parse(rawData);
836
+ } catch (e) {
837
+ throw new FileDatabaseError(`Failed to read metadata for version "${version}": ${e.message}`);
838
+ }
839
+ }
840
+ return null;
841
+ }
842
+ /**
843
+ * Build metadata by scanning files in a version folder (backward compatibility)
844
+ * Reads all files to get accurate counts - used when synopsis calculation is needed
845
+ */
846
+ async figureMetadataFromVersionFiles(version) {
847
+ const versionPath = import_path3.default.join(this.getDestinationPath(), version);
848
+ if (!import_fs3.default.existsSync(versionPath)) {
849
+ return this.getDefaultMetadata();
850
+ }
851
+ const files = (await import_fs3.default.promises.readdir(versionPath)).filter((file) => file !== "metadata.json" && !file.startsWith(".")).sort();
852
+ const metadata = this.getDefaultMetadata();
853
+ metadata.version = version;
854
+ metadata.files = [];
855
+ let totalRecords = 0;
856
+ let detectedDataType = null;
857
+ for (let i = 0; i < files.length; i++) {
858
+ const fileName = files[i];
859
+ const filePath = import_path3.default.join(versionPath, fileName);
860
+ try {
861
+ const rawData = await import_fs3.default.promises.readFile(filePath, "utf8");
862
+ const extension = import_path3.default.extname(fileName).toLowerCase();
863
+ let dataType = "text";
864
+ if (extension === ".json") {
865
+ dataType = "json-array";
866
+ } else if (extension === ".xml") {
867
+ dataType = "xml";
868
+ }
869
+ const fileData = deserializeData(rawData, dataType);
870
+ const recordsCount = Array.isArray(fileData) ? fileData.length : 1;
871
+ if (detectedDataType === null) {
872
+ detectedDataType = detectDataType(fileData);
873
+ }
874
+ const fileInfo = {
875
+ number: i + 1,
876
+ recordsCount,
877
+ fileName
878
+ };
879
+ metadata.files.push(fileInfo);
880
+ totalRecords += recordsCount;
881
+ } catch (error) {
882
+ this.logger.error?.(`[FileDatabase] Failed to read file ${fileName}: ${error.message}`);
883
+ }
884
+ }
885
+ metadata.totalRecords = totalRecords;
886
+ metadata.dataType = detectedDataType;
887
+ return metadata;
888
+ }
889
+ /**
890
+ * Build metadata optimized - only reads first and last files
891
+ * Assumes all middle files have the same record count as the first file
892
+ * Much faster for large datasets with many files
893
+ */
894
+ async buildMetadataOptimized(version) {
895
+ const versionPath = import_path3.default.join(this.getDestinationPath(), version);
896
+ if (!import_fs3.default.existsSync(versionPath)) {
897
+ return this.getDefaultMetadata();
898
+ }
899
+ const files = (await import_fs3.default.promises.readdir(versionPath)).filter((file) => file !== "metadata.json" && !file.startsWith(".")).sort();
900
+ if (files.length === 0) {
901
+ return this.getDefaultMetadata();
902
+ }
903
+ const metadata = this.getDefaultMetadata();
904
+ metadata.version = version;
905
+ metadata.files = files.map((fileName, index) => ({
906
+ number: index + 1,
907
+ recordsCount: 0,
908
+ fileName
909
+ }));
910
+ const firstFile = metadata.files[0];
911
+ const firstFilePath = import_path3.default.join(versionPath, firstFile.fileName);
912
+ const firstFileRaw = await import_fs3.default.promises.readFile(firstFilePath, "utf8");
913
+ let firstFileData;
914
+ try {
915
+ firstFileData = JSON.parse(firstFileRaw);
916
+ } catch (e) {
917
+ firstFileData = firstFileRaw;
918
+ }
919
+ metadata.dataType = detectDataType(firstFileData);
920
+ if (metadata.dataType === "json-array") {
921
+ const firstFileCount = Array.isArray(firstFileData) ? firstFileData.length : 1;
922
+ firstFile.recordsCount = firstFileCount;
923
+ for (let i = 1; i < metadata.files.length - 1; i++) {
924
+ metadata.files[i].recordsCount = firstFileCount;
925
+ }
926
+ if (files.length > 1) {
927
+ const lastFile = metadata.files[metadata.files.length - 1];
928
+ const lastFilePath = import_path3.default.join(versionPath, lastFile.fileName);
929
+ const lastFileRaw = await import_fs3.default.promises.readFile(lastFilePath, "utf8");
930
+ const lastFileData = deserializeData(lastFileRaw, metadata.dataType);
931
+ lastFile.recordsCount = Array.isArray(lastFileData) ? lastFileData.length : 1;
932
+ }
933
+ metadata.totalRecords = metadata.files.reduce((sum, file) => sum + file.recordsCount, 0);
934
+ } else {
935
+ metadata.files.forEach((file) => {
936
+ file.recordsCount = 1;
937
+ });
938
+ metadata.totalRecords = files.length;
939
+ }
940
+ return metadata;
941
+ }
942
+ /**
943
+ * Figure out metadata - tries JSON first, then builds from files
944
+ * Uses optimized building when no synopsis calculation is needed
945
+ */
946
+ async figureMetadata(version, useOptimized = true) {
947
+ if (this.useMetadata) {
948
+ const metadata = await this.loadMetadataJson(version);
949
+ if (metadata) {
950
+ return metadata;
951
+ }
952
+ }
953
+ if (useOptimized && !this.fileSynopsisFunction && !this.versionSynopsisFunction) {
954
+ return await this.buildMetadataOptimized(version);
955
+ }
956
+ return await this.figureMetadataFromVersionFiles(version);
957
+ }
958
+ /**
959
+ * Load version metadata (main entry point for loading)
960
+ */
961
+ async loadVersionMetadata(version) {
962
+ const metadata = await this.figureMetadata(version);
963
+ this.metadata = metadata;
964
+ return metadata;
965
+ }
966
+ /**
967
+ * Save version metadata to file
968
+ */
969
+ async saveVersionMetadata(metadata) {
970
+ if (!this.useMetadata) {
971
+ return;
972
+ }
973
+ const metadataToSave = metadata || this.metadata;
974
+ let metadataFile;
975
+ if (this.versioned) {
976
+ if (!this.currentVersion) {
977
+ return;
978
+ }
979
+ metadataFile = import_path3.default.join(this.getDestinationPath(), this.currentVersion, "metadata.json");
980
+ } else {
981
+ metadataFile = import_path3.default.join(this.getDestinationPath(), "metadata.json");
982
+ }
983
+ await import_fs3.default.promises.writeFile(metadataFile, JSON.stringify(metadataToSave, null, 4), "utf8");
984
+ }
985
+ /**
986
+ * Create a new file entry in metadata
987
+ */
988
+ makeNewFile() {
989
+ this.currentFileNumber = (this.currentFileNumber || 0) + 1;
990
+ const dataType = this.metadata.dataType || "json-array";
991
+ const fileEntry = {
992
+ number: this.currentFileNumber,
993
+ recordsCount: 0,
994
+ fileName: `${this.currentFileNumber.toString().padStart(6, "0")}.${getFileExtension(dataType)}`
995
+ };
996
+ this.metadata.files.push(fileEntry);
997
+ this.lastFileData = null;
998
+ this.logger.silly?.(`[FileDatabase] Created new file: ${fileEntry.fileName}, fileNumber: ${this.currentFileNumber}`);
999
+ }
1000
+ /**
1001
+ * Figure out what data to write and which file to use (for pagination)
1002
+ * @param data - Data to write
1003
+ * @param targetFileIndex - Optional index of existing file to overwrite (when customMetadata matches)
1004
+ * @param forceNewFile - If true, always create a new file (when customMetadata provided but no match)
1005
+ */
1006
+ figureOutDataAndFileToWrite(data, targetFileIndex = null, forceNewFile = false) {
1007
+ let dataToWrite;
1008
+ let dataLeftOver;
1009
+ const incomingDataType = detectDataType(data);
1010
+ if (this.metadata.dataType !== incomingDataType) {
1011
+ this.metadata.dataType = incomingDataType;
1012
+ }
1013
+ if (targetFileIndex !== null && targetFileIndex < this.metadata.files.length) {
1014
+ const targetFile = this.metadata.files[targetFileIndex];
1015
+ if (!Array.isArray(data)) {
1016
+ dataToWrite = data;
1017
+ dataLeftOver = null;
1018
+ return { dataToWrite, dataLeftOver, fileName: targetFile.fileName };
1019
+ } else {
1020
+ dataToWrite = data.slice(0, this.pageSize);
1021
+ dataLeftOver = data.slice(this.pageSize);
1022
+ this.lastFileData = dataToWrite;
1023
+ return { dataToWrite, dataLeftOver, fileName: targetFile.fileName };
1024
+ }
1025
+ }
1026
+ let newlyCreatedFileIndex = null;
1027
+ if (forceNewFile) {
1028
+ const filesBeforeCreate = this.metadata.files.length;
1029
+ this.makeNewFile();
1030
+ newlyCreatedFileIndex = filesBeforeCreate;
1031
+ this.logger.silly?.(`[FileDatabase] Creating new file for unique custom metadata combination, fileNumber: ${this.currentFileNumber}`);
1032
+ } else if (this.metadata.files.length === 0) {
1033
+ this.makeNewFile();
1034
+ }
1035
+ const lastFile = this.metadata.files[this.metadata.files.length - 1];
1036
+ const lastFileRecordsCount = lastFile.recordsCount;
1037
+ if (forceNewFile && newlyCreatedFileIndex !== null) {
1038
+ const newlyCreatedFile = this.metadata.files[newlyCreatedFileIndex];
1039
+ if (newlyCreatedFile && newlyCreatedFile.fileName !== lastFile.fileName) {
1040
+ this.logger.warn?.(`[FileDatabase] Warning: Newly created file ${newlyCreatedFile.fileName} doesn't match last file ${lastFile.fileName}`);
1041
+ }
1042
+ }
1043
+ if (!Array.isArray(data) && !forceNewFile) {
1044
+ const lastFileExtension = import_path3.default.extname(lastFile.fileName);
1045
+ const expectedExtension = `.${getFileExtension(incomingDataType)}`;
1046
+ if (lastFileExtension !== expectedExtension) {
1047
+ if (lastFileRecordsCount > 0) {
1048
+ this.makeNewFile();
1049
+ } else {
1050
+ lastFile.fileName = `${this.currentFileNumber.toString().padStart(6, "0")}.${getFileExtension(incomingDataType)}`;
1051
+ }
1052
+ }
1053
+ } else if (!Array.isArray(data) && forceNewFile) {
1054
+ const lastFileExtension = import_path3.default.extname(lastFile.fileName);
1055
+ const expectedExtension = `.${getFileExtension(incomingDataType)}`;
1056
+ if (lastFileExtension !== expectedExtension) {
1057
+ lastFile.fileName = `${this.currentFileNumber.toString().padStart(6, "0")}.${getFileExtension(incomingDataType)}`;
1058
+ }
1059
+ }
1060
+ if (Array.isArray(data)) {
1061
+ if (forceNewFile) {
1062
+ dataToWrite = data.slice(0, this.pageSize);
1063
+ dataLeftOver = data.slice(this.pageSize);
1064
+ this.lastFileData = dataToWrite;
1065
+ } else if (lastFileRecordsCount < this.pageSize) {
1066
+ dataToWrite = [...this.lastFileData || [], ...data.slice(0, this.pageSize - lastFileRecordsCount)];
1067
+ dataLeftOver = data.slice(this.pageSize - lastFileRecordsCount);
1068
+ this.lastFileData = dataToWrite;
1069
+ } else {
1070
+ this.makeNewFile();
1071
+ dataToWrite = data.slice(0, this.pageSize);
1072
+ dataLeftOver = data.slice(this.pageSize);
1073
+ this.lastFileData = dataToWrite;
1074
+ }
1075
+ } else {
1076
+ dataToWrite = data;
1077
+ dataLeftOver = null;
1078
+ }
1079
+ const fileName = this.metadata.files[this.metadata.files.length - 1].fileName;
1080
+ this.logger.silly?.(
1081
+ `[FileDatabase] figureOutDataAndFileToWrite: filename=${fileName}, forceNewFile=${forceNewFile}, targetFileIndex=${targetFileIndex}, dataToWrite.length=${Array.isArray(dataToWrite) ? dataToWrite.length : "N/A"}, lastFileRecordsCount=${lastFileRecordsCount}`
1082
+ );
1083
+ return { dataToWrite, dataLeftOver, fileName };
1084
+ }
1085
+ /**
1086
+ * Calculate file-level synopsis if function is set
1087
+ */
1088
+ calculateFileSynopsis(data, fileIndex = this.metadata.files.length - 1) {
1089
+ if (!this.fileSynopsisFunction) {
1090
+ return;
1091
+ }
1092
+ const fileInfo = this.metadata.files[fileIndex];
1093
+ const enhancedFileInfo = this.fileSynopsisFunction(fileInfo, data);
1094
+ this.metadata.files[fileIndex] = enhancedFileInfo;
1095
+ }
1096
+ /**
1097
+ * Calculate version-level synopsis if function is set
1098
+ */
1099
+ calculateVersionSynopsis() {
1100
+ if (!this.versionSynopsisFunction) {
1101
+ return;
1102
+ }
1103
+ const enhancedMetadata = this.versionSynopsisFunction(this.metadata);
1104
+ this.metadata = enhancedMetadata;
1105
+ }
1106
+ /**
1107
+ * Update metadata after writing data
1108
+ */
1109
+ updateMetadata(dataToWrite, fileName, customMetadata) {
1110
+ let currentFile;
1111
+ if (fileName) {
1112
+ const foundFile = this.metadata.files.find((file) => file.fileName === fileName);
1113
+ if (!foundFile) {
1114
+ this.logger.warn?.(`[FileDatabase] File ${fileName} not found in metadata, using last file`);
1115
+ currentFile = this.metadata.files[this.metadata.files.length - 1];
1116
+ } else {
1117
+ currentFile = foundFile;
1118
+ }
1119
+ } else {
1120
+ currentFile = this.metadata.files[this.metadata.files.length - 1];
1121
+ }
1122
+ const recordsCount = Array.isArray(dataToWrite) ? dataToWrite.length : 1;
1123
+ currentFile.recordsCount = recordsCount;
1124
+ if (customMetadata) {
1125
+ Object.assign(currentFile, customMetadata);
1126
+ }
1127
+ const fileIndex = this.metadata.files.indexOf(currentFile);
1128
+ if (fileIndex !== -1) {
1129
+ this.calculateFileSynopsis(dataToWrite, fileIndex);
1130
+ }
1131
+ this.metadata.version = this.currentVersion;
1132
+ this.metadata.modifiedAt = (/* @__PURE__ */ new Date()).toISOString();
1133
+ this.metadata.dataType = detectDataType(dataToWrite);
1134
+ this.metadata.totalRecords = this.metadata.files.reduce((sum, file) => sum + (file.recordsCount || 0), 0);
1135
+ this.logger.silly?.(
1136
+ `[FileDatabase] Updated metadata for file ${currentFile.fileName}: recordsCount=${recordsCount}, totalRecords=${this.metadata.totalRecords}`
1137
+ );
1138
+ }
1139
+ /**
1140
+ * Safe write with disk space check
1141
+ */
1142
+ async safeWrite(filePath, data) {
1143
+ const serializedData = serializeData(data);
1144
+ const dir = import_path3.default.dirname(filePath);
1145
+ const requiredBytes = Buffer.byteLength(serializedData, "utf8");
1146
+ const freeBytes = getFreeDiskSpace(dir);
1147
+ if (freeBytes !== null) {
1148
+ if (freeBytes < requiredBytes) {
1149
+ throw new FileDatabaseError(
1150
+ `Not enough disk space. Required: ${bytesToHumanReadable(requiredBytes)}, Free: ${bytesToHumanReadable(freeBytes)}`
1151
+ );
1152
+ }
1153
+ if (freeBytes < this.freeSpaceThreshold) {
1154
+ this.logger.warn?.(`Low disk space warning: only ${bytesToHumanReadable(freeBytes)} left`);
1155
+ }
1156
+ }
1157
+ try {
1158
+ await import_fs3.default.promises.writeFile(filePath, serializedData, "utf8");
1159
+ this.logger.silly?.(`[FileDatabase] Wrote ${bytesToHumanReadable(requiredBytes)} to ${filePath}`);
1160
+ } catch (error) {
1161
+ throw new FileDatabaseError(`Failed to write file ${filePath}: ${error.message}`);
1162
+ }
1163
+ }
1164
+ /**
1165
+ * Prepare the instance for read or write operations
1166
+ * This discovers state and sets up internal members based on mode and current data
1167
+ */
1168
+ async prepare({ write, read, version }) {
1169
+ if (write) {
1170
+ if (this.versioned) {
1171
+ if (this.currentVersion === null) {
1172
+ await this.makeNewVersion();
1173
+ this.metadata = this.getDefaultMetadata();
1174
+ this.metadata.version = this.currentVersion;
1175
+ this.makeNewFile();
1176
+ } else {
1177
+ if (!this.metadata.files.length) {
1178
+ this.metadata = await this.figureMetadata(this.currentVersion);
1179
+ if (this.metadata.files && this.metadata.files.length > 0) {
1180
+ this.currentFileNumber = Math.max(...this.metadata.files.map((f) => f.number || 0));
1181
+ } else {
1182
+ this.currentFileNumber = 0;
1183
+ }
1184
+ }
1185
+ }
1186
+ } else {
1187
+ await ensurePath(this.getDestinationPath());
1188
+ if (this.useMetadata === true) {
1189
+ const metadataPath = import_path3.default.join(this.getDestinationPath(), "metadata.json");
1190
+ if (import_fs3.default.existsSync(metadataPath)) {
1191
+ try {
1192
+ const rawData = await import_fs3.default.promises.readFile(metadataPath, "utf8");
1193
+ this.metadata = JSON.parse(rawData);
1194
+ if (this.metadata.files && this.metadata.files.length > 0) {
1195
+ this.currentFileNumber = Math.max(...this.metadata.files.map((f) => f.number || 0));
1196
+ } else {
1197
+ this.currentFileNumber = 0;
1198
+ }
1199
+ } catch (e) {
1200
+ this.metadata = this.getDefaultMetadata();
1201
+ this.currentFileNumber = 0;
1202
+ }
1203
+ } else {
1204
+ this.metadata = this.getDefaultMetadata();
1205
+ this.currentFileNumber = 0;
1206
+ }
1207
+ } else {
1208
+ this.metadata = this.getDefaultMetadata();
1209
+ this.currentFileNumber = 0;
1210
+ }
1211
+ }
1212
+ } else if (read) {
1213
+ if (this.versioned) {
1214
+ const versions = await this.getVersions();
1215
+ if (versions.length === 0) {
1216
+ throw new FileDatabaseError("[FileDatabase] No versions found, cannot read");
1217
+ }
1218
+ if (version) {
1219
+ if (!versions.includes(version)) {
1220
+ throw new FileDatabaseError(`[FileDatabase] Version "${version}" not found`);
1221
+ }
1222
+ await this.setCurrentVersion(version);
1223
+ } else {
1224
+ await this.setCurrentVersion(versions[versions.length - 1]);
1225
+ }
1226
+ if (!this.metadata.files.length) {
1227
+ this.metadata = await this.figureMetadata(this.currentVersion);
1228
+ if (this.metadata.files && this.metadata.files.length > 0) {
1229
+ this.currentFileNumber = Math.max(...this.metadata.files.map((f) => f.number || 0));
1230
+ } else {
1231
+ this.currentFileNumber = 0;
1232
+ }
1233
+ }
1234
+ } else {
1235
+ this.currentVersion = null;
1236
+ if (this.useMetadata === void 0) {
1237
+ const format = await this.detectDataFormat();
1238
+ this.useMetadata = format.hasMetadata;
1239
+ }
1240
+ if (this.useMetadata) {
1241
+ const destPath = this.getDestinationPath();
1242
+ const metadataPath = import_path3.default.join(destPath, "metadata.json");
1243
+ if (import_fs3.default.existsSync(metadataPath)) {
1244
+ try {
1245
+ const rawData = await import_fs3.default.promises.readFile(metadataPath, "utf8");
1246
+ this.metadata = JSON.parse(rawData);
1247
+ if (this.metadata.files && this.metadata.files.length > 0) {
1248
+ this.currentFileNumber = Math.max(...this.metadata.files.map((f) => f.number || 0));
1249
+ } else {
1250
+ this.currentFileNumber = 0;
1251
+ }
1252
+ } catch (e) {
1253
+ throw new FileDatabaseError(`Failed to read metadata: ${e.message}`);
1254
+ }
1255
+ } else {
1256
+ throw new FileDatabaseError(
1257
+ `[FileDatabase] No metadata found in non-versioned mode. Looked for: ${metadataPath} (table path: ${destPath})`
1258
+ );
1259
+ }
1260
+ } else {
1261
+ this.metadata = await this.figureMetadataFromVersionFiles("");
1262
+ if (this.metadata.files && this.metadata.files.length > 0) {
1263
+ this.currentFileNumber = Math.max(...this.metadata.files.map((f) => f.number || 0));
1264
+ } else {
1265
+ this.currentFileNumber = 0;
1266
+ }
1267
+ }
1268
+ }
1269
+ }
1270
+ }
1271
+ /**
1272
+ * Write data to the file database
1273
+ */
1274
+ async write(data, options = {}) {
1275
+ if (options.filename) {
1276
+ const destPath2 = this.getDestinationPath();
1277
+ await ensurePath(destPath2);
1278
+ const filePath = import_path3.default.join(destPath2, options.filename);
1279
+ await this.safeWrite(filePath, data);
1280
+ return;
1281
+ }
1282
+ if (options.forceNewVersion && !this.versioned) {
1283
+ throw new FileDatabaseError("Cannot use forceNewVersion in non-versioned mode");
1284
+ }
1285
+ await this.prepare({ write: true });
1286
+ const incomingDataType = detectDataType(data);
1287
+ this.metadata.dataType = incomingDataType;
1288
+ if (options.forceNewVersion) {
1289
+ await this.makeNewVersion();
1290
+ this.metadata = this.getDefaultMetadata();
1291
+ this.metadata.version = this.currentVersion;
1292
+ this.metadata.dataType = incomingDataType;
1293
+ this.makeNewFile();
1294
+ }
1295
+ let targetFileIndex = null;
1296
+ const hasCustomMetadata = options.customMetadata && Object.keys(options.customMetadata).length > 0;
1297
+ if (hasCustomMetadata) {
1298
+ for (let i = 0; i < this.metadata.files.length; i++) {
1299
+ const fileEntry = this.metadata.files[i];
1300
+ const matches = Object.keys(options.customMetadata).every((key) => {
1301
+ return key in fileEntry && fileEntry[key] === options.customMetadata[key];
1302
+ });
1303
+ if (matches) {
1304
+ targetFileIndex = i;
1305
+ this.logger.silly?.(`[FileDatabase] Found existing file with matching custom metadata: ${fileEntry.fileName}, metadata: ${JSON.stringify(options.customMetadata)}`);
1306
+ break;
1307
+ } else {
1308
+ this.logger.silly?.(`[FileDatabase] File ${fileEntry.fileName} does not match custom metadata: ${JSON.stringify(options.customMetadata)}`);
1309
+ }
1310
+ }
1311
+ if (targetFileIndex === null) {
1312
+ this.logger.silly?.(`[FileDatabase] No existing file found with custom metadata: ${JSON.stringify(options.customMetadata)}, will create new file`);
1313
+ }
1314
+ } else {
1315
+ this.logger.silly?.(`[FileDatabase] No custom metadata provided, will create new file`);
1316
+ }
1317
+ if (targetFileIndex !== null) {
1318
+ const targetFile = this.metadata.files[targetFileIndex];
1319
+ this.currentFileNumber = targetFile.number;
1320
+ this.lastFileData = null;
1321
+ this.currentRecord = 0;
1322
+ this.hasReadFirstPage = false;
1323
+ }
1324
+ const forceNewFile = hasCustomMetadata && targetFileIndex === null;
1325
+ let { dataToWrite, dataLeftOver, fileName } = this.figureOutDataAndFileToWrite(data, targetFileIndex, forceNewFile);
1326
+ const destPath = this.getDestinationPath(this.currentVersion || void 0);
1327
+ await this.safeWrite(import_path3.default.join(destPath, fileName), dataToWrite);
1328
+ this.updateMetadata(dataToWrite, fileName, options.customMetadata);
1329
+ while (dataLeftOver && dataLeftOver.length > 0 && targetFileIndex === null) {
1330
+ const writeContext = this.figureOutDataAndFileToWrite(dataLeftOver);
1331
+ await this.safeWrite(import_path3.default.join(destPath, writeContext.fileName), writeContext.dataToWrite);
1332
+ this.updateMetadata(writeContext.dataToWrite, writeContext.fileName, options.customMetadata);
1333
+ dataLeftOver = writeContext.dataLeftOver;
1334
+ }
1335
+ this.calculateVersionSynopsis();
1336
+ if (this.useMetadata) {
1337
+ await this.saveVersionMetadata(this.metadata);
1338
+ }
1339
+ }
1340
+ /**
1341
+ * Read data from the file database
1342
+ */
1343
+ async read(options = {}) {
1344
+ const { version, nextPage = false, pageSize, filename } = options;
1345
+ if (filename) {
1346
+ const destPath = this.getDestinationPath(version);
1347
+ const filePath = import_path3.default.join(destPath, filename);
1348
+ try {
1349
+ const rawData = await import_fs3.default.promises.readFile(filePath, "utf8");
1350
+ return JSON.parse(rawData);
1351
+ } catch (error) {
1352
+ throw new FileDatabaseError(`Failed to read file ${filename}: ${error.message}`);
1353
+ }
1354
+ }
1355
+ await this.prepare({ read: true, version });
1356
+ const isNonPaginatedData = this.metadata.dataType === "text" || this.metadata.dataType === "xml" || this.metadata.dataType === "json-object";
1357
+ if (isNonPaginatedData) {
1358
+ const file = this.metadata.files[0];
1359
+ const filePath = import_path3.default.join(this.getDestinationPath(this.currentVersion || void 0), file.fileName);
1360
+ try {
1361
+ const rawData = await import_fs3.default.promises.readFile(filePath, "utf8");
1362
+ return deserializeData(rawData, this.metadata.dataType);
1363
+ } catch (error) {
1364
+ throw new FileDatabaseError(`Failed to read file ${file.fileName}: ${error.message}`);
1365
+ }
1366
+ }
1367
+ let effectivePageSize;
1368
+ if (nextPage && this.hasReadFirstPage) {
1369
+ effectivePageSize = pageSize || this.pageSize;
1370
+ this.currentRecord += effectivePageSize;
1371
+ } else if (!nextPage) {
1372
+ effectivePageSize = pageSize !== void 0 ? pageSize : this.metadata.totalRecords;
1373
+ this.currentRecord = 0;
1374
+ } else {
1375
+ effectivePageSize = pageSize || this.pageSize;
1376
+ }
1377
+ if (this.currentRecord >= this.metadata.totalRecords) {
1378
+ return [];
1379
+ }
1380
+ const result = [];
1381
+ let recordsRead = 0;
1382
+ let currentFileIndex = 0;
1383
+ let currentFileOffset = 0;
1384
+ let totalRecords = 0;
1385
+ for (let i = 0; i < this.metadata.files.length; i++) {
1386
+ const file = this.metadata.files[i];
1387
+ if (this.currentRecord < totalRecords + file.recordsCount) {
1388
+ currentFileIndex = i;
1389
+ currentFileOffset = totalRecords;
1390
+ break;
1391
+ }
1392
+ totalRecords += file.recordsCount;
1393
+ }
1394
+ let cumulativeRecords = currentFileOffset;
1395
+ for (let i = currentFileIndex; i < this.metadata.files.length && recordsRead < effectivePageSize; i++) {
1396
+ const file = this.metadata.files[i];
1397
+ const filePath = import_path3.default.join(this.getDestinationPath(this.currentVersion || void 0), file.fileName);
1398
+ try {
1399
+ const rawData = await import_fs3.default.promises.readFile(filePath, "utf8");
1400
+ const fileData = deserializeData(rawData, this.metadata.dataType);
1401
+ let startIndex = 0;
1402
+ if (i === currentFileIndex) {
1403
+ startIndex = this.currentRecord - cumulativeRecords;
1404
+ }
1405
+ const endIndex = Math.min(startIndex + (effectivePageSize - recordsRead), fileData.length);
1406
+ const recordsFromThisFile = fileData.slice(startIndex, endIndex);
1407
+ result.push(...recordsFromThisFile);
1408
+ recordsRead += recordsFromThisFile.length;
1409
+ cumulativeRecords += file.recordsCount;
1410
+ } catch (error) {
1411
+ throw new FileDatabaseError(`Failed to read file ${file.fileName}: ${error.message}`);
1412
+ }
1413
+ }
1414
+ if (result.length > 0) {
1415
+ if (nextPage || pageSize !== void 0 && pageSize < this.metadata.totalRecords) {
1416
+ this.hasReadFirstPage = true;
1417
+ }
1418
+ }
1419
+ return result;
1420
+ }
1421
+ /**
1422
+ * Set the starting record for pagination (1-based index)
1423
+ */
1424
+ setStartRecord(startRecord) {
1425
+ this.currentRecord = startRecord - 1;
1426
+ this.hasReadFirstPage = false;
1427
+ }
1428
+ /**
1429
+ * Reset read pagination state
1430
+ */
1431
+ resetPagination() {
1432
+ this.currentRecord = 0;
1433
+ this.hasReadFirstPage = false;
1434
+ }
1435
+ /**
1436
+ * List filenames in the table directory.
1437
+ * For catalog/key-value usage (files written with { filename }).
1438
+ * Returns data file names (.json, .txt, .xml) excluding metadata.json.
1439
+ */
1440
+ async listFilenames() {
1441
+ const destPath = this.versioned && this.currentVersion ? import_path3.default.join(this.getDestinationPath(), this.currentVersion) : this.getDestinationPath();
1442
+ try {
1443
+ const entries = await import_fs3.default.promises.readdir(destPath, { withFileTypes: true });
1444
+ return entries.filter((e) => e.isFile() && e.name !== "metadata.json" && /\.(json|txt|xml)$/i.test(e.name)).map((e) => e.name);
1445
+ } catch (err) {
1446
+ if (err?.code === "ENOENT") return [];
1447
+ throw new FileDatabaseError(`Failed to list files: ${err.message}`);
1448
+ }
1449
+ }
1450
+ /**
1451
+ * Remove a file from the table directory (catalog mode).
1452
+ * Use with listFilenames() to manage individual files.
1453
+ */
1454
+ async removeFile(filename) {
1455
+ const destPath = this.versioned && this.currentVersion ? import_path3.default.join(this.getDestinationPath(), this.currentVersion) : this.getDestinationPath();
1456
+ const filePath = import_path3.default.join(destPath, filename);
1457
+ try {
1458
+ await import_fs3.default.promises.unlink(filePath);
1459
+ } catch (err) {
1460
+ if (err?.code === "ENOENT") return;
1461
+ throw new FileDatabaseError(`Failed to remove file ${filename}: ${err.message}`);
1462
+ }
1463
+ }
1464
+ /**
1465
+ * Remove a file and its metadata entry (non-versioned mode with useMetadata).
1466
+ * Use with findData() to get fileName, then call removeFileEntry to delete.
1467
+ */
1468
+ async removeFileEntry(filename) {
1469
+ if (this.versioned) {
1470
+ throw new FileDatabaseError("removeFileEntry is only supported in non-versioned mode");
1471
+ }
1472
+ await this.prepare({ read: true });
1473
+ const idx = this.metadata.files.findIndex((f) => f.fileName === filename);
1474
+ if (idx === -1) {
1475
+ throw new FileDatabaseError(`File entry ${filename} not found in metadata`);
1476
+ }
1477
+ const entry = this.metadata.files[idx];
1478
+ const recordsCount = entry.recordsCount || 0;
1479
+ this.metadata.files.splice(idx, 1);
1480
+ this.metadata.totalRecords = Math.max(0, (this.metadata.totalRecords || 0) - recordsCount);
1481
+ const destPath = this.getDestinationPath();
1482
+ const filePath = import_path3.default.join(destPath, filename);
1483
+ try {
1484
+ await import_fs3.default.promises.unlink(filePath);
1485
+ } catch (err) {
1486
+ if (err?.code === "ENOENT") {
1487
+ this.logger.warn?.(`[FileDatabase] File ${filename} already missing on disk`);
1488
+ } else {
1489
+ throw new FileDatabaseError(`Failed to remove file ${filename}: ${err.message}`);
1490
+ }
1491
+ }
1492
+ if (this.useMetadata) {
1493
+ await this.saveVersionMetadata(this.metadata);
1494
+ }
1495
+ }
1496
+ /**
1497
+ * Set file-level synopsis calculation function
1498
+ */
1499
+ setFileSynopsisFunction(fn) {
1500
+ this.fileSynopsisFunction = fn;
1501
+ }
1502
+ /**
1503
+ * Set version-level synopsis calculation function
1504
+ */
1505
+ setVersionSynopsisFunction(fn) {
1506
+ this.versionSynopsisFunction = fn;
1507
+ }
1508
+ /**
1509
+ * Get current version name
1510
+ */
1511
+ getCurrentVersion() {
1512
+ return this.currentVersion;
1513
+ }
1514
+ /**
1515
+ * Get current metadata
1516
+ */
1517
+ getMetadata() {
1518
+ return { ...this.metadata };
1519
+ }
1520
+ /**
1521
+ * Find data by custom metadata fields
1522
+ * Searches through all versions and files to find entries matching the search criteria
1523
+ *
1524
+ * @param searchCriteria - Object with field names and values to search for (e.g., { ListingKey: "123", id: "456" })
1525
+ * @returns Array of found entries with their file paths and metadata
1526
+ */
1527
+ async findData(searchCriteria) {
1528
+ const results = [];
1529
+ if (!this.versioned) {
1530
+ await this.prepare({ read: true });
1531
+ const metadata = this.getMetadata();
1532
+ for (const fileEntry of metadata.files) {
1533
+ const matches = Object.keys(searchCriteria).every((key) => {
1534
+ return fileEntry[key] === searchCriteria[key];
1535
+ });
1536
+ if (matches) {
1537
+ const destPath = this.getDestinationPath();
1538
+ const filePath = import_path3.default.join(destPath, fileEntry.fileName);
1539
+ const fileData = await import_fs3.default.promises.readFile(filePath, "utf8");
1540
+ const data = deserializeData(fileData, metadata.dataType || "json-object");
1541
+ results.push({
1542
+ filePath,
1543
+ fileName: fileEntry.fileName,
1544
+ version: null,
1545
+ metadata: fileEntry,
1546
+ data
1547
+ });
1548
+ }
1549
+ }
1550
+ } else {
1551
+ const versions = await this.getVersions();
1552
+ for (const version of versions) {
1553
+ await this.prepare({ read: true, version });
1554
+ const metadata = this.getMetadata();
1555
+ for (const fileEntry of metadata.files) {
1556
+ const matches = Object.keys(searchCriteria).every((key) => {
1557
+ return fileEntry[key] === searchCriteria[key];
1558
+ });
1559
+ if (matches) {
1560
+ const destPath = this.getDestinationPath(version);
1561
+ const filePath = import_path3.default.join(destPath, fileEntry.fileName);
1562
+ const fileData = await import_fs3.default.promises.readFile(filePath, "utf8");
1563
+ const data = deserializeData(fileData, metadata.dataType || "json-object");
1564
+ results.push({
1565
+ filePath,
1566
+ fileName: fileEntry.fileName,
1567
+ version,
1568
+ metadata: fileEntry,
1569
+ data
1570
+ });
1571
+ }
1572
+ }
1573
+ }
1574
+ }
1575
+ return results;
1576
+ }
1577
+ };
1578
+
1579
+ // src/tasks/taskLogs.ts
1580
+ function getLogsState(context) {
1581
+ const holder = context;
1582
+ if (holder.__tasksLogsState) return holder.__tasksLogsState;
1583
+ const basePath = holder.params?.get?.("tasksLogsBasePath") || "./data";
1584
+ const namespace = holder.params?.get?.("tasksLogsNamespace") || "tasks-logs";
1585
+ const tableName = holder.params?.get?.("tasksLogsTable") || "runner";
1586
+ const errorTableName = holder.params?.get?.("tasksErrorLogsTable") || `${tableName}-errors`;
1587
+ const maxVersionsRaw = Number(holder.params?.get?.("tasksLogsMaxVersions"));
1588
+ const pageSizeRaw = Number(holder.params?.get?.("tasksLogsPageSize"));
1589
+ const errorDb = new FileDatabase({
1590
+ basePath,
1591
+ namespace,
1592
+ tableName: errorTableName,
1593
+ versioned: true,
1594
+ useMetadata: true,
1595
+ maxVersions: Number.isFinite(maxVersionsRaw) && maxVersionsRaw > 0 ? maxVersionsRaw : 20,
1596
+ pageSize: Number.isFinite(pageSizeRaw) && pageSizeRaw > 0 ? pageSizeRaw : 2e3,
1597
+ logger: holder.logger
1598
+ });
1599
+ const enabledRaw = holder.params?.get?.("tasksLogsEnabled");
1600
+ const enabled = enabledRaw === void 0 ? true : !!enabledRaw;
1601
+ if (!enabled) {
1602
+ const disabledState = {
1603
+ db: null,
1604
+ errorDb,
1605
+ queue: Promise.resolve(),
1606
+ initialized: true,
1607
+ errorInitialized: false
1608
+ };
1609
+ holder.__tasksLogsState = disabledState;
1610
+ return disabledState;
1611
+ }
1612
+ const db = new FileDatabase({
1613
+ basePath,
1614
+ namespace,
1615
+ tableName,
1616
+ versioned: true,
1617
+ useMetadata: true,
1618
+ maxVersions: Number.isFinite(maxVersionsRaw) && maxVersionsRaw > 0 ? maxVersionsRaw : 20,
1619
+ pageSize: Number.isFinite(pageSizeRaw) && pageSizeRaw > 0 ? pageSizeRaw : 2e3,
1620
+ logger: holder.logger
1621
+ });
1622
+ const state = {
1623
+ db,
1624
+ errorDb,
1625
+ queue: Promise.resolve(),
1626
+ initialized: false,
1627
+ errorInitialized: false
1628
+ };
1629
+ holder.__tasksLogsState = state;
1630
+ return state;
1631
+ }
1632
+ function isErrorPayload(payload) {
1633
+ if (!payload) return false;
1634
+ if (typeof payload === "object") {
1635
+ const level = typeof payload.level === "string" ? payload.level.toLowerCase() : "";
1636
+ if (level === "error" || level === "fatal") return true;
1637
+ if (typeof payload.message === "string" && /\berror\b/i.test(payload.message)) return true;
1638
+ return false;
1639
+ }
1640
+ if (typeof payload === "string") {
1641
+ return /\berror\b/i.test(payload);
1642
+ }
1643
+ return false;
1644
+ }
1645
+ function buildLogRecord(task, payload) {
1646
+ const params = task.params && typeof task.params === "object" ? task.params : {};
1647
+ return {
1648
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
1649
+ opid: task.opid ?? null,
1650
+ taskId: task.id,
1651
+ taskName: task.task,
1652
+ target: task.target,
1653
+ source: typeof params.source === "string" ? params.source : null,
1654
+ resource: typeof params.resource === "string" ? params.resource : null,
1655
+ payload
1656
+ };
1657
+ }
1658
+ function appendTaskIpcLog(context, task, payload) {
1659
+ const state = getLogsState(context);
1660
+ if (!state.db && !state.errorDb) return;
1661
+ const record = buildLogRecord(task, payload);
1662
+ state.queue = state.queue.then(async () => {
1663
+ if (state.db) {
1664
+ await state.db.write([record], { forceNewVersion: !state.initialized });
1665
+ state.initialized = true;
1666
+ }
1667
+ if (state.errorDb && isErrorPayload(payload)) {
1668
+ await state.errorDb.write([record], { forceNewVersion: !state.errorInitialized });
1669
+ state.errorInitialized = true;
1670
+ }
1671
+ }).catch((error) => {
1672
+ context.logger.warn?.("[tasks] failed to persist IPC log entry:", error);
1673
+ });
1674
+ }
1675
+
1676
+ // src/tasks/time-matcher.ts
1677
+ var RANGES = ["0-59", "0-59", "0-23", "1-31", "1-12", "0-6"];
1678
+ function resolveAsterisks(field, range) {
1679
+ return field.includes("*") ? field.replace("*", range) : field;
1680
+ }
1681
+ function resolveRanges(field) {
1682
+ const regex = /(\d+)-(\d+)/;
1683
+ let current = field;
1684
+ while (true) {
1685
+ const match = regex.exec(current);
1686
+ if (!match) break;
1687
+ const raw = match[0];
1688
+ let first = Number(match[1]);
1689
+ let last = Number(match[2]);
1690
+ if (last < first) {
1691
+ [first, last] = [last, first];
1692
+ }
1693
+ const values = [];
1694
+ for (let i = first; i <= last; i += 1) {
1695
+ values.push(i);
1696
+ }
1697
+ current = current.replace(raw, values.join(","));
1698
+ }
1699
+ return current;
1700
+ }
1701
+ function resolveSteps(field) {
1702
+ const match = /^(.+)\/(\d+)$/.exec(field);
1703
+ if (!match) return field;
1704
+ const base = match[1];
1705
+ const step = Number(match[2]);
1706
+ if (!Number.isFinite(step) || step <= 0) return field;
1707
+ return base.split(",").map((v) => Number(v)).filter((v) => Number.isFinite(v) && v % step === 0).join(",");
1708
+ }
1709
+ function convertPattern(pattern) {
1710
+ const parts = pattern.trim().split(/\s+/);
1711
+ if (parts.length !== 6) {
1712
+ throw new Error(`Invalid schedule "${pattern}". Expected 6 fields: sec min hour day month weekday`);
1713
+ }
1714
+ return parts.map((field, idx) => resolveAsterisks(field, RANGES[idx])).map((field) => resolveRanges(field)).map((field) => resolveSteps(field));
1715
+ }
1716
+ function fieldMatches(field, value) {
1717
+ const allowed = field.split(",").map((v) => Number(v));
1718
+ return allowed.includes(value);
1719
+ }
1720
+ function timeMatcher(pattern, date = /* @__PURE__ */ new Date()) {
1721
+ const parsed = convertPattern(pattern);
1722
+ return fieldMatches(parsed[0], date.getSeconds()) && fieldMatches(parsed[1], date.getMinutes()) && fieldMatches(parsed[2], date.getHours()) && fieldMatches(parsed[3], date.getDate()) && fieldMatches(parsed[4], date.getMonth() + 1) && fieldMatches(parsed[5], date.getDay());
1723
+ }
1724
+
1725
+ // src/tasks/TaskMaster.ts
1726
+ var TaskMaster = class {
1727
+ context;
1728
+ task;
1729
+ constructor(context, task) {
1730
+ this.context = context;
1731
+ this.task = task;
1732
+ }
1733
+ cantRunReason() {
1734
+ return false;
1735
+ }
1736
+ requestStop(_allowanceMs) {
1737
+ }
1738
+ };
1739
+
1740
+ // src/tasks/coreTasks/TaskPing.ts
1741
+ var TaskPing = class extends TaskMaster {
1742
+ async run() {
1743
+ this.context.logger.info?.(`[TaskPing] pong (${this.task.id})`);
1744
+ return { success: true, results: "pong" };
1745
+ }
1746
+ };
1747
+
1748
+ // src/tasks/coreTasks/TaskSampleProcess.ts
1749
+ var TaskSampleProcess = class extends TaskMaster {
1750
+ stopRequested = false;
1751
+ stopAllowanceMs = 0;
1752
+ stopDecisionLogged = false;
1753
+ requestStop(allowanceMs) {
1754
+ this.stopRequested = true;
1755
+ this.stopAllowanceMs = Number.isFinite(allowanceMs) && allowanceMs > 0 ? allowanceMs : 0;
1756
+ this.context.logger.warn?.(
1757
+ `[TaskSampleProcess] stop signal received (${this.task.id}), allowanceMs=${this.stopAllowanceMs}`
1758
+ );
1759
+ }
1760
+ async run(reportProgress) {
1761
+ const totalRaw = this.task?.params?.total ?? 10;
1762
+ const delayRaw = this.task?.params?.delay ?? 1e3;
1763
+ const nameRaw = this.task?.params?.name;
1764
+ const total = Number(totalRaw);
1765
+ const delay = Number(delayRaw);
1766
+ const name = typeof nameRaw === "string" && nameRaw.trim() ? nameRaw.trim() : "sampleProcess";
1767
+ const errors = [];
1768
+ if (!Number.isInteger(total) || total <= 0) {
1769
+ errors.push('param "total" must be a positive integer');
1770
+ }
1771
+ if (!Number.isInteger(delay) || delay < 0) {
1772
+ errors.push('param "delay" must be an integer >= 0');
1773
+ }
1774
+ if (errors.length > 0) {
1775
+ return {
1776
+ success: false,
1777
+ results: {
1778
+ error: `Validation failed: ${errors.join(", ")}`,
1779
+ received: { total: totalRaw, delay: delayRaw, name: nameRaw }
1780
+ }
1781
+ };
1782
+ }
1783
+ const startedAt = Date.now();
1784
+ for (let i = 1; i <= total; i += 1) {
1785
+ if (this.stopRequested) {
1786
+ const remainingMs = Math.max(0, (total - i + 1) * delay);
1787
+ if (remainingMs <= this.stopAllowanceMs) {
1788
+ if (!this.stopDecisionLogged) {
1789
+ this.stopDecisionLogged = true;
1790
+ this.context.logger.warn?.(
1791
+ `[TaskSampleProcess] continue to finish (${this.task.id}): remainingMs=${remainingMs} <= allowanceMs=${this.stopAllowanceMs}`
1792
+ );
1793
+ }
1794
+ } else {
1795
+ this.context.logger.warn?.(
1796
+ `[TaskSampleProcess] stopping gracefully at iteration ${i}/${total} (${this.task.id}), remainingMs=${remainingMs} > allowanceMs=${this.stopAllowanceMs}`
1797
+ );
1798
+ return {
1799
+ success: false,
1800
+ results: {
1801
+ message: `Stopped before completion at iteration ${i}/${total}`,
1802
+ completed: i - 1,
1803
+ total,
1804
+ name,
1805
+ remainingMs,
1806
+ allowanceMs: this.stopAllowanceMs
1807
+ }
1808
+ };
1809
+ }
1810
+ }
1811
+ const elapsed = Date.now() - startedAt;
1812
+ const remaining = Math.max(0, (total - i) * delay);
1813
+ const progress = {
1814
+ name,
1815
+ count: i,
1816
+ total,
1817
+ elapsedMs: elapsed,
1818
+ remainingMs: remaining,
1819
+ status: `running ${name}: ${i}/${total}`
1820
+ };
1821
+ this.context.logger.progress("running", {
1822
+ prefix: name,
1823
+ count: i,
1824
+ total
1825
+ });
1826
+ await reportProgress(progress);
1827
+ await sleepMs(delay);
1828
+ }
1829
+ return {
1830
+ success: true,
1831
+ results: {
1832
+ message: `Completed ${total} iterations`,
1833
+ total,
1834
+ delay,
1835
+ name
1836
+ }
1837
+ };
1838
+ }
1839
+ };
1840
+
1841
+ // src/tasks/coreTasks/TaskShellCommand.ts
1842
+ var import_node_child_process = require("child_process");
1843
+ function runShellCommand(command, cwd) {
1844
+ return new Promise((resolve, reject) => {
1845
+ const child = (0, import_node_child_process.spawn)(command, {
1846
+ shell: true,
1847
+ cwd: cwd || process.cwd(),
1848
+ stdio: ["ignore", "pipe", "pipe"]
1849
+ });
1850
+ let output = "";
1851
+ let stderr = "";
1852
+ child.stdout.on("data", (chunk) => {
1853
+ output += String(chunk);
1854
+ });
1855
+ child.stderr.on("data", (chunk) => {
1856
+ stderr += String(chunk);
1857
+ });
1858
+ child.on("error", (error) => {
1859
+ reject(error);
1860
+ });
1861
+ child.on("close", (exitCode, signal) => {
1862
+ resolve({
1863
+ exitCode,
1864
+ output: output.trim(),
1865
+ stderr: stderr.trim(),
1866
+ signal
1867
+ });
1868
+ });
1869
+ });
1870
+ }
1871
+ var TaskShellCommand = class extends TaskMaster {
1872
+ async run() {
1873
+ const params = this.task?.params;
1874
+ const commandRaw = typeof params === "string" ? params : params?.command;
1875
+ const cwdRaw = typeof params === "string" ? void 0 : params?.cwd;
1876
+ const command = typeof commandRaw === "string" ? commandRaw.trim() : "";
1877
+ const cwd = typeof cwdRaw === "string" && cwdRaw.trim() ? cwdRaw.trim() : void 0;
1878
+ if (!command) {
1879
+ return {
1880
+ success: false,
1881
+ results: {
1882
+ error: 'Validation failed: param "command" must be a non-empty string',
1883
+ received: this.task?.params ?? null
1884
+ }
1885
+ };
1886
+ }
1887
+ try {
1888
+ const result = await runShellCommand(command, cwd);
1889
+ const success = result.exitCode === 0;
1890
+ this.context.logger.info?.(
1891
+ `[TaskShellCommand] command="${command}" exitCode=${String(result.exitCode)} (${this.task.id})`
1892
+ );
1893
+ return {
1894
+ success,
1895
+ results: {
1896
+ command,
1897
+ cwd: cwd ?? process.cwd(),
1898
+ output: result.output,
1899
+ stderr: result.stderr,
1900
+ exitCode: result.exitCode,
1901
+ signal: result.signal
1902
+ }
1903
+ };
1904
+ } catch (error) {
1905
+ return {
1906
+ success: false,
1907
+ results: {
1908
+ command,
1909
+ cwd: cwd ?? process.cwd(),
1910
+ output: "",
1911
+ stderr: "",
1912
+ exitCode: null,
1913
+ error: error?.message ?? String(error)
1914
+ }
1915
+ };
1916
+ }
1917
+ }
1918
+ };
1919
+
1920
+ // src/tasks/coreTasks/TaskSystemInfo.ts
1921
+ var import_node_os2 = __toESM(require("os"), 1);
1922
+ var import_promises2 = __toESM(require("fs/promises"), 1);
1923
+ function toGb(valueBytes) {
1924
+ return `${(valueBytes / 1024 ** 3).toFixed(2)} GB`;
1925
+ }
1926
+ function toMb(valueBytes) {
1927
+ return `${(valueBytes / 1024 ** 2).toFixed(2)} MB`;
1928
+ }
1929
+ async function getDiskStats() {
1930
+ const stats = await import_promises2.default.statfs("/");
1931
+ const total = Number(stats.bsize) * Number(stats.blocks);
1932
+ const free = Number(stats.bsize) * Number(stats.bavail);
1933
+ const used = total - free;
1934
+ return {
1935
+ total: toGb(total),
1936
+ used: toGb(used),
1937
+ free: toGb(free)
1938
+ };
1939
+ }
1940
+ var TaskSystemInfo = class extends TaskMaster {
1941
+ async run() {
1942
+ try {
1943
+ const totalMemory = import_node_os2.default.totalmem();
1944
+ const freeMemory = import_node_os2.default.freemem();
1945
+ const usedMemory = totalMemory - freeMemory;
1946
+ const cpus = import_node_os2.default.cpus();
1947
+ const cpuUtilization = cpus.map((cpu) => {
1948
+ const total = Object.values(cpu.times).reduce((acc, time) => acc + time, 0);
1949
+ const usage = (total - cpu.times.idle) / total * 100;
1950
+ return Number(usage.toFixed(2));
1951
+ });
1952
+ const processMemory = process.memoryUsage();
1953
+ const disk = await getDiskStats();
1954
+ const results = {
1955
+ memory: {
1956
+ total: toGb(totalMemory),
1957
+ used: toGb(usedMemory),
1958
+ free: toGb(freeMemory)
1959
+ },
1960
+ processMemory: {
1961
+ rss: toMb(processMemory.rss),
1962
+ heapTotal: toMb(processMemory.heapTotal),
1963
+ heapUsed: toMb(processMemory.heapUsed),
1964
+ external: toMb(processMemory.external)
1965
+ },
1966
+ disk,
1967
+ cpu: {
1968
+ cores: cpuUtilization.length,
1969
+ utilization: cpuUtilization
1970
+ },
1971
+ runtime: {
1972
+ platform: import_node_os2.default.platform(),
1973
+ arch: import_node_os2.default.arch(),
1974
+ uptimeSec: import_node_os2.default.uptime(),
1975
+ hostname: import_node_os2.default.hostname()
1976
+ }
1977
+ };
1978
+ this.context.logger.info?.(`[TaskSystemInfo] collected system metrics (${this.task.id})`);
1979
+ return { success: true, results };
1980
+ } catch (error) {
1981
+ return {
1982
+ success: false,
1983
+ results: {
1984
+ error: "Can't collect system stats",
1985
+ message: error?.message ?? String(error)
1986
+ }
1987
+ };
1988
+ }
1989
+ }
1990
+ };
1991
+
1992
+ // src/tasks/coreTasks/TaskSumAB.ts
1993
+ var TaskSumAB = class extends TaskMaster {
1994
+ async run() {
1995
+ const a = this.task?.params?.a;
1996
+ const b = this.task?.params?.b;
1997
+ if (typeof a !== "number" || Number.isNaN(a)) {
1998
+ return {
1999
+ success: false,
2000
+ results: {
2001
+ error: 'Validation failed: param "a" must be a valid number',
2002
+ received: { a, b }
2003
+ }
2004
+ };
2005
+ }
2006
+ if (typeof b !== "number" || Number.isNaN(b)) {
2007
+ return {
2008
+ success: false,
2009
+ results: {
2010
+ error: 'Validation failed: param "b" must be a valid number',
2011
+ received: { a, b }
2012
+ }
2013
+ };
2014
+ }
2015
+ const sum = a + b;
2016
+ this.context.logger.info?.(`[TaskSumAB] ${a} + ${b} = ${sum} (${this.task.id})`);
2017
+ return {
2018
+ success: true,
2019
+ results: { a, b, sum }
2020
+ };
2021
+ }
2022
+ };
2023
+
2024
+ // src/tasks/coreTasks/TaskStopRunner.ts
2025
+ var TaskStopRunner = class extends TaskMaster {
2026
+ async run() {
2027
+ const allowanceMs = Number(this.task?.params?.allowanceMs ?? 5e3);
2028
+ this.context.logger.warn?.(`[TaskStopRunner] stop requested (allowanceMs=${allowanceMs})`);
2029
+ return {
2030
+ success: true,
2031
+ results: {
2032
+ stopRunner: true,
2033
+ allowanceMs,
2034
+ message: "Runner stop requested"
2035
+ }
2036
+ };
2037
+ }
2038
+ };
2039
+
2040
+ // src/tasks/TasksRegistry.ts
2041
+ var TasksRegistry = class _TasksRegistry {
2042
+ map = {};
2043
+ constructor(initial) {
2044
+ if (initial) {
2045
+ this.addMany(initial);
2046
+ }
2047
+ }
2048
+ static withCoreTasks() {
2049
+ return new _TasksRegistry().add("ping", TaskPing).add("sampleProcess", TaskSampleProcess).add("shellCommand", TaskShellCommand).add("systemInfo", TaskSystemInfo).add("taskSumAB", TaskSumAB).add("stopRunner", TaskStopRunner).add("stop", TaskStopRunner);
2050
+ }
2051
+ add(taskName, taskClass) {
2052
+ this.map[taskName] = taskClass;
2053
+ return this;
2054
+ }
2055
+ addMany(entries) {
2056
+ for (const [name, klass] of Object.entries(entries)) {
2057
+ this.add(name, klass);
2058
+ }
2059
+ return this;
2060
+ }
2061
+ get(taskName) {
2062
+ return this.map[taskName];
2063
+ }
2064
+ listSupportedTasks() {
2065
+ return Object.keys(this.map).sort();
2066
+ }
2067
+ toObject() {
2068
+ return { ...this.map };
2069
+ }
2070
+ };
2071
+
2072
+ // src/tasks/taskScriptRunner.ts
2073
+ var import_node_child_process2 = require("child_process");
2074
+ function toCliArgs(args = []) {
2075
+ return args.filter((a) => typeof a === "string" && a.length > 0);
2076
+ }
2077
+ function formatChildLogPrefix(task) {
2078
+ return `${task.task}:${task.id.slice(0, 8)}${task.opid ? `:${task.opid}` : ""}`;
2079
+ }
2080
+ async function runNodeTaskScript(context, options) {
2081
+ const cliArgs = toCliArgs(["--route=ipc", "--mode=json", ...options.args || []]);
2082
+ const inheritedExecArgs = Array.isArray(process.execArgv) ? [...process.execArgv] : [];
2083
+ const hasTsRuntimeInParent = inheritedExecArgs.some((arg) => /tsx|ts-node/i.test(arg));
2084
+ const nodeArgs = hasTsRuntimeInParent ? [...inheritedExecArgs, options.scriptPath, ...cliArgs] : ["--import", "tsx", options.scriptPath, ...cliArgs];
2085
+ const child = (0, import_node_child_process2.spawn)(
2086
+ process.execPath,
2087
+ nodeArgs,
2088
+ {
2089
+ cwd: options.cwd || process.cwd(),
2090
+ stdio: ["ignore", "pipe", "pipe", "ipc"],
2091
+ env: {
2092
+ ...process.env,
2093
+ TASK_ID: options.task.id,
2094
+ TASK_NAME: options.task.task,
2095
+ TASK_OPID: options.task.opid || ""
2096
+ }
2097
+ }
2098
+ );
2099
+ let stdout = "";
2100
+ let stderr = "";
2101
+ let workerResult = null;
2102
+ let hadErrorMessage = false;
2103
+ const prefix = formatChildLogPrefix(options.task);
2104
+ const db = context.db;
2105
+ const tasksTable = context.params?.get?.("table") || "tasks";
2106
+ let progressWriteChain = Promise.resolve();
2107
+ let progressCallbackChain = Promise.resolve();
2108
+ const updateProgress = (text) => {
2109
+ if (!db || !text || !text.trim()) return;
2110
+ progressWriteChain = progressWriteChain.then(async () => {
2111
+ await db(tasksTable).where({ id: options.task.id }).update({ progress: text.slice(0, 4e3) });
2112
+ }).catch((error) => {
2113
+ context.logger.warn?.(
2114
+ `[tasks] failed to update progress for task=${options.task.id}: ${error?.message ?? String(error)}`
2115
+ );
2116
+ });
2117
+ if (options.onProgress) {
2118
+ progressCallbackChain = progressCallbackChain.then(async () => {
2119
+ await options.onProgress?.(text.slice(0, 4e3));
2120
+ }).catch((error) => {
2121
+ context.logger.warn?.(
2122
+ `[tasks] reportProgress callback failed for task=${options.task.id}: ${error?.message ?? String(error)}`
2123
+ );
2124
+ });
2125
+ }
2126
+ };
2127
+ const payloadToProgressText = (payload) => {
2128
+ if (!payload) return "";
2129
+ if (typeof payload === "string") return payload;
2130
+ if (typeof payload.message === "string" && payload.level === "progress" && payload.count !== void 0 && payload.total !== void 0) {
2131
+ const pfx = payload.prefix ? `${payload.prefix} ` : "";
2132
+ return `${pfx}${payload.message} ${payload.count}/${payload.total}`;
2133
+ }
2134
+ if (typeof payload.message === "string") return payload.message;
2135
+ if (payload.level === "progress" && payload.count !== void 0 && payload.total !== void 0) {
2136
+ const pfx = payload.prefix ? `${payload.prefix} ` : "";
2137
+ return `${pfx}${payload.count}/${payload.total}`;
2138
+ }
2139
+ return "";
2140
+ };
2141
+ child.stdout.on("data", (chunk) => {
2142
+ const text = String(chunk);
2143
+ stdout += text;
2144
+ if (text.trim()) {
2145
+ context.logger.info?.(`[child:${prefix}] ${text.trimEnd()}`);
2146
+ updateProgress(text.trim().replace(/\s+/g, " ").slice(0, 400));
2147
+ }
2148
+ });
2149
+ child.stderr.on("data", (chunk) => {
2150
+ const text = String(chunk);
2151
+ stderr += text;
2152
+ if (text.trim()) {
2153
+ context.logger.warn?.(`[child:${prefix}] ${text.trimEnd()}`);
2154
+ }
2155
+ });
2156
+ child.on("message", (message) => {
2157
+ if (message && typeof message === "object" && "__taskWorkerResult" in message) {
2158
+ workerResult = message.__taskWorkerResult;
2159
+ return;
2160
+ }
2161
+ if (message && typeof message === "object") {
2162
+ const level = typeof message.level === "string" ? message.level.toLowerCase() : "";
2163
+ if (level === "error" || level === "fatal") {
2164
+ hadErrorMessage = true;
2165
+ }
2166
+ }
2167
+ appendTaskIpcLog(context, options.task, message);
2168
+ const progressText = payloadToProgressText(message);
2169
+ if (progressText) {
2170
+ updateProgress(progressText);
2171
+ if (typeof message === "object" && message?.level === "progress" && message.count !== void 0 && message.total !== void 0) {
2172
+ const countNum = Number(String(message.count).trim());
2173
+ const totalNum = Number(message.total);
2174
+ if (Number.isFinite(countNum) && Number.isFinite(totalNum) && totalNum > 0) {
2175
+ context.logger.progress(message.message || "progress", {
2176
+ prefix: message.prefix || prefix,
2177
+ count: countNum,
2178
+ total: totalNum
2179
+ });
2180
+ } else {
2181
+ context.logger.info?.(`[child:${prefix}] ${progressText}`);
2182
+ }
2183
+ } else {
2184
+ context.logger.info?.(`[child:${prefix}] ${progressText}`);
2185
+ }
2186
+ }
2187
+ });
2188
+ return await new Promise((resolve, reject) => {
2189
+ child.on("error", (error) => reject(error));
2190
+ child.on("close", (exitCode, signal) => {
2191
+ Promise.allSettled([progressWriteChain, progressCallbackChain]).finally(() => {
2192
+ resolve({
2193
+ exitCode,
2194
+ signal,
2195
+ stdout: stdout.trim(),
2196
+ stderr: stderr.trim(),
2197
+ workerResult,
2198
+ hadErrorMessage
2199
+ });
2200
+ });
2201
+ });
2202
+ });
2203
+ }
2204
+
2205
+ // src/tasks/index.ts
2206
+ var LOCKED_BY_ERROR_MESSAGE = "locked by error";
2207
+ var defaultTasksRegistry = TasksRegistry.withCoreTasks();
2208
+ function getDb3(context) {
2209
+ const db = context.db;
2210
+ if (!db) {
2211
+ throw new Error("Tasks component requires context.db. Initialize DB first and attach to context.");
2212
+ }
2213
+ return db;
2214
+ }
2215
+ function normalizeRegistry(registry) {
2216
+ if (!registry) return defaultTasksRegistry;
2217
+ if (registry instanceof TasksRegistry) return registry;
2218
+ return new TasksRegistry().addMany(registry);
2219
+ }
2220
+ function normalizeAllowedTasks(value) {
2221
+ if (!value) return void 0;
2222
+ if (Array.isArray(value)) {
2223
+ const out2 = value.map((v) => String(v).trim()).filter(Boolean);
2224
+ return out2.length ? out2 : void 0;
2225
+ }
2226
+ const out = String(value).split(",").map((v) => v.trim()).filter(Boolean);
2227
+ return out.length ? out : void 0;
2228
+ }
2229
+ async function enqueueStopTask(context, target, queue = "tasks", allowanceMs = 5e3) {
2230
+ return enqueueTask(context, {
2231
+ queue,
2232
+ target,
2233
+ task: "stopRunner",
2234
+ params: { allowanceMs },
2235
+ priority: 1e6
2236
+ });
2237
+ }
2238
+ async function signalRunningTasksStop(context, runningTaskInstances, allowanceMs) {
2239
+ context.logger.warn?.(`[tasks] signaling ${runningTaskInstances.size} running task(s) to stop`);
2240
+ for (const [, taskInstance] of runningTaskInstances) {
2241
+ if (typeof taskInstance.requestStop === "function") {
2242
+ try {
2243
+ await taskInstance.requestStop(allowanceMs);
2244
+ } catch (error) {
2245
+ context.logger.warn?.("[tasks] task requestStop failed:", error);
2246
+ }
2247
+ }
2248
+ }
2249
+ context.emitter.emit("stop", allowanceMs);
2250
+ }
2251
+ async function executeClaimedTask(context, tasksTable, historyTable, row, registry, runningTaskInstances) {
2252
+ const db = getDb3(context);
2253
+ const taskName = row.task;
2254
+ const TaskClass = registry.get(taskName);
2255
+ const { paused_at: _pausedAt, ...rowForHistory } = row;
2256
+ if (!TaskClass) {
2257
+ const err = { message: `Unknown task "${taskName}"` };
2258
+ await db(historyTable).insert({
2259
+ ...rowForHistory,
2260
+ completed_at: /* @__PURE__ */ new Date(),
2261
+ success: false,
2262
+ params: toJsonColumn(row.params),
2263
+ results: toJsonColumn(err)
2264
+ });
2265
+ if (row.schedule) {
2266
+ await db(tasksTable).where({ id: row.id }).update({
2267
+ started_at: null,
2268
+ completed_at: /* @__PURE__ */ new Date(),
2269
+ success: false,
2270
+ results: toJsonColumn(err),
2271
+ past_due: null,
2272
+ paused_at: db.fn.now(),
2273
+ progress: LOCKED_BY_ERROR_MESSAGE
2274
+ });
2275
+ } else {
2276
+ await db(tasksTable).where({ id: row.id }).delete();
2277
+ }
2278
+ return { stopRunnerRequested: false, stopAllowanceMs: 0 };
2279
+ }
2280
+ let success = false;
2281
+ let results = null;
2282
+ let taskInstance = null;
2283
+ try {
2284
+ taskInstance = new TaskClass(context, row);
2285
+ runningTaskInstances.set(row.id, taskInstance);
2286
+ const runResult = await taskInstance.run((progress) => updateTaskProgress(context, tasksTable, row.id, progress));
2287
+ success = !!runResult?.success;
2288
+ results = runResult?.results ?? null;
2289
+ } catch (error) {
2290
+ success = false;
2291
+ results = {
2292
+ message: error?.message ?? String(error),
2293
+ name: error?.name ?? "Error",
2294
+ stack: error?.stack ?? null
2295
+ };
2296
+ } finally {
2297
+ runningTaskInstances.delete(row.id);
2298
+ }
2299
+ await db(historyTable).insert({
2300
+ ...rowForHistory,
2301
+ completed_at: /* @__PURE__ */ new Date(),
2302
+ success,
2303
+ params: toJsonColumn(row.params),
2304
+ results: toJsonColumn(results)
2305
+ });
2306
+ if (!success) {
2307
+ const dbName = String(context?.params?.get?.("dbName") || "local");
2308
+ const tableName = String(context?.params?.get?.("table") || "tasks");
2309
+ const fallbackRecoverCommand = [
2310
+ "npx",
2311
+ "tsx",
2312
+ "examples/tasks/recover-task.ts",
2313
+ `--dbName='${dbName.replace(/'/g, `'\\''`)}'`,
2314
+ `--table='${tableName.replace(/'/g, `'\\''`)}'`,
2315
+ `--id='${String(row.id).replace(/'/g, `'\\''`)}'`
2316
+ ].join(" ");
2317
+ const rerunCommand = results && typeof results === "object" && results.rerunCommand ? results.rerunCommand : fallbackRecoverCommand;
2318
+ appendTaskIpcLog(context, row, {
2319
+ level: "error",
2320
+ message: `[tasks] task failed: ${row.task} id=${row.id}. Once problem is fixed, re-run: ${rerunCommand}`,
2321
+ details: results
2322
+ });
2323
+ }
2324
+ if (row.schedule) {
2325
+ if (success) {
2326
+ await db(tasksTable).where({ id: row.id }).update({
2327
+ started_at: null,
2328
+ completed_at: /* @__PURE__ */ new Date(),
2329
+ success,
2330
+ results: toJsonColumn(results),
2331
+ progress: null,
2332
+ past_due: null
2333
+ });
2334
+ } else {
2335
+ await db(tasksTable).where({ id: row.id }).update({
2336
+ started_at: null,
2337
+ completed_at: /* @__PURE__ */ new Date(),
2338
+ success,
2339
+ results: toJsonColumn(results),
2340
+ paused_at: db.fn.now(),
2341
+ progress: LOCKED_BY_ERROR_MESSAGE,
2342
+ past_due: null
2343
+ });
2344
+ }
2345
+ } else {
2346
+ await db(tasksTable).where({ id: row.id }).delete();
2347
+ }
2348
+ const stopRunnerRequested = !!(results && typeof results === "object" && results.stopRunner === true);
2349
+ const stopAllowanceMs = stopRunnerRequested ? Number(results.allowanceMs ?? 5e3) : 0;
2350
+ return { stopRunnerRequested, stopAllowanceMs };
2351
+ }
2352
+ async function claimNextRunnableTask(context, tasksTable, target, registry, scanLimit, taskNames) {
2353
+ const db = getDb3(context);
2354
+ let query = db(tasksTable).whereNull("started_at").whereNull("paused_at").where({ target }).orderByRaw("CASE WHEN past_due IS NULL THEN 1 ELSE 0 END ASC").orderBy([{ column: "priority", order: "desc" }]).orderByRaw("CASE WHEN completed_at IS NULL THEN 0 ELSE 1 END ASC").orderBy([{ column: "completed_at", order: "asc" }, { column: "created_at", order: "asc" }]).limit(scanLimit);
2355
+ if (taskNames && taskNames.length > 0) {
2356
+ query = query.whereIn("task", taskNames);
2357
+ }
2358
+ const candidates = await query;
2359
+ for (const row of candidates) {
2360
+ if (!row.past_due && row.schedule && !timeMatcher(row.schedule)) {
2361
+ continue;
2362
+ }
2363
+ const TaskClass = registry.get(row.task);
2364
+ if (TaskClass) {
2365
+ const taskInstance = new TaskClass(context, row);
2366
+ const reason = taskInstance.cantRunReason ? await taskInstance.cantRunReason() : null;
2367
+ if (reason) {
2368
+ if (!row.past_due) {
2369
+ await db(tasksTable).where({ id: row.id }).update({
2370
+ past_due: db.fn.now(),
2371
+ progress: String(reason)
2372
+ });
2373
+ }
2374
+ continue;
2375
+ }
2376
+ }
2377
+ const updated = await db(tasksTable).where({ id: row.id }).whereNull("started_at").whereNull("paused_at").update({ started_at: db.fn.now() }).returning("*");
2378
+ const claimed = Array.isArray(updated) ? updated[0] : null;
2379
+ if (claimed) return claimed;
2380
+ }
2381
+ return null;
2382
+ }
2383
+ async function runTasksLoop(context, options) {
2384
+ const queue = options.queue ?? "tasks";
2385
+ const target = options.target;
2386
+ const pollMs = options.pollMs ?? 1e3;
2387
+ const maxParallel = options.maxParallel ?? 1;
2388
+ const scanLimit = options.scanLimit ?? 100;
2389
+ const allowedTasks = normalizeAllowedTasks(options.allowedTasks);
2390
+ const registry = normalizeRegistry(options.registry);
2391
+ const { tasksTable, historyTable } = queueToTableNames(queue);
2392
+ if (!target) throw new Error("runTasksLoop: target is required");
2393
+ const runningPromises = /* @__PURE__ */ new Set();
2394
+ const runningTaskInstances = /* @__PURE__ */ new Map();
2395
+ let runningStopControlPromise = null;
2396
+ let stopRequested = false;
2397
+ let stopAllowanceMs = 5e3;
2398
+ context.__tasksRunnerStop = false;
2399
+ let registryReg = null;
2400
+ let registryInterval = null;
2401
+ const hbGroup = options.runnerServiceGroup?.trim();
2402
+ if (hbGroup) {
2403
+ const identityDir = options.runnerIdentityDir ?? "./data/runner-identities";
2404
+ const hbIntervalMs = options.runnerHeartbeatIntervalMs ?? 1e4;
2405
+ const staleMs = options.runnerHeartbeatStaleMs ?? 45e3;
2406
+ const defaultMeta = {
2407
+ component: "tasks-runner",
2408
+ allowedTasks: allowedTasks?.length ? allowedTasks.join(",") : "all"
2409
+ };
2410
+ registryReg = await registerInServicesRegistry(context, {
2411
+ queue,
2412
+ target,
2413
+ serviceGroup: hbGroup,
2414
+ serviceName: options.runnerServiceName,
2415
+ identityDir,
2416
+ staleMs,
2417
+ groupMaxInstances: options.runnerGroupMaxInstances,
2418
+ enforceMaxInstances: options.runnerEnforceMaxInstances ?? true,
2419
+ metadata: options.runnerMetadata ?? defaultMeta
2420
+ });
2421
+ registryInterval = setInterval(() => {
2422
+ void touchServicesRegistry(context, registryReg).catch((err) => {
2423
+ context.logger.warn?.(`[services-registry] touch failed: ${err?.message ?? String(err)}`);
2424
+ });
2425
+ }, hbIntervalMs);
2426
+ }
2427
+ try {
2428
+ while (!context.isStop() && !stopRequested && !context.__tasksRunnerStop) {
2429
+ if (!runningStopControlPromise) {
2430
+ const claimedStopTask = await claimNextRunnableTask(
2431
+ context,
2432
+ tasksTable,
2433
+ target,
2434
+ registry,
2435
+ 10,
2436
+ ["stopRunner", "stop"]
2437
+ );
2438
+ if (claimedStopTask) {
2439
+ runningStopControlPromise = executeClaimedTask(
2440
+ context,
2441
+ tasksTable,
2442
+ historyTable,
2443
+ claimedStopTask,
2444
+ registry,
2445
+ runningTaskInstances
2446
+ ).then(async (outcome) => {
2447
+ if (outcome.stopRunnerRequested && !stopRequested) {
2448
+ stopRequested = true;
2449
+ stopAllowanceMs = outcome.stopAllowanceMs || 5e3;
2450
+ context.__tasksRunnerStop = true;
2451
+ await signalRunningTasksStop(context, runningTaskInstances, stopAllowanceMs);
2452
+ }
2453
+ }).finally(() => {
2454
+ runningStopControlPromise = null;
2455
+ });
2456
+ }
2457
+ }
2458
+ while (runningPromises.size < maxParallel) {
2459
+ const claimed = await claimNextRunnableTask(
2460
+ context,
2461
+ tasksTable,
2462
+ target,
2463
+ registry,
2464
+ scanLimit,
2465
+ allowedTasks
2466
+ );
2467
+ if (!claimed) break;
2468
+ const p = executeClaimedTask(context, tasksTable, historyTable, claimed, registry, runningTaskInstances).then(async (outcome) => {
2469
+ if (outcome.stopRunnerRequested && !stopRequested) {
2470
+ stopRequested = true;
2471
+ stopAllowanceMs = outcome.stopAllowanceMs || 5e3;
2472
+ context.__tasksRunnerStop = true;
2473
+ await signalRunningTasksStop(context, runningTaskInstances, stopAllowanceMs);
2474
+ }
2475
+ }).finally(() => {
2476
+ runningPromises.delete(p);
2477
+ });
2478
+ runningPromises.add(p);
2479
+ }
2480
+ await sleepMs(pollMs);
2481
+ }
2482
+ if (context.isStop() && !stopRequested) {
2483
+ await signalRunningTasksStop(context, runningTaskInstances, 5e3);
2484
+ }
2485
+ if (runningPromises.size > 0) {
2486
+ if (stopRequested) {
2487
+ await Promise.race([
2488
+ Promise.allSettled(Array.from(runningPromises)),
2489
+ sleepMs(stopAllowanceMs).then(() => {
2490
+ context.logger.warn?.(
2491
+ `[tasks] stop allowance (${stopAllowanceMs}ms) reached; ${runningPromises.size} task(s) still running`
2492
+ );
2493
+ })
2494
+ ]);
2495
+ } else {
2496
+ await Promise.allSettled(Array.from(runningPromises));
2497
+ }
2498
+ }
2499
+ } finally {
2500
+ if (registryInterval) {
2501
+ clearInterval(registryInterval);
2502
+ registryInterval = null;
2503
+ }
2504
+ if (registryReg) {
2505
+ await unregisterServicesRegistry(context, registryReg).catch((err) => {
2506
+ context.logger.warn?.(`[services-registry] unregister failed: ${err?.message ?? String(err)}`);
2507
+ });
2508
+ registryReg = null;
2509
+ delete context.servicesRegistry;
2510
+ delete context.runnerHeartbeat;
2511
+ }
2512
+ }
2513
+ }
2514
+ async function waitForTaskResult(context, taskId, options = {}) {
2515
+ const db = getDb3(context);
2516
+ const queue = options.queue ?? "tasks";
2517
+ const timeoutMs = options.timeoutMs ?? 6e4;
2518
+ const pollMs = options.pollMs ?? 500;
2519
+ const { tasksTable, historyTable } = queueToTableNames(queue);
2520
+ const deadline = Date.now() + timeoutMs;
2521
+ while (Date.now() <= deadline) {
2522
+ const done = await db(historyTable).where({ id: taskId }).orderBy("created_at", "desc").first();
2523
+ if (done) return done;
2524
+ const pending = await db(tasksTable).where({ id: taskId }).first();
2525
+ if (!pending) {
2526
+ const maybeDone = await db(historyTable).where({ id: taskId }).orderBy("created_at", "desc").first();
2527
+ return maybeDone ?? null;
2528
+ }
2529
+ await sleepMs(pollMs);
2530
+ }
2531
+ return null;
2532
+ }
2533
+ var TasksManager = class _TasksManager {
2534
+ context;
2535
+ queue;
2536
+ target;
2537
+ recreateTaskTables;
2538
+ pollMs;
2539
+ maxParallel;
2540
+ scanLimit;
2541
+ allowedTasks;
2542
+ registry;
2543
+ runnerServiceGroup;
2544
+ runnerServiceName;
2545
+ runnerIdentityDir;
2546
+ runnerHeartbeatIntervalMs;
2547
+ runnerHeartbeatStaleMs;
2548
+ runnerGroupMaxInstances;
2549
+ runnerEnforceMaxInstances;
2550
+ runnerMetadata;
2551
+ constructor(context, options = {}) {
2552
+ this.context = context;
2553
+ this.queue = options.queue ?? "tasks";
2554
+ this.target = options.target ?? "localRunner";
2555
+ this.recreateTaskTables = options.recreateTaskTables ?? false;
2556
+ this.pollMs = options.pollMs ?? 1e3;
2557
+ this.maxParallel = options.maxParallel ?? 1;
2558
+ this.scanLimit = options.scanLimit ?? 100;
2559
+ this.allowedTasks = normalizeAllowedTasks(options.allowedTasks);
2560
+ this.registry = normalizeRegistry(options.registry);
2561
+ this.runnerServiceGroup = options.runnerServiceGroup;
2562
+ this.runnerServiceName = options.runnerServiceName;
2563
+ this.runnerIdentityDir = options.runnerIdentityDir;
2564
+ this.runnerHeartbeatIntervalMs = options.runnerHeartbeatIntervalMs;
2565
+ this.runnerHeartbeatStaleMs = options.runnerHeartbeatStaleMs;
2566
+ this.runnerGroupMaxInstances = options.runnerGroupMaxInstances;
2567
+ this.runnerEnforceMaxInstances = options.runnerEnforceMaxInstances;
2568
+ this.runnerMetadata = options.runnerMetadata;
2569
+ }
2570
+ static init(context, options = {}) {
2571
+ const defs = {
2572
+ table: "string default tasks",
2573
+ target: "string default localRunner",
2574
+ recreateTaskTables: "boolean default false",
2575
+ pollMs: "number default 1000",
2576
+ maxParallel: "number default 1",
2577
+ scanLimit: "number default 100",
2578
+ allowedTasks: "string",
2579
+ runnerServiceGroup: "string",
2580
+ runnerServiceName: "string",
2581
+ runnerIdentityDir: "string default ./data/runner-identities",
2582
+ runnerHeartbeatIntervalMs: "number default 10000",
2583
+ runnerHeartbeatStaleMs: "number default 45000",
2584
+ runnerGroupMaxInstances: "number",
2585
+ runnerEnforceMaxInstances: "boolean default true"
2586
+ };
2587
+ const discovered = context.params.getAllForModule(defs);
2588
+ const resolved = {
2589
+ queue: discovered.table,
2590
+ target: discovered.target,
2591
+ recreateTaskTables: discovered.recreateTaskTables,
2592
+ pollMs: discovered.pollMs,
2593
+ maxParallel: discovered.maxParallel,
2594
+ scanLimit: discovered.scanLimit,
2595
+ allowedTasks: discovered.allowedTasks,
2596
+ runnerServiceGroup: discovered.runnerServiceGroup,
2597
+ runnerServiceName: discovered.runnerServiceName,
2598
+ runnerIdentityDir: discovered.runnerIdentityDir,
2599
+ runnerHeartbeatIntervalMs: discovered.runnerHeartbeatIntervalMs,
2600
+ runnerHeartbeatStaleMs: discovered.runnerHeartbeatStaleMs,
2601
+ runnerGroupMaxInstances: discovered.runnerGroupMaxInstances,
2602
+ runnerEnforceMaxInstances: discovered.runnerEnforceMaxInstances,
2603
+ ...options
2604
+ };
2605
+ return new _TasksManager(context, resolved);
2606
+ }
2607
+ async ensureTaskTables(options = {}) {
2608
+ await ensureTaskTables(this.context, {
2609
+ queue: this.queue,
2610
+ recreate: options.recreate ?? this.recreateTaskTables
2611
+ });
2612
+ }
2613
+ async runTasksLoop(options = {}) {
2614
+ await runTasksLoop(this.context, {
2615
+ queue: options.queue ?? this.queue,
2616
+ target: options.target ?? this.target,
2617
+ pollMs: options.pollMs ?? this.pollMs,
2618
+ maxParallel: options.maxParallel ?? this.maxParallel,
2619
+ scanLimit: options.scanLimit ?? this.scanLimit,
2620
+ allowedTasks: options.allowedTasks ?? this.allowedTasks,
2621
+ registry: options.registry ?? this.registry,
2622
+ runnerServiceGroup: options.runnerServiceGroup ?? this.runnerServiceGroup,
2623
+ runnerServiceName: options.runnerServiceName ?? this.runnerServiceName,
2624
+ runnerIdentityDir: options.runnerIdentityDir ?? this.runnerIdentityDir,
2625
+ runnerHeartbeatIntervalMs: options.runnerHeartbeatIntervalMs ?? this.runnerHeartbeatIntervalMs,
2626
+ runnerHeartbeatStaleMs: options.runnerHeartbeatStaleMs ?? this.runnerHeartbeatStaleMs,
2627
+ runnerGroupMaxInstances: options.runnerGroupMaxInstances ?? this.runnerGroupMaxInstances,
2628
+ runnerEnforceMaxInstances: options.runnerEnforceMaxInstances ?? this.runnerEnforceMaxInstances,
2629
+ runnerMetadata: options.runnerMetadata ?? this.runnerMetadata
2630
+ });
2631
+ }
2632
+ };
2633
+ // Annotate the CommonJS export names for ESM import in node:
2634
+ 0 && (module.exports = {
2635
+ TaskMaster,
2636
+ TaskPing,
2637
+ TaskSampleProcess,
2638
+ TaskShellCommand,
2639
+ TaskStopRunner,
2640
+ TaskSumAB,
2641
+ TaskSystemInfo,
2642
+ TasksManager,
2643
+ TasksRegistry,
2644
+ appendTaskIpcLog,
2645
+ defaultTasksRegistry,
2646
+ enqueueStopTask,
2647
+ enqueueTask,
2648
+ ensureTaskTables,
2649
+ listAliveRunnerHeartbeats,
2650
+ listServicesRegistry,
2651
+ queueToTableNames,
2652
+ registerInServicesRegistry,
2653
+ registerRunnerHeartbeat,
2654
+ runNodeTaskScript,
2655
+ runTasksLoop,
2656
+ runnerHeartbeatsTable,
2657
+ servicesRegistryTable,
2658
+ touchRunnerHeartbeat,
2659
+ touchServicesRegistry,
2660
+ unregisterRunnerHeartbeat,
2661
+ unregisterServicesRegistry,
2662
+ updateServicesRegistryMetadata,
2663
+ updateTaskProgress,
2664
+ waitForTaskResult
2665
+ });
2666
+ //# sourceMappingURL=tasks.cjs.map