@smitdev/ai-skills 0.2.0 → 0.3.1

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/README.md CHANGED
@@ -14,7 +14,8 @@ No global install needed — run it with `npx`:
14
14
  # See what's available
15
15
  npx @smitdev/ai-skills list
16
16
 
17
- # Interactive: pick assistant(s) and skill(s)
17
+ # Interactive: pick assistant(s) and skill(s) with the keyboard
18
+ # ↑/↓ move · space or tab toggle · a select all · enter confirm
18
19
  npx @smitdev/ai-skills install
19
20
 
20
21
  # Non-interactive examples
package/bin/cli.js CHANGED
@@ -1,11 +1,11 @@
1
1
  #!/usr/bin/env node
2
2
  'use strict';
3
3
 
4
- const readline = require('readline');
5
4
  const path = require('path');
6
5
  const { listSkills } = require('../lib/skills');
7
6
  const { adapters } = require('../lib/adapters');
8
7
  const { install } = require('../lib/install');
8
+ const { multiselect, select } = require('../lib/prompt');
9
9
 
10
10
  const ASSISTANT_IDS = Object.keys(adapters);
11
11
 
@@ -31,10 +31,6 @@ function parseArgs(argv) {
31
31
  return args;
32
32
  }
33
33
 
34
- function ask(rl, question) {
35
- return new Promise((resolve) => rl.question(question, (a) => resolve(a.trim())));
36
- }
37
-
38
34
  function parseList(val, valid) {
39
35
  if (val === true || !val) return [];
40
36
  const items = String(val)
@@ -55,6 +51,8 @@ Usage:
55
51
  Commands:
56
52
  list List the skills bundled in this package
57
53
  install Install skills for one or more assistants
54
+ (run with no flags for an interactive picker:
55
+ ↑/↓ move · space/tab toggle · a all · enter confirm)
58
56
 
59
57
  Options for "install":
60
58
  --assistant <ids> Comma-separated: ${ASSISTANT_IDS.join(', ')} (or "all")
@@ -73,20 +71,6 @@ Examples:
73
71
  `);
74
72
  }
75
73
 
76
- async function pickFromList(rl, label, options) {
77
- console.log(`\n${label}`);
78
- options.forEach((o, i) => console.log(` ${i + 1}. ${o.label}`));
79
- console.log(` (comma-separated numbers, or "a" for all)`);
80
- const ans = await ask(rl, '> ');
81
- if (ans.toLowerCase() === 'a' || ans === '') return options.map((o) => o.id);
82
- const picked = ans
83
- .split(',')
84
- .map((s) => parseInt(s.trim(), 10))
85
- .filter((n) => n >= 1 && n <= options.length)
86
- .map((n) => options[n - 1].id);
87
- return [...new Set(picked)];
88
- }
89
-
90
74
  async function runInstall(args) {
91
75
  const skills = listSkills();
92
76
  if (skills.length === 0) {
@@ -98,31 +82,43 @@ async function runInstall(args) {
98
82
  typeof args.flags.dir === 'string' ? args.flags.dir : process.cwd()
99
83
  );
100
84
  const dryRun = !!args.flags['dry-run'];
101
- const scope = args.flags.global ? 'global' : 'project';
85
+ const scopeFlagged = !!args.flags.global || !!args.flags.project;
86
+ let scope = args.flags.global ? 'global' : 'project';
102
87
 
103
88
  let assistants = parseList(args.flags.assistant, ASSISTANT_IDS);
104
89
  let skillNames = parseList(args.flags.skill, skills.map((s) => s.name));
105
90
 
106
- const nonInteractive =
107
- args.flags.yes || (assistants.length > 0);
91
+ const nonInteractive = args.flags.yes || assistants.length > 0;
108
92
 
109
93
  if (!nonInteractive && process.stdin.isTTY) {
110
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
111
94
  if (assistants.length === 0) {
112
- assistants = await pickFromList(
113
- rl,
114
- 'Which assistant(s)?',
115
- ASSISTANT_IDS.map((id) => ({ id, label: adapters[id].label }))
116
- );
95
+ assistants = await multiselect({
96
+ title: 'Which assistant(s)? (Claude Code, Copilot, Cursor, Windsurf)',
97
+ options: ASSISTANT_IDS.map((id) => ({ id, label: adapters[id].label })),
98
+ initial: ['claude'],
99
+ });
117
100
  }
118
101
  if (skillNames.length === 0) {
119
- skillNames = await pickFromList(
120
- rl,
121
- 'Which skill(s)?',
122
- skills.map((s) => ({ id: s.name, label: `${s.name} — ${truncate(s.description, 60)}` }))
123
- );
102
+ skillNames = await multiselect({
103
+ title: 'Which skill(s)?',
104
+ options: skills.map((s) => ({
105
+ id: s.name,
106
+ label: `${s.name} — ${truncate(s.description, 60)}`,
107
+ })),
108
+ initial: skills.map((s) => s.name),
109
+ });
110
+ }
111
+ // Only Claude Code supports a global scope; ask when relevant.
112
+ if (!scopeFlagged && assistants.includes('claude')) {
113
+ scope = await select({
114
+ title: 'Install scope for Claude Code?',
115
+ options: [
116
+ { id: 'project', label: 'This project (./.claude/skills)' },
117
+ { id: 'global', label: 'Global (~/.claude/skills, available everywhere)' },
118
+ ],
119
+ initial: 'project',
120
+ });
124
121
  }
125
- rl.close();
126
122
  }
127
123
 
128
124
  if (assistants.length === 0) assistants = ASSISTANT_IDS.slice();
package/lib/prompt.js ADDED
@@ -0,0 +1,185 @@
1
+ 'use strict';
2
+
3
+ const readline = require('readline');
4
+
5
+ // Tiny zero-dependency interactive prompts (checkbox + radio) built on
6
+ // readline keypress events. Falls back to nothing in non-TTY environments —
7
+ // callers should guard with process.stdin.isTTY.
8
+
9
+ const useColor = !!process.stdout.isTTY && !process.env.NO_COLOR;
10
+ const paint = (code) => (s) => (useColor ? `\x1b[${code}m${s}\x1b[0m` : String(s));
11
+ const dim = paint('2');
12
+ const bold = paint('1');
13
+ const cyan = paint('36');
14
+ const green = paint('32');
15
+
16
+ function setupKeys() {
17
+ const input = process.stdin;
18
+ readline.emitKeypressEvents(input);
19
+ if (input.isTTY) input.setRawMode(true);
20
+ input.resume();
21
+ return input;
22
+ }
23
+
24
+ function teardownKeys(input, onKey) {
25
+ input.removeListener('keypress', onKey);
26
+ if (input.isTTY) input.setRawMode(false);
27
+ input.pause();
28
+ }
29
+
30
+ function hideCursor() {
31
+ if (useColor) process.stdout.write('\x1b[?25l');
32
+ }
33
+ function showCursor() {
34
+ if (useColor) process.stdout.write('\x1b[?25h');
35
+ }
36
+
37
+ /**
38
+ * Multi-select checkbox prompt.
39
+ * options: [{ id, label }]
40
+ * initial: array of pre-selected ids
41
+ * Resolves to an array of selected ids (in option order). Always returns at
42
+ * least one (the highlighted item) so callers never get an empty selection.
43
+ */
44
+ function multiselect({ title, options, initial }) {
45
+ return new Promise((resolve) => {
46
+ const selected = new Set(initial || []);
47
+ let cursor = 0;
48
+ let rendered = 0;
49
+ const output = process.stdout;
50
+
51
+ const hint = dim(
52
+ '↑/↓ move · space/tab toggle · a all · enter confirm'
53
+ );
54
+
55
+ function render(first) {
56
+ if (!first) output.write(`\x1b[${rendered}A`);
57
+ const lines = [bold(title), hint];
58
+ options.forEach((o, i) => {
59
+ const active = i === cursor;
60
+ const pointer = active ? cyan('›') : ' ';
61
+ const box = selected.has(o.id) ? green('◉') : dim('◯');
62
+ const label = active ? cyan(o.label) : o.label;
63
+ lines.push(`${pointer} ${box} ${label}`);
64
+ });
65
+ output.write(lines.map((l) => '\x1b[2K' + l).join('\n') + '\n');
66
+ rendered = lines.length;
67
+ }
68
+
69
+ const input = setupKeys();
70
+ hideCursor();
71
+
72
+ function finish() {
73
+ if (selected.size === 0) selected.add(options[cursor].id);
74
+ teardownKeys(input, onKey);
75
+ showCursor();
76
+ resolve(options.filter((o) => selected.has(o.id)).map((o) => o.id));
77
+ }
78
+
79
+ function onKey(_str, key) {
80
+ if (!key) return;
81
+ if (key.ctrl && key.name === 'c') {
82
+ teardownKeys(input, onKey);
83
+ showCursor();
84
+ output.write('\n');
85
+ process.exit(130);
86
+ }
87
+ switch (key.name) {
88
+ case 'up':
89
+ case 'k':
90
+ cursor = (cursor - 1 + options.length) % options.length;
91
+ break;
92
+ case 'down':
93
+ case 'j':
94
+ cursor = (cursor + 1) % options.length;
95
+ break;
96
+ case 'space':
97
+ case 'tab':
98
+ if (selected.has(options[cursor].id)) selected.delete(options[cursor].id);
99
+ else selected.add(options[cursor].id);
100
+ break;
101
+ case 'a': {
102
+ if (selected.size === options.length) selected.clear();
103
+ else options.forEach((o) => selected.add(o.id));
104
+ break;
105
+ }
106
+ case 'return':
107
+ case 'enter':
108
+ finish();
109
+ return;
110
+ default:
111
+ return;
112
+ }
113
+ render(false);
114
+ }
115
+
116
+ input.on('keypress', onKey);
117
+ render(true);
118
+ });
119
+ }
120
+
121
+ /**
122
+ * Single-select radio prompt.
123
+ * options: [{ id, label }]
124
+ * Resolves to one id.
125
+ */
126
+ function select({ title, options, initial }) {
127
+ return new Promise((resolve) => {
128
+ let cursor = Math.max(0, options.findIndex((o) => o.id === initial));
129
+ if (cursor < 0) cursor = 0;
130
+ let rendered = 0;
131
+ const output = process.stdout;
132
+ const hint = dim('↑/↓ move · enter select');
133
+
134
+ function render(first) {
135
+ if (!first) output.write(`\x1b[${rendered}A`);
136
+ const lines = [bold(title), hint];
137
+ options.forEach((o, i) => {
138
+ const active = i === cursor;
139
+ const pointer = active ? cyan('›') : ' ';
140
+ const dot = active ? green('◉') : dim('◯');
141
+ const label = active ? cyan(o.label) : o.label;
142
+ lines.push(`${pointer} ${dot} ${label}`);
143
+ });
144
+ output.write(lines.map((l) => '\x1b[2K' + l).join('\n') + '\n');
145
+ rendered = lines.length;
146
+ }
147
+
148
+ const input = setupKeys();
149
+ hideCursor();
150
+
151
+ function onKey(_str, key) {
152
+ if (!key) return;
153
+ if (key.ctrl && key.name === 'c') {
154
+ teardownKeys(input, onKey);
155
+ showCursor();
156
+ output.write('\n');
157
+ process.exit(130);
158
+ }
159
+ switch (key.name) {
160
+ case 'up':
161
+ case 'k':
162
+ cursor = (cursor - 1 + options.length) % options.length;
163
+ break;
164
+ case 'down':
165
+ case 'j':
166
+ cursor = (cursor + 1) % options.length;
167
+ break;
168
+ case 'return':
169
+ case 'enter':
170
+ teardownKeys(input, onKey);
171
+ showCursor();
172
+ resolve(options[cursor].id);
173
+ return;
174
+ default:
175
+ return;
176
+ }
177
+ render(false);
178
+ }
179
+
180
+ input.on('keypress', onKey);
181
+ render(true);
182
+ });
183
+ }
184
+
185
+ module.exports = { multiselect, select };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@smitdev/ai-skills",
3
- "version": "0.2.0",
3
+ "version": "0.3.1",
4
4
  "description": "Install reusable AI assistant skills (Claude Code, GitHub Copilot, Cursor, Windsurf) with one command.",
5
5
  "bin": {
6
6
  "ai-skills": "bin/cli.js"
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: understand
3
- description: Read a whole codebase and write a set of plain-language docs that explain it - one overview/map doc plus one doc per aspect (UI, styles, API, models, data, testing, features, services, and so on). Use this when someone wants to understand a project as a whole or get onboarded to a repo - things like "help me understand this codebase", "document this repo", "explain how this project is structured", "map out the code", "onboard me to this project", or "I just joined and need to learn this code". This is for the WHOLE codebase. To explain one single feature in depth, use the feature-explainer skill instead.
3
+ description: Read a whole codebase and write a set of plain-language docs that explain it - one overview/map doc plus one doc per aspect (UI, styles, API, models, data, testing, features, services, and so on). Use this when someone wants to understand a project as a whole or get onboarded to a repo - things like "help me understand this codebase", "document this repo", "explain how this project is structured", "map out the code", "onboard me to this project", or "I just joined and need to learn this code". This is for the WHOLE codebase. It can also explain one single feature in depth on request - things like "explain how login works", "walk me through the checkout flow", "how does feature X work" - but only after confirming with the user first (see "Explaining a single feature on request").
4
4
  ---
5
5
 
6
6
  # Understanding
@@ -124,6 +124,51 @@ Put all the docs in one folder so they're a set. Reuse the project's docs folder
124
124
 
125
125
  Name the files simply: the map is `overview.md`, and each aspect is its lowercase name (`api.md`, `ui.md`, `data.md`, ...). Tell the user the folder path and the list of files when you're done.
126
126
 
127
+ ## Explaining a single feature on request
128
+
129
+ Sometimes the user doesn't want the whole codebase mapped - they want one feature explained in depth (e.g. "explain how login works", "walk me through the checkout flow", "how does search work"). This skill can do that too, but it's a different job from the full map, so **always confirm before starting**.
130
+
131
+ When a request looks like a single-feature explanation:
132
+
133
+ 1. **Confirm first.** Don't dive in. Ask the user something like: "Do you want me to explain just the `<feature>` feature in depth, or map the whole codebase? And should I write it to a doc file or just explain it here in the chat?" Wait for their answer before doing the work. Only proceed once they've confirmed the scope (one feature vs. whole repo) and the output (a file vs. inline).
134
+
135
+ 2. **Trace the feature, not the repo.** Once confirmed, follow the one feature end to end instead of surveying everything. Find its entry point (the button, route, command, or event that kicks it off) and follow it through the layers - UI -> API -> services -> data and back. Read only the files on that path.
136
+
137
+ 3. **Write the explanation** using this shape:
138
+
139
+ ```markdown
140
+ # Feature: <feature name>
141
+
142
+ ## What it does
143
+
144
+ One short paragraph in plain words - what this feature is, from the user's point of view.
145
+
146
+ ## How it's triggered
147
+
148
+ Where it starts - the button, route, command, or event that kicks it off, with the real file path.
149
+
150
+ ## How it flows end to end
151
+
152
+ Follow the feature step by step through the layers (UI -> API -> service -> data
153
+ -> back). This is the core of the doc. A numbered list or a small diagram works well.
154
+ Point to the real file (and function) at each step.
155
+
156
+ ## The main pieces
157
+
158
+ The key files/functions involved and what each one contributes. Just the ones on this
159
+ feature's path, not the whole repo.
160
+
161
+ ## Edge cases and gotchas
162
+
163
+ Error handling, validation, special states, and the non-obvious bits someone would miss.
164
+
165
+ ## Where to start reading
166
+
167
+ The first file or two to open to follow this feature.
168
+ ```
169
+
170
+ 4. **Output where they asked.** If they wanted a file, save it in the same docs folder the full maps use (see "Where to save the docs") as `feature-<name>.md`. If they wanted it inline, just explain it in the chat. Either way, follow the same rules below - real file paths, no invention.
171
+
127
172
  ## Rules to keep in mind
128
173
 
129
174
  - Write for a newcomer. Plain words, real examples, no walls of jargon.