toyon 0.23.0 → 0.24.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 +26 -21
- package/dist/CHANGELOG.md +32 -0
- package/dist/cli.js +1 -1
- package/dist/daemon.js +1609 -1229
- package/dist/shell/assets/{Editor-C2e_GpDQ.js → Editor-Dq4a0gFv.js} +164 -164
- package/dist/shell/assets/Editor-Dq4a0gFv.js.br +0 -0
- package/dist/shell/assets/Editor-Dq4a0gFv.js.gz +0 -0
- package/dist/shell/assets/{XTerm-B84G0H5I.js → XTerm-BfuBg96X.js} +2 -2
- package/dist/shell/assets/XTerm-BfuBg96X.js.br +0 -0
- package/dist/shell/assets/XTerm-BfuBg96X.js.gz +0 -0
- package/dist/shell/assets/{cssMode-DhDLwjyN.js → cssMode-An1EUPnJ.js} +1 -1
- package/dist/shell/assets/cssMode-An1EUPnJ.js.br +0 -0
- package/dist/shell/assets/cssMode-An1EUPnJ.js.gz +0 -0
- package/dist/shell/assets/{freemarker2-rc8sP22g.js → freemarker2-PYOY2M7v.js} +1 -1
- package/dist/shell/assets/freemarker2-PYOY2M7v.js.br +0 -0
- package/dist/shell/assets/freemarker2-PYOY2M7v.js.gz +0 -0
- package/dist/shell/assets/{grammar-Cty-NAQC.js → grammar-e2yQ-Xdq.js} +1 -1
- package/dist/shell/assets/grammar-e2yQ-Xdq.js.br +0 -0
- package/dist/shell/assets/grammar-e2yQ-Xdq.js.gz +0 -0
- package/dist/shell/assets/{handlebars-BOQxV3p5.js → handlebars-BtSTwhBl.js} +1 -1
- package/dist/shell/assets/handlebars-BtSTwhBl.js.br +0 -0
- package/dist/shell/assets/handlebars-BtSTwhBl.js.gz +0 -0
- package/dist/shell/assets/{html-CxndUpse.js → html-lmman0qi.js} +1 -1
- package/dist/shell/assets/html-lmman0qi.js.br +0 -0
- package/dist/shell/assets/html-lmman0qi.js.gz +0 -0
- package/dist/shell/assets/{htmlMode-CG7eVDLo.js → htmlMode-CYUUrmIv.js} +1 -1
- package/dist/shell/assets/htmlMode-CYUUrmIv.js.br +0 -0
- package/dist/shell/assets/htmlMode-CYUUrmIv.js.gz +0 -0
- package/dist/shell/assets/index-BDzHAr7c.css +1 -0
- package/dist/shell/assets/index-BDzHAr7c.css.br +0 -0
- package/dist/shell/assets/index-BDzHAr7c.css.gz +0 -0
- package/dist/shell/assets/index-o7SjsIYr.js +294 -0
- package/dist/shell/assets/index-o7SjsIYr.js.br +0 -0
- package/dist/shell/assets/index-o7SjsIYr.js.gz +0 -0
- package/dist/shell/assets/{javascript-CafoevnZ.js → javascript-CHVOacyY.js} +1 -1
- package/dist/shell/assets/{jsonMode-Cg_4sMB_.js → jsonMode-WaLJvHyp.js} +1 -1
- package/dist/shell/assets/jsonMode-WaLJvHyp.js.br +0 -0
- package/dist/shell/assets/jsonMode-WaLJvHyp.js.gz +0 -0
- package/dist/shell/assets/{liquid-DxW_HNid.js → liquid-Bhzd8TLd.js} +1 -1
- package/dist/shell/assets/liquid-Bhzd8TLd.js.br +0 -0
- package/dist/shell/assets/liquid-Bhzd8TLd.js.gz +0 -0
- package/dist/shell/assets/{lspLanguageFeatures-Dhat9l4L.js → lspLanguageFeatures-DS22S_fR.js} +2 -2
- package/dist/shell/assets/lspLanguageFeatures-DS22S_fR.js.br +0 -0
- package/dist/shell/assets/lspLanguageFeatures-DS22S_fR.js.gz +0 -0
- package/dist/shell/assets/{mdx-ZdMhgPNL.js → mdx-1x45Dt5R.js} +1 -1
- package/dist/shell/assets/mdx-1x45Dt5R.js.br +0 -0
- package/dist/shell/assets/mdx-1x45Dt5R.js.gz +0 -0
- package/dist/shell/assets/{python-Bae_7wMM.js → python-B0fHYRyj.js} +1 -1
- package/dist/shell/assets/python-B0fHYRyj.js.br +0 -0
- package/dist/shell/assets/python-B0fHYRyj.js.gz +0 -0
- package/dist/shell/assets/{razor-CtwiCIJH.js → razor-C48vGnF3.js} +1 -1
- package/dist/shell/assets/razor-C48vGnF3.js.br +0 -0
- package/dist/shell/assets/razor-C48vGnF3.js.gz +0 -0
- package/dist/shell/assets/toyon-mono-bold-u3Ky0k2u.woff2 +0 -0
- package/dist/shell/assets/toyon-mono-regular-BC3CdGld.woff2 +0 -0
- package/dist/shell/assets/{tsMode-DH9rMbRS.js → tsMode-2SIN2sKE.js} +1 -1
- package/dist/shell/assets/tsMode-2SIN2sKE.js.br +0 -0
- package/dist/shell/assets/tsMode-2SIN2sKE.js.gz +0 -0
- package/dist/shell/assets/{xml-rA-vNTCn.js → xml-DPJN-pu8.js} +1 -1
- package/dist/shell/assets/xml-DPJN-pu8.js.br +0 -0
- package/dist/shell/assets/xml-DPJN-pu8.js.gz +0 -0
- package/dist/shell/assets/{yaml-pOtRRozm.js → yaml-CDzmW7EM.js} +1 -1
- package/dist/shell/assets/yaml-CDzmW7EM.js.br +0 -0
- package/dist/shell/assets/yaml-CDzmW7EM.js.gz +0 -0
- package/dist/shell/index.html +2 -2
- package/package.json +1 -1
- package/dist/shell/assets/Editor-C2e_GpDQ.js.br +0 -0
- package/dist/shell/assets/Editor-C2e_GpDQ.js.gz +0 -0
- package/dist/shell/assets/XTerm-B84G0H5I.js.br +0 -0
- package/dist/shell/assets/XTerm-B84G0H5I.js.gz +0 -0
- package/dist/shell/assets/cssMode-DhDLwjyN.js.br +0 -0
- package/dist/shell/assets/cssMode-DhDLwjyN.js.gz +0 -0
- package/dist/shell/assets/freemarker2-rc8sP22g.js.br +0 -0
- package/dist/shell/assets/freemarker2-rc8sP22g.js.gz +0 -0
- package/dist/shell/assets/grammar-Cty-NAQC.js.br +0 -0
- package/dist/shell/assets/grammar-Cty-NAQC.js.gz +0 -0
- package/dist/shell/assets/handlebars-BOQxV3p5.js.br +0 -0
- package/dist/shell/assets/handlebars-BOQxV3p5.js.gz +0 -0
- package/dist/shell/assets/html-CxndUpse.js.br +0 -0
- package/dist/shell/assets/html-CxndUpse.js.gz +0 -0
- package/dist/shell/assets/htmlMode-CG7eVDLo.js.br +0 -0
- package/dist/shell/assets/htmlMode-CG7eVDLo.js.gz +0 -0
- package/dist/shell/assets/index-DpSfPmkM.css +0 -1
- package/dist/shell/assets/index-DpSfPmkM.css.br +0 -0
- package/dist/shell/assets/index-DpSfPmkM.css.gz +0 -0
- package/dist/shell/assets/index-DzfHDra7.js +0 -294
- package/dist/shell/assets/index-DzfHDra7.js.br +0 -0
- package/dist/shell/assets/index-DzfHDra7.js.gz +0 -0
- package/dist/shell/assets/jsonMode-Cg_4sMB_.js.br +0 -0
- package/dist/shell/assets/jsonMode-Cg_4sMB_.js.gz +0 -0
- package/dist/shell/assets/liquid-DxW_HNid.js.br +0 -0
- package/dist/shell/assets/liquid-DxW_HNid.js.gz +0 -0
- package/dist/shell/assets/lspLanguageFeatures-Dhat9l4L.js.br +0 -0
- package/dist/shell/assets/lspLanguageFeatures-Dhat9l4L.js.gz +0 -0
- package/dist/shell/assets/mdx-ZdMhgPNL.js.br +0 -0
- package/dist/shell/assets/mdx-ZdMhgPNL.js.gz +0 -0
- package/dist/shell/assets/python-Bae_7wMM.js.br +0 -0
- package/dist/shell/assets/python-Bae_7wMM.js.gz +0 -0
- package/dist/shell/assets/razor-CtwiCIJH.js.br +0 -0
- package/dist/shell/assets/razor-CtwiCIJH.js.gz +0 -0
- package/dist/shell/assets/toyon-mono-bold-9c1iJO0D.woff2 +0 -0
- package/dist/shell/assets/toyon-mono-regular-BXyxgSUK.woff2 +0 -0
- package/dist/shell/assets/tsMode-DH9rMbRS.js.br +0 -0
- package/dist/shell/assets/tsMode-DH9rMbRS.js.gz +0 -0
- package/dist/shell/assets/xml-rA-vNTCn.js.br +0 -0
- package/dist/shell/assets/xml-rA-vNTCn.js.gz +0 -0
- package/dist/shell/assets/yaml-pOtRRozm.js.br +0 -0
- package/dist/shell/assets/yaml-pOtRRozm.js.gz +0 -0
package/dist/daemon.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// ../daemon/src/index.ts
|
|
3
3
|
import { rmSync as rmSync10, writeFileSync as writeFileSync14 } from "fs";
|
|
4
4
|
import { homedir as homedir15 } from "os";
|
|
5
|
-
import { dirname as dirname16, join as
|
|
5
|
+
import { dirname as dirname16, join as join37 } from "path";
|
|
6
6
|
import { fileURLToPath } from "url";
|
|
7
7
|
|
|
8
8
|
// ../shared/src/protocol/events.ts
|
|
@@ -649,6 +649,9 @@ function movedPastLand(wt, f) {
|
|
|
649
649
|
function prTaken(wt) {
|
|
650
650
|
return !!wt.pr && (wt.lands ?? []).some((l) => l.pr === wt.pr?.number);
|
|
651
651
|
}
|
|
652
|
+
function prPending(wt) {
|
|
653
|
+
return !!wt.pr && wt.pr.state !== "closed" && !prTaken(wt);
|
|
654
|
+
}
|
|
652
655
|
function shipNoun(op) {
|
|
653
656
|
return { land: "a land", commit: "a commit", "sync-main": "a sync", "pull-main": "a pull" }[op];
|
|
654
657
|
}
|
|
@@ -6784,7 +6787,7 @@ ${text.slice(text.length - (CONTEXT_CHARS - HEAD_CHARS))}`;
|
|
|
6784
6787
|
}
|
|
6785
6788
|
|
|
6786
6789
|
// ../shared/src/protocol/limits.ts
|
|
6787
|
-
var PROTOCOL_VERSION =
|
|
6790
|
+
var PROTOCOL_VERSION = 149;
|
|
6788
6791
|
var PAGE_ERROR_MAX_CHARS = 2000;
|
|
6789
6792
|
var PAGE_TITLE_MAX_CHARS = 1000;
|
|
6790
6793
|
var FILE_MAX_CHARS = 5000000;
|
|
@@ -7200,7 +7203,7 @@ var clientMsgSchema = discriminatedUnion("t", [
|
|
|
7200
7203
|
base: string2().max(64).nullable(),
|
|
7201
7204
|
seq
|
|
7202
7205
|
}),
|
|
7203
|
-
object({ t: literal("list-files"), worktreeId: id }),
|
|
7206
|
+
object({ t: literal("list-files"), worktreeId: id, tracked: boolean2() }),
|
|
7204
7207
|
object({ t: literal("list-commands"), worktreeId: id }),
|
|
7205
7208
|
object({ t: literal("search"), worktreeId: id, query: string2().max(500) }),
|
|
7206
7209
|
object({ t: literal("search-stop") }),
|
|
@@ -8841,7 +8844,7 @@ async function turnRemoteOff(ts, daemonPort, file, was) {
|
|
|
8841
8844
|
// ../daemon/package.json
|
|
8842
8845
|
var package_default = {
|
|
8843
8846
|
name: "@toyon/daemon",
|
|
8844
|
-
version: "0.
|
|
8847
|
+
version: "0.24.0",
|
|
8845
8848
|
private: true,
|
|
8846
8849
|
type: "module",
|
|
8847
8850
|
main: "src/index.ts",
|
|
@@ -13347,7 +13350,7 @@ class RemoteSetting {
|
|
|
13347
13350
|
}
|
|
13348
13351
|
}
|
|
13349
13352
|
function sameSecret(given, want) {
|
|
13350
|
-
if (given === null)
|
|
13353
|
+
if (given === null || want === "")
|
|
13351
13354
|
return false;
|
|
13352
13355
|
const a = Buffer.from(given);
|
|
13353
13356
|
const b = Buffer.from(want);
|
|
@@ -13451,7 +13454,7 @@ function passPreview(req, gate) {
|
|
|
13451
13454
|
const code = url.searchParams.get(PREVIEW_GRANT_PARAM);
|
|
13452
13455
|
if (code !== null) {
|
|
13453
13456
|
url.searchParams.delete(PREVIEW_GRANT_PARAM);
|
|
13454
|
-
const location = `${url.pathname}${url.search}`;
|
|
13457
|
+
const location = `${url.pathname.replace(/^\/+/, "/")}${url.search}`;
|
|
13455
13458
|
if (!pass.ok && !gate.redeem(code))
|
|
13456
13459
|
return { ok: false, response: refusedPreview() };
|
|
13457
13460
|
const headers = { location, "cache-control": "no-store" };
|
|
@@ -14104,6 +14107,13 @@ var NO_PROMPT = {
|
|
|
14104
14107
|
GIT_SSH_COMMAND: "ssh -o BatchMode=yes"
|
|
14105
14108
|
};
|
|
14106
14109
|
var SPAWN_ENV = { GIT_OPTIONAL_LOCKS: "0" };
|
|
14110
|
+
function spawnEnv(base = process.env, ...over) {
|
|
14111
|
+
const { LC_ALL: all, LANGUAGE: _language, ...rest } = base;
|
|
14112
|
+
return Object.assign(rest, all ? { LANG: all } : {}, { LC_MESSAGES: "C" }, SPAWN_ENV, ...over);
|
|
14113
|
+
}
|
|
14114
|
+
function nulSplit(out) {
|
|
14115
|
+
return out.split("\x00").filter(Boolean);
|
|
14116
|
+
}
|
|
14107
14117
|
var RUN_CEILING_MS = 10 * 60000;
|
|
14108
14118
|
async function run(cmd, args, cwd, env, opts = {}) {
|
|
14109
14119
|
const started = Date.now();
|
|
@@ -14117,7 +14127,7 @@ async function run(cmd, args, cwd, env, opts = {}) {
|
|
|
14117
14127
|
stdout: "pipe",
|
|
14118
14128
|
stderr: "pipe",
|
|
14119
14129
|
stdin: "ignore",
|
|
14120
|
-
env:
|
|
14130
|
+
env: spawnEnv(process.env, env)
|
|
14121
14131
|
});
|
|
14122
14132
|
const done = (async () => {
|
|
14123
14133
|
const [rawOut, err, status] = await Promise.all([
|
|
@@ -14159,7 +14169,7 @@ async function runLive(cmd, args, cwd, opts = {}) {
|
|
|
14159
14169
|
stdout: "pipe",
|
|
14160
14170
|
stderr: "pipe",
|
|
14161
14171
|
stdin: "ignore",
|
|
14162
|
-
env:
|
|
14172
|
+
env: spawnEnv(process.env, opts.env)
|
|
14163
14173
|
});
|
|
14164
14174
|
const stop = () => p.kill();
|
|
14165
14175
|
opts.signal?.addEventListener("abort", stop, { once: true });
|
|
@@ -14215,7 +14225,7 @@ async function runRows(cmd, args, cwd, opts) {
|
|
|
14215
14225
|
stdout: "pipe",
|
|
14216
14226
|
stderr: "pipe",
|
|
14217
14227
|
stdin: "ignore",
|
|
14218
|
-
env:
|
|
14228
|
+
env: spawnEnv()
|
|
14219
14229
|
});
|
|
14220
14230
|
const reader = p.stdout.getReader();
|
|
14221
14231
|
const stop = () => {
|
|
@@ -14342,7 +14352,7 @@ function runWatched(cmd, args, cwd, opts = {}) {
|
|
|
14342
14352
|
cwd,
|
|
14343
14353
|
stdio: opts.onHook ? ["ignore", "pipe", "pipe", "pipe"] : ["ignore", "pipe", "pipe"],
|
|
14344
14354
|
detached: true,
|
|
14345
|
-
env:
|
|
14355
|
+
env: spawnEnv(process.env, opts.env, opts.onHook ? { GIT_TRACE2_EVENT: "3" } : undefined)
|
|
14346
14356
|
});
|
|
14347
14357
|
} catch (e) {
|
|
14348
14358
|
failed(e instanceof Error ? e.message : String(e));
|
|
@@ -16635,6 +16645,12 @@ function handoffActedPrompt(pieces, outcomes, by = "plan", note) {
|
|
|
16635
16645
|
}
|
|
16636
16646
|
return `Toyon acted on the pieces you handed over with the plan's approval: ${parts.join("; ")}. ${own} The siblings read the plan at its path in this worktree; never do their pieces yourself.`;
|
|
16637
16647
|
}
|
|
16648
|
+
function handoffDeclinedPrompt(pieces, note) {
|
|
16649
|
+
const titles = pieces.map((p) => `"${p.title}"`).join(", ");
|
|
16650
|
+
return `The person answered the handoff card with not now: nothing on the list you handed over (${titles}) was started, and none of it is yours to do from that list. They added: ${note}
|
|
16651
|
+
|
|
16652
|
+
Read that as the word on what to change. Hand over a new list with the handoff tool if a split still fits, or go on without one.`;
|
|
16653
|
+
}
|
|
16638
16654
|
function sentFromLine(path, by) {
|
|
16639
16655
|
const who = by ? `, where ${by} was working` : "";
|
|
16640
16656
|
return `This message was sent from a terminal in ${path} with \`toyon send\`${who}.`;
|
|
@@ -17563,7 +17579,7 @@ class AcpSession {
|
|
|
17563
17579
|
steered = [];
|
|
17564
17580
|
steerMark;
|
|
17565
17581
|
streamingId;
|
|
17566
|
-
refused =
|
|
17582
|
+
refused = [];
|
|
17567
17583
|
turn = null;
|
|
17568
17584
|
stopped = false;
|
|
17569
17585
|
holds = 0;
|
|
@@ -17571,6 +17587,7 @@ class AcpSession {
|
|
|
17571
17587
|
return this.holds > 0;
|
|
17572
17588
|
}
|
|
17573
17589
|
conn = null;
|
|
17590
|
+
start = null;
|
|
17574
17591
|
connecting = null;
|
|
17575
17592
|
opening = null;
|
|
17576
17593
|
live = null;
|
|
@@ -17744,7 +17761,7 @@ class AcpSession {
|
|
|
17744
17761
|
}
|
|
17745
17762
|
}
|
|
17746
17763
|
get runningAgent() {
|
|
17747
|
-
return this.conn?.spec.id ?? null;
|
|
17764
|
+
return this.conn?.spec.id ?? this.start?.spec.id ?? null;
|
|
17748
17765
|
}
|
|
17749
17766
|
get mcpTools() {
|
|
17750
17767
|
return this.conn ? this.conn.mcpServers.length > 0 : null;
|
|
@@ -17783,7 +17800,7 @@ class AcpSession {
|
|
|
17783
17800
|
return this.queue.length;
|
|
17784
17801
|
}
|
|
17785
17802
|
get unsettled() {
|
|
17786
|
-
return this.queue.length > 0 || this.steered.length > 0 || this.refused
|
|
17803
|
+
return this.queue.length > 0 || this.steered.length > 0 || this.refused.length > 0;
|
|
17787
17804
|
}
|
|
17788
17805
|
get queueItems() {
|
|
17789
17806
|
return this.waiting().map(queued);
|
|
@@ -18023,7 +18040,8 @@ class AcpSession {
|
|
|
18023
18040
|
const rejected = !isAuthRequired(e);
|
|
18024
18041
|
if (rejected && !aws)
|
|
18025
18042
|
this.emit({ type: "agent-error", message: this.describe(e), ts: Date.now() });
|
|
18026
|
-
|
|
18043
|
+
if (item)
|
|
18044
|
+
this.refused.push(item);
|
|
18027
18045
|
this.emit({
|
|
18028
18046
|
type: "agent-auth-required",
|
|
18029
18047
|
agent: this.conn.spec.id,
|
|
@@ -18059,9 +18077,12 @@ class AcpSession {
|
|
|
18059
18077
|
if (!turn?.open)
|
|
18060
18078
|
return;
|
|
18061
18079
|
turn.open = false;
|
|
18080
|
+
this.turnOver(stopReason);
|
|
18081
|
+
this.setStopping(false);
|
|
18082
|
+
}
|
|
18083
|
+
turnOver(stopReason) {
|
|
18062
18084
|
this.emit({ type: "turn-end", stopReason, ts: Date.now() });
|
|
18063
18085
|
this.showHeld();
|
|
18064
|
-
this.setStopping(false);
|
|
18065
18086
|
}
|
|
18066
18087
|
async ask(system, prompt, opts = {}) {
|
|
18067
18088
|
if (this.stopped)
|
|
@@ -18116,6 +18137,7 @@ class AcpSession {
|
|
|
18116
18137
|
}
|
|
18117
18138
|
};
|
|
18118
18139
|
}
|
|
18140
|
+
this.armReaper();
|
|
18119
18141
|
try {
|
|
18120
18142
|
await conn.ctx.request(methods.agent.authenticate, {
|
|
18121
18143
|
methodId,
|
|
@@ -18149,10 +18171,14 @@ class AcpSession {
|
|
|
18149
18171
|
this.retry();
|
|
18150
18172
|
}
|
|
18151
18173
|
retry() {
|
|
18152
|
-
const
|
|
18153
|
-
this.refused =
|
|
18154
|
-
if (
|
|
18155
|
-
|
|
18174
|
+
const items = this.refused;
|
|
18175
|
+
this.refused = [];
|
|
18176
|
+
if (items.length === 0 || this.stopped)
|
|
18177
|
+
return;
|
|
18178
|
+
this.queue.unshift(...items);
|
|
18179
|
+
this.queueChanged();
|
|
18180
|
+
if (!this.running && !this.held)
|
|
18181
|
+
fireAndForget(this.d.worktreeId, this.drain(), "agent drain");
|
|
18156
18182
|
}
|
|
18157
18183
|
planWithheld(conn, methodId) {
|
|
18158
18184
|
return this.d.planSignIn?.() === false && conn.spec.terminalLogins?.[methodId]?.plan === true;
|
|
@@ -18259,40 +18285,43 @@ class AcpSession {
|
|
|
18259
18285
|
return Promise.resolve(this.conn);
|
|
18260
18286
|
if (this.connecting)
|
|
18261
18287
|
return this.connecting;
|
|
18262
|
-
|
|
18263
|
-
this.connecting
|
|
18288
|
+
const connecting = this.openConn(why).finally(() => {
|
|
18289
|
+
if (this.connecting === connecting)
|
|
18290
|
+
this.connecting = null;
|
|
18264
18291
|
});
|
|
18265
|
-
|
|
18292
|
+
this.connecting = connecting;
|
|
18293
|
+
return connecting;
|
|
18266
18294
|
}
|
|
18267
18295
|
async openConn(why) {
|
|
18296
|
+
if (this.stopped)
|
|
18297
|
+
throw new UserError("this worktree's agent is closed");
|
|
18268
18298
|
const spec = this.d.spec();
|
|
18269
|
-
const
|
|
18270
|
-
|
|
18271
|
-
const sandboxed = spec.confinement !== "none";
|
|
18272
|
-
const side = new Map;
|
|
18273
|
-
const commands = new Map;
|
|
18274
|
-
const app = client({ name: "toyon" }).onRequest(methods.client.session.requestPermission, (c) => this.onPermission(c.params, bounds, sandboxed)).onRequest(methods.client.elicitation.create, (c) => this.onElicit(c.params, c.signal)).onNotification(methods.client.session.update, (c) => this.onUpdate(c.params, side, commands)).onNotification(AUTH_STATUS_UPDATE_METHOD, parseAuthStatus, (c) => {
|
|
18275
|
-
if (c.params)
|
|
18276
|
-
this.d.onAuth?.(spec.id, { status: c.params });
|
|
18277
|
-
});
|
|
18278
|
-
const link = this.d.connect(app, spec, prepared);
|
|
18279
|
-
this.noteProcess(link, true);
|
|
18280
|
-
log.info(this.d.worktreeId, `agent ${spec.id} started for ${why} (pid ${link.pid ?? "unknown"})`);
|
|
18281
|
-
const ctx = link.conn.agent;
|
|
18282
|
-
link.exited.then(() => {
|
|
18283
|
-
this.noteProcess(link, false);
|
|
18284
|
-
if (this.conn?.link === link) {
|
|
18285
|
-
log.warn(this.d.worktreeId, "agent process exited while idle");
|
|
18286
|
-
this.conn = null;
|
|
18287
|
-
this.live = null;
|
|
18288
|
-
this.d.mcp?.close();
|
|
18289
|
-
this.cancelAsks();
|
|
18290
|
-
this.tasks.endAll("the agent stopped, and the command with it");
|
|
18291
|
-
this.wakeups.endAll("the agent stopped before it woke");
|
|
18292
|
-
this.endOwn("interrupted");
|
|
18293
|
-
}
|
|
18294
|
-
});
|
|
18299
|
+
const start = { spec, link: null };
|
|
18300
|
+
this.start = start;
|
|
18295
18301
|
try {
|
|
18302
|
+
const prepared = await (this.d.prepare ?? prepareLaunch)(this.d.cwd, spec);
|
|
18303
|
+
if (this.start !== start)
|
|
18304
|
+
throw stoppedStarting();
|
|
18305
|
+
const { bounds } = prepared;
|
|
18306
|
+
const sandboxed = spec.confinement !== "none";
|
|
18307
|
+
const side = new Map;
|
|
18308
|
+
const commands = new Map;
|
|
18309
|
+
const app = client({ name: "toyon" }).onRequest(methods.client.session.requestPermission, (c) => this.onPermission(c.params, bounds, sandboxed)).onRequest(methods.client.elicitation.create, (c) => this.onElicit(c.params, c.signal)).onNotification(methods.client.session.update, (c) => this.onUpdate(c.params, side, commands)).onNotification(AUTH_STATUS_UPDATE_METHOD, parseAuthStatus, (c) => {
|
|
18310
|
+
if (c.params)
|
|
18311
|
+
this.d.onAuth?.(spec.id, { status: c.params });
|
|
18312
|
+
});
|
|
18313
|
+
const link = this.d.connect(app, spec, prepared);
|
|
18314
|
+
start.link = link;
|
|
18315
|
+
this.noteProcess(link, true);
|
|
18316
|
+
log.info(this.d.worktreeId, `agent ${spec.id} started for ${why} (pid ${link.pid ?? "unknown"})`);
|
|
18317
|
+
const ctx = link.conn.agent;
|
|
18318
|
+
link.exited.then(() => {
|
|
18319
|
+
this.noteProcess(link, false);
|
|
18320
|
+
if (this.conn?.link === link) {
|
|
18321
|
+
log.warn(this.d.worktreeId, "agent process exited while idle");
|
|
18322
|
+
this.processGone();
|
|
18323
|
+
}
|
|
18324
|
+
});
|
|
18296
18325
|
const init = await ctx.request(methods.agent.initialize, {
|
|
18297
18326
|
protocolVersion: PROTOCOL_VERSION2,
|
|
18298
18327
|
clientCapabilities: {
|
|
@@ -18302,7 +18331,9 @@ class AcpSession {
|
|
|
18302
18331
|
},
|
|
18303
18332
|
clientInfo: { name: "toyon", version: "0" }
|
|
18304
18333
|
});
|
|
18305
|
-
this.
|
|
18334
|
+
if (this.start !== start)
|
|
18335
|
+
throw stoppedStarting();
|
|
18336
|
+
const conn = {
|
|
18306
18337
|
link,
|
|
18307
18338
|
ctx,
|
|
18308
18339
|
spec,
|
|
@@ -18318,10 +18349,17 @@ class AcpSession {
|
|
|
18318
18349
|
commands
|
|
18319
18350
|
};
|
|
18320
18351
|
this.d.onAuth?.(spec.id, { canLogout: supportsLogout(init.agentCapabilities) });
|
|
18321
|
-
|
|
18352
|
+
this.start = null;
|
|
18353
|
+
this.conn = conn;
|
|
18354
|
+
return conn;
|
|
18322
18355
|
} catch (e) {
|
|
18323
|
-
|
|
18324
|
-
|
|
18356
|
+
if (this.start !== start)
|
|
18357
|
+
throw stoppedStarting();
|
|
18358
|
+
this.start = null;
|
|
18359
|
+
if (start.link) {
|
|
18360
|
+
fireAndForget(this.d.worktreeId, start.link.kill(), "kill agent after failed start");
|
|
18361
|
+
this.noteProcess(start.link, false);
|
|
18362
|
+
}
|
|
18325
18363
|
throw e;
|
|
18326
18364
|
}
|
|
18327
18365
|
}
|
|
@@ -18376,6 +18414,8 @@ class AcpSession {
|
|
|
18376
18414
|
if (!this.stopped)
|
|
18377
18415
|
this.d.setSessionId(sessionId);
|
|
18378
18416
|
}
|
|
18417
|
+
if (this.conn !== conn)
|
|
18418
|
+
throw stoppedStarting();
|
|
18379
18419
|
const options = readOptions(configOptions);
|
|
18380
18420
|
this.emit({ type: "session-info", sessionId, ...currentValues(options) });
|
|
18381
18421
|
this.learn(options, !resumed);
|
|
@@ -18764,7 +18804,7 @@ class AcpSession {
|
|
|
18764
18804
|
if (this.own.settle)
|
|
18765
18805
|
clearTimeout(this.own.settle);
|
|
18766
18806
|
this.own = null;
|
|
18767
|
-
this.
|
|
18807
|
+
this.turnOver(stopReason);
|
|
18768
18808
|
this.syncStatus();
|
|
18769
18809
|
this.maybeArmReaper();
|
|
18770
18810
|
}
|
|
@@ -18792,22 +18832,33 @@ class AcpSession {
|
|
|
18792
18832
|
clearTimeout(this.reaper);
|
|
18793
18833
|
this.reaper = null;
|
|
18794
18834
|
}
|
|
18795
|
-
|
|
18796
|
-
this.
|
|
18797
|
-
this.tasks.endAll("the agent stopped, and the command with it");
|
|
18798
|
-
this.wakeups.endAll("the agent stopped before it woke");
|
|
18799
|
-
const conn = this.conn;
|
|
18835
|
+
processGone() {
|
|
18836
|
+
const was = this.conn ?? this.start;
|
|
18800
18837
|
this.conn = null;
|
|
18801
18838
|
this.live = null;
|
|
18839
|
+
this.start = null;
|
|
18840
|
+
this.connecting = null;
|
|
18802
18841
|
this.d.mcp?.close();
|
|
18803
|
-
|
|
18842
|
+
this.cancelAsks();
|
|
18843
|
+
this.tasks.endAll("the agent stopped, and the command with it");
|
|
18844
|
+
this.wakeups.endAll("the agent stopped before it woke");
|
|
18845
|
+
this.endOwn("interrupted");
|
|
18846
|
+
return was ? { link: was.link, spec: was.spec } : null;
|
|
18847
|
+
}
|
|
18848
|
+
async dropConn(why) {
|
|
18849
|
+
const was = this.processGone();
|
|
18850
|
+
if (!was)
|
|
18804
18851
|
return;
|
|
18805
|
-
|
|
18806
|
-
|
|
18807
|
-
|
|
18808
|
-
|
|
18852
|
+
const { link, spec } = was;
|
|
18853
|
+
if (!link)
|
|
18854
|
+
return log.info(this.d.worktreeId, `agent ${spec.id} stopped before it was spawned: ${why}`);
|
|
18855
|
+
log.info(this.d.worktreeId, `agent ${spec.id} stopped: ${why} (pid ${link.pid ?? "unknown"})`);
|
|
18856
|
+
link.conn.close();
|
|
18857
|
+
await link.kill();
|
|
18858
|
+
this.noteProcess(link, false);
|
|
18809
18859
|
}
|
|
18810
18860
|
}
|
|
18861
|
+
var stoppedStarting = () => new UserError("the agent was stopped while it was starting");
|
|
18811
18862
|
function takesHttpMcp(init) {
|
|
18812
18863
|
const http = init.agentCapabilities?.mcpCapabilities?.http;
|
|
18813
18864
|
return http === true || typeof http === "object" && http !== null;
|
|
@@ -22373,6 +22424,23 @@ class RuntimeRegistry {
|
|
|
22373
22424
|
}
|
|
22374
22425
|
return waiting ? "waiting" : null;
|
|
22375
22426
|
}
|
|
22427
|
+
appPorts() {
|
|
22428
|
+
const ports = new Set;
|
|
22429
|
+
for (const wt of this.deps.state.worktrees)
|
|
22430
|
+
ports.add(wt.proxyPort);
|
|
22431
|
+
for (const rt of this.runtimes.values()) {
|
|
22432
|
+
if (rt.proxy)
|
|
22433
|
+
ports.add(rt.proxy.port);
|
|
22434
|
+
for (const f of rt.forwards.values())
|
|
22435
|
+
ports.add(f.port);
|
|
22436
|
+
for (const st of rt.procs?.states() ?? []) {
|
|
22437
|
+
ports.add(st.port);
|
|
22438
|
+
if (st.boundPort !== undefined)
|
|
22439
|
+
ports.add(st.boundPort);
|
|
22440
|
+
}
|
|
22441
|
+
}
|
|
22442
|
+
return ports;
|
|
22443
|
+
}
|
|
22376
22444
|
markSetup(id, on) {
|
|
22377
22445
|
if (on)
|
|
22378
22446
|
this.settingUp.add(id);
|
|
@@ -24821,11 +24889,7 @@ class Knocks {
|
|
|
24821
24889
|
this.hub.emit("knocksChanged");
|
|
24822
24890
|
}
|
|
24823
24891
|
knock(from, client = null) {
|
|
24824
|
-
this.
|
|
24825
|
-
const waiting = this.live.filter((k) => k.answer === null);
|
|
24826
|
-
if (waiting.length >= KNOCK_MAX)
|
|
24827
|
-
return null;
|
|
24828
|
-
if (client !== null && waiting.filter((k) => k.client === client).length >= KNOCK_PER_CLIENT)
|
|
24892
|
+
if (!this.room(client))
|
|
24829
24893
|
return null;
|
|
24830
24894
|
const id = randomBytes6(9).toString("base64url");
|
|
24831
24895
|
let word = knockWord(randomInt);
|
|
@@ -24835,6 +24899,13 @@ class Knocks {
|
|
|
24835
24899
|
this.changed();
|
|
24836
24900
|
return { id, word };
|
|
24837
24901
|
}
|
|
24902
|
+
room(client) {
|
|
24903
|
+
this.sweep();
|
|
24904
|
+
const waiting = this.live.filter((k) => k.answer === null);
|
|
24905
|
+
if (waiting.length >= KNOCK_MAX)
|
|
24906
|
+
return false;
|
|
24907
|
+
return client === null || waiting.filter((k) => k.client === client).length < KNOCK_PER_CLIENT;
|
|
24908
|
+
}
|
|
24838
24909
|
pending() {
|
|
24839
24910
|
this.sweep();
|
|
24840
24911
|
return this.live.filter((k) => k.answer === null).map(({ id, word, from }) => ({ id, word, from }));
|
|
@@ -24871,6 +24942,8 @@ class Knocks {
|
|
|
24871
24942
|
var ANSWER_HOLD_MS = 30000;
|
|
24872
24943
|
|
|
24873
24944
|
// ../daemon/src/core/origins.ts
|
|
24945
|
+
var SHELL_HOSTS = new Set(["127.0.0.1", "localhost", "[::1]", "toyon.localhost"]);
|
|
24946
|
+
|
|
24874
24947
|
class Origins {
|
|
24875
24948
|
d;
|
|
24876
24949
|
constructor(d) {
|
|
@@ -24905,6 +24978,25 @@ class Origins {
|
|
|
24905
24978
|
return false;
|
|
24906
24979
|
}
|
|
24907
24980
|
}
|
|
24981
|
+
isPreview(origin) {
|
|
24982
|
+
let u;
|
|
24983
|
+
try {
|
|
24984
|
+
u = new URL(origin);
|
|
24985
|
+
} catch {
|
|
24986
|
+
return true;
|
|
24987
|
+
}
|
|
24988
|
+
if (isLoopbackHost(u.hostname)) {
|
|
24989
|
+
if (!SHELL_HOSTS.has(u.hostname))
|
|
24990
|
+
return true;
|
|
24991
|
+
const port = Number(u.port || (u.protocol === "https:" ? 443 : 80));
|
|
24992
|
+
for (const p of this.d.appPorts())
|
|
24993
|
+
if (p === port)
|
|
24994
|
+
return true;
|
|
24995
|
+
return false;
|
|
24996
|
+
}
|
|
24997
|
+
const remote = this.d.setting.get();
|
|
24998
|
+
return remote !== null && (matchPreview(remote.previews, u.host) !== null || u.host === frameAuthority(remote));
|
|
24999
|
+
}
|
|
24908
25000
|
}
|
|
24909
25001
|
|
|
24910
25002
|
// ../daemon/src/core/remoteSwitch.ts
|
|
@@ -25100,13 +25192,12 @@ class SelfWatch {
|
|
|
25100
25192
|
async check(repo) {
|
|
25101
25193
|
if (this.booted === null || !this.own(repo))
|
|
25102
25194
|
return false;
|
|
25103
|
-
const names = await
|
|
25195
|
+
const names = await gitRaw(this.root ?? "", "diff", "--name-only", "-z", `${this.booted}..${repo.defaultBranch}`);
|
|
25104
25196
|
if (!names.ok) {
|
|
25105
25197
|
log.debug("self", `could not diff ${this.booted}..${repo.defaultBranch}: ${names.err}`);
|
|
25106
25198
|
return false;
|
|
25107
25199
|
}
|
|
25108
|
-
const paths = names.out
|
|
25109
|
-
`).filter((l) => l !== "");
|
|
25200
|
+
const paths = nulSplit(names.out);
|
|
25110
25201
|
const { rebuild, restart } = classify(paths);
|
|
25111
25202
|
return this.set({ repoId: repo.id, rebuild, restart, building: this.state?.building ?? false });
|
|
25112
25203
|
}
|
|
@@ -25235,11 +25326,19 @@ function saveState(paths, state) {
|
|
|
25235
25326
|
writeFileSync9(tmp, JSON.stringify(state, null, 2));
|
|
25236
25327
|
renameSync5(tmp, paths.stateFile);
|
|
25237
25328
|
}
|
|
25238
|
-
|
|
25329
|
+
var TOKEN_MIN = 32;
|
|
25330
|
+
function loadOrCreateToken(paths, env = process.env) {
|
|
25239
25331
|
ensureDirs(paths);
|
|
25240
25332
|
const TOKEN_FILE = paths.tokenFile;
|
|
25241
|
-
if (existsSync24(TOKEN_FILE))
|
|
25242
|
-
|
|
25333
|
+
if (existsSync24(TOKEN_FILE)) {
|
|
25334
|
+
const held = readFileSync14(TOKEN_FILE, "utf8").trim();
|
|
25335
|
+
if (held.length >= TOKEN_MIN)
|
|
25336
|
+
return held;
|
|
25337
|
+
if (env.TOYON_CLOUD === "1") {
|
|
25338
|
+
throw new Error(`${TOKEN_FILE} holds no token of ${TOKEN_MIN} characters or more; set TOYON_TOKEN to one`);
|
|
25339
|
+
}
|
|
25340
|
+
log.warn("state", `${TOKEN_FILE} held no usable token; a new one was written`);
|
|
25341
|
+
}
|
|
25243
25342
|
const token = randomBytes7(32).toString("hex");
|
|
25244
25343
|
writeFileSync9(TOKEN_FILE, token, { mode: 384 });
|
|
25245
25344
|
return token;
|
|
@@ -25643,7 +25742,7 @@ var PROBE_TIMEOUT_MS2 = 2000;
|
|
|
25643
25742
|
var ANSWERED_RECENTLY_MS = 3 * 60000;
|
|
25644
25743
|
var probe = async (origin, named) => {
|
|
25645
25744
|
try {
|
|
25646
|
-
const r = await fetch(`${origin}/health`, { signal: AbortSignal.timeout(PROBE_TIMEOUT_MS2) });
|
|
25745
|
+
const r = await fetch(`${origin}/health`, { signal: AbortSignal.timeout(PROBE_TIMEOUT_MS2), redirect: "error" });
|
|
25647
25746
|
if (!r.ok)
|
|
25648
25747
|
return null;
|
|
25649
25748
|
const body = await r.json();
|
|
@@ -26208,9 +26307,8 @@ class DesignService {
|
|
|
26208
26307
|
const wt = this.readable(worktreeId);
|
|
26209
26308
|
if (!wt)
|
|
26210
26309
|
throw new UserError("unknown worktree");
|
|
26211
|
-
const listed = await
|
|
26212
|
-
const paths = listed.out
|
|
26213
|
-
`).filter(Boolean);
|
|
26310
|
+
const listed = await gitRaw(wt.path, "ls-files", "-z", "-co", "--exclude-standard");
|
|
26311
|
+
const paths = nulSplit(listed.out);
|
|
26214
26312
|
const cssPaths = paths.filter((p) => CSS_EXT.test(p)).slice(0, MAX_CSS_FILES);
|
|
26215
26313
|
const srcPaths = paths.filter((p) => SOURCE_EXT.test(p)).slice(0, MAX_SOURCE_FILES);
|
|
26216
26314
|
const cssFiles = await readAll(wt.path, cssPaths);
|
|
@@ -27033,7 +27131,8 @@ function looksBinary(text) {
|
|
|
27033
27131
|
|
|
27034
27132
|
// ../daemon/src/files/service.ts
|
|
27035
27133
|
import { spawn as spawn5 } from "child_process";
|
|
27036
|
-
import { stat as stat3, unlink } from "fs/promises";
|
|
27134
|
+
import { lstat as lstat2, stat as stat3, unlink } from "fs/promises";
|
|
27135
|
+
import { join as join24 } from "path";
|
|
27037
27136
|
|
|
27038
27137
|
// ../daemon/src/git/forge.ts
|
|
27039
27138
|
function githubRepoUrl(remoteUrl) {
|
|
@@ -27150,10 +27249,9 @@ async function treeFingerprint(worktreePath) {
|
|
|
27150
27249
|
const [head, diff, untracked] = await Promise.all([
|
|
27151
27250
|
git(worktreePath, "rev-parse", "HEAD"),
|
|
27152
27251
|
gitRaw(worktreePath, "diff", "HEAD", "--no-renames"),
|
|
27153
|
-
|
|
27252
|
+
gitRaw(worktreePath, "ls-files", "-z", "--others", "--exclude-standard")
|
|
27154
27253
|
]);
|
|
27155
|
-
const others = await Promise.all(untracked.out.
|
|
27156
|
-
`).filter(Boolean).map(async (p) => {
|
|
27254
|
+
const others = await Promise.all(nulSplit(untracked.out).map(async (p) => {
|
|
27157
27255
|
try {
|
|
27158
27256
|
const f = Bun.file(join23(worktreePath, p));
|
|
27159
27257
|
const body = f.size > 2000000 ? String(f.size) : Bun.hash(await f.arrayBuffer()).toString(36);
|
|
@@ -27169,7 +27267,7 @@ async function statusFiles(worktreePath, ...only) {
|
|
|
27169
27267
|
return (await readStatus(worktreePath, only)).files;
|
|
27170
27268
|
}
|
|
27171
27269
|
async function readStatus(worktreePath, only) {
|
|
27172
|
-
const args = ["status", "--porcelain", "-uall"];
|
|
27270
|
+
const args = ["status", "--porcelain", "-z", "-uall"];
|
|
27173
27271
|
const r = only.length ? await gitRaw(worktreePath, "--literal-pathspecs", ...args, "--", ...only) : await gitRaw(worktreePath, ...args);
|
|
27174
27272
|
if (!r.ok)
|
|
27175
27273
|
throw new Error(`git status failed: ${r.err}`);
|
|
@@ -27194,7 +27292,7 @@ function foldDropped(entries) {
|
|
|
27194
27292
|
};
|
|
27195
27293
|
}
|
|
27196
27294
|
async function diskAgainstHead(worktreePath, file) {
|
|
27197
|
-
const r = await
|
|
27295
|
+
const r = await gitRaw(worktreePath, "diff", "--numstat", "-z", "--end-of-options", `HEAD:${file}`, file);
|
|
27198
27296
|
if (!r.ok)
|
|
27199
27297
|
return {};
|
|
27200
27298
|
if (!r.out)
|
|
@@ -27203,14 +27301,18 @@ async function diskAgainstHead(worktreePath, file) {
|
|
|
27203
27301
|
return counts ?? {};
|
|
27204
27302
|
}
|
|
27205
27303
|
function parsePorcelain(out) {
|
|
27206
|
-
const
|
|
27207
|
-
|
|
27208
|
-
|
|
27209
|
-
const
|
|
27210
|
-
|
|
27211
|
-
|
|
27212
|
-
|
|
27213
|
-
|
|
27304
|
+
const files = [];
|
|
27305
|
+
const records = out.split("\x00");
|
|
27306
|
+
for (let i = 0;i < records.length; i++) {
|
|
27307
|
+
const record = records[i] ?? "";
|
|
27308
|
+
if (record.length <= 3)
|
|
27309
|
+
continue;
|
|
27310
|
+
const xy = record.slice(0, 2);
|
|
27311
|
+
files.push({ xy, path: record.slice(3) });
|
|
27312
|
+
if (xy.includes("R") || xy.includes("C"))
|
|
27313
|
+
i++;
|
|
27314
|
+
}
|
|
27315
|
+
return files;
|
|
27214
27316
|
}
|
|
27215
27317
|
async function fileBefore(worktreePath, defaultBr, file) {
|
|
27216
27318
|
const base = await git(worktreePath, "merge-base", "HEAD", defaultBr);
|
|
@@ -27224,18 +27326,37 @@ async function fileAt(worktreePath, commit, file) {
|
|
|
27224
27326
|
}
|
|
27225
27327
|
function parseNumstat(out) {
|
|
27226
27328
|
const counts = new Map;
|
|
27227
|
-
|
|
27228
|
-
|
|
27229
|
-
const [a, d, ...rest] =
|
|
27230
|
-
|
|
27329
|
+
const records = out.split("\x00");
|
|
27330
|
+
for (let i = 0;i < records.length; i++) {
|
|
27331
|
+
const [a, d, ...rest] = (records[i] ?? "").split("\t");
|
|
27332
|
+
if (d === undefined)
|
|
27333
|
+
continue;
|
|
27334
|
+
let path = rest.join("\t");
|
|
27335
|
+
if (!path) {
|
|
27336
|
+
path = records[i + 2] ?? "";
|
|
27337
|
+
i += 2;
|
|
27338
|
+
}
|
|
27231
27339
|
if (!path)
|
|
27232
27340
|
continue;
|
|
27233
27341
|
counts.set(path, a === "-" || d === "-" ? {} : { add: Number(a), del: Number(d) });
|
|
27234
27342
|
}
|
|
27235
27343
|
return counts;
|
|
27236
27344
|
}
|
|
27345
|
+
function parseNameStatus(out) {
|
|
27346
|
+
const files = [];
|
|
27347
|
+
const records = out.split("\x00");
|
|
27348
|
+
for (let i = 0;i + 1 < records.length; i += 2) {
|
|
27349
|
+
const status = (records[i] ?? "").slice(0, 1);
|
|
27350
|
+
if (status === "R" || status === "C")
|
|
27351
|
+
i++;
|
|
27352
|
+
const path = records[i + 1] ?? "";
|
|
27353
|
+
if (status && path)
|
|
27354
|
+
files.push({ status, path });
|
|
27355
|
+
}
|
|
27356
|
+
return files;
|
|
27357
|
+
}
|
|
27237
27358
|
async function numstat(worktreePath, ...range) {
|
|
27238
|
-
const r = await
|
|
27359
|
+
const r = await gitRaw(worktreePath, "diff", "--numstat", "-z", "--no-renames", ...range);
|
|
27239
27360
|
return r.ok ? parseNumstat(r.out) : new Map;
|
|
27240
27361
|
}
|
|
27241
27362
|
async function untrackedLines(worktreePath, file, budget) {
|
|
@@ -27300,16 +27421,11 @@ async function committedFiles(worktreePath, defaultBr) {
|
|
|
27300
27421
|
return filesBetween(worktreePath, base.out, "HEAD");
|
|
27301
27422
|
}
|
|
27302
27423
|
async function filesBetween(cwd, from, to) {
|
|
27303
|
-
const r = await
|
|
27424
|
+
const r = await gitRaw(cwd, "diff", "--name-status", "-z", from, to);
|
|
27304
27425
|
if (!r.ok || !r.out)
|
|
27305
27426
|
return [];
|
|
27306
27427
|
const counts = await numstat(cwd, from, to);
|
|
27307
|
-
return r.out.
|
|
27308
|
-
`).filter(Boolean).map((line) => {
|
|
27309
|
-
const [status, ...rest] = line.split("\t");
|
|
27310
|
-
const path = rest[rest.length - 1] ?? "";
|
|
27311
|
-
return { xy: `${(status ?? "M").slice(0, 1)} `, path, ...counts.get(path) ?? {} };
|
|
27312
|
-
});
|
|
27428
|
+
return parseNameStatus(r.out).map(({ status, path }) => ({ xy: `${status} `, path, ...counts.get(path) ?? {} }));
|
|
27313
27429
|
}
|
|
27314
27430
|
async function changedRanges(worktreePath, defaultBr, file) {
|
|
27315
27431
|
const { files, folded } = await readStatus(worktreePath, [file]);
|
|
@@ -27360,6 +27476,11 @@ async function remoteHead(worktreePath, branch) {
|
|
|
27360
27476
|
async function reaches(worktreePath, sha) {
|
|
27361
27477
|
return (await git(worktreePath, "merge-base", "--is-ancestor", sha, "HEAD")).ok;
|
|
27362
27478
|
}
|
|
27479
|
+
async function aheadOf(worktreePath, base) {
|
|
27480
|
+
const r = await git(worktreePath, "rev-list", "--count", `${base}..HEAD`);
|
|
27481
|
+
const n = r.ok ? Number(r.out) : Number.NaN;
|
|
27482
|
+
return Number.isInteger(n) ? n : null;
|
|
27483
|
+
}
|
|
27363
27484
|
async function aheadBehind(worktreePath, defaultBr) {
|
|
27364
27485
|
const [a, b] = await Promise.all([
|
|
27365
27486
|
git(worktreePath, "rev-list", "--count", `${defaultBr}..HEAD`),
|
|
@@ -27415,18 +27536,17 @@ async function aheadShas(worktreePath, defaultBr) {
|
|
|
27415
27536
|
}
|
|
27416
27537
|
async function commitFiles(worktreePath, sha) {
|
|
27417
27538
|
const [names, nums] = await Promise.all([
|
|
27418
|
-
|
|
27419
|
-
|
|
27539
|
+
gitRaw(worktreePath, "show", "--first-parent", "--name-status", "-z", "--no-renames", "--format=", sha),
|
|
27540
|
+
gitRaw(worktreePath, "show", "--first-parent", "--numstat", "-z", "--no-renames", "--format=", sha)
|
|
27420
27541
|
]);
|
|
27421
27542
|
if (!names.ok || !names.out)
|
|
27422
27543
|
return [];
|
|
27423
27544
|
const counts = nums.ok ? parseNumstat(nums.out) : new Map;
|
|
27424
|
-
return names.out.
|
|
27425
|
-
|
|
27426
|
-
|
|
27427
|
-
|
|
27428
|
-
|
|
27429
|
-
}).filter((f) => f.path);
|
|
27545
|
+
return parseNameStatus(names.out).map(({ status, path }) => ({
|
|
27546
|
+
xy: `${status} `,
|
|
27547
|
+
path,
|
|
27548
|
+
...counts.get(path) ?? {}
|
|
27549
|
+
}));
|
|
27430
27550
|
}
|
|
27431
27551
|
async function fileAtCommit(worktreePath, sha, file) {
|
|
27432
27552
|
const [before, after] = await Promise.all([
|
|
@@ -27556,30 +27676,28 @@ function rankElementSources(el, rows) {
|
|
|
27556
27676
|
// ../daemon/src/files/listing.ts
|
|
27557
27677
|
var SUBMODULE_MODE = "160000";
|
|
27558
27678
|
var entries3 = (out) => out.split("\x00").filter(Boolean);
|
|
27559
|
-
function
|
|
27560
|
-
const gone = new Set(entries3(deleted));
|
|
27679
|
+
function parseTracked(staged) {
|
|
27561
27680
|
const paths = new Set;
|
|
27562
27681
|
const submodules = new Set;
|
|
27682
|
+
const unmerged = new Set;
|
|
27563
27683
|
for (const line of entries3(staged)) {
|
|
27564
27684
|
const tab = line.indexOf("\t");
|
|
27565
27685
|
if (tab === -1)
|
|
27566
27686
|
continue;
|
|
27567
27687
|
const path = line.slice(tab + 1);
|
|
27568
|
-
if (gone.has(path))
|
|
27569
|
-
continue;
|
|
27570
27688
|
if (line.startsWith(SUBMODULE_MODE))
|
|
27571
27689
|
submodules.add(path);
|
|
27572
27690
|
else
|
|
27573
27691
|
paths.add(path);
|
|
27692
|
+
if (line.charAt(tab - 1) !== "0")
|
|
27693
|
+
unmerged.add(path);
|
|
27574
27694
|
}
|
|
27575
|
-
|
|
27576
|
-
paths.add(path);
|
|
27577
|
-
return { paths: [...paths], submodules: [...submodules], ignored: collapse(entries3(ignored)) };
|
|
27695
|
+
return { paths: [...paths], submodules: [...submodules], unmerged: [...unmerged] };
|
|
27578
27696
|
}
|
|
27579
|
-
function
|
|
27697
|
+
function parseIgnored(ignored) {
|
|
27580
27698
|
const out = [];
|
|
27581
27699
|
let under = null;
|
|
27582
|
-
for (const p of
|
|
27700
|
+
for (const p of entries3(ignored).sort()) {
|
|
27583
27701
|
if (under !== null && p.startsWith(under))
|
|
27584
27702
|
continue;
|
|
27585
27703
|
under = p.endsWith("/") ? p : null;
|
|
@@ -27781,15 +27899,25 @@ class FileService {
|
|
|
27781
27899
|
return "restored";
|
|
27782
27900
|
});
|
|
27783
27901
|
}
|
|
27784
|
-
async
|
|
27902
|
+
async tracked(worktreeId) {
|
|
27785
27903
|
const cwd = this.require(worktreeId).path;
|
|
27786
|
-
const [staged,
|
|
27904
|
+
const [staged, dropped] = await Promise.all([
|
|
27787
27905
|
git(cwd, "ls-files", "-z", "-s"),
|
|
27788
|
-
|
|
27789
|
-
git(cwd, "ls-files", "-z", "-d"),
|
|
27790
|
-
git(cwd, "ls-files", "-z", "-o", "-i", "--exclude-standard", "--directory")
|
|
27906
|
+
gitRaw(cwd, "diff", "--cached", "--name-only", "-z", "--diff-filter=D")
|
|
27791
27907
|
]);
|
|
27792
|
-
|
|
27908
|
+
const { paths, submodules, unmerged } = parseTracked(staged.out);
|
|
27909
|
+
const there = async (list) => {
|
|
27910
|
+
const found = await Promise.all(list.map((p) => lstat2(join24(cwd, p)).then(() => true, () => false)));
|
|
27911
|
+
return new Set(list.filter((_, i) => found[i]));
|
|
27912
|
+
};
|
|
27913
|
+
const [merging, kept] = await Promise.all([there(unmerged), there(nulSplit(dropped.out))]);
|
|
27914
|
+
const absent = new Set(unmerged.filter((p) => !merging.has(p)));
|
|
27915
|
+
return { paths: [...absent.size > 0 ? paths.filter((p) => !absent.has(p)) : paths, ...kept], submodules };
|
|
27916
|
+
}
|
|
27917
|
+
async ignored(worktreeId) {
|
|
27918
|
+
const cwd = this.require(worktreeId).path;
|
|
27919
|
+
const ignored = await git(cwd, "ls-files", "-z", "-o", "-i", "--exclude-standard", "--directory");
|
|
27920
|
+
return parseIgnored(ignored.out);
|
|
27793
27921
|
}
|
|
27794
27922
|
async search(worktreeId, query, asker) {
|
|
27795
27923
|
const { path: cwd } = this.require(worktreeId);
|
|
@@ -28087,7 +28215,7 @@ async function viewPrRef(repoPath, ref) {
|
|
|
28087
28215
|
if (preview)
|
|
28088
28216
|
parsed.preview = preview;
|
|
28089
28217
|
}
|
|
28090
|
-
return { pr: { ...parsed, at: Date.now() }, head };
|
|
28218
|
+
return { pr: { ...parsed, ...head ? { head } : {}, at: Date.now() }, head };
|
|
28091
28219
|
}
|
|
28092
28220
|
var QUEUE_QUERY = `query($owner: String!, $name: String!, $number: Int!) {
|
|
28093
28221
|
repository(owner: $owner, name: $name) {
|
|
@@ -28531,7 +28659,7 @@ class AfterLand {
|
|
|
28531
28659
|
// ../daemon/src/repos/registry.ts
|
|
28532
28660
|
import { existsSync as existsSync28, mkdirSync as mkdirSync8, rmSync as rmSync5, writeFileSync as writeFileSync11 } from "fs";
|
|
28533
28661
|
import { homedir as homedir10 } from "os";
|
|
28534
|
-
import { basename as basename9, dirname as dirname10, join as
|
|
28662
|
+
import { basename as basename9, dirname as dirname10, join as join25 } from "path";
|
|
28535
28663
|
|
|
28536
28664
|
// ../daemon/src/git/base.ts
|
|
28537
28665
|
async function resolveBase(repoPath, defaultBranch, route) {
|
|
@@ -28632,7 +28760,7 @@ class RepoRegistry {
|
|
|
28632
28760
|
return `${repo.name} is set up now`;
|
|
28633
28761
|
if (main.promptedAt || main.lastTurn)
|
|
28634
28762
|
return `${repo.name} has a chat now`;
|
|
28635
|
-
if (existsSync28(
|
|
28763
|
+
if (existsSync28(join25(repo.path, ".claude")))
|
|
28636
28764
|
return `an agent has already run in ${repo.name}`;
|
|
28637
28765
|
if ((await statusFiles(repo.path)).length > 0 || !await treeEmpty(repo.path)) {
|
|
28638
28766
|
return `${repo.name} has files in it now`;
|
|
@@ -28788,10 +28916,10 @@ class RepoRegistry {
|
|
|
28788
28916
|
throw new UserError(current.reason);
|
|
28789
28917
|
const was = configTarget(repo.path, !!repo.made);
|
|
28790
28918
|
const rel = configSibling(was, kind);
|
|
28791
|
-
const moved = rel !== was && existsSync28(
|
|
28919
|
+
const moved = rel !== was && existsSync28(join25(repo.path, was));
|
|
28792
28920
|
if (moved && !(kind === "local" && await isTracked(repo.path, was))) {
|
|
28793
28921
|
try {
|
|
28794
|
-
rmSync5(
|
|
28922
|
+
rmSync5(join25(repo.path, was));
|
|
28795
28923
|
} catch (e) {
|
|
28796
28924
|
log.warn(repoId, `could not remove ${was}`, e);
|
|
28797
28925
|
}
|
|
@@ -28800,7 +28928,7 @@ class RepoRegistry {
|
|
|
28800
28928
|
}
|
|
28801
28929
|
if (isLocalConfigFile(rel))
|
|
28802
28930
|
await excludeFromGit(repo.path, rel);
|
|
28803
|
-
const file =
|
|
28931
|
+
const file = join25(repo.path, rel);
|
|
28804
28932
|
try {
|
|
28805
28933
|
mkdirSync8(dirname10(file), { recursive: true });
|
|
28806
28934
|
writeFileSync11(file, configText(repo.path, rel, config));
|
|
@@ -28955,7 +29083,7 @@ class RepoRegistry {
|
|
|
28955
29083
|
}
|
|
28956
29084
|
placeConfig(repo) {
|
|
28957
29085
|
const rel = configTarget(repo.path, !!repo.made);
|
|
28958
|
-
if (isLocalConfigFile(rel) && existsSync28(
|
|
29086
|
+
if (isLocalConfigFile(rel) && existsSync28(join25(repo.path, rel))) {
|
|
28959
29087
|
fireAndForget(repo.id, excludeFromGit(repo.path, rel), "exclude local settings");
|
|
28960
29088
|
}
|
|
28961
29089
|
if (repo.configFile === rel)
|
|
@@ -29048,7 +29176,7 @@ class RepoRegistry {
|
|
|
29048
29176
|
}
|
|
29049
29177
|
|
|
29050
29178
|
// ../daemon/src/routes/service.ts
|
|
29051
|
-
import { join as
|
|
29179
|
+
import { join as join26 } from "path";
|
|
29052
29180
|
|
|
29053
29181
|
// ../daemon/src/routes/fileRouters.ts
|
|
29054
29182
|
var SKIP = /(^|\/)(node_modules|dist|build|out|coverage|\.next|\.nuxt|\.output|\.svelte-kit|\.vercel|\.netlify)\//;
|
|
@@ -30220,13 +30348,12 @@ class RouteService {
|
|
|
30220
30348
|
return scan.inflight;
|
|
30221
30349
|
}
|
|
30222
30350
|
async scan(scan) {
|
|
30223
|
-
const listed = await
|
|
30351
|
+
const listed = await gitRaw(scan.root, "ls-files", "-z", "-co", "--exclude-standard");
|
|
30224
30352
|
if (!listed.ok) {
|
|
30225
30353
|
log.warn("routes", `could not list the files in ${scan.root}: ${listed.err}`);
|
|
30226
30354
|
return [];
|
|
30227
30355
|
}
|
|
30228
|
-
const paths = listed.out
|
|
30229
|
-
`).filter(Boolean);
|
|
30356
|
+
const paths = nulSplit(listed.out);
|
|
30230
30357
|
const manifests = [];
|
|
30231
30358
|
const keptManifests = new Map;
|
|
30232
30359
|
for (const p of paths.filter(isManifest).slice(0, MAX_MANIFESTS)) {
|
|
@@ -30261,7 +30388,7 @@ class RouteService {
|
|
|
30261
30388
|
return mergeRoutes(fileRoutes(paths, roots), code);
|
|
30262
30389
|
}
|
|
30263
30390
|
async cachedRead(root, path, had, kept, decode) {
|
|
30264
|
-
const abs =
|
|
30391
|
+
const abs = join26(root, path);
|
|
30265
30392
|
const stat = await this.fs.stat(abs);
|
|
30266
30393
|
if (!stat)
|
|
30267
30394
|
return null;
|
|
@@ -30286,7 +30413,7 @@ class RouteService {
|
|
|
30286
30413
|
}
|
|
30287
30414
|
}
|
|
30288
30415
|
async hash(root, file) {
|
|
30289
|
-
const abs =
|
|
30416
|
+
const abs = join26(root, file);
|
|
30290
30417
|
const stat = await this.fs.stat(abs);
|
|
30291
30418
|
if (!stat)
|
|
30292
30419
|
return null;
|
|
@@ -30814,7 +30941,7 @@ import { homedir as homedir12, hostname as hostname2 } from "os";
|
|
|
30814
30941
|
// ../daemon/src/agent/config.ts
|
|
30815
30942
|
import { existsSync as existsSync30, readFileSync as readFileSync17 } from "fs";
|
|
30816
30943
|
import { homedir as homedir11 } from "os";
|
|
30817
|
-
import { join as
|
|
30944
|
+
import { join as join27 } from "path";
|
|
30818
30945
|
function agentConfigFiles(spec, repoPath, home = homedir11()) {
|
|
30819
30946
|
const file = (id, label, path) => ({
|
|
30820
30947
|
id,
|
|
@@ -30824,30 +30951,30 @@ function agentConfigFiles(spec, repoPath, home = homedir11()) {
|
|
|
30824
30951
|
});
|
|
30825
30952
|
if (spec.id === "claude") {
|
|
30826
30953
|
return [
|
|
30827
|
-
file("user-config", "user config (MCP servers, projects)",
|
|
30828
|
-
file("user-settings", "user settings (permissions, hooks)",
|
|
30829
|
-
...repoPath ? [file("project-mcp", "project MCP servers",
|
|
30830
|
-
...repoPath ? [file("project-settings", "project settings",
|
|
30954
|
+
file("user-config", "user config (MCP servers, projects)", join27(home, ".claude.json")),
|
|
30955
|
+
file("user-settings", "user settings (permissions, hooks)", join27(home, ".claude", "settings.json")),
|
|
30956
|
+
...repoPath ? [file("project-mcp", "project MCP servers", join27(repoPath, ".mcp.json"))] : [],
|
|
30957
|
+
...repoPath ? [file("project-settings", "project settings", join27(repoPath, ".claude", "settings.json"))] : []
|
|
30831
30958
|
];
|
|
30832
30959
|
}
|
|
30833
30960
|
if (spec.id === "codex")
|
|
30834
|
-
return [file("user-config", "config",
|
|
30961
|
+
return [file("user-config", "config", join27(home, ".codex", "config.toml"))];
|
|
30835
30962
|
return [];
|
|
30836
30963
|
}
|
|
30837
30964
|
function describeAgentConfig(spec, repoPath, home = homedir11()) {
|
|
30838
30965
|
const files = agentConfigFiles(spec, repoPath, home);
|
|
30839
30966
|
const servers = [];
|
|
30840
30967
|
if (spec.id === "claude") {
|
|
30841
|
-
const user = readJson(
|
|
30968
|
+
const user = readJson(join27(home, ".claude.json"));
|
|
30842
30969
|
servers.push(...serversOf(user?.mcpServers, "user"));
|
|
30843
30970
|
if (repoPath) {
|
|
30844
30971
|
const projects = user?.projects;
|
|
30845
30972
|
const mine = projects?.[repoPath];
|
|
30846
30973
|
servers.push(...serversOf(mine?.mcpServers, "local"));
|
|
30847
|
-
servers.push(...serversOf(readJson(
|
|
30974
|
+
servers.push(...serversOf(readJson(join27(repoPath, ".mcp.json"))?.mcpServers, "project"));
|
|
30848
30975
|
}
|
|
30849
30976
|
} else if (spec.id === "codex") {
|
|
30850
|
-
servers.push(...codexServers(
|
|
30977
|
+
servers.push(...codexServers(join27(home, ".codex", "config.toml")));
|
|
30851
30978
|
}
|
|
30852
30979
|
return { agent: spec.id, files, servers };
|
|
30853
30980
|
}
|
|
@@ -31677,7 +31804,11 @@ var handlers = {
|
|
|
31677
31804
|
fireAndForget(msg.worktreeId, agent.warmCommands(), "warm commands");
|
|
31678
31805
|
},
|
|
31679
31806
|
async "list-files"(msg, ctx, s) {
|
|
31680
|
-
|
|
31807
|
+
const { worktreeId } = msg;
|
|
31808
|
+
await Promise.all([
|
|
31809
|
+
msg.tracked && s.files.tracked(worktreeId).then((l) => ctx.reply({ t: "files", worktreeId, ...l })),
|
|
31810
|
+
s.files.ignored(worktreeId).then((ignored) => ctx.reply({ t: "ignored-files", worktreeId, ignored }))
|
|
31811
|
+
]);
|
|
31681
31812
|
},
|
|
31682
31813
|
async search(msg, ctx, s) {
|
|
31683
31814
|
const page = await s.files.search(msg.worktreeId, msg.query, ctx.socket);
|
|
@@ -32045,7 +32176,7 @@ async function dispatch(msg, ctx, s) {
|
|
|
32045
32176
|
// ../daemon/src/server/http.ts
|
|
32046
32177
|
import { existsSync as existsSync31 } from "fs";
|
|
32047
32178
|
import { hostname } from "os";
|
|
32048
|
-
import { join as
|
|
32179
|
+
import { join as join28 } from "path";
|
|
32049
32180
|
|
|
32050
32181
|
// ../daemon/src/server/cors.ts
|
|
32051
32182
|
var ALLOW_HEADERS = "authorization, content-type";
|
|
@@ -32118,14 +32249,20 @@ function pairingRoutes(d) {
|
|
|
32118
32249
|
if (url.pathname === "/knock" && req.method === "POST") {
|
|
32119
32250
|
const from = req.headers.get("origin");
|
|
32120
32251
|
const own = from === null || d.origins.isOwn(from);
|
|
32121
|
-
const
|
|
32122
|
-
|
|
32123
|
-
|
|
32252
|
+
const client = door.kind === "shell" ? door.client : null;
|
|
32253
|
+
const full = () => new Response("too many devices are waiting to be let in", { status: 429 });
|
|
32254
|
+
if (!own) {
|
|
32255
|
+
const placed = front || d.origins.isLoopback(from);
|
|
32256
|
+
if (!isShellOrigin(from) || !placed || d.origins.isPreview(from)) {
|
|
32257
|
+
return new Response("not a Toyon page", { status: 403 });
|
|
32258
|
+
}
|
|
32259
|
+
if (!d.knocks.room(client))
|
|
32260
|
+
return full();
|
|
32261
|
+
if (!await d.tailnet.isToyon(from))
|
|
32262
|
+
return new Response("not a Toyon page", { status: 403 });
|
|
32124
32263
|
}
|
|
32125
|
-
const knocked = d.knocks.knock(own ? null : from,
|
|
32126
|
-
|
|
32127
|
-
return new Response("too many devices are waiting to be let in", { status: 429 });
|
|
32128
|
-
return json(knocked);
|
|
32264
|
+
const knocked = d.knocks.knock(own ? null : from, client);
|
|
32265
|
+
return knocked ? json(knocked) : full();
|
|
32129
32266
|
}
|
|
32130
32267
|
const polled = knockPolled(url);
|
|
32131
32268
|
if (polled !== null && req.method === "GET") {
|
|
@@ -32235,6 +32372,11 @@ function attachedHeaders(imageType) {
|
|
|
32235
32372
|
...imageType ? {} : { "content-security-policy": "sandbox" }
|
|
32236
32373
|
};
|
|
32237
32374
|
}
|
|
32375
|
+
var DRAWN_HEADERS = {
|
|
32376
|
+
"cache-control": NO_STORE2,
|
|
32377
|
+
"x-content-type-options": "nosniff",
|
|
32378
|
+
"content-security-policy": "sandbox"
|
|
32379
|
+
};
|
|
32238
32380
|
function createFetch(opts) {
|
|
32239
32381
|
const grant = previewGrant(opts.token);
|
|
32240
32382
|
const authed = (req) => sameSecret(req.headers.get("authorization"), `Bearer ${opts.token}`);
|
|
@@ -32526,7 +32668,7 @@ function createFetch(opts) {
|
|
|
32526
32668
|
if (!path)
|
|
32527
32669
|
return new Response("not found", { status: 404 });
|
|
32528
32670
|
return new Response(Bun.file(path), {
|
|
32529
|
-
headers:
|
|
32671
|
+
headers: DRAWN_HEADERS
|
|
32530
32672
|
});
|
|
32531
32673
|
}
|
|
32532
32674
|
if (url.pathname.startsWith("/loose/")) {
|
|
@@ -32536,11 +32678,11 @@ function createFetch(opts) {
|
|
|
32536
32678
|
if (!path)
|
|
32537
32679
|
return new Response("not found", { status: 404 });
|
|
32538
32680
|
return new Response(Bun.file(path), {
|
|
32539
|
-
headers:
|
|
32681
|
+
headers: DRAWN_HEADERS
|
|
32540
32682
|
});
|
|
32541
32683
|
}
|
|
32542
32684
|
const rel = url.pathname === "/" ? "/index.html" : url.pathname;
|
|
32543
|
-
const file =
|
|
32685
|
+
const file = join28(opts.shellDist, rel.replaceAll("..", ""));
|
|
32544
32686
|
const hashed = rel.startsWith("/assets/");
|
|
32545
32687
|
if (existsSync31(file) && Bun.file(file).size > 0) {
|
|
32546
32688
|
if (rel === "/manifest.json") {
|
|
@@ -32574,7 +32716,7 @@ function createFetch(opts) {
|
|
|
32574
32716
|
}
|
|
32575
32717
|
if (hashed)
|
|
32576
32718
|
return new Response("not found", { status: 404 });
|
|
32577
|
-
const index =
|
|
32719
|
+
const index = join28(opts.shellDist, "index.html");
|
|
32578
32720
|
if (existsSync31(index))
|
|
32579
32721
|
return page(req, index);
|
|
32580
32722
|
return new Response("Toyon daemon running; shell not built (run: bun run build)", { status: 200 });
|
|
@@ -33129,14 +33271,14 @@ function startServer(opts) {
|
|
|
33129
33271
|
// ../daemon/src/themes/store.ts
|
|
33130
33272
|
import { existsSync as existsSync32, readdirSync as readdirSync8, readFileSync as readFileSync18, statSync as statSync11, writeFileSync as writeFileSync12 } from "fs";
|
|
33131
33273
|
import { homedir as homedir13 } from "os";
|
|
33132
|
-
import { basename as basename10, dirname as dirname11, join as
|
|
33274
|
+
import { basename as basename10, dirname as dirname11, join as join29, resolve as resolve11 } from "path";
|
|
33133
33275
|
var home = homedir13();
|
|
33134
33276
|
var defaultExtensionDirs = [
|
|
33135
|
-
|
|
33136
|
-
|
|
33137
|
-
|
|
33138
|
-
|
|
33139
|
-
|
|
33277
|
+
join29(home, ".vscode", "extensions"),
|
|
33278
|
+
join29(home, ".vscode-insiders", "extensions"),
|
|
33279
|
+
join29(home, ".vscode-oss", "extensions"),
|
|
33280
|
+
join29(home, ".cursor", "extensions"),
|
|
33281
|
+
join29(home, ".windsurf", "extensions"),
|
|
33140
33282
|
"/Applications/Visual Studio Code.app/Contents/Resources/app/extensions",
|
|
33141
33283
|
"/Applications/Visual Studio Code - Insiders.app/Contents/Resources/app/extensions",
|
|
33142
33284
|
"/Applications/Cursor.app/Contents/Resources/app/extensions",
|
|
@@ -33220,7 +33362,7 @@ class ThemeStore {
|
|
|
33220
33362
|
name: json?.name ?? base,
|
|
33221
33363
|
source: "file"
|
|
33222
33364
|
});
|
|
33223
|
-
writeFileSync12(
|
|
33365
|
+
writeFileSync12(join29(this.themesDir, `${base}.json`), `${JSON.stringify(theme, null, 2)}
|
|
33224
33366
|
`);
|
|
33225
33367
|
this.load();
|
|
33226
33368
|
return theme;
|
|
@@ -33255,7 +33397,7 @@ function loadDir(dir) {
|
|
|
33255
33397
|
for (const f of readdirSync8(dir)) {
|
|
33256
33398
|
if (!/\.jsonc?$/i.test(f))
|
|
33257
33399
|
continue;
|
|
33258
|
-
const file =
|
|
33400
|
+
const file = join29(dir, f);
|
|
33259
33401
|
const id = `file:${slug(f.replace(/\.(json|jsonc)$/i, ""))}`;
|
|
33260
33402
|
try {
|
|
33261
33403
|
const json = readJsonc(file);
|
|
@@ -33281,8 +33423,8 @@ function discover(dir) {
|
|
|
33281
33423
|
return out;
|
|
33282
33424
|
}
|
|
33283
33425
|
for (const name of entries) {
|
|
33284
|
-
const extDir =
|
|
33285
|
-
const pkgFile =
|
|
33426
|
+
const extDir = join29(dir, name);
|
|
33427
|
+
const pkgFile = join29(extDir, "package.json");
|
|
33286
33428
|
try {
|
|
33287
33429
|
if (!statSync11(extDir).isDirectory() || !existsSync32(pkgFile))
|
|
33288
33430
|
continue;
|
|
@@ -33292,7 +33434,7 @@ function discover(dir) {
|
|
|
33292
33434
|
continue;
|
|
33293
33435
|
const ext = `${pkg.publisher ?? "local"}.${pkg.name ?? name}`;
|
|
33294
33436
|
let nls = {};
|
|
33295
|
-
const nlsFile =
|
|
33437
|
+
const nlsFile = join29(extDir, "package.nls.json");
|
|
33296
33438
|
if (existsSync32(nlsFile)) {
|
|
33297
33439
|
try {
|
|
33298
33440
|
nls = JSON.parse(readFileSync18(nlsFile, "utf8"));
|
|
@@ -33782,7 +33924,7 @@ class BackendShare {
|
|
|
33782
33924
|
import { createHash as createHash4 } from "crypto";
|
|
33783
33925
|
import { existsSync as existsSync33, readFileSync as readFileSync19 } from "fs";
|
|
33784
33926
|
import { mkdir as mkdir5, readdir as readdir2, rename, rm as rm5, stat as stat5, utimes, writeFile as writeFile3 } from "fs/promises";
|
|
33785
|
-
import { join as
|
|
33927
|
+
import { join as join30 } from "path";
|
|
33786
33928
|
|
|
33787
33929
|
// ../daemon/src/core/clone.ts
|
|
33788
33930
|
import { mkdir as mkdir4, rm as rm4 } from "fs/promises";
|
|
@@ -33813,7 +33955,7 @@ var STALE_MS = 30 * 24 * 3600000;
|
|
|
33813
33955
|
var TMP = ".tmp-";
|
|
33814
33956
|
var MANIFEST = "manifest.json";
|
|
33815
33957
|
function defaultTools(dir) {
|
|
33816
|
-
const has = (...files) => files.some((f) => existsSync33(
|
|
33958
|
+
const has = (...files) => files.some((f) => existsSync33(join30(dir, f)));
|
|
33817
33959
|
const out = [];
|
|
33818
33960
|
if (has("bun.lock", "bun.lockb"))
|
|
33819
33961
|
out.push("bun --version");
|
|
@@ -33826,7 +33968,7 @@ function defaultTools(dir) {
|
|
|
33826
33968
|
return out;
|
|
33827
33969
|
}
|
|
33828
33970
|
function withSidecars(root, rel) {
|
|
33829
|
-
return [rel, ...SIDECARS.map((s) => rel + s)].filter((n) => existsSync33(
|
|
33971
|
+
return [rel, ...SIDECARS.map((s) => rel + s)].filter((n) => existsSync33(join30(root, n)));
|
|
33830
33972
|
}
|
|
33831
33973
|
var entryName = (base, at) => `${base.slice(0, 12)}-${at.toString(36)}`;
|
|
33832
33974
|
async function listEntries(familyDir) {
|
|
@@ -33841,8 +33983,8 @@ async function listEntries(familyDir) {
|
|
|
33841
33983
|
const m = /^([0-9a-f]{12})-([0-9a-z]+)$/.exec(name);
|
|
33842
33984
|
if (!m?.[1] || !m[2])
|
|
33843
33985
|
continue;
|
|
33844
|
-
const dir =
|
|
33845
|
-
if (!existsSync33(
|
|
33986
|
+
const dir = join30(familyDir, name);
|
|
33987
|
+
if (!existsSync33(join30(dir, MANIFEST)))
|
|
33846
33988
|
continue;
|
|
33847
33989
|
out.push({ dir, base: m[1], at: Number.parseInt(m[2], 36) });
|
|
33848
33990
|
}
|
|
@@ -33872,7 +34014,7 @@ class ArtifactCache {
|
|
|
33872
34014
|
try {
|
|
33873
34015
|
const family = await this.family(wt, repo, policy);
|
|
33874
34016
|
const base = await this.baseCommit(wt, repo);
|
|
33875
|
-
const entries = await listEntries(
|
|
34017
|
+
const entries = await listEntries(join30(this.d.paths.cacheDir, repo.id, family.key));
|
|
33876
34018
|
const entry = entries.find((e) => e.base === base.slice(0, 12)) ?? entries[0];
|
|
33877
34019
|
if (!entry) {
|
|
33878
34020
|
say(`cache: nothing kept for these lockfiles and tools on ${family.platform}`);
|
|
@@ -33880,14 +34022,14 @@ class ArtifactCache {
|
|
|
33880
34022
|
}
|
|
33881
34023
|
const restored = [];
|
|
33882
34024
|
for (const rel of policy.paths) {
|
|
33883
|
-
if (existsSync33(
|
|
34025
|
+
if (existsSync33(join30(wt.path, rel)))
|
|
33884
34026
|
continue;
|
|
33885
34027
|
const names = withSidecars(entry.dir, rel);
|
|
33886
34028
|
if (names.length === 0)
|
|
33887
34029
|
continue;
|
|
33888
34030
|
let whole = true;
|
|
33889
34031
|
for (const name of names) {
|
|
33890
|
-
const r = await (this.d.clone ?? cloneTree)(
|
|
34032
|
+
const r = await (this.d.clone ?? cloneTree)(join30(entry.dir, name), join30(wt.path, name));
|
|
33891
34033
|
if (r.ok)
|
|
33892
34034
|
continue;
|
|
33893
34035
|
say(`cache: could not restore ${name}: ${r.error}`);
|
|
@@ -33921,12 +34063,12 @@ class ArtifactCache {
|
|
|
33921
34063
|
let tmp;
|
|
33922
34064
|
let slot;
|
|
33923
34065
|
try {
|
|
33924
|
-
const present = policy.paths.filter((rel) => existsSync33(
|
|
34066
|
+
const present = policy.paths.filter((rel) => existsSync33(join30(wt.path, rel)));
|
|
33925
34067
|
if (present.length === 0)
|
|
33926
34068
|
return;
|
|
33927
34069
|
const family = await this.family(wt, repo, policy);
|
|
33928
34070
|
const base = await this.baseCommit(wt, repo);
|
|
33929
|
-
const familyDir =
|
|
34071
|
+
const familyDir = join30(this.d.paths.cacheDir, repo.id, family.key);
|
|
33930
34072
|
const key = `${familyDir}/${base.slice(0, 12)}`;
|
|
33931
34073
|
const kept = (await listEntries(familyDir)).find((e) => e.base === base.slice(0, 12));
|
|
33932
34074
|
if (kept || this.making.has(key)) {
|
|
@@ -33937,21 +34079,21 @@ class ArtifactCache {
|
|
|
33937
34079
|
this.making.add(key);
|
|
33938
34080
|
const at = Date.now();
|
|
33939
34081
|
const name = entryName(base, at);
|
|
33940
|
-
tmp =
|
|
34082
|
+
tmp = join30(familyDir, `${TMP}${name}`);
|
|
33941
34083
|
await mkdir5(tmp, { recursive: true });
|
|
33942
34084
|
let how = "clonefile";
|
|
33943
34085
|
for (const rel of present) {
|
|
33944
34086
|
for (const n of withSidecars(wt.path, rel)) {
|
|
33945
|
-
const r = await (this.d.clone ?? cloneTree)(
|
|
34087
|
+
const r = await (this.d.clone ?? cloneTree)(join30(wt.path, n), join30(tmp, n));
|
|
33946
34088
|
if (!r.ok)
|
|
33947
34089
|
throw new Error(`could not keep ${n}: ${r.error}`);
|
|
33948
34090
|
how = r.how;
|
|
33949
34091
|
}
|
|
33950
34092
|
}
|
|
33951
34093
|
const manifest = { at, base, platform: family.platform, tools: family.tools, paths: present };
|
|
33952
|
-
await writeFile3(
|
|
34094
|
+
await writeFile3(join30(tmp, MANIFEST), `${JSON.stringify(manifest, null, 2)}
|
|
33953
34095
|
`);
|
|
33954
|
-
await rename(tmp,
|
|
34096
|
+
await rename(tmp, join30(familyDir, name));
|
|
33955
34097
|
tmp = undefined;
|
|
33956
34098
|
log.info(worktreeId, `cache: kept ${list(present)} for ${base.slice(0, 7)} via ${how} in ${Date.now() - started}ms`);
|
|
33957
34099
|
await this.prune(familyDir);
|
|
@@ -33979,7 +34121,7 @@ class ArtifactCache {
|
|
|
33979
34121
|
return;
|
|
33980
34122
|
}
|
|
33981
34123
|
for (const repoId of repoIds) {
|
|
33982
|
-
const repoDir =
|
|
34124
|
+
const repoDir = join30(root, repoId);
|
|
33983
34125
|
if (!this.d.state.repo(repoId)) {
|
|
33984
34126
|
await rm5(repoDir, { recursive: true, force: true });
|
|
33985
34127
|
continue;
|
|
@@ -33991,7 +34133,7 @@ class ArtifactCache {
|
|
|
33991
34133
|
continue;
|
|
33992
34134
|
}
|
|
33993
34135
|
for (const family of families) {
|
|
33994
|
-
const familyDir =
|
|
34136
|
+
const familyDir = join30(repoDir, family);
|
|
33995
34137
|
let names;
|
|
33996
34138
|
try {
|
|
33997
34139
|
names = await readdir2(familyDir);
|
|
@@ -34000,7 +34142,7 @@ class ArtifactCache {
|
|
|
34000
34142
|
}
|
|
34001
34143
|
for (const n of names)
|
|
34002
34144
|
if (n.startsWith(TMP))
|
|
34003
|
-
await rm5(
|
|
34145
|
+
await rm5(join30(familyDir, n), { recursive: true, force: true });
|
|
34004
34146
|
const entries = await listEntries(familyDir);
|
|
34005
34147
|
let newest = 0;
|
|
34006
34148
|
for (const e of entries) {
|
|
@@ -34020,7 +34162,7 @@ class ArtifactCache {
|
|
|
34020
34162
|
for (const f of policy.key) {
|
|
34021
34163
|
h.update(`\x00${f}\x00`);
|
|
34022
34164
|
try {
|
|
34023
|
-
h.update(readFileSync19(
|
|
34165
|
+
h.update(readFileSync19(join30(wt.path, f)));
|
|
34024
34166
|
} catch {
|
|
34025
34167
|
h.update("(absent)");
|
|
34026
34168
|
}
|
|
@@ -34337,7 +34479,7 @@ class FixService {
|
|
|
34337
34479
|
// ../daemon/src/worktrees/handoff.ts
|
|
34338
34480
|
import { createHash as createHash5, randomUUID as randomUUID2 } from "crypto";
|
|
34339
34481
|
import { readFileSync as readFileSync20 } from "fs";
|
|
34340
|
-
import { join as
|
|
34482
|
+
import { join as join31 } from "path";
|
|
34341
34483
|
|
|
34342
34484
|
// ../daemon/src/worktrees/proposals.ts
|
|
34343
34485
|
var PER_TURN_MAX = 3;
|
|
@@ -34853,9 +34995,9 @@ class HandoffService {
|
|
|
34853
34995
|
if (go)
|
|
34854
34996
|
await this.act(worktreeId, id, note?.trim() || undefined);
|
|
34855
34997
|
else
|
|
34856
|
-
await this.decline(worktreeId, id);
|
|
34998
|
+
await this.decline(worktreeId, id, undefined, note?.trim() || undefined);
|
|
34857
34999
|
}
|
|
34858
|
-
async decline(worktreeId, id, reason) {
|
|
35000
|
+
async decline(worktreeId, id, reason, note) {
|
|
34859
35001
|
const wt = this.d.state.worktree(worktreeId);
|
|
34860
35002
|
if (!wt)
|
|
34861
35003
|
return;
|
|
@@ -34869,9 +35011,17 @@ class HandoffService {
|
|
|
34869
35011
|
type: "handoff-declined",
|
|
34870
35012
|
id,
|
|
34871
35013
|
...reason ? { outcome: "failed", reason } : {},
|
|
35014
|
+
...note ? { note } : {},
|
|
34872
35015
|
...plan ? { plan } : {},
|
|
34873
35016
|
ts: Date.now()
|
|
34874
35017
|
});
|
|
35018
|
+
if (!note)
|
|
35019
|
+
return;
|
|
35020
|
+
const text = handoffDeclinedPrompt(proposed.pieces, note);
|
|
35021
|
+
fireAndForget(wt.id, this.idle(wt.id, agent).then(() => agent.send(text, {
|
|
35022
|
+
asked: { kind: "handoff", why: "the list was sent back with a note, so the agent reads it before going on" },
|
|
35023
|
+
keepCards: true
|
|
35024
|
+
})), "pieces declined");
|
|
34875
35025
|
}
|
|
34876
35026
|
async act(worktreeId, id, note, tell = false) {
|
|
34877
35027
|
const wt = this.d.state.worktree(worktreeId);
|
|
@@ -35003,7 +35153,7 @@ ${p.section}`;
|
|
|
35003
35153
|
return;
|
|
35004
35154
|
let markdown;
|
|
35005
35155
|
try {
|
|
35006
|
-
markdown = readFileSync20(
|
|
35156
|
+
markdown = readFileSync20(join31(wt.path, card.plan), "utf8");
|
|
35007
35157
|
} catch (e) {
|
|
35008
35158
|
log.warn(worktreeId, `could not read ${card.plan} to see whether the plan moved`, e);
|
|
35009
35159
|
return;
|
|
@@ -35626,6 +35776,32 @@ class LandingService {
|
|
|
35626
35776
|
return;
|
|
35627
35777
|
fireAndForget(worktreeId, this.run(wt, { at: Date.now(), ask: false, quiet: true }), "landing recheck");
|
|
35628
35778
|
}
|
|
35779
|
+
async message(worktreeId) {
|
|
35780
|
+
const wt = this.d.state.worktree(worktreeId);
|
|
35781
|
+
if (!wt || !this.d.judge)
|
|
35782
|
+
return;
|
|
35783
|
+
const repo = this.d.state.requireRepo(wt.repoId);
|
|
35784
|
+
let verdict = null;
|
|
35785
|
+
try {
|
|
35786
|
+
verdict = await this.d.judge(wt, landPrompt(await this.landInput(wt, repo)));
|
|
35787
|
+
} catch (e) {
|
|
35788
|
+
log.warn(worktreeId, "landing message unanswered at the press", e);
|
|
35789
|
+
}
|
|
35790
|
+
if (!verdict?.subject)
|
|
35791
|
+
return;
|
|
35792
|
+
const now = this.d.state.worktree(worktreeId)?.landing;
|
|
35793
|
+
if (now && !now.subject) {
|
|
35794
|
+
const { unanswered: _owed, ...rest } = now;
|
|
35795
|
+
this.d.worktrees.setLanding(worktreeId, {
|
|
35796
|
+
...rest,
|
|
35797
|
+
subject: verdict.subject,
|
|
35798
|
+
...verdict.body ? { body: verdict.body } : {}
|
|
35799
|
+
});
|
|
35800
|
+
}
|
|
35801
|
+
return verdict.body ? `${verdict.subject}
|
|
35802
|
+
|
|
35803
|
+
${verdict.body}` : verdict.subject;
|
|
35804
|
+
}
|
|
35629
35805
|
async landInput(wt, repo) {
|
|
35630
35806
|
const base = baseOf(repo);
|
|
35631
35807
|
const entries = this.d.transcript(wt.id);
|
|
@@ -35793,15 +35969,16 @@ function tail(text) {
|
|
|
35793
35969
|
const t = text.replace(/\s+$/, "");
|
|
35794
35970
|
return t.length > TAIL_CHARS ? t.slice(-TAIL_CHARS) : t;
|
|
35795
35971
|
}
|
|
35972
|
+
var PLAIN_NAMES = ["-c", "core.quotePath=false"];
|
|
35796
35973
|
async function diffSummary(id, path, defaultBr) {
|
|
35797
35974
|
const [base, dirty, untracked] = await Promise.all([
|
|
35798
35975
|
git(path, "merge-base", "HEAD", defaultBr),
|
|
35799
|
-
git(path, "diff", "HEAD", "--stat"),
|
|
35800
|
-
git(path, "ls-files", "--others", "--exclude-standard")
|
|
35976
|
+
git(path, ...PLAIN_NAMES, "diff", "HEAD", "--stat"),
|
|
35977
|
+
git(path, ...PLAIN_NAMES, "ls-files", "--others", "--exclude-standard")
|
|
35801
35978
|
]);
|
|
35802
35979
|
if (!base.ok)
|
|
35803
35980
|
log.warn(id, `landing question: no merge base with ${defaultBr}: ${base.err.trim()}`);
|
|
35804
|
-
const committed = base.ok && base.out ? await git(path, "diff", "--stat", base.out, "HEAD") : null;
|
|
35981
|
+
const committed = base.ok && base.out ? await git(path, ...PLAIN_NAMES, "diff", "--stat", base.out, "HEAD") : null;
|
|
35805
35982
|
const own = [dirty.out ? `Uncommitted:
|
|
35806
35983
|
${dirty.out}` : "", untracked.out ? `New files:
|
|
35807
35984
|
${untracked.out}` : ""].filter(Boolean).join(`
|
|
@@ -36022,14 +36199,28 @@ function parseWorktreeList(out) {
|
|
|
36022
36199
|
entries.push(cur);
|
|
36023
36200
|
return entries;
|
|
36024
36201
|
}
|
|
36202
|
+
var C_ESCAPES = { a: 7, b: 8, t: 9, n: 10, v: 11, f: 12, r: 13 };
|
|
36025
36203
|
function unquote(s) {
|
|
36026
|
-
if (!s.startsWith('"') || !s.endsWith('"'))
|
|
36027
|
-
return s;
|
|
36028
|
-
try {
|
|
36029
|
-
return JSON.parse(s);
|
|
36030
|
-
} catch {
|
|
36204
|
+
if (s.length < 2 || !s.startsWith('"') || !s.endsWith('"'))
|
|
36031
36205
|
return s;
|
|
36206
|
+
const bytes = [];
|
|
36207
|
+
const body = Buffer.from(s.slice(1, -1), "utf8");
|
|
36208
|
+
for (let i = 0;i < body.length; i++) {
|
|
36209
|
+
const b = body[i];
|
|
36210
|
+
if (b !== 92) {
|
|
36211
|
+
bytes.push(b);
|
|
36212
|
+
continue;
|
|
36213
|
+
}
|
|
36214
|
+
const octal = /^[0-3][0-7]{2}/.exec(body.subarray(i + 1, i + 4).toString("latin1"));
|
|
36215
|
+
if (octal) {
|
|
36216
|
+
bytes.push(Number.parseInt(octal[0], 8));
|
|
36217
|
+
i += 3;
|
|
36218
|
+
continue;
|
|
36219
|
+
}
|
|
36220
|
+
const next = String.fromCharCode(body[++i] ?? 92);
|
|
36221
|
+
bytes.push(C_ESCAPES[next] ?? next.charCodeAt(0));
|
|
36032
36222
|
}
|
|
36223
|
+
return Buffer.from(bytes).toString("utf8");
|
|
36033
36224
|
}
|
|
36034
36225
|
async function listWorktrees(repoPath) {
|
|
36035
36226
|
const r = await git(repoPath, "worktree", "list", "--porcelain");
|
|
@@ -36267,6 +36458,11 @@ class PrService {
|
|
|
36267
36458
|
this.d = d;
|
|
36268
36459
|
d.hub.on("repoTick", (repoId) => this.refreshRepo(repoId));
|
|
36269
36460
|
d.hub.on("turnSettled", (id) => fireAndForget(id, this.discover(id, "turn"), "pr discovery"));
|
|
36461
|
+
d.hub.on("prDue", (id) => {
|
|
36462
|
+
const wt = this.d.state.worktree(id);
|
|
36463
|
+
if (wt)
|
|
36464
|
+
this.ask(wt);
|
|
36465
|
+
});
|
|
36270
36466
|
this.timer = setInterval(() => this.refreshAll(), d.everyMs ?? POLL_MS3);
|
|
36271
36467
|
this.timer.unref?.();
|
|
36272
36468
|
this.fast = setInterval(() => this.refreshHeld(), d.fastMs ?? FAST_MS);
|
|
@@ -36298,11 +36494,15 @@ class PrService {
|
|
|
36298
36494
|
continue;
|
|
36299
36495
|
if (!wt.pr)
|
|
36300
36496
|
fireAndForget(wt.id, this.discover(wt.id, why), "pr discovery");
|
|
36301
|
-
else
|
|
36302
|
-
|
|
36303
|
-
|
|
36304
|
-
|
|
36305
|
-
|
|
36497
|
+
else
|
|
36498
|
+
this.ask(wt);
|
|
36499
|
+
}
|
|
36500
|
+
}
|
|
36501
|
+
ask(wt) {
|
|
36502
|
+
if (wt.pr?.state === "open")
|
|
36503
|
+
fireAndForget(wt.id, this.refresh(wt.id), "pr refresh");
|
|
36504
|
+
else if (wt.pr?.state === "merged" && !prTaken(wt)) {
|
|
36505
|
+
fireAndForget(wt.id, this.d.worktrees.prMerged(wt.id), "pr landing");
|
|
36306
36506
|
}
|
|
36307
36507
|
}
|
|
36308
36508
|
async discover(worktreeId, why) {
|
|
@@ -36643,7 +36843,7 @@ function rankRefs({ refs, merged, prs, query, defaultBranch, rowIdForPath }) {
|
|
|
36643
36843
|
// ../daemon/src/worktrees/service.ts
|
|
36644
36844
|
import { cpSync, existsSync as existsSync38, readFileSync as readFileSync22, rmSync as rmSync9 } from "fs";
|
|
36645
36845
|
import { rm as rm6 } from "fs/promises";
|
|
36646
|
-
import { dirname as dirname15, join as
|
|
36846
|
+
import { dirname as dirname15, join as join36 } from "path";
|
|
36647
36847
|
|
|
36648
36848
|
// ../daemon/src/git/archive.ts
|
|
36649
36849
|
import { rmSync as rmSync6 } from "fs";
|
|
@@ -36654,7 +36854,7 @@ async function keepState(repoPath, wtPath, ref, indexFile) {
|
|
|
36654
36854
|
const head = await git(wtPath, "rev-parse", "--verify", "HEAD");
|
|
36655
36855
|
if (!head.ok)
|
|
36656
36856
|
return null;
|
|
36657
|
-
const status = await gitRaw(wtPath, "status", "--porcelain", "-uall");
|
|
36857
|
+
const status = await gitRaw(wtPath, "status", "--porcelain", "-z", "-uall");
|
|
36658
36858
|
const files = status.ok ? parsePorcelain(status.out).length : null;
|
|
36659
36859
|
const dirty = files === null || files > 0;
|
|
36660
36860
|
const snapshot = dirty ? await snapshotCommit(wtPath, head.out, indexFile) : null;
|
|
@@ -36703,9 +36903,8 @@ async function keptAt(repoPath, ref) {
|
|
|
36703
36903
|
const snapshot = subject === SNAPSHOT_SUBJECT && author === SNAPSHOT_AUTHOR && parents && !parents.includes(" ");
|
|
36704
36904
|
if (!snapshot)
|
|
36705
36905
|
return { head: commit };
|
|
36706
|
-
const files = await
|
|
36707
|
-
const dirty = files.ok ? files.out.
|
|
36708
|
-
`).filter(Boolean).length : 0;
|
|
36906
|
+
const files = await gitRaw(repoPath, "diff", "--name-only", "-z", parents, commit);
|
|
36907
|
+
const dirty = files.ok ? nulSplit(files.out).length : 0;
|
|
36709
36908
|
return { head: parents, snapshot: commit, ...dirty ? { dirty } : {} };
|
|
36710
36909
|
}
|
|
36711
36910
|
async function checkOutKept(repoPath, wtPath, kept, branch) {
|
|
@@ -36766,8 +36965,357 @@ async function dropLandRefs(repoPath, worktreeId) {
|
|
|
36766
36965
|
await git(repoPath, "update-ref", "-d", ref);
|
|
36767
36966
|
}
|
|
36768
36967
|
|
|
36968
|
+
// ../daemon/src/git/turn.ts
|
|
36969
|
+
import { copyFileSync, existsSync as existsSync34, rmSync as rmSync7 } from "fs";
|
|
36970
|
+
import { join as join32 } from "path";
|
|
36971
|
+
var turnRef = (worktreeId, which) => `refs/toyon/turns/${worktreeId}/${which}`;
|
|
36972
|
+
var SNAPSHOT_SUBJECT2 = "toyon: tree at a send";
|
|
36973
|
+
var queue = new Map;
|
|
36974
|
+
function inTurn(wtPath, job) {
|
|
36975
|
+
const next = (queue.get(wtPath) ?? Promise.resolve()).then(job, job);
|
|
36976
|
+
queue.set(wtPath, next.catch(() => {}));
|
|
36977
|
+
return next;
|
|
36978
|
+
}
|
|
36979
|
+
var synced = new Map;
|
|
36980
|
+
var MAX_PATHS = 400;
|
|
36981
|
+
async function treeNow(wtPath, head, dirty) {
|
|
36982
|
+
let was = synced.get(wtPath);
|
|
36983
|
+
let index = was?.index;
|
|
36984
|
+
if (!index) {
|
|
36985
|
+
const dir = await git(wtPath, "rev-parse", "--absolute-git-dir");
|
|
36986
|
+
if (!dir.ok || !dir.out)
|
|
36987
|
+
return null;
|
|
36988
|
+
index = join32(dir.out, "toyon-turn-index");
|
|
36989
|
+
}
|
|
36990
|
+
const env = { GIT_INDEX_FILE: index };
|
|
36991
|
+
const writeTree = async () => {
|
|
36992
|
+
const tree = await run(GIT, ["write-tree"], wtPath, env);
|
|
36993
|
+
return tree.ok && tree.out ? tree.out : null;
|
|
36994
|
+
};
|
|
36995
|
+
if (!existsSync34(index))
|
|
36996
|
+
was = undefined;
|
|
36997
|
+
const paths = dirty && !dirty.some((f) => /[RC]/.test(f.xy)) ? dirty.map((f) => f.path) : null;
|
|
36998
|
+
if (was && paths && was.head === head) {
|
|
36999
|
+
const again = [...new Set([...was.dirty, ...paths])];
|
|
37000
|
+
if (again.length <= MAX_PATHS) {
|
|
37001
|
+
const added = again.length === 0 || (await run(GIT, ["--literal-pathspecs", "add", "-A", "--", ...again], wtPath, env)).ok;
|
|
37002
|
+
const tree = added ? await writeTree() : null;
|
|
37003
|
+
if (tree) {
|
|
37004
|
+
synced.set(wtPath, { index, head, dirty: new Set(paths) });
|
|
37005
|
+
return tree;
|
|
37006
|
+
}
|
|
37007
|
+
}
|
|
37008
|
+
}
|
|
37009
|
+
synced.delete(wtPath);
|
|
37010
|
+
if (!existsSync34(index)) {
|
|
37011
|
+
const own = join32(index, "..", "index");
|
|
37012
|
+
if (existsSync34(own))
|
|
37013
|
+
copyFileSync(own, index);
|
|
37014
|
+
else if (!(await run(GIT, ["read-tree", "HEAD"], wtPath, env)).ok)
|
|
37015
|
+
return null;
|
|
37016
|
+
}
|
|
37017
|
+
const full = async () => (await run(GIT, ["add", "-A"], wtPath, env)).ok ? writeTree() : null;
|
|
37018
|
+
let tree = await full();
|
|
37019
|
+
if (!tree) {
|
|
37020
|
+
rmSync7(index, { force: true });
|
|
37021
|
+
if (!(await run(GIT, ["read-tree", "HEAD"], wtPath, env)).ok)
|
|
37022
|
+
return null;
|
|
37023
|
+
tree = await full();
|
|
37024
|
+
if (!tree)
|
|
37025
|
+
return null;
|
|
37026
|
+
}
|
|
37027
|
+
const differs = await gitRaw(wtPath, "diff", "--name-only", "--no-renames", "-z", head, tree);
|
|
37028
|
+
if (differs.ok)
|
|
37029
|
+
synced.set(wtPath, { index, head, dirty: new Set(nulSplit(differs.out)) });
|
|
37030
|
+
return tree;
|
|
37031
|
+
}
|
|
37032
|
+
async function snapshotAt(wtPath, ref) {
|
|
37033
|
+
const r = await git(wtPath, "rev-parse", "--quiet", `${ref}^{commit}`, `${ref}^{tree}`, `${ref}^`);
|
|
37034
|
+
const [commit, tree, parent] = r.ok ? r.out.split(`
|
|
37035
|
+
`) : [];
|
|
37036
|
+
return commit && tree && parent ? { commit, tree, parent } : null;
|
|
37037
|
+
}
|
|
37038
|
+
function markTurn(wtPath, worktreeId) {
|
|
37039
|
+
return inTurn(wtPath, async () => {
|
|
37040
|
+
const head = await git(wtPath, "rev-parse", "--verify", "HEAD");
|
|
37041
|
+
if (!head.ok)
|
|
37042
|
+
return;
|
|
37043
|
+
const tree = await treeNow(wtPath, head.out);
|
|
37044
|
+
if (!tree)
|
|
37045
|
+
return;
|
|
37046
|
+
const at = await snapshotAt(wtPath, turnRef(worktreeId, "at"));
|
|
37047
|
+
if (at?.tree === tree && at.parent === head.out)
|
|
37048
|
+
return;
|
|
37049
|
+
const commit = await git(wtPath, "-c", "user.name=toyon", "-c", "user.email=toyon@localhost", "commit-tree", tree, "-p", head.out, "-m", SNAPSHOT_SUBJECT2);
|
|
37050
|
+
if (!commit.ok)
|
|
37051
|
+
return;
|
|
37052
|
+
if (at && at.tree !== tree)
|
|
37053
|
+
await git(wtPath, "update-ref", turnRef(worktreeId, "before"), at.commit);
|
|
37054
|
+
await git(wtPath, "update-ref", turnRef(worktreeId, "at"), commit.out);
|
|
37055
|
+
});
|
|
37056
|
+
}
|
|
37057
|
+
function turnChanges(wtPath, worktreeId, defaultBr, own, dirty) {
|
|
37058
|
+
return inTurn(wtPath, async () => {
|
|
37059
|
+
const at = await snapshotAt(wtPath, turnRef(worktreeId, "at"));
|
|
37060
|
+
if (!at)
|
|
37061
|
+
return null;
|
|
37062
|
+
const head = await git(wtPath, "rev-parse", "HEAD", "HEAD^{tree}");
|
|
37063
|
+
const [headCommit, headTree] = head.ok ? head.out.split(`
|
|
37064
|
+
`) : [];
|
|
37065
|
+
if (!headCommit)
|
|
37066
|
+
return null;
|
|
37067
|
+
const tree = await treeNow(wtPath, headCommit, dirty);
|
|
37068
|
+
if (!tree)
|
|
37069
|
+
return null;
|
|
37070
|
+
const from = at.tree !== tree ? at : await snapshotAt(wtPath, turnRef(worktreeId, "before"));
|
|
37071
|
+
if (!from || from.tree === tree || from.tree === headTree)
|
|
37072
|
+
return null;
|
|
37073
|
+
if (from.parent !== headCommit) {
|
|
37074
|
+
if (!own)
|
|
37075
|
+
return null;
|
|
37076
|
+
const [then, now] = await Promise.all([
|
|
37077
|
+
git(wtPath, "merge-base", from.parent, defaultBr),
|
|
37078
|
+
git(wtPath, "merge-base", headCommit, defaultBr)
|
|
37079
|
+
]);
|
|
37080
|
+
if (!then.ok || !now.ok || then.out !== now.out)
|
|
37081
|
+
return null;
|
|
37082
|
+
}
|
|
37083
|
+
const files = await filesBetween(wtPath, from.commit, tree);
|
|
37084
|
+
return files.length > 0 ? { base: from.commit, files } : null;
|
|
37085
|
+
});
|
|
37086
|
+
}
|
|
37087
|
+
async function dropTurnRefs(repoPath, worktreeId) {
|
|
37088
|
+
for (const which of ["at", "before"])
|
|
37089
|
+
await git(repoPath, "update-ref", "-d", turnRef(worktreeId, which));
|
|
37090
|
+
}
|
|
37091
|
+
|
|
37092
|
+
// ../daemon/src/worktrees/archive.ts
|
|
37093
|
+
import { existsSync as existsSync35, mkdirSync as mkdirSync9, readdirSync as readdirSync9, readFileSync as readFileSync21, renameSync as renameSync7, rmSync as rmSync8, writeFileSync as writeFileSync13 } from "fs";
|
|
37094
|
+
import { basename as basename13, dirname as dirname14, join as join33 } from "path";
|
|
37095
|
+
class WorktreeArchive {
|
|
37096
|
+
dir;
|
|
37097
|
+
records = new Map;
|
|
37098
|
+
constructor(dir) {
|
|
37099
|
+
this.dir = dir;
|
|
37100
|
+
mkdirSync9(dir, { recursive: true });
|
|
37101
|
+
for (const id of readdirSync9(dir)) {
|
|
37102
|
+
const file = this.filesOf(id).record;
|
|
37103
|
+
if (!existsSync35(file))
|
|
37104
|
+
continue;
|
|
37105
|
+
try {
|
|
37106
|
+
this.records.set(id, JSON.parse(readFileSync21(file, "utf8")));
|
|
37107
|
+
} catch (e) {
|
|
37108
|
+
log.warn("archive", `skipped an unreadable record: ${file}`, e);
|
|
37109
|
+
}
|
|
37110
|
+
}
|
|
37111
|
+
}
|
|
37112
|
+
get(id) {
|
|
37113
|
+
return this.records.get(id);
|
|
37114
|
+
}
|
|
37115
|
+
put(rec, from) {
|
|
37116
|
+
const at = this.filesOf(rec.worktree.id);
|
|
37117
|
+
mkdirSync9(at.dir, { recursive: true });
|
|
37118
|
+
move(from.transcript, at.transcript);
|
|
37119
|
+
move(from.attachments, at.attachments);
|
|
37120
|
+
move(from.plans, at.plans);
|
|
37121
|
+
writeFileSync13(at.record, JSON.stringify(rec, null, 2));
|
|
37122
|
+
this.records.set(rec.worktree.id, rec);
|
|
37123
|
+
return { transcript: at.transcript, attachments: at.attachments, plans: at.plans };
|
|
37124
|
+
}
|
|
37125
|
+
take(id, to) {
|
|
37126
|
+
if (!this.records.has(id))
|
|
37127
|
+
return;
|
|
37128
|
+
const at = this.filesOf(id);
|
|
37129
|
+
move(at.transcript, to.transcript);
|
|
37130
|
+
move(at.attachments, to.attachments);
|
|
37131
|
+
move(at.plans, to.plans);
|
|
37132
|
+
this.delete(id);
|
|
37133
|
+
}
|
|
37134
|
+
delete(id) {
|
|
37135
|
+
if (!this.records.has(id))
|
|
37136
|
+
return;
|
|
37137
|
+
rmSync8(this.filesOf(id).dir, { recursive: true, force: true });
|
|
37138
|
+
this.records.delete(id);
|
|
37139
|
+
}
|
|
37140
|
+
list(repo) {
|
|
37141
|
+
return this.recordsOf(repo).sort((a, b) => b.archivedAt - a.archivedAt).map((r) => summarize(r, repo.id, this.filesOf(r.worktree.id).transcript));
|
|
37142
|
+
}
|
|
37143
|
+
recordsOf(repo) {
|
|
37144
|
+
return [...this.records.values()].filter((r) => r.worktree.repoId === repo.id || r.repoPath === repo.path);
|
|
37145
|
+
}
|
|
37146
|
+
save(id) {
|
|
37147
|
+
const rec = this.records.get(id);
|
|
37148
|
+
if (!rec)
|
|
37149
|
+
return;
|
|
37150
|
+
writeFileSync13(this.filesOf(id).record, JSON.stringify(rec, null, 2));
|
|
37151
|
+
}
|
|
37152
|
+
chatFiles(id) {
|
|
37153
|
+
if (!this.records.has(id))
|
|
37154
|
+
return;
|
|
37155
|
+
const at = this.filesOf(id);
|
|
37156
|
+
return { transcript: at.transcript, attachments: at.attachments, plans: at.plans };
|
|
37157
|
+
}
|
|
37158
|
+
planFile(id, rel) {
|
|
37159
|
+
if (!this.records.has(id) || !isPlanPath(rel))
|
|
37160
|
+
return null;
|
|
37161
|
+
const file = join33(this.filesOf(id).plans, basename13(rel));
|
|
37162
|
+
if (!existsSync35(file))
|
|
37163
|
+
return null;
|
|
37164
|
+
try {
|
|
37165
|
+
return readFileSync21(file, "utf8");
|
|
37166
|
+
} catch (e) {
|
|
37167
|
+
log.warn(id, `could not read its archived plan ${rel}`, e);
|
|
37168
|
+
return null;
|
|
37169
|
+
}
|
|
37170
|
+
}
|
|
37171
|
+
filesOf(id) {
|
|
37172
|
+
const dir = join33(this.dir, id);
|
|
37173
|
+
return {
|
|
37174
|
+
dir,
|
|
37175
|
+
record: join33(dir, "record.json"),
|
|
37176
|
+
transcript: join33(dir, "transcript.jsonl"),
|
|
37177
|
+
attachments: join33(dir, "attachments"),
|
|
37178
|
+
plans: join33(dir, "plans")
|
|
37179
|
+
};
|
|
37180
|
+
}
|
|
37181
|
+
}
|
|
37182
|
+
function summarize(r, repoId, transcript) {
|
|
37183
|
+
return {
|
|
37184
|
+
id: r.worktree.id,
|
|
37185
|
+
repoId,
|
|
37186
|
+
title: r.worktree.title,
|
|
37187
|
+
branch: r.worktree.branch,
|
|
37188
|
+
path: r.worktree.path,
|
|
37189
|
+
createdAt: r.worktree.createdAt,
|
|
37190
|
+
archivedAt: r.archivedAt,
|
|
37191
|
+
...r.prompt ? { prompt: r.prompt } : {},
|
|
37192
|
+
restorable: !!r.kept,
|
|
37193
|
+
transcript,
|
|
37194
|
+
...r.sessionId ? { sessionId: r.sessionId } : {},
|
|
37195
|
+
...r.kept?.snapshot ? { uncommitted: true, ...r.kept.dirty ? { dirty: r.kept.dirty } : {} } : {},
|
|
37196
|
+
...r.worktree.landed ? { landed: true } : {},
|
|
37197
|
+
...r.worktree.lands?.length ? { lands: r.worktree.lands } : {},
|
|
37198
|
+
...r.cost !== undefined ? { cost: r.cost } : {},
|
|
37199
|
+
...r.auto ? { auto: r.auto } : {}
|
|
37200
|
+
};
|
|
37201
|
+
}
|
|
37202
|
+
function lastUsage(transcript) {
|
|
37203
|
+
if (!existsSync35(transcript))
|
|
37204
|
+
return null;
|
|
37205
|
+
const lines = readFileSync21(transcript, "utf8").split(`
|
|
37206
|
+
`);
|
|
37207
|
+
for (let i = lines.length - 1;i >= 0; i--) {
|
|
37208
|
+
if (!lines[i]?.includes('"usage"'))
|
|
37209
|
+
continue;
|
|
37210
|
+
try {
|
|
37211
|
+
const e = JSON.parse(lines[i]).event;
|
|
37212
|
+
if (e?.type === "usage")
|
|
37213
|
+
return { used: e.used, size: e.size, ...e.cost !== undefined ? { cost: e.cost } : {} };
|
|
37214
|
+
} catch {}
|
|
37215
|
+
}
|
|
37216
|
+
return null;
|
|
37217
|
+
}
|
|
37218
|
+
function firstPrompt(transcript) {
|
|
37219
|
+
if (!existsSync35(transcript))
|
|
37220
|
+
return;
|
|
37221
|
+
for (const line of readFileSync21(transcript, "utf8").split(`
|
|
37222
|
+
`)) {
|
|
37223
|
+
if (!line.includes('"user-message"') && !line.includes('"handed"'))
|
|
37224
|
+
continue;
|
|
37225
|
+
try {
|
|
37226
|
+
const event = JSON.parse(line).event;
|
|
37227
|
+
if (event?.type === "user-message")
|
|
37228
|
+
return event.text.slice(0, 500);
|
|
37229
|
+
if (event?.type === "handed")
|
|
37230
|
+
return (event.body ?? event.text).slice(0, 500);
|
|
37231
|
+
} catch {}
|
|
37232
|
+
}
|
|
37233
|
+
return;
|
|
37234
|
+
}
|
|
37235
|
+
function move(from, to) {
|
|
37236
|
+
if (!existsSync35(from))
|
|
37237
|
+
return;
|
|
37238
|
+
mkdirSync9(dirname14(to), { recursive: true });
|
|
37239
|
+
rmSync8(to, { recursive: true, force: true });
|
|
37240
|
+
renameSync7(from, to);
|
|
37241
|
+
}
|
|
37242
|
+
|
|
37243
|
+
// ../daemon/src/worktrees/archivedGit.ts
|
|
37244
|
+
class ArchivedGit {
|
|
37245
|
+
repoPath;
|
|
37246
|
+
kept;
|
|
37247
|
+
lands;
|
|
37248
|
+
defaultBr;
|
|
37249
|
+
constructor(repoPath, kept, lands, defaultBr) {
|
|
37250
|
+
this.repoPath = repoPath;
|
|
37251
|
+
this.kept = kept;
|
|
37252
|
+
this.lands = lands;
|
|
37253
|
+
this.defaultBr = defaultBr;
|
|
37254
|
+
}
|
|
37255
|
+
async tailBase() {
|
|
37256
|
+
const r = await git(this.repoPath, "merge-base", this.kept.head, this.defaultBr);
|
|
37257
|
+
return r.ok && r.out ? r.out : this.kept.head;
|
|
37258
|
+
}
|
|
37259
|
+
async status() {
|
|
37260
|
+
const base = await this.tailBase();
|
|
37261
|
+
const { head, snapshot } = this.kept;
|
|
37262
|
+
const [files, committed] = await Promise.all([
|
|
37263
|
+
snapshot ? filesBetween(this.repoPath, head, snapshot) : [],
|
|
37264
|
+
base === head ? [] : filesBetween(this.repoPath, base, head)
|
|
37265
|
+
]);
|
|
37266
|
+
return { files, committed, head: snapshot ?? head };
|
|
37267
|
+
}
|
|
37268
|
+
async log() {
|
|
37269
|
+
const seen = new Set;
|
|
37270
|
+
const unseen = (list) => {
|
|
37271
|
+
const out = list.filter((c) => !seen.has(c.sha));
|
|
37272
|
+
for (const c of out)
|
|
37273
|
+
seen.add(c.sha);
|
|
37274
|
+
return out;
|
|
37275
|
+
};
|
|
37276
|
+
const landed = [];
|
|
37277
|
+
for (const land of this.lands) {
|
|
37278
|
+
const commits = unseen(await logRange(this.repoPath, land.base, land.tip));
|
|
37279
|
+
landed.push(commits.map((c) => ({ ...c, landedAt: land.at })));
|
|
37280
|
+
}
|
|
37281
|
+
const base = await this.tailBase();
|
|
37282
|
+
const tail = base === this.kept.head ? [] : unseen(await logRange(this.repoPath, base, this.kept.head));
|
|
37283
|
+
return [...tail, ...landed.reverse().flat()];
|
|
37284
|
+
}
|
|
37285
|
+
commitFiles(sha) {
|
|
37286
|
+
return commitFiles(this.repoPath, sha);
|
|
37287
|
+
}
|
|
37288
|
+
async file(path, ref) {
|
|
37289
|
+
if (ref)
|
|
37290
|
+
return fileAtCommit(this.repoPath, ref, path);
|
|
37291
|
+
const base = await this.tailBase();
|
|
37292
|
+
const [before, after] = await Promise.all([
|
|
37293
|
+
gitRaw(this.repoPath, "show", `${base}:${path}`),
|
|
37294
|
+
gitRaw(this.repoPath, "show", `${this.kept.snapshot ?? this.kept.head}:${path}`)
|
|
37295
|
+
]);
|
|
37296
|
+
return { before: before.ok ? before.out : "", after: after.ok ? after.out : "" };
|
|
37297
|
+
}
|
|
37298
|
+
}
|
|
37299
|
+
|
|
37300
|
+
// ../daemon/src/worktrees/landSubjects.ts
|
|
37301
|
+
function unfilled(mark) {
|
|
37302
|
+
return mark.subjects === undefined;
|
|
37303
|
+
}
|
|
37304
|
+
async function fillLandMarks(jobs, read, width = 8) {
|
|
37305
|
+
let next = 0;
|
|
37306
|
+
const worker = async () => {
|
|
37307
|
+
while (next < jobs.length) {
|
|
37308
|
+
const job = jobs[next++];
|
|
37309
|
+
if (!job)
|
|
37310
|
+
return;
|
|
37311
|
+
job.mark.subjects = await read(job.cwd, job.mark) ?? [];
|
|
37312
|
+
}
|
|
37313
|
+
};
|
|
37314
|
+
await Promise.all(Array.from({ length: Math.min(width, jobs.length) }, worker));
|
|
37315
|
+
}
|
|
37316
|
+
|
|
36769
37317
|
// ../daemon/src/git/land.ts
|
|
36770
|
-
import { existsSync as
|
|
37318
|
+
import { existsSync as existsSync36 } from "fs";
|
|
36771
37319
|
var STEP_TIMEOUT_MS = RUN_CEILING_MS;
|
|
36772
37320
|
function rowOf(r) {
|
|
36773
37321
|
return r.shown && r.toolId && r.command ? { toolId: r.toolId, command: r.command, text: r.text } : undefined;
|
|
@@ -36864,9 +37412,8 @@ async function putBack(worktreePath, defaultBr, how, aside, taken) {
|
|
|
36864
37412
|
await git(worktreePath, "update-ref", "-d", CARRIED);
|
|
36865
37413
|
return taken;
|
|
36866
37414
|
}
|
|
36867
|
-
const unmerged = await
|
|
36868
|
-
clash = unmerged.out
|
|
36869
|
-
`).filter(Boolean);
|
|
37415
|
+
const unmerged = await gitRaw(worktreePath, "diff", "--name-only", "-z", "--diff-filter=U");
|
|
37416
|
+
clash = nulSplit(unmerged.out);
|
|
36870
37417
|
await git(worktreePath, "reset", "--hard", "-q", aside.head);
|
|
36871
37418
|
}
|
|
36872
37419
|
const back = await git(worktreePath, "stash", "apply", "--index", aside.sha);
|
|
@@ -36901,7 +37448,7 @@ async function midway(worktreePath) {
|
|
|
36901
37448
|
if (!r.ok)
|
|
36902
37449
|
return null;
|
|
36903
37450
|
const at = r.out.split(`
|
|
36904
|
-
`).findIndex((file) => file &&
|
|
37451
|
+
`).findIndex((file) => file && existsSync36(file));
|
|
36905
37452
|
return MIDWAY[at]?.[1] ?? null;
|
|
36906
37453
|
}
|
|
36907
37454
|
async function takeMainIn(worktreePath, defaultBr, own, w = UNWATCHED, carry = false) {
|
|
@@ -37136,8 +37683,9 @@ async function pushLanding(worktreePath, sha, defaultBr, w = UNWATCHED) {
|
|
|
37136
37683
|
}
|
|
37137
37684
|
async function mergeFailure(cwd, m, conflictMessage) {
|
|
37138
37685
|
const conflict = /CONFLICT|Automatic merge failed/.test(m.text);
|
|
37139
|
-
if (conflict)
|
|
37686
|
+
if (conflict || await midway(cwd) === "merge")
|
|
37140
37687
|
await git(cwd, "merge", "--abort");
|
|
37688
|
+
if (conflict) {
|
|
37141
37689
|
const step = rowOf(m);
|
|
37142
37690
|
return {
|
|
37143
37691
|
ok: false,
|
|
@@ -37146,6 +37694,8 @@ async function mergeFailure(cwd, m, conflictMessage) {
|
|
|
37146
37694
|
message: m.shown ? `${conflictMessage}; the conflicts are on the chat` : `${conflictMessage} (${m.text.slice(0, 200)})`
|
|
37147
37695
|
};
|
|
37148
37696
|
}
|
|
37697
|
+
if (m.hook)
|
|
37698
|
+
return stepRefused("merge refused", m);
|
|
37149
37699
|
return {
|
|
37150
37700
|
ok: false,
|
|
37151
37701
|
message: m.shown ? "merge failed: what git printed is on the chat" : `merge failed: ${m.text.slice(0, 300)}`
|
|
@@ -37164,21 +37714,43 @@ async function fetchBase(worktreePath, repo, w = UNWATCHED) {
|
|
|
37164
37714
|
return { ok: false, message: refused3("fetch failed", f) };
|
|
37165
37715
|
return { ok: true, message: `fetched ${base}` };
|
|
37166
37716
|
}
|
|
37167
|
-
async function pushBranch(worktreePath, branch, w = UNWATCHED) {
|
|
37168
|
-
const
|
|
37717
|
+
async function pushBranch(worktreePath, branch, own, w = UNWATCHED) {
|
|
37718
|
+
const theirs = `${branch} on origin holds commits that are not in the branch here: take them in first, then land again`;
|
|
37719
|
+
const tip = own ? await originTip(worktreePath, branch) : null;
|
|
37720
|
+
const behind = tip !== null && !await reaches(worktreePath, tip);
|
|
37721
|
+
if (tip && behind && !await supersedes(worktreePath, branch, tip))
|
|
37722
|
+
return { ok: false, message: theirs };
|
|
37169
37723
|
w.step(`pushing ${branch}`);
|
|
37170
|
-
|
|
37171
|
-
|
|
37172
|
-
|
|
37173
|
-
|
|
37174
|
-
|
|
37175
|
-
|
|
37176
|
-
|
|
37177
|
-
|
|
37178
|
-
|
|
37179
|
-
|
|
37180
|
-
const r = await run(GIT, ["ls-remote", "
|
|
37181
|
-
|
|
37724
|
+
const lease = tip && behind ? [`--force-with-lease=${branch}:${tip}`] : [];
|
|
37725
|
+
const push = await w.git(worktreePath, ["push", "-u", ...lease, "origin", branch]);
|
|
37726
|
+
if (push.ok)
|
|
37727
|
+
return { ok: true, message: `pushed ${branch}` };
|
|
37728
|
+
if (/\[rejected\]|fetch first|non-fast-forward|stale info/.test(push.err))
|
|
37729
|
+
return { ok: false, message: theirs };
|
|
37730
|
+
return stepRefused("push failed", push, 300);
|
|
37731
|
+
}
|
|
37732
|
+
async function originTip(worktreePath, branch) {
|
|
37733
|
+
const ref = `refs/heads/${branch}`;
|
|
37734
|
+
const r = await run(GIT, ["ls-remote", "origin", ref], worktreePath, NO_PROMPT);
|
|
37735
|
+
if (!r.ok)
|
|
37736
|
+
return null;
|
|
37737
|
+
const line = r.out.split(`
|
|
37738
|
+
`).find((l) => l.split("\t")[1] === ref);
|
|
37739
|
+
return line?.split("\t")[0] || null;
|
|
37740
|
+
}
|
|
37741
|
+
async function supersedes(worktreePath, branch, tip) {
|
|
37742
|
+
const [pushed, stood, lacking] = await Promise.all([
|
|
37743
|
+
git(worktreePath, "reflog", "show", "--format=%H %gs", `refs/remotes/origin/${branch}`),
|
|
37744
|
+
git(worktreePath, "reflog", "show", "--format=%H", `refs/heads/${branch}`),
|
|
37745
|
+
git(worktreePath, "rev-list", "--count", "--cherry-pick", "--left-only", `${tip}...HEAD`)
|
|
37746
|
+
]);
|
|
37747
|
+
if (pushed.ok && pushed.out.split(`
|
|
37748
|
+
`).includes(`${tip} update by push`))
|
|
37749
|
+
return true;
|
|
37750
|
+
if (stood.ok && stood.out.split(`
|
|
37751
|
+
`).includes(tip))
|
|
37752
|
+
return true;
|
|
37753
|
+
return lacking.ok && lacking.out === "0";
|
|
37182
37754
|
}
|
|
37183
37755
|
async function openPr(o, w = UNWATCHED) {
|
|
37184
37756
|
const cErr = await requireClean(o.worktreePath);
|
|
@@ -37191,7 +37763,7 @@ async function openPr(o, w = UNWATCHED) {
|
|
|
37191
37763
|
const remote = await git(o.worktreePath, "remote", "get-url", "origin");
|
|
37192
37764
|
if (!remote.ok)
|
|
37193
37765
|
return { ok: false, message: "no 'origin' remote: add one, or land by merging here" };
|
|
37194
|
-
const push = await pushBranch(o.worktreePath, o.branch, w);
|
|
37766
|
+
const push = await pushBranch(o.worktreePath, o.branch, o.own, w);
|
|
37195
37767
|
if (!push.ok)
|
|
37196
37768
|
return push;
|
|
37197
37769
|
w.step("creating the PR");
|
|
@@ -37267,358 +37839,761 @@ function compareUrl(remoteUrl, base, branch) {
|
|
|
37267
37839
|
return `${repo}/compare/${encodeURIComponent(base)}...${encodeURIComponent(branch)}?expand=1`;
|
|
37268
37840
|
}
|
|
37269
37841
|
|
|
37270
|
-
// ../daemon/src/
|
|
37271
|
-
|
|
37272
|
-
|
|
37273
|
-
|
|
37274
|
-
|
|
37275
|
-
|
|
37276
|
-
|
|
37277
|
-
|
|
37278
|
-
|
|
37279
|
-
|
|
37280
|
-
|
|
37281
|
-
|
|
37282
|
-
|
|
37283
|
-
|
|
37284
|
-
|
|
37285
|
-
|
|
37286
|
-
|
|
37287
|
-
const dir = await git(wtPath, "rev-parse", "--absolute-git-dir");
|
|
37288
|
-
if (!dir.ok || !dir.out)
|
|
37289
|
-
return null;
|
|
37290
|
-
index = join31(dir.out, "toyon-turn-index");
|
|
37842
|
+
// ../daemon/src/worktrees/ship.ts
|
|
37843
|
+
var REFUSAL_CHARS = 400;
|
|
37844
|
+
var uncounted = (base) => ({
|
|
37845
|
+
ok: false,
|
|
37846
|
+
message: `could not count the commits ahead of ${base}; try again`
|
|
37847
|
+
});
|
|
37848
|
+
var commandLine = (args) => args.map((a) => /^[\w./:=@,+-]+$/.test(a) ? a : JSON.stringify(a.split(`
|
|
37849
|
+
`, 1)[0])).join(" ");
|
|
37850
|
+
|
|
37851
|
+
class Ship {
|
|
37852
|
+
d;
|
|
37853
|
+
ops = new Map;
|
|
37854
|
+
syncQueued = new Set;
|
|
37855
|
+
handChecked = new Map;
|
|
37856
|
+
handWaits = new Set;
|
|
37857
|
+
constructor(d) {
|
|
37858
|
+
this.d = d;
|
|
37291
37859
|
}
|
|
37292
|
-
|
|
37293
|
-
|
|
37294
|
-
const tree = await run(GIT, ["write-tree"], wtPath, env);
|
|
37295
|
-
return tree.ok && tree.out ? tree.out : null;
|
|
37296
|
-
};
|
|
37297
|
-
if (!existsSync35(index))
|
|
37298
|
-
was = undefined;
|
|
37299
|
-
const paths = dirty && !dirty.some((f) => /[RC]/.test(f.xy)) ? dirty.map((f) => f.path) : null;
|
|
37300
|
-
if (was && paths && was.head === head) {
|
|
37301
|
-
const again = [...new Set([...was.dirty, ...paths])];
|
|
37302
|
-
if (again.length <= MAX_PATHS) {
|
|
37303
|
-
const added = again.length === 0 || (await run(GIT, ["--literal-pathspecs", "add", "-A", "--", ...again], wtPath, env)).ok;
|
|
37304
|
-
const tree = added ? await writeTree() : null;
|
|
37305
|
-
if (tree) {
|
|
37306
|
-
synced.set(wtPath, { index, head, dirty: new Set(paths) });
|
|
37307
|
-
return tree;
|
|
37308
|
-
}
|
|
37309
|
-
}
|
|
37860
|
+
syncWaiting(id) {
|
|
37861
|
+
return this.syncQueued.has(id);
|
|
37310
37862
|
}
|
|
37311
|
-
|
|
37312
|
-
|
|
37313
|
-
|
|
37314
|
-
|
|
37315
|
-
copyFileSync(own, index);
|
|
37316
|
-
else if (!(await run(GIT, ["read-tree", "HEAD"], wtPath, env)).ok)
|
|
37317
|
-
return null;
|
|
37863
|
+
forget(id) {
|
|
37864
|
+
this.syncQueued.delete(id);
|
|
37865
|
+
this.handWaits.delete(id);
|
|
37866
|
+
this.handChecked.delete(id);
|
|
37318
37867
|
}
|
|
37319
|
-
|
|
37320
|
-
|
|
37321
|
-
|
|
37322
|
-
|
|
37323
|
-
|
|
37868
|
+
setPr(worktreeId, pr) {
|
|
37869
|
+
const wt = this.d.state.worktree(worktreeId);
|
|
37870
|
+
if (!wt || wt.pr === undefined && pr === undefined)
|
|
37871
|
+
return;
|
|
37872
|
+
const pending = prPending(wt);
|
|
37873
|
+
if (pr)
|
|
37874
|
+
wt.pr = pr;
|
|
37875
|
+
else
|
|
37876
|
+
delete wt.pr;
|
|
37877
|
+
this.d.state.save();
|
|
37878
|
+
if (pending && !prPending(wt) && this.handChecked.delete(worktreeId))
|
|
37879
|
+
this.d.tree.invalidate(worktreeId);
|
|
37880
|
+
this.d.hub.emit("worktreesChanged");
|
|
37881
|
+
}
|
|
37882
|
+
async refreshLanded(wt, facts) {
|
|
37883
|
+
if (wt.kind === "main")
|
|
37884
|
+
return;
|
|
37885
|
+
const f = facts ?? await this.landedFacts(wt);
|
|
37886
|
+
if (f)
|
|
37887
|
+
this.setLanded(wt, landedNow(wt, f));
|
|
37888
|
+
}
|
|
37889
|
+
async settleLanded(wt) {
|
|
37890
|
+
const repo = this.d.state.repo(wt.repoId);
|
|
37891
|
+
const f = wt.kind === "main" || !repo ? null : await this.landedFacts(wt);
|
|
37892
|
+
delete wt.landing;
|
|
37893
|
+
let held;
|
|
37894
|
+
if (f && repo) {
|
|
37895
|
+
held = notLandedWhy(wt, f, baseOf(repo));
|
|
37896
|
+
if (held) {
|
|
37897
|
+
delete wt.landed;
|
|
37898
|
+
log.warn(wt.id, `landed, but not marked landed: ${held}`);
|
|
37899
|
+
} else
|
|
37900
|
+
wt.landed = true;
|
|
37901
|
+
}
|
|
37902
|
+
this.d.state.save();
|
|
37903
|
+
this.d.hub.emit("worktreesChanged");
|
|
37904
|
+
return held;
|
|
37905
|
+
}
|
|
37906
|
+
async landedFacts(wt) {
|
|
37907
|
+
const repo = this.d.state.repo(wt.repoId);
|
|
37908
|
+
if (!repo)
|
|
37324
37909
|
return null;
|
|
37325
|
-
|
|
37326
|
-
|
|
37910
|
+
try {
|
|
37911
|
+
const [head, ab] = await Promise.all([git(wt.path, "rev-parse", "HEAD"), aheadBehind(wt.path, baseOf(repo))]);
|
|
37912
|
+
if (!head.ok)
|
|
37913
|
+
return null;
|
|
37914
|
+
return { head: head.out, ahead: ab.ahead };
|
|
37915
|
+
} catch (e) {
|
|
37916
|
+
log.warn(wt.id, "could not read the tree for the landed mark", e);
|
|
37327
37917
|
return null;
|
|
37918
|
+
}
|
|
37328
37919
|
}
|
|
37329
|
-
|
|
37330
|
-
|
|
37331
|
-
synced.set(wtPath, { index, head, dirty: new Set(differs.out.split("\x00").filter(Boolean)) });
|
|
37332
|
-
return tree;
|
|
37333
|
-
}
|
|
37334
|
-
async function snapshotAt(wtPath, ref) {
|
|
37335
|
-
const r = await git(wtPath, "rev-parse", "--quiet", `${ref}^{commit}`, `${ref}^{tree}`, `${ref}^`);
|
|
37336
|
-
const [commit, tree, parent] = r.ok ? r.out.split(`
|
|
37337
|
-
`) : [];
|
|
37338
|
-
return commit && tree && parent ? { commit, tree, parent } : null;
|
|
37339
|
-
}
|
|
37340
|
-
function markTurn(wtPath, worktreeId) {
|
|
37341
|
-
return inTurn(wtPath, async () => {
|
|
37342
|
-
const head = await git(wtPath, "rev-parse", "--verify", "HEAD");
|
|
37343
|
-
if (!head.ok)
|
|
37344
|
-
return;
|
|
37345
|
-
const tree = await treeNow(wtPath, head.out);
|
|
37346
|
-
if (!tree)
|
|
37920
|
+
setLanded(wt, landed) {
|
|
37921
|
+
if (!!wt.landed === landed)
|
|
37347
37922
|
return;
|
|
37348
|
-
|
|
37349
|
-
|
|
37923
|
+
if (landed)
|
|
37924
|
+
wt.landed = true;
|
|
37925
|
+
else
|
|
37926
|
+
delete wt.landed;
|
|
37927
|
+
this.d.state.save();
|
|
37928
|
+
this.d.hub.emit("worktreesChanged");
|
|
37929
|
+
}
|
|
37930
|
+
fingerprint(path) {
|
|
37931
|
+
return (this.d.fingerprint ?? treeFingerprint)(path);
|
|
37932
|
+
}
|
|
37933
|
+
setLanding(worktreeId, landing) {
|
|
37934
|
+
const wt = this.d.state.worktree(worktreeId);
|
|
37935
|
+
if (!wt || wt.landing === undefined && landing === undefined)
|
|
37350
37936
|
return;
|
|
37351
|
-
|
|
37352
|
-
|
|
37937
|
+
if (landing)
|
|
37938
|
+
wt.landing = landing;
|
|
37939
|
+
else
|
|
37940
|
+
delete wt.landing;
|
|
37941
|
+
this.d.state.save();
|
|
37942
|
+
this.d.hub.emit("worktreesChanged");
|
|
37943
|
+
}
|
|
37944
|
+
landable(worktreeId, verb) {
|
|
37945
|
+
const pair = this.d.state.requireWorktreeWithRepo(worktreeId);
|
|
37946
|
+
if (isMain(pair.wt))
|
|
37947
|
+
throw new UserError(`${verb} from a worktree, not main`);
|
|
37948
|
+
if (!canLand(pair.wt))
|
|
37949
|
+
throw new UserError(`${pair.wt.title} is a PR under review; it lands upstream, not here`);
|
|
37950
|
+
return pair;
|
|
37951
|
+
}
|
|
37952
|
+
suggestion(wt, typed) {
|
|
37953
|
+
const m = typed?.trim();
|
|
37954
|
+
if (m)
|
|
37955
|
+
return m;
|
|
37956
|
+
const s = wt.landing?.subject;
|
|
37957
|
+
if (!s)
|
|
37353
37958
|
return;
|
|
37354
|
-
|
|
37355
|
-
|
|
37356
|
-
|
|
37357
|
-
}
|
|
37358
|
-
|
|
37359
|
-
|
|
37360
|
-
|
|
37361
|
-
|
|
37362
|
-
|
|
37363
|
-
|
|
37364
|
-
|
|
37365
|
-
|
|
37366
|
-
`) : [];
|
|
37367
|
-
if (!headCommit)
|
|
37368
|
-
return null;
|
|
37369
|
-
const tree = await treeNow(wtPath, headCommit, dirty);
|
|
37370
|
-
if (!tree)
|
|
37371
|
-
return null;
|
|
37372
|
-
const from = at.tree !== tree ? at : await snapshotAt(wtPath, turnRef(worktreeId, "before"));
|
|
37373
|
-
if (!from || from.tree === tree || from.tree === headTree)
|
|
37959
|
+
return wt.landing?.body ? `${s}
|
|
37960
|
+
|
|
37961
|
+
${wt.landing.body}` : s;
|
|
37962
|
+
}
|
|
37963
|
+
commitMessage(wt, typed) {
|
|
37964
|
+
const m = this.suggestion(wt, typed);
|
|
37965
|
+
if (m)
|
|
37966
|
+
return m;
|
|
37967
|
+
throw new UserError("no commit message yet: write one in the changes panel");
|
|
37968
|
+
}
|
|
37969
|
+
async commitIfDirty(wt, typed) {
|
|
37970
|
+
if ((await statusFiles(wt.path)).length === 0)
|
|
37374
37971
|
return null;
|
|
37375
|
-
|
|
37376
|
-
|
|
37377
|
-
|
|
37378
|
-
|
|
37379
|
-
|
|
37380
|
-
|
|
37381
|
-
|
|
37382
|
-
if (!then.ok || !now.ok || then.out !== now.out)
|
|
37383
|
-
return null;
|
|
37972
|
+
return this.commitRecorded(wt, this.commitMessage(wt, typed));
|
|
37973
|
+
}
|
|
37974
|
+
async commitRecorded(wt, message) {
|
|
37975
|
+
const result = await commitWorktree(wt.path, message, this.landWatch(wt));
|
|
37976
|
+
if (result.ok) {
|
|
37977
|
+
await this.verdictSurvives(wt, { dropMessage: true });
|
|
37978
|
+
await this.d.headMoved(wt.id);
|
|
37384
37979
|
}
|
|
37385
|
-
|
|
37386
|
-
|
|
37387
|
-
|
|
37388
|
-
|
|
37389
|
-
|
|
37390
|
-
|
|
37391
|
-
|
|
37392
|
-
}
|
|
37393
|
-
|
|
37394
|
-
|
|
37395
|
-
|
|
37396
|
-
|
|
37397
|
-
|
|
37398
|
-
|
|
37399
|
-
|
|
37400
|
-
|
|
37401
|
-
|
|
37402
|
-
|
|
37403
|
-
|
|
37404
|
-
|
|
37405
|
-
|
|
37406
|
-
|
|
37407
|
-
|
|
37408
|
-
|
|
37409
|
-
|
|
37410
|
-
|
|
37980
|
+
return result;
|
|
37981
|
+
}
|
|
37982
|
+
landWatch(wt) {
|
|
37983
|
+
const repo = this.d.state.requireRepo(wt.repoId);
|
|
37984
|
+
return {
|
|
37985
|
+
step: (name) => this.step(wt.id, name),
|
|
37986
|
+
git: async (cwd, args) => {
|
|
37987
|
+
const where = cwd === wt.path ? "git" : `git -C ${cwd}`;
|
|
37988
|
+
const commit = args[0] === "commit";
|
|
37989
|
+
const ceiling = commit ? timeoutFor(repo.config, "commit") : STEP_TIMEOUT_MS;
|
|
37990
|
+
const handle = commit ? this.d.runs.begin(wt.id, "commit", { timeoutMs: ceiling }) : null;
|
|
37991
|
+
const run = (onText, signal) => stepRun(cwd, args, onText, signal, {
|
|
37992
|
+
timeoutMs: ceiling,
|
|
37993
|
+
...handle ? { onSpawn: handle.spawned } : {},
|
|
37994
|
+
onHook: (hook) => handle?.stage(hook ? `${hook} hook` : undefined)
|
|
37995
|
+
});
|
|
37996
|
+
const watched = async () => {
|
|
37997
|
+
if (!this.d.watch)
|
|
37998
|
+
return run(() => {});
|
|
37999
|
+
const command = `${where} ${commandLine(args)}`;
|
|
38000
|
+
try {
|
|
38001
|
+
return { ...await this.d.watch(wt.id, command, run), command };
|
|
38002
|
+
} catch (e) {
|
|
38003
|
+
log.warn(wt.id, `landing step runs unwatched: ${e instanceof Error ? e.message : e}`);
|
|
38004
|
+
return run(() => {});
|
|
38005
|
+
}
|
|
38006
|
+
};
|
|
38007
|
+
const r = await watched();
|
|
38008
|
+
handle?.finish(r.exit);
|
|
38009
|
+
return r;
|
|
37411
38010
|
}
|
|
38011
|
+
};
|
|
38012
|
+
}
|
|
38013
|
+
async verdictSurvives(wt, opts) {
|
|
38014
|
+
const l = wt.landing;
|
|
38015
|
+
if (!l || l.check === "pending")
|
|
38016
|
+
return;
|
|
38017
|
+
const { subject, body, unanswered, ...kept } = l;
|
|
38018
|
+
const message = opts.dropMessage ? {} : { ...subject ? { subject } : {}, ...body ? { body } : {}, ...unanswered ? { unanswered } : {} };
|
|
38019
|
+
this.setLanding(wt.id, { ...kept, ...message, fingerprint: await this.fingerprint(wt.path) });
|
|
38020
|
+
}
|
|
38021
|
+
async landedByHand(wt, mark) {
|
|
38022
|
+
const repo = this.d.state.requireRepo(wt.repoId);
|
|
38023
|
+
const base = baseOf(repo);
|
|
38024
|
+
log.info(wt.id, `landed on ${base} outside toyon: ${mark.base.slice(0, 7)}..${mark.tip.slice(0, 7)}`);
|
|
38025
|
+
await this.landing(wt, "auto", async () => ({
|
|
38026
|
+
result: { ok: true, message: `landed on ${base} outside toyon` },
|
|
38027
|
+
landed: true,
|
|
38028
|
+
unsaid: true,
|
|
38029
|
+
...await this.settle(wt, repo, mark)
|
|
38030
|
+
}));
|
|
38031
|
+
}
|
|
38032
|
+
async landing(wt, by, run) {
|
|
38033
|
+
const out = await this.ship(wt.id, "land", by, async () => {
|
|
38034
|
+
await this.followBranch(wt);
|
|
38035
|
+
return run();
|
|
38036
|
+
});
|
|
38037
|
+
if (out.result.ok && out.landed)
|
|
38038
|
+
this.noteLanded(wt, out);
|
|
38039
|
+
return out;
|
|
38040
|
+
}
|
|
38041
|
+
async land(worktreeId, message) {
|
|
38042
|
+
const { wt, repo } = this.landable(worktreeId, "land");
|
|
38043
|
+
const out = await this.landing(wt, "press", async () => {
|
|
38044
|
+
const on = await git(wt.path, "branch", "--show-current");
|
|
38045
|
+
if (on.ok && !on.out)
|
|
38046
|
+
throw new UserError(`${wt.title} is detached: check out a branch in it first`);
|
|
38047
|
+
const moved = await this.movedMidTurn(wt);
|
|
38048
|
+
if (moved)
|
|
38049
|
+
return { result: moved };
|
|
38050
|
+
return this.landOp(wt, repo, message);
|
|
38051
|
+
});
|
|
38052
|
+
return { result: this.fixShip(wt, out.result, baseOf(repo)) };
|
|
38053
|
+
}
|
|
38054
|
+
fixShip(wt, result, base) {
|
|
38055
|
+
if (result.ok)
|
|
38056
|
+
return result;
|
|
38057
|
+
if (result.hook?.name === "commit-msg") {
|
|
38058
|
+
this.d.hub.emit("messageRefused", wt.id, result.hook.text.trim().slice(-REFUSAL_CHARS));
|
|
38059
|
+
return { ...result, message: "the commit-msg hook refused the message: another is being written" };
|
|
37412
38060
|
}
|
|
38061
|
+
const { hook } = result;
|
|
38062
|
+
const failure = hook ? { kind: "hook", hook: hook.name, toolId: hook.toolId, command: hook.command, text: hook.text } : result.conflict ? {
|
|
38063
|
+
kind: "conflict",
|
|
38064
|
+
base,
|
|
38065
|
+
how: result.conflict,
|
|
38066
|
+
...result.step ? { step: result.step } : {},
|
|
38067
|
+
...result.carried ? { carried: result.carried } : {}
|
|
38068
|
+
} : null;
|
|
38069
|
+
return failure && this.d.fix.report(wt.id, failure) ? { ...result, asked: true } : result;
|
|
37413
38070
|
}
|
|
37414
|
-
|
|
37415
|
-
|
|
38071
|
+
async movedMidTurn(wt) {
|
|
38072
|
+
if (!isBusy(this.d.runtime.agentFor(wt.id)?.status))
|
|
38073
|
+
return null;
|
|
38074
|
+
const l = wt.landing;
|
|
38075
|
+
if (!l || l.fingerprint === await this.fingerprint(wt.path))
|
|
38076
|
+
return null;
|
|
38077
|
+
return { ok: false, message: "the agent has changed files since the check: land once its turn ends" };
|
|
37416
38078
|
}
|
|
37417
|
-
|
|
37418
|
-
|
|
37419
|
-
|
|
37420
|
-
|
|
37421
|
-
|
|
37422
|
-
|
|
37423
|
-
|
|
37424
|
-
|
|
37425
|
-
|
|
38079
|
+
noteLanded(wt, o) {
|
|
38080
|
+
if (!o.unsaid) {
|
|
38081
|
+
this.d.runtime.ensureAgent(wt).agent.note({
|
|
38082
|
+
type: "landed",
|
|
38083
|
+
message: o.result.message,
|
|
38084
|
+
...o.mark ? { mark: o.mark } : {},
|
|
38085
|
+
...o.url ? { url: o.url } : {},
|
|
38086
|
+
...o.held ? { held: o.held } : {},
|
|
38087
|
+
ts: Date.now()
|
|
38088
|
+
});
|
|
38089
|
+
}
|
|
38090
|
+
this.d.hub.emit("landed", wt.id);
|
|
37426
38091
|
}
|
|
37427
|
-
|
|
37428
|
-
if (
|
|
38092
|
+
async followBranch(wt, current) {
|
|
38093
|
+
if (wt.kind === "main")
|
|
37429
38094
|
return;
|
|
37430
|
-
const
|
|
37431
|
-
|
|
37432
|
-
move(at.attachments, to.attachments);
|
|
37433
|
-
move(at.plans, to.plans);
|
|
37434
|
-
this.delete(id);
|
|
37435
|
-
}
|
|
37436
|
-
delete(id) {
|
|
37437
|
-
if (!this.records.has(id))
|
|
38095
|
+
const branch = current ?? (await git(wt.path, "branch", "--show-current")).out;
|
|
38096
|
+
if (!branch || branch === wt.branch)
|
|
37438
38097
|
return;
|
|
37439
|
-
|
|
37440
|
-
|
|
38098
|
+
log.info(wt.id, `on ${branch} now, not ${wt.branch}: the row follows the checkout`);
|
|
38099
|
+
wt.branch = branch;
|
|
38100
|
+
this.d.state.save();
|
|
38101
|
+
this.d.hub.emit("worktreesChanged");
|
|
37441
38102
|
}
|
|
37442
|
-
|
|
37443
|
-
return this.
|
|
38103
|
+
shippingOf(id) {
|
|
38104
|
+
return this.ops.get(id);
|
|
37444
38105
|
}
|
|
37445
|
-
|
|
37446
|
-
|
|
38106
|
+
async ship(id, op, by, run) {
|
|
38107
|
+
const out = this.ops.get(id);
|
|
38108
|
+
if (out)
|
|
38109
|
+
throw new UserError(`${shipNoun(out.op)} is already running here`);
|
|
38110
|
+
const release = this.d.runtime.agentFor(id)?.hold();
|
|
38111
|
+
this.ops.set(id, { op });
|
|
38112
|
+
if (by === "press")
|
|
38113
|
+
this.d.stampPrompted(id);
|
|
38114
|
+
this.d.hub.emit("worktreesChanged");
|
|
38115
|
+
const started = Date.now();
|
|
38116
|
+
const watchdog = setInterval(() => {
|
|
38117
|
+
const step = this.ops.get(id)?.step;
|
|
38118
|
+
const at = step ? ` at "${step}"` : "";
|
|
38119
|
+
log.warn(id, `${shipNoun(op)} still running after ${describeDuration(Date.now() - started)}${at}`);
|
|
38120
|
+
}, RUN_CEILING_MS);
|
|
38121
|
+
watchdog.unref?.();
|
|
38122
|
+
try {
|
|
38123
|
+
return await run();
|
|
38124
|
+
} finally {
|
|
38125
|
+
clearInterval(watchdog);
|
|
38126
|
+
this.ops.delete(id);
|
|
38127
|
+
release?.();
|
|
38128
|
+
if (this.handWaits.delete(id))
|
|
38129
|
+
this.d.tree.invalidate(id);
|
|
38130
|
+
this.d.hub.emit("worktreesChanged");
|
|
38131
|
+
if (!isBusy(this.d.runtime.agentFor(id)?.status))
|
|
38132
|
+
this.syncAfterTurn(id);
|
|
38133
|
+
}
|
|
37447
38134
|
}
|
|
37448
|
-
|
|
37449
|
-
const
|
|
37450
|
-
if (!
|
|
38135
|
+
step(id, name) {
|
|
38136
|
+
const out = this.ops.get(id);
|
|
38137
|
+
if (!out || out.step === name)
|
|
37451
38138
|
return;
|
|
37452
|
-
|
|
38139
|
+
out.step = name;
|
|
38140
|
+
this.d.hub.emit("worktreesChanged");
|
|
37453
38141
|
}
|
|
37454
|
-
|
|
37455
|
-
if (!this.
|
|
37456
|
-
return;
|
|
37457
|
-
|
|
37458
|
-
|
|
38142
|
+
async messageOwed(wt, typed, w) {
|
|
38143
|
+
if (!this.d.message || this.suggestion(wt, typed))
|
|
38144
|
+
return typed;
|
|
38145
|
+
if ((await statusFiles(wt.path)).length === 0)
|
|
38146
|
+
return typed;
|
|
38147
|
+
w.step("writing the message");
|
|
38148
|
+
return await this.d.message(wt) ?? typed;
|
|
37459
38149
|
}
|
|
37460
|
-
|
|
37461
|
-
|
|
37462
|
-
|
|
37463
|
-
const
|
|
37464
|
-
|
|
37465
|
-
|
|
37466
|
-
|
|
37467
|
-
|
|
37468
|
-
|
|
37469
|
-
|
|
37470
|
-
|
|
38150
|
+
async landOp(wt, repo, typed) {
|
|
38151
|
+
const policy = landPolicy(repo.config);
|
|
38152
|
+
const w = this.landWatch(wt);
|
|
38153
|
+
const message = await this.messageOwed(wt, typed, w);
|
|
38154
|
+
const suggested = this.suggestion(wt, message);
|
|
38155
|
+
if (policy.land === "pr")
|
|
38156
|
+
return this.landPr(wt, repo, policy, message, suggested, w);
|
|
38157
|
+
if (policy.land === "push")
|
|
38158
|
+
return this.landPush(wt, repo, policy, message, suggested, w);
|
|
38159
|
+
return this.landLocal(wt, repo, policy, message, suggested, w);
|
|
38160
|
+
}
|
|
38161
|
+
async landPr(wt, repo, policy, message, suggested, w) {
|
|
38162
|
+
const base = baseOf(repo);
|
|
38163
|
+
const own = hasOwnBranch(wt);
|
|
38164
|
+
if (wt.pr?.state === "merged" && !prTaken(wt))
|
|
38165
|
+
return this.prMergedOp(wt, undefined, w);
|
|
38166
|
+
const fetched = await this.fetchBase(wt, repo, w);
|
|
38167
|
+
if (!fetched.ok)
|
|
38168
|
+
return { result: fetched };
|
|
38169
|
+
if (wt.pr?.state === "open") {
|
|
38170
|
+
const dirty = (await statusFiles(wt.path)).length > 0;
|
|
38171
|
+
const missing = dirty || (await aheadUpstream(wt.path, wt.branch) ?? 0) > 0;
|
|
38172
|
+
if (!missing) {
|
|
38173
|
+
const result = await mergePr(wt.path, wt.pr.number, policy.merge, w);
|
|
38174
|
+
if (result.ok && result.queued)
|
|
38175
|
+
this.setPr(wt.id, { ...wt.pr, queue: true, queued: "waiting", at: Date.now() });
|
|
38176
|
+
return { result };
|
|
38177
|
+
}
|
|
38178
|
+
const committed = await this.commitIfDirty(wt, message);
|
|
38179
|
+
if (committed && !committed.ok)
|
|
38180
|
+
return { result: committed };
|
|
38181
|
+
this.d.tree.invalidate(wt.id);
|
|
38182
|
+
const taken = await takeMainIn(wt.path, base, own, w);
|
|
38183
|
+
if (!taken.ok)
|
|
38184
|
+
return { result: taken };
|
|
38185
|
+
await this.d.headMoved(wt.id);
|
|
38186
|
+
this.d.tree.invalidate(wt.id);
|
|
38187
|
+
const pushed = await pushBranch(wt.path, wt.branch, own, w);
|
|
38188
|
+
if (!pushed.ok)
|
|
38189
|
+
return { result: pushed };
|
|
38190
|
+
await this.d.headMoved(wt.id);
|
|
38191
|
+
this.setLanding(wt.id, undefined);
|
|
38192
|
+
const did = committed ? "committed and pushed" : "pushed";
|
|
38193
|
+
if (wt.pr.queued) {
|
|
38194
|
+
const { queued: _, ...left } = wt.pr;
|
|
38195
|
+
this.setPr(wt.id, { ...left, at: Date.now() });
|
|
38196
|
+
return {
|
|
38197
|
+
result: {
|
|
38198
|
+
ok: true,
|
|
38199
|
+
url: wt.pr.url,
|
|
38200
|
+
message: `${did}; PR #${wt.pr.number} has the new commits and left the merge queue: land it again once it is ready`
|
|
38201
|
+
}
|
|
38202
|
+
};
|
|
38203
|
+
}
|
|
38204
|
+
return { result: { ok: true, url: wt.pr.url, message: `${did}; PR #${wt.pr.number} has the new commits` } };
|
|
38205
|
+
}
|
|
38206
|
+
const committed = await this.commitIfDirty(wt, message);
|
|
38207
|
+
if (committed && !committed.ok)
|
|
38208
|
+
return { result: committed };
|
|
38209
|
+
const mark = await landingMark(wt.path, base);
|
|
38210
|
+
this.d.tree.invalidate(wt.id);
|
|
38211
|
+
const taken = await takeMainIn(wt.path, base, own, w);
|
|
38212
|
+
if (!taken.ok)
|
|
38213
|
+
return { result: taken };
|
|
38214
|
+
await this.d.headMoved(wt.id);
|
|
38215
|
+
const ahead = await aheadOf(wt.path, base);
|
|
38216
|
+
if (ahead === null)
|
|
38217
|
+
return { result: uncounted(base) };
|
|
38218
|
+
if (ahead === 0) {
|
|
38219
|
+
const landed = await this.prMergedOp(wt, mark, w);
|
|
38220
|
+
const on = `origin's ${repo.defaultBranch} has this work already`;
|
|
38221
|
+
return { ...landed, result: { ...landed.result, message: `${on}; ${landed.result.message}` } };
|
|
38222
|
+
}
|
|
38223
|
+
const named = !suggested ? undefined : splitMessage(policy.merge === "squash" ? await squashMessage(wt.path, base, suggested, !!message?.trim()) : suggested);
|
|
38224
|
+
const result = await openPr({
|
|
38225
|
+
worktreePath: wt.path,
|
|
38226
|
+
branch: wt.branch,
|
|
38227
|
+
defaultBr: repo.defaultBranch,
|
|
38228
|
+
base,
|
|
38229
|
+
subject: named?.subject,
|
|
38230
|
+
body: named?.body,
|
|
38231
|
+
automerge: policy.automerge,
|
|
38232
|
+
own,
|
|
38233
|
+
method: policy.merge
|
|
38234
|
+
}, w);
|
|
38235
|
+
if (result.ok) {
|
|
38236
|
+
this.setLanding(wt.id, undefined);
|
|
38237
|
+
if (result.pr)
|
|
38238
|
+
this.setPr(wt.id, { ...result.pr, at: Date.now() });
|
|
37471
38239
|
}
|
|
38240
|
+
return { result };
|
|
37472
38241
|
}
|
|
37473
|
-
|
|
37474
|
-
const
|
|
37475
|
-
|
|
37476
|
-
|
|
37477
|
-
|
|
37478
|
-
|
|
37479
|
-
|
|
37480
|
-
|
|
38242
|
+
landLocal(wt, repo, policy, message, suggested, w) {
|
|
38243
|
+
const base = baseOf(repo);
|
|
38244
|
+
const own = hasOwnBranch(wt);
|
|
38245
|
+
return withRepoLock(repo.path, async () => {
|
|
38246
|
+
const committed = await this.commitIfDirty(wt, message);
|
|
38247
|
+
if (committed && !committed.ok)
|
|
38248
|
+
return { result: committed };
|
|
38249
|
+
const method = policy.merge ?? DEFAULT_MERGE_METHOD;
|
|
38250
|
+
const merged = own && method === "rebase" ? await requireLinear(wt.path, base) : null;
|
|
38251
|
+
if (merged)
|
|
38252
|
+
return { result: merged };
|
|
38253
|
+
const taken = await takeMainIn(wt.path, base, own, w);
|
|
38254
|
+
if (!taken.ok)
|
|
38255
|
+
return { result: taken };
|
|
38256
|
+
const mark = await landingMark(wt.path, base);
|
|
38257
|
+
const squash = method === "squash" ? await squashMessage(wt.path, base, suggested, !!message?.trim()) : "";
|
|
38258
|
+
const landed = await landLocally(wt.path, wt.branch, repo.path, repo.defaultBranch, method, squash, w);
|
|
38259
|
+
if (!landed.ok)
|
|
38260
|
+
return { result: landed };
|
|
38261
|
+
const settled = await this.settle(wt, repo, mark, { squash });
|
|
38262
|
+
const did = committed ? "committed and merged" : "merged";
|
|
38263
|
+
return { result: { ...landed, message: `${did} into ${repo.defaultBranch}` }, landed: true, ...settled };
|
|
38264
|
+
});
|
|
38265
|
+
}
|
|
38266
|
+
async landPush(wt, repo, policy, message, suggested, w) {
|
|
38267
|
+
const base = baseOf(repo);
|
|
38268
|
+
const own = hasOwnBranch(wt);
|
|
38269
|
+
const method = policy.merge ?? DEFAULT_MERGE_METHOD;
|
|
38270
|
+
const committed = await this.commitIfDirty(wt, message);
|
|
38271
|
+
if (committed && !committed.ok)
|
|
38272
|
+
return { result: committed };
|
|
38273
|
+
const read = await landingMark(wt.path, base);
|
|
38274
|
+
if (!read)
|
|
38275
|
+
return { result: { ok: false, message: `nothing to land: no commits ahead of ${base}` } };
|
|
38276
|
+
let sha = "";
|
|
38277
|
+
let already = false;
|
|
38278
|
+
let squash = "";
|
|
38279
|
+
const merged = own && method === "rebase" ? await requireLinear(wt.path, base) : null;
|
|
38280
|
+
if (merged)
|
|
38281
|
+
return { result: merged };
|
|
38282
|
+
for (let attempt = 0;; attempt++) {
|
|
38283
|
+
const fetched = await this.fetchBase(wt, repo, w);
|
|
38284
|
+
if (!fetched.ok)
|
|
38285
|
+
return { result: fetched };
|
|
38286
|
+
this.d.tree.invalidate(wt.id);
|
|
38287
|
+
const taken = await takeMainIn(wt.path, base, own, w);
|
|
38288
|
+
if (!taken.ok)
|
|
38289
|
+
return { result: taken };
|
|
38290
|
+
await this.d.headMoved(wt.id);
|
|
38291
|
+
const ahead = await aheadOf(wt.path, base);
|
|
38292
|
+
if (ahead === null)
|
|
38293
|
+
return { result: uncounted(base) };
|
|
38294
|
+
if (ahead === 0) {
|
|
38295
|
+
already = true;
|
|
38296
|
+
sha = await commitOf(wt.path, base) ?? "";
|
|
38297
|
+
break;
|
|
38298
|
+
}
|
|
38299
|
+
squash = method === "squash" ? await squashMessage(wt.path, base, suggested, !!message?.trim()) : "";
|
|
38300
|
+
this.d.tree.invalidate(wt.id);
|
|
38301
|
+
const built = await landingCommit(wt.path, wt.branch, base, method, squash, own, w);
|
|
38302
|
+
await this.d.headMoved(wt.id);
|
|
38303
|
+
if (!built.ok || !built.sha)
|
|
38304
|
+
return { result: built };
|
|
38305
|
+
sha = built.sha;
|
|
38306
|
+
const pushed = await pushLanding(wt.path, sha, repo.defaultBranch, w);
|
|
38307
|
+
if (pushed.ok)
|
|
38308
|
+
break;
|
|
38309
|
+
if (!pushed.moved)
|
|
38310
|
+
return { result: pushed };
|
|
38311
|
+
if (attempt > 0) {
|
|
38312
|
+
return { result: { ok: false, message: `${base} moved twice while this landed; land again` } };
|
|
38313
|
+
}
|
|
38314
|
+
}
|
|
38315
|
+
if (await commitOf(wt.path, base) !== sha)
|
|
38316
|
+
await this.fetchBase(wt, repo, w);
|
|
38317
|
+
const settled = await withRepoLock(repo.path, () => this.settle(wt, repo, read, { squash }));
|
|
38318
|
+
const followed = await this.d.trunk.catchUp(repo.id);
|
|
38319
|
+
const here = followed.ok ? `${repo.defaultBranch} here ${followed.moved ? "pulled it" : "has it"}` : `${repo.defaultBranch} here was left where it is: ${followed.message}`;
|
|
38320
|
+
const how = { merge: "merged into", squash: "squashed onto", rebase: "rebased onto" }[method];
|
|
38321
|
+
const did = already ? `origin's ${repo.defaultBranch} has this work already` : `${committed ? `committed and ${how}` : how} ${repo.defaultBranch} and pushed`;
|
|
38322
|
+
return { result: { ok: true, message: `${did}; ${here}` }, landed: true, ...settled };
|
|
38323
|
+
}
|
|
38324
|
+
async settle(wt, repo, mark, opts = {}) {
|
|
38325
|
+
const noted = await this.noteLand(repo, wt, mark, opts);
|
|
38326
|
+
if (!opts.stay)
|
|
38327
|
+
await this.restartFromMain(wt, baseOf(repo));
|
|
38328
|
+
this.d.tree.invalidateAll();
|
|
38329
|
+
return { mark: noted, held: await this.settleLanded(wt) };
|
|
38330
|
+
}
|
|
38331
|
+
async noteLand(repo, wt, mark, opts = {}) {
|
|
38332
|
+
if (!mark)
|
|
38333
|
+
return;
|
|
38334
|
+
const n = wt.lands?.length ?? 0;
|
|
38335
|
+
const pinned = await git(repo.path, "update-ref", landRef(wt.id, n), mark.tip);
|
|
38336
|
+
if (!pinned.ok) {
|
|
38337
|
+
log.warn(wt.id, `could not keep the commits it landed: ${pinned.err}`);
|
|
38338
|
+
return;
|
|
38339
|
+
}
|
|
38340
|
+
const noted = {
|
|
38341
|
+
...mark,
|
|
38342
|
+
at: Date.now(),
|
|
38343
|
+
...opts.pr ? { pr: opts.pr.number } : {},
|
|
38344
|
+
subjects: await this.landedWhat(repo, wt, mark, opts)
|
|
37481
38345
|
};
|
|
38346
|
+
wt.lands = [...wt.lands ?? [], noted];
|
|
38347
|
+
this.d.state.save();
|
|
38348
|
+
return noted;
|
|
37482
38349
|
}
|
|
37483
|
-
|
|
37484
|
-
|
|
37485
|
-
|
|
37486
|
-
|
|
37487
|
-
|
|
37488
|
-
|
|
37489
|
-
|
|
37490
|
-
|
|
37491
|
-
|
|
37492
|
-
|
|
37493
|
-
...r.prompt ? { prompt: r.prompt } : {},
|
|
37494
|
-
restorable: !!r.kept,
|
|
37495
|
-
transcript,
|
|
37496
|
-
...r.sessionId ? { sessionId: r.sessionId } : {},
|
|
37497
|
-
...r.kept?.snapshot ? { uncommitted: true, ...r.kept.dirty ? { dirty: r.kept.dirty } : {} } : {},
|
|
37498
|
-
...r.worktree.landed ? { landed: true } : {},
|
|
37499
|
-
...r.worktree.lands?.length ? { lands: r.worktree.lands } : {},
|
|
37500
|
-
...r.cost !== undefined ? { cost: r.cost } : {},
|
|
37501
|
-
...r.auto ? { auto: r.auto } : {}
|
|
37502
|
-
};
|
|
37503
|
-
}
|
|
37504
|
-
function lastUsage(transcript) {
|
|
37505
|
-
if (!existsSync36(transcript))
|
|
37506
|
-
return null;
|
|
37507
|
-
const lines = readFileSync21(transcript, "utf8").split(`
|
|
37508
|
-
`);
|
|
37509
|
-
for (let i = lines.length - 1;i >= 0; i--) {
|
|
37510
|
-
if (!lines[i]?.includes('"usage"'))
|
|
37511
|
-
continue;
|
|
37512
|
-
try {
|
|
37513
|
-
const e = JSON.parse(lines[i]).event;
|
|
37514
|
-
if (e?.type === "usage")
|
|
37515
|
-
return { used: e.used, size: e.size, ...e.cost !== undefined ? { cost: e.cost } : {} };
|
|
37516
|
-
} catch {}
|
|
38350
|
+
async landedWhat(repo, wt, mark, opts) {
|
|
38351
|
+
if (opts.pr?.title)
|
|
38352
|
+
return [opts.pr.title];
|
|
38353
|
+
const subject = opts.squash ? splitMessage(opts.squash).subject : "";
|
|
38354
|
+
if (subject)
|
|
38355
|
+
return [subject];
|
|
38356
|
+
const read = await rangeSubjects(repo.path, mark.base, mark.tip);
|
|
38357
|
+
if (!read)
|
|
38358
|
+
log.warn(wt.id, `could not read what it landed: ${mark.base.slice(0, 7)}..${mark.tip.slice(0, 7)}`);
|
|
38359
|
+
return read ?? [];
|
|
37517
38360
|
}
|
|
37518
|
-
|
|
37519
|
-
|
|
37520
|
-
|
|
37521
|
-
if (!existsSync36(transcript))
|
|
37522
|
-
return;
|
|
37523
|
-
for (const line of readFileSync21(transcript, "utf8").split(`
|
|
37524
|
-
`)) {
|
|
37525
|
-
if (!line.includes('"user-message"') && !line.includes('"handed"'))
|
|
37526
|
-
continue;
|
|
37527
|
-
try {
|
|
37528
|
-
const event = JSON.parse(line).event;
|
|
37529
|
-
if (event?.type === "user-message")
|
|
37530
|
-
return event.text.slice(0, 500);
|
|
37531
|
-
if (event?.type === "handed")
|
|
37532
|
-
return (event.body ?? event.text).slice(0, 500);
|
|
37533
|
-
} catch {}
|
|
38361
|
+
async tipMark(wt) {
|
|
38362
|
+
const head = await git(wt.path, "rev-parse", "HEAD");
|
|
38363
|
+
return head.ok ? { base: head.out, tip: head.out } : null;
|
|
37534
38364
|
}
|
|
37535
|
-
|
|
37536
|
-
|
|
37537
|
-
|
|
37538
|
-
|
|
37539
|
-
|
|
37540
|
-
|
|
37541
|
-
|
|
37542
|
-
|
|
37543
|
-
}
|
|
37544
|
-
|
|
37545
|
-
// ../daemon/src/worktrees/archivedGit.ts
|
|
37546
|
-
class ArchivedGit {
|
|
37547
|
-
repoPath;
|
|
37548
|
-
kept;
|
|
37549
|
-
lands;
|
|
37550
|
-
defaultBr;
|
|
37551
|
-
constructor(repoPath, kept, lands, defaultBr) {
|
|
37552
|
-
this.repoPath = repoPath;
|
|
37553
|
-
this.kept = kept;
|
|
37554
|
-
this.lands = lands;
|
|
37555
|
-
this.defaultBr = defaultBr;
|
|
38365
|
+
async restartFromMain(wt, base) {
|
|
38366
|
+
if (!hasOwnBranch(wt))
|
|
38367
|
+
return;
|
|
38368
|
+
this.d.tree.invalidate(wt.id);
|
|
38369
|
+
const r = await git(wt.path, "reset", "--keep", base);
|
|
38370
|
+
if (!r.ok)
|
|
38371
|
+
log.warn(wt.id, `could not restart ${wt.branch} from ${base}: ${r.err}`);
|
|
38372
|
+
await this.d.tree.refresh(wt.id);
|
|
37556
38373
|
}
|
|
37557
|
-
async
|
|
37558
|
-
|
|
37559
|
-
|
|
38374
|
+
async fetchBase(wt, repo, w) {
|
|
38375
|
+
if (!baseIsRemote(repo))
|
|
38376
|
+
return { ok: true, message: `${baseOf(repo)} here is the base` };
|
|
38377
|
+
const r = await withFetchLock(repo.path, () => fetchBase(wt.path, repo, w));
|
|
38378
|
+
this.d.trunk.noteFetch(repo.id, { ok: r.ok, err: r.message });
|
|
38379
|
+
return r;
|
|
37560
38380
|
}
|
|
37561
|
-
async
|
|
37562
|
-
const
|
|
37563
|
-
|
|
37564
|
-
|
|
37565
|
-
|
|
37566
|
-
|
|
37567
|
-
|
|
37568
|
-
|
|
38381
|
+
async prMerged(worktreeId) {
|
|
38382
|
+
const wt = this.d.state.worktree(worktreeId);
|
|
38383
|
+
if (!wt)
|
|
38384
|
+
return { ok: false, message: "that worktree is gone" };
|
|
38385
|
+
const pr = wt.pr ? `PR #${wt.pr.number}` : "the PR";
|
|
38386
|
+
const out = this.ops.get(worktreeId);
|
|
38387
|
+
if (out) {
|
|
38388
|
+
const message = `${shipNoun(out.op)} is already running here`;
|
|
38389
|
+
log.info(worktreeId, `${pr} merged on GitHub; ${message}, landing waits`);
|
|
38390
|
+
return { ok: false, message };
|
|
38391
|
+
}
|
|
38392
|
+
if (prTaken(wt))
|
|
38393
|
+
return { ok: true, message: `${pr} merged; landed already` };
|
|
38394
|
+
log.info(worktreeId, `${pr} merged on GitHub; landing it`);
|
|
38395
|
+
const { result } = await this.landing(wt, "auto", () => this.prMergedOp(wt));
|
|
38396
|
+
if (result.ok)
|
|
38397
|
+
log.info(worktreeId, `landed: ${result.message}`);
|
|
38398
|
+
else
|
|
38399
|
+
log.warn(worktreeId, `landing failed: ${result.message}`);
|
|
38400
|
+
return result;
|
|
37569
38401
|
}
|
|
37570
|
-
async
|
|
37571
|
-
const
|
|
37572
|
-
const
|
|
37573
|
-
|
|
37574
|
-
|
|
37575
|
-
|
|
37576
|
-
|
|
37577
|
-
};
|
|
37578
|
-
const
|
|
37579
|
-
|
|
37580
|
-
|
|
37581
|
-
|
|
38402
|
+
async prMergedOp(wt, carried, w = UNWATCHED) {
|
|
38403
|
+
const repo = this.d.state.requireRepo(wt.repoId);
|
|
38404
|
+
const br = repo.defaultBranch;
|
|
38405
|
+
const base = baseOf(repo);
|
|
38406
|
+
const read = carried === undefined ? await landingMark(wt.path, base) : carried;
|
|
38407
|
+
const pulled = await this.d.trunk.pull(repo.id, w);
|
|
38408
|
+
const past = await this.pastPr(wt, base);
|
|
38409
|
+
const settled = await withRepoLock(repo.path, async () => this.settle(wt, repo, await this.prMark(wt, base, read, past), { pr: wt.pr, stay: !!past }));
|
|
38410
|
+
const took = pulled.ok ? `${br} here ${pulled.moved ? "pulled it" : "has it"}` : `${br} here was left where it is: ${pulled.message}`;
|
|
38411
|
+
const left = past?.count ? `; ${past.count === 1 ? "a commit" : `${past.count} commits`} made since its last push stayed on the branch` : "";
|
|
38412
|
+
const message = `${wt.pr ? `PR #${wt.pr.number} merged; ${took}` : took}${left}`;
|
|
38413
|
+
return { result: { ok: true, message }, landed: true, url: wt.pr?.url, ...settled };
|
|
38414
|
+
}
|
|
38415
|
+
async prMark(wt, base, read, past) {
|
|
38416
|
+
if (prTaken(wt))
|
|
38417
|
+
return null;
|
|
38418
|
+
const own = read ?? await handLanding(wt.path, wt.branch, base, wt.lands?.at(-1)?.tip);
|
|
38419
|
+
const mark = own ?? (wt.pr ? await this.tipMark(wt) : null);
|
|
38420
|
+
return mark && past?.tip ? { ...mark, tip: past.tip } : mark;
|
|
38421
|
+
}
|
|
38422
|
+
async pastPr(wt, base) {
|
|
38423
|
+
if (!hasOwnBranch(wt))
|
|
38424
|
+
return null;
|
|
38425
|
+
const named = [wt.pr?.head, await remoteHead(wt.path, wt.branch)];
|
|
38426
|
+
const known = [];
|
|
38427
|
+
for (const sha of named)
|
|
38428
|
+
if (sha && !known.includes(sha) && await commitOf(wt.path, sha))
|
|
38429
|
+
known.push(sha);
|
|
38430
|
+
const read = await git(wt.path, "rev-list", "HEAD", "--not", base, ...known);
|
|
38431
|
+
if (!read.ok) {
|
|
38432
|
+
log.warn(wt.id, `could not read what ${wt.branch} holds past its PR: ${read.err}`);
|
|
38433
|
+
return { count: 0 };
|
|
38434
|
+
}
|
|
38435
|
+
let past = read.out.split(`
|
|
38436
|
+
`).filter(Boolean);
|
|
38437
|
+
for (const sha of known) {
|
|
38438
|
+
if (past.length === 0)
|
|
38439
|
+
break;
|
|
38440
|
+
const fresh = await git(wt.path, "rev-list", "--cherry-pick", "--right-only", `${sha}...HEAD`);
|
|
38441
|
+
if (!fresh.ok)
|
|
38442
|
+
continue;
|
|
38443
|
+
const kept = new Set(fresh.out.split(`
|
|
38444
|
+
`));
|
|
38445
|
+
past = past.filter((c) => kept.has(c));
|
|
37582
38446
|
}
|
|
37583
|
-
const
|
|
37584
|
-
|
|
37585
|
-
|
|
38447
|
+
const oldest = past.at(-1);
|
|
38448
|
+
if (!oldest)
|
|
38449
|
+
return null;
|
|
38450
|
+
const tip = await git(wt.path, "rev-parse", "--verify", "--quiet", `${oldest}^`);
|
|
38451
|
+
return { count: past.length, ...tip.ok && tip.out ? { tip: tip.out } : {} };
|
|
37586
38452
|
}
|
|
37587
|
-
|
|
37588
|
-
|
|
38453
|
+
async sync(worktreeId, by = "press") {
|
|
38454
|
+
const r = this.d.readable(worktreeId);
|
|
38455
|
+
if (!r)
|
|
38456
|
+
throw new UserError("that worktree is gone");
|
|
38457
|
+
if (r.wt && isMain(r.wt))
|
|
38458
|
+
throw new UserError("sync from a worktree, not main");
|
|
38459
|
+
if (r.wt && isProvisional(r.wt))
|
|
38460
|
+
throw new UserError("the new worktree already sits on main; it follows it on its own");
|
|
38461
|
+
if (!r.branch)
|
|
38462
|
+
throw new UserError(`${r.name} is detached: check out a branch in it first`);
|
|
38463
|
+
if (r.locked)
|
|
38464
|
+
throw new UserError(`${r.name} is held by another tool`);
|
|
38465
|
+
const repo = this.d.state.requireRepo(r.repoId);
|
|
38466
|
+
const own = r.wt ? hasOwnBranch(r.wt) : false;
|
|
38467
|
+
const carry = !!r.wt;
|
|
38468
|
+
const busy = isBusy(this.d.runtime.agentFor(worktreeId)?.status);
|
|
38469
|
+
if (carry && busy && (await statusFiles(r.path)).length > 0) {
|
|
38470
|
+
this.syncQueued.add(worktreeId);
|
|
38471
|
+
if (by === "press")
|
|
38472
|
+
this.d.stampPrompted(worktreeId);
|
|
38473
|
+
this.d.hub.emit("worktreesChanged");
|
|
38474
|
+
return { result: { ok: true, message: `${r.name} syncs from ${r.base} once the turn ends` }, base: r.base };
|
|
38475
|
+
}
|
|
38476
|
+
const w = r.wt ? this.landWatch(r.wt) : UNWATCHED;
|
|
38477
|
+
const result = await this.ship(worktreeId, "sync-main", by, async () => {
|
|
38478
|
+
if (r.wt) {
|
|
38479
|
+
const fetched = await this.fetchBase(r.wt, repo, w);
|
|
38480
|
+
if (!fetched.ok)
|
|
38481
|
+
return fetched;
|
|
38482
|
+
}
|
|
38483
|
+
this.d.tree.invalidate(worktreeId);
|
|
38484
|
+
const taken = await withRepoLock(repo.path, () => takeMainIn(r.path, r.base, own, w, carry));
|
|
38485
|
+
if (taken.ok) {
|
|
38486
|
+
await this.d.headMoved(worktreeId);
|
|
38487
|
+
if (r.wt)
|
|
38488
|
+
await this.verdictSurvives(r.wt, { dropMessage: false });
|
|
38489
|
+
}
|
|
38490
|
+
return taken;
|
|
38491
|
+
});
|
|
38492
|
+
return { result: r.wt ? this.fixShip(r.wt, result, r.base) : result, base: r.base };
|
|
37589
38493
|
}
|
|
37590
|
-
|
|
37591
|
-
if (
|
|
37592
|
-
return
|
|
37593
|
-
|
|
37594
|
-
const [before, after] = await Promise.all([
|
|
37595
|
-
gitRaw(this.repoPath, "show", `${base}:${path}`),
|
|
37596
|
-
gitRaw(this.repoPath, "show", `${this.kept.snapshot ?? this.kept.head}:${path}`)
|
|
37597
|
-
]);
|
|
37598
|
-
return { before: before.ok ? before.out : "", after: after.ok ? after.out : "" };
|
|
38494
|
+
unqueueSync(worktreeId) {
|
|
38495
|
+
if (!this.syncQueued.delete(worktreeId))
|
|
38496
|
+
return;
|
|
38497
|
+
this.d.hub.emit("worktreesChanged");
|
|
37599
38498
|
}
|
|
37600
|
-
|
|
37601
|
-
|
|
37602
|
-
|
|
37603
|
-
|
|
37604
|
-
|
|
37605
|
-
|
|
37606
|
-
|
|
37607
|
-
|
|
37608
|
-
|
|
37609
|
-
|
|
37610
|
-
|
|
37611
|
-
|
|
37612
|
-
|
|
37613
|
-
|
|
38499
|
+
syncAfterTurn(worktreeId) {
|
|
38500
|
+
if (!this.syncQueued.has(worktreeId))
|
|
38501
|
+
return;
|
|
38502
|
+
if (this.ops.has(worktreeId))
|
|
38503
|
+
return;
|
|
38504
|
+
this.syncQueued.delete(worktreeId);
|
|
38505
|
+
const release = this.d.runtime.agentFor(worktreeId)?.hold();
|
|
38506
|
+
this.d.hub.emit("worktreesChanged");
|
|
38507
|
+
fireAndForget(worktreeId, (async () => {
|
|
38508
|
+
try {
|
|
38509
|
+
const { result } = await this.sync(worktreeId, "auto");
|
|
38510
|
+
if (!result.ok && !result.asked)
|
|
38511
|
+
this.d.hub.emit("failed", worktreeId, result.message);
|
|
38512
|
+
} catch (e) {
|
|
38513
|
+
this.d.hub.emit("failed", worktreeId, e instanceof Error ? e.message : String(e));
|
|
38514
|
+
} finally {
|
|
38515
|
+
release?.();
|
|
38516
|
+
}
|
|
38517
|
+
})(), "sync after the turn");
|
|
38518
|
+
}
|
|
38519
|
+
async pull(worktreeId) {
|
|
38520
|
+
const wt = this.d.state.requireWorktree(worktreeId);
|
|
38521
|
+
if (!isMain(wt))
|
|
38522
|
+
throw new UserError("pull on main; a worktree syncs from main instead");
|
|
38523
|
+
const repo = this.d.state.requireRepo(wt.repoId);
|
|
38524
|
+
const w = { step: (name) => this.step(worktreeId, name), git: UNWATCHED.git };
|
|
38525
|
+
return this.ship(worktreeId, "pull-main", "auto", async () => {
|
|
38526
|
+
const result = await this.d.trunk.pull(repo.id, w);
|
|
38527
|
+
if (result.ok && result.moved)
|
|
38528
|
+
await this.d.recountAll();
|
|
38529
|
+
return result;
|
|
38530
|
+
});
|
|
38531
|
+
}
|
|
38532
|
+
async commit(worktreeId, message) {
|
|
38533
|
+
const wt = this.d.state.requireWorktree(worktreeId);
|
|
38534
|
+
const m = message.trim();
|
|
38535
|
+
if (!m)
|
|
38536
|
+
throw new UserError("commit message required");
|
|
38537
|
+
const base = baseOf(this.d.state.requireRepo(wt.repoId));
|
|
38538
|
+
return this.fixShip(wt, await this.ship(wt.id, "commit", "press", () => this.commitRecorded(wt, m)), base);
|
|
38539
|
+
}
|
|
38540
|
+
async afterRead(r, wt, core) {
|
|
38541
|
+
const isMain = wt.kind === "main";
|
|
38542
|
+
if (core.branch)
|
|
38543
|
+
await this.followBranch(wt, core.branch);
|
|
38544
|
+
const { files, head } = core;
|
|
38545
|
+
const ahead = core.ahead ?? 0;
|
|
38546
|
+
if (!isMain && hasOwnBranch(wt) && files.length === 0 && ahead === 0 && head && this.handChecked.get(wt.id) !== head) {
|
|
38547
|
+
this.handChecked.set(wt.id, head);
|
|
38548
|
+
const mark = await handLanding(r.path, wt.branch, r.base, wt.lands?.at(-1)?.tip);
|
|
38549
|
+
if (mark) {
|
|
38550
|
+
const merged = prPending(wt) && (wt.pr?.state === "merged" || !!wt.pr?.head && await reaches(r.path, wt.pr.head));
|
|
38551
|
+
const out = this.ops.get(wt.id);
|
|
38552
|
+
if (out) {
|
|
38553
|
+
this.handChecked.delete(wt.id);
|
|
38554
|
+
this.handWaits.add(wt.id);
|
|
38555
|
+
log.info(wt.id, `landed on ${r.base} outside toyon; ${shipNoun(out.op)} is already running here, landing waits`);
|
|
38556
|
+
return false;
|
|
38557
|
+
}
|
|
38558
|
+
const pr = prPending(wt) ? wt.pr : undefined;
|
|
38559
|
+
if (!pr) {
|
|
38560
|
+
await this.landedByHand(wt, mark);
|
|
38561
|
+
return true;
|
|
38562
|
+
}
|
|
38563
|
+
if (merged) {
|
|
38564
|
+
const { queue: _had, queued: _was, ...rest } = pr;
|
|
38565
|
+
if (pr.state !== "merged")
|
|
38566
|
+
this.setPr(wt.id, { ...rest, state: "merged" });
|
|
38567
|
+
fireAndForget(wt.id, this.prMerged(wt.id), "pr landing");
|
|
38568
|
+
} else {
|
|
38569
|
+
log.info(wt.id, `the work is on ${r.base}; PR #${pr.number} lands it once GitHub says it merged`);
|
|
38570
|
+
this.d.hub.emit("prDue", wt.id);
|
|
38571
|
+
}
|
|
38572
|
+
if (this.ops.has(wt.id))
|
|
38573
|
+
return false;
|
|
38574
|
+
}
|
|
37614
38575
|
}
|
|
37615
|
-
|
|
37616
|
-
|
|
38576
|
+
if (!isMain && head) {
|
|
38577
|
+
const facts = { head, ahead };
|
|
38578
|
+
if (prTaken(wt) && movedPastLand(wt, facts))
|
|
38579
|
+
this.setPr(wt.id, undefined);
|
|
38580
|
+
await this.refreshLanded(wt, facts);
|
|
38581
|
+
}
|
|
38582
|
+
const landing = wt.landing;
|
|
38583
|
+
if (landing && landing.check !== "pending") {
|
|
38584
|
+
const moved = landing.fingerprint !== await this.fingerprint(r.path);
|
|
38585
|
+
if (wt.landing === landing && moved !== !!landing.stale) {
|
|
38586
|
+
const { stale: _was, ...rest } = landing;
|
|
38587
|
+
this.setLanding(wt.id, moved ? { ...rest, stale: true } : rest);
|
|
38588
|
+
}
|
|
38589
|
+
}
|
|
38590
|
+
return false;
|
|
38591
|
+
}
|
|
37617
38592
|
}
|
|
37618
38593
|
|
|
37619
38594
|
// ../daemon/src/worktrees/spare.ts
|
|
37620
38595
|
import { existsSync as existsSync37 } from "fs";
|
|
37621
|
-
import { basename as basename14, join as
|
|
38596
|
+
import { basename as basename14, join as join34 } from "path";
|
|
37622
38597
|
class SparePool {
|
|
37623
38598
|
d;
|
|
37624
38599
|
inflight = new Map;
|
|
@@ -37686,11 +38661,11 @@ class SparePool {
|
|
|
37686
38661
|
return this.record(repoId, repo).then((wt) => this.warmUp(wt, repo, {}));
|
|
37687
38662
|
}
|
|
37688
38663
|
async record(repoId, repo) {
|
|
37689
|
-
const { id, dir } = freeSlot(
|
|
38664
|
+
const { id, dir } = freeSlot(join34(this.d.paths.worktreesDir, repo.name));
|
|
37690
38665
|
const wt = {
|
|
37691
38666
|
id,
|
|
37692
38667
|
repoId,
|
|
37693
|
-
path:
|
|
38668
|
+
path: join34(this.d.paths.worktreesDir, repo.name, dir),
|
|
37694
38669
|
branch: repo.defaultBranch,
|
|
37695
38670
|
kind: "spare",
|
|
37696
38671
|
phase: "reserved",
|
|
@@ -37882,7 +38857,7 @@ class SparePool {
|
|
|
37882
38857
|
// ../daemon/src/worktrees/treeState.ts
|
|
37883
38858
|
import { watch as watch2 } from "fs";
|
|
37884
38859
|
import { stat as stat7 } from "fs/promises";
|
|
37885
|
-
import { join as
|
|
38860
|
+
import { join as join35 } from "path";
|
|
37886
38861
|
var EDIT_DELAY_MS = 150;
|
|
37887
38862
|
var SHELL_QUIET_MS2 = 1500;
|
|
37888
38863
|
var WATCH_DELAY_MS = 300;
|
|
@@ -38143,11 +39118,10 @@ class TreeState {
|
|
|
38143
39118
|
});
|
|
38144
39119
|
w.unref?.();
|
|
38145
39120
|
this.watchers.set(id, { w, ignore });
|
|
38146
|
-
fireAndForget(id,
|
|
39121
|
+
fireAndForget(id, gitRaw(r.path, "ls-files", "-z", "-o", "-i", "--exclude-standard", "--directory").then((res) => {
|
|
38147
39122
|
if (!res.ok)
|
|
38148
39123
|
return;
|
|
38149
|
-
for (const line of res.out
|
|
38150
|
-
`)) {
|
|
39124
|
+
for (const line of nulSplit(res.out)) {
|
|
38151
39125
|
const dir = line.replace(/\/$/, "");
|
|
38152
39126
|
if (dir)
|
|
38153
39127
|
ignore.add(dir);
|
|
@@ -38265,7 +39239,7 @@ class TreeState {
|
|
|
38265
39239
|
xy: f.xy,
|
|
38266
39240
|
...entries.folded.has(f.path) ? { add: f.add, del: f.del } : {}
|
|
38267
39241
|
}));
|
|
38268
|
-
const stats = await Promise.all(files.slice(0, SIGNATURE_STATS).map((f) => stat7(
|
|
39242
|
+
const stats = await Promise.all(files.slice(0, SIGNATURE_STATS).map((f) => stat7(join35(r.path, f.path)).then((s) => `${s.mtimeMs}:${s.size}`, () => "-")));
|
|
38269
39243
|
const t0 = performance.now();
|
|
38270
39244
|
const counts = { ...ab, ...unpushed === null ? {} : { unpushed } };
|
|
38271
39245
|
const signature = String(Bun.hash([
|
|
@@ -38588,7 +39562,6 @@ function stopOf(prev, next) {
|
|
|
38588
39562
|
var LIST_FLOOR_MS = 1e4;
|
|
38589
39563
|
var LOCAL_CONFIG_FILES = [".env", ".env.local", ".env.development", ".env.development.local", ".dev.vars"];
|
|
38590
39564
|
var BASE_IGNORE = "base .gitignore";
|
|
38591
|
-
var REFUSAL_CHARS = 400;
|
|
38592
39565
|
async function deleteTree(path) {
|
|
38593
39566
|
try {
|
|
38594
39567
|
await rm6(path, { recursive: true, force: true });
|
|
@@ -38616,16 +39589,13 @@ var idsOf = (rows, trunks) => [
|
|
|
38616
39589
|
...Object.values(trunks).map((t) => t.id)
|
|
38617
39590
|
];
|
|
38618
39591
|
var heldBy2 = (found) => `${found.name} is held by another tool${found.lockReason ? `: ${found.lockReason}` : ""}`;
|
|
38619
|
-
var commandLine = (args) => args.map((a) => /^[\w./:=@,+-]+$/.test(a) ? a : JSON.stringify(a.split(`
|
|
38620
|
-
`, 1)[0])).join(" ");
|
|
38621
39592
|
|
|
38622
39593
|
class WorktreeService {
|
|
38623
39594
|
d;
|
|
38624
39595
|
spare;
|
|
38625
39596
|
runs;
|
|
38626
|
-
ops = new Map;
|
|
38627
|
-
syncQueued = new Set;
|
|
38628
39597
|
trunk;
|
|
39598
|
+
ship;
|
|
38629
39599
|
archive;
|
|
38630
39600
|
archiving = new Map;
|
|
38631
39601
|
restoring = new Map;
|
|
@@ -38633,7 +39603,6 @@ class WorktreeService {
|
|
|
38633
39603
|
arriving = new Set;
|
|
38634
39604
|
firstPrompt = new Map;
|
|
38635
39605
|
tree;
|
|
38636
|
-
handChecked = new Map;
|
|
38637
39606
|
naming = new Set;
|
|
38638
39607
|
discoverCache = new Map;
|
|
38639
39608
|
relisting = new Map;
|
|
@@ -38663,6 +39632,22 @@ class WorktreeService {
|
|
|
38663
39632
|
afterCore: (r, core) => this.afterCore(r, core),
|
|
38664
39633
|
rowsOf: (repoId) => this.rowsOf(repoId)
|
|
38665
39634
|
});
|
|
39635
|
+
this.ship = new Ship({
|
|
39636
|
+
state: d.state,
|
|
39637
|
+
hub: d.hub,
|
|
39638
|
+
runtime: d.runtime,
|
|
39639
|
+
fix: d.fix,
|
|
39640
|
+
runs: this.runs,
|
|
39641
|
+
tree: this.tree,
|
|
39642
|
+
trunk: this.trunk,
|
|
39643
|
+
readable: (id) => this.readable(id),
|
|
39644
|
+
stampPrompted: (id) => this.stampPrompted(id),
|
|
39645
|
+
headMoved: (id) => this.headMoved(id),
|
|
39646
|
+
recountAll: () => this.recountAll(),
|
|
39647
|
+
watch: d.watch,
|
|
39648
|
+
fingerprint: d.fingerprint,
|
|
39649
|
+
message: d.message
|
|
39650
|
+
});
|
|
38666
39651
|
this.spare = new SparePool({
|
|
38667
39652
|
state: d.state,
|
|
38668
39653
|
hub: d.hub,
|
|
@@ -38711,7 +39696,7 @@ class WorktreeService {
|
|
|
38711
39696
|
this.scheduleNaming(wt, prompt);
|
|
38712
39697
|
}
|
|
38713
39698
|
if (turn.end !== "asking")
|
|
38714
|
-
this.syncAfterTurn(worktreeId);
|
|
39699
|
+
this.ship.syncAfterTurn(worktreeId);
|
|
38715
39700
|
if (turn.end !== "done" && turn.end !== "stopped")
|
|
38716
39701
|
return;
|
|
38717
39702
|
for (const p of d.runtime.get(worktreeId)?.procs?.states() ?? []) {
|
|
@@ -38790,8 +39775,8 @@ class WorktreeService {
|
|
|
38790
39775
|
if (!await isGitRepo(repo.path)) {
|
|
38791
39776
|
throw new UserError(`git no longer reads ${repo.name} as a checkout, so there is nothing to start a worktree from. If \`git config core.bare\` says true there, \`git config --unset core.bare\` puts it back.`);
|
|
38792
39777
|
}
|
|
38793
|
-
const { id, dir } = freeSlot(
|
|
38794
|
-
const wtPath =
|
|
39778
|
+
const { id, dir } = freeSlot(join36(this.d.paths.worktreesDir, repo.name));
|
|
39779
|
+
const wtPath = join36(this.d.paths.worktreesDir, repo.name, dir);
|
|
38795
39780
|
const branch = `toyon/${dir}`;
|
|
38796
39781
|
await withRepoLock(repo.path, () => gitOrThrow(repo.path, "worktree", "add", "--no-track", "-b", branch, wtPath, baseOf(repo)));
|
|
38797
39782
|
const wt = {
|
|
@@ -38883,9 +39868,8 @@ class WorktreeService {
|
|
|
38883
39868
|
`).indexOf(sha);
|
|
38884
39869
|
return i < 0 ? null : `stash@{${i}}`;
|
|
38885
39870
|
};
|
|
38886
|
-
const carried = await
|
|
38887
|
-
const restored = (carried.ok ? carried.out.
|
|
38888
|
-
`).filter(Boolean) : []).filter((p) => !existsSync38(join35(wt.path, p)));
|
|
39871
|
+
const carried = await gitRaw(repo.path, "ls-tree", "-r", "-z", "--name-only", `${sha}^3`);
|
|
39872
|
+
const restored = (carried.ok ? nulSplit(carried.out) : []).filter((p) => !existsSync38(join36(wt.path, p)));
|
|
38889
39873
|
const applied = await git(wt.path, "stash", "apply", sha);
|
|
38890
39874
|
if (applied.ok) {
|
|
38891
39875
|
const ref = await entry();
|
|
@@ -38922,9 +39906,9 @@ class WorktreeService {
|
|
|
38922
39906
|
const branch = kind === "pr" ? `pr/${number}` : ref;
|
|
38923
39907
|
const title = kind === "pr" ? `pr-${number}` : cleanTitle2(ref) || "branch";
|
|
38924
39908
|
let slug = branchSlug(title) || "branch";
|
|
38925
|
-
if (existsSync38(
|
|
39909
|
+
if (existsSync38(join36(this.d.paths.worktreesDir, repo.name, slug)))
|
|
38926
39910
|
slug = `${slug}-${shortId().slice(0, 4)}`;
|
|
38927
|
-
const wtPath =
|
|
39911
|
+
const wtPath = join36(this.d.paths.worktreesDir, repo.name, slug);
|
|
38928
39912
|
await withRepoLock(repo.path, async () => {
|
|
38929
39913
|
const out = (await listWorktrees(repo.path)).find((w) => w.branch === branch);
|
|
38930
39914
|
if (out)
|
|
@@ -39293,8 +40277,11 @@ class WorktreeService {
|
|
|
39293
40277
|
this.d.drafts?.drop(worktreeId);
|
|
39294
40278
|
}
|
|
39295
40279
|
async takeDown(wt, archive, reason) {
|
|
40280
|
+
const out = this.ship.shippingOf(wt.id);
|
|
40281
|
+
if (out)
|
|
40282
|
+
throw new UserError(`${shipNoun(out.op)} is still running in ${wt.title}; try again once it ends`);
|
|
39296
40283
|
this.going.add(wt.id);
|
|
39297
|
-
this.
|
|
40284
|
+
this.ship.forget(wt.id);
|
|
39298
40285
|
try {
|
|
39299
40286
|
return await this.removeAndRecord(wt, archive, reason);
|
|
39300
40287
|
} finally {
|
|
@@ -39308,7 +40295,7 @@ class WorktreeService {
|
|
|
39308
40295
|
await this.teardown(wt, repo);
|
|
39309
40296
|
const kept = await withRepoLock(repo.path, async () => {
|
|
39310
40297
|
const k = archive ? await this.keep(repo, wt) : null;
|
|
39311
|
-
if (archive && !k)
|
|
40298
|
+
if (archive && (!k || k.lost))
|
|
39312
40299
|
throw new UserError(`could not keep ${wt.title}'s work, so it was left in place`);
|
|
39313
40300
|
if (archive)
|
|
39314
40301
|
this.keepPlans(wt);
|
|
@@ -39367,12 +40354,12 @@ class WorktreeService {
|
|
|
39367
40354
|
}
|
|
39368
40355
|
}
|
|
39369
40356
|
async keep(repo, wt) {
|
|
39370
|
-
const index =
|
|
40357
|
+
const index = join36(this.d.paths.archiveDir, `${wt.id}.index`);
|
|
39371
40358
|
const kept = await keepState(repo.path, wt.path, archiveRef(wt.id), index) ?? await this.keepUnlinked(repo, wt);
|
|
39372
40359
|
if (!kept)
|
|
39373
40360
|
log.warn(wt.id, "could not keep its git state");
|
|
39374
40361
|
else if (kept.lost)
|
|
39375
|
-
log.warn(wt.id, "could not keep its uncommitted changes
|
|
40362
|
+
log.warn(wt.id, "could not keep its uncommitted changes");
|
|
39376
40363
|
return kept;
|
|
39377
40364
|
}
|
|
39378
40365
|
async keepUnlinked(repo, wt) {
|
|
@@ -39404,10 +40391,10 @@ class WorktreeService {
|
|
|
39404
40391
|
log.warn(wt.id, `its directory could not be deleted and stays at ${wt.path}`);
|
|
39405
40392
|
}
|
|
39406
40393
|
plansScratch(worktreeId) {
|
|
39407
|
-
return
|
|
40394
|
+
return join36(this.d.paths.archiveDir, `${worktreeId}.plans`);
|
|
39408
40395
|
}
|
|
39409
40396
|
keepPlans(wt) {
|
|
39410
|
-
const from =
|
|
40397
|
+
const from = join36(wt.path, PLANS_DIR);
|
|
39411
40398
|
if (!existsSync38(from))
|
|
39412
40399
|
return;
|
|
39413
40400
|
try {
|
|
@@ -39555,7 +40542,7 @@ class WorktreeService {
|
|
|
39555
40542
|
}
|
|
39556
40543
|
archivedAttachment(archiveId, file) {
|
|
39557
40544
|
const files = this.archive.chatFiles(archiveId);
|
|
39558
|
-
return files && isAttachmentFile(file) ?
|
|
40545
|
+
return files && isAttachmentFile(file) ? join36(files.attachments, file) : null;
|
|
39559
40546
|
}
|
|
39560
40547
|
archivedGit(archiveId) {
|
|
39561
40548
|
const rec = this.archive.get(archiveId);
|
|
@@ -39616,7 +40603,7 @@ class WorktreeService {
|
|
|
39616
40603
|
throw new UserError(`${old.title} was archived without its commits, so there is nothing to restore`);
|
|
39617
40604
|
if (rec.auto)
|
|
39618
40605
|
log.info(old.id, `restoring a worktree that archived itself (${rec.auto})`);
|
|
39619
|
-
const path = existsSync38(old.path) ?
|
|
40606
|
+
const path = existsSync38(old.path) ? join36(dirname15(old.path), freeSlot(dirname15(old.path)).dir) : old.path;
|
|
39620
40607
|
const wt = {
|
|
39621
40608
|
...old,
|
|
39622
40609
|
repoId: repo.id,
|
|
@@ -39656,7 +40643,7 @@ class WorktreeService {
|
|
|
39656
40643
|
run.finish(0);
|
|
39657
40644
|
this.arriving.delete(wt.id);
|
|
39658
40645
|
wt.branch = branch;
|
|
39659
|
-
move(chat.plans,
|
|
40646
|
+
move(chat.plans, join36(path, PLANS_DIR));
|
|
39660
40647
|
const dropped = await git(repo.path, "update-ref", "-d", archiveRef(old.id));
|
|
39661
40648
|
if (!dropped.ok)
|
|
39662
40649
|
log.warn(old.id, `could not drop its archive ref: ${dropped.err}`);
|
|
@@ -39795,6 +40782,9 @@ class WorktreeService {
|
|
|
39795
40782
|
throw new UserError("worktrees must belong to one repo");
|
|
39796
40783
|
if (isBusy(this.d.runtime.agentFor(w.id)?.status))
|
|
39797
40784
|
throw new UserError(`${w.title}'s agent is mid-turn; stop it first`);
|
|
40785
|
+
const out = this.ship.shippingOf(w.id);
|
|
40786
|
+
if (out)
|
|
40787
|
+
throw new UserError(`${shipNoun(out.op)} is still running in ${w.title}; try again once it ends`);
|
|
39798
40788
|
}
|
|
39799
40789
|
for (const w of all) {
|
|
39800
40790
|
if ((await statusFiles(w.path)).length > 0)
|
|
@@ -39817,7 +40807,7 @@ class WorktreeService {
|
|
|
39817
40807
|
}
|
|
39818
40808
|
for (const w of sources)
|
|
39819
40809
|
await this.discardWorktree(w.id);
|
|
39820
|
-
await this.refreshLanded(target);
|
|
40810
|
+
await this.ship.refreshLanded(target);
|
|
39821
40811
|
await this.headMoved(target.id);
|
|
39822
40812
|
return { target, grafted: sources.map((w) => w.title) };
|
|
39823
40813
|
}
|
|
@@ -39857,7 +40847,7 @@ class WorktreeService {
|
|
|
39857
40847
|
}
|
|
39858
40848
|
}
|
|
39859
40849
|
async mirrorBaseIgnore(repo) {
|
|
39860
|
-
const src =
|
|
40850
|
+
const src = join36(repo.path, ".gitignore");
|
|
39861
40851
|
const tracked = existsSync38(src) && (await git(repo.path, "ls-files", "--", ".gitignore")).out !== "";
|
|
39862
40852
|
const mirror = existsSync38(src) && !tracked;
|
|
39863
40853
|
const lines = mirror ? readFileSync22(src, "utf8").split(`
|
|
@@ -39875,8 +40865,8 @@ class WorktreeService {
|
|
|
39875
40865
|
await this.d.cache.restore(wt, repo, (line) => this.d.hub.emit("log", wt.id, "setup", line));
|
|
39876
40866
|
run.finish(0);
|
|
39877
40867
|
}
|
|
39878
|
-
const srcNm =
|
|
39879
|
-
const dstNm =
|
|
40868
|
+
const srcNm = join36(depsSource, "node_modules");
|
|
40869
|
+
const dstNm = join36(wt.path, "node_modules");
|
|
39880
40870
|
if (existsSync38(srcNm) && !existsSync38(dstNm)) {
|
|
39881
40871
|
const run = this.runs.begin(wt.id, "setup", { timeoutMs: ceiling, stage: "copying dependencies" });
|
|
39882
40872
|
const r = await cloneTree(srcNm, dstNm);
|
|
@@ -39887,10 +40877,10 @@ class WorktreeService {
|
|
|
39887
40877
|
log.warn(wt.id, "could not copy node_modules from the base checkout", r.error);
|
|
39888
40878
|
}
|
|
39889
40879
|
for (const f of LOCAL_CONFIG_FILES) {
|
|
39890
|
-
const src =
|
|
39891
|
-
if (!existsSync38(src) || existsSync38(
|
|
40880
|
+
const src = join36(depsSource, f);
|
|
40881
|
+
if (!existsSync38(src) || existsSync38(join36(wt.path, f)))
|
|
39892
40882
|
continue;
|
|
39893
|
-
const r = await run("cp", [src,
|
|
40883
|
+
const r = await run("cp", [src, join36(wt.path, f)], wt.path);
|
|
39894
40884
|
if (r.ok)
|
|
39895
40885
|
this.d.hub.emit("log", wt.id, "setup", `copied ${f}`);
|
|
39896
40886
|
else
|
|
@@ -39913,188 +40903,31 @@ class WorktreeService {
|
|
|
39913
40903
|
await this.d.runtime.start(wt, repo);
|
|
39914
40904
|
}
|
|
39915
40905
|
setPr(worktreeId, pr) {
|
|
39916
|
-
|
|
39917
|
-
if (!wt || wt.pr === undefined && pr === undefined)
|
|
39918
|
-
return;
|
|
39919
|
-
if (pr)
|
|
39920
|
-
wt.pr = pr;
|
|
39921
|
-
else
|
|
39922
|
-
delete wt.pr;
|
|
39923
|
-
this.d.state.save();
|
|
39924
|
-
this.d.hub.emit("worktreesChanged");
|
|
39925
|
-
}
|
|
39926
|
-
async refreshLanded(wt, facts) {
|
|
39927
|
-
if (wt.kind === "main")
|
|
39928
|
-
return;
|
|
39929
|
-
const f = facts ?? await this.landedFacts(wt);
|
|
39930
|
-
if (f)
|
|
39931
|
-
this.setLanded(wt, landedNow(wt, f));
|
|
39932
|
-
}
|
|
39933
|
-
async settleLanded(wt) {
|
|
39934
|
-
const repo = this.d.state.repo(wt.repoId);
|
|
39935
|
-
const f = wt.kind === "main" || !repo ? null : await this.landedFacts(wt);
|
|
39936
|
-
delete wt.landing;
|
|
39937
|
-
let held;
|
|
39938
|
-
if (f && repo) {
|
|
39939
|
-
held = notLandedWhy(wt, f, baseOf(repo));
|
|
39940
|
-
if (held) {
|
|
39941
|
-
delete wt.landed;
|
|
39942
|
-
log.warn(wt.id, `landed, but not marked landed: ${held}`);
|
|
39943
|
-
} else
|
|
39944
|
-
wt.landed = true;
|
|
39945
|
-
}
|
|
39946
|
-
this.d.state.save();
|
|
39947
|
-
this.d.hub.emit("worktreesChanged");
|
|
39948
|
-
return held;
|
|
39949
|
-
}
|
|
39950
|
-
async landedFacts(wt) {
|
|
39951
|
-
const repo = this.d.state.repo(wt.repoId);
|
|
39952
|
-
if (!repo)
|
|
39953
|
-
return null;
|
|
39954
|
-
try {
|
|
39955
|
-
const [head, ab] = await Promise.all([git(wt.path, "rev-parse", "HEAD"), aheadBehind(wt.path, baseOf(repo))]);
|
|
39956
|
-
if (!head.ok)
|
|
39957
|
-
return null;
|
|
39958
|
-
return { head: head.out, ahead: ab.ahead };
|
|
39959
|
-
} catch (e) {
|
|
39960
|
-
log.warn(wt.id, "could not read the tree for the landed mark", e);
|
|
39961
|
-
return null;
|
|
39962
|
-
}
|
|
39963
|
-
}
|
|
39964
|
-
setLanded(wt, landed) {
|
|
39965
|
-
if (!!wt.landed === landed)
|
|
39966
|
-
return;
|
|
39967
|
-
if (landed)
|
|
39968
|
-
wt.landed = true;
|
|
39969
|
-
else
|
|
39970
|
-
delete wt.landed;
|
|
39971
|
-
this.d.state.save();
|
|
39972
|
-
this.d.hub.emit("worktreesChanged");
|
|
39973
|
-
}
|
|
39974
|
-
fingerprint(path) {
|
|
39975
|
-
return (this.d.fingerprint ?? treeFingerprint)(path);
|
|
40906
|
+
this.ship.setPr(worktreeId, pr);
|
|
39976
40907
|
}
|
|
39977
40908
|
setLanding(worktreeId, landing) {
|
|
39978
|
-
|
|
39979
|
-
if (!wt || wt.landing === undefined && landing === undefined)
|
|
39980
|
-
return;
|
|
39981
|
-
if (landing)
|
|
39982
|
-
wt.landing = landing;
|
|
39983
|
-
else
|
|
39984
|
-
delete wt.landing;
|
|
39985
|
-
this.d.state.save();
|
|
39986
|
-
this.d.hub.emit("worktreesChanged");
|
|
40909
|
+
this.ship.setLanding(worktreeId, landing);
|
|
39987
40910
|
}
|
|
39988
|
-
|
|
39989
|
-
|
|
39990
|
-
if (isMain(pair.wt))
|
|
39991
|
-
throw new UserError(`${verb} from a worktree, not main`);
|
|
39992
|
-
if (!canLand(pair.wt))
|
|
39993
|
-
throw new UserError(`${pair.wt.title} is a PR under review; it lands upstream, not here`);
|
|
39994
|
-
return pair;
|
|
40911
|
+
land(worktreeId, message) {
|
|
40912
|
+
return this.ship.land(worktreeId, message);
|
|
39995
40913
|
}
|
|
39996
|
-
|
|
39997
|
-
|
|
39998
|
-
if (m)
|
|
39999
|
-
return m;
|
|
40000
|
-
const s = wt.landing?.subject;
|
|
40001
|
-
if (s)
|
|
40002
|
-
return wt.landing?.body ? `${s}
|
|
40003
|
-
|
|
40004
|
-
${wt.landing.body}` : s;
|
|
40005
|
-
throw new UserError("no commit message yet: write one in the changes panel");
|
|
40914
|
+
prMerged(worktreeId) {
|
|
40915
|
+
return this.ship.prMerged(worktreeId);
|
|
40006
40916
|
}
|
|
40007
|
-
|
|
40008
|
-
|
|
40009
|
-
return null;
|
|
40010
|
-
return this.commitRecorded(wt, this.commitMessage(wt, typed));
|
|
40011
|
-
}
|
|
40012
|
-
async commitRecorded(wt, message) {
|
|
40013
|
-
const result = await commitWorktree(wt.path, message, this.landWatch(wt));
|
|
40014
|
-
if (result.ok) {
|
|
40015
|
-
await this.verdictSurvives(wt, { dropMessage: true });
|
|
40016
|
-
await this.headMoved(wt.id);
|
|
40017
|
-
}
|
|
40018
|
-
return result;
|
|
40917
|
+
sync(worktreeId, by = "press") {
|
|
40918
|
+
return this.ship.sync(worktreeId, by);
|
|
40019
40919
|
}
|
|
40020
|
-
|
|
40021
|
-
|
|
40022
|
-
return {
|
|
40023
|
-
step: (name) => this.step(wt.id, name),
|
|
40024
|
-
git: async (cwd, args) => {
|
|
40025
|
-
const where = cwd === wt.path ? "git" : `git -C ${cwd}`;
|
|
40026
|
-
const commit = args[0] === "commit";
|
|
40027
|
-
const ceiling = commit ? timeoutFor(repo.config, "commit") : STEP_TIMEOUT_MS;
|
|
40028
|
-
const handle = commit ? this.runs.begin(wt.id, "commit", { timeoutMs: ceiling }) : null;
|
|
40029
|
-
const run = (onText, signal) => stepRun(cwd, args, onText, signal, {
|
|
40030
|
-
timeoutMs: ceiling,
|
|
40031
|
-
...handle ? { onSpawn: handle.spawned } : {},
|
|
40032
|
-
onHook: (hook) => handle?.stage(hook ? `${hook} hook` : undefined)
|
|
40033
|
-
});
|
|
40034
|
-
const watched = async () => {
|
|
40035
|
-
if (!this.d.watch)
|
|
40036
|
-
return run(() => {});
|
|
40037
|
-
const command = `${where} ${commandLine(args)}`;
|
|
40038
|
-
try {
|
|
40039
|
-
return { ...await this.d.watch(wt.id, command, run), command };
|
|
40040
|
-
} catch (e) {
|
|
40041
|
-
log.warn(wt.id, `landing step runs unwatched: ${e instanceof Error ? e.message : e}`);
|
|
40042
|
-
return run(() => {});
|
|
40043
|
-
}
|
|
40044
|
-
};
|
|
40045
|
-
const r = await watched();
|
|
40046
|
-
handle?.finish(r.exit);
|
|
40047
|
-
return r;
|
|
40048
|
-
}
|
|
40049
|
-
};
|
|
40920
|
+
unqueueSync(worktreeId) {
|
|
40921
|
+
this.ship.unqueueSync(worktreeId);
|
|
40050
40922
|
}
|
|
40051
|
-
|
|
40052
|
-
|
|
40053
|
-
if (!l || l.check === "pending")
|
|
40054
|
-
return;
|
|
40055
|
-
const { subject, body, unanswered, ...kept } = l;
|
|
40056
|
-
const message = opts.dropMessage ? {} : { ...subject ? { subject } : {}, ...body ? { body } : {}, ...unanswered ? { unanswered } : {} };
|
|
40057
|
-
this.setLanding(wt.id, { ...kept, ...message, fingerprint: await this.fingerprint(wt.path) });
|
|
40923
|
+
pull(worktreeId) {
|
|
40924
|
+
return this.ship.pull(worktreeId);
|
|
40058
40925
|
}
|
|
40059
|
-
|
|
40060
|
-
|
|
40061
|
-
log.info(wt.id, `landed on ${baseOf(repo)} outside toyon: ${mark.base.slice(0, 7)}..${mark.tip.slice(0, 7)}`);
|
|
40062
|
-
await this.noteLand(repo, wt, mark);
|
|
40063
|
-
await this.restartFromMain(wt, baseOf(repo));
|
|
40064
|
-
await this.settleLanded(wt);
|
|
40065
|
-
this.d.hub.emit("landed", wt.id);
|
|
40926
|
+
commit(worktreeId, message) {
|
|
40927
|
+
return this.ship.commit(worktreeId, message);
|
|
40066
40928
|
}
|
|
40067
|
-
|
|
40068
|
-
|
|
40069
|
-
const out = await this.ship(wt.id, "land", async () => {
|
|
40070
|
-
await this.followBranch(wt);
|
|
40071
|
-
const moved = await this.movedMidTurn(wt);
|
|
40072
|
-
if (moved)
|
|
40073
|
-
return { result: moved };
|
|
40074
|
-
return this.landOp(wt, repo, message);
|
|
40075
|
-
});
|
|
40076
|
-
if (out.result.ok && out.landed) {
|
|
40077
|
-
this.noteLanded(wt, out.result.message, out);
|
|
40078
|
-
this.markPrompted(wt.id);
|
|
40079
|
-
}
|
|
40080
|
-
return { result: this.fixShip(wt, out.result, baseOf(repo)) };
|
|
40081
|
-
}
|
|
40082
|
-
fixShip(wt, result, base) {
|
|
40083
|
-
if (result.ok)
|
|
40084
|
-
return result;
|
|
40085
|
-
if (result.hook?.name === "commit-msg") {
|
|
40086
|
-
this.d.hub.emit("messageRefused", wt.id, result.hook.text.trim().slice(-REFUSAL_CHARS));
|
|
40087
|
-
return { ...result, message: "the commit-msg hook refused the message: another is being written" };
|
|
40088
|
-
}
|
|
40089
|
-
const { hook } = result;
|
|
40090
|
-
const failure = hook ? { kind: "hook", hook: hook.name, toolId: hook.toolId, command: hook.command, text: hook.text } : result.conflict ? {
|
|
40091
|
-
kind: "conflict",
|
|
40092
|
-
base,
|
|
40093
|
-
how: result.conflict,
|
|
40094
|
-
...result.step ? { step: result.step } : {},
|
|
40095
|
-
...result.carried ? { carried: result.carried } : {}
|
|
40096
|
-
} : null;
|
|
40097
|
-
return failure && this.d.fix.report(wt.id, failure) ? { ...result, asked: true } : result;
|
|
40929
|
+
shippingOf(id) {
|
|
40930
|
+
return this.ship.shippingOf(id);
|
|
40098
40931
|
}
|
|
40099
40932
|
async fixPreview(worktreeId, clientId) {
|
|
40100
40933
|
const wt = this.d.state.requireWorktree(worktreeId);
|
|
@@ -40113,439 +40946,6 @@ ${wt.landing.body}` : s;
|
|
|
40113
40946
|
...isProvisional(wt) ? { worktreeId } : {}
|
|
40114
40947
|
});
|
|
40115
40948
|
}
|
|
40116
|
-
async movedMidTurn(wt) {
|
|
40117
|
-
if (!isBusy(this.d.runtime.agentFor(wt.id)?.status))
|
|
40118
|
-
return null;
|
|
40119
|
-
const l = wt.landing;
|
|
40120
|
-
if (!l || l.fingerprint === await this.fingerprint(wt.path))
|
|
40121
|
-
return null;
|
|
40122
|
-
return { ok: false, message: "the agent has changed files since the check: land once its turn ends" };
|
|
40123
|
-
}
|
|
40124
|
-
noteLanded(wt, message, o) {
|
|
40125
|
-
this.d.runtime.ensureAgent(wt).agent.note({
|
|
40126
|
-
type: "landed",
|
|
40127
|
-
message,
|
|
40128
|
-
...o.mark ? { mark: o.mark } : {},
|
|
40129
|
-
...o.url ? { url: o.url } : {},
|
|
40130
|
-
...o.held ? { held: o.held } : {},
|
|
40131
|
-
ts: Date.now()
|
|
40132
|
-
});
|
|
40133
|
-
this.d.hub.emit("landed", wt.id);
|
|
40134
|
-
}
|
|
40135
|
-
async followBranch(wt, current) {
|
|
40136
|
-
if (wt.kind === "main")
|
|
40137
|
-
return;
|
|
40138
|
-
const branch = current ?? (await git(wt.path, "branch", "--show-current")).out;
|
|
40139
|
-
if (!branch || branch === wt.branch)
|
|
40140
|
-
return;
|
|
40141
|
-
log.info(wt.id, `on ${branch} now, not ${wt.branch}: the row follows the checkout`);
|
|
40142
|
-
wt.branch = branch;
|
|
40143
|
-
this.d.state.save();
|
|
40144
|
-
this.d.hub.emit("worktreesChanged");
|
|
40145
|
-
}
|
|
40146
|
-
shippingOf(id) {
|
|
40147
|
-
return this.ops.get(id);
|
|
40148
|
-
}
|
|
40149
|
-
async ship(id, op, run) {
|
|
40150
|
-
const out = this.ops.get(id);
|
|
40151
|
-
if (out)
|
|
40152
|
-
throw new UserError(`${shipNoun(out.op)} is already running here`);
|
|
40153
|
-
const release = this.d.runtime.agentFor(id)?.hold();
|
|
40154
|
-
this.ops.set(id, { op });
|
|
40155
|
-
this.d.hub.emit("worktreesChanged");
|
|
40156
|
-
const started = Date.now();
|
|
40157
|
-
const watchdog = setInterval(() => {
|
|
40158
|
-
const step = this.ops.get(id)?.step;
|
|
40159
|
-
const at = step ? ` at "${step}"` : "";
|
|
40160
|
-
log.warn(id, `${shipNoun(op)} still running after ${describeDuration(Date.now() - started)}${at}`);
|
|
40161
|
-
}, RUN_CEILING_MS);
|
|
40162
|
-
watchdog.unref?.();
|
|
40163
|
-
try {
|
|
40164
|
-
return await run();
|
|
40165
|
-
} finally {
|
|
40166
|
-
clearInterval(watchdog);
|
|
40167
|
-
this.ops.delete(id);
|
|
40168
|
-
release?.();
|
|
40169
|
-
this.d.hub.emit("worktreesChanged");
|
|
40170
|
-
if (!isBusy(this.d.runtime.agentFor(id)?.status))
|
|
40171
|
-
this.syncAfterTurn(id);
|
|
40172
|
-
}
|
|
40173
|
-
}
|
|
40174
|
-
step(id, name) {
|
|
40175
|
-
const out = this.ops.get(id);
|
|
40176
|
-
if (!out || out.step === name)
|
|
40177
|
-
return;
|
|
40178
|
-
out.step = name;
|
|
40179
|
-
this.d.hub.emit("worktreesChanged");
|
|
40180
|
-
}
|
|
40181
|
-
async landOp(wt, repo, message) {
|
|
40182
|
-
const policy = landPolicy(repo.config);
|
|
40183
|
-
const own = hasOwnBranch(wt);
|
|
40184
|
-
const w = this.landWatch(wt);
|
|
40185
|
-
const typed = message?.trim();
|
|
40186
|
-
const suggested = typed || (wt.landing?.subject ? wt.landing.body ? `${wt.landing.subject}
|
|
40187
|
-
|
|
40188
|
-
${wt.landing.body}` : wt.landing.subject : undefined);
|
|
40189
|
-
const base = baseOf(repo);
|
|
40190
|
-
if (policy.land === "pr") {
|
|
40191
|
-
if (wt.pr?.state === "merged" && !prTaken(wt)) {
|
|
40192
|
-
const { mark, held, ...result } = await this.prMergedOp(wt.id, undefined, w);
|
|
40193
|
-
return result.ok ? { result, landed: true, mark, held, url: wt.pr.url } : { result };
|
|
40194
|
-
}
|
|
40195
|
-
const fetched = await this.fetchBase(wt, repo, w);
|
|
40196
|
-
if (!fetched.ok)
|
|
40197
|
-
return { result: fetched };
|
|
40198
|
-
if (wt.pr?.state === "open") {
|
|
40199
|
-
const dirty = (await statusFiles(wt.path)).length > 0;
|
|
40200
|
-
const missing = dirty || (await aheadUpstream(wt.path, wt.branch) ?? 0) > 0;
|
|
40201
|
-
if (!missing) {
|
|
40202
|
-
const result = await mergePr(wt.path, wt.pr.number, policy.merge, w);
|
|
40203
|
-
if (result.ok && result.queued)
|
|
40204
|
-
this.setPr(wt.id, { ...wt.pr, queue: true, queued: "waiting", at: Date.now() });
|
|
40205
|
-
return { result };
|
|
40206
|
-
}
|
|
40207
|
-
const committed = await this.commitIfDirty(wt, message);
|
|
40208
|
-
if (committed && !committed.ok)
|
|
40209
|
-
return { result: committed };
|
|
40210
|
-
this.tree.invalidate(wt.id);
|
|
40211
|
-
const taken = await takeMainIn(wt.path, base, own, w);
|
|
40212
|
-
if (!taken.ok)
|
|
40213
|
-
return { result: taken };
|
|
40214
|
-
await this.headMoved(wt.id);
|
|
40215
|
-
this.tree.invalidate(wt.id);
|
|
40216
|
-
const pushed = await pushBranch(wt.path, wt.branch, w);
|
|
40217
|
-
if (!pushed.ok)
|
|
40218
|
-
return { result: pushed };
|
|
40219
|
-
await this.headMoved(wt.id);
|
|
40220
|
-
this.setLanding(wt.id, undefined);
|
|
40221
|
-
this.markPrompted(wt.id);
|
|
40222
|
-
const did = committed ? "committed and pushed" : "pushed";
|
|
40223
|
-
if (wt.pr.queued) {
|
|
40224
|
-
const { queued: _, ...left } = wt.pr;
|
|
40225
|
-
this.setPr(wt.id, { ...left, at: Date.now() });
|
|
40226
|
-
return {
|
|
40227
|
-
result: {
|
|
40228
|
-
ok: true,
|
|
40229
|
-
url: wt.pr.url,
|
|
40230
|
-
message: `${did}; PR #${wt.pr.number} has the new commits and left the merge queue: land it again once it is ready`
|
|
40231
|
-
}
|
|
40232
|
-
};
|
|
40233
|
-
}
|
|
40234
|
-
return { result: { ok: true, url: wt.pr.url, message: `${did}; PR #${wt.pr.number} has the new commits` } };
|
|
40235
|
-
}
|
|
40236
|
-
const committed = await this.commitIfDirty(wt, message);
|
|
40237
|
-
if (committed && !committed.ok)
|
|
40238
|
-
return { result: committed };
|
|
40239
|
-
const mark = await landingMark(wt.path, base);
|
|
40240
|
-
this.tree.invalidate(wt.id);
|
|
40241
|
-
const taken = await takeMainIn(wt.path, base, own, w);
|
|
40242
|
-
if (!taken.ok)
|
|
40243
|
-
return { result: taken };
|
|
40244
|
-
await this.headMoved(wt.id);
|
|
40245
|
-
if ((await aheadBehind(wt.path, base)).ahead === 0) {
|
|
40246
|
-
const { mark: noted, held, ...r } = await this.prMergedOp(wt.id, mark, w);
|
|
40247
|
-
const on = `origin's ${repo.defaultBranch} has this work already`;
|
|
40248
|
-
const result = { ...r, message: r.ok ? `${on}; ${r.message}` : `${on}, but ${r.message}` };
|
|
40249
|
-
return r.ok ? { result, landed: true, mark: noted, held, url: wt.pr?.url } : { result };
|
|
40250
|
-
}
|
|
40251
|
-
const named = !suggested ? undefined : splitMessage(policy.merge === "squash" ? await squashMessage(wt.path, base, suggested, !!typed) : suggested);
|
|
40252
|
-
const result = await openPr({
|
|
40253
|
-
worktreePath: wt.path,
|
|
40254
|
-
branch: wt.branch,
|
|
40255
|
-
defaultBr: repo.defaultBranch,
|
|
40256
|
-
base,
|
|
40257
|
-
subject: named?.subject,
|
|
40258
|
-
body: named?.body,
|
|
40259
|
-
automerge: policy.automerge,
|
|
40260
|
-
method: policy.merge
|
|
40261
|
-
}, w);
|
|
40262
|
-
if (result.ok) {
|
|
40263
|
-
this.setLanding(wt.id, undefined);
|
|
40264
|
-
if (result.pr)
|
|
40265
|
-
this.setPr(wt.id, { ...result.pr, at: Date.now() });
|
|
40266
|
-
this.markPrompted(wt.id);
|
|
40267
|
-
}
|
|
40268
|
-
return { result };
|
|
40269
|
-
}
|
|
40270
|
-
if (policy.land === "push")
|
|
40271
|
-
return this.landPush(wt, repo, policy, message, suggested, w);
|
|
40272
|
-
let mergedHere = false;
|
|
40273
|
-
let committedHere = false;
|
|
40274
|
-
let held;
|
|
40275
|
-
let noted;
|
|
40276
|
-
const result = await withRepoLock(repo.path, async () => {
|
|
40277
|
-
const committed = await this.commitIfDirty(wt, message);
|
|
40278
|
-
if (committed && !committed.ok)
|
|
40279
|
-
return committed;
|
|
40280
|
-
committedHere = committed !== null;
|
|
40281
|
-
const method = policy.merge ?? DEFAULT_MERGE_METHOD;
|
|
40282
|
-
const merged = own && method === "rebase" ? await requireLinear(wt.path, base) : null;
|
|
40283
|
-
if (merged)
|
|
40284
|
-
return merged;
|
|
40285
|
-
const taken = await takeMainIn(wt.path, base, own, w);
|
|
40286
|
-
if (!taken.ok)
|
|
40287
|
-
return taken;
|
|
40288
|
-
const mark = await landingMark(wt.path, base);
|
|
40289
|
-
const squash = method === "squash" ? await squashMessage(wt.path, base, suggested, !!message?.trim()) : "";
|
|
40290
|
-
const landed = await landLocally(wt.path, wt.branch, repo.path, repo.defaultBranch, method, squash, w);
|
|
40291
|
-
if (!landed.ok)
|
|
40292
|
-
return landed;
|
|
40293
|
-
mergedHere = true;
|
|
40294
|
-
noted = await this.noteLand(repo, wt, mark, { squash });
|
|
40295
|
-
await this.restartFromMain(wt, base);
|
|
40296
|
-
return landed;
|
|
40297
|
-
});
|
|
40298
|
-
if (mergedHere) {
|
|
40299
|
-
this.invalidateCounts();
|
|
40300
|
-
held = await this.settleLanded(wt);
|
|
40301
|
-
}
|
|
40302
|
-
if (!result.ok)
|
|
40303
|
-
return { result };
|
|
40304
|
-
const did = committedHere ? "committed and merged" : "merged";
|
|
40305
|
-
return {
|
|
40306
|
-
result: { ...result, message: `${did} into ${repo.defaultBranch}` },
|
|
40307
|
-
landed: true,
|
|
40308
|
-
mark: noted,
|
|
40309
|
-
held
|
|
40310
|
-
};
|
|
40311
|
-
}
|
|
40312
|
-
async landPush(wt, repo, policy, message, suggested, w) {
|
|
40313
|
-
const base = baseOf(repo);
|
|
40314
|
-
const own = hasOwnBranch(wt);
|
|
40315
|
-
const method = policy.merge ?? DEFAULT_MERGE_METHOD;
|
|
40316
|
-
const committed = await this.commitIfDirty(wt, message);
|
|
40317
|
-
if (committed && !committed.ok)
|
|
40318
|
-
return { result: committed };
|
|
40319
|
-
const read = await landingMark(wt.path, base);
|
|
40320
|
-
if (!read)
|
|
40321
|
-
return { result: { ok: false, message: `nothing to land: no commits ahead of ${base}` } };
|
|
40322
|
-
let sha = "";
|
|
40323
|
-
let already = false;
|
|
40324
|
-
let squash = "";
|
|
40325
|
-
const merged = own && method === "rebase" ? await requireLinear(wt.path, base) : null;
|
|
40326
|
-
if (merged)
|
|
40327
|
-
return { result: merged };
|
|
40328
|
-
for (let attempt = 0;; attempt++) {
|
|
40329
|
-
const fetched = await this.fetchBase(wt, repo, w);
|
|
40330
|
-
if (!fetched.ok)
|
|
40331
|
-
return { result: fetched };
|
|
40332
|
-
this.tree.invalidate(wt.id);
|
|
40333
|
-
const taken = await takeMainIn(wt.path, base, own, w);
|
|
40334
|
-
if (!taken.ok)
|
|
40335
|
-
return { result: taken };
|
|
40336
|
-
await this.headMoved(wt.id);
|
|
40337
|
-
if ((await aheadBehind(wt.path, base)).ahead === 0) {
|
|
40338
|
-
already = true;
|
|
40339
|
-
sha = await commitOf(wt.path, base) ?? "";
|
|
40340
|
-
break;
|
|
40341
|
-
}
|
|
40342
|
-
squash = method === "squash" ? await squashMessage(wt.path, base, suggested, !!message?.trim()) : "";
|
|
40343
|
-
this.tree.invalidate(wt.id);
|
|
40344
|
-
const built = await landingCommit(wt.path, wt.branch, base, method, squash, own, w);
|
|
40345
|
-
await this.headMoved(wt.id);
|
|
40346
|
-
if (!built.ok || !built.sha)
|
|
40347
|
-
return { result: built };
|
|
40348
|
-
sha = built.sha;
|
|
40349
|
-
const pushed = await pushLanding(wt.path, sha, repo.defaultBranch, w);
|
|
40350
|
-
if (pushed.ok)
|
|
40351
|
-
break;
|
|
40352
|
-
if (!pushed.moved)
|
|
40353
|
-
return { result: pushed };
|
|
40354
|
-
if (attempt > 0) {
|
|
40355
|
-
return { result: { ok: false, message: `${base} moved twice while this landed; land again` } };
|
|
40356
|
-
}
|
|
40357
|
-
}
|
|
40358
|
-
if (await commitOf(wt.path, base) !== sha)
|
|
40359
|
-
await this.fetchBase(wt, repo, w);
|
|
40360
|
-
let noted;
|
|
40361
|
-
await withRepoLock(repo.path, async () => {
|
|
40362
|
-
noted = await this.noteLand(repo, wt, read, { squash });
|
|
40363
|
-
await this.restartFromMain(wt, base);
|
|
40364
|
-
});
|
|
40365
|
-
this.invalidateCounts();
|
|
40366
|
-
const held = await this.settleLanded(wt);
|
|
40367
|
-
const followed = await this.trunk.catchUp(repo.id);
|
|
40368
|
-
const here = followed.ok ? `${repo.defaultBranch} here ${followed.moved ? "pulled it" : "has it"}` : `${repo.defaultBranch} here was left where it is: ${followed.message}`;
|
|
40369
|
-
const how = { merge: "merged into", squash: "squashed onto", rebase: "rebased onto" }[method];
|
|
40370
|
-
const did = already ? `origin's ${repo.defaultBranch} has this work already` : `${committed ? `committed and ${how}` : how} ${repo.defaultBranch} and pushed`;
|
|
40371
|
-
return { result: { ok: true, message: `${did}; ${here}` }, landed: true, mark: noted, held };
|
|
40372
|
-
}
|
|
40373
|
-
async noteLand(repo, wt, mark, opts = {}) {
|
|
40374
|
-
if (!mark)
|
|
40375
|
-
return;
|
|
40376
|
-
const n = wt.lands?.length ?? 0;
|
|
40377
|
-
const pinned = await git(repo.path, "update-ref", landRef(wt.id, n), mark.tip);
|
|
40378
|
-
if (!pinned.ok) {
|
|
40379
|
-
log.warn(wt.id, `could not keep the commits it landed: ${pinned.err}`);
|
|
40380
|
-
return;
|
|
40381
|
-
}
|
|
40382
|
-
const noted = {
|
|
40383
|
-
...mark,
|
|
40384
|
-
at: Date.now(),
|
|
40385
|
-
...opts.pr ? { pr: opts.pr.number } : {},
|
|
40386
|
-
subjects: await this.landedWhat(repo, wt, mark, opts)
|
|
40387
|
-
};
|
|
40388
|
-
wt.lands = [...wt.lands ?? [], noted];
|
|
40389
|
-
this.d.state.save();
|
|
40390
|
-
return noted;
|
|
40391
|
-
}
|
|
40392
|
-
async landedWhat(repo, wt, mark, opts) {
|
|
40393
|
-
if (opts.pr?.title)
|
|
40394
|
-
return [opts.pr.title];
|
|
40395
|
-
const subject = opts.squash ? splitMessage(opts.squash).subject : "";
|
|
40396
|
-
if (subject)
|
|
40397
|
-
return [subject];
|
|
40398
|
-
const read = await rangeSubjects(repo.path, mark.base, mark.tip);
|
|
40399
|
-
if (!read)
|
|
40400
|
-
log.warn(wt.id, `could not read what it landed: ${mark.base.slice(0, 7)}..${mark.tip.slice(0, 7)}`);
|
|
40401
|
-
return read ?? [];
|
|
40402
|
-
}
|
|
40403
|
-
async tipMark(wt) {
|
|
40404
|
-
const head = await git(wt.path, "rev-parse", "HEAD");
|
|
40405
|
-
return head.ok ? { base: head.out, tip: head.out } : null;
|
|
40406
|
-
}
|
|
40407
|
-
async restartFromMain(wt, base) {
|
|
40408
|
-
if (!hasOwnBranch(wt))
|
|
40409
|
-
return;
|
|
40410
|
-
this.tree.invalidate(wt.id);
|
|
40411
|
-
const r = await git(wt.path, "reset", "--keep", base);
|
|
40412
|
-
if (!r.ok)
|
|
40413
|
-
log.warn(wt.id, `could not restart ${wt.branch} from ${base}: ${r.err}`);
|
|
40414
|
-
await this.tree.refresh(wt.id);
|
|
40415
|
-
}
|
|
40416
|
-
async fetchBase(wt, repo, w) {
|
|
40417
|
-
if (!baseIsRemote(repo))
|
|
40418
|
-
return { ok: true, message: `${baseOf(repo)} here is the base` };
|
|
40419
|
-
const r = await withFetchLock(repo.path, () => fetchBase(wt.path, repo, w));
|
|
40420
|
-
this.trunk.noteFetch(repo.id, { ok: r.ok, err: r.message });
|
|
40421
|
-
return r;
|
|
40422
|
-
}
|
|
40423
|
-
async prMerged(worktreeId) {
|
|
40424
|
-
const wt = this.d.state.worktree(worktreeId);
|
|
40425
|
-
const pr = wt?.pr ? `PR #${wt.pr.number}` : "the PR";
|
|
40426
|
-
const out = this.ops.get(worktreeId);
|
|
40427
|
-
if (out) {
|
|
40428
|
-
const message = `${shipNoun(out.op)} is already running here`;
|
|
40429
|
-
log.info(worktreeId, `${pr} merged on GitHub; ${message}, landing waits`);
|
|
40430
|
-
return { ok: false, message };
|
|
40431
|
-
}
|
|
40432
|
-
if (wt && prTaken(wt))
|
|
40433
|
-
return { ok: true, message: `${pr} merged; landed already` };
|
|
40434
|
-
log.info(worktreeId, `${pr} merged on GitHub; landing it`);
|
|
40435
|
-
const { mark, held, ...result } = await this.ship(worktreeId, "land", () => this.prMergedOp(worktreeId));
|
|
40436
|
-
if (result.ok)
|
|
40437
|
-
log.info(worktreeId, `landed: ${result.message}`);
|
|
40438
|
-
else
|
|
40439
|
-
log.warn(worktreeId, `landing failed: ${result.message}`);
|
|
40440
|
-
if (result.ok && wt)
|
|
40441
|
-
this.noteLanded(wt, result.message, { mark, url: wt.pr?.url, held });
|
|
40442
|
-
return result;
|
|
40443
|
-
}
|
|
40444
|
-
async prMergedOp(worktreeId, carried, w = UNWATCHED) {
|
|
40445
|
-
const { wt, repo } = this.d.state.requireWorktreeWithRepo(worktreeId);
|
|
40446
|
-
const br = repo.defaultBranch;
|
|
40447
|
-
const base = baseOf(repo);
|
|
40448
|
-
const read = carried === undefined ? await landingMark(wt.path, base) : carried;
|
|
40449
|
-
const pulled = await this.trunk.pull(repo.id, w);
|
|
40450
|
-
let noted;
|
|
40451
|
-
await withRepoLock(repo.path, async () => {
|
|
40452
|
-
if (!prTaken(wt)) {
|
|
40453
|
-
const own = read ?? await handLanding(wt.path, wt.branch, base, wt.lands?.at(-1)?.tip);
|
|
40454
|
-
const mark = own ?? (wt.pr ? await this.tipMark(wt) : null);
|
|
40455
|
-
noted = await this.noteLand(repo, wt, mark, { pr: wt.pr });
|
|
40456
|
-
}
|
|
40457
|
-
await this.restartFromMain(wt, base);
|
|
40458
|
-
});
|
|
40459
|
-
this.invalidateCounts();
|
|
40460
|
-
const held = await this.settleLanded(wt);
|
|
40461
|
-
const took = pulled.ok ? `${br} here ${pulled.moved ? "pulled it" : "has it"}` : `${br} here was left where it is: ${pulled.message}`;
|
|
40462
|
-
return { ok: true, message: wt.pr ? `PR #${wt.pr.number} merged; ${took}` : took, mark: noted, held };
|
|
40463
|
-
}
|
|
40464
|
-
async sync(worktreeId) {
|
|
40465
|
-
const r = this.readable(worktreeId);
|
|
40466
|
-
if (!r)
|
|
40467
|
-
throw new UserError("that worktree is gone");
|
|
40468
|
-
if (r.wt && isMain(r.wt))
|
|
40469
|
-
throw new UserError("sync from a worktree, not main");
|
|
40470
|
-
if (r.wt && isProvisional(r.wt))
|
|
40471
|
-
throw new UserError("the new worktree already sits on main; it follows it on its own");
|
|
40472
|
-
if (!r.branch)
|
|
40473
|
-
throw new UserError(`${r.name} is detached: check out a branch in it first`);
|
|
40474
|
-
if (r.locked)
|
|
40475
|
-
throw new UserError(`${r.name} is held by another tool`);
|
|
40476
|
-
const repo = this.d.state.requireRepo(r.repoId);
|
|
40477
|
-
const own = r.wt ? hasOwnBranch(r.wt) : false;
|
|
40478
|
-
const carry = !!r.wt;
|
|
40479
|
-
const busy = isBusy(this.d.runtime.agentFor(worktreeId)?.status);
|
|
40480
|
-
if (carry && busy && (await statusFiles(r.path)).length > 0) {
|
|
40481
|
-
this.syncQueued.add(worktreeId);
|
|
40482
|
-
this.d.hub.emit("worktreesChanged");
|
|
40483
|
-
return { result: { ok: true, message: `${r.name} syncs from ${r.base} once the turn ends` }, base: r.base };
|
|
40484
|
-
}
|
|
40485
|
-
const w = r.wt ? this.landWatch(r.wt) : UNWATCHED;
|
|
40486
|
-
const result = await this.ship(worktreeId, "sync-main", async () => {
|
|
40487
|
-
if (r.wt) {
|
|
40488
|
-
const fetched = await this.fetchBase(r.wt, repo, w);
|
|
40489
|
-
if (!fetched.ok)
|
|
40490
|
-
return fetched;
|
|
40491
|
-
}
|
|
40492
|
-
this.tree.invalidate(worktreeId);
|
|
40493
|
-
const taken = await withRepoLock(repo.path, () => takeMainIn(r.path, r.base, own, w, carry));
|
|
40494
|
-
if (taken.ok) {
|
|
40495
|
-
await this.headMoved(worktreeId);
|
|
40496
|
-
if (r.wt)
|
|
40497
|
-
await this.verdictSurvives(r.wt, { dropMessage: false });
|
|
40498
|
-
}
|
|
40499
|
-
return taken;
|
|
40500
|
-
});
|
|
40501
|
-
return { result: r.wt ? this.fixShip(r.wt, result, r.base) : result, base: r.base };
|
|
40502
|
-
}
|
|
40503
|
-
unqueueSync(worktreeId) {
|
|
40504
|
-
if (!this.syncQueued.delete(worktreeId))
|
|
40505
|
-
return;
|
|
40506
|
-
this.d.hub.emit("worktreesChanged");
|
|
40507
|
-
}
|
|
40508
|
-
syncAfterTurn(worktreeId) {
|
|
40509
|
-
if (!this.syncQueued.has(worktreeId))
|
|
40510
|
-
return;
|
|
40511
|
-
if (this.ops.has(worktreeId))
|
|
40512
|
-
return;
|
|
40513
|
-
this.syncQueued.delete(worktreeId);
|
|
40514
|
-
const release = this.d.runtime.agentFor(worktreeId)?.hold();
|
|
40515
|
-
this.d.hub.emit("worktreesChanged");
|
|
40516
|
-
fireAndForget(worktreeId, (async () => {
|
|
40517
|
-
try {
|
|
40518
|
-
const { result } = await this.sync(worktreeId);
|
|
40519
|
-
if (!result.ok && !result.asked)
|
|
40520
|
-
this.d.hub.emit("failed", worktreeId, result.message);
|
|
40521
|
-
} catch (e) {
|
|
40522
|
-
this.d.hub.emit("failed", worktreeId, e instanceof Error ? e.message : String(e));
|
|
40523
|
-
} finally {
|
|
40524
|
-
release?.();
|
|
40525
|
-
}
|
|
40526
|
-
})(), "sync after the turn");
|
|
40527
|
-
}
|
|
40528
|
-
async pull(worktreeId) {
|
|
40529
|
-
const wt = this.d.state.requireWorktree(worktreeId);
|
|
40530
|
-
if (!isMain(wt))
|
|
40531
|
-
throw new UserError("pull on main; a worktree syncs from main instead");
|
|
40532
|
-
const repo = this.d.state.requireRepo(wt.repoId);
|
|
40533
|
-
const w = { step: (name) => this.step(worktreeId, name), git: UNWATCHED.git };
|
|
40534
|
-
return this.ship(worktreeId, "pull-main", async () => {
|
|
40535
|
-
const result = await this.trunk.pull(repo.id, w);
|
|
40536
|
-
if (result.ok && result.moved)
|
|
40537
|
-
await this.recountAll();
|
|
40538
|
-
return result;
|
|
40539
|
-
});
|
|
40540
|
-
}
|
|
40541
|
-
async commit(worktreeId, message) {
|
|
40542
|
-
const wt = this.d.state.requireWorktree(worktreeId);
|
|
40543
|
-
const m = message.trim();
|
|
40544
|
-
if (!m)
|
|
40545
|
-
throw new UserError("commit message required");
|
|
40546
|
-
const base = baseOf(this.d.state.requireRepo(wt.repoId));
|
|
40547
|
-
return this.fixShip(wt, await this.ship(wt.id, "commit", () => this.commitRecorded(wt, m)), base);
|
|
40548
|
-
}
|
|
40549
40949
|
setEmpty(wt, empty) {
|
|
40550
40950
|
if (wt.empty === empty)
|
|
40551
40951
|
return;
|
|
@@ -40559,7 +40959,7 @@ ${wt.landing.body}` : wt.landing.subject : undefined);
|
|
|
40559
40959
|
trunksNow() {
|
|
40560
40960
|
const all = this.trunk.all();
|
|
40561
40961
|
for (const t of Object.values(all)) {
|
|
40562
|
-
const shipping = this.
|
|
40962
|
+
const shipping = this.ship.shippingOf(t.id);
|
|
40563
40963
|
if (shipping)
|
|
40564
40964
|
t.shipping = shipping;
|
|
40565
40965
|
}
|
|
@@ -40736,37 +41136,12 @@ ${wt.landing.body}` : wt.landing.subject : undefined);
|
|
|
40736
41136
|
const wt = r.wt;
|
|
40737
41137
|
if (!wt)
|
|
40738
41138
|
return false;
|
|
40739
|
-
if (this.
|
|
41139
|
+
if (this.ship.shippingOf(wt.id))
|
|
40740
41140
|
return false;
|
|
40741
|
-
|
|
40742
|
-
|
|
40743
|
-
|
|
40744
|
-
|
|
40745
|
-
const ahead = core.ahead ?? 0;
|
|
40746
|
-
if (!isMain && hasOwnBranch(wt) && files.length === 0 && ahead === 0 && head && this.handChecked.get(wt.id) !== head) {
|
|
40747
|
-
this.handChecked.set(wt.id, head);
|
|
40748
|
-
const mark = await handLanding(r.path, wt.branch, r.base, wt.lands?.at(-1)?.tip);
|
|
40749
|
-
if (mark) {
|
|
40750
|
-
await this.landedByHand(wt, mark);
|
|
40751
|
-
return true;
|
|
40752
|
-
}
|
|
40753
|
-
}
|
|
40754
|
-
if (!isMain && head) {
|
|
40755
|
-
const facts = { head, ahead };
|
|
40756
|
-
if (prTaken(wt) && movedPastLand(wt, facts))
|
|
40757
|
-
this.setPr(wt.id, undefined);
|
|
40758
|
-
await this.refreshLanded(wt, facts);
|
|
40759
|
-
}
|
|
40760
|
-
const landing = wt.landing;
|
|
40761
|
-
if (landing && landing.check !== "pending") {
|
|
40762
|
-
const moved = landing.fingerprint !== await this.fingerprint(r.path);
|
|
40763
|
-
if (wt.landing === landing && moved !== !!landing.stale) {
|
|
40764
|
-
const { stale: _was, ...rest } = landing;
|
|
40765
|
-
this.setLanding(wt.id, moved ? { ...rest, stale: true } : rest);
|
|
40766
|
-
}
|
|
40767
|
-
}
|
|
40768
|
-
if (isMain)
|
|
40769
|
-
this.setEmpty(wt, files.length === 0 ? await treeEmpty(r.path) : false);
|
|
41141
|
+
if (await this.ship.afterRead(r, wt, core))
|
|
41142
|
+
return true;
|
|
41143
|
+
if (wt.kind === "main")
|
|
41144
|
+
this.setEmpty(wt, core.files.length === 0 ? await treeEmpty(r.path) : false);
|
|
40770
41145
|
return false;
|
|
40771
41146
|
}
|
|
40772
41147
|
async gitStatus(worktreeId) {
|
|
@@ -40836,12 +41211,16 @@ ${wt.landing.body}` : wt.landing.subject : undefined);
|
|
|
40836
41211
|
this.markPrompted(worktreeId);
|
|
40837
41212
|
}
|
|
40838
41213
|
markPrompted(worktreeId) {
|
|
41214
|
+
if (this.stampPrompted(worktreeId))
|
|
41215
|
+
this.d.hub.emit("worktreesChanged");
|
|
41216
|
+
}
|
|
41217
|
+
stampPrompted(worktreeId) {
|
|
40839
41218
|
const wt = this.d.state.worktree(worktreeId);
|
|
40840
41219
|
if (!wt)
|
|
40841
|
-
return;
|
|
41220
|
+
return false;
|
|
40842
41221
|
wt.promptedAt = Date.now();
|
|
40843
41222
|
this.d.state.save();
|
|
40844
|
-
|
|
41223
|
+
return true;
|
|
40845
41224
|
}
|
|
40846
41225
|
snapshot() {
|
|
40847
41226
|
const rows = [...this.ownedRows(), ...this.foundRows()];
|
|
@@ -40929,8 +41308,8 @@ ${wt.landing.body}` : wt.landing.subject : undefined);
|
|
|
40929
41308
|
usage: this.usageFor(wt.id),
|
|
40930
41309
|
transcript: this.writtenTranscript(wt.id),
|
|
40931
41310
|
sessionId: this.d.state.session(wt.id),
|
|
40932
|
-
shipping: this.
|
|
40933
|
-
syncQueued: this.
|
|
41311
|
+
shipping: this.ship.shippingOf(wt.id),
|
|
41312
|
+
syncQueued: this.ship.syncWaiting(wt.id) || undefined
|
|
40934
41313
|
};
|
|
40935
41314
|
});
|
|
40936
41315
|
}
|
|
@@ -40945,7 +41324,7 @@ ${wt.landing.body}` : wt.landing.subject : undefined);
|
|
|
40945
41324
|
ahead,
|
|
40946
41325
|
behind,
|
|
40947
41326
|
dirty,
|
|
40948
|
-
shipping: this.
|
|
41327
|
+
shipping: this.ship.shippingOf(f.id)
|
|
40949
41328
|
};
|
|
40950
41329
|
});
|
|
40951
41330
|
}
|
|
@@ -41056,17 +41435,17 @@ if (pruned > 0)
|
|
|
41056
41435
|
var managed = await loadManaged();
|
|
41057
41436
|
var paths = makePaths();
|
|
41058
41437
|
ensureDirs(paths);
|
|
41059
|
-
forwardTranscripts([paths.transcriptsDir, paths.archiveDir],
|
|
41438
|
+
forwardTranscripts([paths.transcriptsDir, paths.archiveDir], join37(paths.home, "transcripts-forwarded"));
|
|
41060
41439
|
var token = loadOrCreateToken(paths);
|
|
41061
41440
|
var port = Number(process.env.TOYON_PORT ?? DAEMON_DEFAULT_PORT);
|
|
41062
|
-
var helperApp =
|
|
41441
|
+
var helperApp = join37(paths.home, DAEMON_FILES.helper);
|
|
41063
41442
|
var helper = helperWanted({ platform: process.platform, cloud: cloud.enabled, port, home: process.env.TOYON_HOME }) ? new Helper({
|
|
41064
41443
|
dir: helperApp,
|
|
41065
41444
|
stub: HELPER_STUB,
|
|
41066
41445
|
bun: process.execPath,
|
|
41067
41446
|
cli: CLI_ENTRY,
|
|
41068
|
-
log:
|
|
41069
|
-
icon:
|
|
41447
|
+
log: join37(paths.home, DAEMON_FILES.launcherLog),
|
|
41448
|
+
icon: join37(SHELL_DIST, "icon.svg"),
|
|
41070
41449
|
version: package_default.version
|
|
41071
41450
|
}) : null;
|
|
41072
41451
|
if (helper) {
|
|
@@ -41091,7 +41470,7 @@ var tailscale = tailscaleCli();
|
|
|
41091
41470
|
var tailnet = new Tailnet({ ts: findTailscale() ? tailscale : null, hub });
|
|
41092
41471
|
var remoteSwitch = new RemoteSwitch({ setting, ts: tailscale, port, file: paths.remoteFile, policy: managed.policy });
|
|
41093
41472
|
var knocks = new Knocks(hub);
|
|
41094
|
-
var origins = new Origins({ setting, port, branded: () => branded });
|
|
41473
|
+
var origins = new Origins({ setting, port, branded: () => branded, appPorts: () => runtime.appPorts() });
|
|
41095
41474
|
var linuxSandbox = new LinuxSandbox;
|
|
41096
41475
|
await linuxSandbox.refresh();
|
|
41097
41476
|
var agents = loadAgentRegistry(paths.agentsFile, paths.agentsDir, linuxSandbox, managed.policy, paths.agentsFrom);
|
|
@@ -41167,7 +41546,8 @@ var worktrees = new WorktreeService({
|
|
|
41167
41546
|
fix,
|
|
41168
41547
|
pageErrors: (id) => pageErrors.ambient(id),
|
|
41169
41548
|
interested: (id) => interest.isSubscribed(id),
|
|
41170
|
-
shown: (id) => idle.isShown(id)
|
|
41549
|
+
shown: (id) => idle.isShown(id),
|
|
41550
|
+
message: (wt) => landing.message(wt.id)
|
|
41171
41551
|
});
|
|
41172
41552
|
interest.onFirst((id) => worktrees.tree.watch(id));
|
|
41173
41553
|
interest.onLast((id) => worktrees.tree.unwatch(id));
|