@lizard-build/cli 4.0.44 → 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,11 +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;
21
+ refs(name: string): string[];
22
+ readRef(name: string, ref: string): string | null;
23
+ pathOf(name?: string, ref?: string): string;
19
24
  }
20
25
 
21
26
  function fsSource(dir: string): Source {
@@ -42,7 +47,29 @@ function fsSource(dir: string): Source {
42
47
  return null;
43
48
  }
44
49
  },
45
- pathOf: (name) => (name ? path.join(dir, name, "SKILL.md") : dir),
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");
63
+ } catch {
64
+ return null;
65
+ }
66
+ },
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
+ },
46
73
  };
47
74
  }
48
75
 
@@ -50,8 +77,14 @@ function embeddedSource(): Source {
50
77
  return {
51
78
  list: () => Object.keys(EMBEDDED_SKILLS).sort(),
52
79
  read: (name) => EMBEDDED_SKILLS[name]?.content ?? null,
53
- pathOf: (name) =>
54
- 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
+ },
55
88
  };
56
89
  }
57
90
 
@@ -97,6 +130,29 @@ function readSkill(
97
130
  return { md, meta: parseFrontmatter(md) };
98
131
  }
99
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
+
100
156
  function shortDesc(desc: string, max = 100): string {
101
157
  const oneLine = desc.replace(/\s+/g, " ").trim();
102
158
  return oneLine.length > max ? oneLine.slice(0, max - 1) + "…" : oneLine;
@@ -118,12 +174,13 @@ export function registerSkill(program: Command) {
118
174
  printJSON({
119
175
  dir: src.pathOf(),
120
176
  skills: names.map((n) => {
177
+ let description = "";
121
178
  try {
122
- const { meta } = readSkill(src, n);
123
- return { name: n, description: meta.description || "" };
179
+ description = readSkill(src, n).meta.description || "";
124
180
  } catch {
125
- return { name: n, description: "" };
181
+ // unreadable SKILL.md: list it without a description
126
182
  }
183
+ return { name: n, description, references: src.refs(n) };
127
184
  }),
128
185
  });
129
186
  return;
@@ -133,38 +190,54 @@ export function registerSkill(program: Command) {
133
190
  return;
134
191
  }
135
192
  const rows = names.map((n) => {
193
+ let description = "";
136
194
  try {
137
- const { meta } = readSkill(src, n);
138
- return [n, shortDesc(meta.description || "")];
195
+ description = shortDesc(readSkill(src, n).meta.description || "");
139
196
  } catch {
140
- return [n, ""];
197
+ // unreadable SKILL.md: list it without a description
141
198
  }
199
+ return [n, description, src.refs(n).join(", ")];
142
200
  });
143
- table(["name", "description"], rows);
201
+ table(["name", "description", "references"], rows);
144
202
  });
145
203
 
146
204
  skill
147
205
  .command("get")
148
- .description("Print a skill's full content (markdown)")
206
+ .description("Print a skill's content (markdown), or one of its references")
149
207
  .argument("<name>", "skill name (e.g. core)")
150
- .option(
151
- "--full",
152
- "Include reference files (reserved; currently equivalent to default)",
153
- )
154
- .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 }) => {
155
211
  const src = getSource();
156
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
+ }
157
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
+ }
158
230
  if (isJSONMode()) {
159
231
  printJSON({
160
232
  name,
161
233
  path: src.pathOf(name),
162
234
  frontmatter: meta,
163
- content: md,
235
+ content,
236
+ references: refEntries(src, name),
164
237
  });
165
238
  return;
166
239
  }
167
- process.stdout.write(md.endsWith("\n") ? md : md + "\n");
240
+ process.stdout.write(content.endsWith("\n") ? content : content + "\n");
168
241
  } catch (e: any) {
169
242
  fail(e.message, 3);
170
243
  }
@@ -174,9 +247,10 @@ export function registerSkill(program: Command) {
174
247
  .command("path")
175
248
  .description("Print the filesystem path of a skill (or the skills root)")
176
249
  .argument("[name]", "skill name; omit to print the skills directory")
177
- .action((name?: string) => {
250
+ .option("--ref <ref>", "print the path of one reference instead")
251
+ .action((name: string | undefined, opts: { ref?: string }) => {
178
252
  const src = getSource();
179
- const target = src.pathOf(name);
253
+ const target = src.pathOf(name, name ? opts.ref : undefined);
180
254
  if (isJSONMode()) {
181
255
  printJSON({ path: target });
182
256
  return;
package/src/index.ts CHANGED
@@ -38,6 +38,9 @@ const AGENTS_HELP = [
38
38
  chalk.dim(
39
39
  " copy-paste examples. Prefer this over guessing commands from flag docs alone.",
40
40
  ),
41
+ chalk.dim(
42
+ " For more depth: lizard skills get core --ref sandboxes | deploys | sdk",
43
+ ),
41
44
  ].join("\n");
42
45
 
43
46
  // Compact form of the same pointer, shown on every subcommand's `--help` so the
@@ -387,7 +390,7 @@ async function main() {
387
390
  version: CURRENT_VERSION,
388
391
  agents: {
389
392
  start: "lizard skills get core",
390
- 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.",
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`.",
391
394
  },
392
395
  command: dumpCommand(target),
393
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.44";
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,7 +1,11 @@
1
1
  import { describe, expect, test } from "vitest";
2
2
  import { EMBEDDED_SKILLS } from "../../src/lib/skills-data.generated.js";
3
3
 
4
- const core = EMBEDDED_SKILLS.core.content;
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
+ };
5
9
 
6
10
  /** GitHub-style heading slug, which the skill's own links use. */
7
11
  const slug = (h: string) => h.toLowerCase().replace(/[^\w\s-]/g, "").trim().replace(/\s+/g, "-");
@@ -11,25 +15,40 @@ describe("the core skill", () => {
11
15
  expect(Object.keys(EMBEDDED_SKILLS)).toEqual(["core"]);
12
16
  });
13
17
 
14
- test("covers sandboxes as well as deploys", () => {
15
- expect(core).toContain("\n## Sandboxes\n");
16
- for (const h of ["Quickstart", "Templates", "Running commands", "Files", "Ports", "Lifetime", "Snapshots and pause/resume", "From code: Lizard SDK"]) {
17
- expect(core).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}`);
18
22
  }
19
- for (const h of ["Deploying", "Secrets", "Managed addons", "Sign-in", "Exit codes", "Default project", "Billing"]) {
20
- expect(core).toContain(`\n## ${h}\n`);
23
+ });
24
+
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`);
21
28
  }
22
- expect(core).not.toContain("lizard skills get sandbox");
29
+ expect(core.content).not.toContain("lizard skills get sandbox ");
30
+ });
31
+
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);
23
35
  });
24
36
 
25
- test("every #link points at a heading in the skill", () => {
26
- const headings = new Set([...core.matchAll(/^#{2,4} (.+)$/gm)].map((m) => slug(m[1])));
27
- const links = [...core.matchAll(/\]\(#([\w-]+)\)/g)].map((m) => m[1]);
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]);
28
41
  expect(links.filter((l) => !headings.has(l))).toEqual([]);
29
42
  });
30
43
 
44
+ test("references never link to other files", () => {
45
+ for (const md of Object.values(core.references)) {
46
+ expect(md).not.toMatch(/\]\((?!https?:|#)[^)]+\)/);
47
+ }
48
+ });
49
+
31
50
  test("its description fits the 1024-character limit", () => {
32
- const description = core.match(/^description: "(.*)"$/m)![1];
51
+ const description = core.content.match(/^description: "(.*)"$/m)![1];
33
52
  expect(description.length).toBeLessThanOrEqual(1024);
34
53
  expect(description).toMatch(/^Creates cloud sandboxes and deploys apps/);
35
54
  });