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/web/index.js CHANGED
@@ -7247,26 +7247,26 @@ var require_CronDate = __commonJS({
7247
7247
  * @param {CronDate | Date | number | string} [timestamp] - The timestamp to initialize the CronDate with.
7248
7248
  * @param {string} [tz] - The timezone to use for the CronDate.
7249
7249
  */
7250
- constructor(timestamp, tz) {
7250
+ constructor(timestamp2, tz) {
7251
7251
  const dateOpts = { zone: tz };
7252
- if (!timestamp) {
7252
+ if (!timestamp2) {
7253
7253
  this.#date = luxon_1.DateTime.local();
7254
- } else if (timestamp instanceof _CronDate) {
7255
- this.#date = timestamp.#date;
7256
- this.#dstStart = timestamp.#dstStart;
7257
- this.#dstEnd = timestamp.#dstEnd;
7258
- } else if (timestamp instanceof Date) {
7259
- this.#date = luxon_1.DateTime.fromJSDate(timestamp, dateOpts);
7260
- } else if (typeof timestamp === "number") {
7261
- this.#date = luxon_1.DateTime.fromMillis(timestamp, dateOpts);
7254
+ } else if (timestamp2 instanceof _CronDate) {
7255
+ this.#date = timestamp2.#date;
7256
+ this.#dstStart = timestamp2.#dstStart;
7257
+ this.#dstEnd = timestamp2.#dstEnd;
7258
+ } else if (timestamp2 instanceof Date) {
7259
+ this.#date = luxon_1.DateTime.fromJSDate(timestamp2, dateOpts);
7260
+ } else if (typeof timestamp2 === "number") {
7261
+ this.#date = luxon_1.DateTime.fromMillis(timestamp2, dateOpts);
7262
7262
  } else {
7263
- this.#date = luxon_1.DateTime.fromISO(timestamp, dateOpts);
7264
- this.#date.isValid || (this.#date = luxon_1.DateTime.fromRFC2822(timestamp, dateOpts));
7265
- this.#date.isValid || (this.#date = luxon_1.DateTime.fromSQL(timestamp, dateOpts));
7266
- this.#date.isValid || (this.#date = luxon_1.DateTime.fromFormat(timestamp, "EEE, d MMM yyyy HH:mm:ss", dateOpts));
7263
+ this.#date = luxon_1.DateTime.fromISO(timestamp2, dateOpts);
7264
+ this.#date.isValid || (this.#date = luxon_1.DateTime.fromRFC2822(timestamp2, dateOpts));
7265
+ this.#date.isValid || (this.#date = luxon_1.DateTime.fromSQL(timestamp2, dateOpts));
7266
+ this.#date.isValid || (this.#date = luxon_1.DateTime.fromFormat(timestamp2, "EEE, d MMM yyyy HH:mm:ss", dateOpts));
7267
7267
  }
7268
7268
  if (!this.#date.isValid) {
7269
- throw new Error(`CronDate: unhandled timestamp: ${timestamp}`);
7269
+ throw new Error(`CronDate: unhandled timestamp: ${timestamp2}`);
7270
7270
  }
7271
7271
  if (tz && tz !== this.#date.zoneName) {
7272
7272
  this.#date = this.#date.setZone(tz);
@@ -16650,6 +16650,14 @@ function getSqlite() {
16650
16650
  if (!_sqlite) initDb();
16651
16651
  return _sqlite;
16652
16652
  }
16653
+ function closeDb() {
16654
+ if (_sqlite) {
16655
+ _sqlite.close();
16656
+ _sqlite = null;
16657
+ _db = null;
16658
+ _migrationRan = false;
16659
+ }
16660
+ }
16653
16661
  var db = new Proxy({}, {
16654
16662
  get(_, prop) {
16655
16663
  const target = getDb();
@@ -16840,6 +16848,23 @@ var TaskService = class {
16840
16848
  ).returning();
16841
16849
  return result.length;
16842
16850
  }
16851
+ static async resetOrphanRunningToPending() {
16852
+ const result = await db.update(tasks2).set({
16853
+ status: "pending",
16854
+ startedAt: null,
16855
+ finishedAt: null
16856
+ }).where(
16857
+ and(
16858
+ eq(tasks2.status, "running"),
16859
+ sql`NOT EXISTS (
16860
+ SELECT 1 FROM ${taskRuns2}
16861
+ WHERE ${taskRuns2.taskId} = ${tasks2.id}
16862
+ AND ${taskRuns2.status} = 'running'
16863
+ )`
16864
+ )
16865
+ ).returning();
16866
+ return result.length;
16867
+ }
16843
16868
  static async cancel(id, scope = {}) {
16844
16869
  const conditions = [
16845
16870
  eq(tasks2.id, id),
@@ -17190,8 +17215,345 @@ var TaskTemplateService = class {
17190
17215
  }
17191
17216
  };
17192
17217
 
17218
+ // src/core/services/database-maintenance.service.ts
17219
+ import { Database as Database3 } from "bun:sqlite";
17220
+ import { randomUUID } from "crypto";
17221
+ import {
17222
+ chmodSync,
17223
+ constants,
17224
+ copyFileSync,
17225
+ existsSync as existsSync2,
17226
+ mkdirSync as mkdirSync2,
17227
+ renameSync,
17228
+ statSync,
17229
+ unlinkSync,
17230
+ writeFileSync
17231
+ } from "fs";
17232
+ import { tmpdir } from "os";
17233
+ import { basename, dirname as dirname2, resolve } from "path";
17234
+
17235
+ // src/core/process-control.ts
17236
+ function isProcessAlive(pid) {
17237
+ if (!Number.isInteger(pid) || pid <= 0) return false;
17238
+ try {
17239
+ process.kill(pid, 0);
17240
+ return true;
17241
+ } catch (error) {
17242
+ return error instanceof Error && "code" in error && error.code === "EPERM";
17243
+ }
17244
+ }
17245
+
17246
+ // src/core/services/database-maintenance.service.ts
17247
+ var REQUIRED_TABLES = ["gateway_lock", "tasks", "task_runs", "task_templates"];
17248
+ var DatabaseMaintenanceConflictError = class extends Error {
17249
+ constructor(message) {
17250
+ super(message);
17251
+ this.name = "DatabaseMaintenanceConflictError";
17252
+ }
17253
+ };
17254
+ function timestamp() {
17255
+ return (/* @__PURE__ */ new Date()).toISOString().replace(/[-:]/g, "").replace(/\.\d{3}Z$/, "Z");
17256
+ }
17257
+ function normalizedPath(path) {
17258
+ return path === ":memory:" ? path : resolve(path);
17259
+ }
17260
+ function safeUnlink(path) {
17261
+ try {
17262
+ unlinkSync(path);
17263
+ } catch (error) {
17264
+ const code = error instanceof Error && "code" in error ? error.code : void 0;
17265
+ if (code !== "ENOENT") throw error;
17266
+ }
17267
+ }
17268
+ function errorMessage(error) {
17269
+ return error instanceof Error ? error.message : String(error);
17270
+ }
17271
+ var DatabaseMaintenanceService = class {
17272
+ static check() {
17273
+ return this.inspect(getSqlite(), DB_FILE_PATH);
17274
+ }
17275
+ static backup(outputPath) {
17276
+ const sqlite2 = getSqlite();
17277
+ try {
17278
+ sqlite2.query("PRAGMA wal_checkpoint(PASSIVE)").get();
17279
+ } catch {
17280
+ }
17281
+ return this.writeSnapshot(sqlite2, outputPath ?? this.createBackupPath("backup"));
17282
+ }
17283
+ static clear(options = {}) {
17284
+ const sqlite2 = getSqlite();
17285
+ this.assertGatewaySafe(sqlite2, options.allowCurrentGateway ?? false);
17286
+ this.assertNoRunningWork(sqlite2);
17287
+ sqlite2.exec("BEGIN IMMEDIATE");
17288
+ let backup = null;
17289
+ try {
17290
+ this.assertGatewaySafe(sqlite2, options.allowCurrentGateway ?? false);
17291
+ this.assertNoRunningWork(sqlite2);
17292
+ const before = this.readCounts(sqlite2);
17293
+ backup = this.writeSnapshot(sqlite2, this.createBackupPath("pre-clear"));
17294
+ sqlite2.exec("DELETE FROM task_runs");
17295
+ sqlite2.exec("DELETE FROM tasks");
17296
+ sqlite2.exec("DELETE FROM task_templates");
17297
+ sqlite2.exec("COMMIT");
17298
+ return {
17299
+ backupPath: backup.path,
17300
+ deleted: before,
17301
+ check: this.inspect(sqlite2, DB_FILE_PATH)
17302
+ };
17303
+ } catch (error) {
17304
+ if (sqlite2.inTransaction) sqlite2.exec("ROLLBACK");
17305
+ if (error instanceof DatabaseMaintenanceConflictError) throw error;
17306
+ const backupHint = backup ? `\uFF1B\u4E8B\u52A1\u5DF2\u56DE\u6EDA\uFF0C\u5907\u4EFD\u4FDD\u7559\u5728 ${backup.path}` : "";
17307
+ throw new Error(`\u6E05\u7A7A\u6570\u636E\u5E93\u5931\u8D25${backupHint}\uFF1A${errorMessage(error)}`);
17308
+ }
17309
+ }
17310
+ static restore(sourcePath) {
17311
+ if (DB_FILE_PATH === ":memory:") {
17312
+ throw new Error("\u5185\u5B58\u6570\u636E\u5E93\u4E0D\u652F\u6301 restore");
17313
+ }
17314
+ const source = normalizedPath(sourcePath);
17315
+ const livePath = normalizedPath(DB_FILE_PATH);
17316
+ if (source === livePath) throw new Error("\u6062\u590D\u6765\u6E90\u4E0D\u80FD\u662F\u5F53\u524D\u6570\u636E\u5E93\u6587\u4EF6");
17317
+ const current = getSqlite();
17318
+ this.assertGatewaySafe(current, false);
17319
+ this.assertNoRunningWork(current);
17320
+ if (!existsSync2(source) || !statSync(source).isFile()) {
17321
+ throw new Error(`\u5907\u4EFD\u6587\u4EF6\u4E0D\u5B58\u5728\uFF1A${source}`);
17322
+ }
17323
+ let sourceCheck;
17324
+ try {
17325
+ sourceCheck = this.inspectFile(source);
17326
+ } catch (error) {
17327
+ throw new Error(`\u5907\u4EFD\u6587\u4EF6\u65E0\u6548\uFF1A${errorMessage(error)}`);
17328
+ }
17329
+ if (!sourceCheck.ok) {
17330
+ throw new Error(`\u5907\u4EFD\u6587\u4EF6\u6821\u9A8C\u5931\u8D25\uFF1A${sourceCheck.integrityMessages.join("; ") || "schema/foreign key error"}`);
17331
+ }
17332
+ const stagePath = `${livePath}.restore-${process.pid}-${randomUUID()}.tmp`;
17333
+ const rollbackPath = `${livePath}.rollback-${process.pid}-${randomUUID()}.tmp`;
17334
+ let safetyBackup = null;
17335
+ let liveMoved = false;
17336
+ try {
17337
+ copyFileSync(source, stagePath, constants.COPYFILE_EXCL);
17338
+ chmodSync(stagePath, 384);
17339
+ const staged = new Database3(stagePath);
17340
+ try {
17341
+ const stagedCheck = this.inspect(staged, stagePath);
17342
+ if (!stagedCheck.ok) throw new Error("\u6682\u5B58\u6062\u590D\u6587\u4EF6\u6821\u9A8C\u5931\u8D25");
17343
+ const now = Date.now();
17344
+ staged.query(
17345
+ "INSERT OR REPLACE INTO gateway_lock (id, pid, acquired_at, heartbeat_at, ready_at) VALUES (1, ?, ?, ?, NULL)"
17346
+ ).run(process.pid, now, now);
17347
+ } finally {
17348
+ staged.close();
17349
+ }
17350
+ const sqlite2 = getSqlite();
17351
+ sqlite2.exec("BEGIN IMMEDIATE");
17352
+ try {
17353
+ this.assertGatewaySafe(sqlite2, false);
17354
+ this.assertNoRunningWork(sqlite2);
17355
+ safetyBackup = this.writeSnapshot(sqlite2, this.createBackupPath("pre-restore"));
17356
+ const now = Date.now();
17357
+ sqlite2.query(
17358
+ "INSERT OR REPLACE INTO gateway_lock (id, pid, acquired_at, heartbeat_at, ready_at) VALUES (1, ?, ?, ?, NULL)"
17359
+ ).run(process.pid, now, now);
17360
+ sqlite2.exec("COMMIT");
17361
+ } catch (error) {
17362
+ if (sqlite2.inTransaction) sqlite2.exec("ROLLBACK");
17363
+ throw error;
17364
+ }
17365
+ const checkpoint = sqlite2.query("PRAGMA wal_checkpoint(TRUNCATE)").get();
17366
+ if (checkpoint && checkpoint.busy !== 0) {
17367
+ throw new DatabaseMaintenanceConflictError("\u6570\u636E\u5E93\u4ECD\u88AB\u5176\u4ED6\u8FDE\u63A5\u5360\u7528\uFF0C\u65E0\u6CD5\u5B89\u5168\u6062\u590D");
17368
+ }
17369
+ closeDb();
17370
+ safeUnlink(`${livePath}-wal`);
17371
+ safeUnlink(`${livePath}-shm`);
17372
+ renameSync(livePath, rollbackPath);
17373
+ liveMoved = true;
17374
+ renameSync(stagePath, livePath);
17375
+ const restored = getSqlite();
17376
+ restored.exec("BEGIN IMMEDIATE");
17377
+ let recoveredRunningTasks = 0;
17378
+ let closedRunningRuns = 0;
17379
+ try {
17380
+ recoveredRunningTasks = this.scalar(
17381
+ restored,
17382
+ "SELECT COUNT(*) AS count FROM tasks WHERE status = 'running'"
17383
+ );
17384
+ closedRunningRuns = this.scalar(
17385
+ restored,
17386
+ "SELECT COUNT(*) AS count FROM task_runs WHERE status = 'running'"
17387
+ );
17388
+ const finishedAt = Math.floor(Date.now() / 1e3);
17389
+ restored.query(`
17390
+ UPDATE task_runs
17391
+ SET status = 'failed', finished_at = ?,
17392
+ log = CASE
17393
+ WHEN log IS NULL OR log = '' THEN '\u6570\u636E\u5E93\u6062\u590D\u65F6\u5173\u95ED\u9057\u7559\u8FD0\u884C\u8BB0\u5F55'
17394
+ ELSE log || '
17395
+ \u6570\u636E\u5E93\u6062\u590D\u65F6\u5173\u95ED\u9057\u7559\u8FD0\u884C\u8BB0\u5F55'
17396
+ END
17397
+ WHERE status = 'running'
17398
+ `).run(finishedAt);
17399
+ restored.exec(`
17400
+ UPDATE tasks
17401
+ SET status = 'pending', started_at = NULL, finished_at = NULL
17402
+ WHERE status = 'running'
17403
+ `);
17404
+ restored.query("DELETE FROM gateway_lock WHERE pid = ?").run(process.pid);
17405
+ restored.exec("COMMIT");
17406
+ } catch (error) {
17407
+ if (restored.inTransaction) restored.exec("ROLLBACK");
17408
+ throw error;
17409
+ }
17410
+ const check = this.inspect(restored, livePath);
17411
+ if (!check.ok) throw new Error("\u6062\u590D\u540E\u7684\u6570\u636E\u5E93\u672A\u901A\u8FC7\u5B8C\u6574\u6027\u6821\u9A8C");
17412
+ safeUnlink(rollbackPath);
17413
+ return {
17414
+ sourcePath: source,
17415
+ safetyBackupPath: safetyBackup.path,
17416
+ recoveredRunningTasks,
17417
+ closedRunningRuns,
17418
+ check
17419
+ };
17420
+ } catch (error) {
17421
+ closeDb();
17422
+ safeUnlink(`${livePath}-wal`);
17423
+ safeUnlink(`${livePath}-shm`);
17424
+ safeUnlink(stagePath);
17425
+ safeUnlink(`${stagePath}-journal`);
17426
+ safeUnlink(`${stagePath}-wal`);
17427
+ safeUnlink(`${stagePath}-shm`);
17428
+ if (liveMoved && existsSync2(rollbackPath)) {
17429
+ safeUnlink(livePath);
17430
+ renameSync(rollbackPath, livePath);
17431
+ try {
17432
+ const original = getSqlite();
17433
+ original.query("DELETE FROM gateway_lock WHERE pid = ?").run(process.pid);
17434
+ } catch {
17435
+ }
17436
+ } else if (existsSync2(rollbackPath)) {
17437
+ safeUnlink(rollbackPath);
17438
+ } else {
17439
+ try {
17440
+ getSqlite().query("DELETE FROM gateway_lock WHERE pid = ?").run(process.pid);
17441
+ } catch {
17442
+ }
17443
+ }
17444
+ const backupHint = safetyBackup ? `\uFF1B\u5F53\u524D\u5E93\u5B89\u5168\u5907\u4EFD\uFF1A${safetyBackup.path}` : "";
17445
+ throw new Error(`\u6062\u590D\u6570\u636E\u5E93\u5931\u8D25\uFF0C\u5DF2\u5C1D\u8BD5\u56DE\u6EDA${backupHint}\uFF1A${errorMessage(error)}`);
17446
+ }
17447
+ }
17448
+ static inspectFile(path) {
17449
+ let sqlite2;
17450
+ try {
17451
+ sqlite2 = new Database3(path, { readonly: true, strict: true });
17452
+ } catch (error) {
17453
+ throw new Error(`\u65E0\u6CD5\u6253\u5F00\u5907\u4EFD\u6587\u4EF6\uFF1A${errorMessage(error)}`);
17454
+ }
17455
+ try {
17456
+ return this.inspect(sqlite2, path);
17457
+ } finally {
17458
+ sqlite2.close();
17459
+ }
17460
+ }
17461
+ static inspect(sqlite2, path) {
17462
+ const tableRows = sqlite2.query(
17463
+ "SELECT name FROM sqlite_master WHERE type = 'table'"
17464
+ ).all();
17465
+ const tableNames = new Set(tableRows.map((row) => row.name));
17466
+ const missingTables = REQUIRED_TABLES.filter((table) => !tableNames.has(table));
17467
+ const integrityRows = sqlite2.query("PRAGMA integrity_check").all();
17468
+ const integrityMessages = integrityRows.map((row) => row.integrity_check);
17469
+ const foreignKeyViolations = sqlite2.query("PRAGMA foreign_key_check").all().length;
17470
+ const journal = sqlite2.query("PRAGMA journal_mode").get();
17471
+ const counts = missingTables.length === 0 ? this.readCounts(sqlite2) : { tasks: 0, taskRuns: 0, taskTemplates: 0 };
17472
+ const runningTasks = missingTables.includes("tasks") ? 0 : this.scalar(sqlite2, "SELECT COUNT(*) AS count FROM tasks WHERE status = 'running'");
17473
+ const runningRuns = missingTables.includes("task_runs") ? 0 : this.scalar(sqlite2, "SELECT COUNT(*) AS count FROM task_runs WHERE status = 'running'");
17474
+ const sizeBytes = path === ":memory:" || !existsSync2(path) ? sqlite2.serialize().byteLength : statSync(path).size;
17475
+ return {
17476
+ ok: integrityMessages.length === 1 && integrityMessages[0] === "ok" && foreignKeyViolations === 0 && missingTables.length === 0,
17477
+ path: normalizedPath(path),
17478
+ sizeBytes,
17479
+ journalMode: journal?.journal_mode ?? "unknown",
17480
+ integrityMessages,
17481
+ foreignKeyViolations,
17482
+ missingTables,
17483
+ counts,
17484
+ runningTasks,
17485
+ runningRuns
17486
+ };
17487
+ }
17488
+ static writeSnapshot(sqlite2, outputPath) {
17489
+ const liveCheck = this.inspect(sqlite2, DB_FILE_PATH);
17490
+ 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");
17491
+ const output = normalizedPath(outputPath);
17492
+ if (DB_FILE_PATH !== ":memory:" && output === normalizedPath(DB_FILE_PATH)) {
17493
+ throw new Error("\u5907\u4EFD\u8DEF\u5F84\u4E0D\u80FD\u8986\u76D6\u5F53\u524D\u6570\u636E\u5E93");
17494
+ }
17495
+ if (existsSync2(output)) throw new Error(`\u5907\u4EFD\u6587\u4EF6\u5DF2\u5B58\u5728\uFF1A${output}`);
17496
+ mkdirSync2(dirname2(output), { recursive: true });
17497
+ const temporary = `${output}.tmp-${process.pid}-${randomUUID()}`;
17498
+ try {
17499
+ writeFileSync(temporary, sqlite2.serialize(), { flag: "wx", mode: 384 });
17500
+ const standalone = new Database3(temporary, { readwrite: true, create: false });
17501
+ try {
17502
+ standalone.query("PRAGMA journal_mode = DELETE").get();
17503
+ } finally {
17504
+ standalone.close();
17505
+ }
17506
+ safeUnlink(`${temporary}-wal`);
17507
+ safeUnlink(`${temporary}-shm`);
17508
+ const check = this.inspectFile(temporary);
17509
+ if (!check.ok) throw new Error("\u65B0\u5907\u4EFD\u672A\u901A\u8FC7\u5B8C\u6574\u6027\u6821\u9A8C");
17510
+ renameSync(temporary, output);
17511
+ const finalCheck = { ...check, path: output, sizeBytes: statSync(output).size };
17512
+ return { path: output, sizeBytes: finalCheck.sizeBytes, check: finalCheck };
17513
+ } catch (error) {
17514
+ safeUnlink(temporary);
17515
+ safeUnlink(`${temporary}-wal`);
17516
+ safeUnlink(`${temporary}-shm`);
17517
+ throw error;
17518
+ }
17519
+ }
17520
+ static createBackupPath(kind) {
17521
+ const directory = DB_FILE_PATH === ":memory:" ? tmpdir() : dirname2(normalizedPath(DB_FILE_PATH));
17522
+ const base = DB_FILE_PATH === ":memory:" ? "supertask-memory" : basename(DB_FILE_PATH, ".db");
17523
+ return resolve(directory, `${base}.${kind}-${timestamp()}-${randomUUID().slice(0, 8)}.db`);
17524
+ }
17525
+ static readCounts(sqlite2) {
17526
+ return {
17527
+ tasks: this.scalar(sqlite2, "SELECT COUNT(*) AS count FROM tasks"),
17528
+ taskRuns: this.scalar(sqlite2, "SELECT COUNT(*) AS count FROM task_runs"),
17529
+ taskTemplates: this.scalar(sqlite2, "SELECT COUNT(*) AS count FROM task_templates")
17530
+ };
17531
+ }
17532
+ static scalar(sqlite2, statement) {
17533
+ const row = sqlite2.query(statement).get();
17534
+ return Number(row?.count ?? 0);
17535
+ }
17536
+ static assertGatewaySafe(sqlite2, allowCurrentGateway) {
17537
+ const lock = sqlite2.query("SELECT pid FROM gateway_lock WHERE id = 1").get();
17538
+ if (!lock || !isProcessAlive(lock.pid)) return;
17539
+ if (allowCurrentGateway && lock.pid === process.pid) return;
17540
+ throw new DatabaseMaintenanceConflictError(
17541
+ `Gateway PID ${lock.pid} \u4ECD\u5728\u8FD0\u884C\uFF0C\u8BF7\u5148\u6267\u884C pm2 stop supertask-gateway`
17542
+ );
17543
+ }
17544
+ static assertNoRunningWork(sqlite2) {
17545
+ const runningTasks = this.scalar(sqlite2, "SELECT COUNT(*) AS count FROM tasks WHERE status = 'running'");
17546
+ const runningRuns = this.scalar(sqlite2, "SELECT COUNT(*) AS count FROM task_runs WHERE status = 'running'");
17547
+ if (runningTasks > 0 || runningRuns > 0) {
17548
+ throw new DatabaseMaintenanceConflictError(
17549
+ `\u5B58\u5728\u8FD0\u884C\u4E2D\u4EFB\u52A1\uFF08tasks=${runningTasks}, task_runs=${runningRuns}\uFF09\uFF0C\u5DF2\u62D2\u7EDD\u5371\u9669\u64CD\u4F5C`
17550
+ );
17551
+ }
17552
+ }
17553
+ };
17554
+
17193
17555
  // src/gateway/config.ts
17194
- import { existsSync as existsSync2, readFileSync } from "fs";
17556
+ import { existsSync as existsSync3, readFileSync } from "fs";
17195
17557
  import { homedir as homedir2 } from "os";
17196
17558
  import { join as join2 } from "path";
17197
17559
  var DEFAULT_CONFIG = {
@@ -17296,7 +17658,7 @@ function validateConfig(input) {
17296
17658
  };
17297
17659
  }
17298
17660
  function loadConfig(path = getConfigPath()) {
17299
- if (!existsSync2(path)) return validateConfig({ configVersion: 2 });
17661
+ if (!existsSync3(path)) return validateConfig({ configVersion: 2 });
17300
17662
  try {
17301
17663
  return validateConfig(JSON.parse(readFileSync(path, "utf-8")));
17302
17664
  } catch (error) {
@@ -17306,8 +17668,8 @@ function loadConfig(path = getConfigPath()) {
17306
17668
  }
17307
17669
 
17308
17670
  // src/web/index.tsx
17309
- import { existsSync as existsSync3, readFileSync as readFileSync2, writeFileSync, mkdirSync as mkdirSync2, renameSync } from "fs";
17310
- import { dirname as dirname2 } from "path";
17671
+ import { existsSync as existsSync4, readFileSync as readFileSync2, writeFileSync as writeFileSync2, mkdirSync as mkdirSync3, renameSync as renameSync2 } from "fs";
17672
+ import { dirname as dirname3 } from "path";
17311
17673
 
17312
17674
  // src/gateway/health.ts
17313
17675
  var LOCK_STALE_MS = 3e4;
@@ -17459,7 +17821,7 @@ function esc(s) {
17459
17821
  }
17460
17822
  function readCurrentConfig() {
17461
17823
  const configPath = getConfigPath();
17462
- if (!existsSync3(configPath)) return {};
17824
+ if (!existsSync4(configPath)) return {};
17463
17825
  try {
17464
17826
  return JSON.parse(readFileSync2(configPath, "utf-8"));
17465
17827
  } catch {
@@ -17468,11 +17830,11 @@ function readCurrentConfig() {
17468
17830
  }
17469
17831
  function writeConfig(cfg) {
17470
17832
  const configPath = getConfigPath();
17471
- const dir = dirname2(configPath);
17472
- if (!existsSync3(dir)) mkdirSync2(dir, { recursive: true });
17833
+ const dir = dirname3(configPath);
17834
+ if (!existsSync4(dir)) mkdirSync3(dir, { recursive: true });
17473
17835
  const tempPath = `${configPath}.${process.pid}.tmp`;
17474
- writeFileSync(tempPath, JSON.stringify(cfg, null, 2) + "\n", { mode: 384 });
17475
- renameSync(tempPath, configPath);
17836
+ writeFileSync2(tempPath, JSON.stringify(cfg, null, 2) + "\n", { mode: 384 });
17837
+ renameSync2(tempPath, configPath);
17476
17838
  }
17477
17839
  var SHARED_STYLES = html`
17478
17840
  <style>
@@ -17564,12 +17926,12 @@ async function triggerTmpl(id){if(!confirm('\u7ACB\u5373\u89E6\u53D1\u4E00\u6B21
17564
17926
  function toggleLog(id){const el=document.getElementById('log-'+id);el.style.display=el.style.display==='none'?'block':'none';}
17565
17927
 
17566
17928
  async function clearDatabase(){
17567
- if(!confirm('\u786E\u5B9A\u6E05\u7A7A\u6240\u6709\u4EFB\u52A1\u6570\u636E\uFF1F\u6B64\u64CD\u4F5C\u4E0D\u53EF\u6062\u590D\uFF01'))return;
17568
- 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;
17929
+ 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;
17930
+ 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;
17569
17931
  try{
17570
- const r=await fetch('/api/database/clear',{method:'POST'});
17932
+ const r=await fetch('/api/database/clear',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({confirmation:'CLEAR'})});
17571
17933
  const d=await r.json();
17572
- if(d.success){alert('\u6570\u636E\u5E93\u5DF2\u6E05\u7A7A');location.reload();}
17934
+ if(d.success){alert('\u6570\u636E\u5E93\u5DF2\u6E05\u7A7A\uFF0C\u81EA\u52A8\u5907\u4EFD\uFF1A'+d.backupPath);location.reload();}
17573
17935
  else{alert('\u6E05\u7A7A\u5931\u8D25: '+d.error);}
17574
17936
  }catch(e){alert('\u6E05\u7A7A\u5931\u8D25: '+e.message);}
17575
17937
  }
@@ -17810,7 +18172,7 @@ app.get("/system", async (c) => {
17810
18172
  const stats = await TaskService.stats({});
17811
18173
  const runningRuns = await TaskRunService.getAllRunningRuns();
17812
18174
  const templates = await TaskTemplateService.list(100);
17813
- const configExists = existsSync3(configPath);
18175
+ const configExists = existsSync4(configPath);
17814
18176
  let runRows = "";
17815
18177
  if (runningRuns.length > 0) {
17816
18178
  for (const run of runningRuns) {
@@ -17884,7 +18246,7 @@ app.get("/system", async (c) => {
17884
18246
 
17885
18247
  <div class="card mt16" style="border-color:var(--red)">
17886
18248
  <h3 style="margin:0 0 12px;font-size:14px;color:var(--red)">\u5371\u9669\u64CD\u4F5C</h3>
17887
- <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>
18249
+ <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>
17888
18250
  <button class="btn btn-danger" style="border-color:var(--red);color:var(--red);padding:6px 16px" onclick="clearDatabase()">\u6E05\u7A7A\u6570\u636E\u5E93</button>
17889
18251
  </div>`;
17890
18252
  return c.html(renderLayout("\u7CFB\u7EDF\u72B6\u6001", "system", body));
@@ -17989,14 +18351,16 @@ app.put("/api/config", async (c) => {
17989
18351
  }
17990
18352
  });
17991
18353
  app.post("/api/database/clear", async (c) => {
18354
+ const body = await c.req.json().catch(() => ({}));
18355
+ if (body.confirmation !== "CLEAR") {
18356
+ return c.json({ success: false, error: "confirmation must be CLEAR" }, 400);
18357
+ }
17992
18358
  try {
17993
- const { tasks: tasks3, taskRuns: taskRuns4, taskTemplates: taskTemplates3 } = schema_exports;
17994
- await db.delete(taskRuns4);
17995
- await db.delete(taskTemplates3);
17996
- await db.delete(tasks3);
17997
- return c.json({ success: true });
18359
+ const result = DatabaseMaintenanceService.clear({ allowCurrentGateway: true });
18360
+ return c.json({ success: true, ...result });
17998
18361
  } catch (err) {
17999
- return c.json({ success: false, error: err instanceof Error ? err.message : String(err) }, 500);
18362
+ const status = err instanceof DatabaseMaintenanceConflictError ? 409 : 500;
18363
+ return c.json({ success: false, error: err instanceof Error ? err.message : String(err) }, status);
18000
18364
  }
18001
18365
  });
18002
18366
  var dashboardApp = app;