@getjack/jack 0.1.17 → 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 +2 -5
- package/src/commands/clone.ts +62 -40
- package/src/commands/down.ts +11 -1
- package/src/commands/init.ts +21 -2
- package/src/commands/services.ts +85 -4
- package/src/commands/sync.ts +9 -0
- package/src/commands/update.ts +10 -0
- package/src/lib/agents.ts +3 -1
- package/src/lib/auth/ensure-auth.test.ts +3 -3
- package/src/lib/control-plane.ts +77 -1
- package/src/lib/deploy-upload.ts +26 -1
- package/src/lib/hooks.ts +232 -1
- package/src/lib/managed-deploy.ts +37 -6
- package/src/lib/output.ts +21 -3
- package/src/lib/progress.ts +231 -0
- package/src/lib/project-list.ts +6 -1
- package/src/lib/project-operations.ts +117 -62
- package/src/lib/project-resolver.ts +1 -1
- package/src/lib/services/db-create.ts +6 -3
- package/src/lib/version-check.ts +14 -0
- package/src/lib/wrangler-config.ts +190 -0
- package/src/lib/zip-packager.ts +74 -7
- package/src/lib/zip-utils.ts +38 -0
- 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
package/src/lib/zip-packager.ts
CHANGED
|
@@ -6,6 +6,8 @@ import { join, relative } from "node:path";
|
|
|
6
6
|
import archiver from "archiver";
|
|
7
7
|
import { type AssetManifest, computeAssetHash } from "./asset-hash.ts";
|
|
8
8
|
import type { BuildOutput, WranglerConfig } from "./build-helper.ts";
|
|
9
|
+
import { debug } from "./debug.ts";
|
|
10
|
+
import { formatSize } from "./format.ts";
|
|
9
11
|
import { scanProjectFiles } from "./storage/file-filter.ts";
|
|
10
12
|
|
|
11
13
|
export interface ZipPackageResult {
|
|
@@ -46,17 +48,23 @@ export interface ManifestData {
|
|
|
46
48
|
};
|
|
47
49
|
}
|
|
48
50
|
|
|
51
|
+
export interface ZipProgressCallback {
|
|
52
|
+
(current: number, total: number): void;
|
|
53
|
+
}
|
|
54
|
+
|
|
49
55
|
/**
|
|
50
56
|
* Creates a ZIP archive from source directory
|
|
51
57
|
* @param outputPath - Absolute path for output ZIP file
|
|
52
58
|
* @param sourceDir - Absolute path to directory to archive
|
|
53
59
|
* @param files - Optional list of specific files to include (relative to sourceDir)
|
|
60
|
+
* @param onProgress - Optional callback for file-count progress updates
|
|
54
61
|
* @returns Promise that resolves when ZIP is created
|
|
55
62
|
*/
|
|
56
63
|
async function createZipArchive(
|
|
57
64
|
outputPath: string,
|
|
58
65
|
sourceDir: string,
|
|
59
66
|
files?: string[],
|
|
67
|
+
onProgress?: ZipProgressCallback,
|
|
60
68
|
): Promise<void> {
|
|
61
69
|
return new Promise((resolve, reject) => {
|
|
62
70
|
const output = createWriteStream(outputPath);
|
|
@@ -68,13 +76,16 @@ async function createZipArchive(
|
|
|
68
76
|
archive.pipe(output);
|
|
69
77
|
|
|
70
78
|
if (files) {
|
|
71
|
-
// Add specific files
|
|
72
|
-
|
|
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];
|
|
73
83
|
const filePath = join(sourceDir, file);
|
|
74
84
|
archive.file(filePath, { name: file });
|
|
85
|
+
onProgress?.(i + 1, total);
|
|
75
86
|
}
|
|
76
87
|
} else {
|
|
77
|
-
// Add entire directory
|
|
88
|
+
// Add entire directory (no per-file progress available)
|
|
78
89
|
archive.directory(sourceDir, false);
|
|
79
90
|
}
|
|
80
91
|
|
|
@@ -82,6 +93,42 @@ async function createZipArchive(
|
|
|
82
93
|
});
|
|
83
94
|
}
|
|
84
95
|
|
|
96
|
+
/**
|
|
97
|
+
* Creates source.zip from project files without building.
|
|
98
|
+
* Used for prebuilt deployments that skip the full package flow.
|
|
99
|
+
* @param projectPath - Absolute path to project directory
|
|
100
|
+
* @returns Path to the created source.zip (caller responsible for cleanup)
|
|
101
|
+
*/
|
|
102
|
+
export async function createSourceZip(projectPath: string): Promise<string> {
|
|
103
|
+
const packageDir = join(tmpdir(), `jack-source-${Date.now()}`);
|
|
104
|
+
await mkdir(packageDir, { recursive: true });
|
|
105
|
+
|
|
106
|
+
const sourceZipPath = join(packageDir, "source.zip");
|
|
107
|
+
const projectFiles = await scanProjectFiles(projectPath);
|
|
108
|
+
|
|
109
|
+
// Debug output for source file statistics
|
|
110
|
+
const totalSize = projectFiles.reduce((sum, f) => sum + f.size, 0);
|
|
111
|
+
const largest =
|
|
112
|
+
projectFiles.length > 0
|
|
113
|
+
? projectFiles.reduce((max, f) => (f.size > max.size ? f : max), projectFiles[0])
|
|
114
|
+
: null;
|
|
115
|
+
|
|
116
|
+
debug(`Source: ${projectFiles.length} files, ${formatSize(totalSize)} uncompressed`);
|
|
117
|
+
if (largest) {
|
|
118
|
+
debug(`Largest: ${largest.path} (${formatSize(largest.size)})`);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const sourceFiles = projectFiles.map((f) => f.path);
|
|
122
|
+
await createZipArchive(sourceZipPath, projectPath, sourceFiles);
|
|
123
|
+
|
|
124
|
+
// Debug output for compression statistics
|
|
125
|
+
const zipStats = await stat(sourceZipPath);
|
|
126
|
+
const ratio = totalSize > 0 ? ((1 - zipStats.size / totalSize) * 100).toFixed(0) : 0;
|
|
127
|
+
debug(`Compressed: ${formatSize(zipStats.size)} (${ratio}% reduction)`);
|
|
128
|
+
|
|
129
|
+
return sourceZipPath;
|
|
130
|
+
}
|
|
131
|
+
|
|
85
132
|
/**
|
|
86
133
|
* Recursively collects all file paths in a directory
|
|
87
134
|
*/
|
|
@@ -181,18 +228,38 @@ function extractBindingsFromConfig(config?: WranglerConfig): ManifestData["bindi
|
|
|
181
228
|
return Object.keys(bindings).length > 0 ? bindings : undefined;
|
|
182
229
|
}
|
|
183
230
|
|
|
231
|
+
export interface PackageOptions {
|
|
232
|
+
projectPath: string;
|
|
233
|
+
buildOutput: BuildOutput;
|
|
234
|
+
config?: WranglerConfig;
|
|
235
|
+
onProgress?: ZipProgressCallback;
|
|
236
|
+
}
|
|
237
|
+
|
|
184
238
|
/**
|
|
185
239
|
* Packages a built project for deployment to jack cloud
|
|
186
|
-
* @param
|
|
187
|
-
* @param buildOutput - Build output from buildProject()
|
|
188
|
-
* @param config - Optional wrangler config to extract binding intent
|
|
240
|
+
* @param options - Package options including paths, build output, config, and optional progress callback
|
|
189
241
|
* @returns Package result with ZIP paths and cleanup function
|
|
190
242
|
*/
|
|
243
|
+
export async function packageForDeploy(options: PackageOptions): Promise<ZipPackageResult>;
|
|
244
|
+
/**
|
|
245
|
+
* @deprecated Use options object instead
|
|
246
|
+
*/
|
|
191
247
|
export async function packageForDeploy(
|
|
192
248
|
projectPath: string,
|
|
193
249
|
buildOutput: BuildOutput,
|
|
194
250
|
config?: WranglerConfig,
|
|
251
|
+
): Promise<ZipPackageResult>;
|
|
252
|
+
export async function packageForDeploy(
|
|
253
|
+
optionsOrPath: PackageOptions | string,
|
|
254
|
+
buildOutputArg?: BuildOutput,
|
|
255
|
+
configArg?: WranglerConfig,
|
|
195
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;
|
|
196
263
|
// Create temp directory for package artifacts
|
|
197
264
|
const packageId = `jack-package-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
|
|
198
265
|
const packageDir = join(tmpdir(), packageId);
|
|
@@ -209,7 +276,7 @@ export async function packageForDeploy(
|
|
|
209
276
|
// 2. Create source.zip from project files (filtered)
|
|
210
277
|
const projectFiles = await scanProjectFiles(projectPath);
|
|
211
278
|
const sourceFiles = projectFiles.map((f) => f.path);
|
|
212
|
-
await createZipArchive(sourceZipPath, projectPath, sourceFiles);
|
|
279
|
+
await createZipArchive(sourceZipPath, projectPath, sourceFiles, onProgress);
|
|
213
280
|
|
|
214
281
|
// 3. Create manifest.json
|
|
215
282
|
const manifest: ManifestData = {
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Zip utility functions for extracting zip archives
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
6
|
+
import { dirname, join } from "node:path";
|
|
7
|
+
import { unzipSync } from "fflate";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Extract a zip buffer to a target directory.
|
|
11
|
+
* Creates directories as needed and writes files preserving relative paths.
|
|
12
|
+
*
|
|
13
|
+
* @param zipBuffer - The zip file contents as a Buffer
|
|
14
|
+
* @param targetDir - The directory to extract files to
|
|
15
|
+
* @returns The number of files extracted
|
|
16
|
+
*/
|
|
17
|
+
export async function extractZipToDirectory(zipBuffer: Buffer, targetDir: string): Promise<number> {
|
|
18
|
+
const unzipped = unzipSync(new Uint8Array(zipBuffer));
|
|
19
|
+
let fileCount = 0;
|
|
20
|
+
|
|
21
|
+
for (const [path, content] of Object.entries(unzipped)) {
|
|
22
|
+
// Skip directories (they end with /)
|
|
23
|
+
if (path.endsWith("/")) continue;
|
|
24
|
+
|
|
25
|
+
// Security: prevent path traversal by removing any .. segments
|
|
26
|
+
const normalizedPath = path.replace(/\.\./g, "");
|
|
27
|
+
const fullPath = join(targetDir, normalizedPath);
|
|
28
|
+
|
|
29
|
+
// Ensure directory exists
|
|
30
|
+
await mkdir(dirname(fullPath), { recursive: true });
|
|
31
|
+
|
|
32
|
+
// Write file
|
|
33
|
+
await writeFile(fullPath, content);
|
|
34
|
+
fileCount++;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return fileCount;
|
|
38
|
+
}
|
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
|
+
}
|