opencode-supertask 0.1.40 → 0.1.41

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
@@ -9100,8 +9100,8 @@ var require_CronFileParser = __commonJS({
9100
9100
  * @throws If file cannot be read
9101
9101
  */
9102
9102
  static parseFileSync(filePath) {
9103
- const { readFileSync: readFileSync4 } = __require("fs");
9104
- const data = readFileSync4(filePath, "utf8");
9103
+ const { readFileSync: readFileSync3 } = __require("fs");
9104
+ const data = readFileSync3(filePath, "utf8");
9105
9105
  return _CronFileParser.#parseContent(data);
9106
9106
  }
9107
9107
  /**
@@ -13085,14 +13085,14 @@ function mapRelationalRow(tablesConfig, tableConfig, row, buildQueryResultSelect
13085
13085
  // src/web/index.tsx
13086
13086
  import {
13087
13087
  existsSync as existsSync5,
13088
- mkdirSync as mkdirSync4,
13089
- readFileSync as readFileSync3,
13088
+ mkdirSync as mkdirSync3,
13089
+ readFileSync as readFileSync2,
13090
13090
  readdirSync,
13091
13091
  renameSync as renameSync2,
13092
- statSync as statSync4,
13093
- writeFileSync as writeFileSync3
13092
+ statSync as statSync3,
13093
+ writeFileSync as writeFileSync2
13094
13094
  } from "fs";
13095
- import { homedir as homedir4 } from "os";
13095
+ import { homedir as homedir3 } from "os";
13096
13096
  import { basename as basename2, dirname as dirname4, join as join4 } from "path";
13097
13097
 
13098
13098
  // src/core/db/index.ts
@@ -16502,8 +16502,8 @@ var _sqlite = null;
16502
16502
  var _db = null;
16503
16503
  var _migrationRan = false;
16504
16504
  function getMigrationsFolder() {
16505
- const __dirname2 = dirname(fileURLToPath(import.meta.url));
16506
- let dir = __dirname2;
16505
+ const __dirname = dirname(fileURLToPath(import.meta.url));
16506
+ let dir = __dirname;
16507
16507
  for (let i = 0; i < 5; i++) {
16508
16508
  const candidate = join(dir, "drizzle", "meta", "_journal.json");
16509
16509
  if (existsSync(candidate)) {
@@ -16511,7 +16511,7 @@ function getMigrationsFolder() {
16511
16511
  }
16512
16512
  dir = dirname(dir);
16513
16513
  }
16514
- return join(__dirname2, "../../drizzle");
16514
+ return join(__dirname, "../../drizzle");
16515
16515
  }
16516
16516
  function ensureGatewayLock(sqliteDb) {
16517
16517
  sqliteDb.exec(`
@@ -16667,7 +16667,7 @@ function cleanOutput(value) {
16667
16667
  return value.replace(ANSI_PATTERN, "").replace(/\r/g, "");
16668
16668
  }
16669
16669
  function runOpenCode(executable, args, cwd, timeoutMs) {
16670
- return new Promise((resolve3, reject) => {
16670
+ return new Promise((resolve2, reject) => {
16671
16671
  const child = spawn(executable, args, {
16672
16672
  cwd,
16673
16673
  env: process.env,
@@ -16745,7 +16745,7 @@ function runOpenCode(executable, args, cwd, timeoutMs) {
16745
16745
  reject(new OpenCodeCommandExitError(`OpenCode ${args.join(" ")} \u5931\u8D25\uFF1A${detail}`));
16746
16746
  return;
16747
16747
  }
16748
- resolve3(cleanOutput(stdout));
16748
+ resolve2(cleanOutput(stdout));
16749
16749
  });
16750
16750
  });
16751
16751
  }
@@ -16816,9 +16816,9 @@ async function loadOpenCodeCatalog(cwd, options = {}) {
16816
16816
  const executable = options.executable ?? process.env.SUPERTASK_OPENCODE_BIN ?? "opencode";
16817
16817
  const timeoutMs = options.timeoutMs ?? COMMAND_TIMEOUT_MS;
16818
16818
  const cacheKey = `${executable}\0${cwd}`;
16819
- const cached = catalogCache.get(cacheKey);
16820
- if (options.useCache !== false && cached && cached.expiresAt > Date.now()) {
16821
- return cached.result;
16819
+ const cached2 = catalogCache.get(cacheKey);
16820
+ if (options.useCache !== false && cached2 && cached2.expiresAt > Date.now()) {
16821
+ return cached2.result;
16822
16822
  }
16823
16823
  const result = Promise.all([
16824
16824
  runOpenCode(executable, ["models", "--verbose"], cwd, timeoutMs).catch((error) => {
@@ -16859,6 +16859,59 @@ import { tmpdir } from "os";
16859
16859
  import { basename, dirname as dirname2, resolve } from "path";
16860
16860
 
16861
16861
  // src/core/process-control.ts
16862
+ var OS_COMMAND_TIMEOUT_MS = 2e3;
16863
+ var OS_COMMAND_MAX_OUTPUT_BYTES = 4 * 1024 * 1024;
16864
+ var OS_COMMAND_RESULT_MARKER = "\n\0supertask-os-command-result:";
16865
+ var BOUNDED_OS_COMMAND_RUNNER = `
16866
+ const { writeSync } = await import('fs');
16867
+ let child;
16868
+ try {
16869
+ child = Bun.spawn(process.argv.slice(2), {
16870
+ stdin: 'ignore',
16871
+ stdout: 'pipe',
16872
+ stderr: 'ignore',
16873
+ });
16874
+ } catch {
16875
+ process.exit(127);
16876
+ }
16877
+ const terminateGroup = () => {
16878
+ if (process.platform !== 'win32') {
16879
+ try {
16880
+ process.kill(-process.pid, 'SIGKILL');
16881
+ } catch {
16882
+ }
16883
+ }
16884
+ try {
16885
+ child.kill('SIGKILL');
16886
+ } catch {
16887
+ }
16888
+ process.exit(1);
16889
+ };
16890
+ const timer = setTimeout(terminateGroup, ${OS_COMMAND_TIMEOUT_MS});
16891
+ try {
16892
+ const chunks = [];
16893
+ let outputBytes = 0;
16894
+ const readOutput = async () => {
16895
+ const reader = child.stdout.getReader();
16896
+ while (true) {
16897
+ const { done, value } = await reader.read();
16898
+ if (done) return;
16899
+ outputBytes += value.byteLength;
16900
+ if (outputBytes > ${OS_COMMAND_MAX_OUTPUT_BYTES}) terminateGroup();
16901
+ chunks.push(value);
16902
+ }
16903
+ };
16904
+ const [exitCode] = await Promise.all([child.exited, readOutput()]);
16905
+ clearTimeout(timer);
16906
+ for (const chunk of chunks) writeSync(1, chunk);
16907
+ writeSync(1, ${JSON.stringify(OS_COMMAND_RESULT_MARKER)} + String(exitCode));
16908
+ if (process.platform !== 'win32') terminateGroup();
16909
+ process.exit(0);
16910
+ } catch {
16911
+ clearTimeout(timer);
16912
+ terminateGroup();
16913
+ }
16914
+ `;
16862
16915
  function isProcessAlive(pid) {
16863
16916
  if (!Number.isInteger(pid) || pid <= 0) return false;
16864
16917
  try {
@@ -17425,7 +17478,7 @@ var TaskRunService = class {
17425
17478
  }
17426
17479
  if (current.childPid != null) {
17427
17480
  throw new LegacyRunAbandonConflictError(
17428
- `run #${runId} \u5DF2\u8BB0\u5F55 child PID ${current.childPid}\uFF0C\u5FC5\u987B\u7531 Worker/Watchdog \u786E\u8BA4\u8FDB\u7A0B\u6811\u9000\u51FA`
17481
+ `run #${runId} \u5DF2\u8BB0\u5F55 child PID ${current.childPid}\uFF0C\u5FC5\u987B\u7531 Worker/Watchdog \u786E\u8BA4\u53D7\u7BA1\u8FDB\u7A0B\u7EC4\u6392\u7A7A`
17429
17482
  );
17430
17483
  }
17431
17484
  if (current.workerPid != null && isProcessAlive(current.workerPid)) {
@@ -17623,13 +17676,23 @@ var TaskService = class {
17623
17676
  });
17624
17677
  const maxRetries = normalizedData.maxRetries ?? task.maxRetries ?? 3;
17625
17678
  const exhausted = task.status === "failed" && (task.retryCount ?? 0) > maxRetries;
17626
- return tx.update(tasks3).set({
17679
+ const updated = tx.update(tasks3).set({
17627
17680
  ...normalizedData,
17628
17681
  ...exhausted ? {
17629
17682
  status: "dead_letter",
17630
17683
  retryAfter: null
17631
17684
  } : {}
17632
17685
  }).where(eq(tasks3.id, id)).returning().get() ?? null;
17686
+ if (exhausted && updated) {
17687
+ const finishedAt = /* @__PURE__ */ new Date();
17688
+ tx.update(tasks3).set({
17689
+ status: "dead_letter",
17690
+ finishedAt,
17691
+ retryAfter: null,
17692
+ resultLog: `\u4F9D\u8D56\u4EFB\u52A1 #${id} \u5DF2\u8FDB\u5165\u4E0D\u53EF\u6062\u590D\u7EC8\u6001`
17693
+ }).where(blockedDependentsOf(id)).run();
17694
+ }
17695
+ return updated;
17633
17696
  }, { behavior: "immediate" });
17634
17697
  }
17635
17698
  static validateNewTask(data) {
@@ -17650,7 +17713,7 @@ var TaskService = class {
17650
17713
  throw new Error(`${name} \u5FC5\u987B\u662F ${min} \u5230 ${max} \u4E4B\u95F4\u7684\u6574\u6570`);
17651
17714
  }
17652
17715
  }
17653
- static async next(scope = {}) {
17716
+ static buildRunnableTaskWhere(scope) {
17654
17717
  const baseConditions = [...this.buildScopeWhere(scope)];
17655
17718
  const nowMs = Date.now();
17656
17719
  const retryAfterFilter = or(
@@ -17685,40 +17748,43 @@ var TaskService = class {
17685
17748
  if (batchFilter) {
17686
17749
  conditions.push(batchFilter);
17687
17750
  }
17688
- const result = await db.select().from(tasks3).where(and(
17751
+ return and(
17689
17752
  ...conditions,
17690
17753
  or(
17691
17754
  isNull(tasks3.dependsOn),
17692
17755
  sql`EXISTS (
17693
- SELECT 1 FROM tasks AS dependency_task
17694
- WHERE dependency_task.id = ${tasks3.dependsOn}
17695
- AND dependency_task.status = 'done'
17696
- AND dependency_task.cwd IS ${tasks3.cwd}
17697
- )`
17756
+ SELECT 1 FROM tasks AS dependency_task
17757
+ WHERE dependency_task.id = ${tasks3.dependsOn}
17758
+ AND dependency_task.status = 'done'
17759
+ AND dependency_task.cwd IS ${tasks3.cwd}
17760
+ )`
17698
17761
  ),
17699
17762
  or(
17700
17763
  isNull(tasks3.batchId),
17701
17764
  sql`trim(${tasks3.batchId}, ${TASK_BATCH_TRIM_CHARACTERS}) = ''`,
17702
17765
  sql`NOT EXISTS (
17703
- SELECT 1 FROM tasks AS running_batch_task
17704
- WHERE trim(running_batch_task.batch_id, ${TASK_BATCH_TRIM_CHARACTERS})
17705
- = trim(${tasks3.batchId}, ${TASK_BATCH_TRIM_CHARACTERS})
17706
- AND (
17707
- running_batch_task.status = 'running'
17708
- OR EXISTS (
17709
- SELECT 1 FROM task_runs AS running_batch_run
17710
- WHERE running_batch_run.task_id = running_batch_task.id
17711
- AND running_batch_run.status = 'running'
17712
- )
17766
+ SELECT 1 FROM tasks AS running_batch_task
17767
+ WHERE trim(running_batch_task.batch_id, ${TASK_BATCH_TRIM_CHARACTERS})
17768
+ = trim(${tasks3.batchId}, ${TASK_BATCH_TRIM_CHARACTERS})
17769
+ AND (
17770
+ running_batch_task.status = 'running'
17771
+ OR EXISTS (
17772
+ SELECT 1 FROM task_runs AS running_batch_run
17773
+ WHERE running_batch_run.task_id = running_batch_task.id
17774
+ AND running_batch_run.status = 'running'
17713
17775
  )
17714
- )`
17776
+ )
17777
+ )`
17715
17778
  ),
17716
17779
  sql`NOT EXISTS (
17717
- SELECT 1 FROM task_runs AS candidate_active_run
17718
- WHERE candidate_active_run.task_id = ${tasks3.id}
17719
- AND candidate_active_run.status = 'running'
17720
- )`
17721
- )).orderBy(
17780
+ SELECT 1 FROM task_runs AS candidate_active_run
17781
+ WHERE candidate_active_run.task_id = ${tasks3.id}
17782
+ AND candidate_active_run.status = 'running'
17783
+ )`
17784
+ );
17785
+ }
17786
+ static async next(scope = {}) {
17787
+ const result = await db.select().from(tasks3).where(this.buildRunnableTaskWhere(scope)).orderBy(
17722
17788
  desc(tasks3.urgency),
17723
17789
  desc(tasks3.importance),
17724
17790
  asc(tasks3.createdAt),
@@ -17726,6 +17792,22 @@ var TaskService = class {
17726
17792
  ).limit(1);
17727
17793
  return result[0] ?? null;
17728
17794
  }
17795
+ static async claimNext(scope = {}) {
17796
+ return db.transaction((tx) => {
17797
+ const candidate = tx.select().from(tasks3).where(this.buildRunnableTaskWhere(scope)).orderBy(
17798
+ desc(tasks3.urgency),
17799
+ desc(tasks3.importance),
17800
+ asc(tasks3.createdAt),
17801
+ asc(tasks3.id)
17802
+ ).limit(1).get();
17803
+ if (!candidate) return null;
17804
+ return tx.update(tasks3).set({
17805
+ status: "running",
17806
+ startedAt: /* @__PURE__ */ new Date(),
17807
+ finishedAt: null
17808
+ }).where(eq(tasks3.id, candidate.id)).returning().get() ?? null;
17809
+ }, { behavior: "immediate" });
17810
+ }
17729
17811
  static async countRunning(scope = {}) {
17730
17812
  const scopeConditions = scope.legacyCwd ? [sql`${tasks3.cwd} IS NULL OR trim(${tasks3.cwd}) = ''`] : this.buildScopeWhere(scope);
17731
17813
  if (scope.batchId !== void 0) {
@@ -18818,452 +18900,101 @@ function createTaskFromTemplate(templateId, options) {
18818
18900
  }, { behavior: "immediate" });
18819
18901
  }
18820
18902
 
18821
- // src/daemon/pm2.ts
18822
- import { execSync, spawnSync } from "child_process";
18823
- import {
18824
- accessSync,
18825
- chmodSync as chmodSync2,
18826
- constants,
18827
- existsSync as existsSync4,
18828
- mkdirSync as mkdirSync3,
18829
- readFileSync as readFileSync2,
18830
- rmSync,
18831
- statSync as statSync3,
18832
- writeFileSync as writeFileSync2
18833
- } from "fs";
18834
- import { homedir as homedir3, userInfo } from "os";
18835
- import { delimiter, dirname as dirname3, isAbsolute as isAbsolute2, join as join3, resolve as resolve2 } from "path";
18903
+ // src/web/gateway-diagnostic.ts
18904
+ import { spawn as spawn2 } from "child_process";
18905
+ import { existsSync as existsSync4 } from "fs";
18906
+ import { dirname as dirname3, join as join3 } from "path";
18836
18907
  import { fileURLToPath as fileURLToPath2 } from "url";
18837
- import { Database as Database5 } from "bun:sqlite";
18838
-
18839
- // src/daemon/management-lock.ts
18840
- import { Database as Database4 } from "bun:sqlite";
18841
-
18842
- // src/daemon/pm2.ts
18843
- var __dirname = dirname3(fileURLToPath2(import.meta.url));
18844
- var PROCESS_NAME = "supertask-gateway";
18845
- var MAC_LAUNCH_AGENT_LABEL = "com.supertask.pm2-resurrect";
18846
- var GATEWAY_LOCK_STALE_MS = 3e4;
18847
- function runtimeHome(env) {
18848
- return resolve2(env.HOME || homedir3());
18849
- }
18850
- function runtimePath(value, cwd) {
18851
- return resolve2(cwd, value);
18852
- }
18853
- function versionFile(env = process.env, cwd = process.cwd()) {
18854
- return env.SUPERTASK_VERSION_FILE ? runtimePath(env.SUPERTASK_VERSION_FILE, cwd) : join3(runtimeHome(env), ".local/share/opencode/supertask-gateway-version");
18855
- }
18856
- function getRunningVersion(env = process.env, cwd = process.cwd()) {
18857
- try {
18858
- const path = versionFile(env, cwd);
18859
- if (!existsSync4(path)) return null;
18860
- return readFileSync2(path, "utf-8").trim() || null;
18861
- } catch {
18862
- return null;
18863
- }
18864
- }
18865
- function packageVersionFromGatewayEntry(gatewayEntry) {
18866
- if (gatewayEntry === null) return null;
18867
- let directory = dirname3(gatewayEntry);
18868
- for (let depth = 0; depth < 6; depth += 1) {
18869
- const packagePath = join3(directory, "package.json");
18870
- if (existsSync4(packagePath)) {
18871
- try {
18872
- const pkg = JSON.parse(readFileSync2(packagePath, "utf8"));
18873
- if (pkg.name === "opencode-supertask" && typeof pkg.version === "string") {
18874
- return pkg.version;
18875
- }
18876
- } catch {
18877
- }
18878
- }
18879
- const parent = dirname3(directory);
18880
- if (parent === directory) break;
18881
- directory = parent;
18882
- }
18883
- return null;
18884
- }
18885
- function resolveRuntimeExecutable(command, env, cwd) {
18886
- if (isAbsolute2(command) || command.includes("/")) return runtimePath(command, cwd);
18887
- for (const entry of (env.PATH ?? "").split(delimiter)) {
18888
- if (!entry) continue;
18889
- const candidate = resolve2(cwd, entry, command);
18908
+ var CACHE_TTL_MS = 5e3;
18909
+ var DIAGNOSTIC_TIMEOUT_MS = 2e4;
18910
+ var cached = null;
18911
+ var pending = null;
18912
+ var activeProcessGroups = /* @__PURE__ */ new Set();
18913
+ function killDiagnosticGroup(pid) {
18914
+ if (process.platform !== "win32") {
18890
18915
  try {
18891
- accessSync(candidate, constants.X_OK);
18892
- return candidate;
18916
+ process.kill(-pid, "SIGKILL");
18917
+ return;
18893
18918
  } catch {
18894
18919
  }
18895
18920
  }
18896
- return command;
18897
- }
18898
- function runtimeScope(runtime) {
18899
- const { cwd, env } = runtime;
18900
- const home = runtimeHome(env);
18901
- return {
18902
- cwd: resolve2(cwd),
18903
- databasePath: env.SUPERTASK_DB_PATH ? runtimePath(env.SUPERTASK_DB_PATH, cwd) : join3(home, ".local/share/opencode/tasks.db"),
18904
- configPath: env.SUPERTASK_CONFIG_PATH ? runtimePath(env.SUPERTASK_CONFIG_PATH, cwd) : join3(home, ".config/opencode/supertask.json"),
18905
- opencodePath: resolveRuntimeExecutable(env.SUPERTASK_OPENCODE_BIN ?? "opencode", env, cwd),
18906
- home,
18907
- pm2Home: env.PM2_HOME ? runtimePath(env.PM2_HOME, cwd) : join3(home, ".pm2"),
18908
- managementLockPath: canonicalManagementLockPath(env, cwd)
18909
- };
18910
- }
18911
- function scopesMatch(left, right) {
18912
- return left.databasePath === right.databasePath && left.configPath === right.configPath && left.opencodePath === right.opencodePath && left.home === right.home && left.pm2Home === right.pm2Home && left.managementLockPath === right.managementLockPath;
18913
- }
18914
- function currentScope() {
18915
- return runtimeScope({ cwd: process.cwd(), env: process.env });
18916
- }
18917
- function diagnoseOpenCodeRuntime(runtime = {
18918
- cwd: process.cwd(),
18919
- env: process.env
18920
- }) {
18921
- const executable = runtimeScope(runtime).opencodePath;
18922
- const result = spawnSync(executable, ["--version"], {
18923
- cwd: runtime.cwd,
18924
- env: runtime.env,
18925
- encoding: "utf8",
18926
- timeout: 1e4,
18927
- killSignal: "SIGKILL"
18928
- });
18929
- const ok = result.status === 0 && result.error === void 0;
18930
- return {
18931
- ok,
18932
- executable,
18933
- version: ok ? result.stdout.trim() || result.stderr.trim() || null : null,
18934
- error: ok ? null : result.error?.message || result.stderr.trim() || result.stdout.trim() || `exit code ${result.status}`
18935
- };
18936
- }
18937
- function getGatewayDiagnostic(options = {}) {
18938
- const producerScope = currentScope();
18939
- if (!isPm2Installed()) {
18940
- return {
18941
- pm2Installed: false,
18942
- processFound: false,
18943
- status: null,
18944
- pid: null,
18945
- ready: false,
18946
- runningVersion: getRunningVersion(),
18947
- gatewayEntry: null,
18948
- gatewayPackageVersion: null,
18949
- logRotationInstalled: false,
18950
- startupConfigured: process.platform === "darwin" || process.platform === "linux" ? false : null,
18951
- currentScope: producerScope,
18952
- gatewayScope: null,
18953
- scopeMatches: false,
18954
- gatewayOpenCode: null
18955
- };
18956
- }
18957
- const processes = pm2JsonList();
18958
- const gateway = processes.find((item) => item.name === PROCESS_NAME);
18959
- const runtime = gatewayRuntimeFromProcess(gateway);
18960
- const gatewayEnv = runtime?.env ?? process.env;
18961
- const managedScope = runtime ? runtimeScope(runtime) : null;
18962
- const pid = typeof gateway?.pid === "number" ? gateway.pid : null;
18963
- const readyPath = managedScope?.databasePath ?? databasePath();
18964
- const lockedVersion = pid == null ? void 0 : gatewayVersionFromLock(pid, readyPath);
18965
- const gatewayEntry = runtime?.gatewayEntry ?? null;
18966
- return {
18967
- pm2Installed: true,
18968
- processFound: gateway != null,
18969
- status: gateway?.pm2_env?.status ?? null,
18970
- pid,
18971
- ready: pid != null && isGatewayReady(pid, readyPath),
18972
- runningVersion: lockedVersion === void 0 ? getRunningVersion(gatewayEnv, runtime?.cwd) : lockedVersion,
18973
- gatewayEntry,
18974
- gatewayPackageVersion: packageVersionFromGatewayEntry(gatewayEntry),
18975
- logRotationInstalled: processes.some((item) => item.name === "pm2-logrotate" && item.pm2_env?.status === "online"),
18976
- startupConfigured: process.platform === "darwin" ? isMacLaunchAgentConfigured(
18977
- gatewayEnv.PM2_HOME ?? join3(runtimeHome(gatewayEnv), ".pm2"),
18978
- runtime ?? void 0
18979
- ) : process.platform === "linux" ? isLinuxStartupConfigured(gatewayEnv, runtime?.cwd, runtime ?? void 0) : null,
18980
- currentScope: producerScope,
18981
- gatewayScope: managedScope,
18982
- scopeMatches: managedScope != null && scopesMatch(producerScope, managedScope),
18983
- gatewayOpenCode: runtime === null || !options.probeOpenCode ? null : diagnoseOpenCodeRuntime(runtime)
18984
- };
18985
- }
18986
- function pm2Bin(env = process.env) {
18987
- return env.SUPERTASK_PM2_BIN ?? (process.platform === "win32" ? "pm2.cmd" : "pm2");
18988
- }
18989
- function pm2CommandTimeoutMs(env = process.env) {
18990
- const configured = Number(env.SUPERTASK_PM2_COMMAND_TIMEOUT_MS ?? 15e3);
18991
- return Number.isFinite(configured) && configured > 0 ? configured : 15e3;
18992
- }
18993
- function resolvePm2Bin() {
18994
- const configured = pm2Bin();
18995
- if (isAbsolute2(configured)) return configured;
18996
- const result = spawnSync("which", [configured], { encoding: "utf8" });
18997
- const resolved = result.status === 0 ? result.stdout.trim().split("\n")[0] : "";
18998
- if (!resolved) throw new Error(`[supertask] \u65E0\u6CD5\u89E3\u6790 pm2 \u53EF\u6267\u884C\u6587\u4EF6: ${configured}`);
18999
- return resolved;
19000
- }
19001
- function launchAgentPath() {
19002
- return process.env.SUPERTASK_LAUNCH_AGENT_PATH ?? join3(runtimeHome(process.env), "Library/LaunchAgents", `${MAC_LAUNCH_AGENT_LABEL}.plist`);
19003
- }
19004
- function launchctlBin() {
19005
- return process.env.SUPERTASK_LAUNCHCTL_BIN ?? "launchctl";
19006
- }
19007
- function xmlUnescape(value) {
19008
- return value.replaceAll("&apos;", "'").replaceAll("&quot;", '"').replaceAll("&gt;", ">").replaceAll("&lt;", "<").replaceAll("&amp;", "&");
19009
- }
19010
- function plistValue(contents, key) {
19011
- const escapedKey = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
19012
- const match2 = contents.match(new RegExp(`<key>\\s*${escapedKey}\\s*</key>\\s*<string>([^<]*)</string>`));
19013
- return match2?.[1] ? xmlUnescape(match2[1]) : null;
19014
- }
19015
- function isMacLaunchAgentConfigured(expectedPm2Home, expectedRuntime) {
19016
- if (typeof process.getuid !== "function") return false;
19017
- const plistPath = launchAgentPath();
19018
- if (!existsSync4(plistPath)) return false;
19019
18921
  try {
19020
- const contents = readFileSync2(plistPath, "utf8");
19021
- const argumentsBlock = contents.match(
19022
- /<key>\s*ProgramArguments\s*<\/key>\s*<array>([\s\S]*?)<\/array>/
19023
- )?.[1];
19024
- const programArguments = argumentsBlock ? [...argumentsBlock.matchAll(/<string>([^<]*)<\/string>/g)].map((match2) => xmlUnescape(match2[1])) : [];
19025
- const bunPath = programArguments[0];
19026
- const supervisorEntry = programArguments[1];
19027
- const pm2Path = programArguments[2];
19028
- const pm2Home = plistValue(contents, "PM2_HOME");
19029
- const managementLock = plistValue(contents, "SUPERTASK_PM2_MANAGEMENT_LOCK");
19030
- if (!bunPath || !supervisorEntry || !pm2Path || !pm2Home || !managementLock) return false;
19031
- if (expectedPm2Home && pm2Home !== expectedPm2Home) return false;
19032
- const expectedManagementLock = expectedRuntime ? managementLockPath(expectedRuntime.env, expectedRuntime.cwd) : process.env.SUPERTASK_PM2_MANAGEMENT_LOCK ? managementLockPath() : join3(expectedPm2Home ?? currentScope().pm2Home, "supertask-gateway.manage.sqlite");
19033
- if (managementLock !== expectedManagementLock) return false;
19034
- accessSync(bunPath, constants.X_OK);
19035
- accessSync(supervisorEntry, constants.R_OK);
19036
- accessSync(pm2Path, constants.X_OK);
19037
- if (spawnSync(bunPath, ["--version"], {
19038
- stdio: "ignore",
19039
- timeout: pm2CommandTimeoutMs(),
19040
- killSignal: "SIGKILL"
19041
- }).status !== 0) return false;
19042
- if (spawnSync(pm2Path, ["--version"], {
19043
- stdio: "ignore",
19044
- env: { ...process.env, PM2_HOME: pm2Home },
19045
- timeout: pm2CommandTimeoutMs(),
19046
- killSignal: "SIGKILL"
19047
- }).status !== 0) return false;
19048
- const loaded = spawnSync(
19049
- launchctlBin(),
19050
- ["print", `gui/${process.getuid()}/${MAC_LAUNCH_AGENT_LABEL}`],
19051
- {
19052
- encoding: "utf8",
19053
- stdio: ["ignore", "pipe", "ignore"],
19054
- timeout: pm2CommandTimeoutMs(),
19055
- killSignal: "SIGKILL"
19056
- }
19057
- );
19058
- if (loaded.status !== 0) return false;
19059
- if (!loaded.stdout.includes("state = running")) return false;
19060
- if (!loaded.stdout.includes(`path = ${plistPath}`)) return false;
19061
- if (!loaded.stdout.includes(`program = ${bunPath}`)) return false;
19062
- const dumpPath = join3(pm2Home, "dump.pm2");
19063
- const dump = JSON.parse(readFileSync2(dumpPath, "utf8"));
19064
- if (!Array.isArray(dump)) return false;
19065
- const gateway = dump.find((item) => item.name === PROCESS_NAME);
19066
- const dumpRuntime = gatewayRuntimeFromProcess(gateway);
19067
- if (!dumpRuntime || !hasRestorableSavedGatewayRuntime(gateway)) return false;
19068
- return expectedRuntime === void 0 || dumpRuntime.gatewayEntry === expectedRuntime.gatewayEntry && dumpRuntime.bunPath === expectedRuntime.bunPath && dumpRuntime.cwd === expectedRuntime.cwd && scopesMatch(runtimeScope(dumpRuntime), runtimeScope(expectedRuntime));
18922
+ process.kill(pid, "SIGKILL");
19069
18923
  } catch {
19070
- return false;
19071
18924
  }
19072
18925
  }
19073
- function systemctlBin(env = process.env) {
19074
- return env.SUPERTASK_SYSTEMCTL_BIN ?? "systemctl";
18926
+ process.once("exit", () => {
18927
+ for (const pid of activeProcessGroups) killDiagnosticGroup(pid);
18928
+ });
18929
+ function diagnosticEntry() {
18930
+ const extension = import.meta.url.endsWith(".ts") ? "ts" : "js";
18931
+ const moduleDir = dirname3(fileURLToPath2(import.meta.url));
18932
+ const candidates = [
18933
+ join3(moduleDir, `../daemon/gateway-diagnostic-runner.${extension}`),
18934
+ join3(moduleDir, `../../src/daemon/gateway-diagnostic-runner.${extension}`)
18935
+ ];
18936
+ const entry = candidates.find((candidate) => existsSync4(candidate));
18937
+ if (!entry) throw new Error(`Gateway diagnostic runner \u4E0D\u5B58\u5728\uFF1A${candidates.join(", ")}`);
18938
+ return entry;
19075
18939
  }
19076
- function isLinuxStartupConfigured(env = process.env, cwd = process.cwd(), expectedRuntime) {
19077
- const unit = env.SUPERTASK_PM2_SYSTEMD_UNIT ?? `pm2-${userInfo().username}.service`;
19078
- const enabled = spawnSync(systemctlBin(env), ["is-enabled", unit], {
19079
- encoding: "utf8",
19080
- env,
19081
- stdio: ["ignore", "pipe", "ignore"],
19082
- timeout: pm2CommandTimeoutMs(env),
19083
- killSignal: "SIGKILL"
19084
- });
19085
- if (enabled.status !== 0 || enabled.stdout.trim() !== "enabled") return false;
19086
- const contents = spawnSync(systemctlBin(env), ["cat", unit], {
19087
- encoding: "utf8",
19088
- env,
19089
- stdio: ["ignore", "pipe", "ignore"],
19090
- timeout: pm2CommandTimeoutMs(env),
19091
- killSignal: "SIGKILL"
18940
+ async function runGatewayDiagnostic() {
18941
+ return new Promise((resolve2, reject) => {
18942
+ const child = spawn2(process.execPath, [diagnosticEntry()], {
18943
+ cwd: process.cwd(),
18944
+ env: process.env,
18945
+ detached: process.platform !== "win32",
18946
+ stdio: ["ignore", "pipe", "pipe"]
18947
+ });
18948
+ let stdout = "";
18949
+ let stderr = "";
18950
+ let timedOut = false;
18951
+ if (child.pid) activeProcessGroups.add(child.pid);
18952
+ child.stdout?.on("data", (chunk) => {
18953
+ stdout += chunk.toString();
18954
+ });
18955
+ child.stderr?.on("data", (chunk) => {
18956
+ stderr += chunk.toString();
18957
+ });
18958
+ child.once("error", reject);
18959
+ child.once("close", (exitCode) => {
18960
+ clearTimeout(timer);
18961
+ if (child.pid) {
18962
+ killDiagnosticGroup(child.pid);
18963
+ activeProcessGroups.delete(child.pid);
18964
+ }
18965
+ if (timedOut) {
18966
+ reject(new Error(`Gateway diagnostic runner \u8D85\u8FC7 ${DIAGNOSTIC_TIMEOUT_MS}ms \u672A\u5B8C\u6210`));
18967
+ return;
18968
+ }
18969
+ if (exitCode !== 0) {
18970
+ reject(new Error(stderr.trim() || `Gateway diagnostic runner \u9000\u51FA\u7801 ${exitCode}`));
18971
+ return;
18972
+ }
18973
+ try {
18974
+ resolve2(JSON.parse(stdout));
18975
+ } catch (error) {
18976
+ reject(error);
18977
+ }
18978
+ });
18979
+ const timer = setTimeout(() => {
18980
+ timedOut = true;
18981
+ if (child.pid) killDiagnosticGroup(child.pid);
18982
+ else child.kill("SIGKILL");
18983
+ }, DIAGNOSTIC_TIMEOUT_MS);
19092
18984
  });
19093
- if (contents.status !== 0) return false;
19094
- const expectedPm2Home = env.PM2_HOME ?? join3(runtimeHome(env), ".pm2");
19095
- if (!contents.stdout.includes("resurrect") || !contents.stdout.includes(expectedPm2Home)) {
19096
- return false;
19097
- }
19098
- try {
19099
- const dump = JSON.parse(readFileSync2(join3(expectedPm2Home, "dump.pm2"), "utf8"));
19100
- if (!Array.isArray(dump)) return false;
19101
- const gateway = dump.find((item) => item.name === PROCESS_NAME);
19102
- const runtime = gatewayRuntimeFromProcess(gateway);
19103
- if (!runtime || !hasRestorableSavedGatewayRuntime(gateway)) return false;
19104
- return runtime.cwd === resolve2(cwd) && scopesMatch(runtimeScope(runtime), runtimeScope({ cwd, env })) && (expectedRuntime === void 0 || runtime.gatewayEntry === expectedRuntime.gatewayEntry && runtime.bunPath === expectedRuntime.bunPath);
19105
- } catch {
19106
- return false;
19107
- }
19108
18985
  }
19109
- function isPm2Installed(env = process.env) {
19110
- const result = spawnSync(pm2Bin(env), ["--version"], {
19111
- stdio: "ignore",
19112
- env,
19113
- shell: process.platform === "win32",
19114
- timeout: pm2CommandTimeoutMs(env),
19115
- killSignal: "SIGKILL"
18986
+ async function getDashboardGatewayDiagnostic(options = {}) {
18987
+ if (!options.fresh && cached && cached.expiresAt > Date.now()) return cached.diagnostic;
18988
+ if (!options.fresh && pending) return pending;
18989
+ const operation = runGatewayDiagnostic().then((diagnostic) => {
18990
+ cached = { expiresAt: Date.now() + CACHE_TTL_MS, diagnostic };
18991
+ return diagnostic;
19116
18992
  });
19117
- return result.status === 0;
19118
- }
19119
- function resolveCommand(command) {
19120
- const lookup = process.platform === "win32" ? "where" : "which";
19121
- const result = spawnSync(lookup, [command], { encoding: "utf8" });
19122
- return result.status === 0 ? result.stdout.trim().split("\n")[0] || null : null;
19123
- }
19124
- function npmPreferredEnvironment() {
19125
- if (process.platform === "win32") return null;
19126
- const npm = resolveCommand("npm");
19127
- const node = resolveCommand("node");
19128
- if (!npm || !node) return null;
19129
- const command = resolvePm2Bin();
19130
- const path = [.../* @__PURE__ */ new Set([
19131
- dirname3(command),
19132
- dirname3(npm),
19133
- dirname3(node),
19134
- "/usr/local/bin",
19135
- "/usr/bin",
19136
- "/bin"
19137
- ])].join(delimiter);
19138
- return { command, env: { ...process.env, PATH: path } };
19139
- }
19140
- function pm2Exec(args, options = {}) {
19141
- const npmEnvironment = options.preferNpm ? npmPreferredEnvironment() : null;
19142
- const effectiveEnv = options.env ?? npmEnvironment?.env ?? process.env;
19143
- const result = spawnSync(npmEnvironment?.command ?? pm2Bin(effectiveEnv), args, {
19144
- stdio: ["pipe", "pipe", "pipe"],
19145
- encoding: "utf-8",
19146
- env: effectiveEnv,
19147
- shell: process.platform === "win32",
19148
- timeout: options.timeoutMs ?? pm2CommandTimeoutMs(effectiveEnv),
19149
- killSignal: "SIGKILL"
18993
+ if (options.fresh) return operation;
18994
+ pending = operation.finally(() => {
18995
+ pending = null;
19150
18996
  });
19151
- const output = `${result.stdout ?? ""}${result.stderr ?? ""}`.trim();
19152
- if (result.error) return { ok: false, output: result.error.message };
19153
- return { ok: result.status === 0, output };
19154
- }
19155
- function requirePm2(args, action, options = {}) {
19156
- const result = pm2Exec(args, options);
19157
- if (!result.ok) throw new Error(`[supertask] ${action} failed: ${result.output || "unknown pm2 error"}`);
19158
- return result.output;
19159
- }
19160
- function pm2JsonList(env = process.env) {
19161
- const output = requirePm2(["jlist"], "pm2 jlist", { env });
19162
- try {
19163
- const parsed = JSON.parse(output);
19164
- if (!Array.isArray(parsed)) throw new Error("result is not an array");
19165
- return parsed;
19166
- } catch (error) {
19167
- const message = error instanceof Error ? error.message : String(error);
19168
- throw new Error(`[supertask] Invalid pm2 jlist output: ${message}`);
19169
- }
19170
- }
19171
- function gatewayEntryFromProcess(processInfo) {
19172
- const args = processInfo?.pm2_env?.args ?? processInfo?.args;
19173
- const candidates = Array.isArray(args) ? [...args].reverse() : typeof args === "string" ? [args] : [];
19174
- const savedCwd = processInfo?.pm2_env?.pm_cwd ?? processInfo?.pm_cwd;
19175
- for (const candidate of candidates) {
19176
- const path = typeof savedCwd === "string" ? runtimePath(candidate, savedCwd) : candidate;
19177
- if (existsSync4(path)) return resolve2(path);
19178
- }
19179
- return null;
19180
- }
19181
- function gatewayEnvironmentFromProcess(processInfo) {
19182
- const saved = processInfo?.pm2_env?.env ?? processInfo?.env;
19183
- if (!saved) return { ...process.env };
19184
- const env = {};
19185
- for (const [key, value] of Object.entries(saved)) {
19186
- if (typeof value === "string") env[key] = value;
19187
- }
19188
- return env;
19189
- }
19190
- function gatewayRuntimeFromProcess(processInfo) {
19191
- const gatewayEntry = gatewayEntryFromProcess(processInfo);
19192
- if (!gatewayEntry) return null;
19193
- const savedBunPath = processInfo?.pm2_env?.pm_exec_path ?? processInfo?.pm_exec_path;
19194
- const savedCwd = processInfo?.pm2_env?.pm_cwd ?? processInfo?.pm_cwd;
19195
- const savedEnv = gatewayEnvironmentFromProcess(processInfo);
19196
- if (typeof savedBunPath !== "string" || typeof savedCwd !== "string") return null;
19197
- try {
19198
- accessSync(savedBunPath, constants.X_OK);
19199
- if (!statSync3(savedCwd).isDirectory()) return null;
19200
- if (spawnSync(savedBunPath, ["--version"], {
19201
- stdio: "ignore",
19202
- env: savedEnv,
19203
- timeout: pm2CommandTimeoutMs(savedEnv),
19204
- killSignal: "SIGKILL"
19205
- }).status !== 0) return null;
19206
- } catch {
19207
- return null;
19208
- }
19209
- const killTimeout = processInfo?.pm2_env?.kill_timeout ?? processInfo?.kill_timeout;
19210
- return {
19211
- gatewayEntry,
19212
- bunPath: savedBunPath,
19213
- cwd: resolve2(savedCwd),
19214
- env: savedEnv,
19215
- killTimeoutMs: Number.isInteger(killTimeout) && killTimeout >= 5e3 ? killTimeout : void 0
19216
- };
19217
- }
19218
- function hasRestorableSavedGatewayRuntime(processInfo) {
19219
- return gatewayRuntimeFromProcess(processInfo) !== null;
19220
- }
19221
- function databasePath(env = process.env, cwd = process.cwd()) {
19222
- return env.SUPERTASK_DB_PATH ? runtimePath(env.SUPERTASK_DB_PATH, cwd) : join3(runtimeHome(env), ".local/share/opencode/tasks.db");
19223
- }
19224
- function isGatewayReady(expectedPid, path = databasePath()) {
19225
- if (!existsSync4(path)) return false;
19226
- let database = null;
19227
- try {
19228
- database = new Database5(path, { readonly: true });
19229
- const row = database.query(
19230
- "SELECT pid, heartbeat_at, ready_at FROM gateway_lock WHERE id = 1"
19231
- ).get();
19232
- if (!row || row.ready_at == null) return false;
19233
- if (expectedPid !== void 0 && row.pid !== expectedPid) return false;
19234
- const ageMs = Date.now() - row.heartbeat_at;
19235
- return ageMs >= -5e3 && ageMs < GATEWAY_LOCK_STALE_MS;
19236
- } catch {
19237
- return false;
19238
- } finally {
19239
- database?.close();
19240
- }
19241
- }
19242
- function gatewayVersionFromLock(expectedPid, path) {
19243
- if (!existsSync4(path)) return void 0;
19244
- let database = null;
19245
- try {
19246
- database = new Database5(path, { readonly: true });
19247
- const row = database.query(
19248
- "SELECT pid, version FROM gateway_lock WHERE id = 1"
19249
- ).get();
19250
- if (!row || row.pid !== expectedPid) return void 0;
19251
- return row.version;
19252
- } catch {
19253
- return void 0;
19254
- } finally {
19255
- database?.close();
19256
- }
19257
- }
19258
- function managementLockPath(env = process.env, cwd = process.cwd()) {
19259
- const override = env.SUPERTASK_PM2_MANAGEMENT_LOCK;
19260
- if (override) return runtimePath(override, cwd);
19261
- return join3(runtimeScope({ env, cwd }).pm2Home, "supertask-gateway.manage.sqlite");
19262
- }
19263
- function canonicalManagementLockPath(env = process.env, cwd = process.cwd()) {
19264
- const home = runtimeHome(env);
19265
- const pm2Home = env.PM2_HOME ? runtimePath(env.PM2_HOME, cwd) : join3(home, ".pm2");
19266
- return join3(pm2Home, "supertask-gateway.manage.sqlite");
18997
+ return pending;
19267
18998
  }
19268
18999
 
19269
19000
  // src/web/ui.ts
@@ -19536,7 +19267,7 @@ var ZH = {
19536
19267
  "details.enabledYes": "\u5DF2\u542F\u7528",
19537
19268
  "details.enabledNo": "\u5DF2\u505C\u7528",
19538
19269
  "dialog.cancelTask": "\u53D6\u6D88\u4EFB\u52A1 #{id}\uFF1F",
19539
- "dialog.cancelTaskBody": "\u8FD0\u884C\u4E2D\u7684\u4EFB\u52A1\u4F1A\u5728\u4E0B\u4E00\u4E2A\u8F6E\u8BE2\u5468\u671F\u7EC8\u6B62\u5BF9\u5E94\u8FDB\u7A0B\u6811\u3002",
19270
+ "dialog.cancelTaskBody": "\u8FD0\u884C\u4E2D\u7684\u4EFB\u52A1\u4F1A\u5728\u4E0B\u4E00\u4E2A\u8F6E\u8BE2\u5468\u671F\u7EC8\u6B62\u5BF9\u5E94\u7684\u53D7\u7BA1\u8FDB\u7A0B\u7EC4\u3002",
19540
19271
  "dialog.retryTask": "\u91CD\u8BD5\u4EFB\u52A1 #{id}\uFF1F",
19541
19272
  "dialog.retryTaskBody": "\u4EFB\u52A1\u5C06\u56DE\u5230\u5F85\u6267\u884C\u72B6\u6001\uFF0C\u5E76\u91CD\u7F6E\u81EA\u52A8\u91CD\u8BD5\u9884\u7B97\u3002",
19542
19273
  "dialog.deleteTask": "\u5220\u9664\u4EFB\u52A1 #{id}\uFF1F",
@@ -19841,7 +19572,7 @@ var EN = {
19841
19572
  "details.enabledYes": "Enabled",
19842
19573
  "details.enabledNo": "Disabled",
19843
19574
  "dialog.cancelTask": "Cancel task #{id}?",
19844
- "dialog.cancelTaskBody": "A running task will terminate its process tree on the next worker poll.",
19575
+ "dialog.cancelTaskBody": "A running task will terminate its managed process group on the next worker poll.",
19845
19576
  "dialog.retryTask": "Retry task #{id}?",
19846
19577
  "dialog.retryTaskBody": "The task returns to pending and its automatic retry budget is reset.",
19847
19578
  "dialog.deleteTask": "Delete task #{id}?",
@@ -20652,6 +20383,7 @@ var TASK_STATUSES = /* @__PURE__ */ new Set([
20652
20383
  "cancelled"
20653
20384
  ]);
20654
20385
  var SESSION_ID_PATTERN = /^ses_[A-Za-z0-9_]+$/;
20386
+ var LOOPBACK_HOST_PATTERN = /^(?:localhost|127\.0\.0\.1|\[::1\])(?::([1-9]\d{0,4}))?$/i;
20655
20387
  var runtimeConfig = null;
20656
20388
  var restartScheduled = false;
20657
20389
  function setDashboardRuntimeConfig(config) {
@@ -20678,10 +20410,13 @@ function resolveDashboardConfigState(runtimeAvailable, restartRequired, managedR
20678
20410
  function isSafeDashboardRestartTarget(diagnostic, currentPid) {
20679
20411
  return diagnostic.pm2Installed && diagnostic.processFound && diagnostic.status === "online" && diagnostic.pid === currentPid && diagnostic.ready && diagnostic.scopeMatches;
20680
20412
  }
20681
- function canRestartCurrentGateway() {
20413
+ async function canRestartCurrentGateway(fresh = false) {
20682
20414
  if (runtimeConfig === null) return false;
20683
20415
  try {
20684
- return isSafeDashboardRestartTarget(getGatewayDiagnostic(), process.pid);
20416
+ return isSafeDashboardRestartTarget(
20417
+ await getDashboardGatewayDiagnostic({ fresh }),
20418
+ process.pid
20419
+ );
20685
20420
  } catch {
20686
20421
  return false;
20687
20422
  }
@@ -20700,7 +20435,7 @@ function listChildDirectories(path) {
20700
20435
  if (entry.isDirectory()) return [{ name: entry.name, path: entryPath, hidden: entry.name.startsWith(".") }];
20701
20436
  if (!entry.isSymbolicLink()) return [];
20702
20437
  try {
20703
- return statSync4(entryPath).isDirectory() ? [{ name: entry.name, path: entryPath, hidden: entry.name.startsWith(".") }] : [];
20438
+ return statSync3(entryPath).isDirectory() ? [{ name: entry.name, path: entryPath, hidden: entry.name.startsWith(".") }] : [];
20704
20439
  } catch {
20705
20440
  return [];
20706
20441
  }
@@ -20861,6 +20596,14 @@ function resolveLocale(c) {
20861
20596
  return accepted.startsWith("en") ? "en" : "zh-CN";
20862
20597
  }
20863
20598
  app.use("*", async (c, next) => {
20599
+ const requestHostname = new URL(c.req.url).hostname;
20600
+ const hostHeader = c.req.header("Host");
20601
+ const loopbackHosts = ["localhost", "127.0.0.1", "[::1]"];
20602
+ const hostMatch = hostHeader?.match(LOOPBACK_HOST_PATTERN) ?? null;
20603
+ const hostPort = hostMatch?.[1] === void 0 ? null : Number(hostMatch[1]);
20604
+ if (!loopbackHosts.includes(requestHostname) || hostHeader !== void 0 && (!hostMatch || hostPort !== null && hostPort > 65535)) {
20605
+ return c.json({ error: "invalid dashboard host" }, 421);
20606
+ }
20864
20607
  await next();
20865
20608
  c.header("X-Content-Type-Options", "nosniff");
20866
20609
  c.header("X-Frame-Options", "DENY");
@@ -20892,13 +20635,13 @@ app.get("/health", (c) => {
20892
20635
  return c.json(health, health.status === "ok" ? 200 : 503);
20893
20636
  });
20894
20637
  app.get("/api/filesystem/directories", (c) => {
20895
- const requestedPath = c.req.query("path")?.trim() || homedir4();
20638
+ const requestedPath = c.req.query("path")?.trim() || homedir3();
20896
20639
  try {
20897
20640
  validateTaskWorkingDirectory(requestedPath);
20898
20641
  return c.json({
20899
20642
  path: requestedPath,
20900
20643
  parent: dirname4(requestedPath),
20901
- home: homedir4(),
20644
+ home: homedir3(),
20902
20645
  directories: listChildDirectories(requestedPath)
20903
20646
  });
20904
20647
  } catch (error) {
@@ -20996,7 +20739,7 @@ function readCurrentConfig() {
20996
20739
  const configPath = getConfigPath();
20997
20740
  if (!existsSync5(configPath)) return {};
20998
20741
  try {
20999
- return JSON.parse(readFileSync3(configPath, "utf-8"));
20742
+ return JSON.parse(readFileSync2(configPath, "utf-8"));
21000
20743
  } catch {
21001
20744
  return {};
21002
20745
  }
@@ -21004,9 +20747,9 @@ function readCurrentConfig() {
21004
20747
  function writeConfig(cfg) {
21005
20748
  const configPath = getConfigPath();
21006
20749
  const dir = dirname4(configPath);
21007
- if (!existsSync5(dir)) mkdirSync4(dir, { recursive: true });
20750
+ if (!existsSync5(dir)) mkdirSync3(dir, { recursive: true });
21008
20751
  const tempPath = `${configPath}.${process.pid}.tmp`;
21009
- writeFileSync3(tempPath, JSON.stringify(cfg, null, 2) + "\n", { mode: 384 });
20752
+ writeFileSync2(tempPath, JSON.stringify(cfg, null, 2) + "\n", { mode: 384 });
21010
20753
  renameSync2(tempPath, configPath);
21011
20754
  }
21012
20755
  function statCard(value, label, tone, cardIcon, delay = "") {
@@ -21372,7 +21115,7 @@ app.get("/system", async (c) => {
21372
21115
  const config = loadConfig();
21373
21116
  const activeConfig = runtimeConfig ?? config;
21374
21117
  const restartRequired = runtimeConfig !== null && !configsEqual(config, runtimeConfig);
21375
- const managedRestart = canRestartCurrentGateway();
21118
+ const managedRestart = await canRestartCurrentGateway();
21376
21119
  const configState = resolveDashboardConfigState(
21377
21120
  runtimeConfig !== null,
21378
21121
  restartRequired,
@@ -21492,9 +21235,9 @@ app.get("/api/runs/:id/session-command", async (c) => {
21492
21235
  if (!isValidSessionId(run.sessionId)) return c.json({ error: "session unavailable" }, 409);
21493
21236
  return c.json({ command: sessionCommand(run.sessionId) });
21494
21237
  });
21495
- app.get("/api/gateway/status", (c) => {
21238
+ app.get("/api/gateway/status", async (c) => {
21496
21239
  const savedConfig = loadConfig();
21497
- const managed = canRestartCurrentGateway();
21240
+ const managed = await canRestartCurrentGateway();
21498
21241
  return c.json({
21499
21242
  pid: process.pid,
21500
21243
  managed,
@@ -21619,7 +21362,7 @@ app.put("/api/config", async (c) => {
21619
21362
  return c.json({
21620
21363
  success: true,
21621
21364
  restartRequired: runtimeConfig === null || !configsEqual(savedConfig, runtimeConfig),
21622
- managed: canRestartCurrentGateway()
21365
+ managed: await canRestartCurrentGateway()
21623
21366
  });
21624
21367
  } catch (error) {
21625
21368
  return c.json({
@@ -21635,9 +21378,10 @@ app.post("/api/gateway/restart", async (c) => {
21635
21378
  return c.json({ error: "confirmation must be RESTART" }, 400);
21636
21379
  }
21637
21380
  if (restartScheduled) return c.json({ error: "restart already scheduled" }, 409);
21638
- if (!canRestartCurrentGateway()) {
21381
+ if (!await canRestartCurrentGateway(true)) {
21639
21382
  return c.json({ error: "\u5F53\u524D Gateway \u4E0D\u662F\u7531\u5339\u914D\u8FD0\u884C\u4F5C\u7528\u57DF\u7684 PM2 \u8FDB\u7A0B\u6258\u7BA1\uFF0C\u65E0\u6CD5\u4ECE\u7F51\u9875\u5B89\u5168\u91CD\u542F" }, 409);
21640
21383
  }
21384
+ if (restartScheduled) return c.json({ error: "restart already scheduled" }, 409);
21641
21385
  restartScheduled = true;
21642
21386
  const previousPid = process.pid;
21643
21387
  setTimeout(() => {