@lizard-build/cli 4.0.42 → 4.0.43

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/index.ts CHANGED
@@ -6,6 +6,7 @@ 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";
9
10
 
10
11
  const BANNER = chalk.rgb(16, 185, 129)(
11
12
  [
@@ -27,34 +28,34 @@ const BANNER = chalk.rgb(16, 185, 129)(
27
28
  // of guessing commands from flag docs alone.
28
29
  const AGENTS_HELP = [
29
30
  chalk.bold("Start here (for AI agents):"),
30
- " " + chalk.cyan("lizard skills get core"),
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"),
31
33
  "",
32
34
  chalk.dim(
33
- " Skills ship with the CLI (always version-matched) and cover the full app",
35
+ " Skills ship with the CLI (always version-matched) and come with copy-paste",
34
36
  ),
35
37
  chalk.dim(
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.",
38
+ " examples. Prefer them over guessing commands from flag docs alone.",
40
39
  ),
41
40
  ].join("\n");
42
41
 
42
+
43
43
  // Compact form of the same pointer, shown on every subcommand's `--help` so the
44
44
  // per-command flag docs never read as the whole story. Mirrors the `agents`
45
45
  // block that `--help --json` already emits for every command; one tight line so
46
46
  // it sits above a subcommand's Usage without crowding it.
47
- const AGENTS_HELP_COMPACT =
47
+ const agentsHelpCompact = (skill: string) =>
48
48
  chalk.bold("For AI agents:") +
49
49
  " run " +
50
- chalk.cyan("lizard skills get core") +
50
+ chalk.cyan(`lizard skills get ${skill}`) +
51
51
  chalk.dim(" for version-matched usage and examples.");
52
52
 
53
- /** Attach the compact agents pointer to every (nested) subcommand's help. */
54
- function addAgentsPointerToSubcommands(cmd: Command) {
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) {
55
55
  for (const sub of cmd.commands) {
56
- sub.addHelpText("before", AGENTS_HELP_COMPACT + "\n");
57
- addAgentsPointerToSubcommands(sub);
56
+ const own = skill ?? skillFor(sub.name());
57
+ sub.addHelpText("before", agentsHelpCompact(own) + "\n");
58
+ addAgentsPointerToSubcommands(sub, own);
58
59
  }
59
60
  }
60
61
 
@@ -274,6 +275,13 @@ function findTargetCommand(
274
275
  return { target: cur, unknown: null };
275
276
  }
276
277
 
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
+
277
285
  function dumpOption(o: any) {
278
286
  return {
279
287
  flags: o.flags,
@@ -386,8 +394,9 @@ async function main() {
386
394
  cli: "lizard",
387
395
  version: CURRENT_VERSION,
388
396
  agents: {
389
- start: "lizard skills get core",
390
- note: "Skills ship with this CLI version and cover the full app lifecycle (deploy, link, addons, logs, scaling, secrets, domains) with copy-paste examples. Prefer this over guessing commands from flag docs alone.",
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.",
391
400
  },
392
401
  command: dumpCommand(target),
393
402
  globalOptions: isRoot
@@ -0,0 +1,7 @@
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
+ }
@@ -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.42";
8
+ export const CURRENT_VERSION = "4.0.43";
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
 
@@ -0,0 +1,78 @@
1
+ import { describe, expect, test, afterEach } from "vitest";
2
+ import * as path from "node:path";
3
+ 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
+
8
+ const SKILL_DATA = path.resolve(import.meta.dirname, "../../skill-data");
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. */
18
+ const slug = (h: string) => h.toLowerCase().replace(/[^\w\s-]/g, "").trim().replace(/\s+/g, "-");
19
+
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"]);
23
+ });
24
+
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`);
30
+ }
31
+ });
32
+
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/);
47
+ });
48
+
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");
58
+ });
59
+ });
60
+
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"]);
65
+ });
66
+ });
67
+
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);
72
+ });
73
+
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);
77
+ });
78
+ });