santree 0.7.4 → 0.7.6

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.
Files changed (114) hide show
  1. package/README.md +2 -3
  2. package/dist/commands/config.d.ts +12 -0
  3. package/dist/commands/config.js +444 -0
  4. package/dist/commands/dashboard.js +187 -80
  5. package/dist/commands/helpers/statusline.js +0 -30
  6. package/dist/commands/issue/index.d.ts +1 -1
  7. package/dist/commands/issue/index.js +1 -1
  8. package/dist/commands/pr/context.d.ts +7 -0
  9. package/dist/commands/pr/context.js +36 -0
  10. package/dist/commands/pr/fix.d.ts +11 -2
  11. package/dist/commands/pr/fix.js +64 -12
  12. package/dist/commands/worktree/create.js +17 -11
  13. package/dist/commands/worktree/open.js +3 -2
  14. package/dist/commands/worktree/switch.js +3 -11
  15. package/dist/lib/ai.d.ts +11 -0
  16. package/dist/lib/ai.js +71 -2
  17. package/dist/lib/cd-hint.d.ts +18 -0
  18. package/dist/lib/cd-hint.js +27 -0
  19. package/dist/lib/claude-config.d.ts +26 -0
  20. package/dist/lib/claude-config.js +121 -0
  21. package/dist/lib/config/TrackerPicker.d.ts +10 -0
  22. package/dist/lib/config/TrackerPicker.js +113 -0
  23. package/dist/lib/config/diagnostics.d.ts +37 -0
  24. package/dist/lib/config/diagnostics.js +190 -0
  25. package/dist/lib/config-store.d.ts +23 -0
  26. package/dist/lib/config-store.js +68 -0
  27. package/dist/lib/dashboard/DetailPanel.d.ts +7 -2
  28. package/dist/lib/dashboard/DetailPanel.js +54 -37
  29. package/dist/lib/dashboard/IssueList.d.ts +5 -1
  30. package/dist/lib/dashboard/IssueList.js +24 -5
  31. package/dist/lib/dashboard/MultilineTextArea.js +1 -1
  32. package/dist/lib/dashboard/Overlays.js +3 -4
  33. package/dist/lib/dashboard/ReviewDetailPanel.js +11 -2
  34. package/dist/lib/dashboard/ReviewList.js +6 -3
  35. package/dist/lib/dashboard/data.js +7 -34
  36. package/dist/lib/dashboard/external-editor.d.ts +1 -1
  37. package/dist/lib/dashboard/external-editor.js +3 -2
  38. package/dist/lib/dashboard/types.d.ts +19 -2
  39. package/dist/lib/dashboard/types.js +68 -2
  40. package/dist/lib/exec.d.ts +13 -0
  41. package/dist/lib/exec.js +11 -1
  42. package/dist/lib/fix-loop.d.ts +45 -0
  43. package/dist/lib/fix-loop.js +83 -0
  44. package/dist/lib/git.d.ts +3 -29
  45. package/dist/lib/git.js +6 -62
  46. package/dist/lib/github.d.ts +70 -0
  47. package/dist/lib/github.js +128 -0
  48. package/dist/lib/multiplexer/cmux.js +143 -17
  49. package/dist/lib/multiplexer/none.js +3 -3
  50. package/dist/lib/multiplexer/tmux.js +5 -29
  51. package/dist/lib/multiplexer/types.d.ts +34 -1
  52. package/dist/lib/open-url.d.ts +3 -2
  53. package/dist/lib/open-url.js +5 -4
  54. package/dist/lib/prompts.d.ts +35 -1
  55. package/dist/lib/prompts.js +7 -0
  56. package/dist/lib/setup/apply.d.ts +16 -0
  57. package/dist/lib/setup/apply.js +35 -0
  58. package/dist/lib/setup/gitignore.d.ts +24 -0
  59. package/dist/lib/setup/gitignore.js +95 -0
  60. package/dist/lib/setup/steps.d.ts +39 -0
  61. package/dist/lib/setup/steps.js +333 -0
  62. package/dist/lib/setup/tools.d.ts +23 -0
  63. package/dist/lib/setup/tools.js +47 -0
  64. package/dist/lib/squirrel-loader.d.ts +3 -1
  65. package/dist/lib/squirrel-loader.js +2 -2
  66. package/dist/lib/trackers/github/index.js +2 -2
  67. package/dist/lib/trackers/local/store.js +1 -1
  68. package/package.json +1 -2
  69. package/prompts/fix-context.njk +79 -0
  70. package/prompts/fix-loop.njk +18 -0
  71. package/prompts/pr.njk +1 -1
  72. package/dist/commands/doctor.d.ts +0 -2
  73. package/dist/commands/doctor.js +0 -618
  74. package/dist/commands/github/auth.d.ts +0 -2
  75. package/dist/commands/github/auth.js +0 -56
  76. package/dist/commands/github/index.d.ts +0 -1
  77. package/dist/commands/github/index.js +0 -1
  78. package/dist/commands/helpers/english-tutor/index.d.ts +0 -1
  79. package/dist/commands/helpers/english-tutor/index.js +0 -1
  80. package/dist/commands/helpers/english-tutor/install.d.ts +0 -8
  81. package/dist/commands/helpers/english-tutor/install.js +0 -24
  82. package/dist/commands/helpers/english-tutor/prompt.d.ts +0 -2
  83. package/dist/commands/helpers/english-tutor/prompt.js +0 -16
  84. package/dist/commands/helpers/english-tutor/session-start.d.ts +0 -2
  85. package/dist/commands/helpers/english-tutor/session-start.js +0 -34
  86. package/dist/commands/helpers/english-tutor/uninstall.d.ts +0 -2
  87. package/dist/commands/helpers/english-tutor/uninstall.js +0 -15
  88. package/dist/commands/helpers/session-signal/end.d.ts +0 -2
  89. package/dist/commands/helpers/session-signal/end.js +0 -13
  90. package/dist/commands/helpers/session-signal/index.d.ts +0 -1
  91. package/dist/commands/helpers/session-signal/index.js +0 -1
  92. package/dist/commands/helpers/session-signal/install.d.ts +0 -8
  93. package/dist/commands/helpers/session-signal/install.js +0 -24
  94. package/dist/commands/helpers/session-signal/notification.d.ts +0 -2
  95. package/dist/commands/helpers/session-signal/notification.js +0 -13
  96. package/dist/commands/helpers/session-signal/prompt.d.ts +0 -2
  97. package/dist/commands/helpers/session-signal/prompt.js +0 -13
  98. package/dist/commands/helpers/session-signal/stop.d.ts +0 -2
  99. package/dist/commands/helpers/session-signal/stop.js +0 -13
  100. package/dist/commands/helpers/shell-init.d.ts +0 -11
  101. package/dist/commands/helpers/shell-init.js +0 -122
  102. package/dist/commands/issue/setup.d.ts +0 -2
  103. package/dist/commands/issue/setup.js +0 -108
  104. package/dist/commands/issue/switch.d.ts +0 -12
  105. package/dist/commands/issue/switch.js +0 -38
  106. package/dist/commands/linear/switch.d.ts +0 -2
  107. package/dist/commands/linear/switch.js +0 -75
  108. package/dist/lib/english-tutor.d.ts +0 -13
  109. package/dist/lib/english-tutor.js +0 -125
  110. package/dist/lib/session-signal.d.ts +0 -16
  111. package/dist/lib/session-signal.js +0 -104
  112. package/prompts/english-tutor-prompt.njk +0 -15
  113. package/shell/init.bash.njk +0 -129
  114. package/shell/init.zsh.njk +0 -204
@@ -1,618 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
- import { Box, Text } from "ink";
3
- import { useEffect, useState } from "react";
4
- import SquirrelLoader from "../lib/squirrel-loader.js";
5
- import { exec, execSync } from "child_process";
6
- import { promisify } from "util";
7
- import { createRequire } from "module";
8
- import * as fs from "fs";
9
- import * as path from "path";
10
- const require = createRequire(import.meta.url);
11
- const { version } = require("../../package.json");
12
- import { findMainRepoRoot, getSantreeDir, getInitScriptPath } from "../lib/git.js";
13
- import { getIssueTracker } from "../lib/trackers/index.js";
14
- import { getMultiplexer } from "../lib/multiplexer/index.js";
15
- import { resolveClaudeBinary } from "../lib/ai.js";
16
- import { CURRENT_VERSION, CLAUDE_CODE_PACKAGE, SANTREE_PACKAGE, getLatestVersionFor, isUpdateAvailable, detectPackageManager, getInstallCommandFor, } from "../lib/version.js";
17
- const execAsync = promisify(exec);
18
- export const description = "Check system requirements and integrations";
19
- /**
20
- * Executes a command asynchronously and returns the output, or null if it fails.
21
- */
22
- async function tryExec(command) {
23
- try {
24
- const { stdout } = await execAsync(command);
25
- return stdout.trim();
26
- }
27
- catch {
28
- return null;
29
- }
30
- }
31
- /**
32
- * Gets the path of a command using `which`.
33
- */
34
- async function getPath(command) {
35
- return tryExec(`which ${command}`);
36
- }
37
- /**
38
- * Checks if a tool is installed and gets its version.
39
- */
40
- async function checkTool(name, description, required, versionCommand, hint) {
41
- const path = await getPath(name);
42
- if (!path) {
43
- return {
44
- name,
45
- description,
46
- required,
47
- installed: false,
48
- hint,
49
- };
50
- }
51
- const version = await tryExec(versionCommand);
52
- return {
53
- name,
54
- description,
55
- required,
56
- installed: true,
57
- version: version || "unknown",
58
- path,
59
- };
60
- }
61
- /**
62
- * Reports the active multiplexer (tmux/cmux/none) and verifies the underlying
63
- * binary is reachable. Detection is auto — each adapter's `isActive()` checks
64
- * its own runtime env (`$TMUX`, `$CMUX_SURFACE_ID`).
65
- */
66
- async function checkMultiplexer() {
67
- const mux = getMultiplexer();
68
- const description = `Multiplexer (active: ${mux.kind})`;
69
- if (mux.kind === "none") {
70
- return {
71
- name: "multiplexer",
72
- description,
73
- required: false,
74
- installed: false,
75
- hint: "Run inside tmux or cmux to enable session window renaming. Install tmux: brew install tmux",
76
- };
77
- }
78
- if (mux.kind === "tmux") {
79
- const path = await getPath("tmux");
80
- if (!path) {
81
- return {
82
- name: "tmux",
83
- description,
84
- required: false,
85
- installed: false,
86
- hint: "Install: brew install tmux",
87
- };
88
- }
89
- const version = await tryExec("tmux -V");
90
- return {
91
- name: "tmux",
92
- description,
93
- required: false,
94
- installed: true,
95
- version: version || "unknown",
96
- path,
97
- };
98
- }
99
- // cmux
100
- const path = await getPath("cmux");
101
- if (!path) {
102
- return {
103
- name: "cmux",
104
- description,
105
- required: false,
106
- installed: false,
107
- hint: "Install cmux.app from https://cmux.com (cmux is macOS-only).",
108
- };
109
- }
110
- const version = await tryExec("cmux --version 2>/dev/null");
111
- const ping = await tryExec("cmux ping 2>/dev/null");
112
- // Note: cmux #1472 (programmatic workspaces with dead PTYs) is a real
113
- // limitation but only surfaces when a specific dashboard flow tries to
114
- // auto-execute a command in a freshly-created workspace. Showing it on
115
- // every doctor run made cmux look broken when it isn't — the limitation
116
- // is documented in CLAUDE.md and the README. We only flag a hint here
117
- // when cmux is actually unreachable.
118
- return {
119
- name: "cmux",
120
- description,
121
- required: false,
122
- installed: !!ping,
123
- version: version || "unknown",
124
- path,
125
- hint: !ping ? "cmux app not reachable — open cmux.app." : undefined,
126
- };
127
- }
128
- /**
129
- * Checks the Claude CLI, preferring cmux's bundled binary when running inside
130
- * cmux. The standard `checkTool` uses `which claude` which can't locate the
131
- * cmux shim at /Applications/cmux.app/Contents/Resources/bin/claude — that
132
- * binary isn't on PATH. See manaflow-ai/cmux#2048.
133
- */
134
- async function checkClaude() {
135
- const resolved = resolveClaudeBinary();
136
- const usingBundled = resolved?.startsWith("/Applications/cmux.app/") ?? false;
137
- const inCmux = getMultiplexer().kind === "cmux";
138
- const description = usingBundled ? "Claude Code CLI (cmux-bundled)" : "Claude Code CLI";
139
- if (!resolved) {
140
- return {
141
- name: "claude",
142
- description,
143
- required: true,
144
- installed: false,
145
- hint: inCmux
146
- ? "Open cmux.app to install its bundled Claude, or install standalone: npm install -g @anthropic-ai/claude-code"
147
- : "Install: npm install -g @anthropic-ai/claude-code",
148
- };
149
- }
150
- const version = await tryExec(`"${resolved}" --version 2>/dev/null | head -1`);
151
- return {
152
- name: "claude",
153
- description,
154
- required: true,
155
- installed: true,
156
- version: version || "unknown",
157
- path: resolved,
158
- };
159
- }
160
- /**
161
- * Checks GitHub CLI auth status.
162
- * Uses `gh api user` which works across all gh versions.
163
- */
164
- async function checkGhAuth() {
165
- const path = await getPath("gh");
166
- if (!path) {
167
- return {
168
- name: "gh",
169
- description: "GitHub CLI for PR operations",
170
- required: true,
171
- installed: false,
172
- hint: "Install: brew install gh && gh auth login",
173
- };
174
- }
175
- const version = await tryExec("gh --version | head -1");
176
- const login = await tryExec("gh api user --jq .login 2>/dev/null");
177
- if (!login) {
178
- return {
179
- name: "gh",
180
- description: "GitHub CLI for PR operations",
181
- required: true,
182
- installed: true,
183
- version: version || "unknown",
184
- path,
185
- hint: "Run: gh auth login",
186
- };
187
- }
188
- return {
189
- name: "gh",
190
- description: "GitHub CLI for PR operations",
191
- required: true,
192
- installed: true,
193
- version: version || "unknown",
194
- path,
195
- authStatus: `Authenticated as ${login}`,
196
- };
197
- }
198
- /**
199
- * Checks the active issue tracker's auth state. The tracker (Linear, GitHub)
200
- * is resolved from the repo's `_tracker` config (or env / auto-detect).
201
- * Doctor is the one place that legitimately names the active tracker — it's
202
- * diagnostic context, not a generic UI string.
203
- */
204
- async function checkTrackerAuth() {
205
- const repoRoot = findMainRepoRoot();
206
- const tracker = getIssueTracker(repoRoot);
207
- const status = await tracker.getAuthStatus(repoRoot);
208
- return {
209
- displayName: tracker.displayName,
210
- authenticated: status.authenticated,
211
- accountLabel: status.accountLabel,
212
- expiresAt: status.expiresAt,
213
- repoLinked: status.repoLinked,
214
- hint: status.hint,
215
- };
216
- }
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
- /**
228
- * Checks if Claude Code Remote Control is enabled for all sessions.
229
- * Remote Control lets you continue local sessions from any device.
230
- *
231
- * This reads from ~/.claude.json (the "global config" / application state file),
232
- * which is separate from ~/.claude/settings.json (the declarative settings file).
233
- * See: https://code.claude.com/docs/en/settings#settings-files
234
- */
235
- 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
249
- }
250
- return {
251
- enabled: false,
252
- hint: 'Run /config in Claude Code and enable "Enable Remote Control for all sessions"',
253
- };
254
- }
255
- /**
256
- * Checks statusline configuration:
257
- * If ~/.claude/settings.json has statusLine pointing to santree
258
- */
259
- 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
- }
280
- let hint;
281
- if (!claudeSettingsConfigured) {
282
- hint =
283
- 'Add to ~/.claude/settings.json: "statusLine": { "type": "command", "command": "santree helpers statusline" }';
284
- }
285
- return {
286
- claudeSettingsConfigured,
287
- currentCommand,
288
- hint,
289
- };
290
- }
291
- /**
292
- * Checks if session-signal hooks are configured in ~/.claude/settings.json.
293
- * Looks for hooks on Notification, Stop, UserPromptSubmit, and SessionEnd
294
- * that run "santree helpers session-signal".
295
- */
296
- 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
- }
328
- if (missingHooks.length === 0) {
329
- return { configured: true, missingHooks: [] };
330
- }
331
- return {
332
- configured: false,
333
- missingHooks,
334
- hint: `Missing: ${missingHooks.join(", ")}. Run: santree helpers session-signal install`,
335
- };
336
- }
337
- /**
338
- * Checks if english-tutor hooks are configured. Verifies the UserPromptSubmit
339
- * and SessionStart hooks plus the scoped Edit permission for the practice log.
340
- */
341
- function checkEnglishTutorHooks() {
342
- const home = process.env.HOME || "";
343
- const claudeSettingsPath = path.join(home, ".claude", "settings.json");
344
- const requiredEvents = ["UserPromptSubmit", "SessionStart"];
345
- const missingHooks = [];
346
- let missingPermission = true;
347
- const configDir = process.env.XDG_CONFIG_HOME || path.join(home, ".config");
348
- const expectedPermission = `Edit(${path.join(configDir, "santree", "english-practice-log.md")})`;
349
- try {
350
- if (fs.existsSync(claudeSettingsPath)) {
351
- const settings = JSON.parse(fs.readFileSync(claudeSettingsPath, "utf-8"));
352
- const hooks = settings.hooks || {};
353
- for (const event of requiredEvents) {
354
- const eventHooks = hooks[event];
355
- if (!Array.isArray(eventHooks)) {
356
- missingHooks.push(event);
357
- continue;
358
- }
359
- const found = eventHooks.some((entry) => {
360
- const innerHooks = entry.hooks || [];
361
- return innerHooks.some((h) => typeof h.command === "string" && h.command.includes("english-tutor"));
362
- });
363
- if (!found)
364
- missingHooks.push(event);
365
- }
366
- const allow = settings.permissions?.allow;
367
- if (Array.isArray(allow) && allow.includes(expectedPermission)) {
368
- missingPermission = false;
369
- }
370
- }
371
- else {
372
- missingHooks.push(...requiredEvents);
373
- }
374
- }
375
- catch {
376
- missingHooks.push(...requiredEvents);
377
- }
378
- if (missingHooks.length === 0 && !missingPermission) {
379
- return { configured: true, missingHooks: [], missingPermission: false };
380
- }
381
- return {
382
- configured: false,
383
- missingHooks,
384
- missingPermission,
385
- hint: "Run: santree helpers english-tutor install",
386
- };
387
- }
388
- /**
389
- * Checks if a path is gitignored (via .gitignore or .git/info/exclude).
390
- */
391
- function isGitIgnored(filePath, cwd) {
392
- try {
393
- execSync(`git check-ignore -q "${filePath}"`, { cwd, stdio: "ignore" });
394
- return true; // exit 0 = ignored
395
- }
396
- catch {
397
- return false; // exit 1 = not ignored
398
- }
399
- }
400
- /**
401
- * Checks if the current directory is a git repo and if .santree/init.sh exists and is executable.
402
- */
403
- function checkSantreeSetup() {
404
- const mainRepoRoot = findMainRepoRoot();
405
- if (!mainRepoRoot) {
406
- return {
407
- isGitRepo: false,
408
- santreeFolderExists: false,
409
- initShExists: false,
410
- initShExecutable: false,
411
- worktreesIgnored: false,
412
- metadataIgnored: false,
413
- hints: ["Not in a git repository"],
414
- };
415
- }
416
- const santreeDir = getSantreeDir(mainRepoRoot);
417
- const initShPath = getInitScriptPath(mainRepoRoot);
418
- const santreeFolderExists = fs.existsSync(santreeDir);
419
- const initShExists = fs.existsSync(initShPath);
420
- let initShExecutable = false;
421
- if (initShExists) {
422
- try {
423
- fs.accessSync(initShPath, fs.constants.X_OK);
424
- initShExecutable = true;
425
- }
426
- catch {
427
- initShExecutable = false;
428
- }
429
- }
430
- // Check gitignore status (use relative paths for git check-ignore)
431
- const worktreesIgnored = isGitIgnored(".santree/worktrees", mainRepoRoot);
432
- const metadataIgnored = isGitIgnored(".santree/metadata.json", mainRepoRoot);
433
- const hints = [];
434
- if (!santreeFolderExists) {
435
- hints.push(`Create .santree folder: mkdir ${santreeDir}`);
436
- }
437
- else if (!initShExists) {
438
- hints.push(`Create init.sh: touch ${initShPath} && chmod +x ${initShPath}`);
439
- }
440
- else if (!initShExecutable) {
441
- hints.push(`Make init.sh executable: chmod +x ${initShPath}`);
442
- }
443
- if (!worktreesIgnored) {
444
- hints.push("Add .santree/worktrees to .gitignore");
445
- }
446
- if (!metadataIgnored) {
447
- hints.push("Add .santree/metadata.json to .gitignore");
448
- }
449
- return {
450
- isGitRepo: true,
451
- mainRepoRoot,
452
- santreeFolderExists,
453
- initShExists,
454
- initShExecutable,
455
- worktreesIgnored,
456
- metadataIgnored,
457
- hints,
458
- };
459
- }
460
- function StatusIcon({ ok, required }) {
461
- if (ok) {
462
- return _jsx(Text, { color: "green", children: "\u2713" });
463
- }
464
- return required ? _jsx(Text, { color: "red", children: "\u2717" }) : _jsx(Text, { color: "yellow", children: "\u25CB" });
465
- }
466
- function ToolRow({ tool }) {
467
- return (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsxs(Box, { children: [_jsx(StatusIcon, { ok: tool.installed && !tool.hint, required: tool.required }), _jsx(Text, { children: " " }), _jsx(Text, { bold: true, children: tool.name }), _jsxs(Text, { dimColor: true, children: [" - ", tool.description] }), !tool.required && _jsx(Text, { dimColor: true, children: " (optional)" })] }), tool.installed ? (_jsxs(Box, { marginLeft: 2, flexDirection: "column", children: [_jsxs(Text, { dimColor: true, children: ["Version: ", tool.version] }), tool.latestVersion && tool.version && (_jsxs(Text, { color: isUpdateAvailable(tool.version, tool.latestVersion) ? "yellow" : undefined, dimColor: !isUpdateAvailable(tool.version, tool.latestVersion), children: ["Latest: ", tool.latestVersion, isUpdateAvailable(tool.version, tool.latestVersion) ? " ⬆ update available" : ""] })), tool.path && _jsxs(Text, { dimColor: true, children: ["Path: ", tool.path] }), tool.authStatus && _jsxs(Text, { dimColor: true, children: ["Auth: ", tool.authStatus] }), tool.updateHint && _jsxs(Text, { color: "yellow", children: ["\u21B3 ", tool.updateHint] }), tool.hint && _jsxs(Text, { color: "yellow", children: ["\u21B3 ", tool.hint] })] })) : (_jsx(Box, { marginLeft: 2, children: _jsxs(Text, { color: "yellow", children: ["\u21B3 ", tool.hint] }) }))] }));
468
- }
469
- function TrackerRow({ tracker }) {
470
- const isOk = tracker.authenticated && !tracker.hint;
471
- 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
- }
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
- function StatuslineRow({ status }) {
477
- 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
- }
479
- function RemoteControlRow({ status }) {
480
- 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] })] })] }));
481
- }
482
- function SessionSignalRow({ status }) {
483
- return (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsxs(Box, { children: [_jsx(StatusIcon, { ok: status.configured, required: false }), _jsx(Text, { children: " " }), _jsx(Text, { bold: true, children: "Session Signal Hooks" }), _jsx(Text, { dimColor: true, children: " - Surface session state in dashboard/tmux" }), _jsx(Text, { dimColor: true, children: " (optional)" })] }), _jsx(Box, { marginLeft: 2, flexDirection: "column", children: status.configured ? (_jsx(Text, { dimColor: true, children: "All hooks configured" })) : (_jsxs(_Fragment, { children: [_jsxs(Text, { dimColor: true, children: ["Missing: ", status.missingHooks.join(", ")] }), status.hint && _jsxs(Text, { color: "yellow", children: ["\u21B3 ", status.hint] })] })) })] }));
484
- }
485
- function EnglishTutorRow({ status }) {
486
- const missingParts = [];
487
- if (status.missingHooks.length > 0)
488
- missingParts.push(`hooks: ${status.missingHooks.join(", ")}`);
489
- if (status.missingPermission)
490
- missingParts.push("log Edit permission");
491
- return (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsxs(Box, { children: [_jsx(StatusIcon, { ok: status.configured, required: false }), _jsx(Text, { children: " " }), _jsx(Text, { bold: true, children: "English Tutor Hooks" }), _jsx(Text, { dimColor: true, children: " - Inline grammar corrections" }), _jsx(Text, { dimColor: true, children: " (optional)" })] }), _jsx(Box, { marginLeft: 2, flexDirection: "column", children: status.configured ? (_jsx(Text, { dimColor: true, children: "Hooks and log permission configured" })) : (_jsxs(_Fragment, { children: [_jsxs(Text, { dimColor: true, children: ["Missing: ", missingParts.join("; ") || "—"] }), status.hint && _jsxs(Text, { color: "yellow", children: ["\u21B3 ", status.hint] })] })) })] }));
492
- }
493
- function SantreeSetupRow({ status }) {
494
- const isOk = status.santreeFolderExists &&
495
- status.initShExists &&
496
- status.initShExecutable &&
497
- status.worktreesIgnored &&
498
- status.metadataIgnored;
499
- if (!status.isGitRepo) {
500
- return (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsxs(Box, { children: [_jsx(StatusIcon, { ok: false, required: false }), _jsx(Text, { children: " " }), _jsx(Text, { bold: true, children: "Repository Setup" }), _jsx(Text, { dimColor: true, children: " - .santree configuration" }), _jsx(Text, { dimColor: true, children: " (optional)" })] }), _jsx(Box, { marginLeft: 2, children: _jsx(Text, { dimColor: true, children: "Not in a git repository" }) })] }));
501
- }
502
- return (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsxs(Box, { children: [_jsx(StatusIcon, { ok: isOk, required: false }), _jsx(Text, { children: " " }), _jsx(Text, { bold: true, children: "Repository Setup" }), _jsx(Text, { dimColor: true, children: " - .santree configuration" }), _jsx(Text, { dimColor: true, children: " (optional)" })] }), _jsxs(Box, { marginLeft: 2, flexDirection: "column", children: [_jsxs(Text, { dimColor: true, children: ["Main repo: ", status.mainRepoRoot] }), _jsxs(Text, { dimColor: true, children: [".santree folder: ", status.santreeFolderExists ? "exists" : "missing"] }), status.santreeFolderExists && (_jsxs(Text, { dimColor: true, children: ["init.sh:", " ", status.initShExists
503
- ? status.initShExecutable
504
- ? "executable"
505
- : "not executable"
506
- : "missing"] })), _jsxs(Text, { dimColor: true, children: [".santree/worktrees ignored: ", status.worktreesIgnored ? "yes" : "no"] }), _jsxs(Text, { dimColor: true, children: [".santree/metadata.json ignored: ", status.metadataIgnored ? "yes" : "no"] }), status.hints.map((hint, i) => (_jsxs(Text, { color: "yellow", children: ["\u21B3 ", hint] }, i)))] })] }));
507
- }
508
- export default function Doctor() {
509
- const [tools, setTools] = useState([]);
510
- const [tracker, setTracker] = useState(null);
511
- const [shellStatus, setShellStatus] = useState(null);
512
- const [remoteControl, setRemoteControl] = useState(null);
513
- const [statusline, setStatusline] = useState(null);
514
- const [sessionSignal, setSessionSignal] = useState(null);
515
- const [englishTutor, setEnglishTutor] = useState(null);
516
- const [santreeSetup, setSantreeSetup] = useState(null);
517
- const [loading, setLoading] = useState(true);
518
- useEffect(() => {
519
- async function runChecks() {
520
- const pm = detectPackageManager();
521
- const [results, latestSantree, latestClaude] = await Promise.all([
522
- Promise.all([
523
- checkTool("git", "Version control", true, "git --version | head -1", "Install: brew install git"),
524
- checkGhAuth(),
525
- checkMultiplexer(),
526
- checkClaude(),
527
- ]),
528
- getLatestVersionFor(SANTREE_PACKAGE),
529
- getLatestVersionFor(CLAUDE_CODE_PACKAGE),
530
- ]);
531
- // Synthetic row for santree itself — surfaces update status.
532
- const santreeRow = {
533
- name: "santree",
534
- description: "Santree CLI (this app)",
535
- required: true,
536
- installed: true,
537
- version: CURRENT_VERSION,
538
- latestVersion: latestSantree ?? undefined,
539
- updateHint: latestSantree && isUpdateAvailable(CURRENT_VERSION, latestSantree)
540
- ? "Run: santree update"
541
- : undefined,
542
- };
543
- results.unshift(santreeRow);
544
- // Augment the claude row with latest-version info from npm registry.
545
- // When the resolved binary is the cmux-bundled one, npm install can't
546
- // update it — the bundled binary is shipped inside cmux.app. Show a
547
- // cmux-aware hint instead of the generic npm command.
548
- const claudeRow = results.find((r) => r.name === "claude");
549
- if (claudeRow && claudeRow.installed && latestClaude) {
550
- claudeRow.latestVersion = latestClaude;
551
- if (claudeRow.version && isUpdateAvailable(claudeRow.version, latestClaude)) {
552
- const isCmuxBundled = !!claudeRow.path?.includes("/cmux.app/");
553
- if (isCmuxBundled) {
554
- claudeRow.updateHint = "Bundled with cmux — update cmux.app to get the latest Claude.";
555
- }
556
- else {
557
- const cmd = getInstallCommandFor(pm, `${CLAUDE_CODE_PACKAGE}@latest`);
558
- claudeRow.updateHint = `Run: ${cmd.display}`;
559
- }
560
- }
561
- }
562
- // Optional: a syntax-highlighting diff pager — used by `st worktree diff`
563
- // and the dashboard `v` overlay when SANTREE_DIFF_TOOL is set. Any
564
- // diff pager works (delta, diff-so-fancy, …); without one set, the
565
- // dashboard renders inline with santree's own colorizer and the CLI
566
- // falls back to git's default pager. Delta is the most popular
567
- // choice so we check for it as a convenience, but it is never a
568
- // hard dependency.
569
- const deltaCheck = await checkTool("delta", "Recommended diff pager — any diff pager works", false, "delta --version | head -1", "Optional — santree's built-in colorizer handles the dashboard overlay; the CLI falls back to git's default pager. Set SANTREE_DIFF_TOOL to override. To install delta: brew install git-delta");
570
- results.push(deltaCheck);
571
- // Optional: a `.code-workspace`-aware editor (VSCode or Cursor).
572
- // Santree itself works with any editor via $SANTREE_EDITOR — this
573
- // check exists only because the dashboard's `E workspace` shortcut
574
- // needs an editor that understands `.code-workspace` files. Missing
575
- // here just means the shortcut is hidden; everything else still works.
576
- const workspaceEditorDesc = "Workspace editor (`E workspace` shortcut)";
577
- const [codeCheck, cursorCheck] = await Promise.all([
578
- checkTool("code", workspaceEditorDesc, false, "code --version | head -1", ""),
579
- checkTool("cursor", workspaceEditorDesc, false, "cursor --version | head -1", ""),
580
- ]);
581
- if (codeCheck.installed) {
582
- results.push(codeCheck);
583
- }
584
- else if (cursorCheck.installed) {
585
- results.push(cursorCheck);
586
- }
587
- else {
588
- results.push({
589
- name: "code/cursor",
590
- description: workspaceEditorDesc,
591
- required: false,
592
- installed: false,
593
- hint: "Optional — santree works with any $SANTREE_EDITOR. Only needed for the dashboard's `.code-workspace` shortcut.",
594
- });
595
- }
596
- const trackerResult = await checkTrackerAuth();
597
- const statuslineResult = await checkStatusline();
598
- setTools(results);
599
- setTracker(trackerResult);
600
- setShellStatus(checkShellIntegration());
601
- setRemoteControl(checkRemoteControl());
602
- setStatusline(statuslineResult);
603
- setSessionSignal(checkSessionSignalHooks());
604
- setEnglishTutor(checkEnglishTutorHooks());
605
- setSantreeSetup(checkSantreeSetup());
606
- setLoading(false);
607
- }
608
- runChecks();
609
- }, []);
610
- if (loading) {
611
- return (_jsx(Box, { paddingY: 1, children: _jsx(SquirrelLoader, { text: "Checking system requirements..." }) }));
612
- }
613
- const requiredMissing = tools.filter((t) => t.required && (!t.installed || t.hint));
614
- const optionalMissing = tools.filter((t) => !t.required && !t.installed);
615
- 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"] }))] })) })] }));
618
- }
@@ -1,2 +0,0 @@
1
- export declare const description = "Authenticate with GitHub via the gh CLI";
2
- export default function GithubAuth(): import("react/jsx-runtime").JSX.Element;