skills-manager-cli 2026.9.11 → 2026.9.18

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 (88) hide show
  1. package/README.md +13 -1
  2. package/README.zh-CN.md +13 -1
  3. package/dist/cli/cost-output.js +68 -0
  4. package/dist/cli/cost-output.js.map +1 -0
  5. package/dist/cli/insecure-http-confirm.js +20 -0
  6. package/dist/cli/insecure-http-confirm.js.map +1 -0
  7. package/dist/cli/main.js +256 -19
  8. package/dist/cli/main.js.map +1 -1
  9. package/dist/core/index.js +1 -0
  10. package/dist/core/index.js.map +1 -1
  11. package/dist/core/model/index.js.map +1 -1
  12. package/dist/core/ports/filesystem.js +5 -1
  13. package/dist/core/ports/filesystem.js.map +1 -1
  14. package/dist/core/ports/http-download.js +33 -0
  15. package/dist/core/ports/http-download.js.map +1 -0
  16. package/dist/core/services/archive-extract.js +212 -0
  17. package/dist/core/services/archive-extract.js.map +1 -0
  18. package/dist/core/services/archive-safety.js +137 -0
  19. package/dist/core/services/archive-safety.js.map +1 -0
  20. package/dist/core/services/cost-ledger-service.js +214 -0
  21. package/dist/core/services/cost-ledger-service.js.map +1 -0
  22. package/dist/core/services/detection-service.js +118 -29
  23. package/dist/core/services/detection-service.js.map +1 -1
  24. package/dist/core/services/distribute-service.js +58 -4
  25. package/dist/core/services/distribute-service.js.map +1 -1
  26. package/dist/core/services/doctor-service.js +13 -2
  27. package/dist/core/services/doctor-service.js.map +1 -1
  28. package/dist/core/services/frontmatter-evidence.js +99 -0
  29. package/dist/core/services/frontmatter-evidence.js.map +1 -0
  30. package/dist/core/services/frontmatter-mirror.js +151 -0
  31. package/dist/core/services/frontmatter-mirror.js.map +1 -0
  32. package/dist/core/services/get-service.js +55 -0
  33. package/dist/core/services/get-service.js.map +1 -0
  34. package/dist/core/services/index.js +26 -6
  35. package/dist/core/services/index.js.map +1 -1
  36. package/dist/core/services/init-service.js +15 -5
  37. package/dist/core/services/init-service.js.map +1 -1
  38. package/dist/core/services/install-service.js +62 -9
  39. package/dist/core/services/install-service.js.map +1 -1
  40. package/dist/core/services/marketplace-manifest.js +40 -0
  41. package/dist/core/services/marketplace-manifest.js.map +1 -0
  42. package/dist/core/services/migration-service.js +9 -4
  43. package/dist/core/services/migration-service.js.map +1 -1
  44. package/dist/core/services/provenance-service.js +14 -9
  45. package/dist/core/services/provenance-service.js.map +1 -1
  46. package/dist/core/services/registry-service.js +92 -7
  47. package/dist/core/services/registry-service.js.map +1 -1
  48. package/dist/core/services/skill-home-service.js +5 -1
  49. package/dist/core/services/skill-home-service.js.map +1 -1
  50. package/dist/core/services/source-service.js +358 -47
  51. package/dist/core/services/source-service.js.map +1 -1
  52. package/dist/core/services/sync-service.js +422 -0
  53. package/dist/core/services/sync-service.js.map +1 -0
  54. package/dist/core/services/tar-extract.js +275 -0
  55. package/dist/core/services/tar-extract.js.map +1 -0
  56. package/dist/core/services/tree-content-hash.js +38 -0
  57. package/dist/core/services/tree-content-hash.js.map +1 -0
  58. package/dist/core/services/update-service.js +14 -1
  59. package/dist/core/services/update-service.js.map +1 -1
  60. package/dist/core/services/url-payload.js +92 -0
  61. package/dist/core/services/url-payload.js.map +1 -0
  62. package/dist/core/services/wellknown-index.js +100 -0
  63. package/dist/core/services/wellknown-index.js.map +1 -0
  64. package/dist/dashboard/server/main.js +31 -3
  65. package/dist/dashboard/server/main.js.map +1 -1
  66. package/dist/dashboard-web/assets/index-Cay-QoI-.js +48 -0
  67. package/dist/dashboard-web/assets/{index-DbZ7PRHg.css → index-Upz8ZY12.css} +1 -1
  68. package/dist/dashboard-web/index.html +2 -2
  69. package/dist/infra/fs-skill-home.js +10 -0
  70. package/dist/infra/fs-skill-home.js.map +1 -1
  71. package/dist/infra/git-cli.js +139 -0
  72. package/dist/infra/git-cli.js.map +1 -1
  73. package/dist/infra/http-download-client.js +194 -0
  74. package/dist/infra/http-download-client.js.map +1 -0
  75. package/dist/infra/index.js +1 -0
  76. package/dist/infra/index.js.map +1 -1
  77. package/dist/infra/runtime.js +19 -1
  78. package/dist/infra/runtime.js.map +1 -1
  79. package/dist/shared/redact.js +1 -1
  80. package/dist/shared/redact.js.map +1 -1
  81. package/dist/shared/text-distance.js +31 -0
  82. package/dist/shared/text-distance.js.map +1 -0
  83. package/dist/shared/validation.js +23 -2
  84. package/dist/shared/validation.js.map +1 -1
  85. package/docs/CLI.md +69 -1
  86. package/package.json +12 -3
  87. package/skills/skills-manager/SKILL.md +111 -19
  88. package/dist/dashboard-web/assets/index-DSQV4D7B.js +0 -47
package/README.md CHANGED
@@ -4,6 +4,17 @@
4
4
 
5
5
  Manage your agent skills **from the conversation**: one `npx` command installs the manager skill, and from then on you just ask your agent — install, import, distribute, update, backfill provenance. Works with any skill-loading agent (Claude Code, Codex, Cursor, …).
6
6
 
7
+ > **CLI-first, conversation-driven.** This is `skills-manager-cli` — a terminal/npm tool with an optional local web dashboard. It is not a desktop app; if you were looking for the desktop application of the same name, that is a different project ([xingkongliang/skills-manager](https://github.com/xingkongliang/skills-manager)).
8
+
9
+ **What makes it different:**
10
+
11
+ - **One hub, every agent** — a single content library (`~/.skills-manager`) distributes any subset to any of the 70+ catalog agents, symlink or copy per target, with stale-copy detection and rollback.
12
+ - **Five source types** — git repos and URLs, direct download URLs (SKILL.md / zip / tar), local zip archives, Claude plugin marketplaces, and well-known discovery indexes.
13
+ - **Updates that don't cry wolf** — the update anchor is the git tree SHA of the skill's own subdirectory, so upstream commits outside the skill never light up an update.
14
+ - **The resident-cost ledger** — every distributed skill's frontmatter charges each message; `skills-manager cost` accounts it per runtime path (approximate, `≈`) with report-only recall suggestions. Undistributed = zero. See [ADR-0018](docs/adr/0018-resident-cost-ledger.md).
15
+ - **Provenance backfill** — imported skills without a source get one through lockfile-evidence adoption, agent-driven search, and your approval.
16
+ - **The manager skill** — every management action happens in agent conversation ([ADR-0014](docs/adr/0014-manager-skill-first-bootstrap.md)); the CLI underneath is a plain JSON engine.
17
+
7
18
  ## Quick Start
8
19
 
9
20
  ```sh
@@ -96,10 +107,11 @@ skills-manager provenance adopt # backfill lockfile evidence
96
107
  skills-manager categories set my-skill frontend # tag with a domain category
97
108
  skills-manager categories apply frontend -a claude-code # load only that domain
98
109
  skills-manager categories status # applied sets + drift
110
+ skills-manager cost # resident context-cost ledger (report-only recall suggestions)
99
111
  skills-manager archive old-skill
100
112
  ```
101
113
 
102
- Sources can be GitHub shorthand (`owner/repo`), Git URLs, GitHub tree URLs, or local paths.
114
+ Sources: GitHub shorthand (`owner/repo`), Git URLs, GitHub tree URLs, local paths, direct download URLs (single `SKILL.md` / zip / tar), local zip archives, plugin marketplaces (`.claude-plugin/marketplace.json`), and well-known discovery indexes — see the [CLI reference](docs/CLI.md).
103
115
 
104
116
  ## Optional: global install & dashboard
105
117
 
package/README.zh-CN.md CHANGED
@@ -4,6 +4,17 @@
4
4
 
5
5
  在**对话里**管理你的 agent skills:一条 `npx` 命令装上 manager skill,之后一切都直接吩咐你的 agent——安装、导入、分发、更新、补齐来源。任何会加载 skills 的 agent 都能用(Claude Code、Codex、Cursor、…)。
6
6
 
7
+ > **CLI 优先、对话驱动。** 本项目是 `skills-manager-cli`——终端/npm 工具,附带可选的本地 web dashboard。它**不是**桌面应用;如果你找的是同名的桌面版,那是另一个项目([xingkongliang/skills-manager](https://github.com/xingkongliang/skills-manager))。
8
+
9
+ **差异化在哪:**
10
+
11
+ - **一个库,所有 agent**——单一内容库(`~/.skills-manager`)把任意子集分发到 catalog 里的 70+ agent,按目标选软链接或拷贝,带副本过期检测与回滚。
12
+ - **五类源**——git 仓库与 URL、直下 URL(单个 `SKILL.md` / zip / tar)、本地 zip 归档、Claude 插件市场、well-known 发现索引。
13
+ - **更新不误报**——更新锚点是 skill 自身子目录的 git tree SHA,上游动了 skill 之外的文件不会点亮「可更新」。
14
+ - **常驻成本账本**——每个已分发 skill 的 frontmatter 都在按条消息计费;`skills-manager cost` 按运行时路径记账(近似口径,`≈` 展示)并给出只报告不执行的收回建议。未分发 = 零常驻。见 [ADR-0018](docs/adr/0018-resident-cost-ledger.md)。
15
+ - **来源可回填**——没有来源的导入 skill,经锁文件证据采纳、agent 搜索、你批准三层流程补齐 provenance。
16
+ - **manager skill**——所有管理动作都在 agent 对话里完成([ADR-0014](docs/adr/0014-manager-skill-first-bootstrap.md));底层 CLI 是纯 JSON 引擎。
17
+
7
18
  ## 快速开始
8
19
 
9
20
  ```sh
@@ -96,10 +107,11 @@ skills-manager provenance adopt # 补采锁文件证据
96
107
  skills-manager categories set my-skill 前端 # 打领域类别标签
97
108
  skills-manager categories apply 前端 -a claude-code # 只加载该领域
98
109
  skills-manager categories status # 已应用集合 + 漂移
110
+ skills-manager cost # 常驻上下文成本账本(只报告不执行的收回建议)
99
111
  skills-manager archive old-skill
100
112
  ```
101
113
 
102
- 来源(source)支持 GitHub 简写(`owner/repo`)、Git URL、GitHub tree URL 或本地路径。
114
+ 来源(source):GitHub 简写(`owner/repo`)、Git URL、GitHub tree URL、本地路径、直下 URL(单个 `SKILL.md` / zip / tar)、本地 zip 归档、插件市场(`.claude-plugin/marketplace.json`)、well-known 发现索引——详见 [CLI 参考](docs/CLI.zh-CN.md)。
103
115
 
104
116
  ## 可选:全局安装与 dashboard
105
117
 
@@ -0,0 +1,68 @@
1
+ import { formatApproxTokens } from '../core/services/cost-ledger-service.js';
2
+ /**
3
+ * Human rendering for `skills-manager cost` (spec context-cost, Q10 shape):
4
+ * path groups → per-skill lines → total + unmanaged line → the three
5
+ * suggestion layers. English per repo CLI convention; every number rides
6
+ * formatApproxTokens; every suggestion shows its verbatim command and nothing
7
+ * is ever executed.
8
+ */
9
+ export function renderCostLedger(ledger) {
10
+ const lines = [];
11
+ lines.push(`Resident context cost (${ledger.method}) — total ${formatApproxTokens(ledger.totalTokens)} tokens across ${ledger.paths.length} runtime path(s)`);
12
+ for (const group of ledger.paths) {
13
+ lines.push('');
14
+ const family = group.agents.length > 0 ? group.agents.join(', ') : 'none';
15
+ lines.push(`${group.runtimeDir} (${group.kind}, agents: ${family}) — ${formatApproxTokens(group.tokens)}`);
16
+ const width = Math.max(0, ...group.skills.map((line) => line.skill.length));
17
+ for (const line of group.skills) {
18
+ const marks = [line.archived ? '(archived)' : null, line.incomplete ? 'incomplete' : null].filter(Boolean).join(' ');
19
+ lines.push(` ${line.skill.padEnd(width)} ${formatApproxTokens(line.tokens)}${marks ? ` ${marks}` : ''}`);
20
+ }
21
+ }
22
+ lines.push('');
23
+ lines.push(`${ledger.unmanaged} unmanaged skill(s) not counted`);
24
+ if (ledger.errors.length > 0) {
25
+ lines.push('Errors (counted as 0):');
26
+ for (const error of ledger.errors)
27
+ lines.push(` ${error.skill} — ${error.runtimePath}: ${error.reason}`);
28
+ }
29
+ const suggestions = ledger.suggestions;
30
+ const empty = suggestions.archived.length === 0 && suggestions.topDescriptions.length === 0 && suggestions.scattered.length === 0;
31
+ lines.push('');
32
+ lines.push('Suggestions (report-only — nothing is executed):');
33
+ if (empty) {
34
+ lines.push(' none');
35
+ return `${lines.join('\n')}\n`;
36
+ }
37
+ if (suggestions.archived.length > 0) {
38
+ lines.push('Zero-controversy recalls (archived but still distributed):');
39
+ for (const item of suggestions.archived) {
40
+ lines.push(` ${item.skill} (archived) ${formatApproxTokens(item.tokens)} — ${item.runtimeDir}`);
41
+ lines.push(` ${item.command}`);
42
+ }
43
+ }
44
+ if (suggestions.topDescriptions.length > 0) {
45
+ const count = suggestions.topDescriptions.length;
46
+ lines.push(`Top ${count} most expensive description${count === 1 ? '' : 's'}:`);
47
+ for (const item of suggestions.topDescriptions) {
48
+ lines.push(` ${item.skill} ${formatApproxTokens(item.tokens)} — ${item.runtimeDir}`);
49
+ lines.push(` ${item.command}`);
50
+ }
51
+ }
52
+ if (suggestions.scattered.length > 0) {
53
+ lines.push('Scattered distributions (same skill on several paths — consolidate by recalling all but one):');
54
+ for (const item of suggestions.scattered) {
55
+ lines.push(` ${item.skill} on ${item.runtimeDirs.length} paths: ${item.runtimeDirs.join(', ')}`);
56
+ for (const command of item.commands)
57
+ lines.push(` ${command}`);
58
+ }
59
+ }
60
+ return `${lines.join('\n')}\n`;
61
+ }
62
+ /** The one-line resident-cost tail of a `preset apply` success (ADR-0019
63
+ * US20): ≈-prefixed char-approx tokens, the same magnitude convention as the
64
+ * ledger above. */
65
+ export function renderPresetCostLine(cost) {
66
+ return `Resident cost: ${formatApproxTokens(cost.tokens)} tokens per message (${cost.method})`;
67
+ }
68
+ //# sourceMappingURL=cost-output.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cost-output.js","sourceRoot":"","sources":["../../src/cli/cost-output.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAwC,MAAM,yCAAyC,CAAC;AAEnH;;;;;;GAMG;AACH,MAAM,UAAU,gBAAgB,CAAC,MAAkB;IACjD,MAAM,KAAK,GAAa,EAAE,CAAC;IAC3B,KAAK,CAAC,IAAI,CAAC,0BAA0B,MAAM,CAAC,MAAM,aAAa,kBAAkB,CAAC,MAAM,CAAC,WAAW,CAAC,kBAAkB,MAAM,CAAC,KAAK,CAAC,MAAM,kBAAkB,CAAC,CAAC;IAC9J,KAAK,MAAM,KAAK,IAAI,MAAM,CAAC,KAAK,EAAE,CAAC;QACjC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QACf,MAAM,MAAM,GAAG,KAAK,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC;QAC1E,KAAK,CAAC,IAAI,CAAC,GAAG,KAAK,CAAC,UAAU,KAAK,KAAK,CAAC,IAAI,aAAa,MAAM,OAAO,kBAAkB,CAAC,KAAK,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;QAC3G,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,GAAG,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC;QAC5E,KAAK,MAAM,IAAI,IAAI,KAAK,CAAC,MAAM,EAAE,CAAC;YAChC,MAAM,KAAK,GAAG,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,IAAI,EAAE,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;YACrH,KAAK,CAAC,IAAI,CAAC,KAAK,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,KAAK,kBAAkB,CAAC,IAAI,CAAC,MAAM,CAAC,GAAG,KAAK,CAAC,CAAC,CAAC,KAAK,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;QAC9G,CAAC;IACH,CAAC;IACD,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IACf,KAAK,CAAC,IAAI,CAAC,GAAG,MAAM,CAAC,SAAS,iCAAiC,CAAC,CAAC;IACjE,IAAI,MAAM,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAC7B,KAAK,CAAC,IAAI,CAAC,wBAAwB,CAAC,CAAC;QACrC,KAAK,MAAM,KAAK,IAAI,MAAM,CAAC,MAAM;YAAE,KAAK,CAAC,IAAI,CAAC,KAAK,KAAK,CAAC,KAAK,MAAM,KAAK,CAAC,WAAW,KAAK,KAAK,CAAC,MAAM,EAAE,CAAC,CAAC;IAC5G,CAAC;IAED,MAAM,WAAW,GAAG,MAAM,CAAC,WAAW,CAAC;IACvC,MAAM,KAAK,GAAG,WAAW,CAAC,QAAQ,CAAC,MAAM,KAAK,CAAC,IAAI,WAAW,CAAC,eAAe,CAAC,MAAM,KAAK,CAAC,IAAI,WAAW,CAAC,SAAS,CAAC,MAAM,KAAK,CAAC,CAAC;IAClI,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IACf,KAAK,CAAC,IAAI,CAAC,kDAAkD,CAAC,CAAC;IAC/D,IAAI,KAAK,EAAE,CAAC;QACV,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QACrB,OAAO,GAAG,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC;IACjC,CAAC;IACD,IAAI,WAAW,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACpC,KAAK,CAAC,IAAI,CAAC,4DAA4D,CAAC,CAAC;QACzE,KAAK,MAAM,IAAI,IAAI,WAAW,CAAC,QAAQ,EAAE,CAAC;YACxC,KAAK,CAAC,IAAI,CAAC,KAAK,IAAI,CAAC,KAAK,eAAe,kBAAkB,CAAC,IAAI,CAAC,MAAM,CAAC,MAAM,IAAI,CAAC,UAAU,EAAE,CAAC,CAAC;YACjG,KAAK,CAAC,IAAI,CAAC,OAAO,IAAI,CAAC,OAAO,EAAE,CAAC,CAAC;QACpC,CAAC;IACH,CAAC;IACD,IAAI,WAAW,CAAC,eAAe,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAC3C,MAAM,KAAK,GAAG,WAAW,CAAC,eAAe,CAAC,MAAM,CAAC;QACjD,KAAK,CAAC,IAAI,CAAC,OAAO,KAAK,8BAA8B,KAAK,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,GAAG,CAAC,CAAC;QAChF,KAAK,MAAM,IAAI,IAAI,WAAW,CAAC,eAAe,EAAE,CAAC;YAC/C,KAAK,CAAC,IAAI,CAAC,KAAK,IAAI,CAAC,KAAK,IAAI,kBAAkB,CAAC,IAAI,CAAC,MAAM,CAAC,MAAM,IAAI,CAAC,UAAU,EAAE,CAAC,CAAC;YACtF,KAAK,CAAC,IAAI,CAAC,OAAO,IAAI,CAAC,OAAO,EAAE,CAAC,CAAC;QACpC,CAAC;IACH,CAAC;IACD,IAAI,WAAW,CAAC,SAAS,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACrC,KAAK,CAAC,IAAI,CAAC,+FAA+F,CAAC,CAAC;QAC5G,KAAK,MAAM,IAAI,IAAI,WAAW,CAAC,SAAS,EAAE,CAAC;YACzC,KAAK,CAAC,IAAI,CAAC,KAAK,IAAI,CAAC,KAAK,OAAO,IAAI,CAAC,WAAW,CAAC,MAAM,WAAW,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;YAClG,KAAK,MAAM,OAAO,IAAI,IAAI,CAAC,QAAQ;gBAAE,KAAK,CAAC,IAAI,CAAC,OAAO,OAAO,EAAE,CAAC,CAAC;QACpE,CAAC;IACH,CAAC;IACD,OAAO,GAAG,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC;AACjC,CAAC;AAED;;oBAEoB;AACpB,MAAM,UAAU,oBAAoB,CAAC,IAAoB;IACvD,OAAO,kBAAkB,kBAAkB,CAAC,IAAI,CAAC,MAAM,CAAC,wBAAwB,IAAI,CAAC,MAAM,GAAG,CAAC;AACjG,CAAC"}
@@ -0,0 +1,20 @@
1
+ import readline from 'node:readline/promises';
2
+ /**
3
+ * The plain-http confirmation (spec source-formats, US-12): an http:// URL
4
+ * source downloads only when the user says so — `--yes` on the command line
5
+ * or an interactive y/N. A non-interactive session (agent context, ADR-0014)
6
+ * declines: the core's insecure_http_unconfirmed error then names the remedy.
7
+ */
8
+ export async function confirmInsecureHttp(url) {
9
+ if (!process.stdin.isTTY || !process.stdout.isTTY)
10
+ return false;
11
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
12
+ try {
13
+ const answer = (await rl.question(`Download ${url} over unencrypted http? [y/N] `)).trim().toLowerCase();
14
+ return answer === 'y' || answer === 'yes';
15
+ }
16
+ finally {
17
+ rl.close();
18
+ }
19
+ }
20
+ //# sourceMappingURL=insecure-http-confirm.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"insecure-http-confirm.js","sourceRoot":"","sources":["../../src/cli/insecure-http-confirm.ts"],"names":[],"mappings":"AAAA,OAAO,QAAQ,MAAM,wBAAwB,CAAC;AAE9C;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,mBAAmB,CAAC,GAAW;IACnD,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,KAAK,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,KAAK;QAAE,OAAO,KAAK,CAAC;IAChE,MAAM,EAAE,GAAG,QAAQ,CAAC,eAAe,CAAC,EAAE,KAAK,EAAE,OAAO,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IACtF,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,YAAY,GAAG,gCAAgC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;QACzG,OAAO,MAAM,KAAK,GAAG,IAAI,MAAM,KAAK,KAAK,CAAC;IAC5C,CAAC;YAAS,CAAC;QACT,EAAE,CAAC,KAAK,EAAE,CAAC;IACb,CAAC;AACH,CAAC"}
package/dist/cli/main.js CHANGED
@@ -2,13 +2,18 @@
2
2
  import { createRequire } from 'node:module';
3
3
  import { Command } from 'commander';
4
4
  import { createRuntimeServices, projectRootFromImportMeta } from '../infra/runtime.js';
5
- import { normalizeGitSourceUrl } from '../core/services/source-service.js';
5
+ import { isInsecureHttpSource, normalizeGitSourceUrl } from '../core/services/source-service.js';
6
+ import { isUpdatableSourceType } from '../core/services/update-service.js';
7
+ import { parseFormatFlag } from '../core/services/url-payload.js';
8
+ import { confirmInsecureHttp } from './insecure-http-confirm.js';
6
9
  import { SkillsManagerError } from '../shared/errors.js';
7
10
  import { redactCheckout, redactDiscovered } from '../shared/redact.js';
8
11
  import { NodeFileSystem } from '../infra/fs-skill-home.js';
9
12
  import { GitHubApiClient } from '../infra/github-api-client.js';
13
+ import { HttpDownloadClient } from '../infra/http-download-client.js';
10
14
  import { DetectionService, detectionLogPath } from '../core/services/detection-service.js';
11
15
  import { runBootstrap, managerSkillBundle } from './bootstrap.js';
16
+ import { renderCostLedger, renderPresetCostLine } from './cost-output.js';
12
17
  const pkg = createRequire(import.meta.url)('../../package.json');
13
18
  const projectRoot = projectRootFromImportMeta(import.meta.url);
14
19
  function services(cmd, options = {}) {
@@ -206,6 +211,18 @@ program.command('edit')
206
211
  });
207
212
  // Domain categories: a free-form multi-valued registry axis, orthogonal to the
208
213
  // frozen legacy `category` (ADR-0015). Tag edits never touch runtime dirs.
214
+ /** The applied-set column for `categories status`; a preset stamp rides beside the categories (US22), an unstamped path reads exactly as before. */
215
+ function formatAppliedSet(applied) {
216
+ if (applied === null)
217
+ return 'no filter (no category set applied)';
218
+ if ('all' in applied)
219
+ return 'no filter (--all applied)';
220
+ return `categories: ${applied.categories.join(', ')}${applied.preset ? ` (preset: ${applied.preset})` : ''}`;
221
+ }
222
+ /** Shared drift phrasing so `categories status` and `preset list` never diverge — honest, never implied healthy (US6). */
223
+ function driftNote(drift) {
224
+ return `${drift.length} skill(s) match the set but are undistributed: ${drift.join(', ')}`;
225
+ }
209
226
  const categories = program.command('categories').description('Tag skills with domain categories and inspect the hub vocabulary');
210
227
  categories.command('set')
211
228
  .description('Replace the whole category list for a skill (no categories given = clear)')
@@ -274,12 +291,9 @@ categories.command('status')
274
291
  }
275
292
  for (const item of paths) {
276
293
  const agents = item.agents.length > 0 ? item.agents.join(', ') : 'none';
277
- const set = item.applied === null
278
- ? 'no filter (no category set applied)'
279
- : 'all' in item.applied ? 'no filter (--all applied)' : `categories: ${item.applied.categories.join(', ')}`;
280
- console.log(`${item.runtimeDir} (agents: ${agents}) — ${set}`);
294
+ console.log(`${item.runtimeDir} (agents: ${agents}) — ${formatAppliedSet(item.applied)}`);
281
295
  if (item.drift.length > 0) {
282
- console.log(` drift: ${item.drift.length} skill(s) match the set but are undistributed: ${item.drift.join(', ')}`);
296
+ console.log(` drift: ${driftNote(item.drift)}`);
283
297
  const rerun = item.applied !== null && 'all' in item.applied
284
298
  ? 'skills-manager categories apply --all'
285
299
  : `skills-manager categories apply ${item.applied?.categories.join(' ')}`;
@@ -287,16 +301,80 @@ categories.command('status')
287
301
  }
288
302
  }
289
303
  });
290
- const provenance = program.command('provenance').description('Backfill provenance for source-less skills (lockfile evidence adoption, ADR-0011/0012)');
304
+ // Named presets: a saved category list — the re-appliable loading-set档位
305
+ // (ADR-0019). Storage only; switching runtime dirs is `preset apply`'s job.
306
+ const preset = program.command('preset').description('Manage named lists of domain categories (re-appliable agent loading sets, ADR-0019)');
307
+ preset.command('set')
308
+ .description('Replace the whole member list of a preset (overwrite-in-place when the name exists; categories need not exist in the hub yet)')
309
+ .argument('<name>')
310
+ .argument('<category...>')
311
+ .action((name, values, _opts, cmd) => {
312
+ const s = services(cmd);
313
+ const result = s.registry.setPreset(name, values);
314
+ s.activity.record({ action: 'cli-preset-set', summary: `set preset ${name}`, details: { preset: name, categories: values } });
315
+ print(result);
316
+ });
317
+ preset.command('list')
318
+ .description('List every preset with its member categories and mount footprint (drift on mounted paths reported as-is)')
319
+ .action((_opts, cmd) => {
320
+ const s = services(cmd);
321
+ const presets = s.registry.listPresets();
322
+ if (presets.length === 0) {
323
+ console.log('No presets yet — save one with `preset set <name> <category...>`.');
324
+ return;
325
+ }
326
+ // Mount footprint (US5/US6): which physical runtime paths currently carry
327
+ // each preset — zero mounts stay silent, and a drifted mount is reported,
328
+ // never implied healthy. Paths, not resolved-skill counts.
329
+ const statusPaths = s.distribute.categorySetStatus().paths;
330
+ for (const { name, categories } of presets) {
331
+ console.log(`${name}: ${categories.join(', ')}`);
332
+ const mounts = statusPaths.filter((item) => item.applied !== null && 'categories' in item.applied && item.applied.preset === name);
333
+ if (mounts.length === 0)
334
+ continue;
335
+ console.log(` mounted on ${mounts.length} runtime path(s): ${mounts.map((item) => item.runtimeDir).join(', ')}`);
336
+ for (const item of mounts) {
337
+ if (item.drift.length === 0)
338
+ continue;
339
+ console.log(` drift on ${item.runtimeDir}: ${driftNote(item.drift)}`);
340
+ }
341
+ }
342
+ });
343
+ preset.command('remove')
344
+ .description('Delete a preset and cascade-clear its name off every category-set record referencing it (applied categories kept, runtime untouched); reports how many paths the name was detached from')
345
+ .argument('<name>')
346
+ .action((name, _opts, cmd) => {
347
+ const s = services(cmd);
348
+ const { preset, detached } = s.distribute.removePresetCascade(name);
349
+ s.activity.record({ action: 'cli-preset-remove', summary: `removed preset ${name}, detached from ${detached} runtime path(s)`, details: { preset: name, detached } });
350
+ console.log(`Removed preset ${name} [${preset.categories.join(', ')}] — detached the name from ${detached} runtime path(s)`);
351
+ });
352
+ preset.command('apply')
353
+ .description("Rewrite the selected agents' runtime dirs to exactly the skills in the preset's categories (same strict semantics as `categories apply`: manager skill exempt, foreign untouched, uncategorized removed) and stamp the record with the preset name; success ends with the preset's resident-cost line")
354
+ .argument('<name>')
355
+ .option('-a, --agent <id...>', 'catalog agent ids (repeatable); defaults to the detected set')
356
+ .option('--json', 'machine-readable output for agents')
357
+ .action((name, opts, cmd) => {
358
+ const s = services(cmd);
359
+ const result = s.distribute.applyPreset(name, opts.agent);
360
+ const cost = s.cost.pathCost(result.paths.map((item) => item.runtimeDir));
361
+ const summary = `Applied preset ${name} [${result.categories.join(', ')}] to ${result.paths.length} runtime path(s)`;
362
+ s.activity.record({ action: 'cli-preset-apply', summary, details: { preset: name, categories: result.categories, agents: result.agents, costTokens: cost.tokens } });
363
+ if (opts.json)
364
+ return print({ ...result, cost });
365
+ console.log(summary);
366
+ console.log(renderPresetCostLine(cost));
367
+ });
368
+ const provenance = program.command('provenance').description('Backfill provenance for source-less skills (frontmatter & lockfile evidence adoption, ADR-0011/0012/0017)');
291
369
  provenance.command('adopt')
292
- .description('Adopt npx skills lockfile evidence onto legacy imported skills that have no source yet')
370
+ .description('Adopt frontmatter/lockfile evidence onto legacy imported skills that have no source yet')
293
371
  .option('--dry-run', 'print what would be adopted without touching the registry')
294
372
  .option('-s, --skill <skill...>', 'limit adoption to specific skills')
295
373
  .action((opts, cmd) => {
296
374
  const s = services(cmd);
297
375
  const result = s.provenance.adopt({ dryRun: Boolean(opts.dryRun), skills: opts.skill });
298
376
  if (!opts.dryRun)
299
- s.activity.record({ action: 'cli-provenance-adopt', summary: `Adopted lockfile evidence for ${result.adopted.map((item) => item.skill).join(', ') || 'nothing'}`, details: { adopted: result.adopted.map((item) => item.skill), skipped: result.skipped } });
377
+ s.activity.record({ action: 'cli-provenance-adopt', summary: `Adopted provenance evidence for ${result.adopted.map((item) => item.skill).join(', ') || 'nothing'}`, details: { adopted: result.adopted.map((item) => item.skill), skipped: result.skipped } });
300
378
  print(result);
301
379
  });
302
380
  provenance.command('list')
@@ -313,6 +391,108 @@ provenance.command('list')
313
391
  for (const skill of pending.locallyAuthored)
314
392
  console.log(` ${skill}`);
315
393
  });
394
+ // Hub git-ification for multi-machine sync (ADR-0020): each command thin, git
395
+ // stays the merge tool, and the operator keeps every git decision.
396
+ const sync = program.command('sync').description('Multi-machine sync via a git-ified hub (init/push/pull/status, ADR-0020)');
397
+ sync.command('init')
398
+ .description('Turn the hub into a git repository (or adopt an existing one) with the canonical .gitignore and a baseline commit; optionally attach a remote')
399
+ .option('--remote <url>', 'git remote URL to attach as origin (a local bare repo works too)')
400
+ .action((opts, cmd) => {
401
+ const s = services(cmd);
402
+ const result = s.sync.init({ remote: opts.remote });
403
+ s.activity.record({ action: 'cli-sync-init', summary: `${result.mode} hub git-ification`, details: result });
404
+ // Zero-omission report (US-4): every dimension states what happened, including "nothing".
405
+ console.log(result.mode === 'adopted' ? 'adopted: existing git repository kept (history preserved)' : 'initialized: fresh git repository created');
406
+ console.log(result.ignoreLinesAdded.length > 0
407
+ ? `.gitignore: appended ${result.ignoreLinesAdded.length} line(s): ${result.ignoreLinesAdded.join(', ')}`
408
+ : '.gitignore: nothing to append (canonical lines already present)');
409
+ console.log(result.baselineCommit ? `baseline commit: ${result.baselineCommit}` : 'baseline commit: none (tree already clean)');
410
+ if (result.remote === null)
411
+ console.log('remote origin: not set (pass --remote <url> to attach one)');
412
+ else
413
+ console.log(result.remote.attached ? `remote origin: attached ${result.remote.url}` : `remote origin: already ${result.remote.url}`);
414
+ console.log('Reminder: the hub will be pushed to this remote — confirm it contains no secrets (tokens, keys, .env files).');
415
+ });
416
+ sync.command('status')
417
+ .description('Read-only sync status: git-ification, remote, dirty files, ahead/behind (last fetch), last sync commit — zero network')
418
+ .option('--json', 'machine-readable output for agents')
419
+ .action((opts, cmd) => {
420
+ const result = services(cmd).sync.status();
421
+ if (opts.json)
422
+ return print(result);
423
+ // Read-only probe: no activity record — status itself must not dirty the hub.
424
+ console.log(`hub: ${result.home}`);
425
+ if (!result.gitified) {
426
+ console.log(`git: no — ${result.hint}`);
427
+ return;
428
+ }
429
+ console.log('git: yes');
430
+ console.log(result.remote === null
431
+ ? 'remote origin: not configured (attach one with `sync init --remote <url>`)'
432
+ : `remote origin: ${result.remote}`);
433
+ console.log(result.dirtyFiles === 0 ? 'dirty files: 0 (clean tree)' : `dirty files: ${result.dirtyFiles}`);
434
+ console.log(result.aheadBehind === null
435
+ ? 'ahead/behind: unknown (no upstream tracking ref yet — set by your first push)'
436
+ : `ahead/behind: ahead ${result.aheadBehind.ahead}, behind ${result.aheadBehind.behind} (based on last fetch — run \`git -C ${result.home} fetch\` to refresh)`);
437
+ console.log(result.lastCommit === null
438
+ ? 'last sync commit: none yet (the hub repo has no commits)'
439
+ : `last sync commit: ${result.lastCommit.sha} ${result.lastCommit.message}`);
440
+ });
441
+ sync.command('push')
442
+ .description('Stage all changes, make one summary commit, and push to the remote (no empty commits)')
443
+ .action((_opts, cmd) => {
444
+ const s = services(cmd);
445
+ const result = s.sync.push();
446
+ // Honest summary: an in-sync push sent nothing, and the log must not claim one did.
447
+ s.activity.record({
448
+ action: 'cli-sync-push',
449
+ summary: result.upToDate ? 'Sync push: already in sync, nothing sent' : `Pushed hub to ${result.remote}`,
450
+ details: { commit: result.commit?.sha ?? null, upToDate: result.upToDate },
451
+ });
452
+ if (result.upToDate) {
453
+ console.log('already in sync: no changes to commit, remote is up to date');
454
+ return;
455
+ }
456
+ console.log(result.commit === null
457
+ ? 'commit: none (working tree clean — pushed existing local commits)'
458
+ : `commit: ${result.commit.sha} ${result.commit.message}`);
459
+ console.log(`pushed to origin: ${result.remote}`);
460
+ });
461
+ sync.command('pull')
462
+ .description('Fetch and merge the remote hub (merge commits allowed; a dirty tree is refused, never stashed; user-data conflicts are yours to resolve — only the tool-generated empty registry placeholder is reconciled)')
463
+ .action((_opts, cmd) => {
464
+ const s = services(cmd);
465
+ const result = s.sync.pull();
466
+ s.activity.record({
467
+ action: 'cli-sync-pull',
468
+ summary: result.upToDate ? 'Sync pull: already up to date' : `Pulled hub from ${result.remote}`,
469
+ details: { upToDate: result.upToDate, registryPlaceholderSuperseded: result.registryPlaceholderSuperseded, ...result.stats },
470
+ });
471
+ if (result.upToDate) {
472
+ console.log('already up to date: nothing to merge from the remote');
473
+ return;
474
+ }
475
+ console.log(`pulled from origin: ${result.remote}`);
476
+ const { added, updated, removed } = result.stats;
477
+ const segments = [
478
+ ...(added.length > 0 ? [`${added.length} added (${added.join(', ')})`] : []),
479
+ ...(updated.length > 0 ? [`${updated.length} updated (${updated.join(', ')})`] : []),
480
+ ...(removed.length > 0 ? [`${removed.length} removed (${removed.join(', ')})`] : []),
481
+ ];
482
+ console.log(segments.length > 0 ? `merged skills: ${segments.join(', ')}` : 'merged skills: none changed');
483
+ console.log(result.stats.registryChanged ? 'registry: changed' : 'registry: unchanged');
484
+ if (result.registryPlaceholderSuperseded !== null) {
485
+ // Zero-omission (ticket 07): the merge clashed on registry.yaml and the
486
+ // tool settled it because the local side was provably its own init
487
+ // placeholder — say so plainly.
488
+ console.log('registry conflict: resolved by keeping the remote registry (the local side was the empty init placeholder, not user data)');
489
+ }
490
+ // Plain text, on purpose (US-14): the follow-up stays the operator's call —
491
+ // nothing is detected, distributed, or marked.
492
+ if (added.length > 0) {
493
+ console.log('Newly pulled skills are not distributed yet — run `distribute --skill <name>` when you want them available to your agents.');
494
+ }
495
+ });
316
496
  const backup = program.command('backup').description('Inspect and restore init backups (hub .backups/, 30-day retention)');
317
497
  backup.command('list')
318
498
  .description('List saved backups')
@@ -359,30 +539,87 @@ program.command('list')
359
539
  name: row.name,
360
540
  title: row.title,
361
541
  category: row.category,
362
- updatable: Boolean(row.source?.url && row.source?.subpath),
542
+ updatable: Boolean(row.source?.url && row.source?.subpath && isUpdatableSourceType(row.source.type)),
363
543
  archived: Boolean(row.archived),
364
544
  })));
365
545
  });
546
+ // The reference layer (ADR-0017): a zero-retention read channel. Raw stdout
547
+ // under the `status` human-readable precedent — never JSON `print` — so agents
548
+ // can pipe the skill body; the archived notice rides stderr to keep stdout
549
+ // byte-exact. No borrow/cleanup lifecycle.
550
+ program.command('get')
551
+ .description("Print a hub skill's full SKILL.md to stdout (zero-retention read; provenance evidence rides the frontmatter)")
552
+ .argument('<skill>')
553
+ .option('--path', 'print the canonical hub directory (absolute) for read-only borrowing of sibling files, instead of the file body')
554
+ .action((skill, opts, cmd) => {
555
+ const s = services(cmd);
556
+ const target = s.get.resolve(skill);
557
+ const archivedNotice = `Note: ${skill} is archived — still readable; it only left the management plane (updates, distribution).`;
558
+ if (opts.path) {
559
+ console.log(target.dir);
560
+ console.log('Read-only borrow: do not write into this directory — the hub is canonical; changes go through skills-manager commands.');
561
+ if (target.archived)
562
+ console.log(archivedNotice);
563
+ return;
564
+ }
565
+ if (target.archived)
566
+ console.error(archivedNotice);
567
+ process.stdout.write(s.get.read(target));
568
+ });
569
+ // The resident-cost ledger (ADR-0018): a read-only account grouped by physical
570
+ // runtime path. Suggestions are report-only — each names the verbatim
571
+ // undistribute command and never runs it. No filter flags in v1: consumers
572
+ // filter the `--json` structure themselves.
573
+ program.command('cost')
574
+ .description('Show the resident context-cost ledger: per-runtime-path token cost of distributed skill frontmatter, with report-only recall suggestions')
575
+ .option('--json', 'machine-readable output for agents')
576
+ .option('--top <n>', 'how many of the most expensive descriptions to list', '5')
577
+ .action((opts, cmd) => {
578
+ const s = services(cmd);
579
+ const top = Number.parseInt(opts.top, 10);
580
+ if (!Number.isInteger(top) || top < 0)
581
+ throw new Error(`--top expects a non-negative integer, got "${opts.top}"`);
582
+ const ledger = s.cost.ledger(top);
583
+ if (opts.json)
584
+ return print(ledger);
585
+ process.stdout.write(renderCostLedger(ledger));
586
+ });
366
587
  program.command('add')
367
588
  .description('Discover from a source, then install selected skills')
368
- .argument('<source>', 'Git URL, GitHub owner/repo, GitHub tree URL, or local path')
589
+ .argument('<source>', 'Git URL, GitHub owner/repo, GitHub tree URL, local path, local .zip archive, or http(s) URL of a single SKILL.md / zip / tar archive')
369
590
  .option('--list', 'only list discovered skills')
370
591
  .option('--all', 'install all discovered skills')
371
592
  .option('-s, --skill <skill...>', 'skill name or source subpath to install')
372
- .option('-y, --yes', 'overwrite existing skills without prompting')
373
- .action((source, opts, cmd) => {
593
+ .option('-f, --format <format>', 'how to interpret a url payload: md, zip, or tar (tar covers tar/tar.gz/tgz) — the escape hatch when extension and Content-Type identify nothing')
594
+ .option('-y, --yes', 'overwrite existing skills without prompting; also confirms plain-http URL downloads')
595
+ .action(async (source, opts, cmd) => {
374
596
  const s = services(cmd);
597
+ const format = parseFormatFlag(opts.format);
598
+ // A plain-http url source downloads only with explicit confirmation
599
+ // (US-12): --yes on the command line or an interactive y/N — https and
600
+ // every other source kind needs none.
601
+ const allowInsecureHttp = isInsecureHttpSource(s.source.normalize(source))
602
+ ? Boolean(opts.yes) || await confirmInsecureHttp(source)
603
+ : false;
375
604
  if (opts.list) {
376
605
  return s.source.withCheckout(source, undefined, (checkout) => {
377
606
  const discovered = s.source.discover(checkout);
378
- return print({ source: redactCheckout(checkout), discovered: discovered.map(redactDiscovered) });
379
- });
607
+ // Marketplace sources present the two-level plugin → skills structure
608
+ // (US-15/17): every manifest plugin shows either its skills or why it
609
+ // cannot be consumed. Ordinary sources have no second level.
610
+ const view = s.source.marketplaceView(checkout, discovered);
611
+ return print({
612
+ source: redactCheckout(checkout),
613
+ ...(view ? { plugins: view.plugins } : {}),
614
+ discovered: discovered.map(redactDiscovered),
615
+ });
616
+ }, { allowInsecureHttp, format });
380
617
  }
381
618
  if (!opts.all && (opts.skill || []).length === 0)
382
619
  throw new Error('Use --all or --skill <name-or-subpath> to choose skills in this non-interactive CLI.');
383
620
  // Empty selectors mean "everything discovered" — the --all semantics.
384
621
  const selectors = opts.all ? [] : (opts.skill || []);
385
- const result = s.install.installFromSourceSelection({ source, selectors, overwrite: Boolean(opts.yes) });
622
+ const result = s.install.installFromSourceSelection({ source, selectors, overwrite: Boolean(opts.yes), allowInsecureHttp, format });
386
623
  s.activity.record({ action: 'cli-add', summary: `Installed ${result.installed.join(', ')}`, details: { source, installed: result.installed } });
387
624
  print({ ...result, plan: { ...result.plan, source: redactCheckout(result.plan.source), selected: result.plan.selected.map(redactDiscovered) } });
388
625
  remindStaleTargets(s);
@@ -396,7 +633,7 @@ program.command('update')
396
633
  .action(async (opts, cmd) => {
397
634
  const s = services(cmd);
398
635
  if (opts.check) {
399
- const detection = new DetectionService({ githubApi: new GitHubApiClient(), fs: new NodeFileSystem() });
636
+ const detection = new DetectionService({ githubApi: new GitHubApiClient(), http: new HttpDownloadClient(), fs: new NodeFileSystem() });
400
637
  const listed = s.registry.listSkills({ includeArchived: false });
401
638
  const outcomes = await detection.detect(s, listed);
402
639
  const detectionLog = detectionLogPath(s.resolution.root);
@@ -532,8 +769,8 @@ program.command('migrate-consumers')
532
769
  print(result);
533
770
  });
534
771
  program.command('rebuild-collections').description('Regenerate category collections').action((_opts, cmd) => { services(cmd).views.rebuildCollections(); print('collections rebuilt'); });
535
- program.command('archive').description('Archive canonical skills without permanent deletion').argument('<skills...>').action((skills, cmd) => print(services(cmd).archive.archiveSkills(skills)));
536
- program.command('adopt').description('Adopt a real directory from a generated view into canonical skills').argument('<view>').argument('<skill>').argument('[alsoConsumers...]').action((view, skill, alsoConsumers, cmd) => print(services(cmd).adopt.adopt(view, skill, alsoConsumers || [])));
772
+ program.command('archive').description('Archive canonical skills without permanent deletion').argument('<skills...>').action((skills, _opts, cmd) => print(services(cmd).archive.archiveSkills(skills)));
773
+ program.command('adopt').description('Adopt a real directory from a generated view into canonical skills').argument('<view>').argument('<skill>').argument('[alsoConsumers...]').action((view, skill, alsoConsumers, _opts, cmd) => print(services(cmd).adopt.adopt(view, skill, alsoConsumers || [])));
537
774
  program.parseAsync(process.argv).catch((error) => {
538
775
  console.error(error instanceof Error ? error.message : String(error));
539
776
  process.exitCode = 1;