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.
- package/README.md +237 -237
- package/dist/index.js +1378 -488
- 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/
|
|
718
|
-
var
|
|
719
|
-
var
|
|
720
|
-
"src/scaffold/
|
|
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
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
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
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
882
|
+
export function firstHeading(content) {
|
|
883
|
+
const match = content.match(/^#\\s+(.+)$/m);
|
|
884
|
+
return match ? match[1].trim() : null;
|
|
885
|
+
}
|
|
797
886
|
|
|
798
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1097
|
+
## Finding docs (DOCS-FIRST)
|
|
815
1098
|
|
|
816
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1125
|
+
You are the documentation agent for ${projectName}. You own \`docs/\`.
|
|
827
1126
|
|
|
828
|
-
|
|
1127
|
+
## Onboarding protocol
|
|
829
1128
|
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1143
|
+
## Hard rules
|
|
843
1144
|
|
|
844
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
855
|
-
|
|
856
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1317
|
+
## Behavioral Guidelines
|
|
869
1318
|
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
-
|
|
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
|
-
|
|
1324
|
+
## Project Overview
|
|
880
1325
|
|
|
881
|
-
|
|
1326
|
+
${stack.map((s) => `- ${s}`).join("\n")}
|
|
882
1327
|
|
|
883
|
-
|
|
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
|
-
|
|
891
|
-
## When unsure
|
|
1330
|
+
## Commands
|
|
892
1331
|
|
|
893
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1336
|
+
${isFsd ? fsdArchitecture : bprArchitecture}
|
|
933
1337
|
|
|
934
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
1342
|
+
## Naming Conventions
|
|
967
1343
|
|
|
968
|
-
${
|
|
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
|
-
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
1356
|
+
${testSection}## Agent Routing
|
|
990
1357
|
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
1369
|
+
## Further Reading + DOCS-FIRST RULE
|
|
1009
1370
|
|
|
1010
|
-
\`
|
|
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
|
-
|
|
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
|
-
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
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
|
-
|
|
1392
|
+
# Home Page \u2014 Feature Spec
|
|
1022
1393
|
|
|
1023
|
-
|
|
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
|
-
|
|
1397
|
+
## Root Cause / Key Finding
|
|
1398
|
+
_(For feature specs, use this section for key constraints or discoveries.)_
|
|
1026
1399
|
|
|
1027
|
-
|
|
1028
|
-
|
|
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
|
-
|
|
1031
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1421
|
+
# ${cart.projectName} Conventions
|
|
1040
1422
|
|
|
1041
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
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
|
-
|
|
1436
|
+
## Import alias
|
|
1081
1437
|
|
|
1082
|
-
Use
|
|
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
|
-
|
|
1440
|
+
${cart.stateManagement === "ZUSTAND" ? `## Zustand
|
|
1090
1441
|
|
|
1091
|
-
-
|
|
1092
|
-
-
|
|
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
|
-
|
|
1446
|
+
` : ""}${cart.query === "TANSTACK_QUERY" ? `## TanStack Query
|
|
1095
1447
|
|
|
1096
|
-
|
|
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
|
-
|
|
1099
|
-
@theme {
|
|
1100
|
-
--animate-slide-in: slide-in 0.3s ease-out;
|
|
1452
|
+
` : ""}${cart.router === "TANSTACK_ROUTER" ? `## TanStack Router
|
|
1101
1453
|
|
|
1102
|
-
|
|
1103
|
-
|
|
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
|
-
|
|
1457
|
+
` : ""}${cart.css === "TAILWIND" ? `## Tailwind CSS v4
|
|
1110
1458
|
|
|
1111
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1475
|
+
# ${cart.projectName} Test Conventions
|
|
1123
1476
|
|
|
1124
|
-
|
|
1477
|
+
## Layout
|
|
1125
1478
|
|
|
1126
|
-
\`\`\`
|
|
1127
|
-
|
|
1128
|
-
|
|
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
|
-
|
|
1485
|
+
## Rules
|
|
1133
1486
|
|
|
1134
|
-
|
|
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
|
-
|
|
1508
|
+
You are the implementation agent for ${cart.projectName}.
|
|
1137
1509
|
|
|
1138
|
-
|
|
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
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1155
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1163
|
-
|
|
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
|
-
|
|
1542
|
+
You are the test-writing agent for ${cart.projectName}. You write ONLY under \`test/\`.
|
|
1166
1543
|
|
|
1167
|
-
##
|
|
1544
|
+
## Onboarding protocol
|
|
1168
1545
|
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
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
|
-
##
|
|
1550
|
+
## Workflow
|
|
1179
1551
|
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
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
|
-
|
|
1558
|
+
## Hard rules
|
|
1185
1559
|
|
|
1186
|
-
\
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
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
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
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
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
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
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1639
|
+
unitHomeSpecMdTemplate = () => `# home.test.tsx \u2014 Contract
|
|
1640
|
+
|
|
1641
|
+
Traces to: \`docs/features/home/home.spec.en.md\`
|
|
1207
1642
|
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
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
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
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
|
-
|
|
1227
|
-
const
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
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 (
|
|
1262
|
-
files.push(
|
|
1263
|
-
relativePath:
|
|
1264
|
-
content:
|
|
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
|
-
|
|
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
|
-
...
|
|
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
|
-
|
|
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
|
-
...
|
|
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
|
|
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 =
|
|
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:
|
|
2209
|
+
text: chalk2.green(`Project ${chalk2.bold(projectName)} created successfully!`)
|
|
1706
2210
|
});
|
|
1707
2211
|
console.log("");
|
|
1708
|
-
console.log(
|
|
1709
|
-
console.log(
|
|
1710
|
-
console.log(
|
|
1711
|
-
console.log(
|
|
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(
|
|
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:
|
|
2222
|
+
spinner.error({ text: chalk2.red("Scaffolding failed.") });
|
|
1719
2223
|
if (err instanceof ScaffoldError) {
|
|
1720
|
-
console.error(
|
|
2224
|
+
console.error(chalk2.red(err.message));
|
|
1721
2225
|
} else if (isNodeError(err)) {
|
|
1722
|
-
console.error(
|
|
2226
|
+
console.error(chalk2.red(`File system error (${err.code}): ${err.message}`));
|
|
1723
2227
|
} else {
|
|
1724
|
-
console.error(
|
|
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
|
|
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:
|
|
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:
|
|
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
|
|
2179
|
-
import { createSpinner as
|
|
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 =
|
|
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:
|
|
3019
|
+
text: chalk4.green(`Project ${chalk4.bold(projectName)} created successfully!`)
|
|
2205
3020
|
});
|
|
2206
3021
|
console.log("");
|
|
2207
|
-
console.log(
|
|
2208
|
-
console.log(
|
|
2209
|
-
console.log(
|
|
2210
|
-
console.log(
|
|
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(
|
|
2213
|
-
console.log(
|
|
2214
|
-
console.log(
|
|
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:
|
|
3032
|
+
spinner.error({ text: chalk4.red("Scaffolding failed.") });
|
|
2218
3033
|
if (err instanceof ScaffoldError) {
|
|
2219
|
-
console.error(
|
|
3034
|
+
console.error(chalk4.red(err.message));
|
|
2220
3035
|
} else if (isNodeError(err)) {
|
|
2221
|
-
console.error(
|
|
3036
|
+
console.error(chalk4.red(`File system error (${err.code}): ${err.message}`));
|
|
2222
3037
|
} else {
|
|
2223
|
-
console.error(
|
|
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
|
|
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:
|
|
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:
|
|
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
|
|
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:
|
|
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
|
|
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
|
-
|
|
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(
|
|
3291
|
+
console.error(chalk8.red(err.message));
|
|
2402
3292
|
}
|
|
2403
3293
|
process.exit(1);
|
|
2404
3294
|
}
|