silvermoon 0.0.1 → 0.0.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/README.md +0 -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/repository.js
CHANGED
|
@@ -1,88 +1,88 @@
|
|
|
1
|
-
import { createHash } from "node:crypto";
|
|
2
|
-
import { spawnSync } from "node:child_process";
|
|
3
|
-
|
|
4
|
-
const CONTROL_CHARACTER = /[\u0000-\u001f\u007f]/;
|
|
5
|
-
const ENCODED_SEPARATOR = /%(?:2f|5c)/i;
|
|
6
|
-
const CANONICAL_HOST = /^(?:[a-z0-9](?:[a-z0-9.-]*[a-z0-9])?|\[[0-9a-f:.]+\])$/;
|
|
7
|
-
const INVALID_BRANCH_CHARACTER = /[\u0000-\u0020\u007f~^:?*\[\\]/;
|
|
8
|
-
|
|
9
|
-
export function canonicalRepository(value) {
|
|
10
|
-
if (
|
|
11
|
-
typeof value !== "string" ||
|
|
12
|
-
value.length === 0 ||
|
|
13
|
-
CONTROL_CHARACTER.test(value) ||
|
|
14
|
-
value.includes("\\")
|
|
15
|
-
) {
|
|
16
|
-
return null;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
let parsed;
|
|
20
|
-
try {
|
|
21
|
-
parsed = new URL(value);
|
|
22
|
-
} catch {
|
|
23
|
-
return null;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
if (
|
|
27
|
-
parsed.protocol !== "https:" ||
|
|
28
|
-
!parsed.hostname ||
|
|
29
|
-
!CANONICAL_HOST.test(parsed.hostname) ||
|
|
30
|
-
parsed.username ||
|
|
31
|
-
parsed.password ||
|
|
32
|
-
parsed.search ||
|
|
33
|
-
parsed.hash ||
|
|
34
|
-
parsed.pathname === "/" ||
|
|
35
|
-
parsed.pathname.endsWith("/") ||
|
|
36
|
-
parsed.pathname.includes("//") ||
|
|
37
|
-
ENCODED_SEPARATOR.test(parsed.pathname) ||
|
|
38
|
-
parsed.href !== value
|
|
39
|
-
) {
|
|
40
|
-
return null;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
return parsed.href;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
export function validRepository(value) {
|
|
47
|
-
return canonicalRepository(value) !== null;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
export function repositoryNamespace(value) {
|
|
51
|
-
const canonical = canonicalRepository(value);
|
|
52
|
-
if (!canonical) throw new Error(`Invalid repository URL: ${String(value)}`);
|
|
53
|
-
return createHash("sha256").update(canonical).digest("hex");
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
export function validBranchName(value) {
|
|
57
|
-
if (
|
|
58
|
-
typeof value !== "string" ||
|
|
59
|
-
value.length === 0 ||
|
|
60
|
-
value === "@" ||
|
|
61
|
-
value.startsWith("-") ||
|
|
62
|
-
value.startsWith("refs/") ||
|
|
63
|
-
value.startsWith("remotes/") ||
|
|
64
|
-
value.endsWith("/") ||
|
|
65
|
-
value.endsWith(".") ||
|
|
66
|
-
value.includes("//") ||
|
|
67
|
-
value.includes("..") ||
|
|
68
|
-
value.includes("@{") ||
|
|
69
|
-
INVALID_BRANCH_CHARACTER.test(value)
|
|
70
|
-
) {
|
|
71
|
-
return false;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
const structurallyValid = value
|
|
75
|
-
.split("/")
|
|
76
|
-
.every((component) => component && !component.startsWith(".") && !component.endsWith(".lock"));
|
|
77
|
-
if (!structurallyValid) return false;
|
|
78
|
-
|
|
79
|
-
const checked = spawnSync("git", ["check-ref-format", "--branch", value], {
|
|
80
|
-
encoding: "utf8",
|
|
81
|
-
windowsHide: true,
|
|
82
|
-
});
|
|
83
|
-
return checked.status === 0;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
export function effectiveSourceRepository(config, record) {
|
|
87
|
-
return record.sourceRepository ?? config.primaryRepository;
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { spawnSync } from "node:child_process";
|
|
3
|
+
|
|
4
|
+
const CONTROL_CHARACTER = /[\u0000-\u001f\u007f]/;
|
|
5
|
+
const ENCODED_SEPARATOR = /%(?:2f|5c)/i;
|
|
6
|
+
const CANONICAL_HOST = /^(?:[a-z0-9](?:[a-z0-9.-]*[a-z0-9])?|\[[0-9a-f:.]+\])$/;
|
|
7
|
+
const INVALID_BRANCH_CHARACTER = /[\u0000-\u0020\u007f~^:?*\[\\]/;
|
|
8
|
+
|
|
9
|
+
export function canonicalRepository(value) {
|
|
10
|
+
if (
|
|
11
|
+
typeof value !== "string" ||
|
|
12
|
+
value.length === 0 ||
|
|
13
|
+
CONTROL_CHARACTER.test(value) ||
|
|
14
|
+
value.includes("\\")
|
|
15
|
+
) {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
let parsed;
|
|
20
|
+
try {
|
|
21
|
+
parsed = new URL(value);
|
|
22
|
+
} catch {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (
|
|
27
|
+
parsed.protocol !== "https:" ||
|
|
28
|
+
!parsed.hostname ||
|
|
29
|
+
!CANONICAL_HOST.test(parsed.hostname) ||
|
|
30
|
+
parsed.username ||
|
|
31
|
+
parsed.password ||
|
|
32
|
+
parsed.search ||
|
|
33
|
+
parsed.hash ||
|
|
34
|
+
parsed.pathname === "/" ||
|
|
35
|
+
parsed.pathname.endsWith("/") ||
|
|
36
|
+
parsed.pathname.includes("//") ||
|
|
37
|
+
ENCODED_SEPARATOR.test(parsed.pathname) ||
|
|
38
|
+
parsed.href !== value
|
|
39
|
+
) {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return parsed.href;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function validRepository(value) {
|
|
47
|
+
return canonicalRepository(value) !== null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function repositoryNamespace(value) {
|
|
51
|
+
const canonical = canonicalRepository(value);
|
|
52
|
+
if (!canonical) throw new Error(`Invalid repository URL: ${String(value)}`);
|
|
53
|
+
return createHash("sha256").update(canonical).digest("hex");
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function validBranchName(value) {
|
|
57
|
+
if (
|
|
58
|
+
typeof value !== "string" ||
|
|
59
|
+
value.length === 0 ||
|
|
60
|
+
value === "@" ||
|
|
61
|
+
value.startsWith("-") ||
|
|
62
|
+
value.startsWith("refs/") ||
|
|
63
|
+
value.startsWith("remotes/") ||
|
|
64
|
+
value.endsWith("/") ||
|
|
65
|
+
value.endsWith(".") ||
|
|
66
|
+
value.includes("//") ||
|
|
67
|
+
value.includes("..") ||
|
|
68
|
+
value.includes("@{") ||
|
|
69
|
+
INVALID_BRANCH_CHARACTER.test(value)
|
|
70
|
+
) {
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const structurallyValid = value
|
|
75
|
+
.split("/")
|
|
76
|
+
.every((component) => component && !component.startsWith(".") && !component.endsWith(".lock"));
|
|
77
|
+
if (!structurallyValid) return false;
|
|
78
|
+
|
|
79
|
+
const checked = spawnSync("git", ["check-ref-format", "--branch", value], {
|
|
80
|
+
encoding: "utf8",
|
|
81
|
+
windowsHide: true,
|
|
82
|
+
});
|
|
83
|
+
return checked.status === 0;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function effectiveSourceRepository(config, record) {
|
|
87
|
+
return record.sourceRepository ?? config.primaryRepository;
|
|
88
88
|
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { lstat, readFile } from "node:fs/promises";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { resolve } from "node:path";
|
|
4
|
+
|
|
5
|
+
import { isCanonicalLanguageTag } from "./language.js";
|
|
6
|
+
import { parseStrictYaml, stringifyCanonicalYaml } from "./yaml.js";
|
|
7
|
+
|
|
8
|
+
export const USER_CONFIG_PATH = ".config/silvermoon/config.yaml";
|
|
9
|
+
|
|
10
|
+
const USER_CONFIG_KEYS = new Set(["version", "preferredLanguage"]);
|
|
11
|
+
|
|
12
|
+
function diagnostic(code, path, message, remediation) {
|
|
13
|
+
return { code, level: "error", path, message, remediation };
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function serializeUserConfig(config) {
|
|
17
|
+
const canonical = { version: 1 };
|
|
18
|
+
if (config.preferredLanguage !== undefined) {
|
|
19
|
+
canonical.preferredLanguage = config.preferredLanguage;
|
|
20
|
+
}
|
|
21
|
+
return stringifyCanonicalYaml(canonical);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export async function loadUserConfig({ home = homedir() } = {}) {
|
|
25
|
+
const configPath = resolve(home, USER_CONFIG_PATH);
|
|
26
|
+
let metadata;
|
|
27
|
+
let source;
|
|
28
|
+
try {
|
|
29
|
+
metadata = await lstat(configPath);
|
|
30
|
+
if (!metadata.isFile() || metadata.isSymbolicLink()) {
|
|
31
|
+
throw Object.assign(new Error("User configuration must be a regular file"), {
|
|
32
|
+
code: "EINVAL",
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
source = await readFile(configPath, "utf8");
|
|
36
|
+
} catch (caught) {
|
|
37
|
+
if (caught.code === "ENOENT") {
|
|
38
|
+
return { config: null, configPath, diagnostics: [] };
|
|
39
|
+
}
|
|
40
|
+
return {
|
|
41
|
+
config: null,
|
|
42
|
+
configPath,
|
|
43
|
+
diagnostics: [diagnostic(
|
|
44
|
+
"user-config.invalid-file",
|
|
45
|
+
configPath,
|
|
46
|
+
`Cannot read Silvermoon user configuration: ${caught.message}`,
|
|
47
|
+
`Replace ${configPath} with a readable regular file or remove it.`,
|
|
48
|
+
)],
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const normalizedSource = source.replaceAll("\r\n", "\n");
|
|
53
|
+
let value;
|
|
54
|
+
try {
|
|
55
|
+
value = parseStrictYaml(normalizedSource);
|
|
56
|
+
} catch (caught) {
|
|
57
|
+
return {
|
|
58
|
+
config: null,
|
|
59
|
+
configPath,
|
|
60
|
+
diagnostics: [diagnostic(
|
|
61
|
+
"user-config.invalid-yaml",
|
|
62
|
+
configPath,
|
|
63
|
+
`Cannot parse Silvermoon user configuration: ${caught.message}`,
|
|
64
|
+
`Use the strict YAML contract in ${configPath}.`,
|
|
65
|
+
)],
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (value === null || Array.isArray(value) || typeof value !== "object") {
|
|
70
|
+
return {
|
|
71
|
+
config: null,
|
|
72
|
+
configPath,
|
|
73
|
+
diagnostics: [diagnostic(
|
|
74
|
+
"user-config.invalid-type",
|
|
75
|
+
configPath,
|
|
76
|
+
"The Silvermoon user configuration must be a YAML mapping.",
|
|
77
|
+
`Replace ${configPath} with the documented mapping.`,
|
|
78
|
+
)],
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const diagnostics = [];
|
|
83
|
+
for (const key of Object.keys(value).sort()) {
|
|
84
|
+
if (!USER_CONFIG_KEYS.has(key)) {
|
|
85
|
+
diagnostics.push(diagnostic(
|
|
86
|
+
"user-config.unknown-key",
|
|
87
|
+
`${configPath}#${key}`,
|
|
88
|
+
`Unknown Silvermoon user configuration key: ${key}`,
|
|
89
|
+
`Remove ${key}.`,
|
|
90
|
+
));
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
if (!Object.hasOwn(value, "version")) {
|
|
94
|
+
diagnostics.push(diagnostic(
|
|
95
|
+
"user-config.missing-version",
|
|
96
|
+
`${configPath}#version`,
|
|
97
|
+
"Missing required key: version",
|
|
98
|
+
`Add version: 1 to ${configPath}.`,
|
|
99
|
+
));
|
|
100
|
+
} else if (value.version !== 1) {
|
|
101
|
+
diagnostics.push(diagnostic(
|
|
102
|
+
"user-config.unsupported-version",
|
|
103
|
+
`${configPath}#version`,
|
|
104
|
+
`Unsupported Silvermoon user configuration version: ${String(value.version)}`,
|
|
105
|
+
"Use version: 1.",
|
|
106
|
+
));
|
|
107
|
+
}
|
|
108
|
+
if (
|
|
109
|
+
Object.hasOwn(value, "preferredLanguage") &&
|
|
110
|
+
!isCanonicalLanguageTag(value.preferredLanguage)
|
|
111
|
+
) {
|
|
112
|
+
diagnostics.push(diagnostic(
|
|
113
|
+
"user-config.invalid-preferred-language",
|
|
114
|
+
`${configPath}#preferredLanguage`,
|
|
115
|
+
"preferredLanguage must be a canonical BCP 47 language tag.",
|
|
116
|
+
"Use a canonical tag such as en or zh-CN.",
|
|
117
|
+
));
|
|
118
|
+
}
|
|
119
|
+
if (diagnostics.length === 0 && serializeUserConfig(value) !== normalizedSource) {
|
|
120
|
+
diagnostics.push(diagnostic(
|
|
121
|
+
"user-config.noncanonical",
|
|
122
|
+
configPath,
|
|
123
|
+
"The Silvermoon user configuration is valid but not canonical.",
|
|
124
|
+
"Rewrite properties in version, preferredLanguage order with LF endings.",
|
|
125
|
+
));
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return {
|
|
129
|
+
config: diagnostics.length === 0 ? value : null,
|
|
130
|
+
configPath,
|
|
131
|
+
diagnostics,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export { serializeUserConfig };
|