borgmcp 1.0.5 → 1.0.7

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.
Files changed (157) hide show
  1. package/dist/assimilate-cmd.js +39 -497
  2. package/dist/assimilate-deps.js +3 -177
  3. package/dist/assimilate-welcome.js +2 -24
  4. package/dist/auth-env.js +1 -107
  5. package/dist/auth.js +23 -612
  6. package/dist/claude.js +11 -281
  7. package/dist/cli-help.js +29 -50
  8. package/dist/cli-platform.js +4 -94
  9. package/dist/codex-app-server.js +4 -228
  10. package/dist/codex-app-wake.js +2 -122
  11. package/dist/codex-launch.js +1 -81
  12. package/dist/codex-remote.js +1 -250
  13. package/dist/config-utils.js +3 -385
  14. package/dist/config.js +1 -190
  15. package/dist/console-prefix.js +1 -86
  16. package/dist/cube-name.js +1 -65
  17. package/dist/cubes.js +4 -269
  18. package/dist/debug.js +1 -71
  19. package/dist/device-auth.js +1 -167
  20. package/dist/direct-log.js +1 -11
  21. package/dist/health-beat.js +1 -168
  22. package/dist/inbox-monitor.js +1 -129
  23. package/dist/index.js +26 -1378
  24. package/dist/lifecycle-log-guard.js +2 -93
  25. package/dist/list-roles-render.js +6 -39
  26. package/dist/log-audit.js +3 -186
  27. package/dist/log-stream.js +9 -848
  28. package/dist/name-validator.js +1 -22
  29. package/dist/parse-assimilate-args.js +1 -82
  30. package/dist/postinstall.js +8 -22
  31. package/dist/regen-format.js +11 -329
  32. package/dist/regen.js +5 -83
  33. package/dist/remote-client.js +1 -695
  34. package/dist/role-resolver.js +1 -36
  35. package/dist/role-section.js +8 -208
  36. package/dist/roster-render.js +3 -96
  37. package/dist/setup.js +36 -251
  38. package/dist/shell-escape.js +1 -22
  39. package/dist/spawn.js +10 -29
  40. package/dist/stale-version-check.js +1 -102
  41. package/dist/stream-owner.js +2 -202
  42. package/dist/stream-status.js +3 -211
  43. package/dist/subscription-retry.js +1 -23
  44. package/dist/sync-roles-render.js +3 -118
  45. package/dist/sync.js +22 -286
  46. package/dist/templates.js +120 -563
  47. package/dist/terminal-title.js +1 -68
  48. package/dist/token-crypto.js +1 -91
  49. package/dist/token-store.js +1 -222
  50. package/dist/types.js +0 -5
  51. package/dist/version.js +2 -78
  52. package/dist/worktree-lifecycle.js +2 -173
  53. package/package.json +11 -2
  54. package/dist/assimilate-cmd.d.ts.map +0 -1
  55. package/dist/assimilate-cmd.js.map +0 -1
  56. package/dist/assimilate-deps.d.ts.map +0 -1
  57. package/dist/assimilate-deps.js.map +0 -1
  58. package/dist/assimilate-welcome.d.ts.map +0 -1
  59. package/dist/assimilate-welcome.js.map +0 -1
  60. package/dist/auth-env.d.ts.map +0 -1
  61. package/dist/auth-env.js.map +0 -1
  62. package/dist/auth.d.ts.map +0 -1
  63. package/dist/auth.js.map +0 -1
  64. package/dist/claude.d.ts.map +0 -1
  65. package/dist/claude.js.map +0 -1
  66. package/dist/cli-help.d.ts.map +0 -1
  67. package/dist/cli-help.js.map +0 -1
  68. package/dist/cli-platform.d.ts.map +0 -1
  69. package/dist/cli-platform.js.map +0 -1
  70. package/dist/codex-app-server.d.ts.map +0 -1
  71. package/dist/codex-app-server.js.map +0 -1
  72. package/dist/codex-app-wake.d.ts.map +0 -1
  73. package/dist/codex-app-wake.js.map +0 -1
  74. package/dist/codex-launch.d.ts.map +0 -1
  75. package/dist/codex-launch.js.map +0 -1
  76. package/dist/codex-remote.d.ts.map +0 -1
  77. package/dist/codex-remote.js.map +0 -1
  78. package/dist/config-utils.d.ts.map +0 -1
  79. package/dist/config-utils.js.map +0 -1
  80. package/dist/config.d.ts.map +0 -1
  81. package/dist/config.js.map +0 -1
  82. package/dist/console-prefix.d.ts.map +0 -1
  83. package/dist/console-prefix.js.map +0 -1
  84. package/dist/cube-name.d.ts.map +0 -1
  85. package/dist/cube-name.js.map +0 -1
  86. package/dist/cubes.d.ts.map +0 -1
  87. package/dist/cubes.js.map +0 -1
  88. package/dist/debug.d.ts.map +0 -1
  89. package/dist/debug.js.map +0 -1
  90. package/dist/device-auth.d.ts.map +0 -1
  91. package/dist/device-auth.js.map +0 -1
  92. package/dist/direct-log.d.ts.map +0 -1
  93. package/dist/direct-log.js.map +0 -1
  94. package/dist/health-beat.d.ts.map +0 -1
  95. package/dist/health-beat.js.map +0 -1
  96. package/dist/inbox-monitor.d.ts.map +0 -1
  97. package/dist/inbox-monitor.js.map +0 -1
  98. package/dist/index.d.ts.map +0 -1
  99. package/dist/index.js.map +0 -1
  100. package/dist/lifecycle-log-guard.d.ts.map +0 -1
  101. package/dist/lifecycle-log-guard.js.map +0 -1
  102. package/dist/list-roles-render.d.ts.map +0 -1
  103. package/dist/list-roles-render.js.map +0 -1
  104. package/dist/log-audit.d.ts.map +0 -1
  105. package/dist/log-audit.js.map +0 -1
  106. package/dist/log-stream.d.ts.map +0 -1
  107. package/dist/log-stream.js.map +0 -1
  108. package/dist/name-validator.d.ts.map +0 -1
  109. package/dist/name-validator.js.map +0 -1
  110. package/dist/parse-assimilate-args.d.ts.map +0 -1
  111. package/dist/parse-assimilate-args.js.map +0 -1
  112. package/dist/postinstall.d.ts.map +0 -1
  113. package/dist/postinstall.js.map +0 -1
  114. package/dist/regen-format.d.ts.map +0 -1
  115. package/dist/regen-format.js.map +0 -1
  116. package/dist/regen.d.ts.map +0 -1
  117. package/dist/regen.js.map +0 -1
  118. package/dist/remote-client.d.ts.map +0 -1
  119. package/dist/remote-client.js.map +0 -1
  120. package/dist/role-resolver.d.ts.map +0 -1
  121. package/dist/role-resolver.js.map +0 -1
  122. package/dist/role-section.d.ts.map +0 -1
  123. package/dist/role-section.js.map +0 -1
  124. package/dist/roster-render.d.ts.map +0 -1
  125. package/dist/roster-render.js.map +0 -1
  126. package/dist/setup.d.ts.map +0 -1
  127. package/dist/setup.js.map +0 -1
  128. package/dist/shell-escape.d.ts.map +0 -1
  129. package/dist/shell-escape.js.map +0 -1
  130. package/dist/spawn.d.ts.map +0 -1
  131. package/dist/spawn.js.map +0 -1
  132. package/dist/stale-version-check.d.ts.map +0 -1
  133. package/dist/stale-version-check.js.map +0 -1
  134. package/dist/stream-owner.d.ts.map +0 -1
  135. package/dist/stream-owner.js.map +0 -1
  136. package/dist/stream-status.d.ts.map +0 -1
  137. package/dist/stream-status.js.map +0 -1
  138. package/dist/subscription-retry.d.ts.map +0 -1
  139. package/dist/subscription-retry.js.map +0 -1
  140. package/dist/sync-roles-render.d.ts.map +0 -1
  141. package/dist/sync-roles-render.js.map +0 -1
  142. package/dist/sync.d.ts.map +0 -1
  143. package/dist/sync.js.map +0 -1
  144. package/dist/templates.d.ts.map +0 -1
  145. package/dist/templates.js.map +0 -1
  146. package/dist/terminal-title.d.ts.map +0 -1
  147. package/dist/terminal-title.js.map +0 -1
  148. package/dist/token-crypto.d.ts.map +0 -1
  149. package/dist/token-crypto.js.map +0 -1
  150. package/dist/token-store.d.ts.map +0 -1
  151. package/dist/token-store.js.map +0 -1
  152. package/dist/types.d.ts.map +0 -1
  153. package/dist/types.js.map +0 -1
  154. package/dist/version.d.ts.map +0 -1
  155. package/dist/version.js.map +0 -1
  156. package/dist/worktree-lifecycle.d.ts.map +0 -1
  157. package/dist/worktree-lifecycle.js.map +0 -1
@@ -1,118 +1,3 @@
1
- /**
2
- * gh#473 PR2 non-clobbering sync output rendering.
3
- *
4
- * The dry-run output is UX-LOAD-BEARING: it must CLEARLY communicate each
5
- * conflict (which role/section or taxonomy class, cube-current vs
6
- * template-new, and how to accept) so the operator SEES what would be
7
- * clobbered. Pure string logic (mirrors `roster-render.ts` /
8
- * `list-roles-render.ts`) so it is unit-testable without the MCP runtime.
9
- *
10
- * The shape mirrors the worker's `NonClobberSyncResult`.
11
- */
12
- /** Truncate long fragment bodies for at-a-glance diffs. */
13
- function trunc(s, n = 200) {
14
- if (s == null)
15
- return '(absent)';
16
- const flat = s.replace(/\n/g, '⏎');
17
- return flat.length > n ? flat.slice(0, n) + '…' : flat;
18
- }
19
- /**
20
- * Render a `NonClobberSyncResult` as an operator-facing markdown report.
21
- *
22
- * Conflicts are the headline: each is surfaced with both sides + its
23
- * stable accept key, and the report states explicitly that conflicts are
24
- * KEPT (the cube's version) unless accepted. ADDs are reported as safe
25
- * auto-applies. Custom roles are reported untouched.
26
- */
27
- export function renderSyncRolesResult(result, templateName) {
28
- // Defensive guard against client/worker deploy skew (gh#9 class). A
29
- // pre-#473 worker returns the legacy sync-roles shape
30
- // ({ updated, added, unchanged, skipped, dryRun }) with no `roles[]`, so
31
- // `result.roles.flatMap(...)` below would throw `undefined.flatMap`.
32
- // Detect the legacy shape and render an actionable message instead of
33
- // crashing — the skew window (0.9.47+ client + pre-#473 worker) becomes a
34
- // clean "redeploy the worker" prompt, not an exception.
35
- const maybeLegacy = result;
36
- if (maybeLegacy.roles === undefined && 'updated' in maybeLegacy) {
37
- return [
38
- `## borg:sync-roles — unavailable (server out of date)`,
39
- ``,
40
- `The borg server returned the legacy sync-roles response shape — it is running a version older than #473, which the non-clobbering sync view does not support.`,
41
- ``,
42
- `**Action:** a server (worker) deploy is pending. Retry \`borg:sync-roles\` once it lands.`,
43
- ].join('\n');
44
- }
45
- const mode = result.dryRun
46
- ? '**DRY RUN** (review conflicts below; re-run with `apply: true` + a `decisions` map to commit)'
47
- : '**APPLIED**';
48
- const lines = [`## borg:sync-roles — ${mode}`, `Template: ${templateName}`, ''];
49
- // Gather all fragments across roles + taxonomy for tallying.
50
- const allFragments = [
51
- ...result.roles.flatMap((r) => r.fragments),
52
- ...result.taxonomy,
53
- ];
54
- const conflicts = allFragments.filter((f) => f.kind === 'conflict');
55
- const adds = allFragments.filter((f) => f.kind === 'add');
56
- const newRoles = result.roles.filter((r) => r.status === 'new');
57
- const customRoles = result.roles.filter((r) => r.status === 'custom-skipped');
58
- // ── Conflicts (the headline — what would be clobbered) ──
59
- if (conflicts.length > 0) {
60
- lines.push(`### ⚠ ${conflicts.length} CONFLICT(s) — these fragments differ between your cube and the template`);
61
- if (result.dryRun) {
62
- lines.push('These differ between your cube and the template — may be because you evolved them, or because the template changed them. ' +
63
- 'Surfaced for review, never silently overwritten. Each defaults to **KEEP (reject)** — your version survives. ' +
64
- 'To take the template version of a specific fragment, pass its key in `decisions` as `"<key>": "accept"`.');
65
- }
66
- else {
67
- lines.push('Unless explicitly accepted, each conflict was KEPT (your version preserved).');
68
- }
69
- lines.push('');
70
- for (const f of conflicts) {
71
- const applied = result.applied.acceptedConflicts.includes(f.key);
72
- const status = result.dryRun
73
- ? '(would KEEP your version)'
74
- : applied
75
- ? '✓ accepted — template version applied'
76
- : '↩ kept your version';
77
- lines.push(`- **${f.label}** \`${f.key}\` ${status}`);
78
- lines.push(` - cube (current): "${trunc(f.cubeValue)}"`);
79
- lines.push(` - template (new): "${trunc(f.templateValue)}"`);
80
- }
81
- lines.push('');
82
- }
83
- // ── Unmatched decision keys (typo'd / stale — intended accept dropped) ──
84
- const unmatched = result.unmatchedDecisions ?? [];
85
- if (unmatched.length > 0) {
86
- lines.push(`### ⚠ ${unmatched.length} decision key(s) matched no conflict and were ignored`);
87
- lines.push('These keys in your `decisions` map did not correspond to any classified conflict this run ' +
88
- '(typo or stale key) — their intended accept had NO effect. Check the exact keys against the conflicts above:');
89
- for (const k of unmatched) {
90
- lines.push(`- \`${k}\``);
91
- }
92
- lines.push('');
93
- }
94
- // ── Additions (safe auto-apply, zero clobber risk) ──
95
- if (newRoles.length > 0 || adds.length > 0) {
96
- lines.push(`### Additions (safe — auto-applied, zero clobber risk)`);
97
- for (const r of newRoles) {
98
- const note = result.dryRun ? '(new role — would be created)' : '✓ created';
99
- lines.push(`- new role **${r.name}** ${note}`);
100
- }
101
- for (const f of adds) {
102
- const note = result.dryRun ? '(would be added)' : '✓ added';
103
- lines.push(`- **${f.label}** \`${f.key}\` ${note}`);
104
- }
105
- lines.push('');
106
- }
107
- // ── Custom roles (never touched) ──
108
- if (customRoles.length > 0) {
109
- lines.push(`### Custom roles (untouched): ${customRoles.map((r) => r.name).join(', ')}`);
110
- lines.push('');
111
- }
112
- // ── Clean no-op ──
113
- if (conflicts.length === 0 && adds.length === 0 && newRoles.length === 0) {
114
- lines.push('✓ Cube roles + taxonomy are **up to date** with the template (no changes).');
115
- }
116
- return lines.join('\n').trimEnd();
117
- }
118
- //# sourceMappingURL=sync-roles-render.js.map
1
+ function p(t,r=200){if(t==null)return"(absent)";const o=t.replace(/\n/g,"\u23CE");return o.length>r?o.slice(0,r)+"\u2026":o}function y(t,r){const o=t;if(o.roles===void 0&&"updated"in o)return["## borg:sync-roles \u2014 unavailable (server out of date)","","The borg server returned the legacy sync-roles response shape \u2014 it is running a version older than #473, which the non-clobbering sync view does not support.","","**Action:** a server (worker) deploy is pending. Retry `borg:sync-roles` once it lands."].join(`
2
+ `);const n=[`## borg:sync-roles \u2014 ${t.dryRun?"**DRY RUN** (review conflicts below; re-run with `apply: true` + a `decisions` map to commit)":"**APPLIED**"}`,`Template: ${r}`,""],d=[...t.roles.flatMap(e=>e.fragments),...t.taxonomy],c=d.filter(e=>e.kind==="conflict"),a=d.filter(e=>e.kind==="add"),i=t.roles.filter(e=>e.status==="new"),u=t.roles.filter(e=>e.status==="custom-skipped");if(c.length>0){n.push(`### \u26A0 ${c.length} CONFLICT(s) \u2014 these fragments differ between your cube and the template`),t.dryRun?n.push('These differ between your cube and the template \u2014 may be because you evolved them, or because the template changed them. Surfaced for review, never silently overwritten. Each defaults to **KEEP (reject)** \u2014 your version survives. To take the template version of a specific fragment, pass its key in `decisions` as `"<key>": "accept"`.'):n.push("Unless explicitly accepted, each conflict was KEPT (your version preserved)."),n.push("");for(const e of c){const s=t.applied.acceptedConflicts.includes(e.key),h=t.dryRun?"(would KEEP your version)":s?"\u2713 accepted \u2014 template version applied":"\u21A9 kept your version";n.push(`- **${e.label}** \`${e.key}\` ${h}`),n.push(` - cube (current): "${p(e.cubeValue)}"`),n.push(` - template (new): "${p(e.templateValue)}"`)}n.push("")}const l=t.unmatchedDecisions??[];if(l.length>0){n.push(`### \u26A0 ${l.length} decision key(s) matched no conflict and were ignored`),n.push("These keys in your `decisions` map did not correspond to any classified conflict this run (typo or stale key) \u2014 their intended accept had NO effect. Check the exact keys against the conflicts above:");for(const e of l)n.push(`- \`${e}\``);n.push("")}if(i.length>0||a.length>0){n.push("### Additions (safe \u2014 auto-applied, zero clobber risk)");for(const e of i){const s=t.dryRun?"(new role \u2014 would be created)":"\u2713 created";n.push(`- new role **${e.name}** ${s}`)}for(const e of a){const s=t.dryRun?"(would be added)":"\u2713 added";n.push(`- **${e.label}** \`${e.key}\` ${s}`)}n.push("")}return u.length>0&&(n.push(`### Custom roles (untouched): ${u.map(e=>e.name).join(", ")}`),n.push("")),c.length===0&&a.length===0&&i.length===0&&n.push("\u2713 Cube roles + taxonomy are **up to date** with the template (no changes)."),n.join(`
3
+ `).trimEnd()}export{y as renderSyncRolesResult};
package/dist/sync.js CHANGED
@@ -1,286 +1,22 @@
1
- /**
2
- * `borg sync` worktree lifecycle management subcommand (gh#33).
3
- *
4
- * Reconciled to the per-worktree `wt-<suffix>` branch model (PR-B,
5
- * ruling ea643b33). Replaces the previous main-centric semantics
6
- * ("idle = on main"; "post-merge = checkout main") — under the approved
7
- * gh#33 model `main` is NEVER a working branch in any worktree; it is
8
- * purely the integration target. Every worktree works on a named
9
- * `wt-<suffix>` branch and `borg sync` keeps it current with
10
- * origin/main, returns to it after a feature branch merges, and absorbs
11
- * upstream into an in-progress feature branch — never touching `main`
12
- * as a checkout.
13
- *
14
- * All git mutation/decision logic is delegated to
15
- * `client/src/worktree-lifecycle.ts` (the seam PR-A added:
16
- * `adoptWorktree`, `syncWorktree`, `cleanupMerged`, `isMerged`,
17
- * `localBranchExists`) so the never-discard guards (dirty / unmerged
18
- * HEAD / unmerged target wt- branch) and the args-array subprocess
19
- * shape are shared, not duplicated.
20
- *
21
- * Lifecycle states:
22
- * 1. dirty — uncommitted changes; refuse (never discard).
23
- * 2. on-wt — on the per-worktree `wt-<suffix>` branch;
24
- * fast-forward it to origin/main (ff-only).
25
- * 3. on-main — on `main`/`master` (or detached at a merged
26
- * point); adopt the `wt-<suffix>` branch
27
- * (Q4: main is never a working branch).
28
- * 4. feature-mid-sprint — on a feature branch not yet merged; absorb
29
- * origin/main into it via `git merge --no-edit`
30
- * (no rebase — cube workflow rule (a)) when
31
- * origin/main advanced; else no-op.
32
- * 5. feature-merged — on a feature branch fully merged into
33
- * origin/main; return to `wt-<suffix>` and
34
- * ANNOUNCE the prunable feature branch (prune
35
- * only with `--prune`, Q3).
36
- *
37
- * Anti-features (intentional, unchanged):
38
- * - No auto-stash / auto-commit / auto-discard on dirty tree.
39
- * - No force-push, no rebase, no --force-with-lease.
40
- * - No remote-branch deletion (Coordinator owns merge actions).
41
- * - Local feature-branch deletion only on explicit `--prune` (Q3).
42
- */
43
- import { spawnSync } from 'node:child_process';
44
- import { basename } from 'node:path';
45
- import chalk from 'chalk';
46
- import { adoptWorktree, syncWorktree, cleanupMerged, isMerged, perWorktreeBranchName, } from './worktree-lifecycle.js';
47
- const defaultDeps = {
48
- runSync: (cmd, args, cwd) => {
49
- const r = spawnSync(cmd, args, { cwd, encoding: 'utf-8' });
50
- return {
51
- status: r.status,
52
- stdout: r.stdout ?? '',
53
- stderr: r.stderr ?? '',
54
- };
55
- },
56
- cwd: () => process.cwd(),
57
- stderr: (line) => process.stderr.write(line),
58
- stdout: (line) => process.stdout.write(line),
59
- };
60
- const DEFAULT_BRANCH = 'origin/main';
61
- // ------------------------------------------------------------------
62
- // wt- branch resolution
63
- // ------------------------------------------------------------------
64
- /**
65
- * Resolve the per-worktree `wt-` branch for the current checkout,
66
- * DETERMINISTICALLY per worktree.
67
- *
68
- * - current branch is already `wt-*` → that is the branch.
69
- * - otherwise → derive `wt-<suffix>` from THIS worktree's directory
70
- * basename minus the main worktree's (repo) basename prefix — the
71
- * same `perWorktreeBranchName` derivation the spawn path uses, so
72
- * the name matches what `borg assimilate --worktree` created.
73
- *
74
- * This must NOT list `git branch --list wt-*`: in linked worktrees the
75
- * local branch namespace is SHARED across all siblings (gh#33 CR-v2
76
- * blocker 32bc45da), so every drone's `wt-` branch is visible and a
77
- * "single match" heuristic is never satisfied in a real multi-drone
78
- * cube. The directory-derivation is unique per worktree and never
79
- * ambiguous. The first `worktree <path>` line of
80
- * `git worktree list --porcelain` is the main worktree (the repo dir);
81
- * its basename is the prefix to strip. For an independent clone the
82
- * main worktree IS this worktree, so the prefix == basename and the
83
- * result is `wt-<basename>` (no strip) — consistent with PR-A's
84
- * in-place adoption.
85
- */
86
- export function resolveWtBranch(runSync, cwd, currentBranch) {
87
- if (currentBranch.startsWith('wt-'))
88
- return currentBranch;
89
- const top = runSync('git', ['rev-parse', '--show-toplevel'], cwd);
90
- const thisDir = top.status === 0 ? top.stdout.trim() : cwd;
91
- const wtList = runSync('git', ['worktree', 'list', '--porcelain'], cwd);
92
- let mainDir = thisDir;
93
- if (wtList.status === 0) {
94
- const firstWorktreeLine = wtList.stdout
95
- .split('\n')
96
- .find((l) => l.startsWith('worktree '));
97
- if (firstWorktreeLine)
98
- mainDir = firstWorktreeLine.slice('worktree '.length).trim();
99
- }
100
- return perWorktreeBranchName(basename(thisDir), basename(mainDir));
101
- }
102
- /**
103
- * Detect the worktree's lifecycle state. Read-only except for the
104
- * `git fetch origin --prune` needed to measure against the latest tip.
105
- */
106
- export function detectState(deps) {
107
- const { runSync, cwd } = deps;
108
- const cwdValue = cwd();
109
- // (1) git repo?
110
- if (runSync('git', ['rev-parse', '--show-toplevel'], cwdValue).status !== 0) {
111
- return { kind: 'error', reason: `not in a git repository (cwd: ${cwdValue})` };
112
- }
113
- // (2) dirty FIRST — never act on uncommitted changes.
114
- const status = runSync('git', ['status', '--porcelain'], cwdValue);
115
- if (status.status !== 0) {
116
- return { kind: 'error', reason: `git status failed: ${status.stderr.trim()}` };
117
- }
118
- const dirty = status.stdout.split('\n').map((l) => l.trim()).filter((l) => l.length > 0);
119
- if (dirty.length > 0)
120
- return { kind: 'dirty', files: dirty };
121
- // (3) current branch.
122
- const branchProbe = runSync('git', ['rev-parse', '--abbrev-ref', 'HEAD'], cwdValue);
123
- if (branchProbe.status !== 0) {
124
- return { kind: 'error', reason: `cannot resolve current branch: ${branchProbe.stderr.trim()}` };
125
- }
126
- const branch = branchProbe.stdout.trim();
127
- if (branch === 'HEAD') {
128
- return { kind: 'error', reason: 'detached HEAD; run `borg assimilate` to adopt a wt- branch first' };
129
- }
130
- // (4) fetch — fatal on failure (can't reason about lifecycle offline).
131
- const fetch = runSync('git', ['fetch', 'origin', '--prune'], cwdValue);
132
- if (fetch.status !== 0) {
133
- return { kind: 'error', reason: `git fetch origin failed: ${fetch.stderr.trim()}` };
134
- }
135
- const wtBranch = resolveWtBranch(runSync, cwdValue, branch);
136
- // (5) on the per-worktree wt- branch.
137
- if (branch.startsWith('wt-')) {
138
- return { kind: 'on-wt', branch, wtBranch: branch };
139
- }
140
- // (6) on main/master — Q4: never a working branch; adopt wt-.
141
- if (branch === 'main' || branch === 'master') {
142
- return { kind: 'on-main', branch, wtBranch };
143
- }
144
- // (7) feature branch — merged vs mid-sprint.
145
- // ASSUMPTION (gh#33 CR NIT 0e19637a): merged-detection keys off commit
146
- // ancestry (`isMerged` = HEAD is an ancestor of origin/main), which
147
- // holds for merge-commit / fast-forward integration. A squash- or
148
- // rebase-merged PR leaves the feature tip a NON-ancestor → it would
149
- // classify here as mid-sprint, not merged. That degrades safely
150
- // (merge-back of origin/main, no discard), and this cube integrates
151
- // via merge commits, so it is not triggered. If squash-merge is ever
152
- // adopted, switch this to PR-merged-state detection.
153
- if (isMerged(runSync, cwdValue, 'HEAD', DEFAULT_BRANCH)) {
154
- return { kind: 'feature-merged', branch, wtBranch };
155
- }
156
- // not merged → mid-sprint. Count how far origin/main advanced past the
157
- // merge-base so the message can report it (0 → no-op).
158
- const mergeBase = runSync('git', ['merge-base', 'HEAD', DEFAULT_BRANCH], cwdValue);
159
- const base = mergeBase.status === 0 ? mergeBase.stdout.trim() : '';
160
- const count = runSync('git', ['rev-list', '--count', `${base}..${DEFAULT_BRANCH}`], cwdValue);
161
- const commits = parseInt(count.stdout.trim(), 10) || 0;
162
- return { kind: 'feature-mid-sprint', branch, wtBranch, commits };
163
- }
164
- // ------------------------------------------------------------------
165
- // Orchestrator
166
- // ------------------------------------------------------------------
167
- export async function runSync(deps = {}, opts = { prune: false }) {
168
- const merged = { ...defaultDeps, ...deps };
169
- const { runSync: run, cwd, stderr, stdout } = merged;
170
- const state = detectState(merged);
171
- if (state.kind === 'error') {
172
- stderr(chalk.red(`◼ borg sync: ${state.reason}\n`));
173
- return 1;
174
- }
175
- if (state.kind === 'dirty') {
176
- stderr(chalk.yellow(`◼ Working tree has uncommitted changes.\n`));
177
- for (const line of state.files.slice(0, 5))
178
- stderr(chalk.gray(` ${line}\n`));
179
- if (state.files.length > 5)
180
- stderr(chalk.gray(` ... and ${state.files.length - 5} more\n`));
181
- stderr(chalk.yellow(`◼ Commit, stash, or restore before running \`borg sync\`. Nothing was changed.\n`));
182
- return 1;
183
- }
184
- // on-wt: fast-forward the per-worktree branch to origin/main (ff-only,
185
- // clean-gated, never merge/rebase). Delegates to syncWorktree.
186
- if (state.kind === 'on-wt') {
187
- const res = syncWorktree(run, cwd(), state.wtBranch, DEFAULT_BRANCH);
188
- if (res.action === 'fast-forwarded') {
189
- stdout(chalk.blue(`◼ On \`${state.wtBranch}\`; fast-forwarded to ${DEFAULT_BRANCH}.\n`));
190
- return 0;
191
- }
192
- if (res.action === 'already-current') {
193
- stdout(chalk.blue(`◼ On \`${state.wtBranch}\`; up to date with ${DEFAULT_BRANCH}.\n`));
194
- return 0;
195
- }
196
- // skipped-diverged: the wt- branch has local commits not on origin/main.
197
- stderr(chalk.yellow(`◼ ${res.message ?? 'sync skipped'}.\n`));
198
- return 1;
199
- }
200
- // on-main: adopt the wt- branch (Q4 — move off main). adoptWorktree
201
- // applies the dirty / unmerged-HEAD / unmerged-target guards.
202
- if (state.kind === 'on-main') {
203
- return adoptAndReport(state.wtBranch, run, cwd, stdout, stderr);
204
- }
205
- // feature-mid-sprint: absorb origin/main into the feature branch (no
206
- // rebase). Leaves the drone on the feature branch to keep working.
207
- if (state.kind === 'feature-mid-sprint') {
208
- if (state.commits === 0) {
209
- stdout(chalk.blue(`◼ On \`${state.branch}\` (feature branch); up to date with ${DEFAULT_BRANCH}.\n`));
210
- stdout(chalk.gray(`◼ Continue your sprint, or post REVIEW-READY when complete.\n`));
211
- return 0;
212
- }
213
- const merge = run('git', ['merge', '--no-edit', DEFAULT_BRANCH], cwd());
214
- if (merge.status !== 0) {
215
- stderr(chalk.red(`◼ borg sync: git merge ${DEFAULT_BRANCH} failed (likely conflict). Resolve manually:\n${merge.stderr.trim()}\n`));
216
- return 1;
217
- }
218
- stdout(chalk.blue(`◼ On \`${state.branch}\`; merged ${state.commits} commit${state.commits === 1 ? '' : 's'} from ${DEFAULT_BRANCH} (no rebase).\n`));
219
- stdout(chalk.gray(`◼ Re-run tests; continue your sprint.\n`));
220
- return 0;
221
- }
222
- // feature-merged: the PR merged. Return to the wt- branch (adopt) and
223
- // announce the prunable feature branch (prune only with --prune, Q3).
224
- if (state.kind === 'feature-merged') {
225
- const feature = state.branch;
226
- const code = adoptAndReport(state.wtBranch, run, cwd, stdout, stderr, {
227
- adoptedPrefix: `◼ \`${feature}\` is merged into ${DEFAULT_BRANCH};`,
228
- });
229
- if (code !== 0)
230
- return code; // adoption blocked (dirty/unmerged target) — don't prune
231
- // Now on the wt- branch — safe to prune/announce the merged feature.
232
- const cleanup = cleanupMerged(run, cwd(), feature, DEFAULT_BRANCH, { prune: opts.prune });
233
- if (cleanup.action === 'pruned') {
234
- stdout(chalk.blue(`◼ Pruned merged branch \`${feature}\`.\n`));
235
- }
236
- else if (cleanup.action === 'announced') {
237
- stdout(chalk.gray(`◼ ${cleanup.message}\n`));
238
- }
239
- return 0;
240
- }
241
- // Exhaustiveness.
242
- const _exhaustive = state;
243
- stderr(chalk.red(`◼ borg sync: unhandled state\n`));
244
- return 1;
245
- }
246
- /**
247
- * Shared adopt-the-wt-branch path for the on-main + feature-merged
248
- * states. Surfaces the never-discard outcomes; returns the process exit
249
- * code (0 adopted, 1 blocked/ambiguous).
250
- */
251
- function adoptAndReport(wtBranch, run, cwd, stdout, stderr, opts = {}) {
252
- const res = adoptWorktree(run, cwd(), wtBranch, DEFAULT_BRANCH);
253
- if (res.action === 'adopted') {
254
- if (opts.adoptedPrefix) {
255
- stdout(chalk.blue(`${opts.adoptedPrefix} switched to \`${wtBranch}\` at ${DEFAULT_BRANCH}.\n`));
256
- }
257
- else {
258
- stdout(chalk.blue(`◼ On \`${wtBranch}\` at ${DEFAULT_BRANCH}.\n`));
259
- }
260
- return 0;
261
- }
262
- // skipped-dirty handled upstream (dirty state), but defensively surface.
263
- stderr(chalk.yellow(`◼ borg sync: ${res.message ?? 'not adopted'}. Nothing was changed.\n`));
264
- return 1;
265
- }
266
- /**
267
- * Parse args after `borg sync`. Supports `--prune` (Q3: delete a merged
268
- * feature branch after returning to the wt- branch). Rejects anything
269
- * else to keep room for future flags.
270
- */
271
- export function parseSyncArgs(rawArgs) {
272
- let prune = false;
273
- for (const arg of rawArgs) {
274
- if (arg === '--prune') {
275
- prune = true;
276
- }
277
- else {
278
- return {
279
- ok: false,
280
- error: `unexpected argument: ${arg}. Usage: borg sync [--prune]`,
281
- };
282
- }
283
- }
284
- return { ok: true, options: { prune } };
285
- }
286
- //# sourceMappingURL=sync.js.map
1
+ import{spawnSync as y}from"node:child_process";import{basename as p}from"node:path";import o from"chalk";import{adoptWorktree as b,syncWorktree as k,cleanupMerged as v,isMerged as A,perWorktreeBranchName as D}from"./worktree-lifecycle.js";const W={runSync:(i,e,u)=>{const r=y(i,e,{cwd:u,encoding:"utf-8"});return{status:r.status,stdout:r.stdout??"",stderr:r.stderr??""}},cwd:()=>process.cwd(),stderr:i=>process.stderr.write(i),stdout:i=>process.stdout.write(i)},c="origin/main";function x(i,e,u){if(u.startsWith("wt-"))return u;const r=i("git",["rev-parse","--show-toplevel"],e),d=r.status===0?r.stdout.trim():e,s=i("git",["worktree","list","--porcelain"],e);let n=d;if(s.status===0){const t=s.stdout.split(`
2
+ `).find(f=>f.startsWith("worktree "));t&&(n=t.slice(9).trim())}return D(p(d),p(n))}function E(i){const{runSync:e,cwd:u}=i,r=u();if(e("git",["rev-parse","--show-toplevel"],r).status!==0)return{kind:"error",reason:`not in a git repository (cwd: ${r})`};const d=e("git",["status","--porcelain"],r);if(d.status!==0)return{kind:"error",reason:`git status failed: ${d.stderr.trim()}`};const s=d.stdout.split(`
3
+ `).map(g=>g.trim()).filter(g=>g.length>0);if(s.length>0)return{kind:"dirty",files:s};const n=e("git",["rev-parse","--abbrev-ref","HEAD"],r);if(n.status!==0)return{kind:"error",reason:`cannot resolve current branch: ${n.stderr.trim()}`};const t=n.stdout.trim();if(t==="HEAD")return{kind:"error",reason:"detached HEAD; run `borg assimilate` to adopt a wt- branch first"};const f=e("git",["fetch","origin","--prune"],r);if(f.status!==0)return{kind:"error",reason:`git fetch origin failed: ${f.stderr.trim()}`};const a=x(e,r,t);if(t.startsWith("wt-"))return{kind:"on-wt",branch:t,wtBranch:t};if(t==="main"||t==="master")return{kind:"on-main",branch:t,wtBranch:a};if(A(e,r,"HEAD",c))return{kind:"feature-merged",branch:t,wtBranch:a};const l=e("git",["merge-base","HEAD",c],r),m=l.status===0?l.stdout.trim():"",w=e("git",["rev-list","--count",`${m}..${c}`],r),$=parseInt(w.stdout.trim(),10)||0;return{kind:"feature-mid-sprint",branch:t,wtBranch:a,commits:$}}async function O(i={},e={prune:!1}){const u={...W,...i},{runSync:r,cwd:d,stderr:s,stdout:n}=u,t=E(u);if(t.kind==="error")return s(o.red(`\u25FC borg sync: ${t.reason}
4
+ `)),1;if(t.kind==="dirty"){s(o.yellow(`\u25FC Working tree has uncommitted changes.
5
+ `));for(const a of t.files.slice(0,5))s(o.gray(` ${a}
6
+ `));return t.files.length>5&&s(o.gray(` ... and ${t.files.length-5} more
7
+ `)),s(o.yellow("\u25FC Commit, stash, or restore before running `borg sync`. Nothing was changed.\n")),1}if(t.kind==="on-wt"){const a=k(r,d(),t.wtBranch,c);return a.action==="fast-forwarded"?(n(o.blue(`\u25FC On \`${t.wtBranch}\`; fast-forwarded to ${c}.
8
+ `)),0):a.action==="already-current"?(n(o.blue(`\u25FC On \`${t.wtBranch}\`; up to date with ${c}.
9
+ `)),0):(s(o.yellow(`\u25FC ${a.message??"sync skipped"}.
10
+ `)),1)}if(t.kind==="on-main")return h(t.wtBranch,r,d,n,s);if(t.kind==="feature-mid-sprint"){if(t.commits===0)return n(o.blue(`\u25FC On \`${t.branch}\` (feature branch); up to date with ${c}.
11
+ `)),n(o.gray(`\u25FC Continue your sprint, or post REVIEW-READY when complete.
12
+ `)),0;const a=r("git",["merge","--no-edit",c],d());return a.status!==0?(s(o.red(`\u25FC borg sync: git merge ${c} failed (likely conflict). Resolve manually:
13
+ ${a.stderr.trim()}
14
+ `)),1):(n(o.blue(`\u25FC On \`${t.branch}\`; merged ${t.commits} commit${t.commits===1?"":"s"} from ${c} (no rebase).
15
+ `)),n(o.gray(`\u25FC Re-run tests; continue your sprint.
16
+ `)),0)}if(t.kind==="feature-merged"){const a=t.branch,l=h(t.wtBranch,r,d,n,s,{adoptedPrefix:`\u25FC \`${a}\` is merged into ${c};`});if(l!==0)return l;const m=v(r,d(),a,c,{prune:e.prune});return m.action==="pruned"?n(o.blue(`\u25FC Pruned merged branch \`${a}\`.
17
+ `)):m.action==="announced"&&n(o.gray(`\u25FC ${m.message}
18
+ `)),0}const f=t;return s(o.red(`\u25FC borg sync: unhandled state
19
+ `)),1}function h(i,e,u,r,d,s={}){const n=b(e,u(),i,c);return n.action==="adopted"?(s.adoptedPrefix?r(o.blue(`${s.adoptedPrefix} switched to \`${i}\` at ${c}.
20
+ `)):r(o.blue(`\u25FC On \`${i}\` at ${c}.
21
+ `)),0):(d(o.yellow(`\u25FC borg sync: ${n.message??"not adopted"}. Nothing was changed.
22
+ `)),1)}function P(i){let e=!1;for(const u of i)if(u==="--prune")e=!0;else return{ok:!1,error:`unexpected argument: ${u}. Usage: borg sync [--prune]`};return{ok:!0,options:{prune:e}}}export{E as detectState,P as parseSyncArgs,x as resolveWtBranch,O as runSync};