@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.
- package/README.md +531 -0
- package/bin/aiskills.js +76 -0
- package/lib/cache.js +49 -0
- package/lib/cleanup.js +77 -0
- package/lib/commands/auto-update.js +131 -0
- package/lib/commands/doctor.js +139 -0
- package/lib/commands/list.js +77 -0
- package/lib/commands/skills.js +263 -0
- package/lib/commands/status.js +94 -0
- package/lib/commands/uninstall.js +182 -0
- package/lib/commands/update.js +149 -0
- package/lib/config.js +90 -0
- package/lib/downloader.js +110 -0
- package/lib/hooks.js +74 -0
- package/lib/installer.js +114 -0
- package/lib/platform.js +112 -0
- package/lib/prompts/checkboxCancel.js +264 -0
- package/lib/prompts/selectCancel.js +204 -0
- package/lib/symlink.js +154 -0
- package/lib/utils.js +49 -0
- package/package.json +61 -0
- package/skills/humaniza/SKILL.md +51 -0
- package/skills/humaniza/agents/openai.yaml +4 -0
- package/skills/humaniza/references/ai-patterns-es.md +51 -0
- package/skills/humaniza/references/checklist.md +9 -0
- package/skills/humaniza/references/examples.md +17 -0
- package/skills/humaniza/references/lexicon-es-mx.md +36 -0
- package/skills/humaniza/references/modes-es-mx.md +41 -0
- package/skills/humaniza/references/voice-es-mx.md +24 -0
- package/skills/refactoring-ui/SKILL.md +59 -0
- package/skills/refactoring-ui/references/01-design-process.md +72 -0
- package/skills/refactoring-ui/references/02-visual-hierarchy.md +84 -0
- package/skills/refactoring-ui/references/03-layout-spacing.md +69 -0
- package/skills/refactoring-ui/references/04-typography.md +70 -0
- package/skills/refactoring-ui/references/05-color.md +96 -0
- package/skills/refactoring-ui/references/06-depth-shadows.md +74 -0
- package/skills/refactoring-ui/references/07-images.md +75 -0
- package/skills/refactoring-ui/references/08-finishing-touches.md +91 -0
- package/skills/stitch-showcase/SKILL.md +411 -0
- package/skills/stitch-showcase/references/01-navbar.md +52 -0
- package/skills/stitch-showcase/references/02-hero.md +56 -0
- package/skills/stitch-showcase/references/03-design-system.md +102 -0
- package/skills/stitch-showcase/references/04-screen-gallery.md +102 -0
- package/skills/stitch-showcase/references/05-viewer-web.md +105 -0
- package/skills/stitch-showcase/references/06-viewer-mobile.md +104 -0
- package/skills/stitch-showcase/references/07-theme-system.md +77 -0
- package/skills/stitch-showcase/references/08-type-detection.md +81 -0
- package/skills/stitch-showcase/references/09-quality-standards.md +126 -0
- package/skills/stitch-showcase/references/10-component-standardization.md +40 -0
- package/skills/stitch-showcase/references/11-component-catalog.md +70 -0
- package/skills/stitch-showcase/references/catalog-template.html +841 -0
- package/skills/stitch-showcase/references/index.html +299 -0
- package/skills/stitch-showcase/references/viewer.html +412 -0
- package/skills/stitch-showcase/scripts/__pycache__/build_showcase.cpython-314.pyc +0 -0
- package/skills/stitch-showcase/scripts/__pycache__/component_utils.cpython-313.pyc +0 -0
- package/skills/stitch-showcase/scripts/__pycache__/detect_components.cpython-313.pyc +0 -0
- package/skills/stitch-showcase/scripts/__pycache__/extract_catalog.cpython-313.pyc +0 -0
- package/skills/stitch-showcase/scripts/__pycache__/extract_text.cpython-313.pyc +0 -0
- package/skills/stitch-showcase/scripts/__pycache__/extract_zips.cpython-313.pyc +0 -0
- package/skills/stitch-showcase/scripts/__pycache__/parse_design_md.cpython-313.pyc +0 -0
- package/skills/stitch-showcase/scripts/apply_canonical.py +238 -0
- package/skills/stitch-showcase/scripts/build_showcase.py +2103 -0
- package/skills/stitch-showcase/scripts/component_utils.py +398 -0
- package/skills/stitch-showcase/scripts/detect_components.py +284 -0
- package/skills/stitch-showcase/scripts/extract_catalog.py +913 -0
- package/skills/stitch-showcase/scripts/extract_text.py +268 -0
- package/skills/stitch-showcase/scripts/extract_zips.py +178 -0
- package/skills/stitch-showcase/scripts/parse_design_md.py +397 -0
- package/skills/vscode-extension-dev/SKILL.md +114 -0
- package/skills/vscode-extension-dev/references/api-patterns.md +625 -0
- package/skills/vscode-extension-dev/references/architecture.md +287 -0
- package/skills/vscode-extension-dev/references/package-json-schema.md +345 -0
- package/skills/vscode-extension-dev/references/publishing.md +251 -0
package/lib/installer.js
ADDED
|
@@ -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
|
+
};
|
package/lib/platform.js
ADDED
|
@@ -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;
|