@lizard-build/cli 4.0.43 → 4.0.45

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.
@@ -11,24 +11,16 @@ import { EMBEDDED_SKILLS } from "../lib/skills-data.generated.js";
11
11
  //
12
12
  // LIZARD_SKILLS_DIR overrides the embedded source with a real directory — useful
13
13
  // for authoring new skills locally without a rebuild.
14
+ //
15
+ // A skill is SKILL.md plus optional references/<ref>.md files that go deeper on
16
+ // one part of the platform; `lizard skills get <name> --ref <ref>` prints one.
14
17
 
15
18
  interface Source {
16
19
  list(): string[];
17
20
  read(name: string): string | null;
18
- pathOf(name?: string): string;
19
- }
20
-
21
- /** Inline `<!-- include: _shared/x.md -->` lines, as scripts/gen-skills.mjs does for the embedded copy. */
22
- export function expandIncludes(md: string, root: string): string {
23
- return md.replace(/^<!-- include: ([\w./-]+) -->$/gm, (line, rel: string) => {
24
- const file = path.resolve(root, rel);
25
- if (!file.startsWith(path.resolve(root) + path.sep)) return line;
26
- try {
27
- return readFileSync(file, "utf8").trimEnd();
28
- } catch {
29
- return line;
30
- }
31
- });
21
+ refs(name: string): string[];
22
+ readRef(name: string, ref: string): string | null;
23
+ pathOf(name?: string, ref?: string): string;
32
24
  }
33
25
 
34
26
  function fsSource(dir: string): Source {
@@ -37,7 +29,6 @@ function fsSource(dir: string): Source {
37
29
  try {
38
30
  return readdirSync(dir)
39
31
  .filter((name) => {
40
- if (name.startsWith("_")) return false;
41
32
  try {
42
33
  return statSync(path.join(dir, name)).isDirectory();
43
34
  } catch {
@@ -51,12 +42,34 @@ function fsSource(dir: string): Source {
51
42
  },
52
43
  read: (name) => {
53
44
  try {
54
- return expandIncludes(readFileSync(path.join(dir, name, "SKILL.md"), "utf8"), dir);
45
+ return readFileSync(path.join(dir, name, "SKILL.md"), "utf8");
46
+ } catch {
47
+ return null;
48
+ }
49
+ },
50
+ refs: (name) => {
51
+ try {
52
+ return readdirSync(path.join(dir, name, "references"))
53
+ .filter((f) => f.endsWith(".md"))
54
+ .map((f) => f.slice(0, -3))
55
+ .sort();
56
+ } catch {
57
+ return [];
58
+ }
59
+ },
60
+ readRef: (name, ref) => {
61
+ try {
62
+ return readFileSync(path.join(dir, name, "references", `${ref}.md`), "utf8");
55
63
  } catch {
56
64
  return null;
57
65
  }
58
66
  },
59
- pathOf: (name) => (name ? path.join(dir, name, "SKILL.md") : dir),
67
+ pathOf: (name, ref) => {
68
+ if (!name) return dir;
69
+ return ref
70
+ ? path.join(dir, name, "references", `${ref}.md`)
71
+ : path.join(dir, name, "SKILL.md");
72
+ },
60
73
  };
61
74
  }
62
75
 
@@ -64,8 +77,14 @@ function embeddedSource(): Source {
64
77
  return {
65
78
  list: () => Object.keys(EMBEDDED_SKILLS).sort(),
66
79
  read: (name) => EMBEDDED_SKILLS[name]?.content ?? null,
67
- pathOf: (name) =>
68
- name ? `embedded://skills/${name}/SKILL.md` : "embedded://skills",
80
+ refs: (name) => Object.keys(EMBEDDED_SKILLS[name]?.references ?? {}).sort(),
81
+ readRef: (name, ref) => EMBEDDED_SKILLS[name]?.references[ref] ?? null,
82
+ pathOf: (name, ref) => {
83
+ if (!name) return "embedded://skills";
84
+ return ref
85
+ ? `embedded://skills/${name}/references/${ref}.md`
86
+ : `embedded://skills/${name}/SKILL.md`;
87
+ },
69
88
  };
70
89
  }
71
90
 
@@ -111,6 +130,29 @@ function readSkill(
111
130
  return { md, meta: parseFrontmatter(md) };
112
131
  }
113
132
 
133
+ function readRef(src: Source, name: string, ref: string): string {
134
+ readSkill(src, name);
135
+ const md = src.readRef(name, ref);
136
+ if (md == null) {
137
+ const refs = src.refs(name);
138
+ throw new Error(
139
+ `Reference '${ref}' not found in skill '${name}'. ` +
140
+ (refs.length
141
+ ? `Available: ${refs.join(", ")}.`
142
+ : `This skill has no references.`),
143
+ );
144
+ }
145
+ return md;
146
+ }
147
+
148
+ function refEntries(src: Source, name: string) {
149
+ return src.refs(name).map((ref) => ({
150
+ name: ref,
151
+ path: src.pathOf(name, ref),
152
+ command: `lizard skills get ${name} --ref ${ref}`,
153
+ }));
154
+ }
155
+
114
156
  function shortDesc(desc: string, max = 100): string {
115
157
  const oneLine = desc.replace(/\s+/g, " ").trim();
116
158
  return oneLine.length > max ? oneLine.slice(0, max - 1) + "…" : oneLine;
@@ -132,12 +174,13 @@ export function registerSkill(program: Command) {
132
174
  printJSON({
133
175
  dir: src.pathOf(),
134
176
  skills: names.map((n) => {
177
+ let description = "";
135
178
  try {
136
- const { meta } = readSkill(src, n);
137
- return { name: n, description: meta.description || "" };
179
+ description = readSkill(src, n).meta.description || "";
138
180
  } catch {
139
- return { name: n, description: "" };
181
+ // unreadable SKILL.md: list it without a description
140
182
  }
183
+ return { name: n, description, references: src.refs(n) };
141
184
  }),
142
185
  });
143
186
  return;
@@ -147,38 +190,54 @@ export function registerSkill(program: Command) {
147
190
  return;
148
191
  }
149
192
  const rows = names.map((n) => {
193
+ let description = "";
150
194
  try {
151
- const { meta } = readSkill(src, n);
152
- return [n, shortDesc(meta.description || "")];
195
+ description = shortDesc(readSkill(src, n).meta.description || "");
153
196
  } catch {
154
- return [n, ""];
197
+ // unreadable SKILL.md: list it without a description
155
198
  }
199
+ return [n, description, src.refs(n).join(", ")];
156
200
  });
157
- table(["name", "description"], rows);
201
+ table(["name", "description", "references"], rows);
158
202
  });
159
203
 
160
204
  skill
161
205
  .command("get")
162
- .description("Print a skill's full content (markdown)")
206
+ .description("Print a skill's content (markdown), or one of its references")
163
207
  .argument("<name>", "skill name (e.g. core)")
164
- .option(
165
- "--full",
166
- "Include reference files (reserved; currently equivalent to default)",
167
- )
168
- .action((name: string, _opts: { full?: boolean }) => {
208
+ .option("--ref <ref>", "print one reference instead (e.g. sandboxes, deploys, sdk)")
209
+ .option("--full", "append every reference after the skill")
210
+ .action((name: string, opts: { ref?: string; full?: boolean }) => {
169
211
  const src = getSource();
170
212
  try {
213
+ if (opts.ref) {
214
+ const md = readRef(src, name, opts.ref);
215
+ if (isJSONMode()) {
216
+ printJSON({ name, ref: opts.ref, path: src.pathOf(name, opts.ref), content: md });
217
+ return;
218
+ }
219
+ process.stdout.write(md.endsWith("\n") ? md : md + "\n");
220
+ return;
221
+ }
171
222
  const { md, meta } = readSkill(src, name);
223
+ let content = md;
224
+ if (opts.full) {
225
+ for (const ref of src.refs(name)) {
226
+ const body = src.readRef(name, ref) ?? "";
227
+ content = `${content.trimEnd()}\n\n---\n<!-- references/${ref}.md -->\n\n${body}`;
228
+ }
229
+ }
172
230
  if (isJSONMode()) {
173
231
  printJSON({
174
232
  name,
175
233
  path: src.pathOf(name),
176
234
  frontmatter: meta,
177
- content: md,
235
+ content,
236
+ references: refEntries(src, name),
178
237
  });
179
238
  return;
180
239
  }
181
- process.stdout.write(md.endsWith("\n") ? md : md + "\n");
240
+ process.stdout.write(content.endsWith("\n") ? content : content + "\n");
182
241
  } catch (e: any) {
183
242
  fail(e.message, 3);
184
243
  }
@@ -188,9 +247,10 @@ export function registerSkill(program: Command) {
188
247
  .command("path")
189
248
  .description("Print the filesystem path of a skill (or the skills root)")
190
249
  .argument("[name]", "skill name; omit to print the skills directory")
191
- .action((name?: string) => {
250
+ .option("--ref <ref>", "print the path of one reference instead")
251
+ .action((name: string | undefined, opts: { ref?: string }) => {
192
252
  const src = getSource();
193
- const target = src.pathOf(name);
253
+ const target = src.pathOf(name, name ? opts.ref : undefined);
194
254
  if (isJSONMode()) {
195
255
  printJSON({ path: target });
196
256
  return;
package/src/index.ts CHANGED
@@ -6,7 +6,6 @@ import { setJSONMode, isJSONMode, error } from "./lib/format.js";
6
6
  import { requireAuth, isLoggedIn } from "./lib/auth.js";
7
7
  import { setBaseURL, setAccessToken, APIError, isProjectDeletedError, isAccountScopeError } from "./lib/api.js";
8
8
  import { checkForUpdateInBackground, runBackgroundUpdate, CURRENT_VERSION } from "./lib/updater.js";
9
- import { skillFor } from "./lib/skill-route.js";
10
9
 
11
10
  const BANNER = chalk.rgb(16, 185, 129)(
12
11
  [
@@ -28,34 +27,37 @@ const BANNER = chalk.rgb(16, 185, 129)(
28
27
  // of guessing commands from flag docs alone.
29
28
  const AGENTS_HELP = [
30
29
  chalk.bold("Start here (for AI agents):"),
31
- " " + chalk.cyan("lizard skills get sandbox") + chalk.dim(" sandboxes: run code, files, ports, snapshots, desktop, agents"),
32
- " " + chalk.cyan("lizard skills get core") + chalk.dim(" deploys: services, databases, builds, secrets, domains"),
30
+ " " + chalk.cyan("lizard skills get core"),
33
31
  "",
34
32
  chalk.dim(
35
- " Skills ship with the CLI (always version-matched) and come with copy-paste",
33
+ " Skills ship with the CLI (always version-matched) and cover sandboxes and the full app",
36
34
  ),
37
35
  chalk.dim(
38
- " examples. Prefer them over guessing commands from flag docs alone.",
36
+ " lifecycle — deploy, link, addons, logs, scaling, secrets, domains — with",
37
+ ),
38
+ chalk.dim(
39
+ " copy-paste examples. Prefer this over guessing commands from flag docs alone.",
40
+ ),
41
+ chalk.dim(
42
+ " For more depth: lizard skills get core --ref sandboxes | deploys | sdk",
39
43
  ),
40
44
  ].join("\n");
41
45
 
42
-
43
46
  // Compact form of the same pointer, shown on every subcommand's `--help` so the
44
47
  // per-command flag docs never read as the whole story. Mirrors the `agents`
45
48
  // block that `--help --json` already emits for every command; one tight line so
46
49
  // it sits above a subcommand's Usage without crowding it.
47
- const agentsHelpCompact = (skill: string) =>
50
+ const AGENTS_HELP_COMPACT =
48
51
  chalk.bold("For AI agents:") +
49
52
  " run " +
50
- chalk.cyan(`lizard skills get ${skill}`) +
53
+ chalk.cyan("lizard skills get core") +
51
54
  chalk.dim(" for version-matched usage and examples.");
52
55
 
53
- /** Attach the compact agents pointer to every (nested) subcommand's help, naming the skill for its subtree. */
54
- function addAgentsPointerToSubcommands(cmd: Command, skill?: string) {
56
+ /** Attach the compact agents pointer to every (nested) subcommand's help. */
57
+ function addAgentsPointerToSubcommands(cmd: Command) {
55
58
  for (const sub of cmd.commands) {
56
- const own = skill ?? skillFor(sub.name());
57
- sub.addHelpText("before", agentsHelpCompact(own) + "\n");
58
- addAgentsPointerToSubcommands(sub, own);
59
+ sub.addHelpText("before", AGENTS_HELP_COMPACT + "\n");
60
+ addAgentsPointerToSubcommands(sub);
59
61
  }
60
62
  }
61
63
 
@@ -275,13 +277,6 @@ function findTargetCommand(
275
277
  return { target: cur, unknown: null };
276
278
  }
277
279
 
278
- /** The command directly under the root that `cmd` belongs to. */
279
- function topLevelOf(cmd: Command): Command {
280
- let top = cmd;
281
- while (top.parent && top.parent !== program) top = top.parent;
282
- return top;
283
- }
284
-
285
280
  function dumpOption(o: any) {
286
281
  return {
287
282
  flags: o.flags,
@@ -394,9 +389,8 @@ async function main() {
394
389
  cli: "lizard",
395
390
  version: CURRENT_VERSION,
396
391
  agents: {
397
- start: `lizard skills get ${isRoot ? "core" : skillFor(topLevelOf(target).name())}`,
398
- skills: { sandbox: "lizard skills get sandbox", core: "lizard skills get core" },
399
- note: "Skills ship with this CLI version, with copy-paste examples: `sandbox` for sandboxes (run code, files, ports, snapshots, desktop, coding agents), `core` for deploys (services, databases, builds, secrets, domains). Prefer them over guessing commands from flag docs alone.",
392
+ start: "lizard skills get core",
393
+ note: "Skills ship with this CLI version and cover sandboxes and the full app lifecycle (deploy, link, addons, logs, scaling, secrets, domains) with copy-paste examples. Prefer this over guessing commands from flag docs alone. For more depth: `lizard skills get core --ref sandboxes|deploys|sdk`.",
400
394
  },
401
395
  command: dumpCommand(target),
402
396
  globalOptions: isRoot
@@ -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.43";
8
+ export const CURRENT_VERSION = "4.0.45";
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
 
@@ -1,78 +1,55 @@
1
- import { describe, expect, test, afterEach } from "vitest";
2
- import * as path from "node:path";
1
+ import { describe, expect, test } from "vitest";
3
2
  import { EMBEDDED_SKILLS } from "../../src/lib/skills-data.generated.js";
4
- import { expandIncludes } from "../../src/commands/skill.js";
5
- import { skillFor } from "../../src/lib/skill-route.js";
6
- import * as fs from "node:fs";
7
3
 
8
- const SKILL_DATA = path.resolve(import.meta.dirname, "../../skill-data");
4
+ const core = EMBEDDED_SKILLS.core;
5
+ const files: Record<string, string> = {
6
+ "SKILL.md": core.content,
7
+ ...Object.fromEntries(Object.entries(core.references).map(([ref, md]) => [`references/${ref}.md`, md])),
8
+ };
9
9
 
10
- function frontmatter(md: string): Record<string, string> {
11
- const block = md.slice(4, md.indexOf("\n---", 3));
12
- return Object.fromEntries(
13
- block.split("\n").map((l) => l.match(/^([\w-]+):\s*"?(.*?)"?$/)).filter(Boolean).map((m) => [m![1], m![2]]),
14
- );
15
- }
16
-
17
- /** GitHub-style heading slug, which the skills' own links use. */
10
+ /** GitHub-style heading slug, which the skill's own links use. */
18
11
  const slug = (h: string) => h.toLowerCase().replace(/[^\w\s-]/g, "").trim().replace(/\s+/g, "-");
19
12
 
20
- describe("embedded skills", () => {
21
- test("ship core and sandbox, and not the shared parts as a skill", () => {
22
- expect(Object.keys(EMBEDDED_SKILLS).sort()).toEqual(["core", "sandbox"]);
13
+ describe("the core skill", () => {
14
+ test("is the only embedded skill", () => {
15
+ expect(Object.keys(EMBEDDED_SKILLS)).toEqual(["core"]);
23
16
  });
24
17
 
25
- test.each(["core", "sandbox"])("%s has the shared basics inlined", (name) => {
26
- const md = EMBEDDED_SKILLS[name].content;
27
- expect(md).not.toContain("<!-- include");
28
- for (const h of ["## Execution rules", "## Discovery", "## Sign-in", "## Exit codes", "## Default project", "## Billing"]) {
29
- expect(md).toContain(`\n${h}\n`);
18
+ test("ships the sandboxes, deploys and sdk references, and names each one", () => {
19
+ expect(Object.keys(core.references).sort()).toEqual(["deploys", "sandboxes", "sdk"]);
20
+ for (const ref of Object.keys(core.references)) {
21
+ expect(core.content).toContain(`--ref ${ref}`);
30
22
  }
31
23
  });
32
24
 
33
- test.each(["core", "sandbox"])("%s: every #link points at a heading in the same skill", (name) => {
34
- const md = EMBEDDED_SKILLS[name].content;
35
- const headings = new Set([...md.matchAll(/^#{2,3} (.+)$/gm)].map((m) => slug(m[1])));
36
- const links = [...md.matchAll(/\]\(#([\w-]+)\)/g)].map((m) => m[1]);
37
- expect(links.filter((l) => !headings.has(l))).toEqual([]);
38
- });
39
-
40
- test("descriptions fit the 1024-character limit and say who they are for", () => {
41
- const core = frontmatter(EMBEDDED_SKILLS.core.content);
42
- const sandbox = frontmatter(EMBEDDED_SKILLS.sandbox.content);
43
- expect(sandbox.name).toBe("lizard-sandbox");
44
- expect(core.description.length).toBeLessThanOrEqual(1024);
45
- expect(sandbox.description.length).toBeLessThanOrEqual(1024);
46
- expect(sandbox.description).toMatch(/^Creates and manages Lizard sandboxes/);
25
+ test("covers the whole platform in SKILL.md", () => {
26
+ for (const h of ["Sign-in", "Exit codes", "Default project", "Sandboxes", "Deploys", "Databases and storage", "Secrets and config", "Domains", "Billing"]) {
27
+ expect(core.content).toContain(`\n## ${h}\n`);
28
+ }
29
+ expect(core.content).not.toContain("lizard skills get sandbox ");
47
30
  });
48
31
 
49
- test("sandbox work lives only in the sandbox skill, and core points there", () => {
50
- const core = EMBEDDED_SKILLS.core.content;
51
- const sandbox = EMBEDDED_SKILLS.sandbox.content;
52
- expect(core).not.toMatch(/^## Sandboxes/m);
53
- expect(core).toContain("lizard skills get sandbox --json");
54
- for (const h of ["## Quickstart", "## Templates", "## Lifetime", "## Snapshots and pause/resume", "## From code: Lizard SDK"]) {
55
- expect(sandbox).toContain(`\n${h}\n`);
56
- }
57
- expect(sandbox).toContain("lizard skills get core --json");
32
+ test("keeps SKILL.md under 500 lines", () => {
33
+ const body = core.content.replace(/^---\n[\s\S]*?\n---\n/, "");
34
+ expect(body.split("\n").length).toBeLessThan(500);
58
35
  });
59
- });
60
36
 
61
- describe("skill routing in --help", () => {
62
- test("sandbox, volume and agents point at the sandbox skill, the rest at core", () => {
63
- expect(["sandbox", "volume", "agents"].map(skillFor)).toEqual(["sandbox", "sandbox", "sandbox"]);
64
- expect(["up", "logs", "secrets", "add"].map(skillFor)).toEqual(["core", "core", "core", "core"]);
37
+ test.each(Object.keys(files))("every #link in %s points at a heading in the same file", (file) => {
38
+ const md = files[file];
39
+ const headings = new Set([...md.matchAll(/^#{1,4} (.+)$/gm)].map((m) => slug(m[1])));
40
+ const links = [...md.matchAll(/\]\(#([\w-]+)\)/g)].map((m) => m[1]);
41
+ expect(links.filter((l) => !headings.has(l))).toEqual([]);
65
42
  });
66
- });
67
43
 
68
- describe("LIZARD_SKILLS_DIR (authoring from files)", () => {
69
- test("includes expand the same way as the embedded copy", () => {
70
- const raw = fs.readFileSync(path.join(SKILL_DATA, "sandbox", "SKILL.md"), "utf8");
71
- expect(expandIncludes(raw, SKILL_DATA)).toBe(EMBEDDED_SKILLS.sandbox.content);
44
+ test("references never link to other files", () => {
45
+ for (const md of Object.values(core.references)) {
46
+ expect(md).not.toMatch(/\]\((?!https?:|#)[^)]+\)/);
47
+ }
72
48
  });
73
49
 
74
- test("an include outside the skills folder is left as it is", () => {
75
- const line = "<!-- include: ../package.json -->";
76
- expect(expandIncludes(line, SKILL_DATA)).toBe(line);
50
+ test("its description fits the 1024-character limit", () => {
51
+ const description = core.content.match(/^description: "(.*)"$/m)![1];
52
+ expect(description.length).toBeLessThanOrEqual(1024);
53
+ expect(description).toMatch(/^Creates cloud sandboxes and deploys apps/);
77
54
  });
78
55
  });
@@ -1,2 +0,0 @@
1
- /** Which embedded skill an agent should read for a top-level command. */
2
- export declare function skillFor(topLevel: string): "sandbox" | "core";
@@ -1,7 +0,0 @@
1
- /** Top-level commands whose work the `sandbox` skill covers; everything else is in `core` (deploys). */
2
- const SANDBOX_COMMANDS = new Set(["sandbox", "volume", "agents"]);
3
- /** Which embedded skill an agent should read for a top-level command. */
4
- export function skillFor(topLevel) {
5
- return SANDBOX_COMMANDS.has(topLevel) ? "sandbox" : "core";
6
- }
7
- //# sourceMappingURL=skill-route.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"skill-route.js","sourceRoot":"","sources":["../../src/lib/skill-route.ts"],"names":[],"mappings":"AAAA,wGAAwG;AACxG,MAAM,gBAAgB,GAAG,IAAI,GAAG,CAAC,CAAC,SAAS,EAAE,QAAQ,EAAE,QAAQ,CAAC,CAAC,CAAC;AAElE,yEAAyE;AACzE,MAAM,UAAU,QAAQ,CAAC,QAAgB;IACvC,OAAO,gBAAgB,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,MAAM,CAAC;AAC7D,CAAC"}
@@ -1,92 +0,0 @@
1
- ## Execution rules
2
-
3
- 1. Prefer the `lizard` CLI. For anything it does not expose, ask the user: don't call the API directly.
4
- 2. Always pass `--json` on non-interactive calls. The CLI also switches to JSON by itself when stdout isn't a TTY.
5
- 3. For an unfamiliar command, run `lizard <cmd> --help --json` first: never guess flag shapes. See [Discovery](#discovery).
6
- 4. Resolve context before any change: `lizard status` shows the folder's link and its project. Confirm you are targeting the right project, service or sandbox.
7
- 5. For destructive actions (delete a service or sandbox, drop an addon, overwrite a project-wide secret, restart production), confirm with the user first. The CLI's own prompts only appear on a TTY.
8
-
9
- ## Discovery
10
-
11
- The CLI has ~30 subcommands. Discover at runtime:
12
-
13
- ```
14
- lizard --help --json # root + all commands + global flags + exit codes
15
- lizard <cmd> --help --json # specific command schema
16
- lizard <cmd> <sub> --help --json # nested (e.g. `lizard service set --help --json`)
17
- ```
18
-
19
- Returns `{ cli, version, command: { arguments, options, subcommands }, globalOptions, exitCodes }`.
20
-
21
- ## Sign-in
22
-
23
- Every command that needs an account checks the sign-in first. Without one, in JSON mode it fails with exit code `2` and `error.code: "NOT_AUTHENTICATED"`, and `error.body.login` holds what you need to get the user signed in. `lizard login --json` prints the same object at the top level. It never waits and never opens a browser:
24
-
25
- - `status: "pending"`: `authUrls` holds one link per sign-in method (`github`, `google`), and `authUrl` the one to use when only one applies. Ask the user how they sign in to Lizard and give them that link as a clickable URL. Someone with an account must use the same method as before: another method opens a second, empty account unless both share a verified email. Once they say the browser shows "CLI authorized", run the same command again (`nextCommand` when present); it picks the sign-in up. A link lasts 5 minutes; the next run prints a new one. Follow `instruction`.
26
- - `status: "complete"` (from `lizard login` only): `username`, `email`, `source` (`browser`, `saved`, or `env` for `LIZARD_TOKEN` / `LIZARD_API_KEY`), `account` with the plan's `state` (`trial_available`, `subscription_required`, `trialing`, `active`, `past_due`, `credits`, `enterprise`, `not_owner`, `checkout_unavailable`, `unknown`), `billingUrl`, `payUrl` when a payment failed, and an `instruction` that says what to offer and what not to touch, plus `next` commands.
27
-
28
- Never start a plan (`lizard billing start`) unless the user agrees. `lizard login` is safe to re-run: a signed-in machine skips straight to the plan. In CI nothing can be opened, so set `LIZARD_TOKEN` or `LIZARD_API_KEY`, or save a token on the machine with `printf '%s' "$TOKEN" | lizard login --token-stdin`. `lizard login --token <token>` works too, but the token lands in the process list and the shell history.
29
-
30
- ## Exit codes
31
-
32
- - `0` success — continue
33
- - `1` generic error — inspect message, surface to user
34
- - `2` auth (401/403) — not signed in: give the user the link from `error.body.login` and run the command again once they have signed in (see [Sign-in](#sign-in)). No links in the body (CI, or the platform could not be reached)? Run `lizard login --json`, or ask for `LIZARD_TOKEN`.
35
- - `3` not found (404) — wrong name / resource gone; verify with `lizard project list` / `lizard ps`
36
- - `4` timeout — retry or report
37
- - `5` cancelled by user — stop
38
-
39
- ## Default project
40
-
41
- A command that works on a project takes it from `--project`, then from the folder's link, then falls back to the account's **default project**: the oldest live project in the personal workspace, which every account gets at sign-up (`<username>'s Project`). So `lizard sandbox create`, `lizard up`, `lizard logs` and the rest work in a folder that was never linked. They say so on stderr: `Project: <name> (your default)`.
42
-
43
- - `lizard sandbox create` with no project lets the platform pick the default; its JSON carries `projectId`, `projectName` and `projectDefaulted: true`.
44
- - `lizard up` links the unlinked folder to the default project first, then creates a service named after the folder, so the next `up` from that folder updates the same service. `lizard init --force` relinks it elsewhere.
45
- - No default project (every project deleted): the old `No project linked. Run lizard init or pass --project <id>.` error.
46
-
47
- ## Billing
48
-
49
- Billing belongs to the account; these commands take no project or service selector.
50
-
51
- ```sh
52
- lizard billing --json # plan, trial, this month's usage, next charge, billingUrl
53
- lizard billing start --json # Stripe Checkout link: the Pro trial, or Pro at once
54
- lizard billing promo <code> --json # a longer trial with more trial usage
55
- lizard billing open # the Billing page
56
- lizard metrics --cost --json # spend per project; proCredits for Pro accounts
57
- ```
58
-
59
- Pro costs $19/month, taxes included, and includes $19 of usage each month for
60
- everything on the account; unused usage does not carry over. Usage above that is
61
- pay as you go at the published rates, charged to the card as it builds up. A new
62
- account starts with no plan. The 7-day trial includes $5 of usage; Checkout asks
63
- for a card and charges nothing until the trial ends, then Pro is $19/month. When
64
- the trial usage runs out, running resources pause and nothing new can be created
65
- until Pro starts. Data is kept. Promo codes work only before the first payment.
66
- Trial accounts run 1 replica per app and 5 sandboxes at once; after the first
67
- paid invoice, 5 replicas per app and 100 sandboxes (paused sandboxes don't
68
- count). Pro cards are changed or removed on the Billing page;
69
- `lizard billing payment-methods remove` works only for Enterprise accounts.
70
- Enterprise accounts pay as you go, invoiced monthly. There are no top-ups, no
71
- auto top-up and no crypto or x402 payments.
72
-
73
- When the account needs a plan or a payment, every create command (`up`, `add`,
74
- `sandbox create`, `volume create`, `domain`, redeploys) fails with HTTP 402.
75
- The JSON error has `code: "PAYMENT_REQUIRED"` (`"INSUFFICIENT_CREDITS"` from
76
- older servers), a `message` to show the user as is (its last line is the link),
77
- and `body.status` with the links:
78
-
79
- | `body.status` | What to offer |
80
- |---|---|
81
- | `trial_available` | Start the trial: `body.subscribeUrl`, or `lizard billing start` |
82
- | `subscription_required` | The trial is used: Start Pro ($19 today) the same way |
83
- | `trial_credits_used` | Trial usage is used up: Start Pro now with `lizard billing start-now` or in Billing |
84
- | `past_due` | A payment failed: pay the open invoice in Billing (`body.billingUrl`) |
85
- | `paused` | Running resources are paused, data kept. `message` says why: trial usage used up (Start Pro now, as above) or a failed payment (pay the open invoice in Billing) |
86
- | anything else | Show `message` and `body.billingUrl` |
87
-
88
- Checkout and invoice pages open in the user's browser; you cannot finish them.
89
- Give the user the link, never ask for card details in chat, and retry the
90
- failed command after they say they are done. `start-now` (charges $19 now),
91
- `cancel` and `resume` change what the user pays: run them only when the user
92
- asks. Without a terminal, `start-now` and `cancel` need `-y`.
@@ -1,7 +0,0 @@
1
- /** Top-level commands whose work the `sandbox` skill covers; everything else is in `core` (deploys). */
2
- const SANDBOX_COMMANDS = new Set(["sandbox", "volume", "agents"]);
3
-
4
- /** Which embedded skill an agent should read for a top-level command. */
5
- export function skillFor(topLevel: string): "sandbox" | "core" {
6
- return SANDBOX_COMMANDS.has(topLevel) ? "sandbox" : "core";
7
- }