@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.js CHANGED
@@ -2358,7 +2358,7 @@ var FileDatabase = class _FileDatabase {
2358
2358
  const versions = await this.getVersions();
2359
2359
  while (versions.length > this.maxVersions) {
2360
2360
  const versionToDelete = path3.resolve(this.getDestinationPath(), versions.shift());
2361
- this.logger.debug?.(`[FileDatabase] Deleting old version: ${versionToDelete}`);
2361
+ this.logger.silly?.(`[FileDatabase] Deleting old version: ${versionToDelete}`);
2362
2362
  await fs3.promises.rm(versionToDelete, { recursive: true, force: true });
2363
2363
  }
2364
2364
  return versionName;
@@ -2636,7 +2636,7 @@ var FileDatabase = class _FileDatabase {
2636
2636
  };
2637
2637
  this.metadata.files.push(fileEntry);
2638
2638
  this.lastFileData = null;
2639
- this.logger.debug?.(`[FileDatabase] Created new file: ${fileEntry.fileName}, fileNumber: ${this.currentFileNumber}`);
2639
+ this.logger.silly?.(`[FileDatabase] Created new file: ${fileEntry.fileName}, fileNumber: ${this.currentFileNumber}`);
2640
2640
  }
2641
2641
  /**
2642
2642
  * Figure out what data to write and which file to use (for pagination)
@@ -2669,7 +2669,7 @@ var FileDatabase = class _FileDatabase {
2669
2669
  const filesBeforeCreate = this.metadata.files.length;
2670
2670
  this.makeNewFile();
2671
2671
  newlyCreatedFileIndex = filesBeforeCreate;
2672
- this.logger.debug?.(`[FileDatabase] Creating new file for unique custom metadata combination, fileNumber: ${this.currentFileNumber}`);
2672
+ this.logger.silly?.(`[FileDatabase] Creating new file for unique custom metadata combination, fileNumber: ${this.currentFileNumber}`);
2673
2673
  } else if (this.metadata.files.length === 0) {
2674
2674
  this.makeNewFile();
2675
2675
  }
@@ -2718,7 +2718,7 @@ var FileDatabase = class _FileDatabase {
2718
2718
  dataLeftOver = null;
2719
2719
  }
2720
2720
  const fileName = this.metadata.files[this.metadata.files.length - 1].fileName;
2721
- this.logger.debug?.(
2721
+ this.logger.silly?.(
2722
2722
  `[FileDatabase] figureOutDataAndFileToWrite: filename=${fileName}, forceNewFile=${forceNewFile}, targetFileIndex=${targetFileIndex}, dataToWrite.length=${Array.isArray(dataToWrite) ? dataToWrite.length : "N/A"}, lastFileRecordsCount=${lastFileRecordsCount}`
2723
2723
  );
2724
2724
  return { dataToWrite, dataLeftOver, fileName };
@@ -2773,7 +2773,7 @@ var FileDatabase = class _FileDatabase {
2773
2773
  this.metadata.modifiedAt = (/* @__PURE__ */ new Date()).toISOString();
2774
2774
  this.metadata.dataType = detectDataType(dataToWrite);
2775
2775
  this.metadata.totalRecords = this.metadata.files.reduce((sum, file) => sum + (file.recordsCount || 0), 0);
2776
- this.logger.debug?.(
2776
+ this.logger.silly?.(
2777
2777
  `[FileDatabase] Updated metadata for file ${currentFile.fileName}: recordsCount=${recordsCount}, totalRecords=${this.metadata.totalRecords}`
2778
2778
  );
2779
2779
  }
@@ -2797,7 +2797,7 @@ var FileDatabase = class _FileDatabase {
2797
2797
  }
2798
2798
  try {
2799
2799
  await fs3.promises.writeFile(filePath, serializedData, "utf8");
2800
- this.logger.debug?.(`[FileDatabase] Wrote ${bytesToHumanReadable(requiredBytes)} to ${filePath}`);
2800
+ this.logger.silly?.(`[FileDatabase] Wrote ${bytesToHumanReadable(requiredBytes)} to ${filePath}`);
2801
2801
  } catch (error) {
2802
2802
  throw new FileDatabaseError(`Failed to write file ${filePath}: ${error.message}`);
2803
2803
  }
@@ -2879,7 +2879,8 @@ var FileDatabase = class _FileDatabase {
2879
2879
  this.useMetadata = format.hasMetadata;
2880
2880
  }
2881
2881
  if (this.useMetadata) {
2882
- const metadataPath = path3.join(this.getDestinationPath(), "metadata.json");
2882
+ const destPath = this.getDestinationPath();
2883
+ const metadataPath = path3.join(destPath, "metadata.json");
2883
2884
  if (fs3.existsSync(metadataPath)) {
2884
2885
  try {
2885
2886
  const rawData = await fs3.promises.readFile(metadataPath, "utf8");
@@ -2893,7 +2894,9 @@ var FileDatabase = class _FileDatabase {
2893
2894
  throw new FileDatabaseError(`Failed to read metadata: ${e.message}`);
2894
2895
  }
2895
2896
  } else {
2896
- throw new FileDatabaseError("[FileDatabase] No metadata found in non-versioned mode");
2897
+ throw new FileDatabaseError(
2898
+ `[FileDatabase] No metadata found in non-versioned mode. Looked for: ${metadataPath} (table path: ${destPath})`
2899
+ );
2897
2900
  }
2898
2901
  } else {
2899
2902
  this.metadata = await this.figureMetadataFromVersionFiles("");
@@ -2910,6 +2913,13 @@ var FileDatabase = class _FileDatabase {
2910
2913
  * Write data to the file database
2911
2914
  */
2912
2915
  async write(data, options = {}) {
2916
+ if (options.filename) {
2917
+ const destPath2 = this.getDestinationPath();
2918
+ await ensurePath(destPath2);
2919
+ const filePath = path3.join(destPath2, options.filename);
2920
+ await this.safeWrite(filePath, data);
2921
+ return;
2922
+ }
2913
2923
  if (options.forceNewVersion && !this.versioned) {
2914
2924
  throw new FileDatabaseError("Cannot use forceNewVersion in non-versioned mode");
2915
2925
  }
@@ -2933,17 +2943,17 @@ var FileDatabase = class _FileDatabase {
2933
2943
  });
2934
2944
  if (matches) {
2935
2945
  targetFileIndex = i;
2936
- this.logger.debug?.(`[FileDatabase] Found existing file with matching custom metadata: ${fileEntry.fileName}, metadata: ${JSON.stringify(options.customMetadata)}`);
2946
+ this.logger.silly?.(`[FileDatabase] Found existing file with matching custom metadata: ${fileEntry.fileName}, metadata: ${JSON.stringify(options.customMetadata)}`);
2937
2947
  break;
2938
2948
  } else {
2939
- this.logger.debug?.(`[FileDatabase] File ${fileEntry.fileName} does not match custom metadata: ${JSON.stringify(options.customMetadata)}`);
2949
+ this.logger.silly?.(`[FileDatabase] File ${fileEntry.fileName} does not match custom metadata: ${JSON.stringify(options.customMetadata)}`);
2940
2950
  }
2941
2951
  }
2942
2952
  if (targetFileIndex === null) {
2943
- this.logger.debug?.(`[FileDatabase] No existing file found with custom metadata: ${JSON.stringify(options.customMetadata)}, will create new file`);
2953
+ this.logger.silly?.(`[FileDatabase] No existing file found with custom metadata: ${JSON.stringify(options.customMetadata)}, will create new file`);
2944
2954
  }
2945
2955
  } else {
2946
- this.logger.debug?.(`[FileDatabase] No custom metadata provided, will create new file`);
2956
+ this.logger.silly?.(`[FileDatabase] No custom metadata provided, will create new file`);
2947
2957
  }
2948
2958
  if (targetFileIndex !== null) {
2949
2959
  const targetFile = this.metadata.files[targetFileIndex];
@@ -2972,7 +2982,17 @@ var FileDatabase = class _FileDatabase {
2972
2982
  * Read data from the file database
2973
2983
  */
2974
2984
  async read(options = {}) {
2975
- const { version, nextPage = false, pageSize } = options;
2985
+ const { version, nextPage = false, pageSize, filename } = options;
2986
+ if (filename) {
2987
+ const destPath = this.getDestinationPath(version);
2988
+ const filePath = path3.join(destPath, filename);
2989
+ try {
2990
+ const rawData = await fs3.promises.readFile(filePath, "utf8");
2991
+ return JSON.parse(rawData);
2992
+ } catch (error) {
2993
+ throw new FileDatabaseError(`Failed to read file ${filename}: ${error.message}`);
2994
+ }
2995
+ }
2976
2996
  await this.prepare({ read: true, version });
2977
2997
  const isNonPaginatedData = this.metadata.dataType === "text" || this.metadata.dataType === "xml" || this.metadata.dataType === "json-object";
2978
2998
  if (isNonPaginatedData) {
@@ -3053,6 +3073,67 @@ var FileDatabase = class _FileDatabase {
3053
3073
  this.currentRecord = 0;
3054
3074
  this.hasReadFirstPage = false;
3055
3075
  }
3076
+ /**
3077
+ * List filenames in the table directory.
3078
+ * For catalog/key-value usage (files written with { filename }).
3079
+ * Returns data file names (.json, .txt, .xml) excluding metadata.json.
3080
+ */
3081
+ async listFilenames() {
3082
+ const destPath = this.versioned && this.currentVersion ? path3.join(this.getDestinationPath(), this.currentVersion) : this.getDestinationPath();
3083
+ try {
3084
+ const entries = await fs3.promises.readdir(destPath, { withFileTypes: true });
3085
+ return entries.filter((e) => e.isFile() && e.name !== "metadata.json" && /\.(json|txt|xml)$/i.test(e.name)).map((e) => e.name);
3086
+ } catch (err) {
3087
+ if (err?.code === "ENOENT") return [];
3088
+ throw new FileDatabaseError(`Failed to list files: ${err.message}`);
3089
+ }
3090
+ }
3091
+ /**
3092
+ * Remove a file from the table directory (catalog mode).
3093
+ * Use with listFilenames() to manage individual files.
3094
+ */
3095
+ async removeFile(filename) {
3096
+ const destPath = this.versioned && this.currentVersion ? path3.join(this.getDestinationPath(), this.currentVersion) : this.getDestinationPath();
3097
+ const filePath = path3.join(destPath, filename);
3098
+ try {
3099
+ await fs3.promises.unlink(filePath);
3100
+ } catch (err) {
3101
+ if (err?.code === "ENOENT") return;
3102
+ throw new FileDatabaseError(`Failed to remove file ${filename}: ${err.message}`);
3103
+ }
3104
+ }
3105
+ /**
3106
+ * Remove a file and its metadata entry (non-versioned mode with useMetadata).
3107
+ * Use with findData() to get fileName, then call removeFileEntry to delete.
3108
+ */
3109
+ async removeFileEntry(filename) {
3110
+ if (this.versioned) {
3111
+ throw new FileDatabaseError("removeFileEntry is only supported in non-versioned mode");
3112
+ }
3113
+ await this.prepare({ read: true });
3114
+ const idx = this.metadata.files.findIndex((f) => f.fileName === filename);
3115
+ if (idx === -1) {
3116
+ throw new FileDatabaseError(`File entry ${filename} not found in metadata`);
3117
+ }
3118
+ const entry = this.metadata.files[idx];
3119
+ const recordsCount = entry.recordsCount || 0;
3120
+ this.metadata.files.splice(idx, 1);
3121
+ this.metadata.totalRecords = Math.max(0, (this.metadata.totalRecords || 0) - recordsCount);
3122
+ const destPath = this.getDestinationPath();
3123
+ const filePath = path3.join(destPath, filename);
3124
+ try {
3125
+ await fs3.promises.unlink(filePath);
3126
+ } catch (err) {
3127
+ if (err?.code === "ENOENT") {
3128
+ this.logger.warn?.(`[FileDatabase] File ${filename} already missing on disk`);
3129
+ } else {
3130
+ throw new FileDatabaseError(`Failed to remove file ${filename}: ${err.message}`);
3131
+ }
3132
+ }
3133
+ if (this.useMetadata) {
3134
+ await this.saveVersionMetadata(this.metadata);
3135
+ }
3136
+ }
3056
3137
  /**
3057
3138
  * Set file-level synopsis calculation function
3058
3139
  */
@@ -3586,6 +3667,7 @@ var ALL_LEVELS = [
3586
3667
  "response",
3587
3668
  "progress"
3588
3669
  ];
3670
+ var DEFAULT_LEVELS = ALL_LEVELS.filter((l) => l !== "silly");
3589
3671
  var MAX_LEVEL_LENGTH = Math.max(...ALL_LEVELS.map((level) => level.toUpperCase().length));
3590
3672
  var LEVEL_COLORS = {
3591
3673
  error: chalk.red.bold,
@@ -3671,7 +3753,7 @@ var Logger = class _Logger {
3671
3753
  silent: false,
3672
3754
  showLevel: false,
3673
3755
  timestamp: false,
3674
- levels: ALL_LEVELS,
3756
+ levels: DEFAULT_LEVELS,
3675
3757
  progressTimes: false,
3676
3758
  progressThrottle: void 0
3677
3759
  };
@@ -3831,15 +3913,17 @@ var Logger = class _Logger {
3831
3913
  }
3832
3914
  normalizeLevels(levels) {
3833
3915
  if (!levels || !levels.length) {
3834
- return ALL_LEVELS;
3916
+ return DEFAULT_LEVELS;
3835
3917
  }
3836
- const includes = levels.filter((level) => !level.startsWith("-"));
3837
- const excludes = levels.filter((level) => level.startsWith("-")).map((level) => level.slice(1));
3838
- const unknown = [...includes, ...excludes].filter((level) => !ALL_LEVELS.includes(level));
3918
+ const tokens = levels.map((t) => String(t).trim()).filter(Boolean);
3919
+ const explicitIncludes = tokens.filter((t) => !t.startsWith("+") && !t.startsWith("-")).map((t) => t);
3920
+ const addIncludes = tokens.filter((t) => t.startsWith("+")).map((t) => t.slice(1));
3921
+ const excludes = tokens.filter((t) => t.startsWith("-")).map((t) => t.slice(1));
3922
+ const unknown = [...explicitIncludes, ...addIncludes, ...excludes].filter((level) => !ALL_LEVELS.includes(level));
3839
3923
  if (unknown.length) {
3840
3924
  console.warn(`[Logger] Unknown level(s): ${unknown.join(", ")}`);
3841
3925
  }
3842
- const base = includes.length ? includes : ALL_LEVELS;
3926
+ const base = explicitIncludes.length ? explicitIncludes : Array.from(/* @__PURE__ */ new Set([...DEFAULT_LEVELS, ...addIncludes]));
3843
3927
  return base.filter((level) => !excludes.includes(level));
3844
3928
  }
3845
3929
  isValidMode(mode) {
@@ -3897,6 +3981,1107 @@ function setup(opts = {}) {
3897
3981
  function setupContext(opts = {}) {
3898
3982
  return setup(opts);
3899
3983
  }
3984
+
3985
+ // src/utils/core-utils.ts
3986
+ function sleepMs(ms) {
3987
+ return new Promise((resolve2) => setTimeout(resolve2, ms));
3988
+ }
3989
+ function toJsonColumn(value) {
3990
+ if (value === void 0 || value === null) return null;
3991
+ return JSON.stringify(value);
3992
+ }
3993
+
3994
+ // src/tasks/taskUtils.ts
3995
+ import { randomUUID } from "crypto";
3996
+ function getDb(context) {
3997
+ const db = context.db;
3998
+ if (!db) {
3999
+ throw new Error("Tasks component requires context.db. Initialize DB first and attach to context.");
4000
+ }
4001
+ return db;
4002
+ }
4003
+ function queueToTableNames(queue) {
4004
+ if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(queue)) {
4005
+ throw new Error(`Invalid queue name "${queue}". Use letters, numbers, underscore only.`);
4006
+ }
4007
+ return {
4008
+ tasksTable: queue,
4009
+ historyTable: `${queue}_history`
4010
+ };
4011
+ }
4012
+ async function ensureTaskTables(context, options = {}) {
4013
+ const queue = options.queue ?? "tasks";
4014
+ const recreate = options.recreate ?? false;
4015
+ const db = getDb(context);
4016
+ const { tasksTable, historyTable } = queueToTableNames(queue);
4017
+ const needsTasks = recreate ? true : !await db.tableExists(tasksTable);
4018
+ const needsHistory = recreate ? true : !await db.tableExists(historyTable);
4019
+ if (recreate) {
4020
+ await db.schema.dropTableIfExists(historyTable);
4021
+ await db.schema.dropTableIfExists(tasksTable);
4022
+ }
4023
+ if (needsTasks) {
4024
+ await db.raw(`CREATE EXTENSION IF NOT EXISTS "uuid-ossp"`);
4025
+ await db.schema.createTable(tasksTable, (t) => {
4026
+ t.uuid("id").primary().defaultTo(db.raw("uuid_generate_v4()"));
4027
+ t.timestamp("created_at").notNullable().defaultTo(db.fn.now());
4028
+ t.timestamp("started_at");
4029
+ t.timestamp("completed_at");
4030
+ t.integer("priority").notNullable().defaultTo(0);
4031
+ t.text("schedule");
4032
+ t.timestamp("past_due").defaultTo(null);
4033
+ t.text("target").notNullable();
4034
+ t.text("task").notNullable();
4035
+ t.json("params");
4036
+ t.text("opid");
4037
+ t.timestamp("paused_at").defaultTo(null);
4038
+ t.text("progress");
4039
+ t.boolean("success");
4040
+ t.json("results");
4041
+ });
4042
+ await db.schema.alterTable(tasksTable, (t) => {
4043
+ t.index(["target", "started_at", "created_at"], `${tasksTable}_target_started_created_idx`);
4044
+ t.index(["target", "past_due", "priority", "created_at"], `${tasksTable}_target_past_due_priority_created_idx`);
4045
+ t.index(["target", "task"], `${tasksTable}_target_task_idx`);
4046
+ });
4047
+ }
4048
+ const tasksHasOpid = await db.schema.hasColumn(tasksTable, "opid");
4049
+ if (!tasksHasOpid) {
4050
+ await db.schema.alterTable(tasksTable, (t) => {
4051
+ t.text("opid");
4052
+ });
4053
+ }
4054
+ const tasksHasPausedAt = await db.schema.hasColumn(tasksTable, "paused_at");
4055
+ if (!tasksHasPausedAt) {
4056
+ await db.schema.alterTable(tasksTable, (t) => {
4057
+ t.timestamp("paused_at").defaultTo(null);
4058
+ });
4059
+ }
4060
+ if (needsHistory) {
4061
+ await db.schema.createTable(historyTable, (t) => {
4062
+ t.uuid("id").notNullable();
4063
+ t.timestamp("created_at").notNullable();
4064
+ t.timestamp("started_at");
4065
+ t.timestamp("completed_at");
4066
+ t.integer("priority").notNullable().defaultTo(0);
4067
+ t.text("schedule");
4068
+ t.timestamp("past_due").defaultTo(null);
4069
+ t.text("target").notNullable();
4070
+ t.text("task").notNullable();
4071
+ t.json("params");
4072
+ t.text("opid");
4073
+ t.text("progress");
4074
+ t.boolean("success");
4075
+ t.json("results");
4076
+ });
4077
+ await db.schema.alterTable(historyTable, (t) => {
4078
+ t.index(["target", "created_at"], `${historyTable}_target_created_idx`);
4079
+ t.index(["task", "created_at"], `${historyTable}_task_created_idx`);
4080
+ });
4081
+ }
4082
+ const historyHasOpid = await db.schema.hasColumn(historyTable, "opid");
4083
+ if (!historyHasOpid) {
4084
+ await db.schema.alterTable(historyTable, (t) => {
4085
+ t.text("opid");
4086
+ });
4087
+ }
4088
+ }
4089
+ async function enqueueTask(context, options) {
4090
+ const db = getDb(context);
4091
+ const queue = options.queue ?? "tasks";
4092
+ const { tasksTable } = queueToTableNames(queue);
4093
+ const id = randomUUID();
4094
+ await db(tasksTable).insert({
4095
+ id,
4096
+ target: options.target,
4097
+ task: options.task,
4098
+ params: toJsonColumn(options.params ?? null),
4099
+ opid: options.opid ?? null,
4100
+ priority: options.priority ?? 0,
4101
+ schedule: options.schedule ?? null
4102
+ });
4103
+ return id;
4104
+ }
4105
+ async function updateTaskProgress(context, tasksTable, taskId, progress) {
4106
+ const db = getDb(context);
4107
+ await db(tasksTable).where({ id: taskId }).update({
4108
+ progress: typeof progress === "string" ? progress : JSON.stringify(progress)
4109
+ });
4110
+ }
4111
+
4112
+ // src/tasks/taskLogs.ts
4113
+ function getLogsState(context) {
4114
+ const holder = context;
4115
+ if (holder.__tasksLogsState) return holder.__tasksLogsState;
4116
+ const basePath = holder.params?.get?.("tasksLogsBasePath") || "./data";
4117
+ const namespace = holder.params?.get?.("tasksLogsNamespace") || "tasks-logs";
4118
+ const tableName = holder.params?.get?.("tasksLogsTable") || "runner";
4119
+ const errorTableName = holder.params?.get?.("tasksErrorLogsTable") || `${tableName}-errors`;
4120
+ const maxVersionsRaw = Number(holder.params?.get?.("tasksLogsMaxVersions"));
4121
+ const pageSizeRaw = Number(holder.params?.get?.("tasksLogsPageSize"));
4122
+ const errorDb = new FileDatabase({
4123
+ basePath,
4124
+ namespace,
4125
+ tableName: errorTableName,
4126
+ versioned: true,
4127
+ useMetadata: true,
4128
+ maxVersions: Number.isFinite(maxVersionsRaw) && maxVersionsRaw > 0 ? maxVersionsRaw : 20,
4129
+ pageSize: Number.isFinite(pageSizeRaw) && pageSizeRaw > 0 ? pageSizeRaw : 2e3,
4130
+ logger: holder.logger
4131
+ });
4132
+ const enabledRaw = holder.params?.get?.("tasksLogsEnabled");
4133
+ const enabled = enabledRaw === void 0 ? true : !!enabledRaw;
4134
+ if (!enabled) {
4135
+ const disabledState = {
4136
+ db: null,
4137
+ errorDb,
4138
+ queue: Promise.resolve(),
4139
+ initialized: true,
4140
+ errorInitialized: false
4141
+ };
4142
+ holder.__tasksLogsState = disabledState;
4143
+ return disabledState;
4144
+ }
4145
+ const db = new FileDatabase({
4146
+ basePath,
4147
+ namespace,
4148
+ tableName,
4149
+ versioned: true,
4150
+ useMetadata: true,
4151
+ maxVersions: Number.isFinite(maxVersionsRaw) && maxVersionsRaw > 0 ? maxVersionsRaw : 20,
4152
+ pageSize: Number.isFinite(pageSizeRaw) && pageSizeRaw > 0 ? pageSizeRaw : 2e3,
4153
+ logger: holder.logger
4154
+ });
4155
+ const state = {
4156
+ db,
4157
+ errorDb,
4158
+ queue: Promise.resolve(),
4159
+ initialized: false,
4160
+ errorInitialized: false
4161
+ };
4162
+ holder.__tasksLogsState = state;
4163
+ return state;
4164
+ }
4165
+ function isErrorPayload(payload) {
4166
+ if (!payload) return false;
4167
+ if (typeof payload === "object") {
4168
+ const level = typeof payload.level === "string" ? payload.level.toLowerCase() : "";
4169
+ if (level === "error" || level === "fatal") return true;
4170
+ if (typeof payload.message === "string" && /\berror\b/i.test(payload.message)) return true;
4171
+ return false;
4172
+ }
4173
+ if (typeof payload === "string") {
4174
+ return /\berror\b/i.test(payload);
4175
+ }
4176
+ return false;
4177
+ }
4178
+ function buildLogRecord(task, payload) {
4179
+ const params = task.params && typeof task.params === "object" ? task.params : {};
4180
+ return {
4181
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
4182
+ opid: task.opid ?? null,
4183
+ taskId: task.id,
4184
+ taskName: task.task,
4185
+ target: task.target,
4186
+ source: typeof params.source === "string" ? params.source : null,
4187
+ resource: typeof params.resource === "string" ? params.resource : null,
4188
+ payload
4189
+ };
4190
+ }
4191
+ function appendTaskIpcLog(context, task, payload) {
4192
+ const state = getLogsState(context);
4193
+ if (!state.db && !state.errorDb) return;
4194
+ const record = buildLogRecord(task, payload);
4195
+ state.queue = state.queue.then(async () => {
4196
+ if (state.db) {
4197
+ await state.db.write([record], { forceNewVersion: !state.initialized });
4198
+ state.initialized = true;
4199
+ }
4200
+ if (state.errorDb && isErrorPayload(payload)) {
4201
+ await state.errorDb.write([record], { forceNewVersion: !state.errorInitialized });
4202
+ state.errorInitialized = true;
4203
+ }
4204
+ }).catch((error) => {
4205
+ context.logger.warn?.("[tasks] failed to persist IPC log entry:", error);
4206
+ });
4207
+ }
4208
+
4209
+ // src/tasks/time-matcher.ts
4210
+ var RANGES = ["0-59", "0-59", "0-23", "1-31", "1-12", "0-6"];
4211
+ function resolveAsterisks(field, range) {
4212
+ return field.includes("*") ? field.replace("*", range) : field;
4213
+ }
4214
+ function resolveRanges(field) {
4215
+ const regex = /(\d+)-(\d+)/;
4216
+ let current = field;
4217
+ while (true) {
4218
+ const match = regex.exec(current);
4219
+ if (!match) break;
4220
+ const raw = match[0];
4221
+ let first = Number(match[1]);
4222
+ let last = Number(match[2]);
4223
+ if (last < first) {
4224
+ [first, last] = [last, first];
4225
+ }
4226
+ const values = [];
4227
+ for (let i = first; i <= last; i += 1) {
4228
+ values.push(i);
4229
+ }
4230
+ current = current.replace(raw, values.join(","));
4231
+ }
4232
+ return current;
4233
+ }
4234
+ function resolveSteps(field) {
4235
+ const match = /^(.+)\/(\d+)$/.exec(field);
4236
+ if (!match) return field;
4237
+ const base = match[1];
4238
+ const step = Number(match[2]);
4239
+ if (!Number.isFinite(step) || step <= 0) return field;
4240
+ return base.split(",").map((v) => Number(v)).filter((v) => Number.isFinite(v) && v % step === 0).join(",");
4241
+ }
4242
+ function convertPattern(pattern) {
4243
+ const parts = pattern.trim().split(/\s+/);
4244
+ if (parts.length !== 6) {
4245
+ throw new Error(`Invalid schedule "${pattern}". Expected 6 fields: sec min hour day month weekday`);
4246
+ }
4247
+ return parts.map((field, idx) => resolveAsterisks(field, RANGES[idx])).map((field) => resolveRanges(field)).map((field) => resolveSteps(field));
4248
+ }
4249
+ function fieldMatches(field, value) {
4250
+ const allowed = field.split(",").map((v) => Number(v));
4251
+ return allowed.includes(value);
4252
+ }
4253
+ function timeMatcher(pattern, date = /* @__PURE__ */ new Date()) {
4254
+ const parsed = convertPattern(pattern);
4255
+ 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());
4256
+ }
4257
+
4258
+ // src/tasks/TaskMaster.ts
4259
+ var TaskMaster = class {
4260
+ context;
4261
+ task;
4262
+ constructor(context, task) {
4263
+ this.context = context;
4264
+ this.task = task;
4265
+ }
4266
+ cantRunReason() {
4267
+ return false;
4268
+ }
4269
+ requestStop(_allowanceMs) {
4270
+ }
4271
+ };
4272
+
4273
+ // src/tasks/coreTasks/TaskPing.ts
4274
+ var TaskPing = class extends TaskMaster {
4275
+ async run() {
4276
+ this.context.logger.info?.(`[TaskPing] pong (${this.task.id})`);
4277
+ return { success: true, results: "pong" };
4278
+ }
4279
+ };
4280
+
4281
+ // src/tasks/coreTasks/TaskSampleProcess.ts
4282
+ var TaskSampleProcess = class extends TaskMaster {
4283
+ stopRequested = false;
4284
+ stopAllowanceMs = 0;
4285
+ stopDecisionLogged = false;
4286
+ requestStop(allowanceMs) {
4287
+ this.stopRequested = true;
4288
+ this.stopAllowanceMs = Number.isFinite(allowanceMs) && allowanceMs > 0 ? allowanceMs : 0;
4289
+ this.context.logger.warn?.(
4290
+ `[TaskSampleProcess] stop signal received (${this.task.id}), allowanceMs=${this.stopAllowanceMs}`
4291
+ );
4292
+ }
4293
+ async run(reportProgress) {
4294
+ const totalRaw = this.task?.params?.total ?? 10;
4295
+ const delayRaw = this.task?.params?.delay ?? 1e3;
4296
+ const nameRaw = this.task?.params?.name;
4297
+ const total = Number(totalRaw);
4298
+ const delay = Number(delayRaw);
4299
+ const name = typeof nameRaw === "string" && nameRaw.trim() ? nameRaw.trim() : "sampleProcess";
4300
+ const errors = [];
4301
+ if (!Number.isInteger(total) || total <= 0) {
4302
+ errors.push('param "total" must be a positive integer');
4303
+ }
4304
+ if (!Number.isInteger(delay) || delay < 0) {
4305
+ errors.push('param "delay" must be an integer >= 0');
4306
+ }
4307
+ if (errors.length > 0) {
4308
+ return {
4309
+ success: false,
4310
+ results: {
4311
+ error: `Validation failed: ${errors.join(", ")}`,
4312
+ received: { total: totalRaw, delay: delayRaw, name: nameRaw }
4313
+ }
4314
+ };
4315
+ }
4316
+ const startedAt = Date.now();
4317
+ for (let i = 1; i <= total; i += 1) {
4318
+ if (this.stopRequested) {
4319
+ const remainingMs = Math.max(0, (total - i + 1) * delay);
4320
+ if (remainingMs <= this.stopAllowanceMs) {
4321
+ if (!this.stopDecisionLogged) {
4322
+ this.stopDecisionLogged = true;
4323
+ this.context.logger.warn?.(
4324
+ `[TaskSampleProcess] continue to finish (${this.task.id}): remainingMs=${remainingMs} <= allowanceMs=${this.stopAllowanceMs}`
4325
+ );
4326
+ }
4327
+ } else {
4328
+ this.context.logger.warn?.(
4329
+ `[TaskSampleProcess] stopping gracefully at iteration ${i}/${total} (${this.task.id}), remainingMs=${remainingMs} > allowanceMs=${this.stopAllowanceMs}`
4330
+ );
4331
+ return {
4332
+ success: false,
4333
+ results: {
4334
+ message: `Stopped before completion at iteration ${i}/${total}`,
4335
+ completed: i - 1,
4336
+ total,
4337
+ name,
4338
+ remainingMs,
4339
+ allowanceMs: this.stopAllowanceMs
4340
+ }
4341
+ };
4342
+ }
4343
+ }
4344
+ const elapsed = Date.now() - startedAt;
4345
+ const remaining = Math.max(0, (total - i) * delay);
4346
+ const progress = {
4347
+ name,
4348
+ count: i,
4349
+ total,
4350
+ elapsedMs: elapsed,
4351
+ remainingMs: remaining,
4352
+ status: `running ${name}: ${i}/${total}`
4353
+ };
4354
+ this.context.logger.progress("running", {
4355
+ prefix: name,
4356
+ count: i,
4357
+ total
4358
+ });
4359
+ await reportProgress(progress);
4360
+ await sleepMs(delay);
4361
+ }
4362
+ return {
4363
+ success: true,
4364
+ results: {
4365
+ message: `Completed ${total} iterations`,
4366
+ total,
4367
+ delay,
4368
+ name
4369
+ }
4370
+ };
4371
+ }
4372
+ };
4373
+
4374
+ // src/tasks/coreTasks/TaskShellCommand.ts
4375
+ import { spawn } from "child_process";
4376
+ function runShellCommand(command, cwd) {
4377
+ return new Promise((resolve2, reject) => {
4378
+ const child = spawn(command, {
4379
+ shell: true,
4380
+ cwd: cwd || process.cwd(),
4381
+ stdio: ["ignore", "pipe", "pipe"]
4382
+ });
4383
+ let output = "";
4384
+ let stderr = "";
4385
+ child.stdout.on("data", (chunk) => {
4386
+ output += String(chunk);
4387
+ });
4388
+ child.stderr.on("data", (chunk) => {
4389
+ stderr += String(chunk);
4390
+ });
4391
+ child.on("error", (error) => {
4392
+ reject(error);
4393
+ });
4394
+ child.on("close", (exitCode, signal) => {
4395
+ resolve2({
4396
+ exitCode,
4397
+ output: output.trim(),
4398
+ stderr: stderr.trim(),
4399
+ signal
4400
+ });
4401
+ });
4402
+ });
4403
+ }
4404
+ var TaskShellCommand = class extends TaskMaster {
4405
+ async run() {
4406
+ const params = this.task?.params;
4407
+ const commandRaw = typeof params === "string" ? params : params?.command;
4408
+ const cwdRaw = typeof params === "string" ? void 0 : params?.cwd;
4409
+ const command = typeof commandRaw === "string" ? commandRaw.trim() : "";
4410
+ const cwd = typeof cwdRaw === "string" && cwdRaw.trim() ? cwdRaw.trim() : void 0;
4411
+ if (!command) {
4412
+ return {
4413
+ success: false,
4414
+ results: {
4415
+ error: 'Validation failed: param "command" must be a non-empty string',
4416
+ received: this.task?.params ?? null
4417
+ }
4418
+ };
4419
+ }
4420
+ try {
4421
+ const result = await runShellCommand(command, cwd);
4422
+ const success = result.exitCode === 0;
4423
+ this.context.logger.info?.(
4424
+ `[TaskShellCommand] command="${command}" exitCode=${String(result.exitCode)} (${this.task.id})`
4425
+ );
4426
+ return {
4427
+ success,
4428
+ results: {
4429
+ command,
4430
+ cwd: cwd ?? process.cwd(),
4431
+ output: result.output,
4432
+ stderr: result.stderr,
4433
+ exitCode: result.exitCode,
4434
+ signal: result.signal
4435
+ }
4436
+ };
4437
+ } catch (error) {
4438
+ return {
4439
+ success: false,
4440
+ results: {
4441
+ command,
4442
+ cwd: cwd ?? process.cwd(),
4443
+ output: "",
4444
+ stderr: "",
4445
+ exitCode: null,
4446
+ error: error?.message ?? String(error)
4447
+ }
4448
+ };
4449
+ }
4450
+ }
4451
+ };
4452
+
4453
+ // src/tasks/coreTasks/TaskSystemInfo.ts
4454
+ import os from "os";
4455
+ import fs4 from "fs/promises";
4456
+ function toGb(valueBytes) {
4457
+ return `${(valueBytes / 1024 ** 3).toFixed(2)} GB`;
4458
+ }
4459
+ function toMb(valueBytes) {
4460
+ return `${(valueBytes / 1024 ** 2).toFixed(2)} MB`;
4461
+ }
4462
+ async function getDiskStats() {
4463
+ const stats = await fs4.statfs("/");
4464
+ const total = Number(stats.bsize) * Number(stats.blocks);
4465
+ const free = Number(stats.bsize) * Number(stats.bavail);
4466
+ const used = total - free;
4467
+ return {
4468
+ total: toGb(total),
4469
+ used: toGb(used),
4470
+ free: toGb(free)
4471
+ };
4472
+ }
4473
+ var TaskSystemInfo = class extends TaskMaster {
4474
+ async run() {
4475
+ try {
4476
+ const totalMemory = os.totalmem();
4477
+ const freeMemory = os.freemem();
4478
+ const usedMemory = totalMemory - freeMemory;
4479
+ const cpus = os.cpus();
4480
+ const cpuUtilization = cpus.map((cpu) => {
4481
+ const total = Object.values(cpu.times).reduce((acc, time) => acc + time, 0);
4482
+ const usage = (total - cpu.times.idle) / total * 100;
4483
+ return Number(usage.toFixed(2));
4484
+ });
4485
+ const processMemory = process.memoryUsage();
4486
+ const disk = await getDiskStats();
4487
+ const results = {
4488
+ memory: {
4489
+ total: toGb(totalMemory),
4490
+ used: toGb(usedMemory),
4491
+ free: toGb(freeMemory)
4492
+ },
4493
+ processMemory: {
4494
+ rss: toMb(processMemory.rss),
4495
+ heapTotal: toMb(processMemory.heapTotal),
4496
+ heapUsed: toMb(processMemory.heapUsed),
4497
+ external: toMb(processMemory.external)
4498
+ },
4499
+ disk,
4500
+ cpu: {
4501
+ cores: cpuUtilization.length,
4502
+ utilization: cpuUtilization
4503
+ },
4504
+ runtime: {
4505
+ platform: os.platform(),
4506
+ arch: os.arch(),
4507
+ uptimeSec: os.uptime(),
4508
+ hostname: os.hostname()
4509
+ }
4510
+ };
4511
+ this.context.logger.info?.(`[TaskSystemInfo] collected system metrics (${this.task.id})`);
4512
+ return { success: true, results };
4513
+ } catch (error) {
4514
+ return {
4515
+ success: false,
4516
+ results: {
4517
+ error: "Can't collect system stats",
4518
+ message: error?.message ?? String(error)
4519
+ }
4520
+ };
4521
+ }
4522
+ }
4523
+ };
4524
+
4525
+ // src/tasks/coreTasks/TaskSumAB.ts
4526
+ var TaskSumAB = class extends TaskMaster {
4527
+ async run() {
4528
+ const a = this.task?.params?.a;
4529
+ const b = this.task?.params?.b;
4530
+ if (typeof a !== "number" || Number.isNaN(a)) {
4531
+ return {
4532
+ success: false,
4533
+ results: {
4534
+ error: 'Validation failed: param "a" must be a valid number',
4535
+ received: { a, b }
4536
+ }
4537
+ };
4538
+ }
4539
+ if (typeof b !== "number" || Number.isNaN(b)) {
4540
+ return {
4541
+ success: false,
4542
+ results: {
4543
+ error: 'Validation failed: param "b" must be a valid number',
4544
+ received: { a, b }
4545
+ }
4546
+ };
4547
+ }
4548
+ const sum = a + b;
4549
+ this.context.logger.info?.(`[TaskSumAB] ${a} + ${b} = ${sum} (${this.task.id})`);
4550
+ return {
4551
+ success: true,
4552
+ results: { a, b, sum }
4553
+ };
4554
+ }
4555
+ };
4556
+
4557
+ // src/tasks/coreTasks/TaskStopRunner.ts
4558
+ var TaskStopRunner = class extends TaskMaster {
4559
+ async run() {
4560
+ const allowanceMs = Number(this.task?.params?.allowanceMs ?? 5e3);
4561
+ this.context.logger.warn?.(`[TaskStopRunner] stop requested (allowanceMs=${allowanceMs})`);
4562
+ return {
4563
+ success: true,
4564
+ results: {
4565
+ stopRunner: true,
4566
+ allowanceMs,
4567
+ message: "Runner stop requested"
4568
+ }
4569
+ };
4570
+ }
4571
+ };
4572
+
4573
+ // src/tasks/TasksRegistry.ts
4574
+ var TasksRegistry = class _TasksRegistry {
4575
+ map = {};
4576
+ constructor(initial) {
4577
+ if (initial) {
4578
+ this.addMany(initial);
4579
+ }
4580
+ }
4581
+ static withCoreTasks() {
4582
+ return new _TasksRegistry().add("ping", TaskPing).add("sampleProcess", TaskSampleProcess).add("shellCommand", TaskShellCommand).add("systemInfo", TaskSystemInfo).add("taskSumAB", TaskSumAB).add("stopRunner", TaskStopRunner).add("stop", TaskStopRunner);
4583
+ }
4584
+ add(taskName, taskClass) {
4585
+ this.map[taskName] = taskClass;
4586
+ return this;
4587
+ }
4588
+ addMany(entries) {
4589
+ for (const [name, klass] of Object.entries(entries)) {
4590
+ this.add(name, klass);
4591
+ }
4592
+ return this;
4593
+ }
4594
+ get(taskName) {
4595
+ return this.map[taskName];
4596
+ }
4597
+ listSupportedTasks() {
4598
+ return Object.keys(this.map).sort();
4599
+ }
4600
+ toObject() {
4601
+ return { ...this.map };
4602
+ }
4603
+ };
4604
+
4605
+ // src/tasks/taskScriptRunner.ts
4606
+ import { spawn as spawn2 } from "child_process";
4607
+ function toCliArgs(args = []) {
4608
+ return args.filter((a) => typeof a === "string" && a.length > 0);
4609
+ }
4610
+ function formatChildLogPrefix(task) {
4611
+ return `${task.task}:${task.id.slice(0, 8)}${task.opid ? `:${task.opid}` : ""}`;
4612
+ }
4613
+ async function runNodeTaskScript(context, options) {
4614
+ const cliArgs = toCliArgs(["--route=ipc", "--mode=json", ...options.args || []]);
4615
+ const inheritedExecArgs = Array.isArray(process.execArgv) ? [...process.execArgv] : [];
4616
+ const hasTsRuntimeInParent = inheritedExecArgs.some((arg) => /tsx|ts-node/i.test(arg));
4617
+ const nodeArgs = hasTsRuntimeInParent ? [...inheritedExecArgs, options.scriptPath, ...cliArgs] : ["--import", "tsx", options.scriptPath, ...cliArgs];
4618
+ const child = spawn2(
4619
+ process.execPath,
4620
+ nodeArgs,
4621
+ {
4622
+ cwd: options.cwd || process.cwd(),
4623
+ stdio: ["ignore", "pipe", "pipe", "ipc"],
4624
+ env: {
4625
+ ...process.env,
4626
+ TASK_ID: options.task.id,
4627
+ TASK_NAME: options.task.task,
4628
+ TASK_OPID: options.task.opid || ""
4629
+ }
4630
+ }
4631
+ );
4632
+ let stdout = "";
4633
+ let stderr = "";
4634
+ let workerResult = null;
4635
+ let hadErrorMessage = false;
4636
+ const prefix = formatChildLogPrefix(options.task);
4637
+ const db = context.db;
4638
+ const tasksTable = context.params?.get?.("table") || "tasks";
4639
+ let progressWriteChain = Promise.resolve();
4640
+ let progressCallbackChain = Promise.resolve();
4641
+ const updateProgress = (text) => {
4642
+ if (!db || !text || !text.trim()) return;
4643
+ progressWriteChain = progressWriteChain.then(async () => {
4644
+ await db(tasksTable).where({ id: options.task.id }).update({ progress: text.slice(0, 4e3) });
4645
+ }).catch((error) => {
4646
+ context.logger.warn?.(
4647
+ `[tasks] failed to update progress for task=${options.task.id}: ${error?.message ?? String(error)}`
4648
+ );
4649
+ });
4650
+ if (options.onProgress) {
4651
+ progressCallbackChain = progressCallbackChain.then(async () => {
4652
+ await options.onProgress?.(text.slice(0, 4e3));
4653
+ }).catch((error) => {
4654
+ context.logger.warn?.(
4655
+ `[tasks] reportProgress callback failed for task=${options.task.id}: ${error?.message ?? String(error)}`
4656
+ );
4657
+ });
4658
+ }
4659
+ };
4660
+ const payloadToProgressText = (payload) => {
4661
+ if (!payload) return "";
4662
+ if (typeof payload === "string") return payload;
4663
+ if (typeof payload.message === "string" && payload.level === "progress" && payload.count !== void 0 && payload.total !== void 0) {
4664
+ const pfx = payload.prefix ? `${payload.prefix} ` : "";
4665
+ return `${pfx}${payload.message} ${payload.count}/${payload.total}`;
4666
+ }
4667
+ if (typeof payload.message === "string") return payload.message;
4668
+ if (payload.level === "progress" && payload.count !== void 0 && payload.total !== void 0) {
4669
+ const pfx = payload.prefix ? `${payload.prefix} ` : "";
4670
+ return `${pfx}${payload.count}/${payload.total}`;
4671
+ }
4672
+ return "";
4673
+ };
4674
+ child.stdout.on("data", (chunk) => {
4675
+ const text = String(chunk);
4676
+ stdout += text;
4677
+ if (text.trim()) {
4678
+ context.logger.info?.(`[child:${prefix}] ${text.trimEnd()}`);
4679
+ updateProgress(text.trim().replace(/\s+/g, " ").slice(0, 400));
4680
+ }
4681
+ });
4682
+ child.stderr.on("data", (chunk) => {
4683
+ const text = String(chunk);
4684
+ stderr += text;
4685
+ if (text.trim()) {
4686
+ context.logger.warn?.(`[child:${prefix}] ${text.trimEnd()}`);
4687
+ }
4688
+ });
4689
+ child.on("message", (message) => {
4690
+ if (message && typeof message === "object" && "__taskWorkerResult" in message) {
4691
+ workerResult = message.__taskWorkerResult;
4692
+ return;
4693
+ }
4694
+ if (message && typeof message === "object") {
4695
+ const level = typeof message.level === "string" ? message.level.toLowerCase() : "";
4696
+ if (level === "error" || level === "fatal") {
4697
+ hadErrorMessage = true;
4698
+ }
4699
+ }
4700
+ appendTaskIpcLog(context, options.task, message);
4701
+ const progressText = payloadToProgressText(message);
4702
+ if (progressText) {
4703
+ updateProgress(progressText);
4704
+ if (typeof message === "object" && message?.level === "progress" && message.count !== void 0 && message.total !== void 0) {
4705
+ const countNum = Number(String(message.count).trim());
4706
+ const totalNum = Number(message.total);
4707
+ if (Number.isFinite(countNum) && Number.isFinite(totalNum) && totalNum > 0) {
4708
+ context.logger.progress(message.message || "progress", {
4709
+ prefix: message.prefix || prefix,
4710
+ count: countNum,
4711
+ total: totalNum
4712
+ });
4713
+ } else {
4714
+ context.logger.info?.(`[child:${prefix}] ${progressText}`);
4715
+ }
4716
+ } else {
4717
+ context.logger.info?.(`[child:${prefix}] ${progressText}`);
4718
+ }
4719
+ }
4720
+ });
4721
+ return await new Promise((resolve2, reject) => {
4722
+ child.on("error", (error) => reject(error));
4723
+ child.on("close", (exitCode, signal) => {
4724
+ Promise.allSettled([progressWriteChain, progressCallbackChain]).finally(() => {
4725
+ resolve2({
4726
+ exitCode,
4727
+ signal,
4728
+ stdout: stdout.trim(),
4729
+ stderr: stderr.trim(),
4730
+ workerResult,
4731
+ hadErrorMessage
4732
+ });
4733
+ });
4734
+ });
4735
+ });
4736
+ }
4737
+
4738
+ // src/tasks/index.ts
4739
+ var LOCKED_BY_ERROR_MESSAGE = "locked by error";
4740
+ var defaultTasksRegistry = TasksRegistry.withCoreTasks();
4741
+ function getDb2(context) {
4742
+ const db = context.db;
4743
+ if (!db) {
4744
+ throw new Error("Tasks component requires context.db. Initialize DB first and attach to context.");
4745
+ }
4746
+ return db;
4747
+ }
4748
+ function normalizeRegistry(registry) {
4749
+ if (!registry) return defaultTasksRegistry;
4750
+ if (registry instanceof TasksRegistry) return registry;
4751
+ return new TasksRegistry().addMany(registry);
4752
+ }
4753
+ function normalizeAllowedTasks(value) {
4754
+ if (!value) return void 0;
4755
+ if (Array.isArray(value)) {
4756
+ const out2 = value.map((v) => String(v).trim()).filter(Boolean);
4757
+ return out2.length ? out2 : void 0;
4758
+ }
4759
+ const out = String(value).split(",").map((v) => v.trim()).filter(Boolean);
4760
+ return out.length ? out : void 0;
4761
+ }
4762
+ async function enqueueStopTask(context, target, queue = "tasks", allowanceMs = 5e3) {
4763
+ return enqueueTask(context, {
4764
+ queue,
4765
+ target,
4766
+ task: "stopRunner",
4767
+ params: { allowanceMs },
4768
+ priority: 1e6
4769
+ });
4770
+ }
4771
+ async function signalRunningTasksStop(context, runningTaskInstances, allowanceMs) {
4772
+ context.logger.warn?.(`[tasks] signaling ${runningTaskInstances.size} running task(s) to stop`);
4773
+ for (const [, taskInstance] of runningTaskInstances) {
4774
+ if (typeof taskInstance.requestStop === "function") {
4775
+ try {
4776
+ await taskInstance.requestStop(allowanceMs);
4777
+ } catch (error) {
4778
+ context.logger.warn?.("[tasks] task requestStop failed:", error);
4779
+ }
4780
+ }
4781
+ }
4782
+ context.emitter.emit("stop", allowanceMs);
4783
+ }
4784
+ async function executeClaimedTask(context, tasksTable, historyTable, row, registry, runningTaskInstances) {
4785
+ const db = getDb2(context);
4786
+ const taskName = row.task;
4787
+ const TaskClass = registry.get(taskName);
4788
+ const { paused_at: _pausedAt, ...rowForHistory } = row;
4789
+ if (!TaskClass) {
4790
+ const err = { message: `Unknown task "${taskName}"` };
4791
+ await db(historyTable).insert({
4792
+ ...rowForHistory,
4793
+ completed_at: /* @__PURE__ */ new Date(),
4794
+ success: false,
4795
+ params: toJsonColumn(row.params),
4796
+ results: toJsonColumn(err)
4797
+ });
4798
+ if (row.schedule) {
4799
+ await db(tasksTable).where({ id: row.id }).update({
4800
+ started_at: null,
4801
+ completed_at: /* @__PURE__ */ new Date(),
4802
+ success: false,
4803
+ results: toJsonColumn(err),
4804
+ past_due: null,
4805
+ paused_at: db.fn.now(),
4806
+ progress: LOCKED_BY_ERROR_MESSAGE
4807
+ });
4808
+ } else {
4809
+ await db(tasksTable).where({ id: row.id }).delete();
4810
+ }
4811
+ return { stopRunnerRequested: false, stopAllowanceMs: 0 };
4812
+ }
4813
+ let success = false;
4814
+ let results = null;
4815
+ let taskInstance = null;
4816
+ try {
4817
+ taskInstance = new TaskClass(context, row);
4818
+ runningTaskInstances.set(row.id, taskInstance);
4819
+ const runResult = await taskInstance.run((progress) => updateTaskProgress(context, tasksTable, row.id, progress));
4820
+ success = !!runResult?.success;
4821
+ results = runResult?.results ?? null;
4822
+ } catch (error) {
4823
+ success = false;
4824
+ results = {
4825
+ message: error?.message ?? String(error),
4826
+ name: error?.name ?? "Error",
4827
+ stack: error?.stack ?? null
4828
+ };
4829
+ } finally {
4830
+ runningTaskInstances.delete(row.id);
4831
+ }
4832
+ await db(historyTable).insert({
4833
+ ...rowForHistory,
4834
+ completed_at: /* @__PURE__ */ new Date(),
4835
+ success,
4836
+ params: toJsonColumn(row.params),
4837
+ results: toJsonColumn(results)
4838
+ });
4839
+ if (!success) {
4840
+ const dbName = String(context?.params?.get?.("dbName") || "local");
4841
+ const tableName = String(context?.params?.get?.("table") || "tasks");
4842
+ const fallbackRecoverCommand = [
4843
+ "npx",
4844
+ "tsx",
4845
+ "examples/tasks/recover-task.ts",
4846
+ `--dbName='${dbName.replace(/'/g, `'\\''`)}'`,
4847
+ `--table='${tableName.replace(/'/g, `'\\''`)}'`,
4848
+ `--id='${String(row.id).replace(/'/g, `'\\''`)}'`
4849
+ ].join(" ");
4850
+ const rerunCommand = results && typeof results === "object" && results.rerunCommand ? results.rerunCommand : fallbackRecoverCommand;
4851
+ appendTaskIpcLog(context, row, {
4852
+ level: "error",
4853
+ message: `[tasks] task failed: ${row.task} id=${row.id}. Once problem is fixed, re-run: ${rerunCommand}`,
4854
+ details: results
4855
+ });
4856
+ }
4857
+ if (row.schedule) {
4858
+ if (success) {
4859
+ await db(tasksTable).where({ id: row.id }).update({
4860
+ started_at: null,
4861
+ completed_at: /* @__PURE__ */ new Date(),
4862
+ success,
4863
+ results: toJsonColumn(results),
4864
+ progress: null,
4865
+ past_due: null
4866
+ });
4867
+ } else {
4868
+ await db(tasksTable).where({ id: row.id }).update({
4869
+ started_at: null,
4870
+ completed_at: /* @__PURE__ */ new Date(),
4871
+ success,
4872
+ results: toJsonColumn(results),
4873
+ paused_at: db.fn.now(),
4874
+ progress: LOCKED_BY_ERROR_MESSAGE,
4875
+ past_due: null
4876
+ });
4877
+ }
4878
+ } else {
4879
+ await db(tasksTable).where({ id: row.id }).delete();
4880
+ }
4881
+ const stopRunnerRequested = !!(results && typeof results === "object" && results.stopRunner === true);
4882
+ const stopAllowanceMs = stopRunnerRequested ? Number(results.allowanceMs ?? 5e3) : 0;
4883
+ return { stopRunnerRequested, stopAllowanceMs };
4884
+ }
4885
+ async function claimNextRunnableTask(context, tasksTable, target, registry, scanLimit, taskNames) {
4886
+ const db = getDb2(context);
4887
+ 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);
4888
+ if (taskNames && taskNames.length > 0) {
4889
+ query = query.whereIn("task", taskNames);
4890
+ }
4891
+ const candidates = await query;
4892
+ for (const row of candidates) {
4893
+ if (!row.past_due && row.schedule && !timeMatcher(row.schedule)) {
4894
+ continue;
4895
+ }
4896
+ const TaskClass = registry.get(row.task);
4897
+ if (TaskClass) {
4898
+ const taskInstance = new TaskClass(context, row);
4899
+ const reason = taskInstance.cantRunReason ? await taskInstance.cantRunReason() : null;
4900
+ if (reason) {
4901
+ if (!row.past_due) {
4902
+ await db(tasksTable).where({ id: row.id }).update({
4903
+ past_due: db.fn.now(),
4904
+ progress: String(reason)
4905
+ });
4906
+ }
4907
+ continue;
4908
+ }
4909
+ }
4910
+ const updated = await db(tasksTable).where({ id: row.id }).whereNull("started_at").whereNull("paused_at").update({ started_at: db.fn.now() }).returning("*");
4911
+ const claimed = Array.isArray(updated) ? updated[0] : null;
4912
+ if (claimed) return claimed;
4913
+ }
4914
+ return null;
4915
+ }
4916
+ async function runTasksLoop(context, options) {
4917
+ const queue = options.queue ?? "tasks";
4918
+ const target = options.target;
4919
+ const pollMs = options.pollMs ?? 1e3;
4920
+ const maxParallel = options.maxParallel ?? 1;
4921
+ const scanLimit = options.scanLimit ?? 100;
4922
+ const allowedTasks = normalizeAllowedTasks(options.allowedTasks);
4923
+ const registry = normalizeRegistry(options.registry);
4924
+ const { tasksTable, historyTable } = queueToTableNames(queue);
4925
+ if (!target) throw new Error("runTasksLoop: target is required");
4926
+ const runningPromises = /* @__PURE__ */ new Set();
4927
+ const runningTaskInstances = /* @__PURE__ */ new Map();
4928
+ let runningStopControlPromise = null;
4929
+ let stopRequested = false;
4930
+ let stopAllowanceMs = 5e3;
4931
+ context.__tasksRunnerStop = false;
4932
+ while (!context.isStop() && !stopRequested && !context.__tasksRunnerStop) {
4933
+ if (!runningStopControlPromise) {
4934
+ const claimedStopTask = await claimNextRunnableTask(
4935
+ context,
4936
+ tasksTable,
4937
+ target,
4938
+ registry,
4939
+ 10,
4940
+ ["stopRunner", "stop"]
4941
+ );
4942
+ if (claimedStopTask) {
4943
+ runningStopControlPromise = executeClaimedTask(
4944
+ context,
4945
+ tasksTable,
4946
+ historyTable,
4947
+ claimedStopTask,
4948
+ registry,
4949
+ runningTaskInstances
4950
+ ).then(async (outcome) => {
4951
+ if (outcome.stopRunnerRequested && !stopRequested) {
4952
+ stopRequested = true;
4953
+ stopAllowanceMs = outcome.stopAllowanceMs || 5e3;
4954
+ context.__tasksRunnerStop = true;
4955
+ await signalRunningTasksStop(context, runningTaskInstances, stopAllowanceMs);
4956
+ }
4957
+ }).finally(() => {
4958
+ runningStopControlPromise = null;
4959
+ });
4960
+ }
4961
+ }
4962
+ while (runningPromises.size < maxParallel) {
4963
+ const claimed = await claimNextRunnableTask(
4964
+ context,
4965
+ tasksTable,
4966
+ target,
4967
+ registry,
4968
+ scanLimit,
4969
+ allowedTasks
4970
+ );
4971
+ if (!claimed) break;
4972
+ const p = executeClaimedTask(context, tasksTable, historyTable, claimed, registry, runningTaskInstances).then(async (outcome) => {
4973
+ if (outcome.stopRunnerRequested && !stopRequested) {
4974
+ stopRequested = true;
4975
+ stopAllowanceMs = outcome.stopAllowanceMs || 5e3;
4976
+ context.__tasksRunnerStop = true;
4977
+ await signalRunningTasksStop(context, runningTaskInstances, stopAllowanceMs);
4978
+ }
4979
+ }).finally(() => {
4980
+ runningPromises.delete(p);
4981
+ });
4982
+ runningPromises.add(p);
4983
+ }
4984
+ await sleepMs(pollMs);
4985
+ }
4986
+ if (context.isStop() && !stopRequested) {
4987
+ await signalRunningTasksStop(context, runningTaskInstances, 5e3);
4988
+ }
4989
+ if (runningPromises.size > 0) {
4990
+ if (stopRequested) {
4991
+ await Promise.race([
4992
+ Promise.allSettled(Array.from(runningPromises)),
4993
+ sleepMs(stopAllowanceMs).then(() => {
4994
+ context.logger.warn?.(
4995
+ `[tasks] stop allowance (${stopAllowanceMs}ms) reached; ${runningPromises.size} task(s) still running`
4996
+ );
4997
+ })
4998
+ ]);
4999
+ } else {
5000
+ await Promise.allSettled(Array.from(runningPromises));
5001
+ }
5002
+ }
5003
+ }
5004
+ async function waitForTaskResult(context, taskId, options = {}) {
5005
+ const db = getDb2(context);
5006
+ const queue = options.queue ?? "tasks";
5007
+ const timeoutMs = options.timeoutMs ?? 6e4;
5008
+ const pollMs = options.pollMs ?? 500;
5009
+ const { tasksTable, historyTable } = queueToTableNames(queue);
5010
+ const deadline = Date.now() + timeoutMs;
5011
+ while (Date.now() <= deadline) {
5012
+ const done = await db(historyTable).where({ id: taskId }).orderBy("created_at", "desc").first();
5013
+ if (done) return done;
5014
+ const pending = await db(tasksTable).where({ id: taskId }).first();
5015
+ if (!pending) {
5016
+ const maybeDone = await db(historyTable).where({ id: taskId }).orderBy("created_at", "desc").first();
5017
+ return maybeDone ?? null;
5018
+ }
5019
+ await sleepMs(pollMs);
5020
+ }
5021
+ return null;
5022
+ }
5023
+ var TasksManager = class _TasksManager {
5024
+ context;
5025
+ queue;
5026
+ target;
5027
+ recreateTaskTables;
5028
+ pollMs;
5029
+ maxParallel;
5030
+ scanLimit;
5031
+ allowedTasks;
5032
+ registry;
5033
+ constructor(context, options = {}) {
5034
+ this.context = context;
5035
+ this.queue = options.queue ?? "tasks";
5036
+ this.target = options.target ?? "localRunner";
5037
+ this.recreateTaskTables = options.recreateTaskTables ?? false;
5038
+ this.pollMs = options.pollMs ?? 1e3;
5039
+ this.maxParallel = options.maxParallel ?? 1;
5040
+ this.scanLimit = options.scanLimit ?? 100;
5041
+ this.allowedTasks = normalizeAllowedTasks(options.allowedTasks);
5042
+ this.registry = normalizeRegistry(options.registry);
5043
+ }
5044
+ static init(context, options = {}) {
5045
+ const defs = {
5046
+ table: "string default tasks",
5047
+ target: "string default localRunner",
5048
+ recreateTaskTables: "boolean default false",
5049
+ pollMs: "number default 1000",
5050
+ maxParallel: "number default 1",
5051
+ scanLimit: "number default 100",
5052
+ allowedTasks: "string"
5053
+ };
5054
+ const discovered = context.params.getAllForModule(defs);
5055
+ const resolved = {
5056
+ queue: discovered.table,
5057
+ target: discovered.target,
5058
+ recreateTaskTables: discovered.recreateTaskTables,
5059
+ pollMs: discovered.pollMs,
5060
+ maxParallel: discovered.maxParallel,
5061
+ scanLimit: discovered.scanLimit,
5062
+ allowedTasks: discovered.allowedTasks,
5063
+ ...options
5064
+ };
5065
+ return new _TasksManager(context, resolved);
5066
+ }
5067
+ async ensureTaskTables(options = {}) {
5068
+ await ensureTaskTables(this.context, {
5069
+ queue: this.queue,
5070
+ recreate: options.recreate ?? this.recreateTaskTables
5071
+ });
5072
+ }
5073
+ async runTasksLoop(options = {}) {
5074
+ await runTasksLoop(this.context, {
5075
+ queue: options.queue ?? this.queue,
5076
+ target: options.target ?? this.target,
5077
+ pollMs: options.pollMs ?? this.pollMs,
5078
+ maxParallel: options.maxParallel ?? this.maxParallel,
5079
+ scanLimit: options.scanLimit ?? this.scanLimit,
5080
+ allowedTasks: options.allowedTasks ?? this.allowedTasks,
5081
+ registry: options.registry ?? this.registry
5082
+ });
5083
+ }
5084
+ };
3900
5085
  export {
3901
5086
  Args,
3902
5087
  Box5 as Box,
@@ -3919,8 +5104,18 @@ export {
3919
5104
  ScreenFooter,
3920
5105
  ScreenRow,
3921
5106
  ScreenTitle,
5107
+ TaskMaster,
5108
+ TaskPing,
5109
+ TaskSampleProcess,
5110
+ TaskShellCommand,
5111
+ TaskStopRunner,
5112
+ TaskSumAB,
5113
+ TaskSystemInfo,
5114
+ TasksManager,
5115
+ TasksRegistry,
3922
5116
  Text5 as Text,
3923
5117
  TextBlock,
5118
+ appendTaskIpcLog,
3924
5119
  buildBreadcrumb,
3925
5120
  buildDetailBreadcrumb,
3926
5121
  buildFooter,
@@ -3928,7 +5123,11 @@ export {
3928
5123
  dbFindAndConnect,
3929
5124
  dbInit,
3930
5125
  defaultFileSynopsisFunction,
5126
+ defaultTasksRegistry,
3931
5127
  defaultVersionSynopsisFunction,
5128
+ enqueueStopTask,
5129
+ enqueueTask,
5130
+ ensureTaskTables,
3932
5131
  getArgsInstance,
3933
5132
  createElement2 as h,
3934
5133
  joiEdateType,
@@ -3937,6 +5136,9 @@ export {
3937
5136
  listTables,
3938
5137
  load,
3939
5138
  organizeFooterMessages,
5139
+ queueToTableNames,
5140
+ runNodeTaskScript,
5141
+ runTasksLoop,
3940
5142
  setupContext,
3941
5143
  showListScreen,
3942
5144
  showMenuScreen,
@@ -3944,11 +5146,13 @@ export {
3944
5146
  showMultiColumnListWithPreviewScreen,
3945
5147
  showScreen,
3946
5148
  showWordGridScreen,
5149
+ updateTaskProgress,
3947
5150
  useCallback,
3948
5151
  useEffect3 as useEffect,
3949
5152
  useInput2 as useInput,
3950
5153
  useMemo,
3951
5154
  useRef3 as useRef,
3952
- useState3 as useState
5155
+ useState3 as useState,
5156
+ waitForTaskResult
3953
5157
  };
3954
5158
  //# sourceMappingURL=index.js.map