santree 0.5.3 → 0.5.5
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 +156 -46
- package/dist/commands/dashboard.d.ts +1 -1
- package/dist/commands/dashboard.js +22 -18
- package/dist/commands/doctor.js +97 -76
- 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/english-tutor/index.d.ts +1 -0
- package/dist/commands/helpers/english-tutor/index.js +1 -0
- package/dist/commands/helpers/english-tutor/install.d.ts +8 -0
- package/dist/commands/helpers/english-tutor/install.js +24 -0
- package/dist/commands/helpers/english-tutor/prompt.d.ts +2 -0
- package/dist/commands/helpers/english-tutor/prompt.js +16 -0
- package/dist/commands/helpers/english-tutor/session-start.d.ts +2 -0
- package/dist/commands/helpers/english-tutor/session-start.js +34 -0
- package/dist/commands/helpers/english-tutor/uninstall.d.ts +2 -0
- package/dist/commands/helpers/english-tutor/uninstall.js +15 -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/english-tutor.d.ts +13 -0
- package/dist/lib/english-tutor.js +125 -0
- 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/index.js +5 -12
- 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/english-tutor-prompt.njk +15 -0
- 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
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as os from "os";
|
|
3
|
+
import * as path from "path";
|
|
4
|
+
const MARKER = "english-tutor";
|
|
5
|
+
export function getLogPath() {
|
|
6
|
+
const configDir = process.env.XDG_CONFIG_HOME || path.join(os.homedir(), ".config");
|
|
7
|
+
return path.join(configDir, "santree", "english-practice-log.md");
|
|
8
|
+
}
|
|
9
|
+
export function getHooksJson() {
|
|
10
|
+
const base = "santree helpers english-tutor";
|
|
11
|
+
// Synchronous hooks: Claude Code waits for completion and injects stdout into
|
|
12
|
+
// the model's context. `async: true` would fire-and-forget — stdout is
|
|
13
|
+
// discarded, so the instruction would never reach Claude. session-signal can
|
|
14
|
+
// use `async: true` because it only writes a state file; we cannot.
|
|
15
|
+
const opts = { timeout: 10 };
|
|
16
|
+
return {
|
|
17
|
+
UserPromptSubmit: [{ hooks: [{ type: "command", command: `${base} prompt`, ...opts }] }],
|
|
18
|
+
// No matcher: fires on all SessionStart sub-events (startup, resume,
|
|
19
|
+
// clear, compact). Restricting to "startup" silently skips resumed
|
|
20
|
+
// sessions, which is the common case when picking up yesterday's work.
|
|
21
|
+
SessionStart: [{ hooks: [{ type: "command", command: `${base} session-start`, ...opts }] }],
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
export function getPermissionEntry() {
|
|
25
|
+
return `Edit(${getLogPath()})`;
|
|
26
|
+
}
|
|
27
|
+
function readSettings(settingsPath) {
|
|
28
|
+
try {
|
|
29
|
+
if (fs.existsSync(settingsPath)) {
|
|
30
|
+
return JSON.parse(fs.readFileSync(settingsPath, "utf-8"));
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
// fall through
|
|
35
|
+
}
|
|
36
|
+
return {};
|
|
37
|
+
}
|
|
38
|
+
function writeSettings(settingsPath, settings) {
|
|
39
|
+
const dir = path.dirname(settingsPath);
|
|
40
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
41
|
+
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
|
|
42
|
+
}
|
|
43
|
+
function settingsPath() {
|
|
44
|
+
const home = process.env.HOME || os.homedir();
|
|
45
|
+
return path.join(home, ".claude", "settings.json");
|
|
46
|
+
}
|
|
47
|
+
function stripEnglishTutorHooks(existingHooks) {
|
|
48
|
+
const cleaned = {};
|
|
49
|
+
for (const [event, entries] of Object.entries(existingHooks)) {
|
|
50
|
+
if (!Array.isArray(entries)) {
|
|
51
|
+
cleaned[event] = entries;
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
const filtered = entries.filter((entry) => {
|
|
55
|
+
const inner = entry.hooks || [];
|
|
56
|
+
return !inner.some((h) => typeof h.command === "string" && h.command.includes(MARKER));
|
|
57
|
+
});
|
|
58
|
+
if (filtered.length > 0)
|
|
59
|
+
cleaned[event] = filtered;
|
|
60
|
+
}
|
|
61
|
+
return cleaned;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Create the practice log if it doesn't already exist. Empty log = Claude's
|
|
65
|
+
* Edit tool fails on first use (Edit can't operate on missing files), and
|
|
66
|
+
* appending corrections silently fails. Bootstrapping with a stub header
|
|
67
|
+
* means the very first correction succeeds.
|
|
68
|
+
*/
|
|
69
|
+
function ensureLogExists() {
|
|
70
|
+
const logPath = getLogPath();
|
|
71
|
+
if (fs.existsSync(logPath))
|
|
72
|
+
return logPath;
|
|
73
|
+
fs.mkdirSync(path.dirname(logPath), { recursive: true });
|
|
74
|
+
const stub = "# English Practice Log\n\n" +
|
|
75
|
+
"Tracks grammar/spelling mistakes spotted during Claude Code sessions.\n" +
|
|
76
|
+
"Generated and appended to by santree's english-tutor hook.\n";
|
|
77
|
+
fs.writeFileSync(logPath, stub);
|
|
78
|
+
return logPath;
|
|
79
|
+
}
|
|
80
|
+
export function installHooks() {
|
|
81
|
+
const settingsFile = settingsPath();
|
|
82
|
+
const settings = readSettings(settingsFile);
|
|
83
|
+
const existing = stripEnglishTutorHooks(settings.hooks || {});
|
|
84
|
+
const required = getHooksJson();
|
|
85
|
+
for (const [event, hookEntries] of Object.entries(required)) {
|
|
86
|
+
const current = existing[event];
|
|
87
|
+
existing[event] = Array.isArray(current)
|
|
88
|
+
? [...current, ...hookEntries]
|
|
89
|
+
: hookEntries;
|
|
90
|
+
}
|
|
91
|
+
settings.hooks = existing;
|
|
92
|
+
const permissions = settings.permissions || {};
|
|
93
|
+
const allow = Array.isArray(permissions.allow) ? permissions.allow : [];
|
|
94
|
+
const entry = getPermissionEntry();
|
|
95
|
+
if (!allow.includes(entry))
|
|
96
|
+
allow.push(entry);
|
|
97
|
+
permissions.allow = allow;
|
|
98
|
+
settings.permissions = permissions;
|
|
99
|
+
writeSettings(settingsFile, settings);
|
|
100
|
+
const logPath = ensureLogExists();
|
|
101
|
+
return { settingsPath: settingsFile, logPath };
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Remove hooks and permission entry. Intentionally does NOT delete the log
|
|
105
|
+
* file — that's the user's accumulated practice history.
|
|
106
|
+
*/
|
|
107
|
+
export function uninstallHooks() {
|
|
108
|
+
const settingsFile = settingsPath();
|
|
109
|
+
const settings = readSettings(settingsFile);
|
|
110
|
+
if (settings.hooks) {
|
|
111
|
+
settings.hooks = stripEnglishTutorHooks(settings.hooks);
|
|
112
|
+
}
|
|
113
|
+
if (settings.permissions && Array.isArray(settings.permissions.allow)) {
|
|
114
|
+
const entry = getPermissionEntry();
|
|
115
|
+
settings.permissions.allow = settings.permissions.allow.filter((e) => e !== entry);
|
|
116
|
+
}
|
|
117
|
+
writeSettings(settingsFile, settings);
|
|
118
|
+
return settingsFile;
|
|
119
|
+
}
|
|
120
|
+
export function getInstallSnippet() {
|
|
121
|
+
return {
|
|
122
|
+
hooks: getHooksJson(),
|
|
123
|
+
permissions: { allow: [getPermissionEntry()] },
|
|
124
|
+
};
|
|
125
|
+
}
|
package/dist/lib/git.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
export { getSantreeDir, readAllMetadata, writeAllMetadata } from "./metadata.js";
|
|
1
2
|
export interface SessionState {
|
|
2
3
|
state: "waiting" | "idle" | "active" | "exited";
|
|
3
4
|
message: string | null;
|
|
@@ -53,10 +54,6 @@ export declare function getDefaultBranch(): string;
|
|
|
53
54
|
* Returns an empty array on failure.
|
|
54
55
|
*/
|
|
55
56
|
export declare function listWorktrees(): Worktree[];
|
|
56
|
-
/**
|
|
57
|
-
* Get the path to the .santree directory inside a repo root.
|
|
58
|
-
*/
|
|
59
|
-
export declare function getSantreeDir(repoRoot: string): string;
|
|
60
57
|
/**
|
|
61
58
|
* Get the path to the .santree/worktrees directory inside a repo root.
|
|
62
59
|
*/
|
|
@@ -82,9 +79,14 @@ export declare function removeWorktree(branchName: string, repoRoot: string, for
|
|
|
82
79
|
error?: string;
|
|
83
80
|
}>;
|
|
84
81
|
/**
|
|
85
|
-
* Extract
|
|
86
|
-
*
|
|
87
|
-
*
|
|
82
|
+
* Extract an issue identifier from a branch name. Delegates to the active
|
|
83
|
+
* issue tracker (Linear: `[A-Z]+-\d+`; GitHub: explicit-prefix numeric),
|
|
84
|
+
* resolving the tracker from the main repo's config.
|
|
85
|
+
*
|
|
86
|
+
* This is the single shim that frees every caller from having to know which
|
|
87
|
+
* tracker is active or how it formats IDs. Returns null when the active
|
|
88
|
+
* tracker doesn't recognize the branch's ID shape, or when no main repo can
|
|
89
|
+
* be resolved (e.g. before a worktree is created).
|
|
88
90
|
*/
|
|
89
91
|
export declare function extractTicketId(branch: string): string | null;
|
|
90
92
|
/**
|
|
@@ -93,30 +95,6 @@ export declare function extractTicketId(branch: string): string | null;
|
|
|
93
95
|
* Returns null if no worktree is checked out on that branch.
|
|
94
96
|
*/
|
|
95
97
|
export declare function getWorktreePath(branchName: string): string | null;
|
|
96
|
-
/**
|
|
97
|
-
* Read all entries from .santree/metadata.json.
|
|
98
|
-
* Returns an empty object if the file doesn't exist or can't be parsed.
|
|
99
|
-
*/
|
|
100
|
-
export declare function readAllMetadata(repoRoot: string): Record<string, any>;
|
|
101
|
-
/**
|
|
102
|
-
* Write all entries to .santree/metadata.json.
|
|
103
|
-
*/
|
|
104
|
-
export declare function writeAllMetadata(repoRoot: string, data: Record<string, any>): void;
|
|
105
|
-
/**
|
|
106
|
-
* Get the Linear org slug associated with this repo.
|
|
107
|
-
* Stored as `_linear.org` in .santree/metadata.json.
|
|
108
|
-
*/
|
|
109
|
-
export declare function getRepoLinearOrg(repoRoot: string): string | null;
|
|
110
|
-
/**
|
|
111
|
-
* Associate a Linear org slug with this repo.
|
|
112
|
-
* Stored as `_linear.org` in .santree/metadata.json.
|
|
113
|
-
*/
|
|
114
|
-
export declare function setRepoLinearOrg(repoRoot: string, orgSlug: string): void;
|
|
115
|
-
/**
|
|
116
|
-
* Remove the Linear org association from this repo.
|
|
117
|
-
* Deletes the `_linear` key from .santree/metadata.json.
|
|
118
|
-
*/
|
|
119
|
-
export declare function removeRepoLinearOrg(repoRoot: string): void;
|
|
120
98
|
/**
|
|
121
99
|
* Get the stored session ID for a given ticket from .santree/metadata.json.
|
|
122
100
|
* Returns null if no session ID is stored.
|
|
@@ -191,8 +169,13 @@ export declare function getCommitsAhead(baseBranch: string): number;
|
|
|
191
169
|
*/
|
|
192
170
|
export declare function getCommitsAheadAsync(cwd: string, baseBranch: string): Promise<number>;
|
|
193
171
|
/**
|
|
194
|
-
* Read the SANTREE_DIFF_TOOL env var, returning the configured pager
|
|
195
|
-
* (e.g. "delta", "diff-so-fancy") or null if unset/invalid.
|
|
172
|
+
* Read the SANTREE_DIFF_TOOL env var, returning the configured diff pager
|
|
173
|
+
* command (e.g. "delta", "diff-so-fancy") or null if unset/invalid.
|
|
174
|
+
*
|
|
175
|
+
* The CLI `worktree diff` flow lets the pager do its full job (render +
|
|
176
|
+
* scroll) via git's `core.pager`. The dashboard's `[v]` overlay only uses
|
|
177
|
+
* the rendering half — it captures the pager's stdout as a string and
|
|
178
|
+
* handles scrolling itself in Ink.
|
|
196
179
|
*
|
|
197
180
|
* The value is restricted to a safe shell-token character set since it ends
|
|
198
181
|
* up in arguments passed to spawn() — even though we never use shell:true,
|
package/dist/lib/git.js
CHANGED
|
@@ -4,6 +4,9 @@ import * as path from "path";
|
|
|
4
4
|
import * as fs from "fs";
|
|
5
5
|
import { run, runAsync, spawnAsync } from "./exec.js";
|
|
6
6
|
import { getMultiplexer } from "./multiplexer/index.js";
|
|
7
|
+
import { getSantreeDir, readAllMetadata, writeAllMetadata } from "./metadata.js";
|
|
8
|
+
import { getIssueTracker } from "./trackers/index.js";
|
|
9
|
+
export { getSantreeDir, readAllMetadata, writeAllMetadata } from "./metadata.js";
|
|
7
10
|
const execAsync = promisify(exec);
|
|
8
11
|
/**
|
|
9
12
|
* Find the toplevel directory of the current git repository.
|
|
@@ -114,12 +117,6 @@ export function listWorktrees() {
|
|
|
114
117
|
}
|
|
115
118
|
return worktrees;
|
|
116
119
|
}
|
|
117
|
-
/**
|
|
118
|
-
* Get the path to the .santree directory inside a repo root.
|
|
119
|
-
*/
|
|
120
|
-
export function getSantreeDir(repoRoot) {
|
|
121
|
-
return path.join(repoRoot, ".santree");
|
|
122
|
-
}
|
|
123
120
|
/**
|
|
124
121
|
* Get the path to the .santree/worktrees directory inside a repo root.
|
|
125
122
|
*/
|
|
@@ -247,16 +244,18 @@ export async function removeWorktree(branchName, repoRoot, force = false) {
|
|
|
247
244
|
}
|
|
248
245
|
}
|
|
249
246
|
/**
|
|
250
|
-
* Extract
|
|
251
|
-
*
|
|
252
|
-
*
|
|
247
|
+
* Extract an issue identifier from a branch name. Delegates to the active
|
|
248
|
+
* issue tracker (Linear: `[A-Z]+-\d+`; GitHub: explicit-prefix numeric),
|
|
249
|
+
* resolving the tracker from the main repo's config.
|
|
250
|
+
*
|
|
251
|
+
* This is the single shim that frees every caller from having to know which
|
|
252
|
+
* tracker is active or how it formats IDs. Returns null when the active
|
|
253
|
+
* tracker doesn't recognize the branch's ID shape, or when no main repo can
|
|
254
|
+
* be resolved (e.g. before a worktree is created).
|
|
253
255
|
*/
|
|
254
256
|
export function extractTicketId(branch) {
|
|
255
|
-
const
|
|
256
|
-
|
|
257
|
-
return `${match[1].toUpperCase()}-${match[2]}`;
|
|
258
|
-
}
|
|
259
|
-
return null;
|
|
257
|
+
const repoRoot = findMainRepoRoot();
|
|
258
|
+
return getIssueTracker(repoRoot).extractIdFromBranch(branch);
|
|
260
259
|
}
|
|
261
260
|
/**
|
|
262
261
|
* Get the filesystem path for a worktree by its branch name.
|
|
@@ -278,64 +277,6 @@ export function getWorktreePath(branchName) {
|
|
|
278
277
|
}
|
|
279
278
|
return null;
|
|
280
279
|
}
|
|
281
|
-
/**
|
|
282
|
-
* Get path to centralized metadata file: .santree/metadata.json in the repo root.
|
|
283
|
-
*/
|
|
284
|
-
function getMetadataFilePath(repoRoot) {
|
|
285
|
-
return path.join(getSantreeDir(repoRoot), "metadata.json");
|
|
286
|
-
}
|
|
287
|
-
/**
|
|
288
|
-
* Read all entries from .santree/metadata.json.
|
|
289
|
-
* Returns an empty object if the file doesn't exist or can't be parsed.
|
|
290
|
-
*/
|
|
291
|
-
export function readAllMetadata(repoRoot) {
|
|
292
|
-
const filePath = getMetadataFilePath(repoRoot);
|
|
293
|
-
if (!fs.existsSync(filePath))
|
|
294
|
-
return {};
|
|
295
|
-
try {
|
|
296
|
-
return JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
|
297
|
-
}
|
|
298
|
-
catch {
|
|
299
|
-
return {};
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
/**
|
|
303
|
-
* Write all entries to .santree/metadata.json.
|
|
304
|
-
*/
|
|
305
|
-
export function writeAllMetadata(repoRoot, data) {
|
|
306
|
-
const filePath = getMetadataFilePath(repoRoot);
|
|
307
|
-
const dir = path.dirname(filePath);
|
|
308
|
-
if (!fs.existsSync(dir)) {
|
|
309
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
310
|
-
}
|
|
311
|
-
fs.writeFileSync(filePath, JSON.stringify(data, null, 2) + "\n");
|
|
312
|
-
}
|
|
313
|
-
/**
|
|
314
|
-
* Get the Linear org slug associated with this repo.
|
|
315
|
-
* Stored as `_linear.org` in .santree/metadata.json.
|
|
316
|
-
*/
|
|
317
|
-
export function getRepoLinearOrg(repoRoot) {
|
|
318
|
-
const all = readAllMetadata(repoRoot);
|
|
319
|
-
return all._linear?.org ?? null;
|
|
320
|
-
}
|
|
321
|
-
/**
|
|
322
|
-
* Associate a Linear org slug with this repo.
|
|
323
|
-
* Stored as `_linear.org` in .santree/metadata.json.
|
|
324
|
-
*/
|
|
325
|
-
export function setRepoLinearOrg(repoRoot, orgSlug) {
|
|
326
|
-
const all = readAllMetadata(repoRoot);
|
|
327
|
-
all._linear = { org: orgSlug };
|
|
328
|
-
writeAllMetadata(repoRoot, all);
|
|
329
|
-
}
|
|
330
|
-
/**
|
|
331
|
-
* Remove the Linear org association from this repo.
|
|
332
|
-
* Deletes the `_linear` key from .santree/metadata.json.
|
|
333
|
-
*/
|
|
334
|
-
export function removeRepoLinearOrg(repoRoot) {
|
|
335
|
-
const all = readAllMetadata(repoRoot);
|
|
336
|
-
delete all._linear;
|
|
337
|
-
writeAllMetadata(repoRoot, all);
|
|
338
|
-
}
|
|
339
280
|
/**
|
|
340
281
|
* Get the stored session ID for a given ticket from .santree/metadata.json.
|
|
341
282
|
* Returns null if no session ID is stored.
|
|
@@ -469,8 +410,13 @@ export async function getCommitsAheadAsync(cwd, baseBranch) {
|
|
|
469
410
|
return output ? parseInt(output, 10) || 0 : 0;
|
|
470
411
|
}
|
|
471
412
|
/**
|
|
472
|
-
* Read the SANTREE_DIFF_TOOL env var, returning the configured pager
|
|
473
|
-
* (e.g. "delta", "diff-so-fancy") or null if unset/invalid.
|
|
413
|
+
* Read the SANTREE_DIFF_TOOL env var, returning the configured diff pager
|
|
414
|
+
* command (e.g. "delta", "diff-so-fancy") or null if unset/invalid.
|
|
415
|
+
*
|
|
416
|
+
* The CLI `worktree diff` flow lets the pager do its full job (render +
|
|
417
|
+
* scroll) via git's `core.pager`. The dashboard's `[v]` overlay only uses
|
|
418
|
+
* the rendering half — it captures the pager's stdout as a string and
|
|
419
|
+
* handles scrolling itself in Ink.
|
|
474
420
|
*
|
|
475
421
|
* The value is restricted to a safe shell-token character set since it ends
|
|
476
422
|
* up in arguments passed to spawn() — even though we never use shell:true,
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
export function getSantreeDir(repoRoot) {
|
|
4
|
+
return path.join(repoRoot, ".santree");
|
|
5
|
+
}
|
|
6
|
+
function getMetadataFilePath(repoRoot) {
|
|
7
|
+
return path.join(getSantreeDir(repoRoot), "metadata.json");
|
|
8
|
+
}
|
|
9
|
+
export function readAllMetadata(repoRoot) {
|
|
10
|
+
const filePath = getMetadataFilePath(repoRoot);
|
|
11
|
+
if (!fs.existsSync(filePath))
|
|
12
|
+
return {};
|
|
13
|
+
try {
|
|
14
|
+
return JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return {};
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
export function writeAllMetadata(repoRoot, data) {
|
|
21
|
+
const filePath = getMetadataFilePath(repoRoot);
|
|
22
|
+
const dir = path.dirname(filePath);
|
|
23
|
+
if (!fs.existsSync(dir)) {
|
|
24
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
25
|
+
}
|
|
26
|
+
fs.writeFileSync(filePath, JSON.stringify(data, null, 2) + "\n");
|
|
27
|
+
}
|
|
@@ -1,19 +1,12 @@
|
|
|
1
1
|
import { cmuxMultiplexer } from "./cmux.js";
|
|
2
2
|
import { noneMultiplexer } from "./none.js";
|
|
3
3
|
import { tmuxMultiplexer } from "./tmux.js";
|
|
4
|
+
// Each adapter declares its own runtime detection in `isActive()`. Order matters:
|
|
5
|
+
// if more than one adapter reports active (e.g. tmux running inside a cmux
|
|
6
|
+
// workspace), the first match wins.
|
|
7
|
+
const CANDIDATES = [tmuxMultiplexer, cmuxMultiplexer];
|
|
4
8
|
export function getMultiplexer() {
|
|
5
|
-
|
|
6
|
-
if (explicit === "tmux")
|
|
7
|
-
return tmuxMultiplexer;
|
|
8
|
-
if (explicit === "cmux")
|
|
9
|
-
return cmuxMultiplexer;
|
|
10
|
-
if (explicit === "none")
|
|
11
|
-
return noneMultiplexer;
|
|
12
|
-
if (process.env["TMUX"])
|
|
13
|
-
return tmuxMultiplexer;
|
|
14
|
-
if (process.env["CMUX_SURFACE_ID"])
|
|
15
|
-
return cmuxMultiplexer;
|
|
16
|
-
return noneMultiplexer;
|
|
9
|
+
return CANDIDATES.find((m) => m.isActive()) ?? noneMultiplexer;
|
|
17
10
|
}
|
|
18
11
|
export function getMultiplexerKind() {
|
|
19
12
|
return getMultiplexer().kind;
|
package/dist/lib/prompts.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { Issue } from "./trackers/types.js";
|
|
2
2
|
import type { PRCheck, PRReview, PRReviewComment, FailedCheckDetail, PRConversationComment } from "./github.js";
|
|
3
3
|
/**
|
|
4
4
|
* Render a nunjucks template from the prompts/ directory.
|
|
@@ -7,9 +7,10 @@ import type { PRCheck, PRReview, PRReviewComment, FailedCheckDetail, PRConversat
|
|
|
7
7
|
*/
|
|
8
8
|
export declare function renderPrompt(template: string, context: Record<string, string | undefined>): string;
|
|
9
9
|
/**
|
|
10
|
-
* Render
|
|
10
|
+
* Render an issue into formatted markdown using the ticket template.
|
|
11
|
+
* `trackerName` is injected for header/link text ("Linear" / "GitHub").
|
|
11
12
|
*/
|
|
12
|
-
export declare function renderTicket(issue:
|
|
13
|
+
export declare function renderTicket(issue: Issue, trackerName: string): string;
|
|
13
14
|
export interface DiffData {
|
|
14
15
|
base_branch: string;
|
|
15
16
|
commit_log: string | null;
|
package/dist/lib/prompts.js
CHANGED
|
@@ -33,10 +33,11 @@ export function renderPrompt(template, context) {
|
|
|
33
33
|
return promptsEnv.render(`${template}.njk`, context);
|
|
34
34
|
}
|
|
35
35
|
/**
|
|
36
|
-
* Render
|
|
36
|
+
* Render an issue into formatted markdown using the ticket template.
|
|
37
|
+
* `trackerName` is injected for header/link text ("Linear" / "GitHub").
|
|
37
38
|
*/
|
|
38
|
-
export function renderTicket(issue) {
|
|
39
|
-
return promptsEnv.render("ticket.njk", issue);
|
|
39
|
+
export function renderTicket(issue, trackerName) {
|
|
40
|
+
return promptsEnv.render("ticket.njk", { ...issue, trackerName });
|
|
40
41
|
}
|
|
41
42
|
/**
|
|
42
43
|
* Render diff data into formatted markdown using the diff template.
|
|
@@ -4,11 +4,10 @@ export declare function extractRepoAndTicket(cwd: string): {
|
|
|
4
4
|
repoRoot: string;
|
|
5
5
|
ticketId: string;
|
|
6
6
|
} | null;
|
|
7
|
-
export declare function
|
|
8
|
-
export declare function runHookScript(repoRoot: string, state: SessionStateValue, env: Record<string, string>): void;
|
|
7
|
+
export declare function renameSessionWindow(ticketId: string, state: SessionStateValue): void;
|
|
9
8
|
/**
|
|
10
9
|
* Unified helper: reads stdin, extracts repo/ticket, writes state file,
|
|
11
|
-
* renames
|
|
10
|
+
* renames the multiplexer window, then exits.
|
|
12
11
|
*/
|
|
13
12
|
export declare function signalState(state: SessionStateValue): void;
|
|
14
13
|
export declare function getHooksJson(): Record<string, unknown>;
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import * as fs from "fs";
|
|
2
2
|
import * as path from "path";
|
|
3
|
-
import { spawn } from "child_process";
|
|
4
3
|
import { getMultiplexer } from "./multiplexer/index.js";
|
|
5
4
|
export function readStdin() {
|
|
6
5
|
try {
|
|
@@ -22,7 +21,7 @@ export function extractRepoAndTicket(cwd) {
|
|
|
22
21
|
return null;
|
|
23
22
|
return { repoRoot, ticketId };
|
|
24
23
|
}
|
|
25
|
-
export function
|
|
24
|
+
export function renameSessionWindow(ticketId, state) {
|
|
26
25
|
const mux = getMultiplexer();
|
|
27
26
|
if (!mux.isActive())
|
|
28
27
|
return;
|
|
@@ -40,25 +39,9 @@ export function renameTmuxWindow(ticketId, state) {
|
|
|
40
39
|
}
|
|
41
40
|
mux.renameWindow("", name);
|
|
42
41
|
}
|
|
43
|
-
export function runHookScript(repoRoot, state, env) {
|
|
44
|
-
const script = path.join(repoRoot, ".santree", "hooks", `on-${state}.sh`);
|
|
45
|
-
try {
|
|
46
|
-
fs.accessSync(script, fs.constants.X_OK);
|
|
47
|
-
}
|
|
48
|
-
catch {
|
|
49
|
-
return;
|
|
50
|
-
}
|
|
51
|
-
const child = spawn(script, [], {
|
|
52
|
-
cwd: env.SANTREE_WORKTREE_PATH,
|
|
53
|
-
env: { ...process.env, ...env },
|
|
54
|
-
stdio: "ignore",
|
|
55
|
-
detached: true,
|
|
56
|
-
});
|
|
57
|
-
child.unref();
|
|
58
|
-
}
|
|
59
42
|
/**
|
|
60
43
|
* Unified helper: reads stdin, extracts repo/ticket, writes state file,
|
|
61
|
-
* renames
|
|
44
|
+
* renames the multiplexer window, then exits.
|
|
62
45
|
*/
|
|
63
46
|
export function signalState(state) {
|
|
64
47
|
const input = readStdin();
|
|
@@ -85,16 +68,7 @@ export function signalState(state) {
|
|
|
85
68
|
at: new Date().toISOString(),
|
|
86
69
|
};
|
|
87
70
|
fs.writeFileSync(stateFile, JSON.stringify(payload, null, 2) + "\n");
|
|
88
|
-
|
|
89
|
-
const worktreePath = path.join(repoRoot, ".santree", "worktrees", ticketId);
|
|
90
|
-
runHookScript(repoRoot, state, {
|
|
91
|
-
SANTREE_TICKET_ID: ticketId,
|
|
92
|
-
SANTREE_SESSION_STATE: state,
|
|
93
|
-
SANTREE_SESSION_ID: data.session_id ?? "",
|
|
94
|
-
SANTREE_WORKTREE_PATH: worktreePath,
|
|
95
|
-
SANTREE_REPO_ROOT: repoRoot,
|
|
96
|
-
SANTREE_MESSAGE: payload.message ?? "",
|
|
97
|
-
});
|
|
71
|
+
renameSessionWindow(ticketId, state);
|
|
98
72
|
process.exit(0);
|
|
99
73
|
}
|
|
100
74
|
export function getHooksJson() {
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export interface LinearTokens {
|
|
2
|
+
access_token: string;
|
|
3
|
+
refresh_token: string;
|
|
4
|
+
expires_at: number;
|
|
5
|
+
org_name: string;
|
|
6
|
+
}
|
|
7
|
+
export interface AuthStoreV2 {
|
|
8
|
+
version: 2;
|
|
9
|
+
linear: Record<string, LinearTokens>;
|
|
10
|
+
github: Record<string, never>;
|
|
11
|
+
}
|
|
12
|
+
export declare function readAuthStore(): AuthStoreV2;
|
|
13
|
+
export declare function writeAuthStore(store: AuthStoreV2): void;
|
|
14
|
+
export declare function readLinearAuthStore(): Record<string, LinearTokens>;
|
|
15
|
+
export declare function writeLinearTokens(orgSlug: string, tokens: LinearTokens): void;
|
|
16
|
+
export declare function deleteLinearTokens(orgSlug: string): void;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
import * as os from "os";
|
|
4
|
+
const CONFIG_DIR = process.env.XDG_CONFIG_HOME || path.join(os.homedir(), ".config");
|
|
5
|
+
const AUTH_FILE_PATH = path.join(CONFIG_DIR, "santree", "auth.json");
|
|
6
|
+
function emptyStore() {
|
|
7
|
+
return { version: 2, linear: {}, github: {} };
|
|
8
|
+
}
|
|
9
|
+
export function readAuthStore() {
|
|
10
|
+
if (!fs.existsSync(AUTH_FILE_PATH))
|
|
11
|
+
return emptyStore();
|
|
12
|
+
let raw;
|
|
13
|
+
try {
|
|
14
|
+
raw = JSON.parse(fs.readFileSync(AUTH_FILE_PATH, "utf-8"));
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return emptyStore();
|
|
18
|
+
}
|
|
19
|
+
if (raw && typeof raw === "object" && raw.version === 2) {
|
|
20
|
+
const v2 = raw;
|
|
21
|
+
return {
|
|
22
|
+
version: 2,
|
|
23
|
+
linear: v2.linear ?? {},
|
|
24
|
+
github: (v2.github ?? {}),
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
const migrated = emptyStore();
|
|
28
|
+
if (raw && typeof raw === "object") {
|
|
29
|
+
for (const [k, v] of Object.entries(raw)) {
|
|
30
|
+
if (v && typeof v === "object" && "access_token" in v) {
|
|
31
|
+
migrated.linear[k] = v;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
writeAuthStore(migrated);
|
|
36
|
+
return migrated;
|
|
37
|
+
}
|
|
38
|
+
export function writeAuthStore(store) {
|
|
39
|
+
const dir = path.dirname(AUTH_FILE_PATH);
|
|
40
|
+
if (!fs.existsSync(dir)) {
|
|
41
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
42
|
+
}
|
|
43
|
+
fs.writeFileSync(AUTH_FILE_PATH, JSON.stringify(store, null, 2) + "\n", { mode: 0o600 });
|
|
44
|
+
}
|
|
45
|
+
export function readLinearAuthStore() {
|
|
46
|
+
return readAuthStore().linear;
|
|
47
|
+
}
|
|
48
|
+
export function writeLinearTokens(orgSlug, tokens) {
|
|
49
|
+
const store = readAuthStore();
|
|
50
|
+
store.linear[orgSlug] = tokens;
|
|
51
|
+
writeAuthStore(store);
|
|
52
|
+
}
|
|
53
|
+
export function deleteLinearTokens(orgSlug) {
|
|
54
|
+
const store = readAuthStore();
|
|
55
|
+
delete store.linear[orgSlug];
|
|
56
|
+
writeAuthStore(store);
|
|
57
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { IssueTrackerKind } from "./types.js";
|
|
2
|
+
export interface TrackerConfig {
|
|
3
|
+
kind: IssueTrackerKind | null;
|
|
4
|
+
legacyLinearOrg: string | null;
|
|
5
|
+
}
|
|
6
|
+
export declare function readTrackerConfig(repoRoot: string): TrackerConfig;
|
|
7
|
+
export declare function setRepoTracker(repoRoot: string, kind: IssueTrackerKind): void;
|
|
8
|
+
export declare function removeRepoTracker(repoRoot: string): void;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { readAllMetadata, writeAllMetadata } from "../metadata.js";
|
|
2
|
+
export function readTrackerConfig(repoRoot) {
|
|
3
|
+
const all = readAllMetadata(repoRoot);
|
|
4
|
+
const tracker = all._tracker;
|
|
5
|
+
const linear = all._linear;
|
|
6
|
+
let kind = null;
|
|
7
|
+
if (tracker?.kind === "linear" || tracker?.kind === "github") {
|
|
8
|
+
kind = tracker.kind;
|
|
9
|
+
}
|
|
10
|
+
return { kind, legacyLinearOrg: linear?.org ?? null };
|
|
11
|
+
}
|
|
12
|
+
export function setRepoTracker(repoRoot, kind) {
|
|
13
|
+
const all = readAllMetadata(repoRoot);
|
|
14
|
+
all._tracker = { kind };
|
|
15
|
+
writeAllMetadata(repoRoot, all);
|
|
16
|
+
}
|
|
17
|
+
export function removeRepoTracker(repoRoot) {
|
|
18
|
+
const all = readAllMetadata(repoRoot);
|
|
19
|
+
delete all._tracker;
|
|
20
|
+
writeAllMetadata(repoRoot, all);
|
|
21
|
+
}
|