@getjack/jack 0.1.33 → 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/secrets.ts +3 -1
- 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/crypto.ts +84 -0
- package/src/lib/deploy-upload.ts +7 -3
- package/src/lib/do-config.ts +110 -0
- package/src/lib/do-export-validator.ts +26 -0
- package/src/lib/hooks.ts +1 -2
- 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 +37 -46
- package/src/lib/prompts.ts +2 -2
- 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 +22 -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/nextjs-clerk/app/layout.tsx +2 -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/src/lib/hooks.ts
CHANGED
|
@@ -533,8 +533,7 @@ const actionHandlers: {
|
|
|
533
533
|
const { isCancel, text } = await import("@clack/prompts");
|
|
534
534
|
const value = await text({ message: promptMsg });
|
|
535
535
|
|
|
536
|
-
if (isCancel(value) || !value.trim()) {
|
|
537
|
-
ui.warn(`Skipped ${action.key}`);
|
|
536
|
+
if (isCancel(value) || !value || !value.trim()) {
|
|
538
537
|
return false;
|
|
539
538
|
}
|
|
540
539
|
|
|
@@ -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] : [])]
|
|
@@ -797,10 +785,18 @@ export async function createProject(
|
|
|
797
785
|
? resolve(targetDirOption, name)
|
|
798
786
|
: join(getJackHome(), name);
|
|
799
787
|
if (existsSync(effectiveTargetDir)) {
|
|
788
|
+
const existingLink = await readProjectLink(effectiveTargetDir);
|
|
789
|
+
if (existingLink) {
|
|
790
|
+
throw new JackError(
|
|
791
|
+
JackErrorCode.VALIDATION_ERROR,
|
|
792
|
+
`'${name}' already exists`,
|
|
793
|
+
`cd ${effectiveTargetDir} && jack ship to deploy it, or rm -rf ${effectiveTargetDir} to start fresh.`,
|
|
794
|
+
);
|
|
795
|
+
}
|
|
800
796
|
throw new JackError(
|
|
801
797
|
JackErrorCode.VALIDATION_ERROR,
|
|
802
|
-
`Folder exists at ${effectiveTargetDir}/`,
|
|
803
|
-
"Remove it
|
|
798
|
+
`Folder already exists at ${effectiveTargetDir}/`,
|
|
799
|
+
"Remove it or pick a different name.",
|
|
804
800
|
);
|
|
805
801
|
}
|
|
806
802
|
}
|
|
@@ -873,10 +869,18 @@ export async function createProject(
|
|
|
873
869
|
|
|
874
870
|
// Check directory doesn't exist (only needed for auto-generated names now)
|
|
875
871
|
if (!nameWasProvided && existsSync(targetDir)) {
|
|
872
|
+
const existingLink = await readProjectLink(targetDir);
|
|
873
|
+
if (existingLink) {
|
|
874
|
+
throw new JackError(
|
|
875
|
+
JackErrorCode.VALIDATION_ERROR,
|
|
876
|
+
`'${projectName}' already exists`,
|
|
877
|
+
`cd ${targetDir} && jack ship to deploy it, or rm -rf ${targetDir} to start fresh.`,
|
|
878
|
+
);
|
|
879
|
+
}
|
|
876
880
|
throw new JackError(
|
|
877
881
|
JackErrorCode.VALIDATION_ERROR,
|
|
878
|
-
`Folder exists at ${targetDir}/`,
|
|
879
|
-
"Remove it first
|
|
882
|
+
`Folder already exists at ${targetDir}/`,
|
|
883
|
+
"Remove it first or pick a different name.",
|
|
880
884
|
);
|
|
881
885
|
}
|
|
882
886
|
|
|
@@ -1045,8 +1049,8 @@ export async function createProject(
|
|
|
1045
1049
|
if (!hookResult.success) {
|
|
1046
1050
|
throw new JackError(
|
|
1047
1051
|
JackErrorCode.VALIDATION_ERROR,
|
|
1048
|
-
"Project setup
|
|
1049
|
-
|
|
1052
|
+
"Project setup cancelled",
|
|
1053
|
+
`Run the same command again when you're ready — jack new ${projectName} -t ${resolvedTemplate}`,
|
|
1050
1054
|
);
|
|
1051
1055
|
}
|
|
1052
1056
|
}
|
|
@@ -1111,7 +1115,7 @@ export async function createProject(
|
|
|
1111
1115
|
placeholder: "paste value or press Esc to skip",
|
|
1112
1116
|
});
|
|
1113
1117
|
|
|
1114
|
-
if (!isCancel(value) && value.trim()) {
|
|
1118
|
+
if (!isCancel(value) && value && value.trim()) {
|
|
1115
1119
|
secretsToUse[optionalSecret.name] = value.trim();
|
|
1116
1120
|
// Save to global secrets for reuse
|
|
1117
1121
|
await saveSecrets([
|
|
@@ -1123,7 +1127,7 @@ export async function createProject(
|
|
|
1123
1127
|
]);
|
|
1124
1128
|
reporter.success(`Saved ${optionalSecret.name}`);
|
|
1125
1129
|
} else {
|
|
1126
|
-
reporter.info(`Skipped ${optionalSecret.name}`);
|
|
1130
|
+
reporter.info(`Skipped ${optionalSecret.name} — add it later with: jack secrets add ${optionalSecret.name}`);
|
|
1127
1131
|
}
|
|
1128
1132
|
|
|
1129
1133
|
reporter.start("Creating project...");
|
|
@@ -1590,25 +1594,22 @@ export async function deployProject(options: DeployOptions = {}): Promise<Deploy
|
|
|
1590
1594
|
const interactive = interactiveOption ?? !isCi;
|
|
1591
1595
|
|
|
1592
1596
|
// Check for wrangler config
|
|
1593
|
-
const
|
|
1594
|
-
existsSync(join(projectPath, "wrangler.toml")) ||
|
|
1595
|
-
existsSync(join(projectPath, "wrangler.jsonc")) ||
|
|
1596
|
-
existsSync(join(projectPath, "wrangler.json"));
|
|
1597
|
+
const hasConfig = hasWranglerConfig(projectPath);
|
|
1597
1598
|
|
|
1598
1599
|
// Check for existing project link
|
|
1599
1600
|
const hasProjectLink = existsSync(join(projectPath, ".jack", "project.json"));
|
|
1600
1601
|
|
|
1601
1602
|
// Auto-detect flow: no wrangler config and no project link
|
|
1602
1603
|
let autoDetectResult: AutoDetectResult | null = null;
|
|
1603
|
-
if (!
|
|
1604
|
+
if (!hasConfig && !hasProjectLink) {
|
|
1604
1605
|
autoDetectResult = await runAutoDetectFlow(projectPath, reporter, interactive, dryRun);
|
|
1605
|
-
} else if (!
|
|
1606
|
+
} else if (!hasConfig) {
|
|
1606
1607
|
throw new JackError(
|
|
1607
1608
|
JackErrorCode.PROJECT_NOT_FOUND,
|
|
1608
1609
|
"No wrangler config found in current directory",
|
|
1609
1610
|
"Run: jack new <project-name>",
|
|
1610
1611
|
);
|
|
1611
|
-
} else if (
|
|
1612
|
+
} else if (hasConfig && !hasProjectLink) {
|
|
1612
1613
|
// Orphaned state: wrangler config exists but no project link
|
|
1613
1614
|
// This happens when: linking failed during jack new, user has existing wrangler project,
|
|
1614
1615
|
// or project was moved/copied without .jack directory
|
|
@@ -1798,9 +1799,7 @@ export async function deployProject(options: DeployOptions = {}): Promise<Deploy
|
|
|
1798
1799
|
managedDeployResult = await deployToManagedProject(managedProjectId as string, projectPath, reporter, options.message);
|
|
1799
1800
|
|
|
1800
1801
|
// Construct URL with username if available
|
|
1801
|
-
workerUrl = link?.owner_username
|
|
1802
|
-
? `https://${link.owner_username}-${projectName}.runjack.xyz`
|
|
1803
|
-
: `https://${projectName}.runjack.xyz`;
|
|
1802
|
+
workerUrl = await buildManagedUrl(projectName, link?.owner_username, projectPath);
|
|
1804
1803
|
} else {
|
|
1805
1804
|
// BYO mode: deploy via wrangler
|
|
1806
1805
|
|
|
@@ -1990,11 +1989,8 @@ export async function getProjectStatus(
|
|
|
1990
1989
|
const link = await readProjectLink(resolvedPath);
|
|
1991
1990
|
|
|
1992
1991
|
// Check if local project exists at the resolved path
|
|
1993
|
-
const
|
|
1994
|
-
|
|
1995
|
-
existsSync(join(resolvedPath, "wrangler.toml")) ||
|
|
1996
|
-
existsSync(join(resolvedPath, "wrangler.json"));
|
|
1997
|
-
const localExists = hasWranglerConfig;
|
|
1992
|
+
const hasConfig = hasWranglerConfig(resolvedPath);
|
|
1993
|
+
const localExists = hasConfig;
|
|
1998
1994
|
const localPath = localExists ? resolvedPath : null;
|
|
1999
1995
|
|
|
2000
1996
|
// If no link and no local project, return null
|
|
@@ -2014,9 +2010,7 @@ export async function getProjectStatus(
|
|
|
2014
2010
|
// Determine URL based on mode
|
|
2015
2011
|
let workerUrl: string | null = null;
|
|
2016
2012
|
if (link?.deploy_mode === "managed") {
|
|
2017
|
-
workerUrl = link.owner_username
|
|
2018
|
-
? `https://${link.owner_username}-${projectName}.runjack.xyz`
|
|
2019
|
-
: `https://${projectName}.runjack.xyz`;
|
|
2013
|
+
workerUrl = await buildManagedUrl(projectName, link.owner_username, resolvedPath);
|
|
2020
2014
|
}
|
|
2021
2015
|
|
|
2022
2016
|
// Get database name on-demand
|
|
@@ -2124,12 +2118,9 @@ export async function scanStaleProjects(): Promise<StaleProjectScan> {
|
|
|
2124
2118
|
|
|
2125
2119
|
for (const projectPath of paths) {
|
|
2126
2120
|
// Check if path exists and has valid wrangler config
|
|
2127
|
-
const
|
|
2128
|
-
existsSync(join(projectPath, "wrangler.jsonc")) ||
|
|
2129
|
-
existsSync(join(projectPath, "wrangler.toml")) ||
|
|
2130
|
-
existsSync(join(projectPath, "wrangler.json"));
|
|
2121
|
+
const hasConfig = hasWranglerConfig(projectPath);
|
|
2131
2122
|
|
|
2132
|
-
if (!
|
|
2123
|
+
if (!hasConfig) {
|
|
2133
2124
|
// Type 1: No wrangler config at path (dir deleted/moved)
|
|
2134
2125
|
const name = projectPath.split("/").pop() || projectId;
|
|
2135
2126
|
stale.push({
|
package/src/lib/prompts.ts
CHANGED
|
@@ -68,7 +68,7 @@ async function promptAdditionalSecrets(): Promise<
|
|
|
68
68
|
message: "Enter secret name (or press enter to finish):",
|
|
69
69
|
});
|
|
70
70
|
|
|
71
|
-
if (isCancel(key) || !key.trim()) {
|
|
71
|
+
if (isCancel(key) || !key || !key.trim()) {
|
|
72
72
|
break;
|
|
73
73
|
}
|
|
74
74
|
|
|
@@ -76,7 +76,7 @@ async function promptAdditionalSecrets(): Promise<
|
|
|
76
76
|
message: `Enter value for ${key}:`,
|
|
77
77
|
});
|
|
78
78
|
|
|
79
|
-
if (isCancel(value)) {
|
|
79
|
+
if (isCancel(value) || !value) {
|
|
80
80
|
break;
|
|
81
81
|
}
|
|
82
82
|
|
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 {
|