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,16 +0,0 @@
1
- import { useEffect, useRef } from "react";
2
- import { renderPrompt } from "../../../lib/prompts.js";
3
- import { getLogPath } from "../../../lib/english-tutor.js";
4
- export const description = "English-tutor instruction (UserPromptSubmit hook)";
5
- export default function Prompt() {
6
- const hasRun = useRef(false);
7
- useEffect(() => {
8
- if (hasRun.current)
9
- return;
10
- hasRun.current = true;
11
- const text = renderPrompt("english-tutor-prompt", { logPath: getLogPath() });
12
- process.stdout.write(text);
13
- process.exit(0);
14
- }, []);
15
- return null;
16
- }
@@ -1,2 +0,0 @@
1
- export declare const description = "Replay practice log (SessionStart hook)";
2
- export default function SessionStart(): null;
@@ -1,34 +0,0 @@
1
- import * as fs from "fs";
2
- import { useEffect, useRef } from "react";
3
- import { getLogPath } from "../../../lib/english-tutor.js";
4
- export const description = "Replay practice log (SessionStart hook)";
5
- export default function SessionStart() {
6
- const hasRun = useRef(false);
7
- useEffect(() => {
8
- if (hasRun.current)
9
- return;
10
- hasRun.current = true;
11
- try {
12
- const logPath = getLogPath();
13
- if (fs.existsSync(logPath)) {
14
- const contents = fs.readFileSync(logPath, "utf-8").trim();
15
- const entryCount = (contents.match(/^- /gm) || []).length;
16
- if (contents.length > 0) {
17
- process.stdout.write(`[ENGLISH TUTOR — PRACTICE LOG REPLAY]\n\n${contents}\n\n--- LOG END ---\n`);
18
- // Only ask for a summary once there are enough entries to be useful.
19
- // Below that threshold the log is too sparse to surface real patterns,
20
- // and a forced summary would just noise up new sessions.
21
- if (entryCount >= 3) {
22
- process.stdout.write(`\n[ENGLISH TUTOR — SESSION-START INSTRUCTION]\n` +
23
- `At the very start of your first response in this session, briefly note any RECURRING patterns from the log above (1-3 short bullets, e.g. "you frequently drop articles", "watch for 'their' vs 'there'"). Skip this entirely if no clear patterns emerge or if the user's first message is purely a coding task. Do not list every entry — only patterns.\n`);
24
- }
25
- }
26
- }
27
- }
28
- catch {
29
- // hook must never block the session — silently no-op on read error
30
- }
31
- process.exit(0);
32
- }, []);
33
- return null;
34
- }
@@ -1,2 +0,0 @@
1
- export declare const description = "Remove English-tutor hooks from Claude Code settings";
2
- export default function Uninstall(): null;
@@ -1,15 +0,0 @@
1
- import { useEffect, useRef } from "react";
2
- import { uninstallHooks } from "../../../lib/english-tutor.js";
3
- export const description = "Remove English-tutor hooks from Claude Code settings";
4
- export default function Uninstall() {
5
- const hasRun = useRef(false);
6
- useEffect(() => {
7
- if (hasRun.current)
8
- return;
9
- hasRun.current = true;
10
- const settingsPath = uninstallHooks();
11
- process.stdout.write(`English-tutor hooks removed from ${settingsPath}\n`);
12
- process.exit(0);
13
- }, []);
14
- return null;
15
- }
@@ -1,2 +0,0 @@
1
- export declare const description = "Signal exited state (SessionEnd hook)";
2
- export default function End(): null;
@@ -1,13 +0,0 @@
1
- import { useEffect, useRef } from "react";
2
- import { signalState } from "../../../lib/session-signal.js";
3
- export const description = "Signal exited state (SessionEnd hook)";
4
- export default function End() {
5
- const hasRun = useRef(false);
6
- useEffect(() => {
7
- if (hasRun.current)
8
- return;
9
- hasRun.current = true;
10
- signalState("exited");
11
- }, []);
12
- return null;
13
- }
@@ -1 +0,0 @@
1
- export declare const description = "Signal session state for Claude Code hooks";
@@ -1 +0,0 @@
1
- export const description = "Signal session state for Claude Code hooks";
@@ -1,8 +0,0 @@
1
- import { z } from "zod";
2
- export declare const description = "Install session-signal 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 { getHooksJson, installHooks } from "../../../lib/session-signal.js";
4
- export const description = "Install session-signal 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
- const snippet = { hooks: getHooksJson() };
16
- process.stdout.write(JSON.stringify(snippet, null, 2) + "\n");
17
- process.exit(0);
18
- }
19
- const settingsPath = installHooks();
20
- process.stdout.write(`Session-signal hooks installed in ${settingsPath}\n`);
21
- process.exit(0);
22
- }, []);
23
- return null;
24
- }
@@ -1,2 +0,0 @@
1
- export declare const description = "Signal waiting state (Notification hook)";
2
- export default function Notification(): null;
@@ -1,13 +0,0 @@
1
- import { useEffect, useRef } from "react";
2
- import { signalState } from "../../../lib/session-signal.js";
3
- export const description = "Signal waiting state (Notification hook)";
4
- export default function Notification() {
5
- const hasRun = useRef(false);
6
- useEffect(() => {
7
- if (hasRun.current)
8
- return;
9
- hasRun.current = true;
10
- signalState("waiting");
11
- }, []);
12
- return null;
13
- }
@@ -1,2 +0,0 @@
1
- export declare const description = "Signal active state (UserPromptSubmit hook)";
2
- export default function Prompt(): null;
@@ -1,13 +0,0 @@
1
- import { useEffect, useRef } from "react";
2
- import { signalState } from "../../../lib/session-signal.js";
3
- export const description = "Signal active state (UserPromptSubmit hook)";
4
- export default function Prompt() {
5
- const hasRun = useRef(false);
6
- useEffect(() => {
7
- if (hasRun.current)
8
- return;
9
- hasRun.current = true;
10
- signalState("active");
11
- }, []);
12
- return null;
13
- }
@@ -1,2 +0,0 @@
1
- export declare const description = "Signal idle state (Stop hook)";
2
- export default function Stop(): null;
@@ -1,13 +0,0 @@
1
- import { useEffect, useRef } from "react";
2
- import { signalState } from "../../../lib/session-signal.js";
3
- export const description = "Signal idle state (Stop hook)";
4
- export default function Stop() {
5
- const hasRun = useRef(false);
6
- useEffect(() => {
7
- if (hasRun.current)
8
- return;
9
- hasRun.current = true;
10
- signalState("idle");
11
- }, []);
12
- return null;
13
- }
@@ -1,2 +0,0 @@
1
- export declare const description = "Pick and configure the issue tracker for this repo";
2
- export default function IssueSetup(): import("react/jsx-runtime").JSX.Element;
@@ -1,108 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useEffect, useState } from "react";
3
- import { Text, Box, useInput } from "ink";
4
- import Spinner from "ink-spinner";
5
- import { findMainRepoRoot } from "../../lib/git.js";
6
- import { setRepoTracker, getIssueTracker } from "../../lib/trackers/index.js";
7
- import { setRepoLinearOrg } from "../../lib/trackers/linear/index.js";
8
- import { readLinearAuthStore } from "../../lib/trackers/auth-store.js";
9
- import { getAuthenticatedUser, getCurrentRepoNwo } from "../../lib/trackers/github/auth.js";
10
- export const description = "Pick and configure the issue tracker for this repo";
11
- const TRACKERS = [
12
- { kind: "local", label: "Local", hint: "built-in, file-based — no account needed" },
13
- { kind: "linear", label: "Linear", hint: "OAuth workspace" },
14
- { kind: "github", label: "GitHub", hint: "GitHub Issues via gh CLI" },
15
- ];
16
- export default function IssueSetup() {
17
- const [phase, setPhase] = useState("checking");
18
- const [message, setMessage] = useState("");
19
- const [error, setError] = useState(null);
20
- const [trackerIdx, setTrackerIdx] = useState(0);
21
- const [orgs, setOrgs] = useState([]);
22
- const [orgIdx, setOrgIdx] = useState(0);
23
- const [repoRoot, setRepoRoot] = useState(null);
24
- useEffect(() => {
25
- const root = findMainRepoRoot();
26
- if (!root) {
27
- setError("Not inside a git repository");
28
- setPhase("error");
29
- return;
30
- }
31
- setRepoRoot(root);
32
- setPhase("pick-tracker");
33
- }, []);
34
- function finish(label) {
35
- setMessage(label);
36
- setPhase("done");
37
- }
38
- async function chooseTracker(kind) {
39
- const root = repoRoot;
40
- if (kind === "local") {
41
- setRepoTracker(root, "local");
42
- finish(`Active tracker for this repo: ${getIssueTracker(root).displayName}`);
43
- return;
44
- }
45
- if (kind === "linear") {
46
- const store = readLinearAuthStore();
47
- const list = Object.entries(store).map(([slug, tokens]) => ({
48
- slug,
49
- name: tokens.org_name,
50
- }));
51
- if (list.length === 0) {
52
- setError("No authenticated Linear workspaces. Run: santree linear auth");
53
- setPhase("error");
54
- return;
55
- }
56
- if (list.length === 1) {
57
- setRepoLinearOrg(root, list[0].slug);
58
- setRepoTracker(root, "linear");
59
- finish(`Linked to ${list[0].name} (${list[0].slug})`);
60
- return;
61
- }
62
- setOrgs(list);
63
- setOrgIdx(0);
64
- setPhase("pick-org");
65
- return;
66
- }
67
- // github
68
- setPhase("checking");
69
- const user = await getAuthenticatedUser();
70
- if (!user) {
71
- setError("GitHub CLI not authenticated. Run: santree github auth");
72
- setPhase("error");
73
- return;
74
- }
75
- setRepoTracker(root, "github");
76
- const nwo = await getCurrentRepoNwo(root);
77
- finish(`Active tracker: GitHub (@${user.login}${nwo ? ` · ${nwo}` : ""})`);
78
- }
79
- useInput((_input, key) => {
80
- if (phase === "pick-tracker") {
81
- if (key.upArrow)
82
- setTrackerIdx((i) => Math.max(0, i - 1));
83
- else if (key.downArrow)
84
- setTrackerIdx((i) => Math.min(TRACKERS.length - 1, i + 1));
85
- else if (key.return)
86
- void chooseTracker(TRACKERS[trackerIdx].kind);
87
- }
88
- else if (phase === "pick-org") {
89
- if (key.upArrow)
90
- setOrgIdx((i) => Math.max(0, i - 1));
91
- else if (key.downArrow)
92
- setOrgIdx((i) => Math.min(orgs.length - 1, i + 1));
93
- else if (key.return) {
94
- const org = orgs[orgIdx];
95
- setRepoLinearOrg(repoRoot, org.slug);
96
- setRepoTracker(repoRoot, "linear");
97
- finish(`Linked to ${org.name} (${org.slug})`);
98
- }
99
- }
100
- });
101
- useEffect(() => {
102
- if (phase === "done" || phase === "error") {
103
- const timer = setTimeout(() => process.exit(phase === "error" ? 1 : 0), 100);
104
- return () => clearTimeout(timer);
105
- }
106
- }, [phase]);
107
- return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, color: "cyan", children: "Issue tracker setup" }) }), phase === "checking" && (_jsxs(Box, { children: [_jsx(Text, { color: "cyan", children: _jsx(Spinner, { type: "dots" }) }), _jsx(Text, { children: " Checking\u2026" })] })), phase === "pick-tracker" && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { children: "Select the issue tracker for this repo:" }), _jsx(Box, { flexDirection: "column", marginTop: 1, children: TRACKERS.map((t, i) => (_jsxs(Text, { children: [i === trackerIdx ? (_jsx(Text, { color: "cyan", bold: true, children: "> " })) : (_jsx(Text, { children: " " })), t.label, " ", _jsxs(Text, { dimColor: true, children: ["\u2014 ", t.hint] })] }, t.kind))) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "\u2191/\u2193 to select, Enter to confirm" }) })] })), phase === "pick-org" && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { children: "Select a Linear workspace to link:" }), _jsx(Box, { flexDirection: "column", marginTop: 1, children: orgs.map((org, i) => (_jsxs(Text, { children: [i === orgIdx ? (_jsx(Text, { color: "cyan", bold: true, children: "> " })) : (_jsx(Text, { children: " " })), org.name, " (", org.slug, ")"] }, org.slug))) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "\u2191/\u2193 to select, Enter to confirm" }) })] })), phase === "done" && _jsxs(Text, { color: "green", children: ["\u2713 ", message] }), phase === "error" && (_jsxs(Text, { color: "red", bold: true, children: ["\u2717 ", error] }))] }));
108
- }
@@ -1,12 +0,0 @@
1
- import { z } from "zod/v4";
2
- export declare const description = "Switch the active issue tracker for this repo";
3
- export declare const args: z.ZodTuple<[z.ZodEnum<{
4
- linear: "linear";
5
- github: "github";
6
- local: "local";
7
- }>], null>;
8
- type Props = {
9
- args: z.infer<typeof args>;
10
- };
11
- export default function IssueSwitch({ args }: Props): import("react/jsx-runtime").JSX.Element;
12
- export {};
@@ -1,38 +0,0 @@
1
- import { jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useEffect, useState } from "react";
3
- import { Text, Box } from "ink";
4
- import { argument } from "pastel";
5
- import { z } from "zod/v4";
6
- import { findMainRepoRoot } from "../../lib/git.js";
7
- import { setRepoTracker, getIssueTracker } from "../../lib/trackers/index.js";
8
- export const description = "Switch the active issue tracker for this repo";
9
- export const args = z.tuple([
10
- z.enum(["linear", "github", "local"]).describe(argument({
11
- name: "kind",
12
- description: "Tracker kind: linear, github, or local (built-in)",
13
- })),
14
- ]);
15
- export default function IssueSwitch({ args }) {
16
- const [kind] = args;
17
- const [status, setStatus] = useState("switching");
18
- const [message, setMessage] = useState("");
19
- useEffect(() => {
20
- const repoRoot = findMainRepoRoot();
21
- if (!repoRoot) {
22
- setMessage("Not inside a git repository");
23
- setStatus("error");
24
- return;
25
- }
26
- setRepoTracker(repoRoot, kind);
27
- const tracker = getIssueTracker(repoRoot);
28
- setMessage(`Active tracker for this repo: ${tracker.displayName}`);
29
- setStatus("done");
30
- }, [kind]);
31
- useEffect(() => {
32
- if (status === "done" || status === "error") {
33
- const timer = setTimeout(() => process.exit(status === "error" ? 1 : 0), 50);
34
- return () => clearTimeout(timer);
35
- }
36
- }, [status]);
37
- return (_jsxs(Box, { padding: 1, children: [status === "done" && (_jsxs(Text, { color: "green", bold: true, children: ["\u2713 ", message] })), status === "error" && (_jsxs(Text, { color: "red", bold: true, children: ["\u2717 ", message] }))] }));
38
- }
@@ -1,2 +0,0 @@
1
- export declare const description = "Switch Linear workspace for this repo";
2
- export default function LinearSwitch(): import("react/jsx-runtime").JSX.Element;
@@ -1,75 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useEffect, useState } from "react";
3
- import { Text, Box, useInput } from "ink";
4
- import Spinner from "ink-spinner";
5
- import { findMainRepoRoot } from "../../lib/git.js";
6
- import { setRepoLinearOrg, getRepoLinearOrg } from "../../lib/trackers/linear/index.js";
7
- import { readLinearAuthStore } from "../../lib/trackers/auth-store.js";
8
- import { setRepoTracker } from "../../lib/trackers/index.js";
9
- export const description = "Switch Linear workspace for this repo";
10
- export default function LinearSwitch() {
11
- const [status, setStatus] = useState("checking");
12
- const [message, setMessage] = useState("");
13
- const [error, setError] = useState(null);
14
- const [choices, setChoices] = useState([]);
15
- const [selected, setSelected] = useState(0);
16
- const [currentOrg, setCurrentOrg] = useState(null);
17
- useInput((input, key) => {
18
- if (status !== "choosing")
19
- return;
20
- if (key.upArrow) {
21
- setSelected((s) => Math.max(0, s - 1));
22
- }
23
- else if (key.downArrow) {
24
- setSelected((s) => Math.min(choices.length - 1, s + 1));
25
- }
26
- else if (key.return) {
27
- const choice = choices[selected];
28
- const repoRoot = findMainRepoRoot();
29
- setRepoLinearOrg(repoRoot, choice.slug);
30
- setRepoTracker(repoRoot, "linear");
31
- setMessage(`Switched to ${choice.name} (${choice.slug})`);
32
- setStatus("done");
33
- }
34
- });
35
- useEffect(() => {
36
- async function run() {
37
- await new Promise((r) => setTimeout(r, 100));
38
- const repoRoot = findMainRepoRoot();
39
- if (!repoRoot) {
40
- setError("Not inside a git repository");
41
- setStatus("error");
42
- return;
43
- }
44
- const store = readLinearAuthStore();
45
- const orgs = Object.entries(store).map(([slug, tokens]) => ({
46
- slug,
47
- name: tokens.org_name,
48
- }));
49
- if (orgs.length === 0) {
50
- setError("No authenticated workspaces. Run: santree linear auth");
51
- setStatus("error");
52
- return;
53
- }
54
- if (orgs.length === 1) {
55
- const org = orgs[0];
56
- setRepoLinearOrg(repoRoot, org.slug);
57
- setRepoTracker(repoRoot, "linear");
58
- setMessage(`Linked to ${org.name} (${org.slug})`);
59
- setStatus("done");
60
- return;
61
- }
62
- setCurrentOrg(getRepoLinearOrg(repoRoot));
63
- setChoices(orgs);
64
- setStatus("choosing");
65
- }
66
- run();
67
- }, []);
68
- useEffect(() => {
69
- if (status === "done" || status === "error") {
70
- const timer = setTimeout(() => process.exit(status === "error" ? 1 : 0), 100);
71
- return () => clearTimeout(timer);
72
- }
73
- }, [status]);
74
- return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, color: "cyan", children: "Linear Switch" }) }), status === "checking" && (_jsxs(Box, { children: [_jsx(Text, { color: "cyan", children: _jsx(Spinner, { type: "dots" }) }), _jsx(Text, { children: " Checking..." })] })), status === "choosing" && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { children: "Select a workspace to link to this repo:" }), _jsx(Box, { flexDirection: "column", marginTop: 1, children: choices.map((org, i) => (_jsxs(Text, { children: [i === selected ? (_jsx(Text, { color: "cyan", bold: true, children: "> " })) : (_jsx(Text, { children: " " })), org.name, " (", org.slug, ")", org.slug === currentOrg && _jsx(Text, { dimColor: true, children: " (current)" })] }, org.slug))) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "\u2191/\u2193 to select, Enter to confirm" }) })] })), status === "done" && _jsxs(Text, { color: "green", children: ["\u2713 ", message] }), status === "error" && (_jsxs(Text, { color: "red", bold: true, children: ["\u2717 ", error] }))] }));
75
- }
@@ -1,11 +0,0 @@
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 {};
@@ -1,194 +0,0 @@
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
- }