@agimon-ai/po-git 0.0.0-alpha.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.
@@ -0,0 +1,2122 @@
1
+ Object.defineProperties(exports, {
2
+ __esModule: { value: true },
3
+ [Symbol.toStringTag]: { value: "Module" }
4
+ });
5
+ const require_worktreeOperations = require("../worktreeOperations-MgIVDDu3.cjs");
6
+ let node_fs = require("node:fs");
7
+ node_fs = require_worktreeOperations.__toESM(node_fs, 1);
8
+ let node_os = require("node:os");
9
+ node_os = require_worktreeOperations.__toESM(node_os, 1);
10
+ let node_path = require("node:path");
11
+ node_path = require_worktreeOperations.__toESM(node_path, 1);
12
+ let _agimon_ai_po_core_serverFacet = require("@agimon-ai/po-core/serverFacet");
13
+ let _agimon_ai_po_core_extensionFile = require("@agimon-ai/po-core/extensionFile");
14
+ let _agimon_ai_po_session = require("@agimon-ai/po-session");
15
+ let typebox = require("typebox");
16
+ //#region src/schemas/runWorktreeTool.ts
17
+ /**
18
+ * The tool's action vocabulary.
19
+ *
20
+ * One tool with an action field rather than six tools, matching `subagent`:
21
+ * the actions share a subject and are meaningless apart from it, and six
22
+ * separate tools would spend six description budgets teaching the same nouns.
23
+ */
24
+ const WORKTREE_ACTIONS = {
25
+ spawn_worktree: "spawn_worktree",
26
+ close_worktree: "close_worktree",
27
+ list: "list",
28
+ status: "status",
29
+ merge: "merge",
30
+ prune: "prune",
31
+ send: "send",
32
+ messages: "messages"
33
+ };
34
+ /**
35
+ * Which fields each action accepts.
36
+ *
37
+ * Rejecting an unknown field is what stops a plausible-looking call from doing
38
+ * something other than what it reads like: `close_worktree` with `branch`
39
+ * instead of `id` would otherwise close whatever the default resolved to.
40
+ */
41
+ const WORKTREE_ACTION_FIELDS = {
42
+ spawn_worktree: [
43
+ "action",
44
+ "branch",
45
+ "baseRef",
46
+ "task",
47
+ "name"
48
+ ],
49
+ close_worktree: [
50
+ "action",
51
+ "id",
52
+ "force"
53
+ ],
54
+ list: ["action"],
55
+ status: ["action", "id"],
56
+ merge: [
57
+ "action",
58
+ "id",
59
+ "message"
60
+ ],
61
+ prune: ["action", "dryRun"],
62
+ send: [
63
+ "action",
64
+ "id",
65
+ "message"
66
+ ],
67
+ messages: ["action", "id"]
68
+ };
69
+ const SpawnParams = typebox.Type.Object({
70
+ action: typebox.Type.Literal(WORKTREE_ACTIONS.spawn_worktree),
71
+ branch: typebox.Type.String({ minLength: 1 }),
72
+ baseRef: typebox.Type.Optional(typebox.Type.String({ minLength: 1 })),
73
+ task: typebox.Type.Optional(typebox.Type.String({ minLength: 1 })),
74
+ name: typebox.Type.Optional(typebox.Type.String({ minLength: 1 }))
75
+ }, { additionalProperties: false });
76
+ const CloseParams = typebox.Type.Object({
77
+ action: typebox.Type.Literal(WORKTREE_ACTIONS.close_worktree),
78
+ id: typebox.Type.String({ minLength: 1 }),
79
+ force: typebox.Type.Optional(typebox.Type.Boolean())
80
+ }, { additionalProperties: false });
81
+ const ListParams = typebox.Type.Object({ action: typebox.Type.Literal(WORKTREE_ACTIONS.list) }, { additionalProperties: false });
82
+ const StatusParams = typebox.Type.Object({
83
+ action: typebox.Type.Literal(WORKTREE_ACTIONS.status),
84
+ id: typebox.Type.String({ minLength: 1 })
85
+ }, { additionalProperties: false });
86
+ const MergeParams = typebox.Type.Object({
87
+ action: typebox.Type.Literal(WORKTREE_ACTIONS.merge),
88
+ id: typebox.Type.String({ minLength: 1 }),
89
+ message: typebox.Type.Optional(typebox.Type.String({ minLength: 1 }))
90
+ }, { additionalProperties: false });
91
+ const PruneParams = typebox.Type.Object({
92
+ action: typebox.Type.Literal(WORKTREE_ACTIONS.prune),
93
+ dryRun: typebox.Type.Optional(typebox.Type.Boolean())
94
+ }, { additionalProperties: false });
95
+ const SendParams = typebox.Type.Object({
96
+ action: typebox.Type.Literal(WORKTREE_ACTIONS.send),
97
+ id: typebox.Type.String({ minLength: 1 }),
98
+ message: typebox.Type.String({ minLength: 1 })
99
+ }, { additionalProperties: false });
100
+ const MessagesParams = typebox.Type.Object({
101
+ action: typebox.Type.Literal(WORKTREE_ACTIONS.messages),
102
+ id: typebox.Type.String({ minLength: 1 })
103
+ }, { additionalProperties: false });
104
+ typebox.Type.Union([
105
+ SpawnParams,
106
+ CloseParams,
107
+ ListParams,
108
+ StatusParams,
109
+ MergeParams,
110
+ PruneParams,
111
+ SendParams,
112
+ MessagesParams
113
+ ]);
114
+ /** The actions that accept `field`, so the declared docs cannot drift from the validator. */
115
+ function acceptingActions(field) {
116
+ return Object.values(WORKTREE_ACTIONS).filter((action) => WORKTREE_ACTION_FIELDS[action].includes(field)).join(", ");
117
+ }
118
+ /**
119
+ * The schema declared to the model.
120
+ *
121
+ * A flat object rather than `RunWorktreeParams` itself. A top-level union is
122
+ * `{anyOf:[...]}` with no `properties`, and Pi's Anthropic Messages adapter
123
+ * rebuilds tool input as `{type:'object', properties: schema.properties ?? {},
124
+ * required: schema.required ?? []}`, so the union arrived at the model as an
125
+ * empty object while the host went on validating calls against it. Nothing here
126
+ * relaxes what is accepted: `validateParams` still rejects an unknown action, a
127
+ * field the action does not take, and a missing required one.
128
+ */
129
+ const RunWorktreeToolSchema = typebox.Type.Object({
130
+ action: typebox.Type.String({
131
+ enum: Object.values(WORKTREE_ACTIONS),
132
+ description: "The operation to run. Every other field is accepted only by the actions its description names."
133
+ }),
134
+ branch: typebox.Type.Optional(typebox.Type.String({
135
+ minLength: 1,
136
+ description: `New branch to create. Actions: ${acceptingActions("branch")}.`
137
+ })),
138
+ baseRef: typebox.Type.Optional(typebox.Type.String({
139
+ minLength: 1,
140
+ description: `Base ref, defaulting to the current branch. Actions: ${acceptingActions("baseRef")}.`
141
+ })),
142
+ task: typebox.Type.Optional(typebox.Type.String({
143
+ minLength: 1,
144
+ description: `First message for the new session. Actions: ${acceptingActions("task")}.`
145
+ })),
146
+ name: typebox.Type.Optional(typebox.Type.String({
147
+ minLength: 1,
148
+ description: `Rail label for the worktree. Actions: ${acceptingActions("name")}.`
149
+ })),
150
+ id: typebox.Type.Optional(typebox.Type.String({
151
+ minLength: 1,
152
+ description: `Worktree id. Actions: ${acceptingActions("id")}.`
153
+ })),
154
+ force: typebox.Type.Optional(typebox.Type.Boolean({ description: `Close even when the tree is dirty. Actions: ${acceptingActions("force")}.` })),
155
+ message: typebox.Type.Optional(typebox.Type.String({
156
+ minLength: 1,
157
+ description: `Merge commit message, or the text to send. Actions: ${acceptingActions("message")}.`
158
+ })),
159
+ dryRun: typebox.Type.Optional(typebox.Type.Boolean({ description: `Report the plan without destroying anything. Actions: ${acceptingActions("dryRun")}.` }))
160
+ }, { additionalProperties: false });
161
+ //#endregion
162
+ //#region src/services/runWorktree/index.ts
163
+ const RUN_WORKTREE_TOOL_NAME = "run_worktree";
164
+ const IMPLEMENTED_ACTIONS = new Set(Object.values(WORKTREE_ACTIONS));
165
+ const RUN_WORKTREE_DESCRIPTION = `Create and manage git worktrees, each with its own Po session nested under this one.
166
+
167
+ Actions:
168
+ - spawn_worktree: create a worktree on a new branch and start a session in it. Requires 'branch'. Optional 'baseRef' (defaults to the current branch's remote-tracking ref, then the remote default branch), 'name' for the rail, 'task' as the first message.
169
+ - close_worktree: stop a worktree's session and delete its checkout. Requires 'id'. Refuses a dirty tree unless 'force' is true.
170
+ - list: every worktree for this repository, with status and relationship to the calling session.
171
+ - status: one worktree, its relationship to the calling session, and its uncommitted files. Requires 'id'.
172
+ - merge: merge a worktree's branch into this checkout. Requires 'id'. Refuses when this checkout is dirty.
173
+ - prune: remove orphaned worktrees. Pass 'dryRun' to see the plan without destroying anything.
174
+ - send: send a message to the other side of a worktree. Requires 'id' and 'message'.
175
+ - messages: read messages waiting for you from a worktree. Requires 'id'.
176
+
177
+ For a nested worktree session, use the record labelled 'child' in list to report to your parent. Use records labelled 'parent' to contact your own children. Other records are not communication edges.
178
+ Worktrees live outside the repository, under ~/.po/git/worktrees.`;
179
+ /** Rejects calls that do not match one documented action shape. */
180
+ function validateParams(input) {
181
+ if (typeof input !== "object" || input === null || Array.isArray(input)) throw require_worktreeOperations.invalidRequest("run_worktree parameters must be an object.", "Call it with one documented action shape.");
182
+ const record = input;
183
+ const action = record.action;
184
+ if (typeof action !== "string" || !IMPLEMENTED_ACTIONS.has(action)) throw require_worktreeOperations.invalidRequest(`Unknown action ${typeof action === "string" ? `'${action}'` : "value"}.`, `Use one of: ${[...IMPLEMENTED_ACTIONS].join(", ")}.`);
185
+ const allowed = WORKTREE_ACTION_FIELDS[action];
186
+ const unexpected = Object.keys(record).filter((key) => !allowed.includes(key));
187
+ if (unexpected.length > 0) throw require_worktreeOperations.invalidRequest(`action='${action}' does not accept: ${unexpected.join(", ")}.`, `Fields for '${action}': ${allowed.join(", ")}.`);
188
+ for (const field of ["branch", "id"]) if (allowed.includes(field) && field in record) {
189
+ const value = record[field];
190
+ if (typeof value !== "string" || value.trim() === "") throw require_worktreeOperations.invalidRequest(`action='${action}' requires a nonblank '${field}'.`, `Pass a ${field}.`);
191
+ }
192
+ for (const field of [
193
+ "baseRef",
194
+ "name",
195
+ "task",
196
+ "message"
197
+ ]) if (field in record && (typeof record[field] !== "string" || record[field].trim() === "")) throw require_worktreeOperations.invalidRequest(`action='${action}' requires a nonblank '${field}'.`, `Pass a ${field}.`);
198
+ for (const field of ["force", "dryRun"]) if (field in record && typeof record[field] !== "boolean") throw require_worktreeOperations.invalidRequest(`action='${action}' requires boolean '${field}'.`, `Pass true or false for ${field}.`);
199
+ if (action === WORKTREE_ACTIONS.send && typeof record.message !== "string") throw require_worktreeOperations.invalidRequest("action='send' requires 'message'.", "Pass the message to send.");
200
+ if (action === WORKTREE_ACTIONS.spawn_worktree && typeof record.branch !== "string") throw require_worktreeOperations.invalidRequest("action='spawn_worktree' requires 'branch'.", "Name the branch to create.");
201
+ if ((action === WORKTREE_ACTIONS.close_worktree || action === WORKTREE_ACTIONS.status || action === WORKTREE_ACTIONS.merge || action === WORKTREE_ACTIONS.send || action === WORKTREE_ACTIONS.messages) && typeof record.id !== "string") throw require_worktreeOperations.invalidRequest(`action='${action}' requires 'id'.`, "Call list to see the current ids.");
202
+ return record;
203
+ }
204
+ function describe(record, sessionId) {
205
+ const relationship = record.parentSessionId === sessionId ? "parent" : record.sessionId === sessionId ? "child" : "other session";
206
+ return `${record.id} ${record.branch} ${record.status} (${relationship}) ${record.path}`;
207
+ }
208
+ function text(...lines) {
209
+ return [{
210
+ type: "text",
211
+ text: lines.join("\n")
212
+ }];
213
+ }
214
+ async function executeRunWorktreeTool(params, operations, context, options = {}) {
215
+ switch (params.action) {
216
+ case WORKTREE_ACTIONS.spawn_worktree: {
217
+ const record = await operations.spawn(context, {
218
+ branch: params.branch,
219
+ ...params.baseRef === void 0 ? {} : { baseRef: params.baseRef },
220
+ ...params.name === void 0 ? {} : { name: params.name },
221
+ ...params.task === void 0 ? {} : { task: params.task }
222
+ }, options);
223
+ return {
224
+ content: text(`Worktree ${record.id} created on ${record.branch} from ${record.baseRef}.`, `Path: ${record.path}`, params.task === void 0 ? `Session ${record.sessionId} is starting; it appears nested under this one in the rail.` : `Session ${record.sessionId} is starting with its task; it appears nested under this one in the rail.`),
225
+ details: {
226
+ action: params.action,
227
+ records: [record]
228
+ }
229
+ };
230
+ }
231
+ case WORKTREE_ACTIONS.close_worktree: {
232
+ const record = await operations.close(context, params.id, params.force ?? false);
233
+ return {
234
+ content: text(`Worktree ${record.id} (${record.branch}) closed and removed.`),
235
+ details: {
236
+ action: params.action,
237
+ records: [record]
238
+ }
239
+ };
240
+ }
241
+ case WORKTREE_ACTIONS.list: {
242
+ const records = await operations.list(context);
243
+ return {
244
+ content: records.length === 0 ? text("No worktrees for this repository.") : text(`${String(records.length)} worktree(s):`, ...records.map((record) => describe(record, context.sessionId))),
245
+ details: {
246
+ action: params.action,
247
+ records
248
+ }
249
+ };
250
+ }
251
+ case WORKTREE_ACTIONS.status: {
252
+ const { record, dirtyFiles } = await operations.status(context, params.id);
253
+ return {
254
+ content: text(describe(record, context.sessionId), `Session: ${record.sessionId}`, dirtyFiles.length === 0 ? "Clean." : `${String(dirtyFiles.length)} uncommitted file(s): ${dirtyFiles.slice(0, 20).join(", ")}`),
255
+ details: {
256
+ action: params.action,
257
+ records: [record]
258
+ }
259
+ };
260
+ }
261
+ case WORKTREE_ACTIONS.merge: {
262
+ const record = await operations.merge(context, params.id, ...params.message === void 0 ? [] : [params.message]);
263
+ return {
264
+ content: text(`Merged ${record.branch} into this checkout.`),
265
+ details: {
266
+ action: params.action,
267
+ records: [record]
268
+ }
269
+ };
270
+ }
271
+ case WORKTREE_ACTIONS.send:
272
+ await operations.send(context, params.id, params.message);
273
+ return {
274
+ content: text(`Message sent to worktree ${params.id}.`),
275
+ details: { action: params.action }
276
+ };
277
+ case WORKTREE_ACTIONS.messages: {
278
+ const waiting = await operations.messages(context, params.id);
279
+ return {
280
+ content: waiting.length === 0 ? text("No messages.") : text(`${String(waiting.length)} message(s):`, ...waiting.map((entry) => `[${entry.sentAt}] ${entry.from}: ${entry.text}`)),
281
+ details: { action: params.action }
282
+ };
283
+ }
284
+ case WORKTREE_ACTIONS.prune: {
285
+ const dryRun = params.dryRun ?? false;
286
+ const plan = await operations.prune(context, dryRun);
287
+ return {
288
+ content: text(...[
289
+ dryRun ? "Prune plan (nothing was changed):" : "Pruned:",
290
+ `Remove: ${plan.remove.length === 0 ? "none" : plan.remove.map((record) => record.id).join(", ")}`,
291
+ `Forget: ${plan.forget.length === 0 ? "none" : plan.forget.map((record) => record.id).join(", ")}`,
292
+ `Kept, uncommitted work: ${plan.keptDirty.length === 0 ? "none" : plan.keptDirty.map((record) => record.id).join(", ")}`,
293
+ `Untracked directories, left alone: ${plan.untracked.length === 0 ? "none" : plan.untracked.join(", ")}`,
294
+ ...plan.keptBranches.length === 0 ? [] : [`Branches kept, they hold commits: ${plan.keptBranches.join(", ")}`]
295
+ ]),
296
+ details: { action: params.action }
297
+ };
298
+ }
299
+ }
300
+ }
301
+ //#endregion
302
+ //#region src/extensions/workspaces/sessions/(backend)/_lib/root.server.ts
303
+ function progressResult(action, label) {
304
+ return {
305
+ content: [{
306
+ type: "text",
307
+ text: label
308
+ }],
309
+ details: { action }
310
+ };
311
+ }
312
+ function createGitSession({ context, host: serverHost }) {
313
+ if (serverHost.context.directEvents === void 0) throw new Error("Git headless facet requires the session direct event bus.");
314
+ if (serverHost.context.sessionId === void 0) throw new Error("Git headless facet requires a session identity.");
315
+ const sessionId = serverHost.context.sessionId;
316
+ const directEvents = serverHost.context.directEvents;
317
+ const messageInbox = require_worktreeOperations.createWorktreeMessageInbox(directEvents, sessionId);
318
+ const operations = require_worktreeOperations.createWorktreeOperations({
319
+ git: require_worktreeOperations.createWorktreeGit(),
320
+ sessionService: serverHost.context.sessionService,
321
+ messageInbox,
322
+ sessionDelivery: () => (0, _agimon_ai_po_session.readDoomSessionDelivery)(context)
323
+ });
324
+ const resources = [{
325
+ when: {
326
+ state: { "minor-mode": "help" },
327
+ attribution: {
328
+ kind: "minor",
329
+ mode: "help"
330
+ }
331
+ },
332
+ name: "po-git",
333
+ kind: "context",
334
+ read: () => (0, _agimon_ai_po_core_serverFacet.readPackageResource)(require("url").pathToFileURL(__filename).href, "llms.txt")
335
+ }, {
336
+ name: "po-use-git",
337
+ kind: "skill",
338
+ read: () => (0, _agimon_ai_po_core_serverFacet.readPackageResource)(require("url").pathToFileURL(__filename).href, "src/prompts/po-use-git/SKILL.md")
339
+ }];
340
+ const tool = {
341
+ name: RUN_WORKTREE_TOOL_NAME,
342
+ label: "Worktree",
343
+ description: RUN_WORKTREE_DESCRIPTION,
344
+ parameters: RunWorktreeToolSchema,
345
+ promptSnippet: "Create and manage Git worktree sessions",
346
+ executionMode: "serial",
347
+ async execute(_toolCallId, parameters, signal, onUpdate, execution) {
348
+ const params = validateParams(parameters);
349
+ return executeRunWorktreeTool(params, operations, execution, {
350
+ ...signal === void 0 ? {} : { signal },
351
+ onProgress: (label) => onUpdate?.(progressResult(params.action, label))
352
+ });
353
+ }
354
+ };
355
+ const changed = (_event, execution) => {
356
+ directEvents.publish(require_worktreeOperations.GIT_SESSION_CHANGED_EVENT, execution.sessionId, { version: 1 });
357
+ };
358
+ return {
359
+ value: {
360
+ resources,
361
+ tool,
362
+ hooks: [{
363
+ event: "tool_execution_end",
364
+ handle: changed
365
+ }, {
366
+ event: "agent_settled",
367
+ handle: changed
368
+ }]
369
+ },
370
+ onDispose: () => messageInbox.close()
371
+ };
372
+ }
373
+ const root = (0, _agimon_ai_po_core_extensionFile.defineRoot)(createGitSession);
374
+ //#endregion
375
+ //#region src/extensions/workspaces/sessions/(backend)/root.server.ts
376
+ var root_server_default = (0, _agimon_ai_po_core_extensionFile.defineRoot)(root);
377
+ //#endregion
378
+ //#region src/services/branchDiff/index.ts
379
+ /**
380
+ * What a session's checkout changed against its base, the way a pull request
381
+ * shows it: every commit since the branch left its base, plus staged, unstaged
382
+ * and untracked work.
383
+ *
384
+ * DESIGN PATTERNS:
385
+ * - git computes every diff. The counts come from `--numstat` and the hunks
386
+ * from the same unified diff, so the two always agree.
387
+ * - The base is the worktree's own recorded base when there is one, then the
388
+ * remote's default branch, then the branch's upstream, and with none of those
389
+ * only uncommitted work counts. It is always named in the UI.
390
+ * - Untracked files are read only when they are small regular files; a symlink
391
+ * shows its link text and is never followed.
392
+ *
393
+ * AVOID:
394
+ * - Passing a browser path to git without checking it against the change set.
395
+ */
396
+ /** git's well-known empty tree, the base of a repository with no commits. */
397
+ const EMPTY_TREE = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
398
+ const MAX_LISTED_FILES = 500;
399
+ const MAX_UNTRACKED_COUNTED = 200;
400
+ const MAX_FILE_BYTES = 1048576;
401
+ const MAX_ROWS = 5e3;
402
+ const BINARY_PROBE_BYTES = 8192;
403
+ const SHORT_SHA = 7;
404
+ const DEFAULT_BRANCHES = ["main", "master"];
405
+ async function read(cwd, args) {
406
+ const result = await require_worktreeOperations.runGit(cwd, args, { timeout: require_worktreeOperations.READ_TIMEOUT_MS });
407
+ return result.code === 0 ? result.stdout : void 0;
408
+ }
409
+ async function readLine$1(cwd, args) {
410
+ const out = (await read(cwd, args))?.trim();
411
+ return out === void 0 || out === "" ? void 0 : out;
412
+ }
413
+ async function commitExists(cwd, ref) {
414
+ return await readLine$1(cwd, [
415
+ "rev-parse",
416
+ "--verify",
417
+ "--quiet",
418
+ `${ref}^{commit}`
419
+ ]) !== void 0;
420
+ }
421
+ /** A local branch's upstream, so the review measures against what fetch keeps current. */
422
+ async function upstreamOf(cwd, ref) {
423
+ return readLine$1(cwd, [
424
+ "rev-parse",
425
+ "--abbrev-ref",
426
+ "--symbolic-full-name",
427
+ `${ref}@{upstream}`
428
+ ]);
429
+ }
430
+ /** The ref the review is measured against. Exported for sync, whose rebase uses the same base. */
431
+ async function resolveBaseRef(cwd, branch, recordedBaseRef) {
432
+ if (!(recordedBaseRef === void 0 || branch !== void 0 && (recordedBaseRef === branch || recordedBaseRef.endsWith(`/${branch}`))) && recordedBaseRef !== void 0 && await commitExists(cwd, recordedBaseRef)) return (await readLine$1(cwd, [
433
+ "rev-parse",
434
+ "--verify",
435
+ "--quiet",
436
+ `refs/heads/${recordedBaseRef}`
437
+ ]) !== void 0 ? await upstreamOf(cwd, recordedBaseRef) : void 0) ?? recordedBaseRef;
438
+ const originHead = await readLine$1(cwd, [
439
+ "symbolic-ref",
440
+ "--quiet",
441
+ "--short",
442
+ "refs/remotes/origin/HEAD"
443
+ ]);
444
+ if (originHead !== void 0) return originHead;
445
+ for (const name of DEFAULT_BRANCHES) if (await commitExists(cwd, `origin/${name}`)) return `origin/${name}`;
446
+ return branch === void 0 ? void 0 : upstreamOf(cwd, branch);
447
+ }
448
+ async function resolveBase(cwd, branch, recordedBaseRef) {
449
+ const ref = await resolveBaseRef(cwd, branch, recordedBaseRef);
450
+ const head = await commitExists(cwd, "HEAD");
451
+ if (ref !== void 0 && head) {
452
+ const mergeBase = await readLine$1(cwd, [
453
+ "merge-base",
454
+ ref,
455
+ "HEAD"
456
+ ]);
457
+ if (mergeBase !== void 0) return {
458
+ ref,
459
+ from: mergeBase,
460
+ mergeBase: mergeBase.slice(0, SHORT_SHA)
461
+ };
462
+ }
463
+ return { from: head ? "HEAD" : EMPTY_TREE };
464
+ }
465
+ /** NUL-separated fields from a `-z` listing, without the trailing empty one. */
466
+ function zFields(out) {
467
+ if (out === void 0 || out === "") return [];
468
+ const fields = out.split("\0");
469
+ if (fields.at(-1) === "") fields.pop();
470
+ return fields;
471
+ }
472
+ const NAME_STATUS = {
473
+ A: "added",
474
+ D: "deleted",
475
+ M: "modified",
476
+ T: "modified"
477
+ };
478
+ /** Line count of a small regular file, or undefined when it is binary, large, or not a regular file. */
479
+ function untrackedLines(root, relPath) {
480
+ const absolute = node_path.default.join(root, relPath);
481
+ let stat;
482
+ try {
483
+ stat = node_fs.default.lstatSync(absolute);
484
+ } catch {
485
+ return;
486
+ }
487
+ if (stat.isSymbolicLink()) return {
488
+ lines: 1,
489
+ binary: false
490
+ };
491
+ if (!stat.isFile() || stat.size > MAX_FILE_BYTES) return void 0;
492
+ const content = node_fs.default.readFileSync(absolute);
493
+ if (content.subarray(0, BINARY_PROBE_BYTES).includes(0)) return {
494
+ lines: 0,
495
+ binary: true
496
+ };
497
+ if (content.length === 0) return {
498
+ lines: 0,
499
+ binary: false
500
+ };
501
+ const text = content.toString("utf8");
502
+ const newlines = text.split("\n").length - 1;
503
+ return {
504
+ lines: text.endsWith("\n") ? newlines : newlines + 1,
505
+ binary: false
506
+ };
507
+ }
508
+ /** The hunks of a unified diff, numbering each row by the file it lives in. */
509
+ function parseUnifiedDiff(diff) {
510
+ const hunks = [];
511
+ let current;
512
+ let oldLine = 0;
513
+ let newLine = 0;
514
+ let rows = 0;
515
+ let binary = false;
516
+ for (const line of diff.split("\n")) {
517
+ const header = /^@@ -(\d+)(?:,\d+)? \+(\d+)(?:,\d+)? @@/u.exec(line);
518
+ if (header !== null) {
519
+ oldLine = Number(header[1]);
520
+ newLine = Number(header[2]);
521
+ current = {
522
+ start: newLine,
523
+ rows: []
524
+ };
525
+ hunks.push(current);
526
+ continue;
527
+ }
528
+ if (current === void 0) {
529
+ if (line.startsWith("Binary files ") || line === "GIT binary patch") binary = true;
530
+ continue;
531
+ }
532
+ const marker = line[0];
533
+ let row;
534
+ if (marker === " ") row = {
535
+ marker: " ",
536
+ line: newLine,
537
+ content: line.slice(1)
538
+ };
539
+ else if (marker === "-") row = {
540
+ marker: "-",
541
+ line: oldLine,
542
+ content: line.slice(1)
543
+ };
544
+ else if (marker === "+") row = {
545
+ marker: "+",
546
+ line: newLine,
547
+ content: line.slice(1)
548
+ };
549
+ if (row === void 0) continue;
550
+ if (marker !== "+") oldLine += 1;
551
+ if (marker !== "-") newLine += 1;
552
+ current.rows.push(row);
553
+ rows += 1;
554
+ }
555
+ return {
556
+ hunks,
557
+ binary,
558
+ rows
559
+ };
560
+ }
561
+ function createBranchDiff() {
562
+ return {
563
+ async review(cwd, options = {}) {
564
+ const root = await readLine$1(cwd, ["rev-parse", "--show-toplevel"]);
565
+ if (root === void 0) return void 0;
566
+ const branch = await readLine$1(root, [
567
+ "symbolic-ref",
568
+ "--quiet",
569
+ "--short",
570
+ "HEAD"
571
+ ]);
572
+ const base = await resolveBase(root, branch, options.recordedBaseRef);
573
+ const numstat = zFields(await read(root, [
574
+ "diff",
575
+ "--numstat",
576
+ "-z",
577
+ "--no-renames",
578
+ base.from
579
+ ]));
580
+ const nameStatus = zFields(await read(root, [
581
+ "diff",
582
+ "--name-status",
583
+ "-z",
584
+ "--no-renames",
585
+ base.from
586
+ ]));
587
+ const unmerged = new Set(zFields(await read(root, [
588
+ "diff",
589
+ "--name-only",
590
+ "--diff-filter=U",
591
+ "-z"
592
+ ])));
593
+ const statusOf = /* @__PURE__ */ new Map();
594
+ for (let index = 0; index + 1 < nameStatus.length; index += 2) statusOf.set(nameStatus[index + 1] ?? "", NAME_STATUS[(nameStatus[index] ?? "").charAt(0)] ?? "modified");
595
+ const entries = [];
596
+ for (const field of numstat) {
597
+ const [added = "", removed = "", ...rest] = field.split(" ");
598
+ const filePath = rest.join(" ");
599
+ if (filePath === "") continue;
600
+ const binary = added === "-" && removed === "-";
601
+ entries.push({
602
+ path: filePath,
603
+ status: unmerged.has(filePath) ? "conflicted" : statusOf.get(filePath) ?? "modified",
604
+ added: binary ? 0 : Number(added),
605
+ removed: binary ? 0 : Number(removed),
606
+ ...binary ? { binary: true } : {}
607
+ });
608
+ }
609
+ const untracked = zFields(await read(root, [
610
+ "ls-files",
611
+ "--others",
612
+ "--exclude-standard",
613
+ "-z"
614
+ ]));
615
+ untracked.forEach((relPath, index) => {
616
+ const counted = index < MAX_UNTRACKED_COUNTED ? untrackedLines(root, relPath) : void 0;
617
+ entries.push({
618
+ path: relPath,
619
+ status: "untracked",
620
+ added: counted?.lines ?? 0,
621
+ removed: 0,
622
+ ...counted?.binary ? { binary: true } : {}
623
+ });
624
+ });
625
+ entries.sort((left, right) => left.path.localeCompare(right.path));
626
+ const conflicts = [...unmerged];
627
+ const rebasing = [await readLine$1(root, [
628
+ "rev-parse",
629
+ "--git-path",
630
+ "rebase-merge"
631
+ ]), await readLine$1(root, [
632
+ "rev-parse",
633
+ "--git-path",
634
+ "rebase-apply"
635
+ ])].some((gitPath) => gitPath !== void 0 && node_fs.default.existsSync(node_path.default.resolve(root, gitPath)));
636
+ const upstreamRef = branch === void 0 ? void 0 : await upstreamOf(root, branch);
637
+ let upstream;
638
+ if (upstreamRef !== void 0) {
639
+ const [behind, ahead] = (await readLine$1(root, [
640
+ "rev-list",
641
+ "--left-right",
642
+ "--count",
643
+ `${upstreamRef}...HEAD`
644
+ ]) ?? "").split(/\s+/u).map(Number);
645
+ if (Number.isInteger(ahead) && Number.isInteger(behind)) upstream = {
646
+ ref: upstreamRef,
647
+ ahead,
648
+ behind
649
+ };
650
+ }
651
+ const truncated = entries.length > MAX_LISTED_FILES || untracked.length > MAX_UNTRACKED_COUNTED;
652
+ return {
653
+ root,
654
+ base,
655
+ changes: {
656
+ added: entries.reduce((sum, entry) => sum + entry.added, 0),
657
+ removed: entries.reduce((sum, entry) => sum + entry.removed, 0),
658
+ files: entries.length,
659
+ ...branch === void 0 ? {} : { branch },
660
+ ...base.ref === void 0 ? {} : { base: base.ref },
661
+ ...truncated ? { truncated: true } : {},
662
+ ...upstream === void 0 ? {} : { upstream },
663
+ ...rebasing ? { rebase: { conflicts } } : {}
664
+ },
665
+ summary: {
666
+ repository: true,
667
+ files: entries.slice(0, MAX_LISTED_FILES),
668
+ ...branch === void 0 ? {} : { branch },
669
+ ...base.ref === void 0 ? {} : { base: base.ref },
670
+ ...base.mergeBase === void 0 ? {} : { mergeBase: base.mergeBase },
671
+ ...truncated ? { truncated: true } : {}
672
+ }
673
+ };
674
+ },
675
+ async fileDiff(review, filePath) {
676
+ const entry = review.summary.files.find((file) => file.path === filePath);
677
+ if (entry === void 0) throw new Error("That path is not in this review.");
678
+ if (entry.status === "untracked") {
679
+ const absolute = node_path.default.join(review.root, entry.path);
680
+ const stat = node_fs.default.lstatSync(absolute);
681
+ if (stat.isSymbolicLink()) return {
682
+ path: entry.path,
683
+ hunks: [{
684
+ start: 1,
685
+ rows: [{
686
+ marker: "+",
687
+ line: 1,
688
+ content: node_fs.default.readlinkSync(absolute)
689
+ }]
690
+ }]
691
+ };
692
+ if (!stat.isFile() || stat.size > MAX_FILE_BYTES) return {
693
+ path: entry.path,
694
+ hunks: [],
695
+ tooLarge: true
696
+ };
697
+ const content = node_fs.default.readFileSync(absolute);
698
+ if (content.subarray(0, BINARY_PROBE_BYTES).includes(0)) return {
699
+ path: entry.path,
700
+ hunks: [],
701
+ binary: true
702
+ };
703
+ const lines = content.toString("utf8").split("\n");
704
+ if (lines.at(-1) === "") lines.pop();
705
+ if (lines.length > MAX_ROWS) return {
706
+ path: entry.path,
707
+ hunks: [],
708
+ tooLarge: true
709
+ };
710
+ return {
711
+ path: entry.path,
712
+ hunks: lines.length === 0 ? [] : [{
713
+ start: 1,
714
+ rows: lines.map((content, index) => ({
715
+ marker: "+",
716
+ line: index + 1,
717
+ content
718
+ }))
719
+ }]
720
+ };
721
+ }
722
+ const result = await require_worktreeOperations.runGit(review.root, [
723
+ "diff",
724
+ "-U3",
725
+ "--no-color",
726
+ "--no-ext-diff",
727
+ "--no-textconv",
728
+ "--no-renames",
729
+ review.base.from,
730
+ "--",
731
+ `:(top,literal)${entry.path}`
732
+ ], {
733
+ timeout: require_worktreeOperations.READ_TIMEOUT_MS,
734
+ maxBuffer: MAX_FILE_BYTES
735
+ });
736
+ if (result.overflow) return {
737
+ path: entry.path,
738
+ hunks: [],
739
+ tooLarge: true
740
+ };
741
+ if (result.code !== 0) throw new Error(`git diff failed (exit ${String(result.code)}).`);
742
+ const parsed = parseUnifiedDiff(result.stdout);
743
+ if (parsed.binary || entry.binary === true) return {
744
+ path: entry.path,
745
+ hunks: [],
746
+ binary: true
747
+ };
748
+ if (parsed.rows > MAX_ROWS) return {
749
+ path: entry.path,
750
+ hunks: [],
751
+ tooLarge: true
752
+ };
753
+ return {
754
+ path: entry.path,
755
+ hunks: parsed.hunks
756
+ };
757
+ }
758
+ };
759
+ }
760
+ //#endregion
761
+ //#region src/services/gitAuth/index.ts
762
+ /**
763
+ * Per-workspace remote auth for po-git's own fetch, pull, push and rebase.
764
+ *
765
+ * DESIGN PATTERNS:
766
+ * - One file outside every repository, folder 0700 and file 0600, repaired on
767
+ * read if something loosened it. The token never leaves this module except
768
+ * into the environment of one git child process.
769
+ * - Config reaches git through GIT_CONFIG_COUNT environment entries, never
770
+ * argv, so nothing secret shows in `ps`. The first entry empties the
771
+ * credential helper list, which is what keeps macOS's osxkeychain helper from
772
+ * storing the token after a successful push.
773
+ * - The token is offered only to the host it was entered for. A repository
774
+ * whose remote points anywhere else gets this machine's own credentials.
775
+ *
776
+ * AVOID:
777
+ * - Touching process.env. The hub runs every session; anything set there
778
+ * would reach the agent's shell, which the user ruled out.
779
+ */
780
+ const OWNER_ONLY_DIRECTORY = 448;
781
+ const OWNER_ONLY_FILE = 384;
782
+ const LOOSE_BITS = 63;
783
+ const HOST_PATTERN = /^[a-z0-9.-]{1,253}(:\d{1,5})?$/u;
784
+ const MAX_USERNAME = 256;
785
+ const MAX_TOKEN = 4096;
786
+ const SETTINGS_HINT = "Open settings, then repository, then git remote.";
787
+ /** Environment that would make git log or skip what this module relies on. */
788
+ const STRIPPED_ALWAYS = ["GIT_CURL_VERBOSE", "GIT_TRACE_REDACT"];
789
+ const STRIPPED_PREFIXES = ["GIT_TRACE"];
790
+ /** Environment that could redirect or override credentials when the workspace's own are in play. */
791
+ const STRIPPED_CREDENTIALED = [
792
+ "GIT_CONFIG_PARAMETERS",
793
+ "GIT_CONFIG_COUNT",
794
+ "GIT_SSL_NO_VERIFY",
795
+ "SSH_ASKPASS",
796
+ "GIT_SSH",
797
+ "GIT_SSH_COMMAND"
798
+ ];
799
+ const CONFIG_ENTRY = /^GIT_CONFIG_(KEY|VALUE)_\d+$/u;
800
+ /** The inline helper: answers `get` from this child's environment, and ignores `store` and `erase`. */
801
+ const ENV_CREDENTIAL_HELPER = "!f(){ test \"$1\" = get && printf \"username=%s\\npassword=%s\\n\" \"$PO_GIT_USERNAME\" \"$PO_GIT_TOKEN\"; }; f";
802
+ /** Whether a value holds an ASCII control character; a newline in a credential field could add a line to git's credential protocol. */
803
+ function hasControl(value) {
804
+ for (let index = 0; index < value.length; index += 1) {
805
+ const code = value.charCodeAt(index);
806
+ if (code < 32 || code === 127) return true;
807
+ }
808
+ return false;
809
+ }
810
+ function isRecord(value) {
811
+ return typeof value === "object" && value !== null && !Array.isArray(value);
812
+ }
813
+ /** The workspace key: the canonical root, so a symlinked path and its target share one entry. */
814
+ function workspaceKey(workspaceRoot) {
815
+ try {
816
+ return node_fs.default.realpathSync(workspaceRoot);
817
+ } catch {
818
+ return node_path.default.resolve(workspaceRoot);
819
+ }
820
+ }
821
+ function expandHome(keyPath, homeDir) {
822
+ return keyPath.startsWith("~/") ? node_path.default.join(homeDir, keyPath.slice(2)) : keyPath;
823
+ }
824
+ function parseConfig(value) {
825
+ if (!isRecord(value)) return void 0;
826
+ if (value.method === "none") return { method: "none" };
827
+ if (value.method === "ssh") {
828
+ if (value.keyPath === void 0) return { method: "ssh" };
829
+ return typeof value.keyPath === "string" && value.keyPath !== "" ? {
830
+ method: "ssh",
831
+ keyPath: value.keyPath
832
+ } : void 0;
833
+ }
834
+ if (value.method === "https" && typeof value.host === "string" && typeof value.username === "string" && typeof value.token === "string" && value.token !== "") return {
835
+ method: "https",
836
+ host: value.host,
837
+ username: value.username,
838
+ token: value.token
839
+ };
840
+ }
841
+ /** What the browser may see. The token becomes a yes or no. */
842
+ function publicView(config) {
843
+ if (config.method === "ssh") return config.keyPath === void 0 ? {
844
+ method: "ssh",
845
+ ssh: {}
846
+ } : {
847
+ method: "ssh",
848
+ ssh: { keyPath: config.keyPath }
849
+ };
850
+ if (config.method === "https") return {
851
+ method: "https",
852
+ https: {
853
+ host: config.host,
854
+ username: config.username,
855
+ hasToken: config.token !== ""
856
+ }
857
+ };
858
+ return { method: "none" };
859
+ }
860
+ /**
861
+ * A browser save request, checked field by field. An absent HTTPS token keeps
862
+ * the saved one, and only for the same host: a token never follows a host it
863
+ * was not entered for.
864
+ */
865
+ function parseGitAuthSave(request, current, homeDir) {
866
+ if (!isRecord(request)) throw require_worktreeOperations.invalidRequest("The git auth request is not an object.", "Send method none, ssh or https.");
867
+ if (request.method === "none") return { method: "none" };
868
+ if (request.method === "ssh") {
869
+ if (request.keyPath === void 0 || request.keyPath === "") return { method: "ssh" };
870
+ if (typeof request.keyPath !== "string" || hasControl(request.keyPath)) throw require_worktreeOperations.invalidRequest("The key path is not a plain path.", "Give the key as an absolute path or one under ~/.");
871
+ const keyPath = request.keyPath.trim();
872
+ if (!keyPath.startsWith("/") && !keyPath.startsWith("~/")) throw require_worktreeOperations.invalidRequest("The key path is not absolute.", "Give the key as an absolute path or one under ~/.");
873
+ let isFile = false;
874
+ try {
875
+ isFile = node_fs.default.statSync(expandHome(keyPath, homeDir)).isFile();
876
+ } catch {
877
+ isFile = false;
878
+ }
879
+ if (!isFile) throw require_worktreeOperations.invalidRequest(`No key file at ${keyPath}.`, "Check the path, or leave it blank to use ssh-agent.");
880
+ return {
881
+ method: "ssh",
882
+ keyPath
883
+ };
884
+ }
885
+ if (request.method === "https") {
886
+ const host = typeof request.host === "string" ? request.host.trim().toLowerCase() : "";
887
+ if (!HOST_PATTERN.test(host)) throw require_worktreeOperations.invalidRequest("The host is not a host name.", "Use a host like github.com, with an optional :port.");
888
+ const username = typeof request.username === "string" ? request.username.trim() : "";
889
+ if (username === "" || username.length > MAX_USERNAME || hasControl(username)) throw require_worktreeOperations.invalidRequest("The username is empty or not plain text.", "Enter the username the host knows you by.");
890
+ if (request.token !== void 0 && request.token !== "") {
891
+ if (typeof request.token !== "string" || request.token.length > MAX_TOKEN || hasControl(request.token)) throw require_worktreeOperations.invalidRequest("The token is not plain text.", "Paste the token again without line breaks.");
892
+ return {
893
+ method: "https",
894
+ host,
895
+ username,
896
+ token: request.token
897
+ };
898
+ }
899
+ if (current.method === "https" && current.host === host && current.token !== "") return {
900
+ method: "https",
901
+ host,
902
+ username,
903
+ token: current.token
904
+ };
905
+ throw require_worktreeOperations.invalidRequest(current.method === "https" ? "The host changed, so the saved token cannot follow it." : "A token is required.", "Enter the personal access token for this host.");
906
+ }
907
+ throw require_worktreeOperations.invalidRequest("The git auth method is unknown.", "Send method none, ssh or https.");
908
+ }
909
+ /** Tightens a file or folder something left readable by group or others. */
910
+ function tighten(target, mode) {
911
+ try {
912
+ if ((node_fs.default.statSync(target).mode & LOOSE_BITS) !== 0) node_fs.default.chmodSync(target, mode);
913
+ } catch {}
914
+ }
915
+ function createGitAuthStore(homeDir = node_os.default.homedir()) {
916
+ const file = require_worktreeOperations.credentialsFile(homeDir);
917
+ const readFile = () => {
918
+ tighten(file, OWNER_ONLY_FILE);
919
+ const raw = require_worktreeOperations.readJson(file);
920
+ const workspaces = {};
921
+ if (isRecord(raw) && raw.version === 1 && isRecord(raw.workspaces)) for (const [key, value] of Object.entries(raw.workspaces)) {
922
+ const config = parseConfig(value);
923
+ if (config !== void 0) workspaces[key] = config;
924
+ }
925
+ return {
926
+ version: 1,
927
+ workspaces
928
+ };
929
+ };
930
+ const read = (workspaceRoot) => readFile().workspaces[workspaceKey(workspaceRoot)] ?? { method: "none" };
931
+ return {
932
+ read,
933
+ view: (workspaceRoot) => publicView(read(workspaceRoot)),
934
+ save(workspaceRoot, request) {
935
+ const key = workspaceKey(workspaceRoot);
936
+ const current = readFile();
937
+ const next = parseGitAuthSave(request, current.workspaces[key] ?? { method: "none" }, homeDir);
938
+ const root = require_worktreeOperations.doomGitRoot(homeDir);
939
+ node_fs.default.mkdirSync(root, {
940
+ recursive: true,
941
+ mode: OWNER_ONLY_DIRECTORY
942
+ });
943
+ node_fs.default.chmodSync(root, OWNER_ONLY_DIRECTORY);
944
+ const workspaces = { ...current.workspaces };
945
+ if (next.method === "none") delete workspaces[key];
946
+ else workspaces[key] = next;
947
+ require_worktreeOperations.writeJsonAtomic(file, {
948
+ version: 1,
949
+ workspaces
950
+ });
951
+ node_fs.default.chmodSync(file, OWNER_ONLY_FILE);
952
+ return publicView(next);
953
+ }
954
+ };
955
+ }
956
+ /** The transport and host a remote URL names, or undefined for a local path or anything else. */
957
+ function remoteTransport(url) {
958
+ const trimmed = url.trim();
959
+ if (/^[a-z][a-z0-9+.-]*:\/\//iu.test(trimmed)) try {
960
+ const parsed = new URL(trimmed);
961
+ const kind = parsed.protocol === "https:" ? "https" : parsed.protocol === "http:" ? "http" : parsed.protocol === "ssh:" ? "ssh" : void 0;
962
+ return kind === void 0 ? void 0 : {
963
+ kind,
964
+ host: parsed.host.toLowerCase()
965
+ };
966
+ } catch {
967
+ return;
968
+ }
969
+ const scp = /^(?:[^@/]+@)?([^:/]+):(?!\/\/)/u.exec(trimmed);
970
+ if (scp !== null && scp[1] !== void 0 && scp[1].length > 1) return {
971
+ kind: "ssh",
972
+ host: scp[1].toLowerCase()
973
+ };
974
+ }
975
+ /** Quotes one word for the POSIX shell git runs GIT_SSH_COMMAND through. */
976
+ function shellQuote(value) {
977
+ return `'${value.replaceAll("'", "'\\''")}'`;
978
+ }
979
+ /**
980
+ * The environment for one fetch or push against `remoteUrl`.
981
+ *
982
+ * Every call drops tracing (which can log credentials) and turns prompts off,
983
+ * because no terminal is attached. Only when the workspace's method matches the
984
+ * remote does anything secret go in, and then only into this child's env.
985
+ */
986
+ function remoteEnv(config, remoteUrl, baseEnv, homeDir) {
987
+ const env = { ...baseEnv };
988
+ for (const key of Object.keys(env)) if (STRIPPED_ALWAYS.includes(key) || STRIPPED_PREFIXES.some((prefix) => key.startsWith(prefix))) delete env[key];
989
+ env.GIT_TERMINAL_PROMPT = "0";
990
+ env.GCM_INTERACTIVE = "never";
991
+ const transport = remoteTransport(remoteUrl);
992
+ const credentialed = () => {
993
+ for (const key of Object.keys(env)) if (STRIPPED_CREDENTIALED.includes(key) || CONFIG_ENTRY.test(key)) delete env[key];
994
+ env.GIT_ASKPASS = "true";
995
+ };
996
+ if (config.method === "https") {
997
+ if (transport?.kind !== "https" || transport.host !== config.host) return {
998
+ env,
999
+ credentialed: false,
1000
+ mismatch: `This workspace's token is for https://${config.host}, and this remote is not.`
1001
+ };
1002
+ credentialed();
1003
+ env.GIT_CONFIG_COUNT = "3";
1004
+ env.GIT_CONFIG_KEY_0 = "credential.helper";
1005
+ env.GIT_CONFIG_VALUE_0 = "";
1006
+ env.GIT_CONFIG_KEY_1 = `credential.https://${config.host}.helper`;
1007
+ env.GIT_CONFIG_VALUE_1 = ENV_CREDENTIAL_HELPER;
1008
+ env.GIT_CONFIG_KEY_2 = "http.sslVerify";
1009
+ env.GIT_CONFIG_VALUE_2 = "true";
1010
+ env.GIT_ALLOW_PROTOCOL = "https";
1011
+ env.PO_GIT_USERNAME = config.username;
1012
+ env.PO_GIT_TOKEN = config.token;
1013
+ return {
1014
+ env,
1015
+ credentialed: true
1016
+ };
1017
+ }
1018
+ if (config.method === "ssh") {
1019
+ if (transport?.kind !== "ssh") return {
1020
+ env,
1021
+ credentialed: false,
1022
+ mismatch: "This workspace uses SSH, and this remote is not an SSH remote."
1023
+ };
1024
+ credentialed();
1025
+ env.GIT_SSH_COMMAND = [
1026
+ "ssh",
1027
+ ...config.keyPath === void 0 ? [] : [
1028
+ "-i",
1029
+ shellQuote(expandHome(config.keyPath, homeDir)),
1030
+ "-o",
1031
+ "IdentitiesOnly=yes"
1032
+ ],
1033
+ "-o",
1034
+ "BatchMode=yes",
1035
+ "-o",
1036
+ "ConnectTimeout=15"
1037
+ ].join(" ");
1038
+ env.GIT_ALLOW_PROTOCOL = "ssh";
1039
+ return {
1040
+ env,
1041
+ credentialed: true
1042
+ };
1043
+ }
1044
+ return {
1045
+ env,
1046
+ credentialed: false
1047
+ };
1048
+ }
1049
+ //#endregion
1050
+ //#region src/services/newSessionRequest/index.ts
1051
+ const MAX_NAME_LENGTH = 120;
1052
+ /** Trimmed text, undefined when absent or blank, false when not text at all. */
1053
+ function optionalText(value) {
1054
+ if (value === void 0) return void 0;
1055
+ if (typeof value !== "string") return false;
1056
+ const trimmed = value.trim();
1057
+ return trimmed === "" ? void 0 : trimmed;
1058
+ }
1059
+ /**
1060
+ * Validates a new-session dialog request for a worktree session. Every ref is
1061
+ * checked here, before it can reach git as a positional argument.
1062
+ */
1063
+ function newSessionRequest(body) {
1064
+ if (typeof body !== "object" || body === null || Array.isArray(body)) return { error: "A request body is required." };
1065
+ const record = body;
1066
+ const branch = optionalText(record.branch);
1067
+ if (branch === void 0 || branch === false || !require_worktreeOperations.validRefName(branch)) return { error: "A valid branch name is required." };
1068
+ const name = optionalText(record.name);
1069
+ if (name === false || name !== void 0 && name.length > MAX_NAME_LENGTH) return { error: `The session name must be text of up to ${String(MAX_NAME_LENGTH)} characters.` };
1070
+ const named = name === void 0 ? {} : { name };
1071
+ if (record.mode === "existing-branch") {
1072
+ const remote = optionalText(record.remote);
1073
+ if (remote === false || remote !== void 0 && !require_worktreeOperations.validRefName(remote)) return { error: "That remote name is not valid." };
1074
+ return {
1075
+ mode: "existing-branch",
1076
+ branch,
1077
+ ...remote === void 0 ? {} : { remote },
1078
+ ...named
1079
+ };
1080
+ }
1081
+ if (record.mode === "new-branch") {
1082
+ const baseRef = optionalText(record.baseRef);
1083
+ if (baseRef === false || baseRef !== void 0 && !require_worktreeOperations.validRefName(baseRef)) return { error: "That base branch is not valid." };
1084
+ return {
1085
+ mode: "new-branch",
1086
+ branch,
1087
+ ...baseRef === void 0 ? {} : { baseRef },
1088
+ ...named
1089
+ };
1090
+ }
1091
+ return { error: "The mode must be existing-branch or new-branch." };
1092
+ }
1093
+ //#endregion
1094
+ //#region src/extensions/(backend)/api/git/_lib/route.server.ts
1095
+ /**
1096
+ * The worktree surface the cockpit panel calls.
1097
+ *
1098
+ * Hub-scoped because worktrees are a property of a repository, not of one
1099
+ * session: the panel lists and creates them whether or not the session that
1100
+ * made them is focused, and a session-scoped API would disappear with it.
1101
+ *
1102
+ * The panel names a repository by the hub's opaque id and never by path.
1103
+ * `resolveRepository` turns that into an admitted root, so a browser cannot
1104
+ * point this at a directory the cockpit was never given.
1105
+ */
1106
+ function badRequest(message) {
1107
+ return Response.json({ error: message }, { status: 400 });
1108
+ }
1109
+ function failed(error) {
1110
+ if (error instanceof require_worktreeOperations.DoomGitExpectedError) return Response.json({
1111
+ error: error.message,
1112
+ code: error.code
1113
+ }, { status: error.retryable ? 503 : 409 });
1114
+ return Response.json({ error: "The worktree operation failed." }, { status: 500 });
1115
+ }
1116
+ //#endregion
1117
+ //#region src/extensions/(backend)/api/git/route.server.ts
1118
+ var route_server_default = (0, _agimon_ai_po_core_extensionFile.defineRoute)({
1119
+ basePath: "git",
1120
+ start(context) {
1121
+ if (context.directEvents === void 0) throw new Error("Git hub API requires the direct event bus.");
1122
+ const directEvents = context.directEvents;
1123
+ const publishLifecycle = (sessionId, repositoryRoot) => {
1124
+ directEvents.publish(require_worktreeOperations.GIT_WORKTREE_LIFECYCLE_EVENT, sessionId, {
1125
+ version: 1,
1126
+ repositoryRoot
1127
+ });
1128
+ };
1129
+ const git = require_worktreeOperations.createWorktreeGit();
1130
+ const operations = require_worktreeOperations.createWorktreeOperations({
1131
+ git,
1132
+ sessionService: context.sessionService,
1133
+ ...context.homeDirectory === void 0 ? {} : { homeDir: context.homeDirectory }
1134
+ });
1135
+ /**
1136
+ * The new-session dialog's routes: the workspace's branches, and a top-level
1137
+ * worktree session on one. Workspace-mounted only, and always rooted at the
1138
+ * mount's own workspace, so a request can never name another directory.
1139
+ */
1140
+ const newSession = async (request, url) => {
1141
+ const isBranches = request.method === "GET" && url.pathname === "/branches";
1142
+ const isCreate = request.method === "POST" && url.pathname === "/sessions";
1143
+ if (!isBranches && !isCreate) return void 0;
1144
+ const workspaceRoot = context.scope === "workspace" ? context.workspaceRoot : void 0;
1145
+ if (workspaceRoot === void 0) return Response.json({ error: "This route belongs to a workspace." }, { status: 404 });
1146
+ if (isBranches) {
1147
+ const repositoryRoot = await git.repositoryRoot(workspaceRoot);
1148
+ const branches = repositoryRoot === void 0 ? {
1149
+ repository: false,
1150
+ local: [],
1151
+ remote: []
1152
+ } : {
1153
+ repository: true,
1154
+ ...await git.listBranches(repositoryRoot)
1155
+ };
1156
+ return Response.json(branches);
1157
+ }
1158
+ const parsed = newSessionRequest(await request.json().catch(() => void 0));
1159
+ if ("error" in parsed) return badRequest(parsed.error);
1160
+ const record = await operations.spawn({ cwd: workspaceRoot }, {
1161
+ branch: parsed.branch,
1162
+ checkout: parsed.mode === "existing-branch" ? "existing" : "new",
1163
+ ...parsed.mode === "existing-branch" && parsed.remote !== void 0 ? { remote: parsed.remote } : {},
1164
+ ...parsed.mode === "new-branch" && parsed.baseRef !== void 0 ? { baseRef: parsed.baseRef } : {},
1165
+ ...parsed.name === void 0 ? {} : { name: parsed.name }
1166
+ });
1167
+ publishLifecycle(record.sessionId, record.repositoryRoot);
1168
+ const created = {
1169
+ sessionId: record.sessionId,
1170
+ worktreeId: record.id
1171
+ };
1172
+ return Response.json(created, { status: 201 });
1173
+ };
1174
+ /** The admitted root for a request, or a response explaining why not. */
1175
+ const rootOf = (url) => {
1176
+ const repositoryId = url.searchParams.get("repositoryId");
1177
+ if (repositoryId === null || repositoryId === "") return badRequest("A repositoryId is required.");
1178
+ const root = context.resolveRepository?.(repositoryId);
1179
+ if (root === void 0) return badRequest("That repository is not admitted to this cockpit.");
1180
+ return root;
1181
+ };
1182
+ const homeDir = context.homeDirectory ?? node_os.default.homedir();
1183
+ const gitAuth = createGitAuthStore(homeDir);
1184
+ const branchDiff = createBranchDiff();
1185
+ const pendingReviews = /* @__PURE__ */ new Map();
1186
+ /**
1187
+ * A workspace's remote auth. Workspace mounts only, so the step-up gate on
1188
+ * `PUT /api/workspaces/<id>/plugins/git/auth` is the one way to write it.
1189
+ * The view never carries the token.
1190
+ */
1191
+ const auth = async (request, url) => {
1192
+ if (url.pathname !== "/auth") return void 0;
1193
+ if (context.scope !== "workspace") return Response.json({ error: "This route belongs to a workspace." }, { status: 404 });
1194
+ if (request.method !== "GET" && request.method !== "PUT") return Response.json({ error: "Use GET or PUT." }, { status: 405 });
1195
+ const root = rootOf(url);
1196
+ if (root instanceof Response) return root;
1197
+ if (request.method === "GET") return Response.json(gitAuth.view(root));
1198
+ try {
1199
+ return Response.json(gitAuth.save(root, await request.json().catch(() => void 0)));
1200
+ } catch (error) {
1201
+ if (error instanceof require_worktreeOperations.DoomGitExpectedError && error.code === "invalid_request") {
1202
+ const text = error.message.replace(/^\[[a-z_]+\] /u, "").replace("\nRecovery: ", " ");
1203
+ return Response.json({
1204
+ error: text,
1205
+ code: error.code
1206
+ }, { status: 400 });
1207
+ }
1208
+ throw error;
1209
+ }
1210
+ };
1211
+ /**
1212
+ * The session's change against its base. Session mounts only, rooted at the
1213
+ * session's own checkout; a file is served only when it is in the change set
1214
+ * computed for the request. Overlapping requests share an in-flight snapshot.
1215
+ */
1216
+ const review = async (request, url) => {
1217
+ if (url.pathname !== "/review" && url.pathname !== "/review/file") return void 0;
1218
+ const cwd = context.scope === "session" ? context.cwd : void 0;
1219
+ if (cwd === void 0) return Response.json({ error: "This route belongs to a session." }, { status: 404 });
1220
+ if (request.method !== "GET") return Response.json({ error: "Use GET." }, { status: 405 });
1221
+ let recordedBaseRef;
1222
+ try {
1223
+ recordedBaseRef = require_worktreeOperations.createWorktreeRegistry(require_worktreeOperations.registryFile(cwd, homeDir)).list().find((record) => record.sessionId === context.sessionId)?.baseRef;
1224
+ } catch {
1225
+ recordedBaseRef = void 0;
1226
+ }
1227
+ const key = JSON.stringify([cwd, recordedBaseRef ?? null]);
1228
+ let pending = pendingReviews.get(key);
1229
+ if (pending === void 0) {
1230
+ pending = branchDiff.review(cwd, recordedBaseRef === void 0 ? {} : { recordedBaseRef }).finally(() => pendingReviews.delete(key));
1231
+ pendingReviews.set(key, pending);
1232
+ }
1233
+ const result = await pending;
1234
+ if (url.pathname === "/review") return Response.json(result?.summary ?? {
1235
+ repository: false,
1236
+ files: []
1237
+ });
1238
+ const filePath = url.searchParams.get("path") ?? "";
1239
+ if (result === void 0 || !result.summary.files.some((file) => file.path === filePath)) return Response.json({ error: "That file is not part of this review." }, { status: 404 });
1240
+ return Response.json(await branchDiff.fileDiff(result, filePath));
1241
+ };
1242
+ return {
1243
+ async fetch(request) {
1244
+ const url = new URL(request.url);
1245
+ try {
1246
+ const answered = await newSession(request, url) ?? await auth(request, url) ?? await review(request, url);
1247
+ if (answered !== void 0) return answered;
1248
+ } catch (error) {
1249
+ context.onNotice(`git request failed: ${error.name}`);
1250
+ return failed(error);
1251
+ }
1252
+ const root = rootOf(url);
1253
+ if (root instanceof Response) return root;
1254
+ const callerContext = {
1255
+ cwd: root,
1256
+ sessionId: url.searchParams.get("sessionId") ?? ""
1257
+ };
1258
+ if ((request.method === "POST" && url.pathname === "/worktrees" || request.method === "DELETE" && /^\/worktrees\/[^/]+$/u.test(url.pathname)) && callerContext.sessionId === "") return badRequest("A sessionId is required for worktree mutations.");
1259
+ try {
1260
+ if (request.method === "GET" && url.pathname === "/worktrees") return Response.json({ worktrees: await operations.list(callerContext) });
1261
+ if (request.method === "POST" && url.pathname === "/worktrees") {
1262
+ const body = await request.json();
1263
+ if (typeof body.branch !== "string" || body.branch.trim() === "") return badRequest("A branch is required.");
1264
+ const record = await operations.spawn(callerContext, {
1265
+ branch: body.branch,
1266
+ ...body.baseRef === void 0 ? {} : { baseRef: body.baseRef },
1267
+ ...body.name === void 0 ? {} : { name: body.name }
1268
+ });
1269
+ publishLifecycle(callerContext.sessionId, record.repositoryRoot);
1270
+ return Response.json({ worktree: record }, { status: 201 });
1271
+ }
1272
+ const closing = /^\/worktrees\/([^/]+)$/u.exec(url.pathname);
1273
+ if (request.method === "DELETE" && closing) {
1274
+ const record = await operations.close(callerContext, decodeURIComponent(String(closing[1])), url.searchParams.get("force") === "true");
1275
+ publishLifecycle(callerContext.sessionId, record.repositoryRoot);
1276
+ return Response.json({ worktree: record });
1277
+ }
1278
+ return Response.json({ error: "No such worktree route." }, { status: 404 });
1279
+ } catch (error) {
1280
+ context.onNotice(`worktree request failed: ${error.name}`);
1281
+ return failed(error);
1282
+ }
1283
+ },
1284
+ close() {}
1285
+ };
1286
+ }
1287
+ });
1288
+ //#endregion
1289
+ //#region src/services/gitFailure/index.ts
1290
+ const RULES = [
1291
+ {
1292
+ code: "host_key_changed",
1293
+ phrases: ["remote host identification has changed"],
1294
+ message: "The remote's SSH host key changed since this machine last saw it.",
1295
+ recovery: "Check with the host that the change is expected, then update ~/.ssh/known_hosts from a terminal.",
1296
+ retryable: false
1297
+ },
1298
+ {
1299
+ code: "host_key_unknown",
1300
+ phrases: [
1301
+ "host key verification failed",
1302
+ "no ed25519 host key is known",
1303
+ "no rsa host key is known"
1304
+ ],
1305
+ message: "This machine does not trust the remote's SSH host key yet.",
1306
+ recovery: "Run `ssh -T git@<host>` once in a terminal and accept the key, then try again.",
1307
+ retryable: false
1308
+ },
1309
+ {
1310
+ code: "auth_failed",
1311
+ phrases: [
1312
+ "authentication failed",
1313
+ "could not read username",
1314
+ "could not read password",
1315
+ "permission denied (publickey",
1316
+ "returned error: 401",
1317
+ "returned error: 403",
1318
+ "terminal prompts disabled",
1319
+ "load key",
1320
+ "unprotected private key",
1321
+ "invalid format"
1322
+ ],
1323
+ message: "The remote refused the credentials.",
1324
+ recovery: `Check this workspace's git remote auth. ${SETTINGS_HINT}`,
1325
+ retryable: false
1326
+ },
1327
+ {
1328
+ code: "repository_not_found",
1329
+ phrases: ["repository not found", "does not appear to be a git repository"],
1330
+ message: "The remote repository was not found, or these credentials cannot see it.",
1331
+ recovery: `Check the remote URL and that the credentials have access. ${SETTINGS_HINT}`,
1332
+ retryable: false
1333
+ },
1334
+ {
1335
+ code: "remote_unreachable",
1336
+ phrases: [
1337
+ "could not resolve host",
1338
+ "connection refused",
1339
+ "connection timed out",
1340
+ "operation timed out",
1341
+ "unable to access",
1342
+ "network is unreachable"
1343
+ ],
1344
+ message: "The remote could not be reached.",
1345
+ recovery: "Check the network and the remote URL, then try again.",
1346
+ retryable: true
1347
+ },
1348
+ {
1349
+ code: "push_lease_rejected",
1350
+ phrases: ["(stale info)"],
1351
+ message: "The remote branch moved since your last fetch, so the force push was refused.",
1352
+ recovery: "Pull to see the new commits, then decide whether to push again.",
1353
+ retryable: false
1354
+ },
1355
+ {
1356
+ code: "push_needs_force",
1357
+ phrases: ["(non-fast-forward)", "(fetch first)"],
1358
+ message: "The remote branch has commits yours replaced, usually after a rebase.",
1359
+ recovery: "Confirm a force push with lease, or pull first to keep the remote commits.",
1360
+ retryable: false
1361
+ },
1362
+ {
1363
+ code: "worktree_dirty",
1364
+ phrases: [
1365
+ "unstaged changes",
1366
+ "uncommitted changes",
1367
+ "would be overwritten",
1368
+ "your index contains uncommitted changes"
1369
+ ],
1370
+ message: "There are uncommitted changes in the way.",
1371
+ recovery: "Commit or stash your changes first.",
1372
+ retryable: false
1373
+ }
1374
+ ];
1375
+ function classifyGitFailure(operation, result, mismatch) {
1376
+ if (result.timedOut) return new require_worktreeOperations.DoomGitExpectedError("git_timeout", `git ${operation} took too long and was stopped.`, true, "Check the network, then try again.");
1377
+ const text = `${result.stderr}\n${result.stdout}`.toLowerCase();
1378
+ const rule = RULES.find((candidate) => candidate.phrases.some((phrase) => text.includes(phrase)));
1379
+ if (rule === void 0) return new require_worktreeOperations.DoomGitExpectedError("git_failed", `git ${operation} failed (exit ${String(result.code)}).`, false, "Run the same command in a terminal in this checkout to see what git says.");
1380
+ const recovery = rule.code === "auth_failed" && mismatch !== void 0 ? `${mismatch} ${rule.recovery}` : rule.recovery;
1381
+ return new require_worktreeOperations.DoomGitExpectedError(rule.code, rule.message, rule.retryable, recovery);
1382
+ }
1383
+ //#endregion
1384
+ //#region src/services/gitSync/index.ts
1385
+ /**
1386
+ * Manual pull, push and rebase for a session's checkout.
1387
+ *
1388
+ * DESIGN PATTERNS:
1389
+ * - The workspace's auth goes into the environment of the one fetch or push
1390
+ * that needs it, built fresh per call. Rebase itself runs locally and never
1391
+ * sees a credential.
1392
+ * - Fetch and rebase are separate commands, so a hung network can never leave a
1393
+ * rebase half applied.
1394
+ * - A conflicted rebase is not an error. It is left paused for the reader to
1395
+ * hand to the agent or abort, and git's own state reports it from then on.
1396
+ * - Pull and rebase refuse uncommitted tracked changes rather than stash them.
1397
+ * Untracked files only block when git says they would be overwritten.
1398
+ * - Push never forces on its own. A rejection comes back as `push_needs_force`,
1399
+ * and only a confirmed retry sends `--force-with-lease --force-if-includes`,
1400
+ * which git refuses if the remote holds commits this branch never integrated.
1401
+ */
1402
+ const DONE = { outcome: "done" };
1403
+ function expected(code, message, recovery) {
1404
+ return new require_worktreeOperations.DoomGitExpectedError(code, message, false, recovery);
1405
+ }
1406
+ async function readLine(cwd, args) {
1407
+ const result = await require_worktreeOperations.runGit(cwd, args, { timeout: require_worktreeOperations.READ_TIMEOUT_MS });
1408
+ const out = result.stdout.trim();
1409
+ return result.code === 0 && out !== "" ? out : void 0;
1410
+ }
1411
+ async function toplevel(cwd) {
1412
+ const root = await readLine(cwd, ["rev-parse", "--show-toplevel"]);
1413
+ if (root === void 0) throw expected("not_a_repository", "This session is not in a git checkout.", "Open the session in a git repository.");
1414
+ return root;
1415
+ }
1416
+ async function rebasing(root) {
1417
+ for (const name of ["rebase-merge", "rebase-apply"]) {
1418
+ const gitPath = await readLine(root, [
1419
+ "rev-parse",
1420
+ "--git-path",
1421
+ name
1422
+ ]);
1423
+ if (gitPath !== void 0 && node_fs.default.existsSync(node_path.default.resolve(root, gitPath))) return true;
1424
+ }
1425
+ return false;
1426
+ }
1427
+ async function conflicts(root) {
1428
+ return (await require_worktreeOperations.runGit(root, [
1429
+ "diff",
1430
+ "--name-only",
1431
+ "--diff-filter=U",
1432
+ "-z"
1433
+ ], { timeout: require_worktreeOperations.READ_TIMEOUT_MS })).stdout.split("\0").filter((file) => file !== "");
1434
+ }
1435
+ async function currentBranch(root) {
1436
+ const branch = await readLine(root, [
1437
+ "symbolic-ref",
1438
+ "--quiet",
1439
+ "--short",
1440
+ "HEAD"
1441
+ ]);
1442
+ if (branch === void 0) throw expected("detached_head", "HEAD is detached, so there is no branch to sync.", "Check out a branch first.");
1443
+ return branch;
1444
+ }
1445
+ async function guard(root, needsCleanTree) {
1446
+ if (await rebasing(root)) throw expected("rebase_in_progress", "A rebase is paused in this checkout.", "Ask the agent to resolve it, or abort the rebase.");
1447
+ const branch = await currentBranch(root);
1448
+ if (needsCleanTree) {
1449
+ if (await readLine(root, [
1450
+ "status",
1451
+ "--porcelain",
1452
+ "--untracked-files=no"
1453
+ ]) !== void 0) throw expected("worktree_dirty", "There are uncommitted changes.", "Commit or stash your changes first.");
1454
+ }
1455
+ return branch;
1456
+ }
1457
+ function createGitSync(options) {
1458
+ const baseEnv = options.baseEnv ?? process.env;
1459
+ /** One credentialed network command against `remote`, classified on failure. */
1460
+ const network = async (root, context, remote, operation, args, urlArgs = []) => {
1461
+ const url = await readLine(root, [
1462
+ "remote",
1463
+ "get-url",
1464
+ ...urlArgs,
1465
+ remote
1466
+ ]);
1467
+ if (url === void 0) throw expected("no_remote", `There is no remote named ${remote}.`, "Add the remote in a terminal first.");
1468
+ const prepared = remoteEnv(context.auth, url, baseEnv, options.homeDir);
1469
+ const result = await require_worktreeOperations.runGit(root, args, {
1470
+ timeout: require_worktreeOperations.NETWORK_TIMEOUT_MS,
1471
+ env: prepared.env
1472
+ });
1473
+ if (result.code !== 0) throw classifyGitFailure(operation, result, prepared.mismatch);
1474
+ };
1475
+ /** Rebases onto `onto`; a stop on conflicts leaves the rebase paused and says so. */
1476
+ const rebaseOnto = async (root, onto) => {
1477
+ const result = await require_worktreeOperations.runGit(root, ["rebase", onto], {
1478
+ timeout: require_worktreeOperations.REBASE_TIMEOUT_MS,
1479
+ env: {
1480
+ ...baseEnv,
1481
+ GIT_EDITOR: "true",
1482
+ GIT_TERMINAL_PROMPT: "0"
1483
+ }
1484
+ });
1485
+ if (result.code === 0) return DONE;
1486
+ if (await rebasing(root)) return {
1487
+ outcome: "paused",
1488
+ conflicts: await conflicts(root)
1489
+ };
1490
+ throw classifyGitFailure("rebase", result);
1491
+ };
1492
+ /** The remote a ref like `origin/main` or `refs/remotes/origin/main` belongs to. */
1493
+ const remoteOfRef = async (root, ref) => {
1494
+ const full = await readLine(root, [
1495
+ "rev-parse",
1496
+ "--symbolic-full-name",
1497
+ ref
1498
+ ]);
1499
+ if (full === void 0 || !full.startsWith("refs/remotes/")) return void 0;
1500
+ const rest = full.slice(13);
1501
+ const cut = rest.indexOf("/");
1502
+ return cut > 0 ? rest.slice(0, cut) : void 0;
1503
+ };
1504
+ return {
1505
+ async pull(context) {
1506
+ const root = await toplevel(context.cwd);
1507
+ const branch = await guard(root, true);
1508
+ const remote = await readLine(root, ["config", `branch.${branch}.remote`]);
1509
+ const upstream = await readLine(root, [
1510
+ "rev-parse",
1511
+ "--abbrev-ref",
1512
+ "--symbolic-full-name",
1513
+ `${branch}@{upstream}`
1514
+ ]);
1515
+ if (remote === void 0 || upstream === void 0) throw expected("no_upstream", `${branch} has no upstream to pull from.`, "Push once to set an upstream.");
1516
+ if (remote !== ".") await network(root, context, remote, "fetch", [
1517
+ "fetch",
1518
+ "--prune",
1519
+ remote
1520
+ ]);
1521
+ return rebaseOnto(root, upstream);
1522
+ },
1523
+ async push(context, force) {
1524
+ const root = await toplevel(context.cwd);
1525
+ const branch = await guard(root, false);
1526
+ const configuredRemote = await readLine(root, ["config", `branch.${branch}.remote`]);
1527
+ const merge = await readLine(root, ["config", `branch.${branch}.merge`]);
1528
+ let remote = configuredRemote;
1529
+ let target = merge;
1530
+ const args = ["push", "--porcelain"];
1531
+ if (remote === void 0 || target === void 0 || remote === ".") {
1532
+ const remotes = (await readLine(root, ["remote"]) ?? "").split("\n").filter((name) => name !== "");
1533
+ remote = remotes.includes("origin") ? "origin" : remotes.length === 1 ? remotes[0] : void 0;
1534
+ if (remote === void 0) throw expected("no_remote", "There is no remote to push to.", remotes.length === 0 ? "Add a remote in a terminal first." : "Push once from a terminal to choose the remote.");
1535
+ target = `refs/heads/${branch}`;
1536
+ args.push("--set-upstream");
1537
+ }
1538
+ if (force) args.push(`--force-with-lease=${target}`, "--force-if-includes");
1539
+ args.push(remote, `HEAD:${target}`);
1540
+ await network(root, context, remote, "push", args, ["--push"]);
1541
+ return DONE;
1542
+ },
1543
+ async rebase(context) {
1544
+ const root = await toplevel(context.cwd);
1545
+ const base = await resolveBaseRef(root, await guard(root, true), context.recordedBaseRef);
1546
+ if (base === void 0) throw expected("no_base", "No base branch to rebase onto.", "Fetch the remote, or create the worktree from a base.");
1547
+ const remote = await remoteOfRef(root, base);
1548
+ if (remote !== void 0) await network(root, context, remote, "fetch", [
1549
+ "fetch",
1550
+ "--prune",
1551
+ remote
1552
+ ]);
1553
+ return rebaseOnto(root, base);
1554
+ },
1555
+ async abortRebase(context) {
1556
+ const root = await toplevel(context.cwd);
1557
+ if (!await rebasing(root)) return DONE;
1558
+ const result = await require_worktreeOperations.runGit(root, ["rebase", "--abort"], { timeout: require_worktreeOperations.REBASE_TIMEOUT_MS });
1559
+ if (result.code !== 0) throw classifyGitFailure("rebase --abort", result);
1560
+ return DONE;
1561
+ }
1562
+ };
1563
+ }
1564
+ //#endregion
1565
+ //#region src/types/gitReview.ts
1566
+ /**
1567
+ * The cockpit's view of what this session changed against its base: the counts
1568
+ * the '# diff' group shows, what the review tab lists and draws, and the manual
1569
+ * sync that sits beside them.
1570
+ *
1571
+ * DESIGN PATTERNS:
1572
+ * - Declarations only, shared by the hub and the page.
1573
+ * - Its own channel, apart from worktrees. Worktrees are repository
1574
+ * management; the diff is this session's code change.
1575
+ * - A diff row carries one line number whose file follows from its marker: the
1576
+ * new file's for context and additions, the old file's for removals. It is
1577
+ * the same shape the shared DiffView draws.
1578
+ *
1579
+ * AVOID:
1580
+ * - Behaviour or node builtins. Browser code imports this file.
1581
+ */
1582
+ /** The session channel carrying this checkout's changes and the sync around them. */
1583
+ const GIT_CHANGES_TYPE = "git_changes";
1584
+ //#endregion
1585
+ //#region src/extensions/(backend)/channel/_lib/git-changes.server.ts
1586
+ /**
1587
+ * The '# diff' data channel: this session's change against its base, and the
1588
+ * manual sync commands the review tab sends back through it.
1589
+ *
1590
+ * DESIGN PATTERNS:
1591
+ * - Apart from the worktrees channel. Worktrees are repository management;
1592
+ * this is the session's own code change.
1593
+ * - Push, not poll. The session facet publishes a "changed" event after every
1594
+ * tool call and settled turn; recounts are coalesced to one in flight plus
1595
+ * one rerun, so a burst of tool calls costs at most two.
1596
+ * - One sync command per session. A rebase and a push must never overlap.
1597
+ * - Auth is read fresh for each command from the session's workspace, and goes
1598
+ * only into that command's git child.
1599
+ *
1600
+ * AVOID:
1601
+ * - Echoing git output. Failures go through classifyGitFailure, which matches
1602
+ * stderr and drops it, because a remote URL in it can carry a token.
1603
+ */
1604
+ function payloadOf$1(state) {
1605
+ return {
1606
+ ...state.changes === void 0 ? {} : { changes: state.changes },
1607
+ ...state.pending === void 0 ? {} : { pending: state.pending },
1608
+ ...state.error === void 0 ? {} : { error: state.error },
1609
+ ...state.errorTarget === void 0 ? {} : { errorTarget: state.errorTarget }
1610
+ };
1611
+ }
1612
+ /** A command the page sent, or undefined when the frame was not one. */
1613
+ function parseChangesCommand(payload) {
1614
+ if (typeof payload !== "object" || payload === null) return void 0;
1615
+ const value = payload;
1616
+ if (value.action === "pull" || value.action === "rebase" || value.action === "abort-rebase") return { action: value.action };
1617
+ if (value.action === "push") return value.force === true ? {
1618
+ action: "push",
1619
+ force: true
1620
+ } : { action: "push" };
1621
+ }
1622
+ const PENDING = {
1623
+ pull: "pulling…",
1624
+ push: "pushing…",
1625
+ rebase: "rebasing…",
1626
+ "abort-rebase": "aborting the rebase…"
1627
+ };
1628
+ /** The error target a failure maps to: a rejected plain push asks for a confirmed lease force. */
1629
+ function errorTargetFor$1(command, error) {
1630
+ if (command.action !== "push") return { action: command.action };
1631
+ return error instanceof require_worktreeOperations.DoomGitExpectedError && error.code === "push_needs_force" && command.force !== true ? {
1632
+ action: "push",
1633
+ forceRequired: true
1634
+ } : { action: "push" };
1635
+ }
1636
+ function failureText$1(error) {
1637
+ if (error instanceof require_worktreeOperations.DoomGitExpectedError) return error.message.replace(/^\[[a-z_]+\] /u, "").replace("\nRecovery: ", " ");
1638
+ return "The git command failed.";
1639
+ }
1640
+ function createGitChangesChannel(options = {}) {
1641
+ let receiveCommand;
1642
+ return {
1643
+ frameType: GIT_CHANGES_TYPE,
1644
+ start(host) {
1645
+ const homeDir = options.homeDir ?? node_os.default.homedir();
1646
+ const diff = options.diff ?? createBranchDiff();
1647
+ const sync = options.sync ?? createGitSync({ homeDir });
1648
+ const auth = options.auth ?? createGitAuthStore(homeDir);
1649
+ const latest = /* @__PURE__ */ new Map();
1650
+ const subscriptions = /* @__PURE__ */ new Map();
1651
+ const scopes = /* @__PURE__ */ new Map();
1652
+ const inFlight = /* @__PURE__ */ new Set();
1653
+ const rerun = /* @__PURE__ */ new Set();
1654
+ const busy = /* @__PURE__ */ new Set();
1655
+ /** The base the session's own worktree was created from, when it has one. */
1656
+ const recordedBaseRef = (scope) => {
1657
+ try {
1658
+ return require_worktreeOperations.createWorktreeRegistry(require_worktreeOperations.registryFile(scope.cwd, homeDir)).list().find((record) => record.sessionId === scope.sessionId)?.baseRef;
1659
+ } catch {
1660
+ return;
1661
+ }
1662
+ };
1663
+ /** Republishes a session, skipping a frame that would say nothing new. */
1664
+ const publish = (sessionId, next) => {
1665
+ const previous = latest.get(sessionId);
1666
+ latest.set(sessionId, next);
1667
+ if (previous !== void 0 && JSON.stringify(payloadOf$1(previous)) === JSON.stringify(payloadOf$1(next))) return;
1668
+ host.publish(sessionId, payloadOf$1(next));
1669
+ };
1670
+ const update = (sessionId, patch) => {
1671
+ const previous = latest.get(sessionId) ?? {
1672
+ changes: void 0,
1673
+ pending: void 0,
1674
+ error: void 0,
1675
+ errorTarget: void 0
1676
+ };
1677
+ publish(sessionId, {
1678
+ ...previous,
1679
+ ...patch
1680
+ });
1681
+ };
1682
+ /** Recounts one session; a request while one runs schedules exactly one more. */
1683
+ const refresh = (sessionId) => {
1684
+ const scope = scopes.get(sessionId);
1685
+ if (scope === void 0) return;
1686
+ if (inFlight.has(sessionId)) {
1687
+ rerun.add(sessionId);
1688
+ return;
1689
+ }
1690
+ inFlight.add(sessionId);
1691
+ const base = recordedBaseRef(scope);
1692
+ diff.review(scope.cwd, base === void 0 ? {} : { recordedBaseRef: base }).then((review) => {
1693
+ if (scopes.has(sessionId)) update(sessionId, { changes: review?.changes });
1694
+ }).catch((error) => {
1695
+ host.onNotice(`git diff refresh failed for ${sessionId} (${error instanceof Error ? error.name : "unknown"})`);
1696
+ }).finally(() => {
1697
+ inFlight.delete(sessionId);
1698
+ if (rerun.delete(sessionId)) refresh(sessionId);
1699
+ });
1700
+ };
1701
+ const run = (scope, command) => {
1702
+ const workspaceRoot = scope.sessionContext?.workspaceRoot ?? scope.cwd;
1703
+ const base = recordedBaseRef(scope);
1704
+ const context = {
1705
+ cwd: scope.cwd,
1706
+ auth: auth.read(workspaceRoot),
1707
+ ...base === void 0 ? {} : { recordedBaseRef: base }
1708
+ };
1709
+ if (command.action === "pull") return sync.pull(context);
1710
+ if (command.action === "push") return sync.push(context, command.force === true);
1711
+ if (command.action === "rebase") return sync.rebase(context);
1712
+ return sync.abortRebase(context);
1713
+ };
1714
+ const source = {
1715
+ payloadFor(scope) {
1716
+ const state = latest.get(scope.sessionId);
1717
+ return state === void 0 ? void 0 : payloadOf$1(state);
1718
+ },
1719
+ sessionAdded(scope) {
1720
+ scopes.set(scope.sessionId, scope);
1721
+ refresh(scope.sessionId);
1722
+ const unsubscribe = host.directEvents.subscribe(require_worktreeOperations.GIT_SESSION_CHANGED_EVENT, scope.sessionId, (payload) => {
1723
+ if (require_worktreeOperations.isSessionChangedEvent(payload)) refresh(scope.sessionId);
1724
+ });
1725
+ subscriptions.set(scope.sessionId, unsubscribe);
1726
+ },
1727
+ sessionRemoved(sessionId) {
1728
+ subscriptions.get(sessionId)?.();
1729
+ subscriptions.delete(sessionId);
1730
+ scopes.delete(sessionId);
1731
+ latest.delete(sessionId);
1732
+ rerun.delete(sessionId);
1733
+ busy.delete(sessionId);
1734
+ },
1735
+ close() {
1736
+ for (const unsubscribe of subscriptions.values()) unsubscribe();
1737
+ subscriptions.clear();
1738
+ scopes.clear();
1739
+ latest.clear();
1740
+ rerun.clear();
1741
+ busy.clear();
1742
+ }
1743
+ };
1744
+ receiveCommand = (scope, payload) => {
1745
+ const command = parseChangesCommand(payload);
1746
+ if (command === void 0 || !scopes.has(scope.sessionId)) return;
1747
+ if (busy.has(scope.sessionId)) return;
1748
+ busy.add(scope.sessionId);
1749
+ update(scope.sessionId, {
1750
+ pending: PENDING[command.action],
1751
+ error: void 0,
1752
+ errorTarget: void 0
1753
+ });
1754
+ run(scope, command).then(() => update(scope.sessionId, { pending: void 0 })).catch((error) => {
1755
+ host.onNotice(`git ${command.action} failed for ${scope.sessionId} (${error instanceof require_worktreeOperations.DoomGitExpectedError ? error.code : "unexpected"})`);
1756
+ update(scope.sessionId, {
1757
+ pending: void 0,
1758
+ error: failureText$1(error),
1759
+ errorTarget: errorTargetFor$1(command, error)
1760
+ });
1761
+ }).finally(() => {
1762
+ busy.delete(scope.sessionId);
1763
+ refresh(scope.sessionId);
1764
+ });
1765
+ };
1766
+ return source;
1767
+ },
1768
+ receive(scope, payload) {
1769
+ receiveCommand?.(scope, payload);
1770
+ }
1771
+ };
1772
+ }
1773
+ //#endregion
1774
+ //#region src/extensions/(backend)/channel/git-changes.server.ts
1775
+ var git_changes_server_default = (0, _agimon_ai_po_core_extensionFile.defineChannel)(createGitChangesChannel);
1776
+ //#endregion
1777
+ //#region src/types/webWorktrees.ts
1778
+ /**
1779
+ * The cockpit's view of this session's worktrees.
1780
+ *
1781
+ * DESIGN PATTERNS:
1782
+ * - Declarations only. The channel frame type is a constant so the hub and the
1783
+ * page cannot drift apart on a string.
1784
+ * - A view is the subset of a record the dock needs, not the record. The
1785
+ * registry may hold paths and provenance the page has no business rendering.
1786
+ *
1787
+ * AVOID:
1788
+ * - Adding behaviour here. This file is imported by browser code, so it must
1789
+ * stay free of node builtins.
1790
+ */
1791
+ /** The data channel carrying one session's worktrees to the page. */
1792
+ const GIT_WORKTREES_TYPE = "git_worktrees";
1793
+ //#endregion
1794
+ //#region src/extensions/(backend)/channel/_lib/git-worktrees.server.ts
1795
+ function payloadOf(state) {
1796
+ return {
1797
+ worktrees: state.worktrees,
1798
+ ...state.pending === void 0 ? {} : { pending: state.pending },
1799
+ ...state.error === void 0 ? {} : { error: state.error },
1800
+ ...state.errorTarget === void 0 ? {} : { errorTarget: state.errorTarget }
1801
+ };
1802
+ }
1803
+ /** Worktrees visible to this session, using the hub's live session table for ownership. */
1804
+ function viewsFor(scope, deps) {
1805
+ try {
1806
+ return require_worktreeOperations.createWorktreeRegistry(require_worktreeOperations.registryFile(scope.cwd, deps.homeDir)).list().flatMap((record) => {
1807
+ if (record.parentSessionId === void 0 && record.sessionId !== scope.sessionId) return [];
1808
+ const owned = record.parentSessionId === void 0 || record.parentSessionId === scope.sessionId;
1809
+ const unowned = !owned && !deps.sessionService.isLive(record.parentSessionId);
1810
+ if (!owned && !unowned) return [];
1811
+ return [{
1812
+ id: record.id,
1813
+ branch: record.branch,
1814
+ path: record.path,
1815
+ sessionId: record.status === "orphaned" ? null : record.sessionId,
1816
+ orphaned: record.status === "orphaned",
1817
+ unowned
1818
+ }];
1819
+ });
1820
+ } catch {
1821
+ return [];
1822
+ }
1823
+ }
1824
+ function sameList(left, right) {
1825
+ if (left.length !== right.length) return false;
1826
+ return left.every((entry, index) => {
1827
+ const other = right[index];
1828
+ return other !== void 0 && entry.id === other.id && entry.branch === other.branch && entry.sessionId === other.sessionId && entry.orphaned === other.orphaned && entry.unowned === other.unowned;
1829
+ });
1830
+ }
1831
+ function errorTargetFor(command) {
1832
+ return command.action === "create" ? { action: "create" } : {
1833
+ action: "close",
1834
+ id: command.id
1835
+ };
1836
+ }
1837
+ /** A command the page sent, or undefined when the frame was not one. */
1838
+ function parseCommand(payload) {
1839
+ if (typeof payload !== "object" || payload === null) return void 0;
1840
+ const value = payload;
1841
+ if (value.action === "create") {
1842
+ if (typeof value.branch !== "string" || value.branch.trim() === "") return void 0;
1843
+ if (value.reservationId !== void 0 && (typeof value.reservationId !== "string" || !/^[A-Za-z0-9_-]+$/u.test(value.reservationId))) return void 0;
1844
+ return {
1845
+ action: "create",
1846
+ branch: value.branch.trim(),
1847
+ ...typeof value.baseRef === "string" && value.baseRef.trim() !== "" ? { baseRef: value.baseRef.trim() } : {},
1848
+ ...typeof value.reservationId === "string" ? { reservationId: value.reservationId } : {}
1849
+ };
1850
+ }
1851
+ if (value.action === "close") {
1852
+ if (typeof value.id !== "string" || value.id === "") return void 0;
1853
+ return {
1854
+ action: "close",
1855
+ id: value.id,
1856
+ ...value.force === true ? { force: true } : {}
1857
+ };
1858
+ }
1859
+ }
1860
+ /**
1861
+ * What the reader is told when a command fails.
1862
+ *
1863
+ * An expected failure explains itself. Anything else is summarised, never
1864
+ * echoed: a git error can carry a remote URL with a token in it.
1865
+ */
1866
+ function failureText(error) {
1867
+ return error instanceof require_worktreeOperations.DoomGitExpectedError ? error.message : "The worktree operation failed.";
1868
+ }
1869
+ /** Starts one direct-event subscription per managed session. */
1870
+ function createWorktreesChannel(options = {}) {
1871
+ let receiveCommand;
1872
+ return {
1873
+ frameType: GIT_WORKTREES_TYPE,
1874
+ start(host) {
1875
+ const sessionService = host.sessionService;
1876
+ if (sessionService === void 0) throw new require_worktreeOperations.HubUnavailableError("The cockpit session service is unavailable. Start the cockpit and try again.");
1877
+ const worktrees = options.operations ?? require_worktreeOperations.createWorktreeOperations({
1878
+ git: require_worktreeOperations.createWorktreeGit(),
1879
+ sessionService
1880
+ });
1881
+ /** Announces a successful mutation to this session's lifecycle subscribers. */
1882
+ const publishLifecycle = (scope, repositoryRoot) => {
1883
+ host.directEvents.publish(require_worktreeOperations.GIT_WORKTREE_LIFECYCLE_EVENT, scope.sessionId, {
1884
+ version: 1,
1885
+ repositoryRoot
1886
+ });
1887
+ };
1888
+ const unregisterWorktreeProvisioner = sessionService.registerReservedWorktreeProvisioner?.(async ({ reservationId, parentSessionId, signal }) => {
1889
+ const parent = host.sessions().find((scope) => scope.sessionId === parentSessionId);
1890
+ if (parent === void 0) throw new Error("The conversation parent session is unavailable.");
1891
+ const branch = `po/conversation-${reservationId.slice(0, 12)}`;
1892
+ const record = await worktrees.spawn({
1893
+ cwd: parent.cwd,
1894
+ sessionId: parent.sessionId
1895
+ }, {
1896
+ branch,
1897
+ name: `conversation ${reservationId.slice(0, 8)}`,
1898
+ reservationId
1899
+ }, { signal });
1900
+ const reservations = sessionService.reservations;
1901
+ if (reservations === void 0) throw new Error("Conversation reservations are unavailable.");
1902
+ const session = await reservations.complete(reservationId, parentSessionId);
1903
+ publishLifecycle(parent, record.repositoryRoot);
1904
+ return session;
1905
+ });
1906
+ const views = {
1907
+ homeDir: options.homeDir,
1908
+ sessionService
1909
+ };
1910
+ const latest = /* @__PURE__ */ new Map();
1911
+ const subscriptions = /* @__PURE__ */ new Map();
1912
+ const busy = /* @__PURE__ */ new Set();
1913
+ const generations = /* @__PURE__ */ new Map();
1914
+ const newGeneration = (sessionId) => {
1915
+ const generation = Symbol(sessionId);
1916
+ generations.set(sessionId, generation);
1917
+ return generation;
1918
+ };
1919
+ /** Republishes a session, skipping a frame that would say nothing new. */
1920
+ const publish = (scope, force) => {
1921
+ const previous = latest.get(scope.sessionId);
1922
+ const next = {
1923
+ worktrees: viewsFor(scope, views),
1924
+ pending: previous?.pending,
1925
+ error: previous?.error,
1926
+ errorTarget: previous?.errorTarget
1927
+ };
1928
+ if (!force && previous !== void 0 && sameList(previous.worktrees, next.worktrees) && previous.pending === next.pending && previous.error === next.error && previous.errorTarget === next.errorTarget) return;
1929
+ latest.set(scope.sessionId, next);
1930
+ host.publish(scope.sessionId, payloadOf(next));
1931
+ };
1932
+ const mark = (scope, pending, error, errorTarget) => {
1933
+ const previous = latest.get(scope.sessionId);
1934
+ latest.set(scope.sessionId, {
1935
+ worktrees: previous?.worktrees ?? viewsFor(scope, views),
1936
+ pending,
1937
+ error,
1938
+ errorTarget
1939
+ });
1940
+ publish(scope, true);
1941
+ };
1942
+ const run = async (scope, command, generation) => {
1943
+ const context = {
1944
+ cwd: scope.cwd,
1945
+ sessionId: scope.sessionId
1946
+ };
1947
+ const markCurrent = (pending, error, errorTarget) => {
1948
+ if (generations.get(scope.sessionId) !== generation) return;
1949
+ mark(scope, pending, error, errorTarget);
1950
+ };
1951
+ if (command.action === "create") {
1952
+ markCurrent(`creating ${command.branch}…`, void 0, void 0);
1953
+ const record = await worktrees.spawn(context, {
1954
+ branch: command.branch,
1955
+ ...command.baseRef === void 0 ? {} : { baseRef: command.baseRef },
1956
+ ...command.reservationId === void 0 ? {} : { reservationId: command.reservationId }
1957
+ }, { onProgress: (label) => markCurrent(label, void 0, void 0) });
1958
+ if (generations.get(scope.sessionId) !== generation) return;
1959
+ publishLifecycle(scope, record.repositoryRoot);
1960
+ return;
1961
+ }
1962
+ markCurrent(`closing ${command.id}…`, void 0, void 0);
1963
+ const record = await worktrees.close(context, command.id, command.force ?? false);
1964
+ if (generations.get(scope.sessionId) !== generation) return;
1965
+ publishLifecycle(scope, record.repositoryRoot);
1966
+ };
1967
+ const source = {
1968
+ payloadFor(scope) {
1969
+ const state = latest.get(scope.sessionId);
1970
+ return state === void 0 ? void 0 : payloadOf(state);
1971
+ },
1972
+ sessionAdded(scope) {
1973
+ newGeneration(scope.sessionId);
1974
+ publish(scope, false);
1975
+ const unsubscribe = host.directEvents.subscribe(require_worktreeOperations.GIT_WORKTREE_LIFECYCLE_EVENT, scope.sessionId, (payload) => {
1976
+ if (!require_worktreeOperations.isWorktreeLifecycleEvent(payload)) return;
1977
+ let sameRepository = false;
1978
+ try {
1979
+ sameRepository = require_worktreeOperations.registryFile(scope.cwd, options.homeDir) === require_worktreeOperations.registryFile(payload.repositoryRoot, options.homeDir);
1980
+ } catch {
1981
+ sameRepository = false;
1982
+ }
1983
+ if (sameRepository) publish(scope, false);
1984
+ });
1985
+ subscriptions.set(scope.sessionId, unsubscribe);
1986
+ },
1987
+ sessionRemoved(sessionId) {
1988
+ newGeneration(sessionId);
1989
+ subscriptions.get(sessionId)?.();
1990
+ subscriptions.delete(sessionId);
1991
+ latest.delete(sessionId);
1992
+ busy.delete(sessionId);
1993
+ },
1994
+ close() {
1995
+ unregisterWorktreeProvisioner?.();
1996
+ for (const unsubscribe of subscriptions.values()) unsubscribe();
1997
+ subscriptions.clear();
1998
+ latest.clear();
1999
+ busy.clear();
2000
+ generations.clear();
2001
+ }
2002
+ };
2003
+ receiveCommand = (scope, payload) => {
2004
+ const command = parseCommand(payload);
2005
+ if (command === void 0) return;
2006
+ if (busy.has(scope.sessionId)) return;
2007
+ const generation = generations.get(scope.sessionId);
2008
+ if (generation === void 0) return;
2009
+ busy.add(scope.sessionId);
2010
+ run(scope, command, generation).then(() => {
2011
+ if (generations.get(scope.sessionId) !== generation) return;
2012
+ mark(scope, void 0, void 0, void 0);
2013
+ }).catch((error) => {
2014
+ if (generations.get(scope.sessionId) !== generation) return;
2015
+ host.onNotice(`worktree command failed for ${scope.sessionId} (${failureText(error)})`);
2016
+ mark(scope, void 0, failureText(error), errorTargetFor(command));
2017
+ }).finally(() => {
2018
+ if (generations.get(scope.sessionId) === generation) busy.delete(scope.sessionId);
2019
+ });
2020
+ };
2021
+ return source;
2022
+ },
2023
+ receive(scope, payload) {
2024
+ receiveCommand?.(scope, payload);
2025
+ }
2026
+ };
2027
+ }
2028
+ //#endregion
2029
+ //#region src/extensions/(backend)/channel/git-worktrees.server.ts
2030
+ var git_worktrees_server_default = (0, _agimon_ai_po_core_extensionFile.defineChannel)(createWorktreesChannel);
2031
+ //#endregion
2032
+ //#region src/extensions/workspaces/sessions/(backend)/hook/agent-settled.server.ts
2033
+ var agent_settled_server_default = (0, _agimon_ai_po_core_extensionFile.defineHook)((context) => context.root.hooks[1]);
2034
+ //#endregion
2035
+ //#region src/extensions/workspaces/sessions/(backend)/hook/tool-execution-end.server.ts
2036
+ var tool_execution_end_server_default = (0, _agimon_ai_po_core_extensionFile.defineHook)((context) => context.root.hooks[0]);
2037
+ //#endregion
2038
+ //#region src/extensions/workspaces/sessions/(backend)/resource/_lib/poGit.server.ts
2039
+ var poGit_server_default$1 = (context) => context.root.resources[0];
2040
+ //#endregion
2041
+ //#region src/extensions/workspaces/sessions/(backend)/resource/poGit.server.ts
2042
+ var poGit_server_default = (0, _agimon_ai_po_core_extensionFile.defineResource)(poGit_server_default$1);
2043
+ //#endregion
2044
+ //#region src/extensions/workspaces/sessions/(backend)/resource/_lib/poGitReadme.server.ts
2045
+ var poGitReadme_server_default$1 = (context) => context.root.resources[2];
2046
+ //#endregion
2047
+ //#region src/extensions/workspaces/sessions/(backend)/resource/poGitReadme.server.ts
2048
+ var poGitReadme_server_default = (0, _agimon_ai_po_core_extensionFile.defineResource)(poGitReadme_server_default$1);
2049
+ //#endregion
2050
+ //#region src/extensions/workspaces/sessions/(backend)/resource/_lib/poUseGit.server.ts
2051
+ var poUseGit_server_default$1 = (context) => context.root.resources[1];
2052
+ //#endregion
2053
+ //#region src/extensions/workspaces/sessions/(backend)/resource/poUseGit.server.ts
2054
+ var poUseGit_server_default = (0, _agimon_ai_po_core_extensionFile.defineResource)(poUseGit_server_default$1);
2055
+ //#endregion
2056
+ //#region src/extensions/workspaces/sessions/(backend)/tool/runWorktree.server.ts
2057
+ var runWorktree_server_default = (0, _agimon_ai_po_core_extensionFile.defineServerTool)((context) => context.root.tool);
2058
+ //#endregion
2059
+ //#region generated/server.ts
2060
+ const at = (value, context) => typeof value === "function" ? value(context) : value;
2061
+ const via = (identity, value) => value === void 0 ? void 0 : {
2062
+ ...identity,
2063
+ ...value
2064
+ };
2065
+ const facet = (0, _agimon_ai_po_core_serverFacet.defineServerPlugin)({
2066
+ name: "@agimon-ai/po-git",
2067
+ providerOverrides: (0, _agimon_ai_po_core_serverFacet.composeServerProviderOverrides)(root_server_default),
2068
+ global: (context) => ({
2069
+ get api() {
2070
+ return [at(route_server_default, context)];
2071
+ },
2072
+ get channels() {
2073
+ return [() => via({ frameType: "git_changes" }, git_changes_server_default()), () => via({ frameType: "git_worktrees" }, git_worktrees_server_default())];
2074
+ }
2075
+ }),
2076
+ workspace: (context) => ({
2077
+ get api() {
2078
+ return [at(route_server_default, context)];
2079
+ },
2080
+ get channels() {
2081
+ return [() => via({ frameType: "git_changes" }, git_changes_server_default()), () => via({ frameType: "git_worktrees" }, git_worktrees_server_default())];
2082
+ }
2083
+ }),
2084
+ session: async (context) => {
2085
+ const scopeSession = await root_server_default(context);
2086
+ const contextSession = {
2087
+ ...context,
2088
+ root: scopeSession.value
2089
+ };
2090
+ return {
2091
+ ...(0, _agimon_ai_po_core_extensionFile.composeRootHooks)(scopeSession),
2092
+ get api() {
2093
+ return [at(route_server_default, context)];
2094
+ },
2095
+ get channels() {
2096
+ return [() => via({ frameType: "git_changes" }, git_changes_server_default()), () => via({ frameType: "git_worktrees" }, git_worktrees_server_default())];
2097
+ },
2098
+ get hooks() {
2099
+ return [via({ event: "agent_settled" }, at(agent_settled_server_default, contextSession)), via({ event: "tool_execution_end" }, at(tool_execution_end_server_default, contextSession))];
2100
+ },
2101
+ get resources() {
2102
+ return [
2103
+ at(poGit_server_default, contextSession),
2104
+ at(poGitReadme_server_default, contextSession),
2105
+ at(poUseGit_server_default, contextSession)
2106
+ ];
2107
+ },
2108
+ get tools() {
2109
+ return [via({ name: "run_worktree" }, at(runWorktree_server_default, contextSession))];
2110
+ },
2111
+ services: [...scopeSession.services ?? []],
2112
+ get activities() {
2113
+ return [...scopeSession.activities ?? []];
2114
+ }
2115
+ };
2116
+ }
2117
+ });
2118
+ //#endregion
2119
+ exports.default = facet;
2120
+ exports.facet = facet;
2121
+
2122
+ //# sourceMappingURL=server.cjs.map