mintree 0.3.1 → 0.4.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.
|
@@ -10,7 +10,7 @@ import { resolveClaudeBinary } from "../lib/claude.js";
|
|
|
10
10
|
import { tryExec } from "../lib/exec.js";
|
|
11
11
|
import { getLatestVersion, isNewerVersion } from "../lib/version.js";
|
|
12
12
|
import { ALLOWED_TYPES } from "../lib/branch.js";
|
|
13
|
-
import { runCreate, runCreateDetached } from "../lib/worktreeCreate.js";
|
|
13
|
+
import { runCreate, runCreateDetached, } from "../lib/worktreeCreate.js";
|
|
14
14
|
import { runRemove, runRemoveByPath } from "../lib/worktreeRemove.js";
|
|
15
15
|
import { buildCreateMarkers, emitMarkers } from "../lib/markers.js";
|
|
16
16
|
import { readMetadata } from "../lib/metadata.js";
|
|
@@ -186,7 +186,14 @@ function CreateOverlayView({ overlay, onDescChange, onPromptChange, }) {
|
|
|
186
186
|
const dirPreview = isNewBranch
|
|
187
187
|
? `${overlay.issue.issue.id}-${overlay.desc}`
|
|
188
188
|
: `${overlay.issue.issue.id}-${detachedDesc}`;
|
|
189
|
-
return (_jsxs(Box, { flexGrow: 1, flexDirection: "column", paddingX: 1, children: [_jsxs(Box, { children: [_jsx(Text, { bold: true, color: "cyan", children: "Create worktree" }), _jsx(Text, { dimColor: true, children: ` for ${overlay.issue.issue.id}` })] }), _jsx(Box, { marginTop: 0, children: _jsx(Text, { children: overlay.issue.issue.title }) }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Box, { width: labelWidth, children: _jsx(Text, { bold: overlay.field === "branchMode", children: overlay.field === "branchMode" ? "▸ Branch:" : " Branch:" }) }), _jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "< " }), _jsx(Text, { color: overlay.field === "branchMode" ? "cyan" : undefined, bold: overlay.field === "branchMode", children: isNewBranch ? "new" : `current (${overlay.currentBranch ?? "?"})` }), _jsx(Text, { dimColor: true, children: " >" })] }), overlay.field === "branchMode" && _jsx(Text, { dimColor: true, children: " (use ← / → to toggle)" })] }), isNewBranch && (_jsxs(_Fragment, { children: [_jsxs(Box, { marginTop: 0, children: [_jsx(Box, { width: labelWidth, children: _jsx(Text, { bold: overlay.field === "type", children: overlay.field === "type" ? "▸ Type:" : " Type:" }) }), _jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "< " }), _jsx(Text, { color: overlay.field === "type" ? "cyan" : undefined, bold: overlay.field === "type", children: overlay.type }), _jsx(Text, { dimColor: true, children: " >" })] }), overlay.field === "type" && _jsx(Text, { dimColor: true, children: " (use ← / → to cycle)" })] }), _jsxs(Box, { marginTop: 0, children: [_jsx(Box, { width: labelWidth, children: _jsx(Text, { bold: overlay.field === "desc", children: overlay.field === "desc" ? "▸ Description:" : " Description:" }) }), _jsx(Box, { children: overlay.field === "desc" ? (_jsx(TextInput, { value: overlay.desc, onChange: onDescChange, placeholder: "kebab-case" })) : (_jsx(Text, { children: overlay.desc || "(empty)" })) })] })] })), _jsxs(Box, { marginTop: 0, children: [_jsx(Box, { width: labelWidth, children: _jsx(Text, { bold: overlay.field === "prompt", children: overlay.field === "prompt" ? "▸ Prompt:" : " Prompt:" }) }), _jsx(Box, { children: overlay.field === "prompt" ? (_jsx(TextInput, { value: overlay.prompt, onChange: onPromptChange, placeholder: "(empty = no initial message)" })) : (_jsx(Text, { dimColor: true, children: overlay.prompt || "(empty — Claude starts with no message)" })) })] })] }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Box, { width: labelWidth, children: _jsx(Text, { dimColor: true, children: " Checkout:" }) }), _jsx(Text, { color: "green", children: branchPreview })] }), _jsxs(Box, { children: [_jsx(Box, { width: labelWidth, children: _jsx(Text, { dimColor: true, children: " Worktree:" }) }), _jsxs(Text, { dimColor: true, children: [".mintree/worktrees/", dirPreview] })] }), _jsxs(Box, { children: [_jsx(Box, { width: labelWidth, children: _jsx(Text, { dimColor: true, children: " Mode:" }) }), _jsx(Text, { dimColor: true, children: "--work (Claude launches in the new worktree)" })] })] }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [isNewBranch ? (_jsxs(Text, { dimColor: true, children: ["Suggestion is a kebab of the title (capped at ", SUGGESTED_DESC_MAX_WORDS, " words). Edit it to match your repo's branch conventions."] })) : (_jsxs(Text, { dimColor: true, children: ["Detached HEAD at the tip of ", overlay.currentBranch ?? "the current branch", ". No new branch is created \u2014 commit on a new one with `git switch -c` when ready."] })), isNewBranch && overlay.conventionDoc && (_jsx(Text, { dimColor: true, children: `This repo has \`${overlay.conventionDoc}\` — review it before creating.` }))] }), overlay.error && (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: "red", bold: true, children: ["\u2717 ", overlay.error] }) })), overlay.pending && (_jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: "cyan", children: _jsx(Spinner, { type: "dots" }) }), _jsxs(Text, { children: [" ", overlay.pending] })] }))] }));
|
|
189
|
+
return (_jsxs(Box, { flexGrow: 1, flexDirection: "column", paddingX: 1, children: [_jsxs(Box, { children: [_jsx(Text, { bold: true, color: "cyan", children: "Create worktree" }), _jsx(Text, { dimColor: true, children: ` for ${overlay.issue.issue.id}` })] }), _jsx(Box, { marginTop: 0, children: _jsx(Text, { children: overlay.issue.issue.title }) }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Box, { width: labelWidth, children: _jsx(Text, { bold: overlay.field === "branchMode", children: overlay.field === "branchMode" ? "▸ Branch:" : " Branch:" }) }), _jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "< " }), _jsx(Text, { color: overlay.field === "branchMode" ? "cyan" : undefined, bold: overlay.field === "branchMode", children: isNewBranch ? "new" : `current (${overlay.currentBranch ?? "?"})` }), _jsx(Text, { dimColor: true, children: " >" })] }), overlay.field === "branchMode" && _jsx(Text, { dimColor: true, children: " (use ← / → to toggle)" })] }), isNewBranch && (_jsxs(_Fragment, { children: [_jsxs(Box, { marginTop: 0, children: [_jsx(Box, { width: labelWidth, children: _jsx(Text, { bold: overlay.field === "type", children: overlay.field === "type" ? "▸ Type:" : " Type:" }) }), _jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "< " }), _jsx(Text, { color: overlay.field === "type" ? "cyan" : undefined, bold: overlay.field === "type", children: overlay.type }), _jsx(Text, { dimColor: true, children: " >" })] }), overlay.field === "type" && _jsx(Text, { dimColor: true, children: " (use ← / → to cycle)" })] }), _jsxs(Box, { marginTop: 0, children: [_jsx(Box, { width: labelWidth, children: _jsx(Text, { bold: overlay.field === "desc", children: overlay.field === "desc" ? "▸ Description:" : " Description:" }) }), _jsx(Box, { children: overlay.field === "desc" ? (_jsx(TextInput, { value: overlay.desc, onChange: onDescChange, placeholder: "kebab-case" })) : (_jsx(Text, { children: overlay.desc || "(empty)" })) })] })] })), _jsxs(Box, { marginTop: 0, children: [_jsx(Box, { width: labelWidth, children: _jsx(Text, { bold: overlay.field === "prompt", children: overlay.field === "prompt" ? "▸ Prompt:" : " Prompt:" }) }), _jsx(Box, { children: overlay.field === "prompt" ? (_jsx(TextInput, { value: overlay.prompt, onChange: onPromptChange, placeholder: "(empty = no initial message)" })) : (_jsx(Text, { dimColor: true, children: overlay.prompt || "(empty — Claude starts with no message)" })) })] })] }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Box, { width: labelWidth, children: _jsx(Text, { dimColor: true, children: " Checkout:" }) }), _jsx(Text, { color: "green", children: branchPreview })] }), _jsxs(Box, { children: [_jsx(Box, { width: labelWidth, children: _jsx(Text, { dimColor: true, children: " Worktree:" }) }), _jsxs(Text, { dimColor: true, children: [".mintree/worktrees/", dirPreview] })] }), _jsxs(Box, { children: [_jsx(Box, { width: labelWidth, children: _jsx(Text, { dimColor: true, children: " Mode:" }) }), _jsx(Text, { dimColor: true, children: "--work (Claude launches in the new worktree)" })] })] }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [isNewBranch ? (_jsxs(Text, { dimColor: true, children: ["Suggestion is a kebab of the title (capped at ", SUGGESTED_DESC_MAX_WORDS, " words). Edit it to match your repo's branch conventions."] })) : (_jsxs(Text, { dimColor: true, children: ["Detached HEAD at the tip of ", overlay.currentBranch ?? "the current branch", ". No new branch is created \u2014 commit on a new one with `git switch -c` when ready."] })), isNewBranch && overlay.conventionDoc && (_jsx(Text, { dimColor: true, children: `This repo has \`${overlay.conventionDoc}\` — review it before creating.` }))] }), overlay.error && (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: "red", bold: true, children: ["\u2717 ", overlay.error] }) })), overlay.steps.length > 0 && (_jsx(Box, { marginTop: 1, flexDirection: "column", children: overlay.steps.map((step, i) => (_jsxs(Box, { children: [_jsx(CreateStepIcon, { kind: step.kind }), _jsx(Text, { children: " " }), _jsx(Text, { children: step.label }), step.detail && _jsxs(Text, { dimColor: true, children: [" (", step.detail, ")"] })] }, i))) })), overlay.pending && (_jsxs(Box, { marginTop: overlay.steps.length > 0 ? 0 : 1, children: [_jsx(Text, { color: "cyan", children: _jsx(Spinner, { type: "dots" }) }), _jsxs(Text, { children: [" ", overlay.pending] })] }))] }));
|
|
190
|
+
}
|
|
191
|
+
function CreateStepIcon({ kind }) {
|
|
192
|
+
if (kind === "ok")
|
|
193
|
+
return _jsx(Text, { color: "green", children: "\u2713" });
|
|
194
|
+
if (kind === "warn")
|
|
195
|
+
return _jsx(Text, { color: "yellow", children: "!" });
|
|
196
|
+
return _jsx(Text, { color: "cyan", children: "\u25CB" });
|
|
190
197
|
}
|
|
191
198
|
function IssueListRow({ d, selected, identifierWidth, rowWidth, }) {
|
|
192
199
|
// Display the issue id raw (e.g. "FE-123", "100"). The `#` prefix is a
|
|
@@ -888,6 +895,7 @@ export default function Dashboard() {
|
|
|
888
895
|
error: null,
|
|
889
896
|
conventionDoc: root ? findBranchConventionDoc(root) : null,
|
|
890
897
|
pending: null,
|
|
898
|
+
steps: [],
|
|
891
899
|
},
|
|
892
900
|
toast: null,
|
|
893
901
|
});
|
|
@@ -985,17 +993,37 @@ export default function Dashboard() {
|
|
|
985
993
|
});
|
|
986
994
|
return;
|
|
987
995
|
}
|
|
988
|
-
//
|
|
989
|
-
//
|
|
990
|
-
//
|
|
991
|
-
// slow remotes or a repo with a real init script). Without the
|
|
992
|
-
// setImmediate yield Ink wouldn't get to paint the spinner until after
|
|
993
|
-
// that work finished, leaving the user staring at a frozen overlay.
|
|
996
|
+
// Enter the live-setup view: clear input chrome, reset the step log,
|
|
997
|
+
// show a starting spinner. The actual progress updates come through
|
|
998
|
+
// the runCreate/runCreateDetached callbacks below.
|
|
994
999
|
setState({
|
|
995
1000
|
...state,
|
|
996
|
-
overlay: { ...overlay, error: null, pending: "
|
|
1001
|
+
overlay: { ...overlay, error: null, pending: "Starting...", steps: [] },
|
|
997
1002
|
});
|
|
998
|
-
await new Promise((resolve) =>
|
|
1003
|
+
await new Promise((resolve) => setTimeout(resolve, 32));
|
|
1004
|
+
// Progress callbacks invoked from inside runCreate/runCreateDetached.
|
|
1005
|
+
// Use functional setState so we don't clobber concurrent updates and
|
|
1006
|
+
// don't rely on the stale closure of `state`.
|
|
1007
|
+
const onStep = (step) => {
|
|
1008
|
+
setState((prev) => {
|
|
1009
|
+
if (prev.phase !== "ready" || prev.overlay?.kind !== "create")
|
|
1010
|
+
return prev;
|
|
1011
|
+
return {
|
|
1012
|
+
...prev,
|
|
1013
|
+
overlay: { ...prev.overlay, steps: [...prev.overlay.steps, step] },
|
|
1014
|
+
};
|
|
1015
|
+
});
|
|
1016
|
+
};
|
|
1017
|
+
const onPending = (label) => {
|
|
1018
|
+
setState((prev) => {
|
|
1019
|
+
if (prev.phase !== "ready" || prev.overlay?.kind !== "create")
|
|
1020
|
+
return prev;
|
|
1021
|
+
return {
|
|
1022
|
+
...prev,
|
|
1023
|
+
overlay: { ...prev.overlay, pending: label },
|
|
1024
|
+
};
|
|
1025
|
+
});
|
|
1026
|
+
};
|
|
999
1027
|
const prompt = overlay.prompt.trim();
|
|
1000
1028
|
const issueId = overlay.issue.issue.id;
|
|
1001
1029
|
let result;
|
|
@@ -1004,29 +1032,35 @@ export default function Dashboard() {
|
|
|
1004
1032
|
// from the issue title (kebabized), not user input — keeping the
|
|
1005
1033
|
// "current branch" flow as low-friction as possible.
|
|
1006
1034
|
const descKebab = kebabize(overlay.issue.issue.title) || `issue-${issueId}`;
|
|
1007
|
-
result = runCreateDetached({
|
|
1035
|
+
result = await runCreateDetached({
|
|
1008
1036
|
issueId,
|
|
1009
1037
|
descKebab,
|
|
1010
1038
|
work: true,
|
|
1039
|
+
progress: { onStep, onPending },
|
|
1011
1040
|
...(prompt.length > 0 ? { prompt } : {}),
|
|
1012
1041
|
});
|
|
1013
1042
|
}
|
|
1014
1043
|
else {
|
|
1015
1044
|
const desc = overlay.desc.trim();
|
|
1016
1045
|
const branch = `${overlay.type}/${issueId}-${desc}`;
|
|
1017
|
-
result = runCreate(branch, {
|
|
1046
|
+
result = await runCreate(branch, {
|
|
1018
1047
|
work: true,
|
|
1048
|
+
progress: { onStep, onPending },
|
|
1019
1049
|
...(prompt.length > 0 ? { prompt } : {}),
|
|
1020
1050
|
});
|
|
1021
1051
|
}
|
|
1022
1052
|
if (!result.ok) {
|
|
1023
|
-
setState({
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1053
|
+
setState((prev) => {
|
|
1054
|
+
if (prev.phase !== "ready" || prev.overlay?.kind !== "create")
|
|
1055
|
+
return prev;
|
|
1056
|
+
return {
|
|
1057
|
+
...prev,
|
|
1058
|
+
overlay: {
|
|
1059
|
+
...prev.overlay,
|
|
1060
|
+
pending: null,
|
|
1061
|
+
error: result.ok ? null : result.message + (result.hint ? ` — ${result.hint}` : ""),
|
|
1062
|
+
},
|
|
1063
|
+
};
|
|
1030
1064
|
});
|
|
1031
1065
|
return;
|
|
1032
1066
|
}
|
|
@@ -1034,9 +1068,15 @@ export default function Dashboard() {
|
|
|
1034
1068
|
// to In Progress on its project. Errors from the GraphQL call don't
|
|
1035
1069
|
// block the worktree hand-off; we swallow them and let `mintree doctor`
|
|
1036
1070
|
// surface persistent issues (missing `project` scope, etc.).
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1071
|
+
// Functional update preserves the accumulated `steps` list from the
|
|
1072
|
+
// progress callbacks; using the stale `overlay` closure would wipe it.
|
|
1073
|
+
setState((prev) => {
|
|
1074
|
+
if (prev.phase !== "ready" || prev.overlay?.kind !== "create")
|
|
1075
|
+
return prev;
|
|
1076
|
+
return {
|
|
1077
|
+
...prev,
|
|
1078
|
+
overlay: { ...prev.overlay, error: null, pending: "Updating issue status..." },
|
|
1079
|
+
};
|
|
1040
1080
|
});
|
|
1041
1081
|
const repoRoot = findMainRepoRoot();
|
|
1042
1082
|
if (repoRoot) {
|
|
@@ -55,19 +55,20 @@ export default function Create({ args, options }) {
|
|
|
55
55
|
const [result, setResult] = useState(null);
|
|
56
56
|
const [transition, setTransition] = useState("idle");
|
|
57
57
|
useEffect(() => {
|
|
58
|
-
|
|
58
|
+
(async () => {
|
|
59
59
|
try {
|
|
60
|
-
|
|
60
|
+
const r = await runCreate(branch, {
|
|
61
61
|
base: options.base,
|
|
62
62
|
work: options.work,
|
|
63
63
|
prompt: options.prompt,
|
|
64
64
|
permissionMode: options.permissionMode,
|
|
65
|
-
})
|
|
65
|
+
});
|
|
66
|
+
setResult(r);
|
|
66
67
|
}
|
|
67
68
|
catch (err) {
|
|
68
69
|
setResult({ ok: false, message: err instanceof Error ? err.message : String(err) });
|
|
69
70
|
}
|
|
70
|
-
}
|
|
71
|
+
})();
|
|
71
72
|
}, [branch, options.base, options.work, options.prompt, options.permissionMode]);
|
|
72
73
|
// Kick the Project v2 transition once the worktree is in place. Only when
|
|
73
74
|
// --work was on — non-work creates leave status untouched. Errors from the
|
|
@@ -5,11 +5,24 @@ export type CreateStep = {
|
|
|
5
5
|
label: string;
|
|
6
6
|
detail?: string;
|
|
7
7
|
};
|
|
8
|
+
/**
|
|
9
|
+
* Optional progress callbacks used by the dashboard overlay to render a
|
|
10
|
+
* live setup log (santree-style). `onPending(label)` highlights the
|
|
11
|
+
* currently running blocking operation (rendered with a spinner); call
|
|
12
|
+
* `onPending(null)` when it ends. `onStep(step)` appends a completed step
|
|
13
|
+
* to the log. Between every emission the implementation yields the event
|
|
14
|
+
* loop for one frame so Ink can paint before the next blocking section.
|
|
15
|
+
*/
|
|
16
|
+
export type ProgressCallbacks = {
|
|
17
|
+
onStep?: (step: CreateStep) => void;
|
|
18
|
+
onPending?: (label: string | null) => void;
|
|
19
|
+
};
|
|
8
20
|
export type CreateOpts = {
|
|
9
21
|
base?: string;
|
|
10
22
|
work: boolean;
|
|
11
23
|
prompt?: string;
|
|
12
24
|
permissionMode?: PermissionMode;
|
|
25
|
+
progress?: ProgressCallbacks;
|
|
13
26
|
};
|
|
14
27
|
export type CreateResult = {
|
|
15
28
|
ok: true;
|
|
@@ -32,14 +45,18 @@ export type CreateResult = {
|
|
|
32
45
|
* resolves a base branch, runs `git worktree add`, persists metadata, runs
|
|
33
46
|
* the optional `.mintree/init.sh`, and stages the --prompt to a temp file
|
|
34
47
|
* for the work hand-off when relevant.
|
|
48
|
+
*
|
|
49
|
+
* Async only because progress callbacks need event-loop yields between
|
|
50
|
+
* blocking sections; without them the dashboard overlay would freeze.
|
|
35
51
|
*/
|
|
36
|
-
export declare function runCreate(branchArg: string, opts: CreateOpts): CreateResult
|
|
52
|
+
export declare function runCreate(branchArg: string, opts: CreateOpts): Promise<CreateResult>;
|
|
37
53
|
export type CreateDetachedOpts = {
|
|
38
54
|
issueId: string;
|
|
39
55
|
descKebab: string;
|
|
40
56
|
work: boolean;
|
|
41
57
|
prompt?: string;
|
|
42
58
|
permissionMode?: PermissionMode;
|
|
59
|
+
progress?: ProgressCallbacks;
|
|
43
60
|
};
|
|
44
61
|
/**
|
|
45
62
|
* Variant of `runCreate` that doesn't create a new branch — the worktree is
|
|
@@ -53,4 +70,4 @@ export type CreateDetachedOpts = {
|
|
|
53
70
|
* branch-based flow so `worktree work` can still recover the issueId from
|
|
54
71
|
* the dir name (where it can't read it from the branch).
|
|
55
72
|
*/
|
|
56
|
-
export declare function runCreateDetached(opts: CreateDetachedOpts): CreateResult
|
|
73
|
+
export declare function runCreateDetached(opts: CreateDetachedOpts): Promise<CreateResult>;
|
|
@@ -37,14 +37,26 @@ function writePromptFile(prompt) {
|
|
|
37
37
|
fs.writeFileSync(filePath, prompt);
|
|
38
38
|
return filePath;
|
|
39
39
|
}
|
|
40
|
+
// Wait one frame (~16ms) so Ink has time to commit + paint the latest
|
|
41
|
+
// state before the next blocking execSync. No-op when no progress
|
|
42
|
+
// callbacks are set — CLI invocations skip the cost entirely.
|
|
43
|
+
function nextFrame(progress) {
|
|
44
|
+
if (!progress || (!progress.onStep && !progress.onPending))
|
|
45
|
+
return Promise.resolve();
|
|
46
|
+
return new Promise((resolve) => setTimeout(resolve, 16));
|
|
47
|
+
}
|
|
40
48
|
/**
|
|
41
49
|
* The whole `worktree create` flow as a pure function — same code path used
|
|
42
50
|
* by the CLI command and by the dashboard's `w` overlay. Validates input,
|
|
43
51
|
* resolves a base branch, runs `git worktree add`, persists metadata, runs
|
|
44
52
|
* the optional `.mintree/init.sh`, and stages the --prompt to a temp file
|
|
45
53
|
* for the work hand-off when relevant.
|
|
54
|
+
*
|
|
55
|
+
* Async only because progress callbacks need event-loop yields between
|
|
56
|
+
* blocking sections; without them the dashboard overlay would freeze.
|
|
46
57
|
*/
|
|
47
|
-
export function runCreate(branchArg, opts) {
|
|
58
|
+
export async function runCreate(branchArg, opts) {
|
|
59
|
+
const progress = opts.progress;
|
|
48
60
|
const root = findMainRepoRoot();
|
|
49
61
|
if (!root) {
|
|
50
62
|
return {
|
|
@@ -81,18 +93,27 @@ export function runCreate(branchArg, opts) {
|
|
|
81
93
|
};
|
|
82
94
|
}
|
|
83
95
|
const steps = [];
|
|
84
|
-
|
|
96
|
+
const pushStep = (step) => {
|
|
97
|
+
steps.push(step);
|
|
98
|
+
progress?.onStep?.(step);
|
|
99
|
+
};
|
|
100
|
+
pushStep({
|
|
85
101
|
kind: "ok",
|
|
86
102
|
label: "parsed branch",
|
|
87
103
|
detail: `type=${parsed.type}, issue=${parsed.issueId}, desc=${parsed.desc}`,
|
|
88
104
|
});
|
|
105
|
+
await nextFrame(progress);
|
|
89
106
|
// Fetch before resolving refs so the worktree forks from fresh code, not a
|
|
90
107
|
// stale local checkout. Best-effort: offline / no-remote just warns and we
|
|
91
108
|
// fall back to whatever is already local.
|
|
109
|
+
progress?.onPending?.("Fetching origin...");
|
|
110
|
+
await nextFrame(progress);
|
|
92
111
|
const fetch = fetchRemote(root);
|
|
93
|
-
|
|
112
|
+
progress?.onPending?.(null);
|
|
113
|
+
pushStep(fetch.ok
|
|
94
114
|
? { kind: "ok", label: "fetched origin", detail: "refs up to date" }
|
|
95
115
|
: { kind: "warn", label: "skipped git fetch", detail: fetch.reason });
|
|
116
|
+
await nextFrame(progress);
|
|
96
117
|
const existence = branchExists(root, parsed.branch);
|
|
97
118
|
let base;
|
|
98
119
|
if (existence === null) {
|
|
@@ -119,10 +140,13 @@ export function runCreate(branchArg, opts) {
|
|
|
119
140
|
if (existence === null && base && fetch.ok && remoteBranchExists(root, base)) {
|
|
120
141
|
baseRef = `origin/${base}`;
|
|
121
142
|
}
|
|
143
|
+
progress?.onPending?.("Creating worktree...");
|
|
144
|
+
await nextFrame(progress);
|
|
122
145
|
try {
|
|
123
146
|
addWorktree({ repoRoot: root, branch: parsed.branch, worktreePath, base: baseRef });
|
|
124
147
|
}
|
|
125
148
|
catch (err) {
|
|
149
|
+
progress?.onPending?.(null);
|
|
126
150
|
const stderr = err && typeof err === "object" && "stderr" in err
|
|
127
151
|
? String(err.stderr).trim()
|
|
128
152
|
: err instanceof Error
|
|
@@ -130,48 +154,58 @@ export function runCreate(branchArg, opts) {
|
|
|
130
154
|
: String(err);
|
|
131
155
|
return { ok: false, message: `git worktree add failed: ${stderr}` };
|
|
132
156
|
}
|
|
157
|
+
progress?.onPending?.(null);
|
|
133
158
|
if (existence === "remote") {
|
|
134
|
-
|
|
159
|
+
pushStep({
|
|
135
160
|
kind: "ok",
|
|
136
161
|
label: "checked out tracking branch",
|
|
137
162
|
detail: `from origin/${parsed.branch}`,
|
|
138
163
|
});
|
|
139
164
|
}
|
|
140
165
|
else if (existence === "local") {
|
|
141
|
-
|
|
166
|
+
pushStep({
|
|
142
167
|
kind: "ok",
|
|
143
168
|
label: "checked out existing local branch",
|
|
144
169
|
detail: parsed.branch,
|
|
145
170
|
});
|
|
146
171
|
}
|
|
147
172
|
else {
|
|
148
|
-
|
|
173
|
+
pushStep({
|
|
149
174
|
kind: "ok",
|
|
150
175
|
label: "created new branch",
|
|
151
176
|
detail: `${parsed.branch} (from ${baseRef})`,
|
|
152
177
|
});
|
|
153
178
|
}
|
|
154
|
-
|
|
179
|
+
await nextFrame(progress);
|
|
180
|
+
pushStep({ kind: "ok", label: "worktree created", detail: worktreePath });
|
|
181
|
+
await nextFrame(progress);
|
|
155
182
|
upsertIssue(root, parsed.issueId, base ? { base_branch: base } : {});
|
|
156
|
-
|
|
183
|
+
pushStep({ kind: "ok", label: "metadata updated", detail: `issue ${parsed.issueId}` });
|
|
184
|
+
await nextFrame(progress);
|
|
157
185
|
const initShPath = getInitScriptPath(root);
|
|
186
|
+
if (pathExists(initShPath)) {
|
|
187
|
+
progress?.onPending?.("Running .mintree/init.sh...");
|
|
188
|
+
await nextFrame(progress);
|
|
189
|
+
}
|
|
158
190
|
const initResult = tryRunInitScript(initShPath, worktreePath, root);
|
|
191
|
+
progress?.onPending?.(null);
|
|
159
192
|
if (initResult.ran) {
|
|
160
|
-
|
|
193
|
+
pushStep({ kind: "ok", label: "ran .mintree/init.sh", detail: worktreePath });
|
|
161
194
|
}
|
|
162
195
|
else if (initResult.error) {
|
|
163
|
-
|
|
196
|
+
pushStep({ kind: "warn", label: "init.sh failed", detail: initResult.error });
|
|
164
197
|
}
|
|
165
198
|
else if (!pathExists(initShPath)) {
|
|
166
|
-
|
|
199
|
+
pushStep({ kind: "skip", label: "no init.sh (skipping post-create hook)" });
|
|
167
200
|
}
|
|
201
|
+
await nextFrame(progress);
|
|
168
202
|
let promptFile;
|
|
169
203
|
if (opts.work && opts.prompt && opts.prompt.length > 0) {
|
|
170
204
|
try {
|
|
171
205
|
promptFile = writePromptFile(opts.prompt);
|
|
172
206
|
}
|
|
173
207
|
catch (err) {
|
|
174
|
-
|
|
208
|
+
pushStep({
|
|
175
209
|
kind: "warn",
|
|
176
210
|
label: "failed to stage --prompt for hand-off",
|
|
177
211
|
detail: err instanceof Error ? err.message : String(err),
|
|
@@ -179,7 +213,7 @@ export function runCreate(branchArg, opts) {
|
|
|
179
213
|
}
|
|
180
214
|
}
|
|
181
215
|
if (!opts.work && (opts.prompt || opts.permissionMode)) {
|
|
182
|
-
|
|
216
|
+
pushStep({
|
|
183
217
|
kind: "warn",
|
|
184
218
|
label: "ignoring --prompt / --permission-mode (only meaningful with --work)",
|
|
185
219
|
});
|
|
@@ -208,7 +242,8 @@ export function runCreate(branchArg, opts) {
|
|
|
208
242
|
* branch-based flow so `worktree work` can still recover the issueId from
|
|
209
243
|
* the dir name (where it can't read it from the branch).
|
|
210
244
|
*/
|
|
211
|
-
export function runCreateDetached(opts) {
|
|
245
|
+
export async function runCreateDetached(opts) {
|
|
246
|
+
const progress = opts.progress;
|
|
212
247
|
const root = findMainRepoRoot();
|
|
213
248
|
if (!root) {
|
|
214
249
|
return {
|
|
@@ -255,22 +290,34 @@ export function runCreateDetached(opts) {
|
|
|
255
290
|
};
|
|
256
291
|
}
|
|
257
292
|
const steps = [];
|
|
258
|
-
|
|
293
|
+
const pushStep = (step) => {
|
|
294
|
+
steps.push(step);
|
|
295
|
+
progress?.onStep?.(step);
|
|
296
|
+
};
|
|
297
|
+
pushStep({
|
|
259
298
|
kind: "ok",
|
|
260
299
|
label: "detached worktree",
|
|
261
300
|
detail: `issue=${opts.issueId}, base=${currentBranch}`,
|
|
262
301
|
});
|
|
302
|
+
await nextFrame(progress);
|
|
263
303
|
// Fetch so the detached worktree forks from the fresh remote tip of the
|
|
264
304
|
// current branch instead of a stale local checkout. Best-effort.
|
|
305
|
+
progress?.onPending?.("Fetching origin...");
|
|
306
|
+
await nextFrame(progress);
|
|
265
307
|
const fetch = fetchRemote(root);
|
|
266
|
-
|
|
308
|
+
progress?.onPending?.(null);
|
|
309
|
+
pushStep(fetch.ok
|
|
267
310
|
? { kind: "ok", label: "fetched origin", detail: "refs up to date" }
|
|
268
311
|
: { kind: "warn", label: "skipped git fetch", detail: fetch.reason });
|
|
312
|
+
await nextFrame(progress);
|
|
269
313
|
const baseRef = fetch.ok && remoteBranchExists(root, currentBranch) ? `origin/${currentBranch}` : currentBranch;
|
|
314
|
+
progress?.onPending?.("Creating worktree...");
|
|
315
|
+
await nextFrame(progress);
|
|
270
316
|
try {
|
|
271
317
|
execSync(`git worktree add --detach '${worktreePath.replace(/'/g, `'\\''`)}' '${baseRef.replace(/'/g, `'\\''`)}'`, { cwd: root, stdio: ["ignore", "pipe", "pipe"] });
|
|
272
318
|
}
|
|
273
319
|
catch (err) {
|
|
320
|
+
progress?.onPending?.(null);
|
|
274
321
|
const stderr = err && typeof err === "object" && "stderr" in err
|
|
275
322
|
? String(err.stderr).trim()
|
|
276
323
|
: err instanceof Error
|
|
@@ -278,32 +325,42 @@ export function runCreateDetached(opts) {
|
|
|
278
325
|
: String(err);
|
|
279
326
|
return { ok: false, message: `git worktree add --detach failed: ${stderr}` };
|
|
280
327
|
}
|
|
281
|
-
|
|
328
|
+
progress?.onPending?.(null);
|
|
329
|
+
pushStep({
|
|
282
330
|
kind: "ok",
|
|
283
331
|
label: "checked out detached HEAD",
|
|
284
332
|
detail: `at tip of ${baseRef}`,
|
|
285
333
|
});
|
|
286
|
-
|
|
334
|
+
await nextFrame(progress);
|
|
335
|
+
pushStep({ kind: "ok", label: "worktree created", detail: worktreePath });
|
|
336
|
+
await nextFrame(progress);
|
|
287
337
|
upsertIssue(root, opts.issueId, { base_branch: currentBranch });
|
|
288
|
-
|
|
338
|
+
pushStep({ kind: "ok", label: "metadata updated", detail: `issue ${opts.issueId}` });
|
|
339
|
+
await nextFrame(progress);
|
|
289
340
|
const initShPath = getInitScriptPath(root);
|
|
341
|
+
if (pathExists(initShPath)) {
|
|
342
|
+
progress?.onPending?.("Running .mintree/init.sh...");
|
|
343
|
+
await nextFrame(progress);
|
|
344
|
+
}
|
|
290
345
|
const initResult = tryRunInitScript(initShPath, worktreePath, root);
|
|
346
|
+
progress?.onPending?.(null);
|
|
291
347
|
if (initResult.ran) {
|
|
292
|
-
|
|
348
|
+
pushStep({ kind: "ok", label: "ran .mintree/init.sh", detail: worktreePath });
|
|
293
349
|
}
|
|
294
350
|
else if (initResult.error) {
|
|
295
|
-
|
|
351
|
+
pushStep({ kind: "warn", label: "init.sh failed", detail: initResult.error });
|
|
296
352
|
}
|
|
297
353
|
else if (!pathExists(initShPath)) {
|
|
298
|
-
|
|
354
|
+
pushStep({ kind: "skip", label: "no init.sh (skipping post-create hook)" });
|
|
299
355
|
}
|
|
356
|
+
await nextFrame(progress);
|
|
300
357
|
let promptFile;
|
|
301
358
|
if (opts.work && opts.prompt && opts.prompt.length > 0) {
|
|
302
359
|
try {
|
|
303
360
|
promptFile = writePromptFile(opts.prompt);
|
|
304
361
|
}
|
|
305
362
|
catch (err) {
|
|
306
|
-
|
|
363
|
+
pushStep({
|
|
307
364
|
kind: "warn",
|
|
308
365
|
label: "failed to stage --prompt for hand-off",
|
|
309
366
|
detail: err instanceof Error ? err.message : String(err),
|
package/package.json
CHANGED