opencode-supertask 0.1.21 → 0.1.23

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/cli/index.js CHANGED
@@ -9734,6 +9734,23 @@ var init_task_service = __esm({
9734
9734
  ).returning();
9735
9735
  return result.length;
9736
9736
  }
9737
+ static async resetOrphanRunningToPending() {
9738
+ const result = await db.update(tasks2).set({
9739
+ status: "pending",
9740
+ startedAt: null,
9741
+ finishedAt: null
9742
+ }).where(
9743
+ and(
9744
+ eq(tasks2.status, "running"),
9745
+ sql`NOT EXISTS (
9746
+ SELECT 1 FROM ${taskRuns2}
9747
+ WHERE ${taskRuns2.taskId} = ${tasks2.id}
9748
+ AND ${taskRuns2.status} = 'running'
9749
+ )`
9750
+ )
9751
+ ).returning();
9752
+ return result.length;
9753
+ }
9737
9754
  static async cancel(id, scope = {}) {
9738
9755
  const conditions = [
9739
9756
  eq(tasks2.id, id),
@@ -17078,26 +17095,26 @@ var require_CronDate = __commonJS({
17078
17095
  * @param {CronDate | Date | number | string} [timestamp] - The timestamp to initialize the CronDate with.
17079
17096
  * @param {string} [tz] - The timezone to use for the CronDate.
17080
17097
  */
17081
- constructor(timestamp, tz) {
17098
+ constructor(timestamp2, tz) {
17082
17099
  const dateOpts = { zone: tz };
17083
- if (!timestamp) {
17100
+ if (!timestamp2) {
17084
17101
  this.#date = luxon_1.DateTime.local();
17085
- } else if (timestamp instanceof _CronDate) {
17086
- this.#date = timestamp.#date;
17087
- this.#dstStart = timestamp.#dstStart;
17088
- this.#dstEnd = timestamp.#dstEnd;
17089
- } else if (timestamp instanceof Date) {
17090
- this.#date = luxon_1.DateTime.fromJSDate(timestamp, dateOpts);
17091
- } else if (typeof timestamp === "number") {
17092
- this.#date = luxon_1.DateTime.fromMillis(timestamp, dateOpts);
17102
+ } else if (timestamp2 instanceof _CronDate) {
17103
+ this.#date = timestamp2.#date;
17104
+ this.#dstStart = timestamp2.#dstStart;
17105
+ this.#dstEnd = timestamp2.#dstEnd;
17106
+ } else if (timestamp2 instanceof Date) {
17107
+ this.#date = luxon_1.DateTime.fromJSDate(timestamp2, dateOpts);
17108
+ } else if (typeof timestamp2 === "number") {
17109
+ this.#date = luxon_1.DateTime.fromMillis(timestamp2, dateOpts);
17093
17110
  } else {
17094
- this.#date = luxon_1.DateTime.fromISO(timestamp, dateOpts);
17095
- this.#date.isValid || (this.#date = luxon_1.DateTime.fromRFC2822(timestamp, dateOpts));
17096
- this.#date.isValid || (this.#date = luxon_1.DateTime.fromSQL(timestamp, dateOpts));
17097
- this.#date.isValid || (this.#date = luxon_1.DateTime.fromFormat(timestamp, "EEE, d MMM yyyy HH:mm:ss", dateOpts));
17111
+ this.#date = luxon_1.DateTime.fromISO(timestamp2, dateOpts);
17112
+ this.#date.isValid || (this.#date = luxon_1.DateTime.fromRFC2822(timestamp2, dateOpts));
17113
+ this.#date.isValid || (this.#date = luxon_1.DateTime.fromSQL(timestamp2, dateOpts));
17114
+ this.#date.isValid || (this.#date = luxon_1.DateTime.fromFormat(timestamp2, "EEE, d MMM yyyy HH:mm:ss", dateOpts));
17098
17115
  }
17099
17116
  if (!this.#date.isValid) {
17100
- throw new Error(`CronDate: unhandled timestamp: ${timestamp}`);
17117
+ throw new Error(`CronDate: unhandled timestamp: ${timestamp2}`);
17101
17118
  }
17102
17119
  if (tz && tz !== this.#date.zoneName) {
17103
17120
  this.#date = this.#date.setZone(tz);
@@ -19155,6 +19172,412 @@ var init_task_template_service = __esm({
19155
19172
  }
19156
19173
  });
19157
19174
 
19175
+ // src/core/process-control.ts
19176
+ import { spawnSync } from "child_process";
19177
+ import { basename } from "path";
19178
+ function isSafePid(pid) {
19179
+ return Number.isInteger(pid) && pid > 1 && pid !== process.pid;
19180
+ }
19181
+ function isProcessAlive(pid) {
19182
+ if (!Number.isInteger(pid) || pid <= 0) return false;
19183
+ try {
19184
+ process.kill(pid, 0);
19185
+ return true;
19186
+ } catch (error) {
19187
+ return error instanceof Error && "code" in error && error.code === "EPERM";
19188
+ }
19189
+ }
19190
+ function inspectUnixProcess(pid) {
19191
+ const result = spawnSync("ps", ["-o", "pgid=", "-o", "command=", "-p", String(pid)], {
19192
+ encoding: "utf8",
19193
+ stdio: ["ignore", "pipe", "ignore"]
19194
+ });
19195
+ if (result.status !== 0) return null;
19196
+ const match2 = result.stdout.trim().match(/^(\d+)\s+(.+)$/s);
19197
+ if (!match2) return null;
19198
+ return { processGroupId: Number(match2[1]), command: match2[2] };
19199
+ }
19200
+ function commandMatches(command, expectedExecutable) {
19201
+ const expectedName = basename(expectedExecutable).trim().toLowerCase();
19202
+ if (!expectedName) return false;
19203
+ return command.toLowerCase().includes(expectedName);
19204
+ }
19205
+ function signalSpawnedProcessTree(pid, signal) {
19206
+ if (!isSafePid(pid)) return false;
19207
+ if (process.platform !== "win32") {
19208
+ try {
19209
+ process.kill(-pid, signal);
19210
+ return true;
19211
+ } catch {
19212
+ }
19213
+ }
19214
+ try {
19215
+ process.kill(pid, signal);
19216
+ return true;
19217
+ } catch {
19218
+ return false;
19219
+ }
19220
+ }
19221
+ function signalRecordedProcessTree(pid, signal, expectedExecutable) {
19222
+ if (!isSafePid(pid)) return false;
19223
+ if (process.platform === "win32") {
19224
+ const list = spawnSync("tasklist", ["/FI", `PID eq ${pid}`, "/FO", "CSV", "/NH"], {
19225
+ encoding: "utf8",
19226
+ stdio: ["ignore", "pipe", "ignore"]
19227
+ });
19228
+ if (list.status !== 0 || !commandMatches(list.stdout, expectedExecutable)) return false;
19229
+ const args = ["/PID", String(pid), "/T"];
19230
+ if (signal === "SIGKILL") args.push("/F");
19231
+ return spawnSync("taskkill", args, { stdio: "ignore" }).status === 0;
19232
+ }
19233
+ const info = inspectUnixProcess(pid);
19234
+ if (!info || !commandMatches(info.command, expectedExecutable)) return false;
19235
+ try {
19236
+ if (info.processGroupId === pid) process.kill(-pid, signal);
19237
+ else process.kill(pid, signal);
19238
+ return true;
19239
+ } catch {
19240
+ return false;
19241
+ }
19242
+ }
19243
+ var init_process_control = __esm({
19244
+ "src/core/process-control.ts"() {
19245
+ "use strict";
19246
+ }
19247
+ });
19248
+
19249
+ // src/core/services/database-maintenance.service.ts
19250
+ import { Database as Database3 } from "bun:sqlite";
19251
+ import { randomUUID } from "crypto";
19252
+ import {
19253
+ chmodSync,
19254
+ constants,
19255
+ copyFileSync,
19256
+ existsSync as existsSync2,
19257
+ mkdirSync as mkdirSync2,
19258
+ renameSync,
19259
+ statSync,
19260
+ unlinkSync,
19261
+ writeFileSync
19262
+ } from "fs";
19263
+ import { tmpdir } from "os";
19264
+ import { basename as basename2, dirname as dirname2, resolve } from "path";
19265
+ function timestamp() {
19266
+ return (/* @__PURE__ */ new Date()).toISOString().replace(/[-:]/g, "").replace(/\.\d{3}Z$/, "Z");
19267
+ }
19268
+ function normalizedPath(path) {
19269
+ return path === ":memory:" ? path : resolve(path);
19270
+ }
19271
+ function safeUnlink(path) {
19272
+ try {
19273
+ unlinkSync(path);
19274
+ } catch (error) {
19275
+ const code = error instanceof Error && "code" in error ? error.code : void 0;
19276
+ if (code !== "ENOENT") throw error;
19277
+ }
19278
+ }
19279
+ function errorMessage(error) {
19280
+ return error instanceof Error ? error.message : String(error);
19281
+ }
19282
+ var REQUIRED_TABLES, DatabaseMaintenanceConflictError, DatabaseMaintenanceService;
19283
+ var init_database_maintenance_service = __esm({
19284
+ "src/core/services/database-maintenance.service.ts"() {
19285
+ "use strict";
19286
+ init_db2();
19287
+ init_process_control();
19288
+ REQUIRED_TABLES = ["gateway_lock", "tasks", "task_runs", "task_templates"];
19289
+ DatabaseMaintenanceConflictError = class extends Error {
19290
+ constructor(message) {
19291
+ super(message);
19292
+ this.name = "DatabaseMaintenanceConflictError";
19293
+ }
19294
+ };
19295
+ DatabaseMaintenanceService = class {
19296
+ static check() {
19297
+ return this.inspect(getSqlite(), DB_FILE_PATH);
19298
+ }
19299
+ static backup(outputPath) {
19300
+ const sqlite2 = getSqlite();
19301
+ try {
19302
+ sqlite2.query("PRAGMA wal_checkpoint(PASSIVE)").get();
19303
+ } catch {
19304
+ }
19305
+ return this.writeSnapshot(sqlite2, outputPath ?? this.createBackupPath("backup"));
19306
+ }
19307
+ static clear(options = {}) {
19308
+ const sqlite2 = getSqlite();
19309
+ this.assertGatewaySafe(sqlite2, options.allowCurrentGateway ?? false);
19310
+ this.assertNoRunningWork(sqlite2);
19311
+ sqlite2.exec("BEGIN IMMEDIATE");
19312
+ let backup = null;
19313
+ try {
19314
+ this.assertGatewaySafe(sqlite2, options.allowCurrentGateway ?? false);
19315
+ this.assertNoRunningWork(sqlite2);
19316
+ const before = this.readCounts(sqlite2);
19317
+ backup = this.writeSnapshot(sqlite2, this.createBackupPath("pre-clear"));
19318
+ sqlite2.exec("DELETE FROM task_runs");
19319
+ sqlite2.exec("DELETE FROM tasks");
19320
+ sqlite2.exec("DELETE FROM task_templates");
19321
+ sqlite2.exec("COMMIT");
19322
+ return {
19323
+ backupPath: backup.path,
19324
+ deleted: before,
19325
+ check: this.inspect(sqlite2, DB_FILE_PATH)
19326
+ };
19327
+ } catch (error) {
19328
+ if (sqlite2.inTransaction) sqlite2.exec("ROLLBACK");
19329
+ if (error instanceof DatabaseMaintenanceConflictError) throw error;
19330
+ const backupHint = backup ? `\uFF1B\u4E8B\u52A1\u5DF2\u56DE\u6EDA\uFF0C\u5907\u4EFD\u4FDD\u7559\u5728 ${backup.path}` : "";
19331
+ throw new Error(`\u6E05\u7A7A\u6570\u636E\u5E93\u5931\u8D25${backupHint}\uFF1A${errorMessage(error)}`);
19332
+ }
19333
+ }
19334
+ static restore(sourcePath) {
19335
+ if (DB_FILE_PATH === ":memory:") {
19336
+ throw new Error("\u5185\u5B58\u6570\u636E\u5E93\u4E0D\u652F\u6301 restore");
19337
+ }
19338
+ const source = normalizedPath(sourcePath);
19339
+ const livePath = normalizedPath(DB_FILE_PATH);
19340
+ if (source === livePath) throw new Error("\u6062\u590D\u6765\u6E90\u4E0D\u80FD\u662F\u5F53\u524D\u6570\u636E\u5E93\u6587\u4EF6");
19341
+ const current = getSqlite();
19342
+ this.assertGatewaySafe(current, false);
19343
+ this.assertNoRunningWork(current);
19344
+ if (!existsSync2(source) || !statSync(source).isFile()) {
19345
+ throw new Error(`\u5907\u4EFD\u6587\u4EF6\u4E0D\u5B58\u5728\uFF1A${source}`);
19346
+ }
19347
+ let sourceCheck;
19348
+ try {
19349
+ sourceCheck = this.inspectFile(source);
19350
+ } catch (error) {
19351
+ throw new Error(`\u5907\u4EFD\u6587\u4EF6\u65E0\u6548\uFF1A${errorMessage(error)}`);
19352
+ }
19353
+ if (!sourceCheck.ok) {
19354
+ throw new Error(`\u5907\u4EFD\u6587\u4EF6\u6821\u9A8C\u5931\u8D25\uFF1A${sourceCheck.integrityMessages.join("; ") || "schema/foreign key error"}`);
19355
+ }
19356
+ const stagePath = `${livePath}.restore-${process.pid}-${randomUUID()}.tmp`;
19357
+ const rollbackPath = `${livePath}.rollback-${process.pid}-${randomUUID()}.tmp`;
19358
+ let safetyBackup = null;
19359
+ let liveMoved = false;
19360
+ try {
19361
+ copyFileSync(source, stagePath, constants.COPYFILE_EXCL);
19362
+ chmodSync(stagePath, 384);
19363
+ const staged = new Database3(stagePath);
19364
+ try {
19365
+ const stagedCheck = this.inspect(staged, stagePath);
19366
+ if (!stagedCheck.ok) throw new Error("\u6682\u5B58\u6062\u590D\u6587\u4EF6\u6821\u9A8C\u5931\u8D25");
19367
+ const now = Date.now();
19368
+ staged.query(
19369
+ "INSERT OR REPLACE INTO gateway_lock (id, pid, acquired_at, heartbeat_at, ready_at) VALUES (1, ?, ?, ?, NULL)"
19370
+ ).run(process.pid, now, now);
19371
+ } finally {
19372
+ staged.close();
19373
+ }
19374
+ const sqlite2 = getSqlite();
19375
+ sqlite2.exec("BEGIN IMMEDIATE");
19376
+ try {
19377
+ this.assertGatewaySafe(sqlite2, false);
19378
+ this.assertNoRunningWork(sqlite2);
19379
+ safetyBackup = this.writeSnapshot(sqlite2, this.createBackupPath("pre-restore"));
19380
+ const now = Date.now();
19381
+ sqlite2.query(
19382
+ "INSERT OR REPLACE INTO gateway_lock (id, pid, acquired_at, heartbeat_at, ready_at) VALUES (1, ?, ?, ?, NULL)"
19383
+ ).run(process.pid, now, now);
19384
+ sqlite2.exec("COMMIT");
19385
+ } catch (error) {
19386
+ if (sqlite2.inTransaction) sqlite2.exec("ROLLBACK");
19387
+ throw error;
19388
+ }
19389
+ const checkpoint = sqlite2.query("PRAGMA wal_checkpoint(TRUNCATE)").get();
19390
+ if (checkpoint && checkpoint.busy !== 0) {
19391
+ throw new DatabaseMaintenanceConflictError("\u6570\u636E\u5E93\u4ECD\u88AB\u5176\u4ED6\u8FDE\u63A5\u5360\u7528\uFF0C\u65E0\u6CD5\u5B89\u5168\u6062\u590D");
19392
+ }
19393
+ closeDb();
19394
+ safeUnlink(`${livePath}-wal`);
19395
+ safeUnlink(`${livePath}-shm`);
19396
+ renameSync(livePath, rollbackPath);
19397
+ liveMoved = true;
19398
+ renameSync(stagePath, livePath);
19399
+ const restored = getSqlite();
19400
+ restored.exec("BEGIN IMMEDIATE");
19401
+ let recoveredRunningTasks = 0;
19402
+ let closedRunningRuns = 0;
19403
+ try {
19404
+ recoveredRunningTasks = this.scalar(
19405
+ restored,
19406
+ "SELECT COUNT(*) AS count FROM tasks WHERE status = 'running'"
19407
+ );
19408
+ closedRunningRuns = this.scalar(
19409
+ restored,
19410
+ "SELECT COUNT(*) AS count FROM task_runs WHERE status = 'running'"
19411
+ );
19412
+ const finishedAt = Math.floor(Date.now() / 1e3);
19413
+ restored.query(`
19414
+ UPDATE task_runs
19415
+ SET status = 'failed', finished_at = ?,
19416
+ log = CASE
19417
+ WHEN log IS NULL OR log = '' THEN '\u6570\u636E\u5E93\u6062\u590D\u65F6\u5173\u95ED\u9057\u7559\u8FD0\u884C\u8BB0\u5F55'
19418
+ ELSE log || '
19419
+ \u6570\u636E\u5E93\u6062\u590D\u65F6\u5173\u95ED\u9057\u7559\u8FD0\u884C\u8BB0\u5F55'
19420
+ END
19421
+ WHERE status = 'running'
19422
+ `).run(finishedAt);
19423
+ restored.exec(`
19424
+ UPDATE tasks
19425
+ SET status = 'pending', started_at = NULL, finished_at = NULL
19426
+ WHERE status = 'running'
19427
+ `);
19428
+ restored.query("DELETE FROM gateway_lock WHERE pid = ?").run(process.pid);
19429
+ restored.exec("COMMIT");
19430
+ } catch (error) {
19431
+ if (restored.inTransaction) restored.exec("ROLLBACK");
19432
+ throw error;
19433
+ }
19434
+ const check = this.inspect(restored, livePath);
19435
+ if (!check.ok) throw new Error("\u6062\u590D\u540E\u7684\u6570\u636E\u5E93\u672A\u901A\u8FC7\u5B8C\u6574\u6027\u6821\u9A8C");
19436
+ safeUnlink(rollbackPath);
19437
+ return {
19438
+ sourcePath: source,
19439
+ safetyBackupPath: safetyBackup.path,
19440
+ recoveredRunningTasks,
19441
+ closedRunningRuns,
19442
+ check
19443
+ };
19444
+ } catch (error) {
19445
+ closeDb();
19446
+ safeUnlink(`${livePath}-wal`);
19447
+ safeUnlink(`${livePath}-shm`);
19448
+ safeUnlink(stagePath);
19449
+ safeUnlink(`${stagePath}-journal`);
19450
+ safeUnlink(`${stagePath}-wal`);
19451
+ safeUnlink(`${stagePath}-shm`);
19452
+ if (liveMoved && existsSync2(rollbackPath)) {
19453
+ safeUnlink(livePath);
19454
+ renameSync(rollbackPath, livePath);
19455
+ try {
19456
+ const original = getSqlite();
19457
+ original.query("DELETE FROM gateway_lock WHERE pid = ?").run(process.pid);
19458
+ } catch {
19459
+ }
19460
+ } else if (existsSync2(rollbackPath)) {
19461
+ safeUnlink(rollbackPath);
19462
+ } else {
19463
+ try {
19464
+ getSqlite().query("DELETE FROM gateway_lock WHERE pid = ?").run(process.pid);
19465
+ } catch {
19466
+ }
19467
+ }
19468
+ const backupHint = safetyBackup ? `\uFF1B\u5F53\u524D\u5E93\u5B89\u5168\u5907\u4EFD\uFF1A${safetyBackup.path}` : "";
19469
+ throw new Error(`\u6062\u590D\u6570\u636E\u5E93\u5931\u8D25\uFF0C\u5DF2\u5C1D\u8BD5\u56DE\u6EDA${backupHint}\uFF1A${errorMessage(error)}`);
19470
+ }
19471
+ }
19472
+ static inspectFile(path) {
19473
+ let sqlite2;
19474
+ try {
19475
+ sqlite2 = new Database3(path, { readonly: true, strict: true });
19476
+ } catch (error) {
19477
+ throw new Error(`\u65E0\u6CD5\u6253\u5F00\u5907\u4EFD\u6587\u4EF6\uFF1A${errorMessage(error)}`);
19478
+ }
19479
+ try {
19480
+ return this.inspect(sqlite2, path);
19481
+ } finally {
19482
+ sqlite2.close();
19483
+ }
19484
+ }
19485
+ static inspect(sqlite2, path) {
19486
+ const tableRows = sqlite2.query(
19487
+ "SELECT name FROM sqlite_master WHERE type = 'table'"
19488
+ ).all();
19489
+ const tableNames = new Set(tableRows.map((row) => row.name));
19490
+ const missingTables = REQUIRED_TABLES.filter((table) => !tableNames.has(table));
19491
+ const integrityRows = sqlite2.query("PRAGMA integrity_check").all();
19492
+ const integrityMessages = integrityRows.map((row) => row.integrity_check);
19493
+ const foreignKeyViolations = sqlite2.query("PRAGMA foreign_key_check").all().length;
19494
+ const journal = sqlite2.query("PRAGMA journal_mode").get();
19495
+ const counts = missingTables.length === 0 ? this.readCounts(sqlite2) : { tasks: 0, taskRuns: 0, taskTemplates: 0 };
19496
+ const runningTasks = missingTables.includes("tasks") ? 0 : this.scalar(sqlite2, "SELECT COUNT(*) AS count FROM tasks WHERE status = 'running'");
19497
+ const runningRuns = missingTables.includes("task_runs") ? 0 : this.scalar(sqlite2, "SELECT COUNT(*) AS count FROM task_runs WHERE status = 'running'");
19498
+ const sizeBytes = path === ":memory:" || !existsSync2(path) ? sqlite2.serialize().byteLength : statSync(path).size;
19499
+ return {
19500
+ ok: integrityMessages.length === 1 && integrityMessages[0] === "ok" && foreignKeyViolations === 0 && missingTables.length === 0,
19501
+ path: normalizedPath(path),
19502
+ sizeBytes,
19503
+ journalMode: journal?.journal_mode ?? "unknown",
19504
+ integrityMessages,
19505
+ foreignKeyViolations,
19506
+ missingTables,
19507
+ counts,
19508
+ runningTasks,
19509
+ runningRuns
19510
+ };
19511
+ }
19512
+ static writeSnapshot(sqlite2, outputPath) {
19513
+ const liveCheck = this.inspect(sqlite2, DB_FILE_PATH);
19514
+ if (!liveCheck.ok) throw new Error("\u5F53\u524D\u6570\u636E\u5E93\u672A\u901A\u8FC7\u5B8C\u6574\u6027\u6821\u9A8C\uFF0C\u5DF2\u62D2\u7EDD\u521B\u5EFA\u5907\u4EFD");
19515
+ const output = normalizedPath(outputPath);
19516
+ if (DB_FILE_PATH !== ":memory:" && output === normalizedPath(DB_FILE_PATH)) {
19517
+ throw new Error("\u5907\u4EFD\u8DEF\u5F84\u4E0D\u80FD\u8986\u76D6\u5F53\u524D\u6570\u636E\u5E93");
19518
+ }
19519
+ if (existsSync2(output)) throw new Error(`\u5907\u4EFD\u6587\u4EF6\u5DF2\u5B58\u5728\uFF1A${output}`);
19520
+ mkdirSync2(dirname2(output), { recursive: true });
19521
+ const temporary = `${output}.tmp-${process.pid}-${randomUUID()}`;
19522
+ try {
19523
+ writeFileSync(temporary, sqlite2.serialize(), { flag: "wx", mode: 384 });
19524
+ const standalone = new Database3(temporary, { readwrite: true, create: false });
19525
+ try {
19526
+ standalone.query("PRAGMA journal_mode = DELETE").get();
19527
+ } finally {
19528
+ standalone.close();
19529
+ }
19530
+ safeUnlink(`${temporary}-wal`);
19531
+ safeUnlink(`${temporary}-shm`);
19532
+ const check = this.inspectFile(temporary);
19533
+ if (!check.ok) throw new Error("\u65B0\u5907\u4EFD\u672A\u901A\u8FC7\u5B8C\u6574\u6027\u6821\u9A8C");
19534
+ renameSync(temporary, output);
19535
+ const finalCheck = { ...check, path: output, sizeBytes: statSync(output).size };
19536
+ return { path: output, sizeBytes: finalCheck.sizeBytes, check: finalCheck };
19537
+ } catch (error) {
19538
+ safeUnlink(temporary);
19539
+ safeUnlink(`${temporary}-wal`);
19540
+ safeUnlink(`${temporary}-shm`);
19541
+ throw error;
19542
+ }
19543
+ }
19544
+ static createBackupPath(kind) {
19545
+ const directory = DB_FILE_PATH === ":memory:" ? tmpdir() : dirname2(normalizedPath(DB_FILE_PATH));
19546
+ const base = DB_FILE_PATH === ":memory:" ? "supertask-memory" : basename2(DB_FILE_PATH, ".db");
19547
+ return resolve(directory, `${base}.${kind}-${timestamp()}-${randomUUID().slice(0, 8)}.db`);
19548
+ }
19549
+ static readCounts(sqlite2) {
19550
+ return {
19551
+ tasks: this.scalar(sqlite2, "SELECT COUNT(*) AS count FROM tasks"),
19552
+ taskRuns: this.scalar(sqlite2, "SELECT COUNT(*) AS count FROM task_runs"),
19553
+ taskTemplates: this.scalar(sqlite2, "SELECT COUNT(*) AS count FROM task_templates")
19554
+ };
19555
+ }
19556
+ static scalar(sqlite2, statement) {
19557
+ const row = sqlite2.query(statement).get();
19558
+ return Number(row?.count ?? 0);
19559
+ }
19560
+ static assertGatewaySafe(sqlite2, allowCurrentGateway) {
19561
+ const lock = sqlite2.query("SELECT pid FROM gateway_lock WHERE id = 1").get();
19562
+ if (!lock || !isProcessAlive(lock.pid)) return;
19563
+ if (allowCurrentGateway && lock.pid === process.pid) return;
19564
+ throw new DatabaseMaintenanceConflictError(
19565
+ `Gateway PID ${lock.pid} \u4ECD\u5728\u8FD0\u884C\uFF0C\u8BF7\u5148\u6267\u884C pm2 stop supertask-gateway`
19566
+ );
19567
+ }
19568
+ static assertNoRunningWork(sqlite2) {
19569
+ const runningTasks = this.scalar(sqlite2, "SELECT COUNT(*) AS count FROM tasks WHERE status = 'running'");
19570
+ const runningRuns = this.scalar(sqlite2, "SELECT COUNT(*) AS count FROM task_runs WHERE status = 'running'");
19571
+ if (runningTasks > 0 || runningRuns > 0) {
19572
+ throw new DatabaseMaintenanceConflictError(
19573
+ `\u5B58\u5728\u8FD0\u884C\u4E2D\u4EFB\u52A1\uFF08tasks=${runningTasks}, task_runs=${runningRuns}\uFF09\uFF0C\u5DF2\u62D2\u7EDD\u5371\u9669\u64CD\u4F5C`
19574
+ );
19575
+ }
19576
+ }
19577
+ };
19578
+ }
19579
+ });
19580
+
19158
19581
  // src/gateway/config.ts
19159
19582
  var config_exports = {};
19160
19583
  __export(config_exports, {
@@ -19164,7 +19587,7 @@ __export(config_exports, {
19164
19587
  loadConfig: () => loadConfig,
19165
19588
  validateConfig: () => validateConfig
19166
19589
  });
19167
- import { existsSync as existsSync2, readFileSync } from "fs";
19590
+ import { existsSync as existsSync3, readFileSync } from "fs";
19168
19591
  import { homedir as homedir2 } from "os";
19169
19592
  import { join as join2 } from "path";
19170
19593
  function getConfigPath() {
@@ -19244,7 +19667,7 @@ function validateConfig(input) {
19244
19667
  };
19245
19668
  }
19246
19669
  function loadConfig(path = getConfigPath()) {
19247
- if (!existsSync2(path)) return validateConfig({ configVersion: 2 });
19670
+ if (!existsSync3(path)) return validateConfig({ configVersion: 2 });
19248
19671
  try {
19249
19672
  return validateConfig(JSON.parse(readFileSync(path, "utf-8")));
19250
19673
  } catch (error) {
@@ -19290,6 +19713,7 @@ __export(pm2_exports, {
19290
19713
  ensureGateway: () => ensureGateway,
19291
19714
  getPackageVersion: () => getPackageVersion,
19292
19715
  install: () => install,
19716
+ installMacLaunchAgent: () => installMacLaunchAgent,
19293
19717
  isGatewayReady: () => isGatewayReady,
19294
19718
  isGatewayRunning: () => isGatewayRunning,
19295
19719
  isPm2Installed: () => isPm2Installed,
@@ -19297,12 +19721,12 @@ __export(pm2_exports, {
19297
19721
  uninstall: () => uninstall,
19298
19722
  upgrade: () => upgrade
19299
19723
  });
19300
- import { execSync, spawnSync } from "child_process";
19301
- import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync } from "fs";
19724
+ import { execSync, spawnSync as spawnSync2 } from "child_process";
19725
+ import { chmodSync as chmodSync2, existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
19302
19726
  import { homedir as homedir3 } from "os";
19303
- import { dirname as dirname2, join as join3 } from "path";
19727
+ import { dirname as dirname3, isAbsolute, join as join3 } from "path";
19304
19728
  import { fileURLToPath as fileURLToPath2 } from "url";
19305
- import { Database as Database3 } from "bun:sqlite";
19729
+ import { Database as Database4 } from "bun:sqlite";
19306
19730
  function getPackageVersion() {
19307
19731
  const envVersion = process.env.npm_package_version;
19308
19732
  if (envVersion) return envVersion;
@@ -19317,14 +19741,14 @@ function getPackageVersion() {
19317
19741
  function resolveGatewayEntry() {
19318
19742
  const override = process.env.SUPERTASK_GATEWAY_ENTRY;
19319
19743
  if (override) {
19320
- if (!existsSync3(override)) throw new Error(`[supertask] Gateway entry not found: ${override}`);
19744
+ if (!existsSync4(override)) throw new Error(`[supertask] Gateway entry not found: ${override}`);
19321
19745
  return override;
19322
19746
  }
19323
19747
  const candidates = [
19324
19748
  join3(__dirname, "../gateway/index.js"),
19325
19749
  join3(__dirname, "../gateway/index.ts")
19326
19750
  ];
19327
- const entry = candidates.find((candidate) => existsSync3(candidate));
19751
+ const entry = candidates.find((candidate) => existsSync4(candidate));
19328
19752
  if (!entry) throw new Error(`[supertask] Gateway entry not found. Checked: ${candidates.join(", ")}`);
19329
19753
  return entry;
19330
19754
  }
@@ -19334,7 +19758,7 @@ function versionFile() {
19334
19758
  function getRunningVersion() {
19335
19759
  try {
19336
19760
  const path = versionFile();
19337
- if (!existsSync3(path)) return null;
19761
+ if (!existsSync4(path)) return null;
19338
19762
  return readFileSync2(path, "utf-8").trim() || null;
19339
19763
  } catch {
19340
19764
  return null;
@@ -19342,14 +19766,91 @@ function getRunningVersion() {
19342
19766
  }
19343
19767
  function writeRunningVersion(version2) {
19344
19768
  const path = versionFile();
19345
- mkdirSync2(dirname2(path), { recursive: true });
19346
- writeFileSync(path, version2, "utf-8");
19769
+ mkdirSync3(dirname3(path), { recursive: true });
19770
+ writeFileSync2(path, version2, "utf-8");
19347
19771
  }
19348
19772
  function pm2Bin() {
19349
19773
  return process.env.SUPERTASK_PM2_BIN ?? (process.platform === "win32" ? "pm2.cmd" : "pm2");
19350
19774
  }
19775
+ function xmlEscape(value) {
19776
+ return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&apos;");
19777
+ }
19778
+ function resolvePm2Bin() {
19779
+ const configured = pm2Bin();
19780
+ if (isAbsolute(configured)) return configured;
19781
+ const result = spawnSync2("which", [configured], { encoding: "utf8" });
19782
+ const resolved = result.status === 0 ? result.stdout.trim().split("\n")[0] : "";
19783
+ if (!resolved) throw new Error(`[supertask] \u65E0\u6CD5\u89E3\u6790 pm2 \u53EF\u6267\u884C\u6587\u4EF6: ${configured}`);
19784
+ return resolved;
19785
+ }
19786
+ function launchAgentPath() {
19787
+ return process.env.SUPERTASK_LAUNCH_AGENT_PATH ?? join3(homedir3(), "Library/LaunchAgents", `${MAC_LAUNCH_AGENT_LABEL}.plist`);
19788
+ }
19789
+ function launchctlBin() {
19790
+ return process.env.SUPERTASK_LAUNCHCTL_BIN ?? "launchctl";
19791
+ }
19792
+ function installMacLaunchAgent() {
19793
+ if (typeof process.getuid !== "function") {
19794
+ throw new Error("[supertask] \u5F53\u524D\u8FD0\u884C\u65F6\u65E0\u6CD5\u83B7\u53D6 macOS \u7528\u6237 ID");
19795
+ }
19796
+ const path = launchAgentPath();
19797
+ const home = homedir3();
19798
+ const pm2Home = process.env.PM2_HOME ?? join3(home, ".pm2");
19799
+ const environmentPath = process.env.PATH ?? "/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin";
19800
+ const plist = `<?xml version="1.0" encoding="UTF-8"?>
19801
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
19802
+ <plist version="1.0">
19803
+ <dict>
19804
+ <key>Label</key>
19805
+ <string>${MAC_LAUNCH_AGENT_LABEL}</string>
19806
+ <key>ProgramArguments</key>
19807
+ <array>
19808
+ <string>${xmlEscape(resolvePm2Bin())}</string>
19809
+ <string>resurrect</string>
19810
+ </array>
19811
+ <key>RunAtLoad</key>
19812
+ <true/>
19813
+ <key>KeepAlive</key>
19814
+ <dict>
19815
+ <key>SuccessfulExit</key>
19816
+ <false/>
19817
+ </dict>
19818
+ <key>ThrottleInterval</key>
19819
+ <integer>10</integer>
19820
+ <key>EnvironmentVariables</key>
19821
+ <dict>
19822
+ <key>HOME</key>
19823
+ <string>${xmlEscape(home)}</string>
19824
+ <key>PATH</key>
19825
+ <string>${xmlEscape(environmentPath)}</string>
19826
+ <key>PM2_HOME</key>
19827
+ <string>${xmlEscape(pm2Home)}</string>
19828
+ </dict>
19829
+ <key>StandardErrorPath</key>
19830
+ <string>${xmlEscape(join3(pm2Home, "supertask-launchd-error.log"))}</string>
19831
+ <key>StandardOutPath</key>
19832
+ <string>${xmlEscape(join3(pm2Home, "supertask-launchd-output.log"))}</string>
19833
+ </dict>
19834
+ </plist>
19835
+ `;
19836
+ mkdirSync3(dirname3(path), { recursive: true });
19837
+ writeFileSync2(path, plist, { mode: 384 });
19838
+ chmodSync2(path, 384);
19839
+ const domain = `gui/${process.getuid()}`;
19840
+ spawnSync2(launchctlBin(), ["bootout", `${domain}/${MAC_LAUNCH_AGENT_LABEL}`], {
19841
+ stdio: "ignore"
19842
+ });
19843
+ const loaded = spawnSync2(launchctlBin(), ["bootstrap", domain, path], {
19844
+ encoding: "utf8"
19845
+ });
19846
+ if (loaded.status !== 0) {
19847
+ const output = `${loaded.stdout ?? ""}${loaded.stderr ?? ""}`.trim();
19848
+ throw new Error(`[supertask] macOS LaunchAgent \u52A0\u8F7D\u5931\u8D25: ${output || `\u9000\u51FA\u7801 ${loaded.status}`}`);
19849
+ }
19850
+ return path;
19851
+ }
19351
19852
  function isPm2Installed() {
19352
- const result = spawnSync(pm2Bin(), ["--version"], {
19853
+ const result = spawnSync2(pm2Bin(), ["--version"], {
19353
19854
  stdio: "ignore",
19354
19855
  shell: process.platform === "win32"
19355
19856
  });
@@ -19370,7 +19871,7 @@ function installPm2() {
19370
19871
  }
19371
19872
  }
19372
19873
  function pm2Exec(args) {
19373
- const result = spawnSync(pm2Bin(), args, {
19874
+ const result = spawnSync2(pm2Bin(), args, {
19374
19875
  stdio: ["pipe", "pipe", "pipe"],
19375
19876
  encoding: "utf-8",
19376
19877
  env: process.env,
@@ -19406,10 +19907,10 @@ function databasePath() {
19406
19907
  }
19407
19908
  function isGatewayReady(expectedPid) {
19408
19909
  const path = databasePath();
19409
- if (!existsSync3(path)) return false;
19910
+ if (!existsSync4(path)) return false;
19410
19911
  let database = null;
19411
19912
  try {
19412
- database = new Database3(path, { readonly: true });
19913
+ database = new Database4(path, { readonly: true });
19413
19914
  const row = database.query(
19414
19915
  "SELECT pid, heartbeat_at, ready_at FROM gateway_lock WHERE id = 1"
19415
19916
  ).get();
@@ -19486,10 +19987,19 @@ function install() {
19486
19987
  pm2StartGateway();
19487
19988
  writeRunningVersion(version2);
19488
19989
  savePm2State();
19489
- const startup = pm2Exec(["startup"]);
19490
- if (startup.output) console.log(startup.output);
19491
- if (!startup.ok) {
19492
- console.warn("[supertask] pm2 startup \u672A\u5B8C\u6210\uFF1B\u8BF7\u6309 pm2 \u8F93\u51FA\u6267\u884C\u9700\u8981\u7BA1\u7406\u5458\u6743\u9650\u7684\u547D\u4EE4\uFF0C\u7136\u540E\u8FD0\u884C `pm2 save`\u3002");
19990
+ if (process.platform === "darwin") {
19991
+ try {
19992
+ const path = installMacLaunchAgent();
19993
+ console.log(`[supertask] macOS LaunchAgent installed: ${path}`);
19994
+ } catch (error) {
19995
+ console.warn(error instanceof Error ? error.message : String(error));
19996
+ }
19997
+ } else {
19998
+ const startup = pm2Exec(["startup"]);
19999
+ if (startup.output) console.log(startup.output);
20000
+ if (!startup.ok) {
20001
+ console.warn("[supertask] pm2 startup \u672A\u5B8C\u6210\uFF1B\u8BF7\u6309 pm2 \u8F93\u51FA\u6267\u884C\u9700\u8981\u7BA1\u7406\u5458\u6743\u9650\u7684\u547D\u4EE4\uFF0C\u7136\u540E\u8FD0\u884C `pm2 save`\u3002");
20002
+ }
19493
20003
  }
19494
20004
  console.log("[supertask] Gateway installed and running.");
19495
20005
  console.log("[supertask] Manage with: pm2 status / pm2 logs supertask-gateway");
@@ -19547,13 +20057,14 @@ function ensureGateway() {
19547
20057
  savePm2State();
19548
20058
  return { ok: true, action: existing ? "restarted" : "started" };
19549
20059
  }
19550
- var __dirname, PROCESS_NAME, GATEWAY_LOCK_STALE_MS;
20060
+ var __dirname, PROCESS_NAME, MAC_LAUNCH_AGENT_LABEL, GATEWAY_LOCK_STALE_MS;
19551
20061
  var init_pm2 = __esm({
19552
20062
  "src/daemon/pm2.ts"() {
19553
20063
  "use strict";
19554
20064
  init_config();
19555
- __dirname = dirname2(fileURLToPath2(import.meta.url));
20065
+ __dirname = dirname3(fileURLToPath2(import.meta.url));
19556
20066
  PROCESS_NAME = "supertask-gateway";
20067
+ MAC_LAUNCH_AGENT_LABEL = "com.supertask.pm2-resurrect";
19557
20068
  GATEWAY_LOCK_STALE_MS = 3e4;
19558
20069
  }
19559
20070
  });
@@ -19762,71 +20273,6 @@ var init_health = __esm({
19762
20273
  }
19763
20274
  });
19764
20275
 
19765
- // src/core/process-control.ts
19766
- import { spawnSync as spawnSync2 } from "child_process";
19767
- import { basename } from "path";
19768
- function isSafePid(pid) {
19769
- return Number.isInteger(pid) && pid > 1 && pid !== process.pid;
19770
- }
19771
- function inspectUnixProcess(pid) {
19772
- const result = spawnSync2("ps", ["-o", "pgid=", "-o", "command=", "-p", String(pid)], {
19773
- encoding: "utf8",
19774
- stdio: ["ignore", "pipe", "ignore"]
19775
- });
19776
- if (result.status !== 0) return null;
19777
- const match2 = result.stdout.trim().match(/^(\d+)\s+(.+)$/s);
19778
- if (!match2) return null;
19779
- return { processGroupId: Number(match2[1]), command: match2[2] };
19780
- }
19781
- function commandMatches(command, expectedExecutable) {
19782
- const expectedName = basename(expectedExecutable).trim().toLowerCase();
19783
- if (!expectedName) return false;
19784
- return command.toLowerCase().includes(expectedName);
19785
- }
19786
- function signalSpawnedProcessTree(pid, signal) {
19787
- if (!isSafePid(pid)) return false;
19788
- if (process.platform !== "win32") {
19789
- try {
19790
- process.kill(-pid, signal);
19791
- return true;
19792
- } catch {
19793
- }
19794
- }
19795
- try {
19796
- process.kill(pid, signal);
19797
- return true;
19798
- } catch {
19799
- return false;
19800
- }
19801
- }
19802
- function signalRecordedProcessTree(pid, signal, expectedExecutable) {
19803
- if (!isSafePid(pid)) return false;
19804
- if (process.platform === "win32") {
19805
- const list = spawnSync2("tasklist", ["/FI", `PID eq ${pid}`, "/FO", "CSV", "/NH"], {
19806
- encoding: "utf8",
19807
- stdio: ["ignore", "pipe", "ignore"]
19808
- });
19809
- if (list.status !== 0 || !commandMatches(list.stdout, expectedExecutable)) return false;
19810
- const args = ["/PID", String(pid), "/T"];
19811
- if (signal === "SIGKILL") args.push("/F");
19812
- return spawnSync2("taskkill", args, { stdio: "ignore" }).status === 0;
19813
- }
19814
- const info = inspectUnixProcess(pid);
19815
- if (!info || !commandMatches(info.command, expectedExecutable)) return false;
19816
- try {
19817
- if (info.processGroupId === pid) process.kill(-pid, signal);
19818
- else process.kill(pid, signal);
19819
- return true;
19820
- } catch {
19821
- return false;
19822
- }
19823
- }
19824
- var init_process_control = __esm({
19825
- "src/core/process-control.ts"() {
19826
- "use strict";
19827
- }
19828
- });
19829
-
19830
20276
  // src/worker/index.ts
19831
20277
  import { spawn } from "child_process";
19832
20278
  var DEFAULT_MAX_OUTPUT_CHARS, FORBIDDEN_AGENT, WorkerEngine;
@@ -20099,14 +20545,14 @@ ${output}` : ""}`;
20099
20545
  if (entry.child.exitCode !== null || entry.child.signalCode !== null) {
20100
20546
  return Promise.resolve();
20101
20547
  }
20102
- return new Promise((resolve) => {
20548
+ return new Promise((resolve2) => {
20103
20549
  const timeout = setTimeout(() => {
20104
20550
  this.signalEntry(entry, "SIGKILL");
20105
- resolve();
20551
+ resolve2();
20106
20552
  }, 5e3);
20107
20553
  entry.child.once("close", () => {
20108
20554
  clearTimeout(timeout);
20109
- resolve();
20555
+ resolve2();
20110
20556
  });
20111
20557
  this.signalEntry(entry, "SIGTERM");
20112
20558
  });
@@ -22808,8 +23254,8 @@ __export(web_exports, {
22808
23254
  dashboardApp: () => dashboardApp,
22809
23255
  default: () => web_default
22810
23256
  });
22811
- import { existsSync as existsSync4, readFileSync as readFileSync3, writeFileSync as writeFileSync2, mkdirSync as mkdirSync3, renameSync } from "fs";
22812
- import { dirname as dirname3 } from "path";
23257
+ import { existsSync as existsSync5, readFileSync as readFileSync3, writeFileSync as writeFileSync3, mkdirSync as mkdirSync4, renameSync as renameSync2 } from "fs";
23258
+ import { dirname as dirname4 } from "path";
22813
23259
  function parsePositiveInteger(value) {
22814
23260
  if (!/^\d+$/.test(value)) return null;
22815
23261
  const parsed = Number(value);
@@ -22859,7 +23305,7 @@ function esc(s) {
22859
23305
  }
22860
23306
  function readCurrentConfig() {
22861
23307
  const configPath = getConfigPath();
22862
- if (!existsSync4(configPath)) return {};
23308
+ if (!existsSync5(configPath)) return {};
22863
23309
  try {
22864
23310
  return JSON.parse(readFileSync3(configPath, "utf-8"));
22865
23311
  } catch {
@@ -22868,11 +23314,11 @@ function readCurrentConfig() {
22868
23314
  }
22869
23315
  function writeConfig(cfg) {
22870
23316
  const configPath = getConfigPath();
22871
- const dir = dirname3(configPath);
22872
- if (!existsSync4(dir)) mkdirSync3(dir, { recursive: true });
23317
+ const dir = dirname4(configPath);
23318
+ if (!existsSync5(dir)) mkdirSync4(dir, { recursive: true });
22873
23319
  const tempPath = `${configPath}.${process.pid}.tmp`;
22874
- writeFileSync2(tempPath, JSON.stringify(cfg, null, 2) + "\n", { mode: 384 });
22875
- renameSync(tempPath, configPath);
23320
+ writeFileSync3(tempPath, JSON.stringify(cfg, null, 2) + "\n", { mode: 384 });
23321
+ renameSync2(tempPath, configPath);
22876
23322
  }
22877
23323
  function renderLayout(title, activeTab, body) {
22878
23324
  return `<!DOCTYPE html><html><head><meta charset="UTF-8"><title>${title} - SuperTask</title>${SHARED_STYLES}
@@ -22889,12 +23335,12 @@ async function triggerTmpl(id){if(!confirm('\u7ACB\u5373\u89E6\u53D1\u4E00\u6B21
22889
23335
  function toggleLog(id){const el=document.getElementById('log-'+id);el.style.display=el.style.display==='none'?'block':'none';}
22890
23336
 
22891
23337
  async function clearDatabase(){
22892
- if(!confirm('\u786E\u5B9A\u6E05\u7A7A\u6240\u6709\u4EFB\u52A1\u6570\u636E\uFF1F\u6B64\u64CD\u4F5C\u4E0D\u53EF\u6062\u590D\uFF01'))return;
22893
- if(!confirm('\u518D\u6B21\u786E\u8BA4\uFF1A\u5C06\u5220\u9664\u6240\u6709\u4EFB\u52A1\u3001\u6267\u884C\u8BB0\u5F55\u548C\u8C03\u5EA6\u6A21\u677F\u3002'))return;
23338
+ if(!confirm('\u786E\u5B9A\u6E05\u7A7A\u6240\u6709\u4EFB\u52A1\u6570\u636E\uFF1F\u7CFB\u7EDF\u4F1A\u5148\u81EA\u52A8\u521B\u5EFA\u5907\u4EFD\u3002'))return;
23339
+ if(!confirm('\u518D\u6B21\u786E\u8BA4\uFF1A\u5C06\u4E8B\u52A1\u6027\u5220\u9664\u6240\u6709\u4EFB\u52A1\u3001\u6267\u884C\u8BB0\u5F55\u548C\u8C03\u5EA6\u6A21\u677F\u3002'))return;
22894
23340
  try{
22895
- const r=await fetch('/api/database/clear',{method:'POST'});
23341
+ const r=await fetch('/api/database/clear',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({confirmation:'CLEAR'})});
22896
23342
  const d=await r.json();
22897
- if(d.success){alert('\u6570\u636E\u5E93\u5DF2\u6E05\u7A7A');location.reload();}
23343
+ if(d.success){alert('\u6570\u636E\u5E93\u5DF2\u6E05\u7A7A\uFF0C\u81EA\u52A8\u5907\u4EFD\uFF1A'+d.backupPath);location.reload();}
22898
23344
  else{alert('\u6E05\u7A7A\u5931\u8D25: '+d.error);}
22899
23345
  }catch(e){alert('\u6E05\u7A7A\u5931\u8D25: '+e.message);}
22900
23346
  }
@@ -22955,6 +23401,7 @@ var init_web = __esm({
22955
23401
  init_task_service();
22956
23402
  init_task_run_service();
22957
23403
  init_task_template_service();
23404
+ init_database_maintenance_service();
22958
23405
  init_drizzle_orm();
22959
23406
  init_db2();
22960
23407
  init_config();
@@ -23263,7 +23710,7 @@ var init_web = __esm({
23263
23710
  const stats = await TaskService.stats({});
23264
23711
  const runningRuns = await TaskRunService.getAllRunningRuns();
23265
23712
  const templates = await TaskTemplateService.list(100);
23266
- const configExists = existsSync4(configPath);
23713
+ const configExists = existsSync5(configPath);
23267
23714
  let runRows = "";
23268
23715
  if (runningRuns.length > 0) {
23269
23716
  for (const run of runningRuns) {
@@ -23337,7 +23784,7 @@ var init_web = __esm({
23337
23784
 
23338
23785
  <div class="card mt16" style="border-color:var(--red)">
23339
23786
  <h3 style="margin:0 0 12px;font-size:14px;color:var(--red)">\u5371\u9669\u64CD\u4F5C</h3>
23340
- <p class="sm mu" style="margin:0 0 12px">\u6E05\u7A7A\u6240\u6709\u4EFB\u52A1\u6570\u636E\uFF08tasks + task_runs + task_templates\uFF09\uFF0C\u4E0D\u53EF\u6062\u590D\u3002</p>
23787
+ <p class="sm mu" style="margin:0 0 12px">\u81EA\u52A8\u5907\u4EFD\u540E\u4E8B\u52A1\u6027\u6E05\u7A7A tasks\u3001task_runs \u548C task_templates\uFF1B\u5B58\u5728\u8FD0\u884C\u4E2D\u4EFB\u52A1\u65F6\u4F1A\u62D2\u7EDD\u6267\u884C\u3002</p>
23341
23788
  <button class="btn btn-danger" style="border-color:var(--red);color:var(--red);padding:6px 16px" onclick="clearDatabase()">\u6E05\u7A7A\u6570\u636E\u5E93</button>
23342
23789
  </div>`;
23343
23790
  return c.html(renderLayout("\u7CFB\u7EDF\u72B6\u6001", "system", body));
@@ -23442,14 +23889,16 @@ var init_web = __esm({
23442
23889
  }
23443
23890
  });
23444
23891
  app.post("/api/database/clear", async (c) => {
23892
+ const body = await c.req.json().catch(() => ({}));
23893
+ if (body.confirmation !== "CLEAR") {
23894
+ return c.json({ success: false, error: "confirmation must be CLEAR" }, 400);
23895
+ }
23445
23896
  try {
23446
- const { tasks: tasks3, taskRuns: taskRuns4, taskTemplates: taskTemplates4 } = schema_exports;
23447
- await db.delete(taskRuns4);
23448
- await db.delete(taskTemplates4);
23449
- await db.delete(tasks3);
23450
- return c.json({ success: true });
23897
+ const result = DatabaseMaintenanceService.clear({ allowCurrentGateway: true });
23898
+ return c.json({ success: true, ...result });
23451
23899
  } catch (err) {
23452
- return c.json({ success: false, error: err instanceof Error ? err.message : String(err) }, 500);
23900
+ const status = err instanceof DatabaseMaintenanceConflictError ? 409 : 500;
23901
+ return c.json({ success: false, error: err instanceof Error ? err.message : String(err) }, status);
23453
23902
  }
23454
23903
  });
23455
23904
  dashboardApp = app;
@@ -23463,7 +23912,9 @@ var init_web = __esm({
23463
23912
  // src/gateway/index.ts
23464
23913
  var gateway_exports = {};
23465
23914
  __export(gateway_exports, {
23466
- main: () => main
23915
+ acquireLock: () => acquireLock,
23916
+ main: () => main,
23917
+ releaseLock: () => releaseLock
23467
23918
  });
23468
23919
  function acquireLock() {
23469
23920
  const now = Date.now();
@@ -23472,7 +23923,8 @@ function acquireLock() {
23472
23923
  sqlite.exec("BEGIN IMMEDIATE");
23473
23924
  const existing = sqlite.prepare("SELECT id, pid, heartbeat_at FROM gateway_lock WHERE id = 1").get();
23474
23925
  if (existing) {
23475
- if (now - existing.heartbeat_at < STALE_THRESHOLD_MS) {
23926
+ const lockHolderAlive = existing.pid !== pid && isProcessAlive(existing.pid);
23927
+ if (now - existing.heartbeat_at < STALE_THRESHOLD_MS && lockHolderAlive) {
23476
23928
  sqlite.exec("ROLLBACK");
23477
23929
  console.error(JSON.stringify({
23478
23930
  ts: (/* @__PURE__ */ new Date()).toISOString(),
@@ -23542,6 +23994,15 @@ async function main() {
23542
23994
  const worker = new WorkerEngine(cfg);
23543
23995
  const watchdog = new Watchdog(cfg);
23544
23996
  const scheduler = new Scheduler(cfg);
23997
+ const recoveredOrphans = await TaskService.resetOrphanRunningToPending();
23998
+ if (recoveredOrphans > 0) {
23999
+ console.log(JSON.stringify({
24000
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
24001
+ level: "warn",
24002
+ msg: "reset orphan running tasks to pending",
24003
+ count: recoveredOrphans
24004
+ }));
24005
+ }
23545
24006
  initializeGatewayHealth({
23546
24007
  workerPollIntervalMs: cfg.worker.pollIntervalMs,
23547
24008
  schedulerEnabled: cfg.scheduler.enabled,
@@ -23621,6 +24082,7 @@ var init_gateway = __esm({
23621
24082
  init_task_service();
23622
24083
  init_task_run_service();
23623
24084
  init_health();
24085
+ init_process_control();
23624
24086
  STALE_THRESHOLD_MS = 3e4;
23625
24087
  if (import.meta.main) {
23626
24088
  main();
@@ -23635,13 +24097,13 @@ __export(update_exports, {
23635
24097
  resolveInstalledPlugin: () => resolveInstalledPlugin
23636
24098
  });
23637
24099
  import { spawnSync as spawnSync3 } from "child_process";
23638
- import { existsSync as existsSync5, readFileSync as readFileSync4, readdirSync } from "fs";
24100
+ import { existsSync as existsSync6, readFileSync as readFileSync4, readdirSync } from "fs";
23639
24101
  import { homedir as homedir4 } from "os";
23640
24102
  import { join as join4 } from "path";
23641
24103
  function pluginAt(packageDir) {
23642
24104
  const packageJson = join4(packageDir, "package.json");
23643
24105
  const gatewayEntry = join4(packageDir, "dist/gateway/index.js");
23644
- if (!existsSync5(packageJson) || !existsSync5(gatewayEntry)) return null;
24106
+ if (!existsSync6(packageJson) || !existsSync6(gatewayEntry)) return null;
23645
24107
  try {
23646
24108
  const pkg = JSON.parse(readFileSync4(packageJson, "utf8"));
23647
24109
  if (pkg.name !== PACKAGE_NAME || typeof pkg.version !== "string") return null;
@@ -23674,7 +24136,7 @@ function resolveInstalledPlugin() {
23674
24136
  return plugin;
23675
24137
  }
23676
24138
  const root = cacheRoot();
23677
- const packageDirs = existsSync5(root) ? readdirSync(root, { withFileTypes: true }).filter((entry) => entry.isDirectory() && (entry.name === PACKAGE_NAME || entry.name.startsWith(`${PACKAGE_NAME}@`))).map((entry) => join4(root, entry.name, "node_modules", PACKAGE_NAME)) : [];
24139
+ const packageDirs = existsSync6(root) ? readdirSync(root, { withFileTypes: true }).filter((entry) => entry.isDirectory() && (entry.name === PACKAGE_NAME || entry.name.startsWith(`${PACKAGE_NAME}@`))).map((entry) => join4(root, entry.name, "node_modules", PACKAGE_NAME)) : [];
23678
24140
  const installed = packageDirs.map(pluginAt).filter((plugin) => plugin !== null).sort((left, right) => compareVersions(right.version, left.version));
23679
24141
  if (!installed[0]) {
23680
24142
  throw new Error(`[supertask] OpenCode \u63D2\u4EF6\u7F13\u5B58\u4E2D\u627E\u4E0D\u5230\u53EF\u8FD0\u884C\u7684 ${PACKAGE_NAME}`);
@@ -23732,6 +24194,7 @@ var {
23732
24194
  // src/cli/index.ts
23733
24195
  init_task_service();
23734
24196
  init_task_template_service();
24197
+ init_database_maintenance_service();
23735
24198
  init_db2();
23736
24199
 
23737
24200
  // src/core/duration.ts
@@ -23980,15 +24443,35 @@ program2.command("template").description("\u7BA1\u7406\u4EFB\u52A1\u8C03\u5EA6\u
23980
24443
  console.log(JSON.stringify({ deleted, id: parseInt(options.id) }));
23981
24444
  }))
23982
24445
  );
24446
+ var databaseCommand = new Command("db").description("\u6570\u636E\u5E93\u68C0\u67E5\u3001\u5907\u4EFD\u3001\u6E05\u7A7A\u4E0E\u6062\u590D");
24447
+ databaseCommand.command("check").description("\u68C0\u67E5\u6570\u636E\u5E93\u5B8C\u6574\u6027\u3001\u5916\u952E\u548C\u4E1A\u52A1\u8868\u7EDF\u8BA1").action(async () => withDb(async () => {
24448
+ console.log(JSON.stringify(DatabaseMaintenanceService.check(), null, 2));
24449
+ }));
24450
+ databaseCommand.command("backup").description("\u521B\u5EFA\u7ECF\u8FC7\u5B8C\u6574\u6027\u6821\u9A8C\u7684\u4E00\u81F4\u6027\u5907\u4EFD").option("-o, --output <path>", "\u5907\u4EFD\u6587\u4EF6\u8DEF\u5F84\uFF08\u9ED8\u8BA4\u5199\u5165\u6570\u636E\u5E93\u76EE\u5F55\uFF09").action(async (options) => withDb(async () => {
24451
+ console.log(JSON.stringify(DatabaseMaintenanceService.backup(options.output), null, 2));
24452
+ }));
24453
+ databaseCommand.command("clear").description("\u5907\u4EFD\u540E\u4E8B\u52A1\u6027\u6E05\u7A7A\u4EFB\u52A1\u3001\u6267\u884C\u8BB0\u5F55\u548C\u8C03\u5EA6\u6A21\u677F").option("--confirm <word>", "\u5371\u9669\u64CD\u4F5C\u786E\u8BA4\uFF0C\u5FC5\u987B\u586B\u5199 CLEAR").action(async (options) => withDb(async () => {
24454
+ if (options.confirm !== "CLEAR") {
24455
+ throw new Error("\u6E05\u7A7A\u6570\u636E\u5E93\u5FC5\u987B\u663E\u5F0F\u4F20\u5165 --confirm CLEAR");
24456
+ }
24457
+ console.log(JSON.stringify(DatabaseMaintenanceService.clear(), null, 2));
24458
+ }));
24459
+ databaseCommand.command("restore").description("\u81EA\u52A8\u5907\u4EFD\u5F53\u524D\u5E93\u540E\uFF0C\u4ECE\u6307\u5B9A\u5907\u4EFD\u6062\u590D\u6570\u636E\u5E93").requiredOption("--from <path>", "\u8981\u6062\u590D\u7684 SQLite \u5907\u4EFD\u6587\u4EF6").option("--confirm <word>", "\u5371\u9669\u64CD\u4F5C\u786E\u8BA4\uFF0C\u5FC5\u987B\u586B\u5199 RESTORE").action(async (options) => withDb(async () => {
24460
+ if (options.confirm !== "RESTORE") {
24461
+ throw new Error("\u6062\u590D\u6570\u636E\u5E93\u5FC5\u987B\u663E\u5F0F\u4F20\u5165 --confirm RESTORE");
24462
+ }
24463
+ console.log(JSON.stringify(DatabaseMaintenanceService.restore(options.from), null, 2));
24464
+ }));
24465
+ program2.addCommand(databaseCommand);
23983
24466
  program2.command("init").description("Initialize SuperTask (create config + run migrations)").action(async () => withDb(async () => {
23984
- const { existsSync: existsSync6, mkdirSync: mkdirSync4, writeFileSync: writeFileSync3 } = await import("fs");
23985
- const { dirname: dirname4 } = await import("path");
24467
+ const { existsSync: existsSync7, mkdirSync: mkdirSync5, writeFileSync: writeFileSync4 } = await import("fs");
24468
+ const { dirname: dirname5 } = await import("path");
23986
24469
  const { getConfigPath: getConfigPath2 } = await Promise.resolve().then(() => (init_config(), config_exports));
23987
24470
  const configPath = getConfigPath2();
23988
- if (!existsSync6(configPath)) {
23989
- const dir = dirname4(configPath);
23990
- if (!existsSync6(dir)) mkdirSync4(dir, { recursive: true });
23991
- writeFileSync3(configPath, JSON.stringify({
24471
+ if (!existsSync7(configPath)) {
24472
+ const dir = dirname5(configPath);
24473
+ if (!existsSync7(dir)) mkdirSync5(dir, { recursive: true });
24474
+ writeFileSync4(configPath, JSON.stringify({
23992
24475
  configVersion: 2,
23993
24476
  worker: { maxConcurrency: 2 },
23994
24477
  scheduler: { enabled: true }