santree 0.0.16 → 0.1.0

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 (66) hide show
  1. package/README.md +177 -36
  2. package/dist/commands/doctor.js +37 -64
  3. package/dist/commands/helpers/index.d.ts +1 -0
  4. package/dist/commands/helpers/index.js +1 -0
  5. package/dist/commands/helpers/shell-init.js +122 -0
  6. package/dist/commands/linear/auth.d.ts +12 -0
  7. package/dist/commands/linear/auth.js +173 -0
  8. package/dist/commands/linear/index.d.ts +1 -0
  9. package/dist/commands/linear/index.js +1 -0
  10. package/dist/commands/linear/open.d.ts +2 -0
  11. package/dist/commands/linear/open.js +65 -0
  12. package/dist/commands/{pr.js → pr/create.js} +3 -3
  13. package/dist/commands/pr/fix.d.ts +2 -0
  14. package/dist/commands/pr/fix.js +48 -0
  15. package/dist/commands/pr/index.d.ts +1 -0
  16. package/dist/commands/pr/index.js +1 -0
  17. package/dist/commands/pr/open.d.ts +2 -0
  18. package/dist/commands/pr/open.js +52 -0
  19. package/dist/commands/pr/review.d.ts +2 -0
  20. package/dist/commands/pr/review.js +41 -0
  21. package/dist/commands/{clean.js → worktree/clean.js} +3 -4
  22. package/dist/commands/{commit.js → worktree/commit.js} +4 -8
  23. package/dist/commands/{create.js → worktree/create.js} +4 -4
  24. package/dist/commands/worktree/index.d.ts +1 -0
  25. package/dist/commands/worktree/index.js +1 -0
  26. package/dist/commands/{list.js → worktree/list.js} +2 -2
  27. package/dist/commands/{editor.d.ts → worktree/open.d.ts} +1 -1
  28. package/dist/commands/{editor.js → worktree/open.js} +4 -9
  29. package/dist/commands/{remove.js → worktree/remove.js} +1 -1
  30. package/dist/commands/{setup.js → worktree/setup.js} +2 -2
  31. package/dist/commands/{switch.js → worktree/switch.js} +1 -1
  32. package/dist/commands/{sync.js → worktree/sync.js} +3 -4
  33. package/dist/commands/{work.d.ts → worktree/work.d.ts} +0 -2
  34. package/dist/commands/worktree/work.js +60 -0
  35. package/dist/lib/ai.d.ts +39 -0
  36. package/dist/lib/ai.js +63 -0
  37. package/dist/lib/git.d.ts +19 -0
  38. package/dist/lib/git.js +26 -4
  39. package/dist/lib/github.js +1 -1
  40. package/dist/lib/linear.d.ts +63 -0
  41. package/dist/lib/linear.js +425 -0
  42. package/dist/lib/prompts.d.ts +6 -1
  43. package/dist/lib/prompts.js +26 -1
  44. package/package.json +15 -2
  45. package/prompts/fill-pr.njk +3 -0
  46. package/prompts/fix-pr.njk +5 -1
  47. package/prompts/implement.njk +5 -1
  48. package/prompts/plan.njk +5 -1
  49. package/prompts/review.njk +5 -1
  50. package/prompts/ticket.njk +24 -0
  51. package/shell/init.bash.njk +12 -9
  52. package/shell/init.zsh.njk +11 -8
  53. package/dist/commands/shell-init.js +0 -91
  54. package/dist/commands/work.js +0 -113
  55. /package/dist/commands/{shell-init.d.ts → helpers/shell-init.d.ts} +0 -0
  56. /package/dist/commands/{statusline.d.ts → helpers/statusline.d.ts} +0 -0
  57. /package/dist/commands/{statusline.js → helpers/statusline.js} +0 -0
  58. /package/dist/commands/{pr.d.ts → pr/create.d.ts} +0 -0
  59. /package/dist/commands/{clean.d.ts → worktree/clean.d.ts} +0 -0
  60. /package/dist/commands/{commit.d.ts → worktree/commit.d.ts} +0 -0
  61. /package/dist/commands/{create.d.ts → worktree/create.d.ts} +0 -0
  62. /package/dist/commands/{list.d.ts → worktree/list.d.ts} +0 -0
  63. /package/dist/commands/{remove.d.ts → worktree/remove.d.ts} +0 -0
  64. /package/dist/commands/{setup.d.ts → worktree/setup.d.ts} +0 -0
  65. /package/dist/commands/{switch.d.ts → worktree/switch.d.ts} +0 -0
  66. /package/dist/commands/{sync.d.ts → worktree/sync.d.ts} +0 -0
package/README.md CHANGED
@@ -32,15 +32,16 @@ npm install -g santree
32
32
  Add to your `.zshrc` or `.bashrc`:
33
33
 
34
34
  ```bash
35
- eval "$(santree shell-init zsh)" # for zsh
36
- eval "$(santree shell-init bash)" # for bash
35
+ eval "$(santree helpers shell-init zsh)" # for zsh
36
+ eval "$(santree helpers shell-init bash)" # for bash
37
37
  ```
38
38
 
39
- This enables automatic directory switching after `create` and `switch` commands.
39
+ This enables automatic directory switching after `worktree create` and `worktree switch` commands.
40
40
 
41
41
  The shell integration also provides:
42
42
  - `st` - Alias for `santree`
43
- - `stw` - Quick create worktree with `--work --plan --tmux` (prompts for branch name)
43
+ - `stw` - Alias for `santree worktree` (e.g., `stw list`, `stw create`)
44
+ - `stn` - Quick create worktree with `--work --plan --tmux` (prompts for branch name)
44
45
 
45
46
  ### Verify Setup
46
47
 
@@ -56,36 +57,73 @@ This checks that all required tools are installed and configured correctly.
56
57
 
57
58
  ```bash
58
59
  # Create a new worktree and switch to it
59
- santree create feature/my-new-feature
60
+ santree worktree create feature/TEAM-123-my-feature
60
61
 
61
62
  # List all worktrees with PR status
62
- santree list
63
+ santree worktree list
64
+
65
+ # Launch Claude AI to work on the current ticket
66
+ santree worktree work
63
67
 
64
68
  # Switch to another worktree
65
- santree switch main
69
+ santree worktree switch TEAM-456
70
+
71
+ # Create a PR
72
+ santree pr create
66
73
 
67
74
  # Clean up worktrees with merged PRs
68
- santree clean
75
+ santree worktree clean
69
76
  ```
70
77
 
78
+ With the `stw` alias: `stw create`, `stw list`, `stw switch`, `stw work`, `stw clean`.
79
+
71
80
  ---
72
81
 
73
82
  ## Commands
74
83
 
84
+ ### Worktree (`santree worktree`)
85
+
86
+ | Command | Description |
87
+ |---------|-------------|
88
+ | `santree worktree create <branch>` | Create a new worktree from base branch |
89
+ | `santree worktree list` | List all worktrees with PR status and commits ahead |
90
+ | `santree worktree switch <branch>` | Switch to another worktree |
91
+ | `santree worktree remove <branch>` | Remove a worktree and its branch |
92
+ | `santree worktree clean` | Remove worktrees with merged/closed PRs |
93
+ | `santree worktree sync` | Sync current worktree with base branch |
94
+ | `santree worktree work` | Launch Claude AI to work on the current ticket |
95
+ | `santree worktree open` | Open workspace in VSCode or Cursor |
96
+ | `santree worktree setup` | Run the init script (`.santree/init.sh`) |
97
+ | `santree worktree commit` | Stage and commit changes |
98
+
99
+ ### Pull Requests (`santree pr`)
100
+
101
+ | Command | Description |
102
+ |---------|-------------|
103
+ | `santree pr create` | Create a GitHub pull request |
104
+ | `santree pr open` | Open the current PR in the browser |
105
+ | `santree pr fix` | Fix PR review comments with AI |
106
+ | `santree pr review` | Review changes against ticket with AI |
107
+
108
+ ### Linear (`santree linear`)
109
+
110
+ | Command | Description |
111
+ |---------|-------------|
112
+ | `santree linear auth` | Authenticate with Linear (OAuth) |
113
+ | `santree linear open` | Open the current Linear ticket in the browser |
114
+
115
+ ### Helpers (`santree helpers`)
116
+
117
+ | Command | Description |
118
+ |---------|-------------|
119
+ | `santree helpers shell-init` | Output shell integration script |
120
+ | `santree helpers statusline` | Custom statusline for Claude Code |
121
+
122
+ ### Top-level
123
+
75
124
  | Command | Description |
76
125
  |---------|-------------|
77
- | `santree list` | List all worktrees with PR status and commits ahead |
78
- | `santree create <branch>` | Create a new worktree from base branch |
79
- | `santree switch <branch>` | Switch to another worktree |
80
- | `santree remove <branch>` | Remove a worktree and its branch |
81
- | `santree sync` | Sync current worktree with base branch |
82
- | `santree setup` | Run the init script (`.santree/init.sh`) |
83
- | `santree work` | Launch Claude AI to work on the current ticket |
84
- | `santree pr` | Create a GitHub pull request (opens in browser) |
85
- | `santree clean` | Remove worktrees with merged/closed PRs |
86
126
  | `santree doctor` | Check system requirements and integrations |
87
- | `santree editor` | Open workspace file in VSCode or Cursor |
88
- | `santree statusline` | Statusline wrapper for Claude Code |
89
127
 
90
128
  ---
91
129
 
@@ -97,11 +135,15 @@ Create isolated worktrees for each feature branch. No more stashing or committin
97
135
  ### GitHub Integration
98
136
  See PR status directly in your worktree list. Clean up worktrees automatically when PRs are merged or closed.
99
137
 
138
+ ### Linear Integration
139
+ Santree fetches Linear ticket data (title, description, comments, images) and injects it into prompts when running `santree worktree work`. See [Linear Integration](#linear-integration-1) for setup.
140
+
100
141
  ### Claude AI Integration
101
- Launch Claude with full context about your current ticket using `santree work`. Supports different modes:
102
- - `--plan` - Create an implementation plan only
103
- - `--review` - Review changes against ticket requirements
104
- - `--fix-pr` - Address PR review comments
142
+ Launch Claude with full context about your current ticket. Supports different modes:
143
+ - `santree worktree work` - Implement the ticket
144
+ - `santree worktree work --plan` - Create an implementation plan only
145
+ - `santree pr review` - Review changes against ticket requirements
146
+ - `santree pr fix` - Address PR review comments
105
147
 
106
148
  ### Init Scripts
107
149
  Run custom setup scripts when creating worktrees. Perfect for copying `.env` files, installing dependencies, or any project-specific setup.
@@ -129,12 +171,28 @@ user/TEAM-123-feature-description
129
171
  feature/PROJ-456-add-auth
130
172
  ```
131
173
 
132
- ### Linear MCP (for Claude integration)
174
+ ### Linear Integration
175
+
176
+ Santree fetches Linear ticket data (title, description, comments, images) and injects it into prompts when running `santree worktree work`.
133
177
 
134
178
  ```bash
135
- claude mcp add --transport http linear https://mcp.linear.app/mcp
179
+ # Authenticate with Linear (opens browser for OAuth)
180
+ santree linear auth
181
+
182
+ # Check auth status
183
+ santree linear auth --status
184
+
185
+ # Verify a ticket is fetched correctly
186
+ santree linear auth --test TEAM-123
187
+
188
+ # Log out
189
+ santree linear auth --logout
136
190
  ```
137
191
 
192
+ On first run, `santree linear auth` opens your browser to authorize the app with your Linear workspace. Tokens are stored in `$XDG_CONFIG_HOME/santree/auth.json` (defaults to `~/.config/santree/auth.json`) and auto-refresh transparently.
193
+
194
+ If you have multiple workspaces authenticated, running `santree linear auth` in a new repo will let you pick which one to link. Images from tickets are downloaded to a temp directory and cleaned up after Claude exits.
195
+
138
196
  ### Claude Code Statusline (Optional)
139
197
 
140
198
  Santree provides a custom statusline for Claude Code showing git info, model, context usage, and cost.
@@ -144,7 +202,7 @@ Add to `~/.claude/settings.json`:
144
202
  {
145
203
  "statusLine": {
146
204
  "type": "command",
147
- "command": "santree statusline"
205
+ "command": "santree helpers statusline"
148
206
  }
149
207
  }
150
208
  ```
@@ -155,7 +213,7 @@ The statusline displays: `repo | branch | S: staged | U: unstaged | A: untracked
155
213
 
156
214
  ## Command Options
157
215
 
158
- ### create
216
+ ### worktree create
159
217
  | Option | Description |
160
218
  |--------|-------------|
161
219
  | `--base <branch>` | Base branch to create from (default: main/master) |
@@ -163,36 +221,44 @@ The statusline displays: `repo | branch | S: staged | U: unstaged | A: untracked
163
221
  | `--plan` | With --work, only create implementation plan |
164
222
  | `--no-pull` | Skip pulling latest changes |
165
223
  | `--tmux` | Open worktree in new tmux window |
224
+ | `--name <name>` | Custom tmux window name |
166
225
 
167
- ### sync
226
+ ### worktree sync
168
227
  | Option | Description |
169
228
  |--------|-------------|
170
229
  | `--rebase` | Use rebase instead of merge |
171
230
 
172
- ### remove
231
+ ### worktree remove
173
232
  Removes the worktree and deletes the branch. Uses force mode by default (removes even with uncommitted changes).
174
233
 
175
- ### clean
234
+ ### worktree clean
176
235
  Shows worktrees with merged/closed PRs and prompts for confirmation before removing.
177
236
 
178
- ### editor
237
+ ### worktree open
179
238
  | Option | Description |
180
239
  |--------|-------------|
181
240
  | `--editor <cmd>` | Editor command to use (default: `code`). Also configurable via `SANTREE_EDITOR` env var |
182
241
 
183
- ### pr
242
+ ### worktree work
243
+ | Option | Description |
244
+ |--------|-------------|
245
+ | `--plan` | Only create implementation plan |
246
+
247
+ Automatically fetches Linear ticket data if authenticated. Degrades gracefully if not.
248
+
249
+ ### pr create
184
250
  | Option | Description |
185
251
  |--------|-------------|
186
252
  | `--fill` | Use AI to fill the PR template before opening |
187
253
 
188
254
  Automatically pushes, detects existing PRs, and uses the first commit message as the title. If a closed PR exists for the branch, prompts before creating a new one.
189
255
 
190
- ### work
256
+ ### linear auth
191
257
  | Option | Description |
192
258
  |--------|-------------|
193
- | `--plan` | Only create implementation plan |
194
- | `--review` | Review changes against requirements |
195
- | `--fix-pr` | Fetch and fix PR comments |
259
+ | `--status` | Show current auth status (org, token expiry) |
260
+ | `--test <id>` | Fetch a ticket by ID to verify integration works |
261
+ | `--logout` | Revoke tokens and log out |
196
262
 
197
263
  ---
198
264
 
@@ -205,3 +271,78 @@ Automatically pushes, detects existing PRs, and uses the first commit message as
205
271
  | GitHub CLI (`gh`) | PR integration |
206
272
  | tmux | Optional: new window support |
207
273
  | VSCode (`code`) or Cursor (`cursor`) | Optional: workspace editor |
274
+
275
+ ---
276
+
277
+ ## Development
278
+
279
+ ### Setup
280
+
281
+ ```bash
282
+ git clone https://github.com/santiagotoscanini/santree.git
283
+ cd santree
284
+ npm install
285
+ ```
286
+
287
+ ### Build & Run
288
+
289
+ ```bash
290
+ # Compile TypeScript
291
+ npm run build
292
+
293
+ # Run the local build
294
+ node dist/cli.js <command>
295
+
296
+ # Watch mode (recompiles on save)
297
+ npm run dev
298
+ ```
299
+
300
+ During development, use `node dist/cli.js` instead of `santree` to run the local version:
301
+
302
+ ```bash
303
+ node dist/cli.js worktree list
304
+ node dist/cli.js worktree work
305
+ node dist/cli.js linear auth --test TEAM-123
306
+ ```
307
+
308
+ ### Link globally (optional)
309
+
310
+ To use `santree` as a global command pointing to your local build:
311
+
312
+ ```bash
313
+ npm link
314
+ ```
315
+
316
+ Now `santree` runs your local `dist/cli.js`. Unlink with `npm unlink -g santree`.
317
+
318
+ ### Code Quality
319
+
320
+ ```bash
321
+ npm run lint # Check for lint + formatting errors
322
+ npm run lint:fix # Auto-fix lint + formatting errors
323
+ npm run format # Format all source files with Prettier
324
+ ```
325
+
326
+ A pre-commit hook (via husky + lint-staged) automatically runs ESLint and Prettier on staged files.
327
+
328
+ ### Project Structure
329
+
330
+ ```
331
+ source/
332
+ ├── cli.tsx # Entry point (Pastel app runner)
333
+ ├── lib/
334
+ │ ├── ai.ts # Shared AI logic (context, prompt, launch)
335
+ │ ├── git.ts # Git helpers (worktrees, branches, metadata)
336
+ │ ├── github.ts # GitHub CLI wrapper (PR info, auth, push)
337
+ │ ├── linear.ts # Linear GraphQL API client (OAuth, tickets, images)
338
+ │ ├── exec.ts # Shell command helpers
339
+ │ └── prompts.ts # Nunjucks template renderer
340
+ └── commands/ # One React (Ink) component per CLI command
341
+ ├── doctor.tsx # Top-level: system check
342
+ ├── worktree/ # Worktree management (create, list, switch, etc.)
343
+ ├── pr/ # PR lifecycle (create, open, fix, review)
344
+ ├── linear/ # Linear integration (auth, open)
345
+ └── helpers/ # Shell init, statusline
346
+ prompts/ # Nunjucks templates: implement, plan, review, fix-pr, fill-pr, ticket
347
+ shell/ # Shell integration templates: init.zsh.njk, init.bash.njk
348
+ ```
@@ -6,7 +6,8 @@ import { exec, execSync } from "child_process";
6
6
  import { promisify } from "util";
7
7
  import * as fs from "fs";
8
8
  import * as path from "path";
9
- import { findMainRepoRoot, getSantreeDir, getInitScriptPath, } from "../lib/git.js";
9
+ import { findMainRepoRoot, getSantreeDir, getInitScriptPath } from "../lib/git.js";
10
+ import { getAuthStatus, getValidTokens } from "../lib/linear.js";
10
11
  const execAsync = promisify(exec);
11
12
  export const description = "Check system requirements and integrations";
12
13
  /**
@@ -103,51 +104,30 @@ async function checkGhAuth() {
103
104
  };
104
105
  }
105
106
  /**
106
- * Checks if Linear MCP is configured using `claude mcp list`.
107
- * Output formats:
108
- * linear: https://mcp.linear.app/mcp (HTTP) - ✓ Connected
109
- * linear: https://mcp.linear.app/mcp (HTTP) - ⚠ Needs authentication
107
+ * Checks Linear API authentication status.
110
108
  */
111
- async function checkLinearMcp() {
112
- const output = await tryExec("claude mcp list 2>&1");
113
- if (!output) {
109
+ async function checkLinearAuth() {
110
+ const repoRoot = findMainRepoRoot();
111
+ const status = getAuthStatus(repoRoot);
112
+ if (!status.authenticated || !status.orgSlug) {
114
113
  return {
115
- name: "Linear MCP",
116
- configured: false,
117
- hint: "Claude CLI not available to check MCP servers",
118
- };
119
- }
120
- // Check if "No MCP servers configured"
121
- if (output.includes("No MCP servers configured")) {
122
- return {
123
- name: "Linear MCP",
124
- configured: false,
125
- hint: "Run: claude mcp add --transport http linear https://mcp.linear.app/mcp",
126
- };
127
- }
128
- // Look for a line containing "linear"
129
- const lines = output.split("\n");
130
- const linearLine = lines.find((line) => line.toLowerCase().includes("linear"));
131
- if (linearLine) {
132
- const urlMatch = linearLine.match(/:\s*(https?:\/\/[^\s]+)/);
133
- // Extract status after " - " (e.g., "✓ Connected" or "⚠ Needs authentication")
134
- const statusMatch = linearLine.match(/ - (.+)$/);
135
- const status = statusMatch?.[1]?.trim();
136
- const isConnected = status?.includes("✓") || status?.includes("Connected");
137
- return {
138
- name: "Linear MCP",
139
- configured: true,
140
- url: urlMatch?.[1],
141
- status,
142
- hint: isConnected
143
- ? undefined
144
- : "Open Linear MCP URL in browser to authenticate",
114
+ authenticated: false,
115
+ hint: "Run: santree linear auth",
145
116
  };
146
117
  }
118
+ // Try to validate/refresh tokens
119
+ const valid = await getValidTokens(status.orgSlug);
147
120
  return {
148
- name: "Linear MCP",
149
- configured: false,
150
- hint: "Run: claude mcp add --transport http linear https://mcp.linear.app/mcp",
121
+ authenticated: true,
122
+ orgSlug: status.orgSlug,
123
+ orgName: status.orgName,
124
+ tokenValid: valid !== null,
125
+ repoLinked: status.repoLinked,
126
+ hint: !valid
127
+ ? "Token expired. Run: santree linear auth"
128
+ : !status.repoLinked
129
+ ? "Repo not linked. Run: santree linear auth"
130
+ : undefined,
151
131
  };
152
132
  }
153
133
  /**
@@ -156,11 +136,7 @@ async function checkLinearMcp() {
156
136
  */
157
137
  function checkShellIntegration() {
158
138
  const shell = process.env.SHELL || "";
159
- const shellName = shell.includes("zsh")
160
- ? "zsh"
161
- : shell.includes("bash")
162
- ? "bash"
163
- : null;
139
+ const shellName = shell.includes("zsh") ? "zsh" : shell.includes("bash") ? "bash" : null;
164
140
  const configured = process.env.SANTREE_SHELL_INTEGRATION === "1";
165
141
  return { configured, shell: shellName };
166
142
  }
@@ -181,7 +157,8 @@ async function checkStatusline() {
181
157
  currentCommand = String(settings.statusLine.command);
182
158
  // Check if it points to santree statusline
183
159
  claudeSettingsConfigured =
184
- currentCommand.includes("santree statusline");
160
+ currentCommand.includes("santree statusline") ||
161
+ currentCommand.includes("santree helpers statusline");
185
162
  }
186
163
  }
187
164
  }
@@ -191,7 +168,7 @@ async function checkStatusline() {
191
168
  let hint;
192
169
  if (!claudeSettingsConfigured) {
193
170
  hint =
194
- 'Add to ~/.claude/settings.json: "statusLine": { "type": "command", "command": "santree statusline" }';
171
+ 'Add to ~/.claude/settings.json: "statusLine": { "type": "command", "command": "santree helpers statusline" }';
195
172
  }
196
173
  return {
197
174
  claudeSettingsConfigured,
@@ -280,13 +257,12 @@ function StatusIcon({ ok, required }) {
280
257
  function ToolRow({ tool }) {
281
258
  return (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsxs(Box, { children: [_jsx(StatusIcon, { ok: tool.installed && !tool.hint, required: tool.required }), _jsx(Text, { children: " " }), _jsx(Text, { bold: true, children: tool.name }), _jsxs(Text, { dimColor: true, children: [" - ", tool.description] }), !tool.required && _jsx(Text, { dimColor: true, children: " (optional)" })] }), tool.installed ? (_jsxs(Box, { marginLeft: 2, flexDirection: "column", children: [_jsxs(Text, { dimColor: true, children: ["Version: ", tool.version] }), _jsxs(Text, { dimColor: true, children: ["Path: ", tool.path] }), tool.authStatus && _jsxs(Text, { dimColor: true, children: ["Auth: ", tool.authStatus] }), tool.hint && _jsxs(Text, { color: "yellow", children: ["\u21B3 ", tool.hint] })] })) : (_jsx(Box, { marginLeft: 2, children: _jsxs(Text, { color: "yellow", children: ["\u21B3 ", tool.hint] }) }))] }));
282
259
  }
283
- function McpRow({ mcp }) {
284
- const isOk = mcp.configured &&
285
- Boolean(mcp.status?.includes("") || mcp.status?.includes("Connected"));
286
- return (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsxs(Box, { children: [_jsx(StatusIcon, { ok: isOk, required: true }), _jsx(Text, { children: " " }), _jsx(Text, { bold: true, children: mcp.name }), _jsx(Text, { dimColor: true, children: " - Linear ticket integration for Claude" })] }), mcp.configured ? (_jsxs(Box, { marginLeft: 2, flexDirection: "column", children: [mcp.url && _jsxs(Text, { dimColor: true, children: ["URL: ", mcp.url] }), mcp.status && _jsxs(Text, { dimColor: true, children: ["Status: ", mcp.status] }), mcp.hint && _jsxs(Text, { color: "yellow", children: ["\u21B3 ", mcp.hint] })] })) : (mcp.hint && (_jsx(Box, { marginLeft: 2, children: _jsxs(Text, { color: "yellow", children: ["\u21B3 ", mcp.hint] }) })))] }));
260
+ function LinearRow({ linear }) {
261
+ const isOk = linear.authenticated && linear.tokenValid && linear.repoLinked;
262
+ return (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsxs(Box, { children: [_jsx(StatusIcon, { ok: !!isOk, required: true }), _jsx(Text, { children: " " }), _jsx(Text, { bold: true, children: "Linear API" }), _jsx(Text, { dimColor: true, children: " - Linear ticket integration" })] }), linear.authenticated ? (_jsxs(Box, { marginLeft: 2, flexDirection: "column", children: [_jsxs(Text, { dimColor: true, children: ["Organization: ", linear.orgName, " (", linear.orgSlug, ")"] }), _jsxs(Text, { dimColor: true, children: ["Token: ", linear.tokenValid ? "valid" : "expired"] }), _jsxs(Text, { dimColor: true, children: ["Repo linked: ", linear.repoLinked ? "yes" : "no"] }), linear.hint && _jsxs(Text, { color: "yellow", children: ["\u21B3 ", linear.hint] })] })) : (_jsx(Box, { marginLeft: 2, children: _jsxs(Text, { color: "yellow", children: ["\u21B3 ", linear.hint] }) }))] }));
287
263
  }
288
- function ShellRow({ configured, shell, }) {
289
- return (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsxs(Box, { children: [_jsx(StatusIcon, { ok: configured, required: true }), _jsx(Text, { children: " " }), _jsx(Text, { bold: true, children: "Shell Integration" }), _jsx(Text, { dimColor: true, children: " - Enables directory switching" })] }), configured ? (_jsx(Box, { marginLeft: 2, children: _jsxs(Text, { dimColor: true, children: ["Shell: ", shell] }) })) : (_jsx(Box, { marginLeft: 2, children: _jsxs(Text, { color: "yellow", children: ["\u21B3 Add to .", shell, "rc: eval \"$(santree shell-init ", shell, ")\""] }) }))] }));
264
+ function ShellRow({ configured, shell }) {
265
+ return (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsxs(Box, { children: [_jsx(StatusIcon, { ok: configured, required: true }), _jsx(Text, { children: " " }), _jsx(Text, { bold: true, children: "Shell Integration" }), _jsx(Text, { dimColor: true, children: " - Enables directory switching" })] }), configured ? (_jsx(Box, { marginLeft: 2, children: _jsxs(Text, { dimColor: true, children: ["Shell: ", shell] }) })) : (_jsx(Box, { marginLeft: 2, children: _jsxs(Text, { color: "yellow", children: ["\u21B3 Add to .", shell, "rc: eval \"$(santree helpers shell-init ", shell, ")\""] }) }))] }));
290
266
  }
291
267
  function StatuslineRow({ status }) {
292
268
  return (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsxs(Box, { children: [_jsx(StatusIcon, { ok: status.claudeSettingsConfigured, required: false }), _jsx(Text, { children: " " }), _jsx(Text, { bold: true, children: "Claude Statusline" }), _jsx(Text, { dimColor: true, children: " - Custom statusline in Claude Code" }), _jsx(Text, { dimColor: true, children: " (optional)" })] }), _jsxs(Box, { marginLeft: 2, flexDirection: "column", children: [status.currentCommand ? (_jsxs(Text, { dimColor: true, children: ["Command: ", status.currentCommand] })) : (_jsx(Text, { dimColor: true, children: "Command: not configured" })), status.hint && _jsxs(Text, { color: "yellow", children: ["\u21B3 ", status.hint] })] })] }));
@@ -304,11 +280,11 @@ function SantreeSetupRow({ status }) {
304
280
  ? status.initShExecutable
305
281
  ? "executable"
306
282
  : "not executable"
307
- : "missing"] })), _jsxs(Text, { dimColor: true, children: [".santree/worktrees ignored: ", status.worktreesIgnored ? "yes" : "no"] }), _jsxs(Text, { dimColor: true, children: [".santree/metadata.json ignored:", " ", status.metadataIgnored ? "yes" : "no"] }), status.hints.map((hint, i) => (_jsxs(Text, { color: "yellow", children: ["\u21B3 ", hint] }, i)))] })] }));
283
+ : "missing"] })), _jsxs(Text, { dimColor: true, children: [".santree/worktrees ignored: ", status.worktreesIgnored ? "yes" : "no"] }), _jsxs(Text, { dimColor: true, children: [".santree/metadata.json ignored: ", status.metadataIgnored ? "yes" : "no"] }), status.hints.map((hint, i) => (_jsxs(Text, { color: "yellow", children: ["\u21B3 ", hint] }, i)))] })] }));
308
284
  }
309
285
  export default function Doctor() {
310
286
  const [tools, setTools] = useState([]);
311
- const [mcp, setMcp] = useState(null);
287
+ const [linear, setLinear] = useState(null);
312
288
  const [shellStatus, setShellStatus] = useState(null);
313
289
  const [statusline, setStatusline] = useState(null);
314
290
  const [santreeSetup, setSantreeSetup] = useState(null);
@@ -342,10 +318,10 @@ export default function Doctor() {
342
318
  hint: "Install VSCode (https://code.visualstudio.com) or Cursor (https://cursor.sh)",
343
319
  });
344
320
  }
345
- const mcpResult = await checkLinearMcp();
321
+ const linearResult = await checkLinearAuth();
346
322
  const statuslineResult = await checkStatusline();
347
323
  setTools(results);
348
- setMcp(mcpResult);
324
+ setLinear(linearResult);
349
325
  setShellStatus(checkShellIntegration());
350
326
  setStatusline(statuslineResult);
351
327
  setSantreeSetup(checkSantreeSetup());
@@ -358,10 +334,7 @@ export default function Doctor() {
358
334
  }
359
335
  const requiredMissing = tools.filter((t) => t.required && (!t.installed || t.hint));
360
336
  const optionalMissing = tools.filter((t) => !t.required && !t.installed);
361
- const mcpOk = mcp?.configured &&
362
- (mcp?.status?.includes("✓") || mcp?.status?.includes("Connected"));
363
- const allRequired = requiredMissing.length === 0 && mcpOk && shellStatus?.configured;
364
- return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, color: "cyan", children: "Santree Doctor" }) }), _jsx(Box, { marginBottom: 1, flexDirection: "column", children: _jsx(Text, { bold: true, underline: true, children: "CLI Tools" }) }), tools.map((tool) => (_jsx(ToolRow, { tool: tool }, tool.name))), _jsx(Box, { marginBottom: 1, marginTop: 1, flexDirection: "column", children: _jsx(Text, { bold: true, underline: true, children: "Integrations" }) }), mcp && _jsx(McpRow, { mcp: mcp }), shellStatus && (_jsx(ShellRow, { configured: shellStatus.configured, shell: shellStatus.shell })), santreeSetup && _jsx(SantreeSetupRow, { status: santreeSetup }), _jsx(Box, { marginBottom: 1, marginTop: 1, flexDirection: "column", children: _jsx(Text, { bold: true, underline: true, children: "Aesthetics" }) }), statusline && _jsx(StatuslineRow, { status: statusline }), _jsx(Box, { marginTop: 1, borderStyle: "single", borderColor: allRequired ? "green" : "yellow", paddingX: 2, children: allRequired ? (_jsx(Text, { color: "green", children: "All requirements satisfied! Santree is ready to use." })) : (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: "yellow", children: [requiredMissing.length +
365
- (mcpOk ? 0 : 1) +
366
- (shellStatus?.configured ? 0 : 1), " ", "required item(s) need attention"] }), optionalMissing.length > 0 && (_jsxs(Text, { dimColor: true, children: [optionalMissing.length, " optional item(s) not installed"] }))] })) })] }));
337
+ const linearOk = linear?.authenticated && linear?.tokenValid && linear?.repoLinked;
338
+ const allRequired = requiredMissing.length === 0 && linearOk && shellStatus?.configured;
339
+ return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, color: "cyan", children: "Santree Doctor" }) }), _jsx(Box, { marginBottom: 1, flexDirection: "column", children: _jsx(Text, { bold: true, underline: true, children: "CLI Tools" }) }), tools.map((tool) => (_jsx(ToolRow, { tool: tool }, tool.name))), _jsx(Box, { marginBottom: 1, marginTop: 1, flexDirection: "column", children: _jsx(Text, { bold: true, underline: true, children: "Integrations" }) }), linear && _jsx(LinearRow, { linear: linear }), shellStatus && _jsx(ShellRow, { configured: shellStatus.configured, shell: shellStatus.shell }), santreeSetup && _jsx(SantreeSetupRow, { status: santreeSetup }), _jsx(Box, { marginBottom: 1, marginTop: 1, flexDirection: "column", children: _jsx(Text, { bold: true, underline: true, children: "Aesthetics" }) }), statusline && _jsx(StatuslineRow, { status: statusline }), _jsx(Box, { marginTop: 1, borderStyle: "single", borderColor: allRequired ? "green" : "yellow", paddingX: 2, children: allRequired ? (_jsx(Text, { color: "green", children: "All requirements satisfied! Santree is ready to use." })) : (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: "yellow", children: [requiredMissing.length + (linearOk ? 0 : 1) + (shellStatus?.configured ? 0 : 1), " ", "required item(s) need attention"] }), optionalMissing.length > 0 && (_jsxs(Text, { dimColor: true, children: [optionalMissing.length, " optional item(s) not installed"] }))] })) })] }));
367
340
  }
@@ -0,0 +1 @@
1
+ export declare const description = "Setup and integration helpers";
@@ -0,0 +1 @@
1
+ export const description = "Setup and integration helpers";
@@ -0,0 +1,122 @@
1
+ import { useEffect, useState, useRef } from "react";
2
+ import { argument } from "pastel";
3
+ import { z } from "zod/v4";
4
+ import { readdirSync, statSync, existsSync } from "fs";
5
+ import { fileURLToPath } from "url";
6
+ import { dirname, join } from "path";
7
+ import nunjucks from "nunjucks";
8
+ const __filename = fileURLToPath(import.meta.url);
9
+ const __dirname = dirname(__filename);
10
+ export const description = "Output shell integration script";
11
+ export const args = z.tuple([
12
+ z
13
+ .enum(["zsh", "bash"])
14
+ .default("zsh")
15
+ .describe(argument({ name: "shell", description: "Shell type (zsh or bash)" })),
16
+ ]);
17
+ const ARG_COMPLETIONS = {};
18
+ function extractOptions(mod) {
19
+ const options = [];
20
+ if (mod.options) {
21
+ const shape = mod.options.shape ?? {};
22
+ for (const [key, value] of Object.entries(shape)) {
23
+ const schema = value;
24
+ const desc = schema.description ?? schema._zod?.def?.description ?? "";
25
+ options.push({
26
+ name: key,
27
+ description: desc,
28
+ completion: key === "base" ? "all_branches" : null,
29
+ });
30
+ }
31
+ }
32
+ return options;
33
+ }
34
+ /**
35
+ * Dynamically loads command modules and extracts their metadata.
36
+ * Handles both top-level files and directory-based command groups.
37
+ */
38
+ async function getCommands() {
39
+ const commandsDir = join(__dirname, "..");
40
+ const entries = readdirSync(commandsDir);
41
+ const commands = [];
42
+ for (const entry of entries) {
43
+ const entryPath = join(commandsDir, entry);
44
+ const stat = statSync(entryPath);
45
+ if (stat.isDirectory()) {
46
+ // Directory-based command group — complete arg to subcommand names
47
+ const subFiles = readdirSync(entryPath).filter((f) => f.endsWith(".js") && f !== "index.js");
48
+ const subNames = subFiles.map((f) => f.replace(".js", ""));
49
+ if (subNames.length === 0)
50
+ continue;
51
+ let description = "";
52
+ const indexPath = join(entryPath, "index.js");
53
+ if (existsSync(indexPath)) {
54
+ try {
55
+ const indexMod = await import(indexPath);
56
+ description = indexMod.description ?? "";
57
+ }
58
+ catch {
59
+ // no description
60
+ }
61
+ }
62
+ commands.push({
63
+ name: entry,
64
+ funcName: entry.replace(/-/g, "_"),
65
+ description,
66
+ hasArgs: true,
67
+ argCompletion: "static",
68
+ argCompletionValues: subNames.join(" "),
69
+ options: [],
70
+ });
71
+ continue;
72
+ }
73
+ if (!entry.endsWith(".js"))
74
+ continue;
75
+ const name = entry.replace(".js", "");
76
+ try {
77
+ const mod = await import(join(commandsDir, entry));
78
+ if (!mod.description)
79
+ continue;
80
+ commands.push({
81
+ name,
82
+ funcName: name.replace(/-/g, "_"),
83
+ description: mod.description,
84
+ hasArgs: !!mod.args,
85
+ argCompletion: ARG_COMPLETIONS[name] || null,
86
+ argCompletionValues: "",
87
+ options: extractOptions(mod),
88
+ });
89
+ }
90
+ catch {
91
+ // Skip files that can't be imported
92
+ }
93
+ }
94
+ return commands.sort((a, b) => a.name.localeCompare(b.name));
95
+ }
96
+ // Configure nunjucks
97
+ const templatesDir = join(__dirname, "..", "..", "..", "shell");
98
+ nunjucks.configure(templatesDir, { autoescape: false });
99
+ export default function ShellInit({ args }) {
100
+ const [shell] = args;
101
+ const [done, setDone] = useState(false);
102
+ const hasOutputRef = useRef(false);
103
+ useEffect(() => {
104
+ async function run() {
105
+ if (hasOutputRef.current)
106
+ return;
107
+ hasOutputRef.current = true;
108
+ const commands = await getCommands();
109
+ const templateFile = `init.${shell}.njk`;
110
+ const output = nunjucks.render(templateFile, { commands });
111
+ process.stdout.write(output);
112
+ setDone(true);
113
+ }
114
+ run();
115
+ }, [shell]);
116
+ useEffect(() => {
117
+ if (done) {
118
+ process.exit(0);
119
+ }
120
+ }, [done]);
121
+ return null;
122
+ }
@@ -0,0 +1,12 @@
1
+ import { z } from "zod";
2
+ export declare const description = "Authenticate with Linear";
3
+ export declare const options: z.ZodObject<{
4
+ logout: z.ZodOptional<z.ZodBoolean>;
5
+ status: z.ZodOptional<z.ZodBoolean>;
6
+ test: z.ZodOptional<z.ZodString>;
7
+ }, z.core.$strip>;
8
+ type Props = {
9
+ options: z.infer<typeof options>;
10
+ };
11
+ export default function LinearAuth({ options }: Props): import("react/jsx-runtime").JSX.Element;
12
+ export {};