claude-yes 1.147.0 → 1.149.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-CFaRUz70.js +8 -0
- package/dist/{SUPPORTED_CLIS-BFZPtEsl.js → SUPPORTED_CLIS-DmEBbACr.js} +2 -2
- package/dist/cli.js +3 -3
- package/dist/index.js +2 -2
- package/dist/{schedule-Dq0rsd5g.js → schedule-CdJjOPt7.js} +5 -5
- package/dist/{serve-C5nlXrQt.js → serve-j3Jwmk5O.js} +64 -8
- package/dist/{setup-CYnFh4ZX.js → setup-BsVxgyr7.js} +3 -3
- package/dist/{subcommands-CiRgqMjR.js → subcommands-DCsphJXW.js} +1 -1
- package/dist/{subcommands-DAv18AHb.js → subcommands-PCUd7vJf.js} +4 -4
- package/dist/{ts-DKH7E-F6.js → ts-9In7C_H_.js} +2 -2
- package/dist/{versionChecker-BGsjdYf6.js → versionChecker-xwsWu7p6.js} +2 -2
- package/dist/workspaceConfig-B9VX8tVO.js +91 -0
- package/lab/ui/index.html +126 -34
- package/package.json +1 -1
- package/ts/serve.ts +102 -5
- package/ts/workspaceConfig.spec.ts +72 -1
- package/ts/workspaceConfig.ts +43 -0
- package/dist/SUPPORTED_CLIS-BUNhV-WM.js +0 -8
- package/dist/workspaceConfig-B3ylOZAO.js +0 -56
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import "./ts-9In7C_H_.js";
|
|
2
|
+
import "./logger-CDIsZ-Pp.js";
|
|
3
|
+
import "./versionChecker-xwsWu7p6.js";
|
|
4
|
+
import "./pidStore-fqXqTKkh.js";
|
|
5
|
+
import "./globalPidIndex-DlmmJlO8.js";
|
|
6
|
+
import { t as SUPPORTED_CLIS } from "./SUPPORTED_CLIS-DmEBbACr.js";
|
|
7
|
+
|
|
8
|
+
export { SUPPORTED_CLIS };
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { t as CLIS_CONFIG } from "./ts-
|
|
1
|
+
import { t as CLIS_CONFIG } from "./ts-9In7C_H_.js";
|
|
2
2
|
|
|
3
3
|
//#region ts/SUPPORTED_CLIS.ts
|
|
4
4
|
const SUPPORTED_CLIS = Object.keys(CLIS_CONFIG);
|
|
5
5
|
|
|
6
6
|
//#endregion
|
|
7
7
|
export { SUPPORTED_CLIS as t };
|
|
8
|
-
//# sourceMappingURL=SUPPORTED_CLIS-
|
|
8
|
+
//# sourceMappingURL=SUPPORTED_CLIS-DmEBbACr.js.map
|
package/dist/cli.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
import { t as invokedCliName } from "./invokedCli-uqM2YYA7.js";
|
|
3
3
|
import { n as logger } from "./logger-CDIsZ-Pp.js";
|
|
4
|
-
import { i as versionString, n as displayVersion, r as getInstalledPackage, t as checkAndAutoUpdate } from "./versionChecker-
|
|
4
|
+
import { i as versionString, n as displayVersion, r as getInstalledPackage, t as checkAndAutoUpdate } from "./versionChecker-xwsWu7p6.js";
|
|
5
5
|
import { argv } from "process";
|
|
6
6
|
import { execFileSync, spawn } from "child_process";
|
|
7
7
|
import ms from "ms";
|
|
@@ -480,7 +480,7 @@ function buildRustArgs(argv, cliFromScript, supportedClis) {
|
|
|
480
480
|
const rawArg = process.argv[2];
|
|
481
481
|
const managerCommands = !invokedCliName(process.argv);
|
|
482
482
|
const isHelpFlag = rawArg === "-h" || rawArg === "--help";
|
|
483
|
-
const { isSubcommand, runSubcommand, cmdHelp } = await import("./subcommands-
|
|
483
|
+
const { isSubcommand, runSubcommand, cmdHelp } = await import("./subcommands-DCsphJXW.js");
|
|
484
484
|
if (isHelpFlag && process.argv.length === 3) {
|
|
485
485
|
cmdHelp(managerCommands);
|
|
486
486
|
process.exit(0);
|
|
@@ -513,7 +513,7 @@ if (config.useRust) {
|
|
|
513
513
|
}
|
|
514
514
|
}
|
|
515
515
|
if (rustBinary) {
|
|
516
|
-
const { SUPPORTED_CLIS } = await import("./SUPPORTED_CLIS-
|
|
516
|
+
const { SUPPORTED_CLIS } = await import("./SUPPORTED_CLIS-CFaRUz70.js");
|
|
517
517
|
const rustArgs = buildRustArgs(process.argv, config.cli, SUPPORTED_CLIS);
|
|
518
518
|
if (config.verbose) {
|
|
519
519
|
console.log(`[rust] Using binary: ${rustBinary}`);
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { a as removeControlCharacters, i as AgentContext, n as agentYes, r as config, t as CLIS_CONFIG } from "./ts-
|
|
1
|
+
import { a as removeControlCharacters, i as AgentContext, n as agentYes, r as config, t as CLIS_CONFIG } from "./ts-9In7C_H_.js";
|
|
2
2
|
import "./logger-CDIsZ-Pp.js";
|
|
3
|
-
import "./versionChecker-
|
|
3
|
+
import "./versionChecker-xwsWu7p6.js";
|
|
4
4
|
import "./pidStore-fqXqTKkh.js";
|
|
5
5
|
import "./globalPidIndex-DlmmJlO8.js";
|
|
6
6
|
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import "./ts-
|
|
1
|
+
import "./ts-9In7C_H_.js";
|
|
2
2
|
import "./logger-CDIsZ-Pp.js";
|
|
3
|
-
import "./versionChecker-
|
|
3
|
+
import "./versionChecker-xwsWu7p6.js";
|
|
4
4
|
import "./pidStore-fqXqTKkh.js";
|
|
5
5
|
import "./globalPidIndex-DlmmJlO8.js";
|
|
6
|
-
import { t as SUPPORTED_CLIS } from "./SUPPORTED_CLIS-
|
|
7
|
-
import {
|
|
6
|
+
import { t as SUPPORTED_CLIS } from "./SUPPORTED_CLIS-DmEBbACr.js";
|
|
7
|
+
import { i as resolveSpawnCwd } from "./workspaceConfig-B9VX8tVO.js";
|
|
8
8
|
import { createHash } from "node:crypto";
|
|
9
9
|
|
|
10
10
|
//#region ts/oxmgrService.ts
|
|
@@ -141,4 +141,4 @@ async function cmdSchedule(rest) {
|
|
|
141
141
|
|
|
142
142
|
//#endregion
|
|
143
143
|
export { cmdSchedule };
|
|
144
|
-
//# sourceMappingURL=schedule-
|
|
144
|
+
//# sourceMappingURL=schedule-CdJjOPt7.js.map
|
|
@@ -1,15 +1,16 @@
|
|
|
1
|
-
import "./ts-
|
|
1
|
+
import "./ts-9In7C_H_.js";
|
|
2
2
|
import "./logger-CDIsZ-Pp.js";
|
|
3
|
-
import { r as getInstalledPackage } from "./versionChecker-
|
|
3
|
+
import { r as getInstalledPackage } from "./versionChecker-xwsWu7p6.js";
|
|
4
4
|
import "./pidStore-fqXqTKkh.js";
|
|
5
5
|
import { a as updateGlobalPidStatus } from "./globalPidIndex-DlmmJlO8.js";
|
|
6
6
|
import { t as pgidForWrapper } from "./reaper-C-eWAxIj.js";
|
|
7
7
|
import "./configShared-C1C04bbq.js";
|
|
8
|
-
import { t as SUPPORTED_CLIS } from "./SUPPORTED_CLIS-
|
|
8
|
+
import { t as SUPPORTED_CLIS } from "./SUPPORTED_CLIS-DmEBbACr.js";
|
|
9
9
|
import "./e2e-ClOI_aqV.js";
|
|
10
10
|
import "./webrtcLink-BWhuA74k.js";
|
|
11
11
|
import "./remotes-qK6uozO4.js";
|
|
12
|
-
import {
|
|
12
|
+
import { i as resolveSpawnCwd, r as isProvisionAllowed, t as getProvisionRoot } from "./workspaceConfig-B9VX8tVO.js";
|
|
13
|
+
import { S as snapshotStatus, _ as renderRawLog, c as extractTaskCounts, g as readNotes, i as controlCodeFromName, m as listRecords, o as deriveLiveStatus, w as writeToIpc, y as resolveOne } from "./subcommands-PCUd7vJf.js";
|
|
13
14
|
import yargs from "yargs";
|
|
14
15
|
import { mkdir, open, readFile, stat, writeFile } from "fs/promises";
|
|
15
16
|
import { homedir, hostname, userInfo } from "os";
|
|
@@ -20,6 +21,22 @@ import { randomBytes, timingSafeEqual } from "crypto";
|
|
|
20
21
|
|
|
21
22
|
//#region ts/serve.ts
|
|
22
23
|
const DEFAULT_PORT = 7432;
|
|
24
|
+
/**
|
|
25
|
+
* Normalize a user-supplied GitHub-ish source into the standard
|
|
26
|
+
* `<owner>/<repo>/tree/<branch>` path that codehost/provision's `parseSpec`
|
|
27
|
+
* understands. Interim fallback used only when the linked codehost build
|
|
28
|
+
* predates `parseSource` (the canonical normalizer in the standard):
|
|
29
|
+
* https://github.com/o/r/tree/b · github.com/o/r/tree/b · o/r/tree/b
|
|
30
|
+
* o/r@branch · o/r (→ default branch main)
|
|
31
|
+
*/
|
|
32
|
+
function normalizeGithubSource(s) {
|
|
33
|
+
let v = s.trim().replace(/^https?:\/\//, "").replace(/^github\.com\//, "");
|
|
34
|
+
v = v.replace(/[?#].*$/, "").replace(/\.git$/, "").replace(/^\/+|\/+$/g, "");
|
|
35
|
+
const at = v.match(/^([^/]+)\/([^/@]+)@(.+)$/);
|
|
36
|
+
if (at) return `${at[1]}/${at[2]}/tree/${at[3]}`;
|
|
37
|
+
if (/^[^/]+\/[^/]+$/.test(v)) return `${v}/tree/main`;
|
|
38
|
+
return v;
|
|
39
|
+
}
|
|
23
40
|
function agentYesHome() {
|
|
24
41
|
return process.env.AGENT_YES_HOME ?? path.join(homedir(), ".agent-yes");
|
|
25
42
|
}
|
|
@@ -1031,9 +1048,47 @@ Options:
|
|
|
1031
1048
|
}
|
|
1032
1049
|
const cli = String(body.cli ?? "claude");
|
|
1033
1050
|
if (!SUPPORTED_CLIS.includes(cli)) return new Response(`unsupported cli: ${cli}`, { status: 400 });
|
|
1034
|
-
const cwd = typeof body.cwd === "string" && body.cwd ? body.cwd : process.cwd();
|
|
1035
1051
|
const prompt = String(body.prompt ?? "");
|
|
1036
|
-
|
|
1052
|
+
let cwd;
|
|
1053
|
+
let provisioned = null;
|
|
1054
|
+
const from = typeof body.from === "string" ? body.from.trim() : "";
|
|
1055
|
+
if (from) {
|
|
1056
|
+
let prov;
|
|
1057
|
+
try {
|
|
1058
|
+
prov = await import("codehost/provision");
|
|
1059
|
+
} catch (e) {
|
|
1060
|
+
return new Response(`spawn-from needs the 'codehost' package (codehost/provision) — install it (npm i -g codehost) or 'bun link' it for local dev: ${e.message}`, { status: 501 });
|
|
1061
|
+
}
|
|
1062
|
+
let spec;
|
|
1063
|
+
try {
|
|
1064
|
+
spec = typeof prov.parseSource === "function" ? prov.parseSource(from) : prov.parseSpec(normalizeGithubSource(from));
|
|
1065
|
+
} catch {
|
|
1066
|
+
spec = null;
|
|
1067
|
+
}
|
|
1068
|
+
if (!spec) return new Response(`unrecognized spawn source: ${from}`, { status: 400 });
|
|
1069
|
+
if (!isProvisionAllowed(spec.owner, spec.repo)) return new Response(`provisioning '${spec.owner}/${spec.repo}' is not allowed — add the owner to provisionAllowlist in ~/.agent-yes/config.json (or "*" to allow all)`, { status: 403 });
|
|
1070
|
+
let result;
|
|
1071
|
+
try {
|
|
1072
|
+
const wsRoot = getProvisionRoot();
|
|
1073
|
+
result = await prov.provision(spec, wsRoot ? { wsRoot } : void 0);
|
|
1074
|
+
} catch (e) {
|
|
1075
|
+
return new Response(`provision failed: ${e.message}`, { status: 502 });
|
|
1076
|
+
}
|
|
1077
|
+
if (!result?.ok) return new Response(result?.error ?? "provision failed", { status: 502 });
|
|
1078
|
+
cwd = result.folder;
|
|
1079
|
+
provisioned = {
|
|
1080
|
+
action: result.action,
|
|
1081
|
+
folder: result.folder
|
|
1082
|
+
};
|
|
1083
|
+
} else {
|
|
1084
|
+
cwd = resolveSpawnCwd(body.cwd);
|
|
1085
|
+
try {
|
|
1086
|
+
await mkdir(cwd, { recursive: true });
|
|
1087
|
+
} catch (e) {
|
|
1088
|
+
return new Response(`cannot create cwd ${cwd}: ${e.message}`, { status: 500 });
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
process.stderr.write(`→ console spawned: ay ${cli}${prompt ? ` -- "${prompt.slice(0, 60)}"` : ""} (cwd: ${cwd}${provisioned ? `, ${provisioned.action} from ${from}` : ""})\n`);
|
|
1037
1092
|
const ayBin = Bun.which("ay") ?? process.argv[1];
|
|
1038
1093
|
const ayCmd = process.platform === "win32" && ayBin.toLowerCase().endsWith(".exe") ? [ayBin] : [process.execPath, ayBin];
|
|
1039
1094
|
try {
|
|
@@ -1053,7 +1108,8 @@ Options:
|
|
|
1053
1108
|
ok: true,
|
|
1054
1109
|
pid: child.pid,
|
|
1055
1110
|
cli,
|
|
1056
|
-
cwd
|
|
1111
|
+
cwd,
|
|
1112
|
+
...provisioned ? { provisioned } : {}
|
|
1057
1113
|
});
|
|
1058
1114
|
} catch (e) {
|
|
1059
1115
|
return new Response(e.message, { status: 500 });
|
|
@@ -1179,4 +1235,4 @@ Options:
|
|
|
1179
1235
|
|
|
1180
1236
|
//#endregion
|
|
1181
1237
|
export { cmdServe };
|
|
1182
|
-
//# sourceMappingURL=serve-
|
|
1238
|
+
//# sourceMappingURL=serve-j3Jwmk5O.js.map
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { a as setWorkspaceRoot, n as getWorkspaceRoot } from "./workspaceConfig-B9VX8tVO.js";
|
|
2
2
|
import { existsSync } from "node:fs";
|
|
3
3
|
import { stdin, stdout } from "node:process";
|
|
4
4
|
import { createInterface } from "node:readline/promises";
|
|
@@ -32,7 +32,7 @@ async function cmdSetup(rest) {
|
|
|
32
32
|
if (!existsSync(abs)) process.stderr.write(` note: that directory doesn't exist yet — create it, or agents spawned there will fail\n`);
|
|
33
33
|
if (noShare) return 0;
|
|
34
34
|
process.stdout.write(`\nsharing this machine to agent-yes.com…\n`);
|
|
35
|
-
const { cmdServe } = await import("./serve-
|
|
35
|
+
const { cmdServe } = await import("./serve-j3Jwmk5O.js");
|
|
36
36
|
return cmdServe([
|
|
37
37
|
"install",
|
|
38
38
|
"--share",
|
|
@@ -42,4 +42,4 @@ async function cmdSetup(rest) {
|
|
|
42
42
|
|
|
43
43
|
//#endregion
|
|
44
44
|
export { cmdSetup };
|
|
45
|
-
//# sourceMappingURL=setup-
|
|
45
|
+
//# sourceMappingURL=setup-BsVxgyr7.js.map
|
|
@@ -4,6 +4,6 @@ import "./configShared-C1C04bbq.js";
|
|
|
4
4
|
import "./e2e-ClOI_aqV.js";
|
|
5
5
|
import "./webrtcLink-BWhuA74k.js";
|
|
6
6
|
import "./remotes-qK6uozO4.js";
|
|
7
|
-
import { C as stopTipForCli, S as snapshotStatus, _ as renderRawLog, a as cursorAbs, b as resolveReadWindow, c as extractTaskCounts, d as isExitRequest, f as isPidAlive, g as readNotes, h as matchKeyword, i as controlCodeFromName, l as finalizedLines, m as listRecords, n as READ_PAGE_DEFAULT, o as deriveLiveStatus, p as isSubcommand, r as cmdHelp, s as extractNeedsInput, t as GRACEFUL_EXIT_COMMANDS, u as isAgentStuck, v as renderRawLogLines, w as writeToIpc, x as runSubcommand, y as resolveOne } from "./subcommands-
|
|
7
|
+
import { C as stopTipForCli, S as snapshotStatus, _ as renderRawLog, a as cursorAbs, b as resolveReadWindow, c as extractTaskCounts, d as isExitRequest, f as isPidAlive, g as readNotes, h as matchKeyword, i as controlCodeFromName, l as finalizedLines, m as listRecords, n as READ_PAGE_DEFAULT, o as deriveLiveStatus, p as isSubcommand, r as cmdHelp, s as extractNeedsInput, t as GRACEFUL_EXIT_COMMANDS, u as isAgentStuck, v as renderRawLogLines, w as writeToIpc, x as runSubcommand, y as resolveOne } from "./subcommands-PCUd7vJf.js";
|
|
8
8
|
|
|
9
9
|
export { cmdHelp, isSubcommand, runSubcommand };
|
|
@@ -554,15 +554,15 @@ async function runSubcommand(argv) {
|
|
|
554
554
|
case "restart": return await cmdRestart(rest);
|
|
555
555
|
case "note": return await cmdNote(rest);
|
|
556
556
|
case "serve": {
|
|
557
|
-
const { cmdServe } = await import("./serve-
|
|
557
|
+
const { cmdServe } = await import("./serve-j3Jwmk5O.js");
|
|
558
558
|
return cmdServe(rest);
|
|
559
559
|
}
|
|
560
560
|
case "setup": {
|
|
561
|
-
const { cmdSetup } = await import("./setup-
|
|
561
|
+
const { cmdSetup } = await import("./setup-BsVxgyr7.js");
|
|
562
562
|
return cmdSetup(rest);
|
|
563
563
|
}
|
|
564
564
|
case "schedule": {
|
|
565
|
-
const { cmdSchedule } = await import("./schedule-
|
|
565
|
+
const { cmdSchedule } = await import("./schedule-CdJjOPt7.js");
|
|
566
566
|
return cmdSchedule(rest);
|
|
567
567
|
}
|
|
568
568
|
case "remote": {
|
|
@@ -2593,4 +2593,4 @@ async function cmdResultSet(rest) {
|
|
|
2593
2593
|
|
|
2594
2594
|
//#endregion
|
|
2595
2595
|
export { stopTipForCli as C, snapshotStatus as S, renderRawLog as _, cursorAbs as a, resolveReadWindow as b, extractTaskCounts as c, isExitRequest as d, isPidAlive as f, readNotes as g, matchKeyword as h, controlCodeFromName as i, finalizedLines as l, listRecords as m, READ_PAGE_DEFAULT as n, deriveLiveStatus as o, isSubcommand as p, cmdHelp as r, extractNeedsInput as s, GRACEFUL_EXIT_COMMANDS as t, isAgentStuck as u, renderRawLogLines as v, writeToIpc as w, runSubcommand as x, resolveOne as y };
|
|
2596
|
-
//# sourceMappingURL=subcommands-
|
|
2596
|
+
//# sourceMappingURL=subcommands-PCUd7vJf.js.map
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { n as logger, t as addTransport } from "./logger-CDIsZ-Pp.js";
|
|
2
|
-
import { r as getInstalledPackage } from "./versionChecker-
|
|
2
|
+
import { r as getInstalledPackage } from "./versionChecker-xwsWu7p6.js";
|
|
3
3
|
import { t as agentYesHome } from "./agentYesHome-_eJa3DaX.js";
|
|
4
4
|
import { i as shouldUseLock, r as releaseLock, t as acquireLock } from "./runningLock-V4qvXgAw.js";
|
|
5
5
|
import { t as PidStore } from "./pidStore-fqXqTKkh.js";
|
|
@@ -1800,4 +1800,4 @@ function sleep(ms) {
|
|
|
1800
1800
|
|
|
1801
1801
|
//#endregion
|
|
1802
1802
|
export { removeControlCharacters as a, AgentContext as i, agentYes as n, config as r, CLIS_CONFIG as t };
|
|
1803
|
-
//# sourceMappingURL=ts-
|
|
1803
|
+
//# sourceMappingURL=ts-9In7C_H_.js.map
|
|
@@ -7,7 +7,7 @@ import { fileURLToPath } from "url";
|
|
|
7
7
|
|
|
8
8
|
//#region package.json
|
|
9
9
|
var name = "claude-yes";
|
|
10
|
-
var version = "1.
|
|
10
|
+
var version = "1.149.0";
|
|
11
11
|
|
|
12
12
|
//#endregion
|
|
13
13
|
//#region ts/versionChecker.ts
|
|
@@ -215,4 +215,4 @@ async function displayVersion() {
|
|
|
215
215
|
|
|
216
216
|
//#endregion
|
|
217
217
|
export { versionString as i, displayVersion as n, getInstalledPackage as r, checkAndAutoUpdate as t };
|
|
218
|
-
//# sourceMappingURL=versionChecker-
|
|
218
|
+
//# sourceMappingURL=versionChecker-xwsWu7p6.js.map
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { t as agentYesHome } from "./agentYesHome-_eJa3DaX.js";
|
|
2
|
+
import { mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
3
|
+
import { homedir } from "os";
|
|
4
|
+
import path from "path";
|
|
5
|
+
|
|
6
|
+
//#region ts/workspaceConfig.ts
|
|
7
|
+
function configPath() {
|
|
8
|
+
return path.join(agentYesHome(), "config.json");
|
|
9
|
+
}
|
|
10
|
+
function readConfig() {
|
|
11
|
+
try {
|
|
12
|
+
return JSON.parse(readFileSync(configPath(), "utf-8"));
|
|
13
|
+
} catch {
|
|
14
|
+
return {};
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
/** Expand a leading `~` (`~` or `~/x`) to an absolute home-based path. */
|
|
18
|
+
function expandTilde(p) {
|
|
19
|
+
const s = p.trim();
|
|
20
|
+
if (s === "~") return homedir();
|
|
21
|
+
if (s.startsWith("~/") || s.startsWith("~\\")) return path.join(homedir(), s.slice(2));
|
|
22
|
+
return s;
|
|
23
|
+
}
|
|
24
|
+
/** The configured workspace root (absolute), or the home dir if unset. */
|
|
25
|
+
function getWorkspaceRoot() {
|
|
26
|
+
const w = readConfig().workspace;
|
|
27
|
+
return w && w.trim() ? w : homedir();
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Root for `from`-provisioned worktrees, handed to codehost/provision so they
|
|
31
|
+
* land in `<root>/<owner>/<repo>/tree/<branch>`. Resolution order: env
|
|
32
|
+
* `CODEHOST_WS_ROOT` wins (ops override), then the configured `provisionRoot`,
|
|
33
|
+
* else undefined — letting codehost/provision fall back to its own `~/ws`
|
|
34
|
+
* default. Kept separate from `workspace` (the plain-cwd default), which may be a
|
|
35
|
+
* specific project dir rather than a root.
|
|
36
|
+
*/
|
|
37
|
+
function getProvisionRoot() {
|
|
38
|
+
const env = process.env.CODEHOST_WS_ROOT?.trim();
|
|
39
|
+
if (env) return path.resolve(expandTilde(env));
|
|
40
|
+
const r = readConfig().provisionRoot;
|
|
41
|
+
return r && r.trim() ? path.resolve(expandTilde(r)) : void 0;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Owner/repo allowlist for `from`-provisioning. Provisioning clones a repo and
|
|
45
|
+
* runs its setup script (dependency installs + package lifecycle hooks = code
|
|
46
|
+
* execution on the host), so an **empty allowlist means DENY ALL** — a secure
|
|
47
|
+
* default the host opts out of by listing owners it trusts. Entries match
|
|
48
|
+
* `<owner>` (any repo of that owner), `<owner>/<repo>` (exact), or `*` (allow
|
|
49
|
+
* all — an explicit opt-in to the wide-open behavior). Env
|
|
50
|
+
* `CODEHOST_PROVISION_ALLOWLIST` (comma-separated) overrides the config.
|
|
51
|
+
*/
|
|
52
|
+
function getProvisionAllowlist() {
|
|
53
|
+
const env = process.env.CODEHOST_PROVISION_ALLOWLIST?.trim();
|
|
54
|
+
return (env ? env.split(",") : readConfig().provisionAllowlist ?? []).map((s) => s.trim().toLowerCase()).filter(Boolean);
|
|
55
|
+
}
|
|
56
|
+
/** Whether `<owner>/<repo>` may be `from`-provisioned, per {@link getProvisionAllowlist}. */
|
|
57
|
+
function isProvisionAllowed(owner, repo) {
|
|
58
|
+
const list = getProvisionAllowlist();
|
|
59
|
+
if (list.includes("*")) return true;
|
|
60
|
+
const o = owner.toLowerCase();
|
|
61
|
+
const full = `${owner}/${repo}`.toLowerCase();
|
|
62
|
+
return list.some((e) => e.replace(/\/\*$/, "") === o || e === full);
|
|
63
|
+
}
|
|
64
|
+
/** Persist the workspace root, tilde-expanded and resolved to an absolute path. */
|
|
65
|
+
function setWorkspaceRoot(dir) {
|
|
66
|
+
const abs = path.resolve(expandTilde(dir));
|
|
67
|
+
const cfg = readConfig();
|
|
68
|
+
cfg.workspace = abs;
|
|
69
|
+
mkdirSync(agentYesHome(), { recursive: true });
|
|
70
|
+
writeFileSync(configPath(), JSON.stringify(cfg, null, 2));
|
|
71
|
+
return abs;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Resolve a user-supplied spawn location to an absolute cwd:
|
|
75
|
+
* - empty → the workspace root
|
|
76
|
+
* - a bare name → `<workspace>/<name>` (so "myproject" lands under the root)
|
|
77
|
+
* - `~`-prefixed → home-based absolute
|
|
78
|
+
* - anything with a path separator → resolved as-is
|
|
79
|
+
*/
|
|
80
|
+
function resolveSpawnCwd(input) {
|
|
81
|
+
const root = getWorkspaceRoot();
|
|
82
|
+
const v = (input ?? "").trim();
|
|
83
|
+
if (!v) return root;
|
|
84
|
+
if (v.startsWith("~")) return path.resolve(expandTilde(v));
|
|
85
|
+
if (v.includes("/") || v.includes("\\") || path.isAbsolute(v)) return path.resolve(v);
|
|
86
|
+
return path.join(root, v);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
//#endregion
|
|
90
|
+
export { setWorkspaceRoot as a, resolveSpawnCwd as i, getWorkspaceRoot as n, isProvisionAllowed as r, getProvisionRoot as t };
|
|
91
|
+
//# sourceMappingURL=workspaceConfig-B9VX8tVO.js.map
|
package/lab/ui/index.html
CHANGED
|
@@ -550,7 +550,10 @@
|
|
|
550
550
|
position: fixed;
|
|
551
551
|
inset: 0;
|
|
552
552
|
z-index: 30;
|
|
553
|
-
|
|
553
|
+
/* Light scrim only: the omnibox live-previews the highlighted agent in the
|
|
554
|
+
terminal behind it, so the backdrop must stay see-through. The omnibox's
|
|
555
|
+
own panel bg + shadow keep it legible against the brighter terminal. */
|
|
556
|
+
background: rgba(2, 6, 12, 0.32);
|
|
554
557
|
display: flex;
|
|
555
558
|
align-items: flex-start;
|
|
556
559
|
justify-content: center;
|
|
@@ -1490,8 +1493,8 @@
|
|
|
1490
1493
|
<div class="omni-results" id="omni-results"></div>
|
|
1491
1494
|
<div class="omni-foot">
|
|
1492
1495
|
<span
|
|
1493
|
-
><b>↑↓</b>
|
|
1494
|
-
<b>esc</b>
|
|
1496
|
+
><b>↑↓</b> preview <b>⏎</b> open <b>⌘⏎</b> spawn here
|
|
1497
|
+
<b>esc</b> cancel</span
|
|
1495
1498
|
>
|
|
1496
1499
|
</div>
|
|
1497
1500
|
</div>
|
|
@@ -3326,7 +3329,12 @@
|
|
|
3326
3329
|
document.title = docTitle(name, e && e.status); // pure formatter (tested)
|
|
3327
3330
|
}
|
|
3328
3331
|
|
|
3329
|
-
|
|
3332
|
+
// opts.preview — a transient "peek" used by the Cmd+K omnibox to swap the
|
|
3333
|
+
// background terminal to the highlighted candidate WITHOUT committing: it
|
|
3334
|
+
// renders + live-tails the agent exactly like a real open, but skips the
|
|
3335
|
+
// persistence (localStorage/URL hash) and skips stealing keyboard focus from
|
|
3336
|
+
// the omnibox input. Esc later restores the original via a full select().
|
|
3337
|
+
function select(keyOrPid, opts = {}) {
|
|
3330
3338
|
const e =
|
|
3331
3339
|
entries.find((x) => x._key === keyOrPid) ||
|
|
3332
3340
|
entries.find((x) => String(x.pid) === String(keyOrPid));
|
|
@@ -3343,19 +3351,22 @@
|
|
|
3343
3351
|
renderPeerSelections(); // drop any prior agent's peer-selection overlay now
|
|
3344
3352
|
sendPresence(); // announce we're now watching this agent (host TTLs the prior one)
|
|
3345
3353
|
paintHeaderBadge();
|
|
3346
|
-
//
|
|
3347
|
-
|
|
3348
|
-
|
|
3349
|
-
|
|
3350
|
-
//
|
|
3351
|
-
//
|
|
3352
|
-
|
|
3353
|
-
|
|
3354
|
-
|
|
3355
|
-
|
|
3356
|
-
|
|
3357
|
-
|
|
3358
|
-
|
|
3354
|
+
// Persist the selection (refresh re-opens this agent) and reflect it in the
|
|
3355
|
+
// URL hash as #room:pid — but ONLY for a real open. A preview is transient,
|
|
3356
|
+
// so it must not clobber the remembered selection or the deep-link hash;
|
|
3357
|
+
// those still point at the agent we'll restore to on Esc. The fragment is
|
|
3358
|
+
// never sent in any request, so the agent id stays client-side while
|
|
3359
|
+
// surviving a refresh and being copy-pasteable as a deep link.
|
|
3360
|
+
if (!opts.preview) {
|
|
3361
|
+
try {
|
|
3362
|
+
localStorage.setItem("ay.sel", sel);
|
|
3363
|
+
} catch {}
|
|
3364
|
+
try {
|
|
3365
|
+
const want = "#" + e._room + ":" + e.pid;
|
|
3366
|
+
if (location.hash !== want)
|
|
3367
|
+
history.replaceState(null, document.title, location.pathname + want);
|
|
3368
|
+
} catch {}
|
|
3369
|
+
}
|
|
3359
3370
|
// pid + tx are how we talk to the agent's own host; sel (composite) is
|
|
3360
3371
|
// only for UI identity/highlight, since pids can collide across rooms.
|
|
3361
3372
|
const pid = e.pid;
|
|
@@ -3427,7 +3438,9 @@
|
|
|
3427
3438
|
// On a phone, auto-focusing yanks up the soft keyboard the moment you open
|
|
3428
3439
|
// an agent — even when you only meant to read the tail. Skip it there; a tap
|
|
3429
3440
|
// on the terminal still focuses to type. Desktop keeps instant keyboard input.
|
|
3430
|
-
|
|
3441
|
+
// During a preview the omnibox input owns the keyboard (so ↑↓ keep moving the
|
|
3442
|
+
// selection), so never pull focus into the terminal.
|
|
3443
|
+
if (!opts.preview && window.innerWidth > 720) term.focus();
|
|
3431
3444
|
// An agent can rename itself by emitting an OSC 0/2 title sequence
|
|
3432
3445
|
// (\x1b]2;my-name\x07); xterm parses it out of the raw PTY stream we already
|
|
3433
3446
|
// feed it, so we just surface the latest title as the header name. Falls
|
|
@@ -3537,7 +3550,12 @@
|
|
|
3537
3550
|
const close = tx.subscribe(
|
|
3538
3551
|
"/api/tail/" + encodeURIComponent(pid) + "?raw=1",
|
|
3539
3552
|
(raw) => {
|
|
3540
|
-
|
|
3553
|
+
// Guard on selKey: a late event from a just-closed subscription (the
|
|
3554
|
+
// close is async, so one buffered message can still arrive) must NOT
|
|
3555
|
+
// be written into the NEW terminal — otherwise the previous agent's
|
|
3556
|
+
// raw bleeds into the one we just switched to. Preview makes switches
|
|
3557
|
+
// frequent, so this race is no longer theoretical.
|
|
3558
|
+
if (term && sel === selKey) {
|
|
3541
3559
|
term.write(raw);
|
|
3542
3560
|
perfNote("out", raw?.length ?? 0);
|
|
3543
3561
|
}
|
|
@@ -3689,32 +3707,93 @@
|
|
|
3689
3707
|
let omniIdx = 0;
|
|
3690
3708
|
let omniTailTimer = null;
|
|
3691
3709
|
let omniTailSeq = 0;
|
|
3710
|
+
// Live-preview state. While the omnibox is open it swaps the BACKGROUND
|
|
3711
|
+
// terminal to whatever candidate is highlighted, so you can read it through
|
|
3712
|
+
// the (lightened) backdrop. omniReturnSel is the agent that was open when the
|
|
3713
|
+
// omnibox launched — Esc/cancel restores it, commit (Enter/click) keeps the
|
|
3714
|
+
// previewed one. omniWasDetail remembers the mobile list/detail pane so a
|
|
3715
|
+
// cancel lands back where you were. omniPreviewKey dedupes redundant swaps;
|
|
3716
|
+
// omniPreviewTimer debounces them (rebuilding xterm + the tail SSE on every
|
|
3717
|
+
// arrow keypress would thrash the host).
|
|
3718
|
+
let omniReturnSel = null;
|
|
3719
|
+
let omniWasDetail = false;
|
|
3720
|
+
let omniPreviewing = false;
|
|
3721
|
+
let omniPreviewKey = null;
|
|
3722
|
+
let omniPreviewTimer = null;
|
|
3692
3723
|
const omniOpen = () => $("omni").style.display !== "none";
|
|
3693
3724
|
|
|
3694
3725
|
function openOmni() {
|
|
3726
|
+
omniReturnSel = sel;
|
|
3727
|
+
omniWasDetail = document.querySelector(".app").classList.contains("show-detail");
|
|
3728
|
+
omniPreviewing = false;
|
|
3729
|
+
omniPreviewKey = sel; // start deduped against the already-open agent
|
|
3695
3730
|
$("omni").style.display = "flex";
|
|
3696
3731
|
const inp = $("omni-input");
|
|
3697
3732
|
inp.value = "";
|
|
3698
3733
|
inp.focus();
|
|
3699
|
-
runOmni();
|
|
3700
|
-
}
|
|
3701
|
-
|
|
3734
|
+
runOmni(); // no preview yet — the bg stays on the original until you move/type
|
|
3735
|
+
}
|
|
3736
|
+
// restore=true (default — Esc / backdrop click / Cmd+K toggle) reverts the
|
|
3737
|
+
// terminal to the agent that was open before the omnibox, via a full select()
|
|
3738
|
+
// so the original is re-persisted, and undoes any mobile pane flip the preview
|
|
3739
|
+
// caused. restore=false (commit) leaves the previewed agent in place for the
|
|
3740
|
+
// caller (omniActivate) to finalize with its own select().
|
|
3741
|
+
function closeOmni(restore = true) {
|
|
3702
3742
|
if (omniTailTimer) clearTimeout(omniTailTimer);
|
|
3743
|
+
if (omniPreviewTimer) clearTimeout(omniPreviewTimer);
|
|
3744
|
+
omniPreviewTimer = null;
|
|
3703
3745
|
omniTailSeq++; // cancel any in-flight tail search
|
|
3704
3746
|
$("omni").style.display = "none";
|
|
3747
|
+
if (restore && omniPreviewing) {
|
|
3748
|
+
// Re-open + re-persist the original (only if we actually moved away from
|
|
3749
|
+
// it — when previewing the original itself, sel already matches and the
|
|
3750
|
+
// saved selection was never clobbered, so a rebuild would just flicker).
|
|
3751
|
+
if (omniReturnSel && sel !== omniReturnSel) select(omniReturnSel);
|
|
3752
|
+
// Either way restore the mobile list/detail state the preview flipped.
|
|
3753
|
+
document.querySelector(".app").classList.toggle("show-detail", omniWasDetail);
|
|
3754
|
+
}
|
|
3755
|
+
omniPreviewing = false;
|
|
3705
3756
|
}
|
|
3706
3757
|
|
|
3707
|
-
// The agent the spawn action is anchored to. PINNED to the agent
|
|
3708
|
-
//
|
|
3709
|
-
//
|
|
3710
|
-
//
|
|
3758
|
+
// The agent the spawn action is anchored to. PINNED to the agent that was open
|
|
3759
|
+
// when the omnibox launched (omniReturnSel) — NOT the live `sel`, which now
|
|
3760
|
+
// moves as you arrow-preview candidates — so the spawn cwd/cli stays put as you
|
|
3761
|
+
// browse. Falls back to the top result when nothing was open.
|
|
3711
3762
|
function omniAnchor() {
|
|
3712
|
-
const cur =
|
|
3763
|
+
const cur = omniReturnSel ? entries.find((x) => x._key === omniReturnSel) : null;
|
|
3713
3764
|
if (cur) return cur;
|
|
3714
3765
|
const first = omniRows.find((r) => r.kind === "agent");
|
|
3715
3766
|
return first ? first.entry : null;
|
|
3716
3767
|
}
|
|
3717
3768
|
|
|
3769
|
+
// Which agent the highlighted candidate should preview in the background: an
|
|
3770
|
+
// agent row → that agent; the spawn row / an empty list → the launch-time
|
|
3771
|
+
// agent (so the backdrop shows the spawn target's cwd).
|
|
3772
|
+
function omniPreviewTarget() {
|
|
3773
|
+
const row = omniRows[omniIdx];
|
|
3774
|
+
if (row && row.kind === "agent") return row.entry._key;
|
|
3775
|
+
return omniReturnSel;
|
|
3776
|
+
}
|
|
3777
|
+
// Swap the background terminal to the highlighted candidate. Debounced so a
|
|
3778
|
+
// held arrow key doesn't rebuild xterm + reconnect the tail on every repeat,
|
|
3779
|
+
// and deduped by key so re-renders (filtering, tail-search inserts) don't
|
|
3780
|
+
// re-peek the agent already shown. Called only on explicit user moves (arrow /
|
|
3781
|
+
// type), never on open, so the bg stays on the original until you act.
|
|
3782
|
+
function omniPreview() {
|
|
3783
|
+
const key = omniPreviewTarget();
|
|
3784
|
+
if (key === omniPreviewKey) return; // already targeting this agent
|
|
3785
|
+
omniPreviewKey = key;
|
|
3786
|
+
if (omniPreviewTimer) clearTimeout(omniPreviewTimer);
|
|
3787
|
+
omniPreviewTimer = setTimeout(() => {
|
|
3788
|
+
omniPreviewTimer = null;
|
|
3789
|
+
if (!omniOpen()) return; // closed during the debounce window
|
|
3790
|
+
if (!key || key === sel) return; // nothing to peek / already showing it
|
|
3791
|
+
omniPreviewing = true;
|
|
3792
|
+
select(key, { preview: true });
|
|
3793
|
+
$("omni-input").focus(); // select() must not steal the keyboard
|
|
3794
|
+
}, 120);
|
|
3795
|
+
}
|
|
3796
|
+
|
|
3718
3797
|
function renderOmni() {
|
|
3719
3798
|
const q = $("omni-input").value.trim();
|
|
3720
3799
|
if (omniIdx >= omniRows.length) omniIdx = Math.max(0, omniRows.length - 1);
|
|
@@ -3803,8 +3882,12 @@
|
|
|
3803
3882
|
: esc(raw);
|
|
3804
3883
|
const at = omniRows.findIndex((r) => r.kind === "spawn");
|
|
3805
3884
|
const row = { kind: "agent", entry: e, snippet };
|
|
3806
|
-
if (at >= 0)
|
|
3807
|
-
|
|
3885
|
+
if (at >= 0) {
|
|
3886
|
+
omniRows.splice(at, 0, row);
|
|
3887
|
+
// Keep the highlight (and thus the preview) anchored to the SAME row:
|
|
3888
|
+
// an insert at/above omniIdx shifts our row down one, so follow it.
|
|
3889
|
+
if (at <= omniIdx) omniIdx++;
|
|
3890
|
+
} else omniRows.push(row);
|
|
3808
3891
|
}),
|
|
3809
3892
|
);
|
|
3810
3893
|
if (seq === omniTailSeq) renderOmni();
|
|
@@ -3814,13 +3897,14 @@
|
|
|
3814
3897
|
if (!omniRows.length) return;
|
|
3815
3898
|
omniIdx = (omniIdx + d + omniRows.length) % omniRows.length;
|
|
3816
3899
|
renderOmni();
|
|
3900
|
+
omniPreview();
|
|
3817
3901
|
}
|
|
3818
3902
|
async function omniActivate(forceSpawn) {
|
|
3819
3903
|
const q = $("omni-input").value.trim();
|
|
3820
3904
|
const row = omniRows[omniIdx];
|
|
3821
3905
|
if (forceSpawn || (row && row.kind === "spawn")) {
|
|
3822
3906
|
const anchor = omniAnchor();
|
|
3823
|
-
closeOmni();
|
|
3907
|
+
closeOmni(false); // committing — keep the bg as-is, spawnAndSelect takes over
|
|
3824
3908
|
await spawnAndSelect(
|
|
3825
3909
|
{ cli: anchor?.cli || "claude", cwd: anchor?.cwd || undefined, prompt: q || undefined },
|
|
3826
3910
|
anchor?._room,
|
|
@@ -3828,12 +3912,17 @@
|
|
|
3828
3912
|
return;
|
|
3829
3913
|
}
|
|
3830
3914
|
if (row && row.kind === "agent") {
|
|
3831
|
-
closeOmni();
|
|
3915
|
+
closeOmni(false); // committing the previewed agent — don't restore the original
|
|
3832
3916
|
select(row.entry._key);
|
|
3833
3917
|
}
|
|
3834
3918
|
}
|
|
3835
3919
|
|
|
3836
|
-
|
|
3920
|
+
// Typing re-filters AND re-previews the new top match (user-initiated, so a
|
|
3921
|
+
// bg swap here is expected); the open's own runOmni() call is silent.
|
|
3922
|
+
$("omni-input").addEventListener("input", () => {
|
|
3923
|
+
runOmni();
|
|
3924
|
+
omniPreview();
|
|
3925
|
+
});
|
|
3837
3926
|
$("omni-results").addEventListener("click", (ev) => {
|
|
3838
3927
|
const r = ev.target.closest(".omni-row");
|
|
3839
3928
|
if (!r) return;
|
|
@@ -4245,6 +4334,7 @@
|
|
|
4245
4334
|
const before = new Set(entries.filter((e) => e._room === target.id).map((e) => e.pid));
|
|
4246
4335
|
const res = await target.tx.post("/api/spawn", {
|
|
4247
4336
|
cli: spec.cli || "claude",
|
|
4337
|
+
from: spec.from || undefined,
|
|
4248
4338
|
cwd: spec.cwd || undefined,
|
|
4249
4339
|
prompt: spec.prompt || undefined,
|
|
4250
4340
|
});
|
|
@@ -4296,7 +4386,8 @@
|
|
|
4296
4386
|
$("newform").innerHTML = `<div class="lcard">
|
|
4297
4387
|
<div class="ltitle">New agent · ${esc(where)}</div>
|
|
4298
4388
|
<div class="nfield"><label>CLI</label><input id="nf-cli" value="claude" spellcheck="false" autocapitalize="off" /></div>
|
|
4299
|
-
<div class="nfield"><label>
|
|
4389
|
+
<div class="nfield"><label>Spawn from — optional</label><input id="nf-from" placeholder="github URL / owner/repo@branch — provisions a worktree" spellcheck="false" autocapitalize="off" /></div>
|
|
4390
|
+
<div class="nfield"><label>Working dir</label><input id="nf-cwd" value="${esc(cwd)}" placeholder="(host default — ignored when Spawn from is set)" spellcheck="false" autocapitalize="off" /></div>
|
|
4300
4391
|
<div class="nfield"><label>Prompt — optional</label><textarea id="nf-prompt" rows="3" placeholder="initial message to send the agent…"></textarea></div>
|
|
4301
4392
|
<div class="lrow">
|
|
4302
4393
|
<button class="lfleet" id="nf-go">▷ Launch</button>
|
|
@@ -4312,6 +4403,7 @@
|
|
|
4312
4403
|
if (!go || go.disabled) return;
|
|
4313
4404
|
const spec = {
|
|
4314
4405
|
cli: ($("nf-cli").value || "claude").trim(),
|
|
4406
|
+
from: $("nf-from")?.value.trim() || "",
|
|
4315
4407
|
cwd: $("nf-cwd").value.trim(),
|
|
4316
4408
|
prompt: $("nf-prompt").value,
|
|
4317
4409
|
};
|
|
@@ -4320,7 +4412,7 @@
|
|
|
4320
4412
|
// target fleet. A 2nd `claude -c` in the same repo fights over the same
|
|
4321
4413
|
// session/files and usually exits on startup — which looks like "the new
|
|
4322
4414
|
// agent never appears". Let the user pick a different dir or proceed.
|
|
4323
|
-
if (spec.cwd) {
|
|
4415
|
+
if (spec.cwd && !spec.from) {
|
|
4324
4416
|
await loadList();
|
|
4325
4417
|
const norm = (p) => (p || "").replace(/\/+$/, "");
|
|
4326
4418
|
const busy = entries.some(
|
package/package.json
CHANGED
package/ts/serve.ts
CHANGED
|
@@ -21,9 +21,33 @@ import { updateGlobalPidStatus } from "./globalPidIndex.ts";
|
|
|
21
21
|
import { pgidForWrapper } from "./reaper.ts";
|
|
22
22
|
import { SUPPORTED_CLIS } from "./SUPPORTED_CLIS.ts";
|
|
23
23
|
import { getInstalledPackage } from "./versionChecker.ts";
|
|
24
|
+
import { getProvisionRoot, isProvisionAllowed, resolveSpawnCwd } from "./workspaceConfig.ts";
|
|
24
25
|
|
|
25
26
|
const DEFAULT_PORT = 7432;
|
|
26
27
|
|
|
28
|
+
/**
|
|
29
|
+
* Normalize a user-supplied GitHub-ish source into the standard
|
|
30
|
+
* `<owner>/<repo>/tree/<branch>` path that codehost/provision's `parseSpec`
|
|
31
|
+
* understands. Interim fallback used only when the linked codehost build
|
|
32
|
+
* predates `parseSource` (the canonical normalizer in the standard):
|
|
33
|
+
* https://github.com/o/r/tree/b · github.com/o/r/tree/b · o/r/tree/b
|
|
34
|
+
* o/r@branch · o/r (→ default branch main)
|
|
35
|
+
*/
|
|
36
|
+
function normalizeGithubSource(s: string): string {
|
|
37
|
+
let v = s
|
|
38
|
+
.trim()
|
|
39
|
+
.replace(/^https?:\/\//, "")
|
|
40
|
+
.replace(/^github\.com\//, "");
|
|
41
|
+
v = v
|
|
42
|
+
.replace(/[?#].*$/, "")
|
|
43
|
+
.replace(/\.git$/, "")
|
|
44
|
+
.replace(/^\/+|\/+$/g, "");
|
|
45
|
+
const at = v.match(/^([^/]+)\/([^/@]+)@(.+)$/);
|
|
46
|
+
if (at) return `${at[1]}/${at[2]}/tree/${at[3]}`;
|
|
47
|
+
if (/^[^/]+\/[^/]+$/.test(v)) return `${v}/tree/main`;
|
|
48
|
+
return v;
|
|
49
|
+
}
|
|
50
|
+
|
|
27
51
|
function agentYesHome(): string {
|
|
28
52
|
return process.env.AGENT_YES_HOME ?? path.join(homedir(), ".agent-yes");
|
|
29
53
|
}
|
|
@@ -1448,9 +1472,13 @@ export async function cmdServe(rest: string[]): Promise<number> {
|
|
|
1448
1472
|
return Response.json(live);
|
|
1449
1473
|
}
|
|
1450
1474
|
|
|
1451
|
-
// POST /api/spawn body {cli, cwd
|
|
1475
|
+
// POST /api/spawn body {cli, cwd?, from?, prompt?} — launch a new agent.
|
|
1476
|
+
// `from` (a GitHub URL / owner/repo@branch / owner/repo/tree/branch) is
|
|
1477
|
+
// resolved to a ready worktree via codehost/provision (the shared workspace
|
|
1478
|
+
// standard); otherwise `cwd` is resolved against the workspace root and
|
|
1479
|
+
// created if missing (Layer-0 plain-dir provisioning — no more ENOENT 500).
|
|
1452
1480
|
if (req.method === "POST" && p === "/api/spawn") {
|
|
1453
|
-
let body: { cli?: string; cwd?: string; prompt?: string };
|
|
1481
|
+
let body: { cli?: string; cwd?: string; from?: string; prompt?: string };
|
|
1454
1482
|
try {
|
|
1455
1483
|
body = (await req.json()) as typeof body;
|
|
1456
1484
|
} catch {
|
|
@@ -1459,10 +1487,73 @@ export async function cmdServe(rest: string[]): Promise<number> {
|
|
|
1459
1487
|
const cli = String(body.cli ?? "claude");
|
|
1460
1488
|
if (!SUPPORTED_CLIS.includes(cli as never))
|
|
1461
1489
|
return new Response(`unsupported cli: ${cli}`, { status: 400 });
|
|
1462
|
-
const cwd = typeof body.cwd === "string" && body.cwd ? body.cwd : process.cwd();
|
|
1463
1490
|
const prompt = String(body.prompt ?? "");
|
|
1491
|
+
|
|
1492
|
+
// Resolve the working directory. A `from` source is provisioned (clone /
|
|
1493
|
+
// worktree) through codehost/provision; a plain `cwd` is resolved to the
|
|
1494
|
+
// workspace root and mkdir-p'd so a missing dir no longer ENOENTs.
|
|
1495
|
+
let cwd: string;
|
|
1496
|
+
let provisioned: { action: string; folder: string } | null = null;
|
|
1497
|
+
const from = typeof body.from === "string" ? body.from.trim() : "";
|
|
1498
|
+
if (from) {
|
|
1499
|
+
type Spec = { owner: string; repo: string; branch: string };
|
|
1500
|
+
let prov: {
|
|
1501
|
+
parseSource?: (s: string) => Spec | null;
|
|
1502
|
+
parseSpec: (s: string) => Spec | null;
|
|
1503
|
+
provision: (
|
|
1504
|
+
spec: Spec,
|
|
1505
|
+
opts?: { wsRoot?: string },
|
|
1506
|
+
) => Promise<{ ok: boolean; folder: string; action: string; error?: string }>;
|
|
1507
|
+
};
|
|
1508
|
+
try {
|
|
1509
|
+
prov = (await import("codehost/provision")) as typeof prov;
|
|
1510
|
+
} catch (e) {
|
|
1511
|
+
return new Response(
|
|
1512
|
+
`spawn-from needs the 'codehost' package (codehost/provision) — install it ` +
|
|
1513
|
+
`(npm i -g codehost) or 'bun link' it for local dev: ${(e as Error).message}`,
|
|
1514
|
+
{ status: 501 },
|
|
1515
|
+
);
|
|
1516
|
+
}
|
|
1517
|
+
// Malformed input (e.g. bad %-encoding) must surface as 400, not a 500.
|
|
1518
|
+
let spec: Spec | null;
|
|
1519
|
+
try {
|
|
1520
|
+
spec =
|
|
1521
|
+
typeof prov.parseSource === "function"
|
|
1522
|
+
? prov.parseSource(from)
|
|
1523
|
+
: prov.parseSpec(normalizeGithubSource(from));
|
|
1524
|
+
} catch {
|
|
1525
|
+
spec = null;
|
|
1526
|
+
}
|
|
1527
|
+
if (!spec) return new Response(`unrecognized spawn source: ${from}`, { status: 400 });
|
|
1528
|
+
// Provisioning clones the repo and runs its setup script (dependency
|
|
1529
|
+
// installs + package lifecycle hooks = code execution on the host), so
|
|
1530
|
+
// gate it behind an owner/repo allowlist — empty allowlist = deny all.
|
|
1531
|
+
if (!isProvisionAllowed(spec.owner, spec.repo))
|
|
1532
|
+
return new Response(
|
|
1533
|
+
`provisioning '${spec.owner}/${spec.repo}' is not allowed — add the owner to ` +
|
|
1534
|
+
`provisionAllowlist in ~/.agent-yes/config.json (or "*" to allow all)`,
|
|
1535
|
+
{ status: 403 },
|
|
1536
|
+
);
|
|
1537
|
+
let result: { ok: boolean; folder: string; action: string; error?: string };
|
|
1538
|
+
try {
|
|
1539
|
+
const wsRoot = getProvisionRoot();
|
|
1540
|
+
result = await prov.provision(spec, wsRoot ? { wsRoot } : undefined);
|
|
1541
|
+
} catch (e) {
|
|
1542
|
+
return new Response(`provision failed: ${(e as Error).message}`, { status: 502 });
|
|
1543
|
+
}
|
|
1544
|
+
if (!result?.ok) return new Response(result?.error ?? "provision failed", { status: 502 });
|
|
1545
|
+
cwd = result.folder;
|
|
1546
|
+
provisioned = { action: result.action, folder: result.folder };
|
|
1547
|
+
} else {
|
|
1548
|
+
cwd = resolveSpawnCwd(body.cwd);
|
|
1549
|
+
try {
|
|
1550
|
+
await mkdir(cwd, { recursive: true });
|
|
1551
|
+
} catch (e) {
|
|
1552
|
+
return new Response(`cannot create cwd ${cwd}: ${(e as Error).message}`, { status: 500 });
|
|
1553
|
+
}
|
|
1554
|
+
}
|
|
1464
1555
|
process.stderr.write(
|
|
1465
|
-
`→ console spawned: ay ${cli}${prompt ? ` -- "${prompt.slice(0, 60)}"` : ""} (cwd: ${cwd})\n`,
|
|
1556
|
+
`→ console spawned: ay ${cli}${prompt ? ` -- "${prompt.slice(0, 60)}"` : ""} (cwd: ${cwd}${provisioned ? `, ${provisioned.action} from ${from}` : ""})\n`,
|
|
1466
1557
|
);
|
|
1467
1558
|
// Resolve `ay` to an absolute command. The detached daemon (oxmgr/launchd/
|
|
1468
1559
|
// pm2) usually has a PATH WITHOUT ~/.bun/bin, so a bare "ay" fails with
|
|
@@ -1483,7 +1574,13 @@ export async function cmdServe(rest: string[]): Promise<number> {
|
|
|
1483
1574
|
stderr: "ignore",
|
|
1484
1575
|
});
|
|
1485
1576
|
child.unref();
|
|
1486
|
-
return Response.json({
|
|
1577
|
+
return Response.json({
|
|
1578
|
+
ok: true,
|
|
1579
|
+
pid: child.pid,
|
|
1580
|
+
cli,
|
|
1581
|
+
cwd,
|
|
1582
|
+
...(provisioned ? { provisioned } : {}),
|
|
1583
|
+
});
|
|
1487
1584
|
} catch (e) {
|
|
1488
1585
|
return new Response((e as Error).message, { status: 500 });
|
|
1489
1586
|
}
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
2
|
-
import { mkdtempSync, rmSync } from "fs";
|
|
2
|
+
import { mkdtempSync, rmSync, writeFileSync } from "fs";
|
|
3
3
|
import { homedir, tmpdir } from "os";
|
|
4
4
|
import path from "path";
|
|
5
5
|
import {
|
|
6
6
|
expandTilde,
|
|
7
|
+
getProvisionAllowlist,
|
|
8
|
+
getProvisionRoot,
|
|
7
9
|
getWorkspaceRoot,
|
|
10
|
+
isProvisionAllowed,
|
|
8
11
|
resolveSpawnCwd,
|
|
9
12
|
setWorkspaceRoot,
|
|
10
13
|
} from "./workspaceConfig.ts";
|
|
@@ -20,6 +23,8 @@ describe("workspaceConfig", () => {
|
|
|
20
23
|
afterEach(() => {
|
|
21
24
|
if (original === undefined) delete process.env.AGENT_YES_HOME;
|
|
22
25
|
else process.env.AGENT_YES_HOME = original;
|
|
26
|
+
delete process.env.CODEHOST_WS_ROOT;
|
|
27
|
+
delete process.env.CODEHOST_PROVISION_ALLOWLIST;
|
|
23
28
|
rmSync(tmp, { recursive: true, force: true });
|
|
24
29
|
});
|
|
25
30
|
|
|
@@ -71,4 +76,70 @@ describe("workspaceConfig", () => {
|
|
|
71
76
|
expect(resolveSpawnCwd("a/b")).toBe(path.resolve("a/b"));
|
|
72
77
|
});
|
|
73
78
|
});
|
|
79
|
+
|
|
80
|
+
const writeConfig = (c: Record<string, unknown>) =>
|
|
81
|
+
writeFileSync(path.join(tmp, "config.json"), JSON.stringify(c));
|
|
82
|
+
|
|
83
|
+
describe("getProvisionRoot", () => {
|
|
84
|
+
it("is undefined when neither env nor config is set", () => {
|
|
85
|
+
expect(getProvisionRoot()).toBeUndefined();
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("returns the configured provisionRoot, resolved", () => {
|
|
89
|
+
writeConfig({ provisionRoot: "/code" });
|
|
90
|
+
expect(getProvisionRoot()).toBe(path.resolve("/code"));
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("env CODEHOST_WS_ROOT overrides the config and expands ~", () => {
|
|
94
|
+
writeConfig({ provisionRoot: "/code" });
|
|
95
|
+
process.env.CODEHOST_WS_ROOT = "~/ws";
|
|
96
|
+
expect(getProvisionRoot()).toBe(path.join(homedir(), "ws"));
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("ignores a blank configured value", () => {
|
|
100
|
+
writeConfig({ provisionRoot: " " });
|
|
101
|
+
expect(getProvisionRoot()).toBeUndefined();
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
describe("getProvisionAllowlist", () => {
|
|
106
|
+
it("is empty when unset", () => {
|
|
107
|
+
expect(getProvisionAllowlist()).toEqual([]);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("reads and normalizes the configured list (trim/lowercase/drop empties)", () => {
|
|
111
|
+
writeConfig({ provisionAllowlist: [" Snomiao ", "Acme/Repo", ""] });
|
|
112
|
+
expect(getProvisionAllowlist()).toEqual(["snomiao", "acme/repo"]);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("env CODEHOST_PROVISION_ALLOWLIST (comma-separated) overrides config", () => {
|
|
116
|
+
writeConfig({ provisionAllowlist: ["snomiao"] });
|
|
117
|
+
process.env.CODEHOST_PROVISION_ALLOWLIST = "Foo, bar/baz ,";
|
|
118
|
+
expect(getProvisionAllowlist()).toEqual(["foo", "bar/baz"]);
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
describe("isProvisionAllowed", () => {
|
|
123
|
+
it("denies everything when the allowlist is empty (secure default)", () => {
|
|
124
|
+
expect(isProvisionAllowed("snomiao", "agent-yes")).toBe(false);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it("'*' allows any owner/repo", () => {
|
|
128
|
+
writeConfig({ provisionAllowlist: ["*"] });
|
|
129
|
+
expect(isProvisionAllowed("anyone", "anything")).toBe(true);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it("matches by owner case-insensitively and rejects others", () => {
|
|
133
|
+
writeConfig({ provisionAllowlist: ["snomiao"] });
|
|
134
|
+
expect(isProvisionAllowed("SNOMIAO", "x")).toBe(true);
|
|
135
|
+
expect(isProvisionAllowed("evil", "x")).toBe(false);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it("matches an exact owner/repo and an owner/* glob", () => {
|
|
139
|
+
writeConfig({ provisionAllowlist: ["acme/widget", "org/*"] });
|
|
140
|
+
expect(isProvisionAllowed("acme", "widget")).toBe(true);
|
|
141
|
+
expect(isProvisionAllowed("acme", "other")).toBe(false);
|
|
142
|
+
expect(isProvisionAllowed("org", "anything")).toBe(true);
|
|
143
|
+
});
|
|
144
|
+
});
|
|
74
145
|
});
|
package/ts/workspaceConfig.ts
CHANGED
|
@@ -15,6 +15,10 @@ import { agentYesHome } from "./agentYesHome.ts";
|
|
|
15
15
|
|
|
16
16
|
interface Config {
|
|
17
17
|
workspace?: string;
|
|
18
|
+
/** Root for `from`-provisioned worktrees (`<root>/<owner>/<repo>/tree/<branch>`). */
|
|
19
|
+
provisionRoot?: string;
|
|
20
|
+
/** Owners/repos permitted for `from`-provisioning; empty = deny all. */
|
|
21
|
+
provisionAllowlist?: string[];
|
|
18
22
|
}
|
|
19
23
|
|
|
20
24
|
function configPath(): string {
|
|
@@ -43,6 +47,45 @@ export function getWorkspaceRoot(): string {
|
|
|
43
47
|
return w && w.trim() ? w : homedir();
|
|
44
48
|
}
|
|
45
49
|
|
|
50
|
+
/**
|
|
51
|
+
* Root for `from`-provisioned worktrees, handed to codehost/provision so they
|
|
52
|
+
* land in `<root>/<owner>/<repo>/tree/<branch>`. Resolution order: env
|
|
53
|
+
* `CODEHOST_WS_ROOT` wins (ops override), then the configured `provisionRoot`,
|
|
54
|
+
* else undefined — letting codehost/provision fall back to its own `~/ws`
|
|
55
|
+
* default. Kept separate from `workspace` (the plain-cwd default), which may be a
|
|
56
|
+
* specific project dir rather than a root.
|
|
57
|
+
*/
|
|
58
|
+
export function getProvisionRoot(): string | undefined {
|
|
59
|
+
const env = process.env.CODEHOST_WS_ROOT?.trim();
|
|
60
|
+
if (env) return path.resolve(expandTilde(env));
|
|
61
|
+
const r = readConfig().provisionRoot;
|
|
62
|
+
return r && r.trim() ? path.resolve(expandTilde(r)) : undefined;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Owner/repo allowlist for `from`-provisioning. Provisioning clones a repo and
|
|
67
|
+
* runs its setup script (dependency installs + package lifecycle hooks = code
|
|
68
|
+
* execution on the host), so an **empty allowlist means DENY ALL** — a secure
|
|
69
|
+
* default the host opts out of by listing owners it trusts. Entries match
|
|
70
|
+
* `<owner>` (any repo of that owner), `<owner>/<repo>` (exact), or `*` (allow
|
|
71
|
+
* all — an explicit opt-in to the wide-open behavior). Env
|
|
72
|
+
* `CODEHOST_PROVISION_ALLOWLIST` (comma-separated) overrides the config.
|
|
73
|
+
*/
|
|
74
|
+
export function getProvisionAllowlist(): string[] {
|
|
75
|
+
const env = process.env.CODEHOST_PROVISION_ALLOWLIST?.trim();
|
|
76
|
+
const raw = env ? env.split(",") : (readConfig().provisionAllowlist ?? []);
|
|
77
|
+
return raw.map((s) => s.trim().toLowerCase()).filter(Boolean);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Whether `<owner>/<repo>` may be `from`-provisioned, per {@link getProvisionAllowlist}. */
|
|
81
|
+
export function isProvisionAllowed(owner: string, repo: string): boolean {
|
|
82
|
+
const list = getProvisionAllowlist();
|
|
83
|
+
if (list.includes("*")) return true;
|
|
84
|
+
const o = owner.toLowerCase();
|
|
85
|
+
const full = `${owner}/${repo}`.toLowerCase();
|
|
86
|
+
return list.some((e) => e.replace(/\/\*$/, "") === o || e === full);
|
|
87
|
+
}
|
|
88
|
+
|
|
46
89
|
/** Persist the workspace root, tilde-expanded and resolved to an absolute path. */
|
|
47
90
|
export function setWorkspaceRoot(dir: string): string {
|
|
48
91
|
const abs = path.resolve(expandTilde(dir));
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
import "./ts-DKH7E-F6.js";
|
|
2
|
-
import "./logger-CDIsZ-Pp.js";
|
|
3
|
-
import "./versionChecker-BGsjdYf6.js";
|
|
4
|
-
import "./pidStore-fqXqTKkh.js";
|
|
5
|
-
import "./globalPidIndex-DlmmJlO8.js";
|
|
6
|
-
import { t as SUPPORTED_CLIS } from "./SUPPORTED_CLIS-BFZPtEsl.js";
|
|
7
|
-
|
|
8
|
-
export { SUPPORTED_CLIS };
|
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
import { t as agentYesHome } from "./agentYesHome-_eJa3DaX.js";
|
|
2
|
-
import { mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
3
|
-
import { homedir } from "os";
|
|
4
|
-
import path from "path";
|
|
5
|
-
|
|
6
|
-
//#region ts/workspaceConfig.ts
|
|
7
|
-
function configPath() {
|
|
8
|
-
return path.join(agentYesHome(), "config.json");
|
|
9
|
-
}
|
|
10
|
-
function readConfig() {
|
|
11
|
-
try {
|
|
12
|
-
return JSON.parse(readFileSync(configPath(), "utf-8"));
|
|
13
|
-
} catch {
|
|
14
|
-
return {};
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
/** Expand a leading `~` (`~` or `~/x`) to an absolute home-based path. */
|
|
18
|
-
function expandTilde(p) {
|
|
19
|
-
const s = p.trim();
|
|
20
|
-
if (s === "~") return homedir();
|
|
21
|
-
if (s.startsWith("~/") || s.startsWith("~\\")) return path.join(homedir(), s.slice(2));
|
|
22
|
-
return s;
|
|
23
|
-
}
|
|
24
|
-
/** The configured workspace root (absolute), or the home dir if unset. */
|
|
25
|
-
function getWorkspaceRoot() {
|
|
26
|
-
const w = readConfig().workspace;
|
|
27
|
-
return w && w.trim() ? w : homedir();
|
|
28
|
-
}
|
|
29
|
-
/** Persist the workspace root, tilde-expanded and resolved to an absolute path. */
|
|
30
|
-
function setWorkspaceRoot(dir) {
|
|
31
|
-
const abs = path.resolve(expandTilde(dir));
|
|
32
|
-
const cfg = readConfig();
|
|
33
|
-
cfg.workspace = abs;
|
|
34
|
-
mkdirSync(agentYesHome(), { recursive: true });
|
|
35
|
-
writeFileSync(configPath(), JSON.stringify(cfg, null, 2));
|
|
36
|
-
return abs;
|
|
37
|
-
}
|
|
38
|
-
/**
|
|
39
|
-
* Resolve a user-supplied spawn location to an absolute cwd:
|
|
40
|
-
* - empty → the workspace root
|
|
41
|
-
* - a bare name → `<workspace>/<name>` (so "myproject" lands under the root)
|
|
42
|
-
* - `~`-prefixed → home-based absolute
|
|
43
|
-
* - anything with a path separator → resolved as-is
|
|
44
|
-
*/
|
|
45
|
-
function resolveSpawnCwd(input) {
|
|
46
|
-
const root = getWorkspaceRoot();
|
|
47
|
-
const v = (input ?? "").trim();
|
|
48
|
-
if (!v) return root;
|
|
49
|
-
if (v.startsWith("~")) return path.resolve(expandTilde(v));
|
|
50
|
-
if (v.includes("/") || v.includes("\\") || path.isAbsolute(v)) return path.resolve(v);
|
|
51
|
-
return path.join(root, v);
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
//#endregion
|
|
55
|
-
export { resolveSpawnCwd as n, setWorkspaceRoot as r, getWorkspaceRoot as t };
|
|
56
|
-
//# sourceMappingURL=workspaceConfig-B3ylOZAO.js.map
|