@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 +1 -1
- package/src/commands/services.ts +156 -36
- package/src/lib/control-plane.ts +86 -0
- package/src/lib/services/db-execute.ts +100 -1
- package/src/lib/services/db-list.ts +34 -7
- package/src/lib/services/vectorize-config.ts +569 -0
- package/src/lib/services/vectorize-create.ts +166 -0
- package/src/lib/services/vectorize-delete.ts +54 -0
- package/src/lib/services/vectorize-info.ts +52 -0
- package/src/lib/services/vectorize-list.ts +56 -0
- package/src/lib/version-check.ts +2 -2
- package/src/mcp/test-utils.ts +47 -2
- package/src/mcp/tools/index.ts +282 -0
- package/templates/AI-BINDINGS.md +181 -0
- package/templates/CLAUDE.md +30 -0
- package/templates/ai-chat/.jack.json +3 -4
- package/templates/ai-chat/src/index.ts +45 -5
- package/templates/ai-chat/src/jack-ai.ts +96 -0
- package/templates/semantic-search/.jack.json +3 -4
- package/templates/semantic-search/src/index.ts +70 -12
- package/templates/semantic-search/src/jack-ai.ts +96 -0
- package/templates/semantic-search/src/jack-vectorize.ts +165 -0
package/package.json
CHANGED
package/src/commands/services.ts
CHANGED
|
@@ -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
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
|
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(
|
|
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
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
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
|
-
|
|
299
|
-
|
|
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 (
|
|
305
|
-
item(`Size: ${formatSize(
|
|
306
|
-
|
|
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");
|
package/src/lib/control-plane.ts
CHANGED
|
@@ -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
|
-
*
|
|
23
|
-
*
|
|
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
|
-
//
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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);
|