@getjack/jack 0.1.19 → 0.1.20
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/package.json +1 -1
- package/src/commands/down.ts +11 -1
- package/src/commands/init.ts +19 -6
- package/src/lib/agents.ts +3 -1
- package/src/lib/auth/ensure-auth.test.ts +3 -3
- package/src/lib/control-plane.ts +15 -1
- package/src/lib/deploy-upload.ts +26 -1
- package/src/lib/hooks.ts +232 -1
- package/src/lib/managed-deploy.ts +13 -6
- package/src/lib/progress.ts +76 -5
- package/src/lib/project-list.ts +6 -1
- package/src/lib/project-operations.ts +14 -0
- package/src/lib/project-resolver.ts +1 -1
- package/src/lib/zip-packager.ts +36 -7
- package/src/templates/index.ts +1 -1
- package/src/templates/types.ts +16 -0
- package/templates/CLAUDE.md +103 -0
- package/templates/miniapp/.jack.json +1 -3
- package/templates/saas/.jack.json +154 -0
- package/templates/saas/AGENTS.md +333 -0
- package/templates/saas/bun.lock +925 -0
- package/templates/saas/components.json +21 -0
- package/templates/saas/index.html +12 -0
- package/templates/saas/package.json +75 -0
- package/templates/saas/public/icon.png +0 -0
- package/templates/saas/public/og.png +0 -0
- package/templates/saas/schema.sql +73 -0
- package/templates/saas/src/auth.ts +77 -0
- package/templates/saas/src/client/App.tsx +63 -0
- package/templates/saas/src/client/components/ProtectedRoute.tsx +29 -0
- package/templates/saas/src/client/components/ThemeToggle.tsx +32 -0
- package/templates/saas/src/client/components/ui/accordion.tsx +62 -0
- package/templates/saas/src/client/components/ui/alert-dialog.tsx +133 -0
- package/templates/saas/src/client/components/ui/alert.tsx +60 -0
- package/templates/saas/src/client/components/ui/aspect-ratio.tsx +9 -0
- package/templates/saas/src/client/components/ui/avatar.tsx +39 -0
- package/templates/saas/src/client/components/ui/badge.tsx +39 -0
- package/templates/saas/src/client/components/ui/breadcrumb.tsx +102 -0
- package/templates/saas/src/client/components/ui/button-group.tsx +78 -0
- package/templates/saas/src/client/components/ui/button.tsx +60 -0
- package/templates/saas/src/client/components/ui/card.tsx +75 -0
- package/templates/saas/src/client/components/ui/carousel.tsx +228 -0
- package/templates/saas/src/client/components/ui/chart.tsx +326 -0
- package/templates/saas/src/client/components/ui/checkbox.tsx +29 -0
- package/templates/saas/src/client/components/ui/collapsible.tsx +19 -0
- package/templates/saas/src/client/components/ui/command.tsx +159 -0
- package/templates/saas/src/client/components/ui/context-menu.tsx +224 -0
- package/templates/saas/src/client/components/ui/dialog.tsx +127 -0
- package/templates/saas/src/client/components/ui/drawer.tsx +124 -0
- package/templates/saas/src/client/components/ui/dropdown-menu.tsx +226 -0
- package/templates/saas/src/client/components/ui/empty.tsx +94 -0
- package/templates/saas/src/client/components/ui/field.tsx +232 -0
- package/templates/saas/src/client/components/ui/form.tsx +152 -0
- package/templates/saas/src/client/components/ui/hover-card.tsx +38 -0
- package/templates/saas/src/client/components/ui/input-group.tsx +158 -0
- package/templates/saas/src/client/components/ui/input-otp.tsx +68 -0
- package/templates/saas/src/client/components/ui/input.tsx +21 -0
- package/templates/saas/src/client/components/ui/item.tsx +172 -0
- package/templates/saas/src/client/components/ui/kbd.tsx +28 -0
- package/templates/saas/src/client/components/ui/label.tsx +21 -0
- package/templates/saas/src/client/components/ui/menubar.tsx +250 -0
- package/templates/saas/src/client/components/ui/navigation-menu.tsx +161 -0
- package/templates/saas/src/client/components/ui/pagination.tsx +106 -0
- package/templates/saas/src/client/components/ui/popover.tsx +42 -0
- package/templates/saas/src/client/components/ui/progress.tsx +26 -0
- package/templates/saas/src/client/components/ui/radio-group.tsx +45 -0
- package/templates/saas/src/client/components/ui/resizable.tsx +46 -0
- package/templates/saas/src/client/components/ui/scroll-area.tsx +56 -0
- package/templates/saas/src/client/components/ui/select.tsx +173 -0
- package/templates/saas/src/client/components/ui/separator.tsx +28 -0
- package/templates/saas/src/client/components/ui/sheet.tsx +128 -0
- package/templates/saas/src/client/components/ui/sidebar.tsx +694 -0
- package/templates/saas/src/client/components/ui/skeleton.tsx +13 -0
- package/templates/saas/src/client/components/ui/slider.tsx +58 -0
- package/templates/saas/src/client/components/ui/sonner.tsx +38 -0
- package/templates/saas/src/client/components/ui/spinner.tsx +16 -0
- package/templates/saas/src/client/components/ui/switch.tsx +28 -0
- package/templates/saas/src/client/components/ui/table.tsx +90 -0
- package/templates/saas/src/client/components/ui/tabs.tsx +54 -0
- package/templates/saas/src/client/components/ui/textarea.tsx +18 -0
- package/templates/saas/src/client/components/ui/toggle-group.tsx +80 -0
- package/templates/saas/src/client/components/ui/toggle.tsx +44 -0
- package/templates/saas/src/client/components/ui/tooltip.tsx +57 -0
- package/templates/saas/src/client/hooks/use-mobile.ts +19 -0
- package/templates/saas/src/client/hooks/useAuth.ts +14 -0
- package/templates/saas/src/client/hooks/useSubscription.ts +86 -0
- package/templates/saas/src/client/index.css +165 -0
- package/templates/saas/src/client/lib/auth-client.ts +7 -0
- package/templates/saas/src/client/lib/plans.ts +82 -0
- package/templates/saas/src/client/lib/utils.ts +6 -0
- package/templates/saas/src/client/main.tsx +15 -0
- package/templates/saas/src/client/pages/DashboardPage.tsx +394 -0
- package/templates/saas/src/client/pages/ForgotPasswordPage.tsx +153 -0
- package/templates/saas/src/client/pages/HomePage.tsx +285 -0
- package/templates/saas/src/client/pages/LoginPage.tsx +169 -0
- package/templates/saas/src/client/pages/PricingPage.tsx +467 -0
- package/templates/saas/src/client/pages/ResetPasswordPage.tsx +200 -0
- package/templates/saas/src/client/pages/SignupPage.tsx +192 -0
- package/templates/saas/src/index.ts +208 -0
- package/templates/saas/tsconfig.json +18 -0
- package/templates/saas/vite.config.ts +14 -0
- package/templates/saas/wrangler.jsonc +20 -0
|
@@ -363,7 +363,11 @@ async function runParallelSetup(
|
|
|
363
363
|
installSuccess: boolean;
|
|
364
364
|
remoteResult: ManagedCreateResult;
|
|
365
365
|
}> {
|
|
366
|
+
const setupStart = Date.now();
|
|
367
|
+
debug("Parallel setup started", { template: options.template, usePrebuilt: options.usePrebuilt });
|
|
368
|
+
|
|
366
369
|
// Start both operations
|
|
370
|
+
const installStart = Date.now();
|
|
367
371
|
const installPromise = (async () => {
|
|
368
372
|
const install = Bun.spawn(["bun", "install", "--prefer-offline"], {
|
|
369
373
|
cwd: targetDir,
|
|
@@ -371,15 +375,22 @@ async function runParallelSetup(
|
|
|
371
375
|
stderr: "ignore",
|
|
372
376
|
});
|
|
373
377
|
await install.exited;
|
|
378
|
+
const duration = ((Date.now() - installStart) / 1000).toFixed(1);
|
|
379
|
+
debug(`bun install completed in ${duration}s (exit: ${install.exitCode})`);
|
|
374
380
|
if (install.exitCode !== 0) {
|
|
375
381
|
throw new Error("Dependency installation failed");
|
|
376
382
|
}
|
|
377
383
|
return true;
|
|
378
384
|
})();
|
|
379
385
|
|
|
386
|
+
const remoteStart = Date.now();
|
|
380
387
|
const remotePromise = createManagedProjectRemote(projectName, undefined, {
|
|
381
388
|
template: options.template || "hello",
|
|
382
389
|
usePrebuilt: options.usePrebuilt ?? true,
|
|
390
|
+
}).then((result) => {
|
|
391
|
+
const duration = ((Date.now() - remoteStart) / 1000).toFixed(1);
|
|
392
|
+
debug(`Remote project created in ${duration}s (status: ${result.status})`);
|
|
393
|
+
return result;
|
|
383
394
|
});
|
|
384
395
|
|
|
385
396
|
// Report URL as soon as remote is ready (don't wait for install)
|
|
@@ -434,6 +445,9 @@ async function runParallelSetup(
|
|
|
434
445
|
throw new Error("Unexpected state: remote result not fulfilled");
|
|
435
446
|
}
|
|
436
447
|
|
|
448
|
+
const totalDuration = ((Date.now() - setupStart) / 1000).toFixed(1);
|
|
449
|
+
debug(`Parallel setup completed in ${totalDuration}s`);
|
|
450
|
+
|
|
437
451
|
return {
|
|
438
452
|
installSuccess: true,
|
|
439
453
|
remoteResult: remoteResult.value,
|
|
@@ -382,7 +382,7 @@ export async function listAllProjects(): Promise<ResolvedProject[]> {
|
|
|
382
382
|
!project.sources.controlPlane
|
|
383
383
|
) {
|
|
384
384
|
project.status = "error";
|
|
385
|
-
project.errorMessage = "Project not found in jack cloud
|
|
385
|
+
project.errorMessage = "Project not found in jack cloud";
|
|
386
386
|
}
|
|
387
387
|
}
|
|
388
388
|
} catch {
|
package/src/lib/zip-packager.ts
CHANGED
|
@@ -48,17 +48,23 @@ export interface ManifestData {
|
|
|
48
48
|
};
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
+
export interface ZipProgressCallback {
|
|
52
|
+
(current: number, total: number): void;
|
|
53
|
+
}
|
|
54
|
+
|
|
51
55
|
/**
|
|
52
56
|
* Creates a ZIP archive from source directory
|
|
53
57
|
* @param outputPath - Absolute path for output ZIP file
|
|
54
58
|
* @param sourceDir - Absolute path to directory to archive
|
|
55
59
|
* @param files - Optional list of specific files to include (relative to sourceDir)
|
|
60
|
+
* @param onProgress - Optional callback for file-count progress updates
|
|
56
61
|
* @returns Promise that resolves when ZIP is created
|
|
57
62
|
*/
|
|
58
63
|
async function createZipArchive(
|
|
59
64
|
outputPath: string,
|
|
60
65
|
sourceDir: string,
|
|
61
66
|
files?: string[],
|
|
67
|
+
onProgress?: ZipProgressCallback,
|
|
62
68
|
): Promise<void> {
|
|
63
69
|
return new Promise((resolve, reject) => {
|
|
64
70
|
const output = createWriteStream(outputPath);
|
|
@@ -70,13 +76,16 @@ async function createZipArchive(
|
|
|
70
76
|
archive.pipe(output);
|
|
71
77
|
|
|
72
78
|
if (files) {
|
|
73
|
-
// Add specific files
|
|
74
|
-
|
|
79
|
+
// Add specific files with progress tracking
|
|
80
|
+
const total = files.length;
|
|
81
|
+
for (let i = 0; i < files.length; i++) {
|
|
82
|
+
const file = files[i];
|
|
75
83
|
const filePath = join(sourceDir, file);
|
|
76
84
|
archive.file(filePath, { name: file });
|
|
85
|
+
onProgress?.(i + 1, total);
|
|
77
86
|
}
|
|
78
87
|
} else {
|
|
79
|
-
// Add entire directory
|
|
88
|
+
// Add entire directory (no per-file progress available)
|
|
80
89
|
archive.directory(sourceDir, false);
|
|
81
90
|
}
|
|
82
91
|
|
|
@@ -219,18 +228,38 @@ function extractBindingsFromConfig(config?: WranglerConfig): ManifestData["bindi
|
|
|
219
228
|
return Object.keys(bindings).length > 0 ? bindings : undefined;
|
|
220
229
|
}
|
|
221
230
|
|
|
231
|
+
export interface PackageOptions {
|
|
232
|
+
projectPath: string;
|
|
233
|
+
buildOutput: BuildOutput;
|
|
234
|
+
config?: WranglerConfig;
|
|
235
|
+
onProgress?: ZipProgressCallback;
|
|
236
|
+
}
|
|
237
|
+
|
|
222
238
|
/**
|
|
223
239
|
* Packages a built project for deployment to jack cloud
|
|
224
|
-
* @param
|
|
225
|
-
* @param buildOutput - Build output from buildProject()
|
|
226
|
-
* @param config - Optional wrangler config to extract binding intent
|
|
240
|
+
* @param options - Package options including paths, build output, config, and optional progress callback
|
|
227
241
|
* @returns Package result with ZIP paths and cleanup function
|
|
228
242
|
*/
|
|
243
|
+
export async function packageForDeploy(options: PackageOptions): Promise<ZipPackageResult>;
|
|
244
|
+
/**
|
|
245
|
+
* @deprecated Use options object instead
|
|
246
|
+
*/
|
|
229
247
|
export async function packageForDeploy(
|
|
230
248
|
projectPath: string,
|
|
231
249
|
buildOutput: BuildOutput,
|
|
232
250
|
config?: WranglerConfig,
|
|
251
|
+
): Promise<ZipPackageResult>;
|
|
252
|
+
export async function packageForDeploy(
|
|
253
|
+
optionsOrPath: PackageOptions | string,
|
|
254
|
+
buildOutputArg?: BuildOutput,
|
|
255
|
+
configArg?: WranglerConfig,
|
|
233
256
|
): Promise<ZipPackageResult> {
|
|
257
|
+
// Support both old and new signatures
|
|
258
|
+
const options: PackageOptions =
|
|
259
|
+
typeof optionsOrPath === "string"
|
|
260
|
+
? { projectPath: optionsOrPath, buildOutput: buildOutputArg!, config: configArg }
|
|
261
|
+
: optionsOrPath;
|
|
262
|
+
const { projectPath, buildOutput, config, onProgress } = options;
|
|
234
263
|
// Create temp directory for package artifacts
|
|
235
264
|
const packageId = `jack-package-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
|
|
236
265
|
const packageDir = join(tmpdir(), packageId);
|
|
@@ -247,7 +276,7 @@ export async function packageForDeploy(
|
|
|
247
276
|
// 2. Create source.zip from project files (filtered)
|
|
248
277
|
const projectFiles = await scanProjectFiles(projectPath);
|
|
249
278
|
const sourceFiles = projectFiles.map((f) => f.path);
|
|
250
|
-
await createZipArchive(sourceZipPath, projectPath, sourceFiles);
|
|
279
|
+
await createZipArchive(sourceZipPath, projectPath, sourceFiles, onProgress);
|
|
251
280
|
|
|
252
281
|
// 3. Create manifest.json
|
|
253
282
|
const manifest: ManifestData = {
|
package/src/templates/index.ts
CHANGED
|
@@ -10,7 +10,7 @@ import type { Template } from "./types";
|
|
|
10
10
|
// Resolve templates directory relative to this file (src/templates -> templates)
|
|
11
11
|
const TEMPLATES_DIR = join(dirname(dirname(import.meta.dir)), "templates");
|
|
12
12
|
|
|
13
|
-
export const BUILTIN_TEMPLATES = ["hello", "miniapp", "api", "nextjs"];
|
|
13
|
+
export const BUILTIN_TEMPLATES = ["hello", "miniapp", "api", "nextjs", "saas"];
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
16
|
* Resolved template with origin tracking for lineage
|
package/src/templates/types.ts
CHANGED
|
@@ -11,6 +11,7 @@ export type HookAction =
|
|
|
11
11
|
message: string;
|
|
12
12
|
validate?: "json" | "accountAssociation";
|
|
13
13
|
required?: boolean;
|
|
14
|
+
secret?: boolean; // Mask input (for sensitive values like API keys)
|
|
14
15
|
successMessage?: string;
|
|
15
16
|
writeJson?: {
|
|
16
17
|
path: string;
|
|
@@ -31,9 +32,24 @@ export type HookAction =
|
|
|
31
32
|
key: string;
|
|
32
33
|
message?: string;
|
|
33
34
|
setupUrl?: string;
|
|
35
|
+
onMissing?: "fail" | "prompt" | "generate";
|
|
36
|
+
promptMessage?: string;
|
|
37
|
+
generateCommand?: string;
|
|
38
|
+
}
|
|
39
|
+
| {
|
|
40
|
+
action: "stripe-setup";
|
|
41
|
+
message?: string;
|
|
42
|
+
plans: Array<{
|
|
43
|
+
name: string;
|
|
44
|
+
priceKey: string; // Secret key to store price ID (e.g., "STRIPE_PRO_PRICE_ID")
|
|
45
|
+
amount: number; // in cents
|
|
46
|
+
interval: "month" | "year";
|
|
47
|
+
description?: string;
|
|
48
|
+
}>;
|
|
34
49
|
};
|
|
35
50
|
|
|
36
51
|
export interface TemplateHooks {
|
|
52
|
+
preCreate?: HookAction[];
|
|
37
53
|
preDeploy?: HookAction[];
|
|
38
54
|
postDeploy?: HookAction[];
|
|
39
55
|
}
|
package/templates/CLAUDE.md
CHANGED
|
@@ -205,6 +205,109 @@ These variables are substituted at runtime (different from template placeholders
|
|
|
205
205
|
}
|
|
206
206
|
```
|
|
207
207
|
|
|
208
|
+
### Proposed Hook Extensions
|
|
209
|
+
|
|
210
|
+
These extensions are planned to support more complex setup wizards (like SaaS templates with Stripe):
|
|
211
|
+
|
|
212
|
+
#### 1. `require` + `onMissing: "prompt"`
|
|
213
|
+
|
|
214
|
+
Currently `require` fails if a secret is missing. This extension allows prompting the user instead:
|
|
215
|
+
|
|
216
|
+
```json
|
|
217
|
+
{
|
|
218
|
+
"action": "require",
|
|
219
|
+
"source": "secret",
|
|
220
|
+
"key": "STRIPE_SECRET_KEY",
|
|
221
|
+
"onMissing": "prompt",
|
|
222
|
+
"promptMessage": "Enter your Stripe Secret Key (sk_test_...):",
|
|
223
|
+
"setupUrl": "https://dashboard.stripe.com/apikeys"
|
|
224
|
+
}
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
**Behavior:**
|
|
228
|
+
- If secret exists → continue (no change)
|
|
229
|
+
- If secret missing + interactive → prompt user, save to `.secrets.json`
|
|
230
|
+
- If secret missing + non-interactive → fail with setup instructions
|
|
231
|
+
|
|
232
|
+
#### 2. `shell` + `captureAs`
|
|
233
|
+
|
|
234
|
+
Run a command and save its output as a secret or variable:
|
|
235
|
+
|
|
236
|
+
```json
|
|
237
|
+
{
|
|
238
|
+
"action": "shell",
|
|
239
|
+
"command": "stripe listen --print-secret",
|
|
240
|
+
"captureAs": "secret:STRIPE_WEBHOOK_SECRET",
|
|
241
|
+
"message": "Starting Stripe webhook listener..."
|
|
242
|
+
}
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
**Use cases:**
|
|
246
|
+
- Capture Stripe CLI webhook signing secret
|
|
247
|
+
- Capture generated API keys or tokens
|
|
248
|
+
- Capture any CLI output needed for configuration
|
|
249
|
+
|
|
250
|
+
**`captureAs` syntax:**
|
|
251
|
+
- `secret:KEY_NAME` → saves to `.secrets.json`
|
|
252
|
+
- `var:NAME` → saves to hook variables for later hooks
|
|
253
|
+
|
|
254
|
+
#### 3. `prompt` + `saveAs`
|
|
255
|
+
|
|
256
|
+
Currently `prompt` only writes to JSON files via `writeJson`. This extension allows saving input as a secret:
|
|
257
|
+
|
|
258
|
+
```json
|
|
259
|
+
{
|
|
260
|
+
"action": "prompt",
|
|
261
|
+
"message": "Enter your Stripe Webhook Secret (whsec_...):",
|
|
262
|
+
"saveAs": "secret:STRIPE_WEBHOOK_SECRET",
|
|
263
|
+
"validate": "startsWith:whsec_",
|
|
264
|
+
"successMessage": "Webhook secret saved"
|
|
265
|
+
}
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
**Difference from `require+onMissing`:**
|
|
269
|
+
- `require+onMissing` checks first, prompts only if missing
|
|
270
|
+
- `prompt+saveAs` always prompts (for update flows or explicit input)
|
|
271
|
+
|
|
272
|
+
### Design Principles for Hook Extensions
|
|
273
|
+
|
|
274
|
+
When extending the hook system:
|
|
275
|
+
|
|
276
|
+
1. **Extend existing actions** - prefer `require+onMissing` over a new `requireOrPrompt` action
|
|
277
|
+
2. **Reusable primitives** - `captureAs` works on any action that produces output
|
|
278
|
+
3. **Consistent syntax** - `secret:KEY` pattern for writing to `.secrets.json`
|
|
279
|
+
4. **Non-interactive fallback** - every interactive feature must degrade gracefully in CI/MCP
|
|
280
|
+
|
|
281
|
+
### Example: Complex Setup Wizard
|
|
282
|
+
|
|
283
|
+
A SaaS template with Stripe might use these extensions:
|
|
284
|
+
|
|
285
|
+
```json
|
|
286
|
+
{
|
|
287
|
+
"hooks": {
|
|
288
|
+
"preDeploy": [
|
|
289
|
+
{"action": "require", "source": "secret", "key": "BETTER_AUTH_SECRET", "onMissing": "prompt", "promptMessage": "Enter a random secret (32+ chars):"},
|
|
290
|
+
{"action": "require", "source": "secret", "key": "STRIPE_SECRET_KEY", "onMissing": "prompt", "promptMessage": "Enter Stripe Secret Key:", "setupUrl": "https://dashboard.stripe.com/apikeys"}
|
|
291
|
+
],
|
|
292
|
+
"postDeploy": [
|
|
293
|
+
{"action": "box", "title": "Stripe Webhook Setup", "lines": ["1. Go to Stripe Dashboard → Webhooks", "2. Add endpoint: {{url}}/api/auth/stripe/webhook", "3. Select events: checkout.session.completed, customer.subscription.*"]},
|
|
294
|
+
{"action": "url", "url": "https://dashboard.stripe.com/webhooks/create?endpoint_url={{url}}/api/auth/stripe/webhook", "label": "Create webhook"},
|
|
295
|
+
{"action": "prompt", "message": "Paste webhook signing secret (whsec_...):", "saveAs": "secret:STRIPE_WEBHOOK_SECRET", "validate": "startsWith:whsec_"},
|
|
296
|
+
{"action": "message", "text": "Re-deploying with webhook secret..."},
|
|
297
|
+
{"action": "shell", "command": "jack ship --quiet"}
|
|
298
|
+
]
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
This creates a guided wizard that:
|
|
304
|
+
1. Ensures auth secret exists (prompts if missing)
|
|
305
|
+
2. Ensures Stripe key exists (prompts if missing, with setup link)
|
|
306
|
+
3. Deploys the app
|
|
307
|
+
4. Guides user through webhook setup with direct link
|
|
308
|
+
5. Captures webhook secret
|
|
309
|
+
6. Re-deploys with complete configuration
|
|
310
|
+
|
|
208
311
|
## Farcaster Miniapp Embeds
|
|
209
312
|
|
|
210
313
|
When a cast includes a URL, Farcaster scrapes it for `fc:miniapp` meta tags to render a rich embed.
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "saas",
|
|
3
|
+
"description": "SaaS starter (Auth + Payments + React)",
|
|
4
|
+
"secrets": ["STRIPE_SECRET_KEY", "BETTER_AUTH_SECRET", "STRIPE_PRO_PRICE_ID", "STRIPE_ENTERPRISE_PRICE_ID"],
|
|
5
|
+
"capabilities": ["db"],
|
|
6
|
+
"requires": ["DB"],
|
|
7
|
+
"intent": {
|
|
8
|
+
"keywords": ["saas", "subscription", "auth", "payment", "stripe", "membership"],
|
|
9
|
+
"examples": ["subscription app", "paid membership site", "saas product"]
|
|
10
|
+
},
|
|
11
|
+
"agentContext": {
|
|
12
|
+
"summary": "A SaaS starter with Better Auth authentication, Stripe payments, React + Vite frontend, and Hono API on Cloudflare Workers with D1 SQLite database.",
|
|
13
|
+
"full_text": "## Project Structure\n\n- `src/index.ts` - Hono API entry point with auth and payment routes\n- `src/auth.ts` - Better Auth configuration with Stripe plugin\n- `src/client/App.tsx` - React application entry point\n- `src/client/lib/auth-client.ts` - Better Auth client\n- `src/client/hooks/` - useAuth, useSubscription hooks\n- `src/client/pages/` - Page components (HomePage, DashboardPage, etc.)\n- `src/client/components/ui/` - shadcn/ui components\n- `schema.sql` - D1 database schema (user, session, account, subscription)\n- `wrangler.jsonc` - Cloudflare Workers configuration\n\n## Authentication\n\nUses Better Auth with email/password. Auth routes are handled at `/api/auth/*`.\n\n### Client-side\n```tsx\nimport { authClient } from './lib/auth-client';\n\n// Sign up\nawait authClient.signUp.email({ email, password, name });\n\n// Sign in\nawait authClient.signIn.email({ email, password });\n\n// Get session\nconst { data: session } = await authClient.getSession();\n```\n\n## Payments (Stripe)\n\nUses Better Auth Stripe plugin for subscription management.\n\n### Upgrade Flow\n```tsx\nimport { authClient } from './lib/auth-client';\n\nasync function handleUpgrade(plan: 'pro' | 'enterprise') {\n const { data, error } = await authClient.subscription.upgrade({\n plan,\n successUrl: '/dashboard?upgraded=true',\n cancelUrl: '/pricing',\n });\n\n if (data?.url) {\n window.location.href = data.url; // Redirect to Stripe Checkout\n }\n}\n```\n\n### Check Subscription\n```tsx\nconst { data: subscriptions } = await authClient.subscription.list();\nconst activeSubscription = subscriptions?.find(s =>\n s.status === 'active' || s.status === 'trialing'\n);\nconst plan = activeSubscription?.plan || 'free';\n```\n\n## Webhook Setup\n\nStripe webhooks are handled at `/api/auth/stripe/webhook`. Required events:\n- `checkout.session.completed`\n- `customer.subscription.created`\n- `customer.subscription.updated`\n- `customer.subscription.deleted`\n\n## Database Schema\n\nUses Better Auth's default schema (singular table names, camelCase columns):\n- `user` - User accounts\n- `session` - Active sessions\n- `account` - OAuth/password credentials\n- `verification` - Email verification tokens\n- `subscription` - Stripe subscriptions\n\n## Environment Variables\n\n- `BETTER_AUTH_SECRET` - Random secret for auth tokens (generate with: openssl rand -base64 32)\n- `STRIPE_SECRET_KEY` - Stripe API secret key\n- `STRIPE_WEBHOOK_SECRET` - Stripe webhook signing secret (whsec_...)\n- `STRIPE_PRO_PRICE_ID` - Stripe price ID for Pro plan\n- `STRIPE_ENTERPRISE_PRICE_ID` - Stripe price ID for Enterprise plan\n\n## Resources\n\n- [Better Auth Docs](https://www.betterauth.com/docs)\n- [Better Auth Stripe Plugin](https://www.betterauth.com/docs/plugins/stripe)\n- [Stripe Subscriptions](https://docs.stripe.com/billing/subscriptions)\n- [Hono Documentation](https://hono.dev)\n- [Cloudflare D1 Docs](https://developers.cloudflare.com/d1)"
|
|
14
|
+
},
|
|
15
|
+
"hooks": {
|
|
16
|
+
"preCreate": [
|
|
17
|
+
{
|
|
18
|
+
"action": "require",
|
|
19
|
+
"source": "secret",
|
|
20
|
+
"key": "STRIPE_SECRET_KEY",
|
|
21
|
+
"message": "Stripe API key required for payments",
|
|
22
|
+
"setupUrl": "https://dashboard.stripe.com/apikeys",
|
|
23
|
+
"onMissing": "prompt",
|
|
24
|
+
"promptMessage": "Enter your Stripe Secret Key (sk_test_... or sk_live_...):"
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"action": "require",
|
|
28
|
+
"source": "secret",
|
|
29
|
+
"key": "BETTER_AUTH_SECRET",
|
|
30
|
+
"message": "Generating authentication secret...",
|
|
31
|
+
"onMissing": "generate",
|
|
32
|
+
"generateCommand": "openssl rand -base64 32"
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
"action": "stripe-setup",
|
|
36
|
+
"message": "Setting up Stripe subscription plans...",
|
|
37
|
+
"plans": [
|
|
38
|
+
{
|
|
39
|
+
"name": "Pro",
|
|
40
|
+
"priceKey": "STRIPE_PRO_PRICE_ID",
|
|
41
|
+
"amount": 1900,
|
|
42
|
+
"interval": "month",
|
|
43
|
+
"description": "Pro monthly subscription"
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
"name": "Enterprise",
|
|
47
|
+
"priceKey": "STRIPE_ENTERPRISE_PRICE_ID",
|
|
48
|
+
"amount": 9900,
|
|
49
|
+
"interval": "month",
|
|
50
|
+
"description": "Enterprise monthly subscription"
|
|
51
|
+
}
|
|
52
|
+
]
|
|
53
|
+
}
|
|
54
|
+
],
|
|
55
|
+
"preDeploy": [
|
|
56
|
+
{
|
|
57
|
+
"action": "require",
|
|
58
|
+
"source": "secret",
|
|
59
|
+
"key": "STRIPE_SECRET_KEY"
|
|
60
|
+
}
|
|
61
|
+
],
|
|
62
|
+
"postDeploy": [
|
|
63
|
+
{
|
|
64
|
+
"action": "box",
|
|
65
|
+
"title": "Your SaaS is live!",
|
|
66
|
+
"lines": ["{{url}}"]
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
"action": "clipboard",
|
|
70
|
+
"text": "{{url}}/api/auth/stripe/webhook",
|
|
71
|
+
"message": "Webhook URL copied to clipboard"
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
"action": "message",
|
|
75
|
+
"text": ""
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
"action": "message",
|
|
79
|
+
"text": "━━━ Stripe Webhook Setup ━━━"
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
"action": "message",
|
|
83
|
+
"text": ""
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
"action": "message",
|
|
87
|
+
"text": "1. Create webhook endpoint in Stripe"
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
"action": "message",
|
|
91
|
+
"text": "2. Set endpoint URL to: {{url}}/api/auth/stripe/webhook"
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
"action": "message",
|
|
95
|
+
"text": "3. Select these events:"
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
"action": "message",
|
|
99
|
+
"text": " • checkout.session.completed"
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
"action": "message",
|
|
103
|
+
"text": " • customer.subscription.created"
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
"action": "message",
|
|
107
|
+
"text": " • customer.subscription.updated"
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
"action": "message",
|
|
111
|
+
"text": " • customer.subscription.deleted"
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
"action": "message",
|
|
115
|
+
"text": ""
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
"action": "url",
|
|
119
|
+
"url": "https://dashboard.stripe.com/webhooks",
|
|
120
|
+
"label": "Open Stripe webhooks",
|
|
121
|
+
"prompt": true
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
"action": "prompt",
|
|
125
|
+
"message": "Paste your webhook signing secret (whsec_...):",
|
|
126
|
+
"required": false,
|
|
127
|
+
"secret": true,
|
|
128
|
+
"successMessage": "Webhook secret saved!",
|
|
129
|
+
"deployAfter": true,
|
|
130
|
+
"deployMessage": "Deploying with webhook support...",
|
|
131
|
+
"writeJson": {
|
|
132
|
+
"path": ".secrets.json",
|
|
133
|
+
"set": { "STRIPE_WEBHOOK_SECRET": { "from": "input" } }
|
|
134
|
+
}
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
"action": "message",
|
|
138
|
+
"text": ""
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
"action": "message",
|
|
142
|
+
"text": "━━━ Next Steps ━━━"
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
"action": "message",
|
|
146
|
+
"text": "• Test card: 4242 4242 4242 4242 (any future date, any CVC)"
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
"action": "message",
|
|
150
|
+
"text": "• Customize theme: https://ui.shadcn.com/create"
|
|
151
|
+
}
|
|
152
|
+
]
|
|
153
|
+
}
|
|
154
|
+
}
|