@solcreek/cli 0.3.0

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.
Files changed (47) hide show
  1. package/LICENSE +190 -0
  2. package/README.md +184 -0
  3. package/dist/commands/claim.d.ts +18 -0
  4. package/dist/commands/claim.js +93 -0
  5. package/dist/commands/deploy.d.ts +38 -0
  6. package/dist/commands/deploy.js +820 -0
  7. package/dist/commands/deployments.d.ts +18 -0
  8. package/dist/commands/deployments.js +84 -0
  9. package/dist/commands/domains.d.ts +2 -0
  10. package/dist/commands/domains.js +159 -0
  11. package/dist/commands/env.d.ts +2 -0
  12. package/dist/commands/env.js +104 -0
  13. package/dist/commands/init.d.ts +18 -0
  14. package/dist/commands/init.js +69 -0
  15. package/dist/commands/login.d.ts +23 -0
  16. package/dist/commands/login.js +120 -0
  17. package/dist/commands/projects.d.ts +13 -0
  18. package/dist/commands/projects.js +38 -0
  19. package/dist/commands/status.d.ts +18 -0
  20. package/dist/commands/status.js +115 -0
  21. package/dist/commands/whoami.d.ts +13 -0
  22. package/dist/commands/whoami.js +43 -0
  23. package/dist/index.d.ts +3 -0
  24. package/dist/index.js +38 -0
  25. package/dist/utils/auth-server.d.ts +22 -0
  26. package/dist/utils/auth-server.js +91 -0
  27. package/dist/utils/bundle.d.ts +6 -0
  28. package/dist/utils/bundle.js +39 -0
  29. package/dist/utils/config.d.ts +12 -0
  30. package/dist/utils/config.js +37 -0
  31. package/dist/utils/git-clone.d.ts +44 -0
  32. package/dist/utils/git-clone.js +193 -0
  33. package/dist/utils/nextjs.d.ts +48 -0
  34. package/dist/utils/nextjs.js +368 -0
  35. package/dist/utils/output.d.ts +38 -0
  36. package/dist/utils/output.js +45 -0
  37. package/dist/utils/repo-url.d.ts +48 -0
  38. package/dist/utils/repo-url.js +201 -0
  39. package/dist/utils/sandbox.d.ts +50 -0
  40. package/dist/utils/sandbox.js +69 -0
  41. package/dist/utils/ssr-bundle.d.ts +6 -0
  42. package/dist/utils/ssr-bundle.js +48 -0
  43. package/dist/utils/tos.d.ts +28 -0
  44. package/dist/utils/tos.js +95 -0
  45. package/dist/utils/worker-bundle.d.ts +24 -0
  46. package/dist/utils/worker-bundle.js +136 -0
  47. package/package.json +55 -0
@@ -0,0 +1,201 @@
1
+ /**
2
+ * Git repository URL parsing and security validation.
3
+ *
4
+ * Security model:
5
+ * - HTTPS only (no git://, ssh://, file://, ext::)
6
+ * - Hostname allowlist (github.com, gitlab.com, bitbucket.org only)
7
+ * - Owner/repo validated against strict character set
8
+ * - No embedded credentials
9
+ * - Subpath validated against traversal attacks
10
+ */
11
+ // --- Allowed hostnames (SSRF prevention) ---
12
+ const PROVIDER_HOSTS = {
13
+ "github.com": "github",
14
+ "www.github.com": "github",
15
+ "gitlab.com": "gitlab",
16
+ "www.gitlab.com": "gitlab",
17
+ "bitbucket.org": "bitbucket",
18
+ "www.bitbucket.org": "bitbucket",
19
+ };
20
+ const SHORTHAND_PREFIXES = {
21
+ "github:": "github",
22
+ "gitlab:": "gitlab",
23
+ "bitbucket:": "bitbucket",
24
+ };
25
+ // Strict character set for owner and repo names
26
+ const SAFE_NAME = /^[a-zA-Z0-9._-]+$/;
27
+ // --- Public API ---
28
+ /**
29
+ * Quick check: does this string look like a repo URL or shorthand?
30
+ * Used to route between directory deploy and repo deploy.
31
+ */
32
+ export function isRepoUrl(input) {
33
+ if (!input)
34
+ return false;
35
+ // Shorthand: github:user/repo
36
+ for (const prefix of Object.keys(SHORTHAND_PREFIXES)) {
37
+ if (input.startsWith(prefix))
38
+ return true;
39
+ }
40
+ // HTTPS URL to known host
41
+ try {
42
+ const url = new URL(input.split("#")[0]);
43
+ return url.protocol === "https:" && url.hostname in PROVIDER_HOSTS;
44
+ }
45
+ catch {
46
+ return false;
47
+ }
48
+ }
49
+ /**
50
+ * Parse a repo URL or shorthand into structured components.
51
+ * Supports:
52
+ * https://github.com/owner/repo
53
+ * https://github.com/owner/repo.git
54
+ * https://github.com/owner/repo#branch
55
+ * https://github.com/owner/repo/tree/branch
56
+ * github:owner/repo
57
+ * github:owner/repo#branch
58
+ */
59
+ export function parseRepoUrl(input) {
60
+ if (!input || typeof input !== "string") {
61
+ throw new RepoUrlError("Empty or invalid input");
62
+ }
63
+ // Shorthand: github:owner/repo#branch
64
+ for (const [prefix, provider] of Object.entries(SHORTHAND_PREFIXES)) {
65
+ if (input.startsWith(prefix)) {
66
+ return parseShorthand(input.slice(prefix.length), provider);
67
+ }
68
+ }
69
+ // Full URL
70
+ return parseFullUrl(input);
71
+ }
72
+ /**
73
+ * Validate a parsed repo URL for security.
74
+ * Throws RepoUrlError on any violation.
75
+ */
76
+ export function validateRepoUrl(parsed) {
77
+ // Protocol: only HTTPS (enforced during parsing, but belt-and-suspenders)
78
+ if (!parsed.cloneUrl.startsWith("https://")) {
79
+ throw new RepoUrlError("Only HTTPS URLs are allowed");
80
+ }
81
+ // Owner and repo name: strict character set
82
+ if (!SAFE_NAME.test(parsed.owner)) {
83
+ throw new RepoUrlError(`Invalid owner name: ${JSON.stringify(parsed.owner)}`);
84
+ }
85
+ if (!SAFE_NAME.test(parsed.repo)) {
86
+ throw new RepoUrlError(`Invalid repo name: ${JSON.stringify(parsed.repo)}`);
87
+ }
88
+ // Branch: strict character set (if present)
89
+ if (parsed.branch !== null && !/^[a-zA-Z0-9._\/-]+$/.test(parsed.branch)) {
90
+ throw new RepoUrlError(`Invalid branch name: ${JSON.stringify(parsed.branch)}`);
91
+ }
92
+ // No null bytes anywhere
93
+ const allParts = [parsed.owner, parsed.repo, parsed.branch, parsed.cloneUrl].filter(Boolean);
94
+ for (const part of allParts) {
95
+ if (part.includes("\0")) {
96
+ throw new RepoUrlError("Null bytes are not allowed");
97
+ }
98
+ }
99
+ }
100
+ /**
101
+ * Validate a --path subdirectory argument.
102
+ * Prevents path traversal and other filesystem attacks.
103
+ */
104
+ export function validateSubpath(path) {
105
+ if (!path || !path.trim()) {
106
+ throw new RepoUrlError("Subpath cannot be empty");
107
+ }
108
+ // No absolute paths
109
+ if (path.startsWith("/") || path.startsWith("\\")) {
110
+ throw new RepoUrlError("Subpath must be relative, not absolute");
111
+ }
112
+ // No path traversal
113
+ const segments = path.split(/[/\\]/);
114
+ if (segments.some((s) => s === "..")) {
115
+ throw new RepoUrlError("Path traversal (..) is not allowed in subpath");
116
+ }
117
+ // No null bytes
118
+ if (path.includes("\0")) {
119
+ throw new RepoUrlError("Null bytes are not allowed in subpath");
120
+ }
121
+ // Only safe characters
122
+ if (!/^[a-zA-Z0-9._\/-]+$/.test(path)) {
123
+ throw new RepoUrlError("Subpath contains invalid characters");
124
+ }
125
+ }
126
+ // --- Internal parsers ---
127
+ function parseShorthand(rest, provider) {
128
+ // Split on # for branch
129
+ const [pathPart, ...branchParts] = rest.split("#");
130
+ const branch = branchParts.length > 0 ? branchParts.join("#") : null;
131
+ const segments = pathPart.replace(/\.git$/, "").split("/").filter(Boolean);
132
+ if (segments.length !== 2) {
133
+ throw new RepoUrlError(`Expected owner/repo format, got: ${pathPart}`);
134
+ }
135
+ const [owner, repo] = segments;
136
+ const host = provider === "github" ? "github.com" : provider === "gitlab" ? "gitlab.com" : "bitbucket.org";
137
+ return {
138
+ provider,
139
+ owner,
140
+ repo,
141
+ branch: branch || null,
142
+ cloneUrl: `https://${host}/${owner}/${repo}.git`,
143
+ displayUrl: `${owner}/${repo}${branch ? `#${branch}` : ""}`,
144
+ };
145
+ }
146
+ function parseFullUrl(input) {
147
+ // Extract branch from fragment before parsing URL
148
+ const [urlPart, ...fragmentParts] = input.split("#");
149
+ const fragment = fragmentParts.length > 0 ? fragmentParts.join("#") : null;
150
+ let url;
151
+ try {
152
+ // Strip query params and trailing slash
153
+ const cleanUrl = urlPart.split("?")[0].replace(/\/$/, "");
154
+ url = new URL(cleanUrl);
155
+ }
156
+ catch {
157
+ throw new RepoUrlError(`Invalid URL: ${input}`);
158
+ }
159
+ // Protocol check
160
+ if (url.protocol !== "https:") {
161
+ throw new RepoUrlError(`Only HTTPS URLs are allowed. Got: ${url.protocol}`);
162
+ }
163
+ // Hostname allowlist (SSRF prevention)
164
+ const hostname = url.hostname.toLowerCase();
165
+ const provider = PROVIDER_HOSTS[hostname];
166
+ if (!provider) {
167
+ throw new RepoUrlError(`Unsupported host: ${hostname}. Supported: github.com, gitlab.com, bitbucket.org`);
168
+ }
169
+ // No embedded credentials
170
+ if (url.username || url.password) {
171
+ throw new RepoUrlError("URLs with embedded credentials are not allowed");
172
+ }
173
+ // Parse path segments: /owner/repo[/tree/branch][.git]
174
+ const pathSegments = url.pathname.split("/").filter(Boolean);
175
+ if (pathSegments.length < 2) {
176
+ throw new RepoUrlError(`Expected /{owner}/{repo} in URL path, got: ${url.pathname}`);
177
+ }
178
+ const owner = pathSegments[0];
179
+ const repo = pathSegments[1].replace(/\.git$/, "");
180
+ // Extract branch from /tree/branch path (GitHub convention)
181
+ let branch = fragment;
182
+ if (pathSegments.length >= 4 && pathSegments[2] === "tree") {
183
+ branch = pathSegments.slice(3).join("/");
184
+ }
185
+ return {
186
+ provider,
187
+ owner,
188
+ repo,
189
+ branch: branch || null,
190
+ cloneUrl: `https://${hostname}/${owner}/${repo}.git`,
191
+ displayUrl: `${owner}/${repo}${branch ? `#${branch}` : ""}`,
192
+ };
193
+ }
194
+ // --- Error ---
195
+ export class RepoUrlError extends Error {
196
+ constructor(message) {
197
+ super(message);
198
+ this.name = "RepoUrlError";
199
+ }
200
+ }
201
+ //# sourceMappingURL=repo-url.js.map
@@ -0,0 +1,50 @@
1
+ import type { TosAcceptance } from "./tos.js";
2
+ interface SandboxDeployResponse {
3
+ sandboxId: string;
4
+ status: string;
5
+ statusUrl: string;
6
+ previewUrl: string;
7
+ expiresAt: string;
8
+ tier?: string;
9
+ }
10
+ interface SandboxStatusResponse {
11
+ sandboxId: string;
12
+ status: string;
13
+ previewUrl: string;
14
+ deployDurationMs?: number;
15
+ expiresAt: string;
16
+ expiresInSeconds: number;
17
+ claimable: boolean;
18
+ failedStep?: string;
19
+ errorMessage?: string;
20
+ }
21
+ /**
22
+ * Deploy a bundle to the sandbox API (no auth required).
23
+ * Manifest is optional — the API derives asset list from the assets keys.
24
+ */
25
+ export declare function sandboxDeploy(bundle: {
26
+ manifest?: {
27
+ assets: string[];
28
+ hasWorker: boolean;
29
+ entrypoint: string | null;
30
+ renderMode: string;
31
+ };
32
+ assets: Record<string, string>;
33
+ serverFiles?: Record<string, string>;
34
+ framework?: string;
35
+ templateId?: string;
36
+ source: string;
37
+ }, opts?: {
38
+ tos?: TosAcceptance;
39
+ agentToken?: string;
40
+ }): Promise<SandboxDeployResponse>;
41
+ /**
42
+ * Poll sandbox status until terminal state.
43
+ */
44
+ export declare function pollSandboxStatus(statusUrl: string): Promise<SandboxStatusResponse>;
45
+ /**
46
+ * Print sandbox success message with claim instructions.
47
+ */
48
+ export declare function printSandboxSuccess(previewUrl: string, expiresAt: string, sandboxId: string): void;
49
+ export {};
50
+ //# sourceMappingURL=sandbox.d.ts.map
@@ -0,0 +1,69 @@
1
+ import consola from "consola";
2
+ import { getSandboxApiUrl } from "./config.js";
3
+ import { isTTY } from "./output.js";
4
+ /**
5
+ * Deploy a bundle to the sandbox API (no auth required).
6
+ * Manifest is optional — the API derives asset list from the assets keys.
7
+ */
8
+ export async function sandboxDeploy(bundle, opts) {
9
+ const apiUrl = getSandboxApiUrl();
10
+ const headers = {
11
+ "Content-Type": "application/json",
12
+ };
13
+ // Signal TTY status for tiered rate limiting
14
+ if (isTTY)
15
+ headers["X-Creek-TTY"] = "1";
16
+ // Agent token for elevated rate limit
17
+ if (opts?.agentToken)
18
+ headers["Authorization"] = `Bearer ${opts.agentToken}`;
19
+ // ToS acceptance metadata
20
+ if (opts?.tos) {
21
+ headers["X-Creek-ToS-Version"] = opts.tos.version;
22
+ headers["X-Creek-ToS-Accepted-At"] = opts.tos.acceptedAt;
23
+ }
24
+ const res = await fetch(`${apiUrl}/api/sandbox/deploy`, {
25
+ method: "POST",
26
+ headers,
27
+ body: JSON.stringify(bundle),
28
+ });
29
+ if (!res.ok) {
30
+ const err = await res.json().catch(() => ({ message: res.statusText }));
31
+ throw new Error(err.message ?? `Sandbox deploy failed (${res.status})`);
32
+ }
33
+ return res.json();
34
+ }
35
+ /**
36
+ * Poll sandbox status until terminal state.
37
+ */
38
+ export async function pollSandboxStatus(statusUrl) {
39
+ const POLL_INTERVAL = 1000;
40
+ const POLL_TIMEOUT = 60_000;
41
+ const start = Date.now();
42
+ while (Date.now() - start < POLL_TIMEOUT) {
43
+ const res = await fetch(statusUrl);
44
+ if (!res.ok)
45
+ throw new Error(`Status check failed (${res.status})`);
46
+ const status = (await res.json());
47
+ if (status.status === "active")
48
+ return status;
49
+ if (status.status === "failed") {
50
+ const step = status.failedStep ? ` at ${status.failedStep}` : "";
51
+ throw new Error(`Sandbox deploy failed${step}: ${status.errorMessage ?? "Unknown error"}`);
52
+ }
53
+ if (status.status === "expired") {
54
+ throw new Error("Sandbox expired before activation");
55
+ }
56
+ await new Promise((r) => setTimeout(r, POLL_INTERVAL));
57
+ }
58
+ throw new Error("Sandbox deploy timed out");
59
+ }
60
+ /**
61
+ * Print sandbox success message with claim instructions.
62
+ */
63
+ export function printSandboxSuccess(previewUrl, expiresAt, sandboxId) {
64
+ consola.success(` Live → ${previewUrl}`);
65
+ consola.info("");
66
+ consola.info(" Free preview — available for 60 minutes.");
67
+ consola.info(" Make it permanent: creek login && creek claim " + sandboxId);
68
+ }
69
+ //# sourceMappingURL=sandbox.js.map
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Bundle an SSR server entry point into a single standalone worker script.
3
+ * Uses esbuild (same bundler as wrangler) with node: builtins as externals.
4
+ */
5
+ export declare function bundleSSRServer(entryPoint: string): Promise<string>;
6
+ //# sourceMappingURL=ssr-bundle.d.ts.map
@@ -0,0 +1,48 @@
1
+ import { build } from "esbuild";
2
+ /**
3
+ * Bundle an SSR server entry point into a single standalone worker script.
4
+ * Uses esbuild (same bundler as wrangler) with node: builtins as externals.
5
+ */
6
+ export async function bundleSSRServer(entryPoint) {
7
+ const result = await build({
8
+ entryPoints: [entryPoint],
9
+ bundle: true,
10
+ format: "esm",
11
+ platform: "neutral",
12
+ target: "es2022",
13
+ write: false,
14
+ minify: false,
15
+ external: [
16
+ "node:async_hooks",
17
+ "node:stream",
18
+ "node:stream/web",
19
+ "node:buffer",
20
+ "node:util",
21
+ "node:events",
22
+ "node:crypto",
23
+ "node:path",
24
+ "node:url",
25
+ "node:string_decoder",
26
+ "node:diagnostics_channel",
27
+ "node:process",
28
+ "node:fs",
29
+ "node:os",
30
+ "node:child_process",
31
+ "node:http",
32
+ "node:https",
33
+ "node:net",
34
+ "node:tls",
35
+ "node:zlib",
36
+ "node:perf_hooks",
37
+ "node:worker_threads",
38
+ ],
39
+ conditions: ["workerd", "worker", "import"],
40
+ mainFields: ["module", "main"],
41
+ logLevel: "warning",
42
+ });
43
+ if (result.errors.length > 0) {
44
+ throw new Error(`esbuild: ${result.errors.map((e) => e.text).join(", ")}`);
45
+ }
46
+ return result.outputFiles[0].text;
47
+ }
48
+ //# sourceMappingURL=ssr-bundle.js.map
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Terms of Service acceptance for CLI.
3
+ *
4
+ * - First deploy: shows ToS notice + URL + interactive y/N prompt
5
+ * - Stored locally in ~/.creek/tos-accepted (version + timestamp)
6
+ * - --yes / non-TTY: implicit accept with notice printed
7
+ * - Sends tosVersion + tosAcceptedAt in deploy requests
8
+ */
9
+ export declare const CURRENT_TOS_VERSION = "2026-03-28";
10
+ export interface TosAcceptance {
11
+ version: string;
12
+ acceptedAt: string;
13
+ }
14
+ /** Read stored ToS acceptance, if any. */
15
+ export declare function readTosAcceptance(): TosAcceptance | null;
16
+ /** Store ToS acceptance locally. */
17
+ export declare function writeTosAcceptance(acceptance: TosAcceptance): void;
18
+ /** Check if current ToS version has been accepted. */
19
+ export declare function isTosAccepted(): boolean;
20
+ /**
21
+ * Ensure ToS is accepted before proceeding.
22
+ *
23
+ * @param autoConfirm - true if --yes flag is set
24
+ * @returns TosAcceptance record to send with deploy request
25
+ * @throws if user rejects ToS
26
+ */
27
+ export declare function ensureTosAccepted(autoConfirm: boolean): Promise<TosAcceptance>;
28
+ //# sourceMappingURL=tos.d.ts.map
@@ -0,0 +1,95 @@
1
+ /**
2
+ * Terms of Service acceptance for CLI.
3
+ *
4
+ * - First deploy: shows ToS notice + URL + interactive y/N prompt
5
+ * - Stored locally in ~/.creek/tos-accepted (version + timestamp)
6
+ * - --yes / non-TTY: implicit accept with notice printed
7
+ * - Sends tosVersion + tosAcceptedAt in deploy requests
8
+ */
9
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs";
10
+ import { join } from "node:path";
11
+ import consola from "consola";
12
+ import { getConfigDir } from "./config.js";
13
+ import { isTTY } from "./output.js";
14
+ const TOS_FILE = join(getConfigDir(), "tos-accepted");
15
+ // Bump this when ToS content changes — forces re-acceptance
16
+ export const CURRENT_TOS_VERSION = "2026-03-28";
17
+ const TOS_URL = "https://creek.dev/legal/terms";
18
+ const AUP_URL = "https://creek.dev/legal/acceptable-use";
19
+ /** Read stored ToS acceptance, if any. */
20
+ export function readTosAcceptance() {
21
+ if (!existsSync(TOS_FILE))
22
+ return null;
23
+ try {
24
+ const data = JSON.parse(readFileSync(TOS_FILE, "utf-8"));
25
+ if (data.version && data.acceptedAt)
26
+ return data;
27
+ return null;
28
+ }
29
+ catch {
30
+ return null;
31
+ }
32
+ }
33
+ /** Store ToS acceptance locally. */
34
+ export function writeTosAcceptance(acceptance) {
35
+ const dir = getConfigDir();
36
+ if (!existsSync(dir)) {
37
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
38
+ }
39
+ writeFileSync(TOS_FILE, JSON.stringify(acceptance, null, 2), { mode: 0o600 });
40
+ }
41
+ /** Check if current ToS version has been accepted. */
42
+ export function isTosAccepted() {
43
+ const acceptance = readTosAcceptance();
44
+ return acceptance?.version === CURRENT_TOS_VERSION;
45
+ }
46
+ /**
47
+ * Ensure ToS is accepted before proceeding.
48
+ *
49
+ * @param autoConfirm - true if --yes flag is set
50
+ * @returns TosAcceptance record to send with deploy request
51
+ * @throws if user rejects ToS
52
+ */
53
+ export async function ensureTosAccepted(autoConfirm) {
54
+ // Already accepted current version?
55
+ const existing = readTosAcceptance();
56
+ if (existing?.version === CURRENT_TOS_VERSION)
57
+ return existing;
58
+ // Show ToS notice
59
+ consola.log("");
60
+ consola.log(" By deploying, you agree to Creek's Terms of Service");
61
+ consola.log(" and Acceptable Use Policy:");
62
+ consola.log(` ${TOS_URL}`);
63
+ consola.log(` ${AUP_URL}`);
64
+ consola.log("");
65
+ if (autoConfirm || !isTTY) {
66
+ // Non-interactive: implicit acceptance
67
+ consola.log(" Continuing implies acceptance of the above terms.");
68
+ consola.log("");
69
+ }
70
+ else {
71
+ // Interactive: ask for explicit acceptance
72
+ const readline = await import("node:readline");
73
+ const rl = readline.createInterface({
74
+ input: process.stdin,
75
+ output: process.stdout,
76
+ });
77
+ const answer = await new Promise((resolve) => {
78
+ rl.question(" Accept? [y/N] ", (ans) => {
79
+ rl.close();
80
+ resolve(ans.trim().toLowerCase());
81
+ });
82
+ });
83
+ if (answer !== "y" && answer !== "yes") {
84
+ consola.error("You must accept the Terms of Service to use Creek.");
85
+ process.exit(1);
86
+ }
87
+ }
88
+ const acceptance = {
89
+ version: CURRENT_TOS_VERSION,
90
+ acceptedAt: new Date().toISOString(),
91
+ };
92
+ writeTosAcceptance(acceptance);
93
+ return acceptance;
94
+ }
95
+ //# sourceMappingURL=tos.js.map
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Generate the wrapper entry source that auto-injects _setEnv(env).
3
+ *
4
+ * The wrapper:
5
+ * 1. Imports _setEnv from creek runtime
6
+ * 2. Imports the user's app (Hono, custom Worker, etc.)
7
+ * 3. Calls _setEnv(env) before each request
8
+ * 4. Delegates to the user's fetch handler
9
+ * 5. If the handler returns 404, falls back to static assets (WfP Static Assets API)
10
+ */
11
+ export declare function generateWorkerWrapper(entryPoint: string, wrapperDir: string, options?: {
12
+ hasClientAssets?: boolean;
13
+ }): string;
14
+ /**
15
+ * Bundle a Worker entry point with Creek runtime auto-injection.
16
+ *
17
+ * 1. Generates a wrapper entry that calls _setEnv(env)
18
+ * 2. Bundles wrapper + user code + creek runtime with esbuild
19
+ * 3. Returns the bundled JavaScript string
20
+ */
21
+ export declare function bundleWorker(entryPoint: string, cwd: string, options?: {
22
+ hasClientAssets?: boolean;
23
+ }): Promise<string>;
24
+ //# sourceMappingURL=worker-bundle.d.ts.map
@@ -0,0 +1,136 @@
1
+ import { writeFileSync, mkdirSync } from "fs";
2
+ import { join, relative } from "path";
3
+ import { bundleSSRServer } from "./ssr-bundle.js";
4
+ /**
5
+ * Generate the wrapper entry source that auto-injects _setEnv(env).
6
+ *
7
+ * The wrapper:
8
+ * 1. Imports _setEnv from creek runtime
9
+ * 2. Imports the user's app (Hono, custom Worker, etc.)
10
+ * 3. Calls _setEnv(env) before each request
11
+ * 4. Delegates to the user's fetch handler
12
+ * 5. If the handler returns 404, falls back to static assets (WfP Static Assets API)
13
+ */
14
+ export function generateWorkerWrapper(entryPoint, wrapperDir, options) {
15
+ let importPath = relative(wrapperDir, entryPoint).replace(/\\/g, "/");
16
+ // Remove .ts/.tsx extension — esbuild resolves it
17
+ importPath = importPath.replace(/\.tsx?$/, "");
18
+ if (!importPath.startsWith("."))
19
+ importPath = "./" + importPath;
20
+ const hasAssets = options?.hasClientAssets ?? false;
21
+ // When client assets exist (Worker + SPA hybrid):
22
+ // - Try static assets first for non-API paths
23
+ // - Fall back to the worker handler for API routes
24
+ // - SPA fallback: serve index.html for extensionless paths
25
+ if (hasAssets) {
26
+ return `import { _setEnv, _setCtx, generateWsToken } from "creek";
27
+ import userModule from "${importPath}";
28
+
29
+ const handler = userModule.default ?? userModule;
30
+
31
+ function isApiPath(pathname) {
32
+ return pathname.startsWith("/api/") || pathname === "/__creek/config";
33
+ }
34
+
35
+ function hasExtension(pathname) {
36
+ const last = pathname.split("/").pop() || "";
37
+ return last.includes(".");
38
+ }
39
+
40
+ export default {
41
+ async fetch(request, env, ctx) {
42
+ _setEnv(env);
43
+ _setCtx(ctx);
44
+ const url = new URL(request.url);
45
+
46
+ // /__creek/config — auto-discovery for CreekRoom WebSocket
47
+ if (url.pathname === "/__creek/config" && request.method === "GET") {
48
+ const wsToken = await generateWsToken();
49
+ return new Response(JSON.stringify({
50
+ realtimeUrl: env.CREEK_REALTIME_URL || null,
51
+ projectSlug: env.CREEK_PROJECT_SLUG || null,
52
+ wsToken,
53
+ }), { headers: { "Content-Type": "application/json" } });
54
+ }
55
+
56
+ // API routes → always go to the worker handler
57
+ if (isApiPath(url.pathname)) {
58
+ if (typeof handler.fetch === "function") return handler.fetch(request, env, ctx);
59
+ if (typeof handler === "function") return handler(request, env, ctx);
60
+ }
61
+
62
+ // Static assets → try WfP Static Assets API
63
+ if (env.ASSETS) {
64
+ try {
65
+ const assetResponse = await env.ASSETS.fetch(request);
66
+ if (assetResponse.status !== 404) return assetResponse;
67
+ } catch {}
68
+
69
+ // SPA fallback: extensionless paths → index.html
70
+ if (!hasExtension(url.pathname)) {
71
+ try {
72
+ const indexReq = new Request(new URL("/index.html", request.url), request);
73
+ const indexResponse = await env.ASSETS.fetch(indexReq);
74
+ if (indexResponse.status !== 404) {
75
+ return new Response(indexResponse.body, {
76
+ status: 200,
77
+ headers: indexResponse.headers,
78
+ });
79
+ }
80
+ } catch {}
81
+ }
82
+ }
83
+
84
+ // Fallback: pass to worker handler
85
+ if (typeof handler.fetch === "function") return handler.fetch(request, env, ctx);
86
+ if (typeof handler === "function") return handler(request, env, ctx);
87
+ return new Response("Not Found", { status: 404 });
88
+ },
89
+ };
90
+ `;
91
+ }
92
+ // Pure Worker (no client assets)
93
+ return `import { _setEnv, _setCtx, generateWsToken } from "creek";
94
+ import userModule from "${importPath}";
95
+
96
+ const handler = userModule.default ?? userModule;
97
+
98
+ export default {
99
+ async fetch(request, env, ctx) {
100
+ _setEnv(env);
101
+ _setCtx(ctx);
102
+
103
+ // /__creek/config — auto-discovery for CreekRoom WebSocket
104
+ const url = new URL(request.url);
105
+ if (url.pathname === "/__creek/config" && request.method === "GET") {
106
+ const wsToken = await generateWsToken();
107
+ return new Response(JSON.stringify({
108
+ realtimeUrl: env.CREEK_REALTIME_URL || null,
109
+ projectSlug: env.CREEK_PROJECT_SLUG || null,
110
+ wsToken,
111
+ }), { headers: { "Content-Type": "application/json" } });
112
+ }
113
+
114
+ if (typeof handler.fetch === "function") return handler.fetch(request, env, ctx);
115
+ if (typeof handler === "function") return handler(request, env, ctx);
116
+ throw new Error("[creek] Worker must export default a fetch handler, Hono app, or { fetch() } object.");
117
+ },
118
+ };
119
+ `;
120
+ }
121
+ /**
122
+ * Bundle a Worker entry point with Creek runtime auto-injection.
123
+ *
124
+ * 1. Generates a wrapper entry that calls _setEnv(env)
125
+ * 2. Bundles wrapper + user code + creek runtime with esbuild
126
+ * 3. Returns the bundled JavaScript string
127
+ */
128
+ export async function bundleWorker(entryPoint, cwd, options) {
129
+ const wrapperDir = join(cwd, ".creek");
130
+ mkdirSync(wrapperDir, { recursive: true });
131
+ const wrapper = generateWorkerWrapper(entryPoint, wrapperDir, options);
132
+ const wrapperPath = join(wrapperDir, "__worker_entry.js");
133
+ writeFileSync(wrapperPath, wrapper);
134
+ return bundleSSRServer(wrapperPath);
135
+ }
136
+ //# sourceMappingURL=worker-bundle.js.map