borgmcp 1.0.6 → 1.0.8

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 (160) hide show
  1. package/README.md +5 -3
  2. package/dist/assimilate-cmd.js +39 -511
  3. package/dist/assimilate-deps.js +3 -177
  4. package/dist/assimilate-welcome.js +2 -24
  5. package/dist/auth-env.js +1 -107
  6. package/dist/auth.js +23 -612
  7. package/dist/claude.js +11 -281
  8. package/dist/cli-help.js +29 -50
  9. package/dist/cli-platform.js +4 -94
  10. package/dist/codex-app-server.js +4 -228
  11. package/dist/codex-app-wake.js +2 -122
  12. package/dist/codex-launch.js +1 -81
  13. package/dist/codex-remote.js +1 -250
  14. package/dist/config-utils.js +3 -385
  15. package/dist/config.js +1 -190
  16. package/dist/console-prefix.js +1 -86
  17. package/dist/cube-name.js +1 -65
  18. package/dist/cubes.js +4 -269
  19. package/dist/debug.js +1 -71
  20. package/dist/device-auth.js +1 -167
  21. package/dist/direct-log.js +1 -11
  22. package/dist/health-beat.js +1 -168
  23. package/dist/inbox-monitor.js +1 -129
  24. package/dist/index.js +26 -1378
  25. package/dist/lifecycle-log-guard.js +2 -93
  26. package/dist/list-roles-render.js +6 -39
  27. package/dist/log-audit.js +3 -186
  28. package/dist/log-stream.js +9 -848
  29. package/dist/name-validator.js +1 -22
  30. package/dist/parse-assimilate-args.js +1 -82
  31. package/dist/postinstall.js +8 -22
  32. package/dist/regen-format.js +11 -337
  33. package/dist/regen.js +5 -83
  34. package/dist/remote-client.d.ts +4 -7
  35. package/dist/remote-client.js +1 -695
  36. package/dist/role-resolver.js +1 -36
  37. package/dist/role-section.js +8 -208
  38. package/dist/roster-render.js +3 -96
  39. package/dist/setup.js +41 -251
  40. package/dist/shell-escape.js +1 -22
  41. package/dist/spawn.js +10 -29
  42. package/dist/stale-version-check.js +1 -102
  43. package/dist/stream-owner.js +2 -202
  44. package/dist/stream-status.js +3 -211
  45. package/dist/subscription-retry.js +1 -23
  46. package/dist/sync-roles-render.js +3 -118
  47. package/dist/sync.js +22 -286
  48. package/dist/templates.js +120 -626
  49. package/dist/terminal-title.js +1 -68
  50. package/dist/token-crypto.js +1 -91
  51. package/dist/token-store.js +1 -222
  52. package/dist/types.d.ts +0 -5
  53. package/dist/types.js +0 -5
  54. package/dist/version.js +2 -78
  55. package/dist/worktree-lifecycle.js +2 -173
  56. package/package.json +12 -2
  57. package/dist/assimilate-cmd.d.ts.map +0 -1
  58. package/dist/assimilate-cmd.js.map +0 -1
  59. package/dist/assimilate-deps.d.ts.map +0 -1
  60. package/dist/assimilate-deps.js.map +0 -1
  61. package/dist/assimilate-welcome.d.ts.map +0 -1
  62. package/dist/assimilate-welcome.js.map +0 -1
  63. package/dist/auth-env.d.ts.map +0 -1
  64. package/dist/auth-env.js.map +0 -1
  65. package/dist/auth.d.ts.map +0 -1
  66. package/dist/auth.js.map +0 -1
  67. package/dist/claude.d.ts.map +0 -1
  68. package/dist/claude.js.map +0 -1
  69. package/dist/cli-help.d.ts.map +0 -1
  70. package/dist/cli-help.js.map +0 -1
  71. package/dist/cli-platform.d.ts.map +0 -1
  72. package/dist/cli-platform.js.map +0 -1
  73. package/dist/codex-app-server.d.ts.map +0 -1
  74. package/dist/codex-app-server.js.map +0 -1
  75. package/dist/codex-app-wake.d.ts.map +0 -1
  76. package/dist/codex-app-wake.js.map +0 -1
  77. package/dist/codex-launch.d.ts.map +0 -1
  78. package/dist/codex-launch.js.map +0 -1
  79. package/dist/codex-remote.d.ts.map +0 -1
  80. package/dist/codex-remote.js.map +0 -1
  81. package/dist/config-utils.d.ts.map +0 -1
  82. package/dist/config-utils.js.map +0 -1
  83. package/dist/config.d.ts.map +0 -1
  84. package/dist/config.js.map +0 -1
  85. package/dist/console-prefix.d.ts.map +0 -1
  86. package/dist/console-prefix.js.map +0 -1
  87. package/dist/cube-name.d.ts.map +0 -1
  88. package/dist/cube-name.js.map +0 -1
  89. package/dist/cubes.d.ts.map +0 -1
  90. package/dist/cubes.js.map +0 -1
  91. package/dist/debug.d.ts.map +0 -1
  92. package/dist/debug.js.map +0 -1
  93. package/dist/device-auth.d.ts.map +0 -1
  94. package/dist/device-auth.js.map +0 -1
  95. package/dist/direct-log.d.ts.map +0 -1
  96. package/dist/direct-log.js.map +0 -1
  97. package/dist/health-beat.d.ts.map +0 -1
  98. package/dist/health-beat.js.map +0 -1
  99. package/dist/inbox-monitor.d.ts.map +0 -1
  100. package/dist/inbox-monitor.js.map +0 -1
  101. package/dist/index.d.ts.map +0 -1
  102. package/dist/index.js.map +0 -1
  103. package/dist/lifecycle-log-guard.d.ts.map +0 -1
  104. package/dist/lifecycle-log-guard.js.map +0 -1
  105. package/dist/list-roles-render.d.ts.map +0 -1
  106. package/dist/list-roles-render.js.map +0 -1
  107. package/dist/log-audit.d.ts.map +0 -1
  108. package/dist/log-audit.js.map +0 -1
  109. package/dist/log-stream.d.ts.map +0 -1
  110. package/dist/log-stream.js.map +0 -1
  111. package/dist/name-validator.d.ts.map +0 -1
  112. package/dist/name-validator.js.map +0 -1
  113. package/dist/parse-assimilate-args.d.ts.map +0 -1
  114. package/dist/parse-assimilate-args.js.map +0 -1
  115. package/dist/postinstall.d.ts.map +0 -1
  116. package/dist/postinstall.js.map +0 -1
  117. package/dist/regen-format.d.ts.map +0 -1
  118. package/dist/regen-format.js.map +0 -1
  119. package/dist/regen.d.ts.map +0 -1
  120. package/dist/regen.js.map +0 -1
  121. package/dist/remote-client.d.ts.map +0 -1
  122. package/dist/remote-client.js.map +0 -1
  123. package/dist/role-resolver.d.ts.map +0 -1
  124. package/dist/role-resolver.js.map +0 -1
  125. package/dist/role-section.d.ts.map +0 -1
  126. package/dist/role-section.js.map +0 -1
  127. package/dist/roster-render.d.ts.map +0 -1
  128. package/dist/roster-render.js.map +0 -1
  129. package/dist/setup.d.ts.map +0 -1
  130. package/dist/setup.js.map +0 -1
  131. package/dist/shell-escape.d.ts.map +0 -1
  132. package/dist/shell-escape.js.map +0 -1
  133. package/dist/spawn.d.ts.map +0 -1
  134. package/dist/spawn.js.map +0 -1
  135. package/dist/stale-version-check.d.ts.map +0 -1
  136. package/dist/stale-version-check.js.map +0 -1
  137. package/dist/stream-owner.d.ts.map +0 -1
  138. package/dist/stream-owner.js.map +0 -1
  139. package/dist/stream-status.d.ts.map +0 -1
  140. package/dist/stream-status.js.map +0 -1
  141. package/dist/subscription-retry.d.ts.map +0 -1
  142. package/dist/subscription-retry.js.map +0 -1
  143. package/dist/sync-roles-render.d.ts.map +0 -1
  144. package/dist/sync-roles-render.js.map +0 -1
  145. package/dist/sync.d.ts.map +0 -1
  146. package/dist/sync.js.map +0 -1
  147. package/dist/templates.d.ts.map +0 -1
  148. package/dist/templates.js.map +0 -1
  149. package/dist/terminal-title.d.ts.map +0 -1
  150. package/dist/terminal-title.js.map +0 -1
  151. package/dist/token-crypto.d.ts.map +0 -1
  152. package/dist/token-crypto.js.map +0 -1
  153. package/dist/token-store.d.ts.map +0 -1
  154. package/dist/token-store.js.map +0 -1
  155. package/dist/types.d.ts.map +0 -1
  156. package/dist/types.js.map +0 -1
  157. package/dist/version.d.ts.map +0 -1
  158. package/dist/version.js.map +0 -1
  159. package/dist/worktree-lifecycle.d.ts.map +0 -1
  160. package/dist/worktree-lifecycle.js.map +0 -1
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};