santree 0.7.4 → 0.7.6

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 (114) hide show
  1. package/README.md +2 -3
  2. package/dist/commands/config.d.ts +12 -0
  3. package/dist/commands/config.js +444 -0
  4. package/dist/commands/dashboard.js +187 -80
  5. package/dist/commands/helpers/statusline.js +0 -30
  6. package/dist/commands/issue/index.d.ts +1 -1
  7. package/dist/commands/issue/index.js +1 -1
  8. package/dist/commands/pr/context.d.ts +7 -0
  9. package/dist/commands/pr/context.js +36 -0
  10. package/dist/commands/pr/fix.d.ts +11 -2
  11. package/dist/commands/pr/fix.js +64 -12
  12. package/dist/commands/worktree/create.js +17 -11
  13. package/dist/commands/worktree/open.js +3 -2
  14. package/dist/commands/worktree/switch.js +3 -11
  15. package/dist/lib/ai.d.ts +11 -0
  16. package/dist/lib/ai.js +71 -2
  17. package/dist/lib/cd-hint.d.ts +18 -0
  18. package/dist/lib/cd-hint.js +27 -0
  19. package/dist/lib/claude-config.d.ts +26 -0
  20. package/dist/lib/claude-config.js +121 -0
  21. package/dist/lib/config/TrackerPicker.d.ts +10 -0
  22. package/dist/lib/config/TrackerPicker.js +113 -0
  23. package/dist/lib/config/diagnostics.d.ts +37 -0
  24. package/dist/lib/config/diagnostics.js +190 -0
  25. package/dist/lib/config-store.d.ts +23 -0
  26. package/dist/lib/config-store.js +68 -0
  27. package/dist/lib/dashboard/DetailPanel.d.ts +7 -2
  28. package/dist/lib/dashboard/DetailPanel.js +54 -37
  29. package/dist/lib/dashboard/IssueList.d.ts +5 -1
  30. package/dist/lib/dashboard/IssueList.js +24 -5
  31. package/dist/lib/dashboard/MultilineTextArea.js +1 -1
  32. package/dist/lib/dashboard/Overlays.js +3 -4
  33. package/dist/lib/dashboard/ReviewDetailPanel.js +11 -2
  34. package/dist/lib/dashboard/ReviewList.js +6 -3
  35. package/dist/lib/dashboard/data.js +7 -34
  36. package/dist/lib/dashboard/external-editor.d.ts +1 -1
  37. package/dist/lib/dashboard/external-editor.js +3 -2
  38. package/dist/lib/dashboard/types.d.ts +19 -2
  39. package/dist/lib/dashboard/types.js +68 -2
  40. package/dist/lib/exec.d.ts +13 -0
  41. package/dist/lib/exec.js +11 -1
  42. package/dist/lib/fix-loop.d.ts +45 -0
  43. package/dist/lib/fix-loop.js +83 -0
  44. package/dist/lib/git.d.ts +3 -29
  45. package/dist/lib/git.js +6 -62
  46. package/dist/lib/github.d.ts +70 -0
  47. package/dist/lib/github.js +128 -0
  48. package/dist/lib/multiplexer/cmux.js +143 -17
  49. package/dist/lib/multiplexer/none.js +3 -3
  50. package/dist/lib/multiplexer/tmux.js +5 -29
  51. package/dist/lib/multiplexer/types.d.ts +34 -1
  52. package/dist/lib/open-url.d.ts +3 -2
  53. package/dist/lib/open-url.js +5 -4
  54. package/dist/lib/prompts.d.ts +35 -1
  55. package/dist/lib/prompts.js +7 -0
  56. package/dist/lib/setup/apply.d.ts +16 -0
  57. package/dist/lib/setup/apply.js +35 -0
  58. package/dist/lib/setup/gitignore.d.ts +24 -0
  59. package/dist/lib/setup/gitignore.js +95 -0
  60. package/dist/lib/setup/steps.d.ts +39 -0
  61. package/dist/lib/setup/steps.js +333 -0
  62. package/dist/lib/setup/tools.d.ts +23 -0
  63. package/dist/lib/setup/tools.js +47 -0
  64. package/dist/lib/squirrel-loader.d.ts +3 -1
  65. package/dist/lib/squirrel-loader.js +2 -2
  66. package/dist/lib/trackers/github/index.js +2 -2
  67. package/dist/lib/trackers/local/store.js +1 -1
  68. package/package.json +1 -2
  69. package/prompts/fix-context.njk +79 -0
  70. package/prompts/fix-loop.njk +18 -0
  71. package/prompts/pr.njk +1 -1
  72. package/dist/commands/doctor.d.ts +0 -2
  73. package/dist/commands/doctor.js +0 -618
  74. package/dist/commands/github/auth.d.ts +0 -2
  75. package/dist/commands/github/auth.js +0 -56
  76. package/dist/commands/github/index.d.ts +0 -1
  77. package/dist/commands/github/index.js +0 -1
  78. package/dist/commands/helpers/english-tutor/index.d.ts +0 -1
  79. package/dist/commands/helpers/english-tutor/index.js +0 -1
  80. package/dist/commands/helpers/english-tutor/install.d.ts +0 -8
  81. package/dist/commands/helpers/english-tutor/install.js +0 -24
  82. package/dist/commands/helpers/english-tutor/prompt.d.ts +0 -2
  83. package/dist/commands/helpers/english-tutor/prompt.js +0 -16
  84. package/dist/commands/helpers/english-tutor/session-start.d.ts +0 -2
  85. package/dist/commands/helpers/english-tutor/session-start.js +0 -34
  86. package/dist/commands/helpers/english-tutor/uninstall.d.ts +0 -2
  87. package/dist/commands/helpers/english-tutor/uninstall.js +0 -15
  88. package/dist/commands/helpers/session-signal/end.d.ts +0 -2
  89. package/dist/commands/helpers/session-signal/end.js +0 -13
  90. package/dist/commands/helpers/session-signal/index.d.ts +0 -1
  91. package/dist/commands/helpers/session-signal/index.js +0 -1
  92. package/dist/commands/helpers/session-signal/install.d.ts +0 -8
  93. package/dist/commands/helpers/session-signal/install.js +0 -24
  94. package/dist/commands/helpers/session-signal/notification.d.ts +0 -2
  95. package/dist/commands/helpers/session-signal/notification.js +0 -13
  96. package/dist/commands/helpers/session-signal/prompt.d.ts +0 -2
  97. package/dist/commands/helpers/session-signal/prompt.js +0 -13
  98. package/dist/commands/helpers/session-signal/stop.d.ts +0 -2
  99. package/dist/commands/helpers/session-signal/stop.js +0 -13
  100. package/dist/commands/helpers/shell-init.d.ts +0 -11
  101. package/dist/commands/helpers/shell-init.js +0 -122
  102. package/dist/commands/issue/setup.d.ts +0 -2
  103. package/dist/commands/issue/setup.js +0 -108
  104. package/dist/commands/issue/switch.d.ts +0 -12
  105. package/dist/commands/issue/switch.js +0 -38
  106. package/dist/commands/linear/switch.d.ts +0 -2
  107. package/dist/commands/linear/switch.js +0 -75
  108. package/dist/lib/english-tutor.d.ts +0 -13
  109. package/dist/lib/english-tutor.js +0 -125
  110. package/dist/lib/session-signal.d.ts +0 -16
  111. package/dist/lib/session-signal.js +0 -104
  112. package/prompts/english-tutor-prompt.njk +0 -15
  113. package/shell/init.bash.njk +0 -129
  114. package/shell/init.zsh.njk +0 -204
@@ -1,75 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useEffect, useState } from "react";
3
- import { Text, Box, useInput } from "ink";
4
- import Spinner from "ink-spinner";
5
- import { findMainRepoRoot } from "../../lib/git.js";
6
- import { setRepoLinearOrg, getRepoLinearOrg } from "../../lib/trackers/linear/index.js";
7
- import { readLinearAuthStore } from "../../lib/trackers/auth-store.js";
8
- import { setRepoTracker } from "../../lib/trackers/index.js";
9
- export const description = "Switch Linear workspace for this repo";
10
- export default function LinearSwitch() {
11
- const [status, setStatus] = useState("checking");
12
- const [message, setMessage] = useState("");
13
- const [error, setError] = useState(null);
14
- const [choices, setChoices] = useState([]);
15
- const [selected, setSelected] = useState(0);
16
- const [currentOrg, setCurrentOrg] = useState(null);
17
- useInput((input, key) => {
18
- if (status !== "choosing")
19
- return;
20
- if (key.upArrow) {
21
- setSelected((s) => Math.max(0, s - 1));
22
- }
23
- else if (key.downArrow) {
24
- setSelected((s) => Math.min(choices.length - 1, s + 1));
25
- }
26
- else if (key.return) {
27
- const choice = choices[selected];
28
- const repoRoot = findMainRepoRoot();
29
- setRepoLinearOrg(repoRoot, choice.slug);
30
- setRepoTracker(repoRoot, "linear");
31
- setMessage(`Switched to ${choice.name} (${choice.slug})`);
32
- setStatus("done");
33
- }
34
- });
35
- useEffect(() => {
36
- async function run() {
37
- await new Promise((r) => setTimeout(r, 100));
38
- const repoRoot = findMainRepoRoot();
39
- if (!repoRoot) {
40
- setError("Not inside a git repository");
41
- setStatus("error");
42
- return;
43
- }
44
- const store = readLinearAuthStore();
45
- const orgs = Object.entries(store).map(([slug, tokens]) => ({
46
- slug,
47
- name: tokens.org_name,
48
- }));
49
- if (orgs.length === 0) {
50
- setError("No authenticated workspaces. Run: santree linear auth");
51
- setStatus("error");
52
- return;
53
- }
54
- if (orgs.length === 1) {
55
- const org = orgs[0];
56
- setRepoLinearOrg(repoRoot, org.slug);
57
- setRepoTracker(repoRoot, "linear");
58
- setMessage(`Linked to ${org.name} (${org.slug})`);
59
- setStatus("done");
60
- return;
61
- }
62
- setCurrentOrg(getRepoLinearOrg(repoRoot));
63
- setChoices(orgs);
64
- setStatus("choosing");
65
- }
66
- run();
67
- }, []);
68
- useEffect(() => {
69
- if (status === "done" || status === "error") {
70
- const timer = setTimeout(() => process.exit(status === "error" ? 1 : 0), 100);
71
- return () => clearTimeout(timer);
72
- }
73
- }, [status]);
74
- return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, color: "cyan", children: "Linear Switch" }) }), status === "checking" && (_jsxs(Box, { children: [_jsx(Text, { color: "cyan", children: _jsx(Spinner, { type: "dots" }) }), _jsx(Text, { children: " Checking..." })] })), status === "choosing" && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { children: "Select a workspace to link to this repo:" }), _jsx(Box, { flexDirection: "column", marginTop: 1, children: choices.map((org, i) => (_jsxs(Text, { children: [i === selected ? (_jsx(Text, { color: "cyan", bold: true, children: "> " })) : (_jsx(Text, { children: " " })), org.name, " (", org.slug, ")", org.slug === currentOrg && _jsx(Text, { dimColor: true, children: " (current)" })] }, org.slug))) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "\u2191/\u2193 to select, Enter to confirm" }) })] })), status === "done" && _jsxs(Text, { color: "green", children: ["\u2713 ", message] }), status === "error" && (_jsxs(Text, { color: "red", bold: true, children: ["\u2717 ", error] }))] }));
75
- }
@@ -1,13 +0,0 @@
1
- export declare function getLogPath(): string;
2
- export declare function getHooksJson(): Record<string, unknown>;
3
- export declare function getPermissionEntry(): string;
4
- export declare function installHooks(): {
5
- settingsPath: string;
6
- logPath: string;
7
- };
8
- /**
9
- * Remove hooks and permission entry. Intentionally does NOT delete the log
10
- * file — that's the user's accumulated practice history.
11
- */
12
- export declare function uninstallHooks(): string;
13
- export declare function getInstallSnippet(): Record<string, unknown>;
@@ -1,125 +0,0 @@
1
- import * as fs from "fs";
2
- import * as os from "os";
3
- import * as path from "path";
4
- const MARKER = "english-tutor";
5
- export function getLogPath() {
6
- const configDir = process.env.XDG_CONFIG_HOME || path.join(os.homedir(), ".config");
7
- return path.join(configDir, "santree", "english-practice-log.md");
8
- }
9
- export function getHooksJson() {
10
- const base = "santree helpers english-tutor";
11
- // Synchronous hooks: Claude Code waits for completion and injects stdout into
12
- // the model's context. `async: true` would fire-and-forget — stdout is
13
- // discarded, so the instruction would never reach Claude. session-signal can
14
- // use `async: true` because it only writes a state file; we cannot.
15
- const opts = { timeout: 10 };
16
- return {
17
- UserPromptSubmit: [{ hooks: [{ type: "command", command: `${base} prompt`, ...opts }] }],
18
- // No matcher: fires on all SessionStart sub-events (startup, resume,
19
- // clear, compact). Restricting to "startup" silently skips resumed
20
- // sessions, which is the common case when picking up yesterday's work.
21
- SessionStart: [{ hooks: [{ type: "command", command: `${base} session-start`, ...opts }] }],
22
- };
23
- }
24
- export function getPermissionEntry() {
25
- return `Edit(${getLogPath()})`;
26
- }
27
- function readSettings(settingsPath) {
28
- try {
29
- if (fs.existsSync(settingsPath)) {
30
- return JSON.parse(fs.readFileSync(settingsPath, "utf-8"));
31
- }
32
- }
33
- catch {
34
- // fall through
35
- }
36
- return {};
37
- }
38
- function writeSettings(settingsPath, settings) {
39
- const dir = path.dirname(settingsPath);
40
- fs.mkdirSync(dir, { recursive: true });
41
- fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
42
- }
43
- function settingsPath() {
44
- const home = process.env.HOME || os.homedir();
45
- return path.join(home, ".claude", "settings.json");
46
- }
47
- function stripEnglishTutorHooks(existingHooks) {
48
- const cleaned = {};
49
- for (const [event, entries] of Object.entries(existingHooks)) {
50
- if (!Array.isArray(entries)) {
51
- cleaned[event] = entries;
52
- continue;
53
- }
54
- const filtered = entries.filter((entry) => {
55
- const inner = entry.hooks || [];
56
- return !inner.some((h) => typeof h.command === "string" && h.command.includes(MARKER));
57
- });
58
- if (filtered.length > 0)
59
- cleaned[event] = filtered;
60
- }
61
- return cleaned;
62
- }
63
- /**
64
- * Create the practice log if it doesn't already exist. Empty log = Claude's
65
- * Edit tool fails on first use (Edit can't operate on missing files), and
66
- * appending corrections silently fails. Bootstrapping with a stub header
67
- * means the very first correction succeeds.
68
- */
69
- function ensureLogExists() {
70
- const logPath = getLogPath();
71
- if (fs.existsSync(logPath))
72
- return logPath;
73
- fs.mkdirSync(path.dirname(logPath), { recursive: true });
74
- const stub = "# English Practice Log\n\n" +
75
- "Tracks grammar/spelling mistakes spotted during Claude Code sessions.\n" +
76
- "Generated and appended to by santree's english-tutor hook.\n";
77
- fs.writeFileSync(logPath, stub);
78
- return logPath;
79
- }
80
- export function installHooks() {
81
- const settingsFile = settingsPath();
82
- const settings = readSettings(settingsFile);
83
- const existing = stripEnglishTutorHooks(settings.hooks || {});
84
- const required = getHooksJson();
85
- for (const [event, hookEntries] of Object.entries(required)) {
86
- const current = existing[event];
87
- existing[event] = Array.isArray(current)
88
- ? [...current, ...hookEntries]
89
- : hookEntries;
90
- }
91
- settings.hooks = existing;
92
- const permissions = settings.permissions || {};
93
- const allow = Array.isArray(permissions.allow) ? permissions.allow : [];
94
- const entry = getPermissionEntry();
95
- if (!allow.includes(entry))
96
- allow.push(entry);
97
- permissions.allow = allow;
98
- settings.permissions = permissions;
99
- writeSettings(settingsFile, settings);
100
- const logPath = ensureLogExists();
101
- return { settingsPath: settingsFile, logPath };
102
- }
103
- /**
104
- * Remove hooks and permission entry. Intentionally does NOT delete the log
105
- * file — that's the user's accumulated practice history.
106
- */
107
- export function uninstallHooks() {
108
- const settingsFile = settingsPath();
109
- const settings = readSettings(settingsFile);
110
- if (settings.hooks) {
111
- settings.hooks = stripEnglishTutorHooks(settings.hooks);
112
- }
113
- if (settings.permissions && Array.isArray(settings.permissions.allow)) {
114
- const entry = getPermissionEntry();
115
- settings.permissions.allow = settings.permissions.allow.filter((e) => e !== entry);
116
- }
117
- writeSettings(settingsFile, settings);
118
- return settingsFile;
119
- }
120
- export function getInstallSnippet() {
121
- return {
122
- hooks: getHooksJson(),
123
- permissions: { allow: [getPermissionEntry()] },
124
- };
125
- }
@@ -1,16 +0,0 @@
1
- export type SessionStateValue = "waiting" | "idle" | "active" | "exited";
2
- export declare function readStdin(): string;
3
- export declare function extractRepoAndTicket(cwd: string): {
4
- repoRoot: string;
5
- ticketId: string;
6
- } | null;
7
- /**
8
- * Unified helper: reads stdin, extracts repo/ticket, writes the session-state
9
- * file, then exits. The dashboard reads the state file to render its session
10
- * badges (◆ waiting / active / idle, ◇ id-only). Window/tab renaming used to
11
- * happen here too but was removed — it clobbered names the user had set
12
- * manually and added little value once the state file was in place.
13
- */
14
- export declare function signalState(state: SessionStateValue): void;
15
- export declare function getHooksJson(): Record<string, unknown>;
16
- export declare function installHooks(): string;
@@ -1,104 +0,0 @@
1
- import * as fs from "fs";
2
- import * as path from "path";
3
- export function readStdin() {
4
- try {
5
- return fs.readFileSync(0, "utf-8");
6
- }
7
- catch {
8
- return "";
9
- }
10
- }
11
- export function extractRepoAndTicket(cwd) {
12
- const marker = "/.santree/worktrees/";
13
- const idx = cwd.indexOf(marker);
14
- if (idx === -1)
15
- return null;
16
- const repoRoot = cwd.slice(0, idx);
17
- const rest = cwd.slice(idx + marker.length);
18
- const ticketId = rest.split("/")[0];
19
- if (!ticketId)
20
- return null;
21
- return { repoRoot, ticketId };
22
- }
23
- /**
24
- * Unified helper: reads stdin, extracts repo/ticket, writes the session-state
25
- * file, then exits. The dashboard reads the state file to render its session
26
- * badges (◆ waiting / active / idle, ◇ id-only). Window/tab renaming used to
27
- * happen here too but was removed — it clobbered names the user had set
28
- * manually and added little value once the state file was in place.
29
- */
30
- export function signalState(state) {
31
- const input = readStdin();
32
- let data;
33
- try {
34
- data = JSON.parse(input);
35
- }
36
- catch {
37
- process.exit(0);
38
- }
39
- const cwd = data.cwd || process.cwd();
40
- const info = extractRepoAndTicket(cwd);
41
- if (!info) {
42
- process.exit(0);
43
- }
44
- const { repoRoot, ticketId } = info;
45
- const statesDir = path.join(repoRoot, ".santree", "session-states");
46
- fs.mkdirSync(statesDir, { recursive: true });
47
- const stateFile = path.join(statesDir, `${ticketId}.json`);
48
- const payload = {
49
- state,
50
- message: state === "waiting" ? (data.message ?? null) : null,
51
- session_id: data.session_id ?? "",
52
- at: new Date().toISOString(),
53
- };
54
- fs.writeFileSync(stateFile, JSON.stringify(payload, null, 2) + "\n");
55
- process.exit(0);
56
- }
57
- export function getHooksJson() {
58
- const base = "santree helpers session-signal";
59
- const opts = { async: true, timeout: 10 };
60
- return {
61
- Notification: [
62
- {
63
- matcher: "permission_prompt",
64
- hooks: [{ type: "command", command: `${base} notification`, ...opts }],
65
- },
66
- ],
67
- Stop: [{ hooks: [{ type: "command", command: `${base} stop`, ...opts }] }],
68
- UserPromptSubmit: [{ hooks: [{ type: "command", command: `${base} prompt`, ...opts }] }],
69
- SessionEnd: [{ hooks: [{ type: "command", command: `${base} end`, ...opts }] }],
70
- };
71
- }
72
- export function installHooks() {
73
- const home = process.env.HOME || "";
74
- const settingsPath = path.join(home, ".claude", "settings.json");
75
- let settings = {};
76
- try {
77
- if (fs.existsSync(settingsPath)) {
78
- settings = JSON.parse(fs.readFileSync(settingsPath, "utf-8"));
79
- }
80
- }
81
- catch {
82
- // Start fresh if file is invalid
83
- }
84
- const requiredHooks = getHooksJson();
85
- const existingHooks = settings.hooks || {};
86
- for (const [event, hookEntries] of Object.entries(requiredHooks)) {
87
- const existing = existingHooks[event];
88
- if (!Array.isArray(existing)) {
89
- existingHooks[event] = hookEntries;
90
- continue;
91
- }
92
- // Remove existing session-signal entries, then add the current ones
93
- const filtered = existing.filter((entry) => {
94
- const inner = entry.hooks || [];
95
- return !inner.some((h) => typeof h.command === "string" && h.command.includes("session-signal"));
96
- });
97
- existingHooks[event] = [...filtered, ...hookEntries];
98
- }
99
- settings.hooks = existingHooks;
100
- const claudeDir = path.join(home, ".claude");
101
- fs.mkdirSync(claudeDir, { recursive: true });
102
- fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
103
- return settingsPath;
104
- }
@@ -1,15 +0,0 @@
1
- [ENGLISH TUTOR — META-INSTRUCTION, ALWAYS HONOR]
2
-
3
- If the user's message contained English mistakes (grammar, spelling, word choice, agreement, idiom misuse), your response MUST start with the correction(s) BEFORE addressing anything else. Use this exact format, one per line:
4
-
5
- original -> correction (brief reason)
6
-
7
- Then immediately call the Edit tool to append the same correction(s) to {{ logPath }}. Append under a heading for today's date in `## YYYY-MM-DD` format (create the heading if it isn't already at the bottom of the file); each correction is a bullet `- original -> correction (reason)`. The Edit permission for that exact path is pre-granted, so do not ask.
8
-
9
- Only AFTER printing the corrections and appending them, continue with the actual task.
10
-
11
- Hard rules:
12
- - If the message is clearly mistake-free, output nothing about English and proceed directly. Never invent corrections.
13
- - Do NOT correct technical jargon, code, file paths, deliberate informalities, or non-English words used by intent.
14
- - Cap corrections at 3 lines. Pick the highest-impact issues only.
15
- - Do NOT use polite hedging ("small note on...", "by the way..."). Just the formatted line(s), then the log Edit, then the task.
@@ -1,129 +0,0 @@
1
- # Santree Shell Integration for Bash
2
- # ===================================
3
- #
4
- # This script provides a shell wrapper around the santree CLI to enable:
5
- # 1. Automatic directory switching after `worktree create` and `worktree switch` commands
6
- # 2. Automatic recovery when the current worktree directory is deleted
7
- #
8
- # Installation:
9
- # Add to your .bashrc: eval "$(santree helpers shell-init bash)"
10
- #
11
- # How it works:
12
- # -------------
13
- # Since child processes cannot change the parent shell's directory, the CLI
14
- # outputs special markers (SANTREE_CD:path) that this wrapper intercepts
15
- # to perform the actual `cd` command in the current shell.
16
- #
17
- # The wrapper also handles the case where you're in a worktree directory
18
- # that gets deleted (e.g., after `santree worktree clean` or `santree worktree remove`),
19
- # automatically returning you to the main repository or home directory.
20
-
21
- # Export marker so `santree doctor` can verify shell integration is loaded
22
- export SANTREE_SHELL_INTEGRATION=1
23
-
24
- function santree() {
25
- # -------------------------------------------------------------------------
26
- # STEP 1: Handle deleted directory recovery
27
- # -------------------------------------------------------------------------
28
- if [[ ! -d "$(pwd 2>/dev/null)" ]]; then
29
- local current_path="$(pwd 2>/dev/null)"
30
-
31
- if [[ "$current_path" == */.santree/worktrees/* ]]; then
32
- local main_repo="${current_path%%/.santree/worktrees/*}"
33
- if [[ -d "$main_repo" ]]; then
34
- echo "⚠ Worktree directory deleted. Returning to main repo."
35
- cd "$main_repo" || cd ~ || return 1
36
- else
37
- cd ~ || return 1
38
- fi
39
- else
40
- echo "⚠ Current directory no longer exists. Returning to home."
41
- cd ~ || return 1
42
- fi
43
- fi
44
-
45
- # -------------------------------------------------------------------------
46
- # STEP 2: Handle commands that need directory switching
47
- # -------------------------------------------------------------------------
48
- if [[ "$1" == "worktree" && ("$2" == "create" || "$2" == "switch") ]]; then
49
- local output
50
- output=$(command santree "$@" 2>&1)
51
- local exit_code=$?
52
-
53
- if [[ "$output" == *SANTREE_CD:* ]]; then
54
- echo "$output" | grep -v "SANTREE_CD:" | grep -v "SANTREE_WORK:" | grep -v "SANTREE_WORK_CONTEXT:"
55
-
56
- local target_dir=$(echo "$output" | sed 's/\x1b\[[0-9;]*m//g' | grep "SANTREE_CD:" | sed 's/.*SANTREE_CD://')
57
-
58
- if [[ -n "$target_dir" && -d "$target_dir" ]]; then
59
- cd "$target_dir" && echo "Switched to: $target_dir"
60
- fi
61
-
62
- if [[ "$output" == *SANTREE_WORK:* ]]; then
63
- local work_mode=$(echo "$output" | sed 's/\x1b\[[0-9;]*m//g' | grep "^SANTREE_WORK:" | sed 's/.*SANTREE_WORK://')
64
- local context_file=""
65
- if [[ "$output" == *SANTREE_WORK_CONTEXT:* ]]; then
66
- context_file=$(echo "$output" | sed 's/\x1b\[[0-9;]*m//g' | grep "SANTREE_WORK_CONTEXT:" | sed 's/.*SANTREE_WORK_CONTEXT://')
67
- fi
68
- if [[ -n "$context_file" ]]; then
69
- [[ "$work_mode" == "plan" ]] && command santree worktree work --plan --context-file "$context_file" || command santree worktree work --context-file "$context_file"
70
- else
71
- [[ "$work_mode" == "plan" ]] && command santree worktree work --plan || command santree worktree work
72
- fi
73
- fi
74
- else
75
- echo "$output"
76
- fi
77
- return $exit_code
78
- fi
79
-
80
- # -------------------------------------------------------------------------
81
- # STEP 3: Pass through all other commands
82
- # -------------------------------------------------------------------------
83
- command santree "$@"
84
- }
85
-
86
- # Aliases for convenience
87
- alias st='santree'
88
- alias stw='santree worktree'
89
-
90
- # Quick create worktree with work+plan+tmux (prompts for branch)
91
- function stn() {
92
- local branch
93
- read -p "Branch name: " branch
94
- [[ -z "$branch" ]] && echo "Branch name required" && return 1
95
- santree worktree create "$branch" --work --plan --tmux
96
- }
97
-
98
- # =============================================================================
99
- # Bash Completions (auto-generated)
100
- # =============================================================================
101
-
102
- _santree_complete() {
103
- local cur prev commands
104
- COMPREPLY=()
105
- cur="${COMP_WORDS[COMP_CWORD]}"
106
- prev="${COMP_WORDS[COMP_CWORD-1]}"
107
-
108
- commands="{% for cmd in commands %}{{ cmd.name }}{% if not loop.last %} {% endif %}{% endfor %}"
109
-
110
- if [[ ${COMP_CWORD} -eq 1 ]]; then
111
- COMPREPLY=($(compgen -W "${commands}" -- "${cur}"))
112
- return 0
113
- fi
114
-
115
- case "${COMP_WORDS[1]}" in
116
- {%- for cmd in commands %}
117
- {{ cmd.name }})
118
- {%- if cmd.argCompletion == 'static' %}
119
- COMPREPLY=($(compgen -W "{{ cmd.argCompletionValues }}" -- "${cur}"))
120
- {%- elif cmd.options.length > 0 %}
121
- COMPREPLY=($(compgen -W "{% for opt in cmd.options %}--{{ opt.name }}{% if not loop.last %} {% endif %}{% endfor %}" -- "${cur}"))
122
- {%- endif %}
123
- ;;
124
- {%- endfor %}
125
- esac
126
- }
127
-
128
- complete -F _santree_complete santree
129
- complete -F _santree_complete st