@maccesar/aiskills 1.7.0

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 (73) hide show
  1. package/README.md +531 -0
  2. package/bin/aiskills.js +76 -0
  3. package/lib/cache.js +49 -0
  4. package/lib/cleanup.js +77 -0
  5. package/lib/commands/auto-update.js +131 -0
  6. package/lib/commands/doctor.js +139 -0
  7. package/lib/commands/list.js +77 -0
  8. package/lib/commands/skills.js +263 -0
  9. package/lib/commands/status.js +94 -0
  10. package/lib/commands/uninstall.js +182 -0
  11. package/lib/commands/update.js +149 -0
  12. package/lib/config.js +90 -0
  13. package/lib/downloader.js +110 -0
  14. package/lib/hooks.js +74 -0
  15. package/lib/installer.js +114 -0
  16. package/lib/platform.js +112 -0
  17. package/lib/prompts/checkboxCancel.js +264 -0
  18. package/lib/prompts/selectCancel.js +204 -0
  19. package/lib/symlink.js +154 -0
  20. package/lib/utils.js +49 -0
  21. package/package.json +61 -0
  22. package/skills/humaniza/SKILL.md +51 -0
  23. package/skills/humaniza/agents/openai.yaml +4 -0
  24. package/skills/humaniza/references/ai-patterns-es.md +51 -0
  25. package/skills/humaniza/references/checklist.md +9 -0
  26. package/skills/humaniza/references/examples.md +17 -0
  27. package/skills/humaniza/references/lexicon-es-mx.md +36 -0
  28. package/skills/humaniza/references/modes-es-mx.md +41 -0
  29. package/skills/humaniza/references/voice-es-mx.md +24 -0
  30. package/skills/refactoring-ui/SKILL.md +59 -0
  31. package/skills/refactoring-ui/references/01-design-process.md +72 -0
  32. package/skills/refactoring-ui/references/02-visual-hierarchy.md +84 -0
  33. package/skills/refactoring-ui/references/03-layout-spacing.md +69 -0
  34. package/skills/refactoring-ui/references/04-typography.md +70 -0
  35. package/skills/refactoring-ui/references/05-color.md +96 -0
  36. package/skills/refactoring-ui/references/06-depth-shadows.md +74 -0
  37. package/skills/refactoring-ui/references/07-images.md +75 -0
  38. package/skills/refactoring-ui/references/08-finishing-touches.md +91 -0
  39. package/skills/stitch-showcase/SKILL.md +411 -0
  40. package/skills/stitch-showcase/references/01-navbar.md +52 -0
  41. package/skills/stitch-showcase/references/02-hero.md +56 -0
  42. package/skills/stitch-showcase/references/03-design-system.md +102 -0
  43. package/skills/stitch-showcase/references/04-screen-gallery.md +102 -0
  44. package/skills/stitch-showcase/references/05-viewer-web.md +105 -0
  45. package/skills/stitch-showcase/references/06-viewer-mobile.md +104 -0
  46. package/skills/stitch-showcase/references/07-theme-system.md +77 -0
  47. package/skills/stitch-showcase/references/08-type-detection.md +81 -0
  48. package/skills/stitch-showcase/references/09-quality-standards.md +126 -0
  49. package/skills/stitch-showcase/references/10-component-standardization.md +40 -0
  50. package/skills/stitch-showcase/references/11-component-catalog.md +70 -0
  51. package/skills/stitch-showcase/references/catalog-template.html +841 -0
  52. package/skills/stitch-showcase/references/index.html +299 -0
  53. package/skills/stitch-showcase/references/viewer.html +412 -0
  54. package/skills/stitch-showcase/scripts/__pycache__/build_showcase.cpython-314.pyc +0 -0
  55. package/skills/stitch-showcase/scripts/__pycache__/component_utils.cpython-313.pyc +0 -0
  56. package/skills/stitch-showcase/scripts/__pycache__/detect_components.cpython-313.pyc +0 -0
  57. package/skills/stitch-showcase/scripts/__pycache__/extract_catalog.cpython-313.pyc +0 -0
  58. package/skills/stitch-showcase/scripts/__pycache__/extract_text.cpython-313.pyc +0 -0
  59. package/skills/stitch-showcase/scripts/__pycache__/extract_zips.cpython-313.pyc +0 -0
  60. package/skills/stitch-showcase/scripts/__pycache__/parse_design_md.cpython-313.pyc +0 -0
  61. package/skills/stitch-showcase/scripts/apply_canonical.py +238 -0
  62. package/skills/stitch-showcase/scripts/build_showcase.py +2103 -0
  63. package/skills/stitch-showcase/scripts/component_utils.py +398 -0
  64. package/skills/stitch-showcase/scripts/detect_components.py +284 -0
  65. package/skills/stitch-showcase/scripts/extract_catalog.py +913 -0
  66. package/skills/stitch-showcase/scripts/extract_text.py +268 -0
  67. package/skills/stitch-showcase/scripts/extract_zips.py +178 -0
  68. package/skills/stitch-showcase/scripts/parse_design_md.py +397 -0
  69. package/skills/vscode-extension-dev/SKILL.md +114 -0
  70. package/skills/vscode-extension-dev/references/api-patterns.md +625 -0
  71. package/skills/vscode-extension-dev/references/architecture.md +287 -0
  72. package/skills/vscode-extension-dev/references/package-json-schema.md +345 -0
  73. package/skills/vscode-extension-dev/references/publishing.md +251 -0
@@ -0,0 +1,114 @@
1
+ /**
2
+ * File installation utilities
3
+ * Installs skills to their respective directories
4
+ */
5
+
6
+ import {
7
+ existsSync,
8
+ mkdirSync,
9
+ } from 'fs';
10
+ import { join } from 'path';
11
+ import { remove, copy } from 'fs-extra';
12
+ import os from 'os';
13
+ import {
14
+ SKILLS,
15
+ getAgentsSkillsDir,
16
+ } from './config.js';
17
+ import { removeSkills } from './cleanup.js';
18
+
19
+ /**
20
+ * Recursively copy a directory
21
+ * @param {string} src - Source directory
22
+ * @param {string} dest - Destination directory
23
+ * @returns {Promise<void>}
24
+ */
25
+ export async function copyDirectory(src, dest) {
26
+ if (!existsSync(dest)) {
27
+ mkdirSync(dest, { recursive: true });
28
+ }
29
+ await copy(src, dest, { overwrite: true });
30
+ }
31
+
32
+ /**
33
+ * Install a single skill to the agents skills directory
34
+ * @param {string} repoDir - Repository directory
35
+ * @param {string} skillName - Name of the skill
36
+ * @param {string} baseDir - Base directory for installation
37
+ * @returns {Promise<boolean>} True if installed successfully
38
+ */
39
+ export async function installSkill(repoDir, skillName, baseDir = os.homedir()) {
40
+ const skillsDir = getAgentsSkillsDir(baseDir);
41
+ const skillSrc = join(repoDir, 'skills', skillName);
42
+ const skillDest = join(skillsDir, skillName);
43
+
44
+ if (!existsSync(skillsDir)) {
45
+ mkdirSync(skillsDir, { recursive: true });
46
+ }
47
+
48
+ if (!existsSync(skillSrc)) {
49
+ return false;
50
+ }
51
+
52
+ if (existsSync(skillDest)) {
53
+ await remove(skillDest);
54
+ }
55
+
56
+ await copyDirectory(skillSrc, skillDest);
57
+ return true;
58
+ }
59
+
60
+ /**
61
+ * Install all skills to the agents skills directory
62
+ * @param {string} repoDir - Repository directory
63
+ * @param {string} baseDir - Base directory for installation
64
+ * @returns {Promise<Object>} Results object with success/failure counts
65
+ */
66
+ export async function installSkills(repoDir, baseDir = os.homedir()) {
67
+ const results = {
68
+ installed: [],
69
+ failed: [],
70
+ removed: [],
71
+ };
72
+
73
+ const legacyLocal = removeSkills(baseDir, { legacyOnly: true });
74
+ results.removed.push(...legacyLocal.removed);
75
+ results.failed.push(...legacyLocal.failed);
76
+
77
+ if (baseDir && baseDir !== os.homedir()) {
78
+ const legacyGlobal = removeSkills(undefined, { legacyOnly: true });
79
+ results.removed.push(...legacyGlobal.removed);
80
+ results.failed.push(...legacyGlobal.failed);
81
+ }
82
+
83
+ for (const skill of SKILLS) {
84
+ if (await installSkill(repoDir, skill, baseDir)) {
85
+ results.installed.push(skill);
86
+ } else {
87
+ results.failed.push(skill);
88
+ }
89
+ }
90
+
91
+ return results;
92
+ }
93
+
94
+ /**
95
+ * Get the local repository directory if running from source
96
+ * @returns {string|null} Local repo directory or null
97
+ */
98
+ export function getLocalRepoDir() {
99
+ const scriptDir = new URL('..', import.meta.url).pathname;
100
+ const skillsDir = join(scriptDir, 'skills');
101
+
102
+ if (existsSync(skillsDir)) {
103
+ return scriptDir;
104
+ }
105
+
106
+ return null;
107
+ }
108
+
109
+ export default {
110
+ copyDirectory,
111
+ installSkill,
112
+ installSkills,
113
+ getLocalRepoDir,
114
+ };
@@ -0,0 +1,112 @@
1
+ /**
2
+ * Platform detection utilities
3
+ * Detects AI coding assistants and operating system
4
+ */
5
+
6
+ import { existsSync } from 'fs';
7
+ import { getPlatforms } from './config.js';
8
+
9
+ /**
10
+ * Detect installed AI coding assistant platforms
11
+ * @param {string} baseDir - Optional base directory to check
12
+ * @returns {Array} Detected platforms
13
+ */
14
+ export function detectPlatforms(baseDir) {
15
+ const detected = [];
16
+ const platforms = getPlatforms(baseDir);
17
+
18
+ for (const platform of platforms) {
19
+ if (existsSync(platform.configDir)) {
20
+ detected.push(platform);
21
+ }
22
+ }
23
+
24
+ return detected;
25
+ }
26
+
27
+ /**
28
+ * Get platform by name
29
+ * @param {string} name - Platform name (claude, gemini, codex)
30
+ * @param {string} baseDir - Optional base directory
31
+ * @returns {Object|null} Platform object or null
32
+ */
33
+ export function getPlatformByName(name, baseDir) {
34
+ const platforms = getPlatforms(baseDir);
35
+ return platforms.find((p) => p.name === name) || null;
36
+ }
37
+
38
+ /**
39
+ * Detect operating system
40
+ * @returns {string} OS type: 'macos', 'linux', 'windows'
41
+ */
42
+ export function detectOS() {
43
+ const platform = process.platform;
44
+
45
+ if (platform === 'darwin') {
46
+ return 'macos';
47
+ }
48
+ if (platform === 'win32') {
49
+ return 'windows';
50
+ }
51
+ return 'linux';
52
+ }
53
+
54
+ /**
55
+ * Check if running on Windows
56
+ * @returns {boolean}
57
+ */
58
+ export function isWindows() {
59
+ return process.platform === 'win32';
60
+ }
61
+
62
+ /**
63
+ * Check if running on macOS
64
+ * @returns {boolean}
65
+ */
66
+ export function isMacOS() {
67
+ return process.platform === 'darwin';
68
+ }
69
+
70
+ /**
71
+ * Check if running on Linux
72
+ * @returns {boolean}
73
+ */
74
+ export function isLinux() {
75
+ return process.platform === 'linux';
76
+ }
77
+
78
+ /**
79
+ * Get bin directory path for the current platform
80
+ * @returns {string} Path to bin directory
81
+ */
82
+ export function getBinDir() {
83
+ if (isWindows()) {
84
+ return `${process.env.HOME || process.env.USERPROFILE}\\bin`;
85
+ }
86
+ return '/usr/local/bin';
87
+ }
88
+
89
+ /**
90
+ * Check if we have sudo/admin permissions
91
+ * @returns {boolean}
92
+ */
93
+ export function hasSudo() {
94
+ if (isWindows()) {
95
+ // Windows: check if running as administrator
96
+ return process.env.USERDOMAIN === process.env.COMPUTERNAME;
97
+ }
98
+ // Unix-like: check if we can write to /usr/local/bin
99
+ const binDir = getBinDir();
100
+ return existsSync(binDir) && process.getuid && process.getuid() === 0;
101
+ }
102
+
103
+ export default {
104
+ detectPlatforms,
105
+ getPlatformByName,
106
+ detectOS,
107
+ isWindows,
108
+ isMacOS,
109
+ isLinux,
110
+ getBinDir,
111
+ hasSudo,
112
+ };
@@ -0,0 +1,264 @@
1
+ import {
2
+ Separator,
3
+ ValidationError,
4
+ createPrompt,
5
+ isDownKey,
6
+ isEnterKey,
7
+ isNumberKey,
8
+ isSpaceKey,
9
+ isUpKey,
10
+ makeTheme,
11
+ useKeypress,
12
+ useMemo,
13
+ usePagination,
14
+ usePrefix,
15
+ useState,
16
+ } from '@inquirer/core';
17
+ import { cursorHide } from '@inquirer/ansi';
18
+ import { styleText } from 'node:util';
19
+ import figures from '@inquirer/figures';
20
+
21
+ const checkboxTheme = {
22
+ icon: {
23
+ checked: styleText('green', figures.circleFilled),
24
+ unchecked: figures.circle,
25
+ cursor: figures.pointer,
26
+ },
27
+ style: {
28
+ disabledChoice: (text) => styleText('dim', `- ${text}`),
29
+ renderSelectedChoices: (selectedChoices) =>
30
+ selectedChoices.map((choice) => choice.short).join(', '),
31
+ description: (text) => styleText('cyan', text),
32
+ keysHelpTip: (keys) =>
33
+ keys
34
+ .map(([key, action]) => `${styleText('bold', key)} ${styleText('dim', action)}`)
35
+ .join(styleText('dim', ' • ')),
36
+ },
37
+ keybindings: [],
38
+ };
39
+
40
+ function isSelectable(item) {
41
+ return !Separator.isSeparator(item) && !item.disabled;
42
+ }
43
+
44
+ function isChecked(item) {
45
+ return isSelectable(item) && item.checked;
46
+ }
47
+
48
+ function toggle(item) {
49
+ return isSelectable(item) ? { ...item, checked: !item.checked } : item;
50
+ }
51
+
52
+ function check(checked) {
53
+ return function (item) {
54
+ return isSelectable(item) ? { ...item, checked } : item;
55
+ };
56
+ }
57
+
58
+ function normalizeChoices(choices) {
59
+ return choices.map((choice) => {
60
+ if (Separator.isSeparator(choice)) return choice;
61
+ if (typeof choice === 'string') {
62
+ return {
63
+ value: choice,
64
+ name: choice,
65
+ short: choice,
66
+ checkedName: choice,
67
+ disabled: false,
68
+ checked: false,
69
+ };
70
+ }
71
+ const name = choice.name ?? String(choice.value);
72
+ const normalizedChoice = {
73
+ value: choice.value,
74
+ name,
75
+ short: choice.short ?? name,
76
+ checkedName: choice.checkedName ?? name,
77
+ disabled: choice.disabled ?? false,
78
+ checked: choice.checked ?? false,
79
+ };
80
+ if (choice.description) {
81
+ normalizedChoice.description = choice.description;
82
+ }
83
+ return normalizedChoice;
84
+ });
85
+ }
86
+
87
+ const checkboxCancel = createPrompt((config, done) => {
88
+ const {
89
+ pageSize = 7,
90
+ loop = true,
91
+ required,
92
+ validate = () => true,
93
+ cancelValue = 'cancel',
94
+ } = config;
95
+
96
+ // Shortcuts handling - allow disabling by setting to false or null
97
+ const shortcuts = {
98
+ all: config.shortcuts?.all === undefined ? 'a' : config.shortcuts.all,
99
+ invert: config.shortcuts?.invert === undefined ? 'i' : config.shortcuts.invert
100
+ };
101
+
102
+ const theme = makeTheme(checkboxTheme, config.theme);
103
+ const { keybindings } = theme;
104
+ const [status, setStatus] = useState('idle');
105
+ const prefix = usePrefix({ status, theme });
106
+ const [items, setItems] = useState(normalizeChoices(config.choices));
107
+ const bounds = useMemo(() => {
108
+ const first = items.findIndex(isSelectable);
109
+ const last = items.findLastIndex(isSelectable);
110
+ if (first === -1) {
111
+ throw new ValidationError('[checkbox prompt] No selectable choices. All choices are disabled.');
112
+ }
113
+ return { first, last };
114
+ }, [items]);
115
+ const [active, setActive] = useState(bounds.first);
116
+ const [errorMsg, setError] = useState();
117
+ const [cancelled, setCancelled] = useState(false);
118
+
119
+ useKeypress(async (key) => {
120
+ if (key.name?.toLowerCase() === 'q') {
121
+ setCancelled(true);
122
+ setStatus('done');
123
+ done([cancelValue]);
124
+ return;
125
+ }
126
+ if (isEnterKey(key)) {
127
+ const activeItem = items[active];
128
+
129
+ // If the active item IS the cancel option, cancel immediately
130
+ if (isSelectable(activeItem) && activeItem.value === cancelValue) {
131
+ setCancelled(true);
132
+ setStatus('done');
133
+ done([cancelValue]);
134
+ return;
135
+ }
136
+
137
+ // If ANY checked item is the cancel option, cancel immediately
138
+ const anyCancelChecked = items.some(
139
+ (item) => isSelectable(item) && item.value === cancelValue && item.checked
140
+ );
141
+ if (anyCancelChecked) {
142
+ setCancelled(true);
143
+ setStatus('done');
144
+ done([cancelValue]);
145
+ return;
146
+ }
147
+
148
+ const selection = items.filter(isChecked);
149
+ const isValid = await validate([...selection]);
150
+ if (required && selection.length === 0) {
151
+ setError('At least one choice must be selected');
152
+ } else if (isValid === true) {
153
+ setStatus('done');
154
+ done(selection.map((choice) => choice.value));
155
+ } else {
156
+ setError(isValid || 'You must select a valid value');
157
+ }
158
+ } else if (isUpKey(key, keybindings) || isDownKey(key, keybindings)) {
159
+ if (
160
+ loop ||
161
+ (isUpKey(key, keybindings) && active !== bounds.first) ||
162
+ (isDownKey(key, keybindings) && active !== bounds.last)
163
+ ) {
164
+ const offset = isUpKey(key, keybindings) ? -1 : 1;
165
+ let next = active;
166
+ do {
167
+ next = (next + offset + items.length) % items.length;
168
+ } while (!isSelectable(items[next]));
169
+ setActive(next);
170
+ }
171
+ } else if (isSpaceKey(key)) {
172
+ setError(undefined);
173
+ setItems(items.map((choice, i) => (i === active ? toggle(choice) : choice)));
174
+ } else if (shortcuts.all && key.name === shortcuts.all) {
175
+ const selectAll = items.some((choice) => isSelectable(choice) && !choice.checked && choice.value !== cancelValue);
176
+ setItems(items.map((item) => {
177
+ if (isSelectable(item) && item.value !== cancelValue) {
178
+ return { ...item, checked: selectAll };
179
+ }
180
+ return item;
181
+ }));
182
+ } else if (shortcuts.invert && key.name === shortcuts.invert) {
183
+ setItems(items.map((item) => {
184
+ if (isSelectable(item) && item.value !== cancelValue) {
185
+ return toggle(item);
186
+ }
187
+ return item;
188
+ }));
189
+ } else if (isNumberKey(key)) {
190
+ const selectedIndex = Number(key.name) - 1;
191
+ let selectableIndex = -1;
192
+ const position = items.findIndex((item) => {
193
+ if (Separator.isSeparator(item)) return false;
194
+ selectableIndex++;
195
+ return selectableIndex === selectedIndex;
196
+ });
197
+ const selectedItem = items[position];
198
+ if (selectedItem && isSelectable(selectedItem)) {
199
+ setActive(position);
200
+ setItems(items.map((choice, i) => (i === position ? toggle(choice) : choice)));
201
+ }
202
+ }
203
+ });
204
+
205
+ const message = theme.style.message(config.message, status);
206
+ let description;
207
+ const page = usePagination({
208
+ items,
209
+ active,
210
+ renderItem({ item, isActive }) {
211
+ if (Separator.isSeparator(item)) {
212
+ return ` ${item.separator}`;
213
+ }
214
+ if (item.disabled) {
215
+ const disabledLabel = typeof item.disabled === 'string' ? item.disabled : '(disabled)';
216
+ return theme.style.disabledChoice(`${item.name} ${disabledLabel}`);
217
+ }
218
+ if (isActive) {
219
+ description = item.description;
220
+ }
221
+ const checkbox = item.checked ? theme.icon.checked : theme.icon.unchecked;
222
+ const name = item.checked ? item.checkedName : item.name;
223
+ const color = isActive ? theme.style.highlight : (x) => x;
224
+ const cursor = isActive ? theme.icon.cursor : ' ';
225
+ return color(`${cursor}${checkbox} ${name}`);
226
+ },
227
+ pageSize,
228
+ loop,
229
+ });
230
+
231
+ if (status === 'done') {
232
+ if (cancelled) {
233
+ return [prefix, message].filter(Boolean).join(' ');
234
+ }
235
+ const selection = items.filter(item => isChecked(item) && item.value !== cancelValue);
236
+ const answer = theme.style.answer(theme.style.renderSelectedChoices(selection, items));
237
+ return [prefix, message, answer].filter(Boolean).join(' ');
238
+ }
239
+
240
+ const keys = [
241
+ ['↑↓', 'navigate'],
242
+ ['space', 'select'],
243
+ ];
244
+ if (shortcuts.all) keys.push([shortcuts.all, 'all']);
245
+ if (shortcuts.invert) keys.push([shortcuts.invert, 'invert']);
246
+ keys.push(['⏎', 'submit']);
247
+ keys.push(['q', 'quit']);
248
+ const helpLine = theme.style.keysHelpTip(keys);
249
+ const lines = [
250
+ [prefix, message].filter(Boolean).join(' '),
251
+ page,
252
+ ' ',
253
+ description ? theme.style.description(description) : '',
254
+ errorMsg ? theme.style.error(errorMsg) : '',
255
+ helpLine,
256
+ ]
257
+ .filter(Boolean)
258
+ .join('\n')
259
+ .trimEnd();
260
+ return `${lines}${cursorHide}`;
261
+ });
262
+
263
+ export { Separator };
264
+ export default checkboxCancel;
@@ -0,0 +1,204 @@
1
+ import {
2
+ Separator,
3
+ ValidationError,
4
+ createPrompt,
5
+ isBackspaceKey,
6
+ isDownKey,
7
+ isEnterKey,
8
+ isNumberKey,
9
+ isUpKey,
10
+ makeTheme,
11
+ useEffect,
12
+ useKeypress,
13
+ useMemo,
14
+ usePagination,
15
+ usePrefix,
16
+ useRef,
17
+ useState,
18
+ } from '@inquirer/core';
19
+ import { cursorHide } from '@inquirer/ansi';
20
+ import { styleText } from 'node:util';
21
+ import figures from '@inquirer/figures';
22
+
23
+ const selectTheme = {
24
+ icon: { cursor: figures.pointer },
25
+ style: {
26
+ disabled: (text) => styleText('dim', `- ${text}`),
27
+ description: (text) => styleText('cyan', text),
28
+ keysHelpTip: (keys) =>
29
+ keys
30
+ .map(([key, action]) => `${styleText('bold', key)} ${styleText('dim', action)}`)
31
+ .join(styleText('dim', ' • ')),
32
+ },
33
+ indexMode: 'hidden',
34
+ keybindings: [],
35
+ };
36
+
37
+ function isSelectable(item) {
38
+ return !Separator.isSeparator(item) && !item.disabled;
39
+ }
40
+
41
+ function normalizeChoices(choices) {
42
+ return choices.map((choice) => {
43
+ if (Separator.isSeparator(choice)) return choice;
44
+ if (typeof choice !== 'object' || choice === null || !('value' in choice)) {
45
+ const name = String(choice);
46
+ return {
47
+ value: choice,
48
+ name,
49
+ short: name,
50
+ disabled: false,
51
+ };
52
+ }
53
+ const name = choice.name ?? String(choice.value);
54
+ const normalizedChoice = {
55
+ value: choice.value,
56
+ name,
57
+ short: choice.short ?? name,
58
+ disabled: choice.disabled ?? false,
59
+ };
60
+ if (choice.description) {
61
+ normalizedChoice.description = choice.description;
62
+ }
63
+ return normalizedChoice;
64
+ });
65
+ }
66
+
67
+ const selectCancel = createPrompt((config, done) => {
68
+ const { loop = true, pageSize = 7, cancelValue = 'cancel' } = config;
69
+ const theme = makeTheme(selectTheme, config.theme);
70
+ const { keybindings } = theme;
71
+ const [status, setStatus] = useState('idle');
72
+ const [cancelled, setCancelled] = useState(false);
73
+ const prefix = usePrefix({ status, theme });
74
+ const searchTimeoutRef = useRef();
75
+ const searchEnabled = !keybindings.includes('vim');
76
+ const items = useMemo(() => normalizeChoices(config.choices), [config.choices]);
77
+ const bounds = useMemo(() => {
78
+ const first = items.findIndex(isSelectable);
79
+ const last = items.findLastIndex(isSelectable);
80
+ if (first === -1) {
81
+ throw new ValidationError('[select prompt] No selectable choices. All choices are disabled.');
82
+ }
83
+ return { first, last };
84
+ }, [items]);
85
+ const defaultItemIndex = useMemo(() => {
86
+ if (!('default' in config)) return -1;
87
+ return items.findIndex((item) => isSelectable(item) && item.value === config.default);
88
+ }, [config.default, items]);
89
+ const [active, setActive] = useState(defaultItemIndex === -1 ? bounds.first : defaultItemIndex);
90
+ const selectedChoice = items[active];
91
+
92
+ useKeypress((key, rl) => {
93
+ clearTimeout(searchTimeoutRef.current);
94
+ if (key.name?.toLowerCase() === 'q') {
95
+ setCancelled(true);
96
+ setStatus('done');
97
+ done(cancelValue);
98
+ } else if (isEnterKey(key)) {
99
+ setStatus('done');
100
+ done(selectedChoice.value);
101
+ }
102
+ else if (isUpKey(key, keybindings) || isDownKey(key, keybindings)) {
103
+ rl.clearLine(0);
104
+ if (
105
+ loop ||
106
+ (isUpKey(key, keybindings) && active !== bounds.first) ||
107
+ (isDownKey(key, keybindings) && active !== bounds.last)
108
+ ) {
109
+ const offset = isUpKey(key, keybindings) ? -1 : 1;
110
+ let next = active;
111
+ do {
112
+ next = (next + offset + items.length) % items.length;
113
+ } while (!isSelectable(items[next]));
114
+ setActive(next);
115
+ }
116
+ } else if (isNumberKey(key) && !Number.isNaN(Number(rl.line))) {
117
+ const selectedIndex = Number(rl.line) - 1;
118
+ let selectableIndex = -1;
119
+ const position = items.findIndex((item) => {
120
+ if (Separator.isSeparator(item)) return false;
121
+ selectableIndex++;
122
+ return selectableIndex === selectedIndex;
123
+ });
124
+ const item = items[position];
125
+ if (item != null && isSelectable(item)) {
126
+ setActive(position);
127
+ }
128
+ searchTimeoutRef.current = setTimeout(() => {
129
+ rl.clearLine(0);
130
+ }, 700);
131
+ } else if (isBackspaceKey(key)) {
132
+ rl.clearLine(0);
133
+ } else if (searchEnabled) {
134
+ const searchTerm = rl.line.toLowerCase();
135
+ const matchIndex = items.findIndex((item) => {
136
+ if (Separator.isSeparator(item) || !isSelectable(item)) return false;
137
+ return item.name.toLowerCase().startsWith(searchTerm);
138
+ });
139
+ if (matchIndex !== -1) {
140
+ setActive(matchIndex);
141
+ }
142
+ searchTimeoutRef.current = setTimeout(() => {
143
+ rl.clearLine(0);
144
+ }, 700);
145
+ }
146
+ });
147
+
148
+ useEffect(() => () => {
149
+ clearTimeout(searchTimeoutRef.current);
150
+ }, []);
151
+
152
+ const message = theme.style.message(config.message, status);
153
+ const helpLine = theme.style.keysHelpTip([
154
+ ['↑↓', 'navigate'],
155
+ ['⏎', 'select'],
156
+ ['q', 'quit'],
157
+ ]);
158
+ let separatorCount = 0;
159
+ const page = usePagination({
160
+ items,
161
+ active,
162
+ renderItem({ item, isActive, index }) {
163
+ if (Separator.isSeparator(item)) {
164
+ separatorCount++;
165
+ return ` ${item.separator}`;
166
+ }
167
+ const indexLabel = theme.indexMode === 'number' ? `${index + 1 - separatorCount}. ` : '';
168
+ if (item.disabled) {
169
+ const disabledLabel = typeof item.disabled === 'string' ? item.disabled : '(disabled)';
170
+ return theme.style.disabled(`${indexLabel}${item.name} ${disabledLabel}`);
171
+ }
172
+ const color = isActive ? theme.style.highlight : (x) => x;
173
+ const cursor = isActive ? theme.icon.cursor : ` `;
174
+ return color(`${cursor} ${indexLabel}${item.name}`);
175
+ },
176
+ pageSize,
177
+ loop,
178
+ });
179
+
180
+ if (status === 'done') {
181
+ if (cancelled) {
182
+ return [prefix, message].filter(Boolean).join(' ');
183
+ }
184
+ return [prefix, message, theme.style.answer(selectedChoice.short)]
185
+ .filter(Boolean)
186
+ .join(' ');
187
+ }
188
+
189
+ const { description } = selectedChoice;
190
+ const lines = [
191
+ [prefix, message].filter(Boolean).join(' '),
192
+ page,
193
+ ' ',
194
+ description ? theme.style.description(description) : '',
195
+ helpLine,
196
+ ]
197
+ .filter(Boolean)
198
+ .join('\n')
199
+ .trimEnd();
200
+ return `${lines}${cursorHide}`;
201
+ });
202
+
203
+ export { Separator };
204
+ export default selectCancel;