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.
Files changed (64) hide show
  1. package/README.md +188 -0
  2. package/dist/cli.d.ts +2 -0
  3. package/dist/cli.js +12 -0
  4. package/dist/commands/dashboard.d.ts +2 -0
  5. package/dist/commands/dashboard.js +849 -0
  6. package/dist/commands/doctor.d.ts +2 -0
  7. package/dist/commands/doctor.js +327 -0
  8. package/dist/commands/helpers/index.d.ts +1 -0
  9. package/dist/commands/helpers/index.js +1 -0
  10. package/dist/commands/helpers/session-signal/end.d.ts +2 -0
  11. package/dist/commands/helpers/session-signal/end.js +9 -0
  12. package/dist/commands/helpers/session-signal/index.d.ts +1 -0
  13. package/dist/commands/helpers/session-signal/index.js +1 -0
  14. package/dist/commands/helpers/session-signal/install.d.ts +2 -0
  15. package/dist/commands/helpers/session-signal/install.js +25 -0
  16. package/dist/commands/helpers/session-signal/notification.d.ts +2 -0
  17. package/dist/commands/helpers/session-signal/notification.js +9 -0
  18. package/dist/commands/helpers/session-signal/prompt.d.ts +2 -0
  19. package/dist/commands/helpers/session-signal/prompt.js +9 -0
  20. package/dist/commands/helpers/session-signal/stop.d.ts +2 -0
  21. package/dist/commands/helpers/session-signal/stop.js +9 -0
  22. package/dist/commands/helpers/shell-init.d.ts +11 -0
  23. package/dist/commands/helpers/shell-init.js +111 -0
  24. package/dist/commands/index.d.ts +2 -0
  25. package/dist/commands/index.js +6 -0
  26. package/dist/commands/init.d.ts +2 -0
  27. package/dist/commands/init.js +129 -0
  28. package/dist/commands/worktree/clean.d.ts +11 -0
  29. package/dist/commands/worktree/clean.js +206 -0
  30. package/dist/commands/worktree/create.d.ts +18 -0
  31. package/dist/commands/worktree/create.js +93 -0
  32. package/dist/commands/worktree/index.d.ts +1 -0
  33. package/dist/commands/worktree/index.js +1 -0
  34. package/dist/commands/worktree/list.d.ts +10 -0
  35. package/dist/commands/worktree/list.js +143 -0
  36. package/dist/commands/worktree/remove.d.ts +12 -0
  37. package/dist/commands/worktree/remove.js +46 -0
  38. package/dist/commands/worktree/work.d.ts +15 -0
  39. package/dist/commands/worktree/work.js +192 -0
  40. package/dist/lib/branch.d.ts +26 -0
  41. package/dist/lib/branch.js +57 -0
  42. package/dist/lib/claude.d.ts +26 -0
  43. package/dist/lib/claude.js +67 -0
  44. package/dist/lib/dashboard.d.ts +50 -0
  45. package/dist/lib/dashboard.js +139 -0
  46. package/dist/lib/exec.d.ts +2 -0
  47. package/dist/lib/exec.js +15 -0
  48. package/dist/lib/git.d.ts +110 -0
  49. package/dist/lib/git.js +320 -0
  50. package/dist/lib/github.d.ts +7 -0
  51. package/dist/lib/github.js +15 -0
  52. package/dist/lib/markers.d.ts +21 -0
  53. package/dist/lib/markers.js +43 -0
  54. package/dist/lib/metadata.d.ts +18 -0
  55. package/dist/lib/metadata.js +44 -0
  56. package/dist/lib/session-signal.d.ts +63 -0
  57. package/dist/lib/session-signal.js +160 -0
  58. package/dist/lib/worktreeCreate.d.ts +36 -0
  59. package/dist/lib/worktreeCreate.js +184 -0
  60. package/dist/lib/worktreeRemove.d.ts +21 -0
  61. package/dist/lib/worktreeRemove.js +84 -0
  62. package/package.json +63 -0
  63. package/shell/init.bash +106 -0
  64. 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
+ }
@@ -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
+ }