skill-linker 3.0.8 → 4.0.4
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 +74 -52
- package/package.json +3 -4
- package/src/cli.js +42 -30
- package/src/commands/install.js +178 -322
- package/src/commands/list.js +131 -54
- package/src/utils/agents.js +83 -58
- package/src/utils/git.js +68 -64
package/src/commands/install.js
CHANGED
|
@@ -1,347 +1,203 @@
|
|
|
1
|
-
const
|
|
2
|
-
const
|
|
3
|
-
const
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
1
|
+
const chalk = require("chalk");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
const {
|
|
4
|
+
dirExists,
|
|
5
|
+
ensureDir,
|
|
6
|
+
createSymlink,
|
|
7
|
+
listDirectories,
|
|
8
|
+
} = require("../utils/file-system");
|
|
9
|
+
const {
|
|
10
|
+
getAllAgents,
|
|
11
|
+
detectInstalledAgents,
|
|
12
|
+
findAgentIndex,
|
|
13
|
+
} = require("../utils/agents");
|
|
14
|
+
const { cloneOrUpdateRepo, pullRepo } = require("../utils/git");
|
|
7
15
|
|
|
8
16
|
/**
|
|
9
|
-
* Main install command
|
|
17
|
+
* Main install command (CLI mode only)
|
|
10
18
|
* @param {Object} options - Command options
|
|
19
|
+
* @param {string} options.skill - Skill path or --from clone URL
|
|
20
|
+
* @param {string} [options.from] - GitHub URL to clone from
|
|
21
|
+
* @param {string[]} [options.agents] - Agent names to install to
|
|
22
|
+
* @param {string} [options.scope] - Scope: project, global, or both
|
|
23
|
+
* @param {boolean} [options.yes] - Auto-overwrite existing links
|
|
11
24
|
*/
|
|
12
25
|
async function install(options) {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
const subSkills = listDirectories(path.join(targetPath, 'skills'));
|
|
39
|
-
|
|
40
|
-
if (subSkills.length > 0) {
|
|
41
|
-
const { selectedSkills } = await prompts({
|
|
42
|
-
type: 'multiselect',
|
|
43
|
-
name: 'selectedSkills',
|
|
44
|
-
message: 'This repo contains multiple skills. Select skills to install:',
|
|
45
|
-
choices: [
|
|
46
|
-
...subSkills.map(s => ({ title: s, value: path.join(targetPath, 'skills', s) })),
|
|
47
|
-
{ title: 'Link entire repo', value: targetPath }
|
|
48
|
-
],
|
|
49
|
-
hint: '- Space to select. Return to submit'
|
|
50
|
-
});
|
|
26
|
+
let skillPaths = [];
|
|
27
|
+
|
|
28
|
+
// Handle --from flag: Clone from GitHub
|
|
29
|
+
if (options.from) {
|
|
30
|
+
console.log(chalk.blue("[INFO]"), `Cloning from ${options.from}...`);
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
const {
|
|
34
|
+
skillPath: clonedPath,
|
|
35
|
+
targetPath,
|
|
36
|
+
needsUpdate,
|
|
37
|
+
hasSubpath,
|
|
38
|
+
} = await cloneOrUpdateRepo(options.from);
|
|
39
|
+
|
|
40
|
+
if (needsUpdate) {
|
|
41
|
+
if (options.yes) {
|
|
42
|
+
await pullRepo(targetPath);
|
|
43
|
+
console.log(chalk.green("[SUCCESS]"), "Repository updated!");
|
|
44
|
+
} else {
|
|
45
|
+
console.log(
|
|
46
|
+
chalk.yellow("[WARNING]"),
|
|
47
|
+
"Repository already exists. Use --yes to update.",
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
51
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
// If nothing selected, maybe they just hit enter without selection? Default to repo path?
|
|
56
|
-
// Or better, error out if multiselect returns empty.
|
|
57
|
-
// Let's assume empty selection means exit or user made mistake.
|
|
58
|
-
// But to be safe, if they chose "Link entire repo" in multiselect (which is weird), handle it.
|
|
59
|
-
// Actually multiselect is better for picking subsets.
|
|
60
|
-
// If empty, let's fall back to entire repo (or maybe error).
|
|
61
|
-
// Let's error to be consistent with agent selection.
|
|
62
|
-
}
|
|
63
|
-
} else {
|
|
64
|
-
skillPaths = [targetPath];
|
|
65
|
-
}
|
|
66
|
-
} else {
|
|
67
|
-
skillPaths = [clonedPath];
|
|
68
|
-
}
|
|
52
|
+
// If no subpath, check for skills/ subdirectory
|
|
53
|
+
if (!hasSubpath && dirExists(path.join(targetPath, "skills"))) {
|
|
54
|
+
const subSkills = listDirectories(path.join(targetPath, "skills"));
|
|
69
55
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
56
|
+
if (subSkills.length > 0) {
|
|
57
|
+
// Install all skills from skills/ directory
|
|
58
|
+
skillPaths = subSkills.map((s) => path.join(targetPath, "skills", s));
|
|
59
|
+
} else {
|
|
60
|
+
skillPaths = [targetPath];
|
|
74
61
|
}
|
|
62
|
+
} else {
|
|
63
|
+
skillPaths = [clonedPath];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
console.log(chalk.green("[SUCCESS]"), "Clone completed!");
|
|
67
|
+
} catch (error) {
|
|
68
|
+
console.error(chalk.red("[ERROR]"), error.message);
|
|
69
|
+
process.exit(1);
|
|
75
70
|
}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// If no skill path provided via --from, use --skill
|
|
74
|
+
if (skillPaths.length === 0 && options.skill) {
|
|
75
|
+
skillPaths = [options.skill];
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Validate skill paths
|
|
79
|
+
for (const p of skillPaths) {
|
|
80
|
+
if (!dirExists(p)) {
|
|
81
|
+
console.error(chalk.red("[ERROR]"), `Skill directory not found: ${p}`);
|
|
82
|
+
process.exit(1);
|
|
80
83
|
}
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (skillPaths.length > 1) {
|
|
87
|
+
console.log(chalk.blue("[INFO]"), `Selected ${skillPaths.length} skills`);
|
|
88
|
+
} else {
|
|
89
|
+
const skillName = path.basename(skillPaths[0]);
|
|
90
|
+
console.log(
|
|
91
|
+
chalk.blue("[INFO]"),
|
|
92
|
+
`Selected Skill: ${chalk.cyan(skillName)} (${skillPaths[0]})`,
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Agent selection
|
|
97
|
+
const agents = getAllAgents();
|
|
98
|
+
const installedIndices = detectInstalledAgents();
|
|
99
|
+
|
|
100
|
+
let selectedAgents = [];
|
|
101
|
+
|
|
102
|
+
// Use provided agents list
|
|
103
|
+
if (options.agents && options.agents.length > 0) {
|
|
104
|
+
selectedAgents = options.agents
|
|
105
|
+
.map((agentName) => {
|
|
106
|
+
const idx = findAgentIndex(agentName);
|
|
107
|
+
if (idx === null) {
|
|
108
|
+
console.log(
|
|
109
|
+
chalk.yellow("[WARNING]"),
|
|
110
|
+
`Unknown agent: ${agentName}, skipping...`,
|
|
111
|
+
);
|
|
112
|
+
return null;
|
|
104
113
|
}
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
const { githubUrl } = await prompts({
|
|
109
|
-
type: 'text',
|
|
110
|
-
name: 'githubUrl',
|
|
111
|
-
message: 'Enter GitHub URL:',
|
|
112
|
-
validate: value => value.trim() !== '' || 'Please enter a valid GitHub URL'
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
if (!githubUrl) {
|
|
116
|
-
console.log(chalk.yellow('[WARNING]'), 'No URL provided. Exiting.');
|
|
117
|
-
process.exit(0);
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
// Use the same logic as --from flag
|
|
121
|
-
console.log(chalk.blue('[INFO]'), `Cloning from ${githubUrl}...`);
|
|
122
|
-
|
|
123
|
-
try {
|
|
124
|
-
const { skillPath: clonedPath, targetPath, needsUpdate, hasSubpath } = await cloneOrUpdateRepo(githubUrl);
|
|
125
|
-
|
|
126
|
-
if (needsUpdate) {
|
|
127
|
-
const { shouldUpdate } = await prompts({
|
|
128
|
-
type: 'confirm',
|
|
129
|
-
name: 'shouldUpdate',
|
|
130
|
-
message: `Repository already exists. Update with git pull?`,
|
|
131
|
-
initial: false
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
if (shouldUpdate) {
|
|
135
|
-
await pullRepo(targetPath);
|
|
136
|
-
console.log(chalk.green('[SUCCESS]'), 'Repository updated!');
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
// If no subpath, check for skills/ subdirectory
|
|
141
|
-
if (!hasSubpath && dirExists(path.join(targetPath, 'skills'))) {
|
|
142
|
-
const subSkills = listDirectories(path.join(targetPath, 'skills'));
|
|
143
|
-
|
|
144
|
-
if (subSkills.length > 0) {
|
|
145
|
-
const { selectedSkills } = await prompts({
|
|
146
|
-
type: 'multiselect',
|
|
147
|
-
name: 'selectedSkills',
|
|
148
|
-
message: 'This repo contains multiple skills. Select skills to install:',
|
|
149
|
-
choices: [
|
|
150
|
-
...subSkills.map(s => ({ title: s, value: path.join(targetPath, 'skills', s) })),
|
|
151
|
-
{ title: 'Link entire repo', value: targetPath }
|
|
152
|
-
],
|
|
153
|
-
hint: '- Space to select. Return to submit'
|
|
154
|
-
});
|
|
155
|
-
|
|
156
|
-
if (selectedSkills && selectedSkills.length > 0) {
|
|
157
|
-
skillPaths = selectedSkills;
|
|
158
|
-
}
|
|
159
|
-
} else {
|
|
160
|
-
skillPaths = [targetPath];
|
|
161
|
-
}
|
|
162
|
-
} else {
|
|
163
|
-
skillPaths = [clonedPath];
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
console.log(chalk.green('[SUCCESS]'), 'Clone completed!');
|
|
167
|
-
} catch (error) {
|
|
168
|
-
console.error(chalk.red('[ERROR]'), error.message);
|
|
169
|
-
process.exit(1);
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
// Handle local library source
|
|
174
|
-
if (source === 'local') {
|
|
175
|
-
const repos = findRepos(DEFAULT_LIB_PATH);
|
|
176
|
-
|
|
177
|
-
if (repos.length === 0) {
|
|
178
|
-
console.error(chalk.red('[ERROR]'), `No repos found in ${DEFAULT_LIB_PATH}`);
|
|
179
|
-
console.log(chalk.blue('[INFO]'), 'Use --from <github_url> to clone skills first.');
|
|
180
|
-
process.exit(1);
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
console.log('');
|
|
184
|
-
|
|
185
|
-
// 1. Select Repository
|
|
186
|
-
const { selectedRepo } = await prompts({
|
|
187
|
-
type: 'autocomplete',
|
|
188
|
-
name: 'selectedRepo',
|
|
189
|
-
message: 'Select a repository:',
|
|
190
|
-
choices: repos.map(repo => ({
|
|
191
|
-
title: `${repo.name}${repo.hasSkillsDir ? chalk.dim(' (has skills/)') : ''}`,
|
|
192
|
-
value: repo
|
|
193
|
-
})),
|
|
194
|
-
suggest: (input, choices) => {
|
|
195
|
-
const inputLower = input.toLowerCase();
|
|
196
|
-
return Promise.resolve(
|
|
197
|
-
choices.filter(choice => choice.title.toLowerCase().includes(inputLower))
|
|
198
|
-
);
|
|
199
|
-
}
|
|
200
|
-
});
|
|
201
|
-
|
|
202
|
-
if (!selectedRepo) {
|
|
203
|
-
console.log(chalk.yellow('[WARNING]'), 'No repository selected. Exiting.');
|
|
204
|
-
process.exit(0);
|
|
205
|
-
}
|
|
114
|
+
return idx;
|
|
115
|
+
})
|
|
116
|
+
.filter((idx) => idx !== null);
|
|
206
117
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
const subSkills = listDirectories(skillsDir);
|
|
211
|
-
|
|
212
|
-
if (subSkills.length > 0) {
|
|
213
|
-
const { selectedSubSkills } = await prompts({
|
|
214
|
-
type: 'multiselect',
|
|
215
|
-
name: 'selectedSubSkills',
|
|
216
|
-
message: `Select skills from ${chalk.cyan(selectedRepo.name)} (Space to select):`,
|
|
217
|
-
choices: [
|
|
218
|
-
...subSkills.map(s => ({ title: s, value: path.join(skillsDir, s) })),
|
|
219
|
-
{ title: 'Link entire repo', value: selectedRepo.path }
|
|
220
|
-
],
|
|
221
|
-
hint: '- Space to select. Return to submit'
|
|
222
|
-
});
|
|
223
|
-
|
|
224
|
-
if (!selectedSubSkills || selectedSubSkills.length === 0) {
|
|
225
|
-
console.log(chalk.yellow('[WARNING]'), 'No skills selected. Exiting.');
|
|
226
|
-
process.exit(0);
|
|
227
|
-
}
|
|
228
|
-
skillPaths = selectedSubSkills;
|
|
229
|
-
} else {
|
|
230
|
-
skillPaths = [selectedRepo.path];
|
|
231
|
-
}
|
|
232
|
-
} else {
|
|
233
|
-
skillPaths = [selectedRepo.path];
|
|
234
|
-
}
|
|
235
|
-
}
|
|
118
|
+
if (selectedAgents.length === 0) {
|
|
119
|
+
console.error(chalk.red("[ERROR]"), "No valid agents specified");
|
|
120
|
+
process.exit(1);
|
|
236
121
|
}
|
|
237
|
-
|
|
238
|
-
//
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
122
|
+
} else {
|
|
123
|
+
// Use all installed agents
|
|
124
|
+
selectedAgents = installedIndices;
|
|
125
|
+
if (selectedAgents.length === 0) {
|
|
126
|
+
console.error(
|
|
127
|
+
chalk.red("[ERROR]"),
|
|
128
|
+
"No installed agents detected. Please specify --agent.",
|
|
129
|
+
);
|
|
130
|
+
process.exit(1);
|
|
244
131
|
}
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
console.log(
|
|
135
|
+
chalk.blue("[INFO]"),
|
|
136
|
+
`Installing to ${selectedAgents.length} agent(s): ${selectedAgents.map((i) => agents[i].name).join(", ")}`,
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
// Determine scope
|
|
140
|
+
let scope = options.scope ? options.scope.toLowerCase() : "both";
|
|
141
|
+
if (!["project", "global", "both"].includes(scope)) {
|
|
142
|
+
console.error(
|
|
143
|
+
chalk.red("[ERROR]"),
|
|
144
|
+
`Invalid scope: ${scope}. Use: project, global, or both`,
|
|
145
|
+
);
|
|
146
|
+
process.exit(1);
|
|
147
|
+
}
|
|
148
|
+
console.log(chalk.blue("[INFO]"), `Scope: ${scope}`);
|
|
149
|
+
|
|
150
|
+
// Process each selected agent
|
|
151
|
+
for (const agentIndex of selectedAgents) {
|
|
152
|
+
const agent = agents[agentIndex];
|
|
153
|
+
|
|
154
|
+
console.log("");
|
|
155
|
+
console.log(
|
|
156
|
+
chalk.blue("[INFO]"),
|
|
157
|
+
`Configuring for ${chalk.cyan(agent.name)}...`,
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
const targets = [];
|
|
161
|
+
if (scope === "project" || scope === "both") {
|
|
162
|
+
targets.push(path.join(process.cwd(), agent.projectDir));
|
|
251
163
|
}
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
const agents = getAllAgents();
|
|
255
|
-
const installedIndices = detectInstalledAgents();
|
|
256
|
-
|
|
257
|
-
const { selectedAgents } = await prompts({
|
|
258
|
-
type: 'multiselect',
|
|
259
|
-
name: 'selectedAgents',
|
|
260
|
-
message: 'Select agents to install to (Space to select, Enter to confirm):',
|
|
261
|
-
choices: agents.map((agent, index) => ({
|
|
262
|
-
title: agent.name + (installedIndices.includes(index) ? chalk.green(' (Installed)') : ''),
|
|
263
|
-
value: index,
|
|
264
|
-
selected: installedIndices.includes(index)
|
|
265
|
-
})),
|
|
266
|
-
hint: '- Space to select. Return to submit'
|
|
267
|
-
});
|
|
268
|
-
|
|
269
|
-
if (!selectedAgents || selectedAgents.length === 0) {
|
|
270
|
-
console.log(chalk.yellow('[WARNING]'), 'No agents selected. Exiting.');
|
|
271
|
-
process.exit(0);
|
|
164
|
+
if (scope === "global" || scope === "both") {
|
|
165
|
+
targets.push(agent.globalDir);
|
|
272
166
|
}
|
|
273
167
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
const targets = [];
|
|
296
|
-
if (scope === 'project' || scope === 'both') {
|
|
297
|
-
targets.push(path.join(process.cwd(), agent.projectDir));
|
|
298
|
-
}
|
|
299
|
-
if (scope === 'global' || scope === 'both') {
|
|
300
|
-
targets.push(agent.globalDir);
|
|
168
|
+
for (const targetBase of targets) {
|
|
169
|
+
ensureDir(targetBase);
|
|
170
|
+
|
|
171
|
+
// Loop through all selected skills and link them
|
|
172
|
+
for (const sPath of skillPaths) {
|
|
173
|
+
const sName = path.basename(sPath);
|
|
174
|
+
const targetLink = path.join(targetBase, sName);
|
|
175
|
+
|
|
176
|
+
if (dirExists(targetLink)) {
|
|
177
|
+
if (!options.yes) {
|
|
178
|
+
console.log(
|
|
179
|
+
chalk.yellow("[WARNING]"),
|
|
180
|
+
`Already exists: ${targetLink}. Use --yes to overwrite.`,
|
|
181
|
+
);
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
console.log(
|
|
185
|
+
chalk.blue("[INFO]"),
|
|
186
|
+
`Overwriting existing: ${targetLink}`,
|
|
187
|
+
);
|
|
301
188
|
}
|
|
302
189
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
for (const sPath of skillPaths) {
|
|
308
|
-
const sName = path.basename(sPath);
|
|
309
|
-
const targetLink = path.join(targetBase, sName);
|
|
310
|
-
|
|
311
|
-
if (dirExists(targetLink)) {
|
|
312
|
-
// Check if already correct link to avoid prompt
|
|
313
|
-
// But for simplicity, we prompt or skip.
|
|
314
|
-
// To avoid spamming prompts for multiple skills, maybe auto-overwrite or ask once?
|
|
315
|
-
// Let's ask individually for safety for now, or maybe just log and skip if overwrite not confirmed.
|
|
316
|
-
// Actually, prompting for every file in a loop is annoying.
|
|
317
|
-
// Let's check overlap first? Or just try createSymlink which handles unlink.
|
|
318
|
-
|
|
319
|
-
// Let's prompt once per agent/target if any conflicts? Too complex.
|
|
320
|
-
// Simple approach: Prompt for each conflict.
|
|
321
|
-
const { overwrite } = await prompts({
|
|
322
|
-
type: 'confirm',
|
|
323
|
-
name: 'overwrite',
|
|
324
|
-
message: `${targetLink} already exists. Overwrite?`,
|
|
325
|
-
initial: false
|
|
326
|
-
});
|
|
327
|
-
|
|
328
|
-
if (!overwrite) {
|
|
329
|
-
console.log(chalk.blue('[INFO]'), `Skipping ${sName}...`);
|
|
330
|
-
continue;
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
if (createSymlink(sPath, targetLink)) {
|
|
335
|
-
console.log(chalk.green('[SUCCESS]'), `Linked ${sName}`);
|
|
336
|
-
} else {
|
|
337
|
-
console.error(chalk.red('[ERROR]'), `Failed to link ${sName}`);
|
|
338
|
-
}
|
|
339
|
-
}
|
|
190
|
+
if (createSymlink(sPath, targetLink)) {
|
|
191
|
+
console.log(chalk.green("[SUCCESS]"), `Linked ${sName}`);
|
|
192
|
+
} else {
|
|
193
|
+
console.error(chalk.red("[ERROR]"), `Failed to link ${sName}`);
|
|
340
194
|
}
|
|
195
|
+
}
|
|
341
196
|
}
|
|
197
|
+
}
|
|
342
198
|
|
|
343
|
-
|
|
344
|
-
|
|
199
|
+
console.log("");
|
|
200
|
+
console.log(chalk.green("[SUCCESS]"), "All operations completed.");
|
|
345
201
|
}
|
|
346
202
|
|
|
347
203
|
module.exports = install;
|