@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
package/README.md CHANGED
@@ -27,7 +27,8 @@ But first: config files, deployment setup, secret management, debugging infrastr
27
27
  jack removes the friction between your idea and a live URL.
28
28
 
29
29
  ```bash
30
- bunx @getjack/jack new my-app # → deployed. live. done.
30
+ curl -fsSL docs.getjack.org/install.sh | bash
31
+ jack new my-app # → deployed. live. done.
31
32
  ```
32
33
 
33
34
  That's it. Write code. Ship again with `jack ship`. Stay in flow.
@@ -51,15 +52,14 @@ That's it. Write code. Ship again with `jack ship`. Stay in flow.
51
52
  ## Quick Start
52
53
 
53
54
  ```bash
54
- # One command to create and deploy
55
- bunx @getjack/jack new my-app
55
+ # Install (CLI + MCP for your AI editor)
56
+ curl -fsSL docs.getjack.org/install.sh | bash
56
57
 
57
- # Or install globally
58
- bun add -g @getjack/jack
58
+ # Create and deploy
59
59
  jack new my-app
60
60
  ```
61
61
 
62
- You'll need [Bun](https://bun.sh) and a Cloudflare account (free tier works).
62
+ Or try without installing: `npx -y @getjack/jack new my-app`
63
63
 
64
64
  ---
65
65
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@getjack/jack",
3
- "version": "0.1.34",
3
+ "version": "0.1.35",
4
4
  "description": "Ship before you forget why you started. The vibecoder's deployment CLI.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -16,6 +16,7 @@ import { unregisterPath } from "../lib/paths-index.ts";
16
16
  import { type LocalProjectLink, readProjectLink, unlinkProject } from "../lib/project-link.ts";
17
17
  import { resolveProject } from "../lib/project-resolver.ts";
18
18
  import { parseWranglerResources } from "../lib/resources.ts";
19
+ import { deleteProject } from "../lib/services/project-delete.ts";
19
20
  import { deleteCloudProject, getProjectNameFromDir } from "../lib/storage/index.ts";
20
21
 
21
22
  /**
@@ -124,10 +125,33 @@ export default async function down(projectName?: string, flags: DownFlags = {}):
124
125
  process.exit(1);
125
126
  }
126
127
 
127
- // Route to managed deletion flow
128
- const deleteSuccess = await managedDown({ projectId, runjackUrl, localPath }, name, flags);
129
- if (!deleteSuccess) {
130
- process.exit(0); // User cancelled
128
+ if (flags.force) {
129
+ // Force mode: full teardown via shared service
130
+ const projectDir = resolved?.localPath ?? process.cwd();
131
+ console.error("");
132
+ info(`Undeploying '${name}'`);
133
+ console.error("");
134
+
135
+ output.start("Undeploying from jack cloud...");
136
+ const result = await deleteProject(projectDir, { exportDatabase: false });
137
+ output.stop();
138
+
139
+ for (const w of result.warnings) {
140
+ warn(w);
141
+ }
142
+
143
+ console.error("");
144
+ success(`'${name}' undeployed`);
145
+ if (result.databaseDeleted) {
146
+ info("Database and all resources were deleted");
147
+ }
148
+ console.error("");
149
+ } else {
150
+ // Interactive mode: use managedDown with prompts
151
+ const deleteSuccess = await managedDown({ projectId, runjackUrl, localPath }, name, flags);
152
+ if (!deleteSuccess) {
153
+ process.exit(0); // User cancelled
154
+ }
131
155
  }
132
156
 
133
157
  // Clean up local tracking state (only if project has local path)
@@ -162,19 +186,27 @@ export default async function down(projectName?: string, flags: DownFlags = {}):
162
186
  return;
163
187
  }
164
188
 
165
- // Force mode - quick deletion without prompts
189
+ // Force mode - full teardown (worker + database) without prompts
166
190
  if (flags.force) {
167
191
  console.error("");
168
192
  info(`Undeploying '${name}'`);
169
193
  console.error("");
170
194
 
171
195
  output.start("Undeploying...");
172
- await deleteWorker(name);
196
+ const forceResult = await deleteProject(resolved?.localPath ?? process.cwd(), {
197
+ exportDatabase: false,
198
+ });
173
199
  output.stop();
174
200
 
201
+ for (const w of forceResult.warnings) {
202
+ warn(w);
203
+ }
204
+
175
205
  console.error("");
176
206
  success(`'${name}' undeployed`);
177
- info("Databases and backups were not affected");
207
+ if (forceResult.databaseDeleted && forceResult.databaseName) {
208
+ info(`Database '${forceResult.databaseName}' was also deleted`);
209
+ }
178
210
  console.error("");
179
211
  return;
180
212
  }
@@ -47,10 +47,8 @@ export default async function link(projectName?: string, flags: LinkFlags = {}):
47
47
  }
48
48
 
49
49
  // Check for wrangler config
50
- const hasWranglerConfig =
51
- existsSync("wrangler.jsonc") || existsSync("wrangler.json") || existsSync("wrangler.toml");
52
-
53
- if (!hasWranglerConfig) {
50
+ const { hasWranglerConfig } = await import("../lib/wrangler-config.ts");
51
+ if (!hasWranglerConfig(process.cwd())) {
54
52
  error("No wrangler config found");
55
53
  console.error("");
56
54
  info("Jack needs a wrangler.toml or wrangler.jsonc to deploy.");
@@ -80,10 +80,8 @@ export default async function logs(options: LogsOptions = {}): Promise<void> {
80
80
  }
81
81
 
82
82
  // BYOC requires a wrangler config in the working directory.
83
- const hasWranglerJson = existsSync("wrangler.jsonc") || existsSync("wrangler.json");
84
- const hasWranglerToml = existsSync("wrangler.toml");
85
-
86
- if (!hasWranglerJson && !hasWranglerToml) {
83
+ const { hasWranglerConfig } = await import("../lib/wrangler-config.ts");
84
+ if (!hasWranglerConfig(process.cwd())) {
87
85
  output.error("No wrangler config found");
88
86
  output.info("Run this from a jack project directory");
89
87
  process.exit(1);
@@ -126,18 +126,17 @@ async function parseWranglerBindings(cwd: string): Promise<{
126
126
  kv: string[];
127
127
  } | null> {
128
128
  try {
129
- const jsoncPath = join(cwd, "wrangler.jsonc");
130
- const tomlPath = join(cwd, "wrangler.toml");
129
+ const { findWranglerConfig } = await import("../lib/wrangler-config.ts");
130
+ const { parseJsonc } = await import("../lib/jsonc.ts");
131
+ const configPath = findWranglerConfig(cwd);
132
+ if (!configPath) return null;
133
+
134
+ const content = await readFile(configPath, "utf-8");
131
135
 
132
136
  let config: Record<string, unknown> | null = null;
133
137
 
134
- if (existsSync(jsoncPath)) {
135
- const content = await readFile(jsoncPath, "utf-8");
136
- const jsonContent = content.replace(/\/\*[\s\S]*?\*\//g, "").replace(/^\s*\/\/.*$/gm, "");
137
- config = JSON.parse(jsonContent);
138
- } else if (existsSync(tomlPath)) {
138
+ if (configPath.endsWith(".toml")) {
139
139
  // For toml, just check for key patterns — not worth a full parser here
140
- const content = await readFile(tomlPath, "utf-8");
141
140
  return {
142
141
  databases: content.includes("d1_databases") ? ["(see wrangler.toml)"] : [],
143
142
  buckets: content.includes("r2_buckets") ? ["(see wrangler.toml)"] : [],
@@ -147,6 +146,7 @@ async function parseWranglerBindings(cwd: string): Promise<{
147
146
  };
148
147
  }
149
148
 
149
+ config = parseJsonc<Record<string, unknown>>(content);
150
150
  if (!config) return null;
151
151
 
152
152
  const databases =
@@ -198,8 +198,10 @@ async function outputProjectContext(): Promise<void> {
198
198
 
199
199
  // --- Section 1: Project identity ---
200
200
  const lines = [`# Jack Project: ${name}`, ""];
201
- if (link.deploy_mode === "managed" && link.owner_username) {
202
- lines.push(`- **URL:** https://${link.owner_username}-${name}.runjack.xyz`);
201
+ if (link.deploy_mode === "managed") {
202
+ const { buildManagedUrl } = await import("../lib/project-link.ts");
203
+ const url = await buildManagedUrl(name, link.owner_username, cwd);
204
+ lines.push(`- **URL:** ${url}`);
203
205
  }
204
206
  lines.push(`- **Project ID:** ${link.project_id}`);
205
207
  lines.push(
@@ -517,8 +517,10 @@ async function dbDelete(options: ServiceOptions): Promise<void> {
517
517
  // Remove binding from wrangler.jsonc (both modes)
518
518
  // Note: We need to find the LOCAL database_name from wrangler.jsonc,
519
519
  // which may differ from the control plane's resource_name
520
- const { removeD1Binding, getExistingD1Bindings } = await import("../lib/wrangler-config.ts");
521
- const configPath = join(projectDir, "wrangler.jsonc");
520
+ const { removeD1Binding, getExistingD1Bindings, findWranglerConfig } = await import(
521
+ "../lib/wrangler-config.ts"
522
+ );
523
+ const configPath = findWranglerConfig(projectDir) ?? join(projectDir, "wrangler.jsonc");
522
524
 
523
525
  let bindingRemoved = false;
524
526
  try {
@@ -1,4 +1,3 @@
1
- import { existsSync } from "node:fs";
2
1
  import { getSyncConfig } from "../lib/config.ts";
3
2
  import { error, info, spinner, success, warn } from "../lib/output.ts";
4
3
  import { readProjectLink } from "../lib/project-link.ts";
@@ -10,17 +9,17 @@ export interface SyncFlags {
10
9
  force?: boolean;
11
10
  }
12
11
 
13
- function hasWranglerConfig(): boolean {
14
- return (
15
- existsSync("./wrangler.toml") || existsSync("./wrangler.jsonc") || existsSync("./wrangler.json")
16
- );
12
+ import { hasWranglerConfig } from "../lib/wrangler-config.ts";
13
+
14
+ function hasWranglerConfigInCwd(): boolean {
15
+ return hasWranglerConfig(process.cwd());
17
16
  }
18
17
 
19
18
  export default async function sync(flags: SyncFlags = {}): Promise<void> {
20
19
  const { verbose = false, dryRun = false, force = false } = flags;
21
20
 
22
21
  // Check for wrangler config
23
- if (!hasWranglerConfig()) {
22
+ if (!hasWranglerConfigInCwd()) {
24
23
  error("Not in a project directory");
25
24
  info("Run jack new <name> to create a project");
26
25
  process.exit(1);
@@ -110,9 +110,12 @@ export async function getValidAccessToken(): Promise<string | null> {
110
110
  }
111
111
 
112
112
  export async function authFetch(url: string, options: RequestInit = {}): Promise<Response> {
113
- const token = await getValidAccessToken();
113
+ let token = await getValidAccessToken();
114
114
  if (!token) {
115
- throw new Error("Not authenticated. Run 'jack login' or set JACK_API_TOKEN.");
115
+ // Auto-trigger login flow (works in both CLI and MCP contexts
116
+ // all output goes to stderr, interactive prompts auto-skip when not TTY)
117
+ const { requireAuthOrLogin } = await import("./guard.ts");
118
+ token = await requireAuthOrLogin();
116
119
  }
117
120
 
118
121
  return fetch(url, {
@@ -20,6 +20,7 @@ export const SUPPORTED_BINDINGS = [
20
20
  "r2_buckets",
21
21
  "kv_namespaces",
22
22
  "vectorize",
23
+ "durable_objects",
23
24
  ] as const;
24
25
 
25
26
  /**
@@ -27,7 +28,6 @@ export const SUPPORTED_BINDINGS = [
27
28
  * These will cause validation errors if present in wrangler config.
28
29
  */
29
30
  export const UNSUPPORTED_BINDINGS = [
30
- "durable_objects",
31
31
  "queues",
32
32
  "services",
33
33
  "hyperdrive",
@@ -39,7 +39,6 @@ export const UNSUPPORTED_BINDINGS = [
39
39
  * Human-readable names for unsupported bindings.
40
40
  */
41
41
  const BINDING_DISPLAY_NAMES: Record<string, string> = {
42
- durable_objects: "Durable Objects",
43
42
  queues: "Queues",
44
43
  services: "Service Bindings",
45
44
  hyperdrive: "Hyperdrive",
@@ -71,11 +70,48 @@ export function validateBindings(
71
70
  if (value !== undefined && value !== null) {
72
71
  const displayName = BINDING_DISPLAY_NAMES[binding] || binding;
73
72
  errors.push(
74
- `✗ ${displayName} not supported in managed deploy.\n Managed deploy supports: D1, AI, Assets, R2, KV, Vectorize, vars.\n Fix: Remove ${binding} from wrangler.jsonc, or use 'wrangler deploy' for full control.`,
73
+ `✗ ${displayName} not supported in managed deploy.\n Managed deploy supports: D1, AI, Assets, R2, KV, Vectorize, Durable Objects, vars.\n Fix: Remove ${binding} from wrangler.jsonc, or use 'wrangler deploy' for full control.`,
75
74
  );
76
75
  }
77
76
  }
78
77
 
78
+ // Validate Durable Object constraints
79
+ if (config.durable_objects?.bindings?.length) {
80
+ const doBindings = config.durable_objects.bindings;
81
+
82
+ // Max 3 DO classes per project (free tier)
83
+ if (doBindings.length > 3) {
84
+ errors.push(
85
+ `✗ Too many Durable Object classes (${doBindings.length}).\n Free tier allows max 3 DO classes per project.\n Fix: Remove unused DO classes from wrangler.jsonc.`,
86
+ );
87
+ }
88
+
89
+ // Only allow new_sqlite_classes (not legacy new_classes)
90
+ if (config.migrations?.length) {
91
+ for (const migration of config.migrations) {
92
+ if ((migration as Record<string, unknown>).new_classes) {
93
+ errors.push(
94
+ "✗ Only new_sqlite_classes migrations are supported.\n Fix: Replace new_classes with new_sqlite_classes in wrangler.jsonc.",
95
+ );
96
+ }
97
+ }
98
+ }
99
+
100
+ // Reject __JACK_ prefixed binding or class names
101
+ for (const dob of doBindings) {
102
+ if (dob.name.startsWith("__JACK_")) {
103
+ errors.push(
104
+ `✗ Binding name "${dob.name}" uses reserved __JACK_ prefix.\n Fix: Use a different binding name.`,
105
+ );
106
+ }
107
+ if (dob.class_name.startsWith("__JACK_")) {
108
+ errors.push(
109
+ `✗ Class name "${dob.class_name}" uses reserved __JACK_ prefix.\n Fix: Use a different class name.`,
110
+ );
111
+ }
112
+ }
113
+ }
114
+
79
115
  // Validate assets directory if configured
80
116
  const assetsValidation = validateAssetsDirectory(config, projectPath);
81
117
  if (!assetsValidation.valid) {
@@ -1,4 +1,4 @@
1
- import { existsSync, readFileSync } from "node:fs";
1
+ import { existsSync } from "node:fs";
2
2
  import { mkdir, readFile, readdir } from "node:fs/promises";
3
3
  import { tmpdir } from "node:os";
4
4
  import { basename, join } from "node:path";
@@ -6,19 +6,7 @@ import { $ } from "bun";
6
6
  import { JackError, JackErrorCode } from "./errors.ts";
7
7
  import { parseJsonc } from "./jsonc.ts";
8
8
  import type { OperationReporter } from "./project-operations.ts";
9
-
10
- /**
11
- * Get the wrangler config file path for a project
12
- */
13
- function getWranglerConfigPath(projectPath: string): string | null {
14
- const configs = ["wrangler.jsonc", "wrangler.toml", "wrangler.json"];
15
- for (const config of configs) {
16
- if (existsSync(join(projectPath, config))) {
17
- return config;
18
- }
19
- }
20
- return null;
21
- }
9
+ import { findWranglerConfig } from "./wrangler-config.ts";
22
10
 
23
11
  export interface BuildOutput {
24
12
  outDir: string;
@@ -64,8 +52,19 @@ export interface WranglerConfig {
64
52
  dimensions?: number;
65
53
  metric?: "cosine" | "euclidean" | "dot-product";
66
54
  }>;
55
+ durable_objects?: {
56
+ bindings: Array<{
57
+ name: string;
58
+ class_name: string;
59
+ }>;
60
+ };
61
+ migrations?: Array<{
62
+ tag: string;
63
+ new_sqlite_classes?: string[];
64
+ deleted_classes?: string[];
65
+ renamed_classes?: Array<{ from: string; to: string }>;
66
+ }>;
67
67
  // Unsupported bindings (for validation)
68
- durable_objects?: unknown;
69
68
  queues?: unknown;
70
69
  services?: unknown;
71
70
  hyperdrive?: unknown;
@@ -79,12 +78,12 @@ export interface WranglerConfig {
79
78
  * @returns Parsed wrangler configuration
80
79
  */
81
80
  export async function parseWranglerConfig(projectPath: string): Promise<WranglerConfig> {
82
- const wranglerPath = join(projectPath, "wrangler.jsonc");
81
+ const wranglerPath = findWranglerConfig(projectPath);
83
82
 
84
- if (!existsSync(wranglerPath)) {
83
+ if (!wranglerPath) {
85
84
  throw new JackError(
86
85
  JackErrorCode.VALIDATION_ERROR,
87
- "wrangler.jsonc not found",
86
+ "wrangler config not found",
88
87
  "Ensure your project has a wrangler.jsonc configuration file",
89
88
  );
90
89
  }
@@ -214,7 +213,7 @@ export async function buildProject(options: BuildOptions): Promise<BuildOutput>
214
213
  // Run wrangler dry-run to build without deploying
215
214
  reporter?.start("Bundling runtime...");
216
215
 
217
- const configFile = getWranglerConfigPath(projectPath);
216
+ const configFile = findWranglerConfig(projectPath);
218
217
  const configArg = configFile ? ["--config", configFile] : [];
219
218
  const dryRunResult = await $`wrangler deploy ${configArg} --dry-run --outdir=${outDir}`
220
219
  .cwd(projectPath)
@@ -528,6 +528,7 @@ export async function rollbackDeployment(
528
528
  return response.json() as Promise<RollbackResponse>;
529
529
  }
530
530
 
531
+
531
532
  /**
532
533
  * Create a resource for a managed project.
533
534
  * Uses POST /v1/projects/:id/resources/:type endpoint.
@@ -0,0 +1,110 @@
1
+ /**
2
+ * Durable Objects prerequisite auto-fix.
3
+ *
4
+ * Ensures wrangler.jsonc has nodejs_compat and migrations for all
5
+ * declared DO classes, modifying the file in place when needed.
6
+ */
7
+
8
+ import type { WranglerConfig } from "./build-helper.ts";
9
+ import { addSectionBeforeClosingBrace, findMatchingBracket } from "./jsonc-edit.ts";
10
+
11
+ /**
12
+ * Ensure `compatibility_flags` includes `"nodejs_compat"`.
13
+ * Adds the flag (or the entire section) if missing.
14
+ *
15
+ * @returns true if the file was modified
16
+ */
17
+ export async function ensureNodejsCompat(
18
+ configPath: string,
19
+ config: WranglerConfig,
20
+ ): Promise<boolean> {
21
+ const flags = config.compatibility_flags ?? [];
22
+ if (flags.includes("nodejs_compat")) return false;
23
+
24
+ let content = await Bun.file(configPath).text();
25
+
26
+ if (config.compatibility_flags) {
27
+ // Array exists but missing nodejs_compat — append to it
28
+ const match = content.match(/"compatibility_flags"\s*:\s*\[/);
29
+ if (!match || match.index === undefined) {
30
+ throw new Error("compatibility_flags exists in parsed config but not found in raw JSONC");
31
+ }
32
+
33
+ const arrayOpen = match.index + match[0].length;
34
+ const closingBracket = findMatchingBracket(content, arrayOpen - 1, "[", "]");
35
+ if (closingBracket === -1) {
36
+ throw new Error("Could not find closing bracket for compatibility_flags array");
37
+ }
38
+
39
+ const inner = content.slice(arrayOpen, closingBracket).trim();
40
+ const insertion = inner.length > 0 ? `, "nodejs_compat"` : `"nodejs_compat"`;
41
+
42
+ content = content.slice(0, closingBracket) + insertion + content.slice(closingBracket);
43
+ } else {
44
+ // No compatibility_flags at all — add the section
45
+ content = addSectionBeforeClosingBrace(content, `"compatibility_flags": ["nodejs_compat"]`);
46
+ }
47
+
48
+ await Bun.write(configPath, content);
49
+ return true;
50
+ }
51
+
52
+ /**
53
+ * Ensure every declared DO class has a corresponding migration entry
54
+ * with `new_sqlite_classes`. Only adds — never modifies existing migrations.
55
+ *
56
+ * @returns names of classes that were auto-migrated (empty = nothing done)
57
+ */
58
+ export async function ensureMigrations(
59
+ configPath: string,
60
+ config: WranglerConfig,
61
+ ): Promise<string[]> {
62
+ const bindings = config.durable_objects?.bindings;
63
+ if (!bindings?.length) return [];
64
+
65
+ const declaredClasses = bindings.map((b) => b.class_name);
66
+
67
+ // Collect classes already covered by existing migrations
68
+ const coveredClasses = new Set<string>();
69
+ if (config.migrations) {
70
+ for (const m of config.migrations) {
71
+ for (const c of m.new_sqlite_classes ?? []) coveredClasses.add(c);
72
+ }
73
+ }
74
+
75
+ const uncovered = declaredClasses.filter((c) => !coveredClasses.has(c));
76
+ if (uncovered.length === 0) return [];
77
+
78
+ let content = await Bun.file(configPath).text();
79
+
80
+ if (!config.migrations?.length) {
81
+ // No migrations section — create one
82
+ const migrationJson = JSON.stringify(
83
+ [{ tag: "v1", new_sqlite_classes: uncovered }],
84
+ null,
85
+ "\t\t",
86
+ ).replace(/\n/g, "\n\t");
87
+
88
+ content = addSectionBeforeClosingBrace(content, `"migrations": ${migrationJson}`);
89
+ } else {
90
+ // Migrations exist — append a new step
91
+ const match = content.match(/"migrations"\s*:\s*\[/);
92
+ if (!match || match.index === undefined) {
93
+ throw new Error("migrations exists in parsed config but not found in raw JSONC");
94
+ }
95
+
96
+ const arrayOpen = match.index + match[0].length;
97
+ const closingBracket = findMatchingBracket(content, arrayOpen - 1, "[", "]");
98
+ if (closingBracket === -1) {
99
+ throw new Error("Could not find closing bracket for migrations array");
100
+ }
101
+
102
+ const nextTag = `v${config.migrations.length + 1}`;
103
+ const stepJson = JSON.stringify({ tag: nextTag, new_sqlite_classes: uncovered });
104
+
105
+ content = `${content.slice(0, closingBracket)},\n\t\t${stepJson}${content.slice(closingBracket)}`;
106
+ }
107
+
108
+ await Bun.write(configPath, content);
109
+ return uncovered;
110
+ }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Validates that declared DO classes are actually exported from the built JS.
3
+ */
4
+
5
+ import { join } from "node:path";
6
+
7
+ /**
8
+ * Check built JS exports against declared DO class names.
9
+ *
10
+ * Uses Bun's transpiler to scan exports without executing the code.
11
+ *
12
+ * @returns class names NOT found in exports (empty = all good)
13
+ */
14
+ export async function validateDoExports(
15
+ outDir: string,
16
+ entrypoint: string,
17
+ classNames: string[],
18
+ ): Promise<string[]> {
19
+ const filePath = join(outDir, entrypoint);
20
+ const code = await Bun.file(filePath).text();
21
+
22
+ const transpiler = new Bun.Transpiler({ loader: "js" });
23
+ const { exports } = transpiler.scan(code);
24
+
25
+ return classNames.filter((name) => !exports.includes(name));
26
+ }