@getjack/jack 0.1.23 → 0.1.25

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@getjack/jack",
3
- "version": "0.1.23",
3
+ "version": "0.1.25",
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",
@@ -49,31 +49,29 @@ async function ensureLocalProjectContext(projectName: string): Promise<void> {
49
49
 
50
50
  /**
51
51
  * Get database info for a project.
52
- * For managed: fetch from control plane
52
+ * For managed: fetch from control plane (throws on error - no silent fallback)
53
53
  * For BYO: parse from wrangler.jsonc
54
54
  */
55
55
  async function resolveDatabaseInfo(projectName: string): Promise<ResolvedDatabaseInfo | null> {
56
56
  // Read deploy mode from .jack/project.json
57
57
  const link = await readProjectLink(process.cwd());
58
58
 
59
- // For managed projects, fetch from control plane
59
+ // For managed projects, fetch from control plane (don't fall back to wrangler)
60
60
  if (link?.deploy_mode === "managed") {
61
- try {
62
- const resources = await fetchProjectResources(link.project_id);
63
- const d1 = resources.find((r) => r.resource_type === "d1");
64
- if (d1) {
65
- return {
66
- name: d1.resource_name,
67
- id: d1.provider_id,
68
- source: "control-plane",
69
- };
70
- }
71
- } catch {
72
- // Fall through to wrangler parsing
61
+ const resources = await fetchProjectResources(link.project_id);
62
+ const d1 = resources.find((r) => r.resource_type === "d1");
63
+ if (d1) {
64
+ return {
65
+ name: d1.resource_name,
66
+ id: d1.provider_id,
67
+ source: "control-plane",
68
+ };
73
69
  }
70
+ // No database in control plane for managed project
71
+ return null;
74
72
  }
75
73
 
76
- // For BYO or fallback, parse from wrangler config
74
+ // For BYO, parse from wrangler config
77
75
  try {
78
76
  await ensureLocalProjectContext(projectName);
79
77
  const resources = await parseWranglerResources(process.cwd());
@@ -85,7 +83,7 @@ async function resolveDatabaseInfo(projectName: string): Promise<ResolvedDatabas
85
83
  };
86
84
  }
87
85
  } catch {
88
- // No database found
86
+ // No database found in wrangler config
89
87
  }
90
88
 
91
89
  return null;
@@ -198,6 +196,41 @@ async function resolveProjectName(options: ServiceOptions): Promise<string> {
198
196
  */
199
197
  async function dbInfo(options: ServiceOptions): Promise<void> {
200
198
  const projectName = await resolveProjectName(options);
199
+ const projectDir = process.cwd();
200
+ const link = await readProjectLink(projectDir);
201
+
202
+ // For managed projects, use control plane API (no wrangler dependency)
203
+ if (link?.deploy_mode === "managed") {
204
+ outputSpinner.start("Fetching database info...");
205
+ try {
206
+ const { getManagedDatabaseInfo } = await import("../lib/control-plane.ts");
207
+ const dbInfo = await getManagedDatabaseInfo(link.project_id);
208
+ outputSpinner.stop();
209
+
210
+ console.error("");
211
+ success(`Database: ${dbInfo.name}`);
212
+ console.error("");
213
+ item(`Size: ${formatSize(dbInfo.sizeBytes)}`);
214
+ item(`Tables: ${dbInfo.numTables}`);
215
+ item(`ID: ${dbInfo.id}`);
216
+ item("Source: managed (jack cloud)");
217
+ console.error("");
218
+ return;
219
+ } catch (err) {
220
+ outputSpinner.stop();
221
+ console.error("");
222
+ if (err instanceof Error && err.message.includes("No database found")) {
223
+ error("No database found for this project");
224
+ info("Create one with: jack services db create");
225
+ } else {
226
+ error(`Failed to fetch database info: ${err instanceof Error ? err.message : String(err)}`);
227
+ }
228
+ console.error("");
229
+ process.exit(1);
230
+ }
231
+ }
232
+
233
+ // BYO mode: use wrangler
201
234
  const dbInfo = await resolveDatabaseInfo(projectName);
202
235
 
203
236
  if (!dbInfo) {
@@ -228,9 +261,6 @@ async function dbInfo(options: ServiceOptions): Promise<void> {
228
261
  item(`Size: ${formatSize(wranglerDbInfo.sizeBytes)}`);
229
262
  item(`Tables: ${wranglerDbInfo.numTables}`);
230
263
  item(`ID: ${dbInfo.id || wranglerDbInfo.id}`);
231
- if (dbInfo.source === "control-plane") {
232
- item("Source: managed (jack cloud)");
233
- }
234
264
  console.error("");
235
265
  }
236
266
 
@@ -238,6 +268,54 @@ async function dbInfo(options: ServiceOptions): Promise<void> {
238
268
  * Export database to SQL file
239
269
  */
240
270
  async function dbExport(options: ServiceOptions): Promise<void> {
271
+ const projectDir = process.cwd();
272
+ const link = await readProjectLink(projectDir);
273
+
274
+ // For managed projects, use control plane export API
275
+ if (link?.deploy_mode === "managed") {
276
+ outputSpinner.start("Exporting database...");
277
+ try {
278
+ const { exportManagedDatabase, getManagedDatabaseInfo } = await import(
279
+ "../lib/control-plane.ts"
280
+ );
281
+
282
+ // Get db name for filename
283
+ const dbInfo = await getManagedDatabaseInfo(link.project_id);
284
+ const filename = generateExportFilename(dbInfo.name);
285
+ const outputPath = join(projectDir, filename);
286
+
287
+ // Get export URL from control plane
288
+ const exportResult = await exportManagedDatabase(link.project_id);
289
+
290
+ // Download the export
291
+ const response = await fetch(exportResult.download_url);
292
+ if (!response.ok) {
293
+ throw new Error(`Failed to download export: ${response.status}`);
294
+ }
295
+
296
+ const content = await response.text();
297
+ await Bun.write(outputPath, content);
298
+
299
+ outputSpinner.stop();
300
+ console.error("");
301
+ success(`Exported to ./${filename}`);
302
+ console.error("");
303
+ } catch (err) {
304
+ outputSpinner.stop();
305
+ console.error("");
306
+ if (err instanceof Error && err.message.includes("No database found")) {
307
+ error("No database found for this project");
308
+ info("Create one with: jack services db create");
309
+ } else {
310
+ error(`Failed to export: ${err instanceof Error ? err.message : String(err)}`);
311
+ }
312
+ console.error("");
313
+ process.exit(1);
314
+ }
315
+ return;
316
+ }
317
+
318
+ // BYO mode: use wrangler
241
319
  const projectName = await resolveProjectName(options);
242
320
  const dbInfo = await resolveDatabaseInfo(projectName);
243
321
 
@@ -253,9 +331,9 @@ async function dbExport(options: ServiceOptions): Promise<void> {
253
331
  const filename = generateExportFilename(dbInfo.name);
254
332
 
255
333
  // Export to current directory
256
- const outputPath = join(process.cwd(), filename);
334
+ const outputPath = join(projectDir, filename);
257
335
 
258
- // Export
336
+ // Export via wrangler
259
337
  outputSpinner.start("Exporting database...");
260
338
  try {
261
339
  await exportDatabase(dbInfo.name, outputPath);
@@ -283,27 +361,69 @@ async function dbDelete(options: ServiceOptions): Promise<void> {
283
361
  const link = await readProjectLink(projectDir);
284
362
  const isManaged = link?.deploy_mode === "managed";
285
363
 
286
- const dbInfo = await resolveDatabaseInfo(projectName);
287
-
288
- if (!dbInfo) {
289
- console.error("");
290
- error("No database found for this project");
291
- info("Create one with: jack services db create");
292
- console.error("");
293
- return;
294
- }
364
+ // For managed projects, get database info from control plane
365
+ let dbInfo: ResolvedDatabaseInfo | null = null;
366
+ let displaySizeBytes: number | undefined;
367
+ let displayNumTables: number | undefined;
295
368
 
296
- // Get detailed database info to show what will be deleted
297
369
  outputSpinner.start("Fetching database info...");
298
- const wranglerDbInfo = await getWranglerDatabaseInfo(dbInfo.name);
299
- outputSpinner.stop();
370
+
371
+ if (isManaged && link) {
372
+ try {
373
+ const { getManagedDatabaseInfo } = await import("../lib/control-plane.ts");
374
+ const managedDbInfo = await getManagedDatabaseInfo(link.project_id);
375
+ outputSpinner.stop();
376
+
377
+ dbInfo = {
378
+ name: managedDbInfo.name,
379
+ id: managedDbInfo.id,
380
+ source: "control-plane",
381
+ };
382
+ displaySizeBytes = managedDbInfo.sizeBytes;
383
+ displayNumTables = managedDbInfo.numTables;
384
+ } catch (err) {
385
+ outputSpinner.stop();
386
+ if (err instanceof Error && err.message.includes("No database found")) {
387
+ console.error("");
388
+ error("No database found for this project");
389
+ info("Create one with: jack services db create");
390
+ console.error("");
391
+ return;
392
+ }
393
+ throw err;
394
+ }
395
+ } else {
396
+ // BYO mode
397
+ dbInfo = await resolveDatabaseInfo(projectName);
398
+ outputSpinner.stop();
399
+
400
+ if (!dbInfo) {
401
+ console.error("");
402
+ error("No database found for this project");
403
+ info("Create one with: jack services db create");
404
+ console.error("");
405
+ return;
406
+ }
407
+
408
+ // Get detailed info via wrangler for BYO mode
409
+ outputSpinner.start("Fetching database details...");
410
+ const wranglerDbInfo = await getWranglerDatabaseInfo(dbInfo.name);
411
+ outputSpinner.stop();
412
+
413
+ if (wranglerDbInfo) {
414
+ displaySizeBytes = wranglerDbInfo.sizeBytes;
415
+ displayNumTables = wranglerDbInfo.numTables;
416
+ }
417
+ }
300
418
 
301
419
  // Show what will be deleted
302
420
  console.error("");
303
421
  info(`Database: ${dbInfo.name}`);
304
- if (wranglerDbInfo) {
305
- item(`Size: ${formatSize(wranglerDbInfo.sizeBytes)}`);
306
- item(`Tables: ${wranglerDbInfo.numTables}`);
422
+ if (displaySizeBytes !== undefined) {
423
+ item(`Size: ${formatSize(displaySizeBytes)}`);
424
+ }
425
+ if (displayNumTables !== undefined) {
426
+ item(`Tables: ${displayNumTables}`);
307
427
  }
308
428
  console.error("");
309
429
  warn("This will permanently delete the database and all its data");
@@ -179,12 +179,34 @@ export async function checkSlugAvailability(slug: string): Promise<SlugAvailabil
179
179
  return response.json() as Promise<SlugAvailabilityResponse>;
180
180
  }
181
181
 
182
+ export interface DatabaseInfoResponse {
183
+ name: string;
184
+ id: string;
185
+ sizeBytes: number;
186
+ numTables: number;
187
+ version?: string;
188
+ createdAt?: string;
189
+ }
190
+
182
191
  export interface DatabaseExportResponse {
183
192
  success: boolean;
184
193
  download_url: string;
185
194
  expires_in: number;
186
195
  }
187
196
 
197
+ export interface ExecuteSqlResponse {
198
+ success: boolean;
199
+ results: unknown[];
200
+ meta: {
201
+ changes: number;
202
+ duration_ms: number;
203
+ last_row_id: number;
204
+ rows_read: number;
205
+ rows_written: number;
206
+ };
207
+ error?: string;
208
+ }
209
+
188
210
  export interface DeleteProjectResponse {
189
211
  success: boolean;
190
212
  project_id: string;
@@ -205,6 +227,31 @@ export interface ManagedProject {
205
227
  owner_username?: string | null;
206
228
  }
207
229
 
230
+ /**
231
+ * Get managed project's D1 database info (size, tables, etc.)
232
+ * This avoids calling wrangler and uses the control plane API.
233
+ */
234
+ export async function getManagedDatabaseInfo(projectId: string): Promise<DatabaseInfoResponse> {
235
+ const { authFetch } = await import("./auth/index.ts");
236
+
237
+ const response = await authFetch(
238
+ `${getControlApiUrl()}/v1/projects/${projectId}/database/info`,
239
+ );
240
+
241
+ if (response.status === 404) {
242
+ throw new Error("No database found for this project");
243
+ }
244
+
245
+ if (!response.ok) {
246
+ const err = (await response.json().catch(() => ({ message: "Unknown error" }))) as {
247
+ message?: string;
248
+ };
249
+ throw new Error(err.message || `Failed to get database info: ${response.status}`);
250
+ }
251
+
252
+ return response.json() as Promise<DatabaseInfoResponse>;
253
+ }
254
+
208
255
  /**
209
256
  * Export a managed project's D1 database.
210
257
  */
@@ -229,6 +276,45 @@ export async function exportManagedDatabase(projectId: string): Promise<Database
229
276
  return response.json() as Promise<DatabaseExportResponse>;
230
277
  }
231
278
 
279
+ /**
280
+ * Execute SQL against a managed project's D1 database.
281
+ * Routes through control plane for Jack Cloud auth.
282
+ */
283
+ export async function executeManagedSql(
284
+ projectId: string,
285
+ sql: string,
286
+ params?: unknown[],
287
+ ): Promise<ExecuteSqlResponse> {
288
+ const { authFetch } = await import("./auth/index.ts");
289
+
290
+ const body: { sql: string; params?: unknown[] } = { sql };
291
+ if (params && params.length > 0) {
292
+ body.params = params;
293
+ }
294
+
295
+ const response = await authFetch(
296
+ `${getControlApiUrl()}/v1/projects/${projectId}/database/execute`,
297
+ {
298
+ method: "POST",
299
+ headers: { "Content-Type": "application/json" },
300
+ body: JSON.stringify(body),
301
+ },
302
+ );
303
+
304
+ if (response.status === 504) {
305
+ throw new Error("SQL execution timed out. The query may be too complex.");
306
+ }
307
+
308
+ if (!response.ok) {
309
+ const err = (await response.json().catch(() => ({ message: "Unknown error" }))) as {
310
+ message?: string;
311
+ };
312
+ throw new Error(err.message || `Failed to execute SQL: ${response.status}`);
313
+ }
314
+
315
+ return response.json() as Promise<ExecuteSqlResponse>;
316
+ }
317
+
232
318
  /**
233
319
  * Delete a managed project and all its resources.
234
320
  */
@@ -11,6 +11,9 @@
11
11
  import { existsSync } from "node:fs";
12
12
  import { join } from "node:path";
13
13
  import { $ } from "bun";
14
+ import { type ExecuteSqlResponse, executeManagedSql } from "../control-plane.ts";
15
+ import { readProjectLink } from "../project-link.ts";
16
+ import { type D1BindingConfig, getExistingD1Bindings } from "../wrangler-config.ts";
14
17
  import {
15
18
  type ClassifiedStatement,
16
19
  type RiskLevel,
@@ -19,7 +22,6 @@ import {
19
22
  getRiskDescription,
20
23
  splitStatements,
21
24
  } from "./sql-classifier.ts";
22
- import { getExistingD1Bindings, type D1BindingConfig } from "../wrangler-config.ts";
23
25
 
24
26
  export interface ExecuteSqlOptions {
25
27
  /** Path to project directory */
@@ -367,6 +369,55 @@ export async function executeSql(options: ExecuteSqlOptions): Promise<ExecuteSql
367
369
  }
368
370
  }
369
371
 
372
+ // Check for managed mode - route through control plane
373
+ const link = await readProjectLink(projectDir);
374
+ if (link?.deploy_mode === "managed") {
375
+ try {
376
+ const managedResult = await executeManagedSql(link.project_id, sql);
377
+
378
+ if (!managedResult.success) {
379
+ return {
380
+ success: false,
381
+ risk: highestRisk,
382
+ statements,
383
+ error: managedResult.error || "Failed to execute SQL",
384
+ };
385
+ }
386
+
387
+ // Build result matching wrangler format
388
+ const result: ExecuteSqlResult = {
389
+ success: true,
390
+ risk: highestRisk,
391
+ statements,
392
+ results: managedResult.results,
393
+ meta: {
394
+ changes: managedResult.meta.changes,
395
+ duration_ms: managedResult.meta.duration_ms,
396
+ last_row_id: managedResult.meta.last_row_id,
397
+ },
398
+ };
399
+
400
+ // Add warning for destructive ops
401
+ if (highestRisk === "destructive") {
402
+ const ops = statements
403
+ .filter((s) => s.risk === "destructive")
404
+ .map((s) => s.operation)
405
+ .join(", ");
406
+ result.warning = `Executed destructive operation(s): ${ops}`;
407
+ }
408
+
409
+ return result;
410
+ } catch (error) {
411
+ return {
412
+ success: false,
413
+ risk: highestRisk,
414
+ statements,
415
+ error: error instanceof Error ? error.message : "Failed to execute SQL via control plane",
416
+ };
417
+ }
418
+ }
419
+
420
+ // BYO mode: use wrangler
370
421
  // Get database
371
422
  const db = databaseName
372
423
  ? await getDatabaseByName(projectDir, databaseName)
@@ -475,6 +526,54 @@ export async function executeSqlFile(
475
526
  }
476
527
  }
477
528
 
529
+ // Check for managed mode - route through control plane
530
+ const link = await readProjectLink(projectDir);
531
+ if (link?.deploy_mode === "managed") {
532
+ try {
533
+ const managedResult = await executeManagedSql(link.project_id, sql);
534
+
535
+ if (!managedResult.success) {
536
+ return {
537
+ success: false,
538
+ risk: highestRisk,
539
+ statements,
540
+ error: managedResult.error || "Failed to execute SQL",
541
+ };
542
+ }
543
+
544
+ // Build result matching wrangler format
545
+ const result: ExecuteSqlResult = {
546
+ success: true,
547
+ risk: highestRisk,
548
+ statements,
549
+ results: managedResult.results,
550
+ meta: {
551
+ changes: managedResult.meta.changes,
552
+ duration_ms: managedResult.meta.duration_ms,
553
+ last_row_id: managedResult.meta.last_row_id,
554
+ },
555
+ };
556
+
557
+ // Add warning for destructive ops
558
+ if (highestRisk === "destructive") {
559
+ const ops = [
560
+ ...new Set(statements.filter((s) => s.risk === "destructive").map((s) => s.operation)),
561
+ ].join(", ");
562
+ result.warning = `Executed destructive operation(s): ${ops}`;
563
+ }
564
+
565
+ return result;
566
+ } catch (error) {
567
+ return {
568
+ success: false,
569
+ risk: highestRisk,
570
+ statements,
571
+ error: error instanceof Error ? error.message : "Failed to execute SQL via control plane",
572
+ };
573
+ }
574
+ }
575
+
576
+ // BYO mode: use wrangler
478
577
  // Get database
479
578
  const db = databaseName
480
579
  ? await getDatabaseByName(projectDir, databaseName)
@@ -2,9 +2,11 @@
2
2
  * Database listing logic for jack services db list
3
3
  *
4
4
  * Lists D1 databases configured in wrangler.jsonc with their metadata.
5
+ * For managed projects, fetches metadata via control plane instead of wrangler.
5
6
  */
6
7
 
7
8
  import { join } from "node:path";
9
+ import { readProjectLink } from "../project-link.ts";
8
10
  import { getExistingD1Bindings } from "../wrangler-config.ts";
9
11
  import { getDatabaseInfo } from "./db.ts";
10
12
 
@@ -19,8 +21,8 @@ export interface DatabaseListEntry {
19
21
  /**
20
22
  * List all D1 databases configured for a project.
21
23
  *
22
- * Reads bindings from wrangler.jsonc and fetches additional metadata
23
- * (size, table count) via wrangler d1 info for each database.
24
+ * For managed projects: fetches metadata via control plane API.
25
+ * For BYO projects: reads bindings from wrangler.jsonc and fetches metadata via wrangler.
24
26
  */
25
27
  export async function listDatabases(projectDir: string): Promise<DatabaseListEntry[]> {
26
28
  const wranglerPath = join(projectDir, "wrangler.jsonc");
@@ -32,6 +34,22 @@ export async function listDatabases(projectDir: string): Promise<DatabaseListEnt
32
34
  return [];
33
35
  }
34
36
 
37
+ // Check deploy mode for metadata fetching
38
+ const link = await readProjectLink(projectDir);
39
+ const isManaged = link?.deploy_mode === "managed";
40
+
41
+ // For managed projects, get metadata from control plane
42
+ let managedDbInfo: { name: string; id: string; sizeBytes: number; numTables: number } | null =
43
+ null;
44
+ if (isManaged && link) {
45
+ try {
46
+ const { getManagedDatabaseInfo } = await import("../control-plane.ts");
47
+ managedDbInfo = await getManagedDatabaseInfo(link.project_id);
48
+ } catch {
49
+ // Fall through - will show list without metadata
50
+ }
51
+ }
52
+
35
53
  // Fetch detailed info for each database
36
54
  const entries: DatabaseListEntry[] = [];
37
55
 
@@ -42,11 +60,20 @@ export async function listDatabases(projectDir: string): Promise<DatabaseListEnt
42
60
  id: binding.database_id,
43
61
  };
44
62
 
45
- // Try to get additional metadata via wrangler
46
- const info = await getDatabaseInfo(binding.database_name);
47
- if (info) {
48
- entry.sizeBytes = info.sizeBytes;
49
- entry.numTables = info.numTables;
63
+ // Get metadata based on deploy mode
64
+ if (isManaged && managedDbInfo) {
65
+ // For managed: use control plane data (match by ID or name)
66
+ if (managedDbInfo.id === binding.database_id || managedDbInfo.name.includes(binding.database_name)) {
67
+ entry.sizeBytes = managedDbInfo.sizeBytes;
68
+ entry.numTables = managedDbInfo.numTables;
69
+ }
70
+ } else {
71
+ // For BYO: try to get metadata via wrangler
72
+ const info = await getDatabaseInfo(binding.database_name);
73
+ if (info) {
74
+ entry.sizeBytes = info.sizeBytes;
75
+ entry.numTables = info.numTables;
76
+ }
50
77
  }
51
78
 
52
79
  entries.push(entry);