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