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.
- package/CHANGELOG.md +40 -0
- package/README.md +82 -33
- package/bin/cue-review-progress +107 -0
- package/bin/cue-review-watch +98 -0
- package/dist/cue.js +7352 -3744
- package/package.json +16 -5
- package/profiles/_types.ts +9 -0
- package/profiles/backend/profile.yaml +2 -0
- package/profiles/blog-writer/profile.yaml +10 -0
- package/profiles/browser/profile.yaml +9 -2
- package/profiles/builder/profile.yaml +3 -6
- package/profiles/career/profile.yaml +13 -2
- package/profiles/claude-api/profile.yaml +1 -1
- package/profiles/commerce/profile.yaml +27 -3
- package/profiles/core/logo.png +0 -0
- package/profiles/core/profile.yaml +62 -2
- package/profiles/dash-merge-test/profile.yaml +109 -0
- package/profiles/designer/profile.yaml +2 -0
- package/profiles/designer-medusa-next/profile.yaml +4 -1
- package/profiles/designer-medusa-vite/profile.yaml +4 -1
- package/profiles/docs-writer/profile.yaml +3 -1
- package/profiles/eu-tender-research/README.md +48 -0
- package/profiles/eu-tender-research/logo.png +0 -0
- package/profiles/eu-tender-research/profile.yaml +108 -0
- package/profiles/finance/logo.png +0 -0
- package/profiles/finance/profile.yaml +46 -0
- package/profiles/frontend/profile.yaml +5 -9
- package/profiles/growth/profile.yaml +2 -3
- package/profiles/gstack/profile.yaml +15 -0
- package/profiles/higgsfield/profile.yaml +3 -0
- package/profiles/hyperframes/logo.png +0 -0
- package/profiles/hyperframes/profile.yaml +59 -0
- package/profiles/improver/profile.yaml +88 -0
- package/profiles/marketing/profile.yaml +0 -3
- package/profiles/medusa-dev/profile.yaml +2 -0
- package/profiles/medusa-next/profile.yaml +2 -3
- package/profiles/medusa-vite/profile.yaml +2 -3
- package/profiles/n8n/logo.png +0 -0
- package/profiles/n8n/profile.yaml +50 -0
- package/profiles/nextjs/profile.yaml +2 -3
- package/profiles/ops/profile.yaml +2 -0
- package/profiles/postizz/profile.yaml +13 -3
- package/profiles/python/profile.yaml +3 -0
- package/profiles/research/profile.yaml +3 -1
- package/profiles/schema.json +10 -0
- package/profiles/secops/profile.yaml +2 -0
- package/profiles/seo/profile.yaml +56 -0
- package/profiles/skill-writer/profile.yaml +8 -0
- package/profiles/ssh/profile.yaml +32 -0
- package/profiles/strapi/logo.png +0 -0
- package/profiles/strapi/profile.yaml +45 -0
- package/profiles/stripe/logo.png +0 -0
- package/profiles/stripe/profile.yaml +1 -0
- package/profiles/supabase/logo.png +0 -0
- package/profiles/supabase/profile.yaml +85 -0
- package/profiles/vercel/logo.png +0 -0
- package/profiles/vercel/profile.yaml +25 -1
- package/profiles/vite/profile.yaml +4 -3
- package/profiles/web-frontend-base/profile.yaml +5 -4
- package/profiles/webshop/profile.yaml +23 -5
- package/profiles/x-growth-bot/profile.yaml +44 -0
- package/resources/icons/generate-icons.py +128 -2
- package/resources/mcps/configs/claude.sanitized.json +42 -0
- package/resources/mcps/configs/codex.sanitized.json +7 -0
- package/resources/skills/skills/career/resume-version-manager/SKILL.md +351 -0
- package/resources/skills/skills/career/salary-negotiation-prep/SKILL.md +378 -0
- package/resources/skills/skills/content/pdf/SKILL.md +2 -0
- package/resources/skills/skills/content/postiz-cards/SKILL.md +48 -0
- package/resources/skills/skills/content/postiz-cards/scripts/analytics.sh +38 -0
- package/resources/skills/skills/content/postiz-cards/scripts/card.sh +42 -0
- package/resources/skills/skills/content/postiz-cards/scripts/lint.py +38 -0
- package/resources/skills/skills/design/headless-gif-demo/SKILL.md +1 -1
- package/resources/skills/skills/design/readme-svg-design/SKILL.md +1 -1
- package/resources/skills/skills/eu-funding/grant-outreach/SKILL.md +70 -0
- package/resources/skills/skills/eu-funding/hu-grant-finder/SKILL.md +114 -0
- package/resources/skills/skills/eu-funding/hu-grant-finder/evals.md +26 -0
- package/resources/skills/skills/eu-funding/ted-tender-search/SKILL.md +80 -0
- package/resources/skills/skills/eu-funding/ted-tender-search/evals.md +26 -0
- package/resources/skills/skills/eu-funding/ted-tender-search/scripts/ted-search.sh +46 -0
- package/resources/skills/skills/event-design/wedding-invitations/SKILL.md +1 -1
- package/resources/skills/skills/github/gx-agents/SKILL.md +96 -0
- package/resources/skills/skills/gstack/design-shotgun/SKILL.md +1 -1
- package/resources/skills/skills/marketing/ab-test-analyzer/SKILL.md +1 -1
- package/resources/skills/skills/marketing/ab-test-setup-and-analysis/SKILL.md +1 -1
- package/resources/skills/skills/marketing/account-structure-review/SKILL.md +1 -1
- package/resources/skills/skills/marketing/ad-copy-variant-generator/SKILL.md +1 -1
- package/resources/skills/skills/marketing/ad-extension-audit/SKILL.md +1 -1
- package/resources/skills/skills/marketing/ad-spend-allocator/SKILL.md +1 -1
- package/resources/skills/skills/marketing/anomaly-detection/SKILL.md +1 -1
- package/resources/skills/skills/marketing/attribution-model-comparison/SKILL.md +1 -1
- package/resources/skills/skills/marketing/audience-overlap-analysis/SKILL.md +7 -1
- package/resources/skills/skills/marketing/bid-strategy-recommendations/SKILL.md +7 -1
- package/resources/skills/skills/marketing/budget-scenario-planner/SKILL.md +6 -1
- package/resources/skills/skills/marketing/campaign-naming-convention-builder/SKILL.md +7 -1
- package/resources/skills/skills/marketing/channel-mix-optimizer/SKILL.md +7 -1
- package/resources/skills/skills/marketing/client-report-narratives/SKILL.md +6 -1
- package/resources/skills/skills/marketing/competitor-creative-analysis/SKILL.md +1 -1
- package/resources/skills/skills/marketing/competitor-teardown/SKILL.md +1 -1
- package/resources/skills/skills/marketing/content-repurposer/SKILL.md +1 -1
- package/resources/skills/skills/marketing/conversion-path-analysis/SKILL.md +1 -1
- package/resources/skills/skills/marketing/cpa-diagnostics/SKILL.md +1 -1
- package/resources/skills/skills/marketing/creative-fatigue-detection/SKILL.md +1 -1
- package/resources/skills/skills/marketing/day-hour-performance-breakdown/SKILL.md +1 -1
- package/resources/skills/skills/marketing/device-performance-split/SKILL.md +1 -1
- package/resources/skills/skills/marketing/e2e-seo-assistant/SKILL.md +1 -1
- package/resources/skills/skills/marketing/email-sequence-writer/SKILL.md +1 -1
- package/resources/skills/skills/marketing/frequency-cap-recommendations/SKILL.md +1 -1
- package/resources/skills/skills/marketing/geo-performance-analysis/SKILL.md +1 -1
- package/resources/skills/skills/marketing/google-ads-audit/SKILL.md +1 -1
- package/resources/skills/skills/marketing/icp-research-assistant/SKILL.md +1 -1
- package/resources/skills/skills/marketing/keyword-cannibalization-check/SKILL.md +1 -1
- package/resources/skills/skills/marketing/landing-page-audit/SKILL.md +1 -1
- package/resources/skills/skills/marketing/landing-page-audit-quick/SKILL.md +1 -1
- package/resources/skills/skills/marketing/linkedin-ads-audit/SKILL.md +1 -1
- package/resources/skills/skills/marketing/meta-ads-audit/SKILL.md +1 -1
- package/resources/skills/skills/marketing/pacing-monitor/SKILL.md +1 -1
- package/resources/skills/skills/marketing/performance-benchmarking/SKILL.md +1 -1
- package/resources/skills/skills/marketing/programmatic-seo-builder/SKILL.md +1 -1
- package/resources/skills/skills/marketing/quality-score-breakdown/SKILL.md +1 -1
- package/resources/skills/skills/marketing/reddit-ads-audit/SKILL.md +1 -1
- package/resources/skills/skills/marketing/retargeting-window-analysis/SKILL.md +1 -1
- package/resources/skills/skills/marketing/roas-forecasting/SKILL.md +1 -1
- package/resources/skills/skills/marketing/search-term-mining/SKILL.md +1 -1
- package/resources/skills/skills/marketing/utm-tracking-generator/SKILL.md +1 -1
- package/resources/skills/skills/marketing/wasted-spend-finder/SKILL.md +1 -1
- package/resources/skills/skills/marketing/weekly-account-summary/SKILL.md +1 -1
- package/resources/skills/skills/meta/awesome-list-submit/SKILL.md +4 -4
- package/resources/skills/skills/meta/cue-dashboard/SKILL.md +109 -0
- package/resources/skills/skills/meta/cue-developer/SKILL.md +161 -0
- package/resources/skills/skills/meta/cue-developer/evals/evals.json +57 -0
- package/resources/skills/skills/meta/cue-developer/references/architecture.md +65 -0
- package/resources/skills/skills/meta/cue-developer/references/build_and_test.md +72 -0
- package/resources/skills/skills/meta/cue-developer/references/contributing.md +75 -0
- package/resources/skills/skills/meta/cue-developer/references/conventions.md +57 -0
- package/resources/skills/skills/meta/cue-developer/references/first_time_setup.md +51 -0
- package/resources/skills/skills/meta/cue-developer/references/skill_and_mcp_authoring.md +84 -0
- package/resources/skills/skills/meta/cue-developer/references/troubleshooting.md +42 -0
- package/resources/skills/skills/meta/delegation-check/SKILL.md +148 -0
- package/resources/skills/skills/meta/delegation-check/specs/scan-algorithm.md +125 -0
- package/resources/skills/skills/meta/delegation-check/specs/separation-rules.md +190 -0
- package/resources/skills/skills/meta/focus/SKILL.md +62 -0
- package/resources/skills/skills/meta/help/SKILL.md +1 -1
- package/resources/skills/skills/meta/integrity-tags/SKILL.md +2 -0
- package/resources/skills/skills/meta/next-steps/SKILL.md +124 -0
- package/resources/skills/skills/meta/next-steps/evals/eval-set.json +92 -0
- package/resources/skills/skills/meta/profile-from-docs/SKILL.md +141 -0
- package/resources/skills/skills/meta/ralph-loop/SKILL.md +83 -0
- package/resources/skills/skills/meta/ralph-loop/scripts/loop.sh +73 -0
- package/resources/skills/skills/meta/skill-simplify/SKILL.md +136 -0
- package/resources/skills/skills/meta/skill-simplify/phases/01-analysis.md +173 -0
- package/resources/skills/skills/meta/skill-simplify/phases/02-optimize.md +104 -0
- package/resources/skills/skills/meta/skill-simplify/phases/03-check.md +145 -0
- package/resources/skills/skills/meta/smart-loader/scripts/smart-lookup.sh +13 -4
- package/resources/skills/skills/meta/verify-council/SKILL.md +182 -0
- package/resources/skills/skills/meta/verify-council/references/lane-prompts.md +103 -0
- package/resources/skills/skills/meta/verify-council/references/workflow.js +217 -0
- package/resources/skills/skills/nvidia/aiq-research/SKILL.md +1 -1
- package/resources/skills/skills/nvidia/cuopt-developer/SKILL.md +16 -1
- package/resources/skills/skills/nvidia/cuopt-developer/resources/contributing.md +2 -2
- package/resources/skills/skills/nvidia/cuopt-developer/resources/numerical_debugging.md +128 -0
- package/resources/skills/skills/nvidia/cuopt-developer/resources/python_bindings.md +2 -9
- package/resources/skills/skills/nvidia/cuopt-developer/resources/vrp_skills.md +166 -0
- package/resources/skills/skills/nvidia/cuopt-install/SKILL.md +2 -10
- package/resources/skills/skills/nvidia/cuopt-numerical-optimization-api-c/SKILL.md +3 -23
- package/resources/skills/skills/nvidia/cuopt-numerical-optimization-api-c/resources/examples.md +40 -20
- package/resources/skills/skills/nvidia/cuopt-numerical-optimization-api-python/SKILL.md +5 -1
- package/resources/skills/skills/nvidia/skill-evolution/SKILL.md +4 -5
- package/resources/skills/skills/research/trendradar/SKILL.md +1 -1
- package/resources/skills/skills/ssh/ssh-config/SKILL.md +94 -0
- package/resources/skills/skills/ssh/ssh-copy/SKILL.md +92 -0
- package/resources/skills/skills/ssh/ssh-harden/SKILL.md +108 -0
- package/resources/skills/skills/ssh/ssh-keys/SKILL.md +82 -0
- package/resources/skills/skills/ssh/ssh-paste-image/LICENSE +28 -0
- package/resources/skills/skills/ssh/ssh-paste-image/SKILL.md +149 -0
- package/resources/skills/skills/ssh/ssh-paste-image/scripts/build.sh +29 -0
- package/resources/skills/skills/ssh/ssh-paste-image/scripts/client/go.mod +3 -0
- package/resources/skills/skills/ssh/ssh-paste-image/scripts/client/main.go +79 -0
- package/resources/skills/skills/ssh/ssh-paste-image/scripts/daemon/ccimgd.service +12 -0
- package/resources/skills/skills/ssh/ssh-paste-image/scripts/daemon/com.ccimgd.plist +20 -0
- package/resources/skills/skills/ssh/ssh-paste-image/scripts/daemon/go.mod +3 -0
- package/resources/skills/skills/ssh/ssh-paste-image/scripts/daemon/main.go +98 -0
- package/resources/skills/skills/ssh/ssh-tunnel/SKILL.md +96 -0
- package/resources/skills/skills/strapi/building-with-strapi/SKILL.md +112 -0
- package/resources/skills/skills/strapi/strapi-cli/SKILL.md +93 -0
- package/resources/skills/skills/strapi/strapi-content-api/SKILL.md +115 -0
- package/resources/skills/skills/strapi/strapi-deploy/SKILL.md +89 -0
- package/resources/skills/skills/strapi/strapi-mcp-setup/SKILL.md +101 -0
- package/resources/skills/skills/strapi/strapi-plugins/SKILL.md +97 -0
- package/resources/skills/skills/tools/context7/SKILL.md +101 -0
- package/resources/skills/skills/tools/opensrc/SKILL.md +1 -1
- package/resources/skills/skills/tools/portless/SKILL.md +186 -0
- package/resources/skills/skills/xbot/operate/SKILL.md +229 -0
- package/src/commands/_index.ts +8 -0
- package/src/commands/ai-score.e2e.test.ts +11 -4
- package/src/commands/ai.ts +3 -4
- package/src/commands/auto-detect.ts +1 -1
- package/src/commands/cli.test.ts +1 -2
- package/src/commands/cli.ts +1 -1
- package/src/commands/cloud.ts +1 -1
- package/src/commands/current.ts +1 -4
- package/src/commands/dash.test.ts +110 -0
- package/src/commands/dash.ts +194 -0
- package/src/commands/dashboard.ts +26 -0
- package/src/commands/diff.ts +1 -1
- package/src/commands/discover.test.ts +1 -1
- package/src/commands/discover.ts +90 -40
- package/src/commands/doctor.test.ts +58 -0
- package/src/commands/doctor.ts +79 -3
- package/src/commands/eval-behavior.ts +1 -1
- package/src/commands/eval.ts +2 -2
- package/src/commands/evolve.ts +4 -3
- package/src/commands/failures.test.ts +1 -1
- package/src/commands/features-batch1.test.ts +6 -1
- package/src/commands/icon.ts +1 -5
- package/src/commands/import-profile.ts +1 -1
- package/src/commands/init.ts +50 -7
- package/src/commands/install-sh.e2e.test.ts +65 -0
- package/src/commands/launch-handoff.e2e.test.ts +88 -0
- package/src/commands/launch.e2e.test.ts +8 -1
- package/src/commands/launch.test.ts +29 -0
- package/src/commands/launch.ts +185 -131
- package/src/commands/lock.ts +0 -1
- package/src/commands/marketplace.ts +0 -4
- package/src/commands/materialize.ts +1 -1
- package/src/commands/mem.ts +341 -0
- package/src/commands/optimizer.ts +0 -3
- package/src/commands/playground.ts +1 -2
- package/src/commands/profile-draft-skill.ts +1 -1
- package/src/commands/replay-whatif.ts +1 -6
- package/src/commands/score.ts +2 -2
- package/src/commands/security.test.ts +88 -0
- package/src/commands/security.ts +74 -28
- package/src/commands/shell.test.ts +65 -4
- package/src/commands/shell.ts +67 -7
- package/src/commands/skills-test.ts +0 -1
- package/src/commands/skills.ts +28 -2
- package/src/commands/sources.ts +1 -2
- package/src/commands/status.ts +2 -6
- package/src/commands/submit-profile.ts +1 -1
- package/src/commands/suggest.ts +35 -10
- package/src/commands/trigger-gaps.test.ts +50 -0
- package/src/commands/trigger-gaps.ts +63 -29
- package/src/commands/update.ts +1 -1
- package/src/commands/validate.ts +16 -4
- package/src/commands/watch-live.ts +1 -1
- package/src/commands/workspace.ts +1 -1
- package/src/index.ts +26 -10
- package/src/lib/active-sessions.ts +1 -1
- package/src/lib/agent-adapters.test.ts +100 -0
- package/src/lib/agent-adapters.ts +2 -2
- package/src/lib/analytics.test.ts +88 -0
- package/src/lib/analytics.ts +82 -1
- package/src/lib/auto-detect.test.ts +10 -4
- package/src/lib/auto-detect.ts +19 -23
- package/src/lib/brand-icons.ts +0 -1
- package/src/lib/cache.ts +2 -3
- package/src/lib/claude-mem-env.test.ts +148 -0
- package/src/lib/claude-mem-env.ts +172 -0
- package/src/lib/combo-history.test.ts +53 -0
- package/src/lib/combo-history.ts +83 -0
- package/src/lib/companion-detect.test.ts +108 -0
- package/src/lib/companion-detect.ts +140 -0
- package/src/lib/companion-fetch.ts +4 -6
- package/src/lib/conditional-skills.test.ts +1 -1
- package/src/lib/config-paths.test.ts +53 -0
- package/src/lib/config-paths.ts +33 -0
- package/src/lib/dashboard-server.test.ts +351 -0
- package/src/lib/dashboard-server.ts +1476 -27
- package/src/lib/debug-log.test.ts +66 -0
- package/src/lib/debug-log.ts +45 -0
- package/src/lib/mcp-catalog.test.ts +102 -0
- package/src/lib/mcp-catalog.ts +193 -0
- package/src/lib/pair-suggestions.test.ts +111 -0
- package/src/lib/pair-suggestions.ts +98 -5
- package/src/lib/permissions.test.ts +76 -0
- package/src/lib/permissions.ts +125 -0
- package/src/lib/picker.test.ts +1106 -1
- package/src/lib/picker.ts +1230 -142
- package/src/lib/plugin-discovery.ts +126 -0
- package/src/lib/pr-poster.ts +1 -1
- package/src/lib/pr-throttle.ts +2 -6
- package/src/lib/profile-linter.test.ts +67 -1
- package/src/lib/profile-linter.ts +59 -14
- package/src/lib/profile-loader.test.ts +21 -0
- package/src/lib/profile-loader.ts +22 -3
- package/src/lib/profile-metrics.ts +2 -6
- package/src/lib/profile-names.test.ts +58 -0
- package/src/lib/repos.test.ts +57 -0
- package/src/lib/repos.ts +167 -0
- package/src/lib/resolver-npx.ts +10 -1
- package/src/lib/runtime-materializer.test.ts +200 -3
- package/src/lib/runtime-materializer.ts +129 -20
- package/src/lib/shared-profiles.ts +2 -3
- package/src/lib/skill-clis.test.ts +113 -0
- package/src/lib/skill-clis.ts +232 -0
- package/src/lib/skill-dependencies.ts +9 -1
- package/src/lib/skill-deps.ts +1 -1
- package/src/lib/skill-linter.ts +1 -1
- package/src/lib/skill-quality.ts +0 -1
- package/src/lib/skill-sandbox.test.ts +1 -1
- package/src/lib/skills-lock.test.ts +1 -1
- package/src/lib/telemetry-consent.ts +3 -5
- package/src/lib/telemetry-report.test.ts +2 -2
- package/src/lib/token-budget.ts +111 -0
- package/src/lib/trigger-gaps.test.ts +70 -0
- package/src/lib/trigger-gaps.ts +48 -6
- package/src/lib/tui/data.ts +1 -5
- package/src/lib/workflow-store.ts +150 -0
- package/src/lib/workspace-secrets.ts +0 -4
- package/src/lib/workspaces.ts +1 -1
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Content-aware combine-companion detection.
|
|
3
|
+
*
|
|
4
|
+
* The profile picker's "Combine X with…" multiselect is fed by static
|
|
5
|
+
* `recommends:` lists and historical pairings — both blind to what's actually
|
|
6
|
+
* in the directory. This module scans the cwd for CONTENT signals (image/video
|
|
7
|
+
* assets, markdown drafts, a registered brand folder) and emits companion
|
|
8
|
+
* profiles to surface (and auto-check) in that multiselect.
|
|
9
|
+
*
|
|
10
|
+
* Sibling to `auto-detect.ts`: same best-effort, 0–1 confidence, max-merge
|
|
11
|
+
* discipline — but keyed on directory *content* rather than framework configs.
|
|
12
|
+
* Pure + injectable (`listEntries`) so the rules are unit-testable without a
|
|
13
|
+
* real filesystem.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { readdirSync } from "node:fs";
|
|
17
|
+
import { basename, extname } from "node:path";
|
|
18
|
+
|
|
19
|
+
export interface CompanionSignal {
|
|
20
|
+
/** Companion profile to surface in the combine multiselect. */
|
|
21
|
+
profile: string;
|
|
22
|
+
/** 0–1. Companions at/above the picker's auto-check threshold start checked. */
|
|
23
|
+
confidence: number;
|
|
24
|
+
/** Human-readable trigger, shown as the row hint (e.g. "12 image assets"). */
|
|
25
|
+
reason: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface CompanionDetectInput {
|
|
29
|
+
cwd: string;
|
|
30
|
+
/** Only profiles installed in this cue install are eligible. */
|
|
31
|
+
knownProfiles: ReadonlySet<string>;
|
|
32
|
+
/** Registered postizz brand folder names (basenames under brands/). */
|
|
33
|
+
brands?: ReadonlySet<string>;
|
|
34
|
+
/**
|
|
35
|
+
* Injectable shallow directory listing (entry names only, non-recursive).
|
|
36
|
+
* Defaults to a best-effort `readdirSync(cwd)`; a missing/unreadable dir
|
|
37
|
+
* yields an empty list, so detection silently produces no signals.
|
|
38
|
+
*/
|
|
39
|
+
listEntries?: (cwd: string) => string[];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const IMAGE_EXTS = new Set([".png", ".jpg", ".jpeg", ".webp", ".gif"]);
|
|
43
|
+
const VIDEO_EXTS = new Set([".mp4", ".mov", ".webm"]);
|
|
44
|
+
const CONTENT_DIRS = new Set(["content", "posts", "drafts"]);
|
|
45
|
+
// Markdown that isn't a draft — excluded from the draft count (case-folded stem).
|
|
46
|
+
const NON_DRAFT_MD = new Set([
|
|
47
|
+
"readme",
|
|
48
|
+
"agents",
|
|
49
|
+
"claude",
|
|
50
|
+
"changelog",
|
|
51
|
+
"contributing",
|
|
52
|
+
"license",
|
|
53
|
+
]);
|
|
54
|
+
|
|
55
|
+
// Tunables. Require ≥3 images before reading a directory as "image work" (one
|
|
56
|
+
// stray screenshot shouldn't trigger it); confidence climbs with the count,
|
|
57
|
+
// capped. Image/video/brand land at/above the picker's 0.7 auto-check line;
|
|
58
|
+
// markdown sits just below, so it surfaces but stays unchecked until toggled.
|
|
59
|
+
const MIN_IMAGES = 3;
|
|
60
|
+
const IMAGE_CONF_BASE = 0.7;
|
|
61
|
+
const IMAGE_CONF_STEP = 0.05;
|
|
62
|
+
const IMAGE_CONF_CAP = 0.9;
|
|
63
|
+
const VIDEO_CONF = 0.7;
|
|
64
|
+
const MIN_MD_DRAFTS = 2;
|
|
65
|
+
const MD_CONF = 0.6;
|
|
66
|
+
const BRAND_CONF = 0.8;
|
|
67
|
+
|
|
68
|
+
function defaultListEntries(cwd: string): string[] {
|
|
69
|
+
try {
|
|
70
|
+
return readdirSync(cwd);
|
|
71
|
+
} catch {
|
|
72
|
+
return [];
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Scan `cwd` for content signals and return companion profiles to surface in
|
|
78
|
+
* the combine multiselect — de-duped per profile (max confidence, merged
|
|
79
|
+
* reasons), filtered to installed profiles, sorted by confidence DESC.
|
|
80
|
+
*/
|
|
81
|
+
export function detectCompanions(input: CompanionDetectInput): CompanionSignal[] {
|
|
82
|
+
const list = (input.listEntries ?? defaultListEntries)(input.cwd);
|
|
83
|
+
|
|
84
|
+
// Aggregate per profile: keep the strongest signal, accumulate its reasons.
|
|
85
|
+
const acc = new Map<string, { confidence: number; reasons: string[] }>();
|
|
86
|
+
const add = (profile: string, confidence: number, reason: string): void => {
|
|
87
|
+
const entry = acc.get(profile) ?? { confidence: 0, reasons: [] };
|
|
88
|
+
entry.confidence = Math.max(entry.confidence, confidence);
|
|
89
|
+
if (!entry.reasons.includes(reason)) entry.reasons.push(reason);
|
|
90
|
+
acc.set(profile, entry);
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
let images = 0;
|
|
94
|
+
let videos = 0;
|
|
95
|
+
let mdDrafts = 0;
|
|
96
|
+
let hasContentDir = false;
|
|
97
|
+
for (const name of list) {
|
|
98
|
+
const lower = name.toLowerCase();
|
|
99
|
+
const ext = extname(lower);
|
|
100
|
+
if (IMAGE_EXTS.has(ext)) {
|
|
101
|
+
images++;
|
|
102
|
+
} else if (VIDEO_EXTS.has(ext)) {
|
|
103
|
+
videos++;
|
|
104
|
+
} else if (ext === ".md") {
|
|
105
|
+
if (!NON_DRAFT_MD.has(lower.slice(0, -3))) mdDrafts++;
|
|
106
|
+
}
|
|
107
|
+
// A `content/` (etc.) entry could be a dir or a file; either way it reads
|
|
108
|
+
// as a drafts workspace for our purposes — a shallow listing can't cheaply
|
|
109
|
+
// distinguish, and the false-positive cost (one extra unchecked row) is nil.
|
|
110
|
+
if (CONTENT_DIRS.has(lower)) hasContentDir = true;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// ── images → higgsfield (confidence scales with count) ──
|
|
114
|
+
if (images >= MIN_IMAGES) {
|
|
115
|
+
const conf = Math.min(IMAGE_CONF_CAP, IMAGE_CONF_BASE + IMAGE_CONF_STEP * (images - MIN_IMAGES));
|
|
116
|
+
add("higgsfield", conf, `${images} image assets`);
|
|
117
|
+
}
|
|
118
|
+
// ── video → higgsfield ──
|
|
119
|
+
if (videos >= 1) {
|
|
120
|
+
add("higgsfield", VIDEO_CONF, `${videos} video file${videos === 1 ? "" : "s"}`);
|
|
121
|
+
}
|
|
122
|
+
// ── markdown drafts / content dir → blog-writer ──
|
|
123
|
+
if (mdDrafts >= MIN_MD_DRAFTS || hasContentDir) {
|
|
124
|
+
add("blog-writer", MD_CONF, "markdown drafts");
|
|
125
|
+
}
|
|
126
|
+
// ── registered brand folder → postizz ──
|
|
127
|
+
const brand = basename(input.cwd);
|
|
128
|
+
if (input.brands?.has(brand)) {
|
|
129
|
+
add("postizz", BRAND_CONF, `registered brand: ${brand}`);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return [...acc.entries()]
|
|
133
|
+
.filter(([profile]) => input.knownProfiles.has(profile))
|
|
134
|
+
.map(([profile, { confidence, reasons }]) => ({
|
|
135
|
+
profile,
|
|
136
|
+
confidence,
|
|
137
|
+
reason: reasons.join(", "),
|
|
138
|
+
}))
|
|
139
|
+
.sort((a, b) => b.confidence - a.confidence || a.profile.localeCompare(b.profile));
|
|
140
|
+
}
|
|
@@ -16,9 +16,10 @@
|
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
18
|
import { spawnSync, spawn as nodeSpawn } from "node:child_process";
|
|
19
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync, readdirSync, cpSync, rmSync, mkdtempSync,
|
|
19
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, readdirSync, cpSync, rmSync, mkdtempSync, } from "node:fs";
|
|
20
20
|
import { join } from "node:path";
|
|
21
|
-
import { tmpdir
|
|
21
|
+
import { tmpdir } from "node:os";
|
|
22
|
+
import { cacheDir } from "./config-paths";
|
|
22
23
|
import { createHash } from "node:crypto";
|
|
23
24
|
|
|
24
25
|
// ---------------------------------------------------------------------------
|
|
@@ -68,10 +69,7 @@ interface ETagEntry {
|
|
|
68
69
|
ts: number;
|
|
69
70
|
}
|
|
70
71
|
|
|
71
|
-
const ETAG_CACHE_PATH = join(
|
|
72
|
-
process.env.XDG_CACHE_HOME ?? join(homedir(), ".cache"),
|
|
73
|
-
"cue", "companion-etags.json",
|
|
74
|
-
);
|
|
72
|
+
const ETAG_CACHE_PATH = join(cacheDir(), "companion-etags.json");
|
|
75
73
|
|
|
76
74
|
function loadETagCache(): Record<string, ETagEntry> {
|
|
77
75
|
try {
|
|
@@ -3,7 +3,7 @@ import { mkdtempSync, rmSync, writeFileSync, mkdirSync } from "node:fs";
|
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
|
|
6
|
-
import { evaluateCondition, filterConditionalSkills, type
|
|
6
|
+
import { evaluateCondition, filterConditionalSkills, type ConditionalSkill } from "./conditional-skills";
|
|
7
7
|
|
|
8
8
|
let tmp: string;
|
|
9
9
|
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
|
|
5
|
+
import { cacheDir, configDir } from "./config-paths";
|
|
6
|
+
|
|
7
|
+
// configDir/cacheDir read process.env; snapshot + restore so tests don't leak.
|
|
8
|
+
const SAVED = {
|
|
9
|
+
config: process.env.XDG_CONFIG_HOME,
|
|
10
|
+
cache: process.env.XDG_CACHE_HOME,
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
describe("config-paths", () => {
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
delete process.env.XDG_CONFIG_HOME;
|
|
16
|
+
delete process.env.XDG_CACHE_HOME;
|
|
17
|
+
});
|
|
18
|
+
afterEach(() => {
|
|
19
|
+
if (SAVED.config === undefined) delete process.env.XDG_CONFIG_HOME;
|
|
20
|
+
else process.env.XDG_CONFIG_HOME = SAVED.config;
|
|
21
|
+
if (SAVED.cache === undefined) delete process.env.XDG_CACHE_HOME;
|
|
22
|
+
else process.env.XDG_CACHE_HOME = SAVED.cache;
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test("configDir falls back to ~/.config/cue when XDG unset", () => {
|
|
26
|
+
expect(configDir()).toBe(join(homedir(), ".config", "cue"));
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test("configDir honors a set XDG_CONFIG_HOME", () => {
|
|
30
|
+
process.env.XDG_CONFIG_HOME = "/custom/xdg";
|
|
31
|
+
expect(configDir()).toBe(join("/custom/xdg", "cue"));
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
// The bug this module fixes: a set-but-empty XDG must NOT resolve to a
|
|
35
|
+
// relative "cue" path (the old `XDG ? join(XDG,"cue") : ...` variant did).
|
|
36
|
+
test("configDir treats an empty XDG_CONFIG_HOME as unset (absolute, not relative)", () => {
|
|
37
|
+
process.env.XDG_CONFIG_HOME = "";
|
|
38
|
+
const dir = configDir();
|
|
39
|
+
expect(dir).toBe(join(homedir(), ".config", "cue"));
|
|
40
|
+
expect(dir.startsWith("/")).toBe(true);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test("cacheDir falls back to ~/.cache/cue and honors XDG_CACHE_HOME", () => {
|
|
44
|
+
expect(cacheDir()).toBe(join(homedir(), ".cache", "cue"));
|
|
45
|
+
process.env.XDG_CACHE_HOME = "/c";
|
|
46
|
+
expect(cacheDir()).toBe(join("/c", "cue"));
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("cacheDir treats an empty XDG_CACHE_HOME as unset", () => {
|
|
50
|
+
process.env.XDG_CACHE_HOME = "";
|
|
51
|
+
expect(cacheDir()).toBe(join(homedir(), ".cache", "cue"));
|
|
52
|
+
});
|
|
53
|
+
});
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Single source of truth for cue's on-disk locations.
|
|
3
|
+
*
|
|
4
|
+
* `configDir()` and `cacheDir()` were previously re-implemented ~14 times
|
|
5
|
+
* across lib/ and commands/, in two subtly different variants:
|
|
6
|
+
* - correct: `xdg && xdg.length > 0 ? xdg : ~/.config` (empty XDG = unset)
|
|
7
|
+
* - buggy: `xdg ? join(xdg, "cue") : ~/.config/cue` (empty XDG = set →
|
|
8
|
+
* resolves to a *relative* "cue" path)
|
|
9
|
+
* Centralizing here removes the drift and the empty-string footgun. The repo
|
|
10
|
+
* root (`CUE_REPO_ROOT`/`SOUL_REPO_ROOT` + ~10 local resolutions) is a related
|
|
11
|
+
* but distinct concern, intentionally left for a follow-up.
|
|
12
|
+
*/
|
|
13
|
+
import { homedir } from "node:os";
|
|
14
|
+
import { join } from "node:path";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* cue's base config dir: `$XDG_CONFIG_HOME/cue`, else `~/.config/cue`.
|
|
18
|
+
* A missing OR empty `XDG_CONFIG_HOME` falls back to `~/.config` (an
|
|
19
|
+
* empty-but-set value must NOT resolve to a relative "cue" path).
|
|
20
|
+
*/
|
|
21
|
+
export function configDir(): string {
|
|
22
|
+
const xdg = process.env.XDG_CONFIG_HOME;
|
|
23
|
+
return join(xdg && xdg.length > 0 ? xdg : join(homedir(), ".config"), "cue");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* cue's cache dir: `$XDG_CACHE_HOME/cue`, else `~/.cache/cue`. Same
|
|
28
|
+
* empty-string discipline as {@link configDir}.
|
|
29
|
+
*/
|
|
30
|
+
export function cacheDir(): string {
|
|
31
|
+
const xdg = process.env.XDG_CACHE_HOME;
|
|
32
|
+
return join(xdg && xdg.length > 0 ? xdg : join(homedir(), ".cache"), "cue");
|
|
33
|
+
}
|
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the dashboard server's profile-detail handler — the data source
|
|
3
|
+
* that backs cue studio's explorer / search / mcps views. Runs against the
|
|
4
|
+
* real on-disk `profiles/` + `resources/skills/` tree (no mocks) so the test
|
|
5
|
+
* also guards the loader → resolver → parser wiring the studio depends on.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { describe, expect, test } from "bun:test";
|
|
9
|
+
|
|
10
|
+
import { handleProfileDetail, handleMcpCatalog, handleMcpAdd, handleMarket, createHandler, semverGt, computeVersionInfo, buildTimeline, handleHooks, handleHookSource } from "./dashboard-server";
|
|
11
|
+
|
|
12
|
+
function detail(profile: string) {
|
|
13
|
+
return handleProfileDetail(new URLSearchParams({ profile }));
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
describe("version banner logic", () => {
|
|
17
|
+
test("semverGt compares major.minor.patch", () => {
|
|
18
|
+
expect(semverGt("0.9.1", "0.9.0")).toBe(true);
|
|
19
|
+
expect(semverGt("1.0.0", "0.9.9")).toBe(true);
|
|
20
|
+
expect(semverGt("0.9.0", "0.9.0")).toBe(false);
|
|
21
|
+
expect(semverGt("0.9.0", "0.9.1")).toBe(false);
|
|
22
|
+
expect(semverGt("0.10.0", "0.9.0")).toBe(true); // numeric, not lexical
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test("computeVersionInfo flags an update + carries the registry notice", () => {
|
|
26
|
+
const info = computeVersionInfo("0.9.0", { version: "0.9.1", cue: { notice: { message: "hi", command: "npm i -g cue-ai@latest" } } });
|
|
27
|
+
expect(info.updateAvailable).toBe(true);
|
|
28
|
+
expect(info.latest).toBe("0.9.1");
|
|
29
|
+
expect(info.notice).toEqual({ message: "hi", command: "npm i -g cue-ai@latest" });
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test("computeVersionInfo is fail-soft when the registry doc is null (offline)", () => {
|
|
33
|
+
const info = computeVersionInfo("0.9.0", null);
|
|
34
|
+
expect(info).toEqual({ current: "0.9.0", latest: null, updateAvailable: false, notice: null });
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test("computeVersionInfo shows a notice even when already up to date", () => {
|
|
38
|
+
const info = computeVersionInfo("0.9.0", { version: "0.9.0", cue: { notice: { message: "heads up" } } });
|
|
39
|
+
expect(info.updateAvailable).toBe(false);
|
|
40
|
+
expect(info.notice).toEqual({ message: "heads up", command: undefined });
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test("computeVersionInfo drops an empty notice object", () => {
|
|
44
|
+
const info = computeVersionInfo("0.9.0", { version: "0.9.0", cue: { notice: {} } });
|
|
45
|
+
expect(info.notice).toBeNull();
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
describe("handleProfileDetail", () => {
|
|
50
|
+
test("returns a real, grouped skill catalogue for gstack", async () => {
|
|
51
|
+
const res = await detail("gstack");
|
|
52
|
+
expect(res.ok).toBe(true);
|
|
53
|
+
if (!res.ok) return;
|
|
54
|
+
const d = res.data as {
|
|
55
|
+
profile: string;
|
|
56
|
+
parts: string[];
|
|
57
|
+
counts: { skills: number; mcps: number; plugins: number; commands: number };
|
|
58
|
+
skills: { id: string; ns: string; name: string; body: string; missing: boolean }[];
|
|
59
|
+
mcps: { id: string }[];
|
|
60
|
+
commands: { name: string; ref: string; desc: string; argHint: string | null; body: string; sizeK: number; missing: boolean }[];
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
expect(d.profile).toBe("gstack");
|
|
64
|
+
expect(d.parts).toEqual(["gstack"]);
|
|
65
|
+
expect(d.skills.length).toBeGreaterThan(0);
|
|
66
|
+
expect(d.counts.skills).toBe(d.skills.length);
|
|
67
|
+
|
|
68
|
+
// Every skill carries a namespace (first path segment) and a non-empty body.
|
|
69
|
+
for (const s of d.skills) {
|
|
70
|
+
expect(s.ns.length).toBeGreaterThan(0);
|
|
71
|
+
expect(s.body.length).toBeGreaterThan(0);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// The known meta/analyze skill resolves from disk (not a stub).
|
|
75
|
+
const analyze = d.skills.find((s) => s.id === "meta/analyze");
|
|
76
|
+
expect(analyze).toBeDefined();
|
|
77
|
+
expect(analyze!.missing).toBe(false);
|
|
78
|
+
expect(analyze!.body).toContain("name: analyze");
|
|
79
|
+
|
|
80
|
+
// Commands are slash-prefixed, carry a bare ref, and a body for the preview.
|
|
81
|
+
expect(d.commands.length).toBeGreaterThan(0);
|
|
82
|
+
expect(d.counts.commands).toBe(d.commands.length);
|
|
83
|
+
for (const c of d.commands) {
|
|
84
|
+
expect(c.name).toBe("/" + c.ref);
|
|
85
|
+
expect(c.body.length).toBeGreaterThan(0);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// A known command (/verify) resolves its real markdown from
|
|
89
|
+
// resources/commands (not a stub), with its frontmatter description.
|
|
90
|
+
const verify = d.commands.find((c) => c.ref === "verify");
|
|
91
|
+
expect(verify).toBeDefined();
|
|
92
|
+
expect(verify!.missing).toBe(false);
|
|
93
|
+
expect(verify!.desc.length).toBeGreaterThan(0);
|
|
94
|
+
expect(verify!.body).toContain("description:");
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test("resolves composite selectors and dedupes the union", async () => {
|
|
98
|
+
const res = await detail("gstack+core");
|
|
99
|
+
expect(res.ok).toBe(true);
|
|
100
|
+
if (!res.ok) return;
|
|
101
|
+
const d = res.data as { parts: string[]; skills: { id: string }[] };
|
|
102
|
+
expect(d.parts).toEqual(["gstack", "core"]);
|
|
103
|
+
const ids = d.skills.map((s) => s.id);
|
|
104
|
+
expect(new Set(ids).size).toBe(ids.length); // no duplicate skill ids
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test("errors cleanly for an unknown profile", async () => {
|
|
108
|
+
const res = await detail("definitely-not-a-real-profile-xyz");
|
|
109
|
+
expect(res.ok).toBe(false);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test("surfaces per-profile CLI dependencies from skill frontmatter", async () => {
|
|
113
|
+
const res = await detail("gstack+core");
|
|
114
|
+
expect(res.ok).toBe(true);
|
|
115
|
+
if (!res.ok) return;
|
|
116
|
+
const d = res.data as {
|
|
117
|
+
counts: { clis: number };
|
|
118
|
+
clis: { name: string; install: string; known: boolean; usedBy: string[] }[];
|
|
119
|
+
};
|
|
120
|
+
expect(Array.isArray(d.clis)).toBe(true);
|
|
121
|
+
expect(d.counts.clis).toBe(d.clis.length);
|
|
122
|
+
// Every CLI is attributed to at least one skill and ranked by usage.
|
|
123
|
+
for (const c of d.clis) {
|
|
124
|
+
expect(c.name.length).toBeGreaterThan(0);
|
|
125
|
+
expect(c.usedBy.length).toBeGreaterThan(0);
|
|
126
|
+
}
|
|
127
|
+
for (let i = 1; i < d.clis.length; i++) {
|
|
128
|
+
expect(d.clis[i - 1]!.usedBy.length).toBeGreaterThanOrEqual(d.clis[i]!.usedBy.length);
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test("surfaces the agency profile's declared subagents, grouped by division", async () => {
|
|
133
|
+
const res = await detail("agency");
|
|
134
|
+
expect(res.ok).toBe(true);
|
|
135
|
+
if (!res.ok) return;
|
|
136
|
+
const d = res.data as {
|
|
137
|
+
counts: { subagents: number };
|
|
138
|
+
subagents: { id: string; division: string; name: string }[];
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
// The roster is large and varied — not just a couple inherited bits.
|
|
142
|
+
expect(d.counts.subagents).toBe(d.subagents.length);
|
|
143
|
+
expect(d.subagents.length).toBeGreaterThan(40);
|
|
144
|
+
|
|
145
|
+
// A known ref splits into division + slug for grouped display.
|
|
146
|
+
const ui = d.subagents.find((s) => s.id === "design/design-ui-designer");
|
|
147
|
+
expect(ui).toBeDefined();
|
|
148
|
+
expect(ui!.division).toBe("design");
|
|
149
|
+
expect(ui!.name).toBe("design-ui-designer");
|
|
150
|
+
|
|
151
|
+
// Multiple divisions are represented (design, finance, sales, …).
|
|
152
|
+
expect(new Set(d.subagents.map((s) => s.division)).size).toBeGreaterThan(5);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
test("parses the profile's declared playbooks into workflow cards", async () => {
|
|
156
|
+
const res = await detail("gstack");
|
|
157
|
+
expect(res.ok).toBe(true);
|
|
158
|
+
if (!res.ok) return;
|
|
159
|
+
const d = res.data as {
|
|
160
|
+
playbooks: { id: string; name: string; title: string; emoji: string; trigger: string; est: string; desc: string; steps: { name: string; detail: string }[] }[];
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
expect(Array.isArray(d.playbooks)).toBe(true);
|
|
164
|
+
expect(d.playbooks.length).toBeGreaterThan(0);
|
|
165
|
+
|
|
166
|
+
// ship-feature is declared (inherited from core) and parses with real steps.
|
|
167
|
+
const ship = d.playbooks.find((p) => p.id === "ship-feature");
|
|
168
|
+
expect(ship).toBeDefined();
|
|
169
|
+
expect(ship!.title).toBe("Ship a Feature");
|
|
170
|
+
expect(ship!.trigger).toBe("playbook");
|
|
171
|
+
expect(ship!.est.startsWith("~")).toBe(true);
|
|
172
|
+
expect(ship!.desc.length).toBeGreaterThan(0);
|
|
173
|
+
expect(ship!.steps.length).toBeGreaterThan(0);
|
|
174
|
+
|
|
175
|
+
// Steps strip the leading "N." numbering and never carry markdown noise.
|
|
176
|
+
for (const s of ship!.steps) {
|
|
177
|
+
expect(s.name.length).toBeGreaterThan(0);
|
|
178
|
+
expect(/^\d+\.\s/.test(s.name)).toBe(false);
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
describe("GET /api/v1/plugin-icon", () => {
|
|
184
|
+
const handler = createHandler();
|
|
185
|
+
const get = (qs: string) =>
|
|
186
|
+
handler(new Request(`http://127.0.0.1/api/v1/plugin-icon${qs}`));
|
|
187
|
+
|
|
188
|
+
test("reuses a same-named profile's logo (resend)", async () => {
|
|
189
|
+
const res = await get("?plugin=resend@claude-plugins-official");
|
|
190
|
+
expect(res.status).toBe(200);
|
|
191
|
+
expect(res.headers.get("Content-Type")).toBe("image/png");
|
|
192
|
+
expect((await res.arrayBuffer()).byteLength).toBeGreaterThan(0);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
test("rejects a path-traversal plugin name", async () => {
|
|
196
|
+
const res = await get("?plugin=" + encodeURIComponent("../secret"));
|
|
197
|
+
expect(res.status).toBe(400);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
test("400 when the plugin param is missing", async () => {
|
|
201
|
+
const res = await get("");
|
|
202
|
+
expect(res.status).toBe(400);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
test("404 for a plugin with neither a profile nor a generated logo", async () => {
|
|
206
|
+
const res = await get("?plugin=definitely-not-a-real-plugin-xyz");
|
|
207
|
+
expect(res.status).toBe(404);
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
describe("handleMcpCatalog", () => {
|
|
212
|
+
test("returns the catalog as an array of addable entries", async () => {
|
|
213
|
+
const res = await handleMcpCatalog();
|
|
214
|
+
expect(res.ok).toBe(true);
|
|
215
|
+
if (!res.ok) return;
|
|
216
|
+
const entries = res.data as { id: string; transport: string }[];
|
|
217
|
+
expect(Array.isArray(entries)).toBe(true);
|
|
218
|
+
expect(entries.length).toBeGreaterThan(10);
|
|
219
|
+
expect(entries.some((e) => e.id === "coolify")).toBe(true);
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
describe("handleMarket", () => {
|
|
224
|
+
const VALID_TYPES = new Set(["profile", "workflow", "skill", "cli", "mcp", "plugin"]);
|
|
225
|
+
|
|
226
|
+
test("returns a normalized, counted marketplace feed", async () => {
|
|
227
|
+
const res = await handleMarket();
|
|
228
|
+
expect(res.ok).toBe(true);
|
|
229
|
+
if (!res.ok) return;
|
|
230
|
+
const d = res.data as {
|
|
231
|
+
items: Array<Record<string, unknown>>;
|
|
232
|
+
counts: Record<string, number>;
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
expect(Array.isArray(d.items)).toBe(true);
|
|
236
|
+
expect(d.items.length).toBeGreaterThan(0);
|
|
237
|
+
|
|
238
|
+
// Every item carries the full contract: correct field types + a valid type.
|
|
239
|
+
for (const it of d.items) {
|
|
240
|
+
expect(typeof it.id).toBe("string");
|
|
241
|
+
expect((it.id as string).length).toBeGreaterThan(0);
|
|
242
|
+
expect(VALID_TYPES.has(it.type as string)).toBe(true);
|
|
243
|
+
expect(typeof it.name).toBe("string");
|
|
244
|
+
expect(typeof it.author).toBe("string");
|
|
245
|
+
expect(typeof it.handle).toBe("string");
|
|
246
|
+
expect(typeof it.stars).toBe("number");
|
|
247
|
+
expect(typeof it.installs).toBe("string");
|
|
248
|
+
expect(typeof it.when).toBe("string");
|
|
249
|
+
expect(typeof it.featured).toBe("boolean");
|
|
250
|
+
expect(typeof it.desc).toBe("string");
|
|
251
|
+
expect(Array.isArray(it.tags)).toBe(true);
|
|
252
|
+
expect(["registry", "local"]).toContain(it.source as string);
|
|
253
|
+
expect(typeof it.add).toBe("string");
|
|
254
|
+
expect(VALID_TYPES.has(it.addKind as string)).toBe(true);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// counts.all === items.length, and per-type counts sum to it.
|
|
258
|
+
expect(d.counts.all).toBe(d.items.length);
|
|
259
|
+
const perType = d.counts.profile + d.counts.workflow + d.counts.skill + d.counts.cli + d.counts.mcp + d.counts.plugin;
|
|
260
|
+
expect(perType).toBe(d.counts.all);
|
|
261
|
+
|
|
262
|
+
// The local library is always present — profiles + workflows come from disk.
|
|
263
|
+
expect(d.counts.profile).toBeGreaterThan(0);
|
|
264
|
+
expect(d.counts.workflow).toBeGreaterThan(0);
|
|
265
|
+
});
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
describe("handleMcpAdd validation", () => {
|
|
269
|
+
test("rejects a missing id", async () => {
|
|
270
|
+
const res = await handleMcpAdd({ profile: "core" });
|
|
271
|
+
expect(res.ok).toBe(false);
|
|
272
|
+
if (!res.ok) expect(res.error).toBe("missing-id");
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
test("rejects a missing profile", async () => {
|
|
276
|
+
const res = await handleMcpAdd({ id: "coolify" });
|
|
277
|
+
expect(res.ok).toBe(false);
|
|
278
|
+
if (!res.ok) expect(res.error).toBe("missing-profile");
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
test("surfaces unknown-mcp from the lib as an error envelope", async () => {
|
|
282
|
+
const res = await handleMcpAdd({ id: "not-a-real-mcp-xyz", profile: "core" });
|
|
283
|
+
expect(res.ok).toBe(false);
|
|
284
|
+
if (!res.ok) expect(res.error).toMatch(/unknown-mcp/);
|
|
285
|
+
});
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
describe("buildTimeline", () => {
|
|
289
|
+
test("returns gap-filled daily buckets spanning the window", () => {
|
|
290
|
+
const t = buildTimeline(7);
|
|
291
|
+
expect(t.windowDays).toBe(7);
|
|
292
|
+
expect(t.daily).toHaveLength(7);
|
|
293
|
+
expect(t.daily.every((d) => typeof d.sessions === "number" && /^\d{4}-\d{2}-\d{2}$/.test(d.date))).toBe(true);
|
|
294
|
+
expect(Array.isArray(t.profiles)).toBe(true);
|
|
295
|
+
});
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
describe("GET /api/v1/hook-source", () => {
|
|
299
|
+
test("rejects a missing path", async () => {
|
|
300
|
+
const r = await handleHookSource(new URLSearchParams());
|
|
301
|
+
expect(r.ok).toBe(false);
|
|
302
|
+
if (!r.ok) expect(r.error).toBe("missing-path");
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
test("rejects an arbitrary path — only enumerated hook scripts are readable", async () => {
|
|
306
|
+
const r = await handleHookSource(new URLSearchParams({ path: "/etc/passwd" }));
|
|
307
|
+
expect(r.ok).toBe(false);
|
|
308
|
+
if (!r.ok) expect(r.error).toBe("not-a-hook-script");
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
test("rejects a path traversal that escapes the hooks dir", async () => {
|
|
312
|
+
const r = await handleHookSource(new URLSearchParams({ path: "/x/hooks/../../../../etc/passwd" }));
|
|
313
|
+
expect(r.ok).toBe(false);
|
|
314
|
+
if (!r.ok) expect(r.error).toBe("not-a-hook-script");
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
test("serves a real enumerated hook script's source", async () => {
|
|
318
|
+
const hk = await handleHooks(new URLSearchParams());
|
|
319
|
+
if (!hk.ok) return; // telemetry/profile unresolved in this env — security cases still cover it
|
|
320
|
+
const events = (hk.data as { events: { hooks: { scriptPath: string | null }[] }[] }).events;
|
|
321
|
+
const withScript = events.flatMap((e) => e.hooks).find((h) => h.scriptPath);
|
|
322
|
+
if (!withScript?.scriptPath) return; // no resolvable hook scripts here
|
|
323
|
+
const r = await handleHookSource(new URLSearchParams({ path: withScript.scriptPath }));
|
|
324
|
+
expect(r.ok).toBe(true);
|
|
325
|
+
if (r.ok) {
|
|
326
|
+
const d = r.data as { filename: string; content: string; language: string };
|
|
327
|
+
expect(d.filename.length).toBeGreaterThan(0);
|
|
328
|
+
expect(typeof d.content).toBe("string");
|
|
329
|
+
expect(d.language.length).toBeGreaterThan(0);
|
|
330
|
+
}
|
|
331
|
+
});
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
describe("GET /api/v1/telemetry/stream (SSE)", () => {
|
|
335
|
+
test("responds as an event-stream and pushes an initial frame", async () => {
|
|
336
|
+
const handler = createHandler();
|
|
337
|
+
const res = await handler(new Request("http://localhost/api/v1/telemetry/stream?since=7"));
|
|
338
|
+
expect(res.status).toBe(200);
|
|
339
|
+
expect(res.headers.get("content-type")).toContain("text/event-stream");
|
|
340
|
+
expect(res.body).not.toBeNull();
|
|
341
|
+
// Read just the first frame, then cancel so the stream's interval timers
|
|
342
|
+
// stop and the test process can exit.
|
|
343
|
+
const reader = res.body!.getReader();
|
|
344
|
+
const { value, done } = await reader.read();
|
|
345
|
+
expect(done).toBe(false);
|
|
346
|
+
const text = new TextDecoder().decode(value);
|
|
347
|
+
// A real timeline frame when telemetry is on, or the disabled error frame.
|
|
348
|
+
expect(text).toMatch(/event: (timeline|error)/);
|
|
349
|
+
await reader.cancel();
|
|
350
|
+
});
|
|
351
|
+
});
|