@getjack/jack 0.1.4 → 0.1.6
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/agents.ts +9 -24
- package/src/commands/clone.ts +27 -0
- package/src/commands/down.ts +31 -57
- package/src/commands/feedback.ts +4 -5
- package/src/commands/link.ts +147 -0
- package/src/commands/login.ts +124 -1
- package/src/commands/logs.ts +8 -18
- package/src/commands/new.ts +7 -1
- package/src/commands/projects.ts +166 -105
- package/src/commands/secrets.ts +7 -6
- package/src/commands/services.ts +5 -4
- package/src/commands/tag.ts +282 -0
- package/src/commands/unlink.ts +30 -0
- package/src/index.ts +46 -1
- package/src/lib/auth/index.ts +2 -0
- package/src/lib/auth/store.ts +26 -2
- package/src/lib/binding-validator.ts +4 -13
- package/src/lib/build-helper.ts +93 -5
- package/src/lib/control-plane.ts +137 -0
- package/src/lib/deploy-mode.ts +1 -1
- package/src/lib/managed-deploy.ts +11 -1
- package/src/lib/managed-down.ts +7 -20
- package/src/lib/paths-index.test.ts +546 -0
- package/src/lib/paths-index.ts +310 -0
- package/src/lib/project-link.test.ts +459 -0
- package/src/lib/project-link.ts +279 -0
- package/src/lib/project-list.test.ts +581 -0
- package/src/lib/project-list.ts +449 -0
- package/src/lib/project-operations.ts +304 -183
- package/src/lib/project-resolver.ts +191 -211
- package/src/lib/tags.ts +389 -0
- package/src/lib/telemetry.ts +86 -157
- package/src/lib/zip-packager.ts +9 -0
- package/src/templates/index.ts +5 -3
- package/templates/api/.jack/template.json +4 -0
- package/templates/hello/.jack/template.json +4 -0
- package/templates/miniapp/.jack/template.json +4 -0
- package/templates/nextjs/.jack.json +28 -0
- package/templates/nextjs/app/globals.css +9 -0
- package/templates/nextjs/app/layout.tsx +19 -0
- package/templates/nextjs/app/page.tsx +8 -0
- package/templates/nextjs/bun.lock +2232 -0
- package/templates/nextjs/cloudflare-env.d.ts +3 -0
- package/templates/nextjs/next-env.d.ts +6 -0
- package/templates/nextjs/next.config.ts +8 -0
- package/templates/nextjs/open-next.config.ts +6 -0
- package/templates/nextjs/package.json +24 -0
- package/templates/nextjs/public/_headers +2 -0
- package/templates/nextjs/tsconfig.json +44 -0
- package/templates/nextjs/wrangler.jsonc +17 -0
- package/src/lib/local-paths.test.ts +0 -902
- package/src/lib/local-paths.ts +0 -258
- package/src/lib/registry.ts +0 -181
|
@@ -23,27 +23,33 @@ import {
|
|
|
23
23
|
runAgentOneShot,
|
|
24
24
|
validateAgentPaths,
|
|
25
25
|
} from "./agents.ts";
|
|
26
|
-
import { needsViteBuild, runViteBuild } from "./build-helper.ts";
|
|
26
|
+
import { ensureR2Buckets, needsOpenNextBuild, needsViteBuild, runOpenNextBuild, runViteBuild } from "./build-helper.ts";
|
|
27
27
|
import { checkWorkerExists, getAccountId, listD1Databases } from "./cloudflare-api.ts";
|
|
28
28
|
import { getSyncConfig } from "./config.ts";
|
|
29
|
+
import { deleteManagedProject } from "./control-plane.ts";
|
|
29
30
|
import { debug, isDebug } from "./debug.ts";
|
|
30
31
|
import { resolveDeployMode, validateModeAvailability } from "./deploy-mode.ts";
|
|
31
32
|
import { detectSecrets, generateEnvFile, generateSecretsJson } from "./env-parser.ts";
|
|
32
33
|
import { JackError, JackErrorCode } from "./errors.ts";
|
|
33
34
|
import { type HookOutput, runHook } from "./hooks.ts";
|
|
34
35
|
import { loadTemplateKeywords, matchTemplateByIntent } from "./intent.ts";
|
|
35
|
-
import {
|
|
36
|
-
|
|
36
|
+
import {
|
|
37
|
+
type ManagedCreateResult,
|
|
38
|
+
createManagedProjectRemote,
|
|
39
|
+
deployToManagedProject,
|
|
40
|
+
} from "./managed-deploy.ts";
|
|
37
41
|
import { generateProjectName } from "./names.ts";
|
|
38
|
-
import {
|
|
39
|
-
import type { DeployMode, TemplateOrigin } from "./registry.ts";
|
|
42
|
+
import { getAllPaths, registerPath, unregisterPath } from "./paths-index.ts";
|
|
40
43
|
import {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
44
|
+
type DeployMode,
|
|
45
|
+
type TemplateMetadata as TemplateOrigin,
|
|
46
|
+
generateByoProjectId,
|
|
47
|
+
linkProject,
|
|
48
|
+
readProjectLink,
|
|
49
|
+
unlinkProject,
|
|
50
|
+
writeTemplateMetadata,
|
|
51
|
+
} from "./project-link.ts";
|
|
52
|
+
import { filterNewSecrets, promptSaveSecrets } from "./prompts.ts";
|
|
47
53
|
import { applySchema, getD1Bindings, getD1DatabaseName, hasD1Config } from "./schema.ts";
|
|
48
54
|
import { getSavedSecrets, saveSecrets } from "./secrets.ts";
|
|
49
55
|
import { getProjectNameFromDir, getRemoteManifest, syncToCloud } from "./storage/index.ts";
|
|
@@ -146,6 +152,90 @@ const noopReporter: OperationReporter = {
|
|
|
146
152
|
box() {},
|
|
147
153
|
};
|
|
148
154
|
|
|
155
|
+
/**
|
|
156
|
+
* Run bun install and managed project creation in parallel.
|
|
157
|
+
* Handles partial failures with cleanup.
|
|
158
|
+
*/
|
|
159
|
+
async function runParallelSetup(
|
|
160
|
+
targetDir: string,
|
|
161
|
+
projectName: string,
|
|
162
|
+
options: {
|
|
163
|
+
template?: string;
|
|
164
|
+
usePrebuilt?: boolean;
|
|
165
|
+
},
|
|
166
|
+
): Promise<{
|
|
167
|
+
installSuccess: boolean;
|
|
168
|
+
remoteResult: ManagedCreateResult;
|
|
169
|
+
}> {
|
|
170
|
+
const [installResult, remoteResult] = await Promise.allSettled([
|
|
171
|
+
// Install dependencies
|
|
172
|
+
(async () => {
|
|
173
|
+
const install = Bun.spawn(["bun", "install"], {
|
|
174
|
+
cwd: targetDir,
|
|
175
|
+
stdout: "ignore",
|
|
176
|
+
stderr: "ignore",
|
|
177
|
+
});
|
|
178
|
+
await install.exited;
|
|
179
|
+
if (install.exitCode !== 0) {
|
|
180
|
+
throw new Error("Dependency installation failed");
|
|
181
|
+
}
|
|
182
|
+
return true;
|
|
183
|
+
})(),
|
|
184
|
+
|
|
185
|
+
// Create managed project remote (no reporter to avoid spinner conflicts)
|
|
186
|
+
createManagedProjectRemote(projectName, undefined, {
|
|
187
|
+
template: options.template || "hello",
|
|
188
|
+
usePrebuilt: options.usePrebuilt ?? true,
|
|
189
|
+
}),
|
|
190
|
+
]);
|
|
191
|
+
|
|
192
|
+
const installFailed = installResult.status === "rejected";
|
|
193
|
+
const remoteFailed = remoteResult.status === "rejected";
|
|
194
|
+
|
|
195
|
+
// Handle partial failures
|
|
196
|
+
if (installFailed && remoteResult.status === "fulfilled") {
|
|
197
|
+
// Install failed but remote succeeded - cleanup orphan cloud project
|
|
198
|
+
const remote = remoteResult.value;
|
|
199
|
+
try {
|
|
200
|
+
await deleteManagedProject(remote.projectId);
|
|
201
|
+
debug("Cleaned up orphan cloud project:", remote.projectId);
|
|
202
|
+
} catch (cleanupErr) {
|
|
203
|
+
debug("Failed to cleanup orphan cloud project:", cleanupErr);
|
|
204
|
+
}
|
|
205
|
+
throw new JackError(
|
|
206
|
+
JackErrorCode.BUILD_FAILED,
|
|
207
|
+
"Dependency installation failed",
|
|
208
|
+
"Run: bun install",
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (!installFailed && remoteResult.status === "rejected") {
|
|
213
|
+
// Remote failed but install succeeded - throw remote error
|
|
214
|
+
const error = remoteResult.reason;
|
|
215
|
+
throw error instanceof Error ? error : new Error(String(error));
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (installFailed && remoteFailed) {
|
|
219
|
+
// Both failed - prioritize install error (more actionable)
|
|
220
|
+
throw new JackError(
|
|
221
|
+
JackErrorCode.BUILD_FAILED,
|
|
222
|
+
"Dependency installation failed",
|
|
223
|
+
"Run: bun install",
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Both succeeded - TypeScript knows remoteResult.status === "fulfilled" here
|
|
228
|
+
if (remoteResult.status !== "fulfilled") {
|
|
229
|
+
// Should never happen, but satisfies TypeScript
|
|
230
|
+
throw new Error("Unexpected state: remote result not fulfilled");
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return {
|
|
234
|
+
installSuccess: true,
|
|
235
|
+
remoteResult: remoteResult.value,
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
|
|
149
239
|
const DEFAULT_D1_LIMIT = 10;
|
|
150
240
|
|
|
151
241
|
async function preflightD1Capacity(
|
|
@@ -291,24 +381,9 @@ export async function createProject(
|
|
|
291
381
|
const choice = await promptSelect(["Link existing project", "Choose different name"]);
|
|
292
382
|
|
|
293
383
|
if (choice === 0) {
|
|
294
|
-
// User chose to link -
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
createdAt: existingProject.createdAt,
|
|
298
|
-
lastDeployed: existingProject.updatedAt || null,
|
|
299
|
-
status: existingProject.status === "live" ? "live" : "build_failed",
|
|
300
|
-
deploy_mode: "managed",
|
|
301
|
-
remote: existingProject.remote
|
|
302
|
-
? {
|
|
303
|
-
project_id: existingProject.remote.projectId,
|
|
304
|
-
project_slug: existingProject.slug,
|
|
305
|
-
org_id: existingProject.remote.orgId,
|
|
306
|
-
runjack_url:
|
|
307
|
-
existingProject.url || `https://${existingProject.slug}.runjack.xyz`,
|
|
308
|
-
}
|
|
309
|
-
: undefined,
|
|
310
|
-
});
|
|
311
|
-
reporter.success(`Linked to existing project: ${existingProject.url || projectName}`);
|
|
384
|
+
// User chose to link - proceed with project creation
|
|
385
|
+
// The project will be linked locally when files are created
|
|
386
|
+
reporter.success(`Linking to existing project: ${existingProject.url || projectName}`);
|
|
312
387
|
// Continue with project creation - user wants to link
|
|
313
388
|
} else {
|
|
314
389
|
// User chose different name
|
|
@@ -570,15 +645,8 @@ export async function createProject(
|
|
|
570
645
|
const validation = await validateAgentPaths();
|
|
571
646
|
|
|
572
647
|
if (validation.invalid.length > 0) {
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
for (const { id, path } of validation.invalid) {
|
|
576
|
-
reporter.info(` ${id}: ${path}`);
|
|
577
|
-
}
|
|
578
|
-
reporter.info("Run: jack agents scan");
|
|
579
|
-
reporter.start("Creating project...");
|
|
580
|
-
|
|
581
|
-
// Filter out invalid agents
|
|
648
|
+
// Silently filter out agents with missing paths
|
|
649
|
+
// User can run 'jack agents scan' to see/fix agent config
|
|
582
650
|
activeAgents = activeAgents.filter(
|
|
583
651
|
({ id }) => !validation.invalid.some((inv) => inv.id === id),
|
|
584
652
|
);
|
|
@@ -596,30 +664,55 @@ export async function createProject(
|
|
|
596
664
|
reporter.stop();
|
|
597
665
|
reporter.success(`Created ${projectName}/`);
|
|
598
666
|
|
|
599
|
-
//
|
|
600
|
-
|
|
667
|
+
// Parallel setup for managed mode: install + remote creation
|
|
668
|
+
let remoteResult: ManagedCreateResult | undefined;
|
|
601
669
|
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
670
|
+
if (deployMode === "managed") {
|
|
671
|
+
// Run install and remote creation in parallel
|
|
672
|
+
reporter.start("Setting up project...");
|
|
673
|
+
|
|
674
|
+
try {
|
|
675
|
+
const result = await runParallelSetup(targetDir, projectName, {
|
|
676
|
+
template: resolvedTemplate || "hello",
|
|
677
|
+
usePrebuilt: true,
|
|
678
|
+
});
|
|
679
|
+
remoteResult = result.remoteResult;
|
|
680
|
+
reporter.stop();
|
|
681
|
+
reporter.success("Project setup complete");
|
|
682
|
+
} catch (err) {
|
|
683
|
+
reporter.stop();
|
|
684
|
+
if (err instanceof JackError) {
|
|
685
|
+
reporter.warn(err.suggestion ?? err.message);
|
|
686
|
+
throw err;
|
|
687
|
+
}
|
|
688
|
+
throw err;
|
|
689
|
+
}
|
|
690
|
+
} else {
|
|
691
|
+
// BYO mode: just install dependencies (unchanged from current)
|
|
692
|
+
reporter.start("Installing dependencies...");
|
|
693
|
+
|
|
694
|
+
const install = Bun.spawn(["bun", "install"], {
|
|
695
|
+
cwd: targetDir,
|
|
696
|
+
stdout: "ignore",
|
|
697
|
+
stderr: "ignore",
|
|
698
|
+
});
|
|
699
|
+
await install.exited;
|
|
700
|
+
|
|
701
|
+
if (install.exitCode !== 0) {
|
|
702
|
+
reporter.stop();
|
|
703
|
+
reporter.warn("Failed to install dependencies, run: bun install");
|
|
704
|
+
throw new JackError(
|
|
705
|
+
JackErrorCode.BUILD_FAILED,
|
|
706
|
+
"Dependency installation failed",
|
|
707
|
+
"Run: bun install",
|
|
708
|
+
{ exitCode: 0, reported: hasReporter },
|
|
709
|
+
);
|
|
710
|
+
}
|
|
608
711
|
|
|
609
|
-
if (install.exitCode !== 0) {
|
|
610
712
|
reporter.stop();
|
|
611
|
-
reporter.
|
|
612
|
-
throw new JackError(
|
|
613
|
-
JackErrorCode.BUILD_FAILED,
|
|
614
|
-
"Dependency installation failed",
|
|
615
|
-
"Run: bun install",
|
|
616
|
-
{ exitCode: 0, reported: hasReporter },
|
|
617
|
-
);
|
|
713
|
+
reporter.success("Dependencies installed");
|
|
618
714
|
}
|
|
619
715
|
|
|
620
|
-
reporter.stop();
|
|
621
|
-
reporter.success("Dependencies installed");
|
|
622
|
-
|
|
623
716
|
// Run pre-deploy hooks
|
|
624
717
|
if (template.hooks?.preDeploy?.length) {
|
|
625
718
|
const hookContext = { projectName, projectDir: targetDir };
|
|
@@ -685,30 +778,22 @@ export async function createProject(
|
|
|
685
778
|
|
|
686
779
|
// Deploy based on mode
|
|
687
780
|
if (deployMode === "managed") {
|
|
688
|
-
// Managed mode:
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
781
|
+
// Managed mode: remote was already created in parallel setup
|
|
782
|
+
if (!remoteResult) {
|
|
783
|
+
throw new JackError(
|
|
784
|
+
JackErrorCode.VALIDATION_ERROR,
|
|
785
|
+
"Managed project was not created",
|
|
786
|
+
"This is an internal error - please report it",
|
|
787
|
+
);
|
|
788
|
+
}
|
|
693
789
|
|
|
694
|
-
//
|
|
790
|
+
// Link project locally and register path
|
|
695
791
|
try {
|
|
696
|
-
await
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
lastDeployed: remoteResult.status === "live" ? new Date().toISOString() : null,
|
|
700
|
-
status: remoteResult.status === "live" ? "live" : "created",
|
|
701
|
-
template: templateOrigin,
|
|
702
|
-
deploy_mode: "managed",
|
|
703
|
-
remote: {
|
|
704
|
-
project_id: remoteResult.projectId,
|
|
705
|
-
project_slug: remoteResult.projectSlug,
|
|
706
|
-
org_id: remoteResult.orgId,
|
|
707
|
-
runjack_url: remoteResult.runjackUrl,
|
|
708
|
-
},
|
|
709
|
-
});
|
|
792
|
+
await linkProject(targetDir, remoteResult.projectId, "managed");
|
|
793
|
+
await writeTemplateMetadata(targetDir, templateOrigin);
|
|
794
|
+
await registerPath(remoteResult.projectId, targetDir);
|
|
710
795
|
} catch (err) {
|
|
711
|
-
debug("Failed to
|
|
796
|
+
debug("Failed to link managed project:", err);
|
|
712
797
|
}
|
|
713
798
|
|
|
714
799
|
// Check if prebuilt deployment succeeded
|
|
@@ -725,38 +810,26 @@ export async function createProject(
|
|
|
725
810
|
reporter.info("Pre-built not available, building fresh...");
|
|
726
811
|
}
|
|
727
812
|
|
|
728
|
-
|
|
729
|
-
await deployToManagedProject(remoteResult.projectId, targetDir, reporter);
|
|
730
|
-
} catch (err) {
|
|
731
|
-
try {
|
|
732
|
-
await updateProject(projectName, {
|
|
733
|
-
status: "build_failed",
|
|
734
|
-
workerUrl: remoteResult.runjackUrl,
|
|
735
|
-
});
|
|
736
|
-
} catch (updateErr) {
|
|
737
|
-
debug("Failed to update managed project status:", updateErr);
|
|
738
|
-
}
|
|
739
|
-
throw err;
|
|
740
|
-
}
|
|
813
|
+
await deployToManagedProject(remoteResult.projectId, targetDir, reporter);
|
|
741
814
|
workerUrl = remoteResult.runjackUrl;
|
|
742
815
|
reporter.success(`Created: ${workerUrl}`);
|
|
743
|
-
|
|
744
|
-
// Update project status to live after successful fresh build
|
|
745
|
-
try {
|
|
746
|
-
await updateProject(projectName, {
|
|
747
|
-
lastDeployed: new Date().toISOString(),
|
|
748
|
-
status: "live",
|
|
749
|
-
});
|
|
750
|
-
} catch (err) {
|
|
751
|
-
// Log but don't fail - registry is convenience, not critical path
|
|
752
|
-
debug("Failed to update managed project status:", err);
|
|
753
|
-
}
|
|
754
816
|
}
|
|
755
817
|
} else {
|
|
756
818
|
// BYO mode: deploy via wrangler
|
|
757
819
|
|
|
758
|
-
// Build first if needed (wrangler needs
|
|
759
|
-
if (await
|
|
820
|
+
// Build first if needed (wrangler needs built assets)
|
|
821
|
+
if (await needsOpenNextBuild(targetDir)) {
|
|
822
|
+
reporter.start("Building...");
|
|
823
|
+
try {
|
|
824
|
+
await runOpenNextBuild(targetDir);
|
|
825
|
+
reporter.stop();
|
|
826
|
+
reporter.success("Built");
|
|
827
|
+
} catch (err) {
|
|
828
|
+
reporter.stop();
|
|
829
|
+
reporter.error("Build failed");
|
|
830
|
+
throw err;
|
|
831
|
+
}
|
|
832
|
+
} else if (await needsViteBuild(targetDir)) {
|
|
760
833
|
reporter.start("Building...");
|
|
761
834
|
try {
|
|
762
835
|
await runViteBuild(targetDir);
|
|
@@ -828,23 +901,16 @@ export async function createProject(
|
|
|
828
901
|
reporter.success("Deployed");
|
|
829
902
|
}
|
|
830
903
|
|
|
831
|
-
//
|
|
904
|
+
// Generate BYO project ID and link locally
|
|
905
|
+
const byoProjectId = generateByoProjectId();
|
|
906
|
+
|
|
907
|
+
// Link project locally and register path
|
|
832
908
|
try {
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
await
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
lastDeployed: workerUrl ? new Date().toISOString() : null,
|
|
839
|
-
cloudflare: {
|
|
840
|
-
accountId,
|
|
841
|
-
workerId: projectName,
|
|
842
|
-
},
|
|
843
|
-
template: templateOrigin,
|
|
844
|
-
deploy_mode: "byo",
|
|
845
|
-
});
|
|
846
|
-
} catch {
|
|
847
|
-
// Don't fail the creation if registry update fails
|
|
909
|
+
await linkProject(targetDir, byoProjectId, "byo");
|
|
910
|
+
await writeTemplateMetadata(targetDir, templateOrigin);
|
|
911
|
+
await registerPath(byoProjectId, targetDir);
|
|
912
|
+
} catch (err) {
|
|
913
|
+
debug("Failed to link BYO project:", err);
|
|
848
914
|
}
|
|
849
915
|
}
|
|
850
916
|
|
|
@@ -863,13 +929,6 @@ export async function createProject(
|
|
|
863
929
|
);
|
|
864
930
|
}
|
|
865
931
|
|
|
866
|
-
// Auto-register local path for project discovery
|
|
867
|
-
try {
|
|
868
|
-
await registerLocalPath(projectName, targetDir);
|
|
869
|
-
} catch {
|
|
870
|
-
// Silent fail - registration is best-effort
|
|
871
|
-
}
|
|
872
|
-
|
|
873
932
|
return {
|
|
874
933
|
projectName,
|
|
875
934
|
targetDir,
|
|
@@ -926,8 +985,8 @@ export async function deployProject(options: DeployOptions = {}): Promise<Deploy
|
|
|
926
985
|
// Get project name from directory
|
|
927
986
|
const projectName = await getProjectNameFromDir(projectPath);
|
|
928
987
|
|
|
929
|
-
//
|
|
930
|
-
const
|
|
988
|
+
// Read local project link for stored mode and project ID
|
|
989
|
+
const link = await readProjectLink(projectPath);
|
|
931
990
|
|
|
932
991
|
// Determine effective mode: explicit flag > stored mode > default BYO
|
|
933
992
|
let deployMode: DeployMode;
|
|
@@ -936,7 +995,7 @@ export async function deployProject(options: DeployOptions = {}): Promise<Deploy
|
|
|
936
995
|
} else if (options.byo) {
|
|
937
996
|
deployMode = "byo";
|
|
938
997
|
} else {
|
|
939
|
-
deployMode =
|
|
998
|
+
deployMode = link?.deploy_mode ?? "byo";
|
|
940
999
|
}
|
|
941
1000
|
|
|
942
1001
|
// Validate mode availability
|
|
@@ -951,7 +1010,7 @@ export async function deployProject(options: DeployOptions = {}): Promise<Deploy
|
|
|
951
1010
|
// Deploy based on mode
|
|
952
1011
|
if (deployMode === "managed") {
|
|
953
1012
|
// Managed mode: deploy via jack cloud
|
|
954
|
-
if (!
|
|
1013
|
+
if (!link?.project_id || link.deploy_mode !== "managed") {
|
|
955
1014
|
throw new JackError(
|
|
956
1015
|
JackErrorCode.VALIDATION_ERROR,
|
|
957
1016
|
"Project not linked to jack cloud",
|
|
@@ -960,19 +1019,24 @@ export async function deployProject(options: DeployOptions = {}): Promise<Deploy
|
|
|
960
1019
|
}
|
|
961
1020
|
|
|
962
1021
|
// deployToManagedProject now handles both template and code deploy
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
workerUrl = project.remote.runjack_url;
|
|
1022
|
+
await deployToManagedProject(link.project_id, projectPath, reporter);
|
|
966
1023
|
|
|
967
|
-
//
|
|
968
|
-
|
|
969
|
-
project.lastDeployed = new Date().toISOString();
|
|
970
|
-
}
|
|
1024
|
+
// Get the URL from the resolver or construct it
|
|
1025
|
+
workerUrl = `https://${projectName}.runjack.xyz`;
|
|
971
1026
|
} else {
|
|
972
1027
|
// BYO mode: deploy via wrangler
|
|
973
1028
|
|
|
974
|
-
// Build first if needed (wrangler needs
|
|
975
|
-
if (await
|
|
1029
|
+
// Build first if needed (wrangler needs built assets)
|
|
1030
|
+
if (await needsOpenNextBuild(projectPath)) {
|
|
1031
|
+
const buildSpin = reporter.spinner("Building...");
|
|
1032
|
+
try {
|
|
1033
|
+
await runOpenNextBuild(projectPath);
|
|
1034
|
+
buildSpin.success("Built");
|
|
1035
|
+
} catch (err) {
|
|
1036
|
+
buildSpin.error("Build failed");
|
|
1037
|
+
throw err;
|
|
1038
|
+
}
|
|
1039
|
+
} else if (await needsViteBuild(projectPath)) {
|
|
976
1040
|
const buildSpin = reporter.spinner("Building...");
|
|
977
1041
|
try {
|
|
978
1042
|
await runViteBuild(projectPath);
|
|
@@ -983,6 +1047,17 @@ export async function deployProject(options: DeployOptions = {}): Promise<Deploy
|
|
|
983
1047
|
}
|
|
984
1048
|
}
|
|
985
1049
|
|
|
1050
|
+
// Ensure R2 buckets exist before deploying (omakase - auto-provision)
|
|
1051
|
+
try {
|
|
1052
|
+
const buckets = await ensureR2Buckets(projectPath);
|
|
1053
|
+
if (buckets.length > 0) {
|
|
1054
|
+
reporter.info(`R2 buckets ready: ${buckets.join(", ")}`);
|
|
1055
|
+
}
|
|
1056
|
+
} catch (err) {
|
|
1057
|
+
// Non-fatal: let wrangler deploy fail with a clearer error if bucket is missing
|
|
1058
|
+
debug("R2 preflight failed:", err);
|
|
1059
|
+
}
|
|
1060
|
+
|
|
986
1061
|
const spin = reporter.spinner("Deploying...");
|
|
987
1062
|
const result = await $`wrangler deploy`.cwd(projectPath).nothrow().quiet();
|
|
988
1063
|
|
|
@@ -1021,16 +1096,6 @@ export async function deployProject(options: DeployOptions = {}): Promise<Deploy
|
|
|
1021
1096
|
}
|
|
1022
1097
|
}
|
|
1023
1098
|
|
|
1024
|
-
// Update registry
|
|
1025
|
-
try {
|
|
1026
|
-
await registerProject(projectName, {
|
|
1027
|
-
workerUrl,
|
|
1028
|
-
lastDeployed: new Date().toISOString(),
|
|
1029
|
-
});
|
|
1030
|
-
} catch {
|
|
1031
|
-
// Don't fail the deploy if registry update fails
|
|
1032
|
-
}
|
|
1033
|
-
|
|
1034
1099
|
if (includeSecrets && interactive) {
|
|
1035
1100
|
const detected = await detectSecrets(projectPath);
|
|
1036
1101
|
const newSecrets = await filterNewSecrets(detected);
|
|
@@ -1063,9 +1128,24 @@ export async function deployProject(options: DeployOptions = {}): Promise<Deploy
|
|
|
1063
1128
|
}
|
|
1064
1129
|
}
|
|
1065
1130
|
|
|
1066
|
-
//
|
|
1131
|
+
// Ensure project is linked locally for discovery
|
|
1067
1132
|
try {
|
|
1068
|
-
await
|
|
1133
|
+
const existingLink = await readProjectLink(projectPath);
|
|
1134
|
+
if (!existingLink) {
|
|
1135
|
+
// Not linked yet - create link
|
|
1136
|
+
if (deployMode === "managed" && link?.project_id) {
|
|
1137
|
+
await linkProject(projectPath, link.project_id, "managed");
|
|
1138
|
+
await registerPath(link.project_id, projectPath);
|
|
1139
|
+
} else {
|
|
1140
|
+
// BYO mode - generate new ID
|
|
1141
|
+
const byoProjectId = generateByoProjectId();
|
|
1142
|
+
await linkProject(projectPath, byoProjectId, "byo");
|
|
1143
|
+
await registerPath(byoProjectId, projectPath);
|
|
1144
|
+
}
|
|
1145
|
+
} else {
|
|
1146
|
+
// Already linked - just ensure path is registered
|
|
1147
|
+
await registerPath(existingLink.project_id, projectPath);
|
|
1148
|
+
}
|
|
1069
1149
|
} catch {
|
|
1070
1150
|
// Silent fail - registration is best-effort
|
|
1071
1151
|
}
|
|
@@ -1108,11 +1188,8 @@ export async function getProjectStatus(
|
|
|
1108
1188
|
}
|
|
1109
1189
|
}
|
|
1110
1190
|
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
if (!project) {
|
|
1114
|
-
return null;
|
|
1115
|
-
}
|
|
1191
|
+
// Read local project link
|
|
1192
|
+
const link = await readProjectLink(resolvedPath);
|
|
1116
1193
|
|
|
1117
1194
|
// Check if local project exists at the resolved path
|
|
1118
1195
|
const hasWranglerConfig =
|
|
@@ -1122,6 +1199,11 @@ export async function getProjectStatus(
|
|
|
1122
1199
|
const localExists = hasWranglerConfig;
|
|
1123
1200
|
const localPath = localExists ? resolvedPath : null;
|
|
1124
1201
|
|
|
1202
|
+
// If no link and no local project, return null
|
|
1203
|
+
if (!link && !localExists) {
|
|
1204
|
+
return null;
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1125
1207
|
// Check actual deployment status
|
|
1126
1208
|
const [workerExists, manifest] = await Promise.all([
|
|
1127
1209
|
checkWorkerExists(projectName),
|
|
@@ -1131,13 +1213,19 @@ export async function getProjectStatus(
|
|
|
1131
1213
|
const backupFiles = manifest ? manifest.files.length : null;
|
|
1132
1214
|
const backupLastSync = manifest ? manifest.lastSync : null;
|
|
1133
1215
|
|
|
1216
|
+
// Determine URL based on mode
|
|
1217
|
+
let workerUrl: string | null = null;
|
|
1218
|
+
if (link?.deploy_mode === "managed") {
|
|
1219
|
+
workerUrl = `https://${projectName}.runjack.xyz`;
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1134
1222
|
// Get database name on-demand
|
|
1135
1223
|
let dbName: string | null = null;
|
|
1136
|
-
if (
|
|
1224
|
+
if (link?.deploy_mode === "managed") {
|
|
1137
1225
|
// For managed projects, fetch from control plane
|
|
1138
1226
|
try {
|
|
1139
1227
|
const { fetchProjectResources } = await import("./control-plane.ts");
|
|
1140
|
-
const resources = await fetchProjectResources(
|
|
1228
|
+
const resources = await fetchProjectResources(link.project_id);
|
|
1141
1229
|
const d1 = resources.find((r) => r.resource_type === "d1");
|
|
1142
1230
|
dbName = d1?.resource_name || null;
|
|
1143
1231
|
} catch {
|
|
@@ -1157,16 +1245,16 @@ export async function getProjectStatus(
|
|
|
1157
1245
|
return {
|
|
1158
1246
|
name: projectName,
|
|
1159
1247
|
localPath,
|
|
1160
|
-
workerUrl
|
|
1161
|
-
lastDeployed:
|
|
1162
|
-
createdAt:
|
|
1163
|
-
accountId:
|
|
1164
|
-
workerId:
|
|
1248
|
+
workerUrl,
|
|
1249
|
+
lastDeployed: link?.linked_at ?? null,
|
|
1250
|
+
createdAt: link?.linked_at ?? null,
|
|
1251
|
+
accountId: null, // No longer stored in registry
|
|
1252
|
+
workerId: projectName,
|
|
1165
1253
|
dbName,
|
|
1166
|
-
deployed: workerExists || !!
|
|
1254
|
+
deployed: workerExists || !!workerUrl,
|
|
1167
1255
|
local: localExists,
|
|
1168
1256
|
backedUp,
|
|
1169
|
-
missing: false,
|
|
1257
|
+
missing: false,
|
|
1170
1258
|
backupFiles,
|
|
1171
1259
|
backupLastSync,
|
|
1172
1260
|
};
|
|
@@ -1177,44 +1265,77 @@ export async function getProjectStatus(
|
|
|
1177
1265
|
// ============================================================================
|
|
1178
1266
|
|
|
1179
1267
|
/**
|
|
1180
|
-
* Scan
|
|
1181
|
-
* Checks for
|
|
1268
|
+
* Scan for stale project paths.
|
|
1269
|
+
* Checks for paths in the index that no longer exist on disk or don't have valid links.
|
|
1182
1270
|
* Returns total project count and stale entries with reasons.
|
|
1183
1271
|
*/
|
|
1184
1272
|
export async function scanStaleProjects(): Promise<StaleProjectScan> {
|
|
1185
|
-
const
|
|
1186
|
-
const
|
|
1273
|
+
const allPaths = await getAllPaths();
|
|
1274
|
+
const projectIds = Object.keys(allPaths);
|
|
1187
1275
|
const stale: StaleProject[] = [];
|
|
1276
|
+
let totalPaths = 0;
|
|
1277
|
+
|
|
1278
|
+
for (const projectId of projectIds) {
|
|
1279
|
+
const paths = allPaths[projectId] || [];
|
|
1280
|
+
totalPaths += paths.length;
|
|
1281
|
+
|
|
1282
|
+
for (const projectPath of paths) {
|
|
1283
|
+
// Check if path exists and has valid link
|
|
1284
|
+
const hasWranglerConfig =
|
|
1285
|
+
existsSync(join(projectPath, "wrangler.jsonc")) ||
|
|
1286
|
+
existsSync(join(projectPath, "wrangler.toml")) ||
|
|
1287
|
+
existsSync(join(projectPath, "wrangler.json"));
|
|
1288
|
+
|
|
1289
|
+
if (!hasWranglerConfig) {
|
|
1290
|
+
// Try to get project name from the path
|
|
1291
|
+
let name = projectPath.split("/").pop() || projectId;
|
|
1292
|
+
try {
|
|
1293
|
+
name = await getProjectNameFromDir(projectPath);
|
|
1294
|
+
} catch {
|
|
1295
|
+
// Use path basename as fallback
|
|
1296
|
+
}
|
|
1188
1297
|
|
|
1189
|
-
for (const name of projectNames) {
|
|
1190
|
-
const project = projects[name];
|
|
1191
|
-
if (!project) continue;
|
|
1192
|
-
|
|
1193
|
-
// Check if worker URL is set but worker doesn't exist
|
|
1194
|
-
if (project.workerUrl) {
|
|
1195
|
-
const workerExists = await checkWorkerExists(name);
|
|
1196
|
-
if (!workerExists) {
|
|
1197
1298
|
stale.push({
|
|
1198
1299
|
name,
|
|
1199
1300
|
reason: "worker not deployed",
|
|
1200
|
-
workerUrl:
|
|
1301
|
+
workerUrl: null,
|
|
1201
1302
|
});
|
|
1202
1303
|
}
|
|
1203
1304
|
}
|
|
1204
1305
|
}
|
|
1205
1306
|
|
|
1206
|
-
return { total:
|
|
1307
|
+
return { total: totalPaths, stale };
|
|
1207
1308
|
}
|
|
1208
1309
|
|
|
1209
1310
|
/**
|
|
1210
|
-
* Remove stale
|
|
1311
|
+
* Remove stale project entries by path
|
|
1312
|
+
* Unlinks and unregisters projects.
|
|
1211
1313
|
* Returns the number of entries removed.
|
|
1212
1314
|
*/
|
|
1213
1315
|
export async function cleanupStaleProjects(names: string[]): Promise<number> {
|
|
1214
1316
|
let removed = 0;
|
|
1317
|
+
|
|
1318
|
+
// Get all paths to find matching projects
|
|
1319
|
+
const allPaths = await getAllPaths();
|
|
1320
|
+
|
|
1215
1321
|
for (const name of names) {
|
|
1216
|
-
|
|
1217
|
-
|
|
1322
|
+
// Find project ID by checking each path
|
|
1323
|
+
for (const [projectId, paths] of Object.entries(allPaths)) {
|
|
1324
|
+
for (const projectPath of paths || []) {
|
|
1325
|
+
const pathName = projectPath.split("/").pop();
|
|
1326
|
+
if (pathName === name) {
|
|
1327
|
+
// Unlink and unregister
|
|
1328
|
+
try {
|
|
1329
|
+
await unlinkProject(projectPath);
|
|
1330
|
+
} catch {
|
|
1331
|
+
// Path may not exist
|
|
1332
|
+
}
|
|
1333
|
+
await unregisterPath(projectId, projectPath);
|
|
1334
|
+
removed += 1;
|
|
1335
|
+
}
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1218
1338
|
}
|
|
1339
|
+
|
|
1219
1340
|
return removed;
|
|
1220
1341
|
}
|