@hagicode/skillsbase 0.1.1 → 0.1.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 CHANGED
@@ -48,6 +48,12 @@ Add one skill and sync immediately:
48
48
  skillsbase add documentation-writer --repo /path/to/target-repo
49
49
  ```
50
50
 
51
+ Remove one skill and sync immediately:
52
+
53
+ ```bash
54
+ skillsbase remove documentation-writer --repo /path/to/target-repo
55
+ ```
56
+
51
57
  Generate managed GitHub Actions assets:
52
58
 
53
59
  ```bash
@@ -62,6 +68,7 @@ skillsbase github_action --repo /path/to/target-repo --kind all
62
68
  | `sync` | Reconcile managed skills from `sources.yaml`. | `skillsbase sync --repo ./my-skills-repo` |
63
69
  | `sync --check` | Validate drift without writing files. | `skillsbase sync --check --repo ./my-skills-repo` |
64
70
  | `add <skill-name>` | Add a skill to a source block, then run sync. | `skillsbase add documentation-writer --repo ./my-skills-repo` |
71
+ | `remove <skill-name>` | Remove a skill from a source block, then run sync. | `skillsbase remove documentation-writer --repo ./my-skills-repo` |
65
72
  | `github_action` | Generate managed GitHub Actions workflow or action files. | `skillsbase github_action --repo ./my-skills-repo --kind workflow` |
66
73
 
67
74
  Global options:
@@ -88,6 +95,8 @@ Global options:
88
95
  - first-party: `$HOME/.agents/skills`
89
96
  - system: `$HOME/.codex/skills/.system`
90
97
  - `add` writes to the first declared source block unless `--source <key>` is provided.
98
+ - `remove` deletes from the only matching source block automatically; if multiple source blocks include the same skill, pass `--source <key>`.
99
+ - `add`, `remove`, and `sync` accept `--allow-missing-sources` to keep path-resolution behavior consistent in CI or repo-external execution.
91
100
  - `github_action` defaults to `--kind workflow`.
92
101
  - When the CLI does not have enough context to write safely, it fails with diagnostics instead of prompting interactively.
93
102
 
package/README.zh-CN.md CHANGED
@@ -48,6 +48,12 @@ skillsbase sync --repo /path/to/target-repo
48
48
  skillsbase add documentation-writer --repo /path/to/target-repo
49
49
  ```
50
50
 
51
+ 删除单个技能并立即同步:
52
+
53
+ ```bash
54
+ skillsbase remove documentation-writer --repo /path/to/target-repo
55
+ ```
56
+
51
57
  生成受管 GitHub Actions 资产:
52
58
 
53
59
  ```bash
@@ -62,6 +68,7 @@ skillsbase github_action --repo /path/to/target-repo --kind all
62
68
  | `sync` | 按 `sources.yaml` 对账并同步技能 | `skillsbase sync --repo ./my-skills-repo` |
63
69
  | `sync --check` | 只校验漂移,不写文件 | `skillsbase sync --check --repo ./my-skills-repo` |
64
70
  | `add <skill-name>` | 将技能写入 source block 后执行同步 | `skillsbase add documentation-writer --repo ./my-skills-repo` |
71
+ | `remove <skill-name>` | 从 source block 删除技能后执行同步 | `skillsbase remove documentation-writer --repo ./my-skills-repo` |
65
72
  | `github_action` | 生成受管 GitHub Actions 工作流或 action 文件 | `skillsbase github_action --repo ./my-skills-repo --kind workflow` |
66
73
 
67
74
  全局选项:
@@ -88,6 +95,8 @@ skillsbase github_action --repo /path/to/target-repo --kind all
88
95
  - first-party:`$HOME/.agents/skills`
89
96
  - system:`$HOME/.codex/skills/.system`
90
97
  - `add` 默认写入第一个已声明的 source block;如需指定,传 `--source <key>`。
98
+ - `remove` 若仅命中一个 source block,会直接删除;若同名技能存在于多个 source block,必须显式传 `--source <key>`。
99
+ - `add`、`remove`、`sync` 均接受 `--allow-missing-sources`,以便在 CI 或仓库外 cwd 下保持一致的路径解析与失败语义。
91
100
  - `github_action` 默认使用 `--kind workflow`。
92
101
  - 若上下文不足以安全写入,CLI 会直接失败并输出诊断,不会进入交互提问。
93
102
 
File without changes
package/dist/cli.mjs CHANGED
@@ -25,12 +25,14 @@ function printCommandUsage(stdout) {
25
25
  " init Create the managed repository baseline",
26
26
  " sync Reconcile managed skills from sources.yaml",
27
27
  " add Add a skill to a source block and sync",
28
+ " remove Remove a skill from a source block and sync",
28
29
  " github_action Generate managed GitHub Actions assets",
29
30
  "",
30
31
  "Global Options:",
31
- " --repo <path> Target repository path (default: current directory)",
32
- " --help, -h Show help",
33
- " --version, -v Show version",
32
+ " --repo <path> Target repository path (default: current directory)",
33
+ " --allow-missing-sources Skip missing local source roots during sync/add/remove",
34
+ " --help, -h Show help",
35
+ " --version, -v Show version",
34
36
  ""
35
37
  ].join("\n"));
36
38
  }
@@ -168,6 +170,56 @@ function validateSource(source) {
168
170
  include: [...source.include]
169
171
  };
170
172
  }
173
+ function isRemoteRepositorySource(source) {
174
+ return source.kind === "github-repository";
175
+ }
176
+ function buildSourcePath(source, originalName) {
177
+ if (isRemoteRepositorySource(source)) return `${source.root}@${originalName}`;
178
+ return path.join(source.root, originalName);
179
+ }
180
+ function resolveSourceRoot(repoPath, source) {
181
+ if (isRemoteRepositorySource(source) || path.isAbsolute(source.root)) return source.root;
182
+ return path.resolve(repoPath, source.root);
183
+ }
184
+ function resolveSourcePath(repoPath, source, originalName) {
185
+ if (isRemoteRepositorySource(source)) return buildSourcePath(source, originalName);
186
+ return path.join(resolveSourceRoot(repoPath, source), originalName);
187
+ }
188
+ function cloneSource(source) {
189
+ return {
190
+ ...source,
191
+ include: [...source.include ?? []]
192
+ };
193
+ }
194
+ function cloneManifestWithSources(manifest, sources) {
195
+ return {
196
+ ...manifest,
197
+ sources: sources.map(cloneSource)
198
+ };
199
+ }
200
+ function sortInclude(include) {
201
+ return [...include].sort((left, right) => left.localeCompare(right));
202
+ }
203
+ function assertManifestHasSources(manifest) {
204
+ 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`."] });
205
+ }
206
+ function getSourceKeys(manifest) {
207
+ return manifest.sources.map((source) => source.key).join(", ");
208
+ }
209
+ function findSourceByKey(manifest, sourceKey) {
210
+ const source = manifest.sources.find((candidate) => candidate.key === sourceKey);
211
+ if (!source) throw new CliError(`Unknown source key: ${sourceKey}`, { details: [`Declared sources: ${getSourceKeys(manifest)}`] });
212
+ return source;
213
+ }
214
+ function findSourcesBySkill(manifest, skillName) {
215
+ return manifest.sources.filter((source) => source.include.includes(skillName));
216
+ }
217
+ function buildMissingSkillError(skillName, options = {}) {
218
+ const matchingKeys = options.matchingKeys ?? [];
219
+ const details = options.sourceKey ? [`skill: ${skillName}`, `source: ${options.sourceKey}`] : [`skill: ${skillName}`];
220
+ if (matchingKeys.length > 0) details.push(`matching sources: ${matchingKeys.join(", ")}`);
221
+ return new CliError(options.sourceKey == null ? `Skill "${skillName}" is not declared in sources.yaml.` : `Skill "${skillName}" is not declared in source "${options.sourceKey}".`, { details });
222
+ }
171
223
  function createManifest(repoPath, options = {}) {
172
224
  return {
173
225
  version: 1,
@@ -300,19 +352,25 @@ async function saveManifest(manifest) {
300
352
  }
301
353
  function buildManifestEntries(manifest, repoPath = manifest.repoPath) {
302
354
  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
- });
355
+ for (const source of manifest.sources) {
356
+ const resolvedSourceRoot = resolveSourceRoot(repoPath, source);
357
+ for (const originalName of source.include ?? []) {
358
+ const targetName = `${source.targetPrefix ?? ""}${originalName}`;
359
+ entries.push({
360
+ sourceKey: source.key,
361
+ sourceLabel: source.label,
362
+ sourceKind: source.kind,
363
+ remoteSource: isRemoteRepositorySource(source),
364
+ sourceRoot: source.root,
365
+ sourcePath: buildSourcePath(source, originalName),
366
+ resolvedSourceRoot,
367
+ resolvedSourcePath: resolveSourcePath(repoPath, source, originalName),
368
+ originalName,
369
+ targetName,
370
+ targetPath: path.join(repoPath, manifest.skillsRoot, targetName),
371
+ targetPathRelative: toPosix(path.join(manifest.skillsRoot, targetName))
372
+ });
373
+ }
316
374
  }
317
375
  const collisions = /* @__PURE__ */ new Map();
318
376
  for (const entry of entries) {
@@ -325,19 +383,49 @@ function buildManifestEntries(manifest, repoPath = manifest.repoPath) {
325
383
  return entries.sort((left, right) => left.targetName.localeCompare(right.targetName));
326
384
  }
327
385
  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 } : {
386
+ assertManifestHasSources(manifest);
387
+ const selectedSource = options.sourceKey == null ? manifest.sources[0] : findSourceByKey(manifest, options.sourceKey);
388
+ return cloneManifestWithSources(manifest, manifest.sources.map((source) => {
389
+ if (source.key !== selectedSource.key) return source;
390
+ return {
337
391
  ...source,
338
- include: [...source.include]
339
- })
340
- };
392
+ include: sortInclude(new Set(source.include ?? []).add(skillName))
393
+ };
394
+ }));
395
+ }
396
+ function removeSkillFromManifest(manifest, skillName, options = {}) {
397
+ assertManifestHasSources(manifest);
398
+ const selectedSource = options.sourceKey == null ? null : findSourceByKey(manifest, options.sourceKey);
399
+ if (selectedSource != null) {
400
+ if (!selectedSource.include.includes(skillName)) {
401
+ const matchingKeys = findSourcesBySkill(manifest, skillName).map((source) => source.key);
402
+ throw buildMissingSkillError(skillName, {
403
+ sourceKey: selectedSource.key,
404
+ matchingKeys
405
+ });
406
+ }
407
+ return cloneManifestWithSources(manifest, manifest.sources.map((source) => {
408
+ if (source.key !== selectedSource.key) return source;
409
+ return {
410
+ ...source,
411
+ include: sortInclude(source.include.filter((candidate) => candidate !== skillName))
412
+ };
413
+ }));
414
+ }
415
+ const matchingSources = findSourcesBySkill(manifest, skillName);
416
+ if (matchingSources.length === 0) throw buildMissingSkillError(skillName);
417
+ if (matchingSources.length > 1) {
418
+ const matchingKeys = matchingSources.map((source) => source.key).sort((left, right) => left.localeCompare(right));
419
+ throw new CliError(`Skill "${skillName}" is declared in multiple sources.`, { details: [`matching sources: ${matchingKeys.join(", ")}`, `Use \`skillsbase remove ${skillName} --source <key>\` to disambiguate.`] });
420
+ }
421
+ const [uniqueSource] = matchingSources;
422
+ return cloneManifestWithSources(manifest, manifest.sources.map((source) => {
423
+ if (source.key !== uniqueSource.key) return source;
424
+ return {
425
+ ...source,
426
+ include: sortInclude(source.include.filter((candidate) => candidate !== skillName))
427
+ };
428
+ }));
341
429
  }
342
430
  function buildMetadata(manifest, entry, installRecord) {
343
431
  return {
@@ -362,6 +450,24 @@ function buildMetadata(manifest, entry, installRecord) {
362
450
  //#endregion
363
451
  //#region src/lib/installer.ts
364
452
  var execFile$1 = promisify(execFile);
453
+ var INSTALL_TIMEOUT_MS = 18e4;
454
+ var REMOTE_INSTALL_RETRIES = 3;
455
+ var NPM_ENV_KEYS_TO_UNSET = [
456
+ "INIT_CWD",
457
+ "npm_command",
458
+ "npm_config_local_prefix",
459
+ "npm_config_prefix",
460
+ "npm_execpath",
461
+ "npm_lifecycle_event",
462
+ "npm_lifecycle_script",
463
+ "npm_package_json",
464
+ "npm_prefix"
465
+ ];
466
+ function toInstallReference(entry) {
467
+ if (entry.remoteSource) return entry.sourcePath;
468
+ if (path.isAbsolute(entry.sourcePath) || entry.sourcePath.startsWith(`.${path.sep}`) || entry.sourcePath === ".") return entry.sourcePath;
469
+ return `.${path.sep}${entry.sourcePath}`;
470
+ }
365
471
  function buildNpxArgs(manifest, subcommand, extraArgs) {
366
472
  return [
367
473
  "--yes",
@@ -377,7 +483,25 @@ function renderExecFailure(error) {
377
483
  }
378
484
  return String(error);
379
485
  }
486
+ async function execSkillsAdd(repoPath, manifest, installReference, options) {
487
+ const childEnv = { ...options.env ?? process.env };
488
+ for (const key of NPM_ENV_KEYS_TO_UNSET) delete childEnv[key];
489
+ childEnv.INIT_CWD = repoPath;
490
+ await execFile$1("npx", buildNpxArgs(manifest, "add", [
491
+ installReference,
492
+ "--agent",
493
+ manifest.installAgent,
494
+ "--copy",
495
+ "-y"
496
+ ]), {
497
+ cwd: repoPath,
498
+ env: childEnv,
499
+ maxBuffer: 16 * 1024 * 1024,
500
+ timeout: INSTALL_TIMEOUT_MS
501
+ });
502
+ }
380
503
  async function installIntoCurrentRepository(repoPath, manifest, entry, options = {}) {
504
+ const installReference = toInstallReference(entry);
381
505
  const installPath = path.join(repoPath, ".agents", "skills", entry.originalName);
382
506
  const lockPath = path.join(repoPath, "skills-lock.json");
383
507
  const snapshot = {
@@ -385,52 +509,36 @@ async function installIntoCurrentRepository(repoPath, manifest, entry, options =
385
509
  lockPath,
386
510
  installTree: await snapshotTree(installPath),
387
511
  lockText: await readFileIfExists(lockPath),
388
- installReference: entry.sourcePath
512
+ installReference
389
513
  };
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
- });
514
+ const attempts = entry.remoteSource ? REMOTE_INSTALL_RETRIES : 1;
515
+ const failures = [];
516
+ for (let attempt = 1; attempt <= attempts; attempt += 1) try {
517
+ await execSkillsAdd(repoPath, manifest, installReference, options);
518
+ failures.length = 0;
519
+ break;
403
520
  } catch (error) {
404
- throw new CliError(`skills install failed for ${entry.originalName}.`, { details: [renderExecFailure(error)] });
521
+ failures.push(`attempt ${attempt}/${attempts}: ${renderExecFailure(error)}`);
522
+ await restoreSnapshot(snapshot);
523
+ if (attempt === attempts) break;
405
524
  }
525
+ if (failures.length > 0) throw new CliError(`skills install failed for ${entry.originalName}.`, { details: failures });
406
526
  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
527
  return {
408
528
  installPath,
409
529
  lockPath,
410
- installReference: entry.sourcePath,
530
+ installReference,
411
531
  snapshot
412
532
  };
413
533
  }
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
- }
534
+ async function cleanupInstalledSkill(repoPath, _manifest, entry, installState, _options = {}) {
426
535
  try {
427
536
  await restoreSnapshot(installState.snapshot);
428
537
  await removeIfEmptyUpward(path.join(repoPath, ".agents", "skills"), repoPath);
429
538
  } catch (restoreError) {
430
539
  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)) });
540
+ throw new CliError(`Cleanup failed for ${entry.originalName}.`, { details: [restoreMessage] });
432
541
  }
433
- if (removeError) throw new CliError(`skills uninstall failed for ${entry.originalName}.`, { details: [renderExecFailure(removeError)] });
434
542
  }
435
543
  async function snapshotTree(rootPath) {
436
544
  if (!await pathExists(rootPath)) return null;
@@ -524,14 +632,15 @@ async function convertInstalledSkill(_manifest, entry, installState) {
524
632
  //#endregion
525
633
  //#region src/lib/sync-engine.ts
526
634
  async function assertSourceState(entry, allowMissingSources) {
527
- if (!await pathExists(entry.sourceRoot)) {
635
+ if (entry.remoteSource) return { skip: false };
636
+ if (!await pathExists(entry.resolvedSourceRoot)) {
528
637
  if (allowMissingSources) return {
529
638
  skip: true,
530
- reason: `missing source root: ${entry.sourceRoot}`
639
+ reason: `missing source root: ${entry.resolvedSourceRoot}`
531
640
  };
532
- throw new CliError(`Managed source root does not exist: ${entry.sourceRoot}`, { details: ["Use `skillsbase sync --allow-missing-sources` to skip missing roots."] });
641
+ throw new CliError(`Managed source root does not exist: ${entry.resolvedSourceRoot}`, { details: ["Use `skillsbase sync --allow-missing-sources` to skip missing roots."] });
533
642
  }
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}`] });
643
+ if (!await pathExists(entry.resolvedSourcePath)) throw new CliError(`Managed skill is missing from source root: ${entry.resolvedSourcePath}`, { details: [`source: ${entry.sourceKey}`, `skill: ${entry.originalName}`] });
535
644
  return { skip: false };
536
645
  }
537
646
  async function assertManagedTargetWritable(manifest, entry) {
@@ -713,10 +822,16 @@ async function writeGithubActions(repoPath, options = {}) {
713
822
  if (kind === "workflow" || kind === "all") targets.push({
714
823
  relativePath: path.join(".github", "workflows", "skills-sync.yml"),
715
824
  template: path.join("workflows", "skills-sync.yml")
825
+ }, {
826
+ relativePath: path.join(".github", "workflows", "skills-manage.yml"),
827
+ template: path.join("workflows", "skills-manage.yml")
716
828
  });
717
- if (kind === "action" || kind === "all") targets.push({
829
+ if (kind === "workflow" || kind === "action" || kind === "all") targets.push({
718
830
  relativePath: path.join(".github", "actions", "skillsbase-sync", "action.yml"),
719
831
  template: path.join("actions", "skillsbase-sync", "action.yml")
832
+ }, {
833
+ relativePath: path.join(".github", "actions", "skillsbase-manage", "action.yml"),
834
+ template: path.join("actions", "skillsbase-manage", "action.yml")
720
835
  });
721
836
  if (targets.length === 0) throw new CliError(`Unsupported github_action kind: ${kind}`, { details: ["Supported values: workflow, action, all."] });
722
837
  const items = [];
@@ -826,6 +941,30 @@ async function runInitCommand(context) {
826
941
  });
827
942
  }
828
943
  //#endregion
944
+ //#region src/commands/remove.ts
945
+ async function runRemoveCommand(context) {
946
+ const repoFlag = typeof context.flags.repo === "string" ? context.flags.repo : void 0;
947
+ const sourceFlag = typeof context.flags.source === "string" ? context.flags.source : void 0;
948
+ const repoPath = path.resolve(repoFlag ?? context.cwd);
949
+ const skillName = context.args[0];
950
+ if (!skillName) throw new CliError("`skillsbase remove` requires a skill name.", { details: ["Usage: `skillsbase remove <skill-name> [--source <key>]`."] });
951
+ const nextManifest = removeSkillFromManifest(await loadManifest(repoPath), skillName, { sourceKey: sourceFlag });
952
+ await saveManifest(nextManifest);
953
+ const result = await executeSync({
954
+ repoPath,
955
+ manifest: nextManifest,
956
+ env: context.env,
957
+ check: false,
958
+ allowMissingSources: context.flags["allow-missing-sources"] === true
959
+ });
960
+ return {
961
+ ...result,
962
+ command: "remove",
963
+ title: `skillsbase remove ${skillName}`,
964
+ items: [`manifest updated: ${path.relative(repoPath, nextManifest.manifestPath) || "sources.yaml"}`, ...result.items]
965
+ };
966
+ }
967
+ //#endregion
829
968
  //#region src/commands/sync.ts
830
969
  async function runSyncCommand(context) {
831
970
  const repoFlag = typeof context.flags.repo === "string" ? context.flags.repo : void 0;
@@ -898,6 +1037,7 @@ var commandMap = new Map([
898
1037
  ["init", runInitCommand],
899
1038
  ["sync", runSyncCommand],
900
1039
  ["add", runAddCommand],
1040
+ ["remove", runRemoveCommand],
901
1041
  ["github_action", runGithubActionCommand],
902
1042
  ["github-action", runGithubActionCommand]
903
1043
  ]);
@@ -965,5 +1105,5 @@ async function runCli(argv, environment = {}) {
965
1105
  //#endregion
966
1106
  //#region src/cli-entry.ts
967
1107
  var exitCode = await runCli(process.argv.slice(2));
968
- process.exitCode = exitCode;
1108
+ process.exit(exitCode);
969
1109
  //#endregion
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hagicode/skillsbase",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "Managed skills repository CLI",
5
5
  "homepage": "https://github.com/HagiCode-org/skillsbase#readme",
6
6
  "bugs": {
@@ -0,0 +1,96 @@
1
+ # Managed by skillsbase CLI.
2
+
3
+ name: skillsbase-manage
4
+ description: Run a managed add, remove, or sync operation for a skillsbase repository
5
+
6
+ inputs:
7
+ node-version:
8
+ description: Node.js version used for maintenance
9
+ required: false
10
+ default: "{{NODE_VERSION}}"
11
+ operation:
12
+ description: Maintenance operation to run (`add`, `remove`, or `sync`)
13
+ required: true
14
+ skill-name:
15
+ description: Skill name for `add` or `remove`
16
+ required: false
17
+ default: ""
18
+ source:
19
+ description: Optional source key for `add` or `remove`
20
+ required: false
21
+ default: ""
22
+ allow-missing-sources:
23
+ description: Skip missing local roots during the operation
24
+ required: false
25
+ default: "false"
26
+ run-tests:
27
+ description: Whether to run npm test after the maintenance command
28
+ required: false
29
+ default: "true"
30
+
31
+ runs:
32
+ using: composite
33
+ steps:
34
+ - name: Setup Node.js
35
+ uses: actions/setup-node@v4
36
+ with:
37
+ node-version: ${{ inputs.node-version }}
38
+ cache: npm
39
+
40
+ - name: Install dependencies
41
+ shell: bash
42
+ run: npm ci
43
+
44
+ - name: Upgrade npm
45
+ shell: bash
46
+ run: npm install --global npm@10.9.2
47
+
48
+ - name: Install skillsbase
49
+ shell: bash
50
+ run: npm install --global @hagicode/skillsbase
51
+
52
+ - name: Run maintenance command
53
+ shell: bash
54
+ env:
55
+ OPERATION: ${{ inputs.operation }}
56
+ SKILL_NAME: ${{ inputs.skill-name }}
57
+ SOURCE: ${{ inputs.source }}
58
+ ALLOW_MISSING_SOURCES: ${{ inputs.allow-missing-sources }}
59
+ run: |
60
+ set -euo pipefail
61
+
62
+ base_args=(--repo .)
63
+ if [[ "${ALLOW_MISSING_SOURCES}" == "true" ]]; then
64
+ base_args+=(--allow-missing-sources)
65
+ fi
66
+
67
+ case "${OPERATION}" in
68
+ add|remove)
69
+ if [[ -z "${SKILL_NAME}" ]]; then
70
+ echo "::error::skill-name is required when operation=${OPERATION}."
71
+ exit 1
72
+ fi
73
+
74
+ command=(skillsbase "${OPERATION}" "${SKILL_NAME}" "${base_args[@]}")
75
+ if [[ -n "${SOURCE}" ]]; then
76
+ command+=(--source "${SOURCE}")
77
+ fi
78
+ ;;
79
+ sync)
80
+ command=(skillsbase sync "${base_args[@]}")
81
+ ;;
82
+ *)
83
+ echo "::error::Unsupported operation: ${OPERATION}."
84
+ exit 1
85
+ ;;
86
+ esac
87
+
88
+ printf 'Running:'
89
+ printf ' %q' "${command[@]}"
90
+ printf '\n'
91
+ "${command[@]}"
92
+
93
+ - name: Run tests
94
+ if: ${{ inputs.run-tests == 'true' }}
95
+ shell: bash
96
+ run: npm test
@@ -26,6 +26,14 @@ runs:
26
26
  shell: bash
27
27
  run: npm ci
28
28
 
29
+ - name: Upgrade npm
30
+ shell: bash
31
+ run: npm install --global npm@10.9.2
32
+
33
+ - name: Install skillsbase
34
+ shell: bash
35
+ run: npm install --global @hagicode/skillsbase
36
+
29
37
  - name: Run tests
30
38
  if: ${{ inputs.run-tests == 'true' }}
31
39
  shell: bash
@@ -33,4 +41,4 @@ runs:
33
41
 
34
42
  - name: Run sync check
35
43
  shell: bash
36
- run: node ./bin/skillsbase.mjs sync --check
44
+ run: skillsbase sync --check --repo .
@@ -2,19 +2,31 @@
2
2
 
3
3
  # Maintainer Workflow
4
4
 
5
- 结论是:维护流以 `init -> add -> sync -> github_action` 为主。
5
+ The maintainer flow is `init -> add/remove -> sync -> github_action`.
6
6
 
7
7
  ## Lifecycle
8
8
 
9
9
  1. `skillsbase init`
10
- 2. `skillsbase add <skill-name>`
10
+ 2. `skillsbase add <skill-name>` or `skillsbase remove <skill-name>`
11
11
  3. `skillsbase sync`
12
12
  4. `skillsbase github_action --kind all`
13
13
 
14
+ ## GitHub Maintenance Path
15
+
16
+ Use `.github/workflows/skills-manage.yml` only for explicit non-interactive maintenance from GitHub UI.
17
+
18
+ - `operation` chooses `add`, `remove`, or `sync`
19
+ - `skill-name` is required for `add` and `remove`
20
+ - `source` is optional and maps to `--source`
21
+ - `allow-missing-sources` maps to `--allow-missing-sources`
22
+ - `run-tests` controls the post-operation `npm test`
23
+ - The workflow does not commit, push, or open pull requests
24
+
14
25
  ## Notes
15
26
 
16
- - `sources.yaml` 是单一真相源。
17
- - `skills/` 仅保存受管输出。
18
- - `.skill-source.json` 记录来源与转换元数据。
19
- - `skillsbase sync --check` 只校验,不改仓库。
20
- - 缺少来源根目录时,可用 `skillsbase sync --allow-missing-sources` 跳过。
27
+ - `sources.yaml` is the single source of truth.
28
+ - `skills/` stores managed output only.
29
+ - `.skill-source.json` records source and conversion metadata.
30
+ - `skillsbase sync --check` validates drift without writing files.
31
+ - If a source root is unavailable, use `skillsbase sync --allow-missing-sources` to skip it.
32
+ - Local CLI commands remain the primary maintainer path.
@@ -2,8 +2,8 @@
2
2
 
3
3
  # Managed Skills
4
4
 
5
- 此目录只存放受管 skill 输出。
5
+ This directory contains managed skill output only.
6
6
 
7
- - 每个 skill 目录必须包含 `SKILL.md`
8
- - 每个受管目录必须包含 `.skill-source.json`
9
- - 不要手改受管文件;变更应回到 `sources.yaml` 与上游来源
7
+ - Every skill directory must include `SKILL.md`
8
+ - Every managed directory must include `.skill-source.json`
9
+ - Do not edit managed files by hand; make changes in `sources.yaml` and the upstream source instead
@@ -0,0 +1,54 @@
1
+ # Managed by skillsbase CLI.
2
+
3
+ name: Skills Manage
4
+
5
+ on:
6
+ workflow_dispatch:
7
+ inputs:
8
+ operation:
9
+ description: Operation to run (`add`, `remove`, or `sync`)
10
+ required: true
11
+ type: choice
12
+ default: sync
13
+ options:
14
+ - add
15
+ - remove
16
+ - sync
17
+ skill-name:
18
+ description: Skill name for `add` or `remove`
19
+ required: false
20
+ type: string
21
+ source:
22
+ description: Optional source key for `add` or `remove`
23
+ required: false
24
+ type: string
25
+ allow-missing-sources:
26
+ description: Skip missing local roots during the operation
27
+ required: false
28
+ type: boolean
29
+ default: false
30
+ run-tests:
31
+ description: Run `npm test` after the maintenance command
32
+ required: false
33
+ type: boolean
34
+ default: true
35
+
36
+ permissions:
37
+ contents: read
38
+
39
+ jobs:
40
+ manage:
41
+ runs-on: ubuntu-latest
42
+ steps:
43
+ - name: Checkout repository
44
+ uses: actions/checkout@v4
45
+
46
+ - name: Run skillsbase maintenance
47
+ uses: ./.github/actions/skillsbase-manage
48
+ with:
49
+ node-version: {{NODE_VERSION}}
50
+ operation: ${{ inputs.operation }}
51
+ skill-name: ${{ inputs.skill-name }}
52
+ source: ${{ inputs.source }}
53
+ allow-missing-sources: ${{ inputs.allow-missing-sources }}
54
+ run-tests: ${{ inputs.run-tests }}
@@ -23,17 +23,8 @@ jobs:
23
23
  - name: Checkout repository
24
24
  uses: actions/checkout@v4
25
25
 
26
- - name: Setup Node.js
27
- uses: actions/setup-node@v4
26
+ - name: Run skillsbase validation
27
+ uses: ./.github/actions/skillsbase-sync
28
28
  with:
29
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
30
+ run-tests: "true"