@solcreek/cli 0.4.5 → 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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,35 @@
1
1
  # @solcreek/cli
2
2
 
3
+ ## 0.4.6
4
+
5
+ ### Features
6
+
7
+ - **`creek deploy gh:owner/repo` shorthand** — a shorter alias for
8
+ `https://github.com/owner/repo`. Matches the GitHub CLI convention.
9
+ `gh:`, `gl:` (GitLab), and `bb:` (Bitbucket) are now all registered
10
+ alongside the longer `github:` / `gitlab:` / `bitbucket:` forms.
11
+ Example: `npx creek deploy gh:jiseeeh/serene-ink`.
12
+
13
+ ### Fixes / DX
14
+
15
+ - **Install size cut from ~170MB to ~25MB** by moving `miniflare` out of
16
+ runtime dependencies entirely. Miniflare plus its transitive deps
17
+ (workerd, sharp) was adding ~146MB to every `npm install creek` — a
18
+ pure penalty on the `creek deploy` flow, which never touches
19
+ miniflare. `miniflare` is now listed in `devDependencies` only, so
20
+ the published package is free of it. First-time `npx creek deploy`
21
+ goes from ~20s install to ~3–5s, and the postinstall warnings that
22
+ used to appear on systems without build tools (sharp's node-gyp
23
+ fallback) are gone entirely.
24
+
25
+ - **`creek dev` loads miniflare from multiple locations** via
26
+ `createRequire` — the user's current project (`npm install --save-dev
27
+ miniflare`), a creek-managed cache directory at `~/.creek/deps`, or
28
+ the global npm root (`npm install -g miniflare`). If none of those
29
+ have it, a jargon-free error explains what to install and where.
30
+ Users who only want to deploy never see this error because
31
+ `creek deploy` doesn't load the local runtime at all.
32
+
3
33
  ## 0.4.5
4
34
 
5
35
  ### Features
@@ -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
@@ -1,4 +1,14 @@
1
1
  import type { BindingDeclaration } from "@solcreek/sdk";
2
+ /**
3
+ * Thrown by `creek dev` when no local runtime package can be resolved from
4
+ * the user's project, global npm root, or creek's dep cache. The message
5
+ * is deliberately jargon-free: it talks about "a local runtime" instead of
6
+ * naming miniflare in the explanation, and only mentions the package name
7
+ * as part of the exact npm command the user needs to run.
8
+ */
9
+ export declare class MiniflareNotInstalledError extends Error {
10
+ constructor();
11
+ }
2
12
  export interface WorkerRunnerOptions {
3
13
  /** User's worker entry point (e.g. "worker/index.ts"). */
4
14
  entryPoint: string;
@@ -1,11 +1,93 @@
1
- // Worker execution for `creek dev` — bundles user code and runs it via Miniflare.
1
+ // Worker execution for `creek dev` — bundles user code and runs it via
2
+ // Miniflare for local D1 / KV / R2 / Queues / Durable Objects simulation.
2
3
  //
3
- // Uses esbuild watch mode for hot reload and Miniflare for D1/KV/R2 simulation.
4
+ // `miniflare` is intentionally NOT in @solcreek/cli's runtime dependencies
5
+ // it (plus its transitive deps workerd and sharp) adds ~146MB to every
6
+ // install of `creek`. Since `creek deploy` never touches miniflare, it's
7
+ // a pure penalty on the aha-moment flow: paste a command, see a live URL.
8
+ //
9
+ // Instead, `creek dev` resolves miniflare at runtime from the caller's
10
+ // project (npm install --save-dev miniflare in their own package.json),
11
+ // the global npm root, or a creek-managed cache dir. If none of those
12
+ // have miniflare, we throw a zero-jargon error that tells the user
13
+ // exactly one command to run.
4
14
  import { writeFileSync, mkdirSync } from "node:fs";
5
15
  import { join, resolve } from "node:path";
16
+ import { createRequire } from "node:module";
17
+ import { homedir } from "node:os";
18
+ import { execSync } from "node:child_process";
6
19
  import { context } from "esbuild";
7
- import { Miniflare } from "miniflare";
8
20
  import { generateWorkerWrapper } from "../utils/worker-bundle.js";
21
+ /**
22
+ * Thrown by `creek dev` when no local runtime package can be resolved from
23
+ * the user's project, global npm root, or creek's dep cache. The message
24
+ * is deliberately jargon-free: it talks about "a local runtime" instead of
25
+ * naming miniflare in the explanation, and only mentions the package name
26
+ * as part of the exact npm command the user needs to run.
27
+ */
28
+ export class MiniflareNotInstalledError extends Error {
29
+ constructor() {
30
+ super([
31
+ "`creek dev` needs a local runtime to simulate the edge environment,",
32
+ "and it isn't installed yet.",
33
+ "",
34
+ " One-time setup — run this in your project directory:",
35
+ " npm install --save-dev miniflare",
36
+ "",
37
+ " This downloads ~150MB but only needs to happen once per project.",
38
+ " `creek deploy` already works without it — you can skip this step",
39
+ " entirely if you only want to deploy.",
40
+ ].join("\n"));
41
+ this.name = "MiniflareNotInstalledError";
42
+ }
43
+ }
44
+ /**
45
+ * Try to load miniflare from several plausible locations, in order of
46
+ * preference:
47
+ *
48
+ * 1. The user's current project (npm install --save-dev miniflare)
49
+ * 2. Creek-managed dep cache at ~/.creek/deps/miniflare (for future
50
+ * auto-install support — not wired up yet but already searched here
51
+ * so a future `creek dev install` command can drop files there)
52
+ * 3. Global npm root (for users who `npm install -g miniflare`)
53
+ *
54
+ * Each location is attempted with a fresh `createRequire` rooted at a
55
+ * dummy file in that directory, then `require.resolve("miniflare")` is
56
+ * used to find the package. If all three fail, throws
57
+ * MiniflareNotInstalledError with a zero-jargon explanation.
58
+ */
59
+ async function loadMiniflare() {
60
+ const searchRoots = [];
61
+ // 1. User's current project
62
+ searchRoots.push(process.cwd());
63
+ // 2. Creek-managed cache
64
+ searchRoots.push(join(homedir(), ".creek", "deps"));
65
+ // 3. Global npm root (best-effort — may be slow on first call)
66
+ try {
67
+ const globalRoot = execSync("npm root -g", {
68
+ encoding: "utf-8",
69
+ stdio: ["ignore", "pipe", "ignore"],
70
+ }).trim();
71
+ if (globalRoot)
72
+ searchRoots.push(globalRoot);
73
+ }
74
+ catch {
75
+ // No npm on PATH, or command failed — skip this search path silently.
76
+ }
77
+ for (const root of searchRoots) {
78
+ try {
79
+ // createRequire needs a file path — use a dummy file inside the root
80
+ // (the file doesn't have to exist for node_modules lookup to work).
81
+ const req = createRequire(join(root, "__creek_resolver__.js"));
82
+ const resolved = req.resolve("miniflare");
83
+ return (await import(resolved));
84
+ }
85
+ catch {
86
+ // Try the next root.
87
+ }
88
+ }
89
+ throw new MiniflareNotInstalledError();
90
+ }
9
91
  const NODE_BUILTINS = [
10
92
  "node:async_hooks",
11
93
  "node:stream",
@@ -52,9 +134,14 @@ export class WorkerRunner {
52
134
  writeFileSync(wrapperPath, wrapper);
53
135
  // 2. Initial bundle with esbuild
54
136
  const bundledScript = await this.bundle(wrapperPath);
55
- // 3. Start Miniflare
137
+ // 3. Start Miniflare — loaded from the user's project / cache / global
138
+ // so `creek deploy` can ship without miniflare in @solcreek/cli's
139
+ // runtime dependencies. See loadMiniflare() above for the search
140
+ // order and the MiniflareNotInstalledError message.
141
+ const miniflareModule = await loadMiniflare();
142
+ const MiniflareCtor = miniflareModule.Miniflare;
56
143
  this.mfOptions = this.buildMiniflareOptions(bundledScript, persistDir);
57
- this.mf = new Miniflare(this.mfOptions);
144
+ this.mf = new MiniflareCtor(this.mfOptions);
58
145
  // Wait for Miniflare to be ready and get the port
59
146
  const readyUrl = await this.mf.ready;
60
147
  // mf.ready returns a URL object
@@ -28,8 +28,11 @@ export declare function isRepoUrl(input: string): boolean;
28
28
  * https://github.com/owner/repo.git
29
29
  * https://github.com/owner/repo#branch
30
30
  * https://github.com/owner/repo/tree/branch
31
- * github:owner/repo
32
- * github:owner/repo#branch
31
+ * github:owner/repo (long-form shorthand)
32
+ * gh:owner/repo (short alias — matches gh CLI convention)
33
+ * gh:owner/repo#branch
34
+ * gitlab:owner/repo gl:owner/repo
35
+ * bitbucket:owner/repo bb:owner/repo
33
36
  */
34
37
  export declare function parseRepoUrl(input: string): ParsedRepoUrl;
35
38
  /**
@@ -17,10 +17,17 @@ const PROVIDER_HOSTS = {
17
17
  "bitbucket.org": "bitbucket",
18
18
  "www.bitbucket.org": "bitbucket",
19
19
  };
20
+ // Shorthand prefixes. Short aliases (gh:/gl:/bb:) match the conventions
21
+ // established by the GitHub CLI, GitLab CLI, and other tooling — they're
22
+ // the shortest safe way to reference a repo without colliding with local
23
+ // path syntax (local paths never have ':' before any '/').
20
24
  const SHORTHAND_PREFIXES = {
21
25
  "github:": "github",
26
+ "gh:": "github",
22
27
  "gitlab:": "gitlab",
28
+ "gl:": "gitlab",
23
29
  "bitbucket:": "bitbucket",
30
+ "bb:": "bitbucket",
24
31
  };
25
32
  // Strict character set for owner and repo names
26
33
  const SAFE_NAME = /^[a-zA-Z0-9._-]+$/;
@@ -53,8 +60,11 @@ export function isRepoUrl(input) {
53
60
  * https://github.com/owner/repo.git
54
61
  * https://github.com/owner/repo#branch
55
62
  * https://github.com/owner/repo/tree/branch
56
- * github:owner/repo
57
- * github:owner/repo#branch
63
+ * github:owner/repo (long-form shorthand)
64
+ * gh:owner/repo (short alias — matches gh CLI convention)
65
+ * gh:owner/repo#branch
66
+ * gitlab:owner/repo gl:owner/repo
67
+ * bitbucket:owner/repo bb:owner/repo
58
68
  */
59
69
  export function parseRepoUrl(input) {
60
70
  if (!input || typeof input !== "string") {
@@ -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.5",
3
+ "version": "0.4.7",
4
4
  "description": "CLI for the Creek deployment platform",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -31,7 +31,6 @@
31
31
  "citty": "^0.1.6",
32
32
  "consola": "^3.4.2",
33
33
  "esbuild": "^0.25.0",
34
- "miniflare": "^4.20260317.3",
35
34
  "smol-toml": "^1.3.1",
36
35
  "ws": "^8.20.0",
37
36
  "@solcreek/sdk": "0.4.2"
@@ -44,6 +43,7 @@
44
43
  "@types/ws": "^8.18.1",
45
44
  "hono": "^4.7.4",
46
45
  "jsdom": "^29.0.1",
46
+ "miniflare": "^4.20260317.3",
47
47
  "react": "^19.2.4",
48
48
  "react-dom": "^19.2.4",
49
49
  "typescript": "^5.8.2",