cue-ai 0.9.2 → 0.9.4
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 +4 -3
- package/README.md +154 -394
- package/bin/cue-learnings +30 -4
- package/bin/cue-review-progress +0 -0
- package/bin/cue-review-watch +0 -0
- package/dist/cue.js +4328 -3108
- package/package.json +1 -1
- package/plugins/cue/commands/cue-switch.md +1 -1
- package/plugins/cue/commands/cue.md +1 -1
- package/profiles/backend/profile.yaml +4 -0
- package/profiles/browser/profile.yaml +4 -0
- package/profiles/career/profile.yaml +2 -13
- package/profiles/commerce/profile.yaml +0 -2
- package/profiles/coolify/profile.yaml +0 -1
- package/profiles/core/profile.yaml +78 -11
- package/profiles/dash-merge-test/profile.yaml +6 -1
- package/profiles/designer/profile.yaml +9 -1
- package/profiles/dropshipping/profile.yaml +69 -0
- package/profiles/frontend/profile.yaml +4 -0
- package/profiles/google-ads/profile.yaml +34 -0
- package/profiles/google-analytics/profile.yaml +34 -0
- package/profiles/google-drive/profile.yaml +34 -0
- package/profiles/gstack/profile.yaml +117 -29
- package/profiles/marketing/profile.yaml +0 -1
- package/profiles/media/README.md +70 -0
- package/profiles/media/profile.yaml +104 -0
- package/profiles/nano-banana/profile.yaml +52 -0
- package/profiles/ops/profile.yaml +1 -2
- package/profiles/secops/profile.yaml +3 -0
- package/profiles/skill-writer/profile.yaml +15 -0
- package/profiles/video/profile.yaml +3 -0
- package/profiles/web-frontend-base/profile.yaml +6 -0
- package/profiles/webshop/profile.yaml +0 -1
- package/profiles/webshop-google/profile.yaml +1 -0
- package/profiles/x-growth-bot/profile.yaml +2 -0
- package/resources/icons/generate-icons.py +2 -128
- package/resources/mcps/configs/claude.sanitized.json +88 -20
- package/resources/mcps/configs/claude_runtime.sanitized.json +40 -1
- package/resources/mcps/configs/codex.sanitized.json +29 -0
- package/resources/skills/skills/career/job-hunter/LICENSE +21 -0
- package/resources/skills/skills/career/job-hunter/README.md +323 -0
- package/resources/skills/skills/career/job-hunter/SKILL.md +91 -0
- package/resources/skills/skills/career/job-hunter/agents/README.md +96 -0
- package/resources/skills/skills/career/job-hunter/agents/apply-assessment-prep.md +195 -0
- package/resources/skills/skills/career/job-hunter/agents/apply-ats-scan.md +155 -0
- package/resources/skills/skills/career/job-hunter/agents/apply-bias-audit.md +224 -0
- package/resources/skills/skills/career/job-hunter/agents/apply-cover-letter.md +69 -0
- package/resources/skills/skills/career/job-hunter/agents/apply-decode-jd.md +117 -0
- package/resources/skills/skills/career/job-hunter/agents/apply-fit-score.md +183 -0
- package/resources/skills/skills/career/job-hunter/agents/apply-linkedin-audit.md +74 -0
- package/resources/skills/skills/career/job-hunter/agents/apply-linkedin-scrape.md +255 -0
- package/resources/skills/skills/career/job-hunter/agents/apply-portfolio-brief.md +123 -0
- package/resources/skills/skills/career/job-hunter/agents/apply-reality-check.md +164 -0
- package/resources/skills/skills/career/job-hunter/agents/apply-reference-prep.md +150 -0
- package/resources/skills/skills/career/job-hunter/agents/apply-rejection-analysis.md +172 -0
- package/resources/skills/skills/career/job-hunter/agents/apply-resume.md +70 -0
- package/resources/skills/skills/career/job-hunter/agents/apply-skills-gap-filler.md +109 -0
- package/resources/skills/skills/career/job-hunter/agents/career-internal.md +94 -0
- package/resources/skills/skills/career/job-hunter/agents/career-linkedin-content.md +173 -0
- package/resources/skills/skills/career/job-hunter/agents/career-linkedin-scanner.md +262 -0
- package/resources/skills/skills/career/job-hunter/agents/career-network-message.md +108 -0
- package/resources/skills/skills/career/job-hunter/agents/career-promote.md +102 -0
- package/resources/skills/skills/career/job-hunter/agents/career-review.md +71 -0
- package/resources/skills/skills/career/job-hunter/agents/interview-debrief.md +117 -0
- package/resources/skills/skills/career/job-hunter/agents/interview-mock.md +171 -0
- package/resources/skills/skills/career/job-hunter/agents/interview-panel-decoder.md +152 -0
- package/resources/skills/skills/career/job-hunter/agents/interview-prep.md +184 -0
- package/resources/skills/skills/career/job-hunter/agents/interview-question-bank.md +133 -0
- package/resources/skills/skills/career/job-hunter/agents/interview-research.md +148 -0
- package/resources/skills/skills/career/job-hunter/agents/offer-compare.md +117 -0
- package/resources/skills/skills/career/job-hunter/agents/offer-counteroffer.md +144 -0
- package/resources/skills/skills/career/job-hunter/agents/offer-deadline-manager.md +148 -0
- package/resources/skills/skills/career/job-hunter/agents/offer-negotiate.md +126 -0
- package/resources/skills/skills/career/job-hunter/agents/offer-schedule.md +99 -0
- package/resources/skills/skills/career/job-hunter/agents/offer-thankyou.md +80 -0
- package/resources/skills/skills/career/job-hunter/agents/search-company-research.md +146 -0
- package/resources/skills/skills/career/job-hunter/agents/search-follow-up.md +129 -0
- package/resources/skills/skills/career/job-hunter/agents/search-ghost-job-detector.md +152 -0
- package/resources/skills/skills/career/job-hunter/agents/search-inbox-scan.md +193 -0
- package/resources/skills/skills/career/job-hunter/agents/search-interview-scorecard.md +164 -0
- package/resources/skills/skills/career/job-hunter/agents/search-jobs.md +149 -0
- package/resources/skills/skills/career/job-hunter/agents/search-momentum-check.md +194 -0
- package/resources/skills/skills/career/job-hunter/agents/search-outreach.md +85 -0
- package/resources/skills/skills/career/job-hunter/agents/search-referral-finder.md +124 -0
- package/resources/skills/skills/career/job-hunter/agents/search-salary.md +96 -0
- package/resources/skills/skills/career/job-hunter/agents/search-send-email.md +109 -0
- package/resources/skills/skills/career/job-hunter/agents/search-tracker-update.md +127 -0
- package/resources/skills/skills/career/job-hunter/inputs/README.md +26 -0
- package/resources/skills/skills/career/job-hunter/inputs/apply-linkedin-url.txt +8 -0
- package/resources/skills/skills/career/job-hunter/inputs/interview-context.md +24 -0
- package/resources/skills/skills/career/job-hunter/inputs/job-description.md +20 -0
- package/resources/skills/skills/career/job-hunter/inputs/job-search-criteria.md +36 -0
- package/resources/skills/skills/career/job-hunter/inputs/my-linkedin.md +24 -0
- package/resources/skills/skills/career/job-hunter/inputs/my-resume.md +28 -0
- package/resources/skills/skills/career/job-hunter/inputs/search-outreach-target.md +24 -0
- package/resources/skills/skills/career/job-hunter/rules/README.md +37 -0
- package/resources/skills/skills/career/job-hunter/rules/writing-rules.md +81 -0
- package/resources/skills/skills/design/banana/SKILL.md +375 -0
- package/resources/skills/skills/design/banana/references/cost-tracking.md +47 -0
- package/resources/skills/skills/design/banana/references/gemini-models.md +236 -0
- package/resources/skills/skills/design/banana/references/mcp-tools.md +145 -0
- package/resources/skills/skills/design/banana/references/post-processing.md +192 -0
- package/resources/skills/skills/design/banana/references/presets.md +69 -0
- package/resources/skills/skills/design/banana/references/prompt-engineering.md +481 -0
- package/resources/skills/skills/design/banana/scripts/batch.py +97 -0
- package/resources/skills/skills/design/banana/scripts/cost_tracker.py +191 -0
- package/resources/skills/skills/design/banana/scripts/edit.py +159 -0
- package/resources/skills/skills/design/banana/scripts/generate.py +168 -0
- package/resources/skills/skills/design/banana/scripts/presets.py +154 -0
- package/resources/skills/skills/design/banana/scripts/setup_mcp.py +151 -0
- package/resources/skills/skills/design/banana/scripts/validate_setup.py +133 -0
- package/resources/skills/skills/gstack/ship/SKILL.md +13 -0
- package/resources/skills/skills/media/3d-logo-animation/SKILL.md +59 -0
- package/resources/skills/skills/media/action-figure-generator/SKILL.md +48 -0
- package/resources/skills/skills/media/ad-creative/SKILL.md +79 -0
- package/resources/skills/skills/media/ai-clipping/SKILL.md +194 -0
- package/resources/skills/skills/media/ai-clipping/scripts/run-ai-clipping.sh +200 -0
- package/resources/skills/skills/media/ai-fight-scene/SKILL.md +132 -0
- package/resources/skills/skills/media/amazon-product-listing/SKILL.md +68 -0
- package/resources/skills/skills/media/animal-video-generator/SKILL.md +59 -0
- package/resources/skills/skills/media/award-ceremony-video/SKILL.md +87 -0
- package/resources/skills/skills/media/blog-header/SKILL.md +61 -0
- package/resources/skills/skills/media/brand-kit/SKILL.md +72 -0
- package/resources/skills/skills/media/brochures/SKILL.md +65 -0
- package/resources/skills/skills/media/cartoon-dance-animation/SKILL.md +62 -0
- package/resources/skills/skills/media/character-story-video/SKILL.md +84 -0
- package/resources/skills/skills/media/chibi-collage-effect/SKILL.md +63 -0
- package/resources/skills/skills/media/cinema-director/SKILL.md +93 -0
- package/resources/skills/skills/media/cinema-director/scripts/generate-film.sh +78 -0
- package/resources/skills/skills/media/color-analysis-board/SKILL.md +71 -0
- package/resources/skills/skills/media/core-edit/SKILL.md +48 -0
- package/resources/skills/skills/media/core-edit/edit-image.sh +54 -0
- package/resources/skills/skills/media/core-edit/enhance-image.sh +191 -0
- package/resources/skills/skills/media/core-edit/lipsync.sh +144 -0
- package/resources/skills/skills/media/core-edit/video-effects.sh +193 -0
- package/resources/skills/skills/media/core-media/SKILL.md +49 -0
- package/resources/skills/skills/media/core-media/create-music.sh +169 -0
- package/resources/skills/skills/media/core-media/generate-image.sh +161 -0
- package/resources/skills/skills/media/core-media/generate-video.sh +137 -0
- package/resources/skills/skills/media/core-media/image-to-video.sh +228 -0
- package/resources/skills/skills/media/core-media/schema_data.json +18708 -0
- package/resources/skills/skills/media/core-media/upload.sh +41 -0
- package/resources/skills/skills/media/core-platform/SKILL.md +41 -0
- package/resources/skills/skills/media/core-platform/check-result.sh +37 -0
- package/resources/skills/skills/media/core-platform/setup.sh +31 -0
- package/resources/skills/skills/media/couple-grid-creator/SKILL.md +47 -0
- package/resources/skills/skills/media/design-guide/SKILL.md +73 -0
- package/resources/skills/skills/media/drone-style-video/SKILL.md +61 -0
- package/resources/skills/skills/media/fashion-try-on/SKILL.md +61 -0
- package/resources/skills/skills/media/floor-plan-rendering/SKILL.md +56 -0
- package/resources/skills/skills/media/freeze-effect-video/SKILL.md +100 -0
- package/resources/skills/skills/media/giant-product-showcase/SKILL.md +61 -0
- package/resources/skills/skills/media/instagram-post/SKILL.md +58 -0
- package/resources/skills/skills/media/interior-design/SKILL.md +61 -0
- package/resources/skills/skills/media/interior-design-visualizer/SKILL.md +57 -0
- package/resources/skills/skills/media/jewelry-product-video/SKILL.md +61 -0
- package/resources/skills/skills/media/kdenlive/SKILL.md +106 -0
- package/resources/skills/skills/media/kdenlive/scripts/assemble.sh +57 -0
- package/resources/skills/skills/media/kdenlive/scripts/common.sh +30 -0
- package/resources/skills/skills/media/kdenlive/scripts/inspect.sh +19 -0
- package/resources/skills/skills/media/kdenlive/scripts/reframe.sh +22 -0
- package/resources/skills/skills/media/kdenlive/scripts/render.sh +16 -0
- package/resources/skills/skills/media/kdenlive/scripts/title-card.sh +25 -0
- package/resources/skills/skills/media/keyboard-art-maker/SKILL.md +44 -0
- package/resources/skills/skills/media/logo-branding/SKILL.md +70 -0
- package/resources/skills/skills/media/logo-creator/SKILL.md +80 -0
- package/resources/skills/skills/media/logo-creator/scripts/create-logo.sh +38 -0
- package/resources/skills/skills/media/logo-generator/SKILL.md +56 -0
- package/resources/skills/skills/media/multi-angle-reshoot/SKILL.md +70 -0
- package/resources/skills/skills/media/multi-angle-shots/SKILL.md +73 -0
- package/resources/skills/skills/media/music-video/SKILL.md +61 -0
- package/resources/skills/skills/media/nano-banana/SKILL.md +80 -0
- package/resources/skills/skills/media/nano-banana/scripts/generate-nano-art.sh +54 -0
- package/resources/skills/skills/media/one-shot-video/SKILL.md +56 -0
- package/resources/skills/skills/media/photo-pack-generator/SKILL.md +205 -0
- package/resources/skills/skills/media/photo-pack-generator/scripts/generate-pack.sh +241 -0
- package/resources/skills/skills/media/product-ad-cinematic/SKILL.md +78 -0
- package/resources/skills/skills/media/product-campaign/SKILL.md +76 -0
- package/resources/skills/skills/media/product-showcase-video/SKILL.md +60 -0
- package/resources/skills/skills/media/product-video-ad-maker/SKILL.md +59 -0
- package/resources/skills/skills/media/rednote-cover/SKILL.md +57 -0
- package/resources/skills/skills/media/seedance-2/SKILL.md +632 -0
- package/resources/skills/skills/media/seedance-2/scripts/generate-seedance.sh +701 -0
- package/resources/skills/skills/media/selfie-with-celebrities/SKILL.md +64 -0
- package/resources/skills/skills/media/social-media-video/SKILL.md +277 -0
- package/resources/skills/skills/media/social-media-video/scripts/run-social-video.sh +316 -0
- package/resources/skills/skills/media/social-pack/SKILL.md +58 -0
- package/resources/skills/skills/media/storyboard/SKILL.md +57 -0
- package/resources/skills/skills/media/storyboard-to-cooking-video/SKILL.md +143 -0
- package/resources/skills/skills/media/talking-baby-video/SKILL.md +57 -0
- package/resources/skills/skills/media/ugc-ads-workflow/SKILL.md +70 -0
- package/resources/skills/skills/media/ugc-lifestyle-try-on/SKILL.md +65 -0
- package/resources/skills/skills/media/ugc-video-factory/SKILL.md +134 -0
- package/resources/skills/skills/media/ui-design/SKILL.md +81 -0
- package/resources/skills/skills/media/ui-design/scripts/generate-mockup.sh +49 -0
- package/resources/skills/skills/media/url-to-design/SKILL.md +61 -0
- package/resources/skills/skills/media/workflow/SKILL.md +197 -0
- package/resources/skills/skills/media/workflow/scripts/discover-workflow.sh +18 -0
- package/resources/skills/skills/media/workflow/scripts/generate-workflow.sh +33 -0
- package/resources/skills/skills/media/workflow/scripts/interactive-run.sh +16 -0
- package/resources/skills/skills/media/workflow/scripts/list-workflows.sh +20 -0
- package/resources/skills/skills/media/workflow/scripts/run-workflow.sh +34 -0
- package/resources/skills/skills/media/youtube-shorts/SKILL.md +173 -0
- package/resources/skills/skills/media/youtube-shorts/scripts/run-youtube-shorts.sh +141 -0
- package/resources/skills/skills/media/youtube-thumbnail/SKILL.md +66 -0
- package/resources/skills/skills/meta/cue-developer/references/architecture.md +2 -2
- package/resources/skills/skills/meta/cue-usage/SKILL.md +1 -1
- package/resources/skills/skills/meta/profile-fit-monitor/SKILL.md +2 -2
- package/resources/skills/skills/meta/profile-optimizer/SKILL.md +1 -1
- package/resources/skills/skills/meta/profile-suggest/SKILL.md +7 -7
- package/resources/skills/skills/meta/profile-summon/SKILL.md +159 -0
- package/resources/skills/skills/meta/profile-summon/evals/evals.json +53 -0
- package/resources/skills/skills/meta/save-profile/SKILL.md +1 -1
- package/resources/skills/skills/meta/skill-reviewer/SKILL.md +3 -0
- package/resources/skills/skills/meta/skill-reviewer/references/tdd-for-skills.md +55 -0
- package/resources/skills/skills/research/find-skills/SKILL.md +1 -1
- package/resources/skills/skills/review/code-review-deep/SKILL.md +20 -0
- package/resources/skills/skills/security/trivy-scan/SKILL.md +139 -0
- package/resources/skills/skills/security/trivy-scan/scripts/ensure-trivy.sh +21 -0
- package/resources/skills/skills/tools/ccusage/SKILL.md +142 -0
- package/src/commands/_index.ts +8 -0
- package/src/commands/ai.ts +2 -2
- package/src/commands/auto-detect.test.ts +74 -0
- package/src/commands/auto-detect.ts +9 -7
- package/src/commands/cli.test.ts +20 -4
- package/src/commands/cli.ts +36 -20
- package/src/commands/create-profile.ts +2 -2
- package/src/commands/debug.ts +2 -2
- package/src/commands/discover.ts +14 -4
- package/src/commands/export-docker.ts +1 -1
- package/src/commands/features-batch1.test.ts +1 -1
- package/src/commands/gates.ts +1 -1
- package/src/commands/import-profile.ts +1 -1
- package/src/commands/init.ts +15 -11
- package/src/commands/install.test.ts +192 -0
- package/src/commands/install.ts +610 -0
- package/src/commands/launch-handoff.e2e.test.ts +33 -1
- package/src/commands/launch.e2e.test.ts +15 -10
- package/src/commands/launch.ts +73 -116
- package/src/commands/materialize.ts +2 -2
- package/src/commands/prune.ts +1 -1
- package/src/commands/security-audit.ts +1 -1
- package/src/commands/shell.ts +7 -7
- package/src/commands/skill-report.ts +1 -1
- package/src/commands/skills.ts +3 -3
- package/src/commands/snapshot.ts +2 -2
- package/src/commands/summon.test.ts +116 -0
- package/src/commands/summon.ts +338 -0
- package/src/commands/trigger-gaps.ts +1 -1
- package/src/commands/use.ts +47 -3
- package/src/commands/watch-live.ts +5 -5
- package/src/commands/watch.ts +8 -8
- package/src/index.ts +2 -0
- package/src/lib/active-sessions.test.ts +3 -3
- package/src/lib/active-sessions.ts +4 -4
- package/src/lib/auto-detect.test.ts +172 -8
- package/src/lib/auto-detect.ts +191 -136
- package/src/lib/codex-persona-parity.test.ts +58 -0
- package/src/lib/companion-detect.test.ts +43 -1
- package/src/lib/companion-detect.ts +35 -0
- package/src/lib/credentials-sync.test.ts +121 -1
- package/src/lib/credentials-sync.ts +95 -1
- package/src/lib/cwd-resolver.test.ts +8 -8
- package/src/lib/cwd-resolver.ts +2 -2
- package/src/lib/dashboard-merge.test.ts +9 -4
- package/src/lib/dashboard-server.ts +1 -1
- package/src/lib/picker.test.ts +1 -1
- package/src/lib/picker.ts +5 -5
- package/src/lib/profile-merge.test.ts +8 -0
- package/src/lib/profile-names.test.ts +3 -3
- package/src/lib/runtime-install.ts +166 -0
- package/src/lib/runtime-materializer.test.ts +137 -0
- package/src/lib/runtime-materializer.ts +105 -2
- package/src/lib/skill-router.test.ts +38 -0
- package/src/lib/skill-router.ts +65 -4
- package/profiles/eu-tender-research/README.md +0 -48
- package/profiles/eu-tender-research/logo.png +0 -0
- package/profiles/eu-tender-research/profile.yaml +0 -108
|
@@ -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 {
|
|
6
|
+
import { detectProfileV2 } from "./auto-detect";
|
|
7
7
|
|
|
8
8
|
let tmp: string;
|
|
9
9
|
|
|
@@ -116,6 +116,73 @@ describe("detectProfileV2", () => {
|
|
|
116
116
|
for (const r of results) expect(r.confidence).toBeLessThanOrEqual(0.97);
|
|
117
117
|
});
|
|
118
118
|
|
|
119
|
+
test("package.json with stripe dep → stripe profile suggested", () => {
|
|
120
|
+
writeFileSync(join(tmp, "package.json"), JSON.stringify({
|
|
121
|
+
dependencies: { stripe: "14.0.0" },
|
|
122
|
+
}));
|
|
123
|
+
const results = detectProfileV2(tmp);
|
|
124
|
+
const stripe = results.find(r => r.profile === "stripe");
|
|
125
|
+
expect(stripe).toBeDefined();
|
|
126
|
+
// Above the picker's SUGGESTED_MIN_CONFIDENCE (0.5) so it actually shows,
|
|
127
|
+
// below SUGGESTED_AUTO_PICK_CONFIDENCE (0.7) so it never hijacks Enter.
|
|
128
|
+
expect(stripe!.confidence).toBeGreaterThanOrEqual(0.5);
|
|
129
|
+
expect(stripe!.confidence).toBeLessThan(0.7);
|
|
130
|
+
expect(stripe!.reasons.join(" ")).toContain("stripe");
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test("package.json with @aws-sdk/client-s3 → aws profile suggested", () => {
|
|
134
|
+
writeFileSync(join(tmp, "package.json"), JSON.stringify({
|
|
135
|
+
dependencies: { "@aws-sdk/client-s3": "3.0.0" },
|
|
136
|
+
}));
|
|
137
|
+
const results = detectProfileV2(tmp);
|
|
138
|
+
const aws = results.find(r => r.profile === "aws");
|
|
139
|
+
expect(aws).toBeDefined();
|
|
140
|
+
expect(aws!.confidence).toBeGreaterThanOrEqual(0.5);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test("service deps ride alongside the framework profile, not above it", () => {
|
|
144
|
+
writeFileSync(join(tmp, "package.json"), JSON.stringify({
|
|
145
|
+
dependencies: { next: "14.0.0", stripe: "14.0.0" },
|
|
146
|
+
}));
|
|
147
|
+
const results = detectProfileV2(tmp);
|
|
148
|
+
const nextjs = results.find(r => r.profile === "nextjs");
|
|
149
|
+
const stripe = results.find(r => r.profile === "stripe");
|
|
150
|
+
expect(nextjs).toBeDefined();
|
|
151
|
+
expect(stripe).toBeDefined();
|
|
152
|
+
expect(nextjs!.confidence).toBeGreaterThan(stripe!.confidence);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
test("scoped service deps match by prefix (@supabase/, @slack/)", () => {
|
|
156
|
+
writeFileSync(join(tmp, "package.json"), JSON.stringify({
|
|
157
|
+
dependencies: { "@supabase/supabase-js": "2.0.0" },
|
|
158
|
+
devDependencies: { "@slack/web-api": "7.0.0" },
|
|
159
|
+
}));
|
|
160
|
+
const results = detectProfileV2(tmp);
|
|
161
|
+
expect(results.find(r => r.profile === "supabase")).toBeDefined();
|
|
162
|
+
expect(results.find(r => r.profile === "slack")).toBeDefined();
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
test("no service dep → no service profile suggested", () => {
|
|
166
|
+
writeFileSync(join(tmp, "package.json"), JSON.stringify({
|
|
167
|
+
dependencies: { express: "4.0.0" },
|
|
168
|
+
}));
|
|
169
|
+
const results = detectProfileV2(tmp);
|
|
170
|
+
expect(results.find(r => r.profile === "stripe")).toBeUndefined();
|
|
171
|
+
expect(results.find(r => r.profile === "aws")).toBeUndefined();
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
test("react-native dep → react-native profile outranks generic frontend", () => {
|
|
175
|
+
writeFileSync(join(tmp, "package.json"), JSON.stringify({
|
|
176
|
+
dependencies: { react: "18.0.0", "react-native": "0.74.0" },
|
|
177
|
+
}));
|
|
178
|
+
const results = detectProfileV2(tmp);
|
|
179
|
+
const rn = results.find(r => r.profile === "react-native");
|
|
180
|
+
const frontend = results.find(r => r.profile === "frontend");
|
|
181
|
+
expect(rn).toBeDefined();
|
|
182
|
+
expect(frontend).toBeDefined();
|
|
183
|
+
expect(rn!.confidence).toBeGreaterThan(frontend!.confidence);
|
|
184
|
+
});
|
|
185
|
+
|
|
119
186
|
test("results sorted by confidence descending, max 5", () => {
|
|
120
187
|
writeFileSync(join(tmp, "Cargo.toml"), "");
|
|
121
188
|
mkdirSync(join(tmp, "src"));
|
|
@@ -132,12 +199,109 @@ describe("detectProfileV2", () => {
|
|
|
132
199
|
});
|
|
133
200
|
});
|
|
134
201
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
202
|
+
|
|
203
|
+
describe("detectProfileV2 — Python deps", () => {
|
|
204
|
+
test("requirements.txt with boto3 → aws suggested, python outranks it", () => {
|
|
205
|
+
writeFileSync(join(tmp, "requirements.txt"), "boto3==1.34.0\n");
|
|
206
|
+
const results = detectProfileV2(tmp);
|
|
207
|
+
const aws = results.find(r => r.profile === "aws");
|
|
208
|
+
const python = results.find(r => r.profile === "python");
|
|
209
|
+
expect(aws).toBeDefined();
|
|
210
|
+
expect(aws!.confidence).toBeGreaterThanOrEqual(0.5);
|
|
211
|
+
expect(aws!.confidence).toBeLessThan(0.7);
|
|
212
|
+
expect(python).toBeDefined();
|
|
213
|
+
expect(python!.confidence).toBeGreaterThan(aws!.confidence);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
test("version specifiers, extras, and comments are stripped", () => {
|
|
217
|
+
writeFileSync(join(tmp, "requirements.txt"), [
|
|
218
|
+
"# payments",
|
|
219
|
+
"stripe==7.0.0",
|
|
220
|
+
"psycopg2-binary>=2.9 ; python_version >= '3.8'",
|
|
221
|
+
"uvicorn[standard]~=0.29",
|
|
222
|
+
"",
|
|
223
|
+
].join("\n"));
|
|
224
|
+
const results = detectProfileV2(tmp);
|
|
225
|
+
expect(results.find(r => r.profile === "stripe")).toBeDefined();
|
|
226
|
+
expect(results.find(r => r.profile === "postgres")).toBeDefined();
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
test("pyproject.toml [project] dependencies → supabase suggested", () => {
|
|
230
|
+
writeFileSync(join(tmp, "pyproject.toml"), [
|
|
231
|
+
"[project]",
|
|
232
|
+
'name = "myapp"',
|
|
233
|
+
"dependencies = [",
|
|
234
|
+
' "supabase>=2.0",',
|
|
235
|
+
' "httpx",',
|
|
236
|
+
"]",
|
|
237
|
+
].join("\n"));
|
|
238
|
+
const results = detectProfileV2(tmp);
|
|
239
|
+
expect(results.find(r => r.profile === "supabase")).toBeDefined();
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
test("PEP 503 normalization: slack_sdk matches slack-sdk", () => {
|
|
243
|
+
writeFileSync(join(tmp, "requirements.txt"), "slack_sdk==3.27.0\n");
|
|
244
|
+
const results = detectProfileV2(tmp);
|
|
245
|
+
expect(results.find(r => r.profile === "slack")).toBeDefined();
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
test("python files without service deps suggest no service profiles", () => {
|
|
249
|
+
writeFileSync(join(tmp, "requirements.txt"), "requests==2.31.0\nflask\n");
|
|
250
|
+
const results = detectProfileV2(tmp);
|
|
251
|
+
expect(results.find(r => r.profile === "aws")).toBeUndefined();
|
|
252
|
+
expect(results.find(r => r.profile === "stripe")).toBeUndefined();
|
|
253
|
+
});
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
describe("detectProfileV2 — monorepo workspaces", () => {
|
|
257
|
+
test("workspaces glob: packages/*/package.json deps surface at the root", () => {
|
|
258
|
+
writeFileSync(join(tmp, "package.json"), JSON.stringify({
|
|
259
|
+
private: true,
|
|
260
|
+
workspaces: ["packages/*"],
|
|
261
|
+
}));
|
|
262
|
+
mkdirSync(join(tmp, "packages/api"), { recursive: true });
|
|
263
|
+
writeFileSync(join(tmp, "packages/api/package.json"), JSON.stringify({
|
|
264
|
+
dependencies: { stripe: "14.0.0" },
|
|
265
|
+
}));
|
|
266
|
+
const results = detectProfileV2(tmp);
|
|
267
|
+
const stripe = results.find(r => r.profile === "stripe");
|
|
268
|
+
expect(stripe).toBeDefined();
|
|
269
|
+
expect(stripe!.reasons.join(" ")).toContain("workspace");
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
test("pnpm-workspace.yaml globs are honored", () => {
|
|
273
|
+
writeFileSync(join(tmp, "package.json"), JSON.stringify({ private: true }));
|
|
274
|
+
writeFileSync(join(tmp, "pnpm-workspace.yaml"), 'packages:\n - "apps/*"\n');
|
|
275
|
+
mkdirSync(join(tmp, "apps/web"), { recursive: true });
|
|
276
|
+
writeFileSync(join(tmp, "apps/web/package.json"), JSON.stringify({
|
|
277
|
+
dependencies: { "@aws-sdk/client-s3": "3.0.0" },
|
|
278
|
+
}));
|
|
279
|
+
const results = detectProfileV2(tmp);
|
|
280
|
+
expect(results.find(r => r.profile === "aws")).toBeDefined();
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
test("exact workspace paths (no glob) are scanned too", () => {
|
|
284
|
+
writeFileSync(join(tmp, "package.json"), JSON.stringify({
|
|
285
|
+
private: true,
|
|
286
|
+
workspaces: { packages: ["apps/web"] },
|
|
287
|
+
}));
|
|
288
|
+
mkdirSync(join(tmp, "apps/web"), { recursive: true });
|
|
289
|
+
writeFileSync(join(tmp, "apps/web/package.json"), JSON.stringify({
|
|
290
|
+
dependencies: { "@supabase/supabase-js": "2.0.0" },
|
|
291
|
+
}));
|
|
292
|
+
const results = detectProfileV2(tmp);
|
|
293
|
+
expect(results.find(r => r.profile === "supabase")).toBeDefined();
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
test("packages/ without a workspaces declaration is NOT scanned", () => {
|
|
297
|
+
writeFileSync(join(tmp, "package.json"), JSON.stringify({
|
|
298
|
+
dependencies: { express: "4.0.0" },
|
|
299
|
+
}));
|
|
300
|
+
mkdirSync(join(tmp, "packages/api"), { recursive: true });
|
|
301
|
+
writeFileSync(join(tmp, "packages/api/package.json"), JSON.stringify({
|
|
302
|
+
dependencies: { stripe: "14.0.0" },
|
|
303
|
+
}));
|
|
304
|
+
const results = detectProfileV2(tmp);
|
|
305
|
+
expect(results.find(r => r.profile === "stripe")).toBeUndefined();
|
|
142
306
|
});
|
|
143
307
|
});
|
package/src/lib/auto-detect.ts
CHANGED
|
@@ -3,112 +3,9 @@
|
|
|
3
3
|
* Scans cwd for project signals and scores against known profiles.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
6
|
+
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
7
7
|
import { join } from "node:path";
|
|
8
8
|
|
|
9
|
-
interface Signal {
|
|
10
|
-
file: string; // glob-like path to check (relative to cwd)
|
|
11
|
-
weight: number;
|
|
12
|
-
profile: string;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export const SIGNALS: Signal[] = [
|
|
16
|
-
// Frontend / Next.js
|
|
17
|
-
{ file: "next.config.js", weight: 5, profile: "nextjs" },
|
|
18
|
-
{ file: "next.config.ts", weight: 5, profile: "nextjs" },
|
|
19
|
-
{ file: "next.config.mjs", weight: 5, profile: "nextjs" },
|
|
20
|
-
{ file: "app/layout.tsx", weight: 4, profile: "nextjs" },
|
|
21
|
-
{ file: "app/page.tsx", weight: 3, profile: "nextjs" },
|
|
22
|
-
{ file: "next.config.js", weight: 4, profile: "frontend" },
|
|
23
|
-
{ file: "next.config.ts", weight: 4, profile: "frontend" },
|
|
24
|
-
{ file: "next.config.mjs", weight: 4, profile: "frontend" },
|
|
25
|
-
{ file: "vite.config.ts", weight: 4, profile: "frontend" },
|
|
26
|
-
{ file: "vite.config.js", weight: 4, profile: "frontend" },
|
|
27
|
-
{ file: "tailwind.config.js", weight: 3, profile: "frontend" },
|
|
28
|
-
{ file: "tailwind.config.ts", weight: 3, profile: "frontend" },
|
|
29
|
-
{ file: "postcss.config.js", weight: 2, profile: "frontend" },
|
|
30
|
-
{ file: "tsconfig.json", weight: 1, profile: "frontend" },
|
|
31
|
-
|
|
32
|
-
// Backend (Node/TS)
|
|
33
|
-
{ file: "docker-compose.yml", weight: 3, profile: "backend" },
|
|
34
|
-
{ file: "docker-compose.yaml", weight: 3, profile: "backend" },
|
|
35
|
-
{ file: "Dockerfile", weight: 2, profile: "backend" },
|
|
36
|
-
{ file: "prisma/schema.prisma", weight: 4, profile: "backend" },
|
|
37
|
-
{ file: "migrations", weight: 3, profile: "backend" },
|
|
38
|
-
{ file: "drizzle.config.ts", weight: 4, profile: "backend" },
|
|
39
|
-
{ file: "src/server.ts", weight: 3, profile: "backend" },
|
|
40
|
-
{ file: "src/index.ts", weight: 1, profile: "backend" },
|
|
41
|
-
{ file: ".github/workflows/", weight: 1, profile: "backend" },
|
|
42
|
-
|
|
43
|
-
// Python API
|
|
44
|
-
{ file: "pyproject.toml", weight: 4, profile: "python" },
|
|
45
|
-
{ file: "setup.py", weight: 3, profile: "python" },
|
|
46
|
-
{ file: "requirements.txt", weight: 3, profile: "python" },
|
|
47
|
-
{ file: "app/main.py", weight: 5, profile: "python" },
|
|
48
|
-
{ file: "main.py", weight: 3, profile: "python" },
|
|
49
|
-
{ file: "manage.py", weight: 5, profile: "python" },
|
|
50
|
-
{ file: "uvicorn.ini", weight: 4, profile: "python" },
|
|
51
|
-
{ file: "alembic.ini", weight: 4, profile: "python" },
|
|
52
|
-
{ file: ".python-version", weight: 2, profile: "python" },
|
|
53
|
-
|
|
54
|
-
// Rust
|
|
55
|
-
{ file: "Cargo.toml", weight: 5, profile: "rust" },
|
|
56
|
-
{ file: "Cargo.lock", weight: 3, profile: "rust" },
|
|
57
|
-
{ file: "src/main.rs", weight: 4, profile: "rust" },
|
|
58
|
-
{ file: "src/lib.rs", weight: 3, profile: "rust" },
|
|
59
|
-
{ file: ".cargo/config.toml", weight: 2, profile: "rust" },
|
|
60
|
-
|
|
61
|
-
// Go API
|
|
62
|
-
{ file: "go.mod", weight: 5, profile: "go-api" },
|
|
63
|
-
{ file: "go.sum", weight: 3, profile: "go-api" },
|
|
64
|
-
{ file: "cmd/", weight: 3, profile: "go-api" },
|
|
65
|
-
{ file: "internal/", weight: 2, profile: "go-api" },
|
|
66
|
-
{ file: "main.go", weight: 4, profile: "go-api" },
|
|
67
|
-
|
|
68
|
-
// Medusa
|
|
69
|
-
{ file: "medusa-config.js", weight: 5, profile: "medusa-dev" },
|
|
70
|
-
{ file: "medusa-config.ts", weight: 5, profile: "medusa-dev" },
|
|
71
|
-
{ file: "packages/medusa", weight: 5, profile: "medusa-dev" },
|
|
72
|
-
|
|
73
|
-
// Docs
|
|
74
|
-
{ file: "astro.config.mjs", weight: 4, profile: "docs-writer" },
|
|
75
|
-
{ file: "docusaurus.config.js", weight: 4, profile: "docs-writer" },
|
|
76
|
-
{ file: "mkdocs.yml", weight: 4, profile: "docs-writer" },
|
|
77
|
-
{ file: "content/blog", weight: 3, profile: "docs-writer" },
|
|
78
|
-
{ file: "docs/", weight: 2, profile: "docs-writer" },
|
|
79
|
-
|
|
80
|
-
// Fleet
|
|
81
|
-
{ file: ".colony", weight: 5, profile: "fleet-control" },
|
|
82
|
-
{ file: ".omx", weight: 4, profile: "fleet-control" },
|
|
83
|
-
{ file: "scripts/codex-fleet", weight: 5, profile: "fleet-control" },
|
|
84
|
-
|
|
85
|
-
// Creative
|
|
86
|
-
{ file: "design-tokens", weight: 4, profile: "creative-media" },
|
|
87
|
-
{ file: "figma.config.ts", weight: 4, profile: "creative-media" },
|
|
88
|
-
|
|
89
|
-
// Research
|
|
90
|
-
{ file: "research/", weight: 3, profile: "research" },
|
|
91
|
-
{ file: "papers/", weight: 3, profile: "research" },
|
|
92
|
-
|
|
93
|
-
// Three.js
|
|
94
|
-
{ file: "three.js", weight: 4, profile: "threejs" },
|
|
95
|
-
|
|
96
|
-
// Generic Claude-managed repo → baseline profile
|
|
97
|
-
{ file: "CLAUDE.md", weight: 2, profile: "core" },
|
|
98
|
-
{ file: ".claude/", weight: 2, profile: "core" },
|
|
99
|
-
|
|
100
|
-
// Full (meta)
|
|
101
|
-
{ file: "profiles/", weight: 2, profile: "full" },
|
|
102
|
-
];
|
|
103
|
-
|
|
104
|
-
export interface DetectionResult {
|
|
105
|
-
profile: string;
|
|
106
|
-
score: number;
|
|
107
|
-
maxScore: number;
|
|
108
|
-
confidence: number; // 0-100
|
|
109
|
-
signals: string[]; // which files matched
|
|
110
|
-
}
|
|
111
|
-
|
|
112
9
|
/**
|
|
113
10
|
* V2 detection result with 0-1 confidence and reasons array.
|
|
114
11
|
*/
|
|
@@ -145,9 +42,158 @@ function hasAny(deps: Set<string>, names: string[]): boolean {
|
|
|
145
42
|
return false;
|
|
146
43
|
}
|
|
147
44
|
|
|
45
|
+
/**
|
|
46
|
+
* Service/integration dependency → profile suggestions. Unlike the framework
|
|
47
|
+
* chain below (mutually exclusive — a repo is *either* a Next.js app or a
|
|
48
|
+
* Vite app), these are additive: a Next.js shop with `stripe` installed gets
|
|
49
|
+
* both `nextjs` and `stripe` suggested. Confidence sits in the
|
|
50
|
+
* [SUGGESTED_MIN_CONFIDENCE, SUGGESTED_AUTO_PICK_CONFIDENCE) band on purpose:
|
|
51
|
+
* high enough to show in the picker, low enough to never outrank the primary
|
|
52
|
+
* stack profile or hijack the Enter default. Only profiles that exist in
|
|
53
|
+
* profiles/ belong here — the picker drops unknown names, but a dead rule is
|
|
54
|
+
* still noise.
|
|
55
|
+
*/
|
|
56
|
+
export interface DepProfileRule {
|
|
57
|
+
profile: string;
|
|
58
|
+
/** Exact dependency names that trigger the rule. */
|
|
59
|
+
deps?: string[];
|
|
60
|
+
/** Scoped-package prefixes, e.g. "@aws-sdk/". */
|
|
61
|
+
prefixes?: string[];
|
|
62
|
+
/** Python package names (PEP 503 normalized: lowercase, `_`/`.` → `-`). */
|
|
63
|
+
pyDeps?: string[];
|
|
64
|
+
confidence: number;
|
|
65
|
+
reason: string;
|
|
66
|
+
/**
|
|
67
|
+
* Eligible as a combine-multiselect companion (see `serviceCompanions`).
|
|
68
|
+
* True for service integrations that ride alongside a primary stack;
|
|
69
|
+
* false/omitted for rules that ARE a primary stack (react-native).
|
|
70
|
+
*/
|
|
71
|
+
companion?: boolean;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export const DEP_PROFILE_RULES: DepProfileRule[] = [
|
|
75
|
+
{ profile: "stripe", deps: ["stripe"], prefixes: ["@stripe/"], pyDeps: ["stripe"], confidence: 0.6, reason: "package.json has stripe", companion: true },
|
|
76
|
+
{ profile: "aws", deps: ["aws-sdk", "aws-cdk"], prefixes: ["@aws-sdk/", "@aws-cdk/"], pyDeps: ["boto3", "botocore", "aws-cdk-lib"], confidence: 0.6, reason: "package.json has @aws-sdk/*", companion: true },
|
|
77
|
+
{ profile: "supabase", prefixes: ["@supabase/"], pyDeps: ["supabase"], confidence: 0.6, reason: "package.json has @supabase/*", companion: true },
|
|
78
|
+
{ profile: "slack", prefixes: ["@slack/"], pyDeps: ["slack-sdk"], confidence: 0.6, reason: "package.json has @slack/*", companion: true },
|
|
79
|
+
{ profile: "postgres", deps: ["pg", "postgres", "pg-promise"], pyDeps: ["psycopg", "psycopg2", "psycopg2-binary", "asyncpg"], confidence: 0.55, reason: "package.json has pg/postgres", companion: true },
|
|
80
|
+
{ profile: "resend", deps: ["resend"], pyDeps: ["resend"], confidence: 0.6, reason: "package.json has resend", companion: true },
|
|
81
|
+
{ profile: "strapi", prefixes: ["@strapi/"], confidence: 0.65, reason: "package.json has @strapi/*", companion: true },
|
|
82
|
+
{ profile: "threejs", deps: ["three"], confidence: 0.6, reason: "package.json has three", companion: true },
|
|
83
|
+
// react-native is a primary stack, not a service: an RN repo also has
|
|
84
|
+
// `react`, which the framework chain reads as plain `frontend` (0.8) — so
|
|
85
|
+
// this one rule sits above the band to outrank that misread.
|
|
86
|
+
{ profile: "react-native", deps: ["react-native", "expo"], prefixes: ["@react-native/"], confidence: 0.85, reason: "package.json has react-native/expo" },
|
|
87
|
+
];
|
|
88
|
+
|
|
148
89
|
const ex = (cwd: string, rel: string): boolean => existsSync(join(cwd, rel));
|
|
149
90
|
const exAny = (cwd: string, rels: string[]): boolean => rels.some((r) => ex(cwd, r));
|
|
150
91
|
|
|
92
|
+
/**
|
|
93
|
+
* Cap on workspace child package.jsons read per detection. The picker calls
|
|
94
|
+
* detectProfileV2 synchronously on every launch, so a huge monorepo must not
|
|
95
|
+
* turn profile suggestion into a directory crawl.
|
|
96
|
+
*/
|
|
97
|
+
const MAX_WORKSPACE_PKGS = 24;
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Union of dependency names across workspace child packages, so a monorepo
|
|
101
|
+
* ROOT cwd still detects service deps (stripe in packages/api). Patterns come
|
|
102
|
+
* from package.json `workspaces` (array or {packages}) and pnpm-workspace.yaml.
|
|
103
|
+
* Best-effort glob support: exact paths and single trailing `/*` only — deeper
|
|
104
|
+
* globs (`**`, or a `*` mid-path) are skipped, negations ignored.
|
|
105
|
+
*/
|
|
106
|
+
function readWorkspaceDeps(cwd: string): Set<string> {
|
|
107
|
+
const patterns: string[] = [];
|
|
108
|
+
try {
|
|
109
|
+
const pkg = JSON.parse(readFileSync(join(cwd, "package.json"), "utf8"));
|
|
110
|
+
const ws = Array.isArray(pkg.workspaces) ? pkg.workspaces : pkg.workspaces?.packages;
|
|
111
|
+
if (Array.isArray(ws)) for (const w of ws) if (typeof w === "string") patterns.push(w);
|
|
112
|
+
} catch { /* no root package.json */ }
|
|
113
|
+
try {
|
|
114
|
+
const raw = readFileSync(join(cwd, "pnpm-workspace.yaml"), "utf8");
|
|
115
|
+
const section = raw.match(/^packages:\s*\n((?:[ \t]*-[^\n]*\n?)*)/m);
|
|
116
|
+
if (section) {
|
|
117
|
+
for (const m of section[1]!.matchAll(/-\s*["']?([^"'\n#]+)/g)) patterns.push(m[1]!.trim());
|
|
118
|
+
}
|
|
119
|
+
} catch { /* no pnpm workspace file */ }
|
|
120
|
+
|
|
121
|
+
const deps = new Set<string>();
|
|
122
|
+
let read = 0;
|
|
123
|
+
for (const pattern of patterns) {
|
|
124
|
+
if (pattern.startsWith("!")) continue;
|
|
125
|
+
const dirs: string[] = [];
|
|
126
|
+
if (pattern.endsWith("/*") && !pattern.slice(0, -2).includes("*")) {
|
|
127
|
+
const base = pattern.slice(0, -2);
|
|
128
|
+
try {
|
|
129
|
+
for (const e of readdirSync(join(cwd, base), { withFileTypes: true })) {
|
|
130
|
+
if (e.isDirectory()) dirs.push(join(base, e.name));
|
|
131
|
+
}
|
|
132
|
+
} catch { /* glob base missing */ }
|
|
133
|
+
} else if (!pattern.includes("*")) {
|
|
134
|
+
dirs.push(pattern);
|
|
135
|
+
}
|
|
136
|
+
for (const dir of dirs) {
|
|
137
|
+
if (read >= MAX_WORKSPACE_PKGS) return deps;
|
|
138
|
+
try {
|
|
139
|
+
const pkg = JSON.parse(readFileSync(join(cwd, dir, "package.json"), "utf8"));
|
|
140
|
+
read += 1;
|
|
141
|
+
for (const k of Object.keys(pkg.dependencies ?? {})) deps.add(k);
|
|
142
|
+
for (const k of Object.keys(pkg.devDependencies ?? {})) deps.add(k);
|
|
143
|
+
} catch { /* dir without package.json */ }
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return deps;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** PEP 503 name normalization: lowercase, runs of `-`/`_`/`.` → single `-`. */
|
|
150
|
+
const normPyName = (name: string): string => name.toLowerCase().replace(/[-_.]+/g, "-");
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Best-effort Python dependency names from requirements.txt and
|
|
154
|
+
* pyproject.toml, for the `pyDeps` side of DEP_PROFILE_RULES. requirements
|
|
155
|
+
* lines are parsed properly (comments, extras, version specifiers, env
|
|
156
|
+
* markers stripped); pyproject is a cheap regex over quoted strings in
|
|
157
|
+
* `dependencies = [...]` arrays plus `[tool.poetry.dependencies]` keys — not
|
|
158
|
+
* a TOML parser, same best-effort discipline as the rest of this module.
|
|
159
|
+
* `source` names the file that contributed deps, for detection reasons.
|
|
160
|
+
*/
|
|
161
|
+
function readPythonDeps(cwd: string): { deps: Set<string>; source: string } {
|
|
162
|
+
const deps = new Set<string>();
|
|
163
|
+
const sources: string[] = [];
|
|
164
|
+
try {
|
|
165
|
+
const raw = readFileSync(join(cwd, "requirements.txt"), "utf8");
|
|
166
|
+
for (const line of raw.split("\n")) {
|
|
167
|
+
const bare = line.split("#")[0]!.trim();
|
|
168
|
+
if (!bare || bare.startsWith("-")) continue; // blank / pip flags (-r, -e, --hash)
|
|
169
|
+
const m = bare.match(/^([A-Za-z0-9][A-Za-z0-9._-]*)/);
|
|
170
|
+
if (m) deps.add(normPyName(m[1]!));
|
|
171
|
+
}
|
|
172
|
+
if (deps.size > 0) sources.push("requirements.txt");
|
|
173
|
+
} catch { /* no requirements.txt */ }
|
|
174
|
+
try {
|
|
175
|
+
const raw = readFileSync(join(cwd, "pyproject.toml"), "utf8");
|
|
176
|
+
const before = deps.size;
|
|
177
|
+
// Quoted entries inside any `dependencies = [...]` array ([project] or
|
|
178
|
+
// optional-dependencies groups).
|
|
179
|
+
for (const arr of raw.matchAll(/dependencies\s*=\s*\[([^\]]*)\]/g)) {
|
|
180
|
+
for (const q of arr[1]!.matchAll(/["']([A-Za-z0-9][A-Za-z0-9._-]*)/g)) {
|
|
181
|
+
deps.add(normPyName(q[1]!));
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
// `[tool.poetry.dependencies]` table keys (one `name = ...` per line).
|
|
185
|
+
const poetry = raw.match(/\[tool\.poetry\.dependencies\]([^[]*)/);
|
|
186
|
+
if (poetry) {
|
|
187
|
+
for (const line of poetry[1]!.split("\n")) {
|
|
188
|
+
const m = line.match(/^\s*([A-Za-z0-9][A-Za-z0-9._-]*)\s*=/);
|
|
189
|
+
if (m && m[1] !== "python") deps.add(normPyName(m[1]!));
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
if (deps.size > before) sources.push("pyproject.toml");
|
|
193
|
+
} catch { /* no pyproject.toml */ }
|
|
194
|
+
return { deps, source: sources.join(" + ") };
|
|
195
|
+
}
|
|
196
|
+
|
|
151
197
|
/**
|
|
152
198
|
* Per-extra-signal confidence boost. A profile backed by several independent
|
|
153
199
|
* signals (e.g. `medusa-config.ts` + `@medusajs/*` dep) is a stronger match
|
|
@@ -208,6 +254,11 @@ export function detectProfileV2(cwd: string): DetectionResultV2[] {
|
|
|
208
254
|
if (exAny(cwd, ["vite.config.ts", "vite.config.js"])) add("frontend", 0.6, "vite.config.*");
|
|
209
255
|
if (exAny(cwd, ["tailwind.config.js", "tailwind.config.ts"])) add("frontend", 0.4, "tailwind.config.*");
|
|
210
256
|
|
|
257
|
+
// ── Vercel (deploy target) — an explicit vercel.json / .vercel signals intent
|
|
258
|
+
// to use Vercel, so it edges out the bare framework profile (nextjs/frontend).
|
|
259
|
+
if (exAny(cwd, ["vercel.json", ".vercel/project.json"])) add("vercel", 0.95, "vercel.json");
|
|
260
|
+
else if (ex(cwd, ".vercel")) add("vercel", 0.9, ".vercel/");
|
|
261
|
+
|
|
211
262
|
// ── Docs ──
|
|
212
263
|
if (exAny(cwd, ["astro.config.mjs", "docusaurus.config.js", "mkdocs.yml"])) {
|
|
213
264
|
add("docs-writer", 0.7, "astro / docusaurus / mkdocs config");
|
|
@@ -244,6 +295,42 @@ export function detectProfileV2(cwd: string): DetectionResultV2[] {
|
|
|
244
295
|
} else {
|
|
245
296
|
add("backend", 0.6, "package.json (no framework)");
|
|
246
297
|
}
|
|
298
|
+
// Vercel CLI / SDK in deps corroborates an existing vercel.json signal.
|
|
299
|
+
if (hasPrefix(allDeps, "@vercel/") || allDeps.has("vercel")) {
|
|
300
|
+
add("vercel", 0.6, "package.json @vercel/* or vercel");
|
|
301
|
+
}
|
|
302
|
+
// Service/integration deps (stripe, @aws-sdk/*, …) — additive, see table.
|
|
303
|
+
for (const rule of DEP_PROFILE_RULES) {
|
|
304
|
+
const hit =
|
|
305
|
+
(rule.deps !== undefined && hasAny(allDeps, rule.deps)) ||
|
|
306
|
+
(rule.prefixes ?? []).some((p) => hasPrefix(allDeps, p));
|
|
307
|
+
if (hit) add(rule.profile, rule.confidence, rule.reason);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// ── Workspace child deps (monorepo roots) — same rule table ──
|
|
312
|
+
if (exAny(cwd, ["package.json", "pnpm-workspace.yaml"])) {
|
|
313
|
+
const wsDeps = readWorkspaceDeps(cwd);
|
|
314
|
+
if (wsDeps.size > 0) {
|
|
315
|
+
for (const rule of DEP_PROFILE_RULES) {
|
|
316
|
+
const hit =
|
|
317
|
+
(rule.deps !== undefined && hasAny(wsDeps, rule.deps)) ||
|
|
318
|
+
(rule.prefixes ?? []).some((p) => hasPrefix(wsDeps, p));
|
|
319
|
+
if (hit) add(rule.profile, rule.confidence, `workspace ${rule.reason}`);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// ── Python deps (requirements.txt / pyproject.toml) — same rule table ──
|
|
325
|
+
if (exAny(cwd, ["requirements.txt", "pyproject.toml"])) {
|
|
326
|
+
const { deps: pyDeps, source } = readPythonDeps(cwd);
|
|
327
|
+
if (pyDeps.size > 0) {
|
|
328
|
+
for (const rule of DEP_PROFILE_RULES) {
|
|
329
|
+
if (rule.pyDeps === undefined) continue;
|
|
330
|
+
const matched = rule.pyDeps.find((d) => pyDeps.has(d));
|
|
331
|
+
if (matched !== undefined) add(rule.profile, rule.confidence, `${source} has ${matched}`);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
247
334
|
}
|
|
248
335
|
|
|
249
336
|
return [...results.entries()]
|
|
@@ -258,35 +345,3 @@ export function detectProfileV2(cwd: string): DetectionResultV2[] {
|
|
|
258
345
|
.sort((a, b) => b.confidence - a.confidence)
|
|
259
346
|
.slice(0, 5);
|
|
260
347
|
}
|
|
261
|
-
|
|
262
|
-
export function detectProfile(cwd: string): DetectionResult[] {
|
|
263
|
-
const scores = new Map<string, { score: number; max: number; signals: string[] }>();
|
|
264
|
-
|
|
265
|
-
// Compute max possible score per profile
|
|
266
|
-
for (const s of SIGNALS) {
|
|
267
|
-
const entry = scores.get(s.profile) ?? { score: 0, max: 0, signals: [] };
|
|
268
|
-
entry.max += s.weight;
|
|
269
|
-
scores.set(s.profile, entry);
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
// Score based on what exists
|
|
273
|
-
for (const s of SIGNALS) {
|
|
274
|
-
const target = join(cwd, s.file);
|
|
275
|
-
if (existsSync(target)) {
|
|
276
|
-
const entry = scores.get(s.profile)!;
|
|
277
|
-
entry.score += s.weight;
|
|
278
|
-
entry.signals.push(s.file);
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
return [...scores.entries()]
|
|
283
|
-
.map(([profile, d]) => ({
|
|
284
|
-
profile,
|
|
285
|
-
score: d.score,
|
|
286
|
-
maxScore: d.max,
|
|
287
|
-
confidence: d.max > 0 ? Math.round((d.score / d.max) * 100) : 0,
|
|
288
|
-
signals: d.signals,
|
|
289
|
-
}))
|
|
290
|
-
.filter(r => r.score > 0)
|
|
291
|
-
.sort((a, b) => b.confidence - a.confidence || b.score - a.score);
|
|
292
|
-
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Codex routing parity: a profile's persona (e.g. the core "Model routing"
|
|
3
|
+
* block) must reach BOTH agents' memory files — CLAUDE.md for claude-code and
|
|
4
|
+
* AGENTS.md for codex. The persona is the portable layer of the model-routing
|
|
5
|
+
* feature; the model-route-nudge hook is Claude-only (Codex has no
|
|
6
|
+
* UserPromptSubmit equivalent), so this guards the part Codex actually gets.
|
|
7
|
+
*
|
|
8
|
+
* Standalone file (not folded into runtime-materializer.test.ts) to avoid a
|
|
9
|
+
* cross-agent file lock on that test.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
13
|
+
import { mkdtemp, readFile, rm } from "node:fs/promises";
|
|
14
|
+
import { tmpdir } from "node:os";
|
|
15
|
+
import { join } from "node:path";
|
|
16
|
+
|
|
17
|
+
import { materializeRuntime } from "./runtime-materializer";
|
|
18
|
+
import type { ResolvedProfile } from "../../profiles/_types";
|
|
19
|
+
|
|
20
|
+
let root: string;
|
|
21
|
+
beforeEach(async () => { root = await mkdtemp(join(tmpdir(), "cue-persona-")); });
|
|
22
|
+
afterEach(async () => { await rm(root, { recursive: true, force: true }); });
|
|
23
|
+
|
|
24
|
+
const MARKER = "## Model routing — route by task hardness";
|
|
25
|
+
|
|
26
|
+
const profile: ResolvedProfile = {
|
|
27
|
+
name: "persona-parity-test",
|
|
28
|
+
description: "test",
|
|
29
|
+
agents: ["claude-code", "codex"],
|
|
30
|
+
skills: { local: [], npx: [] },
|
|
31
|
+
mcps: [],
|
|
32
|
+
plugins: [],
|
|
33
|
+
env: {},
|
|
34
|
+
inheritanceChain: ["persona-parity-test"],
|
|
35
|
+
// persona is read via (profile as any).persona by the materializer.
|
|
36
|
+
persona: `${MARKER}\n delegate EASY/SEARCH work to a Sonnet subagent`,
|
|
37
|
+
} as ResolvedProfile;
|
|
38
|
+
|
|
39
|
+
const common = {
|
|
40
|
+
runtimeRoot: "",
|
|
41
|
+
skillSourceLookup: async (id: string) => `/fake/skills/${id}`,
|
|
42
|
+
mcpRegistry: {},
|
|
43
|
+
userClaudeMd: "",
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
describe("persona reaches both agents' memory files", () => {
|
|
47
|
+
test("claude-code → CLAUDE.md contains the persona", async () => {
|
|
48
|
+
const out = await materializeRuntime({ ...common, runtimeRoot: join(root, "rt"), profile, agent: "claude-code" });
|
|
49
|
+
const md = await readFile(join(out.runtimeDir, "CLAUDE.md"), "utf8");
|
|
50
|
+
expect(md).toContain(MARKER);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test("codex → AGENTS.md contains the persona (routing parity)", async () => {
|
|
54
|
+
const out = await materializeRuntime({ ...common, runtimeRoot: join(root, "rt"), profile, agent: "codex" });
|
|
55
|
+
const md = await readFile(join(out.runtimeDir, "AGENTS.md"), "utf8");
|
|
56
|
+
expect(md).toContain(MARKER);
|
|
57
|
+
});
|
|
58
|
+
});
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test";
|
|
2
2
|
|
|
3
|
-
import { detectCompanions, type CompanionDetectInput } from "./companion-detect";
|
|
3
|
+
import { detectCompanions, serviceCompanions, type CompanionDetectInput } from "./companion-detect";
|
|
4
|
+
import { COMBINE_AUTO_CHECK_CONFIDENCE } from "./picker";
|
|
4
5
|
|
|
5
6
|
const KNOWN = new Set(["higgsfield", "blog-writer", "postizz", "creative-media"]);
|
|
6
7
|
|
|
@@ -106,3 +107,44 @@ describe("detectCompanions — gating & robustness", () => {
|
|
|
106
107
|
expect(detectCompanions({ cwd: "/work/x", knownProfiles: KNOWN })).toBeInstanceOf(Array);
|
|
107
108
|
});
|
|
108
109
|
});
|
|
110
|
+
|
|
111
|
+
describe("serviceCompanions", () => {
|
|
112
|
+
const SERVICE_KNOWN = new Set(["stripe", "aws", "react-native", "nextjs"]);
|
|
113
|
+
|
|
114
|
+
test("dep-detected stripe becomes a pre-checked combine companion", () => {
|
|
115
|
+
const out = serviceCompanions(
|
|
116
|
+
[{ profile: "stripe", confidence: 0.6, reasons: ["package.json has stripe"] }],
|
|
117
|
+
SERVICE_KNOWN,
|
|
118
|
+
);
|
|
119
|
+
expect(out).toHaveLength(1);
|
|
120
|
+
expect(out[0]!.profile).toBe("stripe");
|
|
121
|
+
// At/above the combine multiselect's auto-check line (0.7) so the row
|
|
122
|
+
// starts checked — a direct dependency is a strong companion signal.
|
|
123
|
+
expect(out[0]!.confidence).toBeGreaterThanOrEqual(COMBINE_AUTO_CHECK_CONFIDENCE);
|
|
124
|
+
expect(out[0]!.reason).toBe("package.json has stripe");
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test("primary-stack rules (react-native) never become companions", () => {
|
|
128
|
+
const out = serviceCompanions(
|
|
129
|
+
[{ profile: "react-native", confidence: 0.85, reasons: ["package.json has react-native/expo"] }],
|
|
130
|
+
SERVICE_KNOWN,
|
|
131
|
+
);
|
|
132
|
+
expect(out).toEqual([]);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test("non-rule detections (nextjs from framework chain) pass through nothing", () => {
|
|
136
|
+
const out = serviceCompanions(
|
|
137
|
+
[{ profile: "nextjs", confidence: 0.9, reasons: ["package.json has next"] }],
|
|
138
|
+
SERVICE_KNOWN,
|
|
139
|
+
);
|
|
140
|
+
expect(out).toEqual([]);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test("profiles not installed in this cue install are filtered out", () => {
|
|
144
|
+
const out = serviceCompanions(
|
|
145
|
+
[{ profile: "aws", confidence: 0.6, reasons: ["package.json has @aws-sdk/*"] }],
|
|
146
|
+
new Set(["stripe"]),
|
|
147
|
+
);
|
|
148
|
+
expect(out).toEqual([]);
|
|
149
|
+
});
|
|
150
|
+
});
|