baldart 4.6.0 → 4.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +28 -0
- package/README.md +23 -11
- package/VERSION +1 -1
- package/framework/.claude/commands/codexreview.md +14 -6
- package/framework/.claude/skills/baldart-update/SKILL.md +112 -163
- package/framework/.claude/skills/new/SKILL.md +36 -16
- package/package.json +1 -1
- package/src/commands/overlay.js +1 -1
- package/src/commands/update.js +261 -126
- package/src/utils/__tests__/overlay-capture.test.js +215 -0
- package/src/utils/git.js +0 -15
- package/src/utils/overlay-capture.js +225 -0
package/src/commands/update.js
CHANGED
|
@@ -4,6 +4,7 @@ const UI = require('../utils/ui');
|
|
|
4
4
|
const State = require('../utils/state');
|
|
5
5
|
const Hooks = require('../utils/hooks');
|
|
6
6
|
const UpdateNotifier = require('../utils/update-notifier');
|
|
7
|
+
const { captureAndVerify } = require('../utils/overlay-capture');
|
|
7
8
|
|
|
8
9
|
function readEnabledTools(cwd = process.cwd()) {
|
|
9
10
|
try {
|
|
@@ -115,6 +116,28 @@ function isBaldartManagedPath(p) {
|
|
|
115
116
|
return BALDART_MANAGED_PATTERNS.some((rx) => rx.test(p));
|
|
116
117
|
}
|
|
117
118
|
|
|
119
|
+
// Real preview (v4.8.0+) — the old `git log HEAD..FETCH_HEAD -- .framework/`
|
|
120
|
+
// is always empty under subtree-squash, so the user never saw what changed.
|
|
121
|
+
// Instead, read the UPSTREAM CHANGELOG and return every entry above the
|
|
122
|
+
// version we already have. Best-effort; returns null when unavailable.
|
|
123
|
+
async function changelogSinceInstalled(git, installedVersion) {
|
|
124
|
+
let text;
|
|
125
|
+
try { text = await git.git.raw(['show', 'FETCH_HEAD:CHANGELOG.md']); }
|
|
126
|
+
catch (_) { return null; }
|
|
127
|
+
const out = [];
|
|
128
|
+
let started = false;
|
|
129
|
+
for (const line of text.split('\n')) {
|
|
130
|
+
const m = /^##\s+\[?v?(\d+\.\d+\.\d+)\]?/.exec(line);
|
|
131
|
+
if (m) {
|
|
132
|
+
if (m[1] === installedVersion) break; // reached the version already on disk
|
|
133
|
+
started = true;
|
|
134
|
+
}
|
|
135
|
+
if (started) out.push(line);
|
|
136
|
+
if (out.length > 120) { out.push(' … (truncated — see .framework/CHANGELOG.md)'); break; }
|
|
137
|
+
}
|
|
138
|
+
return out.join('\n').trim() || null;
|
|
139
|
+
}
|
|
140
|
+
|
|
118
141
|
async function postUpdateAutoCommit(git, newVersion, options) {
|
|
119
142
|
if (options && options.commit === false) {
|
|
120
143
|
UI.info('Auto-commit skipped (--no-commit).');
|
|
@@ -185,6 +208,129 @@ async function postUpdateAutoCommit(git, newVersion, options) {
|
|
|
185
208
|
}
|
|
186
209
|
}
|
|
187
210
|
|
|
211
|
+
// Single auto-stash engine (v4.8.0+). The ONE place that protects a dirty
|
|
212
|
+
// working tree before a `git subtree pull` (normal flow) OR a `--reset`
|
|
213
|
+
// reinstall. Uses a blanket `git stash push -u` with NO pathspec — a pathspec
|
|
214
|
+
// that lands on a `.gitignore`d directory is exactly what made the previous
|
|
215
|
+
// manual-stash workaround fail. Tracked-but-ignored files (a `.gitignore` that
|
|
216
|
+
// post-dates the file) are still stashed by plain stash; purely-ignored
|
|
217
|
+
// untracked files don't block the pull and aren't added by `git add -A`, so
|
|
218
|
+
// leaving them in place is harmless.
|
|
219
|
+
//
|
|
220
|
+
// Returns { stashRef } on success (or { stashRef: null } when already clean),
|
|
221
|
+
// { aborted: true } when the user declined, or { error } when the stash failed.
|
|
222
|
+
async function autoStashNonFramework(git, { autoStash, interactive }) {
|
|
223
|
+
const isClean = await git.hasCleanWorkingTree();
|
|
224
|
+
if (isClean) return { stashRef: null };
|
|
225
|
+
|
|
226
|
+
UI.newline();
|
|
227
|
+
UI.warning('Working tree has uncommitted changes — they will be stashed and re-applied after the update.');
|
|
228
|
+
const dirty = await git.git.status();
|
|
229
|
+
const dirtyPaths = [
|
|
230
|
+
...dirty.not_added, ...dirty.modified, ...dirty.created,
|
|
231
|
+
...dirty.deleted, ...dirty.renamed.map((r) => r.to), ...dirty.staged
|
|
232
|
+
];
|
|
233
|
+
const unique = [...new Set(dirtyPaths)];
|
|
234
|
+
UI.info(`${unique.length} dirty path(s):`);
|
|
235
|
+
UI.list(unique.slice(0, 8), 'yellow');
|
|
236
|
+
if (unique.length > 8) UI.info(`…and ${unique.length - 8} more.`);
|
|
237
|
+
|
|
238
|
+
let action = 'stash';
|
|
239
|
+
if (!autoStash && interactive) {
|
|
240
|
+
action = await UI.select('How would you like to proceed?', [
|
|
241
|
+
{ name: 'Auto-stash now and re-apply after the update (recommended)', value: 'stash' },
|
|
242
|
+
{ name: 'Abort — I want to commit or stash manually first', value: 'abort' }
|
|
243
|
+
]);
|
|
244
|
+
} else {
|
|
245
|
+
UI.info('Auto-stashing dirty paths.');
|
|
246
|
+
}
|
|
247
|
+
if (action === 'abort') return { aborted: true };
|
|
248
|
+
|
|
249
|
+
const stashRef = `baldart-pre-update-${new Date().toISOString().replace(/[:.]/g, '-')}`;
|
|
250
|
+
try {
|
|
251
|
+
await git.git.stash(['push', '-u', '-m', stashRef]);
|
|
252
|
+
UI.success(`Stashed working tree as "${stashRef}".`);
|
|
253
|
+
return { stashRef };
|
|
254
|
+
} catch (err) {
|
|
255
|
+
return { error: err.message };
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Seamless resolution of `overlay-able` divergence (v4.8.0+). The user's intent
|
|
260
|
+
// is always the same — update and preserve overlays — so this never dead-ends:
|
|
261
|
+
// - every divergent edit verifiably preserved by an overlay (existing or
|
|
262
|
+
// freshly captured) → reset-and-reapply (the `.framework/` commits are
|
|
263
|
+
// redundant; the overlays survive and re-apply on reinstall).
|
|
264
|
+
// - anything that can't be captured+verified (stale overlay, section deletion,
|
|
265
|
+
// frontmatter/preamble change, skill with no overlay, unmappable path) →
|
|
266
|
+
// stop ONCE with the exact list, so nothing is silently lost.
|
|
267
|
+
// On success this calls runReset (which exits the process). On an unrecoverable
|
|
268
|
+
// blocker it emits the single-pause refuse and exits.
|
|
269
|
+
//
|
|
270
|
+
// Pass the FULL touched set to captureAndVerify — never a pre-filtered subset.
|
|
271
|
+
// An overlay-able commit (per classifyCommitByPaths) only touches files under
|
|
272
|
+
// `.claude/{agents,commands,skills}/`, but some (nested agent files, etc.) are
|
|
273
|
+
// not overlay-mappable; pre-filtering them out would let the reset wipe them
|
|
274
|
+
// with no overlay to preserve them. captureAndVerify turns those into blockers.
|
|
275
|
+
async function resolveOverlayDivergenceSeamlessly(git, status, options, autoYes, overlayCommits, remoteVersion) {
|
|
276
|
+
const cwd = process.cwd();
|
|
277
|
+
const touched = [...new Set(overlayCommits.flatMap((c) => c.touched || []))];
|
|
278
|
+
|
|
279
|
+
UI.newline();
|
|
280
|
+
UI.info(`Verifying ${touched.length} divergent edit(s) are preserved by overlays (auto-capture + verify)…`);
|
|
281
|
+
const { captured, blockers } = await captureAndVerify({
|
|
282
|
+
cwd, git, files: touched, frameworkVersion: remoteVersion,
|
|
283
|
+
});
|
|
284
|
+
for (const o of captured) UI.success(`Overlay verified: ${o}`);
|
|
285
|
+
|
|
286
|
+
// Commit captured overlays NOW, before the reset. The reset auto-stashes the
|
|
287
|
+
// working tree, and the symlink/merge step reads overlays FROM DISK — an
|
|
288
|
+
// untracked, stashed overlay would be invisible to the merge and the
|
|
289
|
+
// regenerated agent would silently drop the customization. Committing pins
|
|
290
|
+
// them into HEAD so they survive the reset and feed the merge. (Already-
|
|
291
|
+
// committed, unchanged overlays stage to nothing and are skipped.)
|
|
292
|
+
if (captured.length) {
|
|
293
|
+
try {
|
|
294
|
+
await git.git.add(captured);
|
|
295
|
+
const st = await git.git.status();
|
|
296
|
+
if (st.staged.length) {
|
|
297
|
+
await git.git.commit(
|
|
298
|
+
'chore(baldart): capture/verify overlays for divergent framework edits\n\n'
|
|
299
|
+
+ captured.map((c) => ` - ${c}`).join('\n')
|
|
300
|
+
);
|
|
301
|
+
UI.success(`Committed ${st.staged.length} overlay file(s).`);
|
|
302
|
+
}
|
|
303
|
+
} catch (err) {
|
|
304
|
+
UI.warning(`Could not commit overlays: ${err.message} — they remain in the working tree.`);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
if (blockers.length) {
|
|
309
|
+
UI.newline();
|
|
310
|
+
UI.error(`${blockers.length} edit(s) could not be preserved+verified — stopping so nothing is lost:`);
|
|
311
|
+
UI.list(blockers.map((b) => `${b.file} — ${b.reason}`), 'yellow');
|
|
312
|
+
UI.newline();
|
|
313
|
+
UI.info('Resolve these, then re-run `npx baldart update`:');
|
|
314
|
+
UI.list([
|
|
315
|
+
'Author / reconcile the overlay with `/overlay` (Claude Code) or `npx baldart overlay scaffold`.',
|
|
316
|
+
'Or contribute the edit upstream with `/baldart-push` if it is a generic improvement.',
|
|
317
|
+
'Or keep the commits and merge with `npx baldart update --on-divergence pull`.',
|
|
318
|
+
], 'cyan');
|
|
319
|
+
emitUpdateJson({ ok: false, action: 'divergence-capture-incomplete',
|
|
320
|
+
divergence_class: status.divergenceClass,
|
|
321
|
+
captured, blockers,
|
|
322
|
+
installed_before: status.installedVersion, remote_version: status.remoteVersion,
|
|
323
|
+
reason: `Preserved ${captured.length} edit(s); ${blockers.length} could not be captured+verified — stopped to avoid data loss.`,
|
|
324
|
+
next_command: 'npx baldart update --on-divergence pull' }, 1);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
UI.newline();
|
|
328
|
+
UI.success('All divergent edits are preserved & verified in `.baldart/overlays/` — resolving via clean reinstall (overlays are re-applied).');
|
|
329
|
+
// iKnow is implied: the only files reset deletes inside `.framework/` are
|
|
330
|
+
// framework artifacts that get reinstalled, and every edit is now overlaid.
|
|
331
|
+
await runReset(git, { ...options, reset: true, iKnow: true }, autoYes);
|
|
332
|
+
}
|
|
333
|
+
|
|
188
334
|
// --reset nuclear option (v3.26.0+).
|
|
189
335
|
//
|
|
190
336
|
// When the standard update keeps failing due to subtree divergence the user
|
|
@@ -220,13 +366,25 @@ async function runReset(git, options, autoYes) {
|
|
|
220
366
|
'.claude/{agents,skills,commands}/*.md authored by you (non-framework symlinks)',
|
|
221
367
|
], 'cyan');
|
|
222
368
|
|
|
223
|
-
// Safety gate #1:
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
369
|
+
// Safety gate #1: protect any non-framework work. The reset commits a
|
|
370
|
+
// `git add -A` (the .framework/ deletion), so a dirty tree would otherwise be
|
|
371
|
+
// swept into that commit — auto-stash it instead and re-apply at the end.
|
|
372
|
+
// Same engine the normal flow uses; honours the user's stash authorization
|
|
373
|
+
// instead of hard-refusing (the v4.8.0 fix).
|
|
374
|
+
const stashOutcome = await autoStashNonFramework(git, {
|
|
375
|
+
autoStash: autoYes || options.autoStash === true,
|
|
376
|
+
interactive: !autoYes,
|
|
377
|
+
});
|
|
378
|
+
if (stashOutcome.aborted) { UI.info('Reset aborted.'); process.exit(0); }
|
|
379
|
+
if (stashOutcome.error) {
|
|
380
|
+
UI.error(`Could not stash working tree before reset: ${stashOutcome.error}`);
|
|
381
|
+
if (JSON_MODE) {
|
|
382
|
+
emitUpdateJson({ ok: false, action: 'failed',
|
|
383
|
+
reason: `Could not stash working tree before reset: ${stashOutcome.error}` }, 1);
|
|
384
|
+
}
|
|
228
385
|
process.exit(1);
|
|
229
386
|
}
|
|
387
|
+
const resetStashRef = stashOutcome.stashRef;
|
|
230
388
|
|
|
231
389
|
// Safety gate #2: any untracked / ignored files inside .framework/?
|
|
232
390
|
let untrackedInsideFramework = '';
|
|
@@ -369,9 +527,38 @@ async function runReset(git, options, autoYes) {
|
|
|
369
527
|
process.exit(1);
|
|
370
528
|
}
|
|
371
529
|
|
|
530
|
+
const newVersion = await git.getFrameworkVersion();
|
|
531
|
+
|
|
532
|
+
// Finalizer (v4.8.0+): re-apply the stash and commit BALDART-managed
|
|
533
|
+
// artifacts so the reset path leaves a clean tree — exactly like the normal
|
|
534
|
+
// update flow. Without this, the freshly-regenerated agents + state.json were
|
|
535
|
+
// left uncommitted (the half-done handoff the seamless rewrite removes).
|
|
536
|
+
let stashConflict = false;
|
|
537
|
+
if (resetStashRef) {
|
|
538
|
+
try {
|
|
539
|
+
await git.git.stash(['pop']);
|
|
540
|
+
UI.success(`Re-applied pre-reset stash "${resetStashRef}".`);
|
|
541
|
+
} catch (err) {
|
|
542
|
+
stashConflict = true;
|
|
543
|
+
UI.warning(`Could not auto-re-apply stash "${resetStashRef}": ${err.message}`);
|
|
544
|
+
UI.info('Recover with: git stash list && git stash pop');
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
try {
|
|
548
|
+
await postUpdateAutoCommit(git, newVersion, options);
|
|
549
|
+
} catch (err) {
|
|
550
|
+
UI.warning(`Auto-commit step skipped: ${err.message}`);
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
if (JSON_MODE) {
|
|
554
|
+
emitUpdateJson({ ok: true, action: 'reset',
|
|
555
|
+
installed_after: newVersion, remote_version: newVersion,
|
|
556
|
+
backup_tag: backupTag, stash_conflict: stashConflict }, 0);
|
|
557
|
+
}
|
|
558
|
+
|
|
372
559
|
UI.header('✓ RESET COMPLETE');
|
|
373
560
|
UI.box('RESET RESULT', [
|
|
374
|
-
`Framework: fresh install (${
|
|
561
|
+
`Framework: fresh install (${newVersion})`,
|
|
375
562
|
`Backup tag: ${backupTag}`,
|
|
376
563
|
`Rollback if needed: git reset --hard ${backupTag}`,
|
|
377
564
|
'',
|
|
@@ -705,13 +892,10 @@ async function update(options = {}, unknownArgs = []) {
|
|
|
705
892
|
// picks the cheap, correct resolution. (v3.33.0)
|
|
706
893
|
const allCovered = overlayCommits.length > 0
|
|
707
894
|
&& overlayCommits.every((c) => c.overlayCovered === true);
|
|
708
|
-
if (allCovered) {
|
|
709
|
-
UI.newline();
|
|
710
|
-
UI.success('These edits are already preserved in `.baldart/overlays/` — `--reset` is safe (your overlays are re-applied); scaffolding new overlays would be redundant.');
|
|
711
|
-
UI.info('Fast path: `npx baldart update --reset --yes --i-know`.');
|
|
712
|
-
}
|
|
713
895
|
|
|
714
|
-
//
|
|
896
|
+
// Explicit, non-interactive overrides (agents / CI) via --on-divergence
|
|
897
|
+
// still win. With NO strategy the DEFAULT is now seamless (v4.8.0+):
|
|
898
|
+
// capture+verify any uncovered edit, then reset-and-reapply — no flag maze.
|
|
715
899
|
if (divStrategy === 'scaffold-overlays') {
|
|
716
900
|
UI.newline();
|
|
717
901
|
scaffoldOverlaysForCommits(overlayCommits);
|
|
@@ -736,49 +920,14 @@ async function update(options = {}, unknownArgs = []) {
|
|
|
736
920
|
}
|
|
737
921
|
if (divStrategy === 'pull') {
|
|
738
922
|
UI.info('Keeping the custom commits (--on-divergence pull). The subtree pull will attempt a merge.');
|
|
739
|
-
// fall through
|
|
740
|
-
} else if (autoYes) {
|
|
741
|
-
// --yes without an explicit strategy: never silently overwrite. Tell
|
|
742
|
-
// the (likely non-TTY) caller exactly which flag completes the update.
|
|
743
|
-
UI.error('Custom edits present — refusing to resolve them unattended under --yes.');
|
|
744
|
-
UI.info('Re-run with one of:');
|
|
745
|
-
UI.list([
|
|
746
|
-
allCovered
|
|
747
|
-
? '--reset --yes --i-know → SAFE here: these edits are already in `.baldart/overlays/` (recommended)'
|
|
748
|
-
: '--on-divergence scaffold-overlays → auto-create overlay skeleton(s), then stop so you can fill them',
|
|
749
|
-
'--on-divergence pull → keep the commits and merge the update (non-destructive)',
|
|
750
|
-
allCovered
|
|
751
|
-
? '--on-divergence scaffold-overlays → would create redundant skeletons (edits already covered)'
|
|
752
|
-
: '--reset --yes --i-know → discard .framework/ edits and reinstall clean (destructive)',
|
|
753
|
-
], 'cyan');
|
|
754
|
-
emitUpdateJson({ ok: false, action: 'divergence-refused',
|
|
755
|
-
divergence_class: status.divergenceClass, overlay_already_covered: allCovered,
|
|
756
|
-
installed_before: status.installedVersion, remote_version: status.remoteVersion,
|
|
757
|
-
reason: allCovered
|
|
758
|
-
? 'Custom edits present but already preserved in .baldart/overlays/ — `--reset --yes --i-know` is the safe resolution.'
|
|
759
|
-
: 'Custom edits present — refusing to resolve them unattended under --yes.',
|
|
760
|
-
next_command: allCovered
|
|
761
|
-
? 'npx baldart update --reset --yes --i-know'
|
|
762
|
-
: 'npx baldart update --json --yes --on-divergence scaffold-overlays' }, 1);
|
|
923
|
+
// fall through to the subtree pull below
|
|
763
924
|
} else {
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
if (choice === 'overlay') {
|
|
771
|
-
UI.newline();
|
|
772
|
-
scaffoldOverlaysForCommits(overlayCommits);
|
|
773
|
-
UI.newline();
|
|
774
|
-
UI.info('Next steps:');
|
|
775
|
-
UI.list([
|
|
776
|
-
'Edit the overlay skeleton(s) above to capture your customisations (`/overlay` from Claude Code gives a guided flow).',
|
|
777
|
-
'Then complete the update with `npx baldart update --reset` — discards the redundant `.framework/` edits, reinstalls clean, re-applies your overlays.',
|
|
778
|
-
], 'cyan');
|
|
779
|
-
process.exit(0);
|
|
780
|
-
}
|
|
781
|
-
// 'keep' → fall through, subtree pull will attempt merge
|
|
925
|
+
// DEFAULT seamless resolution (v4.8.0+). Captures+verifies any
|
|
926
|
+
// uncovered edit, then reset-and-reapply. Exits the process on
|
|
927
|
+
// success (reset) or stops once on an unrecoverable blocker.
|
|
928
|
+
await resolveOverlayDivergenceSeamlessly(
|
|
929
|
+
git, status, options, autoYes, overlayCommits, status.remoteVersion
|
|
930
|
+
);
|
|
782
931
|
}
|
|
783
932
|
} else if (status.divergenceClass === 'real-custom' || status.divergenceClass === 'mixed') {
|
|
784
933
|
UI.warning(`Detected ${status.divergenceCommits.filter((c) => c.category === 'custom-other').length} custom commit(s) on .framework/ that are NOT auto-classifiable as overlays.`);
|
|
@@ -818,32 +967,47 @@ async function update(options = {}, unknownArgs = []) {
|
|
|
818
967
|
if (divStrategy === 'pull') {
|
|
819
968
|
UI.info('Pulling over the custom commits (--on-divergence pull). A merge commit will be created — resolve any conflicts that surface.');
|
|
820
969
|
// fall through
|
|
821
|
-
} else
|
|
822
|
-
|
|
823
|
-
|
|
970
|
+
} else {
|
|
971
|
+
// DEFAULT seamless (v4.8.0+): capture+verify the overlay-able subset
|
|
972
|
+
// so those edits are preserved, then stop ONCE. The custom-other
|
|
973
|
+
// commits touch non-overlayable paths (src/, CHANGELOG, …) and cannot
|
|
974
|
+
// be auto-resolved without risking that work — this is the single
|
|
975
|
+
// legitimate pause.
|
|
976
|
+
const cwd = process.cwd();
|
|
977
|
+
const overlayableTouched = [...new Set(overlayCommits.flatMap((c) => c.touched || []))];
|
|
978
|
+
let captured = [];
|
|
979
|
+
let blockers = [];
|
|
980
|
+
if (overlayableTouched.length) {
|
|
981
|
+
UI.newline();
|
|
982
|
+
UI.info(`Capturing ${overlayableTouched.length} overlay-able edit(s) into overlays…`);
|
|
983
|
+
({ captured, blockers } = await captureAndVerify({
|
|
984
|
+
cwd, git, files: overlayableTouched, frameworkVersion: status.remoteVersion,
|
|
985
|
+
}));
|
|
986
|
+
for (const o of captured) UI.success(`Overlay verified: ${o}`);
|
|
987
|
+
for (const b of blockers) UI.warning(`Could not capture ${b.file} — ${b.reason}`);
|
|
988
|
+
// Persist the verified overlays so the user keeps them when re-running.
|
|
989
|
+
if (captured.length) {
|
|
990
|
+
try {
|
|
991
|
+
await git.git.add(captured);
|
|
992
|
+
const st = await git.git.status();
|
|
993
|
+
if (st.staged.length) {
|
|
994
|
+
await git.git.commit('chore(baldart): capture/verify overlays (overlay-able subset of mixed divergence)');
|
|
995
|
+
}
|
|
996
|
+
} catch (_) { /* non-fatal — overlays remain on disk */ }
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
UI.newline();
|
|
1000
|
+
UI.error('Custom commits touch non-overlayable paths (src/, CHANGELOG, …) — these cannot be auto-resolved.');
|
|
1001
|
+
UI.info('Resolve them, then re-run `npx baldart update`:');
|
|
824
1002
|
UI.list([
|
|
825
|
-
'
|
|
826
|
-
'
|
|
827
|
-
'--on-divergence abort → do nothing',
|
|
1003
|
+
'Contribute generic improvements upstream with `/baldart-push` (recommended).',
|
|
1004
|
+
'Or keep the commits and merge with `npx baldart update --on-divergence pull`.',
|
|
828
1005
|
], 'cyan');
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
divergence_class: status.divergenceClass,
|
|
1006
|
+
emitUpdateJson({ ok: false, action: 'divergence-capture-incomplete',
|
|
1007
|
+
divergence_class: status.divergenceClass, captured, blockers,
|
|
832
1008
|
installed_before: status.installedVersion, remote_version: status.remoteVersion,
|
|
833
|
-
reason: '
|
|
834
|
-
next_command: 'npx baldart update --
|
|
835
|
-
} else {
|
|
836
|
-
const choice = await UI.select('How would you like to proceed?', [
|
|
837
|
-
{ name: 'Push these commits upstream first (recommended — `/baldart-push`)', value: 'push' },
|
|
838
|
-
{ name: 'Pull anyway (will create a merge commit — possible conflicts)', value: 'pull' },
|
|
839
|
-
{ name: 'Abort', value: 'abort' },
|
|
840
|
-
]);
|
|
841
|
-
if (choice === 'abort') { UI.info('Update cancelled.'); process.exit(0); }
|
|
842
|
-
if (choice === 'push') {
|
|
843
|
-
UI.info('Run `/baldart-push` from Claude Code (or `npx baldart push`) to contribute upstream, then re-run update.');
|
|
844
|
-
process.exit(0);
|
|
845
|
-
}
|
|
846
|
-
// 'pull' → fall through
|
|
1009
|
+
reason: 'Overlay-able subset captured; custom-other commit(s) touch non-overlayable paths and need /baldart-push or --on-divergence pull.',
|
|
1010
|
+
next_command: 'npx baldart update --on-divergence pull' }, 1);
|
|
847
1011
|
}
|
|
848
1012
|
}
|
|
849
1013
|
// 'unknown' → conservative: continue silently, the subtree pull will
|
|
@@ -853,14 +1017,14 @@ async function update(options = {}, unknownArgs = []) {
|
|
|
853
1017
|
// Step 3: Show changes preview
|
|
854
1018
|
UI.header('STEP 3/5: Preview Changes');
|
|
855
1019
|
|
|
856
|
-
const
|
|
857
|
-
if (
|
|
858
|
-
UI.info('
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
if (
|
|
862
|
-
|
|
863
|
-
}
|
|
1020
|
+
const changelog = await changelogSinceInstalled(git, status.installedVersion);
|
|
1021
|
+
if (changelog) {
|
|
1022
|
+
UI.info(`What's new (v${status.installedVersion} → v${status.remoteVersion}):`);
|
|
1023
|
+
// Human mode only — keep STDOUT clean in --json mode (UI already routes
|
|
1024
|
+
// its own lines to STDERR there; a raw console.log would break the contract).
|
|
1025
|
+
if (!JSON_MODE) console.log(changelog);
|
|
1026
|
+
} else {
|
|
1027
|
+
UI.info(`Updating v${status.installedVersion} → v${status.remoteVersion} (CHANGELOG preview unavailable).`);
|
|
864
1028
|
}
|
|
865
1029
|
|
|
866
1030
|
const proceedUpdate = await confirm('Proceed with update?', true);
|
|
@@ -869,49 +1033,20 @@ async function update(options = {}, unknownArgs = []) {
|
|
|
869
1033
|
process.exit(0);
|
|
870
1034
|
}
|
|
871
1035
|
|
|
872
|
-
// Pre-flight: git subtree pull --squash refuses a dirty worktree
|
|
873
|
-
//
|
|
874
|
-
// and re-apply after the update, so the user doesn't have to abort mid-flow.
|
|
875
|
-
let stashRef = null;
|
|
1036
|
+
// Pre-flight: git subtree pull --squash refuses a dirty worktree. Auto-stash
|
|
1037
|
+
// via the single shared engine and re-apply after the update (v4.8.0+).
|
|
876
1038
|
let stashConflict = false; // surfaced in the --json result (v3.32.0+)
|
|
877
|
-
const
|
|
878
|
-
if (
|
|
879
|
-
UI.
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
const unique = [...new Set(dirtyPaths)];
|
|
887
|
-
UI.info(`${unique.length} dirty path(s):`);
|
|
888
|
-
UI.list(unique.slice(0, 8), 'yellow');
|
|
889
|
-
if (unique.length > 8) UI.info(`…and ${unique.length - 8} more.`);
|
|
890
|
-
|
|
891
|
-
let action;
|
|
892
|
-
if (autoStash) {
|
|
893
|
-
UI.info('Auto-stashing dirty paths (--yes / --auto-stash).');
|
|
894
|
-
action = 'stash';
|
|
895
|
-
} else {
|
|
896
|
-
action = await UI.select('How would you like to proceed?', [
|
|
897
|
-
{ name: 'Auto-stash now and re-apply after the update (recommended)', value: 'stash' },
|
|
898
|
-
{ name: 'Abort — I want to commit or stash manually first', value: 'abort' }
|
|
899
|
-
]);
|
|
900
|
-
}
|
|
901
|
-
if (action === 'abort') {
|
|
902
|
-
UI.info('Aborted. Commit or `git stash -u`, then re-run `npx baldart update`.');
|
|
903
|
-
process.exit(0);
|
|
904
|
-
}
|
|
905
|
-
stashRef = `baldart-pre-update-${new Date().toISOString().replace(/[:.]/g, '-')}`;
|
|
906
|
-
try {
|
|
907
|
-
await git.git.stash(['push', '-u', '-m', stashRef]);
|
|
908
|
-
UI.success(`Stashed working tree as "${stashRef}".`);
|
|
909
|
-
} catch (err) {
|
|
910
|
-
UI.error(`Could not stash: ${err.message}`);
|
|
911
|
-
emitUpdateJson({ ok: false, action: 'failed',
|
|
912
|
-
reason: `Could not stash working tree before update: ${err.message}` }, 1);
|
|
913
|
-
}
|
|
1039
|
+
const stashOutcome = await autoStashNonFramework(git, { autoStash, interactive: !autoYes });
|
|
1040
|
+
if (stashOutcome.aborted) {
|
|
1041
|
+
UI.info('Aborted. Commit or `git stash -u`, then re-run `npx baldart update`.');
|
|
1042
|
+
process.exit(0);
|
|
1043
|
+
}
|
|
1044
|
+
if (stashOutcome.error) {
|
|
1045
|
+
UI.error(`Could not stash: ${stashOutcome.error}`);
|
|
1046
|
+
emitUpdateJson({ ok: false, action: 'failed',
|
|
1047
|
+
reason: `Could not stash working tree before update: ${stashOutcome.error}` }, 1);
|
|
914
1048
|
}
|
|
1049
|
+
const stashRef = stashOutcome.stashRef;
|
|
915
1050
|
|
|
916
1051
|
// Step 4: Create backup
|
|
917
1052
|
UI.header('STEP 4/5: Create Backup');
|