@getjack/jack 0.1.6 → 0.1.8
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/package.json +6 -2
- package/src/commands/down.ts +20 -3
- package/src/commands/mcp.ts +17 -1
- package/src/commands/publish.ts +50 -0
- package/src/commands/ship.ts +4 -3
- package/src/index.ts +7 -0
- package/src/lib/agent-files.ts +0 -2
- package/src/lib/binding-validator.ts +9 -4
- package/src/lib/build-helper.ts +67 -45
- package/src/lib/config-generator.ts +120 -0
- package/src/lib/config.ts +2 -1
- package/src/lib/control-plane.ts +61 -0
- package/src/lib/managed-deploy.ts +10 -2
- package/src/lib/mcp-config.ts +2 -1
- package/src/lib/output.ts +21 -1
- package/src/lib/project-detection.ts +431 -0
- package/src/lib/project-link.test.ts +4 -5
- package/src/lib/project-link.ts +5 -3
- package/src/lib/project-operations.ts +334 -35
- package/src/lib/project-resolver.ts +9 -2
- package/src/lib/secrets.ts +1 -2
- package/src/lib/storage/file-filter.ts +5 -0
- package/src/lib/telemetry-config.ts +3 -3
- package/src/lib/telemetry.ts +4 -0
- package/src/lib/zip-packager.ts +8 -0
- package/src/mcp/test-utils.ts +112 -0
- package/src/templates/index.ts +137 -7
- package/templates/nextjs/.jack.json +26 -26
- package/templates/nextjs/app/globals.css +4 -4
- package/templates/nextjs/app/layout.tsx +11 -11
- package/templates/nextjs/app/page.tsx +8 -6
- package/templates/nextjs/cloudflare-env.d.ts +1 -1
- package/templates/nextjs/next.config.ts +1 -1
- package/templates/nextjs/open-next.config.ts +1 -1
- package/templates/nextjs/package.json +22 -22
- package/templates/nextjs/tsconfig.json +26 -42
- package/templates/nextjs/wrangler.jsonc +15 -15
- package/src/lib/github.ts +0 -151
|
@@ -23,8 +23,22 @@ import {
|
|
|
23
23
|
runAgentOneShot,
|
|
24
24
|
validateAgentPaths,
|
|
25
25
|
} from "./agents.ts";
|
|
26
|
-
import {
|
|
26
|
+
import {
|
|
27
|
+
checkWranglerVersion,
|
|
28
|
+
getWranglerVersion,
|
|
29
|
+
needsOpenNextBuild,
|
|
30
|
+
needsViteBuild,
|
|
31
|
+
parseWranglerConfig,
|
|
32
|
+
runOpenNextBuild,
|
|
33
|
+
runViteBuild,
|
|
34
|
+
} from "./build-helper.ts";
|
|
27
35
|
import { checkWorkerExists, getAccountId, listD1Databases } from "./cloudflare-api.ts";
|
|
36
|
+
import {
|
|
37
|
+
generateWranglerConfig,
|
|
38
|
+
getDefaultProjectName,
|
|
39
|
+
slugify,
|
|
40
|
+
writeWranglerConfig,
|
|
41
|
+
} from "./config-generator.ts";
|
|
28
42
|
import { getSyncConfig } from "./config.ts";
|
|
29
43
|
import { deleteManagedProject } from "./control-plane.ts";
|
|
30
44
|
import { debug, isDebug } from "./debug.ts";
|
|
@@ -40,6 +54,7 @@ import {
|
|
|
40
54
|
} from "./managed-deploy.ts";
|
|
41
55
|
import { generateProjectName } from "./names.ts";
|
|
42
56
|
import { getAllPaths, registerPath, unregisterPath } from "./paths-index.ts";
|
|
57
|
+
import { detectProjectType, validateProject } from "./project-detection.ts";
|
|
43
58
|
import {
|
|
44
59
|
type DeployMode,
|
|
45
60
|
type TemplateMetadata as TemplateOrigin,
|
|
@@ -83,6 +98,7 @@ export interface DeployOptions {
|
|
|
83
98
|
includeSync?: boolean;
|
|
84
99
|
managed?: boolean; // Force managed deploy mode
|
|
85
100
|
byo?: boolean; // Force BYO deploy mode
|
|
101
|
+
dryRun?: boolean; // Stop before actual deployment
|
|
86
102
|
}
|
|
87
103
|
|
|
88
104
|
export interface DeployResult {
|
|
@@ -293,6 +309,213 @@ async function preflightD1Capacity(
|
|
|
293
309
|
}
|
|
294
310
|
}
|
|
295
311
|
|
|
312
|
+
// ============================================================================
|
|
313
|
+
// Auto-detect Flow for Ship Command
|
|
314
|
+
// ============================================================================
|
|
315
|
+
|
|
316
|
+
interface AutoDetectResult {
|
|
317
|
+
projectName: string;
|
|
318
|
+
projectId: string | null; // null when dry run (no cloud project created)
|
|
319
|
+
deployMode: DeployMode;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Run the auto-detect flow when no wrangler config exists.
|
|
324
|
+
* Detects project type, prompts user for confirmation, generates config,
|
|
325
|
+
* and creates managed project on jack cloud.
|
|
326
|
+
*
|
|
327
|
+
* @param dryRun - If true, skip cloud project creation and linking
|
|
328
|
+
*/
|
|
329
|
+
async function runAutoDetectFlow(
|
|
330
|
+
projectPath: string,
|
|
331
|
+
reporter: OperationReporter,
|
|
332
|
+
interactive: boolean,
|
|
333
|
+
dryRun = false,
|
|
334
|
+
): Promise<AutoDetectResult> {
|
|
335
|
+
// Step 1: Validate project (file count, size limits)
|
|
336
|
+
const validation = await validateProject(projectPath);
|
|
337
|
+
if (!validation.valid) {
|
|
338
|
+
track(Events.AUTO_DETECT_REJECTED, { reason: "validation_failed" });
|
|
339
|
+
throw new JackError(
|
|
340
|
+
JackErrorCode.VALIDATION_ERROR,
|
|
341
|
+
validation.error || "Project validation failed",
|
|
342
|
+
);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// Step 2: Detect project type
|
|
346
|
+
const detection = detectProjectType(projectPath);
|
|
347
|
+
|
|
348
|
+
// Step 3: Handle unsupported frameworks
|
|
349
|
+
if (detection.unsupportedFramework) {
|
|
350
|
+
track(Events.AUTO_DETECT_FAILED, {
|
|
351
|
+
reason: "unsupported_framework",
|
|
352
|
+
framework: detection.unsupportedFramework,
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
// Use the detailed error message from detection (includes setup instructions)
|
|
356
|
+
throw new JackError(
|
|
357
|
+
JackErrorCode.VALIDATION_ERROR,
|
|
358
|
+
detection.error || `${detection.unsupportedFramework} is not yet supported`,
|
|
359
|
+
);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// Step 4: Handle unknown project type
|
|
363
|
+
if (detection.type === "unknown") {
|
|
364
|
+
track(Events.AUTO_DETECT_FAILED, { reason: "unknown_type" });
|
|
365
|
+
|
|
366
|
+
throw new JackError(
|
|
367
|
+
JackErrorCode.VALIDATION_ERROR,
|
|
368
|
+
"Could not detect project type\n\nSupported types:\n - Vite (React, Vue, etc.)\n - Hono API\n - SvelteKit (with @sveltejs/adapter-cloudflare)\n\nTo deploy manually, create a wrangler.jsonc file.\nDocs: https://docs.getjack.org/guides/manual-setup",
|
|
369
|
+
);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// Step 5: Handle detection errors (e.g., missing adapter)
|
|
373
|
+
if (detection.error) {
|
|
374
|
+
track(Events.AUTO_DETECT_FAILED, {
|
|
375
|
+
reason: "detection_error",
|
|
376
|
+
type: detection.type,
|
|
377
|
+
});
|
|
378
|
+
throw new JackError(JackErrorCode.VALIDATION_ERROR, detection.error);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// Step 6: Detection succeeded - show what was detected
|
|
382
|
+
const typeLabels: Record<string, string> = {
|
|
383
|
+
vite: "Vite",
|
|
384
|
+
hono: "Hono API",
|
|
385
|
+
sveltekit: "SvelteKit",
|
|
386
|
+
};
|
|
387
|
+
const typeLabel = typeLabels[detection.type] || detection.type;
|
|
388
|
+
const configInfo = detection.configFile || detection.entryPoint || "";
|
|
389
|
+
reporter.info(`Detected: ${typeLabel} project${configInfo ? ` (${configInfo})` : ""}`);
|
|
390
|
+
|
|
391
|
+
// Step 7: Fetch username for URL preview (skip for dry run)
|
|
392
|
+
let ownerUsername: string | null = null;
|
|
393
|
+
if (!dryRun) {
|
|
394
|
+
const { getCurrentUserProfile } = await import("./control-plane.ts");
|
|
395
|
+
const profile = await getCurrentUserProfile();
|
|
396
|
+
ownerUsername = profile?.username ?? null;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// Step 8: Get default project name and prompt user
|
|
400
|
+
const defaultName = getDefaultProjectName(projectPath);
|
|
401
|
+
|
|
402
|
+
if (!interactive) {
|
|
403
|
+
// Non-interactive mode - use defaults
|
|
404
|
+
const projectName = defaultName;
|
|
405
|
+
const runjackUrl = ownerUsername
|
|
406
|
+
? `https://${ownerUsername}-${projectName}.runjack.xyz`
|
|
407
|
+
: `https://${projectName}.runjack.xyz`;
|
|
408
|
+
|
|
409
|
+
reporter.info(`Project name: ${projectName}`);
|
|
410
|
+
reporter.info(`Will deploy to: ${runjackUrl}`);
|
|
411
|
+
|
|
412
|
+
// Generate and write wrangler config
|
|
413
|
+
const wranglerConfig = generateWranglerConfig(
|
|
414
|
+
detection.type,
|
|
415
|
+
projectName,
|
|
416
|
+
detection.entryPoint,
|
|
417
|
+
);
|
|
418
|
+
writeWranglerConfig(projectPath, wranglerConfig);
|
|
419
|
+
reporter.success("Created wrangler.jsonc");
|
|
420
|
+
|
|
421
|
+
// Skip cloud creation and linking for dry run
|
|
422
|
+
if (dryRun) {
|
|
423
|
+
track(Events.AUTO_DETECT_SUCCESS, { type: detection.type });
|
|
424
|
+
return {
|
|
425
|
+
projectName,
|
|
426
|
+
projectId: null,
|
|
427
|
+
deployMode: "managed",
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// Create managed project on jack cloud
|
|
432
|
+
const remoteResult = await createManagedProjectRemote(projectName, reporter, {
|
|
433
|
+
usePrebuilt: false,
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
// Link project locally (include username for correct URL display)
|
|
437
|
+
await linkProject(projectPath, remoteResult.projectId, "managed", ownerUsername ?? undefined);
|
|
438
|
+
await registerPath(remoteResult.projectId, projectPath);
|
|
439
|
+
|
|
440
|
+
track(Events.AUTO_DETECT_SUCCESS, { type: detection.type });
|
|
441
|
+
|
|
442
|
+
return {
|
|
443
|
+
projectName,
|
|
444
|
+
projectId: remoteResult.projectId,
|
|
445
|
+
deployMode: "managed",
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// Interactive mode - prompt for project name
|
|
450
|
+
const { input } = await import("@inquirer/prompts");
|
|
451
|
+
|
|
452
|
+
console.error("");
|
|
453
|
+
const projectName = await input({
|
|
454
|
+
message: "Project name:",
|
|
455
|
+
default: defaultName,
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
const slugifiedName = slugify(projectName.trim());
|
|
459
|
+
const runjackUrl = ownerUsername
|
|
460
|
+
? `https://${ownerUsername}-${slugifiedName}.runjack.xyz`
|
|
461
|
+
: `https://${slugifiedName}.runjack.xyz`;
|
|
462
|
+
|
|
463
|
+
// Confirmation prompt
|
|
464
|
+
console.error("");
|
|
465
|
+
console.error(" This will:");
|
|
466
|
+
console.error(" - Create wrangler.jsonc");
|
|
467
|
+
console.error(" - Create project on jack cloud");
|
|
468
|
+
console.error(` - Deploy to ${runjackUrl}`);
|
|
469
|
+
console.error("");
|
|
470
|
+
|
|
471
|
+
const { promptSelect } = await import("./hooks.ts");
|
|
472
|
+
const choice = await promptSelect(["Continue", "Cancel"]);
|
|
473
|
+
|
|
474
|
+
if (choice !== 0) {
|
|
475
|
+
track(Events.AUTO_DETECT_REJECTED, { reason: "user_cancelled" });
|
|
476
|
+
throw new JackError(JackErrorCode.VALIDATION_ERROR, "Deployment cancelled", undefined, {
|
|
477
|
+
exitCode: 0,
|
|
478
|
+
reported: true,
|
|
479
|
+
});
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// Generate and write wrangler config
|
|
483
|
+
const wranglerConfig = generateWranglerConfig(
|
|
484
|
+
detection.type,
|
|
485
|
+
slugifiedName,
|
|
486
|
+
detection.entryPoint,
|
|
487
|
+
);
|
|
488
|
+
writeWranglerConfig(projectPath, wranglerConfig);
|
|
489
|
+
reporter.success("Created wrangler.jsonc");
|
|
490
|
+
|
|
491
|
+
// Skip cloud creation and linking for dry run
|
|
492
|
+
if (dryRun) {
|
|
493
|
+
track(Events.AUTO_DETECT_SUCCESS, { type: detection.type });
|
|
494
|
+
return {
|
|
495
|
+
projectName: slugifiedName,
|
|
496
|
+
projectId: null,
|
|
497
|
+
deployMode: "managed",
|
|
498
|
+
};
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// Create managed project on jack cloud
|
|
502
|
+
const remoteResult = await createManagedProjectRemote(slugifiedName, reporter, {
|
|
503
|
+
usePrebuilt: false,
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
// Link project locally (include username for correct URL display)
|
|
507
|
+
await linkProject(projectPath, remoteResult.projectId, "managed", ownerUsername ?? undefined);
|
|
508
|
+
await registerPath(remoteResult.projectId, projectPath);
|
|
509
|
+
|
|
510
|
+
track(Events.AUTO_DETECT_SUCCESS, { type: detection.type });
|
|
511
|
+
|
|
512
|
+
return {
|
|
513
|
+
projectName: slugifiedName,
|
|
514
|
+
projectId: remoteResult.projectId,
|
|
515
|
+
deployMode: "managed",
|
|
516
|
+
};
|
|
517
|
+
}
|
|
518
|
+
|
|
296
519
|
// ============================================================================
|
|
297
520
|
// Create Project Operation
|
|
298
521
|
// ============================================================================
|
|
@@ -674,7 +897,7 @@ export async function createProject(
|
|
|
674
897
|
try {
|
|
675
898
|
const result = await runParallelSetup(targetDir, projectName, {
|
|
676
899
|
template: resolvedTemplate || "hello",
|
|
677
|
-
usePrebuilt:
|
|
900
|
+
usePrebuilt: templateOrigin.type === "builtin", // Only builtin templates have prebuilt bundles
|
|
678
901
|
});
|
|
679
902
|
remoteResult = result.remoteResult;
|
|
680
903
|
reporter.stop();
|
|
@@ -787,9 +1010,14 @@ export async function createProject(
|
|
|
787
1010
|
);
|
|
788
1011
|
}
|
|
789
1012
|
|
|
1013
|
+
// Fetch username for link storage
|
|
1014
|
+
const { getCurrentUserProfile } = await import("./control-plane.ts");
|
|
1015
|
+
const profile = await getCurrentUserProfile();
|
|
1016
|
+
const ownerUsername = profile?.username ?? undefined;
|
|
1017
|
+
|
|
790
1018
|
// Link project locally and register path
|
|
791
1019
|
try {
|
|
792
|
-
await linkProject(targetDir, remoteResult.projectId, "managed");
|
|
1020
|
+
await linkProject(targetDir, remoteResult.projectId, "managed", ownerUsername);
|
|
793
1021
|
await writeTemplateMetadata(targetDir, templateOrigin);
|
|
794
1022
|
await registerPath(remoteResult.projectId, targetDir);
|
|
795
1023
|
} catch (err) {
|
|
@@ -819,22 +1047,22 @@ export async function createProject(
|
|
|
819
1047
|
|
|
820
1048
|
// Build first if needed (wrangler needs built assets)
|
|
821
1049
|
if (await needsOpenNextBuild(targetDir)) {
|
|
822
|
-
reporter.start("Building...");
|
|
1050
|
+
reporter.start("Building assets...");
|
|
823
1051
|
try {
|
|
824
1052
|
await runOpenNextBuild(targetDir);
|
|
825
1053
|
reporter.stop();
|
|
826
|
-
reporter.success("Built");
|
|
1054
|
+
reporter.success("Built assets");
|
|
827
1055
|
} catch (err) {
|
|
828
1056
|
reporter.stop();
|
|
829
1057
|
reporter.error("Build failed");
|
|
830
1058
|
throw err;
|
|
831
1059
|
}
|
|
832
1060
|
} else if (await needsViteBuild(targetDir)) {
|
|
833
|
-
reporter.start("Building...");
|
|
1061
|
+
reporter.start("Building assets...");
|
|
834
1062
|
try {
|
|
835
1063
|
await runViteBuild(targetDir);
|
|
836
1064
|
reporter.stop();
|
|
837
|
-
reporter.success("Built");
|
|
1065
|
+
reporter.success("Built assets");
|
|
838
1066
|
} catch (err) {
|
|
839
1067
|
reporter.stop();
|
|
840
1068
|
reporter.error("Build failed");
|
|
@@ -957,6 +1185,7 @@ export async function deployProject(options: DeployOptions = {}): Promise<Deploy
|
|
|
957
1185
|
interactive: interactiveOption,
|
|
958
1186
|
includeSecrets = false,
|
|
959
1187
|
includeSync = false,
|
|
1188
|
+
dryRun = false,
|
|
960
1189
|
} = options;
|
|
961
1190
|
const reporter = providedReporter ?? noopReporter;
|
|
962
1191
|
const hasReporter = Boolean(providedReporter);
|
|
@@ -974,7 +1203,14 @@ export async function deployProject(options: DeployOptions = {}): Promise<Deploy
|
|
|
974
1203
|
existsSync(join(projectPath, "wrangler.jsonc")) ||
|
|
975
1204
|
existsSync(join(projectPath, "wrangler.json"));
|
|
976
1205
|
|
|
977
|
-
|
|
1206
|
+
// Check for existing project link
|
|
1207
|
+
const hasProjectLink = existsSync(join(projectPath, ".jack", "project.json"));
|
|
1208
|
+
|
|
1209
|
+
// Auto-detect flow: no wrangler config and no project link
|
|
1210
|
+
let autoDetectResult: AutoDetectResult | null = null;
|
|
1211
|
+
if (!hasWranglerConfig && !hasProjectLink) {
|
|
1212
|
+
autoDetectResult = await runAutoDetectFlow(projectPath, reporter, interactive, dryRun);
|
|
1213
|
+
} else if (!hasWranglerConfig) {
|
|
978
1214
|
throw new JackError(
|
|
979
1215
|
JackErrorCode.PROJECT_NOT_FOUND,
|
|
980
1216
|
"No wrangler config found in current directory",
|
|
@@ -982,26 +1218,30 @@ export async function deployProject(options: DeployOptions = {}): Promise<Deploy
|
|
|
982
1218
|
);
|
|
983
1219
|
}
|
|
984
1220
|
|
|
985
|
-
// Get project name from directory
|
|
986
|
-
const projectName = await getProjectNameFromDir(projectPath);
|
|
1221
|
+
// Get project name from directory (or auto-detect result)
|
|
1222
|
+
const projectName = autoDetectResult?.projectName ?? (await getProjectNameFromDir(projectPath));
|
|
987
1223
|
|
|
988
1224
|
// Read local project link for stored mode and project ID
|
|
989
1225
|
const link = await readProjectLink(projectPath);
|
|
990
1226
|
|
|
991
|
-
// Determine effective mode: explicit flag > stored mode > default BYO
|
|
1227
|
+
// Determine effective mode: explicit flag > auto-detect > stored mode > default BYO
|
|
992
1228
|
let deployMode: DeployMode;
|
|
993
1229
|
if (options.managed) {
|
|
994
1230
|
deployMode = "managed";
|
|
995
1231
|
} else if (options.byo) {
|
|
996
1232
|
deployMode = "byo";
|
|
1233
|
+
} else if (autoDetectResult) {
|
|
1234
|
+
deployMode = autoDetectResult.deployMode;
|
|
997
1235
|
} else {
|
|
998
1236
|
deployMode = link?.deploy_mode ?? "byo";
|
|
999
1237
|
}
|
|
1000
1238
|
|
|
1001
1239
|
// Validate mode availability
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1240
|
+
if (!dryRun) {
|
|
1241
|
+
const modeError = await validateModeAvailability(deployMode);
|
|
1242
|
+
if (modeError) {
|
|
1243
|
+
throw new JackError(JackErrorCode.VALIDATION_ERROR, modeError);
|
|
1244
|
+
}
|
|
1005
1245
|
}
|
|
1006
1246
|
|
|
1007
1247
|
let workerUrl: string | null = null;
|
|
@@ -1010,7 +1250,11 @@ export async function deployProject(options: DeployOptions = {}): Promise<Deploy
|
|
|
1010
1250
|
// Deploy based on mode
|
|
1011
1251
|
if (deployMode === "managed") {
|
|
1012
1252
|
// Managed mode: deploy via jack cloud
|
|
1013
|
-
if
|
|
1253
|
+
// Use autoDetectResult.projectId if available, otherwise require existing link
|
|
1254
|
+
const managedProjectId = autoDetectResult?.projectId ?? link?.project_id;
|
|
1255
|
+
|
|
1256
|
+
// For dry run, skip the project ID check since we didn't create a cloud project
|
|
1257
|
+
if (!dryRun && (!managedProjectId || (!autoDetectResult && link?.deploy_mode !== "managed"))) {
|
|
1014
1258
|
throw new JackError(
|
|
1015
1259
|
JackErrorCode.VALIDATION_ERROR,
|
|
1016
1260
|
"Project not linked to jack cloud",
|
|
@@ -1018,44 +1262,93 @@ export async function deployProject(options: DeployOptions = {}): Promise<Deploy
|
|
|
1018
1262
|
);
|
|
1019
1263
|
}
|
|
1020
1264
|
|
|
1265
|
+
// Dry run: build for validation then stop before actual deployment
|
|
1266
|
+
// (deployToManagedProject handles its own build, so only build here for dry-run)
|
|
1267
|
+
if (dryRun) {
|
|
1268
|
+
if (await needsOpenNextBuild(projectPath)) {
|
|
1269
|
+
const buildSpin = reporter.spinner("Building assets...");
|
|
1270
|
+
try {
|
|
1271
|
+
await runOpenNextBuild(projectPath);
|
|
1272
|
+
buildSpin.success("Built assets");
|
|
1273
|
+
} catch (err) {
|
|
1274
|
+
buildSpin.error("Build failed");
|
|
1275
|
+
throw err;
|
|
1276
|
+
}
|
|
1277
|
+
} else if (await needsViteBuild(projectPath)) {
|
|
1278
|
+
const buildSpin = reporter.spinner("Building assets...");
|
|
1279
|
+
try {
|
|
1280
|
+
await runViteBuild(projectPath);
|
|
1281
|
+
buildSpin.success("Built assets");
|
|
1282
|
+
} catch (err) {
|
|
1283
|
+
buildSpin.error("Build failed");
|
|
1284
|
+
throw err;
|
|
1285
|
+
}
|
|
1286
|
+
}
|
|
1287
|
+
reporter.success("Dry run complete - config generated, build verified");
|
|
1288
|
+
return {
|
|
1289
|
+
workerUrl: null,
|
|
1290
|
+
projectName,
|
|
1291
|
+
deployMode,
|
|
1292
|
+
};
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1021
1295
|
// deployToManagedProject now handles both template and code deploy
|
|
1022
|
-
await deployToManagedProject(
|
|
1296
|
+
await deployToManagedProject(managedProjectId as string, projectPath, reporter);
|
|
1023
1297
|
|
|
1024
|
-
//
|
|
1025
|
-
workerUrl =
|
|
1298
|
+
// Construct URL with username if available
|
|
1299
|
+
workerUrl = link?.owner_username
|
|
1300
|
+
? `https://${link.owner_username}-${projectName}.runjack.xyz`
|
|
1301
|
+
: `https://${projectName}.runjack.xyz`;
|
|
1026
1302
|
} else {
|
|
1027
1303
|
// BYO mode: deploy via wrangler
|
|
1028
1304
|
|
|
1029
1305
|
// Build first if needed (wrangler needs built assets)
|
|
1030
1306
|
if (await needsOpenNextBuild(projectPath)) {
|
|
1031
|
-
const buildSpin = reporter.spinner("Building...");
|
|
1307
|
+
const buildSpin = reporter.spinner("Building assets...");
|
|
1032
1308
|
try {
|
|
1033
1309
|
await runOpenNextBuild(projectPath);
|
|
1034
|
-
buildSpin.success("Built");
|
|
1310
|
+
buildSpin.success("Built assets");
|
|
1035
1311
|
} catch (err) {
|
|
1036
1312
|
buildSpin.error("Build failed");
|
|
1037
1313
|
throw err;
|
|
1038
1314
|
}
|
|
1039
1315
|
} else if (await needsViteBuild(projectPath)) {
|
|
1040
|
-
const buildSpin = reporter.spinner("Building...");
|
|
1316
|
+
const buildSpin = reporter.spinner("Building assets...");
|
|
1041
1317
|
try {
|
|
1042
1318
|
await runViteBuild(projectPath);
|
|
1043
|
-
buildSpin.success("Built");
|
|
1319
|
+
buildSpin.success("Built assets");
|
|
1044
1320
|
} catch (err) {
|
|
1045
1321
|
buildSpin.error("Build failed");
|
|
1046
1322
|
throw err;
|
|
1047
1323
|
}
|
|
1048
1324
|
}
|
|
1049
1325
|
|
|
1050
|
-
//
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1326
|
+
// Dry run: stop before actual deployment
|
|
1327
|
+
if (dryRun) {
|
|
1328
|
+
reporter.success("Dry run complete - build verified");
|
|
1329
|
+
return {
|
|
1330
|
+
workerUrl: null,
|
|
1331
|
+
projectName,
|
|
1332
|
+
deployMode,
|
|
1333
|
+
};
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
// Check wrangler version for auto-provisioning (KV/R2/D1 without IDs)
|
|
1337
|
+
const config = await parseWranglerConfig(projectPath);
|
|
1338
|
+
const needsAutoProvision =
|
|
1339
|
+
config.kv_namespaces?.some((kv) => !kv.id) ||
|
|
1340
|
+
config.r2_buckets?.some((r2) => r2.bucket_name?.startsWith("jack-template-")) ||
|
|
1341
|
+
config.d1_databases?.some((d1) => !d1.database_id);
|
|
1342
|
+
|
|
1343
|
+
if (needsAutoProvision) {
|
|
1344
|
+
try {
|
|
1345
|
+
const wranglerVersion = await getWranglerVersion();
|
|
1346
|
+
checkWranglerVersion(wranglerVersion);
|
|
1347
|
+
} catch (err) {
|
|
1348
|
+
if (err instanceof JackError) {
|
|
1349
|
+
throw err;
|
|
1350
|
+
}
|
|
1055
1351
|
}
|
|
1056
|
-
} catch (err) {
|
|
1057
|
-
// Non-fatal: let wrangler deploy fail with a clearer error if bucket is missing
|
|
1058
|
-
debug("R2 preflight failed:", err);
|
|
1059
1352
|
}
|
|
1060
1353
|
|
|
1061
1354
|
const spin = reporter.spinner("Deploying...");
|
|
@@ -1105,16 +1398,16 @@ export async function deployProject(options: DeployOptions = {}): Promise<Deploy
|
|
|
1105
1398
|
}
|
|
1106
1399
|
}
|
|
1107
1400
|
|
|
1108
|
-
if (includeSync) {
|
|
1401
|
+
if (includeSync && deployMode !== "byo") {
|
|
1109
1402
|
const syncConfig = await getSyncConfig();
|
|
1110
1403
|
if (syncConfig.enabled && syncConfig.autoSync) {
|
|
1111
|
-
const syncSpin = reporter.spinner("Syncing source to
|
|
1404
|
+
const syncSpin = reporter.spinner("Syncing source to jack storage...");
|
|
1112
1405
|
try {
|
|
1113
1406
|
const syncResult = await syncToCloud(projectPath);
|
|
1114
1407
|
if (syncResult.success) {
|
|
1115
1408
|
if (syncResult.filesUploaded > 0 || syncResult.filesDeleted > 0) {
|
|
1116
1409
|
syncSpin.success(
|
|
1117
|
-
`
|
|
1410
|
+
`Synced source to jack storage (${syncResult.filesUploaded} uploaded, ${syncResult.filesDeleted} removed)`,
|
|
1118
1411
|
);
|
|
1119
1412
|
} else {
|
|
1120
1413
|
syncSpin.success("Source already synced");
|
|
@@ -1134,7 +1427,11 @@ export async function deployProject(options: DeployOptions = {}): Promise<Deploy
|
|
|
1134
1427
|
if (!existingLink) {
|
|
1135
1428
|
// Not linked yet - create link
|
|
1136
1429
|
if (deployMode === "managed" && link?.project_id) {
|
|
1137
|
-
|
|
1430
|
+
// Fetch username for link storage
|
|
1431
|
+
const { getCurrentUserProfile } = await import("./control-plane.ts");
|
|
1432
|
+
const profile = await getCurrentUserProfile();
|
|
1433
|
+
const ownerUsername = profile?.username ?? undefined;
|
|
1434
|
+
await linkProject(projectPath, link.project_id, "managed", ownerUsername);
|
|
1138
1435
|
await registerPath(link.project_id, projectPath);
|
|
1139
1436
|
} else {
|
|
1140
1437
|
// BYO mode - generate new ID
|
|
@@ -1216,7 +1513,9 @@ export async function getProjectStatus(
|
|
|
1216
1513
|
// Determine URL based on mode
|
|
1217
1514
|
let workerUrl: string | null = null;
|
|
1218
1515
|
if (link?.deploy_mode === "managed") {
|
|
1219
|
-
workerUrl =
|
|
1516
|
+
workerUrl = link.owner_username
|
|
1517
|
+
? `https://${link.owner_username}-${projectName}.runjack.xyz`
|
|
1518
|
+
: `https://${projectName}.runjack.xyz`;
|
|
1220
1519
|
}
|
|
1221
1520
|
|
|
1222
1521
|
// Get database name on-demand
|
|
@@ -121,11 +121,15 @@ function fromManagedProject(managed: ManagedProject): ResolvedProject {
|
|
|
121
121
|
}
|
|
122
122
|
}
|
|
123
123
|
|
|
124
|
+
const url = managed.owner_username
|
|
125
|
+
? `https://${managed.owner_username}-${managed.slug}.runjack.xyz`
|
|
126
|
+
: `https://${managed.slug}.runjack.xyz`;
|
|
127
|
+
|
|
124
128
|
return {
|
|
125
129
|
name: managed.name,
|
|
126
130
|
slug: managed.slug,
|
|
127
131
|
status,
|
|
128
|
-
url
|
|
132
|
+
url,
|
|
129
133
|
errorMessage: managed.status === "error" ? "deployment failed" : undefined,
|
|
130
134
|
sources: {
|
|
131
135
|
controlPlane: true,
|
|
@@ -175,6 +179,8 @@ export interface ResolveProjectOptions {
|
|
|
175
179
|
projectPath?: string;
|
|
176
180
|
/** Allow fallback lookup by managed project name when slug lookup fails */
|
|
177
181
|
matchByName?: boolean;
|
|
182
|
+
/** Prefer local .jack/project.json when resolving (default true) */
|
|
183
|
+
preferLocalLink?: boolean;
|
|
178
184
|
}
|
|
179
185
|
|
|
180
186
|
/**
|
|
@@ -221,9 +227,10 @@ export async function resolveProject(
|
|
|
221
227
|
let resolved: ResolvedProject | null = null;
|
|
222
228
|
const matchByName = options?.matchByName !== false;
|
|
223
229
|
const projectPath = options?.projectPath || process.cwd();
|
|
230
|
+
const preferLocalLink = options?.preferLocalLink ?? true;
|
|
224
231
|
|
|
225
232
|
// First, check if we're resolving from a local path with .jack/project.json
|
|
226
|
-
const link = await readProjectLink(projectPath);
|
|
233
|
+
const link = preferLocalLink ? await readProjectLink(projectPath) : null;
|
|
227
234
|
|
|
228
235
|
if (link) {
|
|
229
236
|
// We have a local link - start with that
|
package/src/lib/secrets.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
2
|
import { chmod, mkdir, stat } from "node:fs/promises";
|
|
3
|
-
import { homedir } from "node:os";
|
|
4
3
|
import { join } from "node:path";
|
|
4
|
+
import { CONFIG_DIR } from "./config.ts";
|
|
5
5
|
|
|
6
6
|
export interface SecretEntry {
|
|
7
7
|
value: string;
|
|
@@ -14,7 +14,6 @@ export interface SecretsFile {
|
|
|
14
14
|
secrets: Record<string, SecretEntry>;
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
const CONFIG_DIR = join(homedir(), ".config", "jack");
|
|
18
17
|
const SECRETS_PATH = join(CONFIG_DIR, "secrets.json");
|
|
19
18
|
|
|
20
19
|
/**
|
|
@@ -25,6 +25,7 @@ export const DEFAULT_INCLUDES: string[] = [
|
|
|
25
25
|
"*.mjs",
|
|
26
26
|
"*.cjs",
|
|
27
27
|
"*.json",
|
|
28
|
+
"*.jsonc",
|
|
28
29
|
"*.toml",
|
|
29
30
|
"*.md",
|
|
30
31
|
"*.css",
|
|
@@ -48,6 +49,10 @@ export const DEFAULT_EXCLUDES: string[] = [
|
|
|
48
49
|
".DS_Store",
|
|
49
50
|
"dist/**",
|
|
50
51
|
"build/**",
|
|
52
|
+
".next/**",
|
|
53
|
+
".nuxt/**",
|
|
54
|
+
".output/**",
|
|
55
|
+
".svelte-kit/**",
|
|
51
56
|
"coverage/**",
|
|
52
57
|
".wrangler/**",
|
|
53
58
|
"*.lock",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
2
|
import { mkdir } from "node:fs/promises";
|
|
3
|
-
import { homedir } from "node:os";
|
|
4
3
|
import { join } from "node:path";
|
|
4
|
+
import { CONFIG_DIR } from "./config.ts";
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Telemetry configuration structure
|
|
@@ -12,8 +12,8 @@ export interface TelemetryConfig {
|
|
|
12
12
|
version: number; // config schema version (start at 1)
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
export const TELEMETRY_CONFIG_DIR =
|
|
16
|
-
export const TELEMETRY_CONFIG_PATH = join(
|
|
15
|
+
export const TELEMETRY_CONFIG_DIR = CONFIG_DIR;
|
|
16
|
+
export const TELEMETRY_CONFIG_PATH = join(CONFIG_DIR, "telemetry.json");
|
|
17
17
|
|
|
18
18
|
/**
|
|
19
19
|
* Cached telemetry config for memoization
|
package/src/lib/telemetry.ts
CHANGED
|
@@ -28,6 +28,10 @@ export const Events = {
|
|
|
28
28
|
MANAGED_DEPLOY_STARTED: "managed_deploy_started",
|
|
29
29
|
MANAGED_DEPLOY_COMPLETED: "managed_deploy_completed",
|
|
30
30
|
MANAGED_DEPLOY_FAILED: "managed_deploy_failed",
|
|
31
|
+
// Auto-detect events
|
|
32
|
+
AUTO_DETECT_SUCCESS: "auto_detect_success",
|
|
33
|
+
AUTO_DETECT_FAILED: "auto_detect_failed",
|
|
34
|
+
AUTO_DETECT_REJECTED: "auto_detect_rejected",
|
|
31
35
|
} as const;
|
|
32
36
|
|
|
33
37
|
type EventName = (typeof Events)[keyof typeof Events];
|
package/src/lib/zip-packager.ts
CHANGED
|
@@ -42,6 +42,7 @@ export interface ManifestData {
|
|
|
42
42
|
};
|
|
43
43
|
vars?: Record<string, string>;
|
|
44
44
|
r2?: Array<{ binding: string; bucket_name: string }>;
|
|
45
|
+
kv?: Array<{ binding: string }>;
|
|
45
46
|
};
|
|
46
47
|
}
|
|
47
48
|
|
|
@@ -169,6 +170,13 @@ function extractBindingsFromConfig(config?: WranglerConfig): ManifestData["bindi
|
|
|
169
170
|
}));
|
|
170
171
|
}
|
|
171
172
|
|
|
173
|
+
// Extract KV namespace bindings
|
|
174
|
+
if (config.kv_namespaces && config.kv_namespaces.length > 0) {
|
|
175
|
+
bindings.kv = config.kv_namespaces.map((kv) => ({
|
|
176
|
+
binding: kv.binding,
|
|
177
|
+
}));
|
|
178
|
+
}
|
|
179
|
+
|
|
172
180
|
// Return undefined if no bindings were extracted
|
|
173
181
|
return Object.keys(bindings).length > 0 ? bindings : undefined;
|
|
174
182
|
}
|