agent-yes 1.67.0 → 1.68.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{SUPPORTED_CLIS-CmSMCHW2.js → SUPPORTED_CLIS-Dxapc1qa.js} +2 -2
- package/dist/cli.js +89 -25
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/ts/cli.ts +47 -40
- package/ts/versionChecker.spec.ts +114 -1
- package/ts/versionChecker.ts +81 -0
|
@@ -1059,7 +1059,7 @@ function tryCatch(catchFn, fn) {
|
|
|
1059
1059
|
//#endregion
|
|
1060
1060
|
//#region package.json
|
|
1061
1061
|
var name = "agent-yes";
|
|
1062
|
-
var version = "1.
|
|
1062
|
+
var version = "1.68.1";
|
|
1063
1063
|
|
|
1064
1064
|
//#endregion
|
|
1065
1065
|
//#region ts/pty-fix.ts
|
|
@@ -2139,4 +2139,4 @@ const SUPPORTED_CLIS = Object.keys(CLIS_CONFIG);
|
|
|
2139
2139
|
|
|
2140
2140
|
//#endregion
|
|
2141
2141
|
export { AgentContext as a, PidStore as c, config as i, removeControlCharacters as l, CLIS_CONFIG as n, name as o, agentYes as r, version as s, SUPPORTED_CLIS as t };
|
|
2142
|
-
//# sourceMappingURL=SUPPORTED_CLIS-
|
|
2142
|
+
//# sourceMappingURL=SUPPORTED_CLIS-Dxapc1qa.js.map
|
package/dist/cli.js
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
|
-
import { c as PidStore, o as name, s as version, t as SUPPORTED_CLIS } from "./SUPPORTED_CLIS-
|
|
2
|
+
import { c as PidStore, o as name, s as version, t as SUPPORTED_CLIS } from "./SUPPORTED_CLIS-Dxapc1qa.js";
|
|
3
3
|
import { t as logger } from "./logger-CX77vJDA.js";
|
|
4
4
|
import { argv } from "process";
|
|
5
5
|
import { 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 {
|
|
9
|
+
import { execaCommand } from "execa";
|
|
10
|
+
import { chmod, copyFile, mkdir, readFile, writeFile } from "fs/promises";
|
|
10
11
|
import path from "path";
|
|
12
|
+
import { homedir } from "os";
|
|
11
13
|
import { existsSync, mkdirSync, unlinkSync } from "fs";
|
|
12
14
|
|
|
13
15
|
//#region ts/parseCliArgs.ts
|
|
@@ -202,6 +204,62 @@ function parseCliArgs(argv) {
|
|
|
202
204
|
|
|
203
205
|
//#endregion
|
|
204
206
|
//#region ts/versionChecker.ts
|
|
207
|
+
const CACHE_DIR = path.join(homedir(), ".cache", "agent-yes");
|
|
208
|
+
const CACHE_FILE = path.join(CACHE_DIR, "update-check.json");
|
|
209
|
+
const TTL_MS = 3600 * 1e3;
|
|
210
|
+
async function readUpdateCache() {
|
|
211
|
+
try {
|
|
212
|
+
const raw = await readFile(CACHE_FILE, "utf8");
|
|
213
|
+
return JSON.parse(raw);
|
|
214
|
+
} catch {
|
|
215
|
+
return null;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
async function writeUpdateCache(data) {
|
|
219
|
+
await mkdir(CACHE_DIR, { recursive: true });
|
|
220
|
+
await writeFile(CACHE_FILE, JSON.stringify(data));
|
|
221
|
+
}
|
|
222
|
+
function detectPackageManager() {
|
|
223
|
+
if (process.env.BUN_INSTALL || process.env.npm_execpath?.includes("bun")) return "bun";
|
|
224
|
+
return "npm";
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* Check for updates and auto-install if a newer version is available.
|
|
228
|
+
* Uses a 1-hour TTL cache to avoid hitting the registry on every run.
|
|
229
|
+
* All errors are swallowed — network issues must never break the tool.
|
|
230
|
+
* Set AGENT_YES_NO_UPDATE=1 to opt out.
|
|
231
|
+
*/
|
|
232
|
+
async function checkAndAutoUpdate() {
|
|
233
|
+
if (process.env.AGENT_YES_NO_UPDATE) return;
|
|
234
|
+
try {
|
|
235
|
+
const cache = await readUpdateCache();
|
|
236
|
+
if (cache && Date.now() - cache.checkedAt < TTL_MS) {
|
|
237
|
+
if (compareVersions(version, cache.latestVersion) < 0) await runInstall(cache.latestVersion);
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
const latestVersion = await fetchLatestVersion();
|
|
241
|
+
if (!latestVersion) return;
|
|
242
|
+
await writeUpdateCache({
|
|
243
|
+
checkedAt: Date.now(),
|
|
244
|
+
latestVersion
|
|
245
|
+
});
|
|
246
|
+
if (compareVersions(version, latestVersion) < 0) await runInstall(latestVersion);
|
|
247
|
+
} catch {}
|
|
248
|
+
}
|
|
249
|
+
async function runInstall(latestVersion) {
|
|
250
|
+
const installArgs = detectPackageManager() === "bun" ? `bun add -g agent-yes@${latestVersion}` : `npm install -g agent-yes@${latestVersion}`;
|
|
251
|
+
process.stderr.write(`\x1b[33m[agent-yes] Updating ${version} → ${latestVersion}…\x1b[0m\n`);
|
|
252
|
+
try {
|
|
253
|
+
await execaCommand(installArgs, { stdio: "inherit" });
|
|
254
|
+
await writeUpdateCache({
|
|
255
|
+
checkedAt: 0,
|
|
256
|
+
latestVersion
|
|
257
|
+
});
|
|
258
|
+
process.stderr.write(`\x1b[32m[agent-yes] Updated to ${latestVersion}\x1b[0m\n`);
|
|
259
|
+
} catch {
|
|
260
|
+
process.stderr.write(`\x1b[31m[agent-yes] Auto-update failed. Run: ${installArgs}\x1b[0m\n`);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
205
263
|
/**
|
|
206
264
|
* Fetch the latest version of the package from npm registry
|
|
207
265
|
*/
|
|
@@ -415,37 +473,42 @@ function buildRustArgs(argv, cliFromScript, supportedClis) {
|
|
|
415
473
|
|
|
416
474
|
//#endregion
|
|
417
475
|
//#region ts/cli.ts
|
|
476
|
+
const updateCheckPromise = checkAndAutoUpdate();
|
|
418
477
|
const config = parseCliArgs(process.argv);
|
|
419
478
|
if (config.useRust) {
|
|
420
479
|
let rustBinary;
|
|
421
480
|
try {
|
|
422
481
|
rustBinary = await getRustBinary({ verbose: config.verbose });
|
|
423
482
|
} catch (err) {
|
|
424
|
-
|
|
425
|
-
|
|
483
|
+
if (config.verbose) {
|
|
484
|
+
console.error(`[rust] ${err instanceof Error ? err.message : String(err)}`);
|
|
485
|
+
console.error("[rust] Falling back to TypeScript implementation.");
|
|
486
|
+
}
|
|
426
487
|
}
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
488
|
+
if (rustBinary) {
|
|
489
|
+
const rustArgs = buildRustArgs(process.argv, config.cli, SUPPORTED_CLIS);
|
|
490
|
+
if (config.verbose) {
|
|
491
|
+
console.log(`[rust] Using binary: ${rustBinary}`);
|
|
492
|
+
console.log(`[rust] Args: ${rustArgs.join(" ")}`);
|
|
493
|
+
}
|
|
494
|
+
const child = spawn(rustBinary, rustArgs, {
|
|
495
|
+
stdio: "inherit",
|
|
496
|
+
env: process.env,
|
|
497
|
+
cwd: process.cwd()
|
|
498
|
+
});
|
|
499
|
+
child.on("error", (err) => {
|
|
500
|
+
if (err.code === "ENOENT") console.error(`Rust binary '${rustBinary}' not found. Try: npx agent-yes --rust --verbose`);
|
|
501
|
+
else console.error(`Failed to spawn Rust binary: ${err.message}`);
|
|
502
|
+
process.exit(1);
|
|
503
|
+
});
|
|
504
|
+
child.on("exit", (code, signal) => {
|
|
505
|
+
if (signal) process.exit(128 + (signal === "SIGINT" ? 2 : signal === "SIGTERM" ? 15 : 1));
|
|
506
|
+
process.exit(code ?? 1);
|
|
507
|
+
});
|
|
508
|
+
process.on("SIGINT", () => child.kill("SIGINT"));
|
|
509
|
+
process.on("SIGTERM", () => child.kill("SIGTERM"));
|
|
510
|
+
await new Promise(() => {});
|
|
431
511
|
}
|
|
432
|
-
const child = spawn(rustBinary, rustArgs, {
|
|
433
|
-
stdio: "inherit",
|
|
434
|
-
env: process.env,
|
|
435
|
-
cwd: process.cwd()
|
|
436
|
-
});
|
|
437
|
-
child.on("error", (err) => {
|
|
438
|
-
if (err.code === "ENOENT") console.error(`Rust binary '${rustBinary}' not found. Try: npx agent-yes --rust --verbose`);
|
|
439
|
-
else console.error(`Failed to spawn Rust binary: ${err.message}`);
|
|
440
|
-
process.exit(1);
|
|
441
|
-
});
|
|
442
|
-
child.on("exit", (code, signal) => {
|
|
443
|
-
if (signal) process.exit(128 + (signal === "SIGINT" ? 2 : signal === "SIGTERM" ? 15 : 1));
|
|
444
|
-
process.exit(code ?? 1);
|
|
445
|
-
});
|
|
446
|
-
process.on("SIGINT", () => child.kill("SIGINT"));
|
|
447
|
-
process.on("SIGTERM", () => child.kill("SIGTERM"));
|
|
448
|
-
await new Promise(() => {});
|
|
449
512
|
}
|
|
450
513
|
if (config.showVersion) {
|
|
451
514
|
await displayVersion();
|
|
@@ -504,6 +567,7 @@ const { exitCode } = await cliYes({
|
|
|
504
567
|
...config,
|
|
505
568
|
autoYes: config.autoYes
|
|
506
569
|
});
|
|
570
|
+
await updateCheckPromise;
|
|
507
571
|
console.log("exiting process");
|
|
508
572
|
process.exit(exitCode ?? 1);
|
|
509
573
|
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as AgentContext, i as config, l as removeControlCharacters, n as CLIS_CONFIG, r as agentYes } from "./SUPPORTED_CLIS-
|
|
1
|
+
import { a as AgentContext, i as config, l as removeControlCharacters, n as CLIS_CONFIG, r as agentYes } from "./SUPPORTED_CLIS-Dxapc1qa.js";
|
|
2
2
|
import "./logger-CX77vJDA.js";
|
|
3
3
|
|
|
4
4
|
export { AgentContext, CLIS_CONFIG, config, agentYes as default, removeControlCharacters };
|
package/package.json
CHANGED
package/ts/cli.ts
CHANGED
|
@@ -5,62 +5,65 @@ import { parseCliArgs } from "./parseCliArgs.ts";
|
|
|
5
5
|
import { SUPPORTED_CLIS } from "./SUPPORTED_CLIS.ts";
|
|
6
6
|
import { logger } from "./logger.ts";
|
|
7
7
|
import { PidStore } from "./pidStore.ts";
|
|
8
|
-
import { displayVersion } from "./versionChecker.ts";
|
|
8
|
+
import { checkAndAutoUpdate, displayVersion } from "./versionChecker.ts";
|
|
9
9
|
import { getRustBinary } from "./rustBinary.ts";
|
|
10
10
|
import { buildRustArgs } from "./buildRustArgs.ts";
|
|
11
11
|
|
|
12
|
+
// Start update check in background immediately (runs in parallel with the agent session)
|
|
13
|
+
const updateCheckPromise = checkAndAutoUpdate();
|
|
14
|
+
|
|
12
15
|
// Parse CLI arguments
|
|
13
16
|
const config = parseCliArgs(process.argv);
|
|
14
17
|
|
|
15
|
-
// Handle --rust: spawn the Rust binary instead
|
|
18
|
+
// Handle --rust: spawn the Rust binary instead, fall back to TypeScript if unavailable
|
|
16
19
|
if (config.useRust) {
|
|
17
|
-
let rustBinary: string;
|
|
20
|
+
let rustBinary: string | undefined;
|
|
18
21
|
|
|
19
22
|
try {
|
|
20
|
-
// Get or download the Rust binary for the current platform
|
|
21
23
|
rustBinary = await getRustBinary({ verbose: config.verbose });
|
|
22
24
|
} catch (err) {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
const rustArgs = buildRustArgs(process.argv, config.cli, SUPPORTED_CLIS);
|
|
29
|
-
|
|
30
|
-
if (config.verbose) {
|
|
31
|
-
console.log(`[rust] Using binary: ${rustBinary}`);
|
|
32
|
-
console.log(`[rust] Args: ${rustArgs.join(" ")}`);
|
|
25
|
+
// Rust binary unavailable (not yet released for this version, or network issue) — fall back to TypeScript
|
|
26
|
+
if (config.verbose) {
|
|
27
|
+
console.error(`[rust] ${err instanceof Error ? err.message : String(err)}`);
|
|
28
|
+
console.error("[rust] Falling back to TypeScript implementation.");
|
|
29
|
+
}
|
|
33
30
|
}
|
|
34
31
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
stdio: "inherit",
|
|
38
|
-
env: process.env,
|
|
39
|
-
cwd: process.cwd(),
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
child.on("error", (err) => {
|
|
43
|
-
if ((err as NodeJS.ErrnoException).code === "ENOENT") {
|
|
44
|
-
console.error(`Rust binary '${rustBinary}' not found. Try: npx agent-yes --rust --verbose`);
|
|
45
|
-
} else {
|
|
46
|
-
console.error(`Failed to spawn Rust binary: ${err.message}`);
|
|
47
|
-
}
|
|
48
|
-
process.exit(1);
|
|
49
|
-
});
|
|
32
|
+
if (rustBinary) {
|
|
33
|
+
const rustArgs = buildRustArgs(process.argv, config.cli, SUPPORTED_CLIS);
|
|
50
34
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
35
|
+
if (config.verbose) {
|
|
36
|
+
console.log(`[rust] Using binary: ${rustBinary}`);
|
|
37
|
+
console.log(`[rust] Args: ${rustArgs.join(" ")}`);
|
|
54
38
|
}
|
|
55
|
-
process.exit(code ?? 1);
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
// Forward signals to child
|
|
59
|
-
process.on("SIGINT", () => child.kill("SIGINT"));
|
|
60
|
-
process.on("SIGTERM", () => child.kill("SIGTERM"));
|
|
61
39
|
|
|
62
|
-
|
|
63
|
-
|
|
40
|
+
const child = spawn(rustBinary, rustArgs, {
|
|
41
|
+
stdio: "inherit",
|
|
42
|
+
env: process.env,
|
|
43
|
+
cwd: process.cwd(),
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
child.on("error", (err) => {
|
|
47
|
+
if ((err as NodeJS.ErrnoException).code === "ENOENT") {
|
|
48
|
+
console.error(`Rust binary '${rustBinary}' not found. Try: npx agent-yes --rust --verbose`);
|
|
49
|
+
} else {
|
|
50
|
+
console.error(`Failed to spawn Rust binary: ${err.message}`);
|
|
51
|
+
}
|
|
52
|
+
process.exit(1);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
child.on("exit", (code, signal) => {
|
|
56
|
+
if (signal) {
|
|
57
|
+
process.exit(128 + (signal === "SIGINT" ? 2 : signal === "SIGTERM" ? 15 : 1));
|
|
58
|
+
}
|
|
59
|
+
process.exit(code ?? 1);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
process.on("SIGINT", () => child.kill("SIGINT"));
|
|
63
|
+
process.on("SIGTERM", () => child.kill("SIGTERM"));
|
|
64
|
+
|
|
65
|
+
await new Promise(() => {}); // Never resolves, exits via child.on("exit")
|
|
66
|
+
}
|
|
64
67
|
}
|
|
65
68
|
|
|
66
69
|
// Handle --version: display version and exit
|
|
@@ -134,5 +137,9 @@ if (config.verbose) {
|
|
|
134
137
|
|
|
135
138
|
const { default: cliYes } = await import("./index.ts");
|
|
136
139
|
const { exitCode } = await cliYes({ ...config, autoYes: config.autoYes });
|
|
140
|
+
|
|
141
|
+
// Apply update if one was found during the session
|
|
142
|
+
await updateCheckPromise;
|
|
143
|
+
|
|
137
144
|
console.log("exiting process");
|
|
138
145
|
process.exit(exitCode ?? 1);
|
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
import { describe, expect, it, vi, beforeEach, afterEach } from "vitest";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
checkAndAutoUpdate,
|
|
4
|
+
compareVersions,
|
|
5
|
+
fetchLatestVersion,
|
|
6
|
+
displayVersion,
|
|
7
|
+
} from "./versionChecker";
|
|
8
|
+
|
|
9
|
+
vi.mock("execa", () => ({ execaCommand: vi.fn().mockResolvedValue({}) }));
|
|
10
|
+
vi.mock("fs/promises", () => ({
|
|
11
|
+
mkdir: vi.fn().mockResolvedValue(undefined),
|
|
12
|
+
readFile: vi.fn(),
|
|
13
|
+
writeFile: vi.fn().mockResolvedValue(undefined),
|
|
14
|
+
}));
|
|
3
15
|
|
|
4
16
|
describe("versionChecker", () => {
|
|
5
17
|
describe("compareVersions", () => {
|
|
@@ -64,6 +76,107 @@ describe("versionChecker", () => {
|
|
|
64
76
|
});
|
|
65
77
|
});
|
|
66
78
|
|
|
79
|
+
describe("checkAndAutoUpdate", () => {
|
|
80
|
+
beforeEach(() => {
|
|
81
|
+
vi.clearAllMocks();
|
|
82
|
+
vi.stubGlobal("fetch", vi.fn());
|
|
83
|
+
vi.spyOn(process.stderr, "write").mockImplementation(() => true);
|
|
84
|
+
delete process.env.AGENT_YES_NO_UPDATE;
|
|
85
|
+
delete process.env.BUN_INSTALL;
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
afterEach(() => {
|
|
89
|
+
vi.restoreAllMocks();
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("should skip when AGENT_YES_NO_UPDATE is set", async () => {
|
|
93
|
+
process.env.AGENT_YES_NO_UPDATE = "1";
|
|
94
|
+
await checkAndAutoUpdate();
|
|
95
|
+
expect(fetch).not.toHaveBeenCalled();
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("should use cached result within TTL and not install when up-to-date", async () => {
|
|
99
|
+
const { readFile } = await import("fs/promises");
|
|
100
|
+
vi.mocked(readFile).mockResolvedValueOnce(
|
|
101
|
+
JSON.stringify({ checkedAt: Date.now(), latestVersion: "0.0.1" }) as any,
|
|
102
|
+
);
|
|
103
|
+
await checkAndAutoUpdate();
|
|
104
|
+
expect(fetch).not.toHaveBeenCalled();
|
|
105
|
+
expect(process.stderr.write).not.toHaveBeenCalled();
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("should install from cache when cached version is newer and within TTL", async () => {
|
|
109
|
+
const { readFile } = await import("fs/promises");
|
|
110
|
+
const { execaCommand } = await import("execa");
|
|
111
|
+
vi.mocked(readFile).mockResolvedValueOnce(
|
|
112
|
+
JSON.stringify({ checkedAt: Date.now(), latestVersion: "999.0.0" }) as any,
|
|
113
|
+
);
|
|
114
|
+
await checkAndAutoUpdate();
|
|
115
|
+
expect(execaCommand).toHaveBeenCalled();
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it("should fetch and write cache when stale, install if behind", async () => {
|
|
119
|
+
const { readFile, writeFile } = await import("fs/promises");
|
|
120
|
+
const { execaCommand } = await import("execa");
|
|
121
|
+
vi.mocked(readFile).mockRejectedValueOnce(new Error("no cache"));
|
|
122
|
+
vi.mocked(fetch).mockResolvedValue({
|
|
123
|
+
ok: true,
|
|
124
|
+
json: async () => ({ version: "999.0.0" }),
|
|
125
|
+
} as Response);
|
|
126
|
+
await checkAndAutoUpdate();
|
|
127
|
+
expect(writeFile).toHaveBeenCalled();
|
|
128
|
+
expect(execaCommand).toHaveBeenCalled();
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it("should fetch and write cache but not install if up-to-date", async () => {
|
|
132
|
+
const { readFile, writeFile } = await import("fs/promises");
|
|
133
|
+
const { execaCommand } = await import("execa");
|
|
134
|
+
vi.mocked(readFile).mockRejectedValueOnce(new Error("no cache"));
|
|
135
|
+
vi.mocked(fetch).mockResolvedValue({
|
|
136
|
+
ok: true,
|
|
137
|
+
json: async () => ({ version: "0.0.1" }),
|
|
138
|
+
} as Response);
|
|
139
|
+
await checkAndAutoUpdate();
|
|
140
|
+
expect(writeFile).toHaveBeenCalled();
|
|
141
|
+
expect(execaCommand).not.toHaveBeenCalled();
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it("should silently handle fetch failure", async () => {
|
|
145
|
+
const { readFile } = await import("fs/promises");
|
|
146
|
+
vi.mocked(readFile).mockRejectedValueOnce(new Error("no cache"));
|
|
147
|
+
vi.mocked(fetch).mockRejectedValue(new Error("network error"));
|
|
148
|
+
await expect(checkAndAutoUpdate()).resolves.toBeUndefined();
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it("should use bun when BUN_INSTALL is set", async () => {
|
|
152
|
+
process.env.BUN_INSTALL = "/home/user/.bun";
|
|
153
|
+
const { readFile } = await import("fs/promises");
|
|
154
|
+
const { execaCommand } = await import("execa");
|
|
155
|
+
vi.mocked(readFile).mockRejectedValueOnce(new Error("no cache"));
|
|
156
|
+
vi.mocked(fetch).mockResolvedValue({
|
|
157
|
+
ok: true,
|
|
158
|
+
json: async () => ({ version: "999.0.0" }),
|
|
159
|
+
} as Response);
|
|
160
|
+
await checkAndAutoUpdate();
|
|
161
|
+
expect(vi.mocked(execaCommand).mock.calls[0]?.[0]).toContain("bun");
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it("should print error and not throw when install fails", async () => {
|
|
165
|
+
const { readFile } = await import("fs/promises");
|
|
166
|
+
const { execaCommand } = await import("execa");
|
|
167
|
+
vi.mocked(readFile).mockRejectedValueOnce(new Error("no cache"));
|
|
168
|
+
vi.mocked(fetch).mockResolvedValue({
|
|
169
|
+
ok: true,
|
|
170
|
+
json: async () => ({ version: "999.0.0" }),
|
|
171
|
+
} as Response);
|
|
172
|
+
vi.mocked(execaCommand).mockRejectedValueOnce(new Error("install failed"));
|
|
173
|
+
await expect(checkAndAutoUpdate()).resolves.toBeUndefined();
|
|
174
|
+
expect(process.stderr.write).toHaveBeenCalledWith(
|
|
175
|
+
expect.stringContaining("Auto-update failed"),
|
|
176
|
+
);
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
|
|
67
180
|
describe("displayVersion", () => {
|
|
68
181
|
beforeEach(() => {
|
|
69
182
|
vi.stubGlobal("fetch", vi.fn());
|
package/ts/versionChecker.ts
CHANGED
|
@@ -1,5 +1,86 @@
|
|
|
1
|
+
import { execaCommand } from "execa";
|
|
2
|
+
import { mkdir, readFile, writeFile } from "fs/promises";
|
|
3
|
+
import { homedir } from "os";
|
|
4
|
+
import path from "path";
|
|
1
5
|
import pkg from "../package.json" with { type: "json" };
|
|
2
6
|
|
|
7
|
+
const CACHE_DIR = path.join(homedir(), ".cache", "agent-yes");
|
|
8
|
+
const CACHE_FILE = path.join(CACHE_DIR, "update-check.json");
|
|
9
|
+
const TTL_MS = 60 * 60 * 1000; // 1 hour
|
|
10
|
+
|
|
11
|
+
type UpdateCache = { checkedAt: number; latestVersion: string };
|
|
12
|
+
|
|
13
|
+
async function readUpdateCache(): Promise<UpdateCache | null> {
|
|
14
|
+
try {
|
|
15
|
+
const raw = await readFile(CACHE_FILE, "utf8");
|
|
16
|
+
return JSON.parse(raw) as UpdateCache;
|
|
17
|
+
} catch {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async function writeUpdateCache(data: UpdateCache): Promise<void> {
|
|
23
|
+
await mkdir(CACHE_DIR, { recursive: true });
|
|
24
|
+
await writeFile(CACHE_FILE, JSON.stringify(data));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function detectPackageManager(): string {
|
|
28
|
+
if (process.env.BUN_INSTALL || process.env.npm_execpath?.includes("bun")) return "bun";
|
|
29
|
+
return "npm";
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Check for updates and auto-install if a newer version is available.
|
|
34
|
+
* Uses a 1-hour TTL cache to avoid hitting the registry on every run.
|
|
35
|
+
* All errors are swallowed — network issues must never break the tool.
|
|
36
|
+
* Set AGENT_YES_NO_UPDATE=1 to opt out.
|
|
37
|
+
*/
|
|
38
|
+
export async function checkAndAutoUpdate(): Promise<void> {
|
|
39
|
+
if (process.env.AGENT_YES_NO_UPDATE) return;
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
// Check cache TTL
|
|
43
|
+
const cache = await readUpdateCache();
|
|
44
|
+
if (cache && Date.now() - cache.checkedAt < TTL_MS) {
|
|
45
|
+
// Use cached result
|
|
46
|
+
if (compareVersions(pkg.version, cache.latestVersion) < 0) {
|
|
47
|
+
await runInstall(cache.latestVersion);
|
|
48
|
+
}
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Fetch latest from registry
|
|
53
|
+
const latestVersion = await fetchLatestVersion();
|
|
54
|
+
if (!latestVersion) return;
|
|
55
|
+
|
|
56
|
+
await writeUpdateCache({ checkedAt: Date.now(), latestVersion });
|
|
57
|
+
|
|
58
|
+
if (compareVersions(pkg.version, latestVersion) < 0) {
|
|
59
|
+
await runInstall(latestVersion);
|
|
60
|
+
}
|
|
61
|
+
} catch {
|
|
62
|
+
// Silently ignore all errors
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async function runInstall(latestVersion: string): Promise<void> {
|
|
67
|
+
const pm = detectPackageManager();
|
|
68
|
+
const installArgs =
|
|
69
|
+
pm === "bun"
|
|
70
|
+
? `bun add -g agent-yes@${latestVersion}`
|
|
71
|
+
: `npm install -g agent-yes@${latestVersion}`;
|
|
72
|
+
|
|
73
|
+
process.stderr.write(`\x1b[33m[agent-yes] Updating ${pkg.version} → ${latestVersion}…\x1b[0m\n`);
|
|
74
|
+
try {
|
|
75
|
+
await execaCommand(installArgs, { stdio: "inherit" });
|
|
76
|
+
// Clear cache so next run re-checks
|
|
77
|
+
await writeUpdateCache({ checkedAt: 0, latestVersion });
|
|
78
|
+
process.stderr.write(`\x1b[32m[agent-yes] Updated to ${latestVersion}\x1b[0m\n`);
|
|
79
|
+
} catch {
|
|
80
|
+
process.stderr.write(`\x1b[31m[agent-yes] Auto-update failed. Run: ${installArgs}\x1b[0m\n`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
3
84
|
/**
|
|
4
85
|
* Fetch the latest version of the package from npm registry
|
|
5
86
|
*/
|