skills-manager-cli 2026.9.15 → 2026.10.26
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 +54 -1
- package/README.zh-CN.md +54 -1
- package/dist/cli/cost-output.js +112 -0
- package/dist/cli/cost-output.js.map +1 -0
- package/dist/cli/main.js +253 -8
- package/dist/cli/main.js.map +1 -1
- package/dist/core/ports/filesystem.js +5 -1
- package/dist/core/ports/filesystem.js.map +1 -1
- package/dist/core/ports/security-scanner.js +2 -0
- package/dist/core/ports/security-scanner.js.map +1 -0
- package/dist/core/services/cost-ledger-service.js +214 -0
- package/dist/core/services/cost-ledger-service.js.map +1 -0
- package/dist/core/services/detection-service.js +25 -40
- package/dist/core/services/detection-service.js.map +1 -1
- package/dist/core/services/distribute-service.js +58 -4
- package/dist/core/services/distribute-service.js.map +1 -1
- package/dist/core/services/doctor-service.js +13 -2
- package/dist/core/services/doctor-service.js.map +1 -1
- package/dist/core/services/frontmatter-evidence.js +99 -0
- package/dist/core/services/frontmatter-evidence.js.map +1 -0
- package/dist/core/services/frontmatter-mirror.js +151 -0
- package/dist/core/services/frontmatter-mirror.js.map +1 -0
- package/dist/core/services/get-service.js +55 -0
- package/dist/core/services/get-service.js.map +1 -0
- package/dist/core/services/index.js +26 -5
- package/dist/core/services/index.js.map +1 -1
- package/dist/core/services/init-service.js +15 -5
- package/dist/core/services/init-service.js.map +1 -1
- package/dist/core/services/install-service.js +15 -1
- package/dist/core/services/install-service.js.map +1 -1
- package/dist/core/services/migration-service.js +9 -4
- package/dist/core/services/migration-service.js.map +1 -1
- package/dist/core/services/provenance-service.js +14 -9
- package/dist/core/services/provenance-service.js.map +1 -1
- package/dist/core/services/registry-service.js +92 -7
- package/dist/core/services/registry-service.js.map +1 -1
- package/dist/core/services/security-scan.js +156 -0
- package/dist/core/services/security-scan.js.map +1 -0
- package/dist/core/services/skill-home-service.js +5 -1
- package/dist/core/services/skill-home-service.js.map +1 -1
- package/dist/core/services/sync-service.js +422 -0
- package/dist/core/services/sync-service.js.map +1 -0
- package/dist/core/services/tree-content-hash.js +38 -0
- package/dist/core/services/tree-content-hash.js.map +1 -0
- package/dist/dashboard/server/main.js +4 -0
- package/dist/dashboard/server/main.js.map +1 -1
- package/dist/dashboard-web/assets/index-Cay-QoI-.js +48 -0
- package/dist/dashboard-web/assets/{index-DbZ7PRHg.css → index-Upz8ZY12.css} +1 -1
- package/dist/dashboard-web/index.html +2 -2
- package/dist/infra/git-cli.js +139 -0
- package/dist/infra/git-cli.js.map +1 -1
- package/dist/infra/runtime.js +18 -1
- package/dist/infra/runtime.js.map +1 -1
- package/dist/shared/text-distance.js +31 -0
- package/dist/shared/text-distance.js.map +1 -0
- package/dist/shared/validation.js +23 -2
- package/dist/shared/validation.js.map +1 -1
- package/docs/CLI.md +88 -1
- package/package.json +12 -3
- package/skills/skills-manager/SKILL.md +124 -19
- package/dist/dashboard-web/assets/index-Ks0scTAG.js +0 -47
package/README.md
CHANGED
|
@@ -1,15 +1,53 @@
|
|
|
1
1
|
# Skills Manager
|
|
2
2
|
|
|
3
|
+
[](https://www.npmjs.com/package/skills-manager-cli)
|
|
4
|
+
[](https://www.npmjs.com/package/skills-manager-cli)
|
|
5
|
+
[](https://github.com/shenysun/skills-manager/stargazers)
|
|
6
|
+
[](LICENSE)
|
|
7
|
+
[](https://nodejs.org)
|
|
8
|
+
|
|
3
9
|
**English** | [简体中文](README.zh-CN.md)
|
|
4
10
|
|
|
11
|
+
|
|
5
12
|
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
13
|
|
|
14
|
+
> **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)).
|
|
15
|
+
|
|
16
|
+
**What makes it different:**
|
|
17
|
+
|
|
18
|
+
- **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.
|
|
19
|
+
- **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.
|
|
20
|
+
- **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.
|
|
21
|
+
- **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).
|
|
22
|
+
- **Provenance backfill** — imported skills without a source get one through lockfile-evidence adoption, agent-driven search, and your approval.
|
|
23
|
+
- **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.
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
## Why this instead of `gh skill` / `npx skills` / the desktop app?
|
|
27
|
+
|
|
28
|
+
| | **skills-manager** | `gh skill` | `npx skills` | [desktop skills-manager](https://github.com/xingkongliang/skills-manager) |
|
|
29
|
+
|---|---|---|---|---|
|
|
30
|
+
| Primary UI | **Agent conversation** (manager skill) + CLI | GitHub CLI | CLI | Desktop GUI |
|
|
31
|
+
| Hub model | Single `~/.skills-manager` → distribute subset | Per-agent / shared `.agents/skills` | Per install | Hub + presets |
|
|
32
|
+
| Agents covered | **70+** (catalog snapshot) | ~48 | Depends on target | ~54 |
|
|
33
|
+
| Non-GitHub sources | git / URL / zip / marketplace / well-known | GitHub (+ `--from-local`) | GitHub-centric | Marketplace + git |
|
|
34
|
+
| Update signal | **Skill subtree tree-SHA** (no false positives) | Frontmatter tree SHA | Lockfile / metadata | App sync |
|
|
35
|
+
| Context cost | Built-in **resident cost ledger** | — | — | — |
|
|
36
|
+
| Multi-machine | Hub git sync (`sync push/pull`) | — | — | Device-code backup |
|
|
37
|
+
| Install | `npx skills-manager-cli` | `gh extension` | `npx skills` | Desktop download |
|
|
38
|
+
|
|
39
|
+
Pick this when you want **one library, many agents, conversation-driven ops**, and honest update/cost signals — not another one-shot installer.
|
|
40
|
+
|
|
7
41
|
## Quick Start
|
|
8
42
|
|
|
9
43
|
```sh
|
|
10
44
|
npx skills-manager-cli
|
|
11
45
|
```
|
|
12
46
|
|
|
47
|
+

|
|
48
|
+
|
|
49
|
+
*`--help` and the resident context-cost ledger (`cost`) in action.*
|
|
50
|
+
|
|
13
51
|
That is the whole setup. Bootstrap creates the skill hub (`~/.skills-manager`), installs the **manager skill** into it from the bundled copy, and mounts it (symlink) into the agents it detects on your machine — you confirm the list in the one prompt it shows.
|
|
14
52
|
|
|
15
53
|
Then go back to your agent and say:
|
|
@@ -22,6 +60,20 @@ Then go back to your agent and say:
|
|
|
22
60
|
|
|
23
61
|
Content moves into the hub, originals become symlinks (backed up first). Prefer to preview it yourself first? `skills-manager init --dry-run`.
|
|
24
62
|
|
|
63
|
+
|
|
64
|
+
## Coming from `npx skills` or `gh skill`?
|
|
65
|
+
|
|
66
|
+
You can keep installing with those tools. To consolidate into one hub (tree-SHA updates + the cost ledger) without re-downloading:
|
|
67
|
+
|
|
68
|
+
```sh
|
|
69
|
+
npx skills-manager-cli # bootstrap the hub + manager skill
|
|
70
|
+
skills-manager init --dry-run # preview importing existing runtime skills
|
|
71
|
+
# or ask your agent: Import my existing skills into skills-manager
|
|
72
|
+
skills-manager cost --share # privacy-safe paste card of your resident skill tax
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Runtime copies become hub-backed; originals are backed up first.
|
|
76
|
+
|
|
25
77
|
## How it works
|
|
26
78
|
|
|
27
79
|
```text
|
|
@@ -96,10 +148,11 @@ skills-manager provenance adopt # backfill lockfile evidence
|
|
|
96
148
|
skills-manager categories set my-skill frontend # tag with a domain category
|
|
97
149
|
skills-manager categories apply frontend -a claude-code # load only that domain
|
|
98
150
|
skills-manager categories status # applied sets + drift
|
|
151
|
+
skills-manager cost # resident context-cost ledger (report-only recall suggestions)
|
|
99
152
|
skills-manager archive old-skill
|
|
100
153
|
```
|
|
101
154
|
|
|
102
|
-
Sources
|
|
155
|
+
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
156
|
|
|
104
157
|
## Optional: global install & dashboard
|
|
105
158
|
|
package/README.zh-CN.md
CHANGED
|
@@ -1,15 +1,53 @@
|
|
|
1
1
|
# Skills Manager
|
|
2
2
|
|
|
3
|
+
[](https://www.npmjs.com/package/skills-manager-cli)
|
|
4
|
+
[](https://www.npmjs.com/package/skills-manager-cli)
|
|
5
|
+
[](https://github.com/shenysun/skills-manager/stargazers)
|
|
6
|
+
[](LICENSE)
|
|
7
|
+
[](https://nodejs.org)
|
|
8
|
+
|
|
3
9
|
[English](README.md) | **简体中文**
|
|
4
10
|
|
|
11
|
+
|
|
5
12
|
在**对话里**管理你的 agent skills:一条 `npx` 命令装上 manager skill,之后一切都直接吩咐你的 agent——安装、导入、分发、更新、补齐来源。任何会加载 skills 的 agent 都能用(Claude Code、Codex、Cursor、…)。
|
|
6
13
|
|
|
14
|
+
> **CLI 优先、对话驱动。** 本项目是 `skills-manager-cli`——终端/npm 工具,附带可选的本地 web dashboard。它**不是**桌面应用;如果你找的是同名的桌面版,那是另一个项目([xingkongliang/skills-manager](https://github.com/xingkongliang/skills-manager))。
|
|
15
|
+
|
|
16
|
+
**差异化在哪:**
|
|
17
|
+
|
|
18
|
+
- **一个库,所有 agent**——单一内容库(`~/.skills-manager`)把任意子集分发到 catalog 里的 70+ agent,按目标选软链接或拷贝,带副本过期检测与回滚。
|
|
19
|
+
- **五类源**——git 仓库与 URL、直下 URL(单个 `SKILL.md` / zip / tar)、本地 zip 归档、Claude 插件市场、well-known 发现索引。
|
|
20
|
+
- **更新不误报**——更新锚点是 skill 自身子目录的 git tree SHA,上游动了 skill 之外的文件不会点亮「可更新」。
|
|
21
|
+
- **常驻成本账本**——每个已分发 skill 的 frontmatter 都在按条消息计费;`skills-manager cost` 按运行时路径记账(近似口径,`≈` 展示)并给出只报告不执行的收回建议。未分发 = 零常驻。见 [ADR-0018](docs/adr/0018-resident-cost-ledger.md)。
|
|
22
|
+
- **来源可回填**——没有来源的导入 skill,经锁文件证据采纳、agent 搜索、你批准三层流程补齐 provenance。
|
|
23
|
+
- **manager skill**——所有管理动作都在 agent 对话里完成([ADR-0014](docs/adr/0014-manager-skill-first-bootstrap.md));底层 CLI 是纯 JSON 引擎。
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
## 为什么选它,而不是 `gh skill` / `npx skills` / 桌面版?
|
|
27
|
+
|
|
28
|
+
| | **skills-manager** | `gh skill` | `npx skills` | [桌面版 skills-manager](https://github.com/xingkongliang/skills-manager) |
|
|
29
|
+
|---|---|---|---|---|
|
|
30
|
+
| 主界面 | **Agent 对话**(manager skill)+ CLI | GitHub CLI | CLI | 桌面 GUI |
|
|
31
|
+
| 模型 | 单一 `~/.skills-manager` → 分发子集 | 按 agent / 共享 `.agents/skills` | 按次安装 | Hub + presets |
|
|
32
|
+
| Agent 覆盖 | **70+**(catalog 快照) | ~48 | 视目标而定 | ~54 |
|
|
33
|
+
| 非 GitHub 源 | git / URL / zip / marketplace / well-known | GitHub(+ `--from-local`) | 偏 GitHub | Marketplace + git |
|
|
34
|
+
| 更新信号 | **skill 子目录 tree-SHA**(不误报) | frontmatter tree SHA | lockfile / metadata | App 同步 |
|
|
35
|
+
| 上下文成本 | 内置**常驻成本账本** | — | — | — |
|
|
36
|
+
| 多机同步 | Hub git 化(`sync push/pull`) | — | — | 设备码备份 |
|
|
37
|
+
| 安装 | `npx skills-manager-cli` | `gh extension` | `npx skills` | 桌面安装包 |
|
|
38
|
+
|
|
39
|
+
适合「**一个库、多 agent、对话驱动**,更新与成本信号要诚实」的场景——不是又一个一次性安装器。
|
|
40
|
+
|
|
7
41
|
## 快速开始
|
|
8
42
|
|
|
9
43
|
```sh
|
|
10
44
|
npx skills-manager-cli
|
|
11
45
|
```
|
|
12
46
|
|
|
47
|
+

|
|
48
|
+
|
|
49
|
+
*演示:`--help` 与常驻上下文成本账本(`cost`)。*
|
|
50
|
+
|
|
13
51
|
这就是全部的安装步骤。bootstrap 会创建技能库(`~/.skills-manager`),从包内副本把 **manager skill** 种进库,并(以软链接)挂载到它检测到的 agent——展示一次多选让你确认清单。
|
|
14
52
|
|
|
15
53
|
然后回到你的 agent,说:
|
|
@@ -22,6 +60,20 @@ npx skills-manager-cli
|
|
|
22
60
|
|
|
23
61
|
内容会移入库中,原位置变成软链接(先备份)。想先自己预览?`skills-manager init --dry-run`。
|
|
24
62
|
|
|
63
|
+
|
|
64
|
+
## 从 `npx skills` 或 `gh skill` 过来?
|
|
65
|
+
|
|
66
|
+
可以继续用它们安装。要把已有 skill 收进同一个 hub(树 SHA 更新 + 成本账本)、不必重下:
|
|
67
|
+
|
|
68
|
+
```sh
|
|
69
|
+
npx skills-manager-cli # 引导创建 hub + manager skill
|
|
70
|
+
skills-manager init --dry-run # 预览导入现有运行时 skill
|
|
71
|
+
# 或对 agent 说:把我现有的 skills 导入 skills-manager
|
|
72
|
+
skills-manager cost --share # 可粘贴分享的常驻 skill 税卡片(无绝对路径)
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
运行时副本改为由 hub 托管;原文件会先备份。
|
|
76
|
+
|
|
25
77
|
## 工作原理
|
|
26
78
|
|
|
27
79
|
```text
|
|
@@ -96,10 +148,11 @@ skills-manager provenance adopt # 补采锁文件证据
|
|
|
96
148
|
skills-manager categories set my-skill 前端 # 打领域类别标签
|
|
97
149
|
skills-manager categories apply 前端 -a claude-code # 只加载该领域
|
|
98
150
|
skills-manager categories status # 已应用集合 + 漂移
|
|
151
|
+
skills-manager cost # 常驻上下文成本账本(只报告不执行的收回建议)
|
|
99
152
|
skills-manager archive old-skill
|
|
100
153
|
```
|
|
101
154
|
|
|
102
|
-
来源(source
|
|
155
|
+
来源(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
156
|
|
|
104
157
|
## 可选:全局安装与 dashboard
|
|
105
158
|
|
|
@@ -0,0 +1,112 @@
|
|
|
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
|
+
/**
|
|
69
|
+
* Privacy-safe paste card for `skills-manager cost --share` (growth wedge):
|
|
70
|
+
* no absolute paths, agent-family labels only, top skills by total tokens,
|
|
71
|
+
* a one-line CTA + repo URL. Same ledger, thinner surface.
|
|
72
|
+
*/
|
|
73
|
+
export function renderCostShare(ledger) {
|
|
74
|
+
const skillTotals = new Map();
|
|
75
|
+
for (const group of ledger.paths) {
|
|
76
|
+
for (const line of group.skills) {
|
|
77
|
+
skillTotals.set(line.skill, (skillTotals.get(line.skill) ?? 0) + line.tokens);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
const skillCount = skillTotals.size;
|
|
81
|
+
const heaviest = [...skillTotals.entries()]
|
|
82
|
+
.sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))
|
|
83
|
+
.slice(0, 3)
|
|
84
|
+
.map(([skill, tokens]) => `${skill} ${formatApproxTokens(tokens)}`);
|
|
85
|
+
const pathBits = ledger.paths.map((group) => {
|
|
86
|
+
const agents = group.agents.length > 0 ? group.agents.join('+') : 'none';
|
|
87
|
+
return `${group.kind}/${agents}`;
|
|
88
|
+
});
|
|
89
|
+
const lines = [];
|
|
90
|
+
lines.push(`Resident skill tax · ${ledger.method} · skills-manager`);
|
|
91
|
+
lines.push(`${formatApproxTokens(ledger.totalTokens)} tokens / message · ${skillCount} skill(s) · ${ledger.paths.length} path(s)`);
|
|
92
|
+
if (pathBits.length > 0)
|
|
93
|
+
lines.push(`Paths: ${pathBits.join(' · ')}`);
|
|
94
|
+
if (heaviest.length > 0)
|
|
95
|
+
lines.push(`Heaviest: ${heaviest.join(' · ')}`);
|
|
96
|
+
const archived = ledger.suggestions.archived.length;
|
|
97
|
+
const scattered = ledger.suggestions.scattered.length;
|
|
98
|
+
if (archived > 0 || scattered > 0) {
|
|
99
|
+
const watch = [];
|
|
100
|
+
if (archived > 0)
|
|
101
|
+
watch.push(`${archived} archived still distributed`);
|
|
102
|
+
if (scattered > 0)
|
|
103
|
+
watch.push(`${scattered} scattered`);
|
|
104
|
+
lines.push(`Watch: ${watch.join(' · ')}`);
|
|
105
|
+
}
|
|
106
|
+
if (ledger.unmanaged > 0)
|
|
107
|
+
lines.push(`${ledger.unmanaged} unmanaged skill(s) not counted`);
|
|
108
|
+
lines.push('Share: skills-manager cost --share');
|
|
109
|
+
lines.push('https://github.com/shenysun/skills-manager');
|
|
110
|
+
return `${lines.join('\n')}\n`;
|
|
111
|
+
}
|
|
112
|
+
//# sourceMappingURL=cost-output.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cost-output.js","sourceRoot":"","sources":["../../src/cli/cost-output.ts"],"names":[],"mappings":"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"}
|
package/dist/cli/main.js
CHANGED
|
@@ -13,6 +13,7 @@ import { GitHubApiClient } from '../infra/github-api-client.js';
|
|
|
13
13
|
import { HttpDownloadClient } from '../infra/http-download-client.js';
|
|
14
14
|
import { DetectionService, detectionLogPath } from '../core/services/detection-service.js';
|
|
15
15
|
import { runBootstrap, managerSkillBundle } from './bootstrap.js';
|
|
16
|
+
import { renderCostLedger, renderCostShare, renderPresetCostLine } from './cost-output.js';
|
|
16
17
|
const pkg = createRequire(import.meta.url)('../../package.json');
|
|
17
18
|
const projectRoot = projectRootFromImportMeta(import.meta.url);
|
|
18
19
|
function services(cmd, options = {}) {
|
|
@@ -140,6 +141,19 @@ function countErrored(s) {
|
|
|
140
141
|
function staleOrErroredCount(s) {
|
|
141
142
|
return Object.values(s.distribute.staleSummary()).reduce((total, count) => total + count, 0);
|
|
142
143
|
}
|
|
144
|
+
/** Report-only security scan after install/update: one stderr line per skill so stdout JSON stays parseable. */
|
|
145
|
+
function remindSecurityScan(s, skills) {
|
|
146
|
+
if (skills.length === 0)
|
|
147
|
+
return;
|
|
148
|
+
for (const skill of skills) {
|
|
149
|
+
try {
|
|
150
|
+
console.error(s.security.formatSummaryLine(s.security.scanSkill(skill)));
|
|
151
|
+
}
|
|
152
|
+
catch {
|
|
153
|
+
/* scan must never break the install/update that already succeeded */
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
143
157
|
function remindStaleTargets(s) {
|
|
144
158
|
const stale = staleOrErroredCount(s);
|
|
145
159
|
if (stale > 0) {
|
|
@@ -210,6 +224,18 @@ program.command('edit')
|
|
|
210
224
|
});
|
|
211
225
|
// Domain categories: a free-form multi-valued registry axis, orthogonal to the
|
|
212
226
|
// frozen legacy `category` (ADR-0015). Tag edits never touch runtime dirs.
|
|
227
|
+
/** The applied-set column for `categories status`; a preset stamp rides beside the categories (US22), an unstamped path reads exactly as before. */
|
|
228
|
+
function formatAppliedSet(applied) {
|
|
229
|
+
if (applied === null)
|
|
230
|
+
return 'no filter (no category set applied)';
|
|
231
|
+
if ('all' in applied)
|
|
232
|
+
return 'no filter (--all applied)';
|
|
233
|
+
return `categories: ${applied.categories.join(', ')}${applied.preset ? ` (preset: ${applied.preset})` : ''}`;
|
|
234
|
+
}
|
|
235
|
+
/** Shared drift phrasing so `categories status` and `preset list` never diverge — honest, never implied healthy (US6). */
|
|
236
|
+
function driftNote(drift) {
|
|
237
|
+
return `${drift.length} skill(s) match the set but are undistributed: ${drift.join(', ')}`;
|
|
238
|
+
}
|
|
213
239
|
const categories = program.command('categories').description('Tag skills with domain categories and inspect the hub vocabulary');
|
|
214
240
|
categories.command('set')
|
|
215
241
|
.description('Replace the whole category list for a skill (no categories given = clear)')
|
|
@@ -278,12 +304,9 @@ categories.command('status')
|
|
|
278
304
|
}
|
|
279
305
|
for (const item of paths) {
|
|
280
306
|
const agents = item.agents.length > 0 ? item.agents.join(', ') : 'none';
|
|
281
|
-
|
|
282
|
-
? 'no filter (no category set applied)'
|
|
283
|
-
: 'all' in item.applied ? 'no filter (--all applied)' : `categories: ${item.applied.categories.join(', ')}`;
|
|
284
|
-
console.log(`${item.runtimeDir} (agents: ${agents}) — ${set}`);
|
|
307
|
+
console.log(`${item.runtimeDir} (agents: ${agents}) — ${formatAppliedSet(item.applied)}`);
|
|
285
308
|
if (item.drift.length > 0) {
|
|
286
|
-
console.log(` drift: ${
|
|
309
|
+
console.log(` drift: ${driftNote(item.drift)}`);
|
|
287
310
|
const rerun = item.applied !== null && 'all' in item.applied
|
|
288
311
|
? 'skills-manager categories apply --all'
|
|
289
312
|
: `skills-manager categories apply ${item.applied?.categories.join(' ')}`;
|
|
@@ -291,16 +314,80 @@ categories.command('status')
|
|
|
291
314
|
}
|
|
292
315
|
}
|
|
293
316
|
});
|
|
294
|
-
|
|
317
|
+
// Named presets: a saved category list — the re-appliable loading-set档位
|
|
318
|
+
// (ADR-0019). Storage only; switching runtime dirs is `preset apply`'s job.
|
|
319
|
+
const preset = program.command('preset').description('Manage named lists of domain categories (re-appliable agent loading sets, ADR-0019)');
|
|
320
|
+
preset.command('set')
|
|
321
|
+
.description('Replace the whole member list of a preset (overwrite-in-place when the name exists; categories need not exist in the hub yet)')
|
|
322
|
+
.argument('<name>')
|
|
323
|
+
.argument('<category...>')
|
|
324
|
+
.action((name, values, _opts, cmd) => {
|
|
325
|
+
const s = services(cmd);
|
|
326
|
+
const result = s.registry.setPreset(name, values);
|
|
327
|
+
s.activity.record({ action: 'cli-preset-set', summary: `set preset ${name}`, details: { preset: name, categories: values } });
|
|
328
|
+
print(result);
|
|
329
|
+
});
|
|
330
|
+
preset.command('list')
|
|
331
|
+
.description('List every preset with its member categories and mount footprint (drift on mounted paths reported as-is)')
|
|
332
|
+
.action((_opts, cmd) => {
|
|
333
|
+
const s = services(cmd);
|
|
334
|
+
const presets = s.registry.listPresets();
|
|
335
|
+
if (presets.length === 0) {
|
|
336
|
+
console.log('No presets yet — save one with `preset set <name> <category...>`.');
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
// Mount footprint (US5/US6): which physical runtime paths currently carry
|
|
340
|
+
// each preset — zero mounts stay silent, and a drifted mount is reported,
|
|
341
|
+
// never implied healthy. Paths, not resolved-skill counts.
|
|
342
|
+
const statusPaths = s.distribute.categorySetStatus().paths;
|
|
343
|
+
for (const { name, categories } of presets) {
|
|
344
|
+
console.log(`${name}: ${categories.join(', ')}`);
|
|
345
|
+
const mounts = statusPaths.filter((item) => item.applied !== null && 'categories' in item.applied && item.applied.preset === name);
|
|
346
|
+
if (mounts.length === 0)
|
|
347
|
+
continue;
|
|
348
|
+
console.log(` mounted on ${mounts.length} runtime path(s): ${mounts.map((item) => item.runtimeDir).join(', ')}`);
|
|
349
|
+
for (const item of mounts) {
|
|
350
|
+
if (item.drift.length === 0)
|
|
351
|
+
continue;
|
|
352
|
+
console.log(` drift on ${item.runtimeDir}: ${driftNote(item.drift)}`);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
});
|
|
356
|
+
preset.command('remove')
|
|
357
|
+
.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')
|
|
358
|
+
.argument('<name>')
|
|
359
|
+
.action((name, _opts, cmd) => {
|
|
360
|
+
const s = services(cmd);
|
|
361
|
+
const { preset, detached } = s.distribute.removePresetCascade(name);
|
|
362
|
+
s.activity.record({ action: 'cli-preset-remove', summary: `removed preset ${name}, detached from ${detached} runtime path(s)`, details: { preset: name, detached } });
|
|
363
|
+
console.log(`Removed preset ${name} [${preset.categories.join(', ')}] — detached the name from ${detached} runtime path(s)`);
|
|
364
|
+
});
|
|
365
|
+
preset.command('apply')
|
|
366
|
+
.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")
|
|
367
|
+
.argument('<name>')
|
|
368
|
+
.option('-a, --agent <id...>', 'catalog agent ids (repeatable); defaults to the detected set')
|
|
369
|
+
.option('--json', 'machine-readable output for agents')
|
|
370
|
+
.action((name, opts, cmd) => {
|
|
371
|
+
const s = services(cmd);
|
|
372
|
+
const result = s.distribute.applyPreset(name, opts.agent);
|
|
373
|
+
const cost = s.cost.pathCost(result.paths.map((item) => item.runtimeDir));
|
|
374
|
+
const summary = `Applied preset ${name} [${result.categories.join(', ')}] to ${result.paths.length} runtime path(s)`;
|
|
375
|
+
s.activity.record({ action: 'cli-preset-apply', summary, details: { preset: name, categories: result.categories, agents: result.agents, costTokens: cost.tokens } });
|
|
376
|
+
if (opts.json)
|
|
377
|
+
return print({ ...result, cost });
|
|
378
|
+
console.log(summary);
|
|
379
|
+
console.log(renderPresetCostLine(cost));
|
|
380
|
+
});
|
|
381
|
+
const provenance = program.command('provenance').description('Backfill provenance for source-less skills (frontmatter & lockfile evidence adoption, ADR-0011/0012/0017)');
|
|
295
382
|
provenance.command('adopt')
|
|
296
|
-
.description('Adopt
|
|
383
|
+
.description('Adopt frontmatter/lockfile evidence onto legacy imported skills that have no source yet')
|
|
297
384
|
.option('--dry-run', 'print what would be adopted without touching the registry')
|
|
298
385
|
.option('-s, --skill <skill...>', 'limit adoption to specific skills')
|
|
299
386
|
.action((opts, cmd) => {
|
|
300
387
|
const s = services(cmd);
|
|
301
388
|
const result = s.provenance.adopt({ dryRun: Boolean(opts.dryRun), skills: opts.skill });
|
|
302
389
|
if (!opts.dryRun)
|
|
303
|
-
s.activity.record({ action: 'cli-provenance-adopt', summary: `Adopted
|
|
390
|
+
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 } });
|
|
304
391
|
print(result);
|
|
305
392
|
});
|
|
306
393
|
provenance.command('list')
|
|
@@ -317,6 +404,108 @@ provenance.command('list')
|
|
|
317
404
|
for (const skill of pending.locallyAuthored)
|
|
318
405
|
console.log(` ${skill}`);
|
|
319
406
|
});
|
|
407
|
+
// Hub git-ification for multi-machine sync (ADR-0020): each command thin, git
|
|
408
|
+
// stays the merge tool, and the operator keeps every git decision.
|
|
409
|
+
const sync = program.command('sync').description('Multi-machine sync via a git-ified hub (init/push/pull/status, ADR-0020)');
|
|
410
|
+
sync.command('init')
|
|
411
|
+
.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')
|
|
412
|
+
.option('--remote <url>', 'git remote URL to attach as origin (a local bare repo works too)')
|
|
413
|
+
.action((opts, cmd) => {
|
|
414
|
+
const s = services(cmd);
|
|
415
|
+
const result = s.sync.init({ remote: opts.remote });
|
|
416
|
+
s.activity.record({ action: 'cli-sync-init', summary: `${result.mode} hub git-ification`, details: result });
|
|
417
|
+
// Zero-omission report (US-4): every dimension states what happened, including "nothing".
|
|
418
|
+
console.log(result.mode === 'adopted' ? 'adopted: existing git repository kept (history preserved)' : 'initialized: fresh git repository created');
|
|
419
|
+
console.log(result.ignoreLinesAdded.length > 0
|
|
420
|
+
? `.gitignore: appended ${result.ignoreLinesAdded.length} line(s): ${result.ignoreLinesAdded.join(', ')}`
|
|
421
|
+
: '.gitignore: nothing to append (canonical lines already present)');
|
|
422
|
+
console.log(result.baselineCommit ? `baseline commit: ${result.baselineCommit}` : 'baseline commit: none (tree already clean)');
|
|
423
|
+
if (result.remote === null)
|
|
424
|
+
console.log('remote origin: not set (pass --remote <url> to attach one)');
|
|
425
|
+
else
|
|
426
|
+
console.log(result.remote.attached ? `remote origin: attached ${result.remote.url}` : `remote origin: already ${result.remote.url}`);
|
|
427
|
+
console.log('Reminder: the hub will be pushed to this remote — confirm it contains no secrets (tokens, keys, .env files).');
|
|
428
|
+
});
|
|
429
|
+
sync.command('status')
|
|
430
|
+
.description('Read-only sync status: git-ification, remote, dirty files, ahead/behind (last fetch), last sync commit — zero network')
|
|
431
|
+
.option('--json', 'machine-readable output for agents')
|
|
432
|
+
.action((opts, cmd) => {
|
|
433
|
+
const result = services(cmd).sync.status();
|
|
434
|
+
if (opts.json)
|
|
435
|
+
return print(result);
|
|
436
|
+
// Read-only probe: no activity record — status itself must not dirty the hub.
|
|
437
|
+
console.log(`hub: ${result.home}`);
|
|
438
|
+
if (!result.gitified) {
|
|
439
|
+
console.log(`git: no — ${result.hint}`);
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
console.log('git: yes');
|
|
443
|
+
console.log(result.remote === null
|
|
444
|
+
? 'remote origin: not configured (attach one with `sync init --remote <url>`)'
|
|
445
|
+
: `remote origin: ${result.remote}`);
|
|
446
|
+
console.log(result.dirtyFiles === 0 ? 'dirty files: 0 (clean tree)' : `dirty files: ${result.dirtyFiles}`);
|
|
447
|
+
console.log(result.aheadBehind === null
|
|
448
|
+
? 'ahead/behind: unknown (no upstream tracking ref yet — set by your first push)'
|
|
449
|
+
: `ahead/behind: ahead ${result.aheadBehind.ahead}, behind ${result.aheadBehind.behind} (based on last fetch — run \`git -C ${result.home} fetch\` to refresh)`);
|
|
450
|
+
console.log(result.lastCommit === null
|
|
451
|
+
? 'last sync commit: none yet (the hub repo has no commits)'
|
|
452
|
+
: `last sync commit: ${result.lastCommit.sha} ${result.lastCommit.message}`);
|
|
453
|
+
});
|
|
454
|
+
sync.command('push')
|
|
455
|
+
.description('Stage all changes, make one summary commit, and push to the remote (no empty commits)')
|
|
456
|
+
.action((_opts, cmd) => {
|
|
457
|
+
const s = services(cmd);
|
|
458
|
+
const result = s.sync.push();
|
|
459
|
+
// Honest summary: an in-sync push sent nothing, and the log must not claim one did.
|
|
460
|
+
s.activity.record({
|
|
461
|
+
action: 'cli-sync-push',
|
|
462
|
+
summary: result.upToDate ? 'Sync push: already in sync, nothing sent' : `Pushed hub to ${result.remote}`,
|
|
463
|
+
details: { commit: result.commit?.sha ?? null, upToDate: result.upToDate },
|
|
464
|
+
});
|
|
465
|
+
if (result.upToDate) {
|
|
466
|
+
console.log('already in sync: no changes to commit, remote is up to date');
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
console.log(result.commit === null
|
|
470
|
+
? 'commit: none (working tree clean — pushed existing local commits)'
|
|
471
|
+
: `commit: ${result.commit.sha} ${result.commit.message}`);
|
|
472
|
+
console.log(`pushed to origin: ${result.remote}`);
|
|
473
|
+
});
|
|
474
|
+
sync.command('pull')
|
|
475
|
+
.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)')
|
|
476
|
+
.action((_opts, cmd) => {
|
|
477
|
+
const s = services(cmd);
|
|
478
|
+
const result = s.sync.pull();
|
|
479
|
+
s.activity.record({
|
|
480
|
+
action: 'cli-sync-pull',
|
|
481
|
+
summary: result.upToDate ? 'Sync pull: already up to date' : `Pulled hub from ${result.remote}`,
|
|
482
|
+
details: { upToDate: result.upToDate, registryPlaceholderSuperseded: result.registryPlaceholderSuperseded, ...result.stats },
|
|
483
|
+
});
|
|
484
|
+
if (result.upToDate) {
|
|
485
|
+
console.log('already up to date: nothing to merge from the remote');
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
console.log(`pulled from origin: ${result.remote}`);
|
|
489
|
+
const { added, updated, removed } = result.stats;
|
|
490
|
+
const segments = [
|
|
491
|
+
...(added.length > 0 ? [`${added.length} added (${added.join(', ')})`] : []),
|
|
492
|
+
...(updated.length > 0 ? [`${updated.length} updated (${updated.join(', ')})`] : []),
|
|
493
|
+
...(removed.length > 0 ? [`${removed.length} removed (${removed.join(', ')})`] : []),
|
|
494
|
+
];
|
|
495
|
+
console.log(segments.length > 0 ? `merged skills: ${segments.join(', ')}` : 'merged skills: none changed');
|
|
496
|
+
console.log(result.stats.registryChanged ? 'registry: changed' : 'registry: unchanged');
|
|
497
|
+
if (result.registryPlaceholderSuperseded !== null) {
|
|
498
|
+
// Zero-omission (ticket 07): the merge clashed on registry.yaml and the
|
|
499
|
+
// tool settled it because the local side was provably its own init
|
|
500
|
+
// placeholder — say so plainly.
|
|
501
|
+
console.log('registry conflict: resolved by keeping the remote registry (the local side was the empty init placeholder, not user data)');
|
|
502
|
+
}
|
|
503
|
+
// Plain text, on purpose (US-14): the follow-up stays the operator's call —
|
|
504
|
+
// nothing is detected, distributed, or marked.
|
|
505
|
+
if (added.length > 0) {
|
|
506
|
+
console.log('Newly pulled skills are not distributed yet — run `distribute --skill <name>` when you want them available to your agents.');
|
|
507
|
+
}
|
|
508
|
+
});
|
|
320
509
|
const backup = program.command('backup').description('Inspect and restore init backups (hub .backups/, 30-day retention)');
|
|
321
510
|
backup.command('list')
|
|
322
511
|
.description('List saved backups')
|
|
@@ -367,6 +556,50 @@ program.command('list')
|
|
|
367
556
|
archived: Boolean(row.archived),
|
|
368
557
|
})));
|
|
369
558
|
});
|
|
559
|
+
// The reference layer (ADR-0017): a zero-retention read channel. Raw stdout
|
|
560
|
+
// under the `status` human-readable precedent — never JSON `print` — so agents
|
|
561
|
+
// can pipe the skill body; the archived notice rides stderr to keep stdout
|
|
562
|
+
// byte-exact. No borrow/cleanup lifecycle.
|
|
563
|
+
program.command('get')
|
|
564
|
+
.description("Print a hub skill's full SKILL.md to stdout (zero-retention read; provenance evidence rides the frontmatter)")
|
|
565
|
+
.argument('<skill>')
|
|
566
|
+
.option('--path', 'print the canonical hub directory (absolute) for read-only borrowing of sibling files, instead of the file body')
|
|
567
|
+
.action((skill, opts, cmd) => {
|
|
568
|
+
const s = services(cmd);
|
|
569
|
+
const target = s.get.resolve(skill);
|
|
570
|
+
const archivedNotice = `Note: ${skill} is archived — still readable; it only left the management plane (updates, distribution).`;
|
|
571
|
+
if (opts.path) {
|
|
572
|
+
console.log(target.dir);
|
|
573
|
+
console.log('Read-only borrow: do not write into this directory — the hub is canonical; changes go through skills-manager commands.');
|
|
574
|
+
if (target.archived)
|
|
575
|
+
console.log(archivedNotice);
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
578
|
+
if (target.archived)
|
|
579
|
+
console.error(archivedNotice);
|
|
580
|
+
process.stdout.write(s.get.read(target));
|
|
581
|
+
});
|
|
582
|
+
// The resident-cost ledger (ADR-0018): a read-only account grouped by physical
|
|
583
|
+
// runtime path. Suggestions are report-only — each names the verbatim
|
|
584
|
+
// undistribute command and never runs it. No filter flags in v1: consumers
|
|
585
|
+
// filter the `--json` structure themselves.
|
|
586
|
+
program.command('cost')
|
|
587
|
+
.description('Show the resident context-cost ledger: per-runtime-path token cost of distributed skill frontmatter, with report-only recall suggestions')
|
|
588
|
+
.option('--json', 'machine-readable output for agents')
|
|
589
|
+
.option('--share', 'privacy-safe paste card (no absolute paths) for X / HN / chat')
|
|
590
|
+
.option('--top <n>', 'how many of the most expensive descriptions to list', '5')
|
|
591
|
+
.action((opts, cmd) => {
|
|
592
|
+
const s = services(cmd);
|
|
593
|
+
const top = Number.parseInt(opts.top, 10);
|
|
594
|
+
if (!Number.isInteger(top) || top < 0)
|
|
595
|
+
throw new Error(`--top expects a non-negative integer, got "${opts.top}"`);
|
|
596
|
+
if (opts.json && opts.share)
|
|
597
|
+
throw new Error('use either --json or --share, not both');
|
|
598
|
+
const ledger = s.cost.ledger(top);
|
|
599
|
+
if (opts.json)
|
|
600
|
+
return print(ledger);
|
|
601
|
+
process.stdout.write(opts.share ? renderCostShare(ledger) : renderCostLedger(ledger));
|
|
602
|
+
});
|
|
370
603
|
program.command('add')
|
|
371
604
|
.description('Discover from a source, then install selected skills')
|
|
372
605
|
.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')
|
|
@@ -405,6 +638,7 @@ program.command('add')
|
|
|
405
638
|
const result = s.install.installFromSourceSelection({ source, selectors, overwrite: Boolean(opts.yes), allowInsecureHttp, format });
|
|
406
639
|
s.activity.record({ action: 'cli-add', summary: `Installed ${result.installed.join(', ')}`, details: { source, installed: result.installed } });
|
|
407
640
|
print({ ...result, plan: { ...result.plan, source: redactCheckout(result.plan.source), selected: result.plan.selected.map(redactDiscovered) } });
|
|
641
|
+
remindSecurityScan(s, result.installed);
|
|
408
642
|
remindStaleTargets(s);
|
|
409
643
|
});
|
|
410
644
|
program.command('update')
|
|
@@ -451,8 +685,19 @@ program.command('update')
|
|
|
451
685
|
const result = opts.source ? s.update.updateSource(opts.source) : s.update.updateSkills(opts.skill);
|
|
452
686
|
s.activity.record({ action: 'cli-update', summary: `Updated ${result.updated.join(', ')}`, details: result });
|
|
453
687
|
print(result);
|
|
688
|
+
remindSecurityScan(s, result.updated);
|
|
454
689
|
remindStaleTargets(s);
|
|
455
690
|
});
|
|
691
|
+
const audit = program.command('audit').description('Report-only audits over hub skills (security today; more subcommands later)');
|
|
692
|
+
audit.command('security')
|
|
693
|
+
.description('Scan skill tree(s) for high-signal risks: shell exec, network calls, credential paths, secret-like strings (never blocks)')
|
|
694
|
+
.argument('[skills...]', 'canonical skill names; omit to scan every non-archived hub skill')
|
|
695
|
+
.action((skills, _opts, cmd) => {
|
|
696
|
+
const s = services(cmd);
|
|
697
|
+
const names = skills.length > 0 ? skills : s.registry.listSkills().map((skill) => skill.name);
|
|
698
|
+
const reports = s.security.scanSkills(names);
|
|
699
|
+
print(reports.length === 1 ? reports[0] : reports);
|
|
700
|
+
});
|
|
456
701
|
const distribute = program.command('distribute')
|
|
457
702
|
.description('Distribute hub skills to user or project runtime directories')
|
|
458
703
|
.option('--to <kind>', 'user or project')
|