@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.
- 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/commands/update.ts +1 -0
- package/src/index.ts +8 -0
- 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 +45 -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 +31 -66
- 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 +579 -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 +280 -2
- 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 +1584 -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 +63 -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
|
@@ -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
|
-
|
|
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) {
|
package/src/lib/project-link.ts
CHANGED
|
@@ -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 =
|
|
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
|
|
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 (!
|
|
1604
|
+
if (!hasConfig && !hasProjectLink) {
|
|
1620
1605
|
autoDetectResult = await runAutoDetectFlow(projectPath, reporter, interactive, dryRun);
|
|
1621
|
-
} else if (!
|
|
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 (
|
|
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
|
|
2010
|
-
|
|
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
|
|
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 {
|
|
2071
|
-
const
|
|
2072
|
-
|
|
2073
|
-
|
|
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 —
|
|
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
|
|
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 (!
|
|
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({
|
package/src/lib/resources.ts
CHANGED
|
@@ -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 =
|
|
90
|
+
const wranglerPath = findWranglerConfig(projectPath);
|
|
92
91
|
|
|
93
|
-
if (!
|
|
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 =
|
|
51
|
+
const wranglerPath = findWranglerConfig(projectDir);
|
|
51
52
|
|
|
52
|
-
if (!
|
|
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 =
|
|
75
|
+
const wranglerPath = findWranglerConfig(projectDir);
|
|
75
76
|
|
|
76
|
-
if (!
|
|
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 =
|
|
95
|
+
const wranglerPath = findWranglerConfig(projectDir);
|
|
95
96
|
|
|
96
|
-
if (!
|
|
97
|
+
if (!wranglerPath) {
|
|
97
98
|
return null;
|
|
98
99
|
}
|
|
99
100
|
|
|
100
101
|
try {
|
|
101
102
|
const content = await Bun.file(wranglerPath).text();
|
|
102
|
-
|
|
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
|
|