agent-yes 1.66.0 → 1.68.0
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-DbTReaSd.js → SUPPORTED_CLIS-Cl2oCgKo.js} +22 -12
- package/dist/cli.js +65 -3
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/ts/cli.ts +8 -1
- package/ts/index.ts +21 -10
- package/ts/parseCliArgs.ts +2 -0
- 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.0";
|
|
1063
1063
|
|
|
1064
1064
|
//#endregion
|
|
1065
1065
|
//#region ts/pty-fix.ts
|
|
@@ -1516,7 +1516,7 @@ const CLIS_CONFIG = config.clis;
|
|
|
1516
1516
|
* });
|
|
1517
1517
|
* ```
|
|
1518
1518
|
*/
|
|
1519
|
-
async function agentYes({ cli, cliArgs = [], prompt, robust = true, cwd, env, exitOnIdle, logFile, removeControlCharactersFromStdout = false, verbose = false, queue = false, install = false, resume = false, useSkills = false, useStdinAppend = false, autoYes = true }) {
|
|
1519
|
+
async function agentYes({ cli, cliArgs = [], prompt, robust = true, cwd, env, exitOnIdle, logFile, removeControlCharactersFromStdout = false, verbose = false, queue = false, install = false, resume = false, useSkills = false, useStdinAppend = false, autoYes = true, idleAction }) {
|
|
1520
1520
|
if (!cli) throw new Error(`cli is required`);
|
|
1521
1521
|
const conf = CLIS_CONFIG[cli] || DIE(`Unsupported cli tool: ${cli}, current process.argv: ${process.argv.join(" ")}`);
|
|
1522
1522
|
const workingDir = cwd ?? process.cwd();
|
|
@@ -1908,16 +1908,26 @@ async function agentYes({ cli, cliArgs = [], prompt, robust = true, cwd, env, ex
|
|
|
1908
1908
|
}, 800);
|
|
1909
1909
|
const cleanupHeartbeat = () => clearInterval(heartbeatInterval);
|
|
1910
1910
|
shell.onExit(cleanupHeartbeat);
|
|
1911
|
-
if (exitOnIdle)
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1911
|
+
if (exitOnIdle) (async () => {
|
|
1912
|
+
while (true) {
|
|
1913
|
+
await ctx.idleWaiter.wait(exitOnIdle);
|
|
1914
|
+
await pidStore.updateStatus(shell.pid, "idle").catch(() => null);
|
|
1915
|
+
if (isStillWorkingQ()) {
|
|
1916
|
+
logger.warn(`[${cli}-yes] ${cli} is idle, but seems still working, not exiting yet`);
|
|
1917
|
+
continue;
|
|
1918
|
+
}
|
|
1919
|
+
if (idleAction) {
|
|
1920
|
+
logger.info(`[${cli}-yes] ${cli} is idle, performing idle action: ${idleAction}`);
|
|
1921
|
+
notifyWebhook("IDLE", `action=${idleAction}`, workingDir).catch(() => null);
|
|
1922
|
+
await sendMessage(ctx.messageContext, idleAction);
|
|
1923
|
+
continue;
|
|
1924
|
+
}
|
|
1925
|
+
logger.info(`[${cli}-yes] ${cli} is idle, exiting...`);
|
|
1926
|
+
notifyWebhook("IDLE", "", workingDir).catch(() => null);
|
|
1927
|
+
await exitAgent();
|
|
1928
|
+
break;
|
|
1916
1929
|
}
|
|
1917
|
-
|
|
1918
|
-
notifyWebhook("IDLE", "", workingDir).catch(() => null);
|
|
1919
|
-
await exitAgent();
|
|
1920
|
-
});
|
|
1930
|
+
})();
|
|
1921
1931
|
const stdinStream = new ReadableStream({
|
|
1922
1932
|
start(controller) {
|
|
1923
1933
|
process.stdin.resume();
|
|
@@ -2129,4 +2139,4 @@ const SUPPORTED_CLIS = Object.keys(CLIS_CONFIG);
|
|
|
2129
2139
|
|
|
2130
2140
|
//#endregion
|
|
2131
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 };
|
|
2132
|
-
//# sourceMappingURL=SUPPORTED_CLIS-
|
|
2142
|
+
//# sourceMappingURL=SUPPORTED_CLIS-Cl2oCgKo.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-Cl2oCgKo.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
|
|
@@ -56,7 +58,8 @@ function parseCliArgs(argv) {
|
|
|
56
58
|
alias: "i"
|
|
57
59
|
}).option("idle-action", {
|
|
58
60
|
type: "string",
|
|
59
|
-
description: "Idle action to perform when idle time is reached, e.g., \"/exit\" or \"check TODO.md\""
|
|
61
|
+
description: "Idle action to perform when idle time is reached, e.g., \"/exit\" or \"check TODO.md\"",
|
|
62
|
+
alias: "ia"
|
|
60
63
|
}).option("queue", {
|
|
61
64
|
type: "boolean",
|
|
62
65
|
description: "Queue Agent Commands when spawning multiple agents in the same directory/repo, can be disabled with --no-queue",
|
|
@@ -189,6 +192,7 @@ function parseCliArgs(argv) {
|
|
|
189
192
|
useStdinAppend: Boolean(parsedArgv.stdpush || parsedArgv.ipc || parsedArgv.fifo),
|
|
190
193
|
showVersion: parsedArgv.version,
|
|
191
194
|
autoYes: parsedArgv.auto !== "no",
|
|
195
|
+
idleAction: parsedArgv.idleAction,
|
|
192
196
|
useRust: parsedArgv.rust,
|
|
193
197
|
swarm: parsedArgv.swarm ?? (parsedArgv.experimentalSwarm ? parsedArgv.swarmTopic : void 0),
|
|
194
198
|
experimentalSwarm: parsedArgv.experimentalSwarm,
|
|
@@ -200,6 +204,62 @@ function parseCliArgs(argv) {
|
|
|
200
204
|
|
|
201
205
|
//#endregion
|
|
202
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
|
+
}
|
|
203
263
|
/**
|
|
204
264
|
* Fetch the latest version of the package from npm registry
|
|
205
265
|
*/
|
|
@@ -413,6 +473,7 @@ function buildRustArgs(argv, cliFromScript, supportedClis) {
|
|
|
413
473
|
|
|
414
474
|
//#endregion
|
|
415
475
|
//#region ts/cli.ts
|
|
476
|
+
const updateCheckPromise = checkAndAutoUpdate();
|
|
416
477
|
const config = parseCliArgs(process.argv);
|
|
417
478
|
if (config.useRust) {
|
|
418
479
|
let rustBinary;
|
|
@@ -502,6 +563,7 @@ const { exitCode } = await cliYes({
|
|
|
502
563
|
...config,
|
|
503
564
|
autoYes: config.autoYes
|
|
504
565
|
});
|
|
566
|
+
await updateCheckPromise;
|
|
505
567
|
console.log("exiting process");
|
|
506
568
|
process.exit(exitCode ?? 1);
|
|
507
569
|
|
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-Cl2oCgKo.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,10 +5,13 @@ 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
|
|
|
@@ -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);
|
package/ts/index.ts
CHANGED
|
@@ -119,6 +119,7 @@ export default async function agentYes({
|
|
|
119
119
|
useSkills = false,
|
|
120
120
|
useStdinAppend = false,
|
|
121
121
|
autoYes = true,
|
|
122
|
+
idleAction,
|
|
122
123
|
}: {
|
|
123
124
|
cli: SUPPORTED_CLIS;
|
|
124
125
|
cliArgs?: string[];
|
|
@@ -136,6 +137,7 @@ export default async function agentYes({
|
|
|
136
137
|
useSkills?: boolean; // if true, prepend SKILL.md header to the prompt for non-Claude agents
|
|
137
138
|
useStdinAppend?: boolean; // if true, enable FIFO input stream on Linux, for additional stdin input
|
|
138
139
|
autoYes?: boolean; // if true, auto-yes is enabled (default), toggle with Ctrl+Y during session
|
|
140
|
+
idleAction?: string; // if set, type this message when idle instead of exiting
|
|
139
141
|
}) {
|
|
140
142
|
if (!cli) throw new Error(`cli is required`);
|
|
141
143
|
const conf =
|
|
@@ -689,17 +691,26 @@ export default async function agentYes({
|
|
|
689
691
|
shell.onExit(cleanupHeartbeat);
|
|
690
692
|
|
|
691
693
|
if (exitOnIdle)
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
694
|
+
(async () => {
|
|
695
|
+
while (true) {
|
|
696
|
+
await ctx.idleWaiter.wait(exitOnIdle);
|
|
697
|
+
await pidStore.updateStatus(shell.pid, "idle").catch(() => null);
|
|
698
|
+
if (isStillWorkingQ()) {
|
|
699
|
+
logger.warn(`[${cli}-yes] ${cli} is idle, but seems still working, not exiting yet`);
|
|
700
|
+
continue;
|
|
701
|
+
}
|
|
702
|
+
if (idleAction) {
|
|
703
|
+
logger.info(`[${cli}-yes] ${cli} is idle, performing idle action: ${idleAction}`);
|
|
704
|
+
notifyWebhook("IDLE", `action=${idleAction}`, workingDir).catch(() => null);
|
|
705
|
+
await sendMessage(ctx.messageContext, idleAction);
|
|
706
|
+
continue;
|
|
707
|
+
}
|
|
708
|
+
logger.info(`[${cli}-yes] ${cli} is idle, exiting...`);
|
|
709
|
+
notifyWebhook("IDLE", "", workingDir).catch(() => null);
|
|
710
|
+
await exitAgent();
|
|
711
|
+
break;
|
|
697
712
|
}
|
|
698
|
-
|
|
699
|
-
logger.info(`[${cli}-yes] ${cli} is idle, exiting...`);
|
|
700
|
-
notifyWebhook("IDLE", "", workingDir).catch(() => null);
|
|
701
|
-
await exitAgent();
|
|
702
|
-
});
|
|
713
|
+
})();
|
|
703
714
|
|
|
704
715
|
// Message streaming
|
|
705
716
|
|
package/ts/parseCliArgs.ts
CHANGED
|
@@ -83,6 +83,7 @@ export function parseCliArgs(argv: string[]) {
|
|
|
83
83
|
type: "string",
|
|
84
84
|
description:
|
|
85
85
|
'Idle action to perform when idle time is reached, e.g., "/exit" or "check TODO.md"',
|
|
86
|
+
alias: "ia",
|
|
86
87
|
})
|
|
87
88
|
.option("queue", {
|
|
88
89
|
type: "boolean",
|
|
@@ -273,6 +274,7 @@ export function parseCliArgs(argv: string[]) {
|
|
|
273
274
|
useStdinAppend: Boolean(parsedArgv.stdpush || parsedArgv.ipc || parsedArgv.fifo), // Support --stdpush, --ipc, and --fifo (backward compatibility)
|
|
274
275
|
showVersion: parsedArgv.version,
|
|
275
276
|
autoYes: parsedArgv.auto !== "no", // auto-yes enabled by default, disabled with --auto=no
|
|
277
|
+
idleAction: parsedArgv.idleAction as string | undefined,
|
|
276
278
|
useRust: parsedArgv.rust,
|
|
277
279
|
// New unified --swarm flag (takes precedence over deprecated flags)
|
|
278
280
|
swarm: parsedArgv.swarm ?? (parsedArgv.experimentalSwarm ? parsedArgv.swarmTopic : undefined),
|
|
@@ -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
|
*/
|