@vibe-agent-toolkit/cli 0.1.15 → 0.1.16-rc.2
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/dist/commands/claude/build.d.ts +4 -4
- package/dist/commands/claude/build.d.ts.map +1 -1
- package/dist/commands/claude/build.js +87 -122
- package/dist/commands/claude/build.js.map +1 -1
- package/dist/commands/claude/verify.d.ts +9 -3
- package/dist/commands/claude/verify.d.ts.map +1 -1
- package/dist/commands/claude/verify.js +60 -52
- package/dist/commands/claude/verify.js.map +1 -1
- package/dist/commands/skills/build.d.ts +4 -1
- package/dist/commands/skills/build.d.ts.map +1 -1
- package/dist/commands/skills/build.js +120 -101
- package/dist/commands/skills/build.js.map +1 -1
- package/dist/commands/skills/command-helpers.d.ts +12 -4
- package/dist/commands/skills/command-helpers.d.ts.map +1 -1
- package/dist/commands/skills/command-helpers.js +4 -4
- package/dist/commands/skills/command-helpers.js.map +1 -1
- package/dist/commands/skills/install-helpers.d.ts +2 -3
- package/dist/commands/skills/install-helpers.d.ts.map +1 -1
- package/dist/commands/skills/install-helpers.js.map +1 -1
- package/dist/commands/skills/install.d.ts +6 -1
- package/dist/commands/skills/install.d.ts.map +1 -1
- package/dist/commands/skills/install.js +182 -187
- package/dist/commands/skills/install.js.map +1 -1
- package/dist/commands/skills/shared.d.ts +10 -4
- package/dist/commands/skills/shared.d.ts.map +1 -1
- package/dist/commands/skills/shared.js +11 -9
- package/dist/commands/skills/shared.js.map +1 -1
- package/dist/commands/skills/skill-discovery.d.ts +20 -0
- package/dist/commands/skills/skill-discovery.d.ts.map +1 -0
- package/dist/commands/skills/skill-discovery.js +71 -0
- package/dist/commands/skills/skill-discovery.js.map +1 -0
- package/dist/commands/skills/uninstall.js +1 -1
- package/dist/commands/skills/uninstall.js.map +1 -1
- package/dist/commands/skills/validate-command.js +2 -2
- package/dist/commands/skills/validate-command.js.map +1 -1
- package/dist/commands/skills/validate.d.ts +2 -2
- package/dist/commands/skills/validate.d.ts.map +1 -1
- package/dist/commands/skills/validate.js +48 -66
- package/dist/commands/skills/validate.js.map +1 -1
- package/package.json +11 -11
|
@@ -1,13 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Install
|
|
2
|
+
* Install skills to Claude's plugins directory
|
|
3
3
|
*
|
|
4
4
|
* Supports installing from:
|
|
5
5
|
* - npm packages (npm:@scope/package)
|
|
6
6
|
* - Local ZIP file
|
|
7
7
|
* - Local directory
|
|
8
8
|
* - npm postinstall hook (--npm-postinstall)
|
|
9
|
+
*
|
|
10
|
+
* Plugin detection: looks for dist/.claude/plugins/marketplaces/ directory.
|
|
11
|
+
* When present, copies the pre-built directory tree to ~/.claude/plugins/
|
|
12
|
+
* and updates Claude's plugin registry files. When absent, falls back to
|
|
13
|
+
* copying dist/skills/ to ~/.claude/skills/.
|
|
9
14
|
*/
|
|
10
|
-
import { existsSync, lstatSync, readFileSync } from 'node:fs';
|
|
15
|
+
import { existsSync, lstatSync, readdirSync, readFileSync } from 'node:fs';
|
|
11
16
|
import { cp, mkdir, mkdtemp, rm, symlink } from 'node:fs/promises';
|
|
12
17
|
import { basename, join, resolve } from 'node:path';
|
|
13
18
|
import { getClaudeUserPaths, installPlugin } from '@vibe-agent-toolkit/claude-marketplace';
|
|
@@ -17,44 +22,61 @@ import { Command } from 'commander';
|
|
|
17
22
|
import { handleCommandError } from '../../utils/command-error.js';
|
|
18
23
|
import { createLogger } from '../../utils/logger.js';
|
|
19
24
|
import { detectSource, downloadNpmPackage, isGlobalNpmInstall, readPackageJsonVatMetadata, writeYamlHeader, } from './install-helpers.js';
|
|
20
|
-
/** Relative path within a VAT npm package to its
|
|
21
|
-
const
|
|
25
|
+
/** Relative path within a VAT npm package to its pre-built plugin structure. */
|
|
26
|
+
const PLUGIN_MARKETPLACES_SUBPATH = join('dist', '.claude', 'plugins', 'marketplaces');
|
|
22
27
|
const PACKAGE_JSON = 'package.json';
|
|
28
|
+
/**
|
|
29
|
+
* Install from a pre-built plugin tree: read package.json, copy tree, output success.
|
|
30
|
+
* Shared between npm and local install paths to avoid duplication.
|
|
31
|
+
*/
|
|
32
|
+
async function installPluginTreeAndExit(rootDir, marketplacesDir, sourceLabel, sourceType, startTime, logger, dryRun) {
|
|
33
|
+
// eslint-disable-next-line security/detect-non-literal-fs-filename -- rootDir from controlled source
|
|
34
|
+
const pkgRaw = readFileSync(join(rootDir, PACKAGE_JSON), 'utf-8');
|
|
35
|
+
const packageJson = JSON.parse(pkgRaw);
|
|
36
|
+
await copyPluginTree(marketplacesDir, packageJson, logger);
|
|
37
|
+
const duration = Date.now() - startTime;
|
|
38
|
+
outputInstallSuccess([], sourceLabel, sourceType, duration, logger, dryRun);
|
|
39
|
+
process.exit(0);
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Convert a skill name to a filesystem-safe path segment.
|
|
43
|
+
* Colons in colon-namespaced names (e.g. "pkg:sub") become "__".
|
|
44
|
+
*/
|
|
45
|
+
function skillNameToFsPath(name) {
|
|
46
|
+
return name.replaceAll(':', '__');
|
|
47
|
+
}
|
|
23
48
|
export function createInstallCommand() {
|
|
24
49
|
const command = new Command('install');
|
|
25
50
|
command
|
|
26
|
-
.description('Install
|
|
51
|
+
.description('Install skills to Claude Code plugins directory')
|
|
27
52
|
.argument('[source]', 'Source to install from (npm:package, ZIP file, or directory path)')
|
|
28
53
|
.option('-s, --skills-dir <path>', 'Claude skills directory', getClaudeUserPaths().skillsDir)
|
|
29
54
|
.option('-n, --name <name>', 'Custom name for installed skill (default: auto-detect from source)')
|
|
30
55
|
.option('-f, --force', 'Overwrite existing skill if present', false)
|
|
31
56
|
.option('--dry-run', 'Preview installation without creating files', false)
|
|
32
57
|
.option('--npm-postinstall', 'Run as npm postinstall hook (internal use)', false)
|
|
33
|
-
.option('-d, --dev', 'Development mode: symlink skills from
|
|
58
|
+
.option('-d, --dev', 'Development mode: symlink skills from dist/skills/ (rebuilds reflected immediately)')
|
|
34
59
|
.option('--build', 'Build skills before installing (implies --dev)')
|
|
35
|
-
.option('--user-install-without-plugin', 'Force skills-only install (skip plugin registry even if claude
|
|
60
|
+
.option('--user-install-without-plugin', 'Force skills-only install (skip plugin registry even if dist/.claude/ exists)', false)
|
|
36
61
|
.option('--debug', 'Enable debug logging')
|
|
37
62
|
.action(installCommand)
|
|
38
63
|
.addHelpText('after', `
|
|
39
64
|
Description:
|
|
40
|
-
Installs skills to Claude Code's
|
|
65
|
+
Installs skills to Claude Code's plugins directory from various sources.
|
|
66
|
+
|
|
67
|
+
Plugin detection: If the package contains dist/.claude/plugins/marketplaces/,
|
|
68
|
+
the pre-built directory tree is copied to ~/.claude/plugins/ (dumb copy).
|
|
69
|
+
Otherwise, falls back to copying dist/skills/ to ~/.claude/skills/.
|
|
41
70
|
|
|
42
71
|
Supported sources:
|
|
43
72
|
- npm package: npm:@scope/package-name
|
|
44
73
|
- Local ZIP file: ./path/to/skill.zip
|
|
45
74
|
- Local directory: ./path/to/skill-dir
|
|
46
75
|
- npm postinstall: --npm-postinstall (automatic during global install)
|
|
47
|
-
- Dev mode: --dev (symlinks from
|
|
48
|
-
|
|
49
|
-
Default skills directory: ~/.claude/skills/
|
|
50
|
-
|
|
51
|
-
Dev mode creates symlinks so rebuilds are immediately reflected.
|
|
52
|
-
Use --build to auto-build before symlinking.
|
|
76
|
+
- Dev mode: --dev (symlinks from dist/skills/)
|
|
53
77
|
|
|
54
78
|
Output:
|
|
55
79
|
- status: success/error
|
|
56
|
-
- skillName: Name of installed skill
|
|
57
|
-
- installPath: Where the skill was installed
|
|
58
80
|
- source: Original source
|
|
59
81
|
- sourceType: npm/local/zip/npm-postinstall/dev
|
|
60
82
|
|
|
@@ -66,9 +88,7 @@ Exit Codes:
|
|
|
66
88
|
Example:
|
|
67
89
|
$ vat skills install --dev # Symlink all skills from cwd
|
|
68
90
|
$ vat skills install --build # Build + symlink
|
|
69
|
-
$ vat skills install --dev --name my-skill # Symlink specific skill
|
|
70
91
|
$ vat skills install npm:@scope/package # Install from npm
|
|
71
|
-
$ vat skills install ./my-skill.zip --force # Install from ZIP
|
|
72
92
|
`);
|
|
73
93
|
return command;
|
|
74
94
|
}
|
|
@@ -112,11 +132,9 @@ async function installCommand(source, options) {
|
|
|
112
132
|
break;
|
|
113
133
|
}
|
|
114
134
|
case 'npm-postinstall': {
|
|
115
|
-
// This case is handled by --npm-postinstall flag, not by source detection
|
|
116
135
|
throw new Error('npm-postinstall source type should be handled by --npm-postinstall flag');
|
|
117
136
|
}
|
|
118
137
|
case 'dev': {
|
|
119
|
-
// This case is handled by --dev flag, not by source detection
|
|
120
138
|
throw new Error('dev source type should be handled by --dev flag');
|
|
121
139
|
}
|
|
122
140
|
}
|
|
@@ -127,7 +145,6 @@ async function installCommand(source, options) {
|
|
|
127
145
|
}
|
|
128
146
|
/**
|
|
129
147
|
* Handle npm package installation
|
|
130
|
-
* Installs ALL skills from the package (or filtered by --name)
|
|
131
148
|
*/
|
|
132
149
|
async function handleNpmInstall(source, options, logger, startTime) {
|
|
133
150
|
const packageName = source.startsWith('npm:') ? source.slice(4) : source;
|
|
@@ -138,80 +155,73 @@ async function handleNpmInstall(source, options, logger, startTime) {
|
|
|
138
155
|
// Download and extract npm package
|
|
139
156
|
logger.info(' Downloading package...');
|
|
140
157
|
const extractedPath = downloadNpmPackage(packageName, tempDir);
|
|
141
|
-
// If the package ships a plugin, install via
|
|
142
|
-
|
|
143
|
-
// explicitly opts out with --user-install-without-plugin.
|
|
144
|
-
const marketplaceJsonPath = join(extractedPath, MARKETPLACE_JSON_SUBPATH);
|
|
158
|
+
// If the package ships a pre-built plugin tree, install via dumb copy.
|
|
159
|
+
const marketplacesDir = join(extractedPath, PLUGIN_MARKETPLACES_SUBPATH);
|
|
145
160
|
// eslint-disable-next-line security/detect-non-literal-fs-filename -- path from controlled extractedPath + constant subpath
|
|
146
|
-
const hasPlugin = !options.userInstallWithoutPlugin && existsSync(
|
|
161
|
+
const hasPlugin = !options.userInstallWithoutPlugin && existsSync(marketplacesDir);
|
|
147
162
|
if (hasPlugin) {
|
|
148
|
-
logger.info(' Plugin detected — installing via Claude plugin system
|
|
149
|
-
|
|
150
|
-
const pkgRaw = readFileSync(join(extractedPath, PACKAGE_JSON), 'utf-8');
|
|
151
|
-
const packageJson = JSON.parse(pkgRaw);
|
|
152
|
-
await tryInstallPluginRegistry(extractedPath, packageJson, logger);
|
|
153
|
-
const duration = Date.now() - startTime;
|
|
154
|
-
outputInstallSuccess([], `npm:${packageName}`, 'npm', duration, logger, options.dryRun);
|
|
155
|
-
process.exit(0);
|
|
163
|
+
logger.info(' Plugin detected — installing via Claude plugin system');
|
|
164
|
+
await installPluginTreeAndExit(extractedPath, marketplacesDir, `npm:${packageName}`, 'npm', startTime, logger, options.dryRun);
|
|
156
165
|
}
|
|
157
|
-
// No plugin — install skills directly to ~/.claude/skills/
|
|
166
|
+
// No plugin tree — install skills directly to ~/.claude/skills/
|
|
158
167
|
const { skills } = await readPackageJsonVatMetadata(extractedPath);
|
|
159
168
|
if (skills.length === 0) {
|
|
160
169
|
throw new Error(`No skills found in package ${packageName}`);
|
|
161
170
|
}
|
|
162
|
-
// Filter by --name if specified
|
|
163
|
-
const
|
|
164
|
-
? skills.filter(s => s
|
|
171
|
+
// Filter by --name if specified
|
|
172
|
+
const skillNames = options.name
|
|
173
|
+
? skills.filter(s => s === options.name)
|
|
165
174
|
: skills;
|
|
166
|
-
if (
|
|
175
|
+
if (skillNames.length === 0) {
|
|
167
176
|
throw new Error(`Skill "${options.name ?? ''}" not found in package ${packageName}. ` +
|
|
168
|
-
`Available: ${skills.
|
|
177
|
+
`Available: ${skills.join(', ')}`);
|
|
169
178
|
}
|
|
170
179
|
const skillsDir = options.skillsDir ?? getClaudeUserPaths().skillsDir;
|
|
171
|
-
for (const
|
|
172
|
-
const skillPath =
|
|
173
|
-
await installSkillFromPath(skillPath,
|
|
180
|
+
for (const skillName of skillNames) {
|
|
181
|
+
const skillPath = join(extractedPath, 'dist', 'skills', skillNameToFsPath(skillName));
|
|
182
|
+
await installSkillFromPath(skillPath, skillName, options, logger);
|
|
174
183
|
}
|
|
175
184
|
const duration = Date.now() - startTime;
|
|
176
|
-
outputInstallSuccess(
|
|
177
|
-
name
|
|
178
|
-
installPath: join(skillsDir,
|
|
185
|
+
outputInstallSuccess(skillNames.map(name => ({
|
|
186
|
+
name,
|
|
187
|
+
installPath: join(skillsDir, name),
|
|
179
188
|
})), `npm:${packageName}`, 'npm', duration, logger, options.dryRun);
|
|
180
189
|
process.exit(0);
|
|
181
190
|
}
|
|
182
191
|
finally {
|
|
183
|
-
// Cleanup temp directory
|
|
184
192
|
await rm(tempDir, { recursive: true, force: true });
|
|
185
193
|
}
|
|
186
194
|
}
|
|
187
195
|
/**
|
|
188
196
|
* Handle local directory installation
|
|
189
|
-
* If directory has package.json with vat.skills, installs ALL skills (or filtered by --name)
|
|
190
197
|
*/
|
|
191
198
|
async function handleLocalInstall(source, options, logger, startTime) {
|
|
192
199
|
const sourcePath = resolve(source);
|
|
193
200
|
logger.info(`📥 Installing skill from directory: ${sourcePath}`);
|
|
201
|
+
const skillsDir = options.skillsDir ?? getClaudeUserPaths().skillsDir;
|
|
202
|
+
// Check for pre-built plugin tree first
|
|
203
|
+
const marketplacesDir = join(sourcePath, PLUGIN_MARKETPLACES_SUBPATH);
|
|
204
|
+
// eslint-disable-next-line security/detect-non-literal-fs-filename -- User-provided CLI argument
|
|
205
|
+
if (!options.userInstallWithoutPlugin && existsSync(marketplacesDir)) {
|
|
206
|
+
await installPluginTreeAndExit(sourcePath, marketplacesDir, `local:${sourcePath}`, 'local', startTime, logger, options.dryRun);
|
|
207
|
+
}
|
|
194
208
|
// Check if directory contains package.json with vat.skills
|
|
195
209
|
const packageJsonPath = join(sourcePath, 'package.json');
|
|
196
210
|
// eslint-disable-next-line security/detect-non-literal-fs-filename -- User-provided CLI argument
|
|
197
211
|
const hasPackageJson = existsSync(packageJsonPath);
|
|
198
|
-
const skillsDir = options.skillsDir ?? getClaudeUserPaths().skillsDir;
|
|
199
212
|
let installed;
|
|
200
213
|
if (hasPackageJson) {
|
|
201
|
-
// Read vat metadata - install all skills (or filtered by --name)
|
|
202
214
|
const { packageJson, skills } = await readPackageJsonVatMetadata(sourcePath);
|
|
203
|
-
const
|
|
204
|
-
|
|
205
|
-
: skills;
|
|
206
|
-
if (skillsToInstall.length === 0) {
|
|
215
|
+
const skillNames = options.name ? skills.filter(s => s === options.name) : skills;
|
|
216
|
+
if (skillNames.length === 0) {
|
|
207
217
|
throw new Error(`Skill "${options.name ?? ''}" not found in package ${packageJson.name}. ` +
|
|
208
|
-
`Available: ${skills.
|
|
218
|
+
`Available: ${skills.join(', ')}`);
|
|
209
219
|
}
|
|
210
|
-
for (const
|
|
211
|
-
const skillPath =
|
|
212
|
-
await installSkillFromPath(skillPath,
|
|
220
|
+
for (const skillName of skillNames) {
|
|
221
|
+
const skillPath = join(sourcePath, 'dist', 'skills', skillNameToFsPath(skillName));
|
|
222
|
+
await installSkillFromPath(skillPath, skillName, options, logger);
|
|
213
223
|
}
|
|
214
|
-
installed =
|
|
224
|
+
installed = skillNames.map(name => ({ name, installPath: join(skillsDir, name) }));
|
|
215
225
|
}
|
|
216
226
|
else {
|
|
217
227
|
// Plain directory - use directory name
|
|
@@ -229,7 +239,6 @@ async function handleLocalInstall(source, options, logger, startTime) {
|
|
|
229
239
|
async function handleZipInstall(source, options, logger, startTime) {
|
|
230
240
|
const sourcePath = resolve(source);
|
|
231
241
|
logger.info(`📥 Installing skill from ZIP: ${sourcePath}`);
|
|
232
|
-
// Validate ZIP exists
|
|
233
242
|
// eslint-disable-next-line security/detect-non-literal-fs-filename -- User-provided CLI argument
|
|
234
243
|
if (!existsSync(sourcePath)) {
|
|
235
244
|
throw new Error(`ZIP file not found: ${sourcePath}`);
|
|
@@ -237,7 +246,6 @@ async function handleZipInstall(source, options, logger, startTime) {
|
|
|
237
246
|
const skillName = options.name ?? basename(sourcePath, '.zip');
|
|
238
247
|
const { installPath } = await prepareInstallation(skillName, options);
|
|
239
248
|
if (!options.dryRun) {
|
|
240
|
-
// Extract ZIP
|
|
241
249
|
logger.info(' Extracting ZIP...');
|
|
242
250
|
const zip = new AdmZip(sourcePath);
|
|
243
251
|
// eslint-disable-next-line sonarjs/no-unsafe-unzip -- User-provided local files, isolated plugin directory
|
|
@@ -247,38 +255,62 @@ async function handleZipInstall(source, options, logger, startTime) {
|
|
|
247
255
|
outputInstallSuccess([{ name: skillName, installPath }], sourcePath, 'zip', duration, logger, options.dryRun);
|
|
248
256
|
process.exit(0);
|
|
249
257
|
}
|
|
258
|
+
/**
|
|
259
|
+
* Install a single skill as a dev symlink.
|
|
260
|
+
* Extracted from handleDevInstall to reduce cognitive complexity.
|
|
261
|
+
*/
|
|
262
|
+
async function installDevSkill(skillName, cwd, skillsDir, options, logger) {
|
|
263
|
+
const fsPath = skillNameToFsPath(skillName);
|
|
264
|
+
const sourcePath = resolve(cwd, 'dist', 'skills', fsPath);
|
|
265
|
+
const installPath = join(skillsDir, skillName);
|
|
266
|
+
// eslint-disable-next-line security/detect-non-literal-fs-filename -- Validated path from package.json
|
|
267
|
+
if (!existsSync(sourcePath)) {
|
|
268
|
+
const buildCmd = options.name ? `vat skills build --skill ${skillName}` : 'vat skills build';
|
|
269
|
+
throw new Error(`Skill "${skillName}" not built at ${sourcePath}\n` +
|
|
270
|
+
`Run: ${buildCmd}\n` +
|
|
271
|
+
`Or use: vat skills install --build`);
|
|
272
|
+
}
|
|
273
|
+
const existingIsSymlink = await handleExistingDevInstall(installPath, skillName, options);
|
|
274
|
+
if (!options.dryRun) {
|
|
275
|
+
// eslint-disable-next-line security/detect-non-literal-fs-filename -- Skills directory from config
|
|
276
|
+
await mkdir(skillsDir, { recursive: true });
|
|
277
|
+
// eslint-disable-next-line security/detect-non-literal-fs-filename -- Validated paths
|
|
278
|
+
await symlink(sourcePath, installPath, 'dir');
|
|
279
|
+
}
|
|
280
|
+
const action = existingIsSymlink ? 'Re-symlinked' : 'Symlinked';
|
|
281
|
+
logger.info(` ${options.dryRun ? 'Would symlink' : action}: ${skillName} → ${sourcePath}`);
|
|
282
|
+
return { name: skillName, installPath, sourcePath };
|
|
283
|
+
}
|
|
250
284
|
/**
|
|
251
285
|
* Handle development mode installation (symlinks)
|
|
252
|
-
* Reads package.json vat.skills[], symlinks each built skill to ~/.claude/skills/
|
|
286
|
+
* Reads package.json vat.skills[] (skill name strings), symlinks each built skill to ~/.claude/skills/
|
|
253
287
|
*/
|
|
254
288
|
async function handleDevInstall(options, logger, startTime) {
|
|
255
|
-
// Windows check - symlinks require elevated privileges
|
|
256
289
|
if (process.platform === 'win32') {
|
|
257
290
|
throw new Error('--dev (symlink) not supported on Windows.\n' +
|
|
258
291
|
'Use copy mode (omit --dev) or WSL for development.');
|
|
259
292
|
}
|
|
260
293
|
const cwd = process.cwd();
|
|
261
|
-
// If --build, shell out to vat skills build first
|
|
262
294
|
if (options.build) {
|
|
263
295
|
runSkillsBuild(cwd, options.name, logger);
|
|
264
296
|
}
|
|
265
|
-
// Read package.json for skill
|
|
297
|
+
// Read package.json for skill names
|
|
266
298
|
const { packageJson, skills } = await readPackageJsonVatMetadata(cwd);
|
|
267
299
|
logger.info(`📥 Dev-installing skills from ${packageJson.name}`);
|
|
268
300
|
// Filter by --name if specified
|
|
269
|
-
const
|
|
270
|
-
? skills.filter(s => s
|
|
301
|
+
const skillNames = options.name
|
|
302
|
+
? skills.filter(s => s === options.name)
|
|
271
303
|
: skills;
|
|
272
|
-
if (
|
|
304
|
+
if (skillNames.length === 0) {
|
|
273
305
|
const msg = options.name
|
|
274
|
-
? `Skill "${options.name}" not found in package. Available: ${skills.
|
|
306
|
+
? `Skill "${options.name}" not found in package. Available: ${skills.join(', ')}`
|
|
275
307
|
: `No skills found in ${packageJson.name}`;
|
|
276
308
|
throw new Error(msg);
|
|
277
309
|
}
|
|
278
310
|
const skillsDir = options.skillsDir ?? getClaudeUserPaths().skillsDir;
|
|
279
311
|
const installed = [];
|
|
280
|
-
for (const
|
|
281
|
-
const result = await
|
|
312
|
+
for (const skillName of skillNames) {
|
|
313
|
+
const result = await installDevSkill(skillName, cwd, skillsDir, options, logger);
|
|
282
314
|
installed.push(result);
|
|
283
315
|
}
|
|
284
316
|
const duration = Date.now() - startTime;
|
|
@@ -301,43 +333,14 @@ function runSkillsBuild(cwd, skillName, logger) {
|
|
|
301
333
|
});
|
|
302
334
|
logger.info('');
|
|
303
335
|
}
|
|
304
|
-
/**
|
|
305
|
-
* Symlink a single skill to the skills directory
|
|
306
|
-
* Verifies skill is built, checks for existing installation, creates symlink
|
|
307
|
-
*/
|
|
308
|
-
async function symlinkSkill(skill, cwd, skillsDir, options, logger) {
|
|
309
|
-
const sourcePath = resolve(cwd, skill.path);
|
|
310
|
-
const installPath = join(skillsDir, skill.name);
|
|
311
|
-
// Verify skill is built
|
|
312
|
-
// eslint-disable-next-line security/detect-non-literal-fs-filename -- Validated path from package.json
|
|
313
|
-
if (!existsSync(sourcePath)) {
|
|
314
|
-
const buildCmd = options.name ? `vat skills build --skill ${skill.name}` : 'vat skills build';
|
|
315
|
-
throw new Error(`Skill "${skill.name}" not built at ${sourcePath}\n` +
|
|
316
|
-
`Run: ${buildCmd}\n` +
|
|
317
|
-
`Or use: vat skills install --build`);
|
|
318
|
-
}
|
|
319
|
-
// Check existing installation
|
|
320
|
-
const existingIsSymlink = await handleExistingDevInstall(installPath, skill.name, options);
|
|
321
|
-
if (!options.dryRun) {
|
|
322
|
-
// eslint-disable-next-line security/detect-non-literal-fs-filename -- Skills directory from config
|
|
323
|
-
await mkdir(skillsDir, { recursive: true });
|
|
324
|
-
// eslint-disable-next-line security/detect-non-literal-fs-filename -- Validated paths
|
|
325
|
-
await symlink(sourcePath, installPath, 'dir');
|
|
326
|
-
}
|
|
327
|
-
const action = existingIsSymlink ? 'Re-symlinked' : 'Symlinked';
|
|
328
|
-
logger.info(` ${options.dryRun ? 'Would symlink' : action}: ${skill.name} → ${sourcePath}`);
|
|
329
|
-
return { name: skill.name, installPath, sourcePath };
|
|
330
|
-
}
|
|
331
336
|
/**
|
|
332
337
|
* Check if a skill is already installed at the target path
|
|
333
|
-
* Returns whether the existing entry was a symlink (for logging)
|
|
334
338
|
*/
|
|
335
339
|
async function handleExistingDevInstall(installPath, skillName, options) {
|
|
336
340
|
let existingIsSymlink = false;
|
|
337
341
|
try {
|
|
338
342
|
// eslint-disable-next-line security/detect-non-literal-fs-filename -- Validated install path
|
|
339
343
|
lstatSync(installPath);
|
|
340
|
-
// Path exists (lstat doesn't follow symlinks, so broken symlinks are detected)
|
|
341
344
|
if (!options.force) {
|
|
342
345
|
throw new Error(`Skill "${skillName}" already installed at ${installPath}.\n` +
|
|
343
346
|
`Use --force to overwrite.`);
|
|
@@ -349,69 +352,39 @@ async function handleExistingDevInstall(installPath, skillName, options) {
|
|
|
349
352
|
}
|
|
350
353
|
}
|
|
351
354
|
catch (error) {
|
|
352
|
-
// Re-throw if it's our "already installed" error
|
|
353
355
|
if (error instanceof Error && error.message.includes('already installed')) {
|
|
354
356
|
throw error;
|
|
355
357
|
}
|
|
356
|
-
// Otherwise: not installed, continue
|
|
357
358
|
}
|
|
358
359
|
return existingIsSymlink;
|
|
359
360
|
}
|
|
360
|
-
/**
|
|
361
|
-
* Output success YAML for dev install (multiple skills)
|
|
362
|
-
*/
|
|
363
|
-
function outputDevSuccess(installed, packageName, duration, logger, dryRun) {
|
|
364
|
-
writeYamlHeader(dryRun);
|
|
365
|
-
process.stdout.write(`sourceType: dev\n`);
|
|
366
|
-
process.stdout.write(`package: "${packageName}"\n`);
|
|
367
|
-
process.stdout.write(`skillsInstalled: ${installed.length}\n`);
|
|
368
|
-
process.stdout.write(`symlink: true\n`);
|
|
369
|
-
process.stdout.write(`skills:\n`);
|
|
370
|
-
for (const skill of installed) {
|
|
371
|
-
process.stdout.write(` - name: ${skill.name}\n`);
|
|
372
|
-
process.stdout.write(` installPath: ${skill.installPath}\n`);
|
|
373
|
-
process.stdout.write(` sourcePath: ${skill.sourcePath}\n`);
|
|
374
|
-
}
|
|
375
|
-
process.stdout.write(`duration: ${duration}ms\n`);
|
|
376
|
-
if (dryRun) {
|
|
377
|
-
logger.info(`\n✅ Dry-run complete: ${installed.length} skill(s) would be symlinked`);
|
|
378
|
-
}
|
|
379
|
-
else {
|
|
380
|
-
logger.info(`\n✅ Dev-installed ${installed.length} skill(s) via symlink`);
|
|
381
|
-
logger.info(` After rebuilding, run /reload-skills in Claude Code`);
|
|
382
|
-
}
|
|
383
|
-
}
|
|
384
361
|
/**
|
|
385
362
|
* Handle npm postinstall hook
|
|
386
363
|
*/
|
|
387
364
|
async function handleNpmPostinstall(options, logger, startTime) {
|
|
388
365
|
logger.info(`📥 Running npm postinstall hook`);
|
|
389
|
-
// Check if this is a global install
|
|
390
366
|
if (!isGlobalNpmInstall()) {
|
|
391
367
|
logger.info(' Skipping: Not a global npm install');
|
|
392
368
|
process.exit(0);
|
|
393
369
|
}
|
|
394
|
-
// Read package.json from current directory
|
|
395
370
|
const cwd = process.cwd();
|
|
396
|
-
//
|
|
397
|
-
|
|
398
|
-
const
|
|
399
|
-
const marketplaceExists = existsSync(marketplaceJsonPath);
|
|
371
|
+
// Check for pre-built plugin tree (dist/.claude/plugins/marketplaces/)
|
|
372
|
+
const marketplacesDir = join(cwd, PLUGIN_MARKETPLACES_SUBPATH);
|
|
373
|
+
const hasPluginTree = existsSync(marketplacesDir);
|
|
400
374
|
if (!options.userInstallWithoutPlugin) {
|
|
401
|
-
if (
|
|
375
|
+
if (hasPluginTree) {
|
|
402
376
|
const pkgRaw = readFileSync(join(cwd, PACKAGE_JSON), 'utf-8');
|
|
403
377
|
const packageJson = JSON.parse(pkgRaw);
|
|
404
378
|
logger.info(` Package: ${packageJson.name}@${packageJson.version ?? 'unknown'}`);
|
|
405
|
-
logger.info(` Plugin detected —
|
|
406
|
-
await
|
|
379
|
+
logger.info(` Plugin tree detected — copying to ~/.claude/plugins/`);
|
|
380
|
+
await copyPluginTree(marketplacesDir, packageJson, logger);
|
|
407
381
|
const duration = Date.now() - startTime;
|
|
408
382
|
logger.info(`✅ Installed plugin from ${packageJson.name}`);
|
|
409
383
|
logger.info(` Duration: ${duration}ms`);
|
|
410
384
|
}
|
|
411
385
|
else {
|
|
412
|
-
|
|
413
|
-
logger.info(`
|
|
414
|
-
logger.info(` Run 'vat build' to generate dist/.claude-plugin/marketplace.json before publishing.`);
|
|
386
|
+
logger.info(` No plugin tree found at dist/.claude/plugins/marketplaces/`);
|
|
387
|
+
logger.info(` Run 'vat build' to generate plugin artifacts before publishing.`);
|
|
415
388
|
logger.info(` Skipping install — no skills registered.`);
|
|
416
389
|
}
|
|
417
390
|
process.exit(0);
|
|
@@ -420,9 +393,9 @@ async function handleNpmPostinstall(options, logger, startTime) {
|
|
|
420
393
|
const { packageJson, skills } = await readPackageJsonVatMetadata(cwd);
|
|
421
394
|
logger.info(` Package: ${packageJson.name}@${packageJson.version}`);
|
|
422
395
|
logger.info(` Skills found: ${skills.length}`);
|
|
423
|
-
for (const
|
|
424
|
-
const skillPath =
|
|
425
|
-
await installSkillFromPath(skillPath,
|
|
396
|
+
for (const skillName of skills) {
|
|
397
|
+
const skillPath = join(cwd, 'dist', 'skills', skillNameToFsPath(skillName));
|
|
398
|
+
await installSkillFromPath(skillPath, skillName, options, logger);
|
|
426
399
|
}
|
|
427
400
|
const duration = Date.now() - startTime;
|
|
428
401
|
logger.info(`✅ Installed ${skills.length} skill(s) from ${packageJson.name}`);
|
|
@@ -430,63 +403,63 @@ async function handleNpmPostinstall(options, logger, startTime) {
|
|
|
430
403
|
process.exit(0);
|
|
431
404
|
}
|
|
432
405
|
/**
|
|
433
|
-
*
|
|
434
|
-
*
|
|
435
|
-
*
|
|
406
|
+
* Copy pre-built plugin tree to ~/.claude/plugins/ and update registry.
|
|
407
|
+
*
|
|
408
|
+
* This is a "dumb copy" — the dist/.claude/plugins/marketplaces/ tree mirrors
|
|
409
|
+
* the target ~/.claude/plugins/marketplaces/ structure exactly. No path rewriting,
|
|
410
|
+
* no assembly, no skill resolution. Just recursive copy + registry update.
|
|
436
411
|
*/
|
|
437
|
-
async function
|
|
438
|
-
const marketplaceJsonPath = join(cwd, MARKETPLACE_JSON_SUBPATH);
|
|
439
|
-
// eslint-disable-next-line security/detect-non-literal-fs-filename -- Resolved from known cwd + constant subpath
|
|
440
|
-
if (!existsSync(marketplaceJsonPath)) {
|
|
441
|
-
logger.info(` ℹ️ No plugin artifacts found (dist/.claude-plugin/marketplace.json)`);
|
|
442
|
-
logger.info(` Run 'vat build' before publishing to register plugins in Claude`);
|
|
443
|
-
return;
|
|
444
|
-
}
|
|
445
|
-
let marketplace;
|
|
446
|
-
try {
|
|
447
|
-
// eslint-disable-next-line security/detect-non-literal-fs-filename -- Resolved from known cwd + constant subpath
|
|
448
|
-
const raw = readFileSync(marketplaceJsonPath, 'utf-8');
|
|
449
|
-
marketplace = JSON.parse(raw);
|
|
450
|
-
}
|
|
451
|
-
catch (error) {
|
|
452
|
-
logger.info(` Warning: Could not parse dist/.claude-plugin/marketplace.json: ${String(error)}`);
|
|
453
|
-
return;
|
|
454
|
-
}
|
|
455
|
-
const marketplaceName = marketplace.name;
|
|
456
|
-
const plugins = marketplace.plugins ?? [];
|
|
412
|
+
async function copyPluginTree(marketplacesDir, packageJson, logger) {
|
|
457
413
|
const paths = getClaudeUserPaths();
|
|
458
414
|
const version = packageJson.version ?? '0.0.0';
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
415
|
+
// Scan for marketplace directories
|
|
416
|
+
// eslint-disable-next-line security/detect-non-literal-fs-filename -- Resolved from constant subpath
|
|
417
|
+
const marketplaceNames = readdirSync(marketplacesDir, { withFileTypes: true })
|
|
418
|
+
.filter(d => d.isDirectory())
|
|
419
|
+
.map(d => d.name);
|
|
420
|
+
for (const mpName of marketplaceNames) {
|
|
421
|
+
const srcMpDir = join(marketplacesDir, mpName);
|
|
422
|
+
const destMpDir = join(paths.marketplacesDir, mpName);
|
|
423
|
+
// Copy entire marketplace directory tree
|
|
424
|
+
logger.info(` Copying marketplace: ${mpName} → ${destMpDir}`);
|
|
425
|
+
// eslint-disable-next-line security/detect-non-literal-fs-filename -- Resolved from constant subpath
|
|
426
|
+
await mkdir(destMpDir, { recursive: true });
|
|
427
|
+
await cp(srcMpDir, destMpDir, { recursive: true, force: true });
|
|
428
|
+
// Discover plugins within the marketplace for registry updates
|
|
429
|
+
const pluginsDir = join(srcMpDir, 'plugins');
|
|
430
|
+
// eslint-disable-next-line security/detect-non-literal-fs-filename -- Resolved from constant subpath
|
|
431
|
+
if (!existsSync(pluginsDir)) {
|
|
464
432
|
continue;
|
|
465
433
|
}
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
434
|
+
// eslint-disable-next-line security/detect-non-literal-fs-filename -- Resolved from constant subpath
|
|
435
|
+
const pluginNames = readdirSync(pluginsDir, { withFileTypes: true })
|
|
436
|
+
.filter(d => d.isDirectory())
|
|
437
|
+
.map(d => d.name);
|
|
438
|
+
for (const pluginName of pluginNames) {
|
|
439
|
+
const pluginDir = join(destMpDir, 'plugins', pluginName);
|
|
440
|
+
try {
|
|
441
|
+
await installPlugin({
|
|
442
|
+
marketplaceName: mpName,
|
|
443
|
+
pluginName,
|
|
444
|
+
pluginDir,
|
|
445
|
+
version,
|
|
446
|
+
source: { source: 'npm', package: packageJson.name, version },
|
|
447
|
+
paths,
|
|
448
|
+
});
|
|
449
|
+
logger.info(` Registered plugin ${pluginName}@${mpName} in Claude plugin registry`);
|
|
450
|
+
}
|
|
451
|
+
catch (error) {
|
|
452
|
+
logger.info(` Warning: Could not register plugin ${pluginName}: ${String(error)}`);
|
|
453
|
+
}
|
|
479
454
|
}
|
|
480
455
|
}
|
|
481
456
|
}
|
|
482
457
|
/**
|
|
483
458
|
* Prepare plugins directory and check for conflicts
|
|
484
|
-
* Returns the install path for the skill
|
|
485
459
|
*/
|
|
486
460
|
async function prepareInstallation(skillName, options) {
|
|
487
461
|
const skillsDir = options.skillsDir ?? getClaudeUserPaths().skillsDir;
|
|
488
462
|
const installPath = join(skillsDir, skillName);
|
|
489
|
-
// Check if skill already exists (lstatSync detects broken symlinks too)
|
|
490
463
|
let exists = false;
|
|
491
464
|
try {
|
|
492
465
|
// eslint-disable-next-line security/detect-non-literal-fs-filename -- Install path from config
|
|
@@ -503,7 +476,6 @@ async function prepareInstallation(skillName, options) {
|
|
|
503
476
|
await rm(installPath, { recursive: true, force: true });
|
|
504
477
|
}
|
|
505
478
|
if (!options.dryRun) {
|
|
506
|
-
// Create skills directory
|
|
507
479
|
// eslint-disable-next-line security/detect-non-literal-fs-filename -- Skills directory path, safe
|
|
508
480
|
await mkdir(skillsDir, { recursive: true });
|
|
509
481
|
}
|
|
@@ -519,13 +491,36 @@ async function installSkillFromPath(skillPath, skillName, options, logger) {
|
|
|
519
491
|
}
|
|
520
492
|
const { installPath } = await prepareInstallation(skillName, options);
|
|
521
493
|
if (!options.dryRun) {
|
|
522
|
-
// Copy skill to plugins directory
|
|
523
494
|
logger.info(` Installing ${skillName}...`);
|
|
524
495
|
await cp(skillPath, installPath, { recursive: true, force: true });
|
|
525
496
|
}
|
|
526
497
|
}
|
|
527
498
|
/**
|
|
528
|
-
* Output success YAML
|
|
499
|
+
* Output success YAML for dev install
|
|
500
|
+
*/
|
|
501
|
+
function outputDevSuccess(installed, packageName, duration, logger, dryRun) {
|
|
502
|
+
writeYamlHeader(dryRun);
|
|
503
|
+
process.stdout.write(`sourceType: dev\n`);
|
|
504
|
+
process.stdout.write(`package: "${packageName}"\n`);
|
|
505
|
+
process.stdout.write(`skillsInstalled: ${installed.length}\n`);
|
|
506
|
+
process.stdout.write(`symlink: true\n`);
|
|
507
|
+
process.stdout.write(`skills:\n`);
|
|
508
|
+
for (const skill of installed) {
|
|
509
|
+
process.stdout.write(` - name: ${skill.name}\n`);
|
|
510
|
+
process.stdout.write(` installPath: ${skill.installPath}\n`);
|
|
511
|
+
process.stdout.write(` sourcePath: ${skill.sourcePath}\n`);
|
|
512
|
+
}
|
|
513
|
+
process.stdout.write(`duration: ${duration}ms\n`);
|
|
514
|
+
if (dryRun) {
|
|
515
|
+
logger.info(`\n✅ Dry-run complete: ${installed.length} skill(s) would be symlinked`);
|
|
516
|
+
}
|
|
517
|
+
else {
|
|
518
|
+
logger.info(`\n✅ Dev-installed ${installed.length} skill(s) via symlink`);
|
|
519
|
+
logger.info(` After rebuilding, run /reload-skills in Claude Code`);
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
/**
|
|
523
|
+
* Output success YAML for install
|
|
529
524
|
*/
|
|
530
525
|
function outputInstallSuccess(skills, source, sourceType, duration, logger, dryRun) {
|
|
531
526
|
writeYamlHeader(dryRun);
|