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.
@@ -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 exportDefault = "export default defineSchema({";
113
- if (content.includes(importLine)) {
114
- logger.warn("Schema import already exists - skipping");
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
- const exportPos = content.indexOf(exportDefault);
118
- if (exportPos === -1) {
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 = 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");
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
- 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
- }
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-MU65GMUK.js").then((m) => m.default),
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 # Start dev server
26
- npx convex dev # Start Convex backend
27
- pnpm lint # Check with Biome
28
- forge check # Validate architecture (run before finishing)
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
- // Feature table imports will be added here by forge add:feature
3
+ // [forge:imports]
4
+ // [/forge:imports]
4
5
 
5
6
  export default defineSchema({
6
- // Feature tables will be spread here by forge add:feature
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 <ConvexProvider client={convex}>{children}</ConvexProvider>;
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,8 @@
1
+ export default {
2
+ providers: [
3
+ {
4
+ domain: process.env.CONVEX_SITE_URL,
5
+ applicationID: "convex",
6
+ },
7
+ ],
8
+ };
@@ -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,8 @@
1
+ import { httpRouter } from "convex/server";
2
+ import { auth } from "./auth";
3
+
4
+ const http = httpRouter();
5
+
6
+ auth.addHttpRoutes(http);
7
+
8
+ export default http;
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai-forge-cli",
3
- "version": "0.3.4",
3
+ "version": "0.4.0",
4
4
  "description": "TypeScript stack scaffolding & enforcement CLI for TanStack Start + Convex",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -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 # Start dev server
26
- npx convex dev # Start Convex backend
27
- pnpm lint # Check with Biome
28
- forge check # Validate architecture (run before finishing)
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
- // Feature table imports will be added here by forge add:feature
3
+ // [forge:imports]
4
+ // [/forge:imports]
4
5
 
5
6
  export default defineSchema({
6
- // Feature tables will be spread here by forge add:feature
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 <ConvexProvider client={convex}>{children}</ConvexProvider>;
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,8 @@
1
+ export default {
2
+ providers: [
3
+ {
4
+ domain: process.env.CONVEX_SITE_URL,
5
+ applicationID: "convex",
6
+ },
7
+ ],
8
+ };
@@ -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,8 @@
1
+ import { httpRouter } from "convex/server";
2
+ import { auth } from "./auth";
3
+
4
+ const http = httpRouter();
5
+
6
+ auth.addHttpRoutes(http);
7
+
8
+ export default http;
@@ -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
+ }