@agimon-ai/po-git 0.0.0-alpha.7 → 0.0.0-alpha.71
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +149 -21
- package/README.md +16 -4
- package/dist/apiContracts.cjs +4 -1
- package/dist/apiContracts.cjs.map +1 -1
- package/dist/apiContracts.mjs +4 -1
- package/dist/apiContracts.mjs.map +1 -1
- package/dist/extensions/native.mjs +2163 -534
- package/dist/extensions/native.mjs.map +1 -1
- package/dist/extensions/server.cjs +293 -117
- package/dist/extensions/server.cjs.map +1 -1
- package/dist/extensions/server.d.mts +2 -2
- package/dist/extensions/server.d.mts.map +1 -1
- package/dist/extensions/server.mjs +290 -114
- package/dist/extensions/server.mjs.map +1 -1
- package/dist/extensions/web.mjs +1443 -1292
- package/dist/extensions/web.mjs.map +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.d.mts +2 -0
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +1 -1
- package/dist/{worktreeOperations-BSeyxUai.mjs → worktreeOperations-B3E4JY4H.mjs} +258 -85
- package/dist/worktreeOperations-B3E4JY4H.mjs.map +1 -0
- package/dist/{worktreeOperations-DGUbUnYU.cjs → worktreeOperations-CD7s0YnO.cjs} +288 -85
- package/dist/worktreeOperations-CD7s0YnO.cjs.map +1 -0
- package/llms.txt +1 -1
- package/package.json +21 -19
- package/src/prompts/{po-use-git → codemode-how-to-git}/SKILL.md +35 -32
- package/dist/worktreeOperations-BSeyxUai.mjs.map +0 -1
- package/dist/worktreeOperations-DGUbUnYU.cjs.map +0 -1
|
@@ -21,13 +21,14 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
21
21
|
}) : target, mod));
|
|
22
22
|
//#endregion
|
|
23
23
|
let node_child_process = require("node:child_process");
|
|
24
|
-
let
|
|
25
|
-
|
|
24
|
+
let node_fs_promises = require("node:fs/promises");
|
|
25
|
+
node_fs_promises = __toESM(node_fs_promises, 1);
|
|
26
26
|
let node_crypto = require("node:crypto");
|
|
27
|
-
let node_os = require("node:os");
|
|
28
|
-
node_os = __toESM(node_os, 1);
|
|
29
27
|
let node_path = require("node:path");
|
|
30
28
|
node_path = __toESM(node_path, 1);
|
|
29
|
+
let js_yaml = require("js-yaml");
|
|
30
|
+
let node_os = require("node:os");
|
|
31
|
+
node_os = __toESM(node_os, 1);
|
|
31
32
|
let node_util = require("node:util");
|
|
32
33
|
let _agimon_ai_po_core_piSettings = require("@agimon-ai/po-core/piSettings");
|
|
33
34
|
let node_timers_promises = require("node:timers/promises");
|
|
@@ -44,7 +45,7 @@ let node_timers_promises = require("node:timers/promises");
|
|
|
44
45
|
*/
|
|
45
46
|
const READ_TIMEOUT_MS = 3e4;
|
|
46
47
|
const ADD_TIMEOUT_MS = 3e5;
|
|
47
|
-
const REMOVE_TIMEOUT_MS = 3e5;
|
|
48
|
+
const REMOVE_TIMEOUT_MS$1 = 3e5;
|
|
48
49
|
const PRUNE_TIMEOUT_MS = 15e3;
|
|
49
50
|
/** fetch and push: bounded by the network, so long enough for a slow link, short enough to notice a dead one. */
|
|
50
51
|
const NETWORK_TIMEOUT_MS = 12e4;
|
|
@@ -197,9 +198,9 @@ function createWorktreeGit() {
|
|
|
197
198
|
"remove",
|
|
198
199
|
...force ? ["--force"] : [],
|
|
199
200
|
path
|
|
200
|
-
], REMOVE_TIMEOUT_MS);
|
|
201
|
+
], REMOVE_TIMEOUT_MS$1);
|
|
201
202
|
if (result.code === 0) return;
|
|
202
|
-
if (missingWorktree(result.stderr) && !
|
|
203
|
+
if (missingWorktree(result.stderr) && !await node_fs_promises.default.access(path).then(() => true, () => false)) {
|
|
203
204
|
await prune(repositoryRoot);
|
|
204
205
|
return;
|
|
205
206
|
}
|
|
@@ -285,6 +286,25 @@ function createWorktreeGit() {
|
|
|
285
286
|
};
|
|
286
287
|
}
|
|
287
288
|
//#endregion
|
|
289
|
+
//#region src/constants/git.ts
|
|
290
|
+
/** Workspace-mounted routes behind the new-session dialog. */
|
|
291
|
+
const GIT_BRANCHES_PATH = "/branches";
|
|
292
|
+
const GIT_SESSIONS_PATH = "/sessions";
|
|
293
|
+
/** Workspace-mounted: that workspace's remote auth for manual pull, push and rebase. */
|
|
294
|
+
const GIT_AUTH_PATH = "/auth";
|
|
295
|
+
/** Session-mounted: the session's change against its base, and one file's hunks. */
|
|
296
|
+
const GIT_REVIEW_PATH = "/review";
|
|
297
|
+
const GIT_REVIEW_FILE_PATH = "/review/file";
|
|
298
|
+
/** Repository file, relative to the checkout root, whose `setup` commands run in every new worktree. */
|
|
299
|
+
const WORKTREE_CONFIG_FILE = ".po/worktree.yaml";
|
|
300
|
+
/**
|
|
301
|
+
* po-runner's session command, so setup runs as a runner of the worktree session and its log
|
|
302
|
+
* shows in Runners. Mirrors po-runner's `runnerLaunchLine`; the command follows the separator.
|
|
303
|
+
*/
|
|
304
|
+
const WORKTREE_SETUP_LAUNCH = "/runners start name=worktree-setup --";
|
|
305
|
+
/** Installs the rift CLI. Shown to the person to copy, and named in the error an agent reads. */
|
|
306
|
+
const RIFT_INSTALL_COMMAND = "npm install -g rift-snapshot";
|
|
307
|
+
//#endregion
|
|
288
308
|
//#region src/services/errors/index.ts
|
|
289
309
|
var PoGitExpectedError = class extends Error {
|
|
290
310
|
code;
|
|
@@ -320,12 +340,12 @@ const OWNER_ONLY_FILE = 384;
|
|
|
320
340
|
* record in it. Writing beside the target and renaming makes the swap atomic on
|
|
321
341
|
* the same filesystem, so a reader sees either the old file or the new one.
|
|
322
342
|
*/
|
|
323
|
-
function writeJsonAtomic(file, value) {
|
|
343
|
+
async function writeJsonAtomic(file, value) {
|
|
324
344
|
const directory = node_path.default.dirname(file);
|
|
325
|
-
|
|
326
|
-
const temporary = node_path.default.join(directory, `.${node_path.default.basename(file)}.${
|
|
327
|
-
|
|
328
|
-
|
|
345
|
+
await node_fs_promises.default.mkdir(directory, { recursive: true });
|
|
346
|
+
const temporary = node_path.default.join(directory, `.${node_path.default.basename(file)}.${(0, node_crypto.randomUUID)()}.tmp`);
|
|
347
|
+
await node_fs_promises.default.writeFile(temporary, `${JSON.stringify(value, null, 2)}\n`, { mode: OWNER_ONLY_FILE });
|
|
348
|
+
await node_fs_promises.default.rename(temporary, file);
|
|
329
349
|
}
|
|
330
350
|
/**
|
|
331
351
|
* Reads JSON, treating every failure as absent.
|
|
@@ -334,9 +354,9 @@ function writeJsonAtomic(file, value) {
|
|
|
334
354
|
* here: there is nothing to act on. Distinguishing them would only offer a
|
|
335
355
|
* choice no caller wants to make.
|
|
336
356
|
*/
|
|
337
|
-
function readJson(file) {
|
|
357
|
+
async function readJson(file) {
|
|
338
358
|
try {
|
|
339
|
-
return JSON.parse(
|
|
359
|
+
return JSON.parse(await node_fs_promises.default.readFile(file, "utf8"));
|
|
340
360
|
} catch {
|
|
341
361
|
return;
|
|
342
362
|
}
|
|
@@ -355,12 +375,12 @@ const MAX_LABEL_LENGTH = 32;
|
|
|
355
375
|
* the registry is per repository and must be found from inside a worktree too.
|
|
356
376
|
* A session may run in a subdirectory, so the nearest enclosing checkout is used.
|
|
357
377
|
*/
|
|
358
|
-
function gitCommonDirectory(directory) {
|
|
378
|
+
async function gitCommonDirectory(directory) {
|
|
359
379
|
let root = node_path.default.resolve(directory);
|
|
360
380
|
let marker = node_path.default.join(root, ".git");
|
|
361
381
|
let stat;
|
|
362
382
|
for (;;) try {
|
|
363
|
-
stat =
|
|
383
|
+
stat = await node_fs_promises.default.stat(marker);
|
|
364
384
|
break;
|
|
365
385
|
} catch {
|
|
366
386
|
const parent = node_path.default.dirname(root);
|
|
@@ -371,14 +391,14 @@ function gitCommonDirectory(directory) {
|
|
|
371
391
|
if (stat.isDirectory()) return marker;
|
|
372
392
|
let pointer;
|
|
373
393
|
try {
|
|
374
|
-
pointer =
|
|
394
|
+
pointer = (await node_fs_promises.default.readFile(marker, "utf8")).trim();
|
|
375
395
|
} catch {
|
|
376
396
|
return;
|
|
377
397
|
}
|
|
378
398
|
if (!pointer.startsWith("gitdir:")) return void 0;
|
|
379
399
|
const gitDir = node_path.default.resolve(root, pointer.slice(7).trim());
|
|
380
400
|
try {
|
|
381
|
-
const common =
|
|
401
|
+
const common = (await node_fs_promises.default.readFile(node_path.default.join(gitDir, "commondir"), "utf8")).trim();
|
|
382
402
|
return node_path.default.resolve(gitDir, common);
|
|
383
403
|
} catch {
|
|
384
404
|
return gitDir;
|
|
@@ -397,8 +417,8 @@ function repositoryLabel(root) {
|
|
|
397
417
|
* registry. Falling back to the path keeps a non-repository directory usable
|
|
398
418
|
* instead of failing, though nothing here should reach that case.
|
|
399
419
|
*/
|
|
400
|
-
function repositoryId(root) {
|
|
401
|
-
const common = gitCommonDirectory(root);
|
|
420
|
+
async function repositoryId(root) {
|
|
421
|
+
const common = await gitCommonDirectory(root);
|
|
402
422
|
const token = common === void 0 ? `root:${node_path.default.resolve(root)}` : `git:${common}`;
|
|
403
423
|
return (0, node_crypto.createHash)("sha256").update(token).digest("hex").slice(0, ID_LENGTH);
|
|
404
424
|
}
|
|
@@ -434,22 +454,26 @@ function credentialsFile(homeDir = node_os.default.homedir()) {
|
|
|
434
454
|
return node_path.default.join(poGitRoot(homeDir), "credentials.json");
|
|
435
455
|
}
|
|
436
456
|
/** One registry file per repository, keyed by the shared git directory. */
|
|
437
|
-
function registryFile(repositoryRoot, homeDir = node_os.default.homedir()) {
|
|
438
|
-
const id = repositoryId(repositoryRoot);
|
|
457
|
+
async function registryFile(repositoryRoot, homeDir = node_os.default.homedir()) {
|
|
458
|
+
const id = await repositoryId(repositoryRoot);
|
|
439
459
|
const root = node_path.default.join(poGitRoot(homeDir), "registry");
|
|
440
460
|
const canonical = node_path.default.join(root, id, "worktrees.json");
|
|
441
|
-
if (
|
|
461
|
+
if (await node_fs_promises.default.access(canonical).then(() => true, () => false)) return canonical;
|
|
442
462
|
let names;
|
|
443
463
|
try {
|
|
444
|
-
names =
|
|
464
|
+
names = await node_fs_promises.default.readdir(root);
|
|
445
465
|
} catch {
|
|
446
466
|
return canonical;
|
|
447
467
|
}
|
|
448
|
-
const legacyFiles =
|
|
468
|
+
const legacyFiles = [];
|
|
469
|
+
for (const entry of names.filter((name) => name.endsWith(`--${id}`)).sort()) {
|
|
470
|
+
const file = node_path.default.join(root, entry, "worktrees.json");
|
|
471
|
+
if (await node_fs_promises.default.access(file).then(() => true, () => false)) legacyFiles.push(file);
|
|
472
|
+
}
|
|
449
473
|
if (legacyFiles.length === 0) return canonical;
|
|
450
474
|
const entries = /* @__PURE__ */ new Map();
|
|
451
475
|
for (const file of legacyFiles) {
|
|
452
|
-
const legacy = readJson(file);
|
|
476
|
+
const legacy = await readJson(file);
|
|
453
477
|
if (legacy?.version !== 1 || !Array.isArray(legacy.entries)) continue;
|
|
454
478
|
for (const value of legacy.entries) {
|
|
455
479
|
if (typeof value !== "object" || value === null || !("id" in value) || typeof value.id !== "string" || value.id === "") continue;
|
|
@@ -460,20 +484,119 @@ function registryFile(repositoryRoot, homeDir = node_os.default.homedir()) {
|
|
|
460
484
|
}
|
|
461
485
|
}
|
|
462
486
|
const temporary = `${canonical}.${(0, node_crypto.randomUUID)()}.migration`;
|
|
463
|
-
writeJsonAtomic(temporary, {
|
|
487
|
+
await writeJsonAtomic(temporary, {
|
|
464
488
|
version: 1,
|
|
465
489
|
entries: [...entries.values()]
|
|
466
490
|
});
|
|
467
491
|
try {
|
|
468
|
-
|
|
492
|
+
await node_fs_promises.default.link(temporary, canonical);
|
|
469
493
|
} catch (error) {
|
|
470
494
|
if (error.code !== "EEXIST") throw error;
|
|
471
495
|
} finally {
|
|
472
|
-
|
|
496
|
+
await node_fs_promises.default.rm(temporary);
|
|
473
497
|
}
|
|
474
498
|
return canonical;
|
|
475
499
|
}
|
|
476
500
|
//#endregion
|
|
501
|
+
//#region src/services/riftCli/index.ts
|
|
502
|
+
/** Present at a repository root, it makes worktrees Rift copy-on-write workspaces instead of git worktrees. */
|
|
503
|
+
const RIFT_CONFIG_FILE = ".rift.toml";
|
|
504
|
+
/** Rift's identity marker; a root without it was never `rift init`ed. */
|
|
505
|
+
const RIFT_MARKER = ".rift";
|
|
506
|
+
/** Cloning is instant, but `.rift.toml` hooks such as an install run inside create. */
|
|
507
|
+
const CREATE_TIMEOUT_MS = 6e5;
|
|
508
|
+
const REMOVE_TIMEOUT_MS = 3e5;
|
|
509
|
+
/**
|
|
510
|
+
* Runs the rift CLI. Stderr is dropped rather than piped: hooks inherit it and may keep it open
|
|
511
|
+
* after rift exits (a `docker compose up -d`), and raw output never reaches a tool result anyway.
|
|
512
|
+
*/
|
|
513
|
+
function rift(cwd, args, timeout) {
|
|
514
|
+
return new Promise((resolve, reject) => {
|
|
515
|
+
const child = (0, node_child_process.spawn)("rift", [...args], {
|
|
516
|
+
cwd,
|
|
517
|
+
stdio: [
|
|
518
|
+
"ignore",
|
|
519
|
+
"pipe",
|
|
520
|
+
"ignore"
|
|
521
|
+
],
|
|
522
|
+
timeout
|
|
523
|
+
});
|
|
524
|
+
let stdout = "";
|
|
525
|
+
child.stdout.on("data", (chunk) => stdout += String(chunk));
|
|
526
|
+
child.on("error", (error) => reject(error.code === "ENOENT" ? new PoGitExpectedError("rift_missing", `This repository has ${RIFT_CONFIG_FILE}, but the rift CLI is not installed.`, false, `Install it with \`${RIFT_INSTALL_COMMAND}\`, run \`rift init\` in the repository root, then try again. An agent asks the person with ask_user_question before installing. Removing ${RIFT_CONFIG_FILE} falls back to git worktrees.`) : error));
|
|
527
|
+
child.on("exit", (code) => resolve({
|
|
528
|
+
code: code ?? 1,
|
|
529
|
+
stdout
|
|
530
|
+
}));
|
|
531
|
+
});
|
|
532
|
+
}
|
|
533
|
+
const exists = (file) => node_fs_promises.default.access(file).then(() => true, () => false);
|
|
534
|
+
function createWorktreeRift() {
|
|
535
|
+
const remove = async (target) => {
|
|
536
|
+
if (!await exists(target)) return;
|
|
537
|
+
const result = await rift(node_path.default.dirname(target), ["remove", target], REMOVE_TIMEOUT_MS);
|
|
538
|
+
if (result.code !== 0) throw new Error(`rift remove failed (exit ${String(result.code)})`);
|
|
539
|
+
};
|
|
540
|
+
return {
|
|
541
|
+
enabled: (repositoryRoot) => exists(node_path.default.join(repositoryRoot, RIFT_CONFIG_FILE)),
|
|
542
|
+
async create({ repositoryRoot, path: target, branch, baseRef, existing, remote }) {
|
|
543
|
+
if (!await exists(node_path.default.join(repositoryRoot, RIFT_MARKER))) {
|
|
544
|
+
await rift(repositoryRoot, ["--help"], READ_TIMEOUT_MS);
|
|
545
|
+
throw new PoGitExpectedError("invalid_request", `This repository has ${RIFT_CONFIG_FILE}, but Rift is not initialized for ${repositoryRoot}.`, false, "Run `rift init` in the repository root (not inside a git worktree), then try again.");
|
|
546
|
+
}
|
|
547
|
+
if (await exists(target)) throw new Error(`The worktree path ${target} already exists.`);
|
|
548
|
+
await node_fs_promises.default.mkdir(node_path.default.dirname(target), { recursive: true });
|
|
549
|
+
const created = await rift(repositoryRoot, [
|
|
550
|
+
"create",
|
|
551
|
+
repositoryRoot,
|
|
552
|
+
"--into",
|
|
553
|
+
node_path.default.dirname(target),
|
|
554
|
+
"--name",
|
|
555
|
+
node_path.default.basename(target),
|
|
556
|
+
"--copy-all"
|
|
557
|
+
], CREATE_TIMEOUT_MS);
|
|
558
|
+
if (created.code !== 0) {
|
|
559
|
+
await remove(target).catch(() => void 0);
|
|
560
|
+
throw new Error(`rift create failed (exit ${String(created.code)})`);
|
|
561
|
+
}
|
|
562
|
+
const switchArgs = existing ? remote === void 0 ? [
|
|
563
|
+
"switch",
|
|
564
|
+
"--discard-changes",
|
|
565
|
+
branch
|
|
566
|
+
] : [
|
|
567
|
+
"switch",
|
|
568
|
+
"--discard-changes",
|
|
569
|
+
"--track",
|
|
570
|
+
"-c",
|
|
571
|
+
branch,
|
|
572
|
+
`${remote}/${branch}`
|
|
573
|
+
] : [
|
|
574
|
+
"switch",
|
|
575
|
+
"--discard-changes",
|
|
576
|
+
"-c",
|
|
577
|
+
branch,
|
|
578
|
+
baseRef
|
|
579
|
+
];
|
|
580
|
+
for (const args of [switchArgs, ["clean", "-fd"]]) {
|
|
581
|
+
const result = await runGit(target, args, { timeout: READ_TIMEOUT_MS });
|
|
582
|
+
if (result.code !== 0) {
|
|
583
|
+
await remove(target).catch(() => void 0);
|
|
584
|
+
throw new Error(`git ${String(args[0])} in the rift failed (exit ${String(result.code)})`);
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
},
|
|
588
|
+
remove,
|
|
589
|
+
async fetchBranch({ repositoryRoot, path: source, branch }) {
|
|
590
|
+
const result = await runGit(repositoryRoot, [
|
|
591
|
+
"fetch",
|
|
592
|
+
source,
|
|
593
|
+
`refs/heads/${branch}:refs/heads/${branch}`
|
|
594
|
+
], { timeout: READ_TIMEOUT_MS });
|
|
595
|
+
if (result.code !== 0) throw new Error(`git fetch from the rift failed (exit ${String(result.code)})`);
|
|
596
|
+
}
|
|
597
|
+
};
|
|
598
|
+
}
|
|
599
|
+
//#endregion
|
|
477
600
|
//#region src/services/worktreeEvents/index.ts
|
|
478
601
|
const GIT_WORKTREE_LIFECYCLE_EVENT = "git_worktree_lifecycle";
|
|
479
602
|
const GIT_WORKTREE_MESSAGE_EVENT = "git_worktree_message";
|
|
@@ -588,18 +711,22 @@ function refuseClose(input) {
|
|
|
588
711
|
* uncommitted work, so losing the record would lose the only pointer to it.
|
|
589
712
|
* Deletion is always a separate, named decision.
|
|
590
713
|
*/
|
|
591
|
-
function reconcile(records, probe) {
|
|
714
|
+
async function reconcile(records, probe) {
|
|
592
715
|
const changed = [];
|
|
716
|
+
const next = [];
|
|
717
|
+
for (const record of records) {
|
|
718
|
+
if (record.status === "orphaned" || await probe.exists(record.path) && probe.alive(record)) {
|
|
719
|
+
next.push(record);
|
|
720
|
+
continue;
|
|
721
|
+
}
|
|
722
|
+
changed.push(record.id);
|
|
723
|
+
next.push({
|
|
724
|
+
...record,
|
|
725
|
+
status: "orphaned"
|
|
726
|
+
});
|
|
727
|
+
}
|
|
593
728
|
return {
|
|
594
|
-
records:
|
|
595
|
-
if (record.status === "orphaned") return record;
|
|
596
|
-
if (!(!probe.exists(record.path) || !probe.alive(record))) return record;
|
|
597
|
-
changed.push(record.id);
|
|
598
|
-
return {
|
|
599
|
-
...record,
|
|
600
|
-
status: "orphaned"
|
|
601
|
-
};
|
|
602
|
-
}),
|
|
729
|
+
records: next,
|
|
603
730
|
changed
|
|
604
731
|
};
|
|
605
732
|
}
|
|
@@ -659,25 +786,25 @@ function isRecord(value) {
|
|
|
659
786
|
*/
|
|
660
787
|
function createWorktreeRegistry(file) {
|
|
661
788
|
return {
|
|
662
|
-
list() {
|
|
663
|
-
const parsed = readJson(file);
|
|
789
|
+
async list() {
|
|
790
|
+
const parsed = await readJson(file);
|
|
664
791
|
if (!parsed || parsed.version !== 1 || !Array.isArray(parsed.entries)) return [];
|
|
665
792
|
return parsed.entries.filter(isRecord);
|
|
666
793
|
},
|
|
667
|
-
replace(entries) {
|
|
668
|
-
writeJsonAtomic(file, {
|
|
794
|
+
async replace(entries) {
|
|
795
|
+
await writeJsonAtomic(file, {
|
|
669
796
|
version: 1,
|
|
670
797
|
entries
|
|
671
798
|
});
|
|
672
799
|
},
|
|
673
800
|
async transaction(operation, signal) {
|
|
674
801
|
const lock = `${file}.lock`;
|
|
675
|
-
|
|
802
|
+
await node_fs_promises.default.mkdir(node_path.default.dirname(file), { recursive: true });
|
|
676
803
|
const deadline = Date.now() + LOCK_WAIT_MS;
|
|
677
804
|
for (;;) {
|
|
678
805
|
signal?.throwIfAborted();
|
|
679
806
|
try {
|
|
680
|
-
|
|
807
|
+
await node_fs_promises.default.mkdir(lock, { mode: 448 });
|
|
681
808
|
break;
|
|
682
809
|
} catch (error) {
|
|
683
810
|
if (error.code !== "EEXIST") throw error;
|
|
@@ -686,14 +813,14 @@ function createWorktreeRegistry(file) {
|
|
|
686
813
|
}
|
|
687
814
|
}
|
|
688
815
|
try {
|
|
689
|
-
|
|
816
|
+
await node_fs_promises.default.writeFile(node_path.default.join(lock, "owner.json"), JSON.stringify({
|
|
690
817
|
pid: process.pid,
|
|
691
818
|
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
692
819
|
}), { mode: 384 });
|
|
693
820
|
signal?.throwIfAborted();
|
|
694
821
|
return await operation();
|
|
695
822
|
} finally {
|
|
696
|
-
|
|
823
|
+
await node_fs_promises.default.rm(lock, { recursive: true });
|
|
697
824
|
}
|
|
698
825
|
}
|
|
699
826
|
};
|
|
@@ -703,8 +830,26 @@ function createWorktreeRegistry(file) {
|
|
|
703
830
|
const GIT_WORKTREE_DELEGATION_KIND = "git.worktree.delegation";
|
|
704
831
|
const GIT_WORKTREE_MESSAGE_KIND = "git.worktree.message";
|
|
705
832
|
const GIT_WORKTREE_REPORT_KIND = "git.worktree.report";
|
|
833
|
+
/**
|
|
834
|
+
* The repository's worktree setup as one shell command, or undefined when it asks for none.
|
|
835
|
+
* Read from the checkout the spawn starts in, so an uncommitted edit applies at once.
|
|
836
|
+
*/
|
|
837
|
+
async function readWorktreeSetup(root) {
|
|
838
|
+
const file = node_path.default.join(root, WORKTREE_CONFIG_FILE);
|
|
839
|
+
let setup;
|
|
840
|
+
try {
|
|
841
|
+
setup = (0, js_yaml.load)(await node_fs_promises.default.readFile(file, "utf8"))?.setup;
|
|
842
|
+
} catch (error) {
|
|
843
|
+
if (error.code === "ENOENT") return void 0;
|
|
844
|
+
throw new PoGitExpectedError("invalid_request", `${WORKTREE_CONFIG_FILE} is not valid YAML (${error.message}).`, false, "Fix the file and try again.");
|
|
845
|
+
}
|
|
846
|
+
const commands = [setup ?? []].flat();
|
|
847
|
+
if (!commands.every((command) => typeof command === "string")) throw new PoGitExpectedError("invalid_request", `${WORKTREE_CONFIG_FILE} setup must be a command or a list of commands.`, false, "Fix the file and try again.");
|
|
848
|
+
return commands.filter((command) => command.trim() !== "").join(" && ") || void 0;
|
|
849
|
+
}
|
|
706
850
|
function createWorktreeOperations(deps) {
|
|
707
851
|
const { git } = deps;
|
|
852
|
+
const rift = deps.rift ?? createWorktreeRift();
|
|
708
853
|
const sessionService = deps.sessionService;
|
|
709
854
|
const now = deps.now ?? (() => /* @__PURE__ */ new Date());
|
|
710
855
|
const requireSessionService = () => {
|
|
@@ -723,8 +868,19 @@ function createWorktreeOperations(deps) {
|
|
|
723
868
|
};
|
|
724
869
|
const probe = {
|
|
725
870
|
alive: (record) => requireSessionService().isLive(record.sessionId),
|
|
726
|
-
exists: (path) =>
|
|
871
|
+
exists: (path) => node_fs_promises.default.access(path).then(() => true, () => false)
|
|
727
872
|
};
|
|
873
|
+
/** What git lists as worktrees, plus Rift workspaces, which git cannot see. */
|
|
874
|
+
const listedPaths = async (root, records) => {
|
|
875
|
+
const rifts = records.filter((record) => record.backend === "rift");
|
|
876
|
+
const present = await Promise.all(rifts.map((record) => probe.exists(record.path)));
|
|
877
|
+
return [...await git.listWorktreePaths(root), ...rifts.filter((_, index) => present[index]).map((r) => r.path)];
|
|
878
|
+
};
|
|
879
|
+
const removeCheckout = (root, record, force) => record.backend === "rift" ? rift.remove(record.path) : git.removeWorktree({
|
|
880
|
+
repositoryRoot: root,
|
|
881
|
+
path: record.path,
|
|
882
|
+
force
|
|
883
|
+
});
|
|
728
884
|
/**
|
|
729
885
|
* Everything an action needs about the repository it is acting on.
|
|
730
886
|
*
|
|
@@ -735,11 +891,11 @@ function createWorktreeOperations(deps) {
|
|
|
735
891
|
const resolve = async (context) => {
|
|
736
892
|
const root = await git.repositoryRoot(context.cwd);
|
|
737
893
|
if (root === void 0) throw new PoGitExpectedError("not_a_repository", `${context.cwd} is not inside a git repository.`, false, "Run this from a repository checkout.");
|
|
738
|
-
const store = createWorktreeRegistry(registryFile(root, deps.homeDir));
|
|
894
|
+
const store = createWorktreeRegistry(await registryFile(root, deps.homeDir));
|
|
739
895
|
return {
|
|
740
896
|
root,
|
|
741
897
|
store,
|
|
742
|
-
records: store.list()
|
|
898
|
+
records: await store.list()
|
|
743
899
|
};
|
|
744
900
|
};
|
|
745
901
|
const require$1 = (records, id) => {
|
|
@@ -790,11 +946,11 @@ function createWorktreeOperations(deps) {
|
|
|
790
946
|
const expectedPath = worktreeDirectory({
|
|
791
947
|
worktreesRoot: worktreesRoot(deps.homeDir),
|
|
792
948
|
repositoryLabel: repositoryLabel(root),
|
|
793
|
-
repositoryId: repositoryId(root),
|
|
949
|
+
repositoryId: await repositoryId(root),
|
|
794
950
|
branch: request.branch,
|
|
795
951
|
shortId: shortId(reserved.sessionId)
|
|
796
952
|
});
|
|
797
|
-
if (existing.parentSessionId !== parentSessionId || existing.repositoryRoot !== root || existing.branch !== request.branch || existing.path !== expectedPath || existing.path !== reserved?.cwd || request.baseRef !== void 0 && existing.baseRef !== request.baseRef || !
|
|
953
|
+
if (existing.parentSessionId !== parentSessionId || existing.repositoryRoot !== root || existing.branch !== request.branch || existing.path !== expectedPath || existing.path !== reserved?.cwd || request.baseRef !== void 0 && existing.baseRef !== request.baseRef || !await probe.exists(existing.path) || !(await listedPaths(root, records)).includes(existing.path) || await git.repositoryRoot(existing.path) !== existing.path || await git.currentBranch(existing.path) !== request.branch) throw new PoGitExpectedError("invalid_request", "The reserved checkout no longer matches this setup.", false, "Inspect the existing branch and checkout; they will not be replaced.");
|
|
798
954
|
if (!requireSessionService().isLive(existing.sessionId)) {
|
|
799
955
|
if ((await requireSessionService().create({
|
|
800
956
|
cwd: existing.path,
|
|
@@ -814,18 +970,20 @@ function createWorktreeOperations(deps) {
|
|
|
814
970
|
});
|
|
815
971
|
if (refusal !== void 0) throw new PoGitExpectedError("worktree_exists", refusal, false, "Pick another branch name, or close it.");
|
|
816
972
|
const taskDelivery = request.task === void 0 ? void 0 : requireSessionDelivery();
|
|
973
|
+
const setup = await readWorktreeSetup(root);
|
|
974
|
+
const useRift = await rift.enabled(root);
|
|
817
975
|
const existingBranch = request.checkout === "existing";
|
|
818
976
|
const id = shortId(reserved?.sessionId ?? (0, node_crypto.randomUUID)());
|
|
819
977
|
let path = worktreeDirectory({
|
|
820
978
|
worktreesRoot: worktreesRoot(deps.homeDir),
|
|
821
979
|
repositoryLabel: repositoryLabel(root),
|
|
822
|
-
repositoryId: repositoryId(root),
|
|
980
|
+
repositoryId: await repositoryId(root),
|
|
823
981
|
branch: request.branch,
|
|
824
982
|
shortId: id
|
|
825
983
|
});
|
|
826
984
|
if (request.reservationId !== void 0) path = (await reservations.prepare(request.reservationId, parentSessionId, path)).cwd;
|
|
827
|
-
const recovering = reserved?.cwd !== void 0 &&
|
|
828
|
-
if (recovering && (!(await git.listWorktreePaths(root)).includes(path) || await git.currentBranch(path) !== request.branch)) throw new PoGitExpectedError("invalid_request", "The reserved checkout no longer matches this setup.", false, "Inspect the existing directory; it will not be replaced.");
|
|
985
|
+
const recovering = reserved?.cwd !== void 0 && await probe.exists(path);
|
|
986
|
+
if (recovering && (!(useRift ? await git.repositoryRoot(path) === path : (await git.listWorktreePaths(root)).includes(path)) || await git.currentBranch(path) !== request.branch)) throw new PoGitExpectedError("invalid_request", "The reserved checkout no longer matches this setup.", false, "Inspect the existing directory; it will not be replaced.");
|
|
829
987
|
if (!recovering) {
|
|
830
988
|
if (existingBranch) {
|
|
831
989
|
const branches = await git.listBranches(root);
|
|
@@ -838,11 +996,11 @@ function createWorktreeOperations(deps) {
|
|
|
838
996
|
const cancelled = () => options?.signal?.aborted === true;
|
|
839
997
|
const rollback = async () => {
|
|
840
998
|
if (reserved) return ` The reserved checkout at ${path} was retained for recovery.`;
|
|
841
|
-
await
|
|
842
|
-
repositoryRoot: root,
|
|
999
|
+
await removeCheckout(root, {
|
|
843
1000
|
path,
|
|
844
|
-
|
|
845
|
-
}).catch(() => void 0);
|
|
1001
|
+
...useRift ? { backend: "rift" } : {}
|
|
1002
|
+
}, true).catch(() => void 0);
|
|
1003
|
+
if (useRift) return "";
|
|
846
1004
|
if (existingBranch && request.remote === void 0) return "";
|
|
847
1005
|
return await git.deleteBranch({
|
|
848
1006
|
repositoryRoot: root,
|
|
@@ -851,7 +1009,15 @@ function createWorktreeOperations(deps) {
|
|
|
851
1009
|
};
|
|
852
1010
|
options?.onProgress?.(`creating branch ${request.branch}\u2026`);
|
|
853
1011
|
if (!recovering) {
|
|
854
|
-
if (
|
|
1012
|
+
if (useRift) await rift.create({
|
|
1013
|
+
repositoryRoot: root,
|
|
1014
|
+
path,
|
|
1015
|
+
branch: request.branch,
|
|
1016
|
+
baseRef,
|
|
1017
|
+
existing: existingBranch,
|
|
1018
|
+
...request.remote === void 0 ? {} : { remote: request.remote }
|
|
1019
|
+
});
|
|
1020
|
+
else if (existingBranch) await git.addExistingWorktree({
|
|
855
1021
|
repositoryRoot: root,
|
|
856
1022
|
path,
|
|
857
1023
|
branch: request.branch,
|
|
@@ -893,20 +1059,30 @@ function createWorktreeOperations(deps) {
|
|
|
893
1059
|
sessionId,
|
|
894
1060
|
...parented,
|
|
895
1061
|
status: "running",
|
|
1062
|
+
...useRift ? { backend: "rift" } : {},
|
|
896
1063
|
createdAt: now().toISOString()
|
|
897
1064
|
};
|
|
898
1065
|
try {
|
|
899
|
-
store.replace([...records, record]);
|
|
1066
|
+
await store.replace([...records, record]);
|
|
900
1067
|
} catch (error) {
|
|
901
1068
|
await requireSessionService().close(sessionId).catch(() => void 0);
|
|
902
1069
|
const kept = await rollback();
|
|
903
1070
|
throw new PoGitExpectedError("registry_write_failed", `The worktree was created but the registry could not be written (${error.message}).${kept}`, true, "Check the registry file for permissions or free space, then try again.");
|
|
904
1071
|
}
|
|
1072
|
+
let setupStarted = false;
|
|
1073
|
+
if (setup !== void 0) try {
|
|
1074
|
+
const service = requireSessionService();
|
|
1075
|
+
if (service.prompt === void 0) throw new Error("this host cannot run session commands");
|
|
1076
|
+
await service.prompt(sessionId, `${WORKTREE_SETUP_LAUNCH} ${setup}`);
|
|
1077
|
+
setupStarted = true;
|
|
1078
|
+
} catch (error) {
|
|
1079
|
+
options?.onProgress?.(`worktree setup did not start (${String(error)})`);
|
|
1080
|
+
}
|
|
905
1081
|
if (request.task !== void 0 && taskDelivery !== void 0) try {
|
|
906
1082
|
const receipt = await taskDelivery.deliver({
|
|
907
1083
|
recipientKey: sessionId,
|
|
908
1084
|
kind: GIT_WORKTREE_DELEGATION_KIND,
|
|
909
|
-
prompt: request.task,
|
|
1085
|
+
prompt: setupStarted ? `${request.task}\n\nThe worktree setup (${setup}) is running as the runner "worktree-setup". Check it with po-runner before relying on installed dependencies or builds.` : request.task,
|
|
910
1086
|
metadata: {
|
|
911
1087
|
worktreeId: record.id,
|
|
912
1088
|
fromRole: "parent",
|
|
@@ -929,23 +1105,19 @@ function createWorktreeOperations(deps) {
|
|
|
929
1105
|
requireOwner(context, record, "close");
|
|
930
1106
|
const refusal = refuseClose({
|
|
931
1107
|
record,
|
|
932
|
-
dirtyFiles:
|
|
1108
|
+
dirtyFiles: await probe.exists(record.path) ? await git.dirtyFiles(record.path) : [],
|
|
933
1109
|
force
|
|
934
1110
|
});
|
|
935
1111
|
if (refusal !== void 0) throw new PoGitExpectedError("worktree_dirty", refusal, false, "Commit the work, or pass force: true.");
|
|
936
1112
|
await requireSessionService().close(record.sessionId);
|
|
937
|
-
await
|
|
938
|
-
|
|
939
|
-
path: record.path,
|
|
940
|
-
force
|
|
941
|
-
});
|
|
942
|
-
store.replace(records.filter((entry) => entry.id !== id));
|
|
1113
|
+
await removeCheckout(root, record, force);
|
|
1114
|
+
await store.replace(records.filter((entry) => entry.id !== id));
|
|
943
1115
|
return record;
|
|
944
1116
|
},
|
|
945
1117
|
async list(context) {
|
|
946
1118
|
const { store, records } = await resolve(context);
|
|
947
|
-
const outcome = reconcile(records, probe);
|
|
948
|
-
if (outcome.changed.length > 0) store.replace(outcome.records);
|
|
1119
|
+
const outcome = await reconcile(records, probe);
|
|
1120
|
+
if (outcome.changed.length > 0) await store.replace(outcome.records);
|
|
949
1121
|
return outcome.records;
|
|
950
1122
|
},
|
|
951
1123
|
async status(context, id) {
|
|
@@ -953,7 +1125,7 @@ function createWorktreeOperations(deps) {
|
|
|
953
1125
|
const record = require$1(records, id);
|
|
954
1126
|
return {
|
|
955
1127
|
record,
|
|
956
|
-
dirtyFiles:
|
|
1128
|
+
dirtyFiles: await probe.exists(record.path) ? await git.dirtyFiles(record.path) : []
|
|
957
1129
|
};
|
|
958
1130
|
},
|
|
959
1131
|
async merge(context, id, message) {
|
|
@@ -962,6 +1134,11 @@ function createWorktreeOperations(deps) {
|
|
|
962
1134
|
requireOwner(context, record, "merge");
|
|
963
1135
|
const parentDirty = await git.dirtyFiles(root);
|
|
964
1136
|
if (parentDirty.length > 0) throw new PoGitExpectedError("worktree_dirty", `The parent checkout has ${String(parentDirty.length)} uncommitted file(s): ${parentDirty.slice(0, 10).join(", ")}.`, false, "Commit or stash the parent checkout first.");
|
|
1137
|
+
if (record.backend === "rift") await rift.fetchBranch({
|
|
1138
|
+
repositoryRoot: root,
|
|
1139
|
+
path: record.path,
|
|
1140
|
+
branch: record.branch
|
|
1141
|
+
});
|
|
965
1142
|
await git.mergeBranch({
|
|
966
1143
|
repositoryRoot: root,
|
|
967
1144
|
branch: record.branch,
|
|
@@ -1026,10 +1203,10 @@ function createWorktreeOperations(deps) {
|
|
|
1026
1203
|
},
|
|
1027
1204
|
async prune(context, dryRun) {
|
|
1028
1205
|
const { root, store, records } = await resolve(context);
|
|
1029
|
-
const reconciled = reconcile(records, probe);
|
|
1030
|
-
const gitPaths = await
|
|
1206
|
+
const reconciled = await reconcile(records, probe);
|
|
1207
|
+
const gitPaths = await listedPaths(root, reconciled.records);
|
|
1031
1208
|
const dirtyByPath = /* @__PURE__ */ new Map();
|
|
1032
|
-
for (const record of reconciled.records) if (record.status === "orphaned" &&
|
|
1209
|
+
for (const record of reconciled.records) if (record.status === "orphaned" && await probe.exists(record.path)) dirtyByPath.set(record.path, await git.dirtyFiles(record.path));
|
|
1033
1210
|
const plan = planPrune({
|
|
1034
1211
|
records: reconciled.records,
|
|
1035
1212
|
gitPaths,
|
|
@@ -1037,20 +1214,16 @@ function createWorktreeOperations(deps) {
|
|
|
1037
1214
|
dirtyByPath
|
|
1038
1215
|
});
|
|
1039
1216
|
if (dryRun) {
|
|
1040
|
-
store.replace(reconciled.records);
|
|
1217
|
+
await store.replace(reconciled.records);
|
|
1041
1218
|
return plan;
|
|
1042
1219
|
}
|
|
1043
|
-
for (const record of plan.remove) await
|
|
1044
|
-
repositoryRoot: root,
|
|
1045
|
-
path: record.path,
|
|
1046
|
-
force: false
|
|
1047
|
-
});
|
|
1220
|
+
for (const record of plan.remove) await removeCheckout(root, record, false);
|
|
1048
1221
|
for (const record of [...plan.remove, ...plan.forget]) if (!await git.deleteBranch({
|
|
1049
1222
|
repositoryRoot: root,
|
|
1050
1223
|
branch: record.branch
|
|
1051
|
-
}).catch(() => false)) plan.keptBranches.push(record.branch);
|
|
1224
|
+
}).catch(() => false) && record.backend !== "rift") plan.keptBranches.push(record.branch);
|
|
1052
1225
|
const dropped = new Set([...plan.remove, ...plan.forget].map((record) => record.id));
|
|
1053
|
-
store.replace(reconciled.records.filter((record) => !dropped.has(record.id)));
|
|
1226
|
+
await store.replace(reconciled.records.filter((record) => !dropped.has(record.id)));
|
|
1054
1227
|
return plan;
|
|
1055
1228
|
}
|
|
1056
1229
|
};
|
|
@@ -1072,7 +1245,7 @@ function createWorktreeOperations(deps) {
|
|
|
1072
1245
|
return {
|
|
1073
1246
|
...operations,
|
|
1074
1247
|
spawn: (context, request, options) => {
|
|
1075
|
-
if (sessionService === void 0) return Promise.reject(new PoGitExpectedError("hub_unavailable", "The cockpit session service is unavailable.", true, "Use the cockpit worktree tool after its Git server
|
|
1248
|
+
if (sessionService === void 0) return Promise.reject(new PoGitExpectedError("hub_unavailable", "The cockpit session service is unavailable.", true, "Use the cockpit worktree tool after its Git server plugin is available."));
|
|
1076
1249
|
if (!validRefName(request.branch) || request.baseRef !== void 0 && !validRefName(request.baseRef)) return Promise.reject(new PoGitExpectedError("invalid_request", "That is not a valid branch name.", false, "Pick another name."));
|
|
1077
1250
|
if (request.reservationId !== void 0 && context.sessionId === void 0) return Promise.reject(new PoGitExpectedError("invalid_request", "A reserved worktree session requires its owning parent.", false, "Create the session from its pending setup card."));
|
|
1078
1251
|
return locked(context, () => operations.spawn(context, request, options), options?.signal);
|
|
@@ -1084,6 +1257,36 @@ function createWorktreeOperations(deps) {
|
|
|
1084
1257
|
};
|
|
1085
1258
|
}
|
|
1086
1259
|
//#endregion
|
|
1260
|
+
Object.defineProperty(exports, "GIT_AUTH_PATH", {
|
|
1261
|
+
enumerable: true,
|
|
1262
|
+
get: function() {
|
|
1263
|
+
return GIT_AUTH_PATH;
|
|
1264
|
+
}
|
|
1265
|
+
});
|
|
1266
|
+
Object.defineProperty(exports, "GIT_BRANCHES_PATH", {
|
|
1267
|
+
enumerable: true,
|
|
1268
|
+
get: function() {
|
|
1269
|
+
return GIT_BRANCHES_PATH;
|
|
1270
|
+
}
|
|
1271
|
+
});
|
|
1272
|
+
Object.defineProperty(exports, "GIT_REVIEW_FILE_PATH", {
|
|
1273
|
+
enumerable: true,
|
|
1274
|
+
get: function() {
|
|
1275
|
+
return GIT_REVIEW_FILE_PATH;
|
|
1276
|
+
}
|
|
1277
|
+
});
|
|
1278
|
+
Object.defineProperty(exports, "GIT_REVIEW_PATH", {
|
|
1279
|
+
enumerable: true,
|
|
1280
|
+
get: function() {
|
|
1281
|
+
return GIT_REVIEW_PATH;
|
|
1282
|
+
}
|
|
1283
|
+
});
|
|
1284
|
+
Object.defineProperty(exports, "GIT_SESSIONS_PATH", {
|
|
1285
|
+
enumerable: true,
|
|
1286
|
+
get: function() {
|
|
1287
|
+
return GIT_SESSIONS_PATH;
|
|
1288
|
+
}
|
|
1289
|
+
});
|
|
1087
1290
|
Object.defineProperty(exports, "GIT_SESSION_CHANGED_EVENT", {
|
|
1088
1291
|
enumerable: true,
|
|
1089
1292
|
get: function() {
|
|
@@ -1217,4 +1420,4 @@ Object.defineProperty(exports, "writeJsonAtomic", {
|
|
|
1217
1420
|
}
|
|
1218
1421
|
});
|
|
1219
1422
|
|
|
1220
|
-
//# sourceMappingURL=worktreeOperations-
|
|
1423
|
+
//# sourceMappingURL=worktreeOperations-CD7s0YnO.cjs.map
|