@solcreek/cli 0.3.4 → 0.3.5

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.
@@ -2,7 +2,7 @@ import { defineCommand } from "citty";
2
2
  import consola from "consola";
3
3
  import { CreekClient } from "@solcreek/sdk";
4
4
  import { getToken, getApiUrl, getSandboxApiUrl } from "../utils/config.js";
5
- import { globalArgs, resolveJsonMode, jsonOutput } from "../utils/output.js";
5
+ import { globalArgs, resolveJsonMode, jsonOutput, AUTH_BREADCRUMBS } from "../utils/output.js";
6
6
  export const claimCommand = defineCommand({
7
7
  meta: {
8
8
  name: "claim",
@@ -21,7 +21,7 @@ export const claimCommand = defineCommand({
21
21
  const token = getToken();
22
22
  if (!token) {
23
23
  if (jsonMode)
24
- jsonOutput({ ok: false, error: "not_authenticated", message: "Run `creek login` first" }, 1);
24
+ jsonOutput({ ok: false, error: "not_authenticated", message: "Run `creek login` first" }, 1, AUTH_BREADCRUMBS);
25
25
  consola.error("You need to be logged in to claim a sandbox.");
26
26
  consola.info("Run `creek login` first, then `creek claim` again.");
27
27
  process.exit(1);
@@ -41,7 +41,9 @@ export const claimCommand = defineCommand({
41
41
  ? "This sandbox has expired and can no longer be claimed."
42
42
  : `Sandbox is in '${sandbox.status}' state and cannot be claimed.`;
43
43
  if (jsonMode)
44
- jsonOutput({ ok: false, error: "not_claimable", status: sandbox.status, message: msg }, 1);
44
+ jsonOutput({ ok: false, error: "not_claimable", status: sandbox.status, message: msg }, 1, [
45
+ { command: "creek deploy", description: "Deploy your project permanently" },
46
+ ]);
45
47
  consola.error(msg);
46
48
  if (sandbox.status === "expired")
47
49
  consola.info("Run `creek deploy` to deploy your project permanently.");
@@ -80,7 +82,10 @@ export const claimCommand = defineCommand({
80
82
  // Best effort — claim status update is non-critical
81
83
  }
82
84
  if (jsonMode) {
83
- jsonOutput({ ok: true, sandboxId, project: project.slug, projectId: project.id });
85
+ jsonOutput({ ok: true, sandboxId, project: project.slug, projectId: project.id }, 0, [
86
+ { command: "creek init", description: "Initialize creek.toml for local development" },
87
+ { command: "creek deploy", description: "Deploy permanently" },
88
+ ]);
84
89
  }
85
90
  consola.success("Sandbox claimed!");
86
91
  consola.info("");
@@ -9,7 +9,7 @@ import { collectAssets } from "../utils/bundle.js";
9
9
  import { bundleSSRServer } from "../utils/ssr-bundle.js";
10
10
  import { bundleWorker } from "../utils/worker-bundle.js";
11
11
  import { sandboxDeploy, pollSandboxStatus, printSandboxSuccess } from "../utils/sandbox.js";
12
- import { isTTY, jsonOutput, resolveJsonMode, globalArgs, shouldAutoConfirm } from "../utils/output.js";
12
+ import { isTTY, jsonOutput, resolveJsonMode, globalArgs, shouldAutoConfirm, NO_PROJECT_BREADCRUMBS } from "../utils/output.js";
13
13
  import { ensureTosAccepted } from "../utils/tos.js";
14
14
  import { buildNextjs, patchBundledWorker, hasAdapterOutput } from "../utils/nextjs.js";
15
15
  import { isRepoUrl, parseRepoUrl, validateRepoUrl, validateSubpath, RepoUrlError } from "../utils/repo-url.js";
@@ -90,7 +90,7 @@ export const deployCommand = defineCommand({
90
90
  if (args.dir) {
91
91
  if (!existsSync(cwd)) {
92
92
  if (jsonMode)
93
- jsonOutput({ error: "not_found", message: `Directory not found: ${args.dir}` }, 1);
93
+ jsonOutput({ error: "not_found", message: `Directory not found: ${args.dir}` }, 1, NO_PROJECT_BREADCRUMBS);
94
94
  consola.error(`Directory not found: ${args.dir}`);
95
95
  process.exit(1);
96
96
  }
@@ -128,7 +128,7 @@ export const deployCommand = defineCommand({
128
128
  }
129
129
  // --- Nothing found → guide the user ---
130
130
  if (jsonMode)
131
- jsonOutput({ error: "no_project", message: "No project found in this directory" }, 1);
131
+ jsonOutput({ error: "no_project", message: "No project found in this directory" }, 1, NO_PROJECT_BREADCRUMBS);
132
132
  consola.info("No project found in this directory.\n");
133
133
  consola.info(" creek deploy ./dist Deploy a build output directory");
134
134
  consola.info(" creek deploy --demo Deploy a sample site (see Creek in 5 seconds)");
@@ -198,7 +198,9 @@ async function deployRepoUrl(input, options) {
198
198
  catch (err) {
199
199
  if (err instanceof RepoUrlError || err instanceof GitCloneError) {
200
200
  if (jsonMode)
201
- jsonOutput({ error: "repo_deploy_failed", message: err.message }, 1);
201
+ jsonOutput({ error: "repo_deploy_failed", message: err.message }, 1, [
202
+ { command: "creek deploy --demo", description: "Deploy a sample site instead" },
203
+ ]);
202
204
  consola.error(err.message);
203
205
  process.exit(1);
204
206
  }
@@ -260,12 +262,15 @@ async function deployDemo(jsonMode) {
260
262
  deployDurationMs: status.deployDurationMs,
261
263
  expiresAt: result.expiresAt,
262
264
  mode: "demo",
263
- });
265
+ }, 0, [
266
+ { command: `creek status ${result.sandboxId}`, description: "Check sandbox status" },
267
+ { command: `creek claim ${result.sandboxId}`, description: "Claim as permanent project" },
268
+ ]);
264
269
  }
265
270
  const duration = status.deployDurationMs
266
271
  ? `in ${(status.deployDurationMs / 1000).toFixed(1)}s`
267
272
  : "in seconds";
268
- consola.success(`Live ${duration} → ${status.previewUrl}`);
273
+ consola.success(`⬡ Live ${duration} → ${status.previewUrl}`);
269
274
  consola.info("");
270
275
  consola.info("That's Creek. Now deploy your own project:");
271
276
  consola.info(" cd your-project && npx creek deploy");
@@ -273,7 +278,9 @@ async function deployDemo(jsonMode) {
273
278
  catch (err) {
274
279
  const message = err instanceof Error ? err.message : "Demo deploy failed";
275
280
  if (jsonMode)
276
- jsonOutput({ ok: false, error: "deploy_failed", message }, 1);
281
+ jsonOutput({ ok: false, error: "deploy_failed", message }, 1, [
282
+ { command: "creek deploy --demo", description: "Retry demo deploy" },
283
+ ]);
277
284
  consola.error(message);
278
285
  process.exit(1);
279
286
  }
@@ -287,7 +294,7 @@ async function deployDirectory(dir, jsonMode, tos) {
287
294
  const { assets, fileList } = collectAssets(dir);
288
295
  if (fileList.length === 0) {
289
296
  if (jsonMode)
290
- jsonOutput({ ok: false, error: "no_files", message: `No files found in ${dir}` }, 1);
297
+ jsonOutput({ ok: false, error: "no_files", message: `No files found in ${dir}` }, 1, NO_PROJECT_BREADCRUMBS);
291
298
  consola.error(`No files found in ${dir}\n`);
292
299
  consola.info(" creek deploy ./dist Deploy a build output directory");
293
300
  consola.info(" creek deploy --demo Deploy a sample site (see Creek in 5 seconds)");
@@ -311,14 +318,19 @@ async function deployDirectory(dir, jsonMode, tos) {
311
318
  expiresAt: result.expiresAt,
312
319
  assetCount: fileList.length,
313
320
  mode: "sandbox",
314
- });
321
+ }, 0, [
322
+ { command: `creek status ${result.sandboxId}`, description: "Check sandbox status" },
323
+ { command: `creek claim ${result.sandboxId}`, description: "Claim as permanent project" },
324
+ ]);
315
325
  }
316
326
  printSandboxSuccess(status.previewUrl, result.expiresAt, result.sandboxId);
317
327
  }
318
328
  catch (err) {
319
329
  const message = err instanceof Error ? err.message : "Deploy failed";
320
330
  if (jsonMode)
321
- jsonOutput({ ok: false, error: "deploy_failed", message }, 1);
331
+ jsonOutput({ ok: false, error: "deploy_failed", message }, 1, [
332
+ { command: "creek deploy", description: "Retry deploy" },
333
+ ]);
322
334
  consola.error(message);
323
335
  process.exit(1);
324
336
  }
@@ -438,14 +450,19 @@ async function deploySandbox(cwd, skipBuild, jsonMode = false, resolved, tos) {
438
450
  framework: framework ?? null,
439
451
  assetCount: fileList.length,
440
452
  mode: "sandbox",
441
- });
453
+ }, 0, [
454
+ { command: `creek status ${result.sandboxId}`, description: "Check sandbox status" },
455
+ { command: `creek claim ${result.sandboxId}`, description: "Claim as permanent project" },
456
+ ]);
442
457
  }
443
458
  printSandboxSuccess(status.previewUrl, result.expiresAt, result.sandboxId);
444
459
  }
445
460
  catch (err) {
446
461
  const message = err instanceof Error ? err.message : "Sandbox deploy failed";
447
462
  if (jsonMode)
448
- jsonOutput({ ok: false, error: "deploy_failed", message }, 1);
463
+ jsonOutput({ ok: false, error: "deploy_failed", message }, 1, [
464
+ { command: "creek deploy", description: "Retry deploy" },
465
+ ]);
449
466
  consola.error(message);
450
467
  process.exit(1);
451
468
  }
@@ -787,9 +804,13 @@ async function deployAuthenticated(cwd, resolved, token, skipBuild, jsonMode = f
787
804
  deploymentId: deployment.id,
788
805
  project: project.slug,
789
806
  mode: "production",
790
- });
807
+ }, 0, [
808
+ { command: `creek status`, description: "Check deployment status" },
809
+ { command: `creek deployments --project ${project.slug}`, description: "View deployment history" },
810
+ { command: `creek domains add <HOSTNAME> --project ${project.slug}`, description: "Add custom domain" },
811
+ ]);
791
812
  }
792
- consola.success(` Deployed! ${res.url ?? res.previewUrl}`);
813
+ consola.success(` Deployed! ${res.url ?? res.previewUrl}`);
793
814
  if (res.url && res.previewUrl) {
794
815
  consola.info(` Preview: ${res.previewUrl}`);
795
816
  }
@@ -799,20 +820,28 @@ async function deployAuthenticated(cwd, resolved, token, skipBuild, jsonMode = f
799
820
  const step = failed_step ? ` at ${failed_step}` : "";
800
821
  const msg = error_message ?? "Unknown error";
801
822
  if (jsonMode)
802
- jsonOutput({ ok: false, error: "deploy_failed", message: msg, failedStep: failed_step }, 1);
823
+ jsonOutput({ ok: false, error: "deploy_failed", message: msg, failedStep: failed_step }, 1, [
824
+ { command: `creek deployments --project ${project.slug}`, description: "Check previous deployments" },
825
+ { command: `creek rollback --project ${project.slug}`, description: "Rollback to previous version" },
826
+ ]);
803
827
  consola.error(`Deploy failed${step}: ${msg}`);
804
828
  process.exit(1);
805
829
  }
806
830
  if (status === "cancelled") {
807
831
  if (jsonMode)
808
- jsonOutput({ ok: false, error: "cancelled", message: "Deploy was cancelled" }, 1);
832
+ jsonOutput({ ok: false, error: "cancelled", message: "Deploy was cancelled" }, 1, [
833
+ { command: "creek deploy", description: "Retry deploy" },
834
+ ]);
809
835
  consola.warn("Deploy was cancelled");
810
836
  process.exit(1);
811
837
  }
812
838
  await new Promise((r) => setTimeout(r, POLL_INTERVAL));
813
839
  }
814
840
  if (jsonMode)
815
- jsonOutput({ ok: false, error: "timeout", message: "Deploy timed out after 2 minutes" }, 1);
841
+ jsonOutput({ ok: false, error: "timeout", message: "Deploy timed out after 2 minutes" }, 1, [
842
+ { command: `creek status`, description: "Check if deploy completed" },
843
+ { command: "creek deploy", description: "Retry deploy" },
844
+ ]);
816
845
  consola.error("Deploy timed out after 2 minutes");
817
846
  process.exit(1);
818
847
  }
@@ -5,7 +5,7 @@ import { getToken, getApiUrl } from "../utils/config.js";
5
5
  import { existsSync, readFileSync } from "node:fs";
6
6
  import { join } from "node:path";
7
7
  import { parseConfig } from "@solcreek/sdk";
8
- import { globalArgs, resolveJsonMode, jsonOutput } from "../utils/output.js";
8
+ import { globalArgs, resolveJsonMode, jsonOutput, AUTH_BREADCRUMBS, NO_PROJECT_BREADCRUMBS } from "../utils/output.js";
9
9
  export const deploymentsCommand = defineCommand({
10
10
  meta: {
11
11
  name: "deployments",
@@ -24,7 +24,7 @@ export const deploymentsCommand = defineCommand({
24
24
  const token = getToken();
25
25
  if (!token) {
26
26
  if (jsonMode)
27
- jsonOutput({ ok: false, error: "not_authenticated" }, 1);
27
+ jsonOutput({ ok: false, error: "not_authenticated" }, 1, AUTH_BREADCRUMBS);
28
28
  consola.error("Not authenticated. Run `creek login` first.");
29
29
  process.exit(1);
30
30
  }
@@ -34,7 +34,7 @@ export const deploymentsCommand = defineCommand({
34
34
  const configPath = join(process.cwd(), "creek.toml");
35
35
  if (!existsSync(configPath)) {
36
36
  if (jsonMode)
37
- jsonOutput({ ok: false, error: "no_project", message: "No creek.toml found. Use --project <slug> or run from a project directory." }, 1);
37
+ jsonOutput({ ok: false, error: "no_project", message: "No creek.toml found. Use --project <slug> or run from a project directory." }, 1, NO_PROJECT_BREADCRUMBS);
38
38
  consola.error("No creek.toml found. Use --project <slug> or run from a project directory.");
39
39
  process.exit(1);
40
40
  }
@@ -53,7 +53,13 @@ export const deploymentsCommand = defineCommand({
53
53
  process.exit(1);
54
54
  }
55
55
  if (jsonMode) {
56
- jsonOutput({ ok: true, project: slug, deployments });
56
+ const crumbs = deployments.length > 0
57
+ ? [
58
+ { command: `creek rollback --project ${slug}`, description: "Rollback to previous deployment" },
59
+ { command: `creek status`, description: "Check current project status" },
60
+ ]
61
+ : [{ command: `creek deploy`, description: "Create first deployment" }];
62
+ jsonOutput({ ok: true, project: slug, deployments }, 0, crumbs);
57
63
  }
58
64
  if (deployments.length === 0) {
59
65
  consola.info(`No deployments for ${slug}. Run \`creek deploy\` to create one.`);
@@ -38,7 +38,9 @@ const domainsLs = defineCommand({
38
38
  const slug = getProjectSlug(args);
39
39
  const domains = await client.listDomains(slug);
40
40
  if (jsonMode) {
41
- jsonOutput({ ok: true, project: slug, domains });
41
+ jsonOutput({ ok: true, project: slug, domains }, 0, [
42
+ { command: `creek domains add <HOSTNAME> --project ${slug}`, description: "Add a custom domain" },
43
+ ]);
42
44
  return;
43
45
  }
44
46
  if (domains.length === 0) {
@@ -67,7 +69,13 @@ const domainsAdd = defineCommand({
67
69
  const result = await client.addDomain(slug, args.hostname);
68
70
  const { domain, verification } = result;
69
71
  if (jsonMode) {
70
- jsonOutput({ ok: true, project: slug, domain, verification });
72
+ const crumbs = domain.status === "active"
73
+ ? [{ command: `creek domains ls --project ${slug}`, description: "List all domains" }]
74
+ : [
75
+ { command: `creek domains ls --project ${slug}`, description: "Check domain status" },
76
+ { command: `creek domains activate ${domain.hostname} --project ${slug}`, description: "Activate after DNS setup" },
77
+ ];
78
+ jsonOutput({ ok: true, project: slug, domain, verification }, 0, crumbs);
71
79
  return;
72
80
  }
73
81
  if (domain.status === "active") {
@@ -108,7 +116,9 @@ const domainsRm = defineCommand({
108
116
  }
109
117
  await client.deleteDomain(slug, domain.id);
110
118
  if (jsonMode) {
111
- jsonOutput({ ok: true, hostname: domain.hostname, removed: true, project: slug });
119
+ jsonOutput({ ok: true, hostname: domain.hostname, removed: true, project: slug }, 0, [
120
+ { command: `creek domains ls --project ${slug}`, description: "List remaining domains" },
121
+ ]);
112
122
  return;
113
123
  }
114
124
  consola.success(`Removed ${domain.hostname}`);
@@ -138,7 +148,9 @@ const domainsActivate = defineCommand({
138
148
  }
139
149
  await client.activateDomain(slug, domain.id);
140
150
  if (jsonMode) {
141
- jsonOutput({ ok: true, hostname: domain.hostname, status: "active", project: slug });
151
+ jsonOutput({ ok: true, hostname: domain.hostname, status: "active", project: slug }, 0, [
152
+ { command: `creek domains ls --project ${slug}`, description: "List all domains" },
153
+ ]);
142
154
  return;
143
155
  }
144
156
  consola.success(`Activated ${domain.hostname}`);
@@ -34,7 +34,10 @@ const envSet = defineCommand({
34
34
  const slug = getProjectSlug();
35
35
  await client.setEnvVar(slug, args.key, args.value);
36
36
  if (jsonMode)
37
- jsonOutput({ ok: true, key: args.key, project: slug });
37
+ jsonOutput({ ok: true, key: args.key, project: slug }, 0, [
38
+ { command: `creek env ls --project ${slug}`, description: "List all environment variables" },
39
+ { command: `creek deploy`, description: "Deploy to apply changes" },
40
+ ]);
38
41
  consola.success(`Set ${args.key}`);
39
42
  },
40
43
  });
@@ -55,11 +58,18 @@ const envGet = defineCommand({
55
58
  const slug = getProjectSlug();
56
59
  const vars = await client.listEnvVars(slug);
57
60
  if (jsonMode) {
61
+ const crumbs = [
62
+ { command: `creek env set <KEY> <VALUE>`, description: "Set an environment variable" },
63
+ ];
64
+ if (vars.length > 0) {
65
+ crumbs.push({ command: `creek env rm ${vars[0].key}`, description: `Remove ${vars[0].key}` });
66
+ }
67
+ crumbs.push({ command: "creek deploy", description: "Deploy to apply changes" });
58
68
  jsonOutput({
59
69
  ok: true,
60
70
  project: slug,
61
71
  vars: vars.map((v) => ({ key: v.key, value: args.show ? v.value : redact(v.value) })),
62
- });
72
+ }, 0, crumbs);
63
73
  }
64
74
  if (vars.length === 0) {
65
75
  consola.info("No environment variables set.");
@@ -86,7 +96,10 @@ const envRm = defineCommand({
86
96
  const slug = getProjectSlug();
87
97
  await client.deleteEnvVar(slug, args.key);
88
98
  if (jsonMode)
89
- jsonOutput({ ok: true, key: args.key, removed: true, project: slug });
99
+ jsonOutput({ ok: true, key: args.key, removed: true, project: slug }, 0, [
100
+ { command: `creek env ls --project ${slug}`, description: "List remaining variables" },
101
+ { command: "creek deploy", description: "Deploy to apply changes" },
102
+ ]);
90
103
  consola.success(`Removed ${args.key}`);
91
104
  },
92
105
  });
@@ -61,7 +61,11 @@ export const initCommand = defineCommand({
61
61
  };
62
62
  writeFileSync(configPath, stringify(config));
63
63
  if (jsonMode) {
64
- jsonOutput({ ok: true, name, framework: framework ?? null, path: configPath });
64
+ jsonOutput({ ok: true, name, framework: framework ?? null, path: configPath }, 0, [
65
+ { command: "creek deploy", description: "Deploy the project" },
66
+ { command: "creek env set <KEY> <VALUE>", description: "Set an environment variable" },
67
+ { command: "creek dev", description: "Start local development server" },
68
+ ]);
65
69
  }
66
70
  consola.success(`Created creek.toml for "${name}"`);
67
71
  },
@@ -106,14 +106,21 @@ async function saveAndVerify(apiKey, jsonMode = false) {
106
106
  const session = await client.getSession();
107
107
  if (!session?.user) {
108
108
  if (jsonMode)
109
- jsonOutput({ ok: false, error: "invalid_token", message: "Invalid API key" }, 1);
109
+ jsonOutput({ ok: false, error: "invalid_token", message: "Invalid API key" }, 1, [
110
+ { command: "creek login", description: "Try interactive login" },
111
+ { command: "creek login --headless", description: "Paste API key manually" },
112
+ ]);
110
113
  consola.error("Invalid API key. Please check and try again.");
111
114
  process.exit(1);
112
115
  }
113
116
  const config = readCliConfig();
114
117
  writeCliConfig({ ...config, token: apiKey });
115
118
  if (jsonMode) {
116
- jsonOutput({ ok: true, user: session.user.name, email: session.user.email });
119
+ jsonOutput({ ok: true, user: session.user.name, email: session.user.email }, 0, [
120
+ { command: "creek projects", description: "List your projects" },
121
+ { command: "creek deploy", description: "Deploy current directory" },
122
+ { command: "creek whoami", description: "Verify authenticated user" },
123
+ ]);
117
124
  }
118
125
  consola.success(`Logged in as ${session.user.name} (${session.user.email})`);
119
126
  }
@@ -2,7 +2,7 @@ import { defineCommand } from "citty";
2
2
  import consola from "consola";
3
3
  import { CreekClient } from "@solcreek/sdk";
4
4
  import { getToken, getApiUrl } from "../utils/config.js";
5
- import { globalArgs, resolveJsonMode, jsonOutput } from "../utils/output.js";
5
+ import { globalArgs, resolveJsonMode, jsonOutput, AUTH_BREADCRUMBS } from "../utils/output.js";
6
6
  export const projectsCommand = defineCommand({
7
7
  meta: {
8
8
  name: "projects",
@@ -14,14 +14,20 @@ export const projectsCommand = defineCommand({
14
14
  const token = getToken();
15
15
  if (!token) {
16
16
  if (jsonMode)
17
- jsonOutput({ ok: false, error: "not_authenticated" }, 1);
17
+ jsonOutput({ ok: false, error: "not_authenticated" }, 1, AUTH_BREADCRUMBS);
18
18
  consola.error("Not authenticated. Run `creek login` first.");
19
19
  process.exit(1);
20
20
  }
21
21
  const client = new CreekClient(getApiUrl(), token);
22
22
  const projects = await client.listProjects();
23
23
  if (jsonMode) {
24
- jsonOutput({ ok: true, projects });
24
+ const crumbs = projects.length > 0
25
+ ? projects.slice(0, 3).map((p) => ({
26
+ command: `creek deployments --project ${p.slug}`,
27
+ description: `List deployments for ${p.slug}`,
28
+ }))
29
+ : [{ command: "creek deploy", description: "Deploy your first project" }];
30
+ jsonOutput({ ok: true, projects }, 0, crumbs);
25
31
  }
26
32
  if (projects.length === 0) {
27
33
  consola.info("No projects yet. Deploy one with `creek deploy`.");
@@ -0,0 +1,27 @@
1
+ export declare const rollbackCommand: 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
+ deployment: {
13
+ type: "positional";
14
+ description: string;
15
+ required: false;
16
+ };
17
+ message: {
18
+ type: "string";
19
+ alias: string;
20
+ description: string;
21
+ };
22
+ project: {
23
+ type: "string";
24
+ description: string;
25
+ };
26
+ }>;
27
+ //# sourceMappingURL=rollback.d.ts.map
@@ -0,0 +1,82 @@
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, AUTH_BREADCRUMBS } from "../utils/output.js";
8
+ function getProjectSlug(args) {
9
+ if (args?.project)
10
+ return args.project;
11
+ const configPath = join(process.cwd(), "creek.toml");
12
+ if (!existsSync(configPath)) {
13
+ consola.error("No creek.toml found. Use --project <slug> or run from a project directory.");
14
+ process.exit(1);
15
+ }
16
+ return parseConfig(readFileSync(configPath, "utf-8")).project.name;
17
+ }
18
+ export const rollbackCommand = defineCommand({
19
+ meta: {
20
+ name: "rollback",
21
+ description: "Rollback production to a previous deployment",
22
+ },
23
+ args: {
24
+ deployment: {
25
+ type: "positional",
26
+ description: "Deployment ID to rollback to (default: previous)",
27
+ required: false,
28
+ },
29
+ message: {
30
+ type: "string",
31
+ alias: "m",
32
+ description: "Rollback reason (stored in audit log)",
33
+ },
34
+ project: {
35
+ type: "string",
36
+ description: "Project slug (default: from creek.toml)",
37
+ },
38
+ ...globalArgs,
39
+ },
40
+ async run({ args }) {
41
+ const jsonMode = resolveJsonMode(args);
42
+ const token = getToken();
43
+ if (!token) {
44
+ if (jsonMode)
45
+ jsonOutput({ ok: false, error: "not_authenticated", message: "Not authenticated" }, 1, AUTH_BREADCRUMBS);
46
+ consola.error("Not authenticated. Run `creek login` first.");
47
+ process.exit(1);
48
+ }
49
+ const projectSlug = getProjectSlug(args);
50
+ const client = new CreekClient(getApiUrl(), token);
51
+ const deploymentId = args.deployment;
52
+ const message = args.message;
53
+ try {
54
+ const result = await client.rollback(projectSlug, {
55
+ deploymentId: deploymentId || undefined,
56
+ message: message || undefined,
57
+ });
58
+ if (jsonMode) {
59
+ jsonOutput(result, 0, [
60
+ { command: `creek status`, description: "Verify rollback status" },
61
+ { command: `creek deployments --project ${projectSlug}`, description: "View deployment history" },
62
+ ]);
63
+ return;
64
+ }
65
+ consola.success(`⬡ Rolled back to deployment ${result.rolledBackTo.slice(0, 8)}`);
66
+ consola.info(`Production URL: ${result.url}`);
67
+ if (message) {
68
+ consola.info(`Reason: ${message}`);
69
+ }
70
+ }
71
+ catch (err) {
72
+ const msg = err.message ?? "Rollback failed";
73
+ if (jsonMode)
74
+ jsonOutput({ ok: false, error: "rollback_failed", message: msg }, 1, [
75
+ { command: `creek deployments --project ${projectSlug}`, description: "List available deployments" },
76
+ ]);
77
+ consola.error(msg);
78
+ process.exit(1);
79
+ }
80
+ },
81
+ });
82
+ //# sourceMappingURL=rollback.js.map
@@ -5,7 +5,7 @@ import { getToken, getApiUrl, getSandboxApiUrl } from "../utils/config.js";
5
5
  import { existsSync, readFileSync } from "node:fs";
6
6
  import { join } from "node:path";
7
7
  import { parseConfig } from "@solcreek/sdk";
8
- import { globalArgs, resolveJsonMode, jsonOutput } from "../utils/output.js";
8
+ import { globalArgs, resolveJsonMode, jsonOutput, AUTH_BREADCRUMBS, NO_PROJECT_BREADCRUMBS } from "../utils/output.js";
9
9
  export const statusCommand = defineCommand({
10
10
  meta: {
11
11
  name: "status",
@@ -34,13 +34,18 @@ async function sandboxStatus(sandboxId, jsonMode) {
34
34
  const res = await fetch(`${sandboxApiUrl}/api/sandbox/${sandboxId}/status`);
35
35
  if (!res.ok) {
36
36
  if (jsonMode)
37
- jsonOutput({ ok: false, error: "not_found", message: "Sandbox not found" }, 1);
37
+ jsonOutput({ ok: false, error: "not_found", message: "Sandbox not found" }, 1, [
38
+ { command: "creek deploy --demo", description: "Deploy a new sandbox" },
39
+ ]);
38
40
  consola.error("Sandbox not found. It may have expired.");
39
41
  process.exit(1);
40
42
  }
41
43
  const status = await res.json();
42
44
  if (jsonMode) {
43
- jsonOutput({ ok: true, type: "sandbox", ...status });
45
+ const crumbs = status.claimable
46
+ ? [{ command: `creek claim ${sandboxId}`, description: "Claim as permanent project" }]
47
+ : [{ command: "creek deploy", description: "Deploy a new version" }];
48
+ jsonOutput({ ok: true, type: "sandbox", ...status }, 0, crumbs);
44
49
  }
45
50
  const s = status.status;
46
51
  const statusColor = s === "active" ? `\x1b[32m${s}\x1b[0m`
@@ -68,7 +73,7 @@ async function projectStatus(jsonMode) {
68
73
  const token = getToken();
69
74
  if (!token) {
70
75
  if (jsonMode)
71
- jsonOutput({ ok: false, error: "not_authenticated" }, 1);
76
+ jsonOutput({ ok: false, error: "not_authenticated" }, 1, AUTH_BREADCRUMBS);
72
77
  consola.error("Not authenticated. Run `creek login` first.");
73
78
  consola.info("To check a sandbox: creek status <sandboxId>");
74
79
  process.exit(1);
@@ -76,7 +81,7 @@ async function projectStatus(jsonMode) {
76
81
  const configPath = join(process.cwd(), "creek.toml");
77
82
  if (!existsSync(configPath)) {
78
83
  if (jsonMode)
79
- jsonOutput({ ok: false, error: "no_project", message: "No creek.toml found" }, 1);
84
+ jsonOutput({ ok: false, error: "no_project", message: "No creek.toml found" }, 1, NO_PROJECT_BREADCRUMBS);
80
85
  consola.error("No creek.toml found.");
81
86
  consola.info("To check a sandbox: creek status <sandboxId>");
82
87
  process.exit(1);
@@ -89,7 +94,10 @@ async function projectStatus(jsonMode) {
89
94
  }
90
95
  catch {
91
96
  if (jsonMode)
92
- jsonOutput({ ok: false, error: "not_found", message: `Project "${config.project.name}" not found` }, 1);
97
+ jsonOutput({ ok: false, error: "not_found", message: `Project "${config.project.name}" not found` }, 1, [
98
+ { command: "creek deploy", description: "Deploy to create the project" },
99
+ { command: "creek projects", description: "List existing projects" },
100
+ ]);
93
101
  consola.error(`Project "${config.project.name}" not found.`);
94
102
  process.exit(1);
95
103
  }
@@ -101,7 +109,10 @@ async function projectStatus(jsonMode) {
101
109
  framework: project.framework,
102
110
  productionDeploymentId: project.production_deployment_id,
103
111
  createdAt: project.created_at,
104
- });
112
+ }, 0, [
113
+ { command: `creek deployments --project ${project.slug}`, description: "List deployment history" },
114
+ { command: "creek deploy", description: "Deploy a new version" },
115
+ ]);
105
116
  }
106
117
  const deployed = project.production_deployment_id ? "deployed" : "not deployed";
107
118
  const framework = project.framework ? ` (${project.framework})` : "";
@@ -2,7 +2,7 @@ import { defineCommand } from "citty";
2
2
  import consola from "consola";
3
3
  import { CreekClient } from "@solcreek/sdk";
4
4
  import { getToken, getApiUrl } from "../utils/config.js";
5
- import { globalArgs, resolveJsonMode, jsonOutput } from "../utils/output.js";
5
+ import { globalArgs, resolveJsonMode, jsonOutput, AUTH_BREADCRUMBS } from "../utils/output.js";
6
6
  export const whoamiCommand = defineCommand({
7
7
  meta: {
8
8
  name: "whoami",
@@ -14,7 +14,7 @@ export const whoamiCommand = defineCommand({
14
14
  const token = getToken();
15
15
  if (!token) {
16
16
  if (jsonMode)
17
- jsonOutput({ ok: false, authenticated: false, error: "not_authenticated" }, 1);
17
+ jsonOutput({ ok: false, authenticated: false, error: "not_authenticated" }, 1, AUTH_BREADCRUMBS);
18
18
  consola.error("Not authenticated. Run `creek login` first.");
19
19
  process.exit(1);
20
20
  }
@@ -22,7 +22,7 @@ export const whoamiCommand = defineCommand({
22
22
  const session = await client.getSession();
23
23
  if (!session?.user) {
24
24
  if (jsonMode)
25
- jsonOutput({ ok: false, authenticated: false, error: "session_expired" }, 1);
25
+ jsonOutput({ ok: false, authenticated: false, error: "session_expired" }, 1, AUTH_BREADCRUMBS);
26
26
  consola.error("Session expired or invalid. Run `creek login` to re-authenticate.");
27
27
  process.exit(1);
28
28
  }
@@ -33,7 +33,10 @@ export const whoamiCommand = defineCommand({
33
33
  user: session.user.name,
34
34
  email: session.user.email,
35
35
  api: getApiUrl(),
36
- });
36
+ }, 0, [
37
+ { command: "creek projects", description: "List your projects" },
38
+ { command: "creek deploy", description: "Deploy current directory" },
39
+ ]);
37
40
  }
38
41
  consola.log(` User: ${session.user.name}`);
39
42
  consola.log(` Email: ${session.user.email}`);
@@ -110,7 +110,7 @@ export class DevServer {
110
110
  .map((b) => b.type.toUpperCase());
111
111
  const bindingStr = bindings.length > 0 ? ` (${bindings.join(", ")})` : "";
112
112
  console.log("");
113
- consola.success(`creek dev\n`);
113
+ consola.success(`⬡ creek dev\n`);
114
114
  consola.info(`App: http://localhost:${actualPort}`);
115
115
  if (config.workerEntry) {
116
116
  consola.info(`Worker: ${config.workerEntry}${bindingStr}`);
package/dist/index.js CHANGED
@@ -15,6 +15,7 @@ import { projectsCommand } from "./commands/projects.js";
15
15
  import { deploymentsCommand } from "./commands/deployments.js";
16
16
  import { statusCommand } from "./commands/status.js";
17
17
  import { devCommand } from "./commands/dev.js";
18
+ import { rollbackCommand } from "./commands/rollback.js";
18
19
  const __dirname = dirname(fileURLToPath(import.meta.url));
19
20
  const cliPkg = JSON.parse(readFileSync(join(__dirname, "..", "package.json"), "utf-8"));
20
21
  // Read version from the "creek" facade package (what users install),
@@ -30,7 +31,7 @@ catch {
30
31
  }
31
32
  const main = defineCommand({
32
33
  meta: {
33
- name: "creek",
34
+ name: "creek",
34
35
  version,
35
36
  description: "Deploy full-stack apps to the edge",
36
37
  },
@@ -46,6 +47,7 @@ const main = defineCommand({
46
47
  claim: claimCommand,
47
48
  env: envCommand,
48
49
  domains: domainsCommand,
50
+ rollback: rollbackCommand,
49
51
  },
50
52
  });
51
53
  runMain(main);
@@ -5,8 +5,13 @@
5
5
  * can produce structured output for agents, CI/CD, and pipes.
6
6
  */
7
7
  export declare const isTTY: boolean;
8
+ /** A suggested next command for agents to follow. */
9
+ export interface Breadcrumb {
10
+ command: string;
11
+ description: string;
12
+ }
8
13
  /** Output structured JSON and exit. */
9
- export declare function jsonOutput(data: Record<string, unknown>, exitCode?: number): never;
14
+ export declare function jsonOutput(data: Record<string, unknown>, exitCode?: number, breadcrumbs?: Breadcrumb[]): never;
10
15
  /** Resolve JSON mode: explicit --json flag OR non-TTY environment. */
11
16
  export declare function resolveJsonMode(args: {
12
17
  json?: boolean;
@@ -34,5 +39,8 @@ export declare function shouldAutoConfirm(args: {
34
39
  yes?: boolean;
35
40
  }): boolean;
36
41
  /** Output an error in the appropriate format and exit. */
37
- export declare function exitError(jsonMode: boolean, error: string, message: string, exitCode?: number): never;
42
+ export declare function exitError(jsonMode: boolean, error: string, message: string, exitCode?: number, breadcrumbs?: Breadcrumb[]): never;
43
+ /** Reusable breadcrumbs for common error states. */
44
+ export declare const AUTH_BREADCRUMBS: Breadcrumb[];
45
+ export declare const NO_PROJECT_BREADCRUMBS: Breadcrumb[];
38
46
  //# sourceMappingURL=output.d.ts.map
@@ -6,8 +6,9 @@
6
6
  */
7
7
  export const isTTY = process.stdout.isTTY ?? false;
8
8
  /** Output structured JSON and exit. */
9
- export function jsonOutput(data, exitCode = 0) {
10
- process.stdout.write(JSON.stringify(data, null, 2) + "\n");
9
+ export function jsonOutput(data, exitCode = 0, breadcrumbs) {
10
+ const output = breadcrumbs?.length ? { ...data, breadcrumbs } : data;
11
+ process.stdout.write(JSON.stringify(output, null, 2) + "\n");
11
12
  process.exit(exitCode);
12
13
  }
13
14
  /** Resolve JSON mode: explicit --json flag OR non-TTY environment. */
@@ -37,9 +38,18 @@ export function shouldAutoConfirm(args) {
37
38
  return args.yes === true || !isTTY;
38
39
  }
39
40
  /** Output an error in the appropriate format and exit. */
40
- export function exitError(jsonMode, error, message, exitCode = 1) {
41
+ export function exitError(jsonMode, error, message, exitCode = 1, breadcrumbs) {
41
42
  if (jsonMode)
42
- jsonOutput({ ok: false, error, message }, exitCode);
43
+ jsonOutput({ ok: false, error, message }, exitCode, breadcrumbs);
43
44
  return process.exit(exitCode);
44
45
  }
46
+ /** Reusable breadcrumbs for common error states. */
47
+ export const AUTH_BREADCRUMBS = [
48
+ { command: "creek login", description: "Authenticate interactively" },
49
+ { command: "creek login --token <KEY>", description: "Authenticate with API key (CI/CD)" },
50
+ ];
51
+ export const NO_PROJECT_BREADCRUMBS = [
52
+ { command: "creek init", description: "Initialize creek.toml in current directory" },
53
+ { command: "creek deploy --demo", description: "Deploy a sample site to try Creek" },
54
+ ];
45
55
  //# sourceMappingURL=output.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@solcreek/cli",
3
- "version": "0.3.4",
3
+ "version": "0.3.5",
4
4
  "description": "CLI for the Creek deployment platform",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -32,7 +32,7 @@
32
32
  "directory": "packages/cli"
33
33
  },
34
34
  "dependencies": {
35
- "@solcreek/sdk": "workspace:*",
35
+ "@solcreek/sdk": "^0.1.0-alpha.3",
36
36
  "citty": "^0.1.6",
37
37
  "consola": "^3.4.2",
38
38
  "esbuild": "^0.25.0",