cue-ai 0.9.0 → 0.9.2

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 (310) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/README.md +82 -33
  3. package/bin/cue-review-progress +107 -0
  4. package/bin/cue-review-watch +98 -0
  5. package/dist/cue.js +7352 -3744
  6. package/package.json +16 -5
  7. package/profiles/_types.ts +9 -0
  8. package/profiles/backend/profile.yaml +2 -0
  9. package/profiles/blog-writer/profile.yaml +10 -0
  10. package/profiles/browser/profile.yaml +9 -2
  11. package/profiles/builder/profile.yaml +3 -6
  12. package/profiles/career/profile.yaml +13 -2
  13. package/profiles/claude-api/profile.yaml +1 -1
  14. package/profiles/commerce/profile.yaml +27 -3
  15. package/profiles/core/logo.png +0 -0
  16. package/profiles/core/profile.yaml +62 -2
  17. package/profiles/dash-merge-test/profile.yaml +109 -0
  18. package/profiles/designer/profile.yaml +2 -0
  19. package/profiles/designer-medusa-next/profile.yaml +4 -1
  20. package/profiles/designer-medusa-vite/profile.yaml +4 -1
  21. package/profiles/docs-writer/profile.yaml +3 -1
  22. package/profiles/eu-tender-research/README.md +48 -0
  23. package/profiles/eu-tender-research/logo.png +0 -0
  24. package/profiles/eu-tender-research/profile.yaml +108 -0
  25. package/profiles/finance/logo.png +0 -0
  26. package/profiles/finance/profile.yaml +46 -0
  27. package/profiles/frontend/profile.yaml +5 -9
  28. package/profiles/growth/profile.yaml +2 -3
  29. package/profiles/gstack/profile.yaml +15 -0
  30. package/profiles/higgsfield/profile.yaml +3 -0
  31. package/profiles/hyperframes/logo.png +0 -0
  32. package/profiles/hyperframes/profile.yaml +59 -0
  33. package/profiles/improver/profile.yaml +88 -0
  34. package/profiles/marketing/profile.yaml +0 -3
  35. package/profiles/medusa-dev/profile.yaml +2 -0
  36. package/profiles/medusa-next/profile.yaml +2 -3
  37. package/profiles/medusa-vite/profile.yaml +2 -3
  38. package/profiles/n8n/logo.png +0 -0
  39. package/profiles/n8n/profile.yaml +50 -0
  40. package/profiles/nextjs/profile.yaml +2 -3
  41. package/profiles/ops/profile.yaml +2 -0
  42. package/profiles/postizz/profile.yaml +13 -3
  43. package/profiles/python/profile.yaml +3 -0
  44. package/profiles/research/profile.yaml +3 -1
  45. package/profiles/schema.json +10 -0
  46. package/profiles/secops/profile.yaml +2 -0
  47. package/profiles/seo/profile.yaml +56 -0
  48. package/profiles/skill-writer/profile.yaml +8 -0
  49. package/profiles/ssh/profile.yaml +32 -0
  50. package/profiles/strapi/logo.png +0 -0
  51. package/profiles/strapi/profile.yaml +45 -0
  52. package/profiles/stripe/logo.png +0 -0
  53. package/profiles/stripe/profile.yaml +1 -0
  54. package/profiles/supabase/logo.png +0 -0
  55. package/profiles/supabase/profile.yaml +85 -0
  56. package/profiles/vercel/logo.png +0 -0
  57. package/profiles/vercel/profile.yaml +25 -1
  58. package/profiles/vite/profile.yaml +4 -3
  59. package/profiles/web-frontend-base/profile.yaml +5 -4
  60. package/profiles/webshop/profile.yaml +23 -5
  61. package/profiles/x-growth-bot/profile.yaml +44 -0
  62. package/resources/icons/generate-icons.py +128 -2
  63. package/resources/mcps/configs/claude.sanitized.json +42 -0
  64. package/resources/mcps/configs/codex.sanitized.json +7 -0
  65. package/resources/skills/skills/career/resume-version-manager/SKILL.md +351 -0
  66. package/resources/skills/skills/career/salary-negotiation-prep/SKILL.md +378 -0
  67. package/resources/skills/skills/content/pdf/SKILL.md +2 -0
  68. package/resources/skills/skills/content/postiz-cards/SKILL.md +48 -0
  69. package/resources/skills/skills/content/postiz-cards/scripts/analytics.sh +38 -0
  70. package/resources/skills/skills/content/postiz-cards/scripts/card.sh +42 -0
  71. package/resources/skills/skills/content/postiz-cards/scripts/lint.py +38 -0
  72. package/resources/skills/skills/design/headless-gif-demo/SKILL.md +1 -1
  73. package/resources/skills/skills/design/readme-svg-design/SKILL.md +1 -1
  74. package/resources/skills/skills/eu-funding/grant-outreach/SKILL.md +70 -0
  75. package/resources/skills/skills/eu-funding/hu-grant-finder/SKILL.md +114 -0
  76. package/resources/skills/skills/eu-funding/hu-grant-finder/evals.md +26 -0
  77. package/resources/skills/skills/eu-funding/ted-tender-search/SKILL.md +80 -0
  78. package/resources/skills/skills/eu-funding/ted-tender-search/evals.md +26 -0
  79. package/resources/skills/skills/eu-funding/ted-tender-search/scripts/ted-search.sh +46 -0
  80. package/resources/skills/skills/event-design/wedding-invitations/SKILL.md +1 -1
  81. package/resources/skills/skills/github/gx-agents/SKILL.md +96 -0
  82. package/resources/skills/skills/gstack/design-shotgun/SKILL.md +1 -1
  83. package/resources/skills/skills/marketing/ab-test-analyzer/SKILL.md +1 -1
  84. package/resources/skills/skills/marketing/ab-test-setup-and-analysis/SKILL.md +1 -1
  85. package/resources/skills/skills/marketing/account-structure-review/SKILL.md +1 -1
  86. package/resources/skills/skills/marketing/ad-copy-variant-generator/SKILL.md +1 -1
  87. package/resources/skills/skills/marketing/ad-extension-audit/SKILL.md +1 -1
  88. package/resources/skills/skills/marketing/ad-spend-allocator/SKILL.md +1 -1
  89. package/resources/skills/skills/marketing/anomaly-detection/SKILL.md +1 -1
  90. package/resources/skills/skills/marketing/attribution-model-comparison/SKILL.md +1 -1
  91. package/resources/skills/skills/marketing/audience-overlap-analysis/SKILL.md +7 -1
  92. package/resources/skills/skills/marketing/bid-strategy-recommendations/SKILL.md +7 -1
  93. package/resources/skills/skills/marketing/budget-scenario-planner/SKILL.md +6 -1
  94. package/resources/skills/skills/marketing/campaign-naming-convention-builder/SKILL.md +7 -1
  95. package/resources/skills/skills/marketing/channel-mix-optimizer/SKILL.md +7 -1
  96. package/resources/skills/skills/marketing/client-report-narratives/SKILL.md +6 -1
  97. package/resources/skills/skills/marketing/competitor-creative-analysis/SKILL.md +1 -1
  98. package/resources/skills/skills/marketing/competitor-teardown/SKILL.md +1 -1
  99. package/resources/skills/skills/marketing/content-repurposer/SKILL.md +1 -1
  100. package/resources/skills/skills/marketing/conversion-path-analysis/SKILL.md +1 -1
  101. package/resources/skills/skills/marketing/cpa-diagnostics/SKILL.md +1 -1
  102. package/resources/skills/skills/marketing/creative-fatigue-detection/SKILL.md +1 -1
  103. package/resources/skills/skills/marketing/day-hour-performance-breakdown/SKILL.md +1 -1
  104. package/resources/skills/skills/marketing/device-performance-split/SKILL.md +1 -1
  105. package/resources/skills/skills/marketing/e2e-seo-assistant/SKILL.md +1 -1
  106. package/resources/skills/skills/marketing/email-sequence-writer/SKILL.md +1 -1
  107. package/resources/skills/skills/marketing/frequency-cap-recommendations/SKILL.md +1 -1
  108. package/resources/skills/skills/marketing/geo-performance-analysis/SKILL.md +1 -1
  109. package/resources/skills/skills/marketing/google-ads-audit/SKILL.md +1 -1
  110. package/resources/skills/skills/marketing/icp-research-assistant/SKILL.md +1 -1
  111. package/resources/skills/skills/marketing/keyword-cannibalization-check/SKILL.md +1 -1
  112. package/resources/skills/skills/marketing/landing-page-audit/SKILL.md +1 -1
  113. package/resources/skills/skills/marketing/landing-page-audit-quick/SKILL.md +1 -1
  114. package/resources/skills/skills/marketing/linkedin-ads-audit/SKILL.md +1 -1
  115. package/resources/skills/skills/marketing/meta-ads-audit/SKILL.md +1 -1
  116. package/resources/skills/skills/marketing/pacing-monitor/SKILL.md +1 -1
  117. package/resources/skills/skills/marketing/performance-benchmarking/SKILL.md +1 -1
  118. package/resources/skills/skills/marketing/programmatic-seo-builder/SKILL.md +1 -1
  119. package/resources/skills/skills/marketing/quality-score-breakdown/SKILL.md +1 -1
  120. package/resources/skills/skills/marketing/reddit-ads-audit/SKILL.md +1 -1
  121. package/resources/skills/skills/marketing/retargeting-window-analysis/SKILL.md +1 -1
  122. package/resources/skills/skills/marketing/roas-forecasting/SKILL.md +1 -1
  123. package/resources/skills/skills/marketing/search-term-mining/SKILL.md +1 -1
  124. package/resources/skills/skills/marketing/utm-tracking-generator/SKILL.md +1 -1
  125. package/resources/skills/skills/marketing/wasted-spend-finder/SKILL.md +1 -1
  126. package/resources/skills/skills/marketing/weekly-account-summary/SKILL.md +1 -1
  127. package/resources/skills/skills/meta/awesome-list-submit/SKILL.md +4 -4
  128. package/resources/skills/skills/meta/cue-dashboard/SKILL.md +109 -0
  129. package/resources/skills/skills/meta/cue-developer/SKILL.md +161 -0
  130. package/resources/skills/skills/meta/cue-developer/evals/evals.json +57 -0
  131. package/resources/skills/skills/meta/cue-developer/references/architecture.md +65 -0
  132. package/resources/skills/skills/meta/cue-developer/references/build_and_test.md +72 -0
  133. package/resources/skills/skills/meta/cue-developer/references/contributing.md +75 -0
  134. package/resources/skills/skills/meta/cue-developer/references/conventions.md +57 -0
  135. package/resources/skills/skills/meta/cue-developer/references/first_time_setup.md +51 -0
  136. package/resources/skills/skills/meta/cue-developer/references/skill_and_mcp_authoring.md +84 -0
  137. package/resources/skills/skills/meta/cue-developer/references/troubleshooting.md +42 -0
  138. package/resources/skills/skills/meta/delegation-check/SKILL.md +148 -0
  139. package/resources/skills/skills/meta/delegation-check/specs/scan-algorithm.md +125 -0
  140. package/resources/skills/skills/meta/delegation-check/specs/separation-rules.md +190 -0
  141. package/resources/skills/skills/meta/focus/SKILL.md +62 -0
  142. package/resources/skills/skills/meta/help/SKILL.md +1 -1
  143. package/resources/skills/skills/meta/integrity-tags/SKILL.md +2 -0
  144. package/resources/skills/skills/meta/next-steps/SKILL.md +124 -0
  145. package/resources/skills/skills/meta/next-steps/evals/eval-set.json +92 -0
  146. package/resources/skills/skills/meta/profile-from-docs/SKILL.md +141 -0
  147. package/resources/skills/skills/meta/ralph-loop/SKILL.md +83 -0
  148. package/resources/skills/skills/meta/ralph-loop/scripts/loop.sh +73 -0
  149. package/resources/skills/skills/meta/skill-simplify/SKILL.md +136 -0
  150. package/resources/skills/skills/meta/skill-simplify/phases/01-analysis.md +173 -0
  151. package/resources/skills/skills/meta/skill-simplify/phases/02-optimize.md +104 -0
  152. package/resources/skills/skills/meta/skill-simplify/phases/03-check.md +145 -0
  153. package/resources/skills/skills/meta/smart-loader/scripts/smart-lookup.sh +13 -4
  154. package/resources/skills/skills/meta/verify-council/SKILL.md +182 -0
  155. package/resources/skills/skills/meta/verify-council/references/lane-prompts.md +103 -0
  156. package/resources/skills/skills/meta/verify-council/references/workflow.js +217 -0
  157. package/resources/skills/skills/nvidia/aiq-research/SKILL.md +1 -1
  158. package/resources/skills/skills/nvidia/cuopt-developer/SKILL.md +16 -1
  159. package/resources/skills/skills/nvidia/cuopt-developer/resources/contributing.md +2 -2
  160. package/resources/skills/skills/nvidia/cuopt-developer/resources/numerical_debugging.md +128 -0
  161. package/resources/skills/skills/nvidia/cuopt-developer/resources/python_bindings.md +2 -9
  162. package/resources/skills/skills/nvidia/cuopt-developer/resources/vrp_skills.md +166 -0
  163. package/resources/skills/skills/nvidia/cuopt-install/SKILL.md +2 -10
  164. package/resources/skills/skills/nvidia/cuopt-numerical-optimization-api-c/SKILL.md +3 -23
  165. package/resources/skills/skills/nvidia/cuopt-numerical-optimization-api-c/resources/examples.md +40 -20
  166. package/resources/skills/skills/nvidia/cuopt-numerical-optimization-api-python/SKILL.md +5 -1
  167. package/resources/skills/skills/nvidia/skill-evolution/SKILL.md +4 -5
  168. package/resources/skills/skills/research/trendradar/SKILL.md +1 -1
  169. package/resources/skills/skills/ssh/ssh-config/SKILL.md +94 -0
  170. package/resources/skills/skills/ssh/ssh-copy/SKILL.md +92 -0
  171. package/resources/skills/skills/ssh/ssh-harden/SKILL.md +108 -0
  172. package/resources/skills/skills/ssh/ssh-keys/SKILL.md +82 -0
  173. package/resources/skills/skills/ssh/ssh-paste-image/LICENSE +28 -0
  174. package/resources/skills/skills/ssh/ssh-paste-image/SKILL.md +149 -0
  175. package/resources/skills/skills/ssh/ssh-paste-image/scripts/build.sh +29 -0
  176. package/resources/skills/skills/ssh/ssh-paste-image/scripts/client/go.mod +3 -0
  177. package/resources/skills/skills/ssh/ssh-paste-image/scripts/client/main.go +79 -0
  178. package/resources/skills/skills/ssh/ssh-paste-image/scripts/daemon/ccimgd.service +12 -0
  179. package/resources/skills/skills/ssh/ssh-paste-image/scripts/daemon/com.ccimgd.plist +20 -0
  180. package/resources/skills/skills/ssh/ssh-paste-image/scripts/daemon/go.mod +3 -0
  181. package/resources/skills/skills/ssh/ssh-paste-image/scripts/daemon/main.go +98 -0
  182. package/resources/skills/skills/ssh/ssh-tunnel/SKILL.md +96 -0
  183. package/resources/skills/skills/strapi/building-with-strapi/SKILL.md +112 -0
  184. package/resources/skills/skills/strapi/strapi-cli/SKILL.md +93 -0
  185. package/resources/skills/skills/strapi/strapi-content-api/SKILL.md +115 -0
  186. package/resources/skills/skills/strapi/strapi-deploy/SKILL.md +89 -0
  187. package/resources/skills/skills/strapi/strapi-mcp-setup/SKILL.md +101 -0
  188. package/resources/skills/skills/strapi/strapi-plugins/SKILL.md +97 -0
  189. package/resources/skills/skills/tools/context7/SKILL.md +101 -0
  190. package/resources/skills/skills/tools/opensrc/SKILL.md +1 -1
  191. package/resources/skills/skills/tools/portless/SKILL.md +186 -0
  192. package/resources/skills/skills/xbot/operate/SKILL.md +229 -0
  193. package/src/commands/_index.ts +8 -0
  194. package/src/commands/ai-score.e2e.test.ts +11 -4
  195. package/src/commands/ai.ts +3 -4
  196. package/src/commands/auto-detect.ts +1 -1
  197. package/src/commands/cli.test.ts +1 -2
  198. package/src/commands/cli.ts +1 -1
  199. package/src/commands/cloud.ts +1 -1
  200. package/src/commands/current.ts +1 -4
  201. package/src/commands/dash.test.ts +110 -0
  202. package/src/commands/dash.ts +194 -0
  203. package/src/commands/dashboard.ts +26 -0
  204. package/src/commands/diff.ts +1 -1
  205. package/src/commands/discover.test.ts +1 -1
  206. package/src/commands/discover.ts +90 -40
  207. package/src/commands/doctor.test.ts +58 -0
  208. package/src/commands/doctor.ts +79 -3
  209. package/src/commands/eval-behavior.ts +1 -1
  210. package/src/commands/eval.ts +2 -2
  211. package/src/commands/evolve.ts +4 -3
  212. package/src/commands/failures.test.ts +1 -1
  213. package/src/commands/features-batch1.test.ts +6 -1
  214. package/src/commands/icon.ts +1 -5
  215. package/src/commands/import-profile.ts +1 -1
  216. package/src/commands/init.ts +50 -7
  217. package/src/commands/install-sh.e2e.test.ts +65 -0
  218. package/src/commands/launch-handoff.e2e.test.ts +88 -0
  219. package/src/commands/launch.e2e.test.ts +8 -1
  220. package/src/commands/launch.test.ts +29 -0
  221. package/src/commands/launch.ts +185 -131
  222. package/src/commands/lock.ts +0 -1
  223. package/src/commands/marketplace.ts +0 -4
  224. package/src/commands/materialize.ts +1 -1
  225. package/src/commands/mem.ts +341 -0
  226. package/src/commands/optimizer.ts +0 -3
  227. package/src/commands/playground.ts +1 -2
  228. package/src/commands/profile-draft-skill.ts +1 -1
  229. package/src/commands/replay-whatif.ts +1 -6
  230. package/src/commands/score.ts +2 -2
  231. package/src/commands/security.test.ts +88 -0
  232. package/src/commands/security.ts +74 -28
  233. package/src/commands/shell.test.ts +65 -4
  234. package/src/commands/shell.ts +67 -7
  235. package/src/commands/skills-test.ts +0 -1
  236. package/src/commands/skills.ts +28 -2
  237. package/src/commands/sources.ts +1 -2
  238. package/src/commands/status.ts +2 -6
  239. package/src/commands/submit-profile.ts +1 -1
  240. package/src/commands/suggest.ts +35 -10
  241. package/src/commands/trigger-gaps.test.ts +50 -0
  242. package/src/commands/trigger-gaps.ts +63 -29
  243. package/src/commands/update.ts +1 -1
  244. package/src/commands/validate.ts +16 -4
  245. package/src/commands/watch-live.ts +1 -1
  246. package/src/commands/workspace.ts +1 -1
  247. package/src/index.ts +26 -10
  248. package/src/lib/active-sessions.ts +1 -1
  249. package/src/lib/agent-adapters.test.ts +100 -0
  250. package/src/lib/agent-adapters.ts +2 -2
  251. package/src/lib/analytics.test.ts +88 -0
  252. package/src/lib/analytics.ts +82 -1
  253. package/src/lib/auto-detect.test.ts +10 -4
  254. package/src/lib/auto-detect.ts +19 -23
  255. package/src/lib/brand-icons.ts +0 -1
  256. package/src/lib/cache.ts +2 -3
  257. package/src/lib/claude-mem-env.test.ts +148 -0
  258. package/src/lib/claude-mem-env.ts +172 -0
  259. package/src/lib/combo-history.test.ts +53 -0
  260. package/src/lib/combo-history.ts +83 -0
  261. package/src/lib/companion-detect.test.ts +108 -0
  262. package/src/lib/companion-detect.ts +140 -0
  263. package/src/lib/companion-fetch.ts +4 -6
  264. package/src/lib/conditional-skills.test.ts +1 -1
  265. package/src/lib/config-paths.test.ts +53 -0
  266. package/src/lib/config-paths.ts +33 -0
  267. package/src/lib/dashboard-server.test.ts +351 -0
  268. package/src/lib/dashboard-server.ts +1476 -27
  269. package/src/lib/debug-log.test.ts +66 -0
  270. package/src/lib/debug-log.ts +45 -0
  271. package/src/lib/mcp-catalog.test.ts +102 -0
  272. package/src/lib/mcp-catalog.ts +193 -0
  273. package/src/lib/pair-suggestions.test.ts +111 -0
  274. package/src/lib/pair-suggestions.ts +98 -5
  275. package/src/lib/permissions.test.ts +76 -0
  276. package/src/lib/permissions.ts +125 -0
  277. package/src/lib/picker.test.ts +1106 -1
  278. package/src/lib/picker.ts +1230 -142
  279. package/src/lib/plugin-discovery.ts +126 -0
  280. package/src/lib/pr-poster.ts +1 -1
  281. package/src/lib/pr-throttle.ts +2 -6
  282. package/src/lib/profile-linter.test.ts +67 -1
  283. package/src/lib/profile-linter.ts +59 -14
  284. package/src/lib/profile-loader.test.ts +21 -0
  285. package/src/lib/profile-loader.ts +22 -3
  286. package/src/lib/profile-metrics.ts +2 -6
  287. package/src/lib/profile-names.test.ts +58 -0
  288. package/src/lib/repos.test.ts +57 -0
  289. package/src/lib/repos.ts +167 -0
  290. package/src/lib/resolver-npx.ts +10 -1
  291. package/src/lib/runtime-materializer.test.ts +200 -3
  292. package/src/lib/runtime-materializer.ts +129 -20
  293. package/src/lib/shared-profiles.ts +2 -3
  294. package/src/lib/skill-clis.test.ts +113 -0
  295. package/src/lib/skill-clis.ts +232 -0
  296. package/src/lib/skill-dependencies.ts +9 -1
  297. package/src/lib/skill-deps.ts +1 -1
  298. package/src/lib/skill-linter.ts +1 -1
  299. package/src/lib/skill-quality.ts +0 -1
  300. package/src/lib/skill-sandbox.test.ts +1 -1
  301. package/src/lib/skills-lock.test.ts +1 -1
  302. package/src/lib/telemetry-consent.ts +3 -5
  303. package/src/lib/telemetry-report.test.ts +2 -2
  304. package/src/lib/token-budget.ts +111 -0
  305. package/src/lib/trigger-gaps.test.ts +70 -0
  306. package/src/lib/trigger-gaps.ts +48 -6
  307. package/src/lib/tui/data.ts +1 -5
  308. package/src/lib/workflow-store.ts +150 -0
  309. package/src/lib/workspace-secrets.ts +0 -4
  310. package/src/lib/workspaces.ts +1 -1
@@ -0,0 +1,186 @@
1
+ ---
2
+ name: portless
3
+ description: Named .localhost HTTPS URLs for local dev instead of port numbers. Use when the user says "portless", "https on localhost", "named dev URL", "stop using port 3000", or runs multiple dev servers (Next.js, Vite, Medusa) that collide on ports. Prefer it for Medusa shop local dev.
4
+ allowed-tools: Bash(portless:*), Bash(npm:*)
5
+ category: tools
6
+ tags: [tools, local-dev, https, proxy, dev-server, medusa, monorepo]
7
+ requires_mcps: []
8
+ metadata:
9
+ version: 1.0.0
10
+ homepage: https://github.com/vercel-labs/portless
11
+ ---
12
+
13
+ # portless: named .localhost URLs for local dev
14
+
15
+ Run dev servers behind stable `https://<name>.localhost` URLs instead of memorizing port numbers. portless starts a local HTTPS proxy, assigns each app an ephemeral port via `PORT`, and routes a clean URL to it. HTTP/2 + a trusted local CA come on by default, so no browser warnings and no port collisions when several servers run at once.
16
+
17
+ Pre-1.0 (current 0.13.x): the state-dir format can change between releases, so re-run `portless trust` after upgrades if certs stop working.
18
+
19
+ ## When to activate
20
+
21
+ - User wants `https://myapp.localhost` instead of `http://localhost:3000`.
22
+ - Two or more dev servers fight over ports (admin + storefront, web + api).
23
+ - A Medusa shop needs local URLs for both the backend and the storefront.
24
+ - User asks for HTTPS/HTTP-2 on localhost without manual mkcert setup.
25
+ - A monorepo (pnpm/turbo) needs one URL per workspace package.
26
+
27
+ ## Prerequisites
28
+
29
+ Install once, globally (recommended so every project shares one proxy + CA):
30
+
31
+ ```bash
32
+ npm install -g portless
33
+ portless --version # -> portless 0.13.x
34
+ ```
35
+
36
+ First run generates a local CA, trusts it, and binds port 443 (auto-elevates with `sudo` on macOS/Linux). To skip HTTPS use `--no-tls`.
37
+
38
+ ## Step 1: run a single app
39
+
40
+ Infer the name from `package.json`/git root and run the `dev` script through the proxy:
41
+
42
+ ```bash
43
+ portless run next dev
44
+ # -> https://myapp.localhost
45
+ ```
46
+
47
+ Or name it explicitly:
48
+
49
+ ```bash
50
+ portless myapp next dev
51
+ # -> https://myapp.localhost
52
+ ```
53
+
54
+ Bare `portless` (no args) runs the `dev` script and infers the name:
55
+
56
+ ```bash
57
+ portless # -> runs "dev", https://<project>.localhost
58
+ ```
59
+
60
+ ## Step 2: wire it into package.json
61
+
62
+ Put the proxy in the script once so it works for everyone:
63
+
64
+ ```jsonc
65
+ { "scripts": { "dev": "portless run next dev" } }
66
+ ```
67
+
68
+ With a `portless.json` you can keep the script clean and run `portless` to route it:
69
+
70
+ ```jsonc
71
+ // portless.json
72
+ { "name": "myapp" }
73
+ ```
74
+
75
+ ```bash
76
+ portless # runs "dev", https://myapp.localhost
77
+ PORTLESS=0 pnpm dev # bypass the proxy, use the default port
78
+ ```
79
+
80
+ ## Step 3: subdomains and monorepos
81
+
82
+ Organize services under subdomains:
83
+
84
+ ```bash
85
+ portless api.myapp pnpm start # -> https://api.myapp.localhost
86
+ portless docs.myapp next dev # -> https://docs.myapp.localhost
87
+ ```
88
+
89
+ One `portless.json` at the repo root covers every workspace package (pnpm/npm/yarn/bun). The `apps` map is only for name overrides:
90
+
91
+ ```jsonc
92
+ {
93
+ "apps": {
94
+ "apps/web": { "name": "myapp" },
95
+ "apps/api": { "name": "api.myapp" }
96
+ }
97
+ }
98
+ ```
99
+
100
+ ```bash
101
+ portless # from repo root: start every package with a "dev" script
102
+ cd apps/web && portless # start just one
103
+ ```
104
+
105
+ ## Step 4: Medusa shop local dev (preferred)
106
+
107
+ For `medusa-shops/<shop>`, route both halves through portless so admin and storefront stop colliding on ports:
108
+
109
+ ```bash
110
+ # backend (Medusa serves admin on the injected PORT)
111
+ portless admin.myshop medusa develop # -> https://admin.myshop.localhost
112
+
113
+ # storefront
114
+ portless myshop pnpm dev # -> https://myshop.localhost
115
+ ```
116
+
117
+ Next.js storefronts must allow the dev origin:
118
+
119
+ ```js
120
+ // next.config.js
121
+ module.exports = { allowedDevOrigins: ["myshop.localhost", "*.myshop.localhost"] }
122
+ ```
123
+
124
+ If the storefront proxies `/api` to the backend, set `changeOrigin: true` or portless returns `508 Loop Detected` (it rewrites the `Host` header back to the proxy). See Rules.
125
+
126
+ Related: the `medusa-local-dev` skill runs many shops on a fixed-port registry (`.dev-ports.yaml`) with the `medusa-dev` helper for lifecycle (start/stop/tail). portless is the preferred URL layer on top: keep `medusa-dev` for process management, or pass `portless --app-port <registry-port>` to give each shop a clean `https://<shop>.localhost` instead of a bare port.
127
+
128
+ ## Step 5: git worktrees
129
+
130
+ `portless run` auto-detects linked worktrees and prepends the branch as a subdomain, so each worktree gets its own URL with zero config:
131
+
132
+ ```bash
133
+ # main checkout
134
+ portless run next dev # -> https://myapp.localhost
135
+ # worktree on branch "fix-ui"
136
+ portless run next dev # -> https://fix-ui.myapp.localhost
137
+ ```
138
+
139
+ ## Commands reference
140
+
141
+ ```bash
142
+ portless list # show active routes (local + tailnet)
143
+ portless alias <name> <port> # static route, e.g. point a name at a Docker port
144
+ portless trust # (re)add the local CA to the system trust store
145
+ portless prune # kill orphaned dev servers from crashed sessions
146
+ portless hosts sync # add routes to /etc/hosts (fixes Safari)
147
+ portless clean # remove state, CA trust entry, and hosts block
148
+
149
+ portless proxy start # start the HTTPS proxy (port 443, daemon)
150
+ portless proxy start --no-tls # plain HTTP on port 80
151
+ portless proxy start -p 1355 # custom port, no sudo
152
+ portless proxy stop
153
+
154
+ portless service install # start the proxy at OS startup (survives reboot)
155
+ portless service status
156
+ portless service uninstall
157
+ ```
158
+
159
+ Useful flags: `--no-tls`, `--tld test` (use `.test`, IANA-reserved), `--wildcard` (unregistered subdomains fall back to parent), `--app-port <n>` (fixed port), `--tailscale` / `--funnel` (share over tailnet/public), `--force` (take over a route).
160
+
161
+ ## Examples
162
+
163
+ **User:** "my next storefront and medusa admin both want port 3000, fix it"
164
+ → `npm i -g portless`, then `portless admin.shop medusa develop` and `portless shop pnpm dev`. Two clean HTTPS URLs, no port clash. Add `allowedDevOrigins` to `next.config.js`.
165
+
166
+ **User:** "give me https on localhost without mkcert"
167
+ → `portless run next dev`. First run trusts a local CA automatically; the app is at `https://<project>.localhost` with HTTP/2.
168
+
169
+ **User:** "Safari can't reach myapp.localhost"
170
+ → `portless hosts sync` (Safari uses the system resolver, which may not handle `.localhost` subdomains).
171
+
172
+ **User:** "spin up every app in this pnpm monorepo"
173
+ → from the repo root, `portless`. It discovers workspace packages and starts each one's `dev` script under `<package>.<project>.localhost`.
174
+
175
+ **User:** "I keep getting 508 Loop Detected"
176
+ → the frontend proxies to another portless app without rewriting `Host`. Set `changeOrigin: true` (Vite/webpack) so portless routes to the target app, not back to itself.
177
+
178
+ ## Rules
179
+
180
+ - **Prefer portless in Medusa shops.** Backend (`admin.<shop>.localhost`) and storefront (`<shop>.localhost`) collide on ports otherwise; named URLs remove the juggling and match the deployed `admin.<shop>.hu` shape. WHY: it mirrors prod hostnames and lets both run together.
181
+ - **Install globally for shared state.** One global install means one proxy and one CA across projects. Per-project installs can run different pre-1.0 versions whose state formats diverge, which breaks trust. WHY: avoids re-trusting per repo.
182
+ - **Rewrite the Host header on app-to-app proxies** (`changeOrigin: true`). WHY: without it portless loops the request back to the caller and answers `508 Loop Detected`.
183
+ - **Add storefront origins to `allowedDevOrigins`.** WHY: frameworks reject cross-origin dev requests from the new `.localhost` host otherwise.
184
+ - **CI / non-interactive: portless exits with an error instead of prompting.** Use `PORTLESS=0` to bypass the proxy in scripts that should hit the raw port. WHY: task runners (turbo, CI) must fail fast, not hang on a TTY prompt.
185
+ - **Avoid `.local` and `.dev` TLDs.** `.local` clashes with mDNS/Bonjour; `.dev` is HSTS-forced by Google. Use the default `.localhost` or `--tld test`. WHY: collision and forced-HTTPS surprises.
186
+ - **`run get alias hosts list trust clean prune proxy service` are reserved** subcommands and cannot be app names. Use `portless run <cmd>` or `portless --name <name> <cmd>` to force one.
@@ -0,0 +1,229 @@
1
+ ---
2
+ name: operate
3
+ description: >-
4
+ Use when the user wants to run, drive, or supervise the x-growth-bot (the
5
+ X / Twitter growth bot in ~/Documents/x-growth-bot) from inside Claude Code.
6
+ Triggers: "start the bot", "stop the bot", "bot status", "is the loop
7
+ running", "pause the bot", "resume the bot", "draft a reply", "send a DM",
8
+ "run a dry cycle", "show pending replies", "what's the bot doing", "check
9
+ caps", "engage candidates", "growth report". Routes every action to the
10
+ right surface: the `xbot` MCP tools, the `./botctl` wrapper, or
11
+ `python -m xbot.cli`, and enforces the loop-vs-MCP write-safety rule.
12
+ triggers:
13
+ - start the bot
14
+ - stop the bot
15
+ - bot status
16
+ - pause the bot
17
+ - resume the bot
18
+ - draft a reply
19
+ - send a DM
20
+ - run a dry cycle
21
+ - show pending replies
22
+ - growth report
23
+ allowed-tools:
24
+ - Bash(./botctl:*)
25
+ - Bash(.venv/bin/python:*)
26
+ - Bash(claude:*)
27
+ - Bash(cat:*)
28
+ - Bash(ls:*)
29
+ - Bash(tail:*)
30
+ - Read
31
+ requires_mcps:
32
+ - xbot
33
+ tags: [xbot, twitter, x, growth, social, bot, operate, mcp]
34
+ domain: social
35
+ category: operate
36
+ ---
37
+
38
+ ## What this skill does
39
+
40
+ Operates the **x-growth-bot** (an autonomous X / Twitter growth bot at
41
+ `~/Documents/x-growth-bot`) from a Claude Code session. It picks the correct
42
+ control surface for each request and keeps live actions safe when the
43
+ background loop is running.
44
+
45
+ The bot has three control surfaces, all already built:
46
+
47
+ | Surface | How | Best for |
48
+ | --- | --- | --- |
49
+ | `xbot` **MCP** | MCP tools (`xbot_status`, `xbot_reply`, …) | reads, control, drafting and posting from inside this chat |
50
+ | `./botctl` | Bash wrapper | starting/stopping the loop, operator board, quick status |
51
+ | `xbot.cli` | `.venv/bin/python -m xbot.cli <cmd>` | full surface (`preflight`, `propose`, `commit`, `loop`) |
52
+
53
+ The MCP and the loop are **separate processes sharing the same on-disk state
54
+ with no lock**. The one hard rule below exists because of that.
55
+
56
+ ## Prerequisites
57
+
58
+ The bot lives at `~/Documents/x-growth-bot` and ships its own tooling. No global
59
+ install is needed beyond what the repo already provides:
60
+
61
+ - `./botctl` and `.venv/bin/python -m xbot.cli` come from the repo (the venv is
62
+ created on setup). Run them from the repo root so state files resolve.
63
+ - The `xbot` MCP server is registered with `claude mcp add xbot --scope local --
64
+ bash ~/Documents/x-growth-bot/run-mcp.sh`. Newly added tools appear after a
65
+ Claude Code restart.
66
+
67
+ ## When to activate
68
+
69
+ - "start / stop / restart the bot", "is the loop alive", "bot status"
70
+ - "pause the bot" / "resume the bot" (emergency circuit)
71
+ - "draft a reply to this tweet", "send a DM", "show pending replies"
72
+ - "run a dry cycle so I can watch it think"
73
+ - "show today's caps / growth / attribution / report"
74
+ - "what did the bot skip and why" (audit log)
75
+
76
+ ## The one hard rule (loop vs MCP writes)
77
+
78
+ The live `loop` daemon and the `xbot` MCP each build their own session on the
79
+ same account and share caps / history / circuit files with **no cross-process
80
+ lock**. Reads and control via MCP are always safe. For **live writes** via MCP
81
+ (`xbot_reply`, `xbot_dm`, `xbot_like`, `xbot_retweet`, `xbot_follow`,
82
+ `xbot_cycle` with `dry_run=False`):
83
+
84
+ 1. Check the loop first: `./botctl status`
85
+ 2. If it is alive, pause or stop it before the live write:
86
+ `./botctl pause "manual MCP session"` (or `./botctl stop`)
87
+ 3. Do the live write.
88
+ 4. Resume: `./botctl resume` (or `./botctl start`).
89
+
90
+ Skipping this risks double-counted caps or two processes posting at once, which
91
+ is exactly the ban-risk the circuit guards against.
92
+
93
+ ## Step 1: Orient before acting
94
+
95
+ Always read state before any write. From inside this chat, prefer the MCP:
96
+
97
+ - `xbot_status`: account health, remaining caps, pacing window (read-only)
98
+ - `xbot_control`: circuit state + per-action caps + pending + growth in one call
99
+ - `xbot_plan`: one-shot operator context (status + persona + candidates)
100
+
101
+ From a shell:
102
+
103
+ ```bash
104
+ cd ~/Documents/x-growth-bot
105
+ ./botctl status # is the loop alive? pid + next-cycle heartbeat
106
+ ./botctl board # one-shot operator board
107
+ ```
108
+
109
+ If the MCP tools are not visible in this session, see "Tools missing?" below.
110
+
111
+ ## Step 2: Pick the surface for the request
112
+
113
+ **Reads / dashboards (always safe, no loop interaction):**
114
+
115
+ | Want | MCP tool | CLI fallback |
116
+ | --- | --- | --- |
117
+ | Account health + caps | `xbot_status` | `xbot.cli status` |
118
+ | Operator board | `xbot_control` | `xbot.cli control` |
119
+ | Growth dashboard | `xbot_report` | `xbot.cli report` |
120
+ | Follower delta | `xbot_followers` | `xbot.cli followers` |
121
+ | Follow-back attribution | `xbot_attribution` | `xbot.cli attribution` |
122
+ | Engage candidates | `xbot_candidates` | `xbot.cli discover` |
123
+ | Brain judgement of candidates | `xbot_think` | `xbot.cli think` |
124
+ | Why it skipped | `xbot_log` | `xbot.cli log` |
125
+ | Parked replies | `xbot_pending` | `xbot.cli pending` |
126
+ | Inbound DMs | `xbot_inbox` | n/a |
127
+
128
+ **Control (safe, no posting):**
129
+
130
+ - Pause: `xbot_pause("reason", hours=6)` or `./botctl pause "reason"`
131
+ - Resume: `xbot_resume()` or `./botctl resume`
132
+ - Read/set a knob: `xbot_config("actions.reply.daily_cap")` (omit value to read)
133
+
134
+ **Loop lifecycle (shell only):**
135
+
136
+ ```bash
137
+ ./botctl start # LIVE engagement loop, detached, pid-managed
138
+ ./botctl start --dry # DRY-RUN loop (no live actions) to watch it think
139
+ ./botctl stop
140
+ ./botctl restart
141
+ ```
142
+
143
+ **Live writes (apply the hard rule above first):**
144
+
145
+ - Post a reply: `xbot_reply(...)`: runs every guard (trust gate, caps, circuit)
146
+ - Send a DM: `xbot_dm(...)`: answer inbound by passing the inbound id
147
+ - Like / retweet / follow: `xbot_like`, `xbot_retweet`, `xbot_follow`
148
+ (each defaults to `dry_run=True`; pass `dry_run=False` to act)
149
+ - One full cycle: `xbot_cycle(dry_run=True)` first, then `dry_run=False`
150
+
151
+ ## Step 3: Draft, gate, then post
152
+
153
+ Never post raw. The bot has a trust gate; use it.
154
+
155
+ 1. Draft the reply text yourself (read `xbot_persona` for the voice + rules).
156
+ 2. Second-opinion it: `xbot_trust_check(tweet_text, draft)` runs the
157
+ adversarial skeptic panel.
158
+ 3. Only on a pass, call `xbot_reply(...)` (after the loop-safety rule).
159
+
160
+ For DMs, `xbot_dm_plan` gives the one-shot context (caps + circuit + inbox)
161
+ before you draft.
162
+
163
+ ## Step 4: Verify the action landed
164
+
165
+ After a write, confirm with a read:
166
+
167
+ - `xbot_status`: caps decremented as expected
168
+ - `xbot_log`: the action appears in the audit log
169
+ - `./botctl status`: loop heartbeat unchanged if you paused/resumed cleanly
170
+
171
+ ## First live run
172
+
173
+ Before the first `--live` run, run the offline go/no-go check:
174
+
175
+ ```bash
176
+ cd ~/Documents/x-growth-bot
177
+ .venv/bin/python -m xbot.cli preflight
178
+ ```
179
+
180
+ ## Tools missing?
181
+
182
+ Newly-added MCP tools need a **Claude Code session restart** to appear.
183
+ `claude mcp get xbot` showing `✓ Connected` only means the server handshakes,
184
+ not that this session has the tools. If `xbot_*` tools are not callable, fall
185
+ back to `./botctl` / `xbot.cli` in Bash for this session and restart for the
186
+ MCP next time.
187
+
188
+ ## What this skill does NOT do
189
+
190
+ - Does not write tweet/reply/DM copy for you. You draft; the bot gates and posts.
191
+ - Does not edit the bot's source (`src/xbot/`). Use a coding profile for that.
192
+ - Does not bypass caps, the circuit, or the trust gate. If an action is blocked,
193
+ surface the reason; do not work around the guard.
194
+ - Does not manage X login / cookies. Auth recovery is a separate concern.
195
+
196
+ ## Examples
197
+
198
+ <example>
199
+ User: is the bot running and how are we doing today?
200
+ Action: Call `xbot_control` (or `./botctl board`) for circuit + caps + growth in
201
+ one shot, then `xbot_status` if more cap detail is needed. Report loop liveness,
202
+ remaining caps, and follower delta. No writes.
203
+ </example>
204
+
205
+ <example>
206
+ User: pause the bot, I think it's being too aggressive
207
+ Action: `xbot_pause("operator: too aggressive", hours=6)` (or
208
+ `./botctl pause "too aggressive"`). Confirm with `xbot_control` that the circuit
209
+ is open. Tell the user how to resume.
210
+ </example>
211
+
212
+ <example>
213
+ User: draft a reply to this tweet and post it
214
+ Action: Read `xbot_persona`. Draft the reply. Run `xbot_trust_check(tweet, draft)`.
215
+ If it passes: run `./botctl status`; if the loop is live, `./botctl pause` first,
216
+ then `xbot_reply(...)`, then `./botctl resume`. Verify with `xbot_log`.
217
+ </example>
218
+
219
+ <example>
220
+ User: run a dry cycle so I can see what it would do
221
+ Action: `xbot_cycle(dry_run=True)` (no loop interaction needed for a dry run).
222
+ Summarize the candidates it judged and the actions it would have taken.
223
+ </example>
224
+
225
+ <example>
226
+ User: show me the replies waiting for my approval
227
+ Action: `xbot_pending` (or `xbot.cli pending`). List each parked reply with its
228
+ target so the user can approve. Read-only.
229
+ </example>
@@ -382,10 +382,18 @@ export const COMMANDS = {
382
382
  summary: "Boot the local read-only dashboard server (JSON endpoints; React UI in next turn)",
383
383
  load: () => import("./dashboard"),
384
384
  },
385
+ dash: {
386
+ summary: "Query + drive the running dashboard's API (status/profiles/profile/add-mcp/kill/...)",
387
+ load: () => import("./dash"),
388
+ },
385
389
  mcp: {
386
390
  summary: "Expose cue data over MCP (stdio JSON-RPC) so Claude can query it as tool calls",
387
391
  load: () => import("./mcp"),
388
392
  },
393
+ mem: {
394
+ summary: "Inspect/manage per-profile claude-mem stores (status / path / ports / seed)",
395
+ load: () => import("./mem"),
396
+ },
389
397
  } as const satisfies Record<string, Command>;
390
398
 
391
399
  export type CommandName = keyof typeof COMMANDS;
@@ -8,6 +8,11 @@ import { join } from "node:path";
8
8
 
9
9
  const CUE_BIN = join(import.meta.dir, "../index.ts");
10
10
 
11
+ // Skip when a child `bun` can't be spawned (some sandboxes / odd PATH setups);
12
+ // these tests shell out to `bun run` and would otherwise hard-fail the suite
13
+ // with "Executable not found in $PATH: bun". CI installs bun via setup-bun.
14
+ const BUN_SPAWNABLE = spawnSync("bun", ["--version"], { encoding: "utf8" }).status === 0;
15
+
11
16
  function cue(args: string[]): { status: number; stdout: string; stderr: string } {
12
17
  const res = spawnSync("bun", ["run", CUE_BIN, ...args], {
13
18
  encoding: "utf8",
@@ -16,11 +21,13 @@ function cue(args: string[]): { status: number; stdout: string; stderr: string }
16
21
  return { status: res.status ?? 1, stdout: res.stdout ?? "", stderr: res.stderr ?? "" };
17
22
  }
18
23
 
19
- describe("cue ai", () => {
20
- test("matches python-api for python/fastapi description", () => {
24
+ describe.skipIf(!BUN_SPAWNABLE)("cue ai", () => {
25
+ test("matches python for python/fastapi description", () => {
21
26
  const res = cue(["ai", "python fastapi sqlalchemy"]);
22
27
  expect(res.status).toBe(0);
23
- expect(res.stdout).toContain("python-api");
28
+ expect(res.stdout).toContain("python");
29
+ // Guard against the phantom name returning (it's a substring of "python")
30
+ expect(res.stdout).not.toContain("python-api");
24
31
  });
25
32
 
26
33
  test("matches rust for rust/cargo description", () => {
@@ -66,7 +73,7 @@ describe("cue ai", () => {
66
73
  });
67
74
  });
68
75
 
69
- describe("cue score", () => {
76
+ describe.skipIf(!BUN_SPAWNABLE)("cue score", () => {
70
77
  test("scores a specific profile", () => {
71
78
  const res = cue(["score", "--profile", "core"]);
72
79
  expect(res.status).toBe(0);
@@ -10,12 +10,11 @@
10
10
  * cue ai "rust cli tool"
11
11
  */
12
12
 
13
- import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
13
+ import { readFileSync, writeFileSync, mkdirSync } from "node:fs";
14
14
  import { join, resolve, dirname } from "node:path";
15
15
  import { fileURLToPath } from "node:url";
16
16
  import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
17
17
 
18
- import { listProfiles } from "../lib/profile-loader";
19
18
 
20
19
  const REPO_ROOT = process.env.CUE_REPO_ROOT ?? process.env.SOUL_REPO_ROOT ?? resolve(dirname(fileURLToPath(import.meta.url)), "..", "..");
21
20
  const PROFILES_DIR = join(REPO_ROOT, "profiles");
@@ -28,11 +27,11 @@ interface MatchedProfile {
28
27
  }
29
28
 
30
29
  // Keywords → profile mapping
31
- const PROFILE_KEYWORDS: Record<string, string[]> = {
30
+ export const PROFILE_KEYWORDS: Record<string, string[]> = {
32
31
  "nextjs": ["next", "nextjs", "next.js", "vercel", "app router", "server components", "react ssr"],
33
32
  "frontend": ["react", "vue", "svelte", "frontend", "ui", "tailwind", "vite", "css", "component"],
34
33
  "backend": ["api", "express", "fastify", "hono", "webhook", "rest", "graphql", "node server", "prisma", "drizzle"],
35
- "python-api": ["python", "fastapi", "django", "flask", "sqlalchemy", "alembic", "uvicorn", "pytest", "pip"],
34
+ "python": ["python", "fastapi", "django", "flask", "sqlalchemy", "alembic", "uvicorn", "pytest", "pip"],
36
35
  "rust": ["rust", "cargo", "tokio", "async rust", "cli tool", "systems", "wasm"],
37
36
  "go-api": ["go", "golang", "gin", "echo", "chi", "gorm", "goroutine"],
38
37
  "medusa-dev": ["medusa", "ecommerce", "storefront", "shop", "cart", "checkout"],
@@ -4,7 +4,7 @@
4
4
 
5
5
  import { writeFileSync } from "node:fs";
6
6
  import { join } from "node:path";
7
- import { detectProfile, detectProfileV2 } from "../lib/auto-detect";
7
+ import { detectProfile, } from "../lib/auto-detect";
8
8
  import { scanProject } from "../lib/project-scanner";
9
9
 
10
10
  export async function run(args: string[]): Promise<number> {
@@ -180,8 +180,7 @@ describe("cue cli (top-level)", () => {
180
180
 
181
181
  test("unknown subcommand exits 1", async () => {
182
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; };
183
+ (process.stderr as any).write = () => true;
185
184
  try {
186
185
  const exit = await cliRun(["nonsense"]);
187
186
  expect(exit).toBe(1);
@@ -15,7 +15,7 @@
15
15
  */
16
16
 
17
17
  import { spawnSync } from "node:child_process";
18
- import { existsSync, readFileSync } from "node:fs";
18
+ import { readFileSync } from "node:fs";
19
19
  import { join, dirname, resolve } from "node:path";
20
20
  import { fileURLToPath } from "node:url";
21
21
  import { homedir, platform } from "node:os";
@@ -156,7 +156,7 @@ async function cmdPush(args: string[]): Promise<number> {
156
156
  }
157
157
 
158
158
  process.stderr.write(`⚠️ Push failed (${res.status}): ${await res.text()}\n`);
159
- } catch (err) {
159
+ } catch {
160
160
  process.stderr.write(`⚠️ Cloud API not reachable. Profile saved locally only.\n`);
161
161
  process.stderr.write(` Will sync when API is live. Run \`cue push ${profileName}\` again later.\n`);
162
162
  }
@@ -14,10 +14,7 @@ import { join } from "node:path";
14
14
 
15
15
  import { resolveProfileForCwd } from "../lib/cwd-resolver";
16
16
  import { loadProfile } from "../lib/profile-loader";
17
-
18
- function configDir(): string {
19
- return process.env.XDG_CONFIG_HOME ? join(process.env.XDG_CONFIG_HOME, "cue") : join(homedir(), ".config", "cue");
20
- }
17
+ import { configDir } from "../lib/config-paths";
21
18
 
22
19
  interface InspectResult {
23
20
  profile: string;
@@ -0,0 +1,110 @@
1
+ import { afterAll, beforeAll, describe, expect, test } from "bun:test";
2
+ import { createServer, type Server } from "node:http";
3
+
4
+ import { parseDashArgs, dashUrl, dashFetch, ROUTES } from "./dash";
5
+
6
+ describe("parseDashArgs", () => {
7
+ const savedPort = process.env.CUE_DASH_PORT;
8
+ const savedHost = process.env.CUE_DASH_HOST;
9
+ beforeAll(() => { delete process.env.CUE_DASH_PORT; delete process.env.CUE_DASH_HOST; });
10
+ afterAll(() => {
11
+ if (savedPort === undefined) delete process.env.CUE_DASH_PORT; else process.env.CUE_DASH_PORT = savedPort;
12
+ if (savedHost === undefined) delete process.env.CUE_DASH_HOST; else process.env.CUE_DASH_HOST = savedHost;
13
+ });
14
+
15
+ test("defaults are 127.0.0.1:7891", () => {
16
+ const a = parseDashArgs(["status"]);
17
+ expect(a).toMatchObject({ sub: "status", host: "127.0.0.1", port: 7891, json: false });
18
+ });
19
+
20
+ test("first non-flag is the subcommand; the rest are positionals", () => {
21
+ const a = parseDashArgs(["profile", "browser", "extra"]);
22
+ expect(a.sub).toBe("profile");
23
+ expect(a.rest).toEqual(["browser", "extra"]);
24
+ });
25
+
26
+ test("flags parse and don't leak into positionals", () => {
27
+ const a = parseDashArgs(["kill", "123", "--port", "9000", "--host", "0.0.0.0", "--json", "--sigkill"]);
28
+ expect(a).toMatchObject({ sub: "kill", rest: ["123"], port: 9000, host: "0.0.0.0", json: true, sigkill: true });
29
+ });
30
+
31
+ test("--as captures the merge target name", () => {
32
+ expect(parseDashArgs(["merge-save", "a", "b", "--as", "ab"]).as).toBe("ab");
33
+ });
34
+
35
+ test("env overrides the default host/port", () => {
36
+ process.env.CUE_DASH_PORT = "5555";
37
+ process.env.CUE_DASH_HOST = "example.test";
38
+ const a = parseDashArgs(["status"]);
39
+ expect(a.port).toBe(5555);
40
+ expect(a.host).toBe("example.test");
41
+ delete process.env.CUE_DASH_PORT; delete process.env.CUE_DASH_HOST;
42
+ });
43
+
44
+ test("invalid port is ignored", () => {
45
+ expect(parseDashArgs(["status", "--port", "garbage"]).port).toBe(7891);
46
+ expect(parseDashArgs(["status", "--port", "99999"]).port).toBe(7891);
47
+ });
48
+ });
49
+
50
+ describe("dashUrl", () => {
51
+ test("builds the /api/v1 path with no query", () => {
52
+ expect(dashUrl("127.0.0.1", 7891, "/status")).toBe("http://127.0.0.1:7891/api/v1/status");
53
+ });
54
+ test("appends a query string", () => {
55
+ expect(dashUrl("h", 1, "/profile-detail", { profile: "a b" })).toBe("http://h:1/api/v1/profile-detail?profile=a+b");
56
+ });
57
+ });
58
+
59
+ describe("ROUTES.build", () => {
60
+ const base = parseDashArgs([]);
61
+ test("profile → GET profile-detail with the name query", () => {
62
+ const r = ROUTES.profile!.build({ ...base, rest: ["browser"] });
63
+ expect(r).toEqual({ path: "/profile-detail", query: { profile: "browser" } });
64
+ });
65
+ test("add-mcp → POST mcps/add with {profile,id}", () => {
66
+ const r = ROUTES["add-mcp"]!.build({ ...base, rest: ["frontend", "playwright"] });
67
+ expect(r).toEqual({ path: "/mcps/add", method: "POST", body: { profile: "frontend", id: "playwright" } });
68
+ });
69
+ test("kill → POST sessions/kill, SIGKILL only when --sigkill", () => {
70
+ expect(ROUTES.kill!.build({ ...base, rest: ["42"] }).body).toEqual({ pid: 42, signal: "SIGTERM" });
71
+ expect(ROUTES.kill!.build({ ...base, rest: ["42"], sigkill: true }).body).toEqual({ pid: 42, signal: "SIGKILL" });
72
+ });
73
+ test("merge-save carries names + --as target", () => {
74
+ expect(ROUTES["merge-save"]!.build({ ...base, rest: ["a", "b"], as: "ab" }).body).toEqual({ names: ["a", "b"], name: "ab" });
75
+ });
76
+ test("mutating routes are flagged", () => {
77
+ expect(ROUTES["add-mcp"]!.mutating).toBe(true);
78
+ expect(ROUTES.status!.mutating).toBeUndefined();
79
+ });
80
+ });
81
+
82
+ describe("dashFetch (stub server)", () => {
83
+ let server: Server;
84
+ let port = 0;
85
+ beforeAll(async () => {
86
+ server = createServer((req, res) => {
87
+ const url = req.url ?? "";
88
+ res.setHeader("content-type", "application/json");
89
+ if (url.startsWith("/api/v1/ok")) { res.end(JSON.stringify({ ok: true, data: { hello: "world" } })); return; }
90
+ if (url.startsWith("/api/v1/bad")) { res.statusCode = 400; res.end(JSON.stringify({ ok: false, error: "missing-profile" })); return; }
91
+ res.end("not json");
92
+ });
93
+ await new Promise<void>((r) => server.listen(0, "127.0.0.1", () => r()));
94
+ port = (server.address() as { port: number }).port;
95
+ });
96
+ afterAll(() => { server.close(); });
97
+
98
+ test("unwraps the {ok,data} envelope", async () => {
99
+ expect(await dashFetch({ host: "127.0.0.1", port }, "/ok")).toEqual({ hello: "world" });
100
+ });
101
+ test("throws the server's error message on {ok:false}", async () => {
102
+ await expect(dashFetch({ host: "127.0.0.1", port }, "/bad")).rejects.toThrow("missing-profile");
103
+ });
104
+ test("throws a clear message on non-JSON", async () => {
105
+ await expect(dashFetch({ host: "127.0.0.1", port }, "/other")).rejects.toThrow(/non-JSON/);
106
+ });
107
+ test("refused connection points at `cue dashboard`", async () => {
108
+ await expect(dashFetch({ host: "127.0.0.1", port: 1 }, "/ok")).rejects.toThrow(/cue dashboard/);
109
+ });
110
+ });