@morya-ui/setup 0.3.3 → 0.3.5
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 +21 -21
- package/README.md +107 -103
- package/bin/morya-ui-setup.js +14 -14
- package/catalog/skills.json +46 -46
- package/package.json +1 -1
- package/src/cli.mjs +312 -335
- package/src/copy-template.mjs +78 -78
- package/src/fs-utils.mjs +24 -24
- package/src/install.mjs +134 -63
- package/src/mcp.mjs +50 -50
- package/src/package-json.mjs +30 -30
- package/src/skills.mjs +223 -223
- package/src/styles.mjs +119 -119
- package/template/.agents/skills/morya-ui-pages/SKILL.md +188 -164
- package/template/.agents/skills/morya-ui-pages/evals/evals.json +89 -89
- package/template/.agents/skills/morya-ui-pages/references/component-index.md +99 -99
- package/template/.agents/skills/morya-ui-pages/references/decision-recipes.md +104 -5
- package/template/.agents/skills/morya-ui-pages/references/design-system.md +101 -100
- package/template/.agents/skills/morya-ui-pages/references/feedback.md +68 -68
- package/template/.agents/skills/morya-ui-pages/references/optional-companions.md +62 -34
- package/template/.agents/skills/morya-ui-pages/references/page-layouts.md +133 -119
- package/template/.agents/skills/morya-ui-pages/references/review-checklist.md +63 -61
- package/template/.agents/skills/morya-ui-pages/references/style-presets.md +45 -0
- package/template/.agents/skills/morya-ui-pages/references/surfaces.md +91 -89
- package/template/.agents/skills/morya-ui-pages/references/visual-craft.md +129 -196
- package/template/.cursor/rules/coding-style.mdc +41 -41
- package/template/.cursor/rules/component-usage.mdc +41 -41
- package/template/.cursor/rules/design-system.mdc +17 -17
- package/template/.cursor/rules/page-layout.mdc +97 -78
- package/template/DESIGN.md +59 -59
- package/template/scripts/check-raw-colors.mjs +74 -74
package/src/skills.mjs
CHANGED
|
@@ -1,223 +1,223 @@
|
|
|
1
|
-
import { execSync } from 'node:child_process'
|
|
2
|
-
import { createInterface } from 'node:readline'
|
|
3
|
-
import { dirname, join } from 'node:path'
|
|
4
|
-
import { fileURLToPath } from 'node:url'
|
|
5
|
-
import { readJson } from './fs-utils.mjs'
|
|
6
|
-
|
|
7
|
-
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
8
|
-
const CATALOG_PATH = join(__dirname, '..', 'catalog', 'skills.json')
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* @typedef {{
|
|
12
|
-
* id: string
|
|
13
|
-
* name: string
|
|
14
|
-
* description: string
|
|
15
|
-
* required?: boolean
|
|
16
|
-
* default?: boolean
|
|
17
|
-
* install?: 'template' | 'skills-cli'
|
|
18
|
-
* source?: string
|
|
19
|
-
* skill?: string
|
|
20
|
-
* path: string
|
|
21
|
-
* }} SkillEntry
|
|
22
|
-
*/
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* @returns {{ skills: SkillEntry[] }}
|
|
26
|
-
*/
|
|
27
|
-
export function loadSkillsCatalog() {
|
|
28
|
-
return readJson(CATALOG_PATH)
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* @param {SkillEntry} skill
|
|
33
|
-
*/
|
|
34
|
-
export function isTemplateSkill(skill) {
|
|
35
|
-
return !skill.install || skill.install === 'template'
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* @param {SkillEntry} skill
|
|
40
|
-
*/
|
|
41
|
-
export function isSkillsCliSkill(skill) {
|
|
42
|
-
return skill.install === 'skills-cli'
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* @param {SkillEntry[]} skills
|
|
47
|
-
*/
|
|
48
|
-
export function defaultSkillIds(skills) {
|
|
49
|
-
return skills.filter((s) => s.required || s.default).map((s) => s.id)
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* @param {string | undefined} raw
|
|
54
|
-
* @param {SkillEntry[]} skills
|
|
55
|
-
* @returns {string[]}
|
|
56
|
-
*/
|
|
57
|
-
export function parseSkillsFlag(raw, skills) {
|
|
58
|
-
if (raw == null || raw === '') return defaultSkillIds(skills)
|
|
59
|
-
const known = new Map(skills.map((s) => [s.id, s]))
|
|
60
|
-
if (raw.trim() === 'all') return skills.map((s) => s.id)
|
|
61
|
-
|
|
62
|
-
const ids = raw
|
|
63
|
-
.split(',')
|
|
64
|
-
.map((part) => part.trim())
|
|
65
|
-
.filter(Boolean)
|
|
66
|
-
|
|
67
|
-
if (ids.length === 0) return defaultSkillIds(skills)
|
|
68
|
-
|
|
69
|
-
const missing = ids.filter((id) => !known.has(id))
|
|
70
|
-
if (missing.length) {
|
|
71
|
-
throw new Error(
|
|
72
|
-
`Unknown skill id(s): ${missing.join(', ')}. Available: ${skills.map((s) => s.id).join(', ')}, or all`,
|
|
73
|
-
)
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
const selected = new Set(ids)
|
|
77
|
-
for (const skill of skills) {
|
|
78
|
-
if (skill.required) selected.add(skill.id)
|
|
79
|
-
}
|
|
80
|
-
return skills.map((s) => s.id).filter((id) => selected.has(id))
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* Template-relative paths for skills that ship inside the package template.
|
|
85
|
-
* @param {string[]} skillIds
|
|
86
|
-
* @param {SkillEntry[]} skills
|
|
87
|
-
* @returns {string[]}
|
|
88
|
-
*/
|
|
89
|
-
export function skillPathsForIds(skillIds, skills) {
|
|
90
|
-
const byId = new Map(skills.map((s) => [s.id, s]))
|
|
91
|
-
return skillIds.flatMap((id) => {
|
|
92
|
-
const entry = byId.get(id)
|
|
93
|
-
if (!entry) throw new Error(`Unknown skill id: ${id}`)
|
|
94
|
-
if (!isTemplateSkill(entry)) return []
|
|
95
|
-
return [entry.path]
|
|
96
|
-
})
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
/**
|
|
100
|
-
* Build template include prefixes for the AI pack (first-party files only).
|
|
101
|
-
* @param {string[]} skillIds
|
|
102
|
-
* @param {SkillEntry[]} skills
|
|
103
|
-
*/
|
|
104
|
-
export function buildAiInclude(skillIds, skills) {
|
|
105
|
-
return [
|
|
106
|
-
'DESIGN.md',
|
|
107
|
-
...skillPathsForIds(skillIds, skills),
|
|
108
|
-
'.cursor/rules',
|
|
109
|
-
'scripts/check-raw-colors.mjs',
|
|
110
|
-
]
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
/**
|
|
114
|
-
* Install selected companion skills via the skills CLI (always latest from source).
|
|
115
|
-
* @param {string} cwd
|
|
116
|
-
* @param {string[]} skillIds
|
|
117
|
-
* @param {SkillEntry[]} skills
|
|
118
|
-
* @param {{ dryRun?: boolean }} [options]
|
|
119
|
-
* @returns {{ installed: string[], commands: string[], dryRun?: boolean, skipped?: boolean }}
|
|
120
|
-
*/
|
|
121
|
-
export function installSkillsCli(cwd, skillIds, skills, { dryRun = false } = {}) {
|
|
122
|
-
const selected = skills.filter((s) => skillIds.includes(s.id) && isSkillsCliSkill(s))
|
|
123
|
-
if (!selected.length) {
|
|
124
|
-
return { installed: [], commands: [], skipped: true }
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
for (const entry of selected) {
|
|
128
|
-
if (!entry.source) {
|
|
129
|
-
throw new Error(`Skill "${entry.id}" is marked skills-cli but has no source`)
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
/** @type {Map<string, string[]>} */
|
|
134
|
-
const bySource = new Map()
|
|
135
|
-
for (const entry of selected) {
|
|
136
|
-
const name = entry.skill || entry.id
|
|
137
|
-
const list = bySource.get(entry.source) || []
|
|
138
|
-
list.push(name)
|
|
139
|
-
bySource.set(entry.source, list)
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
const commands = []
|
|
143
|
-
for (const [source, names] of bySource) {
|
|
144
|
-
const skillFlags = names.map((name) => `-s ${name}`).join(' ')
|
|
145
|
-
commands.push(`npx -y skills add ${source} ${skillFlags} -a cursor -y`)
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
if (dryRun) {
|
|
149
|
-
return { installed: selected.map((s) => s.id), commands, dryRun: true }
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
console.log('Companion skills (latest via skills CLI):')
|
|
153
|
-
for (const command of commands) {
|
|
154
|
-
console.log(` $ ${command}`)
|
|
155
|
-
execSync(command, { cwd, stdio: 'inherit', shell: true })
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
return { installed: selected.map((s) => s.id), commands }
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
/**
|
|
162
|
-
* Prompt for optional skills when stdin is a TTY.
|
|
163
|
-
* Required skills are always included.
|
|
164
|
-
* @param {SkillEntry[]} skills
|
|
165
|
-
* @param {{ skipPrompt?: boolean }} [options]
|
|
166
|
-
* @returns {Promise<string[]>}
|
|
167
|
-
*/
|
|
168
|
-
export async function resolveSkillSelection(skills, { skipPrompt = false } = {}) {
|
|
169
|
-
const required = skills.filter((s) => s.required).map((s) => s.id)
|
|
170
|
-
const optional = skills.filter((s) => !s.required)
|
|
171
|
-
|
|
172
|
-
if (skipPrompt || optional.length === 0 || !process.stdin.isTTY) {
|
|
173
|
-
return defaultSkillIds(skills)
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
console.log('Select optional Agent skills (in addition to required):')
|
|
177
|
-
optional.forEach((skill, index) => {
|
|
178
|
-
const mark = skill.default ? 'x' : ' '
|
|
179
|
-
const via = isSkillsCliSkill(skill) ? ' [latest via skills CLI]' : ''
|
|
180
|
-
console.log(` ${index + 1}. [${mark}] ${skill.id} — ${skill.description}${via}`)
|
|
181
|
-
})
|
|
182
|
-
console.log('')
|
|
183
|
-
console.log('Enter comma-separated numbers or ids (empty = defaults only).')
|
|
184
|
-
console.log(`Required: ${required.join(', ') || '(none)'}`)
|
|
185
|
-
|
|
186
|
-
const answer = await question('> ')
|
|
187
|
-
const trimmed = answer.trim()
|
|
188
|
-
if (!trimmed) return defaultSkillIds(skills)
|
|
189
|
-
|
|
190
|
-
const selected = new Set(required)
|
|
191
|
-
const tokens = trimmed.split(/[\s,]+/).filter(Boolean)
|
|
192
|
-
for (const token of tokens) {
|
|
193
|
-
const asIndex = Number(token)
|
|
194
|
-
if (Number.isInteger(asIndex) && asIndex >= 1 && asIndex <= optional.length) {
|
|
195
|
-
selected.add(optional[asIndex - 1].id)
|
|
196
|
-
continue
|
|
197
|
-
}
|
|
198
|
-
if (token === 'all') {
|
|
199
|
-
for (const skill of optional) selected.add(skill.id)
|
|
200
|
-
continue
|
|
201
|
-
}
|
|
202
|
-
if (!skills.some((s) => s.id === token)) {
|
|
203
|
-
throw new Error(`Unknown skill selection: ${token}`)
|
|
204
|
-
}
|
|
205
|
-
selected.add(token)
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
return skills.map((s) => s.id).filter((id) => selected.has(id))
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
/**
|
|
212
|
-
* @param {string} prompt
|
|
213
|
-
* @returns {Promise<string>}
|
|
214
|
-
*/
|
|
215
|
-
function question(prompt) {
|
|
216
|
-
const rl = createInterface({ input: process.stdin, output: process.stdout })
|
|
217
|
-
return new Promise((resolve) => {
|
|
218
|
-
rl.question(prompt, (answer) => {
|
|
219
|
-
rl.close()
|
|
220
|
-
resolve(answer)
|
|
221
|
-
})
|
|
222
|
-
})
|
|
223
|
-
}
|
|
1
|
+
import { execSync } from 'node:child_process'
|
|
2
|
+
import { createInterface } from 'node:readline'
|
|
3
|
+
import { dirname, join } from 'node:path'
|
|
4
|
+
import { fileURLToPath } from 'node:url'
|
|
5
|
+
import { readJson } from './fs-utils.mjs'
|
|
6
|
+
|
|
7
|
+
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
8
|
+
const CATALOG_PATH = join(__dirname, '..', 'catalog', 'skills.json')
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @typedef {{
|
|
12
|
+
* id: string
|
|
13
|
+
* name: string
|
|
14
|
+
* description: string
|
|
15
|
+
* required?: boolean
|
|
16
|
+
* default?: boolean
|
|
17
|
+
* install?: 'template' | 'skills-cli'
|
|
18
|
+
* source?: string
|
|
19
|
+
* skill?: string
|
|
20
|
+
* path: string
|
|
21
|
+
* }} SkillEntry
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* @returns {{ skills: SkillEntry[] }}
|
|
26
|
+
*/
|
|
27
|
+
export function loadSkillsCatalog() {
|
|
28
|
+
return readJson(CATALOG_PATH)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* @param {SkillEntry} skill
|
|
33
|
+
*/
|
|
34
|
+
export function isTemplateSkill(skill) {
|
|
35
|
+
return !skill.install || skill.install === 'template'
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* @param {SkillEntry} skill
|
|
40
|
+
*/
|
|
41
|
+
export function isSkillsCliSkill(skill) {
|
|
42
|
+
return skill.install === 'skills-cli'
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* @param {SkillEntry[]} skills
|
|
47
|
+
*/
|
|
48
|
+
export function defaultSkillIds(skills) {
|
|
49
|
+
return skills.filter((s) => s.required || s.default).map((s) => s.id)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* @param {string | undefined} raw
|
|
54
|
+
* @param {SkillEntry[]} skills
|
|
55
|
+
* @returns {string[]}
|
|
56
|
+
*/
|
|
57
|
+
export function parseSkillsFlag(raw, skills) {
|
|
58
|
+
if (raw == null || raw === '') return defaultSkillIds(skills)
|
|
59
|
+
const known = new Map(skills.map((s) => [s.id, s]))
|
|
60
|
+
if (raw.trim() === 'all') return skills.map((s) => s.id)
|
|
61
|
+
|
|
62
|
+
const ids = raw
|
|
63
|
+
.split(',')
|
|
64
|
+
.map((part) => part.trim())
|
|
65
|
+
.filter(Boolean)
|
|
66
|
+
|
|
67
|
+
if (ids.length === 0) return defaultSkillIds(skills)
|
|
68
|
+
|
|
69
|
+
const missing = ids.filter((id) => !known.has(id))
|
|
70
|
+
if (missing.length) {
|
|
71
|
+
throw new Error(
|
|
72
|
+
`Unknown skill id(s): ${missing.join(', ')}. Available: ${skills.map((s) => s.id).join(', ')}, or all`,
|
|
73
|
+
)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const selected = new Set(ids)
|
|
77
|
+
for (const skill of skills) {
|
|
78
|
+
if (skill.required) selected.add(skill.id)
|
|
79
|
+
}
|
|
80
|
+
return skills.map((s) => s.id).filter((id) => selected.has(id))
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Template-relative paths for skills that ship inside the package template.
|
|
85
|
+
* @param {string[]} skillIds
|
|
86
|
+
* @param {SkillEntry[]} skills
|
|
87
|
+
* @returns {string[]}
|
|
88
|
+
*/
|
|
89
|
+
export function skillPathsForIds(skillIds, skills) {
|
|
90
|
+
const byId = new Map(skills.map((s) => [s.id, s]))
|
|
91
|
+
return skillIds.flatMap((id) => {
|
|
92
|
+
const entry = byId.get(id)
|
|
93
|
+
if (!entry) throw new Error(`Unknown skill id: ${id}`)
|
|
94
|
+
if (!isTemplateSkill(entry)) return []
|
|
95
|
+
return [entry.path]
|
|
96
|
+
})
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Build template include prefixes for the AI pack (first-party files only).
|
|
101
|
+
* @param {string[]} skillIds
|
|
102
|
+
* @param {SkillEntry[]} skills
|
|
103
|
+
*/
|
|
104
|
+
export function buildAiInclude(skillIds, skills) {
|
|
105
|
+
return [
|
|
106
|
+
'DESIGN.md',
|
|
107
|
+
...skillPathsForIds(skillIds, skills),
|
|
108
|
+
'.cursor/rules',
|
|
109
|
+
'scripts/check-raw-colors.mjs',
|
|
110
|
+
]
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Install selected companion skills via the skills CLI (always latest from source).
|
|
115
|
+
* @param {string} cwd
|
|
116
|
+
* @param {string[]} skillIds
|
|
117
|
+
* @param {SkillEntry[]} skills
|
|
118
|
+
* @param {{ dryRun?: boolean }} [options]
|
|
119
|
+
* @returns {{ installed: string[], commands: string[], dryRun?: boolean, skipped?: boolean }}
|
|
120
|
+
*/
|
|
121
|
+
export function installSkillsCli(cwd, skillIds, skills, { dryRun = false } = {}) {
|
|
122
|
+
const selected = skills.filter((s) => skillIds.includes(s.id) && isSkillsCliSkill(s))
|
|
123
|
+
if (!selected.length) {
|
|
124
|
+
return { installed: [], commands: [], skipped: true }
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
for (const entry of selected) {
|
|
128
|
+
if (!entry.source) {
|
|
129
|
+
throw new Error(`Skill "${entry.id}" is marked skills-cli but has no source`)
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** @type {Map<string, string[]>} */
|
|
134
|
+
const bySource = new Map()
|
|
135
|
+
for (const entry of selected) {
|
|
136
|
+
const name = entry.skill || entry.id
|
|
137
|
+
const list = bySource.get(entry.source) || []
|
|
138
|
+
list.push(name)
|
|
139
|
+
bySource.set(entry.source, list)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const commands = []
|
|
143
|
+
for (const [source, names] of bySource) {
|
|
144
|
+
const skillFlags = names.map((name) => `-s ${name}`).join(' ')
|
|
145
|
+
commands.push(`npx -y skills add ${source} ${skillFlags} -a cursor -y`)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (dryRun) {
|
|
149
|
+
return { installed: selected.map((s) => s.id), commands, dryRun: true }
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
console.log('Companion skills (latest via skills CLI):')
|
|
153
|
+
for (const command of commands) {
|
|
154
|
+
console.log(` $ ${command}`)
|
|
155
|
+
execSync(command, { cwd, stdio: 'inherit', shell: true })
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return { installed: selected.map((s) => s.id), commands }
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Prompt for optional skills when stdin is a TTY.
|
|
163
|
+
* Required skills are always included.
|
|
164
|
+
* @param {SkillEntry[]} skills
|
|
165
|
+
* @param {{ skipPrompt?: boolean }} [options]
|
|
166
|
+
* @returns {Promise<string[]>}
|
|
167
|
+
*/
|
|
168
|
+
export async function resolveSkillSelection(skills, { skipPrompt = false } = {}) {
|
|
169
|
+
const required = skills.filter((s) => s.required).map((s) => s.id)
|
|
170
|
+
const optional = skills.filter((s) => !s.required)
|
|
171
|
+
|
|
172
|
+
if (skipPrompt || optional.length === 0 || !process.stdin.isTTY) {
|
|
173
|
+
return defaultSkillIds(skills)
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
console.log('Select optional Agent skills (in addition to required):')
|
|
177
|
+
optional.forEach((skill, index) => {
|
|
178
|
+
const mark = skill.default ? 'x' : ' '
|
|
179
|
+
const via = isSkillsCliSkill(skill) ? ' [latest via skills CLI]' : ''
|
|
180
|
+
console.log(` ${index + 1}. [${mark}] ${skill.id} — ${skill.description}${via}`)
|
|
181
|
+
})
|
|
182
|
+
console.log('')
|
|
183
|
+
console.log('Enter comma-separated numbers or ids (empty = defaults only).')
|
|
184
|
+
console.log(`Required: ${required.join(', ') || '(none)'}`)
|
|
185
|
+
|
|
186
|
+
const answer = await question('> ')
|
|
187
|
+
const trimmed = answer.trim()
|
|
188
|
+
if (!trimmed) return defaultSkillIds(skills)
|
|
189
|
+
|
|
190
|
+
const selected = new Set(required)
|
|
191
|
+
const tokens = trimmed.split(/[\s,]+/).filter(Boolean)
|
|
192
|
+
for (const token of tokens) {
|
|
193
|
+
const asIndex = Number(token)
|
|
194
|
+
if (Number.isInteger(asIndex) && asIndex >= 1 && asIndex <= optional.length) {
|
|
195
|
+
selected.add(optional[asIndex - 1].id)
|
|
196
|
+
continue
|
|
197
|
+
}
|
|
198
|
+
if (token === 'all') {
|
|
199
|
+
for (const skill of optional) selected.add(skill.id)
|
|
200
|
+
continue
|
|
201
|
+
}
|
|
202
|
+
if (!skills.some((s) => s.id === token)) {
|
|
203
|
+
throw new Error(`Unknown skill selection: ${token}`)
|
|
204
|
+
}
|
|
205
|
+
selected.add(token)
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return skills.map((s) => s.id).filter((id) => selected.has(id))
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* @param {string} prompt
|
|
213
|
+
* @returns {Promise<string>}
|
|
214
|
+
*/
|
|
215
|
+
function question(prompt) {
|
|
216
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout })
|
|
217
|
+
return new Promise((resolve) => {
|
|
218
|
+
rl.question(prompt, (answer) => {
|
|
219
|
+
rl.close()
|
|
220
|
+
resolve(answer)
|
|
221
|
+
})
|
|
222
|
+
})
|
|
223
|
+
}
|