santree 0.2.1 → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +82 -60
- package/dist/commands/dashboard.js +140 -12
- package/dist/commands/doctor.js +34 -2
- package/dist/commands/helpers/template.d.ts +1 -1
- package/dist/commands/pr/fix.js +2 -2
- package/dist/commands/pr/review.js +2 -2
- package/dist/commands/worktree/work.js +2 -2
- package/dist/lib/ai.d.ts +6 -8
- package/dist/lib/ai.js +14 -19
- package/dist/lib/dashboard/Overlays.d.ts +4 -1
- package/dist/lib/dashboard/Overlays.js +5 -2
- package/dist/lib/dashboard/types.d.ts +7 -1
- package/dist/lib/dashboard/types.js +19 -1
- package/dist/lib/exec.d.ts +1 -0
- package/dist/lib/exec.js +5 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -39,6 +39,7 @@ eval "$(santree helpers shell-init bash)" # for bash
|
|
|
39
39
|
This enables automatic directory switching after `worktree create` and `worktree switch` commands.
|
|
40
40
|
|
|
41
41
|
The shell integration also provides:
|
|
42
|
+
|
|
42
43
|
- `st` - Alias for `santree`
|
|
43
44
|
- `stw` - Alias for `santree worktree` (e.g., `stw list`, `stw create`)
|
|
44
45
|
- `stn` - Quick create worktree with `--work --plan --tmux` (prompts for branch name)
|
|
@@ -88,55 +89,56 @@ With the `stw` alias: `stw create`, `stw list`, `stw switch`, `stw work`, `stw c
|
|
|
88
89
|
|
|
89
90
|
### Worktree (`santree worktree`)
|
|
90
91
|
|
|
91
|
-
| Command
|
|
92
|
-
|
|
93
|
-
| `santree worktree create <branch>` | Create a new worktree from base branch
|
|
94
|
-
| `santree worktree list`
|
|
95
|
-
| `santree worktree switch <branch>` | Switch to another worktree
|
|
96
|
-
| `santree worktree remove <branch>` | Remove a worktree and its branch
|
|
97
|
-
| `santree worktree clean`
|
|
98
|
-
| `santree worktree sync`
|
|
99
|
-
| `santree worktree work`
|
|
100
|
-
| `santree worktree open`
|
|
101
|
-
| `santree worktree setup`
|
|
102
|
-
| `santree worktree commit`
|
|
92
|
+
| Command | Description |
|
|
93
|
+
| ---------------------------------- | --------------------------------------------------- |
|
|
94
|
+
| `santree worktree create <branch>` | Create a new worktree from base branch |
|
|
95
|
+
| `santree worktree list` | List all worktrees with PR status and commits ahead |
|
|
96
|
+
| `santree worktree switch <branch>` | Switch to another worktree |
|
|
97
|
+
| `santree worktree remove <branch>` | Remove a worktree and its branch |
|
|
98
|
+
| `santree worktree clean` | Remove worktrees with merged/closed PRs |
|
|
99
|
+
| `santree worktree sync` | Sync current worktree with base branch |
|
|
100
|
+
| `santree worktree work` | Launch Claude AI to work on the current ticket |
|
|
101
|
+
| `santree worktree open` | Open workspace in VSCode or Cursor |
|
|
102
|
+
| `santree worktree setup` | Run the init script (`.santree/init.sh`) |
|
|
103
|
+
| `santree worktree commit` | Stage and commit changes |
|
|
103
104
|
|
|
104
105
|
### Pull Requests (`santree pr`)
|
|
105
106
|
|
|
106
|
-
| Command
|
|
107
|
-
|
|
108
|
-
| `santree pr create` | Create a GitHub pull request
|
|
109
|
-
| `santree pr open`
|
|
110
|
-
| `santree pr fix`
|
|
107
|
+
| Command | Description |
|
|
108
|
+
| ------------------- | ------------------------------------- |
|
|
109
|
+
| `santree pr create` | Create a GitHub pull request |
|
|
110
|
+
| `santree pr open` | Open the current PR in the browser |
|
|
111
|
+
| `santree pr fix` | Fix PR review comments with AI |
|
|
111
112
|
| `santree pr review` | Review changes against ticket with AI |
|
|
112
113
|
|
|
113
114
|
### Linear (`santree linear`)
|
|
114
115
|
|
|
115
|
-
| Command
|
|
116
|
-
|
|
117
|
-
| `santree linear auth`
|
|
118
|
-
| `santree linear switch` | Switch Linear workspace for this repo
|
|
119
|
-
| `santree linear open`
|
|
116
|
+
| Command | Description |
|
|
117
|
+
| ----------------------- | --------------------------------------------- |
|
|
118
|
+
| `santree linear auth` | Authenticate with Linear (OAuth) |
|
|
119
|
+
| `santree linear switch` | Switch Linear workspace for this repo |
|
|
120
|
+
| `santree linear open` | Open the current Linear ticket in the browser |
|
|
120
121
|
|
|
121
122
|
### Helpers (`santree helpers`)
|
|
122
123
|
|
|
123
|
-
| Command
|
|
124
|
-
|
|
125
|
-
| `santree helpers shell-init` | Output shell integration script
|
|
124
|
+
| Command | Description |
|
|
125
|
+
| ---------------------------- | --------------------------------- |
|
|
126
|
+
| `santree helpers shell-init` | Output shell integration script |
|
|
126
127
|
| `santree helpers statusline` | Custom statusline for Claude Code |
|
|
127
128
|
|
|
128
129
|
### Top-level
|
|
129
130
|
|
|
130
|
-
| Command
|
|
131
|
-
|
|
131
|
+
| Command | Description |
|
|
132
|
+
| ------------------- | ----------------------------------------------- |
|
|
132
133
|
| `santree dashboard` | Interactive dashboard of all your Linear issues |
|
|
133
|
-
| `santree doctor`
|
|
134
|
+
| `santree doctor` | Check system requirements and integrations |
|
|
134
135
|
|
|
135
136
|
---
|
|
136
137
|
|
|
137
138
|
## Features
|
|
138
139
|
|
|
139
140
|
### Interactive Dashboard
|
|
141
|
+
|
|
140
142
|
`santree dashboard` opens a full-screen TUI to manage all your work in one place. It shows your Linear issues grouped by project, with live status for worktrees, PRs, CI checks, and reviews.
|
|
141
143
|
|
|
142
144
|
**Left pane** — issue list with columns for priority, session ID, PR number, and CI status. Click to select, scroll wheel to navigate, drag the divider to resize panes.
|
|
@@ -158,22 +160,28 @@ With the `stw` alias: `stw create`, `stw list`, `stw switch`, `stw work`, `stw c
|
|
|
158
160
|
Commit and PR creation happen inline without leaving the dashboard. Work, fix, and review open in new tmux windows.
|
|
159
161
|
|
|
160
162
|
### Worktree Management
|
|
163
|
+
|
|
161
164
|
Create isolated worktrees for each feature branch. No more stashing or committing WIP code just to switch tasks.
|
|
162
165
|
|
|
163
166
|
### GitHub Integration
|
|
167
|
+
|
|
164
168
|
See PR status directly in your worktree list. Clean up worktrees automatically when PRs are merged or closed.
|
|
165
169
|
|
|
166
170
|
### Linear Integration
|
|
171
|
+
|
|
167
172
|
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.
|
|
168
173
|
|
|
169
174
|
### Claude AI Integration
|
|
175
|
+
|
|
170
176
|
Launch Claude with full context about your current ticket. Supports different modes:
|
|
177
|
+
|
|
171
178
|
- `santree worktree work` - Implement the ticket
|
|
172
179
|
- `santree worktree work --plan` - Create an implementation plan only
|
|
173
180
|
- `santree pr review` - Review changes against ticket requirements
|
|
174
181
|
- `santree pr fix` - Address PR review comments
|
|
175
182
|
|
|
176
183
|
### Init Scripts
|
|
184
|
+
|
|
177
185
|
Run custom setup scripts when creating worktrees. Perfect for copying `.env` files, installing dependencies, or any project-specific setup.
|
|
178
186
|
|
|
179
187
|
---
|
|
@@ -226,81 +234,95 @@ If you have multiple workspaces authenticated, running `santree linear auth` in
|
|
|
226
234
|
Santree provides a custom statusline for Claude Code showing git info, model, context usage, and cost.
|
|
227
235
|
|
|
228
236
|
Add to `~/.claude/settings.json`:
|
|
237
|
+
|
|
229
238
|
```json
|
|
230
239
|
{
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
240
|
+
"statusLine": {
|
|
241
|
+
"type": "command",
|
|
242
|
+
"command": "santree helpers statusline"
|
|
243
|
+
}
|
|
235
244
|
}
|
|
236
245
|
```
|
|
237
246
|
|
|
238
247
|
The statusline displays: `repo | branch | S: staged | U: unstaged | A: untracked | Model | Context% | $Cost`
|
|
239
248
|
|
|
249
|
+
### Claude Code Remote Control (Optional)
|
|
250
|
+
|
|
251
|
+
Enable [Remote Control](https://code.claude.com/docs/en/remote-control) to continue local Claude Code sessions from your phone, tablet, or any browser. This lets you kick off work with `santree worktree work` and monitor or steer the session remotely.
|
|
252
|
+
|
|
253
|
+
Enable it for all sessions by running `/config` inside Claude Code and setting **Enable Remote Control for all sessions** to `true`. This writes `remoteControlAtStartup: true` to `~/.claude.json`. Run `santree doctor` to verify.
|
|
254
|
+
|
|
240
255
|
---
|
|
241
256
|
|
|
242
257
|
## Command Options
|
|
243
258
|
|
|
244
259
|
### worktree create
|
|
245
|
-
|
|
246
|
-
|
|
260
|
+
|
|
261
|
+
| Option | Description |
|
|
262
|
+
| ----------------- | ------------------------------------------------- |
|
|
247
263
|
| `--base <branch>` | Base branch to create from (default: main/master) |
|
|
248
|
-
| `--work`
|
|
249
|
-
| `--plan`
|
|
250
|
-
| `--no-pull`
|
|
251
|
-
| `--tmux`
|
|
252
|
-
| `--name <name>`
|
|
264
|
+
| `--work` | Launch Claude after creating |
|
|
265
|
+
| `--plan` | With --work, only create implementation plan |
|
|
266
|
+
| `--no-pull` | Skip pulling latest changes |
|
|
267
|
+
| `--tmux` | Open worktree in new tmux window |
|
|
268
|
+
| `--name <name>` | Custom tmux window name |
|
|
253
269
|
|
|
254
270
|
### worktree sync
|
|
255
|
-
|
|
256
|
-
|
|
271
|
+
|
|
272
|
+
| Option | Description |
|
|
273
|
+
| ---------- | --------------------------- |
|
|
257
274
|
| `--rebase` | Use rebase instead of merge |
|
|
258
275
|
|
|
259
276
|
### worktree remove
|
|
277
|
+
|
|
260
278
|
Removes the worktree and deletes the branch. Uses force mode by default (removes even with uncommitted changes).
|
|
261
279
|
|
|
262
280
|
### worktree clean
|
|
281
|
+
|
|
263
282
|
Shows worktrees with merged/closed PRs and prompts for confirmation before removing.
|
|
264
283
|
|
|
265
284
|
### worktree open
|
|
266
|
-
|
|
267
|
-
|
|
285
|
+
|
|
286
|
+
| Option | Description |
|
|
287
|
+
| ---------------- | --------------------------------------------------------------------------------------- |
|
|
268
288
|
| `--editor <cmd>` | Editor command to use (default: `code`). Also configurable via `SANTREE_EDITOR` env var |
|
|
269
289
|
|
|
270
290
|
### worktree work
|
|
271
|
-
|
|
272
|
-
|
|
291
|
+
|
|
292
|
+
| Option | Description |
|
|
293
|
+
| -------- | ------------------------------- |
|
|
273
294
|
| `--plan` | Only create implementation plan |
|
|
274
295
|
|
|
275
296
|
Automatically fetches Linear ticket data if authenticated. Degrades gracefully if not.
|
|
276
297
|
|
|
277
298
|
### pr create
|
|
278
|
-
|
|
279
|
-
|
|
299
|
+
|
|
300
|
+
| Option | Description |
|
|
301
|
+
| -------- | --------------------------------------------- |
|
|
280
302
|
| `--fill` | Use AI to fill the PR template before opening |
|
|
281
303
|
|
|
282
304
|
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.
|
|
283
305
|
|
|
284
306
|
### linear auth
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
|
307
|
+
|
|
308
|
+
| Option | Description |
|
|
309
|
+
| ------------- | ------------------------------------------------ |
|
|
310
|
+
| `--status` | Show current auth status (org, token expiry) |
|
|
288
311
|
| `--test <id>` | Fetch a ticket by ID to verify integration works |
|
|
289
|
-
| `--logout`
|
|
312
|
+
| `--logout` | Revoke tokens and log out |
|
|
290
313
|
|
|
291
314
|
---
|
|
292
315
|
|
|
293
316
|
## Requirements
|
|
294
317
|
|
|
295
|
-
| Tool
|
|
296
|
-
|
|
297
|
-
| Node.js >= 20
|
|
298
|
-
| Git
|
|
299
|
-
| GitHub CLI (`gh`)
|
|
300
|
-
| Claude Code (`claude`)
|
|
301
|
-
|
|
|
302
|
-
|
|
|
303
|
-
| VSCode (`code`) or Cursor (`cursor`) | Optional: workspace editor |
|
|
318
|
+
| Tool | Purpose |
|
|
319
|
+
| ------------------------------------ | ------------------------------------ |
|
|
320
|
+
| Node.js >= 20 | Runtime |
|
|
321
|
+
| Git | Worktree operations |
|
|
322
|
+
| GitHub CLI (`gh`) | PR integration |
|
|
323
|
+
| Claude Code (`claude`) | AI agent for `work`, `fix`, `review` |
|
|
324
|
+
| tmux | Optional: new window support |
|
|
325
|
+
| VSCode (`code`) or Cursor (`cursor`) | Optional: workspace editor |
|
|
304
326
|
|
|
305
327
|
---
|
|
306
328
|
|
|
@@ -10,8 +10,12 @@ import * as path from "path";
|
|
|
10
10
|
const require = createRequire(import.meta.url);
|
|
11
11
|
const { version } = require("../../package.json");
|
|
12
12
|
import { findMainRepoRoot, createWorktree, getDefaultBranch, getBaseBranch, hasInitScript, getInitScriptPath, removeWorktree, } from "../lib/git.js";
|
|
13
|
-
import { spawnAsync } from "../lib/exec.js";
|
|
13
|
+
import { run, spawnAsync } from "../lib/exec.js";
|
|
14
14
|
import { resolveAgentBinary } from "../lib/ai.js";
|
|
15
|
+
import { extractTicketId } from "../lib/git.js";
|
|
16
|
+
import { getPRTemplate } from "../lib/github.js";
|
|
17
|
+
import { renderPrompt, renderDiff } from "../lib/prompts.js";
|
|
18
|
+
import * as os from "os";
|
|
15
19
|
import { initialState, reducer } from "../lib/dashboard/types.js";
|
|
16
20
|
import { loadDashboardData } from "../lib/dashboard/data.js";
|
|
17
21
|
import IssueList from "../lib/dashboard/IssueList.js";
|
|
@@ -563,29 +567,110 @@ export default function Dashboard() {
|
|
|
563
567
|
if (!s.prCreateWorktreePath || !s.prCreateBranch)
|
|
564
568
|
return;
|
|
565
569
|
const base = getBaseBranch(s.prCreateBranch);
|
|
570
|
+
const cwd = s.prCreateWorktreePath;
|
|
566
571
|
// Push first
|
|
567
572
|
dispatch({ type: "PR_CREATE_PHASE", phase: "pushing" });
|
|
568
573
|
try {
|
|
569
|
-
await execAsync(`git -C "${
|
|
574
|
+
await execAsync(`git -C "${cwd}" push -u origin "${s.prCreateBranch}"`);
|
|
570
575
|
}
|
|
571
576
|
catch (e) {
|
|
572
577
|
const msg = e?.stderr?.trim() || e?.message || "Push failed";
|
|
573
578
|
dispatch({ type: "PR_CREATE_ERROR", error: msg });
|
|
574
579
|
return;
|
|
575
580
|
}
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
const url = stdout.trim();
|
|
581
|
-
dispatch({ type: "PR_CREATE_DONE", url });
|
|
582
|
-
}
|
|
583
|
-
else {
|
|
581
|
+
if (!fill) {
|
|
582
|
+
// Web mode — open in browser directly
|
|
583
|
+
try {
|
|
584
|
+
dispatch({ type: "PR_CREATE_PHASE", phase: "creating" });
|
|
584
585
|
await execAsync(`gh pr create --web --base "${base}" --head "${s.prCreateBranch}"`, {
|
|
585
|
-
cwd
|
|
586
|
+
cwd,
|
|
586
587
|
});
|
|
587
588
|
dispatch({ type: "PR_CREATE_DONE", url: "" });
|
|
589
|
+
setTimeout(() => {
|
|
590
|
+
dispatch({ type: "PR_CREATE_CANCEL" });
|
|
591
|
+
refresh();
|
|
592
|
+
}, 2500);
|
|
593
|
+
}
|
|
594
|
+
catch (e) {
|
|
595
|
+
const msg = e?.stderr?.trim() || e?.message || "PR creation failed";
|
|
596
|
+
dispatch({ type: "PR_CREATE_ERROR", error: msg });
|
|
597
|
+
}
|
|
598
|
+
return;
|
|
599
|
+
}
|
|
600
|
+
// Fill mode — use AI to generate body, then review
|
|
601
|
+
try {
|
|
602
|
+
const prTemplate = getPRTemplate();
|
|
603
|
+
if (!prTemplate) {
|
|
604
|
+
dispatch({
|
|
605
|
+
type: "PR_CREATE_ERROR",
|
|
606
|
+
error: "No PR template found at .github/pull_request_template.md",
|
|
607
|
+
});
|
|
608
|
+
return;
|
|
609
|
+
}
|
|
610
|
+
const bin = resolveAgentBinary();
|
|
611
|
+
if (!bin) {
|
|
612
|
+
dispatch({
|
|
613
|
+
type: "PR_CREATE_ERROR",
|
|
614
|
+
error: "Claude CLI not found (npm i -g @anthropic-ai/claude-code)",
|
|
615
|
+
});
|
|
616
|
+
return;
|
|
588
617
|
}
|
|
618
|
+
dispatch({ type: "PR_CREATE_PHASE", phase: "filling" });
|
|
619
|
+
const ticketId = extractTicketId(s.prCreateBranch) ?? "";
|
|
620
|
+
const commitLog = run(`git log ${base}..HEAD --format="- %s"`, { cwd }) || null;
|
|
621
|
+
const diffStat = run(`git diff ${base}..HEAD --stat`, { cwd }) || null;
|
|
622
|
+
const diff = run(`git diff ${base}..HEAD`, { cwd, maxBuffer: 10 * 1024 * 1024 }) || null;
|
|
623
|
+
const diffContent = renderDiff({
|
|
624
|
+
base_branch: base,
|
|
625
|
+
commit_log: commitLog,
|
|
626
|
+
diff_stat: diffStat,
|
|
627
|
+
diff: diff,
|
|
628
|
+
});
|
|
629
|
+
const prompt = renderPrompt("fill-pr", {
|
|
630
|
+
pr_template: prTemplate,
|
|
631
|
+
diff_content: diffContent,
|
|
632
|
+
ticket_id: ticketId,
|
|
633
|
+
branch_name: s.prCreateBranch,
|
|
634
|
+
});
|
|
635
|
+
// Pass prompt via stdin instead of temp file
|
|
636
|
+
const agentResult = await spawnAsync(bin, ["-p", "--output-format", "text"], {
|
|
637
|
+
stdin: prompt,
|
|
638
|
+
});
|
|
639
|
+
const body = agentResult.output.trim();
|
|
640
|
+
if (agentResult.code !== 0 || !body || body.toLowerCase().startsWith("error")) {
|
|
641
|
+
dispatch({
|
|
642
|
+
type: "PR_CREATE_ERROR",
|
|
643
|
+
error: body || "Failed to generate PR body with AI",
|
|
644
|
+
});
|
|
645
|
+
return;
|
|
646
|
+
}
|
|
647
|
+
// Get title from first commit
|
|
648
|
+
const title = run(`git log ${base}..HEAD --reverse --format=%s`, { cwd })?.split("\n")[0] ??
|
|
649
|
+
s.prCreateBranch;
|
|
650
|
+
// Show review instead of creating immediately
|
|
651
|
+
dispatch({ type: "PR_CREATE_REVIEW", body, title });
|
|
652
|
+
}
|
|
653
|
+
catch (e) {
|
|
654
|
+
const msg = e?.stderr?.trim() || e?.message || "PR creation failed";
|
|
655
|
+
dispatch({ type: "PR_CREATE_ERROR", error: msg });
|
|
656
|
+
}
|
|
657
|
+
}, [refresh]);
|
|
658
|
+
const confirmPrCreate = useCallback(async () => {
|
|
659
|
+
const s = stateRef.current;
|
|
660
|
+
if (!s.prCreateWorktreePath || !s.prCreateBranch || !s.prCreateBody || !s.prCreateTitle)
|
|
661
|
+
return;
|
|
662
|
+
const base = getBaseBranch(s.prCreateBranch);
|
|
663
|
+
const cwd = s.prCreateWorktreePath;
|
|
664
|
+
dispatch({ type: "PR_CREATE_PHASE", phase: "creating" });
|
|
665
|
+
try {
|
|
666
|
+
const bodyFile = path.join(os.tmpdir(), `santree-pr-${Date.now()}.md`);
|
|
667
|
+
fs.writeFileSync(bodyFile, s.prCreateBody);
|
|
668
|
+
const { stdout } = await execAsync(`gh pr create --title "${s.prCreateTitle.replace(/"/g, '\\"')}" --base "${base}" --head "${s.prCreateBranch}" --body-file "${bodyFile}"`, { cwd });
|
|
669
|
+
try {
|
|
670
|
+
fs.unlinkSync(bodyFile);
|
|
671
|
+
}
|
|
672
|
+
catch { }
|
|
673
|
+
dispatch({ type: "PR_CREATE_DONE", url: stdout.trim() });
|
|
589
674
|
setTimeout(() => {
|
|
590
675
|
dispatch({ type: "PR_CREATE_CANCEL" });
|
|
591
676
|
refresh();
|
|
@@ -596,6 +681,25 @@ export default function Dashboard() {
|
|
|
596
681
|
dispatch({ type: "PR_CREATE_ERROR", error: msg });
|
|
597
682
|
}
|
|
598
683
|
}, [refresh]);
|
|
684
|
+
const openPrInWeb = useCallback(async () => {
|
|
685
|
+
const s = stateRef.current;
|
|
686
|
+
if (!s.prCreateWorktreePath || !s.prCreateBranch)
|
|
687
|
+
return;
|
|
688
|
+
const base = getBaseBranch(s.prCreateBranch);
|
|
689
|
+
const cwd = s.prCreateWorktreePath;
|
|
690
|
+
try {
|
|
691
|
+
await execAsync(`gh pr create --web --base "${base}" --head "${s.prCreateBranch}"`, { cwd });
|
|
692
|
+
dispatch({ type: "PR_CREATE_DONE", url: "" });
|
|
693
|
+
setTimeout(() => {
|
|
694
|
+
dispatch({ type: "PR_CREATE_CANCEL" });
|
|
695
|
+
refresh();
|
|
696
|
+
}, 2500);
|
|
697
|
+
}
|
|
698
|
+
catch (e) {
|
|
699
|
+
const msg = e?.stderr?.trim() || e?.message || "Failed to open in browser";
|
|
700
|
+
dispatch({ type: "PR_CREATE_ERROR", error: msg });
|
|
701
|
+
}
|
|
702
|
+
}, [refresh]);
|
|
599
703
|
// ── Keyboard ──────────────────────────────────────────────────────
|
|
600
704
|
useInput((input, key) => {
|
|
601
705
|
// Clear action messages on any keypress
|
|
@@ -639,6 +743,30 @@ export default function Dashboard() {
|
|
|
639
743
|
return;
|
|
640
744
|
}
|
|
641
745
|
}
|
|
746
|
+
if (state.prCreatePhase === "review") {
|
|
747
|
+
if (input === "y" || key.return) {
|
|
748
|
+
confirmPrCreate();
|
|
749
|
+
return;
|
|
750
|
+
}
|
|
751
|
+
if (input === "w") {
|
|
752
|
+
openPrInWeb();
|
|
753
|
+
return;
|
|
754
|
+
}
|
|
755
|
+
if (key.shift && key.downArrow) {
|
|
756
|
+
dispatch({ type: "SCROLL_DETAIL", offset: state.detailScrollOffset + 3 });
|
|
757
|
+
return;
|
|
758
|
+
}
|
|
759
|
+
if (key.shift && key.upArrow) {
|
|
760
|
+
dispatch({ type: "SCROLL_DETAIL", offset: Math.max(0, state.detailScrollOffset - 3) });
|
|
761
|
+
return;
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
if (state.prCreatePhase === "error") {
|
|
765
|
+
if (input === "w") {
|
|
766
|
+
openPrInWeb();
|
|
767
|
+
return;
|
|
768
|
+
}
|
|
769
|
+
}
|
|
642
770
|
return;
|
|
643
771
|
}
|
|
644
772
|
// Confirm setup overlay
|
|
@@ -941,5 +1069,5 @@ export default function Dashboard() {
|
|
|
941
1069
|
return (_jsx(Box, { width: columns, height: rows, flexDirection: "column", children: _jsxs(Box, { justifyContent: "center", alignItems: "center", flexGrow: 1, flexDirection: "column", children: [_jsx(Text, { color: "yellow", children: "No active issues assigned to you" }), _jsx(Text, { dimColor: true, children: "Press R to refresh or q to quit" })] }) }));
|
|
942
1070
|
}
|
|
943
1071
|
const selectedIssue = state.flatIssues[state.selectedIndex] ?? null;
|
|
944
|
-
return (_jsxs(Box, { width: columns, height: rows, flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { bold: true, color: "cyan", children: "Santree Dashboard" }), _jsxs(Text, { dimColor: true, children: [" v", version] }), _jsxs(Text, { dimColor: true, children: [" ", "(", state.flatIssues.length, " issues)", state.refreshing ? " refreshing..." : ""] }), state.actionMessage && (_jsxs(Text, { color: "yellow", children: [" ", state.actionMessage] }))] }), state.overlay === "mode-select" ? (_jsx(Box, { flexGrow: 1, justifyContent: "center", alignItems: "center", children: _jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 3, paddingY: 1, children: [_jsx(Text, { bold: true, children: "Select mode:" }), _jsx(Text, { children: " " }), _jsxs(Text, { children: [_jsx(Text, { color: "cyan", bold: true, children: "p" }), " Plan"] }), _jsxs(Text, { children: [_jsx(Text, { color: "cyan", bold: true, children: "i" }), " Implement"] }), _jsx(Text, { children: " " }), _jsx(Text, { dimColor: true, children: "ESC to cancel" })] }) })) : state.overlay === "confirm-delete" ? (_jsx(Box, { flexGrow: 1, justifyContent: "center", alignItems: "center", children: _jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "red", paddingX: 3, paddingY: 1, children: [_jsx(Text, { bold: true, color: "red", children: "Remove worktree?" }), _jsx(Text, { children: " " }), _jsx(Text, { children: selectedIssue?.worktree?.branch ?? "" }), selectedIssue?.worktree?.dirty && (_jsx(Text, { color: "yellow", children: "Warning: worktree has uncommitted changes" })), _jsx(Text, { children: " " }), _jsxs(Text, { children: [_jsx(Text, { color: "red", bold: true, children: "y" }), " Confirm"] }), _jsxs(Text, { children: [_jsx(Text, { color: "cyan", bold: true, children: "n" }), " Cancel"] })] }) })) : state.overlay === "confirm-setup" ? (_jsx(Box, { flexGrow: 1, justifyContent: "center", alignItems: "center", children: _jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "yellow", paddingX: 3, paddingY: 1, children: [_jsx(Text, { bold: true, children: "Run setup script?" }), _jsx(Text, { children: " " }), _jsx(Text, { dimColor: true, children: ".santree/init.sh" }), _jsx(Text, { children: " " }), _jsxs(Text, { children: [_jsx(Text, { color: "green", bold: true, children: "y" }), " Run setup"] }), _jsxs(Text, { children: [_jsx(Text, { color: "yellow", bold: true, children: "n" }), " Skip"] })] }) })) : (_jsxs(Box, { flexGrow: 1, children: [_jsx(Box, { width: leftWidth, children: _jsx(IssueList, { groups: state.groups, flatIssues: state.flatIssues, selectedIndex: state.selectedIndex, scrollOffset: state.listScrollOffset, height: contentHeight, width: leftWidth, creatingForTicket: state.creatingForTicket, deletingForTicket: state.deletingForTicket }) }), _jsx(Box, { flexDirection: "column", width: 3, children: Array.from({ length: contentHeight }).map((_, i) => (_jsx(Text, { dimColor: true, children: " │ " }, i))) }), _jsx(Box, { width: rightWidth, children: state.overlay === "commit" ? (_jsx(CommitOverlay, { width: rightWidth, height: contentHeight, branch: state.commitBranch, ticketId: state.commitTicketId, gitStatus: state.commitGitStatus, phase: state.commitPhase, message: state.commitMessage, error: state.commitError, dispatch: dispatch, onSubmit: handleCommitSubmit })) : state.overlay === "pr-create" ? (_jsx(PrCreateOverlay, { width: rightWidth, height: contentHeight, branch: state.prCreateBranch, ticketId: state.prCreateTicketId, phase: state.prCreatePhase, error: state.prCreateError, url: state.prCreateUrl })) : (_jsx(DetailPanel, { issue: selectedIssue, scrollOffset: state.detailScrollOffset, height: contentHeight, width: rightWidth, creatingForTicket: state.creatingForTicket, creationLogs: state.creationLogs })) })] }))] }));
|
|
1072
|
+
return (_jsxs(Box, { width: columns, height: rows, flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { bold: true, color: "cyan", children: "Santree Dashboard" }), _jsxs(Text, { dimColor: true, children: [" v", version] }), _jsxs(Text, { dimColor: true, children: [" ", "(", state.flatIssues.length, " issues)", state.refreshing ? " refreshing..." : ""] }), state.actionMessage && (_jsxs(Text, { color: "yellow", children: [" ", state.actionMessage] }))] }), state.overlay === "mode-select" ? (_jsx(Box, { flexGrow: 1, justifyContent: "center", alignItems: "center", children: _jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 3, paddingY: 1, children: [_jsx(Text, { bold: true, children: "Select mode:" }), _jsx(Text, { children: " " }), _jsxs(Text, { children: [_jsx(Text, { color: "cyan", bold: true, children: "p" }), " Plan"] }), _jsxs(Text, { children: [_jsx(Text, { color: "cyan", bold: true, children: "i" }), " Implement"] }), _jsx(Text, { children: " " }), _jsx(Text, { dimColor: true, children: "ESC to cancel" })] }) })) : state.overlay === "confirm-delete" ? (_jsx(Box, { flexGrow: 1, justifyContent: "center", alignItems: "center", children: _jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "red", paddingX: 3, paddingY: 1, children: [_jsx(Text, { bold: true, color: "red", children: "Remove worktree?" }), _jsx(Text, { children: " " }), _jsx(Text, { children: selectedIssue?.worktree?.branch ?? "" }), selectedIssue?.worktree?.dirty && (_jsx(Text, { color: "yellow", children: "Warning: worktree has uncommitted changes" })), _jsx(Text, { children: " " }), _jsxs(Text, { children: [_jsx(Text, { color: "red", bold: true, children: "y" }), " Confirm"] }), _jsxs(Text, { children: [_jsx(Text, { color: "cyan", bold: true, children: "n" }), " Cancel"] })] }) })) : state.overlay === "confirm-setup" ? (_jsx(Box, { flexGrow: 1, justifyContent: "center", alignItems: "center", children: _jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "yellow", paddingX: 3, paddingY: 1, children: [_jsx(Text, { bold: true, children: "Run setup script?" }), _jsx(Text, { children: " " }), _jsx(Text, { dimColor: true, children: ".santree/init.sh" }), _jsx(Text, { children: " " }), _jsxs(Text, { children: [_jsx(Text, { color: "green", bold: true, children: "y" }), " Run setup"] }), _jsxs(Text, { children: [_jsx(Text, { color: "yellow", bold: true, children: "n" }), " Skip"] })] }) })) : (_jsxs(Box, { flexGrow: 1, children: [_jsx(Box, { width: leftWidth, children: _jsx(IssueList, { groups: state.groups, flatIssues: state.flatIssues, selectedIndex: state.selectedIndex, scrollOffset: state.listScrollOffset, height: contentHeight, width: leftWidth, creatingForTicket: state.creatingForTicket, deletingForTicket: state.deletingForTicket }) }), _jsx(Box, { flexDirection: "column", width: 3, children: Array.from({ length: contentHeight }).map((_, i) => (_jsx(Text, { dimColor: true, children: " │ " }, i))) }), _jsx(Box, { width: rightWidth, children: state.overlay === "commit" ? (_jsx(CommitOverlay, { width: rightWidth, height: contentHeight, branch: state.commitBranch, ticketId: state.commitTicketId, gitStatus: state.commitGitStatus, phase: state.commitPhase, message: state.commitMessage, error: state.commitError, dispatch: dispatch, onSubmit: handleCommitSubmit })) : state.overlay === "pr-create" ? (_jsx(PrCreateOverlay, { width: rightWidth, height: contentHeight, branch: state.prCreateBranch, ticketId: state.prCreateTicketId, phase: state.prCreatePhase, error: state.prCreateError, url: state.prCreateUrl, body: state.prCreateBody, title: state.prCreateTitle, scrollOffset: state.detailScrollOffset })) : (_jsx(DetailPanel, { issue: selectedIssue, scrollOffset: state.detailScrollOffset, height: contentHeight, width: rightWidth, creatingForTicket: state.creatingForTicket, creationLogs: state.creationLogs })) })] }))] }));
|
|
945
1073
|
}
|
package/dist/commands/doctor.js
CHANGED
|
@@ -143,6 +143,34 @@ function checkShellIntegration() {
|
|
|
143
143
|
const configured = process.env.SANTREE_SHELL_INTEGRATION === "1";
|
|
144
144
|
return { configured, shell: shellName };
|
|
145
145
|
}
|
|
146
|
+
/**
|
|
147
|
+
* Checks if Claude Code Remote Control is enabled for all sessions.
|
|
148
|
+
* Remote Control lets you continue local sessions from any device.
|
|
149
|
+
*
|
|
150
|
+
* This reads from ~/.claude.json (the "global config" / application state file),
|
|
151
|
+
* which is separate from ~/.claude/settings.json (the declarative settings file).
|
|
152
|
+
* See: https://code.claude.com/docs/en/settings#settings-files
|
|
153
|
+
*/
|
|
154
|
+
function checkRemoteControl() {
|
|
155
|
+
const home = process.env.HOME || "";
|
|
156
|
+
const configPath = path.join(home, ".claude.json");
|
|
157
|
+
try {
|
|
158
|
+
if (fs.existsSync(configPath)) {
|
|
159
|
+
const content = fs.readFileSync(configPath, "utf-8");
|
|
160
|
+
const config = JSON.parse(content);
|
|
161
|
+
if (config.remoteControlAtStartup === true) {
|
|
162
|
+
return { enabled: true };
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
catch {
|
|
167
|
+
// JSON parse error or file read error
|
|
168
|
+
}
|
|
169
|
+
return {
|
|
170
|
+
enabled: false,
|
|
171
|
+
hint: 'Run /config in Claude Code and enable "Enable Remote Control for all sessions"',
|
|
172
|
+
};
|
|
173
|
+
}
|
|
146
174
|
/**
|
|
147
175
|
* Checks statusline configuration:
|
|
148
176
|
* If ~/.claude/settings.json has statusLine pointing to santree
|
|
@@ -270,6 +298,9 @@ function ShellRow({ configured, shell }) {
|
|
|
270
298
|
function StatuslineRow({ status }) {
|
|
271
299
|
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] })] })] }));
|
|
272
300
|
}
|
|
301
|
+
function RemoteControlRow({ status }) {
|
|
302
|
+
return (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsxs(Box, { children: [_jsx(StatusIcon, { ok: status.enabled, required: false }), _jsx(Text, { children: " " }), _jsx(Text, { bold: true, children: "Remote Control" }), _jsx(Text, { dimColor: true, children: " - Continue sessions from any device" }), _jsx(Text, { dimColor: true, children: " (optional)" })] }), _jsxs(Box, { marginLeft: 2, flexDirection: "column", children: [_jsxs(Text, { dimColor: true, children: ["Enabled: ", status.enabled ? "yes" : "no"] }), status.hint && _jsxs(Text, { color: "yellow", children: ["\u21B3 ", status.hint] })] })] }));
|
|
303
|
+
}
|
|
273
304
|
function SantreeSetupRow({ status }) {
|
|
274
305
|
const isOk = status.santreeFolderExists &&
|
|
275
306
|
status.initShExists &&
|
|
@@ -289,6 +320,7 @@ export default function Doctor() {
|
|
|
289
320
|
const [tools, setTools] = useState([]);
|
|
290
321
|
const [linear, setLinear] = useState(null);
|
|
291
322
|
const [shellStatus, setShellStatus] = useState(null);
|
|
323
|
+
const [remoteControl, setRemoteControl] = useState(null);
|
|
292
324
|
const [statusline, setStatusline] = useState(null);
|
|
293
325
|
const [santreeSetup, setSantreeSetup] = useState(null);
|
|
294
326
|
const [loading, setLoading] = useState(true);
|
|
@@ -299,7 +331,6 @@ export default function Doctor() {
|
|
|
299
331
|
checkGhAuth(),
|
|
300
332
|
checkTool("tmux", "Terminal multiplexer", false, "tmux -V", "Install: brew install tmux"),
|
|
301
333
|
checkTool("claude", "Claude Code CLI", true, "claude --version 2>/dev/null | head -1", "Install: npm install -g @anthropic-ai/claude-code"),
|
|
302
|
-
checkTool("happy", "Claude CLI wrapper (used over claude if installed)", false, "happy --version 2>/dev/null || echo 'installed'", "Install: npm install -g happy-coder"),
|
|
303
334
|
]);
|
|
304
335
|
// Check for either code or cursor (only need one)
|
|
305
336
|
const [codeCheck, cursorCheck] = await Promise.all([
|
|
@@ -326,6 +357,7 @@ export default function Doctor() {
|
|
|
326
357
|
setTools(results);
|
|
327
358
|
setLinear(linearResult);
|
|
328
359
|
setShellStatus(checkShellIntegration());
|
|
360
|
+
setRemoteControl(checkRemoteControl());
|
|
329
361
|
setStatusline(statuslineResult);
|
|
330
362
|
setSantreeSetup(checkSantreeSetup());
|
|
331
363
|
setLoading(false);
|
|
@@ -339,5 +371,5 @@ export default function Doctor() {
|
|
|
339
371
|
const optionalMissing = tools.filter((t) => !t.required && !t.installed);
|
|
340
372
|
const linearOk = linear?.authenticated && linear?.tokenValid && linear?.repoLinked;
|
|
341
373
|
const allRequired = requiredMissing.length === 0 && linearOk && shellStatus?.configured;
|
|
342
|
-
return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsxs(Box, { marginBottom: 1, children: [_jsx(Text, { bold: true, color: "cyan", children: "Santree Doctor" }), _jsxs(Text, { dimColor: true, children: [" v", version] })] }), _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: "
|
|
374
|
+
return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsxs(Box, { marginBottom: 1, children: [_jsx(Text, { bold: true, color: "cyan", children: "Santree Doctor" }), _jsxs(Text, { dimColor: true, children: [" v", version] })] }), _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: "Claude Code" }) }), remoteControl && _jsx(RemoteControlRow, { status: remoteControl }), 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"] }))] })) })] }));
|
|
343
375
|
}
|
|
@@ -2,10 +2,10 @@ import { z } from "zod/v4";
|
|
|
2
2
|
export declare const description = "Render a template to stdout";
|
|
3
3
|
export declare const args: z.ZodTuple<[z.ZodEnum<{
|
|
4
4
|
linear: "linear";
|
|
5
|
+
review: "review";
|
|
5
6
|
pr: "pr";
|
|
6
7
|
"git-changes": "git-changes";
|
|
7
8
|
"fix-pr": "fix-pr";
|
|
8
|
-
review: "review";
|
|
9
9
|
}>], null>;
|
|
10
10
|
type Props = {
|
|
11
11
|
args: z.infer<typeof args>;
|
package/dist/commands/pr/fix.js
CHANGED
|
@@ -2,7 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import { useEffect, useState } from "react";
|
|
3
3
|
import { Text, Box } from "ink";
|
|
4
4
|
import Spinner from "ink-spinner";
|
|
5
|
-
import { resolveAIContext, renderAIPrompt, launchAgent,
|
|
5
|
+
import { resolveAIContext, renderAIPrompt, launchAgent, cleanupImages, fetchAndRenderPR, fetchAndRenderDiff, } from "../../lib/ai.js";
|
|
6
6
|
export const description = "Fix PR review comments";
|
|
7
7
|
export default function Fix() {
|
|
8
8
|
const [status, setStatus] = useState("loading");
|
|
@@ -54,5 +54,5 @@ export default function Fix() {
|
|
|
54
54
|
}
|
|
55
55
|
init();
|
|
56
56
|
}, []);
|
|
57
|
-
return (_jsxs(Box, { flexDirection: "column", padding: 1, width: "100%", children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, color: "cyan", children: "Fix PR" }) }), _jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: status === "error" ? "red" : "magenta", paddingX: 1, width: "100%", children: [branch && (_jsxs(Box, { gap: 1, children: [_jsx(Text, { dimColor: true, children: "branch:" }), _jsx(Text, { color: "cyan", bold: true, children: branch })] })), ticketId && (_jsxs(Box, { gap: 1, children: [_jsx(Text, { dimColor: true, children: "ticket:" }), _jsx(Text, { color: "blue", bold: true, children: ticketId })] })), _jsxs(Box, { gap: 1, children: [_jsx(Text, { dimColor: true, children: "mode:" }), _jsx(Text, { backgroundColor: "magenta", color: "white", bold: true, children: " fix PR " })] })] }), _jsxs(Box, { marginTop: 1, children: [(status === "loading" || status === "fetching") && (_jsxs(Box, { children: [_jsx(Text, { color: "cyan", children: _jsx(Spinner, { type: "dots" }) }), _jsxs(Text, { children: [" ", status === "loading" ? "Loading..." : "Fetching ticket and PR feedback..."] })] })), status === "launching" && (_jsxs(Box, { flexDirection: "column", children: [
|
|
57
|
+
return (_jsxs(Box, { flexDirection: "column", padding: 1, width: "100%", children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, color: "cyan", children: "Fix PR" }) }), _jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: status === "error" ? "red" : "magenta", paddingX: 1, width: "100%", children: [branch && (_jsxs(Box, { gap: 1, children: [_jsx(Text, { dimColor: true, children: "branch:" }), _jsx(Text, { color: "cyan", bold: true, children: branch })] })), ticketId && (_jsxs(Box, { gap: 1, children: [_jsx(Text, { dimColor: true, children: "ticket:" }), _jsx(Text, { color: "blue", bold: true, children: ticketId })] })), _jsxs(Box, { gap: 1, children: [_jsx(Text, { dimColor: true, children: "mode:" }), _jsx(Text, { backgroundColor: "magenta", color: "white", bold: true, children: " fix PR " })] })] }), _jsxs(Box, { marginTop: 1, children: [(status === "loading" || status === "fetching") && (_jsxs(Box, { children: [_jsx(Text, { color: "cyan", children: _jsx(Spinner, { type: "dots" }) }), _jsxs(Text, { children: [" ", status === "loading" ? "Loading..." : "Fetching ticket and PR feedback..."] })] })), status === "launching" && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: "green", bold: true, children: "\u2713 Launching Claude..." }), _jsxs(Text, { dimColor: true, children: [" claude ", `"<fix-pr prompt for ${ticketId}>"`] })] })), status === "error" && (_jsxs(Text, { color: "red", bold: true, children: ["\u2717 ", error] }))] })] }));
|
|
58
58
|
}
|
|
@@ -2,7 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import { useEffect, useState } from "react";
|
|
3
3
|
import { Text, Box } from "ink";
|
|
4
4
|
import Spinner from "ink-spinner";
|
|
5
|
-
import { resolveAIContext, renderAIPrompt, launchAgent,
|
|
5
|
+
import { resolveAIContext, renderAIPrompt, launchAgent, cleanupImages, fetchAndRenderDiff, } from "../../lib/ai.js";
|
|
6
6
|
export const description = "Review changes against ticket requirements";
|
|
7
7
|
export default function Review() {
|
|
8
8
|
const [status, setStatus] = useState("loading");
|
|
@@ -47,5 +47,5 @@ export default function Review() {
|
|
|
47
47
|
}
|
|
48
48
|
init();
|
|
49
49
|
}, []);
|
|
50
|
-
return (_jsxs(Box, { flexDirection: "column", padding: 1, width: "100%", children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, color: "cyan", children: "Review" }) }), _jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: status === "error" ? "red" : "yellow", paddingX: 1, width: "100%", children: [branch && (_jsxs(Box, { gap: 1, children: [_jsx(Text, { dimColor: true, children: "branch:" }), _jsx(Text, { color: "cyan", bold: true, children: branch })] })), ticketId && (_jsxs(Box, { gap: 1, children: [_jsx(Text, { dimColor: true, children: "ticket:" }), _jsx(Text, { color: "blue", bold: true, children: ticketId })] })), _jsxs(Box, { gap: 1, children: [_jsx(Text, { dimColor: true, children: "mode:" }), _jsx(Text, { backgroundColor: "yellow", color: "white", bold: true, children: " review " })] })] }), _jsxs(Box, { marginTop: 1, children: [(status === "loading" || status === "fetching") && (_jsxs(Box, { children: [_jsx(Text, { color: "cyan", children: _jsx(Spinner, { type: "dots" }) }), _jsxs(Text, { children: [" ", status === "loading" ? "Loading..." : "Fetching ticket, diff, and PR feedback..."] })] })), status === "launching" && (_jsxs(Box, { flexDirection: "column", children: [
|
|
50
|
+
return (_jsxs(Box, { flexDirection: "column", padding: 1, width: "100%", children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, color: "cyan", children: "Review" }) }), _jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: status === "error" ? "red" : "yellow", paddingX: 1, width: "100%", children: [branch && (_jsxs(Box, { gap: 1, children: [_jsx(Text, { dimColor: true, children: "branch:" }), _jsx(Text, { color: "cyan", bold: true, children: branch })] })), ticketId && (_jsxs(Box, { gap: 1, children: [_jsx(Text, { dimColor: true, children: "ticket:" }), _jsx(Text, { color: "blue", bold: true, children: ticketId })] })), _jsxs(Box, { gap: 1, children: [_jsx(Text, { dimColor: true, children: "mode:" }), _jsx(Text, { backgroundColor: "yellow", color: "white", bold: true, children: " review " })] })] }), _jsxs(Box, { marginTop: 1, children: [(status === "loading" || status === "fetching") && (_jsxs(Box, { children: [_jsx(Text, { color: "cyan", children: _jsx(Spinner, { type: "dots" }) }), _jsxs(Text, { children: [" ", status === "loading" ? "Loading..." : "Fetching ticket, diff, and PR feedback..."] })] })), status === "launching" && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: "green", bold: true, children: "\u2713 Launching Claude..." }), _jsxs(Text, { dimColor: true, children: [" claude ", `"<review prompt for ${ticketId}>"`] })] })), status === "error" && (_jsxs(Text, { color: "red", bold: true, children: ["\u2717 ", error] }))] })] }));
|
|
51
51
|
}
|
|
@@ -3,7 +3,7 @@ import { useEffect, useState } from "react";
|
|
|
3
3
|
import { Text, Box } from "ink";
|
|
4
4
|
import Spinner from "ink-spinner";
|
|
5
5
|
import { z } from "zod";
|
|
6
|
-
import { resolveAIContext, renderAIPrompt, launchAgent,
|
|
6
|
+
import { resolveAIContext, renderAIPrompt, launchAgent, cleanupImages, } from "../../lib/ai.js";
|
|
7
7
|
import { randomUUID } from "crypto";
|
|
8
8
|
import { getSessionId, setSessionId } from "../../lib/git.js";
|
|
9
9
|
export const description = "Launch Claude to work on current ticket";
|
|
@@ -78,5 +78,5 @@ export default function Work({ options }) {
|
|
|
78
78
|
setError(err instanceof Error ? err.message : "Failed to launch agent");
|
|
79
79
|
}
|
|
80
80
|
}, [status, aiContext, mode]);
|
|
81
|
-
return (_jsxs(Box, { flexDirection: "column", padding: 1, width: "100%", children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, color: "cyan", children: "Work" }) }), _jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: status === "error" ? "red" : getModeColor(mode), paddingX: 1, width: "100%", children: [branch && (_jsxs(Box, { gap: 1, children: [_jsx(Text, { dimColor: true, children: "branch:" }), _jsx(Text, { color: "cyan", bold: true, children: branch })] })), ticketId && (_jsxs(Box, { gap: 1, children: [_jsx(Text, { dimColor: true, children: "ticket:" }), _jsx(Text, { color: "blue", bold: true, children: ticketId })] })), _jsxs(Box, { gap: 1, children: [_jsx(Text, { dimColor: true, children: "mode:" }), _jsx(Text, { backgroundColor: getModeColor(mode), color: "white", bold: true, children: ` ${getModeLabel(mode)} ` })] })] }), _jsxs(Box, { marginTop: 1, children: [status === "loading" && (_jsxs(Box, { children: [_jsx(Text, { color: "cyan", children: _jsx(Spinner, { type: "dots" }) }), _jsx(Text, { children: " Loading..." })] })), status === "fetching" && (_jsxs(Box, { children: [_jsx(Text, { color: "cyan", children: _jsx(Spinner, { type: "dots" }) }), _jsx(Text, { children: " Fetching ticket from Linear..." })] })), status === "launching" && (_jsxs(Box, { flexDirection: "column", children: [
|
|
81
|
+
return (_jsxs(Box, { flexDirection: "column", padding: 1, width: "100%", children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, color: "cyan", children: "Work" }) }), _jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: status === "error" ? "red" : getModeColor(mode), paddingX: 1, width: "100%", children: [branch && (_jsxs(Box, { gap: 1, children: [_jsx(Text, { dimColor: true, children: "branch:" }), _jsx(Text, { color: "cyan", bold: true, children: branch })] })), ticketId && (_jsxs(Box, { gap: 1, children: [_jsx(Text, { dimColor: true, children: "ticket:" }), _jsx(Text, { color: "blue", bold: true, children: ticketId })] })), _jsxs(Box, { gap: 1, children: [_jsx(Text, { dimColor: true, children: "mode:" }), _jsx(Text, { backgroundColor: getModeColor(mode), color: "white", bold: true, children: ` ${getModeLabel(mode)} ` })] })] }), _jsxs(Box, { marginTop: 1, children: [status === "loading" && (_jsxs(Box, { children: [_jsx(Text, { color: "cyan", children: _jsx(Spinner, { type: "dots" }) }), _jsx(Text, { children: " Loading..." })] })), status === "fetching" && (_jsxs(Box, { children: [_jsx(Text, { color: "cyan", children: _jsx(Spinner, { type: "dots" }) }), _jsx(Text, { children: " Fetching ticket from Linear..." })] })), status === "launching" && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: "green", bold: true, children: "\u2713 Launching Claude..." }), _jsxs(Text, { dimColor: true, children: [" ", "claude", mode === "plan" ? " --permission-mode plan" : "", " ", `"<${getModeLabel(mode)} prompt for ${ticketId}>"`] })] })), status === "error" && (_jsxs(Text, { color: "red", bold: true, children: ["\u2717 ", error] }))] })] }));
|
|
82
82
|
}
|
package/dist/lib/ai.d.ts
CHANGED
|
@@ -37,15 +37,14 @@ export declare function fetchAndRenderPR(branch: string): Promise<string | null>
|
|
|
37
37
|
*/
|
|
38
38
|
export declare function fetchAndRenderDiff(branch: string): Promise<string>;
|
|
39
39
|
/**
|
|
40
|
-
*
|
|
41
|
-
* Returns
|
|
40
|
+
* Check if claude CLI is available on PATH.
|
|
41
|
+
* Returns "claude" or null if not installed.
|
|
42
42
|
*/
|
|
43
43
|
export declare function resolveAgentBinary(): string | null;
|
|
44
44
|
/**
|
|
45
45
|
* Launch an interactive agent session with a prompt.
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
* Throws if no agent binary is found.
|
|
46
|
+
* Passes prompt directly or via temp file if too large for OS arg limit.
|
|
47
|
+
* Throws if claude CLI is not found.
|
|
49
48
|
*/
|
|
50
49
|
export declare function launchAgent(prompt: string, opts?: {
|
|
51
50
|
planMode?: boolean;
|
|
@@ -58,9 +57,8 @@ export interface RunAgentResult {
|
|
|
58
57
|
}
|
|
59
58
|
/**
|
|
60
59
|
* Run an agent in non-interactive print mode and capture output.
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
* Throws if no agent binary is found.
|
|
60
|
+
* Passes prompt directly or via temp file if too large for OS arg limit.
|
|
61
|
+
* Throws if claude CLI is not found.
|
|
64
62
|
*/
|
|
65
63
|
export declare function runAgent(prompt: string): RunAgentResult;
|
|
66
64
|
/**
|
package/dist/lib/ai.js
CHANGED
|
@@ -105,20 +105,17 @@ export async function fetchAndRenderDiff(branch) {
|
|
|
105
105
|
});
|
|
106
106
|
}
|
|
107
107
|
/**
|
|
108
|
-
*
|
|
109
|
-
* Returns
|
|
108
|
+
* Check if claude CLI is available on PATH.
|
|
109
|
+
* Returns "claude" or null if not installed.
|
|
110
110
|
*/
|
|
111
111
|
export function resolveAgentBinary() {
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
continue;
|
|
119
|
-
}
|
|
112
|
+
try {
|
|
113
|
+
execSync("which claude", { stdio: "ignore" });
|
|
114
|
+
return "claude";
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
return null;
|
|
120
118
|
}
|
|
121
|
-
return null;
|
|
122
119
|
}
|
|
123
120
|
// Conservative limit: 200KB leaves room for env vars within macOS 256KB ARG_MAX
|
|
124
121
|
const ARG_MAX_SAFE = 200 * 1024;
|
|
@@ -137,14 +134,13 @@ function promptArg(prompt) {
|
|
|
137
134
|
}
|
|
138
135
|
/**
|
|
139
136
|
* Launch an interactive agent session with a prompt.
|
|
140
|
-
*
|
|
141
|
-
*
|
|
142
|
-
* Throws if no agent binary is found.
|
|
137
|
+
* Passes prompt directly or via temp file if too large for OS arg limit.
|
|
138
|
+
* Throws if claude CLI is not found.
|
|
143
139
|
*/
|
|
144
140
|
export function launchAgent(prompt, opts) {
|
|
145
141
|
const bin = resolveAgentBinary();
|
|
146
142
|
if (!bin) {
|
|
147
|
-
throw new Error("
|
|
143
|
+
throw new Error("Claude CLI not found. Install: npm install -g @anthropic-ai/claude-code");
|
|
148
144
|
}
|
|
149
145
|
const args = [];
|
|
150
146
|
if (opts?.planMode) {
|
|
@@ -163,14 +159,13 @@ export function launchAgent(prompt, opts) {
|
|
|
163
159
|
}
|
|
164
160
|
/**
|
|
165
161
|
* Run an agent in non-interactive print mode and capture output.
|
|
166
|
-
*
|
|
167
|
-
*
|
|
168
|
-
* Throws if no agent binary is found.
|
|
162
|
+
* Passes prompt directly or via temp file if too large for OS arg limit.
|
|
163
|
+
* Throws if claude CLI is not found.
|
|
169
164
|
*/
|
|
170
165
|
export function runAgent(prompt) {
|
|
171
166
|
const bin = resolveAgentBinary();
|
|
172
167
|
if (!bin) {
|
|
173
|
-
throw new Error("
|
|
168
|
+
throw new Error("Claude CLI not found. Install: npm install -g @anthropic-ai/claude-code");
|
|
174
169
|
}
|
|
175
170
|
const result = spawnSync(bin, ["-p", "--output-format", "text", "--", promptArg(prompt)], {
|
|
176
171
|
encoding: "utf-8",
|
|
@@ -20,6 +20,9 @@ interface PrCreateOverlayProps {
|
|
|
20
20
|
phase: PrCreatePhase;
|
|
21
21
|
error: string | null;
|
|
22
22
|
url: string | null;
|
|
23
|
+
body: string | null;
|
|
24
|
+
title: string | null;
|
|
25
|
+
scrollOffset: number;
|
|
23
26
|
}
|
|
24
|
-
export declare function PrCreateOverlay({ width, height, branch, ticketId, phase, error, url, }: PrCreateOverlayProps): import("react/jsx-runtime").JSX.Element;
|
|
27
|
+
export declare function PrCreateOverlay({ width, height, branch, ticketId, phase, error, url, body, title, scrollOffset, }: PrCreateOverlayProps): import("react/jsx-runtime").JSX.Element;
|
|
25
28
|
export {};
|
|
@@ -20,6 +20,9 @@ export function CommitOverlay({ width, height, branch, ticketId, gitStatus, phas
|
|
|
20
20
|
return (_jsxs(Text, { color: color, children: [" ", line] }, i));
|
|
21
21
|
}), gitStatus.split("\n").length > 8 && (_jsxs(Text, { dimColor: true, children: [" +", gitStatus.split("\n").length - 8, " more"] }))] })) : null, _jsx(Text, { children: " " }), phase === "confirm-stage" && (_jsxs(Text, { children: ["Stage all changes?", " ", _jsx(Text, { color: "cyan", bold: true, children: "y" }), "/", _jsx(Text, { color: "cyan", bold: true, children: "n" })] })), phase === "awaiting-message" && (_jsxs(Box, { children: [_jsx(Text, { children: "Message: " }), _jsx(TextInput, { value: message, onChange: (v) => dispatch({ type: "COMMIT_MESSAGE", message: v }), onSubmit: onSubmit })] })), phase === "committing" && (_jsxs(Text, { children: [_jsx(Text, { color: "cyan", children: _jsx(Spinner, { type: "dots" }) }), " ", "Committing..."] })), phase === "pushing" && (_jsxs(Text, { children: [_jsx(Text, { color: "cyan", children: _jsx(Spinner, { type: "dots" }) }), " ", "Pushing..."] })), phase === "done" && (_jsx(Text, { color: "green", bold: true, children: "Committed and pushed!" })), phase === "error" && _jsx(Text, { color: "red", children: error }), _jsx(Text, { children: " " }), _jsx(Text, { dimColor: true, children: "ESC to cancel" })] }));
|
|
22
22
|
}
|
|
23
|
-
export function PrCreateOverlay({ width, height, branch, ticketId, phase, error, url, }) {
|
|
24
|
-
return (_jsxs(Box, { flexDirection: "column", width: width, height: height, children: [_jsx(Text, { bold: true, color: "cyan", children: "Create Pull Request" }), _jsx(Text, { dimColor: true, children: "─".repeat(Math.min(width, 50)) }), _jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "branch: " }), _jsx(Text, { children: branch })] }), _jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "ticket: " }), _jsx(Text, { children: ticketId })] }), _jsx(Text, { children: " " }), phase === "choose-mode" && (_jsxs(_Fragment, { children: [_jsx(Text, { bold: true, children: "How do you want to create this PR?" }), _jsx(Text, { children: " " }), _jsxs(Text, { children: [" ", _jsx(Text, { color: "cyan", bold: true, children: "f" }), " ", "Fill \u2014
|
|
23
|
+
export function PrCreateOverlay({ width, height, branch, ticketId, phase, error, url, body, title, scrollOffset, }) {
|
|
24
|
+
return (_jsxs(Box, { flexDirection: "column", width: width, height: height, children: [_jsx(Text, { bold: true, color: "cyan", children: "Create Pull Request" }), _jsx(Text, { dimColor: true, children: "─".repeat(Math.min(width, 50)) }), _jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "branch: " }), _jsx(Text, { children: branch })] }), _jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "ticket: " }), _jsx(Text, { children: ticketId })] }), _jsx(Text, { children: " " }), phase === "choose-mode" && (_jsxs(_Fragment, { children: [_jsx(Text, { bold: true, children: "How do you want to create this PR?" }), _jsx(Text, { children: " " }), _jsxs(Text, { children: [" ", _jsx(Text, { color: "cyan", bold: true, children: "f" }), " ", "Fill \u2014 use AI to fill the PR template"] }), _jsxs(Text, { children: [" ", _jsx(Text, { color: "cyan", bold: true, children: "w" }), " ", "Web \u2014 open in browser to edit manually"] })] })), phase === "pushing" && (_jsxs(Text, { children: [_jsx(Text, { color: "cyan", children: _jsx(Spinner, { type: "dots" }) }), " ", "Pushing branch..."] })), phase === "filling" && (_jsxs(Text, { children: [_jsx(Text, { color: "cyan", children: _jsx(Spinner, { type: "dots" }) }), " ", "Filling PR template with AI..."] })), phase === "review" && (_jsxs(_Fragment, { children: [_jsx(Text, { bold: true, children: "Review PR" }), _jsx(Text, { children: " " }), _jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "title: " }), _jsx(Text, { children: title })] }), _jsx(Text, { children: " " }), body
|
|
25
|
+
?.split("\n")
|
|
26
|
+
.slice(scrollOffset, scrollOffset + height - 11)
|
|
27
|
+
.map((line, i) => (_jsx(Text, { wrap: "truncate", children: line }, i))), _jsx(Text, { children: " " }), _jsxs(Text, { dimColor: true, children: [_jsx(Text, { color: "cyan", bold: true, children: "y" }), "/Enter create", " ", _jsx(Text, { color: "cyan", bold: true, children: "w" }), " ", "open in browser ESC cancel Shift+arrows scroll"] })] })), phase === "creating" && (_jsxs(Text, { children: [_jsx(Text, { color: "cyan", children: _jsx(Spinner, { type: "dots" }) }), " ", "Creating PR..."] })), phase === "done" && (_jsxs(_Fragment, { children: [_jsx(Text, { color: "green", bold: true, children: "PR created!" }), url ? _jsx(Text, { dimColor: true, children: url }) : null] })), phase === "error" && (_jsxs(_Fragment, { children: [_jsx(Text, { color: "red", children: error }), _jsx(Text, { children: " " }), _jsxs(Text, { dimColor: true, children: [_jsx(Text, { color: "cyan", bold: true, children: "w" }), " ", "open in browser ESC cancel"] })] })), phase !== "review" && phase !== "error" && (_jsxs(_Fragment, { children: [_jsx(Text, { children: " " }), _jsx(Text, { dimColor: true, children: "ESC to cancel" })] }))] }));
|
|
25
28
|
}
|
|
@@ -41,7 +41,7 @@ export interface ProjectGroup {
|
|
|
41
41
|
}
|
|
42
42
|
export type ActionOverlay = "mode-select" | "confirm-delete" | "confirm-setup" | "commit" | "pr-create" | null;
|
|
43
43
|
export type CommitPhase = "idle" | "confirm-stage" | "awaiting-message" | "committing" | "pushing" | "done" | "error";
|
|
44
|
-
export type PrCreatePhase = "idle" | "choose-mode" | "pushing" | "creating" | "done" | "error";
|
|
44
|
+
export type PrCreatePhase = "idle" | "choose-mode" | "pushing" | "filling" | "review" | "creating" | "done" | "error";
|
|
45
45
|
export interface DashboardState {
|
|
46
46
|
groups: ProjectGroup[];
|
|
47
47
|
flatIssues: DashboardIssue[];
|
|
@@ -70,6 +70,8 @@ export interface DashboardState {
|
|
|
70
70
|
prCreateBranch: string | null;
|
|
71
71
|
prCreateError: string | null;
|
|
72
72
|
prCreateUrl: string | null;
|
|
73
|
+
prCreateBody: string | null;
|
|
74
|
+
prCreateTitle: string | null;
|
|
73
75
|
setupMode: "plan" | "implement" | null;
|
|
74
76
|
}
|
|
75
77
|
export type DashboardAction = {
|
|
@@ -146,6 +148,10 @@ export type DashboardAction = {
|
|
|
146
148
|
} | {
|
|
147
149
|
type: "PR_CREATE_ERROR";
|
|
148
150
|
error: string;
|
|
151
|
+
} | {
|
|
152
|
+
type: "PR_CREATE_REVIEW";
|
|
153
|
+
body: string;
|
|
154
|
+
title: string;
|
|
149
155
|
} | {
|
|
150
156
|
type: "PR_CREATE_DONE";
|
|
151
157
|
url: string;
|
|
@@ -27,6 +27,8 @@ export const initialState = {
|
|
|
27
27
|
prCreateBranch: null,
|
|
28
28
|
prCreateError: null,
|
|
29
29
|
prCreateUrl: null,
|
|
30
|
+
prCreateBody: null,
|
|
31
|
+
prCreateTitle: null,
|
|
30
32
|
setupMode: null,
|
|
31
33
|
};
|
|
32
34
|
export function reducer(state, action) {
|
|
@@ -133,8 +135,22 @@ export function reducer(state, action) {
|
|
|
133
135
|
return { ...state, prCreatePhase: action.phase };
|
|
134
136
|
case "PR_CREATE_ERROR":
|
|
135
137
|
return { ...state, prCreatePhase: "error", prCreateError: action.error };
|
|
138
|
+
case "PR_CREATE_REVIEW":
|
|
139
|
+
return {
|
|
140
|
+
...state,
|
|
141
|
+
prCreatePhase: "review",
|
|
142
|
+
prCreateBody: action.body,
|
|
143
|
+
prCreateTitle: action.title,
|
|
144
|
+
detailScrollOffset: 0,
|
|
145
|
+
};
|
|
136
146
|
case "PR_CREATE_DONE":
|
|
137
|
-
return {
|
|
147
|
+
return {
|
|
148
|
+
...state,
|
|
149
|
+
prCreatePhase: "done",
|
|
150
|
+
prCreateUrl: action.url,
|
|
151
|
+
prCreateBody: null,
|
|
152
|
+
prCreateTitle: null,
|
|
153
|
+
};
|
|
138
154
|
case "PR_CREATE_CANCEL":
|
|
139
155
|
return {
|
|
140
156
|
...state,
|
|
@@ -145,6 +161,8 @@ export function reducer(state, action) {
|
|
|
145
161
|
prCreateBranch: null,
|
|
146
162
|
prCreateError: null,
|
|
147
163
|
prCreateUrl: null,
|
|
164
|
+
prCreateBody: null,
|
|
165
|
+
prCreateTitle: null,
|
|
148
166
|
};
|
|
149
167
|
case "SETUP_CONFIRM_SHOW":
|
|
150
168
|
return {
|
package/dist/lib/exec.d.ts
CHANGED
package/dist/lib/exec.js
CHANGED
|
@@ -33,8 +33,12 @@ export function spawnAsync(cmd, args, options) {
|
|
|
33
33
|
const child = spawn(cmd, args, {
|
|
34
34
|
cwd: options?.cwd,
|
|
35
35
|
env: options?.env,
|
|
36
|
-
stdio: "pipe",
|
|
36
|
+
stdio: [options?.stdin !== undefined ? "pipe" : "ignore", "pipe", "pipe"],
|
|
37
37
|
});
|
|
38
|
+
if (options?.stdin !== undefined) {
|
|
39
|
+
child.stdin.write(options.stdin);
|
|
40
|
+
child.stdin.end();
|
|
41
|
+
}
|
|
38
42
|
let output = "";
|
|
39
43
|
child.stdout?.on("data", (data) => {
|
|
40
44
|
output += data.toString();
|