@cremini/skillpack 1.0.4 → 1.0.5

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/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2026 Finpeak Inc
3
+ Copyright (c) 2026 CreminiAI
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
package/README.md CHANGED
@@ -31,17 +31,17 @@ Step-by-Step
31
31
 
32
32
  ```bash
33
33
  npx @cremini/skillpack init --config ./skillpack.json
34
- npx @cremini/skillpack init output --config https://example.com/skillpack.json
34
+ npx @cremini/skillpack init commic_explainer --config https://raw.githubusercontent.com/CreminiAI/skillpack/refs/heads/main/examples/commic_explainer.json
35
35
  ```
36
36
 
37
- This loads a local or remote config, writes `skillpack.json` into the target directory, installs any pending skills, and skips zip packaging unless you pass `--bundle`.
37
+ This loads a local or remote config, writes `skillpack.json` into the target directory, installs the configured skills, and skips zip packaging unless you pass `--bundle`.
38
38
 
39
39
  ### Step-by-Step Commands
40
40
 
41
41
  ```bash
42
42
  # Add skills
43
43
  npx @cremini/skillpack skills add vercel-labs/agent-skills --skill frontend-design
44
- npx @cremini/skillpack skills add ./my-local-skills
44
+ npx @cremini/skillpack skills add ./my-local-skills --skill local-helper
45
45
 
46
46
  # Manage prompts
47
47
  npx @cremini/skillpack prompts add "Collect company data using Skill A, create charts from the data using Skill B, and compile the results into a PowerPoint using Skill C"
@@ -57,7 +57,7 @@ npx @cremini/skillpack build
57
57
  | ------------------------ | ------------------------------------- |
58
58
  | `create` | Create a skill pack interactively |
59
59
  | `init` | Initialize from a config path or URL |
60
- | `skills add <source>` | Add a skill |
60
+ | `skills add <source>` | Add one or more skills with `--skill` |
61
61
  | `skills remove <name>` | Remove a skill |
62
62
  | `skills list` | List installed skills |
63
63
  | `prompts add <text>` | Add a prompt |
package/dist/cli.js CHANGED
@@ -5,6 +5,8 @@ import { Command } from "commander";
5
5
  import chalk8 from "chalk";
6
6
 
7
7
  // src/commands/create.ts
8
+ import fs4 from "fs";
9
+ import path4 from "path";
8
10
  import inquirer from "inquirer";
9
11
  import chalk3 from "chalk";
10
12
 
@@ -24,6 +26,72 @@ function createDefaultConfig(name, description) {
24
26
  skills: []
25
27
  };
26
28
  }
29
+ function validateSkillEntry(value, sourceLabel, index) {
30
+ if (!value || typeof value !== "object") {
31
+ throw new Error(
32
+ `Invalid config from ${sourceLabel}: "skills[${index}]" must be an object`
33
+ );
34
+ }
35
+ const skill = value;
36
+ if ("installSource" in skill || "specificSkills" in skill) {
37
+ throw new Error(
38
+ `Invalid config from ${sourceLabel}: legacy skill fields are no longer supported; keep only "source", "name", and "description"`
39
+ );
40
+ }
41
+ if (typeof skill.source !== "string" || !skill.source.trim()) {
42
+ throw new Error(
43
+ `Invalid config from ${sourceLabel}: "skills[${index}].source" is required`
44
+ );
45
+ }
46
+ if (typeof skill.name !== "string" || !skill.name.trim()) {
47
+ throw new Error(
48
+ `Invalid config from ${sourceLabel}: "skills[${index}].name" is required`
49
+ );
50
+ }
51
+ if (typeof skill.description !== "string") {
52
+ throw new Error(
53
+ `Invalid config from ${sourceLabel}: "skills[${index}].description" must be a string`
54
+ );
55
+ }
56
+ }
57
+ function validateConfigShape(value, sourceLabel) {
58
+ if (!value || typeof value !== "object") {
59
+ throw new Error(`Invalid config from ${sourceLabel}: expected a JSON object`);
60
+ }
61
+ const config = value;
62
+ if (typeof config.name !== "string" || !config.name.trim()) {
63
+ throw new Error(`Invalid config from ${sourceLabel}: "name" is required`);
64
+ }
65
+ if (typeof config.description !== "string") {
66
+ throw new Error(
67
+ `Invalid config from ${sourceLabel}: "description" must be a string`
68
+ );
69
+ }
70
+ if (typeof config.version !== "string") {
71
+ throw new Error(
72
+ `Invalid config from ${sourceLabel}: "version" must be a string`
73
+ );
74
+ }
75
+ if (!Array.isArray(config.prompts) || !config.prompts.every((prompt) => typeof prompt === "string")) {
76
+ throw new Error(
77
+ `Invalid config from ${sourceLabel}: "prompts" must be a string array`
78
+ );
79
+ }
80
+ if (!Array.isArray(config.skills)) {
81
+ throw new Error(`Invalid config from ${sourceLabel}: "skills" must be an array`);
82
+ }
83
+ const names = /* @__PURE__ */ new Set();
84
+ config.skills.forEach((skill, index) => {
85
+ validateSkillEntry(skill, sourceLabel, index);
86
+ const normalizedName = skill.name.trim().toLowerCase();
87
+ if (names.has(normalizedName)) {
88
+ throw new Error(
89
+ `Invalid config from ${sourceLabel}: duplicate skill name "${skill.name}" is not allowed`
90
+ );
91
+ }
92
+ names.add(normalizedName);
93
+ });
94
+ }
27
95
  function loadConfig(workDir) {
28
96
  const filePath = getPackPath(workDir);
29
97
  if (!fs.existsSync(filePath)) {
@@ -32,10 +100,13 @@ function loadConfig(workDir) {
32
100
  );
33
101
  }
34
102
  const raw = fs.readFileSync(filePath, "utf-8");
35
- return JSON.parse(raw);
103
+ const parsed = JSON.parse(raw);
104
+ validateConfigShape(parsed, filePath);
105
+ return parsed;
36
106
  }
37
107
  function saveConfig(workDir, config) {
38
108
  const filePath = getPackPath(workDir);
109
+ validateConfigShape(config, filePath);
39
110
  fs.writeFileSync(filePath, JSON.stringify(config, null, 2) + "\n", "utf-8");
40
111
  }
41
112
  function configExists(workDir) {
@@ -50,98 +121,194 @@ import chalk2 from "chalk";
50
121
  import { fileURLToPath } from "url";
51
122
 
52
123
  // src/core/skill-manager.ts
53
- import { execSync } from "child_process";
124
+ import { spawnSync } from "child_process";
54
125
  import fs2 from "fs";
55
126
  import path2 from "path";
56
127
  import chalk from "chalk";
57
128
  var SKILLS_DIR = "skills";
129
+ function normalizeName(value) {
130
+ return value.trim().toLowerCase();
131
+ }
58
132
  function getSkillsDir(workDir) {
59
133
  return path2.join(workDir, SKILLS_DIR);
60
134
  }
61
- async function installPendingSkills(workDir, config) {
62
- const pendingSkills = config.skills.filter((s) => s.installSource);
63
- if (pendingSkills.length === 0) {
135
+ function groupSkillsBySource(skills) {
136
+ const groups = /* @__PURE__ */ new Map();
137
+ for (const skill of skills) {
138
+ const source = skill.source.trim();
139
+ const name = skill.name.trim();
140
+ const names = groups.get(source) ?? [];
141
+ if (!names.some((entry) => normalizeName(entry) === normalizeName(name))) {
142
+ names.push(name);
143
+ }
144
+ groups.set(source, names);
145
+ }
146
+ return Array.from(groups, ([source, names]) => ({ source, names }));
147
+ }
148
+ function buildInstallArgs(group) {
149
+ const args = [
150
+ "-y",
151
+ "skills",
152
+ "add",
153
+ group.source,
154
+ "--agent",
155
+ "openclaw",
156
+ "--copy",
157
+ "-y"
158
+ ];
159
+ for (const name of group.names) {
160
+ args.push("--skill", name);
161
+ }
162
+ return args;
163
+ }
164
+ function installSkills(workDir, skills) {
165
+ if (skills.length === 0) {
64
166
  return;
65
167
  }
66
- console.log(chalk.blue(`
67
- Installing pending skills...`));
68
- const skillsDir = getSkillsDir(workDir);
69
- fs2.mkdirSync(skillsDir, { recursive: true });
70
- for (const skill of pendingSkills) {
71
- let addCmd = `npx -y skills add ${skill.installSource} --agent openclaw --copy -y`;
72
- if (skill.specificSkills && skill.specificSkills.length > 0) {
73
- for (const name of skill.specificSkills) {
74
- addCmd += ` --skill "${name}"`;
75
- }
168
+ for (const group of groupSkillsBySource(skills)) {
169
+ const args = buildInstallArgs(group);
170
+ const displayArgs = args.map((arg) => /\s/.test(arg) ? JSON.stringify(arg) : arg).join(" ");
171
+ console.log(chalk.dim(`> npx ${displayArgs}`));
172
+ const result = spawnSync("npx", args, {
173
+ cwd: workDir,
174
+ stdio: "inherit",
175
+ encoding: "utf-8"
176
+ });
177
+ if (result.error) {
178
+ throw result.error;
76
179
  }
77
- console.log(chalk.dim(`> ${addCmd}`));
78
- try {
79
- execSync(addCmd, { encoding: "utf-8", cwd: workDir, stdio: "inherit" });
80
- } catch (err) {
81
- console.error(chalk.red(`Failed to install skill: ${err}`));
180
+ if (result.status !== 0) {
181
+ throw new Error(
182
+ `Failed to install skills from ${group.source} (exit code ${result.status ?? "unknown"})`
183
+ );
82
184
  }
83
185
  }
84
- config.skills = scanInstalledSkills(workDir);
85
- saveConfig(workDir, config);
86
- console.log(chalk.green(` Skill installation complete.
87
- `));
88
186
  }
89
187
  function scanInstalledSkills(workDir) {
90
- const results = [];
188
+ const installed = [];
91
189
  const skillsDir = getSkillsDir(workDir);
92
190
  if (!fs2.existsSync(skillsDir)) {
93
- return results;
191
+ return installed;
94
192
  }
95
- function findSkillFiles(dir) {
193
+ function visit(dir) {
96
194
  const entries = fs2.readdirSync(dir, { withFileTypes: true });
97
195
  for (const entry of entries) {
98
196
  const fullPath = path2.join(dir, entry.name);
99
197
  if (entry.isDirectory()) {
100
- findSkillFiles(fullPath);
101
- } else if (entry.name === "SKILL.md") {
102
- const skill = parseSkillMd(fullPath, workDir);
103
- if (skill) {
104
- results.push(skill);
105
- }
198
+ visit(fullPath);
199
+ continue;
200
+ }
201
+ if (entry.name !== "SKILL.md") {
202
+ continue;
203
+ }
204
+ const skill = parseSkillMd(fullPath);
205
+ if (skill) {
206
+ installed.push(skill);
106
207
  }
107
208
  }
108
209
  }
109
- findSkillFiles(skillsDir);
110
- return results;
210
+ visit(skillsDir);
211
+ return installed;
111
212
  }
112
- function parseSkillMd(filePath, workDir) {
213
+ function parseSkillMd(filePath) {
113
214
  try {
114
215
  const content = fs2.readFileSync(filePath, "utf-8");
115
216
  const frontmatterMatch = content.match(/^---\s*\n([\s\S]*?)\n---/);
116
- if (!frontmatterMatch) return null;
217
+ if (!frontmatterMatch) {
218
+ return null;
219
+ }
117
220
  const frontmatter = frontmatterMatch[1];
118
221
  const nameMatch = frontmatter.match(/^name:\s*(.+)$/m);
119
222
  const descMatch = frontmatter.match(/^description:\s*(.+)$/m);
120
- if (!nameMatch) return null;
223
+ if (!nameMatch) {
224
+ return null;
225
+ }
121
226
  return {
122
227
  name: nameMatch[1].trim(),
123
- source: path2.relative(workDir, path2.dirname(filePath)),
124
- description: descMatch ? descMatch[1].trim() : ""
228
+ description: descMatch ? descMatch[1].trim() : "",
229
+ dir: path2.dirname(filePath)
125
230
  };
126
231
  } catch {
127
232
  return null;
128
233
  }
129
234
  }
235
+ function syncSkillDescriptions(workDir, config) {
236
+ const descriptionByName = /* @__PURE__ */ new Map();
237
+ for (const skill of scanInstalledSkills(workDir)) {
238
+ descriptionByName.set(normalizeName(skill.name), skill.description);
239
+ }
240
+ config.skills = config.skills.map((skill) => {
241
+ const description = descriptionByName.get(normalizeName(skill.name));
242
+ return description === void 0 ? skill : { ...skill, description };
243
+ });
244
+ return config;
245
+ }
246
+ function upsertSkills(config, skills) {
247
+ for (const skill of skills) {
248
+ const normalizedName = normalizeName(skill.name);
249
+ const normalizedSource = skill.source.trim();
250
+ const existing = config.skills.find(
251
+ (entry) => normalizeName(entry.name) === normalizedName
252
+ );
253
+ if (existing && existing.source.trim() !== normalizedSource) {
254
+ throw new Error(
255
+ `Skill "${skill.name}" is already declared from source "${existing.source}"`
256
+ );
257
+ }
258
+ const sameEntry = config.skills.findIndex(
259
+ (entry) => normalizeName(entry.name) === normalizedName && entry.source.trim() === normalizedSource
260
+ );
261
+ if (sameEntry >= 0) {
262
+ config.skills[sameEntry] = {
263
+ ...config.skills[sameEntry],
264
+ name: skill.name.trim(),
265
+ source: normalizedSource,
266
+ description: skill.description
267
+ };
268
+ continue;
269
+ }
270
+ config.skills.push({
271
+ name: skill.name.trim(),
272
+ source: normalizedSource,
273
+ description: skill.description
274
+ });
275
+ }
276
+ return config;
277
+ }
278
+ function installConfiguredSkills(workDir, config) {
279
+ installSkills(workDir, config.skills);
280
+ }
281
+ function refreshDescriptionsAndSave(workDir, config) {
282
+ syncSkillDescriptions(workDir, config);
283
+ saveConfig(workDir, config);
284
+ return config;
285
+ }
130
286
  function removeSkill(workDir, skillName) {
131
287
  const config = loadConfig(workDir);
132
- const idx = config.skills.findIndex(
133
- (s) => s.name.toLowerCase() === skillName.toLowerCase()
288
+ const normalizedName = normalizeName(skillName);
289
+ const nextSkills = config.skills.filter(
290
+ (skill) => normalizeName(skill.name) !== normalizedName
134
291
  );
135
- if (idx === -1) {
292
+ if (nextSkills.length === config.skills.length) {
136
293
  console.log(chalk.yellow(`Skill not found: ${skillName}`));
137
294
  return false;
138
295
  }
139
- const skillDir = path2.join(workDir, config.skills[idx].source);
140
- if (fs2.existsSync(skillDir)) {
141
- fs2.rmSync(skillDir, { recursive: true });
142
- }
143
- config.skills.splice(idx, 1);
296
+ config.skills = nextSkills;
144
297
  saveConfig(workDir, config);
298
+ const installedMatches = scanInstalledSkills(workDir).filter(
299
+ (skill) => normalizeName(skill.name) === normalizedName
300
+ );
301
+ if (installedMatches.length === 0) {
302
+ console.log(
303
+ chalk.yellow(`Removed config for ${skillName}, but no installed files were found`)
304
+ );
305
+ return true;
306
+ }
307
+ for (const skill of installedMatches) {
308
+ if (fs2.existsSync(skill.dir)) {
309
+ fs2.rmSync(skill.dir, { recursive: true, force: true });
310
+ }
311
+ }
145
312
  console.log(chalk.green(`Removed skill: ${skillName}`));
146
313
  return true;
147
314
  }
@@ -154,14 +321,15 @@ function getRuntimeDir() {
154
321
  }
155
322
  async function bundle(workDir) {
156
323
  const config = loadConfig(workDir);
157
- saveConfig(workDir, config);
158
324
  const zipName = `${config.name}.zip`;
159
325
  const zipPath = path3.join(workDir, zipName);
160
326
  const runtimeDir = getRuntimeDir();
161
327
  if (!fs3.existsSync(runtimeDir)) {
162
328
  throw new Error(`Runtime directory not found: ${runtimeDir}`);
163
329
  }
164
- await installPendingSkills(workDir, config);
330
+ installConfiguredSkills(workDir, config);
331
+ syncSkillDescriptions(workDir, config);
332
+ saveConfig(workDir, config);
165
333
  console.log(chalk2.blue(`Packaging ${config.name}...`));
166
334
  const output = fs3.createWriteStream(zipPath);
167
335
  const archive = archiver("zip", { zlib: { level: 9 } });
@@ -222,8 +390,9 @@ function addRuntimeFiles(archive, runtimeDir, prefix) {
222
390
  }
223
391
 
224
392
  // src/commands/create.ts
225
- import fs4 from "fs";
226
- import path4 from "path";
393
+ function parseSkillNames(value) {
394
+ return value.split(",").map((name) => name.trim()).filter(Boolean);
395
+ }
227
396
  async function createCommand(directory) {
228
397
  const workDir = directory ? path4.resolve(directory) : process.cwd();
229
398
  if (directory) {
@@ -249,7 +418,7 @@ async function createCommand(directory) {
249
418
  type: "input",
250
419
  name: "name",
251
420
  message: "App name:",
252
- validate: (v) => v.trim() ? true : "Name is required"
421
+ validate: (value) => value.trim() ? true : "Name is required"
253
422
  },
254
423
  {
255
424
  type: "input",
@@ -259,20 +428,15 @@ async function createCommand(directory) {
259
428
  }
260
429
  ]);
261
430
  const config = createDefaultConfig(name.trim(), description.trim());
431
+ const requestedSkills = [];
262
432
  console.log(
263
433
  chalk3.blue("\n Add Skills (enter a skill source, leave blank to skip)\n")
264
434
  );
265
435
  console.log(
266
- chalk3.dim(
267
- " Supported formats: owner/repo, GitHub URL, local path, or a full npx skills add command"
268
- )
269
- );
270
- console.log(chalk3.dim(" Example: vercel-labs/agent-skills"));
271
- console.log(
272
- chalk3.dim(
273
- " Example: npx skills add https://github.com/vercel-labs/skills --skill find-skillsclear\n"
274
- )
436
+ chalk3.dim(" Supported formats: owner/repo, GitHub URL, or local path")
275
437
  );
438
+ console.log(chalk3.dim(" Example source: vercel-labs/agent-skills"));
439
+ console.log(chalk3.dim(" Example skill names: frontend-design, skill-creator\n"));
276
440
  while (true) {
277
441
  const { source } = await inquirer.prompt([
278
442
  {
@@ -281,51 +445,29 @@ async function createCommand(directory) {
281
445
  message: "Skill source (leave blank to skip):"
282
446
  }
283
447
  ]);
284
- if (!source.trim()) break;
285
- let parsedSource = source.trim();
286
- let parsedSpecificSkill;
287
- const skillMatch = parsedSource.match(/(.*?)\s+--skill\s+([^\s]+)(.*)/);
288
- if (skillMatch) {
289
- parsedSpecificSkill = skillMatch[2];
290
- parsedSource = `${skillMatch[1]} ${skillMatch[3]}`.trim();
448
+ if (!source.trim()) {
449
+ break;
291
450
  }
292
- const npxMatch = parsedSource.match(/^npx\s+[^\s]+\s+add\s+(.+)$/);
293
- if (npxMatch) {
294
- parsedSource = npxMatch[1].trim();
295
- }
296
- if (!parsedSource) continue;
297
- let specificSkill = parsedSpecificSkill;
298
- if (specificSkill !== void 0) {
299
- console.log(chalk3.dim(` Auto-detected skill source: ${parsedSource}`));
300
- console.log(
301
- chalk3.dim(` Auto-detected specific skill: ${specificSkill}`)
302
- );
303
- } else {
304
- if (parsedSource !== source.trim()) {
305
- console.log(chalk3.dim(` Auto-detected skill source: ${parsedSource}`));
451
+ const { skillNames } = await inquirer.prompt([
452
+ {
453
+ type: "input",
454
+ name: "skillNames",
455
+ message: "Skill names (comma-separated):",
456
+ validate: (value) => parseSkillNames(value).length > 0 ? true : "Enter at least one skill name"
306
457
  }
307
- const answer = await inquirer.prompt([
308
- {
309
- type: "input",
310
- name: "specificSkill",
311
- message: "Specific skill name (leave blank to install all):"
312
- }
313
- ]);
314
- specificSkill = answer.specificSkill;
315
- }
316
- const skillNames = specificSkill && specificSkill.trim() ? [specificSkill.trim()] : void 0;
317
- config.skills.push({
318
- name: skillNames ? skillNames.join(", ") : parsedSource,
319
- source: parsedSource,
320
- description: "Pending installation",
321
- installSource: parsedSource,
322
- specificSkills: skillNames
323
- });
458
+ ]);
459
+ const nextSkills = parseSkillNames(skillNames).map((skillName) => ({
460
+ source: source.trim(),
461
+ name: skillName,
462
+ description: ""
463
+ }));
464
+ upsertSkills(config, nextSkills);
465
+ requestedSkills.push(...nextSkills);
324
466
  }
325
467
  console.log(chalk3.blue("\n Add Prompts\n"));
326
468
  console.log(
327
469
  chalk3.blue(
328
- "Use a prompt to explain how you will organize the skills you added to complete the task\n"
470
+ "Use prompts to explain how the pack should orchestrate the selected skills\n"
329
471
  )
330
472
  );
331
473
  let promptIndex = 1;
@@ -336,17 +478,15 @@ async function createCommand(directory) {
336
478
  type: "input",
337
479
  name: "prompt",
338
480
  message: isFirst ? `Prompt #${promptIndex} (required):` : `Prompt #${promptIndex} (leave blank to finish):`,
339
- validate: isFirst ? (v) => v.trim() ? true : "The first Prompt cannot be empty" : void 0
481
+ validate: isFirst ? (value) => value.trim() ? true : "The first Prompt cannot be empty" : void 0
340
482
  }
341
483
  ]);
342
- if (!isFirst && !prompt.trim()) break;
484
+ if (!isFirst && !prompt.trim()) {
485
+ break;
486
+ }
343
487
  config.prompts.push(prompt.trim());
344
488
  promptIndex++;
345
489
  }
346
- saveConfig(workDir, config);
347
- console.log(chalk3.green(`
348
- ${PACK_FILE} saved
349
- `));
350
490
  const { shouldBundle } = await inquirer.prompt([
351
491
  {
352
492
  type: "confirm",
@@ -355,6 +495,14 @@ async function createCommand(directory) {
355
495
  default: true
356
496
  }
357
497
  ]);
498
+ saveConfig(workDir, config);
499
+ console.log(chalk3.green(`
500
+ ${PACK_FILE} saved
501
+ `));
502
+ if (requestedSkills.length > 0) {
503
+ installConfiguredSkills(workDir, config);
504
+ refreshDescriptionsAndSave(workDir, config);
505
+ }
358
506
  if (shouldBundle) {
359
507
  await bundle(workDir);
360
508
  }
@@ -379,27 +527,6 @@ function isHttpUrl(value) {
379
527
  return false;
380
528
  }
381
529
  }
382
- function validateConfigShape(value, source) {
383
- if (!value || typeof value !== "object") {
384
- throw new Error(`Invalid config from ${source}: expected a JSON object`);
385
- }
386
- const config = value;
387
- if (typeof config.name !== "string" || !config.name.trim()) {
388
- throw new Error(`Invalid config from ${source}: "name" is required`);
389
- }
390
- if (typeof config.description !== "string") {
391
- throw new Error(`Invalid config from ${source}: "description" must be a string`);
392
- }
393
- if (typeof config.version !== "string") {
394
- throw new Error(`Invalid config from ${source}: "version" must be a string`);
395
- }
396
- if (!Array.isArray(config.prompts) || !config.prompts.every((p) => typeof p === "string")) {
397
- throw new Error(`Invalid config from ${source}: "prompts" must be a string array`);
398
- }
399
- if (!Array.isArray(config.skills)) {
400
- throw new Error(`Invalid config from ${source}: "skills" must be an array`);
401
- }
402
- }
403
530
  async function readConfigSource(source) {
404
531
  let raw = "";
405
532
  if (isHttpUrl(source)) {
@@ -440,7 +567,8 @@ async function initCommand(directory, options) {
440
567
  console.log(chalk4.blue(`
441
568
  Initialize ${config.name} from ${options.config}
442
569
  `));
443
- await installPendingSkills(workDir, config);
570
+ installConfiguredSkills(workDir, config);
571
+ refreshDescriptionsAndSave(workDir, config);
444
572
  if (options.bundle) {
445
573
  await bundle(workDir);
446
574
  }
@@ -460,21 +588,37 @@ import chalk5 from "chalk";
460
588
  function registerSkillsCommand(program2) {
461
589
  const skills = program2.command("skills").description("Manage skills in the app");
462
590
  skills.command("add <source>").description("Add a skill from a git repo, URL, or local path").option("-s, --skill <names...>", "Specify skill name(s)").action(async (source, opts) => {
591
+ if (!opts.skill || opts.skill.length === 0) {
592
+ console.log(
593
+ chalk5.red(
594
+ "Specify at least one skill name with --skill when adding a source"
595
+ )
596
+ );
597
+ process.exitCode = 1;
598
+ return;
599
+ }
463
600
  const workDir = process.cwd();
464
601
  const config = loadConfig(workDir);
465
- config.skills.push({
466
- name: opts.skill ? opts.skill.join(", ") : source,
602
+ const requestedSkills = opts.skill.map((name) => ({
603
+ name: name.trim(),
467
604
  source,
468
- description: "Pending installation",
469
- installSource: source,
470
- specificSkills: opts.skill && opts.skill.length > 0 ? opts.skill : void 0
471
- });
605
+ description: ""
606
+ }));
607
+ upsertSkills(config, requestedSkills);
472
608
  saveConfig(workDir, config);
473
- console.log(
474
- chalk5.green(
475
- `Skill list updated (${config.skills.length} total). Skills will be installed during build.`
476
- )
477
- );
609
+ try {
610
+ installSkills(workDir, requestedSkills);
611
+ refreshDescriptionsAndSave(workDir, config);
612
+ } catch (error) {
613
+ console.log(
614
+ chalk5.red(
615
+ `Skill installation failed: ${error instanceof Error ? error.message : String(error)}`
616
+ )
617
+ );
618
+ process.exitCode = 1;
619
+ return;
620
+ }
621
+ console.log(chalk5.green(`Installed ${requestedSkills.length} skill(s).`));
478
622
  });
479
623
  skills.command("remove <name>").description("Remove a skill").action((name) => {
480
624
  removeSkill(process.cwd(), name);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cremini/skillpack",
3
- "version": "1.0.4",
3
+ "version": "1.0.5",
4
4
  "description": "Turn Skills into a Standalone App with UI",
5
5
  "type": "module",
6
6
  "repository": {
@@ -1,31 +0,0 @@
1
- import fs from "node:fs";
2
- import path from "node:path";
3
-
4
- /**
5
- * Recursively load the contents of all SKILL.md files under skills/.
6
- * @param {string} rootDir - Root directory containing skills/
7
- * @returns {string[]} Array of SKILL.md file contents
8
- */
9
- export function loadSkillContents(rootDir) {
10
- const skillsDir = path.join(rootDir, "skills");
11
- const contents = [];
12
-
13
- if (!fs.existsSync(skillsDir)) {
14
- return contents;
15
- }
16
-
17
- function walk(dir) {
18
- const entries = fs.readdirSync(dir, { withFileTypes: true });
19
- for (const entry of entries) {
20
- const full = path.join(dir, entry.name);
21
- if (entry.isDirectory()) {
22
- walk(full);
23
- } else if (entry.name === "SKILL.md") {
24
- contents.push(fs.readFileSync(full, "utf-8"));
25
- }
26
- }
27
- }
28
-
29
- walk(skillsDir);
30
- return contents;
31
- }