Merge pull request #143 from modelstudioai/feat/skill-init-commend

feat: add skill init & opt commend flags
This commit is contained in:
gujieye
2026-08-07 17:26:21 +08:00
committed by GitHub
11 changed files with 215 additions and 52 deletions
+2
View File
@@ -93,6 +93,7 @@ import {
skillUpdate,
skillRemove,
skillList,
skillInit,
managedAgentInit,
managedAgentValidate,
managedAgentPlan,
@@ -211,6 +212,7 @@ export const commands: Record<string, AnyCommand> = {
"skill update": skillUpdate,
"skill remove": skillRemove,
"skill list": skillList,
"skill init": skillInit,
"managed-agent init": managedAgentInit,
"managed-agent validate": managedAgentValidate,
"managed-agent plan": managedAgentPlan,
+17 -9
View File
@@ -2,7 +2,6 @@ import {
BailianError,
ExitCode,
defineCommand,
detectOutputFormat,
detectInstalledAgents,
fetchSkillsIndex,
getSkillRegistryBaseUrl,
@@ -28,22 +27,31 @@ const INSTALL_CONCURRENCY = 3;
export default defineCommand({
description: "Install skills from the Bailian skill registry into local agents",
auth: "none",
usageArgs: "--name <all|name,...>",
usageArgs: "--all | --name <name,...>",
flags: {
all: {
type: "switch",
description: "Install all skills from the registry",
},
name: {
type: "string",
valueHint: "<all|name,...>",
description: "Skills to install: all or comma-separated skill names",
required: true,
valueHint: "<name,...>",
description: "Comma-separated skill names to install",
},
},
exampleArgs: ["--name all", "--name spark-video,bailian-model-recommend"],
validate(flags) {
if (flags.all && flags.name) return "Use either --all or --name, not both";
if (!flags.all && !flags.name)
return "Specify --all to install everything or --name <name,...> for specific skills";
return undefined;
},
exampleArgs: ["--all", "--name spark-video,bailian-model-recommend"],
async run(ctx) {
const format = detectOutputFormat(ctx.settings.output);
const requested = parseSkillNames(ctx.flags.name, false);
const format = ctx.settings.outputExplicit ? ctx.settings.output : "json";
const index = await fetchSkillsIndex();
const remoteNames = Object.keys(index.skills);
const names = requested === "all" ? remoteNames : requested;
const parsed = ctx.flags.all ? "all" : parseSkillNames(ctx.flags.name, false);
const names = parsed === "all" ? remoteNames : parsed;
const lock = readSkillLock();
const agents = detectInstalledAgents();
@@ -0,0 +1,107 @@
import {
BailianError,
ExitCode,
defineCommand,
detectInstalledAgents,
fetchSkillsIndex,
getSkillRegistryBaseUrl,
installSkillWithFanout,
readSkillLock,
runWithConcurrency,
writeSkillLock,
} from "bailian-cli-core";
import { emitBare, emitResult, formatTable } from "bailian-cli-runtime";
interface InitOutcome {
name: string;
status: "installed" | "failed";
publishedAt?: string;
agents?: string[];
reason?: string;
}
/** Prefix used to identify first-party Bailian skills in the registry. */
const BAILIAN_PREFIX = "bailian-";
/** Max number of skills downloading/installing at the same time. */
const INIT_CONCURRENCY = 3;
export default defineCommand({
description: "Install all bailian-* skills (one-shot bootstrap for new environments)",
auth: "none",
usageArgs: "",
exampleArgs: [""],
notes: [
"Fetches the registry index and installs every skill whose name starts with bailian-",
"Equivalent to: bl skill add --all (filtered to bailian-* skills)",
],
async run(ctx) {
const format = ctx.settings.outputExplicit ? ctx.settings.output : "json";
const index = await fetchSkillsIndex();
// Discover all bailian-* skills from the live registry index
const names = Object.keys(index.skills).filter((name) => name.startsWith(BAILIAN_PREFIX));
const lock = readSkillLock();
const agents = detectInstalledAgents();
const tasks = names.map((name) => async (): Promise<InitOutcome> => {
const entry = index.skills[name];
try {
const record = await installSkillWithFanout(
name,
entry,
agents,
lock.skills[name]?.links ?? [],
);
lock.skills[name] = record.lockEntry;
return {
name,
status: "installed",
publishedAt: entry.publishedAt,
agents: record.linkedAgents,
};
} catch (err) {
return {
name,
status: "failed",
reason: err instanceof Error ? err.message : String(err),
};
}
});
const results = await runWithConcurrency(tasks, INIT_CONCURRENCY);
writeSkillLock(lock);
if (format === "json") {
emitResult(
{
registry: getSkillRegistryBaseUrl(),
agents: agents.map((agent) => agent.id),
skills: results,
},
format,
);
} else if (results.length === 0) {
emitBare("No bailian-* skills found in the registry.");
} else {
const rows = results.map((result) => [
result.name,
result.status,
result.publishedAt ? result.publishedAt.slice(0, 10) : "-",
result.status === "installed" ? result.agents?.join(", ") || "-" : (result.reason ?? "-"),
]);
for (const line of formatTable(["NAME", "STATUS", "PUBLISHED", "AGENTS / REASON"], rows)) {
emitBare(line);
}
}
const failed = results.filter((result) => result.status === "failed");
if (failed.length > 0) {
throw new BailianError(
`${failed.length}/${results.length} skill(s) failed to install`,
ExitCode.GENERAL,
"Check the reason for failed skills in the output; network failures can be retried with bl skill init",
);
}
},
});
+1 -2
View File
@@ -1,6 +1,5 @@
import {
defineCommand,
detectOutputFormat,
computeSkillStatuses,
fetchSkillsIndex,
getSkillRegistryBaseUrl,
@@ -24,7 +23,7 @@ export default defineCommand({
"STATUS: installed | outdated | not-installed | missing (lock has it, dir deleted) | untracked (dir exists, not managed)",
],
async run(ctx) {
const format = detectOutputFormat(ctx.settings.output);
const format = ctx.settings.outputExplicit ? ctx.settings.output : "json";
// Three-way reconciliation: live remote index × skill-lock.json (installation facts) × disk
const index = await fetchSkillsIndex();
const lock = readSkillLock();
@@ -2,7 +2,6 @@ import {
BailianError,
ExitCode,
defineCommand,
detectOutputFormat,
listSkillDirsOnDisk,
parseSkillNames,
readSkillLock,
@@ -34,7 +33,7 @@ export default defineCommand({
exampleArgs: ["--name spark-video", "--name all"],
async run(ctx) {
// Purely local operation: no remote access, works offline
const format = detectOutputFormat(ctx.settings.output);
const format = ctx.settings.outputExplicit ? ctx.settings.output : "json";
const requested = parseSkillNames(ctx.flags.name, false);
const lock = readSkillLock();
const names = requested === "all" ? Object.keys(lock.skills) : requested;
+15 -8
View File
@@ -2,7 +2,6 @@ import {
BailianError,
ExitCode,
defineCommand,
detectOutputFormat,
detectInstalledAgents,
fanOutSkillToAgents,
fetchSkillsIndex,
@@ -29,19 +28,27 @@ const UPDATE_CONCURRENCY = 3;
export default defineCommand({
description: "Update installed skills to the latest registry versions",
auth: "none",
usageArgs: "[--name <all|name,...>]",
usageArgs: "[--all] [--name <name,...>]",
flags: {
all: {
type: "switch",
description: "Update all installed skills (default when neither --all nor --name is given)",
},
name: {
type: "string",
valueHint: "<all|name,...>",
description:
"Skills to update: all (default, only changed ones) or comma-separated names (force update installed skills)",
valueHint: "<name,...>",
description: "Comma-separated skill names to update (must be already installed)",
},
},
exampleArgs: ["", "--name spark-video"],
validate(flags) {
if (flags.all && flags.name) return "Use either --all or --name, not both";
return undefined;
},
exampleArgs: ["", "--all", "--name spark-video"],
async run(ctx) {
const format = detectOutputFormat(ctx.settings.output);
const requested = parseSkillNames(ctx.flags.name, true);
const format = ctx.settings.outputExplicit ? ctx.settings.output : "json";
const updateAll = ctx.flags.all || !ctx.flags.name;
const requested = updateAll ? "all" : parseSkillNames(ctx.flags.name, false);
const index = await fetchSkillsIndex();
const lock = readSkillLock();
const disk = new Set(listSkillDirsOnDisk());
+1
View File
@@ -117,3 +117,4 @@ export { default as skillAdd } from "./commands/skill/add.ts";
export { default as skillUpdate } from "./commands/skill/update.ts";
export { default as skillRemove } from "./commands/skill/remove.ts";
export { default as skillList } from "./commands/skill/list.ts";
export { default as skillInit } from "./commands/skill/init.ts";
+23 -15
View File
@@ -17,12 +17,14 @@ describe("e2e: skill", () => {
test("skill add --help exits successfully", async () => {
const { stderr, exitCode } = await runCommandE2e(SKILL_ROUTES, ["skill", "add", "--help"]);
expect(exitCode, stderr).toBe(0);
expect(stderr).toMatch(/--all/);
expect(stderr).toMatch(/--name/);
});
test("skill update --help exits successfully", async () => {
const { stderr, exitCode } = await runCommandE2e(SKILL_ROUTES, ["skill", "update", "--help"]);
expect(exitCode, stderr).toBe(0);
expect(stderr).toMatch(/--all/);
expect(stderr).toMatch(/--name/);
});
@@ -37,18 +39,37 @@ describe("e2e: skill", () => {
expect(exitCode, stderr).toBe(0);
expect(stderr).toMatch(/list|registry/i);
});
test("skill init --help exits successfully", async () => {
const { stderr, exitCode } = await runCommandE2e(SKILL_ROUTES, ["skill", "init", "--help"]);
expect(exitCode, stderr).toBe(0);
expect(stderr).toMatch(/bailian/i);
});
});
// Local-only cases: auth "none" + validation happens before any network access, no gating needed
describe("e2e: skill (local, no credentials)", () => {
test("skill add without --name errors as usage error (2)", async () => {
test("skill add without --all or --name errors as usage error (2)", async () => {
const { stdout, stderr, exitCode } = await runCommandE2e(SKILL_ROUTES, [
"skill",
"add",
"--quiet",
]);
expect(exitCode).toBe(2);
expect(`${stdout}\n${stderr}`).toMatch(/--name|Usage:/i);
expect(`${stdout}\n${stderr}`).toMatch(/--all|--name|Usage:/i);
});
test("skill add with both --all and --name errors as usage error (2)", async () => {
const { stdout, stderr, exitCode } = await runCommandE2e(SKILL_ROUTES, [
"skill",
"add",
"--all",
"--name",
"spark-video",
"--quiet",
]);
expect(exitCode).toBe(2);
expect(`${stdout}\n${stderr}`).toMatch(/--all|--name|either/i);
});
test("skill remove without --name errors as usage error (2)", async () => {
@@ -61,19 +82,6 @@ describe("e2e: skill (local, no credentials)", () => {
expect(`${stdout}\n${stderr}`).toMatch(/--name|Usage:/i);
});
test("skill add rejects mixing all with specific names (2)", async () => {
// parseSkillNames throws UsageError before fetchSkillsIndex — offline-safe
const { stdout, stderr, exitCode } = await runCommandE2e(SKILL_ROUTES, [
"skill",
"add",
"--name",
"all,spark-video",
"--quiet",
]);
expect(exitCode).toBe(2);
expect(`${stdout}\n${stderr}`).toMatch(/all/i);
});
test("skill remove of a not-installed skill fails with reason (1)", async () => {
const configDir = makeTempConfigDir();
const { stdout, exitCode } = await runCommandE2e(
@@ -165,6 +165,7 @@ export const SKILL_ROUTES: E2eRouteExports = {
"skill update": "skillUpdate",
"skill remove": "skillRemove",
"skill list": "skillList",
"skill init": "skillInit",
};
export const MANAGED_AGENT_ROUTES: E2eRouteExports = {
+2 -1
View File
@@ -52,6 +52,7 @@ Use this index for the skill-scoped quick index and global flags.
| `bl quota request` | Request a temporary quota increase | [quota.md](quota.md) |
| `bl search web` | Search the web using DashScope MCP WebSearch service | [search.md](search.md) |
| `bl skill add` | Install skills from the Bailian skill registry into local agents | [skill.md](skill.md) |
| `bl skill init` | Install all bailian-\* skills (one-shot bootstrap for new environments) | [skill.md](skill.md) |
| `bl skill list` | List registry skills and diff against local installs | [skill.md](skill.md) |
| `bl skill remove` | Remove locally installed skills (registry is untouched) | [skill.md](skill.md) |
| `bl skill update` | Update installed skills to the latest registry versions | [skill.md](skill.md) |
@@ -86,7 +87,7 @@ Use this index for the skill-scoped quick index and global flags.
| `plugin` | `install`, `link`, `list`, `remove` | [plugin.md](plugin.md) |
| `quota` | `check`, `history`, `list`, `request` | [quota.md](quota.md) |
| `search` | `web` | [search.md](search.md) |
| `skill` | `add`, `list`, `remove`, `update` | [skill.md](skill.md) |
| `skill` | `add`, `init`, `list`, `remove`, `update` | [skill.md](skill.md) |
| `text` | `chat` | [text.md](text.md) |
| `token-plan` | `add-member`, `assign-seats`, `create-key`, `list-seats` | [token-plan.md](token-plan.md) |
| `update` | `(root)` | [update.md](update.md) |
+45 -15
View File
@@ -7,12 +7,13 @@ Index: [index.md](index.md)
## Commands in this group
| Command | Description |
| ----------------- | ---------------------------------------------------------------- |
| `bl skill add` | Install skills from the Bailian skill registry into local agents |
| `bl skill list` | List registry skills and diff against local installs |
| `bl skill remove` | Remove locally installed skills (registry is untouched) |
| `bl skill update` | Update installed skills to the latest registry versions |
| Command | Description |
| ----------------- | ----------------------------------------------------------------------- |
| `bl skill add` | Install skills from the Bailian skill registry into local agents |
| `bl skill init` | Install all bailian-\* skills (one-shot bootstrap for new environments) |
| `bl skill list` | List registry skills and diff against local installs |
| `bl skill remove` | Remove locally installed skills (registry is untouched) |
| `bl skill update` | Update installed skills to the latest registry versions |
## Command details
@@ -22,24 +23,48 @@ Index: [index.md](index.md)
| --------------- | ---------------------------------------------------------------- |
| **Name** | `skill add` |
| **Description** | Install skills from the Bailian skill registry into local agents |
| **Usage** | `bl skill add --name <all\|name,...>` |
| **Usage** | `bl skill add --all \| --name <name,...>` |
#### Flags
| Flag | Type | Required | Description |
| ------------------------ | ------ | -------- | ----------------------------------------------------- |
| `--name <all\|name,...>` | string | yes | Skills to install: all or comma-separated skill names |
| Flag | Type | Required | Description |
| ------------------- | ------ | -------- | -------------------------------------- |
| `--all` | switch | no | Install all skills from the registry |
| `--name <name,...>` | string | no | Comma-separated skill names to install |
#### Examples
```bash
bl skill add --name all
bl skill add --all
```
```bash
bl skill add --name spark-video,bailian-model-recommend
```
### `bl skill init`
| Field | Value |
| --------------- | ----------------------------------------------------------------------- |
| **Name** | `skill init` |
| **Description** | Install all bailian-\* skills (one-shot bootstrap for new environments) |
| **Usage** | `bl skill init` |
#### Flags
_No command-specific flags._
#### Notes
- Fetches the registry index and installs every skill whose name starts with bailian-
- Equivalent to: bl skill add --all (filtered to bailian-\* skills)
#### Examples
```bash
bl skill init
```
### `bl skill list`
| Field | Value |
@@ -96,13 +121,14 @@ bl skill remove --name all
| --------------- | ------------------------------------------------------- |
| **Name** | `skill update` |
| **Description** | Update installed skills to the latest registry versions |
| **Usage** | `bl skill update [--name <all\|name,...>]` |
| **Usage** | `bl skill update [--all] [--name <name,...>]` |
#### Flags
| Flag | Type | Required | Description |
| ------------------------ | ------ | -------- | ----------------------------------------------------------------------------------------------------------- |
| `--name <all\|name,...>` | string | no | Skills to update: all (default, only changed ones) or comma-separated names (force update installed skills) |
| Flag | Type | Required | Description |
| ------------------- | ------ | -------- | ---------------------------------------------------------------------------- |
| `--all` | switch | no | Update all installed skills (default when neither --all nor --name is given) |
| `--name <name,...>` | string | no | Comma-separated skill names to update (must be already installed) |
#### Examples
@@ -110,6 +136,10 @@ bl skill remove --name all
bl skill update
```
```bash
bl skill update --all
```
```bash
bl skill update --name spark-video
```