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/README.md +149 -236
- package/README.zh-CN.md +114 -199
- package/assets/silvermoon.svg +176 -175
- package/bin/silvermoon.js +0 -0
- package/docs/assets/silvermoon-avatar.svg +395 -0
- package/docs/core-concepts.md +69 -0
- package/docs/getting-started.md +115 -0
- package/docs/maintaining.md +56 -0
- package/docs/npm-package-releases.md +139 -0
- package/docs/operations.md +110 -0
- package/docs/reference.md +138 -0
- package/docs/repository-tasks.md +61 -0
- package/package.json +69 -65
- package/schema/v1/config.schema.json +27 -24
- package/schema/v1/definitions.schema.json +30 -25
- package/schema/v1/idea-status.schema.json +44 -41
- package/schema/v1/user-config.schema.json +19 -0
- package/skills/silvermoon/SKILL.md +216 -209
- package/skills/silvermoon/references/adoption.md +122 -122
- package/src/adoption.js +313 -313
- package/src/cli.js +200 -196
- package/src/config.js +193 -176
- package/src/create-idea.js +272 -235
- package/src/git.js +284 -284
- package/src/idea-layout.js +439 -439
- package/src/idea-templates.js +109 -109
- package/src/ideas.js +140 -131
- package/src/index.js +260 -260
- package/src/language.js +29 -0
- package/src/layout.js +21 -21
- package/src/repository.js +87 -87
- package/src/user-config.js +135 -0
- package/src/whatsnext.js +453 -378
- package/src/yaml.js +66 -66
package/src/create-idea.js
CHANGED
|
@@ -1,235 +1,272 @@
|
|
|
1
|
-
import { randomBytes } from "node:crypto";
|
|
2
|
-
import { lstat, mkdir, readFile, rm, rmdir, writeFile } from "node:fs/promises";
|
|
3
|
-
import { resolve } from "node:path";
|
|
4
|
-
|
|
5
|
-
import { loadConfig } from "./config.js";
|
|
6
|
-
import {
|
|
7
|
-
DEPLOYMENT_TEMPLATE,
|
|
8
|
-
IDEA_TEMPLATE,
|
|
9
|
-
IMPLEMENTATION_TEMPLATE,
|
|
10
|
-
LEDGER_TEMPLATE,
|
|
11
|
-
} from "./idea-templates.js";
|
|
12
|
-
import { isValidUlid, serializeIdeaStatus } from "./ideas.js";
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
value
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
await
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
2
|
+
import { lstat, mkdir, readFile, rm, rmdir, writeFile } from "node:fs/promises";
|
|
3
|
+
import { resolve } from "node:path";
|
|
4
|
+
|
|
5
|
+
import { loadConfig } from "./config.js";
|
|
6
|
+
import {
|
|
7
|
+
DEPLOYMENT_TEMPLATE,
|
|
8
|
+
IDEA_TEMPLATE,
|
|
9
|
+
IMPLEMENTATION_TEMPLATE,
|
|
10
|
+
LEDGER_TEMPLATE,
|
|
11
|
+
} from "./idea-templates.js";
|
|
12
|
+
import { isValidUlid, serializeIdeaStatus } from "./ideas.js";
|
|
13
|
+
import { canonicalizeLanguageTag } from "./language.js";
|
|
14
|
+
import { IDEAS_ROOT, ideaPaths } from "./layout.js";
|
|
15
|
+
import { whatsNext } from "./whatsnext.js";
|
|
16
|
+
|
|
17
|
+
const ULID_ALPHABET = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
|
|
18
|
+
const MAX_ID_ATTEMPTS = 32;
|
|
19
|
+
|
|
20
|
+
function encode(value, length) {
|
|
21
|
+
let encoded = "";
|
|
22
|
+
for (let index = 0; index < length; index += 1) {
|
|
23
|
+
encoded = ULID_ALPHABET[Number(value & 31n)] + encoded;
|
|
24
|
+
value >>= 5n;
|
|
25
|
+
}
|
|
26
|
+
return encoded;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function generateUlid({ now = Date.now(), random = randomBytes(10) } = {}) {
|
|
30
|
+
return encode(BigInt(now), 10) + encode(BigInt(`0x${Buffer.from(random).toString("hex")}`), 16);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function pathExists(path, inspect) {
|
|
34
|
+
try {
|
|
35
|
+
await inspect(path);
|
|
36
|
+
return true;
|
|
37
|
+
} catch (caught) {
|
|
38
|
+
if (caught.code === "ENOENT") return false;
|
|
39
|
+
throw caught;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function removeOwnedFile(path, expected, read, remove) {
|
|
44
|
+
let current;
|
|
45
|
+
try {
|
|
46
|
+
current = await read(path);
|
|
47
|
+
} catch (caught) {
|
|
48
|
+
if (caught.code === "ENOENT") return;
|
|
49
|
+
throw caught;
|
|
50
|
+
}
|
|
51
|
+
const expectedBytes = Buffer.from(expected);
|
|
52
|
+
if (
|
|
53
|
+
current.length <= expectedBytes.length &&
|
|
54
|
+
expectedBytes.subarray(0, current.length).equals(current)
|
|
55
|
+
) {
|
|
56
|
+
await remove(path, { force: true });
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function removeCreatedDirectories(created, removeDirectory) {
|
|
61
|
+
for (const path of [...created].reverse()) {
|
|
62
|
+
try {
|
|
63
|
+
await removeDirectory(path);
|
|
64
|
+
} catch {
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function ensureDirectoryPath(root, relativePath, inspect, makeDirectory, removeDirectory) {
|
|
71
|
+
const created = [];
|
|
72
|
+
let current = root;
|
|
73
|
+
try {
|
|
74
|
+
for (const segment of relativePath.split("/")) {
|
|
75
|
+
current = resolve(current, segment);
|
|
76
|
+
let metadata;
|
|
77
|
+
try {
|
|
78
|
+
metadata = await inspect(current);
|
|
79
|
+
} catch (caught) {
|
|
80
|
+
if (caught.code !== "ENOENT") throw caught;
|
|
81
|
+
}
|
|
82
|
+
if (metadata) {
|
|
83
|
+
if (!metadata.isDirectory() || metadata.isSymbolicLink()) {
|
|
84
|
+
throw new Error(`Silvermoon path segment is not a regular directory: ${current}`);
|
|
85
|
+
}
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
try {
|
|
89
|
+
await makeDirectory(current);
|
|
90
|
+
created.push(current);
|
|
91
|
+
} catch (caught) {
|
|
92
|
+
if (caught.code !== "EEXIST") throw caught;
|
|
93
|
+
metadata = await inspect(current);
|
|
94
|
+
if (!metadata.isDirectory() || metadata.isSymbolicLink()) throw caught;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return created;
|
|
98
|
+
} catch (caught) {
|
|
99
|
+
await removeCreatedDirectories(created, removeDirectory);
|
|
100
|
+
throw caught;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function failure(root, preflight, caught) {
|
|
105
|
+
return {
|
|
106
|
+
command: "create-idea",
|
|
107
|
+
ok: false,
|
|
108
|
+
root,
|
|
109
|
+
diagnostics: [{
|
|
110
|
+
code: "idea.create.failed",
|
|
111
|
+
level: "error",
|
|
112
|
+
message: caught.message,
|
|
113
|
+
remediation: "Preserve existing paths, resolve the filesystem error, and retry.",
|
|
114
|
+
}],
|
|
115
|
+
result: {
|
|
116
|
+
observedPrimaryCommit: preflight.result?.observedPrimaryCommit ?? null,
|
|
117
|
+
request: { kind: "create-idea" },
|
|
118
|
+
selectedIdea: null,
|
|
119
|
+
action: null,
|
|
120
|
+
createdIdea: null,
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export async function createIdea({
|
|
126
|
+
generateId = generateUlid,
|
|
127
|
+
language,
|
|
128
|
+
operations = {},
|
|
129
|
+
root = process.cwd(),
|
|
130
|
+
userHome,
|
|
131
|
+
} = {}) {
|
|
132
|
+
const repositoryRoot = resolve(root);
|
|
133
|
+
let canonicalLanguage;
|
|
134
|
+
if (language !== undefined) {
|
|
135
|
+
try {
|
|
136
|
+
canonicalLanguage = canonicalizeLanguageTag(language);
|
|
137
|
+
} catch (caught) {
|
|
138
|
+
return {
|
|
139
|
+
command: "create-idea",
|
|
140
|
+
ok: false,
|
|
141
|
+
root: repositoryRoot,
|
|
142
|
+
diagnostics: [{
|
|
143
|
+
code: "idea.language.invalid",
|
|
144
|
+
level: "error",
|
|
145
|
+
message: caught.message,
|
|
146
|
+
remediation: "Use a valid BCP 47 tag such as en or zh-CN.",
|
|
147
|
+
}],
|
|
148
|
+
result: {
|
|
149
|
+
observedPrimaryCommit: null,
|
|
150
|
+
request: { kind: "create-idea" },
|
|
151
|
+
selectedIdea: null,
|
|
152
|
+
action: null,
|
|
153
|
+
createdIdea: null,
|
|
154
|
+
},
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
const preflight = await whatsNext({
|
|
159
|
+
create: true,
|
|
160
|
+
root: repositoryRoot,
|
|
161
|
+
userHome,
|
|
162
|
+
});
|
|
163
|
+
const routed = { ...preflight, command: "create-idea" };
|
|
164
|
+
if (!preflight.ok || preflight.result.action?.code !== "create-idea") return routed;
|
|
165
|
+
|
|
166
|
+
const loaded = await loadConfig({ root: repositoryRoot });
|
|
167
|
+
if (!loaded.config) return { ...routed, ok: false, diagnostics: loaded.diagnostics };
|
|
168
|
+
|
|
169
|
+
const inspect = operations.lstat ?? lstat;
|
|
170
|
+
const makeDirectory = operations.mkdir ?? mkdir;
|
|
171
|
+
const read = operations.readFile ?? readFile;
|
|
172
|
+
const remove = operations.rm ?? rm;
|
|
173
|
+
const removeDirectory = operations.rmdir ?? rmdir;
|
|
174
|
+
const write = operations.writeFile ?? writeFile;
|
|
175
|
+
let rootDirectories;
|
|
176
|
+
try {
|
|
177
|
+
rootDirectories = await ensureDirectoryPath(
|
|
178
|
+
repositoryRoot,
|
|
179
|
+
IDEAS_ROOT,
|
|
180
|
+
inspect,
|
|
181
|
+
makeDirectory,
|
|
182
|
+
removeDirectory,
|
|
183
|
+
);
|
|
184
|
+
} catch (caught) {
|
|
185
|
+
return failure(repositoryRoot, preflight, caught);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const failRoot = async (caught) => {
|
|
189
|
+
await removeCreatedDirectories(rootDirectories, removeDirectory);
|
|
190
|
+
return failure(repositoryRoot, preflight, caught);
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
for (let attempt = 0; attempt < MAX_ID_ATTEMPTS; attempt += 1) {
|
|
194
|
+
const id = await generateId();
|
|
195
|
+
if (!isValidUlid(id)) {
|
|
196
|
+
return failRoot(new Error(`Generated idea id is not a canonical ULID: ${id}`));
|
|
197
|
+
}
|
|
198
|
+
const paths = ideaPaths(id);
|
|
199
|
+
const folder = resolve(repositoryRoot, paths.ideaPath);
|
|
200
|
+
if (await pathExists(folder, inspect)) continue;
|
|
201
|
+
|
|
202
|
+
const directoryPaths = [
|
|
203
|
+
paths.ideaPath,
|
|
204
|
+
paths.outerPath,
|
|
205
|
+
paths.innerPath,
|
|
206
|
+
paths.idealPath,
|
|
207
|
+
];
|
|
208
|
+
const files = [
|
|
209
|
+
[paths.ideaDocumentPath, IDEA_TEMPLATE],
|
|
210
|
+
[paths.implementationDocumentPath, IMPLEMENTATION_TEMPLATE],
|
|
211
|
+
[paths.deploymentDocumentPath, DEPLOYMENT_TEMPLATE],
|
|
212
|
+
[paths.ledgerPath, LEDGER_TEMPLATE],
|
|
213
|
+
[paths.statusPath, serializeIdeaStatus({
|
|
214
|
+
version: 1,
|
|
215
|
+
id,
|
|
216
|
+
...(canonicalLanguage === undefined ? {} : { language: canonicalLanguage }),
|
|
217
|
+
})],
|
|
218
|
+
];
|
|
219
|
+
const createdDirectories = [];
|
|
220
|
+
const cleanupFiles = [];
|
|
221
|
+
try {
|
|
222
|
+
for (const relativePath of directoryPaths) {
|
|
223
|
+
const absolutePath = resolve(repositoryRoot, relativePath);
|
|
224
|
+
await makeDirectory(absolutePath);
|
|
225
|
+
createdDirectories.push(absolutePath);
|
|
226
|
+
}
|
|
227
|
+
for (const [relativePath, source] of files) {
|
|
228
|
+
const absolutePath = resolve(repositoryRoot, relativePath);
|
|
229
|
+
const cleanup = [absolutePath, source];
|
|
230
|
+
cleanupFiles.push(cleanup);
|
|
231
|
+
try {
|
|
232
|
+
await write(absolutePath, source, { flag: "wx" });
|
|
233
|
+
} catch (caught) {
|
|
234
|
+
if (caught.code === "EEXIST") cleanupFiles.pop();
|
|
235
|
+
throw caught;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
return {
|
|
239
|
+
command: "create-idea",
|
|
240
|
+
ok: true,
|
|
241
|
+
root: repositoryRoot,
|
|
242
|
+
diagnostics: [],
|
|
243
|
+
result: {
|
|
244
|
+
observedPrimaryCommit: preflight.result.observedPrimaryCommit,
|
|
245
|
+
request: { kind: "create-idea" },
|
|
246
|
+
selectedIdea: null,
|
|
247
|
+
createdIdea: {
|
|
248
|
+
id,
|
|
249
|
+
ideaPath: paths.ideaPath,
|
|
250
|
+
statusPath: paths.statusPath,
|
|
251
|
+
ideaDocumentPath: paths.ideaDocumentPath,
|
|
252
|
+
implementationDocumentPath: paths.implementationDocumentPath,
|
|
253
|
+
deploymentDocumentPath: paths.deploymentDocumentPath,
|
|
254
|
+
ledgerPath: paths.ledgerPath,
|
|
255
|
+
...(canonicalLanguage === undefined ? {} : { language: canonicalLanguage }),
|
|
256
|
+
},
|
|
257
|
+
},
|
|
258
|
+
};
|
|
259
|
+
} catch (caught) {
|
|
260
|
+
for (const [path, source] of [...cleanupFiles].reverse()) {
|
|
261
|
+
await removeOwnedFile(path, source, read, remove).catch(() => { });
|
|
262
|
+
}
|
|
263
|
+
await removeCreatedDirectories(createdDirectories, removeDirectory);
|
|
264
|
+
if (caught.code === "EEXIST") continue;
|
|
265
|
+
return failRoot(caught);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
return failRoot(
|
|
270
|
+
new Error(`Could not allocate a unique idea id after ${MAX_ID_ATTEMPTS} attempts.`),
|
|
271
|
+
);
|
|
272
|
+
}
|