mintree 0.1.2
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 +188 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +12 -0
- package/dist/commands/dashboard.d.ts +2 -0
- package/dist/commands/dashboard.js +849 -0
- package/dist/commands/doctor.d.ts +2 -0
- package/dist/commands/doctor.js +327 -0
- package/dist/commands/helpers/index.d.ts +1 -0
- package/dist/commands/helpers/index.js +1 -0
- package/dist/commands/helpers/session-signal/end.d.ts +2 -0
- package/dist/commands/helpers/session-signal/end.js +9 -0
- package/dist/commands/helpers/session-signal/index.d.ts +1 -0
- package/dist/commands/helpers/session-signal/index.js +1 -0
- package/dist/commands/helpers/session-signal/install.d.ts +2 -0
- package/dist/commands/helpers/session-signal/install.js +25 -0
- package/dist/commands/helpers/session-signal/notification.d.ts +2 -0
- package/dist/commands/helpers/session-signal/notification.js +9 -0
- package/dist/commands/helpers/session-signal/prompt.d.ts +2 -0
- package/dist/commands/helpers/session-signal/prompt.js +9 -0
- package/dist/commands/helpers/session-signal/stop.d.ts +2 -0
- package/dist/commands/helpers/session-signal/stop.js +9 -0
- package/dist/commands/helpers/shell-init.d.ts +11 -0
- package/dist/commands/helpers/shell-init.js +111 -0
- package/dist/commands/index.d.ts +2 -0
- package/dist/commands/index.js +6 -0
- package/dist/commands/init.d.ts +2 -0
- package/dist/commands/init.js +129 -0
- package/dist/commands/worktree/clean.d.ts +11 -0
- package/dist/commands/worktree/clean.js +206 -0
- package/dist/commands/worktree/create.d.ts +18 -0
- package/dist/commands/worktree/create.js +93 -0
- package/dist/commands/worktree/index.d.ts +1 -0
- package/dist/commands/worktree/index.js +1 -0
- package/dist/commands/worktree/list.d.ts +10 -0
- package/dist/commands/worktree/list.js +143 -0
- package/dist/commands/worktree/remove.d.ts +12 -0
- package/dist/commands/worktree/remove.js +46 -0
- package/dist/commands/worktree/work.d.ts +15 -0
- package/dist/commands/worktree/work.js +192 -0
- package/dist/lib/branch.d.ts +26 -0
- package/dist/lib/branch.js +57 -0
- package/dist/lib/claude.d.ts +26 -0
- package/dist/lib/claude.js +67 -0
- package/dist/lib/dashboard.d.ts +50 -0
- package/dist/lib/dashboard.js +139 -0
- package/dist/lib/exec.d.ts +2 -0
- package/dist/lib/exec.js +15 -0
- package/dist/lib/git.d.ts +110 -0
- package/dist/lib/git.js +320 -0
- package/dist/lib/github.d.ts +7 -0
- package/dist/lib/github.js +15 -0
- package/dist/lib/markers.d.ts +21 -0
- package/dist/lib/markers.js +43 -0
- package/dist/lib/metadata.d.ts +18 -0
- package/dist/lib/metadata.js +44 -0
- package/dist/lib/session-signal.d.ts +63 -0
- package/dist/lib/session-signal.js +160 -0
- package/dist/lib/worktreeCreate.d.ts +36 -0
- package/dist/lib/worktreeCreate.js +184 -0
- package/dist/lib/worktreeRemove.d.ts +21 -0
- package/dist/lib/worktreeRemove.js +84 -0
- package/package.json +63 -0
- package/shell/init.bash +106 -0
- package/shell/init.zsh +125 -0
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useState } from "react";
|
|
3
|
+
import { Box, Text } from "ink";
|
|
4
|
+
import Spinner from "ink-spinner";
|
|
5
|
+
import * as fs from "fs";
|
|
6
|
+
import { findMainRepoRoot, getMintreeDir, getMetadataPath, getWorktreesDir, getSessionStatesDir, ensureGitignoreEntries, isGitTracked, } from "../lib/git.js";
|
|
7
|
+
export const description = "Initialize the current repo for mintree (creates .mintree/, updates .gitignore)";
|
|
8
|
+
const METADATA_TEMPLATE = {
|
|
9
|
+
version: 1,
|
|
10
|
+
issues: {},
|
|
11
|
+
};
|
|
12
|
+
function ensureDir(p, label, steps) {
|
|
13
|
+
if (fs.existsSync(p)) {
|
|
14
|
+
steps.push({ kind: "exists", label });
|
|
15
|
+
}
|
|
16
|
+
else {
|
|
17
|
+
fs.mkdirSync(p, { recursive: true });
|
|
18
|
+
steps.push({ kind: "created", label });
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
function ensureMetadata(metadataPath, steps) {
|
|
22
|
+
if (fs.existsSync(metadataPath)) {
|
|
23
|
+
steps.push({ kind: "exists", label: ".mintree/metadata.json" });
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
fs.writeFileSync(metadataPath, JSON.stringify(METADATA_TEMPLATE, null, 2) + "\n");
|
|
27
|
+
steps.push({ kind: "created", label: ".mintree/metadata.json" });
|
|
28
|
+
}
|
|
29
|
+
function runInit() {
|
|
30
|
+
const root = findMainRepoRoot();
|
|
31
|
+
if (!root) {
|
|
32
|
+
return {
|
|
33
|
+
ok: false,
|
|
34
|
+
message: "Not in a git repository.",
|
|
35
|
+
hint: "Run `git init` first, then re-run `mintree init`.",
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
const steps = [];
|
|
39
|
+
const mintreeDir = getMintreeDir(root);
|
|
40
|
+
const worktreesDir = getWorktreesDir(root);
|
|
41
|
+
const sessionStatesDir = getSessionStatesDir(root);
|
|
42
|
+
const metadataPath = getMetadataPath(root);
|
|
43
|
+
ensureDir(mintreeDir, ".mintree/", steps);
|
|
44
|
+
ensureDir(worktreesDir, ".mintree/worktrees/", steps);
|
|
45
|
+
ensureDir(sessionStatesDir, ".mintree/session-states/", steps);
|
|
46
|
+
ensureMetadata(metadataPath, steps);
|
|
47
|
+
// metadata.json holds the per-issue session_id, which is local-only by
|
|
48
|
+
// nature (each dev gets their own UUIDs from `claude`). Versioning it
|
|
49
|
+
// would only generate noise + merge conflicts, so it's gitignored along
|
|
50
|
+
// with the worktrees and session-states directories.
|
|
51
|
+
const ignoreCandidates = [
|
|
52
|
+
".mintree/worktrees/",
|
|
53
|
+
".mintree/session-states/",
|
|
54
|
+
".mintree/metadata.json",
|
|
55
|
+
];
|
|
56
|
+
const added = ensureGitignoreEntries(root, ignoreCandidates);
|
|
57
|
+
for (const entry of ignoreCandidates) {
|
|
58
|
+
steps.push({
|
|
59
|
+
kind: added.includes(entry) ? "added" : "ignored",
|
|
60
|
+
label: `${entry} → .gitignore`,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
// If metadata.json was committed before being gitignored (likely on a
|
|
64
|
+
// repo that ran an earlier mintree version), gitignore alone won't
|
|
65
|
+
// stop git from tracking it. Surface an actionable hint so the user
|
|
66
|
+
// knows exactly what to run.
|
|
67
|
+
if (isGitTracked(".mintree/metadata.json", root)) {
|
|
68
|
+
steps.push({
|
|
69
|
+
kind: "warn",
|
|
70
|
+
label: ".mintree/metadata.json is currently tracked by git",
|
|
71
|
+
hint: "Run: git rm --cached .mintree/metadata.json && git commit -m 'chore: untrack mintree metadata'",
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
return { ok: true, repoRoot: root, steps };
|
|
75
|
+
}
|
|
76
|
+
function StepIcon({ kind }) {
|
|
77
|
+
switch (kind) {
|
|
78
|
+
case "created":
|
|
79
|
+
case "added":
|
|
80
|
+
return _jsx(Text, { color: "green", children: "\u2713" });
|
|
81
|
+
case "exists":
|
|
82
|
+
case "ignored":
|
|
83
|
+
return _jsx(Text, { color: "cyan", children: "\u25CB" });
|
|
84
|
+
case "warn":
|
|
85
|
+
return _jsx(Text, { color: "yellow", children: "!" });
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
function stepDetail(kind) {
|
|
89
|
+
switch (kind) {
|
|
90
|
+
case "created":
|
|
91
|
+
return "created";
|
|
92
|
+
case "exists":
|
|
93
|
+
return "already exists";
|
|
94
|
+
case "added":
|
|
95
|
+
return "added";
|
|
96
|
+
case "ignored":
|
|
97
|
+
return "already ignored";
|
|
98
|
+
case "warn":
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
export default function Init() {
|
|
103
|
+
const [result, setResult] = useState(null);
|
|
104
|
+
useEffect(() => {
|
|
105
|
+
// Defer one tick so the initial render with the spinner gets to paint.
|
|
106
|
+
setTimeout(() => {
|
|
107
|
+
try {
|
|
108
|
+
setResult(runInit());
|
|
109
|
+
}
|
|
110
|
+
catch (err) {
|
|
111
|
+
setResult({
|
|
112
|
+
ok: false,
|
|
113
|
+
message: err instanceof Error ? err.message : String(err),
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
}, 0);
|
|
117
|
+
}, []);
|
|
118
|
+
if (!result) {
|
|
119
|
+
return (_jsxs(Box, { children: [_jsx(Text, { color: "cyan", children: _jsx(Spinner, { type: "dots" }) }), _jsx(Text, { children: " Initializing mintree..." })] }));
|
|
120
|
+
}
|
|
121
|
+
if (!result.ok) {
|
|
122
|
+
return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsxs(Text, { color: "red", bold: true, children: ["\u2717 ", result.message] }), result.hint && (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: "yellow", children: ["\u21B3 ", result.hint] }) }))] }));
|
|
123
|
+
}
|
|
124
|
+
const anyChange = result.steps.some(s => s.kind === "created" || s.kind === "added");
|
|
125
|
+
return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsxs(Box, { marginBottom: 1, children: [_jsx(Text, { bold: true, color: "cyan", children: "mintree init" }), _jsxs(Text, { dimColor: true, children: [" \u00B7 ", result.repoRoot] })] }), result.steps.map((step, i) => {
|
|
126
|
+
const detail = stepDetail(step.kind);
|
|
127
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(StepIcon, { kind: step.kind }), _jsx(Text, { children: " " }), _jsx(Text, { children: step.label }), detail && _jsxs(Text, { dimColor: true, children: [" (", detail, ")"] })] }), step.hint && (_jsx(Box, { marginLeft: 2, children: _jsxs(Text, { color: "yellow", children: ["\u21B3 ", step.hint] }) }))] }, i));
|
|
128
|
+
}), _jsx(Box, { marginTop: 1, children: anyChange ? (_jsxs(Text, { color: "green", children: ["mintree initialized. Run ", _jsx(Text, { bold: true, children: "mintree doctor" }), " to verify the rest of your setup."] })) : (_jsx(Text, { color: "cyan", children: "mintree was already initialized \u2014 nothing to do." })) })] }));
|
|
129
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export declare const description = "Remove worktrees whose PR is merged or closed";
|
|
3
|
+
export declare const options: z.ZodObject<{
|
|
4
|
+
yes: z.ZodDefault<z.ZodBoolean>;
|
|
5
|
+
force: z.ZodDefault<z.ZodBoolean>;
|
|
6
|
+
}, z.core.$strip>;
|
|
7
|
+
type Props = {
|
|
8
|
+
options: z.infer<typeof options>;
|
|
9
|
+
};
|
|
10
|
+
export default function Clean({ options }: Props): import("react/jsx-runtime").JSX.Element;
|
|
11
|
+
export {};
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useState } from "react";
|
|
3
|
+
import { Box, Text, useApp, useInput } from "ink";
|
|
4
|
+
import Spinner from "ink-spinner";
|
|
5
|
+
import { option } from "pastel";
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
import * as path from "path";
|
|
8
|
+
import { findMainRepoRoot, getMintreeDir, getWorktreesDir, listWorktrees, isDirty, removeWorktree, pathExists, } from "../../lib/git.js";
|
|
9
|
+
import { tryExec } from "../../lib/exec.js";
|
|
10
|
+
export const description = "Remove worktrees whose PR is merged or closed";
|
|
11
|
+
export const options = z.object({
|
|
12
|
+
yes: z
|
|
13
|
+
.boolean()
|
|
14
|
+
.default(false)
|
|
15
|
+
.describe(option({
|
|
16
|
+
description: "Skip the confirmation prompt (required for non-interactive shells)",
|
|
17
|
+
})),
|
|
18
|
+
force: z
|
|
19
|
+
.boolean()
|
|
20
|
+
.default(false)
|
|
21
|
+
.describe(option({
|
|
22
|
+
description: "Include worktrees with uncommitted changes (clean is conservative by default)",
|
|
23
|
+
})),
|
|
24
|
+
});
|
|
25
|
+
function shQuote(value) {
|
|
26
|
+
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
27
|
+
}
|
|
28
|
+
async function fetchPr(branch) {
|
|
29
|
+
const out = await tryExec(`gh pr list --head ${shQuote(branch)} --state all --json number,state --limit 1 2>/dev/null`);
|
|
30
|
+
if (!out)
|
|
31
|
+
return null;
|
|
32
|
+
try {
|
|
33
|
+
const arr = JSON.parse(out);
|
|
34
|
+
if (Array.isArray(arr) && arr.length > 0 && arr[0])
|
|
35
|
+
return arr[0];
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
// fall through
|
|
39
|
+
}
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
async function loadCandidates(force) {
|
|
43
|
+
const root = findMainRepoRoot();
|
|
44
|
+
if (!root) {
|
|
45
|
+
return {
|
|
46
|
+
phase: "error",
|
|
47
|
+
message: "Not in a git repository.",
|
|
48
|
+
hint: "Run `git init` and then `mintree init`.",
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
if (!pathExists(getMintreeDir(root))) {
|
|
52
|
+
return {
|
|
53
|
+
phase: "error",
|
|
54
|
+
message: ".mintree/ not found in this repo.",
|
|
55
|
+
hint: "Run `mintree init` first.",
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
const worktreesDir = getWorktreesDir(root);
|
|
59
|
+
const all = listWorktrees(root);
|
|
60
|
+
const ours = all.filter((w) => {
|
|
61
|
+
if (!w.branch)
|
|
62
|
+
return false;
|
|
63
|
+
const wAbs = path.resolve(w.path);
|
|
64
|
+
const dirAbs = path.resolve(worktreesDir);
|
|
65
|
+
return wAbs === dirAbs || wAbs.startsWith(dirAbs + path.sep);
|
|
66
|
+
});
|
|
67
|
+
if (ours.length === 0) {
|
|
68
|
+
return { phase: "nothing", message: "No mintree worktrees in this repo. Nothing to clean." };
|
|
69
|
+
}
|
|
70
|
+
const prs = await Promise.all(ours.map(w => fetchPr(w.branch)));
|
|
71
|
+
const candidates = [];
|
|
72
|
+
for (let i = 0; i < ours.length; i++) {
|
|
73
|
+
const w = ours[i];
|
|
74
|
+
const pr = prs[i];
|
|
75
|
+
if (!w || !w.branch)
|
|
76
|
+
continue;
|
|
77
|
+
if (!pr || pr.state === "OPEN")
|
|
78
|
+
continue; // only candidates with closed/merged PRs
|
|
79
|
+
const dirty = pathExists(w.path) ? isDirty(w.path) : false;
|
|
80
|
+
const skipForDirty = dirty && !force;
|
|
81
|
+
candidates.push({
|
|
82
|
+
worktreePath: w.path,
|
|
83
|
+
branch: w.branch,
|
|
84
|
+
dirty,
|
|
85
|
+
pr,
|
|
86
|
+
willClean: !skipForDirty,
|
|
87
|
+
reasonSkipped: skipForDirty ? "dirty (pass --force to include)" : undefined,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
if (candidates.length === 0) {
|
|
91
|
+
return {
|
|
92
|
+
phase: "nothing",
|
|
93
|
+
message: "All mintree worktrees still have an open PR (or no PR at all). Nothing to clean.",
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
return { phase: "prompt", repoRoot: root, candidates };
|
|
97
|
+
}
|
|
98
|
+
function executeRemovals(repoRoot, candidates) {
|
|
99
|
+
const toRemove = candidates.filter(c => c.willClean);
|
|
100
|
+
const results = [];
|
|
101
|
+
for (const c of toRemove) {
|
|
102
|
+
try {
|
|
103
|
+
removeWorktree({ repoRoot, worktreePath: c.worktreePath, force: c.dirty });
|
|
104
|
+
results.push({ branch: c.branch, ok: true });
|
|
105
|
+
}
|
|
106
|
+
catch (err) {
|
|
107
|
+
const stderr = err && typeof err === "object" && "stderr" in err
|
|
108
|
+
? String(err.stderr).trim()
|
|
109
|
+
: err instanceof Error
|
|
110
|
+
? err.message
|
|
111
|
+
: String(err);
|
|
112
|
+
results.push({ branch: c.branch, ok: false, error: stderr });
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return results;
|
|
116
|
+
}
|
|
117
|
+
function PrTag({ pr }) {
|
|
118
|
+
const color = pr.state === "MERGED" ? "magenta" : pr.state === "CLOSED" ? "yellow" : "green";
|
|
119
|
+
return (_jsxs(Text, { children: [_jsxs(Text, { children: ["#", pr.number] }), " ", _jsx(Text, { color: color, children: pr.state })] }));
|
|
120
|
+
}
|
|
121
|
+
export default function Clean({ options }) {
|
|
122
|
+
const { exit } = useApp();
|
|
123
|
+
const [state, setState] = useState({ phase: "loading", message: "Inspecting worktrees..." });
|
|
124
|
+
useEffect(() => {
|
|
125
|
+
(async () => {
|
|
126
|
+
try {
|
|
127
|
+
const next = await loadCandidates(options.force);
|
|
128
|
+
if (next.phase === "prompt") {
|
|
129
|
+
// In non-interactive environments useInput will never fire, so we
|
|
130
|
+
// require --yes up front rather than hanging the user.
|
|
131
|
+
if (!process.stdin.isTTY && !options.yes) {
|
|
132
|
+
setState({
|
|
133
|
+
phase: "error",
|
|
134
|
+
message: "Confirmation required but stdin is not a TTY (running non-interactive).",
|
|
135
|
+
hint: "Re-run with `--yes` to skip the prompt.",
|
|
136
|
+
});
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
if (options.yes) {
|
|
140
|
+
setState({
|
|
141
|
+
phase: "executing",
|
|
142
|
+
repoRoot: next.repoRoot,
|
|
143
|
+
candidates: next.candidates,
|
|
144
|
+
});
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
setState(next);
|
|
149
|
+
}
|
|
150
|
+
catch (err) {
|
|
151
|
+
setState({
|
|
152
|
+
phase: "error",
|
|
153
|
+
message: err instanceof Error ? err.message : String(err),
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
})();
|
|
157
|
+
}, [options.force, options.yes]);
|
|
158
|
+
useEffect(() => {
|
|
159
|
+
if (state.phase === "executing") {
|
|
160
|
+
const results = executeRemovals(state.repoRoot, state.candidates);
|
|
161
|
+
setState({ phase: "done", results, cancelled: false });
|
|
162
|
+
}
|
|
163
|
+
}, [state.phase]);
|
|
164
|
+
useEffect(() => {
|
|
165
|
+
if (state.phase === "done" || state.phase === "error" || state.phase === "nothing") {
|
|
166
|
+
// Defer one tick so the final UI paints before Ink unmounts.
|
|
167
|
+
const t = setTimeout(() => exit(), 50);
|
|
168
|
+
return () => clearTimeout(t);
|
|
169
|
+
}
|
|
170
|
+
return;
|
|
171
|
+
}, [state.phase, exit]);
|
|
172
|
+
useInput((input, key) => {
|
|
173
|
+
if (state.phase !== "prompt")
|
|
174
|
+
return;
|
|
175
|
+
if (input === "y" || input === "Y") {
|
|
176
|
+
setState({
|
|
177
|
+
phase: "executing",
|
|
178
|
+
repoRoot: state.repoRoot,
|
|
179
|
+
candidates: state.candidates,
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
else if (input === "n" || input === "N" || key.return || key.escape) {
|
|
183
|
+
setState({ phase: "done", results: [], cancelled: true });
|
|
184
|
+
}
|
|
185
|
+
}, { isActive: state.phase === "prompt" });
|
|
186
|
+
if (state.phase === "loading") {
|
|
187
|
+
return (_jsxs(Box, { children: [_jsx(Text, { color: "cyan", children: _jsx(Spinner, { type: "dots" }) }), _jsxs(Text, { children: [" ", state.message] })] }));
|
|
188
|
+
}
|
|
189
|
+
if (state.phase === "error") {
|
|
190
|
+
return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsxs(Text, { color: "red", bold: true, children: ["\u2717 ", state.message] }), state.hint && (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: "yellow", children: ["\u21B3 ", state.hint] }) }))] }));
|
|
191
|
+
}
|
|
192
|
+
if (state.phase === "nothing") {
|
|
193
|
+
return (_jsx(Box, { padding: 1, children: _jsx(Text, { dimColor: true, children: state.message }) }));
|
|
194
|
+
}
|
|
195
|
+
if (state.phase === "prompt" || state.phase === "executing") {
|
|
196
|
+
const willCleanCount = state.candidates.filter(c => c.willClean).length;
|
|
197
|
+
return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsxs(Box, { marginBottom: 1, children: [_jsx(Text, { bold: true, color: "cyan", children: "mintree worktree clean" }), _jsxs(Text, { dimColor: true, children: [" \u00B7 ", state.candidates.length, " candidate(s)"] })] }), state.candidates.map((c, i) => (_jsxs(Box, { children: [_jsx(Text, { color: c.willClean ? "green" : "yellow", children: c.willClean ? "✓" : "○" }), _jsx(Text, { children: " " }), _jsx(Text, { color: "cyan", children: c.branch }), _jsx(Text, { children: " " }), _jsx(PrTag, { pr: c.pr }), c.dirty && _jsx(Text, { color: "yellow", children: " [dirty]" }), c.reasonSkipped && _jsxs(Text, { dimColor: true, children: [" \u2014 ", c.reasonSkipped] })] }, i))), _jsx(Box, { marginTop: 1, children: state.phase === "prompt" ? (_jsxs(Text, { children: ["Remove ", willCleanCount, " worktree(s)?", " ", _jsx(Text, { bold: true, children: "[y/N]" })] })) : (_jsxs(Box, { children: [_jsx(Text, { color: "cyan", children: _jsx(Spinner, { type: "dots" }) }), _jsx(Text, { children: " Removing..." })] })) })] }));
|
|
198
|
+
}
|
|
199
|
+
// state.phase === "done"
|
|
200
|
+
if (state.cancelled) {
|
|
201
|
+
return (_jsx(Box, { padding: 1, children: _jsx(Text, { dimColor: true, children: "Cancelled. No worktrees were removed." }) }));
|
|
202
|
+
}
|
|
203
|
+
const okCount = state.results.filter(r => r.ok).length;
|
|
204
|
+
const failCount = state.results.length - okCount;
|
|
205
|
+
return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, color: "cyan", children: "mintree worktree clean \u00B7 done" }) }), state.results.map((r, i) => (_jsxs(Box, { children: [_jsx(Text, { color: r.ok ? "green" : "red", children: r.ok ? "✓" : "✗" }), _jsx(Text, { children: " " }), _jsx(Text, { color: "cyan", children: r.branch }), !r.ok && _jsxs(Text, { color: "red", children: [" \u2014 ", r.error] })] }, i))), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { children: ["Removed ", okCount, failCount > 0 && (_jsxs(_Fragment, { children: [", ", _jsxs(Text, { color: "red", children: [failCount, " failed"] })] })), ". Branches and metadata preserved."] }) })] }));
|
|
206
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export declare const description = "Create a worktree for an issue branch";
|
|
3
|
+
export declare const args: z.ZodTuple<[z.ZodString], null>;
|
|
4
|
+
export declare const options: z.ZodObject<{
|
|
5
|
+
base: z.ZodOptional<z.ZodString>;
|
|
6
|
+
work: z.ZodDefault<z.ZodBoolean>;
|
|
7
|
+
prompt: z.ZodOptional<z.ZodString>;
|
|
8
|
+
permissionMode: z.ZodOptional<z.ZodEnum<{
|
|
9
|
+
default: "default";
|
|
10
|
+
auto: "auto";
|
|
11
|
+
}>>;
|
|
12
|
+
}, z.core.$strip>;
|
|
13
|
+
type Props = {
|
|
14
|
+
args: z.infer<typeof args>;
|
|
15
|
+
options: z.infer<typeof options>;
|
|
16
|
+
};
|
|
17
|
+
export default function Create({ args, options }: Props): import("react/jsx-runtime").JSX.Element;
|
|
18
|
+
export {};
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useState } from "react";
|
|
3
|
+
import { Box, Text } from "ink";
|
|
4
|
+
import Spinner from "ink-spinner";
|
|
5
|
+
import { argument, option } from "pastel";
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
import { PERMISSION_MODES } from "../../lib/claude.js";
|
|
8
|
+
import { runCreate } from "../../lib/worktreeCreate.js";
|
|
9
|
+
import { buildCreateMarkers, emitMarkers } from "../../lib/markers.js";
|
|
10
|
+
export const description = "Create a worktree for an issue branch";
|
|
11
|
+
export const args = z.tuple([
|
|
12
|
+
z
|
|
13
|
+
.string()
|
|
14
|
+
.describe(argument({
|
|
15
|
+
name: "branch",
|
|
16
|
+
description: "Branch in `<type>/<issue>-<kebab-desc>` format (e.g. feat/100-claude-md-inicial)",
|
|
17
|
+
})),
|
|
18
|
+
]);
|
|
19
|
+
export const options = z.object({
|
|
20
|
+
base: z
|
|
21
|
+
.string()
|
|
22
|
+
.optional()
|
|
23
|
+
.describe(option({
|
|
24
|
+
description: "Base branch to fork from (defaults to origin/HEAD or main/master)",
|
|
25
|
+
})),
|
|
26
|
+
work: z
|
|
27
|
+
.boolean()
|
|
28
|
+
.default(false)
|
|
29
|
+
.describe(option({
|
|
30
|
+
description: "After creating, launch Claude in the new worktree (requires the shell wrapper)",
|
|
31
|
+
})),
|
|
32
|
+
prompt: z
|
|
33
|
+
.string()
|
|
34
|
+
.optional()
|
|
35
|
+
.describe(option({
|
|
36
|
+
description: "Initial prompt to inject into Claude (only meaningful with --work; literal injection)",
|
|
37
|
+
})),
|
|
38
|
+
permissionMode: z
|
|
39
|
+
.enum(PERMISSION_MODES)
|
|
40
|
+
.optional()
|
|
41
|
+
.describe(option({
|
|
42
|
+
description: `Claude --permission-mode passed through to --work (one of: ${PERMISSION_MODES.join(", ")})`,
|
|
43
|
+
alias: "m",
|
|
44
|
+
})),
|
|
45
|
+
});
|
|
46
|
+
function StepIcon({ kind }) {
|
|
47
|
+
if (kind === "ok")
|
|
48
|
+
return _jsx(Text, { color: "green", children: "\u2713" });
|
|
49
|
+
if (kind === "warn")
|
|
50
|
+
return _jsx(Text, { color: "yellow", children: "!" });
|
|
51
|
+
return _jsx(Text, { color: "cyan", children: "\u25CB" });
|
|
52
|
+
}
|
|
53
|
+
export default function Create({ args, options }) {
|
|
54
|
+
const [branch] = args;
|
|
55
|
+
const [result, setResult] = useState(null);
|
|
56
|
+
useEffect(() => {
|
|
57
|
+
setTimeout(() => {
|
|
58
|
+
try {
|
|
59
|
+
setResult(runCreate(branch, {
|
|
60
|
+
base: options.base,
|
|
61
|
+
work: options.work,
|
|
62
|
+
prompt: options.prompt,
|
|
63
|
+
permissionMode: options.permissionMode,
|
|
64
|
+
}));
|
|
65
|
+
}
|
|
66
|
+
catch (err) {
|
|
67
|
+
setResult({ ok: false, message: err instanceof Error ? err.message : String(err) });
|
|
68
|
+
}
|
|
69
|
+
}, 0);
|
|
70
|
+
}, [branch, options.base, options.work, options.prompt, options.permissionMode]);
|
|
71
|
+
// Emit shell-wrapper markers when create succeeded. Goes through the
|
|
72
|
+
// emitMarkers helper so it lands in MINTREE_MARKER_FILE if set, otherwise
|
|
73
|
+
// stdout. Bypasses Ink so word-wrap can't split a long path mid-marker.
|
|
74
|
+
useEffect(() => {
|
|
75
|
+
if (!result || !result.ok)
|
|
76
|
+
return;
|
|
77
|
+
emitMarkers(buildCreateMarkers({
|
|
78
|
+
worktreePath: result.worktreePath,
|
|
79
|
+
work: result.work,
|
|
80
|
+
promptFile: result.promptFile,
|
|
81
|
+
permissionMode: result.permissionMode,
|
|
82
|
+
}));
|
|
83
|
+
}, [result]);
|
|
84
|
+
if (!result) {
|
|
85
|
+
return (_jsxs(Box, { children: [_jsx(Text, { color: "cyan", children: _jsx(Spinner, { type: "dots" }) }), _jsxs(Text, { children: [" Creating worktree for ", branch, "..."] })] }));
|
|
86
|
+
}
|
|
87
|
+
if (!result.ok) {
|
|
88
|
+
return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsxs(Text, { color: "red", bold: true, children: ["\u2717 ", result.message] }), result.hint && (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: "yellow", children: ["\u21B3 ", result.hint] }) }))] }));
|
|
89
|
+
}
|
|
90
|
+
return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsxs(Box, { marginBottom: 1, children: [_jsx(Text, { bold: true, color: "cyan", children: "mintree worktree create" }), _jsxs(Text, { dimColor: true, children: [" \u00B7 ", result.branch] })] }), result.steps.map((step, i) => (_jsxs(Box, { children: [_jsx(StepIcon, { kind: step.kind }), _jsx(Text, { children: " " }), _jsx(Text, { children: step.label }), step.detail && _jsxs(Text, { dimColor: true, children: [" (", step.detail, ")"] })] }, i))), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsxs(Text, { color: "green", children: ["Worktree ready at ", _jsx(Text, { bold: true, children: result.worktreePath })] }), _jsx(Text, { dimColor: true, children: result.work
|
|
91
|
+
? "Launching Claude in the new worktree..."
|
|
92
|
+
: "Next: `mt worktree work` to start a Claude session, or `cd` and run `claude` directly." })] })] }));
|
|
93
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const description = "Manage mintree worktrees (create, list, remove, ...)";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const description = "Manage mintree worktrees (create, list, remove, ...)";
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export declare const description = "List mintree-managed worktrees with dirty/ahead/PR status";
|
|
3
|
+
export declare const options: z.ZodObject<{
|
|
4
|
+
pr: z.ZodDefault<z.ZodBoolean>;
|
|
5
|
+
}, z.core.$strip>;
|
|
6
|
+
type Props = {
|
|
7
|
+
options: z.infer<typeof options>;
|
|
8
|
+
};
|
|
9
|
+
export default function List({ options }: Props): import("react/jsx-runtime").JSX.Element;
|
|
10
|
+
export {};
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useState } from "react";
|
|
3
|
+
import { Box, Text } from "ink";
|
|
4
|
+
import Spinner from "ink-spinner";
|
|
5
|
+
import { option } from "pastel";
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
import * as path from "path";
|
|
8
|
+
import { findMainRepoRoot, getWorktreesDir, listWorktrees, isDirty, getAheadBehind, pathExists, getMintreeDir, } from "../../lib/git.js";
|
|
9
|
+
import { readMetadata } from "../../lib/metadata.js";
|
|
10
|
+
import { tryExec } from "../../lib/exec.js";
|
|
11
|
+
export const description = "List mintree-managed worktrees with dirty/ahead/PR status";
|
|
12
|
+
export const options = z.object({
|
|
13
|
+
pr: z
|
|
14
|
+
.boolean()
|
|
15
|
+
.default(false)
|
|
16
|
+
.describe(option({ description: "Look up PR status for each branch via `gh` (slower)" })),
|
|
17
|
+
});
|
|
18
|
+
const ISSUE_ID_REGEX = /^[a-z]+\/(\d+)-/;
|
|
19
|
+
function extractIssueId(branch) {
|
|
20
|
+
if (!branch)
|
|
21
|
+
return null;
|
|
22
|
+
const m = branch.match(ISSUE_ID_REGEX);
|
|
23
|
+
return m && m[1] ? m[1] : null;
|
|
24
|
+
}
|
|
25
|
+
async function fetchPrStatus(branch) {
|
|
26
|
+
const out = await tryExec(`gh pr list --head ${shQuote(branch)} --state all --json number,state --limit 1 2>/dev/null`);
|
|
27
|
+
if (!out)
|
|
28
|
+
return undefined;
|
|
29
|
+
try {
|
|
30
|
+
const arr = JSON.parse(out);
|
|
31
|
+
if (Array.isArray(arr) && arr.length > 0 && arr[0]) {
|
|
32
|
+
return { number: arr[0].number, state: arr[0].state };
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
// fall through
|
|
37
|
+
}
|
|
38
|
+
return undefined;
|
|
39
|
+
}
|
|
40
|
+
function shQuote(value) {
|
|
41
|
+
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
42
|
+
}
|
|
43
|
+
async function load(checkPr) {
|
|
44
|
+
const root = findMainRepoRoot();
|
|
45
|
+
if (!root) {
|
|
46
|
+
return {
|
|
47
|
+
phase: "error",
|
|
48
|
+
message: "Not in a git repository.",
|
|
49
|
+
hint: "Run `git init` and then `mintree init`.",
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
if (!pathExists(getMintreeDir(root))) {
|
|
53
|
+
return {
|
|
54
|
+
phase: "error",
|
|
55
|
+
message: ".mintree/ not found in this repo.",
|
|
56
|
+
hint: "Run `mintree init` first.",
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
const worktreesDir = getWorktreesDir(root);
|
|
60
|
+
const all = listWorktrees(root);
|
|
61
|
+
const ours = all.filter(w => {
|
|
62
|
+
// Filter to worktrees that live under .mintree/worktrees/. macOS reports
|
|
63
|
+
// /private/tmp paths so use a relative-prefix check after resolving both
|
|
64
|
+
// to absolute.
|
|
65
|
+
const wAbs = path.resolve(w.path);
|
|
66
|
+
const dirAbs = path.resolve(worktreesDir);
|
|
67
|
+
return wAbs === dirAbs || wAbs.startsWith(dirAbs + path.sep);
|
|
68
|
+
});
|
|
69
|
+
if (ours.length === 0) {
|
|
70
|
+
return { phase: "empty", repoRoot: root };
|
|
71
|
+
}
|
|
72
|
+
const metadata = readMetadata(root);
|
|
73
|
+
const rows = ours.map(w => {
|
|
74
|
+
const issueId = extractIssueId(w.branch);
|
|
75
|
+
const baseFromMeta = issueId ? metadata.issues[issueId]?.base_branch : undefined;
|
|
76
|
+
return {
|
|
77
|
+
worktreePath: w.path,
|
|
78
|
+
branch: w.branch ?? "(detached)",
|
|
79
|
+
issueId,
|
|
80
|
+
dirty: isDirty(w.path),
|
|
81
|
+
ab: getAheadBehind(w.path, baseFromMeta),
|
|
82
|
+
};
|
|
83
|
+
});
|
|
84
|
+
if (checkPr) {
|
|
85
|
+
const prResults = await Promise.all(rows.map(r => (r.branch === "(detached)" ? Promise.resolve(undefined) : fetchPrStatus(r.branch))));
|
|
86
|
+
rows.forEach((r, i) => {
|
|
87
|
+
r.pr = prResults[i];
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
return { phase: "ready", repoRoot: root, rows, checkedPr: checkPr };
|
|
91
|
+
}
|
|
92
|
+
function StatusCell({ dirty }) {
|
|
93
|
+
return dirty ? _jsx(Text, { color: "yellow", children: "dirty" }) : _jsx(Text, { color: "green", children: "clean" });
|
|
94
|
+
}
|
|
95
|
+
function AheadBehindCell({ ab }) {
|
|
96
|
+
if (!ab)
|
|
97
|
+
return _jsx(Text, { dimColor: true, children: "\u2014" });
|
|
98
|
+
const isUp = ab.ahead === 0 && ab.behind === 0;
|
|
99
|
+
if (isUp)
|
|
100
|
+
return _jsx(Text, { dimColor: true, children: "=" });
|
|
101
|
+
return (_jsxs(Text, { children: [_jsxs(Text, { color: ab.ahead > 0 ? "cyan" : undefined, children: ["+", ab.ahead] }), _jsx(Text, { dimColor: true, children: " / " }), _jsxs(Text, { color: ab.behind > 0 ? "magenta" : undefined, children: ["-", ab.behind] })] }));
|
|
102
|
+
}
|
|
103
|
+
function PrCell({ pr, checked }) {
|
|
104
|
+
if (!checked)
|
|
105
|
+
return null;
|
|
106
|
+
if (!pr)
|
|
107
|
+
return _jsx(Text, { dimColor: true, children: "\u2014" });
|
|
108
|
+
const color = pr.state === "OPEN" ? "green" : pr.state === "MERGED" ? "magenta" : "yellow";
|
|
109
|
+
return (_jsxs(Text, { children: [_jsxs(Text, { children: ["#", pr.number] }), _jsx(Text, { dimColor: true, children: " " }), _jsx(Text, { color: color, children: pr.state })] }));
|
|
110
|
+
}
|
|
111
|
+
function pad(s, width) {
|
|
112
|
+
if (s.length >= width)
|
|
113
|
+
return s;
|
|
114
|
+
return s + " ".repeat(width - s.length);
|
|
115
|
+
}
|
|
116
|
+
export default function List({ options }) {
|
|
117
|
+
const [state, setState] = useState({ phase: "loading" });
|
|
118
|
+
useEffect(() => {
|
|
119
|
+
(async () => {
|
|
120
|
+
try {
|
|
121
|
+
setState(await load(options.pr));
|
|
122
|
+
}
|
|
123
|
+
catch (err) {
|
|
124
|
+
setState({
|
|
125
|
+
phase: "error",
|
|
126
|
+
message: err instanceof Error ? err.message : String(err),
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
})();
|
|
130
|
+
}, [options.pr]);
|
|
131
|
+
if (state.phase === "loading") {
|
|
132
|
+
return (_jsxs(Box, { children: [_jsx(Text, { color: "cyan", children: _jsx(Spinner, { type: "dots" }) }), _jsxs(Text, { children: [" Listing worktrees", options.pr ? " (checking PR status)" : "", "..."] })] }));
|
|
133
|
+
}
|
|
134
|
+
if (state.phase === "error") {
|
|
135
|
+
return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsxs(Text, { color: "red", bold: true, children: ["\u2717 ", state.message] }), state.hint && (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: "yellow", children: ["\u21B3 ", state.hint] }) }))] }));
|
|
136
|
+
}
|
|
137
|
+
if (state.phase === "empty") {
|
|
138
|
+
return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsxs(Text, { dimColor: true, children: ["No mintree worktrees in ", state.repoRoot, "."] }), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { children: ["Create one with ", _jsx(Text, { bold: true, children: "mintree worktree create <branch>" }), "."] }) })] }));
|
|
139
|
+
}
|
|
140
|
+
const issueWidth = Math.max(5, ...state.rows.map(r => (r.issueId ?? "—").length));
|
|
141
|
+
const branchWidth = Math.max(6, ...state.rows.map(r => r.branch.length));
|
|
142
|
+
return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsxs(Box, { marginBottom: 1, children: [_jsx(Text, { bold: true, children: pad("ISSUE", issueWidth) }), _jsx(Text, { children: " " }), _jsx(Text, { bold: true, children: pad("BRANCH", branchWidth) }), _jsx(Text, { children: " " }), _jsx(Text, { bold: true, children: "STATUS" }), _jsx(Text, { children: " " }), _jsx(Text, { bold: true, children: "\u0394" }), _jsx(Text, { children: " " }), state.checkedPr && _jsx(Text, { bold: true, children: "PR" })] }), state.rows.map((r, i) => (_jsxs(Box, { children: [_jsx(Text, { children: pad(r.issueId ?? "—", issueWidth) }), _jsx(Text, { children: " " }), _jsx(Text, { color: "cyan", children: pad(r.branch, branchWidth) }), _jsx(Text, { children: " " }), _jsx(Box, { width: 9, children: _jsx(StatusCell, { dirty: r.dirty }) }), _jsx(Box, { width: 12, children: _jsx(AheadBehindCell, { ab: r.ab }) }), _jsx(PrCell, { pr: r.pr, checked: state.checkedPr })] }, i)))] }));
|
|
143
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export declare const description = "Remove a worktree (the branch and metadata are preserved so you can re-attach later)";
|
|
3
|
+
export declare const args: z.ZodTuple<[z.ZodString], null>;
|
|
4
|
+
export declare const options: z.ZodObject<{
|
|
5
|
+
force: z.ZodDefault<z.ZodBoolean>;
|
|
6
|
+
}, z.core.$strip>;
|
|
7
|
+
type Props = {
|
|
8
|
+
args: z.infer<typeof args>;
|
|
9
|
+
options: z.infer<typeof options>;
|
|
10
|
+
};
|
|
11
|
+
export default function Remove({ args, options }: Props): import("react/jsx-runtime").JSX.Element;
|
|
12
|
+
export {};
|