beaver-build 1.0.6 → 1.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +237 -237
  2. package/dist/index.js +1378 -488
  3. package/package.json +57 -52
package/dist/index.js CHANGED
@@ -44,7 +44,7 @@ var init_constants = __esm({
44
44
  });
45
45
 
46
46
  // src/options/react-vite/constants/index.ts
47
- var REACT_MENU_LAYOUT, REACT_MENU_ROUTER, REACT_MENU_STATE_MANAGEMENT, REACT_MENU_QUERY, REACT_MENU_CSS, REACT_MENU_LINTER;
47
+ var REACT_MENU_LAYOUT, REACT_MENU_ROUTER, REACT_MENU_STATE_MANAGEMENT, REACT_MENU_QUERY, REACT_MENU_CSS, REACT_MENU_LINTER, REACT_MENU_TESTING, REACT_MENU_AI;
48
48
  var init_constants2 = __esm({
49
49
  "src/options/react-vite/constants/index.ts"() {
50
50
  "use strict";
@@ -138,6 +138,40 @@ var init_constants2 = __esm({
138
138
  disabled: false
139
139
  }
140
140
  };
141
+ REACT_MENU_TESTING = {
142
+ notUsing: {
143
+ display: "Not Using",
144
+ value: "NOT_USING",
145
+ description: "No testing setup",
146
+ disabled: false
147
+ },
148
+ vitest: {
149
+ display: "Vitest",
150
+ value: "VITEST",
151
+ description: "Unit + component testing with Vitest v3.2.4 + Testing Library",
152
+ disabled: false
153
+ },
154
+ playwright: {
155
+ display: "Playwright",
156
+ value: "PLAYWRIGHT",
157
+ description: "End-to-end testing with Playwright v1.52.0",
158
+ disabled: false
159
+ }
160
+ };
161
+ REACT_MENU_AI = {
162
+ notUsing: {
163
+ display: "Not Using",
164
+ value: "NOT_USING",
165
+ description: "No AI setup",
166
+ disabled: false
167
+ },
168
+ claude: {
169
+ display: "Claude (Claude Code)",
170
+ value: "CLAUDE",
171
+ description: "CLAUDE.md + .claude/ agents + feature docs structure",
172
+ disabled: false
173
+ }
174
+ };
141
175
  }
142
176
  });
143
177
 
@@ -225,6 +259,16 @@ var init_package_json = __esm({
225
259
  devDeps["globals"] = "15.15.0";
226
260
  devDeps["typescript-eslint"] = "8.26.0";
227
261
  }
262
+ if (cart.testing === "VITEST") {
263
+ devDeps["vitest"] = "3.2.4";
264
+ devDeps["@vitest/coverage-v8"] = "3.2.4";
265
+ devDeps["@testing-library/react"] = "16.3.0";
266
+ devDeps["@testing-library/jest-dom"] = "6.6.3";
267
+ devDeps["jsdom"] = "26.1.0";
268
+ }
269
+ if (cart.testing === "PLAYWRIGHT") {
270
+ devDeps["@playwright/test"] = "1.52.0";
271
+ }
228
272
  const scripts = {
229
273
  dev: "vite",
230
274
  build: "tsc && vite build",
@@ -236,6 +280,18 @@ var init_package_json = __esm({
236
280
  } else if (cart.linter === "ESLINT") {
237
281
  scripts["lint"] = "eslint .";
238
282
  }
283
+ if (cart.testing === "VITEST") {
284
+ scripts["test"] = "vitest";
285
+ scripts["test:run"] = "vitest run";
286
+ scripts["coverage"] = "vitest run --coverage";
287
+ }
288
+ if (cart.testing === "PLAYWRIGHT") {
289
+ scripts["test:e2e"] = "playwright test";
290
+ }
291
+ if (cart.ai === "CLAUDE") {
292
+ scripts["docs:index"] = "node .claude/scripts/build-docs-index.mjs";
293
+ scripts["docs:lint"] = "node .claude/scripts/lint-docs-frontmatter.mjs";
294
+ }
239
295
  return JSON.stringify(
240
296
  {
241
297
  name: cart.projectName,
@@ -714,555 +770,999 @@ declare module '*.css' {
714
770
  }
715
771
  });
716
772
 
717
- // src/scaffold/react-vite/templates/copilot-instructions.ts
718
- var FSD_PATHS, BPR_PATHS, frontmatter, generalTemplate, componentsTemplate, hooksTemplate, routerTemplate, zustandTemplate, queryTemplate, tailwindTemplate, importsTemplate, linterTemplate, getCopilotInstructionFiles;
719
- var init_copilot_instructions = __esm({
720
- "src/scaffold/react-vite/templates/copilot-instructions.ts"() {
773
+ // src/scaffold/shared/claude-setup.ts
774
+ var STATUS_ENUM, LANG_ENUM, docsTemplateMdTemplate, docsReadmeTemplate, docsIndexPlaceholderTemplate, docsSharedMjsTemplate, buildDocsIndexMjsTemplate, lintDocsFrontmatterMjsTemplate, docsFirstReminderShTemplate, claudeSettingsTemplate, docsSkillTemplate, docsWriterAgentTemplate, agentMemorySeedTemplate, buildClaudeFileMap;
775
+ var init_claude_setup = __esm({
776
+ "src/scaffold/shared/claude-setup.ts"() {
721
777
  "use strict";
722
- FSD_PATHS = {
723
- components: "src/**/ui/**/*.tsx,src/shared/ui/**/*.tsx",
724
- hooks: "src/**/model/**/*.ts,src/**/lib/**/*.ts",
725
- stores: "src/shared/lib/store.ts,src/**/model/**/*.ts",
726
- apis: "src/**/api/**/*.ts",
727
- routes: "src/routes/**/*.tsx"
728
- };
729
- BPR_PATHS = {
730
- components: "src/components/**/*.tsx,src/features/*/components/**/*.tsx",
731
- hooks: "src/hooks/**/*.ts,src/features/*/hooks/**/*.ts",
732
- stores: "src/stores/**/*.ts,src/features/*/stores/**/*.ts",
733
- apis: "src/features/*/api/**/*.ts,src/lib/**/*.ts",
734
- routes: "src/routes/**/*.tsx"
735
- };
736
- frontmatter = (applyTo) => `---
737
- applyTo: "${applyTo}"
778
+ STATUS_ENUM = ["active", "draft", "deprecated"];
779
+ LANG_ENUM = ["en", "vi"];
780
+ docsTemplateMdTemplate = (flowEnum3, layerEnum3) => `---
781
+ title: <Concise title; mirror the H1 below>
782
+ feature: _app # feature folder name under docs/features/ | _app (cross-cutting)
783
+ flow: _meta # enum: ${flowEnum3.join(" | ")}
784
+ layer: _cross # enum: ${layerEnum3.join(" | ")}
785
+ status: active # enum: ${STATUS_ENUM.join(" | ")}
786
+ lang: en # enum: ${LANG_ENUM.join(" | ")}
787
+ related: [] # array of doc paths relative to docs/
788
+ keywords: [] # lowercase kebab-case; prefer real symbol/file names from the repo
789
+ updated: YYYY-MM-DD
738
790
  ---
739
791
 
792
+ # <Title>
793
+
794
+ ## Context
795
+ What problem was being solved and why it was non-obvious.
796
+
797
+ ## Root Cause / Key Finding
798
+ The core discovery that unblocked the task.
799
+
800
+ ## Solution / Pattern
801
+ What was implemented and why.
802
+
803
+ ## Key Decisions
804
+ Trade-offs made and alternatives rejected.
805
+
806
+ ## Related Files
807
+ - path/to/relevant/file.ts
740
808
  `;
741
- generalTemplate = (cart) => {
742
- const isFsd = cart.layout === "FSD";
743
- const stackLines = [
744
- "- React 19 + Vite 6 + TypeScript 5",
745
- `- Architecture: ${isFsd ? "Feature-Sliced Design (FSD)" : "Bulletproof React (BPR)"}`,
746
- cart.router === "TANSTACK_ROUTER" ? "- Routing: TanStack Router (file-based)" : "- Routing: none",
747
- cart.stateManagement === "ZUSTAND" ? "- State: Zustand" : "- State: local React state only",
748
- cart.query === "TANSTACK_QUERY" ? "- Server state: TanStack Query" : "- Server state: none",
749
- cart.css === "TAILWIND" ? "- CSS: Tailwind CSS v4.1.3 (Vite plugin, `src/index.css`)" : "- CSS: plain CSS",
750
- cart.linter === "BIOME" ? "- Linter/formatter: Biome (`biome.json`)" : cart.linter === "ESLINT" ? "- Linter: ESLint (`eslint.config.js`)" : "- Linter: none"
751
- ].join("\n");
752
- const layoutBlock = isFsd ? `## Architecture \u2014 Feature-Sliced Design
753
-
754
- Layer order (top \u2192 bottom): \`app \u2192 pages \u2192 widgets \u2192 features \u2192 entities \u2192 shared\`.
755
- A layer may only import from layers **below** it. Never import from \`features\` inside \`shared\`, never import from \`pages\` inside \`widgets\`, and so on.
756
-
757
- Inside every slice, only these segments are allowed \u2014 do not invent new ones:
758
-
759
- - \`ui/\` \u2014 React components for the slice
760
- - \`model/\` \u2014 state, hooks, selectors, slice-local types
761
- - \`api/\` \u2014 network calls, request/response types
762
- - \`lib/\` \u2014 pure helpers specific to the slice
763
- - \`config/\` \u2014 constants, enums
764
-
765
- Where new code belongs:
766
-
767
- - Reusable primitive (Button, Input) \u2192 \`src/shared/ui/<Name>/\`
768
- - Cross-cutting helper \u2192 \`src/shared/lib/\`
769
- - Domain model (User, Post) \u2192 \`src/entities/<name>/\`
770
- - User-facing interaction (LoginForm) \u2192 \`src/features/<name>/\`
771
- - Composition of features \u2192 \`src/widgets/<name>/\`
772
- - Page-level composition \u2192 \`src/pages/<name>/ui/<Name>Page.tsx\` with \`src/pages/<name>/index.ts\` barrel
773
-
774
- Pages **compose** widgets/features \u2014 no business logic inside \`pages/*/ui\`.
775
- ` : `## Architecture \u2014 Bulletproof React
776
-
777
- Global, cross-feature folders directly under \`src/\`:
778
-
779
- - \`components/\` \u2014 app-wide reusable UI primitives. No feature-specific UI.
780
- - \`hooks/\` \u2014 hooks used by more than one feature.
781
- - \`utils/\` \u2014 pure, framework-agnostic helpers.
782
- - \`lib/\` \u2014 configured third-party clients (axios, dayjs).
783
- - \`types/\` \u2014 types shared across features.
784
- - \`config/\` \u2014 env readers, constants, feature flags.
785
- - \`stores/\` \u2014 global stores.
786
- - \`providers/\` \u2014 root-level providers. \`providers/index.tsx\` wraps the tree \u2014 compose new providers there.
787
-
788
- Feature-scoped code lives under \`src/features/<feature>/\` with its own \`components/\`, \`hooks/\`, \`api/\`, \`stores/\`, \`types/\`, \`utils/\`.
789
-
790
- Rule of thumb: if only one feature uses it, it stays inside that feature. Promote to \`src/\` only when a second feature needs it.
809
+ docsReadmeTemplate = () => `# Docs \u2014 Knowledge Base
810
+
811
+ Frontmatter-indexed task docs. \`INDEX.md\` is auto-generated \u2014 never hand-edit it.
812
+
813
+ ## Where to put new docs
814
+
815
+ | Doc type | Directory |
816
+ |---|---|
817
+ | Feature spec / feature-scoped finding | \`docs/features/<feature>/\` |
818
+ | Cross-cutting architecture / patterns | \`docs/architecture/\` |
819
+ | Onboarding material | \`docs/onboarding/\` |
820
+
821
+ ## File naming
822
+
823
+ | Case | Name |
824
+ |---|---|
825
+ | Main spec of a feature | \`<feature>.spec.en.md\` |
826
+ | Spec translation alongside | \`<feature>.spec.vi.md\` |
827
+ | Topic doc (English) | \`<topic>.en.md\` |
828
+ | Translation alongside | \`<topic>.vi.md\` |
829
+
830
+ ## Workflow
831
+
832
+ 1. Copy \`docs/_template.md\`, fill ALL frontmatter fields (the index and lint are built from it).
833
+ 2. Save to the right directory per the table above.
834
+ 3. \`npm run docs:index\` \u2014 regenerates \`INDEX.md\`.
835
+ 4. \`npm run docs:lint\` \u2014 must pass before committing.
836
+ 5. Commit the doc and \`INDEX.md\` together.
791
837
  `;
792
- return `# Copilot Instructions \u2014 ${cart.projectName}
838
+ docsIndexPlaceholderTemplate = () => `# Docs Index
839
+
840
+ > Auto-generated by \`.claude/scripts/build-docs-index.mjs\` \u2014 do not edit by hand.
841
+ > Run \`npm run docs:index\` to regenerate.
842
+ `;
843
+ docsSharedMjsTemplate = (flowEnum3, layerEnum3) => `// Shared helpers for docs tooling \u2014 single source of truth for the frontmatter schema.
844
+ import { readdirSync, readFileSync } from 'fs';
845
+ import { join, relative } from 'path';
846
+
847
+ export const DOCS_DIR = 'docs';
848
+ export const SKIP_FILES = new Set(['INDEX.md', 'README.md', '_template.md']);
849
+
850
+ export const ENUMS = {
851
+ flow: ${JSON.stringify(flowEnum3)},
852
+ layer: ${JSON.stringify(layerEnum3)},
853
+ status: ${JSON.stringify(STATUS_ENUM)},
854
+ lang: ${JSON.stringify(LANG_ENUM)},
855
+ };
793
856
 
794
- These are the project-wide conventions GitHub Copilot must follow. Match the structure below exactly \u2014 do not invent new top-level folders.
857
+ export const REQUIRED_FIELDS = ['title', 'feature', 'flow', 'layer', 'status', 'lang', 'keywords', 'updated'];
858
+
859
+ // Minimal YAML frontmatter parser: scalar values and inline arrays ([a, b]).
860
+ export function parseFrontmatter(content) {
861
+ const match = content.match(/^---\\n([\\s\\S]*?)\\n---/);
862
+ if (!match) return null;
863
+ const meta = {};
864
+ for (const line of match[1].split('\\n')) {
865
+ const idx = line.indexOf(':');
866
+ if (idx === -1) continue;
867
+ const key = line.slice(0, idx).trim();
868
+ let value = line.slice(idx + 1).replace(/\\s+#.*$/, '').trim();
869
+ if (!key) continue;
870
+ if (value.startsWith('[') && value.endsWith(']')) {
871
+ const inner = value.slice(1, -1).trim();
872
+ meta[key] = inner === ''
873
+ ? []
874
+ : inner.split(',').map((item) => item.trim().replace(/^['"]|['"]$/g, ''));
875
+ } else {
876
+ meta[key] = value.replace(/^['"]|['"]$/g, '');
877
+ }
878
+ }
879
+ return meta;
880
+ }
795
881
 
796
- Path-specific rules live in \`.github/instructions/*.instructions.md\` \u2014 those files apply automatically to the files their \`applyTo\` glob matches.
882
+ export function firstHeading(content) {
883
+ const match = content.match(/^#\\s+(.+)$/m);
884
+ return match ? match[1].trim() : null;
885
+ }
797
886
 
798
- ## Behavioral Guidelines
887
+ // Walks docs/**/*.md (excluding SKIP_FILES) and returns parsed records.
888
+ export function walkDocs(dir = DOCS_DIR, records = []) {
889
+ for (const item of readdirSync(dir, { withFileTypes: true })) {
890
+ const fullPath = join(dir, item.name);
891
+ if (item.isDirectory()) {
892
+ walkDocs(fullPath, records);
893
+ } else if (item.name.endsWith('.md') && !SKIP_FILES.has(item.name)) {
894
+ const content = readFileSync(fullPath, 'utf-8');
895
+ records.push({
896
+ path: relative(DOCS_DIR, fullPath),
897
+ fm: parseFrontmatter(content),
898
+ title: firstHeading(content),
899
+ });
900
+ }
901
+ }
902
+ return records;
903
+ }
904
+ `;
905
+ buildDocsIndexMjsTemplate = () => `// Regenerates docs/INDEX.md from frontmatter. Deterministic (sorted) so diffs stay clean.
906
+ // Run via: npm run docs:index
907
+ import { writeFileSync } from 'fs';
908
+ import { join } from 'path';
909
+ import { walkDocs, DOCS_DIR } from './_docs-shared.mjs';
910
+
911
+ const records = walkDocs().sort((a, b) => a.path.localeCompare(b.path));
912
+ const withFm = records.filter((record) => record.fm !== null).length;
913
+ const today = new Date().toISOString().slice(0, 10);
914
+
915
+ function entryLine(record) {
916
+ const fm = record.fm ?? {};
917
+ const title = fm.title ?? record.title ?? record.path;
918
+ const tags = ['feature', 'flow', 'layer']
919
+ .map((key) => '\`' + key + ':' + (fm[key] ?? '_unknown') + '\`')
920
+ .join(' ');
921
+ const keywords = Array.isArray(fm.keywords) && fm.keywords.length > 0
922
+ ? ' \u2014 ' + fm.keywords.join(', ')
923
+ : '';
924
+ return '- [' + title + '](' + record.path + ') \u2014 ' + tags + keywords;
925
+ }
926
+
927
+ function section(heading, key) {
928
+ const groups = new Map();
929
+ for (const record of records) {
930
+ const groupKey = record.fm?.[key] ?? '_unknown';
931
+ if (!groups.has(groupKey)) groups.set(groupKey, []);
932
+ groups.get(groupKey).push(record);
933
+ }
934
+ const lines = ['## ' + heading, ''];
935
+ for (const [name, group] of [...groups.entries()].sort(([a], [b]) => a.localeCompare(b))) {
936
+ lines.push('### ' + name + ' (' + group.length + ')');
937
+ lines.push(...group.map(entryLine));
938
+ lines.push('');
939
+ }
940
+ return lines;
941
+ }
799
942
 
800
- Behavioral guidelines to reduce common LLM coding mistakes. Merge with project-specific instructions as needed.
943
+ function keywordIndex() {
944
+ const map = new Map();
945
+ for (const record of records) {
946
+ for (const keyword of record.fm?.keywords ?? []) {
947
+ if (!map.has(keyword)) map.set(keyword, []);
948
+ map.get(keyword).push(record.path);
949
+ }
950
+ }
951
+ if (map.size === 0) return [];
952
+ return [
953
+ '## Keyword Index',
954
+ ...[...map.entries()]
955
+ .sort(([a], [b]) => a.localeCompare(b))
956
+ .map(([keyword, paths]) =>
957
+ '- \`' + keyword + '\` \u2192 ' + paths.map((path) => '[' + path + '](' + path + ')').join(', ')
958
+ ),
959
+ '',
960
+ ];
961
+ }
801
962
 
802
- **Tradeoff:** These guidelines bias toward caution over speed. For trivial tasks, use judgment.
963
+ const lines = [
964
+ '# Docs Index',
965
+ '',
966
+ '> Auto-generated by \`.claude/scripts/build-docs-index.mjs\` \u2014 do not edit by hand.',
967
+ '> Generated: ' + today + ' from ' + records.length + ' files (' + withFm + ' with frontmatter, ' + (records.length - withFm) + ' without).',
968
+ '',
969
+ ];
803
970
 
804
- ### 1. Think Before Coding
971
+ if (records.length === 0) {
972
+ lines.push('_(No docs yet. Copy docs/_template.md to get started.)_', '');
973
+ } else {
974
+ lines.push(...section('By Feature', 'feature'));
975
+ lines.push(...section('By Flow', 'flow'));
976
+ lines.push(...section('By Layer', 'layer'));
977
+ lines.push(...keywordIndex());
978
+ }
979
+
980
+ writeFileSync(join(DOCS_DIR, 'INDEX.md'), lines.join('\\n').replace(/\\n+$/, '\\n'));
981
+ console.log('docs/INDEX.md updated \u2014 ' + records.length + ' doc(s), ' + withFm + ' with frontmatter.');
982
+ `;
983
+ lintDocsFrontmatterMjsTemplate = () => `// Validates every doc against the frontmatter schema. Exits non-zero on violation \u2192 CI-ready.
984
+ // Run via: npm run docs:lint
985
+ import { existsSync } from 'fs';
986
+ import { join } from 'path';
987
+ import { walkDocs, ENUMS, REQUIRED_FIELDS, DOCS_DIR } from './_docs-shared.mjs';
988
+
989
+ const errors = [];
990
+
991
+ for (const record of walkDocs()) {
992
+ const where = 'docs/' + record.path;
993
+ if (record.fm === null) {
994
+ errors.push(where + ': missing frontmatter block');
995
+ continue;
996
+ }
997
+ for (const field of REQUIRED_FIELDS) {
998
+ if (record.fm[field] === undefined || record.fm[field] === '') {
999
+ errors.push(where + ': missing required field "' + field + '"');
1000
+ }
1001
+ }
1002
+ for (const [field, allowed] of Object.entries(ENUMS)) {
1003
+ const value = record.fm[field];
1004
+ if (value !== undefined && !allowed.includes(value)) {
1005
+ errors.push(where + ': invalid ' + field + ' "' + value + '" (allowed: ' + allowed.join(', ') + ')');
1006
+ }
1007
+ }
1008
+ if (record.fm.updated !== undefined && !/^\\d{4}-\\d{2}-\\d{2}$/.test(record.fm.updated)) {
1009
+ errors.push(where + ': "updated" must be YYYY-MM-DD (got "' + record.fm.updated + '")');
1010
+ }
1011
+ if (Array.isArray(record.fm.keywords)) {
1012
+ for (const keyword of record.fm.keywords) {
1013
+ if (keyword !== keyword.toLowerCase()) {
1014
+ errors.push(where + ': keyword "' + keyword + '" must be lowercase');
1015
+ }
1016
+ }
1017
+ }
1018
+ for (const related of record.fm.related ?? []) {
1019
+ if (!existsSync(join(DOCS_DIR, related))) {
1020
+ errors.push(where + ': related path "' + related + '" does not exist under docs/');
1021
+ }
1022
+ }
1023
+ }
805
1024
 
806
- **Don't assume. Don't hide confusion. Surface tradeoffs.**
1025
+ if (errors.length > 0) {
1026
+ console.error('docs:lint failed with ' + errors.length + ' error(s):');
1027
+ for (const error of errors) console.error(' - ' + error);
1028
+ process.exit(1);
1029
+ }
1030
+ console.log('docs:lint passed.');
1031
+ `;
1032
+ docsFirstReminderShTemplate = (trigger) => `#!/usr/bin/env bash
1033
+ # UserPromptSubmit hook: inject a docs-first reminder when the prompt mentions
1034
+ # a documented feature/flow/domain topic. Silent otherwise to avoid noise.
1035
+ # The harness runs this on every user prompt, so the reminder cannot be skipped.
1036
+ payload="$(cat)"
1037
+
1038
+ # Keep in sync with feature folders under docs/features/ + key domain nouns.
1039
+ # Add an alternation entry whenever a new feature doc is created.
1040
+ trigger='${trigger}'
1041
+
1042
+ if echo "$payload" | grep -iqE "$trigger"; then
1043
+ cat <<'EOF'
1044
+ [docs-first guard] This request appears to touch a documented feature/flow.
1045
+ BEFORE opening source code:
1046
+ 1. Read docs/INDEX.md (grouped by feature / flow / layer + keyword index).
1047
+ 2. Narrow with frontmatter grep, e.g.:
1048
+ grep -rlE '^feature: <feature>' docs/ | xargs grep -lE '^flow: <flow>'
1049
+ 3. Read candidate doc bodies (<=5 files), then follow \`related:\` links.
1050
+ 4. Open source only if docs are insufficient \u2014 and state what the docs covered.
1051
+ EOF
1052
+ fi
1053
+ exit 0
1054
+ `;
1055
+ claudeSettingsTemplate = () => JSON.stringify(
1056
+ {
1057
+ permissions: {
1058
+ deny: [
1059
+ "Bash(git push*)",
1060
+ "Bash(git commit*)",
1061
+ "Bash(git merge*)",
1062
+ "Bash(git rebase*)",
1063
+ "Bash(git tag*)",
1064
+ "Bash(git branch -D*)",
1065
+ "Bash(git branch -d*)",
1066
+ "Bash(git reset --hard*)",
1067
+ "Bash(git clean*)"
1068
+ ],
1069
+ allow: []
1070
+ },
1071
+ hooks: {
1072
+ UserPromptSubmit: [
1073
+ {
1074
+ hooks: [
1075
+ {
1076
+ type: "command",
1077
+ command: 'bash "$CLAUDE_PROJECT_DIR/.claude/scripts/docs-first-reminder.sh"'
1078
+ }
1079
+ ]
1080
+ }
1081
+ ]
1082
+ },
1083
+ sandbox: {
1084
+ filesystem: { denyRead: ["**/.env", "**/.env.*"] }
1085
+ }
1086
+ },
1087
+ null,
1088
+ 2
1089
+ );
1090
+ docsSkillTemplate = (projectName, slug, flowEnum3, layerEnum3) => `---
1091
+ name: ${slug}-docs
1092
+ description: How to find and write knowledge-base docs for ${projectName}. Use when asked "is there a doc about\u2026", "explain the X feature/flow", "document this", "write a spec", or the Vietnamese equivalents ("c\xF3 t\xE0i li\u1EC7u v\u1EC1\u2026", "gi\u1EA3i th\xEDch flow\u2026", "vi\u1EBFt docs/spec\u2026"). Also use before modifying any documented feature.
1093
+ ---
807
1094
 
808
- Before implementing:
809
- - State your assumptions explicitly. If uncertain, ask.
810
- - If multiple interpretations exist, present them - don't pick silently.
811
- - If a simpler approach exists, say so. Push back when warranted.
812
- - If something is unclear, stop. Name what's confusing. Ask.
1095
+ # ${projectName} Docs Guide
813
1096
 
814
- ### 2. Simplicity First
1097
+ ## Finding docs (DOCS-FIRST)
815
1098
 
816
- **Minimum code that solves the problem. Nothing speculative.**
1099
+ 1. Start at \`docs/INDEX.md\` \u2014 grouped By Feature / By Flow / By Layer, plus a keyword reverse-index.
1100
+ 2. Narrow with frontmatter grep (never semantic-search bodies):
1101
+ \`\`\`bash
1102
+ grep -rlE '^feature: home' docs/
1103
+ grep -rlE '^feature: home' docs/ | xargs grep -lE '^flow: ui'
1104
+ \`\`\`
1105
+ 3. Read candidate doc bodies (\u22645 files), then follow their \`related:\` links.
1106
+ 4. Only open source when docs are insufficient \u2014 and state what the docs already covered.
817
1107
 
818
- - No features beyond what was asked.
819
- - No abstractions for single-use code.
820
- - No "flexibility" or "configurability" that wasn't requested.
821
- - No error handling for impossible scenarios.
822
- - If you write 200 lines and it could be 50, rewrite it.
1108
+ ## Writing docs
823
1109
 
824
- Ask yourself: "Would a senior engineer say this is overcomplicated?" If yes, simplify.
1110
+ 1. Copy \`docs/_template.md\`; every field is required (see \`docs/README.md\` for placement + naming).
1111
+ 2. Frontmatter axes: \`feature\` (folder under docs/features/ or \`_app\`), \`flow\` (${flowEnum3.join("/")}), \`layer\` (${layerEnum3.join("/")}).
1112
+ 3. Keywords: lowercase, prefer real symbol/file names an engineer would grep for.
1113
+ 4. Rebuild + validate, then commit doc and INDEX.md together:
1114
+ \`\`\`bash
1115
+ npm run docs:index && npm run docs:lint
1116
+ \`\`\`
1117
+ `;
1118
+ docsWriterAgentTemplate = (projectName, slug) => `---
1119
+ name: docs-writer
1120
+ description: "Documentation agent for ${projectName} \u2014 analyzes requirements from any source (conversation, tickets, Slack) and maintains docs/. <example>user: 'Here are the requirements for the profile page, write them up' \u2192 docs-writer <commentary>synthesizing requirements into a feature spec</commentary></example> <example>user: 'We just finished the auth refactor, document what changed' \u2192 docs-writer <commentary>post-task knowledge capture</commentary></example> <example>user: 'Update the home spec \u2014 the hero section was redesigned' \u2192 docs-writer <commentary>doc maintenance</commentary></example>"
1121
+ model: haiku
1122
+ memory: project
1123
+ ---
825
1124
 
826
- ### 3. Surgical Changes
1125
+ You are the documentation agent for ${projectName}. You own \`docs/\`.
827
1126
 
828
- **Touch only what you must. Clean up only your own mess.**
1127
+ ## Onboarding protocol
829
1128
 
830
- When editing existing code:
831
- - Don't "improve" adjacent code, comments, or formatting.
832
- - Don't refactor things that aren't broken.
833
- - Match existing style, even if you'd do it differently.
834
- - If you notice unrelated dead code, mention it - don't delete it.
1129
+ 1. Read \`.claude/agent-memory/docs-writer/MEMORY.md\`.
1130
+ 2. Read \`docs/README.md\` (placement + naming rules) and \`docs/INDEX.md\` (what already exists).
1131
+ 3. Load the \`${slug}-docs\` skill.
835
1132
 
836
- When your changes create orphans:
837
- - Remove imports/variables/functions that YOUR changes made unused.
838
- - Don't remove pre-existing dead code unless asked.
1133
+ ## Workflow
839
1134
 
840
- The test: Every changed line should trace directly to the user's request.
1135
+ 1. Understand the requirement; if sources conflict, surface the conflict instead of guessing.
1136
+ 2. Check INDEX.md for an existing doc to update before creating a new one.
1137
+ 3. Copy \`docs/_template.md\`; fill ALL frontmatter fields (feature, flow, layer, status, lang, keywords, updated). Specs describe WHAT, not HOW.
1138
+ 4. Save per docs/README.md rules: \`docs/features/<feature>/<feature>.spec.en.md\` for feature specs, \`<topic>.en.md\` for findings, \`docs/architecture/\` for cross-cutting topics.
1139
+ 5. Run \`npm run docs:index\` then \`npm run docs:lint\` \u2014 both must succeed.
1140
+ 6. If the feature introduces new domain nouns, add them to the \`trigger\` list in \`.claude/scripts/docs-first-reminder.sh\`.
1141
+ 7. Report created/updated paths. Append lessons to \`.claude/agent-memory/docs-writer/MEMORY.md\`.
841
1142
 
842
- ### 4. Goal-Driven Execution
1143
+ ## Hard rules
843
1144
 
844
- **Define success criteria. Loop until verified.**
1145
+ - Never hand-edit \`docs/INDEX.md\`.
1146
+ - Never edit application code \u2014 you write markdown and run the docs scripts only.
1147
+ - Never commit or push.
1148
+ `;
1149
+ agentMemorySeedTemplate = (agentName) => `# ${agentName} \u2014 Agent Memory
845
1150
 
846
- Transform tasks into verifiable goals:
847
- - "Add validation" \u2192 "Write tests for invalid inputs, then make them pass"
848
- - "Fix the bug" \u2192 "Write a test that reproduces it, then make it pass"
849
- - "Refactor X" \u2192 "Ensure tests pass before and after"
1151
+ Append durable, non-obvious gotchas and patterns discovered while working \u2014 one bullet each, newest first. Link related docs/ files. Read this file at the start of every session.
850
1152
 
851
- For multi-step tasks, state a brief plan:
1153
+ _(no entries yet)_
1154
+ `;
1155
+ buildClaudeFileMap = (params) => {
1156
+ const { projectName, slug, flowEnum: flowEnum3, layerEnum: layerEnum3 } = params;
1157
+ const files = [
1158
+ { relativePath: "CLAUDE.md", content: params.claudeMd },
1159
+ { relativePath: ".claude/settings.json", content: claudeSettingsTemplate() },
1160
+ { relativePath: ".claude/scripts/_docs-shared.mjs", content: docsSharedMjsTemplate(flowEnum3, layerEnum3) },
1161
+ { relativePath: ".claude/scripts/build-docs-index.mjs", content: buildDocsIndexMjsTemplate() },
1162
+ { relativePath: ".claude/scripts/lint-docs-frontmatter.mjs", content: lintDocsFrontmatterMjsTemplate() },
1163
+ { relativePath: ".claude/scripts/docs-first-reminder.sh", content: docsFirstReminderShTemplate(params.reminderTrigger) },
1164
+ { relativePath: `.claude/skills/${slug}-conventions/SKILL.md`, content: params.conventionsSkill },
1165
+ { relativePath: `.claude/skills/${slug}-docs/SKILL.md`, content: docsSkillTemplate(projectName, slug, flowEnum3, layerEnum3) },
1166
+ { relativePath: ".claude/agents/dev.md", content: params.devAgent },
1167
+ { relativePath: ".claude/agents/docs-writer.md", content: docsWriterAgentTemplate(projectName, slug) },
1168
+ { relativePath: ".claude/agent-memory/dev/MEMORY.md", content: agentMemorySeedTemplate("dev") },
1169
+ { relativePath: ".claude/agent-memory/docs-writer/MEMORY.md", content: agentMemorySeedTemplate("docs-writer") },
1170
+ { relativePath: "docs/README.md", content: docsReadmeTemplate() },
1171
+ { relativePath: "docs/INDEX.md", content: docsIndexPlaceholderTemplate() },
1172
+ { relativePath: "docs/_template.md", content: docsTemplateMdTemplate(flowEnum3, layerEnum3) },
1173
+ ...params.seedDocs
1174
+ ];
1175
+ if (params.testing) {
1176
+ files.push(
1177
+ { relativePath: ".claude/agents/test-writer.md", content: params.testing.testWriterAgent },
1178
+ { relativePath: ".claude/agent-memory/test-writer/MEMORY.md", content: agentMemorySeedTemplate("test-writer") },
1179
+ { relativePath: `.claude/skills/${slug}-test-author/SKILL.md`, content: params.testing.testAuthorSkill }
1180
+ );
1181
+ }
1182
+ return files;
1183
+ };
1184
+ }
1185
+ });
852
1186
 
1187
+ // src/scaffold/react-vite/templates/claude-setup.ts
1188
+ var projectSlug, fsdLayers, bprLayers, layerEnum, flowEnum, reminderTrigger, claudeMdTemplate, docsHomeSpecTemplate, conventionsSkillTemplate, testAuthorSkillTemplate, devAgentTemplate, testWriterAgentTemplate, getClaudeFileMap;
1189
+ var init_claude_setup2 = __esm({
1190
+ "src/scaffold/react-vite/templates/claude-setup.ts"() {
1191
+ "use strict";
1192
+ init_claude_setup();
1193
+ projectSlug = (cart) => cart.projectName.toLowerCase().replace(/_/g, "-");
1194
+ fsdLayers = ["app", "pages", "widgets", "features", "entities", "shared"];
1195
+ bprLayers = ["app", "pages", "components", "features", "hooks", "stores", "lib", "utils"];
1196
+ layerEnum = (cart) => [...cart.layout === "FSD" ? fsdLayers : bprLayers, "_cross"];
1197
+ flowEnum = (cart) => [
1198
+ "ui",
1199
+ "data",
1200
+ ...cart.router === "TANSTACK_ROUTER" ? ["routing"] : [],
1201
+ ...cart.stateManagement === "ZUSTAND" ? ["state"] : [],
1202
+ "infra",
1203
+ "architecture",
1204
+ "onboarding",
1205
+ "_meta"
1206
+ ];
1207
+ reminderTrigger = (cart) => `home|landing${cart.router === "TANSTACK_ROUTER" ? "|route|routing" : ""}${cart.stateManagement === "ZUSTAND" ? "|store|state" : ""}${cart.query === "TANSTACK_QUERY" ? "|query|fetch" : ""}`;
1208
+ claudeMdTemplate = (cart) => {
1209
+ const isFsd = cart.layout === "FSD";
1210
+ const hasRouter = cart.router === "TANSTACK_ROUTER";
1211
+ const hasZustand = cart.stateManagement === "ZUSTAND";
1212
+ const hasQuery = cart.query === "TANSTACK_QUERY";
1213
+ const hasVitest = cart.testing === "VITEST";
1214
+ const hasPlaywright = cart.testing === "PLAYWRIGHT";
1215
+ const hasTesting = cart.testing !== "NOT_USING";
1216
+ const slug = projectSlug(cart);
1217
+ const stack = [
1218
+ "React 19, TypeScript 5.8, Vite 6",
1219
+ hasRouter ? "TanStack Router v1.144.0 (file-based routes in src/routes/)" : null,
1220
+ hasZustand ? "Zustand v5.0.5" : null,
1221
+ hasQuery ? "TanStack Query v5.74.4" : null,
1222
+ cart.css === "TAILWIND" ? "Tailwind CSS v4.1.3" : null,
1223
+ cart.linter === "BIOME" ? "Biome v1.9.4" : cart.linter === "ESLINT" ? "ESLint v9" : null,
1224
+ hasVitest ? "Vitest v3.2.4 + Testing Library" : null,
1225
+ hasPlaywright ? "Playwright v1.52.0" : null
1226
+ ].filter(Boolean);
1227
+ const commands = [
1228
+ "- `npm run dev` \u2014 start dev server (http://localhost:5173)",
1229
+ "- `npm run build` \u2014 type-check + production build",
1230
+ cart.linter !== "NOT_USING" ? "- `npm run lint` \u2014 lint code" : null,
1231
+ hasVitest ? "- `npm run test:run` \u2014 run unit/component tests once (`npm test` for watch)" : null,
1232
+ hasPlaywright ? "- `npm run test:e2e` \u2014 run Playwright E2E tests" : null,
1233
+ "- `npm run docs:index` \u2014 regenerate docs/INDEX.md (run after any doc change)",
1234
+ "- `npm run docs:lint` \u2014 validate docs frontmatter (CI-ready, exits non-zero on violation)"
1235
+ ].filter(Boolean);
1236
+ const fsdArchitecture = `\`\`\`
1237
+ app \u2192 app shell, providers, global setup
1238
+ pages \u2192 route-level pages (one folder per page: ui/ + index.ts barrel)
1239
+ widgets \u2192 composite UI blocks assembled from features/entities
1240
+ features \u2192 user interactions with business value
1241
+ entities \u2192 business domain objects (model, api, ui)
1242
+ shared \u2192 reusable foundation: ui/, lib/, api/, config/
853
1243
  \`\`\`
854
- 1. [Step] \u2192 verify: [check]
855
- 2. [Step] \u2192 verify: [check]
856
- 3. [Step] \u2192 verify: [check]
1244
+
1245
+ **Hard rules:**
1246
+ - Upper layers import lower layers only \u2014 NEVER the reverse (shared must not import pages).
1247
+ - Cross-imports inside one layer are forbidden (a feature must not import another feature).
1248
+ - Every page exposes its public API through \`index.ts\` \u2014 import \`@/pages/home\`, never \`@/pages/home/ui/HomePage\`.`;
1249
+ const bprArchitecture = `\`\`\`
1250
+ pages \u2192 route-level pages
1251
+ components \u2192 shared presentational components
1252
+ features \u2192 feature folders (components + hooks + api per feature)
1253
+ hooks \u2192 shared React hooks
1254
+ stores \u2192 global state${hasZustand ? " (Zustand)" : ""}
1255
+ lib \u2192 third-party wrappers / configured clients
1256
+ utils \u2192 pure utility functions
857
1257
  \`\`\`
858
1258
 
859
- Strong success criteria let you loop independently. Weak criteria ("make it work") require constant clarification.
1259
+ **Hard rules:**
1260
+ - A feature may import from shared folders (components/, hooks/, lib/, utils/) \u2014 shared folders must NEVER import from features/ or pages/.
1261
+ - Cross-feature imports are forbidden; lift shared logic into hooks/ or lib/ instead.
1262
+ - Keep components presentational; data fetching and state live in hooks.`;
1263
+ const keyPatterns = [];
1264
+ if (hasZustand) {
1265
+ keyPatterns.push(`**Zustand \u2014 select narrowly, never the whole store:**
1266
+ \`\`\`ts
1267
+ const count = useAppStore((state) => state.count); // \u2705 re-renders on count only
1268
+ const store = useAppStore(); // \u274C re-renders on every change
1269
+ \`\`\``);
1270
+ }
1271
+ if (hasQuery) {
1272
+ keyPatterns.push(`**TanStack Query \u2014 key factories, not inline keys:**
1273
+ \`\`\`ts
1274
+ const todoKeys = {
1275
+ all: ['todos'] as const,
1276
+ detail: (id: string) => [...todoKeys.all, id] as const,
1277
+ };
1278
+ useQuery({ queryKey: todoKeys.detail(id), queryFn: () => fetchTodo(id) });
1279
+ \`\`\``);
1280
+ }
1281
+ if (hasRouter) {
1282
+ keyPatterns.push(`**TanStack Router \u2014 one file per route in src/routes/; never edit routeTree.gen.ts:**
1283
+ \`\`\`ts
1284
+ export const Route = createFileRoute('/about')({ component: AboutPage });
1285
+ \`\`\``);
1286
+ }
1287
+ if (keyPatterns.length === 0) {
1288
+ keyPatterns.push("_(Add canonical code snippets here as the project establishes its patterns.)_");
1289
+ }
1290
+ const namingRows = isFsd ? `| Pattern | Example | Layer |
1291
+ |---|---|---|
1292
+ | PascalCase component, named export | \`HomePage.tsx\` \u2192 \`export const HomePage\` | pages/widgets/features ui |
1293
+ | \`use\` prefix, camelCase | \`useAuth.ts\` | hooks (in lib/ or feature) |
1294
+ | camelCase barrel | \`index.ts\` re-exporting public API | every slice |
1295
+ | kebab-case folders | \`src/features/add-todo/\` | all layers |` : `| Pattern | Example | Layer |
1296
+ |---|---|---|
1297
+ | PascalCase component, default export for pages | \`Home.tsx\` \u2192 \`export default Home\` | pages |
1298
+ | PascalCase component, named export | \`Button.tsx\` \u2192 \`export const Button\` | components |
1299
+ | \`use\` prefix, camelCase | \`useAuth.ts\` | hooks |${hasZustand ? "\n| camelCase store, `use*Store` | `appStore.ts` \u2192 `useAppStore` | stores |" : ""}`;
1300
+ const testSection = hasTesting ? `## Centralized Test Pattern
1301
+
1302
+ All test code lives in the top-level \`test/\` folder \u2014 never inside \`src/\`:
860
1303
 
861
- **These guidelines are working if:** fewer unnecessary changes in diffs, fewer rewrites due to overcomplication, and clarifying questions come before implementation rather than after mistakes.
1304
+ \`\`\`
1305
+ test/
1306
+ \u251C\u2500\u2500 README.md # conventions + how to run
1307
+ ${hasVitest ? "\u251C\u2500\u2500 unit/ # mirrors src/ layer structure; *.test.tsx\n" : ""}${hasPlaywright ? "\u251C\u2500\u2500 e2e/ # mirrors business flows; *.spec.ts\n" : ""}\u251C\u2500\u2500 _shared/ # fixtures/, helpers/, mocks/${hasPlaywright ? ", pages/ (Page Objects)" : ""}
1308
+ \u2514\u2500\u2500 specs/ # paired human-readable *.spec.md contracts \u2014 documentation, never executed
1309
+ \`\`\`
862
1310
 
863
- ## Stack
864
- ${stackLines}
1311
+ Every test file has a paired \`test/specs/.../<name>.spec.md\` describing WHAT it covers. See \`test/README.md\`.
865
1312
 
866
- ## Import aliases
1313
+ ` : "";
1314
+ const testWriterRow = hasTesting ? "\n| Writing or updating tests | `test-writer` | writes only under `test/`; requires a feature spec first |" : "";
1315
+ return `# ${cart.projectName}
867
1316
 
868
- Always use path aliases instead of relative imports. Project-wide aliases are pre-configured in \`tsconfig.json\` and \`vite.config.ts\`:
1317
+ ## Behavioral Guidelines
869
1318
 
870
- - \`@/*\` \u2192 \`./src/\` \u2014 use for any import from src, e.g. \`import Button from '@/components/Button'\`.
871
- - \`@components/*\` \u2192 \`./src/components/\` \u2014 e.g. \`import { Modal } from '@components/Modal'\`.
872
- - \`@pages/*\` \u2192 \`./src/pages/\` \u2014 e.g. \`import { HomePage } from '@pages/Home'\`.
873
- - \`@utils/*\` \u2192 \`./src/utils/\` \u2014 e.g. \`import { cn } from '@utils/cn'\`.
874
- - \`@types/*\` \u2192 \`./src/types/\` \u2014 e.g. \`import type { User } from '@types/user'\`.
875
- - \`@hooks/*\` \u2192 \`./src/hooks/\` \u2014 e.g. \`import { useAuth } from '@hooks/useAuth'\`.
876
- - \`@layouts/*\` \u2192 \`./src/layouts/\` \u2014 e.g. \`import { MainLayout } from '@layouts/MainLayout'\`.
877
- - \`@assets/*\` \u2192 \`./src/assets/\` \u2014 e.g. \`import logo from '@assets/logo.svg'\`.
1319
+ **Think Before Coding** \u2014 state assumptions explicitly; if multiple interpretations exist, present them; if something is unclear, stop and ask.
1320
+ **Simplicity First** \u2014 minimum code that solves the problem; no speculative features, abstractions, or configurability.
1321
+ **Surgical Changes** \u2014 touch only what the request requires; match existing style; every changed line traces to the request.
1322
+ **Goal-Driven Execution** \u2014 turn tasks into verifiable success criteria before starting; loop until verified.
878
1323
 
879
- **Never use relative imports** like \`import Foo from '../../../components/Foo'\`. Always resolve through an alias.
1324
+ ## Project Overview
880
1325
 
881
- ## Naming conventions
1326
+ ${stack.map((s) => `- ${s}`).join("\n")}
882
1327
 
883
- - **Components**: \`PascalCase.tsx\`, one component per file, named export (no default).
884
- - **Hooks**: \`useXxx.ts\`, camelCase, named export, must start with \`use\`.
885
- - **Stores** (Zustand): camelCase ending in \`Store.ts\` (e.g. \`authStore.ts\`). Export a typed hook, never the raw store.
886
- - **Types / interfaces**: PascalCase, in \`*.types.ts\` or inline when used in one file only.
887
- - **Constants**: \`UPPER_SNAKE_CASE\`, grouped in \`constants.ts\` per slice/feature.
888
- - **Utility functions**: camelCase, one concern per file, named exports.
1328
+ Architecture: **${isFsd ? "Feature Slice Design (FSD)" : "Bulletproof React (BPR)"}**. Reference implementation: \`src/pages/home${isFsd ? "/" : ".tsx"}\` \u2014 consult it before generating new pages. Verify actual directory names with \`ls\` before writing paths.
889
1329
 
890
- ${layoutBlock}
891
- ## When unsure
1330
+ ## Commands
892
1331
 
893
- Open the closest existing file of the same kind and match its location, casing, and export style.
894
- `;
895
- };
896
- componentsTemplate = (cart) => {
897
- const paths = cart.layout === "FSD" ? FSD_PATHS : BPR_PATHS;
898
- const placement = cart.layout === "FSD" ? `- Reusable primitives (Button, Input, Modal) \u2192 \`src/shared/ui/<Name>/<Name>.tsx\`.
899
- - Slice-specific components \u2192 \`src/<layer>/<slice>/ui/<Name>.tsx\`.
900
- - Never put a component directly under \`src/pages/<name>/\` \u2014 use \`src/pages/<name>/ui/\`.` : `- App-wide reusable UI \u2192 \`src/components/<Name>/<Name>.tsx\`.
901
- - Feature-specific UI \u2192 \`src/features/<feature>/components/<Name>.tsx\`.
902
- - Do **not** put feature-specific components under \`src/components\`.`;
903
- return frontmatter(paths.components) + `# React components
904
-
905
- - File name matches the component name exactly: \`UserCard.tsx\` exports \`UserCard\`.
906
- - One component per file. Named export only \u2014 never \`export default\`.
907
- - Functional components with TypeScript props interface named \`<Name>Props\`, defined in the same file unless reused.
908
- - Keep JSX return focused. Extract sub-trees into smaller components when the return exceeds ~60 lines.
909
- - Hooks run at the top of the component body, before any conditional return.
910
- - No business logic in components \u2014 delegate to hooks, stores, or query functions.
911
- - Co-locate component-scoped styles, stories, and tests next to the component file.
912
-
913
- ## Where to place a new component
914
-
915
- ${placement}
916
- `;
917
- };
918
- hooksTemplate = (cart) => {
919
- const paths = cart.layout === "FSD" ? FSD_PATHS : BPR_PATHS;
920
- const placement = cart.layout === "FSD" ? `- Slice-specific hook \u2192 \`src/<layer>/<slice>/model/useXxx.ts\` (stateful) or \`src/<layer>/<slice>/lib/useXxx.ts\` (pure helper).
921
- - Reusable across slices \u2192 \`src/shared/lib/useXxx.ts\`.` : `- Cross-feature hook \u2192 \`src/hooks/useXxx.ts\`.
922
- - Feature-specific hook \u2192 \`src/features/<feature>/hooks/useXxx.ts\`.`;
923
- return frontmatter(paths.hooks) + `# Custom hooks
1332
+ ${commands.join("\n")}
924
1333
 
925
- - File name starts with \`use\` and matches the exported hook: \`useDebouncedValue.ts\` exports \`useDebouncedValue\`.
926
- - One hook per file. Named export only.
927
- - Must call other hooks \u2014 if a function does not use any hook, it is a utility, not a hook.
928
- - Return either a tuple \`[value, setter]\` or an object with named fields. Do not mix patterns within one hook.
929
- - Keep hooks focused on one concern. Compose multiple hooks rather than building a single large one.
930
- - Do not call hooks conditionally. Do not rename destructured return values unless renaming makes usage clearer.
1334
+ ## Architecture Layers
931
1335
 
932
- ## Where to place a new hook
1336
+ ${isFsd ? fsdArchitecture : bprArchitecture}
933
1337
 
934
- ${placement}
935
- `;
936
- };
937
- routerTemplate = (cart) => {
938
- const paths = cart.layout === "FSD" ? FSD_PATHS : BPR_PATHS;
939
- return frontmatter(paths.routes) + `# TanStack Router (file-based)
940
-
941
- - Every file in \`src/routes/\` is a route. Do not create unrelated helper files here.
942
- - Root layout lives in \`src/routes/__root.tsx\` \u2014 wrap \`<Outlet />\` with app-wide chrome (header, devtools, providers that need router context).
943
- - Leaf routes use \`createFileRoute('/path')\` and export \`Route\`. File name matches the URL segment.
944
- - Dynamic segments use \`$param\` file names: \`src/routes/posts/$postId.tsx\`.
945
- - Co-locate the route's \`loader\`, \`beforeLoad\`, and \`component\` in the same file. Push heavy logic into hooks / query functions imported from the feature folder.
946
- - **Never edit \`src/routes/routeTree.gen.ts\`** \u2014 it is regenerated on dev/build.
947
- - Internal navigation uses \`<Link to="..." />\` from \`@tanstack/react-router\`. Do not use \`<a href>\` for in-app links.
948
- - Type-safe search params: declare them in the route's \`validateSearch\` and read via \`Route.useSearch()\`.
949
- `;
950
- };
951
- zustandTemplate = (cart) => {
952
- const paths = cart.layout === "FSD" ? FSD_PATHS : BPR_PATHS;
953
- const initialPath = cart.layout === "FSD" ? "src/shared/lib/store.ts" : "src/stores/appStore.ts";
954
- const newStorePath = cart.layout === "FSD" ? "- New global store \u2192 `src/shared/lib/<name>Store.ts`.\n- Entity-owned store \u2192 `src/entities/<name>/model/store.ts`." : "- New global store \u2192 `src/stores/<name>Store.ts`.\n- Feature-scoped store \u2192 `src/features/<feature>/stores/<name>Store.ts`.";
955
- return frontmatter(paths.stores) + `# Zustand stores
1338
+ ## Key Patterns
956
1339
 
957
- - Initial store lives at \`${initialPath}\`.
958
- - One slice per concern \u2014 never pile unrelated state into a single store.
959
- - Export a **typed hook** (e.g. \`useAuthStore\`). Never export the raw store object or the \`create\` result directly.
960
- - Use selectors at call sites to read only what you need: \`const user = useAuthStore(s => s.user)\`. This avoids re-rendering on unrelated state changes.
961
- - Derived values belong in selectors, not inside the store.
962
- - Async actions go inside the store definition. They read/write state via \`set\` / \`get\`.
963
- - Shape a store as \`{ ...state, ...actions }\`. Actions are methods, not separate exports.
964
- - For cross-slice reads, use individual selectors \u2014 do not merge stores.
1340
+ ${keyPatterns.join("\n\n")}
965
1341
 
966
- ## Where to place a new store
1342
+ ## Naming Conventions
967
1343
 
968
- ${newStorePath}
969
- `;
970
- };
971
- queryTemplate = (cart) => {
972
- const paths = cart.layout === "FSD" ? FSD_PATHS : BPR_PATHS;
973
- const keysPath = cart.layout === "FSD" ? `- Query keys, query functions, and hooks live in \`src/<layer>/<slice>/api/\`:
974
- - \`keys.ts\` \u2014 exported query key factories
975
- - \`queries.ts\` \u2014 \`useXxxQuery\` / \`useXxxMutation\` hooks wrapping \`useQuery\` / \`useMutation\`` : `- Query keys, query functions, and hooks live in \`src/features/<feature>/api/\`:
976
- - \`keys.ts\` \u2014 exported query key factories
977
- - \`queries.ts\` \u2014 \`useXxxQuery\` / \`useXxxMutation\` hooks wrapping \`useQuery\` / \`useMutation\``;
978
- return frontmatter(paths.apis) + `# TanStack Query
1344
+ ${namingRows}
979
1345
 
980
- - A single \`QueryClient\` is already instantiated in the root provider. Do **not** create additional clients.
981
- - Do not call \`useQuery\` / \`useMutation\` directly in leaf components unless the component is a page-level container. Wrap them in a hook inside the feature/slice.
1346
+ ## Anti-Patterns
982
1347
 
983
- ## Query keys
1348
+ | Anti-pattern | Correct approach |
1349
+ |---|---|
1350
+ ${isFsd ? "| Importing a slice's internals (`@/pages/home/ui/HomePage`) | Import the barrel: `@/pages/home` |\n| Business logic in `shared/` | `shared/` is generic-only; domain logic goes in `entities/` or `features/` |" : "| Cross-feature imports (`features/a` \u2192 `features/b`) | Lift shared code into `hooks/` or `lib/` |\n| Fetching data inside presentational components | Move fetching into a hook; pass data via props |"}
1351
+ | \`useEffect\` for derived state | Compute during render or use \`useMemo\` |
1352
+ | Hand-editing generated files${hasRouter ? " (`routeTree.gen.ts`)" : ""} | Regenerate via the owning tool |
984
1353
 
985
- - Use a **query key factory** per feature/slice \u2014 do not scatter string literals.
986
- - Factory shape: \`export const postKeys = { all: ['posts'] as const, list: (f) => [...postKeys.all, 'list', f] as const, detail: (id) => [...postKeys.all, 'detail', id] as const };\`
987
- - Invalidate via the factory: \`queryClient.invalidateQueries({ queryKey: postKeys.all })\`.
1354
+ > Fill this table from real review feedback over time \u2014 it is the highest-leverage section for code quality.
988
1355
 
989
- ## Query functions
1356
+ ${testSection}## Agent Routing
990
1357
 
991
- - Query functions return parsed, typed data \u2014 no raw \`Response\`. Throw on non-2xx.
992
- - Mutations invalidate relevant keys in \`onSuccess\`; never manually refetch by calling queries.
993
- - Prefer \`select\` for view-model transforms so the cached data stays normalized.
1358
+ | Task / trigger | Agent | Notes |
1359
+ |---|---|---|
1360
+ | Feature work or bug fix in \`src/\` | \`dev\` | MUST read relevant \`docs/features/\` spec before coding |
1361
+ | Analyzing requirements, writing/updating feature docs | \`docs-writer\` | owns \`docs/\`; rebuilds INDEX.md after every change |${testWriterRow}
994
1362
 
995
- ## Where to place queries
1363
+ Each agent has persistent memory at \`.claude/agent-memory/<agent>/MEMORY.md\` \u2014 agents read it on start and append new gotchas. Do NOT use the general assistant for work an agent owns \u2014 always delegate.
996
1364
 
997
- ${keysPath}
998
- `;
999
- };
1000
- tailwindTemplate = (cart) => {
1001
- const paths = cart.layout === "FSD" ? FSD_PATHS : BPR_PATHS;
1002
- return frontmatter(paths.components) + `# Tailwind CSS v4
1365
+ ## Task Documentation Convention
1003
1366
 
1004
- > **This project uses Tailwind CSS v4.** v4 removes \`tailwind.config.js\` entirely.
1005
- > All theme customization is done inside \`src/index.css\` using the \`@theme\` directive.
1006
- > Do NOT create or reference \`tailwind.config.js\`, \`tailwind.config.ts\`, or \`postcss.config.*\`.
1367
+ After any non-trivial fix or new pattern: copy \`docs/_template.md\`, fill the frontmatter, save as \`docs/features/<feature>/<topic>.en.md\` (or \`docs/architecture/\` for cross-cutting topics), then run \`npm run docs:index\` and commit the doc together with \`INDEX.md\`. Validate with \`npm run docs:lint\`.
1007
1368
 
1008
- ## Entry point
1369
+ ## Further Reading + DOCS-FIRST RULE
1009
1370
 
1010
- \`src/index.css\` is the single Tailwind entry point \u2014 the only file that contains \`@import "tailwindcss"\`.
1011
- Import it once in \`src/main.tsx\`. Do not add additional Tailwind imports elsewhere.
1371
+ Skills: \`.claude/skills/${slug}-conventions\` (architecture depth), \`.claude/skills/${slug}-docs\` (how to query the knowledge base)${hasTesting ? `, \`.claude/skills/${slug}-test-author\` (test conventions)` : ""}.
1012
1372
 
1013
- ## Using utilities
1373
+ **DOCS-FIRST RULE:** for any request to describe, explain, or modify a documented feature, you MUST grep \`docs/\` frontmatter and read the relevant docs BEFORE opening source files \u2014 and state what the docs already covered. Start at \`docs/INDEX.md\`.
1014
1374
 
1015
- - Use Tailwind utility classes directly in JSX \`className\`. Do not write custom CSS for layout or spacing that a utility already covers.
1016
- - Prefer composing utilities over \`@apply\`. Only use \`@apply\` inside \`@layer components\` or \`@layer base\` in \`src/index.css\`, never in component files.
1017
- - Do not use inline \`style={{}}\` for values that a Tailwind utility can express.
1018
- - Responsive variants go smallest \u2192 largest: \`sm:\`, \`md:\`, \`lg:\`, \`xl:\`, \`2xl:\`.
1019
- - Dark mode: use the \`dark:\` variant. Do not add a separate CSS file or a manual class toggle.
1375
+ **Operating loop:** finish a non-trivial task \u2192 write a doc from \`docs/_template.md\` \u2192 rebuild \`INDEX.md\` \u2192 commit together; agents update their \`MEMORY.md\` when they learn a gotcha.
1376
+ `;
1377
+ };
1378
+ docsHomeSpecTemplate = (cart) => {
1379
+ const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
1380
+ return `---
1381
+ title: Home Page \u2014 Feature Spec
1382
+ feature: home
1383
+ flow: ui
1384
+ layer: pages
1385
+ status: draft
1386
+ lang: en
1387
+ related: []
1388
+ keywords: [home, landing]
1389
+ updated: ${today}
1390
+ ---
1020
1391
 
1021
- ## Theme customization \u2014 colors, spacing, and CSS variables
1392
+ # Home Page \u2014 Feature Spec
1022
1393
 
1023
- All design tokens live in \`src/index.css\` under the \`@theme\` block, **not** in a config file.
1394
+ ## Context
1395
+ Starter page generated by the scaffold. Replace this spec with the real requirements for ${cart.projectName}'s home page.
1024
1396
 
1025
- ### Adding custom colors
1397
+ ## Root Cause / Key Finding
1398
+ _(For feature specs, use this section for key constraints or discoveries.)_
1026
1399
 
1027
- \`\`\`css
1028
- @import "tailwindcss";
1400
+ ## Solution / Pattern
1401
+ - Requirements: [list what the page must do]
1402
+ - UI/UX: [screens and interactions]
1403
+ - Data: [data structures, API requirements]
1404
+ - Edge cases: [what must not break]
1029
1405
 
1030
- @theme {
1031
- --color-brand: oklch(0.55 0.22 265);
1032
- --color-brand-light: oklch(0.72 0.14 265);
1033
- --color-brand-dark: oklch(0.38 0.22 265);
1034
- }
1035
- \`\`\`
1406
+ ## Key Decisions
1407
+ _(Trade-offs made and alternatives rejected.)_
1036
1408
 
1037
- This generates \`bg-brand\`, \`text-brand\`, \`border-brand\`, \`fill-brand\`, etc. automatically.
1409
+ ## Related Files
1410
+ - src/pages/${cart.layout === "FSD" ? "home/ui/HomePage.tsx" : "Home.tsx"}
1411
+ `;
1412
+ };
1413
+ conventionsSkillTemplate = (cart) => {
1414
+ const isFsd = cart.layout === "FSD";
1415
+ const slug = projectSlug(cart);
1416
+ return `---
1417
+ name: ${slug}-conventions
1418
+ description: Coding conventions and architecture rules for ${cart.projectName}. Use when writing or reviewing ANY code under src/ \u2014 components, pages, hooks${cart.stateManagement === "ZUSTAND" ? ", stores" : ""}${cart.router === "TANSTACK_ROUTER" ? ", routes" : ""}, or styling. Triggers on tasks mentioning components, pages, layout, naming, imports, or project structure.
1419
+ ---
1038
1420
 
1039
- ### Namespace \u2192 utility mapping (do not invent names outside these namespaces)
1421
+ # ${cart.projectName} Conventions
1040
1422
 
1041
- | CSS variable | Generated utilities |
1042
- |---|---|
1043
- | \`--color-*\` | \`bg-*\`, \`text-*\`, \`border-*\`, \`fill-*\`, \`stroke-*\` |
1044
- | \`--font-*\` | \`font-*\` (font family) |
1045
- | \`--text-*\` | \`text-*\` (font size) |
1046
- | \`--font-weight-*\` | \`font-*\` (weight) |
1047
- | \`--spacing-*\` | \`p-*\`, \`m-*\`, \`w-*\`, \`h-*\`, \`gap-*\`, etc. |
1048
- | \`--radius-*\` | \`rounded-*\` |
1049
- | \`--shadow-*\` | \`shadow-*\` |
1050
- | \`--breakpoint-*\` | responsive variants (\`sm:\`, \`md:\`, \u2026) |
1051
- | \`--animate-*\` | \`animate-*\` |
1052
-
1053
- ### Extending vs. replacing defaults
1054
-
1055
- **Extend** (keep defaults, add yours):
1056
- \`\`\`css
1057
- @theme {
1058
- --color-tahiti: #3ab7bf; /* new color, defaults kept */
1059
- }
1060
- \`\`\`
1423
+ In-depth companion to CLAUDE.md. CLAUDE.md states the rules; this skill explains how to apply them.
1061
1424
 
1062
- **Replace an entire namespace** (remove all defaults in that group):
1063
- \`\`\`css
1064
- @theme {
1065
- --color-*: initial; /* wipe default palette */
1066
- --color-white: #fff;
1067
- --color-primary: oklch(0.55 0.22 265);
1068
- }
1069
- \`\`\`
1425
+ ## Layer boundaries (${isFsd ? "FSD" : "BPR"})
1070
1426
 
1071
- **Replace everything** (no defaults at all):
1072
- \`\`\`css
1073
- @theme {
1074
- --*: initial;
1075
- --spacing: 4px;
1076
- --color-primary: oklch(0.55 0.22 265);
1077
- }
1078
- \`\`\`
1427
+ ${isFsd ? `Import direction is one-way: \`app \u2192 pages \u2192 widgets \u2192 features \u2192 entities \u2192 shared\`.
1428
+ - Adding a page: create \`src/pages/<name>/ui/<Name>Page.tsx\` + \`src/pages/<name>/index.ts\` barrel.
1429
+ - Adding a feature: \`src/features/<name>/\` with its own ui/, model/, api/ as needed.
1430
+ - Anything used by 2+ slices and free of domain logic belongs in \`shared/\`.
1431
+ - NEVER deep-import another slice's internals; only its \`index.ts\` barrel.` : `- Adding a page: \`src/pages/<Name>.tsx\`, default export.
1432
+ - Adding a feature: \`src/features/<name>/\` holding that feature's components, hooks, and api together.
1433
+ - Shared folders (components/, hooks/, lib/, utils/) must stay feature-agnostic \u2014 no imports from features/ or pages/.
1434
+ - Cross-feature reuse: lift into hooks/ or lib/, never import feature \u2192 feature.`}
1079
1435
 
1080
- ### Variables that reference other variables
1436
+ ## Import alias
1081
1437
 
1082
- Use \`@theme inline\` when a token references another CSS variable:
1083
- \`\`\`css
1084
- @theme inline {
1085
- --font-sans: var(--font-inter); /* resolves the value, not the reference */
1086
- }
1087
- \`\`\`
1438
+ Use \`@/\` for everything under src/ (e.g. \`import { HomePage } from '@/pages/home'\`). Never use long relative chains (\`../../..\`).
1088
1439
 
1089
- ### Regular CSS variables vs. theme tokens
1440
+ ${cart.stateManagement === "ZUSTAND" ? `## Zustand
1090
1441
 
1091
- - \`@theme { --color-brand: \u2026 }\` \u2192 creates utility classes (\`bg-brand\`, etc.)
1092
- - \`:root { --brand-opacity: 0.9; }\` \u2192 plain CSS variable, no utility generated
1442
+ - One store per domain; name \`use<Domain>Store\`.
1443
+ - Components select narrow slices: \`useAppStore((s) => s.count)\`.
1444
+ - Async actions live inside the store, setting loading/error state themselves.
1093
1445
 
1094
- Use \`:root\` for runtime values (e.g., animation targets, JS-readable tokens) that must **not** produce utility classes.
1446
+ ` : ""}${cart.query === "TANSTACK_QUERY" ? `## TanStack Query
1095
1447
 
1096
- ### Defining custom animations
1448
+ - Define a key factory per entity next to its query functions.
1449
+ - Mutations invalidate via the same factory: \`queryClient.invalidateQueries({ queryKey: todoKeys.all })\`.
1450
+ - Server state belongs in Query \u2014 do not mirror it into ${cart.stateManagement === "ZUSTAND" ? "Zustand" : "local state"}.
1097
1451
 
1098
- \`\`\`css
1099
- @theme {
1100
- --animate-slide-in: slide-in 0.3s ease-out;
1452
+ ` : ""}${cart.router === "TANSTACK_ROUTER" ? `## TanStack Router
1101
1453
 
1102
- @keyframes slide-in {
1103
- from { transform: translateY(-8px); opacity: 0; }
1104
- to { transform: translateY(0); opacity: 1; }
1105
- }
1106
- }
1107
- \`\`\`
1454
+ - One file per route under \`src/routes/\`; the Vite plugin regenerates \`routeTree.gen.ts\` \u2014 never edit it.
1455
+ - Route files export \`Route = createFileRoute('<path>')({ component })\`; keep components in their layer and import them.
1108
1456
 
1109
- ### Color format \u2014 prefer oklch
1457
+ ` : ""}${cart.css === "TAILWIND" ? `## Tailwind CSS v4
1110
1458
 
1111
- Tailwind v4's built-in palette uses oklch. Use oklch for custom colors too so tokens stay perceptually uniform and mix cleanly with the defaults:
1459
+ - Theme tokens live in \`src/index.css\` under \`@theme\` \u2014 extend there, not in a JS config.
1460
+ - Prefer semantic class composition in components over @apply.
1112
1461
 
1113
- \`\`\`css
1114
- @theme {
1115
- --color-primary: oklch(0.55 0.22 265); /* L C H */
1116
- --color-muted: oklch(0.62 0.01 265);
1117
- }
1118
- \`\`\`
1462
+ ` : ""}## When unsure
1119
1463
 
1120
- Avoid hex or \`rgb()\` unless matching a fixed brand value.
1464
+ Check the reference implementation (\`src/pages/home${isFsd ? "/" : ".tsx"}\`) first, then the docs (\`docs/INDEX.md\`), then ask.
1465
+ `;
1466
+ };
1467
+ testAuthorSkillTemplate = (cart) => {
1468
+ const slug = projectSlug(cart);
1469
+ const hasVitest = cart.testing === "VITEST";
1470
+ return `---
1471
+ name: ${slug}-test-author
1472
+ description: Centralized test conventions for ${cart.projectName}. Use when asked to "write a test", "add a spec", "cover this with tests", or fix a failing test. All test code lives under test/ \u2014 never inside src/.
1473
+ ---
1121
1474
 
1122
- ### Theme variables are also CSS custom properties
1475
+ # ${cart.projectName} Test Conventions
1123
1476
 
1124
- Every \`@theme\` variable is a real CSS custom property \u2014 read it anywhere:
1477
+ ## Layout
1125
1478
 
1126
- \`\`\`css
1127
- .my-element {
1128
- color: var(--color-primary); /* same value as the text-primary utility */
1129
- }
1479
+ \`\`\`
1480
+ test/
1481
+ ${hasVitest ? "\u251C\u2500\u2500 unit/ # mirrors src/ layers; <name>.test.tsx\n" : "\u251C\u2500\u2500 e2e/ # mirrors business flows; <flow>.spec.ts\n"}\u251C\u2500\u2500 _shared/ # fixtures/, helpers/, mocks/${hasVitest ? "" : ", pages/ (Page Object Models)"}
1482
+ \u2514\u2500\u2500 specs/ # paired *.spec.md contracts \u2014 documentation, never executed
1130
1483
  \`\`\`
1131
1484
 
1132
- Use \`var()\` for CSS that Tailwind utilities cannot express (e.g. complex \`box-shadow\`, SVG \`fill\`, pseudo-element content).
1485
+ ## Rules
1133
1486
 
1134
- ### Semantic token pattern \u2014 naming convention
1487
+ - Tests go under \`test/\` ONLY \u2014 never create __tests__ folders or *.test.* files inside src/.
1488
+ - Every test file gets a paired contract: \`test/specs/<same-path>/<name>.spec.md\` listing what each case covers and which feature spec (docs/features/) it traces to.
1489
+ ${hasVitest ? `- Unit tests import via the \`@/\` alias and explicit vitest imports (\`import { describe, it, expect } from 'vitest'\`).
1490
+ - Component tests use Testing Library; query by role/label, never by class name.
1491
+ - Run: \`npm run test:run\` (CI) / \`npm test\` (watch).` : `- E2E tests use Page Object Models from \`test/_shared/pages/\`; specs stay thin.
1492
+ - Run: \`npm run test:e2e\` (starts the dev server automatically).`}
1493
+ - Test behavior, not implementation; cover the edge cases listed in the feature spec.
1494
+ `;
1495
+ };
1496
+ devAgentTemplate = (cart) => {
1497
+ const slug = projectSlug(cart);
1498
+ const hasTesting = cart.testing !== "NOT_USING";
1499
+ const testWriterExample = hasTesting ? " <example>user: 'Cover the login form with tests' \u2192 test-writer, NOT dev <commentary>test authoring belongs to test-writer</commentary></example>" : "";
1500
+ const delegationRule = hasTesting ? "- Never write tests (delegate to test-writer) or docs (delegate to docs-writer); flag when they are needed." : "- Never write docs (delegate to docs-writer); flag when they are needed.";
1501
+ return `---
1502
+ name: dev
1503
+ description: "Implementation agent for ${cart.projectName} \u2014 all feature work and bug fixes under src/. <example>user: 'Add a dark-mode toggle to the header' \u2192 dev <commentary>feature work in src/; dev reads the feature spec first, then implements</commentary></example> <example>user: 'The home page crashes on empty data \u2014 fix it' \u2192 dev <commentary>bug fix inside a documented feature</commentary></example> <example>user: 'Write a spec for the checkout flow' \u2192 docs-writer, NOT dev <commentary>doc authoring belongs to docs-writer</commentary></example>${testWriterExample}"
1504
+ model: sonnet
1505
+ memory: project
1506
+ ---
1135
1507
 
1136
- Prefer **semantic names** (\`surface\`, \`text-muted\`, \`border\`) over raw scale names (\`slate-900\`, \`zinc-300\`). Semantic names decouple the design from the specific value and make theming easy:
1508
+ You are the implementation agent for ${cart.projectName}.
1137
1509
 
1138
- \`\`\`css
1139
- @theme {
1140
- /* Brand */
1141
- --color-primary: oklch(0.55 0.22 265);
1142
- --color-primary-fg: oklch(0.97 0.01 265);
1510
+ ## Onboarding protocol (in order, before any code)
1143
1511
 
1144
- /* Surfaces (dark-first) */
1145
- --color-surface: oklch(0.11 0.015 265);
1146
- --color-surface-muted: oklch(0.16 0.012 265);
1147
- --color-surface-high: oklch(0.21 0.010 265);
1512
+ 1. Read \`.claude/agent-memory/dev/MEMORY.md\` \u2014 your accumulated gotchas.
1513
+ 2. Read \`docs/INDEX.md\` and the relevant \`docs/features/<feature>/\` spec for the task.
1514
+ 3. Load the \`${slug}-conventions\` skill for layer rules and patterns.
1515
+ 4. Read the code under change.
1148
1516
 
1149
- /* Text */
1150
- --color-text: oklch(0.97 0.000 0);
1151
- --color-text-muted: oklch(0.62 0.010 265);
1152
- --color-text-subtle: oklch(0.42 0.010 265);
1517
+ If no relevant feature spec exists, STOP and tell the user to run the docs-writer agent first.
1153
1518
 
1154
- /* Border */
1155
- --color-border: oklch(0.28 0.010 265);
1156
- }
1157
- \`\`\`
1519
+ ## Workflow
1520
+
1521
+ 1. State assumptions and success criteria.
1522
+ 2. Implement the minimum change that satisfies the spec; match existing style.
1523
+ 3. Verify (build${hasTesting ? " + run the relevant tests" : ""}); report results faithfully.
1524
+ 4. Append newly discovered gotchas/patterns to \`.claude/agent-memory/dev/MEMORY.md\`.
1158
1525
 
1159
- This generates: \`bg-primary\`, \`text-primary-fg\`, \`bg-surface\`, \`bg-surface-high/60\`, \`text-text\`, \`text-text-muted\`, \`border-border\`, etc. The \`/opacity\` modifier works on all generated utilities.
1526
+ ## Hard rules
1527
+
1528
+ - Never commit or push \u2014 a human does that.
1529
+ ${delegationRule}
1530
+ - Never edit generated files${cart.router === "TANSTACK_ROUTER" ? " (`src/routes/routeTree.gen.ts`)" : ""} or \`docs/INDEX.md\` by hand.
1160
1531
  `;
1161
1532
  };
1162
- importsTemplate = () => {
1163
- return frontmatter("**") + `# Import aliases and code organization
1533
+ testWriterAgentTemplate = (cart) => {
1534
+ const slug = projectSlug(cart);
1535
+ return `---
1536
+ name: test-writer
1537
+ description: "Test authoring agent for ${cart.projectName} \u2014 writes tests and their paired spec.md contracts, only under test/. Runs after docs-writer has produced the feature spec. <example>user: 'Cover the home page with tests' \u2192 test-writer <commentary>test authoring from an existing feature spec</commentary></example> <example>user: 'Add edge-case tests for the date validator' \u2192 test-writer <commentary>narrow test scope, reads the spec for edge cases</commentary></example> <example>user: 'Fix the validation bug the test caught' \u2192 dev, NOT test-writer <commentary>app-code changes belong to dev</commentary></example>"
1538
+ model: haiku
1539
+ memory: project
1540
+ ---
1164
1541
 
1165
- All imports use path aliases defined in \`tsconfig.json\` and \`vite.config.ts\`. This avoids fragile relative paths and makes moving files safe.
1542
+ You are the test-writing agent for ${cart.projectName}. You write ONLY under \`test/\`.
1166
1543
 
1167
- ## Available aliases
1544
+ ## Onboarding protocol
1168
1545
 
1169
- - \`@/*\` \u2192 \`./src/\` \u2014 shortest form, use when unambiguous
1170
- - \`@components/*\` \u2014 components in \`./src/components/\`
1171
- - \`@pages/*\` \u2014 pages in \`./src/pages/\`
1172
- - \`@utils/*\` \u2014 utilities in \`./src/utils/\`
1173
- - \`@types/*\` \u2014 types in \`./src/types/\`
1174
- - \`@hooks/*\` \u2014 hooks in \`./src/hooks/\`
1175
- - \`@layouts/*\` \u2014 layouts in \`./src/layouts/\`
1176
- - \`@assets/*\` \u2014 assets in \`./src/assets/\`
1546
+ 1. Read \`.claude/agent-memory/test-writer/MEMORY.md\`.
1547
+ 2. Read the feature spec: \`docs/features/<feature>/<feature>.spec.en.md\` \u2014 requirements and edge cases drive the test plan. If it does not exist, STOP and request docs-writer first.
1548
+ 3. Load the \`${slug}-test-author\` skill and \`test/README.md\`.
1177
1549
 
1178
- ## Rules
1550
+ ## Workflow
1179
1551
 
1180
- - **Never use relative imports** (\`../../../\`). Always use an alias.
1181
- - **Group imports**: standard library, third-party, then project aliases.
1182
- - **Organize by type within a file**: imports, types, constants, then code.
1552
+ 1. Derive the test plan from the spec's requirements and edge cases.
1553
+ 2. Write tests under ${cart.testing === "VITEST" ? "`test/unit/` (mirroring src/ layers)" : "`test/e2e/` (mirroring business flows, using Page Objects from `test/_shared/pages/`)"}.
1554
+ 3. Write the paired contract at \`test/specs/<same-path>/<name>.spec.md\`: what each case covers, which feature spec it traces to.
1555
+ 4. Run ${cart.testing === "VITEST" ? "`npm run test:run`" : "`npm run test:e2e`"} and report results faithfully \u2014 including failures.
1556
+ 5. Append lessons to \`.claude/agent-memory/test-writer/MEMORY.md\`.
1183
1557
 
1184
- ### Examples
1558
+ ## Hard rules
1185
1559
 
1186
- \u2705 Good:
1187
- \`\`\`typescript
1188
- import { ReactNode } from 'react';
1189
- import { QueryClient } from '@tanstack/react-query';
1560
+ - Never create test files inside \`src/\` \u2014 \`test/\` only.
1561
+ - Never edit application code, configs, or docs/ \u2014 if a test exposes a bug, report it for the dev agent.
1562
+ - Never weaken a test to make it pass.
1563
+ - Never commit or push.
1564
+ `;
1565
+ };
1566
+ getClaudeFileMap = (cart) => {
1567
+ const hasTesting = cart.testing !== "NOT_USING";
1568
+ return buildClaudeFileMap({
1569
+ projectName: cart.projectName,
1570
+ slug: projectSlug(cart),
1571
+ flowEnum: flowEnum(cart),
1572
+ layerEnum: layerEnum(cart),
1573
+ reminderTrigger: reminderTrigger(cart),
1574
+ claudeMd: claudeMdTemplate(cart),
1575
+ conventionsSkill: conventionsSkillTemplate(cart),
1576
+ devAgent: devAgentTemplate(cart),
1577
+ seedDocs: [{ relativePath: "docs/features/home/home.spec.en.md", content: docsHomeSpecTemplate(cart) }],
1578
+ testing: hasTesting ? {
1579
+ testWriterAgent: testWriterAgentTemplate(cart),
1580
+ testAuthorSkill: testAuthorSkillTemplate(cart)
1581
+ } : void 0
1582
+ });
1583
+ };
1584
+ }
1585
+ });
1190
1586
 
1191
- import { Button } from '@components/Button';
1192
- import { useUser } from '@hooks/useUser';
1193
- import type { User } from '@types/user';
1194
- import { cn } from '@utils/cn';
1195
- \`\`\`
1587
+ // src/scaffold/react-vite/templates/testing-setup.ts
1588
+ var vitestConfigTemplate, vitestSetupTemplate, testTsconfigTemplate, unitHomeTestTemplate, unitHomeSpecMdTemplate, playwrightConfigTemplate, e2eHomePageObjectTemplate, e2eHomeSpecTemplate, e2eHomeSpecMdTemplate, testReadmeTemplate, getTestingFileMap;
1589
+ var init_testing_setup = __esm({
1590
+ "src/scaffold/react-vite/templates/testing-setup.ts"() {
1591
+ "use strict";
1592
+ vitestConfigTemplate = () => `import { defineConfig, mergeConfig } from 'vitest/config';
1593
+ import viteConfig from './vite.config';
1594
+
1595
+ export default mergeConfig(viteConfig, defineConfig({
1596
+ test: {
1597
+ environment: 'jsdom',
1598
+ include: ['test/unit/**/*.test.{ts,tsx}'],
1599
+ setupFiles: ['./test/_shared/setup.ts'],
1600
+ coverage: {
1601
+ provider: 'v8',
1602
+ reporter: ['text', 'json', 'html'],
1603
+ include: ['src/**'],
1604
+ },
1605
+ },
1606
+ }));
1607
+ `;
1608
+ vitestSetupTemplate = () => `import '@testing-library/jest-dom/vitest';
1609
+ import { cleanup } from '@testing-library/react';
1610
+ import { afterEach } from 'vitest';
1196
1611
 
1197
- \u274C Bad:
1198
- \`\`\`typescript
1199
- import { Button } from '../../../components/Button'; // relative path
1200
- import Button from '@components/Button/Button'; // no file extension in alias
1201
- \`\`\`
1612
+ afterEach(cleanup);
1613
+ `;
1614
+ testTsconfigTemplate = (hasVitest) => JSON.stringify(
1615
+ {
1616
+ extends: "../tsconfig.json",
1617
+ compilerOptions: hasVitest ? { types: ["@testing-library/jest-dom"] } : {},
1618
+ include: ["."]
1619
+ },
1620
+ null,
1621
+ 2
1622
+ );
1623
+ unitHomeTestTemplate = (cart) => {
1624
+ const importLine = cart.layout === "FSD" ? `import { HomePage } from '@/pages/home';` : `import Home from '@/pages/Home';`;
1625
+ const component = cart.layout === "FSD" ? "HomePage" : "Home";
1626
+ return `import { render } from '@testing-library/react';
1627
+ import { describe, it, expect } from 'vitest';
1628
+ ${importLine}
1629
+
1630
+ // Paired contract: test/specs/unit/pages/home.spec.md
1631
+ describe('${component}', () => {
1632
+ it('renders without crashing', () => {
1633
+ const { container } = render(<${component} />);
1634
+ expect(container.firstChild).not.toBeNull();
1635
+ });
1636
+ });
1202
1637
  `;
1203
1638
  };
1204
- linterTemplate = (cart) => {
1205
- if (cart.linter === "BIOME") {
1206
- return frontmatter("**") + `# Biome
1639
+ unitHomeSpecMdTemplate = () => `# home.test.tsx \u2014 Contract
1640
+
1641
+ Traces to: \`docs/features/home/home.spec.en.md\`
1207
1642
 
1208
- - Configuration: \`biome.json\` at the repo root. Respect the rules it enforces.
1209
- - Run \`npm run lint\` and \`npm run format\` before committing.
1210
- - Do not disable rules inline (\`// biome-ignore\`) without a comment explaining why.
1211
- - Import order, formatting, and unused imports are enforced by Biome \u2014 do not manually format differently.
1643
+ | Case | Covers |
1644
+ |---|---|
1645
+ | renders without crashing | The home page mounts with no props and produces DOM output |
1646
+
1647
+ Add a row here for every new test case in \`test/unit/pages/home.test.tsx\`.
1212
1648
  `;
1213
- }
1214
- if (cart.linter === "ESLINT") {
1215
- return frontmatter("**") + `# ESLint
1216
-
1217
- - Configuration: \`eslint.config.js\` (flat config) at the repo root.
1218
- - Run \`npm run lint\` before committing; fix all errors and warnings.
1219
- - Do not add \`// eslint-disable\` or \`/* eslint-disable */\` without a short comment explaining why.
1220
- - React hooks rules are enforced \u2014 never call hooks conditionally or in loops.
1221
- - Unused variables must be prefixed with \`_\` or removed.
1649
+ playwrightConfigTemplate = () => `import { defineConfig, devices } from '@playwright/test';
1650
+
1651
+ export default defineConfig({
1652
+ testDir: './test/e2e',
1653
+ fullyParallel: true,
1654
+ forbidOnly: !!process.env.CI,
1655
+ retries: process.env.CI ? 2 : 0,
1656
+ workers: process.env.CI ? 1 : undefined,
1657
+ reporter: 'html',
1658
+ use: {
1659
+ baseURL: 'http://localhost:5173',
1660
+ trace: 'on-first-retry',
1661
+ },
1662
+ projects: [
1663
+ {
1664
+ name: 'chromium',
1665
+ use: { ...devices['Desktop Chrome'] },
1666
+ },
1667
+ ],
1668
+ webServer: {
1669
+ command: 'npm run dev',
1670
+ url: 'http://localhost:5173',
1671
+ reuseExistingServer: !process.env.CI,
1672
+ },
1673
+ });
1674
+ `;
1675
+ e2eHomePageObjectTemplate = () => `import { Page } from '@playwright/test';
1676
+
1677
+ export class HomePage {
1678
+ constructor(private page: Page) {}
1679
+
1680
+ async goto() {
1681
+ await this.page.goto('/');
1682
+ }
1683
+ }
1684
+ `;
1685
+ e2eHomeSpecTemplate = (projectName) => `import { test, expect } from '@playwright/test';
1686
+ import { HomePage } from '../_shared/pages/home.page';
1687
+
1688
+ // Paired contract: test/specs/e2e/home.spec.md
1689
+ test('home page loads', async ({ page }) => {
1690
+ const homePage = new HomePage(page);
1691
+ await homePage.goto();
1692
+ await expect(page).toHaveTitle(/${projectName}/i);
1693
+ });
1694
+ `;
1695
+ e2eHomeSpecMdTemplate = () => `# home.spec.ts \u2014 Contract
1696
+
1697
+ Traces to: \`docs/features/home/home.spec.en.md\`
1698
+
1699
+ | Case | Covers |
1700
+ |---|---|
1701
+ | home page loads | Navigating to \`/\` renders the home page with the project title |
1702
+
1703
+ Add a row here for every new test case in \`test/e2e/home.spec.ts\`.
1704
+ `;
1705
+ testReadmeTemplate = (cart) => {
1706
+ const hasVitest = cart.testing === "VITEST";
1707
+ return `# test/ \u2014 Centralized Tests
1708
+
1709
+ All test code lives here \u2014 never inside \`src/\`. One reviewable location, no test pollution in the source tree, no test-runner config changes needed when adding tests.
1710
+
1711
+ ## Layout
1712
+
1713
+ \`\`\`
1714
+ test/
1715
+ ${hasVitest ? `\u251C\u2500\u2500 unit/ # mirrors src/ layer structure; <name>.test.{ts,tsx}
1716
+ ` : `\u251C\u2500\u2500 e2e/ # mirrors business flows; <flow>.spec.ts
1717
+ `}\u251C\u2500\u2500 _shared/ # fixtures/, helpers/, mocks/${hasVitest ? "" : ", pages/ (Page Object Models)"}
1718
+ \u251C\u2500\u2500 specs/ # paired *.spec.md contracts \u2014 documentation, NEVER executed
1719
+ \u2514\u2500\u2500 tsconfig.json # extends root tsconfig for editor support
1720
+ \`\`\`
1721
+
1722
+ ## Conventions
1723
+
1724
+ - Every test file has a paired contract at \`test/specs/<same-path>/<name>.spec.md\` listing what each case covers and which feature spec (\`docs/features/\`) it traces to. Update both together.
1725
+ ${hasVitest ? `- Unit tests import app code via the \`@/\` alias and use explicit vitest imports.
1726
+ - Component tests use Testing Library \u2014 query by role/label, never by class name.
1727
+
1728
+ ## Running
1729
+
1730
+ - \`npm test\` \u2014 watch mode
1731
+ - \`npm run test:run\` \u2014 single run (CI)
1732
+ - \`npm run coverage\` \u2014 with coverage report` : `- E2E specs stay thin \u2014 page interactions belong in Page Object Models under \`test/_shared/pages/\`.
1733
+
1734
+ ## Running
1735
+
1736
+ - \`npm run test:e2e\` \u2014 runs Playwright (starts the dev server automatically)`}
1222
1737
  `;
1223
- }
1224
- return "";
1225
1738
  };
1226
- getCopilotInstructionFiles = (cart) => {
1227
- const hasRouter = cart.router === "TANSTACK_ROUTER";
1228
- const hasZustand = cart.stateManagement === "ZUSTAND";
1229
- const hasQuery = cart.query === "TANSTACK_QUERY";
1230
- const hasLinter = cart.linter !== "NOT_USING";
1231
- const files = [
1232
- { relativePath: ".github/copilot-instructions.md", content: generalTemplate(cart) },
1233
- { relativePath: ".github/instructions/imports.instructions.md", content: importsTemplate() },
1234
- { relativePath: ".github/instructions/components.instructions.md", content: componentsTemplate(cart) },
1235
- { relativePath: ".github/instructions/hooks.instructions.md", content: hooksTemplate(cart) }
1236
- ];
1237
- if (hasRouter) {
1238
- files.push({
1239
- relativePath: ".github/instructions/tanstack-router.instructions.md",
1240
- content: routerTemplate(cart)
1241
- });
1242
- }
1243
- if (hasZustand) {
1244
- files.push({
1245
- relativePath: ".github/instructions/zustand.instructions.md",
1246
- content: zustandTemplate(cart)
1247
- });
1248
- }
1249
- if (hasQuery) {
1250
- files.push({
1251
- relativePath: ".github/instructions/tanstack-query.instructions.md",
1252
- content: queryTemplate(cart)
1253
- });
1254
- }
1255
- if (cart.css === "TAILWIND") {
1256
- files.push({
1257
- relativePath: ".github/instructions/tailwind.instructions.md",
1258
- content: tailwindTemplate(cart)
1259
- });
1739
+ getTestingFileMap = (cart) => {
1740
+ const files = [];
1741
+ if (cart.testing === "VITEST") {
1742
+ files.push(
1743
+ { relativePath: "vitest.config.ts", content: vitestConfigTemplate() },
1744
+ { relativePath: "test/README.md", content: testReadmeTemplate(cart) },
1745
+ { relativePath: "test/tsconfig.json", content: testTsconfigTemplate(true) },
1746
+ { relativePath: "test/_shared/setup.ts", content: vitestSetupTemplate() },
1747
+ { relativePath: "test/_shared/fixtures/.gitkeep", content: "" },
1748
+ { relativePath: "test/_shared/helpers/.gitkeep", content: "" },
1749
+ { relativePath: "test/_shared/mocks/.gitkeep", content: "" },
1750
+ { relativePath: "test/unit/pages/home.test.tsx", content: unitHomeTestTemplate(cart) },
1751
+ { relativePath: "test/specs/unit/pages/home.spec.md", content: unitHomeSpecMdTemplate() }
1752
+ );
1260
1753
  }
1261
- if (hasLinter) {
1262
- files.push({
1263
- relativePath: `.github/instructions/${cart.linter === "BIOME" ? "biome" : "eslint"}.instructions.md`,
1264
- content: linterTemplate(cart)
1265
- });
1754
+ if (cart.testing === "PLAYWRIGHT") {
1755
+ files.push(
1756
+ { relativePath: "playwright.config.ts", content: playwrightConfigTemplate() },
1757
+ { relativePath: "test/README.md", content: testReadmeTemplate(cart) },
1758
+ { relativePath: "test/tsconfig.json", content: testTsconfigTemplate(false) },
1759
+ { relativePath: "test/_shared/fixtures/.gitkeep", content: "" },
1760
+ { relativePath: "test/_shared/helpers/.gitkeep", content: "" },
1761
+ { relativePath: "test/_shared/mocks/.gitkeep", content: "" },
1762
+ { relativePath: "test/_shared/pages/home.page.ts", content: e2eHomePageObjectTemplate() },
1763
+ { relativePath: "test/e2e/home.spec.ts", content: e2eHomeSpecTemplate(cart.projectName) },
1764
+ { relativePath: "test/specs/e2e/home.spec.md", content: e2eHomeSpecMdTemplate() }
1765
+ );
1266
1766
  }
1267
1767
  return files;
1268
1768
  };
@@ -1507,7 +2007,8 @@ var init_fsd_layout = __esm({
1507
2007
  init_gitignore();
1508
2008
  init_styles();
1509
2009
  init_vite_env_d_ts();
1510
- init_copilot_instructions();
2010
+ init_claude_setup2();
2011
+ init_testing_setup();
1511
2012
  init_home_page();
1512
2013
  getFsdFileMap = (cart) => {
1513
2014
  const hasRouter = cart.router === "TANSTACK_ROUTER";
@@ -1520,7 +2021,8 @@ var init_fsd_layout = __esm({
1520
2021
  { relativePath: "tsconfig.node.json", content: tsconfigNodeTemplate() },
1521
2022
  { relativePath: "index.html", content: indexHtmlTemplate(cart.projectName) },
1522
2023
  { relativePath: ".gitignore", content: gitignoreTemplate() },
1523
- ...getCopilotInstructionFiles(cart),
2024
+ ...cart.ai === "CLAUDE" ? getClaudeFileMap(cart) : [],
2025
+ ...cart.testing !== "NOT_USING" ? getTestingFileMap(cart) : [],
1524
2026
  { relativePath: "src/vite-env.d.ts", content: viteEnvDtsTemplate() },
1525
2027
  { relativePath: "src/main.tsx", content: mainTsxTemplate(cart) },
1526
2028
  { relativePath: "src/app/index.tsx", content: appTsxFsdTemplate(hasRouter, hasQuery) },
@@ -1611,7 +2113,8 @@ var init_bpr_layout = __esm({
1611
2113
  init_gitignore();
1612
2114
  init_styles();
1613
2115
  init_vite_env_d_ts();
1614
- init_copilot_instructions();
2116
+ init_claude_setup2();
2117
+ init_testing_setup();
1615
2118
  init_home_page();
1616
2119
  getBprFileMap = (cart) => {
1617
2120
  const hasRouter = cart.router === "TANSTACK_ROUTER";
@@ -1624,7 +2127,8 @@ var init_bpr_layout = __esm({
1624
2127
  { relativePath: "tsconfig.node.json", content: tsconfigNodeTemplate() },
1625
2128
  { relativePath: "index.html", content: indexHtmlTemplate(cart.projectName) },
1626
2129
  { relativePath: ".gitignore", content: gitignoreTemplate() },
1627
- ...getCopilotInstructionFiles(cart),
2130
+ ...cart.ai === "CLAUDE" ? getClaudeFileMap(cart) : [],
2131
+ ...cart.testing !== "NOT_USING" ? getTestingFileMap(cart) : [],
1628
2132
  { relativePath: "src/vite-env.d.ts", content: viteEnvDtsTemplate() },
1629
2133
  { relativePath: "src/main.tsx", content: mainTsxTemplate(cart) },
1630
2134
  { relativePath: "src/App.tsx", content: appTsxBprTemplate(hasRouter, hasQuery) },
@@ -1675,8 +2179,8 @@ __export(react_vite_exports, {
1675
2179
  scaffoldReactVite: () => scaffoldReactVite
1676
2180
  });
1677
2181
  import path2 from "path";
1678
- import chalk from "chalk";
1679
- import { createSpinner } from "nanospinner";
2182
+ import chalk2 from "chalk";
2183
+ import { createSpinner as createSpinner2 } from "nanospinner";
1680
2184
  var scaffoldReactVite;
1681
2185
  var init_react_vite = __esm({
1682
2186
  "src/scaffold/react-vite/index.ts"() {
@@ -1695,33 +2199,33 @@ var init_react_vite = __esm({
1695
2199
  `Directory "${projectName}" already exists. Please choose a different project name or remove the existing directory.`
1696
2200
  );
1697
2201
  }
1698
- const spinner = createSpinner(`Scaffolding ${chalk.cyan(projectName)}...`).start();
2202
+ const spinner = createSpinner2(`Scaffolding ${chalk2.cyan(projectName)}...`).start();
1699
2203
  try {
1700
2204
  const fileMap = cart.layout === "FSD" ? getFsdFileMap(cart) : getBprFileMap(cart);
1701
2205
  for (const { relativePath, content } of fileMap) {
1702
2206
  await writeProjectFile(projectRoot, relativePath, content);
1703
2207
  }
1704
2208
  spinner.success({
1705
- text: chalk.green(`Project ${chalk.bold(projectName)} created successfully!`)
2209
+ text: chalk2.green(`Project ${chalk2.bold(projectName)} created successfully!`)
1706
2210
  });
1707
2211
  console.log("");
1708
- console.log(chalk.whiteBright("Next steps:"));
1709
- console.log(chalk.cyan(` cd ${projectName}`));
1710
- console.log(chalk.cyan(" npm install"));
1711
- console.log(chalk.cyan(" npm run dev"));
2212
+ console.log(chalk2.whiteBright("Next steps:"));
2213
+ console.log(chalk2.cyan(` cd ${projectName}`));
2214
+ console.log(chalk2.cyan(" npm install"));
2215
+ console.log(chalk2.cyan(" npm run dev"));
1712
2216
  if (cart.router === "TANSTACK_ROUTER") {
1713
2217
  console.log("");
1714
- console.log(chalk.gray(" Note: TanStack Router will auto-generate routeTree.gen.ts on first dev run."));
2218
+ console.log(chalk2.gray(" Note: TanStack Router will auto-generate routeTree.gen.ts on first dev run."));
1715
2219
  }
1716
2220
  console.log("");
1717
2221
  } catch (err) {
1718
- spinner.error({ text: chalk.red("Scaffolding failed.") });
2222
+ spinner.error({ text: chalk2.red("Scaffolding failed.") });
1719
2223
  if (err instanceof ScaffoldError) {
1720
- console.error(chalk.red(err.message));
2224
+ console.error(chalk2.red(err.message));
1721
2225
  } else if (isNodeError(err)) {
1722
- console.error(chalk.red(`File system error (${err.code}): ${err.message}`));
2226
+ console.error(chalk2.red(`File system error (${err.code}): ${err.message}`));
1723
2227
  } else {
1724
- console.error(chalk.red("An unexpected error occurred."), err);
2228
+ console.error(chalk2.red("An unexpected error occurred."), err);
1725
2229
  }
1726
2230
  try {
1727
2231
  const { rm } = await import("fs/promises");
@@ -1738,16 +2242,18 @@ var init_react_vite = __esm({
1738
2242
  var react_vite_exports2 = {};
1739
2243
  __export(react_vite_exports2, {
1740
2244
  flowReactVite: () => flowReactVite,
2245
+ menuAI: () => menuAI,
1741
2246
  menuCss: () => menuCss,
1742
2247
  menuLayout: () => menuLayout,
1743
2248
  menuLinter: () => menuLinter,
1744
2249
  menuQuery: () => menuQuery,
1745
2250
  menuRouter: () => menuRouter,
1746
- menuStateManagement: () => menuStateManagement
2251
+ menuStateManagement: () => menuStateManagement,
2252
+ menuTesting: () => menuTesting
1747
2253
  });
1748
2254
  import { select, input, Separator } from "@inquirer/prompts";
1749
- import chalk2 from "chalk";
1750
- var selectFromMenu, menuProjectName, menuLayout, menuRouter, menuStateManagement, menuQuery, menuCss, menuLinter, flowReactVite;
2255
+ import chalk3 from "chalk";
2256
+ var selectFromMenu, menuProjectName, menuLayout, menuRouter, menuStateManagement, menuQuery, menuCss, menuLinter, menuTesting, menuAI, flowReactVite;
1751
2257
  var init_react_vite2 = __esm({
1752
2258
  "src/options/react-vite/index.ts"() {
1753
2259
  "use strict";
@@ -1762,7 +2268,7 @@ var init_react_vite2 = __esm({
1762
2268
  description: menuOptions[key].description
1763
2269
  }));
1764
2270
  const answer = await select({
1765
- message: chalk2.whiteBright(message),
2271
+ message: chalk3.whiteBright(message),
1766
2272
  choices: [...choices, new Separator()]
1767
2273
  });
1768
2274
  return answer;
@@ -1770,7 +2276,7 @@ var init_react_vite2 = __esm({
1770
2276
  menuProjectName = async (cart) => {
1771
2277
  if (!cart || cart.type !== MENU_OPTIONS_LEVEL_1.ReactVite.value) return;
1772
2278
  cart.projectName = await input({
1773
- message: chalk2.whiteBright("Project name:"),
2279
+ message: chalk3.whiteBright("Project name:"),
1774
2280
  validate: (value) => {
1775
2281
  if (!value.trim()) return "Project name cannot be empty";
1776
2282
  if (!/^[a-zA-Z0-9_-]+$/.test(value.trim()))
@@ -1813,6 +2319,14 @@ var init_react_vite2 = __esm({
1813
2319
  "Choose a Linter / Formatter"
1814
2320
  );
1815
2321
  };
2322
+ menuTesting = async (cart) => {
2323
+ if (!cart || cart.type !== MENU_OPTIONS_LEVEL_1.ReactVite.value) return;
2324
+ cart.testing = await selectFromMenu(REACT_MENU_TESTING, "Choose a Testing setup");
2325
+ };
2326
+ menuAI = async (cart) => {
2327
+ if (!cart || cart.type !== MENU_OPTIONS_LEVEL_1.ReactVite.value) return;
2328
+ cart.ai = await selectFromMenu(REACT_MENU_AI, "Choose an AI setup");
2329
+ };
1816
2330
  flowReactVite = async (cart) => {
1817
2331
  if (!cart || cart.type !== MENU_OPTIONS_LEVEL_1.ReactVite.value) return;
1818
2332
  await menuProjectName(cart);
@@ -1822,6 +2336,8 @@ var init_react_vite2 = __esm({
1822
2336
  await menuQuery(cart);
1823
2337
  await menuCss(cart);
1824
2338
  await menuLinter(cart);
2339
+ await menuTesting(cart);
2340
+ await menuAI(cart);
1825
2341
  const { scaffoldReactVite: scaffoldReactVite2 } = await Promise.resolve().then(() => (init_react_vite(), react_vite_exports));
1826
2342
  await scaffoldReactVite2(cart);
1827
2343
  };
@@ -1829,7 +2345,7 @@ var init_react_vite2 = __esm({
1829
2345
  });
1830
2346
 
1831
2347
  // src/options/chrome-extension/constants/index.ts
1832
- var CHROME_MENU_STATE_MANAGEMENT, CHROME_MENU_QUERY, CHROME_MENU_CSS, CHROME_MENU_LINTER;
2348
+ var CHROME_MENU_STATE_MANAGEMENT, CHROME_MENU_QUERY, CHROME_MENU_CSS, CHROME_MENU_AI, CHROME_MENU_LINTER;
1833
2349
  var init_constants3 = __esm({
1834
2350
  "src/options/chrome-extension/constants/index.ts"() {
1835
2351
  "use strict";
@@ -1875,6 +2391,20 @@ var init_constants3 = __esm({
1875
2391
  disabled: false
1876
2392
  }
1877
2393
  };
2394
+ CHROME_MENU_AI = {
2395
+ notUsing: {
2396
+ display: "Not Using",
2397
+ value: "NOT_USING",
2398
+ description: "No AI setup",
2399
+ disabled: false
2400
+ },
2401
+ claude: {
2402
+ display: "Claude (Claude Code)",
2403
+ value: "CLAUDE",
2404
+ description: "CLAUDE.md + .claude/ agents + feature docs structure",
2405
+ disabled: false
2406
+ }
2407
+ };
1878
2408
  CHROME_MENU_LINTER = {
1879
2409
  notUsing: {
1880
2410
  display: "Not Using",
@@ -1948,6 +2478,10 @@ var init_package_json2 = __esm({
1948
2478
  } else if (cart.linter === "ESLINT") {
1949
2479
  scripts["lint"] = "eslint .";
1950
2480
  }
2481
+ if (cart.ai === "CLAUDE") {
2482
+ scripts["docs:index"] = "node .claude/scripts/build-docs-index.mjs";
2483
+ scripts["docs:lint"] = "node .claude/scripts/lint-docs-frontmatter.mjs";
2484
+ }
1951
2485
  return JSON.stringify(
1952
2486
  {
1953
2487
  name: cart.projectName,
@@ -2115,6 +2649,285 @@ export default App;
2115
2649
  }
2116
2650
  });
2117
2651
 
2652
+ // src/scaffold/chrome-extension/templates/claude-setup.ts
2653
+ var projectSlug2, layerEnum2, flowEnum2, reminderTrigger2, claudeMdTemplate2, docsPopupSpecTemplate, conventionsSkillTemplate2, devAgentTemplate2, getClaudeFileMap2;
2654
+ var init_claude_setup3 = __esm({
2655
+ "src/scaffold/chrome-extension/templates/claude-setup.ts"() {
2656
+ "use strict";
2657
+ init_claude_setup();
2658
+ projectSlug2 = (cart) => cart.projectName.toLowerCase().replace(/_/g, "-");
2659
+ layerEnum2 = (cart) => [
2660
+ "popup",
2661
+ "components",
2662
+ "hooks",
2663
+ "lib",
2664
+ ...cart.stateManagement === "ZUSTAND" ? ["stores"] : [],
2665
+ "types",
2666
+ "utils",
2667
+ "_cross"
2668
+ ];
2669
+ flowEnum2 = (cart) => [
2670
+ "ui",
2671
+ "data",
2672
+ ...cart.stateManagement === "ZUSTAND" ? ["state"] : [],
2673
+ "extension",
2674
+ "infra",
2675
+ "architecture",
2676
+ "onboarding",
2677
+ "_meta"
2678
+ ];
2679
+ reminderTrigger2 = (cart) => `popup|manifest${cart.stateManagement === "ZUSTAND" ? "|store|state" : ""}${cart.query === "TANSTACK_QUERY" ? "|query|fetch" : ""}`;
2680
+ claudeMdTemplate2 = (cart) => {
2681
+ const hasZustand = cart.stateManagement === "ZUSTAND";
2682
+ const hasQuery = cart.query === "TANSTACK_QUERY";
2683
+ const slug = projectSlug2(cart);
2684
+ const stack = [
2685
+ "React 19, TypeScript 5.8, Vite 6",
2686
+ "Chrome Extension Manifest V3 (popup-only)",
2687
+ hasZustand ? "Zustand v5.0.5" : null,
2688
+ hasQuery ? "TanStack Query v5.74.4" : null,
2689
+ cart.css === "TAILWIND" ? "Tailwind CSS v4.1.3" : null,
2690
+ cart.linter === "BIOME" ? "Biome v1.9.4" : cart.linter === "ESLINT" ? "ESLint v9" : null
2691
+ ].filter(Boolean);
2692
+ const commands = [
2693
+ "- `npm run dev` \u2014 develop the popup as a normal Vite page (http://localhost:5173)",
2694
+ "- `npm run build` \u2014 type-check + production build into `dist/`",
2695
+ "- `npm run build-extension` \u2014 build + copy `manifest.json` into `dist/`; load `dist/` as an unpacked extension at chrome://extensions",
2696
+ cart.linter !== "NOT_USING" ? "- `npm run lint` \u2014 lint code" : null,
2697
+ "- `npm run docs:index` \u2014 regenerate docs/INDEX.md (run after any doc change)",
2698
+ "- `npm run docs:lint` \u2014 validate docs frontmatter (CI-ready, exits non-zero on violation)"
2699
+ ].filter(Boolean);
2700
+ const keyPatterns = [
2701
+ `**Popup lifecycle \u2014 the popup unmounts when it closes:**
2702
+ React state dies with the popup window. Anything that must survive a close (settings, session data) belongs in \`chrome.storage\`, not in component state${hasZustand ? " or the Zustand store" : ""}.`
2703
+ ];
2704
+ if (hasZustand) {
2705
+ keyPatterns.push(`**Zustand \u2014 select narrowly, never the whole store:**
2706
+ \`\`\`ts
2707
+ const count = useAppStore((state) => state.count); // \u2705 re-renders on count only
2708
+ const store = useAppStore(); // \u274C re-renders on every change
2709
+ \`\`\``);
2710
+ }
2711
+ if (hasQuery) {
2712
+ keyPatterns.push(`**TanStack Query \u2014 key factories, not inline keys:**
2713
+ \`\`\`ts
2714
+ const todoKeys = {
2715
+ all: ['todos'] as const,
2716
+ detail: (id: string) => [...todoKeys.all, id] as const,
2717
+ };
2718
+ useQuery({ queryKey: todoKeys.detail(id), queryFn: () => fetchTodo(id) });
2719
+ \`\`\``);
2720
+ }
2721
+ return `# ${cart.projectName}
2722
+
2723
+ ## Behavioral Guidelines
2724
+
2725
+ **Think Before Coding** \u2014 state assumptions explicitly; if multiple interpretations exist, present them; if something is unclear, stop and ask.
2726
+ **Simplicity First** \u2014 minimum code that solves the problem; no speculative features, abstractions, or configurability.
2727
+ **Surgical Changes** \u2014 touch only what the request requires; match existing style; every changed line traces to the request.
2728
+ **Goal-Driven Execution** \u2014 turn tasks into verifiable success criteria before starting; loop until verified.
2729
+
2730
+ ## Project Overview
2731
+
2732
+ ${stack.map((s) => `- ${s}`).join("\n")}
2733
+
2734
+ This is a **popup-only MV3 extension**: \`index.html\` \u2192 \`src/main.tsx\` \u2192 \`src/App.tsx\` is the popup root. No background service worker or content scripts are configured \u2014 adding one requires registering it in \`manifest.json\` AND adding a Rollup input in \`vite.config.ts\`.
2735
+
2736
+ ## Commands
2737
+
2738
+ ${commands.join("\n")}
2739
+
2740
+ ## Architecture
2741
+
2742
+ \`\`\`
2743
+ manifest.json \u2192 MV3 source of truth (root of the repo)
2744
+ src/App.tsx \u2192 popup root component
2745
+ components \u2192 shared presentational components
2746
+ hooks \u2192 shared React hooks
2747
+ lib \u2192 third-party wrappers / configured clients
2748
+ ${hasZustand ? "stores \u2192 global state (Zustand)\n" : ""}types \u2192 shared TypeScript types
2749
+ utils \u2192 pure utility functions
2750
+ \`\`\`
2751
+
2752
+ **Hard rules:**
2753
+ - \`manifest.json\` at the repo root is the single source of truth \u2014 NEVER edit \`dist/manifest.json\` (it is a build artifact copied by \`npm run build-extension\`).
2754
+ - Shared folders (components/, hooks/, lib/, utils/) must stay feature-agnostic.
2755
+ - Keep components presentational; data fetching and chrome.* API calls live in hooks or lib/.
2756
+
2757
+ ## Key Patterns
2758
+
2759
+ ${keyPatterns.join("\n\n")}
2760
+
2761
+ ## Import Aliases
2762
+
2763
+ \`@/\` (src root), \`@components\`, \`@hooks\`, \`@lib\`, \`@types\`, \`@utils\` \u2014 defined in \`vite.config.ts\`. Never use long relative chains (\`../../..\`).
2764
+
2765
+ ## Anti-Patterns
2766
+
2767
+ | Anti-pattern | Correct approach |
2768
+ |---|---|
2769
+ | Editing \`dist/manifest.json\` | Edit root \`manifest.json\`; rebuild with \`npm run build-extension\` |
2770
+ | Persisting data in React state${hasZustand ? "/Zustand" : ""} that must survive popup close | Use \`chrome.storage\` |
2771
+ | \`useEffect\` for derived state | Compute during render or use \`useMemo\` |
2772
+
2773
+ > Fill this table from real review feedback over time \u2014 it is the highest-leverage section for code quality.
2774
+
2775
+ ## Agent Routing
2776
+
2777
+ | Task / trigger | Agent | Notes |
2778
+ |---|---|---|
2779
+ | Feature work or bug fix in \`src/\` or \`manifest.json\` | \`dev\` | MUST read relevant \`docs/features/\` spec before coding |
2780
+ | Analyzing requirements, writing/updating feature docs | \`docs-writer\` | owns \`docs/\`; rebuilds INDEX.md after every change |
2781
+
2782
+ Each agent has persistent memory at \`.claude/agent-memory/<agent>/MEMORY.md\` \u2014 agents read it on start and append new gotchas. Do NOT use the general assistant for work an agent owns \u2014 always delegate.
2783
+
2784
+ ## Task Documentation Convention
2785
+
2786
+ After any non-trivial fix or new pattern: copy \`docs/_template.md\`, fill the frontmatter, save as \`docs/features/<feature>/<topic>.en.md\` (or \`docs/architecture/\` for cross-cutting topics), then run \`npm run docs:index\` and commit the doc together with \`INDEX.md\`. Validate with \`npm run docs:lint\`.
2787
+
2788
+ ## Further Reading + DOCS-FIRST RULE
2789
+
2790
+ Skills: \`.claude/skills/${slug}-conventions\` (architecture depth), \`.claude/skills/${slug}-docs\` (how to query the knowledge base).
2791
+
2792
+ **DOCS-FIRST RULE:** for any request to describe, explain, or modify a documented feature, you MUST grep \`docs/\` frontmatter and read the relevant docs BEFORE opening source files \u2014 and state what the docs already covered. Start at \`docs/INDEX.md\`.
2793
+
2794
+ **Operating loop:** finish a non-trivial task \u2192 write a doc from \`docs/_template.md\` \u2192 rebuild \`INDEX.md\` \u2192 commit together; agents update their \`MEMORY.md\` when they learn a gotcha.
2795
+ `;
2796
+ };
2797
+ docsPopupSpecTemplate = (cart) => {
2798
+ const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
2799
+ return `---
2800
+ title: Popup \u2014 Feature Spec
2801
+ feature: popup
2802
+ flow: ui
2803
+ layer: popup
2804
+ status: draft
2805
+ lang: en
2806
+ related: []
2807
+ keywords: [popup, manifest]
2808
+ updated: ${today}
2809
+ ---
2810
+
2811
+ # Popup \u2014 Feature Spec
2812
+
2813
+ ## Context
2814
+ Starter popup generated by the scaffold. Replace this spec with the real requirements for ${cart.projectName}'s popup.
2815
+
2816
+ ## Root Cause / Key Finding
2817
+ _(For feature specs, use this section for key constraints or discoveries.)_
2818
+
2819
+ ## Solution / Pattern
2820
+ - Requirements: [list what the popup must do]
2821
+ - UI/UX: [screens and interactions]
2822
+ - Data: [data structures, chrome.storage keys, API requirements]
2823
+ - Edge cases: [what must not break \u2014 remember the popup unmounts on close]
2824
+
2825
+ ## Key Decisions
2826
+ _(Trade-offs made and alternatives rejected.)_
2827
+
2828
+ ## Related Files
2829
+ - src/App.tsx
2830
+ - manifest.json
2831
+ `;
2832
+ };
2833
+ conventionsSkillTemplate2 = (cart) => {
2834
+ const slug = projectSlug2(cart);
2835
+ const hasZustand = cart.stateManagement === "ZUSTAND";
2836
+ return `---
2837
+ name: ${slug}-conventions
2838
+ description: Coding conventions and architecture rules for ${cart.projectName} (Chrome extension, MV3 popup). Use when writing or reviewing ANY code under src/ \u2014 components, hooks${hasZustand ? ", stores" : ""}, or styling \u2014 or when touching manifest.json. Triggers on tasks mentioning popup, manifest, components, naming, imports, or project structure.
2839
+ ---
2840
+
2841
+ # ${cart.projectName} Conventions
2842
+
2843
+ In-depth companion to CLAUDE.md. CLAUDE.md states the rules; this skill explains how to apply them.
2844
+
2845
+ ## Structure
2846
+
2847
+ - Popup root: \`src/App.tsx\` (entry: \`index.html\` \u2192 \`src/main.tsx\`).
2848
+ - Shared folders (components/, hooks/, lib/, utils/) must stay feature-agnostic.
2849
+ - \`manifest.json\` at the repo root is the MV3 source of truth; \`npm run build-extension\` copies it into \`dist/\` \u2014 never edit the copy.
2850
+ - No background service worker / content scripts are configured. Adding one = register it in \`manifest.json\` + add a Rollup input in \`vite.config.ts\`.
2851
+
2852
+ ## Import aliases
2853
+
2854
+ \`@/\`, \`@components\`, \`@hooks\`, \`@lib\`, \`@types\`, \`@utils\` (see \`vite.config.ts\`). Never use long relative chains (\`../../..\`).
2855
+
2856
+ ## Chrome APIs
2857
+
2858
+ - Wrap \`chrome.*\` calls in \`lib/\` or hooks \u2014 components stay presentational.
2859
+ - State that must survive popup close goes in \`chrome.storage\`, not React state${hasZustand ? " or Zustand" : ""}.
2860
+
2861
+ ${hasZustand ? `## Zustand
2862
+
2863
+ - One store per domain; name \`use<Domain>Store\`.
2864
+ - Components select narrow slices: \`useAppStore((s) => s.count)\`.
2865
+ - Async actions live inside the store, setting loading/error state themselves.
2866
+
2867
+ ` : ""}${cart.query === "TANSTACK_QUERY" ? `## TanStack Query
2868
+
2869
+ - Define a key factory per entity next to its query functions.
2870
+ - Mutations invalidate via the same factory: \`queryClient.invalidateQueries({ queryKey: todoKeys.all })\`.
2871
+ - Server state belongs in Query \u2014 do not mirror it into ${hasZustand ? "Zustand" : "local state"}.
2872
+
2873
+ ` : ""}${cart.css === "TAILWIND" ? `## Tailwind CSS v4
2874
+
2875
+ - Theme tokens live in \`src/index.css\` under \`@theme\` \u2014 extend there, not in a JS config.
2876
+ - Prefer semantic class composition in components over @apply.
2877
+
2878
+ ` : ""}## When unsure
2879
+
2880
+ Check \`src/App.tsx\` first, then the docs (\`docs/INDEX.md\`), then ask.
2881
+ `;
2882
+ };
2883
+ devAgentTemplate2 = (cart) => {
2884
+ const slug = projectSlug2(cart);
2885
+ return `---
2886
+ name: dev
2887
+ description: "Implementation agent for ${cart.projectName} \u2014 all feature work and bug fixes under src/ and manifest.json. <example>user: 'Add a settings toggle to the popup' \u2192 dev <commentary>feature work in src/; dev reads the feature spec first, then implements</commentary></example> <example>user: 'The popup crashes on empty data \u2014 fix it' \u2192 dev <commentary>bug fix inside a documented feature</commentary></example> <example>user: 'Write a spec for the options page' \u2192 docs-writer, NOT dev <commentary>doc authoring belongs to docs-writer</commentary></example>"
2888
+ model: sonnet
2889
+ memory: project
2890
+ ---
2891
+
2892
+ You are the implementation agent for ${cart.projectName}.
2893
+
2894
+ ## Onboarding protocol (in order, before any code)
2895
+
2896
+ 1. Read \`.claude/agent-memory/dev/MEMORY.md\` \u2014 your accumulated gotchas.
2897
+ 2. Read \`docs/INDEX.md\` and the relevant \`docs/features/<feature>/\` spec for the task.
2898
+ 3. Load the \`${slug}-conventions\` skill for structure rules and patterns.
2899
+ 4. Read the code under change.
2900
+
2901
+ If no relevant feature spec exists, STOP and tell the user to run the docs-writer agent first.
2902
+
2903
+ ## Workflow
2904
+
2905
+ 1. State assumptions and success criteria.
2906
+ 2. Implement the minimum change that satisfies the spec; match existing style.
2907
+ 3. Verify (build; for manifest/extension changes run \`npm run build-extension\` and report what to check at chrome://extensions); report results faithfully.
2908
+ 4. Append newly discovered gotchas/patterns to \`.claude/agent-memory/dev/MEMORY.md\`.
2909
+
2910
+ ## Hard rules
2911
+
2912
+ - Never commit or push \u2014 a human does that.
2913
+ - Never write docs (delegate to docs-writer); flag when they are needed.
2914
+ - Never edit \`dist/\` (build artifact, including \`dist/manifest.json\`) or \`docs/INDEX.md\` by hand.
2915
+ `;
2916
+ };
2917
+ getClaudeFileMap2 = (cart) => buildClaudeFileMap({
2918
+ projectName: cart.projectName,
2919
+ slug: projectSlug2(cart),
2920
+ flowEnum: flowEnum2(cart),
2921
+ layerEnum: layerEnum2(cart),
2922
+ reminderTrigger: reminderTrigger2(cart),
2923
+ claudeMd: claudeMdTemplate2(cart),
2924
+ conventionsSkill: conventionsSkillTemplate2(cart),
2925
+ devAgent: devAgentTemplate2(cart),
2926
+ seedDocs: [{ relativePath: "docs/features/popup/popup.spec.en.md", content: docsPopupSpecTemplate(cart) }]
2927
+ });
2928
+ }
2929
+ });
2930
+
2118
2931
  // src/scaffold/chrome-extension/templates/layout.ts
2119
2932
  var getChromeExtensionFileMap;
2120
2933
  var init_layout = __esm({
@@ -2126,6 +2939,7 @@ var init_layout = __esm({
2126
2939
  init_build_extension_script();
2127
2940
  init_main_tsx2();
2128
2941
  init_app_tsx2();
2942
+ init_claude_setup3();
2129
2943
  init_vite_env_d_ts();
2130
2944
  init_tsconfig();
2131
2945
  init_index_html();
@@ -2143,6 +2957,7 @@ var init_layout = __esm({
2143
2957
  { relativePath: "index.html", content: indexHtmlTemplate(cart.projectName) },
2144
2958
  { relativePath: "manifest.json", content: manifestJsonTemplate(cart.projectName) },
2145
2959
  { relativePath: ".gitignore", content: gitignoreTemplate() },
2960
+ ...cart.ai === "CLAUDE" ? getClaudeFileMap2(cart) : [],
2146
2961
  { relativePath: "scripts/build-extension.js", content: buildExtensionScriptTemplate() },
2147
2962
  { relativePath: "src/vite-env.d.ts", content: viteEnvDtsTemplate() },
2148
2963
  { relativePath: "src/main.tsx", content: mainTsxTemplate2(cart) },
@@ -2175,8 +2990,8 @@ __export(chrome_extension_exports, {
2175
2990
  scaffoldChromeExtension: () => scaffoldChromeExtension
2176
2991
  });
2177
2992
  import path3 from "path";
2178
- import chalk3 from "chalk";
2179
- import { createSpinner as createSpinner2 } from "nanospinner";
2993
+ import chalk4 from "chalk";
2994
+ import { createSpinner as createSpinner3 } from "nanospinner";
2180
2995
  var scaffoldChromeExtension;
2181
2996
  var init_chrome_extension = __esm({
2182
2997
  "src/scaffold/chrome-extension/index.ts"() {
@@ -2194,33 +3009,33 @@ var init_chrome_extension = __esm({
2194
3009
  `Directory "${projectName}" already exists. Please choose a different project name or remove the existing directory.`
2195
3010
  );
2196
3011
  }
2197
- const spinner = createSpinner2(`Scaffolding ${chalk3.cyan(projectName)}...`).start();
3012
+ const spinner = createSpinner3(`Scaffolding ${chalk4.cyan(projectName)}...`).start();
2198
3013
  try {
2199
3014
  const fileMap = getChromeExtensionFileMap(cart);
2200
3015
  for (const { relativePath, content } of fileMap) {
2201
3016
  await writeProjectFile(projectRoot, relativePath, content);
2202
3017
  }
2203
3018
  spinner.success({
2204
- text: chalk3.green(`Project ${chalk3.bold(projectName)} created successfully!`)
3019
+ text: chalk4.green(`Project ${chalk4.bold(projectName)} created successfully!`)
2205
3020
  });
2206
3021
  console.log("");
2207
- console.log(chalk3.whiteBright("Next steps:"));
2208
- console.log(chalk3.cyan(` cd ${projectName}`));
2209
- console.log(chalk3.cyan(" npm install"));
2210
- console.log(chalk3.cyan(" npm run dev"));
3022
+ console.log(chalk4.whiteBright("Next steps:"));
3023
+ console.log(chalk4.cyan(` cd ${projectName}`));
3024
+ console.log(chalk4.cyan(" npm install"));
3025
+ console.log(chalk4.cyan(" npm run dev"));
2211
3026
  console.log("");
2212
- console.log(chalk3.whiteBright("To build the extension:"));
2213
- console.log(chalk3.cyan(" npm run build-extension"));
2214
- console.log(chalk3.gray(" Then load the dist/ folder in Chrome at chrome://extensions"));
3027
+ console.log(chalk4.whiteBright("To build the extension:"));
3028
+ console.log(chalk4.cyan(" npm run build-extension"));
3029
+ console.log(chalk4.gray(" Then load the dist/ folder in Chrome at chrome://extensions"));
2215
3030
  console.log("");
2216
3031
  } catch (err) {
2217
- spinner.error({ text: chalk3.red("Scaffolding failed.") });
3032
+ spinner.error({ text: chalk4.red("Scaffolding failed.") });
2218
3033
  if (err instanceof ScaffoldError) {
2219
- console.error(chalk3.red(err.message));
3034
+ console.error(chalk4.red(err.message));
2220
3035
  } else if (isNodeError(err)) {
2221
- console.error(chalk3.red(`File system error (${err.code}): ${err.message}`));
3036
+ console.error(chalk4.red(`File system error (${err.code}): ${err.message}`));
2222
3037
  } else {
2223
- console.error(chalk3.red("An unexpected error occurred."), err);
3038
+ console.error(chalk4.red("An unexpected error occurred."), err);
2224
3039
  }
2225
3040
  try {
2226
3041
  const { rm } = await import("fs/promises");
@@ -2239,8 +3054,8 @@ __export(chrome_extension_exports2, {
2239
3054
  flowChromeExtension: () => flowChromeExtension
2240
3055
  });
2241
3056
  import { select as select2, input as input2, Separator as Separator2 } from "@inquirer/prompts";
2242
- import chalk4 from "chalk";
2243
- var selectFromMenu2, menuProjectName2, menuStateManagement2, menuQuery2, menuCss2, menuLinter2, flowChromeExtension;
3057
+ import chalk5 from "chalk";
3058
+ var selectFromMenu2, menuProjectName2, menuStateManagement2, menuQuery2, menuCss2, menuLinter2, menuAI2, flowChromeExtension;
2244
3059
  var init_chrome_extension2 = __esm({
2245
3060
  "src/options/chrome-extension/index.ts"() {
2246
3061
  "use strict";
@@ -2255,7 +3070,7 @@ var init_chrome_extension2 = __esm({
2255
3070
  description: menuOptions[key].description
2256
3071
  }));
2257
3072
  const answer = await select2({
2258
- message: chalk4.whiteBright(message),
3073
+ message: chalk5.whiteBright(message),
2259
3074
  choices: [...choices, new Separator2()]
2260
3075
  });
2261
3076
  return answer;
@@ -2263,7 +3078,7 @@ var init_chrome_extension2 = __esm({
2263
3078
  menuProjectName2 = async (cart) => {
2264
3079
  if (!cart || cart.type !== MENU_OPTIONS_LEVEL_1.ChromeExtension.value) return;
2265
3080
  cart.projectName = await input2({
2266
- message: chalk4.whiteBright("Project name:"),
3081
+ message: chalk5.whiteBright("Project name:"),
2267
3082
  validate: (value) => {
2268
3083
  if (!value.trim()) return "Project name cannot be empty";
2269
3084
  if (!/^[a-zA-Z0-9_-]+$/.test(value.trim()))
@@ -2292,6 +3107,10 @@ var init_chrome_extension2 = __esm({
2292
3107
  if (!cart || cart.type !== MENU_OPTIONS_LEVEL_1.ChromeExtension.value) return;
2293
3108
  cart.linter = await selectFromMenu2(CHROME_MENU_LINTER, "Choose a Linter / Formatter");
2294
3109
  };
3110
+ menuAI2 = async (cart) => {
3111
+ if (!cart || cart.type !== MENU_OPTIONS_LEVEL_1.ChromeExtension.value) return;
3112
+ cart.ai = await selectFromMenu2(CHROME_MENU_AI, "Choose an AI setup");
3113
+ };
2295
3114
  flowChromeExtension = async (cart) => {
2296
3115
  if (!cart || cart.type !== MENU_OPTIONS_LEVEL_1.ChromeExtension.value) return;
2297
3116
  await menuProjectName2(cart);
@@ -2299,16 +3118,62 @@ var init_chrome_extension2 = __esm({
2299
3118
  await menuQuery2(cart);
2300
3119
  await menuCss2(cart);
2301
3120
  await menuLinter2(cart);
3121
+ await menuAI2(cart);
2302
3122
  const { scaffoldChromeExtension: scaffoldChromeExtension2 } = await Promise.resolve().then(() => (init_chrome_extension(), chrome_extension_exports));
2303
3123
  await scaffoldChromeExtension2(cart);
2304
3124
  };
2305
3125
  }
2306
3126
  });
2307
3127
 
3128
+ // src/commands/update.ts
3129
+ import { exec } from "child_process";
3130
+ import { promisify } from "util";
3131
+ import chalk from "chalk";
3132
+ import { createSpinner } from "nanospinner";
3133
+ var execAsync = promisify(exec);
3134
+ var PACKAGE_NAME = "beaver-build";
3135
+ var runUpdate = async (currentVersion) => {
3136
+ const spinner = createSpinner("Checking for updates...").start();
3137
+ let latestVersion;
3138
+ try {
3139
+ const { stdout } = await execAsync(`npm view ${PACKAGE_NAME} version`);
3140
+ latestVersion = stdout.trim();
3141
+ } catch {
3142
+ spinner.error({ text: chalk.red("Failed to check the latest version from npm.") });
3143
+ console.error(chalk.red("Please check your network connection and try again."));
3144
+ process.exit(1);
3145
+ }
3146
+ if (latestVersion === currentVersion) {
3147
+ spinner.success({
3148
+ text: chalk.green(`Already on the latest version (v${currentVersion}).`)
3149
+ });
3150
+ return;
3151
+ }
3152
+ spinner.update({
3153
+ text: `Updating ${chalk.cyan(PACKAGE_NAME)} v${currentVersion} \u2192 v${latestVersion}...`
3154
+ });
3155
+ try {
3156
+ await execAsync(`npm install -g ${PACKAGE_NAME}@latest`);
3157
+ spinner.success({
3158
+ text: chalk.green(`Updated to ${chalk.bold(`v${latestVersion}`)}!`)
3159
+ });
3160
+ } catch (err) {
3161
+ spinner.error({ text: chalk.red("Update failed.") });
3162
+ const message = err instanceof Error ? err.message : String(err);
3163
+ if (/EACCES|permission/i.test(message)) {
3164
+ console.error(chalk.red("Permission denied while installing globally."));
3165
+ console.error(chalk.yellow(`Try: sudo npm install -g ${PACKAGE_NAME}@latest`));
3166
+ } else {
3167
+ console.error(chalk.red(message));
3168
+ }
3169
+ process.exit(1);
3170
+ }
3171
+ };
3172
+
2308
3173
  // src/options/index.ts
2309
3174
  init_constants();
2310
3175
  import { select as select3, Separator as Separator3 } from "@inquirer/prompts";
2311
- import chalk5 from "chalk";
3176
+ import chalk6 from "chalk";
2312
3177
  var menu = async () => {
2313
3178
  const keys = Object.keys(MENU_OPTIONS_LEVEL_1);
2314
3179
  const enabledKeys = keys.filter((key) => !MENU_OPTIONS_LEVEL_1[key].disabled);
@@ -2326,7 +3191,7 @@ var menu = async () => {
2326
3191
  }
2327
3192
  ];
2328
3193
  const answer = await select3({
2329
- message: chalk5.whiteBright("How can I help you \u{1F468}\u200D\u{1F373}"),
3194
+ message: chalk6.whiteBright("How can I help you \u{1F468}\u200D\u{1F373}"),
2330
3195
  choices
2331
3196
  });
2332
3197
  const cart = { type: answer };
@@ -2352,6 +3217,23 @@ var typeWriter = async (text, colorFunc, speed = 50) => {
2352
3217
  process.stdout.write("\n");
2353
3218
  };
2354
3219
 
3220
+ // src/utils/check-node-version.ts
3221
+ import chalk7 from "chalk";
3222
+ var MIN_NODE_MAJOR = 20;
3223
+ var checkNodeVersion = () => {
3224
+ const major = parseInt(process.version.slice(1).split(".")[0], 10);
3225
+ if (major < MIN_NODE_MAJOR) {
3226
+ console.error(
3227
+ chalk7.red(
3228
+ `\u2716 beaver requires Node.js v${MIN_NODE_MAJOR} or higher.
3229
+ You are running ${process.version}.
3230
+ Please upgrade Node.js: https://nodejs.org`
3231
+ )
3232
+ );
3233
+ process.exit(1);
3234
+ }
3235
+ };
3236
+
2355
3237
  // src/utils/user.ts
2356
3238
  import { execSync } from "child_process";
2357
3239
  var getUserName = () => {
@@ -2360,7 +3242,7 @@ var getUserName = () => {
2360
3242
  };
2361
3243
 
2362
3244
  // src/index.ts
2363
- import chalk6 from "chalk";
3245
+ import chalk8 from "chalk";
2364
3246
  import { readFileSync } from "fs";
2365
3247
  import { dirname, join } from "path";
2366
3248
  import { fileURLToPath } from "url";
@@ -2374,6 +3256,7 @@ var getVersion = () => {
2374
3256
  }
2375
3257
  };
2376
3258
  var main = async () => {
3259
+ checkNodeVersion();
2377
3260
  const args = process.argv.slice(2);
2378
3261
  if (args.includes("--version") || args.includes("-v")) {
2379
3262
  console.log(`beaver-build v${getVersion()}`);
@@ -2384,7 +3267,10 @@ var main = async () => {
2384
3267
  beaver-build - Interactive CLI for scaffolding modern web projects
2385
3268
 
2386
3269
  Usage:
2387
- beaver [options]
3270
+ beaver [command] [options]
3271
+
3272
+ Commands:
3273
+ update Update beaver to the latest version from npm
2388
3274
 
2389
3275
  Options:
2390
3276
  -v, --version Show version
@@ -2392,13 +3278,17 @@ Options:
2392
3278
  `);
2393
3279
  process.exit(0);
2394
3280
  }
2395
- await typeWriter(`Hi! ${getUserName()} \u{1F646}`, chalk6.whiteBright, 50);
3281
+ if (args[0] === "update") {
3282
+ await runUpdate(getVersion());
3283
+ process.exit(0);
3284
+ }
3285
+ await typeWriter(`Hi! ${getUserName()} \u{1F646}`, chalk8.whiteBright, 50);
2396
3286
  await sleep(500);
2397
3287
  try {
2398
3288
  await menu();
2399
3289
  } catch (err) {
2400
3290
  if (err instanceof Error) {
2401
- console.error(chalk6.red(err.message));
3291
+ console.error(chalk8.red(err.message));
2402
3292
  }
2403
3293
  process.exit(1);
2404
3294
  }