@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.
- package/AGENTS.md +3 -0
- package/dist/commands/skill.js +89 -16
- package/dist/commands/skill.js.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/lib/skills-data.generated.d.ts +2 -0
- package/dist/lib/skills-data.generated.js +6 -1
- package/dist/lib/skills-data.generated.js.map +1 -1
- package/dist/lib/updater.d.ts +1 -1
- package/dist/lib/updater.js +1 -1
- package/package.json +1 -1
- package/scripts/gen-skills.mjs +24 -5
- package/skill-data/core/SKILL.md +51 -327
- package/skill-data/core/references/deploys.md +190 -0
- package/skill-data/core/references/sandboxes.md +144 -0
- package/skill-data/core/references/sdk.md +215 -0
- package/src/commands/skill.ts +95 -21
- package/src/index.ts +4 -1
- package/src/lib/updater.ts +1 -1
- package/test/unit/skills.test.ts +31 -12
package/src/commands/skill.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
54
|
-
|
|
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
|
-
|
|
123
|
-
return { name: n, description: meta.description || "" };
|
|
179
|
+
description = readSkill(src, n).meta.description || "";
|
|
124
180
|
} catch {
|
|
125
|
-
|
|
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
|
-
|
|
138
|
-
return [n, shortDesc(meta.description || "")];
|
|
195
|
+
description = shortDesc(readSkill(src, n).meta.description || "");
|
|
139
196
|
} catch {
|
|
140
|
-
|
|
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
|
|
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
|
-
|
|
152
|
-
|
|
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
|
|
235
|
+
content,
|
|
236
|
+
references: refEntries(src, name),
|
|
164
237
|
});
|
|
165
238
|
return;
|
|
166
239
|
}
|
|
167
|
-
process.stdout.write(
|
|
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
|
-
.
|
|
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
|
package/src/lib/updater.ts
CHANGED
|
@@ -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.
|
|
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
|
|
package/test/unit/skills.test.ts
CHANGED
|
@@ -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
|
|
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("
|
|
15
|
-
expect(core).
|
|
16
|
-
for (const
|
|
17
|
-
expect(core).toContain(
|
|
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
|
-
|
|
20
|
-
|
|
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
|
|
26
|
-
const
|
|
27
|
-
const
|
|
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
|
});
|