silvermoon 0.0.1 → 0.0.3

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/src/git.js CHANGED
@@ -1,284 +1,284 @@
1
- import { spawnSync } from "node:child_process";
2
- import { mkdir, mkdtemp, rm } from "node:fs/promises";
3
- import { mkdtempSync, rmSync } from "node:fs";
4
- import { tmpdir } from "node:os";
5
- import { join } from "node:path";
6
-
7
- let commandObserver = null;
8
-
9
- export function runGit(root, args, { env } = {}) {
10
- if (commandObserver) commandObserver([...args]);
11
- const result = spawnSync("git", ["-C", root, ...args], {
12
- encoding: "utf8",
13
- env,
14
- windowsHide: true,
15
- });
16
- return {
17
- error: result.error ?? null,
18
- ok: result.status === 0,
19
- status: result.status,
20
- stderr: result.stderr?.trim() ?? "",
21
- stdout: result.stdout?.trim() ?? "",
22
- };
23
- }
24
-
25
- export async function observeGitCommands(observer, callback) {
26
- if (commandObserver) throw new Error("Git command observation is already active");
27
- commandObserver = observer;
28
- try {
29
- return await callback();
30
- } finally {
31
- commandObserver = null;
32
- }
33
- }
34
-
35
- export function sanitizeGitMessage(value) {
36
- return String(value)
37
- .replace(
38
- /\b([a-z][a-z0-9+.-]*:\/\/)([^\s/@]+)@/gi,
39
- "$1[redacted]@",
40
- )
41
- .replace(
42
- /([?&](?:access_token|auth|credential|key|password|signature|token)=)[^&#\s]+/gi,
43
- "$1[redacted]",
44
- )
45
- .replace(/\bgh[pousr]_[A-Za-z0-9_]{20,}\b/g, "[redacted]");
46
- }
47
-
48
- function requireGit(root, args, label, options) {
49
- const result = runGit(root, args, options);
50
- if (!result.ok) {
51
- const safeResult = {
52
- ...result,
53
- stderr: sanitizeGitMessage(result.stderr),
54
- };
55
- const error = new Error(`${label}: ${safeResult.stderr || result.error?.message || "Git failed"}`);
56
- error.git = safeResult;
57
- throw error;
58
- }
59
- return result.stdout;
60
- }
61
-
62
- export function gitObjectIdLength(root) {
63
- const format = requireGit(
64
- root,
65
- ["rev-parse", "--show-object-format"],
66
- "Cannot determine Git object format",
67
- );
68
- if (format === "sha1") return 40;
69
- if (format === "sha256") return 64;
70
- throw new Error(`Unsupported Git object format: ${format}`);
71
- }
72
-
73
- export function worktreePathTree(root, path) {
74
- const directory = mkdtempSync(join(tmpdir(), "silvermoon-index-"));
75
- const env = { ...process.env, GIT_INDEX_FILE: join(directory, "index") };
76
- try {
77
- const populated = runGit(root, ["read-tree", "HEAD"], { env });
78
- if (!populated.ok) {
79
- requireGit(root, ["read-tree", "--empty"], "Cannot initialize snapshot index", { env });
80
- }
81
- requireGit(
82
- root,
83
- ["add", "--all", "--", path],
84
- `Cannot snapshot ${path}`,
85
- { env },
86
- );
87
- const tree = requireGit(root, ["write-tree"], "Cannot write snapshot tree", { env });
88
- const object = requireGit(
89
- root,
90
- ["rev-parse", `${tree}:${path}`],
91
- `Cannot resolve tree for ${path}`,
92
- );
93
- const type = requireGit(root, ["cat-file", "-t", object], `Cannot inspect ${path}`);
94
- if (type !== "tree") throw new Error(`${path} does not resolve to a Git tree`);
95
- return object;
96
- } finally {
97
- rmSync(directory, { recursive: true, force: true });
98
- }
99
- }
100
- export function worktreeSnapshot(root) {
101
- const directory = mkdtempSync(join(tmpdir(), "silvermoon-index-"));
102
- const env = { ...process.env, GIT_INDEX_FILE: join(directory, "index") };
103
- try {
104
- const populated = runGit(root, ["read-tree", "HEAD"], { env });
105
- if (!populated.ok) {
106
- requireGit(root, ["read-tree", "--empty"], "Cannot initialize snapshot index", { env });
107
- }
108
- requireGit(root, ["add", "--all"], "Cannot snapshot worktree", { env });
109
- return {
110
- tree: requireGit(root, ["write-tree"], "Cannot write worktree snapshot", { env }),
111
- };
112
- } finally {
113
- rmSync(directory, { recursive: true, force: true });
114
- }
115
- }
116
-
117
- function paths(stdout) {
118
- return stdout.split(/\r?\n/).filter(Boolean);
119
- }
120
-
121
- const CHANGE_KINDS = {
122
- A: "added",
123
- C: "copied",
124
- D: "deleted",
125
- M: "modified",
126
- R: "renamed",
127
- T: "type-changed",
128
- U: "unmerged",
129
- };
130
-
131
- const CONFLICT_KINDS = {
132
- AA: "both-added",
133
- AU: "added-by-us",
134
- DD: "both-deleted",
135
- DU: "deleted-by-us",
136
- UA: "added-by-them",
137
- UD: "deleted-by-them",
138
- UU: "both-modified",
139
- };
140
-
141
- function recordFields(record, count) {
142
- const fields = [];
143
- let offset = 2;
144
- for (let index = 0; index < count; index += 1) {
145
- const separator = record.indexOf(" ", offset);
146
- if (separator < 0) throw new Error(`Malformed Git status record: ${record}`);
147
- fields.push(record.slice(offset, separator));
148
- offset = separator + 1;
149
- }
150
- return { fields, path: record.slice(offset) };
151
- }
152
-
153
- function changeEntry(path, code, originalPath) {
154
- const entry = { path, kind: CHANGE_KINDS[code] ?? "unknown" };
155
- if (originalPath !== undefined) entry.originalPath = originalPath;
156
- return entry;
157
- }
158
-
159
- function sortChanges(changes) {
160
- changes.sort((left, right) => left.path < right.path ? -1 : left.path > right.path ? 1 : 0);
161
- }
162
-
163
- export function parseWorktreeChanges(source) {
164
- const result = { staged: [], unstaged: [], untracked: [], conflicted: [] };
165
- const records = source.split("\0");
166
- for (let index = 0; index < records.length; index += 1) {
167
- const record = records[index];
168
- if (!record) continue;
169
- if (record.startsWith("? ")) {
170
- result.untracked.push({ path: record.slice(2) });
171
- continue;
172
- }
173
- if (record.startsWith("! ") || record.startsWith("# ")) continue;
174
- if (record.startsWith("u ")) {
175
- const { fields, path } = recordFields(record, 9);
176
- result.conflicted.push({ path, kind: CONFLICT_KINDS[fields[0]] ?? "unmerged" });
177
- continue;
178
- }
179
- if (record.startsWith("1 ") || record.startsWith("2 ")) {
180
- const renamed = record.startsWith("2 ");
181
- const { fields, path } = recordFields(record, renamed ? 8 : 7);
182
- const [indexCode, worktreeCode] = fields[0];
183
- const originalPath = renamed ? records[++index] : undefined;
184
- if (indexCode !== ".") {
185
- result.staged.push(changeEntry(path, indexCode, originalPath));
186
- }
187
- if (worktreeCode !== ".") {
188
- result.unstaged.push(changeEntry(path, worktreeCode));
189
- }
190
- continue;
191
- }
192
- throw new Error(`Unsupported Git status record: ${record}`);
193
- }
194
- for (const changes of Object.values(result)) sortChanges(changes);
195
- return result;
196
- }
197
-
198
- export function resolveCommit(root, revision) {
199
- return requireGit(
200
- root,
201
- ["rev-parse", "--verify", "--end-of-options", `${revision}^{commit}`],
202
- `Cannot resolve commit ${revision}`,
203
- );
204
- }
205
-
206
- export function commitChangedPaths(root, commit) {
207
- const parent = runGit(root, ["rev-parse", "--verify", `${commit}^1`]);
208
- const changed = parent.ok
209
- ? requireGit(root, ["diff", "--name-only", parent.stdout, commit, "--"], `Cannot inspect commit ${commit}`)
210
- : requireGit(
211
- root,
212
- ["diff-tree", "--root", "--no-commit-id", "--name-only", "-r", commit, "--"],
213
- `Cannot inspect root commit ${commit}`,
214
- );
215
- return paths(changed);
216
- }
217
-
218
- export function indexSnapshot(root) {
219
- return {
220
- paths: paths(requireGit(root, ["diff", "--cached", "--name-only", "--"], "Cannot inspect staged changes")),
221
- tree: requireGit(root, ["write-tree"], "Cannot snapshot the index"),
222
- };
223
- }
224
-
225
- export function fetchRepositoryBranch(root, repository, branch) {
226
- const remoteRef = `refs/heads/${branch}`;
227
- for (let attempt = 0; attempt < 3; attempt += 1) {
228
- const advertised = requireGit(
229
- root,
230
- ["ls-remote", "--refs", repository, remoteRef],
231
- `Cannot inspect ${branch}`,
232
- );
233
- const [primary, advertisedRef] = advertised.split(/\s+/);
234
- if (!primary || advertisedRef !== remoteRef) {
235
- throw new Error(`Cannot resolve remote branch ${branch}`);
236
- }
237
- requireGit(
238
- root,
239
- [
240
- "fetch",
241
- "--no-tags",
242
- "--no-write-fetch-head",
243
- repository,
244
- remoteRef,
245
- ],
246
- `Cannot fetch ${branch}`,
247
- );
248
- if (runGit(root, ["cat-file", "-e", `${primary}^{commit}`]).ok) return primary;
249
- }
250
- throw new Error(`Remote branch ${branch} moved repeatedly while fetching`);
251
- }
252
-
253
- export function fetchPrimary(root, config) {
254
- return fetchRepositoryBranch(
255
- root,
256
- config.primaryRepository,
257
- config.primaryBranch,
258
- );
259
- }
260
-
261
- export async function withTemporaryWorktree(root, commit, callback) {
262
- const tree = requireGit(root, ["rev-parse", `${commit}^{tree}`], `Cannot resolve tree for ${commit}`);
263
- return withTemporaryTree(root, tree, callback);
264
- }
265
-
266
- export async function withTemporaryTree(root, tree, callback) {
267
- const temporaryRoot = await mkdtemp(join(tmpdir(), "silvermoon-tree-"));
268
- const directory = join(temporaryRoot, "snapshot");
269
- const env = { ...process.env, GIT_INDEX_FILE: join(temporaryRoot, "index") };
270
- try {
271
- await mkdir(directory);
272
- requireGit(root, ["read-tree", tree], "Cannot populate isolated index", { env });
273
- const prefix = `${directory.replaceAll("\\", "/")}/`;
274
- requireGit(
275
- root,
276
- ["checkout-index", "--all", "--force", `--prefix=${prefix}`],
277
- "Cannot populate isolated tree snapshot",
278
- { env },
279
- );
280
- return await callback(directory, tree);
281
- } finally {
282
- await rm(temporaryRoot, { recursive: true, force: true });
283
- }
284
- }
1
+ import { spawnSync } from "node:child_process";
2
+ import { mkdir, mkdtemp, rm } from "node:fs/promises";
3
+ import { mkdtempSync, rmSync } from "node:fs";
4
+ import { tmpdir } from "node:os";
5
+ import { join } from "node:path";
6
+
7
+ let commandObserver = null;
8
+
9
+ export function runGit(root, args, { env } = {}) {
10
+ if (commandObserver) commandObserver([...args]);
11
+ const result = spawnSync("git", ["-C", root, ...args], {
12
+ encoding: "utf8",
13
+ env,
14
+ windowsHide: true,
15
+ });
16
+ return {
17
+ error: result.error ?? null,
18
+ ok: result.status === 0,
19
+ status: result.status,
20
+ stderr: result.stderr?.trim() ?? "",
21
+ stdout: result.stdout?.trim() ?? "",
22
+ };
23
+ }
24
+
25
+ export async function observeGitCommands(observer, callback) {
26
+ if (commandObserver) throw new Error("Git command observation is already active");
27
+ commandObserver = observer;
28
+ try {
29
+ return await callback();
30
+ } finally {
31
+ commandObserver = null;
32
+ }
33
+ }
34
+
35
+ export function sanitizeGitMessage(value) {
36
+ return String(value)
37
+ .replace(
38
+ /\b([a-z][a-z0-9+.-]*:\/\/)([^\s/@]+)@/gi,
39
+ "$1[redacted]@",
40
+ )
41
+ .replace(
42
+ /([?&](?:access_token|auth|credential|key|password|signature|token)=)[^&#\s]+/gi,
43
+ "$1[redacted]",
44
+ )
45
+ .replace(/\bgh[pousr]_[A-Za-z0-9_]{20,}\b/g, "[redacted]");
46
+ }
47
+
48
+ function requireGit(root, args, label, options) {
49
+ const result = runGit(root, args, options);
50
+ if (!result.ok) {
51
+ const safeResult = {
52
+ ...result,
53
+ stderr: sanitizeGitMessage(result.stderr),
54
+ };
55
+ const error = new Error(`${label}: ${safeResult.stderr || result.error?.message || "Git failed"}`);
56
+ error.git = safeResult;
57
+ throw error;
58
+ }
59
+ return result.stdout;
60
+ }
61
+
62
+ export function gitObjectIdLength(root) {
63
+ const format = requireGit(
64
+ root,
65
+ ["rev-parse", "--show-object-format"],
66
+ "Cannot determine Git object format",
67
+ );
68
+ if (format === "sha1") return 40;
69
+ if (format === "sha256") return 64;
70
+ throw new Error(`Unsupported Git object format: ${format}`);
71
+ }
72
+
73
+ export function worktreePathTree(root, path) {
74
+ const directory = mkdtempSync(join(tmpdir(), "silvermoon-index-"));
75
+ const env = { ...process.env, GIT_INDEX_FILE: join(directory, "index") };
76
+ try {
77
+ const populated = runGit(root, ["read-tree", "HEAD"], { env });
78
+ if (!populated.ok) {
79
+ requireGit(root, ["read-tree", "--empty"], "Cannot initialize snapshot index", { env });
80
+ }
81
+ requireGit(
82
+ root,
83
+ ["add", "--all", "--", path],
84
+ `Cannot snapshot ${path}`,
85
+ { env },
86
+ );
87
+ const tree = requireGit(root, ["write-tree"], "Cannot write snapshot tree", { env });
88
+ const object = requireGit(
89
+ root,
90
+ ["rev-parse", `${tree}:${path}`],
91
+ `Cannot resolve tree for ${path}`,
92
+ );
93
+ const type = requireGit(root, ["cat-file", "-t", object], `Cannot inspect ${path}`);
94
+ if (type !== "tree") throw new Error(`${path} does not resolve to a Git tree`);
95
+ return object;
96
+ } finally {
97
+ rmSync(directory, { recursive: true, force: true });
98
+ }
99
+ }
100
+ export function worktreeSnapshot(root) {
101
+ const directory = mkdtempSync(join(tmpdir(), "silvermoon-index-"));
102
+ const env = { ...process.env, GIT_INDEX_FILE: join(directory, "index") };
103
+ try {
104
+ const populated = runGit(root, ["read-tree", "HEAD"], { env });
105
+ if (!populated.ok) {
106
+ requireGit(root, ["read-tree", "--empty"], "Cannot initialize snapshot index", { env });
107
+ }
108
+ requireGit(root, ["add", "--all"], "Cannot snapshot worktree", { env });
109
+ return {
110
+ tree: requireGit(root, ["write-tree"], "Cannot write worktree snapshot", { env }),
111
+ };
112
+ } finally {
113
+ rmSync(directory, { recursive: true, force: true });
114
+ }
115
+ }
116
+
117
+ function paths(stdout) {
118
+ return stdout.split(/\r?\n/).filter(Boolean);
119
+ }
120
+
121
+ const CHANGE_KINDS = {
122
+ A: "added",
123
+ C: "copied",
124
+ D: "deleted",
125
+ M: "modified",
126
+ R: "renamed",
127
+ T: "type-changed",
128
+ U: "unmerged",
129
+ };
130
+
131
+ const CONFLICT_KINDS = {
132
+ AA: "both-added",
133
+ AU: "added-by-us",
134
+ DD: "both-deleted",
135
+ DU: "deleted-by-us",
136
+ UA: "added-by-them",
137
+ UD: "deleted-by-them",
138
+ UU: "both-modified",
139
+ };
140
+
141
+ function recordFields(record, count) {
142
+ const fields = [];
143
+ let offset = 2;
144
+ for (let index = 0; index < count; index += 1) {
145
+ const separator = record.indexOf(" ", offset);
146
+ if (separator < 0) throw new Error(`Malformed Git status record: ${record}`);
147
+ fields.push(record.slice(offset, separator));
148
+ offset = separator + 1;
149
+ }
150
+ return { fields, path: record.slice(offset) };
151
+ }
152
+
153
+ function changeEntry(path, code, originalPath) {
154
+ const entry = { path, kind: CHANGE_KINDS[code] ?? "unknown" };
155
+ if (originalPath !== undefined) entry.originalPath = originalPath;
156
+ return entry;
157
+ }
158
+
159
+ function sortChanges(changes) {
160
+ changes.sort((left, right) => left.path < right.path ? -1 : left.path > right.path ? 1 : 0);
161
+ }
162
+
163
+ export function parseWorktreeChanges(source) {
164
+ const result = { staged: [], unstaged: [], untracked: [], conflicted: [] };
165
+ const records = source.split("\0");
166
+ for (let index = 0; index < records.length; index += 1) {
167
+ const record = records[index];
168
+ if (!record) continue;
169
+ if (record.startsWith("? ")) {
170
+ result.untracked.push({ path: record.slice(2) });
171
+ continue;
172
+ }
173
+ if (record.startsWith("! ") || record.startsWith("# ")) continue;
174
+ if (record.startsWith("u ")) {
175
+ const { fields, path } = recordFields(record, 9);
176
+ result.conflicted.push({ path, kind: CONFLICT_KINDS[fields[0]] ?? "unmerged" });
177
+ continue;
178
+ }
179
+ if (record.startsWith("1 ") || record.startsWith("2 ")) {
180
+ const renamed = record.startsWith("2 ");
181
+ const { fields, path } = recordFields(record, renamed ? 8 : 7);
182
+ const [indexCode, worktreeCode] = fields[0];
183
+ const originalPath = renamed ? records[++index] : undefined;
184
+ if (indexCode !== ".") {
185
+ result.staged.push(changeEntry(path, indexCode, originalPath));
186
+ }
187
+ if (worktreeCode !== ".") {
188
+ result.unstaged.push(changeEntry(path, worktreeCode));
189
+ }
190
+ continue;
191
+ }
192
+ throw new Error(`Unsupported Git status record: ${record}`);
193
+ }
194
+ for (const changes of Object.values(result)) sortChanges(changes);
195
+ return result;
196
+ }
197
+
198
+ export function resolveCommit(root, revision) {
199
+ return requireGit(
200
+ root,
201
+ ["rev-parse", "--verify", "--end-of-options", `${revision}^{commit}`],
202
+ `Cannot resolve commit ${revision}`,
203
+ );
204
+ }
205
+
206
+ export function commitChangedPaths(root, commit) {
207
+ const parent = runGit(root, ["rev-parse", "--verify", `${commit}^1`]);
208
+ const changed = parent.ok
209
+ ? requireGit(root, ["diff", "--name-only", parent.stdout, commit, "--"], `Cannot inspect commit ${commit}`)
210
+ : requireGit(
211
+ root,
212
+ ["diff-tree", "--root", "--no-commit-id", "--name-only", "-r", commit, "--"],
213
+ `Cannot inspect root commit ${commit}`,
214
+ );
215
+ return paths(changed);
216
+ }
217
+
218
+ export function indexSnapshot(root) {
219
+ return {
220
+ paths: paths(requireGit(root, ["diff", "--cached", "--name-only", "--"], "Cannot inspect staged changes")),
221
+ tree: requireGit(root, ["write-tree"], "Cannot snapshot the index"),
222
+ };
223
+ }
224
+
225
+ export function fetchRepositoryBranch(root, repository, branch) {
226
+ const remoteRef = `refs/heads/${branch}`;
227
+ for (let attempt = 0; attempt < 3; attempt += 1) {
228
+ const advertised = requireGit(
229
+ root,
230
+ ["ls-remote", "--refs", repository, remoteRef],
231
+ `Cannot inspect ${branch}`,
232
+ );
233
+ const [primary, advertisedRef] = advertised.split(/\s+/);
234
+ if (!primary || advertisedRef !== remoteRef) {
235
+ throw new Error(`Cannot resolve remote branch ${branch}`);
236
+ }
237
+ requireGit(
238
+ root,
239
+ [
240
+ "fetch",
241
+ "--no-tags",
242
+ "--no-write-fetch-head",
243
+ repository,
244
+ remoteRef,
245
+ ],
246
+ `Cannot fetch ${branch}`,
247
+ );
248
+ if (runGit(root, ["cat-file", "-e", `${primary}^{commit}`]).ok) return primary;
249
+ }
250
+ throw new Error(`Remote branch ${branch} moved repeatedly while fetching`);
251
+ }
252
+
253
+ export function fetchPrimary(root, config) {
254
+ return fetchRepositoryBranch(
255
+ root,
256
+ config.primaryRepository,
257
+ config.primaryBranch,
258
+ );
259
+ }
260
+
261
+ export async function withTemporaryWorktree(root, commit, callback) {
262
+ const tree = requireGit(root, ["rev-parse", `${commit}^{tree}`], `Cannot resolve tree for ${commit}`);
263
+ return withTemporaryTree(root, tree, callback);
264
+ }
265
+
266
+ export async function withTemporaryTree(root, tree, callback) {
267
+ const temporaryRoot = await mkdtemp(join(tmpdir(), "silvermoon-tree-"));
268
+ const directory = join(temporaryRoot, "snapshot");
269
+ const env = { ...process.env, GIT_INDEX_FILE: join(temporaryRoot, "index") };
270
+ try {
271
+ await mkdir(directory);
272
+ requireGit(root, ["read-tree", tree], "Cannot populate isolated index", { env });
273
+ const prefix = `${directory.replaceAll("\\", "/")}/`;
274
+ requireGit(
275
+ root,
276
+ ["checkout-index", "--all", "--force", `--prefix=${prefix}`],
277
+ "Cannot populate isolated tree snapshot",
278
+ { env },
279
+ );
280
+ return await callback(directory, tree);
281
+ } finally {
282
+ await rm(temporaryRoot, { recursive: true, force: true });
283
+ }
284
+ }