@hagicode/skillsbase 0.1.0-dev.20260406025440.0.0.local
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 +78 -0
- package/bin/skillsbase.mjs +44 -0
- package/dist/cli.mjs +969 -0
- package/package.json +49 -0
- package/templates/actions/skillsbase-sync/action.yml +36 -0
- package/templates/docs/maintainer-workflow.md +20 -0
- package/templates/skills/README.md +9 -0
- package/templates/sources.yaml +25 -0
- package/templates/workflows/skills-sync.yml +39 -0
package/README.md
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# skillsbase
|
|
2
|
+
|
|
3
|
+
`skillsbase` 是一个独立 Node CLI,用于初始化和维护“别的 skills 仓库”。
|
|
4
|
+
重点是:本仓库只放 CLI 代码与模板,不提交受管 `skills/` 内容。
|
|
5
|
+
当前源码使用 `TypeScript + Vite 8`。
|
|
6
|
+
|
|
7
|
+
## Commands
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
node ./bin/skillsbase.mjs init
|
|
11
|
+
node ./bin/skillsbase.mjs sync
|
|
12
|
+
node ./bin/skillsbase.mjs sync --check
|
|
13
|
+
node ./bin/skillsbase.mjs add <skill-name>
|
|
14
|
+
node ./bin/skillsbase.mjs github_action --kind all
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
开发期常用:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npm run build
|
|
21
|
+
npm run cli -- --help
|
|
22
|
+
npm test
|
|
23
|
+
npm run smoke
|
|
24
|
+
node ./bin/skillsbase.mjs <command> --repo /path/to/target-repo
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
打包产物仍为 `dist/cli.mjs`,`bin/skillsbase.mjs` 只负责发布态加载与开发态回退。
|
|
28
|
+
|
|
29
|
+
## npm Publish
|
|
30
|
+
|
|
31
|
+
- workflow: `.github/workflows/npm-publish-dev.yml`
|
|
32
|
+
- `push` 到 `main` 时发布 `dev` dist-tag
|
|
33
|
+
- GitHub Release `published` 且非 draft / prerelease 时发布 `latest`
|
|
34
|
+
- 发布前会执行:
|
|
35
|
+
- `npm test`
|
|
36
|
+
- `npm run pack:check`
|
|
37
|
+
- 当前包名:`@hagicode/skillsbase`
|
|
38
|
+
|
|
39
|
+
发布依赖 npm Trusted Publishing;仓库侧需要在 npm 绑定此 GitHub repository。
|
|
40
|
+
|
|
41
|
+
## Managed Repo Contract
|
|
42
|
+
|
|
43
|
+
- `sources.yaml`
|
|
44
|
+
清单单一真相源,声明来源根目录、命名规则、包含列表与非交互默认值。
|
|
45
|
+
- `skills/<name>/SKILL.md`
|
|
46
|
+
受管输出。内容来自当前仓库内临时 `npx skills add` 安装结果,再转换为最终形态。
|
|
47
|
+
- `skills/<name>/.skill-source.json`
|
|
48
|
+
来源、转换、目标路径与安装元数据。
|
|
49
|
+
- `.github/workflows/skills-sync.yml`
|
|
50
|
+
受管 workflow,执行 `npm test` 与 `skillsbase sync --check`。
|
|
51
|
+
- `.github/actions/skillsbase-sync/action.yml`
|
|
52
|
+
可复用 composite action。
|
|
53
|
+
|
|
54
|
+
以上文件属于 **目标 skills 仓库**,不属于本 CLI 仓库本身。
|
|
55
|
+
|
|
56
|
+
## Non-Interactive Defaults
|
|
57
|
+
|
|
58
|
+
- 目标仓库默认为当前工作目录。
|
|
59
|
+
- `init` 默认来源根目录:
|
|
60
|
+
- first-party: `$HOME/.agents/skills`
|
|
61
|
+
- system: `$HOME/.codex/skills/.system`
|
|
62
|
+
- `add` 默认写入第一个已声明 source block。
|
|
63
|
+
- `github_action` 默认 `--kind workflow`。
|
|
64
|
+
- 若上下文不足以安全写入,命令直接失败并给出诊断;不会进入交互提问。
|
|
65
|
+
|
|
66
|
+
## Usage Notes
|
|
67
|
+
|
|
68
|
+
- `sync` 会执行“安装到当前仓库 -> 转换 -> 卸载临时产物 -> 对账写盘”闭环。
|
|
69
|
+
- `sync --check` 不修改最终仓库状态;若发现漂移则返回非零退出码。
|
|
70
|
+
- `sync --allow-missing-sources` 可在来源根目录暂缺时跳过该来源。
|
|
71
|
+
- `github_action` 仅覆盖带 `Managed by skillsbase CLI` 标记的文件;冲突文件需显式 `--force`。
|
|
72
|
+
|
|
73
|
+
## Development
|
|
74
|
+
|
|
75
|
+
- `npm test`
|
|
76
|
+
跑 CLI 单测。
|
|
77
|
+
- `npm run smoke`
|
|
78
|
+
在临时目录创建一个示例 managed repo,验证 `init -> sync -> sync --check -> github_action`。
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { spawn } from "node:child_process";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { access } from "node:fs/promises";
|
|
6
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
7
|
+
|
|
8
|
+
const moduleDir = path.dirname(fileURLToPath(import.meta.url));
|
|
9
|
+
const bundledEntry = path.resolve(moduleDir, "..", "dist", "cli.mjs");
|
|
10
|
+
const sourceEntry = path.resolve(moduleDir, "..", "src", "cli-entry.ts");
|
|
11
|
+
|
|
12
|
+
async function canAccess(targetPath) {
|
|
13
|
+
try {
|
|
14
|
+
await access(targetPath);
|
|
15
|
+
return true;
|
|
16
|
+
} catch {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (await canAccess(bundledEntry)) {
|
|
22
|
+
await import(pathToFileURL(bundledEntry).href);
|
|
23
|
+
} else if (await canAccess(sourceEntry)) {
|
|
24
|
+
const exitCode = await new Promise((resolve, reject) => {
|
|
25
|
+
const child = spawn(process.execPath, ["--import", "tsx", sourceEntry, ...process.argv.slice(2)], {
|
|
26
|
+
stdio: "inherit",
|
|
27
|
+
env: process.env,
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
child.on("error", reject);
|
|
31
|
+
child.on("exit", (code, signal) => {
|
|
32
|
+
if (signal) {
|
|
33
|
+
reject(new Error(`skillsbase CLI terminated with signal ${signal}.`));
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
resolve(code ?? 1);
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
process.exitCode = exitCode;
|
|
42
|
+
} else {
|
|
43
|
+
throw new Error("Unable to locate the skillsbase CLI entrypoint.");
|
|
44
|
+
}
|
package/dist/cli.mjs
ADDED
|
@@ -0,0 +1,969 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { promises } from "node:fs";
|
|
3
|
+
import { execFile } from "node:child_process";
|
|
4
|
+
import { promisify } from "node:util";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import os from "node:os";
|
|
7
|
+
//#region src/lib/output.ts
|
|
8
|
+
var CliError = class extends Error {
|
|
9
|
+
exitCode;
|
|
10
|
+
details;
|
|
11
|
+
nextSteps;
|
|
12
|
+
constructor(message, options = {}) {
|
|
13
|
+
super(message);
|
|
14
|
+
this.name = "CliError";
|
|
15
|
+
this.exitCode = options.exitCode ?? 1;
|
|
16
|
+
this.details = options.details ?? [];
|
|
17
|
+
this.nextSteps = options.nextSteps ?? [];
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
function printCommandUsage(stdout) {
|
|
21
|
+
stdout.write([
|
|
22
|
+
"Usage: skillsbase <command> [options]",
|
|
23
|
+
"",
|
|
24
|
+
"Commands:",
|
|
25
|
+
" init Create the managed repository baseline",
|
|
26
|
+
" sync Reconcile managed skills from sources.yaml",
|
|
27
|
+
" add Add a skill to a source block and sync",
|
|
28
|
+
" github_action Generate managed GitHub Actions assets",
|
|
29
|
+
"",
|
|
30
|
+
"Global Options:",
|
|
31
|
+
" --repo <path> Target repository path (default: current directory)",
|
|
32
|
+
" --help, -h Show help",
|
|
33
|
+
" --version, -v Show version",
|
|
34
|
+
""
|
|
35
|
+
].join("\n"));
|
|
36
|
+
}
|
|
37
|
+
function printCommandResult(result, output) {
|
|
38
|
+
const lines = [
|
|
39
|
+
`## ${result.title ?? result.command}`,
|
|
40
|
+
"",
|
|
41
|
+
`repository: ${result.repository}`,
|
|
42
|
+
`exit_code: ${result.exitCode ?? 0}`
|
|
43
|
+
];
|
|
44
|
+
if (result.schema) lines.push(`schema: ${result.schema}`);
|
|
45
|
+
if (result.items?.length) {
|
|
46
|
+
lines.push("", "items:");
|
|
47
|
+
for (const item of result.items) lines.push(`- ${item}`);
|
|
48
|
+
}
|
|
49
|
+
if (result.nextSteps?.length) {
|
|
50
|
+
lines.push("", "next:");
|
|
51
|
+
for (const nextStep of result.nextSteps) lines.push(`- ${nextStep}`);
|
|
52
|
+
}
|
|
53
|
+
lines.push("");
|
|
54
|
+
output.write(`${lines.join("\n")}\n`);
|
|
55
|
+
}
|
|
56
|
+
//#endregion
|
|
57
|
+
//#region src/lib/constants.ts
|
|
58
|
+
var MANAGED_SIGNATURE = "Managed by skillsbase CLI";
|
|
59
|
+
var DEFAULT_SKILLS_CLI_VERSION = "1.4.8";
|
|
60
|
+
var DEFAULT_INSTALL_AGENT = "codex";
|
|
61
|
+
var DEFAULT_SKILLS_ROOT = "skills";
|
|
62
|
+
var DEFAULT_METADATA_FILE = ".skill-source.json";
|
|
63
|
+
var DEFAULT_MANAGED_BY = "skillsbase";
|
|
64
|
+
var DEFAULT_NODE_VERSION = "22.12.0";
|
|
65
|
+
//#endregion
|
|
66
|
+
//#region src/lib/files.ts
|
|
67
|
+
async function pathExists(targetPath) {
|
|
68
|
+
try {
|
|
69
|
+
await promises.access(targetPath);
|
|
70
|
+
return true;
|
|
71
|
+
} catch {
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
async function readFileIfExists(targetPath, encoding = "utf8") {
|
|
76
|
+
if (!await pathExists(targetPath)) return null;
|
|
77
|
+
return promises.readFile(targetPath, encoding);
|
|
78
|
+
}
|
|
79
|
+
async function ensureDirectory(targetPath) {
|
|
80
|
+
await promises.mkdir(targetPath, { recursive: true });
|
|
81
|
+
}
|
|
82
|
+
async function listDirectories(rootPath) {
|
|
83
|
+
if (!await pathExists(rootPath)) return [];
|
|
84
|
+
return (await promises.readdir(rootPath, { withFileTypes: true })).filter((entry) => entry.isDirectory()).map((entry) => entry.name).sort((left, right) => left.localeCompare(right));
|
|
85
|
+
}
|
|
86
|
+
async function collectRelativeFiles(rootPath, basePath = rootPath) {
|
|
87
|
+
const entries = await promises.readdir(rootPath, { withFileTypes: true });
|
|
88
|
+
const files = [];
|
|
89
|
+
for (const entry of [...entries].sort((left, right) => left.name.localeCompare(right.name))) {
|
|
90
|
+
const absolutePath = path.join(rootPath, entry.name);
|
|
91
|
+
if (entry.isDirectory()) {
|
|
92
|
+
files.push(...await collectRelativeFiles(absolutePath, basePath));
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
if (entry.isFile()) files.push(toPosix(path.relative(basePath, absolutePath)));
|
|
96
|
+
}
|
|
97
|
+
return files;
|
|
98
|
+
}
|
|
99
|
+
async function readTree(rootPath) {
|
|
100
|
+
const files = await collectRelativeFiles(rootPath);
|
|
101
|
+
const tree = /* @__PURE__ */ new Map();
|
|
102
|
+
for (const relativePath of files) tree.set(relativePath, await promises.readFile(path.join(rootPath, relativePath)));
|
|
103
|
+
return tree;
|
|
104
|
+
}
|
|
105
|
+
async function writeTree(rootPath, tree) {
|
|
106
|
+
await promises.rm(rootPath, {
|
|
107
|
+
recursive: true,
|
|
108
|
+
force: true
|
|
109
|
+
});
|
|
110
|
+
await promises.mkdir(rootPath, { recursive: true });
|
|
111
|
+
for (const [relativePath, content] of [...tree.entries()].sort(([left], [right]) => left.localeCompare(right))) {
|
|
112
|
+
const targetPath = path.join(rootPath, relativePath);
|
|
113
|
+
await promises.mkdir(path.dirname(targetPath), { recursive: true });
|
|
114
|
+
await promises.writeFile(targetPath, content);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
async function removeIfEmptyUpward(startPath, stopPath) {
|
|
118
|
+
let currentPath = startPath;
|
|
119
|
+
const normalizedStop = path.resolve(stopPath);
|
|
120
|
+
while (currentPath.startsWith(normalizedStop) && currentPath !== normalizedStop) {
|
|
121
|
+
if (!await pathExists(currentPath)) {
|
|
122
|
+
currentPath = path.dirname(currentPath);
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
if ((await promises.readdir(currentPath)).length > 0) return;
|
|
126
|
+
await promises.rmdir(currentPath);
|
|
127
|
+
currentPath = path.dirname(currentPath);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
function stableJson(value) {
|
|
131
|
+
return `${JSON.stringify(value, null, 2)}\n`;
|
|
132
|
+
}
|
|
133
|
+
function toPosix(filePath) {
|
|
134
|
+
return filePath.split(path.sep).join(path.posix.sep);
|
|
135
|
+
}
|
|
136
|
+
//#endregion
|
|
137
|
+
//#region src/lib/manifest.ts
|
|
138
|
+
function parseScalar(rawValue) {
|
|
139
|
+
const value = rawValue.trim();
|
|
140
|
+
if (value.length === 0) return "";
|
|
141
|
+
if (value === "true") return true;
|
|
142
|
+
if (value === "false") return false;
|
|
143
|
+
if (/^-?\d+$/.test(value)) return Number(value);
|
|
144
|
+
if (value.startsWith("\"") && value.endsWith("\"") || value.startsWith("'") && value.endsWith("'")) return value.slice(1, -1);
|
|
145
|
+
return value;
|
|
146
|
+
}
|
|
147
|
+
function quoteYamlString(value) {
|
|
148
|
+
if (value === "") return "\"\"";
|
|
149
|
+
if (/^[A-Za-z0-9._/@:+-]+$/.test(value)) return value;
|
|
150
|
+
return JSON.stringify(value);
|
|
151
|
+
}
|
|
152
|
+
function validateSource(source) {
|
|
153
|
+
for (const key of [
|
|
154
|
+
"key",
|
|
155
|
+
"label",
|
|
156
|
+
"kind",
|
|
157
|
+
"root",
|
|
158
|
+
"targetPrefix",
|
|
159
|
+
"include"
|
|
160
|
+
]) if (!(key in source)) throw new CliError(`Source "${source.key ?? "<unknown>"}" is missing key "${key}".`, { details: ["Repair `sources.yaml` or rerun `skillsbase init`."] });
|
|
161
|
+
if (!Array.isArray(source.include) || source.include.some((value) => typeof value !== "string")) throw new CliError(`Source "${source.key}" must define an include list.`, { details: ["Use `include: []` only via the CLI serializer; do not change its type."] });
|
|
162
|
+
return {
|
|
163
|
+
key: String(source.key),
|
|
164
|
+
label: String(source.label),
|
|
165
|
+
kind: String(source.kind),
|
|
166
|
+
root: String(source.root),
|
|
167
|
+
targetPrefix: String(source.targetPrefix),
|
|
168
|
+
include: [...source.include]
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
function createManifest(repoPath, options = {}) {
|
|
172
|
+
return {
|
|
173
|
+
version: 1,
|
|
174
|
+
skillsRoot: DEFAULT_SKILLS_ROOT,
|
|
175
|
+
metadataFile: DEFAULT_METADATA_FILE,
|
|
176
|
+
managedBy: DEFAULT_MANAGED_BY,
|
|
177
|
+
remoteRepository: options.remoteRepository ?? path.basename(repoPath),
|
|
178
|
+
staleCleanup: true,
|
|
179
|
+
skillsCliVersion: DEFAULT_SKILLS_CLI_VERSION,
|
|
180
|
+
installAgent: DEFAULT_INSTALL_AGENT,
|
|
181
|
+
sources: options.sources ?? [],
|
|
182
|
+
manifestPath: path.join(repoPath, "sources.yaml"),
|
|
183
|
+
repoPath
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
function serialiseManifest(manifest) {
|
|
187
|
+
const lines = [
|
|
188
|
+
"# Managed by skillsbase CLI.",
|
|
189
|
+
"# Edit source entries to add or remove managed skills.",
|
|
190
|
+
`version: ${manifest.version}`,
|
|
191
|
+
`skillsRoot: ${quoteYamlString(manifest.skillsRoot)}`,
|
|
192
|
+
`metadataFile: ${quoteYamlString(manifest.metadataFile)}`,
|
|
193
|
+
`managedBy: ${quoteYamlString(manifest.managedBy)}`,
|
|
194
|
+
`remoteRepository: ${quoteYamlString(manifest.remoteRepository)}`,
|
|
195
|
+
`staleCleanup: ${manifest.staleCleanup ? "true" : "false"}`,
|
|
196
|
+
`skillsCliVersion: ${quoteYamlString(manifest.skillsCliVersion ?? "1.4.8")}`,
|
|
197
|
+
`installAgent: ${quoteYamlString(manifest.installAgent ?? "codex")}`,
|
|
198
|
+
"sources:"
|
|
199
|
+
];
|
|
200
|
+
for (const source of manifest.sources) {
|
|
201
|
+
lines.push(` - key: ${quoteYamlString(source.key)}`);
|
|
202
|
+
lines.push(` label: ${quoteYamlString(source.label)}`);
|
|
203
|
+
lines.push(` kind: ${quoteYamlString(source.kind)}`);
|
|
204
|
+
lines.push(` root: ${quoteYamlString(source.root)}`);
|
|
205
|
+
lines.push(` targetPrefix: ${quoteYamlString(source.targetPrefix ?? "")}`);
|
|
206
|
+
lines.push(" include:");
|
|
207
|
+
for (const skillName of source.include ?? []) lines.push(` - ${quoteYamlString(skillName)}`);
|
|
208
|
+
}
|
|
209
|
+
return `${lines.join("\n")}\n`;
|
|
210
|
+
}
|
|
211
|
+
function validateManifest(manifest) {
|
|
212
|
+
for (const key of [
|
|
213
|
+
"version",
|
|
214
|
+
"skillsRoot",
|
|
215
|
+
"metadataFile",
|
|
216
|
+
"managedBy",
|
|
217
|
+
"remoteRepository",
|
|
218
|
+
"staleCleanup",
|
|
219
|
+
"sources"
|
|
220
|
+
]) if (!(key in manifest)) throw new CliError(`Missing required manifest key: ${key}`, { details: ["Run `skillsbase init` to recreate the baseline contract."] });
|
|
221
|
+
if (!Array.isArray(manifest.sources) || typeof manifest.version !== "number" || typeof manifest.skillsRoot !== "string" || typeof manifest.metadataFile !== "string" || typeof manifest.managedBy !== "string" || typeof manifest.remoteRepository !== "string" || typeof manifest.staleCleanup !== "boolean") throw new CliError("Manifest `sources` must be a list.", { details: ["Repair `sources.yaml` and try again."] });
|
|
222
|
+
return {
|
|
223
|
+
version: manifest.version,
|
|
224
|
+
skillsRoot: manifest.skillsRoot,
|
|
225
|
+
metadataFile: manifest.metadataFile,
|
|
226
|
+
managedBy: manifest.managedBy,
|
|
227
|
+
remoteRepository: manifest.remoteRepository,
|
|
228
|
+
staleCleanup: manifest.staleCleanup,
|
|
229
|
+
skillsCliVersion: typeof manifest.skillsCliVersion === "string" ? manifest.skillsCliVersion : DEFAULT_SKILLS_CLI_VERSION,
|
|
230
|
+
installAgent: typeof manifest.installAgent === "string" ? manifest.installAgent : DEFAULT_INSTALL_AGENT,
|
|
231
|
+
sources: manifest.sources.map(validateSource),
|
|
232
|
+
manifestPath: typeof manifest.manifestPath === "string" ? manifest.manifestPath : "",
|
|
233
|
+
repoPath: typeof manifest.repoPath === "string" ? manifest.repoPath : "",
|
|
234
|
+
skillsRootPath: typeof manifest.skillsRootPath === "string" ? manifest.skillsRootPath : void 0
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
async function loadManifest(repoPath) {
|
|
238
|
+
const manifestPath = path.join(repoPath, "sources.yaml");
|
|
239
|
+
if (!await pathExists(manifestPath)) throw new CliError("Missing `sources.yaml`.", { details: [`Repository: ${repoPath}`, "Run `skillsbase init` first, then retry the command."] });
|
|
240
|
+
const text = await promises.readFile(manifestPath, "utf8");
|
|
241
|
+
const manifest = { sources: [] };
|
|
242
|
+
const lines = text.split(/\r?\n/);
|
|
243
|
+
let currentSource = null;
|
|
244
|
+
let currentListKey = null;
|
|
245
|
+
for (const [index, rawLine] of lines.entries()) {
|
|
246
|
+
const line = rawLine.replace(/\s+$/, "");
|
|
247
|
+
const lineNumber = index + 1;
|
|
248
|
+
if (line.length === 0 || line.trimStart().startsWith("#")) continue;
|
|
249
|
+
const topLevelMatch = /^([A-Za-z][A-Za-z0-9]*):\s*(.*)$/.exec(line);
|
|
250
|
+
if (topLevelMatch && !line.startsWith(" ")) {
|
|
251
|
+
const [, key, value] = topLevelMatch;
|
|
252
|
+
if (key === "sources") {
|
|
253
|
+
currentSource = null;
|
|
254
|
+
currentListKey = null;
|
|
255
|
+
} else manifest[key] = parseScalar(value);
|
|
256
|
+
continue;
|
|
257
|
+
}
|
|
258
|
+
const sourceStartMatch = /^ - key:\s*(.+)$/.exec(line);
|
|
259
|
+
if (sourceStartMatch) {
|
|
260
|
+
currentSource = {
|
|
261
|
+
key: parseScalar(sourceStartMatch[1]),
|
|
262
|
+
include: []
|
|
263
|
+
};
|
|
264
|
+
manifest.sources.push(currentSource);
|
|
265
|
+
currentListKey = null;
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
const sourcePropertyMatch = /^ ([A-Za-z][A-Za-z0-9]*):\s*(.*)$/.exec(line);
|
|
269
|
+
if (sourcePropertyMatch && currentSource) {
|
|
270
|
+
const [, key, value] = sourcePropertyMatch;
|
|
271
|
+
if (value.length === 0) {
|
|
272
|
+
currentSource[key] = [];
|
|
273
|
+
currentListKey = key;
|
|
274
|
+
} else {
|
|
275
|
+
currentSource[key] = parseScalar(value);
|
|
276
|
+
currentListKey = null;
|
|
277
|
+
}
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
const listItemMatch = /^ - (.+)$/.exec(line);
|
|
281
|
+
if (listItemMatch && currentSource && currentListKey) {
|
|
282
|
+
const listValue = currentSource[currentListKey];
|
|
283
|
+
if (!Array.isArray(listValue)) throw new CliError(`Invalid list state for "${currentListKey}" at line ${lineNumber}.`);
|
|
284
|
+
listValue.push(parseScalar(listItemMatch[1]));
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
throw new CliError(`Unsupported sources.yaml syntax at line ${lineNumber}.`, { details: [rawLine] });
|
|
288
|
+
}
|
|
289
|
+
const validated = validateManifest(manifest);
|
|
290
|
+
return {
|
|
291
|
+
...validated,
|
|
292
|
+
manifestPath,
|
|
293
|
+
repoPath,
|
|
294
|
+
skillsRootPath: path.join(repoPath, validated.skillsRoot)
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
async function saveManifest(manifest) {
|
|
298
|
+
const nextText = serialiseManifest(manifest);
|
|
299
|
+
await promises.writeFile(manifest.manifestPath, nextText, "utf8");
|
|
300
|
+
}
|
|
301
|
+
function buildManifestEntries(manifest, repoPath = manifest.repoPath) {
|
|
302
|
+
const entries = [];
|
|
303
|
+
for (const source of manifest.sources) for (const originalName of source.include ?? []) {
|
|
304
|
+
const targetName = `${source.targetPrefix ?? ""}${originalName}`;
|
|
305
|
+
entries.push({
|
|
306
|
+
sourceKey: source.key,
|
|
307
|
+
sourceLabel: source.label,
|
|
308
|
+
sourceKind: source.kind,
|
|
309
|
+
sourceRoot: source.root,
|
|
310
|
+
sourcePath: path.join(source.root, originalName),
|
|
311
|
+
originalName,
|
|
312
|
+
targetName,
|
|
313
|
+
targetPath: path.join(repoPath, manifest.skillsRoot, targetName),
|
|
314
|
+
targetPathRelative: toPosix(path.join(manifest.skillsRoot, targetName))
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
const collisions = /* @__PURE__ */ new Map();
|
|
318
|
+
for (const entry of entries) {
|
|
319
|
+
const keys = collisions.get(entry.targetName) ?? [];
|
|
320
|
+
keys.push(entry.sourceKey);
|
|
321
|
+
collisions.set(entry.targetName, keys);
|
|
322
|
+
}
|
|
323
|
+
const duplicateTargets = [...collisions.entries()].filter(([, keys]) => keys.length > 1);
|
|
324
|
+
if (duplicateTargets.length > 0) throw new CliError(`Manifest target-name collision detected: ${duplicateTargets.map(([targetName, keys]) => `${targetName} (${keys.join(", ")})`).join(", ")}`, { details: ["Adjust `targetPrefix` or `include` entries in `sources.yaml`."] });
|
|
325
|
+
return entries.sort((left, right) => left.targetName.localeCompare(right.targetName));
|
|
326
|
+
}
|
|
327
|
+
function addSkillToManifest(manifest, skillName, options = {}) {
|
|
328
|
+
if (manifest.sources.length === 0) throw new CliError("Manifest does not declare any source blocks.", { details: ["Run `skillsbase init` first or add a source block to `sources.yaml`."] });
|
|
329
|
+
const selectedSource = options.sourceKey == null ? manifest.sources[0] : manifest.sources.find((source) => source.key === options.sourceKey);
|
|
330
|
+
if (!selectedSource) throw new CliError(`Unknown source key: ${options.sourceKey}`, { details: [`Declared sources: ${manifest.sources.map((source) => source.key).join(", ")}`] });
|
|
331
|
+
const include = new Set(selectedSource.include ?? []);
|
|
332
|
+
include.add(skillName);
|
|
333
|
+
selectedSource.include = [...include].sort((left, right) => left.localeCompare(right));
|
|
334
|
+
return {
|
|
335
|
+
...manifest,
|
|
336
|
+
sources: manifest.sources.map((source) => source.key === selectedSource.key ? { ...selectedSource } : {
|
|
337
|
+
...source,
|
|
338
|
+
include: [...source.include]
|
|
339
|
+
})
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
function buildMetadata(manifest, entry, installRecord) {
|
|
343
|
+
return {
|
|
344
|
+
schemaVersion: 1,
|
|
345
|
+
managed: true,
|
|
346
|
+
managedBy: manifest.managedBy,
|
|
347
|
+
sourceKey: entry.sourceKey,
|
|
348
|
+
sourceKind: entry.sourceKind,
|
|
349
|
+
sourceLabel: entry.sourceLabel,
|
|
350
|
+
sourceRoot: entry.sourceRoot,
|
|
351
|
+
sourcePath: entry.sourcePath,
|
|
352
|
+
originalName: entry.originalName,
|
|
353
|
+
targetName: entry.targetName,
|
|
354
|
+
targetPath: entry.targetPathRelative,
|
|
355
|
+
remoteRepository: manifest.remoteRepository,
|
|
356
|
+
installAgent: manifest.installAgent,
|
|
357
|
+
installReference: installRecord.installReference,
|
|
358
|
+
installedMetadata: installRecord.installedMetadata,
|
|
359
|
+
files: [...installRecord.files].sort((left, right) => left.localeCompare(right))
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
//#endregion
|
|
363
|
+
//#region src/lib/installer.ts
|
|
364
|
+
var execFile$1 = promisify(execFile);
|
|
365
|
+
function buildNpxArgs(manifest, subcommand, extraArgs) {
|
|
366
|
+
return [
|
|
367
|
+
"--yes",
|
|
368
|
+
`skills@${manifest.skillsCliVersion}`,
|
|
369
|
+
subcommand,
|
|
370
|
+
...extraArgs
|
|
371
|
+
];
|
|
372
|
+
}
|
|
373
|
+
function renderExecFailure(error) {
|
|
374
|
+
if (error instanceof Error) {
|
|
375
|
+
const execError = error;
|
|
376
|
+
return execError.stderr ?? execError.stdout ?? execError.message;
|
|
377
|
+
}
|
|
378
|
+
return String(error);
|
|
379
|
+
}
|
|
380
|
+
async function installIntoCurrentRepository(repoPath, manifest, entry, options = {}) {
|
|
381
|
+
const installPath = path.join(repoPath, ".agents", "skills", entry.originalName);
|
|
382
|
+
const lockPath = path.join(repoPath, "skills-lock.json");
|
|
383
|
+
const snapshot = {
|
|
384
|
+
installPath,
|
|
385
|
+
lockPath,
|
|
386
|
+
installTree: await snapshotTree(installPath),
|
|
387
|
+
lockText: await readFileIfExists(lockPath),
|
|
388
|
+
installReference: entry.sourcePath
|
|
389
|
+
};
|
|
390
|
+
try {
|
|
391
|
+
await execFile$1("npx", buildNpxArgs(manifest, "add", [
|
|
392
|
+
entry.sourcePath,
|
|
393
|
+
"--agent",
|
|
394
|
+
manifest.installAgent,
|
|
395
|
+
"--copy",
|
|
396
|
+
"-y"
|
|
397
|
+
]), {
|
|
398
|
+
cwd: repoPath,
|
|
399
|
+
env: options.env,
|
|
400
|
+
maxBuffer: 16 * 1024 * 1024,
|
|
401
|
+
timeout: 6e4
|
|
402
|
+
});
|
|
403
|
+
} catch (error) {
|
|
404
|
+
throw new CliError(`skills install failed for ${entry.originalName}.`, { details: [renderExecFailure(error)] });
|
|
405
|
+
}
|
|
406
|
+
if (!await pathExists(installPath)) throw new CliError(`skills install did not create ${path.relative(repoPath, installPath)}.`, { details: ["The `npx skills` install output was not in the expected current-repository shape."] });
|
|
407
|
+
return {
|
|
408
|
+
installPath,
|
|
409
|
+
lockPath,
|
|
410
|
+
installReference: entry.sourcePath,
|
|
411
|
+
snapshot
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
async function cleanupInstalledSkill(repoPath, manifest, entry, installState, options = {}) {
|
|
415
|
+
let removeError = null;
|
|
416
|
+
try {
|
|
417
|
+
await execFile$1("npx", buildNpxArgs(manifest, "remove", [entry.originalName, "-y"]), {
|
|
418
|
+
cwd: repoPath,
|
|
419
|
+
env: options.env,
|
|
420
|
+
maxBuffer: 16 * 1024 * 1024,
|
|
421
|
+
timeout: 6e4
|
|
422
|
+
});
|
|
423
|
+
} catch (error) {
|
|
424
|
+
removeError = error;
|
|
425
|
+
}
|
|
426
|
+
try {
|
|
427
|
+
await restoreSnapshot(installState.snapshot);
|
|
428
|
+
await removeIfEmptyUpward(path.join(repoPath, ".agents", "skills"), repoPath);
|
|
429
|
+
} catch (restoreError) {
|
|
430
|
+
const restoreMessage = restoreError instanceof Error ? restoreError.message : String(restoreError);
|
|
431
|
+
throw new CliError(`Cleanup failed for ${entry.originalName}.`, { details: [restoreMessage, removeError ? renderExecFailure(removeError) : null].filter((value) => Boolean(value)) });
|
|
432
|
+
}
|
|
433
|
+
if (removeError) throw new CliError(`skills uninstall failed for ${entry.originalName}.`, { details: [renderExecFailure(removeError)] });
|
|
434
|
+
}
|
|
435
|
+
async function snapshotTree(rootPath) {
|
|
436
|
+
if (!await pathExists(rootPath)) return null;
|
|
437
|
+
const entries = await promises.readdir(rootPath, { withFileTypes: true });
|
|
438
|
+
const tree = /* @__PURE__ */ new Map();
|
|
439
|
+
for (const entry of entries) {
|
|
440
|
+
const absolutePath = path.join(rootPath, entry.name);
|
|
441
|
+
if (entry.isDirectory()) {
|
|
442
|
+
const nested = await snapshotTree(absolutePath);
|
|
443
|
+
if (!nested) continue;
|
|
444
|
+
for (const [relativePath, content] of nested.entries()) tree.set(path.join(entry.name, relativePath), content);
|
|
445
|
+
continue;
|
|
446
|
+
}
|
|
447
|
+
if (entry.isFile()) tree.set(entry.name, await promises.readFile(absolutePath));
|
|
448
|
+
}
|
|
449
|
+
return tree;
|
|
450
|
+
}
|
|
451
|
+
async function restoreSnapshot(snapshot) {
|
|
452
|
+
if (snapshot.installTree == null) await promises.rm(snapshot.installPath, {
|
|
453
|
+
recursive: true,
|
|
454
|
+
force: true
|
|
455
|
+
});
|
|
456
|
+
else {
|
|
457
|
+
await promises.rm(snapshot.installPath, {
|
|
458
|
+
recursive: true,
|
|
459
|
+
force: true
|
|
460
|
+
});
|
|
461
|
+
await promises.mkdir(snapshot.installPath, { recursive: true });
|
|
462
|
+
for (const [relativePath, content] of snapshot.installTree.entries()) {
|
|
463
|
+
const targetPath = path.join(snapshot.installPath, relativePath);
|
|
464
|
+
await promises.mkdir(path.dirname(targetPath), { recursive: true });
|
|
465
|
+
await promises.writeFile(targetPath, content);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
if (snapshot.lockText == null) await promises.rm(snapshot.lockPath, { force: true });
|
|
469
|
+
else await promises.writeFile(snapshot.lockPath, snapshot.lockText, "utf8");
|
|
470
|
+
await removeIfEmptyUpward(path.dirname(snapshot.installPath), path.dirname(snapshot.lockPath));
|
|
471
|
+
}
|
|
472
|
+
//#endregion
|
|
473
|
+
//#region src/lib/skill-converter.ts
|
|
474
|
+
function rewriteSkillName(content, targetName) {
|
|
475
|
+
const frontmatterMatch = /^---\n([\s\S]*?)\n---/.exec(content);
|
|
476
|
+
if (!frontmatterMatch) throw new CliError("Installed SKILL.md is missing YAML frontmatter.", { details: ["The upstream skill must contain a valid `name` field."] });
|
|
477
|
+
if (!/^name:\s*.+$/m.test(frontmatterMatch[1])) throw new CliError("Installed SKILL.md frontmatter is missing a `name` field.");
|
|
478
|
+
const updatedFrontmatter = frontmatterMatch[1].replace(/^name:\s*.+$/m, `name: ${targetName}`);
|
|
479
|
+
return content.replace(frontmatterMatch[0], `---\n${updatedFrontmatter}\n---`);
|
|
480
|
+
}
|
|
481
|
+
async function convertInstalledSkill(_manifest, entry, installState) {
|
|
482
|
+
const installedPath = installState.installPath;
|
|
483
|
+
const skillPath = path.join(installedPath, "SKILL.md");
|
|
484
|
+
try {
|
|
485
|
+
await promises.access(skillPath);
|
|
486
|
+
} catch {
|
|
487
|
+
throw new CliError(`Installed skill is missing SKILL.md: ${entry.originalName}`);
|
|
488
|
+
}
|
|
489
|
+
const filePaths = await collectRelativeFiles(installedPath);
|
|
490
|
+
const outputTree = /* @__PURE__ */ new Map();
|
|
491
|
+
for (const relativePath of filePaths) {
|
|
492
|
+
if (relativePath === "hagicode-skill.json" || relativePath === ".skill-source.json") continue;
|
|
493
|
+
const absolutePath = path.join(installedPath, relativePath);
|
|
494
|
+
if (relativePath === "SKILL.md") {
|
|
495
|
+
const content = await promises.readFile(absolutePath, "utf8");
|
|
496
|
+
outputTree.set(relativePath, Buffer.from(rewriteSkillName(content, entry.targetName), "utf8"));
|
|
497
|
+
continue;
|
|
498
|
+
}
|
|
499
|
+
outputTree.set(relativePath, await promises.readFile(absolutePath));
|
|
500
|
+
}
|
|
501
|
+
const installedMetadataPath = path.join(installedPath, "hagicode-skill.json");
|
|
502
|
+
let installedMetadata;
|
|
503
|
+
try {
|
|
504
|
+
installedMetadata = JSON.parse(await promises.readFile(installedMetadataPath, "utf8"));
|
|
505
|
+
} catch {
|
|
506
|
+
installedMetadata = {
|
|
507
|
+
schemaVersion: 1,
|
|
508
|
+
source: entry.sourceRoot,
|
|
509
|
+
skillSlug: entry.originalName,
|
|
510
|
+
installReference: installState.installReference,
|
|
511
|
+
synthesizedBy: "skillsbase"
|
|
512
|
+
};
|
|
513
|
+
}
|
|
514
|
+
return {
|
|
515
|
+
files: [...outputTree.keys()].sort((left, right) => left.localeCompare(right)),
|
|
516
|
+
outputTree,
|
|
517
|
+
installedMetadata,
|
|
518
|
+
installReference: installState.installReference,
|
|
519
|
+
targetName: entry.targetName,
|
|
520
|
+
targetPath: entry.targetPath,
|
|
521
|
+
targetPathRelative: entry.targetPathRelative
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
//#endregion
|
|
525
|
+
//#region src/lib/sync-engine.ts
|
|
526
|
+
async function assertSourceState(entry, allowMissingSources) {
|
|
527
|
+
if (!await pathExists(entry.sourceRoot)) {
|
|
528
|
+
if (allowMissingSources) return {
|
|
529
|
+
skip: true,
|
|
530
|
+
reason: `missing source root: ${entry.sourceRoot}`
|
|
531
|
+
};
|
|
532
|
+
throw new CliError(`Managed source root does not exist: ${entry.sourceRoot}`, { details: ["Use `skillsbase sync --allow-missing-sources` to skip missing roots."] });
|
|
533
|
+
}
|
|
534
|
+
if (!await pathExists(entry.sourcePath)) throw new CliError(`Managed skill is missing from source root: ${entry.sourcePath}`, { details: [`source: ${entry.sourceKey}`, `skill: ${entry.originalName}`] });
|
|
535
|
+
return { skip: false };
|
|
536
|
+
}
|
|
537
|
+
async function assertManagedTargetWritable(manifest, entry) {
|
|
538
|
+
if (!await pathExists(entry.targetPath)) return;
|
|
539
|
+
const metadataPath = path.join(entry.targetPath, manifest.metadataFile);
|
|
540
|
+
if (!await pathExists(metadataPath)) throw new CliError(`Refusing to overwrite unmanaged directory: ${entry.targetPathRelative}`, { details: ["Add metadata manually or remove the conflicting directory first."] });
|
|
541
|
+
const metadata = JSON.parse(await promises.readFile(metadataPath, "utf8"));
|
|
542
|
+
if (!metadata.managed || metadata.managedBy !== manifest.managedBy) throw new CliError(`Refusing to overwrite unmanaged directory: ${entry.targetPathRelative}`, { details: [`Found metadata managedBy=${JSON.stringify(metadata.managedBy)}`] });
|
|
543
|
+
}
|
|
544
|
+
async function snapshotTargetDirectory(targetPath) {
|
|
545
|
+
if (!await pathExists(targetPath)) return null;
|
|
546
|
+
return {
|
|
547
|
+
path: targetPath,
|
|
548
|
+
tree: await readTree(targetPath)
|
|
549
|
+
};
|
|
550
|
+
}
|
|
551
|
+
async function restoreTargetDirectory(snapshot) {
|
|
552
|
+
if (snapshot == null) return;
|
|
553
|
+
await writeTree(snapshot.path, snapshot.tree);
|
|
554
|
+
}
|
|
555
|
+
async function compareTarget(manifest, entry, desiredTree, desiredMetadata) {
|
|
556
|
+
if (!await pathExists(entry.targetPath)) return [`missing target directory: ${entry.targetPathRelative}`];
|
|
557
|
+
const actualFiles = (await collectRelativeFiles(entry.targetPath)).sort((left, right) => left.localeCompare(right));
|
|
558
|
+
const desiredFiles = [...desiredTree.keys(), manifest.metadataFile].sort((left, right) => left.localeCompare(right));
|
|
559
|
+
if (JSON.stringify(actualFiles) !== JSON.stringify(desiredFiles)) return [`file set drift: ${entry.targetPathRelative}`];
|
|
560
|
+
const actualTree = await readTree(entry.targetPath);
|
|
561
|
+
for (const [relativePath, buffer] of desiredTree.entries()) {
|
|
562
|
+
const actual = actualTree.get(relativePath);
|
|
563
|
+
if (!actual || !actual.equals(buffer)) return [`file content drift: ${entry.targetPathRelative}/${relativePath}`];
|
|
564
|
+
}
|
|
565
|
+
const metadataPath = path.join(entry.targetPath, manifest.metadataFile);
|
|
566
|
+
if (await promises.readFile(metadataPath, "utf8") !== stableJson(desiredMetadata)) return [`metadata drift: ${entry.targetPathRelative}/${manifest.metadataFile}`];
|
|
567
|
+
return [];
|
|
568
|
+
}
|
|
569
|
+
async function reconcileStaleTargets(repoPath, manifest, declaredTargets, check) {
|
|
570
|
+
const changes = [];
|
|
571
|
+
const skillsRootPath = path.join(repoPath, manifest.skillsRoot);
|
|
572
|
+
const existingDirectories = await listDirectories(skillsRootPath);
|
|
573
|
+
for (const directoryName of existingDirectories) {
|
|
574
|
+
if (declaredTargets.has(directoryName)) continue;
|
|
575
|
+
const candidatePath = path.join(skillsRootPath, directoryName);
|
|
576
|
+
const metadataPath = path.join(candidatePath, manifest.metadataFile);
|
|
577
|
+
if (!await pathExists(metadataPath)) continue;
|
|
578
|
+
const metadata = JSON.parse(await promises.readFile(metadataPath, "utf8"));
|
|
579
|
+
if (!metadata.managed || metadata.managedBy !== manifest.managedBy) continue;
|
|
580
|
+
if (check) {
|
|
581
|
+
changes.push(`stale managed directory: ${path.posix.join(manifest.skillsRoot, directoryName)}`);
|
|
582
|
+
continue;
|
|
583
|
+
}
|
|
584
|
+
await promises.rm(candidatePath, {
|
|
585
|
+
recursive: true,
|
|
586
|
+
force: true
|
|
587
|
+
});
|
|
588
|
+
changes.push(`removed stale: ${path.posix.join(manifest.skillsRoot, directoryName)}`);
|
|
589
|
+
}
|
|
590
|
+
return changes;
|
|
591
|
+
}
|
|
592
|
+
async function executeSync(options) {
|
|
593
|
+
const { repoPath, manifest, check, allowMissingSources, env } = options;
|
|
594
|
+
const entries = buildManifestEntries(manifest, repoPath);
|
|
595
|
+
const items = [];
|
|
596
|
+
const skipped = [];
|
|
597
|
+
const declaredTargets = new Set(entries.map((entry) => entry.targetName));
|
|
598
|
+
const preparedEntries = [];
|
|
599
|
+
const checkSnapshots = /* @__PURE__ */ new Map();
|
|
600
|
+
await ensureDirectory(path.join(repoPath, manifest.skillsRoot));
|
|
601
|
+
if (check) for (const entry of entries) checkSnapshots.set(entry.targetPath, await snapshotTargetDirectory(entry.targetPath));
|
|
602
|
+
for (const entry of entries) {
|
|
603
|
+
if ((await assertSourceState(entry, allowMissingSources)).skip) {
|
|
604
|
+
skipped.push(`${entry.sourceKey}: ${entry.originalName}`);
|
|
605
|
+
continue;
|
|
606
|
+
}
|
|
607
|
+
const installState = await installIntoCurrentRepository(repoPath, manifest, entry, { env });
|
|
608
|
+
try {
|
|
609
|
+
const converted = await convertInstalledSkill(manifest, entry, installState);
|
|
610
|
+
const metadata = buildMetadata(manifest, entry, {
|
|
611
|
+
installReference: converted.installReference,
|
|
612
|
+
installedMetadata: converted.installedMetadata,
|
|
613
|
+
files: converted.files
|
|
614
|
+
});
|
|
615
|
+
preparedEntries.push({
|
|
616
|
+
entry,
|
|
617
|
+
converted,
|
|
618
|
+
metadata
|
|
619
|
+
});
|
|
620
|
+
} finally {
|
|
621
|
+
await cleanupInstalledSkill(repoPath, manifest, entry, installState, { env });
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
if (check) for (const prepared of preparedEntries) {
|
|
625
|
+
await restoreTargetDirectory(checkSnapshots.get(prepared.entry.targetPath) ?? null);
|
|
626
|
+
items.push(...await compareTarget(manifest, prepared.entry, prepared.converted.outputTree, prepared.metadata));
|
|
627
|
+
}
|
|
628
|
+
else for (const prepared of preparedEntries) {
|
|
629
|
+
await assertManagedTargetWritable(manifest, prepared.entry);
|
|
630
|
+
const nextTree = new Map(prepared.converted.outputTree);
|
|
631
|
+
nextTree.set(manifest.metadataFile, Buffer.from(stableJson(prepared.metadata), "utf8"));
|
|
632
|
+
await writeTree(prepared.entry.targetPath, nextTree);
|
|
633
|
+
items.push(`synced: ${prepared.entry.targetPathRelative}`);
|
|
634
|
+
}
|
|
635
|
+
items.push(...await reconcileStaleTargets(repoPath, manifest, declaredTargets, check));
|
|
636
|
+
if (skipped.length > 0) items.push(`skipped missing sources: ${skipped.join(", ")}`);
|
|
637
|
+
const driftDetected = check && items.some((item) => !item.startsWith("skipped "));
|
|
638
|
+
return {
|
|
639
|
+
command: "sync",
|
|
640
|
+
title: check ? "skillsbase sync --check" : "skillsbase sync",
|
|
641
|
+
repository: repoPath,
|
|
642
|
+
exitCode: driftDetected ? 1 : 0,
|
|
643
|
+
schema: "spec-driven",
|
|
644
|
+
items: items.length > 0 ? items : [check ? "no drift detected" : "nothing to sync"],
|
|
645
|
+
nextSteps: driftDetected ? ["skillsbase sync"] : []
|
|
646
|
+
};
|
|
647
|
+
}
|
|
648
|
+
//#endregion
|
|
649
|
+
//#region src/commands/add.ts
|
|
650
|
+
async function runAddCommand(context) {
|
|
651
|
+
const repoFlag = typeof context.flags.repo === "string" ? context.flags.repo : void 0;
|
|
652
|
+
const sourceFlag = typeof context.flags.source === "string" ? context.flags.source : void 0;
|
|
653
|
+
const repoPath = path.resolve(repoFlag ?? context.cwd);
|
|
654
|
+
const skillName = context.args[0];
|
|
655
|
+
if (!skillName) throw new CliError("`skillsbase add` requires a skill name.", { details: ["Usage: `skillsbase add <skill-name> [--source <key>]`."] });
|
|
656
|
+
const nextManifest = addSkillToManifest(await loadManifest(repoPath), skillName, { sourceKey: sourceFlag });
|
|
657
|
+
await saveManifest(nextManifest);
|
|
658
|
+
const result = await executeSync({
|
|
659
|
+
repoPath,
|
|
660
|
+
manifest: nextManifest,
|
|
661
|
+
env: context.env,
|
|
662
|
+
check: false,
|
|
663
|
+
allowMissingSources: context.flags["allow-missing-sources"] === true
|
|
664
|
+
});
|
|
665
|
+
return {
|
|
666
|
+
...result,
|
|
667
|
+
title: `skillsbase add ${skillName}`,
|
|
668
|
+
items: [`manifest updated: ${path.relative(repoPath, nextManifest.manifestPath) || "sources.yaml"}`, ...result.items]
|
|
669
|
+
};
|
|
670
|
+
}
|
|
671
|
+
//#endregion
|
|
672
|
+
//#region src/lib/templates.ts
|
|
673
|
+
var moduleDir = path.dirname(fileURLToPath(import.meta.url));
|
|
674
|
+
var templateRootCandidates = [path.resolve(moduleDir, "..", "..", "templates"), path.resolve(moduleDir, "..", "templates")];
|
|
675
|
+
async function resolveTemplateRoot() {
|
|
676
|
+
for (const candidate of templateRootCandidates) if (await pathExists(candidate)) return candidate;
|
|
677
|
+
throw new CliError("Unable to locate the bundled templates directory.");
|
|
678
|
+
}
|
|
679
|
+
async function renderTemplate(relativePath, variables) {
|
|
680
|
+
const templateRoot = await resolveTemplateRoot();
|
|
681
|
+
const templatePath = path.join(templateRoot, relativePath);
|
|
682
|
+
let content = await promises.readFile(templatePath, "utf8");
|
|
683
|
+
for (const [key, value] of Object.entries(variables)) content = content.replaceAll(`{{${key}}}`, value);
|
|
684
|
+
return content;
|
|
685
|
+
}
|
|
686
|
+
function managedMarkerFor(targetPath) {
|
|
687
|
+
if (targetPath.endsWith(".md")) return `<!-- ${MANAGED_SIGNATURE}. -->`;
|
|
688
|
+
return `# ${MANAGED_SIGNATURE}.`;
|
|
689
|
+
}
|
|
690
|
+
function isManagedContent(targetPath, content) {
|
|
691
|
+
return content.includes(managedMarkerFor(targetPath));
|
|
692
|
+
}
|
|
693
|
+
async function writeManagedFile(targetPath, content, options = {}) {
|
|
694
|
+
const marker = managedMarkerFor(targetPath);
|
|
695
|
+
if (!content.includes(marker)) throw new CliError(`Managed template is missing its marker: ${targetPath}`);
|
|
696
|
+
const current = await pathExists(targetPath) ? await promises.readFile(targetPath, "utf8") : null;
|
|
697
|
+
if (current === content) return {
|
|
698
|
+
status: "unchanged",
|
|
699
|
+
path: targetPath
|
|
700
|
+
};
|
|
701
|
+
if (current != null && !isManagedContent(targetPath, current) && !options.force) throw new CliError(`Refusing to overwrite unmanaged file: ${targetPath}`, { details: ["Use `--force` to replace the conflicting file."] });
|
|
702
|
+
await promises.mkdir(path.dirname(targetPath), { recursive: true });
|
|
703
|
+
await promises.writeFile(targetPath, content, "utf8");
|
|
704
|
+
return {
|
|
705
|
+
status: current == null ? "created" : "updated",
|
|
706
|
+
path: targetPath
|
|
707
|
+
};
|
|
708
|
+
}
|
|
709
|
+
async function writeGithubActions(repoPath, options = {}) {
|
|
710
|
+
const kind = options.kind ?? "workflow";
|
|
711
|
+
const variables = { NODE_VERSION: DEFAULT_NODE_VERSION };
|
|
712
|
+
const targets = [];
|
|
713
|
+
if (kind === "workflow" || kind === "all") targets.push({
|
|
714
|
+
relativePath: path.join(".github", "workflows", "skills-sync.yml"),
|
|
715
|
+
template: path.join("workflows", "skills-sync.yml")
|
|
716
|
+
});
|
|
717
|
+
if (kind === "action" || kind === "all") targets.push({
|
|
718
|
+
relativePath: path.join(".github", "actions", "skillsbase-sync", "action.yml"),
|
|
719
|
+
template: path.join("actions", "skillsbase-sync", "action.yml")
|
|
720
|
+
});
|
|
721
|
+
if (targets.length === 0) throw new CliError(`Unsupported github_action kind: ${kind}`, { details: ["Supported values: workflow, action, all."] });
|
|
722
|
+
const items = [];
|
|
723
|
+
for (const target of targets) {
|
|
724
|
+
const content = await renderTemplate(target.template, variables);
|
|
725
|
+
const status = await writeManagedFile(path.join(repoPath, target.relativePath), content, { force: Boolean(options.force) });
|
|
726
|
+
items.push(`${status.status}: ${target.relativePath}`);
|
|
727
|
+
}
|
|
728
|
+
return {
|
|
729
|
+
command: "github_action",
|
|
730
|
+
title: `skillsbase github_action --kind ${kind}`,
|
|
731
|
+
repository: repoPath,
|
|
732
|
+
exitCode: 0,
|
|
733
|
+
items,
|
|
734
|
+
nextSteps: []
|
|
735
|
+
};
|
|
736
|
+
}
|
|
737
|
+
//#endregion
|
|
738
|
+
//#region src/commands/github-action.ts
|
|
739
|
+
async function runGithubActionCommand(context) {
|
|
740
|
+
const repoFlag = typeof context.flags.repo === "string" ? context.flags.repo : void 0;
|
|
741
|
+
const kindFlag = typeof context.flags.kind === "string" ? context.flags.kind : void 0;
|
|
742
|
+
return writeGithubActions(path.resolve(repoFlag ?? context.cwd), {
|
|
743
|
+
kind: kindFlag ?? "workflow",
|
|
744
|
+
force: context.flags.force === true
|
|
745
|
+
});
|
|
746
|
+
}
|
|
747
|
+
//#endregion
|
|
748
|
+
//#region src/lib/repo-init.ts
|
|
749
|
+
function defaultRoots(options = {}) {
|
|
750
|
+
const home = os.homedir();
|
|
751
|
+
return {
|
|
752
|
+
firstPartyRoot: path.resolve(options.firstPartyRoot ?? path.join(home, ".agents", "skills")),
|
|
753
|
+
systemRoot: path.resolve(options.systemRoot ?? path.join(home, ".codex", "skills", ".system"))
|
|
754
|
+
};
|
|
755
|
+
}
|
|
756
|
+
async function createDefaultSource(key, label, kind, root, targetPrefix) {
|
|
757
|
+
return {
|
|
758
|
+
key,
|
|
759
|
+
label,
|
|
760
|
+
kind,
|
|
761
|
+
root,
|
|
762
|
+
targetPrefix,
|
|
763
|
+
include: await pathExists(root) ? await listDirectories(root) : []
|
|
764
|
+
};
|
|
765
|
+
}
|
|
766
|
+
async function initialiseRepository(repoPath, options = {}) {
|
|
767
|
+
await ensureDirectory(repoPath);
|
|
768
|
+
const roots = defaultRoots(options);
|
|
769
|
+
const manifestPath = path.join(repoPath, "sources.yaml");
|
|
770
|
+
const createdItems = [];
|
|
771
|
+
const preservedItems = [];
|
|
772
|
+
if (!await pathExists(manifestPath)) {
|
|
773
|
+
const sources = [await createDefaultSource("first-party", "First-party local skills", "first-party", roots.firstPartyRoot, ""), await createDefaultSource("system", "Mirrored system skills", "mirrored-system", roots.systemRoot, "system-")];
|
|
774
|
+
await saveManifest(createManifest(repoPath, {
|
|
775
|
+
remoteRepository: options.remoteRepository ?? path.basename(repoPath),
|
|
776
|
+
sources
|
|
777
|
+
}));
|
|
778
|
+
createdItems.push("sources.yaml");
|
|
779
|
+
} else preservedItems.push("sources.yaml");
|
|
780
|
+
await ensureDirectory(path.join(repoPath, "skills"));
|
|
781
|
+
await ensureDirectory(path.join(repoPath, "docs"));
|
|
782
|
+
await ensureDirectory(path.join(repoPath, ".github", "actions", "skillsbase-sync"));
|
|
783
|
+
await ensureDirectory(path.join(repoPath, ".github", "workflows"));
|
|
784
|
+
const writeStatuses = [];
|
|
785
|
+
for (const file of [{
|
|
786
|
+
relativePath: path.join("skills", "README.md"),
|
|
787
|
+
template: "skills/README.md",
|
|
788
|
+
variables: {}
|
|
789
|
+
}, {
|
|
790
|
+
relativePath: path.join("docs", "maintainer-workflow.md"),
|
|
791
|
+
template: "docs/maintainer-workflow.md",
|
|
792
|
+
variables: {}
|
|
793
|
+
}]) {
|
|
794
|
+
const content = await renderTemplate(file.template, file.variables);
|
|
795
|
+
const status = await writeManagedFile(path.join(repoPath, file.relativePath), content, { force: Boolean(options.force) });
|
|
796
|
+
writeStatuses.push(`${status.status}: ${file.relativePath}`);
|
|
797
|
+
}
|
|
798
|
+
const actionResult = await writeGithubActions(repoPath, {
|
|
799
|
+
kind: "all",
|
|
800
|
+
force: Boolean(options.force)
|
|
801
|
+
});
|
|
802
|
+
return {
|
|
803
|
+
command: "init",
|
|
804
|
+
title: "skillsbase init",
|
|
805
|
+
repository: repoPath,
|
|
806
|
+
exitCode: 0,
|
|
807
|
+
schema: "spec-driven",
|
|
808
|
+
items: [
|
|
809
|
+
...createdItems.map((item) => `created: ${item}`),
|
|
810
|
+
...preservedItems.map((item) => `preserved: ${item}`),
|
|
811
|
+
...writeStatuses,
|
|
812
|
+
...actionResult.items ?? []
|
|
813
|
+
],
|
|
814
|
+
nextSteps: ["skillsbase sync"]
|
|
815
|
+
};
|
|
816
|
+
}
|
|
817
|
+
//#endregion
|
|
818
|
+
//#region src/commands/init.ts
|
|
819
|
+
async function runInitCommand(context) {
|
|
820
|
+
const repoFlag = typeof context.flags.repo === "string" ? context.flags.repo : void 0;
|
|
821
|
+
return initialiseRepository(path.resolve(repoFlag ?? context.cwd), {
|
|
822
|
+
firstPartyRoot: typeof context.flags["first-party-root"] === "string" ? context.flags["first-party-root"] : void 0,
|
|
823
|
+
systemRoot: typeof context.flags["system-root"] === "string" ? context.flags["system-root"] : void 0,
|
|
824
|
+
remoteRepository: typeof context.flags["remote-repository"] === "string" ? context.flags["remote-repository"] : void 0,
|
|
825
|
+
force: context.flags.force === true
|
|
826
|
+
});
|
|
827
|
+
}
|
|
828
|
+
//#endregion
|
|
829
|
+
//#region src/commands/sync.ts
|
|
830
|
+
async function runSyncCommand(context) {
|
|
831
|
+
const repoFlag = typeof context.flags.repo === "string" ? context.flags.repo : void 0;
|
|
832
|
+
const repoPath = path.resolve(repoFlag ?? context.cwd);
|
|
833
|
+
return executeSync({
|
|
834
|
+
repoPath,
|
|
835
|
+
manifest: await loadManifest(repoPath),
|
|
836
|
+
check: context.flags.check === true,
|
|
837
|
+
allowMissingSources: context.flags["allow-missing-sources"] === true,
|
|
838
|
+
env: context.env
|
|
839
|
+
});
|
|
840
|
+
}
|
|
841
|
+
//#endregion
|
|
842
|
+
//#region src/lib/parse-argv.ts
|
|
843
|
+
var booleanFlags = new Set([
|
|
844
|
+
"help",
|
|
845
|
+
"version",
|
|
846
|
+
"check",
|
|
847
|
+
"allow-missing-sources",
|
|
848
|
+
"force"
|
|
849
|
+
]);
|
|
850
|
+
function parseArgv(argv) {
|
|
851
|
+
const result = {
|
|
852
|
+
command: null,
|
|
853
|
+
args: [],
|
|
854
|
+
flags: {},
|
|
855
|
+
help: false,
|
|
856
|
+
version: false
|
|
857
|
+
};
|
|
858
|
+
let index = 0;
|
|
859
|
+
while (index < argv.length) {
|
|
860
|
+
const token = argv[index];
|
|
861
|
+
if (result.command == null && !token.startsWith("-")) {
|
|
862
|
+
result.command = token;
|
|
863
|
+
index += 1;
|
|
864
|
+
continue;
|
|
865
|
+
}
|
|
866
|
+
if (token === "--help" || token === "-h") {
|
|
867
|
+
result.help = true;
|
|
868
|
+
index += 1;
|
|
869
|
+
continue;
|
|
870
|
+
}
|
|
871
|
+
if (token === "--version" || token === "-v") {
|
|
872
|
+
result.version = true;
|
|
873
|
+
index += 1;
|
|
874
|
+
continue;
|
|
875
|
+
}
|
|
876
|
+
if (token.startsWith("--")) {
|
|
877
|
+
const [flagName, inlineValue] = token.slice(2).split("=", 2);
|
|
878
|
+
if (booleanFlags.has(flagName)) {
|
|
879
|
+
result.flags[flagName] = inlineValue == null ? true : inlineValue !== "false";
|
|
880
|
+
index += 1;
|
|
881
|
+
continue;
|
|
882
|
+
}
|
|
883
|
+
const nextValue = inlineValue ?? argv[index + 1];
|
|
884
|
+
if (nextValue == null) throw new CliError(`Missing value for --${flagName}.`);
|
|
885
|
+
result.flags[flagName] = nextValue;
|
|
886
|
+
index += inlineValue == null ? 2 : 1;
|
|
887
|
+
continue;
|
|
888
|
+
}
|
|
889
|
+
if (token.startsWith("-")) throw new CliError(`Unsupported short option: ${token}`, { details: ["Use long-form flags for command options."] });
|
|
890
|
+
if (result.command != null) result.args.push(token);
|
|
891
|
+
index += 1;
|
|
892
|
+
}
|
|
893
|
+
return result;
|
|
894
|
+
}
|
|
895
|
+
//#endregion
|
|
896
|
+
//#region src/cli.ts
|
|
897
|
+
var commandMap = new Map([
|
|
898
|
+
["init", runInitCommand],
|
|
899
|
+
["sync", runSyncCommand],
|
|
900
|
+
["add", runAddCommand],
|
|
901
|
+
["github_action", runGithubActionCommand],
|
|
902
|
+
["github-action", runGithubActionCommand]
|
|
903
|
+
]);
|
|
904
|
+
function getErrorMessage(error) {
|
|
905
|
+
if (error instanceof Error) return error.message;
|
|
906
|
+
return String(error);
|
|
907
|
+
}
|
|
908
|
+
async function runCli(argv, environment = {}) {
|
|
909
|
+
const io = {
|
|
910
|
+
stdout: environment.stdout ?? process.stdout,
|
|
911
|
+
stderr: environment.stderr ?? process.stderr
|
|
912
|
+
};
|
|
913
|
+
const cwd = path.resolve(environment.cwd ?? process.cwd());
|
|
914
|
+
const env = {
|
|
915
|
+
...process.env,
|
|
916
|
+
...environment.env ?? {}
|
|
917
|
+
};
|
|
918
|
+
try {
|
|
919
|
+
const parsed = parseArgv(argv);
|
|
920
|
+
if (parsed.help || !parsed.command) {
|
|
921
|
+
printCommandUsage(io.stdout);
|
|
922
|
+
return 0;
|
|
923
|
+
}
|
|
924
|
+
if (parsed.version) {
|
|
925
|
+
io.stdout.write("skillsbase 0.1.0\n");
|
|
926
|
+
return 0;
|
|
927
|
+
}
|
|
928
|
+
const command = commandMap.get(parsed.command);
|
|
929
|
+
if (!command) throw new CliError(`Unknown command: ${parsed.command}`, {
|
|
930
|
+
exitCode: 1,
|
|
931
|
+
details: ["Use `skillsbase --help` to view supported commands."]
|
|
932
|
+
});
|
|
933
|
+
const result = await command({
|
|
934
|
+
cwd,
|
|
935
|
+
env,
|
|
936
|
+
io,
|
|
937
|
+
command: parsed.command,
|
|
938
|
+
args: parsed.args,
|
|
939
|
+
flags: parsed.flags,
|
|
940
|
+
rawArgv: argv
|
|
941
|
+
});
|
|
942
|
+
printCommandResult(result, io.stdout);
|
|
943
|
+
return result.exitCode ?? 0;
|
|
944
|
+
} catch (error) {
|
|
945
|
+
if (error instanceof CliError) {
|
|
946
|
+
printCommandResult({
|
|
947
|
+
command: "error",
|
|
948
|
+
title: error.message,
|
|
949
|
+
repository: cwd,
|
|
950
|
+
exitCode: error.exitCode ?? 1,
|
|
951
|
+
items: error.details ?? [],
|
|
952
|
+
nextSteps: error.nextSteps ?? []
|
|
953
|
+
}, io.stderr);
|
|
954
|
+
return error.exitCode ?? 1;
|
|
955
|
+
}
|
|
956
|
+
printCommandResult({
|
|
957
|
+
command: "error",
|
|
958
|
+
title: getErrorMessage(error),
|
|
959
|
+
repository: cwd,
|
|
960
|
+
exitCode: 1
|
|
961
|
+
}, io.stderr);
|
|
962
|
+
return 1;
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
//#endregion
|
|
966
|
+
//#region src/cli-entry.ts
|
|
967
|
+
var exitCode = await runCli(process.argv.slice(2));
|
|
968
|
+
process.exitCode = exitCode;
|
|
969
|
+
//#endregion
|
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@hagicode/skillsbase",
|
|
3
|
+
"version": "0.1.0-dev.20260406025440.0.0.local",
|
|
4
|
+
"description": "Managed skills repository CLI",
|
|
5
|
+
"homepage": "https://github.com/HagiCode-org/skillsbase#readme",
|
|
6
|
+
"bugs": {
|
|
7
|
+
"url": "https://github.com/HagiCode-org/skillsbase/issues"
|
|
8
|
+
},
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+https://github.com/HagiCode-org/skillsbase.git"
|
|
12
|
+
},
|
|
13
|
+
"type": "module",
|
|
14
|
+
"packageManager": "npm@10.9.2",
|
|
15
|
+
"bin": {
|
|
16
|
+
"skillsbase": "./bin/skillsbase.mjs"
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"bin",
|
|
20
|
+
"dist",
|
|
21
|
+
"templates",
|
|
22
|
+
"README.md"
|
|
23
|
+
],
|
|
24
|
+
"scripts": {
|
|
25
|
+
"build": "vite build",
|
|
26
|
+
"clean": "rm -rf dist",
|
|
27
|
+
"cli": "node --import tsx ./src/cli-entry.ts",
|
|
28
|
+
"pack:check": "node ./scripts/verify-package.mjs",
|
|
29
|
+
"publish:prepare-dev-version": "node ./scripts/prepare-dev-version.mjs",
|
|
30
|
+
"publish:verify-release": "node ./scripts/verify-release-version.mjs",
|
|
31
|
+
"test": "node --import tsx --test ./tests/*.test.ts",
|
|
32
|
+
"smoke": "node --import tsx ./scripts/smoke.mjs"
|
|
33
|
+
},
|
|
34
|
+
"engines": {
|
|
35
|
+
"node": ">=22.12.0",
|
|
36
|
+
"npm": ">=10.9.2"
|
|
37
|
+
},
|
|
38
|
+
"publishConfig": {
|
|
39
|
+
"access": "public",
|
|
40
|
+
"provenance": true,
|
|
41
|
+
"registry": "https://registry.npmjs.org/"
|
|
42
|
+
},
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"@types/node": "^24.0.0",
|
|
45
|
+
"tsx": "^4.20.6",
|
|
46
|
+
"typescript": "^5.9.3",
|
|
47
|
+
"vite": "^8.0.0"
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# Managed by skillsbase CLI.
|
|
2
|
+
|
|
3
|
+
name: skillsbase-sync
|
|
4
|
+
description: Validate a managed skillsbase repository and run sync in check mode
|
|
5
|
+
|
|
6
|
+
inputs:
|
|
7
|
+
node-version:
|
|
8
|
+
description: Node.js version used for validation
|
|
9
|
+
required: false
|
|
10
|
+
default: "{{NODE_VERSION}}"
|
|
11
|
+
run-tests:
|
|
12
|
+
description: Whether to run npm test before sync --check
|
|
13
|
+
required: false
|
|
14
|
+
default: "true"
|
|
15
|
+
|
|
16
|
+
runs:
|
|
17
|
+
using: composite
|
|
18
|
+
steps:
|
|
19
|
+
- name: Setup Node.js
|
|
20
|
+
uses: actions/setup-node@v4
|
|
21
|
+
with:
|
|
22
|
+
node-version: ${{ inputs.node-version }}
|
|
23
|
+
cache: npm
|
|
24
|
+
|
|
25
|
+
- name: Install dependencies
|
|
26
|
+
shell: bash
|
|
27
|
+
run: npm ci
|
|
28
|
+
|
|
29
|
+
- name: Run tests
|
|
30
|
+
if: ${{ inputs.run-tests == 'true' }}
|
|
31
|
+
shell: bash
|
|
32
|
+
run: npm test
|
|
33
|
+
|
|
34
|
+
- name: Run sync check
|
|
35
|
+
shell: bash
|
|
36
|
+
run: node ./bin/skillsbase.mjs sync --check
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
<!-- Managed by skillsbase CLI. -->
|
|
2
|
+
|
|
3
|
+
# Maintainer Workflow
|
|
4
|
+
|
|
5
|
+
结论是:维护流以 `init -> add -> sync -> github_action` 为主。
|
|
6
|
+
|
|
7
|
+
## Lifecycle
|
|
8
|
+
|
|
9
|
+
1. `skillsbase init`
|
|
10
|
+
2. `skillsbase add <skill-name>`
|
|
11
|
+
3. `skillsbase sync`
|
|
12
|
+
4. `skillsbase github_action --kind all`
|
|
13
|
+
|
|
14
|
+
## Notes
|
|
15
|
+
|
|
16
|
+
- `sources.yaml` 是单一真相源。
|
|
17
|
+
- `skills/` 仅保存受管输出。
|
|
18
|
+
- `.skill-source.json` 记录来源与转换元数据。
|
|
19
|
+
- `skillsbase sync --check` 只校验,不改仓库。
|
|
20
|
+
- 缺少来源根目录时,可用 `skillsbase sync --allow-missing-sources` 跳过。
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Managed by skillsbase CLI.
|
|
2
|
+
# This template documents the expected manifest shape. The CLI serializes the real file.
|
|
3
|
+
version: 1
|
|
4
|
+
skillsRoot: skills
|
|
5
|
+
metadataFile: .skill-source.json
|
|
6
|
+
managedBy: skillsbase
|
|
7
|
+
remoteRepository: "{{REMOTE_REPOSITORY}}"
|
|
8
|
+
staleCleanup: true
|
|
9
|
+
skillsCliVersion: 1.4.8
|
|
10
|
+
installAgent: codex
|
|
11
|
+
sources:
|
|
12
|
+
- key: first-party
|
|
13
|
+
label: "First-party local skills"
|
|
14
|
+
kind: first-party
|
|
15
|
+
root: "{{FIRST_PARTY_ROOT}}"
|
|
16
|
+
targetPrefix: ""
|
|
17
|
+
include:
|
|
18
|
+
- "{{FIRST_PARTY_EXAMPLE}}"
|
|
19
|
+
- key: system
|
|
20
|
+
label: "Mirrored system skills"
|
|
21
|
+
kind: mirrored-system
|
|
22
|
+
root: "{{SYSTEM_ROOT}}"
|
|
23
|
+
targetPrefix: system-
|
|
24
|
+
include:
|
|
25
|
+
- "{{SYSTEM_EXAMPLE}}"
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# Managed by skillsbase CLI.
|
|
2
|
+
|
|
3
|
+
name: Skills Sync
|
|
4
|
+
|
|
5
|
+
on:
|
|
6
|
+
workflow_dispatch:
|
|
7
|
+
pull_request:
|
|
8
|
+
push:
|
|
9
|
+
branches:
|
|
10
|
+
- main
|
|
11
|
+
|
|
12
|
+
permissions:
|
|
13
|
+
contents: read
|
|
14
|
+
|
|
15
|
+
concurrency:
|
|
16
|
+
group: skills-sync-${{ github.ref }}
|
|
17
|
+
cancel-in-progress: false
|
|
18
|
+
|
|
19
|
+
jobs:
|
|
20
|
+
validate:
|
|
21
|
+
runs-on: ubuntu-latest
|
|
22
|
+
steps:
|
|
23
|
+
- name: Checkout repository
|
|
24
|
+
uses: actions/checkout@v4
|
|
25
|
+
|
|
26
|
+
- name: Setup Node.js
|
|
27
|
+
uses: actions/setup-node@v4
|
|
28
|
+
with:
|
|
29
|
+
node-version: {{NODE_VERSION}}
|
|
30
|
+
cache: npm
|
|
31
|
+
|
|
32
|
+
- name: Install dependencies
|
|
33
|
+
run: npm ci
|
|
34
|
+
|
|
35
|
+
- name: Run tests
|
|
36
|
+
run: npm test
|
|
37
|
+
|
|
38
|
+
- name: Validate managed repository state
|
|
39
|
+
run: node ./bin/skillsbase.mjs sync --check
|