@solcreek/cli 0.4.6 → 0.4.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.
@@ -14,6 +14,11 @@ export declare const deployCommand: import("citty").CommandDef<{
14
14
  description: string;
15
15
  required: false;
16
16
  };
17
+ "no-cache": {
18
+ type: "boolean";
19
+ description: string;
20
+ default: false;
21
+ };
17
22
  "from-github": {
18
23
  type: "boolean";
19
24
  description: string;
@@ -4,7 +4,7 @@ import { existsSync, readFileSync, writeFileSync, readdirSync, statSync, rmSync
4
4
  // ajv is lazy-imported only when --template --data is used (avoid top-level crash if deps missing)
5
5
  import { join, resolve } from "node:path";
6
6
  import { execSync, execFileSync } from "node:child_process";
7
- import { CreekClient, CreekAuthError, isSSRFramework, getSSRServerEntry, getClientAssetsDir, getDefaultBuildOutput, detectFramework, resolveConfig, formatDetectionSummary, resolvedConfigToResources, resolvedConfigToBindingRequirements, ConfigNotFoundError, getSSRServerDir, collectServerFiles, isPreBundledFramework, detectNextjsMode, detectMonorepo, } from "@solcreek/sdk";
7
+ import { CreekClient, CreekAuthError, isSSRFramework, getSSRServerEntry, getClientAssetsDir, getDefaultBuildOutput, detectFramework, resolveConfig, formatDetectionSummary, resolvedConfigToResources, resolvedConfigToBindingRequirements, ConfigNotFoundError, getSSRServerDir, collectServerFiles, isPreBundledFramework, detectAstroCloudflareBuild, detectNextjsMode, detectMonorepo, } from "@solcreek/sdk";
8
8
  import { getToken, getApiUrl } from "../utils/config.js";
9
9
  import { collectAssets } from "../utils/bundle.js";
10
10
  import { bundleSSRServer } from "../utils/ssr-bundle.js";
@@ -201,6 +201,11 @@ export const deployCommand = defineCommand({
201
201
  description: "Subdirectory within the repo to deploy, for monorepos. Example: --path apps/web",
202
202
  required: false,
203
203
  },
204
+ "no-cache": {
205
+ type: "boolean",
206
+ description: "Skip build cache check — always build locally. Use when you suspect cached output is stale or you changed build config without changing source files.",
207
+ default: false,
208
+ },
204
209
  "from-github": {
205
210
  type: "boolean",
206
211
  description: "Skip local build; trigger a remote deploy of the latest commit on the project's production branch via its GitHub connection.",
@@ -287,7 +292,7 @@ export const deployCommand = defineCommand({
287
292
  }
288
293
  }
289
294
  if (token) {
290
- return await deployAuthenticated(cwd, resolved, token, args["skip-build"], jsonMode);
295
+ return await deployAuthenticated(cwd, resolved, token, args["skip-build"], jsonMode, args["no-cache"]);
291
296
  }
292
297
  return await deploySandbox(cwd, args["skip-build"], jsonMode, resolved, tos);
293
298
  }
@@ -726,24 +731,47 @@ async function deploySandbox(cwd, skipBuild, jsonMode = false, resolved, tos) {
726
731
  }
727
732
  process.exit(1);
728
733
  }
729
- // Collect assets
734
+ // Collect assets + determine render mode.
735
+ //
736
+ // Three shapes, mirroring the authenticated deploy path:
737
+ // 1. Framework-detected SSR (Astro/Nuxt/etc.) → bundle the framework's
738
+ // server entry, upload dist/ as assets.
739
+ // 2. User-declared Worker (`[build].worker` in creek.toml, no framework)
740
+ // → bundle the Worker entry with esbuild, serverFiles = { worker.js }.
741
+ // `dist/` assets are skipped for now — the "Workers + Static Assets
742
+ // coexist" zero-config pattern is tracked separately and requires
743
+ // build-pipeline changes; until it lands, users inline HTML/JS/CSS
744
+ // into the Worker (see docs).
745
+ // 3. Neither → plain SPA/static, everything goes as assets.
730
746
  const isSSR = isSSRFramework(framework);
731
- const renderMode = isSSR ? "ssr" : "spa";
732
- let clientAssetsDir;
733
- if (useAdapterOutput) {
734
- clientAssetsDir = resolve(outputDir, "assets");
735
- }
736
- else {
737
- clientAssetsDir = outputDir;
738
- if (isSSR && framework) {
739
- const subdir = getClientAssetsDir(framework);
740
- if (subdir)
741
- clientAssetsDir = resolve(outputDir, subdir);
747
+ const isWorker = !framework && !!resolved?.workerEntry;
748
+ const renderMode = isWorker ? "worker" : (isSSR ? "ssr" : "spa");
749
+ let clientAssets = {};
750
+ let fileList = [];
751
+ if (!isWorker) {
752
+ let clientAssetsDir;
753
+ if (useAdapterOutput) {
754
+ clientAssetsDir = resolve(outputDir, "assets");
742
755
  }
756
+ else {
757
+ clientAssetsDir = outputDir;
758
+ if (isSSR && framework) {
759
+ const subdir = getClientAssetsDir(framework);
760
+ if (subdir)
761
+ clientAssetsDir = resolve(outputDir, subdir);
762
+ }
763
+ }
764
+ const collected = collectAssets(clientAssetsDir);
765
+ clientAssets = collected.assets;
766
+ fileList = collected.fileList;
743
767
  }
744
768
  section("Upload");
745
- const { assets: clientAssets, fileList } = collectAssets(clientAssetsDir);
746
- consola.info(` ${fileList.length} assets (${assetSummary(fileList)})`);
769
+ if (isWorker) {
770
+ consola.info(` Worker mode (${resolved.workerEntry})`);
771
+ }
772
+ else {
773
+ consola.info(` ${fileList.length} assets (${assetSummary(fileList)})`);
774
+ }
747
775
  let serverFiles;
748
776
  if (isSSR && framework) {
749
777
  const serverEntry = getSSRServerEntry(framework);
@@ -757,6 +785,19 @@ async function deploySandbox(cwd, skipBuild, jsonMode = false, resolved, tos) {
757
785
  }
758
786
  }
759
787
  }
788
+ else if (isWorker && resolved?.workerEntry) {
789
+ const workerEntryPath = resolve(cwd, resolved.workerEntry);
790
+ if (!existsSync(workerEntryPath)) {
791
+ consola.error(`Worker entry not found: ${resolved.workerEntry}`);
792
+ process.exit(1);
793
+ }
794
+ consola.start(" Bundling worker...");
795
+ const bundled = await bundleWorker(workerEntryPath, cwd, {
796
+ hasClientAssets: false,
797
+ });
798
+ serverFiles = { "worker.js": Buffer.from(bundled).toString("base64") };
799
+ consola.success(` Worker bundled (${Math.round(bundled.length / 1024)}KB)`);
800
+ }
760
801
  // Deploy to sandbox
761
802
  if (!jsonMode) {
762
803
  section("Deploy");
@@ -764,10 +805,25 @@ async function deploySandbox(cwd, skipBuild, jsonMode = false, resolved, tos) {
764
805
  }
765
806
  try {
766
807
  const result = await sandboxDeploy({
808
+ manifest: {
809
+ assets: fileList,
810
+ hasWorker: isSSR || isWorker,
811
+ entrypoint: resolved?.workerEntry ?? null,
812
+ renderMode,
813
+ },
767
814
  assets: clientAssets,
768
815
  serverFiles,
769
816
  framework: framework ?? undefined,
770
817
  source: "cli",
818
+ ...(resolved
819
+ ? { bindings: resolvedConfigToBindingRequirements(resolved) }
820
+ : {}),
821
+ ...(resolved?.compatibilityDate
822
+ ? { compatibilityDate: resolved.compatibilityDate }
823
+ : {}),
824
+ ...(resolved && resolved.compatibilityFlags.length > 0
825
+ ? { compatibilityFlags: resolved.compatibilityFlags }
826
+ : {}),
771
827
  }, { tos });
772
828
  const status = await pollSandboxStatus(result.statusUrl);
773
829
  if (jsonMode) {
@@ -899,7 +955,7 @@ function cleanupDir(dir) {
899
955
  // ============================================================================
900
956
  // Authenticated deploy — existing flow
901
957
  // ============================================================================
902
- async function deployAuthenticated(cwd, resolved, token, skipBuild, jsonMode = false) {
958
+ async function deployAuthenticated(cwd, resolved, token, skipBuild, jsonMode = false, noCache = false) {
903
959
  try {
904
960
  const client = new CreekClient(getApiUrl(), token);
905
961
  section("Auth");
@@ -946,6 +1002,14 @@ async function deployAuthenticated(cwd, resolved, token, skipBuild, jsonMode = f
946
1002
  if (!jsonMode)
947
1003
  consola.info(` Next.js mode: ${nextjsMode}${monorepo.isMonorepo ? " (monorepo)" : ""}`);
948
1004
  }
1005
+ // --- ⚡ Turbo deploy: check if server has a cached build for this commit ---
1006
+ // Read the local git HEAD SHA. If the working tree is clean and the
1007
+ // server has a cached bundle for this exact commit, skip the entire
1008
+ // local build + upload and let the server deploy from cache.
1009
+ const turboResult = await tryTurboDeploy(cwd, client, project, noCache, jsonMode);
1010
+ if (turboResult) {
1011
+ return; // ⚡ done — server deployed from cache
1012
+ }
949
1013
  // Build (skip for pure Workers with no build command)
950
1014
  if (!skipBuild && resolved.buildCommand) {
951
1015
  section("Build");
@@ -976,6 +1040,11 @@ async function deployAuthenticated(cwd, resolved, token, skipBuild, jsonMode = f
976
1040
  consola.success(" Build complete");
977
1041
  }
978
1042
  }
1043
+ // Post-build detection: Astro + @astrojs/cloudflare produces a
1044
+ // pre-bundled Worker (dist/server/entry.mjs) + split client assets
1045
+ // (dist/client/). We can only detect this after build — before
1046
+ // build `framework === "astro"` could mean SSG or CF-adapter-SSR.
1047
+ const astroCF = framework === "astro" ? detectAstroCloudflareBuild(cwd) : null;
979
1048
  // Collect client assets
980
1049
  // Worker + SPA hybrid: if a Worker project has buildOutput with built files, collect them too
981
1050
  let clientAssets = {};
@@ -988,6 +1057,11 @@ async function deployAuthenticated(cwd, resolved, token, skipBuild, jsonMode = f
988
1057
  if (framework === "nextjs" && hasAdapterOutput(cwd)) {
989
1058
  clientAssetsDir = resolve(cwd, ".creek/adapter-output/assets");
990
1059
  }
1060
+ else if (astroCF) {
1061
+ // Astro CF adapter splits its output: client assets live in
1062
+ // dist/client/ (not dist/), so redirect the collector there.
1063
+ clientAssetsDir = resolve(cwd, astroCF.assetsDir);
1064
+ }
991
1065
  else {
992
1066
  const outputDir = resolve(cwd, resolved.buildOutput);
993
1067
  if (!existsSync(outputDir)) {
@@ -1008,7 +1082,19 @@ async function deployAuthenticated(cwd, resolved, token, skipBuild, jsonMode = f
1008
1082
  }
1009
1083
  // Bundle server/worker files
1010
1084
  let serverFiles;
1011
- if (isSSR && framework) {
1085
+ if (astroCF) {
1086
+ // Astro CF adapter: upload dist/server/ as worker modules
1087
+ // (same shape Nuxt/SolidStart use — pre-bundled, do not re-bundle).
1088
+ const serverDir = resolve(cwd, astroCF.serverDir);
1089
+ if (existsSync(serverDir)) {
1090
+ consola.start(" Collecting Astro CF server files...");
1091
+ const collected = collectServerFiles(serverDir);
1092
+ const fileCount = Object.keys(collected).length;
1093
+ serverFiles = Object.fromEntries(Object.entries(collected).map(([p, buf]) => [p, buf.toString("base64")]));
1094
+ consola.success(` Astro CF worker: ${fileCount} files`);
1095
+ }
1096
+ }
1097
+ else if (isSSR && framework) {
1012
1098
  if (framework === "nextjs" && hasAdapterOutput(cwd)) {
1013
1099
  // Adapter path: read pre-bundled output from .creek/adapter-output/
1014
1100
  const adapterServerDir = resolve(cwd, ".creek/adapter-output/server");
@@ -1110,12 +1196,18 @@ async function deployAuthenticated(cwd, resolved, token, skipBuild, jsonMode = f
1110
1196
  consola.start(" Creating deployment...");
1111
1197
  const { deployment } = await client.createDeployment(project.id);
1112
1198
  consola.start(" Uploading bundle...");
1199
+ // If the Astro CF adapter fired post-build, the project is actually
1200
+ // SSR (not SPA): overwrite the pre-build-computed renderMode and
1201
+ // point the entrypoint at the adapter-emitted entry.mjs.
1202
+ const effectiveRenderMode = astroCF ? "ssr" : renderMode;
1203
+ const effectiveHasWorker = astroCF ? true : (isSSR || isWorker);
1204
+ const effectiveEntrypoint = astroCF ? "entry.mjs" : resolved.workerEntry;
1113
1205
  const bundle = {
1114
1206
  manifest: {
1115
1207
  assets: fileList,
1116
- hasWorker: isSSR || isWorker,
1117
- entrypoint: resolved.workerEntry,
1118
- renderMode,
1208
+ hasWorker: effectiveHasWorker,
1209
+ entrypoint: effectiveEntrypoint,
1210
+ renderMode: effectiveRenderMode,
1119
1211
  framework: framework ?? undefined,
1120
1212
  },
1121
1213
  workerScript: null,
@@ -1192,7 +1284,7 @@ async function deployAuthenticated(cwd, resolved, token, skipBuild, jsonMode = f
1192
1284
  }
1193
1285
  // Contextual next-step hints (non-JSON only)
1194
1286
  if (!jsonMode) {
1195
- printNextStepHint(renderMode, resolved);
1287
+ printNextStepHint(effectiveRenderMode, resolved);
1196
1288
  }
1197
1289
  return;
1198
1290
  }
@@ -1272,4 +1364,117 @@ export function patchBareNodeImports(code) {
1272
1364
  .replace(/from\s+["']([a-z_]+)["']/g, (match, mod) => NODE_BUILTINS.has(mod) ? match.replace(`"${mod}"`, `"node:${mod}"`).replace(`'${mod}'`, `'node:${mod}'`) : match)
1273
1365
  .replace(/require\(["']([a-z_]+)["']\)/g, (match, mod) => NODE_BUILTINS.has(mod) ? match.replace(`"${mod}"`, `"node:${mod}"`).replace(`'${mod}'`, `'node:${mod}'`) : match);
1274
1366
  }
1367
+ // --- ⚡ Turbo deploy ---
1368
+ /**
1369
+ * Attempt a Turbo deploy: read the local git HEAD SHA, send it to
1370
+ * the server with cacheCheck, and if the server has a cached bundle
1371
+ * for this exact commit, let it deploy from cache. Returns true if
1372
+ * Turbo succeeded (caller should return), false if caller should
1373
+ * proceed with normal build + upload.
1374
+ *
1375
+ * Graceful: any failure (no git, dirty tree, API error, cache miss)
1376
+ * silently returns false → normal deploy. Turbo is always opt-in bonus.
1377
+ */
1378
+ async function tryTurboDeploy(cwd, client, project, noCache, jsonMode) {
1379
+ if (noCache)
1380
+ return false;
1381
+ // 1. Read git HEAD SHA
1382
+ let sha;
1383
+ try {
1384
+ sha = execSync("git rev-parse HEAD", { cwd, encoding: "utf-8" }).trim();
1385
+ if (!sha || sha.length < 12)
1386
+ return false;
1387
+ }
1388
+ catch {
1389
+ return false; // not a git repo or git not installed
1390
+ }
1391
+ // 2. Check working tree is clean
1392
+ try {
1393
+ const dirty = execSync("git status --porcelain", { cwd, encoding: "utf-8" }).trim();
1394
+ if (dirty) {
1395
+ // Uncommitted changes → can't trust cache (source differs from commit)
1396
+ return false;
1397
+ }
1398
+ }
1399
+ catch {
1400
+ return false;
1401
+ }
1402
+ // 3. Detect branch
1403
+ let branch = "main";
1404
+ try {
1405
+ branch = execSync("git rev-parse --abbrev-ref HEAD", { cwd, encoding: "utf-8" }).trim();
1406
+ }
1407
+ catch {
1408
+ // default to main
1409
+ }
1410
+ // 4. Ask server to create deployment with cache check
1411
+ const shortSha = sha.slice(0, 12);
1412
+ if (!jsonMode) {
1413
+ consola.info(` Commit: ${shortSha} (${branch}, clean)`);
1414
+ consola.start(" \x1b[33m⚡\x1b[0m Checking build cache...");
1415
+ }
1416
+ try {
1417
+ const res = await client.createDeployment(project.id, {
1418
+ branch,
1419
+ commitSha: shortSha,
1420
+ });
1421
+ if (!res.cacheHit) {
1422
+ if (!jsonMode)
1423
+ consola.info(" \x1b[2mFirst-time build — building from source\x1b[0m");
1424
+ return false;
1425
+ }
1426
+ // ⚡ Turbo build — server is deploying from cache.
1427
+ if (!jsonMode) {
1428
+ consola.success(" \x1b[33m⚡\x1b[0m \x1b[1mTurbo build — ready\x1b[0m");
1429
+ }
1430
+ // Poll until deployment is active (same as normal deploy path)
1431
+ const POLL_INTERVAL = 1000;
1432
+ const POLL_TIMEOUT = 30_000; // Turbo should be fast — 30s max
1433
+ const TERMINAL = new Set(["active", "failed", "cancelled"]);
1434
+ let lastStatus = "";
1435
+ const start = Date.now();
1436
+ while (Date.now() - start < POLL_TIMEOUT) {
1437
+ const status = await client.getDeploymentStatus(project.id, res.deployment.id);
1438
+ const { status: s } = status.deployment;
1439
+ if (s !== lastStatus && !jsonMode) {
1440
+ if (s === "deploying")
1441
+ consola.start(" Deploying to edge...");
1442
+ lastStatus = s;
1443
+ }
1444
+ if (TERMINAL.has(s)) {
1445
+ if (s === "active") {
1446
+ const elapsed = ((Date.now() - start) / 1000).toFixed(1);
1447
+ if (jsonMode) {
1448
+ jsonOutput({
1449
+ ok: true,
1450
+ url: status.url ?? status.previewUrl,
1451
+ previewUrl: status.previewUrl,
1452
+ deploymentId: res.deployment.id,
1453
+ project: project.slug,
1454
+ mode: "production",
1455
+ turbo: true,
1456
+ elapsed: `${elapsed}s`,
1457
+ }, 0, []);
1458
+ }
1459
+ consola.success(` ⬡ Deployed! ${status.url ?? status.previewUrl}`);
1460
+ consola.log(`\n \x1b[33m⚡ Turbo deploy\x1b[0m ${elapsed}s\n`);
1461
+ return true;
1462
+ }
1463
+ // Failed — don't fall back to normal build (server already tried)
1464
+ if (!jsonMode)
1465
+ consola.error(` Deploy from cache failed: ${status.deployment.error_message || "unknown"}`);
1466
+ return true; // return true to prevent double-deploy attempt
1467
+ }
1468
+ await new Promise((r) => setTimeout(r, POLL_INTERVAL));
1469
+ }
1470
+ // Timeout — deploy might still complete, but CLI gives up waiting
1471
+ if (!jsonMode)
1472
+ consola.warn(" Cache deploy timed out — check `creek status`");
1473
+ return true;
1474
+ }
1475
+ catch {
1476
+ // API error — fall through to normal build
1477
+ return false;
1478
+ }
1479
+ }
1275
1480
  //# sourceMappingURL=deploy.js.map
@@ -34,6 +34,24 @@ export declare function sandboxDeploy(bundle: {
34
34
  framework?: string;
35
35
  templateId?: string;
36
36
  source: string;
37
+ /**
38
+ * Binding declarations from creek.toml / wrangler.*. Sandbox-api
39
+ * provisions ephemeral D1/R2/KV per entry so `env.DB` etc. work
40
+ * in the user's Worker without any auth or extra setup.
41
+ */
42
+ bindings?: Array<{
43
+ type: string;
44
+ bindingName: string;
45
+ }>;
46
+ /** Compat overrides — required for Node-API-heavy bundles. */
47
+ compatibilityDate?: string;
48
+ compatibilityFlags?: string[];
49
+ /** Framework-aware hint (admin URL, warnings) for the UI layer. */
50
+ hint?: {
51
+ adminPath?: string;
52
+ adminLabel?: string;
53
+ warnings?: string[];
54
+ };
37
55
  }, opts?: {
38
56
  tos?: TosAcceptance;
39
57
  agentToken?: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@solcreek/cli",
3
- "version": "0.4.6",
3
+ "version": "0.4.7",
4
4
  "description": "CLI for the Creek deployment platform",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",