ai-forge-cli 0.3.4 → 0.4.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/dist/{add-feature-MU65GMUK.js → add-feature-SOACPDYP.js} +16 -18
- package/dist/add-integration-DZT6E4XW.js +239 -0
- package/dist/chunk-I7QOMYL4.js +45 -0
- package/dist/index.js +2 -1
- package/dist/templates/init/claude.md.hbs +18 -5
- package/dist/templates/init/convex/schema.ts.hbs +4 -2
- package/dist/templates/init/src/providers/index.tsx.hbs +11 -1
- package/dist/templates/integration/auth/convex/auth.config.ts.hbs +8 -0
- package/dist/templates/integration/auth/convex/auth.ts.hbs +7 -0
- package/dist/templates/integration/auth/convex/http.ts.hbs +8 -0
- package/dist/templates/integration/auth/src/lib/auth.ts.hbs +14 -0
- package/dist/templates/integration/storage/convex/lib/storage.ts.hbs +32 -0
- package/dist/templates/integration/storage/src/lib/storage.ts.hbs +42 -0
- package/package.json +1 -1
- package/templates/init/claude.md.hbs +18 -5
- package/templates/init/convex/schema.ts.hbs +4 -2
- package/templates/init/src/providers/index.tsx.hbs +11 -1
- package/templates/integration/auth/convex/auth.config.ts.hbs +8 -0
- package/templates/integration/auth/convex/auth.ts.hbs +7 -0
- package/templates/integration/auth/convex/http.ts.hbs +8 -0
- package/templates/integration/auth/src/lib/auth.ts.hbs +14 -0
- package/templates/integration/storage/convex/lib/storage.ts.hbs +32 -0
- package/templates/integration/storage/src/lib/storage.ts.hbs +42 -0
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
insertAtMarker
|
|
4
|
+
} from "./chunk-I7QOMYL4.js";
|
|
2
5
|
import {
|
|
3
6
|
camelCase,
|
|
4
7
|
kebabCase,
|
|
@@ -109,30 +112,25 @@ async function updateConvexSchema(cwd, featureName) {
|
|
|
109
112
|
}
|
|
110
113
|
let content = await readFile(schemaPath);
|
|
111
114
|
const importLine = `import { ${camelName}Tables } from "./features/${featureName}/schema";`;
|
|
112
|
-
const
|
|
113
|
-
|
|
114
|
-
|
|
115
|
+
const spreadLine = ` ...${camelName}Tables,`;
|
|
116
|
+
const importResult = insertAtMarker(content, "imports", importLine, "ts");
|
|
117
|
+
if (!importResult.success) {
|
|
118
|
+
logger.warn("Markers not found in schema.ts - please add imports manually");
|
|
119
|
+
logger.warn(` Add: ${importLine}`);
|
|
115
120
|
return;
|
|
116
121
|
}
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
logger.warn("Could not find 'export default defineSchema({' in schema.ts - skipping");
|
|
122
|
+
if (importResult.alreadyPresent) {
|
|
123
|
+
logger.warn("Schema import already exists - skipping");
|
|
120
124
|
return;
|
|
121
125
|
}
|
|
122
|
-
content =
|
|
123
|
-
const
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
if (closingIndex === -1) {
|
|
128
|
-
logger.warn("Could not find closing '});' in schema.ts - skipping spread");
|
|
126
|
+
content = importResult.content;
|
|
127
|
+
const tableResult = insertAtMarker(content, "tables", spreadLine, "ts");
|
|
128
|
+
if (!tableResult.success) {
|
|
129
|
+
logger.warn("Table markers not found in schema.ts - please add table spread manually");
|
|
130
|
+
logger.warn(` Add: ${spreadLine}`);
|
|
129
131
|
return;
|
|
130
132
|
}
|
|
131
|
-
|
|
132
|
-
logger.warn("Schema spread already exists - skipping");
|
|
133
|
-
} else {
|
|
134
|
-
content = content.slice(0, closingIndex) + spreadLine + "\n" + content.slice(closingIndex);
|
|
135
|
-
}
|
|
133
|
+
content = tableResult.content;
|
|
136
134
|
await writeFile(schemaPath, content);
|
|
137
135
|
logger.success("Updated convex/schema.ts");
|
|
138
136
|
}
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
insertAtMarker,
|
|
4
|
+
replaceAtMarker
|
|
5
|
+
} from "./chunk-I7QOMYL4.js";
|
|
6
|
+
import {
|
|
7
|
+
renderTemplate
|
|
8
|
+
} from "./chunk-PIFX2L5H.js";
|
|
9
|
+
import {
|
|
10
|
+
fileExists,
|
|
11
|
+
logger,
|
|
12
|
+
readFile,
|
|
13
|
+
writeFile
|
|
14
|
+
} from "./chunk-HL4K5AHI.js";
|
|
15
|
+
|
|
16
|
+
// src/commands/add-integration.ts
|
|
17
|
+
import { defineCommand } from "citty";
|
|
18
|
+
import { spawn } from "child_process";
|
|
19
|
+
import { join } from "path";
|
|
20
|
+
import pc from "picocolors";
|
|
21
|
+
var INTEGRATIONS = ["auth", "storage"];
|
|
22
|
+
function runCommand(cmd, args, cwd) {
|
|
23
|
+
return new Promise((resolve, reject) => {
|
|
24
|
+
const proc = spawn(cmd, args, { stdio: "inherit", cwd });
|
|
25
|
+
proc.on("close", (code) => {
|
|
26
|
+
if (code === 0) resolve();
|
|
27
|
+
else reject(new Error(`Command failed with code ${code}`));
|
|
28
|
+
});
|
|
29
|
+
proc.on("error", reject);
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
var add_integration_default = defineCommand({
|
|
33
|
+
meta: {
|
|
34
|
+
name: "add:integration",
|
|
35
|
+
description: "Add an infrastructure integration (auth, storage)"
|
|
36
|
+
},
|
|
37
|
+
args: {
|
|
38
|
+
name: {
|
|
39
|
+
type: "positional",
|
|
40
|
+
description: `Integration to add: ${INTEGRATIONS.join(", ")}`,
|
|
41
|
+
required: true
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
async run({ args }) {
|
|
45
|
+
const name = args.name;
|
|
46
|
+
const cwd = process.cwd();
|
|
47
|
+
if (!INTEGRATIONS.includes(name)) {
|
|
48
|
+
logger.error(`Unknown integration: ${name}`);
|
|
49
|
+
logger.log(` Available: ${INTEGRATIONS.join(", ")}`);
|
|
50
|
+
process.exit(1);
|
|
51
|
+
}
|
|
52
|
+
logger.blank();
|
|
53
|
+
if (name === "auth") {
|
|
54
|
+
await setupAuth(cwd);
|
|
55
|
+
} else if (name === "storage") {
|
|
56
|
+
await setupStorage(cwd);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
async function setupAuth(cwd) {
|
|
61
|
+
const authPath = join(cwd, "convex/auth.ts");
|
|
62
|
+
if (await fileExists(authPath)) {
|
|
63
|
+
logger.error("Auth already configured (convex/auth.ts exists)");
|
|
64
|
+
process.exit(1);
|
|
65
|
+
}
|
|
66
|
+
logger.log(` ${pc.bold("Setting up Convex Auth...")}`);
|
|
67
|
+
logger.blank();
|
|
68
|
+
const step1 = logger.step("Installing dependencies...");
|
|
69
|
+
try {
|
|
70
|
+
await runCommand("pnpm", ["add", "@convex-dev/auth", "@auth/core"], cwd);
|
|
71
|
+
step1.succeed("Dependencies installed");
|
|
72
|
+
} catch {
|
|
73
|
+
step1.fail("Failed to install dependencies");
|
|
74
|
+
process.exit(1);
|
|
75
|
+
}
|
|
76
|
+
const step2 = logger.step("Creating auth files...");
|
|
77
|
+
try {
|
|
78
|
+
const files = [
|
|
79
|
+
{ template: "integration/auth/convex/auth.ts.hbs", dest: "convex/auth.ts" },
|
|
80
|
+
{ template: "integration/auth/convex/auth.config.ts.hbs", dest: "convex/auth.config.ts" },
|
|
81
|
+
{ template: "integration/auth/convex/http.ts.hbs", dest: "convex/http.ts" },
|
|
82
|
+
{ template: "integration/auth/src/lib/auth.ts.hbs", dest: "src/lib/auth.ts" }
|
|
83
|
+
];
|
|
84
|
+
for (const file of files) {
|
|
85
|
+
const content = renderTemplate(file.template, {});
|
|
86
|
+
await writeFile(join(cwd, file.dest), content);
|
|
87
|
+
}
|
|
88
|
+
step2.succeed("Auth files created");
|
|
89
|
+
} catch (err) {
|
|
90
|
+
step2.fail("Failed to create auth files");
|
|
91
|
+
throw err;
|
|
92
|
+
}
|
|
93
|
+
const step3 = logger.step("Updating convex/schema.ts...");
|
|
94
|
+
try {
|
|
95
|
+
await updateSchemaForAuth(cwd);
|
|
96
|
+
step3.succeed("Schema updated");
|
|
97
|
+
} catch {
|
|
98
|
+
step3.fail("Failed to update schema");
|
|
99
|
+
}
|
|
100
|
+
const step4 = logger.step("Updating providers...");
|
|
101
|
+
try {
|
|
102
|
+
await updateProvidersForAuth(cwd);
|
|
103
|
+
step4.succeed("Providers updated");
|
|
104
|
+
} catch {
|
|
105
|
+
step4.fail("Failed to update providers");
|
|
106
|
+
}
|
|
107
|
+
const step5 = logger.step("Updating .env.example...");
|
|
108
|
+
try {
|
|
109
|
+
const envPath = join(cwd, ".env.example");
|
|
110
|
+
let env = await fileExists(envPath) ? await readFile(envPath) : "";
|
|
111
|
+
const authVars = `
|
|
112
|
+
# Auth (get from GitHub/Google OAuth apps)
|
|
113
|
+
AUTH_GITHUB_ID=
|
|
114
|
+
AUTH_GITHUB_SECRET=
|
|
115
|
+
AUTH_GOOGLE_ID=
|
|
116
|
+
AUTH_GOOGLE_SECRET=
|
|
117
|
+
`;
|
|
118
|
+
if (!env.includes("AUTH_GITHUB_ID")) {
|
|
119
|
+
await writeFile(envPath, env.trim() + "\n" + authVars);
|
|
120
|
+
}
|
|
121
|
+
step5.succeed(".env.example updated");
|
|
122
|
+
} catch {
|
|
123
|
+
step5.fail("Failed to update .env.example");
|
|
124
|
+
}
|
|
125
|
+
logger.blank();
|
|
126
|
+
logger.log(` ${pc.green("\u2713")} ${pc.bold("Convex Auth configured!")}`);
|
|
127
|
+
logger.blank();
|
|
128
|
+
logger.log(" Next steps:");
|
|
129
|
+
logger.log(" 1. Create OAuth apps on GitHub/Google");
|
|
130
|
+
logger.log(" 2. Add secrets: npx convex env set AUTH_GITHUB_ID=xxx");
|
|
131
|
+
logger.log(" 3. Generate JWT keys: npx @convex-dev/auth");
|
|
132
|
+
logger.log(" 4. Run: npx convex dev");
|
|
133
|
+
logger.blank();
|
|
134
|
+
}
|
|
135
|
+
async function setupStorage(cwd) {
|
|
136
|
+
const storagePath = join(cwd, "convex/lib/storage.ts");
|
|
137
|
+
if (await fileExists(storagePath)) {
|
|
138
|
+
logger.error("Storage already configured (convex/lib/storage.ts exists)");
|
|
139
|
+
process.exit(1);
|
|
140
|
+
}
|
|
141
|
+
logger.log(` ${pc.bold("Setting up Convex Storage...")}`);
|
|
142
|
+
logger.blank();
|
|
143
|
+
const step1 = logger.step("Creating storage files...");
|
|
144
|
+
try {
|
|
145
|
+
const files = [
|
|
146
|
+
{ template: "integration/storage/convex/lib/storage.ts.hbs", dest: "convex/lib/storage.ts" },
|
|
147
|
+
{ template: "integration/storage/src/lib/storage.ts.hbs", dest: "src/lib/storage.ts" }
|
|
148
|
+
];
|
|
149
|
+
for (const file of files) {
|
|
150
|
+
const content = renderTemplate(file.template, {});
|
|
151
|
+
await writeFile(join(cwd, file.dest), content);
|
|
152
|
+
}
|
|
153
|
+
step1.succeed("Storage files created");
|
|
154
|
+
} catch (err) {
|
|
155
|
+
step1.fail("Failed to create storage files");
|
|
156
|
+
throw err;
|
|
157
|
+
}
|
|
158
|
+
logger.blank();
|
|
159
|
+
logger.log(` ${pc.green("\u2713")} ${pc.bold("Convex Storage configured!")}`);
|
|
160
|
+
logger.blank();
|
|
161
|
+
logger.log(" Next steps:");
|
|
162
|
+
logger.log(" 1. Use generateUploadUrl() in your mutations");
|
|
163
|
+
logger.log(" 2. Use useUpload() hook in your components");
|
|
164
|
+
logger.blank();
|
|
165
|
+
}
|
|
166
|
+
async function updateSchemaForAuth(cwd) {
|
|
167
|
+
const schemaPath = join(cwd, "convex/schema.ts");
|
|
168
|
+
if (!await fileExists(schemaPath)) {
|
|
169
|
+
logger.warn("convex/schema.ts not found - skipping");
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
let content = await readFile(schemaPath);
|
|
173
|
+
const importLine = 'import { authTables } from "@convex-dev/auth/server";';
|
|
174
|
+
const spreadLine = " ...authTables,";
|
|
175
|
+
const importResult = insertAtMarker(content, "imports", importLine, "ts");
|
|
176
|
+
if (!importResult.success) {
|
|
177
|
+
logger.warn("Markers not found in schema.ts - please add auth import manually");
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
if (importResult.alreadyPresent) {
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
content = importResult.content;
|
|
184
|
+
const tableResult = insertAtMarker(content, "tables", spreadLine, "ts");
|
|
185
|
+
if (!tableResult.success) {
|
|
186
|
+
logger.warn("Table markers not found in schema.ts - please add auth tables manually");
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
content = tableResult.content;
|
|
190
|
+
await writeFile(schemaPath, content);
|
|
191
|
+
}
|
|
192
|
+
async function updateProvidersForAuth(cwd) {
|
|
193
|
+
const providersPath = join(cwd, "src/providers/index.tsx");
|
|
194
|
+
if (!await fileExists(providersPath)) {
|
|
195
|
+
logger.warn("src/providers/index.tsx not found - skipping");
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
let content = await readFile(providersPath);
|
|
199
|
+
if (content.includes("ConvexAuthProvider")) {
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
const importLine = 'import { ConvexAuthProvider } from "@convex-dev/auth/react";';
|
|
203
|
+
const importResult = insertAtMarker(content, "imports", importLine, "ts");
|
|
204
|
+
if (!importResult.success) {
|
|
205
|
+
logger.warn("Import markers not found in providers - please add auth import manually");
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
content = importResult.content;
|
|
209
|
+
const openResult = replaceAtMarker(
|
|
210
|
+
content,
|
|
211
|
+
"providers-open",
|
|
212
|
+
" <ConvexAuthProvider client={convex}>",
|
|
213
|
+
"jsx"
|
|
214
|
+
);
|
|
215
|
+
if (!openResult.success) {
|
|
216
|
+
logger.warn("Provider markers not found - please update providers manually");
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
content = openResult.content;
|
|
220
|
+
const closeResult = replaceAtMarker(
|
|
221
|
+
content,
|
|
222
|
+
"providers-close",
|
|
223
|
+
" </ConvexAuthProvider>",
|
|
224
|
+
"jsx"
|
|
225
|
+
);
|
|
226
|
+
if (!closeResult.success) {
|
|
227
|
+
logger.warn("Provider close markers not found - please update providers manually");
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
content = closeResult.content;
|
|
231
|
+
content = content.replace(
|
|
232
|
+
'import { ConvexProvider, ConvexReactClient } from "convex/react";',
|
|
233
|
+
'import { ConvexReactClient } from "convex/react";'
|
|
234
|
+
);
|
|
235
|
+
await writeFile(providersPath, content);
|
|
236
|
+
}
|
|
237
|
+
export {
|
|
238
|
+
add_integration_default as default
|
|
239
|
+
};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/utils/markers.ts
|
|
4
|
+
function insertAtMarker(content, marker, insertion, commentStyle = "ts") {
|
|
5
|
+
const startMarker = commentStyle === "ts" ? `// [forge:${marker}]` : `{/* [forge:${marker}] */}`;
|
|
6
|
+
const endMarker = commentStyle === "ts" ? `// [/forge:${marker}]` : `{/* [/forge:${marker}] */}`;
|
|
7
|
+
const startIdx = content.indexOf(startMarker);
|
|
8
|
+
const endIdx = content.indexOf(endMarker);
|
|
9
|
+
if (startIdx === -1 || endIdx === -1) {
|
|
10
|
+
return { success: false, content };
|
|
11
|
+
}
|
|
12
|
+
const markerContent = content.slice(startIdx, endIdx);
|
|
13
|
+
if (markerContent.includes(insertion.trim())) {
|
|
14
|
+
return { success: true, content, alreadyPresent: true };
|
|
15
|
+
}
|
|
16
|
+
const before = content.slice(0, endIdx);
|
|
17
|
+
const after = content.slice(endIdx);
|
|
18
|
+
return {
|
|
19
|
+
success: true,
|
|
20
|
+
content: `${before}${insertion}
|
|
21
|
+
${after}`
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
function replaceAtMarker(content, marker, replacement, commentStyle = "ts") {
|
|
25
|
+
const startMarker = commentStyle === "ts" ? `// [forge:${marker}]` : `{/* [forge:${marker}] */}`;
|
|
26
|
+
const endMarker = commentStyle === "ts" ? `// [/forge:${marker}]` : `{/* [/forge:${marker}] */}`;
|
|
27
|
+
const startIdx = content.indexOf(startMarker);
|
|
28
|
+
const endIdx = content.indexOf(endMarker);
|
|
29
|
+
if (startIdx === -1 || endIdx === -1) {
|
|
30
|
+
return { success: false, content };
|
|
31
|
+
}
|
|
32
|
+
const before = content.slice(0, startIdx + startMarker.length);
|
|
33
|
+
const after = content.slice(endIdx);
|
|
34
|
+
return {
|
|
35
|
+
success: true,
|
|
36
|
+
content: `${before}
|
|
37
|
+
${replacement}
|
|
38
|
+
${after}`
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export {
|
|
43
|
+
insertAtMarker,
|
|
44
|
+
replaceAtMarker
|
|
45
|
+
};
|
package/dist/index.js
CHANGED
|
@@ -20,7 +20,8 @@ var main = defineCommand({
|
|
|
20
20
|
},
|
|
21
21
|
subCommands: {
|
|
22
22
|
init: () => import("./init-OYJP5QCZ.js").then((m) => m.default),
|
|
23
|
-
"add:feature": () => import("./add-feature-
|
|
23
|
+
"add:feature": () => import("./add-feature-SOACPDYP.js").then((m) => m.default),
|
|
24
|
+
"add:integration": () => import("./add-integration-DZT6E4XW.js").then((m) => m.default),
|
|
24
25
|
check: () => import("./check-YMGJNKME.js").then((m) => m.default),
|
|
25
26
|
version: () => import("./version-VO3LHLDO.js").then((m) => m.default)
|
|
26
27
|
},
|
|
@@ -22,12 +22,23 @@ The ONLY route you may create directly is `src/routes/index.tsx` (homepage).
|
|
|
22
22
|
## Commands
|
|
23
23
|
|
|
24
24
|
```bash
|
|
25
|
-
pnpm dev
|
|
26
|
-
npx convex dev
|
|
27
|
-
pnpm lint
|
|
28
|
-
forge
|
|
25
|
+
pnpm dev # Start dev server
|
|
26
|
+
npx convex dev # Start Convex backend
|
|
27
|
+
pnpm lint # Check with Biome
|
|
28
|
+
forge add:feature <name> # Scaffold a new feature
|
|
29
|
+
forge add:integration auth # Add Convex Auth
|
|
30
|
+
forge add:integration storage # Add Convex file storage
|
|
31
|
+
forge check # Validate architecture (run before finishing)
|
|
29
32
|
```
|
|
30
33
|
|
|
34
|
+
## Integrations
|
|
35
|
+
|
|
36
|
+
For infrastructure (not features), use `forge add:integration`:
|
|
37
|
+
- `auth` - Convex Auth with GitHub/Google OAuth
|
|
38
|
+
- `storage` - Convex file upload/download
|
|
39
|
+
|
|
40
|
+
These create files in `convex/` and `src/lib/` - NOT in features.
|
|
41
|
+
|
|
31
42
|
## Architecture
|
|
32
43
|
|
|
33
44
|
```
|
|
@@ -35,8 +46,10 @@ src/routes/ → Thin route files (import from features, no logic)
|
|
|
35
46
|
src/features/ → Feature code (components/, hooks.ts)
|
|
36
47
|
src/components/ui/ → shadcn primitives only
|
|
37
48
|
src/components/ → Truly shared components (Header, Footer, Logo)
|
|
38
|
-
src/lib/ → Utilities
|
|
49
|
+
src/lib/ → Utilities + integration hooks (auth.ts, storage.ts)
|
|
39
50
|
convex/features/ → Backend (mirrors src/features/)
|
|
51
|
+
convex/lib/ → Shared backend utilities (storage.ts)
|
|
52
|
+
convex/auth.ts → Auth configuration (from forge add:integration auth)
|
|
40
53
|
```
|
|
41
54
|
|
|
42
55
|
## Where Components Go
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { defineSchema } from "convex/server";
|
|
2
2
|
|
|
3
|
-
//
|
|
3
|
+
// [forge:imports]
|
|
4
|
+
// [/forge:imports]
|
|
4
5
|
|
|
5
6
|
export default defineSchema({
|
|
6
|
-
//
|
|
7
|
+
// [forge:tables]
|
|
8
|
+
// [/forge:tables]
|
|
7
9
|
});
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
// [forge:imports]
|
|
2
|
+
// [/forge:imports]
|
|
1
3
|
import { ConvexProvider, ConvexReactClient } from "convex/react";
|
|
2
4
|
import { useState } from "react";
|
|
3
5
|
|
|
@@ -6,5 +8,13 @@ export function Providers({ children }: { children: React.ReactNode }) {
|
|
|
6
8
|
() => new ConvexReactClient(import.meta.env.VITE_CONVEX_URL as string)
|
|
7
9
|
);
|
|
8
10
|
|
|
9
|
-
return
|
|
11
|
+
return (
|
|
12
|
+
{/* [forge:providers-open] */}
|
|
13
|
+
<ConvexProvider client={convex}>
|
|
14
|
+
{/* [/forge:providers-open] */}
|
|
15
|
+
{children}
|
|
16
|
+
{/* [forge:providers-close] */}
|
|
17
|
+
</ConvexProvider>
|
|
18
|
+
{/* [/forge:providers-close] */}
|
|
19
|
+
);
|
|
10
20
|
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import GitHub from "@auth/core/providers/github";
|
|
2
|
+
import Google from "@auth/core/providers/google";
|
|
3
|
+
import { convexAuth } from "@convex-dev/auth/server";
|
|
4
|
+
|
|
5
|
+
export const { auth, signIn, signOut, store, isAuthenticated } = convexAuth({
|
|
6
|
+
providers: [GitHub, Google],
|
|
7
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { useConvexAuth } from "convex/react";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Hook to get authentication state
|
|
5
|
+
*/
|
|
6
|
+
export function useAuth() {
|
|
7
|
+
const { isAuthenticated, isLoading } = useConvexAuth();
|
|
8
|
+
return { isAuthenticated, isLoading };
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Re-export auth components for convenience
|
|
13
|
+
*/
|
|
14
|
+
export { Authenticated, Unauthenticated, AuthLoading } from "convex/react";
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { mutation, query } from "../_generated/server";
|
|
2
|
+
import { v } from "convex/values";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Generate a URL for uploading a file
|
|
6
|
+
*/
|
|
7
|
+
export const generateUploadUrl = mutation({
|
|
8
|
+
args: {},
|
|
9
|
+
handler: async (ctx) => {
|
|
10
|
+
return await ctx.storage.generateUploadUrl();
|
|
11
|
+
},
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Get a URL for accessing a stored file
|
|
16
|
+
*/
|
|
17
|
+
export const getUrl = query({
|
|
18
|
+
args: { storageId: v.id("_storage") },
|
|
19
|
+
handler: async (ctx, args) => {
|
|
20
|
+
return await ctx.storage.getUrl(args.storageId);
|
|
21
|
+
},
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Delete a stored file
|
|
26
|
+
*/
|
|
27
|
+
export const deleteFile = mutation({
|
|
28
|
+
args: { storageId: v.id("_storage") },
|
|
29
|
+
handler: async (ctx, args) => {
|
|
30
|
+
return await ctx.storage.delete(args.storageId);
|
|
31
|
+
},
|
|
32
|
+
});
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { useMutation, useQuery } from "convex/react";
|
|
2
|
+
import { api } from "@convex/_generated/api";
|
|
3
|
+
import type { Id } from "@convex/_generated/dataModel";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Hook for uploading files to Convex storage
|
|
7
|
+
*/
|
|
8
|
+
export function useUpload() {
|
|
9
|
+
const generateUploadUrl = useMutation(api.lib.storage.generateUploadUrl);
|
|
10
|
+
|
|
11
|
+
const upload = async (file: File): Promise<Id<"_storage">> => {
|
|
12
|
+
const uploadUrl = await generateUploadUrl();
|
|
13
|
+
|
|
14
|
+
const response = await fetch(uploadUrl, {
|
|
15
|
+
method: "POST",
|
|
16
|
+
headers: { "Content-Type": file.type },
|
|
17
|
+
body: file,
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
const { storageId } = await response.json();
|
|
21
|
+
return storageId;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
return { upload };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Hook for getting a file URL
|
|
29
|
+
*/
|
|
30
|
+
export function useFileUrl(storageId: Id<"_storage"> | null | undefined) {
|
|
31
|
+
return useQuery(
|
|
32
|
+
api.lib.storage.getUrl,
|
|
33
|
+
storageId ? { storageId } : "skip"
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Hook for deleting a file
|
|
39
|
+
*/
|
|
40
|
+
export function useDeleteFile() {
|
|
41
|
+
return useMutation(api.lib.storage.deleteFile);
|
|
42
|
+
}
|
package/package.json
CHANGED
|
@@ -22,12 +22,23 @@ The ONLY route you may create directly is `src/routes/index.tsx` (homepage).
|
|
|
22
22
|
## Commands
|
|
23
23
|
|
|
24
24
|
```bash
|
|
25
|
-
pnpm dev
|
|
26
|
-
npx convex dev
|
|
27
|
-
pnpm lint
|
|
28
|
-
forge
|
|
25
|
+
pnpm dev # Start dev server
|
|
26
|
+
npx convex dev # Start Convex backend
|
|
27
|
+
pnpm lint # Check with Biome
|
|
28
|
+
forge add:feature <name> # Scaffold a new feature
|
|
29
|
+
forge add:integration auth # Add Convex Auth
|
|
30
|
+
forge add:integration storage # Add Convex file storage
|
|
31
|
+
forge check # Validate architecture (run before finishing)
|
|
29
32
|
```
|
|
30
33
|
|
|
34
|
+
## Integrations
|
|
35
|
+
|
|
36
|
+
For infrastructure (not features), use `forge add:integration`:
|
|
37
|
+
- `auth` - Convex Auth with GitHub/Google OAuth
|
|
38
|
+
- `storage` - Convex file upload/download
|
|
39
|
+
|
|
40
|
+
These create files in `convex/` and `src/lib/` - NOT in features.
|
|
41
|
+
|
|
31
42
|
## Architecture
|
|
32
43
|
|
|
33
44
|
```
|
|
@@ -35,8 +46,10 @@ src/routes/ → Thin route files (import from features, no logic)
|
|
|
35
46
|
src/features/ → Feature code (components/, hooks.ts)
|
|
36
47
|
src/components/ui/ → shadcn primitives only
|
|
37
48
|
src/components/ → Truly shared components (Header, Footer, Logo)
|
|
38
|
-
src/lib/ → Utilities
|
|
49
|
+
src/lib/ → Utilities + integration hooks (auth.ts, storage.ts)
|
|
39
50
|
convex/features/ → Backend (mirrors src/features/)
|
|
51
|
+
convex/lib/ → Shared backend utilities (storage.ts)
|
|
52
|
+
convex/auth.ts → Auth configuration (from forge add:integration auth)
|
|
40
53
|
```
|
|
41
54
|
|
|
42
55
|
## Where Components Go
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { defineSchema } from "convex/server";
|
|
2
2
|
|
|
3
|
-
//
|
|
3
|
+
// [forge:imports]
|
|
4
|
+
// [/forge:imports]
|
|
4
5
|
|
|
5
6
|
export default defineSchema({
|
|
6
|
-
//
|
|
7
|
+
// [forge:tables]
|
|
8
|
+
// [/forge:tables]
|
|
7
9
|
});
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
// [forge:imports]
|
|
2
|
+
// [/forge:imports]
|
|
1
3
|
import { ConvexProvider, ConvexReactClient } from "convex/react";
|
|
2
4
|
import { useState } from "react";
|
|
3
5
|
|
|
@@ -6,5 +8,13 @@ export function Providers({ children }: { children: React.ReactNode }) {
|
|
|
6
8
|
() => new ConvexReactClient(import.meta.env.VITE_CONVEX_URL as string)
|
|
7
9
|
);
|
|
8
10
|
|
|
9
|
-
return
|
|
11
|
+
return (
|
|
12
|
+
{/* [forge:providers-open] */}
|
|
13
|
+
<ConvexProvider client={convex}>
|
|
14
|
+
{/* [/forge:providers-open] */}
|
|
15
|
+
{children}
|
|
16
|
+
{/* [forge:providers-close] */}
|
|
17
|
+
</ConvexProvider>
|
|
18
|
+
{/* [/forge:providers-close] */}
|
|
19
|
+
);
|
|
10
20
|
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import GitHub from "@auth/core/providers/github";
|
|
2
|
+
import Google from "@auth/core/providers/google";
|
|
3
|
+
import { convexAuth } from "@convex-dev/auth/server";
|
|
4
|
+
|
|
5
|
+
export const { auth, signIn, signOut, store, isAuthenticated } = convexAuth({
|
|
6
|
+
providers: [GitHub, Google],
|
|
7
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { useConvexAuth } from "convex/react";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Hook to get authentication state
|
|
5
|
+
*/
|
|
6
|
+
export function useAuth() {
|
|
7
|
+
const { isAuthenticated, isLoading } = useConvexAuth();
|
|
8
|
+
return { isAuthenticated, isLoading };
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Re-export auth components for convenience
|
|
13
|
+
*/
|
|
14
|
+
export { Authenticated, Unauthenticated, AuthLoading } from "convex/react";
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { mutation, query } from "../_generated/server";
|
|
2
|
+
import { v } from "convex/values";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Generate a URL for uploading a file
|
|
6
|
+
*/
|
|
7
|
+
export const generateUploadUrl = mutation({
|
|
8
|
+
args: {},
|
|
9
|
+
handler: async (ctx) => {
|
|
10
|
+
return await ctx.storage.generateUploadUrl();
|
|
11
|
+
},
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Get a URL for accessing a stored file
|
|
16
|
+
*/
|
|
17
|
+
export const getUrl = query({
|
|
18
|
+
args: { storageId: v.id("_storage") },
|
|
19
|
+
handler: async (ctx, args) => {
|
|
20
|
+
return await ctx.storage.getUrl(args.storageId);
|
|
21
|
+
},
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Delete a stored file
|
|
26
|
+
*/
|
|
27
|
+
export const deleteFile = mutation({
|
|
28
|
+
args: { storageId: v.id("_storage") },
|
|
29
|
+
handler: async (ctx, args) => {
|
|
30
|
+
return await ctx.storage.delete(args.storageId);
|
|
31
|
+
},
|
|
32
|
+
});
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { useMutation, useQuery } from "convex/react";
|
|
2
|
+
import { api } from "@convex/_generated/api";
|
|
3
|
+
import type { Id } from "@convex/_generated/dataModel";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Hook for uploading files to Convex storage
|
|
7
|
+
*/
|
|
8
|
+
export function useUpload() {
|
|
9
|
+
const generateUploadUrl = useMutation(api.lib.storage.generateUploadUrl);
|
|
10
|
+
|
|
11
|
+
const upload = async (file: File): Promise<Id<"_storage">> => {
|
|
12
|
+
const uploadUrl = await generateUploadUrl();
|
|
13
|
+
|
|
14
|
+
const response = await fetch(uploadUrl, {
|
|
15
|
+
method: "POST",
|
|
16
|
+
headers: { "Content-Type": file.type },
|
|
17
|
+
body: file,
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
const { storageId } = await response.json();
|
|
21
|
+
return storageId;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
return { upload };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Hook for getting a file URL
|
|
29
|
+
*/
|
|
30
|
+
export function useFileUrl(storageId: Id<"_storage"> | null | undefined) {
|
|
31
|
+
return useQuery(
|
|
32
|
+
api.lib.storage.getUrl,
|
|
33
|
+
storageId ? { storageId } : "skip"
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Hook for deleting a file
|
|
39
|
+
*/
|
|
40
|
+
export function useDeleteFile() {
|
|
41
|
+
return useMutation(api.lib.storage.deleteFile);
|
|
42
|
+
}
|