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,160 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
/**
|
|
4
|
+
* Synchronously slurps stdin to a string. Used by hook handlers to read the
|
|
5
|
+
* JSON payload Claude pipes in. Returns "" on any failure so the caller can
|
|
6
|
+
* exit silently — a hook crash would interrupt Claude.
|
|
7
|
+
*/
|
|
8
|
+
export function readStdin() {
|
|
9
|
+
try {
|
|
10
|
+
return fs.readFileSync(0, "utf-8");
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
return "";
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Extracts the main repo root and worktree directory name from an absolute
|
|
18
|
+
* cwd that lives under `<repo>/.mintree/worktrees/<dir>/...`. Returns null
|
|
19
|
+
* when the cwd is outside that pattern (Claude was launched somewhere else
|
|
20
|
+
* or the worktree was already removed).
|
|
21
|
+
*/
|
|
22
|
+
export function extractRepoAndDir(cwd) {
|
|
23
|
+
const marker = "/.mintree/worktrees/";
|
|
24
|
+
const idx = cwd.indexOf(marker);
|
|
25
|
+
if (idx === -1)
|
|
26
|
+
return null;
|
|
27
|
+
const repoRoot = cwd.slice(0, idx);
|
|
28
|
+
const rest = cwd.slice(idx + marker.length);
|
|
29
|
+
const worktreeDir = rest.split("/")[0];
|
|
30
|
+
if (!worktreeDir)
|
|
31
|
+
return null;
|
|
32
|
+
return { repoRoot, worktreeDir };
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Pulls the issue number out of a `<issue>-<desc>` worktree directory name.
|
|
36
|
+
* Returns null when the directory name doesn't follow the convention (e.g.
|
|
37
|
+
* a manually-created worktree dropped under .mintree/worktrees/).
|
|
38
|
+
*/
|
|
39
|
+
export function issueIdFromWorktreeDir(worktreeDir) {
|
|
40
|
+
const m = worktreeDir.match(/^(\d+)-/);
|
|
41
|
+
return m && m[1] ? m[1] : null;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Writes the state file for an issue under `<repo>/.mintree/session-states/`.
|
|
45
|
+
* Creates the directory if missing. Atomic-ish: same write pattern as
|
|
46
|
+
* metadata.json — fine for state probing from the dashboard, fast to refresh.
|
|
47
|
+
*/
|
|
48
|
+
export function writeStateFile(repoRoot, issueId, payload) {
|
|
49
|
+
const statesDir = path.join(repoRoot, ".mintree", "session-states");
|
|
50
|
+
fs.mkdirSync(statesDir, { recursive: true });
|
|
51
|
+
const file = path.join(statesDir, `${issueId}.json`);
|
|
52
|
+
fs.writeFileSync(file, JSON.stringify(payload, null, 2) + "\n");
|
|
53
|
+
return file;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* The only entry point the hook sub-commands call. Reads the JSON payload
|
|
57
|
+
* Claude piped in, locates the worktree+issue from the payload's `cwd`, and
|
|
58
|
+
* writes the state file. Exits 0 unconditionally — the worst case is a
|
|
59
|
+
* silent no-op, never an error that would interrupt Claude.
|
|
60
|
+
*/
|
|
61
|
+
export function signalState(state) {
|
|
62
|
+
const input = readStdin();
|
|
63
|
+
let data = {};
|
|
64
|
+
if (input) {
|
|
65
|
+
try {
|
|
66
|
+
data = JSON.parse(input);
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
process.exit(0);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
const cwd = data.cwd || process.cwd();
|
|
73
|
+
const info = extractRepoAndDir(cwd);
|
|
74
|
+
if (!info)
|
|
75
|
+
process.exit(0);
|
|
76
|
+
const { repoRoot, worktreeDir } = info;
|
|
77
|
+
const issueId = issueIdFromWorktreeDir(worktreeDir);
|
|
78
|
+
if (!issueId)
|
|
79
|
+
process.exit(0);
|
|
80
|
+
writeStateFile(repoRoot, issueId, {
|
|
81
|
+
state,
|
|
82
|
+
session_id: data.session_id ?? "",
|
|
83
|
+
issue_id: issueId,
|
|
84
|
+
worktree_dir: worktreeDir,
|
|
85
|
+
message: state === "waiting" ? (data.message ?? null) : null,
|
|
86
|
+
at: new Date().toISOString(),
|
|
87
|
+
});
|
|
88
|
+
process.exit(0);
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* The hook tree mintree wants to see in `~/.claude/settings.json`. Each
|
|
92
|
+
* inner command runs async with a 10s timeout — slow hooks would otherwise
|
|
93
|
+
* block Claude's UI thread. The Notification entry is gated on the
|
|
94
|
+
* `permission_prompt` matcher so the dashboard's "waiting" state only
|
|
95
|
+
* lights up when Claude is actually waiting for a permission decision,
|
|
96
|
+
* not for every notification.
|
|
97
|
+
*/
|
|
98
|
+
export function getHooksJson() {
|
|
99
|
+
const base = "mintree helpers session-signal";
|
|
100
|
+
const opts = { async: true, timeout: 10 };
|
|
101
|
+
return {
|
|
102
|
+
Notification: [
|
|
103
|
+
{
|
|
104
|
+
matcher: "permission_prompt",
|
|
105
|
+
hooks: [{ type: "command", command: `${base} notification`, ...opts }],
|
|
106
|
+
},
|
|
107
|
+
],
|
|
108
|
+
Stop: [{ hooks: [{ type: "command", command: `${base} stop`, ...opts }] }],
|
|
109
|
+
UserPromptSubmit: [{ hooks: [{ type: "command", command: `${base} prompt`, ...opts }] }],
|
|
110
|
+
SessionEnd: [{ hooks: [{ type: "command", command: `${base} end`, ...opts }] }],
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Installs (or replaces) the four mintree hooks in `~/.claude/settings.json`.
|
|
115
|
+
* Existing non-mintree hooks for the same events are preserved; previous
|
|
116
|
+
* mintree entries are filtered out and re-added so re-running this is safe.
|
|
117
|
+
* Returns the path of the file we wrote.
|
|
118
|
+
*/
|
|
119
|
+
export function installHooks() {
|
|
120
|
+
const home = process.env["HOME"] || "";
|
|
121
|
+
const claudeDir = path.join(home, ".claude");
|
|
122
|
+
const settingsPath = path.join(claudeDir, "settings.json");
|
|
123
|
+
const created = !fs.existsSync(settingsPath);
|
|
124
|
+
let settings = {};
|
|
125
|
+
try {
|
|
126
|
+
if (!created)
|
|
127
|
+
settings = JSON.parse(fs.readFileSync(settingsPath, "utf-8"));
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
// Corrupt settings — start fresh rather than refusing to install.
|
|
131
|
+
settings = {};
|
|
132
|
+
}
|
|
133
|
+
const required = getHooksJson();
|
|
134
|
+
const existingHooks = typeof settings["hooks"] === "object" && settings["hooks"] !== null
|
|
135
|
+
? settings["hooks"]
|
|
136
|
+
: {};
|
|
137
|
+
for (const [event, hookEntries] of Object.entries(required)) {
|
|
138
|
+
const existing = existingHooks[event];
|
|
139
|
+
if (!Array.isArray(existing)) {
|
|
140
|
+
existingHooks[event] = hookEntries;
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
const filtered = existing.filter(entry => {
|
|
144
|
+
if (!entry || typeof entry !== "object")
|
|
145
|
+
return true;
|
|
146
|
+
const inner = entry.hooks ?? [];
|
|
147
|
+
return !inner.some(h => {
|
|
148
|
+
return (h !== null &&
|
|
149
|
+
typeof h === "object" &&
|
|
150
|
+
typeof h.command === "string" &&
|
|
151
|
+
(h.command).includes("mintree helpers session-signal"));
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
existingHooks[event] = [...filtered, ...hookEntries];
|
|
155
|
+
}
|
|
156
|
+
settings["hooks"] = existingHooks;
|
|
157
|
+
fs.mkdirSync(claudeDir, { recursive: true });
|
|
158
|
+
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
|
|
159
|
+
return { settingsPath, created };
|
|
160
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { PermissionMode } from "./claude.js";
|
|
2
|
+
export type CreateStepKind = "ok" | "skip" | "warn";
|
|
3
|
+
export type CreateStep = {
|
|
4
|
+
kind: CreateStepKind;
|
|
5
|
+
label: string;
|
|
6
|
+
detail?: string;
|
|
7
|
+
};
|
|
8
|
+
export type CreateOpts = {
|
|
9
|
+
base?: string;
|
|
10
|
+
work: boolean;
|
|
11
|
+
prompt?: string;
|
|
12
|
+
permissionMode?: PermissionMode;
|
|
13
|
+
};
|
|
14
|
+
export type CreateResult = {
|
|
15
|
+
ok: true;
|
|
16
|
+
steps: CreateStep[];
|
|
17
|
+
worktreePath: string;
|
|
18
|
+
branch: string;
|
|
19
|
+
issueId: string;
|
|
20
|
+
base?: string;
|
|
21
|
+
work: boolean;
|
|
22
|
+
promptFile?: string;
|
|
23
|
+
permissionMode?: PermissionMode;
|
|
24
|
+
} | {
|
|
25
|
+
ok: false;
|
|
26
|
+
message: string;
|
|
27
|
+
hint?: string;
|
|
28
|
+
};
|
|
29
|
+
/**
|
|
30
|
+
* The whole `worktree create` flow as a pure function — same code path used
|
|
31
|
+
* by the CLI command and by the dashboard's `w` overlay. Validates input,
|
|
32
|
+
* resolves a base branch, runs `git worktree add`, persists metadata, runs
|
|
33
|
+
* the optional `.mintree/init.sh`, and stages the --prompt to a temp file
|
|
34
|
+
* for the work hand-off when relevant.
|
|
35
|
+
*/
|
|
36
|
+
export declare function runCreate(branchArg: string, opts: CreateOpts): CreateResult;
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as os from "os";
|
|
3
|
+
import * as path from "path";
|
|
4
|
+
import { execSync } from "child_process";
|
|
5
|
+
import { parseBranch, isParseError } from "./branch.js";
|
|
6
|
+
import { findMainRepoRoot, getMintreeDir, getWorktreesDir, getInitScriptPath, getDefaultBranch, branchExists, worktreeForBranch, addWorktree, pathExists, isExecutable, } from "./git.js";
|
|
7
|
+
import { upsertIssue } from "./metadata.js";
|
|
8
|
+
function tryRunInitScript(scriptPath, worktreePath, repoRoot) {
|
|
9
|
+
if (!pathExists(scriptPath))
|
|
10
|
+
return { ran: false };
|
|
11
|
+
if (!isExecutable(scriptPath)) {
|
|
12
|
+
return { ran: false, error: `init.sh exists but is not executable (chmod +x ${scriptPath})` };
|
|
13
|
+
}
|
|
14
|
+
try {
|
|
15
|
+
execSync(scriptPath, {
|
|
16
|
+
cwd: worktreePath,
|
|
17
|
+
env: { ...process.env, MINTREE_WORKTREE_PATH: worktreePath, MINTREE_REPO_ROOT: repoRoot },
|
|
18
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
19
|
+
});
|
|
20
|
+
return { ran: true };
|
|
21
|
+
}
|
|
22
|
+
catch (err) {
|
|
23
|
+
return {
|
|
24
|
+
ran: false,
|
|
25
|
+
error: err instanceof Error ? err.message : String(err),
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Stashes a `--prompt` value into a temp file so the shell wrapper can hand
|
|
31
|
+
* it back to `worktree work` via `--prompt-file`. Plain stdout markers can't
|
|
32
|
+
* carry multi-line / shell-special text safely, hence the file.
|
|
33
|
+
*/
|
|
34
|
+
function writePromptFile(prompt) {
|
|
35
|
+
const fileName = `mintree-prompt-${process.pid}-${Date.now()}.txt`;
|
|
36
|
+
const filePath = path.join(os.tmpdir(), fileName);
|
|
37
|
+
fs.writeFileSync(filePath, prompt);
|
|
38
|
+
return filePath;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* The whole `worktree create` flow as a pure function — same code path used
|
|
42
|
+
* by the CLI command and by the dashboard's `w` overlay. Validates input,
|
|
43
|
+
* resolves a base branch, runs `git worktree add`, persists metadata, runs
|
|
44
|
+
* the optional `.mintree/init.sh`, and stages the --prompt to a temp file
|
|
45
|
+
* for the work hand-off when relevant.
|
|
46
|
+
*/
|
|
47
|
+
export function runCreate(branchArg, opts) {
|
|
48
|
+
const root = findMainRepoRoot();
|
|
49
|
+
if (!root) {
|
|
50
|
+
return {
|
|
51
|
+
ok: false,
|
|
52
|
+
message: "Not in a git repository.",
|
|
53
|
+
hint: "Run `git init` and then `mintree init`.",
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
if (!pathExists(getMintreeDir(root))) {
|
|
57
|
+
return {
|
|
58
|
+
ok: false,
|
|
59
|
+
message: ".mintree/ not found in this repo.",
|
|
60
|
+
hint: "Run `mintree init` first.",
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
const parsed = parseBranch(branchArg);
|
|
64
|
+
if (isParseError(parsed)) {
|
|
65
|
+
return { ok: false, message: parsed.error, hint: parsed.hint };
|
|
66
|
+
}
|
|
67
|
+
const existingWorktree = worktreeForBranch(root, parsed.branch);
|
|
68
|
+
if (existingWorktree) {
|
|
69
|
+
return {
|
|
70
|
+
ok: false,
|
|
71
|
+
message: `Branch ${parsed.branch} is already checked out at ${existingWorktree}`,
|
|
72
|
+
hint: "Use `mintree worktree work` to resume, or `mintree worktree remove` to delete.",
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
const worktreePath = path.join(getWorktreesDir(root), parsed.worktreeDirName);
|
|
76
|
+
if (pathExists(worktreePath)) {
|
|
77
|
+
return {
|
|
78
|
+
ok: false,
|
|
79
|
+
message: `Worktree directory already exists: ${worktreePath}`,
|
|
80
|
+
hint: "Remove it first or pick a different branch description.",
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
const existence = branchExists(root, parsed.branch);
|
|
84
|
+
let base;
|
|
85
|
+
if (existence === null) {
|
|
86
|
+
base = opts.base ?? getDefaultBranch(root) ?? undefined;
|
|
87
|
+
if (!base) {
|
|
88
|
+
return {
|
|
89
|
+
ok: false,
|
|
90
|
+
message: "Could not determine a base branch (no origin/HEAD, no main/master).",
|
|
91
|
+
hint: "Pass --base <branch> explicitly.",
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
if (branchExists(root, base) === null) {
|
|
95
|
+
return {
|
|
96
|
+
ok: false,
|
|
97
|
+
message: `Base branch \`${base}\` does not exist locally or on origin.`,
|
|
98
|
+
hint: "Pick a different --base or fetch the missing branch first.",
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
const steps = [];
|
|
103
|
+
steps.push({
|
|
104
|
+
kind: "ok",
|
|
105
|
+
label: "parsed branch",
|
|
106
|
+
detail: `type=${parsed.type}, issue=${parsed.issueId}, desc=${parsed.desc}`,
|
|
107
|
+
});
|
|
108
|
+
try {
|
|
109
|
+
addWorktree({ repoRoot: root, branch: parsed.branch, worktreePath, base });
|
|
110
|
+
}
|
|
111
|
+
catch (err) {
|
|
112
|
+
const stderr = err && typeof err === "object" && "stderr" in err
|
|
113
|
+
? String(err.stderr).trim()
|
|
114
|
+
: err instanceof Error
|
|
115
|
+
? err.message
|
|
116
|
+
: String(err);
|
|
117
|
+
return { ok: false, message: `git worktree add failed: ${stderr}` };
|
|
118
|
+
}
|
|
119
|
+
if (existence === "remote") {
|
|
120
|
+
steps.push({
|
|
121
|
+
kind: "ok",
|
|
122
|
+
label: "checked out tracking branch",
|
|
123
|
+
detail: `from origin/${parsed.branch}`,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
else if (existence === "local") {
|
|
127
|
+
steps.push({
|
|
128
|
+
kind: "ok",
|
|
129
|
+
label: "checked out existing local branch",
|
|
130
|
+
detail: parsed.branch,
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
else {
|
|
134
|
+
steps.push({
|
|
135
|
+
kind: "ok",
|
|
136
|
+
label: "created new branch",
|
|
137
|
+
detail: `${parsed.branch} (from ${base})`,
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
steps.push({ kind: "ok", label: "worktree created", detail: worktreePath });
|
|
141
|
+
upsertIssue(root, parsed.issueId, base ? { base_branch: base } : {});
|
|
142
|
+
steps.push({ kind: "ok", label: "metadata updated", detail: `issue ${parsed.issueId}` });
|
|
143
|
+
const initShPath = getInitScriptPath(root);
|
|
144
|
+
const initResult = tryRunInitScript(initShPath, worktreePath, root);
|
|
145
|
+
if (initResult.ran) {
|
|
146
|
+
steps.push({ kind: "ok", label: "ran .mintree/init.sh", detail: worktreePath });
|
|
147
|
+
}
|
|
148
|
+
else if (initResult.error) {
|
|
149
|
+
steps.push({ kind: "warn", label: "init.sh failed", detail: initResult.error });
|
|
150
|
+
}
|
|
151
|
+
else if (!pathExists(initShPath)) {
|
|
152
|
+
steps.push({ kind: "skip", label: "no init.sh (skipping post-create hook)" });
|
|
153
|
+
}
|
|
154
|
+
let promptFile;
|
|
155
|
+
if (opts.work && opts.prompt && opts.prompt.length > 0) {
|
|
156
|
+
try {
|
|
157
|
+
promptFile = writePromptFile(opts.prompt);
|
|
158
|
+
}
|
|
159
|
+
catch (err) {
|
|
160
|
+
steps.push({
|
|
161
|
+
kind: "warn",
|
|
162
|
+
label: "failed to stage --prompt for hand-off",
|
|
163
|
+
detail: err instanceof Error ? err.message : String(err),
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
if (!opts.work && (opts.prompt || opts.permissionMode)) {
|
|
168
|
+
steps.push({
|
|
169
|
+
kind: "warn",
|
|
170
|
+
label: "ignoring --prompt / --permission-mode (only meaningful with --work)",
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
return {
|
|
174
|
+
ok: true,
|
|
175
|
+
steps,
|
|
176
|
+
worktreePath,
|
|
177
|
+
branch: parsed.branch,
|
|
178
|
+
issueId: parsed.issueId,
|
|
179
|
+
base,
|
|
180
|
+
work: opts.work,
|
|
181
|
+
promptFile,
|
|
182
|
+
permissionMode: opts.permissionMode,
|
|
183
|
+
};
|
|
184
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export type RemoveResult = {
|
|
2
|
+
ok: true;
|
|
3
|
+
branch: string;
|
|
4
|
+
worktreePath: string;
|
|
5
|
+
variant: "removed" | "pruned-orphan";
|
|
6
|
+
wasDirty: boolean;
|
|
7
|
+
} | {
|
|
8
|
+
ok: false;
|
|
9
|
+
message: string;
|
|
10
|
+
hint?: string;
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* Removes the worktree backing `branchArg`. Same behavior as the CLI command:
|
|
14
|
+
* - dirty + !force → refuse
|
|
15
|
+
* - directory missing on disk → prune the dangling git reference
|
|
16
|
+
* - otherwise → `git worktree remove` (with --force when asked)
|
|
17
|
+
*
|
|
18
|
+
* Branch and metadata are deliberately preserved so a later re-attach can
|
|
19
|
+
* resume the same Claude session.
|
|
20
|
+
*/
|
|
21
|
+
export declare function runRemove(branchArg: string, force: boolean): RemoveResult;
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { parseBranch, isParseError } from "./branch.js";
|
|
2
|
+
import { findMainRepoRoot, getMintreeDir, worktreeForBranch, isDirty, removeWorktree, pruneWorktrees, pathExists, } from "./git.js";
|
|
3
|
+
/**
|
|
4
|
+
* Removes the worktree backing `branchArg`. Same behavior as the CLI command:
|
|
5
|
+
* - dirty + !force → refuse
|
|
6
|
+
* - directory missing on disk → prune the dangling git reference
|
|
7
|
+
* - otherwise → `git worktree remove` (with --force when asked)
|
|
8
|
+
*
|
|
9
|
+
* Branch and metadata are deliberately preserved so a later re-attach can
|
|
10
|
+
* resume the same Claude session.
|
|
11
|
+
*/
|
|
12
|
+
export function runRemove(branchArg, force) {
|
|
13
|
+
const root = findMainRepoRoot();
|
|
14
|
+
if (!root) {
|
|
15
|
+
return {
|
|
16
|
+
ok: false,
|
|
17
|
+
message: "Not in a git repository.",
|
|
18
|
+
hint: "Run `git init` and then `mintree init`.",
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
if (!pathExists(getMintreeDir(root))) {
|
|
22
|
+
return {
|
|
23
|
+
ok: false,
|
|
24
|
+
message: ".mintree/ not found in this repo.",
|
|
25
|
+
hint: "Run `mintree init` first.",
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
const parsed = parseBranch(branchArg);
|
|
29
|
+
if (isParseError(parsed)) {
|
|
30
|
+
return { ok: false, message: parsed.error, hint: parsed.hint };
|
|
31
|
+
}
|
|
32
|
+
const worktreePath = worktreeForBranch(root, parsed.branch);
|
|
33
|
+
if (!worktreePath) {
|
|
34
|
+
return {
|
|
35
|
+
ok: false,
|
|
36
|
+
message: `No worktree found for branch ${parsed.branch}.`,
|
|
37
|
+
hint: "Use `mintree worktree list` to see existing worktrees.",
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
if (!pathExists(worktreePath)) {
|
|
41
|
+
try {
|
|
42
|
+
pruneWorktrees(root);
|
|
43
|
+
}
|
|
44
|
+
catch (err) {
|
|
45
|
+
return {
|
|
46
|
+
ok: false,
|
|
47
|
+
message: `git worktree prune failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
return {
|
|
51
|
+
ok: true,
|
|
52
|
+
branch: parsed.branch,
|
|
53
|
+
worktreePath,
|
|
54
|
+
variant: "pruned-orphan",
|
|
55
|
+
wasDirty: false,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
const dirty = isDirty(worktreePath);
|
|
59
|
+
if (dirty && !force) {
|
|
60
|
+
return {
|
|
61
|
+
ok: false,
|
|
62
|
+
message: `Worktree at ${worktreePath} has uncommitted changes.`,
|
|
63
|
+
hint: "Commit/stash first, or pass --force to discard them.",
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
try {
|
|
67
|
+
removeWorktree({ repoRoot: root, worktreePath, force });
|
|
68
|
+
}
|
|
69
|
+
catch (err) {
|
|
70
|
+
const stderr = err && typeof err === "object" && "stderr" in err
|
|
71
|
+
? String(err.stderr).trim()
|
|
72
|
+
: err instanceof Error
|
|
73
|
+
? err.message
|
|
74
|
+
: String(err);
|
|
75
|
+
return { ok: false, message: `git worktree remove failed: ${stderr}` };
|
|
76
|
+
}
|
|
77
|
+
return {
|
|
78
|
+
ok: true,
|
|
79
|
+
branch: parsed.branch,
|
|
80
|
+
worktreePath,
|
|
81
|
+
variant: "removed",
|
|
82
|
+
wasDirty: dirty,
|
|
83
|
+
};
|
|
84
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "mintree",
|
|
3
|
+
"version": "0.1.2",
|
|
4
|
+
"description": "Issue-driven git worktrees + Claude Code sessions for repos with an opinionated SDD+TDD flow.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "Martin Mineo <mmineo@canarytechnologies.com>",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/minex-labs/mintree.git"
|
|
10
|
+
},
|
|
11
|
+
"bugs": {
|
|
12
|
+
"url": "https://github.com/minex-labs/mintree/issues"
|
|
13
|
+
},
|
|
14
|
+
"homepage": "https://github.com/minex-labs/mintree#readme",
|
|
15
|
+
"keywords": [
|
|
16
|
+
"git",
|
|
17
|
+
"worktree",
|
|
18
|
+
"cli",
|
|
19
|
+
"github",
|
|
20
|
+
"issues",
|
|
21
|
+
"claude",
|
|
22
|
+
"ai"
|
|
23
|
+
],
|
|
24
|
+
"bin": {
|
|
25
|
+
"mintree": "dist/cli.js"
|
|
26
|
+
},
|
|
27
|
+
"type": "module",
|
|
28
|
+
"engines": {
|
|
29
|
+
"node": ">=20"
|
|
30
|
+
},
|
|
31
|
+
"scripts": {
|
|
32
|
+
"build": "rm -rf dist && node ./node_modules/typescript/bin/tsc && chmod +x dist/cli.js",
|
|
33
|
+
"dev": "node ./node_modules/typescript/bin/tsc --watch",
|
|
34
|
+
"start": "node dist/cli.js",
|
|
35
|
+
"lint": "eslint source",
|
|
36
|
+
"lint:fix": "eslint source --fix",
|
|
37
|
+
"format": "prettier --write source"
|
|
38
|
+
},
|
|
39
|
+
"files": [
|
|
40
|
+
"dist",
|
|
41
|
+
"shell",
|
|
42
|
+
"README.md"
|
|
43
|
+
],
|
|
44
|
+
"dependencies": {
|
|
45
|
+
"ink": "^6.0.0",
|
|
46
|
+
"ink-spinner": "^5.0.0",
|
|
47
|
+
"ink-text-input": "^6.0.0",
|
|
48
|
+
"pastel": "^4.0.0",
|
|
49
|
+
"react": "^19.0.0",
|
|
50
|
+
"zod": "^4.0.0"
|
|
51
|
+
},
|
|
52
|
+
"devDependencies": {
|
|
53
|
+
"@types/node": "^22.0.0",
|
|
54
|
+
"@types/react": "^19.0.0",
|
|
55
|
+
"@typescript-eslint/eslint-plugin": "^8.52.0",
|
|
56
|
+
"@typescript-eslint/parser": "^8.52.0",
|
|
57
|
+
"eslint": "^9.39.2",
|
|
58
|
+
"eslint-config-prettier": "^10.1.8",
|
|
59
|
+
"eslint-plugin-prettier": "^5.5.5",
|
|
60
|
+
"prettier": "^3.7.4",
|
|
61
|
+
"typescript": "^5.7.0"
|
|
62
|
+
}
|
|
63
|
+
}
|
package/shell/init.bash
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# Mintree Shell Integration for Bash
|
|
2
|
+
# ===================================
|
|
3
|
+
#
|
|
4
|
+
# Generated by `mintree helpers shell-init bash`. Eval this in your ~/.bashrc:
|
|
5
|
+
# eval "$(mintree helpers shell-init bash)"
|
|
6
|
+
#
|
|
7
|
+
# What it does:
|
|
8
|
+
# 1. Wraps the `mintree` binary so the parent shell can `cd` into a freshly
|
|
9
|
+
# created worktree. The binary itself can't change the parent's cwd, so
|
|
10
|
+
# it emits `MINTREE_CD:<path>` markers on stdout that this wrapper picks
|
|
11
|
+
# up and translates into a real `cd`.
|
|
12
|
+
# 2. Recovers the current shell when its cwd was deleted (e.g. after
|
|
13
|
+
# `mintree worktree remove`) — falls back to the main repo, then to $HOME.
|
|
14
|
+
# 3. Exposes the `mt` / `mtw` / `mtn` aliases (`mtn` prompts for a branch).
|
|
15
|
+
# 4. Exports MINTREE_SHELL_INTEGRATION=1 so `mintree doctor` can verify the
|
|
16
|
+
# wrapper is loaded.
|
|
17
|
+
|
|
18
|
+
export MINTREE_SHELL_INTEGRATION=1
|
|
19
|
+
|
|
20
|
+
function mintree() {
|
|
21
|
+
if [[ ! -d "$(pwd 2>/dev/null)" ]]; then
|
|
22
|
+
local current_path="$(pwd 2>/dev/null)"
|
|
23
|
+
if [[ "$current_path" == */.mintree/worktrees/* ]]; then
|
|
24
|
+
local main_repo="${current_path%%/.mintree/worktrees/*}"
|
|
25
|
+
if [[ -d "$main_repo" ]]; then
|
|
26
|
+
echo "⚠ Worktree directory deleted. Returning to main repo."
|
|
27
|
+
cd "$main_repo" || cd ~ || return 1
|
|
28
|
+
else
|
|
29
|
+
cd ~ || return 1
|
|
30
|
+
fi
|
|
31
|
+
else
|
|
32
|
+
echo "⚠ Current directory no longer exists. Returning to home."
|
|
33
|
+
cd ~ || return 1
|
|
34
|
+
fi
|
|
35
|
+
fi
|
|
36
|
+
|
|
37
|
+
_mintree_handle_markers() {
|
|
38
|
+
local clean_output="$1"
|
|
39
|
+
local target_dir
|
|
40
|
+
target_dir=$(echo "$clean_output" | grep "MINTREE_CD:" | sed 's/.*MINTREE_CD://')
|
|
41
|
+
if [[ -z "$target_dir" || ! -d "$target_dir" ]]; then
|
|
42
|
+
return 1
|
|
43
|
+
fi
|
|
44
|
+
cd "$target_dir" && echo "Switched to: $target_dir"
|
|
45
|
+
|
|
46
|
+
if [[ "$clean_output" != *MINTREE_WORK:* ]]; then
|
|
47
|
+
return 0
|
|
48
|
+
fi
|
|
49
|
+
local extra=()
|
|
50
|
+
local prompt_file
|
|
51
|
+
prompt_file=$(echo "$clean_output" | grep "MINTREE_WORK_PROMPT_FILE:" | sed 's/.*MINTREE_WORK_PROMPT_FILE://')
|
|
52
|
+
local perm_mode
|
|
53
|
+
perm_mode=$(echo "$clean_output" | grep "MINTREE_PERMISSION_MODE:" | sed 's/.*MINTREE_PERMISSION_MODE://')
|
|
54
|
+
[[ -n "$prompt_file" ]] && extra+=(--prompt-file "$prompt_file")
|
|
55
|
+
[[ -n "$perm_mode" ]] && extra+=(--permission-mode "$perm_mode")
|
|
56
|
+
command mintree worktree work "${extra[@]}"
|
|
57
|
+
return $?
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if [[ "$1" == "worktree" && "$2" == "create" ]]; then
|
|
61
|
+
local output
|
|
62
|
+
output=$(command mintree "$@" 2>&1)
|
|
63
|
+
local exit_code=$?
|
|
64
|
+
|
|
65
|
+
if [[ "$output" == *MINTREE_CD:* ]]; then
|
|
66
|
+
echo "$output" | grep -vE "MINTREE_(CD|WORK|WORK_PROMPT_FILE|PERMISSION_MODE):"
|
|
67
|
+
local clean_output=$(echo "$output" | sed 's/\x1b\[[0-9;]*m//g')
|
|
68
|
+
_mintree_handle_markers "$clean_output"
|
|
69
|
+
else
|
|
70
|
+
echo "$output"
|
|
71
|
+
fi
|
|
72
|
+
return $exit_code
|
|
73
|
+
fi
|
|
74
|
+
|
|
75
|
+
if [[ "$1" == "dashboard" ]]; then
|
|
76
|
+
local marker_file
|
|
77
|
+
marker_file=$(mktemp -t mintree-markers)
|
|
78
|
+
MINTREE_MARKER_FILE="$marker_file" command mintree "$@"
|
|
79
|
+
local exit_code=$?
|
|
80
|
+
if [[ -s "$marker_file" ]]; then
|
|
81
|
+
local clean_output=$(cat "$marker_file" | sed 's/\x1b\[[0-9;]*m//g')
|
|
82
|
+
_mintree_handle_markers "$clean_output"
|
|
83
|
+
fi
|
|
84
|
+
rm -f "$marker_file"
|
|
85
|
+
return $exit_code
|
|
86
|
+
fi
|
|
87
|
+
|
|
88
|
+
command mintree "$@"
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
# Aliases as functions so they work in non-interactive shells too (bash
|
|
92
|
+
# expands aliases only in interactive mode; functions always work).
|
|
93
|
+
function mt() {
|
|
94
|
+
mintree "$@"
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function mtw() {
|
|
98
|
+
mintree worktree "$@"
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function mtn() {
|
|
102
|
+
local branch
|
|
103
|
+
read -p "Branch name: " branch
|
|
104
|
+
[[ -z "$branch" ]] && echo "Branch name required" && return 1
|
|
105
|
+
mintree worktree create "$branch" --work
|
|
106
|
+
}
|