diff --git a/packages/cli/src/commands.ts b/packages/cli/src/commands.ts index 2cbda30d..652da452 100644 --- a/packages/cli/src/commands.ts +++ b/packages/cli/src/commands.ts @@ -93,6 +93,7 @@ import { skillUpdate, skillRemove, skillList, + skillInit, managedAgentInit, managedAgentValidate, managedAgentPlan, @@ -211,6 +212,7 @@ export const commands: Record = { "skill update": skillUpdate, "skill remove": skillRemove, "skill list": skillList, + "skill init": skillInit, "managed-agent init": managedAgentInit, "managed-agent validate": managedAgentValidate, "managed-agent plan": managedAgentPlan, diff --git a/packages/commands/src/commands/skill/add.ts b/packages/commands/src/commands/skill/add.ts index 073a80a9..5c5c035c 100644 --- a/packages/commands/src/commands/skill/add.ts +++ b/packages/commands/src/commands/skill/add.ts @@ -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 ", + usageArgs: "--all | --name ", flags: { + all: { + type: "switch", + description: "Install all skills from the registry", + }, name: { type: "string", - valueHint: "", - description: "Skills to install: all or comma-separated skill names", - required: true, + valueHint: "", + 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 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(); diff --git a/packages/commands/src/commands/skill/init.ts b/packages/commands/src/commands/skill/init.ts new file mode 100644 index 00000000..22d5f1d2 --- /dev/null +++ b/packages/commands/src/commands/skill/init.ts @@ -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 => { + 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", + ); + } + }, +}); diff --git a/packages/commands/src/commands/skill/list.ts b/packages/commands/src/commands/skill/list.ts index aa6552cc..1a21887d 100644 --- a/packages/commands/src/commands/skill/list.ts +++ b/packages/commands/src/commands/skill/list.ts @@ -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(); diff --git a/packages/commands/src/commands/skill/remove.ts b/packages/commands/src/commands/skill/remove.ts index e51106c2..50749629 100644 --- a/packages/commands/src/commands/skill/remove.ts +++ b/packages/commands/src/commands/skill/remove.ts @@ -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; diff --git a/packages/commands/src/commands/skill/update.ts b/packages/commands/src/commands/skill/update.ts index 61e1b1f6..249c808f 100644 --- a/packages/commands/src/commands/skill/update.ts +++ b/packages/commands/src/commands/skill/update.ts @@ -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 ]", + usageArgs: "[--all] [--name ]", flags: { + all: { + type: "switch", + description: "Update all installed skills (default when neither --all nor --name is given)", + }, name: { type: "string", - valueHint: "", - description: - "Skills to update: all (default, only changed ones) or comma-separated names (force update installed skills)", + valueHint: "", + 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()); diff --git a/packages/commands/src/index.ts b/packages/commands/src/index.ts index cba8b321..a5af82d1 100644 --- a/packages/commands/src/index.ts +++ b/packages/commands/src/index.ts @@ -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"; diff --git a/packages/commands/tests/e2e/skill.e2e.test.ts b/packages/commands/tests/e2e/skill.e2e.test.ts index d0af753d..5c77f9b1 100644 --- a/packages/commands/tests/e2e/skill.e2e.test.ts +++ b/packages/commands/tests/e2e/skill.e2e.test.ts @@ -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,41 +39,47 @@ 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 remove without --name errors as usage error (2)", async () => { + test("skill add with both --all and --name errors as usage error (2)", async () => { const { stdout, stderr, exitCode } = await runCommandE2e(SKILL_ROUTES, [ "skill", - "remove", + "add", + "--all", + "--name", + "spark-video", "--quiet", ]); expect(exitCode).toBe(2); - expect(`${stdout}\n${stderr}`).toMatch(/--name|Usage:/i); + expect(`${stdout}\n${stderr}`).toMatch(/--all|--name|either/i); }); - test("skill add rejects mixing all with specific names (2)", async () => { - // parseSkillNames throws UsageError before fetchSkillsIndex — offline-safe + test("skill remove without --name errors as usage error (2)", async () => { const { stdout, stderr, exitCode } = await runCommandE2e(SKILL_ROUTES, [ "skill", - "add", - "--name", - "all,spark-video", + "remove", "--quiet", ]); expect(exitCode).toBe(2); - expect(`${stdout}\n${stderr}`).toMatch(/all/i); + expect(`${stdout}\n${stderr}`).toMatch(/--name|Usage:/i); }); test("skill remove of a not-installed skill fails with reason (1)", async () => { diff --git a/packages/commands/tests/e2e/topic-routes.ts b/packages/commands/tests/e2e/topic-routes.ts index 35497843..84761f4a 100644 --- a/packages/commands/tests/e2e/topic-routes.ts +++ b/packages/commands/tests/e2e/topic-routes.ts @@ -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 = { diff --git a/skills/bailian-cli/reference/index.md b/skills/bailian-cli/reference/index.md index 31bc4c73..c4dd1cd4 100644 --- a/skills/bailian-cli/reference/index.md +++ b/skills/bailian-cli/reference/index.md @@ -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) | diff --git a/skills/bailian-cli/reference/skill.md b/skills/bailian-cli/reference/skill.md index 7020f0fb..86709673 100644 --- a/skills/bailian-cli/reference/skill.md +++ b/skills/bailian-cli/reference/skill.md @@ -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 ` | +| **Usage** | `bl skill add --all \| --name ` | #### Flags -| Flag | Type | Required | Description | -| ------------------------ | ------ | -------- | ----------------------------------------------------- | -| `--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 ` | 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 ]` | +| **Usage** | `bl skill update [--all] [--name ]` | #### Flags -| Flag | Type | Required | Description | -| ------------------------ | ------ | -------- | ----------------------------------------------------------------------------------------------------------- | -| `--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 ` | 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 ```