agent-yes 1.72.4 → 1.73.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-Bqw9gxey.js → SUPPORTED_CLIS-DgHs-Q6i.js} +128 -27
- package/dist/cli.js +131 -9
- package/dist/index.js +1 -1
- package/package.json +8 -3
- package/ts/cli.ts +3 -1
- package/ts/index.ts +86 -99
- package/ts/parseCliArgs.spec.ts +88 -0
- package/ts/parseCliArgs.ts +45 -6
- package/ts/rustBinary.ts +68 -0
- package/ts/versionChecker.spec.ts +48 -0
- package/ts/versionChecker.ts +66 -9
- package/ts/xterm-proxy.ts +130 -0
package/ts/index.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { mkdir, readFile, writeFile } from "fs/promises";
|
|
|
4
4
|
import path from "path";
|
|
5
5
|
import DIE from "phpdie";
|
|
6
6
|
import sflow from "sflow";
|
|
7
|
-
import {
|
|
7
|
+
import { XtermProxy } from "./xterm-proxy.ts";
|
|
8
8
|
import {
|
|
9
9
|
extractSessionId,
|
|
10
10
|
getSessionForCwd,
|
|
@@ -185,9 +185,14 @@ export default async function agentYes({
|
|
|
185
185
|
process.stdin.setRawMode?.(true); // must be called any stdout/stdin usage
|
|
186
186
|
if (verbose) logger.debug(`[stdin] Raw mode set, isRaw: ${(process.stdin as any).isRaw}`);
|
|
187
187
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
188
|
+
// XtermProxy: headless xterm emulator that auto-responds to all terminal
|
|
189
|
+
// queries (DSR, DA, etc.) so the spawned CLI never blocks waiting for replies.
|
|
190
|
+
// writeToPty is set after shell spawn (see below).
|
|
191
|
+
let shellWrite: (data: string) => void = () => {};
|
|
192
|
+
const xtermProxy = new XtermProxy({
|
|
193
|
+
...getTerminalDimensions(),
|
|
194
|
+
writeToPty: (data) => shellWrite(data),
|
|
195
|
+
});
|
|
191
196
|
|
|
192
197
|
logger.debug(`Using ${ptyPackage} for pseudo terminal management.`);
|
|
193
198
|
|
|
@@ -329,11 +334,14 @@ export default async function agentYes({
|
|
|
329
334
|
ptyOptions,
|
|
330
335
|
});
|
|
331
336
|
|
|
337
|
+
// Wire up the xterm proxy to write back to the PTY
|
|
338
|
+
shellWrite = (data: string) => shell.write(data);
|
|
339
|
+
|
|
332
340
|
// Attach data handler IMMEDIATELY after spawn to avoid losing early PTY output.
|
|
333
341
|
// node-pty emits 'data' events eagerly — if no listener is attached, events are lost.
|
|
334
342
|
function onData(data: string) {
|
|
335
343
|
const currentPid = shell.pid;
|
|
336
|
-
|
|
344
|
+
xtermProxy.write(data);
|
|
337
345
|
globalAgentRegistry.appendStdout(currentPid, data);
|
|
338
346
|
}
|
|
339
347
|
shell.onData(onData);
|
|
@@ -447,6 +455,7 @@ export default async function agentYes({
|
|
|
447
455
|
env: ptyEnv,
|
|
448
456
|
};
|
|
449
457
|
shell = pty.spawn(bin!, args, restartPtyOptions);
|
|
458
|
+
shellWrite = (data: string) => shell.write(data);
|
|
450
459
|
// Register process in pidStore (non-blocking)
|
|
451
460
|
try {
|
|
452
461
|
await pidStore.registerProcess({ pid: shell.pid, cli, args, prompt, cwd: workingDir });
|
|
@@ -548,6 +557,7 @@ export default async function agentYes({
|
|
|
548
557
|
env: ptyEnv,
|
|
549
558
|
};
|
|
550
559
|
shell = pty.spawn(cli, restoreArgs, restorePtyOptions);
|
|
560
|
+
shellWrite = (data: string) => shell.write(data);
|
|
551
561
|
// Register process in pidStore (non-blocking)
|
|
552
562
|
try {
|
|
553
563
|
await pidStore.registerProcess({
|
|
@@ -601,14 +611,15 @@ export default async function agentYes({
|
|
|
601
611
|
return pendingExitCode.resolve(exitCode);
|
|
602
612
|
});
|
|
603
613
|
|
|
604
|
-
// when current tty resized, resize
|
|
614
|
+
// when current tty resized, resize both pty and xterm proxy
|
|
605
615
|
process.stdout.on("resize", () => {
|
|
606
616
|
const { cols, rows } = getTerminalDimensions();
|
|
607
617
|
shell.resize(cols, rows);
|
|
618
|
+
xtermProxy.resize(cols, rows);
|
|
608
619
|
});
|
|
609
620
|
|
|
610
621
|
const isStillWorkingQ = () => {
|
|
611
|
-
const rendered =
|
|
622
|
+
const rendered = xtermProxy.tail(24).replace(/\s+/g, " ");
|
|
612
623
|
return conf.working?.some((rgx) => rgx.test(rendered));
|
|
613
624
|
};
|
|
614
625
|
|
|
@@ -617,7 +628,7 @@ export default async function agentYes({
|
|
|
617
628
|
let lastHeartbeatRendered = "";
|
|
618
629
|
const heartbeatInterval = setInterval(async () => {
|
|
619
630
|
try {
|
|
620
|
-
const rendered = removeControlCharacters(
|
|
631
|
+
const rendered = removeControlCharacters(xtermProxy.tail(12));
|
|
621
632
|
|
|
622
633
|
// Skip if output hasn't changed since last heartbeat
|
|
623
634
|
if (rendered === lastHeartbeatRendered) return;
|
|
@@ -895,7 +906,7 @@ export default async function agentYes({
|
|
|
895
906
|
shell.write(data);
|
|
896
907
|
},
|
|
897
908
|
}),
|
|
898
|
-
readable:
|
|
909
|
+
readable: xtermProxy.readable,
|
|
899
910
|
})
|
|
900
911
|
|
|
901
912
|
.forEach(() => {
|
|
@@ -926,97 +937,72 @@ export default async function agentYes({
|
|
|
926
937
|
// if (cli === "codex" && !process.stdin.isTTY) shell.write(`\u001b[1;1R`); // send cursor position response when stdin is not tty
|
|
927
938
|
|
|
928
939
|
let lastRendered = "";
|
|
929
|
-
return
|
|
930
|
-
|
|
931
|
-
//
|
|
932
|
-
//
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
940
|
+
return (
|
|
941
|
+
e
|
|
942
|
+
// Terminal query responses (DA, DSR, etc.) are handled automatically
|
|
943
|
+
// by XtermProxy via @xterm/headless — no ad-hoc interception needed.
|
|
944
|
+
.forEach(async (line, lineIndex) => {
|
|
945
|
+
// ============ respond on rendered screen
|
|
946
|
+
const rendered = xtermProxy.tail(24);
|
|
947
|
+
// Skip processing if output hasn't changed
|
|
948
|
+
if (rendered === lastRendered) return;
|
|
949
|
+
lastRendered = rendered;
|
|
950
|
+
|
|
951
|
+
logger.debug(`stdout|${line}`);
|
|
952
|
+
|
|
953
|
+
// ready matcher: if matched, mark stdin ready
|
|
954
|
+
if (conf.ready?.some((rx: RegExp) => line.match(rx))) {
|
|
955
|
+
logger.debug(`ready |${line}`);
|
|
956
|
+
if (cli === "gemini" && lineIndex <= 80) return; // gemini initial noise, only after many lines
|
|
957
|
+
ctx.stdinReady.ready();
|
|
958
|
+
ctx.stdinFirstReady.ready();
|
|
943
959
|
}
|
|
944
|
-
return;
|
|
945
|
-
}
|
|
946
|
-
|
|
947
|
-
// Only handle cursor position when stdin is not tty, because tty already handled this
|
|
948
|
-
if (process.stdin.isTTY) return;
|
|
949
|
-
|
|
950
|
-
// Handle cursor position request - ESC[6n
|
|
951
|
-
if (!chunk.includes("\u001b[6n")) return;
|
|
952
|
-
|
|
953
|
-
// xterm replies CSI row; column R if asked cursor position
|
|
954
|
-
// https://en.wikipedia.org/wiki/ANSI_escape_code#:~:text=citation%20needed%5D-,xterm%20replies,-CSI%20row%C2%A0%3B
|
|
955
|
-
const { col, row } = terminalRender.getCursorPosition();
|
|
956
|
-
shell.write(`\u001b[${row};${col}R`); // reply cli when getting cursor position
|
|
957
|
-
|
|
958
|
-
logger.debug(`cursor|respond position: row=${String(row)}, col=${String(col)}`);
|
|
959
|
-
})
|
|
960
|
-
.forEach(async (line, lineIndex) => {
|
|
961
|
-
// ============ respond on rendered screen
|
|
962
|
-
const rendered = terminalRender.tail(24);
|
|
963
|
-
// Skip processing if output hasn't changed
|
|
964
|
-
if (rendered === lastRendered) return;
|
|
965
|
-
|
|
966
|
-
logger.debug(`stdout|${line}`);
|
|
967
|
-
|
|
968
|
-
// ready matcher: if matched, mark stdin ready
|
|
969
|
-
if (conf.ready?.some((rx: RegExp) => line.match(rx))) {
|
|
970
|
-
logger.debug(`ready |${line}`);
|
|
971
|
-
if (cli === "gemini" && lineIndex <= 80) return; // gemini initial noise, only after many lines
|
|
972
|
-
ctx.stdinReady.ready();
|
|
973
|
-
ctx.stdinFirstReady.ready();
|
|
974
|
-
}
|
|
975
960
|
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
961
|
+
// enter matchers: send Enter when any enter regex matches
|
|
962
|
+
if (conf.enter?.some((rx: RegExp) => line.match(rx))) {
|
|
963
|
+
logger.debug(`sendEnter matched|${line}`);
|
|
964
|
+
return await sendEnter(ctx.messageContext, 400); // wait for idle for a short while and then send Enter
|
|
965
|
+
}
|
|
981
966
|
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
967
|
+
// typingRespond matcher: if matched, send the specified message
|
|
968
|
+
const typeingRespondMatched = Object.entries(conf.typingRespond ?? {}).filter(
|
|
969
|
+
([_sendString, onThePatterns]) => onThePatterns.some((rx) => line.match(rx)),
|
|
970
|
+
);
|
|
971
|
+
const typingResponded =
|
|
972
|
+
typeingRespondMatched.length &&
|
|
973
|
+
(await sflow(typeingRespondMatched)
|
|
974
|
+
.map(
|
|
975
|
+
async ([sendString]) =>
|
|
976
|
+
await sendMessage(ctx.messageContext, sendString, { waitForReady: false }),
|
|
977
|
+
)
|
|
978
|
+
.toCount());
|
|
979
|
+
if (typingResponded) return;
|
|
980
|
+
|
|
981
|
+
// fatal matchers: set isFatal flag when matched
|
|
982
|
+
if (conf.fatal?.some((rx: RegExp) => line.match(rx))) {
|
|
983
|
+
logger.debug(`fatal |${line}`);
|
|
984
|
+
ctx.isFatal = true;
|
|
985
|
+
await exitAgent();
|
|
986
|
+
}
|
|
1002
987
|
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
988
|
+
// restartWithoutContinueArg matchers: set flag to restart without continue args
|
|
989
|
+
if (conf.restartWithoutContinueArg?.some((rx: RegExp) => line.match(rx))) {
|
|
990
|
+
logger.debug(`restart-without-continue|${line}`);
|
|
991
|
+
ctx.shouldRestartWithoutContinue = true;
|
|
992
|
+
ctx.isFatal = true; // also set fatal to trigger exit
|
|
993
|
+
await exitAgent();
|
|
994
|
+
}
|
|
1010
995
|
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
996
|
+
// session ID capture for codex
|
|
997
|
+
if (cli === "codex") {
|
|
998
|
+
const sessionId = extractSessionId(line);
|
|
999
|
+
if (sessionId) {
|
|
1000
|
+
logger.debug(`session|captured session ID: ${sessionId}`);
|
|
1001
|
+
await storeSessionForCwd(workingDir, sessionId);
|
|
1002
|
+
}
|
|
1017
1003
|
}
|
|
1018
|
-
}
|
|
1019
|
-
|
|
1004
|
+
})
|
|
1005
|
+
);
|
|
1020
1006
|
})
|
|
1021
1007
|
|
|
1022
1008
|
// auto-response
|
|
@@ -1043,7 +1029,7 @@ export default async function agentYes({
|
|
|
1043
1029
|
.by(createTerminatorStream(pendingExitCode.promise))
|
|
1044
1030
|
.to(fromWritable(process.stdout));
|
|
1045
1031
|
|
|
1046
|
-
await saveLogFile(ctx.logPaths.logPath,
|
|
1032
|
+
await saveLogFile(ctx.logPaths.logPath, xtermProxy.render());
|
|
1047
1033
|
|
|
1048
1034
|
// and then get its exitcode
|
|
1049
1035
|
const exitCode = await pendingExitCode.promise;
|
|
@@ -1052,13 +1038,14 @@ export default async function agentYes({
|
|
|
1052
1038
|
// Final pidStore cleanup
|
|
1053
1039
|
await pidStore.close();
|
|
1054
1040
|
|
|
1055
|
-
//
|
|
1056
|
-
|
|
1041
|
+
// Capture final render before disposing xterm proxy
|
|
1042
|
+
const finalRender = xtermProxy.render();
|
|
1043
|
+
xtermProxy.dispose();
|
|
1057
1044
|
|
|
1058
1045
|
// deprecated logFile option, we have logPath now, but keep for backward compatibility
|
|
1059
|
-
await saveDeprecatedLogFile(logFile,
|
|
1046
|
+
await saveDeprecatedLogFile(logFile, finalRender, verbose);
|
|
1060
1047
|
|
|
1061
|
-
return { exitCode, logs:
|
|
1048
|
+
return { exitCode, logs: finalRender };
|
|
1062
1049
|
|
|
1063
1050
|
async function exitAgent() {
|
|
1064
1051
|
ctx.robust = false; // disable robust to avoid auto restart
|
package/ts/parseCliArgs.spec.ts
CHANGED
|
@@ -333,4 +333,92 @@ describe("CLI argument parsing", () => {
|
|
|
333
333
|
expect(result.cli).toBe("claude");
|
|
334
334
|
expect(result.autoYes).toBe(true);
|
|
335
335
|
});
|
|
336
|
+
|
|
337
|
+
it("should parse bare positional words as prompt (cy arg1 arg2)", () => {
|
|
338
|
+
const result = parseCliArgs(["node", "/usr/local/bin/cy", "arg1", "arg2"]);
|
|
339
|
+
|
|
340
|
+
expect(result.cli).toBe("claude");
|
|
341
|
+
expect(result.prompt).toBe("arg1 arg2");
|
|
342
|
+
expect(result.cliArgs).toEqual([]);
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
it("should parse bare positional words as prompt with explicit CLI name", () => {
|
|
346
|
+
const result = parseCliArgs(["node", "/path/to/cli", "claude", "fix", "this", "bug"]);
|
|
347
|
+
|
|
348
|
+
expect(result.cli).toBe("claude");
|
|
349
|
+
expect(result.prompt).toBe("fix this bug");
|
|
350
|
+
expect(result.cliArgs).toEqual([]);
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
it("should parse bare words as prompt with claude-yes script name", () => {
|
|
354
|
+
const result = parseCliArgs(["node", "/usr/local/bin/claude-yes", "solve", "all", "todos"]);
|
|
355
|
+
|
|
356
|
+
expect(result.cli).toBe("claude");
|
|
357
|
+
expect(result.prompt).toBe("solve all todos");
|
|
358
|
+
expect(result.cliArgs).toEqual([]);
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
it("should separate CLI flags from bare words as prompt", () => {
|
|
362
|
+
const result = parseCliArgs([
|
|
363
|
+
"node",
|
|
364
|
+
"/usr/local/bin/claude-yes",
|
|
365
|
+
"--some-flag",
|
|
366
|
+
"value",
|
|
367
|
+
"fix",
|
|
368
|
+
"the",
|
|
369
|
+
"bug",
|
|
370
|
+
]);
|
|
371
|
+
|
|
372
|
+
expect(result.cli).toBe("claude");
|
|
373
|
+
expect(result.prompt).toBe("fix the bug");
|
|
374
|
+
expect(result.cliArgs).toContain("--some-flag");
|
|
375
|
+
expect(result.cliArgs).toContain("value");
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
it("should combine --prompt flag with bare positional words", () => {
|
|
379
|
+
const result = parseCliArgs([
|
|
380
|
+
"node",
|
|
381
|
+
"/usr/local/bin/claude-yes",
|
|
382
|
+
"--prompt",
|
|
383
|
+
"prefix:",
|
|
384
|
+
"solve",
|
|
385
|
+
"this",
|
|
386
|
+
]);
|
|
387
|
+
|
|
388
|
+
expect(result.prompt).toBe("prefix: solve this");
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
it("should combine bare positional words with dash prompt", () => {
|
|
392
|
+
const result = parseCliArgs([
|
|
393
|
+
"node",
|
|
394
|
+
"/usr/local/bin/claude-yes",
|
|
395
|
+
"solve",
|
|
396
|
+
"--",
|
|
397
|
+
"this",
|
|
398
|
+
"too",
|
|
399
|
+
]);
|
|
400
|
+
|
|
401
|
+
expect(result.prompt).toBe("solve this too");
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
it("should not pass --no-rust to the target CLI", () => {
|
|
405
|
+
const result = parseCliArgs(["node", "/path/to/cy", "--no-rust"]);
|
|
406
|
+
|
|
407
|
+
expect(result.useRust).toBe(false);
|
|
408
|
+
expect(result.cliArgs).not.toContain("--no-rust");
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
it("should not pass --no-robust to the target CLI", () => {
|
|
412
|
+
const result = parseCliArgs(["node", "/path/to/cy", "--no-robust"]);
|
|
413
|
+
|
|
414
|
+
expect(result.robust).toBe(false);
|
|
415
|
+
expect(result.cliArgs).not.toContain("--no-robust");
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
it("should not pass --no-queue to the target CLI", () => {
|
|
419
|
+
const result = parseCliArgs(["node", "/path/to/cy", "--no-queue"]);
|
|
420
|
+
|
|
421
|
+
expect(result.queue).toBe(false);
|
|
422
|
+
expect(result.cliArgs).not.toContain("--no-queue");
|
|
423
|
+
});
|
|
336
424
|
});
|
package/ts/parseCliArgs.ts
CHANGED
|
@@ -211,12 +211,33 @@ export function parseCliArgs(argv: string[]) {
|
|
|
211
211
|
}
|
|
212
212
|
});
|
|
213
213
|
|
|
214
|
+
// Collect bare positional words as prompt text (e.g., `cy arg1 arg2` → prompt = "arg1 arg2")
|
|
215
|
+
const positionalPromptWords: string[] = [];
|
|
216
|
+
|
|
214
217
|
const cliArgsForSpawn = (() => {
|
|
215
218
|
if (parsedArgv._[0] && !cliName) {
|
|
216
|
-
// Explicit CLI name provided as positional arg
|
|
217
|
-
|
|
219
|
+
// Explicit CLI name provided as positional arg — separate flags from bare words
|
|
220
|
+
const allAfterCli = rawArgs.slice((cliArgIndex ?? 0) + 1, dashIndex ?? undefined);
|
|
221
|
+
const result: string[] = [];
|
|
222
|
+
for (let i = 0; i < allAfterCli.length; i++) {
|
|
223
|
+
const arg = allAfterCli[i]!;
|
|
224
|
+
if (arg.startsWith("-")) {
|
|
225
|
+
result.push(arg);
|
|
226
|
+
// Consume the next arg as the flag's value if separate (--flag value)
|
|
227
|
+
if (!arg.includes("=") && i + 1 < allAfterCli.length) {
|
|
228
|
+
const nextArg = allAfterCli[i + 1];
|
|
229
|
+
if (nextArg && !nextArg.startsWith("-")) {
|
|
230
|
+
result.push(nextArg);
|
|
231
|
+
i++;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
} else {
|
|
235
|
+
positionalPromptWords.push(arg);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
return result;
|
|
218
239
|
} else if (cliName) {
|
|
219
|
-
// CLI name from script, filter out
|
|
240
|
+
// CLI name from script, filter out what yargs consumed; bare words become prompt
|
|
220
241
|
const result: string[] = [];
|
|
221
242
|
const argsToCheck = rawArgs.slice(0, dashIndex ?? undefined);
|
|
222
243
|
|
|
@@ -226,7 +247,11 @@ export function parseCliArgs(argv: string[]) {
|
|
|
226
247
|
|
|
227
248
|
const [flag] = arg.split("=");
|
|
228
249
|
|
|
229
|
-
|
|
250
|
+
// Check both the flag itself and its --no- negation (yargs stores --no-x as key "x")
|
|
251
|
+
const isConsumed =
|
|
252
|
+
(flag && yargsConsumed.has(flag)) ||
|
|
253
|
+
(flag?.startsWith("--no-") && yargsConsumed.has(`--${flag.slice(5)}`));
|
|
254
|
+
if (isConsumed) {
|
|
230
255
|
// Skip consumed flag and its value if separate
|
|
231
256
|
if (!arg.includes("=") && i + 1 < argsToCheck.length) {
|
|
232
257
|
const nextArg = argsToCheck[i + 1];
|
|
@@ -234,14 +259,27 @@ export function parseCliArgs(argv: string[]) {
|
|
|
234
259
|
i++; // Skip value
|
|
235
260
|
}
|
|
236
261
|
}
|
|
237
|
-
} else {
|
|
262
|
+
} else if (arg.startsWith("-")) {
|
|
263
|
+
// Non-consumed flag → pass to target CLI
|
|
238
264
|
result.push(arg);
|
|
265
|
+
// Consume the next arg as the flag's value if separate
|
|
266
|
+
if (!arg.includes("=") && i + 1 < argsToCheck.length) {
|
|
267
|
+
const nextArg = argsToCheck[i + 1];
|
|
268
|
+
if (nextArg && !nextArg.startsWith("-")) {
|
|
269
|
+
result.push(nextArg);
|
|
270
|
+
i++;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
} else {
|
|
274
|
+
// Bare word → treat as prompt text
|
|
275
|
+
positionalPromptWords.push(arg);
|
|
239
276
|
}
|
|
240
277
|
}
|
|
241
278
|
return result;
|
|
242
279
|
}
|
|
243
280
|
return [];
|
|
244
281
|
})();
|
|
282
|
+
const positionalPrompt = positionalPromptWords.join(" ") || undefined;
|
|
245
283
|
const dashPrompt: string | undefined =
|
|
246
284
|
dashIndex === undefined ? undefined : rawArgs.slice(dashIndex + 1).join(" ");
|
|
247
285
|
|
|
@@ -262,7 +300,8 @@ export function parseCliArgs(argv: string[]) {
|
|
|
262
300
|
? parsedArgv._[0]?.toString()?.replace?.(/-yes$/, "")
|
|
263
301
|
: undefined)) as (typeof SUPPORTED_CLIS)[number],
|
|
264
302
|
cliArgs: [...cliArgsForSpawn, ...(parsedArgv.yes ? ["--dangerously-skip-permissions"] : [])],
|
|
265
|
-
prompt:
|
|
303
|
+
prompt:
|
|
304
|
+
[parsedArgv.prompt, positionalPrompt, dashPrompt].filter(Boolean).join(" ") || undefined,
|
|
266
305
|
install: parsedArgv.install,
|
|
267
306
|
exitOnIdle: Number(
|
|
268
307
|
(parsedArgv.timeout || parsedArgv.idle || parsedArgv.exitOnIdle)?.replace(/.*/, (e) =>
|
package/ts/rustBinary.ts
CHANGED
|
@@ -2,9 +2,11 @@
|
|
|
2
2
|
* Rust binary helper - finds or downloads the appropriate prebuilt binary
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
+
import { execFileSync } from "child_process";
|
|
5
6
|
import { existsSync, mkdirSync, unlinkSync } from "fs";
|
|
6
7
|
import { chmod, copyFile } from "fs/promises";
|
|
7
8
|
import path from "path";
|
|
9
|
+
import pkg from "../package.json" with { type: "json" };
|
|
8
10
|
|
|
9
11
|
// Platform/arch to binary name mapping
|
|
10
12
|
const PLATFORM_MAP: Record<string, string> = {
|
|
@@ -192,6 +194,70 @@ export async function downloadBinary(verbose = false): Promise<string> {
|
|
|
192
194
|
return binaryPath;
|
|
193
195
|
}
|
|
194
196
|
|
|
197
|
+
/**
|
|
198
|
+
* Get the version of a Rust binary by running it with --version
|
|
199
|
+
*/
|
|
200
|
+
function getRustBinaryVersion(binaryPath: string): string | null {
|
|
201
|
+
try {
|
|
202
|
+
const output = execFileSync(binaryPath, ["--version"], {
|
|
203
|
+
timeout: 5000,
|
|
204
|
+
encoding: "utf8",
|
|
205
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
206
|
+
});
|
|
207
|
+
// Output is like "agent-yes 1.72.3" or "agent-yes v1.72.3"
|
|
208
|
+
const match = output.match(/(\d+\.\d+\.\d+)/);
|
|
209
|
+
return match ? match[1] : null;
|
|
210
|
+
} catch {
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Check if a binary path is inside a git repo (dev build), and rebuild if outdated.
|
|
217
|
+
* Returns the same path if up-to-date or rebuilt, undefined if rebuild failed.
|
|
218
|
+
*/
|
|
219
|
+
function autoRebuildIfOutdated(binaryPath: string, verbose: boolean): boolean {
|
|
220
|
+
// Only auto-rebuild for local dev builds (target/release or target/debug)
|
|
221
|
+
if (!binaryPath.includes("/target/release") && !binaryPath.includes("/target/debug")) {
|
|
222
|
+
return true; // not a dev build, skip
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const binaryVersion = getRustBinaryVersion(binaryPath);
|
|
226
|
+
if (verbose) {
|
|
227
|
+
console.log(`[rust] Binary version: ${binaryVersion}, package version: ${pkg.version}`);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (binaryVersion === pkg.version) {
|
|
231
|
+
return true; // up to date
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Find the rs/ directory relative to the binary (binary is at rs/target/release/agent-yes)
|
|
235
|
+
const rsDir = binaryPath.replace(/\/target\/(release|debug)\/agent-yes.*$/, "");
|
|
236
|
+
if (!existsSync(path.join(rsDir, "Cargo.toml"))) {
|
|
237
|
+
if (verbose) console.log(`[rust] Cannot find Cargo.toml at ${rsDir}, skipping rebuild`);
|
|
238
|
+
return true; // can't rebuild, use as-is
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
process.stderr.write(
|
|
242
|
+
`\x1b[33m[rust] Binary outdated (${binaryVersion ?? "unknown"} → ${pkg.version}), rebuilding…\x1b[0m\n`,
|
|
243
|
+
);
|
|
244
|
+
|
|
245
|
+
try {
|
|
246
|
+
const isRelease = binaryPath.includes("/target/release");
|
|
247
|
+
const args = ["build", ...(isRelease ? ["--release"] : [])];
|
|
248
|
+
execFileSync("cargo", args, {
|
|
249
|
+
cwd: rsDir,
|
|
250
|
+
stdio: "inherit",
|
|
251
|
+
timeout: 300_000, // 5 min max
|
|
252
|
+
});
|
|
253
|
+
process.stderr.write(`\x1b[32m[rust] Rebuild complete\x1b[0m\n`);
|
|
254
|
+
return true;
|
|
255
|
+
} catch {
|
|
256
|
+
process.stderr.write(`\x1b[31m[rust] Auto-rebuild failed, using outdated binary\x1b[0m\n`);
|
|
257
|
+
return true; // still usable, just old
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
195
261
|
/**
|
|
196
262
|
* Get or download the Rust binary
|
|
197
263
|
*/
|
|
@@ -210,6 +276,8 @@ export async function getRustBinary(
|
|
|
210
276
|
if (verbose) {
|
|
211
277
|
console.log(`[rust] Using existing binary: ${existing}`);
|
|
212
278
|
}
|
|
279
|
+
// Auto-rebuild if it's a dev build and version is outdated
|
|
280
|
+
autoRebuildIfOutdated(existing, verbose);
|
|
213
281
|
return existing;
|
|
214
282
|
}
|
|
215
283
|
}
|
|
@@ -4,6 +4,8 @@ import {
|
|
|
4
4
|
compareVersions,
|
|
5
5
|
fetchLatestVersion,
|
|
6
6
|
displayVersion,
|
|
7
|
+
detectInstallMethod,
|
|
8
|
+
versionString,
|
|
7
9
|
} from "./versionChecker";
|
|
8
10
|
|
|
9
11
|
vi.mock("execa", () => ({ execaCommand: vi.fn().mockResolvedValue({}) }));
|
|
@@ -12,6 +14,16 @@ vi.mock("fs/promises", () => ({
|
|
|
12
14
|
readFile: vi.fn(),
|
|
13
15
|
writeFile: vi.fn().mockResolvedValue(undefined),
|
|
14
16
|
}));
|
|
17
|
+
vi.mock("fs", async (importOriginal) => {
|
|
18
|
+
const actual = await importOriginal<typeof import("fs")>();
|
|
19
|
+
return {
|
|
20
|
+
...actual,
|
|
21
|
+
// Return false for .git checks so the dev-checkout guard doesn't skip auto-update in tests
|
|
22
|
+
existsSync: vi.fn(() => false),
|
|
23
|
+
lstatSync: actual.lstatSync,
|
|
24
|
+
readlinkSync: actual.readlinkSync,
|
|
25
|
+
};
|
|
26
|
+
});
|
|
15
27
|
vi.mock("child_process", () => ({
|
|
16
28
|
execFileSync: vi.fn(() => {
|
|
17
29
|
// Simulate successful re-exec by throwing an exit-like error
|
|
@@ -106,6 +118,14 @@ describe("versionChecker", () => {
|
|
|
106
118
|
expect(fetch).not.toHaveBeenCalled();
|
|
107
119
|
});
|
|
108
120
|
|
|
121
|
+
it("should skip when running from a git dev checkout", async () => {
|
|
122
|
+
const fs = await import("fs");
|
|
123
|
+
// Make the .git check return true so the dev-checkout guard triggers
|
|
124
|
+
vi.mocked(fs.existsSync).mockReturnValueOnce(true);
|
|
125
|
+
await checkAndAutoUpdate();
|
|
126
|
+
expect(fetch).not.toHaveBeenCalled();
|
|
127
|
+
});
|
|
128
|
+
|
|
109
129
|
it("should skip when AGENT_YES_UPDATED matches current version", async () => {
|
|
110
130
|
const pkg = await import("../package.json");
|
|
111
131
|
process.env.AGENT_YES_UPDATED = pkg.default.version;
|
|
@@ -252,4 +272,32 @@ describe("versionChecker", () => {
|
|
|
252
272
|
expect(console.log).toHaveBeenCalledWith(expect.stringContaining("unable to check"));
|
|
253
273
|
});
|
|
254
274
|
});
|
|
275
|
+
|
|
276
|
+
describe("detectInstallMethod", () => {
|
|
277
|
+
it("should return a string", () => {
|
|
278
|
+
const method = detectInstallMethod();
|
|
279
|
+
expect(typeof method).toBe("string");
|
|
280
|
+
expect(method.length).toBeGreaterThan(0);
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
it("should return 'git' when .git exists in parent of script dir", async () => {
|
|
284
|
+
const fs = await import("fs");
|
|
285
|
+
vi.mocked(fs.existsSync).mockReturnValueOnce(true);
|
|
286
|
+
expect(detectInstallMethod()).toBe("git");
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
it("should return 'source' when not in node_modules and no .git", async () => {
|
|
290
|
+
const fs = await import("fs");
|
|
291
|
+
vi.mocked(fs.existsSync).mockReturnValueOnce(false);
|
|
292
|
+
expect(detectInstallMethod()).toBe("source");
|
|
293
|
+
});
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
describe("versionString", () => {
|
|
297
|
+
it("should include version and install method", () => {
|
|
298
|
+
const str = versionString();
|
|
299
|
+
expect(str).toContain("agent-yes v");
|
|
300
|
+
expect(str).toMatch(/agent-yes v\d+\.\d+\.\d+ \(.+\)/);
|
|
301
|
+
});
|
|
302
|
+
});
|
|
255
303
|
});
|