agent-yes 1.75.2 → 1.75.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{SUPPORTED_CLIS-C2w9JqbM.js → SUPPORTED_CLIS-C9qkWtPg.js} +3 -2
- package/dist/cli.js +10 -180
- package/dist/index.js +2 -1
- package/dist/{runningLock-DQWJSptq.js → runningLock-C22d9SRJ.js} +18 -2
- package/dist/{tray-D5deJPjk.js → tray-CH_G7aXM.js} +2 -2
- package/dist/{ts-Cuys5-PF.js → ts-vc6cm9z6.js} +5 -5
- package/dist/versionChecker-IVJmMHfo.js +224 -0
- package/package.json +2 -2
- package/ts/runningLock.ts +17 -0
- package/ts/rustBinary.ts +5 -4
- package/ts/versionChecker.spec.ts +82 -2
- package/ts/versionChecker.ts +77 -9
- package/dist/package-8zpT1iww.js +0 -7
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { t as CLIS_CONFIG } from "./ts-
|
|
1
|
+
import { t as CLIS_CONFIG } from "./ts-vc6cm9z6.js";
|
|
2
2
|
import "./logger-B9h0djqx.js";
|
|
3
|
+
import "./versionChecker-IVJmMHfo.js";
|
|
3
4
|
import "./pidStore-C1JXxoPi.js";
|
|
4
5
|
import "./globalPidIndex-Cr-g75QF.js";
|
|
5
6
|
|
|
@@ -8,4 +9,4 @@ const SUPPORTED_CLIS = Object.keys(CLIS_CONFIG);
|
|
|
8
9
|
|
|
9
10
|
//#endregion
|
|
10
11
|
export { SUPPORTED_CLIS };
|
|
11
|
-
//# sourceMappingURL=SUPPORTED_CLIS-
|
|
12
|
+
//# sourceMappingURL=SUPPORTED_CLIS-C9qkWtPg.js.map
|
package/dist/cli.js
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
import { n as logger } from "./logger-B9h0djqx.js";
|
|
3
|
-
import { n as
|
|
3
|
+
import { i as versionString, n as displayVersion, r as getInstalledPackage, t as checkAndAutoUpdate } from "./versionChecker-IVJmMHfo.js";
|
|
4
4
|
import { argv } from "process";
|
|
5
5
|
import { execFileSync, spawn } from "child_process";
|
|
6
6
|
import ms from "ms";
|
|
7
7
|
import yargs from "yargs";
|
|
8
8
|
import { hideBin } from "yargs/helpers";
|
|
9
|
-
import { existsSync,
|
|
10
|
-
import { chmod, copyFile
|
|
11
|
-
import { homedir } from "os";
|
|
9
|
+
import { existsSync, mkdirSync, unlinkSync } from "fs";
|
|
10
|
+
import { chmod, copyFile } from "fs/promises";
|
|
12
11
|
import path from "path";
|
|
13
12
|
|
|
14
13
|
//#region ts/parseCliArgs.ts
|
|
@@ -237,176 +236,6 @@ function parseCliArgs(argv, supportedClis) {
|
|
|
237
236
|
};
|
|
238
237
|
}
|
|
239
238
|
|
|
240
|
-
//#endregion
|
|
241
|
-
//#region ts/versionChecker.ts
|
|
242
|
-
const CACHE_DIR = path.join(homedir(), ".cache", "agent-yes");
|
|
243
|
-
const CACHE_FILE = path.join(CACHE_DIR, "update-check.json");
|
|
244
|
-
const TTL_MS = 3600 * 1e3;
|
|
245
|
-
async function readUpdateCache() {
|
|
246
|
-
try {
|
|
247
|
-
const raw = await readFile(CACHE_FILE, "utf8");
|
|
248
|
-
return JSON.parse(raw);
|
|
249
|
-
} catch {
|
|
250
|
-
return null;
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
async function writeUpdateCache(data) {
|
|
254
|
-
await mkdir(CACHE_DIR, { recursive: true });
|
|
255
|
-
await writeFile(CACHE_FILE, JSON.stringify(data));
|
|
256
|
-
}
|
|
257
|
-
function detectPackageManager() {
|
|
258
|
-
if (process.env.BUN_INSTALL || process.execPath?.includes("bun") || process.env.npm_execpath?.includes("bun")) return "bun";
|
|
259
|
-
return "npm";
|
|
260
|
-
}
|
|
261
|
-
/**
|
|
262
|
-
* Check for updates, auto-install if newer version is available, and re-exec
|
|
263
|
-
* so the current invocation always runs the latest code.
|
|
264
|
-
*
|
|
265
|
-
* Uses a 1-hour TTL cache to avoid hitting the registry on every run.
|
|
266
|
-
* All errors are swallowed — network issues must never break the tool.
|
|
267
|
-
* Set AGENT_YES_NO_UPDATE=1 to opt out.
|
|
268
|
-
*
|
|
269
|
-
* The AGENT_YES_UPDATED env var prevents infinite re-exec loops:
|
|
270
|
-
* after updating we re-exec with AGENT_YES_UPDATED=<version> so the
|
|
271
|
-
* new process skips the update check.
|
|
272
|
-
*/
|
|
273
|
-
async function checkAndAutoUpdate() {
|
|
274
|
-
if (process.env.AGENT_YES_NO_UPDATE) return;
|
|
275
|
-
if (process.env.AGENT_YES_UPDATED) return;
|
|
276
|
-
if (import.meta.url.startsWith("file://") && !import.meta.url.includes("node_modules")) {
|
|
277
|
-
const scriptDir = path.dirname(new URL(import.meta.url).pathname);
|
|
278
|
-
const repoRoot = path.resolve(scriptDir, "..");
|
|
279
|
-
if (existsSync(path.join(repoRoot, ".git"))) return;
|
|
280
|
-
}
|
|
281
|
-
try {
|
|
282
|
-
let latestVersion;
|
|
283
|
-
const cache = await readUpdateCache();
|
|
284
|
-
if (cache && Date.now() - cache.checkedAt < TTL_MS) latestVersion = cache.latestVersion;
|
|
285
|
-
else {
|
|
286
|
-
const fetched = await fetchLatestVersion();
|
|
287
|
-
if (!fetched) return;
|
|
288
|
-
latestVersion = fetched;
|
|
289
|
-
await writeUpdateCache({
|
|
290
|
-
checkedAt: Date.now(),
|
|
291
|
-
latestVersion
|
|
292
|
-
});
|
|
293
|
-
}
|
|
294
|
-
if (compareVersions(version, latestVersion) < 0) {
|
|
295
|
-
if (await runInstall(latestVersion)) reExec(latestVersion);
|
|
296
|
-
}
|
|
297
|
-
} catch {}
|
|
298
|
-
}
|
|
299
|
-
async function runInstall(latestVersion) {
|
|
300
|
-
const installCmd = detectPackageManager() === "bun" ? `bun add -g agent-yes@${latestVersion}` : `npm install -g agent-yes@${latestVersion}`;
|
|
301
|
-
process.stderr.write(`\x1b[33m[agent-yes] Updating ${version} → ${latestVersion}…\x1b[0m\n`);
|
|
302
|
-
try {
|
|
303
|
-
const { execaCommand } = await import("execa");
|
|
304
|
-
await execaCommand(installCmd, { stdio: "inherit" });
|
|
305
|
-
process.stderr.write(`\x1b[32m[agent-yes] Updated to ${latestVersion}\x1b[0m\n`);
|
|
306
|
-
return true;
|
|
307
|
-
} catch {
|
|
308
|
-
process.stderr.write(`\x1b[31m[agent-yes] Auto-update failed. Run: ${installCmd}\x1b[0m\n`);
|
|
309
|
-
return false;
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
/**
|
|
313
|
-
* Re-exec the current process so the newly installed version runs.
|
|
314
|
-
* Sets AGENT_YES_UPDATED=<version> to prevent an infinite loop.
|
|
315
|
-
*/
|
|
316
|
-
function reExec(version) {
|
|
317
|
-
const [bin, ...args] = process.argv;
|
|
318
|
-
process.stderr.write(`\x1b[36m[agent-yes] Restarting with v${version}…\x1b[0m\n`);
|
|
319
|
-
try {
|
|
320
|
-
execFileSync(bin, args, {
|
|
321
|
-
stdio: "inherit",
|
|
322
|
-
env: {
|
|
323
|
-
...process.env,
|
|
324
|
-
AGENT_YES_UPDATED: version
|
|
325
|
-
}
|
|
326
|
-
});
|
|
327
|
-
process.exit(0);
|
|
328
|
-
} catch (err) {
|
|
329
|
-
process.exit(err.status ?? 1);
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
|
-
/**
|
|
333
|
-
* Fetch the latest version of the package from npm registry
|
|
334
|
-
*/
|
|
335
|
-
async function fetchLatestVersion() {
|
|
336
|
-
try {
|
|
337
|
-
const response = await fetch(`https://registry.npmjs.org/${name}/latest`, { signal: AbortSignal.timeout(3e3) });
|
|
338
|
-
if (!response.ok) return null;
|
|
339
|
-
return (await response.json()).version;
|
|
340
|
-
} catch {
|
|
341
|
-
return null;
|
|
342
|
-
}
|
|
343
|
-
}
|
|
344
|
-
/**
|
|
345
|
-
* Compare two semantic versions
|
|
346
|
-
* Returns: 1 if v1 > v2, -1 if v1 < v2, 0 if equal
|
|
347
|
-
*/
|
|
348
|
-
function compareVersions(v1, v2) {
|
|
349
|
-
const parts1 = v1.split(".").map(Number);
|
|
350
|
-
const parts2 = v2.split(".").map(Number);
|
|
351
|
-
for (let i = 0; i < Math.max(parts1.length, parts2.length); i++) {
|
|
352
|
-
const part1 = parts1[i] || 0;
|
|
353
|
-
const part2 = parts2[i] || 0;
|
|
354
|
-
if (part1 > part2) return 1;
|
|
355
|
-
if (part1 < part2) return -1;
|
|
356
|
-
}
|
|
357
|
-
return 0;
|
|
358
|
-
}
|
|
359
|
-
/**
|
|
360
|
-
* Detect how agent-yes was installed.
|
|
361
|
-
* Returns a short label: "git", "bun link", "bun", "npm", "npx", or "unknown"
|
|
362
|
-
*/
|
|
363
|
-
function detectInstallMethod() {
|
|
364
|
-
try {
|
|
365
|
-
const scriptDir = path.dirname(new URL(import.meta.url).pathname);
|
|
366
|
-
if (!scriptDir.includes("node_modules")) {
|
|
367
|
-
const repoRoot = path.resolve(scriptDir, "..");
|
|
368
|
-
if (existsSync(path.join(repoRoot, ".git"))) return "git";
|
|
369
|
-
return "source";
|
|
370
|
-
}
|
|
371
|
-
const nodeModulesEntry = scriptDir.replace(/\/dist$/, "");
|
|
372
|
-
try {
|
|
373
|
-
if (lstatSync(nodeModulesEntry).isSymbolicLink()) {
|
|
374
|
-
const target = readlinkSync(nodeModulesEntry);
|
|
375
|
-
const resolvedTarget = path.resolve(path.dirname(nodeModulesEntry), target);
|
|
376
|
-
if (existsSync(path.join(resolvedTarget, ".git"))) return "bun link (git)";
|
|
377
|
-
return "bun link";
|
|
378
|
-
}
|
|
379
|
-
} catch {}
|
|
380
|
-
if (scriptDir.includes(".bun/")) return "bun";
|
|
381
|
-
if (scriptDir.includes(".npm/")) return "npx";
|
|
382
|
-
if (process.env.npm_execpath?.includes("bun")) return "bun";
|
|
383
|
-
if (process.env.npm_config_user_agent?.startsWith("bun")) return "bun";
|
|
384
|
-
if (process.env.npm_config_user_agent?.startsWith("npm")) return "npm";
|
|
385
|
-
return "npm";
|
|
386
|
-
} catch {
|
|
387
|
-
return "unknown";
|
|
388
|
-
}
|
|
389
|
-
}
|
|
390
|
-
/**
|
|
391
|
-
* Format version string with install method
|
|
392
|
-
*/
|
|
393
|
-
function versionString() {
|
|
394
|
-
return `agent-yes v${version} (${detectInstallMethod()})`;
|
|
395
|
-
}
|
|
396
|
-
/**
|
|
397
|
-
* Display version information with async latest version check
|
|
398
|
-
*/
|
|
399
|
-
async function displayVersion() {
|
|
400
|
-
console.log(versionString());
|
|
401
|
-
const latestVersion = await fetchLatestVersion();
|
|
402
|
-
if (latestVersion) {
|
|
403
|
-
const comparison = compareVersions(version, latestVersion);
|
|
404
|
-
if (comparison < 0) console.log(`\x1b[33m${latestVersion} (update available)\x1b[0m`);
|
|
405
|
-
else if (comparison > 0) console.log(`${latestVersion} (latest published)`);
|
|
406
|
-
else console.log(`${latestVersion} (latest)`);
|
|
407
|
-
} else console.log("(unable to check for updates)");
|
|
408
|
-
}
|
|
409
|
-
|
|
410
239
|
//#endregion
|
|
411
240
|
//#region ts/rustBinary.ts
|
|
412
241
|
/**
|
|
@@ -564,14 +393,15 @@ function getRustBinaryVersion(binaryPath) {
|
|
|
564
393
|
function autoRebuildIfOutdated(binaryPath, verbose) {
|
|
565
394
|
if (!binaryPath.includes("/target/release") && !binaryPath.includes("/target/debug")) return true;
|
|
566
395
|
const binaryVersion = getRustBinaryVersion(binaryPath);
|
|
567
|
-
|
|
568
|
-
if (binaryVersion
|
|
396
|
+
const pkgVersion = getInstalledPackage().version;
|
|
397
|
+
if (verbose) console.log(`[rust] Binary version: ${binaryVersion}, package version: ${pkgVersion}`);
|
|
398
|
+
if (binaryVersion === pkgVersion) return true;
|
|
569
399
|
const rsDir = binaryPath.replace(/\/target\/(release|debug)\/agent-yes.*$/, "");
|
|
570
400
|
if (!existsSync(path.join(rsDir, "Cargo.toml"))) {
|
|
571
401
|
if (verbose) console.log(`[rust] Cannot find Cargo.toml at ${rsDir}, skipping rebuild`);
|
|
572
402
|
return true;
|
|
573
403
|
}
|
|
574
|
-
process.stderr.write(`\x1b[33m[rust] Binary outdated (${binaryVersion ?? "unknown"} → ${
|
|
404
|
+
process.stderr.write(`\x1b[33m[rust] Binary outdated (${binaryVersion ?? "unknown"} → ${pkgVersion}), rebuilding…\x1b[0m\n`);
|
|
575
405
|
try {
|
|
576
406
|
execFileSync("cargo", ["build", ...binaryPath.includes("/target/release") ? ["--release"] : []], {
|
|
577
407
|
cwd: rsDir,
|
|
@@ -645,12 +475,12 @@ await checkAndAutoUpdate();
|
|
|
645
475
|
logger.info(versionString());
|
|
646
476
|
const config = parseCliArgs(process.argv);
|
|
647
477
|
if (config.tray) {
|
|
648
|
-
const { startTray } = await import("./tray-
|
|
478
|
+
const { startTray } = await import("./tray-CH_G7aXM.js");
|
|
649
479
|
await startTray();
|
|
650
480
|
await new Promise(() => {});
|
|
651
481
|
}
|
|
652
482
|
{
|
|
653
|
-
const { ensureTray } = await import("./tray-
|
|
483
|
+
const { ensureTray } = await import("./tray-CH_G7aXM.js");
|
|
654
484
|
ensureTray();
|
|
655
485
|
}
|
|
656
486
|
if (config.useRust) {
|
|
@@ -664,7 +494,7 @@ if (config.useRust) {
|
|
|
664
494
|
}
|
|
665
495
|
}
|
|
666
496
|
if (rustBinary) {
|
|
667
|
-
const { SUPPORTED_CLIS } = await import("./SUPPORTED_CLIS-
|
|
497
|
+
const { SUPPORTED_CLIS } = await import("./SUPPORTED_CLIS-C9qkWtPg.js");
|
|
668
498
|
const rustArgs = buildRustArgs(process.argv, config.cli, SUPPORTED_CLIS);
|
|
669
499
|
if (config.verbose) {
|
|
670
500
|
console.log(`[rust] Using binary: ${rustBinary}`);
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { a as removeControlCharacters, i as AgentContext, n as agentYes, r as config, t as CLIS_CONFIG } from "./ts-
|
|
1
|
+
import { a as removeControlCharacters, i as AgentContext, n as agentYes, r as config, t as CLIS_CONFIG } from "./ts-vc6cm9z6.js";
|
|
2
2
|
import "./logger-B9h0djqx.js";
|
|
3
|
+
import "./versionChecker-IVJmMHfo.js";
|
|
3
4
|
import "./pidStore-C1JXxoPi.js";
|
|
4
5
|
import "./globalPidIndex-Cr-g75QF.js";
|
|
5
6
|
|
|
@@ -28,6 +28,21 @@ function isProcessRunning(pid) {
|
|
|
28
28
|
}
|
|
29
29
|
}
|
|
30
30
|
/**
|
|
31
|
+
* Build an env that scrubs inherited GIT_* repo-locating vars so the spawned
|
|
32
|
+
* `git` resolves the repo from `cwd` only. Without this, running inside a git
|
|
33
|
+
* hook (where GIT_DIR / GIT_INDEX_FILE / GIT_WORK_TREE / GIT_COMMON_DIR are
|
|
34
|
+
* exported) makes `git rev-parse --show-toplevel` use the hook's repo context
|
|
35
|
+
* instead of the requested directory.
|
|
36
|
+
*/
|
|
37
|
+
function gitCleanEnv() {
|
|
38
|
+
const env = { ...process.env };
|
|
39
|
+
delete env.GIT_DIR;
|
|
40
|
+
delete env.GIT_WORK_TREE;
|
|
41
|
+
delete env.GIT_INDEX_FILE;
|
|
42
|
+
delete env.GIT_COMMON_DIR;
|
|
43
|
+
return env;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
31
46
|
* Get git repository root for a directory
|
|
32
47
|
*/
|
|
33
48
|
function getGitRoot(cwd) {
|
|
@@ -39,7 +54,8 @@ function getGitRoot(cwd) {
|
|
|
39
54
|
"pipe",
|
|
40
55
|
"pipe",
|
|
41
56
|
"ignore"
|
|
42
|
-
]
|
|
57
|
+
],
|
|
58
|
+
env: gitCleanEnv()
|
|
43
59
|
}).trim();
|
|
44
60
|
} catch {
|
|
45
61
|
return null;
|
|
@@ -260,4 +276,4 @@ function shouldUseLock(_cwd) {
|
|
|
260
276
|
|
|
261
277
|
//#endregion
|
|
262
278
|
export { shouldUseLock as i, getRunningAgentCount as n, releaseLock as r, acquireLock as t };
|
|
263
|
-
//# sourceMappingURL=runningLock-
|
|
279
|
+
//# sourceMappingURL=runningLock-C22d9SRJ.js.map
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { n as getRunningAgentCount } from "./runningLock-
|
|
1
|
+
import { n as getRunningAgentCount } from "./runningLock-C22d9SRJ.js";
|
|
2
2
|
import { existsSync } from "fs";
|
|
3
3
|
import { mkdir, readFile, unlink, writeFile } from "fs/promises";
|
|
4
4
|
import { homedir } from "os";
|
|
@@ -175,4 +175,4 @@ async function startTray() {
|
|
|
175
175
|
|
|
176
176
|
//#endregion
|
|
177
177
|
export { ensureTray, startTray };
|
|
178
|
-
//# sourceMappingURL=tray-
|
|
178
|
+
//# sourceMappingURL=tray-CH_G7aXM.js.map
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { n as logger, t as addTransport } from "./logger-B9h0djqx.js";
|
|
2
|
-
import {
|
|
3
|
-
import { i as shouldUseLock, r as releaseLock, t as acquireLock } from "./runningLock-
|
|
2
|
+
import { r as getInstalledPackage } from "./versionChecker-IVJmMHfo.js";
|
|
3
|
+
import { i as shouldUseLock, r as releaseLock, t as acquireLock } from "./runningLock-C22d9SRJ.js";
|
|
4
4
|
import { t as PidStore } from "./pidStore-C1JXxoPi.js";
|
|
5
5
|
import { arch, platform } from "process";
|
|
6
6
|
import { execSync } from "child_process";
|
|
@@ -8,6 +8,7 @@ import { closeSync, constants, createReadStream, existsSync, mkdirSync, openSync
|
|
|
8
8
|
import { mkdir, readFile, readdir, unlink, writeFile } from "fs/promises";
|
|
9
9
|
import { homedir } from "os";
|
|
10
10
|
import path, { dirname, join } from "path";
|
|
11
|
+
import { fileURLToPath } from "url";
|
|
11
12
|
import { execaCommandSync, parseCommandString } from "execa";
|
|
12
13
|
import { fromReadable, fromWritable } from "from-node-stream";
|
|
13
14
|
import DIE from "phpdie";
|
|
@@ -15,7 +16,6 @@ import sflow from "sflow";
|
|
|
15
16
|
import xterm from "@xterm/headless";
|
|
16
17
|
import { createServer } from "net";
|
|
17
18
|
import { execSync as execSync$1 } from "node:child_process";
|
|
18
|
-
import { fileURLToPath } from "url";
|
|
19
19
|
import os from "node:os";
|
|
20
20
|
import { readFile as readFile$1 } from "node:fs/promises";
|
|
21
21
|
import path$1 from "node:path";
|
|
@@ -775,7 +775,7 @@ function spawnAgent(options) {
|
|
|
775
775
|
let [bin, ...args] = [...parseCommandString(cliConf?.binary || cli), ...cliArgs];
|
|
776
776
|
logger.debug(`Spawning ${bin} with args: ${JSON.stringify(args)}`);
|
|
777
777
|
const spawned = pty.spawn(bin, args, ptyOptions);
|
|
778
|
-
logger.info(`[${cli}-yes] Spawned ${bin} with PID ${spawned.pid} (agent-yes v${version})`);
|
|
778
|
+
logger.info(`[${cli}-yes] Spawned ${bin} with PID ${spawned.pid} (agent-yes v${getInstalledPackage().version})`);
|
|
779
779
|
return spawned;
|
|
780
780
|
};
|
|
781
781
|
return tryCatch((error, attempts, spawn, ...args) => {
|
|
@@ -1679,4 +1679,4 @@ function sleep(ms) {
|
|
|
1679
1679
|
|
|
1680
1680
|
//#endregion
|
|
1681
1681
|
export { removeControlCharacters as a, AgentContext as i, agentYes as n, config as r, CLIS_CONFIG as t };
|
|
1682
|
-
//# sourceMappingURL=ts-
|
|
1682
|
+
//# sourceMappingURL=ts-vc6cm9z6.js.map
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import { execFileSync } from "child_process";
|
|
2
|
+
import { existsSync, lstatSync, readFileSync, readlinkSync } from "fs";
|
|
3
|
+
import { mkdir, readFile, writeFile } from "fs/promises";
|
|
4
|
+
import { homedir } from "os";
|
|
5
|
+
import path from "path";
|
|
6
|
+
import { fileURLToPath } from "url";
|
|
7
|
+
|
|
8
|
+
//#region package.json
|
|
9
|
+
var name = "agent-yes";
|
|
10
|
+
var version = "1.75.3";
|
|
11
|
+
|
|
12
|
+
//#endregion
|
|
13
|
+
//#region ts/versionChecker.ts
|
|
14
|
+
const CACHE_DIR = path.join(homedir(), ".cache", "agent-yes");
|
|
15
|
+
const CACHE_FILE = path.join(CACHE_DIR, "update-check.json");
|
|
16
|
+
const TTL_MS = 3600 * 1e3;
|
|
17
|
+
const CANONICAL_PKG_NAME = "agent-yes";
|
|
18
|
+
let cachedInstalledPkg = null;
|
|
19
|
+
/**
|
|
20
|
+
* Read the live `package.json` from disk for the running module.
|
|
21
|
+
*
|
|
22
|
+
* The bundled `package.json` import is inlined at build time; if `dist/` is
|
|
23
|
+
* published without a fresh build (issue #39), the inlined `version` lies
|
|
24
|
+
* and the auto-update loop fires forever. Reading the on-disk manifest each
|
|
25
|
+
* run keeps the version honest even when the bundle is stale.
|
|
26
|
+
*/
|
|
27
|
+
function getInstalledPackage() {
|
|
28
|
+
if (cachedInstalledPkg) return cachedInstalledPkg;
|
|
29
|
+
let dir = null;
|
|
30
|
+
try {
|
|
31
|
+
dir = path.dirname(fileURLToPath(import.meta.url));
|
|
32
|
+
} catch {}
|
|
33
|
+
if (dir) for (let i = 0; i < 6; i++) {
|
|
34
|
+
const candidate = path.join(dir, "package.json");
|
|
35
|
+
try {
|
|
36
|
+
if (existsSync(candidate)) {
|
|
37
|
+
const json = JSON.parse(readFileSync(candidate, "utf8"));
|
|
38
|
+
if (json.name === name && typeof json.version === "string") {
|
|
39
|
+
cachedInstalledPkg = {
|
|
40
|
+
name: json.name,
|
|
41
|
+
version: json.version
|
|
42
|
+
};
|
|
43
|
+
return cachedInstalledPkg;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
} catch {}
|
|
47
|
+
const parent = path.dirname(dir);
|
|
48
|
+
if (parent === dir) break;
|
|
49
|
+
dir = parent;
|
|
50
|
+
}
|
|
51
|
+
cachedInstalledPkg = {
|
|
52
|
+
name,
|
|
53
|
+
version
|
|
54
|
+
};
|
|
55
|
+
return cachedInstalledPkg;
|
|
56
|
+
}
|
|
57
|
+
async function readUpdateCache() {
|
|
58
|
+
try {
|
|
59
|
+
const raw = await readFile(CACHE_FILE, "utf8");
|
|
60
|
+
return JSON.parse(raw);
|
|
61
|
+
} catch {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
async function writeUpdateCache(data) {
|
|
66
|
+
await mkdir(CACHE_DIR, { recursive: true });
|
|
67
|
+
await writeFile(CACHE_FILE, JSON.stringify(data));
|
|
68
|
+
}
|
|
69
|
+
function detectPackageManager() {
|
|
70
|
+
if (process.env.BUN_INSTALL || process.execPath?.includes("bun") || process.env.npm_execpath?.includes("bun")) return "bun";
|
|
71
|
+
return "npm";
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Check for updates, auto-install if newer version is available, and re-exec
|
|
75
|
+
* so the current invocation always runs the latest code.
|
|
76
|
+
*
|
|
77
|
+
* Uses a 1-hour TTL cache to avoid hitting the registry on every run.
|
|
78
|
+
* All errors are swallowed — network issues must never break the tool.
|
|
79
|
+
* Set AGENT_YES_NO_UPDATE=1 to opt out.
|
|
80
|
+
*
|
|
81
|
+
* The AGENT_YES_UPDATED env var prevents infinite re-exec loops:
|
|
82
|
+
* after updating we re-exec with AGENT_YES_UPDATED=<version> so the
|
|
83
|
+
* new process skips the update check.
|
|
84
|
+
*/
|
|
85
|
+
async function checkAndAutoUpdate() {
|
|
86
|
+
if (process.env.AGENT_YES_NO_UPDATE) return;
|
|
87
|
+
if (process.env.AGENT_YES_UPDATED) return;
|
|
88
|
+
if (import.meta.url.startsWith("file://") && !import.meta.url.includes("node_modules")) {
|
|
89
|
+
const scriptDir = path.dirname(new URL(import.meta.url).pathname);
|
|
90
|
+
const repoRoot = path.resolve(scriptDir, "..");
|
|
91
|
+
if (existsSync(path.join(repoRoot, ".git"))) return;
|
|
92
|
+
}
|
|
93
|
+
try {
|
|
94
|
+
let latestVersion;
|
|
95
|
+
const cache = await readUpdateCache();
|
|
96
|
+
if (cache && Date.now() - cache.checkedAt < TTL_MS) latestVersion = cache.latestVersion;
|
|
97
|
+
else {
|
|
98
|
+
const fetched = await fetchLatestVersion();
|
|
99
|
+
if (!fetched) return;
|
|
100
|
+
latestVersion = fetched;
|
|
101
|
+
await writeUpdateCache({
|
|
102
|
+
checkedAt: Date.now(),
|
|
103
|
+
latestVersion
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
if (compareVersions(getInstalledPackage().version, latestVersion) < 0) {
|
|
107
|
+
if (await runInstall(latestVersion)) reExec(latestVersion);
|
|
108
|
+
}
|
|
109
|
+
} catch {}
|
|
110
|
+
}
|
|
111
|
+
async function runInstall(latestVersion) {
|
|
112
|
+
const installCmd = detectPackageManager() === "bun" ? `bun add -g ${CANONICAL_PKG_NAME}@${latestVersion}` : `npm install -g ${CANONICAL_PKG_NAME}@${latestVersion}`;
|
|
113
|
+
process.stderr.write(`\x1b[33m[agent-yes] Updating ${getInstalledPackage().version} → ${latestVersion}…\x1b[0m\n`);
|
|
114
|
+
try {
|
|
115
|
+
const { execaCommand } = await import("execa");
|
|
116
|
+
await execaCommand(installCmd, { stdio: "inherit" });
|
|
117
|
+
process.stderr.write(`\x1b[32m[agent-yes] Updated to ${latestVersion}\x1b[0m\n`);
|
|
118
|
+
return true;
|
|
119
|
+
} catch {
|
|
120
|
+
process.stderr.write(`\x1b[31m[agent-yes] Auto-update failed. Run: ${installCmd}\x1b[0m\n`);
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Re-exec the current process so the newly installed version runs.
|
|
126
|
+
* Sets AGENT_YES_UPDATED=<version> to prevent an infinite loop.
|
|
127
|
+
*/
|
|
128
|
+
function reExec(version) {
|
|
129
|
+
const [bin, ...args] = process.argv;
|
|
130
|
+
process.stderr.write(`\x1b[36m[agent-yes] Restarting with v${version}…\x1b[0m\n`);
|
|
131
|
+
try {
|
|
132
|
+
execFileSync(bin, args, {
|
|
133
|
+
stdio: "inherit",
|
|
134
|
+
env: {
|
|
135
|
+
...process.env,
|
|
136
|
+
AGENT_YES_UPDATED: version
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
process.exit(0);
|
|
140
|
+
} catch (err) {
|
|
141
|
+
process.exit(err.status ?? 1);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Fetch the latest version of the package from npm registry
|
|
146
|
+
*/
|
|
147
|
+
async function fetchLatestVersion() {
|
|
148
|
+
try {
|
|
149
|
+
const response = await fetch(`https://registry.npmjs.org/${CANONICAL_PKG_NAME}/latest`, { signal: AbortSignal.timeout(3e3) });
|
|
150
|
+
if (!response.ok) return null;
|
|
151
|
+
return (await response.json()).version;
|
|
152
|
+
} catch {
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Compare two semantic versions
|
|
158
|
+
* Returns: 1 if v1 > v2, -1 if v1 < v2, 0 if equal
|
|
159
|
+
*/
|
|
160
|
+
function compareVersions(v1, v2) {
|
|
161
|
+
const parts1 = v1.split(".").map(Number);
|
|
162
|
+
const parts2 = v2.split(".").map(Number);
|
|
163
|
+
for (let i = 0; i < Math.max(parts1.length, parts2.length); i++) {
|
|
164
|
+
const part1 = parts1[i] || 0;
|
|
165
|
+
const part2 = parts2[i] || 0;
|
|
166
|
+
if (part1 > part2) return 1;
|
|
167
|
+
if (part1 < part2) return -1;
|
|
168
|
+
}
|
|
169
|
+
return 0;
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Detect how agent-yes was installed.
|
|
173
|
+
* Returns a short label: "git", "bun link", "bun", "npm", "npx", or "unknown"
|
|
174
|
+
*/
|
|
175
|
+
function detectInstallMethod() {
|
|
176
|
+
try {
|
|
177
|
+
const scriptDir = path.dirname(new URL(import.meta.url).pathname);
|
|
178
|
+
if (!scriptDir.includes("node_modules")) {
|
|
179
|
+
const repoRoot = path.resolve(scriptDir, "..");
|
|
180
|
+
if (existsSync(path.join(repoRoot, ".git"))) return "git";
|
|
181
|
+
return "source";
|
|
182
|
+
}
|
|
183
|
+
const nodeModulesEntry = scriptDir.replace(/\/dist$/, "");
|
|
184
|
+
try {
|
|
185
|
+
if (lstatSync(nodeModulesEntry).isSymbolicLink()) {
|
|
186
|
+
const target = readlinkSync(nodeModulesEntry);
|
|
187
|
+
const resolvedTarget = path.resolve(path.dirname(nodeModulesEntry), target);
|
|
188
|
+
if (existsSync(path.join(resolvedTarget, ".git"))) return "bun link (git)";
|
|
189
|
+
return "bun link";
|
|
190
|
+
}
|
|
191
|
+
} catch {}
|
|
192
|
+
if (scriptDir.includes(".bun/")) return "bun";
|
|
193
|
+
if (scriptDir.includes(".npm/")) return "npx";
|
|
194
|
+
if (process.env.npm_execpath?.includes("bun")) return "bun";
|
|
195
|
+
if (process.env.npm_config_user_agent?.startsWith("bun")) return "bun";
|
|
196
|
+
if (process.env.npm_config_user_agent?.startsWith("npm")) return "npm";
|
|
197
|
+
return "npm";
|
|
198
|
+
} catch {
|
|
199
|
+
return "unknown";
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Format version string with install method
|
|
204
|
+
*/
|
|
205
|
+
function versionString() {
|
|
206
|
+
return `agent-yes v${getInstalledPackage().version} (${detectInstallMethod()})`;
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Display version information with async latest version check
|
|
210
|
+
*/
|
|
211
|
+
async function displayVersion() {
|
|
212
|
+
console.log(versionString());
|
|
213
|
+
const latestVersion = await fetchLatestVersion();
|
|
214
|
+
if (latestVersion) {
|
|
215
|
+
const comparison = compareVersions(getInstalledPackage().version, latestVersion);
|
|
216
|
+
if (comparison < 0) console.log(`\x1b[33m${latestVersion} (update available)\x1b[0m`);
|
|
217
|
+
else if (comparison > 0) console.log(`${latestVersion} (latest published)`);
|
|
218
|
+
else console.log(`${latestVersion} (latest)`);
|
|
219
|
+
} else console.log("(unable to check for updates)");
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
//#endregion
|
|
223
|
+
export { versionString as i, displayVersion as n, getInstalledPackage as r, checkAndAutoUpdate as t };
|
|
224
|
+
//# sourceMappingURL=versionChecker-IVJmMHfo.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-yes",
|
|
3
|
-
"version": "1.75.
|
|
3
|
+
"version": "1.75.3",
|
|
4
4
|
"description": "A wrapper tool that automates interactions with various AI CLI tools by automatically handling common prompts and responses.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai",
|
|
@@ -77,7 +77,7 @@
|
|
|
77
77
|
"fmt": "oxlint --fix --fix-suggestions && oxfmt",
|
|
78
78
|
"verify-deps": "node ./scripts/verify-deps.js",
|
|
79
79
|
"_prepack": "bun run build",
|
|
80
|
-
"prepublishOnly": "npm run verify-deps",
|
|
80
|
+
"prepublishOnly": "bun run build && npm run verify-deps",
|
|
81
81
|
"prepare": "husky",
|
|
82
82
|
"release": "standard-version && npm publish",
|
|
83
83
|
"release:beta": "standard-version && npm publish --tag beta",
|
package/ts/runningLock.ts
CHANGED
|
@@ -43,6 +43,22 @@ function isProcessRunning(pid: number): boolean {
|
|
|
43
43
|
}
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
/**
|
|
47
|
+
* Build an env that scrubs inherited GIT_* repo-locating vars so the spawned
|
|
48
|
+
* `git` resolves the repo from `cwd` only. Without this, running inside a git
|
|
49
|
+
* hook (where GIT_DIR / GIT_INDEX_FILE / GIT_WORK_TREE / GIT_COMMON_DIR are
|
|
50
|
+
* exported) makes `git rev-parse --show-toplevel` use the hook's repo context
|
|
51
|
+
* instead of the requested directory.
|
|
52
|
+
*/
|
|
53
|
+
function gitCleanEnv(): NodeJS.ProcessEnv {
|
|
54
|
+
const env: NodeJS.ProcessEnv = { ...process.env };
|
|
55
|
+
delete env.GIT_DIR;
|
|
56
|
+
delete env.GIT_WORK_TREE;
|
|
57
|
+
delete env.GIT_INDEX_FILE;
|
|
58
|
+
delete env.GIT_COMMON_DIR;
|
|
59
|
+
return env;
|
|
60
|
+
}
|
|
61
|
+
|
|
46
62
|
/**
|
|
47
63
|
* Get git repository root for a directory
|
|
48
64
|
*/
|
|
@@ -52,6 +68,7 @@ function getGitRoot(cwd: string): string | null {
|
|
|
52
68
|
cwd,
|
|
53
69
|
encoding: "utf8",
|
|
54
70
|
stdio: ["pipe", "pipe", "ignore"],
|
|
71
|
+
env: gitCleanEnv(),
|
|
55
72
|
});
|
|
56
73
|
return result.trim();
|
|
57
74
|
} catch {
|
package/ts/rustBinary.ts
CHANGED
|
@@ -6,7 +6,7 @@ import { execFileSync } from "child_process";
|
|
|
6
6
|
import { existsSync, mkdirSync, unlinkSync } from "fs";
|
|
7
7
|
import { chmod, copyFile } from "fs/promises";
|
|
8
8
|
import path from "path";
|
|
9
|
-
import
|
|
9
|
+
import { getInstalledPackage } from "./versionChecker.ts";
|
|
10
10
|
|
|
11
11
|
// Platform/arch to binary name mapping
|
|
12
12
|
const PLATFORM_MAP: Record<string, string> = {
|
|
@@ -223,11 +223,12 @@ function autoRebuildIfOutdated(binaryPath: string, verbose: boolean): boolean {
|
|
|
223
223
|
}
|
|
224
224
|
|
|
225
225
|
const binaryVersion = getRustBinaryVersion(binaryPath);
|
|
226
|
+
const pkgVersion = getInstalledPackage().version;
|
|
226
227
|
if (verbose) {
|
|
227
|
-
console.log(`[rust] Binary version: ${binaryVersion}, package version: ${
|
|
228
|
+
console.log(`[rust] Binary version: ${binaryVersion}, package version: ${pkgVersion}`);
|
|
228
229
|
}
|
|
229
230
|
|
|
230
|
-
if (binaryVersion ===
|
|
231
|
+
if (binaryVersion === pkgVersion) {
|
|
231
232
|
return true; // up to date
|
|
232
233
|
}
|
|
233
234
|
|
|
@@ -239,7 +240,7 @@ function autoRebuildIfOutdated(binaryPath: string, verbose: boolean): boolean {
|
|
|
239
240
|
}
|
|
240
241
|
|
|
241
242
|
process.stderr.write(
|
|
242
|
-
`\x1b[33m[rust] Binary outdated (${binaryVersion ?? "unknown"} → ${
|
|
243
|
+
`\x1b[33m[rust] Binary outdated (${binaryVersion ?? "unknown"} → ${pkgVersion}), rebuilding…\x1b[0m\n`,
|
|
243
244
|
);
|
|
244
245
|
|
|
245
246
|
try {
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { describe, expect, it, vi, beforeEach, afterEach } from "vitest";
|
|
2
2
|
import {
|
|
3
|
+
_setInstalledPackageForTesting,
|
|
3
4
|
checkAndAutoUpdate,
|
|
4
5
|
compareVersions,
|
|
5
6
|
fetchLatestVersion,
|
|
6
7
|
displayVersion,
|
|
7
8
|
detectInstallMethod,
|
|
9
|
+
getInstalledPackage,
|
|
8
10
|
versionString,
|
|
9
|
-
} from "./versionChecker";
|
|
11
|
+
} from "./versionChecker.ts";
|
|
10
12
|
|
|
11
13
|
vi.mock("execa", () => ({ execaCommand: vi.fn().mockResolvedValue({}) }));
|
|
12
14
|
vi.mock("fs/promises", () => ({
|
|
@@ -18,8 +20,12 @@ vi.mock("fs", async (importOriginal) => {
|
|
|
18
20
|
const actual = await importOriginal<typeof import("fs")>();
|
|
19
21
|
return {
|
|
20
22
|
...actual,
|
|
21
|
-
// Return false for .git
|
|
23
|
+
// Return false for .git / package.json lookups so neither the dev-checkout
|
|
24
|
+
// guard nor getInstalledPackage's disk read fires during the auto-update tests.
|
|
22
25
|
existsSync: vi.fn(() => false),
|
|
26
|
+
readFileSync: vi.fn(() => {
|
|
27
|
+
throw new Error("readFileSync not stubbed");
|
|
28
|
+
}),
|
|
23
29
|
lstatSync: actual.lstatSync,
|
|
24
30
|
readlinkSync: actual.readlinkSync,
|
|
25
31
|
};
|
|
@@ -99,6 +105,7 @@ describe("versionChecker", () => {
|
|
|
99
105
|
describe("checkAndAutoUpdate", () => {
|
|
100
106
|
beforeEach(() => {
|
|
101
107
|
vi.clearAllMocks();
|
|
108
|
+
_setInstalledPackageForTesting(null);
|
|
102
109
|
vi.stubGlobal("fetch", vi.fn());
|
|
103
110
|
vi.spyOn(process.stderr, "write").mockImplementation(() => true);
|
|
104
111
|
// Use a mock for process.exit to prevent actual exit in tests
|
|
@@ -300,4 +307,77 @@ describe("versionChecker", () => {
|
|
|
300
307
|
expect(str).toMatch(/agent-yes v\d+\.\d+\.\d+ \(.+\)/);
|
|
301
308
|
});
|
|
302
309
|
});
|
|
310
|
+
|
|
311
|
+
// Regression test for https://github.com/snomiao/agent-yes/issues/39:
|
|
312
|
+
// a stale bundled version string must not pin the auto-update comparison
|
|
313
|
+
// when a fresh package.json is on disk next to the running module.
|
|
314
|
+
describe("getInstalledPackage (issue #39)", () => {
|
|
315
|
+
beforeEach(() => {
|
|
316
|
+
vi.clearAllMocks();
|
|
317
|
+
_setInstalledPackageForTesting(null);
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
afterEach(() => {
|
|
321
|
+
vi.restoreAllMocks();
|
|
322
|
+
_setInstalledPackageForTesting(null);
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
it("prefers the on-disk package.json over the bundled (potentially stale) import", async () => {
|
|
326
|
+
const fs = await import("fs");
|
|
327
|
+
vi.mocked(fs.existsSync).mockReturnValueOnce(true);
|
|
328
|
+
vi.mocked(fs.readFileSync).mockReturnValueOnce(
|
|
329
|
+
JSON.stringify({ name: "agent-yes", version: "999.0.0" }) as any,
|
|
330
|
+
);
|
|
331
|
+
|
|
332
|
+
const resolved = getInstalledPackage();
|
|
333
|
+
expect(resolved.version).toBe("999.0.0");
|
|
334
|
+
expect(resolved.name).toBe("agent-yes");
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
it("continues walking parents when a candidate package.json is unreadable", async () => {
|
|
338
|
+
// Per-candidate try/catch: an unreadable/unparsable manifest at one
|
|
339
|
+
// level must not abort the upward walk and silently fall back to the
|
|
340
|
+
// bundled (stale) manifest. The walk must keep going until it finds
|
|
341
|
+
// a matching package.json or exhausts parents.
|
|
342
|
+
const fs = await import("fs");
|
|
343
|
+
let call = 0;
|
|
344
|
+
vi.mocked(fs.existsSync).mockImplementation(() => true);
|
|
345
|
+
vi.mocked(fs.readFileSync).mockImplementation(() => {
|
|
346
|
+
call += 1;
|
|
347
|
+
if (call === 1) throw new Error("EACCES");
|
|
348
|
+
if (call === 2) return "{not json" as any;
|
|
349
|
+
return JSON.stringify({ name: "agent-yes", version: "999.0.0" }) as any;
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
const resolved = getInstalledPackage();
|
|
353
|
+
expect(resolved.version).toBe("999.0.0");
|
|
354
|
+
expect(call).toBeGreaterThanOrEqual(3);
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
it("does not trigger an auto-update when on-disk version already matches the registry", async () => {
|
|
358
|
+
// Simulate the post-fix scenario: the bundled `pkg.version` (frozen at
|
|
359
|
+
// build time) is older than the registry, but the runtime resolver
|
|
360
|
+
// surfaces the correct on-disk version and the comparison short-circuits.
|
|
361
|
+
// Pre-fix behavior was install + reExec on every invocation → infinite loop.
|
|
362
|
+
_setInstalledPackageForTesting({ name: "agent-yes", version: "999.0.0" });
|
|
363
|
+
|
|
364
|
+
const { readFile } = await import("fs/promises");
|
|
365
|
+
const { execaCommand } = await import("execa");
|
|
366
|
+
vi.mocked(readFile).mockRejectedValueOnce(new Error("no cache"));
|
|
367
|
+
vi.stubGlobal(
|
|
368
|
+
"fetch",
|
|
369
|
+
vi.fn().mockResolvedValue({
|
|
370
|
+
ok: true,
|
|
371
|
+
json: async () => ({ version: "999.0.0" }),
|
|
372
|
+
} as Response),
|
|
373
|
+
);
|
|
374
|
+
vi.spyOn(process.stderr, "write").mockImplementation(() => true);
|
|
375
|
+
vi.spyOn(process, "exit").mockImplementation(() => undefined as never);
|
|
376
|
+
delete process.env.AGENT_YES_NO_UPDATE;
|
|
377
|
+
delete process.env.AGENT_YES_UPDATED;
|
|
378
|
+
|
|
379
|
+
await checkAndAutoUpdate();
|
|
380
|
+
expect(execaCommand).not.toHaveBeenCalled();
|
|
381
|
+
});
|
|
382
|
+
});
|
|
303
383
|
});
|
package/ts/versionChecker.ts
CHANGED
|
@@ -1,14 +1,80 @@
|
|
|
1
1
|
import { execFileSync } from "child_process";
|
|
2
|
-
import { existsSync, lstatSync, readlinkSync } from "fs";
|
|
2
|
+
import { existsSync, lstatSync, readFileSync, readlinkSync } from "fs";
|
|
3
3
|
import { mkdir, readFile, writeFile } from "fs/promises";
|
|
4
4
|
import { homedir } from "os";
|
|
5
5
|
import path from "path";
|
|
6
|
-
import
|
|
6
|
+
import { fileURLToPath } from "url";
|
|
7
|
+
import bundledPkg from "../package.json" with { type: "json" };
|
|
7
8
|
|
|
8
9
|
const CACHE_DIR = path.join(homedir(), ".cache", "agent-yes");
|
|
9
10
|
const CACHE_FILE = path.join(CACHE_DIR, "update-check.json");
|
|
10
11
|
const TTL_MS = 60 * 60 * 1000; // 1 hour
|
|
11
12
|
|
|
13
|
+
// The release pipeline publishes both `agent-yes` and `claude-yes` from the
|
|
14
|
+
// same source by flipping `package.json#name` and re-running `npm publish`
|
|
15
|
+
// (which now triggers `bun run build`, rebuilding dist with whichever name is
|
|
16
|
+
// set). The auto-updater's registry lookup, install command, and shared
|
|
17
|
+
// cache file must all stay pinned to the canonical package — otherwise a
|
|
18
|
+
// `claude-yes` install would query `claude-yes/latest` while `runInstall`
|
|
19
|
+
// still hard-codes `agent-yes`.
|
|
20
|
+
const CANONICAL_PKG_NAME = "agent-yes";
|
|
21
|
+
|
|
22
|
+
let cachedInstalledPkg: { name: string; version: string } | null = null;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Read the live `package.json` from disk for the running module.
|
|
26
|
+
*
|
|
27
|
+
* The bundled `package.json` import is inlined at build time; if `dist/` is
|
|
28
|
+
* published without a fresh build (issue #39), the inlined `version` lies
|
|
29
|
+
* and the auto-update loop fires forever. Reading the on-disk manifest each
|
|
30
|
+
* run keeps the version honest even when the bundle is stale.
|
|
31
|
+
*/
|
|
32
|
+
export function getInstalledPackage(): { name: string; version: string } {
|
|
33
|
+
if (cachedInstalledPkg) return cachedInstalledPkg;
|
|
34
|
+
let dir: string | null = null;
|
|
35
|
+
try {
|
|
36
|
+
dir = path.dirname(fileURLToPath(import.meta.url));
|
|
37
|
+
} catch {
|
|
38
|
+
// import.meta.url malformed; fall through to bundled
|
|
39
|
+
}
|
|
40
|
+
if (dir) {
|
|
41
|
+
for (let i = 0; i < 6; i++) {
|
|
42
|
+
const candidate = path.join(dir, "package.json");
|
|
43
|
+
// A per-candidate try/catch: a transient read error, partial write, or
|
|
44
|
+
// BOM on any single package.json must NOT abort the upward walk —
|
|
45
|
+
// otherwise we'd silently fall back to the stale bundled manifest that
|
|
46
|
+
// issue #39 was about. Keep walking until we either find a matching
|
|
47
|
+
// manifest or exhaust parents.
|
|
48
|
+
try {
|
|
49
|
+
if (existsSync(candidate)) {
|
|
50
|
+
const json = JSON.parse(readFileSync(candidate, "utf8")) as {
|
|
51
|
+
name?: string;
|
|
52
|
+
version?: string;
|
|
53
|
+
};
|
|
54
|
+
if (json.name === bundledPkg.name && typeof json.version === "string") {
|
|
55
|
+
cachedInstalledPkg = { name: json.name, version: json.version };
|
|
56
|
+
return cachedInstalledPkg;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
} catch {
|
|
60
|
+
// unreadable / unparsable — continue walking
|
|
61
|
+
}
|
|
62
|
+
const parent = path.dirname(dir);
|
|
63
|
+
if (parent === dir) break;
|
|
64
|
+
dir = parent;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
cachedInstalledPkg = { name: bundledPkg.name, version: bundledPkg.version };
|
|
68
|
+
return cachedInstalledPkg;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Test-only: clear or seed the memoized lookup. */
|
|
72
|
+
export function _setInstalledPackageForTesting(
|
|
73
|
+
value: { name: string; version: string } | null,
|
|
74
|
+
): void {
|
|
75
|
+
cachedInstalledPkg = value;
|
|
76
|
+
}
|
|
77
|
+
|
|
12
78
|
type UpdateCache = { checkedAt: number; latestVersion: string };
|
|
13
79
|
|
|
14
80
|
async function readUpdateCache(): Promise<UpdateCache | null> {
|
|
@@ -75,7 +141,7 @@ export async function checkAndAutoUpdate(): Promise<void> {
|
|
|
75
141
|
await writeUpdateCache({ checkedAt: Date.now(), latestVersion });
|
|
76
142
|
}
|
|
77
143
|
|
|
78
|
-
if (compareVersions(
|
|
144
|
+
if (compareVersions(getInstalledPackage().version, latestVersion) < 0) {
|
|
79
145
|
const installed = await runInstall(latestVersion);
|
|
80
146
|
if (installed) {
|
|
81
147
|
reExec(latestVersion);
|
|
@@ -90,10 +156,12 @@ async function runInstall(latestVersion: string): Promise<boolean> {
|
|
|
90
156
|
const pm = detectPackageManager();
|
|
91
157
|
const installCmd =
|
|
92
158
|
pm === "bun"
|
|
93
|
-
? `bun add -g
|
|
94
|
-
: `npm install -g
|
|
159
|
+
? `bun add -g ${CANONICAL_PKG_NAME}@${latestVersion}`
|
|
160
|
+
: `npm install -g ${CANONICAL_PKG_NAME}@${latestVersion}`;
|
|
95
161
|
|
|
96
|
-
process.stderr.write(
|
|
162
|
+
process.stderr.write(
|
|
163
|
+
`\x1b[33m[agent-yes] Updating ${getInstalledPackage().version} → ${latestVersion}…\x1b[0m\n`,
|
|
164
|
+
);
|
|
97
165
|
try {
|
|
98
166
|
const { execaCommand } = await import("execa");
|
|
99
167
|
await execaCommand(installCmd, { stdio: "inherit" });
|
|
@@ -128,7 +196,7 @@ function reExec(version: string): never {
|
|
|
128
196
|
*/
|
|
129
197
|
export async function fetchLatestVersion(): Promise<string | null> {
|
|
130
198
|
try {
|
|
131
|
-
const response = await fetch(`https://registry.npmjs.org/${
|
|
199
|
+
const response = await fetch(`https://registry.npmjs.org/${CANONICAL_PKG_NAME}/latest`, {
|
|
132
200
|
signal: AbortSignal.timeout(3000), // 3 second timeout
|
|
133
201
|
});
|
|
134
202
|
|
|
@@ -215,7 +283,7 @@ export function detectInstallMethod(): string {
|
|
|
215
283
|
* Format version string with install method
|
|
216
284
|
*/
|
|
217
285
|
export function versionString(): string {
|
|
218
|
-
return `agent-yes v${
|
|
286
|
+
return `agent-yes v${getInstalledPackage().version} (${detectInstallMethod()})`;
|
|
219
287
|
}
|
|
220
288
|
|
|
221
289
|
/**
|
|
@@ -227,7 +295,7 @@ export async function displayVersion(): Promise<void> {
|
|
|
227
295
|
const latestVersion = await fetchLatestVersion();
|
|
228
296
|
|
|
229
297
|
if (latestVersion) {
|
|
230
|
-
const comparison = compareVersions(
|
|
298
|
+
const comparison = compareVersions(getInstalledPackage().version, latestVersion);
|
|
231
299
|
|
|
232
300
|
if (comparison < 0) {
|
|
233
301
|
console.log(`\x1b[33m${latestVersion} (update available)\x1b[0m`);
|