@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.
Files changed (43) 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/login.js +1 -1
  11. package/dist/commands/logs.d.ts +12 -0
  12. package/dist/commands/logs.js +69 -1
  13. package/dist/commands/restart.d.ts +26 -0
  14. package/dist/commands/restart.js +55 -0
  15. package/dist/commands/rollback.d.ts +13 -0
  16. package/dist/commands/rollback.js +188 -1
  17. package/dist/commands/stop.d.ts +26 -0
  18. package/dist/commands/stop.js +65 -0
  19. package/dist/commands/top.d.ts +28 -0
  20. package/dist/commands/top.js +171 -0
  21. package/dist/dev/creekd-runner.d.ts +22 -0
  22. package/dist/dev/creekd-runner.js +188 -0
  23. package/dist/index.js +8 -0
  24. package/dist/utils/auth-server.d.ts +2 -2
  25. package/dist/utils/auth-server.js +16 -3
  26. package/dist/utils/creekd-client.d.ts +152 -0
  27. package/dist/utils/creekd-client.js +144 -0
  28. package/dist/utils/gitignore.d.ts +2 -0
  29. package/dist/utils/gitignore.js +32 -0
  30. package/dist/utils/hostkey.d.ts +39 -0
  31. package/dist/utils/hostkey.js +84 -0
  32. package/dist/utils/hosts.d.ts +70 -0
  33. package/dist/utils/hosts.js +90 -0
  34. package/dist/utils/local-cache.d.ts +69 -0
  35. package/dist/utils/local-cache.js +100 -0
  36. package/dist/utils/nextjs.d.ts +4 -2
  37. package/dist/utils/nextjs.js +107 -38
  38. package/dist/utils/prepare-bundle.js +1 -1
  39. package/dist/utils/top-format.d.ts +4 -0
  40. package/dist/utils/top-format.js +32 -0
  41. package/dist/utils/watch.d.ts +81 -0
  42. package/dist/utils/watch.js +87 -0
  43. package/package.json +2 -2
package/CHANGELOG.md CHANGED
@@ -1,5 +1,26 @@
1
1
  # @solcreek/cli
2
2
 
3
+ ## 0.4.22
4
+
5
+ ### Fixes / DX
6
+
7
+ - **Next.js deploys now set up the Creek adapter automatically.** Deploying
8
+ a Next.js (≥ 16.2.3) project no longer requires installing or configuring
9
+ anything — `creek deploy` fetches and runs the adapter on first use.
10
+ Projects outside the Creek repo previously fell back to an older build
11
+ path without it.
12
+
13
+ - **`creek deploy` no longer publishes from a non-interactive shell without
14
+ `--yes`.** In CI, an AI coding agent, or a pipe there is no prompt to
15
+ confirm, so a bare `creek deploy` now refuses and points you at
16
+ `--dry-run` (preview the plan) or `--yes` (confirm and deploy) instead of
17
+ shipping on its own. Interactive (terminal) use is unchanged.
18
+
19
+ - **Clearer Next.js diagnostics.** `creek doctor` and `creek deploy
20
+ --dry-run` no longer tell you to run `next build` to produce output that
21
+ Creek generates itself at deploy time, and the reported build-output path
22
+ now matches what actually ships.
23
+
3
24
  ## 0.4.6
4
25
 
5
26
  ### Features
@@ -0,0 +1,21 @@
1
+ export declare const dashboardCommand: import("citty").CommandDef<{
2
+ json: {
3
+ type: "boolean";
4
+ description: string;
5
+ default: boolean;
6
+ };
7
+ yes: {
8
+ type: "boolean";
9
+ description: string;
10
+ default: boolean;
11
+ };
12
+ server: {
13
+ type: "string";
14
+ description: string;
15
+ };
16
+ token: {
17
+ type: "string";
18
+ description: string;
19
+ };
20
+ }>;
21
+ //# sourceMappingURL=dashboard.d.ts.map
@@ -0,0 +1,72 @@
1
+ import { defineCommand } from "citty";
2
+ import consola from "consola";
3
+ import { execSync } from "node:child_process";
4
+ import { globalArgs, resolveJsonMode, jsonOutput } from "../utils/output.js";
5
+ import { CreekdClient, CreekdApiError, getCreekdUrl } from "../utils/creekd-client.js";
6
+ export const dashboardCommand = defineCommand({
7
+ meta: {
8
+ name: "dashboard",
9
+ description: "Open the creekd web dashboard in your browser",
10
+ },
11
+ args: {
12
+ server: {
13
+ type: "string",
14
+ description: "creekd admin API URL (or $CREEKD_URL)",
15
+ },
16
+ token: {
17
+ type: "string",
18
+ description: "Bearer token (or $CREEKD_TOKEN)",
19
+ },
20
+ ...globalArgs,
21
+ },
22
+ async run({ args }) {
23
+ const jsonMode = resolveJsonMode(args);
24
+ const url = args.server || getCreekdUrl();
25
+ const client = new CreekdClient(url, args.token || process.env.CREEKD_TOKEN || process.env.CREEKCTL_TOKEN || "");
26
+ // Verify creekd is reachable
27
+ try {
28
+ await client.listApps();
29
+ }
30
+ catch (err) {
31
+ if (err instanceof CreekdApiError && err.status === 401) {
32
+ if (jsonMode)
33
+ jsonOutput({ ok: false, error: "unauthorized", message: "Authentication required" }, 1);
34
+ consola.error("Authentication failed. Set CREEKD_TOKEN or use --token.");
35
+ process.exit(1);
36
+ }
37
+ if (jsonMode)
38
+ jsonOutput({ ok: false, error: "unreachable", message: `Cannot reach creekd at ${url}` }, 1, [
39
+ { command: "creek top --server <url>", description: "Check connection" },
40
+ ]);
41
+ consola.error(`Cannot reach creekd at ${url}`);
42
+ consola.info("Is creekd running? Start it with: creekd");
43
+ process.exit(1);
44
+ }
45
+ // In production, dashboard is served at the same URL as creekd (via Caddy).
46
+ // In dev, it's typically at localhost:3000 (Vite).
47
+ const dashboardUrl = url.includes(":9080")
48
+ ? url.replace(":9080", ":3000")
49
+ : url;
50
+ if (jsonMode) {
51
+ jsonOutput({ ok: true, url: dashboardUrl }, 0, [
52
+ { command: "creek top", description: "CLI monitoring alternative" },
53
+ ]);
54
+ }
55
+ consola.success(`Opening dashboard: ${dashboardUrl}`);
56
+ openBrowser(dashboardUrl);
57
+ },
58
+ });
59
+ function openBrowser(url) {
60
+ try {
61
+ const cmd = process.platform === "darwin"
62
+ ? `open "${url}"`
63
+ : process.platform === "win32"
64
+ ? `start "" "${url}"`
65
+ : `xdg-open "${url}"`;
66
+ execSync(cmd, { stdio: "ignore" });
67
+ }
68
+ catch {
69
+ consola.info(`Open manually: ${url}`);
70
+ }
71
+ }
72
+ //# sourceMappingURL=dashboard.js.map
@@ -29,6 +29,16 @@ export declare const deployCommand: import("citty").CommandDef<{
29
29
  description: string;
30
30
  required: false;
31
31
  };
32
+ watch: {
33
+ type: "boolean";
34
+ description: string;
35
+ default: false;
36
+ };
37
+ "watch-timeout-ms": {
38
+ type: "string";
39
+ description: string;
40
+ required: false;
41
+ };
32
42
  json: {
33
43
  type: "boolean";
34
44
  description: string;
@@ -16,6 +16,8 @@ import { ensureTosAccepted } from "../utils/tos.js";
16
16
  import { hasAdapterOutput } from "../utils/nextjs.js";
17
17
  import { isRepoUrl, parseRepoUrl, validateRepoUrl, validateSubpath, RepoUrlError } from "../utils/repo-url.js";
18
18
  import { checkGitInstalled, cloneRepo, detectPackageManager, installDependencies, cleanupDir as cleanupCloneDir, GitCloneError } from "../utils/git-clone.js";
19
+ import { CreekdClient, getCreekdUrl } from "../utils/creekd-client.js";
20
+ import { watchDeploy } from "../utils/watch.js";
19
21
  function section(name) {
20
22
  consola.log(`\n \x1b[2m[${name}]\x1b[0m`);
21
23
  }
@@ -243,6 +245,16 @@ export const deployCommand = defineCommand({
243
245
  description: "Target project slug or UUID. Required with --from-github when not run inside a project directory.",
244
246
  required: false,
245
247
  },
248
+ watch: {
249
+ type: "boolean",
250
+ description: "After deploy lands, poll status.conditions[] until Ready=True or Degraded reason=DeployTimeout (self-host creekd target only). Default poll = 1s, default budget = 5min.",
251
+ default: false,
252
+ },
253
+ "watch-timeout-ms": {
254
+ type: "string",
255
+ description: "Client-side watch budget in milliseconds (default 300000 = 5min). Independent of the daemon's progressing_timeout.",
256
+ required: false,
257
+ },
246
258
  },
247
259
  async run({ args }) {
248
260
  const jsonMode = resolveJsonMode(args);
@@ -256,6 +268,27 @@ export const deployCommand = defineCommand({
256
268
  : process.cwd();
257
269
  return await dryRunPlan(dryCwd, args, jsonMode);
258
270
  }
271
+ // --- Safety gate: no silent deploys in non-interactive environments ---
272
+ // Deploying publishes to a public URL — irreversible enough that we
273
+ // don't want it to happen by accident. In a TTY a human is present to
274
+ // see and abort; in a non-TTY (AI agent, CI, pipe) there is no prompt,
275
+ // so a bare `creek deploy` would otherwise just ship. Require an
276
+ // explicit --yes there instead. This is what makes the "safe to run
277
+ // from an AI coding agent" promise true: a bare call refuses and points
278
+ // at --dry-run / --yes rather than publishing on its own. Documented CI
279
+ // already passes --yes, so this doesn't regress automated deploys.
280
+ if (!isTTY && !args.yes) {
281
+ const breadcrumbs = [
282
+ { command: "creek deploy --dry-run", description: "Preview the plan without executing (no network, no uploads)" },
283
+ { command: "creek deploy --yes", description: "Confirm and deploy without an interactive prompt" },
284
+ ];
285
+ const message = "Refusing to deploy from a non-interactive environment without confirmation. Re-run with --dry-run to preview the plan, or --yes to deploy.";
286
+ if (jsonMode) {
287
+ jsonOutput({ ok: false, error: "confirmation_required", message }, 1, breadcrumbs);
288
+ }
289
+ consola.error(message);
290
+ process.exit(1);
291
+ }
259
292
  // --- Ensure ToS accepted ---
260
293
  const autoConfirm = shouldAutoConfirm(args);
261
294
  const tos = await ensureTosAccepted(autoConfirm);
@@ -318,6 +351,13 @@ export const deployCommand = defineCommand({
318
351
  consola.warn(` Binding '${ub.name}' (${ub.type}) is not yet supported — will be skipped`);
319
352
  }
320
353
  }
354
+ // creekd target → deploy to local creekd via admin API
355
+ if (resolved.target === "creekd") {
356
+ return await deployCreekd(cwd, resolved, args["skip-build"], jsonMode, {
357
+ watch: args.watch === true,
358
+ watchTimeoutMs: parseWatchTimeoutMs(args["watch-timeout-ms"]),
359
+ });
360
+ }
321
361
  if (token) {
322
362
  return await deployAuthenticated(cwd, resolved, token, args["skip-build"], jsonMode, args["no-cache"]);
323
363
  }
@@ -344,6 +384,203 @@ export const deployCommand = defineCommand({
344
384
  process.exit(1);
345
385
  },
346
386
  });
387
+ async function deployCreekd(cwd, resolved, skipBuild, jsonMode, watchOpts = { watch: false }) {
388
+ const client = new CreekdClient();
389
+ // 1. Verify creekd is reachable
390
+ try {
391
+ await client.listApps();
392
+ }
393
+ catch {
394
+ const msg = `Cannot reach creekd at ${getCreekdUrl()}. Is it running?`;
395
+ if (jsonMode)
396
+ jsonOutput({ error: "creekd_unreachable", message: msg }, 1, [
397
+ { command: "creekd", description: "Start the daemon" },
398
+ ]);
399
+ consola.error(msg);
400
+ process.exit(1);
401
+ }
402
+ if (!jsonMode) {
403
+ section("Detect");
404
+ consola.info(` Target: creekd (local)`);
405
+ consola.info(` Project: ${resolved.projectName}`);
406
+ if (resolved.framework)
407
+ consola.info(` Framework: ${resolved.framework}`);
408
+ }
409
+ // 2. Build (unless skipped)
410
+ if (!skipBuild && resolved.buildCommand) {
411
+ if (!jsonMode) {
412
+ section("Build");
413
+ consola.start(` ${resolved.buildCommand}`);
414
+ }
415
+ try {
416
+ execSync(resolved.buildCommand, { cwd, stdio: jsonMode ? "pipe" : "inherit" });
417
+ if (!jsonMode)
418
+ consola.success(" Build complete");
419
+ }
420
+ catch (e) {
421
+ const msg = `Build failed: ${resolved.buildCommand}`;
422
+ if (jsonMode)
423
+ jsonOutput({ ok: false, error: "build_failed", message: msg }, 1, []);
424
+ consola.error(msg);
425
+ process.exit(1);
426
+ }
427
+ }
428
+ // 3. Detect entry point + runtime
429
+ const entryPoint = resolved.workerEntry ?? resolved.buildOutput + "/index.js";
430
+ const runtime = "bun"; // default; could read from creek.toml [app].runtime
431
+ const port = 3000; // TODO: auto-assign or read from config
432
+ // 4. Deploy via CreekdClient (idempotent: spawn if absent, deploy if running)
433
+ if (!jsonMode) {
434
+ section("Deploy");
435
+ consola.start(" Deploying to creekd...");
436
+ }
437
+ const startTime = Date.now();
438
+ try {
439
+ const spawnReq = { id: resolved.projectName, runtime, entry: entryPoint, port };
440
+ let app;
441
+ try {
442
+ app = await client.spawnApp(spawnReq);
443
+ }
444
+ catch (e) {
445
+ if (e.code === "already_running") {
446
+ app = await client.deployApp(resolved.projectName, { runtime, entry: entryPoint, port });
447
+ }
448
+ else {
449
+ throw e;
450
+ }
451
+ }
452
+ // Release phase (pre-traffic command, e.g. migrations)
453
+ if (resolved.releaseCommand) {
454
+ if (!jsonMode) {
455
+ section("Release");
456
+ consola.start(` ${resolved.releaseCommand}`);
457
+ }
458
+ try {
459
+ execSync(resolved.releaseCommand, {
460
+ cwd,
461
+ stdio: jsonMode ? "pipe" : "inherit",
462
+ timeout: (resolved.releaseTimeout ?? 300) * 1000,
463
+ });
464
+ if (!jsonMode)
465
+ consola.success(" Release complete");
466
+ }
467
+ catch (e) {
468
+ const msg = e.killed
469
+ ? `Release command timed out after ${resolved.releaseTimeout ?? 300}s`
470
+ : `Release command failed: ${e.stderr?.toString() || e.message}`;
471
+ if (jsonMode)
472
+ jsonOutput({ ok: false, error: "release_failed", message: msg }, 1);
473
+ consola.error(msg);
474
+ try {
475
+ await client.stopApp(resolved.projectName);
476
+ }
477
+ catch { }
478
+ process.exit(1);
479
+ }
480
+ }
481
+ const elapsedS = ((Date.now() - startTime) / 1000).toFixed(1);
482
+ // --watch — poll status.conditions[] until Ready or
483
+ // deploy_stuck. Runs AFTER `creekctl ensure` returns success
484
+ // so the daemon already has the spec; we're observing the
485
+ // supervisor's convergence. Skipped when --watch is off.
486
+ let watchSummary;
487
+ if (watchOpts.watch) {
488
+ if (!jsonMode) {
489
+ section("Watch");
490
+ consola.start(" Polling status.conditions[]...");
491
+ }
492
+ watchSummary = await watchDeploy(new CreekdClient(), resolved.projectName, {
493
+ timeoutMs: watchOpts.watchTimeoutMs,
494
+ onPoll: jsonMode
495
+ ? undefined
496
+ : (envelope) => {
497
+ const conds = envelope.status?.conditions ?? [];
498
+ const summary = conds.map((c) => `${c.type}=${c.status}`).join(" ");
499
+ consola.info(` ${summary}`);
500
+ },
501
+ });
502
+ }
503
+ if (jsonMode) {
504
+ jsonOutput({
505
+ ok: watchSummary ? watchSummary.ok : true,
506
+ url: `http://localhost:${port}`,
507
+ appId: resolved.projectName,
508
+ port,
509
+ runtime,
510
+ target: "creekd",
511
+ buildTimeS: parseFloat(elapsedS),
512
+ mode: "creekd-local",
513
+ ...(watchSummary ? { watch: watchSummaryWire(watchSummary) } : {}),
514
+ }, watchSummary && !watchSummary.ok ? 1 : 0, [
515
+ { command: `creek top ${resolved.projectName}`, description: "Check app status" },
516
+ { command: `creek logs --server ${resolved.projectName}`, description: "View app logs" },
517
+ { command: `creek exec -- bun run seed.ts`, description: "Run one-off command" },
518
+ ]);
519
+ }
520
+ else {
521
+ if (watchSummary && !watchSummary.ok) {
522
+ consola.error(` Watch failed: ${watchSummary.reason}`);
523
+ if (watchSummary.reason === "deploy_stuck") {
524
+ consola.info(" The daemon flipped Degraded reason=DeployTimeout — supervisor did not converge.");
525
+ consola.info(` creek logs --server ${resolved.projectName} Inspect why`);
526
+ }
527
+ else if (watchSummary.reason === "watch_timeout") {
528
+ consola.info(` Client gave up after ${watchSummary.elapsedMs}ms. The daemon may still converge.`);
529
+ }
530
+ else if (watchSummary.reason === "fetch_failed") {
531
+ consola.info(` Polling failed: ${watchSummary.error.message}`);
532
+ }
533
+ process.exit(1);
534
+ }
535
+ consola.success(` Deployed to creekd (${elapsedS}s)`);
536
+ consola.info(` http://localhost:${port}`);
537
+ console.log("");
538
+ consola.info(" Next steps:");
539
+ consola.info(` creek top ${resolved.projectName} Check status`);
540
+ consola.info(` creek logs --server ${resolved.projectName} View logs`);
541
+ consola.info(` creekctl events ${resolved.projectName} Stream events`);
542
+ }
543
+ }
544
+ catch (e) {
545
+ const output = e.stdout?.toString() || e.stderr?.toString() || e.message;
546
+ const msg = `creekctl deploy failed: ${output}`;
547
+ if (jsonMode)
548
+ jsonOutput({ ok: false, error: "creekd_deploy_failed", message: msg }, 1, []);
549
+ consola.error(msg);
550
+ process.exit(1);
551
+ }
552
+ }
553
+ /** Parse the --watch-timeout-ms flag. Empty / unset → undefined
554
+ * so watchDeploy applies its own default. Non-numeric → exits. */
555
+ function parseWatchTimeoutMs(raw) {
556
+ if (raw === undefined || raw === null || raw === "")
557
+ return undefined;
558
+ const s = String(raw);
559
+ if (!/^\d+$/.test(s)) {
560
+ consola.error(`--watch-timeout-ms must be a positive integer (got "${raw}")`);
561
+ process.exit(1);
562
+ }
563
+ const n = Number.parseInt(s, 10);
564
+ if (!Number.isFinite(n) || n <= 0) {
565
+ consola.error(`--watch-timeout-ms must be a positive integer (got "${raw}")`);
566
+ process.exit(1);
567
+ }
568
+ return n;
569
+ }
570
+ /** Pluck the JSON-friendly fields out of a WatchResult — strips
571
+ * the AppEnvelope which is too noisy for the deploy summary. */
572
+ function watchSummaryWire(r) {
573
+ if (r.ok)
574
+ return { ok: true, reason: r.reason };
575
+ if (r.reason === "deploy_stuck") {
576
+ const conds = r.envelope.status?.conditions ?? [];
577
+ return { ok: false, reason: r.reason, conditions: conds };
578
+ }
579
+ if (r.reason === "watch_timeout") {
580
+ return { ok: false, reason: r.reason, elapsedMs: r.elapsedMs };
581
+ }
582
+ return { ok: false, reason: r.reason, error: r.error.message };
583
+ }
347
584
  export const CLI_TERMINAL_STATUSES = new Set(["active", "failed", "cancelled"]);
348
585
  export const CLI_IN_FLIGHT_STATUSES = new Set([
349
586
  "queued",
@@ -514,12 +751,18 @@ async function deployFromGithub(options) {
514
751
  // Report the outcome
515
752
  if (targetDeployment.status === "active") {
516
753
  if (jsonMode) {
754
+ const elapsedS = ((Date.now() - startedAt) / 1000).toFixed(1);
517
755
  jsonOutput({
518
756
  ok: true,
519
757
  deploymentId: targetDeployment.id,
520
758
  version: targetDeployment.version,
521
759
  status: "active",
522
760
  url: targetDeployment.url,
761
+ branch: targetDeployment.branch,
762
+ buildTimeS: parseFloat(elapsedS),
763
+ mode: "production",
764
+ source: "github",
765
+ rollbackCommand: `creek rollback --project ${projectSlug}`,
523
766
  }, 0, NO_PROJECT_BREADCRUMBS);
524
767
  }
525
768
  else {
@@ -777,9 +1020,11 @@ async function deploySandbox(cwd, skipBuild, jsonMode = false, resolved, tos) {
777
1020
  sandboxId: result.sandboxId,
778
1021
  url: status.previewUrl,
779
1022
  deployDurationMs: status.deployDurationMs,
1023
+ buildTimeS: status.deployDurationMs ? parseFloat((status.deployDurationMs / 1000).toFixed(1)) : null,
780
1024
  expiresAt: result.expiresAt,
781
1025
  expiresInMinutes: expiresInMinutes(result.expiresAt),
782
1026
  framework: framework ?? null,
1027
+ renderMode: effectiveRenderMode,
783
1028
  assetCount: fileList.length,
784
1029
  mode: "sandbox",
785
1030
  }, 0, [
@@ -1062,13 +1307,20 @@ async function deployAuthenticated(cwd, resolved, token, skipBuild, jsonMode = f
1062
1307
  // Silent — build log is best-effort for now.
1063
1308
  });
1064
1309
  if (jsonMode) {
1310
+ const elapsedS = ((Date.now() - start) / 1000).toFixed(1);
1065
1311
  jsonOutput({
1066
1312
  ok: true,
1067
1313
  url: res.url ?? res.previewUrl,
1068
1314
  previewUrl: res.previewUrl,
1069
1315
  deploymentId: deployment.id,
1316
+ version: res.deployment?.version ?? null,
1070
1317
  project: project.slug,
1318
+ framework: framework ?? null,
1319
+ renderMode: effectiveRenderMode,
1320
+ assetCount: fileList.length,
1321
+ buildTimeS: parseFloat(elapsedS),
1071
1322
  mode: "production",
1323
+ rollbackCommand: `creek rollback --project ${project.slug}`,
1072
1324
  ...(resolved.cron.length > 0 ? { cron: resolved.cron } : {}),
1073
1325
  }, 0, [
1074
1326
  { command: `creek status`, description: "Check deployment status" },
@@ -4,10 +4,23 @@ export declare const devCommand: import("citty").CommandDef<{
4
4
  description: string;
5
5
  default: string;
6
6
  };
7
+ target: {
8
+ type: "string";
9
+ description: string;
10
+ required: false;
11
+ };
7
12
  reset: {
8
13
  type: "boolean";
9
14
  description: string;
10
15
  };
16
+ dashboard: {
17
+ type: "boolean";
18
+ description: string;
19
+ };
20
+ top: {
21
+ type: "boolean";
22
+ description: string;
23
+ };
11
24
  json: {
12
25
  type: "boolean";
13
26
  description: string;
@@ -1,12 +1,13 @@
1
1
  import { defineCommand } from "citty";
2
2
  import { consola } from "consola";
3
- import { resolveConfig, formatDetectionSummary } from "@solcreek/sdk";
3
+ import { execSync } from "node:child_process";
4
+ import { resolveConfig, formatDetectionSummary, DEPLOY_TARGETS } from "@solcreek/sdk";
4
5
  import { globalArgs } from "../utils/output.js";
5
6
  import { DevServer } from "../dev/server.js";
6
7
  export const devCommand = defineCommand({
7
8
  meta: {
8
9
  name: "dev",
9
- description: "Start local development server",
10
+ description: "Start local development server. Auto-detects target from creek.toml (cf = Miniflare, creekd = sandbox VM with real Postgres/Redis).",
10
11
  },
11
12
  args: {
12
13
  ...globalArgs,
@@ -15,10 +16,23 @@ export const devCommand = defineCommand({
15
16
  description: "Port number (default: 3000)",
16
17
  default: "3000",
17
18
  },
19
+ target: {
20
+ type: "string",
21
+ description: "Deploy target override: cf (Miniflare) or creekd (sandbox VM)",
22
+ required: false,
23
+ },
18
24
  reset: {
19
25
  type: "boolean",
20
26
  description: "Clear local data before starting",
21
27
  },
28
+ dashboard: {
29
+ type: "boolean",
30
+ description: "Open the web dashboard after server starts",
31
+ },
32
+ top: {
33
+ type: "boolean",
34
+ description: "Show creek top alongside the dev server",
35
+ },
22
36
  },
23
37
  async run({ args }) {
24
38
  const cwd = process.cwd();
@@ -35,7 +49,36 @@ export const devCommand = defineCommand({
35
49
  consola.error(`Invalid port: ${args.port}`);
36
50
  process.exit(1);
37
51
  }
52
+ // Target: CLI flag > creek.toml > auto-detect (already resolved)
53
+ const target = args.target ?? config.target;
54
+ if (args.target && !DEPLOY_TARGETS.includes(args.target)) {
55
+ consola.error(`Invalid target "${args.target}". Valid: ${DEPLOY_TARGETS.join(", ")}`);
56
+ process.exit(1);
57
+ }
38
58
  consola.info(`Detected: ${formatDetectionSummary(config)}`);
59
+ consola.info(`Target: ${target}${args.target ? " (--target override)" : " (from config)"}`);
60
+ if (target === "creekd") {
61
+ const { CreekdDevServer } = await import("../dev/creekd-runner.js");
62
+ const server = new CreekdDevServer({ cwd, port, config, reset: !!args.reset });
63
+ const shutdown = async () => {
64
+ consola.info("Shutting down...");
65
+ await server.stop();
66
+ process.exit(0);
67
+ };
68
+ process.on("SIGINT", shutdown);
69
+ process.on("SIGTERM", shutdown);
70
+ try {
71
+ await server.start();
72
+ }
73
+ catch (e) {
74
+ consola.error(`Failed to start creekd dev server: ${e.message}`);
75
+ await server.stop();
76
+ process.exit(1);
77
+ }
78
+ afterStart(args, port);
79
+ return;
80
+ }
81
+ // CF Workers path (existing)
39
82
  const server = new DevServer({
40
83
  cwd,
41
84
  port,
@@ -58,6 +101,7 @@ export const devCommand = defineCommand({
58
101
  await server.stop();
59
102
  process.exit(1);
60
103
  }
104
+ afterStart(args, port);
61
105
  // Interactive trigger commands (only if cron/queue configured)
62
106
  if (config.cron.length > 0 || config.queue) {
63
107
  const { createInterface } = await import("node:readline");
@@ -99,4 +143,35 @@ export const devCommand = defineCommand({
99
143
  }
100
144
  },
101
145
  });
146
+ function afterStart(args, port) {
147
+ if (args.dashboard) {
148
+ const url = `http://localhost:${port}`;
149
+ consola.info(`Opening dashboard: ${url}`);
150
+ openBrowser(url);
151
+ }
152
+ if (args.top) {
153
+ import("../utils/creekd-client.js").then(({ getCreekdUrl }) => {
154
+ const topUrl = getCreekdUrl();
155
+ consola.info(`Starting creek top (${topUrl})...`);
156
+ import("node:child_process").then(({ spawn }) => {
157
+ spawn(process.execPath, [process.argv[1], "top", "--server", topUrl], {
158
+ stdio: "inherit",
159
+ });
160
+ });
161
+ });
162
+ }
163
+ }
164
+ function openBrowser(url) {
165
+ try {
166
+ const cmd = process.platform === "darwin"
167
+ ? `open "${url}"`
168
+ : process.platform === "win32"
169
+ ? `start "" "${url}"`
170
+ : `xdg-open "${url}"`;
171
+ execSync(cmd, { stdio: "ignore" });
172
+ }
173
+ catch {
174
+ consola.info(`Open manually: ${url}`);
175
+ }
176
+ }
102
177
  //# sourceMappingURL=dev.js.map
@@ -14,5 +14,15 @@ export declare const initCommand: import("citty").CommandDef<{
14
14
  description: string;
15
15
  required: false;
16
16
  };
17
+ adopt: {
18
+ type: "string";
19
+ description: string;
20
+ required: false;
21
+ };
22
+ "hostkey-fingerprint": {
23
+ type: "string";
24
+ description: string;
25
+ required: false;
26
+ };
17
27
  }>;
18
28
  //# sourceMappingURL=init.d.ts.map