@getjack/jack 0.1.20 → 0.1.22
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 +5 -2
- package/src/commands/new.ts +56 -4
- package/src/commands/publish.ts +1 -1
- package/src/lib/managed-down.ts +66 -45
- package/src/lib/project-operations.ts +7 -31
- package/templates/CLAUDE.md +117 -53
package/package.json
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@getjack/jack",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.22",
|
|
4
4
|
"description": "Ship before you forget why you started. The vibecoder's deployment CLI.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"bin": {
|
|
8
8
|
"jack": "./src/index.ts"
|
|
9
9
|
},
|
|
10
|
-
"files": [
|
|
10
|
+
"files": [
|
|
11
|
+
"src",
|
|
12
|
+
"templates"
|
|
13
|
+
],
|
|
11
14
|
"engines": {
|
|
12
15
|
"bun": ">=1.0.0"
|
|
13
16
|
},
|
package/src/commands/new.ts
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
getAgentDefinition,
|
|
3
|
+
getPreferredLaunchAgent,
|
|
4
|
+
launchAgent,
|
|
5
|
+
scanAgents,
|
|
6
|
+
updateAgent,
|
|
7
|
+
} from "../lib/agents.ts";
|
|
2
8
|
import { debug } from "../lib/debug.ts";
|
|
3
9
|
import { getErrorDetails } from "../lib/errors.ts";
|
|
4
10
|
import { promptSelect } from "../lib/hooks.ts";
|
|
@@ -120,9 +126,55 @@ export default async function newProject(
|
|
|
120
126
|
}
|
|
121
127
|
}
|
|
122
128
|
} else {
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
129
|
+
// No agents configured - auto-scan and offer to open in detected agents
|
|
130
|
+
const detectionResult = await scanAgents();
|
|
131
|
+
|
|
132
|
+
if (detectionResult.detected.length > 0) {
|
|
133
|
+
// Auto-enable newly detected agents
|
|
134
|
+
for (const { id, path, launch } of detectionResult.detected) {
|
|
135
|
+
await updateAgent(id, {
|
|
136
|
+
active: true,
|
|
137
|
+
path,
|
|
138
|
+
detectedAt: new Date().toISOString(),
|
|
139
|
+
launch,
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Build menu options: detected agents + Skip
|
|
144
|
+
const menuOptions = detectionResult.detected.map(({ id }) => {
|
|
145
|
+
const definition = getAgentDefinition(id);
|
|
146
|
+
return definition?.name ?? id;
|
|
147
|
+
});
|
|
148
|
+
menuOptions.push("Skip");
|
|
149
|
+
|
|
150
|
+
console.error("");
|
|
151
|
+
console.error(" Open project in:");
|
|
152
|
+
console.error("");
|
|
153
|
+
const choice = await promptSelect(menuOptions);
|
|
154
|
+
|
|
155
|
+
// Launch selected agent (unless Skip or cancelled)
|
|
156
|
+
if (choice >= 0 && choice < detectionResult.detected.length) {
|
|
157
|
+
const selected = detectionResult.detected[choice];
|
|
158
|
+
const launchConfig = selected.launch;
|
|
159
|
+
if (launchConfig) {
|
|
160
|
+
const launchResult = await launchAgent(launchConfig, result.targetDir, {
|
|
161
|
+
projectName: result.projectName,
|
|
162
|
+
url: result.workerUrl,
|
|
163
|
+
});
|
|
164
|
+
if (!launchResult.success) {
|
|
165
|
+
const definition = getAgentDefinition(selected.id);
|
|
166
|
+
output.warn(`Failed to launch ${definition?.name ?? selected.id}`);
|
|
167
|
+
if (launchResult.command?.length) {
|
|
168
|
+
output.info(`Run manually: ${launchResult.command.join(" ")}`);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
} else {
|
|
174
|
+
console.error("");
|
|
175
|
+
output.info("No AI agents detected");
|
|
176
|
+
output.info("Install Claude Code or Codex, then run: jack agents scan");
|
|
177
|
+
}
|
|
126
178
|
}
|
|
127
179
|
}
|
|
128
180
|
}
|
package/src/commands/publish.ts
CHANGED
|
@@ -39,7 +39,7 @@ export default async function publish(): Promise<void> {
|
|
|
39
39
|
output.success(`Published as ${result.published_as}`);
|
|
40
40
|
|
|
41
41
|
console.error("");
|
|
42
|
-
output.info("
|
|
42
|
+
output.info("Share this project:");
|
|
43
43
|
output.info(` ${result.fork_command}`);
|
|
44
44
|
} catch (err) {
|
|
45
45
|
spin.stop();
|
package/src/lib/managed-down.ts
CHANGED
|
@@ -5,7 +5,11 @@
|
|
|
5
5
|
|
|
6
6
|
import { writeFile } from "node:fs/promises";
|
|
7
7
|
import { join } from "node:path";
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
deleteManagedProject,
|
|
10
|
+
exportManagedDatabase,
|
|
11
|
+
fetchProjectResources,
|
|
12
|
+
} from "./control-plane.ts";
|
|
9
13
|
import { promptSelect } from "./hooks.ts";
|
|
10
14
|
import { error, info, item, output, success, warn } from "./output.ts";
|
|
11
15
|
|
|
@@ -47,14 +51,29 @@ export async function managedDown(
|
|
|
47
51
|
}
|
|
48
52
|
}
|
|
49
53
|
|
|
50
|
-
// Interactive mode
|
|
54
|
+
// Interactive mode - fetch actual resources
|
|
55
|
+
let hasDatabase = false;
|
|
56
|
+
let databaseName: string | null = null;
|
|
57
|
+
try {
|
|
58
|
+
const resources = await fetchProjectResources(projectId);
|
|
59
|
+
const d1Resource = resources.find((r) => r.resource_type === "d1");
|
|
60
|
+
if (d1Resource) {
|
|
61
|
+
hasDatabase = true;
|
|
62
|
+
databaseName = d1Resource.resource_name;
|
|
63
|
+
}
|
|
64
|
+
} catch {
|
|
65
|
+
// If fetch fails, assume no database (safer than showing wrong info)
|
|
66
|
+
}
|
|
67
|
+
|
|
51
68
|
console.error("");
|
|
52
69
|
info(`Project: ${projectName}`);
|
|
53
70
|
if (runjackUrl) {
|
|
54
71
|
item(`URL: ${runjackUrl}`);
|
|
55
72
|
}
|
|
56
73
|
item("Mode: jack cloud (managed)");
|
|
57
|
-
|
|
74
|
+
if (hasDatabase) {
|
|
75
|
+
item(`Database: ${databaseName ?? "managed D1"}`);
|
|
76
|
+
}
|
|
58
77
|
console.error("");
|
|
59
78
|
|
|
60
79
|
// Confirm undeploy
|
|
@@ -67,49 +86,51 @@ export async function managedDown(
|
|
|
67
86
|
return false;
|
|
68
87
|
}
|
|
69
88
|
|
|
70
|
-
// Ask about database export
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
console.error("");
|
|
75
|
-
info("Export database before deleting?");
|
|
76
|
-
const exportAction = await promptSelect(["Yes", "No"]);
|
|
77
|
-
|
|
78
|
-
if (exportAction === 0) {
|
|
79
|
-
const exportPath = join(process.cwd(), `${projectName}-backup.sql`);
|
|
80
|
-
output.start(`Exporting database to ${exportPath}...`);
|
|
81
|
-
|
|
82
|
-
try {
|
|
83
|
-
const exportResult = await exportManagedDatabase(projectId);
|
|
84
|
-
|
|
85
|
-
// Download the SQL file
|
|
86
|
-
const response = await fetch(exportResult.download_url);
|
|
87
|
-
if (!response.ok) {
|
|
88
|
-
throw new Error(`Failed to download export: ${response.statusText}`);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
const sqlContent = await response.text();
|
|
92
|
-
await writeFile(exportPath, sqlContent, "utf-8");
|
|
93
|
-
|
|
94
|
-
output.stop();
|
|
95
|
-
success(`Database exported to ${exportPath}`);
|
|
96
|
-
} catch (err) {
|
|
97
|
-
output.stop();
|
|
98
|
-
error(`Failed to export database: ${err instanceof Error ? err.message : String(err)}`);
|
|
99
|
-
|
|
100
|
-
// If export times out, abort
|
|
101
|
-
if (err instanceof Error && err.message.includes("timed out")) {
|
|
102
|
-
error("Export timeout - deletion aborted");
|
|
103
|
-
return false;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
console.error("");
|
|
107
|
-
info("Continue without exporting?");
|
|
108
|
-
const continueAction = await promptSelect(["Yes", "No"]);
|
|
89
|
+
// Ask about database export (only if database exists)
|
|
90
|
+
if (hasDatabase) {
|
|
91
|
+
console.error("");
|
|
92
|
+
info("Database will be deleted with the project");
|
|
109
93
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
94
|
+
console.error("");
|
|
95
|
+
info("Export database before deleting?");
|
|
96
|
+
const exportAction = await promptSelect(["Yes", "No"]);
|
|
97
|
+
|
|
98
|
+
if (exportAction === 0) {
|
|
99
|
+
const exportPath = join(process.cwd(), `${projectName}-backup.sql`);
|
|
100
|
+
output.start(`Exporting database to ${exportPath}...`);
|
|
101
|
+
|
|
102
|
+
try {
|
|
103
|
+
const exportResult = await exportManagedDatabase(projectId);
|
|
104
|
+
|
|
105
|
+
// Download the SQL file
|
|
106
|
+
const response = await fetch(exportResult.download_url);
|
|
107
|
+
if (!response.ok) {
|
|
108
|
+
throw new Error(`Failed to download export: ${response.statusText}`);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const sqlContent = await response.text();
|
|
112
|
+
await writeFile(exportPath, sqlContent, "utf-8");
|
|
113
|
+
|
|
114
|
+
output.stop();
|
|
115
|
+
success(`Database exported to ${exportPath}`);
|
|
116
|
+
} catch (err) {
|
|
117
|
+
output.stop();
|
|
118
|
+
error(`Failed to export database: ${err instanceof Error ? err.message : String(err)}`);
|
|
119
|
+
|
|
120
|
+
// If export times out, abort
|
|
121
|
+
if (err instanceof Error && err.message.includes("timed out")) {
|
|
122
|
+
error("Export timeout - deletion aborted");
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
console.error("");
|
|
127
|
+
info("Continue without exporting?");
|
|
128
|
+
const continueAction = await promptSelect(["Yes", "No"]);
|
|
129
|
+
|
|
130
|
+
if (continueAction !== 0) {
|
|
131
|
+
info("Cancelled");
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
113
134
|
}
|
|
114
135
|
}
|
|
115
136
|
}
|
|
@@ -825,43 +825,19 @@ export async function createProject(
|
|
|
825
825
|
reporter.stop();
|
|
826
826
|
reporter.success("Name available");
|
|
827
827
|
} else {
|
|
828
|
-
// Slug not available - check if it's the user's own project
|
|
828
|
+
// Slug not available - check if it's the user's own project
|
|
829
829
|
const { checkAvailability } = await import("./project-resolver.ts");
|
|
830
830
|
const { existingProject } = await checkAvailability(projectName);
|
|
831
831
|
timings.push({ label: "Slug check", duration: timerEnd("slug-check") });
|
|
832
832
|
reporter.stop();
|
|
833
833
|
|
|
834
834
|
if (existingProject?.sources.controlPlane && !existingProject.sources.filesystem) {
|
|
835
|
-
//
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
const choice = await promptSelect(["Link existing project", "Choose different name"]);
|
|
843
|
-
|
|
844
|
-
if (choice === 0) {
|
|
845
|
-
// User chose to link - proceed with project creation
|
|
846
|
-
reporter.success(`Linking to existing project: ${existingProject.url || projectName}`);
|
|
847
|
-
// Continue with project creation - user wants to link
|
|
848
|
-
} else {
|
|
849
|
-
// User chose different name
|
|
850
|
-
throw new JackError(
|
|
851
|
-
JackErrorCode.VALIDATION_ERROR,
|
|
852
|
-
`Project "${projectName}" already exists on jack cloud`,
|
|
853
|
-
`Try a different name: jack new ${projectName}-2`,
|
|
854
|
-
{ exitCode: 0, reported: true },
|
|
855
|
-
);
|
|
856
|
-
}
|
|
857
|
-
} else {
|
|
858
|
-
// Non-interactive mode - fail with clear message
|
|
859
|
-
throw new JackError(
|
|
860
|
-
JackErrorCode.VALIDATION_ERROR,
|
|
861
|
-
`Project "${projectName}" already exists on jack cloud`,
|
|
862
|
-
`Try a different name: jack new ${projectName}-2`,
|
|
863
|
-
);
|
|
864
|
-
}
|
|
835
|
+
// User's project exists on jack cloud but not locally - suggest clone
|
|
836
|
+
throw new JackError(
|
|
837
|
+
JackErrorCode.VALIDATION_ERROR,
|
|
838
|
+
`Project "${projectName}" already exists on jack cloud`,
|
|
839
|
+
`To download it: jack clone ${projectName}`,
|
|
840
|
+
);
|
|
865
841
|
} else if (existingProject) {
|
|
866
842
|
// Project exists in registry with local path - it's truly taken by user
|
|
867
843
|
throw new JackError(
|
package/templates/CLAUDE.md
CHANGED
|
@@ -119,16 +119,18 @@ Templates can define hooks in `.jack.json` that run at specific lifecycle points
|
|
|
119
119
|
| `url` | `url` | Prints label + URL |
|
|
120
120
|
| `clipboard` | `text` | Prints text |
|
|
121
121
|
| `pause` | _(none)_ | Skipped |
|
|
122
|
-
| `require` | `source`, `key` | Validates, prints setup if provided |
|
|
122
|
+
| `require` | `source`, `key` | Validates, prints setup if provided. Supports `onMissing: "prompt" \| "generate"` |
|
|
123
123
|
| `shell` | `command` | Runs with stdin ignored |
|
|
124
|
-
| `prompt` | `message` | Skipped
|
|
124
|
+
| `prompt` | `message` | Skipped. Supports `secret: true` for masked input, `validate`, `writeJson`, `deployAfter` |
|
|
125
125
|
| `writeJson` | `path`, `set` | Runs (safe in CI) |
|
|
126
|
+
| `stripe-setup` | `plans` | Creates Stripe products/prices, saves price IDs to secrets |
|
|
126
127
|
|
|
127
128
|
### Hook Lifecycle
|
|
128
129
|
|
|
129
130
|
```json
|
|
130
131
|
{
|
|
131
132
|
"hooks": {
|
|
133
|
+
"preCreate": [...], // During project creation (secret collection, auto-generation)
|
|
132
134
|
"preDeploy": [...], // Before wrangler deploy (validation)
|
|
133
135
|
"postDeploy": [...] // After successful deploy (notifications, testing)
|
|
134
136
|
}
|
|
@@ -145,9 +147,10 @@ Templates can define hooks in `.jack.json` that run at specific lifecycle points
|
|
|
145
147
|
| `clipboard` | Copy text to clipboard | `{"action": "clipboard", "text": "{{url}}", "message": "Copied!"}` |
|
|
146
148
|
| `shell` | Execute shell command | `{"action": "shell", "command": "curl {{url}}/health"}` |
|
|
147
149
|
| `pause` | Wait for Enter key | `{"action": "pause", "message": "Press Enter..."}` |
|
|
148
|
-
| `require` | Verify secret or
|
|
149
|
-
| `prompt` | Prompt for input
|
|
150
|
+
| `require` | Verify secret/env, optionally prompt or generate | `{"action": "require", "source": "secret", "key": "API_KEY", "onMissing": "prompt"}` |
|
|
151
|
+
| `prompt` | Prompt for input, optionally masked | `{"action": "prompt", "message": "Secret:", "secret": true, "writeJson": {...}}` |
|
|
150
152
|
| `writeJson` | Update JSON file with template vars | `{"action": "writeJson", "path": "public/data.json", "set": {"siteUrl": "{{url}}"}}` |
|
|
153
|
+
| `stripe-setup` | Create Stripe products/prices | `{"action": "stripe-setup", "plans": [{"name": "Pro", "priceKey": "STRIPE_PRO_PRICE_ID", "amount": 1900, "interval": "month"}]}` |
|
|
151
154
|
|
|
152
155
|
### Non-Interactive Mode
|
|
153
156
|
|
|
@@ -169,7 +172,7 @@ These variables are substituted at runtime (different from template placeholders
|
|
|
169
172
|
|----------|-------|--------------|
|
|
170
173
|
| `{{url}}` | Full deployed URL | postDeploy |
|
|
171
174
|
| `{{domain}}` | Domain without protocol | postDeploy |
|
|
172
|
-
| `{{name}}` | Project name | preDeploy, postDeploy |
|
|
175
|
+
| `{{name}}` | Project name | preCreate, preDeploy, postDeploy |
|
|
173
176
|
|
|
174
177
|
### Example: API Template Hooks
|
|
175
178
|
|
|
@@ -205,13 +208,13 @@ These variables are substituted at runtime (different from template placeholders
|
|
|
205
208
|
}
|
|
206
209
|
```
|
|
207
210
|
|
|
208
|
-
###
|
|
211
|
+
### Advanced Hook Features
|
|
209
212
|
|
|
210
|
-
These
|
|
213
|
+
These features support complex setup wizards (like the SaaS template with Stripe):
|
|
211
214
|
|
|
212
|
-
#### 1. `require` + `onMissing: "prompt"`
|
|
215
|
+
#### 1. `require` + `onMissing: "prompt" | "generate"`
|
|
213
216
|
|
|
214
|
-
|
|
217
|
+
The `require` action supports automatic secret collection when a secret is missing:
|
|
215
218
|
|
|
216
219
|
```json
|
|
217
220
|
{
|
|
@@ -225,88 +228,149 @@ Currently `require` fails if a secret is missing. This extension allows promptin
|
|
|
225
228
|
```
|
|
226
229
|
|
|
227
230
|
**Behavior:**
|
|
228
|
-
- If secret exists → continue (
|
|
231
|
+
- If secret exists → continue (shows "Using saved KEY")
|
|
229
232
|
- If secret missing + interactive → prompt user, save to `.secrets.json`
|
|
230
233
|
- If secret missing + non-interactive → fail with setup instructions
|
|
231
234
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
Run a command and save its output as a secret or variable:
|
|
235
|
+
**Auto-generate secrets with `onMissing: "generate"`:**
|
|
235
236
|
|
|
236
237
|
```json
|
|
237
238
|
{
|
|
238
|
-
"action": "
|
|
239
|
-
"
|
|
240
|
-
"
|
|
241
|
-
"message": "
|
|
239
|
+
"action": "require",
|
|
240
|
+
"source": "secret",
|
|
241
|
+
"key": "BETTER_AUTH_SECRET",
|
|
242
|
+
"message": "Generating authentication secret...",
|
|
243
|
+
"onMissing": "generate",
|
|
244
|
+
"generateCommand": "openssl rand -base64 32"
|
|
242
245
|
}
|
|
243
246
|
```
|
|
244
247
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
248
|
+
This runs the command, captures stdout, and saves it as the secret automatically.
|
|
249
|
+
|
|
250
|
+
#### 2. `stripe-setup` Action
|
|
251
|
+
|
|
252
|
+
Automatically creates Stripe products and prices, saving the price IDs as secrets:
|
|
253
|
+
|
|
254
|
+
```json
|
|
255
|
+
{
|
|
256
|
+
"action": "stripe-setup",
|
|
257
|
+
"message": "Setting up Stripe subscription plans...",
|
|
258
|
+
"plans": [
|
|
259
|
+
{
|
|
260
|
+
"name": "Pro",
|
|
261
|
+
"priceKey": "STRIPE_PRO_PRICE_ID",
|
|
262
|
+
"amount": 1900,
|
|
263
|
+
"interval": "month",
|
|
264
|
+
"description": "Pro monthly subscription"
|
|
265
|
+
},
|
|
266
|
+
{
|
|
267
|
+
"name": "Enterprise",
|
|
268
|
+
"priceKey": "STRIPE_ENTERPRISE_PRICE_ID",
|
|
269
|
+
"amount": 9900,
|
|
270
|
+
"interval": "month"
|
|
271
|
+
}
|
|
272
|
+
]
|
|
273
|
+
}
|
|
274
|
+
```
|
|
249
275
|
|
|
250
|
-
|
|
251
|
-
- `
|
|
252
|
-
-
|
|
276
|
+
**Behavior:**
|
|
277
|
+
- Requires `STRIPE_SECRET_KEY` to be set first
|
|
278
|
+
- Checks for existing prices by lookup key (`jack_pro_month`)
|
|
279
|
+
- Creates product + price if not found
|
|
280
|
+
- Saves price IDs to secrets
|
|
253
281
|
|
|
254
|
-
#### 3. `prompt`
|
|
282
|
+
#### 3. `prompt` with `secret` Flag
|
|
255
283
|
|
|
256
|
-
|
|
284
|
+
Mask sensitive input (like API keys):
|
|
257
285
|
|
|
258
286
|
```json
|
|
259
287
|
{
|
|
260
288
|
"action": "prompt",
|
|
261
|
-
"message": "
|
|
262
|
-
"
|
|
263
|
-
"
|
|
264
|
-
|
|
289
|
+
"message": "Paste your webhook signing secret (whsec_...):",
|
|
290
|
+
"secret": true,
|
|
291
|
+
"writeJson": {
|
|
292
|
+
"path": ".secrets.json",
|
|
293
|
+
"set": { "STRIPE_WEBHOOK_SECRET": { "from": "input" } }
|
|
294
|
+
}
|
|
265
295
|
}
|
|
266
296
|
```
|
|
267
297
|
|
|
268
|
-
|
|
269
|
-
- `require+onMissing` checks first, prompts only if missing
|
|
270
|
-
- `prompt+saveAs` always prompts (for update flows or explicit input)
|
|
298
|
+
#### 4. `prompt` with `deployAfter`
|
|
271
299
|
|
|
272
|
-
|
|
300
|
+
Automatically redeploy after user provides input:
|
|
301
|
+
|
|
302
|
+
```json
|
|
303
|
+
{
|
|
304
|
+
"action": "prompt",
|
|
305
|
+
"message": "Paste webhook signing secret:",
|
|
306
|
+
"secret": true,
|
|
307
|
+
"deployAfter": true,
|
|
308
|
+
"deployMessage": "Deploying with webhook support...",
|
|
309
|
+
"writeJson": {
|
|
310
|
+
"path": ".secrets.json",
|
|
311
|
+
"set": { "STRIPE_WEBHOOK_SECRET": { "from": "input" } }
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
### Design Principles
|
|
273
317
|
|
|
274
318
|
When extending the hook system:
|
|
275
319
|
|
|
276
320
|
1. **Extend existing actions** - prefer `require+onMissing` over a new `requireOrPrompt` action
|
|
277
|
-
2. **
|
|
278
|
-
3. **
|
|
279
|
-
4. **Non-interactive fallback** - every interactive feature must degrade gracefully in CI/MCP
|
|
321
|
+
2. **Non-interactive fallback** - every interactive feature must degrade gracefully in CI/MCP
|
|
322
|
+
3. **Secrets via `.secrets.json`** - use `writeJson` with `.secrets.json` for secret storage
|
|
280
323
|
|
|
281
|
-
### Example:
|
|
324
|
+
### Example: SaaS Template Setup Wizard
|
|
282
325
|
|
|
283
|
-
|
|
326
|
+
The `saas` template uses `preCreate` hooks for a complete setup wizard:
|
|
284
327
|
|
|
285
328
|
```json
|
|
286
329
|
{
|
|
287
330
|
"hooks": {
|
|
288
|
-
"
|
|
289
|
-
{
|
|
290
|
-
|
|
331
|
+
"preCreate": [
|
|
332
|
+
{
|
|
333
|
+
"action": "require",
|
|
334
|
+
"source": "secret",
|
|
335
|
+
"key": "STRIPE_SECRET_KEY",
|
|
336
|
+
"message": "Stripe API key required for payments",
|
|
337
|
+
"setupUrl": "https://dashboard.stripe.com/apikeys",
|
|
338
|
+
"onMissing": "prompt",
|
|
339
|
+
"promptMessage": "Enter your Stripe Secret Key (sk_test_... or sk_live_...):"
|
|
340
|
+
},
|
|
341
|
+
{
|
|
342
|
+
"action": "require",
|
|
343
|
+
"source": "secret",
|
|
344
|
+
"key": "BETTER_AUTH_SECRET",
|
|
345
|
+
"message": "Generating authentication secret...",
|
|
346
|
+
"onMissing": "generate",
|
|
347
|
+
"generateCommand": "openssl rand -base64 32"
|
|
348
|
+
},
|
|
349
|
+
{
|
|
350
|
+
"action": "stripe-setup",
|
|
351
|
+
"message": "Setting up Stripe subscription plans...",
|
|
352
|
+
"plans": [
|
|
353
|
+
{"name": "Pro", "priceKey": "STRIPE_PRO_PRICE_ID", "amount": 1900, "interval": "month"},
|
|
354
|
+
{"name": "Enterprise", "priceKey": "STRIPE_ENTERPRISE_PRICE_ID", "amount": 9900, "interval": "month"}
|
|
355
|
+
]
|
|
356
|
+
}
|
|
291
357
|
],
|
|
292
358
|
"postDeploy": [
|
|
293
|
-
{"action": "box", "title": "
|
|
294
|
-
{"action": "
|
|
295
|
-
{"action": "prompt", "message": "Paste webhook signing secret (whsec_...):", "
|
|
296
|
-
{"action": "message", "text": "Re-deploying with webhook secret..."},
|
|
297
|
-
{"action": "shell", "command": "jack ship --quiet"}
|
|
359
|
+
{"action": "box", "title": "Your SaaS is live!", "lines": ["{{url}}"]},
|
|
360
|
+
{"action": "clipboard", "text": "{{url}}/api/auth/stripe/webhook", "message": "Webhook URL copied"},
|
|
361
|
+
{"action": "prompt", "message": "Paste your webhook signing secret (whsec_...):", "secret": true, "deployAfter": true, "writeJson": {"path": ".secrets.json", "set": {"STRIPE_WEBHOOK_SECRET": {"from": "input"}}}}
|
|
298
362
|
]
|
|
299
363
|
}
|
|
300
364
|
}
|
|
301
365
|
```
|
|
302
366
|
|
|
303
367
|
This creates a guided wizard that:
|
|
304
|
-
1.
|
|
305
|
-
2.
|
|
306
|
-
3.
|
|
307
|
-
4.
|
|
308
|
-
5.
|
|
309
|
-
6. Re-deploys with
|
|
368
|
+
1. Prompts for Stripe key (with setup URL)
|
|
369
|
+
2. Auto-generates auth secret
|
|
370
|
+
3. Creates Stripe products/prices automatically
|
|
371
|
+
4. Deploys the app
|
|
372
|
+
5. Guides through webhook setup
|
|
373
|
+
6. Re-deploys with webhook secret
|
|
310
374
|
|
|
311
375
|
## Farcaster Miniapp Embeds
|
|
312
376
|
|