@getjack/jack 0.1.34 → 0.1.35

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.
Files changed (88) hide show
  1. package/README.md +6 -6
  2. package/package.json +1 -1
  3. package/src/commands/down.ts +39 -7
  4. package/src/commands/link.ts +2 -4
  5. package/src/commands/logs.ts +2 -4
  6. package/src/commands/mcp.ts +12 -10
  7. package/src/commands/services.ts +4 -2
  8. package/src/commands/sync.ts +5 -6
  9. package/src/lib/auth/client.ts +5 -2
  10. package/src/lib/binding-validator.ts +39 -3
  11. package/src/lib/build-helper.ts +18 -19
  12. package/src/lib/control-plane.ts +1 -0
  13. package/src/lib/do-config.ts +110 -0
  14. package/src/lib/do-export-validator.ts +26 -0
  15. package/src/lib/jsonc-edit.ts +292 -0
  16. package/src/lib/managed-deploy.ts +36 -1
  17. package/src/lib/project-link.ts +37 -0
  18. package/src/lib/project-operations.ts +13 -38
  19. package/src/lib/resources.ts +4 -5
  20. package/src/lib/schema.ts +8 -12
  21. package/src/lib/services/db-create.ts +2 -2
  22. package/src/lib/services/db-execute.ts +9 -6
  23. package/src/lib/services/db-list.ts +6 -4
  24. package/src/lib/services/endpoint-test.ts +275 -0
  25. package/src/lib/services/project-delete.ts +190 -0
  26. package/src/lib/services/project-environment.ts +457 -0
  27. package/src/lib/services/storage-config.ts +7 -309
  28. package/src/lib/services/storage-create.ts +2 -1
  29. package/src/lib/services/storage-delete.ts +3 -2
  30. package/src/lib/services/storage-info.ts +2 -1
  31. package/src/lib/services/storage-list.ts +6 -3
  32. package/src/lib/services/vectorize-config.ts +7 -264
  33. package/src/lib/services/vectorize-create.ts +2 -1
  34. package/src/lib/services/vectorize-delete.ts +6 -4
  35. package/src/lib/services/vectorize-list.ts +6 -3
  36. package/src/lib/storage/index.ts +21 -23
  37. package/src/lib/telemetry.ts +1 -0
  38. package/src/lib/wrangler-config.ts +43 -312
  39. package/src/lib/zip-packager.ts +28 -0
  40. package/src/mcp/test-utils.ts +31 -0
  41. package/src/mcp/tools/index.ts +271 -0
  42. package/src/templates/index.ts +5 -0
  43. package/src/templates/types.ts +4 -0
  44. package/templates/AI-BINDINGS.md +34 -76
  45. package/templates/CLAUDE.md +1 -1
  46. package/templates/ai-chat/src/index.ts +7 -14
  47. package/templates/ai-chat/src/jack-ai.ts +0 -6
  48. package/templates/chat/.jack.json +45 -0
  49. package/templates/chat/bun.lock +1588 -0
  50. package/templates/chat/components.json +23 -0
  51. package/templates/chat/index.html +12 -0
  52. package/templates/chat/package.json +41 -0
  53. package/templates/chat/src/chat-agent.ts +61 -0
  54. package/templates/chat/src/client/app.tsx +189 -0
  55. package/templates/chat/src/client/chat.tsx +222 -0
  56. package/templates/chat/src/client/components/prompt-kit/chat-container.tsx +47 -0
  57. package/templates/chat/src/client/components/prompt-kit/loader.tsx +33 -0
  58. package/templates/chat/src/client/components/prompt-kit/markdown.tsx +84 -0
  59. package/templates/chat/src/client/components/prompt-kit/message.tsx +54 -0
  60. package/templates/chat/src/client/components/prompt-kit/prompt-suggestion.tsx +20 -0
  61. package/templates/chat/src/client/components/prompt-kit/reasoning.tsx +134 -0
  62. package/templates/chat/src/client/components/prompt-kit/scroll-button.tsx +28 -0
  63. package/templates/chat/src/client/components/ui/button.tsx +38 -0
  64. package/templates/chat/src/client/lib/utils.ts +6 -0
  65. package/templates/chat/src/client/main.tsx +11 -0
  66. package/templates/chat/src/client/styles.css +125 -0
  67. package/templates/chat/src/index.ts +25 -0
  68. package/templates/chat/src/jack-ai.ts +94 -0
  69. package/templates/chat/tsconfig.json +18 -0
  70. package/templates/chat/vite.config.ts +14 -0
  71. package/templates/chat/wrangler.jsonc +18 -0
  72. package/templates/cron/.jack.json +18 -28
  73. package/templates/cron/schema.sql +10 -20
  74. package/templates/cron/src/admin.ts +321 -0
  75. package/templates/cron/src/index.ts +151 -81
  76. package/templates/cron/src/monitor.ts +124 -0
  77. package/templates/semantic-search/src/index.ts +5 -43
  78. package/templates/semantic-search/src/jack-ai.ts +0 -6
  79. package/templates/telegram-bot/.jack.json +56 -0
  80. package/templates/telegram-bot/bun.lock +41 -0
  81. package/templates/telegram-bot/package.json +16 -0
  82. package/templates/telegram-bot/src/index.ts +236 -0
  83. package/templates/telegram-bot/src/jack-ai.ts +100 -0
  84. package/templates/telegram-bot/tsconfig.json +11 -0
  85. package/templates/telegram-bot/wrangler.jsonc +8 -0
  86. package/templates/cron/src/jobs.ts +0 -139
  87. package/templates/cron/src/webhooks.ts +0 -95
  88. package/templates/semantic-search/src/jack-vectorize.ts +0 -169
@@ -0,0 +1,292 @@
1
+ /**
2
+ * Shared JSONC text-manipulation helpers.
3
+ *
4
+ * These operate on raw JSONC strings so that comments and formatting
5
+ * are preserved when adding/removing sections.
6
+ */
7
+
8
+ /**
9
+ * Find the matching closing bracket/brace for an opening one,
10
+ * respecting strings and comments.
11
+ */
12
+ export function findMatchingBracket(
13
+ content: string,
14
+ startIndex: number,
15
+ openChar: string,
16
+ closeChar: string,
17
+ ): number {
18
+ let depth = 0;
19
+ let inString = false;
20
+ let stringChar = "";
21
+ let escaped = false;
22
+ let inLineComment = false;
23
+ let inBlockComment = false;
24
+
25
+ for (let i = startIndex; i < content.length; i++) {
26
+ const char = content[i] ?? "";
27
+ const next = content[i + 1] ?? "";
28
+
29
+ if (inLineComment) {
30
+ if (char === "\n") inLineComment = false;
31
+ continue;
32
+ }
33
+
34
+ if (inBlockComment) {
35
+ if (char === "*" && next === "/") {
36
+ inBlockComment = false;
37
+ i++;
38
+ }
39
+ continue;
40
+ }
41
+
42
+ if (inString) {
43
+ if (escaped) {
44
+ escaped = false;
45
+ continue;
46
+ }
47
+ if (char === "\\") {
48
+ escaped = true;
49
+ continue;
50
+ }
51
+ if (char === stringChar) {
52
+ inString = false;
53
+ stringChar = "";
54
+ }
55
+ continue;
56
+ }
57
+
58
+ if (char === "/" && next === "/") {
59
+ inLineComment = true;
60
+ i++;
61
+ continue;
62
+ }
63
+ if (char === "/" && next === "*") {
64
+ inBlockComment = true;
65
+ i++;
66
+ continue;
67
+ }
68
+
69
+ if (char === '"' || char === "'") {
70
+ inString = true;
71
+ stringChar = char;
72
+ continue;
73
+ }
74
+
75
+ if (char === openChar) {
76
+ depth++;
77
+ } else if (char === closeChar) {
78
+ depth--;
79
+ if (depth === 0) return i;
80
+ }
81
+ }
82
+
83
+ return -1;
84
+ }
85
+
86
+ /**
87
+ * Check if content is only whitespace and comments.
88
+ */
89
+ export function isOnlyCommentsAndWhitespace(content: string): boolean {
90
+ let inLineComment = false;
91
+ let inBlockComment = false;
92
+
93
+ for (let i = 0; i < content.length; i++) {
94
+ const char = content[i] ?? "";
95
+ const next = content[i + 1] ?? "";
96
+
97
+ if (inLineComment) {
98
+ if (char === "\n") inLineComment = false;
99
+ continue;
100
+ }
101
+
102
+ if (inBlockComment) {
103
+ if (char === "*" && next === "/") {
104
+ inBlockComment = false;
105
+ i++;
106
+ }
107
+ continue;
108
+ }
109
+
110
+ if (char === "/" && next === "/") {
111
+ inLineComment = true;
112
+ i++;
113
+ continue;
114
+ }
115
+
116
+ if (char === "/" && next === "*") {
117
+ inBlockComment = true;
118
+ i++;
119
+ continue;
120
+ }
121
+
122
+ if (!/\s/.test(char)) return false;
123
+ }
124
+
125
+ return true;
126
+ }
127
+
128
+ /**
129
+ * Find the last closing brace of an object within an array range.
130
+ */
131
+ export function findLastObjectEndInArray(
132
+ content: string,
133
+ startIndex: number,
134
+ endIndex: number,
135
+ ): number {
136
+ let lastBraceIndex = -1;
137
+ let inString = false;
138
+ let stringChar = "";
139
+ let escaped = false;
140
+ let inLineComment = false;
141
+ let inBlockComment = false;
142
+
143
+ for (let i = startIndex; i < endIndex; i++) {
144
+ const char = content[i] ?? "";
145
+ const next = content[i + 1] ?? "";
146
+
147
+ if (inLineComment) {
148
+ if (char === "\n") inLineComment = false;
149
+ continue;
150
+ }
151
+
152
+ if (inBlockComment) {
153
+ if (char === "*" && next === "/") {
154
+ inBlockComment = false;
155
+ i++;
156
+ }
157
+ continue;
158
+ }
159
+
160
+ if (inString) {
161
+ if (escaped) {
162
+ escaped = false;
163
+ continue;
164
+ }
165
+ if (char === "\\") {
166
+ escaped = true;
167
+ continue;
168
+ }
169
+ if (char === stringChar) {
170
+ inString = false;
171
+ stringChar = "";
172
+ }
173
+ continue;
174
+ }
175
+
176
+ if (char === "/" && next === "/") {
177
+ inLineComment = true;
178
+ i++;
179
+ continue;
180
+ }
181
+
182
+ if (char === "/" && next === "*") {
183
+ inBlockComment = true;
184
+ i++;
185
+ continue;
186
+ }
187
+
188
+ if (char === '"' || char === "'") {
189
+ inString = true;
190
+ stringChar = char;
191
+ continue;
192
+ }
193
+
194
+ if (char === "}") lastBraceIndex = i;
195
+ }
196
+
197
+ return lastBraceIndex;
198
+ }
199
+
200
+ /**
201
+ * Find the start of a line comment (//) in a string, respecting strings.
202
+ */
203
+ export function findLineCommentStart(line: string): number {
204
+ let inString = false;
205
+ let stringChar = "";
206
+ let escaped = false;
207
+
208
+ for (let i = 0; i < line.length - 1; i++) {
209
+ const char = line[i] ?? "";
210
+ const next = line[i + 1] ?? "";
211
+
212
+ if (inString) {
213
+ if (escaped) {
214
+ escaped = false;
215
+ continue;
216
+ }
217
+ if (char === "\\") {
218
+ escaped = true;
219
+ continue;
220
+ }
221
+ if (char === stringChar) {
222
+ inString = false;
223
+ stringChar = "";
224
+ }
225
+ continue;
226
+ }
227
+
228
+ if (char === '"' || char === "'") {
229
+ inString = true;
230
+ stringChar = char;
231
+ continue;
232
+ }
233
+
234
+ if (char === "/" && next === "/") return i;
235
+ }
236
+
237
+ return -1;
238
+ }
239
+
240
+ /**
241
+ * Determine if we need to add a comma before new content.
242
+ * Looks at the last non-whitespace, non-comment character.
243
+ */
244
+ export function shouldAddCommaBefore(content: string): boolean {
245
+ let i = content.length - 1;
246
+
247
+ // First pass: find where any trailing line comment starts
248
+ for (let j = content.length - 1; j >= 0; j--) {
249
+ if (content[j] === "\n") {
250
+ const lineStart = content.lastIndexOf("\n", j - 1) + 1;
251
+ const line = content.slice(lineStart, j);
252
+ const commentIndex = findLineCommentStart(line);
253
+ if (commentIndex !== -1) {
254
+ i = lineStart + commentIndex - 1;
255
+ }
256
+ break;
257
+ }
258
+ }
259
+
260
+ // Skip whitespace
261
+ while (i >= 0 && /\s/.test(content[i] ?? "")) {
262
+ i--;
263
+ }
264
+
265
+ if (i < 0) return false;
266
+
267
+ const lastChar = content[i];
268
+ return lastChar !== "{" && lastChar !== "[" && lastChar !== ",";
269
+ }
270
+
271
+ /**
272
+ * Add a new top-level section (key + value) to a JSONC file before the
273
+ * final closing brace, preserving existing comments and formatting.
274
+ *
275
+ * @param content Raw JSONC file content
276
+ * @param sectionJson The `"key": value` string to insert (no trailing comma)
277
+ * @returns Updated file content
278
+ */
279
+ export function addSectionBeforeClosingBrace(content: string, sectionJson: string): string {
280
+ const lastBraceIndex = content.lastIndexOf("}");
281
+ if (lastBraceIndex === -1) {
282
+ throw new Error("Invalid JSON: no closing brace found");
283
+ }
284
+
285
+ const beforeBrace = content.slice(0, lastBraceIndex);
286
+ const needsComma = shouldAddCommaBefore(beforeBrace);
287
+ const trimmedBefore = beforeBrace.trimEnd();
288
+
289
+ const insertion = needsComma ? `,\n\t${sectionJson}` : `\n\t${sectionJson}`;
290
+
291
+ return `${trimmedBefore + insertion}\n${content.slice(lastBraceIndex)}`;
292
+ }
@@ -5,17 +5,21 @@
5
5
  */
6
6
 
7
7
  import { stat } from "node:fs/promises";
8
+ import { join } from "node:path";
8
9
  import { validateBindings } from "./binding-validator.ts";
9
10
  import { buildProject, parseWranglerConfig } from "./build-helper.ts";
10
11
  import { createManagedProject, syncProjectTags } from "./control-plane.ts";
11
12
  import { debug } from "./debug.ts";
12
13
  import { uploadDeployment } from "./deploy-upload.ts";
14
+ import { ensureMigrations, ensureNodejsCompat } from "./do-config.ts";
15
+ import { validateDoExports } from "./do-export-validator.ts";
13
16
  import { JackError, JackErrorCode } from "./errors.ts";
14
17
  import { formatSize } from "./format.ts";
15
18
  import { createFileCountProgress, createUploadProgress } from "./progress.ts";
16
19
  import type { OperationReporter } from "./project-operations.ts";
17
20
  import { getProjectTags } from "./tags.ts";
18
21
  import { Events, track, trackActivationIfFirst } from "./telemetry.ts";
22
+ import { findWranglerConfig } from "./wrangler-config.ts";
19
23
  import { packageForDeploy } from "./zip-packager.ts";
20
24
 
21
25
  export interface ManagedCreateResult {
@@ -101,12 +105,43 @@ export async function deployCodeToManagedProject(
101
105
  let pkg: Awaited<ReturnType<typeof packageForDeploy>> | null = null;
102
106
 
103
107
  try {
104
- const config = await parseWranglerConfig(projectPath);
108
+ let config = await parseWranglerConfig(projectPath);
105
109
 
106
110
  // Step 1: Build the project (must happen before validation, as build creates dist/)
107
111
  reporter?.start("Building project...");
108
112
  const buildOutput = await buildProject({ projectPath, reporter });
109
113
 
114
+ // Step 1.5: Auto-fix DO prerequisites (after build so we have output to validate)
115
+ if (config.durable_objects?.bindings?.length) {
116
+ const configPath = findWranglerConfig(projectPath) ?? join(projectPath, "wrangler.jsonc");
117
+ const fixes: string[] = [];
118
+
119
+ const addedCompat = await ensureNodejsCompat(configPath, config);
120
+ if (addedCompat) fixes.push("nodejs_compat");
121
+
122
+ const migratedClasses = await ensureMigrations(configPath, config);
123
+ if (migratedClasses.length > 0) fixes.push(`migrations for ${migratedClasses.join(", ")}`);
124
+
125
+ if (fixes.length > 0) {
126
+ config = await parseWranglerConfig(projectPath);
127
+ reporter?.success(`Auto-configured: ${fixes.join(", ")}`);
128
+ }
129
+
130
+ // Validate DO class exports
131
+ const missing = await validateDoExports(
132
+ buildOutput.outDir,
133
+ buildOutput.entrypoint,
134
+ config.durable_objects.bindings.map((b) => b.class_name),
135
+ );
136
+ if (missing.length > 0) {
137
+ throw new JackError(
138
+ JackErrorCode.VALIDATION_ERROR,
139
+ `Durable Object class${missing.length > 1 ? "es" : ""} not exported: ${missing.join(", ")}`,
140
+ missing.map((c) => `Add "export" before "class ${c}" in your source code`).join("\n"),
141
+ );
142
+ }
143
+ }
144
+
110
145
  // Step 2: Validate bindings are supported (after build, so assets dir exists)
111
146
  const validation = validateBindings(config, projectPath);
112
147
  if (!validation.valid) {
@@ -86,6 +86,43 @@ export function generateByoProjectId(): string {
86
86
  return `byo_${uuid}`;
87
87
  }
88
88
 
89
+ /**
90
+ * Build the managed project URL for a given slug.
91
+ *
92
+ * If the link has owner_username, uses it directly. Otherwise fetches it
93
+ * from the control plane and backfills the local link for future calls.
94
+ *
95
+ * @param slug - Project slug (e.g. "cozy-paws-relate")
96
+ * @param ownerUsername - Owner username if already known
97
+ * @param projectDir - Project directory (for backfilling the link). If omitted, no backfill.
98
+ */
99
+ export async function buildManagedUrl(
100
+ slug: string,
101
+ ownerUsername?: string | null,
102
+ projectDir?: string,
103
+ ): Promise<string> {
104
+ if (ownerUsername) {
105
+ return `https://${ownerUsername}-${slug}.runjack.xyz`;
106
+ }
107
+
108
+ // Fetch from control plane to resolve owner_username
109
+ try {
110
+ const { findProjectBySlug } = await import("./control-plane.ts");
111
+ const project = await findProjectBySlug(slug);
112
+ if (project?.owner_username) {
113
+ // Backfill local link so subsequent calls are fast
114
+ if (projectDir) {
115
+ updateProjectLink(projectDir, { owner_username: project.owner_username }).catch(() => {});
116
+ }
117
+ return `https://${project.owner_username}-${slug}.runjack.xyz`;
118
+ }
119
+ } catch {
120
+ // Control plane unavailable, fall through
121
+ }
122
+
123
+ return `https://${slug}.runjack.xyz`;
124
+ }
125
+
89
126
  /**
90
127
  * Link a local directory to a control plane project (managed)
91
128
  * or create a local-only link (BYO with provided/generated ID)
@@ -60,6 +60,7 @@ import { detectProjectType, validateProject } from "./project-detection.ts";
60
60
  import {
61
61
  type DeployMode,
62
62
  type TemplateMetadata as TemplateOrigin,
63
+ buildManagedUrl,
63
64
  generateByoProjectId,
64
65
  linkProject,
65
66
  readProjectLink,
@@ -71,6 +72,7 @@ import { applySchema, getD1Bindings, getD1DatabaseName, hasD1Config } from "./sc
71
72
  import { getSavedSecrets, saveSecrets } from "./secrets.ts";
72
73
  import { getProjectNameFromDir, getRemoteManifest } from "./storage/index.ts";
73
74
  import { Events, track, trackActivationIfFirst } from "./telemetry.ts";
75
+ import { findWranglerConfig, hasWranglerConfig } from "./wrangler-config.ts";
74
76
 
75
77
  // ============================================================================
76
78
  // Type Definitions
@@ -304,20 +306,6 @@ async function promptEnvVars(
304
306
  return result;
305
307
  }
306
308
 
307
- /**
308
- * Get the wrangler config file path for a project
309
- * Returns the first found: wrangler.jsonc, wrangler.toml, wrangler.json
310
- */
311
- function getWranglerConfigPath(projectPath: string): string | null {
312
- const configs = ["wrangler.jsonc", "wrangler.toml", "wrangler.json"];
313
- for (const config of configs) {
314
- if (existsSync(join(projectPath, config))) {
315
- return config;
316
- }
317
- }
318
- return null;
319
- }
320
-
321
309
  /**
322
310
  * Run wrangler deploy with explicit config to avoid parent directory conflicts
323
311
  */
@@ -325,7 +313,7 @@ async function runWranglerDeploy(
325
313
  projectPath: string,
326
314
  options: { dryRun?: boolean; outDir?: string } = {},
327
315
  ) {
328
- const configFile = getWranglerConfigPath(projectPath);
316
+ const configFile = findWranglerConfig(projectPath);
329
317
  const configArg = configFile ? ["--config", configFile] : [];
330
318
  const dryRunArgs = options.dryRun
331
319
  ? ["--dry-run", ...(options.outDir ? ["--outdir", options.outDir] : [])]
@@ -1606,25 +1594,22 @@ export async function deployProject(options: DeployOptions = {}): Promise<Deploy
1606
1594
  const interactive = interactiveOption ?? !isCi;
1607
1595
 
1608
1596
  // Check for wrangler config
1609
- const hasWranglerConfig =
1610
- existsSync(join(projectPath, "wrangler.toml")) ||
1611
- existsSync(join(projectPath, "wrangler.jsonc")) ||
1612
- existsSync(join(projectPath, "wrangler.json"));
1597
+ const hasConfig = hasWranglerConfig(projectPath);
1613
1598
 
1614
1599
  // Check for existing project link
1615
1600
  const hasProjectLink = existsSync(join(projectPath, ".jack", "project.json"));
1616
1601
 
1617
1602
  // Auto-detect flow: no wrangler config and no project link
1618
1603
  let autoDetectResult: AutoDetectResult | null = null;
1619
- if (!hasWranglerConfig && !hasProjectLink) {
1604
+ if (!hasConfig && !hasProjectLink) {
1620
1605
  autoDetectResult = await runAutoDetectFlow(projectPath, reporter, interactive, dryRun);
1621
- } else if (!hasWranglerConfig) {
1606
+ } else if (!hasConfig) {
1622
1607
  throw new JackError(
1623
1608
  JackErrorCode.PROJECT_NOT_FOUND,
1624
1609
  "No wrangler config found in current directory",
1625
1610
  "Run: jack new <project-name>",
1626
1611
  );
1627
- } else if (hasWranglerConfig && !hasProjectLink) {
1612
+ } else if (hasConfig && !hasProjectLink) {
1628
1613
  // Orphaned state: wrangler config exists but no project link
1629
1614
  // This happens when: linking failed during jack new, user has existing wrangler project,
1630
1615
  // or project was moved/copied without .jack directory
@@ -1814,9 +1799,7 @@ export async function deployProject(options: DeployOptions = {}): Promise<Deploy
1814
1799
  managedDeployResult = await deployToManagedProject(managedProjectId as string, projectPath, reporter, options.message);
1815
1800
 
1816
1801
  // Construct URL with username if available
1817
- workerUrl = link?.owner_username
1818
- ? `https://${link.owner_username}-${projectName}.runjack.xyz`
1819
- : `https://${projectName}.runjack.xyz`;
1802
+ workerUrl = await buildManagedUrl(projectName, link?.owner_username, projectPath);
1820
1803
  } else {
1821
1804
  // BYO mode: deploy via wrangler
1822
1805
 
@@ -2006,11 +1989,8 @@ export async function getProjectStatus(
2006
1989
  const link = await readProjectLink(resolvedPath);
2007
1990
 
2008
1991
  // Check if local project exists at the resolved path
2009
- const hasWranglerConfig =
2010
- existsSync(join(resolvedPath, "wrangler.jsonc")) ||
2011
- existsSync(join(resolvedPath, "wrangler.toml")) ||
2012
- existsSync(join(resolvedPath, "wrangler.json"));
2013
- const localExists = hasWranglerConfig;
1992
+ const hasConfig = hasWranglerConfig(resolvedPath);
1993
+ const localExists = hasConfig;
2014
1994
  const localPath = localExists ? resolvedPath : null;
2015
1995
 
2016
1996
  // If no link and no local project, return null
@@ -2030,9 +2010,7 @@ export async function getProjectStatus(
2030
2010
  // Determine URL based on mode
2031
2011
  let workerUrl: string | null = null;
2032
2012
  if (link?.deploy_mode === "managed") {
2033
- workerUrl = link.owner_username
2034
- ? `https://${link.owner_username}-${projectName}.runjack.xyz`
2035
- : `https://${projectName}.runjack.xyz`;
2013
+ workerUrl = await buildManagedUrl(projectName, link.owner_username, resolvedPath);
2036
2014
  }
2037
2015
 
2038
2016
  // Get database name on-demand
@@ -2140,12 +2118,9 @@ export async function scanStaleProjects(): Promise<StaleProjectScan> {
2140
2118
 
2141
2119
  for (const projectPath of paths) {
2142
2120
  // Check if path exists and has valid wrangler config
2143
- const hasWranglerConfig =
2144
- existsSync(join(projectPath, "wrangler.jsonc")) ||
2145
- existsSync(join(projectPath, "wrangler.toml")) ||
2146
- existsSync(join(projectPath, "wrangler.json"));
2121
+ const hasConfig = hasWranglerConfig(projectPath);
2147
2122
 
2148
- if (!hasWranglerConfig) {
2123
+ if (!hasConfig) {
2149
2124
  // Type 1: No wrangler config at path (dir deleted/moved)
2150
2125
  const name = projectPath.split("/").pop() || projectId;
2151
2126
  stale.push({
@@ -5,9 +5,8 @@
5
5
  * or parsed from wrangler.jsonc (BYO).
6
6
  */
7
7
 
8
- import { existsSync } from "node:fs";
9
- import { join } from "node:path";
10
8
  import { parseJsonc } from "./jsonc.ts";
9
+ import { findWranglerConfig } from "./wrangler-config.ts";
11
10
 
12
11
  // Resource types matching control plane schema
13
12
  export type ResourceType =
@@ -88,10 +87,10 @@ export function convertControlPlaneResources(resources: ControlPlaneResource[]):
88
87
  * Returns a unified resource view.
89
88
  */
90
89
  export async function parseWranglerResources(projectPath: string): Promise<ResolvedResources> {
91
- const wranglerPath = join(projectPath, "wrangler.jsonc");
90
+ const wranglerPath = findWranglerConfig(projectPath);
92
91
 
93
- if (!existsSync(wranglerPath)) {
94
- return {};
92
+ if (!wranglerPath) {
93
+ return {} as ResolvedResources;
95
94
  }
96
95
 
97
96
  try {
package/src/lib/schema.ts CHANGED
@@ -4,6 +4,7 @@ import { $ } from "bun";
4
4
  import { debug } from "./debug.ts";
5
5
  import { parseJsonc } from "./jsonc.ts";
6
6
  import { output } from "./output.ts";
7
+ import { findWranglerConfig } from "./wrangler-config.ts";
7
8
 
8
9
  /**
9
10
  * Execute schema.sql on a D1 database after deploy
@@ -47,9 +48,9 @@ export async function applySchema(bindingOrDbName: string, projectDir: string):
47
48
  * Check if project has D1 database configured (has d1_databases in wrangler config)
48
49
  */
49
50
  export async function hasD1Config(projectDir: string): Promise<boolean> {
50
- const wranglerPath = join(projectDir, "wrangler.jsonc");
51
+ const wranglerPath = findWranglerConfig(projectDir);
51
52
 
52
- if (!existsSync(wranglerPath)) {
53
+ if (!wranglerPath) {
53
54
  return false;
54
55
  }
55
56
 
@@ -71,9 +72,9 @@ export interface D1Binding {
71
72
  * Read D1 bindings from wrangler.jsonc
72
73
  */
73
74
  export async function getD1Bindings(projectDir: string): Promise<D1Binding[]> {
74
- const wranglerPath = join(projectDir, "wrangler.jsonc");
75
+ const wranglerPath = findWranglerConfig(projectDir);
75
76
 
76
- if (!existsSync(wranglerPath)) {
77
+ if (!wranglerPath) {
77
78
  return [];
78
79
  }
79
80
 
@@ -91,20 +92,15 @@ export async function getD1Bindings(projectDir: string): Promise<D1Binding[]> {
91
92
  * Returns the database_name field which is needed for wrangler d1 execute
92
93
  */
93
94
  export async function getD1DatabaseName(projectDir: string): Promise<string | null> {
94
- const wranglerPath = join(projectDir, "wrangler.jsonc");
95
+ const wranglerPath = findWranglerConfig(projectDir);
95
96
 
96
- if (!existsSync(wranglerPath)) {
97
+ if (!wranglerPath) {
97
98
  return null;
98
99
  }
99
100
 
100
101
  try {
101
102
  const content = await Bun.file(wranglerPath).text();
102
- // Strip comments for parsing
103
- // Note: Only remove line comments at the start of a line to avoid breaking URLs
104
- const cleaned = content
105
- .replace(/\/\*[\s\S]*?\*\//g, "") // block comments
106
- .replace(/^\s*\/\/.*$/gm, ""); // line comments at start of line only
107
- const config = JSON.parse(cleaned);
103
+ const config = parseJsonc<{ d1_databases?: { database_name?: string }[] }>(content);
108
104
 
109
105
  return config.d1_databases?.[0]?.database_name || null;
110
106
  } catch {
@@ -9,7 +9,7 @@ import { $ } from "bun";
9
9
  import { createProjectResource } from "../control-plane.ts";
10
10
  import { readProjectLink } from "../project-link.ts";
11
11
  import { getProjectNameFromDir } from "../storage/index.ts";
12
- import { addD1Binding, getExistingD1Bindings } from "../wrangler-config.ts";
12
+ import { addD1Binding, findWranglerConfig, getExistingD1Bindings } from "../wrangler-config.ts";
13
13
 
14
14
  export interface CreateDatabaseOptions {
15
15
  name?: string;
@@ -135,7 +135,7 @@ export async function createDatabase(
135
135
  const projectName = await getProjectNameFromDir(projectDir);
136
136
 
137
137
  // Get existing D1 bindings to determine naming
138
- const wranglerPath = join(projectDir, "wrangler.jsonc");
138
+ const wranglerPath = findWranglerConfig(projectDir) ?? join(projectDir, "wrangler.jsonc");
139
139
  const existingBindings = await getExistingD1Bindings(wranglerPath);
140
140
  const existingCount = existingBindings.length;
141
141
 
@@ -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 { type D1BindingConfig, getExistingD1Bindings } from "../wrangler-config.ts";
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 = join(projectDir, "wrangler.jsonc");
104
+ const wranglerPath = findWranglerConfig(projectDir);
102
105
 
103
- if (!existsSync(wranglerPath)) {
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 = join(projectDir, "wrangler.jsonc");
125
+ const wranglerPath = findWranglerConfig(projectDir);
123
126
 
124
- if (!existsSync(wranglerPath)) {
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 = join(projectDir, "wrangler.jsonc");
27
+ const wranglerPath = findWranglerConfig(projectDir);
28
+ if (!wranglerPath) {
29
+ return [];
30
+ }
29
31
 
30
- // Get existing D1 bindings from wrangler.jsonc
32
+ // Get existing D1 bindings from wrangler config
31
33
  const bindings = await getExistingD1Bindings(wranglerPath);
32
34
 
33
35
  if (bindings.length === 0) {