@getjack/jack 0.1.6 → 0.1.7
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 -1
- package/src/commands/publish.ts +50 -0
- package/src/commands/ship.ts +4 -3
- package/src/index.ts +7 -0
- package/src/lib/binding-validator.ts +9 -4
- package/src/lib/build-helper.ts +61 -39
- package/src/lib/config-generator.ts +107 -0
- package/src/lib/control-plane.ts +61 -0
- package/src/lib/managed-deploy.ts +10 -2
- package/src/lib/project-detection.ts +412 -0
- package/src/lib/project-link.test.ts +4 -5
- package/src/lib/project-link.ts +5 -3
- package/src/lib/project-operations.ts +318 -21
- package/src/lib/project-resolver.ts +5 -1
- package/src/lib/storage/file-filter.ts +5 -0
- package/src/lib/telemetry.ts +4 -0
- package/src/lib/zip-packager.ts +8 -0
- package/src/templates/index.ts +137 -7
- package/templates/nextjs/.jack.json +26 -26
- package/templates/nextjs/app/globals.css +4 -4
- package/templates/nextjs/app/layout.tsx +11 -11
- package/templates/nextjs/app/page.tsx +8 -6
- package/templates/nextjs/cloudflare-env.d.ts +1 -1
- package/templates/nextjs/next.config.ts +1 -1
- package/templates/nextjs/open-next.config.ts +1 -1
- package/templates/nextjs/package.json +22 -22
- package/templates/nextjs/tsconfig.json +26 -42
- package/templates/nextjs/wrangler.jsonc +15 -15
- package/src/lib/github.ts +0 -151
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@getjack/jack",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.7",
|
|
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",
|
|
@@ -44,6 +44,7 @@
|
|
|
44
44
|
"@inquirer/prompts": "^7.0.0",
|
|
45
45
|
"@modelcontextprotocol/sdk": "^1.25.1",
|
|
46
46
|
"archiver": "^7.0.1",
|
|
47
|
+
"fflate": "^0.8.2",
|
|
47
48
|
"human-id": "^4.1.3",
|
|
48
49
|
"meow": "^14.0.0",
|
|
49
50
|
"yocto-spinner": "^1.0.0",
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { getCurrentUserProfile, publishProject } from "../lib/control-plane.ts";
|
|
2
|
+
import { output, spinner } from "../lib/output.ts";
|
|
3
|
+
import { readProjectLink } from "../lib/project-link.ts";
|
|
4
|
+
|
|
5
|
+
export default async function publish(): Promise<void> {
|
|
6
|
+
// Check we're in a project directory
|
|
7
|
+
const link = await readProjectLink(process.cwd());
|
|
8
|
+
if (!link) {
|
|
9
|
+
output.error("Not in a jack project directory");
|
|
10
|
+
output.info("Run this command from a directory with a .jack folder");
|
|
11
|
+
process.exit(1);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
if (link.deploy_mode !== "managed") {
|
|
15
|
+
output.error("Only managed projects can be published");
|
|
16
|
+
output.info("Projects deployed with BYOC (bring your own cloud) cannot be published");
|
|
17
|
+
process.exit(1);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (!link.project_id) {
|
|
21
|
+
output.error("Project not linked to jack cloud");
|
|
22
|
+
output.info("Run: jack ship (to deploy and link the project)");
|
|
23
|
+
process.exit(1);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Check user has username
|
|
27
|
+
const profile = await getCurrentUserProfile();
|
|
28
|
+
if (!profile?.username) {
|
|
29
|
+
output.error("You need a username to publish projects");
|
|
30
|
+
output.info("Run: jack login (to set up your username)");
|
|
31
|
+
process.exit(1);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const spin = spinner("Publishing project...");
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
const result = await publishProject(link.project_id);
|
|
38
|
+
spin.stop();
|
|
39
|
+
output.success(`Published as ${result.published_as}`);
|
|
40
|
+
|
|
41
|
+
console.error("");
|
|
42
|
+
output.info("Others can now fork your project:");
|
|
43
|
+
output.info(` ${result.fork_command}`);
|
|
44
|
+
} catch (err) {
|
|
45
|
+
spin.stop();
|
|
46
|
+
const message = err instanceof Error ? err.message : "Unknown error";
|
|
47
|
+
output.error(`Publish failed: ${message}`);
|
|
48
|
+
process.exit(1);
|
|
49
|
+
}
|
|
50
|
+
}
|
package/src/commands/ship.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { output, spinner } from "../lib/output.ts";
|
|
|
3
3
|
import { deployProject } from "../lib/project-operations.ts";
|
|
4
4
|
|
|
5
5
|
export default async function ship(
|
|
6
|
-
options: { managed?: boolean; byo?: boolean } = {},
|
|
6
|
+
options: { managed?: boolean; byo?: boolean; dryRun?: boolean } = {},
|
|
7
7
|
): Promise<void> {
|
|
8
8
|
const isCi = process.env.CI === "true" || process.env.CI === "1";
|
|
9
9
|
try {
|
|
@@ -20,10 +20,11 @@ export default async function ship(
|
|
|
20
20
|
box: output.box,
|
|
21
21
|
},
|
|
22
22
|
interactive: !isCi,
|
|
23
|
-
includeSecrets:
|
|
24
|
-
includeSync:
|
|
23
|
+
includeSecrets: !options.dryRun,
|
|
24
|
+
includeSync: !options.dryRun,
|
|
25
25
|
managed: options.managed,
|
|
26
26
|
byo: options.byo,
|
|
27
|
+
dryRun: options.dryRun,
|
|
27
28
|
});
|
|
28
29
|
|
|
29
30
|
if (!result.workerUrl && result.deployOutput) {
|
package/src/index.ts
CHANGED
|
@@ -25,6 +25,7 @@ const cli = meow(
|
|
|
25
25
|
down [name] Undeploy from cloud
|
|
26
26
|
ls List all projects
|
|
27
27
|
info [name] Show project details
|
|
28
|
+
publish Make your project forkable by others
|
|
28
29
|
|
|
29
30
|
Cloud & Sync
|
|
30
31
|
clone <project> Pull from cloud backup
|
|
@@ -206,6 +207,7 @@ try {
|
|
|
206
207
|
)({
|
|
207
208
|
managed: cli.flags.managed,
|
|
208
209
|
byo: cli.flags.byo,
|
|
210
|
+
dryRun: cli.flags.dryRun,
|
|
209
211
|
});
|
|
210
212
|
break;
|
|
211
213
|
}
|
|
@@ -264,6 +266,11 @@ try {
|
|
|
264
266
|
await withTelemetry("down", down)(args[0], { force: cli.flags.force });
|
|
265
267
|
break;
|
|
266
268
|
}
|
|
269
|
+
case "publish": {
|
|
270
|
+
const { default: publish } = await import("./commands/publish.ts");
|
|
271
|
+
await withTelemetry("publish", publish)();
|
|
272
|
+
break;
|
|
273
|
+
}
|
|
267
274
|
case "open": {
|
|
268
275
|
const { default: open } = await import("./commands/open.ts");
|
|
269
276
|
await withTelemetry("open", open)(args[0], { dash: cli.flags.dash, logs: cli.flags.logs });
|
|
@@ -12,14 +12,20 @@ import type { WranglerConfig } from "./build-helper.ts";
|
|
|
12
12
|
/**
|
|
13
13
|
* Bindings supported by jack cloud managed deployments.
|
|
14
14
|
*/
|
|
15
|
-
export const SUPPORTED_BINDINGS = [
|
|
15
|
+
export const SUPPORTED_BINDINGS = [
|
|
16
|
+
"d1_databases",
|
|
17
|
+
"ai",
|
|
18
|
+
"assets",
|
|
19
|
+
"vars",
|
|
20
|
+
"r2_buckets",
|
|
21
|
+
"kv_namespaces",
|
|
22
|
+
] as const;
|
|
16
23
|
|
|
17
24
|
/**
|
|
18
25
|
* Bindings not yet supported by jack cloud.
|
|
19
26
|
* These will cause validation errors if present in wrangler config.
|
|
20
27
|
*/
|
|
21
28
|
export const UNSUPPORTED_BINDINGS = [
|
|
22
|
-
"kv_namespaces",
|
|
23
29
|
"durable_objects",
|
|
24
30
|
"queues",
|
|
25
31
|
"services",
|
|
@@ -33,7 +39,6 @@ export const UNSUPPORTED_BINDINGS = [
|
|
|
33
39
|
* Human-readable names for unsupported bindings.
|
|
34
40
|
*/
|
|
35
41
|
const BINDING_DISPLAY_NAMES: Record<string, string> = {
|
|
36
|
-
kv_namespaces: "KV Namespaces",
|
|
37
42
|
durable_objects: "Durable Objects",
|
|
38
43
|
queues: "Queues",
|
|
39
44
|
services: "Service Bindings",
|
|
@@ -67,7 +72,7 @@ export function validateBindings(
|
|
|
67
72
|
if (value !== undefined && value !== null) {
|
|
68
73
|
const displayName = BINDING_DISPLAY_NAMES[binding] || binding;
|
|
69
74
|
errors.push(
|
|
70
|
-
`✗ ${displayName} not supported in managed deploy.\n Managed deploy supports: D1, AI, Assets, R2, vars.\n Fix: Remove ${binding} from wrangler.jsonc, or use 'wrangler deploy' for full control.`,
|
|
75
|
+
`✗ ${displayName} not supported in managed deploy.\n Managed deploy supports: D1, AI, Assets, R2, KV, vars.\n Fix: Remove ${binding} from wrangler.jsonc, or use 'wrangler deploy' for full control.`,
|
|
71
76
|
);
|
|
72
77
|
}
|
|
73
78
|
}
|
package/src/lib/build-helper.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync } from "node:fs";
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
2
|
import { mkdir, readFile, readdir } from "node:fs/promises";
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import { basename, join } from "node:path";
|
|
@@ -40,8 +40,11 @@ export interface WranglerConfig {
|
|
|
40
40
|
binding: string;
|
|
41
41
|
bucket_name: string;
|
|
42
42
|
}>;
|
|
43
|
+
kv_namespaces?: Array<{
|
|
44
|
+
binding: string;
|
|
45
|
+
id?: string; // Optional - wrangler auto-provisions if missing
|
|
46
|
+
}>;
|
|
43
47
|
// Unsupported bindings (for validation)
|
|
44
|
-
kv_namespaces?: unknown;
|
|
45
48
|
durable_objects?: unknown;
|
|
46
49
|
queues?: unknown;
|
|
47
50
|
services?: unknown;
|
|
@@ -103,9 +106,20 @@ export async function needsOpenNextBuild(projectPath: string): Promise<boolean>
|
|
|
103
106
|
* @throws JackError if build fails
|
|
104
107
|
*/
|
|
105
108
|
export async function runViteBuild(projectPath: string): Promise<void> {
|
|
106
|
-
//
|
|
107
|
-
//
|
|
108
|
-
|
|
109
|
+
// Use local vite if installed to avoid module resolution issues
|
|
110
|
+
// bunx vite installs to temp dir, but vite.config.js may require('vite') from node_modules
|
|
111
|
+
// Don't use project's build script - it might do more than just vite build (e.g., Tauri)
|
|
112
|
+
let buildCommand: string[];
|
|
113
|
+
|
|
114
|
+
if (existsSync(join(projectPath, "node_modules", ".bin", "vite"))) {
|
|
115
|
+
// Local vite installed - use it directly
|
|
116
|
+
buildCommand = ["bun", "run", "vite", "build"];
|
|
117
|
+
} else {
|
|
118
|
+
// Fallback to bunx
|
|
119
|
+
buildCommand = ["bunx", "vite", "build"];
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const buildResult = await $`${buildCommand}`.cwd(projectPath).nothrow().quiet();
|
|
109
123
|
|
|
110
124
|
if (buildResult.exitCode !== 0) {
|
|
111
125
|
throw new JackError(
|
|
@@ -258,42 +272,50 @@ async function resolveEntrypoint(outDir: string, main?: string): Promise<string>
|
|
|
258
272
|
}
|
|
259
273
|
|
|
260
274
|
/**
|
|
261
|
-
*
|
|
262
|
-
*
|
|
263
|
-
* @param projectPath - Absolute path to project directory
|
|
264
|
-
* @returns Array of bucket names that were created or already existed
|
|
275
|
+
* Gets the installed wrangler version.
|
|
276
|
+
* @returns Version string (e.g., "4.55.0")
|
|
265
277
|
*/
|
|
266
|
-
export async function
|
|
267
|
-
const
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
278
|
+
export async function getWranglerVersion(): Promise<string> {
|
|
279
|
+
const result = await $`wrangler --version`.nothrow().quiet();
|
|
280
|
+
if (result.exitCode !== 0) {
|
|
281
|
+
throw new JackError(
|
|
282
|
+
JackErrorCode.VALIDATION_ERROR,
|
|
283
|
+
"wrangler not found",
|
|
284
|
+
"Install wrangler: npm install -g wrangler",
|
|
285
|
+
);
|
|
271
286
|
}
|
|
287
|
+
// Parse "wrangler 4.55.0" -> "4.55.0"
|
|
288
|
+
const match = result.stdout.toString().match(/(\d+\.\d+\.\d+)/);
|
|
289
|
+
return match?.[1] ?? "0.0.0";
|
|
290
|
+
}
|
|
272
291
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
for (const bucket of config.r2_buckets) {
|
|
276
|
-
const bucketName = bucket.bucket_name;
|
|
277
|
-
|
|
278
|
-
// Try to create the bucket (wrangler handles "already exists" gracefully)
|
|
279
|
-
const result = await $`wrangler r2 bucket create ${bucketName}`
|
|
280
|
-
.cwd(projectPath)
|
|
281
|
-
.nothrow()
|
|
282
|
-
.quiet();
|
|
283
|
-
|
|
284
|
-
// Exit code 0 = created, non-zero with "already exists" = fine
|
|
285
|
-
const stderr = result.stderr.toString();
|
|
286
|
-
if (result.exitCode === 0 || stderr.includes("already exists")) {
|
|
287
|
-
results.push(bucketName);
|
|
288
|
-
} else {
|
|
289
|
-
throw new JackError(
|
|
290
|
-
JackErrorCode.RESOURCE_ERROR,
|
|
291
|
-
`Failed to create R2 bucket: ${bucketName}`,
|
|
292
|
-
"Check your Cloudflare account has R2 enabled",
|
|
293
|
-
{ stderr },
|
|
294
|
-
);
|
|
295
|
-
}
|
|
296
|
-
}
|
|
292
|
+
const MIN_WRANGLER_VERSION = "4.45.0";
|
|
297
293
|
|
|
298
|
-
|
|
294
|
+
/**
|
|
295
|
+
* Checks if wrangler version meets minimum requirement for auto-provisioning.
|
|
296
|
+
* @throws JackError if version is too old
|
|
297
|
+
*/
|
|
298
|
+
export function checkWranglerVersion(version: string): void {
|
|
299
|
+
const parts = version.split(".").map(Number);
|
|
300
|
+
const minParts = MIN_WRANGLER_VERSION.split(".").map(Number);
|
|
301
|
+
|
|
302
|
+
const major = parts[0] ?? 0;
|
|
303
|
+
const minor = parts[1] ?? 0;
|
|
304
|
+
const patch = parts[2] ?? 0;
|
|
305
|
+
const minMajor = minParts[0] ?? 0;
|
|
306
|
+
const minMinor = minParts[1] ?? 0;
|
|
307
|
+
const minPatch = minParts[2] ?? 0;
|
|
308
|
+
|
|
309
|
+
const isValid =
|
|
310
|
+
major > minMajor ||
|
|
311
|
+
(major === minMajor && minor > minMinor) ||
|
|
312
|
+
(major === minMajor && minor === minMinor && patch >= minPatch);
|
|
313
|
+
|
|
314
|
+
if (!isValid) {
|
|
315
|
+
throw new JackError(
|
|
316
|
+
JackErrorCode.VALIDATION_ERROR,
|
|
317
|
+
`wrangler ${MIN_WRANGLER_VERSION}+ required (found ${version})`,
|
|
318
|
+
"Run: npm install -g wrangler@latest",
|
|
319
|
+
);
|
|
320
|
+
}
|
|
299
321
|
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
|
|
4
|
+
export type ProjectType = "nextjs" | "vite" | "hono" | "sveltekit" | "worker" | "unknown";
|
|
5
|
+
|
|
6
|
+
export interface WranglerConfig {
|
|
7
|
+
name: string;
|
|
8
|
+
main?: string;
|
|
9
|
+
compatibility_date: string;
|
|
10
|
+
assets?: {
|
|
11
|
+
directory: string;
|
|
12
|
+
binding?: string;
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const COMPATIBILITY_DATE = "2024-12-01";
|
|
17
|
+
|
|
18
|
+
export function generateWranglerConfig(
|
|
19
|
+
projectType: ProjectType,
|
|
20
|
+
projectName: string,
|
|
21
|
+
entryPoint?: string,
|
|
22
|
+
): WranglerConfig {
|
|
23
|
+
switch (projectType) {
|
|
24
|
+
case "nextjs":
|
|
25
|
+
return {
|
|
26
|
+
name: projectName,
|
|
27
|
+
main: ".open-next/worker.js",
|
|
28
|
+
compatibility_date: COMPATIBILITY_DATE,
|
|
29
|
+
assets: {
|
|
30
|
+
directory: ".open-next/assets",
|
|
31
|
+
binding: "ASSETS",
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
case "vite":
|
|
36
|
+
// Pure Vite SPAs use assets-only mode (no worker entry)
|
|
37
|
+
// Cloudflare auto-generates a worker that serves static files
|
|
38
|
+
return {
|
|
39
|
+
name: projectName,
|
|
40
|
+
compatibility_date: COMPATIBILITY_DATE,
|
|
41
|
+
assets: {
|
|
42
|
+
directory: "./dist",
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
case "hono":
|
|
47
|
+
return {
|
|
48
|
+
name: projectName,
|
|
49
|
+
main: entryPoint || "src/index.ts",
|
|
50
|
+
compatibility_date: COMPATIBILITY_DATE,
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
case "sveltekit":
|
|
54
|
+
return {
|
|
55
|
+
name: projectName,
|
|
56
|
+
compatibility_date: COMPATIBILITY_DATE,
|
|
57
|
+
assets: {
|
|
58
|
+
directory: "./.svelte-kit/cloudflare",
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
default:
|
|
63
|
+
return {
|
|
64
|
+
name: projectName,
|
|
65
|
+
main: entryPoint || "src/index.ts",
|
|
66
|
+
compatibility_date: COMPATIBILITY_DATE,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function writeWranglerConfig(projectPath: string, config: WranglerConfig): void {
|
|
72
|
+
const header = "// wrangler.jsonc (auto-generated by jack)\n";
|
|
73
|
+
const json = JSON.stringify(config, null, 2);
|
|
74
|
+
const content = `${header}${json}\n`;
|
|
75
|
+
const filePath = path.join(projectPath, "wrangler.jsonc");
|
|
76
|
+
fs.writeFileSync(filePath, content, "utf-8");
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function getDefaultProjectName(projectPath: string): string {
|
|
80
|
+
const packageJsonPath = path.join(projectPath, "package.json");
|
|
81
|
+
|
|
82
|
+
if (fs.existsSync(packageJsonPath)) {
|
|
83
|
+
try {
|
|
84
|
+
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"));
|
|
85
|
+
if (packageJson.name && typeof packageJson.name === "string") {
|
|
86
|
+
const slugified = slugify(packageJson.name);
|
|
87
|
+
if (slugified) {
|
|
88
|
+
return slugified;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
} catch {
|
|
92
|
+
// Fall through to folder name
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const folderName = path.basename(path.resolve(projectPath));
|
|
97
|
+
return slugify(folderName) || "my-project";
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function slugify(name: string): string {
|
|
101
|
+
return name
|
|
102
|
+
.toLowerCase()
|
|
103
|
+
.replace(/[\s_]+/g, "-")
|
|
104
|
+
.replace(/[^a-z0-9-]/g, "")
|
|
105
|
+
.replace(/-+/g, "-")
|
|
106
|
+
.replace(/^-+|-+$/g, "");
|
|
107
|
+
}
|
package/src/lib/control-plane.ts
CHANGED
|
@@ -70,6 +70,12 @@ export interface UserProfile {
|
|
|
70
70
|
updated_at: string;
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
+
export interface PublishProjectResponse {
|
|
74
|
+
success: boolean;
|
|
75
|
+
published_as: string;
|
|
76
|
+
fork_command: string;
|
|
77
|
+
}
|
|
78
|
+
|
|
73
79
|
export interface CreateDeploymentRequest {
|
|
74
80
|
source: string;
|
|
75
81
|
}
|
|
@@ -187,6 +193,7 @@ export interface ManagedProject {
|
|
|
187
193
|
created_at: string;
|
|
188
194
|
updated_at: string;
|
|
189
195
|
tags?: string; // JSON string array from DB, e.g., '["backend", "api"]'
|
|
196
|
+
owner_username?: string | null;
|
|
190
197
|
}
|
|
191
198
|
|
|
192
199
|
/**
|
|
@@ -430,3 +437,57 @@ export async function getCurrentUserProfile(): Promise<UserProfile | null> {
|
|
|
430
437
|
return null;
|
|
431
438
|
}
|
|
432
439
|
}
|
|
440
|
+
|
|
441
|
+
export interface SourceSnapshotResponse {
|
|
442
|
+
success: boolean;
|
|
443
|
+
source_key: string;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* Upload a source snapshot for a project.
|
|
448
|
+
* Used to enable project forking.
|
|
449
|
+
*/
|
|
450
|
+
export async function uploadSourceSnapshot(
|
|
451
|
+
projectId: string,
|
|
452
|
+
sourceZipPath: string,
|
|
453
|
+
): Promise<SourceSnapshotResponse> {
|
|
454
|
+
const { authFetch } = await import("./auth/index.ts");
|
|
455
|
+
|
|
456
|
+
const formData = new FormData();
|
|
457
|
+
const sourceFile = Bun.file(sourceZipPath);
|
|
458
|
+
formData.append("source", sourceFile);
|
|
459
|
+
|
|
460
|
+
const response = await authFetch(`${getControlApiUrl()}/v1/projects/${projectId}/source`, {
|
|
461
|
+
method: "POST",
|
|
462
|
+
body: formData,
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
if (!response.ok) {
|
|
466
|
+
const error = (await response.json().catch(() => ({ message: "Upload failed" }))) as {
|
|
467
|
+
message?: string;
|
|
468
|
+
};
|
|
469
|
+
throw new Error(error.message || `Source upload failed: ${response.status}`);
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
return response.json() as Promise<SourceSnapshotResponse>;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/**
|
|
476
|
+
* Publish a project to make it forkable by others.
|
|
477
|
+
*/
|
|
478
|
+
export async function publishProject(projectId: string): Promise<PublishProjectResponse> {
|
|
479
|
+
const { authFetch } = await import("./auth/index.ts");
|
|
480
|
+
|
|
481
|
+
const response = await authFetch(`${getControlApiUrl()}/v1/projects/${projectId}/publish`, {
|
|
482
|
+
method: "POST",
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
if (!response.ok) {
|
|
486
|
+
const err = (await response.json().catch(() => ({ message: "Unknown error" }))) as {
|
|
487
|
+
message?: string;
|
|
488
|
+
};
|
|
489
|
+
throw new Error(err.message || `Failed to publish project: ${response.status}`);
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
return response.json() as Promise<PublishProjectResponse>;
|
|
493
|
+
}
|
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
|
|
7
7
|
import { validateBindings } from "./binding-validator.ts";
|
|
8
8
|
import { buildProject, parseWranglerConfig } from "./build-helper.ts";
|
|
9
|
-
import { createManagedProject, syncProjectTags } from "./control-plane.ts";
|
|
9
|
+
import { createManagedProject, syncProjectTags, uploadSourceSnapshot } from "./control-plane.ts";
|
|
10
|
+
import { debug } from "./debug.ts";
|
|
10
11
|
import { uploadDeployment } from "./deploy-upload.ts";
|
|
11
12
|
import { JackError, JackErrorCode } from "./errors.ts";
|
|
12
13
|
import type { OperationReporter } from "./project-operations.ts";
|
|
@@ -48,7 +49,7 @@ export async function createManagedProjectRemote(
|
|
|
48
49
|
usePrebuilt: options?.usePrebuilt ?? true,
|
|
49
50
|
});
|
|
50
51
|
|
|
51
|
-
const runjackUrl = `https://${result.project.slug}.runjack.xyz`;
|
|
52
|
+
const runjackUrl = result.url || `https://${result.project.slug}.runjack.xyz`;
|
|
52
53
|
|
|
53
54
|
reporter?.stop();
|
|
54
55
|
reporter?.success("Created managed project");
|
|
@@ -148,6 +149,13 @@ export async function deployCodeToManagedProject(
|
|
|
148
149
|
})
|
|
149
150
|
.catch(() => {});
|
|
150
151
|
|
|
152
|
+
// Upload source snapshot for forking (non-fatal, but must await before cleanup)
|
|
153
|
+
try {
|
|
154
|
+
await uploadSourceSnapshot(projectId, pkg.sourceZipPath);
|
|
155
|
+
} catch (err) {
|
|
156
|
+
debug("Source snapshot upload failed:", err instanceof Error ? err.message : String(err));
|
|
157
|
+
}
|
|
158
|
+
|
|
151
159
|
return {
|
|
152
160
|
deploymentId: result.id,
|
|
153
161
|
status: result.status,
|