@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.
Files changed (38) hide show
  1. package/package.json +6 -2
  2. package/src/commands/down.ts +20 -3
  3. package/src/commands/mcp.ts +17 -1
  4. package/src/commands/publish.ts +50 -0
  5. package/src/commands/ship.ts +4 -3
  6. package/src/index.ts +7 -0
  7. package/src/lib/agent-files.ts +0 -2
  8. package/src/lib/binding-validator.ts +9 -4
  9. package/src/lib/build-helper.ts +67 -45
  10. package/src/lib/config-generator.ts +120 -0
  11. package/src/lib/config.ts +2 -1
  12. package/src/lib/control-plane.ts +61 -0
  13. package/src/lib/managed-deploy.ts +10 -2
  14. package/src/lib/mcp-config.ts +2 -1
  15. package/src/lib/output.ts +21 -1
  16. package/src/lib/project-detection.ts +431 -0
  17. package/src/lib/project-link.test.ts +4 -5
  18. package/src/lib/project-link.ts +5 -3
  19. package/src/lib/project-operations.ts +334 -35
  20. package/src/lib/project-resolver.ts +9 -2
  21. package/src/lib/secrets.ts +1 -2
  22. package/src/lib/storage/file-filter.ts +5 -0
  23. package/src/lib/telemetry-config.ts +3 -3
  24. package/src/lib/telemetry.ts +4 -0
  25. package/src/lib/zip-packager.ts +8 -0
  26. package/src/mcp/test-utils.ts +112 -0
  27. package/src/templates/index.ts +137 -7
  28. package/templates/nextjs/.jack.json +26 -26
  29. package/templates/nextjs/app/globals.css +4 -4
  30. package/templates/nextjs/app/layout.tsx +11 -11
  31. package/templates/nextjs/app/page.tsx +8 -6
  32. package/templates/nextjs/cloudflare-env.d.ts +1 -1
  33. package/templates/nextjs/next.config.ts +1 -1
  34. package/templates/nextjs/open-next.config.ts +1 -1
  35. package/templates/nextjs/package.json +22 -22
  36. package/templates/nextjs/tsconfig.json +26 -42
  37. package/templates/nextjs/wrangler.jsonc +15 -15
  38. 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 { ensureR2Buckets, needsOpenNextBuild, needsViteBuild, runOpenNextBuild, runViteBuild } from "./build-helper.ts";
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: true,
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
- if (!hasWranglerConfig) {
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
- const modeError = await validateModeAvailability(deployMode);
1003
- if (modeError) {
1004
- throw new JackError(JackErrorCode.VALIDATION_ERROR, modeError);
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 (!link?.project_id || link.deploy_mode !== "managed") {
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(link.project_id, projectPath, reporter);
1296
+ await deployToManagedProject(managedProjectId as string, projectPath, reporter);
1023
1297
 
1024
- // Get the URL from the resolver or construct it
1025
- workerUrl = `https://${projectName}.runjack.xyz`;
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
- // Ensure R2 buckets exist before deploying (omakase - auto-provision)
1051
- try {
1052
- const buckets = await ensureR2Buckets(projectPath);
1053
- if (buckets.length > 0) {
1054
- reporter.info(`R2 buckets ready: ${buckets.join(", ")}`);
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 cloud...");
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
- `Backed up ${syncResult.filesUploaded} files to jack-storage/${projectName}/`,
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
- await linkProject(projectPath, link.project_id, "managed");
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 = `https://${projectName}.runjack.xyz`;
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: `https://${managed.slug}.runjack.xyz`,
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
@@ -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 = join(homedir(), ".config", "jack");
16
- export const TELEMETRY_CONFIG_PATH = join(TELEMETRY_CONFIG_DIR, "telemetry.json");
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
@@ -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];
@@ -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
  }