santree 0.0.12 → 0.0.14

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
+ }
@@ -132,7 +132,7 @@ function formatChanges(changes) {
132
132
  // Build statusline for santree worktree
133
133
  function buildSantreeStatusline(cwd, metadata, model, usedPercentage) {
134
134
  const parts = [];
135
- const branch = metadata.branch_name || git(cwd, "rev-parse --abbrev-ref HEAD") || "unknown";
135
+ const branch = git(cwd, "rev-parse --abbrev-ref HEAD") || "unknown";
136
136
  // Ticket ID (prominent)
137
137
  const ticketId = extractTicketId(branch);
138
138
  if (ticketId) {
package/dist/lib/git.d.ts CHANGED
@@ -25,9 +25,7 @@ export declare function removeWorktree(branchName: string, repoRoot: string, for
25
25
  export declare function extractTicketId(branch: string): string | null;
26
26
  export declare function getWorktreePath(branchName: string): string | null;
27
27
  export declare function getWorktreeMetadata(worktreePath: string): {
28
- branch_name?: string;
29
28
  base_branch?: string;
30
- created_at?: string;
31
29
  } | null;
32
30
  export declare function hasUncommittedChanges(): boolean;
33
31
  export declare function hasStagedChanges(): boolean;
package/dist/lib/git.js CHANGED
@@ -130,7 +130,11 @@ export function getWorktreesDir(repoRoot) {
130
130
  return path.join(getSantreeDir(repoRoot), "worktrees");
131
131
  }
132
132
  export async function createWorktree(branchName, baseBranch, repoRoot) {
133
- const dirName = branchName.replace(/\//g, "__");
133
+ const ticketId = extractTicketId(branchName);
134
+ if (!ticketId) {
135
+ return { success: false, error: "No ticket ID found in branch name (expected pattern like TEAM-123)" };
136
+ }
137
+ const dirName = ticketId;
134
138
  const worktreesDir = getWorktreesDir(repoRoot);
135
139
  const worktreePath = path.join(worktreesDir, dirName);
136
140
  if (fs.existsSync(worktreePath)) {
@@ -164,9 +168,7 @@ export async function createWorktree(branchName, baseBranch, repoRoot) {
164
168
  }
165
169
  // Save metadata
166
170
  const metadata = {
167
- branch_name: branchName,
168
171
  base_branch: baseBranch,
169
- created_at: new Date().toISOString(),
170
172
  };
171
173
  fs.writeFileSync(path.join(worktreePath, ".santree_metadata.json"), JSON.stringify(metadata, null, 2));
172
174
  return { success: true, path: worktreePath };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "santree",
3
- "version": "0.0.12",
3
+ "version": "0.0.14",
4
4
  "description": "Git worktree manager with Linear integration",
5
5
  "license": "MIT",
6
6
  "author": "Santiago Toscanini",