@netpilot/skills 0.3.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.
Files changed (61) hide show
  1. package/.claude-plugin/marketplace.json +26 -0
  2. package/.claude-plugin/plugin.json +18 -0
  3. package/.codex-plugin/plugin.json +34 -0
  4. package/AGENTS.md +55 -0
  5. package/CHANGELOG.md +27 -0
  6. package/LICENSE +21 -0
  7. package/README.md +151 -0
  8. package/SECURITY.md +7 -0
  9. package/THIRD_PARTY_NOTICES.md +29 -0
  10. package/agents/codex/architecture-designer.toml +11 -0
  11. package/agents/codex/backend-reviewer.toml +11 -0
  12. package/agents/codex/code-reader.toml +11 -0
  13. package/agents/codex/frontend-reviewer.toml +11 -0
  14. package/agents/codex/test-verifier.toml +11 -0
  15. package/bin/netpilot-skills.mjs +68 -0
  16. package/docs/agent-authoring.md +64 -0
  17. package/package.json +55 -0
  18. package/scripts/doctor.mjs +81 -0
  19. package/scripts/public-hygiene.mjs +232 -0
  20. package/scripts/sync.mjs +699 -0
  21. package/scripts/validate.mjs +461 -0
  22. package/skills/ask/SKILL.md +67 -0
  23. package/skills/ask/agents/openai.yaml +6 -0
  24. package/skills/code-review/SKILL.md +79 -0
  25. package/skills/code-review/agents/openai.yaml +6 -0
  26. package/skills/codebase-design/SKILL.md +78 -0
  27. package/skills/codebase-design/agents/openai.yaml +6 -0
  28. package/skills/diagnosing-bugs/SKILL.md +82 -0
  29. package/skills/diagnosing-bugs/agents/openai.yaml +6 -0
  30. package/skills/domain-modeling/SKILL.md +85 -0
  31. package/skills/domain-modeling/agents/openai.yaml +6 -0
  32. package/skills/grill/SKILL.md +54 -0
  33. package/skills/grill/agents/openai.yaml +6 -0
  34. package/skills/grill-with-docs/SKILL.md +75 -0
  35. package/skills/grill-with-docs/agents/openai.yaml +6 -0
  36. package/skills/grilling/SKILL.md +66 -0
  37. package/skills/grilling/agents/openai.yaml +6 -0
  38. package/skills/handoff/SKILL.md +72 -0
  39. package/skills/handoff/agents/openai.yaml +6 -0
  40. package/skills/implement/SKILL.md +68 -0
  41. package/skills/implement/agents/openai.yaml +6 -0
  42. package/skills/prototype/SKILL.md +71 -0
  43. package/skills/prototype/agents/openai.yaml +6 -0
  44. package/skills/research/SKILL.md +77 -0
  45. package/skills/research/agents/openai.yaml +6 -0
  46. package/skills/tdd/SKILL.md +71 -0
  47. package/skills/tdd/agents/openai.yaml +6 -0
  48. package/skills/teach/SKILL.md +68 -0
  49. package/skills/teach/agents/openai.yaml +6 -0
  50. package/skills/teach/references/glossary-format.md +21 -0
  51. package/skills/teach/references/learning-record-format.md +18 -0
  52. package/skills/teach/references/mission-format.md +28 -0
  53. package/skills/teach/references/resources-format.md +28 -0
  54. package/skills/to-spec/SKILL.md +76 -0
  55. package/skills/to-spec/agents/openai.yaml +6 -0
  56. package/skills/to-tickets/SKILL.md +69 -0
  57. package/skills/to-tickets/agents/openai.yaml +6 -0
  58. package/skills/wayfinder/SKILL.md +81 -0
  59. package/skills/wayfinder/agents/openai.yaml +6 -0
  60. package/skills/writing-great-skills/SKILL.md +83 -0
  61. package/skills/writing-great-skills/agents/openai.yaml +6 -0
@@ -0,0 +1,81 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+
5
+ import { validateRepository } from "./validate.mjs";
6
+
7
+ function commandVersion(command) {
8
+ const result = spawnSync(`${command} --version`, {
9
+ encoding: "utf8",
10
+ shell: true,
11
+ windowsHide: true,
12
+ timeout: 10_000,
13
+ });
14
+ if (result.error || result.status !== 0) {
15
+ return { available: false, detail: result.error?.message ?? result.stderr?.trim() ?? "命令不可用" };
16
+ }
17
+ return { available: true, detail: (result.stdout || result.stderr).trim().split(/\r?\n/u)[0] };
18
+ }
19
+
20
+ export function isSupportedNodeMajor(nodeMajor) {
21
+ return Number.isInteger(nodeMajor) && nodeMajor >= 22;
22
+ }
23
+
24
+ export async function runDoctor(rootDir) {
25
+ const checks = [];
26
+ const nodeMajor = Number.parseInt(process.versions.node.split(".")[0], 10);
27
+ checks.push({
28
+ name: "Node.js >= 22",
29
+ ok: isSupportedNodeMajor(nodeMajor),
30
+ required: true,
31
+ detail: process.version,
32
+ });
33
+
34
+ const git = commandVersion("git");
35
+ checks.push({ name: "Git", ok: git.available, required: true, detail: git.detail });
36
+
37
+ const codex = commandVersion("codex");
38
+ checks.push({ name: "Codex CLI", ok: codex.available, required: false, detail: codex.detail });
39
+
40
+ const claude = commandVersion("claude");
41
+ checks.push({ name: "Claude Code", ok: claude.available, required: false, detail: claude.detail });
42
+ checks.push({
43
+ name: "至少一个 AI 宿主",
44
+ ok: codex.available || claude.available,
45
+ required: true,
46
+ detail: codex.available || claude.available ? "可用" : "Codex CLI 与 Claude Code 均不可用",
47
+ });
48
+
49
+ const validation = await validateRepository(rootDir);
50
+ checks.push({
51
+ name: "仓库结构",
52
+ ok: validation.ok,
53
+ required: true,
54
+ detail: validation.ok
55
+ ? `${validation.skillCount} 个 skills,${validation.agentCount} 个 Codex agents`
56
+ : `${validation.errors.length} 个问题`,
57
+ });
58
+
59
+ return {
60
+ ok: checks.every((check) => !check.required || check.ok),
61
+ checks,
62
+ validation,
63
+ };
64
+ }
65
+
66
+ async function main() {
67
+ const rootDir = fileURLToPath(new URL("..", import.meta.url));
68
+ const result = await runDoctor(rootDir);
69
+ for (const check of result.checks) {
70
+ const status = check.ok ? "OK" : check.required ? "ERROR" : "WARN";
71
+ console.log(`[${status}] ${check.name}: ${check.detail}`);
72
+ }
73
+ if (!result.ok) {
74
+ for (const error of result.validation.errors) console.error(`- ${error}`);
75
+ process.exitCode = 1;
76
+ }
77
+ }
78
+
79
+ if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
80
+ await main();
81
+ }
@@ -0,0 +1,232 @@
1
+ import { execFile } from "node:child_process";
2
+ import { lstat, readFile, readdir, readlink } from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { promisify } from "node:util";
5
+ import { fileURLToPath } from "node:url";
6
+
7
+ const UTF8_DECODER = new TextDecoder("utf-8", { fatal: true });
8
+ const execFileAsync = promisify(execFile);
9
+ const EXCLUDED_DIRECTORIES = new Set([".git", "node_modules"]);
10
+
11
+ const RULES = [
12
+ {
13
+ id: "windows-absolute-path",
14
+ message: "使用了固定的 Windows 绝对路径",
15
+ pattern: /(?<![A-Za-z0-9])[A-Za-z]:[\\/][^\s"'<>|\r\n]*/gu,
16
+ },
17
+ {
18
+ id: "windows-unc-path",
19
+ message: "使用了固定的 Windows UNC 路径",
20
+ pattern: /(?<![:\\/])(?:\\\\|\/\/)[A-Za-z0-9._-]+[\\/][A-Za-z0-9.$_-]+(?:[\\/][^\s"'<>|]*)?/gu,
21
+ },
22
+ {
23
+ id: "personal-home-path",
24
+ message: "使用了具体用户的 Unix home 路径",
25
+ pattern: /(?<![A-Za-z0-9])(?:\/(?:Users|home)\/[A-Za-z0-9._-]+(?:\/[^\s"'<>]*)?|\/root(?:\/[^\s"'<>]*|(?=$|[\s"'<>])))/gu,
26
+ },
27
+ {
28
+ id: "legacy-project-name",
29
+ message: "包含不应进入公开资产的旧项目标识",
30
+ pattern: /\bai[-_ ]?playbook\b/giu,
31
+ },
32
+ {
33
+ id: "legacy-package-name",
34
+ message: "包含已经停用的 npm 包标识",
35
+ pattern: /@netpilot-z\/skills\b/giu,
36
+ },
37
+ {
38
+ id: "default-branch-git-spec",
39
+ message: "GitHub 安装规格不应硬编码默认分支",
40
+ pattern: /(?:\bgithub:|\b(?:git\+)?https?:\/\/github\.com\/|\bgit(?:\+ssh)?:\/\/(?:git@)?github\.com\/|\bgit@github\.com:)[A-Za-z0-9._-]+\/[A-Za-z0-9._-]+#(?:master|main)\b/giu,
41
+ },
42
+ ];
43
+
44
+ function lineNumberAt(content, index) {
45
+ let line = 1;
46
+ for (let cursor = 0; cursor < index; cursor += 1) {
47
+ if (content.charCodeAt(cursor) === 10) line += 1;
48
+ }
49
+ return line;
50
+ }
51
+
52
+ export function findPublicHygieneViolations(content, filePath = "<content>") {
53
+ const violations = [];
54
+ for (const rule of RULES) {
55
+ rule.pattern.lastIndex = 0;
56
+ for (const match of content.matchAll(rule.pattern)) {
57
+ violations.push({
58
+ filePath,
59
+ line: lineNumberAt(content, match.index),
60
+ ruleId: rule.id,
61
+ message: rule.message,
62
+ value: match[0],
63
+ });
64
+ }
65
+ }
66
+ return violations.sort(
67
+ (left, right) =>
68
+ left.filePath.localeCompare(right.filePath) ||
69
+ left.line - right.line ||
70
+ left.ruleId.localeCompare(right.ruleId),
71
+ );
72
+ }
73
+
74
+ async function collectTextFiles(targetPath, files) {
75
+ let metadata;
76
+ try {
77
+ metadata = await lstat(targetPath);
78
+ } catch (error) {
79
+ if (error.code === "ENOENT") return;
80
+ throw error;
81
+ }
82
+
83
+ if (metadata.isSymbolicLink() || metadata.isFile()) {
84
+ files.add(path.resolve(targetPath));
85
+ return;
86
+ }
87
+ if (!metadata.isDirectory()) return;
88
+
89
+ const entries = await readdir(targetPath, { withFileTypes: true });
90
+ for (const entry of entries.sort((left, right) => left.name.localeCompare(right.name))) {
91
+ if (entry.isDirectory() && EXCLUDED_DIRECTORIES.has(entry.name)) continue;
92
+ await collectTextFiles(path.join(targetPath, entry.name), files);
93
+ }
94
+ }
95
+
96
+ async function collectGitFiles(repositoryRoot, files) {
97
+ let repositoryPrefix;
98
+ try {
99
+ ({ stdout: repositoryPrefix } = await execFileAsync(
100
+ "git",
101
+ ["-C", repositoryRoot, "rev-parse", "--show-prefix"],
102
+ { encoding: "utf8", windowsHide: true },
103
+ ));
104
+ } catch {
105
+ return false;
106
+ }
107
+ if (repositoryPrefix.trim() !== "") {
108
+ return false;
109
+ }
110
+
111
+ const [{ stdout }, { stdout: staged }] = await Promise.all([
112
+ execFileAsync(
113
+ "git",
114
+ ["-C", repositoryRoot, "ls-files", "--cached", "--others", "--exclude-standard", "-z"],
115
+ { encoding: "utf8", maxBuffer: 16 * 1024 * 1024, windowsHide: true },
116
+ ),
117
+ execFileAsync(
118
+ "git",
119
+ ["-C", repositoryRoot, "ls-files", "--cached", "--stage", "-z"],
120
+ { encoding: "utf8", maxBuffer: 16 * 1024 * 1024, windowsHide: true },
121
+ ),
122
+ ]);
123
+ const gitlinks = new Set(
124
+ staged
125
+ .split("\0")
126
+ .flatMap((entry) => {
127
+ const separator = entry.indexOf("\t");
128
+ return entry.startsWith("160000 ") && separator >= 0
129
+ ? [entry.slice(separator + 1).replaceAll("\\", "/")]
130
+ : [];
131
+ }),
132
+ );
133
+ for (const relativePath of stdout.split("\0").filter(Boolean)) {
134
+ const gitPath = relativePath.replaceAll("\\", "/");
135
+ if ([...gitlinks].some((gitlink) => gitPath === gitlink || gitPath.startsWith(`${gitlink}/`))) {
136
+ continue;
137
+ }
138
+ const targetPath = path.resolve(repositoryRoot, relativePath);
139
+ const portableRelative = path.relative(repositoryRoot, targetPath);
140
+ if (
141
+ portableRelative === "" ||
142
+ path.isAbsolute(portableRelative) ||
143
+ portableRelative === ".." ||
144
+ portableRelative.startsWith(`..${path.sep}`)
145
+ ) {
146
+ throw new Error(`Git 文件路径越界:${relativePath}`);
147
+ }
148
+ let metadata;
149
+ try {
150
+ metadata = await lstat(targetPath);
151
+ } catch (error) {
152
+ if (error.code === "ENOENT") continue;
153
+ throw error;
154
+ }
155
+ if (metadata.isSymbolicLink() || metadata.isFile()) files.add(targetPath);
156
+ }
157
+ return true;
158
+ }
159
+
160
+ function decodeUtf16(buffer, littleEndian, offset = 0) {
161
+ const body = Buffer.from(buffer.subarray(offset));
162
+ if (body.length % 2 !== 0) return null;
163
+ if (!littleEndian) body.swap16();
164
+ return body.toString("utf16le");
165
+ }
166
+
167
+ function decodePublicText(buffer) {
168
+ if (buffer.length >= 2 && buffer[0] === 0xff && buffer[1] === 0xfe) {
169
+ return decodeUtf16(buffer, true, 2);
170
+ }
171
+ if (buffer.length >= 2 && buffer[0] === 0xfe && buffer[1] === 0xff) {
172
+ return decodeUtf16(buffer, false, 2);
173
+ }
174
+ if (buffer.includes(0)) {
175
+ const sampleLength = Math.min(buffer.length - (buffer.length % 2), 1024);
176
+ let evenNulls = 0;
177
+ let oddNulls = 0;
178
+ for (let index = 0; index < sampleLength; index += 2) {
179
+ if (buffer[index] === 0) evenNulls += 1;
180
+ if (buffer[index + 1] === 0) oddNulls += 1;
181
+ }
182
+ const pairs = sampleLength / 2;
183
+ if (pairs > 0 && oddNulls / pairs >= 0.3 && evenNulls / pairs <= 0.05) {
184
+ return decodeUtf16(buffer, true);
185
+ }
186
+ if (pairs > 0 && evenNulls / pairs >= 0.3 && oddNulls / pairs <= 0.05) {
187
+ return decodeUtf16(buffer, false);
188
+ }
189
+ return null;
190
+ }
191
+ try {
192
+ return UTF8_DECODER.decode(buffer);
193
+ } catch {
194
+ return null;
195
+ }
196
+ }
197
+
198
+ export async function scanPublicPackage(repositoryRoot) {
199
+ const files = new Set();
200
+ if (!(await collectGitFiles(repositoryRoot, files))) {
201
+ await collectTextFiles(repositoryRoot, files);
202
+ }
203
+
204
+ const violations = [];
205
+ for (const filePath of [...files].sort()) {
206
+ const relativePath = path.relative(repositoryRoot, filePath).replaceAll("\\", "/");
207
+ const metadata = await lstat(filePath);
208
+ const content = metadata.isSymbolicLink()
209
+ ? await readlink(filePath)
210
+ : decodePublicText(await readFile(filePath));
211
+ if (content === null) continue;
212
+ violations.push(...findPublicHygieneViolations(content, relativePath));
213
+ }
214
+ return violations;
215
+ }
216
+
217
+ const modulePath = fileURLToPath(import.meta.url);
218
+ if (process.argv[1] && path.resolve(process.argv[1]) === path.resolve(modulePath)) {
219
+ const repositoryRoot = path.dirname(path.dirname(modulePath));
220
+ const violations = await scanPublicPackage(repositoryRoot);
221
+ if (violations.length > 0) {
222
+ process.stderr.write(`公网卫生校验失败:发现 ${violations.length} 个问题。\n`);
223
+ for (const violation of violations) {
224
+ process.stderr.write(
225
+ `- ${violation.filePath}:${violation.line} [${violation.ruleId}] ${violation.message}:${violation.value}\n`,
226
+ );
227
+ }
228
+ process.exitCode = 1;
229
+ } else {
230
+ process.stdout.write("OK: 公开发布文件不含本机路径、旧项目标识或默认分支硬编码。\n");
231
+ }
232
+ }