@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 +30 -0
- package/dist/commands/deploy.d.ts +5 -0
- package/dist/commands/deploy.js +227 -22
- package/dist/dev/worker-runner.d.ts +10 -0
- package/dist/dev/worker-runner.js +92 -5
- package/dist/utils/repo-url.d.ts +5 -2
- package/dist/utils/repo-url.js +12 -2
- package/dist/utils/sandbox.d.ts +18 -0
- package/package.json +2 -2
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;
|
package/dist/commands/deploy.js
CHANGED
|
@@ -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
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
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
|
-
|
|
746
|
-
|
|
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 (
|
|
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:
|
|
1117
|
-
entrypoint:
|
|
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(
|
|
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
|
|
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
|
-
//
|
|
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
|
|
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
|
package/dist/utils/repo-url.d.ts
CHANGED
|
@@ -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
|
-
*
|
|
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
|
/**
|
package/dist/utils/repo-url.js
CHANGED
|
@@ -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
|
-
*
|
|
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") {
|
package/dist/utils/sandbox.d.ts
CHANGED
|
@@ -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.
|
|
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",
|