ai-forge-cli 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +148 -0
  3. package/dist/add-feature-YXWSRIVE.js +141 -0
  4. package/dist/check-RCJRXIU5.js +377 -0
  5. package/dist/chunk-J4V5PGVT.js +55 -0
  6. package/dist/chunk-PIFX2L5H.js +46 -0
  7. package/dist/index.d.ts +2 -0
  8. package/dist/index.js +17 -0
  9. package/dist/init-GQA3WDXQ.js +114 -0
  10. package/dist/templates/feature/convex/index.ts.hbs +3 -0
  11. package/dist/templates/feature/convex/mutations.ts.hbs +37 -0
  12. package/dist/templates/feature/convex/queries.ts.hbs +16 -0
  13. package/dist/templates/feature/convex/schema.ts.hbs +10 -0
  14. package/dist/templates/feature/routes/$id.tsx.hbs +27 -0
  15. package/dist/templates/feature/routes/index.tsx.hbs +21 -0
  16. package/dist/templates/feature/src/components/index.ts.hbs +4 -0
  17. package/dist/templates/feature/src/hooks.ts.hbs +29 -0
  18. package/dist/templates/feature/src/index.ts.hbs +2 -0
  19. package/dist/templates/init/app/client.tsx.hbs +7 -0
  20. package/dist/templates/init/app/router.tsx.hbs +17 -0
  21. package/dist/templates/init/app/routes/__root.tsx.hbs +38 -0
  22. package/dist/templates/init/app/routes/index.tsx.hbs +18 -0
  23. package/dist/templates/init/app/ssr.tsx.hbs +11 -0
  24. package/dist/templates/init/app.config.ts.hbs +8 -0
  25. package/dist/templates/init/biome.json.hbs +32 -0
  26. package/dist/templates/init/claude.md.hbs +93 -0
  27. package/dist/templates/init/convex/schema.ts.hbs +7 -0
  28. package/dist/templates/init/package.json.hbs +34 -0
  29. package/dist/templates/init/postcss.config.js.hbs +6 -0
  30. package/dist/templates/init/src/lib/cn.ts.hbs +6 -0
  31. package/dist/templates/init/src/providers/index.tsx.hbs +10 -0
  32. package/dist/templates/init/tailwind.config.ts.hbs +12 -0
  33. package/dist/templates/init/tsconfig.json.hbs +24 -0
  34. package/package.json +59 -0
  35. package/templates/feature/convex/index.ts.hbs +3 -0
  36. package/templates/feature/convex/mutations.ts.hbs +37 -0
  37. package/templates/feature/convex/queries.ts.hbs +16 -0
  38. package/templates/feature/convex/schema.ts.hbs +10 -0
  39. package/templates/feature/routes/$id.tsx.hbs +27 -0
  40. package/templates/feature/routes/index.tsx.hbs +21 -0
  41. package/templates/feature/src/components/index.ts.hbs +4 -0
  42. package/templates/feature/src/hooks.ts.hbs +29 -0
  43. package/templates/feature/src/index.ts.hbs +2 -0
  44. package/templates/init/app/client.tsx.hbs +7 -0
  45. package/templates/init/app/router.tsx.hbs +17 -0
  46. package/templates/init/app/routes/__root.tsx.hbs +38 -0
  47. package/templates/init/app/routes/index.tsx.hbs +18 -0
  48. package/templates/init/app/ssr.tsx.hbs +11 -0
  49. package/templates/init/app.config.ts.hbs +8 -0
  50. package/templates/init/biome.json.hbs +32 -0
  51. package/templates/init/claude.md.hbs +93 -0
  52. package/templates/init/convex/schema.ts.hbs +7 -0
  53. package/templates/init/package.json.hbs +34 -0
  54. package/templates/init/postcss.config.js.hbs +6 -0
  55. package/templates/init/src/lib/cn.ts.hbs +6 -0
  56. package/templates/init/src/providers/index.tsx.hbs +10 -0
  57. package/templates/init/tailwind.config.ts.hbs +12 -0
  58. package/templates/init/tsconfig.json.hbs +24 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Pablo García
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,148 @@
1
+ # Forge CLI
2
+
3
+ TypeScript stack scaffolding & enforcement CLI for **TanStack Start + Convex + Tailwind**.
4
+
5
+ The key insight: Claude Code reads `CLAUDE.md` automatically. Forge uses this as a **hook** — imperative instructions that tell Claude it MUST use the forge CLI commands. No more AI drift.
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ npm install -g forge-cli
11
+ ```
12
+
13
+ Or use with npx:
14
+
15
+ ```bash
16
+ npx forge-cli init my-app
17
+ ```
18
+
19
+ ## Commands
20
+
21
+ ### `forge init <project-name>`
22
+
23
+ Creates a complete project with:
24
+ - TanStack Start (file-based routing, SSR-ready)
25
+ - Convex (real-time database)
26
+ - Tailwind CSS + shadcn/ui ready
27
+ - Biome (linting/formatting)
28
+ - TypeScript strict mode
29
+ - **CLAUDE.md** — the AI hook
30
+
31
+ ```bash
32
+ forge init my-app
33
+ cd my-app
34
+ pnpm install
35
+ npx convex init
36
+ pnpm dlx shadcn@latest init
37
+ pnpm dev
38
+ ```
39
+
40
+ ### `forge add:feature <name>`
41
+
42
+ Creates a full vertical slice:
43
+
44
+ ```
45
+ convex/features/<name>/
46
+ ├── schema.ts # Table definition (exports <name>Tables)
47
+ ├── queries.ts # All queries
48
+ ├── mutations.ts # All mutations
49
+ └── index.ts # Barrel export
50
+
51
+ src/features/<name>/
52
+ ├── components/
53
+ │ └── index.ts # Component exports
54
+ ├── hooks.ts # Feature hooks
55
+ └── index.ts # Barrel export
56
+
57
+ app/routes/<name>/
58
+ ├── index.tsx # List view
59
+ └── $id.tsx # Detail view
60
+ ```
61
+
62
+ Also auto-registers the schema in `convex/schema.ts`.
63
+
64
+ ```bash
65
+ forge add:feature projects
66
+
67
+ # Output:
68
+ ✓ Created convex/features/projects/schema.ts
69
+ ✓ Created convex/features/projects/queries.ts
70
+ ✓ Created convex/features/projects/mutations.ts
71
+ ✓ Created convex/features/projects/index.ts
72
+ ✓ Created src/features/projects/components/index.ts
73
+ ✓ Created src/features/projects/hooks.ts
74
+ ✓ Created src/features/projects/index.ts
75
+ ✓ Created app/routes/projects/index.tsx
76
+ ✓ Created app/routes/projects/$id.tsx
77
+ ✓ Updated convex/schema.ts
78
+ ```
79
+
80
+ ### `forge check`
81
+
82
+ Validates project structure. Fails if rules are broken. Use in CI/pre-commit.
83
+
84
+ ```bash
85
+ forge check
86
+
87
+ # Output:
88
+ ✓ Feature structure valid
89
+ ✓ Component locations valid
90
+ ✓ Hook locations valid
91
+ ✓ Thin routes valid
92
+ ✓ Cross-feature imports valid
93
+ ✓ Feature parity valid
94
+
95
+ All checks passed!
96
+ ```
97
+
98
+ ## Validation Rules
99
+
100
+ 1. **Feature structure** — Every feature has required files
101
+ 2. **Component location** — Components only in `src/features/*/components/` or `src/components/`
102
+ 3. **Hook location** — Hooks only in `src/features/*/hooks.ts` or `src/hooks/`
103
+ 4. **Thin routes** — Route files can only import from features/components, no business logic
104
+ 5. **No cross-feature imports** — `src/features/X/` cannot import from `src/features/Y/`
105
+ 6. **Feature parity** — Every `src/features/X` has matching `convex/features/X`
106
+
107
+ ## The CLAUDE.md Hook
108
+
109
+ When you run `forge init`, it generates a `CLAUDE.md` file that instructs Claude Code to:
110
+
111
+ 1. **Always** run `forge add:feature <name>` before building any feature
112
+ 2. **Never** create feature files manually
113
+ 3. **Always** run `forge check` before completing any task
114
+
115
+ This eliminates AI drift and enforces consistent architecture.
116
+
117
+ ```markdown
118
+ # From generated CLAUDE.md
119
+
120
+ ## YOU MUST USE FORGE CLI
121
+
122
+ When the user asks you to build ANY feature, you MUST:
123
+ 1. FIRST run `forge add:feature <name>`
124
+ 2. THEN fill in the generated files
125
+ 3. NEVER create feature files manually
126
+ ```
127
+
128
+ ## Project Structure
129
+
130
+ ```
131
+ app/routes/ → Thin route files (import from features, no logic)
132
+ src/features/ → All feature code (components, hooks, types)
133
+ src/components/ → Shared UI only (used across features)
134
+ src/lib/ → Pure utilities
135
+ convex/features/ → Backend mirrors frontend features
136
+ ```
137
+
138
+ ## Stack
139
+
140
+ - **Frontend**: TanStack Start
141
+ - **Backend**: Convex
142
+ - **Styling**: Tailwind CSS + shadcn/ui
143
+ - **Linting**: Biome
144
+ - **Language**: TypeScript (strict)
145
+
146
+ ## License
147
+
148
+ MIT
@@ -0,0 +1,141 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ camelCase,
4
+ kebabCase,
5
+ renderTemplate
6
+ } from "./chunk-PIFX2L5H.js";
7
+ import {
8
+ fileExists,
9
+ logger,
10
+ readFile,
11
+ writeFile
12
+ } from "./chunk-J4V5PGVT.js";
13
+
14
+ // src/commands/add-feature.ts
15
+ import { defineCommand } from "citty";
16
+ import { join } from "path";
17
+ var add_feature_default = defineCommand({
18
+ meta: {
19
+ name: "add:feature",
20
+ description: "Create a new feature with vertical slice architecture"
21
+ },
22
+ args: {
23
+ name: {
24
+ type: "positional",
25
+ description: "Name of the feature (will be converted to kebab-case)",
26
+ required: true
27
+ }
28
+ },
29
+ async run({ args }) {
30
+ const rawName = args.name;
31
+ const name = kebabCase(rawName);
32
+ const cwd = process.cwd();
33
+ const srcFeaturePath = join(cwd, "src/features", name);
34
+ const convexFeaturePath = join(cwd, "convex/features", name);
35
+ if (await fileExists(srcFeaturePath)) {
36
+ logger.error(`Feature "${name}" already exists at src/features/${name}`);
37
+ process.exit(1);
38
+ }
39
+ if (await fileExists(convexFeaturePath)) {
40
+ logger.error(`Feature "${name}" already exists at convex/features/${name}`);
41
+ process.exit(1);
42
+ }
43
+ logger.blank();
44
+ const templateData = { name };
45
+ const files = [
46
+ // Convex files
47
+ {
48
+ templatePath: "feature/convex/schema.ts.hbs",
49
+ destPath: join(cwd, "convex/features", name, "schema.ts")
50
+ },
51
+ {
52
+ templatePath: "feature/convex/queries.ts.hbs",
53
+ destPath: join(cwd, "convex/features", name, "queries.ts")
54
+ },
55
+ {
56
+ templatePath: "feature/convex/mutations.ts.hbs",
57
+ destPath: join(cwd, "convex/features", name, "mutations.ts")
58
+ },
59
+ {
60
+ templatePath: "feature/convex/index.ts.hbs",
61
+ destPath: join(cwd, "convex/features", name, "index.ts")
62
+ },
63
+ // Src files
64
+ {
65
+ templatePath: "feature/src/components/index.ts.hbs",
66
+ destPath: join(cwd, "src/features", name, "components/index.ts")
67
+ },
68
+ {
69
+ templatePath: "feature/src/hooks.ts.hbs",
70
+ destPath: join(cwd, "src/features", name, "hooks.ts")
71
+ },
72
+ {
73
+ templatePath: "feature/src/index.ts.hbs",
74
+ destPath: join(cwd, "src/features", name, "index.ts")
75
+ },
76
+ // Route files
77
+ {
78
+ templatePath: "feature/routes/index.tsx.hbs",
79
+ destPath: join(cwd, "app/routes", name, "index.tsx")
80
+ },
81
+ {
82
+ templatePath: "feature/routes/$id.tsx.hbs",
83
+ destPath: join(cwd, "app/routes", name, "$id.tsx")
84
+ }
85
+ ];
86
+ for (const file of files) {
87
+ const content = renderTemplate(file.templatePath, templateData);
88
+ await writeFile(file.destPath, content);
89
+ const relativePath = file.destPath.replace(cwd + "/", "");
90
+ logger.success(`Created ${relativePath}`);
91
+ }
92
+ await updateConvexSchema(cwd, name);
93
+ logger.blank();
94
+ logger.log(` Feature "${name}" created successfully.`);
95
+ logger.blank();
96
+ logger.log(" Next steps:");
97
+ logger.log(` 1. Define your schema in convex/features/${name}/schema.ts`);
98
+ logger.log(" 2. Run `npx convex dev` to sync");
99
+ logger.log(` 3. Build components in src/features/${name}/components/`);
100
+ logger.blank();
101
+ }
102
+ });
103
+ async function updateConvexSchema(cwd, featureName) {
104
+ const schemaPath = join(cwd, "convex/schema.ts");
105
+ const camelName = camelCase(featureName);
106
+ if (!await fileExists(schemaPath)) {
107
+ logger.warn("convex/schema.ts not found - skipping schema registration");
108
+ return;
109
+ }
110
+ let content = await readFile(schemaPath);
111
+ const importLine = `import { ${camelName}Tables } from "./features/${featureName}/schema";`;
112
+ const exportDefault = "export default defineSchema({";
113
+ if (content.includes(importLine)) {
114
+ logger.warn("Schema import already exists - skipping");
115
+ return;
116
+ }
117
+ const exportPos = content.indexOf(exportDefault);
118
+ if (exportPos === -1) {
119
+ logger.warn("Could not find 'export default defineSchema({' in schema.ts - skipping");
120
+ return;
121
+ }
122
+ content = content.slice(0, exportPos) + importLine + "\n\n" + content.slice(exportPos);
123
+ const spreadLine = ` ...${camelName}Tables,`;
124
+ const defineSchemaStart = content.indexOf(exportDefault);
125
+ const afterDefineSchema = content.slice(defineSchemaStart + exportDefault.length);
126
+ const closingIndex = content.lastIndexOf("});");
127
+ if (closingIndex === -1) {
128
+ logger.warn("Could not find closing '});' in schema.ts - skipping spread");
129
+ return;
130
+ }
131
+ if (content.includes(spreadLine)) {
132
+ logger.warn("Schema spread already exists - skipping");
133
+ } else {
134
+ content = content.slice(0, closingIndex) + spreadLine + "\n" + content.slice(closingIndex);
135
+ }
136
+ await writeFile(schemaPath, content);
137
+ logger.success("Updated convex/schema.ts");
138
+ }
139
+ export {
140
+ add_feature_default as default
141
+ };
@@ -0,0 +1,377 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ fileExists,
4
+ logger,
5
+ readFile
6
+ } from "./chunk-J4V5PGVT.js";
7
+
8
+ // src/commands/check.ts
9
+ import { defineCommand } from "citty";
10
+
11
+ // src/validators/feature-structure.ts
12
+ import { join } from "path";
13
+ import fg from "fast-glob";
14
+ var REQUIRED_SRC_FILES = ["components/index.ts", "hooks.ts", "index.ts"];
15
+ var REQUIRED_CONVEX_FILES = ["schema.ts", "queries.ts", "mutations.ts", "index.ts"];
16
+ var featureStructureValidator = {
17
+ name: "Feature structure",
18
+ async validate(cwd) {
19
+ const errors = [];
20
+ const warnings = [];
21
+ const srcFeatures = await fg("src/features/*", {
22
+ cwd,
23
+ onlyDirectories: true
24
+ });
25
+ for (const featurePath of srcFeatures) {
26
+ const featureName = featurePath.split("/").pop();
27
+ for (const requiredFile of REQUIRED_SRC_FILES) {
28
+ const filePath = join(cwd, featurePath, requiredFile);
29
+ if (!await fileExists(filePath)) {
30
+ errors.push({
31
+ file: join(featurePath, requiredFile),
32
+ message: `Missing required file in feature "${featureName}"`
33
+ });
34
+ }
35
+ }
36
+ }
37
+ const convexFeatures = await fg("convex/features/*", {
38
+ cwd,
39
+ onlyDirectories: true
40
+ });
41
+ for (const featurePath of convexFeatures) {
42
+ const featureName = featurePath.split("/").pop();
43
+ for (const requiredFile of REQUIRED_CONVEX_FILES) {
44
+ const filePath = join(cwd, featurePath, requiredFile);
45
+ if (!await fileExists(filePath)) {
46
+ errors.push({
47
+ file: join(featurePath, requiredFile),
48
+ message: `Missing required file in feature "${featureName}"`
49
+ });
50
+ }
51
+ }
52
+ }
53
+ return {
54
+ rule: this.name,
55
+ passed: errors.length === 0,
56
+ errors,
57
+ warnings
58
+ };
59
+ }
60
+ };
61
+
62
+ // src/validators/locations.ts
63
+ import fg2 from "fast-glob";
64
+ var componentLocationValidator = {
65
+ name: "Component locations",
66
+ async validate(cwd) {
67
+ const errors = [];
68
+ const warnings = [];
69
+ const tsxFiles = await fg2("**/*.tsx", {
70
+ cwd,
71
+ ignore: [
72
+ "node_modules/**",
73
+ "dist/**",
74
+ ".vinxi/**",
75
+ "app/**",
76
+ // TanStack Start entry files and routes
77
+ "src/features/*/components/**",
78
+ // Valid location
79
+ "src/components/**",
80
+ // Valid location
81
+ "src/providers/**"
82
+ // Provider wrappers
83
+ ]
84
+ });
85
+ for (const file of tsxFiles) {
86
+ errors.push({
87
+ file,
88
+ message: "Component outside valid location (should be in src/features/*/components/ or src/components/)"
89
+ });
90
+ }
91
+ return {
92
+ rule: this.name,
93
+ passed: errors.length === 0,
94
+ errors,
95
+ warnings
96
+ };
97
+ }
98
+ };
99
+ var hookLocationValidator = {
100
+ name: "Hook locations",
101
+ async validate(cwd) {
102
+ const errors = [];
103
+ const warnings = [];
104
+ const potentialHookFiles = await fg2(["**/use*.ts", "**/use*.tsx", "**/*hook*.ts", "**/*hook*.tsx"], {
105
+ cwd,
106
+ ignore: [
107
+ "node_modules/**",
108
+ "dist/**",
109
+ "src/features/*/hooks.ts",
110
+ // Valid location
111
+ "src/hooks/**",
112
+ // Valid location
113
+ "src/hooks.ts"
114
+ // Valid location (root hooks file)
115
+ ]
116
+ });
117
+ for (const file of potentialHookFiles) {
118
+ if (file.startsWith("app/routes/")) continue;
119
+ errors.push({
120
+ file,
121
+ message: "Hook file outside valid location (should be in src/features/*/hooks.ts or src/hooks/)"
122
+ });
123
+ }
124
+ return {
125
+ rule: this.name,
126
+ passed: errors.length === 0,
127
+ errors,
128
+ warnings
129
+ };
130
+ }
131
+ };
132
+
133
+ // src/validators/thin-routes.ts
134
+ import { join as join2 } from "path";
135
+ import fg3 from "fast-glob";
136
+ var ALLOWED_IMPORT_PATTERNS = [
137
+ /^~/,
138
+ // ~/features/, ~/components/, ~/lib/
139
+ /^@tanstack\//,
140
+ // @tanstack/react-router, etc.
141
+ /^convex\//,
142
+ // convex/react, etc.
143
+ /^@convex\//,
144
+ // @convex/_generated/
145
+ /^react$/,
146
+ // react
147
+ /^react-dom$/
148
+ // react-dom
149
+ ];
150
+ var LINE_THRESHOLD = 50;
151
+ var thinRoutesValidator = {
152
+ name: "Thin routes",
153
+ async validate(cwd) {
154
+ const errors = [];
155
+ const warnings = [];
156
+ const routeFiles = await fg3("app/routes/**/*.{ts,tsx}", {
157
+ cwd,
158
+ ignore: ["node_modules/**"]
159
+ });
160
+ for (const file of routeFiles) {
161
+ const fullPath = join2(cwd, file);
162
+ const content = await readFile(fullPath);
163
+ const lines = content.split("\n");
164
+ if (lines.length > LINE_THRESHOLD) {
165
+ warnings.push({
166
+ file,
167
+ message: `Route file exceeds ${LINE_THRESHOLD} lines (${lines.length} lines) - consider moving logic to feature`
168
+ });
169
+ }
170
+ const importMatches = content.matchAll(/import\s+.*?\s+from\s+["']([^"']+)["']/g);
171
+ for (const match of importMatches) {
172
+ const importPath = match[1];
173
+ if (importPath.startsWith(".")) continue;
174
+ const isAllowed = ALLOWED_IMPORT_PATTERNS.some((pattern) => pattern.test(importPath));
175
+ if (!isAllowed) {
176
+ errors.push({
177
+ file,
178
+ message: `Invalid import "${importPath}" - routes should only import from ~/features/, ~/components/, ~/lib/, or framework packages`
179
+ });
180
+ }
181
+ }
182
+ }
183
+ return {
184
+ rule: this.name,
185
+ passed: errors.length === 0,
186
+ errors,
187
+ warnings
188
+ };
189
+ }
190
+ };
191
+
192
+ // src/validators/cross-feature.ts
193
+ import { join as join3, dirname } from "path";
194
+ import fg4 from "fast-glob";
195
+ var crossFeatureValidator = {
196
+ name: "Cross-feature imports",
197
+ async validate(cwd) {
198
+ const errors = [];
199
+ const warnings = [];
200
+ const featureFiles = await fg4("src/features/**/*.{ts,tsx}", {
201
+ cwd,
202
+ ignore: ["node_modules/**"]
203
+ });
204
+ for (const file of featureFiles) {
205
+ const parts = file.split("/");
206
+ const featureIndex = parts.indexOf("features");
207
+ if (featureIndex === -1 || featureIndex + 1 >= parts.length) continue;
208
+ const currentFeature = parts[featureIndex + 1];
209
+ const fullPath = join3(cwd, file);
210
+ const content = await readFile(fullPath);
211
+ const importMatches = content.matchAll(/import\s+.*?\s+from\s+["']([^"']+)["']/g);
212
+ for (const match of importMatches) {
213
+ const importPath = match[1];
214
+ const tildeMatch = importPath.match(/^~\/features\/([^/]+)/);
215
+ if (tildeMatch) {
216
+ const importedFeature = tildeMatch[1];
217
+ if (importedFeature !== currentFeature) {
218
+ errors.push({
219
+ file,
220
+ message: `Cross-feature import from "${currentFeature}" to "${importedFeature}"`
221
+ });
222
+ }
223
+ continue;
224
+ }
225
+ if (importPath.startsWith("..")) {
226
+ const fileDir = dirname(file);
227
+ const resolvedParts = resolveRelativePath(fileDir, importPath);
228
+ if (resolvedParts.length >= 3 && resolvedParts[0] === "src" && resolvedParts[1] === "features") {
229
+ const importedFeature = resolvedParts[2];
230
+ if (importedFeature !== currentFeature) {
231
+ errors.push({
232
+ file,
233
+ message: `Cross-feature import from "${currentFeature}" to "${importedFeature}" via relative path`
234
+ });
235
+ }
236
+ }
237
+ }
238
+ }
239
+ }
240
+ return {
241
+ rule: this.name,
242
+ passed: errors.length === 0,
243
+ errors,
244
+ warnings
245
+ };
246
+ }
247
+ };
248
+ function resolveRelativePath(fromDir, relativePath) {
249
+ const fromParts = fromDir.split("/").filter(Boolean);
250
+ const relativeParts = relativePath.split("/").filter(Boolean);
251
+ const result = [...fromParts];
252
+ for (const part of relativeParts) {
253
+ if (part === "..") {
254
+ result.pop();
255
+ } else if (part !== ".") {
256
+ result.push(part);
257
+ }
258
+ }
259
+ return result;
260
+ }
261
+
262
+ // src/validators/feature-parity.ts
263
+ import fg5 from "fast-glob";
264
+ var featureParityValidator = {
265
+ name: "Feature parity",
266
+ async validate(cwd) {
267
+ const errors = [];
268
+ const warnings = [];
269
+ const srcFeatures = await fg5("src/features/*", {
270
+ cwd,
271
+ onlyDirectories: true
272
+ });
273
+ const srcFeatureNames = new Set(srcFeatures.map((f) => f.split("/").pop()));
274
+ const convexFeatures = await fg5("convex/features/*", {
275
+ cwd,
276
+ onlyDirectories: true
277
+ });
278
+ const convexFeatureNames = new Set(convexFeatures.map((f) => f.split("/").pop()));
279
+ for (const name of srcFeatureNames) {
280
+ if (!convexFeatureNames.has(name)) {
281
+ errors.push({
282
+ file: `src/features/${name}`,
283
+ message: `Feature "${name}" exists in src/features but not in convex/features`
284
+ });
285
+ }
286
+ }
287
+ for (const name of convexFeatureNames) {
288
+ if (!srcFeatureNames.has(name)) {
289
+ errors.push({
290
+ file: `convex/features/${name}`,
291
+ message: `Feature "${name}" exists in convex/features but not in src/features`
292
+ });
293
+ }
294
+ }
295
+ return {
296
+ rule: this.name,
297
+ passed: errors.length === 0,
298
+ errors,
299
+ warnings
300
+ };
301
+ }
302
+ };
303
+
304
+ // src/validators/index.ts
305
+ var validators = [
306
+ featureStructureValidator,
307
+ componentLocationValidator,
308
+ hookLocationValidator,
309
+ thinRoutesValidator,
310
+ crossFeatureValidator,
311
+ featureParityValidator
312
+ ];
313
+ async function runAllValidators(cwd) {
314
+ const results = [];
315
+ for (const validator of validators) {
316
+ const result = await validator.validate(cwd);
317
+ results.push(result);
318
+ }
319
+ return results;
320
+ }
321
+
322
+ // src/commands/check.ts
323
+ import pc from "picocolors";
324
+ var check_default = defineCommand({
325
+ meta: {
326
+ name: "check",
327
+ description: "Validate project structure and conventions"
328
+ },
329
+ async run() {
330
+ const cwd = process.cwd();
331
+ logger.blank();
332
+ logger.log(" Checking project structure...");
333
+ logger.blank();
334
+ const results = await runAllValidators(cwd);
335
+ let totalErrors = 0;
336
+ let totalWarnings = 0;
337
+ for (const result of results) {
338
+ if (result.passed && result.warnings.length === 0) {
339
+ logger.success(`${result.rule} valid`);
340
+ } else if (result.passed && result.warnings.length > 0) {
341
+ logger.warn(`${result.rule} (${result.warnings.length} warning${result.warnings.length > 1 ? "s" : ""})`);
342
+ for (const warning of result.warnings) {
343
+ logger.log(` ${pc.yellow("\u2192")} ${warning.file}: ${warning.message}`);
344
+ }
345
+ totalWarnings += result.warnings.length;
346
+ } else {
347
+ logger.error(`${result.rule} violated`);
348
+ for (const error of result.errors) {
349
+ logger.log(` ${pc.red("\u2192")} ${error.file}: ${error.message}`);
350
+ }
351
+ for (const warning of result.warnings) {
352
+ logger.log(` ${pc.yellow("\u2192")} ${warning.file}: ${warning.message}`);
353
+ }
354
+ totalErrors += result.errors.length;
355
+ totalWarnings += result.warnings.length;
356
+ }
357
+ }
358
+ logger.blank();
359
+ if (totalErrors > 0) {
360
+ logger.log(` ${pc.red(`${totalErrors} error${totalErrors > 1 ? "s" : ""} found.`)}`);
361
+ if (totalWarnings > 0) {
362
+ logger.log(` ${pc.yellow(`${totalWarnings} warning${totalWarnings > 1 ? "s" : ""}.`)}`);
363
+ }
364
+ logger.blank();
365
+ process.exit(1);
366
+ } else if (totalWarnings > 0) {
367
+ logger.log(` ${pc.yellow(`${totalWarnings} warning${totalWarnings > 1 ? "s" : ""} found.`)}`);
368
+ logger.blank();
369
+ } else {
370
+ logger.log(` ${pc.green("All checks passed!")}`);
371
+ logger.blank();
372
+ }
373
+ }
374
+ });
375
+ export {
376
+ check_default as default
377
+ };