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.
- package/LICENSE +21 -0
- package/README.md +148 -0
- package/dist/add-feature-YXWSRIVE.js +141 -0
- package/dist/check-RCJRXIU5.js +377 -0
- package/dist/chunk-J4V5PGVT.js +55 -0
- package/dist/chunk-PIFX2L5H.js +46 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +17 -0
- package/dist/init-GQA3WDXQ.js +114 -0
- package/dist/templates/feature/convex/index.ts.hbs +3 -0
- package/dist/templates/feature/convex/mutations.ts.hbs +37 -0
- package/dist/templates/feature/convex/queries.ts.hbs +16 -0
- package/dist/templates/feature/convex/schema.ts.hbs +10 -0
- package/dist/templates/feature/routes/$id.tsx.hbs +27 -0
- package/dist/templates/feature/routes/index.tsx.hbs +21 -0
- package/dist/templates/feature/src/components/index.ts.hbs +4 -0
- package/dist/templates/feature/src/hooks.ts.hbs +29 -0
- package/dist/templates/feature/src/index.ts.hbs +2 -0
- package/dist/templates/init/app/client.tsx.hbs +7 -0
- package/dist/templates/init/app/router.tsx.hbs +17 -0
- package/dist/templates/init/app/routes/__root.tsx.hbs +38 -0
- package/dist/templates/init/app/routes/index.tsx.hbs +18 -0
- package/dist/templates/init/app/ssr.tsx.hbs +11 -0
- package/dist/templates/init/app.config.ts.hbs +8 -0
- package/dist/templates/init/biome.json.hbs +32 -0
- package/dist/templates/init/claude.md.hbs +93 -0
- package/dist/templates/init/convex/schema.ts.hbs +7 -0
- package/dist/templates/init/package.json.hbs +34 -0
- package/dist/templates/init/postcss.config.js.hbs +6 -0
- package/dist/templates/init/src/lib/cn.ts.hbs +6 -0
- package/dist/templates/init/src/providers/index.tsx.hbs +10 -0
- package/dist/templates/init/tailwind.config.ts.hbs +12 -0
- package/dist/templates/init/tsconfig.json.hbs +24 -0
- package/package.json +59 -0
- package/templates/feature/convex/index.ts.hbs +3 -0
- package/templates/feature/convex/mutations.ts.hbs +37 -0
- package/templates/feature/convex/queries.ts.hbs +16 -0
- package/templates/feature/convex/schema.ts.hbs +10 -0
- package/templates/feature/routes/$id.tsx.hbs +27 -0
- package/templates/feature/routes/index.tsx.hbs +21 -0
- package/templates/feature/src/components/index.ts.hbs +4 -0
- package/templates/feature/src/hooks.ts.hbs +29 -0
- package/templates/feature/src/index.ts.hbs +2 -0
- package/templates/init/app/client.tsx.hbs +7 -0
- package/templates/init/app/router.tsx.hbs +17 -0
- package/templates/init/app/routes/__root.tsx.hbs +38 -0
- package/templates/init/app/routes/index.tsx.hbs +18 -0
- package/templates/init/app/ssr.tsx.hbs +11 -0
- package/templates/init/app.config.ts.hbs +8 -0
- package/templates/init/biome.json.hbs +32 -0
- package/templates/init/claude.md.hbs +93 -0
- package/templates/init/convex/schema.ts.hbs +7 -0
- package/templates/init/package.json.hbs +34 -0
- package/templates/init/postcss.config.js.hbs +6 -0
- package/templates/init/src/lib/cn.ts.hbs +6 -0
- package/templates/init/src/providers/index.tsx.hbs +10 -0
- package/templates/init/tailwind.config.ts.hbs +12 -0
- 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
|
+
};
|
package/dist/index.d.ts
ADDED
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,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,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,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,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 |
|