@solcreek/cli 0.4.2 → 0.4.4
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 +49 -0
- package/dist/commands/deploy.d.ts +29 -0
- package/dist/commands/deploy.js +223 -1
- package/dist/commands/dev.js +39 -0
- package/dist/commands/queue.d.ts +2 -0
- package/dist/commands/queue.js +92 -0
- package/dist/dev/server.d.ts +4 -0
- package/dist/dev/server.js +32 -0
- package/dist/dev/worker-runner.d.ts +8 -0
- package/dist/dev/worker-runner.js +29 -0
- package/dist/index.js +2 -0
- package/dist/utils/nextjs.d.ts +8 -4
- package/dist/utils/nextjs.js +11 -7
- package/package.json +11 -10
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# @solcreek/cli
|
|
2
|
+
|
|
3
|
+
## 0.4.4
|
|
4
|
+
|
|
5
|
+
### Features
|
|
6
|
+
|
|
7
|
+
- **`creek deploy --from-github [--project <slug>]`** — trigger a deploy
|
|
8
|
+
of the latest commit on a project's production branch via its
|
|
9
|
+
stored GitHub connection, skipping the local build entirely. Same
|
|
10
|
+
server code path that a real `git push` webhook uses — runs in
|
|
11
|
+
remote-builder, deploys via the existing pipeline, posts commit
|
|
12
|
+
status. Use `--project` to target by slug or UUID, or omit it to
|
|
13
|
+
infer from `creek.toml` in the current directory.
|
|
14
|
+
|
|
15
|
+
Flow: the CLI snapshots the newest existing deployment, POSTs
|
|
16
|
+
`/github/deploy-latest`, then polls `/projects/:id/deployments`
|
|
17
|
+
every 1.5–2s to pick up the new row (handlePush runs in
|
|
18
|
+
`waitUntil` on the server, so the row appears a beat after the
|
|
19
|
+
trigger). Streams status transitions to the terminal until the
|
|
20
|
+
deployment settles on `active`, `failed`, or `cancelled`. Hard
|
|
21
|
+
cap of 15 minutes. `--json` prints a single structured result.
|
|
22
|
+
|
|
23
|
+
Pairs with the dashboard's new "Deploy latest" button on the
|
|
24
|
+
project detail page — both call the same endpoint.
|
|
25
|
+
|
|
26
|
+
## 0.4.3
|
|
27
|
+
|
|
28
|
+
- Cron/queue trigger support flowed through the deploy pipeline
|
|
29
|
+
(bump commit `c1110b1`).
|
|
30
|
+
|
|
31
|
+
## 0.4.2
|
|
32
|
+
|
|
33
|
+
- Bundled with the queue binding + worker wrapper work landed in
|
|
34
|
+
`@solcreek/runtime` 0.4.0 (bump commit `28fdb11`).
|
|
35
|
+
|
|
36
|
+
## 0.4.1
|
|
37
|
+
|
|
38
|
+
- Bump alongside `@solcreek/sdk` 0.4.0 (semantic resource names in
|
|
39
|
+
`creek.toml`).
|
|
40
|
+
|
|
41
|
+
## 0.4.0
|
|
42
|
+
|
|
43
|
+
- Version bump ahead of the cron trigger pipeline work.
|
|
44
|
+
|
|
45
|
+
## 0.3.9
|
|
46
|
+
|
|
47
|
+
- First tagged release via the publish-cli GitHub Actions workflow
|
|
48
|
+
(the workflow has been dormant since; 0.4.x went out via manual
|
|
49
|
+
`pnpm publish` until 0.4.4 restored automated releases).
|
|
@@ -14,6 +14,16 @@ export declare const deployCommand: import("citty").CommandDef<{
|
|
|
14
14
|
description: string;
|
|
15
15
|
required: false;
|
|
16
16
|
};
|
|
17
|
+
"from-github": {
|
|
18
|
+
type: "boolean";
|
|
19
|
+
description: string;
|
|
20
|
+
default: false;
|
|
21
|
+
};
|
|
22
|
+
project: {
|
|
23
|
+
type: "string";
|
|
24
|
+
description: string;
|
|
25
|
+
required: false;
|
|
26
|
+
};
|
|
17
27
|
json: {
|
|
18
28
|
type: "boolean";
|
|
19
29
|
description: string;
|
|
@@ -35,6 +45,25 @@ export declare const deployCommand: import("citty").CommandDef<{
|
|
|
35
45
|
default: false;
|
|
36
46
|
};
|
|
37
47
|
}>;
|
|
48
|
+
export interface CliDeployment {
|
|
49
|
+
id: string;
|
|
50
|
+
version: number;
|
|
51
|
+
status: string;
|
|
52
|
+
branch: string | null;
|
|
53
|
+
failedStep: string | null;
|
|
54
|
+
errorMessage: string | null;
|
|
55
|
+
createdAt: number;
|
|
56
|
+
url: string | null;
|
|
57
|
+
}
|
|
58
|
+
export declare const CLI_TERMINAL_STATUSES: Set<string>;
|
|
59
|
+
export declare const CLI_IN_FLIGHT_STATUSES: Set<string>;
|
|
60
|
+
/**
|
|
61
|
+
* Given a deployment list and a "previous latest createdAt" snapshot,
|
|
62
|
+
* return the most recently-created deployment that arrived after the
|
|
63
|
+
* snapshot, or null if none have appeared yet. Used by `--from-github`
|
|
64
|
+
* to find the row that handlePush just inserted in the background.
|
|
65
|
+
*/
|
|
66
|
+
export declare function findNewDeployment(deployments: CliDeployment[], previousLatestCreatedAt: number): CliDeployment | null;
|
|
38
67
|
/**
|
|
39
68
|
* Patch bare Node.js module imports (e.g. `from "fs"`) to use the `node:` prefix
|
|
40
69
|
* (e.g. `from "node:fs"`). Workerd requires the `node:` prefix for all built-in modules.
|
package/dist/commands/deploy.js
CHANGED
|
@@ -10,7 +10,7 @@ import { collectAssets } from "../utils/bundle.js";
|
|
|
10
10
|
import { bundleSSRServer } from "../utils/ssr-bundle.js";
|
|
11
11
|
import { bundleWorker } from "../utils/worker-bundle.js";
|
|
12
12
|
import { sandboxDeploy, pollSandboxStatus, printSandboxSuccess } from "../utils/sandbox.js";
|
|
13
|
-
import { isTTY, jsonOutput, resolveJsonMode, globalArgs, shouldAutoConfirm, NO_PROJECT_BREADCRUMBS } from "../utils/output.js";
|
|
13
|
+
import { isTTY, jsonOutput, resolveJsonMode, globalArgs, shouldAutoConfirm, AUTH_BREADCRUMBS, NO_PROJECT_BREADCRUMBS } from "../utils/output.js";
|
|
14
14
|
import { ensureTosAccepted } from "../utils/tos.js";
|
|
15
15
|
import { buildNextjs, patchBundledWorker, hasAdapterOutput } from "../utils/nextjs.js";
|
|
16
16
|
import { isRepoUrl, parseRepoUrl, validateRepoUrl, validateSubpath, RepoUrlError } from "../utils/repo-url.js";
|
|
@@ -62,6 +62,16 @@ export const deployCommand = defineCommand({
|
|
|
62
62
|
description: "Subdirectory within repo to deploy (for monorepos)",
|
|
63
63
|
required: false,
|
|
64
64
|
},
|
|
65
|
+
"from-github": {
|
|
66
|
+
type: "boolean",
|
|
67
|
+
description: "Skip local build; trigger a deploy of the latest commit on the project's production branch via its GitHub connection",
|
|
68
|
+
default: false,
|
|
69
|
+
},
|
|
70
|
+
project: {
|
|
71
|
+
type: "string",
|
|
72
|
+
description: "Target project slug or UUID (required with --from-github when not run inside a project directory)",
|
|
73
|
+
required: false,
|
|
74
|
+
},
|
|
65
75
|
},
|
|
66
76
|
async run({ args }) {
|
|
67
77
|
const jsonMode = resolveJsonMode(args);
|
|
@@ -82,6 +92,14 @@ export const deployCommand = defineCommand({
|
|
|
82
92
|
}
|
|
83
93
|
return await deployTemplate(args.template, templateData);
|
|
84
94
|
}
|
|
95
|
+
// --- Trigger a deploy from the project's GitHub connection (no local build) ---
|
|
96
|
+
if (args["from-github"]) {
|
|
97
|
+
return await deployFromGithub({
|
|
98
|
+
project: args.project,
|
|
99
|
+
cwd: args.dir ? resolve(args.dir) : process.cwd(),
|
|
100
|
+
jsonMode,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
85
103
|
// --- Repo URL deploy (creek deploy https://github.com/user/repo) ---
|
|
86
104
|
if (args.dir && isRepoUrl(args.dir)) {
|
|
87
105
|
return await deployRepoUrl(args.dir, {
|
|
@@ -145,6 +163,210 @@ export const deployCommand = defineCommand({
|
|
|
145
163
|
process.exit(1);
|
|
146
164
|
},
|
|
147
165
|
});
|
|
166
|
+
export const CLI_TERMINAL_STATUSES = new Set(["active", "failed", "cancelled"]);
|
|
167
|
+
export const CLI_IN_FLIGHT_STATUSES = new Set([
|
|
168
|
+
"queued",
|
|
169
|
+
"uploading",
|
|
170
|
+
"provisioning",
|
|
171
|
+
"deploying",
|
|
172
|
+
]);
|
|
173
|
+
/**
|
|
174
|
+
* Given a deployment list and a "previous latest createdAt" snapshot,
|
|
175
|
+
* return the most recently-created deployment that arrived after the
|
|
176
|
+
* snapshot, or null if none have appeared yet. Used by `--from-github`
|
|
177
|
+
* to find the row that handlePush just inserted in the background.
|
|
178
|
+
*/
|
|
179
|
+
export function findNewDeployment(deployments, previousLatestCreatedAt) {
|
|
180
|
+
const newer = deployments.filter((d) => d.createdAt > previousLatestCreatedAt);
|
|
181
|
+
if (newer.length === 0)
|
|
182
|
+
return null;
|
|
183
|
+
return newer.reduce((latest, d) => (d.createdAt > latest.createdAt ? d : latest), newer[0]);
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Trigger a deploy that uses the project's github_connection instead of a
|
|
187
|
+
* local build. Polls the deployments list until the new row settles and
|
|
188
|
+
* streams status transitions to the terminal.
|
|
189
|
+
*
|
|
190
|
+
* The server endpoint (POST /github/deploy-latest) returns 200 immediately
|
|
191
|
+
* while handlePush runs in waitUntil, so we fingerprint the deployments list
|
|
192
|
+
* before the POST and look for a new row afterward.
|
|
193
|
+
*/
|
|
194
|
+
async function deployFromGithub(options) {
|
|
195
|
+
const { project: projectArg, cwd, jsonMode } = options;
|
|
196
|
+
const token = getToken();
|
|
197
|
+
if (!token) {
|
|
198
|
+
if (jsonMode)
|
|
199
|
+
jsonOutput({ error: "not_authenticated", message: "Run `creek login` first." }, 1, AUTH_BREADCRUMBS);
|
|
200
|
+
consola.error("Not logged in. Run `creek login` first.");
|
|
201
|
+
process.exit(1);
|
|
202
|
+
}
|
|
203
|
+
// Resolve the target project: explicit --project flag wins, otherwise
|
|
204
|
+
// fall back to the current directory's resolved config (creek.toml
|
|
205
|
+
// [project] name or inferred project name).
|
|
206
|
+
let projectSlug = projectArg;
|
|
207
|
+
if (!projectSlug) {
|
|
208
|
+
try {
|
|
209
|
+
const resolved = resolveConfig(cwd);
|
|
210
|
+
projectSlug = resolved.projectName;
|
|
211
|
+
}
|
|
212
|
+
catch (err) {
|
|
213
|
+
if (!(err instanceof ConfigNotFoundError))
|
|
214
|
+
throw err;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
if (!projectSlug) {
|
|
218
|
+
if (jsonMode)
|
|
219
|
+
jsonOutput({ error: "validation", message: "Could not determine target project. Pass --project <slug>." }, 1, NO_PROJECT_BREADCRUMBS);
|
|
220
|
+
consola.error("Could not determine target project. Pass --project <slug> or run inside a directory with a creek.toml.");
|
|
221
|
+
process.exit(1);
|
|
222
|
+
}
|
|
223
|
+
const client = new CreekClient(getApiUrl(), token);
|
|
224
|
+
// Snapshot the current newest deployment so we can detect the one this
|
|
225
|
+
// command creates. createdAt is the cleanest marker — the version number
|
|
226
|
+
// also works but we don't need to parse it.
|
|
227
|
+
let previousLatestCreatedAt = 0;
|
|
228
|
+
try {
|
|
229
|
+
const existing = (await client.listDeployments(projectSlug));
|
|
230
|
+
previousLatestCreatedAt = existing.reduce((max, d) => Math.max(max, d.createdAt), 0);
|
|
231
|
+
}
|
|
232
|
+
catch (err) {
|
|
233
|
+
if (err instanceof CreekAuthError)
|
|
234
|
+
throw err;
|
|
235
|
+
if (jsonMode)
|
|
236
|
+
jsonOutput({ error: "not_found", message: err.message }, 1, NO_PROJECT_BREADCRUMBS);
|
|
237
|
+
consola.error(`Could not load deployments for project '${projectSlug}': ${err.message}`);
|
|
238
|
+
process.exit(1);
|
|
239
|
+
}
|
|
240
|
+
// Kick off the build
|
|
241
|
+
if (!jsonMode) {
|
|
242
|
+
section("Trigger");
|
|
243
|
+
consola.start(` Triggering deploy of '${projectSlug}' from its GitHub connection...`);
|
|
244
|
+
}
|
|
245
|
+
let triggerResult;
|
|
246
|
+
try {
|
|
247
|
+
triggerResult = await client.deployFromGithub(projectSlug);
|
|
248
|
+
}
|
|
249
|
+
catch (err) {
|
|
250
|
+
const msg = err.message;
|
|
251
|
+
if (jsonMode)
|
|
252
|
+
jsonOutput({ error: "trigger_failed", message: msg }, 1, NO_PROJECT_BREADCRUMBS);
|
|
253
|
+
consola.error(`Trigger failed: ${msg}`);
|
|
254
|
+
process.exit(1);
|
|
255
|
+
}
|
|
256
|
+
if (!jsonMode) {
|
|
257
|
+
consola.success(` Triggered (${triggerResult.branch} @ ${triggerResult.commitSha.slice(0, 7)})`);
|
|
258
|
+
section("Watch");
|
|
259
|
+
}
|
|
260
|
+
// Poll for the new deployment row, then follow its status until it settles.
|
|
261
|
+
// First phase: wait up to 20s for the row to appear (handlePush runs in
|
|
262
|
+
// waitUntil and may take a beat to insert).
|
|
263
|
+
const startedAt = Date.now();
|
|
264
|
+
const rowAppearDeadline = startedAt + 20_000;
|
|
265
|
+
const overallDeadline = startedAt + 15 * 60_000; // 15 min hard cap
|
|
266
|
+
let targetDeployment = null;
|
|
267
|
+
let lastStatus = null;
|
|
268
|
+
while (Date.now() < overallDeadline) {
|
|
269
|
+
let list;
|
|
270
|
+
try {
|
|
271
|
+
list = (await client.listDeployments(projectSlug));
|
|
272
|
+
}
|
|
273
|
+
catch (err) {
|
|
274
|
+
if (!jsonMode)
|
|
275
|
+
consola.warn(` Poll failed: ${err.message}`);
|
|
276
|
+
await sleep(2000);
|
|
277
|
+
continue;
|
|
278
|
+
}
|
|
279
|
+
// Phase 1: find the new row. It's the newest deployment with a
|
|
280
|
+
// createdAt greater than our snapshot. triggerType should be 'github'
|
|
281
|
+
// but we don't require it (rollback/promote could race, but extremely
|
|
282
|
+
// unlikely in the narrow window between snapshot + poll).
|
|
283
|
+
if (!targetDeployment) {
|
|
284
|
+
const candidate = list
|
|
285
|
+
.filter((d) => d.createdAt > previousLatestCreatedAt)
|
|
286
|
+
.sort((a, b) => b.createdAt - a.createdAt)[0];
|
|
287
|
+
if (candidate) {
|
|
288
|
+
targetDeployment = candidate;
|
|
289
|
+
if (!jsonMode) {
|
|
290
|
+
consola.info(` v${candidate.version} ${candidate.branch ?? ""}`.trim());
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
else if (Date.now() > rowAppearDeadline) {
|
|
294
|
+
// 20s and no new row — handlePush must have errored before it could
|
|
295
|
+
// insert. Surface the timeout rather than hanging.
|
|
296
|
+
if (jsonMode)
|
|
297
|
+
jsonOutput({ error: "no_deployment", message: "Deployment row did not appear within 20s. Check server logs." }, 1, NO_PROJECT_BREADCRUMBS);
|
|
298
|
+
consola.error(" Deployment row did not appear within 20s. Check control-plane logs.");
|
|
299
|
+
process.exit(1);
|
|
300
|
+
}
|
|
301
|
+
else {
|
|
302
|
+
await sleep(1500);
|
|
303
|
+
continue;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
// Phase 2: follow the target row's status
|
|
307
|
+
const current = list.find((d) => d.id === targetDeployment.id);
|
|
308
|
+
if (!current) {
|
|
309
|
+
// Shouldn't happen — row was there a moment ago
|
|
310
|
+
await sleep(1500);
|
|
311
|
+
continue;
|
|
312
|
+
}
|
|
313
|
+
targetDeployment = current;
|
|
314
|
+
if (current.status !== lastStatus) {
|
|
315
|
+
lastStatus = current.status;
|
|
316
|
+
if (!jsonMode) {
|
|
317
|
+
if (CLI_IN_FLIGHT_STATUSES.has(current.status)) {
|
|
318
|
+
consola.start(` ${current.status}...`);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
if (CLI_TERMINAL_STATUSES.has(current.status)) {
|
|
323
|
+
break;
|
|
324
|
+
}
|
|
325
|
+
await sleep(2000);
|
|
326
|
+
}
|
|
327
|
+
if (!targetDeployment) {
|
|
328
|
+
if (jsonMode)
|
|
329
|
+
jsonOutput({ error: "timeout", message: "Timed out waiting for deployment" }, 1, NO_PROJECT_BREADCRUMBS);
|
|
330
|
+
consola.error("Timed out waiting for deployment");
|
|
331
|
+
process.exit(1);
|
|
332
|
+
}
|
|
333
|
+
// Report the outcome
|
|
334
|
+
if (targetDeployment.status === "active") {
|
|
335
|
+
if (jsonMode) {
|
|
336
|
+
jsonOutput({
|
|
337
|
+
ok: true,
|
|
338
|
+
deploymentId: targetDeployment.id,
|
|
339
|
+
version: targetDeployment.version,
|
|
340
|
+
status: "active",
|
|
341
|
+
url: targetDeployment.url,
|
|
342
|
+
}, 0, NO_PROJECT_BREADCRUMBS);
|
|
343
|
+
}
|
|
344
|
+
else {
|
|
345
|
+
consola.success(` Deployed v${targetDeployment.version}`);
|
|
346
|
+
if (targetDeployment.url)
|
|
347
|
+
consola.log(` ${targetDeployment.url}`);
|
|
348
|
+
}
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
// Failed / cancelled
|
|
352
|
+
const failureMsg = targetDeployment.errorMessage || `Deployment ${targetDeployment.status}`;
|
|
353
|
+
if (jsonMode) {
|
|
354
|
+
jsonOutput({
|
|
355
|
+
ok: false,
|
|
356
|
+
deploymentId: targetDeployment.id,
|
|
357
|
+
status: targetDeployment.status,
|
|
358
|
+
failedStep: targetDeployment.failedStep,
|
|
359
|
+
errorMessage: failureMsg,
|
|
360
|
+
}, 1, NO_PROJECT_BREADCRUMBS);
|
|
361
|
+
}
|
|
362
|
+
else {
|
|
363
|
+
consola.error(` Deployment ${targetDeployment.status}${targetDeployment.failedStep ? ` at ${targetDeployment.failedStep}` : ""}: ${failureMsg}`);
|
|
364
|
+
}
|
|
365
|
+
process.exit(1);
|
|
366
|
+
}
|
|
367
|
+
function sleep(ms) {
|
|
368
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
369
|
+
}
|
|
148
370
|
async function deployRepoUrl(input, options) {
|
|
149
371
|
const { path: subpath, skipBuild, json: jsonMode } = options;
|
|
150
372
|
try {
|
package/dist/commands/dev.js
CHANGED
|
@@ -58,6 +58,45 @@ export const devCommand = defineCommand({
|
|
|
58
58
|
await server.stop();
|
|
59
59
|
process.exit(1);
|
|
60
60
|
}
|
|
61
|
+
// Interactive trigger commands (only if cron/queue configured)
|
|
62
|
+
if (config.cron.length > 0 || config.queue) {
|
|
63
|
+
const { createInterface } = await import("node:readline");
|
|
64
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout, terminal: false });
|
|
65
|
+
rl.on("line", async (line) => {
|
|
66
|
+
const input = line.trim();
|
|
67
|
+
if (!input)
|
|
68
|
+
return;
|
|
69
|
+
if (input === "cron" && config.cron.length > 0) {
|
|
70
|
+
try {
|
|
71
|
+
await server.triggerScheduled();
|
|
72
|
+
consola.success("Triggered scheduled()");
|
|
73
|
+
}
|
|
74
|
+
catch (e) {
|
|
75
|
+
consola.error(`scheduled() error: ${e.message}`);
|
|
76
|
+
}
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
if (input.startsWith("queue ") && config.queue) {
|
|
80
|
+
const payload = input.slice(6).trim();
|
|
81
|
+
let parsed = payload;
|
|
82
|
+
try {
|
|
83
|
+
parsed = JSON.parse(payload);
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
// Not JSON, send as string
|
|
87
|
+
}
|
|
88
|
+
try {
|
|
89
|
+
await server.sendQueueMessage(parsed);
|
|
90
|
+
consola.success(`Sent message to queue()`);
|
|
91
|
+
}
|
|
92
|
+
catch (e) {
|
|
93
|
+
consola.error(`queue() error: ${e.message}`);
|
|
94
|
+
}
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
consola.warn(`Unknown command: ${input}`);
|
|
98
|
+
});
|
|
99
|
+
}
|
|
61
100
|
},
|
|
62
101
|
});
|
|
63
102
|
//# sourceMappingURL=dev.js.map
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { defineCommand } from "citty";
|
|
2
|
+
import consola from "consola";
|
|
3
|
+
import { CreekClient, parseConfig } from "@solcreek/sdk";
|
|
4
|
+
import { getToken, getApiUrl } from "../utils/config.js";
|
|
5
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
import { globalArgs, resolveJsonMode, jsonOutput } from "../utils/output.js";
|
|
8
|
+
function getProjectSlug() {
|
|
9
|
+
const configPath = join(process.cwd(), "creek.toml");
|
|
10
|
+
if (!existsSync(configPath)) {
|
|
11
|
+
consola.error("No creek.toml found. Run `creek init` first.");
|
|
12
|
+
process.exit(1);
|
|
13
|
+
}
|
|
14
|
+
return parseConfig(readFileSync(configPath, "utf-8")).project.name;
|
|
15
|
+
}
|
|
16
|
+
function getClient() {
|
|
17
|
+
const token = getToken();
|
|
18
|
+
if (!token) {
|
|
19
|
+
consola.error("Not authenticated. Run `creek login` first.");
|
|
20
|
+
process.exit(1);
|
|
21
|
+
}
|
|
22
|
+
return new CreekClient(getApiUrl(), token);
|
|
23
|
+
}
|
|
24
|
+
const queueSend = defineCommand({
|
|
25
|
+
meta: { name: "send", description: "Send a message to the project's queue" },
|
|
26
|
+
args: {
|
|
27
|
+
message: {
|
|
28
|
+
type: "positional",
|
|
29
|
+
description: "Message body (string, or use --json for JSON content)",
|
|
30
|
+
required: true,
|
|
31
|
+
},
|
|
32
|
+
parseJson: {
|
|
33
|
+
type: "boolean",
|
|
34
|
+
alias: "j",
|
|
35
|
+
description: "Parse message as JSON",
|
|
36
|
+
default: false,
|
|
37
|
+
},
|
|
38
|
+
project: {
|
|
39
|
+
type: "string",
|
|
40
|
+
description: "Project slug (defaults to current creek.toml)",
|
|
41
|
+
},
|
|
42
|
+
...globalArgs,
|
|
43
|
+
},
|
|
44
|
+
async run({ args }) {
|
|
45
|
+
const jsonMode = resolveJsonMode(args);
|
|
46
|
+
const client = getClient();
|
|
47
|
+
const slug = args.project ?? getProjectSlug();
|
|
48
|
+
let payload = args.message;
|
|
49
|
+
if (args.parseJson) {
|
|
50
|
+
try {
|
|
51
|
+
payload = JSON.parse(args.message);
|
|
52
|
+
}
|
|
53
|
+
catch (err) {
|
|
54
|
+
if (jsonMode) {
|
|
55
|
+
jsonOutput({ ok: false, error: "invalid_json", message: err instanceof Error ? err.message : String(err) }, 1, []);
|
|
56
|
+
}
|
|
57
|
+
consola.error(`Invalid JSON: ${err instanceof Error ? err.message : err}`);
|
|
58
|
+
process.exit(1);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
try {
|
|
62
|
+
const result = await client.sendQueueMessage(slug, payload);
|
|
63
|
+
if (jsonMode) {
|
|
64
|
+
jsonOutput({ ok: true, project: slug, queueId: result.queueId }, 0, [
|
|
65
|
+
{ command: `creek deployments --project ${slug}`, description: "View deployment history" },
|
|
66
|
+
]);
|
|
67
|
+
}
|
|
68
|
+
consola.success(`Sent message to ${slug} queue`);
|
|
69
|
+
}
|
|
70
|
+
catch (err) {
|
|
71
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
72
|
+
if (jsonMode) {
|
|
73
|
+
jsonOutput({ ok: false, error: "send_failed", message: msg }, 1, [
|
|
74
|
+
{ command: `creek status --project ${slug}`, description: "Check project triggers" },
|
|
75
|
+
]);
|
|
76
|
+
}
|
|
77
|
+
consola.error(`Failed to send: ${msg}`);
|
|
78
|
+
consola.info("Make sure your creek.toml has `queue = true` under [triggers] and the project is deployed.");
|
|
79
|
+
process.exit(1);
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
export const queueCommand = defineCommand({
|
|
84
|
+
meta: {
|
|
85
|
+
name: "queue",
|
|
86
|
+
description: "Send messages to the project's queue",
|
|
87
|
+
},
|
|
88
|
+
subCommands: {
|
|
89
|
+
send: queueSend,
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
//# sourceMappingURL=queue.js.map
|
package/dist/dev/server.d.ts
CHANGED
|
@@ -13,6 +13,10 @@ export declare class DevServer {
|
|
|
13
13
|
private proxy;
|
|
14
14
|
constructor(options: DevServerOptions);
|
|
15
15
|
start(): Promise<void>;
|
|
16
|
+
/** Trigger the worker's scheduled() handler. */
|
|
17
|
+
triggerScheduled(): Promise<void>;
|
|
18
|
+
/** Send a message to the worker's queue() handler. */
|
|
19
|
+
sendQueueMessage(message: unknown): Promise<void>;
|
|
16
20
|
stop(): Promise<void>;
|
|
17
21
|
}
|
|
18
22
|
//# sourceMappingURL=server.d.ts.map
|
package/dist/dev/server.js
CHANGED
|
@@ -47,6 +47,8 @@ export class DevServer {
|
|
|
47
47
|
realtimeUrl: `http://127.0.0.1:${this.realtimeServer.getPort()}`,
|
|
48
48
|
projectSlug: config.projectName,
|
|
49
49
|
vars: config.vars,
|
|
50
|
+
cron: config.cron,
|
|
51
|
+
queue: config.queue,
|
|
50
52
|
onRebuild: (ms) => {
|
|
51
53
|
consola.info(`Worker rebuilt in ${ms}ms`);
|
|
52
54
|
},
|
|
@@ -117,8 +119,38 @@ export class DevServer {
|
|
|
117
119
|
}
|
|
118
120
|
consola.info(`Realtime: ws://localhost:${actualPort}`);
|
|
119
121
|
consola.info(`Data: .creek/dev/`);
|
|
122
|
+
if (config.cron.length > 0 || config.queue) {
|
|
123
|
+
const triggers = [];
|
|
124
|
+
if (config.cron.length > 0)
|
|
125
|
+
triggers.push(`${config.cron.length} cron`);
|
|
126
|
+
if (config.queue)
|
|
127
|
+
triggers.push("queue");
|
|
128
|
+
consola.info(`Triggers: ${triggers.join(", ")}`);
|
|
129
|
+
}
|
|
120
130
|
console.log("");
|
|
121
131
|
consola.success(`Ready in ${elapsed}ms`);
|
|
132
|
+
if (config.cron.length > 0 || config.queue) {
|
|
133
|
+
console.log("");
|
|
134
|
+
consola.info("Trigger commands (type and press Enter):");
|
|
135
|
+
if (config.cron.length > 0) {
|
|
136
|
+
consola.info(" cron Trigger scheduled() handler");
|
|
137
|
+
}
|
|
138
|
+
if (config.queue) {
|
|
139
|
+
consola.info(' queue <message> Send a message to queue() handler');
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
/** Trigger the worker's scheduled() handler. */
|
|
144
|
+
async triggerScheduled() {
|
|
145
|
+
if (!this.workerRunner)
|
|
146
|
+
throw new Error("Worker not running");
|
|
147
|
+
await this.workerRunner.triggerScheduled();
|
|
148
|
+
}
|
|
149
|
+
/** Send a message to the worker's queue() handler. */
|
|
150
|
+
async sendQueueMessage(message) {
|
|
151
|
+
if (!this.workerRunner)
|
|
152
|
+
throw new Error("Worker not running");
|
|
153
|
+
await this.workerRunner.sendQueueMessage(message);
|
|
122
154
|
}
|
|
123
155
|
async stop() {
|
|
124
156
|
// Stop in reverse order
|
|
@@ -16,6 +16,10 @@ export interface WorkerRunnerOptions {
|
|
|
16
16
|
vars?: Record<string, string>;
|
|
17
17
|
/** Whether the project has client assets (Worker + SPA hybrid). */
|
|
18
18
|
hasClientAssets?: boolean;
|
|
19
|
+
/** Cron schedules from creek.toml [triggers].cron */
|
|
20
|
+
cron?: string[];
|
|
21
|
+
/** Whether project uses a queue (auto-provisions a local queue) */
|
|
22
|
+
queue?: boolean;
|
|
19
23
|
/** Callback when worker is rebuilt. */
|
|
20
24
|
onRebuild?: (durationMs: number) => void;
|
|
21
25
|
/**
|
|
@@ -42,6 +46,10 @@ export declare class WorkerRunner {
|
|
|
42
46
|
/** Dispatch a fetch request to the worker (via Miniflare). */
|
|
43
47
|
dispatchFetch(input: string, init?: RequestInit): Promise<Response>;
|
|
44
48
|
private buildMiniflareOptions;
|
|
49
|
+
/** Trigger the scheduled() handler manually (for local cron simulation). */
|
|
50
|
+
triggerScheduled(scheduledTime?: number, cron?: string): Promise<Response>;
|
|
51
|
+
/** Send a message to the local queue (consumed by the worker's queue() handler). */
|
|
52
|
+
sendQueueMessage(message: unknown): Promise<void>;
|
|
45
53
|
private get esbuildOptions();
|
|
46
54
|
private bundle;
|
|
47
55
|
private startWatching;
|
|
@@ -124,8 +124,37 @@ export class WorkerRunner {
|
|
|
124
124
|
opts.r2Buckets = { [r2BindingName]: "creek-dev-r2" };
|
|
125
125
|
opts.r2Persist = persistDir ? join(persistDir, "r2") : false;
|
|
126
126
|
}
|
|
127
|
+
// Queue (producer + consumer wired to the same worker)
|
|
128
|
+
if (this.options.queue) {
|
|
129
|
+
opts.queueProducers = { QUEUE: "creek-dev-queue" };
|
|
130
|
+
opts.queueConsumers = {
|
|
131
|
+
"creek-dev-queue": {
|
|
132
|
+
maxBatchSize: 10,
|
|
133
|
+
maxBatchTimeout: 1,
|
|
134
|
+
maxRetries: 3,
|
|
135
|
+
},
|
|
136
|
+
};
|
|
137
|
+
}
|
|
127
138
|
return opts;
|
|
128
139
|
}
|
|
140
|
+
/** Trigger the scheduled() handler manually (for local cron simulation). */
|
|
141
|
+
async triggerScheduled(scheduledTime = Date.now(), cron = "* * * * *") {
|
|
142
|
+
if (!this.mf)
|
|
143
|
+
throw new Error("WorkerRunner not started");
|
|
144
|
+
const worker = await this.mf.getWorker();
|
|
145
|
+
const url = `http://placeholder/cdn-cgi/handler/scheduled?time=${scheduledTime}&cron=${encodeURIComponent(cron)}`;
|
|
146
|
+
return worker.fetch(url);
|
|
147
|
+
}
|
|
148
|
+
/** Send a message to the local queue (consumed by the worker's queue() handler). */
|
|
149
|
+
async sendQueueMessage(message) {
|
|
150
|
+
if (!this.mf)
|
|
151
|
+
throw new Error("WorkerRunner not started");
|
|
152
|
+
if (!this.options.queue) {
|
|
153
|
+
throw new Error("Queue is not enabled. Add `queue = true` to [triggers] in creek.toml.");
|
|
154
|
+
}
|
|
155
|
+
const queue = await this.mf.getQueueProducer("QUEUE");
|
|
156
|
+
await queue.send(message);
|
|
157
|
+
}
|
|
129
158
|
get esbuildOptions() {
|
|
130
159
|
return {
|
|
131
160
|
absWorkingDir: this.options.cwd,
|
package/dist/index.js
CHANGED
|
@@ -17,6 +17,7 @@ import { statusCommand } from "./commands/status.js";
|
|
|
17
17
|
import { devCommand } from "./commands/dev.js";
|
|
18
18
|
import { rollbackCommand } from "./commands/rollback.js";
|
|
19
19
|
import { opsCommand } from "./commands/ops.js";
|
|
20
|
+
import { queueCommand } from "./commands/queue.js";
|
|
20
21
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
21
22
|
const cliPkg = JSON.parse(readFileSync(join(__dirname, "..", "package.json"), "utf-8"));
|
|
22
23
|
// Read version from the "creek" facade package (what users install),
|
|
@@ -47,6 +48,7 @@ const main = defineCommand({
|
|
|
47
48
|
init: initCommand,
|
|
48
49
|
claim: claimCommand,
|
|
49
50
|
env: envCommand,
|
|
51
|
+
queue: queueCommand,
|
|
50
52
|
domains: domainsCommand,
|
|
51
53
|
rollback: rollbackCommand,
|
|
52
54
|
ops: opsCommand,
|
package/dist/utils/nextjs.d.ts
CHANGED
|
@@ -2,9 +2,10 @@
|
|
|
2
2
|
* Next.js-specific build utilities for Creek CLI.
|
|
3
3
|
*
|
|
4
4
|
* Two build paths:
|
|
5
|
-
* - **Adapter path** (Next.js >= 16.2): Uses @solcreek/adapter-
|
|
5
|
+
* - **Adapter path** (Next.js >= 16.2.3): Uses @solcreek/adapter-creek via
|
|
6
6
|
* NEXT_ADAPTER_PATH. Zero workarounds, typed outputs, direct esbuild bundle.
|
|
7
|
-
*
|
|
7
|
+
* Min version reflects the adapter's peerDependency (CVE-2026-23869).
|
|
8
|
+
* - **Legacy path** (Next.js < 16.2.3): Uses @opennextjs/cloudflare with
|
|
8
9
|
* workarounds (standalone patch, middleware manifest inline, etc.)
|
|
9
10
|
*
|
|
10
11
|
* The CLI auto-detects the Next.js version and picks the right path.
|
|
@@ -14,8 +15,11 @@ export declare function getNextVersion(cwd: string): string | null;
|
|
|
14
15
|
/**
|
|
15
16
|
* Unified Next.js build entry point.
|
|
16
17
|
*
|
|
17
|
-
* - Next.js >= 16.2: Creek adapter path (recommended)
|
|
18
|
-
* - Next.js < 16.2: legacy opennext path (best effort)
|
|
18
|
+
* - Next.js >= 16.2.3: Creek adapter path (recommended)
|
|
19
|
+
* - Next.js < 16.2.3: legacy opennext path (best effort)
|
|
20
|
+
*
|
|
21
|
+
* Min version for the adapter path matches @solcreek/adapter-creek's
|
|
22
|
+
* peerDependency, which pins Next.js >= 16.2.3 to fix CVE-2026-23869.
|
|
19
23
|
*/
|
|
20
24
|
export declare function buildNextjs(cwd: string, isMonorepo: boolean, projectName?: string): void;
|
|
21
25
|
/** Check if the adapter output exists (vs legacy opennext output). */
|
package/dist/utils/nextjs.js
CHANGED
|
@@ -2,9 +2,10 @@
|
|
|
2
2
|
* Next.js-specific build utilities for Creek CLI.
|
|
3
3
|
*
|
|
4
4
|
* Two build paths:
|
|
5
|
-
* - **Adapter path** (Next.js >= 16.2): Uses @solcreek/adapter-
|
|
5
|
+
* - **Adapter path** (Next.js >= 16.2.3): Uses @solcreek/adapter-creek via
|
|
6
6
|
* NEXT_ADAPTER_PATH. Zero workarounds, typed outputs, direct esbuild bundle.
|
|
7
|
-
*
|
|
7
|
+
* Min version reflects the adapter's peerDependency (CVE-2026-23869).
|
|
8
|
+
* - **Legacy path** (Next.js < 16.2.3): Uses @opennextjs/cloudflare with
|
|
8
9
|
* workarounds (standalone patch, middleware manifest inline, etc.)
|
|
9
10
|
*
|
|
10
11
|
* The CLI auto-detects the Next.js version and picks the right path.
|
|
@@ -39,7 +40,7 @@ function semverGte(version, target) {
|
|
|
39
40
|
return aPat >= bPat;
|
|
40
41
|
}
|
|
41
42
|
/**
|
|
42
|
-
* Build a Next.js app using the Creek adapter (>= 16.2).
|
|
43
|
+
* Build a Next.js app using the Creek adapter (>= 16.2.3).
|
|
43
44
|
*
|
|
44
45
|
* Sets NEXT_ADAPTER_PATH to the adapter bundled with the CLI.
|
|
45
46
|
* No opennext, no wrangler, no config patching — the adapter handles
|
|
@@ -57,7 +58,7 @@ function resolveAdapterPath() {
|
|
|
57
58
|
function buildWithAdapter(cwd) {
|
|
58
59
|
const adapterPath = resolveAdapterPath();
|
|
59
60
|
if (!adapterPath) {
|
|
60
|
-
consola.warn(" @solcreek/adapter-
|
|
61
|
+
consola.warn(" @solcreek/adapter-creek not found — install it for optimal Next.js builds");
|
|
61
62
|
return; // caller falls back to legacy
|
|
62
63
|
}
|
|
63
64
|
consola.start(" Building Next.js with Creek adapter...\n");
|
|
@@ -72,12 +73,15 @@ function buildWithAdapter(cwd) {
|
|
|
72
73
|
/**
|
|
73
74
|
* Unified Next.js build entry point.
|
|
74
75
|
*
|
|
75
|
-
* - Next.js >= 16.2: Creek adapter path (recommended)
|
|
76
|
-
* - Next.js < 16.2: legacy opennext path (best effort)
|
|
76
|
+
* - Next.js >= 16.2.3: Creek adapter path (recommended)
|
|
77
|
+
* - Next.js < 16.2.3: legacy opennext path (best effort)
|
|
78
|
+
*
|
|
79
|
+
* Min version for the adapter path matches @solcreek/adapter-creek's
|
|
80
|
+
* peerDependency, which pins Next.js >= 16.2.3 to fix CVE-2026-23869.
|
|
77
81
|
*/
|
|
78
82
|
export function buildNextjs(cwd, isMonorepo, projectName) {
|
|
79
83
|
const version = getNextVersion(cwd);
|
|
80
|
-
const useAdapter = version && semverGte(version, "16.2.
|
|
84
|
+
const useAdapter = version && semverGte(version, "16.2.3") && resolveAdapterPath();
|
|
81
85
|
if (useAdapter) {
|
|
82
86
|
buildWithAdapter(cwd);
|
|
83
87
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@solcreek/cli",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.4",
|
|
4
4
|
"description": "CLI for the Creek deployment platform",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -8,14 +8,9 @@
|
|
|
8
8
|
"dist",
|
|
9
9
|
"!dist/**/*.test.*",
|
|
10
10
|
"!dist/**/*.map",
|
|
11
|
+
"CHANGELOG.md",
|
|
11
12
|
"LICENSE"
|
|
12
13
|
],
|
|
13
|
-
"scripts": {
|
|
14
|
-
"build": "tsc",
|
|
15
|
-
"dev": "tsc --watch",
|
|
16
|
-
"typecheck": "tsc --noEmit",
|
|
17
|
-
"clean": "rm -rf dist"
|
|
18
|
-
},
|
|
19
14
|
"keywords": [
|
|
20
15
|
"creek",
|
|
21
16
|
"cli",
|
|
@@ -32,14 +27,14 @@
|
|
|
32
27
|
"directory": "packages/cli"
|
|
33
28
|
},
|
|
34
29
|
"dependencies": {
|
|
35
|
-
"@solcreek/sdk": "workspace:*",
|
|
36
30
|
"ajv": "^8.17.1",
|
|
37
31
|
"citty": "^0.1.6",
|
|
38
32
|
"consola": "^3.4.2",
|
|
39
33
|
"esbuild": "^0.25.0",
|
|
40
34
|
"miniflare": "^4.20260317.3",
|
|
41
35
|
"smol-toml": "^1.3.1",
|
|
42
|
-
"ws": "^8.20.0"
|
|
36
|
+
"ws": "^8.20.0",
|
|
37
|
+
"@solcreek/sdk": "0.4.1"
|
|
43
38
|
},
|
|
44
39
|
"devDependencies": {
|
|
45
40
|
"@testing-library/dom": "^10.4.1",
|
|
@@ -53,5 +48,11 @@
|
|
|
53
48
|
"react-dom": "^19.2.4",
|
|
54
49
|
"typescript": "^5.8.2",
|
|
55
50
|
"vite": "^6.3.5"
|
|
51
|
+
},
|
|
52
|
+
"scripts": {
|
|
53
|
+
"build": "tsc",
|
|
54
|
+
"dev": "tsc --watch",
|
|
55
|
+
"typecheck": "tsc --noEmit",
|
|
56
|
+
"clean": "rm -rf dist"
|
|
56
57
|
}
|
|
57
|
-
}
|
|
58
|
+
}
|