@getjack/jack 0.1.12 → 0.1.14

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.
@@ -41,8 +41,8 @@ import {
41
41
  } from "./config-generator.ts";
42
42
  import { getSyncConfig } from "./config.ts";
43
43
  import { deleteManagedProject } from "./control-plane.ts";
44
- import { debug, isDebug } from "./debug.ts";
45
- import { resolveDeployMode, validateModeAvailability } from "./deploy-mode.ts";
44
+ import { debug, isDebug, printTimingSummary, timerEnd, timerStart } from "./debug.ts";
45
+ import { ensureWranglerInstalled, validateModeAvailability } from "./deploy-mode.ts";
46
46
  import { detectSecrets, generateEnvFile, generateSecretsJson } from "./env-parser.ts";
47
47
  import { JackError, JackErrorCode } from "./errors.ts";
48
48
  import { type HookOutput, runHook } from "./hooks.ts";
@@ -580,6 +580,18 @@ export async function createProject(
580
580
  process.env.CI === "1";
581
581
  const interactive = interactiveOption ?? !isCi;
582
582
 
583
+ // Track timings for each step (shown with --debug)
584
+ const timings: Array<{ label: string; duration: number }> = [];
585
+
586
+ // Fast local validation first - check directory before any network calls
587
+ const nameWasProvided = name !== undefined;
588
+ if (nameWasProvided) {
589
+ const targetDir = resolve(name);
590
+ if (existsSync(targetDir)) {
591
+ throw new JackError(JackErrorCode.VALIDATION_ERROR, `Directory ${name} already exists`);
592
+ }
593
+ }
594
+
583
595
  // Check if jack init was run (throws if not)
584
596
  const { isInitialized } = await import("../commands/init.ts");
585
597
  const initialized = await isInitialized();
@@ -587,44 +599,59 @@ export async function createProject(
587
599
  throw new JackError(JackErrorCode.VALIDATION_ERROR, "jack is not set up yet", "Run: jack init");
588
600
  }
589
601
 
590
- // Resolve deploy mode (omakase: logged in => managed, logged out => BYO)
591
- const deployMode = await resolveDeployMode({
592
- managed: options.managed,
593
- byo: options.byo,
602
+ // Auth gate - check/prompt for authentication before any work
603
+ timerStart("auth-gate");
604
+ const { ensureAuthForCreate } = await import("./auth/ensure-auth.ts");
605
+ const authResult = await ensureAuthForCreate({
606
+ interactive,
607
+ forceManaged: options.managed,
608
+ forceByo: options.byo,
594
609
  });
595
- const modeError = await validateModeAvailability(deployMode);
596
- if (modeError) {
597
- throw new JackError(JackErrorCode.VALIDATION_ERROR, modeError);
598
- }
610
+ timings.push({ label: "Auth gate", duration: timerEnd("auth-gate") });
611
+
612
+ // Use authResult.mode (auth gate handles mode resolution)
613
+ const deployMode = authResult.mode;
599
614
 
600
615
  // Close the "Starting..." spinner from new.ts
601
616
  reporter.stop();
602
- reporter.success("Initialized");
617
+ if (deployMode === "managed") {
618
+ reporter.success("Connected to jack cloud");
619
+ } else {
620
+ reporter.success("Ready");
621
+ }
603
622
 
604
623
  // Generate or use provided name
605
- const nameWasProvided = name !== undefined;
606
624
  const projectName = name ?? generateProjectName();
607
625
  const targetDir = resolve(projectName);
608
626
 
609
- // Check directory doesn't exist
610
- if (existsSync(targetDir)) {
627
+ // Check directory doesn't exist (only needed for auto-generated names now)
628
+ if (!nameWasProvided && existsSync(targetDir)) {
611
629
  throw new JackError(JackErrorCode.VALIDATION_ERROR, `Directory ${projectName} already exists`);
612
630
  }
613
631
 
614
632
  // Early slug availability check for managed mode (only if user provided explicit name)
615
633
  // Skip for auto-generated names - collision is rare, control plane will catch it anyway
616
634
  if (deployMode === "managed" && nameWasProvided) {
635
+ timerStart("slug-check");
617
636
  reporter.start("Checking name availability...");
618
- const { checkAvailability } = await import("./project-resolver.ts");
619
- const { available, existingProject } = await checkAvailability(projectName);
620
- reporter.stop();
621
- if (available) {
637
+
638
+ // First check if the slug is available globally (includes system-reserved names)
639
+ const { checkSlugAvailability } = await import("./control-plane.ts");
640
+ const slugCheck = await checkSlugAvailability(projectName);
641
+
642
+ if (slugCheck.available) {
643
+ timings.push({ label: "Slug check", duration: timerEnd("slug-check") });
644
+ reporter.stop();
622
645
  reporter.success("Name available");
623
- }
646
+ } else {
647
+ // Slug not available - check if it's the user's own project (for linking flow)
648
+ const { checkAvailability } = await import("./project-resolver.ts");
649
+ const { existingProject } = await checkAvailability(projectName);
650
+ timings.push({ label: "Slug check", duration: timerEnd("slug-check") });
651
+ reporter.stop();
624
652
 
625
- if (!available && existingProject) {
626
- // Project exists remotely but not locally - offer to link
627
- if (existingProject.sources.controlPlane && !existingProject.sources.filesystem) {
653
+ if (existingProject?.sources.controlPlane && !existingProject.sources.filesystem) {
654
+ // It's the user's project on jack cloud but not locally - offer to link
628
655
  if (interactive) {
629
656
  const { promptSelect } = await import("./hooks.ts");
630
657
  console.error("");
@@ -635,7 +662,6 @@ export async function createProject(
635
662
 
636
663
  if (choice === 0) {
637
664
  // User chose to link - proceed with project creation
638
- // The project will be linked locally when files are created
639
665
  reporter.success(`Linking to existing project: ${existingProject.url || projectName}`);
640
666
  // Continue with project creation - user wants to link
641
667
  } else {
@@ -655,13 +681,20 @@ export async function createProject(
655
681
  `Try a different name: jack new ${projectName}-2`,
656
682
  );
657
683
  }
658
- } else {
659
- // Project exists in registry with local path - it's truly taken
684
+ } else if (existingProject) {
685
+ // Project exists in registry with local path - it's truly taken by user
660
686
  throw new JackError(
661
687
  JackErrorCode.VALIDATION_ERROR,
662
688
  `Project "${projectName}" already exists`,
663
689
  `Try a different name: jack new ${projectName}-2`,
664
690
  );
691
+ } else {
692
+ // Slug taken but not by this user (reserved or another user's project)
693
+ throw new JackError(
694
+ JackErrorCode.VALIDATION_ERROR,
695
+ `Name "${projectName}" is not available`,
696
+ `Try a different name: jack new ${projectName}-2`,
697
+ );
665
698
  }
666
699
  }
667
700
  }
@@ -755,6 +788,7 @@ export async function createProject(
755
788
  }
756
789
 
757
790
  // Load template with origin tracking for lineage
791
+ timerStart("template-load");
758
792
  let template: Template;
759
793
  let templateOrigin: TemplateOrigin;
760
794
  try {
@@ -762,12 +796,14 @@ export async function createProject(
762
796
  template = resolved.template;
763
797
  templateOrigin = resolved.origin;
764
798
  } catch (err) {
799
+ timerEnd("template-load");
765
800
  reporter.stop();
766
801
  const message = err instanceof Error ? err.message : String(err);
767
802
  throw new JackError(JackErrorCode.TEMPLATE_NOT_FOUND, message);
768
803
  }
769
804
 
770
805
  const rendered = renderTemplate(template, { name: projectName });
806
+ timings.push({ label: "Template load", duration: timerEnd("template-load") });
771
807
 
772
808
  // Handle template-specific secrets
773
809
  const secretsToUse: Record<string, string> = {};
@@ -856,348 +892,380 @@ export async function createProject(
856
892
  }
857
893
  }
858
894
 
859
- // Write all template files
860
- for (const [filePath, content] of Object.entries(rendered.files)) {
861
- await Bun.write(join(targetDir, filePath), content);
862
- }
895
+ // Track if we created the directory (for cleanup on failure)
896
+ let directoryCreated = false;
863
897
 
864
- // Preflight: check D1 capacity before spending time on installs (BYO only)
865
- reporter.stop();
866
- if (deployMode === "byo") {
867
- await preflightD1Capacity(targetDir, reporter, interactive);
868
- }
869
- reporter.start("Creating project...");
898
+ try {
899
+ // Write all template files
900
+ timerStart("file-write");
901
+ for (const [filePath, content] of Object.entries(rendered.files)) {
902
+ await Bun.write(join(targetDir, filePath), content);
903
+ directoryCreated = true; // Directory now exists
904
+ }
870
905
 
871
- // Write secrets files (.env for Vite, .dev.vars for wrangler local, .secrets.json for wrangler bulk)
872
- if (Object.keys(secretsToUse).length > 0) {
873
- const envContent = generateEnvFile(secretsToUse);
874
- const jsonContent = generateSecretsJson(secretsToUse);
875
- await Bun.write(join(targetDir, ".env"), envContent);
876
- await Bun.write(join(targetDir, ".dev.vars"), envContent);
877
- await Bun.write(join(targetDir, ".secrets.json"), jsonContent);
906
+ // Preflight: check D1 capacity before spending time on installs (BYO only)
907
+ reporter.stop();
908
+ if (deployMode === "byo") {
909
+ await preflightD1Capacity(targetDir, reporter, interactive);
910
+ }
911
+ reporter.start("Creating project...");
878
912
 
879
- const gitignorePath = join(targetDir, ".gitignore");
880
- const gitignoreExists = existsSync(gitignorePath);
913
+ // Write secrets files (.env for Vite, .dev.vars for wrangler local, .secrets.json for wrangler bulk)
914
+ if (Object.keys(secretsToUse).length > 0) {
915
+ const envContent = generateEnvFile(secretsToUse);
916
+ const jsonContent = generateSecretsJson(secretsToUse);
917
+ await Bun.write(join(targetDir, ".env"), envContent);
918
+ await Bun.write(join(targetDir, ".dev.vars"), envContent);
919
+ await Bun.write(join(targetDir, ".secrets.json"), jsonContent);
881
920
 
882
- if (!gitignoreExists) {
883
- await Bun.write(gitignorePath, ".env\n.env.*\n.dev.vars\n.secrets.json\nnode_modules/\n");
884
- } else {
885
- const existingContent = await Bun.file(gitignorePath).text();
886
- if (!existingContent.includes(".env")) {
887
- await Bun.write(
888
- gitignorePath,
889
- `${existingContent}\n.env\n.env.*\n.dev.vars\n.secrets.json\n`,
890
- );
921
+ const gitignorePath = join(targetDir, ".gitignore");
922
+ const gitignoreExists = existsSync(gitignorePath);
923
+
924
+ if (!gitignoreExists) {
925
+ await Bun.write(gitignorePath, ".env\n.env.*\n.dev.vars\n.secrets.json\nnode_modules/\n");
926
+ } else {
927
+ const existingContent = await Bun.file(gitignorePath).text();
928
+ if (!existingContent.includes(".env")) {
929
+ await Bun.write(
930
+ gitignorePath,
931
+ `${existingContent}\n.env\n.env.*\n.dev.vars\n.secrets.json\n`,
932
+ );
933
+ }
891
934
  }
892
935
  }
893
- }
936
+ timings.push({ label: "File write", duration: timerEnd("file-write") });
894
937
 
895
- // Generate agent context files
896
- let activeAgents = await getActiveAgents();
897
- if (activeAgents.length > 0) {
898
- const validation = await validateAgentPaths();
938
+ // Generate agent context files
939
+ let activeAgents = await getActiveAgents();
940
+ if (activeAgents.length > 0) {
941
+ const validation = await validateAgentPaths();
899
942
 
900
- if (validation.invalid.length > 0) {
901
- // Silently filter out agents with missing paths
902
- // User can run 'jack agents scan' to see/fix agent config
903
- activeAgents = activeAgents.filter(
904
- ({ id }) => !validation.invalid.some((inv) => inv.id === id),
905
- );
906
- }
943
+ if (validation.invalid.length > 0) {
944
+ // Silently filter out agents with missing paths
945
+ // User can run 'jack agents scan' to see/fix agent config
946
+ activeAgents = activeAgents.filter(
947
+ ({ id }) => !validation.invalid.some((inv) => inv.id === id),
948
+ );
949
+ }
907
950
 
908
- if (activeAgents.length > 0) {
909
- await generateAgentFiles(targetDir, projectName, template, activeAgents);
910
- const agentNames = activeAgents.map(({ definition }) => definition.name).join(", ");
911
- reporter.stop();
912
- reporter.success(`Generated context for: ${agentNames}`);
913
- reporter.start("Creating project...");
951
+ if (activeAgents.length > 0) {
952
+ await generateAgentFiles(targetDir, projectName, template, activeAgents);
953
+ const agentNames = activeAgents.map(({ definition }) => definition.name).join(", ");
954
+ reporter.stop();
955
+ reporter.success(`Generated context for: ${agentNames}`);
956
+ reporter.start("Creating project...");
957
+ }
914
958
  }
915
- }
916
959
 
917
- reporter.stop();
918
- reporter.success(`Created ${projectName}/`);
960
+ reporter.stop();
961
+ reporter.success(`Created ${projectName}/`);
919
962
 
920
- // Parallel setup for managed mode: install + remote creation
921
- let remoteResult: ManagedCreateResult | undefined;
963
+ // Parallel setup for managed mode: install + remote creation
964
+ let remoteResult: ManagedCreateResult | undefined;
922
965
 
923
- if (deployMode === "managed") {
924
- // Run install and remote creation in parallel
925
- reporter.start("Setting up project...");
966
+ if (deployMode === "managed") {
967
+ // Run install and remote creation in parallel
968
+ timerStart("parallel-setup");
969
+ reporter.start("Setting up project...");
926
970
 
927
- try {
928
- const result = await runParallelSetup(targetDir, projectName, {
929
- template: resolvedTemplate || "hello",
930
- usePrebuilt: templateOrigin.type === "builtin", // Only builtin templates have prebuilt bundles
931
- });
932
- remoteResult = result.remoteResult;
933
- reporter.stop();
934
- reporter.success("Project setup complete");
935
- } catch (err) {
936
- reporter.stop();
937
- if (err instanceof JackError) {
938
- reporter.warn(err.suggestion ?? err.message);
971
+ try {
972
+ const result = await runParallelSetup(targetDir, projectName, {
973
+ template: resolvedTemplate || "hello",
974
+ usePrebuilt: templateOrigin.type === "builtin", // Only builtin templates have prebuilt bundles
975
+ });
976
+ remoteResult = result.remoteResult;
977
+ timings.push({ label: "Parallel setup", duration: timerEnd("parallel-setup") });
978
+ reporter.stop();
979
+ reporter.success("Project setup complete");
980
+ } catch (err) {
981
+ timerEnd("parallel-setup");
982
+ reporter.stop();
983
+ if (err instanceof JackError) {
984
+ reporter.warn(err.suggestion ?? err.message);
985
+ throw err;
986
+ }
939
987
  throw err;
940
988
  }
941
- throw err;
942
- }
943
- } else {
944
- // BYO mode: just install dependencies (unchanged from current)
945
- reporter.start("Installing dependencies...");
946
-
947
- const install = Bun.spawn(["bun", "install"], {
948
- cwd: targetDir,
949
- stdout: "ignore",
950
- stderr: "ignore",
951
- });
952
- await install.exited;
989
+ } else {
990
+ // BYO mode: just install dependencies (unchanged from current)
991
+ timerStart("bun-install");
992
+ reporter.start("Installing dependencies...");
953
993
 
954
- if (install.exitCode !== 0) {
955
- reporter.stop();
956
- reporter.warn("Failed to install dependencies, run: bun install");
957
- throw new JackError(
958
- JackErrorCode.BUILD_FAILED,
959
- "Dependency installation failed",
960
- "Run: bun install",
961
- { exitCode: 0, reported: hasReporter },
962
- );
963
- }
994
+ const install = Bun.spawn(["bun", "install"], {
995
+ cwd: targetDir,
996
+ stdout: "ignore",
997
+ stderr: "ignore",
998
+ });
999
+ await install.exited;
964
1000
 
965
- reporter.stop();
966
- reporter.success("Dependencies installed");
967
- }
1001
+ if (install.exitCode !== 0) {
1002
+ timerEnd("bun-install");
1003
+ reporter.stop();
1004
+ reporter.warn("Failed to install dependencies, run: bun install");
1005
+ throw new JackError(
1006
+ JackErrorCode.BUILD_FAILED,
1007
+ "Dependency installation failed",
1008
+ "Run: bun install",
1009
+ { exitCode: 0, reported: hasReporter },
1010
+ );
1011
+ }
968
1012
 
969
- // Run pre-deploy hooks
970
- if (template.hooks?.preDeploy?.length) {
971
- const hookContext = { projectName, projectDir: targetDir };
972
- const hookResult = await runHook(template.hooks.preDeploy, hookContext, {
973
- interactive,
974
- output: reporter,
975
- });
976
- if (!hookResult.success) {
977
- reporter.error("Pre-deploy checks failed");
978
- throw new JackError(JackErrorCode.VALIDATION_ERROR, "Pre-deploy checks failed", undefined, {
979
- exitCode: 0,
980
- reported: hasReporter,
981
- });
1013
+ timings.push({ label: "Bun install", duration: timerEnd("bun-install") });
1014
+ reporter.stop();
1015
+ reporter.success("Dependencies installed");
982
1016
  }
983
- }
984
1017
 
985
- // One-shot agent customization if intent was provided
986
- if (intentPhrase) {
987
- const oneShotAgent = await getOneShotAgent();
988
-
989
- if (oneShotAgent) {
990
- const agentDefinition = getAgentDefinition(oneShotAgent);
991
- const agentLabel = agentDefinition?.name ?? oneShotAgent;
992
- reporter.info(`Customizing with ${agentLabel}`);
993
- reporter.info(`Intent: ${intentPhrase}`);
994
- const debugEnabled = isDebug();
995
- const customizationSpinner = debugEnabled ? null : reporter.spinner("Customizing...");
996
-
997
- // Track customization start
998
- track(Events.INTENT_CUSTOMIZATION_STARTED, { agent: oneShotAgent });
999
-
1000
- const result = await runAgentOneShot(oneShotAgent, targetDir, intentPhrase, {
1001
- info: reporter.info,
1002
- warn: reporter.warn,
1003
- status: customizationSpinner
1004
- ? (message) => {
1005
- customizationSpinner.text = message;
1006
- }
1007
- : undefined,
1018
+ // Run pre-deploy hooks
1019
+ if (template.hooks?.preDeploy?.length) {
1020
+ const hookContext = { projectName, projectDir: targetDir };
1021
+ const hookResult = await runHook(template.hooks.preDeploy, hookContext, {
1022
+ interactive,
1023
+ output: reporter,
1008
1024
  });
1009
-
1010
- if (customizationSpinner) {
1011
- customizationSpinner.stop();
1012
- }
1013
- if (result.success) {
1014
- reporter.success("Project customized");
1015
- // Track successful customization
1016
- track(Events.INTENT_CUSTOMIZATION_COMPLETED, { agent: oneShotAgent });
1017
- } else {
1018
- reporter.warn(`Customization skipped: ${result.error ?? "unknown error"}`);
1019
- // Track failed customization
1020
- track(Events.INTENT_CUSTOMIZATION_FAILED, {
1021
- agent: oneShotAgent,
1022
- error_type: "agent_error",
1025
+ if (!hookResult.success) {
1026
+ reporter.error("Pre-deploy checks failed");
1027
+ throw new JackError(JackErrorCode.VALIDATION_ERROR, "Pre-deploy checks failed", undefined, {
1028
+ exitCode: 0,
1029
+ reported: hasReporter,
1023
1030
  });
1024
1031
  }
1025
- } else {
1026
- reporter.info?.("No compatible agent for customization (Claude Code or Codex required)");
1027
1032
  }
1028
- }
1029
1033
 
1030
- let workerUrl: string | null = null;
1034
+ // One-shot agent customization if intent was provided
1035
+ if (intentPhrase) {
1036
+ const oneShotAgent = await getOneShotAgent();
1037
+
1038
+ if (oneShotAgent) {
1039
+ const agentDefinition = getAgentDefinition(oneShotAgent);
1040
+ const agentLabel = agentDefinition?.name ?? oneShotAgent;
1041
+ reporter.info(`Customizing with ${agentLabel}`);
1042
+ reporter.info(`Intent: ${intentPhrase}`);
1043
+ const debugEnabled = isDebug();
1044
+ const customizationSpinner = debugEnabled ? null : reporter.spinner("Customizing...");
1045
+
1046
+ // Track customization start
1047
+ track(Events.INTENT_CUSTOMIZATION_STARTED, { agent: oneShotAgent });
1048
+
1049
+ const result = await runAgentOneShot(oneShotAgent, targetDir, intentPhrase, {
1050
+ info: reporter.info,
1051
+ warn: reporter.warn,
1052
+ status: customizationSpinner
1053
+ ? (message) => {
1054
+ customizationSpinner.text = message;
1055
+ }
1056
+ : undefined,
1057
+ });
1031
1058
 
1032
- // Deploy based on mode
1033
- if (deployMode === "managed") {
1034
- // Managed mode: remote was already created in parallel setup
1035
- if (!remoteResult) {
1036
- throw new JackError(
1037
- JackErrorCode.VALIDATION_ERROR,
1038
- "Managed project was not created",
1039
- "This is an internal error - please report it",
1040
- );
1059
+ if (customizationSpinner) {
1060
+ customizationSpinner.stop();
1061
+ }
1062
+ if (result.success) {
1063
+ reporter.success("Project customized");
1064
+ // Track successful customization
1065
+ track(Events.INTENT_CUSTOMIZATION_COMPLETED, { agent: oneShotAgent });
1066
+ } else {
1067
+ reporter.warn(`Customization skipped: ${result.error ?? "unknown error"}`);
1068
+ // Track failed customization
1069
+ track(Events.INTENT_CUSTOMIZATION_FAILED, {
1070
+ agent: oneShotAgent,
1071
+ error_type: "agent_error",
1072
+ });
1073
+ }
1074
+ } else {
1075
+ reporter.info?.("No compatible agent for customization (Claude Code or Codex required)");
1076
+ }
1041
1077
  }
1042
1078
 
1043
- // Fetch username for link storage
1044
- const { getCurrentUserProfile } = await import("./control-plane.ts");
1045
- const profile = await getCurrentUserProfile();
1046
- const ownerUsername = profile?.username ?? undefined;
1047
-
1048
- // Link project locally and register path
1049
- try {
1050
- await linkProject(targetDir, remoteResult.projectId, "managed", ownerUsername);
1051
- await writeTemplateMetadata(targetDir, templateOrigin);
1052
- await registerPath(remoteResult.projectId, targetDir);
1053
- } catch (err) {
1054
- debug("Failed to link managed project:", err);
1055
- }
1079
+ let workerUrl: string | null = null;
1056
1080
 
1057
- // Check if prebuilt deployment succeeded
1058
- if (remoteResult.status === "live") {
1059
- // Prebuilt succeeded - skip the fresh build
1060
- workerUrl = remoteResult.runjackUrl;
1061
- reporter.success(`Deployed: ${workerUrl}`);
1062
- } else {
1063
- // Prebuilt not available - fall back to fresh build
1064
- if (remoteResult.prebuiltFailed) {
1065
- // Show debug info about why prebuilt failed
1066
- const errorDetail = remoteResult.prebuiltError ? ` (${remoteResult.prebuiltError})` : "";
1067
- debug(`Prebuilt failed${errorDetail}`);
1068
- reporter.info("Pre-built not available, building fresh...");
1081
+ // Deploy based on mode
1082
+ timerStart("deploy");
1083
+ if (deployMode === "managed") {
1084
+ // Managed mode: remote was already created in parallel setup
1085
+ if (!remoteResult) {
1086
+ throw new JackError(
1087
+ JackErrorCode.VALIDATION_ERROR,
1088
+ "Managed project was not created",
1089
+ "This is an internal error - please report it",
1090
+ );
1069
1091
  }
1070
1092
 
1071
- await deployToManagedProject(remoteResult.projectId, targetDir, reporter);
1072
- workerUrl = remoteResult.runjackUrl;
1073
- reporter.success(`Created: ${workerUrl}`);
1074
- }
1075
- } else {
1076
- // BYO mode: deploy via wrangler
1093
+ // Fetch username for link storage
1094
+ const { getCurrentUserProfile } = await import("./control-plane.ts");
1095
+ const profile = await getCurrentUserProfile();
1096
+ const ownerUsername = profile?.username ?? undefined;
1077
1097
 
1078
- // Build first if needed (wrangler needs built assets)
1079
- if (await needsOpenNextBuild(targetDir)) {
1080
- reporter.start("Building assets...");
1098
+ // Link project locally and register path
1081
1099
  try {
1082
- await runOpenNextBuild(targetDir);
1083
- reporter.stop();
1084
- reporter.success("Built assets");
1100
+ await linkProject(targetDir, remoteResult.projectId, "managed", ownerUsername);
1101
+ await writeTemplateMetadata(targetDir, templateOrigin);
1102
+ await registerPath(remoteResult.projectId, targetDir);
1085
1103
  } catch (err) {
1086
- reporter.stop();
1087
- reporter.error("Build failed");
1088
- throw err;
1089
- }
1090
- } else if (await needsViteBuild(targetDir)) {
1091
- reporter.start("Building assets...");
1092
- try {
1093
- await runViteBuild(targetDir);
1094
- reporter.stop();
1095
- reporter.success("Built assets");
1096
- } catch (err) {
1097
- reporter.stop();
1098
- reporter.error("Build failed");
1099
- throw err;
1104
+ debug("Failed to link managed project:", err);
1100
1105
  }
1101
- }
1102
-
1103
- reporter.start("Deploying...");
1104
1106
 
1105
- const deployResult = await runWranglerDeploy(targetDir);
1107
+ // Check if prebuilt deployment succeeded
1108
+ if (remoteResult.status === "live") {
1109
+ // Prebuilt succeeded - skip the fresh build
1110
+ workerUrl = remoteResult.runjackUrl;
1111
+ reporter.success(`Deployed: ${workerUrl}`);
1112
+ } else {
1113
+ // Prebuilt not available - fall back to fresh build
1114
+ if (remoteResult.prebuiltFailed) {
1115
+ // Show debug info about why prebuilt failed
1116
+ const errorDetail = remoteResult.prebuiltError ? ` (${remoteResult.prebuiltError})` : "";
1117
+ debug(`Prebuilt failed${errorDetail}`);
1118
+ reporter.info("Pre-built not available, building fresh...");
1119
+ }
1106
1120
 
1107
- if (deployResult.exitCode !== 0) {
1108
- reporter.stop();
1109
- reporter.error("Deploy failed");
1110
- throw new JackError(JackErrorCode.DEPLOY_FAILED, "Deploy failed", undefined, {
1111
- exitCode: 0,
1112
- stderr: deployResult.stderr.toString(),
1113
- reported: hasReporter,
1114
- });
1115
- }
1121
+ await deployToManagedProject(remoteResult.projectId, targetDir, reporter);
1122
+ workerUrl = remoteResult.runjackUrl;
1123
+ reporter.success(`Created: ${workerUrl}`);
1124
+ }
1125
+ } else {
1126
+ // BYO mode: deploy via wrangler
1116
1127
 
1117
- // Apply schema.sql after deploy
1118
- if (await hasD1Config(targetDir)) {
1119
- const dbName = await getD1DatabaseName(targetDir);
1120
- if (dbName) {
1128
+ // Build first if needed (wrangler needs built assets)
1129
+ if (await needsOpenNextBuild(targetDir)) {
1130
+ reporter.start("Building assets...");
1131
+ try {
1132
+ await runOpenNextBuild(targetDir);
1133
+ reporter.stop();
1134
+ reporter.success("Built assets");
1135
+ } catch (err) {
1136
+ reporter.stop();
1137
+ reporter.error("Build failed");
1138
+ throw err;
1139
+ }
1140
+ } else if (await needsViteBuild(targetDir)) {
1141
+ reporter.start("Building assets...");
1121
1142
  try {
1122
- await applySchema(dbName, targetDir);
1143
+ await runViteBuild(targetDir);
1144
+ reporter.stop();
1145
+ reporter.success("Built assets");
1123
1146
  } catch (err) {
1124
- reporter.warn(`Schema application failed: ${err}`);
1125
- reporter.info("Run manually: bun run db:migrate");
1147
+ reporter.stop();
1148
+ reporter.error("Build failed");
1149
+ throw err;
1126
1150
  }
1127
1151
  }
1128
- }
1129
1152
 
1130
- // Push secrets to Cloudflare
1131
- const secretsJsonPath = join(targetDir, ".secrets.json");
1132
- if (existsSync(secretsJsonPath)) {
1133
- reporter.start("Configuring secrets...");
1153
+ reporter.start("Deploying...");
1134
1154
 
1135
- const secretsResult = await $`wrangler secret bulk .secrets.json`
1136
- .cwd(targetDir)
1137
- .nothrow()
1138
- .quiet();
1155
+ const deployResult = await runWranglerDeploy(targetDir);
1139
1156
 
1140
- if (secretsResult.exitCode !== 0) {
1157
+ if (deployResult.exitCode !== 0) {
1141
1158
  reporter.stop();
1142
- reporter.warn("Failed to push secrets to Cloudflare");
1143
- reporter.info("Run manually: wrangler secret bulk .secrets.json");
1159
+ reporter.error("Deploy failed");
1160
+ throw new JackError(JackErrorCode.DEPLOY_FAILED, "Deploy failed", undefined, {
1161
+ exitCode: 0,
1162
+ stderr: deployResult.stderr.toString(),
1163
+ reported: hasReporter,
1164
+ });
1165
+ }
1166
+
1167
+ // Apply schema.sql after deploy
1168
+ if (await hasD1Config(targetDir)) {
1169
+ const dbName = await getD1DatabaseName(targetDir);
1170
+ if (dbName) {
1171
+ try {
1172
+ await applySchema(dbName, targetDir);
1173
+ } catch (err) {
1174
+ reporter.warn(`Schema application failed: ${err}`);
1175
+ reporter.info("Run manually: bun run db:migrate");
1176
+ }
1177
+ }
1178
+ }
1179
+
1180
+ // Push secrets to Cloudflare
1181
+ const secretsJsonPath = join(targetDir, ".secrets.json");
1182
+ if (existsSync(secretsJsonPath)) {
1183
+ reporter.start("Configuring secrets...");
1184
+
1185
+ const secretsResult = await $`wrangler secret bulk .secrets.json`
1186
+ .cwd(targetDir)
1187
+ .nothrow()
1188
+ .quiet();
1189
+
1190
+ if (secretsResult.exitCode !== 0) {
1191
+ reporter.stop();
1192
+ reporter.warn("Failed to push secrets to Cloudflare");
1193
+ reporter.info("Run manually: wrangler secret bulk .secrets.json");
1194
+ } else {
1195
+ reporter.stop();
1196
+ reporter.success("Secrets configured");
1197
+ }
1198
+ }
1199
+
1200
+ // Parse URL from output
1201
+ const deployOutput = deployResult.stdout.toString();
1202
+ const urlMatch = deployOutput.match(/https:\/\/[\w-]+\.[\w-]+\.workers\.dev/);
1203
+ workerUrl = urlMatch ? urlMatch[0] : null;
1204
+
1205
+ reporter.stop();
1206
+ if (workerUrl) {
1207
+ reporter.success(`Live: ${workerUrl}`);
1144
1208
  } else {
1145
- reporter.stop();
1146
- reporter.success("Secrets configured");
1209
+ reporter.success("Deployed");
1147
1210
  }
1148
- }
1149
1211
 
1150
- // Parse URL from output
1151
- const deployOutput = deployResult.stdout.toString();
1152
- const urlMatch = deployOutput.match(/https:\/\/[\w-]+\.[\w-]+\.workers\.dev/);
1153
- workerUrl = urlMatch ? urlMatch[0] : null;
1212
+ // Generate BYO project ID and link locally
1213
+ const byoProjectId = generateByoProjectId();
1154
1214
 
1155
- reporter.stop();
1156
- if (workerUrl) {
1157
- reporter.success(`Live: ${workerUrl}`);
1158
- } else {
1159
- reporter.success("Deployed");
1215
+ // Link project locally and register path
1216
+ try {
1217
+ await linkProject(targetDir, byoProjectId, "byo");
1218
+ await writeTemplateMetadata(targetDir, templateOrigin);
1219
+ await registerPath(byoProjectId, targetDir);
1220
+ } catch (err) {
1221
+ debug("Failed to link BYO project:", err);
1222
+ }
1160
1223
  }
1224
+ timings.push({ label: "Deploy", duration: timerEnd("deploy") });
1225
+
1226
+ // Run post-deploy hooks (for both modes)
1227
+ if (template.hooks?.postDeploy?.length && workerUrl) {
1228
+ timerStart("post-deploy-hooks");
1229
+ const domain = workerUrl.replace(/^https?:\/\//, "");
1230
+ const hookResult = await runHook(
1231
+ template.hooks.postDeploy,
1232
+ {
1233
+ domain,
1234
+ url: workerUrl,
1235
+ projectName,
1236
+ projectDir: targetDir,
1237
+ },
1238
+ { interactive, output: reporter },
1239
+ );
1240
+ timings.push({ label: "Post-deploy hooks", duration: timerEnd("post-deploy-hooks") });
1161
1241
 
1162
- // Generate BYO project ID and link locally
1163
- const byoProjectId = generateByoProjectId();
1164
-
1165
- // Link project locally and register path
1166
- try {
1167
- await linkProject(targetDir, byoProjectId, "byo");
1168
- await writeTemplateMetadata(targetDir, templateOrigin);
1169
- await registerPath(byoProjectId, targetDir);
1170
- } catch (err) {
1171
- debug("Failed to link BYO project:", err);
1242
+ // Show final celebration if there were interactive prompts (URL might have scrolled away)
1243
+ if (hookResult.hadInteractiveActions && reporter.celebrate) {
1244
+ reporter.celebrate("You're live!", [domain]);
1245
+ }
1172
1246
  }
1173
- }
1174
1247
 
1175
- // Run post-deploy hooks (for both modes)
1176
- if (template.hooks?.postDeploy?.length && workerUrl) {
1177
- const domain = workerUrl.replace(/^https?:\/\//, "");
1178
- const hookResult = await runHook(
1179
- template.hooks.postDeploy,
1180
- {
1181
- domain,
1182
- url: workerUrl,
1183
- projectName,
1184
- projectDir: targetDir,
1185
- },
1186
- { interactive, output: reporter },
1187
- );
1248
+ // Print timing summary (only shown with --debug)
1249
+ printTimingSummary(timings);
1188
1250
 
1189
- // Show final celebration if there were interactive prompts (URL might have scrolled away)
1190
- if (hookResult.hadInteractiveActions && reporter.celebrate) {
1191
- reporter.celebrate("You're live!", [domain]);
1251
+ return {
1252
+ projectName,
1253
+ targetDir,
1254
+ workerUrl,
1255
+ deployMode,
1256
+ };
1257
+ } catch (error) {
1258
+ // Clean up directory if we created it
1259
+ if (directoryCreated && existsSync(targetDir)) {
1260
+ try {
1261
+ const { rm } = await import("node:fs/promises");
1262
+ await rm(targetDir, { recursive: true, force: true });
1263
+ } catch {
1264
+ // Ignore cleanup errors - user will see directory exists on retry
1265
+ }
1192
1266
  }
1267
+ throw error;
1193
1268
  }
1194
-
1195
- return {
1196
- projectName,
1197
- targetDir,
1198
- workerUrl,
1199
- deployMode,
1200
- };
1201
1269
  }
1202
1270
 
1203
1271
  // ============================================================================
@@ -1271,8 +1339,21 @@ export async function deployProject(options: DeployOptions = {}): Promise<Deploy
1271
1339
  deployMode = link?.deploy_mode ?? "byo";
1272
1340
  }
1273
1341
 
1274
- // Validate mode availability
1342
+ // Ensure wrangler is installed (auto-install if needed)
1275
1343
  if (!dryRun) {
1344
+ let installSpinner: OperationSpinner | null = null;
1345
+ const wranglerReady = await ensureWranglerInstalled(() => {
1346
+ installSpinner = reporter.spinner("Installing dependencies...");
1347
+ });
1348
+ if (installSpinner) {
1349
+ if (wranglerReady) {
1350
+ (installSpinner as OperationSpinner).success("Dependencies installed");
1351
+ } else {
1352
+ (installSpinner as OperationSpinner).error("Failed to install dependencies");
1353
+ }
1354
+ }
1355
+
1356
+ // Validate mode availability
1276
1357
  const modeError = await validateModeAvailability(deployMode);
1277
1358
  if (modeError) {
1278
1359
  throw new JackError(JackErrorCode.VALIDATION_ERROR, modeError);