@getjack/jack 0.1.26 → 0.1.28
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/logs.ts +74 -12
- package/src/commands/services.ts +255 -4
- package/src/index.ts +4 -1
- package/src/lib/control-plane.ts +39 -0
- package/src/lib/hooks.ts +4 -0
- package/src/lib/services/db-execute.ts +6 -3
- package/src/lib/services/storage-config.ts +669 -0
- package/src/lib/services/storage-create.ts +152 -0
- package/src/lib/services/storage-delete.ts +89 -0
- package/src/lib/services/storage-info.ts +105 -0
- package/src/lib/services/storage-list.ts +42 -0
- package/src/mcp/resources/index.ts +1 -1
- package/src/mcp/tools/index.ts +480 -0
|
@@ -0,0 +1,669 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* R2 storage binding configuration utilities for wrangler.jsonc
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { existsSync } from "node:fs";
|
|
6
|
+
import { parseJsonc } from "../jsonc.ts";
|
|
7
|
+
|
|
8
|
+
export interface R2BindingConfig {
|
|
9
|
+
binding: string; // e.g., "STORAGE" or "IMAGES"
|
|
10
|
+
bucket_name: string; // e.g., "my-app-storage"
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface WranglerConfig {
|
|
14
|
+
r2_buckets?: Array<{
|
|
15
|
+
binding: string;
|
|
16
|
+
bucket_name?: string;
|
|
17
|
+
}>;
|
|
18
|
+
[key: string]: unknown;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Get existing R2 bindings from wrangler.jsonc
|
|
23
|
+
*/
|
|
24
|
+
export async function getExistingR2Bindings(configPath: string): Promise<R2BindingConfig[]> {
|
|
25
|
+
if (!existsSync(configPath)) {
|
|
26
|
+
throw new Error(`wrangler.jsonc not found at ${configPath}`);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const content = await Bun.file(configPath).text();
|
|
30
|
+
const config = parseJsonc<WranglerConfig>(content);
|
|
31
|
+
|
|
32
|
+
if (!config.r2_buckets || !Array.isArray(config.r2_buckets)) {
|
|
33
|
+
return [];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return config.r2_buckets
|
|
37
|
+
.filter((bucket) => bucket.binding && bucket.bucket_name)
|
|
38
|
+
.map((bucket) => ({
|
|
39
|
+
binding: bucket.binding,
|
|
40
|
+
bucket_name: bucket.bucket_name as string,
|
|
41
|
+
}));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Convert a bucket name to SCREAMING_SNAKE_CASE for the binding name.
|
|
46
|
+
* Special case: first bucket in a project gets "STORAGE" as the binding.
|
|
47
|
+
*/
|
|
48
|
+
export function toStorageBindingName(bucketName: string, isFirst: boolean): string {
|
|
49
|
+
if (isFirst) {
|
|
50
|
+
return "STORAGE";
|
|
51
|
+
}
|
|
52
|
+
// Convert kebab-case/snake_case to SCREAMING_SNAKE_CASE
|
|
53
|
+
return bucketName
|
|
54
|
+
.replace(/-/g, "_")
|
|
55
|
+
.replace(/[^a-zA-Z0-9_]/g, "")
|
|
56
|
+
.toUpperCase();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Generate a unique bucket name for a project.
|
|
61
|
+
* First bucket: {project}-storage
|
|
62
|
+
* Subsequent buckets: {project}-storage-{n}
|
|
63
|
+
*/
|
|
64
|
+
export function generateBucketName(projectName: string, existingCount: number): string {
|
|
65
|
+
if (existingCount === 0) {
|
|
66
|
+
return `${projectName}-storage`;
|
|
67
|
+
}
|
|
68
|
+
return `${projectName}-storage-${existingCount + 1}`;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Format an R2 binding as a JSON object string with proper indentation
|
|
73
|
+
*/
|
|
74
|
+
function formatR2BindingEntry(binding: R2BindingConfig): string {
|
|
75
|
+
return `{
|
|
76
|
+
"binding": "${binding.binding}",
|
|
77
|
+
"bucket_name": "${binding.bucket_name}"
|
|
78
|
+
}`;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Add an R2 bucket binding to wrangler.jsonc while preserving comments.
|
|
83
|
+
*
|
|
84
|
+
* Uses text manipulation to preserve comments rather than full JSON parsing.
|
|
85
|
+
*/
|
|
86
|
+
export async function addR2Binding(configPath: string, binding: R2BindingConfig): Promise<void> {
|
|
87
|
+
if (!existsSync(configPath)) {
|
|
88
|
+
throw new Error(
|
|
89
|
+
`wrangler.jsonc not found at ${configPath}. Create a wrangler.jsonc file first or run 'jack new' to create a new project.`,
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const content = await Bun.file(configPath).text();
|
|
94
|
+
|
|
95
|
+
// Parse to understand existing structure
|
|
96
|
+
const config = parseJsonc<WranglerConfig>(content);
|
|
97
|
+
|
|
98
|
+
// Format the new binding entry
|
|
99
|
+
const bindingJson = formatR2BindingEntry(binding);
|
|
100
|
+
|
|
101
|
+
let newContent: string;
|
|
102
|
+
|
|
103
|
+
if (config.r2_buckets && Array.isArray(config.r2_buckets)) {
|
|
104
|
+
// r2_buckets exists - append to the array
|
|
105
|
+
newContent = appendToR2BucketsArray(content, bindingJson);
|
|
106
|
+
} else {
|
|
107
|
+
// r2_buckets doesn't exist - add it before closing brace
|
|
108
|
+
newContent = addR2BucketsSection(content, bindingJson);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
await Bun.write(configPath, newContent);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Append a new entry to an existing r2_buckets array.
|
|
116
|
+
* Finds the closing bracket of the array and inserts before it.
|
|
117
|
+
*/
|
|
118
|
+
function appendToR2BucketsArray(content: string, bindingJson: string): string {
|
|
119
|
+
// Find "r2_buckets" and then find its closing bracket
|
|
120
|
+
const r2Match = content.match(/"r2_buckets"\s*:\s*\[/);
|
|
121
|
+
if (!r2Match || r2Match.index === undefined) {
|
|
122
|
+
throw new Error("Could not find r2_buckets array in config");
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const arrayStartIndex = r2Match.index + r2Match[0].length;
|
|
126
|
+
|
|
127
|
+
// Find the matching closing bracket, accounting for nested structures
|
|
128
|
+
const closingBracketIndex = findMatchingBracket(content, arrayStartIndex - 1, "[", "]");
|
|
129
|
+
if (closingBracketIndex === -1) {
|
|
130
|
+
throw new Error("Could not find closing bracket for r2_buckets array");
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Check if array is empty or has content
|
|
134
|
+
const arrayContent = content.slice(arrayStartIndex, closingBracketIndex).trim();
|
|
135
|
+
const isEmpty = arrayContent === "" || isOnlyCommentsAndWhitespace(arrayContent);
|
|
136
|
+
|
|
137
|
+
// Build the insertion
|
|
138
|
+
let insertion: string;
|
|
139
|
+
if (isEmpty) {
|
|
140
|
+
// Empty array - just add the entry
|
|
141
|
+
insertion = `\n\t\t${bindingJson}\n\t`;
|
|
142
|
+
} else {
|
|
143
|
+
// Has existing entries - add comma and new entry
|
|
144
|
+
insertion = `,\n\t\t${bindingJson}`;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Find position just before the closing bracket
|
|
148
|
+
const beforeBracket = content.slice(0, closingBracketIndex);
|
|
149
|
+
const afterBracket = content.slice(closingBracketIndex);
|
|
150
|
+
|
|
151
|
+
if (isEmpty) {
|
|
152
|
+
return beforeBracket + insertion + afterBracket;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// For non-empty arrays, find the last closing brace of an object in the array
|
|
156
|
+
const lastObjectEnd = findLastObjectEndInArray(content, arrayStartIndex, closingBracketIndex);
|
|
157
|
+
if (lastObjectEnd === -1) {
|
|
158
|
+
// Fallback: insert before closing bracket
|
|
159
|
+
return beforeBracket + insertion + afterBracket;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return content.slice(0, lastObjectEnd + 1) + insertion + content.slice(lastObjectEnd + 1);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Add a new r2_buckets section to the config.
|
|
167
|
+
* Inserts before the final closing brace.
|
|
168
|
+
*/
|
|
169
|
+
function addR2BucketsSection(content: string, bindingJson: string): string {
|
|
170
|
+
// Find the last closing brace in the file
|
|
171
|
+
const lastBraceIndex = content.lastIndexOf("}");
|
|
172
|
+
if (lastBraceIndex === -1) {
|
|
173
|
+
throw new Error("Invalid JSON: no closing brace found");
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Check what comes before the last brace to determine if we need a comma
|
|
177
|
+
const beforeBrace = content.slice(0, lastBraceIndex);
|
|
178
|
+
const needsComma = shouldAddCommaBefore(beforeBrace);
|
|
179
|
+
|
|
180
|
+
// Build the r2_buckets section
|
|
181
|
+
const r2Section = `"r2_buckets": [
|
|
182
|
+
${bindingJson}
|
|
183
|
+
]`;
|
|
184
|
+
|
|
185
|
+
// Find proper insertion point - look for last non-whitespace content
|
|
186
|
+
const trimmedBefore = beforeBrace.trimEnd();
|
|
187
|
+
|
|
188
|
+
let insertion: string;
|
|
189
|
+
if (needsComma) {
|
|
190
|
+
insertion = `,\n\t${r2Section}`;
|
|
191
|
+
} else {
|
|
192
|
+
insertion = `\n\t${r2Section}`;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Reconstruct: content before + insertion + newline + closing brace
|
|
196
|
+
return trimmedBefore + insertion + "\n" + content.slice(lastBraceIndex);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Find the matching closing bracket/brace for an opening one
|
|
201
|
+
*/
|
|
202
|
+
function findMatchingBracket(
|
|
203
|
+
content: string,
|
|
204
|
+
startIndex: number,
|
|
205
|
+
openChar: string,
|
|
206
|
+
closeChar: string,
|
|
207
|
+
): number {
|
|
208
|
+
let depth = 0;
|
|
209
|
+
let inString = false;
|
|
210
|
+
let stringChar = "";
|
|
211
|
+
let escaped = false;
|
|
212
|
+
let inLineComment = false;
|
|
213
|
+
let inBlockComment = false;
|
|
214
|
+
|
|
215
|
+
for (let i = startIndex; i < content.length; i++) {
|
|
216
|
+
const char = content[i] ?? "";
|
|
217
|
+
const next = content[i + 1] ?? "";
|
|
218
|
+
|
|
219
|
+
// Handle line comments
|
|
220
|
+
if (inLineComment) {
|
|
221
|
+
if (char === "\n") {
|
|
222
|
+
inLineComment = false;
|
|
223
|
+
}
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Handle block comments
|
|
228
|
+
if (inBlockComment) {
|
|
229
|
+
if (char === "*" && next === "/") {
|
|
230
|
+
inBlockComment = false;
|
|
231
|
+
i++;
|
|
232
|
+
}
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Handle strings
|
|
237
|
+
if (inString) {
|
|
238
|
+
if (escaped) {
|
|
239
|
+
escaped = false;
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
if (char === "\\") {
|
|
243
|
+
escaped = true;
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
if (char === stringChar) {
|
|
247
|
+
inString = false;
|
|
248
|
+
stringChar = "";
|
|
249
|
+
}
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Check for comment start
|
|
254
|
+
if (char === "/" && next === "/") {
|
|
255
|
+
inLineComment = true;
|
|
256
|
+
i++;
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
if (char === "/" && next === "*") {
|
|
260
|
+
inBlockComment = true;
|
|
261
|
+
i++;
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Check for string start
|
|
266
|
+
if (char === '"' || char === "'") {
|
|
267
|
+
inString = true;
|
|
268
|
+
stringChar = char;
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Track bracket depth
|
|
273
|
+
if (char === openChar) {
|
|
274
|
+
depth++;
|
|
275
|
+
} else if (char === closeChar) {
|
|
276
|
+
depth--;
|
|
277
|
+
if (depth === 0) {
|
|
278
|
+
return i;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
return -1;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Check if content is only whitespace and comments
|
|
288
|
+
*/
|
|
289
|
+
function isOnlyCommentsAndWhitespace(content: string): boolean {
|
|
290
|
+
let inLineComment = false;
|
|
291
|
+
let inBlockComment = false;
|
|
292
|
+
|
|
293
|
+
for (let i = 0; i < content.length; i++) {
|
|
294
|
+
const char = content[i] ?? "";
|
|
295
|
+
const next = content[i + 1] ?? "";
|
|
296
|
+
|
|
297
|
+
if (inLineComment) {
|
|
298
|
+
if (char === "\n") {
|
|
299
|
+
inLineComment = false;
|
|
300
|
+
}
|
|
301
|
+
continue;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
if (inBlockComment) {
|
|
305
|
+
if (char === "*" && next === "/") {
|
|
306
|
+
inBlockComment = false;
|
|
307
|
+
i++;
|
|
308
|
+
}
|
|
309
|
+
continue;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
if (char === "/" && next === "/") {
|
|
313
|
+
inLineComment = true;
|
|
314
|
+
i++;
|
|
315
|
+
continue;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
if (char === "/" && next === "*") {
|
|
319
|
+
inBlockComment = true;
|
|
320
|
+
i++;
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
if (!/\s/.test(char)) {
|
|
325
|
+
return false;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
return true;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Find the last closing brace of an object within an array range
|
|
334
|
+
*/
|
|
335
|
+
function findLastObjectEndInArray(content: string, startIndex: number, endIndex: number): number {
|
|
336
|
+
let lastBraceIndex = -1;
|
|
337
|
+
let inString = false;
|
|
338
|
+
let stringChar = "";
|
|
339
|
+
let escaped = false;
|
|
340
|
+
let inLineComment = false;
|
|
341
|
+
let inBlockComment = false;
|
|
342
|
+
|
|
343
|
+
for (let i = startIndex; i < endIndex; i++) {
|
|
344
|
+
const char = content[i] ?? "";
|
|
345
|
+
const next = content[i + 1] ?? "";
|
|
346
|
+
|
|
347
|
+
if (inLineComment) {
|
|
348
|
+
if (char === "\n") {
|
|
349
|
+
inLineComment = false;
|
|
350
|
+
}
|
|
351
|
+
continue;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
if (inBlockComment) {
|
|
355
|
+
if (char === "*" && next === "/") {
|
|
356
|
+
inBlockComment = false;
|
|
357
|
+
i++;
|
|
358
|
+
}
|
|
359
|
+
continue;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
if (inString) {
|
|
363
|
+
if (escaped) {
|
|
364
|
+
escaped = false;
|
|
365
|
+
continue;
|
|
366
|
+
}
|
|
367
|
+
if (char === "\\") {
|
|
368
|
+
escaped = true;
|
|
369
|
+
continue;
|
|
370
|
+
}
|
|
371
|
+
if (char === stringChar) {
|
|
372
|
+
inString = false;
|
|
373
|
+
stringChar = "";
|
|
374
|
+
}
|
|
375
|
+
continue;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
if (char === "/" && next === "/") {
|
|
379
|
+
inLineComment = true;
|
|
380
|
+
i++;
|
|
381
|
+
continue;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
if (char === "/" && next === "*") {
|
|
385
|
+
inBlockComment = true;
|
|
386
|
+
i++;
|
|
387
|
+
continue;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
if (char === '"' || char === "'") {
|
|
391
|
+
inString = true;
|
|
392
|
+
stringChar = char;
|
|
393
|
+
continue;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
if (char === "}") {
|
|
397
|
+
lastBraceIndex = i;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
return lastBraceIndex;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* Determine if we need to add a comma before new content.
|
|
406
|
+
* Looks at the last non-whitespace, non-comment character.
|
|
407
|
+
*/
|
|
408
|
+
function shouldAddCommaBefore(content: string): boolean {
|
|
409
|
+
// Strip trailing comments and whitespace to find last meaningful char
|
|
410
|
+
let i = content.length - 1;
|
|
411
|
+
|
|
412
|
+
// First pass: find where any trailing line comment starts
|
|
413
|
+
for (let j = content.length - 1; j >= 0; j--) {
|
|
414
|
+
if (content[j] === "\n") {
|
|
415
|
+
// Check if there's a // comment on this line
|
|
416
|
+
const lineStart = content.lastIndexOf("\n", j - 1) + 1;
|
|
417
|
+
const line = content.slice(lineStart, j);
|
|
418
|
+
const commentIndex = findLineCommentStart(line);
|
|
419
|
+
if (commentIndex !== -1) {
|
|
420
|
+
i = lineStart + commentIndex - 1;
|
|
421
|
+
}
|
|
422
|
+
break;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// Skip whitespace
|
|
427
|
+
while (i >= 0 && /\s/.test(content[i] ?? "")) {
|
|
428
|
+
i--;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
if (i < 0) return false;
|
|
432
|
+
|
|
433
|
+
const lastChar = content[i];
|
|
434
|
+
// Need comma if last char is }, ], ", number, or identifier char
|
|
435
|
+
// Don't need comma if last char is { or [ or ,
|
|
436
|
+
return lastChar !== "{" && lastChar !== "[" && lastChar !== ",";
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/**
|
|
440
|
+
* Find the start of a line comment (//) in a string, respecting strings
|
|
441
|
+
*/
|
|
442
|
+
function findLineCommentStart(line: string): number {
|
|
443
|
+
let inString = false;
|
|
444
|
+
let stringChar = "";
|
|
445
|
+
let escaped = false;
|
|
446
|
+
|
|
447
|
+
for (let i = 0; i < line.length - 1; i++) {
|
|
448
|
+
const char = line[i] ?? "";
|
|
449
|
+
const next = line[i + 1] ?? "";
|
|
450
|
+
|
|
451
|
+
if (inString) {
|
|
452
|
+
if (escaped) {
|
|
453
|
+
escaped = false;
|
|
454
|
+
continue;
|
|
455
|
+
}
|
|
456
|
+
if (char === "\\") {
|
|
457
|
+
escaped = true;
|
|
458
|
+
continue;
|
|
459
|
+
}
|
|
460
|
+
if (char === stringChar) {
|
|
461
|
+
inString = false;
|
|
462
|
+
stringChar = "";
|
|
463
|
+
}
|
|
464
|
+
continue;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
if (char === '"' || char === "'") {
|
|
468
|
+
inString = true;
|
|
469
|
+
stringChar = char;
|
|
470
|
+
continue;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
if (char === "/" && next === "/") {
|
|
474
|
+
return i;
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
return -1;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/**
|
|
482
|
+
* Remove an R2 bucket binding from wrangler.jsonc by bucket_name.
|
|
483
|
+
* Preserves comments and formatting.
|
|
484
|
+
*
|
|
485
|
+
* @returns true if binding was found and removed, false if not found
|
|
486
|
+
*/
|
|
487
|
+
export async function removeR2Binding(configPath: string, bucketName: string): Promise<boolean> {
|
|
488
|
+
if (!existsSync(configPath)) {
|
|
489
|
+
throw new Error(`wrangler.jsonc not found at ${configPath}. Cannot remove binding.`);
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
const content = await Bun.file(configPath).text();
|
|
493
|
+
|
|
494
|
+
// Parse to understand existing structure
|
|
495
|
+
const config = parseJsonc<WranglerConfig>(content);
|
|
496
|
+
|
|
497
|
+
// Check if r2_buckets exists and has entries
|
|
498
|
+
if (!config.r2_buckets || !Array.isArray(config.r2_buckets)) {
|
|
499
|
+
return false;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
// Find the binding to remove
|
|
503
|
+
const bindingIndex = config.r2_buckets.findIndex((bucket) => bucket.bucket_name === bucketName);
|
|
504
|
+
|
|
505
|
+
if (bindingIndex === -1) {
|
|
506
|
+
return false; // Binding not found
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
// Use text manipulation to remove the binding while preserving formatting
|
|
510
|
+
const newContent = removeR2BucketEntryFromContent(content, bucketName);
|
|
511
|
+
|
|
512
|
+
if (newContent === content) {
|
|
513
|
+
return false; // Nothing changed
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
await Bun.write(configPath, newContent);
|
|
517
|
+
return true;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
/**
|
|
521
|
+
* Remove a specific R2 bucket entry from the r2_buckets array in content.
|
|
522
|
+
* Handles comma placement and preserves comments.
|
|
523
|
+
*/
|
|
524
|
+
function removeR2BucketEntryFromContent(content: string, bucketName: string): string {
|
|
525
|
+
// Find the r2_buckets array
|
|
526
|
+
const r2Match = content.match(/"r2_buckets"\s*:\s*\[/);
|
|
527
|
+
if (!r2Match || r2Match.index === undefined) {
|
|
528
|
+
return content;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
const arrayStartIndex = r2Match.index + r2Match[0].length;
|
|
532
|
+
const closingBracketIndex = findMatchingBracket(content, arrayStartIndex - 1, "[", "]");
|
|
533
|
+
|
|
534
|
+
if (closingBracketIndex === -1) {
|
|
535
|
+
return content;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
const arrayContent = content.slice(arrayStartIndex, closingBracketIndex);
|
|
539
|
+
|
|
540
|
+
// Find the object containing this bucket_name
|
|
541
|
+
const escapedName = bucketName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
542
|
+
const bucketNamePattern = new RegExp(`"bucket_name"\\s*:\\s*"${escapedName}"`);
|
|
543
|
+
|
|
544
|
+
const match = bucketNamePattern.exec(arrayContent);
|
|
545
|
+
if (!match) {
|
|
546
|
+
return content;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
// Find the enclosing object boundaries
|
|
550
|
+
const matchPosInArray = match.index;
|
|
551
|
+
const objectStart = findObjectStartBefore(arrayContent, matchPosInArray);
|
|
552
|
+
const objectEnd = findObjectEndAfter(arrayContent, matchPosInArray);
|
|
553
|
+
|
|
554
|
+
if (objectStart === -1 || objectEnd === -1) {
|
|
555
|
+
return content;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
// Determine comma handling
|
|
559
|
+
let removeStart = objectStart;
|
|
560
|
+
let removeEnd = objectEnd + 1;
|
|
561
|
+
|
|
562
|
+
// Check for trailing comma after the object
|
|
563
|
+
const afterObject = arrayContent.slice(objectEnd + 1);
|
|
564
|
+
const trailingCommaMatch = afterObject.match(/^\s*,/);
|
|
565
|
+
|
|
566
|
+
// Check for leading comma before the object
|
|
567
|
+
const beforeObject = arrayContent.slice(0, objectStart);
|
|
568
|
+
const leadingCommaMatch = beforeObject.match(/,\s*$/);
|
|
569
|
+
|
|
570
|
+
if (trailingCommaMatch) {
|
|
571
|
+
// Remove trailing comma
|
|
572
|
+
removeEnd = objectEnd + 1 + trailingCommaMatch[0].length;
|
|
573
|
+
} else if (leadingCommaMatch) {
|
|
574
|
+
// Remove leading comma
|
|
575
|
+
removeStart = objectStart - leadingCommaMatch[0].length;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
// Build new array content
|
|
579
|
+
const newArrayContent = arrayContent.slice(0, removeStart) + arrayContent.slice(removeEnd);
|
|
580
|
+
|
|
581
|
+
// Check if array is now effectively empty (only whitespace/comments)
|
|
582
|
+
const trimmedArray = newArrayContent.replace(/\/\/[^\n]*/g, "").trim();
|
|
583
|
+
if (trimmedArray === "" || trimmedArray === "[]") {
|
|
584
|
+
// Remove the entire r2_buckets property
|
|
585
|
+
return removeR2BucketsProperty(content, r2Match.index, closingBracketIndex);
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
return content.slice(0, arrayStartIndex) + newArrayContent + content.slice(closingBracketIndex);
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
/**
|
|
592
|
+
* Find the start of the object (opening brace) before the given position.
|
|
593
|
+
*/
|
|
594
|
+
function findObjectStartBefore(content: string, fromPos: number): number {
|
|
595
|
+
let depth = 0;
|
|
596
|
+
for (let i = fromPos; i >= 0; i--) {
|
|
597
|
+
const char = content[i];
|
|
598
|
+
if (char === "}") depth++;
|
|
599
|
+
if (char === "{") {
|
|
600
|
+
if (depth === 0) return i;
|
|
601
|
+
depth--;
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
return -1;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
/**
|
|
608
|
+
* Find the end of the object (closing brace) after the given position.
|
|
609
|
+
*/
|
|
610
|
+
function findObjectEndAfter(content: string, fromPos: number): number {
|
|
611
|
+
let depth = 0;
|
|
612
|
+
let inString = false;
|
|
613
|
+
let escaped = false;
|
|
614
|
+
|
|
615
|
+
for (let i = fromPos; i < content.length; i++) {
|
|
616
|
+
const char = content[i];
|
|
617
|
+
|
|
618
|
+
if (inString) {
|
|
619
|
+
if (escaped) {
|
|
620
|
+
escaped = false;
|
|
621
|
+
} else if (char === "\\") {
|
|
622
|
+
escaped = true;
|
|
623
|
+
} else if (char === '"') {
|
|
624
|
+
inString = false;
|
|
625
|
+
}
|
|
626
|
+
continue;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
if (char === '"') {
|
|
630
|
+
inString = true;
|
|
631
|
+
continue;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
if (char === "{") depth++;
|
|
635
|
+
if (char === "}") {
|
|
636
|
+
if (depth === 0) return i;
|
|
637
|
+
depth--;
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
return -1;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
/**
|
|
644
|
+
* Remove the entire r2_buckets property when it becomes empty.
|
|
645
|
+
*/
|
|
646
|
+
function removeR2BucketsProperty(
|
|
647
|
+
content: string,
|
|
648
|
+
propertyStart: number,
|
|
649
|
+
arrayEnd: number,
|
|
650
|
+
): string {
|
|
651
|
+
let removeStart = propertyStart;
|
|
652
|
+
let removeEnd = arrayEnd + 1;
|
|
653
|
+
|
|
654
|
+
// Look backward for a comma to remove
|
|
655
|
+
const beforeProperty = content.slice(0, propertyStart);
|
|
656
|
+
const leadingCommaMatch = beforeProperty.match(/,\s*$/);
|
|
657
|
+
|
|
658
|
+
// Look forward for a trailing comma
|
|
659
|
+
const afterProperty = content.slice(arrayEnd + 1);
|
|
660
|
+
const trailingCommaMatch = afterProperty.match(/^\s*,/);
|
|
661
|
+
|
|
662
|
+
if (leadingCommaMatch) {
|
|
663
|
+
removeStart = propertyStart - leadingCommaMatch[0].length;
|
|
664
|
+
} else if (trailingCommaMatch) {
|
|
665
|
+
removeEnd = arrayEnd + 1 + trailingCommaMatch[0].length;
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
return content.slice(0, removeStart) + content.slice(removeEnd);
|
|
669
|
+
}
|