@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 CHANGED
@@ -1,13 +1,16 @@
1
1
  {
2
2
  "name": "@getjack/jack",
3
- "version": "0.1.20",
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": ["src", "templates"],
10
+ "files": [
11
+ "src",
12
+ "templates"
13
+ ],
11
14
  "engines": {
12
15
  "bun": ">=1.0.0"
13
16
  },
@@ -1,4 +1,10 @@
1
- import { getPreferredLaunchAgent, launchAgent } from "../lib/agents.ts";
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
- console.error("");
124
- output.info("No launchable AI agent detected");
125
- output.info("Run: jack agents scan");
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
  }
@@ -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("Others can now fork your project:");
42
+ output.info("Share this project:");
43
43
  output.info(` ${result.fork_command}`);
44
44
  } catch (err) {
45
45
  spin.stop();
@@ -5,7 +5,11 @@
5
5
 
6
6
  import { writeFile } from "node:fs/promises";
7
7
  import { join } from "node:path";
8
- import { deleteManagedProject, exportManagedDatabase } from "./control-plane.ts";
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
- item("Database: managed D1");
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
- console.error("");
72
- info("Database will be deleted with the project");
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
- if (continueAction !== 0) {
111
- info("Cancelled");
112
- return false;
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 (for linking flow)
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
- // It's the user's project on jack cloud but not locally - offer to link
836
- if (interactive) {
837
- const { promptSelect } = await import("./hooks.ts");
838
- console.error("");
839
- console.error(` Project "${projectName}" exists on jack cloud but not locally.`);
840
- console.error("");
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(
@@ -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 (supports `validate: "json" | "accountAssociation"`) |
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 env | `{"action": "require", "source": "secret", "key": "API_KEY"}` |
149
- | `prompt` | Prompt for input and update JSON file | `{"action": "prompt", "message": "Paste JSON", "validate": "json", "successMessage": "Saved", "writeJson": {"path": "public/data.json", "set": {"data": {"from": "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
- ### Proposed Hook Extensions
211
+ ### Advanced Hook Features
209
212
 
210
- These extensions are planned to support more complex setup wizards (like SaaS templates with Stripe):
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
- Currently `require` fails if a secret is missing. This extension allows prompting the user instead:
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 (no change)
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
- #### 2. `shell` + `captureAs`
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": "shell",
239
- "command": "stripe listen --print-secret",
240
- "captureAs": "secret:STRIPE_WEBHOOK_SECRET",
241
- "message": "Starting Stripe webhook listener..."
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
- **Use cases:**
246
- - Capture Stripe CLI webhook signing secret
247
- - Capture generated API keys or tokens
248
- - Capture any CLI output needed for configuration
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
- **`captureAs` syntax:**
251
- - `secret:KEY_NAME` saves to `.secrets.json`
252
- - `var:NAME` saves to hook variables for later hooks
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` + `saveAs`
282
+ #### 3. `prompt` with `secret` Flag
255
283
 
256
- Currently `prompt` only writes to JSON files via `writeJson`. This extension allows saving input as a secret:
284
+ Mask sensitive input (like API keys):
257
285
 
258
286
  ```json
259
287
  {
260
288
  "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"
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
- **Difference from `require+onMissing`:**
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
- ### Design Principles for Hook Extensions
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. **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
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: Complex Setup Wizard
324
+ ### Example: SaaS Template Setup Wizard
282
325
 
283
- A SaaS template with Stripe might use these extensions:
326
+ The `saas` template uses `preCreate` hooks for a complete setup wizard:
284
327
 
285
328
  ```json
286
329
  {
287
330
  "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"}
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": "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"}
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. 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
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