cue-ai 0.5.0 → 0.7.0

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 (154) hide show
  1. package/README.md +757 -110
  2. package/package.json +5 -5
  3. package/profiles/README.md +12 -12
  4. package/profiles/SCHEMA.md +31 -3
  5. package/profiles/_cache/README.md +1 -1
  6. package/profiles/_types.ts +26 -1
  7. package/profiles/backend/profile.yaml +1 -0
  8. package/profiles/career/profile.yaml +13 -0
  9. package/profiles/core/profile.yaml +76 -9
  10. package/profiles/creative-media/README.md +1 -1
  11. package/profiles/cybersecurity/profile.yaml +779 -756
  12. package/profiles/ecc/profile.yaml +39 -0
  13. package/profiles/event-design/profile.yaml +10 -0
  14. package/profiles/fleet-control/README.md +1 -1
  15. package/profiles/frontend/profile.yaml +14 -0
  16. package/profiles/full/README.md +1 -1
  17. package/profiles/go-api/profile.yaml +1 -0
  18. package/profiles/marketing/profile.yaml +12 -1
  19. package/profiles/predict-everything/profile.yaml +9 -0
  20. package/profiles/rust/profile.yaml +22 -3
  21. package/profiles/rust-cli/profile.yaml +14 -0
  22. package/profiles/rust-core/profile.yaml +35 -0
  23. package/profiles/rust-embedded/profile.yaml +11 -0
  24. package/profiles/rust-ffi/profile.yaml +13 -0
  25. package/profiles/rust-game/profile.yaml +11 -0
  26. package/profiles/rust-wasm/profile.yaml +11 -0
  27. package/profiles/rust-web/profile.yaml +17 -0
  28. package/profiles/schema.json +44 -4
  29. package/profiles/trendradar/profile.yaml +11 -0
  30. package/resources/mcps/README.md +39 -164
  31. package/resources/mcps/configs/claude.sanitized.json +55 -0
  32. package/resources/mcps/configs/claude_runtime.sanitized.json +47 -0
  33. package/resources/skills/README.md +70 -113
  34. package/resources/skills/skills/event-design/wedding-invitations/SKILL.md +43 -0
  35. package/resources/skills/skills/meta/acpx/SKILL.md +78 -0
  36. package/resources/skills/skills/meta/cue-usage/SKILL.md +24 -0
  37. package/resources/skills/skills/meta/profile-fit-monitor/SKILL.md +24 -0
  38. package/resources/skills/skills/predict-everything/mirofish/SKILL.md +75 -0
  39. package/resources/skills/skills/research/trendradar/SKILL.md +88 -0
  40. package/resources/skills/skills/rust/async-tokio/SKILL.md +27 -0
  41. package/resources/skills/skills/rust/axum-api/SKILL.md +38 -0
  42. package/resources/skills/skills/rust/bacon-watch/SKILL.md +24 -0
  43. package/resources/skills/skills/rust/bevy/SKILL.md +43 -0
  44. package/resources/skills/skills/rust/bindgen/SKILL.md +39 -0
  45. package/resources/skills/skills/rust/cargo-audit/SKILL.md +26 -0
  46. package/resources/skills/skills/rust/cargo-basics/SKILL.md +28 -0
  47. package/resources/skills/skills/rust/cargo-chef/SKILL.md +43 -0
  48. package/resources/skills/skills/rust/cargo-edit/SKILL.md +26 -0
  49. package/resources/skills/skills/rust/cargo-expand/SKILL.md +24 -0
  50. package/resources/skills/skills/rust/cargo-flamegraph/SKILL.md +26 -0
  51. package/resources/skills/skills/rust/cargo-fuzz/SKILL.md +34 -0
  52. package/resources/skills/skills/rust/cargo-hack/SKILL.md +26 -0
  53. package/resources/skills/skills/rust/cargo-msrv/SKILL.md +30 -0
  54. package/resources/skills/skills/rust/cargo-mutants/SKILL.md +26 -0
  55. package/resources/skills/skills/rust/cargo-nextest/SKILL.md +24 -0
  56. package/resources/skills/skills/rust/cargo-readme/SKILL.md +36 -0
  57. package/resources/skills/skills/rust/cbindgen/SKILL.md +41 -0
  58. package/resources/skills/skills/rust/chisel-tool/SKILL.md +32 -0
  59. package/resources/skills/skills/rust/clap-cli/SKILL.md +44 -0
  60. package/resources/skills/skills/rust/clippy-and-fmt/SKILL.md +25 -0
  61. package/resources/skills/skills/rust/cross-compile/SKILL.md +26 -0
  62. package/resources/skills/skills/rust/embedded/SKILL.md +33 -0
  63. package/resources/skills/skills/rust/error-handling/SKILL.md +32 -0
  64. package/resources/skills/skills/rust/just-runner/SKILL.md +26 -0
  65. package/resources/skills/skills/rust/mdbook/SKILL.md +25 -0
  66. package/resources/skills/skills/rust/napi-rs/SKILL.md +32 -0
  67. package/resources/skills/skills/rust/no-std/SKILL.md +42 -0
  68. package/resources/skills/skills/rust/property-testing/SKILL.md +35 -0
  69. package/resources/skills/skills/rust/pyo3/SKILL.md +40 -0
  70. package/resources/skills/skills/rust/ratatui-tui/SKILL.md +36 -0
  71. package/resources/skills/skills/rust/release-plz/SKILL.md +27 -0
  72. package/resources/skills/skills/rust/reqwest/SKILL.md +37 -0
  73. package/resources/skills/skills/rust/sccache/SKILL.md +28 -0
  74. package/resources/skills/skills/rust/serde/SKILL.md +30 -0
  75. package/resources/skills/skills/rust/snapshot-testing/SKILL.md +30 -0
  76. package/resources/skills/skills/rust/sqlx-cli/SKILL.md +33 -0
  77. package/resources/skills/skills/rust/tracing/SKILL.md +36 -0
  78. package/resources/skills/skills/rust/typos-spellcheck/SKILL.md +31 -0
  79. package/resources/skills/skills/rust/uniffi/SKILL.md +38 -0
  80. package/resources/skills/skills/rust/wasm-rust/SKILL.md +27 -0
  81. package/resources/skills/skills/security/agentshield/SKILL.md +119 -0
  82. package/src/commands/_index.ts +47 -3
  83. package/src/commands/cli.test.ts +192 -0
  84. package/src/commands/cli.ts +303 -0
  85. package/src/commands/current.ts +1 -1
  86. package/src/commands/debug.test.ts +62 -0
  87. package/src/commands/debug.ts +212 -0
  88. package/src/commands/discover.scoring.test.ts +216 -0
  89. package/src/commands/discover.test.ts +145 -0
  90. package/src/commands/discover.ts +2618 -0
  91. package/src/commands/eval-behavior.test.ts +56 -0
  92. package/src/commands/eval-behavior.ts +189 -0
  93. package/src/commands/eval.test.ts +102 -0
  94. package/src/commands/eval.ts +348 -0
  95. package/src/commands/evolve.ts +291 -0
  96. package/src/commands/failures.test.ts +78 -0
  97. package/src/commands/failures.ts +393 -0
  98. package/src/commands/feedback.ts +219 -0
  99. package/src/commands/init.ts +26 -0
  100. package/src/commands/launch.e2e.test.ts +9 -1
  101. package/src/commands/launch.ts +174 -11
  102. package/src/commands/lint-skill.ts +157 -0
  103. package/src/commands/marketplace.ts +763 -2
  104. package/src/commands/new.ts +1 -1
  105. package/src/commands/optimizer.ts +92 -28
  106. package/src/commands/profile-draft-skill.test.ts +96 -0
  107. package/src/commands/profile-draft-skill.ts +287 -0
  108. package/src/commands/profile-evolve.test.ts +126 -0
  109. package/src/commands/profile-evolve.ts +0 -0
  110. package/src/commands/profile-suggest.ts +223 -0
  111. package/src/commands/profile.ts +41 -0
  112. package/src/commands/quick.ts +2 -17
  113. package/src/commands/scan.ts +2 -2
  114. package/src/commands/score.ts +1 -1
  115. package/src/commands/share.ts +1 -1
  116. package/src/commands/sources.ts +2 -2
  117. package/src/commands/submit-profile.ts +262 -0
  118. package/src/commands/upgrade.ts +1 -1
  119. package/src/commands/use.ts +35 -5
  120. package/src/commands/validate.ts +1 -1
  121. package/src/index.ts +66 -0
  122. package/src/lib/analytics.ts +48 -2
  123. package/src/lib/claude-binary.ts +39 -0
  124. package/src/lib/cli-extractor.ts +77 -0
  125. package/src/lib/cluster-skills.test.ts +268 -0
  126. package/src/lib/cluster-skills.ts +290 -0
  127. package/src/lib/credentials-sync.test.ts +208 -0
  128. package/src/lib/credentials-sync.ts +205 -0
  129. package/src/lib/mcp-materializer.test.ts +1 -1
  130. package/src/lib/persona-playbooks.test.ts +111 -0
  131. package/src/lib/pr-poster.test.ts +243 -0
  132. package/src/lib/pr-poster.ts +285 -0
  133. package/src/lib/pr-throttle.test.ts +148 -0
  134. package/src/lib/pr-throttle.ts +209 -0
  135. package/src/lib/profile-generator.test.ts +1 -1
  136. package/src/lib/profile-generator.ts +2 -2
  137. package/src/lib/profile-linter.test.ts +6 -3
  138. package/src/lib/profile-linter.ts +71 -8
  139. package/src/lib/profile-loader.test.ts +1 -1
  140. package/src/lib/profile-loader.ts +16 -0
  141. package/src/lib/resolver-local.test.ts +1 -1
  142. package/src/lib/resolver-npx.test.ts +76 -1
  143. package/src/lib/resolver-npx.ts +35 -3
  144. package/src/lib/resolver-plugins.test.ts +1 -1
  145. package/src/lib/runtime-materializer.test.ts +191 -7
  146. package/src/lib/runtime-materializer.ts +310 -42
  147. package/src/lib/scan-plugins.test.ts +1 -1
  148. package/src/lib/skill-linter.test.ts +174 -0
  149. package/src/lib/skill-linter.ts +507 -0
  150. package/src/lib/skill-subset.test.ts +95 -0
  151. package/src/lib/skill-subset.ts +166 -0
  152. package/src/lib/star-prompt.ts +1 -1
  153. package/src/lib/uvx-installer.test.ts +229 -0
  154. package/src/lib/uvx-installer.ts +278 -0
@@ -0,0 +1,27 @@
1
+ ---
2
+ name: wasm-rust
3
+ description: Use when building Rust → WASM — modules with JS bindings (wasm-pack), full SPA frontends (trunk + Yew/Leptos/Sycamore), or Dioxus apps.
4
+ allowed-tools: Bash(cargo:*), Bash(wasm-pack:*), Bash(trunk:*), Bash(dioxus:*), Bash(rustup:*)
5
+ ---
6
+
7
+ # Rust → WebAssembly
8
+
9
+ Pick the toolchain that matches the target.
10
+
11
+ ## When to use
12
+ - **Library for JS to consume** (npm, web, node, deno): `wasm-pack build --target web|nodejs|bundler`
13
+ - Generates `pkg/` with `.wasm`, `.js`, `.d.ts`, `package.json`
14
+ - Use `#[wasm_bindgen]` on exported fns/structs
15
+ - **Full Rust SPA** (Yew, Leptos, Sycamore): `trunk serve` (dev) · `trunk build --release` (prod)
16
+ - Entry is `index.html` with a `<link data-trunk rel="rust" />`
17
+ - **Dioxus app** (web/desktop/mobile from one codebase): `dx new <name>` · `dx serve --platform web`
18
+
19
+ ## Prerequisites
20
+ - WASM target: `rustup target add wasm32-unknown-unknown`
21
+ - wasm-pack, trunk, or dioxus-cli depending on path
22
+
23
+ ## Notes
24
+ - For web SPAs, `wasm-opt` (from binaryen) shaves significant size — trunk runs it automatically in `--release`.
25
+ - `wee_alloc` saves ~10KB but is unmaintained; the default allocator is usually fine.
26
+ - Async works in browser via `wasm-bindgen-futures`; spawn with `spawn_local`.
27
+ - Cargo features: gate native-only code behind `#[cfg(not(target_arch = "wasm32"))]` so the WASM build doesn't pull in tokio's mio.
@@ -0,0 +1,119 @@
1
+ ---
2
+ name: agentshield
3
+ description: Use when the user asks to scan or audit a Claude Code / agent configuration for security issues — hardcoded secrets, overly-permissive Bash allow rules, hook injection, risky MCP servers, agent prompt-injection vectors — or mentions "agentshield", "scan my .claude", "audit my settings.json", "ecc-agentshield", or "miniclaw".
4
+ ---
5
+
6
+ # AgentShield — security auditor for AI agent configurations
7
+
8
+ `ecc-agentshield` is a CLI scanner from [affaan-m/agentshield](https://github.com/affaan-m/agentshield) that audits `.claude/` directories, `settings.json`, `mcp.json`, hook scripts, and agent definitions for secrets, permission misconfigs, hook injection, MCP supply-chain risks, and prompt-injection vectors. Output is a graded report (A–F, 0–100) with severity-tagged findings.
9
+
10
+ ## When to use
11
+
12
+ - The user wants to **vet a Claude Code / Codex / agent config** before sharing, committing, or deploying it.
13
+ - After installing a community plugin / skill / MCP — to verify the new surface didn't introduce a vulnerability.
14
+ - In CI on a PR that touches `.claude/`, `.codex/`, `settings.json`, `mcp.json`, hook scripts, or an `agents/` directory.
15
+ - The user mentions the recent agent-marketplace incidents (12% malicious skills, 1.5M leaked tokens) and wants a baseline scan.
16
+
17
+ ## Quick start
18
+
19
+ ```bash
20
+ # Scan the active ~/.claude/ (auto-discovery)
21
+ npx ecc-agentshield scan
22
+
23
+ # Scan a specific path (a repo's .claude/, .codex/, or a settings.json)
24
+ npx ecc-agentshield scan --path /path/to/project/.claude
25
+
26
+ # Auto-fix safe findings (env-var references for hardcoded secrets)
27
+ npx ecc-agentshield scan --fix
28
+
29
+ # Machine-readable
30
+ npx ecc-agentshield scan --format json
31
+ npx ecc-agentshield scan --format html > report.html
32
+
33
+ # Portable audit bundle (evidence + findings for handoff)
34
+ npx ecc-agentshield scan --evidence-pack ./agentshield-evidence
35
+
36
+ # Deep three-agent adversarial analysis via Opus (needs ANTHROPIC_API_KEY)
37
+ npx ecc-agentshield scan --opus --stream
38
+ ```
39
+
40
+ Discovery automatically skips `node_modules/`, build output, and `.dmux` worktree mirrors.
41
+
42
+ ## What it catches (5 rule categories, ~102 rules)
43
+
44
+ | Category | Count | Examples |
45
+ |---|---|---|
46
+ | **secrets** | 10 | hardcoded API keys, tokens, passwords, exposed env values, leaked webhooks, base64-encoded creds, private keys, internal IPs |
47
+ | **permissions** | 10 | `Bash(*)`, dangerous git flags, mutable tools, sensitive path access, network access without deny list |
48
+ | **hooks** | 34 | command injection, exfiltration, persistence, container escape, clipboard reads, log tampering, reverse shells |
49
+ | **mcp** | 23 | risky servers, env override, `npx` supply-chain risk, auto-approve, missing timeouts, bind-all interfaces, CORS misconfig |
50
+ | **agents** | 25 | tool-restriction gaps, prompt-injection vectors, reflection attacks, output manipulation, social-engineering pretexts |
51
+
52
+ Severity → deductions: `critical` -25, `high` -15, `medium` -5, `low` -2, `info` 0. Grade thresholds: A ≥90, B ≥75, C ≥60, D ≥40, F <40.
53
+
54
+ ## CI / GitHub Action
55
+
56
+ ```yaml
57
+ # .github/workflows/agentshield.yml
58
+ on: [pull_request]
59
+ jobs:
60
+ agentshield:
61
+ runs-on: ubuntu-latest
62
+ steps:
63
+ - uses: actions/checkout@v4
64
+ - uses: affaan-m/agentshield@v1
65
+ with:
66
+ path: .claude
67
+ fail-on: high # fail PR if any high/critical findings
68
+ ```
69
+
70
+ A GitHub App ([`ecc-tools`](https://github.com/apps/ecc-tools)) also exists for org-wide PR commenting.
71
+
72
+ ## MiniClaw — sandboxed agent execution server
73
+
74
+ AgentShield ships a separate sub-tool (`miniclaw`) that runs Claude Code in a hardened HTTP sandbox with tool whitelisting, prompt sanitization, rate limiting, and CORS. Use when the user wants to expose Claude as an internal HTTP API without giving it full shell.
75
+
76
+ ```bash
77
+ npx ecc-agentshield miniclaw start # localhost:3000
78
+ npx ecc-agentshield miniclaw start --port 4000 --rate-limit 20
79
+ ```
80
+
81
+ API + dashboard docs: `src/miniclaw/README.md` in the upstream repo.
82
+
83
+ ## How to interpret the report
84
+
85
+ 1. **Critical / high findings** are the only ones that should block a merge. Medium/low surface during a baseline audit.
86
+ 2. **Active-runtime** findings (`mcp.json`, `.claude/mcp.json`, `.claude.json`, active `settings.json`) are real exposure. **Template-example** findings (`mcp-configs/`, `config/mcp/`) are catalog risks, not active runtime.
87
+ 3. For hardcoded secrets, prefer `--fix` to auto-replace with `${ENV_VAR}` references — then move the actual value to a real env vault (e.g. envoult for Coolify projects).
88
+ 4. For `Bash(*)` rules, replace with narrow patterns: `Bash(git *)`, `Bash(npm *)`, `Bash(node *)`.
89
+ 5. For risky MCPs (`npx`-launched servers without pinned versions, broad filesystem access), pin a tag or sha and tighten the allowed root path.
90
+
91
+ ## Output reference
92
+
93
+ ```text
94
+ AgentShield Security Report
95
+ Grade: F (0/100)
96
+ Score Breakdown
97
+ Secrets ░░░░░░░░░░░░░░░░░░░░ 0
98
+ Permissions ░░░░░░░░░░░░░░░░░░░░ 0
99
+ Hooks ░░░░░░░░░░░░░░░░░░░░ 0
100
+ MCP Servers ░░░░░░░░░░░░░░░░░░░░ 0
101
+ Agents ░░░░░░░░░░░░░░░░░░░░ 0
102
+
103
+ ● CRITICAL Hardcoded Anthropic API key
104
+ CLAUDE.md:13
105
+ Fix: Replace with environment variable reference [auto-fixable]
106
+ ```
107
+
108
+ ## When NOT to use
109
+
110
+ - General OWASP / web-app security review of application code → use the `security-review` skill instead.
111
+ - Secret-scanning of arbitrary repos that have nothing to do with agent configs → use `gitleaks` / `trufflehog`.
112
+ - Running it against `node_modules/` or generated dirs — AgentShield already skips these; don't override unless you have a reason.
113
+
114
+ ## Upstream
115
+
116
+ - Repo: <https://github.com/affaan-m/agentshield>
117
+ - npm: `ecc-agentshield`
118
+ - Author: Affaan Mustafa (built at Claude Code Hackathon, Feb 2026)
119
+ - Part of the [Everything Claude Code](https://github.com/affaan-m/everything-claude-code) ecosystem
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Subcommand registry.
3
3
  *
4
- * Each entry declares a one-line summary (for `soul --help`) and a lazy
4
+ * Each entry declares a one-line summary (for `cue --help`) and a lazy
5
5
  * loader that returns the command module. Lazy loading keeps cold start
6
6
  * fast — only the command actually invoked is imported.
7
7
  *
@@ -10,7 +10,7 @@
10
10
  */
11
11
 
12
12
  export interface Command {
13
- /** One-line description shown by `soul --help`. */
13
+ /** One-line description shown by `cue --help`. */
14
14
  summary: string;
15
15
  /** Lazy import of the command module. Must export `run(args): Promise<number>`. */
16
16
  load: () => Promise<{ run: (args: string[]) => Promise<number> }>;
@@ -109,6 +109,10 @@ export const COMMANDS = {
109
109
  summary: "Create a new profile.yaml from skills/MCPs (interactive or from agent skill)",
110
110
  load: () => import("./create-profile"),
111
111
  },
112
+ profile: {
113
+ summary: "Profile-scoped operations: `cue profile suggest` audits for regroupings",
114
+ load: () => import("./profile"),
115
+ },
112
116
  skills: {
113
117
  summary: "Manage skills: list, search, add/remove from profiles",
114
118
  load: () => import("./skills"),
@@ -141,6 +145,30 @@ export const COMMANDS = {
141
145
  summary: "Compare two profiles side-by-side",
142
146
  load: () => import("./diff"),
143
147
  },
148
+ eval: {
149
+ summary: "Benchmark profile performance — token savings, usage, score",
150
+ load: () => import("./eval"),
151
+ },
152
+ debug: {
153
+ summary: "Trace why skills/MCPs aren't loading — full resolution chain",
154
+ load: () => import("./debug"),
155
+ },
156
+ cli: {
157
+ summary: "List or install the system CLIs a profile's skills need",
158
+ load: () => import("./cli"),
159
+ },
160
+ "lint-skill": {
161
+ summary: "Validate a SKILL.md against the skill spec (R001-R008); --fix to auto-correct",
162
+ load: () => import("./lint-skill"),
163
+ },
164
+ "eval-behavior": {
165
+ summary: "Structural eval — does this profile have the skills/commands/playbooks/gates for its declared scenarios?",
166
+ load: () => import("./eval-behavior"),
167
+ },
168
+ failures: {
169
+ summary: "Review session-log.jsonl + transcripts for failure patterns per profile",
170
+ load: () => import("./failures"),
171
+ },
144
172
  snapshot: {
145
173
  summary: "Export/restore current profile state as portable YAML",
146
174
  load: () => import("./snapshot"),
@@ -253,6 +281,14 @@ export const COMMANDS = {
253
281
  summary: "Show GitHub repos that provide skills for a profile",
254
282
  load: () => import("./sources"),
255
283
  },
284
+ discover: {
285
+ summary: "Find hidden gem skill repos on GitHub and export docs/discovered.md",
286
+ load: () => import("./discover"),
287
+ },
288
+ evolve: {
289
+ summary: "Auto-evolve profiles: detect gaps, suggest skills, prune unused",
290
+ load: () => import("./evolve"),
291
+ },
256
292
  sponsor: {
257
293
  summary: "Star the repo / show support links",
258
294
  load: async () => ({
@@ -270,9 +306,17 @@ export const COMMANDS = {
270
306
  }),
271
307
  },
272
308
  "migrate-symlinks": {
273
- summary: "Rewrite ~/.codex and ~/.claude-accounts symlinks from soul/ to cue/",
309
+ summary: "Rewrite ~/.codex and ~/.claude-accounts symlinks from soul/ to cue/ to cue/",
274
310
  load: () => import("./migrate-symlinks"),
275
311
  },
312
+ feedback: {
313
+ summary: "Share what's working / what's missing (local-only, opt-in to share as GitHub issue)",
314
+ load: () => import("./feedback"),
315
+ },
316
+ "submit-profile": {
317
+ summary: "Fork opencue/cue, branch, commit your profile.yaml, open PR (community contribution)",
318
+ load: () => import("./submit-profile"),
319
+ },
276
320
  } as const satisfies Record<string, Command>;
277
321
 
278
322
  export type CommandName = keyof typeof COMMANDS;
@@ -0,0 +1,192 @@
1
+ /**
2
+ * Tests for `cue cli`. Use JSON mode to assert structure without parsing ANSI.
3
+ * Install command is exercised only in --dry-run mode (default) — we never
4
+ * actually exec apt during tests.
5
+ */
6
+
7
+ import { describe, expect, test, beforeEach } from "bun:test";
8
+ import { run as cliRun } from "./cli";
9
+
10
+ beforeEach(() => {
11
+ delete process.env.CUE_PROFILES_DIR;
12
+ delete process.env.SOUL_PROFILES_DIR;
13
+ });
14
+
15
+ async function capture<T>(fn: () => Promise<T>): Promise<{ stdout: string; value: T }> {
16
+ const orig = process.stdout.write.bind(process.stdout);
17
+ let buf = "";
18
+ (process.stdout as any).write = (c: string | Uint8Array) => { buf += String(c); return true; };
19
+ try {
20
+ const value = await fn();
21
+ return { stdout: buf, value };
22
+ } finally {
23
+ (process.stdout as any).write = orig;
24
+ }
25
+ }
26
+
27
+ describe("cue cli list", () => {
28
+ test("--json shape: rows with cli + installed + plan", async () => {
29
+ const { stdout, value } = await capture(() => cliRun(["list", "full", "--json"]));
30
+ expect(value).toBe(0);
31
+ const out = JSON.parse(stdout) as { profile: string; rows: Array<{ cli: string; installed: boolean; skillCount: number; plan: { mode: string } }> };
32
+ expect(out.profile).toBe("full");
33
+ expect(out.rows.length).toBeGreaterThan(20);
34
+ for (const r of out.rows) {
35
+ expect(typeof r.cli).toBe("string");
36
+ expect(typeof r.installed).toBe("boolean");
37
+ expect(r.skillCount).toBeGreaterThan(0);
38
+ expect(typeof r.plan.mode).toBe("string");
39
+ }
40
+ });
41
+
42
+ test("at least one row has a real install command (apt/pip/script)", async () => {
43
+ const { stdout } = await capture(() => cliRun(["list", "full", "--json"]));
44
+ const out = JSON.parse(stdout) as { rows: Array<{ plan: { mode: string; command?: string } }> };
45
+ const installable = out.rows.filter((r) => r.plan.command);
46
+ expect(installable.length).toBeGreaterThan(5);
47
+ });
48
+
49
+ test("no positional + no .cue-profile → usage error", async () => {
50
+ const orig = process.stderr.write.bind(process.stderr);
51
+ let err = "";
52
+ (process.stderr as any).write = (c: string | Uint8Array) => { err += String(c); return true; };
53
+ try {
54
+ // run from /tmp so no .cue-profile lookup succeeds
55
+ const cwd = process.cwd();
56
+ process.chdir("/tmp");
57
+ try {
58
+ const exit = await cliRun(["list"]);
59
+ // either succeeds via parent .cue-profile resolution, or returns 1 with usage
60
+ if (exit !== 0) expect(err).toContain("Usage");
61
+ } finally {
62
+ process.chdir(cwd);
63
+ }
64
+ } finally {
65
+ (process.stderr as any).write = orig;
66
+ }
67
+ });
68
+ });
69
+
70
+ describe("cue cli install", () => {
71
+ test("dry-run --all is the default (no execution) and produces a plan", async () => {
72
+ const { stdout, value } = await capture(() => cliRun(["install", "--all", "full", "--json"]));
73
+ expect(value).toBe(0);
74
+ const out = JSON.parse(stdout) as { dryRun: boolean; plans: Array<{ cli: string; mode: string; command?: string; hint?: string }> };
75
+ expect(out.dryRun).toBe(true);
76
+ expect(out.plans.length).toBeGreaterThan(10);
77
+ // Every plan has either a command (auto-installable) or a hint (manual).
78
+ for (const p of out.plans) {
79
+ expect(p.command || p.hint).toBeTruthy();
80
+ }
81
+ });
82
+
83
+ test("install <single-tool> without args returns usage error", async () => {
84
+ const orig = process.stderr.write.bind(process.stderr);
85
+ let err = "";
86
+ (process.stderr as any).write = (c: string | Uint8Array) => { err += String(c); return true; };
87
+ try {
88
+ const exit = await cliRun(["install"]);
89
+ expect(exit).toBe(1);
90
+ expect(err).toContain("Usage");
91
+ } finally {
92
+ (process.stderr as any).write = orig;
93
+ }
94
+ });
95
+
96
+ test("install <known-tool> dry-run produces apt or pip plan", async () => {
97
+ const { stdout } = await capture(() => cliRun(["install", "nmap", "--json"]));
98
+ const out = JSON.parse(stdout) as { plans: Array<{ cli: string; mode: string; command?: string }> };
99
+ expect(out.plans).toHaveLength(1);
100
+ expect(out.plans[0]!.cli).toBe("nmap");
101
+ // On Linux with apt available, mode should be apt; otherwise some other available manager.
102
+ expect(["apt", "brew", "dnf", "pacman", "winget", "manual"]).toContain(out.plans[0]!.mode);
103
+ });
104
+
105
+ test("install <unknown-tool> dry-run reports no recipe", async () => {
106
+ const { stdout } = await capture(() => cliRun(["install", "definitely-not-a-real-tool-xyz", "--json"]));
107
+ const out = JSON.parse(stdout) as { plans: Array<{ cli: string; mode: string; hint?: string }> };
108
+ expect(out.plans[0]!.mode).toBe("unknown");
109
+ expect(out.plans[0]!.hint).toContain("no recipe");
110
+ });
111
+ });
112
+
113
+ describe("cue cli list --all-profiles", () => {
114
+ test("--json returns flat array with profileCount across all profiles", async () => {
115
+ const { stdout, value } = await capture(() => cliRun(["list", "--all-profiles", "--json"]));
116
+ expect(value).toBe(0);
117
+ const out = JSON.parse(stdout) as { rows: Array<{ cli: string; profileCount: number; profiles: string[]; installed: boolean; plan: { mode: string } }> };
118
+ expect(out.rows.length).toBeGreaterThan(20);
119
+ // A CLI used by full AND full should report profileCount >= 2.
120
+ const multiUse = out.rows.find((r) => r.profileCount >= 2);
121
+ expect(multiUse).toBeDefined();
122
+ expect(multiUse!.profiles.length).toBe(multiUse!.profileCount);
123
+ });
124
+
125
+ test("--missing-only filters out installed CLIs", async () => {
126
+ const { stdout } = await capture(() => cliRun(["list", "--all-profiles", "--missing-only", "--json"]));
127
+ const out = JSON.parse(stdout) as { rows: Array<{ installed: boolean }> };
128
+ for (const r of out.rows) expect(r.installed).toBe(false);
129
+ });
130
+ });
131
+
132
+ describe("optimizer.parseCLIsFromContent", () => {
133
+ test("extracts CLIs from allowed-tools frontmatter and Prerequisites", async () => {
134
+ const { parseCLIsFromContent } = await import("./optimizer");
135
+ const skillMd = `---
136
+ name: test
137
+ allowed-tools: Bash(nmap:*), Bash(sqlmap:*), Bash(curl arg)
138
+ ---
139
+
140
+ # Test
141
+
142
+ ## Prerequisites
143
+
144
+ - nmap installed
145
+ - docker
146
+ - random line with no known CLI
147
+ `;
148
+ const clis = parseCLIsFromContent(skillMd);
149
+ expect(clis).toContain("nmap");
150
+ expect(clis).toContain("sqlmap");
151
+ expect(clis).toContain("curl");
152
+ expect(clis).toContain("docker");
153
+ });
154
+
155
+ test("empty content returns empty array", async () => {
156
+ const { parseCLIsFromContent } = await import("./optimizer");
157
+ expect(parseCLIsFromContent("")).toEqual([]);
158
+ });
159
+
160
+ test("preserves case for binary names (e.g. Xvfb)", async () => {
161
+ const { parseCLIsFromContent } = await import("./optimizer");
162
+ const md = `---\nallowed-tools: Bash(Xvfb:*)\n---\n`;
163
+ expect(parseCLIsFromContent(md)).toContain("Xvfb");
164
+ });
165
+ });
166
+
167
+ describe("cue cli (top-level)", () => {
168
+ test("no subcommand prints usage with exit 0", async () => {
169
+ const orig = process.stderr.write.bind(process.stderr);
170
+ let err = "";
171
+ (process.stderr as any).write = (c: string | Uint8Array) => { err += String(c); return true; };
172
+ try {
173
+ const exit = await cliRun([]);
174
+ expect(exit).toBe(0);
175
+ expect(err).toContain("cue cli");
176
+ } finally {
177
+ (process.stderr as any).write = orig;
178
+ }
179
+ });
180
+
181
+ test("unknown subcommand exits 1", async () => {
182
+ const orig = process.stderr.write.bind(process.stderr);
183
+ let err = "";
184
+ (process.stderr as any).write = (c: string | Uint8Array) => { err += String(c); return true; };
185
+ try {
186
+ const exit = await cliRun(["nonsense"]);
187
+ expect(exit).toBe(1);
188
+ } finally {
189
+ (process.stderr as any).write = orig;
190
+ }
191
+ });
192
+ });