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
@@ -0,0 +1,55 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/utils/logger.ts
4
+ import { consola } from "consola";
5
+ import pc from "picocolors";
6
+ var logger = {
7
+ success(msg) {
8
+ consola.log(` ${pc.green("\u2713")} ${msg}`);
9
+ },
10
+ error(msg) {
11
+ consola.log(` ${pc.red("\u2717")} ${msg}`);
12
+ },
13
+ warn(msg) {
14
+ consola.log(` ${pc.yellow("!")} ${msg}`);
15
+ },
16
+ info(msg) {
17
+ consola.log(` ${pc.blue("\u2192")} ${msg}`);
18
+ },
19
+ log(msg) {
20
+ consola.log(msg);
21
+ },
22
+ blank() {
23
+ consola.log("");
24
+ }
25
+ };
26
+
27
+ // src/utils/fs.ts
28
+ import { mkdir, readFile as fsReadFile, writeFile as fsWriteFile, access } from "fs/promises";
29
+ import { dirname } from "path";
30
+ async function ensureDir(path) {
31
+ await mkdir(path, { recursive: true });
32
+ }
33
+ async function fileExists(path) {
34
+ try {
35
+ await access(path);
36
+ return true;
37
+ } catch {
38
+ return false;
39
+ }
40
+ }
41
+ async function writeFile(path, content) {
42
+ await ensureDir(dirname(path));
43
+ await fsWriteFile(path, content, "utf-8");
44
+ }
45
+ async function readFile(path) {
46
+ return fsReadFile(path, "utf-8");
47
+ }
48
+
49
+ export {
50
+ logger,
51
+ ensureDir,
52
+ fileExists,
53
+ writeFile,
54
+ readFile
55
+ };
@@ -0,0 +1,46 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/utils/case.ts
4
+ function camelCase(str) {
5
+ return str.replace(/[-_\s]+(.)?/g, (_, c) => c ? c.toUpperCase() : "").replace(/^[A-Z]/, (c) => c.toLowerCase());
6
+ }
7
+ function pascalCase(str) {
8
+ const camel = camelCase(str);
9
+ return camel.charAt(0).toUpperCase() + camel.slice(1);
10
+ }
11
+ function kebabCase(str) {
12
+ return str.replace(/([a-z])([A-Z])/g, "$1-$2").replace(/[\s_]+/g, "-").toLowerCase();
13
+ }
14
+
15
+ // src/utils/template.ts
16
+ import Handlebars from "handlebars";
17
+ import { readFileSync, existsSync } from "fs";
18
+ import { join, dirname } from "path";
19
+ import { fileURLToPath } from "url";
20
+ var __dirname2 = dirname(fileURLToPath(import.meta.url));
21
+ Handlebars.registerHelper("camelCase", (str) => camelCase(str));
22
+ Handlebars.registerHelper("pascalCase", (str) => pascalCase(str));
23
+ Handlebars.registerHelper("kebabCase", (str) => kebabCase(str));
24
+ function getTemplatesDir() {
25
+ const devPath = join(__dirname2, "../../templates");
26
+ if (existsSync(devPath)) return devPath;
27
+ const prodPath = join(__dirname2, "../templates");
28
+ if (existsSync(prodPath)) return prodPath;
29
+ throw new Error("Templates directory not found");
30
+ }
31
+ function renderTemplate(templatePath, data) {
32
+ const templatesDir = getTemplatesDir();
33
+ const fullPath = join(templatesDir, templatePath);
34
+ if (!existsSync(fullPath)) {
35
+ throw new Error(`Template not found: ${fullPath}`);
36
+ }
37
+ const templateContent = readFileSync(fullPath, "utf-8");
38
+ const template = Handlebars.compile(templateContent);
39
+ return template(data);
40
+ }
41
+
42
+ export {
43
+ camelCase,
44
+ kebabCase,
45
+ renderTemplate
46
+ };
@@ -0,0 +1,2 @@
1
+
2
+ export { }
package/dist/index.js ADDED
@@ -0,0 +1,17 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { defineCommand, runMain } from "citty";
5
+ var main = defineCommand({
6
+ meta: {
7
+ name: "forge",
8
+ version: "0.1.0",
9
+ description: "TypeScript stack scaffolding & enforcement CLI"
10
+ },
11
+ subCommands: {
12
+ init: () => import("./init-GQA3WDXQ.js").then((m) => m.default),
13
+ "add:feature": () => import("./add-feature-YXWSRIVE.js").then((m) => m.default),
14
+ check: () => import("./check-RCJRXIU5.js").then((m) => m.default)
15
+ }
16
+ });
17
+ runMain(main);
@@ -0,0 +1,114 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ kebabCase,
4
+ renderTemplate
5
+ } from "./chunk-PIFX2L5H.js";
6
+ import {
7
+ ensureDir,
8
+ fileExists,
9
+ logger,
10
+ writeFile
11
+ } from "./chunk-J4V5PGVT.js";
12
+
13
+ // src/commands/init.ts
14
+ import { defineCommand } from "citty";
15
+ import { join } from "path";
16
+ import pc from "picocolors";
17
+ var init_default = defineCommand({
18
+ meta: {
19
+ name: "init",
20
+ description: "Create a new project with TanStack Start + Convex + Tailwind"
21
+ },
22
+ args: {
23
+ name: {
24
+ type: "positional",
25
+ description: "Name of the project",
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 projectDir = join(cwd, name);
34
+ if (await fileExists(projectDir)) {
35
+ logger.error(`Directory "${name}" already exists`);
36
+ process.exit(1);
37
+ }
38
+ logger.blank();
39
+ logger.log(` Creating project "${name}"...`);
40
+ logger.blank();
41
+ const templateData = { name };
42
+ const files = [
43
+ // Root config files
44
+ { templatePath: "init/package.json.hbs", destPath: join(projectDir, "package.json") },
45
+ { templatePath: "init/tsconfig.json.hbs", destPath: join(projectDir, "tsconfig.json") },
46
+ { templatePath: "init/biome.json.hbs", destPath: join(projectDir, "biome.json") },
47
+ { templatePath: "init/tailwind.config.ts.hbs", destPath: join(projectDir, "tailwind.config.ts") },
48
+ { templatePath: "init/postcss.config.js.hbs", destPath: join(projectDir, "postcss.config.js") },
49
+ { templatePath: "init/app.config.ts.hbs", destPath: join(projectDir, "app.config.ts") },
50
+ // App files
51
+ { templatePath: "init/app/client.tsx.hbs", destPath: join(projectDir, "app/client.tsx") },
52
+ { templatePath: "init/app/ssr.tsx.hbs", destPath: join(projectDir, "app/ssr.tsx") },
53
+ { templatePath: "init/app/router.tsx.hbs", destPath: join(projectDir, "app/router.tsx") },
54
+ { templatePath: "init/app/routes/__root.tsx.hbs", destPath: join(projectDir, "app/routes/__root.tsx") },
55
+ { templatePath: "init/app/routes/index.tsx.hbs", destPath: join(projectDir, "app/routes/index.tsx") },
56
+ // Src files
57
+ { templatePath: "init/src/lib/cn.ts.hbs", destPath: join(projectDir, "src/lib/cn.ts") },
58
+ { templatePath: "init/src/providers/index.tsx.hbs", destPath: join(projectDir, "src/providers/index.tsx") },
59
+ // Convex files
60
+ { templatePath: "init/convex/schema.ts.hbs", destPath: join(projectDir, "convex/schema.ts") },
61
+ // CLAUDE.md - THE HOOK
62
+ { templatePath: "init/claude.md.hbs", destPath: join(projectDir, "CLAUDE.md") }
63
+ ];
64
+ for (const file of files) {
65
+ const content = renderTemplate(file.templatePath, templateData);
66
+ await writeFile(file.destPath, content);
67
+ const relativePath = file.destPath.replace(projectDir + "/", "");
68
+ logger.success(`Created ${relativePath}`);
69
+ }
70
+ const emptyDirs = [
71
+ join(projectDir, "src/components/ui"),
72
+ join(projectDir, "src/features"),
73
+ join(projectDir, "src/hooks"),
74
+ join(projectDir, "convex/features")
75
+ ];
76
+ for (const dir of emptyDirs) {
77
+ await ensureDir(dir);
78
+ await writeFile(join(dir, ".gitkeep"), "");
79
+ }
80
+ const globalCss = `@tailwind base;
81
+ @tailwind components;
82
+ @tailwind utilities;
83
+ `;
84
+ await writeFile(join(projectDir, "src/styles.css"), globalCss);
85
+ logger.success("Created src/styles.css");
86
+ const gitignore = `node_modules
87
+ dist
88
+ .vinxi
89
+ .env
90
+ .env.local
91
+ `;
92
+ await writeFile(join(projectDir, ".gitignore"), gitignore);
93
+ logger.success("Created .gitignore");
94
+ const envExample = `VITE_CONVEX_URL=
95
+ `;
96
+ await writeFile(join(projectDir, ".env.example"), envExample);
97
+ logger.success("Created .env.example");
98
+ logger.blank();
99
+ logger.log(` ${pc.green("Project created successfully!")}`);
100
+ logger.blank();
101
+ logger.log(" Next steps:");
102
+ logger.log(` 1. ${pc.cyan(`cd ${name}`)}`);
103
+ logger.log(` 2. ${pc.cyan("pnpm install")}`);
104
+ logger.log(` 3. ${pc.cyan("npx convex init")}`);
105
+ logger.log(` 4. ${pc.cyan("pnpm dlx shadcn@latest init")}`);
106
+ logger.log(` 5. ${pc.cyan("pnpm dev")}`);
107
+ logger.blank();
108
+ logger.log(` ${pc.dim("CLAUDE.md is configured. Claude Code will use forge CLI automatically.")}`);
109
+ logger.blank();
110
+ }
111
+ });
112
+ export {
113
+ init_default as default
114
+ };
@@ -0,0 +1,3 @@
1
+ export * as queries from "./queries";
2
+ export * as mutations from "./mutations";
3
+ export { {{camelCase name}}Tables } from "./schema";
@@ -0,0 +1,37 @@
1
+ import { mutation } from "convex/_generated/server";
2
+ import { v } from "convex/values";
3
+
4
+ export const create = mutation({
5
+ args: {
6
+ // Define your args here
7
+ },
8
+ handler: async (ctx, args) => {
9
+ const now = Date.now();
10
+ return await ctx.db.insert("{{camelCase name}}", {
11
+ ...args,
12
+ createdAt: now,
13
+ updatedAt: now,
14
+ });
15
+ },
16
+ });
17
+
18
+ export const update = mutation({
19
+ args: {
20
+ id: v.id("{{camelCase name}}"),
21
+ // Define your update args here
22
+ },
23
+ handler: async (ctx, args) => {
24
+ const { id, ...fields } = args;
25
+ await ctx.db.patch(id, {
26
+ ...fields,
27
+ updatedAt: Date.now(),
28
+ });
29
+ },
30
+ });
31
+
32
+ export const remove = mutation({
33
+ args: { id: v.id("{{camelCase name}}") },
34
+ handler: async (ctx, args) => {
35
+ await ctx.db.delete(args.id);
36
+ },
37
+ });
@@ -0,0 +1,16 @@
1
+ import { query } from "convex/_generated/server";
2
+ import { v } from "convex/values";
3
+
4
+ export const list = query({
5
+ args: {},
6
+ handler: async (ctx) => {
7
+ return await ctx.db.query("{{camelCase name}}").collect();
8
+ },
9
+ });
10
+
11
+ export const getById = query({
12
+ args: { id: v.id("{{camelCase name}}") },
13
+ handler: async (ctx, args) => {
14
+ return await ctx.db.get(args.id);
15
+ },
16
+ });
@@ -0,0 +1,10 @@
1
+ import { defineTable } from "convex/server";
2
+ import { v } from "convex/values";
3
+
4
+ export const {{camelCase name}}Tables = {
5
+ {{camelCase name}}: defineTable({
6
+ // Define your fields here
7
+ createdAt: v.number(),
8
+ updatedAt: v.number(),
9
+ }).index("by_created", ["createdAt"]),
10
+ };
@@ -0,0 +1,27 @@
1
+ import { createFileRoute } from "@tanstack/react-router";
2
+ import { use{{pascalCase name}} } from "~/features/{{camelCase name}}";
3
+ import type { Id } from "@convex/_generated/dataModel";
4
+
5
+ export const Route = createFileRoute("/{{kebabCase name}}/$id")({
6
+ component: {{pascalCase name}}DetailRoute,
7
+ });
8
+
9
+ function {{pascalCase name}}DetailRoute() {
10
+ const { id } = Route.useParams();
11
+ const { item, isLoading } = use{{pascalCase name}}(id as Id<"{{camelCase name}}">);
12
+
13
+ if (isLoading) {
14
+ return <div>Loading...</div>;
15
+ }
16
+
17
+ if (!item) {
18
+ return <div>Not found</div>;
19
+ }
20
+
21
+ return (
22
+ <div>
23
+ <h1>{{pascalCase name}} Detail</h1>
24
+ {/* Build your UI here */}
25
+ </div>
26
+ );
27
+ }
@@ -0,0 +1,21 @@
1
+ import { createFileRoute } from "@tanstack/react-router";
2
+ import { use{{pascalCase name}}List } from "~/features/{{camelCase name}}";
3
+
4
+ export const Route = createFileRoute("/{{kebabCase name}}/")({
5
+ component: {{pascalCase name}}IndexRoute,
6
+ });
7
+
8
+ function {{pascalCase name}}IndexRoute() {
9
+ const { items, isLoading } = use{{pascalCase name}}List();
10
+
11
+ if (isLoading) {
12
+ return <div>Loading...</div>;
13
+ }
14
+
15
+ return (
16
+ <div>
17
+ <h1>{{pascalCase name}}</h1>
18
+ {/* Build your UI here */}
19
+ </div>
20
+ );
21
+ }
@@ -0,0 +1,4 @@
1
+ // Export components as you create them
2
+ // export { {{pascalCase name}}Card } from "./{{pascalCase name}}Card";
3
+ // export { {{pascalCase name}}List } from "./{{pascalCase name}}List";
4
+ // export { {{pascalCase name}}Form } from "./{{pascalCase name}}Form";
@@ -0,0 +1,29 @@
1
+ import { useQuery, useMutation } from "convex/react";
2
+ import { api } from "@convex/_generated/api";
3
+ import type { Id } from "@convex/_generated/dataModel";
4
+
5
+ export function use{{pascalCase name}}List() {
6
+ const items = useQuery(api.features.{{camelCase name}}.queries.list);
7
+
8
+ return {
9
+ items: items ?? [],
10
+ isLoading: items === undefined,
11
+ };
12
+ }
13
+
14
+ export function use{{pascalCase name}}(id: Id<"{{camelCase name}}">) {
15
+ const item = useQuery(api.features.{{camelCase name}}.queries.getById, { id });
16
+
17
+ return {
18
+ item,
19
+ isLoading: item === undefined,
20
+ };
21
+ }
22
+
23
+ export function use{{pascalCase name}}Mutations() {
24
+ const create = useMutation(api.features.{{camelCase name}}.mutations.create);
25
+ const update = useMutation(api.features.{{camelCase name}}.mutations.update);
26
+ const remove = useMutation(api.features.{{camelCase name}}.mutations.remove);
27
+
28
+ return { create, update, remove };
29
+ }
@@ -0,0 +1,2 @@
1
+ export * from "./hooks";
2
+ export * from "./components";
@@ -0,0 +1,7 @@
1
+ import { hydrateRoot } from "react-dom/client";
2
+ import { StartClient } from "@tanstack/start";
3
+ import { createRouter } from "./router";
4
+
5
+ const router = createRouter();
6
+
7
+ hydrateRoot(document, <StartClient router={router} />);
@@ -0,0 +1,17 @@
1
+ import { createRouter as createTanStackRouter } from "@tanstack/react-router";
2
+ import { routeTree } from "./routeTree.gen";
3
+
4
+ export function createRouter() {
5
+ const router = createTanStackRouter({
6
+ routeTree,
7
+ defaultPreload: "intent",
8
+ });
9
+
10
+ return router;
11
+ }
12
+
13
+ declare module "@tanstack/react-router" {
14
+ interface Register {
15
+ router: ReturnType<typeof createRouter>;
16
+ }
17
+ }
@@ -0,0 +1,38 @@
1
+ import { Outlet, ScrollRestoration, createRootRoute } from "@tanstack/react-router";
2
+ import { Meta, Scripts } from "@tanstack/start";
3
+ import { Providers } from "~/providers";
4
+
5
+ export const Route = createRootRoute({
6
+ head: () => ({
7
+ meta: [
8
+ { charSet: "utf-8" },
9
+ { name: "viewport", content: "width=device-width, initial-scale=1" },
10
+ { title: "{{pascalCase name}}" },
11
+ ],
12
+ links: [{ rel: "stylesheet", href: "/styles.css" }],
13
+ }),
14
+ component: RootComponent,
15
+ });
16
+
17
+ function RootComponent() {
18
+ return (
19
+ <RootDocument>
20
+ <Outlet />
21
+ </RootDocument>
22
+ );
23
+ }
24
+
25
+ function RootDocument({ children }: { children: React.ReactNode }) {
26
+ return (
27
+ <html lang="en">
28
+ <head>
29
+ <Meta />
30
+ </head>
31
+ <body>
32
+ <Providers>{children}</Providers>
33
+ <ScrollRestoration />
34
+ <Scripts />
35
+ </body>
36
+ </html>
37
+ );
38
+ }
@@ -0,0 +1,18 @@
1
+ import { createFileRoute } from "@tanstack/react-router";
2
+
3
+ export const Route = createFileRoute("/")({
4
+ component: Home,
5
+ });
6
+
7
+ function Home() {
8
+ return (
9
+ <div className="min-h-screen flex items-center justify-center">
10
+ <div className="text-center">
11
+ <h1 className="text-4xl font-bold mb-4">{{pascalCase name}}</h1>
12
+ <p className="text-gray-600">
13
+ Your TanStack Start + Convex app is ready.
14
+ </p>
15
+ </div>
16
+ </div>
17
+ );
18
+ }
@@ -0,0 +1,11 @@
1
+ import {
2
+ createStartHandler,
3
+ defaultStreamHandler,
4
+ } from "@tanstack/start/server";
5
+ import { getRouterManifest } from "@tanstack/start/router-manifest";
6
+ import { createRouter } from "./router";
7
+
8
+ export default createStartHandler({
9
+ createRouter,
10
+ getRouterManifest,
11
+ })(defaultStreamHandler);
@@ -0,0 +1,8 @@
1
+ import { defineConfig } from "@tanstack/start/config";
2
+ import viteTsConfigPaths from "vite-tsconfig-paths";
3
+
4
+ export default defineConfig({
5
+ vite: {
6
+ plugins: [viteTsConfigPaths()],
7
+ },
8
+ });
@@ -0,0 +1,32 @@
1
+ {
2
+ "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",
3
+ "vcs": {
4
+ "enabled": true,
5
+ "clientKind": "git",
6
+ "useIgnoreFile": true
7
+ },
8
+ "files": {
9
+ "ignoreUnknown": false,
10
+ "ignore": ["node_modules", "dist", ".vinxi", "convex/_generated"]
11
+ },
12
+ "formatter": {
13
+ "enabled": true,
14
+ "indentStyle": "space",
15
+ "indentWidth": 2
16
+ },
17
+ "organizeImports": {
18
+ "enabled": true
19
+ },
20
+ "linter": {
21
+ "enabled": true,
22
+ "rules": {
23
+ "recommended": true
24
+ }
25
+ },
26
+ "javascript": {
27
+ "formatter": {
28
+ "quoteStyle": "double",
29
+ "semicolons": "always"
30
+ }
31
+ }
32
+ }
@@ -0,0 +1,93 @@
1
+ # CLAUDE.md
2
+
3
+ ## YOU MUST USE FORGE CLI
4
+
5
+ When the user asks you to build ANY feature, you MUST:
6
+
7
+ 1. FIRST run `forge add:feature <name>`
8
+ 2. THEN fill in the generated files
9
+ 3. NEVER create feature files manually
10
+
11
+ ## Trigger Words → Action
12
+
13
+ | User says | You run |
14
+ |-----------|---------|
15
+ | "add feature", "create feature", "build feature" | `forge add:feature <name>` |
16
+ | "add X with Y" (any domain entity) | `forge add:feature <name>` |
17
+ | "new page for X" | `forge add:feature <name>` |
18
+ | "build X functionality" | `forge add:feature <name>` |
19
+
20
+ ## File Creation Rules
21
+
22
+ DO NOT create these files manually:
23
+ - `src/features/**/*` (forge creates the structure)
24
+ - `convex/features/**/*` (forge creates the structure)
25
+ - `app/routes/<feature>/**/*` (forge creates the structure)
26
+
27
+ ONLY the forge CLI creates these. You fill them in after.
28
+
29
+ ## What You CAN Create Manually
30
+
31
+ - Files inside existing feature component folders (after forge created them)
32
+ - `src/components/*` (shared UI)
33
+ - `src/lib/*` (utilities)
34
+ - `src/hooks/*` (global hooks)
35
+
36
+ ## Workflow Example
37
+
38
+ User: "Add a comments feature with replies"
39
+
40
+ You MUST:
41
+ ```
42
+ Run: forge add:feature comments
43
+ Edit: convex/features/comments/schema.ts (add fields for comments + replies)
44
+ Edit: convex/features/comments/queries.ts (add queries)
45
+ Edit: convex/features/comments/mutations.ts (add mutations)
46
+ Edit: src/features/comments/hooks.ts (add/modify hooks)
47
+ Create: src/features/comments/components/CommentCard.tsx
48
+ Create: src/features/comments/components/CommentList.tsx
49
+ Create: src/features/comments/components/ReplyForm.tsx
50
+ Update: src/features/comments/components/index.ts (add exports)
51
+ Edit: app/routes/comments/index.tsx (wire up components)
52
+ ```
53
+
54
+ ## Validation
55
+
56
+ Before finishing ANY task, run:
57
+ ```bash
58
+ forge check
59
+ ```
60
+ If it fails, fix ALL issues before responding to the user.
61
+
62
+ ## Project Structure
63
+
64
+ ```
65
+ app/routes/ → Thin route files only (import from features, no logic)
66
+ src/features/ → All feature code (components, hooks, types)
67
+ src/components/ → Shared UI only (used across features)
68
+ src/lib/ → Pure utilities
69
+ convex/features/ → Backend mirrors frontend features
70
+ ```
71
+
72
+ ## Rules
73
+
74
+ 1. **Routes are thin**: Only import and render. No business logic. No hooks defined here.
75
+ 2. **No cross-feature imports**: `src/features/X` cannot import from `src/features/Y`
76
+ 3. **Hooks in hooks.ts**: All feature hooks in `src/features/<name>/hooks.ts`
77
+ 4. **Components in components/**: All feature components in `src/features/<name>/components/`
78
+ 5. **Mirror structure**: Every `src/features/X` has `convex/features/X`
79
+
80
+ ## Stack Quick Reference
81
+
82
+ - **Data fetching**: Use hooks from `src/features/<name>/hooks.ts` (they wrap Convex)
83
+ - **Mutations**: Use mutation hooks from `src/features/<name>/hooks.ts`
84
+ - **UI primitives**: Import from `~/components/ui/*` (shadcn)
85
+ - **Styling**: Tailwind classes only, no CSS files
86
+ - **Forms**: react-hook-form + zod for validation
87
+
88
+ ## Commands Reference
89
+
90
+ | Command | When to use |
91
+ |---------|-------------|
92
+ | `forge add:feature <name>` | BEFORE building any new feature |
93
+ | `forge check` | BEFORE completing any task |
@@ -0,0 +1,7 @@
1
+ import { defineSchema } from "convex/server";
2
+
3
+ // Feature table imports will be added here by forge add:feature
4
+
5
+ export default defineSchema({
6
+ // Feature tables will be spread here by forge add:feature
7
+ });