@lawaty/lacode 0.1.6 → 0.1.7
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 +13 -0
- package/dist/agents.js +26 -19
- package/dist/index.js +282 -41
- package/dist/lacode.js +206 -4
- package/dist/pool.js +40 -33
- package/docs/USER-STORIES.md +20 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -283,6 +283,19 @@ option is optional:
|
|
|
283
283
|
| `models` | — | An inline slot list. Highest-priority source. |
|
|
284
284
|
| `modelsFile` | — | An explicit slot file; short-circuits the chain below. |
|
|
285
285
|
| `reapMs` | `30000` | Hang-reaper interval. |
|
|
286
|
+
| `selfUpdate` | `true` | Check for a newer release at most once per ~6h and install it in the background; it applies on the next opencode start. `false` — or `LACODE_NO_SELFUPDATE=1` — turns the check off. |
|
|
287
|
+
|
|
288
|
+
### Staying up to date
|
|
289
|
+
|
|
290
|
+
opencode's plugin cache is **write-once**: it resolves `@lawaty/lacode` to an
|
|
291
|
+
exact version once and never re-resolves — not on restart, not with
|
|
292
|
+
`opencode plugin -g -f`, not with a range pin, not with a tagged config spec. So
|
|
293
|
+
LaCode keeps itself current instead (US-27): at startup it compares its own
|
|
294
|
+
version to npm's `latest` (at most once per ~6h, silent when offline) and, when
|
|
295
|
+
behind, reinstalls its own cache entry in the background using the opencode
|
|
296
|
+
binary — no npm or bun required. The running session keeps its loaded code; the
|
|
297
|
+
new version takes effect on the next start, and the log says so. Run
|
|
298
|
+
`lacode update` to do it on demand, or `lacode update --check` to just report.
|
|
286
299
|
|
|
287
300
|
### Pool preset resolution
|
|
288
301
|
|
package/dist/agents.js
CHANGED
|
@@ -1,25 +1,18 @@
|
|
|
1
1
|
// src/lib/pool.ts
|
|
2
|
-
import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync as readFileSync2, renameSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
|
+
import { join as join2 } from "node:path";
|
|
5
|
+
|
|
6
|
+
// src/lib/package-root.ts
|
|
7
|
+
import { readFileSync } from "node:fs";
|
|
4
8
|
import { dirname, join } from "node:path";
|
|
5
9
|
import { fileURLToPath } from "node:url";
|
|
6
|
-
var
|
|
7
|
-
var DEAD_FILE_MS = 2 * CLAIM_TTL_MS;
|
|
8
|
-
var ORPHAN_TMP_MS = 60 * 1e3;
|
|
9
|
-
var CATALOG_TTL_MS = 10 * 60 * 1e3;
|
|
10
|
-
var STUCK_MIN_AGE_MS = 5 * 60 * 1e3;
|
|
11
|
-
var STUCK_IDLE_MS = 3 * 60 * 1e3;
|
|
12
|
-
var HANG_COOLDOWN_MS = 10 * 60 * 1e3;
|
|
13
|
-
var UNAVAILABLE_COOLDOWN_MS = 60 * 1e3;
|
|
14
|
-
var MAX_SLOTS = 4;
|
|
15
|
-
var COOLDOWN_BASE_MS = 60 * 1e3;
|
|
16
|
-
var COOLDOWN_MAX_MS = 15 * 60 * 1e3;
|
|
17
|
-
var STRIKE_DECAY_MS = 30 * 60 * 1e3;
|
|
10
|
+
var PACKAGE_NAME = "@lawaty/lacode";
|
|
18
11
|
var findPackageRoot = (start) => {
|
|
19
12
|
let dir = start;
|
|
20
13
|
for (let up = 0; up < 6; up++) {
|
|
21
14
|
try {
|
|
22
|
-
if (JSON.parse(readFileSync(join(dir, "package.json"), "utf8")).name ===
|
|
15
|
+
if (JSON.parse(readFileSync(join(dir, "package.json"), "utf8")).name === PACKAGE_NAME) return dir;
|
|
23
16
|
} catch {
|
|
24
17
|
}
|
|
25
18
|
const parent = dirname(dir);
|
|
@@ -29,11 +22,25 @@ var findPackageRoot = (start) => {
|
|
|
29
22
|
return join(start, "..", "..");
|
|
30
23
|
};
|
|
31
24
|
var PACKAGE_ROOT = findPackageRoot(dirname(fileURLToPath(import.meta.url)));
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
var
|
|
35
|
-
var
|
|
36
|
-
var
|
|
25
|
+
|
|
26
|
+
// src/lib/pool.ts
|
|
27
|
+
var CLAIM_TTL_MS = 10 * 60 * 1e3;
|
|
28
|
+
var DEAD_FILE_MS = 2 * CLAIM_TTL_MS;
|
|
29
|
+
var ORPHAN_TMP_MS = 60 * 1e3;
|
|
30
|
+
var CATALOG_TTL_MS = 10 * 60 * 1e3;
|
|
31
|
+
var STUCK_MIN_AGE_MS = 5 * 60 * 1e3;
|
|
32
|
+
var STUCK_IDLE_MS = 3 * 60 * 1e3;
|
|
33
|
+
var HANG_COOLDOWN_MS = 10 * 60 * 1e3;
|
|
34
|
+
var UNAVAILABLE_COOLDOWN_MS = 60 * 1e3;
|
|
35
|
+
var MAX_SLOTS = 4;
|
|
36
|
+
var COOLDOWN_BASE_MS = 60 * 1e3;
|
|
37
|
+
var COOLDOWN_MAX_MS = 15 * 60 * 1e3;
|
|
38
|
+
var STRIKE_DECAY_MS = 30 * 60 * 1e3;
|
|
39
|
+
var PRESETS_DIR = join2(PACKAGE_ROOT, "presets");
|
|
40
|
+
var DEFAULT_PRESET = join2(PRESETS_DIR, "free-tier.json");
|
|
41
|
+
var USER_CONFIG_DIR = join2(homedir(), ".config", "lacode");
|
|
42
|
+
var USER_MODELS_PATH = join2(USER_CONFIG_DIR, "pool.json");
|
|
43
|
+
var USER_MODELS_TXT = join2(USER_CONFIG_DIR, "pool.txt");
|
|
37
44
|
|
|
38
45
|
// src/agents.ts
|
|
39
46
|
var POOLED_BASES = ["explore", "implement", "context"];
|
package/dist/index.js
CHANGED
|
@@ -1,8 +1,29 @@
|
|
|
1
1
|
// src/lib/pool.ts
|
|
2
|
-
import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync as readFileSync2, renameSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
|
+
import { join as join2 } from "node:path";
|
|
5
|
+
|
|
6
|
+
// src/lib/package-root.ts
|
|
7
|
+
import { readFileSync } from "node:fs";
|
|
4
8
|
import { dirname, join } from "node:path";
|
|
5
9
|
import { fileURLToPath } from "node:url";
|
|
10
|
+
var PACKAGE_NAME = "@lawaty/lacode";
|
|
11
|
+
var findPackageRoot = (start) => {
|
|
12
|
+
let dir = start;
|
|
13
|
+
for (let up = 0; up < 6; up++) {
|
|
14
|
+
try {
|
|
15
|
+
if (JSON.parse(readFileSync(join(dir, "package.json"), "utf8")).name === PACKAGE_NAME) return dir;
|
|
16
|
+
} catch {
|
|
17
|
+
}
|
|
18
|
+
const parent = dirname(dir);
|
|
19
|
+
if (parent === dir) break;
|
|
20
|
+
dir = parent;
|
|
21
|
+
}
|
|
22
|
+
return join(start, "..", "..");
|
|
23
|
+
};
|
|
24
|
+
var PACKAGE_ROOT = findPackageRoot(dirname(fileURLToPath(import.meta.url)));
|
|
25
|
+
|
|
26
|
+
// src/lib/pool.ts
|
|
6
27
|
var CLAIM_TTL_MS = 10 * 60 * 1e3;
|
|
7
28
|
var DEAD_FILE_MS = 2 * CLAIM_TTL_MS;
|
|
8
29
|
var ORPHAN_TMP_MS = 60 * 1e3;
|
|
@@ -17,25 +38,11 @@ var COOLDOWN_BASE_MS = 60 * 1e3;
|
|
|
17
38
|
var COOLDOWN_MAX_MS = 15 * 60 * 1e3;
|
|
18
39
|
var STRIKE_DECAY_MS = 30 * 60 * 1e3;
|
|
19
40
|
var BASES = ["explore", "implement", "context"];
|
|
20
|
-
var
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
} catch {
|
|
26
|
-
}
|
|
27
|
-
const parent = dirname(dir);
|
|
28
|
-
if (parent === dir) break;
|
|
29
|
-
dir = parent;
|
|
30
|
-
}
|
|
31
|
-
return join(start, "..", "..");
|
|
32
|
-
};
|
|
33
|
-
var PACKAGE_ROOT = findPackageRoot(dirname(fileURLToPath(import.meta.url)));
|
|
34
|
-
var PRESETS_DIR = join(PACKAGE_ROOT, "presets");
|
|
35
|
-
var DEFAULT_PRESET = join(PRESETS_DIR, "free-tier.json");
|
|
36
|
-
var USER_CONFIG_DIR = join(homedir(), ".config", "lacode");
|
|
37
|
-
var USER_MODELS_PATH = join(USER_CONFIG_DIR, "pool.json");
|
|
38
|
-
var USER_MODELS_TXT = join(USER_CONFIG_DIR, "pool.txt");
|
|
41
|
+
var PRESETS_DIR = join2(PACKAGE_ROOT, "presets");
|
|
42
|
+
var DEFAULT_PRESET = join2(PRESETS_DIR, "free-tier.json");
|
|
43
|
+
var USER_CONFIG_DIR = join2(homedir(), ".config", "lacode");
|
|
44
|
+
var USER_MODELS_PATH = join2(USER_CONFIG_DIR, "pool.json");
|
|
45
|
+
var USER_MODELS_TXT = join2(USER_CONFIG_DIR, "pool.txt");
|
|
39
46
|
var FALLBACK_SLOTS = [
|
|
40
47
|
{ index: 1, model: "opencode/space-bunny-free", weight: 3 },
|
|
41
48
|
{ index: 2, model: "opencode-go/step-5-preview-free", weight: 2 },
|
|
@@ -89,7 +96,7 @@ function parseSlotText(text, origin) {
|
|
|
89
96
|
}
|
|
90
97
|
var readJSON = (path4) => {
|
|
91
98
|
try {
|
|
92
|
-
return { ok: true, value: JSON.parse(
|
|
99
|
+
return { ok: true, value: JSON.parse(readFileSync2(path4, "utf8")) };
|
|
93
100
|
} catch (error) {
|
|
94
101
|
return { ok: false, error: String(error) };
|
|
95
102
|
}
|
|
@@ -105,7 +112,7 @@ function readOptional(path4, origin, problems) {
|
|
|
105
112
|
function readTextOptional(path4, origin, problems) {
|
|
106
113
|
if (!existsSync(path4)) return void 0;
|
|
107
114
|
try {
|
|
108
|
-
return
|
|
115
|
+
return readFileSync2(path4, "utf8");
|
|
109
116
|
} catch (error) {
|
|
110
117
|
problems.push(`${origin} unreadable (${String(error)}); trying the next source`);
|
|
111
118
|
return void 0;
|
|
@@ -147,7 +154,7 @@ function resolveModels(input = {}) {
|
|
|
147
154
|
function readSlotFile(path4) {
|
|
148
155
|
let text;
|
|
149
156
|
try {
|
|
150
|
-
text =
|
|
157
|
+
text = readFileSync2(path4, "utf8");
|
|
151
158
|
} catch (error) {
|
|
152
159
|
return { slots: FALLBACK_SLOTS, problems: [`${path4} unreadable (${String(error)}); using built-in defaults`] };
|
|
153
160
|
}
|
|
@@ -215,7 +222,7 @@ var retryAfterMs = (headers) => {
|
|
|
215
222
|
};
|
|
216
223
|
function state(opts = {}) {
|
|
217
224
|
const id = opts.id ?? String(process.pid);
|
|
218
|
-
const dir = opts.dir ??
|
|
225
|
+
const dir = opts.dir ?? join2(homedir(), ".local", "share", "opencode", "agent-pool");
|
|
219
226
|
const modelsFile = opts.modelsFile;
|
|
220
227
|
const models = opts.models;
|
|
221
228
|
const key = /* @__PURE__ */ Symbol.for(`agent-pool:state:${id}:${dir}:${modelsFile ?? ""}:${models === void 0 ? "" : "inline"}`);
|
|
@@ -264,7 +271,7 @@ function init(s, onWarn) {
|
|
|
264
271
|
mkdirSync(s.dir, { recursive: true });
|
|
265
272
|
s.dirReady = true;
|
|
266
273
|
try {
|
|
267
|
-
unlinkSync(
|
|
274
|
+
unlinkSync(join2(s.dir, selfFile(s.id)));
|
|
268
275
|
} catch {
|
|
269
276
|
}
|
|
270
277
|
} catch {
|
|
@@ -281,8 +288,8 @@ function publish(s) {
|
|
|
281
288
|
const payload = {};
|
|
282
289
|
for (const [key, claim] of s.own) if (now - claim.t <= CLAIM_TTL_MS) payload[key] = claim;
|
|
283
290
|
try {
|
|
284
|
-
writeFileSync(
|
|
285
|
-
renameSync(
|
|
291
|
+
writeFileSync(join2(s.dir, `${selfFile(s.id)}.tmp`), JSON.stringify(payload));
|
|
292
|
+
renameSync(join2(s.dir, `${selfFile(s.id)}.tmp`), join2(s.dir, selfFile(s.id)));
|
|
286
293
|
} catch {
|
|
287
294
|
s.dirReady = false;
|
|
288
295
|
}
|
|
@@ -297,7 +304,7 @@ function readSiblings(s) {
|
|
|
297
304
|
}
|
|
298
305
|
const now = Date.now();
|
|
299
306
|
for (const entry of entries) {
|
|
300
|
-
const path4 =
|
|
307
|
+
const path4 = join2(s.dir, entry);
|
|
301
308
|
let age;
|
|
302
309
|
try {
|
|
303
310
|
age = now - statSync(path4).mtimeMs;
|
|
@@ -321,7 +328,7 @@ function readSiblings(s) {
|
|
|
321
328
|
continue;
|
|
322
329
|
}
|
|
323
330
|
try {
|
|
324
|
-
const parsed = JSON.parse(
|
|
331
|
+
const parsed = JSON.parse(readFileSync2(path4, "utf8"));
|
|
325
332
|
const claims = Object.values(parsed).filter(
|
|
326
333
|
(c) => c && typeof c.v === "string" && typeof c.m === "string" && now - c.t <= CLAIM_TTL_MS
|
|
327
334
|
);
|
|
@@ -342,9 +349,9 @@ function readLimits(s) {
|
|
|
342
349
|
const now = Date.now();
|
|
343
350
|
for (const entry of entries) {
|
|
344
351
|
if (!entry.startsWith("limit.") || !entry.endsWith(".json")) continue;
|
|
345
|
-
const path4 =
|
|
352
|
+
const path4 = join2(s.dir, entry);
|
|
346
353
|
try {
|
|
347
|
-
const strike = JSON.parse(
|
|
354
|
+
const strike = JSON.parse(readFileSync2(path4, "utf8"));
|
|
348
355
|
if (typeof strike.until !== "number" || typeof strike.model !== "string") continue;
|
|
349
356
|
if (strike.until > now) cooling.set(strike.model, strike);
|
|
350
357
|
else unlinkSync(path4);
|
|
@@ -366,7 +373,7 @@ function recordStrike(s, model, reason, stated) {
|
|
|
366
373
|
const backoff = Math.min(COOLDOWN_MAX_MS, COOLDOWN_BASE_MS * 2 ** (strikes - 1));
|
|
367
374
|
const strike = { model, until: now + (stated ?? backoff), strikes, reason, last: now };
|
|
368
375
|
s.cooling.set(model, strike);
|
|
369
|
-
const path4 =
|
|
376
|
+
const path4 = join2(s.dir, limitFile(model));
|
|
370
377
|
try {
|
|
371
378
|
writeFileSync(`${path4}.tmp`, JSON.stringify(strike));
|
|
372
379
|
renameSync(`${path4}.tmp`, path4);
|
|
@@ -382,7 +389,7 @@ function coolUnavailable(s, model) {
|
|
|
382
389
|
if (prior && prior.until > until) return prior;
|
|
383
390
|
const strike = { model, until, strikes: 1, reason: "endpoint-unavailable", last: now };
|
|
384
391
|
s.cooling.set(model, strike);
|
|
385
|
-
const path4 =
|
|
392
|
+
const path4 = join2(s.dir, limitFile(model));
|
|
386
393
|
try {
|
|
387
394
|
writeFileSync(`${path4}.tmp`, JSON.stringify(strike));
|
|
388
395
|
renameSync(`${path4}.tmp`, path4);
|
|
@@ -1494,7 +1501,7 @@ import { existsSync as existsSync2, readdirSync as readdirSync2, statSync as sta
|
|
|
1494
1501
|
import * as path2 from "node:path";
|
|
1495
1502
|
|
|
1496
1503
|
// src/lib/writer-rule.ts
|
|
1497
|
-
import { readFileSync as
|
|
1504
|
+
import { readFileSync as readFileSync3 } from "node:fs";
|
|
1498
1505
|
import * as path from "node:path";
|
|
1499
1506
|
var CONTEXT_DIR = path.join(".opencode", "context");
|
|
1500
1507
|
var WRITER = "context";
|
|
@@ -1567,7 +1574,7 @@ var evaluatedForm = (mapDir, projectRoot) => {
|
|
|
1567
1574
|
var verifyWriterRule = (configPath, mapDir, projectRoot) => {
|
|
1568
1575
|
let raw;
|
|
1569
1576
|
try {
|
|
1570
|
-
raw =
|
|
1577
|
+
raw = readFileSync3(configPath, "utf8");
|
|
1571
1578
|
} catch {
|
|
1572
1579
|
return void 0;
|
|
1573
1580
|
}
|
|
@@ -1655,11 +1662,11 @@ var changedSince = (before, dir) => {
|
|
|
1655
1662
|
for (const name of before.keys()) if (!after.has(name)) changed++;
|
|
1656
1663
|
return changed;
|
|
1657
1664
|
};
|
|
1658
|
-
var contextAutoUpdateHooks = (async ({ client, directory }) => {
|
|
1665
|
+
var contextAutoUpdateHooks = (async ({ client, directory }, options) => {
|
|
1659
1666
|
const pending = /* @__PURE__ */ new Set();
|
|
1660
1667
|
const owned = /* @__PURE__ */ new Map();
|
|
1661
1668
|
let lastRun = 0;
|
|
1662
|
-
const state2 = state();
|
|
1669
|
+
const state2 = state(options);
|
|
1663
1670
|
const log = (level, message, extra) => client.app.log({ body: { service: SERVICE2, level, message, extra } }).catch(() => {
|
|
1664
1671
|
});
|
|
1665
1672
|
const root = path2.resolve(directory);
|
|
@@ -1988,14 +1995,14 @@ var notifyHooks = (async ({ client, directory }, _options, deps = {}) => {
|
|
|
1988
1995
|
const info = await client.session.get({ path: { id: sessionID }, query: { directory } }).then((result) => result.data).catch(() => void 0);
|
|
1989
1996
|
return info;
|
|
1990
1997
|
};
|
|
1991
|
-
const deliverViaHelper = (title, message, urgency) => new Promise((
|
|
1998
|
+
const deliverViaHelper = (title, message, urgency) => new Promise((resolve3) => {
|
|
1992
1999
|
const helper = findHelper();
|
|
1993
2000
|
if (!helper) {
|
|
1994
2001
|
if (!warnedHelper) {
|
|
1995
2002
|
warnedHelper = true;
|
|
1996
2003
|
void log("warn", "no oc-notify helper found; run 'bin/oc-sync' to install one");
|
|
1997
2004
|
}
|
|
1998
|
-
|
|
2005
|
+
resolve3({ ok: false, detail: "no oc-notify helper found" });
|
|
1999
2006
|
return;
|
|
2000
2007
|
}
|
|
2001
2008
|
const child = spawn(helper, helperArgv(helper, title, message, urgency, "utilities-terminal"), {
|
|
@@ -2008,7 +2015,7 @@ var notifyHooks = (async ({ client, directory }, _options, deps = {}) => {
|
|
|
2008
2015
|
});
|
|
2009
2016
|
child.on("error", (error) => {
|
|
2010
2017
|
void log("warn", `notification helper failed: ${String(error)}`, { title });
|
|
2011
|
-
|
|
2018
|
+
resolve3({ ok: false, detail: String(error) });
|
|
2012
2019
|
});
|
|
2013
2020
|
child.on("exit", (code) => {
|
|
2014
2021
|
const noise = stderr.trim();
|
|
@@ -2019,7 +2026,7 @@ var notifyHooks = (async ({ client, directory }, _options, deps = {}) => {
|
|
|
2019
2026
|
message
|
|
2020
2027
|
});
|
|
2021
2028
|
}
|
|
2022
|
-
|
|
2029
|
+
resolve3({ ok, detail: noise || void 0 });
|
|
2023
2030
|
});
|
|
2024
2031
|
child.unref();
|
|
2025
2032
|
});
|
|
@@ -2144,6 +2151,238 @@ var notifyHooks = (async ({ client, directory }, _options, deps = {}) => {
|
|
|
2144
2151
|
};
|
|
2145
2152
|
});
|
|
2146
2153
|
|
|
2154
|
+
// src/lib/selfupdate.ts
|
|
2155
|
+
import { spawn as spawn2 } from "node:child_process";
|
|
2156
|
+
import { closeSync, mkdirSync as mkdirSync2, openSync, readFileSync as readFileSync4, renameSync as renameSync2, rmSync, statSync as statSync3, unlinkSync as unlinkSync2, writeFileSync as writeFileSync2, writeSync } from "node:fs";
|
|
2157
|
+
import { homedir as homedir2 } from "node:os";
|
|
2158
|
+
import { dirname as dirname3, join as join6, resolve as resolve2 } from "node:path";
|
|
2159
|
+
var REGISTRY_LATEST = `https://registry.npmjs.org/${PACKAGE_NAME}/latest`;
|
|
2160
|
+
var CHECK_INTERVAL_MS = 6 * 60 * 60 * 1e3;
|
|
2161
|
+
var FETCH_TIMEOUT_MS = 4e3;
|
|
2162
|
+
var INSTALL_TIMEOUT_MS = 12e4;
|
|
2163
|
+
var LOCK_STALE_MS = 10 * 60 * 1e3;
|
|
2164
|
+
var CHILD_ENV = "LACODE_SELFUPDATE_CHILD";
|
|
2165
|
+
var OPT_OUT_ENV = "LACODE_NO_SELFUPDATE";
|
|
2166
|
+
var STATE_DIR = join6(homedir2(), ".local", "share", "opencode", "lacode");
|
|
2167
|
+
var STATE_PATH = join6(STATE_DIR, "selfupdate.json");
|
|
2168
|
+
var LOCK_PATH = join6(STATE_DIR, "selfupdate.lock");
|
|
2169
|
+
function compareSemver(a, b) {
|
|
2170
|
+
const parse = (v) => v.split("-")[0].split("+")[0].split(".").map((n) => Number.parseInt(n, 10) || 0);
|
|
2171
|
+
const left = parse(a);
|
|
2172
|
+
const right = parse(b);
|
|
2173
|
+
for (let i = 0; i < 3; i++) {
|
|
2174
|
+
const l = left[i] ?? 0;
|
|
2175
|
+
const r = right[i] ?? 0;
|
|
2176
|
+
if (l !== r) return l > r ? 1 : -1;
|
|
2177
|
+
}
|
|
2178
|
+
return 0;
|
|
2179
|
+
}
|
|
2180
|
+
function readInstalledVersion(packageRoot) {
|
|
2181
|
+
try {
|
|
2182
|
+
const manifest = JSON.parse(readFileSync4(join6(packageRoot, "package.json"), "utf8"));
|
|
2183
|
+
if (manifest.name !== PACKAGE_NAME) return void 0;
|
|
2184
|
+
return typeof manifest.version === "string" ? manifest.version : void 0;
|
|
2185
|
+
} catch {
|
|
2186
|
+
return void 0;
|
|
2187
|
+
}
|
|
2188
|
+
}
|
|
2189
|
+
function resolveCacheRoot(packageRoot, exists = (p) => {
|
|
2190
|
+
try {
|
|
2191
|
+
return statSync3(p).isFile();
|
|
2192
|
+
} catch {
|
|
2193
|
+
return false;
|
|
2194
|
+
}
|
|
2195
|
+
}) {
|
|
2196
|
+
const tail = join6("node_modules", "@lawaty", "lacode");
|
|
2197
|
+
if (!packageRoot.endsWith(tail)) return void 0;
|
|
2198
|
+
const cacheRoot = resolve2(packageRoot, "..", "..", "..");
|
|
2199
|
+
if (!exists(join6(cacheRoot, "package.json"))) return void 0;
|
|
2200
|
+
return cacheRoot;
|
|
2201
|
+
}
|
|
2202
|
+
function resolveOpencodeBin(execPath = process.execPath, env = process.env, exists = (p) => {
|
|
2203
|
+
try {
|
|
2204
|
+
return statSync3(p).isFile();
|
|
2205
|
+
} catch {
|
|
2206
|
+
return false;
|
|
2207
|
+
}
|
|
2208
|
+
}) {
|
|
2209
|
+
if (execPath && /(^|[\\/])opencode(\.exe)?$/.test(execPath)) return execPath;
|
|
2210
|
+
for (const dir of (env.PATH ?? "").split(":")) {
|
|
2211
|
+
if (!dir) continue;
|
|
2212
|
+
const candidate = join6(dir, "opencode");
|
|
2213
|
+
if (exists(candidate)) return candidate;
|
|
2214
|
+
}
|
|
2215
|
+
const fallback = join6(homedir2(), ".opencode", "bin", "opencode");
|
|
2216
|
+
return exists(fallback) ? fallback : void 0;
|
|
2217
|
+
}
|
|
2218
|
+
async function fetchLatest(fetchImpl = fetch, timeoutMs = FETCH_TIMEOUT_MS) {
|
|
2219
|
+
const controller = new AbortController();
|
|
2220
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
2221
|
+
try {
|
|
2222
|
+
const response = await fetchImpl(REGISTRY_LATEST, { signal: controller.signal });
|
|
2223
|
+
if (!response.ok) return void 0;
|
|
2224
|
+
const body = await response.json();
|
|
2225
|
+
return typeof body.version === "string" ? body.version : void 0;
|
|
2226
|
+
} catch {
|
|
2227
|
+
return void 0;
|
|
2228
|
+
} finally {
|
|
2229
|
+
clearTimeout(timer);
|
|
2230
|
+
}
|
|
2231
|
+
}
|
|
2232
|
+
function readState(path4 = STATE_PATH) {
|
|
2233
|
+
try {
|
|
2234
|
+
return JSON.parse(readFileSync4(path4, "utf8"));
|
|
2235
|
+
} catch {
|
|
2236
|
+
return {};
|
|
2237
|
+
}
|
|
2238
|
+
}
|
|
2239
|
+
function writeState(state2, path4 = STATE_PATH) {
|
|
2240
|
+
try {
|
|
2241
|
+
mkdirSync2(dirname3(path4), { recursive: true });
|
|
2242
|
+
writeFileSync2(path4, JSON.stringify(state2));
|
|
2243
|
+
} catch {
|
|
2244
|
+
}
|
|
2245
|
+
}
|
|
2246
|
+
var isDue = (state2, now, interval = CHECK_INTERVAL_MS) => typeof state2.checkedAt !== "number" || now - state2.checkedAt >= interval;
|
|
2247
|
+
function acquireLock(path4 = LOCK_PATH, now = Date.now()) {
|
|
2248
|
+
try {
|
|
2249
|
+
mkdirSync2(dirname3(path4), { recursive: true });
|
|
2250
|
+
const fd = openSync(path4, "wx");
|
|
2251
|
+
writeSync(fd, String(process.pid));
|
|
2252
|
+
closeSync(fd);
|
|
2253
|
+
return () => {
|
|
2254
|
+
try {
|
|
2255
|
+
unlinkSync2(path4);
|
|
2256
|
+
} catch {
|
|
2257
|
+
}
|
|
2258
|
+
};
|
|
2259
|
+
} catch {
|
|
2260
|
+
try {
|
|
2261
|
+
if (now - statSync3(path4).mtimeMs > LOCK_STALE_MS) unlinkSync2(path4);
|
|
2262
|
+
} catch {
|
|
2263
|
+
}
|
|
2264
|
+
return void 0;
|
|
2265
|
+
}
|
|
2266
|
+
}
|
|
2267
|
+
async function checkForUpdate(opts) {
|
|
2268
|
+
const installed = readInstalledVersion(opts.packageRoot);
|
|
2269
|
+
const latest = await fetchLatest(opts.fetchImpl);
|
|
2270
|
+
return {
|
|
2271
|
+
installed,
|
|
2272
|
+
latest,
|
|
2273
|
+
newer: Boolean(installed && latest && compareSemver(latest, installed) > 0),
|
|
2274
|
+
cacheRoot: resolveCacheRoot(opts.packageRoot, opts.exists),
|
|
2275
|
+
bin: resolveOpencodeBin(opts.execPath, opts.env, opts.exists)
|
|
2276
|
+
};
|
|
2277
|
+
}
|
|
2278
|
+
async function refreshCache(opts) {
|
|
2279
|
+
const { cacheRoot, bin, latest } = opts;
|
|
2280
|
+
const spawnImpl = opts.spawnImpl ?? spawn2;
|
|
2281
|
+
const timeoutMs = opts.timeoutMs ?? INSTALL_TIMEOUT_MS;
|
|
2282
|
+
const fresh = join6(cacheRoot, "node_modules", "@lawaty", "lacode");
|
|
2283
|
+
const aside = `${cacheRoot}.stale-${Date.now()}`;
|
|
2284
|
+
try {
|
|
2285
|
+
rmSync(aside, { recursive: true, force: true });
|
|
2286
|
+
renameSync2(cacheRoot, aside);
|
|
2287
|
+
} catch (error) {
|
|
2288
|
+
return { ok: false, detail: `could not set the old cache aside: ${String(error)}` };
|
|
2289
|
+
}
|
|
2290
|
+
const exited = await new Promise((settle) => {
|
|
2291
|
+
let done = false;
|
|
2292
|
+
const finish = (value) => {
|
|
2293
|
+
if (done) return;
|
|
2294
|
+
done = true;
|
|
2295
|
+
clearTimeout(timer);
|
|
2296
|
+
settle(value);
|
|
2297
|
+
};
|
|
2298
|
+
const child = spawnImpl(bin, ["plugin", PACKAGE_NAME, "-g"], {
|
|
2299
|
+
env: { ...process.env, [CHILD_ENV]: "1" },
|
|
2300
|
+
stdio: "ignore"
|
|
2301
|
+
});
|
|
2302
|
+
const timer = setTimeout(() => {
|
|
2303
|
+
try {
|
|
2304
|
+
child.kill("SIGKILL");
|
|
2305
|
+
} catch {
|
|
2306
|
+
}
|
|
2307
|
+
finish(false);
|
|
2308
|
+
}, timeoutMs);
|
|
2309
|
+
child.on("error", () => finish(false));
|
|
2310
|
+
child.on("exit", (code) => finish(code === 0));
|
|
2311
|
+
});
|
|
2312
|
+
const installed = readInstalledVersion(fresh);
|
|
2313
|
+
const ok = exited && installed === latest;
|
|
2314
|
+
if (ok) {
|
|
2315
|
+
try {
|
|
2316
|
+
rmSync(aside, { recursive: true, force: true });
|
|
2317
|
+
} catch {
|
|
2318
|
+
}
|
|
2319
|
+
} else {
|
|
2320
|
+
try {
|
|
2321
|
+
renameSync2(aside, cacheRoot);
|
|
2322
|
+
} catch {
|
|
2323
|
+
}
|
|
2324
|
+
}
|
|
2325
|
+
return {
|
|
2326
|
+
ok,
|
|
2327
|
+
detail: ok ? `installed ${installed}` : `installer exited ${exited ? "0" : "non-zero"}, cache now ${installed ?? "missing"}`
|
|
2328
|
+
};
|
|
2329
|
+
}
|
|
2330
|
+
async function maybeSelfUpdate(opts) {
|
|
2331
|
+
const env = opts.env ?? process.env;
|
|
2332
|
+
if (opts.enabled === false || env[CHILD_ENV] === "1" || env[OPT_OUT_ENV] === "1") {
|
|
2333
|
+
return { action: "disabled" };
|
|
2334
|
+
}
|
|
2335
|
+
const now = (opts.now ?? Date.now)();
|
|
2336
|
+
const statePath = opts.statePath ?? STATE_PATH;
|
|
2337
|
+
const state2 = readState(statePath);
|
|
2338
|
+
if (!isDue(state2, now, opts.intervalMs ?? CHECK_INTERVAL_MS)) return { action: "skipped", detail: "checked recently" };
|
|
2339
|
+
const release2 = acquireLock(opts.lockPath, now);
|
|
2340
|
+
if (!release2) return { action: "skipped", detail: "another process is updating" };
|
|
2341
|
+
try {
|
|
2342
|
+
const check = await checkForUpdate(opts);
|
|
2343
|
+
if (!check.installed) return { action: "skipped", detail: "no installed version" };
|
|
2344
|
+
if (!check.latest) {
|
|
2345
|
+
writeState({ ...state2, checkedAt: now }, statePath);
|
|
2346
|
+
return { action: "skipped", detail: "registry unreachable" };
|
|
2347
|
+
}
|
|
2348
|
+
if (!check.newer) {
|
|
2349
|
+
writeState({ ...state2, checkedAt: now, latest: check.latest }, statePath);
|
|
2350
|
+
return { action: "current", detail: `${check.installed} is current` };
|
|
2351
|
+
}
|
|
2352
|
+
if (!check.cacheRoot) {
|
|
2353
|
+
writeState({ ...state2, checkedAt: now, latest: check.latest }, statePath);
|
|
2354
|
+
return { action: "skipped", detail: "not an opencode cache install" };
|
|
2355
|
+
}
|
|
2356
|
+
if (!check.bin) {
|
|
2357
|
+
writeState({ ...state2, checkedAt: now, latest: check.latest }, statePath);
|
|
2358
|
+
opts.log?.("warn", `LaCode ${check.latest} is available but no opencode binary was found to install it`);
|
|
2359
|
+
return { action: "failed", detail: "no opencode binary" };
|
|
2360
|
+
}
|
|
2361
|
+
const result = await refreshCache({ cacheRoot: check.cacheRoot, bin: check.bin, latest: check.latest, spawnImpl: opts.spawnImpl });
|
|
2362
|
+
if (result.ok) {
|
|
2363
|
+
writeState({ ...state2, checkedAt: now, latest: check.latest, updatedTo: check.latest }, statePath);
|
|
2364
|
+
opts.log?.("info", `LaCode updated ${check.installed} -> ${check.latest}; restart opencode to apply`);
|
|
2365
|
+
return { action: "updated", detail: `${check.installed} -> ${check.latest}` };
|
|
2366
|
+
}
|
|
2367
|
+
writeState({ ...state2, checkedAt: now, latest: check.latest }, statePath);
|
|
2368
|
+
opts.log?.("warn", `LaCode update to ${check.latest} failed: ${result.detail}`);
|
|
2369
|
+
return { action: "failed", detail: result.detail };
|
|
2370
|
+
} finally {
|
|
2371
|
+
release2();
|
|
2372
|
+
}
|
|
2373
|
+
}
|
|
2374
|
+
|
|
2375
|
+
// src/plugins/selfupdate.ts
|
|
2376
|
+
async function selfUpdateHooks(input, options) {
|
|
2377
|
+
const opts = options ?? {};
|
|
2378
|
+
if (opts.enabled === false || opts.selfUpdate === false) return {};
|
|
2379
|
+
const log = (level, message, extra) => input.client.app.log({ body: { service: "lacode", level, message, extra } }).catch(() => {
|
|
2380
|
+
});
|
|
2381
|
+
void maybeSelfUpdate({ packageRoot: PACKAGE_ROOT, enabled: opts.enabled, log }).catch(() => {
|
|
2382
|
+
});
|
|
2383
|
+
return {};
|
|
2384
|
+
}
|
|
2385
|
+
|
|
2147
2386
|
// src/index.ts
|
|
2148
2387
|
var inertHooks = {
|
|
2149
2388
|
config: async () => {
|
|
@@ -2170,6 +2409,7 @@ var LaCode = (async (input, options) => {
|
|
|
2170
2409
|
const pool = await settled("agent-pool", () => agentPoolHooks(input, options)) ?? inertHooks;
|
|
2171
2410
|
const autoupdate = await settled("context-autoupdate", () => contextAutoUpdateHooks(input, options)) ?? inertHooks;
|
|
2172
2411
|
const notifier = await settled("notify", () => notifyHooks(input, options)) ?? inertHooks;
|
|
2412
|
+
const selfupdate = await settled("self-update", () => selfUpdateHooks(input, options)) ?? inertHooks;
|
|
2173
2413
|
const config = async (cfg) => {
|
|
2174
2414
|
try {
|
|
2175
2415
|
const injected = applyAgents(cfg, agentSet);
|
|
@@ -2210,6 +2450,7 @@ var LaCode = (async (input, options) => {
|
|
|
2210
2450
|
await pool.dispose?.();
|
|
2211
2451
|
await autoupdate.dispose?.();
|
|
2212
2452
|
await notifier.dispose?.();
|
|
2453
|
+
await selfupdate.dispose?.();
|
|
2213
2454
|
}
|
|
2214
2455
|
};
|
|
2215
2456
|
});
|
package/dist/lacode.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// bin/lacode
|
|
4
|
-
import { fileURLToPath } from "node:url";
|
|
5
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
4
|
+
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
5
|
+
import { existsSync, readFileSync as readFileSync3 } from "node:fs";
|
|
6
6
|
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
7
7
|
|
|
8
8
|
// src/tunnel/index.ts
|
|
@@ -189,6 +189,167 @@ function describe(env = process.env, platform = process.platform) {
|
|
|
189
189
|
};
|
|
190
190
|
}
|
|
191
191
|
|
|
192
|
+
// src/lib/package-root.ts
|
|
193
|
+
import { readFileSync } from "node:fs";
|
|
194
|
+
import { dirname, join as join2 } from "node:path";
|
|
195
|
+
import { fileURLToPath } from "node:url";
|
|
196
|
+
var PACKAGE_NAME = "@lawaty/lacode";
|
|
197
|
+
var findPackageRoot = (start) => {
|
|
198
|
+
let dir = start;
|
|
199
|
+
for (let up = 0; up < 6; up++) {
|
|
200
|
+
try {
|
|
201
|
+
if (JSON.parse(readFileSync(join2(dir, "package.json"), "utf8")).name === PACKAGE_NAME) return dir;
|
|
202
|
+
} catch {
|
|
203
|
+
}
|
|
204
|
+
const parent = dirname(dir);
|
|
205
|
+
if (parent === dir) break;
|
|
206
|
+
dir = parent;
|
|
207
|
+
}
|
|
208
|
+
return join2(start, "..", "..");
|
|
209
|
+
};
|
|
210
|
+
var PACKAGE_ROOT = findPackageRoot(dirname(fileURLToPath(import.meta.url)));
|
|
211
|
+
|
|
212
|
+
// src/lib/selfupdate.ts
|
|
213
|
+
import { spawn } from "node:child_process";
|
|
214
|
+
import { closeSync, mkdirSync, openSync, readFileSync as readFileSync2, renameSync, rmSync, statSync, unlinkSync, writeFileSync, writeSync } from "node:fs";
|
|
215
|
+
import { homedir as homedir2 } from "node:os";
|
|
216
|
+
import { dirname as dirname2, join as join3, resolve } from "node:path";
|
|
217
|
+
var REGISTRY_LATEST = `https://registry.npmjs.org/${PACKAGE_NAME}/latest`;
|
|
218
|
+
var CHECK_INTERVAL_MS = 6 * 60 * 60 * 1e3;
|
|
219
|
+
var FETCH_TIMEOUT_MS = 4e3;
|
|
220
|
+
var INSTALL_TIMEOUT_MS = 12e4;
|
|
221
|
+
var LOCK_STALE_MS = 10 * 60 * 1e3;
|
|
222
|
+
var CHILD_ENV = "LACODE_SELFUPDATE_CHILD";
|
|
223
|
+
var STATE_DIR = join3(homedir2(), ".local", "share", "opencode", "lacode");
|
|
224
|
+
var STATE_PATH = join3(STATE_DIR, "selfupdate.json");
|
|
225
|
+
var LOCK_PATH = join3(STATE_DIR, "selfupdate.lock");
|
|
226
|
+
function compareSemver(a, b) {
|
|
227
|
+
const parse = (v) => v.split("-")[0].split("+")[0].split(".").map((n) => Number.parseInt(n, 10) || 0);
|
|
228
|
+
const left = parse(a);
|
|
229
|
+
const right = parse(b);
|
|
230
|
+
for (let i = 0; i < 3; i++) {
|
|
231
|
+
const l = left[i] ?? 0;
|
|
232
|
+
const r = right[i] ?? 0;
|
|
233
|
+
if (l !== r) return l > r ? 1 : -1;
|
|
234
|
+
}
|
|
235
|
+
return 0;
|
|
236
|
+
}
|
|
237
|
+
function readInstalledVersion(packageRoot) {
|
|
238
|
+
try {
|
|
239
|
+
const manifest = JSON.parse(readFileSync2(join3(packageRoot, "package.json"), "utf8"));
|
|
240
|
+
if (manifest.name !== PACKAGE_NAME) return void 0;
|
|
241
|
+
return typeof manifest.version === "string" ? manifest.version : void 0;
|
|
242
|
+
} catch {
|
|
243
|
+
return void 0;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
function resolveCacheRoot(packageRoot, exists = (p) => {
|
|
247
|
+
try {
|
|
248
|
+
return statSync(p).isFile();
|
|
249
|
+
} catch {
|
|
250
|
+
return false;
|
|
251
|
+
}
|
|
252
|
+
}) {
|
|
253
|
+
const tail = join3("node_modules", "@lawaty", "lacode");
|
|
254
|
+
if (!packageRoot.endsWith(tail)) return void 0;
|
|
255
|
+
const cacheRoot = resolve(packageRoot, "..", "..", "..");
|
|
256
|
+
if (!exists(join3(cacheRoot, "package.json"))) return void 0;
|
|
257
|
+
return cacheRoot;
|
|
258
|
+
}
|
|
259
|
+
function resolveOpencodeBin(execPath = process.execPath, env = process.env, exists = (p) => {
|
|
260
|
+
try {
|
|
261
|
+
return statSync(p).isFile();
|
|
262
|
+
} catch {
|
|
263
|
+
return false;
|
|
264
|
+
}
|
|
265
|
+
}) {
|
|
266
|
+
if (execPath && /(^|[\\/])opencode(\.exe)?$/.test(execPath)) return execPath;
|
|
267
|
+
for (const dir of (env.PATH ?? "").split(":")) {
|
|
268
|
+
if (!dir) continue;
|
|
269
|
+
const candidate = join3(dir, "opencode");
|
|
270
|
+
if (exists(candidate)) return candidate;
|
|
271
|
+
}
|
|
272
|
+
const fallback = join3(homedir2(), ".opencode", "bin", "opencode");
|
|
273
|
+
return exists(fallback) ? fallback : void 0;
|
|
274
|
+
}
|
|
275
|
+
async function fetchLatest(fetchImpl = fetch, timeoutMs = FETCH_TIMEOUT_MS) {
|
|
276
|
+
const controller = new AbortController();
|
|
277
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
278
|
+
try {
|
|
279
|
+
const response = await fetchImpl(REGISTRY_LATEST, { signal: controller.signal });
|
|
280
|
+
if (!response.ok) return void 0;
|
|
281
|
+
const body = await response.json();
|
|
282
|
+
return typeof body.version === "string" ? body.version : void 0;
|
|
283
|
+
} catch {
|
|
284
|
+
return void 0;
|
|
285
|
+
} finally {
|
|
286
|
+
clearTimeout(timer);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
async function checkForUpdate(opts) {
|
|
290
|
+
const installed = readInstalledVersion(opts.packageRoot);
|
|
291
|
+
const latest = await fetchLatest(opts.fetchImpl);
|
|
292
|
+
return {
|
|
293
|
+
installed,
|
|
294
|
+
latest,
|
|
295
|
+
newer: Boolean(installed && latest && compareSemver(latest, installed) > 0),
|
|
296
|
+
cacheRoot: resolveCacheRoot(opts.packageRoot, opts.exists),
|
|
297
|
+
bin: resolveOpencodeBin(opts.execPath, opts.env, opts.exists)
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
async function refreshCache(opts) {
|
|
301
|
+
const { cacheRoot, bin, latest } = opts;
|
|
302
|
+
const spawnImpl = opts.spawnImpl ?? spawn;
|
|
303
|
+
const timeoutMs = opts.timeoutMs ?? INSTALL_TIMEOUT_MS;
|
|
304
|
+
const fresh = join3(cacheRoot, "node_modules", "@lawaty", "lacode");
|
|
305
|
+
const aside = `${cacheRoot}.stale-${Date.now()}`;
|
|
306
|
+
try {
|
|
307
|
+
rmSync(aside, { recursive: true, force: true });
|
|
308
|
+
renameSync(cacheRoot, aside);
|
|
309
|
+
} catch (error) {
|
|
310
|
+
return { ok: false, detail: `could not set the old cache aside: ${String(error)}` };
|
|
311
|
+
}
|
|
312
|
+
const exited = await new Promise((settle) => {
|
|
313
|
+
let done = false;
|
|
314
|
+
const finish = (value) => {
|
|
315
|
+
if (done) return;
|
|
316
|
+
done = true;
|
|
317
|
+
clearTimeout(timer);
|
|
318
|
+
settle(value);
|
|
319
|
+
};
|
|
320
|
+
const child = spawnImpl(bin, ["plugin", PACKAGE_NAME, "-g"], {
|
|
321
|
+
env: { ...process.env, [CHILD_ENV]: "1" },
|
|
322
|
+
stdio: "ignore"
|
|
323
|
+
});
|
|
324
|
+
const timer = setTimeout(() => {
|
|
325
|
+
try {
|
|
326
|
+
child.kill("SIGKILL");
|
|
327
|
+
} catch {
|
|
328
|
+
}
|
|
329
|
+
finish(false);
|
|
330
|
+
}, timeoutMs);
|
|
331
|
+
child.on("error", () => finish(false));
|
|
332
|
+
child.on("exit", (code) => finish(code === 0));
|
|
333
|
+
});
|
|
334
|
+
const installed = readInstalledVersion(fresh);
|
|
335
|
+
const ok = exited && installed === latest;
|
|
336
|
+
if (ok) {
|
|
337
|
+
try {
|
|
338
|
+
rmSync(aside, { recursive: true, force: true });
|
|
339
|
+
} catch {
|
|
340
|
+
}
|
|
341
|
+
} else {
|
|
342
|
+
try {
|
|
343
|
+
renameSync(aside, cacheRoot);
|
|
344
|
+
} catch {
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
return {
|
|
348
|
+
ok,
|
|
349
|
+
detail: ok ? `installed ${installed}` : `installer exited ${exited ? "0" : "non-zero"}, cache now ${installed ?? "missing"}`
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
|
|
192
353
|
// bin/lacode
|
|
193
354
|
if (!process.features?.typescript) {
|
|
194
355
|
if (process.env.LACODE_STRIP_TYPECHECKED === "1") {
|
|
@@ -198,7 +359,7 @@ if (!process.features?.typescript) {
|
|
|
198
359
|
const { spawnSync: spawnSync3 } = await import("node:child_process");
|
|
199
360
|
const result = spawnSync3(
|
|
200
361
|
process.execPath,
|
|
201
|
-
["--experimental-strip-types", "--no-warnings",
|
|
362
|
+
["--experimental-strip-types", "--no-warnings", fileURLToPath2(import.meta.url), ...process.argv.slice(2)],
|
|
202
363
|
{ stdio: "inherit", env: { ...process.env, LACODE_STRIP_TYPECHECKED: "1" } }
|
|
203
364
|
);
|
|
204
365
|
process.exit(result.status ?? 1);
|
|
@@ -216,6 +377,13 @@ var USAGE = `lacode \u2014 portable notifications and remote exec for LaCode
|
|
|
216
377
|
lacode doctor
|
|
217
378
|
Print what the environment resolves to. Diagnostics only.
|
|
218
379
|
|
|
380
|
+
lacode update [--check]
|
|
381
|
+
Bring this machine's installed LaCode up to the latest published
|
|
382
|
+
version. opencode's plugin cache never re-resolves on its own, so
|
|
383
|
+
this is the manual half of the automatic check the plugin runs at
|
|
384
|
+
startup. --check only reports; it installs nothing. The new version
|
|
385
|
+
takes effect on the next opencode start.
|
|
386
|
+
|
|
219
387
|
Environment:
|
|
220
388
|
OC_LOCAL_USER desktop user the tunnel authenticates as
|
|
221
389
|
OC_TUNNEL_PORT reverse-forward listener port (default 10022)
|
|
@@ -269,6 +437,40 @@ switch (command) {
|
|
|
269
437
|
process.exit(0);
|
|
270
438
|
break;
|
|
271
439
|
}
|
|
440
|
+
case "update": {
|
|
441
|
+
const checkOnly = argv.includes("--check");
|
|
442
|
+
const check = await checkForUpdate({ packageRoot: PACKAGE_ROOT });
|
|
443
|
+
if (!check.installed) {
|
|
444
|
+
console.error("lacode update: could not read the installed version (run from an installed package).");
|
|
445
|
+
process.exit(1);
|
|
446
|
+
}
|
|
447
|
+
if (!check.latest) {
|
|
448
|
+
console.error("lacode update: could not reach the npm registry; check your connection and retry.");
|
|
449
|
+
process.exit(1);
|
|
450
|
+
}
|
|
451
|
+
if (!check.newer) {
|
|
452
|
+
console.log(`lacode ${check.installed} is up to date (latest ${check.latest}).`);
|
|
453
|
+
process.exit(0);
|
|
454
|
+
}
|
|
455
|
+
console.log(`lacode ${check.installed} -> ${check.latest} available.`);
|
|
456
|
+
if (checkOnly) process.exit(0);
|
|
457
|
+
if (!check.cacheRoot) {
|
|
458
|
+
console.error("This is not an opencode cache install; update it with your package manager instead.");
|
|
459
|
+
process.exit(1);
|
|
460
|
+
}
|
|
461
|
+
if (!check.bin) {
|
|
462
|
+
console.error("lacode update: no opencode binary found to perform the install.");
|
|
463
|
+
process.exit(1);
|
|
464
|
+
}
|
|
465
|
+
const result = await refreshCache({ cacheRoot: check.cacheRoot, bin: check.bin, latest: check.latest });
|
|
466
|
+
if (!result.ok) {
|
|
467
|
+
console.error(`lacode update: install failed (${result.detail}).`);
|
|
468
|
+
process.exit(1);
|
|
469
|
+
}
|
|
470
|
+
console.log(`Updated to ${check.latest}. Restart opencode to apply.`);
|
|
471
|
+
process.exit(0);
|
|
472
|
+
break;
|
|
473
|
+
}
|
|
272
474
|
case "receive": {
|
|
273
475
|
const request = parsePayload(process.env.SSH_ORIGINAL_COMMAND ?? "");
|
|
274
476
|
const result = sendLocal(request);
|
|
@@ -292,7 +494,7 @@ switch (command) {
|
|
|
292
494
|
}
|
|
293
495
|
console.log(`Generated ${key}`);
|
|
294
496
|
}
|
|
295
|
-
const pubkey = existsSync(pub) ?
|
|
497
|
+
const pubkey = existsSync(pub) ? readFileSync3(pub, "utf8").trim() : "(public key missing)";
|
|
296
498
|
const self = process.argv[1] ?? "lacode";
|
|
297
499
|
console.log("\nOn the REMOTE host (where opencode runs), export:");
|
|
298
500
|
console.log(` OC_LOCAL_USER=${user || "<desktop-user>"}`);
|
package/dist/pool.js
CHANGED
|
@@ -1,8 +1,29 @@
|
|
|
1
1
|
// src/lib/pool.ts
|
|
2
|
-
import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync as readFileSync2, renameSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
|
+
import { join as join2 } from "node:path";
|
|
5
|
+
|
|
6
|
+
// src/lib/package-root.ts
|
|
7
|
+
import { readFileSync } from "node:fs";
|
|
4
8
|
import { dirname, join } from "node:path";
|
|
5
9
|
import { fileURLToPath } from "node:url";
|
|
10
|
+
var PACKAGE_NAME = "@lawaty/lacode";
|
|
11
|
+
var findPackageRoot = (start) => {
|
|
12
|
+
let dir = start;
|
|
13
|
+
for (let up = 0; up < 6; up++) {
|
|
14
|
+
try {
|
|
15
|
+
if (JSON.parse(readFileSync(join(dir, "package.json"), "utf8")).name === PACKAGE_NAME) return dir;
|
|
16
|
+
} catch {
|
|
17
|
+
}
|
|
18
|
+
const parent = dirname(dir);
|
|
19
|
+
if (parent === dir) break;
|
|
20
|
+
dir = parent;
|
|
21
|
+
}
|
|
22
|
+
return join(start, "..", "..");
|
|
23
|
+
};
|
|
24
|
+
var PACKAGE_ROOT = findPackageRoot(dirname(fileURLToPath(import.meta.url)));
|
|
25
|
+
|
|
26
|
+
// src/lib/pool.ts
|
|
6
27
|
var CLAIM_TTL_MS = 10 * 60 * 1e3;
|
|
7
28
|
var DEAD_FILE_MS = 2 * CLAIM_TTL_MS;
|
|
8
29
|
var ORPHAN_TMP_MS = 60 * 1e3;
|
|
@@ -17,25 +38,11 @@ var COOLDOWN_BASE_MS = 60 * 1e3;
|
|
|
17
38
|
var COOLDOWN_MAX_MS = 15 * 60 * 1e3;
|
|
18
39
|
var STRIKE_DECAY_MS = 30 * 60 * 1e3;
|
|
19
40
|
var BASES = ["explore", "implement", "context"];
|
|
20
|
-
var
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
} catch {
|
|
26
|
-
}
|
|
27
|
-
const parent = dirname(dir);
|
|
28
|
-
if (parent === dir) break;
|
|
29
|
-
dir = parent;
|
|
30
|
-
}
|
|
31
|
-
return join(start, "..", "..");
|
|
32
|
-
};
|
|
33
|
-
var PACKAGE_ROOT = findPackageRoot(dirname(fileURLToPath(import.meta.url)));
|
|
34
|
-
var PRESETS_DIR = join(PACKAGE_ROOT, "presets");
|
|
35
|
-
var DEFAULT_PRESET = join(PRESETS_DIR, "free-tier.json");
|
|
36
|
-
var USER_CONFIG_DIR = join(homedir(), ".config", "lacode");
|
|
37
|
-
var USER_MODELS_PATH = join(USER_CONFIG_DIR, "pool.json");
|
|
38
|
-
var USER_MODELS_TXT = join(USER_CONFIG_DIR, "pool.txt");
|
|
41
|
+
var PRESETS_DIR = join2(PACKAGE_ROOT, "presets");
|
|
42
|
+
var DEFAULT_PRESET = join2(PRESETS_DIR, "free-tier.json");
|
|
43
|
+
var USER_CONFIG_DIR = join2(homedir(), ".config", "lacode");
|
|
44
|
+
var USER_MODELS_PATH = join2(USER_CONFIG_DIR, "pool.json");
|
|
45
|
+
var USER_MODELS_TXT = join2(USER_CONFIG_DIR, "pool.txt");
|
|
39
46
|
var FALLBACK_SLOTS = [
|
|
40
47
|
{ index: 1, model: "opencode/space-bunny-free", weight: 3 },
|
|
41
48
|
{ index: 2, model: "opencode-go/step-5-preview-free", weight: 2 },
|
|
@@ -89,7 +96,7 @@ function parseSlotText(text, origin) {
|
|
|
89
96
|
}
|
|
90
97
|
var readJSON = (path) => {
|
|
91
98
|
try {
|
|
92
|
-
return { ok: true, value: JSON.parse(
|
|
99
|
+
return { ok: true, value: JSON.parse(readFileSync2(path, "utf8")) };
|
|
93
100
|
} catch (error) {
|
|
94
101
|
return { ok: false, error: String(error) };
|
|
95
102
|
}
|
|
@@ -105,7 +112,7 @@ function readOptional(path, origin, problems) {
|
|
|
105
112
|
function readTextOptional(path, origin, problems) {
|
|
106
113
|
if (!existsSync(path)) return void 0;
|
|
107
114
|
try {
|
|
108
|
-
return
|
|
115
|
+
return readFileSync2(path, "utf8");
|
|
109
116
|
} catch (error) {
|
|
110
117
|
problems.push(`${origin} unreadable (${String(error)}); trying the next source`);
|
|
111
118
|
return void 0;
|
|
@@ -147,7 +154,7 @@ function resolveModels(input = {}) {
|
|
|
147
154
|
function readSlotFile(path) {
|
|
148
155
|
let text;
|
|
149
156
|
try {
|
|
150
|
-
text =
|
|
157
|
+
text = readFileSync2(path, "utf8");
|
|
151
158
|
} catch (error) {
|
|
152
159
|
return { slots: FALLBACK_SLOTS, problems: [`${path} unreadable (${String(error)}); using built-in defaults`] };
|
|
153
160
|
}
|
|
@@ -215,7 +222,7 @@ var retryAfterMs = (headers) => {
|
|
|
215
222
|
};
|
|
216
223
|
function state(opts = {}) {
|
|
217
224
|
const id = opts.id ?? String(process.pid);
|
|
218
|
-
const dir = opts.dir ??
|
|
225
|
+
const dir = opts.dir ?? join2(homedir(), ".local", "share", "opencode", "agent-pool");
|
|
219
226
|
const modelsFile = opts.modelsFile;
|
|
220
227
|
const models = opts.models;
|
|
221
228
|
const key = /* @__PURE__ */ Symbol.for(`agent-pool:state:${id}:${dir}:${modelsFile ?? ""}:${models === void 0 ? "" : "inline"}`);
|
|
@@ -264,7 +271,7 @@ function init(s, onWarn) {
|
|
|
264
271
|
mkdirSync(s.dir, { recursive: true });
|
|
265
272
|
s.dirReady = true;
|
|
266
273
|
try {
|
|
267
|
-
unlinkSync(
|
|
274
|
+
unlinkSync(join2(s.dir, selfFile(s.id)));
|
|
268
275
|
} catch {
|
|
269
276
|
}
|
|
270
277
|
} catch {
|
|
@@ -281,8 +288,8 @@ function publish(s) {
|
|
|
281
288
|
const payload = {};
|
|
282
289
|
for (const [key, claim] of s.own) if (now - claim.t <= CLAIM_TTL_MS) payload[key] = claim;
|
|
283
290
|
try {
|
|
284
|
-
writeFileSync(
|
|
285
|
-
renameSync(
|
|
291
|
+
writeFileSync(join2(s.dir, `${selfFile(s.id)}.tmp`), JSON.stringify(payload));
|
|
292
|
+
renameSync(join2(s.dir, `${selfFile(s.id)}.tmp`), join2(s.dir, selfFile(s.id)));
|
|
286
293
|
} catch {
|
|
287
294
|
s.dirReady = false;
|
|
288
295
|
}
|
|
@@ -297,7 +304,7 @@ function readSiblings(s) {
|
|
|
297
304
|
}
|
|
298
305
|
const now = Date.now();
|
|
299
306
|
for (const entry of entries) {
|
|
300
|
-
const path =
|
|
307
|
+
const path = join2(s.dir, entry);
|
|
301
308
|
let age;
|
|
302
309
|
try {
|
|
303
310
|
age = now - statSync(path).mtimeMs;
|
|
@@ -321,7 +328,7 @@ function readSiblings(s) {
|
|
|
321
328
|
continue;
|
|
322
329
|
}
|
|
323
330
|
try {
|
|
324
|
-
const parsed = JSON.parse(
|
|
331
|
+
const parsed = JSON.parse(readFileSync2(path, "utf8"));
|
|
325
332
|
const claims = Object.values(parsed).filter(
|
|
326
333
|
(c) => c && typeof c.v === "string" && typeof c.m === "string" && now - c.t <= CLAIM_TTL_MS
|
|
327
334
|
);
|
|
@@ -342,9 +349,9 @@ function readLimits(s) {
|
|
|
342
349
|
const now = Date.now();
|
|
343
350
|
for (const entry of entries) {
|
|
344
351
|
if (!entry.startsWith("limit.") || !entry.endsWith(".json")) continue;
|
|
345
|
-
const path =
|
|
352
|
+
const path = join2(s.dir, entry);
|
|
346
353
|
try {
|
|
347
|
-
const strike = JSON.parse(
|
|
354
|
+
const strike = JSON.parse(readFileSync2(path, "utf8"));
|
|
348
355
|
if (typeof strike.until !== "number" || typeof strike.model !== "string") continue;
|
|
349
356
|
if (strike.until > now) cooling.set(strike.model, strike);
|
|
350
357
|
else unlinkSync(path);
|
|
@@ -366,7 +373,7 @@ function recordStrike(s, model, reason, stated) {
|
|
|
366
373
|
const backoff = Math.min(COOLDOWN_MAX_MS, COOLDOWN_BASE_MS * 2 ** (strikes - 1));
|
|
367
374
|
const strike = { model, until: now + (stated ?? backoff), strikes, reason, last: now };
|
|
368
375
|
s.cooling.set(model, strike);
|
|
369
|
-
const path =
|
|
376
|
+
const path = join2(s.dir, limitFile(model));
|
|
370
377
|
try {
|
|
371
378
|
writeFileSync(`${path}.tmp`, JSON.stringify(strike));
|
|
372
379
|
renameSync(`${path}.tmp`, path);
|
|
@@ -382,7 +389,7 @@ function coolUnavailable(s, model) {
|
|
|
382
389
|
if (prior && prior.until > until) return prior;
|
|
383
390
|
const strike = { model, until, strikes: 1, reason: "endpoint-unavailable", last: now };
|
|
384
391
|
s.cooling.set(model, strike);
|
|
385
|
-
const path =
|
|
392
|
+
const path = join2(s.dir, limitFile(model));
|
|
386
393
|
try {
|
|
387
394
|
writeFileSync(`${path}.tmp`, JSON.stringify(strike));
|
|
388
395
|
renameSync(`${path}.tmp`, path);
|
package/docs/USER-STORIES.md
CHANGED
|
@@ -317,6 +317,25 @@ The pain: LaCode once shipped a `tools: { "playwright_*": true }` grant on its f
|
|
|
317
317
|
|
|
318
318
|
---
|
|
319
319
|
|
|
320
|
+
### US-27: The package keeps itself current, without npm and without being asked
|
|
321
|
+
**As a** user who installed LaCode once, **I want** each new release to reach my machine automatically, **so that** I never have to learn that opencode freezes the plugin version and hand-clear a cache to upgrade.
|
|
322
|
+
|
|
323
|
+
The pain: opencode's plugin cache is **write-once**. It resolves `@lawaty/lacode` to an exact version once and never re-resolves — not on restart, not with `opencode plugin -g -f`, not with a range pin, not with a tagged config spec (all four verified against opencode 1.18.34). A published release therefore reaches a client only when something installs it into that cache again, and nothing in opencode ever does. The plugin has to do it itself, and it may assume only one tool: the opencode binary the code already runs inside.
|
|
324
|
+
|
|
325
|
+
**Story ID(s) for tests:** US-27
|
|
326
|
+
**Acceptance criteria:**
|
|
327
|
+
- [ ] Given a client runs the plugin and the registry reports a newer version, When the check runs, Then the plugin reinstalls its own cache entry in the background and logs one line naming the old and new version, and startup is never blocked on the registry.
|
|
328
|
+
- [ ] Given the check ran recently, When the plugin loads again within the interval, Then the registry is consulted at most once per interval, so a session start never waits on npm and npm is not hammered.
|
|
329
|
+
- [ ] Given a machine with neither npm nor bun, When an update runs, Then it is performed by the opencode binary alone, because that is the one tool every client is guaranteed to have.
|
|
330
|
+
- [ ] Given an update is installed, When the running session continues, Then it keeps its loaded code and the new version takes effect on the next start, and the log line says exactly that.
|
|
331
|
+
- [ ] Given two opencode processes start together and both find an update, When they race, Then a cross-process lock ensures only one performs the install.
|
|
332
|
+
- [ ] Given `selfUpdate: false` in the plugin options or `LACODE_NO_SELFUPDATE=1` in the environment, When the plugin loads, Then it performs no check at all.
|
|
333
|
+
- [ ] Given the registry is unreachable, When the check runs, Then it is silent, records nothing that would suppress a later retry forever, and never throws into opencode.
|
|
334
|
+
- [ ] Given a plain source checkout rather than an opencode cache install, When the check runs, Then it does not attempt to reinstall, so a development tree is never overwritten.
|
|
335
|
+
- [ ] Given `lacode update` or `lacode update --check`, When it is run, Then it reports (or installs) the update for this machine and exits non-zero only on a real failure — a missing registry, no cache to update, or a failed install.
|
|
336
|
+
|
|
337
|
+
---
|
|
338
|
+
|
|
320
339
|
## Epic G — Context and cost (a maintained map instead of cold exploration)
|
|
321
340
|
|
|
322
341
|
The pain: every task opened by exploring the repository from scratch. The same
|
|
@@ -408,6 +427,7 @@ table was filled in.
|
|
|
408
427
|
| US-16 | F | `tests/stories/f-adoption-safety.test.mjs` |
|
|
409
428
|
| US-17 | F | `tests/stories/f-adoption-safety.test.mjs` |
|
|
410
429
|
| US-24 | F | `tests/stories/f-adoption-safety.test.mjs` |
|
|
430
|
+
| US-27 | F | `tests/stories/h-self-update.test.mjs` |
|
|
411
431
|
| US-19 | G | `tests/stories/g-context-management.test.mjs` |
|
|
412
432
|
| US-20 | G | `tests/stories/g-context-management.test.mjs` |
|
|
413
433
|
| US-21 | G | `tests/stories/g-context-management.test.mjs`, `tests/permission-test.mjs` |
|
package/package.json
CHANGED