mintree 0.1.2 → 0.1.4
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/README.md +6 -10
- package/dist/commands/dashboard.js +67 -27
- package/dist/commands/worktree/work.js +21 -20
- package/dist/lib/claude.d.ts +1 -0
- package/dist/lib/claude.js +11 -0
- package/dist/lib/dashboard.d.ts +1 -1
- package/dist/lib/dashboard.js +13 -10
- package/dist/lib/worktreeCreate.d.ts +20 -0
- package/dist/lib/worktreeCreate.js +117 -1
- package/dist/lib/worktreeRemove.d.ts +7 -0
- package/dist/lib/worktreeRemove.js +68 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -16,19 +16,13 @@ It is a smaller, opinionated cousin of [santree](https://github.com/santiagotosc
|
|
|
16
16
|
## Install
|
|
17
17
|
|
|
18
18
|
```bash
|
|
19
|
-
|
|
20
|
-
# `npm install -g github:user/repo` flow has a known bug in npm 10+
|
|
21
|
-
# where dependencies don't make it into the global install dir.
|
|
22
|
-
git clone git@github.com:minex-labs/mintree.git ~/dev/mintree
|
|
23
|
-
cd ~/dev/mintree
|
|
24
|
-
npm install
|
|
25
|
-
npm link
|
|
19
|
+
npm install -g mintree
|
|
26
20
|
|
|
27
|
-
#
|
|
28
|
-
#
|
|
21
|
+
# Upgrade later:
|
|
22
|
+
# npm update -g mintree
|
|
29
23
|
|
|
30
24
|
# Verify
|
|
31
|
-
mintree --version #
|
|
25
|
+
mintree --version # should match the latest published version
|
|
32
26
|
mintree doctor # checks toolchain (git, gh, claude, tmux, ...)
|
|
33
27
|
|
|
34
28
|
# Enable the shell wrapper so `mintree worktree create` and the
|
|
@@ -50,6 +44,8 @@ _MT=${XDG_CACHE_HOME:-$HOME/.cache}/mintree/init-bash.bash
|
|
|
50
44
|
EOF
|
|
51
45
|
```
|
|
52
46
|
|
|
47
|
+
> Working on mintree itself? Clone the repo, run `npm install` and `npm link` instead — that wires `mintree` to your local checkout so source edits show up after `npm run build`.
|
|
48
|
+
|
|
53
49
|
`mintree doctor` should report **all required checks pass** before you continue. The most common gaps are:
|
|
54
50
|
|
|
55
51
|
- `gh` not authenticated → `gh auth login`
|
|
@@ -5,12 +5,12 @@ import TextInput from "ink-text-input";
|
|
|
5
5
|
import Spinner from "ink-spinner";
|
|
6
6
|
import { execSync } from "child_process";
|
|
7
7
|
import { createRequire } from "module";
|
|
8
|
-
import { findBranchConventionDoc, findMainRepoRoot, getMintreeDir, pathExists, } from "../lib/git.js";
|
|
8
|
+
import { findBranchConventionDoc, findMainRepoRoot, getCurrentBranch, getMintreeDir, pathExists, } from "../lib/git.js";
|
|
9
9
|
import { resolveClaudeBinary } from "../lib/claude.js";
|
|
10
10
|
import { tryExec } from "../lib/exec.js";
|
|
11
11
|
import { ALLOWED_TYPES } from "../lib/branch.js";
|
|
12
|
-
import { runCreate } from "../lib/worktreeCreate.js";
|
|
13
|
-
import { runRemove } from "../lib/worktreeRemove.js";
|
|
12
|
+
import { runCreate, runCreateDetached } from "../lib/worktreeCreate.js";
|
|
13
|
+
import { runRemove, runRemoveByPath } from "../lib/worktreeRemove.js";
|
|
14
14
|
import { buildCreateMarkers, emitMarkers } from "../lib/markers.js";
|
|
15
15
|
import { loadDashboard, } from "../lib/dashboard.js";
|
|
16
16
|
const require = createRequire(import.meta.url);
|
|
@@ -160,7 +160,7 @@ function FooterRow({ phase, overlayKind, }) {
|
|
|
160
160
|
return (_jsx(Box, { children: _jsx(Text, { dimColor: true, children: "q quit" }) }));
|
|
161
161
|
}
|
|
162
162
|
if (overlayKind === "create") {
|
|
163
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { bold: true, children: "Tab" }), _jsx(Text, { dimColor: true, children: " switch field " }), _jsx(Text, { bold: true, children: "\u2190/\u2192" }), _jsx(Text, { dimColor: true, children: "
|
|
163
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { bold: true, children: "Tab" }), _jsx(Text, { dimColor: true, children: " switch field " }), _jsx(Text, { bold: true, children: "\u2190/\u2192" }), _jsx(Text, { dimColor: true, children: " toggle branch / cycle type " }), _jsx(Text, { bold: true, children: "Enter" }), _jsx(Text, { dimColor: true, children: " create + work" })] }), _jsxs(Box, { children: [_jsx(Text, { bold: true, children: "Esc" }), _jsx(Text, { dimColor: true, children: " cancel" })] })] }));
|
|
164
164
|
}
|
|
165
165
|
if (overlayKind === "remove") {
|
|
166
166
|
return (_jsxs(Box, { children: [_jsx(Text, { bold: true, children: "y/Y" }), _jsx(Text, { dimColor: true, children: " confirm " }), _jsx(Text, { bold: true, children: "n/Esc" }), _jsx(Text, { dimColor: true, children: " cancel" })] }));
|
|
@@ -168,12 +168,21 @@ function FooterRow({ phase, overlayKind, }) {
|
|
|
168
168
|
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { children: _jsxs(Text, { children: [_jsx(Text, { bold: true, children: "j/k" }), _jsx(Text, { dimColor: true, children: " nav " }), _jsx(Text, { bold: true, children: "\u21B5" }), _jsx(Text, { dimColor: true, children: " work (resume / create) " }), _jsx(Text, { bold: true, children: "w" }), _jsx(Text, { dimColor: true, children: " work (always create) " }), _jsx(Text, { bold: true, children: "d" }), _jsx(Text, { dimColor: true, children: " remove" })] }) }), _jsxs(Box, { children: [_jsx(Text, { bold: true, children: "r" }), _jsx(Text, { dimColor: true, children: " refresh " }), _jsx(Text, { bold: true, children: "o" }), _jsx(Text, { dimColor: true, children: " open in browser " }), _jsx(Text, { bold: true, children: "PgUp/PgDn" }), _jsx(Text, { dimColor: true, children: "/" }), _jsx(Text, { bold: true, children: "wheel" }), _jsx(Text, { dimColor: true, children: " scroll detail " }), _jsx(Text, { bold: true, children: "q" }), _jsx(Text, { dimColor: true, children: " quit" })] })] }));
|
|
169
169
|
}
|
|
170
170
|
function RemoveOverlayView({ overlay }) {
|
|
171
|
-
return (_jsxs(Box, { flexGrow: 1, flexDirection: "column", paddingX: 1, children: [_jsxs(Box, { children: [_jsx(Text, { bold: true, color: "cyan", children: "Remove worktree" }), _jsx(Text, { dimColor: true, children: ` for #${overlay.issue.issue.number}` })] }), _jsx(Box, { marginTop: 0, children: _jsx(Text, { children: overlay.issue.issue.title }) }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "Branch: " }), _jsx(Text, { color: "cyan", children: overlay.branch })] }), _jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "State: " }), overlay.dirty ? (_jsx(Text, { color: "yellow", children: "dirty (uncommitted changes will be lost)" })) : (_jsx(Text, { color: "green", children: "clean" }))] })] }), _jsx(Box, { marginTop: 1, flexDirection: "column", children: _jsx(Text, { dimColor: true, children: "Removing the worktree leaves the branch and the issue's session_id in place. You can re-attach later with `mintree worktree create`." }) }), _jsx(Box, { marginTop: 1, children: overlay.dirty ? (_jsxs(Text, { children: ["This worktree is dirty. Press ", _jsx(Text, { bold: true, color: "red", children: "Y" }), " to force-remove,", " ", _jsx(Text, { bold: true, children: "N" }), "/", _jsx(Text, { bold: true, children: "Esc" }), " to cancel."] })) : (_jsxs(Text, { children: ["Press ", _jsx(Text, { bold: true, color: "green", children: "y" }), " to remove,", " ", _jsx(Text, { bold: true, children: "N" }), "/", _jsx(Text, { bold: true, children: "Esc" }), " to cancel."] })) }), overlay.error && (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: "red", bold: true, children: ["\u2717 ", overlay.error] }) }))] }));
|
|
171
|
+
return (_jsxs(Box, { flexGrow: 1, flexDirection: "column", paddingX: 1, children: [_jsxs(Box, { children: [_jsx(Text, { bold: true, color: "cyan", children: "Remove worktree" }), _jsx(Text, { dimColor: true, children: ` for #${overlay.issue.issue.number}` })] }), _jsx(Box, { marginTop: 0, children: _jsx(Text, { children: overlay.issue.issue.title }) }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "Branch: " }), _jsx(Text, { color: "cyan", children: overlay.branch ?? `(detached) ${overlay.worktreePath}` })] }), _jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "State: " }), overlay.dirty ? (_jsx(Text, { color: "yellow", children: "dirty (uncommitted changes will be lost)" })) : (_jsx(Text, { color: "green", children: "clean" }))] })] }), _jsx(Box, { marginTop: 1, flexDirection: "column", children: _jsx(Text, { dimColor: true, children: "Removing the worktree leaves the branch and the issue's session_id in place. You can re-attach later with `mintree worktree create`." }) }), _jsx(Box, { marginTop: 1, children: overlay.dirty ? (_jsxs(Text, { children: ["This worktree is dirty. Press ", _jsx(Text, { bold: true, color: "red", children: "Y" }), " to force-remove,", " ", _jsx(Text, { bold: true, children: "N" }), "/", _jsx(Text, { bold: true, children: "Esc" }), " to cancel."] })) : (_jsxs(Text, { children: ["Press ", _jsx(Text, { bold: true, color: "green", children: "y" }), " to remove,", " ", _jsx(Text, { bold: true, children: "N" }), "/", _jsx(Text, { bold: true, children: "Esc" }), " to cancel."] })) }), overlay.error && (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: "red", bold: true, children: ["\u2717 ", overlay.error] }) }))] }));
|
|
172
172
|
}
|
|
173
173
|
function CreateOverlayView({ overlay, onDescChange, onPromptChange, }) {
|
|
174
174
|
const labelWidth = 14;
|
|
175
|
-
const
|
|
176
|
-
|
|
175
|
+
const isNewBranch = overlay.branchMode === "new";
|
|
176
|
+
const detachedDesc = kebabize(overlay.issue.issue.title) || `issue-${overlay.issue.issue.number}`;
|
|
177
|
+
const branchPreview = isNewBranch
|
|
178
|
+
? `${overlay.type}/${overlay.issue.issue.number}-${overlay.desc}`
|
|
179
|
+
: `detached @ ${overlay.currentBranch ?? "(unknown)"}`;
|
|
180
|
+
const dirPreview = isNewBranch
|
|
181
|
+
? `${overlay.issue.issue.number}-${overlay.desc}`
|
|
182
|
+
: `${overlay.issue.issue.number}-${detachedDesc}`;
|
|
183
|
+
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.number}` })] }), _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
|
|
184
|
+
? "new"
|
|
185
|
+
: `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: "(optional) initial message for Claude" })) : (_jsx(Text, { dimColor: true, children: overlay.prompt || "(optional — 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] }) }))] }));
|
|
177
186
|
}
|
|
178
187
|
function stateChar(state) {
|
|
179
188
|
if (!state)
|
|
@@ -296,7 +305,7 @@ function buildDetailLines(d, width) {
|
|
|
296
305
|
lines.push(blank());
|
|
297
306
|
lines.push([{ text: "⌥ Worktree", bold: true }]);
|
|
298
307
|
if (d.worktree) {
|
|
299
|
-
for (const w2 of wrapLine(` branch: ${d.worktree.branch}`, w))
|
|
308
|
+
for (const w2 of wrapLine(` branch: ${d.worktree.branch ?? "(detached HEAD)"}`, w))
|
|
300
309
|
lines.push([{ text: w2, dim: true }]);
|
|
301
310
|
for (const w2 of wrapLine(` path: ${d.worktree.path}`, w))
|
|
302
311
|
lines.push([{ text: w2, dim: true }]);
|
|
@@ -638,6 +647,7 @@ export default function Dashboard() {
|
|
|
638
647
|
kind: "remove",
|
|
639
648
|
issue,
|
|
640
649
|
branch: issue.worktree.branch,
|
|
650
|
+
worktreePath: issue.worktree.path,
|
|
641
651
|
dirty: issue.worktree.dirty,
|
|
642
652
|
error: null,
|
|
643
653
|
},
|
|
@@ -655,10 +665,12 @@ export default function Dashboard() {
|
|
|
655
665
|
overlay: {
|
|
656
666
|
kind: "create",
|
|
657
667
|
issue,
|
|
668
|
+
branchMode: "new",
|
|
669
|
+
currentBranch: root ? getCurrentBranch(root) : null,
|
|
658
670
|
type: "feat",
|
|
659
671
|
desc: kebabize(issue.issue.title) || `issue-${issue.issue.number}`,
|
|
660
672
|
prompt: "",
|
|
661
|
-
field: "
|
|
673
|
+
field: "branchMode",
|
|
662
674
|
error: null,
|
|
663
675
|
conventionDoc: root ? findBranchConventionDoc(root) : null,
|
|
664
676
|
},
|
|
@@ -678,18 +690,28 @@ export default function Dashboard() {
|
|
|
678
690
|
return;
|
|
679
691
|
}
|
|
680
692
|
// Create overlay from here on.
|
|
693
|
+
// In "current" branch mode we skip type+desc fields entirely — they have
|
|
694
|
+
// no meaning when the worktree is going to be detached. Tab cycles
|
|
695
|
+
// branchMode ⇄ prompt only.
|
|
681
696
|
if (key.tab) {
|
|
682
|
-
const
|
|
683
|
-
? "
|
|
684
|
-
:
|
|
685
|
-
|
|
686
|
-
|
|
697
|
+
const order = overlay.branchMode === "current"
|
|
698
|
+
? ["branchMode", "prompt"]
|
|
699
|
+
: ["branchMode", "type", "desc", "prompt"];
|
|
700
|
+
const i = order.indexOf(overlay.field);
|
|
701
|
+
const nextField = order[(i + 1) % order.length];
|
|
687
702
|
setState({
|
|
688
703
|
...state,
|
|
689
704
|
overlay: { ...overlay, field: nextField },
|
|
690
705
|
});
|
|
691
706
|
return;
|
|
692
707
|
}
|
|
708
|
+
if (overlay.field === "branchMode") {
|
|
709
|
+
if (key.leftArrow || key.rightArrow || input === "h" || input === "l") {
|
|
710
|
+
const next = overlay.branchMode === "new" ? "current" : "new";
|
|
711
|
+
setState({ ...state, overlay: { ...overlay, branchMode: next, error: null } });
|
|
712
|
+
return;
|
|
713
|
+
}
|
|
714
|
+
}
|
|
693
715
|
if (overlay.field === "type") {
|
|
694
716
|
if (key.leftArrow || input === "h") {
|
|
695
717
|
const idx = ALLOWED_TYPES.indexOf(overlay.type);
|
|
@@ -734,20 +756,36 @@ export default function Dashboard() {
|
|
|
734
756
|
async function confirmCreate(overlay) {
|
|
735
757
|
if (state.phase !== "ready")
|
|
736
758
|
return;
|
|
737
|
-
const
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
759
|
+
const prompt = overlay.prompt.trim();
|
|
760
|
+
const issueNumber = overlay.issue.issue.number;
|
|
761
|
+
let result;
|
|
762
|
+
if (overlay.branchMode === "current") {
|
|
763
|
+
// Detached worktree off the main repo's current branch. Desc comes
|
|
764
|
+
// from the issue title (kebabized), not user input — keeping the
|
|
765
|
+
// "current branch" flow as low-friction as possible.
|
|
766
|
+
const descKebab = kebabize(overlay.issue.issue.title) || `issue-${issueNumber}`;
|
|
767
|
+
result = runCreateDetached({
|
|
768
|
+
issueId: String(issueNumber),
|
|
769
|
+
descKebab,
|
|
770
|
+
work: true,
|
|
771
|
+
...(prompt.length > 0 ? { prompt } : {}),
|
|
772
|
+
});
|
|
773
|
+
}
|
|
774
|
+
else {
|
|
775
|
+
const desc = overlay.desc.trim();
|
|
776
|
+
if (!desc) {
|
|
777
|
+
setState({
|
|
778
|
+
...state,
|
|
779
|
+
overlay: { ...overlay, error: "Description is required." },
|
|
780
|
+
});
|
|
781
|
+
return;
|
|
782
|
+
}
|
|
783
|
+
const branch = `${overlay.type}/${issueNumber}-${desc}`;
|
|
784
|
+
result = runCreate(branch, {
|
|
785
|
+
work: true,
|
|
786
|
+
...(prompt.length > 0 ? { prompt } : {}),
|
|
742
787
|
});
|
|
743
|
-
return;
|
|
744
788
|
}
|
|
745
|
-
const branch = `${overlay.type}/${overlay.issue.issue.number}-${desc}`;
|
|
746
|
-
const prompt = overlay.prompt.trim();
|
|
747
|
-
const result = runCreate(branch, {
|
|
748
|
-
work: true,
|
|
749
|
-
...(prompt.length > 0 ? { prompt } : {}),
|
|
750
|
-
});
|
|
751
789
|
if (!result.ok) {
|
|
752
790
|
setState({
|
|
753
791
|
...state,
|
|
@@ -766,7 +804,9 @@ export default function Dashboard() {
|
|
|
766
804
|
async function confirmRemove(overlay, force) {
|
|
767
805
|
if (state.phase !== "ready")
|
|
768
806
|
return;
|
|
769
|
-
const result =
|
|
807
|
+
const result = overlay.branch
|
|
808
|
+
? runRemove(overlay.branch, force)
|
|
809
|
+
: runRemoveByPath(overlay.worktreePath, force);
|
|
770
810
|
if (!result.ok) {
|
|
771
811
|
setState({
|
|
772
812
|
...state,
|
|
@@ -7,7 +7,6 @@ import { z } from "zod";
|
|
|
7
7
|
import { randomUUID } from "crypto";
|
|
8
8
|
import { readFileSync, unlinkSync } from "fs";
|
|
9
9
|
import * as path from "path";
|
|
10
|
-
import { parseBranch, isParseError } from "../../lib/branch.js";
|
|
11
10
|
import { findMainRepoRoot, getMintreeDir, getWorktreesDir, getCurrentBranch, pathExists, } from "../../lib/git.js";
|
|
12
11
|
import { getSessionId, setSessionId } from "../../lib/metadata.js";
|
|
13
12
|
import { launchClaude, PERMISSION_MODES } from "../../lib/claude.js";
|
|
@@ -61,28 +60,28 @@ function resolve(cwd) {
|
|
|
61
60
|
hint: "Run `mintree worktree work` from inside `.mintree/worktrees/<issue>-<desc>`.",
|
|
62
61
|
};
|
|
63
62
|
}
|
|
64
|
-
const branch = getCurrentBranch(cwdAbs);
|
|
65
|
-
if (!branch) {
|
|
66
|
-
return {
|
|
67
|
-
ok: false,
|
|
68
|
-
message: "Could not determine the current branch (detached HEAD?)",
|
|
69
|
-
};
|
|
70
|
-
}
|
|
71
|
-
const parsed = parseBranch(branch);
|
|
72
|
-
if (isParseError(parsed)) {
|
|
73
|
-
return {
|
|
74
|
-
ok: false,
|
|
75
|
-
message: `Branch '${branch}' does not match the mintree convention.`,
|
|
76
|
-
hint: parsed.hint,
|
|
77
|
-
};
|
|
78
|
-
}
|
|
79
63
|
// The worktree path that git knows about is the *root* of this checkout.
|
|
80
64
|
// Walk up from cwd until we land directly under .mintree/worktrees/<name>.
|
|
81
65
|
const segmentBeneathWorktreesDir = cwdAbs.slice(worktreesDir.length + 1).split(path.sep)[0];
|
|
82
66
|
const worktreePath = segmentBeneathWorktreesDir
|
|
83
67
|
? path.join(worktreesDir, segmentBeneathWorktreesDir)
|
|
84
68
|
: cwdAbs;
|
|
85
|
-
const
|
|
69
|
+
const worktreeDirName = path.basename(worktreePath);
|
|
70
|
+
// IssueId comes from the worktree dir name, not the branch — that way
|
|
71
|
+
// detached-HEAD worktrees (the "current branch" path from the dashboard)
|
|
72
|
+
// still resolve their session_id. Convention guarantees the dir is named
|
|
73
|
+
// `<issueId>-<desc>` for both attached and detached creates.
|
|
74
|
+
const issueIdMatch = worktreeDirName.match(/^(\d+)-/);
|
|
75
|
+
if (!issueIdMatch || !issueIdMatch[1]) {
|
|
76
|
+
return {
|
|
77
|
+
ok: false,
|
|
78
|
+
message: `Worktree directory '${worktreeDirName}' doesn't start with an issue number.`,
|
|
79
|
+
hint: "Expected `<issue>-<desc>` (e.g. 100-claude-md-inicial).",
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
const issueId = issueIdMatch[1];
|
|
83
|
+
const branch = getCurrentBranch(cwdAbs); // null = detached HEAD, that's fine
|
|
84
|
+
const existing = getSessionId(repoRoot, issueId);
|
|
86
85
|
let sessionId;
|
|
87
86
|
let resume;
|
|
88
87
|
if (existing) {
|
|
@@ -91,7 +90,7 @@ function resolve(cwd) {
|
|
|
91
90
|
}
|
|
92
91
|
else {
|
|
93
92
|
sessionId = randomUUID();
|
|
94
|
-
setSessionId(repoRoot,
|
|
93
|
+
setSessionId(repoRoot, issueId, sessionId);
|
|
95
94
|
resume = false;
|
|
96
95
|
}
|
|
97
96
|
return {
|
|
@@ -99,8 +98,9 @@ function resolve(cwd) {
|
|
|
99
98
|
data: {
|
|
100
99
|
repoRoot,
|
|
101
100
|
worktreePath,
|
|
101
|
+
worktreeDirName,
|
|
102
102
|
branch,
|
|
103
|
-
issueId
|
|
103
|
+
issueId,
|
|
104
104
|
sessionId,
|
|
105
105
|
resume,
|
|
106
106
|
},
|
|
@@ -156,6 +156,7 @@ export default function Work({ options }) {
|
|
|
156
156
|
resume: resolved.resume,
|
|
157
157
|
prompt: effectivePrompt,
|
|
158
158
|
cwd: resolved.worktreePath,
|
|
159
|
+
remoteControlName: resolved.worktreeDirName,
|
|
159
160
|
});
|
|
160
161
|
child.on("error", (err) => {
|
|
161
162
|
setState({
|
|
@@ -183,7 +184,7 @@ export default function Work({ options }) {
|
|
|
183
184
|
const { resolved } = state;
|
|
184
185
|
const sessionShort = resolved.sessionId.slice(0, 8);
|
|
185
186
|
const action = resolved.resume ? "resuming" : "starting";
|
|
186
|
-
return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsxs(Box, { marginBottom: 1, children: [_jsx(Text, { bold: true, color: "cyan", children: "mintree worktree work" }), _jsxs(Text, { dimColor: true, children: [" \u00B7 ", resolved.branch] })] }), _jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "session: " }), _jsxs(Text, { children: [sessionShort, "\u2026"] }), _jsxs(Text, { dimColor: true, children: [" (", action, ")"] })] }), _jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "permission-mode: " }), _jsx(Text, { children: options.permissionMode })] }), options.prompt && (_jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "initial prompt: " }), _jsxs(Text, { children: ["\"", truncate(options.prompt, 60), "\""] })] })), _jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "cwd: " }), _jsx(Text, { dimColor: true, children: resolved.worktreePath })] })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "green", bold: true, children: "\u2713 Launching Claude..." }) })] }));
|
|
187
|
+
return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsxs(Box, { marginBottom: 1, children: [_jsx(Text, { bold: true, color: "cyan", children: "mintree worktree work" }), _jsxs(Text, { dimColor: true, children: [" \u00B7 ", resolved.branch ?? `detached @ ${resolved.worktreeDirName}`] })] }), _jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "session: " }), _jsxs(Text, { children: [sessionShort, "\u2026"] }), _jsxs(Text, { dimColor: true, children: [" (", action, ")"] })] }), _jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "permission-mode: " }), _jsx(Text, { children: options.permissionMode })] }), options.prompt && (_jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "initial prompt: " }), _jsxs(Text, { children: ["\"", truncate(options.prompt, 60), "\""] })] })), _jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "cwd: " }), _jsx(Text, { dimColor: true, children: resolved.worktreePath })] })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "green", bold: true, children: "\u2713 Launching Claude..." }) })] }));
|
|
187
188
|
}
|
|
188
189
|
function truncate(s, max) {
|
|
189
190
|
if (s.length <= max)
|
package/dist/lib/claude.d.ts
CHANGED
package/dist/lib/claude.js
CHANGED
|
@@ -60,6 +60,17 @@ export function launchClaude(options) {
|
|
|
60
60
|
else {
|
|
61
61
|
args.push("--session-id", options.sessionId);
|
|
62
62
|
}
|
|
63
|
+
// Always launch with Remote Control. Mintree's whole premise is being able
|
|
64
|
+
// to resume sessions from other devices, so the flag is non-optional —
|
|
65
|
+
// global `remoteControlAtStartup` becomes irrelevant for mintree-launched
|
|
66
|
+
// sessions. The optional name = worktree dir, so the RC UI can identify
|
|
67
|
+
// the session at a glance instead of showing a UUID.
|
|
68
|
+
if (options.remoteControlName) {
|
|
69
|
+
args.push("--remote-control", options.remoteControlName);
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
args.push("--remote-control");
|
|
73
|
+
}
|
|
63
74
|
if (options.prompt && options.prompt.length > 0) {
|
|
64
75
|
args.push("--", promptArg(options.prompt));
|
|
65
76
|
}
|
package/dist/lib/dashboard.d.ts
CHANGED
package/dist/lib/dashboard.js
CHANGED
|
@@ -26,21 +26,21 @@ export async function fetchAssignedIssues() {
|
|
|
26
26
|
}
|
|
27
27
|
/**
|
|
28
28
|
* Builds a map from issue id (number, as string) to the matching mintree
|
|
29
|
-
* worktree
|
|
30
|
-
* the
|
|
31
|
-
*
|
|
29
|
+
* worktree. IssueId comes from the worktree dir name (`<issue>-<desc>`)
|
|
30
|
+
* rather than the branch, so detached worktrees (created via the dashboard's
|
|
31
|
+
* "current branch" mode) are included alongside the regular branch-based
|
|
32
|
+
* ones. Worktrees outside `.mintree/worktrees/` are skipped.
|
|
32
33
|
*/
|
|
33
34
|
function buildWorktreeIndex(repoRoot) {
|
|
34
35
|
const worktreesRoot = path.resolve(getWorktreesDir(repoRoot));
|
|
35
|
-
const
|
|
36
|
+
const dirNameRegex = /^(\d+)-/;
|
|
36
37
|
const index = new Map();
|
|
37
38
|
for (const w of listWorktrees(repoRoot)) {
|
|
38
|
-
if (!w.branch)
|
|
39
|
-
continue;
|
|
40
39
|
const wAbs = path.resolve(w.path);
|
|
41
40
|
if (wAbs !== worktreesRoot && !wAbs.startsWith(worktreesRoot + path.sep))
|
|
42
41
|
continue;
|
|
43
|
-
const
|
|
42
|
+
const dirName = path.basename(wAbs);
|
|
43
|
+
const m = dirName.match(dirNameRegex);
|
|
44
44
|
const issueId = m && m[1] ? m[1] : null;
|
|
45
45
|
if (!issueId)
|
|
46
46
|
continue;
|
|
@@ -115,9 +115,12 @@ export async function loadDashboard(repoRoot) {
|
|
|
115
115
|
const metadata = readMetadata(repoRoot);
|
|
116
116
|
// Fetch PRs in parallel for branches that actually have a worktree —
|
|
117
117
|
// issues without one wouldn't have a branch on this user's repo, so we
|
|
118
|
-
// skip the per-issue gh call for them.
|
|
118
|
+
// skip the per-issue gh call for them. Detached worktrees (branch=null)
|
|
119
|
+
// have no PR by definition.
|
|
119
120
|
const prByBranch = new Map();
|
|
120
|
-
const prFetches = Array.from(worktreesByIssue.values())
|
|
121
|
+
const prFetches = Array.from(worktreesByIssue.values())
|
|
122
|
+
.filter((w) => w.branch !== null)
|
|
123
|
+
.map(async (w) => {
|
|
121
124
|
const pr = await fetchPrForBranch(w.branch);
|
|
122
125
|
if (pr)
|
|
123
126
|
prByBranch.set(w.branch, pr);
|
|
@@ -128,7 +131,7 @@ export async function loadDashboard(repoRoot) {
|
|
|
128
131
|
const worktreeRaw = worktreesByIssue.get(issueId) ?? null;
|
|
129
132
|
const sessionId = metadata.issues[issueId]?.session_id;
|
|
130
133
|
const worktree = worktreeRaw ? { ...worktreeRaw, sessionId } : null;
|
|
131
|
-
const pr = worktree ? (prByBranch.get(worktree.branch) ?? null) : null;
|
|
134
|
+
const pr = worktree && worktree.branch ? (prByBranch.get(worktree.branch) ?? null) : null;
|
|
132
135
|
return {
|
|
133
136
|
issue,
|
|
134
137
|
worktree,
|
|
@@ -34,3 +34,23 @@ export type CreateResult = {
|
|
|
34
34
|
* for the work hand-off when relevant.
|
|
35
35
|
*/
|
|
36
36
|
export declare function runCreate(branchArg: string, opts: CreateOpts): CreateResult;
|
|
37
|
+
export type CreateDetachedOpts = {
|
|
38
|
+
issueId: string;
|
|
39
|
+
descKebab: string;
|
|
40
|
+
work: boolean;
|
|
41
|
+
prompt?: string;
|
|
42
|
+
permissionMode?: PermissionMode;
|
|
43
|
+
};
|
|
44
|
+
/**
|
|
45
|
+
* Variant of `runCreate` that doesn't create a new branch — the worktree is
|
|
46
|
+
* checked out in detached HEAD at the tip of the main repo's current branch.
|
|
47
|
+
* Used by the dashboard's "current branch" overlay mode: lets the user spin
|
|
48
|
+
* up a worktree off whatever they're on (typically `main`) without forcing
|
|
49
|
+
* the `<type>/<issue>-<desc>` convention upfront. They can `git switch -c`
|
|
50
|
+
* later if/when the work warrants a branch.
|
|
51
|
+
*
|
|
52
|
+
* Worktree dir naming follows the same `<issueId>-<desc>` shape as the
|
|
53
|
+
* branch-based flow so `worktree work` can still recover the issueId from
|
|
54
|
+
* the dir name (where it can't read it from the branch).
|
|
55
|
+
*/
|
|
56
|
+
export declare function runCreateDetached(opts: CreateDetachedOpts): CreateResult;
|
|
@@ -3,7 +3,7 @@ import * as os from "os";
|
|
|
3
3
|
import * as path from "path";
|
|
4
4
|
import { execSync } from "child_process";
|
|
5
5
|
import { parseBranch, isParseError } from "./branch.js";
|
|
6
|
-
import { findMainRepoRoot, getMintreeDir, getWorktreesDir, getInitScriptPath, getDefaultBranch, branchExists, worktreeForBranch, addWorktree, pathExists, isExecutable, } from "./git.js";
|
|
6
|
+
import { findMainRepoRoot, getMintreeDir, getWorktreesDir, getInitScriptPath, getDefaultBranch, getCurrentBranch, branchExists, worktreeForBranch, addWorktree, pathExists, isExecutable, } from "./git.js";
|
|
7
7
|
import { upsertIssue } from "./metadata.js";
|
|
8
8
|
function tryRunInitScript(scriptPath, worktreePath, repoRoot) {
|
|
9
9
|
if (!pathExists(scriptPath))
|
|
@@ -182,3 +182,119 @@ export function runCreate(branchArg, opts) {
|
|
|
182
182
|
permissionMode: opts.permissionMode,
|
|
183
183
|
};
|
|
184
184
|
}
|
|
185
|
+
/**
|
|
186
|
+
* Variant of `runCreate` that doesn't create a new branch — the worktree is
|
|
187
|
+
* checked out in detached HEAD at the tip of the main repo's current branch.
|
|
188
|
+
* Used by the dashboard's "current branch" overlay mode: lets the user spin
|
|
189
|
+
* up a worktree off whatever they're on (typically `main`) without forcing
|
|
190
|
+
* the `<type>/<issue>-<desc>` convention upfront. They can `git switch -c`
|
|
191
|
+
* later if/when the work warrants a branch.
|
|
192
|
+
*
|
|
193
|
+
* Worktree dir naming follows the same `<issueId>-<desc>` shape as the
|
|
194
|
+
* branch-based flow so `worktree work` can still recover the issueId from
|
|
195
|
+
* the dir name (where it can't read it from the branch).
|
|
196
|
+
*/
|
|
197
|
+
export function runCreateDetached(opts) {
|
|
198
|
+
const root = findMainRepoRoot();
|
|
199
|
+
if (!root) {
|
|
200
|
+
return {
|
|
201
|
+
ok: false,
|
|
202
|
+
message: "Not in a git repository.",
|
|
203
|
+
hint: "Run `git init` and then `mintree init`.",
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
if (!pathExists(getMintreeDir(root))) {
|
|
207
|
+
return {
|
|
208
|
+
ok: false,
|
|
209
|
+
message: ".mintree/ not found in this repo.",
|
|
210
|
+
hint: "Run `mintree init` first.",
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
if (!/^\d+$/.test(opts.issueId)) {
|
|
214
|
+
return { ok: false, message: `Invalid issueId: ${opts.issueId}` };
|
|
215
|
+
}
|
|
216
|
+
if (!/^[a-z0-9][a-z0-9-]*$/.test(opts.descKebab)) {
|
|
217
|
+
return {
|
|
218
|
+
ok: false,
|
|
219
|
+
message: `Invalid desc: ${opts.descKebab}`,
|
|
220
|
+
hint: "Expected kebab-case starting with [a-z0-9].",
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
const currentBranch = getCurrentBranch(root);
|
|
224
|
+
if (!currentBranch) {
|
|
225
|
+
return {
|
|
226
|
+
ok: false,
|
|
227
|
+
message: "Main repo is in detached HEAD — can't determine current branch to fork from.",
|
|
228
|
+
hint: "Switch the main repo to a branch first (`git switch main`) and try again.",
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
const worktreeDirName = `${opts.issueId}-${opts.descKebab}`;
|
|
232
|
+
const worktreePath = path.join(getWorktreesDir(root), worktreeDirName);
|
|
233
|
+
if (pathExists(worktreePath)) {
|
|
234
|
+
return {
|
|
235
|
+
ok: false,
|
|
236
|
+
message: `Worktree directory already exists: ${worktreePath}`,
|
|
237
|
+
hint: "Remove it first or pick a different description.",
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
const steps = [];
|
|
241
|
+
steps.push({
|
|
242
|
+
kind: "ok",
|
|
243
|
+
label: "detached worktree",
|
|
244
|
+
detail: `issue=${opts.issueId}, base=${currentBranch}`,
|
|
245
|
+
});
|
|
246
|
+
try {
|
|
247
|
+
execSync(`git worktree add --detach '${worktreePath.replace(/'/g, `'\\''`)}' '${currentBranch.replace(/'/g, `'\\''`)}'`, { cwd: root, stdio: ["ignore", "pipe", "pipe"] });
|
|
248
|
+
}
|
|
249
|
+
catch (err) {
|
|
250
|
+
const stderr = err && typeof err === "object" && "stderr" in err
|
|
251
|
+
? String(err.stderr).trim()
|
|
252
|
+
: err instanceof Error
|
|
253
|
+
? err.message
|
|
254
|
+
: String(err);
|
|
255
|
+
return { ok: false, message: `git worktree add --detach failed: ${stderr}` };
|
|
256
|
+
}
|
|
257
|
+
steps.push({
|
|
258
|
+
kind: "ok",
|
|
259
|
+
label: "checked out detached HEAD",
|
|
260
|
+
detail: `at tip of ${currentBranch}`,
|
|
261
|
+
});
|
|
262
|
+
steps.push({ kind: "ok", label: "worktree created", detail: worktreePath });
|
|
263
|
+
upsertIssue(root, opts.issueId, { base_branch: currentBranch });
|
|
264
|
+
steps.push({ kind: "ok", label: "metadata updated", detail: `issue ${opts.issueId}` });
|
|
265
|
+
const initShPath = getInitScriptPath(root);
|
|
266
|
+
const initResult = tryRunInitScript(initShPath, worktreePath, root);
|
|
267
|
+
if (initResult.ran) {
|
|
268
|
+
steps.push({ kind: "ok", label: "ran .mintree/init.sh", detail: worktreePath });
|
|
269
|
+
}
|
|
270
|
+
else if (initResult.error) {
|
|
271
|
+
steps.push({ kind: "warn", label: "init.sh failed", detail: initResult.error });
|
|
272
|
+
}
|
|
273
|
+
else if (!pathExists(initShPath)) {
|
|
274
|
+
steps.push({ kind: "skip", label: "no init.sh (skipping post-create hook)" });
|
|
275
|
+
}
|
|
276
|
+
let promptFile;
|
|
277
|
+
if (opts.work && opts.prompt && opts.prompt.length > 0) {
|
|
278
|
+
try {
|
|
279
|
+
promptFile = writePromptFile(opts.prompt);
|
|
280
|
+
}
|
|
281
|
+
catch (err) {
|
|
282
|
+
steps.push({
|
|
283
|
+
kind: "warn",
|
|
284
|
+
label: "failed to stage --prompt for hand-off",
|
|
285
|
+
detail: err instanceof Error ? err.message : String(err),
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
return {
|
|
290
|
+
ok: true,
|
|
291
|
+
steps,
|
|
292
|
+
worktreePath,
|
|
293
|
+
branch: `detached @ ${currentBranch}`,
|
|
294
|
+
issueId: opts.issueId,
|
|
295
|
+
base: currentBranch,
|
|
296
|
+
work: opts.work,
|
|
297
|
+
promptFile,
|
|
298
|
+
permissionMode: opts.permissionMode,
|
|
299
|
+
};
|
|
300
|
+
}
|
|
@@ -19,3 +19,10 @@ export type RemoveResult = {
|
|
|
19
19
|
* resume the same Claude session.
|
|
20
20
|
*/
|
|
21
21
|
export declare function runRemove(branchArg: string, force: boolean): RemoveResult;
|
|
22
|
+
/**
|
|
23
|
+
* Path-keyed counterpart to `runRemove`, used for worktrees that don't have
|
|
24
|
+
* a parseable branch (detached HEAD ones created via the dashboard's
|
|
25
|
+
* "current branch" mode). Same dirty/force/prune semantics as runRemove —
|
|
26
|
+
* just skips the `parseBranch` step and reports the worktree by its path.
|
|
27
|
+
*/
|
|
28
|
+
export declare function runRemoveByPath(worktreePath: string, force: boolean): RemoveResult;
|
|
@@ -82,3 +82,71 @@ export function runRemove(branchArg, force) {
|
|
|
82
82
|
wasDirty: dirty,
|
|
83
83
|
};
|
|
84
84
|
}
|
|
85
|
+
/**
|
|
86
|
+
* Path-keyed counterpart to `runRemove`, used for worktrees that don't have
|
|
87
|
+
* a parseable branch (detached HEAD ones created via the dashboard's
|
|
88
|
+
* "current branch" mode). Same dirty/force/prune semantics as runRemove —
|
|
89
|
+
* just skips the `parseBranch` step and reports the worktree by its path.
|
|
90
|
+
*/
|
|
91
|
+
export function runRemoveByPath(worktreePath, force) {
|
|
92
|
+
const root = findMainRepoRoot();
|
|
93
|
+
if (!root) {
|
|
94
|
+
return {
|
|
95
|
+
ok: false,
|
|
96
|
+
message: "Not in a git repository.",
|
|
97
|
+
hint: "Run `git init` and then `mintree init`.",
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
if (!pathExists(getMintreeDir(root))) {
|
|
101
|
+
return {
|
|
102
|
+
ok: false,
|
|
103
|
+
message: ".mintree/ not found in this repo.",
|
|
104
|
+
hint: "Run `mintree init` first.",
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
const label = worktreePath.split("/").pop() ?? worktreePath;
|
|
108
|
+
if (!pathExists(worktreePath)) {
|
|
109
|
+
try {
|
|
110
|
+
pruneWorktrees(root);
|
|
111
|
+
}
|
|
112
|
+
catch (err) {
|
|
113
|
+
return {
|
|
114
|
+
ok: false,
|
|
115
|
+
message: `git worktree prune failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
return {
|
|
119
|
+
ok: true,
|
|
120
|
+
branch: label,
|
|
121
|
+
worktreePath,
|
|
122
|
+
variant: "pruned-orphan",
|
|
123
|
+
wasDirty: false,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
const dirty = isDirty(worktreePath);
|
|
127
|
+
if (dirty && !force) {
|
|
128
|
+
return {
|
|
129
|
+
ok: false,
|
|
130
|
+
message: `Worktree at ${worktreePath} has uncommitted changes.`,
|
|
131
|
+
hint: "Commit/stash first, or pass --force to discard them.",
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
try {
|
|
135
|
+
removeWorktree({ repoRoot: root, worktreePath, force });
|
|
136
|
+
}
|
|
137
|
+
catch (err) {
|
|
138
|
+
const stderr = err && typeof err === "object" && "stderr" in err
|
|
139
|
+
? String(err.stderr).trim()
|
|
140
|
+
: err instanceof Error
|
|
141
|
+
? err.message
|
|
142
|
+
: String(err);
|
|
143
|
+
return { ok: false, message: `git worktree remove failed: ${stderr}` };
|
|
144
|
+
}
|
|
145
|
+
return {
|
|
146
|
+
ok: true,
|
|
147
|
+
branch: label,
|
|
148
|
+
worktreePath,
|
|
149
|
+
variant: "removed",
|
|
150
|
+
wasDirty: dirty,
|
|
151
|
+
};
|
|
152
|
+
}
|
package/package.json
CHANGED