@nmakarov/cli-toolkit 0.16.0 → 0.18.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.
package/dist/index.cjs CHANGED
@@ -1107,8 +1107,18 @@ __export(src_exports, {
1107
1107
  ScreenFooter: () => ScreenFooter,
1108
1108
  ScreenRow: () => ScreenRow,
1109
1109
  ScreenTitle: () => ScreenTitle,
1110
+ TaskMaster: () => TaskMaster,
1111
+ TaskPing: () => TaskPing,
1112
+ TaskSampleProcess: () => TaskSampleProcess,
1113
+ TaskShellCommand: () => TaskShellCommand,
1114
+ TaskStopRunner: () => TaskStopRunner,
1115
+ TaskSumAB: () => TaskSumAB,
1116
+ TaskSystemInfo: () => TaskSystemInfo,
1117
+ TasksManager: () => TasksManager,
1118
+ TasksRegistry: () => TasksRegistry,
1110
1119
  Text: () => import_ink5.Text,
1111
1120
  TextBlock: () => TextBlock,
1121
+ appendTaskIpcLog: () => appendTaskIpcLog,
1112
1122
  buildBreadcrumb: () => buildBreadcrumb,
1113
1123
  buildDetailBreadcrumb: () => buildDetailBreadcrumb,
1114
1124
  buildFooter: () => buildFooter,
@@ -1116,7 +1126,11 @@ __export(src_exports, {
1116
1126
  dbFindAndConnect: () => dbFindAndConnect,
1117
1127
  dbInit: () => dbInit,
1118
1128
  defaultFileSynopsisFunction: () => defaultFileSynopsisFunction,
1129
+ defaultTasksRegistry: () => defaultTasksRegistry,
1119
1130
  defaultVersionSynopsisFunction: () => defaultVersionSynopsisFunction,
1131
+ enqueueStopTask: () => enqueueStopTask,
1132
+ enqueueTask: () => enqueueTask,
1133
+ ensureTaskTables: () => ensureTaskTables,
1120
1134
  getArgsInstance: () => getArgsInstance,
1121
1135
  h: () => import_react5.createElement,
1122
1136
  joiEdateType: () => joiEdateType,
@@ -1125,6 +1139,9 @@ __export(src_exports, {
1125
1139
  listTables: () => listTables,
1126
1140
  load: () => load,
1127
1141
  organizeFooterMessages: () => organizeFooterMessages,
1142
+ queueToTableNames: () => queueToTableNames,
1143
+ runNodeTaskScript: () => runNodeTaskScript,
1144
+ runTasksLoop: () => runTasksLoop,
1128
1145
  setupContext: () => setupContext,
1129
1146
  showListScreen: () => showListScreen,
1130
1147
  showMenuScreen: () => showMenuScreen,
@@ -1132,12 +1149,14 @@ __export(src_exports, {
1132
1149
  showMultiColumnListWithPreviewScreen: () => showMultiColumnListWithPreviewScreen,
1133
1150
  showScreen: () => showScreen,
1134
1151
  showWordGridScreen: () => showWordGridScreen,
1152
+ updateTaskProgress: () => updateTaskProgress,
1135
1153
  useCallback: () => import_react5.useCallback,
1136
1154
  useEffect: () => import_react5.useEffect,
1137
1155
  useInput: () => import_ink5.useInput,
1138
1156
  useMemo: () => import_react5.useMemo,
1139
1157
  useRef: () => import_react5.useRef,
1140
- useState: () => import_react5.useState
1158
+ useState: () => import_react5.useState,
1159
+ waitForTaskResult: () => waitForTaskResult
1141
1160
  });
1142
1161
  module.exports = __toCommonJS(src_exports);
1143
1162
 
@@ -2444,7 +2463,7 @@ var FileDatabase = class _FileDatabase {
2444
2463
  const versions = await this.getVersions();
2445
2464
  while (versions.length > this.maxVersions) {
2446
2465
  const versionToDelete = import_path4.default.resolve(this.getDestinationPath(), versions.shift());
2447
- this.logger.debug?.(`[FileDatabase] Deleting old version: ${versionToDelete}`);
2466
+ this.logger.silly?.(`[FileDatabase] Deleting old version: ${versionToDelete}`);
2448
2467
  await import_fs4.default.promises.rm(versionToDelete, { recursive: true, force: true });
2449
2468
  }
2450
2469
  return versionName;
@@ -2722,7 +2741,7 @@ var FileDatabase = class _FileDatabase {
2722
2741
  };
2723
2742
  this.metadata.files.push(fileEntry);
2724
2743
  this.lastFileData = null;
2725
- this.logger.debug?.(`[FileDatabase] Created new file: ${fileEntry.fileName}, fileNumber: ${this.currentFileNumber}`);
2744
+ this.logger.silly?.(`[FileDatabase] Created new file: ${fileEntry.fileName}, fileNumber: ${this.currentFileNumber}`);
2726
2745
  }
2727
2746
  /**
2728
2747
  * Figure out what data to write and which file to use (for pagination)
@@ -2755,7 +2774,7 @@ var FileDatabase = class _FileDatabase {
2755
2774
  const filesBeforeCreate = this.metadata.files.length;
2756
2775
  this.makeNewFile();
2757
2776
  newlyCreatedFileIndex = filesBeforeCreate;
2758
- this.logger.debug?.(`[FileDatabase] Creating new file for unique custom metadata combination, fileNumber: ${this.currentFileNumber}`);
2777
+ this.logger.silly?.(`[FileDatabase] Creating new file for unique custom metadata combination, fileNumber: ${this.currentFileNumber}`);
2759
2778
  } else if (this.metadata.files.length === 0) {
2760
2779
  this.makeNewFile();
2761
2780
  }
@@ -2804,7 +2823,7 @@ var FileDatabase = class _FileDatabase {
2804
2823
  dataLeftOver = null;
2805
2824
  }
2806
2825
  const fileName = this.metadata.files[this.metadata.files.length - 1].fileName;
2807
- this.logger.debug?.(
2826
+ this.logger.silly?.(
2808
2827
  `[FileDatabase] figureOutDataAndFileToWrite: filename=${fileName}, forceNewFile=${forceNewFile}, targetFileIndex=${targetFileIndex}, dataToWrite.length=${Array.isArray(dataToWrite) ? dataToWrite.length : "N/A"}, lastFileRecordsCount=${lastFileRecordsCount}`
2809
2828
  );
2810
2829
  return { dataToWrite, dataLeftOver, fileName };
@@ -2859,7 +2878,7 @@ var FileDatabase = class _FileDatabase {
2859
2878
  this.metadata.modifiedAt = (/* @__PURE__ */ new Date()).toISOString();
2860
2879
  this.metadata.dataType = detectDataType(dataToWrite);
2861
2880
  this.metadata.totalRecords = this.metadata.files.reduce((sum, file) => sum + (file.recordsCount || 0), 0);
2862
- this.logger.debug?.(
2881
+ this.logger.silly?.(
2863
2882
  `[FileDatabase] Updated metadata for file ${currentFile.fileName}: recordsCount=${recordsCount}, totalRecords=${this.metadata.totalRecords}`
2864
2883
  );
2865
2884
  }
@@ -2883,7 +2902,7 @@ var FileDatabase = class _FileDatabase {
2883
2902
  }
2884
2903
  try {
2885
2904
  await import_fs4.default.promises.writeFile(filePath, serializedData, "utf8");
2886
- this.logger.debug?.(`[FileDatabase] Wrote ${bytesToHumanReadable(requiredBytes)} to ${filePath}`);
2905
+ this.logger.silly?.(`[FileDatabase] Wrote ${bytesToHumanReadable(requiredBytes)} to ${filePath}`);
2887
2906
  } catch (error) {
2888
2907
  throw new FileDatabaseError(`Failed to write file ${filePath}: ${error.message}`);
2889
2908
  }
@@ -2965,7 +2984,8 @@ var FileDatabase = class _FileDatabase {
2965
2984
  this.useMetadata = format.hasMetadata;
2966
2985
  }
2967
2986
  if (this.useMetadata) {
2968
- const metadataPath = import_path4.default.join(this.getDestinationPath(), "metadata.json");
2987
+ const destPath = this.getDestinationPath();
2988
+ const metadataPath = import_path4.default.join(destPath, "metadata.json");
2969
2989
  if (import_fs4.default.existsSync(metadataPath)) {
2970
2990
  try {
2971
2991
  const rawData = await import_fs4.default.promises.readFile(metadataPath, "utf8");
@@ -2979,7 +2999,9 @@ var FileDatabase = class _FileDatabase {
2979
2999
  throw new FileDatabaseError(`Failed to read metadata: ${e.message}`);
2980
3000
  }
2981
3001
  } else {
2982
- throw new FileDatabaseError("[FileDatabase] No metadata found in non-versioned mode");
3002
+ throw new FileDatabaseError(
3003
+ `[FileDatabase] No metadata found in non-versioned mode. Looked for: ${metadataPath} (table path: ${destPath})`
3004
+ );
2983
3005
  }
2984
3006
  } else {
2985
3007
  this.metadata = await this.figureMetadataFromVersionFiles("");
@@ -2996,6 +3018,13 @@ var FileDatabase = class _FileDatabase {
2996
3018
  * Write data to the file database
2997
3019
  */
2998
3020
  async write(data, options = {}) {
3021
+ if (options.filename) {
3022
+ const destPath2 = this.getDestinationPath();
3023
+ await ensurePath(destPath2);
3024
+ const filePath = import_path4.default.join(destPath2, options.filename);
3025
+ await this.safeWrite(filePath, data);
3026
+ return;
3027
+ }
2999
3028
  if (options.forceNewVersion && !this.versioned) {
3000
3029
  throw new FileDatabaseError("Cannot use forceNewVersion in non-versioned mode");
3001
3030
  }
@@ -3019,17 +3048,17 @@ var FileDatabase = class _FileDatabase {
3019
3048
  });
3020
3049
  if (matches) {
3021
3050
  targetFileIndex = i;
3022
- this.logger.debug?.(`[FileDatabase] Found existing file with matching custom metadata: ${fileEntry.fileName}, metadata: ${JSON.stringify(options.customMetadata)}`);
3051
+ this.logger.silly?.(`[FileDatabase] Found existing file with matching custom metadata: ${fileEntry.fileName}, metadata: ${JSON.stringify(options.customMetadata)}`);
3023
3052
  break;
3024
3053
  } else {
3025
- this.logger.debug?.(`[FileDatabase] File ${fileEntry.fileName} does not match custom metadata: ${JSON.stringify(options.customMetadata)}`);
3054
+ this.logger.silly?.(`[FileDatabase] File ${fileEntry.fileName} does not match custom metadata: ${JSON.stringify(options.customMetadata)}`);
3026
3055
  }
3027
3056
  }
3028
3057
  if (targetFileIndex === null) {
3029
- this.logger.debug?.(`[FileDatabase] No existing file found with custom metadata: ${JSON.stringify(options.customMetadata)}, will create new file`);
3058
+ this.logger.silly?.(`[FileDatabase] No existing file found with custom metadata: ${JSON.stringify(options.customMetadata)}, will create new file`);
3030
3059
  }
3031
3060
  } else {
3032
- this.logger.debug?.(`[FileDatabase] No custom metadata provided, will create new file`);
3061
+ this.logger.silly?.(`[FileDatabase] No custom metadata provided, will create new file`);
3033
3062
  }
3034
3063
  if (targetFileIndex !== null) {
3035
3064
  const targetFile = this.metadata.files[targetFileIndex];
@@ -3058,7 +3087,17 @@ var FileDatabase = class _FileDatabase {
3058
3087
  * Read data from the file database
3059
3088
  */
3060
3089
  async read(options = {}) {
3061
- const { version, nextPage = false, pageSize } = options;
3090
+ const { version, nextPage = false, pageSize, filename } = options;
3091
+ if (filename) {
3092
+ const destPath = this.getDestinationPath(version);
3093
+ const filePath = import_path4.default.join(destPath, filename);
3094
+ try {
3095
+ const rawData = await import_fs4.default.promises.readFile(filePath, "utf8");
3096
+ return JSON.parse(rawData);
3097
+ } catch (error) {
3098
+ throw new FileDatabaseError(`Failed to read file ${filename}: ${error.message}`);
3099
+ }
3100
+ }
3062
3101
  await this.prepare({ read: true, version });
3063
3102
  const isNonPaginatedData = this.metadata.dataType === "text" || this.metadata.dataType === "xml" || this.metadata.dataType === "json-object";
3064
3103
  if (isNonPaginatedData) {
@@ -3139,6 +3178,67 @@ var FileDatabase = class _FileDatabase {
3139
3178
  this.currentRecord = 0;
3140
3179
  this.hasReadFirstPage = false;
3141
3180
  }
3181
+ /**
3182
+ * List filenames in the table directory.
3183
+ * For catalog/key-value usage (files written with { filename }).
3184
+ * Returns data file names (.json, .txt, .xml) excluding metadata.json.
3185
+ */
3186
+ async listFilenames() {
3187
+ const destPath = this.versioned && this.currentVersion ? import_path4.default.join(this.getDestinationPath(), this.currentVersion) : this.getDestinationPath();
3188
+ try {
3189
+ const entries = await import_fs4.default.promises.readdir(destPath, { withFileTypes: true });
3190
+ return entries.filter((e) => e.isFile() && e.name !== "metadata.json" && /\.(json|txt|xml)$/i.test(e.name)).map((e) => e.name);
3191
+ } catch (err) {
3192
+ if (err?.code === "ENOENT") return [];
3193
+ throw new FileDatabaseError(`Failed to list files: ${err.message}`);
3194
+ }
3195
+ }
3196
+ /**
3197
+ * Remove a file from the table directory (catalog mode).
3198
+ * Use with listFilenames() to manage individual files.
3199
+ */
3200
+ async removeFile(filename) {
3201
+ const destPath = this.versioned && this.currentVersion ? import_path4.default.join(this.getDestinationPath(), this.currentVersion) : this.getDestinationPath();
3202
+ const filePath = import_path4.default.join(destPath, filename);
3203
+ try {
3204
+ await import_fs4.default.promises.unlink(filePath);
3205
+ } catch (err) {
3206
+ if (err?.code === "ENOENT") return;
3207
+ throw new FileDatabaseError(`Failed to remove file ${filename}: ${err.message}`);
3208
+ }
3209
+ }
3210
+ /**
3211
+ * Remove a file and its metadata entry (non-versioned mode with useMetadata).
3212
+ * Use with findData() to get fileName, then call removeFileEntry to delete.
3213
+ */
3214
+ async removeFileEntry(filename) {
3215
+ if (this.versioned) {
3216
+ throw new FileDatabaseError("removeFileEntry is only supported in non-versioned mode");
3217
+ }
3218
+ await this.prepare({ read: true });
3219
+ const idx = this.metadata.files.findIndex((f) => f.fileName === filename);
3220
+ if (idx === -1) {
3221
+ throw new FileDatabaseError(`File entry ${filename} not found in metadata`);
3222
+ }
3223
+ const entry = this.metadata.files[idx];
3224
+ const recordsCount = entry.recordsCount || 0;
3225
+ this.metadata.files.splice(idx, 1);
3226
+ this.metadata.totalRecords = Math.max(0, (this.metadata.totalRecords || 0) - recordsCount);
3227
+ const destPath = this.getDestinationPath();
3228
+ const filePath = import_path4.default.join(destPath, filename);
3229
+ try {
3230
+ await import_fs4.default.promises.unlink(filePath);
3231
+ } catch (err) {
3232
+ if (err?.code === "ENOENT") {
3233
+ this.logger.warn?.(`[FileDatabase] File ${filename} already missing on disk`);
3234
+ } else {
3235
+ throw new FileDatabaseError(`Failed to remove file ${filename}: ${err.message}`);
3236
+ }
3237
+ }
3238
+ if (this.useMetadata) {
3239
+ await this.saveVersionMetadata(this.metadata);
3240
+ }
3241
+ }
3142
3242
  /**
3143
3243
  * Set file-level synopsis calculation function
3144
3244
  */
@@ -3672,6 +3772,7 @@ var ALL_LEVELS = [
3672
3772
  "response",
3673
3773
  "progress"
3674
3774
  ];
3775
+ var DEFAULT_LEVELS = ALL_LEVELS.filter((l) => l !== "silly");
3675
3776
  var MAX_LEVEL_LENGTH = Math.max(...ALL_LEVELS.map((level) => level.toUpperCase().length));
3676
3777
  var LEVEL_COLORS = {
3677
3778
  error: import_chalk.default.red.bold,
@@ -3757,7 +3858,7 @@ var Logger = class _Logger {
3757
3858
  silent: false,
3758
3859
  showLevel: false,
3759
3860
  timestamp: false,
3760
- levels: ALL_LEVELS,
3861
+ levels: DEFAULT_LEVELS,
3761
3862
  progressTimes: false,
3762
3863
  progressThrottle: void 0
3763
3864
  };
@@ -3917,15 +4018,17 @@ var Logger = class _Logger {
3917
4018
  }
3918
4019
  normalizeLevels(levels) {
3919
4020
  if (!levels || !levels.length) {
3920
- return ALL_LEVELS;
4021
+ return DEFAULT_LEVELS;
3921
4022
  }
3922
- const includes = levels.filter((level) => !level.startsWith("-"));
3923
- const excludes = levels.filter((level) => level.startsWith("-")).map((level) => level.slice(1));
3924
- const unknown = [...includes, ...excludes].filter((level) => !ALL_LEVELS.includes(level));
4023
+ const tokens = levels.map((t) => String(t).trim()).filter(Boolean);
4024
+ const explicitIncludes = tokens.filter((t) => !t.startsWith("+") && !t.startsWith("-")).map((t) => t);
4025
+ const addIncludes = tokens.filter((t) => t.startsWith("+")).map((t) => t.slice(1));
4026
+ const excludes = tokens.filter((t) => t.startsWith("-")).map((t) => t.slice(1));
4027
+ const unknown = [...explicitIncludes, ...addIncludes, ...excludes].filter((level) => !ALL_LEVELS.includes(level));
3925
4028
  if (unknown.length) {
3926
4029
  console.warn(`[Logger] Unknown level(s): ${unknown.join(", ")}`);
3927
4030
  }
3928
- const base = includes.length ? includes : ALL_LEVELS;
4031
+ const base = explicitIncludes.length ? explicitIncludes : Array.from(/* @__PURE__ */ new Set([...DEFAULT_LEVELS, ...addIncludes]));
3929
4032
  return base.filter((level) => !excludes.includes(level));
3930
4033
  }
3931
4034
  isValidMode(mode) {
@@ -3983,6 +4086,1107 @@ function setup(opts = {}) {
3983
4086
  function setupContext(opts = {}) {
3984
4087
  return setup(opts);
3985
4088
  }
4089
+
4090
+ // src/utils/core-utils.ts
4091
+ function sleepMs(ms) {
4092
+ return new Promise((resolve2) => setTimeout(resolve2, ms));
4093
+ }
4094
+ function toJsonColumn(value) {
4095
+ if (value === void 0 || value === null) return null;
4096
+ return JSON.stringify(value);
4097
+ }
4098
+
4099
+ // src/tasks/taskUtils.ts
4100
+ var import_node_crypto = require("crypto");
4101
+ function getDb(context) {
4102
+ const db = context.db;
4103
+ if (!db) {
4104
+ throw new Error("Tasks component requires context.db. Initialize DB first and attach to context.");
4105
+ }
4106
+ return db;
4107
+ }
4108
+ function queueToTableNames(queue) {
4109
+ if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(queue)) {
4110
+ throw new Error(`Invalid queue name "${queue}". Use letters, numbers, underscore only.`);
4111
+ }
4112
+ return {
4113
+ tasksTable: queue,
4114
+ historyTable: `${queue}_history`
4115
+ };
4116
+ }
4117
+ async function ensureTaskTables(context, options = {}) {
4118
+ const queue = options.queue ?? "tasks";
4119
+ const recreate = options.recreate ?? false;
4120
+ const db = getDb(context);
4121
+ const { tasksTable, historyTable } = queueToTableNames(queue);
4122
+ const needsTasks = recreate ? true : !await db.tableExists(tasksTable);
4123
+ const needsHistory = recreate ? true : !await db.tableExists(historyTable);
4124
+ if (recreate) {
4125
+ await db.schema.dropTableIfExists(historyTable);
4126
+ await db.schema.dropTableIfExists(tasksTable);
4127
+ }
4128
+ if (needsTasks) {
4129
+ await db.raw(`CREATE EXTENSION IF NOT EXISTS "uuid-ossp"`);
4130
+ await db.schema.createTable(tasksTable, (t) => {
4131
+ t.uuid("id").primary().defaultTo(db.raw("uuid_generate_v4()"));
4132
+ t.timestamp("created_at").notNullable().defaultTo(db.fn.now());
4133
+ t.timestamp("started_at");
4134
+ t.timestamp("completed_at");
4135
+ t.integer("priority").notNullable().defaultTo(0);
4136
+ t.text("schedule");
4137
+ t.timestamp("past_due").defaultTo(null);
4138
+ t.text("target").notNullable();
4139
+ t.text("task").notNullable();
4140
+ t.json("params");
4141
+ t.text("opid");
4142
+ t.timestamp("paused_at").defaultTo(null);
4143
+ t.text("progress");
4144
+ t.boolean("success");
4145
+ t.json("results");
4146
+ });
4147
+ await db.schema.alterTable(tasksTable, (t) => {
4148
+ t.index(["target", "started_at", "created_at"], `${tasksTable}_target_started_created_idx`);
4149
+ t.index(["target", "past_due", "priority", "created_at"], `${tasksTable}_target_past_due_priority_created_idx`);
4150
+ t.index(["target", "task"], `${tasksTable}_target_task_idx`);
4151
+ });
4152
+ }
4153
+ const tasksHasOpid = await db.schema.hasColumn(tasksTable, "opid");
4154
+ if (!tasksHasOpid) {
4155
+ await db.schema.alterTable(tasksTable, (t) => {
4156
+ t.text("opid");
4157
+ });
4158
+ }
4159
+ const tasksHasPausedAt = await db.schema.hasColumn(tasksTable, "paused_at");
4160
+ if (!tasksHasPausedAt) {
4161
+ await db.schema.alterTable(tasksTable, (t) => {
4162
+ t.timestamp("paused_at").defaultTo(null);
4163
+ });
4164
+ }
4165
+ if (needsHistory) {
4166
+ await db.schema.createTable(historyTable, (t) => {
4167
+ t.uuid("id").notNullable();
4168
+ t.timestamp("created_at").notNullable();
4169
+ t.timestamp("started_at");
4170
+ t.timestamp("completed_at");
4171
+ t.integer("priority").notNullable().defaultTo(0);
4172
+ t.text("schedule");
4173
+ t.timestamp("past_due").defaultTo(null);
4174
+ t.text("target").notNullable();
4175
+ t.text("task").notNullable();
4176
+ t.json("params");
4177
+ t.text("opid");
4178
+ t.text("progress");
4179
+ t.boolean("success");
4180
+ t.json("results");
4181
+ });
4182
+ await db.schema.alterTable(historyTable, (t) => {
4183
+ t.index(["target", "created_at"], `${historyTable}_target_created_idx`);
4184
+ t.index(["task", "created_at"], `${historyTable}_task_created_idx`);
4185
+ });
4186
+ }
4187
+ const historyHasOpid = await db.schema.hasColumn(historyTable, "opid");
4188
+ if (!historyHasOpid) {
4189
+ await db.schema.alterTable(historyTable, (t) => {
4190
+ t.text("opid");
4191
+ });
4192
+ }
4193
+ }
4194
+ async function enqueueTask(context, options) {
4195
+ const db = getDb(context);
4196
+ const queue = options.queue ?? "tasks";
4197
+ const { tasksTable } = queueToTableNames(queue);
4198
+ const id = (0, import_node_crypto.randomUUID)();
4199
+ await db(tasksTable).insert({
4200
+ id,
4201
+ target: options.target,
4202
+ task: options.task,
4203
+ params: toJsonColumn(options.params ?? null),
4204
+ opid: options.opid ?? null,
4205
+ priority: options.priority ?? 0,
4206
+ schedule: options.schedule ?? null
4207
+ });
4208
+ return id;
4209
+ }
4210
+ async function updateTaskProgress(context, tasksTable, taskId, progress) {
4211
+ const db = getDb(context);
4212
+ await db(tasksTable).where({ id: taskId }).update({
4213
+ progress: typeof progress === "string" ? progress : JSON.stringify(progress)
4214
+ });
4215
+ }
4216
+
4217
+ // src/tasks/taskLogs.ts
4218
+ function getLogsState(context) {
4219
+ const holder = context;
4220
+ if (holder.__tasksLogsState) return holder.__tasksLogsState;
4221
+ const basePath = holder.params?.get?.("tasksLogsBasePath") || "./data";
4222
+ const namespace = holder.params?.get?.("tasksLogsNamespace") || "tasks-logs";
4223
+ const tableName = holder.params?.get?.("tasksLogsTable") || "runner";
4224
+ const errorTableName = holder.params?.get?.("tasksErrorLogsTable") || `${tableName}-errors`;
4225
+ const maxVersionsRaw = Number(holder.params?.get?.("tasksLogsMaxVersions"));
4226
+ const pageSizeRaw = Number(holder.params?.get?.("tasksLogsPageSize"));
4227
+ const errorDb = new FileDatabase({
4228
+ basePath,
4229
+ namespace,
4230
+ tableName: errorTableName,
4231
+ versioned: true,
4232
+ useMetadata: true,
4233
+ maxVersions: Number.isFinite(maxVersionsRaw) && maxVersionsRaw > 0 ? maxVersionsRaw : 20,
4234
+ pageSize: Number.isFinite(pageSizeRaw) && pageSizeRaw > 0 ? pageSizeRaw : 2e3,
4235
+ logger: holder.logger
4236
+ });
4237
+ const enabledRaw = holder.params?.get?.("tasksLogsEnabled");
4238
+ const enabled = enabledRaw === void 0 ? true : !!enabledRaw;
4239
+ if (!enabled) {
4240
+ const disabledState = {
4241
+ db: null,
4242
+ errorDb,
4243
+ queue: Promise.resolve(),
4244
+ initialized: true,
4245
+ errorInitialized: false
4246
+ };
4247
+ holder.__tasksLogsState = disabledState;
4248
+ return disabledState;
4249
+ }
4250
+ const db = new FileDatabase({
4251
+ basePath,
4252
+ namespace,
4253
+ tableName,
4254
+ versioned: true,
4255
+ useMetadata: true,
4256
+ maxVersions: Number.isFinite(maxVersionsRaw) && maxVersionsRaw > 0 ? maxVersionsRaw : 20,
4257
+ pageSize: Number.isFinite(pageSizeRaw) && pageSizeRaw > 0 ? pageSizeRaw : 2e3,
4258
+ logger: holder.logger
4259
+ });
4260
+ const state = {
4261
+ db,
4262
+ errorDb,
4263
+ queue: Promise.resolve(),
4264
+ initialized: false,
4265
+ errorInitialized: false
4266
+ };
4267
+ holder.__tasksLogsState = state;
4268
+ return state;
4269
+ }
4270
+ function isErrorPayload(payload) {
4271
+ if (!payload) return false;
4272
+ if (typeof payload === "object") {
4273
+ const level = typeof payload.level === "string" ? payload.level.toLowerCase() : "";
4274
+ if (level === "error" || level === "fatal") return true;
4275
+ if (typeof payload.message === "string" && /\berror\b/i.test(payload.message)) return true;
4276
+ return false;
4277
+ }
4278
+ if (typeof payload === "string") {
4279
+ return /\berror\b/i.test(payload);
4280
+ }
4281
+ return false;
4282
+ }
4283
+ function buildLogRecord(task, payload) {
4284
+ const params = task.params && typeof task.params === "object" ? task.params : {};
4285
+ return {
4286
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
4287
+ opid: task.opid ?? null,
4288
+ taskId: task.id,
4289
+ taskName: task.task,
4290
+ target: task.target,
4291
+ source: typeof params.source === "string" ? params.source : null,
4292
+ resource: typeof params.resource === "string" ? params.resource : null,
4293
+ payload
4294
+ };
4295
+ }
4296
+ function appendTaskIpcLog(context, task, payload) {
4297
+ const state = getLogsState(context);
4298
+ if (!state.db && !state.errorDb) return;
4299
+ const record = buildLogRecord(task, payload);
4300
+ state.queue = state.queue.then(async () => {
4301
+ if (state.db) {
4302
+ await state.db.write([record], { forceNewVersion: !state.initialized });
4303
+ state.initialized = true;
4304
+ }
4305
+ if (state.errorDb && isErrorPayload(payload)) {
4306
+ await state.errorDb.write([record], { forceNewVersion: !state.errorInitialized });
4307
+ state.errorInitialized = true;
4308
+ }
4309
+ }).catch((error) => {
4310
+ context.logger.warn?.("[tasks] failed to persist IPC log entry:", error);
4311
+ });
4312
+ }
4313
+
4314
+ // src/tasks/time-matcher.ts
4315
+ var RANGES = ["0-59", "0-59", "0-23", "1-31", "1-12", "0-6"];
4316
+ function resolveAsterisks(field, range) {
4317
+ return field.includes("*") ? field.replace("*", range) : field;
4318
+ }
4319
+ function resolveRanges(field) {
4320
+ const regex = /(\d+)-(\d+)/;
4321
+ let current = field;
4322
+ while (true) {
4323
+ const match = regex.exec(current);
4324
+ if (!match) break;
4325
+ const raw = match[0];
4326
+ let first = Number(match[1]);
4327
+ let last = Number(match[2]);
4328
+ if (last < first) {
4329
+ [first, last] = [last, first];
4330
+ }
4331
+ const values = [];
4332
+ for (let i = first; i <= last; i += 1) {
4333
+ values.push(i);
4334
+ }
4335
+ current = current.replace(raw, values.join(","));
4336
+ }
4337
+ return current;
4338
+ }
4339
+ function resolveSteps(field) {
4340
+ const match = /^(.+)\/(\d+)$/.exec(field);
4341
+ if (!match) return field;
4342
+ const base = match[1];
4343
+ const step = Number(match[2]);
4344
+ if (!Number.isFinite(step) || step <= 0) return field;
4345
+ return base.split(",").map((v) => Number(v)).filter((v) => Number.isFinite(v) && v % step === 0).join(",");
4346
+ }
4347
+ function convertPattern(pattern) {
4348
+ const parts = pattern.trim().split(/\s+/);
4349
+ if (parts.length !== 6) {
4350
+ throw new Error(`Invalid schedule "${pattern}". Expected 6 fields: sec min hour day month weekday`);
4351
+ }
4352
+ return parts.map((field, idx) => resolveAsterisks(field, RANGES[idx])).map((field) => resolveRanges(field)).map((field) => resolveSteps(field));
4353
+ }
4354
+ function fieldMatches(field, value) {
4355
+ const allowed = field.split(",").map((v) => Number(v));
4356
+ return allowed.includes(value);
4357
+ }
4358
+ function timeMatcher(pattern, date = /* @__PURE__ */ new Date()) {
4359
+ const parsed = convertPattern(pattern);
4360
+ 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());
4361
+ }
4362
+
4363
+ // src/tasks/TaskMaster.ts
4364
+ var TaskMaster = class {
4365
+ context;
4366
+ task;
4367
+ constructor(context, task) {
4368
+ this.context = context;
4369
+ this.task = task;
4370
+ }
4371
+ cantRunReason() {
4372
+ return false;
4373
+ }
4374
+ requestStop(_allowanceMs) {
4375
+ }
4376
+ };
4377
+
4378
+ // src/tasks/coreTasks/TaskPing.ts
4379
+ var TaskPing = class extends TaskMaster {
4380
+ async run() {
4381
+ this.context.logger.info?.(`[TaskPing] pong (${this.task.id})`);
4382
+ return { success: true, results: "pong" };
4383
+ }
4384
+ };
4385
+
4386
+ // src/tasks/coreTasks/TaskSampleProcess.ts
4387
+ var TaskSampleProcess = class extends TaskMaster {
4388
+ stopRequested = false;
4389
+ stopAllowanceMs = 0;
4390
+ stopDecisionLogged = false;
4391
+ requestStop(allowanceMs) {
4392
+ this.stopRequested = true;
4393
+ this.stopAllowanceMs = Number.isFinite(allowanceMs) && allowanceMs > 0 ? allowanceMs : 0;
4394
+ this.context.logger.warn?.(
4395
+ `[TaskSampleProcess] stop signal received (${this.task.id}), allowanceMs=${this.stopAllowanceMs}`
4396
+ );
4397
+ }
4398
+ async run(reportProgress) {
4399
+ const totalRaw = this.task?.params?.total ?? 10;
4400
+ const delayRaw = this.task?.params?.delay ?? 1e3;
4401
+ const nameRaw = this.task?.params?.name;
4402
+ const total = Number(totalRaw);
4403
+ const delay = Number(delayRaw);
4404
+ const name = typeof nameRaw === "string" && nameRaw.trim() ? nameRaw.trim() : "sampleProcess";
4405
+ const errors = [];
4406
+ if (!Number.isInteger(total) || total <= 0) {
4407
+ errors.push('param "total" must be a positive integer');
4408
+ }
4409
+ if (!Number.isInteger(delay) || delay < 0) {
4410
+ errors.push('param "delay" must be an integer >= 0');
4411
+ }
4412
+ if (errors.length > 0) {
4413
+ return {
4414
+ success: false,
4415
+ results: {
4416
+ error: `Validation failed: ${errors.join(", ")}`,
4417
+ received: { total: totalRaw, delay: delayRaw, name: nameRaw }
4418
+ }
4419
+ };
4420
+ }
4421
+ const startedAt = Date.now();
4422
+ for (let i = 1; i <= total; i += 1) {
4423
+ if (this.stopRequested) {
4424
+ const remainingMs = Math.max(0, (total - i + 1) * delay);
4425
+ if (remainingMs <= this.stopAllowanceMs) {
4426
+ if (!this.stopDecisionLogged) {
4427
+ this.stopDecisionLogged = true;
4428
+ this.context.logger.warn?.(
4429
+ `[TaskSampleProcess] continue to finish (${this.task.id}): remainingMs=${remainingMs} <= allowanceMs=${this.stopAllowanceMs}`
4430
+ );
4431
+ }
4432
+ } else {
4433
+ this.context.logger.warn?.(
4434
+ `[TaskSampleProcess] stopping gracefully at iteration ${i}/${total} (${this.task.id}), remainingMs=${remainingMs} > allowanceMs=${this.stopAllowanceMs}`
4435
+ );
4436
+ return {
4437
+ success: false,
4438
+ results: {
4439
+ message: `Stopped before completion at iteration ${i}/${total}`,
4440
+ completed: i - 1,
4441
+ total,
4442
+ name,
4443
+ remainingMs,
4444
+ allowanceMs: this.stopAllowanceMs
4445
+ }
4446
+ };
4447
+ }
4448
+ }
4449
+ const elapsed = Date.now() - startedAt;
4450
+ const remaining = Math.max(0, (total - i) * delay);
4451
+ const progress = {
4452
+ name,
4453
+ count: i,
4454
+ total,
4455
+ elapsedMs: elapsed,
4456
+ remainingMs: remaining,
4457
+ status: `running ${name}: ${i}/${total}`
4458
+ };
4459
+ this.context.logger.progress("running", {
4460
+ prefix: name,
4461
+ count: i,
4462
+ total
4463
+ });
4464
+ await reportProgress(progress);
4465
+ await sleepMs(delay);
4466
+ }
4467
+ return {
4468
+ success: true,
4469
+ results: {
4470
+ message: `Completed ${total} iterations`,
4471
+ total,
4472
+ delay,
4473
+ name
4474
+ }
4475
+ };
4476
+ }
4477
+ };
4478
+
4479
+ // src/tasks/coreTasks/TaskShellCommand.ts
4480
+ var import_node_child_process = require("child_process");
4481
+ function runShellCommand(command, cwd) {
4482
+ return new Promise((resolve2, reject) => {
4483
+ const child = (0, import_node_child_process.spawn)(command, {
4484
+ shell: true,
4485
+ cwd: cwd || process.cwd(),
4486
+ stdio: ["ignore", "pipe", "pipe"]
4487
+ });
4488
+ let output = "";
4489
+ let stderr = "";
4490
+ child.stdout.on("data", (chunk) => {
4491
+ output += String(chunk);
4492
+ });
4493
+ child.stderr.on("data", (chunk) => {
4494
+ stderr += String(chunk);
4495
+ });
4496
+ child.on("error", (error) => {
4497
+ reject(error);
4498
+ });
4499
+ child.on("close", (exitCode, signal) => {
4500
+ resolve2({
4501
+ exitCode,
4502
+ output: output.trim(),
4503
+ stderr: stderr.trim(),
4504
+ signal
4505
+ });
4506
+ });
4507
+ });
4508
+ }
4509
+ var TaskShellCommand = class extends TaskMaster {
4510
+ async run() {
4511
+ const params = this.task?.params;
4512
+ const commandRaw = typeof params === "string" ? params : params?.command;
4513
+ const cwdRaw = typeof params === "string" ? void 0 : params?.cwd;
4514
+ const command = typeof commandRaw === "string" ? commandRaw.trim() : "";
4515
+ const cwd = typeof cwdRaw === "string" && cwdRaw.trim() ? cwdRaw.trim() : void 0;
4516
+ if (!command) {
4517
+ return {
4518
+ success: false,
4519
+ results: {
4520
+ error: 'Validation failed: param "command" must be a non-empty string',
4521
+ received: this.task?.params ?? null
4522
+ }
4523
+ };
4524
+ }
4525
+ try {
4526
+ const result = await runShellCommand(command, cwd);
4527
+ const success = result.exitCode === 0;
4528
+ this.context.logger.info?.(
4529
+ `[TaskShellCommand] command="${command}" exitCode=${String(result.exitCode)} (${this.task.id})`
4530
+ );
4531
+ return {
4532
+ success,
4533
+ results: {
4534
+ command,
4535
+ cwd: cwd ?? process.cwd(),
4536
+ output: result.output,
4537
+ stderr: result.stderr,
4538
+ exitCode: result.exitCode,
4539
+ signal: result.signal
4540
+ }
4541
+ };
4542
+ } catch (error) {
4543
+ return {
4544
+ success: false,
4545
+ results: {
4546
+ command,
4547
+ cwd: cwd ?? process.cwd(),
4548
+ output: "",
4549
+ stderr: "",
4550
+ exitCode: null,
4551
+ error: error?.message ?? String(error)
4552
+ }
4553
+ };
4554
+ }
4555
+ }
4556
+ };
4557
+
4558
+ // src/tasks/coreTasks/TaskSystemInfo.ts
4559
+ var import_node_os = __toESM(require("os"), 1);
4560
+ var import_promises = __toESM(require("fs/promises"), 1);
4561
+ function toGb(valueBytes) {
4562
+ return `${(valueBytes / 1024 ** 3).toFixed(2)} GB`;
4563
+ }
4564
+ function toMb(valueBytes) {
4565
+ return `${(valueBytes / 1024 ** 2).toFixed(2)} MB`;
4566
+ }
4567
+ async function getDiskStats() {
4568
+ const stats = await import_promises.default.statfs("/");
4569
+ const total = Number(stats.bsize) * Number(stats.blocks);
4570
+ const free = Number(stats.bsize) * Number(stats.bavail);
4571
+ const used = total - free;
4572
+ return {
4573
+ total: toGb(total),
4574
+ used: toGb(used),
4575
+ free: toGb(free)
4576
+ };
4577
+ }
4578
+ var TaskSystemInfo = class extends TaskMaster {
4579
+ async run() {
4580
+ try {
4581
+ const totalMemory = import_node_os.default.totalmem();
4582
+ const freeMemory = import_node_os.default.freemem();
4583
+ const usedMemory = totalMemory - freeMemory;
4584
+ const cpus = import_node_os.default.cpus();
4585
+ const cpuUtilization = cpus.map((cpu) => {
4586
+ const total = Object.values(cpu.times).reduce((acc, time) => acc + time, 0);
4587
+ const usage = (total - cpu.times.idle) / total * 100;
4588
+ return Number(usage.toFixed(2));
4589
+ });
4590
+ const processMemory = process.memoryUsage();
4591
+ const disk = await getDiskStats();
4592
+ const results = {
4593
+ memory: {
4594
+ total: toGb(totalMemory),
4595
+ used: toGb(usedMemory),
4596
+ free: toGb(freeMemory)
4597
+ },
4598
+ processMemory: {
4599
+ rss: toMb(processMemory.rss),
4600
+ heapTotal: toMb(processMemory.heapTotal),
4601
+ heapUsed: toMb(processMemory.heapUsed),
4602
+ external: toMb(processMemory.external)
4603
+ },
4604
+ disk,
4605
+ cpu: {
4606
+ cores: cpuUtilization.length,
4607
+ utilization: cpuUtilization
4608
+ },
4609
+ runtime: {
4610
+ platform: import_node_os.default.platform(),
4611
+ arch: import_node_os.default.arch(),
4612
+ uptimeSec: import_node_os.default.uptime(),
4613
+ hostname: import_node_os.default.hostname()
4614
+ }
4615
+ };
4616
+ this.context.logger.info?.(`[TaskSystemInfo] collected system metrics (${this.task.id})`);
4617
+ return { success: true, results };
4618
+ } catch (error) {
4619
+ return {
4620
+ success: false,
4621
+ results: {
4622
+ error: "Can't collect system stats",
4623
+ message: error?.message ?? String(error)
4624
+ }
4625
+ };
4626
+ }
4627
+ }
4628
+ };
4629
+
4630
+ // src/tasks/coreTasks/TaskSumAB.ts
4631
+ var TaskSumAB = class extends TaskMaster {
4632
+ async run() {
4633
+ const a = this.task?.params?.a;
4634
+ const b = this.task?.params?.b;
4635
+ if (typeof a !== "number" || Number.isNaN(a)) {
4636
+ return {
4637
+ success: false,
4638
+ results: {
4639
+ error: 'Validation failed: param "a" must be a valid number',
4640
+ received: { a, b }
4641
+ }
4642
+ };
4643
+ }
4644
+ if (typeof b !== "number" || Number.isNaN(b)) {
4645
+ return {
4646
+ success: false,
4647
+ results: {
4648
+ error: 'Validation failed: param "b" must be a valid number',
4649
+ received: { a, b }
4650
+ }
4651
+ };
4652
+ }
4653
+ const sum = a + b;
4654
+ this.context.logger.info?.(`[TaskSumAB] ${a} + ${b} = ${sum} (${this.task.id})`);
4655
+ return {
4656
+ success: true,
4657
+ results: { a, b, sum }
4658
+ };
4659
+ }
4660
+ };
4661
+
4662
+ // src/tasks/coreTasks/TaskStopRunner.ts
4663
+ var TaskStopRunner = class extends TaskMaster {
4664
+ async run() {
4665
+ const allowanceMs = Number(this.task?.params?.allowanceMs ?? 5e3);
4666
+ this.context.logger.warn?.(`[TaskStopRunner] stop requested (allowanceMs=${allowanceMs})`);
4667
+ return {
4668
+ success: true,
4669
+ results: {
4670
+ stopRunner: true,
4671
+ allowanceMs,
4672
+ message: "Runner stop requested"
4673
+ }
4674
+ };
4675
+ }
4676
+ };
4677
+
4678
+ // src/tasks/TasksRegistry.ts
4679
+ var TasksRegistry = class _TasksRegistry {
4680
+ map = {};
4681
+ constructor(initial) {
4682
+ if (initial) {
4683
+ this.addMany(initial);
4684
+ }
4685
+ }
4686
+ static withCoreTasks() {
4687
+ return new _TasksRegistry().add("ping", TaskPing).add("sampleProcess", TaskSampleProcess).add("shellCommand", TaskShellCommand).add("systemInfo", TaskSystemInfo).add("taskSumAB", TaskSumAB).add("stopRunner", TaskStopRunner).add("stop", TaskStopRunner);
4688
+ }
4689
+ add(taskName, taskClass) {
4690
+ this.map[taskName] = taskClass;
4691
+ return this;
4692
+ }
4693
+ addMany(entries) {
4694
+ for (const [name, klass] of Object.entries(entries)) {
4695
+ this.add(name, klass);
4696
+ }
4697
+ return this;
4698
+ }
4699
+ get(taskName) {
4700
+ return this.map[taskName];
4701
+ }
4702
+ listSupportedTasks() {
4703
+ return Object.keys(this.map).sort();
4704
+ }
4705
+ toObject() {
4706
+ return { ...this.map };
4707
+ }
4708
+ };
4709
+
4710
+ // src/tasks/taskScriptRunner.ts
4711
+ var import_node_child_process2 = require("child_process");
4712
+ function toCliArgs(args = []) {
4713
+ return args.filter((a) => typeof a === "string" && a.length > 0);
4714
+ }
4715
+ function formatChildLogPrefix(task) {
4716
+ return `${task.task}:${task.id.slice(0, 8)}${task.opid ? `:${task.opid}` : ""}`;
4717
+ }
4718
+ async function runNodeTaskScript(context, options) {
4719
+ const cliArgs = toCliArgs(["--route=ipc", "--mode=json", ...options.args || []]);
4720
+ const inheritedExecArgs = Array.isArray(process.execArgv) ? [...process.execArgv] : [];
4721
+ const hasTsRuntimeInParent = inheritedExecArgs.some((arg) => /tsx|ts-node/i.test(arg));
4722
+ const nodeArgs = hasTsRuntimeInParent ? [...inheritedExecArgs, options.scriptPath, ...cliArgs] : ["--import", "tsx", options.scriptPath, ...cliArgs];
4723
+ const child = (0, import_node_child_process2.spawn)(
4724
+ process.execPath,
4725
+ nodeArgs,
4726
+ {
4727
+ cwd: options.cwd || process.cwd(),
4728
+ stdio: ["ignore", "pipe", "pipe", "ipc"],
4729
+ env: {
4730
+ ...process.env,
4731
+ TASK_ID: options.task.id,
4732
+ TASK_NAME: options.task.task,
4733
+ TASK_OPID: options.task.opid || ""
4734
+ }
4735
+ }
4736
+ );
4737
+ let stdout = "";
4738
+ let stderr = "";
4739
+ let workerResult = null;
4740
+ let hadErrorMessage = false;
4741
+ const prefix = formatChildLogPrefix(options.task);
4742
+ const db = context.db;
4743
+ const tasksTable = context.params?.get?.("table") || "tasks";
4744
+ let progressWriteChain = Promise.resolve();
4745
+ let progressCallbackChain = Promise.resolve();
4746
+ const updateProgress = (text) => {
4747
+ if (!db || !text || !text.trim()) return;
4748
+ progressWriteChain = progressWriteChain.then(async () => {
4749
+ await db(tasksTable).where({ id: options.task.id }).update({ progress: text.slice(0, 4e3) });
4750
+ }).catch((error) => {
4751
+ context.logger.warn?.(
4752
+ `[tasks] failed to update progress for task=${options.task.id}: ${error?.message ?? String(error)}`
4753
+ );
4754
+ });
4755
+ if (options.onProgress) {
4756
+ progressCallbackChain = progressCallbackChain.then(async () => {
4757
+ await options.onProgress?.(text.slice(0, 4e3));
4758
+ }).catch((error) => {
4759
+ context.logger.warn?.(
4760
+ `[tasks] reportProgress callback failed for task=${options.task.id}: ${error?.message ?? String(error)}`
4761
+ );
4762
+ });
4763
+ }
4764
+ };
4765
+ const payloadToProgressText = (payload) => {
4766
+ if (!payload) return "";
4767
+ if (typeof payload === "string") return payload;
4768
+ if (typeof payload.message === "string" && payload.level === "progress" && payload.count !== void 0 && payload.total !== void 0) {
4769
+ const pfx = payload.prefix ? `${payload.prefix} ` : "";
4770
+ return `${pfx}${payload.message} ${payload.count}/${payload.total}`;
4771
+ }
4772
+ if (typeof payload.message === "string") return payload.message;
4773
+ if (payload.level === "progress" && payload.count !== void 0 && payload.total !== void 0) {
4774
+ const pfx = payload.prefix ? `${payload.prefix} ` : "";
4775
+ return `${pfx}${payload.count}/${payload.total}`;
4776
+ }
4777
+ return "";
4778
+ };
4779
+ child.stdout.on("data", (chunk) => {
4780
+ const text = String(chunk);
4781
+ stdout += text;
4782
+ if (text.trim()) {
4783
+ context.logger.info?.(`[child:${prefix}] ${text.trimEnd()}`);
4784
+ updateProgress(text.trim().replace(/\s+/g, " ").slice(0, 400));
4785
+ }
4786
+ });
4787
+ child.stderr.on("data", (chunk) => {
4788
+ const text = String(chunk);
4789
+ stderr += text;
4790
+ if (text.trim()) {
4791
+ context.logger.warn?.(`[child:${prefix}] ${text.trimEnd()}`);
4792
+ }
4793
+ });
4794
+ child.on("message", (message) => {
4795
+ if (message && typeof message === "object" && "__taskWorkerResult" in message) {
4796
+ workerResult = message.__taskWorkerResult;
4797
+ return;
4798
+ }
4799
+ if (message && typeof message === "object") {
4800
+ const level = typeof message.level === "string" ? message.level.toLowerCase() : "";
4801
+ if (level === "error" || level === "fatal") {
4802
+ hadErrorMessage = true;
4803
+ }
4804
+ }
4805
+ appendTaskIpcLog(context, options.task, message);
4806
+ const progressText = payloadToProgressText(message);
4807
+ if (progressText) {
4808
+ updateProgress(progressText);
4809
+ if (typeof message === "object" && message?.level === "progress" && message.count !== void 0 && message.total !== void 0) {
4810
+ const countNum = Number(String(message.count).trim());
4811
+ const totalNum = Number(message.total);
4812
+ if (Number.isFinite(countNum) && Number.isFinite(totalNum) && totalNum > 0) {
4813
+ context.logger.progress(message.message || "progress", {
4814
+ prefix: message.prefix || prefix,
4815
+ count: countNum,
4816
+ total: totalNum
4817
+ });
4818
+ } else {
4819
+ context.logger.info?.(`[child:${prefix}] ${progressText}`);
4820
+ }
4821
+ } else {
4822
+ context.logger.info?.(`[child:${prefix}] ${progressText}`);
4823
+ }
4824
+ }
4825
+ });
4826
+ return await new Promise((resolve2, reject) => {
4827
+ child.on("error", (error) => reject(error));
4828
+ child.on("close", (exitCode, signal) => {
4829
+ Promise.allSettled([progressWriteChain, progressCallbackChain]).finally(() => {
4830
+ resolve2({
4831
+ exitCode,
4832
+ signal,
4833
+ stdout: stdout.trim(),
4834
+ stderr: stderr.trim(),
4835
+ workerResult,
4836
+ hadErrorMessage
4837
+ });
4838
+ });
4839
+ });
4840
+ });
4841
+ }
4842
+
4843
+ // src/tasks/index.ts
4844
+ var LOCKED_BY_ERROR_MESSAGE = "locked by error";
4845
+ var defaultTasksRegistry = TasksRegistry.withCoreTasks();
4846
+ function getDb2(context) {
4847
+ const db = context.db;
4848
+ if (!db) {
4849
+ throw new Error("Tasks component requires context.db. Initialize DB first and attach to context.");
4850
+ }
4851
+ return db;
4852
+ }
4853
+ function normalizeRegistry(registry) {
4854
+ if (!registry) return defaultTasksRegistry;
4855
+ if (registry instanceof TasksRegistry) return registry;
4856
+ return new TasksRegistry().addMany(registry);
4857
+ }
4858
+ function normalizeAllowedTasks(value) {
4859
+ if (!value) return void 0;
4860
+ if (Array.isArray(value)) {
4861
+ const out2 = value.map((v) => String(v).trim()).filter(Boolean);
4862
+ return out2.length ? out2 : void 0;
4863
+ }
4864
+ const out = String(value).split(",").map((v) => v.trim()).filter(Boolean);
4865
+ return out.length ? out : void 0;
4866
+ }
4867
+ async function enqueueStopTask(context, target, queue = "tasks", allowanceMs = 5e3) {
4868
+ return enqueueTask(context, {
4869
+ queue,
4870
+ target,
4871
+ task: "stopRunner",
4872
+ params: { allowanceMs },
4873
+ priority: 1e6
4874
+ });
4875
+ }
4876
+ async function signalRunningTasksStop(context, runningTaskInstances, allowanceMs) {
4877
+ context.logger.warn?.(`[tasks] signaling ${runningTaskInstances.size} running task(s) to stop`);
4878
+ for (const [, taskInstance] of runningTaskInstances) {
4879
+ if (typeof taskInstance.requestStop === "function") {
4880
+ try {
4881
+ await taskInstance.requestStop(allowanceMs);
4882
+ } catch (error) {
4883
+ context.logger.warn?.("[tasks] task requestStop failed:", error);
4884
+ }
4885
+ }
4886
+ }
4887
+ context.emitter.emit("stop", allowanceMs);
4888
+ }
4889
+ async function executeClaimedTask(context, tasksTable, historyTable, row, registry, runningTaskInstances) {
4890
+ const db = getDb2(context);
4891
+ const taskName = row.task;
4892
+ const TaskClass = registry.get(taskName);
4893
+ const { paused_at: _pausedAt, ...rowForHistory } = row;
4894
+ if (!TaskClass) {
4895
+ const err = { message: `Unknown task "${taskName}"` };
4896
+ await db(historyTable).insert({
4897
+ ...rowForHistory,
4898
+ completed_at: /* @__PURE__ */ new Date(),
4899
+ success: false,
4900
+ params: toJsonColumn(row.params),
4901
+ results: toJsonColumn(err)
4902
+ });
4903
+ if (row.schedule) {
4904
+ await db(tasksTable).where({ id: row.id }).update({
4905
+ started_at: null,
4906
+ completed_at: /* @__PURE__ */ new Date(),
4907
+ success: false,
4908
+ results: toJsonColumn(err),
4909
+ past_due: null,
4910
+ paused_at: db.fn.now(),
4911
+ progress: LOCKED_BY_ERROR_MESSAGE
4912
+ });
4913
+ } else {
4914
+ await db(tasksTable).where({ id: row.id }).delete();
4915
+ }
4916
+ return { stopRunnerRequested: false, stopAllowanceMs: 0 };
4917
+ }
4918
+ let success = false;
4919
+ let results = null;
4920
+ let taskInstance = null;
4921
+ try {
4922
+ taskInstance = new TaskClass(context, row);
4923
+ runningTaskInstances.set(row.id, taskInstance);
4924
+ const runResult = await taskInstance.run((progress) => updateTaskProgress(context, tasksTable, row.id, progress));
4925
+ success = !!runResult?.success;
4926
+ results = runResult?.results ?? null;
4927
+ } catch (error) {
4928
+ success = false;
4929
+ results = {
4930
+ message: error?.message ?? String(error),
4931
+ name: error?.name ?? "Error",
4932
+ stack: error?.stack ?? null
4933
+ };
4934
+ } finally {
4935
+ runningTaskInstances.delete(row.id);
4936
+ }
4937
+ await db(historyTable).insert({
4938
+ ...rowForHistory,
4939
+ completed_at: /* @__PURE__ */ new Date(),
4940
+ success,
4941
+ params: toJsonColumn(row.params),
4942
+ results: toJsonColumn(results)
4943
+ });
4944
+ if (!success) {
4945
+ const dbName = String(context?.params?.get?.("dbName") || "local");
4946
+ const tableName = String(context?.params?.get?.("table") || "tasks");
4947
+ const fallbackRecoverCommand = [
4948
+ "npx",
4949
+ "tsx",
4950
+ "examples/tasks/recover-task.ts",
4951
+ `--dbName='${dbName.replace(/'/g, `'\\''`)}'`,
4952
+ `--table='${tableName.replace(/'/g, `'\\''`)}'`,
4953
+ `--id='${String(row.id).replace(/'/g, `'\\''`)}'`
4954
+ ].join(" ");
4955
+ const rerunCommand = results && typeof results === "object" && results.rerunCommand ? results.rerunCommand : fallbackRecoverCommand;
4956
+ appendTaskIpcLog(context, row, {
4957
+ level: "error",
4958
+ message: `[tasks] task failed: ${row.task} id=${row.id}. Once problem is fixed, re-run: ${rerunCommand}`,
4959
+ details: results
4960
+ });
4961
+ }
4962
+ if (row.schedule) {
4963
+ if (success) {
4964
+ await db(tasksTable).where({ id: row.id }).update({
4965
+ started_at: null,
4966
+ completed_at: /* @__PURE__ */ new Date(),
4967
+ success,
4968
+ results: toJsonColumn(results),
4969
+ progress: null,
4970
+ past_due: null
4971
+ });
4972
+ } else {
4973
+ await db(tasksTable).where({ id: row.id }).update({
4974
+ started_at: null,
4975
+ completed_at: /* @__PURE__ */ new Date(),
4976
+ success,
4977
+ results: toJsonColumn(results),
4978
+ paused_at: db.fn.now(),
4979
+ progress: LOCKED_BY_ERROR_MESSAGE,
4980
+ past_due: null
4981
+ });
4982
+ }
4983
+ } else {
4984
+ await db(tasksTable).where({ id: row.id }).delete();
4985
+ }
4986
+ const stopRunnerRequested = !!(results && typeof results === "object" && results.stopRunner === true);
4987
+ const stopAllowanceMs = stopRunnerRequested ? Number(results.allowanceMs ?? 5e3) : 0;
4988
+ return { stopRunnerRequested, stopAllowanceMs };
4989
+ }
4990
+ async function claimNextRunnableTask(context, tasksTable, target, registry, scanLimit, taskNames) {
4991
+ const db = getDb2(context);
4992
+ 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);
4993
+ if (taskNames && taskNames.length > 0) {
4994
+ query = query.whereIn("task", taskNames);
4995
+ }
4996
+ const candidates = await query;
4997
+ for (const row of candidates) {
4998
+ if (!row.past_due && row.schedule && !timeMatcher(row.schedule)) {
4999
+ continue;
5000
+ }
5001
+ const TaskClass = registry.get(row.task);
5002
+ if (TaskClass) {
5003
+ const taskInstance = new TaskClass(context, row);
5004
+ const reason = taskInstance.cantRunReason ? await taskInstance.cantRunReason() : null;
5005
+ if (reason) {
5006
+ if (!row.past_due) {
5007
+ await db(tasksTable).where({ id: row.id }).update({
5008
+ past_due: db.fn.now(),
5009
+ progress: String(reason)
5010
+ });
5011
+ }
5012
+ continue;
5013
+ }
5014
+ }
5015
+ const updated = await db(tasksTable).where({ id: row.id }).whereNull("started_at").whereNull("paused_at").update({ started_at: db.fn.now() }).returning("*");
5016
+ const claimed = Array.isArray(updated) ? updated[0] : null;
5017
+ if (claimed) return claimed;
5018
+ }
5019
+ return null;
5020
+ }
5021
+ async function runTasksLoop(context, options) {
5022
+ const queue = options.queue ?? "tasks";
5023
+ const target = options.target;
5024
+ const pollMs = options.pollMs ?? 1e3;
5025
+ const maxParallel = options.maxParallel ?? 1;
5026
+ const scanLimit = options.scanLimit ?? 100;
5027
+ const allowedTasks = normalizeAllowedTasks(options.allowedTasks);
5028
+ const registry = normalizeRegistry(options.registry);
5029
+ const { tasksTable, historyTable } = queueToTableNames(queue);
5030
+ if (!target) throw new Error("runTasksLoop: target is required");
5031
+ const runningPromises = /* @__PURE__ */ new Set();
5032
+ const runningTaskInstances = /* @__PURE__ */ new Map();
5033
+ let runningStopControlPromise = null;
5034
+ let stopRequested = false;
5035
+ let stopAllowanceMs = 5e3;
5036
+ context.__tasksRunnerStop = false;
5037
+ while (!context.isStop() && !stopRequested && !context.__tasksRunnerStop) {
5038
+ if (!runningStopControlPromise) {
5039
+ const claimedStopTask = await claimNextRunnableTask(
5040
+ context,
5041
+ tasksTable,
5042
+ target,
5043
+ registry,
5044
+ 10,
5045
+ ["stopRunner", "stop"]
5046
+ );
5047
+ if (claimedStopTask) {
5048
+ runningStopControlPromise = executeClaimedTask(
5049
+ context,
5050
+ tasksTable,
5051
+ historyTable,
5052
+ claimedStopTask,
5053
+ registry,
5054
+ runningTaskInstances
5055
+ ).then(async (outcome) => {
5056
+ if (outcome.stopRunnerRequested && !stopRequested) {
5057
+ stopRequested = true;
5058
+ stopAllowanceMs = outcome.stopAllowanceMs || 5e3;
5059
+ context.__tasksRunnerStop = true;
5060
+ await signalRunningTasksStop(context, runningTaskInstances, stopAllowanceMs);
5061
+ }
5062
+ }).finally(() => {
5063
+ runningStopControlPromise = null;
5064
+ });
5065
+ }
5066
+ }
5067
+ while (runningPromises.size < maxParallel) {
5068
+ const claimed = await claimNextRunnableTask(
5069
+ context,
5070
+ tasksTable,
5071
+ target,
5072
+ registry,
5073
+ scanLimit,
5074
+ allowedTasks
5075
+ );
5076
+ if (!claimed) break;
5077
+ const p = executeClaimedTask(context, tasksTable, historyTable, claimed, registry, runningTaskInstances).then(async (outcome) => {
5078
+ if (outcome.stopRunnerRequested && !stopRequested) {
5079
+ stopRequested = true;
5080
+ stopAllowanceMs = outcome.stopAllowanceMs || 5e3;
5081
+ context.__tasksRunnerStop = true;
5082
+ await signalRunningTasksStop(context, runningTaskInstances, stopAllowanceMs);
5083
+ }
5084
+ }).finally(() => {
5085
+ runningPromises.delete(p);
5086
+ });
5087
+ runningPromises.add(p);
5088
+ }
5089
+ await sleepMs(pollMs);
5090
+ }
5091
+ if (context.isStop() && !stopRequested) {
5092
+ await signalRunningTasksStop(context, runningTaskInstances, 5e3);
5093
+ }
5094
+ if (runningPromises.size > 0) {
5095
+ if (stopRequested) {
5096
+ await Promise.race([
5097
+ Promise.allSettled(Array.from(runningPromises)),
5098
+ sleepMs(stopAllowanceMs).then(() => {
5099
+ context.logger.warn?.(
5100
+ `[tasks] stop allowance (${stopAllowanceMs}ms) reached; ${runningPromises.size} task(s) still running`
5101
+ );
5102
+ })
5103
+ ]);
5104
+ } else {
5105
+ await Promise.allSettled(Array.from(runningPromises));
5106
+ }
5107
+ }
5108
+ }
5109
+ async function waitForTaskResult(context, taskId, options = {}) {
5110
+ const db = getDb2(context);
5111
+ const queue = options.queue ?? "tasks";
5112
+ const timeoutMs = options.timeoutMs ?? 6e4;
5113
+ const pollMs = options.pollMs ?? 500;
5114
+ const { tasksTable, historyTable } = queueToTableNames(queue);
5115
+ const deadline = Date.now() + timeoutMs;
5116
+ while (Date.now() <= deadline) {
5117
+ const done = await db(historyTable).where({ id: taskId }).orderBy("created_at", "desc").first();
5118
+ if (done) return done;
5119
+ const pending = await db(tasksTable).where({ id: taskId }).first();
5120
+ if (!pending) {
5121
+ const maybeDone = await db(historyTable).where({ id: taskId }).orderBy("created_at", "desc").first();
5122
+ return maybeDone ?? null;
5123
+ }
5124
+ await sleepMs(pollMs);
5125
+ }
5126
+ return null;
5127
+ }
5128
+ var TasksManager = class _TasksManager {
5129
+ context;
5130
+ queue;
5131
+ target;
5132
+ recreateTaskTables;
5133
+ pollMs;
5134
+ maxParallel;
5135
+ scanLimit;
5136
+ allowedTasks;
5137
+ registry;
5138
+ constructor(context, options = {}) {
5139
+ this.context = context;
5140
+ this.queue = options.queue ?? "tasks";
5141
+ this.target = options.target ?? "localRunner";
5142
+ this.recreateTaskTables = options.recreateTaskTables ?? false;
5143
+ this.pollMs = options.pollMs ?? 1e3;
5144
+ this.maxParallel = options.maxParallel ?? 1;
5145
+ this.scanLimit = options.scanLimit ?? 100;
5146
+ this.allowedTasks = normalizeAllowedTasks(options.allowedTasks);
5147
+ this.registry = normalizeRegistry(options.registry);
5148
+ }
5149
+ static init(context, options = {}) {
5150
+ const defs = {
5151
+ table: "string default tasks",
5152
+ target: "string default localRunner",
5153
+ recreateTaskTables: "boolean default false",
5154
+ pollMs: "number default 1000",
5155
+ maxParallel: "number default 1",
5156
+ scanLimit: "number default 100",
5157
+ allowedTasks: "string"
5158
+ };
5159
+ const discovered = context.params.getAllForModule(defs);
5160
+ const resolved = {
5161
+ queue: discovered.table,
5162
+ target: discovered.target,
5163
+ recreateTaskTables: discovered.recreateTaskTables,
5164
+ pollMs: discovered.pollMs,
5165
+ maxParallel: discovered.maxParallel,
5166
+ scanLimit: discovered.scanLimit,
5167
+ allowedTasks: discovered.allowedTasks,
5168
+ ...options
5169
+ };
5170
+ return new _TasksManager(context, resolved);
5171
+ }
5172
+ async ensureTaskTables(options = {}) {
5173
+ await ensureTaskTables(this.context, {
5174
+ queue: this.queue,
5175
+ recreate: options.recreate ?? this.recreateTaskTables
5176
+ });
5177
+ }
5178
+ async runTasksLoop(options = {}) {
5179
+ await runTasksLoop(this.context, {
5180
+ queue: options.queue ?? this.queue,
5181
+ target: options.target ?? this.target,
5182
+ pollMs: options.pollMs ?? this.pollMs,
5183
+ maxParallel: options.maxParallel ?? this.maxParallel,
5184
+ scanLimit: options.scanLimit ?? this.scanLimit,
5185
+ allowedTasks: options.allowedTasks ?? this.allowedTasks,
5186
+ registry: options.registry ?? this.registry
5187
+ });
5188
+ }
5189
+ };
3986
5190
  // Annotate the CommonJS export names for ESM import in node:
3987
5191
  0 && (module.exports = {
3988
5192
  Args,
@@ -4006,8 +5210,18 @@ function setupContext(opts = {}) {
4006
5210
  ScreenFooter,
4007
5211
  ScreenRow,
4008
5212
  ScreenTitle,
5213
+ TaskMaster,
5214
+ TaskPing,
5215
+ TaskSampleProcess,
5216
+ TaskShellCommand,
5217
+ TaskStopRunner,
5218
+ TaskSumAB,
5219
+ TaskSystemInfo,
5220
+ TasksManager,
5221
+ TasksRegistry,
4009
5222
  Text,
4010
5223
  TextBlock,
5224
+ appendTaskIpcLog,
4011
5225
  buildBreadcrumb,
4012
5226
  buildDetailBreadcrumb,
4013
5227
  buildFooter,
@@ -4015,7 +5229,11 @@ function setupContext(opts = {}) {
4015
5229
  dbFindAndConnect,
4016
5230
  dbInit,
4017
5231
  defaultFileSynopsisFunction,
5232
+ defaultTasksRegistry,
4018
5233
  defaultVersionSynopsisFunction,
5234
+ enqueueStopTask,
5235
+ enqueueTask,
5236
+ ensureTaskTables,
4019
5237
  getArgsInstance,
4020
5238
  h,
4021
5239
  joiEdateType,
@@ -4024,6 +5242,9 @@ function setupContext(opts = {}) {
4024
5242
  listTables,
4025
5243
  load,
4026
5244
  organizeFooterMessages,
5245
+ queueToTableNames,
5246
+ runNodeTaskScript,
5247
+ runTasksLoop,
4027
5248
  setupContext,
4028
5249
  showListScreen,
4029
5250
  showMenuScreen,
@@ -4031,11 +5252,13 @@ function setupContext(opts = {}) {
4031
5252
  showMultiColumnListWithPreviewScreen,
4032
5253
  showScreen,
4033
5254
  showWordGridScreen,
5255
+ updateTaskProgress,
4034
5256
  useCallback,
4035
5257
  useEffect,
4036
5258
  useInput,
4037
5259
  useMemo,
4038
5260
  useRef,
4039
- useState
5261
+ useState,
5262
+ waitForTaskResult
4040
5263
  });
4041
5264
  //# sourceMappingURL=index.cjs.map