@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.
Files changed (90) 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/commands/update.ts +1 -0
  10. package/src/index.ts +8 -0
  11. package/src/lib/auth/client.ts +5 -2
  12. package/src/lib/binding-validator.ts +39 -3
  13. package/src/lib/build-helper.ts +18 -19
  14. package/src/lib/control-plane.ts +45 -0
  15. package/src/lib/do-config.ts +110 -0
  16. package/src/lib/do-export-validator.ts +26 -0
  17. package/src/lib/jsonc-edit.ts +292 -0
  18. package/src/lib/managed-deploy.ts +36 -1
  19. package/src/lib/project-link.ts +37 -0
  20. package/src/lib/project-operations.ts +31 -66
  21. package/src/lib/resources.ts +4 -5
  22. package/src/lib/schema.ts +8 -12
  23. package/src/lib/services/db-create.ts +2 -2
  24. package/src/lib/services/db-execute.ts +9 -6
  25. package/src/lib/services/db-list.ts +6 -4
  26. package/src/lib/services/endpoint-test.ts +275 -0
  27. package/src/lib/services/project-delete.ts +190 -0
  28. package/src/lib/services/project-environment.ts +579 -0
  29. package/src/lib/services/storage-config.ts +7 -309
  30. package/src/lib/services/storage-create.ts +2 -1
  31. package/src/lib/services/storage-delete.ts +3 -2
  32. package/src/lib/services/storage-info.ts +2 -1
  33. package/src/lib/services/storage-list.ts +6 -3
  34. package/src/lib/services/vectorize-config.ts +7 -264
  35. package/src/lib/services/vectorize-create.ts +2 -1
  36. package/src/lib/services/vectorize-delete.ts +6 -4
  37. package/src/lib/services/vectorize-list.ts +6 -3
  38. package/src/lib/storage/index.ts +21 -23
  39. package/src/lib/telemetry.ts +1 -0
  40. package/src/lib/wrangler-config.ts +43 -312
  41. package/src/lib/zip-packager.ts +28 -0
  42. package/src/mcp/test-utils.ts +31 -0
  43. package/src/mcp/tools/index.ts +280 -2
  44. package/src/templates/index.ts +5 -0
  45. package/src/templates/types.ts +4 -0
  46. package/templates/AI-BINDINGS.md +34 -76
  47. package/templates/CLAUDE.md +1 -1
  48. package/templates/ai-chat/src/index.ts +7 -14
  49. package/templates/ai-chat/src/jack-ai.ts +0 -6
  50. package/templates/chat/.jack.json +45 -0
  51. package/templates/chat/bun.lock +1584 -0
  52. package/templates/chat/components.json +23 -0
  53. package/templates/chat/index.html +12 -0
  54. package/templates/chat/package.json +41 -0
  55. package/templates/chat/src/chat-agent.ts +63 -0
  56. package/templates/chat/src/client/app.tsx +189 -0
  57. package/templates/chat/src/client/chat.tsx +222 -0
  58. package/templates/chat/src/client/components/prompt-kit/chat-container.tsx +47 -0
  59. package/templates/chat/src/client/components/prompt-kit/loader.tsx +33 -0
  60. package/templates/chat/src/client/components/prompt-kit/markdown.tsx +84 -0
  61. package/templates/chat/src/client/components/prompt-kit/message.tsx +54 -0
  62. package/templates/chat/src/client/components/prompt-kit/prompt-suggestion.tsx +20 -0
  63. package/templates/chat/src/client/components/prompt-kit/reasoning.tsx +134 -0
  64. package/templates/chat/src/client/components/prompt-kit/scroll-button.tsx +28 -0
  65. package/templates/chat/src/client/components/ui/button.tsx +38 -0
  66. package/templates/chat/src/client/lib/utils.ts +6 -0
  67. package/templates/chat/src/client/main.tsx +11 -0
  68. package/templates/chat/src/client/styles.css +125 -0
  69. package/templates/chat/src/index.ts +25 -0
  70. package/templates/chat/src/jack-ai.ts +94 -0
  71. package/templates/chat/tsconfig.json +18 -0
  72. package/templates/chat/vite.config.ts +14 -0
  73. package/templates/chat/wrangler.jsonc +18 -0
  74. package/templates/cron/.jack.json +18 -28
  75. package/templates/cron/schema.sql +10 -20
  76. package/templates/cron/src/admin.ts +321 -0
  77. package/templates/cron/src/index.ts +151 -81
  78. package/templates/cron/src/monitor.ts +124 -0
  79. package/templates/semantic-search/src/index.ts +5 -43
  80. package/templates/semantic-search/src/jack-ai.ts +0 -6
  81. package/templates/telegram-bot/.jack.json +56 -0
  82. package/templates/telegram-bot/bun.lock +41 -0
  83. package/templates/telegram-bot/package.json +16 -0
  84. package/templates/telegram-bot/src/index.ts +236 -0
  85. package/templates/telegram-bot/src/jack-ai.ts +100 -0
  86. package/templates/telegram-bot/tsconfig.json +11 -0
  87. package/templates/telegram-bot/wrangler.jsonc +8 -0
  88. package/templates/cron/src/jobs.ts +0 -139
  89. package/templates/cron/src/webhooks.ts +0 -95
  90. 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,35 +2010,11 @@ 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
- // Get database name on-demand
2016
+ // Get database name and deployment data
2039
2017
  let dbName: string | null = null;
2040
- if (link?.deploy_mode === "managed") {
2041
- // For managed projects, fetch from control plane
2042
- try {
2043
- const { fetchProjectResources } = await import("./control-plane.ts");
2044
- const resources = await fetchProjectResources(link.project_id);
2045
- const d1 = resources.find((r) => r.resource_type === "d1");
2046
- dbName = d1?.resource_name || null;
2047
- } catch {
2048
- // Ignore errors, dbName stays null
2049
- }
2050
- } else if (localExists) {
2051
- // For BYO, parse from wrangler config
2052
- try {
2053
- const { parseWranglerResources } = await import("./resources.ts");
2054
- const resources = await parseWranglerResources(resolvedPath);
2055
- dbName = resources.d1?.name || null;
2056
- } catch {
2057
- // Ignore errors, dbName stays null
2058
- }
2059
- }
2060
-
2061
- // Fetch real deployment data for managed projects
2062
2018
  let lastDeployAt: string | null = null;
2063
2019
  let deployCount = 0;
2064
2020
  let lastDeployStatus: string | null = null;
@@ -2066,11 +2022,14 @@ export async function getProjectStatus(
2066
2022
  let lastDeployMessage: string | null = null;
2067
2023
 
2068
2024
  if (link?.deploy_mode === "managed") {
2025
+ // Single overview call replaces fetchProjectResources + fetchDeployments
2069
2026
  try {
2070
- const { fetchDeployments } = await import("./control-plane.ts");
2071
- const result = await fetchDeployments(link.project_id);
2072
- deployCount = result.total;
2073
- const latest = result.deployments[0];
2027
+ const { fetchProjectOverview } = await import("./control-plane.ts");
2028
+ const overview = await fetchProjectOverview(link.project_id);
2029
+ const d1 = overview.resources.find((r) => r.resource_type === "d1");
2030
+ dbName = d1?.resource_name || null;
2031
+
2032
+ const latest = overview.latest_deployment;
2074
2033
  if (latest) {
2075
2034
  lastDeployAt = latest.created_at;
2076
2035
  lastDeployStatus = latest.status;
@@ -2078,7 +2037,16 @@ export async function getProjectStatus(
2078
2037
  lastDeployMessage = latest.message;
2079
2038
  }
2080
2039
  } catch {
2081
- // Silent fail — deploy tracking is supplementary
2040
+ // Silent fail — supplementary data
2041
+ }
2042
+ } else if (localExists) {
2043
+ // For BYO, parse from wrangler config
2044
+ try {
2045
+ const { parseWranglerResources } = await import("./resources.ts");
2046
+ const resources = await parseWranglerResources(resolvedPath);
2047
+ dbName = resources.d1?.name || null;
2048
+ } catch {
2049
+ // Ignore errors, dbName stays null
2082
2050
  }
2083
2051
  }
2084
2052
 
@@ -2140,12 +2108,9 @@ export async function scanStaleProjects(): Promise<StaleProjectScan> {
2140
2108
 
2141
2109
  for (const projectPath of paths) {
2142
2110
  // 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"));
2111
+ const hasConfig = hasWranglerConfig(projectPath);
2147
2112
 
2148
- if (!hasWranglerConfig) {
2113
+ if (!hasConfig) {
2149
2114
  // Type 1: No wrangler config at path (dir deleted/moved)
2150
2115
  const name = projectPath.split("/").pop() || projectId;
2151
2116
  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