Skip to content

Commit b9ecd5c

Browse files
authored
Merge pull request #143 from modelstudioai/feat/skill-init-commend
feat: add skill init & opt commend flags
2 parents 978f332 + 4502424 commit b9ecd5c

11 files changed

Lines changed: 211 additions & 48 deletions

File tree

packages/cli/src/commands.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ import {
9393
skillUpdate,
9494
skillRemove,
9595
skillList,
96+
skillInit,
9697
managedAgentInit,
9798
managedAgentValidate,
9899
managedAgentPlan,
@@ -211,6 +212,7 @@ export const commands: Record<string, AnyCommand> = {
211212
"skill update": skillUpdate,
212213
"skill remove": skillRemove,
213214
"skill list": skillList,
215+
"skill init": skillInit,
214216
"managed-agent init": managedAgentInit,
215217
"managed-agent validate": managedAgentValidate,
216218
"managed-agent plan": managedAgentPlan,

packages/commands/src/commands/skill/add.ts

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import {
22
BailianError,
33
ExitCode,
44
defineCommand,
5-
detectOutputFormat,
65
detectInstalledAgents,
76
fetchSkillsIndex,
87
getSkillRegistryBaseUrl,
@@ -28,22 +27,31 @@ const INSTALL_CONCURRENCY = 3;
2827
export default defineCommand({
2928
description: "Install skills from the Bailian skill registry into local agents",
3029
auth: "none",
31-
usageArgs: "--name <all|name,...>",
30+
usageArgs: "--all | --name <name,...>",
3231
flags: {
32+
all: {
33+
type: "switch",
34+
description: "Install all skills from the registry",
35+
},
3336
name: {
3437
type: "string",
35-
valueHint: "<all|name,...>",
36-
description: "Skills to install: all or comma-separated skill names",
37-
required: true,
38+
valueHint: "<name,...>",
39+
description: "Comma-separated skill names to install",
3840
},
3941
},
40-
exampleArgs: ["--name all", "--name spark-video,bailian-model-recommend"],
42+
validate(flags) {
43+
if (flags.all && flags.name) return "Use either --all or --name, not both";
44+
if (!flags.all && !flags.name)
45+
return "Specify --all to install everything or --name <name,...> for specific skills";
46+
return undefined;
47+
},
48+
exampleArgs: ["--all", "--name spark-video,bailian-model-recommend"],
4149
async run(ctx) {
42-
const format = detectOutputFormat(ctx.settings.output);
43-
const requested = parseSkillNames(ctx.flags.name, false);
50+
const format = ctx.settings.outputExplicit ? ctx.settings.output : "json";
4451
const index = await fetchSkillsIndex();
4552
const remoteNames = Object.keys(index.skills);
46-
const names = requested === "all" ? remoteNames : requested;
53+
const parsed = ctx.flags.all ? "all" : parseSkillNames(ctx.flags.name, false);
54+
const names = parsed === "all" ? remoteNames : parsed;
4755

4856
const lock = readSkillLock();
4957
const agents = detectInstalledAgents();
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import {
2+
BailianError,
3+
ExitCode,
4+
defineCommand,
5+
detectInstalledAgents,
6+
fetchSkillsIndex,
7+
getSkillRegistryBaseUrl,
8+
installSkillWithFanout,
9+
readSkillLock,
10+
runWithConcurrency,
11+
writeSkillLock,
12+
} from "bailian-cli-core";
13+
import { emitBare, emitResult, formatTable } from "bailian-cli-runtime";
14+
15+
interface InitOutcome {
16+
name: string;
17+
status: "installed" | "failed";
18+
publishedAt?: string;
19+
agents?: string[];
20+
reason?: string;
21+
}
22+
23+
/** Prefix used to identify first-party Bailian skills in the registry. */
24+
const BAILIAN_PREFIX = "bailian-";
25+
26+
/** Max number of skills downloading/installing at the same time. */
27+
const INIT_CONCURRENCY = 3;
28+
29+
export default defineCommand({
30+
description: "Install all bailian-* skills (one-shot bootstrap for new environments)",
31+
auth: "none",
32+
usageArgs: "",
33+
exampleArgs: [""],
34+
notes: [
35+
"Fetches the registry index and installs every skill whose name starts with bailian-",
36+
"Equivalent to: bl skill add --all (filtered to bailian-* skills)",
37+
],
38+
async run(ctx) {
39+
const format = ctx.settings.outputExplicit ? ctx.settings.output : "json";
40+
const index = await fetchSkillsIndex();
41+
42+
// Discover all bailian-* skills from the live registry index
43+
const names = Object.keys(index.skills).filter((name) => name.startsWith(BAILIAN_PREFIX));
44+
45+
const lock = readSkillLock();
46+
const agents = detectInstalledAgents();
47+
48+
const tasks = names.map((name) => async (): Promise<InitOutcome> => {
49+
const entry = index.skills[name];
50+
try {
51+
const record = await installSkillWithFanout(
52+
name,
53+
entry,
54+
agents,
55+
lock.skills[name]?.links ?? [],
56+
);
57+
lock.skills[name] = record.lockEntry;
58+
return {
59+
name,
60+
status: "installed",
61+
publishedAt: entry.publishedAt,
62+
agents: record.linkedAgents,
63+
};
64+
} catch (err) {
65+
return {
66+
name,
67+
status: "failed",
68+
reason: err instanceof Error ? err.message : String(err),
69+
};
70+
}
71+
});
72+
const results = await runWithConcurrency(tasks, INIT_CONCURRENCY);
73+
writeSkillLock(lock);
74+
75+
if (format === "json") {
76+
emitResult(
77+
{
78+
registry: getSkillRegistryBaseUrl(),
79+
agents: agents.map((agent) => agent.id),
80+
skills: results,
81+
},
82+
format,
83+
);
84+
} else if (results.length === 0) {
85+
emitBare("No bailian-* skills found in the registry.");
86+
} else {
87+
const rows = results.map((result) => [
88+
result.name,
89+
result.status,
90+
result.publishedAt ? result.publishedAt.slice(0, 10) : "-",
91+
result.status === "installed" ? result.agents?.join(", ") || "-" : (result.reason ?? "-"),
92+
]);
93+
for (const line of formatTable(["NAME", "STATUS", "PUBLISHED", "AGENTS / REASON"], rows)) {
94+
emitBare(line);
95+
}
96+
}
97+
98+
const failed = results.filter((result) => result.status === "failed");
99+
if (failed.length > 0) {
100+
throw new BailianError(
101+
`${failed.length}/${results.length} skill(s) failed to install`,
102+
ExitCode.GENERAL,
103+
"Check the reason for failed skills in the output; network failures can be retried with bl skill init",
104+
);
105+
}
106+
},
107+
});

packages/commands/src/commands/skill/list.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import {
22
defineCommand,
3-
detectOutputFormat,
43
computeSkillStatuses,
54
fetchSkillsIndex,
65
getSkillRegistryBaseUrl,
@@ -24,7 +23,7 @@ export default defineCommand({
2423
"STATUS: installed | outdated | not-installed | missing (lock has it, dir deleted) | untracked (dir exists, not managed)",
2524
],
2625
async run(ctx) {
27-
const format = detectOutputFormat(ctx.settings.output);
26+
const format = ctx.settings.outputExplicit ? ctx.settings.output : "json";
2827
// Three-way reconciliation: live remote index × skill-lock.json (installation facts) × disk
2928
const index = await fetchSkillsIndex();
3029
const lock = readSkillLock();

packages/commands/src/commands/skill/remove.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import {
22
BailianError,
33
ExitCode,
44
defineCommand,
5-
detectOutputFormat,
65
listSkillDirsOnDisk,
76
parseSkillNames,
87
readSkillLock,
@@ -34,7 +33,7 @@ export default defineCommand({
3433
exampleArgs: ["--name spark-video", "--name all"],
3534
async run(ctx) {
3635
// Purely local operation: no remote access, works offline
37-
const format = detectOutputFormat(ctx.settings.output);
36+
const format = ctx.settings.outputExplicit ? ctx.settings.output : "json";
3837
const requested = parseSkillNames(ctx.flags.name, false);
3938
const lock = readSkillLock();
4039
const names = requested === "all" ? Object.keys(lock.skills) : requested;

packages/commands/src/commands/skill/update.ts

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import {
22
BailianError,
33
ExitCode,
44
defineCommand,
5-
detectOutputFormat,
65
detectInstalledAgents,
76
fanOutSkillToAgents,
87
fetchSkillsIndex,
@@ -29,19 +28,27 @@ const UPDATE_CONCURRENCY = 3;
2928
export default defineCommand({
3029
description: "Update installed skills to the latest registry versions",
3130
auth: "none",
32-
usageArgs: "[--name <all|name,...>]",
31+
usageArgs: "[--all] [--name <name,...>]",
3332
flags: {
33+
all: {
34+
type: "switch",
35+
description: "Update all installed skills (default when neither --all nor --name is given)",
36+
},
3437
name: {
3538
type: "string",
36-
valueHint: "<all|name,...>",
37-
description:
38-
"Skills to update: all (default, only changed ones) or comma-separated names (force update installed skills)",
39+
valueHint: "<name,...>",
40+
description: "Comma-separated skill names to update (must be already installed)",
3941
},
4042
},
41-
exampleArgs: ["", "--name spark-video"],
43+
validate(flags) {
44+
if (flags.all && flags.name) return "Use either --all or --name, not both";
45+
return undefined;
46+
},
47+
exampleArgs: ["", "--all", "--name spark-video"],
4248
async run(ctx) {
43-
const format = detectOutputFormat(ctx.settings.output);
44-
const requested = parseSkillNames(ctx.flags.name, true);
49+
const format = ctx.settings.outputExplicit ? ctx.settings.output : "json";
50+
const updateAll = ctx.flags.all || !ctx.flags.name;
51+
const requested = updateAll ? "all" : parseSkillNames(ctx.flags.name, false);
4552
const index = await fetchSkillsIndex();
4653
const lock = readSkillLock();
4754
const disk = new Set(listSkillDirsOnDisk());

packages/commands/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,3 +117,4 @@ export { default as skillAdd } from "./commands/skill/add.ts";
117117
export { default as skillUpdate } from "./commands/skill/update.ts";
118118
export { default as skillRemove } from "./commands/skill/remove.ts";
119119
export { default as skillList } from "./commands/skill/list.ts";
120+
export { default as skillInit } from "./commands/skill/init.ts";

packages/commands/tests/e2e/skill.e2e.test.ts

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,14 @@ describe("e2e: skill", () => {
1717
test("skill add --help exits successfully", async () => {
1818
const { stderr, exitCode } = await runCommandE2e(SKILL_ROUTES, ["skill", "add", "--help"]);
1919
expect(exitCode, stderr).toBe(0);
20+
expect(stderr).toMatch(/--all/);
2021
expect(stderr).toMatch(/--name/);
2122
});
2223

2324
test("skill update --help exits successfully", async () => {
2425
const { stderr, exitCode } = await runCommandE2e(SKILL_ROUTES, ["skill", "update", "--help"]);
2526
expect(exitCode, stderr).toBe(0);
27+
expect(stderr).toMatch(/--all/);
2628
expect(stderr).toMatch(/--name/);
2729
});
2830

@@ -37,41 +39,47 @@ describe("e2e: skill", () => {
3739
expect(exitCode, stderr).toBe(0);
3840
expect(stderr).toMatch(/list|registry/i);
3941
});
42+
43+
test("skill init --help exits successfully", async () => {
44+
const { stderr, exitCode } = await runCommandE2e(SKILL_ROUTES, ["skill", "init", "--help"]);
45+
expect(exitCode, stderr).toBe(0);
46+
expect(stderr).toMatch(/bailian/i);
47+
});
4048
});
4149

4250
// Local-only cases: auth "none" + validation happens before any network access, no gating needed
4351
describe("e2e: skill (local, no credentials)", () => {
44-
test("skill add without --name errors as usage error (2)", async () => {
52+
test("skill add without --all or --name errors as usage error (2)", async () => {
4553
const { stdout, stderr, exitCode } = await runCommandE2e(SKILL_ROUTES, [
4654
"skill",
4755
"add",
4856
"--quiet",
4957
]);
5058
expect(exitCode).toBe(2);
51-
expect(`${stdout}\n${stderr}`).toMatch(/--name|Usage:/i);
59+
expect(`${stdout}\n${stderr}`).toMatch(/--all|--name|Usage:/i);
5260
});
5361

54-
test("skill remove without --name errors as usage error (2)", async () => {
62+
test("skill add with both --all and --name errors as usage error (2)", async () => {
5563
const { stdout, stderr, exitCode } = await runCommandE2e(SKILL_ROUTES, [
5664
"skill",
57-
"remove",
65+
"add",
66+
"--all",
67+
"--name",
68+
"spark-video",
5869
"--quiet",
5970
]);
6071
expect(exitCode).toBe(2);
61-
expect(`${stdout}\n${stderr}`).toMatch(/--name|Usage:/i);
72+
expect(`${stdout}\n${stderr}`).toMatch(/--all|--name|either/i);
6273
});
6374

64-
test("skill add rejects mixing all with specific names (2)", async () => {
65-
// parseSkillNames throws UsageError before fetchSkillsIndex — offline-safe
75+
test("skill remove without --name errors as usage error (2)", async () => {
6676
const { stdout, stderr, exitCode } = await runCommandE2e(SKILL_ROUTES, [
6777
"skill",
68-
"add",
69-
"--name",
70-
"all,spark-video",
78+
"remove",
7179
"--quiet",
7280
]);
7381
expect(exitCode).toBe(2);
74-
expect(`${stdout}\n${stderr}`).toMatch(/all/i);
82+
expect(`${stdout}\n${stderr}`).toMatch(/--name|Usage:/i);
7583
});
7684

7785
test("skill remove of a not-installed skill fails with reason (1)", async () => {

packages/commands/tests/e2e/topic-routes.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,7 @@ export const SKILL_ROUTES: E2eRouteExports = {
165165
"skill update": "skillUpdate",
166166
"skill remove": "skillRemove",
167167
"skill list": "skillList",
168+
"skill init": "skillInit",
168169
};
169170

170171
export const MANAGED_AGENT_ROUTES: E2eRouteExports = {

skills/bailian-cli/reference/index.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ Use this index for the skill-scoped quick index and global flags.
5252
| `bl quota request` | Request a temporary quota increase | [quota.md](quota.md) |
5353
| `bl search web` | Search the web using DashScope MCP WebSearch service | [search.md](search.md) |
5454
| `bl skill add` | Install skills from the Bailian skill registry into local agents | [skill.md](skill.md) |
55+
| `bl skill init` | Install all bailian-\* skills (one-shot bootstrap for new environments) | [skill.md](skill.md) |
5556
| `bl skill list` | List registry skills and diff against local installs | [skill.md](skill.md) |
5657
| `bl skill remove` | Remove locally installed skills (registry is untouched) | [skill.md](skill.md) |
5758
| `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.
8687
| `plugin` | `install`, `link`, `list`, `remove` | [plugin.md](plugin.md) |
8788
| `quota` | `check`, `history`, `list`, `request` | [quota.md](quota.md) |
8889
| `search` | `web` | [search.md](search.md) |
89-
| `skill` | `add`, `list`, `remove`, `update` | [skill.md](skill.md) |
90+
| `skill` | `add`, `init`, `list`, `remove`, `update` | [skill.md](skill.md) |
9091
| `text` | `chat` | [text.md](text.md) |
9192
| `token-plan` | `add-member`, `assign-seats`, `create-key`, `list-seats` | [token-plan.md](token-plan.md) |
9293
| `update` | `(root)` | [update.md](update.md) |

0 commit comments

Comments
 (0)