@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 +1 -1
- package/package.json +5 -2
- package/src/commands/community.ts +47 -0
- package/src/commands/services.ts +269 -5
- package/src/index.ts +7 -0
- package/src/lib/hooks.ts +20 -0
- package/src/lib/project-operations.ts +278 -31
- package/src/lib/services/db-execute.ts +485 -0
- package/src/lib/services/sql-classifier.test.ts +404 -0
- package/src/lib/services/sql-classifier.ts +346 -0
- package/src/lib/storage/file-filter.ts +4 -0
- package/src/lib/telemetry.ts +3 -0
- package/src/lib/wrangler-config.test.ts +322 -0
- package/src/lib/wrangler-config.ts +459 -0
- package/src/mcp/tools/index.ts +161 -0
- package/src/templates/index.ts +4 -0
- package/src/templates/types.ts +12 -0
- package/templates/api/AGENTS.md +33 -0
- package/templates/hello/AGENTS.md +33 -0
- package/templates/miniapp/.jack.json +4 -5
- package/templates/miniapp/AGENTS.md +33 -0
- package/templates/nextjs/AGENTS.md +33 -0
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://
|
|
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.
|
|
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": [
|
|
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
|
+
}
|
package/src/commands/services.ts
CHANGED
|
@@ -1,10 +1,17 @@
|
|
|
1
|
-
import {
|
|
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(
|
|
133
|
-
|
|
134
|
-
|
|
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;
|