@getjack/jack 0.1.16 → 0.1.17

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,13 +1,16 @@
1
1
  {
2
2
  "name": "@getjack/jack",
3
- "version": "0.1.16",
3
+ "version": "0.1.17",
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
  },
@@ -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
+ }
@@ -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
  }
@@ -427,3 +443,251 @@ async function dbList(options: ServiceOptions): Promise<void> {
427
443
  process.exit(1);
428
444
  }
429
445
  }
446
+
447
+ /**
448
+ * Parse execute command arguments
449
+ * Supports:
450
+ * jack services db execute "SELECT * FROM users"
451
+ * jack services db execute "INSERT..." --write
452
+ * jack services db execute --file schema.sql --write
453
+ * jack services db execute --db my-other-db "SELECT..."
454
+ */
455
+ interface ExecuteArgs {
456
+ sql?: string;
457
+ filePath?: string;
458
+ allowWrite: boolean;
459
+ databaseName?: string;
460
+ }
461
+
462
+ function parseExecuteArgs(args: string[]): ExecuteArgs {
463
+ const result: ExecuteArgs = {
464
+ allowWrite: false,
465
+ };
466
+
467
+ for (let i = 0; i < args.length; i++) {
468
+ const arg = args[i];
469
+ if (!arg) continue;
470
+
471
+ if (arg === "--write" || arg === "-w") {
472
+ result.allowWrite = true;
473
+ continue;
474
+ }
475
+
476
+ if (arg === "--file" || arg === "-f") {
477
+ result.filePath = args[i + 1];
478
+ i++; // Skip the next arg
479
+ continue;
480
+ }
481
+
482
+ if (arg.startsWith("--file=")) {
483
+ result.filePath = arg.slice("--file=".length);
484
+ continue;
485
+ }
486
+
487
+ if (arg === "--db" || arg === "--database") {
488
+ result.databaseName = args[i + 1];
489
+ i++; // Skip the next arg
490
+ continue;
491
+ }
492
+
493
+ if (arg.startsWith("--db=")) {
494
+ result.databaseName = arg.slice("--db=".length);
495
+ continue;
496
+ }
497
+
498
+ if (arg.startsWith("--database=")) {
499
+ result.databaseName = arg.slice("--database=".length);
500
+ continue;
501
+ }
502
+
503
+ // Any other non-flag argument is the SQL query
504
+ if (!arg.startsWith("-")) {
505
+ result.sql = arg;
506
+ }
507
+ }
508
+
509
+ return result;
510
+ }
511
+
512
+ /**
513
+ * Execute SQL against the database
514
+ */
515
+ async function dbExecute(args: string[], _options: ServiceOptions): Promise<void> {
516
+ const execArgs = parseExecuteArgs(args);
517
+
518
+ // Validate input
519
+ if (!execArgs.sql && !execArgs.filePath) {
520
+ console.error("");
521
+ error("No SQL provided");
522
+ info('Usage: jack services db execute "SELECT * FROM users"');
523
+ info(" jack services db execute --file schema.sql --write");
524
+ console.error("");
525
+ process.exit(1);
526
+ }
527
+
528
+ // Cannot specify both SQL and file
529
+ if (execArgs.sql && execArgs.filePath) {
530
+ console.error("");
531
+ error("Cannot specify both inline SQL and --file");
532
+ info("Use either inline SQL or --file, not both");
533
+ console.error("");
534
+ process.exit(1);
535
+ }
536
+
537
+ // If using --file, verify file exists
538
+ if (execArgs.filePath) {
539
+ const absPath = resolve(process.cwd(), execArgs.filePath);
540
+ if (!existsSync(absPath)) {
541
+ console.error("");
542
+ error(`File not found: ${execArgs.filePath}`);
543
+ console.error("");
544
+ process.exit(1);
545
+ }
546
+ execArgs.filePath = absPath;
547
+ }
548
+
549
+ const projectDir = process.cwd();
550
+
551
+ try {
552
+ outputSpinner.start("Executing SQL...");
553
+
554
+ let result;
555
+ if (execArgs.filePath) {
556
+ result = await executeSqlFile({
557
+ projectDir,
558
+ filePath: execArgs.filePath,
559
+ databaseName: execArgs.databaseName,
560
+ allowWrite: execArgs.allowWrite,
561
+ interactive: true,
562
+ });
563
+ } else {
564
+ result = await executeSql({
565
+ projectDir,
566
+ sql: execArgs.sql!,
567
+ databaseName: execArgs.databaseName,
568
+ allowWrite: execArgs.allowWrite,
569
+ interactive: true,
570
+ });
571
+ }
572
+
573
+ // Handle destructive operations - need confirmation BEFORE execution
574
+ if (result.requiresConfirmation) {
575
+ outputSpinner.stop();
576
+
577
+ // Find the destructive statements
578
+ const destructiveStmts = result.statements.filter((s) => s.risk === "destructive");
579
+
580
+ console.error("");
581
+ warn("This SQL contains destructive operations:");
582
+ for (const stmt of destructiveStmts) {
583
+ item(`${stmt.operation}: ${stmt.sql.slice(0, 60)}${stmt.sql.length > 60 ? "..." : ""}`);
584
+ }
585
+ console.error("");
586
+
587
+ // Require typed confirmation
588
+ const { text } = await import("@clack/prompts");
589
+ const confirmText = destructiveStmts
590
+ .map((s) => s.operation)
591
+ .join(" ")
592
+ .toUpperCase();
593
+
594
+ const userInput = await text({
595
+ message: `Type "${confirmText}" to confirm:`,
596
+ validate: (value) => {
597
+ if (value.toUpperCase() !== confirmText) {
598
+ return `Please type "${confirmText}" exactly to confirm`;
599
+ }
600
+ },
601
+ });
602
+
603
+ if (typeof userInput !== "string") {
604
+ info("Cancelled");
605
+ return;
606
+ }
607
+
608
+ // NOW execute with confirmation (interactive: false means "already confirmed")
609
+ outputSpinner.start("Executing SQL...");
610
+ if (execArgs.filePath) {
611
+ result = await executeSqlFile({
612
+ projectDir,
613
+ filePath: execArgs.filePath,
614
+ databaseName: execArgs.databaseName,
615
+ allowWrite: true,
616
+ interactive: false, // Already confirmed, execute now
617
+ });
618
+ } else {
619
+ result = await executeSql({
620
+ projectDir,
621
+ sql: execArgs.sql!,
622
+ databaseName: execArgs.databaseName,
623
+ allowWrite: true,
624
+ interactive: false, // Already confirmed, execute now
625
+ });
626
+ }
627
+ }
628
+
629
+ outputSpinner.stop();
630
+
631
+ if (!result.success) {
632
+ console.error("");
633
+ error(result.error || "SQL execution failed");
634
+ console.error("");
635
+ process.exit(1);
636
+ }
637
+
638
+ // Show results
639
+ console.error("");
640
+ success(`SQL executed (${getRiskDescription(result.risk)})`);
641
+
642
+ if (result.meta?.changes !== undefined && result.meta.changes > 0) {
643
+ item(`Rows affected: ${result.meta.changes}`);
644
+ }
645
+
646
+ if (result.meta?.duration_ms !== undefined) {
647
+ item(`Duration: ${result.meta.duration_ms}ms`);
648
+ }
649
+
650
+ if (result.warning) {
651
+ console.error("");
652
+ warn(result.warning);
653
+ }
654
+
655
+ // Output query results
656
+ if (result.results && result.results.length > 0) {
657
+ console.error("");
658
+ console.log(JSON.stringify(result.results, null, 2));
659
+ }
660
+ console.error("");
661
+
662
+ // Track telemetry
663
+ track(Events.SQL_EXECUTED, {
664
+ risk_level: result.risk,
665
+ statement_count: result.statements.length,
666
+ from_file: !!execArgs.filePath,
667
+ });
668
+ } catch (err) {
669
+ outputSpinner.stop();
670
+
671
+ if (err instanceof WriteNotAllowedError) {
672
+ console.error("");
673
+ error(err.message);
674
+ info("Add the --write flag to allow data modification:");
675
+ info(` jack services db execute "${execArgs.sql || `--file ${execArgs.filePath}`}" --write`);
676
+ console.error("");
677
+ process.exit(1);
678
+ }
679
+
680
+ if (err instanceof DestructiveOperationError) {
681
+ console.error("");
682
+ error(err.message);
683
+ info("Destructive operations require confirmation via CLI.");
684
+ console.error("");
685
+ process.exit(1);
686
+ }
687
+
688
+ console.error("");
689
+ error(`SQL execution failed: ${err instanceof Error ? err.message : String(err)}`);
690
+ console.error("");
691
+ process.exit(1);
692
+ }
693
+ }
package/src/index.ts CHANGED
@@ -49,6 +49,7 @@ const cli = meow(
49
49
  mcp MCP server for AI agents
50
50
  telemetry Usage data settings
51
51
  feedback Share feedback or report issues
52
+ community Join the jack Discord
52
53
 
53
54
  Run 'jack <command> --help' for command-specific options.
54
55
 
@@ -370,6 +371,12 @@ try {
370
371
  await withTelemetry("feedback", feedback)();
371
372
  break;
372
373
  }
374
+ case "community":
375
+ case "discord": {
376
+ const { default: community } = await import("./commands/community.ts");
377
+ await withTelemetry("community", community)();
378
+ break;
379
+ }
373
380
  case "link": {
374
381
  const { default: link } = await import("./commands/link.ts");
375
382
  await withTelemetry("link", link)(args[0], { byo: cli.flags.byo });
package/src/lib/hooks.ts CHANGED
@@ -478,6 +478,26 @@ const actionHandlers: {
478
478
  if (action.successMessage) {
479
479
  ui.success(substituteVars(action.successMessage, context));
480
480
  }
481
+
482
+ // Redeploy if deployAfter is set and we have a valid project directory
483
+ if (action.deployAfter && context.projectDir) {
484
+ const deployMsg = action.deployMessage || "Deploying...";
485
+ ui.info(deployMsg);
486
+
487
+ const proc = Bun.spawn(["wrangler", "deploy"], {
488
+ cwd: context.projectDir,
489
+ stdout: "ignore",
490
+ stderr: "pipe",
491
+ });
492
+ await proc.exited;
493
+
494
+ if (proc.exitCode === 0) {
495
+ ui.success("Deployed");
496
+ } else {
497
+ const stderr = await new Response(proc.stderr).text();
498
+ ui.warn(`Deploy failed: ${stderr.slice(0, 200)}`);
499
+ }
500
+ }
481
501
  }
482
502
 
483
503
  return true;