santree 0.5.3 → 0.5.4
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 +145 -52
- package/dist/commands/dashboard.d.ts +1 -1
- package/dist/commands/dashboard.js +22 -18
- package/dist/commands/doctor.js +33 -71
- package/dist/commands/github/auth.d.ts +2 -0
- package/dist/commands/github/auth.js +56 -0
- package/dist/commands/github/index.d.ts +1 -0
- package/dist/commands/github/index.js +1 -0
- package/dist/commands/helpers/template.d.ts +1 -0
- package/dist/commands/helpers/template.js +13 -10
- package/dist/commands/issue/index.d.ts +1 -0
- package/dist/commands/issue/index.js +1 -0
- package/dist/commands/issue/open.d.ts +2 -0
- package/dist/commands/{linear → issue}/open.js +13 -11
- package/dist/commands/issue/switch.d.ts +11 -0
- package/dist/commands/issue/switch.js +38 -0
- package/dist/commands/linear/auth.js +23 -10
- package/dist/commands/linear/switch.js +7 -3
- package/dist/commands/pr/create.js +7 -5
- package/dist/commands/worktree/create.js +4 -6
- package/dist/commands/worktree/work.js +1 -1
- package/dist/lib/ai.d.ts +8 -6
- package/dist/lib/ai.js +29 -15
- package/dist/lib/dashboard/DetailPanel.d.ts +5 -2
- package/dist/lib/dashboard/DetailPanel.js +6 -3
- package/dist/lib/dashboard/data.js +17 -9
- package/dist/lib/dashboard/types.d.ts +3 -16
- package/dist/lib/git.d.ts +16 -33
- package/dist/lib/git.js +20 -74
- package/dist/lib/metadata.d.ts +3 -0
- package/dist/lib/metadata.js +27 -0
- package/dist/lib/multiplexer/cmux.js +1 -1
- package/dist/lib/multiplexer/types.d.ts +1 -1
- package/dist/lib/prompts.d.ts +4 -3
- package/dist/lib/prompts.js +4 -3
- package/dist/lib/session-signal.d.ts +2 -3
- package/dist/lib/session-signal.js +3 -29
- package/dist/lib/trackers/auth-store.d.ts +16 -0
- package/dist/lib/trackers/auth-store.js +57 -0
- package/dist/lib/trackers/config.d.ts +8 -0
- package/dist/lib/trackers/config.js +21 -0
- package/dist/lib/trackers/github/api.d.ts +3 -0
- package/dist/lib/trackers/github/api.js +90 -0
- package/dist/lib/trackers/github/auth.d.ts +5 -0
- package/dist/lib/trackers/github/auth.js +27 -0
- package/dist/lib/trackers/github/images.d.ts +2 -0
- package/dist/lib/trackers/github/images.js +42 -0
- package/dist/lib/trackers/github/index.d.ts +2 -0
- package/dist/lib/trackers/github/index.js +78 -0
- package/dist/lib/trackers/index.d.ts +12 -0
- package/dist/lib/trackers/index.js +34 -0
- package/dist/lib/trackers/linear/api.d.ts +4 -0
- package/dist/lib/trackers/linear/api.js +128 -0
- package/dist/lib/trackers/linear/auth.d.ts +11 -0
- package/dist/lib/trackers/linear/auth.js +206 -0
- package/dist/lib/trackers/linear/images.d.ts +2 -0
- package/dist/lib/trackers/linear/images.js +44 -0
- package/dist/lib/trackers/linear/index.d.ts +3 -0
- package/dist/lib/trackers/linear/index.js +100 -0
- package/dist/lib/trackers/types.d.ts +52 -0
- package/dist/lib/trackers/types.js +1 -0
- package/package.json +1 -1
- package/prompts/ticket.njk +3 -3
- package/dist/commands/linear/open.d.ts +0 -2
- package/dist/lib/linear.d.ts +0 -83
- package/dist/lib/linear.js +0 -482
package/dist/commands/doctor.js
CHANGED
|
@@ -10,7 +10,7 @@ import * as path from "path";
|
|
|
10
10
|
const require = createRequire(import.meta.url);
|
|
11
11
|
const { version } = require("../../package.json");
|
|
12
12
|
import { findMainRepoRoot, getSantreeDir, getInitScriptPath } from "../lib/git.js";
|
|
13
|
-
import {
|
|
13
|
+
import { getIssueTracker } from "../lib/trackers/index.js";
|
|
14
14
|
import { getMultiplexer } from "../lib/multiplexer/index.js";
|
|
15
15
|
import { resolveClaudeBinary } from "../lib/ai.js";
|
|
16
16
|
import { CURRENT_VERSION, CLAUDE_CODE_PACKAGE, SANTREE_PACKAGE, getLatestVersionFor, isUpdateAvailable, detectPackageManager, getInstallCommandFor, } from "../lib/version.js";
|
|
@@ -135,8 +135,9 @@ async function checkMultiplexer() {
|
|
|
135
135
|
*/
|
|
136
136
|
async function checkClaude() {
|
|
137
137
|
const resolved = resolveClaudeBinary();
|
|
138
|
+
const usingBundled = resolved?.startsWith("/Applications/cmux.app/") ?? false;
|
|
138
139
|
const inCmux = getMultiplexer().kind === "cmux";
|
|
139
|
-
const description =
|
|
140
|
+
const description = usingBundled ? "Claude Code CLI (cmux-bundled)" : "Claude Code CLI";
|
|
140
141
|
if (!resolved) {
|
|
141
142
|
return {
|
|
142
143
|
name: "claude",
|
|
@@ -197,30 +198,22 @@ async function checkGhAuth() {
|
|
|
197
198
|
};
|
|
198
199
|
}
|
|
199
200
|
/**
|
|
200
|
-
* Checks
|
|
201
|
+
* Checks the active issue tracker's auth state. The tracker (Linear, GitHub)
|
|
202
|
+
* is resolved from the repo's `_tracker` config (or env / auto-detect).
|
|
203
|
+
* Doctor is the one place that legitimately names the active tracker — it's
|
|
204
|
+
* diagnostic context, not a generic UI string.
|
|
201
205
|
*/
|
|
202
|
-
async function
|
|
206
|
+
async function checkTrackerAuth() {
|
|
203
207
|
const repoRoot = findMainRepoRoot();
|
|
204
|
-
const
|
|
205
|
-
|
|
206
|
-
return {
|
|
207
|
-
authenticated: false,
|
|
208
|
-
hint: "Run: santree linear auth",
|
|
209
|
-
};
|
|
210
|
-
}
|
|
211
|
-
// Try to validate/refresh tokens
|
|
212
|
-
const valid = await getValidTokens(status.orgSlug);
|
|
208
|
+
const tracker = getIssueTracker(repoRoot);
|
|
209
|
+
const status = await tracker.getAuthStatus(repoRoot);
|
|
213
210
|
return {
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
211
|
+
displayName: tracker.displayName,
|
|
212
|
+
authenticated: status.authenticated,
|
|
213
|
+
accountLabel: status.accountLabel,
|
|
214
|
+
expiresAt: status.expiresAt,
|
|
218
215
|
repoLinked: status.repoLinked,
|
|
219
|
-
hint:
|
|
220
|
-
? "Token expired. Run: santree linear auth"
|
|
221
|
-
: !status.repoLinked
|
|
222
|
-
? "Repo not linked. Run: santree linear auth"
|
|
223
|
-
: undefined,
|
|
216
|
+
hint: status.hint,
|
|
224
217
|
};
|
|
225
218
|
}
|
|
226
219
|
/**
|
|
@@ -334,39 +327,12 @@ function checkSessionSignalHooks() {
|
|
|
334
327
|
catch {
|
|
335
328
|
missingHooks.push(...requiredEvents);
|
|
336
329
|
}
|
|
337
|
-
// Check hook script files in .santree/hooks/
|
|
338
|
-
const hookScripts = [];
|
|
339
|
-
const mainRepoRoot = findMainRepoRoot();
|
|
340
|
-
let hooksDir = null;
|
|
341
|
-
if (mainRepoRoot) {
|
|
342
|
-
hooksDir = path.join(mainRepoRoot, ".santree", "hooks");
|
|
343
|
-
const scriptNames = ["on-waiting.sh", "on-active.sh", "on-idle.sh", "on-exited.sh"];
|
|
344
|
-
for (const name of scriptNames) {
|
|
345
|
-
const scriptPath = path.join(hooksDir, name);
|
|
346
|
-
const exists = fs.existsSync(scriptPath);
|
|
347
|
-
let executable = false;
|
|
348
|
-
if (exists) {
|
|
349
|
-
try {
|
|
350
|
-
fs.accessSync(scriptPath, fs.constants.X_OK);
|
|
351
|
-
executable = true;
|
|
352
|
-
}
|
|
353
|
-
catch {
|
|
354
|
-
// not executable
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
|
-
if (exists) {
|
|
358
|
-
hookScripts.push({ name, exists, executable });
|
|
359
|
-
}
|
|
360
|
-
}
|
|
361
|
-
}
|
|
362
330
|
if (missingHooks.length === 0) {
|
|
363
|
-
return { configured: true, missingHooks: []
|
|
331
|
+
return { configured: true, missingHooks: [] };
|
|
364
332
|
}
|
|
365
333
|
return {
|
|
366
334
|
configured: false,
|
|
367
335
|
missingHooks,
|
|
368
|
-
hookScripts,
|
|
369
|
-
hooksDir,
|
|
370
336
|
hint: `Missing: ${missingHooks.join(", ")}. Run: santree helpers session-signal install`,
|
|
371
337
|
};
|
|
372
338
|
}
|
|
@@ -451,9 +417,9 @@ function StatusIcon({ ok, required }) {
|
|
|
451
417
|
function ToolRow({ tool }) {
|
|
452
418
|
return (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsxs(Box, { children: [_jsx(StatusIcon, { ok: tool.installed && !tool.hint, required: tool.required }), _jsx(Text, { children: " " }), _jsx(Text, { bold: true, children: tool.name }), _jsxs(Text, { dimColor: true, children: [" - ", tool.description] }), !tool.required && _jsx(Text, { dimColor: true, children: " (optional)" })] }), tool.installed ? (_jsxs(Box, { marginLeft: 2, flexDirection: "column", children: [_jsxs(Text, { dimColor: true, children: ["Version: ", tool.version] }), tool.latestVersion && tool.version && (_jsxs(Text, { color: isUpdateAvailable(tool.version, tool.latestVersion) ? "yellow" : undefined, dimColor: !isUpdateAvailable(tool.version, tool.latestVersion), children: ["Latest: ", tool.latestVersion, isUpdateAvailable(tool.version, tool.latestVersion) ? " ⬆ update available" : ""] })), tool.path && _jsxs(Text, { dimColor: true, children: ["Path: ", tool.path] }), tool.authStatus && _jsxs(Text, { dimColor: true, children: ["Auth: ", tool.authStatus] }), tool.updateHint && _jsxs(Text, { color: "yellow", children: ["\u21B3 ", tool.updateHint] }), tool.hint && _jsxs(Text, { color: "yellow", children: ["\u21B3 ", tool.hint] })] })) : (_jsx(Box, { marginLeft: 2, children: _jsxs(Text, { color: "yellow", children: ["\u21B3 ", tool.hint] }) }))] }));
|
|
453
419
|
}
|
|
454
|
-
function
|
|
455
|
-
const isOk =
|
|
456
|
-
return (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsxs(Box, { children: [_jsx(StatusIcon, { ok:
|
|
420
|
+
function TrackerRow({ tracker }) {
|
|
421
|
+
const isOk = tracker.authenticated && !tracker.hint;
|
|
422
|
+
return (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsxs(Box, { children: [_jsx(StatusIcon, { ok: isOk, required: true }), _jsx(Text, { children: " " }), _jsxs(Text, { bold: true, children: [tracker.displayName, " API"] }), _jsx(Text, { dimColor: true, children: " - Issue tracker integration" })] }), tracker.authenticated ? (_jsxs(Box, { marginLeft: 2, flexDirection: "column", children: [tracker.accountLabel && _jsxs(Text, { dimColor: true, children: ["Account: ", tracker.accountLabel] }), tracker.repoLinked !== undefined && (_jsxs(Text, { dimColor: true, children: ["Repo linked: ", tracker.repoLinked ? "yes" : "no"] })), tracker.hint && _jsxs(Text, { color: "yellow", children: ["\u21B3 ", tracker.hint] })] })) : (_jsx(Box, { marginLeft: 2, children: _jsxs(Text, { color: "yellow", children: ["\u21B3 ", tracker.hint] }) }))] }));
|
|
457
423
|
}
|
|
458
424
|
function ShellRow({ configured, shell }) {
|
|
459
425
|
return (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsxs(Box, { children: [_jsx(StatusIcon, { ok: configured, required: true }), _jsx(Text, { children: " " }), _jsx(Text, { bold: true, children: "Shell Integration" }), _jsx(Text, { dimColor: true, children: " - Enables directory switching" })] }), configured ? (_jsx(Box, { marginLeft: 2, children: _jsxs(Text, { dimColor: true, children: ["Shell: ", shell] }) })) : (_jsx(Box, { marginLeft: 2, children: _jsxs(Text, { color: "yellow", children: ["\u21B3 Add to .", shell, "rc: eval \"$(santree helpers shell-init ", shell, ")\""] }) }))] }));
|
|
@@ -465,13 +431,7 @@ function RemoteControlRow({ status }) {
|
|
|
465
431
|
return (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsxs(Box, { children: [_jsx(StatusIcon, { ok: status.enabled, required: false }), _jsx(Text, { children: " " }), _jsx(Text, { bold: true, children: "Remote Control" }), _jsx(Text, { dimColor: true, children: " - Continue sessions from any device" }), _jsx(Text, { dimColor: true, children: " (optional)" })] }), _jsxs(Box, { marginLeft: 2, flexDirection: "column", children: [_jsxs(Text, { dimColor: true, children: ["Enabled: ", status.enabled ? "yes" : "no"] }), status.hint && _jsxs(Text, { color: "yellow", children: ["\u21B3 ", status.hint] })] })] }));
|
|
466
432
|
}
|
|
467
433
|
function SessionSignalRow({ status }) {
|
|
468
|
-
|
|
469
|
-
const existingNames = new Set(status.hookScripts.map((s) => s.name));
|
|
470
|
-
const missingScripts = allScriptNames.filter((n) => !existingNames.has(n));
|
|
471
|
-
const nonExecutable = status.hookScripts.filter((s) => !s.executable);
|
|
472
|
-
return (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsxs(Box, { children: [_jsx(StatusIcon, { ok: status.configured && nonExecutable.length === 0, required: false }), _jsx(Text, { children: " " }), _jsx(Text, { bold: true, children: "Session Signal Hooks" }), _jsx(Text, { dimColor: true, children: " - Surface session state in dashboard/tmux" }), _jsx(Text, { dimColor: true, children: " (optional)" })] }), _jsxs(Box, { marginLeft: 2, flexDirection: "column", children: [status.configured ? (_jsx(Text, { dimColor: true, children: "All hooks configured" })) : (_jsxs(_Fragment, { children: [_jsxs(Text, { dimColor: true, children: ["Missing: ", status.missingHooks.join(", ")] }), status.hint && _jsxs(Text, { color: "yellow", children: ["\u21B3 ", status.hint] })] })), status.hooksDir && (_jsxs(Text, { dimColor: true, children: ["Hook scripts:", " ", status.hookScripts.length > 0
|
|
473
|
-
? status.hookScripts.map((s) => s.name).join(", ")
|
|
474
|
-
: "none"] })), status.hooksDir && missingScripts.length > 0 && (_jsxs(Text, { color: "yellow", children: ["\u21B3 Create in ", status.hooksDir, ": ", missingScripts.join(", ")] })), nonExecutable.map((s) => (_jsxs(Text, { color: "yellow", children: ["\u21B3 ", s.name, " is not executable. Run: chmod +x .santree/hooks/", s.name] }, s.name)))] })] }));
|
|
434
|
+
return (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsxs(Box, { children: [_jsx(StatusIcon, { ok: status.configured, required: false }), _jsx(Text, { children: " " }), _jsx(Text, { bold: true, children: "Session Signal Hooks" }), _jsx(Text, { dimColor: true, children: " - Surface session state in dashboard/tmux" }), _jsx(Text, { dimColor: true, children: " (optional)" })] }), _jsx(Box, { marginLeft: 2, flexDirection: "column", children: status.configured ? (_jsx(Text, { dimColor: true, children: "All hooks configured" })) : (_jsxs(_Fragment, { children: [_jsxs(Text, { dimColor: true, children: ["Missing: ", status.missingHooks.join(", ")] }), status.hint && _jsxs(Text, { color: "yellow", children: ["\u21B3 ", status.hint] })] })) })] }));
|
|
475
435
|
}
|
|
476
436
|
function SantreeSetupRow({ status }) {
|
|
477
437
|
const isOk = status.santreeFolderExists &&
|
|
@@ -490,7 +450,7 @@ function SantreeSetupRow({ status }) {
|
|
|
490
450
|
}
|
|
491
451
|
export default function Doctor() {
|
|
492
452
|
const [tools, setTools] = useState([]);
|
|
493
|
-
const [
|
|
453
|
+
const [tracker, setTracker] = useState(null);
|
|
494
454
|
const [shellStatus, setShellStatus] = useState(null);
|
|
495
455
|
const [remoteControl, setRemoteControl] = useState(null);
|
|
496
456
|
const [statusline, setStatusline] = useState(null);
|
|
@@ -541,12 +501,14 @@ export default function Doctor() {
|
|
|
541
501
|
}
|
|
542
502
|
}
|
|
543
503
|
}
|
|
544
|
-
// Optional: a syntax-
|
|
504
|
+
// Optional: a syntax-highlighting diff pager — used by `st worktree diff`
|
|
545
505
|
// and the dashboard `v` overlay when SANTREE_DIFF_TOOL is set. Any
|
|
546
|
-
// pager works (delta, diff-so-fancy, …); without one set,
|
|
547
|
-
//
|
|
548
|
-
//
|
|
549
|
-
|
|
506
|
+
// diff pager works (delta, diff-so-fancy, …); without one set, the
|
|
507
|
+
// dashboard renders inline with santree's own colorizer and the CLI
|
|
508
|
+
// falls back to git's default pager. Delta is the most popular
|
|
509
|
+
// choice so we check for it as a convenience, but it is never a
|
|
510
|
+
// hard dependency.
|
|
511
|
+
const deltaCheck = await checkTool("delta", "Recommended diff pager — any diff pager works", false, "delta --version | head -1", "Optional — santree's built-in colorizer handles the dashboard overlay; the CLI falls back to git's default pager. Set SANTREE_DIFF_TOOL to override. To install delta: brew install git-delta");
|
|
550
512
|
results.push(deltaCheck);
|
|
551
513
|
// Optional: a `.code-workspace`-aware editor (VSCode or Cursor).
|
|
552
514
|
// Santree itself works with any editor via $SANTREE_EDITOR — this
|
|
@@ -573,10 +535,10 @@ export default function Doctor() {
|
|
|
573
535
|
hint: "Optional — santree works with any $SANTREE_EDITOR. Only needed for the dashboard's `.code-workspace` shortcut.",
|
|
574
536
|
});
|
|
575
537
|
}
|
|
576
|
-
const
|
|
538
|
+
const trackerResult = await checkTrackerAuth();
|
|
577
539
|
const statuslineResult = await checkStatusline();
|
|
578
540
|
setTools(results);
|
|
579
|
-
|
|
541
|
+
setTracker(trackerResult);
|
|
580
542
|
setShellStatus(checkShellIntegration());
|
|
581
543
|
setRemoteControl(checkRemoteControl());
|
|
582
544
|
setStatusline(statuslineResult);
|
|
@@ -591,7 +553,7 @@ export default function Doctor() {
|
|
|
591
553
|
}
|
|
592
554
|
const requiredMissing = tools.filter((t) => t.required && (!t.installed || t.hint));
|
|
593
555
|
const optionalMissing = tools.filter((t) => !t.required && !t.installed);
|
|
594
|
-
const
|
|
595
|
-
const allRequired = requiredMissing.length === 0 &&
|
|
596
|
-
return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsxs(Box, { marginBottom: 1, children: [_jsx(Text, { bold: true, color: "cyan", children: "Santree Doctor" }), _jsxs(Text, { dimColor: true, children: [" v", version] })] }), _jsx(Box, { marginBottom: 1, flexDirection: "column", children: _jsx(Text, { bold: true, underline: true, children: "CLI Tools" }) }), tools.map((tool) => (_jsx(ToolRow, { tool: tool }, tool.name))), _jsx(Box, { marginBottom: 1, marginTop: 1, flexDirection: "column", children: _jsx(Text, { bold: true, underline: true, children: "Integrations" }) }),
|
|
556
|
+
const trackerOk = tracker?.authenticated && !tracker?.hint;
|
|
557
|
+
const allRequired = requiredMissing.length === 0 && trackerOk && shellStatus?.configured;
|
|
558
|
+
return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsxs(Box, { marginBottom: 1, children: [_jsx(Text, { bold: true, color: "cyan", children: "Santree Doctor" }), _jsxs(Text, { dimColor: true, children: [" v", version] })] }), _jsx(Box, { marginBottom: 1, flexDirection: "column", children: _jsx(Text, { bold: true, underline: true, children: "CLI Tools" }) }), tools.map((tool) => (_jsx(ToolRow, { tool: tool }, tool.name))), _jsx(Box, { marginBottom: 1, marginTop: 1, flexDirection: "column", children: _jsx(Text, { bold: true, underline: true, children: "Integrations" }) }), tracker && _jsx(TrackerRow, { tracker: tracker }), shellStatus && _jsx(ShellRow, { configured: shellStatus.configured, shell: shellStatus.shell }), santreeSetup && _jsx(SantreeSetupRow, { status: santreeSetup }), _jsx(Box, { marginBottom: 1, marginTop: 1, flexDirection: "column", children: _jsx(Text, { bold: true, underline: true, children: "Claude Code" }) }), remoteControl && _jsx(RemoteControlRow, { status: remoteControl }), statusline && _jsx(StatuslineRow, { status: statusline }), sessionSignal && _jsx(SessionSignalRow, { status: sessionSignal }), _jsx(Box, { marginTop: 1, borderStyle: "single", borderColor: allRequired ? "green" : "yellow", paddingX: 2, children: allRequired ? (_jsx(Text, { color: "green", children: "All requirements satisfied! Santree is ready to use." })) : (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: "yellow", children: [requiredMissing.length + (trackerOk ? 0 : 1) + (shellStatus?.configured ? 0 : 1), " ", "required item(s) need attention"] }), optionalMissing.length > 0 && (_jsxs(Text, { dimColor: true, children: [optionalMissing.length, " optional item(s) not installed"] }))] })) })] }));
|
|
597
559
|
}
|
|
@@ -0,0 +1,56 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const description = "GitHub Issues tracker commands";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const description = "GitHub Issues tracker commands";
|
|
@@ -6,13 +6,13 @@ import { argument } from "pastel";
|
|
|
6
6
|
import { z } from "zod/v4";
|
|
7
7
|
import { findRepoRoot, findMainRepoRoot, getCurrentBranch, extractTicketId, } from "../../lib/git.js";
|
|
8
8
|
import { renderTicket } from "../../lib/prompts.js";
|
|
9
|
-
import {
|
|
9
|
+
import { getIssueTracker } from "../../lib/trackers/index.js";
|
|
10
10
|
import { resolveAIContext, renderAIPrompt, fetchAndRenderPR, fetchAndRenderDiff, } from "../../lib/ai.js";
|
|
11
11
|
export const description = "Render a template to stdout";
|
|
12
12
|
export const args = z.tuple([
|
|
13
|
-
z.enum(["linear", "git-changes", "pr", "fix-pr", "review"]).describe(argument({
|
|
13
|
+
z.enum(["ticket", "linear", "git-changes", "pr", "fix-pr", "review"]).describe(argument({
|
|
14
14
|
name: "type",
|
|
15
|
-
description: "Template type (
|
|
15
|
+
description: "Template type (ticket, git-changes, pr, fix-pr, or review)",
|
|
16
16
|
})),
|
|
17
17
|
]);
|
|
18
18
|
export default function Template({ args }) {
|
|
@@ -93,22 +93,24 @@ export default function Template({ args }) {
|
|
|
93
93
|
setTimeout(() => exit(), 100);
|
|
94
94
|
}
|
|
95
95
|
else {
|
|
96
|
+
const mainRoot = findMainRepoRoot() ?? repoRoot;
|
|
97
|
+
const tracker = getIssueTracker(mainRoot);
|
|
96
98
|
const ticketId = extractTicketId(branch);
|
|
97
99
|
if (!ticketId) {
|
|
98
100
|
setStatus("error");
|
|
99
|
-
setMessage(
|
|
101
|
+
setMessage(`Could not extract ${tracker.issueNoun} ID from branch name '${branch}'.`);
|
|
100
102
|
setTimeout(() => exit(), 100);
|
|
101
103
|
return;
|
|
102
104
|
}
|
|
103
|
-
const
|
|
104
|
-
|
|
105
|
-
|
|
105
|
+
const result = await tracker.getIssue(ticketId, mainRoot);
|
|
106
|
+
if (!result.ok) {
|
|
107
|
+
const status = await tracker.getAuthStatus(mainRoot);
|
|
106
108
|
setStatus("error");
|
|
107
|
-
setMessage(`Could not fetch
|
|
109
|
+
setMessage(`Could not fetch ${tracker.issueNoun} ${ticketId}.${status.hint ? ` ${status.hint}` : ""}`);
|
|
108
110
|
setTimeout(() => exit(), 100);
|
|
109
111
|
return;
|
|
110
112
|
}
|
|
111
|
-
process.stdout.write(renderTicket(
|
|
113
|
+
process.stdout.write(renderTicket(result.value, tracker.displayName));
|
|
112
114
|
setStatus("done");
|
|
113
115
|
setTimeout(() => exit(), 100);
|
|
114
116
|
}
|
|
@@ -118,7 +120,8 @@ export default function Template({ args }) {
|
|
|
118
120
|
if (status === "done")
|
|
119
121
|
return null;
|
|
120
122
|
const spinnerTexts = {
|
|
121
|
-
|
|
123
|
+
ticket: "Fetching issue...",
|
|
124
|
+
linear: "Fetching issue...",
|
|
122
125
|
"git-changes": "Gathering changes...",
|
|
123
126
|
pr: "Fetching PR feedback...",
|
|
124
127
|
"fix-pr": "Building fix-pr prompt...",
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const description = "Issue tracker commands (Linear / GitHub)";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const description = "Issue tracker commands (Linear / GitHub)";
|
|
@@ -4,11 +4,11 @@ import { Text, Box } from "ink";
|
|
|
4
4
|
import Spinner from "ink-spinner";
|
|
5
5
|
import { exec } from "child_process";
|
|
6
6
|
import { promisify } from "util";
|
|
7
|
-
import { findMainRepoRoot, getCurrentBranch
|
|
8
|
-
import {
|
|
7
|
+
import { findMainRepoRoot, getCurrentBranch } from "../../lib/git.js";
|
|
8
|
+
import { getIssueTracker } from "../../lib/trackers/index.js";
|
|
9
9
|
const execAsync = promisify(exec);
|
|
10
|
-
export const description = "Open the current
|
|
11
|
-
export default function
|
|
10
|
+
export const description = "Open the current branch's issue in the browser";
|
|
11
|
+
export default function IssueOpen() {
|
|
12
12
|
const [status, setStatus] = useState("checking");
|
|
13
13
|
const [message, setMessage] = useState("");
|
|
14
14
|
useEffect(() => {
|
|
@@ -26,16 +26,18 @@ export default function LinearOpen() {
|
|
|
26
26
|
setMessage("Could not determine current branch");
|
|
27
27
|
return;
|
|
28
28
|
}
|
|
29
|
-
const
|
|
29
|
+
const tracker = getIssueTracker(repoRoot);
|
|
30
|
+
const ticketId = tracker.extractIdFromBranch(branch);
|
|
30
31
|
if (!ticketId) {
|
|
31
32
|
setStatus("error");
|
|
32
|
-
setMessage(
|
|
33
|
+
setMessage(`No ${tracker.issueNoun} ID found in branch '${branch}'`);
|
|
33
34
|
return;
|
|
34
35
|
}
|
|
35
|
-
const
|
|
36
|
-
if (!
|
|
36
|
+
const result = await tracker.getIssue(ticketId, repoRoot);
|
|
37
|
+
if (!result.ok || !result.value.url) {
|
|
38
|
+
const auth = await tracker.getAuthStatus(repoRoot);
|
|
37
39
|
setStatus("error");
|
|
38
|
-
setMessage(`Could not fetch
|
|
40
|
+
setMessage(`Could not fetch ${tracker.issueNoun} ${ticketId}.${auth.hint ? ` ${auth.hint}` : ""}`);
|
|
39
41
|
return;
|
|
40
42
|
}
|
|
41
43
|
try {
|
|
@@ -44,7 +46,7 @@ export default function LinearOpen() {
|
|
|
44
46
|
: process.platform === "win32"
|
|
45
47
|
? "start"
|
|
46
48
|
: "xdg-open";
|
|
47
|
-
await execAsync(`${openCmd} "${
|
|
49
|
+
await execAsync(`${openCmd} "${result.value.url}"`);
|
|
48
50
|
setStatus("done");
|
|
49
51
|
setMessage(`Opened ${ticketId} in browser`);
|
|
50
52
|
}
|
|
@@ -61,5 +63,5 @@ export default function LinearOpen() {
|
|
|
61
63
|
return () => clearTimeout(timer);
|
|
62
64
|
}
|
|
63
65
|
}, [status]);
|
|
64
|
-
return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [status === "checking" && (_jsxs(Box, { children: [_jsx(Text, { color: "cyan", children: _jsx(Spinner, { type: "dots" }) }), _jsx(Text, { children: " Opening
|
|
66
|
+
return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [status === "checking" && (_jsxs(Box, { children: [_jsx(Text, { color: "cyan", children: _jsx(Spinner, { type: "dots" }) }), _jsx(Text, { children: " Opening issue..." })] })), status === "done" && (_jsxs(Text, { color: "green", bold: true, children: ["\u2713 ", message] })), status === "error" && (_jsxs(Text, { color: "red", bold: true, children: ["\u2717 ", message] }))] }));
|
|
65
67
|
}
|
|
@@ -0,0 +1,11 @@
|
|
|
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
|
+
}>], null>;
|
|
7
|
+
type Props = {
|
|
8
|
+
args: z.infer<typeof args>;
|
|
9
|
+
};
|
|
10
|
+
export default function IssueSwitch({ args }: Props): import("react/jsx-runtime").JSX.Element;
|
|
11
|
+
export {};
|
|
@@ -0,0 +1,38 @@
|
|
|
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"]).describe(argument({
|
|
11
|
+
name: "kind",
|
|
12
|
+
description: "Tracker kind: linear or github",
|
|
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
|
+
}
|
|
@@ -3,8 +3,10 @@ import { useEffect, useState } from "react";
|
|
|
3
3
|
import { Text, Box, useInput } from "ink";
|
|
4
4
|
import Spinner from "ink-spinner";
|
|
5
5
|
import { z } from "zod";
|
|
6
|
-
import { findMainRepoRoot
|
|
7
|
-
import {
|
|
6
|
+
import { findMainRepoRoot } from "../../lib/git.js";
|
|
7
|
+
import { setRepoLinearOrg, getRepoLinearOrg, removeRepoLinearOrg, startOAuthFlow, getValidTokens, linearTracker, } from "../../lib/trackers/linear/index.js";
|
|
8
|
+
import { readLinearAuthStore } from "../../lib/trackers/auth-store.js";
|
|
9
|
+
import { setRepoTracker } from "../../lib/trackers/index.js";
|
|
8
10
|
import { renderTicket } from "../../lib/prompts.js";
|
|
9
11
|
export const description = "Authenticate with Linear";
|
|
10
12
|
export const options = z.object({
|
|
@@ -46,6 +48,7 @@ export default function LinearAuth({ options }) {
|
|
|
46
48
|
return;
|
|
47
49
|
}
|
|
48
50
|
setRepoLinearOrg(repoRoot, result.orgSlug);
|
|
51
|
+
setRepoTracker(repoRoot, "linear");
|
|
49
52
|
setMessage(`Authenticated as ${result.orgName} (${result.orgSlug})`);
|
|
50
53
|
setStatus("done");
|
|
51
54
|
});
|
|
@@ -54,6 +57,7 @@ export default function LinearAuth({ options }) {
|
|
|
54
57
|
// Link existing org
|
|
55
58
|
const choice = choices[selected];
|
|
56
59
|
setRepoLinearOrg(repoRoot, choice.slug);
|
|
60
|
+
setRepoTracker(repoRoot, "linear");
|
|
57
61
|
setMessage(`Linked repo to ${choice.name} (${choice.slug})`);
|
|
58
62
|
setStatus("done");
|
|
59
63
|
}
|
|
@@ -67,35 +71,42 @@ export default function LinearAuth({ options }) {
|
|
|
67
71
|
setStatus("error");
|
|
68
72
|
return;
|
|
69
73
|
}
|
|
70
|
-
const
|
|
71
|
-
if (!
|
|
74
|
+
const result = await linearTracker.getIssue(options.test, repoRoot);
|
|
75
|
+
if (!result.ok) {
|
|
72
76
|
setError(`Could not fetch ticket ${options.test}. Check auth and ticket ID.`);
|
|
73
77
|
setStatus("error");
|
|
74
78
|
return;
|
|
75
79
|
}
|
|
76
|
-
setMessage(renderTicket(
|
|
80
|
+
setMessage(renderTicket(result.value, linearTracker.displayName).trim());
|
|
77
81
|
setStatus("done");
|
|
78
82
|
return;
|
|
79
83
|
}
|
|
80
84
|
if (options.status) {
|
|
81
85
|
const repoRoot = findMainRepoRoot();
|
|
82
|
-
const authStatus = getAuthStatus(repoRoot);
|
|
86
|
+
const authStatus = await linearTracker.getAuthStatus(repoRoot);
|
|
83
87
|
if (!authStatus.authenticated) {
|
|
84
88
|
setMessage("Not authenticated with Linear");
|
|
85
89
|
setStatus("done");
|
|
86
90
|
return;
|
|
87
91
|
}
|
|
88
|
-
|
|
89
|
-
|
|
92
|
+
const orgSlug = repoRoot ? getRepoLinearOrg(repoRoot) : null;
|
|
93
|
+
if (orgSlug) {
|
|
94
|
+
const valid = await getValidTokens(orgSlug);
|
|
90
95
|
const expiry = valid
|
|
91
96
|
? new Date(valid.expires_at).toLocaleString()
|
|
92
97
|
: "expired (refresh failed)";
|
|
93
98
|
setMessage([
|
|
94
|
-
`
|
|
99
|
+
`Account: ${authStatus.accountLabel ?? orgSlug}`,
|
|
95
100
|
`Token expires: ${expiry}`,
|
|
96
101
|
`Repo linked: ${authStatus.repoLinked ? "yes" : "no"}`,
|
|
97
102
|
].join("\n"));
|
|
98
103
|
}
|
|
104
|
+
else {
|
|
105
|
+
setMessage([
|
|
106
|
+
`Account: ${authStatus.accountLabel ?? "unknown"}`,
|
|
107
|
+
`Repo linked: ${authStatus.repoLinked ? "yes" : "no"}`,
|
|
108
|
+
].join("\n"));
|
|
109
|
+
}
|
|
99
110
|
setStatus("done");
|
|
100
111
|
return;
|
|
101
112
|
}
|
|
@@ -142,12 +153,13 @@ export default function LinearAuth({ options }) {
|
|
|
142
153
|
return;
|
|
143
154
|
}
|
|
144
155
|
setRepoLinearOrg(repoRoot, result.orgSlug);
|
|
156
|
+
setRepoTracker(repoRoot, "linear");
|
|
145
157
|
setMessage(`Re-authenticated as ${result.orgName} (${result.orgSlug})`);
|
|
146
158
|
setStatus("done");
|
|
147
159
|
return;
|
|
148
160
|
}
|
|
149
161
|
// Check for existing authenticated orgs
|
|
150
|
-
const store =
|
|
162
|
+
const store = readLinearAuthStore();
|
|
151
163
|
const orgs = Object.entries(store).map(([slug, tokens]) => ({
|
|
152
164
|
slug,
|
|
153
165
|
name: tokens.org_name,
|
|
@@ -162,6 +174,7 @@ export default function LinearAuth({ options }) {
|
|
|
162
174
|
return;
|
|
163
175
|
}
|
|
164
176
|
setRepoLinearOrg(repoRoot, result.orgSlug);
|
|
177
|
+
setRepoTracker(repoRoot, "linear");
|
|
165
178
|
setMessage(`Authenticated as ${result.orgName} (${result.orgSlug})`);
|
|
166
179
|
setStatus("done");
|
|
167
180
|
return;
|
|
@@ -2,8 +2,10 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import { useEffect, useState } from "react";
|
|
3
3
|
import { Text, Box, useInput } from "ink";
|
|
4
4
|
import Spinner from "ink-spinner";
|
|
5
|
-
import { findMainRepoRoot
|
|
6
|
-
import {
|
|
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";
|
|
7
9
|
export const description = "Switch Linear workspace for this repo";
|
|
8
10
|
export default function LinearSwitch() {
|
|
9
11
|
const [status, setStatus] = useState("checking");
|
|
@@ -25,6 +27,7 @@ export default function LinearSwitch() {
|
|
|
25
27
|
const choice = choices[selected];
|
|
26
28
|
const repoRoot = findMainRepoRoot();
|
|
27
29
|
setRepoLinearOrg(repoRoot, choice.slug);
|
|
30
|
+
setRepoTracker(repoRoot, "linear");
|
|
28
31
|
setMessage(`Switched to ${choice.name} (${choice.slug})`);
|
|
29
32
|
setStatus("done");
|
|
30
33
|
}
|
|
@@ -38,7 +41,7 @@ export default function LinearSwitch() {
|
|
|
38
41
|
setStatus("error");
|
|
39
42
|
return;
|
|
40
43
|
}
|
|
41
|
-
const store =
|
|
44
|
+
const store = readLinearAuthStore();
|
|
42
45
|
const orgs = Object.entries(store).map(([slug, tokens]) => ({
|
|
43
46
|
slug,
|
|
44
47
|
name: tokens.org_name,
|
|
@@ -51,6 +54,7 @@ export default function LinearSwitch() {
|
|
|
51
54
|
if (orgs.length === 1) {
|
|
52
55
|
const org = orgs[0];
|
|
53
56
|
setRepoLinearOrg(repoRoot, org.slug);
|
|
57
|
+
setRepoTracker(repoRoot, "linear");
|
|
54
58
|
setMessage(`Linked to ${org.name} (${org.slug})`);
|
|
55
59
|
setStatus("done");
|
|
56
60
|
return;
|
|
@@ -12,7 +12,7 @@ import { findMainRepoRoot, findRepoRoot, getCurrentBranch, getBaseBranch, hasUnc
|
|
|
12
12
|
import { ghCliAvailable, getPRInfoAsync, pushBranch, createPR, getPRTemplate, } from "../../lib/github.js";
|
|
13
13
|
import { renderPrompt, renderDiff, renderTicket } from "../../lib/prompts.js";
|
|
14
14
|
import { runAgent } from "../../lib/ai.js";
|
|
15
|
-
import {
|
|
15
|
+
import { getIssueTracker } from "../../lib/trackers/index.js";
|
|
16
16
|
const execAsync = promisify(exec);
|
|
17
17
|
export const description = "Create a GitHub pull request";
|
|
18
18
|
export const options = z.object({
|
|
@@ -63,12 +63,14 @@ export default function PR({ options }) {
|
|
|
63
63
|
}
|
|
64
64
|
const ticketId = extractTicketId(branch);
|
|
65
65
|
const mainRepoRoot = findMainRepoRoot();
|
|
66
|
-
// Fetch
|
|
66
|
+
// Fetch issue content from the active tracker (downloads images
|
|
67
|
+
// inline so Claude can read them via --allowedTools Read).
|
|
67
68
|
let ticketContent;
|
|
68
69
|
if (ticketId && mainRepoRoot) {
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
|
|
70
|
+
const tracker = getIssueTracker(mainRepoRoot);
|
|
71
|
+
const result = await tracker.getIssue(ticketId, mainRepoRoot);
|
|
72
|
+
if (result.ok) {
|
|
73
|
+
ticketContent = renderTicket(result.value, tracker.displayName);
|
|
72
74
|
}
|
|
73
75
|
}
|
|
74
76
|
const diffContent = renderDiff({
|