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.
- package/dist/assimilate-cmd.js +39 -497
- package/dist/assimilate-deps.js +3 -177
- package/dist/assimilate-welcome.js +2 -24
- package/dist/auth-env.js +1 -107
- package/dist/auth.js +23 -612
- package/dist/claude.js +11 -281
- package/dist/cli-help.js +29 -50
- package/dist/cli-platform.js +4 -94
- package/dist/codex-app-server.js +4 -228
- package/dist/codex-app-wake.js +2 -122
- package/dist/codex-launch.js +1 -81
- package/dist/codex-remote.js +1 -250
- package/dist/config-utils.js +3 -385
- package/dist/config.js +1 -190
- package/dist/console-prefix.js +1 -86
- package/dist/cube-name.js +1 -65
- package/dist/cubes.js +4 -269
- package/dist/debug.js +1 -71
- package/dist/device-auth.js +1 -167
- package/dist/direct-log.js +1 -11
- package/dist/health-beat.js +1 -168
- package/dist/inbox-monitor.js +1 -129
- package/dist/index.js +26 -1378
- package/dist/lifecycle-log-guard.js +2 -93
- package/dist/list-roles-render.js +6 -39
- package/dist/log-audit.js +3 -186
- package/dist/log-stream.js +9 -848
- package/dist/name-validator.js +1 -22
- package/dist/parse-assimilate-args.js +1 -82
- package/dist/postinstall.js +8 -22
- package/dist/regen-format.js +11 -329
- package/dist/regen.js +5 -83
- package/dist/remote-client.js +1 -695
- package/dist/role-resolver.js +1 -36
- package/dist/role-section.js +8 -208
- package/dist/roster-render.js +3 -96
- package/dist/setup.js +36 -251
- package/dist/shell-escape.js +1 -22
- package/dist/spawn.js +10 -29
- package/dist/stale-version-check.js +1 -102
- package/dist/stream-owner.js +2 -202
- package/dist/stream-status.js +3 -211
- package/dist/subscription-retry.js +1 -23
- package/dist/sync-roles-render.js +3 -118
- package/dist/sync.js +22 -286
- package/dist/templates.js +120 -563
- package/dist/terminal-title.js +1 -68
- package/dist/token-crypto.js +1 -91
- package/dist/token-store.js +1 -222
- package/dist/types.js +0 -5
- package/dist/version.js +2 -78
- package/dist/worktree-lifecycle.js +2 -173
- package/package.json +11 -2
- package/dist/assimilate-cmd.d.ts.map +0 -1
- package/dist/assimilate-cmd.js.map +0 -1
- package/dist/assimilate-deps.d.ts.map +0 -1
- package/dist/assimilate-deps.js.map +0 -1
- package/dist/assimilate-welcome.d.ts.map +0 -1
- package/dist/assimilate-welcome.js.map +0 -1
- package/dist/auth-env.d.ts.map +0 -1
- package/dist/auth-env.js.map +0 -1
- package/dist/auth.d.ts.map +0 -1
- package/dist/auth.js.map +0 -1
- package/dist/claude.d.ts.map +0 -1
- package/dist/claude.js.map +0 -1
- package/dist/cli-help.d.ts.map +0 -1
- package/dist/cli-help.js.map +0 -1
- package/dist/cli-platform.d.ts.map +0 -1
- package/dist/cli-platform.js.map +0 -1
- package/dist/codex-app-server.d.ts.map +0 -1
- package/dist/codex-app-server.js.map +0 -1
- package/dist/codex-app-wake.d.ts.map +0 -1
- package/dist/codex-app-wake.js.map +0 -1
- package/dist/codex-launch.d.ts.map +0 -1
- package/dist/codex-launch.js.map +0 -1
- package/dist/codex-remote.d.ts.map +0 -1
- package/dist/codex-remote.js.map +0 -1
- package/dist/config-utils.d.ts.map +0 -1
- package/dist/config-utils.js.map +0 -1
- package/dist/config.d.ts.map +0 -1
- package/dist/config.js.map +0 -1
- package/dist/console-prefix.d.ts.map +0 -1
- package/dist/console-prefix.js.map +0 -1
- package/dist/cube-name.d.ts.map +0 -1
- package/dist/cube-name.js.map +0 -1
- package/dist/cubes.d.ts.map +0 -1
- package/dist/cubes.js.map +0 -1
- package/dist/debug.d.ts.map +0 -1
- package/dist/debug.js.map +0 -1
- package/dist/device-auth.d.ts.map +0 -1
- package/dist/device-auth.js.map +0 -1
- package/dist/direct-log.d.ts.map +0 -1
- package/dist/direct-log.js.map +0 -1
- package/dist/health-beat.d.ts.map +0 -1
- package/dist/health-beat.js.map +0 -1
- package/dist/inbox-monitor.d.ts.map +0 -1
- package/dist/inbox-monitor.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/lifecycle-log-guard.d.ts.map +0 -1
- package/dist/lifecycle-log-guard.js.map +0 -1
- package/dist/list-roles-render.d.ts.map +0 -1
- package/dist/list-roles-render.js.map +0 -1
- package/dist/log-audit.d.ts.map +0 -1
- package/dist/log-audit.js.map +0 -1
- package/dist/log-stream.d.ts.map +0 -1
- package/dist/log-stream.js.map +0 -1
- package/dist/name-validator.d.ts.map +0 -1
- package/dist/name-validator.js.map +0 -1
- package/dist/parse-assimilate-args.d.ts.map +0 -1
- package/dist/parse-assimilate-args.js.map +0 -1
- package/dist/postinstall.d.ts.map +0 -1
- package/dist/postinstall.js.map +0 -1
- package/dist/regen-format.d.ts.map +0 -1
- package/dist/regen-format.js.map +0 -1
- package/dist/regen.d.ts.map +0 -1
- package/dist/regen.js.map +0 -1
- package/dist/remote-client.d.ts.map +0 -1
- package/dist/remote-client.js.map +0 -1
- package/dist/role-resolver.d.ts.map +0 -1
- package/dist/role-resolver.js.map +0 -1
- package/dist/role-section.d.ts.map +0 -1
- package/dist/role-section.js.map +0 -1
- package/dist/roster-render.d.ts.map +0 -1
- package/dist/roster-render.js.map +0 -1
- package/dist/setup.d.ts.map +0 -1
- package/dist/setup.js.map +0 -1
- package/dist/shell-escape.d.ts.map +0 -1
- package/dist/shell-escape.js.map +0 -1
- package/dist/spawn.d.ts.map +0 -1
- package/dist/spawn.js.map +0 -1
- package/dist/stale-version-check.d.ts.map +0 -1
- package/dist/stale-version-check.js.map +0 -1
- package/dist/stream-owner.d.ts.map +0 -1
- package/dist/stream-owner.js.map +0 -1
- package/dist/stream-status.d.ts.map +0 -1
- package/dist/stream-status.js.map +0 -1
- package/dist/subscription-retry.d.ts.map +0 -1
- package/dist/subscription-retry.js.map +0 -1
- package/dist/sync-roles-render.d.ts.map +0 -1
- package/dist/sync-roles-render.js.map +0 -1
- package/dist/sync.d.ts.map +0 -1
- package/dist/sync.js.map +0 -1
- package/dist/templates.d.ts.map +0 -1
- package/dist/templates.js.map +0 -1
- package/dist/terminal-title.d.ts.map +0 -1
- package/dist/terminal-title.js.map +0 -1
- package/dist/token-crypto.d.ts.map +0 -1
- package/dist/token-crypto.js.map +0 -1
- package/dist/token-store.d.ts.map +0 -1
- package/dist/token-store.js.map +0 -1
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js.map +0 -1
- package/dist/version.d.ts.map +0 -1
- package/dist/version.js.map +0 -1
- package/dist/worktree-lifecycle.d.ts.map +0 -1
- package/dist/worktree-lifecycle.js.map +0 -1
|
@@ -1,118 +1,3 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
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
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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};
|