santree 0.7.4 → 0.7.5

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.
@@ -10,6 +10,7 @@ const require = createRequire(import.meta.url);
10
10
  const { version } = require("../../package.json");
11
11
  import { findMainRepoRoot, createWorktree, getDefaultBranch, getBaseBranch, hasInitScript, getInitScriptPath, removeWorktree, getDiffTool, getWorktreeStatus, stageFile, unstageFile, stageAll, unstageAll, discardFile, } from "../lib/git.js";
12
12
  import { run, spawnAsync } from "../lib/exec.js";
13
+ import { printCdHint } from "../lib/cd-hint.js";
13
14
  import { resolveAgentBinary, resolveClaudeBinary, fillCommitMessage, askTicketQuestion, } from "../lib/ai.js";
14
15
  import { readTriageInvestigateConfig, isTriageInvestigateConfigured, buildInvestigatePrompt, buildInvestigateCommand, } from "../lib/triage-config.js";
15
16
  import { getInstalledClaudeVersion } from "../lib/version.js";
@@ -1049,7 +1050,9 @@ export default function Dashboard() {
1049
1050
  ? `cd ${shellEscape(sessionCwd)} && ${bin} --resume ${sessionId}`
1050
1051
  : null;
1051
1052
  const contextArg = contextFile ? ` --context-file "${contextFile}"` : "";
1052
- const workCmd = mode === "plan" ? `st worktree work --plan${contextArg}` : `st worktree work${contextArg}`;
1053
+ const workCmd = mode === "plan"
1054
+ ? `santree worktree work --plan${contextArg}`
1055
+ : `santree worktree work${contextArg}`;
1053
1056
  const cmd = resumeCmd ?? workCmd;
1054
1057
  const mux = getMultiplexer();
1055
1058
  const selected = await mux.selectWindow(windowName);
@@ -1091,7 +1094,7 @@ export default function Dashboard() {
1091
1094
  });
1092
1095
  }
1093
1096
  }
1094
- // Delayed refresh to pick up session ID created by `st worktree work`
1097
+ // Delayed refresh to pick up session ID created by `santree worktree work`
1095
1098
  setTimeout(() => refresh(), 3000);
1096
1099
  }, [refresh]);
1097
1100
  const launchAfterCreation = useCallback(async (mode, worktreePath, ticketId, contextFile) => {
@@ -1100,8 +1103,8 @@ export default function Dashboard() {
1100
1103
  const windowName = ticketId;
1101
1104
  const contextArg = contextFile ? ` --context-file "${contextFile}"` : "";
1102
1105
  const workCmd = mode === "plan"
1103
- ? `st worktree work --plan${contextArg}`
1104
- : `st worktree work${contextArg}`;
1106
+ ? `santree worktree work --plan${contextArg}`
1107
+ : `santree worktree work${contextArg}`;
1105
1108
  const created = await mux.createWindow({
1106
1109
  name: windowName,
1107
1110
  cwd: worktreePath,
@@ -1123,10 +1126,10 @@ export default function Dashboard() {
1123
1126
  }
1124
1127
  else {
1125
1128
  leaveAltScreen();
1126
- console.log(`SANTREE_CD:${worktreePath}`);
1127
- console.log(`SANTREE_WORK:${mode}`);
1128
- if (contextFile)
1129
- console.log(`SANTREE_WORK_CONTEXT:${contextFile}`);
1129
+ printCdHint({
1130
+ path: worktreePath,
1131
+ work: { mode, contextFile: contextFile || undefined },
1132
+ });
1130
1133
  exit();
1131
1134
  }
1132
1135
  }, [exit, refresh]);
@@ -1277,10 +1280,10 @@ export default function Dashboard() {
1277
1280
  }
1278
1281
  else {
1279
1282
  leaveAltScreen();
1280
- console.log(`SANTREE_CD:${di.worktree.path}`);
1281
- console.log(`SANTREE_WORK:${mode}`);
1282
- if (contextFile)
1283
- console.log(`SANTREE_WORK_CONTEXT:${contextFile}`);
1283
+ printCdHint({
1284
+ path: di.worktree.path,
1285
+ work: { mode, contextFile: contextFile || undefined },
1286
+ });
1284
1287
  exit();
1285
1288
  }
1286
1289
  }
@@ -2572,7 +2575,7 @@ export default function Dashboard() {
2572
2575
  const created = await mux.createWindow({
2573
2576
  name: windowName,
2574
2577
  cwd,
2575
- command: "st pr review",
2578
+ command: "santree pr review",
2576
2579
  });
2577
2580
  dispatch({
2578
2581
  type: "SET_ACTION_MESSAGE",
@@ -2584,7 +2587,7 @@ export default function Dashboard() {
2584
2587
  }
2585
2588
  else {
2586
2589
  leaveAltScreen();
2587
- console.log(`SANTREE_CD:${ri.worktree.path}`);
2590
+ printCdHint({ path: ri.worktree.path });
2588
2591
  exit();
2589
2592
  }
2590
2593
  return;
@@ -2849,7 +2852,7 @@ export default function Dashboard() {
2849
2852
  const selected = await mux.selectWindow(windowName);
2850
2853
  if (selected.ok)
2851
2854
  return;
2852
- const cmd = resumeCmd ?? "st worktree work";
2855
+ const cmd = resumeCmd ?? "santree worktree work";
2853
2856
  const created = await mux.createWindow({
2854
2857
  name: windowName,
2855
2858
  cwd: worktreePath,
@@ -2865,7 +2868,7 @@ export default function Dashboard() {
2865
2868
  }
2866
2869
  else {
2867
2870
  leaveAltScreen();
2868
- console.log(`SANTREE_CD:${di.worktree.path}`);
2871
+ printCdHint({ path: di.worktree.path });
2869
2872
  exit();
2870
2873
  }
2871
2874
  return;
@@ -2924,7 +2927,7 @@ export default function Dashboard() {
2924
2927
  const created = await mux.createWindow({
2925
2928
  name: windowName,
2926
2929
  cwd,
2927
- command: "st pr review",
2930
+ command: "santree pr review",
2928
2931
  });
2929
2932
  dispatch({
2930
2933
  type: "SET_ACTION_MESSAGE",
@@ -2936,7 +2939,7 @@ export default function Dashboard() {
2936
2939
  }
2937
2940
  else {
2938
2941
  leaveAltScreen();
2939
- console.log(`SANTREE_CD:${di.worktree.path}`);
2942
+ printCdHint({ path: di.worktree.path });
2940
2943
  exit();
2941
2944
  }
2942
2945
  return;
@@ -2993,7 +2996,7 @@ export default function Dashboard() {
2993
2996
  const created = await mux.createWindow({
2994
2997
  name: windowName,
2995
2998
  cwd,
2996
- command: "st pr fix",
2999
+ command: "santree pr fix",
2997
3000
  });
2998
3001
  dispatch({
2999
3002
  type: "SET_ACTION_MESSAGE",
@@ -3005,7 +3008,7 @@ export default function Dashboard() {
3005
3008
  }
3006
3009
  else {
3007
3010
  leaveAltScreen();
3008
- console.log(`SANTREE_CD:${di.worktree.path}`);
3011
+ printCdHint({ path: di.worktree.path });
3009
3012
  exit();
3010
3013
  }
3011
3014
  return;
@@ -3052,7 +3055,7 @@ export default function Dashboard() {
3052
3055
  });
3053
3056
  // ── Render ─────────────────────────────────────────────────────────
3054
3057
  if (state.loading) {
3055
- return (_jsx(Box, { width: columns, height: rows, flexDirection: "column", children: _jsx(Box, { justifyContent: "center", alignItems: "center", flexGrow: 1, children: _jsx(SquirrelLoader, { text: "Loading dashboard..." }) }) }));
3058
+ return (_jsx(Box, { width: columns, height: rows, flexDirection: "column", children: _jsx(Box, { justifyContent: "center", alignItems: "center", flexGrow: 1, children: _jsx(SquirrelLoader, { text: "Loading dashboard...", version: version }) }) }));
3056
3059
  }
3057
3060
  if (state.error) {
3058
3061
  return (_jsx(Box, { width: columns, height: rows, flexDirection: "column", children: _jsxs(Box, { justifyContent: "center", alignItems: "center", flexGrow: 1, flexDirection: "column", children: [_jsxs(Text, { color: "red", bold: true, children: ["Error: ", state.error] }), _jsx(Text, { dimColor: true, children: "Press R to retry or q to quit" })] }) }));
@@ -3152,7 +3155,7 @@ export default function Dashboard() {
3152
3155
  ? state.treeDetailScrollOffset
3153
3156
  : state.activeTab === "triage"
3154
3157
  ? state.triageDetailScrollOffset
3155
- : state.detailScrollOffset, height: contentHeight, width: rightWidth, creatingForTicket: state.creatingForTicket, creationLogs: state.creationLogs, deleteStatus: selectedDeleteStatus, triage: state.activeTab === "triage", comments: triageComments, onCall: onCall })) })] })), _jsx(Box, { children: state.overlay === "diff" ? (_jsx(Box, { width: innerWidth, paddingX: 1, children: _jsx(CommandBar, { showWorkspace: hasWorkspaceFile, mode: "diff" }) })) : state.overlay === "triage-schedule" ? (_jsx(Box, { width: innerWidth, paddingX: 1, children: _jsxs(Text, { children: [_jsx(Text, { color: "cyan", bold: true, children: "j/k" }), _jsx(Text, { dimColor: true, children: " scroll" }), _jsx(Text, { dimColor: true, children: " · " }), _jsx(Text, { color: "cyan", bold: true, children: "q" }), _jsx(Text, { dimColor: true, children: " close" })] }) })) : (_jsxs(_Fragment, { children: [_jsx(Box, { width: leftWidth + separatorWidth, paddingX: 1, children: _jsx(CommandBar, { showWorkspace: hasWorkspaceFile, mode: "default" }) }), _jsx(Box, { width: rightWidth, children: _jsx(ActionRow, { activeTab: state.activeTab, selectedIssue: selectedIssue, selectedReview: selectedReview, overlay: state.overlay, trackerName: activeTracker.displayName, canMutate: activeTracker.canMutate === true, triageInvestigateConfigured: triageInvestigateConfigured }) })] })) })] })] }));
3158
+ : state.detailScrollOffset, height: contentHeight, width: rightWidth, creatingForTicket: state.creatingForTicket, creationLogs: state.creationLogs, deleteStatus: selectedDeleteStatus, prSyncing: !!selectedIssue && !!state.pendingPrs[selectedIssue.issue.identifier], triage: state.activeTab === "triage", comments: triageComments, onCall: onCall })) })] })), _jsx(Box, { children: state.overlay === "diff" ? (_jsx(Box, { width: innerWidth, paddingX: 1, children: _jsx(CommandBar, { showWorkspace: hasWorkspaceFile, mode: "diff" }) })) : state.overlay === "triage-schedule" ? (_jsx(Box, { width: innerWidth, paddingX: 1, children: _jsxs(Text, { children: [_jsx(Text, { color: "cyan", bold: true, children: "j/k" }), _jsx(Text, { dimColor: true, children: " scroll" }), _jsx(Text, { dimColor: true, children: " · " }), _jsx(Text, { color: "cyan", bold: true, children: "q" }), _jsx(Text, { dimColor: true, children: " close" })] }) })) : (_jsxs(_Fragment, { children: [_jsx(Box, { width: leftWidth + separatorWidth, paddingX: 1, children: _jsx(CommandBar, { showWorkspace: hasWorkspaceFile, mode: "default" }) }), _jsx(Box, { width: rightWidth, children: _jsx(ActionRow, { activeTab: state.activeTab, selectedIssue: selectedIssue, selectedReview: selectedReview, overlay: state.overlay, trackerName: activeTracker.displayName, canMutate: activeTracker.canMutate === true, triageInvestigateConfigured: triageInvestigateConfigured }) })] })) })] })] }));
3156
3159
  }
3157
3160
  /**
3158
3161
  * Renders the per-issue action key hints (Resume / Editor / View diff / …)
@@ -13,6 +13,7 @@ import { findMainRepoRoot, getSantreeDir, getInitScriptPath } from "../lib/git.j
13
13
  import { getIssueTracker } from "../lib/trackers/index.js";
14
14
  import { getMultiplexer } from "../lib/multiplexer/index.js";
15
15
  import { resolveClaudeBinary } from "../lib/ai.js";
16
+ import { isStatuslineConfigured, getStatuslineCommand, isRemoteControlEnabled, missingSessionSignalHooks, } from "../lib/claude-config.js";
16
17
  import { CURRENT_VERSION, CLAUDE_CODE_PACKAGE, SANTREE_PACKAGE, getLatestVersionFor, isUpdateAvailable, detectPackageManager, getInstallCommandFor, } from "../lib/version.js";
17
18
  const execAsync = promisify(exec);
18
19
  export const description = "Check system requirements and integrations";
@@ -214,16 +215,6 @@ async function checkTrackerAuth() {
214
215
  hint: status.hint,
215
216
  };
216
217
  }
217
- /**
218
- * Checks if the shell integration is set up by looking for the
219
- * SANTREE_SHELL_INTEGRATION environment variable exported by the shell scripts.
220
- */
221
- function checkShellIntegration() {
222
- const shell = process.env.SHELL || "";
223
- const shellName = shell.includes("zsh") ? "zsh" : shell.includes("bash") ? "bash" : null;
224
- const configured = process.env.SANTREE_SHELL_INTEGRATION === "1";
225
- return { configured, shell: shellName };
226
- }
227
218
  /**
228
219
  * Checks if Claude Code Remote Control is enabled for all sessions.
229
220
  * Remote Control lets you continue local sessions from any device.
@@ -233,23 +224,12 @@ function checkShellIntegration() {
233
224
  * See: https://code.claude.com/docs/en/settings#settings-files
234
225
  */
235
226
  function checkRemoteControl() {
236
- const home = process.env.HOME || "";
237
- const configPath = path.join(home, ".claude.json");
238
- try {
239
- if (fs.existsSync(configPath)) {
240
- const content = fs.readFileSync(configPath, "utf-8");
241
- const config = JSON.parse(content);
242
- if (config.remoteControlAtStartup === true) {
243
- return { enabled: true };
244
- }
245
- }
246
- }
247
- catch {
248
- // JSON parse error or file read error
227
+ if (isRemoteControlEnabled()) {
228
+ return { enabled: true };
249
229
  }
250
230
  return {
251
231
  enabled: false,
252
- hint: 'Run /config in Claude Code and enable "Enable Remote Control for all sessions"',
232
+ hint: 'Run `santree setup` (or /config in Claude Code "Enable Remote Control for all sessions")',
253
233
  };
254
234
  }
255
235
  /**
@@ -257,30 +237,12 @@ function checkRemoteControl() {
257
237
  * If ~/.claude/settings.json has statusLine pointing to santree
258
238
  */
259
239
  async function checkStatusline() {
260
- const home = process.env.HOME || "";
261
- const claudeSettingsPath = path.join(home, ".claude", "settings.json");
262
- let claudeSettingsConfigured = false;
263
- let currentCommand;
264
- try {
265
- if (fs.existsSync(claudeSettingsPath)) {
266
- const content = fs.readFileSync(claudeSettingsPath, "utf-8");
267
- const settings = JSON.parse(content);
268
- if (settings.statusLine?.command) {
269
- currentCommand = String(settings.statusLine.command);
270
- // Check if it points to santree statusline
271
- claudeSettingsConfigured =
272
- currentCommand.includes("santree statusline") ||
273
- currentCommand.includes("santree helpers statusline");
274
- }
275
- }
276
- }
277
- catch {
278
- // JSON parse error or file read error
279
- }
240
+ const claudeSettingsConfigured = isStatuslineConfigured();
241
+ const currentCommand = getStatuslineCommand();
280
242
  let hint;
281
243
  if (!claudeSettingsConfigured) {
282
244
  hint =
283
- 'Add to ~/.claude/settings.json: "statusLine": { "type": "command", "command": "santree helpers statusline" }';
245
+ 'Run `santree setup`, or add to ~/.claude/settings.json: "statusLine": { "type": "command", "command": "santree helpers statusline" }';
284
246
  }
285
247
  return {
286
248
  claudeSettingsConfigured,
@@ -294,44 +256,14 @@ async function checkStatusline() {
294
256
  * that run "santree helpers session-signal".
295
257
  */
296
258
  function checkSessionSignalHooks() {
297
- const home = process.env.HOME || "";
298
- const claudeSettingsPath = path.join(home, ".claude", "settings.json");
299
- const requiredEvents = ["Notification", "Stop", "UserPromptSubmit", "SessionEnd"];
300
- const missingHooks = [];
301
- try {
302
- if (fs.existsSync(claudeSettingsPath)) {
303
- const content = fs.readFileSync(claudeSettingsPath, "utf-8");
304
- const settings = JSON.parse(content);
305
- const hooks = settings.hooks || {};
306
- for (const event of requiredEvents) {
307
- const eventHooks = hooks[event];
308
- if (!Array.isArray(eventHooks)) {
309
- missingHooks.push(event);
310
- continue;
311
- }
312
- // Check if any hook entry has a nested hook command containing session-signal
313
- const found = eventHooks.some((entry) => {
314
- const innerHooks = entry.hooks || [];
315
- return innerHooks.some((h) => typeof h.command === "string" && h.command.includes("session-signal"));
316
- });
317
- if (!found)
318
- missingHooks.push(event);
319
- }
320
- }
321
- else {
322
- missingHooks.push(...requiredEvents);
323
- }
324
- }
325
- catch {
326
- missingHooks.push(...requiredEvents);
327
- }
259
+ const missingHooks = missingSessionSignalHooks();
328
260
  if (missingHooks.length === 0) {
329
261
  return { configured: true, missingHooks: [] };
330
262
  }
331
263
  return {
332
264
  configured: false,
333
265
  missingHooks,
334
- hint: `Missing: ${missingHooks.join(", ")}. Run: santree helpers session-signal install`,
266
+ hint: `Missing: ${missingHooks.join(", ")}. Run: santree setup (or santree helpers session-signal install)`,
335
267
  };
336
268
  }
337
269
  /**
@@ -470,9 +402,6 @@ function TrackerRow({ tracker }) {
470
402
  const isOk = tracker.authenticated && !tracker.hint;
471
403
  return (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsxs(Box, { children: [_jsx(StatusIcon, { ok: isOk, required: true }), _jsx(Text, { children: " " }), _jsxs(Text, { bold: true, children: [tracker.displayName, " API"] }), _jsx(Text, { dimColor: true, children: " - Issue tracker integration" })] }), tracker.authenticated ? (_jsxs(Box, { marginLeft: 2, flexDirection: "column", children: [tracker.accountLabel && _jsxs(Text, { dimColor: true, children: ["Account: ", tracker.accountLabel] }), tracker.repoLinked !== undefined && (_jsxs(Text, { dimColor: true, children: ["Repo linked: ", tracker.repoLinked ? "yes" : "no"] })), tracker.hint && _jsxs(Text, { color: "yellow", children: ["\u21B3 ", tracker.hint] })] })) : (_jsx(Box, { marginLeft: 2, children: _jsxs(Text, { color: "yellow", children: ["\u21B3 ", tracker.hint] }) }))] }));
472
404
  }
473
- function ShellRow({ configured, shell }) {
474
- 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, ")\""] }) }))] }));
475
- }
476
405
  function StatuslineRow({ status }) {
477
406
  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] })] })] }));
478
407
  }
@@ -508,7 +437,6 @@ function SantreeSetupRow({ status }) {
508
437
  export default function Doctor() {
509
438
  const [tools, setTools] = useState([]);
510
439
  const [tracker, setTracker] = useState(null);
511
- const [shellStatus, setShellStatus] = useState(null);
512
440
  const [remoteControl, setRemoteControl] = useState(null);
513
441
  const [statusline, setStatusline] = useState(null);
514
442
  const [sessionSignal, setSessionSignal] = useState(null);
@@ -559,7 +487,7 @@ export default function Doctor() {
559
487
  }
560
488
  }
561
489
  }
562
- // Optional: a syntax-highlighting diff pager — used by `st worktree diff`
490
+ // Optional: a syntax-highlighting diff pager — used by `santree worktree diff`
563
491
  // and the dashboard `v` overlay when SANTREE_DIFF_TOOL is set. Any
564
492
  // diff pager works (delta, diff-so-fancy, …); without one set, the
565
493
  // dashboard renders inline with santree's own colorizer and the CLI
@@ -597,7 +525,6 @@ export default function Doctor() {
597
525
  const statuslineResult = await checkStatusline();
598
526
  setTools(results);
599
527
  setTracker(trackerResult);
600
- setShellStatus(checkShellIntegration());
601
528
  setRemoteControl(checkRemoteControl());
602
529
  setStatusline(statuslineResult);
603
530
  setSessionSignal(checkSessionSignalHooks());
@@ -613,6 +540,6 @@ export default function Doctor() {
613
540
  const requiredMissing = tools.filter((t) => t.required && (!t.installed || t.hint));
614
541
  const optionalMissing = tools.filter((t) => !t.required && !t.installed);
615
542
  const trackerOk = tracker?.authenticated && !tracker?.hint;
616
- const allRequired = requiredMissing.length === 0 && trackerOk && shellStatus?.configured;
617
- 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" }) }), tracker && _jsx(TrackerRow, { tracker: tracker }), 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 }), sessionSignal && _jsx(SessionSignalRow, { status: sessionSignal }), englishTutor && _jsx(EnglishTutorRow, { status: englishTutor }), _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 + (trackerOk ? 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"] }))] })) })] }));
543
+ const allRequired = requiredMissing.length === 0 && trackerOk;
544
+ 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" }) }), tracker && _jsx(TrackerRow, { tracker: tracker }), 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 }), sessionSignal && _jsx(SessionSignalRow, { status: sessionSignal }), englishTutor && _jsx(EnglishTutorRow, { status: englishTutor }), _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 + (trackerOk ? 0 : 1), " required item(s) need attention"] }), optionalMissing.length > 0 && (_jsxs(Text, { dimColor: true, children: [optionalMissing.length, " optional item(s) not installed"] }))] })) })] }));
618
545
  }
@@ -0,0 +1,11 @@
1
+ import { z } from "zod";
2
+ export declare const description = "Guided setup \u2014 configure editor, diff tool, Claude Code & this repo";
3
+ export declare const options: z.ZodObject<{
4
+ dryRun: z.ZodDefault<z.ZodBoolean>;
5
+ yes: z.ZodDefault<z.ZodBoolean>;
6
+ }, z.core.$strip>;
7
+ type Props = {
8
+ options: z.infer<typeof options>;
9
+ };
10
+ export default function Setup({ options: opts }: Props): import("react/jsx-runtime").JSX.Element;
11
+ export {};
@@ -0,0 +1,194 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useState } from "react";
3
+ import { Box, Text, useInput } from "ink";
4
+ import Spinner from "ink-spinner";
5
+ import { z } from "zod";
6
+ import { buildContext, buildSteps, } from "../lib/setup/steps.js";
7
+ export const description = "Guided setup — configure editor, diff tool, Claude Code & this repo";
8
+ export const options = z.object({
9
+ dryRun: z.boolean().default(false).describe("Preview what would change without writing anything"),
10
+ yes: z.boolean().default(false).describe("Apply all recommended steps non-interactively"),
11
+ });
12
+ export default function Setup({ options: opts }) {
13
+ const dryRun = opts.dryRun;
14
+ const [phase, setPhase] = useState("detecting");
15
+ const [steps, setSteps] = useState([]);
16
+ const [ctx, setCtx] = useState(null);
17
+ // select phase
18
+ const [cursor, setCursor] = useState(0);
19
+ const [selected, setSelected] = useState(new Set());
20
+ // configure phase
21
+ const [configQueue, setConfigQueue] = useState([]);
22
+ const [configPos, setConfigPos] = useState(0);
23
+ const [optionCursor, setOptionCursor] = useState(0);
24
+ const [choices, setChoices] = useState({});
25
+ // applying phase
26
+ const [applyIdx, setApplyIdx] = useState(0);
27
+ const [applyOrder, setApplyOrder] = useState([]);
28
+ const [applied, setApplied] = useState([]);
29
+ const [running, setRunning] = useState(null);
30
+ // ── Detect ────────────────────────────────────────────────────────────────
31
+ useEffect(() => {
32
+ (async () => {
33
+ await new Promise((r) => setTimeout(r, 80)); // let the spinner paint first
34
+ const c = buildContext(dryRun);
35
+ const all = buildSteps(c).filter((s) => s.detect === "actionable");
36
+ setCtx(c);
37
+ setSteps(all);
38
+ if (all.length === 0) {
39
+ setPhase("done");
40
+ return;
41
+ }
42
+ const rec = new Set();
43
+ all.forEach((s, i) => {
44
+ if (s.recommended)
45
+ rec.add(i);
46
+ });
47
+ setSelected(rec);
48
+ if (opts.yes) {
49
+ beginApply(all, rec, {});
50
+ }
51
+ else {
52
+ setPhase("select");
53
+ }
54
+ })();
55
+ }, []);
56
+ function beginApply(allSteps, sel, picked) {
57
+ const order = [...sel].sort((a, b) => a - b);
58
+ // For steps with options that weren't interactively chosen, default to the
59
+ // first option (used by --yes and single-option steps).
60
+ const merged = { ...picked };
61
+ for (const i of order) {
62
+ const s = allSteps[i];
63
+ if (s.options && s.options.length > 0 && merged[s.id] === undefined) {
64
+ merged[s.id] = s.options[0].value;
65
+ }
66
+ }
67
+ setChoices(merged);
68
+ setApplyOrder(order);
69
+ setApplyIdx(0);
70
+ setApplied([]);
71
+ setPhase("applying");
72
+ }
73
+ // ── Select phase input ──────────────────────────────────────────────────────
74
+ useInput((input, key) => {
75
+ if (key.escape)
76
+ process.exit(0);
77
+ else if (key.upArrow)
78
+ setCursor((c) => Math.max(0, c - 1));
79
+ else if (key.downArrow)
80
+ setCursor((c) => Math.min(steps.length - 1, c + 1));
81
+ else if (input === " ") {
82
+ setSelected((s) => {
83
+ const next = new Set(s);
84
+ if (next.has(cursor))
85
+ next.delete(cursor);
86
+ else
87
+ next.add(cursor);
88
+ return next;
89
+ });
90
+ }
91
+ else if (key.return) {
92
+ // Steps needing an interactive choice (>1 option).
93
+ const queue = [...selected]
94
+ .sort((a, b) => a - b)
95
+ .filter((i) => (steps[i].options?.length ?? 0) > 1);
96
+ if (queue.length > 0) {
97
+ setConfigQueue(queue);
98
+ setConfigPos(0);
99
+ setOptionCursor(0);
100
+ setPhase("configure");
101
+ }
102
+ else {
103
+ beginApply(steps, selected, {});
104
+ }
105
+ }
106
+ }, { isActive: phase === "select" });
107
+ // ── Configure phase input ───────────────────────────────────────────────────
108
+ useInput((_input, key) => {
109
+ if (key.escape) {
110
+ setPhase("select");
111
+ return;
112
+ }
113
+ const stepIdx = configQueue[configPos];
114
+ const step = steps[stepIdx];
115
+ const optCount = step.options.length;
116
+ if (key.upArrow)
117
+ setOptionCursor((c) => Math.max(0, c - 1));
118
+ else if (key.downArrow)
119
+ setOptionCursor((c) => Math.min(optCount - 1, c + 1));
120
+ else if (key.return) {
121
+ const value = step.options[optionCursor].value;
122
+ const nextChoices = { ...choices, [step.id]: value };
123
+ setChoices(nextChoices);
124
+ if (configPos + 1 < configQueue.length) {
125
+ setConfigPos((p) => p + 1);
126
+ setOptionCursor(0);
127
+ }
128
+ else {
129
+ beginApply(steps, selected, nextChoices);
130
+ }
131
+ }
132
+ }, { isActive: phase === "configure" });
133
+ // ── Applying phase ──────────────────────────────────────────────────────────
134
+ useEffect(() => {
135
+ if (phase !== "applying")
136
+ return;
137
+ if (applyIdx >= applyOrder.length) {
138
+ setRunning(null);
139
+ setPhase("done");
140
+ return;
141
+ }
142
+ const step = steps[applyOrder[applyIdx]];
143
+ setRunning(step.title);
144
+ let cancelled = false;
145
+ (async () => {
146
+ await new Promise((r) => setTimeout(r, 60)); // paint "running" frame
147
+ let result;
148
+ try {
149
+ result = await step.apply(choices[step.id]);
150
+ }
151
+ catch (e) {
152
+ result = { ok: false, message: e instanceof Error ? e.message : String(e) };
153
+ }
154
+ if (cancelled)
155
+ return;
156
+ setApplied((prev) => [...prev, { step, result }]);
157
+ setApplyIdx((i) => i + 1);
158
+ })();
159
+ return () => {
160
+ cancelled = true;
161
+ };
162
+ }, [phase, applyIdx]);
163
+ // ── Exit ────────────────────────────────────────────────────────────────────
164
+ useEffect(() => {
165
+ if (phase === "done") {
166
+ const t = setTimeout(() => process.exit(0), 120);
167
+ return () => clearTimeout(t);
168
+ }
169
+ }, [phase]);
170
+ // ── Render ──────────────────────────────────────────────────────────────────
171
+ const title = (_jsxs(Box, { marginBottom: 1, children: [_jsx(Text, { bold: true, color: "cyan", children: "santree setup" }), dryRun && _jsx(Text, { color: "yellow", children: " (dry run \u2014 nothing will be written)" })] }));
172
+ if (phase === "detecting") {
173
+ return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [title, _jsxs(Box, { children: [_jsx(Text, { color: "cyan", children: _jsx(Spinner, { type: "dots" }) }), _jsx(Text, { children: " Detecting your setup\u2026" })] })] }));
174
+ }
175
+ if (phase === "done" && steps.length === 0) {
176
+ return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [title, _jsx(Text, { color: "green", children: "\u2713 Everything's already configured \u2014 you're all set." }), !ctx?.repoRoot && (_jsx(Text, { dimColor: true, children: "Tip: run this again inside a git repo to configure repo-specific bits." }))] }));
177
+ }
178
+ if (phase === "select") {
179
+ let lastScope = null;
180
+ return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [title, _jsx(Text, { children: "What should I set up? (Space toggles, Enter applies)" }), _jsx(Box, { flexDirection: "column", marginTop: 1, children: steps.map((s, i) => {
181
+ const head = s.scope !== lastScope;
182
+ lastScope = s.scope;
183
+ const box = selected.has(i) ? "[x]" : "[ ]";
184
+ const isCur = i === cursor;
185
+ return (_jsxs(Box, { flexDirection: "column", children: [head && _jsx(Text, { dimColor: true, children: s.scope === "global" ? "Global" : "This repo" }), _jsxs(Text, { children: [_jsxs(Text, { color: isCur ? "cyan" : undefined, bold: isCur, children: [isCur ? "> " : " ", box] }), " ", s.title, s.kind === "spawn" && _jsx(Text, { color: "yellow", children: " \u2937 runs a command" })] }), isCur && _jsx(Text, { dimColor: true, children: " " + s.detail })] }, s.id));
186
+ }) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "\u2191/\u2193 move \u00B7 Space toggle \u00B7 Enter apply \u00B7 Esc cancel" }) })] }));
187
+ }
188
+ if (phase === "configure") {
189
+ const step = steps[configQueue[configPos]];
190
+ return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [title, _jsx(Text, { bold: true, children: step.title }), _jsx(Text, { children: step.optionPrompt || "Pick one:" }), _jsx(Box, { flexDirection: "column", marginTop: 1, children: step.options.map((o, i) => (_jsxs(Text, { children: [_jsx(Text, { color: i === optionCursor ? "cyan" : undefined, bold: i === optionCursor, children: i === optionCursor ? "> " : " " }), o.label] }, o.value))) }), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { dimColor: true, children: ["\u2191/\u2193 select \u00B7 Enter confirm", configQueue.length > 1 ? ` (${configPos + 1}/${configQueue.length})` : ""] }) })] }));
191
+ }
192
+ // applying / done
193
+ return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [title, applied.map(({ step, result }, i) => (_jsxs(Text, { color: result.ok ? "green" : "red", children: [result.ok ? "✓" : "✗", " ", step.title, " ", _jsxs(Text, { dimColor: true, children: ["\u2014 ", result.message] })] }, i))), phase === "applying" && running && (_jsxs(Box, { children: [_jsx(Text, { color: "cyan", children: _jsx(Spinner, { type: "dots" }) }), _jsxs(Text, { children: [" ", running, "\u2026"] })] })), phase === "done" && (_jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { color: "green", bold: true, children: "Done. Run `santree doctor` to verify." }), !dryRun && ctx?.shell && (_jsxs(Text, { dimColor: true, children: ["Restart your shell (or `source ", ctx.shell.rcPath, "`) to load the changes."] }))] }))] }));
194
+ }
@@ -6,6 +6,7 @@ import { z } from "zod";
6
6
  import * as fs from "fs";
7
7
  import { createWorktree, findMainRepoRoot, getDefaultBranch, pullLatest, hasInitScript, getInitScriptPath, extractTicketId, } from "../../lib/git.js";
8
8
  import { spawnAsync } from "../../lib/exec.js";
9
+ import { formatCdCommand } from "../../lib/cd-hint.js";
9
10
  import { getMultiplexer } from "../../lib/multiplexer/index.js";
10
11
  export const description = "Create a new worktree from a branch";
11
12
  export const options = z.object({
@@ -36,6 +37,7 @@ export default function Create({ options, args }) {
36
37
  const [worktreePath, setWorktreePath] = useState("");
37
38
  const [baseBranch, setBaseBranch] = useState(null);
38
39
  const [muxWindowName, setMuxWindowName] = useState(null);
40
+ const [cdHint, setCdHint] = useState(null);
39
41
  async function finalize(path, branch) {
40
42
  const wantsWindow = options.window || options.tmux;
41
43
  if (wantsWindow) {
@@ -43,7 +45,7 @@ export default function Create({ options, args }) {
43
45
  if (!mux.isActive()) {
44
46
  setMessage("Worktree created, but no active multiplexer");
45
47
  setStatus("done");
46
- console.log(`SANTREE_CD:${path}`);
48
+ setCdHint(formatCdCommand({ path }));
47
49
  return;
48
50
  }
49
51
  setStatus("spawning-window");
@@ -52,28 +54,27 @@ export default function Create({ options, args }) {
52
54
  setMuxWindowName(windowName);
53
55
  let runCommand;
54
56
  if (options.work) {
55
- runCommand = options.plan ? "st worktree work --plan" : "st worktree work";
57
+ runCommand = options.plan ? "santree worktree work --plan" : "santree worktree work";
56
58
  }
57
59
  const result = await mux.createWindow({ name: windowName, cwd: path, command: runCommand });
58
60
  if (!result.ok) {
59
61
  setMessage(`Worktree created, but failed to create window${result.message ? `: ${result.message}` : ""}`);
60
62
  setStatus("done");
61
- console.log(`SANTREE_CD:${path}`);
63
+ setCdHint(formatCdCommand({ path }));
62
64
  return;
63
65
  }
64
66
  setStatus("done");
65
67
  const workInfo = options.work ? (options.plan ? " + Claude (plan)" : " + Claude") : "";
66
68
  setMessage(`Worktree and window created!${workInfo}`);
67
- // Don't output SANTREE_CD when a window is created — user is already in the new window
69
+ // No cd hint when a window is created — the user is already in the new window
68
70
  return;
69
71
  }
70
72
  setStatus("done");
71
73
  setMessage("Worktree created successfully!");
72
- console.log(`SANTREE_CD:${path}`);
73
- if (options.work) {
74
- const mode = options.plan ? "plan" : "implement";
75
- console.log(`SANTREE_WORK:${mode}`);
76
- }
74
+ setCdHint(formatCdCommand({
75
+ path,
76
+ work: options.work ? { mode: options.plan ? "plan" : "implement" } : undefined,
77
+ }));
77
78
  }
78
79
  useEffect(() => {
79
80
  async function run() {
@@ -159,5 +160,5 @@ export default function Create({ options, args }) {
159
160
  status === "creating" ||
160
161
  status === "init-script" ||
161
162
  status === "spawning-window";
162
- return (_jsxs(Box, { flexDirection: "column", padding: 1, width: "100%", children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, color: "cyan", children: "\uD83C\uDF31 Create Worktree" }) }), branchName && (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: status === "error" ? "red" : status === "done" ? "green" : "blue", paddingX: 1, width: "100%", children: [_jsxs(Box, { gap: 1, children: [_jsx(Text, { dimColor: true, children: "branch:" }), _jsx(Text, { color: "cyan", bold: true, children: branchName })] }), baseBranch && (_jsxs(Box, { gap: 1, children: [_jsx(Text, { dimColor: true, children: "base:" }), _jsx(Text, { color: "blue", children: baseBranch })] })), options["no-pull"] && (_jsxs(Box, { gap: 1, children: [_jsx(Text, { dimColor: true, children: "skip pull:" }), _jsx(Text, { color: "yellow", children: "yes" })] })), options.work && (_jsxs(Box, { gap: 1, children: [_jsx(Text, { dimColor: true, children: "after:" }), _jsx(Text, { backgroundColor: "magenta", color: "white", children: options.plan ? " plan " : " work " })] })), (options.window || options.tmux) && (_jsxs(Box, { gap: 1, children: [_jsx(Text, { dimColor: true, children: "window:" }), _jsx(Text, { backgroundColor: "green", color: "white", children: ` ${options.name || "auto"} ` })] }))] })), _jsxs(Box, { marginTop: 1, children: [isLoading && (_jsxs(Box, { children: [_jsx(Text, { color: "cyan", children: _jsx(Spinner, { type: "dots" }) }), _jsxs(Text, { children: [" ", message] })] })), status === "done" && (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: "green", bold: true, children: ["\u2713 ", message] }), _jsxs(Text, { dimColor: true, children: [" ", worktreePath] }), muxWindowName && _jsxs(Text, { dimColor: true, children: [" window: ", muxWindowName] })] })), status === "error" && (_jsxs(Text, { color: "red", bold: true, children: ["\u2717 ", message] }))] })] }));
163
+ return (_jsxs(Box, { flexDirection: "column", padding: 1, width: "100%", children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, color: "cyan", children: "\uD83C\uDF31 Create Worktree" }) }), branchName && (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: status === "error" ? "red" : status === "done" ? "green" : "blue", paddingX: 1, width: "100%", children: [_jsxs(Box, { gap: 1, children: [_jsx(Text, { dimColor: true, children: "branch:" }), _jsx(Text, { color: "cyan", bold: true, children: branchName })] }), baseBranch && (_jsxs(Box, { gap: 1, children: [_jsx(Text, { dimColor: true, children: "base:" }), _jsx(Text, { color: "blue", children: baseBranch })] })), options["no-pull"] && (_jsxs(Box, { gap: 1, children: [_jsx(Text, { dimColor: true, children: "skip pull:" }), _jsx(Text, { color: "yellow", children: "yes" })] })), options.work && (_jsxs(Box, { gap: 1, children: [_jsx(Text, { dimColor: true, children: "after:" }), _jsx(Text, { backgroundColor: "magenta", color: "white", children: options.plan ? " plan " : " work " })] })), (options.window || options.tmux) && (_jsxs(Box, { gap: 1, children: [_jsx(Text, { dimColor: true, children: "window:" }), _jsx(Text, { backgroundColor: "green", color: "white", children: ` ${options.name || "auto"} ` })] }))] })), _jsxs(Box, { marginTop: 1, children: [isLoading && (_jsxs(Box, { children: [_jsx(Text, { color: "cyan", children: _jsx(Spinner, { type: "dots" }) }), _jsxs(Text, { children: [" ", message] })] })), status === "done" && (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: "green", bold: true, children: ["\u2713 ", message] }), _jsxs(Text, { dimColor: true, children: [" ", worktreePath] }), muxWindowName && _jsxs(Text, { dimColor: true, children: [" window: ", muxWindowName] }), cdHint && (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { dimColor: true, children: "\u2192 Run this to enter the worktree:" }), _jsxs(Text, { color: "cyan", children: [" ", cdHint] })] }))] })), status === "error" && (_jsxs(Text, { color: "red", bold: true, children: ["\u2717 ", message] }))] })] }));
163
164
  }