@getjack/jack 0.1.34 → 0.1.36
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 +6 -6
- package/package.json +1 -1
- package/src/commands/down.ts +39 -7
- package/src/commands/link.ts +2 -4
- package/src/commands/logs.ts +2 -4
- package/src/commands/mcp.ts +12 -10
- package/src/commands/services.ts +4 -2
- package/src/commands/sync.ts +5 -6
- package/src/commands/update.ts +1 -0
- package/src/index.ts +8 -0
- package/src/lib/auth/client.ts +5 -2
- package/src/lib/binding-validator.ts +39 -3
- package/src/lib/build-helper.ts +18 -19
- package/src/lib/control-plane.ts +45 -0
- package/src/lib/do-config.ts +110 -0
- package/src/lib/do-export-validator.ts +26 -0
- package/src/lib/jsonc-edit.ts +292 -0
- package/src/lib/managed-deploy.ts +36 -1
- package/src/lib/project-link.ts +37 -0
- package/src/lib/project-operations.ts +31 -66
- package/src/lib/resources.ts +4 -5
- package/src/lib/schema.ts +8 -12
- package/src/lib/services/db-create.ts +2 -2
- package/src/lib/services/db-execute.ts +9 -6
- package/src/lib/services/db-list.ts +6 -4
- package/src/lib/services/endpoint-test.ts +275 -0
- package/src/lib/services/project-delete.ts +190 -0
- package/src/lib/services/project-environment.ts +579 -0
- package/src/lib/services/storage-config.ts +7 -309
- package/src/lib/services/storage-create.ts +2 -1
- package/src/lib/services/storage-delete.ts +3 -2
- package/src/lib/services/storage-info.ts +2 -1
- package/src/lib/services/storage-list.ts +6 -3
- package/src/lib/services/vectorize-config.ts +7 -264
- package/src/lib/services/vectorize-create.ts +2 -1
- package/src/lib/services/vectorize-delete.ts +6 -4
- package/src/lib/services/vectorize-list.ts +6 -3
- package/src/lib/storage/index.ts +21 -23
- package/src/lib/telemetry.ts +1 -0
- package/src/lib/wrangler-config.ts +43 -312
- package/src/lib/zip-packager.ts +28 -0
- package/src/mcp/test-utils.ts +31 -0
- package/src/mcp/tools/index.ts +280 -2
- package/src/templates/index.ts +5 -0
- package/src/templates/types.ts +4 -0
- package/templates/AI-BINDINGS.md +34 -76
- package/templates/CLAUDE.md +1 -1
- package/templates/ai-chat/src/index.ts +7 -14
- package/templates/ai-chat/src/jack-ai.ts +0 -6
- package/templates/chat/.jack.json +45 -0
- package/templates/chat/bun.lock +1584 -0
- package/templates/chat/components.json +23 -0
- package/templates/chat/index.html +12 -0
- package/templates/chat/package.json +41 -0
- package/templates/chat/src/chat-agent.ts +63 -0
- package/templates/chat/src/client/app.tsx +189 -0
- package/templates/chat/src/client/chat.tsx +222 -0
- package/templates/chat/src/client/components/prompt-kit/chat-container.tsx +47 -0
- package/templates/chat/src/client/components/prompt-kit/loader.tsx +33 -0
- package/templates/chat/src/client/components/prompt-kit/markdown.tsx +84 -0
- package/templates/chat/src/client/components/prompt-kit/message.tsx +54 -0
- package/templates/chat/src/client/components/prompt-kit/prompt-suggestion.tsx +20 -0
- package/templates/chat/src/client/components/prompt-kit/reasoning.tsx +134 -0
- package/templates/chat/src/client/components/prompt-kit/scroll-button.tsx +28 -0
- package/templates/chat/src/client/components/ui/button.tsx +38 -0
- package/templates/chat/src/client/lib/utils.ts +6 -0
- package/templates/chat/src/client/main.tsx +11 -0
- package/templates/chat/src/client/styles.css +125 -0
- package/templates/chat/src/index.ts +25 -0
- package/templates/chat/src/jack-ai.ts +94 -0
- package/templates/chat/tsconfig.json +18 -0
- package/templates/chat/vite.config.ts +14 -0
- package/templates/chat/wrangler.jsonc +18 -0
- package/templates/cron/.jack.json +18 -28
- package/templates/cron/schema.sql +10 -20
- package/templates/cron/src/admin.ts +321 -0
- package/templates/cron/src/index.ts +151 -81
- package/templates/cron/src/monitor.ts +124 -0
- package/templates/semantic-search/src/index.ts +5 -43
- package/templates/semantic-search/src/jack-ai.ts +0 -6
- package/templates/telegram-bot/.jack.json +56 -0
- package/templates/telegram-bot/bun.lock +41 -0
- package/templates/telegram-bot/package.json +16 -0
- package/templates/telegram-bot/src/index.ts +236 -0
- package/templates/telegram-bot/src/jack-ai.ts +100 -0
- package/templates/telegram-bot/tsconfig.json +11 -0
- package/templates/telegram-bot/wrangler.jsonc +8 -0
- package/templates/cron/src/jobs.ts +0 -139
- package/templates/cron/src/webhooks.ts +0 -95
- package/templates/semantic-search/src/jack-vectorize.ts +0 -169
|
@@ -9,11 +9,14 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import { existsSync } from "node:fs";
|
|
12
|
-
import { join } from "node:path";
|
|
13
12
|
import { $ } from "bun";
|
|
14
13
|
import { type ExecuteSqlResponse, executeManagedSql } from "../control-plane.ts";
|
|
15
14
|
import { readProjectLink } from "../project-link.ts";
|
|
16
|
-
import {
|
|
15
|
+
import {
|
|
16
|
+
type D1BindingConfig,
|
|
17
|
+
findWranglerConfig,
|
|
18
|
+
getExistingD1Bindings,
|
|
19
|
+
} from "../wrangler-config.ts";
|
|
17
20
|
import {
|
|
18
21
|
type ClassifiedStatement,
|
|
19
22
|
type RiskLevel,
|
|
@@ -98,9 +101,9 @@ export class DestructiveOperationError extends Error {
|
|
|
98
101
|
* Get the first D1 database configured for a project
|
|
99
102
|
*/
|
|
100
103
|
export async function getDefaultDatabase(projectDir: string): Promise<D1BindingConfig | null> {
|
|
101
|
-
const wranglerPath =
|
|
104
|
+
const wranglerPath = findWranglerConfig(projectDir);
|
|
102
105
|
|
|
103
|
-
if (!
|
|
106
|
+
if (!wranglerPath) {
|
|
104
107
|
return null;
|
|
105
108
|
}
|
|
106
109
|
|
|
@@ -119,9 +122,9 @@ export async function getDatabaseByName(
|
|
|
119
122
|
projectDir: string,
|
|
120
123
|
databaseName: string,
|
|
121
124
|
): Promise<D1BindingConfig | null> {
|
|
122
|
-
const wranglerPath =
|
|
125
|
+
const wranglerPath = findWranglerConfig(projectDir);
|
|
123
126
|
|
|
124
|
-
if (!
|
|
127
|
+
if (!wranglerPath) {
|
|
125
128
|
return null;
|
|
126
129
|
}
|
|
127
130
|
|
|
@@ -5,9 +5,8 @@
|
|
|
5
5
|
* For managed projects, fetches metadata via control plane instead of wrangler.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import { join } from "node:path";
|
|
9
8
|
import { readProjectLink } from "../project-link.ts";
|
|
10
|
-
import { getExistingD1Bindings } from "../wrangler-config.ts";
|
|
9
|
+
import { findWranglerConfig, getExistingD1Bindings } from "../wrangler-config.ts";
|
|
11
10
|
import { getDatabaseInfo } from "./db.ts";
|
|
12
11
|
|
|
13
12
|
export interface DatabaseListEntry {
|
|
@@ -25,9 +24,12 @@ export interface DatabaseListEntry {
|
|
|
25
24
|
* For BYO projects: reads bindings from wrangler.jsonc and fetches metadata via wrangler.
|
|
26
25
|
*/
|
|
27
26
|
export async function listDatabases(projectDir: string): Promise<DatabaseListEntry[]> {
|
|
28
|
-
const wranglerPath =
|
|
27
|
+
const wranglerPath = findWranglerConfig(projectDir);
|
|
28
|
+
if (!wranglerPath) {
|
|
29
|
+
return [];
|
|
30
|
+
}
|
|
29
31
|
|
|
30
|
-
// Get existing D1 bindings from wrangler
|
|
32
|
+
// Get existing D1 bindings from wrangler config
|
|
31
33
|
const bindings = await getExistingD1Bindings(wranglerPath);
|
|
32
34
|
|
|
33
35
|
if (bindings.length === 0) {
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Endpoint testing service
|
|
3
|
+
*
|
|
4
|
+
* Makes HTTP requests to deployed workers and optionally captures
|
|
5
|
+
* runtime logs during the request. Used by MCP (test_endpoint tool).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { readProjectLink } from "../project-link.ts";
|
|
9
|
+
import { getProjectStatus } from "../project-operations.ts";
|
|
10
|
+
|
|
11
|
+
// ============================================================================
|
|
12
|
+
// Types
|
|
13
|
+
// ============================================================================
|
|
14
|
+
|
|
15
|
+
export interface TestEndpointOptions {
|
|
16
|
+
/** Path to project directory */
|
|
17
|
+
projectDir: string;
|
|
18
|
+
/** URL path to test, e.g. /api/todos */
|
|
19
|
+
path: string;
|
|
20
|
+
/** HTTP method */
|
|
21
|
+
method?: "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
|
|
22
|
+
/** Request headers */
|
|
23
|
+
headers?: Record<string, string>;
|
|
24
|
+
/** Request body (JSON string for POST/PUT/PATCH) */
|
|
25
|
+
body?: string;
|
|
26
|
+
/** Capture runtime logs during the request (managed mode only) */
|
|
27
|
+
includeLogs?: boolean;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface TestEndpointResult {
|
|
31
|
+
request: {
|
|
32
|
+
method: string;
|
|
33
|
+
url: string;
|
|
34
|
+
headers: Record<string, string>;
|
|
35
|
+
body: string | null;
|
|
36
|
+
};
|
|
37
|
+
response: {
|
|
38
|
+
status: number;
|
|
39
|
+
status_text: string;
|
|
40
|
+
headers: Record<string, string>;
|
|
41
|
+
body: string;
|
|
42
|
+
duration_ms: number;
|
|
43
|
+
};
|
|
44
|
+
logs: LogEntry[];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface LogEntry {
|
|
48
|
+
level: string;
|
|
49
|
+
message: unknown[];
|
|
50
|
+
timestamp: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// ============================================================================
|
|
54
|
+
// Main Function
|
|
55
|
+
// ============================================================================
|
|
56
|
+
|
|
57
|
+
const REQUEST_TIMEOUT_MS = 30_000;
|
|
58
|
+
const LOG_SETTLE_DELAY_MS = 500;
|
|
59
|
+
const LOG_FLUSH_DELAY_MS = 1_500;
|
|
60
|
+
const LOG_COLLECT_DURATION_MS = 3_000;
|
|
61
|
+
const MAX_RESPONSE_BODY_SIZE = 1_000_000; // 1MB
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Test a deployed endpoint by making an HTTP request and optionally capturing logs.
|
|
65
|
+
*/
|
|
66
|
+
export async function testEndpoint(options: TestEndpointOptions): Promise<TestEndpointResult> {
|
|
67
|
+
const { projectDir, path, method = "GET", headers = {}, body, includeLogs = true } = options;
|
|
68
|
+
|
|
69
|
+
// 1. Resolve project URL
|
|
70
|
+
const link = await readProjectLink(projectDir);
|
|
71
|
+
const status = await getProjectStatus(undefined, projectDir);
|
|
72
|
+
|
|
73
|
+
if (!status?.workerUrl) {
|
|
74
|
+
throw new Error("Project has no deployed URL. Deploy first with 'jack ship'.");
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const baseUrl = status.workerUrl.replace(/\/$/, "");
|
|
78
|
+
const normalizedPath = path.startsWith("/") ? path : `/${path}`;
|
|
79
|
+
|
|
80
|
+
// Validate path stays on the same origin (prevent URL manipulation)
|
|
81
|
+
const resolvedUrl = new URL(normalizedPath, baseUrl);
|
|
82
|
+
if (resolvedUrl.origin !== new URL(baseUrl).origin) {
|
|
83
|
+
throw new Error("Path must not redirect to a different host");
|
|
84
|
+
}
|
|
85
|
+
const fullUrl = resolvedUrl.href;
|
|
86
|
+
|
|
87
|
+
// 2. Optionally start log session (managed only)
|
|
88
|
+
let logSessionCleanup: (() => void) | null = null;
|
|
89
|
+
let logCollector: Promise<LogEntry[]> | null = null;
|
|
90
|
+
|
|
91
|
+
const isManaged = link?.deploy_mode === "managed" && link.project_id;
|
|
92
|
+
if (includeLogs && isManaged) {
|
|
93
|
+
const logResult = await startLogCollection(link.project_id);
|
|
94
|
+
logSessionCleanup = logResult.cleanup;
|
|
95
|
+
logCollector = logResult.collector;
|
|
96
|
+
|
|
97
|
+
// Small delay for SSE connection to establish
|
|
98
|
+
await sleep(LOG_SETTLE_DELAY_MS);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// 3. Make the HTTP request
|
|
102
|
+
const requestHeaders: Record<string, string> = { ...headers };
|
|
103
|
+
if (body && !requestHeaders["content-type"]) {
|
|
104
|
+
requestHeaders["content-type"] = "application/json";
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const requestStart = Date.now();
|
|
108
|
+
let response: Response;
|
|
109
|
+
try {
|
|
110
|
+
const controller = new AbortController();
|
|
111
|
+
const timeout = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
|
|
112
|
+
|
|
113
|
+
response = await fetch(fullUrl, {
|
|
114
|
+
method,
|
|
115
|
+
headers: requestHeaders,
|
|
116
|
+
body: body || undefined,
|
|
117
|
+
signal: controller.signal,
|
|
118
|
+
redirect: "follow",
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
clearTimeout(timeout);
|
|
122
|
+
} catch (error) {
|
|
123
|
+
logSessionCleanup?.();
|
|
124
|
+
if (error instanceof Error && error.name === "AbortError") {
|
|
125
|
+
throw new Error(`Request timed out after ${REQUEST_TIMEOUT_MS / 1000}s`);
|
|
126
|
+
}
|
|
127
|
+
throw new Error(`Request failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const durationMs = Date.now() - requestStart;
|
|
131
|
+
|
|
132
|
+
// 4. Read response + collect logs, ensuring cleanup on any failure
|
|
133
|
+
try {
|
|
134
|
+
const rawBody = await response.text();
|
|
135
|
+
// Truncate large responses to prevent MCP message bloat (1MB limit)
|
|
136
|
+
const responseBody =
|
|
137
|
+
rawBody.length > MAX_RESPONSE_BODY_SIZE
|
|
138
|
+
? `${rawBody.slice(0, MAX_RESPONSE_BODY_SIZE)}\n... [truncated, ${rawBody.length} bytes total]`
|
|
139
|
+
: rawBody;
|
|
140
|
+
const responseHeaders: Record<string, string> = {};
|
|
141
|
+
response.headers.forEach((value, key) => {
|
|
142
|
+
responseHeaders[key] = value;
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
// 5. Collect logs if started
|
|
146
|
+
let logs: LogEntry[] = [];
|
|
147
|
+
if (logCollector) {
|
|
148
|
+
// Wait for logs to flush from the worker
|
|
149
|
+
await sleep(LOG_FLUSH_DELAY_MS);
|
|
150
|
+
logSessionCleanup?.();
|
|
151
|
+
logSessionCleanup = null;
|
|
152
|
+
logs = await logCollector;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return {
|
|
156
|
+
request: {
|
|
157
|
+
method,
|
|
158
|
+
url: fullUrl,
|
|
159
|
+
headers: requestHeaders,
|
|
160
|
+
body: body ?? null,
|
|
161
|
+
},
|
|
162
|
+
response: {
|
|
163
|
+
status: response.status,
|
|
164
|
+
status_text: response.statusText,
|
|
165
|
+
headers: responseHeaders,
|
|
166
|
+
body: responseBody,
|
|
167
|
+
duration_ms: durationMs,
|
|
168
|
+
},
|
|
169
|
+
logs,
|
|
170
|
+
};
|
|
171
|
+
} finally {
|
|
172
|
+
// Ensure log session is always cleaned up
|
|
173
|
+
logSessionCleanup?.();
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// ============================================================================
|
|
178
|
+
// Internal Helpers
|
|
179
|
+
// ============================================================================
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Start collecting logs from a managed project's SSE stream.
|
|
183
|
+
* Returns a collector promise that resolves with accumulated log entries
|
|
184
|
+
* and a cleanup function to stop the stream.
|
|
185
|
+
*/
|
|
186
|
+
async function startLogCollection(
|
|
187
|
+
projectId: string,
|
|
188
|
+
): Promise<{ collector: Promise<LogEntry[]>; cleanup: () => void }> {
|
|
189
|
+
const { startLogSession, getControlApiUrl } = await import("../control-plane.ts");
|
|
190
|
+
const { authFetch } = await import("../auth/index.ts");
|
|
191
|
+
|
|
192
|
+
const session = await startLogSession(projectId, "endpoint-test");
|
|
193
|
+
const streamUrl = `${getControlApiUrl()}${session.stream.url}`;
|
|
194
|
+
|
|
195
|
+
const controller = new AbortController();
|
|
196
|
+
const cleanup = () => controller.abort();
|
|
197
|
+
|
|
198
|
+
const collector = collectLogEvents(streamUrl, controller, authFetch);
|
|
199
|
+
|
|
200
|
+
return { collector, cleanup };
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Read log events from an SSE stream until aborted or timeout.
|
|
205
|
+
*/
|
|
206
|
+
async function collectLogEvents(
|
|
207
|
+
streamUrl: string,
|
|
208
|
+
controller: AbortController,
|
|
209
|
+
authFetch: (url: string, init?: RequestInit) => Promise<Response>,
|
|
210
|
+
): Promise<LogEntry[]> {
|
|
211
|
+
const events: LogEntry[] = [];
|
|
212
|
+
|
|
213
|
+
// Auto-timeout for safety
|
|
214
|
+
const timeout = setTimeout(() => controller.abort(), LOG_COLLECT_DURATION_MS);
|
|
215
|
+
|
|
216
|
+
try {
|
|
217
|
+
const response = await authFetch(streamUrl, {
|
|
218
|
+
method: "GET",
|
|
219
|
+
headers: { Accept: "text/event-stream" },
|
|
220
|
+
signal: controller.signal,
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
if (!response.ok || !response.body) {
|
|
224
|
+
return events;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const reader = response.body.getReader();
|
|
228
|
+
const decoder = new TextDecoder();
|
|
229
|
+
let buffer = "";
|
|
230
|
+
|
|
231
|
+
while (true) {
|
|
232
|
+
const { done, value } = await reader.read();
|
|
233
|
+
if (done) break;
|
|
234
|
+
|
|
235
|
+
buffer += decoder.decode(value, { stream: true });
|
|
236
|
+
const lines = buffer.split("\n");
|
|
237
|
+
buffer = lines.pop() || "";
|
|
238
|
+
|
|
239
|
+
for (const line of lines) {
|
|
240
|
+
if (!line.startsWith("data:")) continue;
|
|
241
|
+
const data = line.slice(5).trim();
|
|
242
|
+
if (!data) continue;
|
|
243
|
+
|
|
244
|
+
try {
|
|
245
|
+
const parsed = JSON.parse(data) as {
|
|
246
|
+
type?: string;
|
|
247
|
+
level?: string;
|
|
248
|
+
message?: unknown[];
|
|
249
|
+
timestamp?: string;
|
|
250
|
+
};
|
|
251
|
+
if (parsed.type === "event") {
|
|
252
|
+
events.push({
|
|
253
|
+
level: parsed.level ?? "log",
|
|
254
|
+
message: parsed.message ?? [],
|
|
255
|
+
timestamp: parsed.timestamp ?? new Date().toISOString(),
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
} catch {}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
} catch (error) {
|
|
262
|
+
// Abort is normal (cleanup or timeout). Non-abort errors: return whatever we collected.
|
|
263
|
+
if (error instanceof Error && error.name !== "AbortError") {
|
|
264
|
+
// Silently swallow — partial logs are better than no response
|
|
265
|
+
}
|
|
266
|
+
} finally {
|
|
267
|
+
clearTimeout(timeout);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return events;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function sleep(ms: number): Promise<void> {
|
|
274
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
275
|
+
}
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Project deletion service — shared between CLI (jack down --force) and MCP (delete_project).
|
|
3
|
+
*
|
|
4
|
+
* Handles both managed (control plane) and BYO (wrangler) deploy modes.
|
|
5
|
+
* Non-fatal failures (export, individual resource) populate warnings[], not thrown.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { mkdirSync } from "node:fs";
|
|
9
|
+
import { writeFile } from "node:fs/promises";
|
|
10
|
+
import { join } from "node:path";
|
|
11
|
+
import {
|
|
12
|
+
checkWorkerExists,
|
|
13
|
+
deleteDatabase,
|
|
14
|
+
deleteWorker,
|
|
15
|
+
exportDatabase,
|
|
16
|
+
} from "../cloudflare-api.ts";
|
|
17
|
+
import { getJackHome } from "../config.ts";
|
|
18
|
+
import {
|
|
19
|
+
deleteManagedProject,
|
|
20
|
+
exportManagedDatabase,
|
|
21
|
+
fetchProjectResources,
|
|
22
|
+
} from "../control-plane.ts";
|
|
23
|
+
import { readProjectLink } from "../project-link.ts";
|
|
24
|
+
import { parseWranglerResources } from "../resources.ts";
|
|
25
|
+
import { getProjectNameFromDir } from "../storage/index.ts";
|
|
26
|
+
|
|
27
|
+
export interface DeleteProjectOptions {
|
|
28
|
+
exportDatabase?: boolean;
|
|
29
|
+
exportDir?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface DeleteProjectResult {
|
|
33
|
+
projectName: string;
|
|
34
|
+
deployMode: "managed" | "byo";
|
|
35
|
+
workerDeleted: boolean;
|
|
36
|
+
databaseDeleted: boolean;
|
|
37
|
+
databaseName: string | null;
|
|
38
|
+
databaseExportPath: string | null;
|
|
39
|
+
resourceResults?: Array<{ resource: string; success: boolean; error?: string }>;
|
|
40
|
+
warnings: string[];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export async function deleteProject(
|
|
44
|
+
projectDir: string,
|
|
45
|
+
options?: DeleteProjectOptions,
|
|
46
|
+
): Promise<DeleteProjectResult> {
|
|
47
|
+
const exportDb = options?.exportDatabase ?? false;
|
|
48
|
+
const warnings: string[] = [];
|
|
49
|
+
|
|
50
|
+
const projectName = await getProjectNameFromDir(projectDir);
|
|
51
|
+
const link = await readProjectLink(projectDir);
|
|
52
|
+
const deployMode = link?.deploy_mode ?? "byo";
|
|
53
|
+
|
|
54
|
+
if (deployMode === "managed" && link) {
|
|
55
|
+
return deleteManagedFlow(link.project_id, projectName, projectDir, exportDb, options, warnings);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return deleteByoFlow(projectName, projectDir, exportDb, options, warnings);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function deleteManagedFlow(
|
|
62
|
+
projectId: string,
|
|
63
|
+
projectName: string,
|
|
64
|
+
projectDir: string,
|
|
65
|
+
exportDb: boolean,
|
|
66
|
+
options: DeleteProjectOptions | undefined,
|
|
67
|
+
warnings: string[],
|
|
68
|
+
): Promise<DeleteProjectResult> {
|
|
69
|
+
let databaseName: string | null = null;
|
|
70
|
+
let databaseExportPath: string | null = null;
|
|
71
|
+
|
|
72
|
+
// Resolve database name from control plane
|
|
73
|
+
try {
|
|
74
|
+
const resources = await fetchProjectResources(projectId);
|
|
75
|
+
const d1 = resources.find((r) => r.resource_type === "d1");
|
|
76
|
+
databaseName = d1?.resource_name ?? null;
|
|
77
|
+
} catch {
|
|
78
|
+
// Can't resolve — continue without DB info
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Optional export before deletion
|
|
82
|
+
if (exportDb && databaseName) {
|
|
83
|
+
const exportDir = options?.exportDir ?? projectDir ?? join(getJackHome(), projectName);
|
|
84
|
+
mkdirSync(exportDir, { recursive: true });
|
|
85
|
+
const exportPath = join(exportDir, `${projectName}-backup.sql`);
|
|
86
|
+
|
|
87
|
+
try {
|
|
88
|
+
const exportResult = await exportManagedDatabase(projectId);
|
|
89
|
+
const response = await fetch(exportResult.download_url);
|
|
90
|
+
if (!response.ok) {
|
|
91
|
+
throw new Error(`Failed to download export: ${response.statusText}`);
|
|
92
|
+
}
|
|
93
|
+
const sqlContent = await response.text();
|
|
94
|
+
await writeFile(exportPath, sqlContent, "utf-8");
|
|
95
|
+
databaseExportPath = exportPath;
|
|
96
|
+
} catch (err) {
|
|
97
|
+
warnings.push(`Database export failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Delete everything via control plane
|
|
102
|
+
const result = await deleteManagedProject(projectId);
|
|
103
|
+
|
|
104
|
+
for (const resource of result.resources) {
|
|
105
|
+
if (!resource.success) {
|
|
106
|
+
warnings.push(`Failed to delete ${resource.resource}: ${resource.error}`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
projectName,
|
|
112
|
+
deployMode: "managed",
|
|
113
|
+
workerDeleted: true,
|
|
114
|
+
databaseDeleted: databaseName !== null,
|
|
115
|
+
databaseName,
|
|
116
|
+
databaseExportPath,
|
|
117
|
+
resourceResults: result.resources,
|
|
118
|
+
warnings,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async function deleteByoFlow(
|
|
123
|
+
projectName: string,
|
|
124
|
+
projectDir: string,
|
|
125
|
+
exportDb: boolean,
|
|
126
|
+
options: DeleteProjectOptions | undefined,
|
|
127
|
+
warnings: string[],
|
|
128
|
+
): Promise<DeleteProjectResult> {
|
|
129
|
+
let databaseName: string | null = null;
|
|
130
|
+
let databaseExportPath: string | null = null;
|
|
131
|
+
let databaseDeleted = false;
|
|
132
|
+
let workerDeleted = false;
|
|
133
|
+
|
|
134
|
+
// Resolve DB name from wrangler.jsonc
|
|
135
|
+
try {
|
|
136
|
+
const resources = await parseWranglerResources(projectDir);
|
|
137
|
+
databaseName = resources.d1?.name ?? null;
|
|
138
|
+
} catch {
|
|
139
|
+
// Can't parse — continue without DB info
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Optional export before deletion
|
|
143
|
+
if (exportDb && databaseName) {
|
|
144
|
+
const exportDir = options?.exportDir ?? projectDir ?? join(getJackHome(), projectName);
|
|
145
|
+
mkdirSync(exportDir, { recursive: true });
|
|
146
|
+
const exportPath = join(exportDir, `${databaseName}-backup.sql`);
|
|
147
|
+
|
|
148
|
+
try {
|
|
149
|
+
await exportDatabase(databaseName, exportPath);
|
|
150
|
+
databaseExportPath = exportPath;
|
|
151
|
+
} catch (err) {
|
|
152
|
+
warnings.push(`Database export failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Delete worker
|
|
157
|
+
const workerExists = await checkWorkerExists(projectName);
|
|
158
|
+
if (workerExists) {
|
|
159
|
+
try {
|
|
160
|
+
await deleteWorker(projectName);
|
|
161
|
+
workerDeleted = true;
|
|
162
|
+
} catch (err) {
|
|
163
|
+
warnings.push(`Worker deletion failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
164
|
+
}
|
|
165
|
+
} else {
|
|
166
|
+
warnings.push(`Worker '${projectName}' not found — may already be deleted`);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Delete database
|
|
170
|
+
if (databaseName) {
|
|
171
|
+
try {
|
|
172
|
+
await deleteDatabase(databaseName);
|
|
173
|
+
databaseDeleted = true;
|
|
174
|
+
} catch (err) {
|
|
175
|
+
warnings.push(
|
|
176
|
+
`Database deletion failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return {
|
|
182
|
+
projectName,
|
|
183
|
+
deployMode: "byo",
|
|
184
|
+
workerDeleted,
|
|
185
|
+
databaseDeleted,
|
|
186
|
+
databaseName,
|
|
187
|
+
databaseExportPath,
|
|
188
|
+
warnings,
|
|
189
|
+
};
|
|
190
|
+
}
|