trans-genderian-orchestra 0.2.0 → 0.2.1
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/README.md +7 -0
- package/dist/server.js +374 -5
- package/package.json +1 -1
- package/schema/tgo.config.schema.json +11 -0
- package/src/config.ts +5 -0
- package/src/plugin.ts +33 -1
- package/src/self-update.ts +359 -0
package/README.md
CHANGED
|
@@ -134,6 +134,7 @@ Put options in the second element of the plugin tuple: `["trans-genderian-orches
|
|
|
134
134
|
| `checkVersion` | Whether to do that gentle npm drift check on startup. | `true` |
|
|
135
135
|
| `sessionReuse` | `{ enabled, maxContextTokens }` — continues prior delegation sessions via task_id when the stored session is under the token budget. | `true`, 100000 |
|
|
136
136
|
| `termination` | `{ enabled }` — stops a delegated session after it declares STATUS: complete with its exit gate satisfied and then makes a residual tool call. | `true` |
|
|
137
|
+
| `selfUpdate` | `{ enabled }` — refreshes TGO's own plugin cache slot when a newer version is on npm; activates on next restart. | `true` |
|
|
137
138
|
| `watchdog` | `{ enabled, wallClockMs, idleMs, checkMs, stuckLoopTools, stuckLoopMs }` — aborts a delegated session that’s hung or gone silent and asks Bernstein to re-dispatch. stuck-loop = <3 distinct tool signatures across the last 20 tools within 5m (read-only seats no longer false-trip). | `true`, 30m, 15m, 10s, stuckLoopTools 20, stuckLoopMs 5m |
|
|
138
139
|
|
|
139
140
|
The JSON schema is at `plugin/schema/tgo.config.schema.json`.
|
|
@@ -181,6 +182,12 @@ Autonomy is a mode, not the default. “Keep going” is opt-in and bounded —
|
|
|
181
182
|
|
|
182
183
|
For the human-readable long form: `docs/ARCHITECTURE.md` (shape and hooks), `docs/ROSTER.md` (the five seats), `docs/CONCISION.md` (the house style), `docs/SETUP.md` (install and per-repo setup), `docs/CONTRIBUTING.md` (dev workflow), and `CHANGELOG.md` (release history). The canonical contracts stay under `docs/spec/`.
|
|
183
184
|
|
|
185
|
+
## Troubleshooting
|
|
186
|
+
|
|
187
|
+
### Updating TGO
|
|
188
|
+
|
|
189
|
+
Normally automatic via self-update (activates on restart) — when npm has a newer version, TGO refreshes its own plugin cache slot in the background and the update activates on next restart. If a slot is ever stuck, manual refresh is `rm -rf ~/.cache/opencode/packages/trans-genderian-orchestra*` then restart — `opencode plugin --force -g` is a no-op for same-spec entries (upstream behavior).
|
|
190
|
+
|
|
184
191
|
## Developing on TGO
|
|
185
192
|
|
|
186
193
|
```bash
|
package/dist/server.js
CHANGED
|
@@ -14351,6 +14351,9 @@ var sessionReuseConfig = exports_external.object({
|
|
|
14351
14351
|
var terminationConfig = exports_external.object({
|
|
14352
14352
|
enabled: exports_external.boolean().default(true)
|
|
14353
14353
|
});
|
|
14354
|
+
var selfUpdateConfig = exports_external.object({
|
|
14355
|
+
enabled: exports_external.boolean().default(true)
|
|
14356
|
+
});
|
|
14354
14357
|
var tgoConfigSchema = exports_external.object({
|
|
14355
14358
|
preset: exports_external.enum(PRESET_NAMES).default("balanced"),
|
|
14356
14359
|
presets: exports_external.object({
|
|
@@ -14373,7 +14376,8 @@ var tgoConfigSchema = exports_external.object({
|
|
|
14373
14376
|
stuckLoopMs: 5 * 60 * 1000
|
|
14374
14377
|
})),
|
|
14375
14378
|
sessionReuse: sessionReuseConfig.optional().default(() => ({ enabled: true, maxContextTokens: 1e5 })),
|
|
14376
|
-
termination: terminationConfig.optional().default(() => ({ enabled: true }))
|
|
14379
|
+
termination: terminationConfig.optional().default(() => ({ enabled: true })),
|
|
14380
|
+
selfUpdate: selfUpdateConfig.optional().default(() => ({ enabled: true }))
|
|
14377
14381
|
});
|
|
14378
14382
|
function estimateTokens(text) {
|
|
14379
14383
|
const normalized = text.replace(/\s+/g, " ").trim();
|
|
@@ -16801,9 +16805,344 @@ function parseCompletionSignal(text) {
|
|
|
16801
16805
|
var and = (...cs) => (i) => cs.every((c) => c(i));
|
|
16802
16806
|
var terminationDecision = and((i) => i.signal.complete, (i) => !i.exitGateRequired || i.signal.exitGate === true, (i) => i.toolCallsAfterCompletion >= 1);
|
|
16803
16807
|
|
|
16804
|
-
// src/
|
|
16808
|
+
// src/self-update.ts
|
|
16809
|
+
import * as fs8 from "node:fs/promises";
|
|
16810
|
+
import * as fsSync from "node:fs";
|
|
16805
16811
|
import * as path9 from "node:path";
|
|
16806
16812
|
import * as os2 from "node:os";
|
|
16813
|
+
var LOCK_STALE_MS = 120000;
|
|
16814
|
+
var LOCK_FILE = ".tgo-selfupdate.lock";
|
|
16815
|
+
function resolveCacheRoot(homeDir) {
|
|
16816
|
+
const base = process.env.OPENCODE_TEST_HOME ?? process.env.XDG_CACHE_HOME ?? path9.join(homeDir ?? os2.homedir(), ".cache");
|
|
16817
|
+
return path9.join(base, "opencode");
|
|
16818
|
+
}
|
|
16819
|
+
function slotDirs(cacheRoot, pkgName) {
|
|
16820
|
+
const candidates = [
|
|
16821
|
+
path9.join(cacheRoot, "packages", `${pkgName}@latest`),
|
|
16822
|
+
path9.join(cacheRoot, "packages", pkgName)
|
|
16823
|
+
];
|
|
16824
|
+
return candidates.filter((dir) => {
|
|
16825
|
+
try {
|
|
16826
|
+
return fsSync.existsSync(dir) && fsSync.statSync(dir).isDirectory();
|
|
16827
|
+
} catch {
|
|
16828
|
+
return false;
|
|
16829
|
+
}
|
|
16830
|
+
});
|
|
16831
|
+
}
|
|
16832
|
+
function parseSemver(v) {
|
|
16833
|
+
if (typeof v !== "string")
|
|
16834
|
+
return null;
|
|
16835
|
+
let s = v.trim().replace(/^v/, "");
|
|
16836
|
+
if (s.length === 0)
|
|
16837
|
+
return null;
|
|
16838
|
+
const plusIdx = s.indexOf("+");
|
|
16839
|
+
if (plusIdx !== -1) {
|
|
16840
|
+
const build = s.slice(plusIdx + 1);
|
|
16841
|
+
if (build.length === 0)
|
|
16842
|
+
return null;
|
|
16843
|
+
const buildIds = build.split(".");
|
|
16844
|
+
for (const id of buildIds) {
|
|
16845
|
+
if (id.length === 0 || !/^[0-9A-Za-z-]+$/.test(id))
|
|
16846
|
+
return null;
|
|
16847
|
+
}
|
|
16848
|
+
s = s.slice(0, plusIdx);
|
|
16849
|
+
}
|
|
16850
|
+
let coreStr;
|
|
16851
|
+
let preStr = null;
|
|
16852
|
+
const dashIdx = s.indexOf("-");
|
|
16853
|
+
if (dashIdx !== -1) {
|
|
16854
|
+
coreStr = s.slice(0, dashIdx);
|
|
16855
|
+
preStr = s.slice(dashIdx + 1);
|
|
16856
|
+
if (preStr.length === 0)
|
|
16857
|
+
return null;
|
|
16858
|
+
} else {
|
|
16859
|
+
coreStr = s;
|
|
16860
|
+
}
|
|
16861
|
+
const coreParts = coreStr.split(".");
|
|
16862
|
+
if (coreParts.length !== 3)
|
|
16863
|
+
return null;
|
|
16864
|
+
const nums = [];
|
|
16865
|
+
for (const p of coreParts) {
|
|
16866
|
+
if (!/^(0|[1-9]\d*)$/.test(p))
|
|
16867
|
+
return null;
|
|
16868
|
+
nums.push(parseInt(p, 10));
|
|
16869
|
+
}
|
|
16870
|
+
let prerelease = null;
|
|
16871
|
+
if (preStr !== null) {
|
|
16872
|
+
const ids = preStr.split(".");
|
|
16873
|
+
for (const id of ids) {
|
|
16874
|
+
if (id.length === 0 || !/^[0-9A-Za-z-]+$/.test(id))
|
|
16875
|
+
return null;
|
|
16876
|
+
if (/^[0-9]+$/.test(id) && !/^(0|[1-9]\d*)$/.test(id))
|
|
16877
|
+
return null;
|
|
16878
|
+
}
|
|
16879
|
+
prerelease = ids;
|
|
16880
|
+
}
|
|
16881
|
+
return { major: nums[0], minor: nums[1], patch: nums[2], prerelease };
|
|
16882
|
+
}
|
|
16883
|
+
function semverGt(a, b) {
|
|
16884
|
+
const pa = parseSemver(a);
|
|
16885
|
+
const pb = parseSemver(b);
|
|
16886
|
+
if (!pa || !pb)
|
|
16887
|
+
return false;
|
|
16888
|
+
if (pa.major !== pb.major)
|
|
16889
|
+
return pa.major > pb.major;
|
|
16890
|
+
if (pa.minor !== pb.minor)
|
|
16891
|
+
return pa.minor > pb.minor;
|
|
16892
|
+
if (pa.patch !== pb.patch)
|
|
16893
|
+
return pa.patch > pb.patch;
|
|
16894
|
+
const aPre = pa.prerelease;
|
|
16895
|
+
const bPre = pb.prerelease;
|
|
16896
|
+
if (aPre === null && bPre === null)
|
|
16897
|
+
return false;
|
|
16898
|
+
if (aPre === null && bPre !== null)
|
|
16899
|
+
return true;
|
|
16900
|
+
if (aPre !== null && bPre === null)
|
|
16901
|
+
return false;
|
|
16902
|
+
const aA = aPre;
|
|
16903
|
+
const bA = bPre;
|
|
16904
|
+
const len = Math.min(aA.length, bA.length);
|
|
16905
|
+
for (let i = 0;i < len; i++) {
|
|
16906
|
+
const aId = aA[i];
|
|
16907
|
+
const bId = bA[i];
|
|
16908
|
+
if (aId === bId)
|
|
16909
|
+
continue;
|
|
16910
|
+
const aIsNum = /^[0-9]+$/.test(aId);
|
|
16911
|
+
const bIsNum = /^[0-9]+$/.test(bId);
|
|
16912
|
+
if (aIsNum && bIsNum) {
|
|
16913
|
+
return parseInt(aId, 10) > parseInt(bId, 10);
|
|
16914
|
+
}
|
|
16915
|
+
if (aIsNum && !bIsNum)
|
|
16916
|
+
return false;
|
|
16917
|
+
if (!aIsNum && bIsNum)
|
|
16918
|
+
return true;
|
|
16919
|
+
return aId > bId;
|
|
16920
|
+
}
|
|
16921
|
+
return aA.length > bA.length;
|
|
16922
|
+
}
|
|
16923
|
+
function shouldRefresh(runningVersion, latestVersion) {
|
|
16924
|
+
return semverGt(latestVersion, runningVersion);
|
|
16925
|
+
}
|
|
16926
|
+
function buildInstallArgs(dir, pkgName) {
|
|
16927
|
+
return ["npm", "install", "--prefix", dir, `${pkgName}@latest`, "--save-exact", "--ignore-scripts", "--no-audit", "--no-fund"];
|
|
16928
|
+
}
|
|
16929
|
+
async function recoverOrphans(dir) {
|
|
16930
|
+
try {
|
|
16931
|
+
const dirExists = await fs8.stat(dir).then(() => true).catch(() => false);
|
|
16932
|
+
const backup = `${dir}.tgo-backup`;
|
|
16933
|
+
const staging = `${dir}.tgo-staging`;
|
|
16934
|
+
if (!dirExists) {
|
|
16935
|
+
const backupExists = await fs8.stat(backup).then(() => true).catch(() => false);
|
|
16936
|
+
if (backupExists) {
|
|
16937
|
+
try {
|
|
16938
|
+
await fs8.rename(backup, dir);
|
|
16939
|
+
} catch {}
|
|
16940
|
+
}
|
|
16941
|
+
return;
|
|
16942
|
+
}
|
|
16943
|
+
await rmRf(staging);
|
|
16944
|
+
await rmRf(backup);
|
|
16945
|
+
} catch {}
|
|
16946
|
+
}
|
|
16947
|
+
async function rmRf(p) {
|
|
16948
|
+
try {
|
|
16949
|
+
await fs8.rm(p, { recursive: true, force: true });
|
|
16950
|
+
} catch {}
|
|
16951
|
+
}
|
|
16952
|
+
async function copyDir(src, dest) {
|
|
16953
|
+
const cp2 = fs8.cp;
|
|
16954
|
+
if (typeof cp2 === "function") {
|
|
16955
|
+
await cp2.call(fs8, src, dest, { recursive: true, force: true });
|
|
16956
|
+
return;
|
|
16957
|
+
}
|
|
16958
|
+
await fs8.mkdir(dest, { recursive: true });
|
|
16959
|
+
const entries = await fs8.readdir(src, { withFileTypes: true });
|
|
16960
|
+
for (const e of entries) {
|
|
16961
|
+
const s = path9.join(src, e.name);
|
|
16962
|
+
const d = path9.join(dest, e.name);
|
|
16963
|
+
if (e.isDirectory()) {
|
|
16964
|
+
await copyDir(s, d);
|
|
16965
|
+
} else if (e.isSymbolicLink()) {
|
|
16966
|
+
const target = await fs8.readlink(s);
|
|
16967
|
+
await fs8.symlink(target, d);
|
|
16968
|
+
} else {
|
|
16969
|
+
await fs8.copyFile(s, d);
|
|
16970
|
+
}
|
|
16971
|
+
}
|
|
16972
|
+
}
|
|
16973
|
+
async function selfUpdate(deps) {
|
|
16974
|
+
try {
|
|
16975
|
+
let latest;
|
|
16976
|
+
try {
|
|
16977
|
+
latest = await deps.fetchLatest();
|
|
16978
|
+
} catch {
|
|
16979
|
+
return;
|
|
16980
|
+
}
|
|
16981
|
+
if (!latest)
|
|
16982
|
+
return;
|
|
16983
|
+
if (typeof latest !== "string" || latest.trim().length === 0)
|
|
16984
|
+
return;
|
|
16985
|
+
if (!shouldRefresh(deps.runningVersion, latest))
|
|
16986
|
+
return;
|
|
16987
|
+
const cacheRoot = resolveCacheRoot(deps.homeDir);
|
|
16988
|
+
const dirs = slotDirs(cacheRoot, deps.pkgName);
|
|
16989
|
+
if (dirs.length === 0)
|
|
16990
|
+
return;
|
|
16991
|
+
const nowMs = deps.now ? deps.now().getTime() : Date.now();
|
|
16992
|
+
for (const dir of dirs) {
|
|
16993
|
+
try {
|
|
16994
|
+
await recoverOrphans(dir);
|
|
16995
|
+
} catch {}
|
|
16996
|
+
const lockPath = path9.join(dir, LOCK_FILE);
|
|
16997
|
+
const staging = `${dir}.tgo-staging`;
|
|
16998
|
+
const backup = `${dir}.tgo-backup`;
|
|
16999
|
+
let ownerToken = null;
|
|
17000
|
+
let acquired = false;
|
|
17001
|
+
try {
|
|
17002
|
+
try {
|
|
17003
|
+
await fs8.mkdir(dir, { recursive: true });
|
|
17004
|
+
} catch {}
|
|
17005
|
+
const token = `${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2)}`;
|
|
17006
|
+
const tryAcquire = async () => {
|
|
17007
|
+
let handle;
|
|
17008
|
+
try {
|
|
17009
|
+
handle = await fs8.open(lockPath, "wx");
|
|
17010
|
+
try {
|
|
17011
|
+
await handle.writeFile(token, "utf-8");
|
|
17012
|
+
} catch {}
|
|
17013
|
+
ownerToken = token;
|
|
17014
|
+
acquired = true;
|
|
17015
|
+
return true;
|
|
17016
|
+
} catch (err) {
|
|
17017
|
+
const code = err?.code;
|
|
17018
|
+
if (code !== "EEXIST")
|
|
17019
|
+
return false;
|
|
17020
|
+
return false;
|
|
17021
|
+
} finally {
|
|
17022
|
+
if (handle) {
|
|
17023
|
+
try {
|
|
17024
|
+
await handle.close();
|
|
17025
|
+
} catch {}
|
|
17026
|
+
}
|
|
17027
|
+
}
|
|
17028
|
+
};
|
|
17029
|
+
let ok = await tryAcquire();
|
|
17030
|
+
if (!ok) {
|
|
17031
|
+
try {
|
|
17032
|
+
const stat3 = await fs8.stat(lockPath);
|
|
17033
|
+
const age = nowMs - stat3.mtimeMs;
|
|
17034
|
+
if (age > LOCK_STALE_MS) {
|
|
17035
|
+
try {
|
|
17036
|
+
await fs8.unlink(lockPath);
|
|
17037
|
+
} catch {}
|
|
17038
|
+
ok = await tryAcquire();
|
|
17039
|
+
if (!ok)
|
|
17040
|
+
continue;
|
|
17041
|
+
} else {
|
|
17042
|
+
continue;
|
|
17043
|
+
}
|
|
17044
|
+
} catch {
|
|
17045
|
+
continue;
|
|
17046
|
+
}
|
|
17047
|
+
}
|
|
17048
|
+
if (!acquired || !ownerToken)
|
|
17049
|
+
continue;
|
|
17050
|
+
let innerError = null;
|
|
17051
|
+
let newVersionForLog = null;
|
|
17052
|
+
try {
|
|
17053
|
+
try {
|
|
17054
|
+
await rmRf(staging);
|
|
17055
|
+
await copyDir(dir, staging);
|
|
17056
|
+
} catch (e) {
|
|
17057
|
+
throw new Error(`self-update staging failed for ${dir}: ${String(e)}`);
|
|
17058
|
+
}
|
|
17059
|
+
const args = buildInstallArgs(staging, deps.pkgName);
|
|
17060
|
+
let result;
|
|
17061
|
+
try {
|
|
17062
|
+
result = await deps.spawn(args);
|
|
17063
|
+
} catch (e) {
|
|
17064
|
+
throw new Error(`self-update spawn failed for ${dir}: ${String(e)}`);
|
|
17065
|
+
}
|
|
17066
|
+
if (result.exitCode !== 0) {
|
|
17067
|
+
throw new Error(`self-update failed for ${dir}: exit ${result.exitCode} ${result.stderr || result.stdout}`.trim());
|
|
17068
|
+
}
|
|
17069
|
+
let newVersion = "";
|
|
17070
|
+
try {
|
|
17071
|
+
const pkgJsonPath = path9.join(staging, "node_modules", deps.pkgName, "package.json");
|
|
17072
|
+
const raw = await fs8.readFile(pkgJsonPath, "utf-8");
|
|
17073
|
+
const json2 = JSON.parse(raw);
|
|
17074
|
+
newVersion = typeof json2.version === "string" ? json2.version : "";
|
|
17075
|
+
} catch (e) {
|
|
17076
|
+
throw new Error(`self-update verification failed for ${dir}: ${String(e)}`);
|
|
17077
|
+
}
|
|
17078
|
+
if (!newVersion) {
|
|
17079
|
+
throw new Error(`self-update verification failed for ${dir}: missing version`);
|
|
17080
|
+
}
|
|
17081
|
+
if (!semverGt(newVersion, deps.runningVersion)) {
|
|
17082
|
+
throw new Error(`self-update verification failed for ${dir}: installed ${newVersion} not > ${deps.runningVersion}`);
|
|
17083
|
+
}
|
|
17084
|
+
newVersionForLog = newVersion;
|
|
17085
|
+
try {
|
|
17086
|
+
await rmRf(backup);
|
|
17087
|
+
await fs8.rename(dir, backup);
|
|
17088
|
+
await fs8.rename(staging, dir);
|
|
17089
|
+
await rmRf(backup);
|
|
17090
|
+
} catch (e) {
|
|
17091
|
+
try {
|
|
17092
|
+
const backupExists = await fs8.stat(backup).then(() => true).catch(() => false);
|
|
17093
|
+
const dirExists = await fs8.stat(dir).then(() => true).catch(() => false);
|
|
17094
|
+
if (backupExists && !dirExists) {
|
|
17095
|
+
try {
|
|
17096
|
+
await fs8.rename(backup, dir);
|
|
17097
|
+
} catch {}
|
|
17098
|
+
}
|
|
17099
|
+
await rmRf(staging);
|
|
17100
|
+
} catch {}
|
|
17101
|
+
throw new Error(`self-update swap failed for ${dir}: ${String(e)}`);
|
|
17102
|
+
}
|
|
17103
|
+
try {
|
|
17104
|
+
deps.log("info", `self-updated ${deps.pkgName} to ${newVersionForLog} — restart opencode to activate`);
|
|
17105
|
+
} catch {}
|
|
17106
|
+
} catch (e) {
|
|
17107
|
+
innerError = e;
|
|
17108
|
+
try {
|
|
17109
|
+
const backupExists = await fs8.stat(backup).then(() => true).catch(() => false);
|
|
17110
|
+
const dirExists = await fs8.stat(dir).then(() => true).catch(() => false);
|
|
17111
|
+
if (backupExists && !dirExists) {
|
|
17112
|
+
try {
|
|
17113
|
+
await fs8.rename(backup, dir);
|
|
17114
|
+
} catch {}
|
|
17115
|
+
}
|
|
17116
|
+
await rmRf(staging);
|
|
17117
|
+
} catch {}
|
|
17118
|
+
try {
|
|
17119
|
+
const msg = String(e?.message ?? e);
|
|
17120
|
+
if (msg.includes("self-update")) {
|
|
17121
|
+
deps.log("warn", msg);
|
|
17122
|
+
} else {
|
|
17123
|
+
deps.log("warn", `self-update failed for ${dir}: ${String(e)}`);
|
|
17124
|
+
}
|
|
17125
|
+
} catch {}
|
|
17126
|
+
}
|
|
17127
|
+
} finally {
|
|
17128
|
+
try {
|
|
17129
|
+
if (ownerToken) {
|
|
17130
|
+
const cur = await fs8.readFile(lockPath, "utf-8").catch(() => "");
|
|
17131
|
+
if (cur === ownerToken) {
|
|
17132
|
+
await fs8.unlink(lockPath).catch(() => {});
|
|
17133
|
+
}
|
|
17134
|
+
}
|
|
17135
|
+
} catch {}
|
|
17136
|
+
}
|
|
17137
|
+
}
|
|
17138
|
+
} catch {
|
|
17139
|
+
return;
|
|
17140
|
+
}
|
|
17141
|
+
}
|
|
17142
|
+
|
|
17143
|
+
// src/plugin.ts
|
|
17144
|
+
import * as path10 from "node:path";
|
|
17145
|
+
import * as os3 from "node:os";
|
|
16807
17146
|
var TgoPlugin = async ({ client, $, project, directory, worktree }, options) => {
|
|
16808
17147
|
const config2 = await loadTgoConfig(options);
|
|
16809
17148
|
const appLog = (level, message, extra) => {
|
|
@@ -16820,7 +17159,37 @@ var TgoPlugin = async ({ client, $, project, directory, worktree }, options) =>
|
|
|
16820
17159
|
}
|
|
16821
17160
|
}).catch(() => {});
|
|
16822
17161
|
}
|
|
16823
|
-
|
|
17162
|
+
if (config2.selfUpdate?.enabled !== false) {
|
|
17163
|
+
(async () => {
|
|
17164
|
+
try {
|
|
17165
|
+
const runningVersion = await readLocalVersion() ?? "0.0.0";
|
|
17166
|
+
await selfUpdate({
|
|
17167
|
+
runningVersion,
|
|
17168
|
+
pkgName: PLUGIN_NPM_NAME,
|
|
17169
|
+
fetchLatest: () => fetchLatestVersion().then((v) => v ?? undefined),
|
|
17170
|
+
spawn: async (args) => {
|
|
17171
|
+
try {
|
|
17172
|
+
const proc = Bun.spawn(args, {
|
|
17173
|
+
stdout: "pipe",
|
|
17174
|
+
stderr: "pipe",
|
|
17175
|
+
env: BD_ENV
|
|
17176
|
+
});
|
|
17177
|
+
const [stdout, stderr, exitCode] = await Promise.all([
|
|
17178
|
+
new Response(proc.stdout).text(),
|
|
17179
|
+
new Response(proc.stderr).text(),
|
|
17180
|
+
proc.exited
|
|
17181
|
+
]);
|
|
17182
|
+
return { exitCode, stdout, stderr };
|
|
17183
|
+
} catch (error51) {
|
|
17184
|
+
return { exitCode: 1, stdout: "", stderr: String(error51) };
|
|
17185
|
+
}
|
|
17186
|
+
},
|
|
17187
|
+
log: (level, msg) => appLog(level, msg)
|
|
17188
|
+
});
|
|
17189
|
+
} catch {}
|
|
17190
|
+
})().catch(() => {});
|
|
17191
|
+
}
|
|
17192
|
+
const seatDir = config2.agentDir ?? path10.join(os3.homedir(), ".config", "opencode", "agent");
|
|
16824
17193
|
try {
|
|
16825
17194
|
const checked = await validateAgentDir(seatDir);
|
|
16826
17195
|
if (checked > 0) {
|
|
@@ -16899,7 +17268,7 @@ var TgoPlugin = async ({ client, $, project, directory, worktree }, options) =>
|
|
|
16899
17268
|
try {
|
|
16900
17269
|
const args = command.split(/\s+/);
|
|
16901
17270
|
const proc = cwd ? $`${args}`.cwd(cwd) : $`${args}`;
|
|
16902
|
-
const completed = await proc.env({ ...process.env, BD_NON_INTERACTIVE: "1", HOME:
|
|
17271
|
+
const completed = await proc.env({ ...process.env, BD_NON_INTERACTIVE: "1", HOME: os3.homedir() }).nothrow();
|
|
16903
17272
|
return {
|
|
16904
17273
|
exitCode: completed.exitCode,
|
|
16905
17274
|
stdout: completed.stdout.toString(),
|
|
@@ -17032,7 +17401,7 @@ var TgoPlugin = async ({ client, $, project, directory, worktree }, options) =>
|
|
|
17032
17401
|
const nextPermission = preapproveExternalDirectory(input.permission, worktreeRoot);
|
|
17033
17402
|
if (nextPermission && Object.keys(nextPermission).length > 0) {
|
|
17034
17403
|
input.permission = nextPermission;
|
|
17035
|
-
const parent = worktreeRoot ?
|
|
17404
|
+
const parent = worktreeRoot ? path10.dirname(worktreeRoot) : undefined;
|
|
17036
17405
|
appLog("info", `pre-approved external_directory for worktree family ${parent}/*`, {
|
|
17037
17406
|
worktreeRoot,
|
|
17038
17407
|
projectWorktree: project?.worktree ?? null,
|
package/package.json
CHANGED
|
@@ -145,6 +145,17 @@
|
|
|
145
145
|
},
|
|
146
146
|
"additionalProperties": false
|
|
147
147
|
},
|
|
148
|
+
"selfUpdate": {
|
|
149
|
+
"description": "Self-update settings.",
|
|
150
|
+
"type": "object",
|
|
151
|
+
"properties": {
|
|
152
|
+
"enabled": {
|
|
153
|
+
"type": "boolean",
|
|
154
|
+
"default": true
|
|
155
|
+
}
|
|
156
|
+
},
|
|
157
|
+
"additionalProperties": false
|
|
158
|
+
},
|
|
148
159
|
"presets": {
|
|
149
160
|
"description": "Partial override of the built-in seat→model map. Missing presets fall back to built-ins. Model IDs are free-form strings.",
|
|
150
161
|
"type": "object",
|
package/src/config.ts
CHANGED
|
@@ -75,6 +75,10 @@ const terminationConfig = z.object({
|
|
|
75
75
|
enabled: z.boolean().default(true),
|
|
76
76
|
});
|
|
77
77
|
|
|
78
|
+
const selfUpdateConfig = z.object({
|
|
79
|
+
enabled: z.boolean().default(true),
|
|
80
|
+
});
|
|
81
|
+
|
|
78
82
|
export const tgoConfigSchema = z.object({
|
|
79
83
|
preset: z.enum(PRESET_NAMES).default("balanced"),
|
|
80
84
|
presets: z
|
|
@@ -100,6 +104,7 @@ export const tgoConfigSchema = z.object({
|
|
|
100
104
|
})),
|
|
101
105
|
sessionReuse: sessionReuseConfig.optional().default(() => ({ enabled: true, maxContextTokens: 100000 })),
|
|
102
106
|
termination: terminationConfig.optional().default(() => ({ enabled: true })),
|
|
107
|
+
selfUpdate: selfUpdateConfig.optional().default(() => ({ enabled: true })),
|
|
103
108
|
});
|
|
104
109
|
|
|
105
110
|
export type TgoConfig = z.infer<typeof tgoConfigSchema>;
|
package/src/plugin.ts
CHANGED
|
@@ -15,8 +15,9 @@ import { validateDelegationBoundary, validateDelegationPacket, verifyClaimObserv
|
|
|
15
15
|
import { captureDelegationSession, probeSessionReuseCapability, persistAbortHandback } from "./session-reuse";
|
|
16
16
|
import { authorizeLifecycleSession, evaluateClosure, verifyClaimObserved } from "./lifecycle";
|
|
17
17
|
import { loadBeadsTui, renderBeadsTui } from "./tui";
|
|
18
|
-
import { checkVersionDrift } from "./version";
|
|
18
|
+
import { checkVersionDrift, fetchLatestVersion, PLUGIN_NPM_NAME, readLocalVersion } from "./version";
|
|
19
19
|
import { parseCompletionSignal, terminationDecision, type CompletionSignal } from "./termination";
|
|
20
|
+
import { selfUpdate } from "./self-update";
|
|
20
21
|
// No runtime function re-exports here: opencode's legacy plugin loader calls
|
|
21
22
|
// EVERY exported function as a plugin factory (input, options), so an entry
|
|
22
23
|
// re-export like evaluateClosure gets invoked as one and throws inside the
|
|
@@ -63,6 +64,37 @@ export const TgoPlugin: Plugin = async (
|
|
|
63
64
|
.catch(() => {});
|
|
64
65
|
}
|
|
65
66
|
|
|
67
|
+
if (config.selfUpdate?.enabled !== false) {
|
|
68
|
+
void (async () => {
|
|
69
|
+
try {
|
|
70
|
+
const runningVersion = (await readLocalVersion()) ?? "0.0.0";
|
|
71
|
+
await selfUpdate({
|
|
72
|
+
runningVersion,
|
|
73
|
+
pkgName: PLUGIN_NPM_NAME,
|
|
74
|
+
fetchLatest: () => fetchLatestVersion().then((v) => v ?? undefined),
|
|
75
|
+
spawn: async (args: string[]) => {
|
|
76
|
+
try {
|
|
77
|
+
const proc = Bun.spawn(args, {
|
|
78
|
+
stdout: "pipe",
|
|
79
|
+
stderr: "pipe",
|
|
80
|
+
env: BD_ENV as unknown as Record<string, string>,
|
|
81
|
+
});
|
|
82
|
+
const [stdout, stderr, exitCode] = await Promise.all([
|
|
83
|
+
new Response(proc.stdout).text(),
|
|
84
|
+
new Response(proc.stderr).text(),
|
|
85
|
+
proc.exited,
|
|
86
|
+
]);
|
|
87
|
+
return { exitCode, stdout, stderr };
|
|
88
|
+
} catch (error) {
|
|
89
|
+
return { exitCode: 1, stdout: "", stderr: String(error) };
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
log: (level, msg) => appLog(level, msg),
|
|
93
|
+
});
|
|
94
|
+
} catch {}
|
|
95
|
+
})().catch(() => {});
|
|
96
|
+
}
|
|
97
|
+
|
|
66
98
|
const seatDir =
|
|
67
99
|
config.agentDir ?? path.join(os.homedir(), ".config", "opencode", "agent");
|
|
68
100
|
// Load-time budget re-check. Warn, never throw: a throwing factory makes
|
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
import * as fs from "node:fs/promises";
|
|
2
|
+
import * as fsSync from "node:fs";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import * as os from "node:os";
|
|
5
|
+
|
|
6
|
+
export const LOCK_STALE_MS = 120_000;
|
|
7
|
+
export const LOCK_FILE = ".tgo-selfupdate.lock";
|
|
8
|
+
|
|
9
|
+
export function resolveCacheRoot(homeDir?: string): string {
|
|
10
|
+
const base =
|
|
11
|
+
process.env.OPENCODE_TEST_HOME ??
|
|
12
|
+
process.env.XDG_CACHE_HOME ??
|
|
13
|
+
path.join(homeDir ?? os.homedir(), ".cache");
|
|
14
|
+
return path.join(base, "opencode");
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function slotDirs(cacheRoot: string, pkgName: string): string[] {
|
|
18
|
+
const candidates = [
|
|
19
|
+
path.join(cacheRoot, "packages", `${pkgName}@latest`),
|
|
20
|
+
path.join(cacheRoot, "packages", pkgName),
|
|
21
|
+
];
|
|
22
|
+
return candidates.filter((dir) => {
|
|
23
|
+
try {
|
|
24
|
+
return fsSync.existsSync(dir) && fsSync.statSync(dir).isDirectory();
|
|
25
|
+
} catch {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function parseSemver(v: string): { major: number; minor: number; patch: number; prerelease: string[] | null } | null {
|
|
32
|
+
if (typeof v !== "string") return null;
|
|
33
|
+
let s = v.trim().replace(/^v/, "");
|
|
34
|
+
if (s.length === 0) return null;
|
|
35
|
+
// strip build metadata
|
|
36
|
+
const plusIdx = s.indexOf("+");
|
|
37
|
+
if (plusIdx !== -1) {
|
|
38
|
+
const build = s.slice(plusIdx + 1);
|
|
39
|
+
if (build.length === 0) return null;
|
|
40
|
+
const buildIds = build.split(".");
|
|
41
|
+
for (const id of buildIds) {
|
|
42
|
+
if (id.length === 0 || !/^[0-9A-Za-z-]+$/.test(id)) return null;
|
|
43
|
+
}
|
|
44
|
+
s = s.slice(0, plusIdx);
|
|
45
|
+
}
|
|
46
|
+
let coreStr: string;
|
|
47
|
+
let preStr: string | null = null;
|
|
48
|
+
const dashIdx = s.indexOf("-");
|
|
49
|
+
if (dashIdx !== -1) {
|
|
50
|
+
coreStr = s.slice(0, dashIdx);
|
|
51
|
+
preStr = s.slice(dashIdx + 1);
|
|
52
|
+
if (preStr.length === 0) return null;
|
|
53
|
+
} else {
|
|
54
|
+
coreStr = s;
|
|
55
|
+
}
|
|
56
|
+
const coreParts = coreStr.split(".");
|
|
57
|
+
if (coreParts.length !== 3) return null;
|
|
58
|
+
const nums: number[] = [];
|
|
59
|
+
for (const p of coreParts) {
|
|
60
|
+
if (!/^(0|[1-9]\d*)$/.test(p)) return null;
|
|
61
|
+
nums.push(parseInt(p, 10));
|
|
62
|
+
}
|
|
63
|
+
let prerelease: string[] | null = null;
|
|
64
|
+
if (preStr !== null) {
|
|
65
|
+
const ids = preStr.split(".");
|
|
66
|
+
for (const id of ids) {
|
|
67
|
+
if (id.length === 0 || !/^[0-9A-Za-z-]+$/.test(id)) return null;
|
|
68
|
+
if (/^[0-9]+$/.test(id) && !/^(0|[1-9]\d*)$/.test(id)) return null;
|
|
69
|
+
}
|
|
70
|
+
prerelease = ids;
|
|
71
|
+
}
|
|
72
|
+
return { major: nums[0]!, minor: nums[1]!, patch: nums[2]!, prerelease };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function semverGt(a: string, b: string): boolean {
|
|
76
|
+
const pa = parseSemver(a);
|
|
77
|
+
const pb = parseSemver(b);
|
|
78
|
+
if (!pa || !pb) return false;
|
|
79
|
+
if (pa.major !== pb.major) return pa.major > pb.major;
|
|
80
|
+
if (pa.minor !== pb.minor) return pa.minor > pb.minor;
|
|
81
|
+
if (pa.patch !== pb.patch) return pa.patch > pb.patch;
|
|
82
|
+
const aPre = pa.prerelease;
|
|
83
|
+
const bPre = pb.prerelease;
|
|
84
|
+
if (aPre === null && bPre === null) return false;
|
|
85
|
+
if (aPre === null && bPre !== null) return true;
|
|
86
|
+
if (aPre !== null && bPre === null) return false;
|
|
87
|
+
// both have prerelease
|
|
88
|
+
const aA = aPre!;
|
|
89
|
+
const bA = bPre!;
|
|
90
|
+
const len = Math.min(aA.length, bA.length);
|
|
91
|
+
for (let i = 0; i < len; i++) {
|
|
92
|
+
const aId = aA[i]!;
|
|
93
|
+
const bId = bA[i]!;
|
|
94
|
+
if (aId === bId) continue;
|
|
95
|
+
const aIsNum = /^[0-9]+$/.test(aId);
|
|
96
|
+
const bIsNum = /^[0-9]+$/.test(bId);
|
|
97
|
+
if (aIsNum && bIsNum) {
|
|
98
|
+
return parseInt(aId, 10) > parseInt(bId, 10);
|
|
99
|
+
}
|
|
100
|
+
if (aIsNum && !bIsNum) return false;
|
|
101
|
+
if (!aIsNum && bIsNum) return true;
|
|
102
|
+
// both alphanumeric lexical (ASCII)
|
|
103
|
+
return aId > bId;
|
|
104
|
+
}
|
|
105
|
+
return aA.length > bA.length;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function shouldRefresh(runningVersion: string, latestVersion: string): boolean {
|
|
109
|
+
return semverGt(latestVersion, runningVersion);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function buildInstallArgs(dir: string, pkgName: string): string[] {
|
|
113
|
+
return ["npm", "install", "--prefix", dir, `${pkgName}@latest`, "--save-exact", "--ignore-scripts", "--no-audit", "--no-fund"];
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// deprecated compat: keep for old callers/tests
|
|
117
|
+
export function buildInstallCommand(dir: string, pkgName: string): string {
|
|
118
|
+
return buildInstallArgs(dir, pkgName).join(" ");
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export async function recoverOrphans(dir: string): Promise<void> {
|
|
122
|
+
try {
|
|
123
|
+
const dirExists = await fs.stat(dir).then(() => true).catch(() => false);
|
|
124
|
+
const backup = `${dir}.tgo-backup`;
|
|
125
|
+
const staging = `${dir}.tgo-staging`;
|
|
126
|
+
if (!dirExists) {
|
|
127
|
+
const backupExists = await fs.stat(backup).then(() => true).catch(() => false);
|
|
128
|
+
if (backupExists) {
|
|
129
|
+
try {
|
|
130
|
+
await fs.rename(backup, dir);
|
|
131
|
+
} catch {}
|
|
132
|
+
}
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
await rmRf(staging);
|
|
136
|
+
await rmRf(backup);
|
|
137
|
+
} catch {}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async function rmRf(p: string): Promise<void> {
|
|
141
|
+
try {
|
|
142
|
+
await fs.rm(p, { recursive: true, force: true });
|
|
143
|
+
} catch {}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async function copyDir(src: string, dest: string): Promise<void> {
|
|
147
|
+
// prefer fs.cp if available
|
|
148
|
+
const cp = (fs as unknown as { cp?: (s: string, d: string, opts: unknown) => Promise<void> }).cp;
|
|
149
|
+
if (typeof cp === "function") {
|
|
150
|
+
await cp.call(fs, src, dest, { recursive: true, force: true });
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
await fs.mkdir(dest, { recursive: true });
|
|
154
|
+
const entries = await fs.readdir(src, { withFileTypes: true });
|
|
155
|
+
for (const e of entries) {
|
|
156
|
+
const s = path.join(src, e.name);
|
|
157
|
+
const d = path.join(dest, e.name);
|
|
158
|
+
if (e.isDirectory()) {
|
|
159
|
+
await copyDir(s, d);
|
|
160
|
+
} else if (e.isSymbolicLink()) {
|
|
161
|
+
const target = await fs.readlink(s);
|
|
162
|
+
await fs.symlink(target, d);
|
|
163
|
+
} else {
|
|
164
|
+
await fs.copyFile(s, d);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export async function selfUpdate(deps: {
|
|
170
|
+
runningVersion: string;
|
|
171
|
+
pkgName: string;
|
|
172
|
+
fetchLatest: () => Promise<string | undefined>;
|
|
173
|
+
spawn: (args: string[]) => Promise<{ exitCode: number; stdout: string; stderr: string }>;
|
|
174
|
+
log: (level: "info" | "warn", msg: string) => void;
|
|
175
|
+
homeDir?: string;
|
|
176
|
+
now?: () => Date;
|
|
177
|
+
}): Promise<void> {
|
|
178
|
+
try {
|
|
179
|
+
let latest: string | undefined | null;
|
|
180
|
+
try {
|
|
181
|
+
latest = await deps.fetchLatest();
|
|
182
|
+
} catch {
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
if (!latest) return;
|
|
186
|
+
if (typeof latest !== "string" || latest.trim().length === 0) return;
|
|
187
|
+
if (!shouldRefresh(deps.runningVersion, latest)) return;
|
|
188
|
+
|
|
189
|
+
const cacheRoot = resolveCacheRoot(deps.homeDir);
|
|
190
|
+
const dirs = slotDirs(cacheRoot, deps.pkgName);
|
|
191
|
+
if (dirs.length === 0) return;
|
|
192
|
+
|
|
193
|
+
const nowMs = deps.now ? deps.now().getTime() : Date.now();
|
|
194
|
+
|
|
195
|
+
for (const dir of dirs) {
|
|
196
|
+
try {
|
|
197
|
+
await recoverOrphans(dir);
|
|
198
|
+
} catch {}
|
|
199
|
+
const lockPath = path.join(dir, LOCK_FILE);
|
|
200
|
+
const staging = `${dir}.tgo-staging`;
|
|
201
|
+
const backup = `${dir}.tgo-backup`;
|
|
202
|
+
let ownerToken: string | null = null;
|
|
203
|
+
let acquired = false;
|
|
204
|
+
|
|
205
|
+
try {
|
|
206
|
+
try {
|
|
207
|
+
await fs.mkdir(dir, { recursive: true });
|
|
208
|
+
} catch {}
|
|
209
|
+
const token = `${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2)}`;
|
|
210
|
+
|
|
211
|
+
const tryAcquire = async (): Promise<boolean> => {
|
|
212
|
+
let handle: fs.FileHandle | undefined;
|
|
213
|
+
try {
|
|
214
|
+
handle = await fs.open(lockPath, "wx");
|
|
215
|
+
try {
|
|
216
|
+
await handle.writeFile(token, "utf-8");
|
|
217
|
+
} catch {}
|
|
218
|
+
ownerToken = token;
|
|
219
|
+
acquired = true;
|
|
220
|
+
return true;
|
|
221
|
+
} catch (err) {
|
|
222
|
+
const code = (err as NodeJS.ErrnoException)?.code;
|
|
223
|
+
if (code !== "EEXIST") return false;
|
|
224
|
+
return false;
|
|
225
|
+
} finally {
|
|
226
|
+
if (handle) {
|
|
227
|
+
try {
|
|
228
|
+
await handle.close();
|
|
229
|
+
} catch {}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
let ok = await tryAcquire();
|
|
235
|
+
if (!ok) {
|
|
236
|
+
try {
|
|
237
|
+
const stat = await fs.stat(lockPath);
|
|
238
|
+
const age = nowMs - stat.mtimeMs;
|
|
239
|
+
if (age > LOCK_STALE_MS) {
|
|
240
|
+
try {
|
|
241
|
+
await fs.unlink(lockPath);
|
|
242
|
+
} catch {}
|
|
243
|
+
ok = await tryAcquire();
|
|
244
|
+
if (!ok) continue;
|
|
245
|
+
} else {
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
} catch {
|
|
249
|
+
continue;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
if (!acquired || !ownerToken) continue;
|
|
253
|
+
|
|
254
|
+
let innerError: unknown = null;
|
|
255
|
+
let newVersionForLog: string | null = null;
|
|
256
|
+
|
|
257
|
+
try {
|
|
258
|
+
// (b) staging copy
|
|
259
|
+
try {
|
|
260
|
+
await rmRf(staging);
|
|
261
|
+
await copyDir(dir, staging);
|
|
262
|
+
} catch (e) {
|
|
263
|
+
throw new Error(`self-update staging failed for ${dir}: ${String(e)}`);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// (c) npm install with --prefix staging
|
|
267
|
+
const args = buildInstallArgs(staging, deps.pkgName);
|
|
268
|
+
let result: { exitCode: number; stdout: string; stderr: string };
|
|
269
|
+
try {
|
|
270
|
+
result = await deps.spawn(args);
|
|
271
|
+
} catch (e) {
|
|
272
|
+
throw new Error(`self-update spawn failed for ${dir}: ${String(e)}`);
|
|
273
|
+
}
|
|
274
|
+
if (result.exitCode !== 0) {
|
|
275
|
+
throw new Error(`self-update failed for ${dir}: exit ${result.exitCode} ${result.stderr || result.stdout}`.trim());
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// (d) verify staging version gt running
|
|
279
|
+
let newVersion = "";
|
|
280
|
+
try {
|
|
281
|
+
const pkgJsonPath = path.join(staging, "node_modules", deps.pkgName, "package.json");
|
|
282
|
+
const raw = await fs.readFile(pkgJsonPath, "utf-8");
|
|
283
|
+
const json = JSON.parse(raw) as { version?: unknown };
|
|
284
|
+
newVersion = typeof json.version === "string" ? json.version : "";
|
|
285
|
+
} catch (e) {
|
|
286
|
+
throw new Error(`self-update verification failed for ${dir}: ${String(e)}`);
|
|
287
|
+
}
|
|
288
|
+
if (!newVersion) {
|
|
289
|
+
throw new Error(`self-update verification failed for ${dir}: missing version`);
|
|
290
|
+
}
|
|
291
|
+
if (!semverGt(newVersion, deps.runningVersion)) {
|
|
292
|
+
throw new Error(`self-update verification failed for ${dir}: installed ${newVersion} not > ${deps.runningVersion}`);
|
|
293
|
+
}
|
|
294
|
+
newVersionForLog = newVersion;
|
|
295
|
+
|
|
296
|
+
// (e) swap
|
|
297
|
+
try {
|
|
298
|
+
await rmRf(backup);
|
|
299
|
+
await fs.rename(dir, backup);
|
|
300
|
+
await fs.rename(staging, dir);
|
|
301
|
+
await rmRf(backup);
|
|
302
|
+
} catch (e) {
|
|
303
|
+
// swap failure: try to restore backup if dir missing
|
|
304
|
+
try {
|
|
305
|
+
const backupExists = await fs.stat(backup).then(() => true).catch(() => false);
|
|
306
|
+
const dirExists = await fs.stat(dir).then(() => true).catch(() => false);
|
|
307
|
+
if (backupExists && !dirExists) {
|
|
308
|
+
try {
|
|
309
|
+
await fs.rename(backup, dir);
|
|
310
|
+
} catch {}
|
|
311
|
+
}
|
|
312
|
+
await rmRf(staging);
|
|
313
|
+
} catch {}
|
|
314
|
+
throw new Error(`self-update swap failed for ${dir}: ${String(e)}`);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// success log
|
|
318
|
+
try {
|
|
319
|
+
deps.log("info", `self-updated ${deps.pkgName} to ${newVersionForLog} — restart opencode to activate`);
|
|
320
|
+
} catch {}
|
|
321
|
+
} catch (e) {
|
|
322
|
+
innerError = e;
|
|
323
|
+
// (f) ANY failure → rm staging, restore backup if needed, log warn, original untouched
|
|
324
|
+
try {
|
|
325
|
+
const backupExists = await fs.stat(backup).then(() => true).catch(() => false);
|
|
326
|
+
const dirExists = await fs.stat(dir).then(() => true).catch(() => false);
|
|
327
|
+
if (backupExists && !dirExists) {
|
|
328
|
+
try {
|
|
329
|
+
await fs.rename(backup, dir);
|
|
330
|
+
} catch {}
|
|
331
|
+
}
|
|
332
|
+
await rmRf(staging);
|
|
333
|
+
} catch {}
|
|
334
|
+
try {
|
|
335
|
+
const msg = String((e as Error)?.message ?? e);
|
|
336
|
+
// ensure warn contains needed substrings; if message already contains warn-like text, log it directly
|
|
337
|
+
if (msg.includes("self-update")) {
|
|
338
|
+
deps.log("warn", msg);
|
|
339
|
+
} else {
|
|
340
|
+
deps.log("warn", `self-update failed for ${dir}: ${String(e)}`);
|
|
341
|
+
}
|
|
342
|
+
} catch {}
|
|
343
|
+
}
|
|
344
|
+
// lock release handled in finally below
|
|
345
|
+
} finally {
|
|
346
|
+
try {
|
|
347
|
+
if (ownerToken) {
|
|
348
|
+
const cur = await fs.readFile(lockPath, "utf-8").catch(() => "");
|
|
349
|
+
if (cur === ownerToken) {
|
|
350
|
+
await fs.unlink(lockPath).catch(() => {});
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
} catch {}
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
} catch {
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
}
|