santree 0.7.4 → 0.7.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (114) hide show
  1. package/README.md +2 -3
  2. package/dist/commands/config.d.ts +12 -0
  3. package/dist/commands/config.js +444 -0
  4. package/dist/commands/dashboard.js +187 -80
  5. package/dist/commands/helpers/statusline.js +0 -30
  6. package/dist/commands/issue/index.d.ts +1 -1
  7. package/dist/commands/issue/index.js +1 -1
  8. package/dist/commands/pr/context.d.ts +7 -0
  9. package/dist/commands/pr/context.js +36 -0
  10. package/dist/commands/pr/fix.d.ts +11 -2
  11. package/dist/commands/pr/fix.js +64 -12
  12. package/dist/commands/worktree/create.js +17 -11
  13. package/dist/commands/worktree/open.js +3 -2
  14. package/dist/commands/worktree/switch.js +3 -11
  15. package/dist/lib/ai.d.ts +11 -0
  16. package/dist/lib/ai.js +71 -2
  17. package/dist/lib/cd-hint.d.ts +18 -0
  18. package/dist/lib/cd-hint.js +27 -0
  19. package/dist/lib/claude-config.d.ts +26 -0
  20. package/dist/lib/claude-config.js +121 -0
  21. package/dist/lib/config/TrackerPicker.d.ts +10 -0
  22. package/dist/lib/config/TrackerPicker.js +113 -0
  23. package/dist/lib/config/diagnostics.d.ts +37 -0
  24. package/dist/lib/config/diagnostics.js +190 -0
  25. package/dist/lib/config-store.d.ts +23 -0
  26. package/dist/lib/config-store.js +68 -0
  27. package/dist/lib/dashboard/DetailPanel.d.ts +7 -2
  28. package/dist/lib/dashboard/DetailPanel.js +54 -37
  29. package/dist/lib/dashboard/IssueList.d.ts +5 -1
  30. package/dist/lib/dashboard/IssueList.js +24 -5
  31. package/dist/lib/dashboard/MultilineTextArea.js +1 -1
  32. package/dist/lib/dashboard/Overlays.js +3 -4
  33. package/dist/lib/dashboard/ReviewDetailPanel.js +11 -2
  34. package/dist/lib/dashboard/ReviewList.js +6 -3
  35. package/dist/lib/dashboard/data.js +7 -34
  36. package/dist/lib/dashboard/external-editor.d.ts +1 -1
  37. package/dist/lib/dashboard/external-editor.js +3 -2
  38. package/dist/lib/dashboard/types.d.ts +19 -2
  39. package/dist/lib/dashboard/types.js +68 -2
  40. package/dist/lib/exec.d.ts +13 -0
  41. package/dist/lib/exec.js +11 -1
  42. package/dist/lib/fix-loop.d.ts +45 -0
  43. package/dist/lib/fix-loop.js +83 -0
  44. package/dist/lib/git.d.ts +3 -29
  45. package/dist/lib/git.js +6 -62
  46. package/dist/lib/github.d.ts +70 -0
  47. package/dist/lib/github.js +128 -0
  48. package/dist/lib/multiplexer/cmux.js +143 -17
  49. package/dist/lib/multiplexer/none.js +3 -3
  50. package/dist/lib/multiplexer/tmux.js +5 -29
  51. package/dist/lib/multiplexer/types.d.ts +34 -1
  52. package/dist/lib/open-url.d.ts +3 -2
  53. package/dist/lib/open-url.js +5 -4
  54. package/dist/lib/prompts.d.ts +35 -1
  55. package/dist/lib/prompts.js +7 -0
  56. package/dist/lib/setup/apply.d.ts +16 -0
  57. package/dist/lib/setup/apply.js +35 -0
  58. package/dist/lib/setup/gitignore.d.ts +24 -0
  59. package/dist/lib/setup/gitignore.js +95 -0
  60. package/dist/lib/setup/steps.d.ts +39 -0
  61. package/dist/lib/setup/steps.js +333 -0
  62. package/dist/lib/setup/tools.d.ts +23 -0
  63. package/dist/lib/setup/tools.js +47 -0
  64. package/dist/lib/squirrel-loader.d.ts +3 -1
  65. package/dist/lib/squirrel-loader.js +2 -2
  66. package/dist/lib/trackers/github/index.js +2 -2
  67. package/dist/lib/trackers/local/store.js +1 -1
  68. package/package.json +1 -2
  69. package/prompts/fix-context.njk +79 -0
  70. package/prompts/fix-loop.njk +18 -0
  71. package/prompts/pr.njk +1 -1
  72. package/dist/commands/doctor.d.ts +0 -2
  73. package/dist/commands/doctor.js +0 -618
  74. package/dist/commands/github/auth.d.ts +0 -2
  75. package/dist/commands/github/auth.js +0 -56
  76. package/dist/commands/github/index.d.ts +0 -1
  77. package/dist/commands/github/index.js +0 -1
  78. package/dist/commands/helpers/english-tutor/index.d.ts +0 -1
  79. package/dist/commands/helpers/english-tutor/index.js +0 -1
  80. package/dist/commands/helpers/english-tutor/install.d.ts +0 -8
  81. package/dist/commands/helpers/english-tutor/install.js +0 -24
  82. package/dist/commands/helpers/english-tutor/prompt.d.ts +0 -2
  83. package/dist/commands/helpers/english-tutor/prompt.js +0 -16
  84. package/dist/commands/helpers/english-tutor/session-start.d.ts +0 -2
  85. package/dist/commands/helpers/english-tutor/session-start.js +0 -34
  86. package/dist/commands/helpers/english-tutor/uninstall.d.ts +0 -2
  87. package/dist/commands/helpers/english-tutor/uninstall.js +0 -15
  88. package/dist/commands/helpers/session-signal/end.d.ts +0 -2
  89. package/dist/commands/helpers/session-signal/end.js +0 -13
  90. package/dist/commands/helpers/session-signal/index.d.ts +0 -1
  91. package/dist/commands/helpers/session-signal/index.js +0 -1
  92. package/dist/commands/helpers/session-signal/install.d.ts +0 -8
  93. package/dist/commands/helpers/session-signal/install.js +0 -24
  94. package/dist/commands/helpers/session-signal/notification.d.ts +0 -2
  95. package/dist/commands/helpers/session-signal/notification.js +0 -13
  96. package/dist/commands/helpers/session-signal/prompt.d.ts +0 -2
  97. package/dist/commands/helpers/session-signal/prompt.js +0 -13
  98. package/dist/commands/helpers/session-signal/stop.d.ts +0 -2
  99. package/dist/commands/helpers/session-signal/stop.js +0 -13
  100. package/dist/commands/helpers/shell-init.d.ts +0 -11
  101. package/dist/commands/helpers/shell-init.js +0 -122
  102. package/dist/commands/issue/setup.d.ts +0 -2
  103. package/dist/commands/issue/setup.js +0 -108
  104. package/dist/commands/issue/switch.d.ts +0 -12
  105. package/dist/commands/issue/switch.js +0 -38
  106. package/dist/commands/linear/switch.d.ts +0 -2
  107. package/dist/commands/linear/switch.js +0 -75
  108. package/dist/lib/english-tutor.d.ts +0 -13
  109. package/dist/lib/english-tutor.js +0 -125
  110. package/dist/lib/session-signal.d.ts +0 -16
  111. package/dist/lib/session-signal.js +0 -104
  112. package/prompts/english-tutor-prompt.njk +0 -15
  113. package/shell/init.bash.njk +0 -129
  114. package/shell/init.zsh.njk +0 -204
@@ -1,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;
@@ -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,11 +0,0 @@
1
- import { z } from "zod/v4";
2
- export declare const description = "Output shell integration script";
3
- export declare const args: z.ZodTuple<[z.ZodDefault<z.ZodEnum<{
4
- zsh: "zsh";
5
- bash: "bash";
6
- }>>], null>;
7
- type Props = {
8
- args: z.infer<typeof args>;
9
- };
10
- export default function ShellInit({ args }: Props): null;
11
- export {};
@@ -1,122 +0,0 @@
1
- import { useEffect, useState, useRef } from "react";
2
- import { argument } from "pastel";
3
- import { z } from "zod/v4";
4
- import { readdirSync, statSync, existsSync } from "fs";
5
- import { fileURLToPath } from "url";
6
- import { dirname, join } from "path";
7
- import nunjucks from "nunjucks";
8
- const __filename = fileURLToPath(import.meta.url);
9
- const __dirname = dirname(__filename);
10
- export const description = "Output shell integration script";
11
- export const args = z.tuple([
12
- z
13
- .enum(["zsh", "bash"])
14
- .default("zsh")
15
- .describe(argument({ name: "shell", description: "Shell type (zsh or bash)" })),
16
- ]);
17
- const ARG_COMPLETIONS = {};
18
- function extractOptions(mod) {
19
- const options = [];
20
- if (mod.options) {
21
- const shape = mod.options.shape ?? {};
22
- for (const [key, value] of Object.entries(shape)) {
23
- const schema = value;
24
- const desc = schema.description ?? schema._zod?.def?.description ?? "";
25
- options.push({
26
- name: key,
27
- description: desc,
28
- completion: key === "base" ? "all_branches" : null,
29
- });
30
- }
31
- }
32
- return options;
33
- }
34
- /**
35
- * Dynamically loads command modules and extracts their metadata.
36
- * Handles both top-level files and directory-based command groups.
37
- */
38
- async function getCommands() {
39
- const commandsDir = join(__dirname, "..");
40
- const entries = readdirSync(commandsDir);
41
- const commands = [];
42
- for (const entry of entries) {
43
- const entryPath = join(commandsDir, entry);
44
- const stat = statSync(entryPath);
45
- if (stat.isDirectory()) {
46
- // Directory-based command group — complete arg to subcommand names
47
- const subFiles = readdirSync(entryPath).filter((f) => f.endsWith(".js") && f !== "index.js");
48
- const subNames = subFiles.map((f) => f.replace(".js", ""));
49
- if (subNames.length === 0)
50
- continue;
51
- let description = "";
52
- const indexPath = join(entryPath, "index.js");
53
- if (existsSync(indexPath)) {
54
- try {
55
- const indexMod = await import(indexPath);
56
- description = indexMod.description ?? "";
57
- }
58
- catch {
59
- // no description
60
- }
61
- }
62
- commands.push({
63
- name: entry,
64
- funcName: entry.replace(/-/g, "_"),
65
- description,
66
- hasArgs: true,
67
- argCompletion: "static",
68
- argCompletionValues: subNames.join(" "),
69
- options: [],
70
- });
71
- continue;
72
- }
73
- if (!entry.endsWith(".js"))
74
- continue;
75
- const name = entry.replace(".js", "");
76
- try {
77
- const mod = await import(join(commandsDir, entry));
78
- if (!mod.description)
79
- continue;
80
- commands.push({
81
- name,
82
- funcName: name.replace(/-/g, "_"),
83
- description: mod.description,
84
- hasArgs: !!mod.args,
85
- argCompletion: ARG_COMPLETIONS[name] || null,
86
- argCompletionValues: "",
87
- options: extractOptions(mod),
88
- });
89
- }
90
- catch {
91
- // Skip files that can't be imported
92
- }
93
- }
94
- return commands.sort((a, b) => a.name.localeCompare(b.name));
95
- }
96
- // Configure nunjucks
97
- const templatesDir = join(__dirname, "..", "..", "..", "shell");
98
- nunjucks.configure(templatesDir, { autoescape: false });
99
- export default function ShellInit({ args }) {
100
- const [shell] = args;
101
- const [done, setDone] = useState(false);
102
- const hasOutputRef = useRef(false);
103
- useEffect(() => {
104
- async function run() {
105
- if (hasOutputRef.current)
106
- return;
107
- hasOutputRef.current = true;
108
- const commands = await getCommands();
109
- const templateFile = `init.${shell}.njk`;
110
- const output = nunjucks.render(templateFile, { commands });
111
- process.stdout.write(output);
112
- setDone(true);
113
- }
114
- run();
115
- }, [shell]);
116
- useEffect(() => {
117
- if (done) {
118
- process.exit(0);
119
- }
120
- }, [done]);
121
- return null;
122
- }
@@ -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;