silvermoon 0.0.3 → 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.
- package/README.md +36 -13
- package/README.zh-CN.md +32 -13
- package/assets/silvermoon-avatar.svg +395 -0
- package/docs/core-concepts.md +14 -12
- package/docs/getting-started.md +71 -29
- package/docs/maintaining.md +15 -3
- package/docs/npm-package-releases.md +25 -7
- package/docs/operations.md +96 -37
- package/docs/reference.md +223 -38
- package/docs/repository-tasks.md +34 -13
- package/package.json +2 -2
- package/schema/v1/idea-status.schema.json +3 -3
- package/skills/silvermoon/SKILL.md +147 -187
- package/skills/silvermoon/references/adoption.md +34 -20
- package/src/adoption.js +136 -260
- package/src/cli.js +123 -110
- package/src/create-idea.js +222 -110
- package/src/dialogue.js +327 -0
- package/src/git.js +172 -97
- package/src/idea-layout.js +59 -123
- package/src/index.js +240 -170
- package/src/language.js +37 -0
- package/src/observation.js +350 -0
- package/src/trace.js +171 -0
- package/src/whatsnext.js +541 -378
package/src/dialogue.js
ADDED
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DEFAULT_LANGUAGE,
|
|
3
|
+
resolveOutputLanguage,
|
|
4
|
+
} from "./language.js";
|
|
5
|
+
|
|
6
|
+
const ACTIVE_STATES = new Set(["preparing", "implementing", "deploying"]);
|
|
7
|
+
const IDEA_STATES = [
|
|
8
|
+
"preparing",
|
|
9
|
+
"implementing",
|
|
10
|
+
"deploying",
|
|
11
|
+
"completed",
|
|
12
|
+
"abandoned",
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
function isChinese(language) {
|
|
16
|
+
return language?.toLowerCase().startsWith("zh");
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function localize(language, english, chinese) {
|
|
20
|
+
return isChinese(language) ? chinese : english;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function diagnosticProblem(diagnostic, language = "en-US") {
|
|
24
|
+
const location = diagnostic.path ? ` (${diagnostic.path})` : "";
|
|
25
|
+
return {
|
|
26
|
+
type: diagnostic.code.replaceAll(".", "-"),
|
|
27
|
+
summary: localize(
|
|
28
|
+
language,
|
|
29
|
+
`${diagnostic.message}${location}`.trim(),
|
|
30
|
+
diagnostic.path
|
|
31
|
+
? `在 ${diagnostic.path} 发现 ${diagnostic.code}:${diagnostic.message}`
|
|
32
|
+
: `发现 ${diagnostic.code}:${diagnostic.message}`,
|
|
33
|
+
),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function diagnosticInstruction(diagnostic, language = "en-US") {
|
|
38
|
+
return localize(
|
|
39
|
+
language,
|
|
40
|
+
diagnostic.remediation,
|
|
41
|
+
diagnostic.path
|
|
42
|
+
? `修复 ${diagnostic.path} 的 ${diagnostic.code}:${diagnostic.remediation}`
|
|
43
|
+
: `修复 ${diagnostic.code}:${diagnostic.remediation}`,
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function outcome(type, status, summary) {
|
|
48
|
+
return { type, status, summary };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function summarizeIdeas(ideas) {
|
|
52
|
+
const counts = Object.fromEntries(IDEA_STATES.map((state) => [state, 0]));
|
|
53
|
+
const activeIdeas = [];
|
|
54
|
+
for (const idea of ideas) {
|
|
55
|
+
counts[idea.state] += 1;
|
|
56
|
+
if (ACTIVE_STATES.has(idea.state)) {
|
|
57
|
+
const reference = { id: idea.id, state: idea.state };
|
|
58
|
+
if (idea.alias !== undefined) reference.alias = idea.alias;
|
|
59
|
+
activeIdeas.push(reference);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
activeIdeas.sort((left, right) => left.id.localeCompare(right.id));
|
|
63
|
+
return { counts, activeIdeas };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function resolvedConfiguration(config, preferredLanguage) {
|
|
67
|
+
return {
|
|
68
|
+
primaryRepository: config.primaryRepository,
|
|
69
|
+
primaryBranch: config.primaryBranch,
|
|
70
|
+
preferredLanguage,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function createEnvelope(intention, observation, outcomes, instructions) {
|
|
75
|
+
return { intention, observation, outcomes, instructions };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function dialogueReadyObservation(observation, state, details = {}) {
|
|
79
|
+
const { root, version, configuration, outputLanguage, problems } = observation;
|
|
80
|
+
return {
|
|
81
|
+
state,
|
|
82
|
+
root,
|
|
83
|
+
version,
|
|
84
|
+
configuration,
|
|
85
|
+
outputLanguage,
|
|
86
|
+
problems,
|
|
87
|
+
...details,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function renderIntention(intention, language, contentLanguage) {
|
|
92
|
+
if (intention.command === "whats-next") {
|
|
93
|
+
return intention.args.idea === null
|
|
94
|
+
? localize(
|
|
95
|
+
language,
|
|
96
|
+
"Determine the available next work.",
|
|
97
|
+
"查看当前可推进的工作。",
|
|
98
|
+
)
|
|
99
|
+
: localize(
|
|
100
|
+
language,
|
|
101
|
+
`Determine what is next for idea ${intention.args.idea}.`,
|
|
102
|
+
`查看 idea ${intention.args.idea} 接下来应做什么。`,
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
if (intention.command === "create-idea") {
|
|
106
|
+
return intention.args.language === null
|
|
107
|
+
? localize(
|
|
108
|
+
language,
|
|
109
|
+
`Create one new idea and use ${contentLanguage} for natural-language content.`,
|
|
110
|
+
`创建一个新 idea,并使用 ${contentLanguage} 撰写自然语言内容。`,
|
|
111
|
+
)
|
|
112
|
+
: localize(
|
|
113
|
+
language,
|
|
114
|
+
`Create one new idea with language ${intention.args.language}.`,
|
|
115
|
+
`使用 ${intention.args.language} 创建一个新 idea。`,
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
throw new Error(`Cannot render dialogue for ${intention.command}`);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function renderVersion(version) {
|
|
122
|
+
if (!version) return null;
|
|
123
|
+
if (version.type === "commit" || version.type === "remote") {
|
|
124
|
+
return version.commit === null
|
|
125
|
+
? version.type
|
|
126
|
+
: `${version.type} ${version.commit}`;
|
|
127
|
+
}
|
|
128
|
+
return version.type;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function codeSpan(value) {
|
|
132
|
+
const text = String(value);
|
|
133
|
+
let longest = 0;
|
|
134
|
+
for (const [run] of text.matchAll(/`+/g)) {
|
|
135
|
+
longest = Math.max(longest, run.length);
|
|
136
|
+
}
|
|
137
|
+
const fence = "`".repeat(longest + 1);
|
|
138
|
+
return longest === 0 ? `${fence}${text}${fence}` : `${fence} ${text} ${fence}`;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function renderObservation(observation, language) {
|
|
142
|
+
if (observation.state === "idea-selected") {
|
|
143
|
+
const idea = observation.selectedIdea;
|
|
144
|
+
const alias = idea.alias === undefined ? "" : ` (${idea.alias})`;
|
|
145
|
+
return localize(
|
|
146
|
+
language,
|
|
147
|
+
`The project and repository are ready. Selected idea: ${codeSpan(idea.id)}${alias}; state: ${idea.state}.`,
|
|
148
|
+
`项目与 repository 已就绪。当前 idea:${codeSpan(idea.id)}${alias};状态:${idea.state}。`,
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
if (observation.state === "idea-created") {
|
|
152
|
+
return localize(
|
|
153
|
+
language,
|
|
154
|
+
`The local repository was ready before creation. Created idea: ${codeSpan(observation.createdIdea.id)}; path: ${codeSpan(observation.createdIdea.path)}; state: preparing.`,
|
|
155
|
+
`创建前项目与本地 repository 已就绪。新建 idea:${codeSpan(observation.createdIdea.id)};路径:${codeSpan(observation.createdIdea.path)};状态:preparing。`,
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
if (observation.state === "idea-create-failed") {
|
|
159
|
+
return localize(
|
|
160
|
+
language,
|
|
161
|
+
"The repository was ready before creation, but creating the idea failed.",
|
|
162
|
+
"创建前项目与 repository 已就绪,但创建 idea 失败。",
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
const state = {
|
|
166
|
+
"project-setup-required": localize(
|
|
167
|
+
language,
|
|
168
|
+
"The project requires Silvermoon setup.",
|
|
169
|
+
"项目需要完成 Silvermoon 整备。",
|
|
170
|
+
),
|
|
171
|
+
"repository-sync-required": localize(
|
|
172
|
+
language,
|
|
173
|
+
"The project is configured, but the repository requires synchronization.",
|
|
174
|
+
"项目配置已就绪,但 repository 仍需整备或同步。",
|
|
175
|
+
),
|
|
176
|
+
"repository-preparation-required": localize(
|
|
177
|
+
language,
|
|
178
|
+
"The project is ready, but the local repository must be prepared before creating an idea.",
|
|
179
|
+
"项目已就绪,但创建 idea 前仍需整备本地 repository。",
|
|
180
|
+
),
|
|
181
|
+
"navigation-ready": localize(
|
|
182
|
+
language,
|
|
183
|
+
observation.ideas?.activeIdeas.length > 0
|
|
184
|
+
? "The project and repository are ready, and work is available."
|
|
185
|
+
: "The project and repository are ready, with no active idea.",
|
|
186
|
+
observation.ideas?.activeIdeas.length > 0
|
|
187
|
+
? "项目与 repository 已就绪,当前有工作可以推进。"
|
|
188
|
+
: "项目与 repository 已就绪,当前没有 active idea。",
|
|
189
|
+
),
|
|
190
|
+
"idea-not-found": localize(
|
|
191
|
+
language,
|
|
192
|
+
"The project and repository are ready, but the requested idea was not found.",
|
|
193
|
+
"项目与 repository 已就绪,但未找到指定的 idea。",
|
|
194
|
+
),
|
|
195
|
+
}[observation.state];
|
|
196
|
+
const lines = [
|
|
197
|
+
state,
|
|
198
|
+
`- ${localize(language, "root", "根目录")}: ${codeSpan(observation.root)}`,
|
|
199
|
+
];
|
|
200
|
+
if (observation.observedThrough) {
|
|
201
|
+
lines.push(
|
|
202
|
+
`- ${localize(language, "observed through", "观测至")}: ${observation.observedThrough}`,
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
const version = renderVersion(observation.version);
|
|
206
|
+
if (version) {
|
|
207
|
+
lines.push(`- ${localize(language, "version", "版本")}: ${codeSpan(version)}`);
|
|
208
|
+
}
|
|
209
|
+
if (observation.configuration) {
|
|
210
|
+
lines.push(
|
|
211
|
+
`- ${localize(language, "primary", "主仓库")}: ${codeSpan(`${observation.configuration.primaryRepository}#${observation.configuration.primaryBranch}`)}`,
|
|
212
|
+
);
|
|
213
|
+
lines.push(
|
|
214
|
+
`- ${localize(language, "content language", "内容语言")}: ${observation.configuration.preferredLanguage}`,
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
if (observation.ideas) {
|
|
218
|
+
const counts = observation.ideas.counts;
|
|
219
|
+
lines.push(
|
|
220
|
+
`- ${localize(language, "ideas", "ideas")}: preparing=${counts.preparing}, implementing=${counts.implementing}, `
|
|
221
|
+
+ `deploying=${counts.deploying}, completed=${counts.completed}, abandoned=${counts.abandoned}`,
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
const sections = [`${lines[0]}\n\n${lines.slice(1).join("\n")}`];
|
|
225
|
+
const candidates = observation.state === "idea-not-found"
|
|
226
|
+
? observation.candidates
|
|
227
|
+
: observation.ideas?.activeIdeas;
|
|
228
|
+
if (candidates?.length > 0) {
|
|
229
|
+
const active = candidates.map((idea) => {
|
|
230
|
+
const alias = idea.alias === undefined ? "" : ` (${idea.alias})`;
|
|
231
|
+
return `- ${codeSpan(idea.id)}${alias} ${idea.state}`;
|
|
232
|
+
});
|
|
233
|
+
sections.push(`### ${localize(language, "Ideas you can continue", "可继续推进的想法")}\n\n${active.join("\n")}`);
|
|
234
|
+
}
|
|
235
|
+
if (observation.problems.length > 0) {
|
|
236
|
+
const problems = observation.problems.map((problem) =>
|
|
237
|
+
`- [${problem.type}] ${problem.summary}`
|
|
238
|
+
);
|
|
239
|
+
sections.push(`### ${localize(language, "Issues to address", "需要处理的问题")}\n\n${problems.join("\n")}`);
|
|
240
|
+
}
|
|
241
|
+
return sections.join("\n\n");
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function renderOutcomes(outcomes, language) {
|
|
245
|
+
return outcomes
|
|
246
|
+
.map((item) => {
|
|
247
|
+
const status = item.status === "success"
|
|
248
|
+
? localize(language, "success", "成功")
|
|
249
|
+
: localize(language, "failure", "失败");
|
|
250
|
+
return `- ${status} [${item.type}]: ${item.summary}`;
|
|
251
|
+
})
|
|
252
|
+
.join("\n");
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
export function renderDialogue(report) {
|
|
256
|
+
const contentLanguage =
|
|
257
|
+
report.observation.configuration?.preferredLanguage
|
|
258
|
+
?? DEFAULT_LANGUAGE;
|
|
259
|
+
const language = report.observation.outputLanguage
|
|
260
|
+
?? resolveOutputLanguage({
|
|
261
|
+
content: contentLanguage,
|
|
262
|
+
override: report.intention.command === "whats-next"
|
|
263
|
+
? report.intention.args.language ?? undefined
|
|
264
|
+
: undefined,
|
|
265
|
+
}).tag;
|
|
266
|
+
const visibleOutcomes = report.outcomes.filter((item) =>
|
|
267
|
+
!(item.type === "fetch-primary"
|
|
268
|
+
&& item.status === "success"
|
|
269
|
+
&& ["navigation-ready", "idea-selected", "idea-not-found", "idea-created", "idea-create-failed"]
|
|
270
|
+
.includes(report.observation.state))
|
|
271
|
+
);
|
|
272
|
+
const sections = [
|
|
273
|
+
[
|
|
274
|
+
localize(language, "Current instruction", "本次指示"),
|
|
275
|
+
renderIntention(report.intention, language, contentLanguage),
|
|
276
|
+
],
|
|
277
|
+
[
|
|
278
|
+
localize(language, "Project status", "项目现状"),
|
|
279
|
+
renderObservation(report.observation, language),
|
|
280
|
+
],
|
|
281
|
+
...(visibleOutcomes.length > 0
|
|
282
|
+
? [[
|
|
283
|
+
localize(language, "Actions and results", "本次操作及结果"),
|
|
284
|
+
renderOutcomes(visibleOutcomes, language),
|
|
285
|
+
]]
|
|
286
|
+
: []),
|
|
287
|
+
[
|
|
288
|
+
localize(language, "Suggested next steps", "下一步建议"),
|
|
289
|
+
report.instructions,
|
|
290
|
+
],
|
|
291
|
+
];
|
|
292
|
+
return sections.map(([heading, body]) => `## ${heading}\n\n${body}`).join("\n\n");
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
export function renderCheck(report) {
|
|
296
|
+
const language = report.observation.outputLanguage
|
|
297
|
+
?? resolveOutputLanguage({
|
|
298
|
+
content: report.observation.configuration?.preferredLanguage,
|
|
299
|
+
override: report.intention.args.language ?? undefined,
|
|
300
|
+
}).tag;
|
|
301
|
+
const { target } = report.intention.args;
|
|
302
|
+
const version = renderVersion(report.observation.version);
|
|
303
|
+
const label = target.type === "commit"
|
|
304
|
+
? `${target.type} ${target.revision}`
|
|
305
|
+
: target.type;
|
|
306
|
+
const valid = report.observation.state === "project-ready";
|
|
307
|
+
const lines = [
|
|
308
|
+
`## ${localize(language, "Check", "检查")}`,
|
|
309
|
+
"",
|
|
310
|
+
`- ${localize(language, "Target", "目标")}: ${codeSpan(label)}`,
|
|
311
|
+
];
|
|
312
|
+
if (version) lines.push(`- ${localize(language, "Version", "版本")}: ${codeSpan(version)}`);
|
|
313
|
+
lines.push(
|
|
314
|
+
`- ${localize(language, "Result", "结果")}: ${valid
|
|
315
|
+
? localize(language, "valid", "通过")
|
|
316
|
+
: localize(language, "invalid or unavailable", "未通过或无法验证")}`,
|
|
317
|
+
);
|
|
318
|
+
if (report.observation.problems.length > 0) {
|
|
319
|
+
lines.push("", `### ${localize(language, "Issues to address", "需要处理的问题")}`, "");
|
|
320
|
+
for (const problem of report.observation.problems) {
|
|
321
|
+
lines.push(`- [${problem.type}] ${problem.summary}`);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
return lines.join("\n");
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
export { ACTIVE_STATES, IDEA_STATES };
|
package/src/git.js
CHANGED
|
@@ -4,15 +4,33 @@ import { mkdtempSync, rmSync } from "node:fs";
|
|
|
4
4
|
import { tmpdir } from "node:os";
|
|
5
5
|
import { join } from "node:path";
|
|
6
6
|
|
|
7
|
+
import { traceAsync, traceSync } from "./trace.js";
|
|
8
|
+
|
|
7
9
|
let commandObserver = null;
|
|
8
10
|
|
|
9
|
-
export function runGit(root, args, { env } = {}) {
|
|
11
|
+
export function runGit(root, args, { env, input } = {}) {
|
|
10
12
|
if (commandObserver) commandObserver([...args]);
|
|
11
|
-
const result =
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
13
|
+
const result = traceSync(
|
|
14
|
+
"git.command",
|
|
15
|
+
{
|
|
16
|
+
argumentCount: args.length - 1,
|
|
17
|
+
network: args[0] === "fetch" || args[0] === "ls-remote",
|
|
18
|
+
subcommand: args[0] ?? null,
|
|
19
|
+
},
|
|
20
|
+
() => spawnSync("git", ["-C", root, ...args], {
|
|
21
|
+
encoding: "utf8",
|
|
22
|
+
env: { ...process.env, GIT_TERMINAL_PROMPT: "0", ...env },
|
|
23
|
+
input,
|
|
24
|
+
windowsHide: true,
|
|
25
|
+
}),
|
|
26
|
+
(completed) => ({
|
|
27
|
+
attributes: {
|
|
28
|
+
exitCode: completed.status,
|
|
29
|
+
signal: completed.signal,
|
|
30
|
+
},
|
|
31
|
+
status: completed.status === 0 ? "ok" : "error",
|
|
32
|
+
}),
|
|
33
|
+
);
|
|
16
34
|
return {
|
|
17
35
|
error: result.error ?? null,
|
|
18
36
|
ok: result.status === 0,
|
|
@@ -59,45 +77,31 @@ function requireGit(root, args, label, options) {
|
|
|
59
77
|
return result.stdout;
|
|
60
78
|
}
|
|
61
79
|
|
|
62
|
-
export function
|
|
63
|
-
|
|
80
|
+
export function inspectTreePaths(root, tree, paths) {
|
|
81
|
+
if (paths.length === 0) return new Map();
|
|
82
|
+
const input = `${paths.map((path) => `${tree}:${path}`).join("\n")}\n`;
|
|
83
|
+
const output = requireGit(
|
|
64
84
|
root,
|
|
65
|
-
["
|
|
66
|
-
"Cannot
|
|
85
|
+
["cat-file", "--batch-check=%(objectname) %(objecttype)"],
|
|
86
|
+
"Cannot inspect snapshot world trees",
|
|
87
|
+
{ input },
|
|
67
88
|
);
|
|
68
|
-
|
|
69
|
-
if (
|
|
70
|
-
|
|
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 },
|
|
89
|
+
const lines = output.split(/\r?\n/);
|
|
90
|
+
if (lines.length !== paths.length) {
|
|
91
|
+
throw new Error(
|
|
92
|
+
`Cannot inspect snapshot world trees: expected ${paths.length} result(s), received ${lines.length}`,
|
|
86
93
|
);
|
|
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
94
|
}
|
|
95
|
+
return new Map(paths.map((path, index) => {
|
|
96
|
+
const [object, type, ...unexpected] = lines[index].split(" ");
|
|
97
|
+
if (!object || !type || unexpected.length > 0) {
|
|
98
|
+
throw new Error(`Cannot parse snapshot world tree result for ${path}`);
|
|
99
|
+
}
|
|
100
|
+
return [path, { object: type === "missing" ? null : object, type }];
|
|
101
|
+
}));
|
|
99
102
|
}
|
|
100
|
-
|
|
103
|
+
|
|
104
|
+
export function worktreeSnapshot(root, { paths } = {}) {
|
|
101
105
|
const directory = mkdtempSync(join(tmpdir(), "silvermoon-index-"));
|
|
102
106
|
const env = { ...process.env, GIT_INDEX_FILE: join(directory, "index") };
|
|
103
107
|
try {
|
|
@@ -105,7 +109,9 @@ export function worktreeSnapshot(root) {
|
|
|
105
109
|
if (!populated.ok) {
|
|
106
110
|
requireGit(root, ["read-tree", "--empty"], "Cannot initialize snapshot index", { env });
|
|
107
111
|
}
|
|
108
|
-
|
|
112
|
+
const add = ["add", "--all"];
|
|
113
|
+
if (paths !== undefined) add.push("--", ...paths);
|
|
114
|
+
requireGit(root, add, "Cannot snapshot worktree", { env });
|
|
109
115
|
return {
|
|
110
116
|
tree: requireGit(root, ["write-tree"], "Cannot write worktree snapshot", { env }),
|
|
111
117
|
};
|
|
@@ -114,10 +120,6 @@ export function worktreeSnapshot(root) {
|
|
|
114
120
|
}
|
|
115
121
|
}
|
|
116
122
|
|
|
117
|
-
function paths(stdout) {
|
|
118
|
-
return stdout.split(/\r?\n/).filter(Boolean);
|
|
119
|
-
}
|
|
120
|
-
|
|
121
123
|
const CHANGE_KINDS = {
|
|
122
124
|
A: "added",
|
|
123
125
|
C: "copied",
|
|
@@ -195,6 +197,87 @@ export function parseWorktreeChanges(source) {
|
|
|
195
197
|
return result;
|
|
196
198
|
}
|
|
197
199
|
|
|
200
|
+
export function inspectWorktreeChanges(root) {
|
|
201
|
+
const source = requireGit(
|
|
202
|
+
root,
|
|
203
|
+
["status", "--porcelain=v2", "--untracked-files=all", "-z"],
|
|
204
|
+
"Cannot inspect worktree changes",
|
|
205
|
+
);
|
|
206
|
+
return parseWorktreeChanges(`${source}\0`);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export function resolveHead(root) {
|
|
210
|
+
const result = runGit(root, ["rev-parse", "--verify", "HEAD^{commit}"]);
|
|
211
|
+
return result.ok ? result.stdout : null;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export function inspectCurrentBranch(root) {
|
|
215
|
+
const branch = runGit(root, ["symbolic-ref", "--quiet", "--short", "HEAD"]);
|
|
216
|
+
if (!branch.ok) {
|
|
217
|
+
return { branch: null, remote: null, repository: null, upstreamBranch: null };
|
|
218
|
+
}
|
|
219
|
+
const remote = runGit(root, ["config", "--get", `branch.${branch.stdout}.remote`]);
|
|
220
|
+
const merge = runGit(root, ["config", "--get", `branch.${branch.stdout}.merge`]);
|
|
221
|
+
if (!remote.ok || !merge.ok || !merge.stdout.startsWith("refs/heads/")) {
|
|
222
|
+
return {
|
|
223
|
+
branch: branch.stdout,
|
|
224
|
+
remote: remote.ok ? remote.stdout : null,
|
|
225
|
+
repository: null,
|
|
226
|
+
upstreamBranch: merge.ok && merge.stdout.startsWith("refs/heads/")
|
|
227
|
+
? merge.stdout.slice("refs/heads/".length)
|
|
228
|
+
: null,
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
const repository = remote.stdout === "."
|
|
232
|
+
? null
|
|
233
|
+
: runGit(root, ["config", "--get", `remote.${remote.stdout}.url`]);
|
|
234
|
+
return {
|
|
235
|
+
branch: branch.stdout,
|
|
236
|
+
remote: remote.stdout,
|
|
237
|
+
repository: repository?.ok ? repository.stdout : null,
|
|
238
|
+
upstreamBranch: merge.stdout.slice("refs/heads/".length),
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
export function compareCommits(root, local, remote) {
|
|
243
|
+
if (local === remote) return "aligned";
|
|
244
|
+
const localAncestor = runGit(
|
|
245
|
+
root,
|
|
246
|
+
["merge-base", "--is-ancestor", local, remote],
|
|
247
|
+
);
|
|
248
|
+
if (localAncestor.ok) {
|
|
249
|
+
return "behind";
|
|
250
|
+
}
|
|
251
|
+
if (localAncestor.status !== 1) {
|
|
252
|
+
throw new Error(
|
|
253
|
+
`Cannot compare local and primary commits: `
|
|
254
|
+
+ `${sanitizeGitMessage(localAncestor.stderr || localAncestor.error?.message)}`,
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
const remoteAncestor = runGit(
|
|
258
|
+
root,
|
|
259
|
+
["merge-base", "--is-ancestor", remote, local],
|
|
260
|
+
);
|
|
261
|
+
if (remoteAncestor.ok) {
|
|
262
|
+
return "ahead";
|
|
263
|
+
}
|
|
264
|
+
if (remoteAncestor.status !== 1) {
|
|
265
|
+
throw new Error(
|
|
266
|
+
`Cannot compare local and primary commits: `
|
|
267
|
+
+ `${sanitizeGitMessage(remoteAncestor.stderr || remoteAncestor.error?.message)}`,
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
const shallow = runGit(root, ["rev-parse", "--is-shallow-repository"]);
|
|
271
|
+
if (!shallow.ok) {
|
|
272
|
+
throw new Error(
|
|
273
|
+
`Cannot determine repository history depth: `
|
|
274
|
+
+ `${sanitizeGitMessage(shallow.stderr || shallow.error?.message)}`,
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
if (shallow.stdout === "true") return "unknown";
|
|
278
|
+
return "diverged";
|
|
279
|
+
}
|
|
280
|
+
|
|
198
281
|
export function resolveCommit(root, revision) {
|
|
199
282
|
return requireGit(
|
|
200
283
|
root,
|
|
@@ -203,51 +286,41 @@ export function resolveCommit(root, revision) {
|
|
|
203
286
|
);
|
|
204
287
|
}
|
|
205
288
|
|
|
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
289
|
export function indexSnapshot(root) {
|
|
219
290
|
return {
|
|
220
|
-
paths: paths(requireGit(root, ["diff", "--cached", "--name-only", "--"], "Cannot inspect staged changes")),
|
|
221
291
|
tree: requireGit(root, ["write-tree"], "Cannot snapshot the index"),
|
|
222
292
|
};
|
|
223
293
|
}
|
|
224
294
|
|
|
225
295
|
export function fetchRepositoryBranch(root, repository, branch) {
|
|
226
296
|
const remoteRef = `refs/heads/${branch}`;
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
],
|
|
246
|
-
`Cannot fetch ${branch}`,
|
|
297
|
+
const fetched = requireGit(
|
|
298
|
+
root,
|
|
299
|
+
[
|
|
300
|
+
"fetch",
|
|
301
|
+
"--porcelain",
|
|
302
|
+
"--no-tags",
|
|
303
|
+
repository,
|
|
304
|
+
remoteRef,
|
|
305
|
+
],
|
|
306
|
+
`Cannot fetch ${branch}`,
|
|
307
|
+
);
|
|
308
|
+
const updates = fetched
|
|
309
|
+
.split(/\r?\n/)
|
|
310
|
+
.map((line) => line.trim().split(/\s+/))
|
|
311
|
+
.filter((fields) =>
|
|
312
|
+
fields.length === 4
|
|
313
|
+
&& fields[3] === "FETCH_HEAD"
|
|
314
|
+
&& /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/.test(fields[2])
|
|
247
315
|
);
|
|
248
|
-
|
|
316
|
+
if (updates.length !== 1) {
|
|
317
|
+
throw new Error(`Cannot determine fetched commit for branch ${branch}`);
|
|
249
318
|
}
|
|
250
|
-
|
|
319
|
+
const primary = updates[0][2];
|
|
320
|
+
if (!runGit(root, ["cat-file", "-e", `${primary}^{commit}`]).ok) {
|
|
321
|
+
throw new Error(`Fetched branch ${branch} does not resolve to a commit`);
|
|
322
|
+
}
|
|
323
|
+
return primary;
|
|
251
324
|
}
|
|
252
325
|
|
|
253
326
|
export function fetchPrimary(root, config) {
|
|
@@ -264,21 +337,23 @@ export async function withTemporaryWorktree(root, commit, callback) {
|
|
|
264
337
|
}
|
|
265
338
|
|
|
266
339
|
export async function withTemporaryTree(root, tree, callback) {
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
340
|
+
return traceAsync("snapshot.materialize", {}, async () => {
|
|
341
|
+
const temporaryRoot = await mkdtemp(join(tmpdir(), "silvermoon-tree-"));
|
|
342
|
+
const directory = join(temporaryRoot, "snapshot");
|
|
343
|
+
const env = { ...process.env, GIT_INDEX_FILE: join(temporaryRoot, "index") };
|
|
344
|
+
try {
|
|
345
|
+
await mkdir(directory);
|
|
346
|
+
requireGit(root, ["read-tree", tree], "Cannot populate isolated index", { env });
|
|
347
|
+
const prefix = `${directory.replaceAll("\\", "/")}/`;
|
|
348
|
+
requireGit(
|
|
349
|
+
root,
|
|
350
|
+
["checkout-index", "--all", "--force", `--prefix=${prefix}`],
|
|
351
|
+
"Cannot populate isolated tree snapshot",
|
|
352
|
+
{ env },
|
|
353
|
+
);
|
|
354
|
+
return await callback(directory, tree);
|
|
355
|
+
} finally {
|
|
356
|
+
await rm(temporaryRoot, { recursive: true, force: true });
|
|
357
|
+
}
|
|
358
|
+
});
|
|
284
359
|
}
|