silvermoon 0.2.2 → 0.3.0-rc.2
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/LICENSE +21 -0
- package/README.md +115 -121
- package/README.zh-CN.md +96 -95
- package/bin/silvermoon.js +7 -2
- package/docs/getting-started.md +21 -9
- package/docs/maintaining.md +83 -6
- package/docs/npm-package-releases.md +33 -4
- package/docs/operations.md +11 -7
- package/docs/reference.md +131 -32
- package/docs/repository-tasks.md +43 -15
- package/package.json +46 -8
- package/skills/silvermoon/SKILL.md +224 -123
- package/src/adoption.js +30 -16
- package/src/cli.js +148 -39
- package/src/config.js +76 -16
- package/src/create-idea.js +22 -10
- package/src/git-snapshot.js +212 -0
- package/src/git.js +209 -18
- package/src/guidance.js +65 -23
- package/src/idea-layout.js +59 -23
- package/src/idea-metadata.js +48 -0
- package/src/idea-templates.js +133 -0
- package/src/index.js +64 -53
- package/src/language.js +4 -0
- package/src/list-ideas.js +118 -55
- package/src/observation.js +9 -1
- package/src/repository.js +2 -8
- package/src/response.js +93 -95
- package/src/subprocess.js +27 -0
- package/src/trace.js +37 -2
- package/src/tui-table.js +71 -0
- package/src/tui.js +205 -0
- package/src/whatsnext.js +71 -18
- package/assets/silvermoon-avatar.svg +0 -395
- package/assets/silvermoon.svg +0 -176
- package/docs/assets/silvermoon-avatar.svg +0 -395
package/src/list-ideas.js
CHANGED
|
@@ -1,71 +1,129 @@
|
|
|
1
|
-
import { readFile } from "node:fs/promises";
|
|
2
|
-
import { resolve } from "node:path";
|
|
3
|
-
|
|
4
1
|
import { createCommandRun } from "./domain.js";
|
|
5
2
|
import {
|
|
6
|
-
extractIdeaTitle,
|
|
7
3
|
ideaInventoryItem,
|
|
8
4
|
normalizeIdeaQuery,
|
|
9
5
|
queryIdeaInventory,
|
|
10
6
|
} from "./idea-query.js";
|
|
11
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
metadataFailureObservation,
|
|
9
|
+
readIdeaInventoryItem,
|
|
10
|
+
} from "./idea-metadata.js";
|
|
11
|
+
import { canonicalizeOutputLanguage } from "./language.js";
|
|
12
12
|
import { observeSnapshot } from "./observation.js";
|
|
13
13
|
import { traceAsync } from "./trace.js";
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
try {
|
|
18
|
-
const source = await readFile(
|
|
19
|
-
resolve(root, ...documentPath.split("/")),
|
|
20
|
-
"utf8",
|
|
21
|
-
);
|
|
22
|
-
return ideaInventoryItem(idea, extractIdeaTitle(source));
|
|
23
|
-
} catch (caught) {
|
|
24
|
-
throw new Error(
|
|
25
|
-
`Cannot read idea metadata for ${idea.id} at ${documentPath}: ${caught.message}`,
|
|
26
|
-
{ cause: caught },
|
|
27
|
-
);
|
|
28
|
-
}
|
|
15
|
+
function withoutTitleQuery(query, { limit = query.limit } = {}) {
|
|
16
|
+
return { ...query, limit, query: null };
|
|
29
17
|
}
|
|
30
18
|
|
|
31
|
-
function
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
19
|
+
function matchesIdentity(item, needle) {
|
|
20
|
+
return [item.id, item.alias]
|
|
21
|
+
.filter((value) => value !== undefined)
|
|
22
|
+
.some((value) => value.toLowerCase().includes(needle));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function queryObservedInventory({
|
|
26
|
+
ideas,
|
|
27
|
+
metadataReader,
|
|
28
|
+
normalizedQuery,
|
|
29
|
+
root,
|
|
30
|
+
}) {
|
|
31
|
+
const ideasById = new Map(ideas.map((idea) => [idea.id, idea]));
|
|
32
|
+
const baseItems = ideas.map((idea) => ideaInventoryItem(idea));
|
|
33
|
+
const candidates = queryIdeaInventory(
|
|
34
|
+
baseItems,
|
|
35
|
+
withoutTitleQuery(normalizedQuery, { limit: null }),
|
|
36
|
+
).ideas;
|
|
37
|
+
|
|
38
|
+
return traceAsync(
|
|
39
|
+
"ideas.inventory-metadata",
|
|
40
|
+
{
|
|
41
|
+
candidateCount: candidates.length,
|
|
42
|
+
ideaCount: ideas.length,
|
|
47
43
|
},
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
)
|
|
44
|
+
async () => {
|
|
45
|
+
const loadedItems = new Map();
|
|
46
|
+
let queryCandidateCount = 0;
|
|
47
|
+
let titleReadCount = 0;
|
|
48
|
+
|
|
49
|
+
const readItems = async (items) => {
|
|
50
|
+
const unread = items.filter(({ id }) => !loadedItems.has(id));
|
|
51
|
+
titleReadCount += unread.length;
|
|
52
|
+
const loaded = await Promise.all(unread.map(({ id }) => {
|
|
53
|
+
const idea = ideasById.get(id);
|
|
54
|
+
if (idea === undefined) {
|
|
55
|
+
throw new Error(`Missing observed idea metadata for ${id}`);
|
|
56
|
+
}
|
|
57
|
+
return metadataReader(root, idea);
|
|
58
|
+
}));
|
|
59
|
+
for (const item of loaded) loadedItems.set(item.id, item);
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
let matchingItems = candidates;
|
|
63
|
+
if (normalizedQuery.query !== null) {
|
|
64
|
+
const needle = normalizedQuery.query.toLowerCase();
|
|
65
|
+
const directMatchIds = new Set(
|
|
66
|
+
candidates
|
|
67
|
+
.filter((item) => matchesIdentity(item, needle))
|
|
68
|
+
.map(({ id }) => id),
|
|
69
|
+
);
|
|
70
|
+
const titleCandidates = candidates.filter(
|
|
71
|
+
({ id }) => !directMatchIds.has(id),
|
|
72
|
+
);
|
|
73
|
+
queryCandidateCount = titleCandidates.length;
|
|
74
|
+
await readItems(titleCandidates);
|
|
75
|
+
matchingItems = candidates
|
|
76
|
+
.filter(({ id }) =>
|
|
77
|
+
directMatchIds.has(id)
|
|
78
|
+
|| loadedItems.get(id)?.title?.toLowerCase().includes(needle)
|
|
79
|
+
)
|
|
80
|
+
.map((item) => loadedItems.get(item.id) ?? item);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const selected = queryIdeaInventory(
|
|
84
|
+
matchingItems,
|
|
85
|
+
withoutTitleQuery(normalizedQuery),
|
|
86
|
+
);
|
|
87
|
+
await readItems(selected.ideas);
|
|
88
|
+
const inventory = {
|
|
89
|
+
summary: selected.summary,
|
|
90
|
+
ideas: selected.ideas.map(({ id }) => {
|
|
91
|
+
const item = loadedItems.get(id);
|
|
92
|
+
if (item === undefined) {
|
|
93
|
+
throw new Error(`Missing selected idea metadata for ${id}`);
|
|
94
|
+
}
|
|
95
|
+
return item;
|
|
96
|
+
}),
|
|
97
|
+
};
|
|
98
|
+
return { inventory, queryCandidateCount, titleReadCount };
|
|
54
99
|
},
|
|
55
|
-
|
|
100
|
+
({ inventory, queryCandidateCount, titleReadCount }) => ({
|
|
101
|
+
attributes: {
|
|
102
|
+
matchedCount: inventory.summary.matched,
|
|
103
|
+
queryCandidateCount,
|
|
104
|
+
returnedCount: inventory.summary.returned,
|
|
105
|
+
titleReadCount,
|
|
106
|
+
},
|
|
107
|
+
}),
|
|
108
|
+
);
|
|
56
109
|
}
|
|
57
110
|
|
|
58
111
|
export async function listIdeas({
|
|
59
112
|
all,
|
|
60
113
|
createdBefore,
|
|
61
114
|
createdSince,
|
|
115
|
+
language,
|
|
62
116
|
limit,
|
|
117
|
+
metadataReader = readIdeaInventoryItem,
|
|
63
118
|
query,
|
|
64
119
|
root = process.cwd(),
|
|
65
120
|
sort,
|
|
66
121
|
states,
|
|
67
122
|
userHome,
|
|
68
123
|
} = {}) {
|
|
124
|
+
const canonicalLanguage = language === undefined
|
|
125
|
+
? undefined
|
|
126
|
+
: canonicalizeOutputLanguage(language);
|
|
69
127
|
const normalizedQuery = normalizeIdeaQuery({
|
|
70
128
|
all,
|
|
71
129
|
createdBefore,
|
|
@@ -77,11 +135,15 @@ export async function listIdeas({
|
|
|
77
135
|
});
|
|
78
136
|
const intention = {
|
|
79
137
|
command: "list-ideas",
|
|
80
|
-
args:
|
|
138
|
+
args: {
|
|
139
|
+
...normalizedQuery,
|
|
140
|
+
language: canonicalLanguage ?? null,
|
|
141
|
+
},
|
|
81
142
|
};
|
|
82
143
|
const runtime = createCommandRun(intention);
|
|
83
144
|
const observed = await observeSnapshot({
|
|
84
145
|
root,
|
|
146
|
+
outputLanguage: canonicalLanguage,
|
|
85
147
|
userHome,
|
|
86
148
|
version: { type: "worktree" },
|
|
87
149
|
});
|
|
@@ -93,19 +155,20 @@ export async function listIdeas({
|
|
|
93
155
|
);
|
|
94
156
|
}
|
|
95
157
|
|
|
96
|
-
let
|
|
158
|
+
let queried;
|
|
97
159
|
try {
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
),
|
|
105
|
-
),
|
|
106
|
-
);
|
|
160
|
+
queried = await queryObservedInventory({
|
|
161
|
+
ideas: observed.layout.ideas,
|
|
162
|
+
metadataReader,
|
|
163
|
+
normalizedQuery,
|
|
164
|
+
root: observed.observation.root,
|
|
165
|
+
});
|
|
107
166
|
} catch (caught) {
|
|
108
|
-
const failure = metadataFailureObservation(
|
|
167
|
+
const failure = metadataFailureObservation(
|
|
168
|
+
observed,
|
|
169
|
+
caught,
|
|
170
|
+
"silvermoon list-ideas",
|
|
171
|
+
);
|
|
109
172
|
return runtime.complete(
|
|
110
173
|
failure.observation,
|
|
111
174
|
failure.responseContext,
|
|
@@ -113,7 +176,7 @@ export async function listIdeas({
|
|
|
113
176
|
);
|
|
114
177
|
}
|
|
115
178
|
|
|
116
|
-
const inventory =
|
|
179
|
+
const { inventory } = queried;
|
|
117
180
|
const {
|
|
118
181
|
configuration,
|
|
119
182
|
outputLanguage,
|
package/src/observation.js
CHANGED
|
@@ -132,6 +132,7 @@ export function repositoryProblemObservation(observation, problems) {
|
|
|
132
132
|
async function observeSnapshotInternal({
|
|
133
133
|
allowMissingIdeas = false,
|
|
134
134
|
contentRoot,
|
|
135
|
+
filesystem,
|
|
135
136
|
gitRoot,
|
|
136
137
|
ideaLanguage,
|
|
137
138
|
outputLanguage: requestedOutputLanguage,
|
|
@@ -147,7 +148,12 @@ async function observeSnapshotInternal({
|
|
|
147
148
|
const adoption = await traceAsync(
|
|
148
149
|
"adoption.inspect",
|
|
149
150
|
{},
|
|
150
|
-
() => inspectAdoption({
|
|
151
|
+
() => inspectAdoption({
|
|
152
|
+
contentRoot,
|
|
153
|
+
filesystem,
|
|
154
|
+
repositoryRoot: gitRoot,
|
|
155
|
+
root,
|
|
156
|
+
}),
|
|
151
157
|
);
|
|
152
158
|
const user = await traceAsync(
|
|
153
159
|
"user-config.load",
|
|
@@ -233,6 +239,7 @@ async function observeSnapshotInternal({
|
|
|
233
239
|
{},
|
|
234
240
|
() => inspectIdeaLayout({
|
|
235
241
|
config: adoption.config,
|
|
242
|
+
filesystem,
|
|
236
243
|
gitRoot: gitRoot ?? adoption.root,
|
|
237
244
|
root: contentRoot ?? adoption.root,
|
|
238
245
|
snapshotTree,
|
|
@@ -257,6 +264,7 @@ async function observeSnapshotInternal({
|
|
|
257
264
|
|
|
258
265
|
const guidanceDiagnostics = projectOnly
|
|
259
266
|
? (await inspectAllGuidance({
|
|
267
|
+
filesystem,
|
|
260
268
|
gitRoot: gitRoot ?? adoption.root,
|
|
261
269
|
snapshotTree,
|
|
262
270
|
})).diagnostics
|
package/src/repository.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
import { spawnSync } from "node:child_process";
|
|
3
2
|
|
|
4
3
|
const CONTROL_CHARACTER = /[\u0000-\u001f\u007f]/;
|
|
5
4
|
const ENCODED_SEPARATOR = /%(?:2f|5c)/i;
|
|
@@ -58,6 +57,7 @@ export function validBranchName(value) {
|
|
|
58
57
|
typeof value !== "string" ||
|
|
59
58
|
value.length === 0 ||
|
|
60
59
|
value === "@" ||
|
|
60
|
+
value === "HEAD" ||
|
|
61
61
|
value.startsWith("-") ||
|
|
62
62
|
value.startsWith("refs/") ||
|
|
63
63
|
value.startsWith("remotes/") ||
|
|
@@ -74,13 +74,7 @@ export function validBranchName(value) {
|
|
|
74
74
|
const structurallyValid = value
|
|
75
75
|
.split("/")
|
|
76
76
|
.every((component) => component && !component.startsWith(".") && !component.endsWith(".lock"));
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
const checked = spawnSync("git", ["check-ref-format", "--branch", value], {
|
|
80
|
-
encoding: "utf8",
|
|
81
|
-
windowsHide: true,
|
|
82
|
-
});
|
|
83
|
-
return checked.status === 0;
|
|
77
|
+
return structurallyValid;
|
|
84
78
|
}
|
|
85
79
|
|
|
86
80
|
export function effectiveSourceRepository(config, record) {
|
package/src/response.js
CHANGED
|
@@ -163,13 +163,13 @@ function dialogueResponse(intention, internalObservation, language) {
|
|
|
163
163
|
summary: choices.length === 0
|
|
164
164
|
? localize(
|
|
165
165
|
language,
|
|
166
|
-
"No active ideas are available.",
|
|
167
|
-
"
|
|
166
|
+
"Current state: navigation-ready. No active ideas are available.",
|
|
167
|
+
"当前状态:navigation-ready。当前没有 active idea。",
|
|
168
168
|
)
|
|
169
169
|
: localize(
|
|
170
170
|
language,
|
|
171
|
-
|
|
172
|
-
|
|
171
|
+
`Current state: navigation-ready. ${choices.length} active idea(s) are available.`,
|
|
172
|
+
`当前状态:navigation-ready。当前有 ${choices.length} 个 active idea。`,
|
|
173
173
|
),
|
|
174
174
|
choices,
|
|
175
175
|
nextSteps,
|
|
@@ -304,34 +304,85 @@ function markdownTableCell(value) {
|
|
|
304
304
|
.replaceAll(/\r?\n/g, " ");
|
|
305
305
|
}
|
|
306
306
|
|
|
307
|
-
function
|
|
307
|
+
function renderMarkdownTable(headers, rows) {
|
|
308
|
+
return [
|
|
309
|
+
`| ${headers.join(" | ")} |`,
|
|
310
|
+
`| ${headers.map(() => "---").join(" | ")} |`,
|
|
311
|
+
...rows.map((row) =>
|
|
312
|
+
`| ${row.map(markdownTableCell).join(" | ")} |`
|
|
313
|
+
),
|
|
314
|
+
];
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function formatDate(timestamp, language, now) {
|
|
318
|
+
const date = new Date(timestamp);
|
|
319
|
+
if (!Number.isFinite(date.getTime()) || !Number.isFinite(now.getTime())) {
|
|
320
|
+
throw new TypeError("Cannot render an invalid timestamp");
|
|
321
|
+
}
|
|
322
|
+
const duration = now.getTime() - date.getTime();
|
|
323
|
+
const elapsed = Math.abs(duration);
|
|
324
|
+
const minute = 60_000;
|
|
325
|
+
const hour = 60 * minute;
|
|
326
|
+
const day = 24 * hour;
|
|
327
|
+
if (elapsed > 7 * day) {
|
|
328
|
+
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}-${String(date.getDate()).padStart(2, "0")}`;
|
|
329
|
+
}
|
|
330
|
+
if (elapsed < minute) {
|
|
331
|
+
return duration < 0
|
|
332
|
+
? localize(language, "soon", "马上")
|
|
333
|
+
: localize(language, "just now", "刚刚");
|
|
334
|
+
}
|
|
335
|
+
const [amount, unit, chineseUnit] = elapsed < hour
|
|
336
|
+
? [Math.floor(elapsed / minute), "m", "分钟"]
|
|
337
|
+
: elapsed < day
|
|
338
|
+
? [Math.floor(elapsed / hour), "h", "小时"]
|
|
339
|
+
: [Math.floor(elapsed / day), "d", "天"];
|
|
340
|
+
return duration < 0
|
|
341
|
+
? localize(language, `in ${amount}${unit}`, `${amount}${chineseUnit}后`)
|
|
342
|
+
: localize(language, `${amount}${unit} ago`, `${amount}${chineseUnit}前`);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function renderIdeaTable(ideas, language, now) {
|
|
308
346
|
const headers = [
|
|
309
|
-
|
|
310
|
-
localize(language, "Alias", "Alias"),
|
|
347
|
+
"Alias / ID",
|
|
311
348
|
localize(language, "State", "状态"),
|
|
312
349
|
localize(language, "Created", "创建时间"),
|
|
313
350
|
localize(language, "Title", "标题"),
|
|
314
351
|
];
|
|
352
|
+
return renderMarkdownTable(
|
|
353
|
+
headers,
|
|
354
|
+
ideas.map((idea) => [
|
|
355
|
+
idea.alias ?? idea.id,
|
|
356
|
+
idea.state,
|
|
357
|
+
formatDate(idea.createdAt, language, now),
|
|
358
|
+
idea.title,
|
|
359
|
+
]),
|
|
360
|
+
);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function renderIdeaList(response, language, now) {
|
|
315
364
|
const counts = Object.entries(response.inventory.counts)
|
|
365
|
+
.filter(([, count]) => count > 0)
|
|
316
366
|
.map(([state, count]) => `${state}=${count}`)
|
|
317
|
-
.join(", ");
|
|
318
|
-
const lines = [
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
367
|
+
.join(localize(language, ", ", ","));
|
|
368
|
+
const lines = [counts.length === 0
|
|
369
|
+
? response.summary
|
|
370
|
+
: localize(
|
|
371
|
+
language,
|
|
372
|
+
`${response.summary} Counts: ${counts}.`,
|
|
373
|
+
`${response.summary}计数:${counts}。`,
|
|
374
|
+
)];
|
|
375
|
+
if (response.query?.createdSince) {
|
|
376
|
+
lines.push(
|
|
377
|
+
`- ${localize(language, "Created since", "创建时间下界")}: ${formatDate(response.query.createdSince, language, now)}`,
|
|
378
|
+
);
|
|
379
|
+
}
|
|
380
|
+
if (response.query?.createdBefore) {
|
|
325
381
|
lines.push(
|
|
326
|
-
|
|
327
|
-
idea.id,
|
|
328
|
-
idea.alias,
|
|
329
|
-
idea.state,
|
|
330
|
-
idea.createdAt,
|
|
331
|
-
idea.title,
|
|
332
|
-
].map(markdownTableCell).join(" | ")} |`,
|
|
382
|
+
`- ${localize(language, "Created before", "创建时间上界")}: ${formatDate(response.query.createdBefore, language, now)}`,
|
|
333
383
|
);
|
|
334
384
|
}
|
|
385
|
+
lines.push("", ...renderIdeaTable(response.items, language, now));
|
|
335
386
|
if (response.items.length === 0) {
|
|
336
387
|
lines.push(
|
|
337
388
|
"",
|
|
@@ -341,56 +392,17 @@ function renderIdeaList(response, language) {
|
|
|
341
392
|
return lines.join("\n");
|
|
342
393
|
}
|
|
343
394
|
|
|
344
|
-
export function renderResponse(response) {
|
|
395
|
+
export function renderResponse(response, { now = new Date() } = {}) {
|
|
345
396
|
const language = response.language ?? DEFAULT_LANGUAGE;
|
|
346
397
|
if (response.kind === "idea-list") {
|
|
347
398
|
return [
|
|
348
399
|
`## ${responseTitle(response)}`,
|
|
349
400
|
"",
|
|
350
|
-
renderIdeaList(response, language),
|
|
401
|
+
renderIdeaList(response, language, now),
|
|
351
402
|
].join("\n");
|
|
352
403
|
}
|
|
353
404
|
const lines = [`## ${responseTitle(response)}`, "", response.summary];
|
|
354
405
|
|
|
355
|
-
if (response.inventory) {
|
|
356
|
-
const counts = Object.entries(response.inventory.counts)
|
|
357
|
-
.map(([state, count]) => `${state}=${count}`)
|
|
358
|
-
.join(", ");
|
|
359
|
-
lines.push(
|
|
360
|
-
"",
|
|
361
|
-
`- ${localize(language, "States", "状态")}: ${response.query.states
|
|
362
|
-
.map(codeSpan)
|
|
363
|
-
.join(", ")}`,
|
|
364
|
-
);
|
|
365
|
-
if (response.query.query !== null) {
|
|
366
|
-
lines.push(
|
|
367
|
-
`- ${localize(language, "Query", "查询")}: ${codeSpan(response.query.query)}`,
|
|
368
|
-
);
|
|
369
|
-
}
|
|
370
|
-
if (response.query.createdSince !== null) {
|
|
371
|
-
lines.push(
|
|
372
|
-
`- ${localize(language, "Created since", "创建时间下界")}: ${codeSpan(response.query.createdSince)}`,
|
|
373
|
-
);
|
|
374
|
-
}
|
|
375
|
-
if (response.query.createdBefore !== null) {
|
|
376
|
-
lines.push(
|
|
377
|
-
`- ${localize(language, "Created before", "创建时间上界")}: ${codeSpan(response.query.createdBefore)}`,
|
|
378
|
-
);
|
|
379
|
-
}
|
|
380
|
-
lines.push(
|
|
381
|
-
`- ${localize(language, "Sort", "排序")}: ${codeSpan(response.query.sort)}`,
|
|
382
|
-
`- ${localize(language, "Limit", "上限")}: ${response.query.limit === null
|
|
383
|
-
? localize(language, "none", "无")
|
|
384
|
-
: codeSpan(response.query.limit)}`,
|
|
385
|
-
`- ${localize(language, "Matched", "匹配")}: ${response.inventory.matched}`,
|
|
386
|
-
`- ${localize(language, "Returned", "返回")}: ${response.inventory.returned}`,
|
|
387
|
-
`- ${localize(language, "Truncated", "已截断")}: ${response.inventory.truncated
|
|
388
|
-
? localize(language, "yes", "是")
|
|
389
|
-
: localize(language, "no", "否")}`,
|
|
390
|
-
`- ${localize(language, "Counts", "计数")}: ${counts}`,
|
|
391
|
-
);
|
|
392
|
-
}
|
|
393
|
-
|
|
394
406
|
if (response.validation) {
|
|
395
407
|
const target = response.validation.target.type === "commit"
|
|
396
408
|
? `${response.validation.target.type} ${response.validation.target.revision}`
|
|
@@ -432,40 +444,22 @@ export function renderResponse(response) {
|
|
|
432
444
|
);
|
|
433
445
|
}
|
|
434
446
|
|
|
435
|
-
if (response.choices
|
|
447
|
+
if (response.choices) {
|
|
436
448
|
lines.push(
|
|
437
449
|
"",
|
|
438
|
-
`### ${localize(language, "
|
|
450
|
+
`### ${localize(language, "Active ideas", "Active ideas")}`,
|
|
439
451
|
"",
|
|
440
452
|
);
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
if (response.items.length === 0) {
|
|
450
|
-
lines.push(localize(language, "No ideas matched.", "没有匹配的 idea。"));
|
|
453
|
+
if (response.choices.length === 0) {
|
|
454
|
+
lines.push(
|
|
455
|
+
localize(
|
|
456
|
+
language,
|
|
457
|
+
"No active ideas are available.",
|
|
458
|
+
"当前没有 active idea。",
|
|
459
|
+
),
|
|
460
|
+
);
|
|
451
461
|
} else {
|
|
452
|
-
|
|
453
|
-
const details = [
|
|
454
|
-
`${localize(language, "State", "状态")}=${codeSpan(idea.state)}`,
|
|
455
|
-
`${localize(language, "Created", "创建时间")}=${codeSpan(idea.createdAt)}`,
|
|
456
|
-
];
|
|
457
|
-
if (idea.alias !== undefined) {
|
|
458
|
-
details.push(
|
|
459
|
-
`${localize(language, "Alias", "Alias")}=${codeSpan(idea.alias)}`,
|
|
460
|
-
);
|
|
461
|
-
}
|
|
462
|
-
if (idea.title !== undefined) {
|
|
463
|
-
details.push(
|
|
464
|
-
`${localize(language, "Title", "标题")}=${codeSpan(idea.title)}`,
|
|
465
|
-
);
|
|
466
|
-
}
|
|
467
|
-
lines.push(`- ${codeSpan(idea.id)}: ${details.join("; ")}`);
|
|
468
|
-
}
|
|
462
|
+
lines.push(...renderIdeaTable(response.choices, language, now));
|
|
469
463
|
}
|
|
470
464
|
}
|
|
471
465
|
|
|
@@ -475,9 +469,13 @@ export function renderResponse(response) {
|
|
|
475
469
|
`### ${localize(language, "Issues to address", "需要处理的问题")}`,
|
|
476
470
|
"",
|
|
477
471
|
);
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
472
|
+
lines.push(...renderMarkdownTable(
|
|
473
|
+
[
|
|
474
|
+
localize(language, "Type", "类型"),
|
|
475
|
+
localize(language, "Summary", "概要"),
|
|
476
|
+
],
|
|
477
|
+
response.problems.map(({ type, summary }) => [type, summary]),
|
|
478
|
+
));
|
|
481
479
|
}
|
|
482
480
|
|
|
483
481
|
if (response.nextSteps?.length > 0) {
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
|
|
3
|
+
import { traceSync } from "./trace.js";
|
|
4
|
+
|
|
5
|
+
export function runSubprocess(
|
|
6
|
+
command,
|
|
7
|
+
args,
|
|
8
|
+
options,
|
|
9
|
+
{
|
|
10
|
+
attributes = {},
|
|
11
|
+
spanName = "process.command",
|
|
12
|
+
spawn = spawnSync,
|
|
13
|
+
} = {},
|
|
14
|
+
) {
|
|
15
|
+
return traceSync(
|
|
16
|
+
spanName,
|
|
17
|
+
attributes,
|
|
18
|
+
() => spawn(command, args, options),
|
|
19
|
+
(completed) => ({
|
|
20
|
+
attributes: {
|
|
21
|
+
exitCode: completed.status,
|
|
22
|
+
signal: completed.signal,
|
|
23
|
+
},
|
|
24
|
+
status: completed.status === 0 ? "ok" : "error",
|
|
25
|
+
}),
|
|
26
|
+
);
|
|
27
|
+
}
|
package/src/trace.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
2
|
import { createHash, randomUUID } from "node:crypto";
|
|
3
3
|
import { constants } from "node:fs";
|
|
4
|
-
import { access, lstat,
|
|
4
|
+
import { access, lstat, open } from "node:fs/promises";
|
|
5
5
|
import { dirname, resolve } from "node:path";
|
|
6
6
|
|
|
7
7
|
export const TRACE_SCHEMA_VERSION = 2;
|
|
@@ -217,6 +217,41 @@ export function traceSync(name, attributes, callback, summarize = () => ({})) {
|
|
|
217
217
|
);
|
|
218
218
|
}
|
|
219
219
|
|
|
220
|
+
async function flushTrace(path, trace) {
|
|
221
|
+
const span = trace.start("trace.flush", null, {});
|
|
222
|
+
let handle;
|
|
223
|
+
let failure;
|
|
224
|
+
let spanEnded = false;
|
|
225
|
+
try {
|
|
226
|
+
handle = await open(path, "wx");
|
|
227
|
+
await handle.writeFile(trace.serialize(), { encoding: "utf8" });
|
|
228
|
+
await handle.sync();
|
|
229
|
+
trace.end(span, "ok", { eventCount: trace.events.length + 1 });
|
|
230
|
+
spanEnded = true;
|
|
231
|
+
await handle.appendFile(
|
|
232
|
+
`${JSON.stringify(trace.events.at(-1))}\n`,
|
|
233
|
+
{ encoding: "utf8" },
|
|
234
|
+
);
|
|
235
|
+
await handle.sync();
|
|
236
|
+
} catch (caught) {
|
|
237
|
+
if (!spanEnded) {
|
|
238
|
+
trace.end(span, "error", { errorName: errorName(caught) });
|
|
239
|
+
}
|
|
240
|
+
failure = caught;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (handle) {
|
|
244
|
+
try {
|
|
245
|
+
await handle.close();
|
|
246
|
+
} catch (caught) {
|
|
247
|
+
failure = failure
|
|
248
|
+
? new AggregateError([failure, caught], `Cannot close trace file: ${path}`)
|
|
249
|
+
: caught;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
if (failure) throw failure;
|
|
253
|
+
}
|
|
254
|
+
|
|
220
255
|
export async function withTraceFile(file, name, attributes, callback) {
|
|
221
256
|
if (file === undefined) return callback();
|
|
222
257
|
|
|
@@ -236,7 +271,7 @@ export async function withTraceFile(file, name, attributes, callback) {
|
|
|
236
271
|
|
|
237
272
|
let traceError;
|
|
238
273
|
try {
|
|
239
|
-
await
|
|
274
|
+
await flushTrace(path, trace);
|
|
240
275
|
} catch (caught) {
|
|
241
276
|
traceError = caught;
|
|
242
277
|
}
|