@getjack/jack 0.1.16 → 0.1.19
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 +1 -1
- package/package.json +1 -1
- package/src/commands/clone.ts +62 -40
- package/src/commands/community.ts +47 -0
- package/src/commands/init.ts +6 -0
- package/src/commands/services.ts +354 -9
- package/src/commands/sync.ts +9 -0
- package/src/commands/update.ts +10 -0
- package/src/index.ts +7 -0
- package/src/lib/control-plane.ts +62 -0
- package/src/lib/hooks.ts +20 -0
- package/src/lib/managed-deploy.ts +26 -2
- package/src/lib/output.ts +21 -3
- package/src/lib/progress.ts +160 -0
- package/src/lib/project-operations.ts +381 -93
- package/src/lib/services/db-create.ts +6 -3
- package/src/lib/services/db-execute.ts +485 -0
- package/src/lib/services/sql-classifier.test.ts +404 -0
- package/src/lib/services/sql-classifier.ts +346 -0
- package/src/lib/storage/file-filter.ts +4 -0
- package/src/lib/telemetry.ts +3 -0
- package/src/lib/version-check.ts +14 -0
- package/src/lib/wrangler-config.test.ts +322 -0
- package/src/lib/wrangler-config.ts +649 -0
- package/src/lib/zip-packager.ts +38 -0
- package/src/lib/zip-utils.ts +38 -0
- package/src/mcp/tools/index.ts +161 -0
- package/src/templates/index.ts +4 -0
- package/src/templates/types.ts +12 -0
- package/templates/api/AGENTS.md +33 -0
- package/templates/hello/AGENTS.md +33 -0
- package/templates/miniapp/.jack.json +4 -5
- package/templates/miniapp/AGENTS.md +33 -0
- package/templates/nextjs/AGENTS.md +33 -0
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
renderTemplate,
|
|
15
15
|
resolveTemplateWithOrigin,
|
|
16
16
|
} from "../templates/index.ts";
|
|
17
|
-
import type { Template } from "../templates/types.ts";
|
|
17
|
+
import type { EnvVar, Template } from "../templates/types.ts";
|
|
18
18
|
import { generateAgentFiles } from "./agent-files.ts";
|
|
19
19
|
import {
|
|
20
20
|
getActiveAgents,
|
|
@@ -39,8 +39,7 @@ import {
|
|
|
39
39
|
slugify,
|
|
40
40
|
writeWranglerConfig,
|
|
41
41
|
} from "./config-generator.ts";
|
|
42
|
-
import {
|
|
43
|
-
import { deleteManagedProject } from "./control-plane.ts";
|
|
42
|
+
import { deleteManagedProject, listManagedProjects } from "./control-plane.ts";
|
|
44
43
|
import { debug, isDebug, printTimingSummary, timerEnd, timerStart } from "./debug.ts";
|
|
45
44
|
import { ensureWranglerInstalled, validateModeAvailability } from "./deploy-mode.ts";
|
|
46
45
|
import { detectSecrets, generateEnvFile, generateSecretsJson } from "./env-parser.ts";
|
|
@@ -67,7 +66,7 @@ import {
|
|
|
67
66
|
import { filterNewSecrets, promptSaveSecrets } from "./prompts.ts";
|
|
68
67
|
import { applySchema, getD1Bindings, getD1DatabaseName, hasD1Config } from "./schema.ts";
|
|
69
68
|
import { getSavedSecrets, saveSecrets } from "./secrets.ts";
|
|
70
|
-
import { getProjectNameFromDir, getRemoteManifest
|
|
69
|
+
import { getProjectNameFromDir, getRemoteManifest } from "./storage/index.ts";
|
|
71
70
|
import { Events, track } from "./telemetry.ts";
|
|
72
71
|
|
|
73
72
|
// ============================================================================
|
|
@@ -168,6 +167,129 @@ const noopReporter: OperationReporter = {
|
|
|
168
167
|
box() {},
|
|
169
168
|
};
|
|
170
169
|
|
|
170
|
+
/**
|
|
171
|
+
* Check if an environment variable already exists in a .env file
|
|
172
|
+
* Returns the existing value if found, null otherwise
|
|
173
|
+
*/
|
|
174
|
+
async function checkEnvVarExists(envPath: string, key: string): Promise<string | null> {
|
|
175
|
+
if (!existsSync(envPath)) {
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const content = await Bun.file(envPath).text();
|
|
180
|
+
for (const line of content.split("\n")) {
|
|
181
|
+
const trimmed = line.trim();
|
|
182
|
+
if (!trimmed || trimmed.startsWith("#")) {
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const eqIndex = trimmed.indexOf("=");
|
|
187
|
+
if (eqIndex === -1) {
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const lineKey = trimmed.slice(0, eqIndex).trim();
|
|
192
|
+
if (lineKey === key) {
|
|
193
|
+
let value = trimmed.slice(eqIndex + 1).trim();
|
|
194
|
+
// Remove surrounding quotes
|
|
195
|
+
if (
|
|
196
|
+
(value.startsWith('"') && value.endsWith('"')) ||
|
|
197
|
+
(value.startsWith("'") && value.endsWith("'"))
|
|
198
|
+
) {
|
|
199
|
+
value = value.slice(1, -1);
|
|
200
|
+
}
|
|
201
|
+
return value;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return null;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Prompt for environment variables defined in a template
|
|
210
|
+
* Returns a record of env var name -> value for vars that were provided
|
|
211
|
+
*/
|
|
212
|
+
async function promptEnvVars(
|
|
213
|
+
envVars: EnvVar[],
|
|
214
|
+
targetDir: string,
|
|
215
|
+
reporter: OperationReporter,
|
|
216
|
+
interactive: boolean,
|
|
217
|
+
): Promise<Record<string, string>> {
|
|
218
|
+
const result: Record<string, string> = {};
|
|
219
|
+
const envPath = join(targetDir, ".env");
|
|
220
|
+
|
|
221
|
+
for (const envVar of envVars) {
|
|
222
|
+
// Check if already exists in .env
|
|
223
|
+
const existingValue = await checkEnvVarExists(envPath, envVar.name);
|
|
224
|
+
if (existingValue) {
|
|
225
|
+
reporter.stop();
|
|
226
|
+
reporter.success(`${envVar.name}: already configured`);
|
|
227
|
+
reporter.start("Creating project...");
|
|
228
|
+
result[envVar.name] = existingValue;
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (!interactive) {
|
|
233
|
+
// Non-interactive mode: use default if available, otherwise warn
|
|
234
|
+
if (envVar.defaultValue !== undefined) {
|
|
235
|
+
result[envVar.name] = envVar.defaultValue;
|
|
236
|
+
reporter.stop();
|
|
237
|
+
reporter.info(`${envVar.name}: using default value`);
|
|
238
|
+
reporter.start("Creating project...");
|
|
239
|
+
} else if (envVar.required !== false) {
|
|
240
|
+
reporter.stop();
|
|
241
|
+
reporter.warn(`${envVar.name}: required but not set (no default available)`);
|
|
242
|
+
reporter.start("Creating project...");
|
|
243
|
+
}
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Interactive mode: prompt user
|
|
248
|
+
reporter.stop();
|
|
249
|
+
const { isCancel, text } = await import("@clack/prompts");
|
|
250
|
+
|
|
251
|
+
console.error("");
|
|
252
|
+
console.error(` ${envVar.description}`);
|
|
253
|
+
if (envVar.setupUrl) {
|
|
254
|
+
console.error(` Get it at: ${envVar.setupUrl}`);
|
|
255
|
+
}
|
|
256
|
+
if (envVar.example) {
|
|
257
|
+
console.error(` Example: ${envVar.example}`);
|
|
258
|
+
}
|
|
259
|
+
console.error("");
|
|
260
|
+
|
|
261
|
+
const value = await text({
|
|
262
|
+
message: `${envVar.name}:`,
|
|
263
|
+
defaultValue: envVar.defaultValue,
|
|
264
|
+
placeholder: envVar.defaultValue ?? (envVar.example ? `e.g. ${envVar.example}` : undefined),
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
if (isCancel(value)) {
|
|
268
|
+
// User cancelled - skip this var
|
|
269
|
+
if (envVar.required !== false) {
|
|
270
|
+
reporter.warn(`Skipped required env var: ${envVar.name}`);
|
|
271
|
+
}
|
|
272
|
+
reporter.start("Creating project...");
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const trimmedValue = value.trim();
|
|
277
|
+
if (trimmedValue) {
|
|
278
|
+
result[envVar.name] = trimmedValue;
|
|
279
|
+
reporter.success(`Set ${envVar.name}`);
|
|
280
|
+
} else if (envVar.defaultValue !== undefined) {
|
|
281
|
+
result[envVar.name] = envVar.defaultValue;
|
|
282
|
+
reporter.info(`${envVar.name}: using default value`);
|
|
283
|
+
} else if (envVar.required !== false) {
|
|
284
|
+
reporter.warn(`Skipped required env var: ${envVar.name}`);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
reporter.start("Creating project...");
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
return result;
|
|
291
|
+
}
|
|
292
|
+
|
|
171
293
|
/**
|
|
172
294
|
* Get the wrangler config file path for a project
|
|
173
295
|
* Returns the first found: wrangler.jsonc, wrangler.toml, wrangler.json
|
|
@@ -198,9 +320,36 @@ async function runWranglerDeploy(
|
|
|
198
320
|
return await $`wrangler deploy ${configArg} ${dryRunArgs}`.cwd(projectPath).nothrow().quiet();
|
|
199
321
|
}
|
|
200
322
|
|
|
323
|
+
/**
|
|
324
|
+
* Ensure Cloudflare authentication is in place before BYO operations.
|
|
325
|
+
* Checks wrangler auth and CLOUDFLARE_API_TOKEN env var.
|
|
326
|
+
*/
|
|
327
|
+
async function ensureCloudflareAuth(
|
|
328
|
+
interactive: boolean,
|
|
329
|
+
reporter: OperationReporter,
|
|
330
|
+
): Promise<void> {
|
|
331
|
+
const { isAuthenticated, ensureAuth } = await import("./wrangler.ts");
|
|
332
|
+
const cfAuthenticated = await isAuthenticated();
|
|
333
|
+
const hasApiToken = Boolean(process.env.CLOUDFLARE_API_TOKEN);
|
|
334
|
+
|
|
335
|
+
if (!cfAuthenticated && !hasApiToken) {
|
|
336
|
+
if (interactive) {
|
|
337
|
+
reporter.info("Cloudflare authentication required");
|
|
338
|
+
await ensureAuth();
|
|
339
|
+
} else {
|
|
340
|
+
throw new JackError(
|
|
341
|
+
JackErrorCode.AUTH_FAILED,
|
|
342
|
+
"Not authenticated with Cloudflare",
|
|
343
|
+
"Run: wrangler login\nOr set CLOUDFLARE_API_TOKEN environment variable",
|
|
344
|
+
);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
201
349
|
/**
|
|
202
350
|
* Run bun install and managed project creation in parallel.
|
|
203
351
|
* Handles partial failures with cleanup.
|
|
352
|
+
* Optionally reports URL early via onRemoteReady callback.
|
|
204
353
|
*/
|
|
205
354
|
async function runParallelSetup(
|
|
206
355
|
targetDir: string,
|
|
@@ -208,32 +357,41 @@ async function runParallelSetup(
|
|
|
208
357
|
options: {
|
|
209
358
|
template?: string;
|
|
210
359
|
usePrebuilt?: boolean;
|
|
360
|
+
onRemoteReady?: (result: ManagedCreateResult) => void;
|
|
211
361
|
},
|
|
212
362
|
): Promise<{
|
|
213
363
|
installSuccess: boolean;
|
|
214
364
|
remoteResult: ManagedCreateResult;
|
|
215
365
|
}> {
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
366
|
+
// Start both operations
|
|
367
|
+
const installPromise = (async () => {
|
|
368
|
+
const install = Bun.spawn(["bun", "install", "--prefer-offline"], {
|
|
369
|
+
cwd: targetDir,
|
|
370
|
+
stdout: "ignore",
|
|
371
|
+
stderr: "ignore",
|
|
372
|
+
});
|
|
373
|
+
await install.exited;
|
|
374
|
+
if (install.exitCode !== 0) {
|
|
375
|
+
throw new Error("Dependency installation failed");
|
|
376
|
+
}
|
|
377
|
+
return true;
|
|
378
|
+
})();
|
|
379
|
+
|
|
380
|
+
const remotePromise = createManagedProjectRemote(projectName, undefined, {
|
|
381
|
+
template: options.template || "hello",
|
|
382
|
+
usePrebuilt: options.usePrebuilt ?? true,
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
// Report URL as soon as remote is ready (don't wait for install)
|
|
386
|
+
remotePromise
|
|
387
|
+
.then((result) => {
|
|
388
|
+
if (result.status === "live" && options.onRemoteReady) {
|
|
389
|
+
options.onRemoteReady(result);
|
|
227
390
|
}
|
|
228
|
-
|
|
229
|
-
})
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
createManagedProjectRemote(projectName, undefined, {
|
|
233
|
-
template: options.template || "hello",
|
|
234
|
-
usePrebuilt: options.usePrebuilt ?? true,
|
|
235
|
-
}),
|
|
236
|
-
]);
|
|
391
|
+
})
|
|
392
|
+
.catch(() => {}); // Errors handled below in allSettled
|
|
393
|
+
|
|
394
|
+
const [installResult, remoteResult] = await Promise.allSettled([installPromise, remotePromise]);
|
|
237
395
|
|
|
238
396
|
const installFailed = installResult.status === "rejected";
|
|
239
397
|
const remoteFailed = remoteResult.status === "rejected";
|
|
@@ -814,6 +972,25 @@ export async function createProject(
|
|
|
814
972
|
const rendered = renderTemplate(template, { name: projectName });
|
|
815
973
|
timings.push({ label: "Template load", duration: timerEnd("template-load") });
|
|
816
974
|
|
|
975
|
+
// Run preCreate hooks (for interactive secret collection, auto-generation, etc.)
|
|
976
|
+
if (template.hooks?.preCreate?.length) {
|
|
977
|
+
timerStart("pre-create-hooks");
|
|
978
|
+
const hookContext = { projectName, projectDir: targetDir };
|
|
979
|
+
const hookResult = await runHook(template.hooks.preCreate, hookContext, {
|
|
980
|
+
interactive,
|
|
981
|
+
output: reporter,
|
|
982
|
+
});
|
|
983
|
+
timings.push({ label: "Pre-create hooks", duration: timerEnd("pre-create-hooks") });
|
|
984
|
+
|
|
985
|
+
if (!hookResult.success) {
|
|
986
|
+
throw new JackError(
|
|
987
|
+
JackErrorCode.VALIDATION_ERROR,
|
|
988
|
+
"Project setup incomplete",
|
|
989
|
+
"Missing required configuration",
|
|
990
|
+
);
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
|
|
817
994
|
// Handle template-specific secrets
|
|
818
995
|
const secretsToUse: Record<string, string> = {};
|
|
819
996
|
if (template.secrets?.length) {
|
|
@@ -859,47 +1036,46 @@ export async function createProject(
|
|
|
859
1036
|
continue;
|
|
860
1037
|
}
|
|
861
1038
|
|
|
862
|
-
// Prompt user
|
|
1039
|
+
// Prompt user - single text input, empty/Esc to skip
|
|
863
1040
|
reporter.stop();
|
|
864
|
-
const { isCancel,
|
|
1041
|
+
const { isCancel, text } = await import("@clack/prompts");
|
|
865
1042
|
console.error("");
|
|
866
1043
|
console.error(` ${optionalSecret.description}`);
|
|
867
1044
|
if (optionalSecret.setupUrl) {
|
|
868
|
-
console.error(`
|
|
1045
|
+
console.error(` Get it at: \x1b[36m${optionalSecret.setupUrl}\x1b[0m`);
|
|
869
1046
|
}
|
|
870
1047
|
console.error("");
|
|
871
1048
|
|
|
872
|
-
const
|
|
873
|
-
message:
|
|
874
|
-
|
|
875
|
-
{ label: "Yes", value: "yes" },
|
|
876
|
-
{ label: "Skip", value: "skip" },
|
|
877
|
-
],
|
|
1049
|
+
const value = await text({
|
|
1050
|
+
message: `${optionalSecret.name}:`,
|
|
1051
|
+
placeholder: "paste value or press Esc to skip",
|
|
878
1052
|
});
|
|
879
1053
|
|
|
880
|
-
if (!isCancel(
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
},
|
|
894
|
-
]);
|
|
895
|
-
reporter.success(`Saved ${optionalSecret.name}`);
|
|
896
|
-
}
|
|
1054
|
+
if (!isCancel(value) && value.trim()) {
|
|
1055
|
+
secretsToUse[optionalSecret.name] = value.trim();
|
|
1056
|
+
// Save to global secrets for reuse
|
|
1057
|
+
await saveSecrets([
|
|
1058
|
+
{
|
|
1059
|
+
key: optionalSecret.name,
|
|
1060
|
+
value: value.trim(),
|
|
1061
|
+
source: "optional-template",
|
|
1062
|
+
},
|
|
1063
|
+
]);
|
|
1064
|
+
reporter.success(`Saved ${optionalSecret.name}`);
|
|
1065
|
+
} else {
|
|
1066
|
+
reporter.info(`Skipped ${optionalSecret.name}`);
|
|
897
1067
|
}
|
|
898
1068
|
|
|
899
1069
|
reporter.start("Creating project...");
|
|
900
1070
|
}
|
|
901
1071
|
}
|
|
902
1072
|
|
|
1073
|
+
// Handle environment variables (non-secret configuration)
|
|
1074
|
+
let envVarsToUse: Record<string, string> = {};
|
|
1075
|
+
if (template.envVars?.length) {
|
|
1076
|
+
envVarsToUse = await promptEnvVars(template.envVars, targetDir, reporter, interactive);
|
|
1077
|
+
}
|
|
1078
|
+
|
|
903
1079
|
// Track if we created the directory (for cleanup on failure)
|
|
904
1080
|
let directoryCreated = false;
|
|
905
1081
|
|
|
@@ -918,13 +1094,24 @@ export async function createProject(
|
|
|
918
1094
|
}
|
|
919
1095
|
reporter.start("Creating project...");
|
|
920
1096
|
|
|
921
|
-
// Write secrets
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
1097
|
+
// Write secrets and env vars files
|
|
1098
|
+
// - Secrets go to: .env, .dev.vars, .secrets.json (for wrangler bulk upload)
|
|
1099
|
+
// - Env vars go to: .env, .dev.vars only (not secrets.json - they're not secrets)
|
|
1100
|
+
const hasSecrets = Object.keys(secretsToUse).length > 0;
|
|
1101
|
+
const hasEnvVars = Object.keys(envVarsToUse).length > 0;
|
|
1102
|
+
|
|
1103
|
+
if (hasSecrets || hasEnvVars) {
|
|
1104
|
+
// Combine secrets and env vars for .env and .dev.vars
|
|
1105
|
+
const allEnvVars = { ...secretsToUse, ...envVarsToUse };
|
|
1106
|
+
const envContent = generateEnvFile(allEnvVars);
|
|
925
1107
|
await Bun.write(join(targetDir, ".env"), envContent);
|
|
926
1108
|
await Bun.write(join(targetDir, ".dev.vars"), envContent);
|
|
927
|
-
|
|
1109
|
+
|
|
1110
|
+
// Only write secrets to .secrets.json (for wrangler secret bulk)
|
|
1111
|
+
if (hasSecrets) {
|
|
1112
|
+
const jsonContent = generateSecretsJson(secretsToUse);
|
|
1113
|
+
await Bun.write(join(targetDir, ".secrets.json"), jsonContent);
|
|
1114
|
+
}
|
|
928
1115
|
|
|
929
1116
|
const gitignorePath = join(targetDir, ".gitignore");
|
|
930
1117
|
const gitignoreExists = existsSync(gitignorePath);
|
|
@@ -970,6 +1157,7 @@ export async function createProject(
|
|
|
970
1157
|
|
|
971
1158
|
// Parallel setup for managed mode: install + remote creation
|
|
972
1159
|
let remoteResult: ManagedCreateResult | undefined;
|
|
1160
|
+
let urlShownEarly = false;
|
|
973
1161
|
|
|
974
1162
|
if (deployMode === "managed") {
|
|
975
1163
|
// Run install and remote creation in parallel
|
|
@@ -980,11 +1168,22 @@ export async function createProject(
|
|
|
980
1168
|
const result = await runParallelSetup(targetDir, projectName, {
|
|
981
1169
|
template: resolvedTemplate || "hello",
|
|
982
1170
|
usePrebuilt: templateOrigin.type === "builtin", // Only builtin templates have prebuilt bundles
|
|
1171
|
+
onRemoteReady: (remote) => {
|
|
1172
|
+
// Show URL immediately when prebuilt succeeds
|
|
1173
|
+
reporter.stop();
|
|
1174
|
+
reporter.success(`Live: ${remote.runjackUrl}`);
|
|
1175
|
+
reporter.start("Installing dependencies locally...");
|
|
1176
|
+
urlShownEarly = true;
|
|
1177
|
+
},
|
|
983
1178
|
});
|
|
984
1179
|
remoteResult = result.remoteResult;
|
|
985
1180
|
timings.push({ label: "Parallel setup", duration: timerEnd("parallel-setup") });
|
|
986
1181
|
reporter.stop();
|
|
987
|
-
|
|
1182
|
+
if (urlShownEarly) {
|
|
1183
|
+
reporter.success("Ready for local development");
|
|
1184
|
+
} else {
|
|
1185
|
+
reporter.success("Project setup complete");
|
|
1186
|
+
}
|
|
988
1187
|
} catch (err) {
|
|
989
1188
|
timerEnd("parallel-setup");
|
|
990
1189
|
reporter.stop();
|
|
@@ -995,11 +1194,11 @@ export async function createProject(
|
|
|
995
1194
|
throw err;
|
|
996
1195
|
}
|
|
997
1196
|
} else {
|
|
998
|
-
// BYO mode: just install dependencies
|
|
1197
|
+
// BYO mode: just install dependencies
|
|
999
1198
|
timerStart("bun-install");
|
|
1000
1199
|
reporter.start("Installing dependencies...");
|
|
1001
1200
|
|
|
1002
|
-
const install = Bun.spawn(["bun", "install"], {
|
|
1201
|
+
const install = Bun.spawn(["bun", "install", "--prefer-offline"], {
|
|
1003
1202
|
cwd: targetDir,
|
|
1004
1203
|
stdout: "ignore",
|
|
1005
1204
|
stderr: "ignore",
|
|
@@ -1109,6 +1308,7 @@ export async function createProject(
|
|
|
1109
1308
|
await writeTemplateMetadata(targetDir, templateOrigin);
|
|
1110
1309
|
await registerPath(remoteResult.projectId, targetDir);
|
|
1111
1310
|
} catch (err) {
|
|
1311
|
+
reporter.warn("Could not save project link (deploy still works)");
|
|
1112
1312
|
debug("Failed to link managed project:", err);
|
|
1113
1313
|
}
|
|
1114
1314
|
|
|
@@ -1116,7 +1316,27 @@ export async function createProject(
|
|
|
1116
1316
|
if (remoteResult.status === "live") {
|
|
1117
1317
|
// Prebuilt succeeded - skip the fresh build
|
|
1118
1318
|
workerUrl = remoteResult.runjackUrl;
|
|
1119
|
-
|
|
1319
|
+
// Only show if not already shown by parallel setup
|
|
1320
|
+
if (!urlShownEarly) {
|
|
1321
|
+
reporter.success(`Deployed: ${workerUrl}`);
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
// Upload source snapshot for forking (prebuilt path needs this too)
|
|
1325
|
+
try {
|
|
1326
|
+
const { createSourceZip } = await import("./zip-packager.ts");
|
|
1327
|
+
const { uploadSourceSnapshot } = await import("./control-plane.ts");
|
|
1328
|
+
const { rm } = await import("node:fs/promises");
|
|
1329
|
+
|
|
1330
|
+
const sourceZipPath = await createSourceZip(targetDir);
|
|
1331
|
+
await uploadSourceSnapshot(remoteResult.projectId, sourceZipPath);
|
|
1332
|
+
await rm(sourceZipPath, { force: true });
|
|
1333
|
+
debug("Source snapshot uploaded for prebuilt project");
|
|
1334
|
+
} catch (err) {
|
|
1335
|
+
debug(
|
|
1336
|
+
"Source snapshot upload failed (prebuilt):",
|
|
1337
|
+
err instanceof Error ? err.message : String(err),
|
|
1338
|
+
);
|
|
1339
|
+
}
|
|
1120
1340
|
} else {
|
|
1121
1341
|
// Prebuilt not available - fall back to fresh build
|
|
1122
1342
|
if (remoteResult.prebuiltFailed) {
|
|
@@ -1226,6 +1446,7 @@ export async function createProject(
|
|
|
1226
1446
|
await writeTemplateMetadata(targetDir, templateOrigin);
|
|
1227
1447
|
await registerPath(byoProjectId, targetDir);
|
|
1228
1448
|
} catch (err) {
|
|
1449
|
+
reporter.warn("Could not save project link (deploy still works)");
|
|
1229
1450
|
debug("Failed to link BYO project:", err);
|
|
1230
1451
|
}
|
|
1231
1452
|
}
|
|
@@ -1249,7 +1470,7 @@ export async function createProject(
|
|
|
1249
1470
|
|
|
1250
1471
|
// Show final celebration if there were interactive prompts (URL might have scrolled away)
|
|
1251
1472
|
if (hookResult.hadInteractiveActions && reporter.celebrate) {
|
|
1252
|
-
reporter.celebrate("You're live!", [
|
|
1473
|
+
reporter.celebrate("You're live!", [workerUrl]);
|
|
1253
1474
|
}
|
|
1254
1475
|
}
|
|
1255
1476
|
|
|
@@ -1327,6 +1548,57 @@ export async function deployProject(options: DeployOptions = {}): Promise<Deploy
|
|
|
1327
1548
|
"No wrangler config found in current directory",
|
|
1328
1549
|
"Run: jack new <project-name>",
|
|
1329
1550
|
);
|
|
1551
|
+
} else if (hasWranglerConfig && !hasProjectLink) {
|
|
1552
|
+
// Orphaned state: wrangler config exists but no project link
|
|
1553
|
+
// This happens when: linking failed during jack new, user has existing wrangler project,
|
|
1554
|
+
// or project was moved/copied without .jack directory
|
|
1555
|
+
const { isLoggedIn } = await import("./auth/store.ts");
|
|
1556
|
+
const loggedIn = await isLoggedIn();
|
|
1557
|
+
|
|
1558
|
+
if (loggedIn && !options.byo) {
|
|
1559
|
+
// User is logged into Jack Cloud - create managed project
|
|
1560
|
+
const orphanedProjectName = await getProjectNameFromDir(projectPath);
|
|
1561
|
+
|
|
1562
|
+
reporter.info(`Linking "${orphanedProjectName}" to jack cloud...`);
|
|
1563
|
+
|
|
1564
|
+
// Get username for URL construction
|
|
1565
|
+
const { getCurrentUserProfile } = await import("./control-plane.ts");
|
|
1566
|
+
const profile = await getCurrentUserProfile();
|
|
1567
|
+
const ownerUsername = profile?.username ?? undefined;
|
|
1568
|
+
|
|
1569
|
+
// Create managed project on jack cloud
|
|
1570
|
+
const remoteResult = await createManagedProjectRemote(orphanedProjectName, reporter, {
|
|
1571
|
+
usePrebuilt: false,
|
|
1572
|
+
});
|
|
1573
|
+
|
|
1574
|
+
// Link project locally
|
|
1575
|
+
await linkProject(projectPath, remoteResult.projectId, "managed", ownerUsername);
|
|
1576
|
+
await registerPath(remoteResult.projectId, projectPath);
|
|
1577
|
+
|
|
1578
|
+
// Set autoDetectResult so the rest of the flow uses managed mode
|
|
1579
|
+
autoDetectResult = {
|
|
1580
|
+
projectName: orphanedProjectName,
|
|
1581
|
+
projectId: remoteResult.projectId,
|
|
1582
|
+
deployMode: "managed",
|
|
1583
|
+
};
|
|
1584
|
+
|
|
1585
|
+
reporter.success("Linked to jack cloud");
|
|
1586
|
+
} else if (!options.managed) {
|
|
1587
|
+
// BYO path - ensure wrangler auth before proceeding
|
|
1588
|
+
await ensureCloudflareAuth(interactive, reporter);
|
|
1589
|
+
|
|
1590
|
+
// Create BYO link for tracking (non-blocking)
|
|
1591
|
+
const orphanedProjectName = await getProjectNameFromDir(projectPath);
|
|
1592
|
+
const byoProjectId = generateByoProjectId();
|
|
1593
|
+
|
|
1594
|
+
try {
|
|
1595
|
+
await linkProject(projectPath, byoProjectId, "byo");
|
|
1596
|
+
await registerPath(byoProjectId, projectPath);
|
|
1597
|
+
debug("Created BYO project link for orphaned project");
|
|
1598
|
+
} catch (err) {
|
|
1599
|
+
debug("Failed to create BYO project link:", err);
|
|
1600
|
+
}
|
|
1601
|
+
}
|
|
1330
1602
|
}
|
|
1331
1603
|
|
|
1332
1604
|
// Get project name from directory (or auto-detect result)
|
|
@@ -1475,6 +1747,9 @@ export async function deployProject(options: DeployOptions = {}): Promise<Deploy
|
|
|
1475
1747
|
}
|
|
1476
1748
|
}
|
|
1477
1749
|
|
|
1750
|
+
// Ensure Cloudflare auth before BYO deploy
|
|
1751
|
+
await ensureCloudflareAuth(interactive, reporter);
|
|
1752
|
+
|
|
1478
1753
|
const spin = reporter.spinner("Deploying...");
|
|
1479
1754
|
const result = await runWranglerDeploy(projectPath);
|
|
1480
1755
|
|
|
@@ -1522,28 +1797,9 @@ export async function deployProject(options: DeployOptions = {}): Promise<Deploy
|
|
|
1522
1797
|
}
|
|
1523
1798
|
}
|
|
1524
1799
|
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
const syncSpin = reporter.spinner("Syncing source to jack storage...");
|
|
1529
|
-
try {
|
|
1530
|
-
const syncResult = await syncToCloud(projectPath);
|
|
1531
|
-
if (syncResult.success) {
|
|
1532
|
-
if (syncResult.filesUploaded > 0 || syncResult.filesDeleted > 0) {
|
|
1533
|
-
syncSpin.success(
|
|
1534
|
-
`Synced source to jack storage (${syncResult.filesUploaded} uploaded, ${syncResult.filesDeleted} removed)`,
|
|
1535
|
-
);
|
|
1536
|
-
} else {
|
|
1537
|
-
syncSpin.success("Source already synced");
|
|
1538
|
-
}
|
|
1539
|
-
}
|
|
1540
|
-
} catch {
|
|
1541
|
-
syncSpin.stop();
|
|
1542
|
-
reporter.warn("Cloud sync failed (deploy succeeded)");
|
|
1543
|
-
reporter.info("Run: jack sync");
|
|
1544
|
-
}
|
|
1545
|
-
}
|
|
1546
|
-
}
|
|
1800
|
+
// Note: Auto-sync to User R2 was removed for managed mode.
|
|
1801
|
+
// Managed projects use control-plane source.zip for clone instead.
|
|
1802
|
+
// BYO users can run 'jack sync' manually if needed.
|
|
1547
1803
|
|
|
1548
1804
|
// Ensure project is linked locally for discovery
|
|
1549
1805
|
try {
|
|
@@ -1689,7 +1945,9 @@ export async function getProjectStatus(
|
|
|
1689
1945
|
|
|
1690
1946
|
/**
|
|
1691
1947
|
* Scan for stale project paths.
|
|
1692
|
-
* Checks for
|
|
1948
|
+
* Checks for:
|
|
1949
|
+
* 1. Paths in the index that no longer have wrangler config (dir deleted/moved)
|
|
1950
|
+
* 2. Managed projects where the cloud project no longer exists (orphaned links)
|
|
1693
1951
|
* Returns total project count and stale entries with reasons.
|
|
1694
1952
|
*/
|
|
1695
1953
|
export async function scanStaleProjects(): Promise<StaleProjectScan> {
|
|
@@ -1698,31 +1956,61 @@ export async function scanStaleProjects(): Promise<StaleProjectScan> {
|
|
|
1698
1956
|
const stale: StaleProject[] = [];
|
|
1699
1957
|
let totalPaths = 0;
|
|
1700
1958
|
|
|
1959
|
+
// Get list of valid managed project IDs (if logged in)
|
|
1960
|
+
let validManagedIds: Set<string> = new Set();
|
|
1961
|
+
try {
|
|
1962
|
+
const { isLoggedIn } = await import("./auth/store.ts");
|
|
1963
|
+
if (await isLoggedIn()) {
|
|
1964
|
+
const managedProjects = await listManagedProjects();
|
|
1965
|
+
validManagedIds = new Set(managedProjects.map((p) => p.id));
|
|
1966
|
+
}
|
|
1967
|
+
} catch {
|
|
1968
|
+
// Control plane unavailable, skip orphan detection
|
|
1969
|
+
}
|
|
1970
|
+
|
|
1701
1971
|
for (const projectId of projectIds) {
|
|
1702
1972
|
const paths = allPaths[projectId] || [];
|
|
1703
1973
|
totalPaths += paths.length;
|
|
1704
1974
|
|
|
1705
1975
|
for (const projectPath of paths) {
|
|
1706
|
-
// Check if path exists and has valid
|
|
1976
|
+
// Check if path exists and has valid wrangler config
|
|
1707
1977
|
const hasWranglerConfig =
|
|
1708
1978
|
existsSync(join(projectPath, "wrangler.jsonc")) ||
|
|
1709
1979
|
existsSync(join(projectPath, "wrangler.toml")) ||
|
|
1710
1980
|
existsSync(join(projectPath, "wrangler.json"));
|
|
1711
1981
|
|
|
1712
1982
|
if (!hasWranglerConfig) {
|
|
1713
|
-
//
|
|
1714
|
-
|
|
1715
|
-
try {
|
|
1716
|
-
name = await getProjectNameFromDir(projectPath);
|
|
1717
|
-
} catch {
|
|
1718
|
-
// Use path basename as fallback
|
|
1719
|
-
}
|
|
1720
|
-
|
|
1983
|
+
// Type 1: No wrangler config at path (dir deleted/moved)
|
|
1984
|
+
const name = projectPath.split("/").pop() || projectId;
|
|
1721
1985
|
stale.push({
|
|
1722
1986
|
name,
|
|
1723
|
-
reason: "
|
|
1987
|
+
reason: "directory missing or no wrangler config",
|
|
1724
1988
|
workerUrl: null,
|
|
1725
1989
|
});
|
|
1990
|
+
continue;
|
|
1991
|
+
}
|
|
1992
|
+
|
|
1993
|
+
// Check for Type 2: Managed project link pointing to deleted cloud project
|
|
1994
|
+
try {
|
|
1995
|
+
const link = await readProjectLink(projectPath);
|
|
1996
|
+
if (link?.deploy_mode === "managed" && validManagedIds.size > 0) {
|
|
1997
|
+
if (!validManagedIds.has(link.project_id)) {
|
|
1998
|
+
// Orphaned managed link - cloud project doesn't exist
|
|
1999
|
+
let name = projectPath.split("/").pop() || projectId;
|
|
2000
|
+
try {
|
|
2001
|
+
name = await getProjectNameFromDir(projectPath);
|
|
2002
|
+
} catch {
|
|
2003
|
+
// Use path basename as fallback
|
|
2004
|
+
}
|
|
2005
|
+
stale.push({
|
|
2006
|
+
name,
|
|
2007
|
+
reason: "cloud project deleted",
|
|
2008
|
+
workerUrl: null,
|
|
2009
|
+
});
|
|
2010
|
+
}
|
|
2011
|
+
}
|
|
2012
|
+
} catch {
|
|
2013
|
+
// Can't read link, skip
|
|
1726
2014
|
}
|
|
1727
2015
|
}
|
|
1728
2016
|
}
|