@lizard-build/cli 4.0.35 → 4.0.37

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.
@@ -66,7 +66,7 @@ export function accountInstruction(state: AccountState, sub: Subscription | null
66
66
  case "not_owner":
67
67
  return (
68
68
  "This account deploys on its workspace owner's plan. Do NOT start or change a subscription: only the owner can." +
69
- (sub?.plan === "none" ? " Deploys stay blocked until the owner starts Pro." : "")
69
+ (sub?.plan === "none" ? " Sandboxes and deploys stay blocked until the owner starts Pro." : "")
70
70
  );
71
71
  case "enterprise":
72
72
  return "Enterprise plan, invoiced monthly. Nothing to set up.";
@@ -84,12 +84,12 @@ export function accountInstruction(state: AccountState, sub: Subscription | null
84
84
  return "Pro is active. Do NOT open billing or start a subscription.";
85
85
  case "trial_available":
86
86
  return (
87
- `No plan yet, and deploys need one. Offer the user the ${sub?.trial.days}-day Pro trial with ${usd(sub?.trial.creditCents ?? 0)} in credits: ` +
87
+ `No plan yet, and sandboxes and deploys need one. Offer the user the ${sub?.trial.days}-day Pro trial with ${usd(sub?.trial.creditCents ?? 0)} in credits: ` +
88
88
  `no charge today, then ${price}/month, taxes included. ${start}`
89
89
  );
90
90
  case "subscription_required":
91
91
  return (
92
- "No plan yet, and deploys need one. This account has had its trial, so do NOT promise one. " +
92
+ "No plan yet, and sandboxes and deploys need one. This account has had its trial, so do NOT promise one. " +
93
93
  `Pro is ${price}/month, taxes included, with ${usd(sub?.includedCents ?? 1900)} in credits each month. ${start}`
94
94
  );
95
95
  case "checkout_unavailable":
@@ -97,16 +97,33 @@ export function accountInstruction(state: AccountState, sub: Subscription | null
97
97
  }
98
98
  }
99
99
 
100
- /** Commands an agent can run next. */
100
+ /** Commands an agent can run next. Sandboxes first; both they and deploys live in a project. */
101
101
  export function nextSteps(linked: boolean): Array<{ command: string; why: string }> {
102
102
  return [
103
103
  {
104
104
  command: "lizard skills get core",
105
105
  why: "The guide to every command, matched to this CLI version. Read it before running others.",
106
106
  },
107
- ...(linked ? [] : [{ command: "lizard init", why: "Create a project and link the current folder to it." }]),
108
- { command: "lizard up", why: linked ? "Deploy the linked folder." : "Deploy the folder once it is linked." },
107
+ ...(linked
108
+ ? []
109
+ : [{ command: "lizard init", why: "Create a project and link the current folder to it. Sandboxes and deploys both need one." }]),
110
+ {
111
+ command: "lizard sandbox create",
112
+ why: "Start a sandbox in the project, then run commands in it with `lizard sandbox exec <id> -- <cmd>`.",
113
+ },
114
+ { command: "lizard up", why: "Or deploy the linked folder." },
115
+ ];
116
+ }
117
+
118
+ /** The same steps for a person, as aligned lines for the closing note. */
119
+ export function nextStepsNote(linked: boolean): string {
120
+ const rows: Array<[string, string]> = [
121
+ ...(linked ? [] : ([["lizard init", "Create a project and link this folder"]] as Array<[string, string]>)),
122
+ ["lizard sandbox create", linked ? "Start a sandbox in this project" : "Start a sandbox in it"],
123
+ ["lizard up", "Or deploy this folder"],
109
124
  ];
125
+ const width = Math.max(...rows.map(([cmd]) => cmd.length)) + 3;
126
+ return rows.map(([cmd, why]) => chalk.cyan(cmd) + " ".repeat(width - cmd.length) + why).join("\n");
110
127
  }
111
128
 
112
129
  async function fetchSubscription(): Promise<Subscription | null> {
@@ -169,7 +186,7 @@ async function offerPlan(canAsk: boolean): Promise<void> {
169
186
  const offer = trial
170
187
  ? `Pro trial: ${sub.trial.days} days with ${usd(sub.trial.creditCents ?? 0)} in credits. No charge today, then ${usd(sub.priceCents)}/month, taxes included.`
171
188
  : `Pro: ${usd(sub.priceCents)}/month, taxes included, with ${usd(sub.includedCents)} in credits each month.`;
172
- p.log.info(`No plan yet. Deploys need one.\n${offer}`);
189
+ p.log.info(`No plan yet. Sandboxes and deploys need one.\n${offer}`);
173
190
 
174
191
  const go = canAsk ? await p.confirm({ message: trial ? "Start the trial now?" : "Start Pro now?" }) : false;
175
192
  if (p.isCancel(go) || !go) {
@@ -204,14 +221,7 @@ async function loginInteractive(flag?: AuthProvider): Promise<void> {
204
221
 
205
222
  await offerPlan(canAsk);
206
223
 
207
- const linked = Boolean(getProjectLink());
208
- p.note(
209
- (linked
210
- ? [`${chalk.cyan("lizard up")} Deploy this folder`]
211
- : [`${chalk.cyan("lizard init")} Create a project and link this folder`, `${chalk.cyan("lizard up")} Deploy it`]
212
- ).join("\n"),
213
- "Next",
214
- );
224
+ p.note(nextStepsNote(Boolean(getProjectLink())), "Next");
215
225
  p.outro("You're all set");
216
226
  }
217
227
 
@@ -57,6 +57,9 @@ export function registerRestart(program: Command) {
57
57
  }
58
58
  }
59
59
 
60
+ // `ps` takes no service argument: it lists every service in the project.
61
+ const psHint = `lizard ps${opts.project ? ` --project ${opts.project}` : ""}`;
62
+
60
63
  const timeoutSeconds = parseInt(opts.timeout, 10);
61
64
  if (!(timeoutSeconds > 0)) {
62
65
  fail(`--timeout must be a positive number of seconds, got ${JSON.stringify(opts.timeout)}`, 1, "INVALID_ARGUMENT");
@@ -104,7 +107,7 @@ export function registerRestart(program: Command) {
104
107
  printJSON({ id, status: "restarting" });
105
108
  } else {
106
109
  success("Restart started");
107
- info(chalk.dim(` Check status: lizard ps ${id}`));
110
+ info(chalk.dim(` Check status: ${psHint}`));
108
111
  }
109
112
  return;
110
113
  }
@@ -138,7 +141,7 @@ export function registerRestart(program: Command) {
138
141
  } else if (finalStatus) {
139
142
  error("Restart failed");
140
143
  } else {
141
- info(chalk.dim("Still restarting — check status with: lizard ps " + id));
144
+ info(chalk.dim(`Still restarting — check status with: ${psHint}`));
142
145
  }
143
146
  });
144
147
  }
@@ -11,7 +11,7 @@ import * as http from "node:http";
11
11
  export function registerSSH(program: Command) {
12
12
  program
13
13
  .command("ssh")
14
- .description("Execute a command inside a running service container")
14
+ .description("Execute a command inside a running service container (disabled: the API answers 403 \"Shell access to services is disabled\"; for sandboxes use `lizard sandbox ssh`)")
15
15
  .argument("[cmd...]", "Command and args to run inside the container (required; pass after `--` to stop flag parsing, e.g. `-- ls -la /app`)")
16
16
  .option("-s, --service <id>", "Service name or ID")
17
17
  .option("-p, --project <id>", "Project name, slug, or ID")
@@ -1,7 +1,14 @@
1
1
  import chalk from "chalk";
2
2
  import { Command } from "commander";
3
3
  import { info, success, isJSONMode, printJSON } from "../lib/format.js";
4
- import { CURRENT_VERSION, getLatestVersion, selfUpdate, isStandaloneBinary } from "../lib/updater.js";
4
+ import {
5
+ CURRENT_VERSION,
6
+ canSelfUpdate,
7
+ getLatestVersion,
8
+ installerCommand,
9
+ isStandaloneBinary,
10
+ selfUpdate,
11
+ } from "../lib/updater.js";
5
12
 
6
13
  export function registerUpgrade(program: Command) {
7
14
  program
@@ -87,21 +94,40 @@ export function registerUpgrade(program: Command) {
87
94
  return;
88
95
  }
89
96
 
97
+ // No binary to swap in (the Windows build): say so and fail, instead of
98
+ // reporting an upgrade that never happened.
99
+ if (!canSelfUpdate()) {
100
+ throw notInstalled(`v${latest} is available, but this copy of Lizard CLI can't update itself.`);
101
+ }
102
+
90
103
  info(`Upgrading v${CURRENT_VERSION} → ${chalk.green("v" + latest)}...`);
91
104
 
105
+ let upgraded: boolean;
92
106
  try {
93
- await selfUpdate((msg) => info(chalk.dim(msg)));
94
- if (isJSONMode()) {
95
- printJSON({
96
- previousVersion: CURRENT_VERSION,
97
- latestVersion: latest,
98
- upgraded: true,
99
- });
100
- } else {
101
- success(`Upgraded to v${latest}`);
102
- }
107
+ upgraded = await selfUpdate((msg) => info(chalk.dim(msg)));
103
108
  } catch (e: any) {
104
109
  throw new Error(`Upgrade failed: ${e.message}`);
105
110
  }
111
+ if (!upgraded) {
112
+ throw notInstalled(`Nothing was replaced: v${CURRENT_VERSION} is still installed.`);
113
+ }
114
+ if (isJSONMode()) {
115
+ printJSON({
116
+ previousVersion: CURRENT_VERSION,
117
+ latestVersion: latest,
118
+ upgraded: true,
119
+ });
120
+ } else {
121
+ success(`Upgraded to v${latest}`);
122
+ }
106
123
  });
107
124
  }
125
+
126
+ /** Exit 1 with the installer command that does the upgrade instead. */
127
+ function notInstalled(reason: string): Error {
128
+ const shell = process.platform === "win32" ? " in PowerShell" : "";
129
+ return Object.assign(
130
+ new Error(`${reason} Re-run the installer${shell}:\n ${installerCommand()}`),
131
+ { code: "UPGRADE_NOT_INSTALLED" },
132
+ );
133
+ }
@@ -5,7 +5,7 @@ import { join, dirname } from "node:path";
5
5
  import os from "node:os";
6
6
  import { spawn } from "node:child_process";
7
7
 
8
- export const CURRENT_VERSION = "4.0.35";
8
+ export const CURRENT_VERSION = "4.0.37";
9
9
  const RELEASES_API = "https://api.github.com/repos/lizard-build/lizard-cli/releases/latest";
10
10
  const RELEASE_BASE = "https://github.com/lizard-build/lizard-cli/releases/latest/download";
11
11
 
@@ -22,6 +22,22 @@ function getBinaryName(): string | null {
22
22
  return null;
23
23
  }
24
24
 
25
+ /**
26
+ * True when selfUpdate() has a release asset to swap in for this platform.
27
+ * False on Windows: a running .exe can't be replaced in place, so the
28
+ * PowerShell installer does the update there.
29
+ */
30
+ export function canSelfUpdate(): boolean {
31
+ return getBinaryName() !== null;
32
+ }
33
+
34
+ /** The installer that puts the latest standalone binary over this one. */
35
+ export function installerCommand(): string {
36
+ return process.platform === "win32"
37
+ ? "irm https://lizard.build/api/cli/install.ps1 | iex"
38
+ : "curl -fsSL https://lizard.build/install.sh | bash";
39
+ }
40
+
25
41
  /**
26
42
  * True only when running as the Bun-compiled standalone binary. Under
27
43
  * npm/node, `process.execPath` is the *node* executable — self-update would
@@ -4,7 +4,7 @@ import * as os from "node:os";
4
4
  import * as path from "node:path";
5
5
  import { Command } from "commander";
6
6
  import { Readable } from "node:stream";
7
- import { registerLogin, accountState, accountInstruction, nextSteps, readTokenFromStdin } from "../../src/commands/login.js";
7
+ import { registerLogin, accountState, accountInstruction, nextSteps, nextStepsNote, readTokenFromStdin } from "../../src/commands/login.js";
8
8
  import { authUrlFor, providerFlag, pendingPayload } from "../../src/lib/signin.js";
9
9
  import type { Subscription } from "../../src/commands/billing.js";
10
10
  import { requireAuth } from "../../src/lib/auth.js";
@@ -184,9 +184,22 @@ describe("account state", () => {
184
184
  );
185
185
  });
186
186
 
187
+ test("the closing note leads with sandboxes, aligned in two columns", () => {
188
+ const plain = (t: string) => t.replace(/\x1b\[[0-9;]*m/g, "");
189
+ expect(plain(nextStepsNote(false)).split("\n")).toEqual([
190
+ "lizard init Create a project and link this folder",
191
+ "lizard sandbox create Start a sandbox in it",
192
+ "lizard up Or deploy this folder",
193
+ ]);
194
+ expect(plain(nextStepsNote(true)).split("\n")).toEqual([
195
+ "lizard sandbox create Start a sandbox in this project",
196
+ "lizard up Or deploy this folder",
197
+ ]);
198
+ });
199
+
187
200
  test("an unlinked folder gets lizard init before lizard up", () => {
188
- expect(nextSteps(false).map((s) => s.command)).toEqual(["lizard skills get core", "lizard init", "lizard up"]);
189
- expect(nextSteps(true).map((s) => s.command)).toEqual(["lizard skills get core", "lizard up"]);
201
+ expect(nextSteps(false).map((s) => s.command)).toEqual(["lizard skills get core", "lizard init", "lizard sandbox create", "lizard up"]);
202
+ expect(nextSteps(true).map((s) => s.command)).toEqual(["lizard skills get core", "lizard sandbox create", "lizard up"]);
190
203
  });
191
204
  });
192
205