toyon 0.1.0 → 0.3.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 +62 -39
- package/dist/bridge.js +1 -1
- package/dist/cli.js +255 -68
- package/dist/daemon.js +2404 -516
- package/dist/shell/assets/{Editor-Cn-KsoFr.js → Editor-Lle8R8mY.js} +148 -148
- package/dist/shell/assets/{XTerm-DSSGYyFZ.js → XTerm-DcWpV1FL.js} +13 -13
- package/dist/shell/assets/{cssMode-BU_otbjh.js → cssMode-Ck0mAapo.js} +1 -1
- package/dist/shell/assets/{freemarker2-C-NGmKSC.js → freemarker2-B5OkSZOW.js} +1 -1
- package/dist/shell/assets/{handlebars-BWvVdY_e.js → handlebars-C6vrFwYj.js} +1 -1
- package/dist/shell/assets/{html-CBO-8nU2.js → html-CE92cnIr.js} +1 -1
- package/dist/shell/assets/{htmlMode-Dwz6npQ-.js → htmlMode-CfM52A9Q.js} +1 -1
- package/dist/shell/assets/index-DOf1M9v5.js +233 -0
- package/dist/shell/assets/index-b4LMTPcs.css +1 -0
- package/dist/shell/assets/{javascript-__evPE7m.js → javascript-C7Q1bCJn.js} +1 -1
- package/dist/shell/assets/{jsonMode-DpbQTbP6.js → jsonMode-Cq43R9DW.js} +1 -1
- package/dist/shell/assets/{liquid-CgOzfjEh.js → liquid-CnxQAGJm.js} +1 -1
- package/dist/shell/assets/{lspLanguageFeatures-DM57zeAM.js → lspLanguageFeatures-BtjVqb14.js} +1 -1
- package/dist/shell/assets/{mdx-DxN7Bq5u.js → mdx-ycMLgnto.js} +1 -1
- package/dist/shell/assets/{python-C3rGYQJF.js → python-se_0lOPN.js} +1 -1
- package/dist/shell/assets/{razor-C5rkTP_6.js → razor-DjByizBq.js} +1 -1
- package/dist/shell/assets/{tsMode-DDCDv-RH.js → tsMode-D1mOZbLC.js} +1 -1
- package/dist/shell/assets/{typescript-CyHgeKIZ.js → typescript-B-dZdvXG.js} +1 -1
- package/dist/shell/assets/{workers-C7XbwGBM.js → workers-DXwGNRtC.js} +1 -1
- package/dist/shell/assets/{xml-Dmw92JAf.js → xml-CoqNFco9.js} +1 -1
- package/dist/shell/assets/{yaml-Bj_aHuSs.js → yaml-D5Qru8sI.js} +1 -1
- package/dist/shell/index.html +2 -2
- package/dist/shell/manifest.json +1 -1
- package/package.json +2 -2
- 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 writeFileSync9 } from "fs";
|
|
4
4
|
import { dirname as dirname9 } from "path";
|
|
5
5
|
import { fileURLToPath } from "url";
|
|
6
6
|
|
|
@@ -13,6 +13,111 @@ function isWriteTool(ev) {
|
|
|
13
13
|
return ev.kind ? WRITE_KINDS.has(ev.kind) : WRITE_TOOLS.has(ev.name);
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
+
// ../shared/src/model.ts
|
|
17
|
+
var DEFAULT_PERMISSION_MODE = "auto";
|
|
18
|
+
function hasOwnBranch(wt) {
|
|
19
|
+
return wt.branch.startsWith("toyon/");
|
|
20
|
+
}
|
|
21
|
+
function isUnseen(wt) {
|
|
22
|
+
return wt.unread === true || wt.lastTurn != null && (wt.seenAt == null || wt.seenAt < wt.lastTurn.at);
|
|
23
|
+
}
|
|
24
|
+
var SHELL_STREAM = "shell";
|
|
25
|
+
var LOGIN_STREAM = "login";
|
|
26
|
+
function streamKey(worktreeId, stream) {
|
|
27
|
+
return `${worktreeId}/${stream}`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// ../shared/src/worktree-caps.ts
|
|
31
|
+
function isMain(wt) {
|
|
32
|
+
return wt.kind === "main";
|
|
33
|
+
}
|
|
34
|
+
function canArchive(wt) {
|
|
35
|
+
return wt.kind === "worktree";
|
|
36
|
+
}
|
|
37
|
+
function canRename(wt) {
|
|
38
|
+
return wt.kind === "worktree" && hasOwnBranch(wt);
|
|
39
|
+
}
|
|
40
|
+
function canLand(wt) {
|
|
41
|
+
return wt.kind === "worktree" && wt.from?.kind !== "pr";
|
|
42
|
+
}
|
|
43
|
+
function canGraft(wt) {
|
|
44
|
+
return wt.kind === "worktree";
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// ../shared/src/railOrder.ts
|
|
48
|
+
var sentAt = (w) => w.promptedAt ?? w.lastTurn?.at ?? w.createdAt;
|
|
49
|
+
var tierOf = (w) => isMain(w) ? 0 : w.landed ? 2 : 1;
|
|
50
|
+
var railUnitOf = (w) => w.variant ? `${tierOf(w)}:${w.variant.group}` : `row:${w.id}`;
|
|
51
|
+
function railOrder(rows) {
|
|
52
|
+
const units = [];
|
|
53
|
+
const groups = new Map;
|
|
54
|
+
for (const row of rows) {
|
|
55
|
+
const w = row.worktree;
|
|
56
|
+
const key = railUnitOf(w);
|
|
57
|
+
const unit = groups.get(key);
|
|
58
|
+
if (unit) {
|
|
59
|
+
unit.rows.push(row);
|
|
60
|
+
unit.at = Math.max(unit.at, sentAt(w));
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
const fresh = { tier: tierOf(w), at: sentAt(w), rows: [row] };
|
|
64
|
+
units.push(fresh);
|
|
65
|
+
groups.set(key, fresh);
|
|
66
|
+
}
|
|
67
|
+
units.sort((a, b) => a.tier - b.tier || b.at - a.at);
|
|
68
|
+
return units.flatMap((u) => u.rows.length > 1 ? u.rows.sort((a, b) => (a.worktree.variant?.index ?? 0) - (b.worktree.variant?.index ?? 0)) : u.rows);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ../shared/src/archiveRule.ts
|
|
72
|
+
var ARCHIVE_KEEP_RECENT = 5;
|
|
73
|
+
function archiveReason(wt, f) {
|
|
74
|
+
const group = wt.variant ? f.rows.filter((w) => w.variant?.group === wt.variant?.group) : [wt];
|
|
75
|
+
const decided = group.some((w) => !!w.lands?.length);
|
|
76
|
+
const lost = decided && !wt.lands?.length;
|
|
77
|
+
if (decided) {
|
|
78
|
+
if (!nothingToKeep(wt, f, lost))
|
|
79
|
+
return null;
|
|
80
|
+
} else if (!group.every((w) => nothingToKeep(w, f, false)))
|
|
81
|
+
return null;
|
|
82
|
+
if (unitsAhead(wt, f.rows) < ARCHIVE_KEEP_RECENT)
|
|
83
|
+
return null;
|
|
84
|
+
const what = lost ? "a sibling landed" : wt.landed ? "landed" : "no changes";
|
|
85
|
+
return `${what}, not opened in ${duration(f.afterMs)}`;
|
|
86
|
+
}
|
|
87
|
+
function nothingToKeep(w, f, lost) {
|
|
88
|
+
if (w.kind !== "worktree" || !hasOwnBranch(w))
|
|
89
|
+
return false;
|
|
90
|
+
if (w.lastTurn?.end !== "done" || isUnseen(w))
|
|
91
|
+
return false;
|
|
92
|
+
if (w.planned || w.pr?.state === "open")
|
|
93
|
+
return false;
|
|
94
|
+
if (f.viewed(w.id) || f.pending(w.id) || f.drafted(w.id))
|
|
95
|
+
return false;
|
|
96
|
+
if (f.now - (w.viewedAt ?? w.createdAt) < f.afterMs)
|
|
97
|
+
return false;
|
|
98
|
+
const c = f.counts(w.id);
|
|
99
|
+
return c.dirty === 0 && (lost ? c.ahead !== undefined : c.ahead === 0);
|
|
100
|
+
}
|
|
101
|
+
function unitsAhead(wt, rows) {
|
|
102
|
+
const tasks = railOrder(rows.filter((w) => w.kind === "worktree").map((worktree) => ({ worktree })));
|
|
103
|
+
const mine = railUnitOf(wt);
|
|
104
|
+
const seen = new Set;
|
|
105
|
+
for (const { worktree: w } of tasks) {
|
|
106
|
+
const unit = railUnitOf(w);
|
|
107
|
+
if (unit === mine)
|
|
108
|
+
return seen.size;
|
|
109
|
+
seen.add(unit);
|
|
110
|
+
}
|
|
111
|
+
return seen.size;
|
|
112
|
+
}
|
|
113
|
+
function duration(ms) {
|
|
114
|
+
if (ms >= 60 * 60000)
|
|
115
|
+
return `${Math.round(ms / (60 * 60000))}h`;
|
|
116
|
+
if (ms >= 60000)
|
|
117
|
+
return `${Math.round(ms / 60000)} min`;
|
|
118
|
+
return `${Math.round(ms / 1000)}s`;
|
|
119
|
+
}
|
|
120
|
+
|
|
16
121
|
// ../shared/src/attachment.ts
|
|
17
122
|
var ATTACHMENT_KINDS = ["image", "paste", "pick"];
|
|
18
123
|
var ATTACHMENT_LIMITS = { image: 6, paste: 4, pick: 8 };
|
|
@@ -40,12 +145,17 @@ var CONFIG_DIR = ".toyon";
|
|
|
40
145
|
function isLocalConfigFile(rel) {
|
|
41
146
|
return rel === CONFIG_FILES.folder.local || rel === CONFIG_FILES.root.local;
|
|
42
147
|
}
|
|
148
|
+
function configSibling(rel, kind) {
|
|
149
|
+
const place = Object.values(CONFIG_FILES).find((p) => p.shared === rel || p.local === rel) ?? CONFIG_FILES.folder;
|
|
150
|
+
return place[kind];
|
|
151
|
+
}
|
|
43
152
|
|
|
44
153
|
// ../shared/src/daemon.ts
|
|
45
154
|
var DAEMON_FILES = {
|
|
46
155
|
token: "token",
|
|
47
156
|
pid: "daemon.pid",
|
|
48
157
|
state: "state.json",
|
|
158
|
+
drafts: "drafts.json",
|
|
49
159
|
log: "daemon.log",
|
|
50
160
|
remote: "remote.json"
|
|
51
161
|
};
|
|
@@ -159,6 +269,11 @@ function middle(a, b) {
|
|
|
159
269
|
return ops;
|
|
160
270
|
}
|
|
161
271
|
|
|
272
|
+
// ../shared/src/drafts.ts
|
|
273
|
+
var DRAFT_PREFIX = "draft:";
|
|
274
|
+
var draftKey = (repoId) => DRAFT_PREFIX + repoId;
|
|
275
|
+
var draftRepoOf = (boxId) => boxId.startsWith(DRAFT_PREFIX) ? boxId.slice(DRAFT_PREFIX.length) : null;
|
|
276
|
+
|
|
162
277
|
// ../shared/src/land.ts
|
|
163
278
|
var DEFAULT_LAND_ROUTE = "merge";
|
|
164
279
|
var DEFAULT_MERGE_METHOD = "merge";
|
|
@@ -171,19 +286,8 @@ function landPolicy(config) {
|
|
|
171
286
|
};
|
|
172
287
|
}
|
|
173
288
|
|
|
174
|
-
// ../shared/src/model.ts
|
|
175
|
-
var DEFAULT_PERMISSION_MODE = "auto";
|
|
176
|
-
function hasOwnBranch(wt) {
|
|
177
|
-
return wt.branch.startsWith("toyon/");
|
|
178
|
-
}
|
|
179
|
-
var SHELL_STREAM = "shell";
|
|
180
|
-
var LOGIN_STREAM = "login";
|
|
181
|
-
function streamKey(worktreeId, stream) {
|
|
182
|
-
return `${worktreeId}/${stream}`;
|
|
183
|
-
}
|
|
184
|
-
|
|
185
289
|
// ../shared/src/protocol/limits.ts
|
|
186
|
-
var PROTOCOL_VERSION =
|
|
290
|
+
var PROTOCOL_VERSION = 56;
|
|
187
291
|
var FILE_MAX_CHARS = 5000000;
|
|
188
292
|
var IMAGE_MIME_TYPES = ["image/png", "image/jpeg", "image/gif", "image/webp"];
|
|
189
293
|
var IMAGE_MAX_EDGE = 2576;
|
|
@@ -1126,7 +1230,7 @@ var nanoid = /^[a-zA-Z0-9_-]{21}$/;
|
|
|
1126
1230
|
function nanoidOfLength(length) {
|
|
1127
1231
|
return new RegExp(`^[a-zA-Z0-9_-]{${length}}$`);
|
|
1128
1232
|
}
|
|
1129
|
-
var
|
|
1233
|
+
var duration2 = /^P(?:(\d+W)|(?!.*W)(?=\d|T\d)(\d+Y)?(\d+M)?(\d+D)?(T(?=\d)(\d+H)?(\d+M)?(\d+([.,]\d+)?S)?)?)$/;
|
|
1130
1234
|
var guid = /^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})$/;
|
|
1131
1235
|
var uuid = (version) => {
|
|
1132
1236
|
if (!version)
|
|
@@ -1919,7 +2023,7 @@ var $ZodISOTime = /* @__PURE__ */ $constructor("$ZodISOTime", (inst, def) => {
|
|
|
1919
2023
|
$ZodStringFormat.init(inst, def);
|
|
1920
2024
|
});
|
|
1921
2025
|
var $ZodISODuration = /* @__PURE__ */ $constructor("$ZodISODuration", (inst, def) => {
|
|
1922
|
-
def.pattern ?? (def.pattern =
|
|
2026
|
+
def.pattern ?? (def.pattern = duration2);
|
|
1923
2027
|
$ZodStringFormat.init(inst, def);
|
|
1924
2028
|
});
|
|
1925
2029
|
var $ZodIPv4 = /* @__PURE__ */ $constructor("$ZodIPv4", (inst, def) => {
|
|
@@ -6221,6 +6325,7 @@ var toyonConfigSchema = object({
|
|
|
6221
6325
|
setup: array(shellCommand).max(50).optional(),
|
|
6222
6326
|
run: record(declaredProcName, shellCommand),
|
|
6223
6327
|
check: shellCommand.optional(),
|
|
6328
|
+
afterLand: array(shellCommand).max(50).optional(),
|
|
6224
6329
|
land: landConfigSchema.optional(),
|
|
6225
6330
|
preview: procName.optional(),
|
|
6226
6331
|
profiles: record(procName, runProfileSchema).optional(),
|
|
@@ -6255,9 +6360,11 @@ var variantSchema = object({ group: string2(), index: number2().int().min(1), of
|
|
|
6255
6360
|
var clientMsgSchema = discriminatedUnion("t", [
|
|
6256
6361
|
object({ t: literal("subscribe"), worktreeId: id }),
|
|
6257
6362
|
object({ t: literal("unsubscribe"), worktreeId: id }),
|
|
6363
|
+
object({ t: literal("view"), worktreeId: id.nullable() }),
|
|
6258
6364
|
object({
|
|
6259
6365
|
t: literal("chat"),
|
|
6260
6366
|
worktreeId: id,
|
|
6367
|
+
clientId: string2().max(64).optional(),
|
|
6261
6368
|
text: prose,
|
|
6262
6369
|
context: prose.optional(),
|
|
6263
6370
|
attachments
|
|
@@ -6267,7 +6374,6 @@ var clientMsgSchema = discriminatedUnion("t", [
|
|
|
6267
6374
|
clientId: string2().max(64).optional(),
|
|
6268
6375
|
repoId: id,
|
|
6269
6376
|
prompt,
|
|
6270
|
-
baseWorktreeId: id.optional(),
|
|
6271
6377
|
variant: variantSchema.optional(),
|
|
6272
6378
|
context: prose.optional(),
|
|
6273
6379
|
attachments,
|
|
@@ -6275,7 +6381,8 @@ var clientMsgSchema = discriminatedUnion("t", [
|
|
|
6275
6381
|
profile: string2().max(100).optional(),
|
|
6276
6382
|
mode: permissionModeSchema.optional(),
|
|
6277
6383
|
model: string2().max(200).optional(),
|
|
6278
|
-
effort: string2().max(100).optional()
|
|
6384
|
+
effort: string2().max(100).optional(),
|
|
6385
|
+
carry: boolean2().optional()
|
|
6279
6386
|
}),
|
|
6280
6387
|
object({
|
|
6281
6388
|
t: literal("batch-worktrees"),
|
|
@@ -6289,12 +6396,13 @@ var clientMsgSchema = discriminatedUnion("t", [
|
|
|
6289
6396
|
object({ t: literal("set-worktree-model"), worktreeId: id, model: string2().max(200) }),
|
|
6290
6397
|
object({ t: literal("set-worktree-effort"), worktreeId: id, effort: string2().max(100) }),
|
|
6291
6398
|
object({ t: literal("set-worktree-profile"), worktreeId: id, profile: string2().max(100) }),
|
|
6292
|
-
object({ t: literal("
|
|
6399
|
+
object({ t: literal("archive-worktree"), worktreeId: id }),
|
|
6293
6400
|
object({ t: literal("list-archived"), repoId: id }),
|
|
6294
6401
|
object({
|
|
6295
6402
|
t: literal("restore-worktree"),
|
|
6296
6403
|
archiveId: id,
|
|
6297
|
-
clientId: string2().max(64).optional()
|
|
6404
|
+
clientId: string2().max(64).optional(),
|
|
6405
|
+
message: object({ text: prose, attachments }).optional()
|
|
6298
6406
|
}),
|
|
6299
6407
|
object({ t: literal("delete-archived"), archiveId: id }),
|
|
6300
6408
|
object({
|
|
@@ -6310,6 +6418,9 @@ var clientMsgSchema = discriminatedUnion("t", [
|
|
|
6310
6418
|
object({ t: literal("commit"), worktreeId: id, message: string2().max(5000) }),
|
|
6311
6419
|
object({ t: literal("graft"), targetId: id, sourceIds: array(id).min(1).max(20) }),
|
|
6312
6420
|
object({ t: literal("sync-main"), worktreeId: id }),
|
|
6421
|
+
object({ t: literal("run-after-land"), repoId: id }),
|
|
6422
|
+
object({ t: literal("restart-daemon") }),
|
|
6423
|
+
object({ t: literal("update-now") }),
|
|
6313
6424
|
object({ t: literal("pull-main"), worktreeId: id }),
|
|
6314
6425
|
object({
|
|
6315
6426
|
t: literal("write-file"),
|
|
@@ -6329,6 +6440,7 @@ var clientMsgSchema = discriminatedUnion("t", [
|
|
|
6329
6440
|
object({ t: literal("stop-agent"), worktreeId: id }),
|
|
6330
6441
|
object({ t: literal("seen"), worktreeId: id }),
|
|
6331
6442
|
object({ t: literal("mark-unread"), worktreeId: id }),
|
|
6443
|
+
object({ t: literal("set-draft"), boxId: id, text: prose, clientId: string2().max(64).optional() }),
|
|
6332
6444
|
object({ t: literal("refresh-git"), repoId: id }),
|
|
6333
6445
|
object({ t: literal("visit"), worktreeId: id, path: routePath, title: pageTitle.optional() }),
|
|
6334
6446
|
object({ t: literal("page-title"), worktreeId: id, path: routePath, title: pageTitle }),
|
|
@@ -6337,7 +6449,12 @@ var clientMsgSchema = discriminatedUnion("t", [
|
|
|
6337
6449
|
object({ t: literal("unqueue"), worktreeId: id, index: number2().int().min(0) }),
|
|
6338
6450
|
object({ t: literal("changed-ranges"), worktreeId: id, path: relPath }),
|
|
6339
6451
|
object({ t: literal("rename-worktree"), worktreeId: id, title: string2().min(1).max(200) }),
|
|
6340
|
-
object({
|
|
6452
|
+
object({
|
|
6453
|
+
t: literal("confirm-config"),
|
|
6454
|
+
repoId: id,
|
|
6455
|
+
config: toyonConfigSchema,
|
|
6456
|
+
kind: _enum(["shared", "local"])
|
|
6457
|
+
}),
|
|
6341
6458
|
object({ t: literal("register-repo"), path: string2().min(1).max(4000) }),
|
|
6342
6459
|
object({
|
|
6343
6460
|
t: literal("create-repo"),
|
|
@@ -6393,6 +6510,7 @@ var clientMsgSchema = discriminatedUnion("t", [
|
|
|
6393
6510
|
object({ t: literal("exec"), worktreeId: id, command: shellCommand.min(1) }),
|
|
6394
6511
|
object({ t: literal("exec-stop"), worktreeId: id }),
|
|
6395
6512
|
object({ t: literal("search-refs"), repoId: id, query: string2().max(200) }),
|
|
6513
|
+
object({ t: literal("search-chats"), repoId: id, query: string2().max(200) }),
|
|
6396
6514
|
object({
|
|
6397
6515
|
t: literal("open-ref"),
|
|
6398
6516
|
clientId: string2().max(64).optional(),
|
|
@@ -7362,6 +7480,52 @@ function isDark(hex) {
|
|
|
7362
7480
|
return luminance(hex) < 0.4;
|
|
7363
7481
|
}
|
|
7364
7482
|
|
|
7483
|
+
// ../shared/src/update.ts
|
|
7484
|
+
function installMethod(packageJson) {
|
|
7485
|
+
if (!packageJson)
|
|
7486
|
+
return "none";
|
|
7487
|
+
const p = packageJson.replaceAll("\\", "/");
|
|
7488
|
+
if (p.includes("/_npx/"))
|
|
7489
|
+
return "npx";
|
|
7490
|
+
if (p.includes("/.bun/install/global/"))
|
|
7491
|
+
return "bun";
|
|
7492
|
+
if (p.includes("/node_modules/toyon/"))
|
|
7493
|
+
return "npm";
|
|
7494
|
+
return "none";
|
|
7495
|
+
}
|
|
7496
|
+
function installCommand(method, version) {
|
|
7497
|
+
if (method === "npm")
|
|
7498
|
+
return ["npm", "install", "-g", `toyon@${version}`];
|
|
7499
|
+
if (method === "bun")
|
|
7500
|
+
return ["bun", "add", "-g", `toyon@${version}`];
|
|
7501
|
+
return null;
|
|
7502
|
+
}
|
|
7503
|
+
function newer(a, b) {
|
|
7504
|
+
const pa = parseVersion(a);
|
|
7505
|
+
const pb = parseVersion(b);
|
|
7506
|
+
if (!pa || !pb)
|
|
7507
|
+
return false;
|
|
7508
|
+
for (let i = 0;i < 3; i++) {
|
|
7509
|
+
const x = pa.nums[i] ?? 0;
|
|
7510
|
+
const y = pb.nums[i] ?? 0;
|
|
7511
|
+
if (x !== y)
|
|
7512
|
+
return x > y;
|
|
7513
|
+
}
|
|
7514
|
+
if (pa.pre === pb.pre)
|
|
7515
|
+
return false;
|
|
7516
|
+
if (pa.pre === null)
|
|
7517
|
+
return true;
|
|
7518
|
+
if (pb.pre === null)
|
|
7519
|
+
return false;
|
|
7520
|
+
return pa.pre > pb.pre;
|
|
7521
|
+
}
|
|
7522
|
+
function parseVersion(v) {
|
|
7523
|
+
const m = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?$/.exec(v.trim());
|
|
7524
|
+
if (!m)
|
|
7525
|
+
return null;
|
|
7526
|
+
return { nums: [Number(m[1]), Number(m[2]), Number(m[3])], pre: m[4] ?? null };
|
|
7527
|
+
}
|
|
7528
|
+
|
|
7365
7529
|
// ../shared/src/vscode-theme.ts
|
|
7366
7530
|
var workbenchKeys = {
|
|
7367
7531
|
surface0: ["editor.background"],
|
|
@@ -7538,27 +7702,10 @@ function pickSyntax(tokenColors) {
|
|
|
7538
7702
|
function slug(s) {
|
|
7539
7703
|
return s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "theme";
|
|
7540
7704
|
}
|
|
7541
|
-
|
|
7542
|
-
// ../shared/src/worktree-caps.ts
|
|
7543
|
-
function isMain(wt) {
|
|
7544
|
-
return wt.kind === "main";
|
|
7545
|
-
}
|
|
7546
|
-
function canRemove(wt) {
|
|
7547
|
-
return wt.kind === "worktree";
|
|
7548
|
-
}
|
|
7549
|
-
function canRename(wt) {
|
|
7550
|
-
return wt.kind === "worktree" && hasOwnBranch(wt);
|
|
7551
|
-
}
|
|
7552
|
-
function canLand(wt) {
|
|
7553
|
-
return wt.kind === "worktree" && wt.from?.kind !== "pr";
|
|
7554
|
-
}
|
|
7555
|
-
function canGraft(wt) {
|
|
7556
|
-
return wt.kind === "worktree";
|
|
7557
|
-
}
|
|
7558
7705
|
// ../daemon/package.json
|
|
7559
7706
|
var package_default = {
|
|
7560
7707
|
name: "@toyon/daemon",
|
|
7561
|
-
version: "0.
|
|
7708
|
+
version: "0.3.0",
|
|
7562
7709
|
private: true,
|
|
7563
7710
|
type: "module",
|
|
7564
7711
|
main: "src/index.ts",
|
|
@@ -11885,6 +12032,7 @@ var EXT = {
|
|
|
11885
12032
|
};
|
|
11886
12033
|
var ID = /^[A-Za-z0-9_-]{1,64}$/;
|
|
11887
12034
|
var FILE = /^\d{1,6}\.(png|jpg|gif|webp|txt)$/;
|
|
12035
|
+
var isAttachmentFile = (file) => FILE.test(file);
|
|
11888
12036
|
function attachmentsDirFor(attachmentsDir, worktreeId) {
|
|
11889
12037
|
return join(attachmentsDir, worktreeId);
|
|
11890
12038
|
}
|
|
@@ -12478,8 +12626,10 @@ function makePaths(home = process.env.TOYON_HOME ?? join3(homedir(), ".toyon"))
|
|
|
12478
12626
|
return {
|
|
12479
12627
|
home,
|
|
12480
12628
|
stateFile: join3(home, DAEMON_FILES.state),
|
|
12629
|
+
draftsFile: join3(home, DAEMON_FILES.drafts),
|
|
12481
12630
|
tokenFile: join3(home, DAEMON_FILES.token),
|
|
12482
12631
|
pidFile: join3(home, DAEMON_FILES.pid),
|
|
12632
|
+
logFile: join3(home, DAEMON_FILES.log),
|
|
12483
12633
|
remoteFile: join3(home, DAEMON_FILES.remote),
|
|
12484
12634
|
transcriptsDir: join3(home, "transcripts"),
|
|
12485
12635
|
attachmentsDir: join3(home, "attachments"),
|
|
@@ -12506,11 +12656,17 @@ function ensureDirs(p) {
|
|
|
12506
12656
|
// ../daemon/src/git/exclude.ts
|
|
12507
12657
|
import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync } from "fs";
|
|
12508
12658
|
import { dirname } from "path";
|
|
12509
|
-
async function
|
|
12659
|
+
async function excludeFile(cwd) {
|
|
12510
12660
|
const r = await git(cwd, "rev-parse", "--path-format=absolute", "--git-path", "info/exclude");
|
|
12511
|
-
if (
|
|
12512
|
-
return
|
|
12513
|
-
|
|
12661
|
+
if (r.ok && r.out)
|
|
12662
|
+
return r.out;
|
|
12663
|
+
log.warn("git", `cannot locate info/exclude for ${cwd}: ${r.err}`);
|
|
12664
|
+
return null;
|
|
12665
|
+
}
|
|
12666
|
+
async function excludeFromGit(cwd, pattern) {
|
|
12667
|
+
const file = await excludeFile(cwd);
|
|
12668
|
+
if (!file)
|
|
12669
|
+
return;
|
|
12514
12670
|
const current = existsSync3(file) ? readFileSync3(file, "utf8") : "";
|
|
12515
12671
|
if (current.split(`
|
|
12516
12672
|
`).some((l) => l.trim() === pattern))
|
|
@@ -12521,6 +12677,18 @@ async function excludeFromGit(cwd, pattern) {
|
|
|
12521
12677
|
` : ""}${pattern}
|
|
12522
12678
|
`);
|
|
12523
12679
|
}
|
|
12680
|
+
async function unexcludeFromGit(cwd, pattern) {
|
|
12681
|
+
const file = await excludeFile(cwd);
|
|
12682
|
+
if (!file || !existsSync3(file))
|
|
12683
|
+
return;
|
|
12684
|
+
const lines = readFileSync3(file, "utf8").split(`
|
|
12685
|
+
`);
|
|
12686
|
+
const kept = lines.filter((l) => l.trim() !== pattern);
|
|
12687
|
+
if (kept.length === lines.length)
|
|
12688
|
+
return;
|
|
12689
|
+
writeFileSync(file, kept.join(`
|
|
12690
|
+
`));
|
|
12691
|
+
}
|
|
12524
12692
|
|
|
12525
12693
|
// ../daemon/src/agent/bounds.ts
|
|
12526
12694
|
import { existsSync as existsSync4, realpathSync } from "fs";
|
|
@@ -13013,7 +13181,7 @@ var SYSTEM_APPEND = [
|
|
|
13013
13181
|
"Never run `git commit` unless the user explicitly asks you to; leave changes uncommitted for the user to review and commit themselves.",
|
|
13014
13182
|
"Keep the scope tight: do the asked task well, then stop. Suggest follow-ups in chat instead of expanding scope.",
|
|
13015
13183
|
"`@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
|
|
13184
|
+
`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
13185
|
"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
13186
|
"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
13187
|
].join(" ");
|
|
@@ -13050,10 +13218,11 @@ function buildPrompt(text, context, prefix, attachments = []) {
|
|
|
13050
13218
|
}
|
|
13051
13219
|
for (const a of attachments)
|
|
13052
13220
|
blocks.push(...attachmentBlocks(a));
|
|
13053
|
-
|
|
13054
|
-
blocks.push(textBlock(prefix ? `${prefix}
|
|
13221
|
+
const body = [prefix, text].filter(Boolean).join(`
|
|
13055
13222
|
|
|
13056
|
-
|
|
13223
|
+
`);
|
|
13224
|
+
if (!leads && body)
|
|
13225
|
+
blocks.push(textBlock(body));
|
|
13057
13226
|
if (context && !leads)
|
|
13058
13227
|
blocks.push(textBlock(context));
|
|
13059
13228
|
return blocks;
|
|
@@ -13077,26 +13246,41 @@ ${a.text}
|
|
|
13077
13246
|
}
|
|
13078
13247
|
|
|
13079
13248
|
// ../daemon/src/agent/transcript.ts
|
|
13080
|
-
import { existsSync as existsSync7, readFileSync as readFileSync6
|
|
13081
|
-
import { appendFile
|
|
13249
|
+
import { existsSync as existsSync7, readFileSync as readFileSync6 } from "fs";
|
|
13250
|
+
import { appendFile } from "fs/promises";
|
|
13082
13251
|
import { join as join6 } from "path";
|
|
13083
|
-
var MAX_ENTRIES = 4000;
|
|
13084
|
-
var KEEP_ENTRIES = 2000;
|
|
13085
13252
|
function transcriptPathFor(transcriptsDir, worktreeId) {
|
|
13086
13253
|
return join6(transcriptsDir, `${worktreeId}.jsonl`);
|
|
13087
13254
|
}
|
|
13088
|
-
function
|
|
13089
|
-
const
|
|
13090
|
-
for (
|
|
13091
|
-
const
|
|
13092
|
-
|
|
13093
|
-
|
|
13255
|
+
function coalesce(entries) {
|
|
13256
|
+
const out = [];
|
|
13257
|
+
for (const entry of entries) {
|
|
13258
|
+
const last = out.at(-1);
|
|
13259
|
+
const { event } = entry;
|
|
13260
|
+
if ((event.type === "text-delta" || event.type === "thinking-delta") && last?.event.type === event.type) {
|
|
13261
|
+
out[out.length - 1] = { seq: last.seq, event: { type: event.type, text: last.event.text + event.text } };
|
|
13262
|
+
} else {
|
|
13263
|
+
out.push(entry);
|
|
13264
|
+
}
|
|
13094
13265
|
}
|
|
13095
|
-
return
|
|
13266
|
+
return out;
|
|
13096
13267
|
}
|
|
13097
|
-
function
|
|
13098
|
-
|
|
13099
|
-
|
|
13268
|
+
function parseTranscript(text, tag) {
|
|
13269
|
+
const out = [];
|
|
13270
|
+
let torn = 0;
|
|
13271
|
+
for (const line of text.split(`
|
|
13272
|
+
`)) {
|
|
13273
|
+
if (!line)
|
|
13274
|
+
continue;
|
|
13275
|
+
try {
|
|
13276
|
+
out.push(JSON.parse(line));
|
|
13277
|
+
} catch {
|
|
13278
|
+
torn++;
|
|
13279
|
+
}
|
|
13280
|
+
}
|
|
13281
|
+
if (torn)
|
|
13282
|
+
log.warn(tag, `transcript: skipped ${torn} unparsable line(s)`);
|
|
13283
|
+
return out;
|
|
13100
13284
|
}
|
|
13101
13285
|
|
|
13102
13286
|
class Transcript {
|
|
@@ -13112,31 +13296,7 @@ class Transcript {
|
|
|
13112
13296
|
this.seq = (this.entries.at(-1)?.seq ?? -1) + 1;
|
|
13113
13297
|
}
|
|
13114
13298
|
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;
|
|
13299
|
+
return existsSync7(this.path) ? parseTranscript(readFileSync6(this.path, "utf8"), this.tag) : [];
|
|
13140
13300
|
}
|
|
13141
13301
|
append(event) {
|
|
13142
13302
|
const entry = { seq: this.seq++, event };
|
|
@@ -13144,19 +13304,8 @@ class Transcript {
|
|
|
13144
13304
|
const line = `${JSON.stringify(entry)}
|
|
13145
13305
|
`;
|
|
13146
13306
|
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
13307
|
return entry;
|
|
13150
13308
|
}
|
|
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
13309
|
flush() {
|
|
13161
13310
|
return this.writes;
|
|
13162
13311
|
}
|
|
@@ -13692,6 +13841,9 @@ class AcpSession {
|
|
|
13692
13841
|
get queueLength() {
|
|
13693
13842
|
return this.queue.length;
|
|
13694
13843
|
}
|
|
13844
|
+
get unsettled() {
|
|
13845
|
+
return this.queue.length > 0 || this.steered.length > 0 || this.refused !== null || this.asks.size > 0;
|
|
13846
|
+
}
|
|
13695
13847
|
get queueItems() {
|
|
13696
13848
|
return this.waiting().map((q) => q.text);
|
|
13697
13849
|
}
|
|
@@ -13929,7 +14081,7 @@ class AcpSession {
|
|
|
13929
14081
|
describe(e) {
|
|
13930
14082
|
if (isAuthRequired(e))
|
|
13931
14083
|
return this.conn?.spec.loginHint ?? this.d.spec().loginHint;
|
|
13932
|
-
const message = e instanceof Error ? e.message : String(e);
|
|
14084
|
+
const message = (e instanceof Error ? e.message : String(e)).replace(/^Internal error: /, "");
|
|
13933
14085
|
return /connection closed/i.test(message) ? this.conn?.link.exitInfo() ?? message : message;
|
|
13934
14086
|
}
|
|
13935
14087
|
async record({ text, attachments }) {
|
|
@@ -14214,6 +14366,8 @@ class AcpSession {
|
|
|
14214
14366
|
}
|
|
14215
14367
|
askPermission(params) {
|
|
14216
14368
|
const plan = params.toolCall.kind === "switch_mode";
|
|
14369
|
+
if (plan)
|
|
14370
|
+
this.d.onPlan?.();
|
|
14217
14371
|
const offered = plan ? params.options : params.options.filter((o) => o.kind !== "allow_always");
|
|
14218
14372
|
const choices = offered.map((o) => ({ id: o.optionId, name: o.name, kind: o.kind }));
|
|
14219
14373
|
return this.openAsk((id) => ({
|
|
@@ -14611,24 +14765,31 @@ function setsGrant(line) {
|
|
|
14611
14765
|
// ../daemon/src/runtime/proxy.ts
|
|
14612
14766
|
var DIAL_TIMEOUT_MS = 5000;
|
|
14613
14767
|
var frame = (ev) => typeof ev.data === "string" ? ev.data : new Uint8Array(ev.data);
|
|
14768
|
+
var VITE_PING = "text/x-vite-ping";
|
|
14614
14769
|
function previewHandler(opts) {
|
|
14615
14770
|
const hostPart = (t) => t.host.includes(":") ? `[${t.host}]` : t.host;
|
|
14616
14771
|
return {
|
|
14617
14772
|
async fetch(req, upgrade) {
|
|
14618
14773
|
const url = new URL(req.url);
|
|
14619
|
-
const target = opts.getTarget();
|
|
14620
14774
|
if (url.pathname === "/__toyon/bridge.js") {
|
|
14621
14775
|
return new Response(opts.bridgeScript(), {
|
|
14622
14776
|
headers: { "content-type": "text/javascript", "cache-control": "no-store" }
|
|
14623
14777
|
});
|
|
14624
14778
|
}
|
|
14779
|
+
const isUpgrade = req.headers.get("upgrade")?.toLowerCase() === "websocket";
|
|
14780
|
+
const machine = isUpgrade || req.headers.get("accept") === VITE_PING;
|
|
14781
|
+
if (!machine)
|
|
14782
|
+
opts.onRequest?.();
|
|
14783
|
+
let target = opts.getTarget();
|
|
14784
|
+
if (target == null && !machine && opts.ready)
|
|
14785
|
+
target = await opts.ready();
|
|
14625
14786
|
if (target == null) {
|
|
14626
14787
|
return new Response(waitingPage(), {
|
|
14627
14788
|
status: 503,
|
|
14628
14789
|
headers: { "content-type": "text/html; charset=utf-8", "retry-after": "2" }
|
|
14629
14790
|
});
|
|
14630
14791
|
}
|
|
14631
|
-
if (
|
|
14792
|
+
if (isUpgrade) {
|
|
14632
14793
|
const proto = req.headers.get("sec-websocket-protocol") ?? undefined;
|
|
14633
14794
|
const upstreamUrl = `ws://${hostPart(target)}:${target.port}${url.pathname}${url.search}`;
|
|
14634
14795
|
const pending = [];
|
|
@@ -14982,6 +15143,8 @@ async function reachableHost(port) {
|
|
|
14982
15143
|
var LOG_RING_SIZE = 500;
|
|
14983
15144
|
var POLL_INTERVAL_MS = 500;
|
|
14984
15145
|
var POLL_ATTEMPTS = 120;
|
|
15146
|
+
var FAST_POLL_MS = 100;
|
|
15147
|
+
var FAST_POLL_WINDOW_MS = 3000;
|
|
14985
15148
|
var PROC_RING = 128 * 1024;
|
|
14986
15149
|
var PROC_COLS = 120;
|
|
14987
15150
|
var PROC_ROWS = 30;
|
|
@@ -14995,6 +15158,7 @@ class WorktreeProcs {
|
|
|
14995
15158
|
opts;
|
|
14996
15159
|
procs = new Map;
|
|
14997
15160
|
stopped = false;
|
|
15161
|
+
asleep = false;
|
|
14998
15162
|
lines = [];
|
|
14999
15163
|
constructor(worktreePath, onProc, onLog, onData = () => {}, onStreamExit = () => {}, opts = {}) {
|
|
15000
15164
|
this.worktreePath = worktreePath;
|
|
@@ -15041,7 +15205,7 @@ class WorktreeProcs {
|
|
|
15041
15205
|
cols: mp.pty?.cols ?? PROC_COLS,
|
|
15042
15206
|
rows: mp.pty?.rows ?? PROC_ROWS,
|
|
15043
15207
|
file: "sh",
|
|
15044
|
-
args: ["-c", command],
|
|
15208
|
+
args: ["-c", execForm(command)],
|
|
15045
15209
|
ring: PROC_RING
|
|
15046
15210
|
}, (data) => {
|
|
15047
15211
|
this.onData(name, data);
|
|
@@ -15062,7 +15226,7 @@ class WorktreeProcs {
|
|
|
15062
15226
|
const { name } = mp.state;
|
|
15063
15227
|
mp.state.exitCode = code;
|
|
15064
15228
|
this.onStreamExit(name, code);
|
|
15065
|
-
if (this.stopped || mp.state.status === "stopped")
|
|
15229
|
+
if (this.stopped || mp.state.status === "stopped" || mp.state.status === "asleep")
|
|
15066
15230
|
return;
|
|
15067
15231
|
if (mp.handInput) {
|
|
15068
15232
|
mp.state.status = "stopped";
|
|
@@ -15088,8 +15252,9 @@ class WorktreeProcs {
|
|
|
15088
15252
|
}
|
|
15089
15253
|
}
|
|
15090
15254
|
async pollPort(mp) {
|
|
15091
|
-
const
|
|
15092
|
-
|
|
15255
|
+
const deadline = Date.now() + (this.opts.pollAttempts ?? POLL_ATTEMPTS) * POLL_INTERVAL_MS;
|
|
15256
|
+
const started = Date.now();
|
|
15257
|
+
while (Date.now() < deadline) {
|
|
15093
15258
|
if (this.stopped || mp.state.status !== "starting")
|
|
15094
15259
|
return;
|
|
15095
15260
|
const host = await reachableHost(mp.state.port);
|
|
@@ -15099,7 +15264,7 @@ class WorktreeProcs {
|
|
|
15099
15264
|
this.onProc({ ...mp.state });
|
|
15100
15265
|
return;
|
|
15101
15266
|
}
|
|
15102
|
-
await Bun.sleep(POLL_INTERVAL_MS);
|
|
15267
|
+
await Bun.sleep(Date.now() - started < FAST_POLL_WINDOW_MS ? FAST_POLL_MS : POLL_INTERVAL_MS);
|
|
15103
15268
|
}
|
|
15104
15269
|
await this.diagnose(mp);
|
|
15105
15270
|
}
|
|
@@ -15146,7 +15311,7 @@ class WorktreeProcs {
|
|
|
15146
15311
|
}
|
|
15147
15312
|
restart(name) {
|
|
15148
15313
|
const mp = this.procs.get(name);
|
|
15149
|
-
if (!mp)
|
|
15314
|
+
if (!mp || this.asleep)
|
|
15150
15315
|
return;
|
|
15151
15316
|
mp.restarts = 0;
|
|
15152
15317
|
if (mp.state.status === "crashed") {
|
|
@@ -15162,6 +15327,35 @@ class WorktreeProcs {
|
|
|
15162
15327
|
async killProc(mp) {
|
|
15163
15328
|
await mp.pty?.kill();
|
|
15164
15329
|
}
|
|
15330
|
+
async sleep() {
|
|
15331
|
+
if (this.asleep || this.stopped)
|
|
15332
|
+
return;
|
|
15333
|
+
this.asleep = true;
|
|
15334
|
+
const exits = [];
|
|
15335
|
+
for (const mp of this.procs.values()) {
|
|
15336
|
+
mp.state.status = "asleep";
|
|
15337
|
+
mp.state.pid = undefined;
|
|
15338
|
+
this.onProc({ ...mp.state });
|
|
15339
|
+
exits.push(this.killProc(mp));
|
|
15340
|
+
}
|
|
15341
|
+
await Promise.all(exits);
|
|
15342
|
+
}
|
|
15343
|
+
wake() {
|
|
15344
|
+
if (!this.asleep || this.stopped)
|
|
15345
|
+
return;
|
|
15346
|
+
this.asleep = false;
|
|
15347
|
+
for (const mp of this.procs.values()) {
|
|
15348
|
+
mp.restarts = 0;
|
|
15349
|
+
this.spawnProc(mp);
|
|
15350
|
+
}
|
|
15351
|
+
}
|
|
15352
|
+
pgids() {
|
|
15353
|
+
const out = [];
|
|
15354
|
+
for (const mp of this.procs.values())
|
|
15355
|
+
if (mp.pty?.alive)
|
|
15356
|
+
out.push(mp.pty.pid);
|
|
15357
|
+
return out;
|
|
15358
|
+
}
|
|
15165
15359
|
async stopAll() {
|
|
15166
15360
|
this.stopped = true;
|
|
15167
15361
|
const exits = [];
|
|
@@ -15185,6 +15379,12 @@ class WorktreeProcs {
|
|
|
15185
15379
|
this.onLog(proc, line);
|
|
15186
15380
|
}
|
|
15187
15381
|
}
|
|
15382
|
+
function execForm(command) {
|
|
15383
|
+
const trimmed = command.trim();
|
|
15384
|
+
if (/[;&|<>()`\n]|\$\(/.test(trimmed) || /^[A-Za-z_][A-Za-z0-9_]*=/.test(trimmed))
|
|
15385
|
+
return command;
|
|
15386
|
+
return `exec ${trimmed}`;
|
|
15387
|
+
}
|
|
15188
15388
|
function envStrings(base) {
|
|
15189
15389
|
const env = {};
|
|
15190
15390
|
for (const [k, v] of Object.entries(base))
|
|
@@ -15222,6 +15422,8 @@ function terminalEnv(base, own, urls) {
|
|
|
15222
15422
|
return { ...env, ...urls, TERM: "xterm-256color", COLORTERM: "truecolor", ...own };
|
|
15223
15423
|
}
|
|
15224
15424
|
var DEFAULT_AGENT_ID = "claude";
|
|
15425
|
+
var PREVIEW_WAKE_MS = 8000;
|
|
15426
|
+
var PREVIEW_POLL_MS = 100;
|
|
15225
15427
|
function defaultAgent(wt, d) {
|
|
15226
15428
|
const agent = new AcpSession({
|
|
15227
15429
|
worktreeId: wt.id,
|
|
@@ -15255,6 +15457,13 @@ function defaultAgent(wt, d) {
|
|
|
15255
15457
|
d.state.save();
|
|
15256
15458
|
d.hub.emit("worktreesChanged");
|
|
15257
15459
|
},
|
|
15460
|
+
onPlan: () => {
|
|
15461
|
+
const w = d.state.worktree(wt.id);
|
|
15462
|
+
if (!w || w.planned)
|
|
15463
|
+
return;
|
|
15464
|
+
w.planned = true;
|
|
15465
|
+
d.state.save();
|
|
15466
|
+
},
|
|
15258
15467
|
option: (category) => d.state.requireWorktree(wt.id)[OPTION_FIELDS[category]],
|
|
15259
15468
|
onOptionsLearned: (category, choices) => {
|
|
15260
15469
|
const agentId = d.state.requireWorktree(wt.id).agent ?? "";
|
|
@@ -15272,19 +15481,24 @@ function defaultProcs(wt, d) {
|
|
|
15272
15481
|
function defaultTerminal(_wt, opts, onData, onExit) {
|
|
15273
15482
|
return new PtyStream(opts, onData, onExit);
|
|
15274
15483
|
}
|
|
15275
|
-
function defaultProxy(wt, previewName, procs, d) {
|
|
15484
|
+
function defaultProxy(wt, previewName, procs, d, wake) {
|
|
15276
15485
|
return startProxy({
|
|
15277
15486
|
port: wt.proxyPort,
|
|
15278
15487
|
hostname: cloud.bindHost,
|
|
15279
15488
|
remote: d.remote ?? null,
|
|
15280
15489
|
grant: d.grant ?? "",
|
|
15281
15490
|
bridgeScript: d.bridgeScript,
|
|
15282
|
-
getTarget: () => previewTargetOf(procs, previewName)
|
|
15491
|
+
getTarget: () => previewTargetOf(procs, previewName),
|
|
15492
|
+
onRequest: wake.onRequest,
|
|
15493
|
+
ready: wake.ready
|
|
15283
15494
|
});
|
|
15284
15495
|
}
|
|
15496
|
+
function previewProcOf(procs, previewName) {
|
|
15497
|
+
return procs.states().find((p) => p.name === previewName) ?? procs.states()[0];
|
|
15498
|
+
}
|
|
15285
15499
|
function previewTargetOf(procs, previewName) {
|
|
15286
|
-
const st = procs
|
|
15287
|
-
if (
|
|
15500
|
+
const st = previewProcOf(procs, previewName);
|
|
15501
|
+
if (st?.status !== "running")
|
|
15288
15502
|
return null;
|
|
15289
15503
|
return { port: st.boundPort ?? st.port, host: st.host ?? "127.0.0.1" };
|
|
15290
15504
|
}
|
|
@@ -15293,9 +15507,118 @@ class RuntimeRegistry {
|
|
|
15293
15507
|
deps;
|
|
15294
15508
|
runtimes = new Map;
|
|
15295
15509
|
looseShells = new Map;
|
|
15510
|
+
holds = new Map;
|
|
15511
|
+
settingUp = new Set;
|
|
15512
|
+
starting = new Set;
|
|
15296
15513
|
constructor(deps) {
|
|
15297
15514
|
this.deps = deps;
|
|
15298
15515
|
}
|
|
15516
|
+
hold(id, tag) {
|
|
15517
|
+
let tags = this.holds.get(id);
|
|
15518
|
+
if (!tags) {
|
|
15519
|
+
tags = new Set;
|
|
15520
|
+
this.holds.set(id, tags);
|
|
15521
|
+
}
|
|
15522
|
+
tags.add(tag);
|
|
15523
|
+
this.deps.hub.emit("holdsChanged", id, tags.size);
|
|
15524
|
+
}
|
|
15525
|
+
release(id, tag) {
|
|
15526
|
+
const tags = this.holds.get(id);
|
|
15527
|
+
if (!tags?.delete(tag))
|
|
15528
|
+
return;
|
|
15529
|
+
if (tags.size === 0)
|
|
15530
|
+
this.holds.delete(id);
|
|
15531
|
+
this.deps.hub.emit("holdsChanged", id, tags.size);
|
|
15532
|
+
}
|
|
15533
|
+
holdCount(id) {
|
|
15534
|
+
return this.holds.get(id)?.size ?? 0;
|
|
15535
|
+
}
|
|
15536
|
+
busy(id) {
|
|
15537
|
+
const agent = this.agentFor(id);
|
|
15538
|
+
if (agent && (agent.status !== "idle" || agent.unsettled))
|
|
15539
|
+
return true;
|
|
15540
|
+
return this.holdCount(id) > 0;
|
|
15541
|
+
}
|
|
15542
|
+
anyBusy() {
|
|
15543
|
+
for (const id of this.runtimes.keys())
|
|
15544
|
+
if (this.busy(id))
|
|
15545
|
+
return true;
|
|
15546
|
+
for (const id of this.holds.keys())
|
|
15547
|
+
if (this.busy(id))
|
|
15548
|
+
return true;
|
|
15549
|
+
return false;
|
|
15550
|
+
}
|
|
15551
|
+
markSetup(id, on) {
|
|
15552
|
+
if (on)
|
|
15553
|
+
this.settingUp.add(id);
|
|
15554
|
+
else
|
|
15555
|
+
this.settingUp.delete(id);
|
|
15556
|
+
}
|
|
15557
|
+
async wake(id) {
|
|
15558
|
+
const wt = this.deps.state.worktree(id);
|
|
15559
|
+
if (!wt)
|
|
15560
|
+
return;
|
|
15561
|
+
const rt = this.runtimes.get(id);
|
|
15562
|
+
if (rt?.procs) {
|
|
15563
|
+
if (!rt.procs.asleep)
|
|
15564
|
+
return;
|
|
15565
|
+
rt.procs.wake();
|
|
15566
|
+
log.info(id, "awake");
|
|
15567
|
+
this.deps.hub.emit("worktreesChanged");
|
|
15568
|
+
return;
|
|
15569
|
+
}
|
|
15570
|
+
if (this.settingUp.has(id))
|
|
15571
|
+
return;
|
|
15572
|
+
const repo = this.deps.state.repos.find((r) => r.id === wt.repoId);
|
|
15573
|
+
if (repo)
|
|
15574
|
+
await this.start(wt, repo);
|
|
15575
|
+
}
|
|
15576
|
+
async sleep(id) {
|
|
15577
|
+
const rt = this.runtimes.get(id);
|
|
15578
|
+
if (!rt?.procs || rt.procs.asleep)
|
|
15579
|
+
return;
|
|
15580
|
+
await rt.procs.sleep();
|
|
15581
|
+
this.deps.hub.emit("worktreesChanged");
|
|
15582
|
+
}
|
|
15583
|
+
async awaitPreview(id, ms) {
|
|
15584
|
+
const deadline = Date.now() + ms;
|
|
15585
|
+
for (;; ) {
|
|
15586
|
+
const rt = this.runtimes.get(id);
|
|
15587
|
+
if (!rt?.procs)
|
|
15588
|
+
return null;
|
|
15589
|
+
const st = previewProcOf(rt.procs, rt.previewName);
|
|
15590
|
+
if (st?.status === "running")
|
|
15591
|
+
return previewTargetOf(rt.procs, rt.previewName);
|
|
15592
|
+
const coming = st ? st.status === "starting" : this.starting.has(id);
|
|
15593
|
+
if (!coming || Date.now() >= deadline)
|
|
15594
|
+
return null;
|
|
15595
|
+
await Bun.sleep(PREVIEW_POLL_MS);
|
|
15596
|
+
}
|
|
15597
|
+
}
|
|
15598
|
+
tiers() {
|
|
15599
|
+
let awake = 0;
|
|
15600
|
+
let asleep = 0;
|
|
15601
|
+
for (const rt of this.runtimes.values()) {
|
|
15602
|
+
if (!rt.procs)
|
|
15603
|
+
continue;
|
|
15604
|
+
if (rt.procs.asleep)
|
|
15605
|
+
asleep++;
|
|
15606
|
+
else
|
|
15607
|
+
awake++;
|
|
15608
|
+
}
|
|
15609
|
+
return { awake, asleep };
|
|
15610
|
+
}
|
|
15611
|
+
awake() {
|
|
15612
|
+
const out = [];
|
|
15613
|
+
for (const [id, rt] of this.runtimes) {
|
|
15614
|
+
if (rt.procs && !rt.procs.asleep)
|
|
15615
|
+
out.push({ id, pgids: rt.procs.pgids() });
|
|
15616
|
+
}
|
|
15617
|
+
return out;
|
|
15618
|
+
}
|
|
15619
|
+
isAsleep(id) {
|
|
15620
|
+
return this.runtimes.get(id)?.procs?.asleep ?? false;
|
|
15621
|
+
}
|
|
15299
15622
|
openLooseShell(id, cwd, cols, rows) {
|
|
15300
15623
|
let term = this.looseShells.get(id);
|
|
15301
15624
|
if (!term?.alive) {
|
|
@@ -15380,19 +15703,30 @@ class RuntimeRegistry {
|
|
|
15380
15703
|
const urls = { ...procUrlEnv(procs.states(), previewName), ...worktreeEnv(wt, repo) };
|
|
15381
15704
|
return { ...urls, ...expandEnv(run.env, urls) };
|
|
15382
15705
|
};
|
|
15383
|
-
|
|
15384
|
-
|
|
15385
|
-
|
|
15386
|
-
|
|
15387
|
-
|
|
15388
|
-
|
|
15706
|
+
this.starting.add(wt.id);
|
|
15707
|
+
try {
|
|
15708
|
+
for (const [name, cmd] of Object.entries(run.procs)) {
|
|
15709
|
+
if (name !== previewName)
|
|
15710
|
+
await procs.start(name, cmd, envFor());
|
|
15711
|
+
}
|
|
15712
|
+
if (previewName && run.procs[previewName]) {
|
|
15713
|
+
await procs.start(previewName, run.procs[previewName], envFor());
|
|
15714
|
+
}
|
|
15715
|
+
} finally {
|
|
15716
|
+
this.starting.delete(wt.id);
|
|
15389
15717
|
}
|
|
15390
15718
|
if (!this.deps.state.worktree(wt.id) || this.runtimes.get(wt.id) !== rt) {
|
|
15391
15719
|
this.returnLease(wt.id);
|
|
15392
15720
|
await procs.stopAll();
|
|
15393
15721
|
return;
|
|
15394
15722
|
}
|
|
15395
|
-
rt.proxy = (this.deps.makeProxy ?? defaultProxy)(live, previewName, procs, this.deps
|
|
15723
|
+
rt.proxy = (this.deps.makeProxy ?? defaultProxy)(live, previewName, procs, this.deps, {
|
|
15724
|
+
onRequest: () => {
|
|
15725
|
+
this.deps.hub.emit("previewRequest", wt.id);
|
|
15726
|
+
fireAndForget(wt.id, this.wake(wt.id), "wake on request");
|
|
15727
|
+
},
|
|
15728
|
+
ready: () => this.awaitPreview(wt.id, PREVIEW_WAKE_MS)
|
|
15729
|
+
});
|
|
15396
15730
|
this.deps.hub.emit("worktreesChanged");
|
|
15397
15731
|
}
|
|
15398
15732
|
leased = new Map;
|
|
@@ -15415,6 +15749,8 @@ class RuntimeRegistry {
|
|
|
15415
15749
|
await procs?.stopAll();
|
|
15416
15750
|
}
|
|
15417
15751
|
async stop(id) {
|
|
15752
|
+
this.holds.delete(id);
|
|
15753
|
+
this.settingUp.delete(id);
|
|
15418
15754
|
const rt = this.runtimes.get(id);
|
|
15419
15755
|
if (!rt)
|
|
15420
15756
|
return;
|
|
@@ -15537,6 +15873,8 @@ class RuntimeRegistry {
|
|
|
15537
15873
|
this.startLogin(id, rt.login.run);
|
|
15538
15874
|
} else if (stream === SHELL_STREAM)
|
|
15539
15875
|
await rt?.shell?.kill();
|
|
15876
|
+
else if (rt?.procs?.asleep)
|
|
15877
|
+
await this.wake(id);
|
|
15540
15878
|
else
|
|
15541
15879
|
rt?.procs?.restart(stream);
|
|
15542
15880
|
}
|
|
@@ -15545,11 +15883,7 @@ class RuntimeRegistry {
|
|
|
15545
15883
|
}
|
|
15546
15884
|
previewTarget(id) {
|
|
15547
15885
|
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;
|
|
15886
|
+
return rt?.procs ? previewTargetOf(rt.procs, rt.previewName) : null;
|
|
15553
15887
|
}
|
|
15554
15888
|
async shutdown() {
|
|
15555
15889
|
this.pruneLooseShells(new Set);
|
|
@@ -15859,6 +16193,20 @@ var NAME_SYSTEM = "You are a naming assistant. Reply with only the requested nam
|
|
|
15859
16193
|
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
16194
|
|
|
15861
16195
|
Task: ${task.slice(0, 500)}`;
|
|
16196
|
+
function taskText(text, attachments = []) {
|
|
16197
|
+
if (text.trim())
|
|
16198
|
+
return text;
|
|
16199
|
+
return attachments.map(attachmentText).join(`
|
|
16200
|
+
|
|
16201
|
+
`);
|
|
16202
|
+
}
|
|
16203
|
+
function attachmentText(a) {
|
|
16204
|
+
if (a.kind === "paste")
|
|
16205
|
+
return a.text;
|
|
16206
|
+
if (a.kind === "image")
|
|
16207
|
+
return a.name ? `image ${a.name}` : "an image";
|
|
16208
|
+
return `element <${a.component ?? a.tag}>${a.text ? ` "${a.text}"` : ""}`;
|
|
16209
|
+
}
|
|
15862
16210
|
function parseName(text) {
|
|
15863
16211
|
if (!text)
|
|
15864
16212
|
return null;
|
|
@@ -15932,15 +16280,47 @@ function makePlanner(agents) {
|
|
|
15932
16280
|
}
|
|
15933
16281
|
|
|
15934
16282
|
// ../daemon/src/core/assets.ts
|
|
15935
|
-
import { existsSync as existsSync9 } from "fs";
|
|
15936
|
-
import { join as join7 } from "path";
|
|
16283
|
+
import { existsSync as existsSync9, readdirSync as readdirSync2, rmSync, statSync as statSync2 } from "fs";
|
|
16284
|
+
import { join as join7, resolve as resolve5 } from "path";
|
|
15937
16285
|
function locateAssets(here, env = process.env) {
|
|
15938
16286
|
const packaged = existsSync9(join7(here, "shell", "index.html"));
|
|
15939
16287
|
return {
|
|
15940
16288
|
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"))
|
|
16289
|
+
bridgeJs: env.TOYON_BRIDGE_JS ?? (packaged ? join7(here, "bridge.js") : join7(here, "../../bridge/dist/bridge.js")),
|
|
16290
|
+
sourceRoot: packaged ? null : resolve5(here, "../../.."),
|
|
16291
|
+
packageJson: packaged ? join7(here, "..", "package.json") : null
|
|
15942
16292
|
};
|
|
15943
16293
|
}
|
|
16294
|
+
var KEEP_MS = 3 * 24 * 60 * 60 * 1000;
|
|
16295
|
+
function pruneAssets(shellDist) {
|
|
16296
|
+
const dir = join7(shellDist, "assets");
|
|
16297
|
+
let names;
|
|
16298
|
+
try {
|
|
16299
|
+
names = readdirSync2(dir);
|
|
16300
|
+
} catch {
|
|
16301
|
+
return 0;
|
|
16302
|
+
}
|
|
16303
|
+
const found = [];
|
|
16304
|
+
for (const name of names) {
|
|
16305
|
+
const path = join7(dir, name);
|
|
16306
|
+
try {
|
|
16307
|
+
found.push({ path, mtime: statSync2(path).mtimeMs });
|
|
16308
|
+
} catch {}
|
|
16309
|
+
}
|
|
16310
|
+
if (found.length === 0)
|
|
16311
|
+
return 0;
|
|
16312
|
+
const newest = Math.max(...found.map((f) => f.mtime));
|
|
16313
|
+
let gone = 0;
|
|
16314
|
+
for (const f of found) {
|
|
16315
|
+
if (newest - f.mtime <= KEEP_MS)
|
|
16316
|
+
continue;
|
|
16317
|
+
try {
|
|
16318
|
+
rmSync(f.path, { recursive: true, force: true });
|
|
16319
|
+
gone++;
|
|
16320
|
+
} catch {}
|
|
16321
|
+
}
|
|
16322
|
+
return gone;
|
|
16323
|
+
}
|
|
15944
16324
|
|
|
15945
16325
|
// ../daemon/src/core/dialog.ts
|
|
15946
16326
|
import { existsSync as existsSync10 } from "fs";
|
|
@@ -16030,38 +16410,181 @@ class Hub {
|
|
|
16030
16410
|
}
|
|
16031
16411
|
}
|
|
16032
16412
|
|
|
16033
|
-
// ../daemon/src/core/
|
|
16034
|
-
import {
|
|
16035
|
-
import {
|
|
16036
|
-
|
|
16037
|
-
|
|
16038
|
-
|
|
16039
|
-
ensureDirs(paths);
|
|
16040
|
-
const STATE_FILE = paths.stateFile;
|
|
16041
|
-
if (!existsSync11(STATE_FILE))
|
|
16042
|
-
return structuredClone(empty);
|
|
16043
|
-
let raw;
|
|
16044
|
-
try {
|
|
16045
|
-
raw = readFileSync8(STATE_FILE, "utf8");
|
|
16046
|
-
} catch (e) {
|
|
16047
|
-
log.error("state", `cannot read ${STATE_FILE}; starting empty`, e);
|
|
16048
|
-
return structuredClone(empty);
|
|
16049
|
-
}
|
|
16050
|
-
let state;
|
|
16051
|
-
try {
|
|
16052
|
-
state = { ...structuredClone(empty), ...JSON.parse(raw) };
|
|
16053
|
-
} catch (e) {
|
|
16054
|
-
const backup = `${STATE_FILE}.corrupt-${Date.now()}`;
|
|
16055
|
-
try {
|
|
16056
|
-
writeFileSync5(backup, raw);
|
|
16057
|
-
} catch (be) {
|
|
16058
|
-
log.warn("state", "could not write the corrupt-state backup", be);
|
|
16059
|
-
}
|
|
16060
|
-
log.error("state", `${STATE_FILE} is not valid JSON; copied to ${backup} and starting empty`, e);
|
|
16061
|
-
return structuredClone(empty);
|
|
16413
|
+
// ../daemon/src/core/restart.ts
|
|
16414
|
+
import { spawn as spawn2 } from "child_process";
|
|
16415
|
+
import { openSync } from "fs";
|
|
16416
|
+
function restartable(env = process.env) {
|
|
16417
|
+
if (env.TOYON_WORKTREE !== undefined) {
|
|
16418
|
+
return { ok: false, reason: "this daemon runs as a toyon process; restart it from its terminal tab" };
|
|
16062
16419
|
}
|
|
16063
|
-
|
|
16064
|
-
|
|
16420
|
+
return { ok: true };
|
|
16421
|
+
}
|
|
16422
|
+
function respawn(logFile) {
|
|
16423
|
+
const logFd = openSync(logFile, "a");
|
|
16424
|
+
const child = spawn2(process.execPath, process.argv.slice(1), {
|
|
16425
|
+
detached: true,
|
|
16426
|
+
stdio: ["ignore", logFd, logFd],
|
|
16427
|
+
env: { ...process.env }
|
|
16428
|
+
});
|
|
16429
|
+
child.on("error", (e) => log.warn("daemon", `the replacement did not start: ${e.message}`));
|
|
16430
|
+
child.unref();
|
|
16431
|
+
}
|
|
16432
|
+
|
|
16433
|
+
// ../daemon/src/core/restarter.ts
|
|
16434
|
+
class Restarter {
|
|
16435
|
+
d;
|
|
16436
|
+
asked = false;
|
|
16437
|
+
gone = false;
|
|
16438
|
+
announced = null;
|
|
16439
|
+
constructor(d) {
|
|
16440
|
+
this.d = d;
|
|
16441
|
+
const settle = () => {
|
|
16442
|
+
if (this.asked)
|
|
16443
|
+
this.attempt();
|
|
16444
|
+
};
|
|
16445
|
+
d.hub.on("agentStatus", settle);
|
|
16446
|
+
d.hub.on("worktreesChanged", settle);
|
|
16447
|
+
}
|
|
16448
|
+
request() {
|
|
16449
|
+
const refused = this.d.refusal();
|
|
16450
|
+
if (refused)
|
|
16451
|
+
return refused;
|
|
16452
|
+
this.asked = true;
|
|
16453
|
+
this.attempt();
|
|
16454
|
+
return null;
|
|
16455
|
+
}
|
|
16456
|
+
waitingOn() {
|
|
16457
|
+
if (!this.asked)
|
|
16458
|
+
return null;
|
|
16459
|
+
return this.gone ? [] : this.working();
|
|
16460
|
+
}
|
|
16461
|
+
working() {
|
|
16462
|
+
return this.d.state.worktrees.filter((w) => this.d.runtime.agentFor(w.id)?.status === "working").map((w) => w.title);
|
|
16463
|
+
}
|
|
16464
|
+
attempt() {
|
|
16465
|
+
if (this.gone)
|
|
16466
|
+
return;
|
|
16467
|
+
const names = this.working();
|
|
16468
|
+
if (names.length === 0)
|
|
16469
|
+
this.gone = true;
|
|
16470
|
+
const key = JSON.stringify(this.waitingOn());
|
|
16471
|
+
if (key !== this.announced) {
|
|
16472
|
+
this.announced = key;
|
|
16473
|
+
this.d.hub.emit("updateChanged");
|
|
16474
|
+
}
|
|
16475
|
+
if (this.gone)
|
|
16476
|
+
this.d.go();
|
|
16477
|
+
}
|
|
16478
|
+
}
|
|
16479
|
+
|
|
16480
|
+
// ../daemon/src/core/self.ts
|
|
16481
|
+
function needsRestart(path) {
|
|
16482
|
+
return path.startsWith("packages/daemon/") || path.startsWith("packages/shared/") || path === "package.json" || path === "bun.lock";
|
|
16483
|
+
}
|
|
16484
|
+
function needsRebuild(path) {
|
|
16485
|
+
return path.startsWith("packages/shell/") || path.startsWith("packages/bridge/") || path.startsWith("packages/shared/");
|
|
16486
|
+
}
|
|
16487
|
+
function classify(paths) {
|
|
16488
|
+
let rebuild = false;
|
|
16489
|
+
let restart = false;
|
|
16490
|
+
for (const path of paths) {
|
|
16491
|
+
if (needsRebuild(path))
|
|
16492
|
+
rebuild = true;
|
|
16493
|
+
if (needsRestart(path))
|
|
16494
|
+
restart = true;
|
|
16495
|
+
}
|
|
16496
|
+
return { rebuild, restart };
|
|
16497
|
+
}
|
|
16498
|
+
|
|
16499
|
+
class SelfWatch {
|
|
16500
|
+
root;
|
|
16501
|
+
booted = null;
|
|
16502
|
+
state = null;
|
|
16503
|
+
constructor(root) {
|
|
16504
|
+
this.root = root;
|
|
16505
|
+
}
|
|
16506
|
+
async start() {
|
|
16507
|
+
if (!this.root)
|
|
16508
|
+
return;
|
|
16509
|
+
const head = await git(this.root, "rev-parse", "HEAD");
|
|
16510
|
+
if (!head.ok) {
|
|
16511
|
+
log.debug("self", `no HEAD in ${this.root}; not watching it`);
|
|
16512
|
+
return;
|
|
16513
|
+
}
|
|
16514
|
+
this.booted = head.out.trim();
|
|
16515
|
+
}
|
|
16516
|
+
own(repo) {
|
|
16517
|
+
return this.root !== null && repo.path === this.root;
|
|
16518
|
+
}
|
|
16519
|
+
async check(repo) {
|
|
16520
|
+
if (this.booted === null || !this.own(repo))
|
|
16521
|
+
return false;
|
|
16522
|
+
const names = await git(this.root ?? "", "diff", "--name-only", `${this.booted}..${repo.defaultBranch}`);
|
|
16523
|
+
if (!names.ok) {
|
|
16524
|
+
log.debug("self", `could not diff ${this.booted}..${repo.defaultBranch}: ${names.err}`);
|
|
16525
|
+
return false;
|
|
16526
|
+
}
|
|
16527
|
+
const paths = names.out.split(`
|
|
16528
|
+
`).filter((l) => l !== "");
|
|
16529
|
+
const { rebuild, restart } = classify(paths);
|
|
16530
|
+
return this.set({ repoId: repo.id, rebuild, restart, building: this.state?.building ?? false });
|
|
16531
|
+
}
|
|
16532
|
+
building(on, failure) {
|
|
16533
|
+
if (!this.state)
|
|
16534
|
+
return false;
|
|
16535
|
+
const done = !on && failure === undefined;
|
|
16536
|
+
return this.set({
|
|
16537
|
+
...this.state,
|
|
16538
|
+
building: on,
|
|
16539
|
+
rebuild: done ? false : this.state.rebuild,
|
|
16540
|
+
buildFailed: on ? undefined : failure
|
|
16541
|
+
});
|
|
16542
|
+
}
|
|
16543
|
+
get() {
|
|
16544
|
+
return this.state;
|
|
16545
|
+
}
|
|
16546
|
+
set(next) {
|
|
16547
|
+
const quiet = !next.rebuild && !next.restart && !next.building && next.buildFailed === undefined;
|
|
16548
|
+
const value = quiet ? null : next;
|
|
16549
|
+
if (JSON.stringify(value) === JSON.stringify(this.state))
|
|
16550
|
+
return false;
|
|
16551
|
+
this.state = value;
|
|
16552
|
+
return true;
|
|
16553
|
+
}
|
|
16554
|
+
}
|
|
16555
|
+
|
|
16556
|
+
// ../daemon/src/core/state.ts
|
|
16557
|
+
import { randomBytes } from "crypto";
|
|
16558
|
+
import { existsSync as existsSync11, readFileSync as readFileSync8, renameSync as renameSync2, writeFileSync as writeFileSync4 } from "fs";
|
|
16559
|
+
var SEEN_WORKTREES = 50;
|
|
16560
|
+
var empty = { repos: [], worktrees: [], sessions: {} };
|
|
16561
|
+
function loadState(paths) {
|
|
16562
|
+
ensureDirs(paths);
|
|
16563
|
+
const STATE_FILE = paths.stateFile;
|
|
16564
|
+
if (!existsSync11(STATE_FILE))
|
|
16565
|
+
return structuredClone(empty);
|
|
16566
|
+
let raw;
|
|
16567
|
+
try {
|
|
16568
|
+
raw = readFileSync8(STATE_FILE, "utf8");
|
|
16569
|
+
} catch (e) {
|
|
16570
|
+
log.error("state", `cannot read ${STATE_FILE}; starting empty`, e);
|
|
16571
|
+
return structuredClone(empty);
|
|
16572
|
+
}
|
|
16573
|
+
let state;
|
|
16574
|
+
try {
|
|
16575
|
+
state = { ...structuredClone(empty), ...JSON.parse(raw) };
|
|
16576
|
+
} catch (e) {
|
|
16577
|
+
const backup = `${STATE_FILE}.corrupt-${Date.now()}`;
|
|
16578
|
+
try {
|
|
16579
|
+
writeFileSync4(backup, raw);
|
|
16580
|
+
} catch (be) {
|
|
16581
|
+
log.warn("state", "could not write the corrupt-state backup", be);
|
|
16582
|
+
}
|
|
16583
|
+
log.error("state", `${STATE_FILE} is not valid JSON; copied to ${backup} and starting empty`, e);
|
|
16584
|
+
return structuredClone(empty);
|
|
16585
|
+
}
|
|
16586
|
+
const ids = new Set(state.worktrees.map((w) => w.id));
|
|
16587
|
+
for (const id of Object.keys(state.sessions))
|
|
16065
16588
|
if (!ids.has(id))
|
|
16066
16589
|
delete state.sessions[id];
|
|
16067
16590
|
for (const id of Object.keys(state.seen ?? {})) {
|
|
@@ -16080,8 +16603,8 @@ function loadState(paths) {
|
|
|
16080
16603
|
}
|
|
16081
16604
|
function saveState(paths, state) {
|
|
16082
16605
|
const tmp = `${paths.stateFile}.tmp`;
|
|
16083
|
-
|
|
16084
|
-
|
|
16606
|
+
writeFileSync4(tmp, JSON.stringify(state, null, 2));
|
|
16607
|
+
renameSync2(tmp, paths.stateFile);
|
|
16085
16608
|
}
|
|
16086
16609
|
function loadOrCreateToken(paths) {
|
|
16087
16610
|
ensureDirs(paths);
|
|
@@ -16089,7 +16612,7 @@ function loadOrCreateToken(paths) {
|
|
|
16089
16612
|
if (existsSync11(TOKEN_FILE))
|
|
16090
16613
|
return readFileSync8(TOKEN_FILE, "utf8").trim();
|
|
16091
16614
|
const token = randomBytes(32).toString("hex");
|
|
16092
|
-
|
|
16615
|
+
writeFileSync4(TOKEN_FILE, token, { mode: 384 });
|
|
16093
16616
|
return token;
|
|
16094
16617
|
}
|
|
16095
16618
|
|
|
@@ -16221,6 +16744,13 @@ class StateStore {
|
|
|
16221
16744
|
this.state.theme = prefs;
|
|
16222
16745
|
this.save();
|
|
16223
16746
|
}
|
|
16747
|
+
get updateFailed() {
|
|
16748
|
+
return this.state.updateFailed;
|
|
16749
|
+
}
|
|
16750
|
+
setUpdateFailed(failed) {
|
|
16751
|
+
this.state.updateFailed = failed;
|
|
16752
|
+
this.save();
|
|
16753
|
+
}
|
|
16224
16754
|
save() {
|
|
16225
16755
|
saveState(this.paths, this.state);
|
|
16226
16756
|
}
|
|
@@ -16798,6 +17328,87 @@ async function loadProjectTypescript(root) {
|
|
|
16798
17328
|
}
|
|
16799
17329
|
}
|
|
16800
17330
|
|
|
17331
|
+
// ../daemon/src/drafts/store.ts
|
|
17332
|
+
import { existsSync as existsSync12, readFileSync as readFileSync9, renameSync as renameSync3, writeFileSync as writeFileSync5 } from "fs";
|
|
17333
|
+
var SAVE_DELAY_MS = 1000;
|
|
17334
|
+
|
|
17335
|
+
class DraftStore {
|
|
17336
|
+
d;
|
|
17337
|
+
drafts = {};
|
|
17338
|
+
saveTimer = null;
|
|
17339
|
+
constructor(d) {
|
|
17340
|
+
this.d = d;
|
|
17341
|
+
if (!existsSync12(d.file))
|
|
17342
|
+
return;
|
|
17343
|
+
try {
|
|
17344
|
+
const raw = JSON.parse(readFileSync9(d.file, "utf8"));
|
|
17345
|
+
if (!raw || typeof raw !== "object")
|
|
17346
|
+
return;
|
|
17347
|
+
for (const [id, text] of Object.entries(raw)) {
|
|
17348
|
+
if (typeof text === "string" && text)
|
|
17349
|
+
this.drafts[id] = text;
|
|
17350
|
+
}
|
|
17351
|
+
} catch (e) {
|
|
17352
|
+
log.warn("drafts", `skipped an unreadable ${d.file}`, e);
|
|
17353
|
+
}
|
|
17354
|
+
}
|
|
17355
|
+
all() {
|
|
17356
|
+
return { ...this.drafts };
|
|
17357
|
+
}
|
|
17358
|
+
has(boxId) {
|
|
17359
|
+
return !!this.drafts[boxId]?.trim();
|
|
17360
|
+
}
|
|
17361
|
+
set(boxId, text, clientId) {
|
|
17362
|
+
if ((this.drafts[boxId] ?? "") === text)
|
|
17363
|
+
return;
|
|
17364
|
+
if (text)
|
|
17365
|
+
this.drafts[boxId] = text;
|
|
17366
|
+
else
|
|
17367
|
+
delete this.drafts[boxId];
|
|
17368
|
+
this.scheduleSave();
|
|
17369
|
+
this.d.hub.emit("draftChanged", boxId, text, clientId);
|
|
17370
|
+
}
|
|
17371
|
+
drop(boxId) {
|
|
17372
|
+
this.set(boxId, "");
|
|
17373
|
+
}
|
|
17374
|
+
prune(known) {
|
|
17375
|
+
let changed = false;
|
|
17376
|
+
for (const id of Object.keys(this.drafts)) {
|
|
17377
|
+
if (known(id))
|
|
17378
|
+
continue;
|
|
17379
|
+
delete this.drafts[id];
|
|
17380
|
+
changed = true;
|
|
17381
|
+
}
|
|
17382
|
+
if (changed)
|
|
17383
|
+
this.scheduleSave();
|
|
17384
|
+
}
|
|
17385
|
+
flush() {
|
|
17386
|
+
if (!this.saveTimer)
|
|
17387
|
+
return;
|
|
17388
|
+
clearTimeout(this.saveTimer);
|
|
17389
|
+
this.saveTimer = null;
|
|
17390
|
+
this.write();
|
|
17391
|
+
}
|
|
17392
|
+
scheduleSave() {
|
|
17393
|
+
if (this.saveTimer)
|
|
17394
|
+
return;
|
|
17395
|
+
this.saveTimer = setTimeout(() => {
|
|
17396
|
+
this.saveTimer = null;
|
|
17397
|
+
this.write();
|
|
17398
|
+
}, this.d.saveDelayMs ?? SAVE_DELAY_MS);
|
|
17399
|
+
this.saveTimer.unref?.();
|
|
17400
|
+
}
|
|
17401
|
+
write() {
|
|
17402
|
+
const tmp = `${this.d.file}.tmp`;
|
|
17403
|
+
try {
|
|
17404
|
+
writeFileSync5(tmp, JSON.stringify(this.drafts));
|
|
17405
|
+
renameSync3(tmp, this.d.file);
|
|
17406
|
+
} catch (e) {
|
|
17407
|
+
log.error("drafts", "could not write the drafts", e);
|
|
17408
|
+
}
|
|
17409
|
+
}
|
|
17410
|
+
}
|
|
17411
|
+
|
|
16801
17412
|
// ../daemon/src/exec/service.ts
|
|
16802
17413
|
var OUTPUT_CAP = 200000;
|
|
16803
17414
|
var TIMEOUT_MS = 10 * 60000;
|
|
@@ -16841,6 +17452,7 @@ class ExecService {
|
|
|
16841
17452
|
}
|
|
16842
17453
|
const timer = setTimeout(() => this.kill(worktreeId, toolId), TIMEOUT_MS);
|
|
16843
17454
|
this.track(worktreeId, toolId, { proc, timer });
|
|
17455
|
+
this.deps.runtime.hold(worktreeId, `exec:${toolId}`);
|
|
16844
17456
|
return this.collect(worktreeId, toolId, proc, agent);
|
|
16845
17457
|
}
|
|
16846
17458
|
stop(worktreeId) {
|
|
@@ -16906,6 +17518,7 @@ class ExecService {
|
|
|
16906
17518
|
const r = this.untrack(worktreeId, toolId);
|
|
16907
17519
|
if (r)
|
|
16908
17520
|
clearTimeout(r.timer);
|
|
17521
|
+
this.deps.runtime.release(worktreeId, `exec:${toolId}`);
|
|
16909
17522
|
}
|
|
16910
17523
|
agent.note({ type: "tool-end", toolId, output: formatOutput(text, exit, truncated), isError: exit !== 0 });
|
|
16911
17524
|
return { exit, text };
|
|
@@ -16931,7 +17544,7 @@ ${body}
|
|
|
16931
17544
|
}
|
|
16932
17545
|
|
|
16933
17546
|
// ../daemon/src/files/service.ts
|
|
16934
|
-
import { spawn as
|
|
17547
|
+
import { spawn as spawn3 } from "child_process";
|
|
16935
17548
|
import { stat, unlink } from "fs/promises";
|
|
16936
17549
|
|
|
16937
17550
|
// ../daemon/src/git/lock.ts
|
|
@@ -17047,10 +17660,13 @@ async function committedFiles(worktreePath, defaultBr) {
|
|
|
17047
17660
|
const base = await git(worktreePath, "merge-base", "HEAD", defaultBr);
|
|
17048
17661
|
if (!base.ok || !base.out)
|
|
17049
17662
|
return [];
|
|
17050
|
-
|
|
17663
|
+
return filesBetween(worktreePath, base.out, "HEAD");
|
|
17664
|
+
}
|
|
17665
|
+
async function filesBetween(cwd, from, to) {
|
|
17666
|
+
const r = await git(cwd, "diff", "--name-status", from, to);
|
|
17051
17667
|
if (!r.ok || !r.out)
|
|
17052
17668
|
return [];
|
|
17053
|
-
const counts = await numstat(
|
|
17669
|
+
const counts = await numstat(cwd, from, to);
|
|
17054
17670
|
return r.out.split(`
|
|
17055
17671
|
`).filter(Boolean).map((line) => {
|
|
17056
17672
|
const [status, ...rest] = line.split("\t");
|
|
@@ -17080,6 +17696,9 @@ async function treeEmpty(worktreePath) {
|
|
|
17080
17696
|
const r = await git(worktreePath, "ls-tree", "--name-only", "HEAD");
|
|
17081
17697
|
return !r.ok || r.out === "";
|
|
17082
17698
|
}
|
|
17699
|
+
async function isTracked(worktreePath, rel) {
|
|
17700
|
+
return (await git(worktreePath, "ls-files", "--error-unmatch", "--", rel)).ok;
|
|
17701
|
+
}
|
|
17083
17702
|
async function behindUpstream(worktreePath) {
|
|
17084
17703
|
const r = await git(worktreePath, "rev-list", "--count", "HEAD..@{upstream}");
|
|
17085
17704
|
return r.ok ? Number(r.out) || 0 : null;
|
|
@@ -17100,10 +17719,15 @@ async function logCommits(worktreePath, defaultBr, limit = LOG_LIMIT) {
|
|
|
17100
17719
|
git(worktreePath, "log", `-n${limit}`, FORMAT),
|
|
17101
17720
|
aheadShas(worktreePath, defaultBr)
|
|
17102
17721
|
]);
|
|
17103
|
-
|
|
17104
|
-
|
|
17722
|
+
return r.ok && r.out ? parseLog(r.out, (sha) => ahead.has(sha)) : [];
|
|
17723
|
+
}
|
|
17724
|
+
async function logRange(cwd, from, to, limit = LOG_LIMIT) {
|
|
17725
|
+
const r = await git(cwd, "log", `-n${limit}`, FORMAT, `${from}..${to}`);
|
|
17726
|
+
return r.ok && r.out ? parseLog(r.out, () => true) : [];
|
|
17727
|
+
}
|
|
17728
|
+
function parseLog(out, ahead) {
|
|
17105
17729
|
const commits = [];
|
|
17106
|
-
for (const rec of
|
|
17730
|
+
for (const rec of out.split("\x1E")) {
|
|
17107
17731
|
const [sha, short, author, at, subject] = rec.trim().split("\x00");
|
|
17108
17732
|
if (!sha || !short || subject === undefined)
|
|
17109
17733
|
continue;
|
|
@@ -17113,7 +17737,7 @@ async function logCommits(worktreePath, defaultBr, limit = LOG_LIMIT) {
|
|
|
17113
17737
|
subject,
|
|
17114
17738
|
author: author ?? "",
|
|
17115
17739
|
at: Number(at) * 1000 || 0,
|
|
17116
|
-
ahead: ahead
|
|
17740
|
+
ahead: ahead(sha)
|
|
17117
17741
|
});
|
|
17118
17742
|
}
|
|
17119
17743
|
return commits;
|
|
@@ -17149,12 +17773,12 @@ async function fileAtCommit(worktreePath, sha, file) {
|
|
|
17149
17773
|
}
|
|
17150
17774
|
|
|
17151
17775
|
// ../daemon/src/worktrees/paths.ts
|
|
17152
|
-
import { resolve as
|
|
17776
|
+
import { resolve as resolve6 } from "path";
|
|
17153
17777
|
function resolveInside(worktreePath, rel, opts = {}) {
|
|
17154
17778
|
if (typeof rel !== "string" || rel.includes("\x00"))
|
|
17155
17779
|
throw new UserError("invalid path");
|
|
17156
|
-
const root = canonical(
|
|
17157
|
-
const target = canonical(
|
|
17780
|
+
const root = canonical(resolve6(worktreePath));
|
|
17781
|
+
const target = canonical(resolve6(worktreePath, rel));
|
|
17158
17782
|
if (target === root) {
|
|
17159
17783
|
if (opts.allowRoot)
|
|
17160
17784
|
return target;
|
|
@@ -17294,6 +17918,30 @@ function rankElementSources(el, rows) {
|
|
|
17294
17918
|
};
|
|
17295
17919
|
}
|
|
17296
17920
|
|
|
17921
|
+
// ../daemon/src/files/listing.ts
|
|
17922
|
+
var SUBMODULE_MODE = "160000";
|
|
17923
|
+
var entries = (out) => out.split("\x00").filter(Boolean);
|
|
17924
|
+
function parseListing(staged, untracked, deleted) {
|
|
17925
|
+
const gone = new Set(entries(deleted));
|
|
17926
|
+
const paths = new Set;
|
|
17927
|
+
const submodules = new Set;
|
|
17928
|
+
for (const line of entries(staged)) {
|
|
17929
|
+
const tab = line.indexOf("\t");
|
|
17930
|
+
if (tab === -1)
|
|
17931
|
+
continue;
|
|
17932
|
+
const path = line.slice(tab + 1);
|
|
17933
|
+
if (gone.has(path))
|
|
17934
|
+
continue;
|
|
17935
|
+
if (line.startsWith(SUBMODULE_MODE))
|
|
17936
|
+
submodules.add(path);
|
|
17937
|
+
else
|
|
17938
|
+
paths.add(path);
|
|
17939
|
+
}
|
|
17940
|
+
for (const path of entries(untracked))
|
|
17941
|
+
paths.add(path);
|
|
17942
|
+
return { paths: [...paths], submodules: [...submodules] };
|
|
17943
|
+
}
|
|
17944
|
+
|
|
17297
17945
|
// ../daemon/src/files/vite-offset.ts
|
|
17298
17946
|
async function viteLineOffset(worktreePath, path, target) {
|
|
17299
17947
|
if (!target)
|
|
@@ -17374,6 +18022,9 @@ function sides(before, after) {
|
|
|
17374
18022
|
}
|
|
17375
18023
|
return { before, after, binary: false, tooLarge: false };
|
|
17376
18024
|
}
|
|
18025
|
+
function keptRead(before, after) {
|
|
18026
|
+
return { ...sides(before, after), version: null, writable: false };
|
|
18027
|
+
}
|
|
17377
18028
|
|
|
17378
18029
|
class FileService {
|
|
17379
18030
|
state;
|
|
@@ -17452,9 +18103,13 @@ class FileService {
|
|
|
17452
18103
|
});
|
|
17453
18104
|
}
|
|
17454
18105
|
async list(worktreeId) {
|
|
17455
|
-
const
|
|
17456
|
-
|
|
17457
|
-
|
|
18106
|
+
const cwd = this.require(worktreeId).path;
|
|
18107
|
+
const [staged, untracked, deleted] = await Promise.all([
|
|
18108
|
+
git(cwd, "ls-files", "-z", "-s"),
|
|
18109
|
+
git(cwd, "ls-files", "-z", "-o", "--exclude-standard"),
|
|
18110
|
+
git(cwd, "ls-files", "-z", "-d")
|
|
18111
|
+
]);
|
|
18112
|
+
return parseListing(staged.out, untracked.out, deleted.out);
|
|
17458
18113
|
}
|
|
17459
18114
|
async search(worktreeId, query) {
|
|
17460
18115
|
const { path: cwd } = this.require(worktreeId);
|
|
@@ -17521,7 +18176,7 @@ class FileService {
|
|
|
17521
18176
|
revealPath(target) {
|
|
17522
18177
|
if (process.platform !== "darwin")
|
|
17523
18178
|
throw new UserError("reveal is only available on macOS");
|
|
17524
|
-
const child =
|
|
18179
|
+
const child = spawn3("open", ["-R", target], { stdio: "ignore" });
|
|
17525
18180
|
child.on("error", () => {});
|
|
17526
18181
|
child.unref();
|
|
17527
18182
|
}
|
|
@@ -17614,8 +18269,89 @@ function prNumberOf(url) {
|
|
|
17614
18269
|
return m ? Number(m[1]) : null;
|
|
17615
18270
|
}
|
|
17616
18271
|
|
|
18272
|
+
// ../daemon/src/runtime/setup.ts
|
|
18273
|
+
var SETUP_RING = 64 * 1024;
|
|
18274
|
+
var SETUP_COLS = 120;
|
|
18275
|
+
var SETUP_ROWS = 30;
|
|
18276
|
+
function runSetup(cmd, cwd, onLine, extra = {}) {
|
|
18277
|
+
return new Promise((resolve) => {
|
|
18278
|
+
const lines = new LineSplitter;
|
|
18279
|
+
try {
|
|
18280
|
+
new PtyStream({
|
|
18281
|
+
cwd,
|
|
18282
|
+
env: { ...envStrings2(process.env), TERM: "xterm-256color", COLORTERM: "truecolor", ...extra },
|
|
18283
|
+
cols: SETUP_COLS,
|
|
18284
|
+
rows: SETUP_ROWS,
|
|
18285
|
+
file: "sh",
|
|
18286
|
+
args: ["-c", cmd],
|
|
18287
|
+
ring: SETUP_RING
|
|
18288
|
+
}, (data) => {
|
|
18289
|
+
for (const line of lines.feed(data))
|
|
18290
|
+
onLine(line);
|
|
18291
|
+
}, resolve);
|
|
18292
|
+
} catch (e) {
|
|
18293
|
+
onLine(`could not run: ${e instanceof Error ? e.message : String(e)}`);
|
|
18294
|
+
resolve(1);
|
|
18295
|
+
}
|
|
18296
|
+
});
|
|
18297
|
+
}
|
|
18298
|
+
function envStrings2(base) {
|
|
18299
|
+
const env = {};
|
|
18300
|
+
for (const [k, v] of Object.entries(base))
|
|
18301
|
+
if (typeof v === "string")
|
|
18302
|
+
env[k] = v;
|
|
18303
|
+
return env;
|
|
18304
|
+
}
|
|
18305
|
+
|
|
18306
|
+
// ../daemon/src/repos/afterLand.ts
|
|
18307
|
+
class AfterLand {
|
|
18308
|
+
d;
|
|
18309
|
+
running = new Set;
|
|
18310
|
+
constructor(d) {
|
|
18311
|
+
this.d = d;
|
|
18312
|
+
}
|
|
18313
|
+
busy(repoId) {
|
|
18314
|
+
return this.running.has(repoId);
|
|
18315
|
+
}
|
|
18316
|
+
run(repoId) {
|
|
18317
|
+
const repo = this.d.state.repo(repoId);
|
|
18318
|
+
const commands = repo?.config.afterLand ?? [];
|
|
18319
|
+
if (!repo || commands.length === 0 || this.running.has(repoId))
|
|
18320
|
+
return;
|
|
18321
|
+
this.running.add(repoId);
|
|
18322
|
+
const done = this.exec(repo, commands).finally(() => this.running.delete(repoId));
|
|
18323
|
+
fireAndForget(repo.id, done, "afterLand");
|
|
18324
|
+
}
|
|
18325
|
+
async exec(repo, commands) {
|
|
18326
|
+
if (this.d.self.building(true))
|
|
18327
|
+
this.d.hub.emit("selfChanged");
|
|
18328
|
+
let failure;
|
|
18329
|
+
for (const command of commands) {
|
|
18330
|
+
this.tellMain(repo, `afterLand: ${command}`);
|
|
18331
|
+
let last = "";
|
|
18332
|
+
const code = await runSetup(command, repo.path, (line) => {
|
|
18333
|
+
last = line;
|
|
18334
|
+
this.tellMain(repo, line);
|
|
18335
|
+
});
|
|
18336
|
+
if (code === 0)
|
|
18337
|
+
continue;
|
|
18338
|
+
failure = last.trim() === "" ? `${command} (exit ${code})` : last.trim();
|
|
18339
|
+
this.tellMain(repo, `afterLand stopped: ${command} exited ${code}`);
|
|
18340
|
+
log.warn(repo.id, `afterLand stopped: ${command} exited ${code}`);
|
|
18341
|
+
break;
|
|
18342
|
+
}
|
|
18343
|
+
if (this.d.self.building(false, failure))
|
|
18344
|
+
this.d.hub.emit("selfChanged");
|
|
18345
|
+
}
|
|
18346
|
+
tellMain(repo, line) {
|
|
18347
|
+
const main = this.d.state.worktrees.find((w) => w.repoId === repo.id && w.kind === "main");
|
|
18348
|
+
if (main)
|
|
18349
|
+
this.d.hub.emit("log", main.id, "land", line);
|
|
18350
|
+
}
|
|
18351
|
+
}
|
|
18352
|
+
|
|
17617
18353
|
// ../daemon/src/repos/registry.ts
|
|
17618
|
-
import { existsSync as
|
|
18354
|
+
import { existsSync as existsSync17, mkdirSync as mkdirSync5, rmSync as rmSync2, writeFileSync as writeFileSync6 } from "fs";
|
|
17619
18355
|
import { homedir as homedir7 } from "os";
|
|
17620
18356
|
import { basename as basename3, dirname as dirname6, join as join14 } from "path";
|
|
17621
18357
|
|
|
@@ -17643,11 +18379,11 @@ function variantLens(index) {
|
|
|
17643
18379
|
}
|
|
17644
18380
|
|
|
17645
18381
|
// ../daemon/src/repos/browse.ts
|
|
17646
|
-
import { existsSync as
|
|
18382
|
+
import { existsSync as existsSync13, statSync as statSync3 } from "fs";
|
|
17647
18383
|
import { readdir } from "fs/promises";
|
|
17648
18384
|
import { homedir as homedir5 } from "os";
|
|
17649
18385
|
import { dirname as dirname4, join as join10 } from "path";
|
|
17650
|
-
var
|
|
18386
|
+
var MAX_ENTRIES = 40;
|
|
17651
18387
|
function expandTilde(raw) {
|
|
17652
18388
|
return raw === "~" || raw.startsWith("~/") ? join10(homedir5(), raw.slice(1)) : raw;
|
|
17653
18389
|
}
|
|
@@ -17660,7 +18396,7 @@ function holdsNothing(names) {
|
|
|
17660
18396
|
}
|
|
17661
18397
|
async function describeFolder(abs) {
|
|
17662
18398
|
const path = collapseTilde(abs);
|
|
17663
|
-
if (
|
|
18399
|
+
if (existsSync13(join10(abs, ".git")))
|
|
17664
18400
|
return { path, kind: "project" };
|
|
17665
18401
|
try {
|
|
17666
18402
|
return { path, kind: holdsNothing(await readdir(abs)) ? "empty" : "folder" };
|
|
@@ -17672,13 +18408,13 @@ var NOWHERE = { exists: false, isDir: false, isRepo: false, parentExists: false
|
|
|
17672
18408
|
function describe2(typed, parent) {
|
|
17673
18409
|
let isDir = false;
|
|
17674
18410
|
try {
|
|
17675
|
-
isDir =
|
|
18411
|
+
isDir = statSync3(typed).isDirectory();
|
|
17676
18412
|
} catch {}
|
|
17677
18413
|
return {
|
|
17678
|
-
exists:
|
|
18414
|
+
exists: existsSync13(typed),
|
|
17679
18415
|
isDir,
|
|
17680
|
-
isRepo: isDir &&
|
|
17681
|
-
parentExists:
|
|
18416
|
+
isRepo: isDir && existsSync13(join10(typed, ".git")),
|
|
18417
|
+
parentExists: existsSync13(parent)
|
|
17682
18418
|
};
|
|
17683
18419
|
}
|
|
17684
18420
|
async function browsePath(raw) {
|
|
@@ -17703,17 +18439,17 @@ async function browsePath(raw) {
|
|
|
17703
18439
|
if (!name.toLowerCase().startsWith(prefix))
|
|
17704
18440
|
continue;
|
|
17705
18441
|
const abs = join10(dir, name);
|
|
17706
|
-
entries.push({ path: collapseTilde(abs), name, isRepo:
|
|
18442
|
+
entries.push({ path: collapseTilde(abs), name, isRepo: existsSync13(join10(abs, ".git")) });
|
|
17707
18443
|
}
|
|
17708
18444
|
entries.sort((a, b) => a.isRepo !== b.isRepo ? a.isRepo ? -1 : 1 : a.name.localeCompare(b.name));
|
|
17709
|
-
return { entries: entries.slice(0,
|
|
18445
|
+
return { entries: entries.slice(0, MAX_ENTRIES), target };
|
|
17710
18446
|
}
|
|
17711
18447
|
|
|
17712
18448
|
// ../daemon/src/repos/config.ts
|
|
17713
|
-
import { closeSync, existsSync as
|
|
18449
|
+
import { closeSync, existsSync as existsSync14, openSync as openSync2, readdirSync as readdirSync3, readFileSync as readFileSync10, readSync } from "fs";
|
|
17714
18450
|
import { join as join11 } from "path";
|
|
17715
18451
|
var PLACES = [CONFIG_FILES.folder, CONFIG_FILES.root];
|
|
17716
|
-
var present = (repoPath, place) => [place.shared, place.local].filter((rel) =>
|
|
18452
|
+
var present = (repoPath, place) => [place.shared, place.local].filter((rel) => existsSync14(join11(repoPath, rel)));
|
|
17717
18453
|
var isObject2 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
17718
18454
|
function readConfigFile(repoPath) {
|
|
17719
18455
|
const found = PLACES.map((p) => present(repoPath, p)).filter((files) => files.length > 0);
|
|
@@ -17728,7 +18464,7 @@ function readConfigFile(repoPath) {
|
|
|
17728
18464
|
for (const rel of files) {
|
|
17729
18465
|
let raw;
|
|
17730
18466
|
try {
|
|
17731
|
-
raw = JSON.parse(
|
|
18467
|
+
raw = JSON.parse(readFileSync10(join11(repoPath, rel), "utf8"));
|
|
17732
18468
|
} catch (e) {
|
|
17733
18469
|
return { ok: false, reason: `${rel} is not valid JSON: ${e instanceof Error ? e.message : String(e)}` };
|
|
17734
18470
|
}
|
|
@@ -17786,11 +18522,11 @@ function configTarget(repoPath, made) {
|
|
|
17786
18522
|
function configBody(repoPath, rel, config) {
|
|
17787
18523
|
const place = PLACES.find((p) => p.local === rel);
|
|
17788
18524
|
const abs = place && join11(repoPath, place.shared);
|
|
17789
|
-
if (!abs || !
|
|
18525
|
+
if (!abs || !existsSync14(abs))
|
|
17790
18526
|
return config;
|
|
17791
18527
|
let shared;
|
|
17792
18528
|
try {
|
|
17793
|
-
shared = JSON.parse(
|
|
18529
|
+
shared = JSON.parse(readFileSync10(abs, "utf8"));
|
|
17794
18530
|
} catch {
|
|
17795
18531
|
return config;
|
|
17796
18532
|
}
|
|
@@ -17803,8 +18539,8 @@ function detectConfig(repoPath) {
|
|
|
17803
18539
|
if (explicit)
|
|
17804
18540
|
log.warn("config", `${repoPath}: ${explicit.reason}; detecting instead`);
|
|
17805
18541
|
const pkgPath = join11(repoPath, "package.json");
|
|
17806
|
-
if (
|
|
17807
|
-
const pkg = JSON.parse(
|
|
18542
|
+
if (existsSync14(pkgPath)) {
|
|
18543
|
+
const pkg = JSON.parse(readFileSync10(pkgPath, "utf8"));
|
|
17808
18544
|
const scripts = pkg.scripts ?? {};
|
|
17809
18545
|
const runner = detectRunner(repoPath, pkg.packageManager);
|
|
17810
18546
|
const pages = detectPages(repoPath, scripts, runner);
|
|
@@ -17825,7 +18561,7 @@ function detectConfig(repoPath) {
|
|
|
17825
18561
|
};
|
|
17826
18562
|
}
|
|
17827
18563
|
}
|
|
17828
|
-
if (
|
|
18564
|
+
if (existsSync14(join11(repoPath, "start.sh"))) {
|
|
17829
18565
|
return { config: { run: { app: "./start.sh" } }, needsSetup: true, from: "start.sh" };
|
|
17830
18566
|
}
|
|
17831
18567
|
const assumed = assumeNothingToRun(repoPath);
|
|
@@ -17868,19 +18604,19 @@ function assumeNothingToRun(repoPath) {
|
|
|
17868
18604
|
return marker && !servesPage(repoPath) ? marker : undefined;
|
|
17869
18605
|
}
|
|
17870
18606
|
function markerIn(repoPath) {
|
|
17871
|
-
const named = NO_PAGE_MARKERS.find((f) =>
|
|
18607
|
+
const named = NO_PAGE_MARKERS.find((f) => existsSync14(join11(repoPath, f)));
|
|
17872
18608
|
if (named)
|
|
17873
18609
|
return named;
|
|
17874
18610
|
let entries;
|
|
17875
18611
|
try {
|
|
17876
|
-
entries =
|
|
18612
|
+
entries = readdirSync3(repoPath);
|
|
17877
18613
|
} catch {
|
|
17878
18614
|
return;
|
|
17879
18615
|
}
|
|
17880
18616
|
return entries.find((f) => NO_PAGE_EXTENSIONS.some((ext) => f.endsWith(ext)));
|
|
17881
18617
|
}
|
|
17882
18618
|
function servesPage(repoPath) {
|
|
17883
|
-
if (WEB_FILES.some((f) =>
|
|
18619
|
+
if (WEB_FILES.some((f) => existsSync14(join11(repoPath, f))))
|
|
17884
18620
|
return true;
|
|
17885
18621
|
if (/^web:/m.test(head(join11(repoPath, "Procfile"))))
|
|
17886
18622
|
return true;
|
|
@@ -17895,7 +18631,7 @@ function servesPage(repoPath) {
|
|
|
17895
18631
|
function frontEndBeside(repoPath) {
|
|
17896
18632
|
let dirs;
|
|
17897
18633
|
try {
|
|
17898
|
-
dirs =
|
|
18634
|
+
dirs = readdirSync3(repoPath, { withFileTypes: true });
|
|
17899
18635
|
} catch {
|
|
17900
18636
|
return false;
|
|
17901
18637
|
}
|
|
@@ -17917,10 +18653,10 @@ function mentions(text, name) {
|
|
|
17917
18653
|
return new RegExp(`(?<![\\w-])${escaped}(?![\\w-])`, "i").test(text);
|
|
17918
18654
|
}
|
|
17919
18655
|
function head(path) {
|
|
17920
|
-
if (!
|
|
18656
|
+
if (!existsSync14(path))
|
|
17921
18657
|
return "";
|
|
17922
18658
|
try {
|
|
17923
|
-
const fd =
|
|
18659
|
+
const fd = openSync2(path, "r");
|
|
17924
18660
|
try {
|
|
17925
18661
|
const buf = Buffer.alloc(READ_CAP);
|
|
17926
18662
|
return buf.toString("utf8", 0, readSync(fd, buf, 0, READ_CAP, 0));
|
|
@@ -17945,31 +18681,31 @@ function procCommand(runner, script, body) {
|
|
|
17945
18681
|
return `${base}${runner === "npm" ? " --" : ""} ${flag}`;
|
|
17946
18682
|
}
|
|
17947
18683
|
function detectPages(repoPath, scripts, runner) {
|
|
17948
|
-
if (!scripts.build || !
|
|
18684
|
+
if (!scripts.build || !existsSync14(join11(repoPath, "functions")))
|
|
17949
18685
|
return null;
|
|
17950
|
-
const cfg = ["wrangler.jsonc", "wrangler.json", "wrangler.toml"].map((f) => join11(repoPath, f)).find((f) =>
|
|
18686
|
+
const cfg = ["wrangler.jsonc", "wrangler.json", "wrangler.toml"].map((f) => join11(repoPath, f)).find((f) => existsSync14(f));
|
|
17951
18687
|
if (!cfg)
|
|
17952
18688
|
return null;
|
|
17953
|
-
const out =
|
|
18689
|
+
const out = readFileSync10(cfg, "utf8").match(/pages_build_output_dir"?\s*[:=]\s*"([^"]+)"/)?.[1];
|
|
17954
18690
|
if (!out)
|
|
17955
18691
|
return null;
|
|
17956
18692
|
return { web: `${runner} run build && npx wrangler pages dev ${out} --ip 127.0.0.1 --port $PORT` };
|
|
17957
18693
|
}
|
|
17958
18694
|
function detectRunner(repoPath, packageManager) {
|
|
17959
|
-
if (
|
|
18695
|
+
if (existsSync14(join11(repoPath, "bun.lock")) || existsSync14(join11(repoPath, "bun.lockb")))
|
|
17960
18696
|
return "bun";
|
|
17961
|
-
if (
|
|
18697
|
+
if (existsSync14(join11(repoPath, "pnpm-lock.yaml")))
|
|
17962
18698
|
return "pnpm";
|
|
17963
|
-
if (
|
|
18699
|
+
if (existsSync14(join11(repoPath, "yarn.lock")) || typeof packageManager === "string" && packageManager.startsWith("yarn"))
|
|
17964
18700
|
return "yarn";
|
|
17965
18701
|
return "npm";
|
|
17966
18702
|
}
|
|
17967
18703
|
|
|
17968
18704
|
// ../daemon/src/repos/create.ts
|
|
17969
|
-
import { existsSync as
|
|
18705
|
+
import { existsSync as existsSync15, readdirSync as readdirSync4, realpathSync as realpathSync2, statSync as statSync4 } from "fs";
|
|
17970
18706
|
import { appendFile as appendFile2, mkdir as mkdir2, rm } from "fs/promises";
|
|
17971
18707
|
import { homedir as homedir6 } from "os";
|
|
17972
|
-
import { dirname as dirname5, join as join12, resolve as
|
|
18708
|
+
import { dirname as dirname5, join as join12, resolve as resolve7 } from "path";
|
|
17973
18709
|
function isInside(dir, parent) {
|
|
17974
18710
|
const p = parent.replace(/\/+$/, "");
|
|
17975
18711
|
return dir === p || dir.startsWith(`${p}/`);
|
|
@@ -17981,13 +18717,13 @@ function planProject(opts, home = homedir6()) {
|
|
|
17981
18717
|
const first = firstProjectsFolder(opts.parent, home);
|
|
17982
18718
|
const parent = first ?? resolveParent(opts.parent);
|
|
17983
18719
|
const dir = join12(parent, opts.name.trim());
|
|
17984
|
-
if (
|
|
18720
|
+
if (existsSync15(dir))
|
|
17985
18721
|
throw new UserError(`${dir} already exists`);
|
|
17986
18722
|
return { parent, dir, makeParent: first !== null };
|
|
17987
18723
|
}
|
|
17988
18724
|
function firstProjectsFolder(raw, home) {
|
|
17989
18725
|
const typed = expandTilde(raw.trim()).replace(/\/+$/, "");
|
|
17990
|
-
if (typed !== join12(home, PROJECTS_FOLDER) ||
|
|
18726
|
+
if (typed !== join12(home, PROJECTS_FOLDER) || existsSync15(typed) || !existsSync15(home))
|
|
17991
18727
|
return null;
|
|
17992
18728
|
return join12(realpathSync2(home), PROJECTS_FOLDER);
|
|
17993
18729
|
}
|
|
@@ -17997,12 +18733,12 @@ function planInPlace(opts) {
|
|
|
17997
18733
|
throw new UserError(`${opts.name} is not a folder name`);
|
|
17998
18734
|
}
|
|
17999
18735
|
const typed = join12(resolveParent(opts.parent), name);
|
|
18000
|
-
if (!
|
|
18736
|
+
if (!existsSync15(typed) || !statSync4(typed).isDirectory())
|
|
18001
18737
|
throw new UserError(`${typed} is not a folder`);
|
|
18002
18738
|
const dir = realpathSync2(typed);
|
|
18003
|
-
if (
|
|
18739
|
+
if (existsSync15(join12(dir, ".git")))
|
|
18004
18740
|
throw new UserError(`${dir} is already a project; open it instead`);
|
|
18005
|
-
if (!holdsNothing(
|
|
18741
|
+
if (!holdsNothing(readdirSync4(dir))) {
|
|
18006
18742
|
throw new UserError(`${dir} is not empty; only an empty folder can become a project where it is`);
|
|
18007
18743
|
}
|
|
18008
18744
|
return { parent: dirname5(dir), dir, makeParent: false };
|
|
@@ -18011,9 +18747,9 @@ function resolveParent(raw) {
|
|
|
18011
18747
|
const typed = expandTilde(raw.trim());
|
|
18012
18748
|
if (!typed.startsWith("/"))
|
|
18013
18749
|
throw new UserError(`${raw} is not a folder path`);
|
|
18014
|
-
if (!
|
|
18750
|
+
if (!existsSync15(typed))
|
|
18015
18751
|
throw new UserError(`${raw} does not exist; make it first`);
|
|
18016
|
-
if (!
|
|
18752
|
+
if (!statSync4(typed).isDirectory())
|
|
18017
18753
|
throw new UserError(`${raw} is not a folder`);
|
|
18018
18754
|
return realpathSync2(typed);
|
|
18019
18755
|
}
|
|
@@ -18051,7 +18787,7 @@ async function initWithEmptyCommit(dir) {
|
|
|
18051
18787
|
const exclude = await git(dir, "rev-parse", "--git-path", "info/exclude");
|
|
18052
18788
|
if (!exclude.ok)
|
|
18053
18789
|
throw new UserError(`git rev-parse failed: ${exclude.err}`);
|
|
18054
|
-
const excludePath =
|
|
18790
|
+
const excludePath = resolve7(dir, exclude.out);
|
|
18055
18791
|
await mkdir2(dirname5(excludePath), { recursive: true });
|
|
18056
18792
|
await appendFile2(excludePath, `
|
|
18057
18793
|
${LOCAL_EXCLUDES.join(`
|
|
@@ -18127,7 +18863,7 @@ async function ok(p, what) {
|
|
|
18127
18863
|
}
|
|
18128
18864
|
|
|
18129
18865
|
// ../daemon/src/repos/watcher.ts
|
|
18130
|
-
import { existsSync as
|
|
18866
|
+
import { existsSync as existsSync16, watch } from "fs";
|
|
18131
18867
|
import { basename as basename2, join as join13 } from "path";
|
|
18132
18868
|
function watchDefaultBranch(repoPath, branch, onMove) {
|
|
18133
18869
|
let last = null;
|
|
@@ -18239,7 +18975,7 @@ function watchConfigFile(repoPath, onChange) {
|
|
|
18239
18975
|
const armInner = () => {
|
|
18240
18976
|
inner?.close();
|
|
18241
18977
|
inner = null;
|
|
18242
|
-
if (!
|
|
18978
|
+
if (!existsSync16(dir))
|
|
18243
18979
|
return;
|
|
18244
18980
|
try {
|
|
18245
18981
|
inner = watch(dir, (_event, filename) => {
|
|
@@ -18293,7 +19029,7 @@ class RepoRegistry {
|
|
|
18293
19029
|
}
|
|
18294
19030
|
async boot() {
|
|
18295
19031
|
const { state } = this.d;
|
|
18296
|
-
const gone = state.worktrees.filter((wt) => !
|
|
19032
|
+
const gone = state.worktrees.filter((wt) => !existsSync17(wt.path) || !state.repos.some((r) => r.id === wt.repoId));
|
|
18297
19033
|
for (const wt of gone)
|
|
18298
19034
|
await this.d.worktrees.forgetGone(wt);
|
|
18299
19035
|
for (const repo of state.repos) {
|
|
@@ -18313,25 +19049,15 @@ class RepoRegistry {
|
|
|
18313
19049
|
log.info("daemon", `${cold} worktrees across ${state.repos.length} repos start when opened`);
|
|
18314
19050
|
}
|
|
18315
19051
|
warm(repoId) {
|
|
18316
|
-
if (this.warmed.has(repoId))
|
|
19052
|
+
if (this.warmed.has(repoId) || !this.d.state.repo(repoId))
|
|
18317
19053
|
return;
|
|
18318
19054
|
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
19055
|
this.d.worktrees.spare.warm(repoId);
|
|
18326
19056
|
}
|
|
18327
19057
|
touch(worktreeId) {
|
|
18328
|
-
|
|
18329
|
-
if (!this.warmed.has(repo.id)) {
|
|
18330
|
-
this.warm(repo.id);
|
|
19058
|
+
if (!this.d.state.worktree(worktreeId))
|
|
18331
19059
|
return;
|
|
18332
|
-
|
|
18333
|
-
if (!this.d.runtime.get(wt.id)?.procs)
|
|
18334
|
-
fireAndForget(wt.id, this.d.runtime.start(wt, repo), "start on open");
|
|
19060
|
+
fireAndForget(worktreeId, this.d.runtime.wake(worktreeId), "start on open");
|
|
18335
19061
|
}
|
|
18336
19062
|
async create(opts) {
|
|
18337
19063
|
const inPlace = opts.mode === "init";
|
|
@@ -18370,7 +19096,7 @@ class RepoRegistry {
|
|
|
18370
19096
|
return `${repo.name} is set up now`;
|
|
18371
19097
|
if (main.promptedAt || main.lastTurn)
|
|
18372
19098
|
return `${repo.name} has a chat now`;
|
|
18373
|
-
if (
|
|
19099
|
+
if (existsSync17(join14(repo.path, ".claude")))
|
|
18374
19100
|
return `an agent has already run in ${repo.name}`;
|
|
18375
19101
|
if ((await statusFiles(repo.path)).length > 0 || !await treeEmpty(repo.path)) {
|
|
18376
19102
|
return `${repo.name} has files in it now`;
|
|
@@ -18447,7 +19173,7 @@ class RepoRegistry {
|
|
|
18447
19173
|
}
|
|
18448
19174
|
async register(rawPath, made) {
|
|
18449
19175
|
const path = expandTilde(rawPath);
|
|
18450
|
-
if (!
|
|
19176
|
+
if (!existsSync17(path))
|
|
18451
19177
|
throw new UserError(`${path} does not exist`);
|
|
18452
19178
|
if (!await isGitRepo(path))
|
|
18453
19179
|
throw new UserError(`${path} is not a git repository`);
|
|
@@ -18497,28 +19223,39 @@ class RepoRegistry {
|
|
|
18497
19223
|
const mine = this.d.state.worktrees.filter((w) => w.repoId === repoId);
|
|
18498
19224
|
const tasks = mine.filter((w) => w.kind !== "main" && w.kind !== "spare");
|
|
18499
19225
|
if (tasks.length > 0) {
|
|
18500
|
-
throw new UserError(`${repo.name} still has ${tasks.length} worktree${tasks.length === 1 ? "" : "s"};
|
|
19226
|
+
throw new UserError(`${repo.name} still has ${tasks.length} worktree${tasks.length === 1 ? "" : "s"}; archive them first`);
|
|
18501
19227
|
}
|
|
18502
19228
|
this.stopWatcher(repoId);
|
|
18503
19229
|
for (const wt of mine.filter((w) => w.kind === "spare"))
|
|
18504
|
-
await this.d.worktrees.
|
|
19230
|
+
await this.d.worktrees.discardWorktree(wt.id);
|
|
18505
19231
|
for (const wt of mine.filter((w) => w.kind === "main")) {
|
|
18506
19232
|
await this.d.runtime.stop(wt.id);
|
|
18507
19233
|
this.d.state.removeWorktree(wt.id);
|
|
18508
19234
|
this.d.worktrees.deleteChat(wt.id);
|
|
18509
19235
|
releasePort(wt.proxyPort);
|
|
18510
19236
|
}
|
|
19237
|
+
this.d.drafts?.drop(draftKey(repoId));
|
|
18511
19238
|
this.d.state.removeRepo(repoId);
|
|
18512
19239
|
this.d.hub.emit("reposChanged");
|
|
18513
19240
|
this.d.hub.emit("worktreesChanged");
|
|
18514
19241
|
}
|
|
18515
|
-
async confirmConfig(repoId, config) {
|
|
19242
|
+
async confirmConfig(repoId, config, kind) {
|
|
18516
19243
|
const repo = this.d.state.requireRepo(repoId);
|
|
18517
19244
|
const current = readConfigFile(repo.path);
|
|
18518
19245
|
if (current && !current.ok && current.conflict)
|
|
18519
19246
|
throw new UserError(current.reason);
|
|
18520
|
-
|
|
18521
|
-
const rel =
|
|
19247
|
+
const was = configTarget(repo.path, !!repo.made);
|
|
19248
|
+
const rel = configSibling(was, kind);
|
|
19249
|
+
const moved = rel !== was && existsSync17(join14(repo.path, was));
|
|
19250
|
+
if (moved && !(kind === "local" && await isTracked(repo.path, was))) {
|
|
19251
|
+
try {
|
|
19252
|
+
rmSync2(join14(repo.path, was));
|
|
19253
|
+
} catch (e) {
|
|
19254
|
+
log.warn(repoId, `could not remove ${was}`, e);
|
|
19255
|
+
}
|
|
19256
|
+
if (isLocalConfigFile(was))
|
|
19257
|
+
await unexcludeFromGit(repo.path, was);
|
|
19258
|
+
}
|
|
18522
19259
|
if (isLocalConfigFile(rel))
|
|
18523
19260
|
await excludeFromGit(repo.path, rel);
|
|
18524
19261
|
const file = join14(repo.path, rel);
|
|
@@ -18529,6 +19266,7 @@ class RepoRegistry {
|
|
|
18529
19266
|
} catch (e) {
|
|
18530
19267
|
log.warn(repoId, `could not write ${rel}`, e);
|
|
18531
19268
|
}
|
|
19269
|
+
this.placeConfig(repo);
|
|
18532
19270
|
const back = readConfigFile(repo.path);
|
|
18533
19271
|
repo.config = back?.ok ? back.config : config;
|
|
18534
19272
|
repo.needsSetup = false;
|
|
@@ -18598,7 +19336,7 @@ class RepoRegistry {
|
|
|
18598
19336
|
}
|
|
18599
19337
|
placeConfig(repo) {
|
|
18600
19338
|
const rel = configTarget(repo.path, !!repo.made);
|
|
18601
|
-
if (isLocalConfigFile(rel) &&
|
|
19339
|
+
if (isLocalConfigFile(rel) && existsSync17(join14(repo.path, rel))) {
|
|
18602
19340
|
fireAndForget(repo.id, excludeFromGit(repo.path, rel), "exclude local settings");
|
|
18603
19341
|
}
|
|
18604
19342
|
if (repo.configFile === rel)
|
|
@@ -18637,6 +19375,10 @@ class RepoRegistry {
|
|
|
18637
19375
|
if (main)
|
|
18638
19376
|
this.d.hub.emit("log", main.id, "config", line);
|
|
18639
19377
|
}
|
|
19378
|
+
async checkSelf(repo) {
|
|
19379
|
+
if (await this.d.self.check(repo))
|
|
19380
|
+
this.d.hub.emit("selfChanged");
|
|
19381
|
+
}
|
|
18640
19382
|
startWatcher(repo) {
|
|
18641
19383
|
if (this.watchers.has(repo.id))
|
|
18642
19384
|
return;
|
|
@@ -18644,6 +19386,7 @@ class RepoRegistry {
|
|
|
18644
19386
|
this.d.worktrees.invalidateCounts();
|
|
18645
19387
|
this.d.hub.emit("repoTick", repo.id);
|
|
18646
19388
|
fireAndForget(repo.id, this.d.worktrees.spare.refresh(repo.id), "spare refresh");
|
|
19389
|
+
fireAndForget(repo.id, this.checkSelf(repo).then(() => this.d.afterLand.run(repo.id)), "self check");
|
|
18647
19390
|
});
|
|
18648
19391
|
const stopCfg = watchConfigFile(repo.path, () => this.reloadConfig(repo.id));
|
|
18649
19392
|
const stopWt = watchWorktreeDir(repo.path, () => {
|
|
@@ -18930,7 +19673,7 @@ function retitle(pages, key, title) {
|
|
|
18930
19673
|
function rank(pages, now, n = KEEP) {
|
|
18931
19674
|
return Object.keys(pages).sort(byStanding(pages, now)).slice(0, n);
|
|
18932
19675
|
}
|
|
18933
|
-
function
|
|
19676
|
+
function entries2(pages, now) {
|
|
18934
19677
|
return rank(pages, now).map((path) => {
|
|
18935
19678
|
const v = pages[path];
|
|
18936
19679
|
return { path, score: rounded(decayed(v, now)), last: v.last, ...v.title ? { title: v.title } : {} };
|
|
@@ -19625,7 +20368,7 @@ function unseenOf(candidates, seen, hashes) {
|
|
|
19625
20368
|
}
|
|
19626
20369
|
|
|
19627
20370
|
// ../daemon/src/routes/service.ts
|
|
19628
|
-
var
|
|
20371
|
+
var SAVE_DELAY_MS2 = 5000;
|
|
19629
20372
|
var MAX_MANIFESTS = 50;
|
|
19630
20373
|
var SCAN_CACHE = 20;
|
|
19631
20374
|
var MAX_HASHED = 64;
|
|
@@ -19700,7 +20443,7 @@ class RouteService {
|
|
|
19700
20443
|
}
|
|
19701
20444
|
history(repoId) {
|
|
19702
20445
|
const pages = this.d.state.visitsOf(repoId);
|
|
19703
|
-
return pages ?
|
|
20446
|
+
return pages ? entries2(pages, this.now()) : [];
|
|
19704
20447
|
}
|
|
19705
20448
|
historyAll() {
|
|
19706
20449
|
const out = {};
|
|
@@ -19925,7 +20668,7 @@ class RouteService {
|
|
|
19925
20668
|
} catch (e) {
|
|
19926
20669
|
log.error("routes", "could not write the page lists", e);
|
|
19927
20670
|
}
|
|
19928
|
-
}, this.d.saveDelayMs ??
|
|
20671
|
+
}, this.d.saveDelayMs ?? SAVE_DELAY_MS2);
|
|
19929
20672
|
this.saveTimer.unref();
|
|
19930
20673
|
}
|
|
19931
20674
|
repoOf(worktreeId) {
|
|
@@ -19937,7 +20680,7 @@ class RouteService {
|
|
|
19937
20680
|
}
|
|
19938
20681
|
|
|
19939
20682
|
// ../daemon/src/runtime/bridge-script.ts
|
|
19940
|
-
import { existsSync as
|
|
20683
|
+
import { existsSync as existsSync18, readFileSync as readFileSync11, statSync as statSync5 } from "fs";
|
|
19941
20684
|
var LEARNED_MAX = 8;
|
|
19942
20685
|
|
|
19943
20686
|
class BridgeScript {
|
|
@@ -19967,16 +20710,16 @@ class BridgeScript {
|
|
|
19967
20710
|
this.cache = null;
|
|
19968
20711
|
}
|
|
19969
20712
|
get() {
|
|
19970
|
-
if (!
|
|
20713
|
+
if (!existsSync18(this.path))
|
|
19971
20714
|
return "// toyon bridge not built";
|
|
19972
|
-
const s =
|
|
20715
|
+
const s = statSync5(this.path);
|
|
19973
20716
|
const stamp = `${s.mtimeMs}:${s.size}`;
|
|
19974
20717
|
if (this.cache && stamp === this.stamp)
|
|
19975
20718
|
return this.cache;
|
|
19976
20719
|
this.stamp = stamp;
|
|
19977
20720
|
const origins = [...this.origins, ...this.learned];
|
|
19978
20721
|
this.cache = `window.__toyonShellOrigins=${JSON.stringify(origins)};
|
|
19979
|
-
${
|
|
20722
|
+
${readFileSync11(this.path, "utf8")}`;
|
|
19980
20723
|
return this.cache;
|
|
19981
20724
|
}
|
|
19982
20725
|
}
|
|
@@ -19993,62 +20736,394 @@ function servedLocally(origin) {
|
|
|
19993
20736
|
return h === "127.0.0.1" || h === "localhost" || h === "::1" || h === "[::1]" || h.endsWith(".localhost");
|
|
19994
20737
|
}
|
|
19995
20738
|
|
|
19996
|
-
// ../daemon/src/
|
|
19997
|
-
|
|
19998
|
-
|
|
19999
|
-
|
|
20000
|
-
|
|
20001
|
-
|
|
20002
|
-
|
|
20003
|
-
|
|
20004
|
-
|
|
20005
|
-
|
|
20006
|
-
|
|
20007
|
-
|
|
20008
|
-
|
|
20009
|
-
|
|
20010
|
-
if (
|
|
20011
|
-
return
|
|
20012
|
-
|
|
20013
|
-
|
|
20014
|
-
|
|
20015
|
-
|
|
20016
|
-
];
|
|
20017
|
-
}
|
|
20018
|
-
if (spec.id === "codex")
|
|
20019
|
-
return [file("user-config", "config", join16(home, ".codex", "config.toml"))];
|
|
20020
|
-
return [];
|
|
20739
|
+
// ../daemon/src/runtime/memory.ts
|
|
20740
|
+
var MAC_PRESSURE_CRITICAL = 4;
|
|
20741
|
+
var AVAILABLE_FLOOR = 0.15;
|
|
20742
|
+
var unsupportedLogged = false;
|
|
20743
|
+
async function memoryTight() {
|
|
20744
|
+
const signal = process.platform === "darwin" ? await darwin() : process.platform === "linux" ? await linux() : null;
|
|
20745
|
+
if (signal === null && !unsupportedLogged) {
|
|
20746
|
+
unsupportedLogged = true;
|
|
20747
|
+
log.warn("memory", `no memory pressure reading on ${process.platform}; nothing sleeps for memory`);
|
|
20748
|
+
}
|
|
20749
|
+
return signal;
|
|
20750
|
+
}
|
|
20751
|
+
async function darwin() {
|
|
20752
|
+
const r = await run("sysctl", ["-n", "kern.memorystatus_vm_pressure_level", "kern.memorystatus_level"], "/");
|
|
20753
|
+
if (!r.ok)
|
|
20754
|
+
return null;
|
|
20755
|
+
const [level, available] = r.out.split(/\s+/).map(Number);
|
|
20756
|
+
if (level === undefined || available === undefined || Number.isNaN(level) || Number.isNaN(available))
|
|
20757
|
+
return null;
|
|
20758
|
+
return darwinSignal(level, available);
|
|
20021
20759
|
}
|
|
20022
|
-
function
|
|
20023
|
-
const
|
|
20024
|
-
|
|
20025
|
-
|
|
20026
|
-
|
|
20027
|
-
|
|
20028
|
-
if (repoPath) {
|
|
20029
|
-
const projects = user?.projects;
|
|
20030
|
-
const mine = projects?.[repoPath];
|
|
20031
|
-
servers.push(...serversOf(mine?.mcpServers, "local"));
|
|
20032
|
-
servers.push(...serversOf(readJson(join16(repoPath, ".mcp.json"))?.mcpServers, "project"));
|
|
20033
|
-
}
|
|
20034
|
-
} else if (spec.id === "codex") {
|
|
20035
|
-
servers.push(...codexServers(join16(home, ".codex", "config.toml")));
|
|
20036
|
-
}
|
|
20037
|
-
return { agent: spec.id, files, servers };
|
|
20760
|
+
function darwinSignal(level, available) {
|
|
20761
|
+
const pressure = level === 1 ? "normal" : level === 2 ? "warn" : level === MAC_PRESSURE_CRITICAL ? "critical" : `level ${level}`;
|
|
20762
|
+
return {
|
|
20763
|
+
tight: level === MAC_PRESSURE_CRITICAL || available < AVAILABLE_FLOOR * 100,
|
|
20764
|
+
why: `memory pressure ${pressure} with ${available}% available`
|
|
20765
|
+
};
|
|
20038
20766
|
}
|
|
20039
|
-
function
|
|
20040
|
-
|
|
20041
|
-
return null;
|
|
20767
|
+
async function linux() {
|
|
20768
|
+
let text;
|
|
20042
20769
|
try {
|
|
20043
|
-
|
|
20044
|
-
|
|
20045
|
-
} catch (e) {
|
|
20046
|
-
log.warn("agent-config", `${path} is not valid JSON`, e);
|
|
20770
|
+
text = await Bun.file("/proc/meminfo").text();
|
|
20771
|
+
} catch {
|
|
20047
20772
|
return null;
|
|
20048
20773
|
}
|
|
20049
|
-
|
|
20050
|
-
|
|
20051
|
-
|
|
20774
|
+
const field = (name) => {
|
|
20775
|
+
const m = text.match(new RegExp(`^${name}:\\s+(\\d+)`, "m"));
|
|
20776
|
+
return m ? Number(m[1]) : null;
|
|
20777
|
+
};
|
|
20778
|
+
const total = field("MemTotal");
|
|
20779
|
+
const available = field("MemAvailable");
|
|
20780
|
+
if (!total || available === null)
|
|
20781
|
+
return null;
|
|
20782
|
+
const percent = Math.round(available / total * 100);
|
|
20783
|
+
return { tight: available / total < AVAILABLE_FLOOR, why: `${percent}% of memory available` };
|
|
20784
|
+
}
|
|
20785
|
+
async function sampleCosts(pgids) {
|
|
20786
|
+
const costs = new Map;
|
|
20787
|
+
if (pgids.length === 0)
|
|
20788
|
+
return costs;
|
|
20789
|
+
const wanted = new Set(pgids);
|
|
20790
|
+
const r = await run("ps", ["-axo", "pgid=,rss="], "/");
|
|
20791
|
+
if (!r.ok)
|
|
20792
|
+
return costs;
|
|
20793
|
+
for (const line of r.out.split(`
|
|
20794
|
+
`)) {
|
|
20795
|
+
const [pgid, rss] = line.trim().split(/\s+/).map(Number);
|
|
20796
|
+
if (pgid === undefined || rss === undefined || !wanted.has(pgid))
|
|
20797
|
+
continue;
|
|
20798
|
+
costs.set(pgid, (costs.get(pgid) ?? 0) + rss);
|
|
20799
|
+
}
|
|
20800
|
+
return costs;
|
|
20801
|
+
}
|
|
20802
|
+
|
|
20803
|
+
// ../daemon/src/runtime/idle.ts
|
|
20804
|
+
var LOCAL_SLEEP_MS = 2 * 60 * 60000;
|
|
20805
|
+
var CLOUD_SLEEP_MS = 5 * 60000;
|
|
20806
|
+
function sleepMsFrom(raw, inCloud) {
|
|
20807
|
+
if (raw === "off")
|
|
20808
|
+
return null;
|
|
20809
|
+
const n = Number(raw);
|
|
20810
|
+
if (raw && Number.isFinite(n) && n > 0)
|
|
20811
|
+
return n;
|
|
20812
|
+
return inCloud ? CLOUD_SLEEP_MS : LOCAL_SLEEP_MS;
|
|
20813
|
+
}
|
|
20814
|
+
var DEFAULT_SLEEP_MS = sleepMsFrom(process.env.TOYON_PROC_SLEEP_MS, cloud.enabled);
|
|
20815
|
+
var PRESSURE_EVERY_MS = 15000;
|
|
20816
|
+
var COSTS_EVERY_MS = 60000;
|
|
20817
|
+
var STAGGER_MS = 3000;
|
|
20818
|
+
|
|
20819
|
+
class IdlePolicy {
|
|
20820
|
+
d;
|
|
20821
|
+
entries = new Map;
|
|
20822
|
+
viewing = new Map;
|
|
20823
|
+
queue = [];
|
|
20824
|
+
draining = false;
|
|
20825
|
+
sleepMs;
|
|
20826
|
+
now;
|
|
20827
|
+
constructor(d) {
|
|
20828
|
+
this.d = d;
|
|
20829
|
+
this.sleepMs = d.sleepMs === undefined ? DEFAULT_SLEEP_MS : d.sleepMs;
|
|
20830
|
+
this.now = d.now ?? Date.now;
|
|
20831
|
+
d.hub.on("previewRequest", (id) => this.touch(id));
|
|
20832
|
+
d.hub.on("agentStatus", (id) => this.stamp(id));
|
|
20833
|
+
d.hub.on("holdsChanged", (id) => this.stamp(id));
|
|
20834
|
+
d.hub.on("turnSettled", (id) => {
|
|
20835
|
+
if (this.recentlyViewed(id))
|
|
20836
|
+
d.wake(id);
|
|
20837
|
+
});
|
|
20838
|
+
d.hub.on("proc", (id, p) => {
|
|
20839
|
+
if (p.status !== "starting")
|
|
20840
|
+
this.reconsider(id);
|
|
20841
|
+
});
|
|
20842
|
+
d.hub.on("worktreesChanged", () => this.reconsiderAll());
|
|
20843
|
+
const every = d.setInterval ?? ((fn, ms) => {
|
|
20844
|
+
setInterval(fn, ms).unref?.();
|
|
20845
|
+
});
|
|
20846
|
+
every(() => fireAndForget("idle", this.checkPressure(), "memory check"), PRESSURE_EVERY_MS);
|
|
20847
|
+
every(() => fireAndForget("idle", this.sampleCosts(), "cost sample"), COSTS_EVERY_MS);
|
|
20848
|
+
}
|
|
20849
|
+
view(key, id) {
|
|
20850
|
+
const prev = this.viewing.get(key) ?? null;
|
|
20851
|
+
if (prev === id)
|
|
20852
|
+
return;
|
|
20853
|
+
if (prev !== null) {
|
|
20854
|
+
this.viewing.delete(key);
|
|
20855
|
+
this.entry(prev).viewers.delete(key);
|
|
20856
|
+
this.record(prev);
|
|
20857
|
+
this.stamp(prev);
|
|
20858
|
+
}
|
|
20859
|
+
if (id === null)
|
|
20860
|
+
return;
|
|
20861
|
+
this.viewing.set(key, id);
|
|
20862
|
+
this.entry(id).viewers.add(key);
|
|
20863
|
+
const wt = this.record(id);
|
|
20864
|
+
this.stamp(id);
|
|
20865
|
+
this.d.wake(id);
|
|
20866
|
+
if (wt && wt.kind !== "spare") {
|
|
20867
|
+
this.enqueue(`spare:${wt.repoId}`, async () => {
|
|
20868
|
+
await this.d.runtime.awaitPreview(id, STAGGER_MS);
|
|
20869
|
+
this.d.warmSpare(wt.repoId);
|
|
20870
|
+
const spare = this.spareOf(wt.repoId);
|
|
20871
|
+
if (spare)
|
|
20872
|
+
this.d.wake(spare.id);
|
|
20873
|
+
});
|
|
20874
|
+
}
|
|
20875
|
+
}
|
|
20876
|
+
drop(key) {
|
|
20877
|
+
this.view(key, null);
|
|
20878
|
+
}
|
|
20879
|
+
shutdown() {
|
|
20880
|
+
this.recordViewed();
|
|
20881
|
+
}
|
|
20882
|
+
record(id) {
|
|
20883
|
+
const wt = this.d.state.worktree(id);
|
|
20884
|
+
if (!wt)
|
|
20885
|
+
return;
|
|
20886
|
+
wt.viewedAt = this.now();
|
|
20887
|
+
this.d.state.save();
|
|
20888
|
+
return wt;
|
|
20889
|
+
}
|
|
20890
|
+
recordViewed() {
|
|
20891
|
+
let changed = false;
|
|
20892
|
+
for (const id of new Set(this.viewing.values())) {
|
|
20893
|
+
const wt = this.d.state.worktree(id);
|
|
20894
|
+
if (!wt)
|
|
20895
|
+
continue;
|
|
20896
|
+
wt.viewedAt = this.now();
|
|
20897
|
+
changed = true;
|
|
20898
|
+
}
|
|
20899
|
+
if (changed)
|
|
20900
|
+
this.d.state.save();
|
|
20901
|
+
}
|
|
20902
|
+
touch(id) {
|
|
20903
|
+
this.stamp(id);
|
|
20904
|
+
}
|
|
20905
|
+
boot() {
|
|
20906
|
+
const recent = this.d.state.worktrees.filter((w) => w.kind !== "spare" && this.recentlyViewed(w.id)).sort((a, b) => (b.viewedAt ?? 0) - (a.viewedAt ?? 0));
|
|
20907
|
+
for (const wt of recent) {
|
|
20908
|
+
this.enqueue(`boot:${wt.id}`, async () => {
|
|
20909
|
+
this.d.wake(wt.id);
|
|
20910
|
+
await this.d.runtime.awaitPreview(wt.id, STAGGER_MS);
|
|
20911
|
+
});
|
|
20912
|
+
}
|
|
20913
|
+
}
|
|
20914
|
+
recentlyViewed(id) {
|
|
20915
|
+
const wt = this.d.state.worktree(id);
|
|
20916
|
+
return !!wt && this.now() - (wt.viewedAt ?? 0) < (this.sleepMs ?? LOCAL_SLEEP_MS);
|
|
20917
|
+
}
|
|
20918
|
+
isViewed(id) {
|
|
20919
|
+
if ((this.entries.get(id)?.viewers.size ?? 0) > 0)
|
|
20920
|
+
return true;
|
|
20921
|
+
const wt = this.d.state.worktree(id);
|
|
20922
|
+
if (wt?.kind !== "spare")
|
|
20923
|
+
return false;
|
|
20924
|
+
return this.d.state.worktrees.some((w) => w.repoId === wt.repoId && (this.entries.get(w.id)?.viewers.size ?? 0) > 0);
|
|
20925
|
+
}
|
|
20926
|
+
costs() {
|
|
20927
|
+
const out = {};
|
|
20928
|
+
for (const [id, e] of this.entries)
|
|
20929
|
+
if (e.costKb !== undefined)
|
|
20930
|
+
out[id] = e.costKb;
|
|
20931
|
+
return out;
|
|
20932
|
+
}
|
|
20933
|
+
async checkPressure() {
|
|
20934
|
+
const signal = await (this.d.memory ?? memoryTight)();
|
|
20935
|
+
if (!signal?.tight)
|
|
20936
|
+
return;
|
|
20937
|
+
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];
|
|
20938
|
+
if (!pick)
|
|
20939
|
+
return;
|
|
20940
|
+
this.sleep(pick.id, `${signal.why}; least recently used`);
|
|
20941
|
+
}
|
|
20942
|
+
async sampleCosts() {
|
|
20943
|
+
this.recordViewed();
|
|
20944
|
+
const awake = this.d.runtime.awake();
|
|
20945
|
+
const pgids = awake.flatMap((a) => a.pgids);
|
|
20946
|
+
if (pgids.length === 0)
|
|
20947
|
+
return;
|
|
20948
|
+
const rss = await (this.d.costs ?? sampleCosts)(pgids);
|
|
20949
|
+
for (const a of awake)
|
|
20950
|
+
this.entry(a.id).costKb = a.pgids.reduce((sum, g) => sum + (rss.get(g) ?? 0), 0);
|
|
20951
|
+
}
|
|
20952
|
+
entry(id) {
|
|
20953
|
+
let e = this.entries.get(id);
|
|
20954
|
+
if (!e) {
|
|
20955
|
+
e = { viewers: new Set, activeAt: this.now(), timer: null };
|
|
20956
|
+
this.entries.set(id, e);
|
|
20957
|
+
}
|
|
20958
|
+
return e;
|
|
20959
|
+
}
|
|
20960
|
+
spareOf(repoId) {
|
|
20961
|
+
return this.d.state.worktrees.find((w) => w.repoId === repoId && w.kind === "spare");
|
|
20962
|
+
}
|
|
20963
|
+
stamp(id) {
|
|
20964
|
+
this.entry(id).activeAt = this.now();
|
|
20965
|
+
this.reconsider(id);
|
|
20966
|
+
const wt = this.d.state.worktree(id);
|
|
20967
|
+
if (!wt || wt.kind === "spare")
|
|
20968
|
+
return;
|
|
20969
|
+
const spare = this.spareOf(wt.repoId);
|
|
20970
|
+
if (!spare)
|
|
20971
|
+
return;
|
|
20972
|
+
this.entry(spare.id).activeAt = this.now();
|
|
20973
|
+
this.reconsider(spare.id);
|
|
20974
|
+
}
|
|
20975
|
+
candidate(id) {
|
|
20976
|
+
const wt = this.d.state.worktree(id);
|
|
20977
|
+
if (!wt || this.isViewed(id) || this.busy(id))
|
|
20978
|
+
return false;
|
|
20979
|
+
const procs = this.d.runtime.get(id)?.procs;
|
|
20980
|
+
if (!procs || procs.asleep)
|
|
20981
|
+
return false;
|
|
20982
|
+
const states = procs.states();
|
|
20983
|
+
if (states.length === 0 || states.some((s) => s.status === "starting"))
|
|
20984
|
+
return false;
|
|
20985
|
+
if (wt.kind === "spare") {
|
|
20986
|
+
return !this.d.state.worktrees.some((w) => w.repoId === wt.repoId && w.id !== id && this.busy(w.id));
|
|
20987
|
+
}
|
|
20988
|
+
return true;
|
|
20989
|
+
}
|
|
20990
|
+
busy(id) {
|
|
20991
|
+
return this.d.runtime.busy(id);
|
|
20992
|
+
}
|
|
20993
|
+
reconsider(id) {
|
|
20994
|
+
if (!this.candidate(id)) {
|
|
20995
|
+
this.disarm(id);
|
|
20996
|
+
return;
|
|
20997
|
+
}
|
|
20998
|
+
if (this.sleepMs === null)
|
|
20999
|
+
return;
|
|
21000
|
+
this.arm(id, Math.max(0, this.entry(id).activeAt + this.sleepMs - this.now()));
|
|
21001
|
+
}
|
|
21002
|
+
reconsiderAll() {
|
|
21003
|
+
for (const id of this.entries.keys()) {
|
|
21004
|
+
if (this.d.state.worktree(id))
|
|
21005
|
+
continue;
|
|
21006
|
+
this.disarm(id);
|
|
21007
|
+
this.entries.delete(id);
|
|
21008
|
+
}
|
|
21009
|
+
for (const wt of this.d.state.worktrees)
|
|
21010
|
+
this.reconsider(wt.id);
|
|
21011
|
+
}
|
|
21012
|
+
arm(id, ms) {
|
|
21013
|
+
this.disarm(id);
|
|
21014
|
+
const set = this.d.setTimer ?? ((fn, delay) => {
|
|
21015
|
+
const t = setTimeout(fn, delay);
|
|
21016
|
+
t.unref?.();
|
|
21017
|
+
return t;
|
|
21018
|
+
});
|
|
21019
|
+
this.entry(id).timer = set(() => this.fire(id), ms);
|
|
21020
|
+
}
|
|
21021
|
+
disarm(id) {
|
|
21022
|
+
const e = this.entries.get(id);
|
|
21023
|
+
if (!e?.timer)
|
|
21024
|
+
return;
|
|
21025
|
+
(this.d.clearTimer ?? ((t) => clearTimeout(t)))(e.timer);
|
|
21026
|
+
e.timer = null;
|
|
21027
|
+
}
|
|
21028
|
+
fire(id) {
|
|
21029
|
+
const e = this.entry(id);
|
|
21030
|
+
e.timer = null;
|
|
21031
|
+
if (this.sleepMs === null || !this.candidate(id))
|
|
21032
|
+
return;
|
|
21033
|
+
const idle = this.now() - e.activeAt;
|
|
21034
|
+
if (idle < this.sleepMs) {
|
|
21035
|
+
this.arm(id, this.sleepMs - idle);
|
|
21036
|
+
return;
|
|
21037
|
+
}
|
|
21038
|
+
this.sleep(id, `idle ${humanMs(this.sleepMs)}`);
|
|
21039
|
+
}
|
|
21040
|
+
sleep(id, why) {
|
|
21041
|
+
this.disarm(id);
|
|
21042
|
+
this.entry(id).costKb = undefined;
|
|
21043
|
+
log.info(id, `asleep: ${why}`);
|
|
21044
|
+
fireAndForget(id, this.d.runtime.sleep(id), "sleep");
|
|
21045
|
+
}
|
|
21046
|
+
enqueue(key, run) {
|
|
21047
|
+
if (this.queue.some((q) => q.key === key))
|
|
21048
|
+
return;
|
|
21049
|
+
this.queue.push({ key, run });
|
|
21050
|
+
if (!this.draining)
|
|
21051
|
+
fireAndForget("idle", this.drain(), "wake queue");
|
|
21052
|
+
}
|
|
21053
|
+
async drain() {
|
|
21054
|
+
this.draining = true;
|
|
21055
|
+
try {
|
|
21056
|
+
for (let next = this.queue.shift();next; next = this.queue.shift())
|
|
21057
|
+
await next.run();
|
|
21058
|
+
} finally {
|
|
21059
|
+
this.draining = false;
|
|
21060
|
+
}
|
|
21061
|
+
}
|
|
21062
|
+
}
|
|
21063
|
+
function humanMs(ms) {
|
|
21064
|
+
if (ms >= 60 * 60000 && ms % (60 * 60000) === 0)
|
|
21065
|
+
return `${ms / (60 * 60000)}h`;
|
|
21066
|
+
if (ms >= 60000)
|
|
21067
|
+
return `${Math.round(ms / 60000)} min`;
|
|
21068
|
+
return `${Math.round(ms / 1000)}s`;
|
|
21069
|
+
}
|
|
21070
|
+
|
|
21071
|
+
// ../daemon/src/server/ws.ts
|
|
21072
|
+
import { homedir as homedir9 } from "os";
|
|
21073
|
+
|
|
21074
|
+
// ../daemon/src/agent/config.ts
|
|
21075
|
+
import { existsSync as existsSync19, readFileSync as readFileSync12 } from "fs";
|
|
21076
|
+
import { homedir as homedir8 } from "os";
|
|
21077
|
+
import { join as join16 } from "path";
|
|
21078
|
+
function agentConfigFiles(spec, repoPath, home = homedir8()) {
|
|
21079
|
+
const file = (id, label, path) => ({
|
|
21080
|
+
id,
|
|
21081
|
+
label,
|
|
21082
|
+
path,
|
|
21083
|
+
exists: existsSync19(path)
|
|
21084
|
+
});
|
|
21085
|
+
if (spec.id === "claude") {
|
|
21086
|
+
return [
|
|
21087
|
+
file("user-config", "user config (MCP servers, projects)", join16(home, ".claude.json")),
|
|
21088
|
+
file("user-settings", "user settings (permissions, hooks)", join16(home, ".claude", "settings.json")),
|
|
21089
|
+
...repoPath ? [file("project-mcp", "project MCP servers", join16(repoPath, ".mcp.json"))] : [],
|
|
21090
|
+
...repoPath ? [file("project-settings", "project settings", join16(repoPath, ".claude", "settings.json"))] : []
|
|
21091
|
+
];
|
|
21092
|
+
}
|
|
21093
|
+
if (spec.id === "codex")
|
|
21094
|
+
return [file("user-config", "config", join16(home, ".codex", "config.toml"))];
|
|
21095
|
+
return [];
|
|
21096
|
+
}
|
|
21097
|
+
function describeAgentConfig(spec, repoPath, home = homedir8()) {
|
|
21098
|
+
const files = agentConfigFiles(spec, repoPath, home);
|
|
21099
|
+
const servers = [];
|
|
21100
|
+
if (spec.id === "claude") {
|
|
21101
|
+
const user = readJson(join16(home, ".claude.json"));
|
|
21102
|
+
servers.push(...serversOf(user?.mcpServers, "user"));
|
|
21103
|
+
if (repoPath) {
|
|
21104
|
+
const projects = user?.projects;
|
|
21105
|
+
const mine = projects?.[repoPath];
|
|
21106
|
+
servers.push(...serversOf(mine?.mcpServers, "local"));
|
|
21107
|
+
servers.push(...serversOf(readJson(join16(repoPath, ".mcp.json"))?.mcpServers, "project"));
|
|
21108
|
+
}
|
|
21109
|
+
} else if (spec.id === "codex") {
|
|
21110
|
+
servers.push(...codexServers(join16(home, ".codex", "config.toml")));
|
|
21111
|
+
}
|
|
21112
|
+
return { agent: spec.id, files, servers };
|
|
21113
|
+
}
|
|
21114
|
+
function readJson(path) {
|
|
21115
|
+
if (!existsSync19(path))
|
|
21116
|
+
return null;
|
|
21117
|
+
try {
|
|
21118
|
+
const v = JSON.parse(readFileSync12(path, "utf8"));
|
|
21119
|
+
return v && typeof v === "object" && !Array.isArray(v) ? v : null;
|
|
21120
|
+
} catch (e) {
|
|
21121
|
+
log.warn("agent-config", `${path} is not valid JSON`, e);
|
|
21122
|
+
return null;
|
|
21123
|
+
}
|
|
21124
|
+
}
|
|
21125
|
+
function serversOf(raw, scope) {
|
|
21126
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw))
|
|
20052
21127
|
return [];
|
|
20053
21128
|
return Object.entries(raw).map(([name, v]) => {
|
|
20054
21129
|
const rec = v && typeof v === "object" ? v : {};
|
|
@@ -20058,11 +21133,11 @@ function serversOf(raw, scope) {
|
|
|
20058
21133
|
});
|
|
20059
21134
|
}
|
|
20060
21135
|
function codexServers(path) {
|
|
20061
|
-
if (!
|
|
21136
|
+
if (!existsSync19(path))
|
|
20062
21137
|
return [];
|
|
20063
21138
|
const out = [];
|
|
20064
21139
|
let current = null;
|
|
20065
|
-
for (const raw of
|
|
21140
|
+
for (const raw of readFileSync12(path, "utf8").split(`
|
|
20066
21141
|
`)) {
|
|
20067
21142
|
const line = raw.trim();
|
|
20068
21143
|
const header = /^\[mcp_servers\.("?)([^\]"]+)\1\]$/.exec(line);
|
|
@@ -20597,7 +21672,7 @@ function daylightNow(tz, at = new Date) {
|
|
|
20597
21672
|
}
|
|
20598
21673
|
|
|
20599
21674
|
// ../daemon/src/server/handlers.ts
|
|
20600
|
-
var
|
|
21675
|
+
var shipped = (worktreeId, ok, message, extra = {}) => ({ t: "shipped", worktreeId, ok, message, ...extra });
|
|
20601
21676
|
var errorText = (e) => e instanceof Error ? e.message : String(e);
|
|
20602
21677
|
var gitStatus = async (s, ctx, worktreeId) => {
|
|
20603
21678
|
const info = await s.worktrees.gitStatus(worktreeId);
|
|
@@ -20611,6 +21686,8 @@ var gitAndPages = async (s, ctx, worktreeId) => {
|
|
|
20611
21686
|
};
|
|
20612
21687
|
var notify = async (s, ctx, worktreeId, msg) => {
|
|
20613
21688
|
ctx.reply(msg);
|
|
21689
|
+
if (!msg.ok)
|
|
21690
|
+
s.turns.markUnread(worktreeId);
|
|
20614
21691
|
await gitStatus(s, ctx, worktreeId);
|
|
20615
21692
|
};
|
|
20616
21693
|
var requireRun = (s, id) => {
|
|
@@ -20622,8 +21699,18 @@ var requireRun = (s, id) => {
|
|
|
20622
21699
|
var handlers = {
|
|
20623
21700
|
async subscribe(msg, ctx, s) {
|
|
20624
21701
|
const r = s.worktrees.readable(msg.worktreeId);
|
|
20625
|
-
if (!r)
|
|
20626
|
-
|
|
21702
|
+
if (!r) {
|
|
21703
|
+
const kept = s.worktrees.archivedTranscript(msg.worktreeId);
|
|
21704
|
+
if (!kept)
|
|
21705
|
+
throw new UserError("unknown worktree");
|
|
21706
|
+
if (!ctx.subscribe(msg.worktreeId))
|
|
21707
|
+
return;
|
|
21708
|
+
ctx.reply({ t: "backfill", worktreeId: msg.worktreeId, events: coalesce(kept), log: [] });
|
|
21709
|
+
ctx.reply({ t: "queue", worktreeId: msg.worktreeId, items: [] });
|
|
21710
|
+
ctx.reply({ t: "agent-commands", worktreeId: msg.worktreeId, commands: [] });
|
|
21711
|
+
await gitStatus(s, ctx, msg.worktreeId);
|
|
21712
|
+
return;
|
|
21713
|
+
}
|
|
20627
21714
|
if (!ctx.subscribe(msg.worktreeId))
|
|
20628
21715
|
return;
|
|
20629
21716
|
if (!r.wt) {
|
|
@@ -20633,13 +21720,11 @@ var handlers = {
|
|
|
20633
21720
|
await gitAndPages(s, ctx, msg.worktreeId);
|
|
20634
21721
|
return;
|
|
20635
21722
|
}
|
|
20636
|
-
s.repos.touch(msg.worktreeId);
|
|
20637
21723
|
const agent = s.runtime.ensureAgent(r.wt).agent;
|
|
20638
|
-
const events = agent.transcript();
|
|
20639
21724
|
ctx.reply({
|
|
20640
21725
|
t: "backfill",
|
|
20641
21726
|
worktreeId: msg.worktreeId,
|
|
20642
|
-
events:
|
|
21727
|
+
events: coalesce(agent.transcript()),
|
|
20643
21728
|
log: s.runtime.recentLogs(msg.worktreeId)
|
|
20644
21729
|
});
|
|
20645
21730
|
ctx.reply({ t: "queue", worktreeId: msg.worktreeId, items: agent.queueItems });
|
|
@@ -20649,12 +21734,18 @@ var handlers = {
|
|
|
20649
21734
|
unsubscribe(msg, ctx) {
|
|
20650
21735
|
ctx.unsubscribe(msg.worktreeId);
|
|
20651
21736
|
},
|
|
21737
|
+
view(msg, ctx) {
|
|
21738
|
+
ctx.view(msg.worktreeId);
|
|
21739
|
+
},
|
|
20652
21740
|
seen(msg, _ctx, s) {
|
|
20653
21741
|
s.turns.markSeen(msg.worktreeId);
|
|
20654
21742
|
},
|
|
20655
21743
|
"mark-unread"(msg, _ctx, s) {
|
|
20656
21744
|
s.turns.markUnread(msg.worktreeId);
|
|
20657
21745
|
},
|
|
21746
|
+
"set-draft"(msg, _ctx, s) {
|
|
21747
|
+
s.drafts.set(msg.boxId, msg.text, msg.clientId);
|
|
21748
|
+
},
|
|
20658
21749
|
"refresh-git"(msg, _ctx, s) {
|
|
20659
21750
|
s.worktrees.recount(msg.repoId);
|
|
20660
21751
|
},
|
|
@@ -20667,7 +21758,12 @@ var handlers = {
|
|
|
20667
21758
|
"forget-visit"(msg, _ctx, s) {
|
|
20668
21759
|
s.routes.forget(msg.repoId, msg.path);
|
|
20669
21760
|
},
|
|
20670
|
-
chat(msg, _ctx, s) {
|
|
21761
|
+
async chat(msg, _ctx, s) {
|
|
21762
|
+
await s.worktrees.archivingNow(msg.worktreeId);
|
|
21763
|
+
if (!s.state.worktree(msg.worktreeId) && s.worktrees.hasArchived(msg.worktreeId)) {
|
|
21764
|
+
await s.worktrees.restore(msg.worktreeId, msg.clientId, { text: msg.text, attachments: msg.attachments });
|
|
21765
|
+
return;
|
|
21766
|
+
}
|
|
20671
21767
|
requireRun(s, msg.worktreeId);
|
|
20672
21768
|
const agent = s.runtime.agentFor(msg.worktreeId);
|
|
20673
21769
|
if (!agent)
|
|
@@ -20678,7 +21774,6 @@ var handlers = {
|
|
|
20678
21774
|
async "create-worktree"(msg, _ctx, s) {
|
|
20679
21775
|
await s.worktrees.create(msg.repoId, msg.prompt, {
|
|
20680
21776
|
createdBy: msg.clientId,
|
|
20681
|
-
baseWorktreeId: msg.baseWorktreeId,
|
|
20682
21777
|
variant: msg.variant,
|
|
20683
21778
|
context: msg.context,
|
|
20684
21779
|
attachments: msg.attachments,
|
|
@@ -20686,7 +21781,8 @@ var handlers = {
|
|
|
20686
21781
|
profile: msg.profile,
|
|
20687
21782
|
mode: msg.mode,
|
|
20688
21783
|
model: msg.model,
|
|
20689
|
-
effort: msg.effort
|
|
21784
|
+
effort: msg.effort,
|
|
21785
|
+
carry: msg.carry
|
|
20690
21786
|
});
|
|
20691
21787
|
},
|
|
20692
21788
|
"set-worktree-profile"(msg, _ctx, s) {
|
|
@@ -20703,7 +21799,6 @@ var handlers = {
|
|
|
20703
21799
|
},
|
|
20704
21800
|
"batch-worktrees"(msg, ctx, s) {
|
|
20705
21801
|
const repo = s.state.requireRepo(msg.repoId);
|
|
20706
|
-
ctx.reply(toast("", true, "batch: planning tasks\u2026"));
|
|
20707
21802
|
fireAndForget(msg.repoId, (async () => {
|
|
20708
21803
|
const agent = msg.agent ?? s.state.defaultAgent ?? DEFAULT_AGENT_ID;
|
|
20709
21804
|
const tasks = await s.planTasks(msg.prompt, repo.path, agent) ?? [msg.prompt];
|
|
@@ -20717,22 +21812,18 @@ var handlers = {
|
|
|
20717
21812
|
}
|
|
20718
21813
|
}
|
|
20719
21814
|
const started = tasks.length - failed;
|
|
20720
|
-
|
|
21815
|
+
if (failed)
|
|
21816
|
+
ctx.reply(shipped("", false, `batch: ${started} started, ${failed} failed (see daemon log)`));
|
|
20721
21817
|
})(), "batch");
|
|
20722
21818
|
},
|
|
20723
|
-
async "
|
|
20724
|
-
|
|
20725
|
-
if (archived) {
|
|
20726
|
-
ctx.reply(toast(msg.worktreeId, true, `removed ${archived.title}: archived`, {
|
|
20727
|
-
...archived.restorable ? { restoreId: archived.id } : {}
|
|
20728
|
-
}));
|
|
20729
|
-
}
|
|
21819
|
+
async "archive-worktree"(msg, _ctx, s) {
|
|
21820
|
+
await s.worktrees.archiveWorktree(msg.worktreeId);
|
|
20730
21821
|
},
|
|
20731
21822
|
"list-archived"(msg, ctx, s) {
|
|
20732
21823
|
ctx.reply({ t: "archived", repoId: msg.repoId, items: s.worktrees.archived(msg.repoId) });
|
|
20733
21824
|
},
|
|
20734
21825
|
async "restore-worktree"(msg, _ctx, s) {
|
|
20735
|
-
await s.worktrees.restore(msg.archiveId, msg.clientId);
|
|
21826
|
+
await s.worktrees.restore(msg.archiveId, msg.clientId, msg.message);
|
|
20736
21827
|
},
|
|
20737
21828
|
async "delete-archived"(msg, _ctx, s) {
|
|
20738
21829
|
await s.worktrees.deleteArchived(msg.archiveId);
|
|
@@ -20746,7 +21837,9 @@ var handlers = {
|
|
|
20746
21837
|
async "read-file"(msg, ctx, s) {
|
|
20747
21838
|
const head = { t: "file-read", worktreeId: msg.worktreeId, path: msg.path, ref: msg.ref, seq: msg.seq };
|
|
20748
21839
|
try {
|
|
20749
|
-
|
|
21840
|
+
const kept = s.worktrees.readable(msg.worktreeId) ? null : await s.worktrees.archivedFile(msg.worktreeId, msg.path, msg.ref);
|
|
21841
|
+
const read = kept ? keptRead(kept.before, kept.after) : await s.files.read(msg.worktreeId, msg.path, msg.ref);
|
|
21842
|
+
ctx.reply({ ...head, ...read });
|
|
20750
21843
|
} catch (e) {
|
|
20751
21844
|
if (!(e instanceof UserError))
|
|
20752
21845
|
log.error(msg.worktreeId, "read-file failed", e);
|
|
@@ -20763,36 +21856,54 @@ var handlers = {
|
|
|
20763
21856
|
ctx.reply({ t: "git-commit", worktreeId: msg.worktreeId, sha: msg.sha, files });
|
|
20764
21857
|
},
|
|
20765
21858
|
async land(msg, ctx, s) {
|
|
20766
|
-
const { result,
|
|
21859
|
+
const { result, archiveIds } = await s.worktrees.land(msg.worktreeId, msg.message);
|
|
20767
21860
|
const suggestion = !result.ok && result.conflict ? "Bring main into this branch and resolve the conflicts, then verify the app still works." : undefined;
|
|
20768
21861
|
if (result.ok && (result.pr || s.state.worktree(msg.worktreeId)?.pr)) {
|
|
20769
21862
|
fireAndForget(msg.worktreeId, s.prs.refresh(msg.worktreeId), "pr refresh");
|
|
20770
21863
|
}
|
|
20771
|
-
await notify(s, ctx, msg.worktreeId,
|
|
21864
|
+
await notify(s, ctx, msg.worktreeId, shipped(msg.worktreeId, result.ok, result.message, {
|
|
20772
21865
|
merged: result.ok,
|
|
20773
21866
|
url: result.url,
|
|
20774
|
-
|
|
21867
|
+
archiveIds: archiveIds ?? [],
|
|
20775
21868
|
...suggestion ? { suggestion } : {}
|
|
20776
21869
|
}));
|
|
20777
21870
|
},
|
|
20778
21871
|
async "sync-main"(msg, ctx, s) {
|
|
20779
21872
|
const { result, defaultBranch } = await s.worktrees.sync(msg.worktreeId);
|
|
20780
21873
|
const prompt = !result.ok && result.conflict && s.state.worktree(msg.worktreeId);
|
|
20781
|
-
await notify(s, ctx, msg.worktreeId, prompt ?
|
|
21874
|
+
await notify(s, ctx, msg.worktreeId, prompt ? shipped(msg.worktreeId, false, `sync conflicts with ${defaultBranch}: prompt prefilled in chat`, {
|
|
20782
21875
|
suggestion: `Merge ${defaultBranch} into this branch and resolve the conflicts, then verify the app still works.`
|
|
20783
|
-
}) :
|
|
21876
|
+
}) : shipped(msg.worktreeId, result.ok, result.message));
|
|
20784
21877
|
},
|
|
20785
21878
|
async "pull-main"(msg, ctx, s) {
|
|
20786
21879
|
const result = await s.worktrees.pull(msg.worktreeId);
|
|
20787
|
-
await notify(s, ctx, msg.worktreeId,
|
|
21880
|
+
await notify(s, ctx, msg.worktreeId, shipped(msg.worktreeId, result.ok, result.message));
|
|
21881
|
+
},
|
|
21882
|
+
"run-after-land"(msg, ctx, s) {
|
|
21883
|
+
const repo = s.state.repo(msg.repoId);
|
|
21884
|
+
if (!repo)
|
|
21885
|
+
throw new UserError("no such project");
|
|
21886
|
+
if ((repo.config.afterLand ?? []).length === 0) {
|
|
21887
|
+
throw new UserError(`${repo.name} has no afterLand commands in ${repo.configFile}`);
|
|
21888
|
+
}
|
|
21889
|
+
s.afterLand.run(msg.repoId);
|
|
21890
|
+
ctx.reply({ t: "self", self: s.self.get() });
|
|
21891
|
+
},
|
|
21892
|
+
"restart-daemon"(_msg, _ctx, s) {
|
|
21893
|
+
const refused = s.restarter.request();
|
|
21894
|
+
if (refused)
|
|
21895
|
+
throw new UserError(refused);
|
|
21896
|
+
},
|
|
21897
|
+
"update-now"(_msg, _ctx, s) {
|
|
21898
|
+
return s.update.updateNow();
|
|
20788
21899
|
},
|
|
20789
21900
|
async commit(msg, ctx, s) {
|
|
20790
21901
|
const result = await s.worktrees.commit(msg.worktreeId, msg.message);
|
|
20791
|
-
await notify(s, ctx, msg.worktreeId,
|
|
21902
|
+
await notify(s, ctx, msg.worktreeId, shipped(msg.worktreeId, result.ok, result.message));
|
|
20792
21903
|
},
|
|
20793
21904
|
async graft(msg, ctx, s) {
|
|
20794
|
-
const { target
|
|
20795
|
-
await
|
|
21905
|
+
const { target } = await s.worktrees.graft(msg.targetId, msg.sourceIds);
|
|
21906
|
+
await gitStatus(s, ctx, target.id);
|
|
20796
21907
|
},
|
|
20797
21908
|
async "rename-worktree"(msg, _ctx, s) {
|
|
20798
21909
|
await s.worktrees.rename(msg.worktreeId, msg.title);
|
|
@@ -20804,7 +21915,7 @@ var handlers = {
|
|
|
20804
21915
|
fireAndForget(msg.worktreeId, agent.warmCommands(), "warm commands");
|
|
20805
21916
|
},
|
|
20806
21917
|
async "list-files"(msg, ctx, s) {
|
|
20807
|
-
ctx.reply({ t: "files", worktreeId: msg.worktreeId,
|
|
21918
|
+
ctx.reply({ t: "files", worktreeId: msg.worktreeId, ...await s.files.list(msg.worktreeId) });
|
|
20808
21919
|
},
|
|
20809
21920
|
async search(msg, ctx, s) {
|
|
20810
21921
|
const { hits, truncated } = await s.files.search(msg.worktreeId, msg.query);
|
|
@@ -20837,9 +21948,8 @@ var handlers = {
|
|
|
20837
21948
|
reveal(msg, _ctx, s) {
|
|
20838
21949
|
s.files.reveal(msg.worktreeId, msg.path);
|
|
20839
21950
|
},
|
|
20840
|
-
async "discard-file"(msg,
|
|
21951
|
+
async "discard-file"(msg, _ctx, s) {
|
|
20841
21952
|
await s.files.discard(msg.worktreeId, msg.path);
|
|
20842
|
-
ctx.reply(toast(msg.worktreeId, true, `discarded ${msg.path}`));
|
|
20843
21953
|
s.hub.emit("filesChanged", msg.worktreeId);
|
|
20844
21954
|
},
|
|
20845
21955
|
async "write-file"(msg, ctx, s) {
|
|
@@ -20856,19 +21966,17 @@ var handlers = {
|
|
|
20856
21966
|
}
|
|
20857
21967
|
},
|
|
20858
21968
|
async "confirm-config"(msg, _ctx, s) {
|
|
20859
|
-
await s.repos.confirmConfig(msg.repoId, msg.config);
|
|
21969
|
+
await s.repos.confirmConfig(msg.repoId, msg.config, msg.kind);
|
|
20860
21970
|
},
|
|
20861
|
-
async "register-repo"(msg,
|
|
20862
|
-
|
|
20863
|
-
ctx.reply(toast("", true, `opened ${repo.name}`));
|
|
21971
|
+
async "register-repo"(msg, _ctx, s) {
|
|
21972
|
+
await s.repos.register(msg.path);
|
|
20864
21973
|
},
|
|
20865
|
-
async "create-repo"(msg,
|
|
21974
|
+
async "create-repo"(msg, _ctx, s) {
|
|
20866
21975
|
if (msg.mode === "clone") {
|
|
20867
21976
|
s.repos.startImport({ parent: msg.parent, name: msg.name, url: msg.url ?? "" });
|
|
20868
21977
|
return;
|
|
20869
21978
|
}
|
|
20870
|
-
|
|
20871
|
-
ctx.reply(toast("", true, `created ${repo.name}`));
|
|
21979
|
+
await s.repos.create(msg);
|
|
20872
21980
|
},
|
|
20873
21981
|
async "unmake-repo"(msg, _ctx, s) {
|
|
20874
21982
|
await s.repos.unmake(msg.repoId);
|
|
@@ -20892,10 +22000,8 @@ var handlers = {
|
|
|
20892
22000
|
"cancel-folder"(_msg, _ctx, s) {
|
|
20893
22001
|
s.folderDialog.cancel();
|
|
20894
22002
|
},
|
|
20895
|
-
async "forget-repo"(msg,
|
|
20896
|
-
const name = s.state.requireRepo(msg.repoId).name;
|
|
22003
|
+
async "forget-repo"(msg, _ctx, s) {
|
|
20897
22004
|
await s.repos.forget(msg.repoId);
|
|
20898
|
-
ctx.reply(toast("", true, `forgot ${name}`));
|
|
20899
22005
|
},
|
|
20900
22006
|
"set-theme"(msg, _ctx, s) {
|
|
20901
22007
|
s.themes.setPrefs(msg.prefs);
|
|
@@ -21005,6 +22111,10 @@ var handlers = {
|
|
|
21005
22111
|
const refs = await s.refs.search(msg.repoId, msg.query);
|
|
21006
22112
|
ctx.reply({ t: "refs", repoId: msg.repoId, query: msg.query, refs });
|
|
21007
22113
|
},
|
|
22114
|
+
async "search-chats"(msg, ctx, s) {
|
|
22115
|
+
const { hits, truncated } = await s.chats.search(msg.repoId, msg.query);
|
|
22116
|
+
ctx.reply({ t: "chat-hits", repoId: msg.repoId, query: msg.query, hits, truncated });
|
|
22117
|
+
},
|
|
21008
22118
|
async "open-ref"(msg, _ctx, s) {
|
|
21009
22119
|
await s.worktrees.openRef(msg.repoId, msg.kind, msg.ref, { createdBy: msg.clientId, pr: msg.pr });
|
|
21010
22120
|
}
|
|
@@ -21023,7 +22133,7 @@ async function dispatch(msg, ctx, s) {
|
|
|
21023
22133
|
}
|
|
21024
22134
|
|
|
21025
22135
|
// ../daemon/src/server/http.ts
|
|
21026
|
-
import { existsSync as
|
|
22136
|
+
import { existsSync as existsSync20 } from "fs";
|
|
21027
22137
|
import { join as join17 } from "path";
|
|
21028
22138
|
var IMMUTABLE = "private, max-age=31536000, immutable";
|
|
21029
22139
|
var NO_STORE = "no-store";
|
|
@@ -21061,6 +22171,7 @@ function createFetch(opts) {
|
|
|
21061
22171
|
dropped: new Set,
|
|
21062
22172
|
sent: 0,
|
|
21063
22173
|
bytes: 0,
|
|
22174
|
+
view: null,
|
|
21064
22175
|
preview: { handler, data }
|
|
21065
22176
|
}
|
|
21066
22177
|
}));
|
|
@@ -21069,7 +22180,15 @@ function createFetch(opts) {
|
|
|
21069
22180
|
const authed = sameSecret(url.searchParams.get("token"), opts.token);
|
|
21070
22181
|
if (authed)
|
|
21071
22182
|
opts.noteShellOrigin(req.headers.get("origin"));
|
|
21072
|
-
const data = {
|
|
22183
|
+
const data = {
|
|
22184
|
+
authed,
|
|
22185
|
+
subs: new Set,
|
|
22186
|
+
terms: new Set,
|
|
22187
|
+
dropped: new Set,
|
|
22188
|
+
sent: 0,
|
|
22189
|
+
bytes: 0,
|
|
22190
|
+
view: null
|
|
22191
|
+
};
|
|
21073
22192
|
const headers = authed && remote && remoteShell ? { "set-cookie": grantCookie(grant, remote.host) } : undefined;
|
|
21074
22193
|
if (srv.upgrade(req, { data, headers }))
|
|
21075
22194
|
return;
|
|
@@ -21085,6 +22204,12 @@ function createFetch(opts) {
|
|
|
21085
22204
|
headers["set-cookie"] = grantCookie(grant, remote.host);
|
|
21086
22205
|
return Response.json(await opts.bootstrap(), { headers });
|
|
21087
22206
|
}
|
|
22207
|
+
if (url.pathname === "/restart" && req.method === "POST") {
|
|
22208
|
+
if (!sameSecret(url.searchParams.get("token"), opts.token))
|
|
22209
|
+
return new Response("unauthorized", { status: 401 });
|
|
22210
|
+
const refused = opts.restart();
|
|
22211
|
+
return refused ? new Response(refused, { status: 409 }) : new Response(null, { status: 202 });
|
|
22212
|
+
}
|
|
21088
22213
|
if (url.pathname === "/register" && req.method === "POST") {
|
|
21089
22214
|
if (!sameSecret(req.headers.get("authorization"), `Bearer ${opts.token}`)) {
|
|
21090
22215
|
return new Response("unauthorized", { status: 401 });
|
|
@@ -21106,21 +22231,22 @@ function createFetch(opts) {
|
|
|
21106
22231
|
if (!sameSecret(url.searchParams.get("token"), opts.token))
|
|
21107
22232
|
return new Response("unauthorized", { status: 401 });
|
|
21108
22233
|
const [worktreeId, file, extra] = url.pathname.slice("/attachments/".length).split("/");
|
|
21109
|
-
const
|
|
21110
|
-
|
|
22234
|
+
const places = worktreeId && file && !extra ? [opts.attachments.fileFor(worktreeId, file), opts.archivedAttachment(worktreeId, file)] : [];
|
|
22235
|
+
const path = places.find((p) => !!p && existsSync20(p));
|
|
22236
|
+
if (!path)
|
|
21111
22237
|
return new Response("not found", { status: 404 });
|
|
21112
22238
|
return new Response(Bun.file(path), { headers: { "cache-control": IMMUTABLE } });
|
|
21113
22239
|
}
|
|
21114
22240
|
const rel = url.pathname === "/" ? "/index.html" : url.pathname;
|
|
21115
22241
|
const file = join17(opts.shellDist, rel.replaceAll("..", ""));
|
|
21116
22242
|
const hashed = rel.startsWith("/assets/");
|
|
21117
|
-
if (
|
|
22243
|
+
if (existsSync20(file) && Bun.file(file).size > 0) {
|
|
21118
22244
|
return new Response(Bun.file(file), { headers: { "cache-control": hashed ? IMMUTABLE : NO_STORE } });
|
|
21119
22245
|
}
|
|
21120
22246
|
if (hashed)
|
|
21121
22247
|
return new Response("not found", { status: 404 });
|
|
21122
22248
|
const index = join17(opts.shellDist, "index.html");
|
|
21123
|
-
if (
|
|
22249
|
+
if (existsSync20(index))
|
|
21124
22250
|
return new Response(Bun.file(index), { headers: { "cache-control": NO_STORE } });
|
|
21125
22251
|
return new Response("Toyon daemon running; shell not built (run: bun run build)", { status: 200 });
|
|
21126
22252
|
};
|
|
@@ -21214,8 +22340,11 @@ function startServer(opts) {
|
|
|
21214
22340
|
lag,
|
|
21215
22341
|
worktrees: {
|
|
21216
22342
|
total: s.state.worktrees.filter((w) => w.kind !== "spare").length,
|
|
21217
|
-
running: s.runtime.runningCount()
|
|
22343
|
+
running: s.runtime.runningCount(),
|
|
22344
|
+
...s.runtime.tiers()
|
|
21218
22345
|
},
|
|
22346
|
+
costs: s.idle.costs(),
|
|
22347
|
+
updates: s.update.status(),
|
|
21219
22348
|
sockets: [...sockets].map((ws) => ({ subs: [...ws.data.subs], sent: ws.data.sent, bytes: ws.data.bytes }))
|
|
21220
22349
|
});
|
|
21221
22350
|
let statusesInFlight = false;
|
|
@@ -21320,29 +22449,40 @@ function startServer(opts) {
|
|
|
21320
22449
|
const agentsMsg = () => ({
|
|
21321
22450
|
t: "agents",
|
|
21322
22451
|
agents: agentInfos(),
|
|
21323
|
-
defaultAgent: s.state.defaultAgent ?? DEFAULT_AGENT_ID
|
|
22452
|
+
defaultAgent: s.state.defaultAgent ?? DEFAULT_AGENT_ID,
|
|
22453
|
+
agentChosen: s.state.defaultAgent !== undefined
|
|
21324
22454
|
});
|
|
21325
22455
|
s.hub.on("agentsChanged", () => broadcast(agentsMsg()));
|
|
22456
|
+
s.hub.on("selfChanged", () => broadcast({ t: "self", self: s.self.get() }));
|
|
22457
|
+
s.hub.on("updateChanged", () => broadcast({ t: "update", update: s.update.get() }));
|
|
21326
22458
|
s.hub.on("visitsChanged", (repoId) => broadcast({ t: "visits", repoId, pages: s.routes.history(repoId) }));
|
|
21327
22459
|
s.hub.on("archiveChanged", (repoId) => broadcast({ t: "archived", repoId, items: s.worktrees.archived(repoId) }));
|
|
21328
|
-
|
|
21329
|
-
|
|
21330
|
-
|
|
21331
|
-
|
|
21332
|
-
|
|
21333
|
-
|
|
21334
|
-
|
|
21335
|
-
|
|
21336
|
-
|
|
21337
|
-
|
|
21338
|
-
|
|
21339
|
-
|
|
21340
|
-
|
|
21341
|
-
|
|
21342
|
-
|
|
21343
|
-
|
|
21344
|
-
|
|
21345
|
-
|
|
22460
|
+
s.hub.on("draftChanged", (boxId, text, clientId) => broadcast({ t: "draft", boxId, text, ...clientId ? { clientId } : {} }));
|
|
22461
|
+
const helloFrame = async () => {
|
|
22462
|
+
await s.update.refresh();
|
|
22463
|
+
return {
|
|
22464
|
+
t: "hello",
|
|
22465
|
+
version,
|
|
22466
|
+
protocol: PROTOCOL_VERSION,
|
|
22467
|
+
repos: s.state.repos,
|
|
22468
|
+
rows: await s.worktrees.rows({ quick: true }),
|
|
22469
|
+
spares: s.worktrees.spares(),
|
|
22470
|
+
themes: s.themes.themes,
|
|
22471
|
+
themePrefs: s.themes.prefs,
|
|
22472
|
+
agents: agentInfos(),
|
|
22473
|
+
defaultAgent: s.state.defaultAgent ?? DEFAULT_AGENT_ID,
|
|
22474
|
+
agentChosen: s.state.defaultAgent !== undefined,
|
|
22475
|
+
home: homedir9(),
|
|
22476
|
+
folderDialog: process.platform === "darwin" && !cloud.enabled,
|
|
22477
|
+
remote: opts.remote && { host: opts.remote.host, previews: opts.remote.previews },
|
|
22478
|
+
gitIdentity: await s.repos.gitIdentity(),
|
|
22479
|
+
pending: s.repos.pending,
|
|
22480
|
+
visits: s.routes.historyAll(),
|
|
22481
|
+
self: s.self.get(),
|
|
22482
|
+
update: s.update.get(),
|
|
22483
|
+
drafts: s.drafts.all()
|
|
22484
|
+
};
|
|
22485
|
+
};
|
|
21346
22486
|
let branded = false;
|
|
21347
22487
|
const serverConfig = {
|
|
21348
22488
|
hostname: cloud.bindHost,
|
|
@@ -21352,12 +22492,14 @@ function startServer(opts) {
|
|
|
21352
22492
|
version,
|
|
21353
22493
|
repos: s.repos,
|
|
21354
22494
|
attachments: s.attachments,
|
|
22495
|
+
archivedAttachment: (id, file) => s.worktrees.archivedAttachment(id, file),
|
|
21355
22496
|
branded: () => branded,
|
|
21356
22497
|
metrics,
|
|
21357
22498
|
noteShellOrigin: opts.noteShellOrigin,
|
|
21358
22499
|
remote: opts.remote,
|
|
21359
22500
|
preview: (id) => s.runtime.get(id)?.proxy?.handler ?? null,
|
|
21360
|
-
bootstrap: helloFrame
|
|
22501
|
+
bootstrap: helloFrame,
|
|
22502
|
+
restart: () => s.restarter.request()
|
|
21361
22503
|
}),
|
|
21362
22504
|
websocket: {
|
|
21363
22505
|
maxPayloadLength: 64 * 1024 * 1024,
|
|
@@ -21377,6 +22519,8 @@ function startServer(opts) {
|
|
|
21377
22519
|
close(ws) {
|
|
21378
22520
|
if (ws.data.preview)
|
|
21379
22521
|
ws.data.preview.handler.close(ws.data.preview.data);
|
|
22522
|
+
else
|
|
22523
|
+
s.idle.drop(ws.data);
|
|
21380
22524
|
sockets.delete(ws);
|
|
21381
22525
|
},
|
|
21382
22526
|
async message(ws, raw) {
|
|
@@ -21409,6 +22553,14 @@ function startServer(opts) {
|
|
|
21409
22553
|
},
|
|
21410
22554
|
unsubscribe: (id) => {
|
|
21411
22555
|
ws.data.subs.delete(id);
|
|
22556
|
+
if (ws.data.view === id) {
|
|
22557
|
+
ws.data.view = null;
|
|
22558
|
+
s.idle.view(ws.data, null);
|
|
22559
|
+
}
|
|
22560
|
+
},
|
|
22561
|
+
view: (id) => {
|
|
22562
|
+
ws.data.view = id;
|
|
22563
|
+
s.idle.view(ws.data, id);
|
|
21412
22564
|
},
|
|
21413
22565
|
watchTerminal: (id, stream) => {
|
|
21414
22566
|
ws.data.terms.add(streamKey(id, stream));
|
|
@@ -21424,7 +22576,13 @@ function startServer(opts) {
|
|
|
21424
22576
|
} catch (e) {
|
|
21425
22577
|
if (!(e instanceof UserError || e instanceof ThemeImportError))
|
|
21426
22578
|
log.error("ws", `${parsed.msg.t} failed`, e);
|
|
21427
|
-
|
|
22579
|
+
const m = parsed.msg;
|
|
22580
|
+
const worktreeId = "worktreeId" in m ? m.worktreeId : ("archiveId" in m) ? m.archiveId : undefined;
|
|
22581
|
+
send(ws, {
|
|
22582
|
+
t: "error",
|
|
22583
|
+
message: e instanceof Error ? e.message : String(e),
|
|
22584
|
+
...worktreeId ? { worktreeId } : {}
|
|
22585
|
+
});
|
|
21428
22586
|
}
|
|
21429
22587
|
}
|
|
21430
22588
|
}
|
|
@@ -21448,9 +22606,9 @@ function startServer(opts) {
|
|
|
21448
22606
|
}
|
|
21449
22607
|
|
|
21450
22608
|
// ../daemon/src/themes/store.ts
|
|
21451
|
-
import { existsSync as
|
|
22609
|
+
import { existsSync as existsSync21, readdirSync as readdirSync5, readFileSync as readFileSync13, statSync as statSync6, writeFileSync as writeFileSync7 } from "fs";
|
|
21452
22610
|
import { homedir as homedir10 } from "os";
|
|
21453
|
-
import { basename as basename4, dirname as dirname7, join as join18, resolve as
|
|
22611
|
+
import { basename as basename4, dirname as dirname7, join as join18, resolve as resolve8 } from "path";
|
|
21454
22612
|
|
|
21455
22613
|
// ../../node_modules/.bun/jsonc-parser@3.3.1/node_modules/jsonc-parser/lib/esm/impl/scanner.js
|
|
21456
22614
|
function createScanner(text, ignoreTrivia = false) {
|
|
@@ -22354,7 +23512,7 @@ class ThemeStore {
|
|
|
22354
23512
|
}
|
|
22355
23513
|
}
|
|
22356
23514
|
function readJsonc(file) {
|
|
22357
|
-
return parse7(
|
|
23515
|
+
return parse7(readFileSync13(file, "utf8"), [], { allowTrailingComma: true });
|
|
22358
23516
|
}
|
|
22359
23517
|
function readVscodeTheme(file, depth = 0) {
|
|
22360
23518
|
const json = readJsonc(file);
|
|
@@ -22362,7 +23520,7 @@ function readVscodeTheme(file, depth = 0) {
|
|
|
22362
23520
|
throw new ThemeImportError(`${file}: not a JSON object`);
|
|
22363
23521
|
const t = json;
|
|
22364
23522
|
if (typeof t.include === "string" && depth < 5) {
|
|
22365
|
-
const parent = readVscodeTheme(
|
|
23523
|
+
const parent = readVscodeTheme(resolve8(dirname7(file), t.include), depth + 1);
|
|
22366
23524
|
return {
|
|
22367
23525
|
...parent,
|
|
22368
23526
|
...t,
|
|
@@ -22376,10 +23534,10 @@ function readVscodeTheme(file, depth = 0) {
|
|
|
22376
23534
|
return t;
|
|
22377
23535
|
}
|
|
22378
23536
|
function loadDir(dir) {
|
|
22379
|
-
if (!
|
|
23537
|
+
if (!existsSync21(dir))
|
|
22380
23538
|
return [];
|
|
22381
23539
|
const out = [];
|
|
22382
|
-
for (const f of
|
|
23540
|
+
for (const f of readdirSync5(dir)) {
|
|
22383
23541
|
if (!/\.jsonc?$/i.test(f))
|
|
22384
23542
|
continue;
|
|
22385
23543
|
const file = join18(dir, f);
|
|
@@ -22398,12 +23556,12 @@ function loadDir(dir) {
|
|
|
22398
23556
|
return out;
|
|
22399
23557
|
}
|
|
22400
23558
|
function discover(dir) {
|
|
22401
|
-
if (!
|
|
23559
|
+
if (!existsSync21(dir))
|
|
22402
23560
|
return [];
|
|
22403
23561
|
const out = [];
|
|
22404
23562
|
let entries;
|
|
22405
23563
|
try {
|
|
22406
|
-
entries =
|
|
23564
|
+
entries = readdirSync5(dir);
|
|
22407
23565
|
} catch {
|
|
22408
23566
|
return out;
|
|
22409
23567
|
}
|
|
@@ -22411,18 +23569,18 @@ function discover(dir) {
|
|
|
22411
23569
|
const extDir = join18(dir, name);
|
|
22412
23570
|
const pkgFile = join18(extDir, "package.json");
|
|
22413
23571
|
try {
|
|
22414
|
-
if (!
|
|
23572
|
+
if (!statSync6(extDir).isDirectory() || !existsSync21(pkgFile))
|
|
22415
23573
|
continue;
|
|
22416
|
-
const pkg = JSON.parse(
|
|
23574
|
+
const pkg = JSON.parse(readFileSync13(pkgFile, "utf8"));
|
|
22417
23575
|
const themes = pkg.contributes?.themes;
|
|
22418
23576
|
if (!Array.isArray(themes))
|
|
22419
23577
|
continue;
|
|
22420
23578
|
const ext = `${pkg.publisher ?? "local"}.${pkg.name ?? name}`;
|
|
22421
23579
|
let nls = {};
|
|
22422
23580
|
const nlsFile = join18(extDir, "package.nls.json");
|
|
22423
|
-
if (
|
|
23581
|
+
if (existsSync21(nlsFile)) {
|
|
22424
23582
|
try {
|
|
22425
|
-
nls = JSON.parse(
|
|
23583
|
+
nls = JSON.parse(readFileSync13(nlsFile, "utf8"));
|
|
22426
23584
|
} catch {}
|
|
22427
23585
|
}
|
|
22428
23586
|
for (const c of themes) {
|
|
@@ -22431,7 +23589,7 @@ function discover(dir) {
|
|
|
22431
23589
|
const m = /^%(.+)%$/.exec(c.label);
|
|
22432
23590
|
if (m)
|
|
22433
23591
|
c.label = nls[m[1]] ?? m[1];
|
|
22434
|
-
const file =
|
|
23592
|
+
const file = resolve8(extDir, c.path);
|
|
22435
23593
|
try {
|
|
22436
23594
|
const json = readVscodeTheme(file);
|
|
22437
23595
|
out.push(vscodeToTheme(json, { id: `vscode:${ext}:${slug(c.label)}`, name: c.label, source: "vscode" }));
|
|
@@ -22446,6 +23604,334 @@ function discover(dir) {
|
|
|
22446
23604
|
return out;
|
|
22447
23605
|
}
|
|
22448
23606
|
|
|
23607
|
+
// ../daemon/src/update/infra.ts
|
|
23608
|
+
import { homedir as homedir11 } from "os";
|
|
23609
|
+
async function latestVersion() {
|
|
23610
|
+
const config = await run("npm", ["config", "get", "registry"], homedir11());
|
|
23611
|
+
const registry = config.ok && config.out !== "" ? config.out : "the npm registry";
|
|
23612
|
+
const r = await run("npm", ["view", "toyon", "version", "--json"], homedir11());
|
|
23613
|
+
if (!r.ok)
|
|
23614
|
+
return { version: null, registry };
|
|
23615
|
+
try {
|
|
23616
|
+
const v = JSON.parse(r.out);
|
|
23617
|
+
return { version: typeof v === "string" ? v : null, registry };
|
|
23618
|
+
} catch {
|
|
23619
|
+
return { version: null, registry };
|
|
23620
|
+
}
|
|
23621
|
+
}
|
|
23622
|
+
var LOG_POINTER = /complete log of this run/i;
|
|
23623
|
+
async function runInstall(command) {
|
|
23624
|
+
const [cmd, ...args] = command;
|
|
23625
|
+
if (!cmd)
|
|
23626
|
+
return { ok: false, line: "nothing to run" };
|
|
23627
|
+
const r = await runLive(cmd, args, homedir11());
|
|
23628
|
+
const lines = `${r.out}
|
|
23629
|
+
${r.err}`.split(`
|
|
23630
|
+
`).map((l) => l.trim()).filter((l) => l !== "" && !LOG_POINTER.test(l));
|
|
23631
|
+
return { ok: r.ok, line: lines.at(-1) ?? String(r.exit) };
|
|
23632
|
+
}
|
|
23633
|
+
|
|
23634
|
+
// ../daemon/src/update/installed.ts
|
|
23635
|
+
async function readVersion(path) {
|
|
23636
|
+
try {
|
|
23637
|
+
const json = await Bun.file(path).json();
|
|
23638
|
+
return typeof json.version === "string" ? json.version : null;
|
|
23639
|
+
} catch {
|
|
23640
|
+
return null;
|
|
23641
|
+
}
|
|
23642
|
+
}
|
|
23643
|
+
|
|
23644
|
+
// ../daemon/src/update/service.ts
|
|
23645
|
+
var REFRESH_EVERY_MS = 60000;
|
|
23646
|
+
var CHECK_EVERY_MS = 6 * 60 * 60000;
|
|
23647
|
+
var FIRST_CHECK_MS = 2 * 60000;
|
|
23648
|
+
var SETTLED_MS = 2 * 60000;
|
|
23649
|
+
var RETRY_AFTER_MS = 24 * 60 * 60000;
|
|
23650
|
+
|
|
23651
|
+
class UpdateService {
|
|
23652
|
+
d;
|
|
23653
|
+
installed = null;
|
|
23654
|
+
latest = null;
|
|
23655
|
+
unreachable = null;
|
|
23656
|
+
installing = false;
|
|
23657
|
+
failed = null;
|
|
23658
|
+
wanted = false;
|
|
23659
|
+
busyAt;
|
|
23660
|
+
announced;
|
|
23661
|
+
now;
|
|
23662
|
+
constructor(d) {
|
|
23663
|
+
this.d = d;
|
|
23664
|
+
this.now = d.now ?? Date.now;
|
|
23665
|
+
this.busyAt = this.now();
|
|
23666
|
+
this.announced = JSON.stringify(this.get());
|
|
23667
|
+
const every = d.setInterval ?? ((fn, ms) => {
|
|
23668
|
+
setInterval(fn, ms).unref?.();
|
|
23669
|
+
});
|
|
23670
|
+
const later = d.setTimeout ?? ((fn, ms) => {
|
|
23671
|
+
setTimeout(fn, ms).unref?.();
|
|
23672
|
+
});
|
|
23673
|
+
every(() => fireAndForget("update", this.tick(), "update tick"), REFRESH_EVERY_MS);
|
|
23674
|
+
every(() => fireAndForget("update", this.check(), "update check"), CHECK_EVERY_MS);
|
|
23675
|
+
later(() => fireAndForget("update", this.check(), "update check"), FIRST_CHECK_MS);
|
|
23676
|
+
d.hub.on("agentStatus", () => {
|
|
23677
|
+
this.busyAt = this.now();
|
|
23678
|
+
if (this.wanted)
|
|
23679
|
+
fireAndForget("update", this.proceed(), "update");
|
|
23680
|
+
});
|
|
23681
|
+
d.hub.on("holdsChanged", () => {
|
|
23682
|
+
this.busyAt = this.now();
|
|
23683
|
+
});
|
|
23684
|
+
}
|
|
23685
|
+
async tick() {
|
|
23686
|
+
await this.refresh();
|
|
23687
|
+
await this.auto();
|
|
23688
|
+
}
|
|
23689
|
+
async refresh() {
|
|
23690
|
+
const v = await this.d.installed();
|
|
23691
|
+
if (v === null)
|
|
23692
|
+
return;
|
|
23693
|
+
this.installed = v === this.d.running ? null : v;
|
|
23694
|
+
this.announce();
|
|
23695
|
+
}
|
|
23696
|
+
async check() {
|
|
23697
|
+
if (this.d.method === "none" || this.d.managed)
|
|
23698
|
+
return;
|
|
23699
|
+
const answer = await this.d.latest();
|
|
23700
|
+
if (answer.version === null) {
|
|
23701
|
+
this.unreachable = answer.registry;
|
|
23702
|
+
return;
|
|
23703
|
+
}
|
|
23704
|
+
this.unreachable = null;
|
|
23705
|
+
this.latest = newer(answer.version, this.d.running) ? answer.version : null;
|
|
23706
|
+
this.announce();
|
|
23707
|
+
await this.auto();
|
|
23708
|
+
}
|
|
23709
|
+
async updateNow() {
|
|
23710
|
+
if (this.d.managed)
|
|
23711
|
+
throw new UserError("Updates are turned off for this machine (TOYON_UPDATES=off)");
|
|
23712
|
+
const target = this.target();
|
|
23713
|
+
if (target === null)
|
|
23714
|
+
throw new UserError("Toyon is up to date");
|
|
23715
|
+
if (this.needsInstall() && this.d.command(target) === null) {
|
|
23716
|
+
throw new UserError(`Run npx toyon@${target} to use it`);
|
|
23717
|
+
}
|
|
23718
|
+
this.failed = null;
|
|
23719
|
+
this.wanted = true;
|
|
23720
|
+
await this.proceed();
|
|
23721
|
+
}
|
|
23722
|
+
get() {
|
|
23723
|
+
const restarting = this.d.restarter.waitingOn() ?? (this.wanted && !this.installing ? this.d.restarter.working() : null);
|
|
23724
|
+
if (this.installed === null && this.latest === null && !this.installing && this.failed === null && restarting === null) {
|
|
23725
|
+
return null;
|
|
23726
|
+
}
|
|
23727
|
+
return {
|
|
23728
|
+
running: this.d.running,
|
|
23729
|
+
latest: this.latest,
|
|
23730
|
+
installed: this.installed,
|
|
23731
|
+
method: this.d.method,
|
|
23732
|
+
installing: this.installing,
|
|
23733
|
+
failed: this.failed,
|
|
23734
|
+
restarting
|
|
23735
|
+
};
|
|
23736
|
+
}
|
|
23737
|
+
status() {
|
|
23738
|
+
return { managed: this.d.managed, unreachable: this.unreachable, latest: this.latest };
|
|
23739
|
+
}
|
|
23740
|
+
async auto() {
|
|
23741
|
+
if (this.wanted || this.installing || this.d.managed)
|
|
23742
|
+
return;
|
|
23743
|
+
const target = this.target();
|
|
23744
|
+
if (target === null)
|
|
23745
|
+
return;
|
|
23746
|
+
if (this.needsInstall() && this.d.command(target) === null)
|
|
23747
|
+
return;
|
|
23748
|
+
const failed = this.d.state.updateFailed;
|
|
23749
|
+
if (failed?.version === target && this.now() - failed.at < RETRY_AFTER_MS)
|
|
23750
|
+
return;
|
|
23751
|
+
if (this.d.busy()) {
|
|
23752
|
+
this.busyAt = this.now();
|
|
23753
|
+
return;
|
|
23754
|
+
}
|
|
23755
|
+
if (this.now() - this.busyAt < SETTLED_MS)
|
|
23756
|
+
return;
|
|
23757
|
+
log.info("update", `nothing is busy: updating to ${target}`);
|
|
23758
|
+
this.wanted = true;
|
|
23759
|
+
await this.proceed();
|
|
23760
|
+
}
|
|
23761
|
+
target() {
|
|
23762
|
+
if (this.latest && (this.installed === null || newer(this.latest, this.installed)))
|
|
23763
|
+
return this.latest;
|
|
23764
|
+
return this.installed;
|
|
23765
|
+
}
|
|
23766
|
+
needsInstall() {
|
|
23767
|
+
const target = this.target();
|
|
23768
|
+
return target !== null && target !== this.installed;
|
|
23769
|
+
}
|
|
23770
|
+
async proceed() {
|
|
23771
|
+
if (!this.wanted || this.installing)
|
|
23772
|
+
return;
|
|
23773
|
+
if (this.d.restarter.working().length > 0) {
|
|
23774
|
+
this.announce();
|
|
23775
|
+
return;
|
|
23776
|
+
}
|
|
23777
|
+
const target = this.target();
|
|
23778
|
+
const command = target === null ? null : this.d.command(target);
|
|
23779
|
+
if (target !== null && command !== null && this.needsInstall()) {
|
|
23780
|
+
this.installing = true;
|
|
23781
|
+
this.announce();
|
|
23782
|
+
log.info("update", `installing ${target}: ${command.join(" ")}`);
|
|
23783
|
+
const r = await this.d.install(command);
|
|
23784
|
+
this.installing = false;
|
|
23785
|
+
if (!r.ok) {
|
|
23786
|
+
this.wanted = false;
|
|
23787
|
+
this.failed = { version: target, line: r.line, command: command.join(" ") };
|
|
23788
|
+
this.d.state.setUpdateFailed({ version: target, at: this.now() });
|
|
23789
|
+
log.warn("update", `installing ${target} failed: ${r.line}`);
|
|
23790
|
+
this.announce();
|
|
23791
|
+
return;
|
|
23792
|
+
}
|
|
23793
|
+
await this.refresh();
|
|
23794
|
+
}
|
|
23795
|
+
this.wanted = false;
|
|
23796
|
+
const refused = this.d.restarter.request();
|
|
23797
|
+
if (refused) {
|
|
23798
|
+
this.failed = { version: target ?? this.d.running, line: refused, command: "toyon restart" };
|
|
23799
|
+
log.warn("update", `restart refused: ${refused}`);
|
|
23800
|
+
}
|
|
23801
|
+
this.announce();
|
|
23802
|
+
}
|
|
23803
|
+
announce() {
|
|
23804
|
+
const key = JSON.stringify(this.get());
|
|
23805
|
+
if (key === this.announced)
|
|
23806
|
+
return;
|
|
23807
|
+
this.announced = key;
|
|
23808
|
+
this.d.hub.emit("updateChanged");
|
|
23809
|
+
}
|
|
23810
|
+
}
|
|
23811
|
+
|
|
23812
|
+
// ../daemon/src/worktrees/chats.ts
|
|
23813
|
+
var CHAT_HITS_PER_WORKTREE = 20;
|
|
23814
|
+
var CHAT_HITS_MAX = 200;
|
|
23815
|
+
var MIN_QUERY = 2;
|
|
23816
|
+
var SNIPPET = 160;
|
|
23817
|
+
var BEFORE = 50;
|
|
23818
|
+
var bunFs2 = {
|
|
23819
|
+
async stat(path) {
|
|
23820
|
+
try {
|
|
23821
|
+
const s = await Bun.file(path).stat();
|
|
23822
|
+
return { size: s.size, mtimeMs: s.mtimeMs };
|
|
23823
|
+
} catch {
|
|
23824
|
+
return null;
|
|
23825
|
+
}
|
|
23826
|
+
},
|
|
23827
|
+
async text(path) {
|
|
23828
|
+
try {
|
|
23829
|
+
return await Bun.file(path).text();
|
|
23830
|
+
} catch {
|
|
23831
|
+
return null;
|
|
23832
|
+
}
|
|
23833
|
+
}
|
|
23834
|
+
};
|
|
23835
|
+
var flat = (text) => text.replace(/\s+/g, " ").trim();
|
|
23836
|
+
function saidRows(entries) {
|
|
23837
|
+
const out = [];
|
|
23838
|
+
let turnTs = 0;
|
|
23839
|
+
const push = (seq, role, said, ts) => {
|
|
23840
|
+
const text = flat(said);
|
|
23841
|
+
if (text)
|
|
23842
|
+
out.push({ seq, role, text, lower: text.toLowerCase(), ts });
|
|
23843
|
+
};
|
|
23844
|
+
for (const { seq, event } of coalesce(entries)) {
|
|
23845
|
+
if (event.type === "user-message") {
|
|
23846
|
+
turnTs = event.ts;
|
|
23847
|
+
push(seq, "user", event.text, event.ts);
|
|
23848
|
+
} else if (event.type === "turn-start") {
|
|
23849
|
+
turnTs = event.ts;
|
|
23850
|
+
} else if (event.type === "text-delta") {
|
|
23851
|
+
push(seq, "assistant", event.text, turnTs);
|
|
23852
|
+
}
|
|
23853
|
+
}
|
|
23854
|
+
return out;
|
|
23855
|
+
}
|
|
23856
|
+
function snippet(text, at, length) {
|
|
23857
|
+
const start = Math.max(0, Math.min(at - BEFORE, text.length - SNIPPET));
|
|
23858
|
+
const end = Math.min(text.length, start + SNIPPET);
|
|
23859
|
+
const head = start > 0 ? "\u2026" : "";
|
|
23860
|
+
const tail = end < text.length ? "\u2026" : "";
|
|
23861
|
+
return {
|
|
23862
|
+
text: `${head}${text.slice(start, end)}${tail}`,
|
|
23863
|
+
match: [at - start + head.length, Math.min(length, end - at)]
|
|
23864
|
+
};
|
|
23865
|
+
}
|
|
23866
|
+
function searchRows(rows, needle, chat, limit = CHAT_HITS_PER_WORKTREE) {
|
|
23867
|
+
const hits = [];
|
|
23868
|
+
for (let i = rows.length - 1;i >= 0; i--) {
|
|
23869
|
+
const row = rows[i];
|
|
23870
|
+
const at = row.lower.indexOf(needle);
|
|
23871
|
+
if (at < 0)
|
|
23872
|
+
continue;
|
|
23873
|
+
if (hits.length === limit)
|
|
23874
|
+
return { hits, more: true };
|
|
23875
|
+
hits.push({ ...chat, seq: row.seq, role: row.role, ts: row.ts, ...snippet(row.text, at, needle.length) });
|
|
23876
|
+
}
|
|
23877
|
+
return { hits, more: false };
|
|
23878
|
+
}
|
|
23879
|
+
|
|
23880
|
+
class ChatSearch {
|
|
23881
|
+
d;
|
|
23882
|
+
said = new Map;
|
|
23883
|
+
constructor(d) {
|
|
23884
|
+
this.d = d;
|
|
23885
|
+
}
|
|
23886
|
+
async search(repoId, query) {
|
|
23887
|
+
this.d.state.requireRepo(repoId);
|
|
23888
|
+
const needle = flat(query).toLowerCase();
|
|
23889
|
+
if (needle.length < MIN_QUERY)
|
|
23890
|
+
return { hits: [], truncated: false };
|
|
23891
|
+
const chats = [
|
|
23892
|
+
...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) })),
|
|
23893
|
+
...this.d.archivedChats(repoId).map((a) => ({ id: a.id, archived: true, path: a.transcript }))
|
|
23894
|
+
];
|
|
23895
|
+
const read = new Set;
|
|
23896
|
+
const found = await Promise.all(chats.map(async (c) => {
|
|
23897
|
+
const rows = await this.rowsOf(c.id, c.archived, c.path, read);
|
|
23898
|
+
return searchRows(rows, needle, { worktreeId: c.id, archived: c.archived });
|
|
23899
|
+
}));
|
|
23900
|
+
for (const key of this.said.keys())
|
|
23901
|
+
if (!read.has(key))
|
|
23902
|
+
this.said.delete(key);
|
|
23903
|
+
const all = found.filter((f) => f.hits.length > 0).sort((a, b) => b.hits[0].ts - a.hits[0].ts).flatMap((f) => f.hits);
|
|
23904
|
+
return { hits: all.slice(0, CHAT_HITS_MAX), truncated: all.length > CHAT_HITS_MAX || found.some((f) => f.more) };
|
|
23905
|
+
}
|
|
23906
|
+
async rowsOf(id, archived, path, read) {
|
|
23907
|
+
const live = archived ? undefined : this.d.live(id);
|
|
23908
|
+
if (live) {
|
|
23909
|
+
const key = `live:${id}`;
|
|
23910
|
+
read.add(key);
|
|
23911
|
+
return this.remember(key, `n:${live.length}`, () => saidRows(live));
|
|
23912
|
+
}
|
|
23913
|
+
read.add(path);
|
|
23914
|
+
const fs = this.d.fs ?? bunFs2;
|
|
23915
|
+
const at = await fs.stat(path);
|
|
23916
|
+
if (!at)
|
|
23917
|
+
return [];
|
|
23918
|
+
const sig = `${at.mtimeMs}:${at.size}`;
|
|
23919
|
+
const known = this.said.get(path);
|
|
23920
|
+
if (known?.sig === sig)
|
|
23921
|
+
return known.rows;
|
|
23922
|
+
const text = await fs.text(path);
|
|
23923
|
+
return text === null ? [] : this.remember(path, sig, () => saidRows(parseTranscript(text, id)));
|
|
23924
|
+
}
|
|
23925
|
+
remember(key, sig, rows) {
|
|
23926
|
+
const known = this.said.get(key);
|
|
23927
|
+
if (known?.sig === sig)
|
|
23928
|
+
return known.rows;
|
|
23929
|
+
const fresh = rows();
|
|
23930
|
+
this.said.set(key, { sig, rows: fresh });
|
|
23931
|
+
return fresh;
|
|
23932
|
+
}
|
|
23933
|
+
}
|
|
23934
|
+
|
|
22449
23935
|
// ../daemon/src/worktrees/landing.ts
|
|
22450
23936
|
var TAIL_CHARS = 400;
|
|
22451
23937
|
var DIFF_CHARS = 2000;
|
|
@@ -22918,11 +24404,11 @@ function rankRefs({ refs, merged, prs, query, defaultBranch, rowIdForPath }) {
|
|
|
22918
24404
|
}
|
|
22919
24405
|
|
|
22920
24406
|
// ../daemon/src/worktrees/service.ts
|
|
22921
|
-
import { existsSync as
|
|
24407
|
+
import { existsSync as existsSync23, lstatSync, readlinkSync, rmSync as rmSync5, symlinkSync, unlinkSync } from "fs";
|
|
22922
24408
|
import { basename as basename6, dirname as dirname8, join as join21 } from "path";
|
|
22923
24409
|
|
|
22924
24410
|
// ../daemon/src/git/archive.ts
|
|
22925
|
-
import { rmSync } from "fs";
|
|
24411
|
+
import { rmSync as rmSync3 } from "fs";
|
|
22926
24412
|
var archiveRef = (worktreeId) => `refs/toyon/archive/${worktreeId}`;
|
|
22927
24413
|
async function keepState(repoPath, wtPath, ref, indexFile) {
|
|
22928
24414
|
const head = await git(wtPath, "rev-parse", "--verify", "HEAD");
|
|
@@ -22959,7 +24445,7 @@ async function snapshotCommit(wtPath, head, indexFile) {
|
|
|
22959
24445
|
], wtPath);
|
|
22960
24446
|
return commit.ok ? commit.out : null;
|
|
22961
24447
|
} finally {
|
|
22962
|
-
|
|
24448
|
+
rmSync3(indexFile, { force: true });
|
|
22963
24449
|
}
|
|
22964
24450
|
}
|
|
22965
24451
|
async function checkOutKept(repoPath, wtPath, kept, branch) {
|
|
@@ -22975,6 +24461,24 @@ async function commitOf(repoPath, ref) {
|
|
|
22975
24461
|
const r = await git(repoPath, "rev-parse", "--verify", "--quiet", `${ref}^{commit}`);
|
|
22976
24462
|
return r.ok ? r.out : null;
|
|
22977
24463
|
}
|
|
24464
|
+
var landRef = (worktreeId, n) => `refs/toyon/lands/${worktreeId}/${n}`;
|
|
24465
|
+
async function landingMark(worktreePath, defaultBr) {
|
|
24466
|
+
const [tip, base] = await Promise.all([
|
|
24467
|
+
git(worktreePath, "rev-parse", "HEAD"),
|
|
24468
|
+
git(worktreePath, "merge-base", "HEAD", defaultBr)
|
|
24469
|
+
]);
|
|
24470
|
+
if (!tip.ok || !base.ok || !tip.out || tip.out === base.out)
|
|
24471
|
+
return null;
|
|
24472
|
+
return { base: base.out, tip: tip.out };
|
|
24473
|
+
}
|
|
24474
|
+
async function dropLandRefs(repoPath, worktreeId) {
|
|
24475
|
+
const r = await git(repoPath, "for-each-ref", "--format=%(refname)", `refs/toyon/lands/${worktreeId}/`);
|
|
24476
|
+
if (!r.ok || !r.out)
|
|
24477
|
+
return;
|
|
24478
|
+
for (const ref of r.out.split(`
|
|
24479
|
+
`).filter(Boolean))
|
|
24480
|
+
await git(repoPath, "update-ref", "-d", ref);
|
|
24481
|
+
}
|
|
22978
24482
|
|
|
22979
24483
|
// ../daemon/src/git/land.ts
|
|
22980
24484
|
async function commitWorktree(worktreePath, message) {
|
|
@@ -23186,42 +24690,8 @@ function compareUrl(remoteUrl, base, branch) {
|
|
|
23186
24690
|
return `https://github.com/${m[1]}/compare/${encodeURIComponent(base)}...${encodeURIComponent(branch)}?expand=1`;
|
|
23187
24691
|
}
|
|
23188
24692
|
|
|
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
24693
|
// ../daemon/src/worktrees/archive.ts
|
|
23224
|
-
import { existsSync as
|
|
24694
|
+
import { existsSync as existsSync22, mkdirSync as mkdirSync6, readdirSync as readdirSync6, readFileSync as readFileSync14, renameSync as renameSync4, rmSync as rmSync4, writeFileSync as writeFileSync8 } from "fs";
|
|
23225
24695
|
import { join as join19 } from "path";
|
|
23226
24696
|
class WorktreeArchive {
|
|
23227
24697
|
dir;
|
|
@@ -23229,12 +24699,12 @@ class WorktreeArchive {
|
|
|
23229
24699
|
constructor(dir) {
|
|
23230
24700
|
this.dir = dir;
|
|
23231
24701
|
mkdirSync6(dir, { recursive: true });
|
|
23232
|
-
for (const id of
|
|
24702
|
+
for (const id of readdirSync6(dir)) {
|
|
23233
24703
|
const file = this.filesOf(id).record;
|
|
23234
|
-
if (!
|
|
24704
|
+
if (!existsSync22(file))
|
|
23235
24705
|
continue;
|
|
23236
24706
|
try {
|
|
23237
|
-
this.records.set(id, JSON.parse(
|
|
24707
|
+
this.records.set(id, JSON.parse(readFileSync14(file, "utf8")));
|
|
23238
24708
|
} catch (e) {
|
|
23239
24709
|
log.warn("archive", `skipped an unreadable record: ${file}`, e);
|
|
23240
24710
|
}
|
|
@@ -23262,12 +24732,18 @@ class WorktreeArchive {
|
|
|
23262
24732
|
delete(id) {
|
|
23263
24733
|
if (!this.records.has(id))
|
|
23264
24734
|
return;
|
|
23265
|
-
|
|
24735
|
+
rmSync4(this.filesOf(id).dir, { recursive: true, force: true });
|
|
23266
24736
|
this.records.delete(id);
|
|
23267
24737
|
}
|
|
23268
24738
|
list(repo) {
|
|
23269
24739
|
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
24740
|
}
|
|
24741
|
+
chatFiles(id) {
|
|
24742
|
+
if (!this.records.has(id))
|
|
24743
|
+
return;
|
|
24744
|
+
const at = this.filesOf(id);
|
|
24745
|
+
return { transcript: at.transcript, attachments: at.attachments };
|
|
24746
|
+
}
|
|
23271
24747
|
filesOf(id) {
|
|
23272
24748
|
const dir = join19(this.dir, id);
|
|
23273
24749
|
return {
|
|
@@ -23284,18 +24760,37 @@ function summarize(r, repoId) {
|
|
|
23284
24760
|
repoId,
|
|
23285
24761
|
title: r.worktree.title,
|
|
23286
24762
|
branch: r.worktree.branch,
|
|
24763
|
+
path: r.worktree.path,
|
|
23287
24764
|
createdAt: r.worktree.createdAt,
|
|
23288
24765
|
archivedAt: r.archivedAt,
|
|
23289
24766
|
...r.prompt ? { prompt: r.prompt } : {},
|
|
23290
24767
|
restorable: !!r.kept,
|
|
23291
24768
|
...r.kept?.snapshot ? { uncommitted: true } : {},
|
|
23292
|
-
...r.worktree.landed ? { landed: true } : {}
|
|
24769
|
+
...r.worktree.landed ? { landed: true } : {},
|
|
24770
|
+
...r.cost !== undefined ? { cost: r.cost } : {},
|
|
24771
|
+
...r.auto ? { auto: r.auto } : {}
|
|
23293
24772
|
};
|
|
23294
24773
|
}
|
|
24774
|
+
function lastUsage(transcript) {
|
|
24775
|
+
if (!existsSync22(transcript))
|
|
24776
|
+
return null;
|
|
24777
|
+
const lines = readFileSync14(transcript, "utf8").split(`
|
|
24778
|
+
`);
|
|
24779
|
+
for (let i = lines.length - 1;i >= 0; i--) {
|
|
24780
|
+
if (!lines[i]?.includes('"usage"'))
|
|
24781
|
+
continue;
|
|
24782
|
+
try {
|
|
24783
|
+
const e = JSON.parse(lines[i]).event;
|
|
24784
|
+
if (e?.type === "usage")
|
|
24785
|
+
return { used: e.used, size: e.size, ...e.cost !== undefined ? { cost: e.cost } : {} };
|
|
24786
|
+
} catch {}
|
|
24787
|
+
}
|
|
24788
|
+
return null;
|
|
24789
|
+
}
|
|
23295
24790
|
function firstPrompt(transcript) {
|
|
23296
|
-
if (!
|
|
24791
|
+
if (!existsSync22(transcript))
|
|
23297
24792
|
return;
|
|
23298
|
-
for (const line of
|
|
24793
|
+
for (const line of readFileSync14(transcript, "utf8").split(`
|
|
23299
24794
|
`)) {
|
|
23300
24795
|
if (!line.includes('"user-message"'))
|
|
23301
24796
|
continue;
|
|
@@ -23308,12 +24803,69 @@ function firstPrompt(transcript) {
|
|
|
23308
24803
|
return;
|
|
23309
24804
|
}
|
|
23310
24805
|
function move(from, to) {
|
|
23311
|
-
if (!
|
|
24806
|
+
if (!existsSync22(from))
|
|
23312
24807
|
return;
|
|
23313
|
-
|
|
24808
|
+
rmSync4(to, { recursive: true, force: true });
|
|
23314
24809
|
renameSync4(from, to);
|
|
23315
24810
|
}
|
|
23316
24811
|
|
|
24812
|
+
// ../daemon/src/worktrees/archivedGit.ts
|
|
24813
|
+
class ArchivedGit {
|
|
24814
|
+
repoPath;
|
|
24815
|
+
kept;
|
|
24816
|
+
lands;
|
|
24817
|
+
defaultBr;
|
|
24818
|
+
constructor(repoPath, kept, lands, defaultBr) {
|
|
24819
|
+
this.repoPath = repoPath;
|
|
24820
|
+
this.kept = kept;
|
|
24821
|
+
this.lands = lands;
|
|
24822
|
+
this.defaultBr = defaultBr;
|
|
24823
|
+
}
|
|
24824
|
+
async tailBase() {
|
|
24825
|
+
const r = await git(this.repoPath, "merge-base", this.kept.head, this.defaultBr);
|
|
24826
|
+
return r.ok && r.out ? r.out : this.kept.head;
|
|
24827
|
+
}
|
|
24828
|
+
async status() {
|
|
24829
|
+
const base = await this.tailBase();
|
|
24830
|
+
const { head, snapshot } = this.kept;
|
|
24831
|
+
const [files, committed] = await Promise.all([
|
|
24832
|
+
snapshot ? filesBetween(this.repoPath, head, snapshot) : [],
|
|
24833
|
+
base === head ? [] : filesBetween(this.repoPath, base, head)
|
|
24834
|
+
]);
|
|
24835
|
+
return { files, committed, head: snapshot ?? head };
|
|
24836
|
+
}
|
|
24837
|
+
async log() {
|
|
24838
|
+
const seen = new Set;
|
|
24839
|
+
const unseen = (list) => {
|
|
24840
|
+
const out = list.filter((c) => !seen.has(c.sha));
|
|
24841
|
+
for (const c of out)
|
|
24842
|
+
seen.add(c.sha);
|
|
24843
|
+
return out;
|
|
24844
|
+
};
|
|
24845
|
+
const landed = [];
|
|
24846
|
+
for (const land of this.lands) {
|
|
24847
|
+
const commits = unseen(await logRange(this.repoPath, land.base, land.tip));
|
|
24848
|
+
landed.push(commits.map((c) => ({ ...c, landedAt: land.at })));
|
|
24849
|
+
}
|
|
24850
|
+
const base = await this.tailBase();
|
|
24851
|
+
const tail = base === this.kept.head ? [] : unseen(await logRange(this.repoPath, base, this.kept.head));
|
|
24852
|
+
return [...tail, ...landed.reverse().flat()];
|
|
24853
|
+
}
|
|
24854
|
+
commitFiles(sha) {
|
|
24855
|
+
return commitFiles(this.repoPath, sha);
|
|
24856
|
+
}
|
|
24857
|
+
async file(path, ref) {
|
|
24858
|
+
if (ref)
|
|
24859
|
+
return fileAtCommit(this.repoPath, ref, path);
|
|
24860
|
+
const base = await this.tailBase();
|
|
24861
|
+
const [before, after] = await Promise.all([
|
|
24862
|
+
gitRaw(this.repoPath, "show", `${base}:${path}`),
|
|
24863
|
+
gitRaw(this.repoPath, "show", `${this.kept.snapshot ?? this.kept.head}:${path}`)
|
|
24864
|
+
]);
|
|
24865
|
+
return { before: before.ok ? before.out : "", after: after.ok ? after.out : "" };
|
|
24866
|
+
}
|
|
24867
|
+
}
|
|
24868
|
+
|
|
23317
24869
|
// ../daemon/src/worktrees/spare.ts
|
|
23318
24870
|
import { join as join20 } from "path";
|
|
23319
24871
|
class SparePool {
|
|
@@ -23329,7 +24881,7 @@ class SparePool {
|
|
|
23329
24881
|
adopt(repoId) {
|
|
23330
24882
|
const persisted = this.d.state.worktrees.filter((w) => w.repoId === repoId && w.kind === "spare");
|
|
23331
24883
|
for (const extra of persisted.slice(1))
|
|
23332
|
-
fireAndForget(extra.id, this.d.
|
|
24884
|
+
fireAndForget(extra.id, this.d.discard(extra.id), "stale spare removal");
|
|
23333
24885
|
const spare = persisted[0];
|
|
23334
24886
|
if (!spare)
|
|
23335
24887
|
return;
|
|
@@ -23386,7 +24938,7 @@ class SparePool {
|
|
|
23386
24938
|
log.warn(repoId, "spare warm-up failed; rolling back", e);
|
|
23387
24939
|
this.spares.delete(repoId);
|
|
23388
24940
|
if (entry.worktreeId && this.d.state.worktree(entry.worktreeId)) {
|
|
23389
|
-
await this.d.
|
|
24941
|
+
await this.d.discard(entry.worktreeId).catch((re) => log.warn(repoId, "spare rollback failed", re));
|
|
23390
24942
|
}
|
|
23391
24943
|
}
|
|
23392
24944
|
}
|
|
@@ -23442,12 +24994,14 @@ class SparePool {
|
|
|
23442
24994
|
wt.title = slug;
|
|
23443
24995
|
wt.createdAt = Date.now();
|
|
23444
24996
|
this.d.state.save();
|
|
24997
|
+
fireAndForget(wt.id, this.d.runtime.wake(wt.id), "wake on claim");
|
|
23445
24998
|
fireAndForget(repoId, this.ensure(repoId), "spare warm-up");
|
|
23446
24999
|
return wt;
|
|
23447
25000
|
}
|
|
23448
25001
|
}
|
|
23449
25002
|
|
|
23450
25003
|
// ../daemon/src/worktrees/turns.ts
|
|
25004
|
+
var TURN_HOLD = "turn";
|
|
23451
25005
|
var RECAP_DELAY_MS = Number(process.env.TOYON_RECAP_DELAY_MS) || 2 * 60000;
|
|
23452
25006
|
|
|
23453
25007
|
class TurnService {
|
|
@@ -23485,11 +25039,14 @@ class TurnService {
|
|
|
23485
25039
|
onStatus(worktreeId, status) {
|
|
23486
25040
|
const prev = this.lastStatus.get(worktreeId) ?? "idle";
|
|
23487
25041
|
this.lastStatus.set(worktreeId, status);
|
|
23488
|
-
if (status === "working")
|
|
25042
|
+
if (status === "working") {
|
|
23489
25043
|
this.disarm(worktreeId);
|
|
25044
|
+
this.d.hold?.(worktreeId, TURN_HOLD);
|
|
25045
|
+
}
|
|
23490
25046
|
const edge = stopOf(prev, status);
|
|
23491
25047
|
if (!edge)
|
|
23492
25048
|
return;
|
|
25049
|
+
this.d.release?.(worktreeId, TURN_HOLD);
|
|
23493
25050
|
const wt = this.d.state.worktree(worktreeId);
|
|
23494
25051
|
if (!wt || wt.kind === "spare")
|
|
23495
25052
|
return;
|
|
@@ -23587,9 +25144,6 @@ function stopOf(prev, next) {
|
|
|
23587
25144
|
return "failed";
|
|
23588
25145
|
return null;
|
|
23589
25146
|
}
|
|
23590
|
-
function isUnseen(wt) {
|
|
23591
|
-
return wt.unread === true || wt.lastTurn != null && (wt.seenAt == null || wt.seenAt < wt.lastTurn.at);
|
|
23592
|
-
}
|
|
23593
25147
|
|
|
23594
25148
|
// ../daemon/src/worktrees/service.ts
|
|
23595
25149
|
var LOCAL_CONFIG_FILES = [".env", ".env.local", ".env.development", ".env.development.local", ".dev.vars"];
|
|
@@ -23599,12 +25153,22 @@ function withoutAttachments(event) {
|
|
|
23599
25153
|
const { attachments: _attachments, ...rest } = event;
|
|
23600
25154
|
return rest;
|
|
23601
25155
|
}
|
|
25156
|
+
function withCarry(context, c) {
|
|
25157
|
+
if (c.count === 0)
|
|
25158
|
+
return context;
|
|
25159
|
+
const files = `${c.count} uncommitted ${c.count === 1 ? "file" : "files"}`;
|
|
25160
|
+
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.]`;
|
|
25161
|
+
return context ? `${context}
|
|
25162
|
+
|
|
25163
|
+
${line}` : line;
|
|
25164
|
+
}
|
|
23602
25165
|
var FETCH_EVERY_MS = 5 * 60000;
|
|
23603
25166
|
|
|
23604
25167
|
class WorktreeService {
|
|
23605
25168
|
d;
|
|
23606
25169
|
spare;
|
|
23607
25170
|
archive;
|
|
25171
|
+
archiving = new Map;
|
|
23608
25172
|
countsCache = new Map;
|
|
23609
25173
|
discoverCache = new Map;
|
|
23610
25174
|
usage = new Map;
|
|
@@ -23618,8 +25182,8 @@ class WorktreeService {
|
|
|
23618
25182
|
runtime: d.runtime,
|
|
23619
25183
|
paths: d.paths,
|
|
23620
25184
|
setupAndStart: (wt, repo) => this.setupAndStart(wt, repo),
|
|
23621
|
-
|
|
23622
|
-
await this.
|
|
25185
|
+
discard: async (id) => {
|
|
25186
|
+
await this.discardWorktree(id);
|
|
23623
25187
|
}
|
|
23624
25188
|
});
|
|
23625
25189
|
for (const wt of d.state.worktrees)
|
|
@@ -23647,46 +25211,48 @@ class WorktreeService {
|
|
|
23647
25211
|
const repo = this.d.state.requireRepo(repoId);
|
|
23648
25212
|
const agent = this.d.agents.require(opts.agent ?? this.d.state.defaultAgent ?? DEFAULT_AGENT_ID).id;
|
|
23649
25213
|
const profile = this.checkProfile(repo, opts.profile);
|
|
23650
|
-
|
|
25214
|
+
const task = taskText(prompt, attachments);
|
|
25215
|
+
let slug = variant ? `${slugify2(task, false)}-v${variant.index}` : slugify2(task);
|
|
23651
25216
|
if (variant && (await git(repo.path, "show-ref", "--verify", `refs/heads/toyon/${slug}`)).ok) {
|
|
23652
25217
|
slug = `${slug}-${shortId().slice(0, 3)}`;
|
|
23653
25218
|
}
|
|
23654
25219
|
const branch = `toyon/${slug}`;
|
|
23655
|
-
|
|
23656
|
-
|
|
23657
|
-
|
|
25220
|
+
if (opts.carry && variant && variant.of > 1) {
|
|
25221
|
+
throw new UserError(`only a single worktree can take the changes on ${repo.defaultBranch}`);
|
|
25222
|
+
}
|
|
25223
|
+
const agentPrompt = variant && variant.of >= 2 ? [prompt, variantLens(variant.index)].filter(Boolean).join(`
|
|
23658
25224
|
|
|
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;
|
|
25225
|
+
`) : prompt;
|
|
25226
|
+
const claimed = await this.spare.claim(repoId, branch, slug);
|
|
25227
|
+
if (claimed) {
|
|
25228
|
+
if (variant)
|
|
25229
|
+
claimed.variant = variant;
|
|
25230
|
+
if (opts.createdBy)
|
|
25231
|
+
claimed.createdBy = opts.createdBy;
|
|
25232
|
+
claimed.promptedAt = claimed.createdAt;
|
|
25233
|
+
claimed.agent = agent;
|
|
25234
|
+
if (opts.mode)
|
|
25235
|
+
claimed.mode = opts.mode;
|
|
25236
|
+
if (opts.model)
|
|
25237
|
+
claimed.model = opts.model;
|
|
25238
|
+
if (opts.effort)
|
|
25239
|
+
claimed.effort = opts.effort;
|
|
25240
|
+
this.refreshLink(claimed);
|
|
25241
|
+
if (profile !== undefined && profile !== resolveRun(repo, claimed).profile) {
|
|
25242
|
+
claimed.profile = profile;
|
|
25243
|
+
this.restartProcs(claimed, repo);
|
|
23685
25244
|
}
|
|
25245
|
+
const carried = await this.carryMain(repo, claimed, !!opts.carry);
|
|
25246
|
+
this.d.state.save();
|
|
25247
|
+
this.d.hub.emit("worktreesChanged");
|
|
25248
|
+
this.d.runtime.ensureAgent(claimed).agent.send(agentPrompt, { context: withCarry(context, carried), attachments });
|
|
25249
|
+
this.scheduleNaming(claimed, task, variant);
|
|
25250
|
+
if (carried.unmoved)
|
|
25251
|
+
throw new UserError(carried.unmoved);
|
|
25252
|
+
return claimed;
|
|
23686
25253
|
}
|
|
23687
25254
|
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));
|
|
25255
|
+
await withRepoLock(repo.path, () => gitOrThrow(repo.path, "worktree", "add", "-b", branch, wtPath, repo.defaultBranch));
|
|
23690
25256
|
const wt = {
|
|
23691
25257
|
id: shortId(),
|
|
23692
25258
|
repoId,
|
|
@@ -23705,11 +25271,56 @@ ${variantLens(variant.index)}` : prompt;
|
|
|
23705
25271
|
...opts.model ? { model: opts.model } : {},
|
|
23706
25272
|
...opts.effort ? { effort: opts.effort } : {}
|
|
23707
25273
|
};
|
|
23708
|
-
this.
|
|
23709
|
-
this.
|
|
23710
|
-
this.
|
|
25274
|
+
const carried = await this.carryMain(repo, wt, !!opts.carry);
|
|
25275
|
+
this.launch(wt, repo, repo.path);
|
|
25276
|
+
this.d.runtime.ensureAgent(wt).agent.send(agentPrompt, { context: withCarry(context, carried), attachments });
|
|
25277
|
+
this.scheduleNaming(wt, task, variant);
|
|
25278
|
+
if (carried.unmoved)
|
|
25279
|
+
throw new UserError(carried.unmoved);
|
|
23711
25280
|
return wt;
|
|
23712
25281
|
}
|
|
25282
|
+
async carryMain(repo, wt, move) {
|
|
25283
|
+
const main = repo.defaultBranch;
|
|
25284
|
+
if (!move)
|
|
25285
|
+
return { branch: main, moved: false, count: (await statusFiles(repo.path)).length };
|
|
25286
|
+
let count = 0;
|
|
25287
|
+
const unmoved = await withRepoLock(repo.path, async () => {
|
|
25288
|
+
count = (await statusFiles(repo.path)).length;
|
|
25289
|
+
if (count === 0)
|
|
25290
|
+
return;
|
|
25291
|
+
const before = await git(repo.path, "rev-parse", "-q", "--verify", "refs/stash");
|
|
25292
|
+
const pushed = await git(repo.path, "stash", "push", "--include-untracked", "-m", `toyon: into ${wt.title}`);
|
|
25293
|
+
if (!pushed.ok)
|
|
25294
|
+
return `the changes on ${main} could not be moved: ${pushed.err.trim()}`;
|
|
25295
|
+
const after = await git(repo.path, "rev-parse", "-q", "--verify", "refs/stash");
|
|
25296
|
+
const sha = after.out.trim();
|
|
25297
|
+
if (!after.ok || before.ok && before.out.trim() === sha)
|
|
25298
|
+
return;
|
|
25299
|
+
const entry = async () => {
|
|
25300
|
+
const list = await git(repo.path, "stash", "list", "--format=%H");
|
|
25301
|
+
const i = list.out.split(`
|
|
25302
|
+
`).indexOf(sha);
|
|
25303
|
+
return i < 0 ? null : `stash@{${i}}`;
|
|
25304
|
+
};
|
|
25305
|
+
const applied = await git(wt.path, "stash", "apply", sha);
|
|
25306
|
+
if (applied.ok) {
|
|
25307
|
+
const ref = await entry();
|
|
25308
|
+
if (ref)
|
|
25309
|
+
await git(repo.path, "stash", "drop", ref);
|
|
25310
|
+
return;
|
|
25311
|
+
}
|
|
25312
|
+
log.warn(wt.id, `moving ${main}'s changes failed`, applied.err);
|
|
25313
|
+
await git(wt.path, "reset", "--hard");
|
|
25314
|
+
await git(wt.path, "clean", "-fd");
|
|
25315
|
+
const ref = await entry();
|
|
25316
|
+
const back = ref ? await git(repo.path, "stash", "pop", ref) : null;
|
|
25317
|
+
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}"`;
|
|
25318
|
+
});
|
|
25319
|
+
const mainWt = this.d.state.worktrees.find((w) => w.repoId === repo.id && w.kind === "main");
|
|
25320
|
+
if (mainWt)
|
|
25321
|
+
this.countsCache.delete(mainWt.id);
|
|
25322
|
+
return { branch: main, moved: count > 0 && !unmoved, count, unmoved };
|
|
25323
|
+
}
|
|
23713
25324
|
launch(wt, repo, depsSource, opts = {}) {
|
|
23714
25325
|
this.d.state.addWorktree(wt);
|
|
23715
25326
|
this.d.hub.emit("worktreesChanged");
|
|
@@ -23724,7 +25335,7 @@ ${variantLens(variant.index)}` : prompt;
|
|
|
23724
25335
|
const branch = kind === "pr" ? `pr/${number}` : ref;
|
|
23725
25336
|
const title = kind === "pr" ? `pr-${number}` : cleanTitle2(ref) || "branch";
|
|
23726
25337
|
let slug = title;
|
|
23727
|
-
if (
|
|
25338
|
+
if (existsSync23(join21(this.d.paths.worktreesDir, repo.name, slug)))
|
|
23728
25339
|
slug = `${slug}-${shortId().slice(0, 4)}`;
|
|
23729
25340
|
const wtPath = join21(this.d.paths.worktreesDir, repo.name, slug);
|
|
23730
25341
|
await withRepoLock(repo.path, async () => {
|
|
@@ -23859,6 +25470,8 @@ ${variantLens(variant.index)}` : prompt;
|
|
|
23859
25470
|
this.d.hub.emit("worktreesChanged");
|
|
23860
25471
|
}
|
|
23861
25472
|
scheduleNaming(wt, prompt, variant) {
|
|
25473
|
+
if (!prompt.trim())
|
|
25474
|
+
return;
|
|
23862
25475
|
if (!variant) {
|
|
23863
25476
|
fireAndForget(wt.id, (this.d.namer ?? makeNamer(this.d.runtime))(prompt, wt).then((name) => {
|
|
23864
25477
|
if (name)
|
|
@@ -23878,12 +25491,32 @@ ${variantLens(variant.index)}` : prompt;
|
|
|
23878
25491
|
}
|
|
23879
25492
|
}), "auto-naming");
|
|
23880
25493
|
}
|
|
23881
|
-
|
|
25494
|
+
archiveWorktree(worktreeId, opts = {}) {
|
|
25495
|
+
const running = this.archiving.get(worktreeId);
|
|
25496
|
+
if (running)
|
|
25497
|
+
return running;
|
|
23882
25498
|
const wt = this.d.state.worktree(worktreeId);
|
|
23883
|
-
if (!wt || !(
|
|
23884
|
-
return null;
|
|
25499
|
+
if (!wt || !canArchive(wt))
|
|
25500
|
+
return Promise.resolve(null);
|
|
25501
|
+
if (opts.still && !opts.still())
|
|
25502
|
+
return Promise.resolve(null);
|
|
25503
|
+
const done = this.takeDown(wt, true, opts.reason).finally(() => this.archiving.delete(worktreeId));
|
|
25504
|
+
this.archiving.set(worktreeId, done);
|
|
25505
|
+
return done;
|
|
25506
|
+
}
|
|
25507
|
+
archivingNow(worktreeId) {
|
|
25508
|
+
return this.archiving.get(worktreeId);
|
|
25509
|
+
}
|
|
25510
|
+
async discardWorktree(worktreeId) {
|
|
25511
|
+
const wt = this.d.state.worktree(worktreeId);
|
|
25512
|
+
if (!wt || !(canArchive(wt) || wt.kind === "spare"))
|
|
25513
|
+
return;
|
|
25514
|
+
await this.takeDown(wt, false);
|
|
25515
|
+
this.d.drafts?.drop(worktreeId);
|
|
25516
|
+
}
|
|
25517
|
+
async takeDown(wt, archive, reason) {
|
|
25518
|
+
const worktreeId = wt.id;
|
|
23885
25519
|
const repo = this.d.state.requireRepo(wt.repoId);
|
|
23886
|
-
const archive = wt.kind !== "spare" && opts.archive !== false;
|
|
23887
25520
|
await this.d.runtime.stop(worktreeId);
|
|
23888
25521
|
const kept = await withRepoLock(repo.path, async () => {
|
|
23889
25522
|
const k = archive ? await this.keep(repo, wt) : null;
|
|
@@ -23901,9 +25534,12 @@ ${variantLens(variant.index)}` : prompt;
|
|
|
23901
25534
|
releasePort(wt.proxyPort);
|
|
23902
25535
|
let archived = null;
|
|
23903
25536
|
if (archive)
|
|
23904
|
-
archived = this.archiveChat(wt, repo, kept, sessionId);
|
|
23905
|
-
else
|
|
25537
|
+
archived = this.archiveChat(wt, repo, kept, sessionId, reason);
|
|
25538
|
+
else {
|
|
23906
25539
|
this.deleteChat(worktreeId);
|
|
25540
|
+
if (wt.lands?.length)
|
|
25541
|
+
await dropLandRefs(repo.path, worktreeId);
|
|
25542
|
+
}
|
|
23907
25543
|
this.d.hub.emit("worktreesChanged");
|
|
23908
25544
|
return archived;
|
|
23909
25545
|
}
|
|
@@ -23916,16 +25552,19 @@ ${variantLens(variant.index)}` : prompt;
|
|
|
23916
25552
|
log.warn(wt.id, "could not keep its uncommitted changes: only its commits are archived");
|
|
23917
25553
|
return kept;
|
|
23918
25554
|
}
|
|
23919
|
-
archiveChat(wt, repo, kept, sessionId) {
|
|
25555
|
+
archiveChat(wt, repo, kept, sessionId, reason) {
|
|
23920
25556
|
const files = this.chatFiles(wt.id);
|
|
23921
25557
|
const prompt = firstPrompt(files.transcript);
|
|
25558
|
+
const cost = (this.usage.get(wt.id) ?? lastUsage(files.transcript))?.cost;
|
|
23922
25559
|
const rec = {
|
|
23923
25560
|
worktree: wt,
|
|
23924
25561
|
repoPath: repo.path,
|
|
23925
25562
|
archivedAt: Date.now(),
|
|
23926
25563
|
...sessionId ? { sessionId } : {},
|
|
23927
25564
|
...prompt ? { prompt } : {},
|
|
23928
|
-
...
|
|
25565
|
+
...cost !== undefined ? { cost } : {},
|
|
25566
|
+
...kept ? { kept } : {},
|
|
25567
|
+
...reason ? { auto: reason } : {}
|
|
23929
25568
|
};
|
|
23930
25569
|
try {
|
|
23931
25570
|
this.archive.put(rec, files);
|
|
@@ -23945,8 +25584,8 @@ ${variantLens(variant.index)}` : prompt;
|
|
|
23945
25584
|
deleteChat(worktreeId) {
|
|
23946
25585
|
const files = this.chatFiles(worktreeId);
|
|
23947
25586
|
try {
|
|
23948
|
-
|
|
23949
|
-
|
|
25587
|
+
rmSync5(files.transcript, { force: true });
|
|
25588
|
+
rmSync5(files.attachments, { recursive: true, force: true });
|
|
23950
25589
|
} catch (e) {
|
|
23951
25590
|
log.warn(worktreeId, "could not delete transcript or attachments", e);
|
|
23952
25591
|
}
|
|
@@ -23971,14 +25610,56 @@ ${variantLens(variant.index)}` : prompt;
|
|
|
23971
25610
|
});
|
|
23972
25611
|
this.archiveChat(wt, repo, kept, sessionId);
|
|
23973
25612
|
}
|
|
25613
|
+
hasArchived(archiveId) {
|
|
25614
|
+
return !!this.archive.get(archiveId);
|
|
25615
|
+
}
|
|
23974
25616
|
archived(repoId) {
|
|
23975
25617
|
const repo = this.d.state.repo(repoId);
|
|
23976
25618
|
return repo ? this.archive.list(repo) : [];
|
|
23977
25619
|
}
|
|
25620
|
+
archivedChats(repoId) {
|
|
25621
|
+
const repo = this.d.state.repo(repoId);
|
|
25622
|
+
if (!repo)
|
|
25623
|
+
return [];
|
|
25624
|
+
return this.archive.list(repo).flatMap((a) => {
|
|
25625
|
+
const files = this.archive.chatFiles(a.id);
|
|
25626
|
+
return files ? [{ id: a.id, transcript: files.transcript }] : [];
|
|
25627
|
+
});
|
|
25628
|
+
}
|
|
25629
|
+
archivedTranscript(archiveId) {
|
|
25630
|
+
const files = this.archive.chatFiles(archiveId);
|
|
25631
|
+
return files ? new Transcript(files.transcript, archiveId).entries : null;
|
|
25632
|
+
}
|
|
25633
|
+
archivedAttachment(archiveId, file) {
|
|
25634
|
+
const files = this.archive.chatFiles(archiveId);
|
|
25635
|
+
return files && isAttachmentFile(file) ? join21(files.attachments, file) : null;
|
|
25636
|
+
}
|
|
25637
|
+
archivedGit(archiveId) {
|
|
25638
|
+
const rec = this.archive.get(archiveId);
|
|
25639
|
+
const repo = rec && this.repoOf(rec);
|
|
25640
|
+
if (!rec?.kept || !repo)
|
|
25641
|
+
return null;
|
|
25642
|
+
return new ArchivedGit(repo.path, rec.kept, rec.worktree.lands ?? [], repo.defaultBranch);
|
|
25643
|
+
}
|
|
25644
|
+
async archivedStatus(archiveId) {
|
|
25645
|
+
const kept = this.archivedGit(archiveId);
|
|
25646
|
+
if (!kept)
|
|
25647
|
+
return null;
|
|
25648
|
+
try {
|
|
25649
|
+
return await kept.status();
|
|
25650
|
+
} catch (e) {
|
|
25651
|
+
log.warn(archiveId, "reading its kept changes failed", e);
|
|
25652
|
+
return null;
|
|
25653
|
+
}
|
|
25654
|
+
}
|
|
25655
|
+
async archivedFile(archiveId, path, ref) {
|
|
25656
|
+
const kept = this.archivedGit(archiveId);
|
|
25657
|
+
return kept ? kept.file(path, ref) : null;
|
|
25658
|
+
}
|
|
23978
25659
|
repoOf(rec) {
|
|
23979
25660
|
return this.d.state.repo(rec.worktree.repoId) ?? this.d.state.repos.find((r) => r.path === rec.repoPath);
|
|
23980
25661
|
}
|
|
23981
|
-
async restore(archiveId, createdBy) {
|
|
25662
|
+
async restore(archiveId, createdBy, message) {
|
|
23982
25663
|
const rec = this.archive.get(archiveId);
|
|
23983
25664
|
if (!rec)
|
|
23984
25665
|
throw new UserError("that archived worktree is gone");
|
|
@@ -23989,7 +25670,9 @@ ${variantLens(variant.index)}` : prompt;
|
|
|
23989
25670
|
const kept = rec.kept;
|
|
23990
25671
|
if (!kept)
|
|
23991
25672
|
throw new UserError(`${old.title} was archived without its commits, so there is nothing to restore`);
|
|
23992
|
-
|
|
25673
|
+
if (rec.auto)
|
|
25674
|
+
log.info(old.id, `restoring a worktree that archived itself (${rec.auto})`);
|
|
25675
|
+
const path = existsSync23(old.path) ? join21(dirname8(old.path), `${basename6(old.path)}-${shortId().slice(0, 4)}`) : old.path;
|
|
23993
25676
|
const branch = await withRepoLock(repo.path, async () => {
|
|
23994
25677
|
if (!await commitOf(repo.path, archiveRef(old.id))) {
|
|
23995
25678
|
throw new UserError(`${old.title}'s kept commits are gone from git`);
|
|
@@ -24038,6 +25721,8 @@ ${variantLens(variant.index)}` : prompt;
|
|
|
24038
25721
|
this.countsCache.delete(wt.id);
|
|
24039
25722
|
this.refreshLink(wt);
|
|
24040
25723
|
this.launch(wt, repo, repo.path);
|
|
25724
|
+
if (message)
|
|
25725
|
+
this.d.runtime.ensureAgent(wt).agent.send(message.text, { attachments: message.attachments });
|
|
24041
25726
|
this.d.hub.emit("archiveChanged", repo.id);
|
|
24042
25727
|
return wt;
|
|
24043
25728
|
}
|
|
@@ -24047,12 +25732,15 @@ ${variantLens(variant.index)}` : prompt;
|
|
|
24047
25732
|
return;
|
|
24048
25733
|
const repo = this.repoOf(rec);
|
|
24049
25734
|
const repoPath = repo?.path ?? rec.repoPath;
|
|
24050
|
-
if (rec.kept &&
|
|
25735
|
+
if (rec.kept && existsSync23(repoPath)) {
|
|
24051
25736
|
const r = await git(repoPath, "update-ref", "-d", archiveRef(archiveId));
|
|
24052
25737
|
if (!r.ok)
|
|
24053
25738
|
log.warn(archiveId, `could not drop its archive ref: ${r.err}`);
|
|
24054
25739
|
}
|
|
25740
|
+
if (rec.worktree.lands?.length && existsSync23(repoPath))
|
|
25741
|
+
await dropLandRefs(repoPath, archiveId);
|
|
24055
25742
|
this.archive.delete(archiveId);
|
|
25743
|
+
this.d.drafts?.drop(archiveId);
|
|
24056
25744
|
if (repo)
|
|
24057
25745
|
this.d.hub.emit("archiveChanged", repo.id);
|
|
24058
25746
|
}
|
|
@@ -24122,7 +25810,7 @@ ${variantLens(variant.index)}` : prompt;
|
|
|
24122
25810
|
const group = wt.variant.group;
|
|
24123
25811
|
const siblings = this.d.state.worktrees.filter((w) => w.variant?.group === group && w.id !== worktreeId);
|
|
24124
25812
|
for (const sibling of siblings) {
|
|
24125
|
-
await this.
|
|
25813
|
+
await this.archiveWorktree(sibling.id).catch((e) => log.warn(sibling.id, "could not archive variant sibling", e));
|
|
24126
25814
|
}
|
|
24127
25815
|
delete wt.variant;
|
|
24128
25816
|
this.d.state.save();
|
|
@@ -24163,20 +25851,28 @@ ${variantLens(variant.index)}` : prompt;
|
|
|
24163
25851
|
for (const w of sources) {
|
|
24164
25852
|
const entries = this.d.runtime.ensureAgent(w).agent.transcript();
|
|
24165
25853
|
agent.note({ type: "grafted", title: w.title, branch: w.branch, ts: Date.now() });
|
|
24166
|
-
for (const { event } of
|
|
25854
|
+
for (const { event } of coalesce(entries))
|
|
24167
25855
|
agent.note(withoutAttachments(event));
|
|
24168
25856
|
}
|
|
24169
25857
|
for (const w of sources)
|
|
24170
|
-
await this.
|
|
25858
|
+
await this.discardWorktree(w.id);
|
|
24171
25859
|
this.setLanded(target.id, false);
|
|
24172
25860
|
this.countsCache.delete(target.id);
|
|
24173
25861
|
this.d.hub.emit("worktreesChanged");
|
|
24174
25862
|
return { target, grafted: sources.map((w) => w.title) };
|
|
24175
25863
|
}
|
|
24176
|
-
async setupAndStart(wt, repo, depsSource = repo.path,
|
|
25864
|
+
async setupAndStart(wt, repo, depsSource = repo.path, opts = {}) {
|
|
25865
|
+
this.d.runtime.markSetup(wt.id, true);
|
|
25866
|
+
try {
|
|
25867
|
+
await this.setupThenStart(wt, repo, depsSource, opts);
|
|
25868
|
+
} finally {
|
|
25869
|
+
this.d.runtime.markSetup(wt.id, false);
|
|
25870
|
+
}
|
|
25871
|
+
}
|
|
25872
|
+
async setupThenStart(wt, repo, depsSource, { setupCommands = true }) {
|
|
24177
25873
|
const srcNm = join21(depsSource, "node_modules");
|
|
24178
25874
|
const dstNm = join21(wt.path, "node_modules");
|
|
24179
|
-
if (
|
|
25875
|
+
if (existsSync23(srcNm) && !existsSync23(dstNm)) {
|
|
24180
25876
|
const started = Date.now();
|
|
24181
25877
|
const attempts = [
|
|
24182
25878
|
["clonefile", ["-Rc", srcNm, dstNm]],
|
|
@@ -24193,7 +25889,7 @@ ${variantLens(variant.index)}` : prompt;
|
|
|
24193
25889
|
}
|
|
24194
25890
|
for (const f of LOCAL_CONFIG_FILES) {
|
|
24195
25891
|
const src = join21(depsSource, f);
|
|
24196
|
-
if (!
|
|
25892
|
+
if (!existsSync23(src) || existsSync23(join21(wt.path, f)))
|
|
24197
25893
|
continue;
|
|
24198
25894
|
const r = await run("cp", [src, join21(wt.path, f)], wt.path);
|
|
24199
25895
|
if (r.ok)
|
|
@@ -24313,12 +26009,14 @@ ${wt.landing.body}` : wt.landing.subject : undefined;
|
|
|
24313
26009
|
const taken = await takeMainIn(wt.path, repo.defaultBranch, own);
|
|
24314
26010
|
if (!taken.ok)
|
|
24315
26011
|
return taken;
|
|
26012
|
+
const mark = await landingMark(wt.path, repo.defaultBranch);
|
|
24316
26013
|
const method = policy.merge ?? DEFAULT_MERGE_METHOD;
|
|
24317
26014
|
const squash = method === "squash" ? await squashMessage(wt.path, repo.defaultBranch, suggested) : "";
|
|
24318
26015
|
const landed = await landLocally(wt.path, wt.branch, repo.path, repo.defaultBranch, method, squash);
|
|
24319
26016
|
if (!landed.ok)
|
|
24320
26017
|
return landed;
|
|
24321
26018
|
mergedHere = true;
|
|
26019
|
+
await this.noteLand(repo, wt, mark);
|
|
24322
26020
|
await this.restartFromMain(wt, repo.defaultBranch);
|
|
24323
26021
|
if (policy.land === "push") {
|
|
24324
26022
|
const pushed = await pushMain(repo.path, repo.defaultBranch);
|
|
@@ -24334,9 +26032,21 @@ ${wt.landing.body}` : wt.landing.subject : undefined;
|
|
|
24334
26032
|
}
|
|
24335
26033
|
if (!result.ok)
|
|
24336
26034
|
return { result };
|
|
24337
|
-
const
|
|
26035
|
+
const archiveIds = wt.variant ? this.d.state.worktrees.filter((w) => w.variant?.group === wt.variant?.group && w.id !== wt.id).map((w) => w.id) : [];
|
|
24338
26036
|
const where = policy.land === "push" ? `${repo.defaultBranch}, pushed` : repo.defaultBranch;
|
|
24339
|
-
return { result: { ...result, message: `${wt.title} is on ${where}` },
|
|
26037
|
+
return { result: { ...result, message: `${wt.title} is on ${where}` }, archiveIds };
|
|
26038
|
+
}
|
|
26039
|
+
async noteLand(repo, wt, mark) {
|
|
26040
|
+
if (!mark)
|
|
26041
|
+
return;
|
|
26042
|
+
const n = wt.lands?.length ?? 0;
|
|
26043
|
+
const pinned = await git(repo.path, "update-ref", landRef(wt.id, n), mark.tip);
|
|
26044
|
+
if (!pinned.ok) {
|
|
26045
|
+
log.warn(wt.id, `could not keep the commits it landed: ${pinned.err}`);
|
|
26046
|
+
return;
|
|
26047
|
+
}
|
|
26048
|
+
wt.lands = [...wt.lands ?? [], { ...mark, at: Date.now() }];
|
|
26049
|
+
this.d.state.save();
|
|
24340
26050
|
}
|
|
24341
26051
|
async restartFromMain(wt, defaultBr) {
|
|
24342
26052
|
if (!hasOwnBranch(wt))
|
|
@@ -24349,9 +26059,11 @@ ${wt.landing.body}` : wt.landing.subject : undefined;
|
|
|
24349
26059
|
async prMerged(worktreeId) {
|
|
24350
26060
|
const { wt, repo } = this.d.state.requireWorktreeWithRepo(worktreeId);
|
|
24351
26061
|
const result = await withRepoLock(repo.path, async () => {
|
|
26062
|
+
const mark = await landingMark(wt.path, repo.defaultBranch);
|
|
24352
26063
|
const pulled = await fastForwardMain(repo.path, repo.defaultBranch);
|
|
24353
26064
|
if (!pulled.ok)
|
|
24354
26065
|
return pulled;
|
|
26066
|
+
await this.noteLand(repo, wt, mark);
|
|
24355
26067
|
await this.restartFromMain(wt, repo.defaultBranch);
|
|
24356
26068
|
return pulled;
|
|
24357
26069
|
});
|
|
@@ -24430,21 +26142,7 @@ ${wt.landing.body}` : wt.landing.subject : undefined;
|
|
|
24430
26142
|
const known = this.usage.get(worktreeId);
|
|
24431
26143
|
if (known !== undefined)
|
|
24432
26144
|
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
|
-
}
|
|
26145
|
+
const found = lastUsage(transcriptPathFor(this.d.paths.transcriptsDir, worktreeId));
|
|
24448
26146
|
this.usage.set(worktreeId, found);
|
|
24449
26147
|
return found ?? undefined;
|
|
24450
26148
|
}
|
|
@@ -24513,6 +26211,14 @@ ${wt.landing.body}` : wt.landing.subject : undefined;
|
|
|
24513
26211
|
return cached ?? {};
|
|
24514
26212
|
}
|
|
24515
26213
|
}
|
|
26214
|
+
async freshCounts(worktreeId) {
|
|
26215
|
+
const wt = this.d.state.worktree(worktreeId);
|
|
26216
|
+
const repo = wt && this.d.state.repo(wt.repoId);
|
|
26217
|
+
if (!wt || !repo)
|
|
26218
|
+
return {};
|
|
26219
|
+
this.countsCache.delete(worktreeId);
|
|
26220
|
+
return this.counts(wt.id, wt.path, repo.defaultBranch, true);
|
|
26221
|
+
}
|
|
24516
26222
|
readable(id) {
|
|
24517
26223
|
const wt = this.d.state.worktree(id);
|
|
24518
26224
|
if (wt) {
|
|
@@ -24538,7 +26244,7 @@ ${wt.landing.body}` : wt.landing.subject : undefined;
|
|
|
24538
26244
|
async gitStatus(worktreeId) {
|
|
24539
26245
|
const r = this.readable(worktreeId);
|
|
24540
26246
|
if (!r)
|
|
24541
|
-
return
|
|
26247
|
+
return this.archivedStatus(worktreeId);
|
|
24542
26248
|
try {
|
|
24543
26249
|
const isMain = r.wt?.kind === "main";
|
|
24544
26250
|
const [files, counts, head] = await Promise.all([
|
|
@@ -24576,11 +26282,15 @@ ${wt.landing.body}` : wt.landing.subject : undefined;
|
|
|
24576
26282
|
}
|
|
24577
26283
|
async gitLog(worktreeId) {
|
|
24578
26284
|
const r = this.readable(worktreeId);
|
|
24579
|
-
|
|
26285
|
+
if (r)
|
|
26286
|
+
return logCommits(r.path, r.defaultBranch);
|
|
26287
|
+
return await this.archivedGit(worktreeId)?.log() ?? [];
|
|
24580
26288
|
}
|
|
24581
26289
|
async commitFiles(worktreeId, sha) {
|
|
24582
26290
|
const r = this.readable(worktreeId);
|
|
24583
|
-
|
|
26291
|
+
if (r)
|
|
26292
|
+
return commitFiles(r.path, sha);
|
|
26293
|
+
return await this.archivedGit(worktreeId)?.commitFiles(sha) ?? [];
|
|
24584
26294
|
}
|
|
24585
26295
|
markPrompted(worktreeId) {
|
|
24586
26296
|
const wt = this.d.state.worktree(worktreeId);
|
|
@@ -24647,11 +26357,115 @@ ${wt.landing.body}` : wt.landing.subject : undefined;
|
|
|
24647
26357
|
}
|
|
24648
26358
|
}
|
|
24649
26359
|
|
|
26360
|
+
// ../daemon/src/worktrees/sweep.ts
|
|
26361
|
+
var ARCHIVE_AFTER_MS = 2 * 60 * 60000;
|
|
26362
|
+
var SWEEP_EVERY_MS = 10 * 60000;
|
|
26363
|
+
function archiveAfterFrom(raw) {
|
|
26364
|
+
if (raw === "off")
|
|
26365
|
+
return null;
|
|
26366
|
+
const n = Number(raw);
|
|
26367
|
+
return raw && Number.isFinite(n) && n > 0 ? n : ARCHIVE_AFTER_MS;
|
|
26368
|
+
}
|
|
26369
|
+
|
|
26370
|
+
class ArchiveSweep {
|
|
26371
|
+
d;
|
|
26372
|
+
timer = null;
|
|
26373
|
+
running = false;
|
|
26374
|
+
again = false;
|
|
26375
|
+
afterMs;
|
|
26376
|
+
constructor(d) {
|
|
26377
|
+
this.d = d;
|
|
26378
|
+
this.afterMs = d.afterMs === undefined ? ARCHIVE_AFTER_MS : d.afterMs;
|
|
26379
|
+
if (this.afterMs === null)
|
|
26380
|
+
return;
|
|
26381
|
+
const every = d.setInterval ?? ((fn, ms) => {
|
|
26382
|
+
const t = setInterval(fn, ms);
|
|
26383
|
+
t.unref?.();
|
|
26384
|
+
return t;
|
|
26385
|
+
});
|
|
26386
|
+
this.timer = every(() => this.request(), Math.min(d.everyMs ?? SWEEP_EVERY_MS, this.afterMs));
|
|
26387
|
+
}
|
|
26388
|
+
stop() {
|
|
26389
|
+
if (this.timer && !this.d.setInterval)
|
|
26390
|
+
clearInterval(this.timer);
|
|
26391
|
+
this.timer = null;
|
|
26392
|
+
}
|
|
26393
|
+
request() {
|
|
26394
|
+
if (this.running) {
|
|
26395
|
+
this.again = true;
|
|
26396
|
+
return;
|
|
26397
|
+
}
|
|
26398
|
+
this.running = true;
|
|
26399
|
+
fireAndForget("sweep", this.sweep().finally(() => {
|
|
26400
|
+
this.running = false;
|
|
26401
|
+
if (!this.again)
|
|
26402
|
+
return;
|
|
26403
|
+
this.again = false;
|
|
26404
|
+
this.request();
|
|
26405
|
+
}), "archive sweep");
|
|
26406
|
+
}
|
|
26407
|
+
async sweep() {
|
|
26408
|
+
if (this.afterMs === null)
|
|
26409
|
+
return;
|
|
26410
|
+
for (const repo of this.d.state.repos) {
|
|
26411
|
+
const beforeGit = this.facts(repo.id, () => ({ dirty: 0, ahead: 0 }));
|
|
26412
|
+
const maybe = beforeGit.rows.filter((w) => archiveReason(w, beforeGit) !== null);
|
|
26413
|
+
if (maybe.length === 0)
|
|
26414
|
+
continue;
|
|
26415
|
+
const fresh = new Map;
|
|
26416
|
+
for (const w of beforeGit.rows) {
|
|
26417
|
+
if (!maybe.some((m) => m.id === w.id || !!m.variant && m.variant.group === w.variant?.group))
|
|
26418
|
+
continue;
|
|
26419
|
+
fresh.set(w.id, await this.d.worktrees.freshCounts(w.id));
|
|
26420
|
+
}
|
|
26421
|
+
const counts = (id) => fresh.get(id) ?? {};
|
|
26422
|
+
for (const w of maybe) {
|
|
26423
|
+
const reason = archiveReason(w, this.facts(repo.id, counts));
|
|
26424
|
+
if (!reason)
|
|
26425
|
+
continue;
|
|
26426
|
+
log.info(w.id, `archiving on its own: ${reason}`);
|
|
26427
|
+
try {
|
|
26428
|
+
await this.d.worktrees.archiveWorktree(w.id, {
|
|
26429
|
+
reason,
|
|
26430
|
+
still: () => this.stillGoes(repo.id, w.id, counts)
|
|
26431
|
+
});
|
|
26432
|
+
} catch (e) {
|
|
26433
|
+
log.warn(w.id, "could not archive it", e);
|
|
26434
|
+
}
|
|
26435
|
+
}
|
|
26436
|
+
}
|
|
26437
|
+
}
|
|
26438
|
+
facts(repoId, counts) {
|
|
26439
|
+
return {
|
|
26440
|
+
now: (this.d.now ?? Date.now)(),
|
|
26441
|
+
afterMs: this.afterMs ?? ARCHIVE_AFTER_MS,
|
|
26442
|
+
rows: this.d.state.worktrees.filter((w) => w.repoId === repoId && w.kind !== "spare"),
|
|
26443
|
+
viewed: this.d.viewed,
|
|
26444
|
+
pending: this.d.busy,
|
|
26445
|
+
drafted: (id) => this.d.drafts.has(id),
|
|
26446
|
+
counts
|
|
26447
|
+
};
|
|
26448
|
+
}
|
|
26449
|
+
stillGoes(repoId, id, counts) {
|
|
26450
|
+
const f = this.facts(repoId, counts);
|
|
26451
|
+
const wt = f.rows.find((w) => w.id === id);
|
|
26452
|
+
return !!wt && archiveReason(wt, f) !== null;
|
|
26453
|
+
}
|
|
26454
|
+
}
|
|
26455
|
+
|
|
24650
26456
|
// ../daemon/src/index.ts
|
|
24651
26457
|
process.on("unhandledRejection", (e) => log.error("daemon", "unhandled rejection", e));
|
|
24652
26458
|
process.on("uncaughtException", (e) => log.error("daemon", "uncaught exception", e));
|
|
24653
26459
|
var here = dirname9(fileURLToPath(import.meta.url));
|
|
24654
|
-
var {
|
|
26460
|
+
var {
|
|
26461
|
+
shellDist: SHELL_DIST,
|
|
26462
|
+
bridgeJs: BRIDGE_JS,
|
|
26463
|
+
sourceRoot: SOURCE_ROOT,
|
|
26464
|
+
packageJson: PACKAGE_JSON
|
|
26465
|
+
} = locateAssets(here);
|
|
26466
|
+
var pruned = pruneAssets(SHELL_DIST);
|
|
26467
|
+
if (pruned > 0)
|
|
26468
|
+
log.debug("daemon", `pruned ${pruned} assets from builds this one has outlived`);
|
|
24655
26469
|
var paths = makePaths();
|
|
24656
26470
|
ensureDirs(paths);
|
|
24657
26471
|
var token = loadOrCreateToken(paths);
|
|
@@ -24697,12 +26511,15 @@ var runtime = new RuntimeRegistry({
|
|
|
24697
26511
|
remote,
|
|
24698
26512
|
grant: previewGrant(token)
|
|
24699
26513
|
});
|
|
24700
|
-
var
|
|
26514
|
+
var drafts = new DraftStore({ file: paths.draftsFile, hub });
|
|
26515
|
+
var worktrees = new WorktreeService({ state, hub, runtime, paths, agents, drafts });
|
|
24701
26516
|
var turns = new TurnService({
|
|
24702
26517
|
state,
|
|
24703
26518
|
hub,
|
|
24704
26519
|
transcript: (id) => runtime.agentFor(id)?.transcript() ?? [],
|
|
24705
|
-
summarize: makeRecapper(runtime, agents, state)
|
|
26520
|
+
summarize: makeRecapper(runtime, agents, state),
|
|
26521
|
+
hold: (id, tag) => runtime.hold(id, tag),
|
|
26522
|
+
release: (id, tag) => runtime.release(id, tag)
|
|
24706
26523
|
});
|
|
24707
26524
|
var files = new FileService(state, runtime, (id) => worktrees.readable(id));
|
|
24708
26525
|
var design = new DesignService((id) => worktrees.readable(id));
|
|
@@ -24717,10 +26534,61 @@ new LandingService({
|
|
|
24717
26534
|
judge: makeLander(runtime, agents, state)
|
|
24718
26535
|
});
|
|
24719
26536
|
var refs = new RefSearch({ state });
|
|
26537
|
+
var chats = new ChatSearch({
|
|
26538
|
+
state,
|
|
26539
|
+
live: (id) => runtime.agentFor(id)?.transcript(),
|
|
26540
|
+
transcriptPath: (id) => transcriptPathFor(paths.transcriptsDir, id),
|
|
26541
|
+
archivedChats: (repoId) => worktrees.archivedChats(repoId)
|
|
26542
|
+
});
|
|
24720
26543
|
var prs = new PrService({ state, hub, worktrees, view: viewPr });
|
|
24721
|
-
var
|
|
26544
|
+
var self = new SelfWatch(SOURCE_ROOT);
|
|
26545
|
+
await self.start();
|
|
26546
|
+
var afterLand = new AfterLand({ state, hub, self });
|
|
26547
|
+
var repos = new RepoRegistry({ state, hub, runtime, worktrees, afterLand, self, drafts });
|
|
26548
|
+
var idle = new IdlePolicy({
|
|
26549
|
+
runtime,
|
|
26550
|
+
state,
|
|
26551
|
+
hub,
|
|
26552
|
+
wake: (id) => repos.touch(id),
|
|
26553
|
+
warmSpare: (repoId) => repos.warm(repoId)
|
|
26554
|
+
});
|
|
26555
|
+
var sweep = new ArchiveSweep({
|
|
26556
|
+
state,
|
|
26557
|
+
viewed: (id) => idle.isViewed(id),
|
|
26558
|
+
busy: (id) => runtime.busy(id),
|
|
26559
|
+
drafts,
|
|
26560
|
+
worktrees,
|
|
26561
|
+
afterMs: archiveAfterFrom(process.env.TOYON_ARCHIVE_AFTER_MS)
|
|
26562
|
+
});
|
|
24722
26563
|
var themes = new ThemeStore({ get: () => state.theme, set: (p) => state.setTheme(p) }, paths.themesDir);
|
|
24723
26564
|
themes.load();
|
|
26565
|
+
var restarter = new Restarter({
|
|
26566
|
+
hub,
|
|
26567
|
+
state,
|
|
26568
|
+
runtime,
|
|
26569
|
+
refusal: () => {
|
|
26570
|
+
const can = restartable();
|
|
26571
|
+
return can.ok ? null : can.reason;
|
|
26572
|
+
},
|
|
26573
|
+
go: () => {
|
|
26574
|
+
setTimeout(() => fireAndForget("daemon", shutdown("restart", { respawn: true }), "restart"), 100);
|
|
26575
|
+
}
|
|
26576
|
+
});
|
|
26577
|
+
var installedPackage = cloud.enabled || process.env.TOYON_WORKTREE !== undefined ? null : PACKAGE_JSON;
|
|
26578
|
+
var method = installMethod(installedPackage);
|
|
26579
|
+
var update = new UpdateService({
|
|
26580
|
+
hub,
|
|
26581
|
+
state,
|
|
26582
|
+
running: package_default.version,
|
|
26583
|
+
method,
|
|
26584
|
+
restarter,
|
|
26585
|
+
installed: async () => installedPackage ? readVersion(installedPackage) : null,
|
|
26586
|
+
latest: latestVersion,
|
|
26587
|
+
command: (version) => installCommand(method, version),
|
|
26588
|
+
install: runInstall,
|
|
26589
|
+
busy: () => runtime.anyBusy(),
|
|
26590
|
+
managed: process.env.TOYON_UPDATES === "off"
|
|
26591
|
+
});
|
|
24724
26592
|
var { branded, stop: stopServer } = startServer({
|
|
24725
26593
|
port,
|
|
24726
26594
|
token,
|
|
@@ -24738,13 +26606,20 @@ var { branded, stop: stopServer } = startServer({
|
|
|
24738
26606
|
design,
|
|
24739
26607
|
routes,
|
|
24740
26608
|
runtime,
|
|
26609
|
+
idle,
|
|
24741
26610
|
exec,
|
|
24742
26611
|
refs,
|
|
26612
|
+
chats,
|
|
26613
|
+
drafts,
|
|
24743
26614
|
prs,
|
|
24744
26615
|
themes,
|
|
24745
26616
|
agents,
|
|
24746
26617
|
accounts,
|
|
24747
26618
|
attachments: attachments2,
|
|
26619
|
+
self,
|
|
26620
|
+
afterLand,
|
|
26621
|
+
restarter,
|
|
26622
|
+
update,
|
|
24748
26623
|
planTasks: makePlanner(agents),
|
|
24749
26624
|
folderDialog: folderDialog()
|
|
24750
26625
|
}
|
|
@@ -24764,6 +26639,11 @@ writeFileSync9(paths.pidFile, `${process.pid}
|
|
|
24764
26639
|
`);
|
|
24765
26640
|
var stopLagSampler = startLagSampler();
|
|
24766
26641
|
await repos.boot();
|
|
26642
|
+
drafts.prune((id) => {
|
|
26643
|
+
const repoId = draftRepoOf(id);
|
|
26644
|
+
return repoId ? !!state.repo(repoId) : !!state.worktree(id) || worktrees.hasArchived(id);
|
|
26645
|
+
});
|
|
26646
|
+
idle.boot();
|
|
24767
26647
|
fireAndForget("agents", agents.installMissing().then(() => probe.missing()), "agent adapter install");
|
|
24768
26648
|
var repoArg = process.argv[2];
|
|
24769
26649
|
if (repoArg) {
|
|
@@ -24788,19 +26668,27 @@ if (remote?.front === "edge") {
|
|
|
24788
26668
|
}
|
|
24789
26669
|
}
|
|
24790
26670
|
var shuttingDown = false;
|
|
24791
|
-
async function shutdown(signal) {
|
|
26671
|
+
async function shutdown(signal, opts = {}) {
|
|
24792
26672
|
if (shuttingDown)
|
|
24793
26673
|
return;
|
|
24794
26674
|
shuttingDown = true;
|
|
24795
26675
|
log.info("daemon", `${signal}: stopping dev servers`);
|
|
26676
|
+
idle.shutdown();
|
|
24796
26677
|
repos.stopWatchers();
|
|
24797
26678
|
prs.stop();
|
|
26679
|
+
sweep.stop();
|
|
24798
26680
|
stopLagSampler();
|
|
24799
26681
|
stopServer();
|
|
24800
26682
|
routes.flush();
|
|
26683
|
+
drafts.flush();
|
|
24801
26684
|
const deadline = new Promise((resolve) => setTimeout(resolve, 5000));
|
|
24802
26685
|
await Promise.race([runtime.shutdown(), deadline]);
|
|
24803
|
-
|
|
26686
|
+
rmSync6(paths.pidFile, { force: true });
|
|
26687
|
+
if (opts.respawn) {
|
|
26688
|
+
await Bun.sleep(250);
|
|
26689
|
+
log.info("daemon", "starting the replacement");
|
|
26690
|
+
respawn(paths.logFile);
|
|
26691
|
+
}
|
|
24804
26692
|
process.exit(0);
|
|
24805
26693
|
}
|
|
24806
26694
|
process.on("SIGINT", () => fireAndForget("daemon", shutdown("SIGINT"), "shutdown"));
|