@solcreek/cli 0.4.20 → 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.
- package/CHANGELOG.md +21 -0
- package/dist/commands/dashboard.d.ts +21 -0
- package/dist/commands/dashboard.js +72 -0
- package/dist/commands/deploy.d.ts +10 -0
- package/dist/commands/deploy.js +252 -0
- package/dist/commands/dev.d.ts +13 -0
- package/dist/commands/dev.js +77 -2
- package/dist/commands/init.d.ts +10 -0
- package/dist/commands/init.js +158 -2
- package/dist/commands/login.js +1 -1
- package/dist/commands/logs.d.ts +12 -0
- package/dist/commands/logs.js +69 -1
- package/dist/commands/restart.d.ts +26 -0
- package/dist/commands/restart.js +55 -0
- package/dist/commands/rollback.d.ts +13 -0
- package/dist/commands/rollback.js +188 -1
- package/dist/commands/stop.d.ts +26 -0
- package/dist/commands/stop.js +65 -0
- package/dist/commands/top.d.ts +28 -0
- package/dist/commands/top.js +171 -0
- package/dist/dev/creekd-runner.d.ts +22 -0
- package/dist/dev/creekd-runner.js +188 -0
- package/dist/index.js +8 -0
- package/dist/utils/auth-server.d.ts +2 -2
- package/dist/utils/auth-server.js +16 -3
- package/dist/utils/creekd-client.d.ts +152 -0
- package/dist/utils/creekd-client.js +144 -0
- package/dist/utils/gitignore.d.ts +2 -0
- package/dist/utils/gitignore.js +32 -0
- package/dist/utils/hostkey.d.ts +39 -0
- package/dist/utils/hostkey.js +84 -0
- package/dist/utils/hosts.d.ts +70 -0
- package/dist/utils/hosts.js +90 -0
- package/dist/utils/local-cache.d.ts +69 -0
- package/dist/utils/local-cache.js +100 -0
- package/dist/utils/nextjs.d.ts +4 -2
- package/dist/utils/nextjs.js +107 -38
- package/dist/utils/prepare-bundle.js +1 -1
- package/dist/utils/top-format.d.ts +4 -0
- package/dist/utils/top-format.js +32 -0
- package/dist/utils/watch.d.ts +81 -0
- package/dist/utils/watch.js +87 -0
- package/package.json +2 -2
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Project-local cache of the laptop's view of creekd's state.
|
|
3
|
+
*
|
|
4
|
+
* Lives at `<project>/.creek/local.json` and is the canonical
|
|
5
|
+
* source for the `If-Match` header per DESIGN-self-host-state.md
|
|
6
|
+
* §"First-party CLI MUST send If-Match" (line 230-232):
|
|
7
|
+
*
|
|
8
|
+
* "The creek CLI ... MUST send If-Match on every mutating call,
|
|
9
|
+
* sourced from .creek/local.json.lastDeploy.resourceVersion."
|
|
10
|
+
*
|
|
11
|
+
* Schema is intentionally narrow in 0.0.x:
|
|
12
|
+
* {
|
|
13
|
+
* "schemaVersion": 1,
|
|
14
|
+
* "lastDeploy": {
|
|
15
|
+
* "appId": "...",
|
|
16
|
+
* "host": "...", // hosts.json name; "" for default loopback
|
|
17
|
+
* "resourceVersion": "<opaque>",
|
|
18
|
+
* "generation": <int>,
|
|
19
|
+
* "at": "RFC3339"
|
|
20
|
+
* }
|
|
21
|
+
* }
|
|
22
|
+
*
|
|
23
|
+
* Writes are atomic (tmp + rename) to match the hosts.json
|
|
24
|
+
* convention; corrupt-then-crash leaves the file untouched.
|
|
25
|
+
*
|
|
26
|
+
* NOTE: this is per-project state, NOT per-host. Multi-host
|
|
27
|
+
* deploys of the same project use a single lastDeploy slot —
|
|
28
|
+
* switching --host between deploys invalidates the cache and
|
|
29
|
+
* triggers a fresh GET to re-seed. This is intentional: the cache
|
|
30
|
+
* is a hot path optimisation, not authoritative — the daemon's
|
|
31
|
+
* current rv is always the truth.
|
|
32
|
+
*/
|
|
33
|
+
export declare const LOCAL_SCHEMA_VERSION = 1;
|
|
34
|
+
/** Snapshot of the last successful spec-mutation against creekd. */
|
|
35
|
+
export interface LastDeploy {
|
|
36
|
+
appId: string;
|
|
37
|
+
/** hosts.json `name` if any was used; empty string for default localhost. */
|
|
38
|
+
host: string;
|
|
39
|
+
/** Opaque rv string from the wire — clients MUST NOT do arithmetic on it. */
|
|
40
|
+
resourceVersion: string;
|
|
41
|
+
/** Generation as of last successful deploy (sanity check). */
|
|
42
|
+
generation: number;
|
|
43
|
+
/** RFC3339 timestamp of the write. */
|
|
44
|
+
at: string;
|
|
45
|
+
}
|
|
46
|
+
export interface LocalCacheFile {
|
|
47
|
+
schemaVersion: number;
|
|
48
|
+
lastDeploy?: LastDeploy;
|
|
49
|
+
}
|
|
50
|
+
/** Resolve <project>/.creek/local.json. */
|
|
51
|
+
export declare function localCachePath(projectRoot: string): string;
|
|
52
|
+
/** Read the cache. Returns an empty file shape if absent. */
|
|
53
|
+
export declare function readLocalCache(projectRoot: string): LocalCacheFile;
|
|
54
|
+
/** Atomic write — tmp + rename. */
|
|
55
|
+
export declare function writeLocalCache(projectRoot: string, file: LocalCacheFile): void;
|
|
56
|
+
/**
|
|
57
|
+
* Update lastDeploy after a successful spec mutation. Reads the
|
|
58
|
+
* existing file, replaces lastDeploy, writes back. Convenience
|
|
59
|
+
* wrapper for the most common write pattern.
|
|
60
|
+
*/
|
|
61
|
+
export declare function recordLastDeploy(projectRoot: string, lastDeploy: LastDeploy): void;
|
|
62
|
+
/**
|
|
63
|
+
* Read the cached rv for an If-Match header. Returns undefined
|
|
64
|
+
* when the cache is empty, when the appId/host don't match
|
|
65
|
+
* (different project or host than last deploy), or when the cache
|
|
66
|
+
* is corrupt. Callers fall back to a fresh GET on undefined.
|
|
67
|
+
*/
|
|
68
|
+
export declare function cachedResourceVersion(projectRoot: string, appId: string, host: string): string | undefined;
|
|
69
|
+
//# sourceMappingURL=local-cache.d.ts.map
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Project-local cache of the laptop's view of creekd's state.
|
|
3
|
+
*
|
|
4
|
+
* Lives at `<project>/.creek/local.json` and is the canonical
|
|
5
|
+
* source for the `If-Match` header per DESIGN-self-host-state.md
|
|
6
|
+
* §"First-party CLI MUST send If-Match" (line 230-232):
|
|
7
|
+
*
|
|
8
|
+
* "The creek CLI ... MUST send If-Match on every mutating call,
|
|
9
|
+
* sourced from .creek/local.json.lastDeploy.resourceVersion."
|
|
10
|
+
*
|
|
11
|
+
* Schema is intentionally narrow in 0.0.x:
|
|
12
|
+
* {
|
|
13
|
+
* "schemaVersion": 1,
|
|
14
|
+
* "lastDeploy": {
|
|
15
|
+
* "appId": "...",
|
|
16
|
+
* "host": "...", // hosts.json name; "" for default loopback
|
|
17
|
+
* "resourceVersion": "<opaque>",
|
|
18
|
+
* "generation": <int>,
|
|
19
|
+
* "at": "RFC3339"
|
|
20
|
+
* }
|
|
21
|
+
* }
|
|
22
|
+
*
|
|
23
|
+
* Writes are atomic (tmp + rename) to match the hosts.json
|
|
24
|
+
* convention; corrupt-then-crash leaves the file untouched.
|
|
25
|
+
*
|
|
26
|
+
* NOTE: this is per-project state, NOT per-host. Multi-host
|
|
27
|
+
* deploys of the same project use a single lastDeploy slot —
|
|
28
|
+
* switching --host between deploys invalidates the cache and
|
|
29
|
+
* triggers a fresh GET to re-seed. This is intentional: the cache
|
|
30
|
+
* is a hot path optimisation, not authoritative — the daemon's
|
|
31
|
+
* current rv is always the truth.
|
|
32
|
+
*/
|
|
33
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync, renameSync } from "node:fs";
|
|
34
|
+
import { dirname, join } from "node:path";
|
|
35
|
+
export const LOCAL_SCHEMA_VERSION = 1;
|
|
36
|
+
/** Resolve <project>/.creek/local.json. */
|
|
37
|
+
export function localCachePath(projectRoot) {
|
|
38
|
+
return join(projectRoot, ".creek", "local.json");
|
|
39
|
+
}
|
|
40
|
+
/** Read the cache. Returns an empty file shape if absent. */
|
|
41
|
+
export function readLocalCache(projectRoot) {
|
|
42
|
+
const path = localCachePath(projectRoot);
|
|
43
|
+
if (!existsSync(path)) {
|
|
44
|
+
return { schemaVersion: LOCAL_SCHEMA_VERSION };
|
|
45
|
+
}
|
|
46
|
+
const raw = readFileSync(path, "utf-8");
|
|
47
|
+
const parsed = JSON.parse(raw);
|
|
48
|
+
if (typeof parsed.schemaVersion !== "number") {
|
|
49
|
+
throw new Error(`local.json: missing schemaVersion`);
|
|
50
|
+
}
|
|
51
|
+
if (parsed.schemaVersion !== LOCAL_SCHEMA_VERSION) {
|
|
52
|
+
throw new Error(`local.json: unsupported schemaVersion ${parsed.schemaVersion} ` +
|
|
53
|
+
`(want ${LOCAL_SCHEMA_VERSION})`);
|
|
54
|
+
}
|
|
55
|
+
return parsed;
|
|
56
|
+
}
|
|
57
|
+
/** Atomic write — tmp + rename. */
|
|
58
|
+
export function writeLocalCache(projectRoot, file) {
|
|
59
|
+
if (file.schemaVersion !== LOCAL_SCHEMA_VERSION) {
|
|
60
|
+
throw new Error(`writeLocalCache: schemaVersion ${file.schemaVersion} != ${LOCAL_SCHEMA_VERSION}`);
|
|
61
|
+
}
|
|
62
|
+
const path = localCachePath(projectRoot);
|
|
63
|
+
mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
|
|
64
|
+
const tmp = path + ".tmp";
|
|
65
|
+
writeFileSync(tmp, JSON.stringify(file, null, 2) + "\n", { mode: 0o600 });
|
|
66
|
+
renameSync(tmp, path);
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Update lastDeploy after a successful spec mutation. Reads the
|
|
70
|
+
* existing file, replaces lastDeploy, writes back. Convenience
|
|
71
|
+
* wrapper for the most common write pattern.
|
|
72
|
+
*/
|
|
73
|
+
export function recordLastDeploy(projectRoot, lastDeploy) {
|
|
74
|
+
const file = readLocalCache(projectRoot);
|
|
75
|
+
writeLocalCache(projectRoot, { ...file, lastDeploy });
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Read the cached rv for an If-Match header. Returns undefined
|
|
79
|
+
* when the cache is empty, when the appId/host don't match
|
|
80
|
+
* (different project or host than last deploy), or when the cache
|
|
81
|
+
* is corrupt. Callers fall back to a fresh GET on undefined.
|
|
82
|
+
*/
|
|
83
|
+
export function cachedResourceVersion(projectRoot, appId, host) {
|
|
84
|
+
let file;
|
|
85
|
+
try {
|
|
86
|
+
file = readLocalCache(projectRoot);
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
return undefined;
|
|
90
|
+
}
|
|
91
|
+
const last = file.lastDeploy;
|
|
92
|
+
if (!last)
|
|
93
|
+
return undefined;
|
|
94
|
+
if (last.appId !== appId)
|
|
95
|
+
return undefined;
|
|
96
|
+
if (last.host !== host)
|
|
97
|
+
return undefined;
|
|
98
|
+
return last.resourceVersion;
|
|
99
|
+
}
|
|
100
|
+
//# sourceMappingURL=local-cache.js.map
|
package/dist/utils/nextjs.d.ts
CHANGED
|
@@ -15,8 +15,10 @@ export declare function getNextVersion(cwd: string): string | null;
|
|
|
15
15
|
/**
|
|
16
16
|
* Unified Next.js build entry point.
|
|
17
17
|
*
|
|
18
|
-
* - Next.js >= 16.2.3: Creek adapter path (recommended)
|
|
19
|
-
*
|
|
18
|
+
* - Next.js >= 16.2.3: Creek adapter path (recommended). The adapter is
|
|
19
|
+
* lazily installed into .creek/node_modules on first use — the CLI never
|
|
20
|
+
* depends on it directly, so non-Next.js users never pay for it.
|
|
21
|
+
* - Next.js < 16.2.3 (or adapter install fails): legacy opennext path.
|
|
20
22
|
*
|
|
21
23
|
* Min version for the adapter path matches @solcreek/adapter-creek's
|
|
22
24
|
* peerDependency, which pins Next.js >= 16.2.3 to fix CVE-2026-23869.
|
package/dist/utils/nextjs.js
CHANGED
|
@@ -40,27 +40,38 @@ function semverGte(version, target) {
|
|
|
40
40
|
return aPat >= bPat;
|
|
41
41
|
}
|
|
42
42
|
/**
|
|
43
|
-
*
|
|
43
|
+
* Resolve @solcreek/adapter-creek from any reachable location.
|
|
44
44
|
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
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
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
-
|
|
55
|
-
|
|
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
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
36
|
+
"@solcreek/sdk": "0.4.10"
|
|
37
37
|
},
|
|
38
38
|
"devDependencies": {
|
|
39
39
|
"@testing-library/dom": "^10.4.1",
|