@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 +1 -1
- package/package.json +1 -1
- package/src/commands/clone.ts +62 -40
- package/src/commands/community.ts +47 -0
- package/src/commands/init.ts +6 -0
- package/src/commands/services.ts +354 -9
- package/src/commands/sync.ts +9 -0
- package/src/commands/update.ts +10 -0
- package/src/index.ts +7 -0
- package/src/lib/control-plane.ts +62 -0
- package/src/lib/hooks.ts +20 -0
- package/src/lib/managed-deploy.ts +26 -2
- package/src/lib/output.ts +21 -3
- package/src/lib/progress.ts +160 -0
- package/src/lib/project-operations.ts +381 -93
- package/src/lib/services/db-create.ts +6 -3
- 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/version-check.ts +14 -0
- package/src/lib/wrangler-config.test.ts +322 -0
- package/src/lib/wrangler-config.ts +649 -0
- package/src/lib/zip-packager.ts +38 -0
- package/src/lib/zip-utils.ts +38 -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
package/src/commands/clone.ts
CHANGED
|
@@ -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
|
-
//
|
|
57
|
-
const spin = spinner(`
|
|
58
|
-
|
|
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
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
//
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
|
|
70
|
-
|
|
71
|
-
|
|
83
|
+
// Link to control-plane
|
|
84
|
+
await linkProject(targetDir, project.remote.projectId, "managed");
|
|
85
|
+
await registerPath(project.remote.projectId, targetDir);
|
|
72
86
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
-
|
|
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
|
-
|
|
83
|
-
|
|
84
|
-
const
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
-
|
|
102
|
-
|
|
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
|
+
}
|
package/src/commands/init.ts
CHANGED
|
@@ -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
|
|
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
|
}
|
|
@@ -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
|
-
|
|
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
|
+
}
|
package/src/commands/sync.ts
CHANGED
|
@@ -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) {
|