@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.
- package/README.md +6 -6
- package/package.json +1 -1
- package/src/commands/down.ts +39 -7
- package/src/commands/link.ts +2 -4
- package/src/commands/logs.ts +2 -4
- package/src/commands/mcp.ts +12 -10
- package/src/commands/services.ts +4 -2
- package/src/commands/sync.ts +5 -6
- package/src/lib/auth/client.ts +5 -2
- package/src/lib/binding-validator.ts +39 -3
- package/src/lib/build-helper.ts +18 -19
- package/src/lib/control-plane.ts +1 -0
- package/src/lib/do-config.ts +110 -0
- package/src/lib/do-export-validator.ts +26 -0
- package/src/lib/jsonc-edit.ts +292 -0
- package/src/lib/managed-deploy.ts +36 -1
- package/src/lib/project-link.ts +37 -0
- package/src/lib/project-operations.ts +13 -38
- package/src/lib/resources.ts +4 -5
- package/src/lib/schema.ts +8 -12
- package/src/lib/services/db-create.ts +2 -2
- package/src/lib/services/db-execute.ts +9 -6
- package/src/lib/services/db-list.ts +6 -4
- package/src/lib/services/endpoint-test.ts +275 -0
- package/src/lib/services/project-delete.ts +190 -0
- package/src/lib/services/project-environment.ts +457 -0
- package/src/lib/services/storage-config.ts +7 -309
- package/src/lib/services/storage-create.ts +2 -1
- package/src/lib/services/storage-delete.ts +3 -2
- package/src/lib/services/storage-info.ts +2 -1
- package/src/lib/services/storage-list.ts +6 -3
- package/src/lib/services/vectorize-config.ts +7 -264
- package/src/lib/services/vectorize-create.ts +2 -1
- package/src/lib/services/vectorize-delete.ts +6 -4
- package/src/lib/services/vectorize-list.ts +6 -3
- package/src/lib/storage/index.ts +21 -23
- package/src/lib/telemetry.ts +1 -0
- package/src/lib/wrangler-config.ts +43 -312
- package/src/lib/zip-packager.ts +28 -0
- package/src/mcp/test-utils.ts +31 -0
- package/src/mcp/tools/index.ts +271 -0
- package/src/templates/index.ts +5 -0
- package/src/templates/types.ts +4 -0
- package/templates/AI-BINDINGS.md +34 -76
- package/templates/CLAUDE.md +1 -1
- package/templates/ai-chat/src/index.ts +7 -14
- package/templates/ai-chat/src/jack-ai.ts +0 -6
- package/templates/chat/.jack.json +45 -0
- package/templates/chat/bun.lock +1588 -0
- package/templates/chat/components.json +23 -0
- package/templates/chat/index.html +12 -0
- package/templates/chat/package.json +41 -0
- package/templates/chat/src/chat-agent.ts +61 -0
- package/templates/chat/src/client/app.tsx +189 -0
- package/templates/chat/src/client/chat.tsx +222 -0
- package/templates/chat/src/client/components/prompt-kit/chat-container.tsx +47 -0
- package/templates/chat/src/client/components/prompt-kit/loader.tsx +33 -0
- package/templates/chat/src/client/components/prompt-kit/markdown.tsx +84 -0
- package/templates/chat/src/client/components/prompt-kit/message.tsx +54 -0
- package/templates/chat/src/client/components/prompt-kit/prompt-suggestion.tsx +20 -0
- package/templates/chat/src/client/components/prompt-kit/reasoning.tsx +134 -0
- package/templates/chat/src/client/components/prompt-kit/scroll-button.tsx +28 -0
- package/templates/chat/src/client/components/ui/button.tsx +38 -0
- package/templates/chat/src/client/lib/utils.ts +6 -0
- package/templates/chat/src/client/main.tsx +11 -0
- package/templates/chat/src/client/styles.css +125 -0
- package/templates/chat/src/index.ts +25 -0
- package/templates/chat/src/jack-ai.ts +94 -0
- package/templates/chat/tsconfig.json +18 -0
- package/templates/chat/vite.config.ts +14 -0
- package/templates/chat/wrangler.jsonc +18 -0
- package/templates/cron/.jack.json +18 -28
- package/templates/cron/schema.sql +10 -20
- package/templates/cron/src/admin.ts +321 -0
- package/templates/cron/src/index.ts +151 -81
- package/templates/cron/src/monitor.ts +124 -0
- package/templates/semantic-search/src/index.ts +5 -43
- package/templates/semantic-search/src/jack-ai.ts +0 -6
- package/templates/telegram-bot/.jack.json +56 -0
- package/templates/telegram-bot/bun.lock +41 -0
- package/templates/telegram-bot/package.json +16 -0
- package/templates/telegram-bot/src/index.ts +236 -0
- package/templates/telegram-bot/src/jack-ai.ts +100 -0
- package/templates/telegram-bot/tsconfig.json +11 -0
- package/templates/telegram-bot/wrangler.jsonc +8 -0
- package/templates/cron/src/jobs.ts +0 -139
- package/templates/cron/src/webhooks.ts +0 -95
- package/templates/semantic-search/src/jack-vectorize.ts +0 -169
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
|
-
|
|
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
|
-
#
|
|
55
|
-
|
|
55
|
+
# Install (CLI + MCP for your AI editor)
|
|
56
|
+
curl -fsSL docs.getjack.org/install.sh | bash
|
|
56
57
|
|
|
57
|
-
#
|
|
58
|
-
bun add -g @getjack/jack
|
|
58
|
+
# Create and deploy
|
|
59
59
|
jack new my-app
|
|
60
60
|
```
|
|
61
61
|
|
|
62
|
-
|
|
62
|
+
Or try without installing: `npx -y @getjack/jack new my-app`
|
|
63
63
|
|
|
64
64
|
---
|
|
65
65
|
|
package/package.json
CHANGED
package/src/commands/down.ts
CHANGED
|
@@ -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
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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 -
|
|
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
|
|
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
|
-
|
|
207
|
+
if (forceResult.databaseDeleted && forceResult.databaseName) {
|
|
208
|
+
info(`Database '${forceResult.databaseName}' was also deleted`);
|
|
209
|
+
}
|
|
178
210
|
console.error("");
|
|
179
211
|
return;
|
|
180
212
|
}
|
package/src/commands/link.ts
CHANGED
|
@@ -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
|
-
|
|
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.");
|
package/src/commands/logs.ts
CHANGED
|
@@ -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
|
|
84
|
-
|
|
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);
|
package/src/commands/mcp.ts
CHANGED
|
@@ -126,18 +126,17 @@ async function parseWranglerBindings(cwd: string): Promise<{
|
|
|
126
126
|
kv: string[];
|
|
127
127
|
} | null> {
|
|
128
128
|
try {
|
|
129
|
-
const
|
|
130
|
-
const
|
|
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 (
|
|
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"
|
|
202
|
-
|
|
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(
|
package/src/commands/services.ts
CHANGED
|
@@ -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(
|
|
521
|
-
|
|
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 {
|
package/src/commands/sync.ts
CHANGED
|
@@ -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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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 (!
|
|
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);
|
package/src/lib/auth/client.ts
CHANGED
|
@@ -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
|
-
|
|
113
|
+
let token = await getValidAccessToken();
|
|
114
114
|
if (!token) {
|
|
115
|
-
|
|
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) {
|
package/src/lib/build-helper.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync
|
|
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 =
|
|
81
|
+
const wranglerPath = findWranglerConfig(projectPath);
|
|
83
82
|
|
|
84
|
-
if (!
|
|
83
|
+
if (!wranglerPath) {
|
|
85
84
|
throw new JackError(
|
|
86
85
|
JackErrorCode.VALIDATION_ERROR,
|
|
87
|
-
"wrangler
|
|
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 =
|
|
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)
|
package/src/lib/control-plane.ts
CHANGED
|
@@ -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
|
+
}
|