@c0x12c/ai-toolkit 1.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +16 -0
- package/.claude-plugin/plugin.json +12 -0
- package/README.md +439 -0
- package/VERSION +1 -0
- package/agents/design-critic.md +127 -0
- package/agents/idea-killer.md +72 -0
- package/agents/infrastructure-expert.md +49 -0
- package/agents/micronaut-backend-expert.md +45 -0
- package/agents/phase-reviewer.md +150 -0
- package/agents/research-planner.md +70 -0
- package/agents/solution-architect-cto.md +49 -0
- package/agents/sre-architect.md +49 -0
- package/agents/team-coordinator.md +111 -0
- package/bin/cli.js +780 -0
- package/claude-md/00-header.md +39 -0
- package/claude-md/01-core.md +105 -0
- package/claude-md/05-database.md +20 -0
- package/claude-md/11-backend-micronaut.md +19 -0
- package/claude-md/20-frontend-react.md +44 -0
- package/claude-md/25-ux-design.md +56 -0
- package/claude-md/30-infrastructure.md +24 -0
- package/claude-md/30-project-mgmt.md +119 -0
- package/claude-md/40-product.md +39 -0
- package/claude-md/50-ops.md +34 -0
- package/claude-md/60-research.md +27 -0
- package/claude-md/90-footer.md +21 -0
- package/commands/spartan/brainstorm.md +134 -0
- package/commands/spartan/brownfield.md +157 -0
- package/commands/spartan/build.md +435 -0
- package/commands/spartan/careful.md +94 -0
- package/commands/spartan/commit-message.md +112 -0
- package/commands/spartan/content.md +17 -0
- package/commands/spartan/context-save.md +161 -0
- package/commands/spartan/contribute.md +140 -0
- package/commands/spartan/daily.md +42 -0
- package/commands/spartan/debug.md +308 -0
- package/commands/spartan/deep-dive.md +55 -0
- package/commands/spartan/deploy.md +207 -0
- package/commands/spartan/e2e.md +264 -0
- package/commands/spartan/env-setup.md +166 -0
- package/commands/spartan/epic.md +199 -0
- package/commands/spartan/fe-review.md +181 -0
- package/commands/spartan/figma-to-code.md +260 -0
- package/commands/spartan/forensics.md +46 -0
- package/commands/spartan/freeze.md +84 -0
- package/commands/spartan/fundraise.md +53 -0
- package/commands/spartan/gate-review.md +229 -0
- package/commands/spartan/gsd-upgrade.md +376 -0
- package/commands/spartan/guard.md +42 -0
- package/commands/spartan/init-project.md +178 -0
- package/commands/spartan/init-rules.md +298 -0
- package/commands/spartan/interview.md +154 -0
- package/commands/spartan/kickoff.md +73 -0
- package/commands/spartan/kotlin-service.md +109 -0
- package/commands/spartan/lean-canvas.md +222 -0
- package/commands/spartan/lint-rules.md +122 -0
- package/commands/spartan/map-codebase.md +124 -0
- package/commands/spartan/migration.md +82 -0
- package/commands/spartan/next-app.md +317 -0
- package/commands/spartan/next-feature.md +212 -0
- package/commands/spartan/onboard.md +326 -0
- package/commands/spartan/outreach.md +16 -0
- package/commands/spartan/phase.md +142 -0
- package/commands/spartan/pitch.md +18 -0
- package/commands/spartan/plan.md +210 -0
- package/commands/spartan/pr-ready.md +202 -0
- package/commands/spartan/project.md +106 -0
- package/commands/spartan/qa.md +222 -0
- package/commands/spartan/research.md +254 -0
- package/commands/spartan/review.md +132 -0
- package/commands/spartan/scan-rules.md +173 -0
- package/commands/spartan/sessions.md +143 -0
- package/commands/spartan/spec.md +131 -0
- package/commands/spartan/startup.md +257 -0
- package/commands/spartan/team.md +570 -0
- package/commands/spartan/teardown.md +161 -0
- package/commands/spartan/testcontainer.md +97 -0
- package/commands/spartan/tf-cost.md +123 -0
- package/commands/spartan/tf-deploy.md +116 -0
- package/commands/spartan/tf-drift.md +100 -0
- package/commands/spartan/tf-import.md +107 -0
- package/commands/spartan/tf-module.md +121 -0
- package/commands/spartan/tf-plan.md +100 -0
- package/commands/spartan/tf-review.md +106 -0
- package/commands/spartan/tf-scaffold.md +109 -0
- package/commands/spartan/tf-security.md +147 -0
- package/commands/spartan/think.md +221 -0
- package/commands/spartan/unfreeze.md +13 -0
- package/commands/spartan/update.md +134 -0
- package/commands/spartan/ux.md +1233 -0
- package/commands/spartan/validate.md +193 -0
- package/commands/spartan/web-to-prd.md +706 -0
- package/commands/spartan/workstreams.md +109 -0
- package/commands/spartan/write.md +16 -0
- package/commands/spartan.md +386 -0
- package/frameworks/00-framework-comparison-guide.md +317 -0
- package/frameworks/01-lean-canvas.md +196 -0
- package/frameworks/02-design-sprint.md +304 -0
- package/frameworks/03-foundation-sprint.md +337 -0
- package/frameworks/04-business-model-canvas.md +391 -0
- package/frameworks/05-customer-development.md +426 -0
- package/frameworks/06-jobs-to-be-done.md +358 -0
- package/frameworks/07-mom-test.md +392 -0
- package/frameworks/08-value-proposition-canvas.md +488 -0
- package/frameworks/09-javelin-board.md +428 -0
- package/frameworks/10-build-measure-learn.md +467 -0
- package/frameworks/11-mvp-approaches.md +533 -0
- package/frameworks/think-before-build.md +593 -0
- package/lib/assembler.js +197 -0
- package/lib/assembler.test.js +159 -0
- package/lib/detector.js +166 -0
- package/lib/detector.test.js +221 -0
- package/lib/packs.js +16 -0
- package/lib/resolver.js +272 -0
- package/lib/resolver.test.js +298 -0
- package/lib/worktree.sh +104 -0
- package/package.json +50 -0
- package/packs/backend-micronaut.yaml +35 -0
- package/packs/backend-nodejs.yaml +15 -0
- package/packs/backend-python.yaml +15 -0
- package/packs/core.yaml +37 -0
- package/packs/database.yaml +21 -0
- package/packs/frontend-react.yaml +24 -0
- package/packs/infrastructure.yaml +40 -0
- package/packs/ops.yaml +16 -0
- package/packs/packs.compiled.json +371 -0
- package/packs/product.yaml +22 -0
- package/packs/project-mgmt.yaml +24 -0
- package/packs/research.yaml +39 -0
- package/packs/shared-backend.yaml +14 -0
- package/packs/ux-design.yaml +21 -0
- package/rules/backend-micronaut/API_DESIGN.md +313 -0
- package/rules/backend-micronaut/BATCH_PROCESSING.md +92 -0
- package/rules/backend-micronaut/CONTROLLERS.md +388 -0
- package/rules/backend-micronaut/KOTLIN.md +414 -0
- package/rules/backend-micronaut/RETROFIT_PLACEMENT.md +290 -0
- package/rules/backend-micronaut/SERVICES_AND_BEANS.md +325 -0
- package/rules/core/NAMING_CONVENTIONS.md +208 -0
- package/rules/core/SKILL_AUTHORING.md +174 -0
- package/rules/core/TIMEZONE.md +316 -0
- package/rules/database/ORM_AND_REPO.md +289 -0
- package/rules/database/SCHEMA.md +146 -0
- package/rules/database/TRANSACTIONS.md +311 -0
- package/rules/frontend-react/FRONTEND.md +344 -0
- package/rules/infrastructure/MODULES.md +260 -0
- package/rules/infrastructure/NAMING.md +196 -0
- package/rules/infrastructure/PROVIDERS.md +309 -0
- package/rules/infrastructure/SECURITY.md +310 -0
- package/rules/infrastructure/STATE_AND_BACKEND.md +237 -0
- package/rules/infrastructure/STRUCTURE.md +234 -0
- package/rules/infrastructure/VARIABLES.md +285 -0
- package/rules/shared-backend/ARCHITECTURE.md +46 -0
- package/rules/ux-design/DESIGN_PROCESS.md +176 -0
- package/skills/api-endpoint-creator/SKILL.md +455 -0
- package/skills/api-endpoint-creator/error-handling-guide.md +244 -0
- package/skills/api-endpoint-creator/examples.md +522 -0
- package/skills/api-endpoint-creator/testing-patterns.md +302 -0
- package/skills/article-writing/SKILL.md +109 -0
- package/skills/article-writing/examples.md +59 -0
- package/skills/backend-api-design/SKILL.md +84 -0
- package/skills/backend-api-design/code-patterns.md +138 -0
- package/skills/brainstorm/SKILL.md +95 -0
- package/skills/browser-qa/SKILL.md +87 -0
- package/skills/browser-qa/playwright-snippets.md +110 -0
- package/skills/ci-cd-patterns/SKILL.md +108 -0
- package/skills/ci-cd-patterns/workflows.md +149 -0
- package/skills/competitive-teardown/SKILL.md +93 -0
- package/skills/competitive-teardown/example-analysis.md +50 -0
- package/skills/content-engine/SKILL.md +131 -0
- package/skills/content-engine/examples.md +72 -0
- package/skills/database-patterns/SKILL.md +72 -0
- package/skills/database-patterns/code-templates.md +114 -0
- package/skills/database-table-creator/SKILL.md +141 -0
- package/skills/database-table-creator/examples.md +552 -0
- package/skills/database-table-creator/kotlin-templates.md +400 -0
- package/skills/database-table-creator/migration-template.sql +68 -0
- package/skills/database-table-creator/validation-checklist.md +337 -0
- package/skills/deep-research/SKILL.md +80 -0
- package/skills/design-intelligence/SKILL.md +268 -0
- package/skills/design-workflow/SKILL.md +127 -0
- package/skills/design-workflow/checklists.md +45 -0
- package/skills/idea-validation/SKILL.md +129 -0
- package/skills/idea-validation/example-report.md +50 -0
- package/skills/investor-materials/SKILL.md +122 -0
- package/skills/investor-materials/example-outline.md +70 -0
- package/skills/investor-outreach/SKILL.md +112 -0
- package/skills/investor-outreach/examples.md +76 -0
- package/skills/kotlin-best-practices/SKILL.md +58 -0
- package/skills/kotlin-best-practices/code-patterns.md +132 -0
- package/skills/market-research/SKILL.md +99 -0
- package/skills/security-checklist/SKILL.md +65 -0
- package/skills/security-checklist/audit-reference.md +95 -0
- package/skills/service-debugging/SKILL.md +116 -0
- package/skills/service-debugging/common-issues.md +65 -0
- package/skills/startup-pipeline/SKILL.md +152 -0
- package/skills/terraform-best-practices/SKILL.md +244 -0
- package/skills/terraform-module-creator/SKILL.md +284 -0
- package/skills/terraform-review/SKILL.md +222 -0
- package/skills/terraform-security-audit/SKILL.md +280 -0
- package/skills/terraform-service-scaffold/SKILL.md +574 -0
- package/skills/testing-strategies/SKILL.md +116 -0
- package/skills/testing-strategies/examples.md +103 -0
- package/skills/testing-strategies/integration-test-setup.md +71 -0
- package/skills/ui-ux-pro-max/SKILL.md +238 -0
- package/skills/ui-ux-pro-max/data/charts.csv +26 -0
- package/skills/ui-ux-pro-max/data/colors.csv +97 -0
- package/skills/ui-ux-pro-max/data/icons.csv +101 -0
- package/skills/ui-ux-pro-max/data/landing.csv +31 -0
- package/skills/ui-ux-pro-max/data/products.csv +97 -0
- package/skills/ui-ux-pro-max/data/react-performance.csv +45 -0
- package/skills/ui-ux-pro-max/data/stacks/astro.csv +54 -0
- package/skills/ui-ux-pro-max/data/stacks/flutter.csv +53 -0
- package/skills/ui-ux-pro-max/data/stacks/html-tailwind.csv +56 -0
- package/skills/ui-ux-pro-max/data/stacks/jetpack-compose.csv +53 -0
- package/skills/ui-ux-pro-max/data/stacks/nextjs.csv +53 -0
- package/skills/ui-ux-pro-max/data/stacks/nuxt-ui.csv +51 -0
- package/skills/ui-ux-pro-max/data/stacks/nuxtjs.csv +59 -0
- package/skills/ui-ux-pro-max/data/stacks/react-native.csv +52 -0
- package/skills/ui-ux-pro-max/data/stacks/react.csv +54 -0
- package/skills/ui-ux-pro-max/data/stacks/shadcn.csv +61 -0
- package/skills/ui-ux-pro-max/data/stacks/svelte.csv +54 -0
- package/skills/ui-ux-pro-max/data/stacks/swiftui.csv +51 -0
- package/skills/ui-ux-pro-max/data/stacks/vue.csv +50 -0
- package/skills/ui-ux-pro-max/data/styles.csv +68 -0
- package/skills/ui-ux-pro-max/data/typography.csv +58 -0
- package/skills/ui-ux-pro-max/data/ui-reasoning.csv +101 -0
- package/skills/ui-ux-pro-max/data/ux-guidelines.csv +100 -0
- package/skills/ui-ux-pro-max/data/web-interface.csv +31 -0
- package/skills/ui-ux-pro-max/python-setup.md +146 -0
- package/skills/ui-ux-pro-max/scripts/core.py +253 -0
- package/skills/ui-ux-pro-max/scripts/design_system.py +1067 -0
- package/skills/ui-ux-pro-max/scripts/search.py +114 -0
- package/skills/web-to-prd/SKILL.md +478 -0
- package/templates/build-config.yaml +44 -0
- package/templates/commands-config.yaml +55 -0
- package/templates/competitor-analysis.md +60 -0
- package/templates/content/AGENT_TEMPLATE.md +47 -0
- package/templates/content/COMMAND_TEMPLATE.md +27 -0
- package/templates/content/RULE_TEMPLATE.md +40 -0
- package/templates/content/SKILL_TEMPLATE.md +41 -0
- package/templates/design-config.md +105 -0
- package/templates/design-doc.md +207 -0
- package/templates/epic.md +100 -0
- package/templates/feature-spec.md +181 -0
- package/templates/idea-canvas.md +47 -0
- package/templates/implementation-plan.md +159 -0
- package/templates/prd-template.md +86 -0
- package/templates/preamble.md +89 -0
- package/templates/project-readme.md +35 -0
- package/templates/quality-gates.md +230 -0
- package/templates/spartan-config.yaml +164 -0
- package/templates/user-interview.md +69 -0
- package/templates/validation-checklist.md +108 -0
- package/templates/workflow-backend-micronaut.md +409 -0
- package/templates/workflow-frontend-react.md +233 -0
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
// Spartan AI Toolkit — Stack Detector Tests
|
|
2
|
+
// Run: node --test toolkit/lib/detector.test.js
|
|
3
|
+
|
|
4
|
+
import { describe, it } from 'node:test';
|
|
5
|
+
import assert from 'node:assert/strict';
|
|
6
|
+
import { mkdirSync, writeFileSync, rmSync } from 'node:fs';
|
|
7
|
+
import { join } from 'node:path';
|
|
8
|
+
import { detectStacks } from './detector.js';
|
|
9
|
+
|
|
10
|
+
// ── Test fixtures ────────────────────────────────────────────────
|
|
11
|
+
const TMP = join(import.meta.dirname, '..', '.test-tmp-detector');
|
|
12
|
+
|
|
13
|
+
function setup() {
|
|
14
|
+
rmSync(TMP, { recursive: true, force: true });
|
|
15
|
+
mkdirSync(TMP, { recursive: true });
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function cleanup() {
|
|
19
|
+
rmSync(TMP, { recursive: true, force: true });
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// ── Tests ────────────────────────────────────────────────────────
|
|
23
|
+
|
|
24
|
+
describe('detectStacks', () => {
|
|
25
|
+
it('detects Kotlin + Micronaut from build.gradle.kts', () => {
|
|
26
|
+
setup();
|
|
27
|
+
try {
|
|
28
|
+
writeFileSync(join(TMP, 'build.gradle.kts'), `
|
|
29
|
+
plugins {
|
|
30
|
+
id("io.micronaut.application") version "4.0.0"
|
|
31
|
+
}
|
|
32
|
+
dependencies {
|
|
33
|
+
implementation("io.micronaut:micronaut-http-server-netty")
|
|
34
|
+
}
|
|
35
|
+
`);
|
|
36
|
+
const result = detectStacks(TMP);
|
|
37
|
+
assert.ok(result.detected.some(d => d.pack === 'backend-micronaut'), 'should detect backend-micronaut');
|
|
38
|
+
} finally {
|
|
39
|
+
cleanup();
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('detects React + Next.js from package.json', () => {
|
|
44
|
+
setup();
|
|
45
|
+
try {
|
|
46
|
+
writeFileSync(join(TMP, 'package.json'), JSON.stringify({
|
|
47
|
+
dependencies: { next: '14.0.0', react: '18.0.0' },
|
|
48
|
+
}));
|
|
49
|
+
const result = detectStacks(TMP);
|
|
50
|
+
assert.ok(result.detected.some(d => d.pack === 'frontend-react'), 'should detect frontend-react');
|
|
51
|
+
} finally {
|
|
52
|
+
cleanup();
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('detects React without Next.js', () => {
|
|
57
|
+
setup();
|
|
58
|
+
try {
|
|
59
|
+
writeFileSync(join(TMP, 'package.json'), JSON.stringify({
|
|
60
|
+
dependencies: { react: '18.0.0', 'react-dom': '18.0.0' },
|
|
61
|
+
}));
|
|
62
|
+
const result = detectStacks(TMP);
|
|
63
|
+
assert.ok(result.detected.some(d => d.pack === 'frontend-react'), 'should detect frontend-react');
|
|
64
|
+
} finally {
|
|
65
|
+
cleanup();
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('detects Next.js from next.config.js', () => {
|
|
70
|
+
setup();
|
|
71
|
+
try {
|
|
72
|
+
writeFileSync(join(TMP, 'next.config.js'), 'module.exports = {};');
|
|
73
|
+
const result = detectStacks(TMP);
|
|
74
|
+
assert.ok(result.detected.some(d => d.pack === 'frontend-react'), 'should detect frontend-react');
|
|
75
|
+
} finally {
|
|
76
|
+
cleanup();
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('detects Node.js backend (express) — marks as coming soon', () => {
|
|
81
|
+
setup();
|
|
82
|
+
try {
|
|
83
|
+
writeFileSync(join(TMP, 'package.json'), JSON.stringify({
|
|
84
|
+
dependencies: { express: '4.18.0' },
|
|
85
|
+
}));
|
|
86
|
+
const result = detectStacks(TMP);
|
|
87
|
+
assert.ok(!result.detected.some(d => d.pack === 'backend-nodejs'), 'should NOT be in detected');
|
|
88
|
+
assert.ok(result.comingSoon.some(d => d.pack === 'backend-nodejs'), 'should be in comingSoon');
|
|
89
|
+
} finally {
|
|
90
|
+
cleanup();
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('detects Python backend (FastAPI) — marks as coming soon', () => {
|
|
95
|
+
setup();
|
|
96
|
+
try {
|
|
97
|
+
writeFileSync(join(TMP, 'requirements.txt'), 'fastapi==0.100.0\nuvicorn==0.23.0\n');
|
|
98
|
+
const result = detectStacks(TMP);
|
|
99
|
+
assert.ok(!result.detected.some(d => d.pack === 'backend-python'), 'should NOT be in detected');
|
|
100
|
+
assert.ok(result.comingSoon.some(d => d.pack === 'backend-python'), 'should be in comingSoon');
|
|
101
|
+
} finally {
|
|
102
|
+
cleanup();
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('detects Django from manage.py', () => {
|
|
107
|
+
setup();
|
|
108
|
+
try {
|
|
109
|
+
writeFileSync(join(TMP, 'manage.py'), '#!/usr/bin/env python\nimport django\n');
|
|
110
|
+
const result = detectStacks(TMP);
|
|
111
|
+
assert.ok(result.comingSoon.some(d => d.pack === 'backend-python'), 'should detect python as coming soon');
|
|
112
|
+
} finally {
|
|
113
|
+
cleanup();
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('detects Micronaut from pom.xml', () => {
|
|
118
|
+
setup();
|
|
119
|
+
try {
|
|
120
|
+
writeFileSync(join(TMP, 'pom.xml'), `
|
|
121
|
+
<project>
|
|
122
|
+
<dependencies>
|
|
123
|
+
<dependency>
|
|
124
|
+
<groupId>io.micronaut</groupId>
|
|
125
|
+
<artifactId>micronaut-http-server-netty</artifactId>
|
|
126
|
+
</dependency>
|
|
127
|
+
</dependencies>
|
|
128
|
+
</project>
|
|
129
|
+
`);
|
|
130
|
+
const result = detectStacks(TMP);
|
|
131
|
+
assert.ok(result.detected.some(d => d.pack === 'backend-micronaut'), 'should detect backend-micronaut');
|
|
132
|
+
} finally {
|
|
133
|
+
cleanup();
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('returns empty for empty directory', () => {
|
|
138
|
+
setup();
|
|
139
|
+
try {
|
|
140
|
+
const result = detectStacks(TMP);
|
|
141
|
+
assert.equal(result.detected.length, 0);
|
|
142
|
+
assert.equal(result.comingSoon.length, 0);
|
|
143
|
+
} finally {
|
|
144
|
+
cleanup();
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it('detects both backend and frontend in same project', () => {
|
|
149
|
+
setup();
|
|
150
|
+
try {
|
|
151
|
+
writeFileSync(join(TMP, 'build.gradle.kts'), 'id("io.micronaut.application")');
|
|
152
|
+
writeFileSync(join(TMP, 'package.json'), JSON.stringify({
|
|
153
|
+
dependencies: { next: '14.0.0', react: '18.0.0' },
|
|
154
|
+
}));
|
|
155
|
+
const result = detectStacks(TMP);
|
|
156
|
+
assert.ok(result.detected.some(d => d.pack === 'backend-micronaut'), 'should detect backend');
|
|
157
|
+
assert.ok(result.detected.some(d => d.pack === 'frontend-react'), 'should detect frontend');
|
|
158
|
+
} finally {
|
|
159
|
+
cleanup();
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it('prefers frontend-react over backend-nodejs when both next and express are present', () => {
|
|
164
|
+
setup();
|
|
165
|
+
try {
|
|
166
|
+
writeFileSync(join(TMP, 'package.json'), JSON.stringify({
|
|
167
|
+
dependencies: { next: '14.0.0', react: '18.0.0', express: '4.18.0' },
|
|
168
|
+
}));
|
|
169
|
+
const result = detectStacks(TMP);
|
|
170
|
+
assert.ok(result.detected.some(d => d.pack === 'frontend-react'), 'should detect frontend');
|
|
171
|
+
assert.ok(!result.detected.some(d => d.pack === 'backend-nodejs'), 'should NOT detect nodejs backend');
|
|
172
|
+
assert.ok(!result.comingSoon.some(d => d.pack === 'backend-nodejs'), 'should NOT show nodejs as coming soon');
|
|
173
|
+
} finally {
|
|
174
|
+
cleanup();
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it('detects from subdirectory (1 level deep)', () => {
|
|
179
|
+
setup();
|
|
180
|
+
try {
|
|
181
|
+
const subdir = join(TMP, 'backend');
|
|
182
|
+
mkdirSync(subdir);
|
|
183
|
+
writeFileSync(join(subdir, 'build.gradle.kts'), 'id("io.micronaut.application")');
|
|
184
|
+
const result = detectStacks(TMP);
|
|
185
|
+
assert.ok(result.detected.some(d => d.pack === 'backend-micronaut'), 'should detect from subdir');
|
|
186
|
+
} finally {
|
|
187
|
+
cleanup();
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it('includes reason string for each detection', () => {
|
|
192
|
+
setup();
|
|
193
|
+
try {
|
|
194
|
+
writeFileSync(join(TMP, 'build.gradle.kts'), 'id("io.micronaut.application")');
|
|
195
|
+
const result = detectStacks(TMP);
|
|
196
|
+
const match = result.detected.find(d => d.pack === 'backend-micronaut');
|
|
197
|
+
assert.ok(match.reason, 'should have a reason');
|
|
198
|
+
assert.ok(match.reason.length > 0, 'reason should not be empty');
|
|
199
|
+
} finally {
|
|
200
|
+
cleanup();
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it('does not detect Kotlin without Micronaut', () => {
|
|
205
|
+
setup();
|
|
206
|
+
try {
|
|
207
|
+
writeFileSync(join(TMP, 'build.gradle.kts'), `
|
|
208
|
+
plugins {
|
|
209
|
+
kotlin("jvm") version "1.9.0"
|
|
210
|
+
}
|
|
211
|
+
dependencies {
|
|
212
|
+
implementation("io.ktor:ktor-server-core")
|
|
213
|
+
}
|
|
214
|
+
`);
|
|
215
|
+
const result = detectStacks(TMP);
|
|
216
|
+
assert.ok(!result.detected.some(d => d.pack === 'backend-micronaut'), 'should NOT detect micronaut');
|
|
217
|
+
} finally {
|
|
218
|
+
cleanup();
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
});
|
package/lib/packs.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// Spartan AI Toolkit — Pack Definitions
|
|
2
|
+
// Loads from YAML manifests in toolkit/packs/. Single source of truth.
|
|
3
|
+
|
|
4
|
+
import { dirname, join } from 'node:path';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
import { loadManifests, detectCycles, toPacks } from './resolver.js';
|
|
7
|
+
|
|
8
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
9
|
+
const PACKS_DIR = join(__dirname, '..', 'packs');
|
|
10
|
+
|
|
11
|
+
const manifests = loadManifests(PACKS_DIR);
|
|
12
|
+
detectCycles(manifests);
|
|
13
|
+
|
|
14
|
+
const { PACKS, PACK_ORDER } = toPacks(manifests);
|
|
15
|
+
|
|
16
|
+
export { PACKS, PACK_ORDER };
|
package/lib/resolver.js
ADDED
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
// Spartan AI Toolkit — Pack Resolver
|
|
2
|
+
// Loads YAML manifests and resolves dependencies (BFS + cycle detection).
|
|
3
|
+
|
|
4
|
+
import { readFileSync, readdirSync, existsSync } from 'node:fs';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
import { load } from 'js-yaml';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Load all pack manifests from a directory.
|
|
10
|
+
* @param {string} packsDir - Path to toolkit/packs/
|
|
11
|
+
* @returns {Map<string, object>} Pack name → manifest object
|
|
12
|
+
*/
|
|
13
|
+
export function loadManifests(packsDir) {
|
|
14
|
+
const manifests = new Map();
|
|
15
|
+
for (const file of readdirSync(packsDir).filter(f => f.endsWith('.yaml'))) {
|
|
16
|
+
const raw = readFileSync(join(packsDir, file), 'utf-8');
|
|
17
|
+
const manifest = load(raw);
|
|
18
|
+
manifests.set(manifest.name, manifest);
|
|
19
|
+
}
|
|
20
|
+
return manifests;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Detect cycles using DFS.
|
|
25
|
+
* @param {Map<string, object>} manifests
|
|
26
|
+
* @throws {Error} If a cycle is found
|
|
27
|
+
*/
|
|
28
|
+
export function detectCycles(manifests) {
|
|
29
|
+
const visited = new Set();
|
|
30
|
+
const stack = new Set();
|
|
31
|
+
|
|
32
|
+
function dfs(name, path) {
|
|
33
|
+
if (stack.has(name)) {
|
|
34
|
+
throw new Error(`Cycle detected: ${[...path, name].join(' → ')}`);
|
|
35
|
+
}
|
|
36
|
+
if (visited.has(name)) return;
|
|
37
|
+
stack.add(name);
|
|
38
|
+
path.push(name);
|
|
39
|
+
const manifest = manifests.get(name);
|
|
40
|
+
if (manifest?.depends) {
|
|
41
|
+
for (const dep of manifest.depends) {
|
|
42
|
+
dfs(dep, [...path]);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
stack.delete(name);
|
|
46
|
+
visited.add(name);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
for (const name of manifests.keys()) {
|
|
50
|
+
dfs(name, []);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Resolve dependencies using BFS. Returns sorted pack list with core always first.
|
|
56
|
+
* @param {string[]} selected - Pack names the user picked
|
|
57
|
+
* @param {Map<string, object>} manifests - All loaded manifests
|
|
58
|
+
* @returns {string[]} Resolved + sorted pack names
|
|
59
|
+
*/
|
|
60
|
+
export function resolve(selected, manifests) {
|
|
61
|
+
const resolved = new Set();
|
|
62
|
+
const queue = [...selected];
|
|
63
|
+
|
|
64
|
+
while (queue.length > 0) {
|
|
65
|
+
const pack = queue.shift();
|
|
66
|
+
if (resolved.has(pack)) continue;
|
|
67
|
+
if (!manifests.has(pack)) {
|
|
68
|
+
throw new Error(`Unknown pack: '${pack}'`);
|
|
69
|
+
}
|
|
70
|
+
resolved.add(pack);
|
|
71
|
+
const manifest = manifests.get(pack);
|
|
72
|
+
if (manifest.depends) {
|
|
73
|
+
for (const dep of manifest.depends) {
|
|
74
|
+
if (!resolved.has(dep)) queue.push(dep);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Core always included
|
|
80
|
+
resolved.add('core');
|
|
81
|
+
|
|
82
|
+
// Sort by priority, core always first
|
|
83
|
+
const sorted = [...resolved].sort((a, b) => {
|
|
84
|
+
const pa = manifests.get(a)?.priority ?? 999;
|
|
85
|
+
const pb = manifests.get(b)?.priority ?? 999;
|
|
86
|
+
return pa - pb;
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
return sorted;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Pack name aliases for backward compatibility. */
|
|
93
|
+
export const ALIASES = {
|
|
94
|
+
backend: 'backend-micronaut',
|
|
95
|
+
frontend: 'frontend-react',
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Resolve aliases in a list of pack names.
|
|
100
|
+
* @param {string[]} names - Pack names (may include old names)
|
|
101
|
+
* @returns {{ resolved: string[], warnings: string[] }}
|
|
102
|
+
*/
|
|
103
|
+
export function resolveAliases(names) {
|
|
104
|
+
const resolved = [];
|
|
105
|
+
const warnings = [];
|
|
106
|
+
for (const name of names) {
|
|
107
|
+
if (ALIASES[name]) {
|
|
108
|
+
resolved.push(ALIASES[name]);
|
|
109
|
+
warnings.push(`"${name}" is now "${ALIASES[name]}". Update your .spartan-packs file.`);
|
|
110
|
+
} else {
|
|
111
|
+
resolved.push(name);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return { resolved, warnings };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Convert manifests to the PACKS format used by assembler and CLI.
|
|
119
|
+
* @param {Map<string, object>} manifests
|
|
120
|
+
* @returns {{ PACKS: object, PACK_ORDER: string[] }}
|
|
121
|
+
*/
|
|
122
|
+
export function toPacks(manifests) {
|
|
123
|
+
const PACKS = {};
|
|
124
|
+
const allPacks = [...manifests.values()].sort((a, b) => (a.priority ?? 999) - (b.priority ?? 999));
|
|
125
|
+
|
|
126
|
+
for (const m of allPacks) {
|
|
127
|
+
PACKS[m.name] = {
|
|
128
|
+
description: m.description,
|
|
129
|
+
category: m.category || null,
|
|
130
|
+
priority: m.priority ?? 999,
|
|
131
|
+
hidden: m.hidden || false,
|
|
132
|
+
comingSoon: m['coming-soon'] || false,
|
|
133
|
+
depends: m.depends || [],
|
|
134
|
+
commands: m.commands || [],
|
|
135
|
+
rules: m.rules || [],
|
|
136
|
+
skills: m.skills || [],
|
|
137
|
+
agents: m.agents || [],
|
|
138
|
+
claudeSections: m['claude-sections'] || [],
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const PACK_ORDER = allPacks.map(m => m.name);
|
|
143
|
+
return { PACKS, PACK_ORDER };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// ── Community Pack Support ─────────────────────────────────────
|
|
147
|
+
|
|
148
|
+
const KEBAB_RE = /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/;
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Validate a community pack manifest.
|
|
152
|
+
* @param {object} manifest - The parsed YAML manifest
|
|
153
|
+
* @param {string} packDir - Root directory of the external pack (for file checks)
|
|
154
|
+
* @param {Set<string>} builtinNames - Names of built-in packs (collision check)
|
|
155
|
+
* @returns {{ valid: boolean, errors: string[], warnings: string[] }}
|
|
156
|
+
*/
|
|
157
|
+
export function validatePack(manifest, packDir, builtinNames) {
|
|
158
|
+
const errors = [];
|
|
159
|
+
const warnings = [];
|
|
160
|
+
|
|
161
|
+
// Required fields
|
|
162
|
+
if (!manifest.name) {
|
|
163
|
+
errors.push('Missing required field: name');
|
|
164
|
+
}
|
|
165
|
+
if (!manifest.description) {
|
|
166
|
+
errors.push('Missing required field: description');
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Name format
|
|
170
|
+
if (manifest.name && !KEBAB_RE.test(manifest.name)) {
|
|
171
|
+
errors.push(`Pack name "${manifest.name}" must be kebab-case (e.g., "go-backend")`);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Name collision
|
|
175
|
+
if (manifest.name && builtinNames.has(manifest.name)) {
|
|
176
|
+
errors.push(`Pack name "${manifest.name}" collides with a built-in pack`);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Dependency check — deps must be either built-in or in the same external dir
|
|
180
|
+
if (manifest.depends) {
|
|
181
|
+
for (const dep of manifest.depends) {
|
|
182
|
+
if (!builtinNames.has(dep)) {
|
|
183
|
+
// Check if dep is another external pack in same dir
|
|
184
|
+
const depFile = join(packDir, 'packs', `${dep}.yaml`);
|
|
185
|
+
if (!existsSync(depFile)) {
|
|
186
|
+
errors.push(`Dependency "${dep}" is not a built-in pack and not found in pack directory`);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// File existence warnings (non-blocking)
|
|
193
|
+
if (manifest.commands) {
|
|
194
|
+
for (const cmd of manifest.commands) {
|
|
195
|
+
const cmdFile = join(packDir, 'commands', 'spartan', `${cmd}.md`);
|
|
196
|
+
if (!existsSync(cmdFile)) {
|
|
197
|
+
warnings.push(`Command file not found: commands/spartan/${cmd}.md`);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
if (manifest.rules) {
|
|
202
|
+
for (const rule of manifest.rules) {
|
|
203
|
+
const ruleFile = join(packDir, 'rules', rule);
|
|
204
|
+
if (!existsSync(ruleFile)) {
|
|
205
|
+
warnings.push(`Rule file not found: rules/${rule}`);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
if (manifest.skills) {
|
|
210
|
+
for (const skill of manifest.skills) {
|
|
211
|
+
const skillDir = join(packDir, 'skills', skill);
|
|
212
|
+
if (!existsSync(skillDir)) {
|
|
213
|
+
warnings.push(`Skill directory not found: skills/${skill}`);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
if (manifest.agents) {
|
|
218
|
+
for (const agent of manifest.agents) {
|
|
219
|
+
const agentFile = join(packDir, 'agents', agent);
|
|
220
|
+
if (!existsSync(agentFile)) {
|
|
221
|
+
warnings.push(`Agent file not found: agents/${agent}`);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return { valid: errors.length === 0, errors, warnings };
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Load and validate external packs from a community pack directory.
|
|
231
|
+
* Expected structure: packDir/packs/*.yaml
|
|
232
|
+
* @param {string} packDir - Root directory of external pack collection
|
|
233
|
+
* @param {Set<string>} builtinNames - Names of built-in packs
|
|
234
|
+
* @returns {{ loaded: Map<string, object>, errors: string[] }}
|
|
235
|
+
*/
|
|
236
|
+
export function loadExternalPacks(packDir, builtinNames) {
|
|
237
|
+
const loaded = new Map();
|
|
238
|
+
const errors = [];
|
|
239
|
+
|
|
240
|
+
const packsSubdir = join(packDir, 'packs');
|
|
241
|
+
if (!existsSync(packsSubdir)) {
|
|
242
|
+
return { loaded, errors };
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
let files;
|
|
246
|
+
try {
|
|
247
|
+
files = readdirSync(packsSubdir).filter(f => f.endsWith('.yaml'));
|
|
248
|
+
} catch {
|
|
249
|
+
return { loaded, errors };
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
for (const file of files) {
|
|
253
|
+
try {
|
|
254
|
+
const raw = readFileSync(join(packsSubdir, file), 'utf-8');
|
|
255
|
+
const manifest = load(raw);
|
|
256
|
+
|
|
257
|
+
const result = validatePack(manifest, packDir, builtinNames);
|
|
258
|
+
if (!result.valid) {
|
|
259
|
+
for (const err of result.errors) {
|
|
260
|
+
errors.push(`${file}: ${err}`);
|
|
261
|
+
}
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
loaded.set(manifest.name, manifest);
|
|
266
|
+
} catch (err) {
|
|
267
|
+
errors.push(`${file}: Failed to parse — ${err.message}`);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
return { loaded, errors };
|
|
272
|
+
}
|