@getjack/jack 0.1.16 → 0.1.19

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/README.md CHANGED
@@ -7,7 +7,7 @@
7
7
  <a href="https://www.npmjs.com/package/@getjack/jack"><img src="https://img.shields.io/npm/v/@getjack/jack" alt="npm"></a>
8
8
  <a href="https://github.com/getjack-org/jack/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-Apache--2.0-blue" alt="license"></a>
9
9
  <a href="https://docs.getjack.org"><img src="https://img.shields.io/badge/docs-getjack.org-green" alt="docs"></a>
10
- <a href="https://discord.gg/fb64krv48R"><img src="https://img.shields.io/badge/discord-community-5865F2" alt="discord"></a>
10
+ <a href="https://community.getjack.org"><img src="https://img.shields.io/badge/discord-community-5865F2" alt="discord"></a>
11
11
  </p>
12
12
 
13
13
  ---
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@getjack/jack",
3
- "version": "0.1.16",
3
+ "version": "0.1.19",
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,13 +1,14 @@
1
1
  import { existsSync } from "node:fs";
2
2
  import { resolve } from "node:path";
3
3
  import { isCancel, select } from "@clack/prompts";
4
- import { fetchProjectTags } from "../lib/control-plane.ts";
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";
7
7
  import { registerPath } from "../lib/paths-index.ts";
8
8
  import { linkProject, updateProjectLink } from "../lib/project-link.ts";
9
9
  import { resolveProject } from "../lib/project-resolver.ts";
10
10
  import { cloneFromCloud, getRemoteManifest } from "../lib/storage/index.ts";
11
+ import { extractZipToDirectory } from "../lib/zip-utils.ts";
11
12
 
12
13
  export interface CloneFlags {
13
14
  as?: string;
@@ -53,53 +54,74 @@ export default async function clone(projectName?: string, flags: CloneFlags = {}
53
54
  }
54
55
  }
55
56
 
56
- // Fetch remote manifest
57
- const spin = spinner(`Fetching from jack-storage/${projectName}/...`);
58
- const manifest = await getRemoteManifest(projectName);
57
+ // Check if this is a managed project (has source in control-plane)
58
+ const spin = spinner(`Looking up ${projectName}...`);
59
+ let project: Awaited<ReturnType<typeof resolveProject>> = null;
59
60
 
60
- if (!manifest) {
61
- spin.error(`Project not found: ${projectName}`);
62
- process.exit(1);
61
+ try {
62
+ project = await resolveProject(projectName);
63
+ } catch {
64
+ // Not found on control-plane, will fall back to User R2
63
65
  }
64
66
 
65
- // Show file count and size
66
- const totalSize = manifest.files.reduce((sum, f) => sum + f.size, 0);
67
- spin.success(`Found ${manifest.files.length} file(s) (${formatSize(totalSize)})`);
67
+ // Managed mode: download from control-plane
68
+ if (project?.sources.controlPlane && project.remote?.projectId) {
69
+ spin.success("Found on jack cloud");
70
+
71
+ const downloadSpin = spinner("Downloading from jack cloud...");
72
+ try {
73
+ const sourceZip = await downloadProjectSource(projectName);
74
+ const fileCount = await extractZipToDirectory(sourceZip, targetDir);
75
+ downloadSpin.success(`Restored ${fileCount} file(s) to ./${flags.as ?? projectName}/`);
76
+ } catch (err) {
77
+ downloadSpin.error("Download failed");
78
+ const message = err instanceof Error ? err.message : "Could not download project source";
79
+ error(message);
80
+ process.exit(1);
81
+ }
68
82
 
69
- // Download files
70
- const downloadSpin = spinner("Downloading...");
71
- const result = await cloneFromCloud(projectName, targetDir);
83
+ // Link to control-plane
84
+ await linkProject(targetDir, project.remote.projectId, "managed");
85
+ await registerPath(project.remote.projectId, targetDir);
72
86
 
73
- if (!result.success) {
74
- downloadSpin.error("Clone failed");
75
- error(result.error || "Could not download project files");
76
- info("Check your network connection and try again");
77
- process.exit(1);
78
- }
87
+ // Fetch and restore tags from control plane
88
+ try {
89
+ const remoteTags = await fetchProjectTags(project.remote.projectId);
90
+ if (remoteTags.length > 0) {
91
+ await updateProjectLink(targetDir, { tags: remoteTags });
92
+ info(`Restored ${remoteTags.length} tag(s)`);
93
+ }
94
+ } catch {
95
+ // Silent fail - tag restoration is non-critical
96
+ }
97
+ } else {
98
+ // BYO mode: use existing User R2 flow
99
+ spin.stop();
100
+ const fetchSpin = spinner(`Fetching from jack-storage/${projectName}/...`);
101
+ const manifest = await getRemoteManifest(projectName);
102
+
103
+ if (!manifest) {
104
+ fetchSpin.error(`Project not found: ${projectName}`);
105
+ info("For BYO projects, run 'jack sync' first to backup your project.");
106
+ process.exit(1);
107
+ }
79
108
 
80
- downloadSpin.success(`Restored to ./${flags.as ?? projectName}/`);
109
+ // Show file count and size
110
+ const totalSize = manifest.files.reduce((sum, f) => sum + f.size, 0);
111
+ fetchSpin.success(`Found ${manifest.files.length} file(s) (${formatSize(totalSize)})`);
81
112
 
82
- // Link project to control plane if it's a managed project
83
- try {
84
- const project = await resolveProject(projectName);
85
- if (project?.sources.controlPlane && project.remote?.projectId) {
86
- // Managed project - link with control plane project ID
87
- await linkProject(targetDir, project.remote.projectId, "managed");
88
- await registerPath(project.remote.projectId, targetDir);
89
-
90
- // Fetch and restore tags from control plane
91
- try {
92
- const remoteTags = await fetchProjectTags(project.remote.projectId);
93
- if (remoteTags.length > 0) {
94
- await updateProjectLink(targetDir, { tags: remoteTags });
95
- info(`Restored ${remoteTags.length} tag(s)`);
96
- }
97
- } catch {
98
- // Silent fail - tag restoration is non-critical
99
- }
113
+ // Download files
114
+ const downloadSpin = spinner("Downloading...");
115
+ const result = await cloneFromCloud(projectName, targetDir);
116
+
117
+ if (!result.success) {
118
+ downloadSpin.error("Clone failed");
119
+ error(result.error || "Could not download project files");
120
+ info("Check your network connection and try again");
121
+ process.exit(1);
100
122
  }
101
- } catch {
102
- // Not a control plane project or offline - continue without linking
123
+
124
+ downloadSpin.success(`Restored to ./${flags.as ?? projectName}/`);
103
125
  }
104
126
 
105
127
  // Show next steps
@@ -0,0 +1,47 @@
1
+ /**
2
+ * jack community - Open the jack community Discord
3
+ */
4
+
5
+ import { $ } from "bun";
6
+ import { error } from "../lib/output.ts";
7
+
8
+ const COMMUNITY_URL = "https://community.getjack.org";
9
+
10
+ export default async function community(): Promise<void> {
11
+ console.error("");
12
+ console.error(" Chat with other vibecoders and the jack team.");
13
+ console.error("");
14
+ console.error(` Press Enter to open the browser or visit ${COMMUNITY_URL}`);
15
+
16
+ // Wait for Enter key
17
+ await waitForEnter();
18
+
19
+ // Open browser using platform-specific command
20
+ const cmd =
21
+ process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
22
+
23
+ try {
24
+ await $`${cmd} ${COMMUNITY_URL}`;
25
+ } catch (err) {
26
+ error(`Failed to open browser: ${err instanceof Error ? err.message : String(err)}`);
27
+ console.error(` Visit: ${COMMUNITY_URL}`);
28
+ }
29
+ }
30
+
31
+ async function waitForEnter(): Promise<void> {
32
+ return new Promise((resolve) => {
33
+ if (!process.stdin.isTTY) {
34
+ // Non-interactive, just resolve immediately
35
+ resolve();
36
+ return;
37
+ }
38
+
39
+ process.stdin.setRawMode(true);
40
+ process.stdin.resume();
41
+ process.stdin.once("data", () => {
42
+ process.stdin.setRawMode(false);
43
+ process.stdin.pause();
44
+ resolve();
45
+ });
46
+ });
47
+ }
@@ -12,6 +12,12 @@ import { ensureAuth, ensureWrangler, isAuthenticated } from "../lib/wrangler.ts"
12
12
  export async function isInitialized(): Promise<boolean> {
13
13
  const config = await readConfig();
14
14
  if (!config?.initialized) return false;
15
+
16
+ // Check Jack Cloud auth first (most common path)
17
+ const { isLoggedIn } = await import("../lib/auth/store.ts");
18
+ if (await isLoggedIn()) return true;
19
+
20
+ // Fall back to wrangler/Cloudflare auth (BYO mode)
15
21
  return await isAuthenticated();
16
22
  }
17
23
 
@@ -1,10 +1,17 @@
1
- import { join } from "node:path";
1
+ import { existsSync } from "node:fs";
2
+ import { join, resolve } from "node:path";
2
3
  import { fetchProjectResources } from "../lib/control-plane.ts";
3
4
  import { formatSize } from "../lib/format.ts";
4
5
  import { error, info, item, output as outputSpinner, success, warn } from "../lib/output.ts";
5
6
  import { readProjectLink } from "../lib/project-link.ts";
6
7
  import { parseWranglerResources } from "../lib/resources.ts";
7
8
  import { createDatabase } from "../lib/services/db-create.ts";
9
+ import {
10
+ DestructiveOperationError,
11
+ WriteNotAllowedError,
12
+ executeSql,
13
+ executeSqlFile,
14
+ } from "../lib/services/db-execute.ts";
8
15
  import { listDatabases } from "../lib/services/db-list.ts";
9
16
  import {
10
17
  deleteDatabase,
@@ -12,6 +19,7 @@ import {
12
19
  generateExportFilename,
13
20
  getDatabaseInfo as getWranglerDatabaseInfo,
14
21
  } from "../lib/services/db.ts";
22
+ import { getRiskDescription } from "../lib/services/sql-classifier.ts";
15
23
  import { getProjectNameFromDir } from "../lib/storage/index.ts";
16
24
  import { Events, track } from "../lib/telemetry.ts";
17
25
 
@@ -125,13 +133,19 @@ function showDbHelp(): void {
125
133
  console.error(" info Show database information (default)");
126
134
  console.error(" create Create a new database");
127
135
  console.error(" list List all databases in the project");
136
+ console.error(" execute Execute SQL against the database");
128
137
  console.error(" export Export database to SQL file");
129
138
  console.error(" delete Delete a database");
130
139
  console.error("");
131
140
  console.error("Examples:");
132
- console.error(" jack services db Show info about the default database");
133
- console.error(" jack services db create Create a new database");
134
- console.error(" jack services db list List all databases");
141
+ console.error(
142
+ " jack services db Show info about the default database",
143
+ );
144
+ console.error(" jack services db create Create a new database");
145
+ console.error(" jack services db list List all databases");
146
+ console.error(' jack services db execute "SELECT * FROM users" Run a read query');
147
+ console.error(' jack services db execute "INSERT..." --write Run a write query');
148
+ console.error(" jack services db execute --file schema.sql --write Run SQL from file");
135
149
  console.error("");
136
150
  }
137
151
 
@@ -149,13 +163,15 @@ async function dbCommand(args: string[], options: ServiceOptions): Promise<void>
149
163
  return await dbCreate(args.slice(1), options);
150
164
  case "list":
151
165
  return await dbList(options);
166
+ case "execute":
167
+ return await dbExecute(args.slice(1), options);
152
168
  case "export":
153
169
  return await dbExport(options);
154
170
  case "delete":
155
171
  return await dbDelete(options);
156
172
  default:
157
173
  error(`Unknown action: ${action}`);
158
- info("Available: info, create, list, export, delete");
174
+ info("Available: info, create, list, execute, export, delete");
159
175
  process.exit(1);
160
176
  }
161
177
  }
@@ -261,6 +277,12 @@ async function dbExport(options: ServiceOptions): Promise<void> {
261
277
  */
262
278
  async function dbDelete(options: ServiceOptions): Promise<void> {
263
279
  const projectName = await resolveProjectName(options);
280
+ const projectDir = process.cwd();
281
+
282
+ // Check deploy mode
283
+ const link = await readProjectLink(projectDir);
284
+ const isManaged = link?.deploy_mode === "managed";
285
+
264
286
  const dbInfo = await resolveDatabaseInfo(projectName);
265
287
 
266
288
  if (!dbInfo) {
@@ -298,14 +320,80 @@ async function dbDelete(options: ServiceOptions): Promise<void> {
298
320
  return;
299
321
  }
300
322
 
301
- // Delete database
302
323
  outputSpinner.start("Deleting database...");
324
+
325
+ // Track binding_name from control plane for matching in wrangler.jsonc
326
+ let controlPlaneBindingName: string | null = null;
327
+
303
328
  try {
304
- await deleteDatabase(dbInfo.name);
329
+ if (isManaged && link) {
330
+ // Managed mode: delete via control plane
331
+ const { fetchProjectResources, deleteProjectResource } = await import(
332
+ "../lib/control-plane.ts"
333
+ );
334
+
335
+ // Find the resource ID for this database
336
+ const resources = await fetchProjectResources(link.project_id);
337
+ const d1Resource = resources.find(
338
+ (r) => r.resource_type === "d1" && r.resource_name === dbInfo.name,
339
+ );
340
+
341
+ if (d1Resource) {
342
+ // Save binding_name for wrangler.jsonc cleanup
343
+ controlPlaneBindingName = d1Resource.binding_name;
344
+ // Delete via control plane (which also deletes from Cloudflare)
345
+ await deleteProjectResource(link.project_id, d1Resource.id);
346
+ } else {
347
+ // Resource not in control plane - fall back to wrangler for cleanup
348
+ await deleteDatabase(dbInfo.name);
349
+ }
350
+ } else {
351
+ // BYO mode: delete via wrangler directly
352
+ await deleteDatabase(dbInfo.name);
353
+ }
354
+
355
+ // Remove binding from wrangler.jsonc (both modes)
356
+ // Note: We need to find the LOCAL database_name from wrangler.jsonc,
357
+ // which may differ from the control plane's resource_name
358
+ const { removeD1Binding, getExistingD1Bindings } = await import("../lib/wrangler-config.ts");
359
+ const configPath = join(projectDir, "wrangler.jsonc");
360
+
361
+ let bindingRemoved = false;
362
+ try {
363
+ // Find the binding by matching (in order of reliability):
364
+ // 1. binding name (e.g., "DB") - if control plane provided it
365
+ // 2. database_id (provider_id from control plane)
366
+ // 3. database_name
367
+ // 4. If managed mode and we successfully deleted, remove first D1 binding
368
+ const existingBindings = await getExistingD1Bindings(configPath);
369
+ let bindingToRemove = existingBindings.find(
370
+ (b) =>
371
+ (controlPlaneBindingName && b.binding === controlPlaneBindingName) ||
372
+ b.database_id === dbInfo.id ||
373
+ b.database_name === dbInfo.name,
374
+ );
375
+
376
+ // Fallback: if managed mode and we deleted from control plane,
377
+ // remove the first D1 binding (binding_name may be null for older DBs)
378
+ if (!bindingToRemove && isManaged && existingBindings.length > 0) {
379
+ bindingToRemove = existingBindings[0];
380
+ }
381
+
382
+ if (bindingToRemove) {
383
+ bindingRemoved = await removeD1Binding(configPath, bindingToRemove.database_name);
384
+ }
385
+ } catch (bindingErr) {
386
+ // Log but don't fail - the database is already deleted
387
+ // The user can manually clean up wrangler.jsonc if needed
388
+ }
389
+
305
390
  outputSpinner.stop();
306
391
 
307
392
  console.error("");
308
393
  success("Database deleted");
394
+ if (bindingRemoved) {
395
+ item("Binding removed from wrangler.jsonc");
396
+ }
309
397
  console.error("");
310
398
  } catch (err) {
311
399
  outputSpinner.stop();
@@ -316,10 +404,11 @@ async function dbDelete(options: ServiceOptions): Promise<void> {
316
404
  }
317
405
 
318
406
  /**
319
- * Parse --name flag from args
320
- * Supports: --name foo, --name=foo
407
+ * Parse --name flag or positional arg from args
408
+ * Supports: --name foo, --name=foo, or first positional arg
321
409
  */
322
410
  function parseNameFlag(args: string[]): string | undefined {
411
+ // Check --name flag first (takes priority)
323
412
  for (let i = 0; i < args.length; i++) {
324
413
  const arg = args[i];
325
414
  if (arg === "--name" && args[i + 1]) {
@@ -329,6 +418,14 @@ function parseNameFlag(args: string[]): string | undefined {
329
418
  return arg.slice("--name=".length);
330
419
  }
331
420
  }
421
+
422
+ // Fall back to first positional argument (non-flag)
423
+ for (const arg of args) {
424
+ if (!arg.startsWith("-")) {
425
+ return arg;
426
+ }
427
+ }
428
+
332
429
  return undefined;
333
430
  }
334
431
 
@@ -427,3 +524,251 @@ async function dbList(options: ServiceOptions): Promise<void> {
427
524
  process.exit(1);
428
525
  }
429
526
  }
527
+
528
+ /**
529
+ * Parse execute command arguments
530
+ * Supports:
531
+ * jack services db execute "SELECT * FROM users"
532
+ * jack services db execute "INSERT..." --write
533
+ * jack services db execute --file schema.sql --write
534
+ * jack services db execute --db my-other-db "SELECT..."
535
+ */
536
+ interface ExecuteArgs {
537
+ sql?: string;
538
+ filePath?: string;
539
+ allowWrite: boolean;
540
+ databaseName?: string;
541
+ }
542
+
543
+ function parseExecuteArgs(args: string[]): ExecuteArgs {
544
+ const result: ExecuteArgs = {
545
+ allowWrite: false,
546
+ };
547
+
548
+ for (let i = 0; i < args.length; i++) {
549
+ const arg = args[i];
550
+ if (!arg) continue;
551
+
552
+ if (arg === "--write" || arg === "-w") {
553
+ result.allowWrite = true;
554
+ continue;
555
+ }
556
+
557
+ if (arg === "--file" || arg === "-f") {
558
+ result.filePath = args[i + 1];
559
+ i++; // Skip the next arg
560
+ continue;
561
+ }
562
+
563
+ if (arg.startsWith("--file=")) {
564
+ result.filePath = arg.slice("--file=".length);
565
+ continue;
566
+ }
567
+
568
+ if (arg === "--db" || arg === "--database") {
569
+ result.databaseName = args[i + 1];
570
+ i++; // Skip the next arg
571
+ continue;
572
+ }
573
+
574
+ if (arg.startsWith("--db=")) {
575
+ result.databaseName = arg.slice("--db=".length);
576
+ continue;
577
+ }
578
+
579
+ if (arg.startsWith("--database=")) {
580
+ result.databaseName = arg.slice("--database=".length);
581
+ continue;
582
+ }
583
+
584
+ // Any other non-flag argument is the SQL query
585
+ if (!arg.startsWith("-")) {
586
+ result.sql = arg;
587
+ }
588
+ }
589
+
590
+ return result;
591
+ }
592
+
593
+ /**
594
+ * Execute SQL against the database
595
+ */
596
+ async function dbExecute(args: string[], _options: ServiceOptions): Promise<void> {
597
+ const execArgs = parseExecuteArgs(args);
598
+
599
+ // Validate input
600
+ if (!execArgs.sql && !execArgs.filePath) {
601
+ console.error("");
602
+ error("No SQL provided");
603
+ info('Usage: jack services db execute "SELECT * FROM users"');
604
+ info(" jack services db execute --file schema.sql --write");
605
+ console.error("");
606
+ process.exit(1);
607
+ }
608
+
609
+ // Cannot specify both SQL and file
610
+ if (execArgs.sql && execArgs.filePath) {
611
+ console.error("");
612
+ error("Cannot specify both inline SQL and --file");
613
+ info("Use either inline SQL or --file, not both");
614
+ console.error("");
615
+ process.exit(1);
616
+ }
617
+
618
+ // If using --file, verify file exists
619
+ if (execArgs.filePath) {
620
+ const absPath = resolve(process.cwd(), execArgs.filePath);
621
+ if (!existsSync(absPath)) {
622
+ console.error("");
623
+ error(`File not found: ${execArgs.filePath}`);
624
+ console.error("");
625
+ process.exit(1);
626
+ }
627
+ execArgs.filePath = absPath;
628
+ }
629
+
630
+ const projectDir = process.cwd();
631
+
632
+ try {
633
+ outputSpinner.start("Executing SQL...");
634
+
635
+ let result;
636
+ if (execArgs.filePath) {
637
+ result = await executeSqlFile({
638
+ projectDir,
639
+ filePath: execArgs.filePath,
640
+ databaseName: execArgs.databaseName,
641
+ allowWrite: execArgs.allowWrite,
642
+ interactive: true,
643
+ });
644
+ } else {
645
+ result = await executeSql({
646
+ projectDir,
647
+ sql: execArgs.sql!,
648
+ databaseName: execArgs.databaseName,
649
+ allowWrite: execArgs.allowWrite,
650
+ interactive: true,
651
+ });
652
+ }
653
+
654
+ // Handle destructive operations - need confirmation BEFORE execution
655
+ if (result.requiresConfirmation) {
656
+ outputSpinner.stop();
657
+
658
+ // Find the destructive statements
659
+ const destructiveStmts = result.statements.filter((s) => s.risk === "destructive");
660
+
661
+ console.error("");
662
+ warn("This SQL contains destructive operations:");
663
+ for (const stmt of destructiveStmts) {
664
+ item(`${stmt.operation}: ${stmt.sql.slice(0, 60)}${stmt.sql.length > 60 ? "..." : ""}`);
665
+ }
666
+ console.error("");
667
+
668
+ // Require typed confirmation
669
+ const { text } = await import("@clack/prompts");
670
+ const confirmText = destructiveStmts
671
+ .map((s) => s.operation)
672
+ .join(" ")
673
+ .toUpperCase();
674
+
675
+ const userInput = await text({
676
+ message: `Type "${confirmText}" to confirm:`,
677
+ validate: (value) => {
678
+ if (value.toUpperCase() !== confirmText) {
679
+ return `Please type "${confirmText}" exactly to confirm`;
680
+ }
681
+ },
682
+ });
683
+
684
+ if (typeof userInput !== "string") {
685
+ info("Cancelled");
686
+ return;
687
+ }
688
+
689
+ // NOW execute with confirmation (interactive: false means "already confirmed")
690
+ outputSpinner.start("Executing SQL...");
691
+ if (execArgs.filePath) {
692
+ result = await executeSqlFile({
693
+ projectDir,
694
+ filePath: execArgs.filePath,
695
+ databaseName: execArgs.databaseName,
696
+ allowWrite: true,
697
+ interactive: false, // Already confirmed, execute now
698
+ });
699
+ } else {
700
+ result = await executeSql({
701
+ projectDir,
702
+ sql: execArgs.sql!,
703
+ databaseName: execArgs.databaseName,
704
+ allowWrite: true,
705
+ interactive: false, // Already confirmed, execute now
706
+ });
707
+ }
708
+ }
709
+
710
+ outputSpinner.stop();
711
+
712
+ if (!result.success) {
713
+ console.error("");
714
+ error(result.error || "SQL execution failed");
715
+ console.error("");
716
+ process.exit(1);
717
+ }
718
+
719
+ // Show results
720
+ console.error("");
721
+ success(`SQL executed (${getRiskDescription(result.risk)})`);
722
+
723
+ if (result.meta?.changes !== undefined && result.meta.changes > 0) {
724
+ item(`Rows affected: ${result.meta.changes}`);
725
+ }
726
+
727
+ if (result.meta?.duration_ms !== undefined) {
728
+ item(`Duration: ${result.meta.duration_ms}ms`);
729
+ }
730
+
731
+ if (result.warning) {
732
+ console.error("");
733
+ warn(result.warning);
734
+ }
735
+
736
+ // Output query results
737
+ if (result.results && result.results.length > 0) {
738
+ console.error("");
739
+ console.log(JSON.stringify(result.results, null, 2));
740
+ }
741
+ console.error("");
742
+
743
+ // Track telemetry
744
+ track(Events.SQL_EXECUTED, {
745
+ risk_level: result.risk,
746
+ statement_count: result.statements.length,
747
+ from_file: !!execArgs.filePath,
748
+ });
749
+ } catch (err) {
750
+ outputSpinner.stop();
751
+
752
+ if (err instanceof WriteNotAllowedError) {
753
+ console.error("");
754
+ error(err.message);
755
+ info("Add the --write flag to allow data modification:");
756
+ info(` jack services db execute "${execArgs.sql || `--file ${execArgs.filePath}`}" --write`);
757
+ console.error("");
758
+ process.exit(1);
759
+ }
760
+
761
+ if (err instanceof DestructiveOperationError) {
762
+ console.error("");
763
+ error(err.message);
764
+ info("Destructive operations require confirmation via CLI.");
765
+ console.error("");
766
+ process.exit(1);
767
+ }
768
+
769
+ console.error("");
770
+ error(`SQL execution failed: ${err instanceof Error ? err.message : String(err)}`);
771
+ console.error("");
772
+ process.exit(1);
773
+ }
774
+ }
@@ -1,6 +1,7 @@
1
1
  import { existsSync } from "node:fs";
2
2
  import { getSyncConfig } from "../lib/config.ts";
3
3
  import { error, info, spinner, success, warn } from "../lib/output.ts";
4
+ import { readProjectLink } from "../lib/project-link.ts";
4
5
  import { syncToCloud } from "../lib/storage/index.ts";
5
6
 
6
7
  export interface SyncFlags {
@@ -25,6 +26,14 @@ export default async function sync(flags: SyncFlags = {}): Promise<void> {
25
26
  process.exit(1);
26
27
  }
27
28
 
29
+ // Check if this is a managed project
30
+ const link = await readProjectLink(process.cwd());
31
+ if (link?.deploy_mode === "managed") {
32
+ info("Managed projects are automatically backed up to jack cloud during deploy.");
33
+ info("Use 'jack clone <project>' on another machine to restore.");
34
+ return;
35
+ }
36
+
28
37
  // Check if sync is enabled
29
38
  const syncConfig = await getSyncConfig();
30
39
  if (!syncConfig.enabled) {