@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,820 @@
1
+ import { defineCommand } from "citty";
2
+ import consola from "consola";
3
+ import { existsSync, readFileSync, readdirSync, statSync, rmSync } from "node:fs";
4
+ import { join, resolve } from "node:path";
5
+ import { execSync, execFileSync } from "node:child_process";
6
+ import { CreekClient, CreekAuthError, isSSRFramework, getSSRServerEntry, getClientAssetsDir, getDefaultBuildOutput, detectFramework, resolveConfig, formatDetectionSummary, resolvedConfigToResources, resolvedConfigToBindingRequirements, ConfigNotFoundError, getSSRServerDir, collectServerFiles, isPreBundledFramework, detectNextjsMode, detectMonorepo, } from "@solcreek/sdk";
7
+ import { getToken, getApiUrl } from "../utils/config.js";
8
+ import { collectAssets } from "../utils/bundle.js";
9
+ import { bundleSSRServer } from "../utils/ssr-bundle.js";
10
+ import { bundleWorker } from "../utils/worker-bundle.js";
11
+ import { sandboxDeploy, pollSandboxStatus, printSandboxSuccess } from "../utils/sandbox.js";
12
+ import { isTTY, jsonOutput, resolveJsonMode, globalArgs, shouldAutoConfirm } from "../utils/output.js";
13
+ import { ensureTosAccepted } from "../utils/tos.js";
14
+ import { buildNextjs, patchBundledWorker, hasAdapterOutput } from "../utils/nextjs.js";
15
+ import { isRepoUrl, parseRepoUrl, validateRepoUrl, validateSubpath, RepoUrlError } from "../utils/repo-url.js";
16
+ import { checkGitInstalled, cloneRepo, detectPackageManager, installDependencies, cleanupDir as cleanupCloneDir, GitCloneError } from "../utils/git-clone.js";
17
+ function section(name) {
18
+ consola.log(`\n \x1b[2m[${name}]\x1b[0m`);
19
+ }
20
+ function assetSummary(fileList) {
21
+ const byExt = {};
22
+ for (const f of fileList) {
23
+ const ext = f.includes(".") ? `.${f.split(".").pop()}` : "(other)";
24
+ byExt[ext] = (byExt[ext] ?? 0) + 1;
25
+ }
26
+ const parts = Object.entries(byExt)
27
+ .sort((a, b) => b[1] - a[1])
28
+ .slice(0, 4)
29
+ .map(([ext, n]) => `${n} ${ext}`);
30
+ return parts.join(", ");
31
+ }
32
+ export const deployCommand = defineCommand({
33
+ meta: {
34
+ name: "deploy",
35
+ description: "Deploy the current project to Creek",
36
+ },
37
+ args: {
38
+ dir: {
39
+ type: "positional",
40
+ description: "Directory to deploy (default: current directory)",
41
+ required: false,
42
+ },
43
+ "skip-build": {
44
+ type: "boolean",
45
+ description: "Skip the build step",
46
+ default: false,
47
+ },
48
+ demo: {
49
+ type: "boolean",
50
+ description: "Deploy a sample site to see Creek in action",
51
+ default: false,
52
+ },
53
+ ...globalArgs,
54
+ template: {
55
+ type: "string",
56
+ description: "Deploy a template (e.g., react-dashboard, astro-landing)",
57
+ required: false,
58
+ },
59
+ path: {
60
+ type: "string",
61
+ description: "Subdirectory within repo to deploy (for monorepos)",
62
+ required: false,
63
+ },
64
+ },
65
+ async run({ args }) {
66
+ const jsonMode = resolveJsonMode(args);
67
+ // --- Demo deploy (zero-friction showcase) ---
68
+ if (args.demo) {
69
+ return await deployDemo(jsonMode);
70
+ }
71
+ // --- Ensure ToS accepted (all non-demo paths) ---
72
+ const autoConfirm = shouldAutoConfirm(args);
73
+ const tos = await ensureTosAccepted(autoConfirm);
74
+ // --- Template deploy ---
75
+ if (args.template) {
76
+ return await deployTemplate(args.template);
77
+ }
78
+ // --- Repo URL deploy (creek deploy https://github.com/user/repo) ---
79
+ if (args.dir && isRepoUrl(args.dir)) {
80
+ return await deployRepoUrl(args.dir, {
81
+ path: args.path ?? null,
82
+ skipBuild: args["skip-build"],
83
+ json: jsonMode,
84
+ });
85
+ }
86
+ // --- Resolve target directory ---
87
+ const cwd = args.dir ? resolve(args.dir) : process.cwd();
88
+ const token = getToken();
89
+ // --- Explicit directory (creek deploy ./dist) ---
90
+ if (args.dir) {
91
+ if (!existsSync(cwd)) {
92
+ if (jsonMode)
93
+ jsonOutput({ error: "not_found", message: `Directory not found: ${args.dir}` }, 1);
94
+ consola.error(`Directory not found: ${args.dir}`);
95
+ process.exit(1);
96
+ }
97
+ return await deployDirectory(cwd, jsonMode, tos);
98
+ }
99
+ // --- Try auto-detection (creek.toml → wrangler.* → package.json → index.html) ---
100
+ let resolved = null;
101
+ try {
102
+ resolved = resolveConfig(cwd);
103
+ }
104
+ catch (err) {
105
+ if (!(err instanceof ConfigNotFoundError))
106
+ throw err;
107
+ }
108
+ if (resolved) {
109
+ if (!jsonMode) {
110
+ consola.info(` Detected: ${formatDetectionSummary(resolved)}`);
111
+ for (const ub of resolved.unsupportedBindings) {
112
+ consola.warn(` Binding '${ub.name}' (${ub.type}) is not yet supported — will be skipped`);
113
+ }
114
+ }
115
+ if (token) {
116
+ return await deployAuthenticated(cwd, resolved, token, args["skip-build"], jsonMode);
117
+ }
118
+ return await deploySandbox(cwd, args["skip-build"], jsonMode, resolved, tos);
119
+ }
120
+ // --- Auto-detect build output dirs ---
121
+ for (const dir of ["dist", "build", "out", ".output/public"]) {
122
+ const fullPath = resolve(cwd, dir);
123
+ if (existsSync(fullPath)) {
124
+ if (!jsonMode)
125
+ consola.info(`Found build output: ${dir}/`);
126
+ return await deployDirectory(fullPath, jsonMode, tos);
127
+ }
128
+ }
129
+ // --- Nothing found → guide the user ---
130
+ if (jsonMode)
131
+ jsonOutput({ error: "no_project", message: "No project found in this directory" }, 1);
132
+ consola.info("No project found in this directory.\n");
133
+ consola.info(" creek deploy ./dist Deploy a build output directory");
134
+ consola.info(" creek deploy --demo Deploy a sample site (see Creek in 5 seconds)");
135
+ consola.info("");
136
+ consola.info("Or create a project first:");
137
+ consola.info(" npm create vite@latest my-app && cd my-app && npx creek deploy");
138
+ process.exit(1);
139
+ },
140
+ });
141
+ async function deployRepoUrl(input, options) {
142
+ const { path: subpath, skipBuild, json: jsonMode } = options;
143
+ try {
144
+ // 1. Parse and validate URL
145
+ const parsed = parseRepoUrl(input);
146
+ validateRepoUrl(parsed);
147
+ if (subpath)
148
+ validateSubpath(subpath);
149
+ // 2. Check git is available
150
+ checkGitInstalled();
151
+ // 3. Clone
152
+ section("Clone");
153
+ consola.start(` Cloning ${parsed.displayUrl}...`);
154
+ const { tmpDir, workDir, sizeMb } = cloneRepo(parsed, { subpath });
155
+ consola.success(` Cloned (${sizeMb.toFixed(1)} MB)`);
156
+ try {
157
+ // 4. Resolve config
158
+ section("Detect");
159
+ let resolved;
160
+ try {
161
+ resolved = resolveConfig(workDir);
162
+ }
163
+ catch (err) {
164
+ if (err instanceof ConfigNotFoundError) {
165
+ consola.error(`No supported project found in ${parsed.displayUrl}.`);
166
+ consola.info("");
167
+ consola.info(" If this is a monorepo, specify a subdirectory:");
168
+ consola.info(` creek deploy ${input} --path packages/app`);
169
+ consola.info("");
170
+ consola.info(" Creek looks for: creek.toml, wrangler.*, package.json, or index.html");
171
+ process.exit(1);
172
+ }
173
+ throw err;
174
+ }
175
+ consola.info(` Detected: ${formatDetectionSummary(resolved)}`);
176
+ for (const ub of resolved.unsupportedBindings) {
177
+ consola.warn(` Binding '${ub.name}' (${ub.type}) is not yet supported — will be skipped`);
178
+ }
179
+ // 5. Install dependencies (if package.json exists)
180
+ if (existsSync(join(workDir, "package.json"))) {
181
+ section("Install");
182
+ const pm = detectPackageManager(workDir);
183
+ consola.start(` Installing dependencies (${pm})...`);
184
+ installDependencies(workDir, pm);
185
+ consola.success(` Dependencies installed`);
186
+ }
187
+ // 6. Route to authenticated or sandbox deploy
188
+ const token = getToken();
189
+ if (token) {
190
+ return await deployAuthenticated(workDir, resolved, token, skipBuild, jsonMode);
191
+ }
192
+ return await deploySandbox(workDir, skipBuild, jsonMode, resolved);
193
+ }
194
+ finally {
195
+ cleanupCloneDir(tmpDir);
196
+ }
197
+ }
198
+ catch (err) {
199
+ if (err instanceof RepoUrlError || err instanceof GitCloneError) {
200
+ if (jsonMode)
201
+ jsonOutput({ error: "repo_deploy_failed", message: err.message }, 1);
202
+ consola.error(err.message);
203
+ process.exit(1);
204
+ }
205
+ throw err;
206
+ }
207
+ }
208
+ // ============================================================================
209
+ // Demo deploy — pre-built page, zero dependencies, instant
210
+ // ============================================================================
211
+ const DEMO_HTML = `<!DOCTYPE html>
212
+ <html lang="en">
213
+ <head>
214
+ <meta charset="utf-8">
215
+ <meta name="viewport" content="width=device-width, initial-scale=1">
216
+ <title>Creek Deploy Demo</title>
217
+ <style>
218
+ *{margin:0;padding:0;box-sizing:border-box}
219
+ body{font-family:system-ui,-apple-system,sans-serif;background:#0a0a0a;color:#f5f5f5;min-height:100vh;display:flex;flex-direction:column;align-items:center;justify-content:center}
220
+ .card{max-width:480px;text-align:center;padding:3rem 2rem}
221
+ h1{font-size:2rem;font-weight:700;letter-spacing:-0.03em;margin-bottom:0.5rem}
222
+ .accent{background:linear-gradient(135deg,#38bdf8,#818cf8);-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text}
223
+ p{color:#888;line-height:1.6;margin-top:1rem}
224
+ .meta{margin-top:2rem;font-size:0.85rem;color:#555}
225
+ .cta{display:inline-block;margin-top:1.5rem;background:linear-gradient(135deg,#2563eb,#3b82f6);color:#fff;padding:10px 24px;border-radius:8px;text-decoration:none;font-weight:600;font-size:0.9rem;transition:opacity 0.15s}
226
+ .cta:hover{opacity:0.9}
227
+ code{background:#1a1a2e;padding:2px 8px;border-radius:4px;font-size:0.85rem;color:#a5b4fc}
228
+ .pulse{width:12px;height:12px;background:#22c55e;border-radius:50%;display:inline-block;margin-right:8px;animation:pulse 2s infinite}
229
+ @keyframes pulse{0%,100%{opacity:1}50%{opacity:0.4}}
230
+ </style>
231
+ </head>
232
+ <body>
233
+ <div class="card">
234
+ <h1><span class="accent">Creek</span> is live.</h1>
235
+ <p>This site was deployed to the edge in seconds — no build step, no config, no account.</p>
236
+ <p style="margin-top:1.5rem"><span class="pulse"></span>Running on Cloudflare's global network</p>
237
+ <p class="meta">Now try it with your own project:</p>
238
+ <p style="margin-top:0.5rem"><code>cd your-project && npx creek deploy</code></p>
239
+ <a href="https://creek.dev" class="cta">Learn more about Creek</a>
240
+ </div>
241
+ <script>document.title="Creek Deploy Demo — "+new Date().toLocaleTimeString()</script>
242
+ </body>
243
+ </html>`;
244
+ async function deployDemo(jsonMode) {
245
+ if (!jsonMode)
246
+ consola.start("Deploying demo site...");
247
+ try {
248
+ const result = await sandboxDeploy({
249
+ assets: { "index.html": Buffer.from(DEMO_HTML).toString("base64") },
250
+ source: "cli-demo",
251
+ });
252
+ if (!jsonMode)
253
+ consola.start("Waiting for deployment...");
254
+ const status = await pollSandboxStatus(result.statusUrl);
255
+ if (jsonMode) {
256
+ jsonOutput({
257
+ ok: true,
258
+ sandboxId: result.sandboxId,
259
+ url: status.previewUrl,
260
+ deployDurationMs: status.deployDurationMs,
261
+ expiresAt: result.expiresAt,
262
+ mode: "demo",
263
+ });
264
+ }
265
+ const duration = status.deployDurationMs
266
+ ? `in ${(status.deployDurationMs / 1000).toFixed(1)}s`
267
+ : "in seconds";
268
+ consola.success(`Live ${duration} → ${status.previewUrl}`);
269
+ consola.info("");
270
+ consola.info("That's Creek. Now deploy your own project:");
271
+ consola.info(" cd your-project && npx creek deploy");
272
+ }
273
+ catch (err) {
274
+ const message = err instanceof Error ? err.message : "Demo deploy failed";
275
+ if (jsonMode)
276
+ jsonOutput({ ok: false, error: "deploy_failed", message }, 1);
277
+ consola.error(message);
278
+ process.exit(1);
279
+ }
280
+ }
281
+ // ============================================================================
282
+ // Directory deploy — deploy pre-built static files directly
283
+ // ============================================================================
284
+ async function deployDirectory(dir, jsonMode, tos) {
285
+ if (!jsonMode)
286
+ section("Upload");
287
+ const { assets, fileList } = collectAssets(dir);
288
+ if (fileList.length === 0) {
289
+ if (jsonMode)
290
+ jsonOutput({ ok: false, error: "no_files", message: `No files found in ${dir}` }, 1);
291
+ consola.error(`No files found in ${dir}\n`);
292
+ consola.info(" creek deploy ./dist Deploy a build output directory");
293
+ consola.info(" creek deploy --demo Deploy a sample site (see Creek in 5 seconds)");
294
+ process.exit(1);
295
+ }
296
+ if (!jsonMode) {
297
+ consola.info(` ${fileList.length} files (${assetSummary(fileList)})`);
298
+ consola.info(" Mode: sandbox (60 min preview)");
299
+ section("Deploy");
300
+ consola.start(" Deploying to edge...");
301
+ }
302
+ try {
303
+ const result = await sandboxDeploy({ assets, source: "cli" }, { tos });
304
+ const status = await pollSandboxStatus(result.statusUrl);
305
+ if (jsonMode) {
306
+ jsonOutput({
307
+ ok: true,
308
+ sandboxId: result.sandboxId,
309
+ url: status.previewUrl,
310
+ deployDurationMs: status.deployDurationMs,
311
+ expiresAt: result.expiresAt,
312
+ assetCount: fileList.length,
313
+ mode: "sandbox",
314
+ });
315
+ }
316
+ printSandboxSuccess(status.previewUrl, result.expiresAt, result.sandboxId);
317
+ }
318
+ catch (err) {
319
+ const message = err instanceof Error ? err.message : "Deploy failed";
320
+ if (jsonMode)
321
+ jsonOutput({ ok: false, error: "deploy_failed", message }, 1);
322
+ consola.error(message);
323
+ process.exit(1);
324
+ }
325
+ }
326
+ // ============================================================================
327
+ // Sandbox deploy — auto-detect framework, build, deploy
328
+ // ============================================================================
329
+ async function deploySandbox(cwd, skipBuild, jsonMode = false, resolved, tos) {
330
+ const framework = resolved?.framework ?? detectFramework(JSON.parse(readFileSync(join(cwd, "package.json"), "utf-8")));
331
+ // Detect Next.js mode (static vs opennext SSR)
332
+ const pkg = JSON.parse(readFileSync(join(cwd, "package.json"), "utf-8"));
333
+ const nextjsMode = framework === "nextjs" ? detectNextjsMode(pkg, cwd) : null;
334
+ const monorepo = detectMonorepo(cwd);
335
+ section("Detect");
336
+ consola.info(` Framework: ${framework ?? "static site"}`);
337
+ if (nextjsMode)
338
+ consola.info(` Next.js mode: ${nextjsMode}${monorepo.isMonorepo ? " (monorepo)" : ""}`);
339
+ consola.info(" Mode: sandbox (60 min preview)");
340
+ // Build
341
+ if (!skipBuild) {
342
+ section("Build");
343
+ if (nextjsMode === "opennext") {
344
+ // Next.js SSR on CF Workers: adapter (>= 16.2) or legacy opennext
345
+ try {
346
+ buildNextjs(cwd, monorepo.isMonorepo);
347
+ }
348
+ catch {
349
+ consola.error("Next.js build failed");
350
+ process.exit(1);
351
+ }
352
+ consola.success(" Build complete");
353
+ }
354
+ else {
355
+ const buildCommand = resolved?.buildCommand || "npm run build";
356
+ if (!buildCommand) {
357
+ consola.error("No build script found in package.json.");
358
+ consola.info("Add a 'build' script or use --skip-build if already built.");
359
+ process.exit(1);
360
+ }
361
+ consola.start(` ${buildCommand}`);
362
+ try {
363
+ execSync(buildCommand, { cwd, stdio: "inherit" });
364
+ }
365
+ catch {
366
+ consola.error("Build failed");
367
+ consola.info("");
368
+ consola.info(" Common fixes:");
369
+ consola.info(" npm install (missing dependencies?)");
370
+ consola.info(" Check for TypeScript errors");
371
+ process.exit(1);
372
+ }
373
+ consola.success(" Build complete");
374
+ }
375
+ }
376
+ const useAdapterOutput = framework === "nextjs" && hasAdapterOutput(cwd);
377
+ const outputDir = useAdapterOutput
378
+ ? resolve(cwd, ".creek/adapter-output")
379
+ : resolve(cwd, resolved?.buildOutput ?? getDefaultBuildOutput(framework));
380
+ if (!existsSync(outputDir)) {
381
+ consola.error(`Build output not found: ${outputDir}`);
382
+ if (framework) {
383
+ consola.info(`Expected output for ${framework}: ${getDefaultBuildOutput(framework)}`);
384
+ }
385
+ process.exit(1);
386
+ }
387
+ // Collect assets
388
+ const isSSR = isSSRFramework(framework);
389
+ const renderMode = isSSR ? "ssr" : "spa";
390
+ let clientAssetsDir;
391
+ if (useAdapterOutput) {
392
+ clientAssetsDir = resolve(outputDir, "assets");
393
+ }
394
+ else {
395
+ clientAssetsDir = outputDir;
396
+ if (isSSR && framework) {
397
+ const subdir = getClientAssetsDir(framework);
398
+ if (subdir)
399
+ clientAssetsDir = resolve(outputDir, subdir);
400
+ }
401
+ }
402
+ section("Upload");
403
+ const { assets: clientAssets, fileList } = collectAssets(clientAssetsDir);
404
+ consola.info(` ${fileList.length} assets (${assetSummary(fileList)})`);
405
+ let serverFiles;
406
+ if (isSSR && framework) {
407
+ const serverEntry = getSSRServerEntry(framework);
408
+ if (serverEntry) {
409
+ const serverEntryPath = resolve(outputDir, serverEntry);
410
+ if (existsSync(serverEntryPath)) {
411
+ consola.start(" Bundling SSR server...");
412
+ const bundled = await bundleSSRServer(serverEntryPath);
413
+ serverFiles = { "server.js": Buffer.from(bundled).toString("base64") };
414
+ consola.success(` SSR bundled (${Math.round(bundled.length / 1024)}KB)`);
415
+ }
416
+ }
417
+ }
418
+ // Deploy to sandbox
419
+ if (!jsonMode) {
420
+ section("Deploy");
421
+ consola.start(" Deploying to edge...");
422
+ }
423
+ try {
424
+ const result = await sandboxDeploy({
425
+ assets: clientAssets,
426
+ serverFiles,
427
+ framework: framework ?? undefined,
428
+ source: "cli",
429
+ }, { tos });
430
+ const status = await pollSandboxStatus(result.statusUrl);
431
+ if (jsonMode) {
432
+ jsonOutput({
433
+ ok: true,
434
+ sandboxId: result.sandboxId,
435
+ url: status.previewUrl,
436
+ deployDurationMs: status.deployDurationMs,
437
+ expiresAt: result.expiresAt,
438
+ framework: framework ?? null,
439
+ assetCount: fileList.length,
440
+ mode: "sandbox",
441
+ });
442
+ }
443
+ printSandboxSuccess(status.previewUrl, result.expiresAt, result.sandboxId);
444
+ }
445
+ catch (err) {
446
+ const message = err instanceof Error ? err.message : "Sandbox deploy failed";
447
+ if (jsonMode)
448
+ jsonOutput({ ok: false, error: "deploy_failed", message }, 1);
449
+ consola.error(message);
450
+ process.exit(1);
451
+ }
452
+ }
453
+ // ============================================================================
454
+ // Template deploy — clone + build + deploy to sandbox
455
+ // ============================================================================
456
+ async function deployTemplate(templateId) {
457
+ // Validate template ID — alphanumeric, hyphens, underscores only (no path traversal)
458
+ if (!/^[a-zA-Z0-9_-]+$/.test(templateId)) {
459
+ consola.error("Invalid template name. Use only letters, numbers, hyphens, and underscores.");
460
+ process.exit(1);
461
+ }
462
+ consola.info(`Deploying template: ${templateId}`);
463
+ // Clone template to temp dir
464
+ const tmpDir = join(process.env.TMPDIR ?? "/tmp", `creek-template-${Date.now()}`);
465
+ const repoUrl = "https://github.com/solcreek/templates";
466
+ consola.start("Cloning template...");
467
+ try {
468
+ execFileSync("git", [
469
+ "clone", "--depth", "1", "--filter=blob:none", "--sparse", repoUrl, tmpDir,
470
+ ], { stdio: "pipe" });
471
+ execFileSync("git", [
472
+ "sparse-checkout", "set", templateId,
473
+ ], { cwd: tmpDir, stdio: "pipe" });
474
+ }
475
+ catch {
476
+ consola.error(`Template '${templateId}' not found.`);
477
+ consola.info("Available templates: creek deploy --template");
478
+ cleanupDir(tmpDir);
479
+ process.exit(1);
480
+ }
481
+ const templateDir = join(tmpDir, templateId);
482
+ // Verify resolved path is still within tmpDir (prevent path traversal)
483
+ if (!resolve(templateDir).startsWith(resolve(tmpDir))) {
484
+ consola.error("Invalid template path.");
485
+ cleanupDir(tmpDir);
486
+ process.exit(1);
487
+ }
488
+ if (!existsSync(templateDir)) {
489
+ consola.error(`Template directory not found: ${templateId}`);
490
+ cleanupDir(tmpDir);
491
+ process.exit(1);
492
+ }
493
+ consola.start("Installing dependencies...");
494
+ try {
495
+ execFileSync("npm", ["install"], { cwd: templateDir, stdio: "pipe" });
496
+ }
497
+ catch {
498
+ consola.error("Failed to install dependencies");
499
+ cleanupDir(tmpDir);
500
+ process.exit(1);
501
+ }
502
+ // Deploy as sandbox (reuse sandbox flow)
503
+ await deploySandbox(templateDir, false);
504
+ // Cleanup
505
+ cleanupDir(tmpDir);
506
+ }
507
+ function cleanupDir(dir) {
508
+ try {
509
+ rmSync(dir, { recursive: true, force: true });
510
+ }
511
+ catch {
512
+ // ignore
513
+ }
514
+ }
515
+ // ============================================================================
516
+ // Authenticated deploy — existing flow
517
+ // ============================================================================
518
+ async function deployAuthenticated(cwd, resolved, token, skipBuild, jsonMode = false) {
519
+ try {
520
+ const client = new CreekClient(getApiUrl(), token);
521
+ section("Auth");
522
+ const session = await client.getSession();
523
+ if (!session?.user) {
524
+ consola.error("Token is invalid or expired. Run `creek login` to re-authenticate.");
525
+ process.exit(1);
526
+ }
527
+ consola.info(` Deploying as ${session.user.email}`);
528
+ consola.info(` Project: ${resolved.projectName}`);
529
+ // Ensure project exists — confirm before auto-creating
530
+ let project;
531
+ try {
532
+ project = await client.getProject(resolved.projectName);
533
+ }
534
+ catch {
535
+ if (!jsonMode && isTTY) {
536
+ const confirm = await consola.prompt(`Project "${resolved.projectName}" does not exist. Create it?`, { type: "confirm" });
537
+ if (!confirm) {
538
+ consola.info("Deploy cancelled.");
539
+ process.exit(0);
540
+ }
541
+ }
542
+ const res = await client.createProject({
543
+ slug: resolved.projectName,
544
+ framework: resolved.framework ?? undefined,
545
+ });
546
+ project = res.project;
547
+ if (!jsonMode)
548
+ consola.success(` Created project: ${project.slug}`);
549
+ }
550
+ // Determine deploy mode
551
+ const framework = resolved.framework;
552
+ const isSSR = isSSRFramework(framework);
553
+ const isWorker = !framework && !!resolved.workerEntry;
554
+ const renderMode = isWorker ? "worker" : (isSSR ? "ssr" : "spa");
555
+ // Detect Next.js mode for special build handling
556
+ const pkg = existsSync(join(cwd, "package.json"))
557
+ ? JSON.parse(readFileSync(join(cwd, "package.json"), "utf-8"))
558
+ : {};
559
+ const nextjsMode = framework === "nextjs" ? detectNextjsMode(pkg, cwd) : null;
560
+ const monorepo = framework === "nextjs" ? detectMonorepo(cwd) : { isMonorepo: false, root: null };
561
+ if (nextjsMode) {
562
+ if (!jsonMode)
563
+ consola.info(` Next.js mode: ${nextjsMode}${monorepo.isMonorepo ? " (monorepo)" : ""}`);
564
+ }
565
+ // Build (skip for pure Workers with no build command)
566
+ if (!skipBuild && resolved.buildCommand) {
567
+ section("Build");
568
+ if (nextjsMode === "opennext") {
569
+ try {
570
+ buildNextjs(cwd, monorepo.isMonorepo);
571
+ }
572
+ catch {
573
+ consola.error("Next.js build failed");
574
+ process.exit(1);
575
+ }
576
+ consola.success(" Build complete");
577
+ }
578
+ else {
579
+ const buildCmd = resolved.buildCommand;
580
+ if (buildCmd.length > 500) {
581
+ consola.error("Invalid build command (too long)");
582
+ process.exit(1);
583
+ }
584
+ consola.start(` ${buildCmd}`);
585
+ try {
586
+ execSync(buildCmd, { cwd, stdio: "inherit" });
587
+ }
588
+ catch {
589
+ consola.error("Build failed");
590
+ process.exit(1);
591
+ }
592
+ consola.success(" Build complete");
593
+ }
594
+ }
595
+ // Collect client assets
596
+ // Worker + SPA hybrid: if a Worker project has buildOutput with built files, collect them too
597
+ let clientAssets = {};
598
+ let fileList = [];
599
+ const workerHasClientAssets = isWorker && resolved.buildOutput && resolved.buildOutput !== "." &&
600
+ existsSync(resolve(cwd, resolved.buildOutput));
601
+ if (!isWorker || workerHasClientAssets) {
602
+ let clientAssetsDir;
603
+ // Adapter output takes precedence for Next.js
604
+ if (framework === "nextjs" && hasAdapterOutput(cwd)) {
605
+ clientAssetsDir = resolve(cwd, ".creek/adapter-output/assets");
606
+ }
607
+ else {
608
+ const outputDir = resolve(cwd, resolved.buildOutput);
609
+ if (!existsSync(outputDir)) {
610
+ consola.error(`Build output directory not found: ${resolved.buildOutput}`);
611
+ process.exit(1);
612
+ }
613
+ clientAssetsDir = outputDir;
614
+ if (isSSR && framework) {
615
+ const clientSubdir = getClientAssetsDir(framework);
616
+ if (clientSubdir) {
617
+ clientAssetsDir = resolve(outputDir, clientSubdir);
618
+ }
619
+ }
620
+ }
621
+ section("Upload");
622
+ ({ assets: clientAssets, fileList } = collectAssets(clientAssetsDir));
623
+ consola.info(` ${fileList.length} assets (${assetSummary(fileList)})`);
624
+ }
625
+ // Bundle server/worker files
626
+ let serverFiles;
627
+ if (isSSR && framework) {
628
+ if (framework === "nextjs" && hasAdapterOutput(cwd)) {
629
+ // Adapter path: read pre-bundled output from .creek/adapter-output/
630
+ const adapterServerDir = resolve(cwd, ".creek/adapter-output/server");
631
+ consola.start(" Collecting adapter output...");
632
+ const collected = {};
633
+ if (existsSync(adapterServerDir)) {
634
+ for (const f of readdirSync(adapterServerDir)) {
635
+ const fp = join(adapterServerDir, f);
636
+ if (!statSync(fp).isFile())
637
+ continue;
638
+ if (f.endsWith(".map"))
639
+ continue;
640
+ collected[f] = readFileSync(fp);
641
+ }
642
+ }
643
+ const fileCount = Object.keys(collected).length;
644
+ serverFiles = Object.fromEntries(Object.entries(collected).map(([p, buf]) => [p, buf.toString("base64")]));
645
+ consola.success(` Worker bundled: ${fileCount} files (${Math.round(Object.values(collected).reduce((s, b) => s + b.length, 0) / 1024)}KB)`);
646
+ }
647
+ else if (framework === "nextjs") {
648
+ // Legacy path: use wrangler to produce a single bundled worker
649
+ const bundleDir = resolve(cwd, ".creek/bundled");
650
+ consola.start(" Bundling Next.js worker (legacy)...");
651
+ execSync(`npx wrangler deploy --dry-run --outdir "${bundleDir}"`, { cwd, stdio: "pipe" });
652
+ patchBundledWorker(bundleDir, resolve(cwd, ".open-next"));
653
+ const collected = {};
654
+ if (existsSync(bundleDir)) {
655
+ for (const f of readdirSync(bundleDir)) {
656
+ const fp = join(bundleDir, f);
657
+ if (!statSync(fp).isFile())
658
+ continue;
659
+ if (f.endsWith(".map") || f === "README.md")
660
+ continue;
661
+ collected[f] = readFileSync(fp);
662
+ }
663
+ }
664
+ const fileCount = Object.keys(collected).length;
665
+ serverFiles = Object.fromEntries(Object.entries(collected).map(([p, buf]) => [p, buf.toString("base64")]));
666
+ consola.success(` Worker bundled: ${fileCount} files (${Math.round(Object.values(collected).reduce((s, b) => s + b.length, 0) / 1024)}KB)`);
667
+ }
668
+ else if (isPreBundledFramework(framework)) {
669
+ // Other pre-bundled SSR frameworks (Nuxt, SvelteKit, etc.)
670
+ const serverDirRel = getSSRServerDir(framework);
671
+ if (serverDirRel) {
672
+ const serverDir = resolve(cwd, serverDirRel);
673
+ if (existsSync(serverDir)) {
674
+ consola.start(" Collecting SSR server files...");
675
+ const collected = collectServerFiles(serverDir);
676
+ const fileCount = Object.keys(collected).length;
677
+ serverFiles = Object.fromEntries(Object.entries(collected).map(([p, buf]) => [p, buf.toString("base64")]));
678
+ consola.success(` SSR server: ${fileCount} files`);
679
+ }
680
+ }
681
+ }
682
+ else {
683
+ // Non-pre-bundled SSR: esbuild single-file bundle (fallback)
684
+ const outputDir = resolve(cwd, resolved.buildOutput);
685
+ const serverEntry = getSSRServerEntry(framework);
686
+ if (serverEntry) {
687
+ const serverEntryPath = resolve(outputDir, serverEntry);
688
+ if (existsSync(serverEntryPath)) {
689
+ consola.start(" Bundling SSR server...");
690
+ const bundled = await bundleSSRServer(serverEntryPath);
691
+ serverFiles = {
692
+ "server.js": Buffer.from(bundled).toString("base64"),
693
+ };
694
+ consola.success(` SSR bundled (${Math.round(bundled.length / 1024)}KB)`);
695
+ }
696
+ }
697
+ }
698
+ }
699
+ else if (isWorker && resolved.workerEntry) {
700
+ // Worker: auto-generate _setEnv wrapper + esbuild bundle
701
+ const workerEntryPath = resolve(cwd, resolved.workerEntry);
702
+ if (existsSync(workerEntryPath)) {
703
+ section("Bundle");
704
+ consola.start(" Bundling worker...");
705
+ const bundled = await bundleWorker(workerEntryPath, cwd, {
706
+ hasClientAssets: !!workerHasClientAssets,
707
+ });
708
+ serverFiles = {
709
+ "worker.js": Buffer.from(bundled).toString("base64"),
710
+ };
711
+ consola.success(` Worker bundled (${Math.round(bundled.length / 1024)}KB)`);
712
+ }
713
+ else {
714
+ consola.error(`Worker entry not found: ${resolved.workerEntry}`);
715
+ process.exit(1);
716
+ }
717
+ }
718
+ section("Deploy");
719
+ consola.start(" Creating deployment...");
720
+ const { deployment } = await client.createDeployment(project.id);
721
+ consola.start(" Uploading bundle...");
722
+ const bundle = {
723
+ manifest: {
724
+ assets: fileList,
725
+ hasWorker: isSSR || isWorker,
726
+ entrypoint: resolved.workerEntry,
727
+ renderMode,
728
+ framework: framework ?? undefined,
729
+ },
730
+ workerScript: null,
731
+ assets: clientAssets,
732
+ serverFiles,
733
+ // Backward compat: boolean flags
734
+ resources: resolvedConfigToResources(resolved),
735
+ // New: binding declarations with user-defined names
736
+ bindings: resolvedConfigToBindingRequirements(resolved),
737
+ // Pass through wrangler vars and compat settings
738
+ ...(Object.keys(resolved.vars).length > 0 ? { vars: resolved.vars } : {}),
739
+ ...(resolved.compatibilityDate ? { compatibilityDate: resolved.compatibilityDate } : {}),
740
+ // nodejs_compat required for Creek runtime (AsyncLocalStorage).
741
+ // Adapter path uses nodejs_compat_v2 for full Node.js APIs.
742
+ ...(hasAdapterOutput(cwd)
743
+ ? { compatibilityFlags: ["nodejs_compat_v2"] }
744
+ : { compatibilityFlags: [
745
+ "nodejs_compat",
746
+ ...resolved.compatibilityFlags.filter((f) => f !== "nodejs_compat"),
747
+ ] }),
748
+ };
749
+ await client.uploadDeploymentBundle(project.id, deployment.id, bundle);
750
+ // Poll for async deploy progress
751
+ const POLL_INTERVAL = 1000;
752
+ const POLL_TIMEOUT = 120_000;
753
+ const TERMINAL = new Set(["active", "failed", "cancelled"]);
754
+ const STEP_LABELS = {
755
+ queued: " Waiting...",
756
+ uploading: " Uploading...",
757
+ provisioning: " Provisioning resources...",
758
+ deploying: " Deploying to edge...",
759
+ };
760
+ let lastStatus = "";
761
+ const start = Date.now();
762
+ while (Date.now() - start < POLL_TIMEOUT) {
763
+ const res = await client.getDeploymentStatus(project.id, deployment.id);
764
+ const { status, failed_step, error_message } = res.deployment;
765
+ if (status !== lastStatus) {
766
+ if (lastStatus && STEP_LABELS[lastStatus]) {
767
+ consola.success(STEP_LABELS[lastStatus].replace("...", ""));
768
+ }
769
+ if (!TERMINAL.has(status) && STEP_LABELS[status]) {
770
+ consola.start(STEP_LABELS[status]);
771
+ }
772
+ lastStatus = status;
773
+ }
774
+ if (status === "active") {
775
+ if (jsonMode) {
776
+ jsonOutput({
777
+ ok: true,
778
+ url: res.url ?? res.previewUrl,
779
+ previewUrl: res.previewUrl,
780
+ deploymentId: deployment.id,
781
+ project: project.slug,
782
+ mode: "production",
783
+ });
784
+ }
785
+ consola.success(` Deployed! ${res.url ?? res.previewUrl}`);
786
+ if (res.url && res.previewUrl) {
787
+ consola.info(` Preview: ${res.previewUrl}`);
788
+ }
789
+ return;
790
+ }
791
+ if (status === "failed") {
792
+ const step = failed_step ? ` at ${failed_step}` : "";
793
+ const msg = error_message ?? "Unknown error";
794
+ if (jsonMode)
795
+ jsonOutput({ ok: false, error: "deploy_failed", message: msg, failedStep: failed_step }, 1);
796
+ consola.error(`Deploy failed${step}: ${msg}`);
797
+ process.exit(1);
798
+ }
799
+ if (status === "cancelled") {
800
+ if (jsonMode)
801
+ jsonOutput({ ok: false, error: "cancelled", message: "Deploy was cancelled" }, 1);
802
+ consola.warn("Deploy was cancelled");
803
+ process.exit(1);
804
+ }
805
+ await new Promise((r) => setTimeout(r, POLL_INTERVAL));
806
+ }
807
+ if (jsonMode)
808
+ jsonOutput({ ok: false, error: "timeout", message: "Deploy timed out after 2 minutes" }, 1);
809
+ consola.error("Deploy timed out after 2 minutes");
810
+ process.exit(1);
811
+ }
812
+ catch (err) {
813
+ if (err instanceof CreekAuthError) {
814
+ consola.error("Authentication failed. Run `creek login` to re-authenticate.");
815
+ process.exit(1);
816
+ }
817
+ throw err;
818
+ }
819
+ }
820
+ //# sourceMappingURL=deploy.js.map