santree 0.7.5 → 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.
- package/README.md +2 -3
- package/dist/commands/config.d.ts +12 -0
- package/dist/commands/config.js +444 -0
- package/dist/commands/dashboard.js +165 -61
- package/dist/commands/helpers/statusline.js +0 -30
- package/dist/commands/issue/index.d.ts +1 -1
- package/dist/commands/issue/index.js +1 -1
- package/dist/commands/pr/context.d.ts +7 -0
- package/dist/commands/pr/context.js +36 -0
- package/dist/commands/pr/fix.d.ts +11 -2
- package/dist/commands/pr/fix.js +64 -12
- package/dist/commands/worktree/create.js +6 -1
- package/dist/commands/worktree/open.js +3 -2
- package/dist/lib/ai.d.ts +11 -0
- package/dist/lib/ai.js +71 -2
- package/dist/lib/claude-config.d.ts +18 -4
- package/dist/lib/claude-config.js +58 -23
- package/dist/lib/config/TrackerPicker.d.ts +10 -0
- package/dist/lib/config/TrackerPicker.js +113 -0
- package/dist/lib/config/diagnostics.d.ts +37 -0
- package/dist/lib/config/diagnostics.js +190 -0
- package/dist/lib/config-store.d.ts +23 -0
- package/dist/lib/config-store.js +68 -0
- package/dist/lib/dashboard/DetailPanel.d.ts +4 -2
- package/dist/lib/dashboard/DetailPanel.js +44 -37
- package/dist/lib/dashboard/IssueList.d.ts +5 -1
- package/dist/lib/dashboard/IssueList.js +24 -5
- package/dist/lib/dashboard/MultilineTextArea.js +1 -1
- package/dist/lib/dashboard/Overlays.js +3 -4
- package/dist/lib/dashboard/ReviewDetailPanel.js +11 -2
- package/dist/lib/dashboard/ReviewList.js +6 -3
- package/dist/lib/dashboard/data.js +7 -34
- package/dist/lib/dashboard/external-editor.d.ts +1 -1
- package/dist/lib/dashboard/external-editor.js +3 -2
- package/dist/lib/dashboard/types.d.ts +13 -2
- package/dist/lib/dashboard/types.js +33 -2
- package/dist/lib/exec.d.ts +13 -0
- package/dist/lib/exec.js +11 -1
- package/dist/lib/fix-loop.d.ts +45 -0
- package/dist/lib/fix-loop.js +83 -0
- package/dist/lib/git.d.ts +3 -29
- package/dist/lib/git.js +6 -62
- package/dist/lib/github.d.ts +70 -0
- package/dist/lib/github.js +128 -0
- package/dist/lib/multiplexer/cmux.js +143 -17
- package/dist/lib/multiplexer/none.js +3 -3
- package/dist/lib/multiplexer/tmux.js +5 -29
- package/dist/lib/multiplexer/types.d.ts +34 -1
- package/dist/lib/open-url.d.ts +3 -2
- package/dist/lib/open-url.js +5 -4
- package/dist/lib/prompts.d.ts +35 -1
- package/dist/lib/prompts.js +7 -0
- package/dist/lib/setup/apply.d.ts +2 -2
- package/dist/lib/setup/apply.js +2 -2
- package/dist/lib/setup/gitignore.d.ts +7 -0
- package/dist/lib/setup/gitignore.js +39 -1
- package/dist/lib/setup/steps.d.ts +6 -2
- package/dist/lib/setup/steps.js +65 -60
- package/dist/lib/setup/tools.d.ts +1 -1
- package/dist/lib/setup/tools.js +1 -1
- package/dist/lib/trackers/github/index.js +2 -2
- package/dist/lib/trackers/local/store.js +1 -1
- package/package.json +1 -1
- package/prompts/fix-context.njk +79 -0
- package/prompts/fix-loop.njk +18 -0
- package/prompts/pr.njk +1 -1
- package/dist/commands/doctor.d.ts +0 -2
- package/dist/commands/doctor.js +0 -545
- package/dist/commands/github/auth.d.ts +0 -2
- package/dist/commands/github/auth.js +0 -56
- package/dist/commands/github/index.d.ts +0 -1
- package/dist/commands/github/index.js +0 -1
- package/dist/commands/helpers/english-tutor/index.d.ts +0 -1
- package/dist/commands/helpers/english-tutor/index.js +0 -1
- package/dist/commands/helpers/english-tutor/install.d.ts +0 -8
- package/dist/commands/helpers/english-tutor/install.js +0 -24
- package/dist/commands/helpers/english-tutor/prompt.d.ts +0 -2
- package/dist/commands/helpers/english-tutor/prompt.js +0 -16
- package/dist/commands/helpers/english-tutor/session-start.d.ts +0 -2
- package/dist/commands/helpers/english-tutor/session-start.js +0 -34
- package/dist/commands/helpers/english-tutor/uninstall.d.ts +0 -2
- package/dist/commands/helpers/english-tutor/uninstall.js +0 -15
- package/dist/commands/helpers/session-signal/end.d.ts +0 -2
- package/dist/commands/helpers/session-signal/end.js +0 -13
- package/dist/commands/helpers/session-signal/index.d.ts +0 -1
- package/dist/commands/helpers/session-signal/index.js +0 -1
- package/dist/commands/helpers/session-signal/install.d.ts +0 -8
- package/dist/commands/helpers/session-signal/install.js +0 -24
- package/dist/commands/helpers/session-signal/notification.d.ts +0 -2
- package/dist/commands/helpers/session-signal/notification.js +0 -13
- package/dist/commands/helpers/session-signal/prompt.d.ts +0 -2
- package/dist/commands/helpers/session-signal/prompt.js +0 -13
- package/dist/commands/helpers/session-signal/stop.d.ts +0 -2
- package/dist/commands/helpers/session-signal/stop.js +0 -13
- package/dist/commands/issue/setup.d.ts +0 -2
- package/dist/commands/issue/setup.js +0 -108
- package/dist/commands/issue/switch.d.ts +0 -12
- package/dist/commands/issue/switch.js +0 -38
- package/dist/commands/linear/switch.d.ts +0 -2
- package/dist/commands/linear/switch.js +0 -75
- package/dist/commands/setup.d.ts +0 -11
- package/dist/commands/setup.js +0 -194
- package/dist/lib/english-tutor.d.ts +0 -13
- package/dist/lib/english-tutor.js +0 -125
- package/dist/lib/session-signal.d.ts +0 -16
- package/dist/lib/session-signal.js +0 -104
- package/dist/lib/setup/shell-config.d.ts +0 -38
- package/dist/lib/setup/shell-config.js +0 -131
- package/prompts/english-tutor-prompt.njk +0 -15
package/README.md
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
|
|
17
17
|
<p align="center">
|
|
18
18
|
Pick an issue, work on it with Claude in an isolated worktree, ship a PR — without leaving your terminal.<br/>
|
|
19
|
-
Pluggable issue trackers (Linear, GitHub Issues), pluggable multiplexers (
|
|
19
|
+
Pluggable issue trackers (Linear, GitHub Issues), pluggable multiplexers (cmux, tmux), and AI in the loop.
|
|
20
20
|
</p>
|
|
21
21
|
|
|
22
22
|
<p align="center">
|
|
@@ -31,8 +31,7 @@
|
|
|
31
31
|
|
|
32
32
|
```bash
|
|
33
33
|
npm install -g santree
|
|
34
|
-
|
|
35
|
-
santree doctor
|
|
34
|
+
santree config # inspect & set up tools, Claude Code integration, and this repo
|
|
36
35
|
```
|
|
37
36
|
|
|
38
37
|
Full setup: [Installation](https://santree.toscanini.me/installation.html).
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export declare const description = "Inspect and configure santree \u2014 tools, Claude Code integration, and this repo";
|
|
3
|
+
export declare const options: z.ZodObject<{
|
|
4
|
+
check: z.ZodDefault<z.ZodBoolean>;
|
|
5
|
+
yes: z.ZodDefault<z.ZodBoolean>;
|
|
6
|
+
dryRun: z.ZodDefault<z.ZodBoolean>;
|
|
7
|
+
}, z.core.$strip>;
|
|
8
|
+
type Props = {
|
|
9
|
+
options: z.infer<typeof options>;
|
|
10
|
+
};
|
|
11
|
+
export default function Config({ options: opts }: Props): import("react/jsx-runtime").JSX.Element;
|
|
12
|
+
export {};
|
|
@@ -0,0 +1,444 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useRef, 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
|
+
import { loadDiagnostics } from "../lib/config/diagnostics.js";
|
|
8
|
+
import { getConfiguredEditor, getConfiguredDiffTool } from "../lib/config-store.js";
|
|
9
|
+
import { pruneSessionSignalHooks } from "../lib/claude-config.js";
|
|
10
|
+
import TrackerPicker from "../lib/config/TrackerPicker.js";
|
|
11
|
+
import { findMainRepoRoot } from "../lib/git.js";
|
|
12
|
+
import { getActiveTrackerKind, isRepoTrackerConfigured } from "../lib/trackers/index.js";
|
|
13
|
+
import { getRepoLinearOrg } from "../lib/trackers/linear/index.js";
|
|
14
|
+
export const description = "Inspect and configure santree — tools, Claude Code integration, and this repo";
|
|
15
|
+
export const options = z.object({
|
|
16
|
+
check: z
|
|
17
|
+
.boolean()
|
|
18
|
+
.default(false)
|
|
19
|
+
.describe("Print a read-only status report and exit (non-interactive)"),
|
|
20
|
+
yes: z.boolean().default(false).describe("Apply all recommended changes non-interactively"),
|
|
21
|
+
dryRun: z.boolean().default(false).describe("Preview changes without writing anything"),
|
|
22
|
+
});
|
|
23
|
+
// Presentation scope — overrides the catalog's coarse global/repo so install
|
|
24
|
+
// tools land under "System" next to the read-only version rows.
|
|
25
|
+
const SYSTEM_STEP_IDS = new Set(["claude", "gh", "tmux"]);
|
|
26
|
+
const REPO_STEP_IDS = new Set(["tracker", "gitignore", "scaffold"]);
|
|
27
|
+
function presScope(id) {
|
|
28
|
+
if (SYSTEM_STEP_IDS.has(id))
|
|
29
|
+
return "system";
|
|
30
|
+
if (REPO_STEP_IDS.has(id))
|
|
31
|
+
return "repo";
|
|
32
|
+
return "global";
|
|
33
|
+
}
|
|
34
|
+
const ROW_ORDER = [
|
|
35
|
+
// System
|
|
36
|
+
"santree",
|
|
37
|
+
"node",
|
|
38
|
+
"git",
|
|
39
|
+
"multiplexer",
|
|
40
|
+
"claude",
|
|
41
|
+
"gh",
|
|
42
|
+
"tmux",
|
|
43
|
+
"workspace-editor",
|
|
44
|
+
// Global
|
|
45
|
+
"editor",
|
|
46
|
+
"diff-tool",
|
|
47
|
+
"statusline",
|
|
48
|
+
"remote-control",
|
|
49
|
+
// This repo
|
|
50
|
+
"tracker",
|
|
51
|
+
"gitignore",
|
|
52
|
+
"scaffold",
|
|
53
|
+
];
|
|
54
|
+
const SCOPE_TITLES = {
|
|
55
|
+
system: "System",
|
|
56
|
+
global: "Global (editor & Claude Code)",
|
|
57
|
+
repo: "This repo",
|
|
58
|
+
};
|
|
59
|
+
/** Current resolved value for the value-bearing select rows (env override → config file). */
|
|
60
|
+
function currentConfigValue(id) {
|
|
61
|
+
if (id === "editor")
|
|
62
|
+
return getConfiguredEditor();
|
|
63
|
+
if (id === "diff-tool")
|
|
64
|
+
return getConfiguredDiffTool();
|
|
65
|
+
return undefined;
|
|
66
|
+
}
|
|
67
|
+
function infoToRow(info) {
|
|
68
|
+
return {
|
|
69
|
+
id: info.id,
|
|
70
|
+
kind: "info",
|
|
71
|
+
title: info.title,
|
|
72
|
+
detail: info.description,
|
|
73
|
+
scope: info.scope,
|
|
74
|
+
ok: info.ok,
|
|
75
|
+
required: info.required,
|
|
76
|
+
recommended: false,
|
|
77
|
+
lines: info.lines,
|
|
78
|
+
hint: info.hint,
|
|
79
|
+
value: info.lines[0] ? compact(info.lines[0]) : undefined,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
function stepToRow(s, detail) {
|
|
83
|
+
const ok = s.detect === "ok";
|
|
84
|
+
let kind;
|
|
85
|
+
if (s.id === "tracker")
|
|
86
|
+
kind = "tracker";
|
|
87
|
+
else if (s.unapply)
|
|
88
|
+
kind = "toggle";
|
|
89
|
+
else if (s.options && s.options.length)
|
|
90
|
+
kind = "select";
|
|
91
|
+
else
|
|
92
|
+
kind = ok ? "info" : "action";
|
|
93
|
+
const row = {
|
|
94
|
+
id: s.id,
|
|
95
|
+
kind,
|
|
96
|
+
title: s.title,
|
|
97
|
+
detail: s.detail,
|
|
98
|
+
scope: presScope(s.id),
|
|
99
|
+
ok,
|
|
100
|
+
required: s.id === "gh" || s.id === "claude",
|
|
101
|
+
recommended: s.recommended,
|
|
102
|
+
on: s.unapply ? ok : undefined,
|
|
103
|
+
lines: detail?.lines ?? [],
|
|
104
|
+
hint: detail?.hint,
|
|
105
|
+
step: s,
|
|
106
|
+
};
|
|
107
|
+
if (kind === "select")
|
|
108
|
+
row.value = currentConfigValue(s.id) || (ok ? "set" : "not set");
|
|
109
|
+
if (kind === "tracker")
|
|
110
|
+
row.value = ok ? compact(detail?.lines?.[0] || "") : "not configured";
|
|
111
|
+
if (kind === "info" && detail?.lines?.[0])
|
|
112
|
+
row.value = compact(detail.lines[0]);
|
|
113
|
+
return row;
|
|
114
|
+
}
|
|
115
|
+
/** "Label: value" → "value"; otherwise the line unchanged. */
|
|
116
|
+
function compact(line) {
|
|
117
|
+
const idx = line.indexOf(": ");
|
|
118
|
+
return idx >= 0 ? line.slice(idx + 2) : line;
|
|
119
|
+
}
|
|
120
|
+
function buildRows(ctx, diag) {
|
|
121
|
+
const rows = [];
|
|
122
|
+
const steps = buildSteps(ctx).filter((s) => s.detect !== "unavailable");
|
|
123
|
+
for (const s of steps) {
|
|
124
|
+
if (s.id === "tmux" && diag.muxActive)
|
|
125
|
+
continue; // shown as the multiplexer info row
|
|
126
|
+
rows.push(stepToRow(s, diag.stepDetail.get(s.id)));
|
|
127
|
+
}
|
|
128
|
+
for (const info of diag.infoRows)
|
|
129
|
+
rows.push(infoToRow(info));
|
|
130
|
+
rows.sort((a, b) => {
|
|
131
|
+
const ia = ROW_ORDER.indexOf(a.id);
|
|
132
|
+
const ib = ROW_ORDER.indexOf(b.id);
|
|
133
|
+
return (ia < 0 ? 999 : ia) - (ib < 0 ? 999 : ib);
|
|
134
|
+
});
|
|
135
|
+
return rows;
|
|
136
|
+
}
|
|
137
|
+
function rowIcon(row) {
|
|
138
|
+
const on = row.kind === "toggle" ? !!row.on : row.ok;
|
|
139
|
+
if (on)
|
|
140
|
+
return { char: "✓", color: "green" };
|
|
141
|
+
return row.required ? { char: "✗", color: "red" } : { char: "○", color: "yellow" };
|
|
142
|
+
}
|
|
143
|
+
function rowSummary(row) {
|
|
144
|
+
switch (row.kind) {
|
|
145
|
+
case "toggle":
|
|
146
|
+
return row.on ? "on" : "off";
|
|
147
|
+
case "select":
|
|
148
|
+
return row.value || "";
|
|
149
|
+
case "action":
|
|
150
|
+
return row.required ? "missing" : "not installed";
|
|
151
|
+
case "tracker":
|
|
152
|
+
return row.value || "";
|
|
153
|
+
case "info":
|
|
154
|
+
return row.ok ? row.value || "✓" : "—";
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
/** Truncate to a visible width with an ellipsis. Inputs are plain (no ANSI). */
|
|
158
|
+
function clamp(s, w) {
|
|
159
|
+
if (w <= 0)
|
|
160
|
+
return "";
|
|
161
|
+
return s.length <= w ? s : s.slice(0, w - 1) + "…";
|
|
162
|
+
}
|
|
163
|
+
/** Per-row action hint shown in the footer when that row is focused. */
|
|
164
|
+
function footerVerb(row) {
|
|
165
|
+
switch (row?.kind) {
|
|
166
|
+
case "toggle":
|
|
167
|
+
return "Space toggle";
|
|
168
|
+
case "select":
|
|
169
|
+
return "Enter change";
|
|
170
|
+
case "action":
|
|
171
|
+
return "Enter run";
|
|
172
|
+
case "tracker":
|
|
173
|
+
return "Enter choose";
|
|
174
|
+
default:
|
|
175
|
+
return "read-only";
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
const PANEL_WIDTH = Math.max(40, Math.min((process.stdout.columns || 80) - 2, 96));
|
|
179
|
+
export default function Config({ options: opts }) {
|
|
180
|
+
const dryRun = opts.dryRun;
|
|
181
|
+
const ctxRef = useRef(null);
|
|
182
|
+
const diagRef = useRef(null);
|
|
183
|
+
const [mode, setMode] = useState("loading");
|
|
184
|
+
const [rows, setRows] = useState([]);
|
|
185
|
+
const [cursor, setCursor] = useState(0);
|
|
186
|
+
const [submenu, setSubmenu] = useState(null);
|
|
187
|
+
const [busy, setBusy] = useState(null);
|
|
188
|
+
const [toast, setToast] = useState(null);
|
|
189
|
+
// repo context for the tracker picker
|
|
190
|
+
const [repoRoot, setRepoRoot] = useState(null);
|
|
191
|
+
const [activeKind, setActiveKind] = useState(null);
|
|
192
|
+
const [activeOrg, setActiveOrg] = useState(null);
|
|
193
|
+
// Every row is focusable so you can inspect read-only rows too; Space/Enter
|
|
194
|
+
// just no-op on `info` rows. This keeps the cursor's travel predictable and
|
|
195
|
+
// lets the detail panel show full info for whatever's highlighted.
|
|
196
|
+
const selectable = rows;
|
|
197
|
+
const current = selectable[cursor];
|
|
198
|
+
// ── Initial load ────────────────────────────────────────────────────────────
|
|
199
|
+
useEffect(() => {
|
|
200
|
+
(async () => {
|
|
201
|
+
await new Promise((r) => setTimeout(r, 80));
|
|
202
|
+
const ctx = buildContext(dryRun);
|
|
203
|
+
ctxRef.current = ctx;
|
|
204
|
+
const diag = await loadDiagnostics();
|
|
205
|
+
diagRef.current = diag;
|
|
206
|
+
const built = buildRows(ctx, diag);
|
|
207
|
+
const root = findMainRepoRoot();
|
|
208
|
+
setRepoRoot(root);
|
|
209
|
+
if (root && isRepoTrackerConfigured(root)) {
|
|
210
|
+
const kind = getActiveTrackerKind(root);
|
|
211
|
+
setActiveKind(kind);
|
|
212
|
+
if (kind === "linear")
|
|
213
|
+
setActiveOrg(getRepoLinearOrg(root));
|
|
214
|
+
}
|
|
215
|
+
if (opts.check) {
|
|
216
|
+
setRows(built);
|
|
217
|
+
setMode("list"); // rendered read-only; effect below exits
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
// Self-heal: the session-state feature was removed, so strip any
|
|
221
|
+
// leftover session-signal hooks that would otherwise fire a missing
|
|
222
|
+
// command on every Claude event. Skipped in --check (read-only).
|
|
223
|
+
const prunedHooks = dryRun ? 0 : pruneSessionSignalHooks();
|
|
224
|
+
if (opts.yes) {
|
|
225
|
+
await applyRecommended(ctx, built);
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
setRows(built);
|
|
229
|
+
if (prunedHooks > 0) {
|
|
230
|
+
setToast({
|
|
231
|
+
ok: true,
|
|
232
|
+
message: `Removed ${prunedHooks} stale session-signal hook${prunedHooks === 1 ? "" : "s"} from Claude Code settings`,
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
setMode("list");
|
|
236
|
+
})();
|
|
237
|
+
}, []);
|
|
238
|
+
// ── --check / --yes exit ─────────────────────────────────────────────────────
|
|
239
|
+
useEffect(() => {
|
|
240
|
+
if (opts.check && mode === "list" && rows.length > 0) {
|
|
241
|
+
const t = setTimeout(() => {
|
|
242
|
+
const requiredMissing = rows.some((r) => r.required && !r.ok);
|
|
243
|
+
process.exit(requiredMissing ? 1 : 0);
|
|
244
|
+
}, 80);
|
|
245
|
+
return () => clearTimeout(t);
|
|
246
|
+
}
|
|
247
|
+
}, [mode, rows, opts.check]);
|
|
248
|
+
async function applyRecommended(ctx, built) {
|
|
249
|
+
// Non-interactive: apply recommended, reversible/file-kind steps only.
|
|
250
|
+
// Spawn installs and the tracker picker need a human, so we skip them.
|
|
251
|
+
const results = [];
|
|
252
|
+
for (const row of built) {
|
|
253
|
+
const s = row.step;
|
|
254
|
+
if (!s)
|
|
255
|
+
continue;
|
|
256
|
+
if (s.id === "tracker")
|
|
257
|
+
continue;
|
|
258
|
+
if (s.kind === "spawn")
|
|
259
|
+
continue;
|
|
260
|
+
if (s.detect !== "actionable" || !s.recommended)
|
|
261
|
+
continue;
|
|
262
|
+
const choice = s.options?.[0]?.value;
|
|
263
|
+
try {
|
|
264
|
+
results.push({ title: s.title, result: await s.apply(choice) });
|
|
265
|
+
}
|
|
266
|
+
catch (e) {
|
|
267
|
+
results.push({
|
|
268
|
+
title: s.title,
|
|
269
|
+
result: { ok: false, message: e instanceof Error ? e.message : String(e) },
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
setRows(results.map((r) => ({
|
|
274
|
+
id: r.title,
|
|
275
|
+
kind: "info",
|
|
276
|
+
title: r.title,
|
|
277
|
+
detail: "",
|
|
278
|
+
scope: "global",
|
|
279
|
+
ok: r.result.ok,
|
|
280
|
+
required: false,
|
|
281
|
+
recommended: false,
|
|
282
|
+
lines: [r.result.message],
|
|
283
|
+
})));
|
|
284
|
+
setMode("list");
|
|
285
|
+
setTimeout(() => process.exit(0), 100);
|
|
286
|
+
}
|
|
287
|
+
function rebuildRows() {
|
|
288
|
+
const ctx = ctxRef.current;
|
|
289
|
+
const diag = diagRef.current;
|
|
290
|
+
setRows(buildRows(ctx, diag));
|
|
291
|
+
}
|
|
292
|
+
async function applyStep(step, action, choice) {
|
|
293
|
+
setBusy(`${action === "unapply" ? "Disabling" : "Applying"} ${step.title}…`);
|
|
294
|
+
setToast(null);
|
|
295
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
296
|
+
let result;
|
|
297
|
+
try {
|
|
298
|
+
const fn = action === "unapply" ? step.unapply : step.apply;
|
|
299
|
+
result = await fn(choice);
|
|
300
|
+
}
|
|
301
|
+
catch (e) {
|
|
302
|
+
result = { ok: false, message: e instanceof Error ? e.message : String(e) };
|
|
303
|
+
}
|
|
304
|
+
// Spawn steps may have installed a tool — refresh version diagnostics.
|
|
305
|
+
if (step.kind === "spawn" || choice?.startsWith("install:")) {
|
|
306
|
+
diagRef.current = await loadDiagnostics();
|
|
307
|
+
}
|
|
308
|
+
rebuildRows();
|
|
309
|
+
setToast({ ok: result.ok, message: result.message });
|
|
310
|
+
setBusy(null);
|
|
311
|
+
}
|
|
312
|
+
function activate(row) {
|
|
313
|
+
if (row.kind === "tracker") {
|
|
314
|
+
setMode("tracker");
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
if (row.kind === "toggle") {
|
|
318
|
+
toggleRow(row);
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
if (row.kind === "select") {
|
|
322
|
+
const opts2 = row.step?.options ?? [];
|
|
323
|
+
if (opts2.length <= 1) {
|
|
324
|
+
void applyStep(row.step, "apply", opts2[0]?.value);
|
|
325
|
+
}
|
|
326
|
+
else {
|
|
327
|
+
setSubmenu({ row, cursor: 0 });
|
|
328
|
+
setMode("submenu");
|
|
329
|
+
}
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
if (row.kind === "action") {
|
|
333
|
+
void applyStep(row.step, "apply", row.step?.options?.[0]?.value);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
function toggleRow(row) {
|
|
337
|
+
const step = row.step;
|
|
338
|
+
if (row.on) {
|
|
339
|
+
void applyStep(step, "unapply", undefined);
|
|
340
|
+
}
|
|
341
|
+
else if (step.options && step.options.length > 1) {
|
|
342
|
+
// Turning on a step that needs a choice (e.g. gitignore target).
|
|
343
|
+
setSubmenu({ row, cursor: 0 });
|
|
344
|
+
setMode("submenu");
|
|
345
|
+
}
|
|
346
|
+
else {
|
|
347
|
+
void applyStep(step, "apply", step.options?.[0]?.value);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
// ── List input ────────────────────────────────────────────────────────────────
|
|
351
|
+
useInput((_input, key) => {
|
|
352
|
+
if (key.escape || key.leftArrow)
|
|
353
|
+
process.exit(0);
|
|
354
|
+
else if (key.upArrow) {
|
|
355
|
+
setToast(null);
|
|
356
|
+
setCursor((c) => Math.max(0, c - 1));
|
|
357
|
+
}
|
|
358
|
+
else if (key.downArrow) {
|
|
359
|
+
setToast(null);
|
|
360
|
+
setCursor((c) => Math.min(selectable.length - 1, c + 1));
|
|
361
|
+
}
|
|
362
|
+
else if (_input === " ") {
|
|
363
|
+
if (current?.kind === "toggle")
|
|
364
|
+
toggleRow(current);
|
|
365
|
+
}
|
|
366
|
+
else if (key.return || key.rightArrow) {
|
|
367
|
+
if (current)
|
|
368
|
+
activate(current);
|
|
369
|
+
}
|
|
370
|
+
}, { isActive: mode === "list" && !busy && !opts.check && !opts.yes });
|
|
371
|
+
// ── Submenu input ───────────────────────────────────────────────────────────
|
|
372
|
+
useInput((_input, key) => {
|
|
373
|
+
if (!submenu)
|
|
374
|
+
return;
|
|
375
|
+
const opts2 = submenu.row.step.options;
|
|
376
|
+
if (key.escape || key.leftArrow) {
|
|
377
|
+
setSubmenu(null);
|
|
378
|
+
setMode("list");
|
|
379
|
+
}
|
|
380
|
+
else if (key.upArrow) {
|
|
381
|
+
setSubmenu((s) => (s ? { ...s, cursor: Math.max(0, s.cursor - 1) } : s));
|
|
382
|
+
}
|
|
383
|
+
else if (key.downArrow) {
|
|
384
|
+
setSubmenu((s) => (s ? { ...s, cursor: Math.min(opts2.length - 1, s.cursor + 1) } : s));
|
|
385
|
+
}
|
|
386
|
+
else if (key.return) {
|
|
387
|
+
const choice = opts2[submenu.cursor].value;
|
|
388
|
+
const step = submenu.row.step;
|
|
389
|
+
setSubmenu(null);
|
|
390
|
+
setMode("list");
|
|
391
|
+
void applyStep(step, "apply", choice);
|
|
392
|
+
}
|
|
393
|
+
}, { isActive: mode === "submenu" && !busy });
|
|
394
|
+
// ── Render ──────────────────────────────────────────────────────────────────
|
|
395
|
+
const title = (_jsxs(Box, { marginBottom: 1, children: [_jsx(Text, { bold: true, color: "cyan", children: "santree config" }), dryRun && _jsx(Text, { color: "yellow", children: " (dry run \u2014 nothing will be written)" }), opts.check && _jsx(Text, { dimColor: true, children: " \u2014 status report" })] }));
|
|
396
|
+
if (mode === "loading") {
|
|
397
|
+
return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [title, _jsxs(Box, { children: [_jsx(Text, { color: "cyan", children: _jsx(Spinner, { type: "dots" }) }), _jsx(Text, { children: " Inspecting your setup\u2026" })] })] }));
|
|
398
|
+
}
|
|
399
|
+
if (opts.yes) {
|
|
400
|
+
return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [title, rows.length === 0 ? (_jsx(Text, { color: "green", children: "Nothing to apply \u2014 your setup already matches." })) : (rows.map((r) => (_jsxs(Text, { color: r.ok ? "green" : "red", children: [r.ok ? "✓" : "✗", " ", r.title, " ", _jsxs(Text, { dimColor: true, children: ["\u2014 ", r.lines[0]] })] }, r.id))))] }));
|
|
401
|
+
}
|
|
402
|
+
if (mode === "tracker") {
|
|
403
|
+
return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [title, _jsx(TrackerPicker, { repoRoot: repoRoot, activeKind: activeKind, activeOrg: activeOrg, onApplied: (message) => {
|
|
404
|
+
const root = repoRoot;
|
|
405
|
+
setActiveKind(getActiveTrackerKind(root));
|
|
406
|
+
setActiveOrg(getActiveTrackerKind(root) === "linear" ? getRepoLinearOrg(root) : null);
|
|
407
|
+
(async () => {
|
|
408
|
+
diagRef.current = await loadDiagnostics();
|
|
409
|
+
rebuildRows();
|
|
410
|
+
})();
|
|
411
|
+
setToast({ ok: true, message });
|
|
412
|
+
setMode("list");
|
|
413
|
+
}, onCancel: () => setMode("list") })] }));
|
|
414
|
+
}
|
|
415
|
+
if (mode === "submenu" && submenu) {
|
|
416
|
+
const step = submenu.row.step;
|
|
417
|
+
return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [title, _jsx(Text, { bold: true, children: step.title }), _jsx(Text, { dimColor: true, children: step.optionPrompt || "Pick one:" }), _jsx(Box, { flexDirection: "column", marginTop: 1, children: step.options.map((o, i) => (_jsxs(Text, { children: [_jsx(Text, { color: i === submenu.cursor ? "cyan" : undefined, bold: i === submenu.cursor, children: i === submenu.cursor ? "> " : " " }), o.label] }, o.value))) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "\u2191/\u2193 select \u00B7 Enter confirm \u00B7 \u2190 / Esc back" }) })] }));
|
|
418
|
+
}
|
|
419
|
+
// ── --check: flat, read-only report (no cursor, every row expanded) ─────────
|
|
420
|
+
if (opts.check) {
|
|
421
|
+
let lastScope = null;
|
|
422
|
+
return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [title, rows.map((row) => {
|
|
423
|
+
const head = row.scope !== lastScope;
|
|
424
|
+
lastScope = row.scope;
|
|
425
|
+
const icon = rowIcon(row);
|
|
426
|
+
return (_jsxs(Box, { flexDirection: "column", children: [head && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { bold: true, underline: true, children: SCOPE_TITLES[row.scope] }) })), _jsxs(Text, { children: [_jsx(Text, { color: icon.color, children: icon.char }), " ", _jsx(Text, { bold: true, children: row.title }), _jsxs(Text, { dimColor: true, children: [" ", rowSummary(row)] })] }), _jsxs(Box, { marginLeft: 4, flexDirection: "column", children: [_jsx(Text, { dimColor: true, children: row.detail }), row.lines.map((l, i) => (_jsx(Text, { dimColor: true, children: l }, i))), row.hint && _jsxs(Text, { color: "yellow", children: ["\u21B3 ", row.hint] })] })] }, row.id));
|
|
427
|
+
})] }));
|
|
428
|
+
}
|
|
429
|
+
// ── Interactive panel ───────────────────────────────────────────────────────
|
|
430
|
+
// A stable, single-line-per-row list (it never reflows on cursor moves) with
|
|
431
|
+
// the focused row's detail rendered in a fixed-height panel below. Highlight
|
|
432
|
+
// is a colored caret + accent label — no inline expand/collapse.
|
|
433
|
+
const labelW = Math.min(22, rows.reduce((m, r) => Math.max(m, r.title.length), 0));
|
|
434
|
+
const statusLine = current ? current.lines.join(" · ") : "";
|
|
435
|
+
let lastScope = null;
|
|
436
|
+
return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [title, rows.map((row) => {
|
|
437
|
+
const head = row.scope !== lastScope;
|
|
438
|
+
lastScope = row.scope;
|
|
439
|
+
const focused = row === current;
|
|
440
|
+
const icon = rowIcon(row);
|
|
441
|
+
const isInfo = row.kind === "info";
|
|
442
|
+
return (_jsxs(Box, { flexDirection: "column", children: [head && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { bold: true, color: "cyan", children: SCOPE_TITLES[row.scope] }) })), _jsxs(Text, { children: [_jsx(Text, { color: "cyan", bold: true, children: focused ? "❯ " : " " }), _jsx(Text, { color: icon.color, children: icon.char }), " ", _jsx(Text, { bold: !isInfo, color: focused ? "cyan" : undefined, children: row.title.padEnd(labelW) }), _jsxs(Text, { dimColor: true, children: [" ", rowSummary(row)] })] })] }, row.id));
|
|
443
|
+
}), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { dimColor: true, children: "─".repeat(PANEL_WIDTH) }), _jsxs(Text, { children: [_jsx(Text, { bold: true, children: current ? current.title : "" }), _jsx(Text, { dimColor: true, children: current ? " " + clamp(current.detail, PANEL_WIDTH - current.title.length - 2) : "" })] }), _jsx(Text, { dimColor: true, children: statusLine ? clamp(statusLine, PANEL_WIDTH) : " " }), current?.hint ? (_jsx(Text, { color: "yellow", children: clamp("↳ " + current.hint, PANEL_WIDTH) })) : (_jsx(Text, { children: " " }))] }), _jsx(Box, { marginTop: 1, children: busy ? (_jsxs(Text, { children: [_jsx(Text, { color: "cyan", children: _jsx(Spinner, { type: "dots" }) }), " ", busy] })) : toast ? (_jsxs(Text, { color: toast.ok ? "green" : "red", children: [toast.ok ? "✓" : "✗", " ", toast.message] })) : (_jsxs(Text, { dimColor: true, children: ["\u2191/\u2193 move \u00B7 ", footerVerb(current), " \u00B7 \u2190/Esc quit"] })) })] }));
|
|
444
|
+
}
|