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,303 @@
1
+ /**
2
+ * `cue cli list [profile]` — show required CLIs + install status
3
+ * `cue cli install <tool>` — install one tool via the right pkg manager
4
+ * `cue cli install --all [profile]` — install every missing tool the active
5
+ * profile needs (skips manual-install ones)
6
+ *
7
+ * Flags:
8
+ * --dry-run (default) print what would run, don't execute
9
+ * --yes actually execute (still prompts for sudo via the cmd itself)
10
+ * --json machine output
11
+ *
12
+ * OS detection: linux + which package manager is present (apt > dnf > pacman);
13
+ * macOS uses brew; Windows uses winget. Falls back to manual hints when the
14
+ * recipe doesn't declare the needed mode.
15
+ */
16
+
17
+ import { spawnSync } from "node:child_process";
18
+ import { existsSync, readFileSync } from "node:fs";
19
+ import { join, dirname, resolve } from "node:path";
20
+ import { fileURLToPath } from "node:url";
21
+ import { homedir, platform } from "node:os";
22
+
23
+ import { resolveProfileForCwd } from "../lib/cwd-resolver";
24
+ import { requiredClisFor } from "../lib/cli-extractor";
25
+ import { listProfiles } from "../lib/profile-loader";
26
+
27
+ const REPO_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..", "..");
28
+ const RECIPES_PATH = join(REPO_ROOT, "resources", "cli-recipes.json");
29
+
30
+ type Recipe = Partial<Record<"apt" | "brew" | "dnf" | "pacman" | "snap" | "winget" | "pip" | "pipx" | "npm" | "script" | "manual" | "needs", string>>;
31
+
32
+ const bold = (s: string) => `\x1b[1m${s}\x1b[0m`;
33
+ const green = (s: string) => `\x1b[32m${s}\x1b[0m`;
34
+ const red = (s: string) => `\x1b[31m${s}\x1b[0m`;
35
+ const yellow = (s: string) => `\x1b[33m${s}\x1b[0m`;
36
+ const dim = (s: string) => `\x1b[2m${s}\x1b[0m`;
37
+
38
+ function which(cmd: string): boolean {
39
+ return spawnSync("which", [cmd], { stdio: "ignore", timeout: 1000 }).status === 0;
40
+ }
41
+
42
+ function readRecipes(): Record<string, Recipe> {
43
+ try { return JSON.parse(readFileSync(RECIPES_PATH, "utf8")); }
44
+ catch { return {}; }
45
+ }
46
+
47
+ /**
48
+ * Pick the best install command for the current OS.
49
+ * Returns the shell command to run, or { manual: hint } when nothing works.
50
+ */
51
+ interface InstallPlan {
52
+ cli: string;
53
+ mode: "apt" | "brew" | "dnf" | "pacman" | "snap" | "winget" | "pip" | "pipx" | "npm" | "script" | "manual" | "unknown";
54
+ command?: string;
55
+ hint?: string;
56
+ needs?: string;
57
+ }
58
+
59
+ function planInstall(cli: string, recipe: Recipe | undefined): InstallPlan {
60
+ if (!recipe) return { cli, mode: "unknown", hint: `no recipe for "${cli}" in resources/cli-recipes.json` };
61
+ const os = platform();
62
+ const tries: Array<[InstallPlan["mode"], string]> = [];
63
+ if (os === "linux") {
64
+ if (recipe.apt && which("apt")) tries.push(["apt", `sudo apt install -y ${recipe.apt}`]);
65
+ if (recipe.dnf && which("dnf")) tries.push(["dnf", `sudo dnf install -y ${recipe.dnf}`]);
66
+ if (recipe.pacman && which("pacman")) tries.push(["pacman", `sudo pacman -S --noconfirm ${recipe.pacman}`]);
67
+ // snap as a fallback for tools that aren't in distro repos (helm, terraform, etc.)
68
+ if (recipe.snap && which("snap")) {
69
+ const classic = recipe.snap.includes("--classic") ? "" : " --classic";
70
+ const pkg = recipe.snap.replace(/--classic\s*/, "").trim();
71
+ tries.push(["snap", `sudo snap install ${pkg}${classic}`]);
72
+ }
73
+ } else if (os === "darwin") {
74
+ if (recipe.brew && which("brew")) tries.push(["brew", `brew install ${recipe.brew}`]);
75
+ } else if (os === "win32") {
76
+ if (recipe.winget && which("winget")) tries.push(["winget", `winget install --id ${recipe.winget} -e`]);
77
+ }
78
+ // Cross-platform language pkg managers as fallback.
79
+ // For Python packages: prefer pipx (isolated, ships its own pip), then pip3
80
+ // direct binary, then python3 -m pip. Some distros (Nix, some minimal Linux
81
+ // installs) ship python3 without the pip module, so `python3 -m pip` fails
82
+ // even when pip3 works fine. Auto-promote `pip` recipes to pipx when pipx is
83
+ // available — most pip recipes here are CLI tools, which is exactly what
84
+ // pipx is designed for.
85
+ if (recipe.pipx && which("pipx")) {
86
+ tries.push(["pipx", `pipx install ${recipe.pipx}`]);
87
+ } else if (recipe.pip) {
88
+ if (which("pipx")) tries.push(["pipx", `pipx install ${recipe.pip}`]);
89
+ else if (which("pip3")) tries.push(["pip", `pip3 install --user ${recipe.pip}`]);
90
+ else tries.push(["pip", `python3 -m pip install --user ${recipe.pip}`]);
91
+ }
92
+ if (recipe.npm && which("npm")) tries.push(["npm", `npm install -g ${recipe.npm}`]);
93
+ if (recipe.script) tries.push(["script", recipe.script]);
94
+
95
+ if (tries.length === 0) {
96
+ const manual = recipe.manual ?? `no installer for this OS/recipe`;
97
+ return { cli, mode: "manual", hint: manual, needs: recipe.needs };
98
+ }
99
+ const [mode, command] = tries[0]!;
100
+ return { cli, mode, command, needs: recipe.needs };
101
+ }
102
+
103
+ async function resolveProfileArg(args: string[]): Promise<string | undefined> {
104
+ const arg = args.find((a) => !a.startsWith("-"));
105
+ if (arg) return arg;
106
+ try {
107
+ const r = await resolveProfileForCwd({ cwd: process.cwd(), homeDir: homedir(), configDir: join(homedir(), ".config", "cue") });
108
+ if (r.source !== "none") return (r as any).profile;
109
+ } catch {}
110
+ return undefined;
111
+ }
112
+
113
+ async function listAllProfilesCmd(args: string[]): Promise<number> {
114
+ const asJson = args.includes("--json");
115
+ const missingOnly = args.includes("--missing-only");
116
+ const recipes = readRecipes();
117
+
118
+ // Aggregate: which profiles need each CLI?
119
+ const byCli = new Map<string, { profiles: Set<string>; skillCount: number }>();
120
+ const names = await listProfiles();
121
+ for (const name of names) {
122
+ try {
123
+ const reqs = await requiredClisFor(name);
124
+ for (const r of reqs) {
125
+ const entry = byCli.get(r.cli) ?? { profiles: new Set(), skillCount: 0 };
126
+ entry.profiles.add(name);
127
+ entry.skillCount += r.skills.length;
128
+ byCli.set(r.cli, entry);
129
+ }
130
+ } catch { /* skip broken profile */ }
131
+ }
132
+
133
+ let rows = [...byCli.entries()].map(([cli, info]) => ({
134
+ cli,
135
+ installed: which(cli),
136
+ profileCount: info.profiles.size,
137
+ profiles: [...info.profiles].sort(),
138
+ skillCount: info.skillCount,
139
+ plan: planInstall(cli, recipes[cli]),
140
+ }));
141
+ if (missingOnly) rows = rows.filter((r) => !r.installed);
142
+ // Sort: not-installed first (action items), then by # profiles desc.
143
+ rows.sort((a, b) => Number(a.installed) - Number(b.installed) || b.profileCount - a.profileCount);
144
+
145
+ if (asJson) {
146
+ process.stdout.write(JSON.stringify({ rows }, null, 2) + "\n");
147
+ return 0;
148
+ }
149
+
150
+ const totalCli = rows.length;
151
+ const installed = rows.filter((r) => r.installed).length;
152
+ const missing = totalCli - installed;
153
+ process.stdout.write(`\n ${bold("All profiles")} · ${totalCli} unique CLIs · ${green(`✅ ${installed}`)} · ${red(`❌ ${missing}`)}\n\n`);
154
+ process.stdout.write(` ${"cli".padEnd(15)} ${"profiles".padStart(8)} ${"plan".padEnd(8)} used by\n`);
155
+ process.stdout.write(` ${"-".repeat(15)} ${"-".repeat(8)} ${"-".repeat(8)} ${"-".repeat(40)}\n`);
156
+ for (const r of rows) {
157
+ const mark = r.installed ? green("✓") : red("✗");
158
+ const mode = r.installed ? dim("installed")
159
+ : r.plan.mode === "manual" ? yellow("manual")
160
+ : r.plan.mode === "unknown" ? red("none")
161
+ : dim(r.plan.mode);
162
+ const profList = r.profiles.length <= 3
163
+ ? r.profiles.join(", ")
164
+ : `${r.profiles.slice(0, 3).join(", ")} +${r.profiles.length - 3} more`;
165
+ process.stdout.write(` ${mark} ${r.cli.padEnd(15)} ${String(r.profileCount).padStart(8)} ${mode.padEnd(8)} ${dim(profList)}\n`);
166
+ }
167
+ process.stdout.write(`\n Filter: ${bold("cue cli list --all-profiles --missing-only")}\n\n`);
168
+ return 0;
169
+ }
170
+
171
+ async function listCmd(args: string[]): Promise<number> {
172
+ if (args.includes("--all-profiles")) return listAllProfilesCmd(args);
173
+
174
+ const profile = await resolveProfileArg(args);
175
+ if (!profile) {
176
+ process.stderr.write("Usage: cue cli list [profile] | --all-profiles [--missing-only]\n");
177
+ return 1;
178
+ }
179
+ const asJson = args.includes("--json");
180
+ const reqs = await requiredClisFor(profile);
181
+ const recipes = readRecipes();
182
+
183
+ const rows = reqs.map((r) => ({
184
+ cli: r.cli,
185
+ installed: which(r.cli),
186
+ skillCount: r.skills.length,
187
+ plan: planInstall(r.cli, recipes[r.cli]),
188
+ }));
189
+
190
+ if (asJson) {
191
+ process.stdout.write(JSON.stringify({ profile, rows }, null, 2) + "\n");
192
+ return 0;
193
+ }
194
+
195
+ const installed = rows.filter((r) => r.installed).length;
196
+ const missing = rows.length - installed;
197
+ process.stdout.write(`\n ${bold(profile)} — ${rows.length} CLIs · ${green(`✅ ${installed}`)} · ${red(`❌ ${missing}`)}\n\n`);
198
+ for (const r of rows) {
199
+ const mark = r.installed ? green("✓") : red("✗");
200
+ const mode = r.installed ? dim("(installed)")
201
+ : r.plan.mode === "manual" ? yellow("manual")
202
+ : r.plan.mode === "unknown" ? red("no recipe")
203
+ : dim(`→ ${r.plan.mode}`);
204
+ process.stdout.write(` ${mark} ${r.cli.padEnd(15)} ${String(r.skillCount).padStart(3)} skills ${mode}\n`);
205
+ }
206
+ process.stdout.write(`\n Run ${bold("cue cli install --all")} to install the auto-installable ones.\n\n`);
207
+ return 0;
208
+ }
209
+
210
+ async function installCmd(args: string[]): Promise<number> {
211
+ const all = args.includes("--all");
212
+ const yes = args.includes("--yes");
213
+ const dryRun = !yes;
214
+ const asJson = args.includes("--json");
215
+ const positional = args.filter((a) => !a.startsWith("-"));
216
+ const recipes = readRecipes();
217
+
218
+ let targets: string[] = [];
219
+ if (all) {
220
+ const profile = await resolveProfileArg(positional);
221
+ if (!profile) {
222
+ process.stderr.write("Usage: cue cli install --all [profile]\n");
223
+ return 1;
224
+ }
225
+ const reqs = await requiredClisFor(profile);
226
+ targets = reqs.filter((r) => !which(r.cli)).map((r) => r.cli);
227
+ } else {
228
+ if (positional.length === 0) {
229
+ process.stderr.write("Usage: cue cli install <tool> | --all [profile]\n");
230
+ return 1;
231
+ }
232
+ targets = positional;
233
+ }
234
+
235
+ if (targets.length === 0) {
236
+ process.stdout.write(` ${green("All required CLIs are already installed.")}\n`);
237
+ return 0;
238
+ }
239
+
240
+ const plans = targets.map((cli) => planInstall(cli, recipes[cli]));
241
+ const installable = plans.filter((p) => p.command);
242
+ const manual = plans.filter((p) => !p.command);
243
+
244
+ if (asJson) {
245
+ process.stdout.write(JSON.stringify({ dryRun, plans }, null, 2) + "\n");
246
+ return 0;
247
+ }
248
+
249
+ process.stdout.write(`\n ${bold(`${installable.length} installable`)} · ${yellow(`${manual.length} manual`)}\n\n`);
250
+
251
+ if (installable.length > 0) {
252
+ process.stdout.write(` ${bold("Install plan")} ${dryRun ? dim("(dry-run — pass --yes to execute)") : ""}\n`);
253
+ for (const p of installable) {
254
+ process.stdout.write(` ${green("•")} ${p.cli.padEnd(15)} ${dim(`(${p.mode})`)} ${p.command}\n`);
255
+ if (p.needs) process.stdout.write(` ${dim("note: " + p.needs)}\n`);
256
+ }
257
+ process.stdout.write("\n");
258
+ }
259
+
260
+ if (manual.length > 0) {
261
+ process.stdout.write(` ${bold("Manual (skipping)")}\n`);
262
+ for (const p of manual) {
263
+ process.stdout.write(` ${yellow("•")} ${p.cli.padEnd(15)} ${p.hint ?? ""}\n`);
264
+ }
265
+ process.stdout.write("\n");
266
+ }
267
+
268
+ if (dryRun) return 0;
269
+
270
+ // Execute installable plans sequentially. sudo will prompt as needed.
271
+ let failed = 0;
272
+ for (const p of installable) {
273
+ process.stdout.write(`\n ${bold(`→ Installing ${p.cli}`)}\n ${dim("$ " + p.command)}\n`);
274
+ const res = spawnSync("bash", ["-c", p.command!], { stdio: "inherit" });
275
+ if (res.status !== 0) {
276
+ process.stdout.write(` ${red(`✗ ${p.cli} install failed (exit ${res.status})`)}\n`);
277
+ failed++;
278
+ } else {
279
+ process.stdout.write(` ${green(`✓ ${p.cli} installed`)}\n`);
280
+ }
281
+ }
282
+ if (failed > 0) {
283
+ process.stdout.write(`\n ${red(`${failed} install(s) failed.`)}\n`);
284
+ return 1;
285
+ }
286
+ process.stdout.write(`\n ${green(`${installable.length} installed.`)}\n`);
287
+ return 0;
288
+ }
289
+
290
+ export async function run(args: string[]): Promise<number> {
291
+ const sub = args[0];
292
+ const rest = args.slice(1);
293
+ switch (sub) {
294
+ case "list": return listCmd(rest);
295
+ case "install": return installCmd(rest);
296
+ default:
297
+ process.stderr.write("Usage: cue cli <list|install> [args]\n");
298
+ process.stderr.write(" cue cli list [profile]\n");
299
+ process.stderr.write(" cue cli install <tool>\n");
300
+ process.stderr.write(" cue cli install --all [profile] [--yes] [--json]\n");
301
+ return sub ? 1 : 0;
302
+ }
303
+ }
@@ -1,5 +1,5 @@
1
1
  /**
2
- * `soul current` — print the active profile and resolved capability list.
2
+ * `cue current` — print the active profile and resolved capability list.
3
3
  *
4
4
  * Reads .cue-profile / repo-default / global-default via cwd-resolver.
5
5
  * Outputs profile name, source, skill count, MCP count, plugin count, and runtime dir.
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Tests for `cue debug`. Asserts on stdout content for representative cases.
3
+ * Uses the real profiles tree — debug is a diagnostic command, not pure.
4
+ */
5
+
6
+ import { describe, expect, test, beforeEach } from "bun:test";
7
+ import { run as debugRun } from "./debug";
8
+
9
+ beforeEach(() => {
10
+ delete process.env.CUE_PROFILES_DIR;
11
+ delete process.env.SOUL_PROFILES_DIR;
12
+ });
13
+
14
+ function strip(s: string): string {
15
+ return s.replace(/\x1b\[[0-9;]*m/g, "");
16
+ }
17
+
18
+ async function captureStdout<T>(fn: () => Promise<T>): Promise<{ stdout: string; value: T }> {
19
+ const orig = process.stdout.write.bind(process.stdout);
20
+ let buf = "";
21
+ (process.stdout as any).write = (chunk: string | Uint8Array) => { buf += String(chunk); return true; };
22
+ try {
23
+ const value = await fn();
24
+ return { stdout: buf, value };
25
+ } finally {
26
+ (process.stdout as any).write = orig;
27
+ }
28
+ }
29
+
30
+ describe("cue debug", () => {
31
+ test("explicit profile arg wins over cwd auto-detect, reports all sections", async () => {
32
+ const { stdout, value } = await captureStdout(() => debugRun(["ecc"]));
33
+ // Exit code can be 0 or 1 — the profile may have skills missing from disk
34
+ // (managed by other sessions). What matters is that all sections render.
35
+ expect(value === 0 || value === 1).toBe(true);
36
+ const clean = strip(stdout);
37
+ expect(clean).toContain("Profile: ecc (source: cli-arg)");
38
+ expect(clean).toContain("Inheritance: core → ecc");
39
+ expect(clean).toContain("Skills");
40
+ expect(clean).toContain("MCPs");
41
+ expect(clean).toContain("Rules");
42
+ expect(clean).toContain("Commands");
43
+ expect(clean).toContain("Hooks");
44
+ expect(clean).toContain("Summary:");
45
+ });
46
+
47
+ test("glob skill ids (full profile uses */*) are not reported as missing", async () => {
48
+ const { stdout, value } = await captureStdout(() => debugRun(["full"]));
49
+ expect(value === 0 || value === 1).toBe(true);
50
+ const clean = strip(stdout);
51
+ // No "not found" / no "✗" for glob entries — they're resolved at materialize time
52
+ expect(clean).not.toContain("*/* — not found");
53
+ expect(clean).toContain("Summary:");
54
+ });
55
+
56
+ test("verbose mode lists each skill individually", async () => {
57
+ const { stdout } = await captureStdout(() => debugRun(["ecc", "--verbose"]));
58
+ const clean = strip(stdout);
59
+ // ecc has 8 inherited skills; at least one should show up by name
60
+ expect(clean).toMatch(/meta\/analyze|caveman\/caveman|nvidia\/skill-evolution/);
61
+ });
62
+ });
@@ -0,0 +1,212 @@
1
+ /**
2
+ * `cue debug [profile]` — trace why skills/MCPs aren't loading.
3
+ *
4
+ * Walks the full resolution chain and reports at each step:
5
+ * - Profile resolution (which .cue-profile, inheritance chain)
6
+ * - Skill resolution (found/missing, path, size)
7
+ * - MCP resolution (in registry or not, env vars set)
8
+ * - Plugin resolution (installed or not)
9
+ * - Runtime state (hash, stale, symlinks)
10
+ */
11
+
12
+ import { resolve, join, dirname } from "node:path";
13
+ import { existsSync, readFileSync, lstatSync, readlinkSync } from "node:fs";
14
+ import { fileURLToPath } from "node:url";
15
+ import { homedir } from "node:os";
16
+
17
+ import { loadProfile } from "../lib/profile-loader";
18
+ import { resolveProfileForCwd } from "../lib/cwd-resolver";
19
+
20
+ const REPO_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..", "..");
21
+ const SKILLS_ROOT = join(REPO_ROOT, "resources", "skills", "skills");
22
+ const MCP_CONFIGS_DIR = join(REPO_ROOT, "resources", "mcps", "configs");
23
+ const RUNTIME_ROOT = join(homedir(), ".config", "cue", "runtime");
24
+
25
+ export async function run(args: string[]): Promise<number> {
26
+ const explicitProfile = args.find(a => !a.startsWith("-"));
27
+ let profileName = explicitProfile;
28
+ const verbose = args.includes("-v") || args.includes("--verbose");
29
+
30
+ const green = (s: string) => `\x1b[32m${s}\x1b[0m`;
31
+ const red = (s: string) => `\x1b[31m${s}\x1b[0m`;
32
+ const yellow = (s: string) => `\x1b[33m${s}\x1b[0m`;
33
+ const dim = (s: string) => `\x1b[2m${s}\x1b[0m`;
34
+ const bold = (s: string) => `\x1b[1m${s}\x1b[0m`;
35
+
36
+ process.stdout.write(`\n ${bold("cue debug")} — tracing profile resolution\n\n`);
37
+
38
+ // 1. Profile resolution — explicit arg wins over cwd auto-detection
39
+ process.stdout.write(` ${bold("① Profile Resolution")}\n`);
40
+ if (explicitProfile) {
41
+ process.stdout.write(` ${green("✓")} Profile: ${explicitProfile} (source: cli-arg)\n`);
42
+ } else {
43
+ const cwd = process.cwd();
44
+ try {
45
+ const resolved = await resolveProfileForCwd({ cwd, homeDir: homedir(), configDir: join(homedir(), ".config", "cue") });
46
+ if (resolved.source === "none") {
47
+ process.stdout.write(` ${red("✗")} No profile found for ${cwd}\n`);
48
+ process.stdout.write(` ${dim("Fix: echo <profile> > .cue-profile or pass a profile name")}\n\n`);
49
+ return 1;
50
+ }
51
+ profileName = (resolved as any).profile;
52
+ process.stdout.write(` ${green("✓")} Profile: ${profileName} (source: ${resolved.source})\n`);
53
+ } catch (e) {
54
+ process.stdout.write(` ${red("✗")} Resolution failed: ${e}\n`);
55
+ return 1;
56
+ }
57
+ }
58
+
59
+ // 2. Load profile
60
+ process.stdout.write(`\n ${bold("② Profile Loading")}\n`);
61
+ let profile;
62
+ try {
63
+ profile = await loadProfile(profileName!);
64
+ process.stdout.write(` ${green("✓")} Loaded: ${profileName}\n`);
65
+ process.stdout.write(` ${dim(`Inheritance: ${profile.inheritanceChain.join(" → ")}`)}\n`);
66
+ } catch (e) {
67
+ process.stdout.write(` ${red("✗")} Failed to load: ${e}\n`);
68
+ return 1;
69
+ }
70
+
71
+ // 3. Skills — expand glob ids (e.g. "*/*") instead of treating them literally
72
+ process.stdout.write(`\n ${bold("③ Skills")} (${profile.skills.local.length} local, ${profile.skills.npx.length} npx)\n`);
73
+ let skillIssues = 0;
74
+ let resolvedCount = 0;
75
+ for (const s of profile.skills.local) {
76
+ const id = s.id;
77
+ if (id.includes("*")) {
78
+ if (verbose) process.stdout.write(` ${dim(`~ ${id} (glob — resolved at materialize time)`)}\n`);
79
+ continue;
80
+ }
81
+ const path = join(SKILLS_ROOT, id, "SKILL.md");
82
+ if (existsSync(path)) {
83
+ resolvedCount++;
84
+ if (verbose) {
85
+ const size = readFileSync(path, "utf8").length;
86
+ process.stdout.write(` ${green("✓")} ${id} ${dim(`(${Math.ceil(size/4)} tokens)`)}\n`);
87
+ }
88
+ } else {
89
+ process.stdout.write(` ${red("✗")} ${id} — not found at ${path}\n`);
90
+ skillIssues++;
91
+ }
92
+ }
93
+ if (skillIssues === 0 && !verbose) {
94
+ process.stdout.write(` ${green("✓")} ${resolvedCount} literal skill ids resolved\n`);
95
+ }
96
+
97
+ // 4. MCPs
98
+ process.stdout.write(`\n ${bold("④ MCPs")} (${profile.mcps.length})\n`);
99
+ const mcpIds = new Set<string>();
100
+ for (const file of ["claude_runtime.sanitized.json", "claude.sanitized.json", "codex.sanitized.json"]) {
101
+ try {
102
+ const raw = JSON.parse(readFileSync(join(MCP_CONFIGS_DIR, file), "utf8"));
103
+ if (raw.servers) for (const id of Object.keys(raw.servers)) mcpIds.add(id);
104
+ } catch {}
105
+ }
106
+
107
+ let mcpIssues = 0;
108
+ for (const m of profile.mcps) {
109
+ const id = m.id;
110
+ if (mcpIds.has(id)) {
111
+ if (verbose) process.stdout.write(` ${green("✓")} ${id} — in registry\n`);
112
+ } else {
113
+ process.stdout.write(` ${red("✗")} ${id} — NOT in any MCP registry config\n`);
114
+ mcpIssues++;
115
+ }
116
+ }
117
+ if (mcpIssues === 0 && !verbose) {
118
+ process.stdout.write(` ${green("✓")} All ${profile.mcps.length} MCPs found in registry\n`);
119
+ }
120
+
121
+ // 4b. Rules / Commands / Hooks
122
+ const RESOURCES_ROOT = join(REPO_ROOT, "resources");
123
+ let resourceIssues = 0;
124
+ for (const [kind, refs, base] of [
125
+ ["Rules", profile.rules, join(RESOURCES_ROOT, "rules")],
126
+ ["Commands", profile.commands, join(RESOURCES_ROOT, "commands")],
127
+ ["Hooks", profile.hooks, join(RESOURCES_ROOT, "hooks")],
128
+ ] as const) {
129
+ if (refs.length === 0) continue;
130
+ process.stdout.write(`\n ${bold(`④${kind === "Rules" ? "a" : kind === "Commands" ? "b" : "c"} ${kind}`)} (${refs.length})\n`);
131
+ let missing = 0;
132
+ for (const ref of refs) {
133
+ const needsExt = kind !== "Hooks" && !ref.endsWith(".md");
134
+ const path = needsExt ? join(base, `${ref}.md`) : (ref.startsWith("/") ? ref : join(base, ref));
135
+ if (existsSync(path)) {
136
+ if (verbose) process.stdout.write(` ${green("✓")} ${ref}\n`);
137
+ } else {
138
+ process.stdout.write(` ${red("✗")} ${ref} — not found at ${path}\n`);
139
+ missing++;
140
+ }
141
+ }
142
+ if (missing === 0 && !verbose) {
143
+ process.stdout.write(` ${green("✓")} All ${refs.length} ${kind.toLowerCase()} found\n`);
144
+ }
145
+ resourceIssues += missing;
146
+ }
147
+
148
+ // 5. Runtime state
149
+ process.stdout.write(`\n ${bold("⑤ Runtime State")}\n`);
150
+ const runtimeDir = join(RUNTIME_ROOT, profileName!, "claude");
151
+ if (existsSync(runtimeDir)) {
152
+ const hashFile = join(runtimeDir, ".cue-hash");
153
+ if (existsSync(hashFile)) {
154
+ const hash = readFileSync(hashFile, "utf8").trim();
155
+ process.stdout.write(` ${green("✓")} Runtime exists: ${runtimeDir}\n`);
156
+ process.stdout.write(` ${dim(`Hash: ${hash.slice(0, 16)}...`)}\n`);
157
+ } else {
158
+ process.stdout.write(` ${yellow("⚠")} Runtime exists but no hash — may be stale\n`);
159
+ }
160
+
161
+ // Check credentials
162
+ const creds = join(runtimeDir, ".credentials.json");
163
+ if (existsSync(creds)) {
164
+ const st = lstatSync(creds);
165
+ if (st.isSymbolicLink()) {
166
+ const target = readlinkSync(creds);
167
+ process.stdout.write(` ${yellow("⚠")} Credentials: symlink → ${target} ${dim("(should be copy)")}\n`);
168
+ } else {
169
+ process.stdout.write(` ${green("✓")} Credentials: present (${st.size} bytes)\n`);
170
+ }
171
+ } else {
172
+ process.stdout.write(` ${red("✗")} Credentials: missing — will need /login\n`);
173
+ }
174
+
175
+ // Check skills symlinks
176
+ const skillsDir = join(runtimeDir, "skills");
177
+ if (existsSync(skillsDir)) {
178
+ const { readdirSync } = await import("node:fs");
179
+ const entries = readdirSync(skillsDir);
180
+ let broken = 0;
181
+ for (const e of entries) {
182
+ const p = join(skillsDir, e);
183
+ try {
184
+ const st = lstatSync(p);
185
+ if (st.isSymbolicLink()) {
186
+ const target = readlinkSync(p);
187
+ if (!existsSync(resolve(dirname(p), target))) broken++;
188
+ }
189
+ } catch { broken++; }
190
+ }
191
+ if (broken > 0) {
192
+ process.stdout.write(` ${red("✗")} ${broken} broken skill symlinks in runtime\n`);
193
+ } else {
194
+ process.stdout.write(` ${green("✓")} ${entries.length} skill symlinks OK\n`);
195
+ }
196
+ }
197
+ } else {
198
+ process.stdout.write(` ${dim("No runtime materialized yet — will build on next launch")}\n`);
199
+ }
200
+
201
+ // Summary
202
+ const totalIssues = skillIssues + mcpIssues + resourceIssues;
203
+ process.stdout.write(`\n ${bold("Summary:")} `);
204
+ if (totalIssues === 0) {
205
+ process.stdout.write(`${green("All clear")} — profile should load correctly.\n`);
206
+ } else {
207
+ process.stdout.write(`${red(`${totalIssues} issue(s)`)} found. Fix and run \`cue doctor --fix\`.\n`);
208
+ }
209
+ process.stdout.write("\n");
210
+
211
+ return totalIssues > 0 ? 1 : 0;
212
+ }