@getjack/jack 0.1.22 → 0.1.23

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.
Files changed (42) hide show
  1. package/package.json +1 -1
  2. package/src/commands/clone.ts +5 -5
  3. package/src/commands/down.ts +44 -69
  4. package/src/commands/link.ts +9 -6
  5. package/src/commands/new.ts +54 -76
  6. package/src/commands/publish.ts +7 -2
  7. package/src/commands/secrets.ts +2 -1
  8. package/src/commands/services.ts +41 -15
  9. package/src/commands/update.ts +2 -2
  10. package/src/index.ts +43 -2
  11. package/src/lib/agent-integration.ts +217 -0
  12. package/src/lib/auth/login-flow.ts +2 -1
  13. package/src/lib/binding-validator.ts +2 -3
  14. package/src/lib/build-helper.ts +7 -1
  15. package/src/lib/hooks.ts +101 -21
  16. package/src/lib/managed-down.ts +32 -55
  17. package/src/lib/project-detection.ts +48 -21
  18. package/src/lib/project-operations.ts +31 -13
  19. package/src/lib/prompts.ts +16 -23
  20. package/src/lib/services/db-execute.ts +39 -0
  21. package/src/lib/services/sql-classifier.test.ts +2 -2
  22. package/src/lib/services/sql-classifier.ts +5 -4
  23. package/src/lib/version-check.ts +15 -10
  24. package/src/lib/zip-packager.ts +16 -0
  25. package/src/mcp/resources/index.ts +42 -2
  26. package/src/templates/index.ts +63 -3
  27. package/templates/ai-chat/.jack.json +29 -0
  28. package/templates/ai-chat/bun.lock +18 -0
  29. package/templates/ai-chat/package.json +14 -0
  30. package/templates/ai-chat/public/chat.js +149 -0
  31. package/templates/ai-chat/public/index.html +209 -0
  32. package/templates/ai-chat/src/index.ts +105 -0
  33. package/templates/ai-chat/wrangler.jsonc +12 -0
  34. package/templates/semantic-search/.jack.json +26 -0
  35. package/templates/semantic-search/bun.lock +18 -0
  36. package/templates/semantic-search/package.json +12 -0
  37. package/templates/semantic-search/public/app.js +120 -0
  38. package/templates/semantic-search/public/index.html +210 -0
  39. package/templates/semantic-search/schema.sql +5 -0
  40. package/templates/semantic-search/src/index.ts +144 -0
  41. package/templates/semantic-search/tsconfig.json +13 -0
  42. package/templates/semantic-search/wrangler.jsonc +27 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@getjack/jack",
3
- "version": "0.1.22",
3
+ "version": "0.1.23",
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",
@@ -1,6 +1,6 @@
1
1
  import { existsSync } from "node:fs";
2
2
  import { resolve } from "node:path";
3
- import { isCancel, select } from "@clack/prompts";
3
+ import { isCancel, promptSelectValue } from "../lib/hooks.ts";
4
4
  import { downloadProjectSource, fetchProjectTags } from "../lib/control-plane.ts";
5
5
  import { formatSize } from "../lib/format.ts";
6
6
  import { box, error, info, spinner, success } from "../lib/output.ts";
@@ -34,14 +34,14 @@ export default async function clone(projectName?: string, flags: CloneFlags = {}
34
34
  }
35
35
 
36
36
  // Prompt user for action
37
- const action = await select({
38
- message: `Directory ${flags.as ?? projectName} already exists. What would you like to do?`,
39
- options: [
37
+ const action = await promptSelectValue(
38
+ `Directory ${flags.as ?? projectName} already exists. What would you like to do?`,
39
+ [
40
40
  { value: "overwrite", label: "Overwrite (delete and recreate)" },
41
41
  { value: "merge", label: "Merge (keep existing files)" },
42
42
  { value: "cancel", label: "Cancel" },
43
43
  ],
44
- });
44
+ );
45
45
 
46
46
  if (isCancel(action) || action === "cancel") {
47
47
  info("Clone cancelled");
@@ -59,6 +59,7 @@ async function resolveDatabaseName(
59
59
 
60
60
  export interface DownFlags {
61
61
  force?: boolean;
62
+ includeBackup?: boolean;
62
63
  }
63
64
 
64
65
  export default async function down(projectName?: string, flags: DownFlags = {}): Promise<void> {
@@ -186,9 +187,12 @@ export default async function down(projectName?: string, flags: DownFlags = {}):
186
187
  }
187
188
  console.error("");
188
189
 
189
- // Confirm undeploy
190
+ // Single confirmation with clear description
190
191
  console.error("");
191
- info("Undeploy this project?");
192
+ const confirmMsg = dbName
193
+ ? "Undeploy this project? Worker and database will be deleted."
194
+ : "Undeploy this project?";
195
+ info(confirmMsg);
192
196
  const action = await promptSelect(["Yes", "No"]);
193
197
 
194
198
  if (action !== 0) {
@@ -196,77 +200,38 @@ export default async function down(projectName?: string, flags: DownFlags = {}):
196
200
  return;
197
201
  }
198
202
 
199
- // Handle database if it exists
200
- let shouldDeleteDb = false;
201
-
203
+ // Auto-export database if it exists (no prompt)
204
+ let exportPath: string | null = null;
202
205
  if (dbName) {
203
- console.error("");
204
- info(`Found database: ${dbName}`);
205
-
206
- // Ask if they want to export first
207
- console.error("");
208
- info(`Export database '${dbName}' before deleting?`);
209
- const exportAction = await promptSelect(["Yes", "No"]);
210
-
211
- if (exportAction === 0) {
212
- const exportPath = join(process.cwd(), `${dbName}-backup.sql`);
213
- output.start(`Exporting database to ${exportPath}...`);
214
- try {
215
- await exportDatabase(dbName, exportPath);
216
- output.stop();
217
- success(`Database exported to ${exportPath}`);
218
- } catch (err) {
219
- output.stop();
220
- error(`Failed to export database: ${err instanceof Error ? err.message : String(err)}`);
221
- console.error("");
222
- info("Continue without exporting?");
223
- const continueAction = await promptSelect(["Yes", "No"]);
224
- if (continueAction !== 0) {
225
- info("Cancelled");
226
- return;
227
- }
228
- }
206
+ exportPath = join(process.cwd(), `${dbName}-backup.sql`);
207
+ output.start(`Exporting database to ${exportPath}...`);
208
+ try {
209
+ await exportDatabase(dbName, exportPath);
210
+ output.stop();
211
+ } catch (err) {
212
+ output.stop();
213
+ warn(`Could not export database: ${err instanceof Error ? err.message : String(err)}`);
214
+ exportPath = null;
229
215
  }
230
-
231
- // Ask if they want to delete the database
232
- console.error("");
233
- info(`Delete database '${dbName}'?`);
234
- const deleteAction = await promptSelect(["Yes", "No"]);
235
-
236
- shouldDeleteDb = deleteAction === 0;
237
216
  }
238
217
 
239
- // Handle backup deletion
240
- let shouldDeleteR2 = false;
241
- console.error("");
242
- info("Delete backup for this project?");
243
- const deleteR2Action = await promptSelect(["Yes", "No"]);
244
- shouldDeleteR2 = deleteR2Action === 0;
245
-
246
- // Execute deletions
247
- console.error("");
248
- info("Executing cleanup...");
249
- console.error("");
250
-
251
- // Undeploy service
218
+ // Undeploy worker
252
219
  output.start("Undeploying...");
253
220
  try {
254
221
  await deleteWorker(name);
255
222
  output.stop();
256
- success(`'${name}' undeployed`);
257
223
  } catch (err) {
258
224
  output.stop();
259
225
  error(`Failed to undeploy: ${err instanceof Error ? err.message : String(err)}`);
260
226
  process.exit(1);
261
227
  }
262
228
 
263
- // Delete database if requested
264
- if (shouldDeleteDb && dbName) {
229
+ // Delete database if it exists
230
+ if (dbName) {
265
231
  output.start(`Deleting database '${dbName}'...`);
266
232
  try {
267
233
  await deleteDatabase(dbName);
268
234
  output.stop();
269
- success(`Database '${dbName}' deleted`);
270
235
  } catch (err) {
271
236
  output.stop();
272
237
  warn(
@@ -275,25 +240,35 @@ export default async function down(projectName?: string, flags: DownFlags = {}):
275
240
  }
276
241
  }
277
242
 
278
- // Delete backup if requested
279
- if (shouldDeleteR2) {
280
- output.start("Deleting backup...");
281
- try {
282
- const deleted = await deleteCloudProject(name);
283
- output.stop();
284
- if (deleted) {
285
- success("Backup deleted");
286
- } else {
287
- warn("No backup found or already deleted");
243
+ // Only prompt for backup deletion if --include-backup flag is passed
244
+ if (flags.includeBackup) {
245
+ console.error("");
246
+ info("Also delete cloud backup?");
247
+ const deleteR2Action = await promptSelect(["Yes", "No"]);
248
+
249
+ if (deleteR2Action === 0) {
250
+ output.start("Deleting backup...");
251
+ try {
252
+ const deleted = await deleteCloudProject(name);
253
+ output.stop();
254
+ if (deleted) {
255
+ success("Backup deleted");
256
+ } else {
257
+ warn("No backup found or already deleted");
258
+ }
259
+ } catch (err) {
260
+ output.stop();
261
+ warn(`Failed to delete backup: ${err instanceof Error ? err.message : String(err)}`);
288
262
  }
289
- } catch (err) {
290
- output.stop();
291
- warn(`Failed to delete backup: ${err instanceof Error ? err.message : String(err)}`);
292
263
  }
293
264
  }
294
265
 
266
+ // Final success message
295
267
  console.error("");
296
- success(`Project '${name}' undeployed`);
268
+ success(`Undeployed '${name}'`);
269
+ if (exportPath) {
270
+ info(`Backup saved to ./${dbName}-backup.sql`);
271
+ }
297
272
  console.error("");
298
273
  } catch (err) {
299
274
  console.error("");
@@ -8,8 +8,8 @@
8
8
  */
9
9
 
10
10
  import { existsSync } from "node:fs";
11
- import { isCancel, select } from "@clack/prompts";
12
11
  import { isLoggedIn } from "../lib/auth/index.ts";
12
+ import { isCancel, promptSelectValue } from "../lib/hooks.ts";
13
13
  import {
14
14
  type ManagedProject,
15
15
  findProjectBySlug,
@@ -39,7 +39,10 @@ export default async function link(projectName?: string, flags: LinkFlags = {}):
39
39
 
40
40
  if (!hasWranglerConfig) {
41
41
  error("No wrangler config found");
42
- info("Run this from a jack project directory");
42
+ console.error("");
43
+ info("Jack needs a wrangler.toml or wrangler.jsonc to deploy.");
44
+ info(" → For a new project: jack new my-project");
45
+ info(" → For existing code: jack init");
43
46
  process.exit(1);
44
47
  }
45
48
 
@@ -125,13 +128,13 @@ export default async function link(projectName?: string, flags: LinkFlags = {}):
125
128
  }
126
129
 
127
130
  console.error("");
128
- const choice = await select({
129
- message: "Select a project to link:",
130
- options: projects.map((p) => ({
131
+ const choice = await promptSelectValue(
132
+ "Select a project to link:",
133
+ projects.map((p) => ({
131
134
  value: p.id,
132
135
  label: `${p.slug} (${p.status})`,
133
136
  })),
134
- });
137
+ );
135
138
 
136
139
  if (isCancel(choice)) {
137
140
  info("Cancelled");
@@ -1,13 +1,6 @@
1
- import {
2
- getAgentDefinition,
3
- getPreferredLaunchAgent,
4
- launchAgent,
5
- scanAgents,
6
- updateAgent,
7
- } from "../lib/agents.ts";
1
+ import { getPreferredLaunchAgent, launchAgent, scanAgents, updateAgent } from "../lib/agents.ts";
8
2
  import { debug } from "../lib/debug.ts";
9
3
  import { getErrorDetails } from "../lib/errors.ts";
10
- import { promptSelect } from "../lib/hooks.ts";
11
4
  import { isIntentPhrase } from "../lib/intent.ts";
12
5
  import { output, spinner } from "../lib/output.ts";
13
6
  import { createProject } from "../lib/project-operations.ts";
@@ -20,6 +13,8 @@ export default async function newProject(
20
13
  managed?: boolean;
21
14
  byo?: boolean;
22
15
  ci?: boolean;
16
+ open?: boolean;
17
+ noOpen?: boolean;
23
18
  } = {},
24
19
  ): Promise<void> {
25
20
  // Immediate feedback
@@ -104,77 +99,60 @@ export default async function newProject(
104
99
  console.error("");
105
100
  output.info(`Project: ${result.targetDir}`);
106
101
 
107
- // Prompt to open preferred agent (only in interactive TTY, skip in CI mode)
108
- if (process.stdout.isTTY && !isCi) {
109
- const preferred = await getPreferredLaunchAgent();
110
- if (preferred) {
111
- console.error("");
112
- console.error(` Open project in ${preferred.definition.name}?`);
113
- console.error("");
114
- const choice = await promptSelect(["Yes", "No"]);
115
-
116
- if (choice === 0) {
117
- const launchResult = await launchAgent(preferred.launch, result.targetDir, {
118
- projectName: result.projectName,
119
- url: result.workerUrl,
102
+ // Skip agent section entirely if --no-open or env var
103
+ if (options.noOpen || process.env.JACK_NO_OPEN === "1") {
104
+ return;
105
+ }
106
+
107
+ // Skip in CI mode
108
+ if (!process.stdout.isTTY || isCi) {
109
+ return;
110
+ }
111
+
112
+ // Get preferred agent
113
+ let preferred = await getPreferredLaunchAgent();
114
+
115
+ // If no preferred agent, scan and auto-enable detected agents
116
+ if (!preferred) {
117
+ const detectionResult = await scanAgents();
118
+
119
+ if (detectionResult.detected.length > 0) {
120
+ // Auto-enable newly detected agents
121
+ for (const { id, path, launch } of detectionResult.detected) {
122
+ await updateAgent(id, {
123
+ active: true,
124
+ path,
125
+ detectedAt: new Date().toISOString(),
126
+ launch,
120
127
  });
121
- if (!launchResult.success) {
122
- output.warn(`Failed to launch ${preferred.definition.name}`);
123
- if (launchResult.command?.length) {
124
- output.info(`Run manually: ${launchResult.command.join(" ")}`);
125
- }
126
- }
127
128
  }
128
- } else {
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");
129
+ // Use the first detected agent as preferred
130
+ preferred = await getPreferredLaunchAgent();
131
+ }
132
+ }
133
+
134
+ // Auto-open if --open flag
135
+ if (options.open && preferred) {
136
+ const launchResult = await launchAgent(preferred.launch, result.targetDir, {
137
+ projectName: result.projectName,
138
+ url: result.workerUrl,
139
+ });
140
+ if (!launchResult.success) {
141
+ output.warn(`Failed to launch ${preferred.definition.name}`);
142
+ if (launchResult.command?.length) {
143
+ output.info(`Run manually: ${launchResult.command.join(" ")}`);
177
144
  }
178
145
  }
146
+ return;
147
+ }
148
+
149
+ // Default: show next steps (no prompt)
150
+ if (preferred) {
151
+ console.error("");
152
+ output.info(`Next: cd ${result.targetDir} && ${preferred.launch.command}`);
153
+ } else {
154
+ console.error("");
155
+ output.info("No AI agents detected");
156
+ output.info("Install Claude Code or Codex, then run: jack agents scan");
179
157
  }
180
158
  }
@@ -7,7 +7,10 @@ export default async function publish(): Promise<void> {
7
7
  const link = await readProjectLink(process.cwd());
8
8
  if (!link) {
9
9
  output.error("Not in a jack project directory");
10
- output.info("Run this command from a directory with a .jack folder");
10
+ console.error("");
11
+ output.info("This command requires a jack project.");
12
+ output.info(" → cd into an existing project, or");
13
+ output.info(" → Run: jack new my-project");
11
14
  process.exit(1);
12
15
  }
13
16
 
@@ -27,7 +30,9 @@ export default async function publish(): Promise<void> {
27
30
  const profile = await getCurrentUserProfile();
28
31
  if (!profile?.username) {
29
32
  output.error("You need a username to publish projects");
30
- output.info("Run: jack login (to set up your username)");
33
+ console.error("");
34
+ output.info("Usernames identify your published templates.");
35
+ output.info(" → Run: jack login");
31
36
  process.exit(1);
32
37
  }
33
38
 
@@ -6,7 +6,8 @@
6
6
  * For BYO projects: uses wrangler secret commands.
7
7
  */
8
8
 
9
- import { isCancel, password as passwordPrompt } from "@clack/prompts";
9
+ import { password as passwordPrompt } from "@clack/prompts";
10
+ import { isCancel } from "../lib/hooks.ts";
10
11
  import { $ } from "bun";
11
12
  import { getControlApiUrl } from "../lib/control-plane.ts";
12
13
  import { JackError, JackErrorCode } from "../lib/errors.ts";
@@ -310,12 +310,10 @@ async function dbDelete(options: ServiceOptions): Promise<void> {
310
310
  console.error("");
311
311
 
312
312
  // Confirm deletion
313
- const { confirm } = await import("@clack/prompts");
314
- const shouldDelete = await confirm({
315
- message: `Delete database '${dbInfo.name}'?`,
316
- });
313
+ const { promptSelect } = await import("../lib/hooks.ts");
314
+ const choice = await promptSelect(["Yes, delete", "No, cancel"], `Delete database '${dbInfo.name}'?`);
317
315
 
318
- if (shouldDelete !== true) {
316
+ if (choice !== 0) {
319
317
  info("Cancelled");
320
318
  return;
321
319
  }
@@ -460,20 +458,21 @@ async function dbCreate(args: string[], options: ServiceOptions): Promise<void>
460
458
  console.error("");
461
459
  item(`Binding: ${result.bindingName}`);
462
460
  item(`ID: ${result.databaseId}`);
463
- console.error("");
464
-
465
- // Prompt to deploy
466
- const { confirm } = await import("@clack/prompts");
467
- const shouldDeploy = await confirm({
468
- message: "Deploy now?",
469
- });
470
461
 
471
- if (shouldDeploy === true) {
462
+ // Auto-deploy to activate the database binding
463
+ console.error("");
464
+ outputSpinner.start("Deploying to activate database binding...");
465
+ try {
472
466
  const { deployProject } = await import("../lib/project-operations.ts");
473
467
  await deployProject(process.cwd(), { interactive: true });
474
- } else {
468
+ outputSpinner.stop();
469
+ console.error("");
470
+ success("Database ready");
471
+ console.error("");
472
+ } catch (err) {
473
+ outputSpinner.stop();
475
474
  console.error("");
476
- info("Run 'jack ship' when ready to deploy");
475
+ warn("Deploy failed - run 'jack ship' to activate the binding");
477
476
  console.error("");
478
477
  }
479
478
  } catch (err) {
@@ -710,6 +709,15 @@ async function dbExecute(args: string[], _options: ServiceOptions): Promise<void
710
709
  outputSpinner.stop();
711
710
 
712
711
  if (!result.success) {
712
+ // Track failed execution
713
+ track(Events.SQL_EXECUTED, {
714
+ success: false,
715
+ risk_level: result.risk,
716
+ statement_count: result.statements.length,
717
+ from_file: !!execArgs.filePath,
718
+ error_type: "execution_failed",
719
+ });
720
+
713
721
  console.error("");
714
722
  error(result.error || "SQL execution failed");
715
723
  console.error("");
@@ -742,6 +750,7 @@ async function dbExecute(args: string[], _options: ServiceOptions): Promise<void
742
750
 
743
751
  // Track telemetry
744
752
  track(Events.SQL_EXECUTED, {
753
+ success: true,
745
754
  risk_level: result.risk,
746
755
  statement_count: result.statements.length,
747
756
  from_file: !!execArgs.filePath,
@@ -750,6 +759,12 @@ async function dbExecute(args: string[], _options: ServiceOptions): Promise<void
750
759
  outputSpinner.stop();
751
760
 
752
761
  if (err instanceof WriteNotAllowedError) {
762
+ track(Events.SQL_EXECUTED, {
763
+ success: false,
764
+ error_type: "write_not_allowed",
765
+ risk_level: err.risk,
766
+ });
767
+
753
768
  console.error("");
754
769
  error(err.message);
755
770
  info("Add the --write flag to allow data modification:");
@@ -759,6 +774,12 @@ async function dbExecute(args: string[], _options: ServiceOptions): Promise<void
759
774
  }
760
775
 
761
776
  if (err instanceof DestructiveOperationError) {
777
+ track(Events.SQL_EXECUTED, {
778
+ success: false,
779
+ error_type: "destructive_blocked",
780
+ risk_level: "destructive",
781
+ });
782
+
762
783
  console.error("");
763
784
  error(err.message);
764
785
  info("Destructive operations require confirmation via CLI.");
@@ -766,6 +787,11 @@ async function dbExecute(args: string[], _options: ServiceOptions): Promise<void
766
787
  process.exit(1);
767
788
  }
768
789
 
790
+ track(Events.SQL_EXECUTED, {
791
+ success: false,
792
+ error_type: "unknown",
793
+ });
794
+
769
795
  console.error("");
770
796
  error(`SQL execution failed: ${err instanceof Error ? err.message : String(err)}`);
771
797
  console.error("");
@@ -33,9 +33,9 @@ export default async function update(): Promise<void> {
33
33
 
34
34
  info(`Current version: v${currentVersion}`);
35
35
 
36
- // Check for updates
36
+ // Check for updates - skip cache since user explicitly requested update
37
37
  debug("Fetching latest version from npm...");
38
- const latestVersion = await checkForUpdate();
38
+ const latestVersion = await checkForUpdate(true);
39
39
  debug(`Latest version from npm: ${latestVersion ?? "none (you're up to date)"}`);
40
40
 
41
41
  if (!latestVersion) {
package/src/index.ts CHANGED
@@ -149,6 +149,30 @@ const cli = meow(
149
149
  type: "string",
150
150
  isMultiple: true,
151
151
  },
152
+ includeBackup: {
153
+ type: "boolean",
154
+ default: false,
155
+ },
156
+ open: {
157
+ type: "boolean",
158
+ default: false,
159
+ },
160
+ noOpen: {
161
+ type: "boolean",
162
+ default: false,
163
+ },
164
+ write: {
165
+ type: "boolean",
166
+ shortFlag: "w",
167
+ default: false,
168
+ },
169
+ file: {
170
+ type: "string",
171
+ shortFlag: "f",
172
+ },
173
+ db: {
174
+ type: "string",
175
+ },
152
176
  },
153
177
  },
154
178
  );
@@ -200,6 +224,8 @@ try {
200
224
  managed: cli.flags.managed,
201
225
  byo: cli.flags.byo,
202
226
  ci: cli.flags.ci,
227
+ open: cli.flags.open,
228
+ noOpen: cli.flags.noOpen,
203
229
  });
204
230
  break;
205
231
  }
@@ -212,6 +238,11 @@ try {
212
238
  });
213
239
  break;
214
240
  }
241
+ case "shit": {
242
+ // Easter egg for typo
243
+ console.error("💩💩💩");
244
+ // Fall through to ship
245
+ }
215
246
  case "ship":
216
247
  case "push":
217
248
  case "up":
@@ -279,7 +310,10 @@ try {
279
310
  }
280
311
  case "down": {
281
312
  const { default: down } = await import("./commands/down.ts");
282
- await withTelemetry("down", down)(args[0], { force: cli.flags.force });
313
+ await withTelemetry("down", down)(args[0], {
314
+ force: cli.flags.force,
315
+ includeBackup: cli.flags.includeBackup,
316
+ });
283
317
  break;
284
318
  }
285
319
  case "publish": {
@@ -305,7 +339,11 @@ try {
305
339
  }
306
340
  case "services": {
307
341
  const { default: services } = await import("./commands/services.ts");
308
- await withTelemetry("services", services)(args[0], args.slice(1), {
342
+ const serviceArgs = [...args.slice(1)];
343
+ if (cli.flags.write) serviceArgs.push("--write");
344
+ if (cli.flags.file) serviceArgs.push("--file", cli.flags.file);
345
+ if (cli.flags.db) serviceArgs.push("--db", cli.flags.db);
346
+ await withTelemetry("services", services)(args[0], serviceArgs, {
309
347
  project: cli.flags.project,
310
348
  });
311
349
  break;
@@ -414,3 +452,6 @@ try {
414
452
  }
415
453
 
416
454
  await shutdown();
455
+
456
+ // Ensure clean exit (stdin listeners from prompts can keep event loop alive)
457
+ process.exit(0);