@andyqiu/codeforge 0.5.11 → 0.5.13
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/bin/codeforge.mjs +77 -32
- package/dist/index.js +227 -7
- package/package.json +1 -1
package/bin/codeforge.mjs
CHANGED
|
@@ -95,7 +95,7 @@ function parseArgs(argv) {
|
|
|
95
95
|
// ────────────────────────────────────────────────────────────────────
|
|
96
96
|
// opencode installer:薄壳,调现成的 install.ps1 / install.sh
|
|
97
97
|
// ────────────────────────────────────────────────────────────────────
|
|
98
|
-
function installOpencode({ scope, dryRun, extraArgs }) {
|
|
98
|
+
function installOpencode({ scope, dryRun, extraArgs, quiet = false }) {
|
|
99
99
|
const isWin = process.platform === "win32"
|
|
100
100
|
const script = isWin ? path.join(REPO_ROOT, "install.ps1") : path.join(REPO_ROOT, "install.sh")
|
|
101
101
|
if (!existsSync(script)) {
|
|
@@ -116,7 +116,11 @@ function installOpencode({ scope, dryRun, extraArgs }) {
|
|
|
116
116
|
}
|
|
117
117
|
const all = [...baseArgs, ...flagArgs, ...(extraArgs ?? [])]
|
|
118
118
|
log(`opencode: ${cmd} ${all.join(" ")}`)
|
|
119
|
-
const r = spawnSync(cmd, all, { stdio: "inherit", cwd: REPO_ROOT })
|
|
119
|
+
const r = spawnSync(cmd, all, { stdio: quiet ? "pipe" : "inherit", cwd: REPO_ROOT })
|
|
120
|
+
if (quiet && r.status !== 0) {
|
|
121
|
+
if (r.stderr) process.stderr.write(r.stderr)
|
|
122
|
+
if (r.stdout) process.stderr.write(r.stdout)
|
|
123
|
+
}
|
|
120
124
|
return r.status ?? 1
|
|
121
125
|
}
|
|
122
126
|
|
|
@@ -288,46 +292,86 @@ function cmdRollback(args) {
|
|
|
288
292
|
// ────────────────────────────────────────────────────────────────────
|
|
289
293
|
// 子命令:upgrade —— 升级到 npm latest 并重新 install --global
|
|
290
294
|
// ────────────────────────────────────────────────────────────────────
|
|
291
|
-
|
|
295
|
+
|
|
296
|
+
async function cmdUpgrade(args) {
|
|
292
297
|
const dryRun = !!args.flags["dry-run"] || !!args.flags.dryRun
|
|
293
298
|
const currentVersion = getVersion()
|
|
294
299
|
|
|
295
|
-
log(
|
|
296
|
-
log(` 当前版本 : ${currentVersion}`)
|
|
297
|
-
log(` dry-run : ${dryRun}`)
|
|
298
|
-
hr()
|
|
300
|
+
log(`检查版本...`)
|
|
299
301
|
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
302
|
+
// 查询 npm registry 最新版本
|
|
303
|
+
let latestVersion = null
|
|
304
|
+
try {
|
|
305
|
+
latestVersion = await new Promise((resolve) => {
|
|
306
|
+
import("node:https").then((mod) => {
|
|
307
|
+
const https = mod.default ?? mod // node:https ESM 无 default export
|
|
308
|
+
const options = {
|
|
309
|
+
hostname: "registry.npmjs.org",
|
|
310
|
+
path: "/@andyqiu%2Fcodeforge/latest",
|
|
311
|
+
method: "GET",
|
|
312
|
+
headers: { "User-Agent": "codeforge-cli" },
|
|
313
|
+
timeout: 5000,
|
|
314
|
+
}
|
|
315
|
+
const req = https.get(options, (res) => {
|
|
316
|
+
let data = ""
|
|
317
|
+
res.on("data", (chunk) => { data += chunk })
|
|
318
|
+
res.on("end", () => {
|
|
319
|
+
try { resolve(JSON.parse(data).version ?? null) } catch { resolve(null) }
|
|
320
|
+
})
|
|
321
|
+
})
|
|
322
|
+
req.on("timeout", () => { req.destroy(); resolve(null) })
|
|
323
|
+
req.on("error", () => resolve(null))
|
|
324
|
+
}).catch(() => resolve(null))
|
|
325
|
+
})
|
|
326
|
+
} catch {
|
|
327
|
+
latestVersion = null
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
if (latestVersion === null) {
|
|
331
|
+
warn(`版本检查失败,继续升级流程`)
|
|
332
|
+
} else if (latestVersion === currentVersion) {
|
|
333
|
+
ok(`当前已是最新版本 v${currentVersion},无需升级`)
|
|
334
|
+
return 0
|
|
311
335
|
} else {
|
|
312
|
-
log(
|
|
336
|
+
log(`当前版本:${currentVersion} → 最新版本:${latestVersion}`)
|
|
313
337
|
}
|
|
314
338
|
|
|
315
339
|
hr()
|
|
316
|
-
log(
|
|
340
|
+
log(`正在升级...`)
|
|
317
341
|
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
342
|
+
const npmCmd = "npm"
|
|
343
|
+
// 有明确版本号时用精确版本,绕过 @latest dist-tag 缓存
|
|
344
|
+
const installTarget = latestVersion
|
|
345
|
+
? `@andyqiu/codeforge@${latestVersion}`
|
|
346
|
+
: "@andyqiu/codeforge@latest"
|
|
347
|
+
const npmArgs = ["install", "-g", installTarget]
|
|
348
|
+
|
|
349
|
+
if (dryRun) {
|
|
350
|
+
const displayNew = latestVersion ?? "未知(版本查询失败)"
|
|
351
|
+
ok(`[dry-run] 演示升级:${currentVersion} → ${displayNew}`)
|
|
352
|
+
ok(`[dry-run] 实际执行时将自动重启生效提示`)
|
|
353
|
+
return 0
|
|
327
354
|
}
|
|
328
355
|
|
|
329
|
-
|
|
330
|
-
|
|
356
|
+
const r = spawnSync(npmCmd, npmArgs, { stdio: "pipe", shell: true })
|
|
357
|
+
if (r.status !== 0) {
|
|
358
|
+
if (r.stderr) process.stderr.write(r.stderr)
|
|
359
|
+
if (r.stdout) process.stderr.write(r.stdout)
|
|
360
|
+
err(`npm install 失败 (exit=${r.status ?? 1})`)
|
|
361
|
+
err(`提示:可手动跑 npm install -g ${installTarget}`)
|
|
362
|
+
return 1
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
const skipBuildFlag = process.platform === "win32" ? "-SkipBuild" : "--skip-build"
|
|
366
|
+
const code = installOpencode({ scope: "global", dryRun: false, extraArgs: [skipBuildFlag], quiet: true })
|
|
367
|
+
if (code !== 0) {
|
|
368
|
+
err(`install --global 失败 (exit=${code})`)
|
|
369
|
+
return 1
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// 升级后读取新版本(npm install 成功后本地 package.json 已更新)
|
|
373
|
+
const newVersion = latestVersion ?? getVersion()
|
|
374
|
+
ok(`升级完成:${currentVersion} → ${newVersion}`)
|
|
331
375
|
ok(`重启 opencode 后新版本生效`)
|
|
332
376
|
return 0
|
|
333
377
|
}
|
|
@@ -437,7 +481,7 @@ function cmdHelp() {
|
|
|
437
481
|
codeforge list
|
|
438
482
|
codeforge version
|
|
439
483
|
codeforge rollback [--target=<path>] [--dry-run] # 恢复最近 backup(auto_install 失败救场)
|
|
440
|
-
codeforge upgrade
|
|
484
|
+
codeforge upgrade|update [--dry-run] # 升级到 npm latest 并重新 install --global
|
|
441
485
|
codeforge runtime where [<path>] # 打印当前项目的全局运行时目录
|
|
442
486
|
codeforge adr-init [--force] [--dry-run] [--write-prepare] [--no-pre-push]
|
|
443
487
|
# 把 ADR 校验体系(hooks + scripts + 模板)下发到当前 git 项目
|
|
@@ -502,6 +546,7 @@ async function main() {
|
|
|
502
546
|
case "rollback":
|
|
503
547
|
return cmdRollback(args)
|
|
504
548
|
case "upgrade":
|
|
549
|
+
case "update":
|
|
505
550
|
return cmdUpgrade(args)
|
|
506
551
|
case "runtime":
|
|
507
552
|
return cmdRuntime(args)
|
package/dist/index.js
CHANGED
|
@@ -13228,6 +13228,15 @@ async function markPlanReadOk(opts) {
|
|
|
13228
13228
|
return true;
|
|
13229
13229
|
});
|
|
13230
13230
|
}
|
|
13231
|
+
async function touchEntryUpdatedAt(opts) {
|
|
13232
|
+
return await mutateRegistry(opts.mainRoot, (reg) => {
|
|
13233
|
+
const entry = reg.entries.find((e) => e.sessionId === opts.sessionId);
|
|
13234
|
+
if (!entry || entry.status !== "active")
|
|
13235
|
+
return false;
|
|
13236
|
+
entry.updatedAt = new Date().toISOString();
|
|
13237
|
+
return true;
|
|
13238
|
+
});
|
|
13239
|
+
}
|
|
13231
13240
|
async function mergeSessionBack(opts) {
|
|
13232
13241
|
const mainRoot = path13.resolve(opts.mainRoot);
|
|
13233
13242
|
const entry = await getSessionWorktree(opts.sessionId, mainRoot);
|
|
@@ -13470,6 +13479,7 @@ Codeforge-Base: ${baseSha.slice(0, 12)}`;
|
|
|
13470
13479
|
return subject + body + footer;
|
|
13471
13480
|
}
|
|
13472
13481
|
var ORPHAN_GRACE_MS = 60000;
|
|
13482
|
+
var SEMANTIC_ORPHAN_MIN_AGE_MS = 24 * 60 * 60000;
|
|
13473
13483
|
async function pruneDiscardedRegistryEntries(mainRoot, opts = {}) {
|
|
13474
13484
|
const keepRecent = opts.keepRecent ?? 50;
|
|
13475
13485
|
if (keepRecent < 0) {
|
|
@@ -13491,7 +13501,7 @@ async function pruneDiscardedRegistryEntries(mainRoot, opts = {}) {
|
|
|
13491
13501
|
return { pruned, kept: kept.length };
|
|
13492
13502
|
});
|
|
13493
13503
|
}
|
|
13494
|
-
async function pruneOrphanWorktrees(mainRoot) {
|
|
13504
|
+
async function pruneOrphanWorktrees(mainRoot, opts = {}) {
|
|
13495
13505
|
const resolved = path13.resolve(mainRoot);
|
|
13496
13506
|
const cleaned = [];
|
|
13497
13507
|
const failed = [];
|
|
@@ -13551,6 +13561,60 @@ async function pruneOrphanWorktrees(mainRoot) {
|
|
|
13551
13561
|
}
|
|
13552
13562
|
}
|
|
13553
13563
|
});
|
|
13564
|
+
if (opts.isSessionAlive) {
|
|
13565
|
+
const minAge = opts.semanticOrphanMinAgeMs ?? SEMANTIC_ORPHAN_MIN_AGE_MS;
|
|
13566
|
+
const probe = opts.isSessionAlive;
|
|
13567
|
+
await mutateRegistry(resolved, async (reg2) => {
|
|
13568
|
+
const now = Date.now();
|
|
13569
|
+
for (const entry of reg2.entries) {
|
|
13570
|
+
if (entry.status !== "active")
|
|
13571
|
+
continue;
|
|
13572
|
+
const updatedMs = Date.parse(entry.updatedAt);
|
|
13573
|
+
if (!Number.isFinite(updatedMs) || now - updatedMs < minAge) {
|
|
13574
|
+
skipped++;
|
|
13575
|
+
continue;
|
|
13576
|
+
}
|
|
13577
|
+
let aliveResult;
|
|
13578
|
+
try {
|
|
13579
|
+
aliveResult = await probe(entry.sessionId);
|
|
13580
|
+
} catch {
|
|
13581
|
+
skipped++;
|
|
13582
|
+
continue;
|
|
13583
|
+
}
|
|
13584
|
+
if (aliveResult.source === "unknown") {
|
|
13585
|
+
skipped++;
|
|
13586
|
+
continue;
|
|
13587
|
+
}
|
|
13588
|
+
if (aliveResult.alive) {
|
|
13589
|
+
skipped++;
|
|
13590
|
+
continue;
|
|
13591
|
+
}
|
|
13592
|
+
try {
|
|
13593
|
+
await removeWorktree({
|
|
13594
|
+
root: resolved,
|
|
13595
|
+
worktree_path: entry.worktreePath,
|
|
13596
|
+
force: true
|
|
13597
|
+
});
|
|
13598
|
+
} catch (err) {
|
|
13599
|
+
failed.push({
|
|
13600
|
+
worktreePath: entry.worktreePath,
|
|
13601
|
+
error: `D 类 removeWorktree 失败: ${err instanceof Error ? err.message : String(err)}`
|
|
13602
|
+
});
|
|
13603
|
+
continue;
|
|
13604
|
+
}
|
|
13605
|
+
try {
|
|
13606
|
+
await runGit2(resolved, ["branch", "-D", entry.branch]);
|
|
13607
|
+
} catch {}
|
|
13608
|
+
entry.status = "discarded";
|
|
13609
|
+
entry.updatedAt = new Date().toISOString();
|
|
13610
|
+
cleaned.push({
|
|
13611
|
+
sessionId: entry.sessionId,
|
|
13612
|
+
worktreePath: entry.worktreePath,
|
|
13613
|
+
reason: `D 类语义孤儿 (opencode session ${aliveResult.source}: dead)`
|
|
13614
|
+
});
|
|
13615
|
+
}
|
|
13616
|
+
});
|
|
13617
|
+
}
|
|
13554
13618
|
const codeforgeWorktreeRoot = path13.resolve(path13.join(resolved, DEFAULT_WORKTREE_SUBDIR));
|
|
13555
13619
|
const fsWorktreePaths = [];
|
|
13556
13620
|
try {
|
|
@@ -21710,7 +21774,7 @@ import * as zlib from "node:zlib";
|
|
|
21710
21774
|
// lib/version-injected.ts
|
|
21711
21775
|
function getInjectedVersion() {
|
|
21712
21776
|
try {
|
|
21713
|
-
const v = "0.5.
|
|
21777
|
+
const v = "0.5.13";
|
|
21714
21778
|
if (typeof v === "string" && /^\d+\.\d+\.\d+/.test(v)) {
|
|
21715
21779
|
return v;
|
|
21716
21780
|
}
|
|
@@ -22990,9 +23054,9 @@ var handler24 = workflowEngineServer;
|
|
|
22990
23054
|
import path27 from "node:path";
|
|
22991
23055
|
var PLUGIN_NAME25 = "session-worktree-guard";
|
|
22992
23056
|
logLifecycle(PLUGIN_NAME25, "import", {});
|
|
22993
|
-
var WRITE_INTENT_RE = />(
|
|
23057
|
+
var WRITE_INTENT_RE = />(?![=&])|\btee\b|\brm\b|\bmv\b|\bcp\b|\bmkdir\b|\btouch\b|\bchmod\b|\bchown\b|\bln\b/;
|
|
22994
23058
|
var READ_ONLY_COMMANDS = /^\s*(?:ls|cat|head|tail|grep|rg|find|fd|wc|stat|file|which|whereis|echo|pwd|cd|pushd|popd|env|printenv|type|less|more|sort|uniq|awk|tr|cut|jq|date|whoami|id|uname|git(?:\s+-C\s+\S+)?\s+(?:log|show|diff|status|branch|tag|remote|config\s+--get|rev-parse|rev-list|ls-files|ls-tree|cat-file|describe|reflog|blame|shortlog|name-rev|symbolic-ref|merge-base|worktree\s+list|stash\s+list|stash\s+show))\b/;
|
|
22995
|
-
var SIDE_EFFECT_TOKEN_RE = />(
|
|
23059
|
+
var SIDE_EFFECT_TOKEN_RE = />(?![=&])|\|\s*tee\b|\btee\b/;
|
|
22996
23060
|
function isReadOnlyBashCommand(command) {
|
|
22997
23061
|
if (!READ_ONLY_COMMANDS.test(command))
|
|
22998
23062
|
return false;
|
|
@@ -23013,11 +23077,27 @@ function buildGitVcsWriteRegex(mainRoot) {
|
|
|
23013
23077
|
return new RegExp(`git\\b[^\\n]*(?:-C\\s+|--work-tree[=\\s])${esc}`);
|
|
23014
23078
|
}
|
|
23015
23079
|
var WRITE_TOOLS = new Set(["write", "edit", "ast_edit"]);
|
|
23080
|
+
var TOUCH_THROTTLE_MS = 5 * 60000;
|
|
23081
|
+
var _touchCache = new Map;
|
|
23016
23082
|
var CLASS_B_CALLER_WHITELIST = new Set([
|
|
23017
23083
|
"codeforge",
|
|
23018
23084
|
"reviewer",
|
|
23085
|
+
"reviewer-lite",
|
|
23019
23086
|
"general"
|
|
23020
23087
|
]);
|
|
23088
|
+
var CODEFORGE_WORKTREE_DIR_NAME = path27.join(".git", "codeforge-worktrees");
|
|
23089
|
+
function worktreesRoot(mainRoot) {
|
|
23090
|
+
return path27.join(mainRoot, CODEFORGE_WORKTREE_DIR_NAME);
|
|
23091
|
+
}
|
|
23092
|
+
function isInsideAnyWorktreeDir(absPath, mainRoot) {
|
|
23093
|
+
if (!path27.isAbsolute(absPath))
|
|
23094
|
+
return false;
|
|
23095
|
+
const root = worktreesRoot(mainRoot);
|
|
23096
|
+
if (absPath === root)
|
|
23097
|
+
return false;
|
|
23098
|
+
const prefix = root.endsWith(path27.sep) ? root : root + path27.sep;
|
|
23099
|
+
return absPath.startsWith(prefix);
|
|
23100
|
+
}
|
|
23021
23101
|
function rewritePath(value, mainRoot, worktreeRoot) {
|
|
23022
23102
|
if (!value)
|
|
23023
23103
|
return null;
|
|
@@ -23026,6 +23106,9 @@ function rewritePath(value, mainRoot, worktreeRoot) {
|
|
|
23026
23106
|
if (resolved === worktreeRoot || resolved.startsWith(wtPrefix2)) {
|
|
23027
23107
|
return null;
|
|
23028
23108
|
}
|
|
23109
|
+
if (isInsideAnyWorktreeDir(resolved, mainRoot)) {
|
|
23110
|
+
return null;
|
|
23111
|
+
}
|
|
23029
23112
|
if (resolved === mainRoot)
|
|
23030
23113
|
return worktreeRoot;
|
|
23031
23114
|
const prefix = mainRoot.endsWith("/") ? mainRoot : mainRoot + "/";
|
|
@@ -23055,7 +23138,20 @@ function commandContainsMainRootExcludingWorktree(command, mainRoot, worktreePat
|
|
|
23055
23138
|
return true;
|
|
23056
23139
|
}
|
|
23057
23140
|
}
|
|
23058
|
-
const
|
|
23141
|
+
const wtRoot = worktreesRoot(mainRoot);
|
|
23142
|
+
const wtRootPrefix = wtRoot + path27.sep;
|
|
23143
|
+
const escapedWtRootPrefix = escapeRegex2(wtRootPrefix);
|
|
23144
|
+
const wtPathPattern = escapedWtRootPrefix + `[^\\s'"\\x60)]*`;
|
|
23145
|
+
const allWorktreePathsReForEscape = new RegExp(wtPathPattern, "g");
|
|
23146
|
+
const allWorktreePathsReForReplace = new RegExp(wtPathPattern, "g");
|
|
23147
|
+
for (const match of command.matchAll(allWorktreePathsReForEscape)) {
|
|
23148
|
+
const matchedPath = match[0];
|
|
23149
|
+
const tail = matchedPath.slice(wtRootPrefix.length);
|
|
23150
|
+
if (/(?:^|[/\\])\.\.(?:[/\\]|$)/.test(tail)) {
|
|
23151
|
+
return true;
|
|
23152
|
+
}
|
|
23153
|
+
}
|
|
23154
|
+
const sanitized = command.split(worktreePath).join("").replace(allWorktreePathsReForReplace, "");
|
|
23059
23155
|
return commandContainsMainRoot(sanitized, mainRoot);
|
|
23060
23156
|
}
|
|
23061
23157
|
function detectBashWriteIntent(command, mainRoot) {
|
|
@@ -23197,6 +23293,22 @@ var sessionWorktreeGuardPlugin = async (ctx) => {
|
|
|
23197
23293
|
}
|
|
23198
23294
|
}
|
|
23199
23295
|
const worktreePath = entry.worktreePath;
|
|
23296
|
+
if (entry.status === "active" && isWriteOperation(toolName, argsObj, mainRoot)) {
|
|
23297
|
+
const last = _touchCache.get(entry.sessionId) ?? 0;
|
|
23298
|
+
const nowMs = Date.now();
|
|
23299
|
+
if (nowMs - last > TOUCH_THROTTLE_MS) {
|
|
23300
|
+
_touchCache.set(entry.sessionId, nowMs);
|
|
23301
|
+
touchEntryUpdatedAt({
|
|
23302
|
+
sessionId: entry.sessionId,
|
|
23303
|
+
mainRoot
|
|
23304
|
+
}).catch((err) => {
|
|
23305
|
+
log14.warn("touchEntryUpdatedAt 失败 (已忽略)", {
|
|
23306
|
+
sessionId: entry?.sessionId,
|
|
23307
|
+
error: err instanceof Error ? err.message : String(err)
|
|
23308
|
+
});
|
|
23309
|
+
});
|
|
23310
|
+
}
|
|
23311
|
+
}
|
|
23200
23312
|
if (toolName === "session_merge") {
|
|
23201
23313
|
const action = argsObj["action"];
|
|
23202
23314
|
if (action === "merge") {
|
|
@@ -23425,6 +23537,103 @@ var sessionWorktreeGuardPlugin = async (ctx) => {
|
|
|
23425
23537
|
};
|
|
23426
23538
|
var handler25 = sessionWorktreeGuardPlugin;
|
|
23427
23539
|
|
|
23540
|
+
// lib/opencode-session-probe.ts
|
|
23541
|
+
import * as path28 from "node:path";
|
|
23542
|
+
import * as os6 from "node:os";
|
|
23543
|
+
import { createRequire as createRequire2 } from "node:module";
|
|
23544
|
+
var requireFromHere = createRequire2(import.meta.url);
|
|
23545
|
+
var DEFAULT_LIVENESS_MS = 6 * 60 * 60000;
|
|
23546
|
+
var DEFAULT_DB_PATH = path28.join(os6.homedir(), ".local/share/opencode/opencode.db");
|
|
23547
|
+
function createSessionProbe(opts = {}) {
|
|
23548
|
+
const dbPath = opts.dbPath ?? DEFAULT_DB_PATH;
|
|
23549
|
+
const httpBaseUrl = opts.httpBaseUrl ?? process.env["OPENCODE_SERVER_URL"];
|
|
23550
|
+
const livenessWindowMs = opts.livenessWindowMs ?? DEFAULT_LIVENESS_MS;
|
|
23551
|
+
const timeoutMs = opts.timeoutMs ?? 1500;
|
|
23552
|
+
let db = null;
|
|
23553
|
+
let dbInitTried = false;
|
|
23554
|
+
let dbStmt = null;
|
|
23555
|
+
async function tryOpenDb() {
|
|
23556
|
+
if (dbInitTried)
|
|
23557
|
+
return db != null;
|
|
23558
|
+
dbInitTried = true;
|
|
23559
|
+
try {
|
|
23560
|
+
const mod = requireFromHere("node:sqlite");
|
|
23561
|
+
try {
|
|
23562
|
+
db = new mod.DatabaseSync(dbPath, { readOnly: true });
|
|
23563
|
+
} catch {
|
|
23564
|
+
db = null;
|
|
23565
|
+
dbStmt = null;
|
|
23566
|
+
return false;
|
|
23567
|
+
}
|
|
23568
|
+
dbStmt = db.prepare("SELECT time_updated, time_archived FROM session WHERE id = ? LIMIT 1");
|
|
23569
|
+
return true;
|
|
23570
|
+
} catch {
|
|
23571
|
+
db = null;
|
|
23572
|
+
dbStmt = null;
|
|
23573
|
+
return false;
|
|
23574
|
+
}
|
|
23575
|
+
}
|
|
23576
|
+
async function probeHttp(sessionId) {
|
|
23577
|
+
if (!httpBaseUrl)
|
|
23578
|
+
return null;
|
|
23579
|
+
try {
|
|
23580
|
+
const ctrl = new AbortController;
|
|
23581
|
+
const t = setTimeout(() => ctrl.abort(), timeoutMs);
|
|
23582
|
+
const res = await fetch(`${httpBaseUrl.replace(/\/$/, "")}/session`, {
|
|
23583
|
+
signal: ctrl.signal
|
|
23584
|
+
}).finally(() => clearTimeout(t));
|
|
23585
|
+
if (!res.ok)
|
|
23586
|
+
return null;
|
|
23587
|
+
const list = await res.json();
|
|
23588
|
+
const hit = Array.isArray(list) && list.some((s) => s.id === sessionId);
|
|
23589
|
+
return { alive: hit, source: "http" };
|
|
23590
|
+
} catch {
|
|
23591
|
+
return null;
|
|
23592
|
+
}
|
|
23593
|
+
}
|
|
23594
|
+
async function probeSqlite(sessionId) {
|
|
23595
|
+
if (!await tryOpenDb() || !dbStmt)
|
|
23596
|
+
return null;
|
|
23597
|
+
try {
|
|
23598
|
+
const row = dbStmt.get(sessionId);
|
|
23599
|
+
if (!row) {
|
|
23600
|
+
return { alive: false, source: "sqlite" };
|
|
23601
|
+
}
|
|
23602
|
+
if (row.time_archived != null) {
|
|
23603
|
+
return {
|
|
23604
|
+
alive: false,
|
|
23605
|
+
source: "sqlite",
|
|
23606
|
+
time_archived: row.time_archived
|
|
23607
|
+
};
|
|
23608
|
+
}
|
|
23609
|
+
const now = Date.now();
|
|
23610
|
+
const tu = Number(row.time_updated) || 0;
|
|
23611
|
+
const alive = now - tu < livenessWindowMs;
|
|
23612
|
+
return { alive, source: "sqlite", time_updated: tu, time_archived: null };
|
|
23613
|
+
} catch {
|
|
23614
|
+
return null;
|
|
23615
|
+
}
|
|
23616
|
+
}
|
|
23617
|
+
return {
|
|
23618
|
+
async isSessionAlive(sessionId) {
|
|
23619
|
+
const http = await probeHttp(sessionId);
|
|
23620
|
+
if (http)
|
|
23621
|
+
return http;
|
|
23622
|
+
const sql = await probeSqlite(sessionId);
|
|
23623
|
+
if (sql)
|
|
23624
|
+
return sql;
|
|
23625
|
+
return { alive: true, source: "unknown" };
|
|
23626
|
+
},
|
|
23627
|
+
close() {
|
|
23628
|
+
try {
|
|
23629
|
+
db?.close?.();
|
|
23630
|
+
} catch {}
|
|
23631
|
+
db = null;
|
|
23632
|
+
dbStmt = null;
|
|
23633
|
+
}
|
|
23634
|
+
};
|
|
23635
|
+
}
|
|
23636
|
+
|
|
23428
23637
|
// plugins/worktree-lifecycle.ts
|
|
23429
23638
|
var PLUGIN_NAME26 = "worktree-lifecycle";
|
|
23430
23639
|
logLifecycle(PLUGIN_NAME26, "import", {});
|
|
@@ -23435,6 +23644,7 @@ var PRUNE_INTERVAL_MS = 30 * 60000;
|
|
|
23435
23644
|
var lastIdleToastAt = new Map;
|
|
23436
23645
|
var pruneRunning = false;
|
|
23437
23646
|
var _pruneTimer;
|
|
23647
|
+
var _probe = null;
|
|
23438
23648
|
var log15 = makePluginLogger(PLUGIN_NAME26);
|
|
23439
23649
|
var worktreeLifecyclePlugin = async (ctx) => {
|
|
23440
23650
|
const mainRoot = ctx.directory;
|
|
@@ -23446,9 +23656,17 @@ var worktreeLifecyclePlugin = async (ctx) => {
|
|
|
23446
23656
|
return {};
|
|
23447
23657
|
}
|
|
23448
23658
|
const client = ctx.client;
|
|
23659
|
+
if (_probe) {
|
|
23660
|
+
try {
|
|
23661
|
+
_probe.close();
|
|
23662
|
+
} catch {}
|
|
23663
|
+
}
|
|
23664
|
+
_probe = createSessionProbe();
|
|
23449
23665
|
setImmediate(() => {
|
|
23450
23666
|
safeAsync(PLUGIN_NAME26, "activate.pruneOrphan", async () => {
|
|
23451
|
-
const result = await pruneOrphanWorktrees(mainRoot
|
|
23667
|
+
const result = await pruneOrphanWorktrees(mainRoot, {
|
|
23668
|
+
isSessionAlive: _probe.isSessionAlive
|
|
23669
|
+
});
|
|
23452
23670
|
if (result.cleaned.length > 0 || result.failed.length > 0) {
|
|
23453
23671
|
log15.info(`[pruneOrphan] cleaned=${result.cleaned.length} failed=${result.failed.length} skipped=${result.skipped}`);
|
|
23454
23672
|
safeWriteLog(PLUGIN_NAME26, {
|
|
@@ -23474,7 +23692,9 @@ var worktreeLifecyclePlugin = async (ctx) => {
|
|
|
23474
23692
|
pruneRunning = true;
|
|
23475
23693
|
safeAsync(PLUGIN_NAME26, "interval.pruneOrphan", async () => {
|
|
23476
23694
|
try {
|
|
23477
|
-
const result = await pruneOrphanWorktrees(mainRoot
|
|
23695
|
+
const result = await pruneOrphanWorktrees(mainRoot, {
|
|
23696
|
+
isSessionAlive: _probe.isSessionAlive
|
|
23697
|
+
});
|
|
23478
23698
|
if (result.cleaned.length > 0 || result.failed.length > 0) {
|
|
23479
23699
|
log15.info(`[pruneOrphan interval] cleaned=${result.cleaned.length} failed=${result.failed.length} skipped=${result.skipped}`);
|
|
23480
23700
|
safeWriteLog(PLUGIN_NAME26, {
|