skills-ws 1.0.1 → 1.0.2

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.
Files changed (2) hide show
  1. package/bin/cli.mjs +207 -28
  2. package/package.json +1 -1
package/bin/cli.mjs CHANGED
@@ -4,6 +4,7 @@ import { readdir, readFile, copyFile, mkdir, stat } from "node:fs/promises";
4
4
  import { join, resolve, dirname } from "node:path";
5
5
  import { createInterface } from "node:readline";
6
6
  import { fileURLToPath } from "node:url";
7
+ import readline from "node:readline";
7
8
 
8
9
  const __dirname = dirname(fileURLToPath(import.meta.url));
9
10
  const SKILLS_DIR = join(__dirname, "..", "skills");
@@ -15,11 +16,155 @@ const GREEN = "\x1b[32m";
15
16
  const CYAN = "\x1b[36m";
16
17
  const YELLOW = "\x1b[33m";
17
18
  const WHITE = "\x1b[37m";
19
+ const GRAY = "\x1b[90m";
20
+ const MAGENTA = "\x1b[35m";
18
21
 
19
22
  function print(msg = "") {
20
23
  process.stdout.write(msg + "\n");
21
24
  }
22
25
 
26
+ // ── ASCII Animation ──────────────────────────────────────────
27
+
28
+ const BANNER_FRAMES = [
29
+ // Frame 1: empty
30
+ ``,
31
+ // Frame 2: dots forming
32
+ `
33
+ . . .
34
+ `,
35
+ // Frame 3: lines emerging
36
+ `
37
+ _______________
38
+ | |
39
+ |_______________|
40
+ `,
41
+ // Frame 4: box growing
42
+ `
43
+ _____________________________
44
+ | |
45
+ | |
46
+ | |
47
+ |_____________________________|
48
+ `,
49
+ // Frame 5: text appearing
50
+ `
51
+ _____________________________
52
+ | |
53
+ | s k i l l s |
54
+ | |
55
+ |_____________________________|
56
+ `,
57
+ // Frame 6: full banner
58
+ `
59
+ _____________________________
60
+ | |
61
+ | s k i l l s - w s |
62
+ | |
63
+ |_____________________________|
64
+ `,
65
+ // Frame 7: with underline detail
66
+ `
67
+ _____________________________
68
+ | |
69
+ | s k i l l s - w s |
70
+ | ___________________ |
71
+ |_____________________________|
72
+ `,
73
+ // Frame 8: with tagline
74
+ `
75
+ _____________________________
76
+ | |
77
+ | s k i l l s - w s |
78
+ | ___________________ |
79
+ | agent skills for AI |
80
+ |_____________________________|
81
+ `,
82
+ ];
83
+
84
+ const INSTALL_FRAMES = [
85
+ ` [ ] preparing`,
86
+ ` [= ] loading skills`,
87
+ ` [== ] loading skills`,
88
+ ` [=== ] reading manifests`,
89
+ ` [==== ] reading manifests`,
90
+ ` [===== ] copying files`,
91
+ ` [====== ] copying files`,
92
+ ` [======= ] copying files`,
93
+ ` [======== ] writing to disk`,
94
+ ` [========= ] writing to disk`,
95
+ ` [==========] done`,
96
+ ];
97
+
98
+ function hideCursor() {
99
+ process.stdout.write("\x1b[?25l");
100
+ }
101
+
102
+ function showCursor() {
103
+ process.stdout.write("\x1b[?25h");
104
+ }
105
+
106
+ function sleep(ms) {
107
+ return new Promise((r) => setTimeout(r, ms));
108
+ }
109
+
110
+ async function playBanner() {
111
+ // Skip animation if not a TTY or NO_COLOR is set
112
+ if (!process.stdout.isTTY || process.env.NO_COLOR) {
113
+ print(`\n${BOLD}${WHITE} skills-ws${RESET} ${DIM}v1.0.1${RESET}\n`);
114
+ return;
115
+ }
116
+
117
+ hideCursor();
118
+
119
+ for (const frame of BANNER_FRAMES) {
120
+ readline.cursorTo(process.stdout, 0, 0);
121
+ readline.clearScreenDown(process.stdout);
122
+ process.stdout.write(`${CYAN}${frame}${RESET}\n`);
123
+ await sleep(90);
124
+ }
125
+
126
+ // Hold final frame, then fade to colored version
127
+ await sleep(200);
128
+ readline.cursorTo(process.stdout, 0, 0);
129
+ readline.clearScreenDown(process.stdout);
130
+
131
+ print();
132
+ print(`${GRAY} _____________________________${RESET}`);
133
+ print(`${GRAY} | |${RESET}`);
134
+ print(`${GRAY} | ${WHITE}${BOLD}s k i l l s - w s${RESET}${GRAY} |${RESET}`);
135
+ print(`${GRAY} | ${DIM}___________________${RESET}${GRAY} |${RESET}`);
136
+ print(`${GRAY} | ${DIM}agent skills for AI${RESET}${GRAY} |${RESET}`);
137
+ print(`${GRAY} |_____________________________|${RESET}`);
138
+ print();
139
+
140
+ await sleep(300);
141
+ showCursor();
142
+ }
143
+
144
+ async function playInstallProgress(skillCount) {
145
+ if (!process.stdout.isTTY || process.env.NO_COLOR) return;
146
+
147
+ hideCursor();
148
+ const startLine = process.stdout.rows || 20;
149
+
150
+ for (const frame of INSTALL_FRAMES) {
151
+ readline.cursorTo(process.stdout, 0);
152
+ readline.clearLine(process.stdout, 0);
153
+ process.stdout.write(`${GREEN}${frame}${RESET}`);
154
+ await sleep(80);
155
+ }
156
+
157
+ readline.cursorTo(process.stdout, 0);
158
+ readline.clearLine(process.stdout, 0);
159
+ process.stdout.write(
160
+ `${GREEN} [==========]${RESET} ${BOLD}${skillCount} skill${skillCount !== 1 ? "s" : ""} installed${RESET}\n`
161
+ );
162
+
163
+ showCursor();
164
+ }
165
+
166
+ // ── Skill Operations ─────────────────────────────────────────
167
+
23
168
  async function getSkills() {
24
169
  const entries = await readdir(SKILLS_DIR, { withFileTypes: true });
25
170
  const skills = [];
@@ -30,8 +175,8 @@ async function getSkills() {
30
175
  try {
31
176
  await stat(skillMd);
32
177
  const content = await readFile(skillMd, "utf8");
33
- const descMatch = content.match(/^description:\s*(.+)$/m);
34
- const desc = descMatch ? descMatch[1].replace(/^["']|["']$/g, "") : "";
178
+ const descMatch = content.match(/^description:\s*["']?(.+?)["']?\s*$/m);
179
+ const desc = descMatch ? descMatch[1] : "";
35
180
  skills.push({ name: e.name, desc, dir: skillDir });
36
181
  } catch {
37
182
  // skip dirs without SKILL.md
@@ -55,7 +200,6 @@ async function copyDir(src, dest) {
55
200
  }
56
201
 
57
202
  async function detectTarget() {
58
- // Check for common skill directories
59
203
  const candidates = [
60
204
  join(process.cwd(), ".claude", "skills"),
61
205
  join(process.cwd(), "skills"),
@@ -71,46 +215,57 @@ async function detectTarget() {
71
215
  return join(process.cwd(), "skills");
72
216
  }
73
217
 
218
+ // ── Main ─────────────────────────────────────────────────────
219
+
74
220
  async function main() {
75
221
  const args = process.argv.slice(2);
76
222
  const skills = await getSkills();
77
223
 
78
- print();
79
- print(`${BOLD}${WHITE}skills-ws${RESET} ${DIM}v1.0.0${RESET}`);
80
- print(`${DIM}https://skills-ws.vercel.app${RESET}`);
81
- print();
224
+ // Always play banner
225
+ await playBanner();
82
226
 
83
227
  if (args[0] === "list" || args[0] === "ls") {
84
- print(`${BOLD}Available skills:${RESET}`);
228
+ print(`${BOLD} Available skills:${RESET}`);
85
229
  print();
86
230
  for (const s of skills) {
87
- print(` ${GREEN}${s.name}${RESET} ${DIM}${s.desc.slice(0, 80)}${RESET}`);
231
+ print(` ${GREEN}${s.name}${RESET} ${DIM}${s.desc.slice(0, 70)}${RESET}`);
88
232
  }
89
233
  print();
90
- print(`${DIM}${skills.length} skills available. Run: npx skills-ws install <name>${RESET}`);
234
+ print(` ${DIM}${skills.length} skills available. Run: npx skills-ws install <name>${RESET}`);
235
+ print();
91
236
  return;
92
237
  }
93
238
 
94
239
  if (args[0] === "install" || args[0] === "add") {
95
240
  const names = args.slice(1);
241
+
96
242
  if (names.length === 0) {
97
- // Interactive mode
98
- print(`${BOLD}Available skills:${RESET}`);
243
+ print(`${BOLD} Available skills:${RESET}`);
99
244
  print();
100
245
  for (let i = 0; i < skills.length; i++) {
101
- print(` ${DIM}${String(i + 1).padStart(2)}.${RESET} ${GREEN}${skills[i].name}${RESET} ${DIM}${skills[i].desc.slice(0, 60)}${RESET}`);
246
+ print(
247
+ ` ${DIM}${String(i + 1).padStart(2)}.${RESET} ${GREEN}${skills[i].name}${RESET} ${DIM}${skills[i].desc.slice(0, 55)}${RESET}`
248
+ );
102
249
  }
103
250
  print();
104
- print(`${YELLOW}Enter skill numbers or names (comma-separated), or 'all':${RESET}`);
251
+ print(` ${YELLOW}Enter skill numbers or names (comma-separated), or 'all':${RESET}`);
105
252
 
106
- const rl = createInterface({ input: process.stdin, output: process.stdout });
107
- const answer = await new Promise((resolve) => rl.question(`${CYAN}> ${RESET}`, resolve));
253
+ const rl = createInterface({
254
+ input: process.stdin,
255
+ output: process.stdout,
256
+ });
257
+ const answer = await new Promise((resolve) =>
258
+ rl.question(` ${CYAN}> ${RESET}`, resolve)
259
+ );
108
260
  rl.close();
109
261
 
110
262
  if (answer.trim().toLowerCase() === "all") {
111
263
  names.push(...skills.map((s) => s.name));
112
264
  } else {
113
- for (const part of answer.split(",").map((s) => s.trim()).filter(Boolean)) {
265
+ for (const part of answer
266
+ .split(",")
267
+ .map((s) => s.trim())
268
+ .filter(Boolean)) {
114
269
  const num = parseInt(part);
115
270
  if (!isNaN(num) && num >= 1 && num <= skills.length) {
116
271
  names.push(skills[num - 1].name);
@@ -122,45 +277,69 @@ async function main() {
122
277
  }
123
278
 
124
279
  if (names.length === 0) {
125
- print(`${DIM}Nothing selected.${RESET}`);
280
+ print(` ${DIM}Nothing selected.${RESET}`);
126
281
  return;
127
282
  }
128
283
 
129
284
  const target = await detectTarget();
130
285
  print();
131
- print(`${DIM}Installing to: ${target}${RESET}`);
286
+ print(` ${DIM}target: ${target}${RESET}`);
132
287
  print();
133
288
 
134
289
  let installed = 0;
135
290
  for (const name of names) {
136
291
  const skill = skills.find((s) => s.name === name);
137
292
  if (!skill) {
138
- print(` ${YELLOW}skip${RESET} ${name} (not found)`);
293
+ print(` ${YELLOW}skip${RESET} ${name} ${DIM}(not found)${RESET}`);
139
294
  continue;
140
295
  }
141
296
  const dest = join(target, name);
142
297
  await copyDir(skill.dir, dest);
143
- print(` ${GREEN}done${RESET} ${name}`);
144
298
  installed++;
145
299
  }
146
300
 
301
+ // Play install animation
302
+ await playInstallProgress(installed);
303
+ print();
304
+
305
+ // Summary
306
+ for (const name of names) {
307
+ const skill = skills.find((s) => s.name === name);
308
+ if (skill) {
309
+ print(` ${GREEN}+${RESET} ${name}`);
310
+ }
311
+ }
312
+
313
+ print();
314
+ print(` ${DIM}https://skills-ws.vercel.app${RESET}`);
147
315
  print();
148
- print(`${BOLD}${installed} skill${installed !== 1 ? "s" : ""} installed.${RESET}`);
149
316
  return;
150
317
  }
151
318
 
152
319
  // Default: show help
153
- print(`${BOLD}Usage:${RESET}`);
320
+ print(`${BOLD} Usage:${RESET}`);
154
321
  print();
155
- print(` ${CYAN}npx skills-ws list${RESET} List available skills`);
156
- print(` ${CYAN}npx skills-ws install${RESET} Interactive install`);
157
- print(` ${CYAN}npx skills-ws install <name>${RESET} Install specific skill(s)`);
158
- print(` ${CYAN}npx skills-ws install all${RESET} Install everything`);
322
+ print(` ${CYAN}npx skills-ws list${RESET} List available skills`);
323
+ print(` ${CYAN}npx skills-ws install${RESET} Interactive install`);
324
+ print(` ${CYAN}npx skills-ws install <name>${RESET} Install specific skill(s)`);
325
+ print(` ${CYAN}npx skills-ws install all${RESET} Install everything`);
326
+ print();
327
+ print(` ${DIM}${skills.length} skills | https://skills-ws.vercel.app${RESET}`);
159
328
  print();
160
- print(`${DIM}${skills.length} skills available. Browse: https://skills-ws.vercel.app${RESET}`);
161
329
  }
162
330
 
331
+ // Clean up cursor on exit
332
+ process.on("SIGINT", () => {
333
+ showCursor();
334
+ process.exit(0);
335
+ });
336
+
337
+ process.on("exit", () => {
338
+ showCursor();
339
+ });
340
+
163
341
  main().catch((err) => {
342
+ showCursor();
164
343
  console.error(err);
165
344
  process.exit(1);
166
345
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skills-ws",
3
- "version": "1.0.1",
3
+ "version": "1.0.2",
4
4
  "description": "Install agent skills for AI coding assistants — OpenClaw, Claude Code, Cursor, Codex",
5
5
  "bin": {
6
6
  "skills-ws": "./bin/cli.mjs"