agent-yes 1.60.6 → 1.61.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--w_SPLyp.js → SUPPORTED_CLIS-C0l1NfFH.js} +39 -41
- package/dist/{agent-yes.config-B-sre0vp.js → agent-yes.config-DcxG25Gv.js} +4 -4
- package/dist/cli.js +5 -6
- package/dist/index.js +2 -2
- package/dist/{logger-CY9ormLF.js → logger-CX77vJDA.js} +1 -1
- package/package.json +6 -4
- package/ts/JsonlStore.spec.ts +195 -0
- package/ts/JsonlStore.ts +3 -6
- package/ts/cli.ts +0 -3
- package/ts/configLoader.spec.ts +65 -1
- package/ts/defineConfig.spec.ts +29 -0
- package/ts/idleWaiter.spec.ts +16 -0
- package/ts/index.ts +8 -7
- package/ts/parseCliArgs.ts +0 -1
- package/ts/pidStore.spec.ts +14 -0
- package/ts/rustBinary.ts +3 -3
- package/ts/session-integration.spec.ts +1 -1
- package/ts/versionChecker.spec.ts +119 -0
- package/dist/agent-yes.config-XmUcKFde.js +0 -4
|
@@ -1,22 +1,22 @@
|
|
|
1
|
-
import { t as logger } from "./logger-
|
|
1
|
+
import { t as logger } from "./logger-CX77vJDA.js";
|
|
2
2
|
import { arch, platform } from "process";
|
|
3
3
|
import { execSync } from "child_process";
|
|
4
|
-
import { closeSync, existsSync, fsyncSync, mkdirSync, openSync } from "fs";
|
|
5
|
-
import path, { dirname, join } from "path";
|
|
6
|
-
import { readFile } from "node:fs/promises";
|
|
7
|
-
import os from "node:os";
|
|
8
|
-
import path$1 from "node:path";
|
|
9
|
-
import winston from "winston";
|
|
10
4
|
import { execaCommandSync, parseCommandString } from "execa";
|
|
11
5
|
import { fromWritable } from "from-node-stream";
|
|
12
|
-
import { appendFile, mkdir
|
|
6
|
+
import { appendFile, mkdir, readFile, readdir, rename, writeFile } from "fs/promises";
|
|
7
|
+
import path, { dirname, join } from "path";
|
|
13
8
|
import DIE from "phpdie";
|
|
14
9
|
import sflow from "sflow";
|
|
15
10
|
import { TerminalRenderStream } from "terminal-render";
|
|
16
11
|
import { homedir } from "os";
|
|
12
|
+
import winston from "winston";
|
|
13
|
+
import { closeSync, existsSync, fsyncSync, openSync } from "fs";
|
|
17
14
|
import { lock } from "proper-lockfile";
|
|
18
15
|
import { execSync as execSync$1 } from "node:child_process";
|
|
19
16
|
import { fileURLToPath } from "url";
|
|
17
|
+
import os from "node:os";
|
|
18
|
+
import { readFile as readFile$1 } from "node:fs/promises";
|
|
19
|
+
import path$1 from "node:path";
|
|
20
20
|
|
|
21
21
|
//#region \0rolldown/runtime.js
|
|
22
22
|
var __defProp = Object.defineProperty;
|
|
@@ -44,7 +44,7 @@ const getCodexSessionsDir = () => process.env.CLI_YES_TEST_HOME ? path.join(proc
|
|
|
44
44
|
*/
|
|
45
45
|
async function loadSessionMap() {
|
|
46
46
|
try {
|
|
47
|
-
const content = await readFile
|
|
47
|
+
const content = await readFile(getSessionsFile(), "utf-8");
|
|
48
48
|
return JSON.parse(content);
|
|
49
49
|
} catch {
|
|
50
50
|
return {};
|
|
@@ -56,8 +56,8 @@ async function loadSessionMap() {
|
|
|
56
56
|
async function saveSessionMap(sessionMap) {
|
|
57
57
|
try {
|
|
58
58
|
const sessionsFile = getSessionsFile();
|
|
59
|
-
await mkdir
|
|
60
|
-
await writeFile
|
|
59
|
+
await mkdir(path.dirname(sessionsFile), { recursive: true });
|
|
60
|
+
await writeFile(sessionsFile, JSON.stringify(sessionMap, null, 2));
|
|
61
61
|
} catch (error) {
|
|
62
62
|
console.warn("Failed to save codex session map:", error);
|
|
63
63
|
}
|
|
@@ -78,7 +78,7 @@ async function storeSessionForCwd(cwd, sessionId) {
|
|
|
78
78
|
*/
|
|
79
79
|
async function parseCodexSessionFile(filePath) {
|
|
80
80
|
try {
|
|
81
|
-
const lines = (await readFile
|
|
81
|
+
const lines = (await readFile(filePath, "utf-8")).trim().split("\n");
|
|
82
82
|
for (const line of lines) {
|
|
83
83
|
if (!line.trim()) continue;
|
|
84
84
|
const data = JSON.parse(line);
|
|
@@ -252,9 +252,9 @@ async function readLockFile() {
|
|
|
252
252
|
try {
|
|
253
253
|
const lockDir = getLockDir();
|
|
254
254
|
const lockFilePath = getLockFile();
|
|
255
|
-
await mkdir
|
|
255
|
+
await mkdir(lockDir, { recursive: true });
|
|
256
256
|
if (!existsSync(lockFilePath)) return { tasks: [] };
|
|
257
|
-
const content = await readFile
|
|
257
|
+
const content = await readFile(lockFilePath, "utf8");
|
|
258
258
|
const lockFile = JSON.parse(content);
|
|
259
259
|
lockFile.tasks = lockFile.tasks.filter((task) => {
|
|
260
260
|
if (isProcessRunning(task.pid)) return true;
|
|
@@ -272,9 +272,9 @@ async function writeLockFile(lockFile, retryCount = 0) {
|
|
|
272
272
|
try {
|
|
273
273
|
const lockDir = getLockDir();
|
|
274
274
|
const lockFilePath = getLockFile();
|
|
275
|
-
await mkdir
|
|
275
|
+
await mkdir(lockDir, { recursive: true });
|
|
276
276
|
const tempFile = `${lockFilePath}.tmp.${process.pid}`;
|
|
277
|
-
await writeFile
|
|
277
|
+
await writeFile(tempFile, JSON.stringify(lockFile, null, 2), "utf8");
|
|
278
278
|
await rename(tempFile, lockFilePath);
|
|
279
279
|
} catch (error) {
|
|
280
280
|
if (retryCount < MAX_RETRIES) {
|
|
@@ -437,19 +437,17 @@ function shouldUseLock(_cwd) {
|
|
|
437
437
|
var JsonlStore = class {
|
|
438
438
|
filePath;
|
|
439
439
|
tempPath;
|
|
440
|
-
lockPath;
|
|
441
440
|
docs = /* @__PURE__ */ new Map();
|
|
442
441
|
constructor(filePath) {
|
|
443
442
|
this.filePath = filePath;
|
|
444
443
|
this.tempPath = filePath + "~";
|
|
445
|
-
this.lockPath = path.dirname(filePath);
|
|
446
444
|
}
|
|
447
445
|
/**
|
|
448
446
|
* Load all records from the JSONL file. No lock needed.
|
|
449
447
|
* Handles crash recovery: partial last line skipped, temp file recovery.
|
|
450
448
|
*/
|
|
451
449
|
async load() {
|
|
452
|
-
await mkdir
|
|
450
|
+
await mkdir(path.dirname(this.filePath), { recursive: true });
|
|
453
451
|
if (!existsSync(this.filePath) && existsSync(this.tempPath)) {
|
|
454
452
|
logger.debug("[JsonlStore] Recovering from temp file");
|
|
455
453
|
await rename(this.tempPath, this.filePath);
|
|
@@ -457,7 +455,7 @@ var JsonlStore = class {
|
|
|
457
455
|
this.docs = /* @__PURE__ */ new Map();
|
|
458
456
|
let raw = "";
|
|
459
457
|
try {
|
|
460
|
-
raw = await readFile
|
|
458
|
+
raw = await readFile(this.filePath, "utf-8");
|
|
461
459
|
} catch (err) {
|
|
462
460
|
if (err.code === "ENOENT") return this.docs;
|
|
463
461
|
throw err;
|
|
@@ -561,7 +559,7 @@ var JsonlStore = class {
|
|
|
561
559
|
*/
|
|
562
560
|
async compact() {
|
|
563
561
|
const lines = Array.from(this.docs.values()).map((doc) => {
|
|
564
|
-
const { _id, $$deleted, ...rest } = doc;
|
|
562
|
+
const { _id, $$deleted: _$$deleted, ...rest } = doc;
|
|
565
563
|
return JSON.stringify({
|
|
566
564
|
_id,
|
|
567
565
|
...rest
|
|
@@ -570,14 +568,14 @@ var JsonlStore = class {
|
|
|
570
568
|
const content = lines ? lines + "\n" : "";
|
|
571
569
|
try {
|
|
572
570
|
await this.withLock(async () => {
|
|
573
|
-
await writeFile
|
|
571
|
+
await writeFile(this.tempPath, content);
|
|
574
572
|
const fd = openSync(this.tempPath, "r");
|
|
575
573
|
fsyncSync(fd);
|
|
576
574
|
closeSync(fd);
|
|
577
575
|
await rename(this.tempPath, this.filePath);
|
|
578
576
|
});
|
|
579
577
|
} catch {
|
|
580
|
-
await writeFile
|
|
578
|
+
await writeFile(this.filePath, content);
|
|
581
579
|
}
|
|
582
580
|
}
|
|
583
581
|
async withLock(fn) {
|
|
@@ -714,8 +712,8 @@ pid-db/
|
|
|
714
712
|
|
|
715
713
|
`;
|
|
716
714
|
try {
|
|
717
|
-
await mkdir
|
|
718
|
-
await writeFile
|
|
715
|
+
await mkdir(this.storeDir, { recursive: true });
|
|
716
|
+
await writeFile(gitignorePath, gitignoreContent, { flag: "wx" });
|
|
719
717
|
logger.debug(`[pidStore] Created .gitignore in ${this.storeDir}`);
|
|
720
718
|
} catch (error) {
|
|
721
719
|
if (error.code !== "EEXIST") logger.warn(`[pidStore] Failed to create .gitignore:`, error);
|
|
@@ -837,7 +835,7 @@ async function sendMessage(context, message, { waitForReady = true } = {}) {
|
|
|
837
835
|
*/
|
|
838
836
|
async function initializeLogPaths(pidStore, pid) {
|
|
839
837
|
const logDir = pidStore.getLogDir();
|
|
840
|
-
await mkdir
|
|
838
|
+
await mkdir(logDir, { recursive: true });
|
|
841
839
|
return {
|
|
842
840
|
logPath: logDir,
|
|
843
841
|
rawLogPath: path.resolve(path.dirname(logDir), `${pid}.raw.log`),
|
|
@@ -862,8 +860,8 @@ function setupDebugLogging(debuggingLogsPath) {
|
|
|
862
860
|
*/
|
|
863
861
|
async function saveLogFile(logPath, content) {
|
|
864
862
|
if (!logPath) return;
|
|
865
|
-
await mkdir
|
|
866
|
-
await writeFile
|
|
863
|
+
await mkdir(path.dirname(logPath), { recursive: true }).catch(() => null);
|
|
864
|
+
await writeFile(logPath, content).catch(() => null);
|
|
867
865
|
logger.info(`Full logs saved to ${logPath}`);
|
|
868
866
|
}
|
|
869
867
|
/**
|
|
@@ -876,8 +874,8 @@ async function saveDeprecatedLogFile(logFile, content, verbose) {
|
|
|
876
874
|
if (!logFile) return;
|
|
877
875
|
if (verbose) logger.info(`Writing rendered logs to ${logFile}`);
|
|
878
876
|
const logFilePath = path.resolve(logFile);
|
|
879
|
-
await mkdir
|
|
880
|
-
await writeFile
|
|
877
|
+
await mkdir(path.dirname(logFilePath), { recursive: true }).catch(() => null);
|
|
878
|
+
await writeFile(logFilePath, content);
|
|
881
879
|
}
|
|
882
880
|
|
|
883
881
|
//#endregion
|
|
@@ -906,7 +904,7 @@ function tryCatch(catchFn, fn) {
|
|
|
906
904
|
//#endregion
|
|
907
905
|
//#region package.json
|
|
908
906
|
var name = "agent-yes";
|
|
909
|
-
var version = "1.
|
|
907
|
+
var version = "1.61.0";
|
|
910
908
|
|
|
911
909
|
//#endregion
|
|
912
910
|
//#region ts/pty-fix.ts
|
|
@@ -1071,7 +1069,7 @@ function isCommandNotFoundError(e) {
|
|
|
1071
1069
|
* ```
|
|
1072
1070
|
*/
|
|
1073
1071
|
function spawnAgent(options) {
|
|
1074
|
-
const { cli, cliConf, cliArgs, verbose, install, ptyOptions } = options;
|
|
1072
|
+
const { cli, cliConf, cliArgs, verbose: _verbose, install, ptyOptions } = options;
|
|
1075
1073
|
const spawn = () => {
|
|
1076
1074
|
let [bin, ...args] = [...parseCommandString(cliConf?.binary || cli), ...cliArgs];
|
|
1077
1075
|
logger.debug(`Spawning ${bin} with args: ${JSON.stringify(args)}`);
|
|
@@ -1079,7 +1077,7 @@ function spawnAgent(options) {
|
|
|
1079
1077
|
logger.info(`[${cli}-yes] Spawned ${bin} with PID ${spawned.pid} (agent-yes v${version})`);
|
|
1080
1078
|
return spawned;
|
|
1081
1079
|
};
|
|
1082
|
-
return tryCatch((error,
|
|
1080
|
+
return tryCatch((error, _attempts, spawn, ...args) => {
|
|
1083
1081
|
logger.error(`Fatal: Failed to start ${cli}.`);
|
|
1084
1082
|
const isNotFound = isCommandNotFoundError(error);
|
|
1085
1083
|
if (cliConf?.install && isNotFound) {
|
|
@@ -1293,7 +1291,7 @@ async function loadInstallEnv() {
|
|
|
1293
1291
|
if (_installEnv) return _installEnv;
|
|
1294
1292
|
const envPath = path$1.join(installDir, ".env");
|
|
1295
1293
|
try {
|
|
1296
|
-
_installEnv = parseEnvContent(await readFile(envPath, "utf-8"));
|
|
1294
|
+
_installEnv = parseEnvContent(await readFile$1(envPath, "utf-8"));
|
|
1297
1295
|
} catch {
|
|
1298
1296
|
_installEnv = {};
|
|
1299
1297
|
}
|
|
@@ -1332,7 +1330,7 @@ async function notifyWebhook(status, details, cwd = process.cwd()) {
|
|
|
1332
1330
|
|
|
1333
1331
|
//#endregion
|
|
1334
1332
|
//#region ts/index.ts
|
|
1335
|
-
const config = await import("./agent-yes.config-
|
|
1333
|
+
const config = await import("./agent-yes.config-DcxG25Gv.js").then((mod) => mod.default || mod);
|
|
1336
1334
|
const CLIS_CONFIG = config.clis;
|
|
1337
1335
|
/**
|
|
1338
1336
|
* Main function to run agent-cli with automatic yes/no responses
|
|
@@ -1362,7 +1360,7 @@ const CLIS_CONFIG = config.clis;
|
|
|
1362
1360
|
* });
|
|
1363
1361
|
* ```
|
|
1364
1362
|
*/
|
|
1365
|
-
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 }) {
|
|
1363
|
+
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: _useStdinAppend = false, autoYes = true }) {
|
|
1366
1364
|
if (!cli) throw new Error(`cli is required`);
|
|
1367
1365
|
const conf = CLIS_CONFIG[cli] || DIE(`Unsupported cli tool: ${cli}, current process.argv: ${process.argv.join(" ")}`);
|
|
1368
1366
|
const workingDir = cwd ?? process.cwd();
|
|
@@ -1411,7 +1409,7 @@ async function agentYes({ cli, cliArgs = [], prompt, robust = true, cwd, env, ex
|
|
|
1411
1409
|
let currentDir = workingDir;
|
|
1412
1410
|
const searchLimit = gitRoot || path.parse(currentDir).root;
|
|
1413
1411
|
while (true) {
|
|
1414
|
-
const md = await readFile
|
|
1412
|
+
const md = await readFile(path.resolve(currentDir, "SKILL.md"), "utf8").catch(() => null);
|
|
1415
1413
|
if (md) {
|
|
1416
1414
|
const headerMatch = md.match(/^[\s\S]*?(?=\n##\s)/);
|
|
1417
1415
|
const headerRaw = (headerMatch ? headerMatch[0] : md).trim();
|
|
@@ -1776,7 +1774,7 @@ async function agentYes({ cli, cliArgs = [], prompt, robust = true, cwd, env, ex
|
|
|
1776
1774
|
process.stdin.on("close", endHandler);
|
|
1777
1775
|
process.stdin.on("error", errorHandler);
|
|
1778
1776
|
},
|
|
1779
|
-
cancel(
|
|
1777
|
+
cancel(_reason) {
|
|
1780
1778
|
process.stdin.pause();
|
|
1781
1779
|
}
|
|
1782
1780
|
});
|
|
@@ -1857,10 +1855,10 @@ async function agentYes({ cli, cliArgs = [], prompt, robust = true, cwd, env, ex
|
|
|
1857
1855
|
}).forkTo(async function rawLogger(f) {
|
|
1858
1856
|
const rawLogPath = ctx.logPaths.rawLogPath;
|
|
1859
1857
|
if (!rawLogPath) return f.run();
|
|
1860
|
-
return await mkdir
|
|
1858
|
+
return await mkdir(path.dirname(rawLogPath), { recursive: true }).then(() => {
|
|
1861
1859
|
logger.debug(`[${cli}-yes] raw logs streaming to ${rawLogPath}`);
|
|
1862
1860
|
return f.forEach(async (chars) => {
|
|
1863
|
-
await writeFile
|
|
1861
|
+
await writeFile(rawLogPath, chars, { flag: "a" }).catch(() => null);
|
|
1864
1862
|
}).run();
|
|
1865
1863
|
}).catch(() => f.run());
|
|
1866
1864
|
}).by(function consoleResponder(e) {
|
|
@@ -1953,4 +1951,4 @@ const SUPPORTED_CLIS = Object.keys(CLIS_CONFIG);
|
|
|
1953
1951
|
|
|
1954
1952
|
//#endregion
|
|
1955
1953
|
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 };
|
|
1956
|
-
//# sourceMappingURL=SUPPORTED_CLIS
|
|
1954
|
+
//# sourceMappingURL=SUPPORTED_CLIS-C0l1NfFH.js.map
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { t as logger } from "./logger-
|
|
2
|
-
import { access, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
1
|
+
import { t as logger } from "./logger-CX77vJDA.js";
|
|
3
2
|
import os from "node:os";
|
|
3
|
+
import { access, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { parse } from "yaml";
|
|
6
6
|
|
|
@@ -331,5 +331,5 @@ function getDefaultConfig() {
|
|
|
331
331
|
}
|
|
332
332
|
|
|
333
333
|
//#endregion
|
|
334
|
-
export { agent_yes_config_default as
|
|
335
|
-
//# sourceMappingURL=agent-yes.config-
|
|
334
|
+
export { agent_yes_config_default as default };
|
|
335
|
+
//# sourceMappingURL=agent-yes.config-DcxG25Gv.js.map
|
package/dist/cli.js
CHANGED
|
@@ -1,15 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
|
-
import { c as PidStore, o as name, s as version, t as SUPPORTED_CLIS } from "./SUPPORTED_CLIS
|
|
3
|
-
import "./
|
|
4
|
-
import { t as logger } from "./logger-CY9ormLF.js";
|
|
2
|
+
import { c as PidStore, o as name, s as version, t as SUPPORTED_CLIS } from "./SUPPORTED_CLIS-C0l1NfFH.js";
|
|
3
|
+
import { t as logger } from "./logger-CX77vJDA.js";
|
|
5
4
|
import { argv } from "process";
|
|
6
5
|
import { spawn } from "child_process";
|
|
7
|
-
import { existsSync, mkdirSync, unlinkSync } from "fs";
|
|
8
|
-
import path from "path";
|
|
9
6
|
import ms from "ms";
|
|
10
7
|
import yargs from "yargs";
|
|
11
8
|
import { hideBin } from "yargs/helpers";
|
|
12
|
-
import { chmod, copyFile
|
|
9
|
+
import { chmod, copyFile } from "fs/promises";
|
|
10
|
+
import path from "path";
|
|
11
|
+
import { existsSync, mkdirSync, unlinkSync } from "fs";
|
|
13
12
|
|
|
14
13
|
//#region ts/parseCliArgs.ts
|
|
15
14
|
/**
|
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
|
|
2
|
-
import "./logger-
|
|
1
|
+
import { a as AgentContext, i as config, l as removeControlCharacters, n as CLIS_CONFIG, r as agentYes } from "./SUPPORTED_CLIS-C0l1NfFH.js";
|
|
2
|
+
import "./logger-CX77vJDA.js";
|
|
3
3
|
|
|
4
4
|
export { AgentContext, CLIS_CONFIG, config, agentYes as default, removeControlCharacters };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-yes",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.61.0",
|
|
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",
|
|
@@ -80,7 +80,8 @@
|
|
|
80
80
|
"prepare": "husky",
|
|
81
81
|
"release": "standard-version && npm publish",
|
|
82
82
|
"release:beta": "standard-version && npm publish --tag beta",
|
|
83
|
-
"test": "
|
|
83
|
+
"test": "vitest run",
|
|
84
|
+
"test:coverage": "vitest run --coverage"
|
|
84
85
|
},
|
|
85
86
|
"dependencies": {
|
|
86
87
|
"@snomiao/bun-pty": "^0.3.4",
|
|
@@ -91,7 +92,7 @@
|
|
|
91
92
|
"phpdie": "^1.7.0",
|
|
92
93
|
"proper-lockfile": "^4.1.2",
|
|
93
94
|
"sflow": "^1.27.0",
|
|
94
|
-
"terminal-render": "^1.5.
|
|
95
|
+
"terminal-render": "^1.5.1",
|
|
95
96
|
"winston": "^3.19.0",
|
|
96
97
|
"yaml": "^2.8.2",
|
|
97
98
|
"yargs": "^18.0.0"
|
|
@@ -106,6 +107,7 @@
|
|
|
106
107
|
"@types/proper-lockfile": "^4.1.4",
|
|
107
108
|
"@types/yargs": "^17.0.35",
|
|
108
109
|
"@typescript/native-preview": "^7.0.0-dev.20260124.1",
|
|
110
|
+
"@vitest/coverage-v8": "^4.1.0",
|
|
109
111
|
"husky": "^9.1.7",
|
|
110
112
|
"lint-staged": "^16.2.7",
|
|
111
113
|
"node-pty": "^1.1.0",
|
|
@@ -116,7 +118,7 @@
|
|
|
116
118
|
"semantic-release": "^25.0.2",
|
|
117
119
|
"standard-version": "^9.5.0",
|
|
118
120
|
"tsdown": "^0.20.3",
|
|
119
|
-
"vitest": "
|
|
121
|
+
"vitest": "4.1.0",
|
|
120
122
|
"zod": "^3.23.0"
|
|
121
123
|
},
|
|
122
124
|
"peerDependencies": {
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import { describe, expect, it, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import { JsonlStore } from "./JsonlStore";
|
|
3
|
+
import { rm, readFile, writeFile, mkdir } from "fs/promises";
|
|
4
|
+
import path from "path";
|
|
5
|
+
|
|
6
|
+
const TEST_DIR = "/tmp/jsonlstore-test-" + process.pid;
|
|
7
|
+
const TEST_FILE = path.join(TEST_DIR, "test.jsonl");
|
|
8
|
+
|
|
9
|
+
describe("JsonlStore", () => {
|
|
10
|
+
let store: JsonlStore;
|
|
11
|
+
|
|
12
|
+
beforeEach(async () => {
|
|
13
|
+
await rm(TEST_DIR, { recursive: true, force: true });
|
|
14
|
+
await mkdir(TEST_DIR, { recursive: true });
|
|
15
|
+
store = new JsonlStore(TEST_FILE);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
afterEach(async () => {
|
|
19
|
+
await rm(TEST_DIR, { recursive: true, force: true });
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
describe("load", () => {
|
|
23
|
+
it("should return empty map for new file", async () => {
|
|
24
|
+
const docs = await store.load();
|
|
25
|
+
expect(docs.size).toBe(0);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("should load existing JSONL data", async () => {
|
|
29
|
+
await writeFile(TEST_FILE, '{"_id":"1","name":"Alice"}\n{"_id":"2","name":"Bob"}\n');
|
|
30
|
+
const docs = await store.load();
|
|
31
|
+
expect(docs.size).toBe(2);
|
|
32
|
+
expect(docs.get("1")).toEqual({ _id: "1", name: "Alice" });
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("should merge duplicate IDs (last wins)", async () => {
|
|
36
|
+
await writeFile(
|
|
37
|
+
TEST_FILE,
|
|
38
|
+
'{"_id":"1","name":"Alice","age":30}\n{"_id":"1","name":"Alice Updated"}\n',
|
|
39
|
+
);
|
|
40
|
+
const docs = await store.load();
|
|
41
|
+
expect(docs.size).toBe(1);
|
|
42
|
+
expect(docs.get("1")).toEqual({ _id: "1", name: "Alice Updated", age: 30 });
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("should handle $$deleted tombstones", async () => {
|
|
46
|
+
await writeFile(TEST_FILE, '{"_id":"1","name":"Alice"}\n{"_id":"1","$$deleted":true}\n');
|
|
47
|
+
const docs = await store.load();
|
|
48
|
+
expect(docs.size).toBe(0);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("should skip lines without _id", async () => {
|
|
52
|
+
await writeFile(TEST_FILE, '{"name":"no id"}\n{"_id":"1","name":"valid"}\n');
|
|
53
|
+
const docs = await store.load();
|
|
54
|
+
expect(docs.size).toBe(1);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("should skip corrupt lines gracefully", async () => {
|
|
58
|
+
await writeFile(
|
|
59
|
+
TEST_FILE,
|
|
60
|
+
'{"_id":"1","name":"ok"}\n{corrupt json\n{"_id":"2","name":"also ok"}\n',
|
|
61
|
+
);
|
|
62
|
+
const docs = await store.load();
|
|
63
|
+
expect(docs.size).toBe(2);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("should recover from temp file when main file missing", async () => {
|
|
67
|
+
const tempFile = TEST_FILE + "~";
|
|
68
|
+
await writeFile(tempFile, '{"_id":"1","name":"recovered"}\n');
|
|
69
|
+
const docs = await store.load();
|
|
70
|
+
expect(docs.size).toBe(1);
|
|
71
|
+
expect(docs.get("1")!.name).toBe("recovered");
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
describe("getAll / getById / find / findOne", () => {
|
|
76
|
+
beforeEach(async () => {
|
|
77
|
+
await writeFile(
|
|
78
|
+
TEST_FILE,
|
|
79
|
+
'{"_id":"1","name":"Alice","age":30}\n{"_id":"2","name":"Bob","age":25}\n{"_id":"3","name":"Charlie","age":35}\n',
|
|
80
|
+
);
|
|
81
|
+
await store.load();
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("getAll returns all documents", () => {
|
|
85
|
+
expect(store.getAll()).toHaveLength(3);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("getById returns correct doc", () => {
|
|
89
|
+
expect(store.getById("2")).toEqual({ _id: "2", name: "Bob", age: 25 });
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("getById returns undefined for missing id", () => {
|
|
93
|
+
expect(store.getById("999")).toBeUndefined();
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("find returns matching docs", () => {
|
|
97
|
+
const result = store.find((d) => d.age > 28);
|
|
98
|
+
expect(result).toHaveLength(2);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("findOne returns first match", () => {
|
|
102
|
+
const result = store.findOne((d) => d.age > 28);
|
|
103
|
+
expect(result).toBeDefined();
|
|
104
|
+
expect(result!.age).toBeGreaterThan(28);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("findOne returns undefined when no match", () => {
|
|
108
|
+
expect(store.findOne((d) => d.age > 100)).toBeUndefined();
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
describe("append", () => {
|
|
113
|
+
it("should append and return doc with generated id", async () => {
|
|
114
|
+
await store.load();
|
|
115
|
+
const doc = await store.append({ name: "test" } as any);
|
|
116
|
+
expect(doc._id).toBeDefined();
|
|
117
|
+
expect(doc.name).toBe("test");
|
|
118
|
+
expect(store.getAll()).toHaveLength(1);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it("should append with provided _id", async () => {
|
|
122
|
+
await store.load();
|
|
123
|
+
const doc = await store.append({ _id: "custom-id", name: "test" } as any);
|
|
124
|
+
expect(doc._id).toBe("custom-id");
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it("should merge with existing doc of same _id", async () => {
|
|
128
|
+
await store.load();
|
|
129
|
+
await store.append({ _id: "x", name: "first", value: 1 } as any);
|
|
130
|
+
await store.append({ _id: "x", name: "second" } as any);
|
|
131
|
+
const doc = store.getById("x")!;
|
|
132
|
+
expect(doc.name).toBe("second");
|
|
133
|
+
expect(doc.value).toBe(1);
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
describe("updateById", () => {
|
|
138
|
+
it("should update existing doc", async () => {
|
|
139
|
+
await store.load();
|
|
140
|
+
await store.append({ _id: "u1", name: "original", status: "active" } as any);
|
|
141
|
+
await store.updateById("u1", { status: "done" });
|
|
142
|
+
expect(store.getById("u1")!.status).toBe("done");
|
|
143
|
+
expect(store.getById("u1")!.name).toBe("original");
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it("should no-op for non-existent id", async () => {
|
|
147
|
+
await store.load();
|
|
148
|
+
await store.updateById("nonexistent", { status: "done" });
|
|
149
|
+
expect(store.getAll()).toHaveLength(0);
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
describe("deleteById", () => {
|
|
154
|
+
it("should remove doc from memory and write tombstone", async () => {
|
|
155
|
+
await store.load();
|
|
156
|
+
await store.append({ _id: "d1", name: "to delete" } as any);
|
|
157
|
+
expect(store.getAll()).toHaveLength(1);
|
|
158
|
+
|
|
159
|
+
await store.deleteById("d1");
|
|
160
|
+
expect(store.getAll()).toHaveLength(0);
|
|
161
|
+
expect(store.getById("d1")).toBeUndefined();
|
|
162
|
+
|
|
163
|
+
// Tombstone should be written to file
|
|
164
|
+
const content = await readFile(TEST_FILE, "utf-8");
|
|
165
|
+
expect(content).toContain('"$$deleted":true');
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
describe("compact", () => {
|
|
170
|
+
it("should deduplicate entries", async () => {
|
|
171
|
+
await store.load();
|
|
172
|
+
await store.append({ _id: "c1", name: "v1" } as any);
|
|
173
|
+
await store.updateById("c1", { name: "v2" });
|
|
174
|
+
await store.updateById("c1", { name: "v3" });
|
|
175
|
+
|
|
176
|
+
// Before compact: 3 lines
|
|
177
|
+
const before = (await readFile(TEST_FILE, "utf-8")).trim().split("\n");
|
|
178
|
+
expect(before).toHaveLength(3);
|
|
179
|
+
|
|
180
|
+
await store.compact();
|
|
181
|
+
|
|
182
|
+
// After compact: 1 line
|
|
183
|
+
const after = (await readFile(TEST_FILE, "utf-8")).trim().split("\n");
|
|
184
|
+
expect(after).toHaveLength(1);
|
|
185
|
+
expect(JSON.parse(after[0]!).name).toBe("v3");
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it("should handle empty store", async () => {
|
|
189
|
+
await store.load();
|
|
190
|
+
await store.compact();
|
|
191
|
+
const content = await readFile(TEST_FILE, "utf-8");
|
|
192
|
+
expect(content).toBe("");
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
});
|
package/ts/JsonlStore.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { appendFile, mkdir, readFile, rename,
|
|
1
|
+
import { appendFile, mkdir, readFile, rename, writeFile } from "fs/promises";
|
|
2
2
|
import { existsSync } from "fs";
|
|
3
3
|
import { fsyncSync, openSync, closeSync } from "fs";
|
|
4
4
|
import path from "path";
|
|
5
|
-
import { lock
|
|
5
|
+
import { lock } from "proper-lockfile";
|
|
6
6
|
import { logger } from "./logger.ts";
|
|
7
7
|
|
|
8
8
|
export interface JsonlDoc {
|
|
@@ -24,14 +24,11 @@ export interface JsonlDoc {
|
|
|
24
24
|
export class JsonlStore<T extends Record<string, any> = Record<string, any>> {
|
|
25
25
|
private filePath: string;
|
|
26
26
|
private tempPath: string;
|
|
27
|
-
private lockPath: string;
|
|
28
27
|
private docs = new Map<string, T & JsonlDoc>();
|
|
29
28
|
|
|
30
29
|
constructor(filePath: string) {
|
|
31
30
|
this.filePath = filePath;
|
|
32
31
|
this.tempPath = filePath + "~";
|
|
33
|
-
// Lock on the directory (proper-lockfile needs an existing path)
|
|
34
|
-
this.lockPath = path.dirname(filePath);
|
|
35
32
|
}
|
|
36
33
|
|
|
37
34
|
/**
|
|
@@ -169,7 +166,7 @@ export class JsonlStore<T extends Record<string, any> = Record<string, any>> {
|
|
|
169
166
|
async compact(): Promise<void> {
|
|
170
167
|
const lines = Array.from(this.docs.values())
|
|
171
168
|
.map((doc) => {
|
|
172
|
-
const { _id, $$deleted, ...rest } = doc;
|
|
169
|
+
const { _id, $$deleted: _$$deleted, ...rest } = doc;
|
|
173
170
|
return JSON.stringify({ _id, ...rest });
|
|
174
171
|
})
|
|
175
172
|
.join("\n");
|
package/ts/cli.ts
CHANGED
|
@@ -1,9 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
import { argv } from "process";
|
|
3
3
|
import { spawn } from "child_process";
|
|
4
|
-
import { existsSync } from "fs";
|
|
5
|
-
import path from "path";
|
|
6
|
-
import cliYesConfig from "../agent-yes.config.ts";
|
|
7
4
|
import { parseCliArgs } from "./parseCliArgs.ts";
|
|
8
5
|
import { SUPPORTED_CLIS } from "./SUPPORTED_CLIS.ts";
|
|
9
6
|
import { logger } from "./logger.ts";
|
package/ts/configLoader.spec.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach, afterEach } from "
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
2
2
|
import { loadCascadingConfig, getConfigPaths, ensureSchemaInConfigFiles } from "./configLoader.ts";
|
|
3
3
|
import { mkdir, writeFile, readFile, rm } from "node:fs/promises";
|
|
4
4
|
import path from "node:path";
|
|
@@ -187,4 +187,68 @@ configDir: /test
|
|
|
187
187
|
expect(result.skipped).toContain(configPath);
|
|
188
188
|
expect(result.modified).not.toContain(configPath);
|
|
189
189
|
});
|
|
190
|
+
|
|
191
|
+
it("should handle invalid JSON config gracefully", async () => {
|
|
192
|
+
const configPath = path.join(testDir, ".agent-yes.config.json");
|
|
193
|
+
await writeFile(configPath, "not valid json {{{");
|
|
194
|
+
|
|
195
|
+
const config = await loadCascadingConfig({ projectDir: testDir, homeDir: testDir });
|
|
196
|
+
// Should not throw, returns empty
|
|
197
|
+
expect(config).toEqual({});
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it("should handle empty YAML config returning null", async () => {
|
|
201
|
+
const configPath = path.join(testDir, ".agent-yes.config.yaml");
|
|
202
|
+
await writeFile(configPath, "");
|
|
203
|
+
|
|
204
|
+
const config = await loadCascadingConfig({ projectDir: testDir, homeDir: testDir });
|
|
205
|
+
expect(config).toEqual({});
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it("should add schema to YAML with leading comments", async () => {
|
|
209
|
+
const configPath = path.join(testDir, ".agent-yes.config.yaml");
|
|
210
|
+
await writeFile(
|
|
211
|
+
configPath,
|
|
212
|
+
`# My config comment
|
|
213
|
+
# Another comment
|
|
214
|
+
configDir: /test
|
|
215
|
+
`,
|
|
216
|
+
);
|
|
217
|
+
|
|
218
|
+
const result = await ensureSchemaInConfigFiles({ projectDir: testDir, homeDir: testDir });
|
|
219
|
+
expect(result.modified).toContain(configPath);
|
|
220
|
+
const content = await readFile(configPath, "utf-8");
|
|
221
|
+
expect(content).toContain("yaml-language-server:");
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it("should handle ensureSchema when no config files exist", async () => {
|
|
225
|
+
const emptyDir = path.join(testDir, "empty");
|
|
226
|
+
await mkdir(emptyDir, { recursive: true });
|
|
227
|
+
|
|
228
|
+
const result = await ensureSchemaInConfigFiles({ projectDir: emptyDir, homeDir: emptyDir });
|
|
229
|
+
expect(result.modified).toHaveLength(0);
|
|
230
|
+
expect(result.skipped).toHaveLength(0);
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
it("should handle addSchemaReference for JSON that already has schema", async () => {
|
|
234
|
+
const configPath = path.join(testDir, ".agent-yes.config.json");
|
|
235
|
+
const content = JSON.stringify({ $schema: "existing", configDir: "/test" }, null, 2);
|
|
236
|
+
await writeFile(configPath, content);
|
|
237
|
+
|
|
238
|
+
const result = await ensureSchemaInConfigFiles({ projectDir: testDir, homeDir: testDir });
|
|
239
|
+
expect(result.skipped).toContain(configPath);
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
it("should use defaults for getConfigPaths without options", () => {
|
|
243
|
+
const paths = getConfigPaths();
|
|
244
|
+
expect(paths.length).toBeGreaterThan(0);
|
|
245
|
+
expect(paths.some((p) => p.includes(".agent-yes.config.json"))).toBe(true);
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it("should use defaults for loadCascadingConfig without options", async () => {
|
|
249
|
+
// This tests the default path (process.cwd() and os.homedir())
|
|
250
|
+
const config = await loadCascadingConfig();
|
|
251
|
+
// Should not throw, may or may not find configs
|
|
252
|
+
expect(config).toBeDefined();
|
|
253
|
+
});
|
|
190
254
|
});
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { defineCliYesConfig } from "./defineConfig";
|
|
3
|
+
|
|
4
|
+
describe("defineCliYesConfig", () => {
|
|
5
|
+
it("should return a plain config object", async () => {
|
|
6
|
+
const cfg = await defineCliYesConfig({ clis: {} });
|
|
7
|
+
expect(cfg).toEqual({ clis: {} });
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it("should accept a function that receives the default config", async () => {
|
|
11
|
+
const cfg = await defineCliYesConfig((original) => {
|
|
12
|
+
expect(original).toEqual({ clis: {} });
|
|
13
|
+
return { ...original, clis: { claude: { bin: "claude" } } };
|
|
14
|
+
});
|
|
15
|
+
expect(cfg.clis).toHaveProperty("claude");
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("should accept an async function", async () => {
|
|
19
|
+
const cfg = await defineCliYesConfig(async () => {
|
|
20
|
+
return { clis: { test: { bin: "test-cli" } } };
|
|
21
|
+
});
|
|
22
|
+
expect(cfg.clis.test).toEqual({ bin: "test-cli" });
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("should accept a promise", async () => {
|
|
26
|
+
const cfg = await defineCliYesConfig(Promise.resolve({ clis: {} }));
|
|
27
|
+
expect(cfg).toEqual({ clis: {} });
|
|
28
|
+
});
|
|
29
|
+
});
|
package/ts/idleWaiter.spec.ts
CHANGED
|
@@ -52,4 +52,20 @@ describe("IdleWaiter", () => {
|
|
|
52
52
|
const result = waiter.ping().ping().ping();
|
|
53
53
|
expect(result).toBe(waiter);
|
|
54
54
|
});
|
|
55
|
+
|
|
56
|
+
it("should wait until idle period has passed", async () => {
|
|
57
|
+
const waiter = new IdleWaiter();
|
|
58
|
+
waiter.checkInterval = 10;
|
|
59
|
+
|
|
60
|
+
// Ping right now so it's not idle
|
|
61
|
+
waiter.ping();
|
|
62
|
+
|
|
63
|
+
const start = Date.now();
|
|
64
|
+
// Wait for 50ms idle period — the wait loop should actually iterate
|
|
65
|
+
await waiter.wait(50);
|
|
66
|
+
const elapsed = Date.now() - start;
|
|
67
|
+
|
|
68
|
+
// Should have waited at least ~50ms
|
|
69
|
+
expect(elapsed).toBeGreaterThanOrEqual(40);
|
|
70
|
+
});
|
|
55
71
|
});
|
package/ts/index.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { execaCommandSync, parseCommandString } from "execa";
|
|
2
|
-
import {
|
|
2
|
+
import { fromWritable } from "from-node-stream";
|
|
3
3
|
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 { TerminalRenderStream
|
|
7
|
+
import { TerminalRenderStream } from "terminal-render";
|
|
8
8
|
import {
|
|
9
9
|
extractSessionId,
|
|
10
10
|
getSessionForCwd,
|
|
@@ -14,7 +14,7 @@ import pty, { ptyPackage } from "./pty.ts";
|
|
|
14
14
|
import { removeControlCharacters } from "./removeControlCharacters.ts";
|
|
15
15
|
import { acquireLock, releaseLock, shouldUseLock } from "./runningLock.ts";
|
|
16
16
|
import { logger } from "./logger.ts";
|
|
17
|
-
import { createFifoStream } from "./beta/fifo.ts";
|
|
17
|
+
// import { createFifoStream } from "./beta/fifo.ts";
|
|
18
18
|
import { PidStore } from "./pidStore.ts";
|
|
19
19
|
import { SUPPORTED_CLIS } from "./SUPPORTED_CLIS.ts";
|
|
20
20
|
import { sendEnter, sendMessage } from "./core/messaging.ts";
|
|
@@ -26,10 +26,10 @@ import {
|
|
|
26
26
|
} from "./core/logging.ts";
|
|
27
27
|
import { spawnAgent } from "./core/spawner.ts";
|
|
28
28
|
import { AgentContext } from "./core/context.ts";
|
|
29
|
-
import { createAutoResponseHandler } from "./core/responders.ts";
|
|
29
|
+
// import { createAutoResponseHandler } from "./core/responders.ts";
|
|
30
30
|
import { createTerminatorStream } from "./core/streamHelpers.ts";
|
|
31
31
|
import { globalAgentRegistry } from "./agentRegistry.ts";
|
|
32
|
-
import { ReadyManager } from "./ReadyManager.ts";
|
|
32
|
+
// import { ReadyManager } from "./ReadyManager.ts";
|
|
33
33
|
import { notifyWebhook } from "./webhookNotifier.ts";
|
|
34
34
|
|
|
35
35
|
export { removeControlCharacters };
|
|
@@ -119,7 +119,7 @@ export default async function agentYes({
|
|
|
119
119
|
install = false,
|
|
120
120
|
resume = false,
|
|
121
121
|
useSkills = false,
|
|
122
|
-
useStdinAppend = false,
|
|
122
|
+
useStdinAppend: _useStdinAppend = false,
|
|
123
123
|
autoYes = true,
|
|
124
124
|
}: {
|
|
125
125
|
cli: SUPPORTED_CLIS;
|
|
@@ -730,7 +730,7 @@ export default async function agentYes({
|
|
|
730
730
|
process.stdin.on("close", endHandler);
|
|
731
731
|
process.stdin.on("error", errorHandler);
|
|
732
732
|
},
|
|
733
|
-
cancel(
|
|
733
|
+
cancel(_reason) {
|
|
734
734
|
process.stdin.pause();
|
|
735
735
|
},
|
|
736
736
|
});
|
|
@@ -802,6 +802,7 @@ export default async function agentYes({
|
|
|
802
802
|
// Only check for /auto if line is short enough
|
|
803
803
|
if (line.length <= 20) {
|
|
804
804
|
const cleanLine = line
|
|
805
|
+
// oxlint-disable-next-line no-control-regex
|
|
805
806
|
.replace(/[\x00-\x1f]|\x1b\[[0-9;]*[A-Za-z]|\[[A-Z]/g, "")
|
|
806
807
|
.trim();
|
|
807
808
|
if (cleanLine === "/auto") {
|
package/ts/parseCliArgs.ts
CHANGED
|
@@ -2,7 +2,6 @@ import ms from "ms";
|
|
|
2
2
|
import yargs from "yargs";
|
|
3
3
|
import { hideBin } from "yargs/helpers";
|
|
4
4
|
import { SUPPORTED_CLIS } from "./SUPPORTED_CLIS.ts";
|
|
5
|
-
import pkg from "../package.json" with { type: "json" };
|
|
6
5
|
|
|
7
6
|
// const pkg = await JSON.parse(await readFile(path.resolve((import.meta.dir) + "/../package.json"), 'utf8'))
|
|
8
7
|
/**
|
package/ts/pidStore.spec.ts
CHANGED
|
@@ -115,6 +115,20 @@ describe("PidStore", () => {
|
|
|
115
115
|
expect(rec?.exitReason).toBe("crash");
|
|
116
116
|
expect(rec?.exitCode).toBe(1);
|
|
117
117
|
});
|
|
118
|
+
|
|
119
|
+
it("should no-op when updating non-existent pid", async () => {
|
|
120
|
+
await store.updateStatus(999888, "exited");
|
|
121
|
+
// Should not throw and records should be unchanged
|
|
122
|
+
expect(store.getAllRecords()).toHaveLength(0);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it("should update status without extra params", async () => {
|
|
126
|
+
await store.registerProcess({ pid: 333, cli: "test", args: [], cwd: "/tmp" });
|
|
127
|
+
await store.updateStatus(333, "idle");
|
|
128
|
+
const rec = store.getAllRecords().find((r) => r.pid === 333);
|
|
129
|
+
expect(rec?.status).toBe("idle");
|
|
130
|
+
expect(rec?.exitReason).toBe("");
|
|
131
|
+
});
|
|
118
132
|
});
|
|
119
133
|
|
|
120
134
|
describe("getAllRecords", () => {
|
package/ts/rustBinary.ts
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
* Rust binary helper - finds or downloads the appropriate prebuilt binary
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import { existsSync, mkdirSync, unlinkSync
|
|
6
|
-
import { chmod,
|
|
5
|
+
import { existsSync, mkdirSync, unlinkSync } from "fs";
|
|
6
|
+
import { chmod, copyFile } from "fs/promises";
|
|
7
7
|
import path from "path";
|
|
8
8
|
|
|
9
9
|
// Platform/arch to binary name mapping
|
|
@@ -60,7 +60,7 @@ export function getBinDir(): string {
|
|
|
60
60
|
*/
|
|
61
61
|
export function findRustBinary(verbose = false): string | undefined {
|
|
62
62
|
const binaryName = getBinaryName();
|
|
63
|
-
const
|
|
63
|
+
const _baseName = binaryName.replace(/\.exe$/, "");
|
|
64
64
|
|
|
65
65
|
const searchPaths = [
|
|
66
66
|
// 1. Check in npm package bin directory
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { describe, expect, it, vi, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import { compareVersions, fetchLatestVersion, displayVersion } from "./versionChecker";
|
|
3
|
+
|
|
4
|
+
describe("versionChecker", () => {
|
|
5
|
+
describe("compareVersions", () => {
|
|
6
|
+
it("should return 0 for equal versions", () => {
|
|
7
|
+
expect(compareVersions("1.0.0", "1.0.0")).toBe(0);
|
|
8
|
+
expect(compareVersions("2.3.4", "2.3.4")).toBe(0);
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it("should return 1 when v1 > v2", () => {
|
|
12
|
+
expect(compareVersions("2.0.0", "1.0.0")).toBe(1);
|
|
13
|
+
expect(compareVersions("1.1.0", "1.0.0")).toBe(1);
|
|
14
|
+
expect(compareVersions("1.0.1", "1.0.0")).toBe(1);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("should return -1 when v1 < v2", () => {
|
|
18
|
+
expect(compareVersions("1.0.0", "2.0.0")).toBe(-1);
|
|
19
|
+
expect(compareVersions("1.0.0", "1.1.0")).toBe(-1);
|
|
20
|
+
expect(compareVersions("1.0.0", "1.0.1")).toBe(-1);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("should handle versions with different segment counts", () => {
|
|
24
|
+
expect(compareVersions("1.0", "1.0.0")).toBe(0);
|
|
25
|
+
expect(compareVersions("1.0.0", "1.0")).toBe(0);
|
|
26
|
+
expect(compareVersions("1.0.1", "1.0")).toBe(1);
|
|
27
|
+
expect(compareVersions("1.0", "1.0.1")).toBe(-1);
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
describe("fetchLatestVersion", () => {
|
|
32
|
+
beforeEach(() => {
|
|
33
|
+
vi.stubGlobal("fetch", vi.fn());
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
afterEach(() => {
|
|
37
|
+
vi.restoreAllMocks();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("should return version from npm registry", async () => {
|
|
41
|
+
vi.mocked(fetch).mockResolvedValue({
|
|
42
|
+
ok: true,
|
|
43
|
+
json: async () => ({ version: "1.2.3" }),
|
|
44
|
+
} as Response);
|
|
45
|
+
|
|
46
|
+
const version = await fetchLatestVersion();
|
|
47
|
+
expect(version).toBe("1.2.3");
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("should return null on non-ok response", async () => {
|
|
51
|
+
vi.mocked(fetch).mockResolvedValue({
|
|
52
|
+
ok: false,
|
|
53
|
+
} as Response);
|
|
54
|
+
|
|
55
|
+
const version = await fetchLatestVersion();
|
|
56
|
+
expect(version).toBeNull();
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("should return null on network error", async () => {
|
|
60
|
+
vi.mocked(fetch).mockRejectedValue(new Error("network error"));
|
|
61
|
+
|
|
62
|
+
const version = await fetchLatestVersion();
|
|
63
|
+
expect(version).toBeNull();
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
describe("displayVersion", () => {
|
|
68
|
+
beforeEach(() => {
|
|
69
|
+
vi.stubGlobal("fetch", vi.fn());
|
|
70
|
+
vi.spyOn(console, "log").mockImplementation(() => {});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
afterEach(() => {
|
|
74
|
+
vi.restoreAllMocks();
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("should log update available when behind", async () => {
|
|
78
|
+
vi.mocked(fetch).mockResolvedValue({
|
|
79
|
+
ok: true,
|
|
80
|
+
json: async () => ({ version: "999.0.0" }),
|
|
81
|
+
} as Response);
|
|
82
|
+
|
|
83
|
+
await displayVersion();
|
|
84
|
+
|
|
85
|
+
expect(console.log).toHaveBeenCalledWith(expect.stringContaining("update available"));
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("should log latest when versions match", async () => {
|
|
89
|
+
const pkg = await import("../package.json");
|
|
90
|
+
vi.mocked(fetch).mockResolvedValue({
|
|
91
|
+
ok: true,
|
|
92
|
+
json: async () => ({ version: pkg.default.version }),
|
|
93
|
+
} as Response);
|
|
94
|
+
|
|
95
|
+
await displayVersion();
|
|
96
|
+
|
|
97
|
+
expect(console.log).toHaveBeenCalledWith(expect.stringContaining("latest"));
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("should log latest published when ahead", async () => {
|
|
101
|
+
vi.mocked(fetch).mockResolvedValue({
|
|
102
|
+
ok: true,
|
|
103
|
+
json: async () => ({ version: "0.0.1" }),
|
|
104
|
+
} as Response);
|
|
105
|
+
|
|
106
|
+
await displayVersion();
|
|
107
|
+
|
|
108
|
+
expect(console.log).toHaveBeenCalledWith(expect.stringContaining("latest published"));
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("should handle fetch failure gracefully", async () => {
|
|
112
|
+
vi.mocked(fetch).mockRejectedValue(new Error("fail"));
|
|
113
|
+
|
|
114
|
+
await displayVersion();
|
|
115
|
+
|
|
116
|
+
expect(console.log).toHaveBeenCalledWith(expect.stringContaining("unable to check"));
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
});
|