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.
- package/README.md +177 -36
- package/dist/commands/doctor.js +37 -64
- package/dist/commands/helpers/index.d.ts +1 -0
- package/dist/commands/helpers/index.js +1 -0
- package/dist/commands/helpers/shell-init.js +122 -0
- package/dist/commands/linear/auth.d.ts +12 -0
- package/dist/commands/linear/auth.js +173 -0
- package/dist/commands/linear/index.d.ts +1 -0
- package/dist/commands/linear/index.js +1 -0
- package/dist/commands/linear/open.d.ts +2 -0
- package/dist/commands/linear/open.js +65 -0
- package/dist/commands/{pr.js → pr/create.js} +3 -3
- package/dist/commands/pr/fix.d.ts +2 -0
- package/dist/commands/pr/fix.js +48 -0
- package/dist/commands/pr/index.d.ts +1 -0
- package/dist/commands/pr/index.js +1 -0
- package/dist/commands/pr/open.d.ts +2 -0
- package/dist/commands/pr/open.js +52 -0
- package/dist/commands/pr/review.d.ts +2 -0
- package/dist/commands/pr/review.js +41 -0
- package/dist/commands/{clean.js → worktree/clean.js} +3 -4
- package/dist/commands/{commit.js → worktree/commit.js} +4 -8
- package/dist/commands/{create.js → worktree/create.js} +4 -4
- package/dist/commands/worktree/index.d.ts +1 -0
- package/dist/commands/worktree/index.js +1 -0
- package/dist/commands/{list.js → worktree/list.js} +2 -2
- package/dist/commands/{editor.d.ts → worktree/open.d.ts} +1 -1
- package/dist/commands/{editor.js → worktree/open.js} +4 -9
- package/dist/commands/{remove.js → worktree/remove.js} +1 -1
- package/dist/commands/{setup.js → worktree/setup.js} +2 -2
- package/dist/commands/{switch.js → worktree/switch.js} +1 -1
- package/dist/commands/{sync.js → worktree/sync.js} +3 -4
- package/dist/commands/{work.d.ts → worktree/work.d.ts} +0 -2
- package/dist/commands/worktree/work.js +60 -0
- package/dist/lib/ai.d.ts +39 -0
- package/dist/lib/ai.js +63 -0
- package/dist/lib/git.d.ts +19 -0
- package/dist/lib/git.js +26 -4
- package/dist/lib/github.js +1 -1
- package/dist/lib/linear.d.ts +63 -0
- package/dist/lib/linear.js +425 -0
- package/dist/lib/prompts.d.ts +6 -1
- package/dist/lib/prompts.js +26 -1
- package/package.json +15 -2
- package/prompts/fill-pr.njk +3 -0
- package/prompts/fix-pr.njk +5 -1
- package/prompts/implement.njk +5 -1
- package/prompts/plan.njk +5 -1
- package/prompts/review.njk +5 -1
- package/prompts/ticket.njk +24 -0
- package/shell/init.bash.njk +12 -9
- package/shell/init.zsh.njk +11 -8
- package/dist/commands/shell-init.js +0 -91
- package/dist/commands/work.js +0 -113
- /package/dist/commands/{shell-init.d.ts → helpers/shell-init.d.ts} +0 -0
- /package/dist/commands/{statusline.d.ts → helpers/statusline.d.ts} +0 -0
- /package/dist/commands/{statusline.js → helpers/statusline.js} +0 -0
- /package/dist/commands/{pr.d.ts → pr/create.d.ts} +0 -0
- /package/dist/commands/{clean.d.ts → worktree/clean.d.ts} +0 -0
- /package/dist/commands/{commit.d.ts → worktree/commit.d.ts} +0 -0
- /package/dist/commands/{create.d.ts → worktree/create.d.ts} +0 -0
- /package/dist/commands/{list.d.ts → worktree/list.d.ts} +0 -0
- /package/dist/commands/{remove.d.ts → worktree/remove.d.ts} +0 -0
- /package/dist/commands/{setup.d.ts → worktree/setup.d.ts} +0 -0
- /package/dist/commands/{switch.d.ts → worktree/switch.d.ts} +0 -0
- /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` -
|
|
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-
|
|
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
|
|
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
|
|
102
|
-
-
|
|
103
|
-
-
|
|
104
|
-
-
|
|
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
|
|
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
|
-
|
|
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
|
-
###
|
|
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
|
-
###
|
|
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
|
-
###
|
|
256
|
+
### linear auth
|
|
191
257
|
| Option | Description |
|
|
192
258
|
|--------|-------------|
|
|
193
|
-
| `--
|
|
194
|
-
| `--
|
|
195
|
-
| `--
|
|
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
|
+
```
|
package/dist/commands/doctor.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
112
|
-
const
|
|
113
|
-
|
|
109
|
+
async function checkLinearAuth() {
|
|
110
|
+
const repoRoot = findMainRepoRoot();
|
|
111
|
+
const status = getAuthStatus(repoRoot);
|
|
112
|
+
if (!status.authenticated || !status.orgSlug) {
|
|
114
113
|
return {
|
|
115
|
-
|
|
116
|
-
|
|
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
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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
|
|
284
|
-
const isOk =
|
|
285
|
-
|
|
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:
|
|
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 [
|
|
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
|
|
321
|
+
const linearResult = await checkLinearAuth();
|
|
346
322
|
const statuslineResult = await checkStatusline();
|
|
347
323
|
setTools(results);
|
|
348
|
-
|
|
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
|
|
362
|
-
|
|
363
|
-
|
|
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 {};
|