@cli-skill/cli 0.0.1-beta-d3a2d41 → 0.0.1-beta.20260402073007

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/build.ts ADDED
@@ -0,0 +1,371 @@
1
+ import { mkdir, readdir, readFile, stat, writeFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { type ZodRawShape, type ZodTypeAny } from "zod";
4
+ import type { SkillDefinition } from "@cli-skill/core";
5
+
6
+ interface FieldInfo {
7
+ schema: ZodTypeAny;
8
+ optional: boolean;
9
+ defaultValue?: unknown;
10
+ }
11
+
12
+ interface DocRow {
13
+ path: string;
14
+ type: string;
15
+ notes: string;
16
+ }
17
+
18
+ function unwrapField(schema: ZodTypeAny): FieldInfo {
19
+ let current = schema;
20
+ let optional = false;
21
+ let defaultValue: unknown;
22
+
23
+ while (true) {
24
+ if (current?._def?.typeName === "ZodOptional") {
25
+ optional = true;
26
+ current = current._def.innerType;
27
+ continue;
28
+ }
29
+
30
+ if (current?._def?.typeName === "ZodDefault") {
31
+ optional = true;
32
+ defaultValue = current._def.defaultValue();
33
+ current = current._def.innerType;
34
+ continue;
35
+ }
36
+
37
+ if (current?._def?.typeName === "ZodNullable") {
38
+ current = current._def.innerType;
39
+ continue;
40
+ }
41
+
42
+ break;
43
+ }
44
+
45
+ return { schema: current, optional, defaultValue };
46
+ }
47
+
48
+ function getTypeName(schema: ZodTypeAny): string | undefined {
49
+ return schema?._def?.typeName;
50
+ }
51
+
52
+ function getObjectShape(schema: ZodTypeAny): ZodRawShape | null {
53
+ if (getTypeName(schema) !== "ZodObject") {
54
+ return null;
55
+ }
56
+
57
+ const objectSchema = schema as ZodTypeAny & {
58
+ _def: { shape?: (() => ZodRawShape) | ZodRawShape };
59
+ shape?: ZodRawShape;
60
+ };
61
+
62
+ if (typeof objectSchema._def.shape === "function") {
63
+ return objectSchema._def.shape();
64
+ }
65
+
66
+ if (objectSchema._def.shape) {
67
+ return objectSchema._def.shape;
68
+ }
69
+
70
+ return objectSchema.shape ?? null;
71
+ }
72
+
73
+ function describeType(schema: ZodTypeAny): string {
74
+ const field = unwrapField(schema);
75
+ const base = field.schema;
76
+ const typeName = getTypeName(base);
77
+
78
+ if (typeName === "ZodString") {
79
+ return "string";
80
+ }
81
+
82
+ if (typeName === "ZodNumber") {
83
+ return "number";
84
+ }
85
+
86
+ if (typeName === "ZodBoolean") {
87
+ return "boolean";
88
+ }
89
+
90
+ if (typeName === "ZodLiteral") {
91
+ return JSON.stringify(base._def.value);
92
+ }
93
+
94
+ if (typeName === "ZodEnum") {
95
+ return (base as ZodTypeAny & { options: string[] }).options
96
+ .map((item: string) => JSON.stringify(item))
97
+ .join(" | ");
98
+ }
99
+
100
+ if (typeName === "ZodArray") {
101
+ return `array<${describeType(base._def.type)}>`;
102
+ }
103
+
104
+ if (typeName === "ZodObject") {
105
+ return "object";
106
+ }
107
+
108
+ return "unknown";
109
+ }
110
+
111
+ function describeNotes(schema: ZodTypeAny): string {
112
+ const field = unwrapField(schema);
113
+ const notes: string[] = [];
114
+
115
+ if (field.optional) {
116
+ notes.push("可选");
117
+ }
118
+
119
+ if (typeof field.defaultValue !== "undefined") {
120
+ notes.push(`默认值: ${JSON.stringify(field.defaultValue)}`);
121
+ }
122
+
123
+ return notes.join("; ");
124
+ }
125
+
126
+ function collectShapeRows(shape: ZodRawShape, prefix = ""): DocRow[] {
127
+ const rows: DocRow[] = [];
128
+
129
+ for (const [key, schema] of Object.entries(shape)) {
130
+ const field = unwrapField(schema);
131
+ const rowPath = prefix ? `${prefix}.${key}` : key;
132
+ rows.push({
133
+ path: rowPath,
134
+ type: describeType(schema),
135
+ notes: describeNotes(schema),
136
+ });
137
+
138
+ const childShape = getObjectShape(field.schema);
139
+ if (childShape) {
140
+ rows.push(...collectShapeRows(childShape, rowPath));
141
+ }
142
+ }
143
+
144
+ return rows;
145
+ }
146
+
147
+ function collectSchemaRows(schema: ZodTypeAny): DocRow[] {
148
+ const field = unwrapField(schema);
149
+ const shape = getObjectShape(field.schema);
150
+ if (!shape) {
151
+ return [
152
+ {
153
+ path: "(value)",
154
+ type: describeType(schema),
155
+ notes: describeNotes(schema),
156
+ },
157
+ ];
158
+ }
159
+
160
+ const rows = collectShapeRows(shape);
161
+ return rows.length > 0 ? rows : [];
162
+ }
163
+
164
+ function escapeCell(value: string): string {
165
+ return value.replaceAll("|", "\\|").replaceAll("\n", "<br/>");
166
+ }
167
+
168
+ function renderTable(headers: string[], rows: string[][]): string {
169
+ if (rows.length === 0) {
170
+ return "- 无";
171
+ }
172
+
173
+ const lines = [
174
+ `| ${headers.join(" | ")} |`,
175
+ `| ${headers.map(() => "---").join(" | ")} |`,
176
+ ...rows.map((row) => `| ${row.map((cell) => escapeCell(cell || " ")).join(" | ")} |`),
177
+ ];
178
+
179
+ return lines.join("\n");
180
+ }
181
+
182
+ function renderConfigSection(skill: SkillDefinition): string {
183
+ const rows = collectShapeRows(skill.config).map((row) => [row.path, row.type, row.notes || ""]);
184
+ return renderTable(["字段", "类型", "说明"], rows.length > 0 ? rows : [["-", "-", "-"]]);
185
+ }
186
+
187
+ function renderToolsSection(skill: SkillDefinition): string {
188
+ const blocks: string[] = [];
189
+
190
+ const toolTable = renderTable(
191
+ ["工具", "说明"],
192
+ skill.tools.map((tool) => [tool.name, tool.description]),
193
+ );
194
+ blocks.push(toolTable);
195
+
196
+ for (const tool of skill.tools) {
197
+ blocks.push("");
198
+ blocks.push(`### \`${tool.name}\``);
199
+ blocks.push("");
200
+ blocks.push("**例子**");
201
+ blocks.push("");
202
+ const exampleRows =
203
+ tool.examples && tool.examples.length > 0
204
+ ? tool.examples.map((example) => [example.scenario, example.command])
205
+ : [["默认调用", `${skill.name} run ${tool.name} '<json-input>'`]];
206
+ blocks.push(renderTable(["场景", "命令"], exampleRows));
207
+ blocks.push("");
208
+ blocks.push("**输入**");
209
+ blocks.push("");
210
+ blocks.push(
211
+ renderTable(
212
+ ["字段", "类型", "说明"],
213
+ collectSchemaRows(tool.inputSchema).map((row) => [row.path, row.type, row.notes || ""]),
214
+ ),
215
+ );
216
+ blocks.push("");
217
+ blocks.push("**输出**");
218
+ blocks.push("");
219
+ blocks.push(
220
+ renderTable(
221
+ ["字段", "类型", "说明"],
222
+ collectSchemaRows(tool.outputSchema).map((row) => [row.path, row.type, row.notes || ""]),
223
+ ),
224
+ );
225
+ }
226
+
227
+ return blocks.join("\n");
228
+ }
229
+
230
+ function getDefaultSkillDocsMarkdown(): string {
231
+ return [
232
+ "---",
233
+ `name: {{name}}`,
234
+ `description: {{description}}`,
235
+ "---",
236
+ "",
237
+ `# {{name}}`,
238
+ "",
239
+ "## 概述",
240
+ "",
241
+ "{{overview}}",
242
+ "",
243
+ "## Tool Reference",
244
+ "",
245
+ "{{toolReference}}",
246
+ "",
247
+ "## Config Reference",
248
+ "",
249
+ "{{configReference}}",
250
+ ].join("\n");
251
+ }
252
+
253
+ function getDefaultOpenAIYamlTemplate(): string {
254
+ return [
255
+ "display_name: {{name}}",
256
+ "short_description: {{description}}",
257
+ "default_prompt: 使用 {{name}} 处理 {{name}} 相关任务。",
258
+ "",
259
+ ].join("\n");
260
+ }
261
+
262
+ function buildTemplateValues(skill: SkillDefinition): Record<string, string> {
263
+ return {
264
+ name: skill.name,
265
+ description: skill.description,
266
+ overview: skill.overview ?? `${skill.name} skill。`,
267
+ toolReference: renderToolsSection(skill),
268
+ configReference: renderConfigSection(skill),
269
+ };
270
+ }
271
+
272
+ function renderSkillTemplate(template: string, skill: SkillDefinition): string {
273
+ const values = buildTemplateValues(skill);
274
+ return template.replace(/\{\{\s*(\w+)\s*\}\}/g, (_match, key: string) => values[key] ?? "");
275
+ }
276
+
277
+ function isTemplatedTextFile(filePath: string): boolean {
278
+ return [".md", ".yaml", ".yml"].includes(path.extname(filePath));
279
+ }
280
+
281
+ async function copySkillTemplateDirectory(sourceDir: string, targetDir: string, skill: SkillDefinition): Promise<void> {
282
+ const entries = await readdir(sourceDir, { withFileTypes: true });
283
+
284
+ for (const entry of entries) {
285
+ const sourcePath = path.join(sourceDir, entry.name);
286
+ const targetPath = path.join(targetDir, entry.name);
287
+
288
+ if (entry.isDirectory()) {
289
+ await mkdir(targetPath, { recursive: true });
290
+ await copySkillTemplateDirectory(sourcePath, targetPath, skill);
291
+ continue;
292
+ }
293
+
294
+ if (!entry.isFile()) {
295
+ continue;
296
+ }
297
+
298
+ const template = await readFile(sourcePath, "utf8");
299
+ const content = isTemplatedTextFile(sourcePath)
300
+ ? renderSkillTemplate(template, skill)
301
+ : template;
302
+ await writeFile(targetPath, `${content}${content.endsWith("\n") ? "" : "\n"}`, "utf8");
303
+ }
304
+ }
305
+
306
+ async function getLegacySkillDocsTemplate(skill: SkillDefinition): Promise<string | null> {
307
+ const skillRoot = skill.rootDir ?? process.cwd();
308
+ const templatePath = path.join(skillRoot, "src", "skill.md");
309
+
310
+ try {
311
+ return await readFile(templatePath, "utf8");
312
+ } catch {
313
+ return null;
314
+ }
315
+ }
316
+
317
+ async function getSkillTemplateDirectory(skill: SkillDefinition): Promise<string | null> {
318
+ const skillRoot = skill.rootDir ?? process.cwd();
319
+ const templateDir = path.join(skillRoot, "src", "skill");
320
+
321
+ try {
322
+ const templateStat = await stat(templateDir);
323
+ return templateStat.isDirectory() ? templateDir : null;
324
+ } catch {
325
+ return null;
326
+ }
327
+ }
328
+
329
+ async function getSkillDocsTemplateFromDirectory(skill: SkillDefinition): Promise<string | null> {
330
+ const templateDir = await getSkillTemplateDirectory(skill);
331
+ if (!templateDir) {
332
+ return null;
333
+ }
334
+
335
+ try {
336
+ return await readFile(path.join(templateDir, "SKILL.md"), "utf8");
337
+ } catch {
338
+ return null;
339
+ }
340
+ }
341
+
342
+ export async function renderSkillDocsMarkdown(skill: SkillDefinition): Promise<string> {
343
+ const directoryTemplate = await getSkillDocsTemplateFromDirectory(skill);
344
+ const legacyTemplate = directoryTemplate ? null : await getLegacySkillDocsTemplate(skill);
345
+ return renderSkillTemplate(directoryTemplate ?? legacyTemplate ?? getDefaultSkillDocsMarkdown(), skill);
346
+ }
347
+
348
+ export function renderSkillOpenAIYaml(skill: SkillDefinition): string {
349
+ return renderSkillTemplate(getDefaultOpenAIYamlTemplate(), skill);
350
+ }
351
+
352
+ export async function writeSkillDocsMarkdown(skill: SkillDefinition): Promise<string> {
353
+ const skillRoot = skill.rootDir ?? process.cwd();
354
+ const targetDir = path.join(skillRoot, "skill");
355
+ const skillMdPath = path.join(targetDir, "SKILL.md");
356
+ const templateDir = await getSkillTemplateDirectory(skill);
357
+
358
+ await mkdir(targetDir, { recursive: true });
359
+
360
+ if (templateDir) {
361
+ await copySkillTemplateDirectory(templateDir, targetDir, skill);
362
+ } else {
363
+ const skillMarkdown = await renderSkillDocsMarkdown(skill);
364
+ const openAIYamlPath = path.join(targetDir, "agents", "openai.yaml");
365
+ await mkdir(path.dirname(openAIYamlPath), { recursive: true });
366
+ await writeFile(skillMdPath, `${skillMarkdown}\n`, "utf8");
367
+ await writeFile(openAIYamlPath, renderSkillOpenAIYaml(skill), "utf8");
368
+ }
369
+
370
+ return skillMdPath;
371
+ }
@@ -0,0 +1,170 @@
1
+ import type { CAC } from "cac";
2
+ import { runCli } from "@cli-skill/core";
3
+ import {
4
+ getConfigValue,
5
+ loadBrowserSkillCliConfig,
6
+ parseConfigCliValue,
7
+ saveBrowserSkillCliConfig,
8
+ setConfigValue,
9
+ unsetConfigValue,
10
+ } from "../config";
11
+ import { writeSkillDocsMarkdown } from "../build";
12
+ import { loadSkillDefinition } from "../project";
13
+ import {
14
+ getLocalSkillProjectDir,
15
+ mountSkillProject,
16
+ resolveSkillProject,
17
+ unmountSkillProject,
18
+ } from "../registry";
19
+ import { runBun } from "../bun";
20
+
21
+ function printConfigValue(value: unknown): void {
22
+ if (typeof value === "string") {
23
+ console.log(value);
24
+ return;
25
+ }
26
+
27
+ if (typeof value === "undefined") {
28
+ console.log("undefined");
29
+ return;
30
+ }
31
+
32
+ console.log(JSON.stringify(value, null, 2));
33
+ }
34
+
35
+ function printSkillUsage(skillName: string): never {
36
+ throw new Error(
37
+ [
38
+ `Usage: cli-skill ${skillName} run <toolName> [rawInput]`,
39
+ ` cli-skill ${skillName} list`,
40
+ ` cli-skill ${skillName} config get [keyPath]`,
41
+ ` cli-skill ${skillName} config set <keyPath> <value>`,
42
+ ` cli-skill ${skillName} config unset <keyPath>`,
43
+ ` cli-skill ${skillName} mount [targetPath]`,
44
+ ` cli-skill ${skillName} unmount [targetPath]`,
45
+ ` cli-skill ${skillName} build`,
46
+ ` cli-skill ${skillName} publish [--dry-run] [--tag <tag>]`,
47
+ ].join("\n"),
48
+ );
49
+ }
50
+
51
+ async function handleSkillConfig(skillName: string, args: string[]): Promise<void> {
52
+ const [subcommand, keyPath, rawValue] = args;
53
+ const configRoot = `skillConfig.${skillName}`;
54
+
55
+ if (subcommand === "get") {
56
+ const currentConfig = await loadBrowserSkillCliConfig();
57
+ const value = getConfigValue(currentConfig, keyPath ? `${configRoot}.${keyPath}` : configRoot);
58
+ printConfigValue(value);
59
+ return;
60
+ }
61
+
62
+ if (subcommand === "set") {
63
+ if (!keyPath || typeof rawValue === "undefined") {
64
+ throw new Error(`Usage: cli-skill ${skillName} config set <keyPath> <value>`);
65
+ }
66
+
67
+ const currentConfig = await loadBrowserSkillCliConfig();
68
+ const nextConfig = setConfigValue(
69
+ currentConfig,
70
+ `${configRoot}.${keyPath}`,
71
+ parseConfigCliValue(rawValue),
72
+ );
73
+ await saveBrowserSkillCliConfig(nextConfig);
74
+ return;
75
+ }
76
+
77
+ if (subcommand === "unset") {
78
+ if (!keyPath) {
79
+ throw new Error(`Usage: cli-skill ${skillName} config unset <keyPath>`);
80
+ }
81
+
82
+ const currentConfig = await loadBrowserSkillCliConfig();
83
+ const nextConfig = unsetConfigValue(currentConfig, `${configRoot}.${keyPath}`);
84
+ await saveBrowserSkillCliConfig(nextConfig);
85
+ return;
86
+ }
87
+
88
+ throw new Error(
89
+ `Usage: cli-skill ${skillName} config get [keyPath] | set <keyPath> <value> | unset <keyPath>`,
90
+ );
91
+ }
92
+
93
+ async function handleSkillCommand(skillName: string, args: string[] = []): Promise<void> {
94
+ if (args.length === 0) {
95
+ printSkillUsage(skillName);
96
+ }
97
+
98
+ const [subcommand, ...rest] = args;
99
+ const resolved = await resolveSkillProject(skillName);
100
+
101
+ if (subcommand === "run" || subcommand === "list") {
102
+ const skill = await loadSkillDefinition(resolved.projectPath);
103
+ const exitCode = await runCli(
104
+ skill,
105
+ subcommand === "list" ? ["list"] : ["run", ...rest],
106
+ { rootDir: resolved.projectPath },
107
+ );
108
+ process.exitCode = exitCode;
109
+ return;
110
+ }
111
+
112
+ if (subcommand === "config") {
113
+ await handleSkillConfig(skillName, rest);
114
+ return;
115
+ }
116
+
117
+ if (subcommand === "mount") {
118
+ const [targetPath] = rest;
119
+ const mountedPath = await mountSkillProject(resolved.projectPath, { skillRoot: targetPath });
120
+ console.log(mountedPath);
121
+ return;
122
+ }
123
+
124
+ if (subcommand === "unmount") {
125
+ const [targetPath] = rest;
126
+ const mountedPath = await unmountSkillProject(resolved.projectPath, { skillRoot: targetPath });
127
+ console.log(mountedPath);
128
+ return;
129
+ }
130
+
131
+ if (subcommand === "build") {
132
+ const skill = await loadSkillDefinition(resolved.projectPath);
133
+ const updatedPath = await writeSkillDocsMarkdown(skill);
134
+ console.log(updatedPath);
135
+ return;
136
+ }
137
+
138
+ if (subcommand === "publish") {
139
+ const localProjectDir = await getLocalSkillProjectDir(skillName);
140
+ const publishArgs = ["publish"];
141
+ const dryRunIndex = rest.indexOf("--dry-run");
142
+ const tagIndex = rest.indexOf("--tag");
143
+
144
+ if (dryRunIndex >= 0) {
145
+ publishArgs.push("--dry-run");
146
+ }
147
+
148
+ if (tagIndex >= 0) {
149
+ const tag = rest[tagIndex + 1];
150
+ if (!tag) {
151
+ throw new Error(`Usage: cli-skill ${skillName} publish [--dry-run] [--tag <tag>]`);
152
+ }
153
+ publishArgs.push("--tag", tag);
154
+ }
155
+
156
+ await runBun(publishArgs, localProjectDir);
157
+ return;
158
+ }
159
+
160
+ printSkillUsage(skillName);
161
+ }
162
+
163
+ export function registerSkillCommand(cli: CAC): void {
164
+ cli
165
+ .command("<skillName> [...args]", "Operate on a specific cli skill")
166
+ .allowUnknownOptions()
167
+ .action(async (skillName: string, args: string[] = []) => {
168
+ await handleSkillCommand(skillName, args);
169
+ });
170
+ }
package/src/config.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { mkdir, readFile, writeFile } from "node:fs/promises";
2
2
  import get from "lodash/get";
3
3
  import set from "lodash/set";
4
+ import unset from "lodash/unset";
4
5
  import os from "node:os";
5
6
  import path from "node:path";
6
7
 
@@ -11,20 +12,24 @@ export interface BrowserSkillCliConfig {
11
12
  skillConfig?: Record<string, Record<string, unknown>>;
12
13
  }
13
14
 
15
+ function getUserHome(): string {
16
+ return process.env.HOME || os.homedir();
17
+ }
18
+
14
19
  function resolveUserPath(inputPath: string): string {
15
20
  if (inputPath === "~") {
16
- return os.homedir();
21
+ return getUserHome();
17
22
  }
18
23
 
19
24
  if (inputPath.startsWith("~/")) {
20
- return path.join(os.homedir(), inputPath.slice(2));
25
+ return path.join(getUserHome(), inputPath.slice(2));
21
26
  }
22
27
 
23
28
  return inputPath;
24
29
  }
25
30
 
26
31
  export function getBrowserSkillHome(): string {
27
- return path.join(os.homedir(), ".cli-skill");
32
+ return path.join(getUserHome(), ".cli-skill");
28
33
  }
29
34
 
30
35
  export function getBrowserSkillConfigPath(): string {
@@ -46,7 +51,7 @@ export function getDefaultBrowserSkillCliConfig(): Required<
46
51
  return {
47
52
  skillsRoot: path.join(getBrowserSkillHome(), "skills"),
48
53
  installedSkillsRoot: path.join(getBrowserSkillHome(), "installed"),
49
- agentsSkillsRoot: path.join(os.homedir(), ".agents", "skills"),
54
+ agentsSkillsRoot: path.join(getUserHome(), ".agents", "skills"),
50
55
  skillConfig: {},
51
56
  };
52
57
  }
@@ -117,6 +122,15 @@ export function setConfigValue(
117
122
  return nextConfig;
118
123
  }
119
124
 
125
+ export function unsetConfigValue(
126
+ config: BrowserSkillCliConfig,
127
+ keyPath: string,
128
+ ): BrowserSkillCliConfig {
129
+ const nextConfig = structuredClone(config);
130
+ unset(nextConfig as object, keyPath);
131
+ return nextConfig;
132
+ }
133
+
120
134
  export function parseConfigCliValue(rawValue: string): unknown {
121
135
  if (rawValue === "true") {
122
136
  return true;
package/src/constants.ts CHANGED
@@ -1,10 +1,20 @@
1
1
  import path from "node:path";
2
+ import { access } from "node:fs/promises";
2
3
  import { getResolvedBrowserSkillCliConfig } from "./config";
3
4
 
4
5
  export const LOCAL_CORE_PACKAGE_PATH = path.resolve(import.meta.dirname, "../../core");
5
6
  export const LOCAL_TEMPLATE_PACKAGE_PATH = path.resolve(import.meta.dirname, "../../templates");
6
7
  export const DEFAULT_TEMPLATE_NAME = "basic";
7
8
 
9
+ export async function hasLocalTemplatesPackage(): Promise<boolean> {
10
+ try {
11
+ await access(path.join(LOCAL_TEMPLATE_PACKAGE_PATH, "package.json"));
12
+ return true;
13
+ } catch {
14
+ return false;
15
+ }
16
+ }
17
+
8
18
  export async function getDefaultSkillsRoot(): Promise<string> {
9
19
  const config = await getResolvedBrowserSkillCliConfig();
10
20
  return config.skillsRoot;
package/src/index.ts CHANGED
@@ -1,10 +1,11 @@
1
1
  import { createApp } from "./app";
2
2
  import { ensureBrowserSkillCliConfig } from "./config";
3
+ import { getCliVersion } from "./version";
3
4
  export { createSkillProject } from "./project";
4
5
 
5
6
  async function main(argv = process.argv.slice(2)): Promise<void> {
6
7
  await ensureBrowserSkillCliConfig();
7
- const cli = createApp();
8
+ const cli = createApp(await getCliVersion());
8
9
  cli.parse(["node", "cli-skill", ...argv], { run: false });
9
10
  await cli.runMatchedCommand();
10
11
  }