@solcreek/cli 0.4.21 → 0.4.22

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 (40) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/dist/commands/dashboard.d.ts +21 -0
  3. package/dist/commands/dashboard.js +72 -0
  4. package/dist/commands/deploy.d.ts +10 -0
  5. package/dist/commands/deploy.js +252 -0
  6. package/dist/commands/dev.d.ts +13 -0
  7. package/dist/commands/dev.js +77 -2
  8. package/dist/commands/init.d.ts +10 -0
  9. package/dist/commands/init.js +158 -2
  10. package/dist/commands/logs.d.ts +12 -0
  11. package/dist/commands/logs.js +69 -1
  12. package/dist/commands/restart.d.ts +26 -0
  13. package/dist/commands/restart.js +55 -0
  14. package/dist/commands/rollback.d.ts +13 -0
  15. package/dist/commands/rollback.js +188 -1
  16. package/dist/commands/stop.d.ts +26 -0
  17. package/dist/commands/stop.js +65 -0
  18. package/dist/commands/top.d.ts +28 -0
  19. package/dist/commands/top.js +171 -0
  20. package/dist/dev/creekd-runner.d.ts +22 -0
  21. package/dist/dev/creekd-runner.js +188 -0
  22. package/dist/index.js +8 -0
  23. package/dist/utils/creekd-client.d.ts +152 -0
  24. package/dist/utils/creekd-client.js +144 -0
  25. package/dist/utils/gitignore.d.ts +2 -0
  26. package/dist/utils/gitignore.js +32 -0
  27. package/dist/utils/hostkey.d.ts +39 -0
  28. package/dist/utils/hostkey.js +84 -0
  29. package/dist/utils/hosts.d.ts +70 -0
  30. package/dist/utils/hosts.js +90 -0
  31. package/dist/utils/local-cache.d.ts +69 -0
  32. package/dist/utils/local-cache.js +100 -0
  33. package/dist/utils/nextjs.d.ts +4 -2
  34. package/dist/utils/nextjs.js +107 -38
  35. package/dist/utils/prepare-bundle.js +1 -1
  36. package/dist/utils/top-format.d.ts +4 -0
  37. package/dist/utils/top-format.js +32 -0
  38. package/dist/utils/watch.d.ts +81 -0
  39. package/dist/utils/watch.js +87 -0
  40. package/package.json +2 -2
@@ -40,27 +40,38 @@ function semverGte(version, target) {
40
40
  return aPat >= bPat;
41
41
  }
42
42
  /**
43
- * Build a Next.js app using the Creek adapter (>= 16.2.3).
43
+ * Resolve @solcreek/adapter-creek from any reachable location.
44
44
  *
45
- * Sets NEXT_ADAPTER_PATH to the adapter bundled with the CLI.
46
- * No opennext, no wrangler, no config patching the adapter handles
47
- * everything inside onBuildComplete().
45
+ * Tries, in order: the CLI's own install (monorepo workspace / global
46
+ * install alongside the adapter), the project's own node_modules, then the
47
+ * lazy-installed copy under .creek/node_modules. Returns the adapter entry
48
+ * path (for NEXT_ADAPTER_PATH), or null if not installed anywhere.
48
49
  */
49
- function resolveAdapterPath() {
50
- try {
51
- const require = createRequire(import.meta.url);
52
- return require.resolve("@solcreek/adapter-creek");
50
+ function resolveAdapterPath(cwd) {
51
+ const bases = [import.meta.url];
52
+ if (cwd) {
53
+ // createRequire walks node_modules up from the base file's directory;
54
+ // the base file itself need not exist.
55
+ bases.push(join(cwd, "package.json"));
56
+ bases.push(join(cwd, CREEK_DIR, "package.json"));
53
57
  }
54
- catch {
55
- return null;
58
+ for (const base of bases) {
59
+ try {
60
+ return createRequire(base).resolve("@solcreek/adapter-creek");
61
+ }
62
+ catch {
63
+ // try next base
64
+ }
56
65
  }
66
+ return null;
57
67
  }
58
- function buildWithAdapter(cwd) {
59
- const adapterPath = resolveAdapterPath();
60
- if (!adapterPath) {
61
- consola.warn(" @solcreek/adapter-creek not found install it for optimal Next.js builds");
62
- return; // caller falls back to legacy
63
- }
68
+ /**
69
+ * Build a Next.js app using the Creek adapter (>= 16.2.3).
70
+ *
71
+ * Sets NEXT_ADAPTER_PATH to the resolved adapter. No opennext, no wrangler,
72
+ * no config patching the adapter handles everything inside onBuildComplete().
73
+ */
74
+ function buildWithAdapter(cwd, adapterPath) {
64
75
  consola.start(" Building Next.js with Creek adapter...\n");
65
76
  // --webpack is required: Turbopack does not generate standalone output,
66
77
  // and its chunked format uses a custom runtime incompatible with esbuild.
@@ -73,24 +84,28 @@ function buildWithAdapter(cwd) {
73
84
  /**
74
85
  * Unified Next.js build entry point.
75
86
  *
76
- * - Next.js >= 16.2.3: Creek adapter path (recommended)
77
- * - Next.js < 16.2.3: legacy opennext path (best effort)
87
+ * - Next.js >= 16.2.3: Creek adapter path (recommended). The adapter is
88
+ * lazily installed into .creek/node_modules on first use the CLI never
89
+ * depends on it directly, so non-Next.js users never pay for it.
90
+ * - Next.js < 16.2.3 (or adapter install fails): legacy opennext path.
78
91
  *
79
92
  * Min version for the adapter path matches @solcreek/adapter-creek's
80
93
  * peerDependency, which pins Next.js >= 16.2.3 to fix CVE-2026-23869.
81
94
  */
82
95
  export function buildNextjs(cwd, isMonorepo, projectName) {
83
96
  const version = getNextVersion(cwd);
84
- const useAdapter = version && semverGte(version, "16.2.3") && resolveAdapterPath();
85
- if (useAdapter) {
86
- buildWithAdapter(cwd);
87
- }
88
- else {
89
- if (version) {
90
- consola.warn(` Next.js ${version} — using legacy build path`);
97
+ if (version && semverGte(version, "16.2.3")) {
98
+ const adapterPath = ensureAdapter(cwd);
99
+ if (adapterPath) {
100
+ buildWithAdapter(cwd, adapterPath);
101
+ return;
91
102
  }
92
- buildNextjsForWorkers(cwd, isMonorepo, projectName);
103
+ consola.warn(` Falling back to legacy build path for Next.js ${version}`);
93
104
  }
105
+ else if (version) {
106
+ consola.warn(` Next.js ${version} — using legacy build path`);
107
+ }
108
+ buildNextjsForWorkers(cwd, isMonorepo, projectName);
94
109
  }
95
110
  /** Check if the adapter output exists (vs legacy opennext output). */
96
111
  export function hasAdapterOutput(cwd) {
@@ -132,6 +147,71 @@ export function patchBundledWorker(bundleDir, openNextDir) {
132
147
  const CREEK_DIR = ".creek";
133
148
  const OPENNEXT_PKG = "@opennextjs/cloudflare";
134
149
  const OPENNEXT_VERSION = "^1.18.0";
150
+ const ADAPTER_PKG = "@solcreek/adapter-creek";
151
+ const ADAPTER_VERSION = "^0.2.0";
152
+ /**
153
+ * Merge a dependency into .creek/package.json without clobbering deps that
154
+ * a previous install (adapter or opennext) may have already written.
155
+ */
156
+ function upsertCreekDep(creekDir, pkg, version) {
157
+ const pkgPath = join(creekDir, "package.json");
158
+ let manifest = {
159
+ private: true,
160
+ dependencies: {},
161
+ };
162
+ if (existsSync(pkgPath)) {
163
+ try {
164
+ manifest = JSON.parse(readFileSync(pkgPath, "utf-8"));
165
+ manifest.dependencies ??= {};
166
+ }
167
+ catch {
168
+ manifest = { private: true, dependencies: {} };
169
+ }
170
+ }
171
+ manifest.dependencies[pkg] = version;
172
+ writeFileSync(pkgPath, JSON.stringify(manifest, null, 2));
173
+ }
174
+ /**
175
+ * Install a package into .creek/node_modules. Returns false if npm fails.
176
+ */
177
+ function installCreekDep(creekDir, pkg, version) {
178
+ mkdirSync(creekDir, { recursive: true });
179
+ upsertCreekDep(creekDir, pkg, version);
180
+ try {
181
+ execSync("npm install --no-audit --no-fund --ignore-scripts --no-optional", {
182
+ cwd: creekDir,
183
+ stdio: "pipe",
184
+ });
185
+ return true;
186
+ }
187
+ catch {
188
+ return false;
189
+ }
190
+ }
191
+ /**
192
+ * Ensure @solcreek/adapter-creek is resolvable, lazily installing it into
193
+ * .creek/node_modules on demand. Returns the resolved adapter entry path
194
+ * (for NEXT_ADAPTER_PATH), or null if it could not be made available.
195
+ *
196
+ * Lazy by design: the CLI stays framework-neutral, so the adapter — a
197
+ * Next.js-specific package with its own Next peerDependency — is only
198
+ * fetched when a Next.js project is actually deployed. It is never a hard
199
+ * CLI dependency that every `npx creek` user would pay for.
200
+ */
201
+ function ensureAdapter(cwd) {
202
+ const existing = resolveAdapterPath(cwd);
203
+ if (existing)
204
+ return existing;
205
+ consola.start(` Installing ${ADAPTER_PKG} (one-time setup)...`);
206
+ if (!installCreekDep(join(cwd, CREEK_DIR), ADAPTER_PKG, ADAPTER_VERSION)) {
207
+ consola.warn(` Could not install ${ADAPTER_PKG}`);
208
+ return null;
209
+ }
210
+ const resolved = resolveAdapterPath(cwd);
211
+ if (resolved)
212
+ consola.success(` ${ADAPTER_PKG} installed`);
213
+ return resolved;
214
+ }
135
215
  /**
136
216
  * Ensure @opennextjs/cloudflare is available in .creek/node_modules.
137
217
  * Returns the path to the opennextjs-cloudflare CLI binary.
@@ -142,18 +222,7 @@ function ensureOpenNext(cwd) {
142
222
  if (existsSync(opennextBin))
143
223
  return opennextBin;
144
224
  consola.start(` Installing ${OPENNEXT_PKG} (one-time setup)...`);
145
- mkdirSync(creekDir, { recursive: true });
146
- const pkgPath = join(creekDir, "package.json");
147
- if (!existsSync(pkgPath)) {
148
- writeFileSync(pkgPath, JSON.stringify({
149
- private: true,
150
- dependencies: { [OPENNEXT_PKG]: OPENNEXT_VERSION },
151
- }, null, 2));
152
- }
153
- execSync("npm install --no-audit --no-fund --ignore-scripts --no-optional", {
154
- cwd: creekDir,
155
- stdio: "pipe",
156
- });
225
+ installCreekDep(creekDir, OPENNEXT_PKG, OPENNEXT_VERSION);
157
226
  consola.success(` ${OPENNEXT_PKG} installed`);
158
227
  return opennextBin;
159
228
  }
@@ -82,7 +82,7 @@ export async function prepareDeployBundle(input) {
82
82
  const useAdapterOutput = framework === "nextjs" && hasAdapterOutput(cwd);
83
83
  const outputDir = useAdapterOutput
84
84
  ? resolve(cwd, ".creek/adapter-output")
85
- : resolve(cwd, resolved.buildOutput || getDefaultBuildOutput(framework));
85
+ : resolve(cwd, resolved.buildOutput || getDefaultBuildOutput(framework, cwd));
86
86
  // 4. Post-build framework adapter detection. Astro can be either SSG
87
87
  // or CF-adapter-SSR; we only know which after build.
88
88
  const astroAdapter = framework === "astro" ? detectAstroCloudflareBuild(cwd) : null;
@@ -0,0 +1,4 @@
1
+ export declare function fmtBytes(bytes: number): string;
2
+ export declare function fmtDuration(ms: number): string;
3
+ export declare function calcCpuPercent(prevUsec: number, prevTs: number, currUsec: number, currTs: number): number | null;
4
+ //# sourceMappingURL=top-format.d.ts.map
@@ -0,0 +1,32 @@
1
+ export function fmtBytes(bytes) {
2
+ if (bytes < 1024)
3
+ return bytes + "B";
4
+ if (bytes < 1024 * 1024)
5
+ return (bytes / 1024).toFixed(1) + "K";
6
+ if (bytes < 1024 * 1024 * 1024)
7
+ return (bytes / (1024 * 1024)).toFixed(1) + "M";
8
+ return (bytes / (1024 * 1024 * 1024)).toFixed(1) + "G";
9
+ }
10
+ export function fmtDuration(ms) {
11
+ if (ms < 1000)
12
+ return "0s";
13
+ const s = Math.floor(ms / 1000);
14
+ if (s < 60)
15
+ return s + "s";
16
+ const m = Math.floor(s / 60);
17
+ if (m < 60)
18
+ return m + "m" + (s % 60 > 0 ? (s % 60) + "s" : "");
19
+ const h = Math.floor(m / 60);
20
+ if (h < 24)
21
+ return h + "h" + (m % 60 > 0 ? (m % 60) + "m" : "");
22
+ const d = Math.floor(h / 24);
23
+ return d + "d" + (h % 24 > 0 ? (h % 24) + "h" : "");
24
+ }
25
+ export function calcCpuPercent(prevUsec, prevTs, currUsec, currTs) {
26
+ const dtMs = currTs - prevTs;
27
+ if (dtMs <= 0)
28
+ return null;
29
+ const dtUs = currUsec - prevUsec;
30
+ return (dtUs / 1000 / dtMs) * 100;
31
+ }
32
+ //# sourceMappingURL=top-format.js.map
@@ -0,0 +1,81 @@
1
+ /**
2
+ * Poll `GET /v1/apps/{id}` until the app reaches a terminal
3
+ * deploy state: Ready=True + Progressing=False (success), or
4
+ * Degraded=True reason=DeployTimeout (the daemon's own
5
+ * progressing_timeout has flipped — surfaces here as
6
+ * deploy_stuck per DESIGN-self-host-state.md §"progressing_timeout
7
+ * uses monotonic clock"), or the client-side watch budget runs
8
+ * out.
9
+ *
10
+ * This is the consumer of #10's observedGeneration / conditions
11
+ * machinery and #8a's status.conditions[] surface. The whole
12
+ * point of those server-side primitives is exactly THIS loop:
13
+ * a watcher polling GET, inspecting conditions[], deciding
14
+ * "still progressing vs converged vs stuck" without needing a
15
+ * separate event stream.
16
+ *
17
+ * `watchDeploy` is pure (modulo fetch + setTimeout) — the
18
+ * test harness drives it through synthetic state transitions
19
+ * by sequencing mock responses.
20
+ */
21
+ import type { CreekdClient, AppEnvelope } from "./creekd-client.js";
22
+ /** Terminal outcome of a watch loop. */
23
+ export type WatchResult = {
24
+ ok: true;
25
+ reason: "ready";
26
+ envelope: AppEnvelope;
27
+ } | {
28
+ ok: false;
29
+ reason: "deploy_stuck";
30
+ envelope: AppEnvelope;
31
+ } | {
32
+ ok: false;
33
+ reason: "watch_timeout";
34
+ elapsedMs: number;
35
+ lastEnvelope?: AppEnvelope;
36
+ } | {
37
+ ok: false;
38
+ reason: "fetch_failed";
39
+ error: Error;
40
+ };
41
+ export interface WatchOptions {
42
+ /** Milliseconds between polls. Default 1000. Clamped to >=100. */
43
+ pollIntervalMs?: number;
44
+ /**
45
+ * Maximum total wall time the watch will hang before returning
46
+ * watch_timeout. Default 5 min. Independent of the daemon's
47
+ * progressing_timeout — the client's bound is "user's patience"
48
+ * not "server's deploy budget"; the two normally agree.
49
+ */
50
+ timeoutMs?: number;
51
+ /**
52
+ * Injected clock + sleep for tests. Production callers omit; the
53
+ * defaults are Date.now + setTimeout-based delay.
54
+ */
55
+ now?: () => number;
56
+ sleep?: (ms: number) => Promise<void>;
57
+ /**
58
+ * Optional progress callback invoked after each poll. Tests use
59
+ * this to assert intermediate state; production callers can
60
+ * stream it to a spinner.
61
+ */
62
+ onPoll?: (envelope: AppEnvelope, elapsedMs: number) => void;
63
+ }
64
+ export declare function watchDeploy(client: CreekdClient, appId: string, opts?: WatchOptions): Promise<WatchResult>;
65
+ type Verdict = "ready" | "deploy_stuck" | "progressing" | "unknown";
66
+ /**
67
+ * Inspect a single envelope's status.conditions[] and decide
68
+ * whether the watch loop is done.
69
+ *
70
+ * Ready=True AND Progressing=False → ready (success)
71
+ * Degraded=True AND reason=DeployTimeout → deploy_stuck (DESIGN code)
72
+ * Progressing=True (any reason) → progressing (keep polling)
73
+ * anything else → unknown (keep polling — server
74
+ * hasn't classified yet)
75
+ *
76
+ * Exported for unit-test visibility; not part of the consumer
77
+ * API.
78
+ */
79
+ export declare function classifyConditions(envelope: AppEnvelope): Verdict;
80
+ export {};
81
+ //# sourceMappingURL=watch.d.ts.map
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Poll `GET /v1/apps/{id}` until the app reaches a terminal
3
+ * deploy state: Ready=True + Progressing=False (success), or
4
+ * Degraded=True reason=DeployTimeout (the daemon's own
5
+ * progressing_timeout has flipped — surfaces here as
6
+ * deploy_stuck per DESIGN-self-host-state.md §"progressing_timeout
7
+ * uses monotonic clock"), or the client-side watch budget runs
8
+ * out.
9
+ *
10
+ * This is the consumer of #10's observedGeneration / conditions
11
+ * machinery and #8a's status.conditions[] surface. The whole
12
+ * point of those server-side primitives is exactly THIS loop:
13
+ * a watcher polling GET, inspecting conditions[], deciding
14
+ * "still progressing vs converged vs stuck" without needing a
15
+ * separate event stream.
16
+ *
17
+ * `watchDeploy` is pure (modulo fetch + setTimeout) — the
18
+ * test harness drives it through synthetic state transitions
19
+ * by sequencing mock responses.
20
+ */
21
+ const DEFAULT_POLL_MS = 1000;
22
+ const DEFAULT_TIMEOUT_MS = 5 * 60 * 1000;
23
+ const MIN_POLL_MS = 100;
24
+ export async function watchDeploy(client, appId, opts = {}) {
25
+ const pollMs = Math.max(MIN_POLL_MS, opts.pollIntervalMs ?? DEFAULT_POLL_MS);
26
+ const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
27
+ const now = opts.now ?? Date.now;
28
+ const sleep = opts.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
29
+ const start = now();
30
+ let lastEnvelope;
31
+ while (true) {
32
+ const elapsedMs = now() - start;
33
+ if (elapsedMs > timeoutMs) {
34
+ return { ok: false, reason: "watch_timeout", elapsedMs, lastEnvelope };
35
+ }
36
+ let envelope;
37
+ try {
38
+ envelope = await client.getApp(appId);
39
+ }
40
+ catch (e) {
41
+ return { ok: false, reason: "fetch_failed", error: e instanceof Error ? e : new Error(String(e)) };
42
+ }
43
+ lastEnvelope = envelope;
44
+ opts.onPoll?.(envelope, elapsedMs);
45
+ const verdict = classifyConditions(envelope);
46
+ if (verdict === "ready")
47
+ return { ok: true, reason: "ready", envelope };
48
+ if (verdict === "deploy_stuck")
49
+ return { ok: false, reason: "deploy_stuck", envelope };
50
+ // "progressing" or "unknown" → keep polling.
51
+ await sleep(pollMs);
52
+ }
53
+ }
54
+ /**
55
+ * Inspect a single envelope's status.conditions[] and decide
56
+ * whether the watch loop is done.
57
+ *
58
+ * Ready=True AND Progressing=False → ready (success)
59
+ * Degraded=True AND reason=DeployTimeout → deploy_stuck (DESIGN code)
60
+ * Progressing=True (any reason) → progressing (keep polling)
61
+ * anything else → unknown (keep polling — server
62
+ * hasn't classified yet)
63
+ *
64
+ * Exported for unit-test visibility; not part of the consumer
65
+ * API.
66
+ */
67
+ export function classifyConditions(envelope) {
68
+ const conds = envelope.status?.conditions ?? [];
69
+ const ready = conds.find((c) => c.type === "Ready");
70
+ const progressing = conds.find((c) => c.type === "Progressing");
71
+ const degraded = conds.find((c) => c.type === "Degraded");
72
+ // deploy_stuck has highest priority — even if Ready=True somehow,
73
+ // a DeployTimeout-flagged Degraded means the daemon gave up on
74
+ // this generation's convergence and the client should report
75
+ // failure rather than racing the wire.
76
+ if (degraded?.status === "True" && degraded.reason === "DeployTimeout") {
77
+ return "deploy_stuck";
78
+ }
79
+ if (ready?.status === "True" && progressing?.status === "False") {
80
+ return "ready";
81
+ }
82
+ if (progressing?.status === "True") {
83
+ return "progressing";
84
+ }
85
+ return "unknown";
86
+ }
87
+ //# sourceMappingURL=watch.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@solcreek/cli",
3
- "version": "0.4.21",
3
+ "version": "0.4.22",
4
4
  "description": "CLI for the Creek deployment platform",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -33,7 +33,7 @@
33
33
  "esbuild": "^0.25.0",
34
34
  "smol-toml": "^1.3.1",
35
35
  "ws": "^8.20.0",
36
- "@solcreek/sdk": "0.4.9"
36
+ "@solcreek/sdk": "0.4.10"
37
37
  },
38
38
  "devDependencies": {
39
39
  "@testing-library/dom": "^10.4.1",