happy-stacks 0.6.11 → 0.6.13
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/docs/commit-audits/happy/_tools/generate-plans.mjs +453 -0
- package/docs/commit-audits/happy/_tools/generate-pr-assignment.mjs +430 -0
- package/docs/commit-audits/happy/_tools/init-pr-assignment-working.mjs +107 -0
- package/docs/commit-audits/happy/leeroy-wip.commit-analysis.md +1849 -0
- package/docs/commit-audits/happy/leeroy-wip.commit-export.fuller-stat.md +747 -1
- package/docs/commit-audits/happy/leeroy-wip.commit-index.json +11740 -0
- package/docs/commit-audits/happy/leeroy-wip.commit-index.tsv +252 -0
- package/docs/commit-audits/happy/leeroy-wip.commit-inventory.md +18 -11
- package/docs/commit-audits/happy/leeroy-wip.commit-manual-review.md +1297 -140
- package/docs/commit-audits/happy/leeroy-wip.maintainers-overview.draft.md +448 -0
- package/docs/commit-audits/happy/leeroy-wip.pr-assignment.draft.tsv +252 -0
- package/docs/commit-audits/happy/leeroy-wip.pr-assignment.working.tsv +288 -0
- package/docs/commit-audits/happy/leeroy-wip.pr-catalog.draft.md +245 -0
- package/docs/commit-audits/happy/leeroy-wip.pr-stack-plan.draft.md +350 -0
- package/docs/commit-audits/happy/leeroy-wip.rewrite-deferred-fragments.tsv +65 -0
- package/docs/commit-audits/happy/leeroy-wip.rewrite-ledger.tsv +56 -0
- package/docs/commit-audits/happy/leeroy-wip.rewrite-process.md +240 -0
- package/docs/commit-audits/happy/leeroy-wip.rewrite-status.tsv +39 -0
- package/docs/commit-audits/happy/leeroy-wip.split-plan.draft.md +93 -0
- package/docs/commit-audits/happy/leeroy-wip.topic-buckets.md +76 -0
- package/docs/commit-audits/happy/pr-desc.extraction-ledger.tsv +279 -0
- package/docs/commit-audits/happy/pr-desc.original.md +0 -0
- package/docs/commit-audits/happy/pr-desc.post-audit-extraction-ledger.tsv +54 -0
- package/docs/commit-audits/happy/pr-desc.working-document.md +536 -0
- package/docs/happy-development.md +18 -1
- package/docs/isolated-linux-vm.md +23 -1
- package/docs/stacks.md +21 -1
- package/package.json +1 -1
- package/scripts/auth.mjs +46 -8
- package/scripts/daemon.mjs +44 -21
- package/scripts/doctor.mjs +2 -2
- package/scripts/doctor_cmd.test.mjs +67 -0
- package/scripts/happy.mjs +18 -5
- package/scripts/import.mjs +19 -13
- package/scripts/monorepo.mjs +414 -23
- package/scripts/monorepo_port.test.mjs +136 -0
- package/scripts/provision/linux-ubuntu-review-pr.sh +5 -1
- package/scripts/provision/macos-lima-happy-vm.sh +34 -2
- package/scripts/review.mjs +347 -124
- package/scripts/review_pr.mjs +78 -2
- package/scripts/run.mjs +2 -1
- package/scripts/stack.mjs +265 -19
- package/scripts/stack_daemon_cmd.test.mjs +196 -0
- package/scripts/stack_happy_cmd.test.mjs +103 -0
- package/scripts/utils/cli/prereqs.mjs +12 -1
- package/scripts/utils/cli/wizard.mjs +19 -1
- package/scripts/utils/dev/daemon.mjs +3 -1
- package/scripts/utils/llm/assist.mjs +64 -31
- package/scripts/utils/llm/happys_runner.mjs +60 -0
- package/scripts/utils/llm/tools.mjs +3 -3
- package/scripts/utils/proc/pm.mjs +1 -1
- package/scripts/utils/review/detached_worktree.mjs +61 -0
- package/scripts/utils/review/detached_worktree.test.mjs +62 -0
- package/scripts/utils/review/findings.mjs +133 -20
- package/scripts/utils/review/findings.test.mjs +88 -1
- package/scripts/utils/review/runners/augment.mjs +71 -0
- package/scripts/utils/review/runners/augment.test.mjs +42 -0
- package/scripts/utils/review/runners/coderabbit.mjs +54 -10
- package/scripts/utils/review/runners/coderabbit.test.mjs +15 -48
- package/scripts/utils/review/sliced_runner.mjs +39 -0
- package/scripts/utils/review/sliced_runner.test.mjs +47 -0
- package/scripts/utils/review/tool_home_seed.mjs +99 -0
- package/scripts/utils/review/tool_home_seed.test.mjs +113 -0
- package/scripts/utils/stack/cli_identities.mjs +29 -0
- package/scripts/utils/stack/startup.mjs +45 -7
- package/scripts/worktrees.mjs +8 -5
package/scripts/monorepo.mjs
CHANGED
|
@@ -14,6 +14,7 @@ import { bold, cyan, dim, green, red, yellow } from './utils/ui/ansi.mjs';
|
|
|
14
14
|
import { clipboardAvailable, copyTextToClipboard } from './utils/ui/clipboard.mjs';
|
|
15
15
|
import { detectInstalledLlmTools } from './utils/llm/tools.mjs';
|
|
16
16
|
import { launchLlmAssistant } from './utils/llm/assist.mjs';
|
|
17
|
+
import { buildHappyStacksRunnerShellSnippet } from './utils/llm/happys_runner.mjs';
|
|
17
18
|
|
|
18
19
|
function usage() {
|
|
19
20
|
return [
|
|
@@ -258,7 +259,7 @@ async function ensureClonedHappyMonorepo({ targetPath, repoUrl }) {
|
|
|
258
259
|
return dest;
|
|
259
260
|
}
|
|
260
261
|
|
|
261
|
-
async function resolveOrCloneTargetRepoRoot({ targetInput, targetArg, flags, kv }) {
|
|
262
|
+
async function resolveOrCloneTargetRepoRoot({ targetInput, targetArg, flags, kv, progress } = {}) {
|
|
262
263
|
const hint = String(targetInput ?? '').trim();
|
|
263
264
|
if (!hint) throw new Error('[monorepo] missing target');
|
|
264
265
|
|
|
@@ -284,7 +285,9 @@ async function resolveOrCloneTargetRepoRoot({ targetInput, targetArg, flags, kv
|
|
|
284
285
|
throw new Error('[monorepo] --clone-target requires an explicit --target=<dir>');
|
|
285
286
|
}
|
|
286
287
|
const targetRepo = String(kv?.get?.('--target-repo') ?? '').trim();
|
|
288
|
+
const spin = progress?.spinner?.(`Cloning target monorepo into ${hint}`);
|
|
287
289
|
const cloned = await ensureClonedHappyMonorepo({ targetPath: hint, repoUrl: targetRepo || 'https://github.com/slopus/happy.git' });
|
|
290
|
+
spin?.succeed?.(`Cloned target monorepo (${hint})`);
|
|
288
291
|
const clonedRoot = await resolveGitRoot(cloned);
|
|
289
292
|
if (!clonedRoot || !isHappyMonorepoRoot(clonedRoot)) {
|
|
290
293
|
throw new Error(`[monorepo] cloned target does not look like a slopus/happy monorepo root: ${cloned}`);
|
|
@@ -296,7 +299,7 @@ async function resolveOrCloneTargetRepoRoot({ targetInput, targetArg, flags, kv
|
|
|
296
299
|
throw new Error(`[monorepo] target is not a git repo: ${hint}`);
|
|
297
300
|
}
|
|
298
301
|
|
|
299
|
-
async function ensureRepoSpecCheckedOut({ targetRepoRoot, label, spec, desiredRef = '' }) {
|
|
302
|
+
async function ensureRepoSpecCheckedOut({ targetRepoRoot, label, spec, desiredRef = '', progress } = {}) {
|
|
300
303
|
const raw = String(spec ?? '').trim();
|
|
301
304
|
if (!raw) return '';
|
|
302
305
|
|
|
@@ -324,12 +327,16 @@ async function ensureRepoSpecCheckedOut({ targetRepoRoot, label, spec, desiredRe
|
|
|
324
327
|
|
|
325
328
|
if (!(await pathExists(dir))) {
|
|
326
329
|
await mkdir(dirname(dir), { recursive: true });
|
|
330
|
+
const spin = progress?.spinner?.(`Cloning ${label} PR repo (${pr.owner}/${pr.repo}#${pr.number})`);
|
|
327
331
|
await runCapture('git', ['clone', '--quiet', repoUrl, dir], { cwd: dirname(dir), env: gitNonInteractiveEnv() });
|
|
332
|
+
spin?.succeed?.(`Cloned ${label} PR repo (${pr.owner}/${pr.repo}#${pr.number})`);
|
|
328
333
|
}
|
|
329
334
|
|
|
330
335
|
const prRef = `refs/pull/${pr.number}/head`;
|
|
336
|
+
const spinFetch = progress?.spinner?.(`Fetching ${label} PR head (${prRef})`);
|
|
331
337
|
await runCapture('git', ['fetch', '--quiet', 'origin', prRef], { cwd: dir, env: gitNonInteractiveEnv() });
|
|
332
338
|
await runCapture('git', ['checkout', '--quiet', 'FETCH_HEAD'], { cwd: dir, env: gitNonInteractiveEnv() });
|
|
339
|
+
spinFetch?.succeed?.(`Checked out ${label} PR head`);
|
|
333
340
|
return dir;
|
|
334
341
|
}
|
|
335
342
|
|
|
@@ -338,7 +345,9 @@ async function ensureRepoSpecCheckedOut({ targetRepoRoot, label, spec, desiredRe
|
|
|
338
345
|
const dir = join(scratch, `${label}-${key}`);
|
|
339
346
|
if (!(await pathExists(dir))) {
|
|
340
347
|
await mkdir(dirname(dir), { recursive: true });
|
|
348
|
+
const spin = progress?.spinner?.(`Cloning ${label} source repo`);
|
|
341
349
|
await runCapture('git', ['clone', '--quiet', raw, dir], { cwd: dirname(dir), env: gitNonInteractiveEnv() });
|
|
350
|
+
spin?.succeed?.(`Cloned ${label} source repo`);
|
|
342
351
|
}
|
|
343
352
|
|
|
344
353
|
// Best-effort: ensure the desired ref exists (if provided).
|
|
@@ -346,7 +355,9 @@ async function ensureRepoSpecCheckedOut({ targetRepoRoot, label, spec, desiredRe
|
|
|
346
355
|
if (ref) {
|
|
347
356
|
const ok = await gitOk(dir, ['rev-parse', '--verify', '--quiet', ref]);
|
|
348
357
|
if (!ok) {
|
|
358
|
+
const spin = progress?.spinner?.(`Fetching ${label} ref (${ref})`);
|
|
349
359
|
await git(dir, ['fetch', '--quiet', 'origin', ref]).catch(() => {});
|
|
360
|
+
spin?.succeed?.(`Fetched ${label} ref (${ref})`);
|
|
350
361
|
}
|
|
351
362
|
}
|
|
352
363
|
return dir;
|
|
@@ -358,6 +369,105 @@ async function resolveGitPath(repoRoot, relPath) {
|
|
|
358
369
|
return rel.startsWith('/') ? rel : join(repoRoot, rel);
|
|
359
370
|
}
|
|
360
371
|
|
|
372
|
+
function isTestTty() {
|
|
373
|
+
return String(process.env.HAPPY_STACKS_TEST_TTY ?? '').trim() === '1';
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function shouldShowProgress({ json, silent = false } = {}) {
|
|
377
|
+
if (silent) return false;
|
|
378
|
+
if (json) return false;
|
|
379
|
+
return true;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function createProgressReporter({ enabled, label = '[monorepo]' } = {}) {
|
|
383
|
+
const on = Boolean(enabled);
|
|
384
|
+
const canSpin = on && isTty() && !isTestTty();
|
|
385
|
+
const frames = ['|', '/', '-', '\\'];
|
|
386
|
+
|
|
387
|
+
const line = (s) => {
|
|
388
|
+
// eslint-disable-next-line no-console
|
|
389
|
+
console.log(s);
|
|
390
|
+
};
|
|
391
|
+
|
|
392
|
+
const spinner = (text) => {
|
|
393
|
+
const msg = String(text ?? '').trim();
|
|
394
|
+
if (!on) {
|
|
395
|
+
return {
|
|
396
|
+
update: () => {},
|
|
397
|
+
succeed: () => {},
|
|
398
|
+
fail: () => {},
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
if (!canSpin) {
|
|
403
|
+
line(`${dim(label)} ${msg}`);
|
|
404
|
+
return {
|
|
405
|
+
update: () => {},
|
|
406
|
+
succeed: (doneText) => {
|
|
407
|
+
const done = String(doneText ?? '').trim();
|
|
408
|
+
if (done) line(`${green('✓')} ${done}`);
|
|
409
|
+
},
|
|
410
|
+
fail: (failText) => {
|
|
411
|
+
const fail = String(failText ?? '').trim();
|
|
412
|
+
if (fail) line(`${yellow('!')} ${fail}`);
|
|
413
|
+
},
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
let idx = 0;
|
|
418
|
+
let current = msg;
|
|
419
|
+
let active = true;
|
|
420
|
+
|
|
421
|
+
const render = () => {
|
|
422
|
+
if (!active) return;
|
|
423
|
+
const f = frames[idx % frames.length];
|
|
424
|
+
idx += 1;
|
|
425
|
+
try {
|
|
426
|
+
process.stdout.write(`\r${dim(label)} ${current} ${dim(f)} `);
|
|
427
|
+
} catch {
|
|
428
|
+
// ignore
|
|
429
|
+
}
|
|
430
|
+
};
|
|
431
|
+
|
|
432
|
+
// Initial render + keepalive.
|
|
433
|
+
render();
|
|
434
|
+
const t = setInterval(render, 120);
|
|
435
|
+
|
|
436
|
+
const stop = () => {
|
|
437
|
+
active = false;
|
|
438
|
+
try {
|
|
439
|
+
clearInterval(t);
|
|
440
|
+
} catch {
|
|
441
|
+
// ignore
|
|
442
|
+
}
|
|
443
|
+
try {
|
|
444
|
+
process.stdout.write('\r' + ' '.repeat(Math.min(140, current.length + String(label).length + 16)) + '\r');
|
|
445
|
+
} catch {
|
|
446
|
+
// ignore
|
|
447
|
+
}
|
|
448
|
+
};
|
|
449
|
+
|
|
450
|
+
return {
|
|
451
|
+
update: (nextText) => {
|
|
452
|
+
current = String(nextText ?? '').trim() || current;
|
|
453
|
+
render();
|
|
454
|
+
},
|
|
455
|
+
succeed: (doneText) => {
|
|
456
|
+
stop();
|
|
457
|
+
const done = String(doneText ?? '').trim();
|
|
458
|
+
if (done) line(`${green('✓')} ${done}`);
|
|
459
|
+
},
|
|
460
|
+
fail: (failText) => {
|
|
461
|
+
stop();
|
|
462
|
+
const fail = String(failText ?? '').trim();
|
|
463
|
+
if (fail) line(`${yellow('!')} ${fail}`);
|
|
464
|
+
},
|
|
465
|
+
};
|
|
466
|
+
};
|
|
467
|
+
|
|
468
|
+
return { spinner, line };
|
|
469
|
+
}
|
|
470
|
+
|
|
361
471
|
function section(title) {
|
|
362
472
|
// eslint-disable-next-line no-console
|
|
363
473
|
console.log('');
|
|
@@ -452,6 +562,28 @@ async function listConflictedFiles(repoRoot) {
|
|
|
452
562
|
return Array.from(new Set(files)).sort();
|
|
453
563
|
}
|
|
454
564
|
|
|
565
|
+
function hasConflictMarkers(text) {
|
|
566
|
+
const s = String(text ?? '');
|
|
567
|
+
// Typical git conflict markers at the start of a line.
|
|
568
|
+
return /^(<<<<<<< |>>>>>>> |\|\|\|\|\|\|\| )/m.test(s);
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
async function listFilesWithConflictMarkers(repoRoot, files) {
|
|
572
|
+
const fs = Array.isArray(files) ? files : [];
|
|
573
|
+
const hits = [];
|
|
574
|
+
for (const f of fs) {
|
|
575
|
+
const p = join(repoRoot, f);
|
|
576
|
+
try {
|
|
577
|
+
// eslint-disable-next-line no-await-in-loop
|
|
578
|
+
const raw = await readFile(p, 'utf-8');
|
|
579
|
+
if (hasConflictMarkers(raw)) hits.push(f);
|
|
580
|
+
} catch {
|
|
581
|
+
// ignore unreadable files
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
return hits;
|
|
585
|
+
}
|
|
586
|
+
|
|
455
587
|
async function readGitAmStatus(targetRepoRoot) {
|
|
456
588
|
const inProgress = await isGitAmInProgress(targetRepoRoot);
|
|
457
589
|
const conflictedFiles = await listConflictedFiles(targetRepoRoot);
|
|
@@ -492,8 +624,13 @@ async function readGitAmStatus(targetRepoRoot) {
|
|
|
492
624
|
return { inProgress, currentPatch, conflictedFiles };
|
|
493
625
|
}
|
|
494
626
|
|
|
495
|
-
async function formatPatchesToDir({ sourceRepoRoot, base, head, outDir }) {
|
|
627
|
+
async function formatPatchesToDir({ sourceRepoRoot, base, head, outDir, progressLabel = '', progress } = {}) {
|
|
628
|
+
const range = `${base}..${head}`;
|
|
629
|
+
const spin = progress?.spinner?.(
|
|
630
|
+
`Formatting patches${progressLabel ? ` (${progressLabel})` : ''} ${dim(`(${range})`)}`
|
|
631
|
+
);
|
|
496
632
|
await run('git', ['format-patch', '--quiet', '--output-directory', outDir, `${base}..${head}`], { cwd: sourceRepoRoot });
|
|
633
|
+
spin?.succeed?.(`Formatted patches${progressLabel ? ` (${progressLabel})` : ''}`);
|
|
497
634
|
const entries = await readdir(outDir, { withFileTypes: true });
|
|
498
635
|
const patches = entries
|
|
499
636
|
.filter((e) => e.isFile() && e.name.endsWith('.patch'))
|
|
@@ -639,7 +776,7 @@ async function checkPureNewFilesAlreadyExistIdentically({ targetRepoRoot, direct
|
|
|
639
776
|
return { ok: true, paths };
|
|
640
777
|
}
|
|
641
778
|
|
|
642
|
-
async function applyPatches({ targetRepoRoot, directory, patches, threeWay, skipApplied, continueOnFailure, quietGit }) {
|
|
779
|
+
async function applyPatches({ targetRepoRoot, directory, patches, threeWay, skipApplied, continueOnFailure, quietGit, progress } = {}) {
|
|
643
780
|
if (!patches.length) {
|
|
644
781
|
return { applied: [], skippedAlreadyApplied: [], skippedAlreadyExistsIdentical: [], failed: [] };
|
|
645
782
|
}
|
|
@@ -648,8 +785,21 @@ async function applyPatches({ targetRepoRoot, directory, patches, threeWay, skip
|
|
|
648
785
|
const skippedAlreadyApplied = [];
|
|
649
786
|
const skippedAlreadyExistsIdentical = [];
|
|
650
787
|
const failed = [];
|
|
651
|
-
|
|
652
|
-
|
|
788
|
+
const total = patches.length;
|
|
789
|
+
const targetLabel = directory ? `${directory}/` : '.';
|
|
790
|
+
const spin = progress?.spinner?.(`Applying patches into ${targetLabel} ${dim(`(0/${total})`)}`);
|
|
791
|
+
let lastUpdateAt = 0;
|
|
792
|
+
|
|
793
|
+
for (let i = 0; i < patches.length; i++) {
|
|
794
|
+
const patch = patches[i];
|
|
795
|
+
if (spin?.update) {
|
|
796
|
+
const now = Date.now();
|
|
797
|
+
// Avoid hammering the terminal. Update at most ~5x/sec.
|
|
798
|
+
if (now - lastUpdateAt > 200) {
|
|
799
|
+
lastUpdateAt = now;
|
|
800
|
+
spin.update(`Applying patches into ${targetLabel} ${dim(`(${i + 1}/${total})`)}`);
|
|
801
|
+
}
|
|
802
|
+
}
|
|
653
803
|
const patchFile = basename(patch);
|
|
654
804
|
// eslint-disable-next-line no-await-in-loop
|
|
655
805
|
const patchText = await readFile(patch, 'utf-8');
|
|
@@ -763,6 +913,9 @@ async function applyPatches({ targetRepoRoot, directory, patches, threeWay, skip
|
|
|
763
913
|
}
|
|
764
914
|
}
|
|
765
915
|
|
|
916
|
+
spin?.succeed?.(
|
|
917
|
+
`Applied patches into ${targetLabel} ${dim(`(applied=${applied.length} skipped=${skippedAlreadyApplied.length + skippedAlreadyExistsIdentical.length} failed=${failed.length})`)}`
|
|
918
|
+
);
|
|
766
919
|
return {
|
|
767
920
|
applied,
|
|
768
921
|
skippedAlreadyApplied,
|
|
@@ -783,6 +936,7 @@ async function portOne({
|
|
|
783
936
|
skipApplied,
|
|
784
937
|
continueOnFailure,
|
|
785
938
|
quietGit,
|
|
939
|
+
progress,
|
|
786
940
|
}) {
|
|
787
941
|
const sourceRepoRoot = await resolveGitRoot(sourcePath);
|
|
788
942
|
if (!sourceRepoRoot) {
|
|
@@ -807,7 +961,14 @@ async function portOne({
|
|
|
807
961
|
|
|
808
962
|
const tmp = await mkdtemp(join(tmpdir(), 'happy-stacks-port-'));
|
|
809
963
|
try {
|
|
810
|
-
const patches = await formatPatchesToDir({
|
|
964
|
+
const patches = await formatPatchesToDir({
|
|
965
|
+
sourceRepoRoot,
|
|
966
|
+
base,
|
|
967
|
+
head,
|
|
968
|
+
outDir: tmp,
|
|
969
|
+
progressLabel: label,
|
|
970
|
+
progress,
|
|
971
|
+
});
|
|
811
972
|
if (dryRun) {
|
|
812
973
|
return {
|
|
813
974
|
label,
|
|
@@ -829,6 +990,7 @@ async function portOne({
|
|
|
829
990
|
skipApplied,
|
|
830
991
|
continueOnFailure,
|
|
831
992
|
quietGit,
|
|
993
|
+
progress,
|
|
832
994
|
});
|
|
833
995
|
return {
|
|
834
996
|
label,
|
|
@@ -853,7 +1015,8 @@ async function portOne({
|
|
|
853
1015
|
async function cmdPortRun({ argv, flags, kv, json, silent = false }) {
|
|
854
1016
|
const targetArg = (kv.get('--target') ?? '').trim();
|
|
855
1017
|
const targetHint = targetArg || process.cwd();
|
|
856
|
-
const
|
|
1018
|
+
const progress = createProgressReporter({ enabled: shouldShowProgress({ json, silent }) });
|
|
1019
|
+
const targetRepoRoot = await resolveOrCloneTargetRepoRoot({ targetInput: targetHint, targetArg, flags, kv, progress });
|
|
857
1020
|
|
|
858
1021
|
// Prefer a clearer error message if the user is in the middle of conflict resolution.
|
|
859
1022
|
// (A git am session often makes the worktree dirty, which would otherwise trigger a generic "not clean" error.)
|
|
@@ -930,7 +1093,13 @@ async function cmdPortRun({ argv, flags, kv, json, silent = false }) {
|
|
|
930
1093
|
for (const s of sources) {
|
|
931
1094
|
// Allow sources to be local paths OR URL/PR specs (cloned into target/.git scratch).
|
|
932
1095
|
// eslint-disable-next-line no-await-in-loop
|
|
933
|
-
const resolvedPath = await ensureRepoSpecCheckedOut({
|
|
1096
|
+
const resolvedPath = await ensureRepoSpecCheckedOut({
|
|
1097
|
+
targetRepoRoot,
|
|
1098
|
+
label: s.label,
|
|
1099
|
+
spec: s.path,
|
|
1100
|
+
desiredRef: s.ref,
|
|
1101
|
+
progress,
|
|
1102
|
+
});
|
|
934
1103
|
// eslint-disable-next-line no-await-in-loop
|
|
935
1104
|
const r = await portOne({
|
|
936
1105
|
label: s.label,
|
|
@@ -944,6 +1113,7 @@ async function cmdPortRun({ argv, flags, kv, json, silent = false }) {
|
|
|
944
1113
|
skipApplied,
|
|
945
1114
|
continueOnFailure,
|
|
946
1115
|
quietGit,
|
|
1116
|
+
progress,
|
|
947
1117
|
});
|
|
948
1118
|
results.push(r);
|
|
949
1119
|
}
|
|
@@ -1041,20 +1211,22 @@ async function cmdPortStatus({ kv, json }) {
|
|
|
1041
1211
|
}
|
|
1042
1212
|
|
|
1043
1213
|
function buildPortLlmPromptText({ targetRepoRoot }) {
|
|
1214
|
+
const hs = buildHappyStacksRunnerShellSnippet();
|
|
1044
1215
|
return [
|
|
1045
1216
|
'You are an assistant helping the user port split-repo commits into the slopus/happy monorepo.',
|
|
1046
1217
|
'',
|
|
1218
|
+
hs,
|
|
1047
1219
|
`Target monorepo root: ${targetRepoRoot}`,
|
|
1048
1220
|
'',
|
|
1049
1221
|
'How to run the port:',
|
|
1050
|
-
`- guided (recommended):
|
|
1051
|
-
`- machine-readable report:
|
|
1222
|
+
`- guided (recommended): hs monorepo port guide --target=${targetRepoRoot}`,
|
|
1223
|
+
`- machine-readable report: hs monorepo port --target=${targetRepoRoot} --json`,
|
|
1052
1224
|
'',
|
|
1053
1225
|
'If a conflict happens (git am in progress):',
|
|
1054
|
-
`- inspect state (JSON):
|
|
1055
|
-
`- inspect state (text):
|
|
1226
|
+
`- inspect state (JSON): hs monorepo port status --target=${targetRepoRoot} --json`,
|
|
1227
|
+
`- inspect state (text): hs monorepo port status --target=${targetRepoRoot}`,
|
|
1056
1228
|
`- after fixing files: git -C ${targetRepoRoot} am --continue`,
|
|
1057
|
-
`- or via wrapper:
|
|
1229
|
+
`- or via wrapper: hs monorepo port continue --target=${targetRepoRoot}`,
|
|
1058
1230
|
`- to skip current patch: git -C ${targetRepoRoot} am --skip`,
|
|
1059
1231
|
`- to abort: git -C ${targetRepoRoot} am --abort`,
|
|
1060
1232
|
'',
|
|
@@ -1069,10 +1241,11 @@ function buildPortLlmPromptText({ targetRepoRoot }) {
|
|
|
1069
1241
|
|
|
1070
1242
|
function buildPortGuideLlmPromptText({ targetRepoRoot, initialCommandArgs }) {
|
|
1071
1243
|
const parts = Array.isArray(initialCommandArgs) ? initialCommandArgs : [];
|
|
1072
|
-
const cmd = ['
|
|
1244
|
+
const cmd = ['hs', 'monorepo', ...parts.map((p) => String(p))].join(' ');
|
|
1073
1245
|
return [
|
|
1074
1246
|
'You are an assistant helping the user port split-repo commits into the slopus/happy monorepo.',
|
|
1075
1247
|
'',
|
|
1248
|
+
buildHappyStacksRunnerShellSnippet(),
|
|
1076
1249
|
`Target monorepo root: ${targetRepoRoot}`,
|
|
1077
1250
|
'',
|
|
1078
1251
|
'Goal:',
|
|
@@ -1087,10 +1260,10 @@ function buildPortGuideLlmPromptText({ targetRepoRoot, initialCommandArgs }) {
|
|
|
1087
1260
|
cmd,
|
|
1088
1261
|
'',
|
|
1089
1262
|
'If it stops with conflicts:',
|
|
1090
|
-
`- Inspect status (JSON):
|
|
1263
|
+
`- Inspect status (JSON): hs monorepo port status --target=${targetRepoRoot} --json`,
|
|
1091
1264
|
`- Resolve conflicted files`,
|
|
1092
1265
|
`- Stage: git -C ${targetRepoRoot} add <files>`,
|
|
1093
|
-
`- Continue:
|
|
1266
|
+
`- Continue: hs monorepo port continue --target=${targetRepoRoot}`,
|
|
1094
1267
|
'',
|
|
1095
1268
|
'Notes:',
|
|
1096
1269
|
'- Conflicts are resolved one patch at a time (git am stops at the first conflict).',
|
|
@@ -1100,11 +1273,78 @@ function buildPortGuideLlmPromptText({ targetRepoRoot, initialCommandArgs }) {
|
|
|
1100
1273
|
].join('\n');
|
|
1101
1274
|
}
|
|
1102
1275
|
|
|
1103
|
-
async function cmdPortContinue({ kv, json }) {
|
|
1276
|
+
async function cmdPortContinue({ kv, flags, json }) {
|
|
1104
1277
|
const targetRepoRoot = await resolveTargetRepoRootFromArgs({ kv });
|
|
1105
1278
|
const runAmContinue = async () => {
|
|
1106
1279
|
const inProgressBefore = await isGitAmInProgress(targetRepoRoot);
|
|
1107
1280
|
if (!inProgressBefore) return { ok: true, didRun: false };
|
|
1281
|
+
const stageWanted = flags?.has?.('--stage') === true || flags?.has?.('--stage-conflicts') === true;
|
|
1282
|
+
const { conflictedFiles, currentPatch } = await readGitAmStatus(targetRepoRoot);
|
|
1283
|
+
|
|
1284
|
+
const stageCandidates = conflictedFiles.length
|
|
1285
|
+
? conflictedFiles
|
|
1286
|
+
: Array.isArray(currentPatch?.files)
|
|
1287
|
+
? currentPatch.files.filter(Boolean)
|
|
1288
|
+
: [];
|
|
1289
|
+
|
|
1290
|
+
if (conflictedFiles.length) {
|
|
1291
|
+
if (!stageWanted) {
|
|
1292
|
+
const hint = [
|
|
1293
|
+
`${yellow('[monorepo]')} continue blocked: ${bold('files still need staging')}`,
|
|
1294
|
+
`[monorepo] git reports unmerged files (e.g. ${dim('UU')}). This usually means you resolved them in an editor but forgot ${bold('git add')}.`,
|
|
1295
|
+
`[monorepo] conflicted files: ${conflictedFiles.join(', ')}`,
|
|
1296
|
+
`[monorepo] next: git -C ${targetRepoRoot} add ${conflictedFiles.map((f) => JSON.stringify(f)).join(' ')}`,
|
|
1297
|
+
`[monorepo] then re-run: happys monorepo port continue --target=${targetRepoRoot}`,
|
|
1298
|
+
`[monorepo] tip: you can also run: happys monorepo port continue --target=${targetRepoRoot} --stage`,
|
|
1299
|
+
].join('\n');
|
|
1300
|
+
printResult({
|
|
1301
|
+
json,
|
|
1302
|
+
data: { ok: false, targetRepoRoot, inProgress: true, conflictedFiles, needsStage: true, currentPatch },
|
|
1303
|
+
text: json ? '' : hint,
|
|
1304
|
+
});
|
|
1305
|
+
process.exitCode = 1;
|
|
1306
|
+
return { ok: false, didRun: false };
|
|
1307
|
+
}
|
|
1308
|
+
|
|
1309
|
+
const markerHits = await listFilesWithConflictMarkers(targetRepoRoot, stageCandidates);
|
|
1310
|
+
if (markerHits.length) {
|
|
1311
|
+
const hint = [
|
|
1312
|
+
`${yellow('[monorepo]')} refusing to auto-stage: conflict markers still present`,
|
|
1313
|
+
`[monorepo] files: ${markerHits.join(', ')}`,
|
|
1314
|
+
`[monorepo] next: open the file(s), remove ${dim('<<<<<<< / ======= / >>>>>>>')} markers, then run:`,
|
|
1315
|
+
` git -C ${targetRepoRoot} add ${markerHits.map((f) => JSON.stringify(f)).join(' ')}`,
|
|
1316
|
+
` happys monorepo port continue --target=${targetRepoRoot}`,
|
|
1317
|
+
].join('\n');
|
|
1318
|
+
printResult({
|
|
1319
|
+
json,
|
|
1320
|
+
data: { ok: false, targetRepoRoot, inProgress: true, conflictedFiles, conflictMarkers: markerHits },
|
|
1321
|
+
text: json ? '' : hint,
|
|
1322
|
+
});
|
|
1323
|
+
process.exitCode = 1;
|
|
1324
|
+
return { ok: false, didRun: false };
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1327
|
+
await runCapture('git', ['add', '-A', '--', ...stageCandidates], { cwd: targetRepoRoot });
|
|
1328
|
+
} else if (stageWanted && stageCandidates.length) {
|
|
1329
|
+
const markerHits = await listFilesWithConflictMarkers(targetRepoRoot, stageCandidates);
|
|
1330
|
+
if (markerHits.length) {
|
|
1331
|
+
const hint = [
|
|
1332
|
+
`${yellow('[monorepo]')} refusing to auto-stage: conflict markers still present`,
|
|
1333
|
+
`[monorepo] files: ${markerHits.join(', ')}`,
|
|
1334
|
+
`[monorepo] next: open the file(s), remove ${dim('<<<<<<< / ======= / >>>>>>>')} markers, then run:`,
|
|
1335
|
+
` git -C ${targetRepoRoot} add ${markerHits.map((f) => JSON.stringify(f)).join(' ')}`,
|
|
1336
|
+
` happys monorepo port continue --target=${targetRepoRoot}`,
|
|
1337
|
+
].join('\n');
|
|
1338
|
+
printResult({
|
|
1339
|
+
json,
|
|
1340
|
+
data: { ok: false, targetRepoRoot, inProgress: true, conflictedFiles, conflictMarkers: markerHits },
|
|
1341
|
+
text: json ? '' : hint,
|
|
1342
|
+
});
|
|
1343
|
+
process.exitCode = 1;
|
|
1344
|
+
return { ok: false, didRun: false };
|
|
1345
|
+
}
|
|
1346
|
+
await runCapture('git', ['add', '-A', '--', ...stageCandidates], { cwd: targetRepoRoot });
|
|
1347
|
+
}
|
|
1108
1348
|
try {
|
|
1109
1349
|
await runCapture('git', ['am', '--continue'], { cwd: targetRepoRoot });
|
|
1110
1350
|
return { ok: true, didRun: true };
|
|
@@ -1115,7 +1355,7 @@ async function cmdPortContinue({ kv, json }) {
|
|
|
1115
1355
|
`${red('[monorepo]')} continue failed (still conflicted).`,
|
|
1116
1356
|
conflictedFiles.length ? `[monorepo] conflicted files: ${conflictedFiles.join(', ')}` : '',
|
|
1117
1357
|
stderr ? `[monorepo] git:\n${stderr}` : '',
|
|
1118
|
-
`[monorepo] next: resolve, then re-run: happys monorepo port continue --target=${targetRepoRoot}`,
|
|
1358
|
+
`[monorepo] next: resolve, stage (${bold('git add')}), then re-run: happys monorepo port continue --target=${targetRepoRoot}`,
|
|
1119
1359
|
]
|
|
1120
1360
|
.filter(Boolean)
|
|
1121
1361
|
.join('\n');
|
|
@@ -1301,7 +1541,9 @@ async function cmdPortGuide({ kv, flags, json }) {
|
|
|
1301
1541
|
let targetRepoRoot = await resolveGitRoot(targetInput);
|
|
1302
1542
|
if (!targetRepoRoot) {
|
|
1303
1543
|
const wantsClone = flags?.has?.('--clone-target') || flags?.has?.('--clone');
|
|
1304
|
-
|
|
1544
|
+
// If the target doesn't exist yet, default to cloning without asking (best UX for `--target=...`).
|
|
1545
|
+
const targetExists = await pathExists(targetInput);
|
|
1546
|
+
if (targetExists && !wantsClone) {
|
|
1305
1547
|
const shouldClone =
|
|
1306
1548
|
(await promptSelect(rl, {
|
|
1307
1549
|
title:
|
|
@@ -1319,12 +1561,148 @@ async function cmdPortGuide({ kv, flags, json }) {
|
|
|
1319
1561
|
}
|
|
1320
1562
|
}
|
|
1321
1563
|
const repoUrl = (kv.get('--target-repo') ?? '').trim() || 'https://github.com/slopus/happy.git';
|
|
1564
|
+
// eslint-disable-next-line no-console
|
|
1565
|
+
console.log(dim(`[monorepo] cloning target monorepo -> ${targetInput}`));
|
|
1322
1566
|
await ensureClonedHappyMonorepo({ targetPath: targetInput, repoUrl });
|
|
1323
1567
|
targetRepoRoot = await resolveGitRoot(targetInput);
|
|
1324
1568
|
}
|
|
1325
1569
|
if (!targetRepoRoot || !isHappyMonorepoRoot(targetRepoRoot)) {
|
|
1326
1570
|
throw new Error(`[monorepo] invalid target (expected slopus/happy monorepo root): ${targetInput}`);
|
|
1327
1571
|
}
|
|
1572
|
+
const existingPlan = await readPortPlan(targetRepoRoot);
|
|
1573
|
+
if (existingPlan?.plan?.resumeArgv && Array.isArray(existingPlan.plan.resumeArgv)) {
|
|
1574
|
+
// Resume mode:
|
|
1575
|
+
// - do NOT require a clean worktree
|
|
1576
|
+
// - do NOT reject if git am is in progress
|
|
1577
|
+
// - do NOT re-prompt for options; use the stored plan
|
|
1578
|
+
// eslint-disable-next-line no-console
|
|
1579
|
+
console.log('');
|
|
1580
|
+
// eslint-disable-next-line no-console
|
|
1581
|
+
console.log(`${bold('[monorepo]')} guide: resuming existing port plan`);
|
|
1582
|
+
|
|
1583
|
+
const plan = existingPlan.plan;
|
|
1584
|
+
const resumeArgv = [...plan.resumeArgv];
|
|
1585
|
+
const initialArgv = Array.isArray(plan.initialArgv) ? [...plan.initialArgv] : null;
|
|
1586
|
+
|
|
1587
|
+
// Ensure we are on the intended branch if we can.
|
|
1588
|
+
try {
|
|
1589
|
+
const intended = String(plan.branch ?? '').trim();
|
|
1590
|
+
if (intended && intended !== 'HEAD') {
|
|
1591
|
+
const cur = (await git(targetRepoRoot, ['rev-parse', '--abbrev-ref', 'HEAD'])).trim() || 'HEAD';
|
|
1592
|
+
if (cur !== intended) {
|
|
1593
|
+
const exists = await gitOk(targetRepoRoot, ['show-ref', '--verify', '--quiet', `refs/heads/${intended}`]);
|
|
1594
|
+
if (exists) {
|
|
1595
|
+
await git(targetRepoRoot, ['checkout', '--quiet', intended]);
|
|
1596
|
+
}
|
|
1597
|
+
}
|
|
1598
|
+
}
|
|
1599
|
+
} catch {
|
|
1600
|
+
// ignore; we'll still rely on status/continue instructions
|
|
1601
|
+
}
|
|
1602
|
+
|
|
1603
|
+
const attemptArgv = initialArgv && !(await gitOk(targetRepoRoot, ['show-ref', '--verify', '--quiet', `refs/heads/${String(plan.branch ?? '').trim()}`]))
|
|
1604
|
+
? initialArgv
|
|
1605
|
+
: resumeArgv;
|
|
1606
|
+
|
|
1607
|
+
// If git am is already in progress, jump directly to the conflict loop.
|
|
1608
|
+
const inProgress = await isGitAmInProgress(targetRepoRoot);
|
|
1609
|
+
const { flags: attemptFlags, kv: attemptKv } = parseArgs(resumeArgv);
|
|
1610
|
+
const allowAutoLlm = String(process.env.HAPPY_STACKS_DISABLE_LLM_AUTOEXEC ?? '').trim() !== '1';
|
|
1611
|
+
const canAutoLaunchLlm = allowAutoLlm && (await detectInstalledLlmTools({ onlyAutoExec: true })).length > 0;
|
|
1612
|
+
const preferredConflictMode = String(plan.preferredConflictMode ?? '').trim() || 'guided';
|
|
1613
|
+
|
|
1614
|
+
if (inProgress) {
|
|
1615
|
+
// eslint-disable-next-line no-console
|
|
1616
|
+
console.log('');
|
|
1617
|
+
// eslint-disable-next-line no-console
|
|
1618
|
+
console.log(`${yellow('[monorepo]')} guide: conflict detected`);
|
|
1619
|
+
// eslint-disable-next-line no-console
|
|
1620
|
+
console.log(dim('[monorepo] guide: waiting for conflict resolution'));
|
|
1621
|
+
// Reuse the same action loop as the main guide path.
|
|
1622
|
+
// eslint-disable-next-line no-await-in-loop
|
|
1623
|
+
while (await isGitAmInProgress(targetRepoRoot)) {
|
|
1624
|
+
await cmdPortStatus({ kv: attemptKv, json: false });
|
|
1625
|
+
const action = await promptSelect(rl, {
|
|
1626
|
+
title: bold('Resolve conflicts, then choose an action:'),
|
|
1627
|
+
options: [
|
|
1628
|
+
{ label: `${green('continue')} (git am --continue)`, value: 'continue' },
|
|
1629
|
+
{ label: `${green('stage + continue')} ${dim('(git add conflicted files, then continue)')}`, value: 'stage-continue' },
|
|
1630
|
+
{ label: `${cyan('show status again')}`, value: 'status' },
|
|
1631
|
+
...(canAutoLaunchLlm
|
|
1632
|
+
? [{ label: `${green('launch LLM now')} ${dim('(recommended)')}`, value: 'llm-launch' }]
|
|
1633
|
+
: []),
|
|
1634
|
+
{ label: `${cyan('llm prompt')} ${dim('(copy/paste)')}`, value: 'llm' },
|
|
1635
|
+
{ label: `${yellow('skip current patch')} (git am --skip)`, value: 'skip' },
|
|
1636
|
+
{ label: `${red('abort')} (git am --abort)`, value: 'abort' },
|
|
1637
|
+
{ label: `${dim('quit guide (leave state as-is)')}`, value: 'quit' },
|
|
1638
|
+
],
|
|
1639
|
+
defaultIndex: 0,
|
|
1640
|
+
});
|
|
1641
|
+
if (action === 'status') continue;
|
|
1642
|
+
if (action === 'llm-launch') {
|
|
1643
|
+
const promptText = buildPortLlmPromptText({ targetRepoRoot });
|
|
1644
|
+
// eslint-disable-next-line no-console
|
|
1645
|
+
console.log('');
|
|
1646
|
+
// eslint-disable-next-line no-console
|
|
1647
|
+
console.log(bold('[monorepo] launching LLM...'));
|
|
1648
|
+
const res = await launchLlmAssistant({
|
|
1649
|
+
rl,
|
|
1650
|
+
title: 'Happy Stacks port conflict',
|
|
1651
|
+
subtitle: 'Resolve current git am conflict',
|
|
1652
|
+
promptText,
|
|
1653
|
+
cwd: targetRepoRoot,
|
|
1654
|
+
});
|
|
1655
|
+
if (!res.ok) {
|
|
1656
|
+
// eslint-disable-next-line no-console
|
|
1657
|
+
console.log(`${yellow('!')} Could not auto-launch an LLM (${res.reason || 'unknown'}).`);
|
|
1658
|
+
}
|
|
1659
|
+
continue;
|
|
1660
|
+
}
|
|
1661
|
+
if (action === 'llm') {
|
|
1662
|
+
const llmFlags = new Set([...(attemptFlags ?? []), '--copy']);
|
|
1663
|
+
await cmdPortLlm({ kv: attemptKv, flags: llmFlags, json: false });
|
|
1664
|
+
continue;
|
|
1665
|
+
}
|
|
1666
|
+
if (action === 'abort') {
|
|
1667
|
+
await runCapture('git', ['am', '--abort'], { cwd: targetRepoRoot });
|
|
1668
|
+
await deletePortPlan(targetRepoRoot);
|
|
1669
|
+
throw new Error('[monorepo] guide aborted (git am --abort)');
|
|
1670
|
+
}
|
|
1671
|
+
if (action === 'skip') {
|
|
1672
|
+
await runCapture('git', ['am', '--skip'], { cwd: targetRepoRoot });
|
|
1673
|
+
continue;
|
|
1674
|
+
}
|
|
1675
|
+
if (action === 'quit') {
|
|
1676
|
+
throw new Error('[monorepo] guide stopped (git am still in progress). Run `happys monorepo port status` / `... continue` to proceed.');
|
|
1677
|
+
}
|
|
1678
|
+
if (action === 'stage-continue') {
|
|
1679
|
+
const stageFlags = new Set([...(attemptFlags ?? []), '--stage']);
|
|
1680
|
+
await cmdPortContinue({ kv: attemptKv, flags: stageFlags, json: false });
|
|
1681
|
+
continue;
|
|
1682
|
+
}
|
|
1683
|
+
await cmdPortContinue({ kv: attemptKv, flags: attemptFlags, json: false });
|
|
1684
|
+
}
|
|
1685
|
+
// If am completed, fall through to resume remaining patches below.
|
|
1686
|
+
}
|
|
1687
|
+
|
|
1688
|
+
// If we're not in a conflict, just resume applying the remaining patches onto the current branch.
|
|
1689
|
+
try {
|
|
1690
|
+
const { flags: rFlags, kv: rKv } = parseArgs(attemptArgv);
|
|
1691
|
+
const jsonWanted = wantsJson(attemptArgv, { flags: rFlags });
|
|
1692
|
+
await cmdPortRun({ argv: attemptArgv, flags: rFlags, kv: rKv, json: jsonWanted });
|
|
1693
|
+
} catch (e) {
|
|
1694
|
+
const inProgressAfter = await isGitAmInProgress(targetRepoRoot);
|
|
1695
|
+
if (!inProgressAfter) throw e;
|
|
1696
|
+
// Once the port is paused on conflicts, the conflict loop above will handle it on the next rerun.
|
|
1697
|
+
throw e;
|
|
1698
|
+
}
|
|
1699
|
+
|
|
1700
|
+
await deletePortPlan(targetRepoRoot);
|
|
1701
|
+
// eslint-disable-next-line no-console
|
|
1702
|
+
console.log(`${green('[monorepo]')} guide complete`);
|
|
1703
|
+
return;
|
|
1704
|
+
}
|
|
1705
|
+
|
|
1328
1706
|
await ensureCleanGitWorktree(targetRepoRoot);
|
|
1329
1707
|
await ensureNoGitAmInProgress(targetRepoRoot);
|
|
1330
1708
|
|
|
@@ -1533,12 +1911,15 @@ async function cmdPortGuide({ kv, flags, json }) {
|
|
|
1533
1911
|
];
|
|
1534
1912
|
|
|
1535
1913
|
await writePortPlan(targetRepoRoot, {
|
|
1536
|
-
version:
|
|
1914
|
+
version: 2,
|
|
1537
1915
|
createdAt: new Date().toISOString(),
|
|
1538
1916
|
targetRepoRoot,
|
|
1539
1917
|
base,
|
|
1540
1918
|
branch,
|
|
1541
1919
|
use3way,
|
|
1920
|
+
preferredConflictMode,
|
|
1921
|
+
sources,
|
|
1922
|
+
initialArgv,
|
|
1542
1923
|
resumeArgv,
|
|
1543
1924
|
});
|
|
1544
1925
|
|
|
@@ -1586,6 +1967,7 @@ async function cmdPortGuide({ kv, flags, json }) {
|
|
|
1586
1967
|
console.log(bold('[monorepo] launching LLM to resolve conflicts...'));
|
|
1587
1968
|
// eslint-disable-next-line no-await-in-loop
|
|
1588
1969
|
const res = await launchLlmAssistant({
|
|
1970
|
+
rl,
|
|
1589
1971
|
title: 'Happy Stacks monorepo port',
|
|
1590
1972
|
subtitle: 'Resolve conflicts and complete the port',
|
|
1591
1973
|
promptText,
|
|
@@ -1620,6 +2002,7 @@ async function cmdPortGuide({ kv, flags, json }) {
|
|
|
1620
2002
|
title: bold('Resolve conflicts, then choose an action:'),
|
|
1621
2003
|
options: [
|
|
1622
2004
|
{ label: `${green('continue')} (git am --continue)`, value: 'continue' },
|
|
2005
|
+
{ label: `${green('stage + continue')} ${dim('(git add conflicted files, then continue)')}`, value: 'stage-continue' },
|
|
1623
2006
|
{ label: `${cyan('show status again')}`, value: 'status' },
|
|
1624
2007
|
...(canAutoLaunchLlm ? [{ label: `${green('launch LLM now')} ${dim('(recommended)')}`, value: 'llm-launch' }] : []),
|
|
1625
2008
|
{ label: `${cyan('llm prompt')} ${dim('(copy/paste)')}`, value: 'llm' },
|
|
@@ -1641,6 +2024,7 @@ async function cmdPortGuide({ kv, flags, json }) {
|
|
|
1641
2024
|
console.log(bold('[monorepo] launching LLM...'));
|
|
1642
2025
|
// eslint-disable-next-line no-await-in-loop
|
|
1643
2026
|
const res = await launchLlmAssistant({
|
|
2027
|
+
rl,
|
|
1644
2028
|
title: 'Happy Stacks port conflict',
|
|
1645
2029
|
subtitle: 'Resolve current git am conflict',
|
|
1646
2030
|
promptText,
|
|
@@ -1671,9 +2055,16 @@ async function cmdPortGuide({ kv, flags, json }) {
|
|
|
1671
2055
|
throw new Error('[monorepo] guide stopped (git am still in progress). Run `happys monorepo port status` / `... continue` to proceed.');
|
|
1672
2056
|
}
|
|
1673
2057
|
|
|
2058
|
+
if (action === 'stage-continue') {
|
|
2059
|
+
const stageFlags = new Set([...(attemptFlags ?? []), '--stage']);
|
|
2060
|
+
// eslint-disable-next-line no-await-in-loop
|
|
2061
|
+
await cmdPortContinue({ kv: attemptKv, flags: stageFlags, json: false });
|
|
2062
|
+
continue;
|
|
2063
|
+
}
|
|
2064
|
+
|
|
1674
2065
|
// continue
|
|
1675
2066
|
// eslint-disable-next-line no-await-in-loop
|
|
1676
|
-
await cmdPortContinue({ kv: attemptKv, json: false });
|
|
2067
|
+
await cmdPortContinue({ kv: attemptKv, flags: attemptFlags, json: false });
|
|
1677
2068
|
}
|
|
1678
2069
|
}
|
|
1679
2070
|
}
|
|
@@ -1762,7 +2153,7 @@ async function main() {
|
|
|
1762
2153
|
return;
|
|
1763
2154
|
}
|
|
1764
2155
|
if (sub === 'continue') {
|
|
1765
|
-
await cmdPortContinue({ kv, json });
|
|
2156
|
+
await cmdPortContinue({ kv, flags, json });
|
|
1766
2157
|
return;
|
|
1767
2158
|
}
|
|
1768
2159
|
if (sub === 'preflight') {
|