@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.
- package/package.json +2 -5
- package/src/commands/down.ts +1 -4
- package/src/commands/login.ts +6 -199
- package/src/lib/auth/ensure-auth.test.ts +285 -0
- package/src/lib/auth/ensure-auth.ts +165 -0
- package/src/lib/auth/index.ts +10 -0
- package/src/lib/auth/login-flow.ts +287 -0
- package/src/lib/control-plane.ts +42 -0
- package/src/lib/deploy-mode.ts +23 -2
- package/src/lib/deploy-upload.ts +9 -0
- package/src/lib/hooks.ts +25 -9
- package/src/lib/json-edit.ts +1 -5
- package/src/lib/project-operations.ts +391 -310
- package/src/lib/project-resolver.ts +12 -0
- package/src/lib/telemetry.ts +1 -0
- package/templates/miniapp/.jack.json +5 -5
- package/templates/miniapp/public/.well-known/farcaster.json +15 -15
- package/templates/miniapp/src/lib/api.ts +71 -3
- package/templates/miniapp/src/worker.ts +45 -5
|
@@ -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 {
|
|
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
|
-
//
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
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
|
-
|
|
596
|
-
|
|
597
|
-
|
|
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
|
-
|
|
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
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
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
|
-
|
|
626
|
-
|
|
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
|
-
//
|
|
860
|
-
|
|
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
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
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
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
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
|
-
|
|
880
|
-
|
|
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
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
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
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
938
|
+
// Generate agent context files
|
|
939
|
+
let activeAgents = await getActiveAgents();
|
|
940
|
+
if (activeAgents.length > 0) {
|
|
941
|
+
const validation = await validateAgentPaths();
|
|
899
942
|
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
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
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
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
|
-
|
|
918
|
-
|
|
960
|
+
reporter.stop();
|
|
961
|
+
reporter.success(`Created ${projectName}/`);
|
|
919
962
|
|
|
920
|
-
|
|
921
|
-
|
|
963
|
+
// Parallel setup for managed mode: install + remote creation
|
|
964
|
+
let remoteResult: ManagedCreateResult | undefined;
|
|
922
965
|
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
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
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
reporter.
|
|
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
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
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
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
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
|
-
|
|
966
|
-
|
|
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
|
-
|
|
970
|
-
|
|
971
|
-
|
|
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
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
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
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
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
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
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
|
-
|
|
1079
|
-
if (await needsOpenNextBuild(targetDir)) {
|
|
1080
|
-
reporter.start("Building assets...");
|
|
1098
|
+
// Link project locally and register path
|
|
1081
1099
|
try {
|
|
1082
|
-
await
|
|
1083
|
-
|
|
1084
|
-
|
|
1100
|
+
await linkProject(targetDir, remoteResult.projectId, "managed", ownerUsername);
|
|
1101
|
+
await writeTemplateMetadata(targetDir, templateOrigin);
|
|
1102
|
+
await registerPath(remoteResult.projectId, targetDir);
|
|
1085
1103
|
} catch (err) {
|
|
1086
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
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
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
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
|
|
1143
|
+
await runViteBuild(targetDir);
|
|
1144
|
+
reporter.stop();
|
|
1145
|
+
reporter.success("Built assets");
|
|
1123
1146
|
} catch (err) {
|
|
1124
|
-
reporter.
|
|
1125
|
-
reporter.
|
|
1147
|
+
reporter.stop();
|
|
1148
|
+
reporter.error("Build failed");
|
|
1149
|
+
throw err;
|
|
1126
1150
|
}
|
|
1127
1151
|
}
|
|
1128
|
-
}
|
|
1129
1152
|
|
|
1130
|
-
|
|
1131
|
-
const secretsJsonPath = join(targetDir, ".secrets.json");
|
|
1132
|
-
if (existsSync(secretsJsonPath)) {
|
|
1133
|
-
reporter.start("Configuring secrets...");
|
|
1153
|
+
reporter.start("Deploying...");
|
|
1134
1154
|
|
|
1135
|
-
const
|
|
1136
|
-
.cwd(targetDir)
|
|
1137
|
-
.nothrow()
|
|
1138
|
-
.quiet();
|
|
1155
|
+
const deployResult = await runWranglerDeploy(targetDir);
|
|
1139
1156
|
|
|
1140
|
-
if (
|
|
1157
|
+
if (deployResult.exitCode !== 0) {
|
|
1141
1158
|
reporter.stop();
|
|
1142
|
-
reporter.
|
|
1143
|
-
|
|
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.
|
|
1146
|
-
reporter.success("Secrets configured");
|
|
1209
|
+
reporter.success("Deployed");
|
|
1147
1210
|
}
|
|
1148
|
-
}
|
|
1149
1211
|
|
|
1150
|
-
|
|
1151
|
-
|
|
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
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
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
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
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
|
-
|
|
1176
|
-
|
|
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
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
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
|
-
//
|
|
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);
|