git-stint 0.1.1

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.
@@ -0,0 +1,165 @@
1
+ import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
2
+ import { join, resolve } from "node:path";
3
+ import { getGitCommonDir, gitInDir } from "./git.js";
4
+ const MANIFEST_VERSION = 1;
5
+ // --- Path constants ---
6
+ const BRANCH_PREFIX = "stint/";
7
+ const WORKTREE_DIR = ".stint";
8
+ const SESSIONS_DIR = "sessions";
9
+ export { BRANCH_PREFIX, WORKTREE_DIR, MANIFEST_VERSION };
10
+ export function getSessionsDir() {
11
+ const gitDir = resolve(getGitCommonDir());
12
+ const dir = join(gitDir, SESSIONS_DIR);
13
+ if (!existsSync(dir)) {
14
+ mkdirSync(dir, { recursive: true });
15
+ }
16
+ return dir;
17
+ }
18
+ /**
19
+ * Validate that parsed JSON has the required manifest fields.
20
+ * Returns null if invalid, the validated manifest otherwise.
21
+ */
22
+ function validateManifest(data) {
23
+ if (!data || typeof data !== "object")
24
+ return null;
25
+ const d = data;
26
+ if (typeof d.name !== "string" || !d.name)
27
+ return null;
28
+ if (typeof d.startedAt !== "string")
29
+ return null;
30
+ if (typeof d.baseline !== "string")
31
+ return null;
32
+ if (typeof d.branch !== "string")
33
+ return null;
34
+ if (typeof d.worktree !== "string")
35
+ return null;
36
+ if (!Array.isArray(d.changesets))
37
+ return null;
38
+ if (!Array.isArray(d.pending))
39
+ return null;
40
+ // Back-compat: manifests created before version field was added
41
+ if (!d.version)
42
+ d.version = 1;
43
+ return d;
44
+ }
45
+ export function loadManifest(name) {
46
+ const file = join(getSessionsDir(), `${name}.json`);
47
+ if (!existsSync(file))
48
+ return null;
49
+ try {
50
+ const data = JSON.parse(readFileSync(file, "utf-8"));
51
+ return validateManifest(data);
52
+ }
53
+ catch {
54
+ return null; // corrupted manifest — treat as missing
55
+ }
56
+ }
57
+ /**
58
+ * Atomic write: write to temp file, then rename.
59
+ * Prevents corruption if the process is killed mid-write.
60
+ */
61
+ export function saveManifest(manifest) {
62
+ const dir = getSessionsDir();
63
+ const target = join(dir, `${manifest.name}.json`);
64
+ const tmp = join(dir, `${manifest.name}.json.tmp`);
65
+ writeFileSync(tmp, JSON.stringify(manifest, null, 2) + "\n");
66
+ renameSync(tmp, target);
67
+ }
68
+ export function listManifests() {
69
+ const dir = getSessionsDir();
70
+ const files = readdirSync(dir).filter((f) => f.endsWith(".json") && !f.endsWith(".tmp"));
71
+ const manifests = [];
72
+ for (const f of files) {
73
+ try {
74
+ const data = JSON.parse(readFileSync(join(dir, f), "utf-8"));
75
+ const m = validateManifest(data);
76
+ if (m)
77
+ manifests.push(m);
78
+ }
79
+ catch {
80
+ // skip corrupted manifests
81
+ }
82
+ }
83
+ return manifests;
84
+ }
85
+ export function deleteManifest(name) {
86
+ const file = join(getSessionsDir(), `${name}.json`);
87
+ if (existsSync(file))
88
+ unlinkSync(file);
89
+ // Also clean up tmp file if it exists
90
+ const tmp = file + ".tmp";
91
+ if (existsSync(tmp))
92
+ unlinkSync(tmp);
93
+ }
94
+ /**
95
+ * Resolve which session is active.
96
+ * Priority:
97
+ * 1. Explicit name passed via --session flag
98
+ * 2. CWD is inside a .stint/<name>/ worktree
99
+ * 3. Only one session exists → use it
100
+ * 4. Error
101
+ */
102
+ export function resolveSession(explicit) {
103
+ if (explicit) {
104
+ const m = loadManifest(explicit);
105
+ if (!m)
106
+ throw new Error(`Session '${explicit}' not found.`);
107
+ return m;
108
+ }
109
+ // Check if CWD is inside a stint worktree by matching against known worktree paths.
110
+ // This avoids false positives from directories that happen to contain "/.stint/".
111
+ const cwd = process.cwd();
112
+ try {
113
+ const repoRoot = getRepoRoot();
114
+ const stintRoot = resolve(repoRoot, WORKTREE_DIR);
115
+ if (cwd.startsWith(stintRoot + "/")) {
116
+ const relative = cwd.slice(stintRoot.length + 1); // e.g. "my-session/src/lib"
117
+ const name = relative.split("/")[0];
118
+ if (name) {
119
+ const m = loadManifest(name);
120
+ if (m)
121
+ return m;
122
+ }
123
+ }
124
+ }
125
+ catch { /* not in a git repo — fall through */ }
126
+ const manifests = listManifests();
127
+ if (manifests.length === 1)
128
+ return manifests[0];
129
+ if (manifests.length === 0)
130
+ throw new Error("No active sessions. Run `git stint start <name>` to create one.");
131
+ const names = manifests.map((m) => m.name).join(", ");
132
+ throw new Error(`Multiple active sessions: ${names}.\n` +
133
+ `Use --session <name> to specify, or cd into a worktree.\n` +
134
+ `Run 'git stint list' to see all sessions.`);
135
+ }
136
+ /**
137
+ * Get the repo root (main worktree root, not a stint worktree).
138
+ * Uses git's --show-toplevel from the main worktree context.
139
+ */
140
+ export function getRepoRoot() {
141
+ const commonDir = resolve(getGitCommonDir());
142
+ // Use git to resolve the toplevel from the common dir's parent.
143
+ // This handles submodules, bare repos, and $GIT_DIR overrides correctly.
144
+ try {
145
+ return gitInDir(resolve(commonDir, ".."), "rev-parse", "--show-toplevel");
146
+ }
147
+ catch {
148
+ // Fallback: parent of .git dir (works for standard layouts)
149
+ return resolve(commonDir, "..");
150
+ }
151
+ }
152
+ /**
153
+ * Get the absolute worktree path for a session.
154
+ */
155
+ export function getWorktreePath(manifest) {
156
+ const root = getRepoRoot();
157
+ return resolve(root, manifest.worktree);
158
+ }
159
+ /**
160
+ * Check if any sessions exist. Cheaper than loading all manifests.
161
+ */
162
+ export function hasAnySessions() {
163
+ const dir = getSessionsDir();
164
+ return readdirSync(dir).some((f) => f.endsWith(".json") && !f.endsWith(".tmp"));
165
+ }
@@ -0,0 +1,19 @@
1
+ export declare function start(name?: string): void;
2
+ export declare function track(files: string[], sessionName?: string): void;
3
+ export declare function status(sessionName?: string): void;
4
+ /** Show both staged and unstaged changes. */
5
+ export declare function diff(sessionName?: string): void;
6
+ export declare function sessionCommit(message: string, sessionName?: string): void;
7
+ export declare function log(sessionName?: string): void;
8
+ export declare function squash(message: string, sessionName?: string): void;
9
+ export declare function merge(sessionName?: string): void;
10
+ /** Push branch and create PR via GitHub CLI. Uses execFileSync to prevent command injection. */
11
+ export declare function pr(title?: string, sessionName?: string): void;
12
+ export declare function end(sessionName?: string): void;
13
+ export declare function abort(sessionName?: string): void;
14
+ /** Revert last commit, keeping changes as unstaged files. */
15
+ export declare function undo(sessionName?: string): void;
16
+ export declare function list(): void;
17
+ export declare function listJson(): void;
18
+ /** Clean up orphaned worktrees, manifests, and branches. */
19
+ export declare function prune(): void;