santree 0.0.12 → 0.0.13

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 CHANGED
@@ -83,6 +83,7 @@ santree clean
83
83
  | `santree work` | Launch Claude AI to work on the current ticket |
84
84
  | `santree clean` | Remove worktrees with merged/closed PRs |
85
85
  | `santree doctor` | Check system requirements and integrations |
86
+ | `santree editor` | Open workspace file in VSCode or Cursor |
86
87
  | `santree statusline` | Statusline wrapper for Claude Code |
87
88
 
88
89
  ---
@@ -173,6 +174,11 @@ Removes the worktree and deletes the branch. Uses force mode by default (removes
173
174
  ### clean
174
175
  Shows worktrees with merged/closed PRs and prompts for confirmation before removing.
175
176
 
177
+ ### editor
178
+ | Option | Description |
179
+ |--------|-------------|
180
+ | `--editor <cmd>` | Editor command to use (default: `code`). Also configurable via `SANTREE_EDITOR` env var |
181
+
176
182
  ### work
177
183
  | Option | Description |
178
184
  |--------|-------------|
@@ -190,3 +196,4 @@ Shows worktrees with merged/closed PRs and prompts for confirmation before remov
190
196
  | Git | Worktree operations |
191
197
  | GitHub CLI (`gh`) | PR integration |
192
198
  | tmux | Optional: new window support |
199
+ | VSCode (`code`) or Cursor (`cursor`) | Optional: workspace editor |
@@ -228,6 +228,26 @@ export default function Doctor() {
228
228
  checkTool("claude", "Claude Code CLI", true, "claude --version 2>/dev/null | head -1", "Install: npm install -g @anthropic-ai/claude-code"),
229
229
  checkTool("happy", "Claude CLI wrapper", true, "happy --version 2>/dev/null || echo 'installed'", "Install: npm install -g happy-coder"),
230
230
  ]);
231
+ // Check for either code or cursor (only need one)
232
+ const [codeCheck, cursorCheck] = await Promise.all([
233
+ checkTool("code", "VSCode editor", false, "code --version | head -1", ""),
234
+ checkTool("cursor", "Cursor editor", false, "cursor --version | head -1", ""),
235
+ ]);
236
+ if (codeCheck.installed) {
237
+ results.push({ ...codeCheck, description: "Editor (VSCode)" });
238
+ }
239
+ else if (cursorCheck.installed) {
240
+ results.push({ ...cursorCheck, description: "Editor (Cursor)" });
241
+ }
242
+ else {
243
+ results.push({
244
+ name: "code/cursor",
245
+ description: "Editor (VSCode or Cursor)",
246
+ required: false,
247
+ installed: false,
248
+ hint: "Install VSCode (https://code.visualstudio.com) or Cursor (https://cursor.sh)",
249
+ });
250
+ }
231
251
  const mcpResult = await checkLinearMcp();
232
252
  const statuslineResult = await checkStatusline();
233
253
  setTools(results);
@@ -0,0 +1,10 @@
1
+ import { z } from "zod";
2
+ export declare const description = "Open workspace file in VSCode or Cursor";
3
+ export declare const options: z.ZodObject<{
4
+ editor: z.ZodOptional<z.ZodString>;
5
+ }, z.core.$strip>;
6
+ type Props = {
7
+ options: z.infer<typeof options>;
8
+ };
9
+ export default function Editor({ options: opts }: Props): import("react/jsx-runtime").JSX.Element | null;
10
+ export {};
@@ -0,0 +1,72 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useEffect, useState } from "react";
3
+ import { Text, Box } from "ink";
4
+ import { z } from "zod";
5
+ import { findMainRepoRoot } from "../lib/git.js";
6
+ import { execSync, spawn } from "child_process";
7
+ import * as fs from "fs";
8
+ import * as path from "path";
9
+ export const description = "Open workspace file in VSCode or Cursor";
10
+ export const options = z.object({
11
+ editor: z
12
+ .string()
13
+ .optional()
14
+ .describe("Editor command (code, cursor)"),
15
+ });
16
+ export default function Editor({ options: opts }) {
17
+ const [status, setStatus] = useState({ state: "loading" });
18
+ useEffect(() => {
19
+ const repoRoot = findMainRepoRoot();
20
+ if (!repoRoot) {
21
+ setStatus({ state: "error", message: "Not inside a git repository" });
22
+ return;
23
+ }
24
+ // Find *.code-workspace file
25
+ let workspaceFile = null;
26
+ try {
27
+ const entries = fs.readdirSync(repoRoot);
28
+ const wsFiles = entries
29
+ .filter((f) => f.endsWith(".code-workspace"))
30
+ .sort();
31
+ if (wsFiles.length > 0) {
32
+ workspaceFile = wsFiles[0];
33
+ }
34
+ }
35
+ catch {
36
+ setStatus({ state: "error", message: "Failed to read repository root" });
37
+ return;
38
+ }
39
+ if (!workspaceFile) {
40
+ setStatus({
41
+ state: "error",
42
+ message: `No .code-workspace file found in ${repoRoot}`,
43
+ });
44
+ return;
45
+ }
46
+ // Resolve editor: --editor flag > SANTREE_EDITOR env > "code" default
47
+ const editor = opts.editor || process.env.SANTREE_EDITOR || "code";
48
+ // Validate editor exists in PATH
49
+ try {
50
+ execSync(`which ${editor}`, { stdio: "ignore" });
51
+ }
52
+ catch {
53
+ setStatus({
54
+ state: "error",
55
+ message: `Editor "${editor}" not found in PATH`,
56
+ });
57
+ return;
58
+ }
59
+ // Spawn editor detached
60
+ const fullPath = path.join(repoRoot, workspaceFile);
61
+ const child = spawn(editor, [fullPath], {
62
+ detached: true,
63
+ stdio: "ignore",
64
+ });
65
+ child.unref();
66
+ setStatus({ state: "done", repo: repoRoot, file: workspaceFile, editor });
67
+ }, []);
68
+ if (status.state === "loading") {
69
+ return null;
70
+ }
71
+ return (_jsxs(Box, { flexDirection: "column", padding: 1, width: "100%", children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, color: "cyan", children: "Editor" }) }), _jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: status.state === "error" ? "red" : "green", paddingX: 1, width: "100%", children: [status.state === "done" && (_jsxs(_Fragment, { children: [_jsxs(Box, { gap: 1, children: [_jsx(Text, { dimColor: true, children: "repo:" }), _jsx(Text, { dimColor: true, children: status.repo })] }), _jsxs(Box, { gap: 1, children: [_jsx(Text, { dimColor: true, children: "file:" }), _jsx(Text, { color: "cyan", bold: true, children: status.file })] }), _jsxs(Box, { gap: 1, children: [_jsx(Text, { dimColor: true, children: "editor:" }), _jsx(Text, { dimColor: true, children: status.editor })] })] })), status.state === "error" && (_jsxs(Box, { gap: 1, children: [_jsx(Text, { dimColor: true, children: "error:" }), _jsx(Text, { color: "red", children: status.message })] }))] }), _jsxs(Box, { marginTop: 1, children: [status.state === "done" && (_jsxs(Text, { color: "green", bold: true, children: ["\u2713 Opened workspace in ", status.editor] })), status.state === "error" && (_jsxs(Text, { color: "red", bold: true, children: ["\u2717 ", status.message] }))] })] }));
72
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "santree",
3
- "version": "0.0.12",
3
+ "version": "0.0.13",
4
4
  "description": "Git worktree manager with Linear integration",
5
5
  "license": "MIT",
6
6
  "author": "Santiago Toscanini",