@cremini/skillpack 1.0.4 → 1.0.6

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
@@ -27,21 +27,21 @@ Step-by-Step
27
27
  3. Add prompts to orchestrate and organize skills you added to accomplish tasks
28
28
  4. (Optional) bundle the result as a zip
29
29
 
30
- ### Initialize from an Existing Config
30
+ ### Initialize with Configuration
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
+ Bootstrap a SkillPack using a local file or remote URL.
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 |
@@ -73,11 +73,11 @@ The extracted archive looks like this:
73
73
  skillpack/
74
74
  ├── skillpack.json # Pack configuration
75
75
  ├── skills/ # Collected SKILL.md files
76
- ├── server/ # Express backend
77
- ├── web/ # Web chat UI
76
+ ├── server/ # Runtime backend
77
+ ├── web/ # Runtime web UI
78
78
  ├── start.sh # One-click launcher for macOS/Linux
79
79
  ├── start.bat # One-click launcher for Windows
80
- └── README.md
80
+ └── README.md # Runtime guide
81
81
  ```
82
82
 
83
83
  ### Run the Skill Pack
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 fs5 from "fs";
9
+ import path5 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) {
@@ -43,127 +114,296 @@ function configExists(workDir) {
43
114
  }
44
115
 
45
116
  // src/core/bundler.ts
46
- import fs3 from "fs";
47
- import path3 from "path";
117
+ import fs4 from "fs";
118
+ import path4 from "path";
48
119
  import archiver from "archiver";
49
120
  import chalk2 from "chalk";
50
- import { fileURLToPath } from "url";
51
121
 
52
122
  // src/core/skill-manager.ts
53
- import { execSync } from "child_process";
123
+ import { spawnSync } from "child_process";
54
124
  import fs2 from "fs";
55
125
  import path2 from "path";
56
126
  import chalk from "chalk";
57
127
  var SKILLS_DIR = "skills";
128
+ function normalizeName(value) {
129
+ return value.trim().toLowerCase();
130
+ }
58
131
  function getSkillsDir(workDir) {
59
132
  return path2.join(workDir, SKILLS_DIR);
60
133
  }
61
- async function installPendingSkills(workDir, config) {
62
- const pendingSkills = config.skills.filter((s) => s.installSource);
63
- if (pendingSkills.length === 0) {
134
+ function groupSkillsBySource(skills) {
135
+ const groups = /* @__PURE__ */ new Map();
136
+ for (const skill of skills) {
137
+ const source = skill.source.trim();
138
+ const name = skill.name.trim();
139
+ const names = groups.get(source) ?? [];
140
+ if (!names.some((entry) => normalizeName(entry) === normalizeName(name))) {
141
+ names.push(name);
142
+ }
143
+ groups.set(source, names);
144
+ }
145
+ return Array.from(groups, ([source, names]) => ({ source, names }));
146
+ }
147
+ function buildInstallArgs(group) {
148
+ const args = [
149
+ "-y",
150
+ "skills",
151
+ "add",
152
+ group.source,
153
+ "--agent",
154
+ "openclaw",
155
+ "--copy",
156
+ "-y"
157
+ ];
158
+ for (const name of group.names) {
159
+ args.push("--skill", name);
160
+ }
161
+ return args;
162
+ }
163
+ function installSkills(workDir, skills) {
164
+ if (skills.length === 0) {
64
165
  return;
65
166
  }
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
- }
167
+ for (const group of groupSkillsBySource(skills)) {
168
+ const args = buildInstallArgs(group);
169
+ const displayArgs = args.map((arg) => /\s/.test(arg) ? JSON.stringify(arg) : arg).join(" ");
170
+ console.log(chalk.dim(`> npx ${displayArgs}`));
171
+ const result = spawnSync("npx", args, {
172
+ cwd: workDir,
173
+ stdio: "inherit",
174
+ encoding: "utf-8"
175
+ });
176
+ if (result.error) {
177
+ throw result.error;
76
178
  }
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}`));
179
+ if (result.status !== 0) {
180
+ throw new Error(
181
+ `Failed to install skills from ${group.source} (exit code ${result.status ?? "unknown"})`
182
+ );
82
183
  }
83
184
  }
84
- config.skills = scanInstalledSkills(workDir);
85
- saveConfig(workDir, config);
86
- console.log(chalk.green(` Skill installation complete.
87
- `));
88
185
  }
89
186
  function scanInstalledSkills(workDir) {
90
- const results = [];
187
+ const installed = [];
91
188
  const skillsDir = getSkillsDir(workDir);
92
189
  if (!fs2.existsSync(skillsDir)) {
93
- return results;
190
+ return installed;
94
191
  }
95
- function findSkillFiles(dir) {
192
+ function visit(dir) {
96
193
  const entries = fs2.readdirSync(dir, { withFileTypes: true });
97
194
  for (const entry of entries) {
98
195
  const fullPath = path2.join(dir, entry.name);
99
196
  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
- }
197
+ visit(fullPath);
198
+ continue;
199
+ }
200
+ if (entry.name !== "SKILL.md") {
201
+ continue;
202
+ }
203
+ const skill = parseSkillMd(fullPath);
204
+ if (skill) {
205
+ installed.push(skill);
106
206
  }
107
207
  }
108
208
  }
109
- findSkillFiles(skillsDir);
110
- return results;
209
+ visit(skillsDir);
210
+ return installed;
111
211
  }
112
- function parseSkillMd(filePath, workDir) {
212
+ function parseSkillMd(filePath) {
113
213
  try {
114
214
  const content = fs2.readFileSync(filePath, "utf-8");
115
215
  const frontmatterMatch = content.match(/^---\s*\n([\s\S]*?)\n---/);
116
- if (!frontmatterMatch) return null;
216
+ if (!frontmatterMatch) {
217
+ return null;
218
+ }
117
219
  const frontmatter = frontmatterMatch[1];
118
220
  const nameMatch = frontmatter.match(/^name:\s*(.+)$/m);
119
221
  const descMatch = frontmatter.match(/^description:\s*(.+)$/m);
120
- if (!nameMatch) return null;
222
+ if (!nameMatch) {
223
+ return null;
224
+ }
121
225
  return {
122
226
  name: nameMatch[1].trim(),
123
- source: path2.relative(workDir, path2.dirname(filePath)),
124
- description: descMatch ? descMatch[1].trim() : ""
227
+ description: descMatch ? descMatch[1].trim() : "",
228
+ dir: path2.dirname(filePath)
125
229
  };
126
230
  } catch {
127
231
  return null;
128
232
  }
129
233
  }
234
+ function syncSkillDescriptions(workDir, config) {
235
+ const descriptionByName = /* @__PURE__ */ new Map();
236
+ for (const skill of scanInstalledSkills(workDir)) {
237
+ descriptionByName.set(normalizeName(skill.name), skill.description);
238
+ }
239
+ config.skills = config.skills.map((skill) => {
240
+ const description = descriptionByName.get(normalizeName(skill.name));
241
+ return description === void 0 ? skill : { ...skill, description };
242
+ });
243
+ return config;
244
+ }
245
+ function upsertSkills(config, skills) {
246
+ for (const skill of skills) {
247
+ const normalizedName = normalizeName(skill.name);
248
+ const normalizedSource = skill.source.trim();
249
+ const existing = config.skills.find(
250
+ (entry) => normalizeName(entry.name) === normalizedName
251
+ );
252
+ if (existing && existing.source.trim() !== normalizedSource) {
253
+ throw new Error(
254
+ `Skill "${skill.name}" is already declared from source "${existing.source}"`
255
+ );
256
+ }
257
+ const sameEntry = config.skills.findIndex(
258
+ (entry) => normalizeName(entry.name) === normalizedName && entry.source.trim() === normalizedSource
259
+ );
260
+ if (sameEntry >= 0) {
261
+ config.skills[sameEntry] = {
262
+ ...config.skills[sameEntry],
263
+ name: skill.name.trim(),
264
+ source: normalizedSource,
265
+ description: skill.description
266
+ };
267
+ continue;
268
+ }
269
+ config.skills.push({
270
+ name: skill.name.trim(),
271
+ source: normalizedSource,
272
+ description: skill.description
273
+ });
274
+ }
275
+ return config;
276
+ }
277
+ function installConfiguredSkills(workDir, config) {
278
+ installSkills(workDir, config.skills);
279
+ }
280
+ function refreshDescriptionsAndSave(workDir, config) {
281
+ syncSkillDescriptions(workDir, config);
282
+ saveConfig(workDir, config);
283
+ return config;
284
+ }
130
285
  function removeSkill(workDir, skillName) {
131
286
  const config = loadConfig(workDir);
132
- const idx = config.skills.findIndex(
133
- (s) => s.name.toLowerCase() === skillName.toLowerCase()
287
+ const normalizedName = normalizeName(skillName);
288
+ const nextSkills = config.skills.filter(
289
+ (skill) => normalizeName(skill.name) !== normalizedName
134
290
  );
135
- if (idx === -1) {
291
+ if (nextSkills.length === config.skills.length) {
136
292
  console.log(chalk.yellow(`Skill not found: ${skillName}`));
137
293
  return false;
138
294
  }
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);
295
+ config.skills = nextSkills;
144
296
  saveConfig(workDir, config);
297
+ const installedMatches = scanInstalledSkills(workDir).filter(
298
+ (skill) => normalizeName(skill.name) === normalizedName
299
+ );
300
+ if (installedMatches.length === 0) {
301
+ console.log(
302
+ chalk.yellow(`Removed config for ${skillName}, but no installed files were found`)
303
+ );
304
+ return true;
305
+ }
306
+ for (const skill of installedMatches) {
307
+ if (fs2.existsSync(skill.dir)) {
308
+ fs2.rmSync(skill.dir, { recursive: true, force: true });
309
+ }
310
+ }
145
311
  console.log(chalk.green(`Removed skill: ${skillName}`));
146
312
  return true;
147
313
  }
148
314
 
149
- // src/core/bundler.ts
315
+ // src/core/runtime-template.ts
316
+ import fs3 from "fs";
317
+ import path3 from "path";
318
+ import { fileURLToPath } from "url";
150
319
  var __dirname = path3.dirname(fileURLToPath(import.meta.url));
151
320
  function getRuntimeDir() {
152
321
  const projectRoot = path3.resolve(__dirname, "..");
153
322
  return path3.join(projectRoot, "runtime");
154
323
  }
324
+ function assertRuntimeDirExists(runtimeDir) {
325
+ if (!fs3.existsSync(runtimeDir)) {
326
+ throw new Error(`Runtime directory not found: ${runtimeDir}`);
327
+ }
328
+ }
329
+ function collectRuntimeTemplateEntries(runtimeDir) {
330
+ assertRuntimeDirExists(runtimeDir);
331
+ const entries = [];
332
+ function visit(currentDir, relativeDir = "") {
333
+ const dirEntries = fs3.readdirSync(currentDir, { withFileTypes: true });
334
+ for (const dirEntry of dirEntries) {
335
+ if (dirEntry.name === "node_modules") {
336
+ continue;
337
+ }
338
+ const absolutePath = path3.join(currentDir, dirEntry.name);
339
+ const relativePath = relativeDir ? path3.posix.join(relativeDir, dirEntry.name) : dirEntry.name;
340
+ const stats = fs3.statSync(absolutePath);
341
+ if (dirEntry.isDirectory()) {
342
+ entries.push({
343
+ absolutePath,
344
+ relativePath,
345
+ stats,
346
+ type: "directory"
347
+ });
348
+ visit(absolutePath, relativePath);
349
+ continue;
350
+ }
351
+ if (dirEntry.isFile()) {
352
+ entries.push({
353
+ absolutePath,
354
+ relativePath,
355
+ stats,
356
+ type: "file"
357
+ });
358
+ }
359
+ }
360
+ }
361
+ visit(runtimeDir);
362
+ return entries;
363
+ }
364
+ function copyRuntimeTemplate(runtimeDir, workDir) {
365
+ const entries = collectRuntimeTemplateEntries(runtimeDir);
366
+ for (const entry of entries) {
367
+ const destinationPath = path3.join(workDir, entry.relativePath);
368
+ if (entry.type === "directory") {
369
+ fs3.mkdirSync(destinationPath, { recursive: true });
370
+ continue;
371
+ }
372
+ fs3.mkdirSync(path3.dirname(destinationPath), { recursive: true });
373
+ fs3.copyFileSync(entry.absolutePath, destinationPath);
374
+ fs3.chmodSync(destinationPath, entry.stats.mode);
375
+ }
376
+ }
377
+ function addRuntimeFiles(archive, runtimeDir, prefix) {
378
+ const entries = collectRuntimeTemplateEntries(runtimeDir);
379
+ for (const entry of entries) {
380
+ const archivePath = `${prefix}/${entry.relativePath}`;
381
+ if (entry.type === "directory") {
382
+ archive.append("", {
383
+ name: `${archivePath}/`,
384
+ mode: entry.stats.mode
385
+ });
386
+ continue;
387
+ }
388
+ archive.file(entry.absolutePath, {
389
+ name: archivePath,
390
+ mode: entry.stats.mode
391
+ });
392
+ }
393
+ }
394
+
395
+ // src/core/bundler.ts
155
396
  async function bundle(workDir) {
156
397
  const config = loadConfig(workDir);
157
- saveConfig(workDir, config);
158
398
  const zipName = `${config.name}.zip`;
159
- const zipPath = path3.join(workDir, zipName);
399
+ const zipPath = path4.join(workDir, zipName);
160
400
  const runtimeDir = getRuntimeDir();
161
- if (!fs3.existsSync(runtimeDir)) {
162
- throw new Error(`Runtime directory not found: ${runtimeDir}`);
163
- }
164
- await installPendingSkills(workDir, config);
401
+ assertRuntimeDirExists(runtimeDir);
402
+ installConfiguredSkills(workDir, config);
403
+ syncSkillDescriptions(workDir, config);
404
+ saveConfig(workDir, config);
165
405
  console.log(chalk2.blue(`Packaging ${config.name}...`));
166
- const output = fs3.createWriteStream(zipPath);
406
+ const output = fs4.createWriteStream(zipPath);
167
407
  const archive = archiver("zip", { zlib: { level: 9 } });
168
408
  return new Promise((resolve, reject) => {
169
409
  output.on("close", () => {
@@ -180,54 +420,23 @@ async function bundle(workDir) {
180
420
  archive.file(getPackPath(workDir), {
181
421
  name: `${prefix}/${PACK_FILE}`
182
422
  });
183
- const skillsDir = path3.join(workDir, "skills");
184
- if (fs3.existsSync(skillsDir)) {
423
+ const skillsDir = path4.join(workDir, "skills");
424
+ if (fs4.existsSync(skillsDir)) {
185
425
  archive.directory(skillsDir, `${prefix}/skills`);
186
426
  }
187
427
  addRuntimeFiles(archive, runtimeDir, prefix);
188
428
  archive.finalize();
189
429
  });
190
430
  }
191
- function addRuntimeFiles(archive, runtimeDir, prefix) {
192
- const serverDir = path3.join(runtimeDir, "server");
193
- if (fs3.existsSync(serverDir)) {
194
- archive.glob(
195
- "**/*",
196
- {
197
- cwd: serverDir,
198
- ignore: ["node_modules/**"]
199
- },
200
- { prefix: `${prefix}/server` }
201
- );
202
- }
203
- const webDir = path3.join(runtimeDir, "web");
204
- if (fs3.existsSync(webDir)) {
205
- archive.directory(webDir, `${prefix}/web`);
206
- }
207
- const scriptsDir = path3.join(runtimeDir, "scripts");
208
- if (fs3.existsSync(scriptsDir)) {
209
- const startSh = path3.join(scriptsDir, "start.sh");
210
- if (fs3.existsSync(startSh)) {
211
- archive.file(startSh, { name: `${prefix}/start.sh`, mode: 493 });
212
- }
213
- const startBat = path3.join(scriptsDir, "start.bat");
214
- if (fs3.existsSync(startBat)) {
215
- archive.file(startBat, { name: `${prefix}/start.bat` });
216
- }
217
- }
218
- const readme = path3.join(runtimeDir, "README.md");
219
- if (fs3.existsSync(readme)) {
220
- archive.file(readme, { name: `${prefix}/README.md` });
221
- }
222
- }
223
431
 
224
432
  // src/commands/create.ts
225
- import fs4 from "fs";
226
- import path4 from "path";
433
+ function parseSkillNames(value) {
434
+ return value.split(",").map((name) => name.trim()).filter(Boolean);
435
+ }
227
436
  async function createCommand(directory) {
228
- const workDir = directory ? path4.resolve(directory) : process.cwd();
437
+ const workDir = directory ? path5.resolve(directory) : process.cwd();
229
438
  if (directory) {
230
- fs4.mkdirSync(workDir, { recursive: true });
439
+ fs5.mkdirSync(workDir, { recursive: true });
231
440
  }
232
441
  if (configExists(workDir)) {
233
442
  const { overwrite } = await inquirer.prompt([
@@ -249,7 +458,7 @@ async function createCommand(directory) {
249
458
  type: "input",
250
459
  name: "name",
251
460
  message: "App name:",
252
- validate: (v) => v.trim() ? true : "Name is required"
461
+ validate: (value) => value.trim() ? true : "Name is required"
253
462
  },
254
463
  {
255
464
  type: "input",
@@ -259,20 +468,15 @@ async function createCommand(directory) {
259
468
  }
260
469
  ]);
261
470
  const config = createDefaultConfig(name.trim(), description.trim());
471
+ const requestedSkills = [];
262
472
  console.log(
263
473
  chalk3.blue("\n Add Skills (enter a skill source, leave blank to skip)\n")
264
474
  );
265
475
  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
- )
476
+ chalk3.dim(" Supported formats: owner/repo, GitHub URL, or local path")
275
477
  );
478
+ console.log(chalk3.dim(" Example source: vercel-labs/agent-skills"));
479
+ console.log(chalk3.dim(" Example skill names: frontend-design, skill-creator\n"));
276
480
  while (true) {
277
481
  const { source } = await inquirer.prompt([
278
482
  {
@@ -281,51 +485,29 @@ async function createCommand(directory) {
281
485
  message: "Skill source (leave blank to skip):"
282
486
  }
283
487
  ]);
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();
291
- }
292
- const npxMatch = parsedSource.match(/^npx\s+[^\s]+\s+add\s+(.+)$/);
293
- if (npxMatch) {
294
- parsedSource = npxMatch[1].trim();
488
+ if (!source.trim()) {
489
+ break;
295
490
  }
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}`));
491
+ const { skillNames } = await inquirer.prompt([
492
+ {
493
+ type: "input",
494
+ name: "skillNames",
495
+ message: "Skill names (comma-separated):",
496
+ validate: (value) => parseSkillNames(value).length > 0 ? true : "Enter at least one skill name"
306
497
  }
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
- });
498
+ ]);
499
+ const nextSkills = parseSkillNames(skillNames).map((skillName) => ({
500
+ source: source.trim(),
501
+ name: skillName,
502
+ description: ""
503
+ }));
504
+ upsertSkills(config, nextSkills);
505
+ requestedSkills.push(...nextSkills);
324
506
  }
325
507
  console.log(chalk3.blue("\n Add Prompts\n"));
326
508
  console.log(
327
509
  chalk3.blue(
328
- "Use a prompt to explain how you will organize the skills you added to complete the task\n"
510
+ "Use prompts to explain how the pack should orchestrate the selected skills\n"
329
511
  )
330
512
  );
331
513
  let promptIndex = 1;
@@ -336,17 +518,15 @@ async function createCommand(directory) {
336
518
  type: "input",
337
519
  name: "prompt",
338
520
  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
521
+ validate: isFirst ? (value) => value.trim() ? true : "The first Prompt cannot be empty" : void 0
340
522
  }
341
523
  ]);
342
- if (!isFirst && !prompt.trim()) break;
524
+ if (!isFirst && !prompt.trim()) {
525
+ break;
526
+ }
343
527
  config.prompts.push(prompt.trim());
344
528
  promptIndex++;
345
529
  }
346
- saveConfig(workDir, config);
347
- console.log(chalk3.green(`
348
- ${PACK_FILE} saved
349
- `));
350
530
  const { shouldBundle } = await inquirer.prompt([
351
531
  {
352
532
  type: "confirm",
@@ -355,6 +535,14 @@ async function createCommand(directory) {
355
535
  default: true
356
536
  }
357
537
  ]);
538
+ saveConfig(workDir, config);
539
+ console.log(chalk3.green(`
540
+ ${PACK_FILE} saved
541
+ `));
542
+ if (requestedSkills.length > 0) {
543
+ installConfiguredSkills(workDir, config);
544
+ refreshDescriptionsAndSave(workDir, config);
545
+ }
358
546
  if (shouldBundle) {
359
547
  await bundle(workDir);
360
548
  }
@@ -367,8 +555,8 @@ async function createCommand(directory) {
367
555
  }
368
556
 
369
557
  // src/commands/init.ts
370
- import fs5 from "fs";
371
- import path5 from "path";
558
+ import fs6 from "fs";
559
+ import path6 from "path";
372
560
  import inquirer2 from "inquirer";
373
561
  import chalk4 from "chalk";
374
562
  function isHttpUrl(value) {
@@ -379,27 +567,6 @@ function isHttpUrl(value) {
379
567
  return false;
380
568
  }
381
569
  }
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
570
  async function readConfigSource(source) {
404
571
  let raw = "";
405
572
  if (isHttpUrl(source)) {
@@ -409,17 +576,17 @@ async function readConfigSource(source) {
409
576
  }
410
577
  raw = await response.text();
411
578
  } else {
412
- const filePath = path5.resolve(source);
413
- raw = fs5.readFileSync(filePath, "utf-8");
579
+ const filePath = path6.resolve(source);
580
+ raw = fs6.readFileSync(filePath, "utf-8");
414
581
  }
415
582
  const parsed = JSON.parse(raw);
416
583
  validateConfigShape(parsed, source);
417
584
  return parsed;
418
585
  }
419
586
  async function initCommand(directory, options) {
420
- const workDir = directory ? path5.resolve(directory) : process.cwd();
587
+ const workDir = directory ? path6.resolve(directory) : process.cwd();
421
588
  if (directory) {
422
- fs5.mkdirSync(workDir, { recursive: true });
589
+ fs6.mkdirSync(workDir, { recursive: true });
423
590
  }
424
591
  if (configExists(workDir)) {
425
592
  const { overwrite } = await inquirer2.prompt([
@@ -440,13 +607,16 @@ async function initCommand(directory, options) {
440
607
  console.log(chalk4.blue(`
441
608
  Initialize ${config.name} from ${options.config}
442
609
  `));
443
- await installPendingSkills(workDir, config);
610
+ installConfiguredSkills(workDir, config);
611
+ refreshDescriptionsAndSave(workDir, config);
612
+ copyRuntimeTemplate(getRuntimeDir(), workDir);
444
613
  if (options.bundle) {
445
614
  await bundle(workDir);
446
615
  }
447
616
  console.log(chalk4.green(`
448
617
  ${PACK_FILE} saved
449
618
  `));
619
+ console.log(chalk4.green(" Runtime template expanded.\n"));
450
620
  console.log(chalk4.green(" Initialization complete.\n"));
451
621
  if (!options.bundle) {
452
622
  console.log(
@@ -460,21 +630,37 @@ import chalk5 from "chalk";
460
630
  function registerSkillsCommand(program2) {
461
631
  const skills = program2.command("skills").description("Manage skills in the app");
462
632
  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) => {
633
+ if (!opts.skill || opts.skill.length === 0) {
634
+ console.log(
635
+ chalk5.red(
636
+ "Specify at least one skill name with --skill when adding a source"
637
+ )
638
+ );
639
+ process.exitCode = 1;
640
+ return;
641
+ }
463
642
  const workDir = process.cwd();
464
643
  const config = loadConfig(workDir);
465
- config.skills.push({
466
- name: opts.skill ? opts.skill.join(", ") : source,
644
+ const requestedSkills = opts.skill.map((name) => ({
645
+ name: name.trim(),
467
646
  source,
468
- description: "Pending installation",
469
- installSource: source,
470
- specificSkills: opts.skill && opts.skill.length > 0 ? opts.skill : void 0
471
- });
647
+ description: ""
648
+ }));
649
+ upsertSkills(config, requestedSkills);
472
650
  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
- );
651
+ try {
652
+ installSkills(workDir, requestedSkills);
653
+ refreshDescriptionsAndSave(workDir, config);
654
+ } catch (error) {
655
+ console.log(
656
+ chalk5.red(
657
+ `Skill installation failed: ${error instanceof Error ? error.message : String(error)}`
658
+ )
659
+ );
660
+ process.exitCode = 1;
661
+ return;
662
+ }
663
+ console.log(chalk5.green(`Installed ${requestedSkills.length} skill(s).`));
478
664
  });
479
665
  skills.command("remove <name>").description("Remove a skill").action((name) => {
480
666
  removeSkill(process.cwd(), name);
@@ -562,16 +748,18 @@ function registerPromptsCommand(program2) {
562
748
  }
563
749
 
564
750
  // src/cli.ts
565
- import fs6 from "fs";
751
+ import fs7 from "fs";
566
752
  var packageJson = JSON.parse(
567
- fs6.readFileSync(new URL("../package.json", import.meta.url), "utf-8")
753
+ fs7.readFileSync(new URL("../package.json", import.meta.url), "utf-8")
568
754
  );
569
755
  var program = new Command();
570
756
  program.name("skillpack").description("Assemble, package, and run Agent Skills packs").version(packageJson.version);
571
757
  program.command("create [directory]").description("Create a skills pack interactively").action(async (directory) => {
572
758
  await createCommand(directory);
573
759
  });
574
- program.command("init [directory]").description("Initialize a skills pack from a local config file or URL").requiredOption("--config <path-or-url>", "Path or URL to a skillpack.json file").option("--bundle", "Bundle as a zip after initialization").action(
760
+ program.command("init [directory]").description(
761
+ "Initialize a skills pack from a local config file or URL and expand runtime files"
762
+ ).requiredOption("--config <path-or-url>", "Path or URL to a skillpack.json file").option("--bundle", "Bundle as a zip after initialization").action(
575
763
  async (directory, options) => {
576
764
  await initCommand(directory, options);
577
765
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cremini/skillpack",
3
- "version": "1.0.4",
3
+ "version": "1.0.6",
4
4
  "description": "Turn Skills into a Standalone App with UI",
5
5
  "type": "module",
6
6
  "repository": {
@@ -1,15 +0,0 @@
1
- @echo off
2
- cd /d "%~dp0"
3
-
4
- echo.
5
- echo Starting Skills Pack...
6
- echo.
7
-
8
- if not exist "server\node_modules" (
9
- echo Installing dependencies...
10
- cd server && npm ci --omit=dev && cd ..
11
- echo.
12
- )
13
-
14
- rem Start the server (port detection and browser launch are handled by server\index.js)
15
- cd server && node index.js
@@ -1,22 +0,0 @@
1
- #!/bin/bash
2
- cd "$(dirname "$0")"
3
-
4
- # Read the pack name
5
- PACK_NAME="Skills Pack"
6
- if [ -f "skillpack.json" ] && command -v node &> /dev/null; then
7
- PACK_NAME=$(node -e "console.log(JSON.parse(require('fs').readFileSync('skillpack.json','utf-8')).name)" 2>/dev/null || echo "Skills Pack")
8
- fi
9
-
10
- echo ""
11
- echo " Starting ${PACK_NAME}..."
12
- echo ""
13
-
14
- # Install dependencies
15
- if [ ! -d "server/node_modules" ]; then
16
- echo " Installing dependencies..."
17
- cd server && npm install --omit=dev && cd ..
18
- echo ""
19
- fi
20
-
21
- # Start the server
22
- cd server && node index.js
@@ -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
- }