opencode-supertask 0.1.22 → 0.1.24

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.
@@ -6895,6 +6895,80 @@ var init_health = __esm({
6895
6895
  }
6896
6896
  });
6897
6897
 
6898
+ // src/core/process-control.ts
6899
+ import { spawnSync } from "child_process";
6900
+ import { basename } from "path";
6901
+ function isSafePid(pid) {
6902
+ return Number.isInteger(pid) && pid > 1 && pid !== process.pid;
6903
+ }
6904
+ function isProcessAlive(pid) {
6905
+ if (!Number.isInteger(pid) || pid <= 0) return false;
6906
+ try {
6907
+ process.kill(pid, 0);
6908
+ return true;
6909
+ } catch (error) {
6910
+ return error instanceof Error && "code" in error && error.code === "EPERM";
6911
+ }
6912
+ }
6913
+ function inspectUnixProcess(pid) {
6914
+ const result = spawnSync("ps", ["-o", "pgid=", "-o", "command=", "-p", String(pid)], {
6915
+ encoding: "utf8",
6916
+ stdio: ["ignore", "pipe", "ignore"]
6917
+ });
6918
+ if (result.status !== 0) return null;
6919
+ const match2 = result.stdout.trim().match(/^(\d+)\s+(.+)$/s);
6920
+ if (!match2) return null;
6921
+ return { processGroupId: Number(match2[1]), command: match2[2] };
6922
+ }
6923
+ function commandMatches(command, expectedExecutable) {
6924
+ const expectedName = basename(expectedExecutable).trim().toLowerCase();
6925
+ if (!expectedName) return false;
6926
+ return command.toLowerCase().includes(expectedName);
6927
+ }
6928
+ function signalSpawnedProcessTree(pid, signal) {
6929
+ if (!isSafePid(pid)) return false;
6930
+ if (process.platform !== "win32") {
6931
+ try {
6932
+ process.kill(-pid, signal);
6933
+ return true;
6934
+ } catch {
6935
+ }
6936
+ }
6937
+ try {
6938
+ process.kill(pid, signal);
6939
+ return true;
6940
+ } catch {
6941
+ return false;
6942
+ }
6943
+ }
6944
+ function signalRecordedProcessTree(pid, signal, expectedExecutable) {
6945
+ if (!isSafePid(pid)) return false;
6946
+ if (process.platform === "win32") {
6947
+ const list = spawnSync("tasklist", ["/FI", `PID eq ${pid}`, "/FO", "CSV", "/NH"], {
6948
+ encoding: "utf8",
6949
+ stdio: ["ignore", "pipe", "ignore"]
6950
+ });
6951
+ if (list.status !== 0 || !commandMatches(list.stdout, expectedExecutable)) return false;
6952
+ const args = ["/PID", String(pid), "/T"];
6953
+ if (signal === "SIGKILL") args.push("/F");
6954
+ return spawnSync("taskkill", args, { stdio: "ignore" }).status === 0;
6955
+ }
6956
+ const info = inspectUnixProcess(pid);
6957
+ if (!info || !commandMatches(info.command, expectedExecutable)) return false;
6958
+ try {
6959
+ if (info.processGroupId === pid) process.kill(-pid, signal);
6960
+ else process.kill(pid, signal);
6961
+ return true;
6962
+ } catch {
6963
+ return false;
6964
+ }
6965
+ }
6966
+ var init_process_control = __esm({
6967
+ "src/core/process-control.ts"() {
6968
+ "use strict";
6969
+ }
6970
+ });
6971
+
6898
6972
  // node_modules/cron-parser/dist/fields/types.js
6899
6973
  var require_types = __commonJS({
6900
6974
  "node_modules/cron-parser/dist/fields/types.js"(exports) {
@@ -14108,26 +14182,26 @@ var require_CronDate = __commonJS({
14108
14182
  * @param {CronDate | Date | number | string} [timestamp] - The timestamp to initialize the CronDate with.
14109
14183
  * @param {string} [tz] - The timezone to use for the CronDate.
14110
14184
  */
14111
- constructor(timestamp, tz) {
14185
+ constructor(timestamp2, tz) {
14112
14186
  const dateOpts = { zone: tz };
14113
- if (!timestamp) {
14187
+ if (!timestamp2) {
14114
14188
  this.#date = luxon_1.DateTime.local();
14115
- } else if (timestamp instanceof _CronDate) {
14116
- this.#date = timestamp.#date;
14117
- this.#dstStart = timestamp.#dstStart;
14118
- this.#dstEnd = timestamp.#dstEnd;
14119
- } else if (timestamp instanceof Date) {
14120
- this.#date = luxon_1.DateTime.fromJSDate(timestamp, dateOpts);
14121
- } else if (typeof timestamp === "number") {
14122
- this.#date = luxon_1.DateTime.fromMillis(timestamp, dateOpts);
14189
+ } else if (timestamp2 instanceof _CronDate) {
14190
+ this.#date = timestamp2.#date;
14191
+ this.#dstStart = timestamp2.#dstStart;
14192
+ this.#dstEnd = timestamp2.#dstEnd;
14193
+ } else if (timestamp2 instanceof Date) {
14194
+ this.#date = luxon_1.DateTime.fromJSDate(timestamp2, dateOpts);
14195
+ } else if (typeof timestamp2 === "number") {
14196
+ this.#date = luxon_1.DateTime.fromMillis(timestamp2, dateOpts);
14123
14197
  } else {
14124
- this.#date = luxon_1.DateTime.fromISO(timestamp, dateOpts);
14125
- this.#date.isValid || (this.#date = luxon_1.DateTime.fromRFC2822(timestamp, dateOpts));
14126
- this.#date.isValid || (this.#date = luxon_1.DateTime.fromSQL(timestamp, dateOpts));
14127
- this.#date.isValid || (this.#date = luxon_1.DateTime.fromFormat(timestamp, "EEE, d MMM yyyy HH:mm:ss", dateOpts));
14198
+ this.#date = luxon_1.DateTime.fromISO(timestamp2, dateOpts);
14199
+ this.#date.isValid || (this.#date = luxon_1.DateTime.fromRFC2822(timestamp2, dateOpts));
14200
+ this.#date.isValid || (this.#date = luxon_1.DateTime.fromSQL(timestamp2, dateOpts));
14201
+ this.#date.isValid || (this.#date = luxon_1.DateTime.fromFormat(timestamp2, "EEE, d MMM yyyy HH:mm:ss", dateOpts));
14128
14202
  }
14129
14203
  if (!this.#date.isValid) {
14130
- throw new Error(`CronDate: unhandled timestamp: ${timestamp}`);
14204
+ throw new Error(`CronDate: unhandled timestamp: ${timestamp2}`);
14131
14205
  }
14132
14206
  if (tz && tz !== this.#date.zoneName) {
14133
14207
  this.#date = this.#date.setZone(tz);
@@ -18519,14 +18593,346 @@ var init_html2 = __esm({
18519
18593
  }
18520
18594
  });
18521
18595
 
18596
+ // src/core/services/database-maintenance.service.ts
18597
+ import { Database as Database3 } from "bun:sqlite";
18598
+ import { randomUUID } from "crypto";
18599
+ import {
18600
+ chmodSync,
18601
+ constants,
18602
+ copyFileSync,
18603
+ existsSync as existsSync3,
18604
+ mkdirSync as mkdirSync2,
18605
+ renameSync,
18606
+ statSync,
18607
+ unlinkSync,
18608
+ writeFileSync
18609
+ } from "fs";
18610
+ import { tmpdir } from "os";
18611
+ import { basename as basename2, dirname as dirname2, resolve } from "path";
18612
+ function timestamp() {
18613
+ return (/* @__PURE__ */ new Date()).toISOString().replace(/[-:]/g, "").replace(/\.\d{3}Z$/, "Z");
18614
+ }
18615
+ function normalizedPath(path) {
18616
+ return path === ":memory:" ? path : resolve(path);
18617
+ }
18618
+ function safeUnlink(path) {
18619
+ try {
18620
+ unlinkSync(path);
18621
+ } catch (error) {
18622
+ const code = error instanceof Error && "code" in error ? error.code : void 0;
18623
+ if (code !== "ENOENT") throw error;
18624
+ }
18625
+ }
18626
+ function errorMessage(error) {
18627
+ return error instanceof Error ? error.message : String(error);
18628
+ }
18629
+ var REQUIRED_TABLES, DatabaseMaintenanceConflictError, DatabaseMaintenanceService;
18630
+ var init_database_maintenance_service = __esm({
18631
+ "src/core/services/database-maintenance.service.ts"() {
18632
+ "use strict";
18633
+ init_db2();
18634
+ init_process_control();
18635
+ REQUIRED_TABLES = ["gateway_lock", "tasks", "task_runs", "task_templates"];
18636
+ DatabaseMaintenanceConflictError = class extends Error {
18637
+ constructor(message) {
18638
+ super(message);
18639
+ this.name = "DatabaseMaintenanceConflictError";
18640
+ }
18641
+ };
18642
+ DatabaseMaintenanceService = class {
18643
+ static check() {
18644
+ return this.inspect(getSqlite(), DB_FILE_PATH);
18645
+ }
18646
+ static backup(outputPath) {
18647
+ const sqlite2 = getSqlite();
18648
+ try {
18649
+ sqlite2.query("PRAGMA wal_checkpoint(PASSIVE)").get();
18650
+ } catch {
18651
+ }
18652
+ return this.writeSnapshot(sqlite2, outputPath ?? this.createBackupPath("backup"));
18653
+ }
18654
+ static clear(options = {}) {
18655
+ const sqlite2 = getSqlite();
18656
+ this.assertGatewaySafe(sqlite2, options.allowCurrentGateway ?? false);
18657
+ this.assertNoRunningWork(sqlite2);
18658
+ sqlite2.exec("BEGIN IMMEDIATE");
18659
+ let backup = null;
18660
+ try {
18661
+ this.assertGatewaySafe(sqlite2, options.allowCurrentGateway ?? false);
18662
+ this.assertNoRunningWork(sqlite2);
18663
+ const before = this.readCounts(sqlite2);
18664
+ backup = this.writeSnapshot(sqlite2, this.createBackupPath("pre-clear"));
18665
+ sqlite2.exec("DELETE FROM task_runs");
18666
+ sqlite2.exec("DELETE FROM tasks");
18667
+ sqlite2.exec("DELETE FROM task_templates");
18668
+ sqlite2.exec("COMMIT");
18669
+ return {
18670
+ backupPath: backup.path,
18671
+ deleted: before,
18672
+ check: this.inspect(sqlite2, DB_FILE_PATH)
18673
+ };
18674
+ } catch (error) {
18675
+ if (sqlite2.inTransaction) sqlite2.exec("ROLLBACK");
18676
+ if (error instanceof DatabaseMaintenanceConflictError) throw error;
18677
+ const backupHint = backup ? `\uFF1B\u4E8B\u52A1\u5DF2\u56DE\u6EDA\uFF0C\u5907\u4EFD\u4FDD\u7559\u5728 ${backup.path}` : "";
18678
+ throw new Error(`\u6E05\u7A7A\u6570\u636E\u5E93\u5931\u8D25${backupHint}\uFF1A${errorMessage(error)}`);
18679
+ }
18680
+ }
18681
+ static restore(sourcePath) {
18682
+ if (DB_FILE_PATH === ":memory:") {
18683
+ throw new Error("\u5185\u5B58\u6570\u636E\u5E93\u4E0D\u652F\u6301 restore");
18684
+ }
18685
+ const source = normalizedPath(sourcePath);
18686
+ const livePath = normalizedPath(DB_FILE_PATH);
18687
+ if (source === livePath) throw new Error("\u6062\u590D\u6765\u6E90\u4E0D\u80FD\u662F\u5F53\u524D\u6570\u636E\u5E93\u6587\u4EF6");
18688
+ const current = getSqlite();
18689
+ this.assertGatewaySafe(current, false);
18690
+ this.assertNoRunningWork(current);
18691
+ if (!existsSync3(source) || !statSync(source).isFile()) {
18692
+ throw new Error(`\u5907\u4EFD\u6587\u4EF6\u4E0D\u5B58\u5728\uFF1A${source}`);
18693
+ }
18694
+ let sourceCheck;
18695
+ try {
18696
+ sourceCheck = this.inspectFile(source);
18697
+ } catch (error) {
18698
+ throw new Error(`\u5907\u4EFD\u6587\u4EF6\u65E0\u6548\uFF1A${errorMessage(error)}`);
18699
+ }
18700
+ if (!sourceCheck.ok) {
18701
+ throw new Error(`\u5907\u4EFD\u6587\u4EF6\u6821\u9A8C\u5931\u8D25\uFF1A${sourceCheck.integrityMessages.join("; ") || "schema/foreign key error"}`);
18702
+ }
18703
+ const stagePath = `${livePath}.restore-${process.pid}-${randomUUID()}.tmp`;
18704
+ const rollbackPath = `${livePath}.rollback-${process.pid}-${randomUUID()}.tmp`;
18705
+ let safetyBackup = null;
18706
+ let liveMoved = false;
18707
+ try {
18708
+ copyFileSync(source, stagePath, constants.COPYFILE_EXCL);
18709
+ chmodSync(stagePath, 384);
18710
+ const staged = new Database3(stagePath);
18711
+ try {
18712
+ const stagedCheck = this.inspect(staged, stagePath);
18713
+ if (!stagedCheck.ok) throw new Error("\u6682\u5B58\u6062\u590D\u6587\u4EF6\u6821\u9A8C\u5931\u8D25");
18714
+ const now = Date.now();
18715
+ staged.query(
18716
+ "INSERT OR REPLACE INTO gateway_lock (id, pid, acquired_at, heartbeat_at, ready_at) VALUES (1, ?, ?, ?, NULL)"
18717
+ ).run(process.pid, now, now);
18718
+ } finally {
18719
+ staged.close();
18720
+ }
18721
+ const sqlite2 = getSqlite();
18722
+ sqlite2.exec("BEGIN IMMEDIATE");
18723
+ try {
18724
+ this.assertGatewaySafe(sqlite2, false);
18725
+ this.assertNoRunningWork(sqlite2);
18726
+ safetyBackup = this.writeSnapshot(sqlite2, this.createBackupPath("pre-restore"));
18727
+ const now = Date.now();
18728
+ sqlite2.query(
18729
+ "INSERT OR REPLACE INTO gateway_lock (id, pid, acquired_at, heartbeat_at, ready_at) VALUES (1, ?, ?, ?, NULL)"
18730
+ ).run(process.pid, now, now);
18731
+ sqlite2.exec("COMMIT");
18732
+ } catch (error) {
18733
+ if (sqlite2.inTransaction) sqlite2.exec("ROLLBACK");
18734
+ throw error;
18735
+ }
18736
+ const checkpoint = sqlite2.query("PRAGMA wal_checkpoint(TRUNCATE)").get();
18737
+ if (checkpoint && checkpoint.busy !== 0) {
18738
+ throw new DatabaseMaintenanceConflictError("\u6570\u636E\u5E93\u4ECD\u88AB\u5176\u4ED6\u8FDE\u63A5\u5360\u7528\uFF0C\u65E0\u6CD5\u5B89\u5168\u6062\u590D");
18739
+ }
18740
+ closeDb();
18741
+ safeUnlink(`${livePath}-wal`);
18742
+ safeUnlink(`${livePath}-shm`);
18743
+ renameSync(livePath, rollbackPath);
18744
+ liveMoved = true;
18745
+ renameSync(stagePath, livePath);
18746
+ const restored = getSqlite();
18747
+ restored.exec("BEGIN IMMEDIATE");
18748
+ let recoveredRunningTasks = 0;
18749
+ let closedRunningRuns = 0;
18750
+ try {
18751
+ recoveredRunningTasks = this.scalar(
18752
+ restored,
18753
+ "SELECT COUNT(*) AS count FROM tasks WHERE status = 'running'"
18754
+ );
18755
+ closedRunningRuns = this.scalar(
18756
+ restored,
18757
+ "SELECT COUNT(*) AS count FROM task_runs WHERE status = 'running'"
18758
+ );
18759
+ const finishedAt = Math.floor(Date.now() / 1e3);
18760
+ restored.query(`
18761
+ UPDATE task_runs
18762
+ SET status = 'failed', finished_at = ?,
18763
+ log = CASE
18764
+ WHEN log IS NULL OR log = '' THEN '\u6570\u636E\u5E93\u6062\u590D\u65F6\u5173\u95ED\u9057\u7559\u8FD0\u884C\u8BB0\u5F55'
18765
+ ELSE log || '
18766
+ \u6570\u636E\u5E93\u6062\u590D\u65F6\u5173\u95ED\u9057\u7559\u8FD0\u884C\u8BB0\u5F55'
18767
+ END
18768
+ WHERE status = 'running'
18769
+ `).run(finishedAt);
18770
+ restored.exec(`
18771
+ UPDATE tasks
18772
+ SET status = 'pending', started_at = NULL, finished_at = NULL
18773
+ WHERE status = 'running'
18774
+ `);
18775
+ restored.query("DELETE FROM gateway_lock WHERE pid = ?").run(process.pid);
18776
+ restored.exec("COMMIT");
18777
+ } catch (error) {
18778
+ if (restored.inTransaction) restored.exec("ROLLBACK");
18779
+ throw error;
18780
+ }
18781
+ const check = this.inspect(restored, livePath);
18782
+ if (!check.ok) throw new Error("\u6062\u590D\u540E\u7684\u6570\u636E\u5E93\u672A\u901A\u8FC7\u5B8C\u6574\u6027\u6821\u9A8C");
18783
+ safeUnlink(rollbackPath);
18784
+ return {
18785
+ sourcePath: source,
18786
+ safetyBackupPath: safetyBackup.path,
18787
+ recoveredRunningTasks,
18788
+ closedRunningRuns,
18789
+ check
18790
+ };
18791
+ } catch (error) {
18792
+ closeDb();
18793
+ safeUnlink(`${livePath}-wal`);
18794
+ safeUnlink(`${livePath}-shm`);
18795
+ safeUnlink(stagePath);
18796
+ safeUnlink(`${stagePath}-journal`);
18797
+ safeUnlink(`${stagePath}-wal`);
18798
+ safeUnlink(`${stagePath}-shm`);
18799
+ if (liveMoved && existsSync3(rollbackPath)) {
18800
+ safeUnlink(livePath);
18801
+ renameSync(rollbackPath, livePath);
18802
+ try {
18803
+ const original = getSqlite();
18804
+ original.query("DELETE FROM gateway_lock WHERE pid = ?").run(process.pid);
18805
+ } catch {
18806
+ }
18807
+ } else if (existsSync3(rollbackPath)) {
18808
+ safeUnlink(rollbackPath);
18809
+ } else {
18810
+ try {
18811
+ getSqlite().query("DELETE FROM gateway_lock WHERE pid = ?").run(process.pid);
18812
+ } catch {
18813
+ }
18814
+ }
18815
+ const backupHint = safetyBackup ? `\uFF1B\u5F53\u524D\u5E93\u5B89\u5168\u5907\u4EFD\uFF1A${safetyBackup.path}` : "";
18816
+ throw new Error(`\u6062\u590D\u6570\u636E\u5E93\u5931\u8D25\uFF0C\u5DF2\u5C1D\u8BD5\u56DE\u6EDA${backupHint}\uFF1A${errorMessage(error)}`);
18817
+ }
18818
+ }
18819
+ static inspectFile(path) {
18820
+ let sqlite2;
18821
+ try {
18822
+ sqlite2 = new Database3(path, { readonly: true, strict: true });
18823
+ } catch (error) {
18824
+ throw new Error(`\u65E0\u6CD5\u6253\u5F00\u5907\u4EFD\u6587\u4EF6\uFF1A${errorMessage(error)}`);
18825
+ }
18826
+ try {
18827
+ return this.inspect(sqlite2, path);
18828
+ } finally {
18829
+ sqlite2.close();
18830
+ }
18831
+ }
18832
+ static inspect(sqlite2, path) {
18833
+ const tableRows = sqlite2.query(
18834
+ "SELECT name FROM sqlite_master WHERE type = 'table'"
18835
+ ).all();
18836
+ const tableNames = new Set(tableRows.map((row) => row.name));
18837
+ const missingTables = REQUIRED_TABLES.filter((table) => !tableNames.has(table));
18838
+ const integrityRows = sqlite2.query("PRAGMA integrity_check").all();
18839
+ const integrityMessages = integrityRows.map((row) => row.integrity_check);
18840
+ const foreignKeyViolations = sqlite2.query("PRAGMA foreign_key_check").all().length;
18841
+ const journal = sqlite2.query("PRAGMA journal_mode").get();
18842
+ const counts = missingTables.length === 0 ? this.readCounts(sqlite2) : { tasks: 0, taskRuns: 0, taskTemplates: 0 };
18843
+ const runningTasks = missingTables.includes("tasks") ? 0 : this.scalar(sqlite2, "SELECT COUNT(*) AS count FROM tasks WHERE status = 'running'");
18844
+ const runningRuns = missingTables.includes("task_runs") ? 0 : this.scalar(sqlite2, "SELECT COUNT(*) AS count FROM task_runs WHERE status = 'running'");
18845
+ const sizeBytes = path === ":memory:" || !existsSync3(path) ? sqlite2.serialize().byteLength : statSync(path).size;
18846
+ return {
18847
+ ok: integrityMessages.length === 1 && integrityMessages[0] === "ok" && foreignKeyViolations === 0 && missingTables.length === 0,
18848
+ path: normalizedPath(path),
18849
+ sizeBytes,
18850
+ journalMode: journal?.journal_mode ?? "unknown",
18851
+ integrityMessages,
18852
+ foreignKeyViolations,
18853
+ missingTables,
18854
+ counts,
18855
+ runningTasks,
18856
+ runningRuns
18857
+ };
18858
+ }
18859
+ static writeSnapshot(sqlite2, outputPath) {
18860
+ const liveCheck = this.inspect(sqlite2, DB_FILE_PATH);
18861
+ 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");
18862
+ const output = normalizedPath(outputPath);
18863
+ if (DB_FILE_PATH !== ":memory:" && output === normalizedPath(DB_FILE_PATH)) {
18864
+ throw new Error("\u5907\u4EFD\u8DEF\u5F84\u4E0D\u80FD\u8986\u76D6\u5F53\u524D\u6570\u636E\u5E93");
18865
+ }
18866
+ if (existsSync3(output)) throw new Error(`\u5907\u4EFD\u6587\u4EF6\u5DF2\u5B58\u5728\uFF1A${output}`);
18867
+ mkdirSync2(dirname2(output), { recursive: true });
18868
+ const temporary = `${output}.tmp-${process.pid}-${randomUUID()}`;
18869
+ try {
18870
+ writeFileSync(temporary, sqlite2.serialize(), { flag: "wx", mode: 384 });
18871
+ const standalone = new Database3(temporary, { readwrite: true, create: false });
18872
+ try {
18873
+ standalone.query("PRAGMA journal_mode = DELETE").get();
18874
+ } finally {
18875
+ standalone.close();
18876
+ }
18877
+ safeUnlink(`${temporary}-wal`);
18878
+ safeUnlink(`${temporary}-shm`);
18879
+ const check = this.inspectFile(temporary);
18880
+ if (!check.ok) throw new Error("\u65B0\u5907\u4EFD\u672A\u901A\u8FC7\u5B8C\u6574\u6027\u6821\u9A8C");
18881
+ renameSync(temporary, output);
18882
+ const finalCheck = { ...check, path: output, sizeBytes: statSync(output).size };
18883
+ return { path: output, sizeBytes: finalCheck.sizeBytes, check: finalCheck };
18884
+ } catch (error) {
18885
+ safeUnlink(temporary);
18886
+ safeUnlink(`${temporary}-wal`);
18887
+ safeUnlink(`${temporary}-shm`);
18888
+ throw error;
18889
+ }
18890
+ }
18891
+ static createBackupPath(kind) {
18892
+ const directory = DB_FILE_PATH === ":memory:" ? tmpdir() : dirname2(normalizedPath(DB_FILE_PATH));
18893
+ const base = DB_FILE_PATH === ":memory:" ? "supertask-memory" : basename2(DB_FILE_PATH, ".db");
18894
+ return resolve(directory, `${base}.${kind}-${timestamp()}-${randomUUID().slice(0, 8)}.db`);
18895
+ }
18896
+ static readCounts(sqlite2) {
18897
+ return {
18898
+ tasks: this.scalar(sqlite2, "SELECT COUNT(*) AS count FROM tasks"),
18899
+ taskRuns: this.scalar(sqlite2, "SELECT COUNT(*) AS count FROM task_runs"),
18900
+ taskTemplates: this.scalar(sqlite2, "SELECT COUNT(*) AS count FROM task_templates")
18901
+ };
18902
+ }
18903
+ static scalar(sqlite2, statement) {
18904
+ const row = sqlite2.query(statement).get();
18905
+ return Number(row?.count ?? 0);
18906
+ }
18907
+ static assertGatewaySafe(sqlite2, allowCurrentGateway) {
18908
+ const lock = sqlite2.query("SELECT pid FROM gateway_lock WHERE id = 1").get();
18909
+ if (!lock || !isProcessAlive(lock.pid)) return;
18910
+ if (allowCurrentGateway && lock.pid === process.pid) return;
18911
+ throw new DatabaseMaintenanceConflictError(
18912
+ `Gateway PID ${lock.pid} \u4ECD\u5728\u8FD0\u884C\uFF0C\u8BF7\u5148\u6267\u884C pm2 stop supertask-gateway`
18913
+ );
18914
+ }
18915
+ static assertNoRunningWork(sqlite2) {
18916
+ const runningTasks = this.scalar(sqlite2, "SELECT COUNT(*) AS count FROM tasks WHERE status = 'running'");
18917
+ const runningRuns = this.scalar(sqlite2, "SELECT COUNT(*) AS count FROM task_runs WHERE status = 'running'");
18918
+ if (runningTasks > 0 || runningRuns > 0) {
18919
+ throw new DatabaseMaintenanceConflictError(
18920
+ `\u5B58\u5728\u8FD0\u884C\u4E2D\u4EFB\u52A1\uFF08tasks=${runningTasks}, task_runs=${runningRuns}\uFF09\uFF0C\u5DF2\u62D2\u7EDD\u5371\u9669\u64CD\u4F5C`
18921
+ );
18922
+ }
18923
+ }
18924
+ };
18925
+ }
18926
+ });
18927
+
18522
18928
  // src/web/index.tsx
18523
18929
  var web_exports = {};
18524
18930
  __export(web_exports, {
18525
18931
  dashboardApp: () => dashboardApp,
18526
18932
  default: () => web_default
18527
18933
  });
18528
- import { existsSync as existsSync3, readFileSync as readFileSync2, writeFileSync, mkdirSync as mkdirSync2, renameSync } from "fs";
18529
- import { dirname as dirname2 } from "path";
18934
+ import { existsSync as existsSync4, readFileSync as readFileSync2, writeFileSync as writeFileSync2, mkdirSync as mkdirSync3, renameSync as renameSync2 } from "fs";
18935
+ import { dirname as dirname3 } from "path";
18530
18936
  function parsePositiveInteger(value) {
18531
18937
  if (!/^\d+$/.test(value)) return null;
18532
18938
  const parsed = Number(value);
@@ -18576,7 +18982,7 @@ function esc(s) {
18576
18982
  }
18577
18983
  function readCurrentConfig() {
18578
18984
  const configPath = getConfigPath();
18579
- if (!existsSync3(configPath)) return {};
18985
+ if (!existsSync4(configPath)) return {};
18580
18986
  try {
18581
18987
  return JSON.parse(readFileSync2(configPath, "utf-8"));
18582
18988
  } catch {
@@ -18585,11 +18991,11 @@ function readCurrentConfig() {
18585
18991
  }
18586
18992
  function writeConfig(cfg) {
18587
18993
  const configPath = getConfigPath();
18588
- const dir = dirname2(configPath);
18589
- if (!existsSync3(dir)) mkdirSync2(dir, { recursive: true });
18994
+ const dir = dirname3(configPath);
18995
+ if (!existsSync4(dir)) mkdirSync3(dir, { recursive: true });
18590
18996
  const tempPath = `${configPath}.${process.pid}.tmp`;
18591
- writeFileSync(tempPath, JSON.stringify(cfg, null, 2) + "\n", { mode: 384 });
18592
- renameSync(tempPath, configPath);
18997
+ writeFileSync2(tempPath, JSON.stringify(cfg, null, 2) + "\n", { mode: 384 });
18998
+ renameSync2(tempPath, configPath);
18593
18999
  }
18594
19000
  function renderLayout(title, activeTab, body) {
18595
19001
  return `<!DOCTYPE html><html><head><meta charset="UTF-8"><title>${title} - SuperTask</title>${SHARED_STYLES}
@@ -18606,12 +19012,12 @@ async function triggerTmpl(id){if(!confirm('\u7ACB\u5373\u89E6\u53D1\u4E00\u6B21
18606
19012
  function toggleLog(id){const el=document.getElementById('log-'+id);el.style.display=el.style.display==='none'?'block':'none';}
18607
19013
 
18608
19014
  async function clearDatabase(){
18609
- if(!confirm('\u786E\u5B9A\u6E05\u7A7A\u6240\u6709\u4EFB\u52A1\u6570\u636E\uFF1F\u6B64\u64CD\u4F5C\u4E0D\u53EF\u6062\u590D\uFF01'))return;
18610
- 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;
19015
+ 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;
19016
+ 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;
18611
19017
  try{
18612
- const r=await fetch('/api/database/clear',{method:'POST'});
19018
+ const r=await fetch('/api/database/clear',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({confirmation:'CLEAR'})});
18613
19019
  const d=await r.json();
18614
- if(d.success){alert('\u6570\u636E\u5E93\u5DF2\u6E05\u7A7A');location.reload();}
19020
+ if(d.success){alert('\u6570\u636E\u5E93\u5DF2\u6E05\u7A7A\uFF0C\u81EA\u52A8\u5907\u4EFD\uFF1A'+d.backupPath);location.reload();}
18615
19021
  else{alert('\u6E05\u7A7A\u5931\u8D25: '+d.error);}
18616
19022
  }catch(e){alert('\u6E05\u7A7A\u5931\u8D25: '+e.message);}
18617
19023
  }
@@ -18672,6 +19078,7 @@ var init_web = __esm({
18672
19078
  init_task_service();
18673
19079
  init_task_run_service();
18674
19080
  init_task_template_service();
19081
+ init_database_maintenance_service();
18675
19082
  init_drizzle_orm();
18676
19083
  init_db2();
18677
19084
  init_config();
@@ -18980,7 +19387,7 @@ var init_web = __esm({
18980
19387
  const stats = await TaskService.stats({});
18981
19388
  const runningRuns = await TaskRunService.getAllRunningRuns();
18982
19389
  const templates = await TaskTemplateService.list(100);
18983
- const configExists = existsSync3(configPath);
19390
+ const configExists = existsSync4(configPath);
18984
19391
  let runRows = "";
18985
19392
  if (runningRuns.length > 0) {
18986
19393
  for (const run of runningRuns) {
@@ -19054,7 +19461,7 @@ var init_web = __esm({
19054
19461
 
19055
19462
  <div class="card mt16" style="border-color:var(--red)">
19056
19463
  <h3 style="margin:0 0 12px;font-size:14px;color:var(--red)">\u5371\u9669\u64CD\u4F5C</h3>
19057
- <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>
19464
+ <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>
19058
19465
  <button class="btn btn-danger" style="border-color:var(--red);color:var(--red);padding:6px 16px" onclick="clearDatabase()">\u6E05\u7A7A\u6570\u636E\u5E93</button>
19059
19466
  </div>`;
19060
19467
  return c.html(renderLayout("\u7CFB\u7EDF\u72B6\u6001", "system", body));
@@ -19159,14 +19566,16 @@ var init_web = __esm({
19159
19566
  }
19160
19567
  });
19161
19568
  app.post("/api/database/clear", async (c) => {
19569
+ const body = await c.req.json().catch(() => ({}));
19570
+ if (body.confirmation !== "CLEAR") {
19571
+ return c.json({ success: false, error: "confirmation must be CLEAR" }, 400);
19572
+ }
19162
19573
  try {
19163
- const { tasks: tasks3, taskRuns: taskRuns4, taskTemplates: taskTemplates4 } = schema_exports;
19164
- await db.delete(taskRuns4);
19165
- await db.delete(taskTemplates4);
19166
- await db.delete(tasks3);
19167
- return c.json({ success: true });
19574
+ const result = DatabaseMaintenanceService.clear({ allowCurrentGateway: true });
19575
+ return c.json({ success: true, ...result });
19168
19576
  } catch (err) {
19169
- return c.json({ success: false, error: err instanceof Error ? err.message : String(err) }, 500);
19577
+ const status = err instanceof DatabaseMaintenanceConflictError ? 409 : 500;
19578
+ return c.json({ success: false, error: err instanceof Error ? err.message : String(err) }, status);
19170
19579
  }
19171
19580
  });
19172
19581
  dashboardApp = app;
@@ -19185,78 +19594,8 @@ init_config();
19185
19594
  init_task_service();
19186
19595
  init_task_run_service();
19187
19596
  init_health();
19597
+ init_process_control();
19188
19598
  import { spawn } from "child_process";
19189
-
19190
- // src/core/process-control.ts
19191
- import { spawnSync } from "child_process";
19192
- import { basename } from "path";
19193
- function isSafePid(pid) {
19194
- return Number.isInteger(pid) && pid > 1 && pid !== process.pid;
19195
- }
19196
- function isProcessAlive(pid) {
19197
- if (!Number.isInteger(pid) || pid <= 0) return false;
19198
- try {
19199
- process.kill(pid, 0);
19200
- return true;
19201
- } catch (error) {
19202
- return error instanceof Error && "code" in error && error.code === "EPERM";
19203
- }
19204
- }
19205
- function inspectUnixProcess(pid) {
19206
- const result = spawnSync("ps", ["-o", "pgid=", "-o", "command=", "-p", String(pid)], {
19207
- encoding: "utf8",
19208
- stdio: ["ignore", "pipe", "ignore"]
19209
- });
19210
- if (result.status !== 0) return null;
19211
- const match2 = result.stdout.trim().match(/^(\d+)\s+(.+)$/s);
19212
- if (!match2) return null;
19213
- return { processGroupId: Number(match2[1]), command: match2[2] };
19214
- }
19215
- function commandMatches(command, expectedExecutable) {
19216
- const expectedName = basename(expectedExecutable).trim().toLowerCase();
19217
- if (!expectedName) return false;
19218
- return command.toLowerCase().includes(expectedName);
19219
- }
19220
- function signalSpawnedProcessTree(pid, signal) {
19221
- if (!isSafePid(pid)) return false;
19222
- if (process.platform !== "win32") {
19223
- try {
19224
- process.kill(-pid, signal);
19225
- return true;
19226
- } catch {
19227
- }
19228
- }
19229
- try {
19230
- process.kill(pid, signal);
19231
- return true;
19232
- } catch {
19233
- return false;
19234
- }
19235
- }
19236
- function signalRecordedProcessTree(pid, signal, expectedExecutable) {
19237
- if (!isSafePid(pid)) return false;
19238
- if (process.platform === "win32") {
19239
- const list = spawnSync("tasklist", ["/FI", `PID eq ${pid}`, "/FO", "CSV", "/NH"], {
19240
- encoding: "utf8",
19241
- stdio: ["ignore", "pipe", "ignore"]
19242
- });
19243
- if (list.status !== 0 || !commandMatches(list.stdout, expectedExecutable)) return false;
19244
- const args = ["/PID", String(pid), "/T"];
19245
- if (signal === "SIGKILL") args.push("/F");
19246
- return spawnSync("taskkill", args, { stdio: "ignore" }).status === 0;
19247
- }
19248
- const info = inspectUnixProcess(pid);
19249
- if (!info || !commandMatches(info.command, expectedExecutable)) return false;
19250
- try {
19251
- if (info.processGroupId === pid) process.kill(-pid, signal);
19252
- else process.kill(pid, signal);
19253
- return true;
19254
- } catch {
19255
- return false;
19256
- }
19257
- }
19258
-
19259
- // src/worker/index.ts
19260
19599
  var DEFAULT_MAX_OUTPUT_CHARS = 64 * 1024;
19261
19600
  var FORBIDDEN_AGENT = "supertask-runner";
19262
19601
  var WorkerEngine = class {
@@ -19519,14 +19858,14 @@ ${output}` : ""}`;
19519
19858
  if (entry.child.exitCode !== null || entry.child.signalCode !== null) {
19520
19859
  return Promise.resolve();
19521
19860
  }
19522
- return new Promise((resolve) => {
19861
+ return new Promise((resolve2) => {
19523
19862
  const timeout = setTimeout(() => {
19524
19863
  this.signalEntry(entry, "SIGKILL");
19525
- resolve();
19864
+ resolve2();
19526
19865
  }, 5e3);
19527
19866
  entry.child.once("close", () => {
19528
19867
  clearTimeout(timeout);
19529
- resolve();
19868
+ resolve2();
19530
19869
  });
19531
19870
  this.signalEntry(entry, "SIGTERM");
19532
19871
  });
@@ -19558,6 +19897,7 @@ ${output}` : ""}`;
19558
19897
  init_task_run_service();
19559
19898
  init_task_service();
19560
19899
  init_backoff();
19900
+ init_process_control();
19561
19901
  async function checkHeartbeats(heartbeatTimeoutMs) {
19562
19902
  const staleRuns = await TaskRunService.getStaleRuns(heartbeatTimeoutMs);
19563
19903
  if (staleRuns.length === 0) return;
@@ -19859,6 +20199,7 @@ init_db2();
19859
20199
  init_task_service();
19860
20200
  init_task_run_service();
19861
20201
  init_health();
20202
+ init_process_control();
19862
20203
  var STALE_THRESHOLD_MS = 3e4;
19863
20204
  function acquireLock() {
19864
20205
  const now = Date.now();