toyon 0.1.0 → 0.2.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/README.md +3 -3
- package/dist/bridge.js +1 -1
- package/dist/cli.js +26 -2
- package/dist/daemon.js +1326 -295
- package/dist/shell/assets/{Editor-Cn-KsoFr.js → Editor-D-PlbMSY.js} +148 -148
- package/dist/shell/assets/{XTerm-DSSGYyFZ.js → XTerm-CM_FJEwN.js} +13 -13
- package/dist/shell/assets/{cssMode-BU_otbjh.js → cssMode-Bkqyr9Lr.js} +1 -1
- package/dist/shell/assets/{freemarker2-C-NGmKSC.js → freemarker2-DVAK5zoJ.js} +1 -1
- package/dist/shell/assets/{handlebars-BWvVdY_e.js → handlebars-Bdtj7uMl.js} +1 -1
- package/dist/shell/assets/{html-CBO-8nU2.js → html-B1pqTvyA.js} +1 -1
- package/dist/shell/assets/{htmlMode-Dwz6npQ-.js → htmlMode-CBtvzewS.js} +1 -1
- package/dist/shell/assets/index-GBQLHG7z.css +1 -0
- package/dist/shell/assets/index-jy_zeFGs.js +230 -0
- package/dist/shell/assets/{javascript-__evPE7m.js → javascript-FNavUzsm.js} +1 -1
- package/dist/shell/assets/{jsonMode-DpbQTbP6.js → jsonMode-HgW1GXQH.js} +1 -1
- package/dist/shell/assets/{liquid-CgOzfjEh.js → liquid-6wkVlojW.js} +1 -1
- package/dist/shell/assets/{lspLanguageFeatures-DM57zeAM.js → lspLanguageFeatures-CvNG6b_1.js} +1 -1
- package/dist/shell/assets/{mdx-DxN7Bq5u.js → mdx-C-byKky3.js} +1 -1
- package/dist/shell/assets/{python-C3rGYQJF.js → python-BEJMbP4i.js} +1 -1
- package/dist/shell/assets/{razor-C5rkTP_6.js → razor-DeXWW25R.js} +1 -1
- package/dist/shell/assets/{tsMode-DDCDv-RH.js → tsMode-DMwJnvD-.js} +1 -1
- package/dist/shell/assets/{typescript-CyHgeKIZ.js → typescript-Cj57ccSp.js} +1 -1
- package/dist/shell/assets/{workers-C7XbwGBM.js → workers-BZi9jqBp.js} +1 -1
- package/dist/shell/assets/{xml-Dmw92JAf.js → xml-DUDeQI0I.js} +1 -1
- package/dist/shell/assets/{yaml-Bj_aHuSs.js → yaml-BCD8Shg5.js} +1 -1
- package/dist/shell/index.html +2 -2
- package/package.json +1 -1
- package/dist/shell/assets/index-B61fMQI2.css +0 -1
- package/dist/shell/assets/index-Mb4zTZaD.js +0 -230
package/dist/daemon.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// @bun
|
|
2
2
|
// ../daemon/src/index.ts
|
|
3
|
-
import { rmSync as
|
|
3
|
+
import { rmSync as rmSync6, writeFileSync as writeFileSync8 } from "fs";
|
|
4
4
|
import { dirname as dirname9 } from "path";
|
|
5
5
|
import { fileURLToPath } from "url";
|
|
6
6
|
|
|
@@ -40,6 +40,10 @@ var CONFIG_DIR = ".toyon";
|
|
|
40
40
|
function isLocalConfigFile(rel) {
|
|
41
41
|
return rel === CONFIG_FILES.folder.local || rel === CONFIG_FILES.root.local;
|
|
42
42
|
}
|
|
43
|
+
function configSibling(rel, kind) {
|
|
44
|
+
const place = Object.values(CONFIG_FILES).find((p) => p.shared === rel || p.local === rel) ?? CONFIG_FILES.folder;
|
|
45
|
+
return place[kind];
|
|
46
|
+
}
|
|
43
47
|
|
|
44
48
|
// ../shared/src/daemon.ts
|
|
45
49
|
var DAEMON_FILES = {
|
|
@@ -183,7 +187,7 @@ function streamKey(worktreeId, stream) {
|
|
|
183
187
|
}
|
|
184
188
|
|
|
185
189
|
// ../shared/src/protocol/limits.ts
|
|
186
|
-
var PROTOCOL_VERSION =
|
|
190
|
+
var PROTOCOL_VERSION = 50;
|
|
187
191
|
var FILE_MAX_CHARS = 5000000;
|
|
188
192
|
var IMAGE_MIME_TYPES = ["image/png", "image/jpeg", "image/gif", "image/webp"];
|
|
189
193
|
var IMAGE_MAX_EDGE = 2576;
|
|
@@ -6221,6 +6225,7 @@ var toyonConfigSchema = object({
|
|
|
6221
6225
|
setup: array(shellCommand).max(50).optional(),
|
|
6222
6226
|
run: record(declaredProcName, shellCommand),
|
|
6223
6227
|
check: shellCommand.optional(),
|
|
6228
|
+
afterLand: array(shellCommand).max(50).optional(),
|
|
6224
6229
|
land: landConfigSchema.optional(),
|
|
6225
6230
|
preview: procName.optional(),
|
|
6226
6231
|
profiles: record(procName, runProfileSchema).optional(),
|
|
@@ -6255,6 +6260,7 @@ var variantSchema = object({ group: string2(), index: number2().int().min(1), of
|
|
|
6255
6260
|
var clientMsgSchema = discriminatedUnion("t", [
|
|
6256
6261
|
object({ t: literal("subscribe"), worktreeId: id }),
|
|
6257
6262
|
object({ t: literal("unsubscribe"), worktreeId: id }),
|
|
6263
|
+
object({ t: literal("view"), worktreeId: id.nullable() }),
|
|
6258
6264
|
object({
|
|
6259
6265
|
t: literal("chat"),
|
|
6260
6266
|
worktreeId: id,
|
|
@@ -6267,7 +6273,6 @@ var clientMsgSchema = discriminatedUnion("t", [
|
|
|
6267
6273
|
clientId: string2().max(64).optional(),
|
|
6268
6274
|
repoId: id,
|
|
6269
6275
|
prompt,
|
|
6270
|
-
baseWorktreeId: id.optional(),
|
|
6271
6276
|
variant: variantSchema.optional(),
|
|
6272
6277
|
context: prose.optional(),
|
|
6273
6278
|
attachments,
|
|
@@ -6275,7 +6280,8 @@ var clientMsgSchema = discriminatedUnion("t", [
|
|
|
6275
6280
|
profile: string2().max(100).optional(),
|
|
6276
6281
|
mode: permissionModeSchema.optional(),
|
|
6277
6282
|
model: string2().max(200).optional(),
|
|
6278
|
-
effort: string2().max(100).optional()
|
|
6283
|
+
effort: string2().max(100).optional(),
|
|
6284
|
+
carry: boolean2().optional()
|
|
6279
6285
|
}),
|
|
6280
6286
|
object({
|
|
6281
6287
|
t: literal("batch-worktrees"),
|
|
@@ -6294,7 +6300,8 @@ var clientMsgSchema = discriminatedUnion("t", [
|
|
|
6294
6300
|
object({
|
|
6295
6301
|
t: literal("restore-worktree"),
|
|
6296
6302
|
archiveId: id,
|
|
6297
|
-
clientId: string2().max(64).optional()
|
|
6303
|
+
clientId: string2().max(64).optional(),
|
|
6304
|
+
message: object({ text: prose, attachments }).optional()
|
|
6298
6305
|
}),
|
|
6299
6306
|
object({ t: literal("delete-archived"), archiveId: id }),
|
|
6300
6307
|
object({
|
|
@@ -6310,6 +6317,8 @@ var clientMsgSchema = discriminatedUnion("t", [
|
|
|
6310
6317
|
object({ t: literal("commit"), worktreeId: id, message: string2().max(5000) }),
|
|
6311
6318
|
object({ t: literal("graft"), targetId: id, sourceIds: array(id).min(1).max(20) }),
|
|
6312
6319
|
object({ t: literal("sync-main"), worktreeId: id }),
|
|
6320
|
+
object({ t: literal("run-after-land"), repoId: id }),
|
|
6321
|
+
object({ t: literal("restart-daemon") }),
|
|
6313
6322
|
object({ t: literal("pull-main"), worktreeId: id }),
|
|
6314
6323
|
object({
|
|
6315
6324
|
t: literal("write-file"),
|
|
@@ -6337,7 +6346,12 @@ var clientMsgSchema = discriminatedUnion("t", [
|
|
|
6337
6346
|
object({ t: literal("unqueue"), worktreeId: id, index: number2().int().min(0) }),
|
|
6338
6347
|
object({ t: literal("changed-ranges"), worktreeId: id, path: relPath }),
|
|
6339
6348
|
object({ t: literal("rename-worktree"), worktreeId: id, title: string2().min(1).max(200) }),
|
|
6340
|
-
object({
|
|
6349
|
+
object({
|
|
6350
|
+
t: literal("confirm-config"),
|
|
6351
|
+
repoId: id,
|
|
6352
|
+
config: toyonConfigSchema,
|
|
6353
|
+
kind: _enum(["shared", "local"])
|
|
6354
|
+
}),
|
|
6341
6355
|
object({ t: literal("register-repo"), path: string2().min(1).max(4000) }),
|
|
6342
6356
|
object({
|
|
6343
6357
|
t: literal("create-repo"),
|
|
@@ -6393,6 +6407,7 @@ var clientMsgSchema = discriminatedUnion("t", [
|
|
|
6393
6407
|
object({ t: literal("exec"), worktreeId: id, command: shellCommand.min(1) }),
|
|
6394
6408
|
object({ t: literal("exec-stop"), worktreeId: id }),
|
|
6395
6409
|
object({ t: literal("search-refs"), repoId: id, query: string2().max(200) }),
|
|
6410
|
+
object({ t: literal("search-chats"), repoId: id, query: string2().max(200) }),
|
|
6396
6411
|
object({
|
|
6397
6412
|
t: literal("open-ref"),
|
|
6398
6413
|
clientId: string2().max(64).optional(),
|
|
@@ -7558,7 +7573,7 @@ function canGraft(wt) {
|
|
|
7558
7573
|
// ../daemon/package.json
|
|
7559
7574
|
var package_default = {
|
|
7560
7575
|
name: "@toyon/daemon",
|
|
7561
|
-
version: "0.
|
|
7576
|
+
version: "0.2.0",
|
|
7562
7577
|
private: true,
|
|
7563
7578
|
type: "module",
|
|
7564
7579
|
main: "src/index.ts",
|
|
@@ -11885,6 +11900,7 @@ var EXT = {
|
|
|
11885
11900
|
};
|
|
11886
11901
|
var ID = /^[A-Za-z0-9_-]{1,64}$/;
|
|
11887
11902
|
var FILE = /^\d{1,6}\.(png|jpg|gif|webp|txt)$/;
|
|
11903
|
+
var isAttachmentFile = (file) => FILE.test(file);
|
|
11888
11904
|
function attachmentsDirFor(attachmentsDir, worktreeId) {
|
|
11889
11905
|
return join(attachmentsDir, worktreeId);
|
|
11890
11906
|
}
|
|
@@ -12480,6 +12496,7 @@ function makePaths(home = process.env.TOYON_HOME ?? join3(homedir(), ".toyon"))
|
|
|
12480
12496
|
stateFile: join3(home, DAEMON_FILES.state),
|
|
12481
12497
|
tokenFile: join3(home, DAEMON_FILES.token),
|
|
12482
12498
|
pidFile: join3(home, DAEMON_FILES.pid),
|
|
12499
|
+
logFile: join3(home, DAEMON_FILES.log),
|
|
12483
12500
|
remoteFile: join3(home, DAEMON_FILES.remote),
|
|
12484
12501
|
transcriptsDir: join3(home, "transcripts"),
|
|
12485
12502
|
attachmentsDir: join3(home, "attachments"),
|
|
@@ -12506,11 +12523,17 @@ function ensureDirs(p) {
|
|
|
12506
12523
|
// ../daemon/src/git/exclude.ts
|
|
12507
12524
|
import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync } from "fs";
|
|
12508
12525
|
import { dirname } from "path";
|
|
12509
|
-
async function
|
|
12526
|
+
async function excludeFile(cwd) {
|
|
12510
12527
|
const r = await git(cwd, "rev-parse", "--path-format=absolute", "--git-path", "info/exclude");
|
|
12511
|
-
if (
|
|
12512
|
-
return
|
|
12513
|
-
|
|
12528
|
+
if (r.ok && r.out)
|
|
12529
|
+
return r.out;
|
|
12530
|
+
log.warn("git", `cannot locate info/exclude for ${cwd}: ${r.err}`);
|
|
12531
|
+
return null;
|
|
12532
|
+
}
|
|
12533
|
+
async function excludeFromGit(cwd, pattern) {
|
|
12534
|
+
const file = await excludeFile(cwd);
|
|
12535
|
+
if (!file)
|
|
12536
|
+
return;
|
|
12514
12537
|
const current = existsSync3(file) ? readFileSync3(file, "utf8") : "";
|
|
12515
12538
|
if (current.split(`
|
|
12516
12539
|
`).some((l) => l.trim() === pattern))
|
|
@@ -12521,6 +12544,18 @@ async function excludeFromGit(cwd, pattern) {
|
|
|
12521
12544
|
` : ""}${pattern}
|
|
12522
12545
|
`);
|
|
12523
12546
|
}
|
|
12547
|
+
async function unexcludeFromGit(cwd, pattern) {
|
|
12548
|
+
const file = await excludeFile(cwd);
|
|
12549
|
+
if (!file || !existsSync3(file))
|
|
12550
|
+
return;
|
|
12551
|
+
const lines = readFileSync3(file, "utf8").split(`
|
|
12552
|
+
`);
|
|
12553
|
+
const kept = lines.filter((l) => l.trim() !== pattern);
|
|
12554
|
+
if (kept.length === lines.length)
|
|
12555
|
+
return;
|
|
12556
|
+
writeFileSync(file, kept.join(`
|
|
12557
|
+
`));
|
|
12558
|
+
}
|
|
12524
12559
|
|
|
12525
12560
|
// ../daemon/src/agent/bounds.ts
|
|
12526
12561
|
import { existsSync as existsSync4, realpathSync } from "fs";
|
|
@@ -13013,7 +13048,7 @@ var SYSTEM_APPEND = [
|
|
|
13013
13048
|
"Never run `git commit` unless the user explicitly asks you to; leave changes uncommitted for the user to review and commit themselves.",
|
|
13014
13049
|
"Keep the scope tight: do the asked task well, then stop. Suggest follow-ups in chat instead of expanding scope.",
|
|
13015
13050
|
"`@some/path` in a message means read that file or directory first; `@changes` means this worktree's uncommitted files, which `git status` lists.",
|
|
13016
|
-
`Toyon previews the project in a browser panel by running the commands in its settings file, .toyon/settings.json (or toyon.json at the repo root, in a repo that keeps it there), shaped like {"setup": ["bun install"], "run": {"web": "bun run dev --port $PORT"}, "check": "bun run check", "land": {"route": "merge"}}: setup runs once in every new worktree, every command in run keeps running and must listen on $PORT, which toyon sets differently for each worktree, check (optional) runs after each of your turns and must pass before the work can
|
|
13051
|
+
`Toyon previews the project in a browser panel by running the commands in its settings file, .toyon/settings.json (or toyon.json at the repo root, in a repo that keeps it there), shaped like {"setup": ["bun install"], "run": {"web": "bun run dev --port $PORT"}, "check": "bun run check", "land": {"route": "merge"}}: setup runs once in every new worktree, every command in run keeps running and must listen on $PORT, which toyon sets differently for each worktree, check (optional) runs after each of your turns and must pass before the work can land, and land (optional) says how the user lands work on main: route "merge" (onto main here), "push" (onto main here, then pushed) or "pr" (a pull request); method "merge", "squash" or "rebase" for how the commits arrive on main; automerge true when GitHub should merge the pull request itself. A settings.local.json beside it (toyon.local.json at the root) holds one person's overrides and is never committed.`,
|
|
13017
13052
|
"When you scaffold a project, write its .gitignore (dependencies, build output, local env files) before installing anything, so an install never leaves thousands of untracked files for the user to wade through or commit.",
|
|
13018
13053
|
"When you scaffold a project or change how it installs or starts, finish by updating the settings file Toyon already reads, or writing .toyon/settings.json when there is none, so the preview can run it."
|
|
13019
13054
|
].join(" ");
|
|
@@ -13050,10 +13085,11 @@ function buildPrompt(text, context, prefix, attachments = []) {
|
|
|
13050
13085
|
}
|
|
13051
13086
|
for (const a of attachments)
|
|
13052
13087
|
blocks.push(...attachmentBlocks(a));
|
|
13053
|
-
|
|
13054
|
-
blocks.push(textBlock(prefix ? `${prefix}
|
|
13088
|
+
const body = [prefix, text].filter(Boolean).join(`
|
|
13055
13089
|
|
|
13056
|
-
|
|
13090
|
+
`);
|
|
13091
|
+
if (!leads && body)
|
|
13092
|
+
blocks.push(textBlock(body));
|
|
13057
13093
|
if (context && !leads)
|
|
13058
13094
|
blocks.push(textBlock(context));
|
|
13059
13095
|
return blocks;
|
|
@@ -13077,26 +13113,41 @@ ${a.text}
|
|
|
13077
13113
|
}
|
|
13078
13114
|
|
|
13079
13115
|
// ../daemon/src/agent/transcript.ts
|
|
13080
|
-
import { existsSync as existsSync7, readFileSync as readFileSync6
|
|
13081
|
-
import { appendFile
|
|
13116
|
+
import { existsSync as existsSync7, readFileSync as readFileSync6 } from "fs";
|
|
13117
|
+
import { appendFile } from "fs/promises";
|
|
13082
13118
|
import { join as join6 } from "path";
|
|
13083
|
-
var MAX_ENTRIES = 4000;
|
|
13084
|
-
var KEEP_ENTRIES = 2000;
|
|
13085
13119
|
function transcriptPathFor(transcriptsDir, worktreeId) {
|
|
13086
13120
|
return join6(transcriptsDir, `${worktreeId}.jsonl`);
|
|
13087
13121
|
}
|
|
13088
|
-
function
|
|
13089
|
-
const
|
|
13090
|
-
for (
|
|
13091
|
-
const
|
|
13092
|
-
|
|
13093
|
-
|
|
13122
|
+
function coalesce(entries) {
|
|
13123
|
+
const out = [];
|
|
13124
|
+
for (const entry of entries) {
|
|
13125
|
+
const last = out.at(-1);
|
|
13126
|
+
const { event } = entry;
|
|
13127
|
+
if ((event.type === "text-delta" || event.type === "thinking-delta") && last?.event.type === event.type) {
|
|
13128
|
+
out[out.length - 1] = { seq: last.seq, event: { type: event.type, text: last.event.text + event.text } };
|
|
13129
|
+
} else {
|
|
13130
|
+
out.push(entry);
|
|
13131
|
+
}
|
|
13094
13132
|
}
|
|
13095
|
-
return
|
|
13133
|
+
return out;
|
|
13096
13134
|
}
|
|
13097
|
-
function
|
|
13098
|
-
|
|
13099
|
-
|
|
13135
|
+
function parseTranscript(text, tag) {
|
|
13136
|
+
const out = [];
|
|
13137
|
+
let torn = 0;
|
|
13138
|
+
for (const line of text.split(`
|
|
13139
|
+
`)) {
|
|
13140
|
+
if (!line)
|
|
13141
|
+
continue;
|
|
13142
|
+
try {
|
|
13143
|
+
out.push(JSON.parse(line));
|
|
13144
|
+
} catch {
|
|
13145
|
+
torn++;
|
|
13146
|
+
}
|
|
13147
|
+
}
|
|
13148
|
+
if (torn)
|
|
13149
|
+
log.warn(tag, `transcript: skipped ${torn} unparsable line(s)`);
|
|
13150
|
+
return out;
|
|
13100
13151
|
}
|
|
13101
13152
|
|
|
13102
13153
|
class Transcript {
|
|
@@ -13112,31 +13163,7 @@ class Transcript {
|
|
|
13112
13163
|
this.seq = (this.entries.at(-1)?.seq ?? -1) + 1;
|
|
13113
13164
|
}
|
|
13114
13165
|
read() {
|
|
13115
|
-
|
|
13116
|
-
return [];
|
|
13117
|
-
const out = [];
|
|
13118
|
-
let torn = 0;
|
|
13119
|
-
for (const line of readFileSync6(this.path, "utf8").split(`
|
|
13120
|
-
`)) {
|
|
13121
|
-
if (!line)
|
|
13122
|
-
continue;
|
|
13123
|
-
try {
|
|
13124
|
-
out.push(JSON.parse(line));
|
|
13125
|
-
} catch {
|
|
13126
|
-
torn++;
|
|
13127
|
-
}
|
|
13128
|
-
}
|
|
13129
|
-
if (torn)
|
|
13130
|
-
log.warn(this.tag, `transcript: skipped ${torn} unparsable line(s)`);
|
|
13131
|
-
if (out.length > MAX_ENTRIES) {
|
|
13132
|
-
const kept = out.slice(cutPoint(out));
|
|
13133
|
-
const tmp = `${this.path}.tmp`;
|
|
13134
|
-
writeFileSync4(tmp, serialize(kept));
|
|
13135
|
-
renameSync2(tmp, this.path);
|
|
13136
|
-
log.info(this.tag, `transcript: compacted ${out.length} entries to ${kept.length}`);
|
|
13137
|
-
return kept;
|
|
13138
|
-
}
|
|
13139
|
-
return out;
|
|
13166
|
+
return existsSync7(this.path) ? parseTranscript(readFileSync6(this.path, "utf8"), this.tag) : [];
|
|
13140
13167
|
}
|
|
13141
13168
|
append(event) {
|
|
13142
13169
|
const entry = { seq: this.seq++, event };
|
|
@@ -13144,19 +13171,8 @@ class Transcript {
|
|
|
13144
13171
|
const line = `${JSON.stringify(entry)}
|
|
13145
13172
|
`;
|
|
13146
13173
|
this.writes = this.writes.then(() => appendFile(this.path, line)).catch((e) => log.warn(this.tag, "transcript append failed", e));
|
|
13147
|
-
if (this.entries.length > MAX_ENTRIES)
|
|
13148
|
-
this.compact();
|
|
13149
13174
|
return entry;
|
|
13150
13175
|
}
|
|
13151
|
-
compact() {
|
|
13152
|
-
this.entries.splice(0, cutPoint(this.entries));
|
|
13153
|
-
const snapshot = serialize(this.entries);
|
|
13154
|
-
const tmp = `${this.path}.tmp`;
|
|
13155
|
-
this.writes = this.writes.then(async () => {
|
|
13156
|
-
await writeFile2(tmp, snapshot);
|
|
13157
|
-
await rename(tmp, this.path);
|
|
13158
|
-
}).catch((e) => log.warn(this.tag, "transcript compaction failed", e));
|
|
13159
|
-
}
|
|
13160
13176
|
flush() {
|
|
13161
13177
|
return this.writes;
|
|
13162
13178
|
}
|
|
@@ -13929,7 +13945,7 @@ class AcpSession {
|
|
|
13929
13945
|
describe(e) {
|
|
13930
13946
|
if (isAuthRequired(e))
|
|
13931
13947
|
return this.conn?.spec.loginHint ?? this.d.spec().loginHint;
|
|
13932
|
-
const message = e instanceof Error ? e.message : String(e);
|
|
13948
|
+
const message = (e instanceof Error ? e.message : String(e)).replace(/^Internal error: /, "");
|
|
13933
13949
|
return /connection closed/i.test(message) ? this.conn?.link.exitInfo() ?? message : message;
|
|
13934
13950
|
}
|
|
13935
13951
|
async record({ text, attachments }) {
|
|
@@ -14611,24 +14627,31 @@ function setsGrant(line) {
|
|
|
14611
14627
|
// ../daemon/src/runtime/proxy.ts
|
|
14612
14628
|
var DIAL_TIMEOUT_MS = 5000;
|
|
14613
14629
|
var frame = (ev) => typeof ev.data === "string" ? ev.data : new Uint8Array(ev.data);
|
|
14630
|
+
var VITE_PING = "text/x-vite-ping";
|
|
14614
14631
|
function previewHandler(opts) {
|
|
14615
14632
|
const hostPart = (t) => t.host.includes(":") ? `[${t.host}]` : t.host;
|
|
14616
14633
|
return {
|
|
14617
14634
|
async fetch(req, upgrade) {
|
|
14618
14635
|
const url = new URL(req.url);
|
|
14619
|
-
const target = opts.getTarget();
|
|
14620
14636
|
if (url.pathname === "/__toyon/bridge.js") {
|
|
14621
14637
|
return new Response(opts.bridgeScript(), {
|
|
14622
14638
|
headers: { "content-type": "text/javascript", "cache-control": "no-store" }
|
|
14623
14639
|
});
|
|
14624
14640
|
}
|
|
14641
|
+
const isUpgrade = req.headers.get("upgrade")?.toLowerCase() === "websocket";
|
|
14642
|
+
const machine = isUpgrade || req.headers.get("accept") === VITE_PING;
|
|
14643
|
+
if (!machine)
|
|
14644
|
+
opts.onRequest?.();
|
|
14645
|
+
let target = opts.getTarget();
|
|
14646
|
+
if (target == null && !machine && opts.ready)
|
|
14647
|
+
target = await opts.ready();
|
|
14625
14648
|
if (target == null) {
|
|
14626
14649
|
return new Response(waitingPage(), {
|
|
14627
14650
|
status: 503,
|
|
14628
14651
|
headers: { "content-type": "text/html; charset=utf-8", "retry-after": "2" }
|
|
14629
14652
|
});
|
|
14630
14653
|
}
|
|
14631
|
-
if (
|
|
14654
|
+
if (isUpgrade) {
|
|
14632
14655
|
const proto = req.headers.get("sec-websocket-protocol") ?? undefined;
|
|
14633
14656
|
const upstreamUrl = `ws://${hostPart(target)}:${target.port}${url.pathname}${url.search}`;
|
|
14634
14657
|
const pending = [];
|
|
@@ -14982,6 +15005,8 @@ async function reachableHost(port) {
|
|
|
14982
15005
|
var LOG_RING_SIZE = 500;
|
|
14983
15006
|
var POLL_INTERVAL_MS = 500;
|
|
14984
15007
|
var POLL_ATTEMPTS = 120;
|
|
15008
|
+
var FAST_POLL_MS = 100;
|
|
15009
|
+
var FAST_POLL_WINDOW_MS = 3000;
|
|
14985
15010
|
var PROC_RING = 128 * 1024;
|
|
14986
15011
|
var PROC_COLS = 120;
|
|
14987
15012
|
var PROC_ROWS = 30;
|
|
@@ -14995,6 +15020,7 @@ class WorktreeProcs {
|
|
|
14995
15020
|
opts;
|
|
14996
15021
|
procs = new Map;
|
|
14997
15022
|
stopped = false;
|
|
15023
|
+
asleep = false;
|
|
14998
15024
|
lines = [];
|
|
14999
15025
|
constructor(worktreePath, onProc, onLog, onData = () => {}, onStreamExit = () => {}, opts = {}) {
|
|
15000
15026
|
this.worktreePath = worktreePath;
|
|
@@ -15041,7 +15067,7 @@ class WorktreeProcs {
|
|
|
15041
15067
|
cols: mp.pty?.cols ?? PROC_COLS,
|
|
15042
15068
|
rows: mp.pty?.rows ?? PROC_ROWS,
|
|
15043
15069
|
file: "sh",
|
|
15044
|
-
args: ["-c", command],
|
|
15070
|
+
args: ["-c", execForm(command)],
|
|
15045
15071
|
ring: PROC_RING
|
|
15046
15072
|
}, (data) => {
|
|
15047
15073
|
this.onData(name, data);
|
|
@@ -15062,7 +15088,7 @@ class WorktreeProcs {
|
|
|
15062
15088
|
const { name } = mp.state;
|
|
15063
15089
|
mp.state.exitCode = code;
|
|
15064
15090
|
this.onStreamExit(name, code);
|
|
15065
|
-
if (this.stopped || mp.state.status === "stopped")
|
|
15091
|
+
if (this.stopped || mp.state.status === "stopped" || mp.state.status === "asleep")
|
|
15066
15092
|
return;
|
|
15067
15093
|
if (mp.handInput) {
|
|
15068
15094
|
mp.state.status = "stopped";
|
|
@@ -15088,8 +15114,9 @@ class WorktreeProcs {
|
|
|
15088
15114
|
}
|
|
15089
15115
|
}
|
|
15090
15116
|
async pollPort(mp) {
|
|
15091
|
-
const
|
|
15092
|
-
|
|
15117
|
+
const deadline = Date.now() + (this.opts.pollAttempts ?? POLL_ATTEMPTS) * POLL_INTERVAL_MS;
|
|
15118
|
+
const started = Date.now();
|
|
15119
|
+
while (Date.now() < deadline) {
|
|
15093
15120
|
if (this.stopped || mp.state.status !== "starting")
|
|
15094
15121
|
return;
|
|
15095
15122
|
const host = await reachableHost(mp.state.port);
|
|
@@ -15099,7 +15126,7 @@ class WorktreeProcs {
|
|
|
15099
15126
|
this.onProc({ ...mp.state });
|
|
15100
15127
|
return;
|
|
15101
15128
|
}
|
|
15102
|
-
await Bun.sleep(POLL_INTERVAL_MS);
|
|
15129
|
+
await Bun.sleep(Date.now() - started < FAST_POLL_WINDOW_MS ? FAST_POLL_MS : POLL_INTERVAL_MS);
|
|
15103
15130
|
}
|
|
15104
15131
|
await this.diagnose(mp);
|
|
15105
15132
|
}
|
|
@@ -15146,7 +15173,7 @@ class WorktreeProcs {
|
|
|
15146
15173
|
}
|
|
15147
15174
|
restart(name) {
|
|
15148
15175
|
const mp = this.procs.get(name);
|
|
15149
|
-
if (!mp)
|
|
15176
|
+
if (!mp || this.asleep)
|
|
15150
15177
|
return;
|
|
15151
15178
|
mp.restarts = 0;
|
|
15152
15179
|
if (mp.state.status === "crashed") {
|
|
@@ -15162,6 +15189,35 @@ class WorktreeProcs {
|
|
|
15162
15189
|
async killProc(mp) {
|
|
15163
15190
|
await mp.pty?.kill();
|
|
15164
15191
|
}
|
|
15192
|
+
async sleep() {
|
|
15193
|
+
if (this.asleep || this.stopped)
|
|
15194
|
+
return;
|
|
15195
|
+
this.asleep = true;
|
|
15196
|
+
const exits = [];
|
|
15197
|
+
for (const mp of this.procs.values()) {
|
|
15198
|
+
mp.state.status = "asleep";
|
|
15199
|
+
mp.state.pid = undefined;
|
|
15200
|
+
this.onProc({ ...mp.state });
|
|
15201
|
+
exits.push(this.killProc(mp));
|
|
15202
|
+
}
|
|
15203
|
+
await Promise.all(exits);
|
|
15204
|
+
}
|
|
15205
|
+
wake() {
|
|
15206
|
+
if (!this.asleep || this.stopped)
|
|
15207
|
+
return;
|
|
15208
|
+
this.asleep = false;
|
|
15209
|
+
for (const mp of this.procs.values()) {
|
|
15210
|
+
mp.restarts = 0;
|
|
15211
|
+
this.spawnProc(mp);
|
|
15212
|
+
}
|
|
15213
|
+
}
|
|
15214
|
+
pgids() {
|
|
15215
|
+
const out = [];
|
|
15216
|
+
for (const mp of this.procs.values())
|
|
15217
|
+
if (mp.pty?.alive)
|
|
15218
|
+
out.push(mp.pty.pid);
|
|
15219
|
+
return out;
|
|
15220
|
+
}
|
|
15165
15221
|
async stopAll() {
|
|
15166
15222
|
this.stopped = true;
|
|
15167
15223
|
const exits = [];
|
|
@@ -15185,6 +15241,12 @@ class WorktreeProcs {
|
|
|
15185
15241
|
this.onLog(proc, line);
|
|
15186
15242
|
}
|
|
15187
15243
|
}
|
|
15244
|
+
function execForm(command) {
|
|
15245
|
+
const trimmed = command.trim();
|
|
15246
|
+
if (/[;&|<>()`\n]|\$\(/.test(trimmed) || /^[A-Za-z_][A-Za-z0-9_]*=/.test(trimmed))
|
|
15247
|
+
return command;
|
|
15248
|
+
return `exec ${trimmed}`;
|
|
15249
|
+
}
|
|
15188
15250
|
function envStrings(base) {
|
|
15189
15251
|
const env = {};
|
|
15190
15252
|
for (const [k, v] of Object.entries(base))
|
|
@@ -15222,6 +15284,8 @@ function terminalEnv(base, own, urls) {
|
|
|
15222
15284
|
return { ...env, ...urls, TERM: "xterm-256color", COLORTERM: "truecolor", ...own };
|
|
15223
15285
|
}
|
|
15224
15286
|
var DEFAULT_AGENT_ID = "claude";
|
|
15287
|
+
var PREVIEW_WAKE_MS = 8000;
|
|
15288
|
+
var PREVIEW_POLL_MS = 100;
|
|
15225
15289
|
function defaultAgent(wt, d) {
|
|
15226
15290
|
const agent = new AcpSession({
|
|
15227
15291
|
worktreeId: wt.id,
|
|
@@ -15272,19 +15336,24 @@ function defaultProcs(wt, d) {
|
|
|
15272
15336
|
function defaultTerminal(_wt, opts, onData, onExit) {
|
|
15273
15337
|
return new PtyStream(opts, onData, onExit);
|
|
15274
15338
|
}
|
|
15275
|
-
function defaultProxy(wt, previewName, procs, d) {
|
|
15339
|
+
function defaultProxy(wt, previewName, procs, d, wake) {
|
|
15276
15340
|
return startProxy({
|
|
15277
15341
|
port: wt.proxyPort,
|
|
15278
15342
|
hostname: cloud.bindHost,
|
|
15279
15343
|
remote: d.remote ?? null,
|
|
15280
15344
|
grant: d.grant ?? "",
|
|
15281
15345
|
bridgeScript: d.bridgeScript,
|
|
15282
|
-
getTarget: () => previewTargetOf(procs, previewName)
|
|
15346
|
+
getTarget: () => previewTargetOf(procs, previewName),
|
|
15347
|
+
onRequest: wake.onRequest,
|
|
15348
|
+
ready: wake.ready
|
|
15283
15349
|
});
|
|
15284
15350
|
}
|
|
15351
|
+
function previewProcOf(procs, previewName) {
|
|
15352
|
+
return procs.states().find((p) => p.name === previewName) ?? procs.states()[0];
|
|
15353
|
+
}
|
|
15285
15354
|
function previewTargetOf(procs, previewName) {
|
|
15286
|
-
const st = procs
|
|
15287
|
-
if (
|
|
15355
|
+
const st = previewProcOf(procs, previewName);
|
|
15356
|
+
if (st?.status !== "running")
|
|
15288
15357
|
return null;
|
|
15289
15358
|
return { port: st.boundPort ?? st.port, host: st.host ?? "127.0.0.1" };
|
|
15290
15359
|
}
|
|
@@ -15293,9 +15362,103 @@ class RuntimeRegistry {
|
|
|
15293
15362
|
deps;
|
|
15294
15363
|
runtimes = new Map;
|
|
15295
15364
|
looseShells = new Map;
|
|
15365
|
+
holds = new Map;
|
|
15366
|
+
settingUp = new Set;
|
|
15367
|
+
starting = new Set;
|
|
15296
15368
|
constructor(deps) {
|
|
15297
15369
|
this.deps = deps;
|
|
15298
15370
|
}
|
|
15371
|
+
hold(id, tag) {
|
|
15372
|
+
let tags = this.holds.get(id);
|
|
15373
|
+
if (!tags) {
|
|
15374
|
+
tags = new Set;
|
|
15375
|
+
this.holds.set(id, tags);
|
|
15376
|
+
}
|
|
15377
|
+
tags.add(tag);
|
|
15378
|
+
this.deps.hub.emit("holdsChanged", id, tags.size);
|
|
15379
|
+
}
|
|
15380
|
+
release(id, tag) {
|
|
15381
|
+
const tags = this.holds.get(id);
|
|
15382
|
+
if (!tags?.delete(tag))
|
|
15383
|
+
return;
|
|
15384
|
+
if (tags.size === 0)
|
|
15385
|
+
this.holds.delete(id);
|
|
15386
|
+
this.deps.hub.emit("holdsChanged", id, tags.size);
|
|
15387
|
+
}
|
|
15388
|
+
holdCount(id) {
|
|
15389
|
+
return this.holds.get(id)?.size ?? 0;
|
|
15390
|
+
}
|
|
15391
|
+
markSetup(id, on) {
|
|
15392
|
+
if (on)
|
|
15393
|
+
this.settingUp.add(id);
|
|
15394
|
+
else
|
|
15395
|
+
this.settingUp.delete(id);
|
|
15396
|
+
}
|
|
15397
|
+
async wake(id) {
|
|
15398
|
+
const wt = this.deps.state.worktree(id);
|
|
15399
|
+
if (!wt)
|
|
15400
|
+
return;
|
|
15401
|
+
const rt = this.runtimes.get(id);
|
|
15402
|
+
if (rt?.procs) {
|
|
15403
|
+
if (!rt.procs.asleep)
|
|
15404
|
+
return;
|
|
15405
|
+
rt.procs.wake();
|
|
15406
|
+
log.info(id, "awake");
|
|
15407
|
+
this.deps.hub.emit("worktreesChanged");
|
|
15408
|
+
return;
|
|
15409
|
+
}
|
|
15410
|
+
if (this.settingUp.has(id))
|
|
15411
|
+
return;
|
|
15412
|
+
const repo = this.deps.state.repos.find((r) => r.id === wt.repoId);
|
|
15413
|
+
if (repo)
|
|
15414
|
+
await this.start(wt, repo);
|
|
15415
|
+
}
|
|
15416
|
+
async sleep(id) {
|
|
15417
|
+
const rt = this.runtimes.get(id);
|
|
15418
|
+
if (!rt?.procs || rt.procs.asleep)
|
|
15419
|
+
return;
|
|
15420
|
+
await rt.procs.sleep();
|
|
15421
|
+
this.deps.hub.emit("worktreesChanged");
|
|
15422
|
+
}
|
|
15423
|
+
async awaitPreview(id, ms) {
|
|
15424
|
+
const deadline = Date.now() + ms;
|
|
15425
|
+
for (;; ) {
|
|
15426
|
+
const rt = this.runtimes.get(id);
|
|
15427
|
+
if (!rt?.procs)
|
|
15428
|
+
return null;
|
|
15429
|
+
const st = previewProcOf(rt.procs, rt.previewName);
|
|
15430
|
+
if (st?.status === "running")
|
|
15431
|
+
return previewTargetOf(rt.procs, rt.previewName);
|
|
15432
|
+
const coming = st ? st.status === "starting" : this.starting.has(id);
|
|
15433
|
+
if (!coming || Date.now() >= deadline)
|
|
15434
|
+
return null;
|
|
15435
|
+
await Bun.sleep(PREVIEW_POLL_MS);
|
|
15436
|
+
}
|
|
15437
|
+
}
|
|
15438
|
+
tiers() {
|
|
15439
|
+
let awake = 0;
|
|
15440
|
+
let asleep = 0;
|
|
15441
|
+
for (const rt of this.runtimes.values()) {
|
|
15442
|
+
if (!rt.procs)
|
|
15443
|
+
continue;
|
|
15444
|
+
if (rt.procs.asleep)
|
|
15445
|
+
asleep++;
|
|
15446
|
+
else
|
|
15447
|
+
awake++;
|
|
15448
|
+
}
|
|
15449
|
+
return { awake, asleep };
|
|
15450
|
+
}
|
|
15451
|
+
awake() {
|
|
15452
|
+
const out = [];
|
|
15453
|
+
for (const [id, rt] of this.runtimes) {
|
|
15454
|
+
if (rt.procs && !rt.procs.asleep)
|
|
15455
|
+
out.push({ id, pgids: rt.procs.pgids() });
|
|
15456
|
+
}
|
|
15457
|
+
return out;
|
|
15458
|
+
}
|
|
15459
|
+
isAsleep(id) {
|
|
15460
|
+
return this.runtimes.get(id)?.procs?.asleep ?? false;
|
|
15461
|
+
}
|
|
15299
15462
|
openLooseShell(id, cwd, cols, rows) {
|
|
15300
15463
|
let term = this.looseShells.get(id);
|
|
15301
15464
|
if (!term?.alive) {
|
|
@@ -15380,19 +15543,30 @@ class RuntimeRegistry {
|
|
|
15380
15543
|
const urls = { ...procUrlEnv(procs.states(), previewName), ...worktreeEnv(wt, repo) };
|
|
15381
15544
|
return { ...urls, ...expandEnv(run.env, urls) };
|
|
15382
15545
|
};
|
|
15383
|
-
|
|
15384
|
-
|
|
15385
|
-
|
|
15386
|
-
|
|
15387
|
-
|
|
15388
|
-
|
|
15546
|
+
this.starting.add(wt.id);
|
|
15547
|
+
try {
|
|
15548
|
+
for (const [name, cmd] of Object.entries(run.procs)) {
|
|
15549
|
+
if (name !== previewName)
|
|
15550
|
+
await procs.start(name, cmd, envFor());
|
|
15551
|
+
}
|
|
15552
|
+
if (previewName && run.procs[previewName]) {
|
|
15553
|
+
await procs.start(previewName, run.procs[previewName], envFor());
|
|
15554
|
+
}
|
|
15555
|
+
} finally {
|
|
15556
|
+
this.starting.delete(wt.id);
|
|
15389
15557
|
}
|
|
15390
15558
|
if (!this.deps.state.worktree(wt.id) || this.runtimes.get(wt.id) !== rt) {
|
|
15391
15559
|
this.returnLease(wt.id);
|
|
15392
15560
|
await procs.stopAll();
|
|
15393
15561
|
return;
|
|
15394
15562
|
}
|
|
15395
|
-
rt.proxy = (this.deps.makeProxy ?? defaultProxy)(live, previewName, procs, this.deps
|
|
15563
|
+
rt.proxy = (this.deps.makeProxy ?? defaultProxy)(live, previewName, procs, this.deps, {
|
|
15564
|
+
onRequest: () => {
|
|
15565
|
+
this.deps.hub.emit("previewRequest", wt.id);
|
|
15566
|
+
fireAndForget(wt.id, this.wake(wt.id), "wake on request");
|
|
15567
|
+
},
|
|
15568
|
+
ready: () => this.awaitPreview(wt.id, PREVIEW_WAKE_MS)
|
|
15569
|
+
});
|
|
15396
15570
|
this.deps.hub.emit("worktreesChanged");
|
|
15397
15571
|
}
|
|
15398
15572
|
leased = new Map;
|
|
@@ -15415,6 +15589,8 @@ class RuntimeRegistry {
|
|
|
15415
15589
|
await procs?.stopAll();
|
|
15416
15590
|
}
|
|
15417
15591
|
async stop(id) {
|
|
15592
|
+
this.holds.delete(id);
|
|
15593
|
+
this.settingUp.delete(id);
|
|
15418
15594
|
const rt = this.runtimes.get(id);
|
|
15419
15595
|
if (!rt)
|
|
15420
15596
|
return;
|
|
@@ -15537,6 +15713,8 @@ class RuntimeRegistry {
|
|
|
15537
15713
|
this.startLogin(id, rt.login.run);
|
|
15538
15714
|
} else if (stream === SHELL_STREAM)
|
|
15539
15715
|
await rt?.shell?.kill();
|
|
15716
|
+
else if (rt?.procs?.asleep)
|
|
15717
|
+
await this.wake(id);
|
|
15540
15718
|
else
|
|
15541
15719
|
rt?.procs?.restart(stream);
|
|
15542
15720
|
}
|
|
@@ -15545,11 +15723,7 @@ class RuntimeRegistry {
|
|
|
15545
15723
|
}
|
|
15546
15724
|
previewTarget(id) {
|
|
15547
15725
|
const rt = this.runtimes.get(id);
|
|
15548
|
-
|
|
15549
|
-
return null;
|
|
15550
|
-
const t = previewTargetOf(rt.procs, rt.previewName);
|
|
15551
|
-
const st = rt.procs.states().find((p) => p.port === t?.port);
|
|
15552
|
-
return st?.status === "running" ? t : null;
|
|
15726
|
+
return rt?.procs ? previewTargetOf(rt.procs, rt.previewName) : null;
|
|
15553
15727
|
}
|
|
15554
15728
|
async shutdown() {
|
|
15555
15729
|
this.pruneLooseShells(new Set);
|
|
@@ -15859,6 +16033,20 @@ var NAME_SYSTEM = "You are a naming assistant. Reply with only the requested nam
|
|
|
15859
16033
|
var namePrompt = (task) => `Name this coding task in 2 to 4 lowercase kebab-case words (like "sticky-header" or "dark-mode-toggle"). Reply with ONLY the name, nothing else.
|
|
15860
16034
|
|
|
15861
16035
|
Task: ${task.slice(0, 500)}`;
|
|
16036
|
+
function taskText(text, attachments = []) {
|
|
16037
|
+
if (text.trim())
|
|
16038
|
+
return text;
|
|
16039
|
+
return attachments.map(attachmentText).join(`
|
|
16040
|
+
|
|
16041
|
+
`);
|
|
16042
|
+
}
|
|
16043
|
+
function attachmentText(a) {
|
|
16044
|
+
if (a.kind === "paste")
|
|
16045
|
+
return a.text;
|
|
16046
|
+
if (a.kind === "image")
|
|
16047
|
+
return a.name ? `image ${a.name}` : "an image";
|
|
16048
|
+
return `element <${a.component ?? a.tag}>${a.text ? ` "${a.text}"` : ""}`;
|
|
16049
|
+
}
|
|
15862
16050
|
function parseName(text) {
|
|
15863
16051
|
if (!text)
|
|
15864
16052
|
return null;
|
|
@@ -15932,15 +16120,46 @@ function makePlanner(agents) {
|
|
|
15932
16120
|
}
|
|
15933
16121
|
|
|
15934
16122
|
// ../daemon/src/core/assets.ts
|
|
15935
|
-
import { existsSync as existsSync9 } from "fs";
|
|
15936
|
-
import { join as join7 } from "path";
|
|
16123
|
+
import { existsSync as existsSync9, readdirSync as readdirSync2, rmSync, statSync as statSync2 } from "fs";
|
|
16124
|
+
import { join as join7, resolve as resolve5 } from "path";
|
|
15937
16125
|
function locateAssets(here, env = process.env) {
|
|
15938
16126
|
const packaged = existsSync9(join7(here, "shell", "index.html"));
|
|
15939
16127
|
return {
|
|
15940
16128
|
shellDist: env.TOYON_SHELL_DIST ?? (packaged ? join7(here, "shell") : join7(here, "../../shell/dist")),
|
|
15941
|
-
bridgeJs: env.TOYON_BRIDGE_JS ?? (packaged ? join7(here, "bridge.js") : join7(here, "../../bridge/dist/bridge.js"))
|
|
16129
|
+
bridgeJs: env.TOYON_BRIDGE_JS ?? (packaged ? join7(here, "bridge.js") : join7(here, "../../bridge/dist/bridge.js")),
|
|
16130
|
+
sourceRoot: packaged ? null : resolve5(here, "../../..")
|
|
15942
16131
|
};
|
|
15943
16132
|
}
|
|
16133
|
+
var KEEP_MS = 3 * 24 * 60 * 60 * 1000;
|
|
16134
|
+
function pruneAssets(shellDist) {
|
|
16135
|
+
const dir = join7(shellDist, "assets");
|
|
16136
|
+
let names;
|
|
16137
|
+
try {
|
|
16138
|
+
names = readdirSync2(dir);
|
|
16139
|
+
} catch {
|
|
16140
|
+
return 0;
|
|
16141
|
+
}
|
|
16142
|
+
const found = [];
|
|
16143
|
+
for (const name of names) {
|
|
16144
|
+
const path = join7(dir, name);
|
|
16145
|
+
try {
|
|
16146
|
+
found.push({ path, mtime: statSync2(path).mtimeMs });
|
|
16147
|
+
} catch {}
|
|
16148
|
+
}
|
|
16149
|
+
if (found.length === 0)
|
|
16150
|
+
return 0;
|
|
16151
|
+
const newest = Math.max(...found.map((f) => f.mtime));
|
|
16152
|
+
let gone = 0;
|
|
16153
|
+
for (const f of found) {
|
|
16154
|
+
if (newest - f.mtime <= KEEP_MS)
|
|
16155
|
+
continue;
|
|
16156
|
+
try {
|
|
16157
|
+
rmSync(f.path, { recursive: true, force: true });
|
|
16158
|
+
gone++;
|
|
16159
|
+
} catch {}
|
|
16160
|
+
}
|
|
16161
|
+
return gone;
|
|
16162
|
+
}
|
|
15944
16163
|
|
|
15945
16164
|
// ../daemon/src/core/dialog.ts
|
|
15946
16165
|
import { existsSync as existsSync10 } from "fs";
|
|
@@ -16030,9 +16249,104 @@ class Hub {
|
|
|
16030
16249
|
}
|
|
16031
16250
|
}
|
|
16032
16251
|
|
|
16252
|
+
// ../daemon/src/core/restart.ts
|
|
16253
|
+
import { spawn as spawn2 } from "child_process";
|
|
16254
|
+
import { openSync } from "fs";
|
|
16255
|
+
function restartable(env = process.env) {
|
|
16256
|
+
if (env.TOYON_WORKTREE !== undefined) {
|
|
16257
|
+
return { ok: false, reason: "this daemon runs as a toyon process; restart it from its terminal tab" };
|
|
16258
|
+
}
|
|
16259
|
+
return { ok: true };
|
|
16260
|
+
}
|
|
16261
|
+
function respawn(logFile) {
|
|
16262
|
+
const logFd = openSync(logFile, "a");
|
|
16263
|
+
const child = spawn2(process.execPath, process.argv.slice(1), {
|
|
16264
|
+
detached: true,
|
|
16265
|
+
stdio: ["ignore", logFd, logFd],
|
|
16266
|
+
env: { ...process.env }
|
|
16267
|
+
});
|
|
16268
|
+
child.unref();
|
|
16269
|
+
}
|
|
16270
|
+
|
|
16271
|
+
// ../daemon/src/core/self.ts
|
|
16272
|
+
function needsRestart(path) {
|
|
16273
|
+
return path.startsWith("packages/daemon/") || path.startsWith("packages/shared/") || path === "package.json" || path === "bun.lock";
|
|
16274
|
+
}
|
|
16275
|
+
function needsRebuild(path) {
|
|
16276
|
+
return path.startsWith("packages/shell/") || path.startsWith("packages/bridge/") || path.startsWith("packages/shared/");
|
|
16277
|
+
}
|
|
16278
|
+
function classify(paths) {
|
|
16279
|
+
let rebuild = false;
|
|
16280
|
+
let restart = false;
|
|
16281
|
+
for (const path of paths) {
|
|
16282
|
+
if (needsRebuild(path))
|
|
16283
|
+
rebuild = true;
|
|
16284
|
+
if (needsRestart(path))
|
|
16285
|
+
restart = true;
|
|
16286
|
+
}
|
|
16287
|
+
return { rebuild, restart };
|
|
16288
|
+
}
|
|
16289
|
+
|
|
16290
|
+
class SelfWatch {
|
|
16291
|
+
root;
|
|
16292
|
+
booted = null;
|
|
16293
|
+
state = null;
|
|
16294
|
+
constructor(root) {
|
|
16295
|
+
this.root = root;
|
|
16296
|
+
}
|
|
16297
|
+
async start() {
|
|
16298
|
+
if (!this.root)
|
|
16299
|
+
return;
|
|
16300
|
+
const head = await git(this.root, "rev-parse", "HEAD");
|
|
16301
|
+
if (!head.ok) {
|
|
16302
|
+
log.debug("self", `no HEAD in ${this.root}; not watching it`);
|
|
16303
|
+
return;
|
|
16304
|
+
}
|
|
16305
|
+
this.booted = head.out.trim();
|
|
16306
|
+
}
|
|
16307
|
+
own(repo) {
|
|
16308
|
+
return this.root !== null && repo.path === this.root;
|
|
16309
|
+
}
|
|
16310
|
+
async check(repo) {
|
|
16311
|
+
if (this.booted === null || !this.own(repo))
|
|
16312
|
+
return false;
|
|
16313
|
+
const names = await git(this.root ?? "", "diff", "--name-only", `${this.booted}..${repo.defaultBranch}`);
|
|
16314
|
+
if (!names.ok) {
|
|
16315
|
+
log.debug("self", `could not diff ${this.booted}..${repo.defaultBranch}: ${names.err}`);
|
|
16316
|
+
return false;
|
|
16317
|
+
}
|
|
16318
|
+
const paths = names.out.split(`
|
|
16319
|
+
`).filter((l) => l !== "");
|
|
16320
|
+
const { rebuild, restart } = classify(paths);
|
|
16321
|
+
return this.set({ repoId: repo.id, rebuild, restart, building: this.state?.building ?? false });
|
|
16322
|
+
}
|
|
16323
|
+
building(on, failure) {
|
|
16324
|
+
if (!this.state)
|
|
16325
|
+
return false;
|
|
16326
|
+
const done = !on && failure === undefined;
|
|
16327
|
+
return this.set({
|
|
16328
|
+
...this.state,
|
|
16329
|
+
building: on,
|
|
16330
|
+
rebuild: done ? false : this.state.rebuild,
|
|
16331
|
+
buildFailed: on ? undefined : failure
|
|
16332
|
+
});
|
|
16333
|
+
}
|
|
16334
|
+
get() {
|
|
16335
|
+
return this.state;
|
|
16336
|
+
}
|
|
16337
|
+
set(next) {
|
|
16338
|
+
const quiet = !next.rebuild && !next.restart && !next.building && next.buildFailed === undefined;
|
|
16339
|
+
const value = quiet ? null : next;
|
|
16340
|
+
if (JSON.stringify(value) === JSON.stringify(this.state))
|
|
16341
|
+
return false;
|
|
16342
|
+
this.state = value;
|
|
16343
|
+
return true;
|
|
16344
|
+
}
|
|
16345
|
+
}
|
|
16346
|
+
|
|
16033
16347
|
// ../daemon/src/core/state.ts
|
|
16034
16348
|
import { randomBytes } from "crypto";
|
|
16035
|
-
import { existsSync as existsSync11, readFileSync as readFileSync8, renameSync as
|
|
16349
|
+
import { existsSync as existsSync11, readFileSync as readFileSync8, renameSync as renameSync2, writeFileSync as writeFileSync4 } from "fs";
|
|
16036
16350
|
var SEEN_WORKTREES = 50;
|
|
16037
16351
|
var empty = { repos: [], worktrees: [], sessions: {} };
|
|
16038
16352
|
function loadState(paths) {
|
|
@@ -16053,7 +16367,7 @@ function loadState(paths) {
|
|
|
16053
16367
|
} catch (e) {
|
|
16054
16368
|
const backup = `${STATE_FILE}.corrupt-${Date.now()}`;
|
|
16055
16369
|
try {
|
|
16056
|
-
|
|
16370
|
+
writeFileSync4(backup, raw);
|
|
16057
16371
|
} catch (be) {
|
|
16058
16372
|
log.warn("state", "could not write the corrupt-state backup", be);
|
|
16059
16373
|
}
|
|
@@ -16080,8 +16394,8 @@ function loadState(paths) {
|
|
|
16080
16394
|
}
|
|
16081
16395
|
function saveState(paths, state) {
|
|
16082
16396
|
const tmp = `${paths.stateFile}.tmp`;
|
|
16083
|
-
|
|
16084
|
-
|
|
16397
|
+
writeFileSync4(tmp, JSON.stringify(state, null, 2));
|
|
16398
|
+
renameSync2(tmp, paths.stateFile);
|
|
16085
16399
|
}
|
|
16086
16400
|
function loadOrCreateToken(paths) {
|
|
16087
16401
|
ensureDirs(paths);
|
|
@@ -16089,7 +16403,7 @@ function loadOrCreateToken(paths) {
|
|
|
16089
16403
|
if (existsSync11(TOKEN_FILE))
|
|
16090
16404
|
return readFileSync8(TOKEN_FILE, "utf8").trim();
|
|
16091
16405
|
const token = randomBytes(32).toString("hex");
|
|
16092
|
-
|
|
16406
|
+
writeFileSync4(TOKEN_FILE, token, { mode: 384 });
|
|
16093
16407
|
return token;
|
|
16094
16408
|
}
|
|
16095
16409
|
|
|
@@ -16841,6 +17155,7 @@ class ExecService {
|
|
|
16841
17155
|
}
|
|
16842
17156
|
const timer = setTimeout(() => this.kill(worktreeId, toolId), TIMEOUT_MS);
|
|
16843
17157
|
this.track(worktreeId, toolId, { proc, timer });
|
|
17158
|
+
this.deps.runtime.hold(worktreeId, `exec:${toolId}`);
|
|
16844
17159
|
return this.collect(worktreeId, toolId, proc, agent);
|
|
16845
17160
|
}
|
|
16846
17161
|
stop(worktreeId) {
|
|
@@ -16906,6 +17221,7 @@ class ExecService {
|
|
|
16906
17221
|
const r = this.untrack(worktreeId, toolId);
|
|
16907
17222
|
if (r)
|
|
16908
17223
|
clearTimeout(r.timer);
|
|
17224
|
+
this.deps.runtime.release(worktreeId, `exec:${toolId}`);
|
|
16909
17225
|
}
|
|
16910
17226
|
agent.note({ type: "tool-end", toolId, output: formatOutput(text, exit, truncated), isError: exit !== 0 });
|
|
16911
17227
|
return { exit, text };
|
|
@@ -16931,7 +17247,7 @@ ${body}
|
|
|
16931
17247
|
}
|
|
16932
17248
|
|
|
16933
17249
|
// ../daemon/src/files/service.ts
|
|
16934
|
-
import { spawn as
|
|
17250
|
+
import { spawn as spawn3 } from "child_process";
|
|
16935
17251
|
import { stat, unlink } from "fs/promises";
|
|
16936
17252
|
|
|
16937
17253
|
// ../daemon/src/git/lock.ts
|
|
@@ -17080,6 +17396,9 @@ async function treeEmpty(worktreePath) {
|
|
|
17080
17396
|
const r = await git(worktreePath, "ls-tree", "--name-only", "HEAD");
|
|
17081
17397
|
return !r.ok || r.out === "";
|
|
17082
17398
|
}
|
|
17399
|
+
async function isTracked(worktreePath, rel) {
|
|
17400
|
+
return (await git(worktreePath, "ls-files", "--error-unmatch", "--", rel)).ok;
|
|
17401
|
+
}
|
|
17083
17402
|
async function behindUpstream(worktreePath) {
|
|
17084
17403
|
const r = await git(worktreePath, "rev-list", "--count", "HEAD..@{upstream}");
|
|
17085
17404
|
return r.ok ? Number(r.out) || 0 : null;
|
|
@@ -17149,12 +17468,12 @@ async function fileAtCommit(worktreePath, sha, file) {
|
|
|
17149
17468
|
}
|
|
17150
17469
|
|
|
17151
17470
|
// ../daemon/src/worktrees/paths.ts
|
|
17152
|
-
import { resolve as
|
|
17471
|
+
import { resolve as resolve6 } from "path";
|
|
17153
17472
|
function resolveInside(worktreePath, rel, opts = {}) {
|
|
17154
17473
|
if (typeof rel !== "string" || rel.includes("\x00"))
|
|
17155
17474
|
throw new UserError("invalid path");
|
|
17156
|
-
const root = canonical(
|
|
17157
|
-
const target = canonical(
|
|
17475
|
+
const root = canonical(resolve6(worktreePath));
|
|
17476
|
+
const target = canonical(resolve6(worktreePath, rel));
|
|
17158
17477
|
if (target === root) {
|
|
17159
17478
|
if (opts.allowRoot)
|
|
17160
17479
|
return target;
|
|
@@ -17521,7 +17840,7 @@ class FileService {
|
|
|
17521
17840
|
revealPath(target) {
|
|
17522
17841
|
if (process.platform !== "darwin")
|
|
17523
17842
|
throw new UserError("reveal is only available on macOS");
|
|
17524
|
-
const child =
|
|
17843
|
+
const child = spawn3("open", ["-R", target], { stdio: "ignore" });
|
|
17525
17844
|
child.on("error", () => {});
|
|
17526
17845
|
child.unref();
|
|
17527
17846
|
}
|
|
@@ -17614,8 +17933,89 @@ function prNumberOf(url) {
|
|
|
17614
17933
|
return m ? Number(m[1]) : null;
|
|
17615
17934
|
}
|
|
17616
17935
|
|
|
17936
|
+
// ../daemon/src/runtime/setup.ts
|
|
17937
|
+
var SETUP_RING = 64 * 1024;
|
|
17938
|
+
var SETUP_COLS = 120;
|
|
17939
|
+
var SETUP_ROWS = 30;
|
|
17940
|
+
function runSetup(cmd, cwd, onLine, extra = {}) {
|
|
17941
|
+
return new Promise((resolve) => {
|
|
17942
|
+
const lines = new LineSplitter;
|
|
17943
|
+
try {
|
|
17944
|
+
new PtyStream({
|
|
17945
|
+
cwd,
|
|
17946
|
+
env: { ...envStrings2(process.env), TERM: "xterm-256color", COLORTERM: "truecolor", ...extra },
|
|
17947
|
+
cols: SETUP_COLS,
|
|
17948
|
+
rows: SETUP_ROWS,
|
|
17949
|
+
file: "sh",
|
|
17950
|
+
args: ["-c", cmd],
|
|
17951
|
+
ring: SETUP_RING
|
|
17952
|
+
}, (data) => {
|
|
17953
|
+
for (const line of lines.feed(data))
|
|
17954
|
+
onLine(line);
|
|
17955
|
+
}, resolve);
|
|
17956
|
+
} catch (e) {
|
|
17957
|
+
onLine(`could not run: ${e instanceof Error ? e.message : String(e)}`);
|
|
17958
|
+
resolve(1);
|
|
17959
|
+
}
|
|
17960
|
+
});
|
|
17961
|
+
}
|
|
17962
|
+
function envStrings2(base) {
|
|
17963
|
+
const env = {};
|
|
17964
|
+
for (const [k, v] of Object.entries(base))
|
|
17965
|
+
if (typeof v === "string")
|
|
17966
|
+
env[k] = v;
|
|
17967
|
+
return env;
|
|
17968
|
+
}
|
|
17969
|
+
|
|
17970
|
+
// ../daemon/src/repos/afterLand.ts
|
|
17971
|
+
class AfterLand {
|
|
17972
|
+
d;
|
|
17973
|
+
running = new Set;
|
|
17974
|
+
constructor(d) {
|
|
17975
|
+
this.d = d;
|
|
17976
|
+
}
|
|
17977
|
+
busy(repoId) {
|
|
17978
|
+
return this.running.has(repoId);
|
|
17979
|
+
}
|
|
17980
|
+
run(repoId) {
|
|
17981
|
+
const repo = this.d.state.repo(repoId);
|
|
17982
|
+
const commands = repo?.config.afterLand ?? [];
|
|
17983
|
+
if (!repo || commands.length === 0 || this.running.has(repoId))
|
|
17984
|
+
return;
|
|
17985
|
+
this.running.add(repoId);
|
|
17986
|
+
const done = this.exec(repo, commands).finally(() => this.running.delete(repoId));
|
|
17987
|
+
fireAndForget(repo.id, done, "afterLand");
|
|
17988
|
+
}
|
|
17989
|
+
async exec(repo, commands) {
|
|
17990
|
+
if (this.d.self.building(true))
|
|
17991
|
+
this.d.hub.emit("selfChanged");
|
|
17992
|
+
let failure;
|
|
17993
|
+
for (const command of commands) {
|
|
17994
|
+
this.tellMain(repo, `afterLand: ${command}`);
|
|
17995
|
+
let last = "";
|
|
17996
|
+
const code = await runSetup(command, repo.path, (line) => {
|
|
17997
|
+
last = line;
|
|
17998
|
+
this.tellMain(repo, line);
|
|
17999
|
+
});
|
|
18000
|
+
if (code === 0)
|
|
18001
|
+
continue;
|
|
18002
|
+
failure = last.trim() === "" ? `${command} (exit ${code})` : last.trim();
|
|
18003
|
+
this.tellMain(repo, `afterLand stopped: ${command} exited ${code}`);
|
|
18004
|
+
log.warn(repo.id, `afterLand stopped: ${command} exited ${code}`);
|
|
18005
|
+
break;
|
|
18006
|
+
}
|
|
18007
|
+
if (this.d.self.building(false, failure))
|
|
18008
|
+
this.d.hub.emit("selfChanged");
|
|
18009
|
+
}
|
|
18010
|
+
tellMain(repo, line) {
|
|
18011
|
+
const main = this.d.state.worktrees.find((w) => w.repoId === repo.id && w.kind === "main");
|
|
18012
|
+
if (main)
|
|
18013
|
+
this.d.hub.emit("log", main.id, "land", line);
|
|
18014
|
+
}
|
|
18015
|
+
}
|
|
18016
|
+
|
|
17617
18017
|
// ../daemon/src/repos/registry.ts
|
|
17618
|
-
import { existsSync as existsSync16, mkdirSync as mkdirSync5, writeFileSync as
|
|
18018
|
+
import { existsSync as existsSync16, mkdirSync as mkdirSync5, rmSync as rmSync2, writeFileSync as writeFileSync5 } from "fs";
|
|
17619
18019
|
import { homedir as homedir7 } from "os";
|
|
17620
18020
|
import { basename as basename3, dirname as dirname6, join as join14 } from "path";
|
|
17621
18021
|
|
|
@@ -17643,11 +18043,11 @@ function variantLens(index) {
|
|
|
17643
18043
|
}
|
|
17644
18044
|
|
|
17645
18045
|
// ../daemon/src/repos/browse.ts
|
|
17646
|
-
import { existsSync as existsSync12, statSync as
|
|
18046
|
+
import { existsSync as existsSync12, statSync as statSync3 } from "fs";
|
|
17647
18047
|
import { readdir } from "fs/promises";
|
|
17648
18048
|
import { homedir as homedir5 } from "os";
|
|
17649
18049
|
import { dirname as dirname4, join as join10 } from "path";
|
|
17650
|
-
var
|
|
18050
|
+
var MAX_ENTRIES = 40;
|
|
17651
18051
|
function expandTilde(raw) {
|
|
17652
18052
|
return raw === "~" || raw.startsWith("~/") ? join10(homedir5(), raw.slice(1)) : raw;
|
|
17653
18053
|
}
|
|
@@ -17672,7 +18072,7 @@ var NOWHERE = { exists: false, isDir: false, isRepo: false, parentExists: false
|
|
|
17672
18072
|
function describe2(typed, parent) {
|
|
17673
18073
|
let isDir = false;
|
|
17674
18074
|
try {
|
|
17675
|
-
isDir =
|
|
18075
|
+
isDir = statSync3(typed).isDirectory();
|
|
17676
18076
|
} catch {}
|
|
17677
18077
|
return {
|
|
17678
18078
|
exists: existsSync12(typed),
|
|
@@ -17706,11 +18106,11 @@ async function browsePath(raw) {
|
|
|
17706
18106
|
entries.push({ path: collapseTilde(abs), name, isRepo: existsSync12(join10(abs, ".git")) });
|
|
17707
18107
|
}
|
|
17708
18108
|
entries.sort((a, b) => a.isRepo !== b.isRepo ? a.isRepo ? -1 : 1 : a.name.localeCompare(b.name));
|
|
17709
|
-
return { entries: entries.slice(0,
|
|
18109
|
+
return { entries: entries.slice(0, MAX_ENTRIES), target };
|
|
17710
18110
|
}
|
|
17711
18111
|
|
|
17712
18112
|
// ../daemon/src/repos/config.ts
|
|
17713
|
-
import { closeSync, existsSync as existsSync13, openSync, readdirSync as
|
|
18113
|
+
import { closeSync, existsSync as existsSync13, openSync as openSync2, readdirSync as readdirSync3, readFileSync as readFileSync9, readSync } from "fs";
|
|
17714
18114
|
import { join as join11 } from "path";
|
|
17715
18115
|
var PLACES = [CONFIG_FILES.folder, CONFIG_FILES.root];
|
|
17716
18116
|
var present = (repoPath, place) => [place.shared, place.local].filter((rel) => existsSync13(join11(repoPath, rel)));
|
|
@@ -17873,7 +18273,7 @@ function markerIn(repoPath) {
|
|
|
17873
18273
|
return named;
|
|
17874
18274
|
let entries;
|
|
17875
18275
|
try {
|
|
17876
|
-
entries =
|
|
18276
|
+
entries = readdirSync3(repoPath);
|
|
17877
18277
|
} catch {
|
|
17878
18278
|
return;
|
|
17879
18279
|
}
|
|
@@ -17895,7 +18295,7 @@ function servesPage(repoPath) {
|
|
|
17895
18295
|
function frontEndBeside(repoPath) {
|
|
17896
18296
|
let dirs;
|
|
17897
18297
|
try {
|
|
17898
|
-
dirs =
|
|
18298
|
+
dirs = readdirSync3(repoPath, { withFileTypes: true });
|
|
17899
18299
|
} catch {
|
|
17900
18300
|
return false;
|
|
17901
18301
|
}
|
|
@@ -17920,7 +18320,7 @@ function head(path) {
|
|
|
17920
18320
|
if (!existsSync13(path))
|
|
17921
18321
|
return "";
|
|
17922
18322
|
try {
|
|
17923
|
-
const fd =
|
|
18323
|
+
const fd = openSync2(path, "r");
|
|
17924
18324
|
try {
|
|
17925
18325
|
const buf = Buffer.alloc(READ_CAP);
|
|
17926
18326
|
return buf.toString("utf8", 0, readSync(fd, buf, 0, READ_CAP, 0));
|
|
@@ -17966,10 +18366,10 @@ function detectRunner(repoPath, packageManager) {
|
|
|
17966
18366
|
}
|
|
17967
18367
|
|
|
17968
18368
|
// ../daemon/src/repos/create.ts
|
|
17969
|
-
import { existsSync as existsSync14, readdirSync as
|
|
18369
|
+
import { existsSync as existsSync14, readdirSync as readdirSync4, realpathSync as realpathSync2, statSync as statSync4 } from "fs";
|
|
17970
18370
|
import { appendFile as appendFile2, mkdir as mkdir2, rm } from "fs/promises";
|
|
17971
18371
|
import { homedir as homedir6 } from "os";
|
|
17972
|
-
import { dirname as dirname5, join as join12, resolve as
|
|
18372
|
+
import { dirname as dirname5, join as join12, resolve as resolve7 } from "path";
|
|
17973
18373
|
function isInside(dir, parent) {
|
|
17974
18374
|
const p = parent.replace(/\/+$/, "");
|
|
17975
18375
|
return dir === p || dir.startsWith(`${p}/`);
|
|
@@ -17997,12 +18397,12 @@ function planInPlace(opts) {
|
|
|
17997
18397
|
throw new UserError(`${opts.name} is not a folder name`);
|
|
17998
18398
|
}
|
|
17999
18399
|
const typed = join12(resolveParent(opts.parent), name);
|
|
18000
|
-
if (!existsSync14(typed) || !
|
|
18400
|
+
if (!existsSync14(typed) || !statSync4(typed).isDirectory())
|
|
18001
18401
|
throw new UserError(`${typed} is not a folder`);
|
|
18002
18402
|
const dir = realpathSync2(typed);
|
|
18003
18403
|
if (existsSync14(join12(dir, ".git")))
|
|
18004
18404
|
throw new UserError(`${dir} is already a project; open it instead`);
|
|
18005
|
-
if (!holdsNothing(
|
|
18405
|
+
if (!holdsNothing(readdirSync4(dir))) {
|
|
18006
18406
|
throw new UserError(`${dir} is not empty; only an empty folder can become a project where it is`);
|
|
18007
18407
|
}
|
|
18008
18408
|
return { parent: dirname5(dir), dir, makeParent: false };
|
|
@@ -18013,7 +18413,7 @@ function resolveParent(raw) {
|
|
|
18013
18413
|
throw new UserError(`${raw} is not a folder path`);
|
|
18014
18414
|
if (!existsSync14(typed))
|
|
18015
18415
|
throw new UserError(`${raw} does not exist; make it first`);
|
|
18016
|
-
if (!
|
|
18416
|
+
if (!statSync4(typed).isDirectory())
|
|
18017
18417
|
throw new UserError(`${raw} is not a folder`);
|
|
18018
18418
|
return realpathSync2(typed);
|
|
18019
18419
|
}
|
|
@@ -18051,7 +18451,7 @@ async function initWithEmptyCommit(dir) {
|
|
|
18051
18451
|
const exclude = await git(dir, "rev-parse", "--git-path", "info/exclude");
|
|
18052
18452
|
if (!exclude.ok)
|
|
18053
18453
|
throw new UserError(`git rev-parse failed: ${exclude.err}`);
|
|
18054
|
-
const excludePath =
|
|
18454
|
+
const excludePath = resolve7(dir, exclude.out);
|
|
18055
18455
|
await mkdir2(dirname5(excludePath), { recursive: true });
|
|
18056
18456
|
await appendFile2(excludePath, `
|
|
18057
18457
|
${LOCAL_EXCLUDES.join(`
|
|
@@ -18313,25 +18713,15 @@ class RepoRegistry {
|
|
|
18313
18713
|
log.info("daemon", `${cold} worktrees across ${state.repos.length} repos start when opened`);
|
|
18314
18714
|
}
|
|
18315
18715
|
warm(repoId) {
|
|
18316
|
-
if (this.warmed.has(repoId))
|
|
18716
|
+
if (this.warmed.has(repoId) || !this.d.state.repo(repoId))
|
|
18317
18717
|
return;
|
|
18318
18718
|
this.warmed.add(repoId);
|
|
18319
|
-
const repo = this.d.state.requireRepo(repoId);
|
|
18320
|
-
const mine = this.d.state.worktrees.filter((w) => w.repoId === repoId && w.kind !== "spare");
|
|
18321
|
-
fireAndForget(repoId, (async () => {
|
|
18322
|
-
for (const wt of mine)
|
|
18323
|
-
await this.d.runtime.start(wt, repo);
|
|
18324
|
-
})(), "warm");
|
|
18325
18719
|
this.d.worktrees.spare.warm(repoId);
|
|
18326
18720
|
}
|
|
18327
18721
|
touch(worktreeId) {
|
|
18328
|
-
|
|
18329
|
-
if (!this.warmed.has(repo.id)) {
|
|
18330
|
-
this.warm(repo.id);
|
|
18722
|
+
if (!this.d.state.worktree(worktreeId))
|
|
18331
18723
|
return;
|
|
18332
|
-
|
|
18333
|
-
if (!this.d.runtime.get(wt.id)?.procs)
|
|
18334
|
-
fireAndForget(wt.id, this.d.runtime.start(wt, repo), "start on open");
|
|
18724
|
+
fireAndForget(worktreeId, this.d.runtime.wake(worktreeId), "start on open");
|
|
18335
18725
|
}
|
|
18336
18726
|
async create(opts) {
|
|
18337
18727
|
const inPlace = opts.mode === "init";
|
|
@@ -18512,23 +18902,34 @@ class RepoRegistry {
|
|
|
18512
18902
|
this.d.hub.emit("reposChanged");
|
|
18513
18903
|
this.d.hub.emit("worktreesChanged");
|
|
18514
18904
|
}
|
|
18515
|
-
async confirmConfig(repoId, config) {
|
|
18905
|
+
async confirmConfig(repoId, config, kind) {
|
|
18516
18906
|
const repo = this.d.state.requireRepo(repoId);
|
|
18517
18907
|
const current = readConfigFile(repo.path);
|
|
18518
18908
|
if (current && !current.ok && current.conflict)
|
|
18519
18909
|
throw new UserError(current.reason);
|
|
18520
|
-
|
|
18521
|
-
const rel =
|
|
18910
|
+
const was = configTarget(repo.path, !!repo.made);
|
|
18911
|
+
const rel = configSibling(was, kind);
|
|
18912
|
+
const moved = rel !== was && existsSync16(join14(repo.path, was));
|
|
18913
|
+
if (moved && !(kind === "local" && await isTracked(repo.path, was))) {
|
|
18914
|
+
try {
|
|
18915
|
+
rmSync2(join14(repo.path, was));
|
|
18916
|
+
} catch (e) {
|
|
18917
|
+
log.warn(repoId, `could not remove ${was}`, e);
|
|
18918
|
+
}
|
|
18919
|
+
if (isLocalConfigFile(was))
|
|
18920
|
+
await unexcludeFromGit(repo.path, was);
|
|
18921
|
+
}
|
|
18522
18922
|
if (isLocalConfigFile(rel))
|
|
18523
18923
|
await excludeFromGit(repo.path, rel);
|
|
18524
18924
|
const file = join14(repo.path, rel);
|
|
18525
18925
|
try {
|
|
18526
18926
|
mkdirSync5(dirname6(file), { recursive: true });
|
|
18527
|
-
|
|
18927
|
+
writeFileSync5(file, `${JSON.stringify(configBody(repo.path, rel, config), null, 2)}
|
|
18528
18928
|
`);
|
|
18529
18929
|
} catch (e) {
|
|
18530
18930
|
log.warn(repoId, `could not write ${rel}`, e);
|
|
18531
18931
|
}
|
|
18932
|
+
this.placeConfig(repo);
|
|
18532
18933
|
const back = readConfigFile(repo.path);
|
|
18533
18934
|
repo.config = back?.ok ? back.config : config;
|
|
18534
18935
|
repo.needsSetup = false;
|
|
@@ -18637,6 +19038,10 @@ class RepoRegistry {
|
|
|
18637
19038
|
if (main)
|
|
18638
19039
|
this.d.hub.emit("log", main.id, "config", line);
|
|
18639
19040
|
}
|
|
19041
|
+
async checkSelf(repo) {
|
|
19042
|
+
if (await this.d.self.check(repo))
|
|
19043
|
+
this.d.hub.emit("selfChanged");
|
|
19044
|
+
}
|
|
18640
19045
|
startWatcher(repo) {
|
|
18641
19046
|
if (this.watchers.has(repo.id))
|
|
18642
19047
|
return;
|
|
@@ -18644,6 +19049,7 @@ class RepoRegistry {
|
|
|
18644
19049
|
this.d.worktrees.invalidateCounts();
|
|
18645
19050
|
this.d.hub.emit("repoTick", repo.id);
|
|
18646
19051
|
fireAndForget(repo.id, this.d.worktrees.spare.refresh(repo.id), "spare refresh");
|
|
19052
|
+
fireAndForget(repo.id, this.checkSelf(repo).then(() => this.d.afterLand.run(repo.id)), "self check");
|
|
18647
19053
|
});
|
|
18648
19054
|
const stopCfg = watchConfigFile(repo.path, () => this.reloadConfig(repo.id));
|
|
18649
19055
|
const stopWt = watchWorktreeDir(repo.path, () => {
|
|
@@ -19937,7 +20343,7 @@ class RouteService {
|
|
|
19937
20343
|
}
|
|
19938
20344
|
|
|
19939
20345
|
// ../daemon/src/runtime/bridge-script.ts
|
|
19940
|
-
import { existsSync as existsSync17, readFileSync as readFileSync10, statSync as
|
|
20346
|
+
import { existsSync as existsSync17, readFileSync as readFileSync10, statSync as statSync5 } from "fs";
|
|
19941
20347
|
var LEARNED_MAX = 8;
|
|
19942
20348
|
|
|
19943
20349
|
class BridgeScript {
|
|
@@ -19969,7 +20375,7 @@ class BridgeScript {
|
|
|
19969
20375
|
get() {
|
|
19970
20376
|
if (!existsSync17(this.path))
|
|
19971
20377
|
return "// toyon bridge not built";
|
|
19972
|
-
const s =
|
|
20378
|
+
const s = statSync5(this.path);
|
|
19973
20379
|
const stamp = `${s.mtimeMs}:${s.size}`;
|
|
19974
20380
|
if (this.cache && stamp === this.stamp)
|
|
19975
20381
|
return this.cache;
|
|
@@ -19993,6 +20399,343 @@ function servedLocally(origin) {
|
|
|
19993
20399
|
return h === "127.0.0.1" || h === "localhost" || h === "::1" || h === "[::1]" || h.endsWith(".localhost");
|
|
19994
20400
|
}
|
|
19995
20401
|
|
|
20402
|
+
// ../daemon/src/runtime/memory.ts
|
|
20403
|
+
var MAC_PRESSURE_NORMAL = 1;
|
|
20404
|
+
var AVAILABLE_FLOOR = 0.15;
|
|
20405
|
+
var unsupportedLogged = false;
|
|
20406
|
+
async function memoryTight() {
|
|
20407
|
+
const signal = process.platform === "darwin" ? await darwin() : process.platform === "linux" ? await linux() : null;
|
|
20408
|
+
if (signal === null && !unsupportedLogged) {
|
|
20409
|
+
unsupportedLogged = true;
|
|
20410
|
+
log.warn("memory", `no memory pressure reading on ${process.platform}; nothing sleeps for memory`);
|
|
20411
|
+
}
|
|
20412
|
+
return signal;
|
|
20413
|
+
}
|
|
20414
|
+
async function darwin() {
|
|
20415
|
+
const r = await run("sysctl", ["-n", "kern.memorystatus_vm_pressure_level", "kern.memorystatus_level"], "/");
|
|
20416
|
+
if (!r.ok)
|
|
20417
|
+
return null;
|
|
20418
|
+
const [level, available] = r.out.split(/\s+/).map(Number);
|
|
20419
|
+
if (level === undefined || available === undefined || Number.isNaN(level) || Number.isNaN(available))
|
|
20420
|
+
return null;
|
|
20421
|
+
const pressure = level === MAC_PRESSURE_NORMAL ? "normal" : level === 2 ? "warn" : level === 4 ? "critical" : `level ${level}`;
|
|
20422
|
+
return {
|
|
20423
|
+
tight: level !== MAC_PRESSURE_NORMAL,
|
|
20424
|
+
backstop: level === MAC_PRESSURE_NORMAL && available < AVAILABLE_FLOOR * 100,
|
|
20425
|
+
why: `memory pressure ${pressure} with ${available}% available`
|
|
20426
|
+
};
|
|
20427
|
+
}
|
|
20428
|
+
async function linux() {
|
|
20429
|
+
let text;
|
|
20430
|
+
try {
|
|
20431
|
+
text = await Bun.file("/proc/meminfo").text();
|
|
20432
|
+
} catch {
|
|
20433
|
+
return null;
|
|
20434
|
+
}
|
|
20435
|
+
const field = (name) => {
|
|
20436
|
+
const m = text.match(new RegExp(`^${name}:\\s+(\\d+)`, "m"));
|
|
20437
|
+
return m ? Number(m[1]) : null;
|
|
20438
|
+
};
|
|
20439
|
+
const total = field("MemTotal");
|
|
20440
|
+
const available = field("MemAvailable");
|
|
20441
|
+
if (!total || available === null)
|
|
20442
|
+
return null;
|
|
20443
|
+
const percent = Math.round(available / total * 100);
|
|
20444
|
+
return { tight: available / total < AVAILABLE_FLOOR, backstop: false, why: `${percent}% of memory available` };
|
|
20445
|
+
}
|
|
20446
|
+
async function sampleCosts(pgids) {
|
|
20447
|
+
const costs = new Map;
|
|
20448
|
+
if (pgids.length === 0)
|
|
20449
|
+
return costs;
|
|
20450
|
+
const wanted = new Set(pgids);
|
|
20451
|
+
const r = await run("ps", ["-axo", "pgid=,rss="], "/");
|
|
20452
|
+
if (!r.ok)
|
|
20453
|
+
return costs;
|
|
20454
|
+
for (const line of r.out.split(`
|
|
20455
|
+
`)) {
|
|
20456
|
+
const [pgid, rss] = line.trim().split(/\s+/).map(Number);
|
|
20457
|
+
if (pgid === undefined || rss === undefined || !wanted.has(pgid))
|
|
20458
|
+
continue;
|
|
20459
|
+
costs.set(pgid, (costs.get(pgid) ?? 0) + rss);
|
|
20460
|
+
}
|
|
20461
|
+
return costs;
|
|
20462
|
+
}
|
|
20463
|
+
|
|
20464
|
+
// ../daemon/src/runtime/idle.ts
|
|
20465
|
+
var LOCAL_SLEEP_MS = 2 * 60 * 60000;
|
|
20466
|
+
var CLOUD_SLEEP_MS = 5 * 60000;
|
|
20467
|
+
function sleepMsFrom(raw, inCloud) {
|
|
20468
|
+
if (raw === "off")
|
|
20469
|
+
return null;
|
|
20470
|
+
const n = Number(raw);
|
|
20471
|
+
if (raw && Number.isFinite(n) && n > 0)
|
|
20472
|
+
return n;
|
|
20473
|
+
return inCloud ? CLOUD_SLEEP_MS : LOCAL_SLEEP_MS;
|
|
20474
|
+
}
|
|
20475
|
+
var DEFAULT_SLEEP_MS = sleepMsFrom(process.env.TOYON_PROC_SLEEP_MS, cloud.enabled);
|
|
20476
|
+
var PRESSURE_EVERY_MS = 15000;
|
|
20477
|
+
var COSTS_EVERY_MS = 60000;
|
|
20478
|
+
var STAGGER_MS = 3000;
|
|
20479
|
+
|
|
20480
|
+
class IdlePolicy {
|
|
20481
|
+
d;
|
|
20482
|
+
entries = new Map;
|
|
20483
|
+
viewing = new Map;
|
|
20484
|
+
queue = [];
|
|
20485
|
+
draining = false;
|
|
20486
|
+
sleepMs;
|
|
20487
|
+
now;
|
|
20488
|
+
constructor(d) {
|
|
20489
|
+
this.d = d;
|
|
20490
|
+
this.sleepMs = d.sleepMs === undefined ? DEFAULT_SLEEP_MS : d.sleepMs;
|
|
20491
|
+
this.now = d.now ?? Date.now;
|
|
20492
|
+
d.hub.on("previewRequest", (id) => this.touch(id));
|
|
20493
|
+
d.hub.on("agentStatus", (id) => this.stamp(id));
|
|
20494
|
+
d.hub.on("holdsChanged", (id) => this.stamp(id));
|
|
20495
|
+
d.hub.on("turnSettled", (id) => {
|
|
20496
|
+
if (this.recentlyViewed(id))
|
|
20497
|
+
d.wake(id);
|
|
20498
|
+
});
|
|
20499
|
+
d.hub.on("proc", (id, p) => {
|
|
20500
|
+
if (p.status !== "starting")
|
|
20501
|
+
this.reconsider(id);
|
|
20502
|
+
});
|
|
20503
|
+
d.hub.on("worktreesChanged", () => this.reconsiderAll());
|
|
20504
|
+
const every = d.setInterval ?? ((fn, ms) => {
|
|
20505
|
+
setInterval(fn, ms).unref?.();
|
|
20506
|
+
});
|
|
20507
|
+
every(() => fireAndForget("idle", this.checkPressure(), "memory check"), PRESSURE_EVERY_MS);
|
|
20508
|
+
every(() => fireAndForget("idle", this.sampleCosts(), "cost sample"), COSTS_EVERY_MS);
|
|
20509
|
+
}
|
|
20510
|
+
view(key, id) {
|
|
20511
|
+
const prev = this.viewing.get(key) ?? null;
|
|
20512
|
+
if (prev === id)
|
|
20513
|
+
return;
|
|
20514
|
+
if (prev !== null) {
|
|
20515
|
+
this.viewing.delete(key);
|
|
20516
|
+
this.entry(prev).viewers.delete(key);
|
|
20517
|
+
this.record(prev);
|
|
20518
|
+
this.stamp(prev);
|
|
20519
|
+
}
|
|
20520
|
+
if (id === null)
|
|
20521
|
+
return;
|
|
20522
|
+
this.viewing.set(key, id);
|
|
20523
|
+
this.entry(id).viewers.add(key);
|
|
20524
|
+
const wt = this.record(id);
|
|
20525
|
+
this.stamp(id);
|
|
20526
|
+
this.d.wake(id);
|
|
20527
|
+
if (wt && wt.kind !== "spare") {
|
|
20528
|
+
this.enqueue(`spare:${wt.repoId}`, async () => {
|
|
20529
|
+
await this.d.runtime.awaitPreview(id, STAGGER_MS);
|
|
20530
|
+
this.d.warmSpare(wt.repoId);
|
|
20531
|
+
const spare = this.spareOf(wt.repoId);
|
|
20532
|
+
if (spare)
|
|
20533
|
+
this.d.wake(spare.id);
|
|
20534
|
+
});
|
|
20535
|
+
}
|
|
20536
|
+
}
|
|
20537
|
+
drop(key) {
|
|
20538
|
+
this.view(key, null);
|
|
20539
|
+
}
|
|
20540
|
+
shutdown() {
|
|
20541
|
+
this.recordViewed();
|
|
20542
|
+
}
|
|
20543
|
+
record(id) {
|
|
20544
|
+
const wt = this.d.state.worktree(id);
|
|
20545
|
+
if (!wt)
|
|
20546
|
+
return;
|
|
20547
|
+
wt.viewedAt = this.now();
|
|
20548
|
+
this.d.state.save();
|
|
20549
|
+
return wt;
|
|
20550
|
+
}
|
|
20551
|
+
recordViewed() {
|
|
20552
|
+
let changed = false;
|
|
20553
|
+
for (const id of new Set(this.viewing.values())) {
|
|
20554
|
+
const wt = this.d.state.worktree(id);
|
|
20555
|
+
if (!wt)
|
|
20556
|
+
continue;
|
|
20557
|
+
wt.viewedAt = this.now();
|
|
20558
|
+
changed = true;
|
|
20559
|
+
}
|
|
20560
|
+
if (changed)
|
|
20561
|
+
this.d.state.save();
|
|
20562
|
+
}
|
|
20563
|
+
touch(id) {
|
|
20564
|
+
this.stamp(id);
|
|
20565
|
+
}
|
|
20566
|
+
boot() {
|
|
20567
|
+
const recent = this.d.state.worktrees.filter((w) => w.kind !== "spare" && this.recentlyViewed(w.id)).sort((a, b) => (b.viewedAt ?? 0) - (a.viewedAt ?? 0));
|
|
20568
|
+
for (const wt of recent) {
|
|
20569
|
+
this.enqueue(`boot:${wt.id}`, async () => {
|
|
20570
|
+
this.d.wake(wt.id);
|
|
20571
|
+
await this.d.runtime.awaitPreview(wt.id, STAGGER_MS);
|
|
20572
|
+
});
|
|
20573
|
+
}
|
|
20574
|
+
}
|
|
20575
|
+
recentlyViewed(id) {
|
|
20576
|
+
const wt = this.d.state.worktree(id);
|
|
20577
|
+
return !!wt && this.now() - (wt.viewedAt ?? 0) < (this.sleepMs ?? LOCAL_SLEEP_MS);
|
|
20578
|
+
}
|
|
20579
|
+
isViewed(id) {
|
|
20580
|
+
if ((this.entries.get(id)?.viewers.size ?? 0) > 0)
|
|
20581
|
+
return true;
|
|
20582
|
+
const wt = this.d.state.worktree(id);
|
|
20583
|
+
if (wt?.kind !== "spare")
|
|
20584
|
+
return false;
|
|
20585
|
+
return this.d.state.worktrees.some((w) => w.repoId === wt.repoId && (this.entries.get(w.id)?.viewers.size ?? 0) > 0);
|
|
20586
|
+
}
|
|
20587
|
+
costs() {
|
|
20588
|
+
const out = {};
|
|
20589
|
+
for (const [id, e] of this.entries)
|
|
20590
|
+
if (e.costKb !== undefined)
|
|
20591
|
+
out[id] = e.costKb;
|
|
20592
|
+
return out;
|
|
20593
|
+
}
|
|
20594
|
+
async checkPressure() {
|
|
20595
|
+
const signal = await (this.d.memory ?? memoryTight)();
|
|
20596
|
+
if (!signal || !signal.tight && !signal.backstop)
|
|
20597
|
+
return;
|
|
20598
|
+
const pick = this.d.state.worktrees.filter((w) => this.candidate(w.id)).sort((a, b) => this.entry(a.id).activeAt - this.entry(b.id).activeAt)[0];
|
|
20599
|
+
if (!pick)
|
|
20600
|
+
return;
|
|
20601
|
+
const why = `${signal.why}; least recently used`;
|
|
20602
|
+
if (signal.tight)
|
|
20603
|
+
this.sleep(pick.id, why);
|
|
20604
|
+
else
|
|
20605
|
+
log.info(pick.id, `would sleep on the backstop: ${why}`);
|
|
20606
|
+
}
|
|
20607
|
+
async sampleCosts() {
|
|
20608
|
+
this.recordViewed();
|
|
20609
|
+
const awake = this.d.runtime.awake();
|
|
20610
|
+
const pgids = awake.flatMap((a) => a.pgids);
|
|
20611
|
+
if (pgids.length === 0)
|
|
20612
|
+
return;
|
|
20613
|
+
const rss = await (this.d.costs ?? sampleCosts)(pgids);
|
|
20614
|
+
for (const a of awake)
|
|
20615
|
+
this.entry(a.id).costKb = a.pgids.reduce((sum, g) => sum + (rss.get(g) ?? 0), 0);
|
|
20616
|
+
}
|
|
20617
|
+
entry(id) {
|
|
20618
|
+
let e = this.entries.get(id);
|
|
20619
|
+
if (!e) {
|
|
20620
|
+
e = { viewers: new Set, activeAt: this.now(), timer: null };
|
|
20621
|
+
this.entries.set(id, e);
|
|
20622
|
+
}
|
|
20623
|
+
return e;
|
|
20624
|
+
}
|
|
20625
|
+
spareOf(repoId) {
|
|
20626
|
+
return this.d.state.worktrees.find((w) => w.repoId === repoId && w.kind === "spare");
|
|
20627
|
+
}
|
|
20628
|
+
stamp(id) {
|
|
20629
|
+
this.entry(id).activeAt = this.now();
|
|
20630
|
+
this.reconsider(id);
|
|
20631
|
+
const wt = this.d.state.worktree(id);
|
|
20632
|
+
if (!wt || wt.kind === "spare")
|
|
20633
|
+
return;
|
|
20634
|
+
const spare = this.spareOf(wt.repoId);
|
|
20635
|
+
if (!spare)
|
|
20636
|
+
return;
|
|
20637
|
+
this.entry(spare.id).activeAt = this.now();
|
|
20638
|
+
this.reconsider(spare.id);
|
|
20639
|
+
}
|
|
20640
|
+
candidate(id) {
|
|
20641
|
+
const wt = this.d.state.worktree(id);
|
|
20642
|
+
if (!wt || this.isViewed(id) || this.busy(id))
|
|
20643
|
+
return false;
|
|
20644
|
+
const procs = this.d.runtime.get(id)?.procs;
|
|
20645
|
+
if (!procs || procs.asleep)
|
|
20646
|
+
return false;
|
|
20647
|
+
const states = procs.states();
|
|
20648
|
+
if (states.length === 0 || states.some((s) => s.status === "starting"))
|
|
20649
|
+
return false;
|
|
20650
|
+
if (wt.kind === "spare") {
|
|
20651
|
+
return !this.d.state.worktrees.some((w) => w.repoId === wt.repoId && w.id !== id && this.busy(w.id));
|
|
20652
|
+
}
|
|
20653
|
+
return true;
|
|
20654
|
+
}
|
|
20655
|
+
busy(id) {
|
|
20656
|
+
const agent = this.d.runtime.agentFor(id);
|
|
20657
|
+
if (agent && (agent.status !== "idle" || agent.queueItems.length > 0))
|
|
20658
|
+
return true;
|
|
20659
|
+
return this.d.runtime.holdCount(id) > 0;
|
|
20660
|
+
}
|
|
20661
|
+
reconsider(id) {
|
|
20662
|
+
if (!this.candidate(id)) {
|
|
20663
|
+
this.disarm(id);
|
|
20664
|
+
return;
|
|
20665
|
+
}
|
|
20666
|
+
if (this.sleepMs === null)
|
|
20667
|
+
return;
|
|
20668
|
+
this.arm(id, Math.max(0, this.entry(id).activeAt + this.sleepMs - this.now()));
|
|
20669
|
+
}
|
|
20670
|
+
reconsiderAll() {
|
|
20671
|
+
for (const id of this.entries.keys()) {
|
|
20672
|
+
if (this.d.state.worktree(id))
|
|
20673
|
+
continue;
|
|
20674
|
+
this.disarm(id);
|
|
20675
|
+
this.entries.delete(id);
|
|
20676
|
+
}
|
|
20677
|
+
for (const wt of this.d.state.worktrees)
|
|
20678
|
+
this.reconsider(wt.id);
|
|
20679
|
+
}
|
|
20680
|
+
arm(id, ms) {
|
|
20681
|
+
this.disarm(id);
|
|
20682
|
+
const set = this.d.setTimer ?? ((fn, delay) => {
|
|
20683
|
+
const t = setTimeout(fn, delay);
|
|
20684
|
+
t.unref?.();
|
|
20685
|
+
return t;
|
|
20686
|
+
});
|
|
20687
|
+
this.entry(id).timer = set(() => this.fire(id), ms);
|
|
20688
|
+
}
|
|
20689
|
+
disarm(id) {
|
|
20690
|
+
const e = this.entries.get(id);
|
|
20691
|
+
if (!e?.timer)
|
|
20692
|
+
return;
|
|
20693
|
+
(this.d.clearTimer ?? ((t) => clearTimeout(t)))(e.timer);
|
|
20694
|
+
e.timer = null;
|
|
20695
|
+
}
|
|
20696
|
+
fire(id) {
|
|
20697
|
+
const e = this.entry(id);
|
|
20698
|
+
e.timer = null;
|
|
20699
|
+
if (this.sleepMs === null || !this.candidate(id))
|
|
20700
|
+
return;
|
|
20701
|
+
const idle = this.now() - e.activeAt;
|
|
20702
|
+
if (idle < this.sleepMs) {
|
|
20703
|
+
this.arm(id, this.sleepMs - idle);
|
|
20704
|
+
return;
|
|
20705
|
+
}
|
|
20706
|
+
this.sleep(id, `idle ${humanMs(this.sleepMs)}`);
|
|
20707
|
+
}
|
|
20708
|
+
sleep(id, why) {
|
|
20709
|
+
this.disarm(id);
|
|
20710
|
+
this.entry(id).costKb = undefined;
|
|
20711
|
+
log.info(id, `asleep: ${why}`);
|
|
20712
|
+
fireAndForget(id, this.d.runtime.sleep(id), "sleep");
|
|
20713
|
+
}
|
|
20714
|
+
enqueue(key, run) {
|
|
20715
|
+
if (this.queue.some((q) => q.key === key))
|
|
20716
|
+
return;
|
|
20717
|
+
this.queue.push({ key, run });
|
|
20718
|
+
if (!this.draining)
|
|
20719
|
+
fireAndForget("idle", this.drain(), "wake queue");
|
|
20720
|
+
}
|
|
20721
|
+
async drain() {
|
|
20722
|
+
this.draining = true;
|
|
20723
|
+
try {
|
|
20724
|
+
for (let next = this.queue.shift();next; next = this.queue.shift())
|
|
20725
|
+
await next.run();
|
|
20726
|
+
} finally {
|
|
20727
|
+
this.draining = false;
|
|
20728
|
+
}
|
|
20729
|
+
}
|
|
20730
|
+
}
|
|
20731
|
+
function humanMs(ms) {
|
|
20732
|
+
if (ms >= 60 * 60000 && ms % (60 * 60000) === 0)
|
|
20733
|
+
return `${ms / (60 * 60000)}h`;
|
|
20734
|
+
if (ms >= 60000)
|
|
20735
|
+
return `${Math.round(ms / 60000)} min`;
|
|
20736
|
+
return `${Math.round(ms / 1000)}s`;
|
|
20737
|
+
}
|
|
20738
|
+
|
|
19996
20739
|
// ../daemon/src/server/ws.ts
|
|
19997
20740
|
import { homedir as homedir9 } from "os";
|
|
19998
20741
|
|
|
@@ -20597,7 +21340,7 @@ function daylightNow(tz, at = new Date) {
|
|
|
20597
21340
|
}
|
|
20598
21341
|
|
|
20599
21342
|
// ../daemon/src/server/handlers.ts
|
|
20600
|
-
var
|
|
21343
|
+
var shipped = (worktreeId, ok, message, extra = {}) => ({ t: "shipped", worktreeId, ok, message, ...extra });
|
|
20601
21344
|
var errorText = (e) => e instanceof Error ? e.message : String(e);
|
|
20602
21345
|
var gitStatus = async (s, ctx, worktreeId) => {
|
|
20603
21346
|
const info = await s.worktrees.gitStatus(worktreeId);
|
|
@@ -20611,6 +21354,8 @@ var gitAndPages = async (s, ctx, worktreeId) => {
|
|
|
20611
21354
|
};
|
|
20612
21355
|
var notify = async (s, ctx, worktreeId, msg) => {
|
|
20613
21356
|
ctx.reply(msg);
|
|
21357
|
+
if (!msg.ok)
|
|
21358
|
+
s.turns.markUnread(worktreeId);
|
|
20614
21359
|
await gitStatus(s, ctx, worktreeId);
|
|
20615
21360
|
};
|
|
20616
21361
|
var requireRun = (s, id) => {
|
|
@@ -20622,8 +21367,17 @@ var requireRun = (s, id) => {
|
|
|
20622
21367
|
var handlers = {
|
|
20623
21368
|
async subscribe(msg, ctx, s) {
|
|
20624
21369
|
const r = s.worktrees.readable(msg.worktreeId);
|
|
20625
|
-
if (!r)
|
|
20626
|
-
|
|
21370
|
+
if (!r) {
|
|
21371
|
+
const kept = s.worktrees.archivedTranscript(msg.worktreeId);
|
|
21372
|
+
if (!kept)
|
|
21373
|
+
throw new UserError("unknown worktree");
|
|
21374
|
+
if (!ctx.subscribe(msg.worktreeId))
|
|
21375
|
+
return;
|
|
21376
|
+
ctx.reply({ t: "backfill", worktreeId: msg.worktreeId, events: coalesce(kept), log: [] });
|
|
21377
|
+
ctx.reply({ t: "queue", worktreeId: msg.worktreeId, items: [] });
|
|
21378
|
+
ctx.reply({ t: "agent-commands", worktreeId: msg.worktreeId, commands: [] });
|
|
21379
|
+
return;
|
|
21380
|
+
}
|
|
20627
21381
|
if (!ctx.subscribe(msg.worktreeId))
|
|
20628
21382
|
return;
|
|
20629
21383
|
if (!r.wt) {
|
|
@@ -20633,13 +21387,11 @@ var handlers = {
|
|
|
20633
21387
|
await gitAndPages(s, ctx, msg.worktreeId);
|
|
20634
21388
|
return;
|
|
20635
21389
|
}
|
|
20636
|
-
s.repos.touch(msg.worktreeId);
|
|
20637
21390
|
const agent = s.runtime.ensureAgent(r.wt).agent;
|
|
20638
|
-
const events = agent.transcript();
|
|
20639
21391
|
ctx.reply({
|
|
20640
21392
|
t: "backfill",
|
|
20641
21393
|
worktreeId: msg.worktreeId,
|
|
20642
|
-
events:
|
|
21394
|
+
events: coalesce(agent.transcript()),
|
|
20643
21395
|
log: s.runtime.recentLogs(msg.worktreeId)
|
|
20644
21396
|
});
|
|
20645
21397
|
ctx.reply({ t: "queue", worktreeId: msg.worktreeId, items: agent.queueItems });
|
|
@@ -20649,6 +21401,9 @@ var handlers = {
|
|
|
20649
21401
|
unsubscribe(msg, ctx) {
|
|
20650
21402
|
ctx.unsubscribe(msg.worktreeId);
|
|
20651
21403
|
},
|
|
21404
|
+
view(msg, ctx) {
|
|
21405
|
+
ctx.view(msg.worktreeId);
|
|
21406
|
+
},
|
|
20652
21407
|
seen(msg, _ctx, s) {
|
|
20653
21408
|
s.turns.markSeen(msg.worktreeId);
|
|
20654
21409
|
},
|
|
@@ -20678,7 +21433,6 @@ var handlers = {
|
|
|
20678
21433
|
async "create-worktree"(msg, _ctx, s) {
|
|
20679
21434
|
await s.worktrees.create(msg.repoId, msg.prompt, {
|
|
20680
21435
|
createdBy: msg.clientId,
|
|
20681
|
-
baseWorktreeId: msg.baseWorktreeId,
|
|
20682
21436
|
variant: msg.variant,
|
|
20683
21437
|
context: msg.context,
|
|
20684
21438
|
attachments: msg.attachments,
|
|
@@ -20686,7 +21440,8 @@ var handlers = {
|
|
|
20686
21440
|
profile: msg.profile,
|
|
20687
21441
|
mode: msg.mode,
|
|
20688
21442
|
model: msg.model,
|
|
20689
|
-
effort: msg.effort
|
|
21443
|
+
effort: msg.effort,
|
|
21444
|
+
carry: msg.carry
|
|
20690
21445
|
});
|
|
20691
21446
|
},
|
|
20692
21447
|
"set-worktree-profile"(msg, _ctx, s) {
|
|
@@ -20703,7 +21458,6 @@ var handlers = {
|
|
|
20703
21458
|
},
|
|
20704
21459
|
"batch-worktrees"(msg, ctx, s) {
|
|
20705
21460
|
const repo = s.state.requireRepo(msg.repoId);
|
|
20706
|
-
ctx.reply(toast("", true, "batch: planning tasks\u2026"));
|
|
20707
21461
|
fireAndForget(msg.repoId, (async () => {
|
|
20708
21462
|
const agent = msg.agent ?? s.state.defaultAgent ?? DEFAULT_AGENT_ID;
|
|
20709
21463
|
const tasks = await s.planTasks(msg.prompt, repo.path, agent) ?? [msg.prompt];
|
|
@@ -20717,22 +21471,18 @@ var handlers = {
|
|
|
20717
21471
|
}
|
|
20718
21472
|
}
|
|
20719
21473
|
const started = tasks.length - failed;
|
|
20720
|
-
|
|
21474
|
+
if (failed)
|
|
21475
|
+
ctx.reply(shipped("", false, `batch: ${started} started, ${failed} failed (see daemon log)`));
|
|
20721
21476
|
})(), "batch");
|
|
20722
21477
|
},
|
|
20723
|
-
async "remove-worktree"(msg,
|
|
20724
|
-
|
|
20725
|
-
if (archived) {
|
|
20726
|
-
ctx.reply(toast(msg.worktreeId, true, `removed ${archived.title}: archived`, {
|
|
20727
|
-
...archived.restorable ? { restoreId: archived.id } : {}
|
|
20728
|
-
}));
|
|
20729
|
-
}
|
|
21478
|
+
async "remove-worktree"(msg, _ctx, s) {
|
|
21479
|
+
await s.worktrees.remove(msg.worktreeId);
|
|
20730
21480
|
},
|
|
20731
21481
|
"list-archived"(msg, ctx, s) {
|
|
20732
21482
|
ctx.reply({ t: "archived", repoId: msg.repoId, items: s.worktrees.archived(msg.repoId) });
|
|
20733
21483
|
},
|
|
20734
21484
|
async "restore-worktree"(msg, _ctx, s) {
|
|
20735
|
-
await s.worktrees.restore(msg.archiveId, msg.clientId);
|
|
21485
|
+
await s.worktrees.restore(msg.archiveId, msg.clientId, msg.message);
|
|
20736
21486
|
},
|
|
20737
21487
|
async "delete-archived"(msg, _ctx, s) {
|
|
20738
21488
|
await s.worktrees.deleteArchived(msg.archiveId);
|
|
@@ -20768,7 +21518,7 @@ var handlers = {
|
|
|
20768
21518
|
if (result.ok && (result.pr || s.state.worktree(msg.worktreeId)?.pr)) {
|
|
20769
21519
|
fireAndForget(msg.worktreeId, s.prs.refresh(msg.worktreeId), "pr refresh");
|
|
20770
21520
|
}
|
|
20771
|
-
await notify(s, ctx, msg.worktreeId,
|
|
21521
|
+
await notify(s, ctx, msg.worktreeId, shipped(msg.worktreeId, result.ok, result.message, {
|
|
20772
21522
|
merged: result.ok,
|
|
20773
21523
|
url: result.url,
|
|
20774
21524
|
removeIds: removeIds ?? [],
|
|
@@ -20778,21 +21528,41 @@ var handlers = {
|
|
|
20778
21528
|
async "sync-main"(msg, ctx, s) {
|
|
20779
21529
|
const { result, defaultBranch } = await s.worktrees.sync(msg.worktreeId);
|
|
20780
21530
|
const prompt = !result.ok && result.conflict && s.state.worktree(msg.worktreeId);
|
|
20781
|
-
await notify(s, ctx, msg.worktreeId, prompt ?
|
|
21531
|
+
await notify(s, ctx, msg.worktreeId, prompt ? shipped(msg.worktreeId, false, `sync conflicts with ${defaultBranch}: prompt prefilled in chat`, {
|
|
20782
21532
|
suggestion: `Merge ${defaultBranch} into this branch and resolve the conflicts, then verify the app still works.`
|
|
20783
|
-
}) :
|
|
21533
|
+
}) : shipped(msg.worktreeId, result.ok, result.message));
|
|
20784
21534
|
},
|
|
20785
21535
|
async "pull-main"(msg, ctx, s) {
|
|
20786
21536
|
const result = await s.worktrees.pull(msg.worktreeId);
|
|
20787
|
-
await notify(s, ctx, msg.worktreeId,
|
|
21537
|
+
await notify(s, ctx, msg.worktreeId, shipped(msg.worktreeId, result.ok, result.message));
|
|
21538
|
+
},
|
|
21539
|
+
"run-after-land"(msg, ctx, s) {
|
|
21540
|
+
const repo = s.state.repo(msg.repoId);
|
|
21541
|
+
if (!repo)
|
|
21542
|
+
throw new UserError("no such project");
|
|
21543
|
+
if ((repo.config.afterLand ?? []).length === 0) {
|
|
21544
|
+
throw new UserError(`${repo.name} has no afterLand commands in ${repo.configFile}`);
|
|
21545
|
+
}
|
|
21546
|
+
s.afterLand.run(msg.repoId);
|
|
21547
|
+
ctx.reply({ t: "self", self: s.self.get() });
|
|
21548
|
+
},
|
|
21549
|
+
"restart-daemon"(_msg, _ctx, s) {
|
|
21550
|
+
const busy = s.state.worktrees.filter((w) => s.runtime.agentFor(w.id)?.status === "working");
|
|
21551
|
+
if (busy.length > 0) {
|
|
21552
|
+
const names = busy.map((w) => w.title).join(", ");
|
|
21553
|
+
throw new UserError(`still working on ${names}; restart once that settles`);
|
|
21554
|
+
}
|
|
21555
|
+
const refused = s.restart();
|
|
21556
|
+
if (refused)
|
|
21557
|
+
throw new UserError(refused);
|
|
20788
21558
|
},
|
|
20789
21559
|
async commit(msg, ctx, s) {
|
|
20790
21560
|
const result = await s.worktrees.commit(msg.worktreeId, msg.message);
|
|
20791
|
-
await notify(s, ctx, msg.worktreeId,
|
|
21561
|
+
await notify(s, ctx, msg.worktreeId, shipped(msg.worktreeId, result.ok, result.message));
|
|
20792
21562
|
},
|
|
20793
21563
|
async graft(msg, ctx, s) {
|
|
20794
|
-
const { target
|
|
20795
|
-
await
|
|
21564
|
+
const { target } = await s.worktrees.graft(msg.targetId, msg.sourceIds);
|
|
21565
|
+
await gitStatus(s, ctx, target.id);
|
|
20796
21566
|
},
|
|
20797
21567
|
async "rename-worktree"(msg, _ctx, s) {
|
|
20798
21568
|
await s.worktrees.rename(msg.worktreeId, msg.title);
|
|
@@ -20837,9 +21607,8 @@ var handlers = {
|
|
|
20837
21607
|
reveal(msg, _ctx, s) {
|
|
20838
21608
|
s.files.reveal(msg.worktreeId, msg.path);
|
|
20839
21609
|
},
|
|
20840
|
-
async "discard-file"(msg,
|
|
21610
|
+
async "discard-file"(msg, _ctx, s) {
|
|
20841
21611
|
await s.files.discard(msg.worktreeId, msg.path);
|
|
20842
|
-
ctx.reply(toast(msg.worktreeId, true, `discarded ${msg.path}`));
|
|
20843
21612
|
s.hub.emit("filesChanged", msg.worktreeId);
|
|
20844
21613
|
},
|
|
20845
21614
|
async "write-file"(msg, ctx, s) {
|
|
@@ -20856,19 +21625,17 @@ var handlers = {
|
|
|
20856
21625
|
}
|
|
20857
21626
|
},
|
|
20858
21627
|
async "confirm-config"(msg, _ctx, s) {
|
|
20859
|
-
await s.repos.confirmConfig(msg.repoId, msg.config);
|
|
21628
|
+
await s.repos.confirmConfig(msg.repoId, msg.config, msg.kind);
|
|
20860
21629
|
},
|
|
20861
|
-
async "register-repo"(msg,
|
|
20862
|
-
|
|
20863
|
-
ctx.reply(toast("", true, `opened ${repo.name}`));
|
|
21630
|
+
async "register-repo"(msg, _ctx, s) {
|
|
21631
|
+
await s.repos.register(msg.path);
|
|
20864
21632
|
},
|
|
20865
|
-
async "create-repo"(msg,
|
|
21633
|
+
async "create-repo"(msg, _ctx, s) {
|
|
20866
21634
|
if (msg.mode === "clone") {
|
|
20867
21635
|
s.repos.startImport({ parent: msg.parent, name: msg.name, url: msg.url ?? "" });
|
|
20868
21636
|
return;
|
|
20869
21637
|
}
|
|
20870
|
-
|
|
20871
|
-
ctx.reply(toast("", true, `created ${repo.name}`));
|
|
21638
|
+
await s.repos.create(msg);
|
|
20872
21639
|
},
|
|
20873
21640
|
async "unmake-repo"(msg, _ctx, s) {
|
|
20874
21641
|
await s.repos.unmake(msg.repoId);
|
|
@@ -20892,10 +21659,8 @@ var handlers = {
|
|
|
20892
21659
|
"cancel-folder"(_msg, _ctx, s) {
|
|
20893
21660
|
s.folderDialog.cancel();
|
|
20894
21661
|
},
|
|
20895
|
-
async "forget-repo"(msg,
|
|
20896
|
-
const name = s.state.requireRepo(msg.repoId).name;
|
|
21662
|
+
async "forget-repo"(msg, _ctx, s) {
|
|
20897
21663
|
await s.repos.forget(msg.repoId);
|
|
20898
|
-
ctx.reply(toast("", true, `forgot ${name}`));
|
|
20899
21664
|
},
|
|
20900
21665
|
"set-theme"(msg, _ctx, s) {
|
|
20901
21666
|
s.themes.setPrefs(msg.prefs);
|
|
@@ -21005,6 +21770,10 @@ var handlers = {
|
|
|
21005
21770
|
const refs = await s.refs.search(msg.repoId, msg.query);
|
|
21006
21771
|
ctx.reply({ t: "refs", repoId: msg.repoId, query: msg.query, refs });
|
|
21007
21772
|
},
|
|
21773
|
+
async "search-chats"(msg, ctx, s) {
|
|
21774
|
+
const { hits, truncated } = await s.chats.search(msg.repoId, msg.query);
|
|
21775
|
+
ctx.reply({ t: "chat-hits", repoId: msg.repoId, query: msg.query, hits, truncated });
|
|
21776
|
+
},
|
|
21008
21777
|
async "open-ref"(msg, _ctx, s) {
|
|
21009
21778
|
await s.worktrees.openRef(msg.repoId, msg.kind, msg.ref, { createdBy: msg.clientId, pr: msg.pr });
|
|
21010
21779
|
}
|
|
@@ -21061,6 +21830,7 @@ function createFetch(opts) {
|
|
|
21061
21830
|
dropped: new Set,
|
|
21062
21831
|
sent: 0,
|
|
21063
21832
|
bytes: 0,
|
|
21833
|
+
view: null,
|
|
21064
21834
|
preview: { handler, data }
|
|
21065
21835
|
}
|
|
21066
21836
|
}));
|
|
@@ -21069,7 +21839,15 @@ function createFetch(opts) {
|
|
|
21069
21839
|
const authed = sameSecret(url.searchParams.get("token"), opts.token);
|
|
21070
21840
|
if (authed)
|
|
21071
21841
|
opts.noteShellOrigin(req.headers.get("origin"));
|
|
21072
|
-
const data = {
|
|
21842
|
+
const data = {
|
|
21843
|
+
authed,
|
|
21844
|
+
subs: new Set,
|
|
21845
|
+
terms: new Set,
|
|
21846
|
+
dropped: new Set,
|
|
21847
|
+
sent: 0,
|
|
21848
|
+
bytes: 0,
|
|
21849
|
+
view: null
|
|
21850
|
+
};
|
|
21073
21851
|
const headers = authed && remote && remoteShell ? { "set-cookie": grantCookie(grant, remote.host) } : undefined;
|
|
21074
21852
|
if (srv.upgrade(req, { data, headers }))
|
|
21075
21853
|
return;
|
|
@@ -21106,8 +21884,9 @@ function createFetch(opts) {
|
|
|
21106
21884
|
if (!sameSecret(url.searchParams.get("token"), opts.token))
|
|
21107
21885
|
return new Response("unauthorized", { status: 401 });
|
|
21108
21886
|
const [worktreeId, file, extra] = url.pathname.slice("/attachments/".length).split("/");
|
|
21109
|
-
const
|
|
21110
|
-
|
|
21887
|
+
const places = worktreeId && file && !extra ? [opts.attachments.fileFor(worktreeId, file), opts.archivedAttachment(worktreeId, file)] : [];
|
|
21888
|
+
const path = places.find((p) => !!p && existsSync19(p));
|
|
21889
|
+
if (!path)
|
|
21111
21890
|
return new Response("not found", { status: 404 });
|
|
21112
21891
|
return new Response(Bun.file(path), { headers: { "cache-control": IMMUTABLE } });
|
|
21113
21892
|
}
|
|
@@ -21214,8 +21993,10 @@ function startServer(opts) {
|
|
|
21214
21993
|
lag,
|
|
21215
21994
|
worktrees: {
|
|
21216
21995
|
total: s.state.worktrees.filter((w) => w.kind !== "spare").length,
|
|
21217
|
-
running: s.runtime.runningCount()
|
|
21996
|
+
running: s.runtime.runningCount(),
|
|
21997
|
+
...s.runtime.tiers()
|
|
21218
21998
|
},
|
|
21999
|
+
costs: s.idle.costs(),
|
|
21219
22000
|
sockets: [...sockets].map((ws) => ({ subs: [...ws.data.subs], sent: ws.data.sent, bytes: ws.data.bytes }))
|
|
21220
22001
|
});
|
|
21221
22002
|
let statusesInFlight = false;
|
|
@@ -21320,9 +22101,11 @@ function startServer(opts) {
|
|
|
21320
22101
|
const agentsMsg = () => ({
|
|
21321
22102
|
t: "agents",
|
|
21322
22103
|
agents: agentInfos(),
|
|
21323
|
-
defaultAgent: s.state.defaultAgent ?? DEFAULT_AGENT_ID
|
|
22104
|
+
defaultAgent: s.state.defaultAgent ?? DEFAULT_AGENT_ID,
|
|
22105
|
+
agentChosen: s.state.defaultAgent !== undefined
|
|
21324
22106
|
});
|
|
21325
22107
|
s.hub.on("agentsChanged", () => broadcast(agentsMsg()));
|
|
22108
|
+
s.hub.on("selfChanged", () => broadcast({ t: "self", self: s.self.get() }));
|
|
21326
22109
|
s.hub.on("visitsChanged", (repoId) => broadcast({ t: "visits", repoId, pages: s.routes.history(repoId) }));
|
|
21327
22110
|
s.hub.on("archiveChanged", (repoId) => broadcast({ t: "archived", repoId, items: s.worktrees.archived(repoId) }));
|
|
21328
22111
|
const helloFrame = async () => ({
|
|
@@ -21336,12 +22119,14 @@ function startServer(opts) {
|
|
|
21336
22119
|
themePrefs: s.themes.prefs,
|
|
21337
22120
|
agents: agentInfos(),
|
|
21338
22121
|
defaultAgent: s.state.defaultAgent ?? DEFAULT_AGENT_ID,
|
|
22122
|
+
agentChosen: s.state.defaultAgent !== undefined,
|
|
21339
22123
|
home: homedir9(),
|
|
21340
22124
|
folderDialog: process.platform === "darwin" && !cloud.enabled,
|
|
21341
22125
|
remote: opts.remote && { host: opts.remote.host, previews: opts.remote.previews },
|
|
21342
22126
|
gitIdentity: await s.repos.gitIdentity(),
|
|
21343
22127
|
pending: s.repos.pending,
|
|
21344
|
-
visits: s.routes.historyAll()
|
|
22128
|
+
visits: s.routes.historyAll(),
|
|
22129
|
+
self: s.self.get()
|
|
21345
22130
|
});
|
|
21346
22131
|
let branded = false;
|
|
21347
22132
|
const serverConfig = {
|
|
@@ -21352,6 +22137,7 @@ function startServer(opts) {
|
|
|
21352
22137
|
version,
|
|
21353
22138
|
repos: s.repos,
|
|
21354
22139
|
attachments: s.attachments,
|
|
22140
|
+
archivedAttachment: (id, file) => s.worktrees.archivedAttachment(id, file),
|
|
21355
22141
|
branded: () => branded,
|
|
21356
22142
|
metrics,
|
|
21357
22143
|
noteShellOrigin: opts.noteShellOrigin,
|
|
@@ -21377,6 +22163,8 @@ function startServer(opts) {
|
|
|
21377
22163
|
close(ws) {
|
|
21378
22164
|
if (ws.data.preview)
|
|
21379
22165
|
ws.data.preview.handler.close(ws.data.preview.data);
|
|
22166
|
+
else
|
|
22167
|
+
s.idle.drop(ws.data);
|
|
21380
22168
|
sockets.delete(ws);
|
|
21381
22169
|
},
|
|
21382
22170
|
async message(ws, raw) {
|
|
@@ -21409,6 +22197,14 @@ function startServer(opts) {
|
|
|
21409
22197
|
},
|
|
21410
22198
|
unsubscribe: (id) => {
|
|
21411
22199
|
ws.data.subs.delete(id);
|
|
22200
|
+
if (ws.data.view === id) {
|
|
22201
|
+
ws.data.view = null;
|
|
22202
|
+
s.idle.view(ws.data, null);
|
|
22203
|
+
}
|
|
22204
|
+
},
|
|
22205
|
+
view: (id) => {
|
|
22206
|
+
ws.data.view = id;
|
|
22207
|
+
s.idle.view(ws.data, id);
|
|
21412
22208
|
},
|
|
21413
22209
|
watchTerminal: (id, stream) => {
|
|
21414
22210
|
ws.data.terms.add(streamKey(id, stream));
|
|
@@ -21424,7 +22220,12 @@ function startServer(opts) {
|
|
|
21424
22220
|
} catch (e) {
|
|
21425
22221
|
if (!(e instanceof UserError || e instanceof ThemeImportError))
|
|
21426
22222
|
log.error("ws", `${parsed.msg.t} failed`, e);
|
|
21427
|
-
|
|
22223
|
+
const worktreeId = "worktreeId" in parsed.msg ? parsed.msg.worktreeId : undefined;
|
|
22224
|
+
send(ws, {
|
|
22225
|
+
t: "error",
|
|
22226
|
+
message: e instanceof Error ? e.message : String(e),
|
|
22227
|
+
...worktreeId ? { worktreeId } : {}
|
|
22228
|
+
});
|
|
21428
22229
|
}
|
|
21429
22230
|
}
|
|
21430
22231
|
}
|
|
@@ -21448,9 +22249,9 @@ function startServer(opts) {
|
|
|
21448
22249
|
}
|
|
21449
22250
|
|
|
21450
22251
|
// ../daemon/src/themes/store.ts
|
|
21451
|
-
import { existsSync as existsSync20, readdirSync as
|
|
22252
|
+
import { existsSync as existsSync20, readdirSync as readdirSync5, readFileSync as readFileSync12, statSync as statSync6, writeFileSync as writeFileSync6 } from "fs";
|
|
21452
22253
|
import { homedir as homedir10 } from "os";
|
|
21453
|
-
import { basename as basename4, dirname as dirname7, join as join18, resolve as
|
|
22254
|
+
import { basename as basename4, dirname as dirname7, join as join18, resolve as resolve8 } from "path";
|
|
21454
22255
|
|
|
21455
22256
|
// ../../node_modules/.bun/jsonc-parser@3.3.1/node_modules/jsonc-parser/lib/esm/impl/scanner.js
|
|
21456
22257
|
function createScanner(text, ignoreTrivia = false) {
|
|
@@ -22347,7 +23148,7 @@ class ThemeStore {
|
|
|
22347
23148
|
name: json?.name ?? base,
|
|
22348
23149
|
source: "file"
|
|
22349
23150
|
});
|
|
22350
|
-
|
|
23151
|
+
writeFileSync6(join18(this.themesDir, `${base}.json`), `${JSON.stringify(theme, null, 2)}
|
|
22351
23152
|
`);
|
|
22352
23153
|
this.load();
|
|
22353
23154
|
return theme;
|
|
@@ -22362,7 +23163,7 @@ function readVscodeTheme(file, depth = 0) {
|
|
|
22362
23163
|
throw new ThemeImportError(`${file}: not a JSON object`);
|
|
22363
23164
|
const t = json;
|
|
22364
23165
|
if (typeof t.include === "string" && depth < 5) {
|
|
22365
|
-
const parent = readVscodeTheme(
|
|
23166
|
+
const parent = readVscodeTheme(resolve8(dirname7(file), t.include), depth + 1);
|
|
22366
23167
|
return {
|
|
22367
23168
|
...parent,
|
|
22368
23169
|
...t,
|
|
@@ -22379,7 +23180,7 @@ function loadDir(dir) {
|
|
|
22379
23180
|
if (!existsSync20(dir))
|
|
22380
23181
|
return [];
|
|
22381
23182
|
const out = [];
|
|
22382
|
-
for (const f of
|
|
23183
|
+
for (const f of readdirSync5(dir)) {
|
|
22383
23184
|
if (!/\.jsonc?$/i.test(f))
|
|
22384
23185
|
continue;
|
|
22385
23186
|
const file = join18(dir, f);
|
|
@@ -22403,7 +23204,7 @@ function discover(dir) {
|
|
|
22403
23204
|
const out = [];
|
|
22404
23205
|
let entries;
|
|
22405
23206
|
try {
|
|
22406
|
-
entries =
|
|
23207
|
+
entries = readdirSync5(dir);
|
|
22407
23208
|
} catch {
|
|
22408
23209
|
return out;
|
|
22409
23210
|
}
|
|
@@ -22411,7 +23212,7 @@ function discover(dir) {
|
|
|
22411
23212
|
const extDir = join18(dir, name);
|
|
22412
23213
|
const pkgFile = join18(extDir, "package.json");
|
|
22413
23214
|
try {
|
|
22414
|
-
if (!
|
|
23215
|
+
if (!statSync6(extDir).isDirectory() || !existsSync20(pkgFile))
|
|
22415
23216
|
continue;
|
|
22416
23217
|
const pkg = JSON.parse(readFileSync12(pkgFile, "utf8"));
|
|
22417
23218
|
const themes = pkg.contributes?.themes;
|
|
@@ -22431,7 +23232,7 @@ function discover(dir) {
|
|
|
22431
23232
|
const m = /^%(.+)%$/.exec(c.label);
|
|
22432
23233
|
if (m)
|
|
22433
23234
|
c.label = nls[m[1]] ?? m[1];
|
|
22434
|
-
const file =
|
|
23235
|
+
const file = resolve8(extDir, c.path);
|
|
22435
23236
|
try {
|
|
22436
23237
|
const json = readVscodeTheme(file);
|
|
22437
23238
|
out.push(vscodeToTheme(json, { id: `vscode:${ext}:${slug(c.label)}`, name: c.label, source: "vscode" }));
|
|
@@ -22446,6 +23247,129 @@ function discover(dir) {
|
|
|
22446
23247
|
return out;
|
|
22447
23248
|
}
|
|
22448
23249
|
|
|
23250
|
+
// ../daemon/src/worktrees/chats.ts
|
|
23251
|
+
var CHAT_HITS_PER_WORKTREE = 20;
|
|
23252
|
+
var CHAT_HITS_MAX = 200;
|
|
23253
|
+
var MIN_QUERY = 2;
|
|
23254
|
+
var SNIPPET = 160;
|
|
23255
|
+
var BEFORE = 50;
|
|
23256
|
+
var bunFs2 = {
|
|
23257
|
+
async stat(path) {
|
|
23258
|
+
try {
|
|
23259
|
+
const s = await Bun.file(path).stat();
|
|
23260
|
+
return { size: s.size, mtimeMs: s.mtimeMs };
|
|
23261
|
+
} catch {
|
|
23262
|
+
return null;
|
|
23263
|
+
}
|
|
23264
|
+
},
|
|
23265
|
+
async text(path) {
|
|
23266
|
+
try {
|
|
23267
|
+
return await Bun.file(path).text();
|
|
23268
|
+
} catch {
|
|
23269
|
+
return null;
|
|
23270
|
+
}
|
|
23271
|
+
}
|
|
23272
|
+
};
|
|
23273
|
+
var flat = (text) => text.replace(/\s+/g, " ").trim();
|
|
23274
|
+
function saidRows(entries) {
|
|
23275
|
+
const out = [];
|
|
23276
|
+
let turnTs = 0;
|
|
23277
|
+
const push = (seq, role, said, ts) => {
|
|
23278
|
+
const text = flat(said);
|
|
23279
|
+
if (text)
|
|
23280
|
+
out.push({ seq, role, text, lower: text.toLowerCase(), ts });
|
|
23281
|
+
};
|
|
23282
|
+
for (const { seq, event } of coalesce(entries)) {
|
|
23283
|
+
if (event.type === "user-message") {
|
|
23284
|
+
turnTs = event.ts;
|
|
23285
|
+
push(seq, "user", event.text, event.ts);
|
|
23286
|
+
} else if (event.type === "turn-start") {
|
|
23287
|
+
turnTs = event.ts;
|
|
23288
|
+
} else if (event.type === "text-delta") {
|
|
23289
|
+
push(seq, "assistant", event.text, turnTs);
|
|
23290
|
+
}
|
|
23291
|
+
}
|
|
23292
|
+
return out;
|
|
23293
|
+
}
|
|
23294
|
+
function snippet(text, at, length) {
|
|
23295
|
+
const start = Math.max(0, Math.min(at - BEFORE, text.length - SNIPPET));
|
|
23296
|
+
const end = Math.min(text.length, start + SNIPPET);
|
|
23297
|
+
const head = start > 0 ? "\u2026" : "";
|
|
23298
|
+
const tail = end < text.length ? "\u2026" : "";
|
|
23299
|
+
return {
|
|
23300
|
+
text: `${head}${text.slice(start, end)}${tail}`,
|
|
23301
|
+
match: [at - start + head.length, Math.min(length, end - at)]
|
|
23302
|
+
};
|
|
23303
|
+
}
|
|
23304
|
+
function searchRows(rows, needle, chat, limit = CHAT_HITS_PER_WORKTREE) {
|
|
23305
|
+
const hits = [];
|
|
23306
|
+
for (let i = rows.length - 1;i >= 0; i--) {
|
|
23307
|
+
const row = rows[i];
|
|
23308
|
+
const at = row.lower.indexOf(needle);
|
|
23309
|
+
if (at < 0)
|
|
23310
|
+
continue;
|
|
23311
|
+
if (hits.length === limit)
|
|
23312
|
+
return { hits, more: true };
|
|
23313
|
+
hits.push({ ...chat, seq: row.seq, role: row.role, ts: row.ts, ...snippet(row.text, at, needle.length) });
|
|
23314
|
+
}
|
|
23315
|
+
return { hits, more: false };
|
|
23316
|
+
}
|
|
23317
|
+
|
|
23318
|
+
class ChatSearch {
|
|
23319
|
+
d;
|
|
23320
|
+
said = new Map;
|
|
23321
|
+
constructor(d) {
|
|
23322
|
+
this.d = d;
|
|
23323
|
+
}
|
|
23324
|
+
async search(repoId, query) {
|
|
23325
|
+
this.d.state.requireRepo(repoId);
|
|
23326
|
+
const needle = flat(query).toLowerCase();
|
|
23327
|
+
if (needle.length < MIN_QUERY)
|
|
23328
|
+
return { hits: [], truncated: false };
|
|
23329
|
+
const chats = [
|
|
23330
|
+
...this.d.state.worktrees.filter((w) => w.repoId === repoId && w.kind === "worktree").map((w) => ({ id: w.id, archived: false, path: this.d.transcriptPath(w.id) })),
|
|
23331
|
+
...this.d.archivedChats(repoId).map((a) => ({ id: a.id, archived: true, path: a.transcript }))
|
|
23332
|
+
];
|
|
23333
|
+
const read = new Set;
|
|
23334
|
+
const found = await Promise.all(chats.map(async (c) => {
|
|
23335
|
+
const rows = await this.rowsOf(c.id, c.archived, c.path, read);
|
|
23336
|
+
return searchRows(rows, needle, { worktreeId: c.id, archived: c.archived });
|
|
23337
|
+
}));
|
|
23338
|
+
for (const key of this.said.keys())
|
|
23339
|
+
if (!read.has(key))
|
|
23340
|
+
this.said.delete(key);
|
|
23341
|
+
const all = found.filter((f) => f.hits.length > 0).sort((a, b) => b.hits[0].ts - a.hits[0].ts).flatMap((f) => f.hits);
|
|
23342
|
+
return { hits: all.slice(0, CHAT_HITS_MAX), truncated: all.length > CHAT_HITS_MAX || found.some((f) => f.more) };
|
|
23343
|
+
}
|
|
23344
|
+
async rowsOf(id, archived, path, read) {
|
|
23345
|
+
const live = archived ? undefined : this.d.live(id);
|
|
23346
|
+
if (live) {
|
|
23347
|
+
const key = `live:${id}`;
|
|
23348
|
+
read.add(key);
|
|
23349
|
+
return this.remember(key, `n:${live.length}`, () => saidRows(live));
|
|
23350
|
+
}
|
|
23351
|
+
read.add(path);
|
|
23352
|
+
const fs = this.d.fs ?? bunFs2;
|
|
23353
|
+
const at = await fs.stat(path);
|
|
23354
|
+
if (!at)
|
|
23355
|
+
return [];
|
|
23356
|
+
const sig = `${at.mtimeMs}:${at.size}`;
|
|
23357
|
+
const known = this.said.get(path);
|
|
23358
|
+
if (known?.sig === sig)
|
|
23359
|
+
return known.rows;
|
|
23360
|
+
const text = await fs.text(path);
|
|
23361
|
+
return text === null ? [] : this.remember(path, sig, () => saidRows(parseTranscript(text, id)));
|
|
23362
|
+
}
|
|
23363
|
+
remember(key, sig, rows) {
|
|
23364
|
+
const known = this.said.get(key);
|
|
23365
|
+
if (known?.sig === sig)
|
|
23366
|
+
return known.rows;
|
|
23367
|
+
const fresh = rows();
|
|
23368
|
+
this.said.set(key, { sig, rows: fresh });
|
|
23369
|
+
return fresh;
|
|
23370
|
+
}
|
|
23371
|
+
}
|
|
23372
|
+
|
|
22449
23373
|
// ../daemon/src/worktrees/landing.ts
|
|
22450
23374
|
var TAIL_CHARS = 400;
|
|
22451
23375
|
var DIFF_CHARS = 2000;
|
|
@@ -22918,11 +23842,11 @@ function rankRefs({ refs, merged, prs, query, defaultBranch, rowIdForPath }) {
|
|
|
22918
23842
|
}
|
|
22919
23843
|
|
|
22920
23844
|
// ../daemon/src/worktrees/service.ts
|
|
22921
|
-
import { existsSync as existsSync22, lstatSync,
|
|
23845
|
+
import { existsSync as existsSync22, lstatSync, readlinkSync, rmSync as rmSync5, symlinkSync, unlinkSync } from "fs";
|
|
22922
23846
|
import { basename as basename6, dirname as dirname8, join as join21 } from "path";
|
|
22923
23847
|
|
|
22924
23848
|
// ../daemon/src/git/archive.ts
|
|
22925
|
-
import { rmSync } from "fs";
|
|
23849
|
+
import { rmSync as rmSync3 } from "fs";
|
|
22926
23850
|
var archiveRef = (worktreeId) => `refs/toyon/archive/${worktreeId}`;
|
|
22927
23851
|
async function keepState(repoPath, wtPath, ref, indexFile) {
|
|
22928
23852
|
const head = await git(wtPath, "rev-parse", "--verify", "HEAD");
|
|
@@ -22959,7 +23883,7 @@ async function snapshotCommit(wtPath, head, indexFile) {
|
|
|
22959
23883
|
], wtPath);
|
|
22960
23884
|
return commit.ok ? commit.out : null;
|
|
22961
23885
|
} finally {
|
|
22962
|
-
|
|
23886
|
+
rmSync3(indexFile, { force: true });
|
|
22963
23887
|
}
|
|
22964
23888
|
}
|
|
22965
23889
|
async function checkOutKept(repoPath, wtPath, kept, branch) {
|
|
@@ -23186,42 +24110,8 @@ function compareUrl(remoteUrl, base, branch) {
|
|
|
23186
24110
|
return `https://github.com/${m[1]}/compare/${encodeURIComponent(base)}...${encodeURIComponent(branch)}?expand=1`;
|
|
23187
24111
|
}
|
|
23188
24112
|
|
|
23189
|
-
// ../daemon/src/runtime/setup.ts
|
|
23190
|
-
var SETUP_RING = 64 * 1024;
|
|
23191
|
-
var SETUP_COLS = 120;
|
|
23192
|
-
var SETUP_ROWS = 30;
|
|
23193
|
-
function runSetup(cmd, cwd, onLine, extra = {}) {
|
|
23194
|
-
return new Promise((resolve) => {
|
|
23195
|
-
const lines = new LineSplitter;
|
|
23196
|
-
try {
|
|
23197
|
-
new PtyStream({
|
|
23198
|
-
cwd,
|
|
23199
|
-
env: { ...envStrings2(process.env), TERM: "xterm-256color", COLORTERM: "truecolor", ...extra },
|
|
23200
|
-
cols: SETUP_COLS,
|
|
23201
|
-
rows: SETUP_ROWS,
|
|
23202
|
-
file: "sh",
|
|
23203
|
-
args: ["-c", cmd],
|
|
23204
|
-
ring: SETUP_RING
|
|
23205
|
-
}, (data) => {
|
|
23206
|
-
for (const line of lines.feed(data))
|
|
23207
|
-
onLine(line);
|
|
23208
|
-
}, resolve);
|
|
23209
|
-
} catch (e) {
|
|
23210
|
-
onLine(`could not run: ${e instanceof Error ? e.message : String(e)}`);
|
|
23211
|
-
resolve(1);
|
|
23212
|
-
}
|
|
23213
|
-
});
|
|
23214
|
-
}
|
|
23215
|
-
function envStrings2(base) {
|
|
23216
|
-
const env = {};
|
|
23217
|
-
for (const [k, v] of Object.entries(base))
|
|
23218
|
-
if (typeof v === "string")
|
|
23219
|
-
env[k] = v;
|
|
23220
|
-
return env;
|
|
23221
|
-
}
|
|
23222
|
-
|
|
23223
24113
|
// ../daemon/src/worktrees/archive.ts
|
|
23224
|
-
import { existsSync as existsSync21, mkdirSync as mkdirSync6, readdirSync as
|
|
24114
|
+
import { existsSync as existsSync21, mkdirSync as mkdirSync6, readdirSync as readdirSync6, readFileSync as readFileSync13, renameSync as renameSync3, rmSync as rmSync4, writeFileSync as writeFileSync7 } from "fs";
|
|
23225
24115
|
import { join as join19 } from "path";
|
|
23226
24116
|
class WorktreeArchive {
|
|
23227
24117
|
dir;
|
|
@@ -23229,7 +24119,7 @@ class WorktreeArchive {
|
|
|
23229
24119
|
constructor(dir) {
|
|
23230
24120
|
this.dir = dir;
|
|
23231
24121
|
mkdirSync6(dir, { recursive: true });
|
|
23232
|
-
for (const id of
|
|
24122
|
+
for (const id of readdirSync6(dir)) {
|
|
23233
24123
|
const file = this.filesOf(id).record;
|
|
23234
24124
|
if (!existsSync21(file))
|
|
23235
24125
|
continue;
|
|
@@ -23248,7 +24138,7 @@ class WorktreeArchive {
|
|
|
23248
24138
|
mkdirSync6(at.dir, { recursive: true });
|
|
23249
24139
|
move(from.transcript, at.transcript);
|
|
23250
24140
|
move(from.attachments, at.attachments);
|
|
23251
|
-
|
|
24141
|
+
writeFileSync7(at.record, JSON.stringify(rec, null, 2));
|
|
23252
24142
|
this.records.set(rec.worktree.id, rec);
|
|
23253
24143
|
}
|
|
23254
24144
|
take(id, to) {
|
|
@@ -23262,12 +24152,18 @@ class WorktreeArchive {
|
|
|
23262
24152
|
delete(id) {
|
|
23263
24153
|
if (!this.records.has(id))
|
|
23264
24154
|
return;
|
|
23265
|
-
|
|
24155
|
+
rmSync4(this.filesOf(id).dir, { recursive: true, force: true });
|
|
23266
24156
|
this.records.delete(id);
|
|
23267
24157
|
}
|
|
23268
24158
|
list(repo) {
|
|
23269
24159
|
return [...this.records.values()].filter((r) => r.worktree.repoId === repo.id || r.repoPath === repo.path).sort((a, b) => b.archivedAt - a.archivedAt).map((r) => summarize(r, repo.id));
|
|
23270
24160
|
}
|
|
24161
|
+
chatFiles(id) {
|
|
24162
|
+
if (!this.records.has(id))
|
|
24163
|
+
return;
|
|
24164
|
+
const at = this.filesOf(id);
|
|
24165
|
+
return { transcript: at.transcript, attachments: at.attachments };
|
|
24166
|
+
}
|
|
23271
24167
|
filesOf(id) {
|
|
23272
24168
|
const dir = join19(this.dir, id);
|
|
23273
24169
|
return {
|
|
@@ -23284,14 +24180,32 @@ function summarize(r, repoId) {
|
|
|
23284
24180
|
repoId,
|
|
23285
24181
|
title: r.worktree.title,
|
|
23286
24182
|
branch: r.worktree.branch,
|
|
24183
|
+
path: r.worktree.path,
|
|
23287
24184
|
createdAt: r.worktree.createdAt,
|
|
23288
24185
|
archivedAt: r.archivedAt,
|
|
23289
24186
|
...r.prompt ? { prompt: r.prompt } : {},
|
|
23290
24187
|
restorable: !!r.kept,
|
|
23291
24188
|
...r.kept?.snapshot ? { uncommitted: true } : {},
|
|
23292
|
-
...r.worktree.landed ? { landed: true } : {}
|
|
24189
|
+
...r.worktree.landed ? { landed: true } : {},
|
|
24190
|
+
...r.cost !== undefined ? { cost: r.cost } : {}
|
|
23293
24191
|
};
|
|
23294
24192
|
}
|
|
24193
|
+
function lastUsage(transcript) {
|
|
24194
|
+
if (!existsSync21(transcript))
|
|
24195
|
+
return null;
|
|
24196
|
+
const lines = readFileSync13(transcript, "utf8").split(`
|
|
24197
|
+
`);
|
|
24198
|
+
for (let i = lines.length - 1;i >= 0; i--) {
|
|
24199
|
+
if (!lines[i]?.includes('"usage"'))
|
|
24200
|
+
continue;
|
|
24201
|
+
try {
|
|
24202
|
+
const e = JSON.parse(lines[i]).event;
|
|
24203
|
+
if (e?.type === "usage")
|
|
24204
|
+
return { used: e.used, size: e.size, ...e.cost !== undefined ? { cost: e.cost } : {} };
|
|
24205
|
+
} catch {}
|
|
24206
|
+
}
|
|
24207
|
+
return null;
|
|
24208
|
+
}
|
|
23295
24209
|
function firstPrompt(transcript) {
|
|
23296
24210
|
if (!existsSync21(transcript))
|
|
23297
24211
|
return;
|
|
@@ -23310,8 +24224,8 @@ function firstPrompt(transcript) {
|
|
|
23310
24224
|
function move(from, to) {
|
|
23311
24225
|
if (!existsSync21(from))
|
|
23312
24226
|
return;
|
|
23313
|
-
|
|
23314
|
-
|
|
24227
|
+
rmSync4(to, { recursive: true, force: true });
|
|
24228
|
+
renameSync3(from, to);
|
|
23315
24229
|
}
|
|
23316
24230
|
|
|
23317
24231
|
// ../daemon/src/worktrees/spare.ts
|
|
@@ -23442,12 +24356,14 @@ class SparePool {
|
|
|
23442
24356
|
wt.title = slug;
|
|
23443
24357
|
wt.createdAt = Date.now();
|
|
23444
24358
|
this.d.state.save();
|
|
24359
|
+
fireAndForget(wt.id, this.d.runtime.wake(wt.id), "wake on claim");
|
|
23445
24360
|
fireAndForget(repoId, this.ensure(repoId), "spare warm-up");
|
|
23446
24361
|
return wt;
|
|
23447
24362
|
}
|
|
23448
24363
|
}
|
|
23449
24364
|
|
|
23450
24365
|
// ../daemon/src/worktrees/turns.ts
|
|
24366
|
+
var TURN_HOLD = "turn";
|
|
23451
24367
|
var RECAP_DELAY_MS = Number(process.env.TOYON_RECAP_DELAY_MS) || 2 * 60000;
|
|
23452
24368
|
|
|
23453
24369
|
class TurnService {
|
|
@@ -23485,11 +24401,14 @@ class TurnService {
|
|
|
23485
24401
|
onStatus(worktreeId, status) {
|
|
23486
24402
|
const prev = this.lastStatus.get(worktreeId) ?? "idle";
|
|
23487
24403
|
this.lastStatus.set(worktreeId, status);
|
|
23488
|
-
if (status === "working")
|
|
24404
|
+
if (status === "working") {
|
|
23489
24405
|
this.disarm(worktreeId);
|
|
24406
|
+
this.d.hold?.(worktreeId, TURN_HOLD);
|
|
24407
|
+
}
|
|
23490
24408
|
const edge = stopOf(prev, status);
|
|
23491
24409
|
if (!edge)
|
|
23492
24410
|
return;
|
|
24411
|
+
this.d.release?.(worktreeId, TURN_HOLD);
|
|
23493
24412
|
const wt = this.d.state.worktree(worktreeId);
|
|
23494
24413
|
if (!wt || wt.kind === "spare")
|
|
23495
24414
|
return;
|
|
@@ -23599,6 +24518,15 @@ function withoutAttachments(event) {
|
|
|
23599
24518
|
const { attachments: _attachments, ...rest } = event;
|
|
23600
24519
|
return rest;
|
|
23601
24520
|
}
|
|
24521
|
+
function withCarry(context, c) {
|
|
24522
|
+
if (c.count === 0)
|
|
24523
|
+
return context;
|
|
24524
|
+
const files = `${c.count} uncommitted ${c.count === 1 ? "file" : "files"}`;
|
|
24525
|
+
const line = c.moved ? `[This worktree started with ${files} the person moved here from ${c.branch} by hand. They are part of the task, not something to clean up.]` : `[${c.branch} has ${files} the person chose to leave there. This worktree does not have them.]`;
|
|
24526
|
+
return context ? `${context}
|
|
24527
|
+
|
|
24528
|
+
${line}` : line;
|
|
24529
|
+
}
|
|
23602
24530
|
var FETCH_EVERY_MS = 5 * 60000;
|
|
23603
24531
|
|
|
23604
24532
|
class WorktreeService {
|
|
@@ -23647,46 +24575,48 @@ class WorktreeService {
|
|
|
23647
24575
|
const repo = this.d.state.requireRepo(repoId);
|
|
23648
24576
|
const agent = this.d.agents.require(opts.agent ?? this.d.state.defaultAgent ?? DEFAULT_AGENT_ID).id;
|
|
23649
24577
|
const profile = this.checkProfile(repo, opts.profile);
|
|
23650
|
-
|
|
24578
|
+
const task = taskText(prompt, attachments);
|
|
24579
|
+
let slug = variant ? `${slugify2(task, false)}-v${variant.index}` : slugify2(task);
|
|
23651
24580
|
if (variant && (await git(repo.path, "show-ref", "--verify", `refs/heads/toyon/${slug}`)).ok) {
|
|
23652
24581
|
slug = `${slug}-${shortId().slice(0, 3)}`;
|
|
23653
24582
|
}
|
|
23654
24583
|
const branch = `toyon/${slug}`;
|
|
23655
|
-
|
|
23656
|
-
|
|
23657
|
-
|
|
24584
|
+
if (opts.carry && variant && variant.of > 1) {
|
|
24585
|
+
throw new UserError(`only a single worktree can take the changes on ${repo.defaultBranch}`);
|
|
24586
|
+
}
|
|
24587
|
+
const agentPrompt = variant && variant.of >= 2 ? [prompt, variantLens(variant.index)].filter(Boolean).join(`
|
|
23658
24588
|
|
|
23659
|
-
|
|
23660
|
-
|
|
23661
|
-
|
|
23662
|
-
if (
|
|
23663
|
-
|
|
23664
|
-
|
|
23665
|
-
|
|
23666
|
-
|
|
23667
|
-
|
|
23668
|
-
|
|
23669
|
-
|
|
23670
|
-
|
|
23671
|
-
|
|
23672
|
-
|
|
23673
|
-
|
|
23674
|
-
|
|
23675
|
-
|
|
23676
|
-
|
|
23677
|
-
|
|
23678
|
-
this.restartProcs(claimed, repo);
|
|
23679
|
-
}
|
|
23680
|
-
this.d.state.save();
|
|
23681
|
-
this.d.hub.emit("worktreesChanged");
|
|
23682
|
-
this.d.runtime.ensureAgent(claimed).agent.send(agentPrompt, { context, attachments });
|
|
23683
|
-
this.scheduleNaming(claimed, prompt, variant);
|
|
23684
|
-
return claimed;
|
|
24589
|
+
`) : prompt;
|
|
24590
|
+
const claimed = await this.spare.claim(repoId, branch, slug);
|
|
24591
|
+
if (claimed) {
|
|
24592
|
+
if (variant)
|
|
24593
|
+
claimed.variant = variant;
|
|
24594
|
+
if (opts.createdBy)
|
|
24595
|
+
claimed.createdBy = opts.createdBy;
|
|
24596
|
+
claimed.promptedAt = claimed.createdAt;
|
|
24597
|
+
claimed.agent = agent;
|
|
24598
|
+
if (opts.mode)
|
|
24599
|
+
claimed.mode = opts.mode;
|
|
24600
|
+
if (opts.model)
|
|
24601
|
+
claimed.model = opts.model;
|
|
24602
|
+
if (opts.effort)
|
|
24603
|
+
claimed.effort = opts.effort;
|
|
24604
|
+
this.refreshLink(claimed);
|
|
24605
|
+
if (profile !== undefined && profile !== resolveRun(repo, claimed).profile) {
|
|
24606
|
+
claimed.profile = profile;
|
|
24607
|
+
this.restartProcs(claimed, repo);
|
|
23685
24608
|
}
|
|
24609
|
+
const carried = await this.carryMain(repo, claimed, !!opts.carry);
|
|
24610
|
+
this.d.state.save();
|
|
24611
|
+
this.d.hub.emit("worktreesChanged");
|
|
24612
|
+
this.d.runtime.ensureAgent(claimed).agent.send(agentPrompt, { context: withCarry(context, carried), attachments });
|
|
24613
|
+
this.scheduleNaming(claimed, task, variant);
|
|
24614
|
+
if (carried.unmoved)
|
|
24615
|
+
throw new UserError(carried.unmoved);
|
|
24616
|
+
return claimed;
|
|
23686
24617
|
}
|
|
23687
24618
|
const wtPath = join21(this.d.paths.worktreesDir, repo.name, slug);
|
|
23688
|
-
|
|
23689
|
-
await withRepoLock(repo.path, () => gitOrThrow(repo.path, "worktree", "add", "-b", branch, wtPath, baseBranch));
|
|
24619
|
+
await withRepoLock(repo.path, () => gitOrThrow(repo.path, "worktree", "add", "-b", branch, wtPath, repo.defaultBranch));
|
|
23690
24620
|
const wt = {
|
|
23691
24621
|
id: shortId(),
|
|
23692
24622
|
repoId,
|
|
@@ -23705,11 +24635,56 @@ ${variantLens(variant.index)}` : prompt;
|
|
|
23705
24635
|
...opts.model ? { model: opts.model } : {},
|
|
23706
24636
|
...opts.effort ? { effort: opts.effort } : {}
|
|
23707
24637
|
};
|
|
23708
|
-
this.
|
|
23709
|
-
this.
|
|
23710
|
-
this.
|
|
24638
|
+
const carried = await this.carryMain(repo, wt, !!opts.carry);
|
|
24639
|
+
this.launch(wt, repo, repo.path);
|
|
24640
|
+
this.d.runtime.ensureAgent(wt).agent.send(agentPrompt, { context: withCarry(context, carried), attachments });
|
|
24641
|
+
this.scheduleNaming(wt, task, variant);
|
|
24642
|
+
if (carried.unmoved)
|
|
24643
|
+
throw new UserError(carried.unmoved);
|
|
23711
24644
|
return wt;
|
|
23712
24645
|
}
|
|
24646
|
+
async carryMain(repo, wt, move) {
|
|
24647
|
+
const main = repo.defaultBranch;
|
|
24648
|
+
if (!move)
|
|
24649
|
+
return { branch: main, moved: false, count: (await statusFiles(repo.path)).length };
|
|
24650
|
+
let count = 0;
|
|
24651
|
+
const unmoved = await withRepoLock(repo.path, async () => {
|
|
24652
|
+
count = (await statusFiles(repo.path)).length;
|
|
24653
|
+
if (count === 0)
|
|
24654
|
+
return;
|
|
24655
|
+
const before = await git(repo.path, "rev-parse", "-q", "--verify", "refs/stash");
|
|
24656
|
+
const pushed = await git(repo.path, "stash", "push", "--include-untracked", "-m", `toyon: into ${wt.title}`);
|
|
24657
|
+
if (!pushed.ok)
|
|
24658
|
+
return `the changes on ${main} could not be moved: ${pushed.err.trim()}`;
|
|
24659
|
+
const after = await git(repo.path, "rev-parse", "-q", "--verify", "refs/stash");
|
|
24660
|
+
const sha = after.out.trim();
|
|
24661
|
+
if (!after.ok || before.ok && before.out.trim() === sha)
|
|
24662
|
+
return;
|
|
24663
|
+
const entry = async () => {
|
|
24664
|
+
const list = await git(repo.path, "stash", "list", "--format=%H");
|
|
24665
|
+
const i = list.out.split(`
|
|
24666
|
+
`).indexOf(sha);
|
|
24667
|
+
return i < 0 ? null : `stash@{${i}}`;
|
|
24668
|
+
};
|
|
24669
|
+
const applied = await git(wt.path, "stash", "apply", sha);
|
|
24670
|
+
if (applied.ok) {
|
|
24671
|
+
const ref = await entry();
|
|
24672
|
+
if (ref)
|
|
24673
|
+
await git(repo.path, "stash", "drop", ref);
|
|
24674
|
+
return;
|
|
24675
|
+
}
|
|
24676
|
+
log.warn(wt.id, `moving ${main}'s changes failed`, applied.err);
|
|
24677
|
+
await git(wt.path, "reset", "--hard");
|
|
24678
|
+
await git(wt.path, "clean", "-fd");
|
|
24679
|
+
const ref = await entry();
|
|
24680
|
+
const back = ref ? await git(repo.path, "stash", "pop", ref) : null;
|
|
24681
|
+
return back?.ok ? `the changes on ${main} did not apply in ${wt.title}, so they stayed on ${main}` : `the changes on ${main} did not apply in ${wt.title}; they are kept in git stash as "toyon: into ${wt.title}"`;
|
|
24682
|
+
});
|
|
24683
|
+
const mainWt = this.d.state.worktrees.find((w) => w.repoId === repo.id && w.kind === "main");
|
|
24684
|
+
if (mainWt)
|
|
24685
|
+
this.countsCache.delete(mainWt.id);
|
|
24686
|
+
return { branch: main, moved: count > 0 && !unmoved, count, unmoved };
|
|
24687
|
+
}
|
|
23713
24688
|
launch(wt, repo, depsSource, opts = {}) {
|
|
23714
24689
|
this.d.state.addWorktree(wt);
|
|
23715
24690
|
this.d.hub.emit("worktreesChanged");
|
|
@@ -23859,6 +24834,8 @@ ${variantLens(variant.index)}` : prompt;
|
|
|
23859
24834
|
this.d.hub.emit("worktreesChanged");
|
|
23860
24835
|
}
|
|
23861
24836
|
scheduleNaming(wt, prompt, variant) {
|
|
24837
|
+
if (!prompt.trim())
|
|
24838
|
+
return;
|
|
23862
24839
|
if (!variant) {
|
|
23863
24840
|
fireAndForget(wt.id, (this.d.namer ?? makeNamer(this.d.runtime))(prompt, wt).then((name) => {
|
|
23864
24841
|
if (name)
|
|
@@ -23919,12 +24896,14 @@ ${variantLens(variant.index)}` : prompt;
|
|
|
23919
24896
|
archiveChat(wt, repo, kept, sessionId) {
|
|
23920
24897
|
const files = this.chatFiles(wt.id);
|
|
23921
24898
|
const prompt = firstPrompt(files.transcript);
|
|
24899
|
+
const cost = (this.usage.get(wt.id) ?? lastUsage(files.transcript))?.cost;
|
|
23922
24900
|
const rec = {
|
|
23923
24901
|
worktree: wt,
|
|
23924
24902
|
repoPath: repo.path,
|
|
23925
24903
|
archivedAt: Date.now(),
|
|
23926
24904
|
...sessionId ? { sessionId } : {},
|
|
23927
24905
|
...prompt ? { prompt } : {},
|
|
24906
|
+
...cost !== undefined ? { cost } : {},
|
|
23928
24907
|
...kept ? { kept } : {}
|
|
23929
24908
|
};
|
|
23930
24909
|
try {
|
|
@@ -23945,8 +24924,8 @@ ${variantLens(variant.index)}` : prompt;
|
|
|
23945
24924
|
deleteChat(worktreeId) {
|
|
23946
24925
|
const files = this.chatFiles(worktreeId);
|
|
23947
24926
|
try {
|
|
23948
|
-
|
|
23949
|
-
|
|
24927
|
+
rmSync5(files.transcript, { force: true });
|
|
24928
|
+
rmSync5(files.attachments, { recursive: true, force: true });
|
|
23950
24929
|
} catch (e) {
|
|
23951
24930
|
log.warn(worktreeId, "could not delete transcript or attachments", e);
|
|
23952
24931
|
}
|
|
@@ -23975,10 +24954,27 @@ ${variantLens(variant.index)}` : prompt;
|
|
|
23975
24954
|
const repo = this.d.state.repo(repoId);
|
|
23976
24955
|
return repo ? this.archive.list(repo) : [];
|
|
23977
24956
|
}
|
|
24957
|
+
archivedChats(repoId) {
|
|
24958
|
+
const repo = this.d.state.repo(repoId);
|
|
24959
|
+
if (!repo)
|
|
24960
|
+
return [];
|
|
24961
|
+
return this.archive.list(repo).flatMap((a) => {
|
|
24962
|
+
const files = this.archive.chatFiles(a.id);
|
|
24963
|
+
return files ? [{ id: a.id, transcript: files.transcript }] : [];
|
|
24964
|
+
});
|
|
24965
|
+
}
|
|
24966
|
+
archivedTranscript(archiveId) {
|
|
24967
|
+
const files = this.archive.chatFiles(archiveId);
|
|
24968
|
+
return files ? new Transcript(files.transcript, archiveId).entries : null;
|
|
24969
|
+
}
|
|
24970
|
+
archivedAttachment(archiveId, file) {
|
|
24971
|
+
const files = this.archive.chatFiles(archiveId);
|
|
24972
|
+
return files && isAttachmentFile(file) ? join21(files.attachments, file) : null;
|
|
24973
|
+
}
|
|
23978
24974
|
repoOf(rec) {
|
|
23979
24975
|
return this.d.state.repo(rec.worktree.repoId) ?? this.d.state.repos.find((r) => r.path === rec.repoPath);
|
|
23980
24976
|
}
|
|
23981
|
-
async restore(archiveId, createdBy) {
|
|
24977
|
+
async restore(archiveId, createdBy, message) {
|
|
23982
24978
|
const rec = this.archive.get(archiveId);
|
|
23983
24979
|
if (!rec)
|
|
23984
24980
|
throw new UserError("that archived worktree is gone");
|
|
@@ -24038,6 +25034,8 @@ ${variantLens(variant.index)}` : prompt;
|
|
|
24038
25034
|
this.countsCache.delete(wt.id);
|
|
24039
25035
|
this.refreshLink(wt);
|
|
24040
25036
|
this.launch(wt, repo, repo.path);
|
|
25037
|
+
if (message)
|
|
25038
|
+
this.d.runtime.ensureAgent(wt).agent.send(message.text, { attachments: message.attachments });
|
|
24041
25039
|
this.d.hub.emit("archiveChanged", repo.id);
|
|
24042
25040
|
return wt;
|
|
24043
25041
|
}
|
|
@@ -24163,7 +25161,7 @@ ${variantLens(variant.index)}` : prompt;
|
|
|
24163
25161
|
for (const w of sources) {
|
|
24164
25162
|
const entries = this.d.runtime.ensureAgent(w).agent.transcript();
|
|
24165
25163
|
agent.note({ type: "grafted", title: w.title, branch: w.branch, ts: Date.now() });
|
|
24166
|
-
for (const { event } of
|
|
25164
|
+
for (const { event } of coalesce(entries))
|
|
24167
25165
|
agent.note(withoutAttachments(event));
|
|
24168
25166
|
}
|
|
24169
25167
|
for (const w of sources)
|
|
@@ -24173,7 +25171,15 @@ ${variantLens(variant.index)}` : prompt;
|
|
|
24173
25171
|
this.d.hub.emit("worktreesChanged");
|
|
24174
25172
|
return { target, grafted: sources.map((w) => w.title) };
|
|
24175
25173
|
}
|
|
24176
|
-
async setupAndStart(wt, repo, depsSource = repo.path,
|
|
25174
|
+
async setupAndStart(wt, repo, depsSource = repo.path, opts = {}) {
|
|
25175
|
+
this.d.runtime.markSetup(wt.id, true);
|
|
25176
|
+
try {
|
|
25177
|
+
await this.setupThenStart(wt, repo, depsSource, opts);
|
|
25178
|
+
} finally {
|
|
25179
|
+
this.d.runtime.markSetup(wt.id, false);
|
|
25180
|
+
}
|
|
25181
|
+
}
|
|
25182
|
+
async setupThenStart(wt, repo, depsSource, { setupCommands = true }) {
|
|
24177
25183
|
const srcNm = join21(depsSource, "node_modules");
|
|
24178
25184
|
const dstNm = join21(wt.path, "node_modules");
|
|
24179
25185
|
if (existsSync22(srcNm) && !existsSync22(dstNm)) {
|
|
@@ -24430,21 +25436,7 @@ ${wt.landing.body}` : wt.landing.subject : undefined;
|
|
|
24430
25436
|
const known = this.usage.get(worktreeId);
|
|
24431
25437
|
if (known !== undefined)
|
|
24432
25438
|
return known ?? undefined;
|
|
24433
|
-
|
|
24434
|
-
const file = transcriptPathFor(this.d.paths.transcriptsDir, worktreeId);
|
|
24435
|
-
if (existsSync22(file)) {
|
|
24436
|
-
const lines = readFileSync14(file, "utf8").split(`
|
|
24437
|
-
`);
|
|
24438
|
-
for (let i = lines.length - 1;i >= 0 && !found; i--) {
|
|
24439
|
-
if (!lines[i]?.includes('"usage"'))
|
|
24440
|
-
continue;
|
|
24441
|
-
try {
|
|
24442
|
-
const e = JSON.parse(lines[i]).event;
|
|
24443
|
-
if (e?.type === "usage")
|
|
24444
|
-
found = { used: e.used, size: e.size, ...e.cost !== undefined ? { cost: e.cost } : {} };
|
|
24445
|
-
} catch {}
|
|
24446
|
-
}
|
|
24447
|
-
}
|
|
25439
|
+
const found = lastUsage(transcriptPathFor(this.d.paths.transcriptsDir, worktreeId));
|
|
24448
25440
|
this.usage.set(worktreeId, found);
|
|
24449
25441
|
return found ?? undefined;
|
|
24450
25442
|
}
|
|
@@ -24651,7 +25643,10 @@ ${wt.landing.body}` : wt.landing.subject : undefined;
|
|
|
24651
25643
|
process.on("unhandledRejection", (e) => log.error("daemon", "unhandled rejection", e));
|
|
24652
25644
|
process.on("uncaughtException", (e) => log.error("daemon", "uncaught exception", e));
|
|
24653
25645
|
var here = dirname9(fileURLToPath(import.meta.url));
|
|
24654
|
-
var { shellDist: SHELL_DIST, bridgeJs: BRIDGE_JS } = locateAssets(here);
|
|
25646
|
+
var { shellDist: SHELL_DIST, bridgeJs: BRIDGE_JS, sourceRoot: SOURCE_ROOT } = locateAssets(here);
|
|
25647
|
+
var pruned = pruneAssets(SHELL_DIST);
|
|
25648
|
+
if (pruned > 0)
|
|
25649
|
+
log.debug("daemon", `pruned ${pruned} assets from builds this one has outlived`);
|
|
24655
25650
|
var paths = makePaths();
|
|
24656
25651
|
ensureDirs(paths);
|
|
24657
25652
|
var token = loadOrCreateToken(paths);
|
|
@@ -24702,7 +25697,9 @@ var turns = new TurnService({
|
|
|
24702
25697
|
state,
|
|
24703
25698
|
hub,
|
|
24704
25699
|
transcript: (id) => runtime.agentFor(id)?.transcript() ?? [],
|
|
24705
|
-
summarize: makeRecapper(runtime, agents, state)
|
|
25700
|
+
summarize: makeRecapper(runtime, agents, state),
|
|
25701
|
+
hold: (id, tag) => runtime.hold(id, tag),
|
|
25702
|
+
release: (id, tag) => runtime.release(id, tag)
|
|
24706
25703
|
});
|
|
24707
25704
|
var files = new FileService(state, runtime, (id) => worktrees.readable(id));
|
|
24708
25705
|
var design = new DesignService((id) => worktrees.readable(id));
|
|
@@ -24717,8 +25714,24 @@ new LandingService({
|
|
|
24717
25714
|
judge: makeLander(runtime, agents, state)
|
|
24718
25715
|
});
|
|
24719
25716
|
var refs = new RefSearch({ state });
|
|
25717
|
+
var chats = new ChatSearch({
|
|
25718
|
+
state,
|
|
25719
|
+
live: (id) => runtime.agentFor(id)?.transcript(),
|
|
25720
|
+
transcriptPath: (id) => transcriptPathFor(paths.transcriptsDir, id),
|
|
25721
|
+
archivedChats: (repoId) => worktrees.archivedChats(repoId)
|
|
25722
|
+
});
|
|
24720
25723
|
var prs = new PrService({ state, hub, worktrees, view: viewPr });
|
|
24721
|
-
var
|
|
25724
|
+
var self = new SelfWatch(SOURCE_ROOT);
|
|
25725
|
+
await self.start();
|
|
25726
|
+
var afterLand = new AfterLand({ state, hub, self });
|
|
25727
|
+
var repos = new RepoRegistry({ state, hub, runtime, worktrees, afterLand, self });
|
|
25728
|
+
var idle = new IdlePolicy({
|
|
25729
|
+
runtime,
|
|
25730
|
+
state,
|
|
25731
|
+
hub,
|
|
25732
|
+
wake: (id) => repos.touch(id),
|
|
25733
|
+
warmSpare: (repoId) => repos.warm(repoId)
|
|
25734
|
+
});
|
|
24722
25735
|
var themes = new ThemeStore({ get: () => state.theme, set: (p) => state.setTheme(p) }, paths.themesDir);
|
|
24723
25736
|
themes.load();
|
|
24724
25737
|
var { branded, stop: stopServer } = startServer({
|
|
@@ -24738,13 +25751,24 @@ var { branded, stop: stopServer } = startServer({
|
|
|
24738
25751
|
design,
|
|
24739
25752
|
routes,
|
|
24740
25753
|
runtime,
|
|
25754
|
+
idle,
|
|
24741
25755
|
exec,
|
|
24742
25756
|
refs,
|
|
25757
|
+
chats,
|
|
24743
25758
|
prs,
|
|
24744
25759
|
themes,
|
|
24745
25760
|
agents,
|
|
24746
25761
|
accounts,
|
|
24747
25762
|
attachments: attachments2,
|
|
25763
|
+
self,
|
|
25764
|
+
afterLand,
|
|
25765
|
+
restart: () => {
|
|
25766
|
+
const can = restartable();
|
|
25767
|
+
if (!can.ok)
|
|
25768
|
+
return can.reason;
|
|
25769
|
+
fireAndForget("daemon", shutdown("restart", { respawn: true }), "restart");
|
|
25770
|
+
return null;
|
|
25771
|
+
},
|
|
24748
25772
|
planTasks: makePlanner(agents),
|
|
24749
25773
|
folderDialog: folderDialog()
|
|
24750
25774
|
}
|
|
@@ -24760,10 +25784,11 @@ bridge.setShellOrigins([
|
|
|
24760
25784
|
],
|
|
24761
25785
|
...remote ? [`https://${remote.host}`] : []
|
|
24762
25786
|
]);
|
|
24763
|
-
|
|
25787
|
+
writeFileSync8(paths.pidFile, `${process.pid}
|
|
24764
25788
|
`);
|
|
24765
25789
|
var stopLagSampler = startLagSampler();
|
|
24766
25790
|
await repos.boot();
|
|
25791
|
+
idle.boot();
|
|
24767
25792
|
fireAndForget("agents", agents.installMissing().then(() => probe.missing()), "agent adapter install");
|
|
24768
25793
|
var repoArg = process.argv[2];
|
|
24769
25794
|
if (repoArg) {
|
|
@@ -24788,11 +25813,12 @@ if (remote?.front === "edge") {
|
|
|
24788
25813
|
}
|
|
24789
25814
|
}
|
|
24790
25815
|
var shuttingDown = false;
|
|
24791
|
-
async function shutdown(signal) {
|
|
25816
|
+
async function shutdown(signal, opts = {}) {
|
|
24792
25817
|
if (shuttingDown)
|
|
24793
25818
|
return;
|
|
24794
25819
|
shuttingDown = true;
|
|
24795
25820
|
log.info("daemon", `${signal}: stopping dev servers`);
|
|
25821
|
+
idle.shutdown();
|
|
24796
25822
|
repos.stopWatchers();
|
|
24797
25823
|
prs.stop();
|
|
24798
25824
|
stopLagSampler();
|
|
@@ -24800,7 +25826,12 @@ async function shutdown(signal) {
|
|
|
24800
25826
|
routes.flush();
|
|
24801
25827
|
const deadline = new Promise((resolve) => setTimeout(resolve, 5000));
|
|
24802
25828
|
await Promise.race([runtime.shutdown(), deadline]);
|
|
24803
|
-
|
|
25829
|
+
rmSync6(paths.pidFile, { force: true });
|
|
25830
|
+
if (opts.respawn) {
|
|
25831
|
+
await Bun.sleep(250);
|
|
25832
|
+
log.info("daemon", "starting the replacement");
|
|
25833
|
+
respawn(paths.logFile);
|
|
25834
|
+
}
|
|
24804
25835
|
process.exit(0);
|
|
24805
25836
|
}
|
|
24806
25837
|
process.on("SIGINT", () => fireAndForget("daemon", shutdown("SIGINT"), "shutdown"));
|