@getjack/jack 0.1.13 → 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/lib/deploy-mode.ts +23 -2
- package/src/lib/deploy-upload.ts +9 -0
- package/src/lib/project-operations.ts +382 -302
- package/src/lib/project-resolver.ts +12 -0
- 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
package/package.json
CHANGED
|
@@ -1,16 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@getjack/jack",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.14",
|
|
4
4
|
"description": "Ship before you forget why you started. The vibecoder's deployment CLI.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"bin": {
|
|
8
8
|
"jack": "./src/index.ts"
|
|
9
9
|
},
|
|
10
|
-
"files": [
|
|
11
|
-
"src",
|
|
12
|
-
"templates"
|
|
13
|
-
],
|
|
10
|
+
"files": ["src", "templates"],
|
|
14
11
|
"engines": {
|
|
15
12
|
"bun": ">=1.0.0"
|
|
16
13
|
},
|
package/src/lib/deploy-mode.ts
CHANGED
|
@@ -24,6 +24,27 @@ export async function isWranglerAvailable(): Promise<boolean> {
|
|
|
24
24
|
}
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
/**
|
|
28
|
+
* Ensure wrangler is installed, auto-installing if needed.
|
|
29
|
+
*
|
|
30
|
+
* @param onInstalling - Optional callback when installation starts (for UI feedback)
|
|
31
|
+
* @returns true if wrangler is available, false if installation failed
|
|
32
|
+
*/
|
|
33
|
+
export async function ensureWranglerInstalled(onInstalling?: () => void): Promise<boolean> {
|
|
34
|
+
if (await isWranglerAvailable()) {
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Auto-install wrangler
|
|
39
|
+
onInstalling?.();
|
|
40
|
+
try {
|
|
41
|
+
await $`bun add -g wrangler`.quiet();
|
|
42
|
+
return await isWranglerAvailable();
|
|
43
|
+
} catch {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
27
48
|
/**
|
|
28
49
|
* Determine deploy mode based on login status and flags.
|
|
29
50
|
*
|
|
@@ -71,14 +92,14 @@ export async function validateModeAvailability(mode: DeployMode): Promise<string
|
|
|
71
92
|
}
|
|
72
93
|
const hasWrangler = await isWranglerAvailable();
|
|
73
94
|
if (!hasWrangler) {
|
|
74
|
-
return "wrangler
|
|
95
|
+
return "wrangler installation failed. Please install manually: bun add -g wrangler";
|
|
75
96
|
}
|
|
76
97
|
}
|
|
77
98
|
|
|
78
99
|
if (mode === "byo") {
|
|
79
100
|
const hasWrangler = await isWranglerAvailable();
|
|
80
101
|
if (!hasWrangler) {
|
|
81
|
-
return "wrangler
|
|
102
|
+
return "wrangler installation failed. Please install manually: bun add -g wrangler";
|
|
82
103
|
}
|
|
83
104
|
}
|
|
84
105
|
|
package/src/lib/deploy-upload.ts
CHANGED
|
@@ -84,7 +84,16 @@ export async function uploadDeployment(options: DeployUploadOptions): Promise<De
|
|
|
84
84
|
if (!response.ok) {
|
|
85
85
|
const err = (await response.json().catch(() => ({ message: "Unknown error" }))) as {
|
|
86
86
|
message?: string;
|
|
87
|
+
error?: string;
|
|
87
88
|
};
|
|
89
|
+
|
|
90
|
+
// Provide actionable error for orphaned local links
|
|
91
|
+
if (response.status === 404 && err.error === "not_found") {
|
|
92
|
+
throw new Error(
|
|
93
|
+
"Project not found in jack cloud. The local link may be orphaned.\nFix: jack unlink && jack ship",
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
88
97
|
throw new Error(err.message || `Upload failed: ${response.status}`);
|
|
89
98
|
}
|
|
90
99
|
|
|
@@ -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";
|
|
@@ -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();
|
|
@@ -588,44 +600,58 @@ export async function createProject(
|
|
|
588
600
|
}
|
|
589
601
|
|
|
590
602
|
// Auth gate - check/prompt for authentication before any work
|
|
603
|
+
timerStart("auth-gate");
|
|
591
604
|
const { ensureAuthForCreate } = await import("./auth/ensure-auth.ts");
|
|
592
605
|
const authResult = await ensureAuthForCreate({
|
|
593
606
|
interactive,
|
|
594
607
|
forceManaged: options.managed,
|
|
595
608
|
forceByo: options.byo,
|
|
596
609
|
});
|
|
610
|
+
timings.push({ label: "Auth gate", duration: timerEnd("auth-gate") });
|
|
597
611
|
|
|
598
612
|
// Use authResult.mode (auth gate handles mode resolution)
|
|
599
613
|
const deployMode = authResult.mode;
|
|
600
614
|
|
|
601
615
|
// Close the "Starting..." spinner from new.ts
|
|
602
616
|
reporter.stop();
|
|
603
|
-
|
|
617
|
+
if (deployMode === "managed") {
|
|
618
|
+
reporter.success("Connected to jack cloud");
|
|
619
|
+
} else {
|
|
620
|
+
reporter.success("Ready");
|
|
621
|
+
}
|
|
604
622
|
|
|
605
623
|
// Generate or use provided name
|
|
606
|
-
const nameWasProvided = name !== undefined;
|
|
607
624
|
const projectName = name ?? generateProjectName();
|
|
608
625
|
const targetDir = resolve(projectName);
|
|
609
626
|
|
|
610
|
-
// Check directory doesn't exist
|
|
611
|
-
if (existsSync(targetDir)) {
|
|
627
|
+
// Check directory doesn't exist (only needed for auto-generated names now)
|
|
628
|
+
if (!nameWasProvided && existsSync(targetDir)) {
|
|
612
629
|
throw new JackError(JackErrorCode.VALIDATION_ERROR, `Directory ${projectName} already exists`);
|
|
613
630
|
}
|
|
614
631
|
|
|
615
632
|
// Early slug availability check for managed mode (only if user provided explicit name)
|
|
616
633
|
// Skip for auto-generated names - collision is rare, control plane will catch it anyway
|
|
617
634
|
if (deployMode === "managed" && nameWasProvided) {
|
|
635
|
+
timerStart("slug-check");
|
|
618
636
|
reporter.start("Checking name availability...");
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
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();
|
|
623
645
|
reporter.success("Name available");
|
|
624
|
-
}
|
|
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();
|
|
625
652
|
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
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
|
|
629
655
|
if (interactive) {
|
|
630
656
|
const { promptSelect } = await import("./hooks.ts");
|
|
631
657
|
console.error("");
|
|
@@ -636,7 +662,6 @@ export async function createProject(
|
|
|
636
662
|
|
|
637
663
|
if (choice === 0) {
|
|
638
664
|
// User chose to link - proceed with project creation
|
|
639
|
-
// The project will be linked locally when files are created
|
|
640
665
|
reporter.success(`Linking to existing project: ${existingProject.url || projectName}`);
|
|
641
666
|
// Continue with project creation - user wants to link
|
|
642
667
|
} else {
|
|
@@ -656,13 +681,20 @@ export async function createProject(
|
|
|
656
681
|
`Try a different name: jack new ${projectName}-2`,
|
|
657
682
|
);
|
|
658
683
|
}
|
|
659
|
-
} else {
|
|
660
|
-
// 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
|
|
661
686
|
throw new JackError(
|
|
662
687
|
JackErrorCode.VALIDATION_ERROR,
|
|
663
688
|
`Project "${projectName}" already exists`,
|
|
664
689
|
`Try a different name: jack new ${projectName}-2`,
|
|
665
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
|
+
);
|
|
666
698
|
}
|
|
667
699
|
}
|
|
668
700
|
}
|
|
@@ -756,6 +788,7 @@ export async function createProject(
|
|
|
756
788
|
}
|
|
757
789
|
|
|
758
790
|
// Load template with origin tracking for lineage
|
|
791
|
+
timerStart("template-load");
|
|
759
792
|
let template: Template;
|
|
760
793
|
let templateOrigin: TemplateOrigin;
|
|
761
794
|
try {
|
|
@@ -763,12 +796,14 @@ export async function createProject(
|
|
|
763
796
|
template = resolved.template;
|
|
764
797
|
templateOrigin = resolved.origin;
|
|
765
798
|
} catch (err) {
|
|
799
|
+
timerEnd("template-load");
|
|
766
800
|
reporter.stop();
|
|
767
801
|
const message = err instanceof Error ? err.message : String(err);
|
|
768
802
|
throw new JackError(JackErrorCode.TEMPLATE_NOT_FOUND, message);
|
|
769
803
|
}
|
|
770
804
|
|
|
771
805
|
const rendered = renderTemplate(template, { name: projectName });
|
|
806
|
+
timings.push({ label: "Template load", duration: timerEnd("template-load") });
|
|
772
807
|
|
|
773
808
|
// Handle template-specific secrets
|
|
774
809
|
const secretsToUse: Record<string, string> = {};
|
|
@@ -857,348 +892,380 @@ export async function createProject(
|
|
|
857
892
|
}
|
|
858
893
|
}
|
|
859
894
|
|
|
860
|
-
//
|
|
861
|
-
|
|
862
|
-
await Bun.write(join(targetDir, filePath), content);
|
|
863
|
-
}
|
|
895
|
+
// Track if we created the directory (for cleanup on failure)
|
|
896
|
+
let directoryCreated = false;
|
|
864
897
|
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
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
|
+
}
|
|
871
905
|
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
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...");
|
|
879
912
|
|
|
880
|
-
|
|
881
|
-
|
|
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);
|
|
882
920
|
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
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
|
+
}
|
|
892
934
|
}
|
|
893
935
|
}
|
|
894
|
-
|
|
936
|
+
timings.push({ label: "File write", duration: timerEnd("file-write") });
|
|
895
937
|
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
938
|
+
// Generate agent context files
|
|
939
|
+
let activeAgents = await getActiveAgents();
|
|
940
|
+
if (activeAgents.length > 0) {
|
|
941
|
+
const validation = await validateAgentPaths();
|
|
900
942
|
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
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
|
+
}
|
|
908
950
|
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
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
|
+
}
|
|
915
958
|
}
|
|
916
|
-
}
|
|
917
959
|
|
|
918
|
-
|
|
919
|
-
|
|
960
|
+
reporter.stop();
|
|
961
|
+
reporter.success(`Created ${projectName}/`);
|
|
920
962
|
|
|
921
|
-
|
|
922
|
-
|
|
963
|
+
// Parallel setup for managed mode: install + remote creation
|
|
964
|
+
let remoteResult: ManagedCreateResult | undefined;
|
|
923
965
|
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
966
|
+
if (deployMode === "managed") {
|
|
967
|
+
// Run install and remote creation in parallel
|
|
968
|
+
timerStart("parallel-setup");
|
|
969
|
+
reporter.start("Setting up project...");
|
|
927
970
|
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
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
|
+
}
|
|
940
987
|
throw err;
|
|
941
988
|
}
|
|
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;
|
|
989
|
+
} else {
|
|
990
|
+
// BYO mode: just install dependencies (unchanged from current)
|
|
991
|
+
timerStart("bun-install");
|
|
992
|
+
reporter.start("Installing dependencies...");
|
|
954
993
|
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
"Run: bun install",
|
|
962
|
-
{ exitCode: 0, reported: hasReporter },
|
|
963
|
-
);
|
|
964
|
-
}
|
|
994
|
+
const install = Bun.spawn(["bun", "install"], {
|
|
995
|
+
cwd: targetDir,
|
|
996
|
+
stdout: "ignore",
|
|
997
|
+
stderr: "ignore",
|
|
998
|
+
});
|
|
999
|
+
await install.exited;
|
|
965
1000
|
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
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
|
+
}
|
|
969
1012
|
|
|
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
|
-
});
|
|
1013
|
+
timings.push({ label: "Bun install", duration: timerEnd("bun-install") });
|
|
1014
|
+
reporter.stop();
|
|
1015
|
+
reporter.success("Dependencies installed");
|
|
983
1016
|
}
|
|
984
|
-
}
|
|
985
1017
|
|
|
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,
|
|
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,
|
|
1009
1024
|
});
|
|
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",
|
|
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,
|
|
1024
1030
|
});
|
|
1025
1031
|
}
|
|
1026
|
-
} else {
|
|
1027
|
-
reporter.info?.("No compatible agent for customization (Claude Code or Codex required)");
|
|
1028
1032
|
}
|
|
1029
|
-
}
|
|
1030
1033
|
|
|
1031
|
-
|
|
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
|
+
});
|
|
1032
1058
|
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
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
|
+
}
|
|
1042
1077
|
}
|
|
1043
1078
|
|
|
1044
|
-
|
|
1045
|
-
const { getCurrentUserProfile } = await import("./control-plane.ts");
|
|
1046
|
-
const profile = await getCurrentUserProfile();
|
|
1047
|
-
const ownerUsername = profile?.username ?? undefined;
|
|
1079
|
+
let workerUrl: string | null = null;
|
|
1048
1080
|
|
|
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...");
|
|
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
|
+
);
|
|
1070
1091
|
}
|
|
1071
1092
|
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
} else {
|
|
1077
|
-
// 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;
|
|
1078
1097
|
|
|
1079
|
-
|
|
1080
|
-
if (await needsOpenNextBuild(targetDir)) {
|
|
1081
|
-
reporter.start("Building assets...");
|
|
1082
|
-
try {
|
|
1083
|
-
await runOpenNextBuild(targetDir);
|
|
1084
|
-
reporter.stop();
|
|
1085
|
-
reporter.success("Built assets");
|
|
1086
|
-
} catch (err) {
|
|
1087
|
-
reporter.stop();
|
|
1088
|
-
reporter.error("Build failed");
|
|
1089
|
-
throw err;
|
|
1090
|
-
}
|
|
1091
|
-
} else if (await needsViteBuild(targetDir)) {
|
|
1092
|
-
reporter.start("Building assets...");
|
|
1098
|
+
// Link project locally and register path
|
|
1093
1099
|
try {
|
|
1094
|
-
await
|
|
1095
|
-
|
|
1096
|
-
|
|
1100
|
+
await linkProject(targetDir, remoteResult.projectId, "managed", ownerUsername);
|
|
1101
|
+
await writeTemplateMetadata(targetDir, templateOrigin);
|
|
1102
|
+
await registerPath(remoteResult.projectId, targetDir);
|
|
1097
1103
|
} catch (err) {
|
|
1098
|
-
|
|
1099
|
-
reporter.error("Build failed");
|
|
1100
|
-
throw err;
|
|
1104
|
+
debug("Failed to link managed project:", err);
|
|
1101
1105
|
}
|
|
1102
|
-
}
|
|
1103
|
-
|
|
1104
|
-
reporter.start("Deploying...");
|
|
1105
1106
|
|
|
1106
|
-
|
|
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
|
+
}
|
|
1107
1120
|
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
reported: hasReporter,
|
|
1115
|
-
});
|
|
1116
|
-
}
|
|
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
|
|
1117
1127
|
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
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...");
|
|
1122
1142
|
try {
|
|
1123
|
-
await
|
|
1143
|
+
await runViteBuild(targetDir);
|
|
1144
|
+
reporter.stop();
|
|
1145
|
+
reporter.success("Built assets");
|
|
1124
1146
|
} catch (err) {
|
|
1125
|
-
reporter.
|
|
1126
|
-
reporter.
|
|
1147
|
+
reporter.stop();
|
|
1148
|
+
reporter.error("Build failed");
|
|
1149
|
+
throw err;
|
|
1127
1150
|
}
|
|
1128
1151
|
}
|
|
1129
|
-
}
|
|
1130
1152
|
|
|
1131
|
-
|
|
1132
|
-
const secretsJsonPath = join(targetDir, ".secrets.json");
|
|
1133
|
-
if (existsSync(secretsJsonPath)) {
|
|
1134
|
-
reporter.start("Configuring secrets...");
|
|
1153
|
+
reporter.start("Deploying...");
|
|
1135
1154
|
|
|
1136
|
-
const
|
|
1137
|
-
.cwd(targetDir)
|
|
1138
|
-
.nothrow()
|
|
1139
|
-
.quiet();
|
|
1155
|
+
const deployResult = await runWranglerDeploy(targetDir);
|
|
1140
1156
|
|
|
1141
|
-
if (
|
|
1157
|
+
if (deployResult.exitCode !== 0) {
|
|
1142
1158
|
reporter.stop();
|
|
1143
|
-
reporter.
|
|
1144
|
-
|
|
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}`);
|
|
1145
1208
|
} else {
|
|
1146
|
-
reporter.
|
|
1147
|
-
reporter.success("Secrets configured");
|
|
1209
|
+
reporter.success("Deployed");
|
|
1148
1210
|
}
|
|
1149
|
-
}
|
|
1150
1211
|
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
const urlMatch = deployOutput.match(/https:\/\/[\w-]+\.[\w-]+\.workers\.dev/);
|
|
1154
|
-
workerUrl = urlMatch ? urlMatch[0] : null;
|
|
1212
|
+
// Generate BYO project ID and link locally
|
|
1213
|
+
const byoProjectId = generateByoProjectId();
|
|
1155
1214
|
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
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
|
+
}
|
|
1161
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") });
|
|
1162
1241
|
|
|
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);
|
|
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
|
+
}
|
|
1173
1246
|
}
|
|
1174
|
-
}
|
|
1175
1247
|
|
|
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
|
-
);
|
|
1248
|
+
// Print timing summary (only shown with --debug)
|
|
1249
|
+
printTimingSummary(timings);
|
|
1189
1250
|
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
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
|
+
}
|
|
1193
1266
|
}
|
|
1267
|
+
throw error;
|
|
1194
1268
|
}
|
|
1195
|
-
|
|
1196
|
-
return {
|
|
1197
|
-
projectName,
|
|
1198
|
-
targetDir,
|
|
1199
|
-
workerUrl,
|
|
1200
|
-
deployMode,
|
|
1201
|
-
};
|
|
1202
1269
|
}
|
|
1203
1270
|
|
|
1204
1271
|
// ============================================================================
|
|
@@ -1272,8 +1339,21 @@ export async function deployProject(options: DeployOptions = {}): Promise<Deploy
|
|
|
1272
1339
|
deployMode = link?.deploy_mode ?? "byo";
|
|
1273
1340
|
}
|
|
1274
1341
|
|
|
1275
|
-
//
|
|
1342
|
+
// Ensure wrangler is installed (auto-install if needed)
|
|
1276
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
|
|
1277
1357
|
const modeError = await validateModeAvailability(deployMode);
|
|
1278
1358
|
if (modeError) {
|
|
1279
1359
|
throw new JackError(JackErrorCode.VALIDATION_ERROR, modeError);
|
|
@@ -373,6 +373,18 @@ export async function listAllProjects(): Promise<ResolvedProject[]> {
|
|
|
373
373
|
projectMap.set(managed.id, resolved);
|
|
374
374
|
}
|
|
375
375
|
}
|
|
376
|
+
|
|
377
|
+
// Mark orphaned local managed projects (not found on control plane) as errors
|
|
378
|
+
for (const [, project] of projectMap) {
|
|
379
|
+
if (
|
|
380
|
+
project.deployMode === "managed" &&
|
|
381
|
+
project.status === "syncing" &&
|
|
382
|
+
!project.sources.controlPlane
|
|
383
|
+
) {
|
|
384
|
+
project.status = "error";
|
|
385
|
+
project.errorMessage = "Project not found in jack cloud. Run: jack unlink && jack ship";
|
|
386
|
+
}
|
|
387
|
+
}
|
|
376
388
|
} catch {
|
|
377
389
|
// Control plane unavailable, use local-only data
|
|
378
390
|
}
|
|
@@ -50,11 +50,11 @@
|
|
|
50
50
|
"path": "public/.well-known/farcaster.json",
|
|
51
51
|
"successMessage": "Updated manifest URLs to {{url}}",
|
|
52
52
|
"set": {
|
|
53
|
-
"
|
|
54
|
-
"
|
|
55
|
-
"
|
|
56
|
-
"
|
|
57
|
-
"
|
|
53
|
+
"frame.name": "{{name}}",
|
|
54
|
+
"frame.homeUrl": "{{url}}",
|
|
55
|
+
"frame.iconUrl": "{{url}}/icon.png",
|
|
56
|
+
"frame.imageUrl": "{{url}}/og.png",
|
|
57
|
+
"frame.splashImageUrl": "{{url}}/icon.png"
|
|
58
58
|
}
|
|
59
59
|
},
|
|
60
60
|
{
|
|
@@ -1,6 +1,74 @@
|
|
|
1
1
|
import { hc } from "hono/client";
|
|
2
2
|
import type { AppType } from "../worker";
|
|
3
3
|
|
|
4
|
-
//
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
// Create Hono RPC client
|
|
5
|
+
// Note: Type inference may show 'unknown' in IDE but works at runtime
|
|
6
|
+
// The explicit response types below ensure type safety for API consumers
|
|
7
|
+
const client = hc<AppType>("/");
|
|
8
|
+
|
|
9
|
+
// Export typed client - if AppType inference fails, these explicit types provide safety
|
|
10
|
+
export const api = client as {
|
|
11
|
+
api: {
|
|
12
|
+
guestbook: {
|
|
13
|
+
$get: () => Promise<
|
|
14
|
+
Response & {
|
|
15
|
+
json: () => Promise<{
|
|
16
|
+
entries: Array<{
|
|
17
|
+
id: number;
|
|
18
|
+
fid: number;
|
|
19
|
+
username: string;
|
|
20
|
+
display_name: string | null;
|
|
21
|
+
pfp_url: string | null;
|
|
22
|
+
message: string;
|
|
23
|
+
created_at: string;
|
|
24
|
+
}>;
|
|
25
|
+
}>;
|
|
26
|
+
}
|
|
27
|
+
>;
|
|
28
|
+
$post: (options: {
|
|
29
|
+
json: {
|
|
30
|
+
fid: number;
|
|
31
|
+
username: string;
|
|
32
|
+
displayName?: string;
|
|
33
|
+
pfpUrl?: string;
|
|
34
|
+
message: string;
|
|
35
|
+
};
|
|
36
|
+
}) => Promise<
|
|
37
|
+
Response & {
|
|
38
|
+
json: () => Promise<{
|
|
39
|
+
entry?: {
|
|
40
|
+
id: number;
|
|
41
|
+
fid: number;
|
|
42
|
+
username: string;
|
|
43
|
+
display_name: string | null;
|
|
44
|
+
pfp_url: string | null;
|
|
45
|
+
message: string;
|
|
46
|
+
created_at: string;
|
|
47
|
+
};
|
|
48
|
+
error?: string;
|
|
49
|
+
}>;
|
|
50
|
+
}
|
|
51
|
+
>;
|
|
52
|
+
};
|
|
53
|
+
ai: {
|
|
54
|
+
generate: {
|
|
55
|
+
$post: (options: {
|
|
56
|
+
json: { prompt: string; schema?: object };
|
|
57
|
+
}) => Promise<
|
|
58
|
+
Response & {
|
|
59
|
+
json: () => Promise<{
|
|
60
|
+
result?: string;
|
|
61
|
+
provider?: "openai" | "workers-ai";
|
|
62
|
+
error?: string;
|
|
63
|
+
}>;
|
|
64
|
+
}
|
|
65
|
+
>;
|
|
66
|
+
};
|
|
67
|
+
};
|
|
68
|
+
notifications: {
|
|
69
|
+
$get: (options?: {
|
|
70
|
+
query: { fid: string };
|
|
71
|
+
}) => Promise<Response & { json: () => Promise<unknown> }>;
|
|
72
|
+
};
|
|
73
|
+
};
|
|
74
|
+
};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
// Server-side Worker - handles API routes, keeps secrets secure
|
|
2
|
+
/// <reference types="@cloudflare/workers-types" />
|
|
2
3
|
|
|
3
4
|
import { Hono } from "hono";
|
|
4
5
|
import { cors } from "hono/cors";
|
|
@@ -624,6 +625,49 @@ app.get("/share", (c) => {
|
|
|
624
625
|
});
|
|
625
626
|
});
|
|
626
627
|
|
|
628
|
+
// GET / - Inject fc:miniapp meta tags for Farcaster embeds on the main page
|
|
629
|
+
// Without this, sharing the app URL in a cast won't render an embed card
|
|
630
|
+
app.get("/", async (c) => {
|
|
631
|
+
// Fetch the static index.html from Vite build
|
|
632
|
+
const response = await c.env.ASSETS.fetch(c.req.raw);
|
|
633
|
+
const html = await response.text();
|
|
634
|
+
|
|
635
|
+
const baseUrl = getBaseUrl(c.env, c);
|
|
636
|
+
|
|
637
|
+
// Local dev - serve without meta tags (they require https URLs)
|
|
638
|
+
if (!baseUrl) {
|
|
639
|
+
return c.html(html);
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
// Build embed JSON (same structure as /share route)
|
|
643
|
+
const embedJson = JSON.stringify({
|
|
644
|
+
version: "1",
|
|
645
|
+
imageUrl: `${baseUrl}/og.png`,
|
|
646
|
+
button: {
|
|
647
|
+
title: "Open App",
|
|
648
|
+
action: {
|
|
649
|
+
type: "launch_miniapp",
|
|
650
|
+
name: "jack-template",
|
|
651
|
+
url: baseUrl,
|
|
652
|
+
splashImageUrl: `${baseUrl}/icon.png`,
|
|
653
|
+
splashBackgroundColor: "#0a0a0a",
|
|
654
|
+
},
|
|
655
|
+
},
|
|
656
|
+
});
|
|
657
|
+
|
|
658
|
+
// Meta tags to inject
|
|
659
|
+
const metaTags = `
|
|
660
|
+
<meta property="og:title" content="jack-template" />
|
|
661
|
+
<meta property="og:image" content="${baseUrl}/og.png" />
|
|
662
|
+
<meta name="fc:miniapp" content='${embedJson}' />
|
|
663
|
+
<meta name="fc:frame" content='${embedJson}' />
|
|
664
|
+
`;
|
|
665
|
+
|
|
666
|
+
// Inject before </head>
|
|
667
|
+
const injectedHtml = html.replace("</head>", `${metaTags}</head>`);
|
|
668
|
+
return c.html(injectedHtml);
|
|
669
|
+
});
|
|
670
|
+
|
|
627
671
|
// Serve React app for all other routes
|
|
628
672
|
app.get("*", (c) => c.env.ASSETS.fetch(c.req.raw));
|
|
629
673
|
|