santree 0.7.5 → 0.7.7

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 (109) 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 +165 -61
  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 +10 -2
  11. package/dist/commands/pr/fix.js +61 -12
  12. package/dist/commands/worktree/create.js +6 -1
  13. package/dist/commands/worktree/open.js +3 -2
  14. package/dist/lib/ai.d.ts +11 -0
  15. package/dist/lib/ai.js +71 -2
  16. package/dist/lib/claude-config.d.ts +18 -4
  17. package/dist/lib/claude-config.js +58 -23
  18. package/dist/lib/config/TrackerPicker.d.ts +10 -0
  19. package/dist/lib/config/TrackerPicker.js +113 -0
  20. package/dist/lib/config/diagnostics.d.ts +37 -0
  21. package/dist/lib/config/diagnostics.js +190 -0
  22. package/dist/lib/config-store.d.ts +23 -0
  23. package/dist/lib/config-store.js +68 -0
  24. package/dist/lib/dashboard/DetailPanel.d.ts +4 -2
  25. package/dist/lib/dashboard/DetailPanel.js +44 -37
  26. package/dist/lib/dashboard/IssueList.d.ts +5 -1
  27. package/dist/lib/dashboard/IssueList.js +24 -5
  28. package/dist/lib/dashboard/MultilineTextArea.js +1 -1
  29. package/dist/lib/dashboard/Overlays.js +3 -4
  30. package/dist/lib/dashboard/ReviewDetailPanel.js +11 -2
  31. package/dist/lib/dashboard/ReviewList.js +6 -3
  32. package/dist/lib/dashboard/data.js +7 -34
  33. package/dist/lib/dashboard/external-editor.d.ts +1 -1
  34. package/dist/lib/dashboard/external-editor.js +3 -2
  35. package/dist/lib/dashboard/types.d.ts +13 -2
  36. package/dist/lib/dashboard/types.js +33 -2
  37. package/dist/lib/exec.d.ts +13 -0
  38. package/dist/lib/exec.js +11 -1
  39. package/dist/lib/fix-loop.d.ts +45 -0
  40. package/dist/lib/fix-loop.js +83 -0
  41. package/dist/lib/git.d.ts +3 -29
  42. package/dist/lib/git.js +6 -62
  43. package/dist/lib/github.d.ts +70 -0
  44. package/dist/lib/github.js +128 -0
  45. package/dist/lib/multiplexer/cmux.js +143 -17
  46. package/dist/lib/multiplexer/none.js +3 -3
  47. package/dist/lib/multiplexer/tmux.js +5 -29
  48. package/dist/lib/multiplexer/types.d.ts +34 -1
  49. package/dist/lib/open-url.d.ts +3 -2
  50. package/dist/lib/open-url.js +5 -4
  51. package/dist/lib/prompts.d.ts +35 -1
  52. package/dist/lib/prompts.js +7 -0
  53. package/dist/lib/setup/apply.d.ts +2 -2
  54. package/dist/lib/setup/apply.js +2 -2
  55. package/dist/lib/setup/gitignore.d.ts +7 -0
  56. package/dist/lib/setup/gitignore.js +39 -1
  57. package/dist/lib/setup/steps.d.ts +6 -2
  58. package/dist/lib/setup/steps.js +65 -60
  59. package/dist/lib/setup/tools.d.ts +1 -1
  60. package/dist/lib/setup/tools.js +1 -1
  61. package/dist/lib/trackers/github/index.js +2 -2
  62. package/dist/lib/trackers/local/store.js +1 -1
  63. package/package.json +1 -1
  64. package/prompts/fix-context.njk +79 -0
  65. package/prompts/fix-loop.njk +18 -0
  66. package/prompts/pr.njk +1 -1
  67. package/dist/commands/doctor.d.ts +0 -2
  68. package/dist/commands/doctor.js +0 -545
  69. package/dist/commands/github/auth.d.ts +0 -2
  70. package/dist/commands/github/auth.js +0 -56
  71. package/dist/commands/github/index.d.ts +0 -1
  72. package/dist/commands/github/index.js +0 -1
  73. package/dist/commands/helpers/english-tutor/index.d.ts +0 -1
  74. package/dist/commands/helpers/english-tutor/index.js +0 -1
  75. package/dist/commands/helpers/english-tutor/install.d.ts +0 -8
  76. package/dist/commands/helpers/english-tutor/install.js +0 -24
  77. package/dist/commands/helpers/english-tutor/prompt.d.ts +0 -2
  78. package/dist/commands/helpers/english-tutor/prompt.js +0 -16
  79. package/dist/commands/helpers/english-tutor/session-start.d.ts +0 -2
  80. package/dist/commands/helpers/english-tutor/session-start.js +0 -34
  81. package/dist/commands/helpers/english-tutor/uninstall.d.ts +0 -2
  82. package/dist/commands/helpers/english-tutor/uninstall.js +0 -15
  83. package/dist/commands/helpers/session-signal/end.d.ts +0 -2
  84. package/dist/commands/helpers/session-signal/end.js +0 -13
  85. package/dist/commands/helpers/session-signal/index.d.ts +0 -1
  86. package/dist/commands/helpers/session-signal/index.js +0 -1
  87. package/dist/commands/helpers/session-signal/install.d.ts +0 -8
  88. package/dist/commands/helpers/session-signal/install.js +0 -24
  89. package/dist/commands/helpers/session-signal/notification.d.ts +0 -2
  90. package/dist/commands/helpers/session-signal/notification.js +0 -13
  91. package/dist/commands/helpers/session-signal/prompt.d.ts +0 -2
  92. package/dist/commands/helpers/session-signal/prompt.js +0 -13
  93. package/dist/commands/helpers/session-signal/stop.d.ts +0 -2
  94. package/dist/commands/helpers/session-signal/stop.js +0 -13
  95. package/dist/commands/issue/setup.d.ts +0 -2
  96. package/dist/commands/issue/setup.js +0 -108
  97. package/dist/commands/issue/switch.d.ts +0 -12
  98. package/dist/commands/issue/switch.js +0 -38
  99. package/dist/commands/linear/switch.d.ts +0 -2
  100. package/dist/commands/linear/switch.js +0 -75
  101. package/dist/commands/setup.d.ts +0 -11
  102. package/dist/commands/setup.js +0 -194
  103. package/dist/lib/english-tutor.d.ts +0 -13
  104. package/dist/lib/english-tutor.js +0 -125
  105. package/dist/lib/session-signal.d.ts +0 -16
  106. package/dist/lib/session-signal.js +0 -104
  107. package/dist/lib/setup/shell-config.d.ts +0 -38
  108. package/dist/lib/setup/shell-config.js +0 -131
  109. package/prompts/english-tutor-prompt.njk +0 -15
@@ -1,545 +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 { isStatuslineConfigured, getStatuslineCommand, isRemoteControlEnabled, missingSessionSignalHooks, } from "../lib/claude-config.js";
17
- import { CURRENT_VERSION, CLAUDE_CODE_PACKAGE, SANTREE_PACKAGE, getLatestVersionFor, isUpdateAvailable, detectPackageManager, getInstallCommandFor, } from "../lib/version.js";
18
- const execAsync = promisify(exec);
19
- export const description = "Check system requirements and integrations";
20
- /**
21
- * Executes a command asynchronously and returns the output, or null if it fails.
22
- */
23
- async function tryExec(command) {
24
- try {
25
- const { stdout } = await execAsync(command);
26
- return stdout.trim();
27
- }
28
- catch {
29
- return null;
30
- }
31
- }
32
- /**
33
- * Gets the path of a command using `which`.
34
- */
35
- async function getPath(command) {
36
- return tryExec(`which ${command}`);
37
- }
38
- /**
39
- * Checks if a tool is installed and gets its version.
40
- */
41
- async function checkTool(name, description, required, versionCommand, hint) {
42
- const path = await getPath(name);
43
- if (!path) {
44
- return {
45
- name,
46
- description,
47
- required,
48
- installed: false,
49
- hint,
50
- };
51
- }
52
- const version = await tryExec(versionCommand);
53
- return {
54
- name,
55
- description,
56
- required,
57
- installed: true,
58
- version: version || "unknown",
59
- path,
60
- };
61
- }
62
- /**
63
- * Reports the active multiplexer (tmux/cmux/none) and verifies the underlying
64
- * binary is reachable. Detection is auto — each adapter's `isActive()` checks
65
- * its own runtime env (`$TMUX`, `$CMUX_SURFACE_ID`).
66
- */
67
- async function checkMultiplexer() {
68
- const mux = getMultiplexer();
69
- const description = `Multiplexer (active: ${mux.kind})`;
70
- if (mux.kind === "none") {
71
- return {
72
- name: "multiplexer",
73
- description,
74
- required: false,
75
- installed: false,
76
- hint: "Run inside tmux or cmux to enable session window renaming. Install tmux: brew install tmux",
77
- };
78
- }
79
- if (mux.kind === "tmux") {
80
- const path = await getPath("tmux");
81
- if (!path) {
82
- return {
83
- name: "tmux",
84
- description,
85
- required: false,
86
- installed: false,
87
- hint: "Install: brew install tmux",
88
- };
89
- }
90
- const version = await tryExec("tmux -V");
91
- return {
92
- name: "tmux",
93
- description,
94
- required: false,
95
- installed: true,
96
- version: version || "unknown",
97
- path,
98
- };
99
- }
100
- // cmux
101
- const path = await getPath("cmux");
102
- if (!path) {
103
- return {
104
- name: "cmux",
105
- description,
106
- required: false,
107
- installed: false,
108
- hint: "Install cmux.app from https://cmux.com (cmux is macOS-only).",
109
- };
110
- }
111
- const version = await tryExec("cmux --version 2>/dev/null");
112
- const ping = await tryExec("cmux ping 2>/dev/null");
113
- // Note: cmux #1472 (programmatic workspaces with dead PTYs) is a real
114
- // limitation but only surfaces when a specific dashboard flow tries to
115
- // auto-execute a command in a freshly-created workspace. Showing it on
116
- // every doctor run made cmux look broken when it isn't — the limitation
117
- // is documented in CLAUDE.md and the README. We only flag a hint here
118
- // when cmux is actually unreachable.
119
- return {
120
- name: "cmux",
121
- description,
122
- required: false,
123
- installed: !!ping,
124
- version: version || "unknown",
125
- path,
126
- hint: !ping ? "cmux app not reachable — open cmux.app." : undefined,
127
- };
128
- }
129
- /**
130
- * Checks the Claude CLI, preferring cmux's bundled binary when running inside
131
- * cmux. The standard `checkTool` uses `which claude` which can't locate the
132
- * cmux shim at /Applications/cmux.app/Contents/Resources/bin/claude — that
133
- * binary isn't on PATH. See manaflow-ai/cmux#2048.
134
- */
135
- async function checkClaude() {
136
- const resolved = resolveClaudeBinary();
137
- const usingBundled = resolved?.startsWith("/Applications/cmux.app/") ?? false;
138
- const inCmux = getMultiplexer().kind === "cmux";
139
- const description = usingBundled ? "Claude Code CLI (cmux-bundled)" : "Claude Code CLI";
140
- if (!resolved) {
141
- return {
142
- name: "claude",
143
- description,
144
- required: true,
145
- installed: false,
146
- hint: inCmux
147
- ? "Open cmux.app to install its bundled Claude, or install standalone: npm install -g @anthropic-ai/claude-code"
148
- : "Install: npm install -g @anthropic-ai/claude-code",
149
- };
150
- }
151
- const version = await tryExec(`"${resolved}" --version 2>/dev/null | head -1`);
152
- return {
153
- name: "claude",
154
- description,
155
- required: true,
156
- installed: true,
157
- version: version || "unknown",
158
- path: resolved,
159
- };
160
- }
161
- /**
162
- * Checks GitHub CLI auth status.
163
- * Uses `gh api user` which works across all gh versions.
164
- */
165
- async function checkGhAuth() {
166
- const path = await getPath("gh");
167
- if (!path) {
168
- return {
169
- name: "gh",
170
- description: "GitHub CLI for PR operations",
171
- required: true,
172
- installed: false,
173
- hint: "Install: brew install gh && gh auth login",
174
- };
175
- }
176
- const version = await tryExec("gh --version | head -1");
177
- const login = await tryExec("gh api user --jq .login 2>/dev/null");
178
- if (!login) {
179
- return {
180
- name: "gh",
181
- description: "GitHub CLI for PR operations",
182
- required: true,
183
- installed: true,
184
- version: version || "unknown",
185
- path,
186
- hint: "Run: gh auth login",
187
- };
188
- }
189
- return {
190
- name: "gh",
191
- description: "GitHub CLI for PR operations",
192
- required: true,
193
- installed: true,
194
- version: version || "unknown",
195
- path,
196
- authStatus: `Authenticated as ${login}`,
197
- };
198
- }
199
- /**
200
- * Checks the active issue tracker's auth state. The tracker (Linear, GitHub)
201
- * is resolved from the repo's `_tracker` config (or env / auto-detect).
202
- * Doctor is the one place that legitimately names the active tracker — it's
203
- * diagnostic context, not a generic UI string.
204
- */
205
- async function checkTrackerAuth() {
206
- const repoRoot = findMainRepoRoot();
207
- const tracker = getIssueTracker(repoRoot);
208
- const status = await tracker.getAuthStatus(repoRoot);
209
- return {
210
- displayName: tracker.displayName,
211
- authenticated: status.authenticated,
212
- accountLabel: status.accountLabel,
213
- expiresAt: status.expiresAt,
214
- repoLinked: status.repoLinked,
215
- hint: status.hint,
216
- };
217
- }
218
- /**
219
- * Checks if Claude Code Remote Control is enabled for all sessions.
220
- * Remote Control lets you continue local sessions from any device.
221
- *
222
- * This reads from ~/.claude.json (the "global config" / application state file),
223
- * which is separate from ~/.claude/settings.json (the declarative settings file).
224
- * See: https://code.claude.com/docs/en/settings#settings-files
225
- */
226
- function checkRemoteControl() {
227
- if (isRemoteControlEnabled()) {
228
- return { enabled: true };
229
- }
230
- return {
231
- enabled: false,
232
- hint: 'Run `santree setup` (or /config in Claude Code → "Enable Remote Control for all sessions")',
233
- };
234
- }
235
- /**
236
- * Checks statusline configuration:
237
- * If ~/.claude/settings.json has statusLine pointing to santree
238
- */
239
- async function checkStatusline() {
240
- const claudeSettingsConfigured = isStatuslineConfigured();
241
- const currentCommand = getStatuslineCommand();
242
- let hint;
243
- if (!claudeSettingsConfigured) {
244
- hint =
245
- 'Run `santree setup`, or add to ~/.claude/settings.json: "statusLine": { "type": "command", "command": "santree helpers statusline" }';
246
- }
247
- return {
248
- claudeSettingsConfigured,
249
- currentCommand,
250
- hint,
251
- };
252
- }
253
- /**
254
- * Checks if session-signal hooks are configured in ~/.claude/settings.json.
255
- * Looks for hooks on Notification, Stop, UserPromptSubmit, and SessionEnd
256
- * that run "santree helpers session-signal".
257
- */
258
- function checkSessionSignalHooks() {
259
- const missingHooks = missingSessionSignalHooks();
260
- if (missingHooks.length === 0) {
261
- return { configured: true, missingHooks: [] };
262
- }
263
- return {
264
- configured: false,
265
- missingHooks,
266
- hint: `Missing: ${missingHooks.join(", ")}. Run: santree setup (or santree helpers session-signal install)`,
267
- };
268
- }
269
- /**
270
- * Checks if english-tutor hooks are configured. Verifies the UserPromptSubmit
271
- * and SessionStart hooks plus the scoped Edit permission for the practice log.
272
- */
273
- function checkEnglishTutorHooks() {
274
- const home = process.env.HOME || "";
275
- const claudeSettingsPath = path.join(home, ".claude", "settings.json");
276
- const requiredEvents = ["UserPromptSubmit", "SessionStart"];
277
- const missingHooks = [];
278
- let missingPermission = true;
279
- const configDir = process.env.XDG_CONFIG_HOME || path.join(home, ".config");
280
- const expectedPermission = `Edit(${path.join(configDir, "santree", "english-practice-log.md")})`;
281
- try {
282
- if (fs.existsSync(claudeSettingsPath)) {
283
- const settings = JSON.parse(fs.readFileSync(claudeSettingsPath, "utf-8"));
284
- const hooks = settings.hooks || {};
285
- for (const event of requiredEvents) {
286
- const eventHooks = hooks[event];
287
- if (!Array.isArray(eventHooks)) {
288
- missingHooks.push(event);
289
- continue;
290
- }
291
- const found = eventHooks.some((entry) => {
292
- const innerHooks = entry.hooks || [];
293
- return innerHooks.some((h) => typeof h.command === "string" && h.command.includes("english-tutor"));
294
- });
295
- if (!found)
296
- missingHooks.push(event);
297
- }
298
- const allow = settings.permissions?.allow;
299
- if (Array.isArray(allow) && allow.includes(expectedPermission)) {
300
- missingPermission = false;
301
- }
302
- }
303
- else {
304
- missingHooks.push(...requiredEvents);
305
- }
306
- }
307
- catch {
308
- missingHooks.push(...requiredEvents);
309
- }
310
- if (missingHooks.length === 0 && !missingPermission) {
311
- return { configured: true, missingHooks: [], missingPermission: false };
312
- }
313
- return {
314
- configured: false,
315
- missingHooks,
316
- missingPermission,
317
- hint: "Run: santree helpers english-tutor install",
318
- };
319
- }
320
- /**
321
- * Checks if a path is gitignored (via .gitignore or .git/info/exclude).
322
- */
323
- function isGitIgnored(filePath, cwd) {
324
- try {
325
- execSync(`git check-ignore -q "${filePath}"`, { cwd, stdio: "ignore" });
326
- return true; // exit 0 = ignored
327
- }
328
- catch {
329
- return false; // exit 1 = not ignored
330
- }
331
- }
332
- /**
333
- * Checks if the current directory is a git repo and if .santree/init.sh exists and is executable.
334
- */
335
- function checkSantreeSetup() {
336
- const mainRepoRoot = findMainRepoRoot();
337
- if (!mainRepoRoot) {
338
- return {
339
- isGitRepo: false,
340
- santreeFolderExists: false,
341
- initShExists: false,
342
- initShExecutable: false,
343
- worktreesIgnored: false,
344
- metadataIgnored: false,
345
- hints: ["Not in a git repository"],
346
- };
347
- }
348
- const santreeDir = getSantreeDir(mainRepoRoot);
349
- const initShPath = getInitScriptPath(mainRepoRoot);
350
- const santreeFolderExists = fs.existsSync(santreeDir);
351
- const initShExists = fs.existsSync(initShPath);
352
- let initShExecutable = false;
353
- if (initShExists) {
354
- try {
355
- fs.accessSync(initShPath, fs.constants.X_OK);
356
- initShExecutable = true;
357
- }
358
- catch {
359
- initShExecutable = false;
360
- }
361
- }
362
- // Check gitignore status (use relative paths for git check-ignore)
363
- const worktreesIgnored = isGitIgnored(".santree/worktrees", mainRepoRoot);
364
- const metadataIgnored = isGitIgnored(".santree/metadata.json", mainRepoRoot);
365
- const hints = [];
366
- if (!santreeFolderExists) {
367
- hints.push(`Create .santree folder: mkdir ${santreeDir}`);
368
- }
369
- else if (!initShExists) {
370
- hints.push(`Create init.sh: touch ${initShPath} && chmod +x ${initShPath}`);
371
- }
372
- else if (!initShExecutable) {
373
- hints.push(`Make init.sh executable: chmod +x ${initShPath}`);
374
- }
375
- if (!worktreesIgnored) {
376
- hints.push("Add .santree/worktrees to .gitignore");
377
- }
378
- if (!metadataIgnored) {
379
- hints.push("Add .santree/metadata.json to .gitignore");
380
- }
381
- return {
382
- isGitRepo: true,
383
- mainRepoRoot,
384
- santreeFolderExists,
385
- initShExists,
386
- initShExecutable,
387
- worktreesIgnored,
388
- metadataIgnored,
389
- hints,
390
- };
391
- }
392
- function StatusIcon({ ok, required }) {
393
- if (ok) {
394
- return _jsx(Text, { color: "green", children: "\u2713" });
395
- }
396
- return required ? _jsx(Text, { color: "red", children: "\u2717" }) : _jsx(Text, { color: "yellow", children: "\u25CB" });
397
- }
398
- function ToolRow({ tool }) {
399
- 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] }) }))] }));
400
- }
401
- function TrackerRow({ tracker }) {
402
- const isOk = tracker.authenticated && !tracker.hint;
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] }) }))] }));
404
- }
405
- function StatuslineRow({ status }) {
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] })] })] }));
407
- }
408
- function RemoteControlRow({ status }) {
409
- 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] })] })] }));
410
- }
411
- function SessionSignalRow({ status }) {
412
- 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] })] })) })] }));
413
- }
414
- function EnglishTutorRow({ status }) {
415
- const missingParts = [];
416
- if (status.missingHooks.length > 0)
417
- missingParts.push(`hooks: ${status.missingHooks.join(", ")}`);
418
- if (status.missingPermission)
419
- missingParts.push("log Edit permission");
420
- 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] })] })) })] }));
421
- }
422
- function SantreeSetupRow({ status }) {
423
- const isOk = status.santreeFolderExists &&
424
- status.initShExists &&
425
- status.initShExecutable &&
426
- status.worktreesIgnored &&
427
- status.metadataIgnored;
428
- if (!status.isGitRepo) {
429
- 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" }) })] }));
430
- }
431
- 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
432
- ? status.initShExecutable
433
- ? "executable"
434
- : "not executable"
435
- : "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)))] })] }));
436
- }
437
- export default function Doctor() {
438
- const [tools, setTools] = useState([]);
439
- const [tracker, setTracker] = useState(null);
440
- const [remoteControl, setRemoteControl] = useState(null);
441
- const [statusline, setStatusline] = useState(null);
442
- const [sessionSignal, setSessionSignal] = useState(null);
443
- const [englishTutor, setEnglishTutor] = useState(null);
444
- const [santreeSetup, setSantreeSetup] = useState(null);
445
- const [loading, setLoading] = useState(true);
446
- useEffect(() => {
447
- async function runChecks() {
448
- const pm = detectPackageManager();
449
- const [results, latestSantree, latestClaude] = await Promise.all([
450
- Promise.all([
451
- checkTool("git", "Version control", true, "git --version | head -1", "Install: brew install git"),
452
- checkGhAuth(),
453
- checkMultiplexer(),
454
- checkClaude(),
455
- ]),
456
- getLatestVersionFor(SANTREE_PACKAGE),
457
- getLatestVersionFor(CLAUDE_CODE_PACKAGE),
458
- ]);
459
- // Synthetic row for santree itself — surfaces update status.
460
- const santreeRow = {
461
- name: "santree",
462
- description: "Santree CLI (this app)",
463
- required: true,
464
- installed: true,
465
- version: CURRENT_VERSION,
466
- latestVersion: latestSantree ?? undefined,
467
- updateHint: latestSantree && isUpdateAvailable(CURRENT_VERSION, latestSantree)
468
- ? "Run: santree update"
469
- : undefined,
470
- };
471
- results.unshift(santreeRow);
472
- // Augment the claude row with latest-version info from npm registry.
473
- // When the resolved binary is the cmux-bundled one, npm install can't
474
- // update it — the bundled binary is shipped inside cmux.app. Show a
475
- // cmux-aware hint instead of the generic npm command.
476
- const claudeRow = results.find((r) => r.name === "claude");
477
- if (claudeRow && claudeRow.installed && latestClaude) {
478
- claudeRow.latestVersion = latestClaude;
479
- if (claudeRow.version && isUpdateAvailable(claudeRow.version, latestClaude)) {
480
- const isCmuxBundled = !!claudeRow.path?.includes("/cmux.app/");
481
- if (isCmuxBundled) {
482
- claudeRow.updateHint = "Bundled with cmux — update cmux.app to get the latest Claude.";
483
- }
484
- else {
485
- const cmd = getInstallCommandFor(pm, `${CLAUDE_CODE_PACKAGE}@latest`);
486
- claudeRow.updateHint = `Run: ${cmd.display}`;
487
- }
488
- }
489
- }
490
- // Optional: a syntax-highlighting diff pager — used by `santree worktree diff`
491
- // and the dashboard `v` overlay when SANTREE_DIFF_TOOL is set. Any
492
- // diff pager works (delta, diff-so-fancy, …); without one set, the
493
- // dashboard renders inline with santree's own colorizer and the CLI
494
- // falls back to git's default pager. Delta is the most popular
495
- // choice so we check for it as a convenience, but it is never a
496
- // hard dependency.
497
- 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");
498
- results.push(deltaCheck);
499
- // Optional: a `.code-workspace`-aware editor (VSCode or Cursor).
500
- // Santree itself works with any editor via $SANTREE_EDITOR — this
501
- // check exists only because the dashboard's `E workspace` shortcut
502
- // needs an editor that understands `.code-workspace` files. Missing
503
- // here just means the shortcut is hidden; everything else still works.
504
- const workspaceEditorDesc = "Workspace editor (`E workspace` shortcut)";
505
- const [codeCheck, cursorCheck] = await Promise.all([
506
- checkTool("code", workspaceEditorDesc, false, "code --version | head -1", ""),
507
- checkTool("cursor", workspaceEditorDesc, false, "cursor --version | head -1", ""),
508
- ]);
509
- if (codeCheck.installed) {
510
- results.push(codeCheck);
511
- }
512
- else if (cursorCheck.installed) {
513
- results.push(cursorCheck);
514
- }
515
- else {
516
- results.push({
517
- name: "code/cursor",
518
- description: workspaceEditorDesc,
519
- required: false,
520
- installed: false,
521
- hint: "Optional — santree works with any $SANTREE_EDITOR. Only needed for the dashboard's `.code-workspace` shortcut.",
522
- });
523
- }
524
- const trackerResult = await checkTrackerAuth();
525
- const statuslineResult = await checkStatusline();
526
- setTools(results);
527
- setTracker(trackerResult);
528
- setRemoteControl(checkRemoteControl());
529
- setStatusline(statuslineResult);
530
- setSessionSignal(checkSessionSignalHooks());
531
- setEnglishTutor(checkEnglishTutorHooks());
532
- setSantreeSetup(checkSantreeSetup());
533
- setLoading(false);
534
- }
535
- runChecks();
536
- }, []);
537
- if (loading) {
538
- return (_jsx(Box, { paddingY: 1, children: _jsx(SquirrelLoader, { text: "Checking system requirements..." }) }));
539
- }
540
- const requiredMissing = tools.filter((t) => t.required && (!t.installed || t.hint));
541
- const optionalMissing = tools.filter((t) => !t.required && !t.installed);
542
- const trackerOk = tracker?.authenticated && !tracker?.hint;
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"] }))] })) })] }));
545
- }
@@ -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;
@@ -1,56 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useEffect, useState } from "react";
3
- import { Text, Box } from "ink";
4
- import Spinner from "ink-spinner";
5
- import { exec } from "child_process";
6
- import { promisify } from "util";
7
- import { findMainRepoRoot } from "../../lib/git.js";
8
- import { getAuthenticatedUser } from "../../lib/trackers/github/auth.js";
9
- import { setRepoTracker } from "../../lib/trackers/index.js";
10
- const execAsync = promisify(exec);
11
- export const description = "Authenticate with GitHub via the gh CLI";
12
- export default function GithubAuth() {
13
- const [status, setStatus] = useState("checking");
14
- const [message, setMessage] = useState("");
15
- const [error, setError] = useState(null);
16
- useEffect(() => {
17
- async function run() {
18
- await new Promise((r) => setTimeout(r, 100));
19
- const repoRoot = findMainRepoRoot();
20
- if (!repoRoot) {
21
- setError("Not inside a git repository");
22
- setStatus("error");
23
- return;
24
- }
25
- let user = await getAuthenticatedUser();
26
- if (!user) {
27
- setStatus("logging-in");
28
- try {
29
- await execAsync("gh auth login -p https -h github.com -w", { stdio: "inherit" });
30
- }
31
- catch (e) {
32
- setError(e instanceof Error ? e.message : "gh auth login failed");
33
- setStatus("error");
34
- return;
35
- }
36
- user = await getAuthenticatedUser();
37
- if (!user) {
38
- setError("gh auth login completed but no authenticated user — try `gh auth status`");
39
- setStatus("error");
40
- return;
41
- }
42
- }
43
- setRepoTracker(repoRoot, "github");
44
- setMessage(`Authenticated as @${user.login}; this repo now uses GitHub Issues`);
45
- setStatus("done");
46
- }
47
- run();
48
- }, []);
49
- useEffect(() => {
50
- if (status === "done" || status === "error") {
51
- const timer = setTimeout(() => process.exit(status === "error" ? 1 : 0), 100);
52
- return () => clearTimeout(timer);
53
- }
54
- }, [status]);
55
- return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, color: "cyan", children: "GitHub Auth" }) }), status === "checking" && (_jsxs(Box, { children: [_jsx(Text, { color: "cyan", children: _jsx(Spinner, { type: "dots" }) }), _jsx(Text, { children: " Checking gh auth status..." })] })), status === "logging-in" && (_jsxs(Box, { children: [_jsx(Text, { color: "cyan", children: _jsx(Spinner, { type: "dots" }) }), _jsx(Text, { children: " Running `gh auth login` \u2014 follow the browser prompt..." })] })), status === "done" && (_jsxs(Text, { color: "green", bold: true, children: ["\u2713 ", message] })), status === "error" && (_jsxs(Text, { color: "red", bold: true, children: ["\u2717 ", error] }))] }));
56
- }
@@ -1 +0,0 @@
1
- export declare const description = "GitHub Issues tracker commands";
@@ -1 +0,0 @@
1
- export const description = "GitHub Issues tracker commands";
@@ -1 +0,0 @@
1
- export declare const description = "Nudge Claude to correct your English (Claude Code hooks)";
@@ -1 +0,0 @@
1
- export const description = "Nudge Claude to correct your English (Claude Code hooks)";
@@ -1,8 +0,0 @@
1
- import { z } from "zod";
2
- export declare const description = "Install English-tutor hooks into Claude Code settings";
3
- export declare const options: z.ZodObject<{
4
- dry: z.ZodOptional<z.ZodBoolean>;
5
- }, z.core.$strip>;
6
- export default function Install({ options: opts }: {
7
- options: z.infer<typeof options>;
8
- }): null;
@@ -1,24 +0,0 @@
1
- import { useEffect, useRef } from "react";
2
- import { z } from "zod";
3
- import { getInstallSnippet, installHooks } from "../../../lib/english-tutor.js";
4
- export const description = "Install English-tutor hooks into Claude Code settings";
5
- export const options = z.object({
6
- dry: z.boolean().optional().describe("Print the hooks JSON without writing"),
7
- });
8
- export default function Install({ options: opts }) {
9
- const hasRun = useRef(false);
10
- useEffect(() => {
11
- if (hasRun.current)
12
- return;
13
- hasRun.current = true;
14
- if (opts?.dry) {
15
- process.stdout.write(JSON.stringify(getInstallSnippet(), null, 2) + "\n");
16
- process.exit(0);
17
- }
18
- const { settingsPath, logPath } = installHooks();
19
- process.stdout.write(`English-tutor hooks installed in ${settingsPath}\n`);
20
- process.stdout.write(`Practice log: ${logPath}\n`);
21
- process.exit(0);
22
- }, []);
23
- return null;
24
- }
@@ -1,2 +0,0 @@
1
- export declare const description = "English-tutor instruction (UserPromptSubmit hook)";
2
- export default function Prompt(): null;