wyvrnpm 2.0.4 → 2.3.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.
Files changed (44) hide show
  1. package/README.md +639 -5
  2. package/bin/wyvrn.js +220 -10
  3. package/claude/skills/wyvrnpm.skill +0 -0
  4. package/cmake/cpp.cmake +9 -0
  5. package/cmake/functions.cmake +224 -0
  6. package/cmake/macros.cmake +233 -0
  7. package/cmake/options.cmake +23 -0
  8. package/cmake/variables.cmake +171 -0
  9. package/package.json +3 -1
  10. package/src/build/cache.js +148 -0
  11. package/src/build/clone.js +170 -0
  12. package/src/build/cmake.js +342 -0
  13. package/src/build/index.js +275 -0
  14. package/src/build/msvc-env.js +217 -0
  15. package/src/build/recipe.js +155 -0
  16. package/src/commands/build.js +283 -0
  17. package/src/commands/clean.js +56 -16
  18. package/src/commands/configure.js +6 -5
  19. package/src/commands/init.js +3 -2
  20. package/src/commands/install-skill.js +107 -0
  21. package/src/commands/install.js +262 -19
  22. package/src/commands/link.js +18 -15
  23. package/src/commands/profile.js +15 -12
  24. package/src/commands/publish.js +216 -65
  25. package/src/commands/show.js +237 -0
  26. package/src/compat.js +261 -0
  27. package/src/config.js +3 -1
  28. package/src/download.js +431 -87
  29. package/src/ignore.js +118 -0
  30. package/src/logger.js +94 -0
  31. package/src/manifest.js +12 -7
  32. package/src/options.js +303 -0
  33. package/src/profile.js +56 -4
  34. package/src/providers/base.js +16 -1
  35. package/src/providers/file.js +12 -6
  36. package/src/providers/http.js +15 -10
  37. package/src/providers/s3.js +14 -9
  38. package/src/resolve.js +179 -19
  39. package/src/toolchain/deps.js +164 -0
  40. package/src/toolchain/index.js +141 -0
  41. package/src/toolchain/presets.js +263 -0
  42. package/src/toolchain/template.cmake +66 -0
  43. package/src/upload-built.js +256 -0
  44. package/src/version-range.js +301 -0
@@ -0,0 +1,283 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const { spawn } = require('child_process');
6
+
7
+ const { readConfig } = require('../config');
8
+ const { loadProfile } = require('../profile');
9
+ const { readManifest } = require('../manifest');
10
+ const { normalizeRecipe, hasBuildRecipe } = require('../build/recipe');
11
+ const { loadMsvcEnv } = require('../build/msvc-env');
12
+ const install = require('./install');
13
+ const log = require('../logger');
14
+
15
+ // ---------------------------------------------------------------------------
16
+ // Pure helpers (unit-testable — no process spawning, no filesystem writes)
17
+ // ---------------------------------------------------------------------------
18
+
19
+ /**
20
+ * Decide whether `wyvrn install` must run before build.
21
+ *
22
+ * - toolchain-missing : wyvrn_toolchain.cmake does not exist → definitely stale.
23
+ * - lock-newer : wyvrn.lock was modified after the toolchain was written,
24
+ * so the toolchain's package list is out of date.
25
+ * - fresh : no action needed.
26
+ *
27
+ * @param {{ rootDir: string, manifestPath: string }} args
28
+ * @returns {{ stale: boolean, reason: string }}
29
+ */
30
+ function isToolchainStale({ rootDir, manifestPath }) {
31
+ const lockPath = path.join(path.dirname(manifestPath), 'wyvrn.lock');
32
+ const toolchainPath = path.join(rootDir, 'wyvrn_internal', 'wyvrn_toolchain.cmake');
33
+
34
+ if (!fs.existsSync(toolchainPath)) {
35
+ return { stale: true, reason: 'toolchain-missing' };
36
+ }
37
+ if (!fs.existsSync(lockPath)) {
38
+ return { stale: false, reason: 'no-lock' };
39
+ }
40
+
41
+ const lockMtime = fs.statSync(lockPath).mtimeMs;
42
+ const toolchainMtime = fs.statSync(toolchainPath).mtimeMs;
43
+ if (lockMtime > toolchainMtime) {
44
+ return { stale: true, reason: 'lock-newer-than-toolchain' };
45
+ }
46
+ return { stale: false, reason: 'fresh' };
47
+ }
48
+
49
+ /**
50
+ * Resolve the configure preset name from CLI override or profile convention.
51
+ *
52
+ * @param {{ presetOverride?: string, profileName: string }} args
53
+ * @returns {string}
54
+ */
55
+ function resolvePresetName({ presetOverride, profileName }) {
56
+ if (presetOverride) return presetOverride;
57
+ return `wyvrn-${profileName}`;
58
+ }
59
+
60
+ /**
61
+ * Compute the build directory for a preset, matching the pattern emitted
62
+ * by [src/toolchain/presets.js](../toolchain/presets.js) — `${sourceDir}/build/<preset>`.
63
+ *
64
+ * @param {{ rootDir: string, presetName: string }} args
65
+ * @returns {string}
66
+ */
67
+ function resolveBinaryDir({ rootDir, presetName }) {
68
+ return path.join(rootDir, 'build', presetName);
69
+ }
70
+
71
+ /**
72
+ * Args passed to `cmake` for the configure step.
73
+ * @param {{ presetName: string }} args
74
+ * @returns {string[]}
75
+ */
76
+ function buildConfigureArgs({ presetName }) {
77
+ return ['--preset', presetName];
78
+ }
79
+
80
+ /**
81
+ * Args passed to `cmake` for the build step.
82
+ *
83
+ * @param {{
84
+ * binaryDir: string,
85
+ * config: string,
86
+ * target?: string,
87
+ * verbose?: boolean,
88
+ * passthru?: string[],
89
+ * }} args
90
+ * @returns {string[]}
91
+ */
92
+ function buildBuildArgs({ binaryDir, config, target, verbose, passthru = [] }) {
93
+ const args = ['--build', binaryDir, '--config', config];
94
+ if (verbose) args.push('--verbose');
95
+ if (target) args.push('--target', target);
96
+ if (passthru.length > 0) args.push('--', ...passthru);
97
+ return args;
98
+ }
99
+
100
+ /**
101
+ * Args passed to `cmake --install` for the install step.
102
+ *
103
+ * @param {{
104
+ * binaryDir: string,
105
+ * config: string,
106
+ * prefix?: string, // override CMAKE_INSTALL_PREFIX at install time
107
+ * }} args
108
+ * @returns {string[]}
109
+ */
110
+ function buildInstallArgs({ binaryDir, config, prefix }) {
111
+ const args = ['--install', binaryDir, '--config', config];
112
+ if (prefix) args.push('--prefix', prefix);
113
+ return args;
114
+ }
115
+
116
+ // ---------------------------------------------------------------------------
117
+ // Spawn wrapper
118
+ // ---------------------------------------------------------------------------
119
+
120
+ /**
121
+ * Run `cmake` with the given args. Inherits stdio for real-time output.
122
+ * Rejects if the process exits non-zero or fails to spawn.
123
+ *
124
+ * When `env` is provided, it replaces the ambient environment (used to
125
+ * activate MSVC via vcvarsall for non-VS generators on Windows).
126
+ *
127
+ * @param {string[]} args
128
+ * @param {NodeJS.ProcessEnv|null} [env]
129
+ * @returns {Promise<void>}
130
+ */
131
+ function runCmake(args, env = null) {
132
+ return new Promise((resolve, reject) => {
133
+ const opts = { stdio: 'inherit' };
134
+ if (env) opts.env = env;
135
+ const proc = spawn('cmake', args, opts);
136
+ proc.on('error', (err) => {
137
+ reject(new Error(
138
+ `Failed to spawn cmake: ${err.message}\n` +
139
+ ' Is CMake installed and on PATH?',
140
+ ));
141
+ });
142
+ proc.on('close', (code) => {
143
+ if (code === 0) resolve();
144
+ else reject(new Error(`cmake ${args.join(' ')} exited with code ${code}`));
145
+ });
146
+ });
147
+ }
148
+
149
+ // ---------------------------------------------------------------------------
150
+ // Command entry
151
+ // ---------------------------------------------------------------------------
152
+
153
+ /**
154
+ * `wyvrnpm build` — configure + build via the generated CMake preset.
155
+ *
156
+ * Defaults:
157
+ * - preset : `wyvrn-<profileName>` (from --profile or config.defaultProfile)
158
+ * - config : Release
159
+ * - auto-install when toolchain is missing / older than wyvrn.lock
160
+ *
161
+ * @param {object} argv
162
+ */
163
+ async function build(argv) {
164
+ const rootDir = path.resolve(argv.root);
165
+ const manifestPath = path.resolve(argv.manifest);
166
+
167
+ const config = readConfig();
168
+ const profileName = argv.profile ?? config.defaultProfile ?? 'default';
169
+ const presetName = resolvePresetName({ presetOverride: argv.preset, profileName });
170
+ const buildConfig = argv.config ?? 'Release';
171
+
172
+ // ── Stale check / auto-install ────────────────────────────────────────────
173
+ // `--auto-install` gates the "run `wyvrnpm install` first when the
174
+ // toolchain is missing or older than wyvrn.lock" behaviour. The separate
175
+ // `--install` flag below runs `cmake --install` AFTER build; the two are
176
+ // deliberately distinct concepts.
177
+ const staleness = isToolchainStale({ rootDir, manifestPath });
178
+ if (staleness.stale) {
179
+ if (argv.autoInstall === false) {
180
+ log.error(
181
+ `toolchain is stale (${staleness.reason}) and --no-auto-install was passed.\n` +
182
+ ' Run: wyvrnpm install',
183
+ );
184
+ process.exit(1);
185
+ }
186
+ log.info(`Toolchain ${staleness.reason} — running install first`);
187
+ await install({
188
+ manifest: argv.manifest,
189
+ root: argv.root,
190
+ profile: argv.profile,
191
+ source: argv.source,
192
+ platform: argv.platform ?? 'win_x64',
193
+ timeout: argv.timeout ?? 300,
194
+ });
195
+ }
196
+
197
+ const binaryDir = resolveBinaryDir({ rootDir, presetName });
198
+
199
+ // ── Clean ─────────────────────────────────────────────────────────────────
200
+ if (argv.clean && fs.existsSync(binaryDir)) {
201
+ log.info(`Removing ${binaryDir}`);
202
+ fs.rmSync(binaryDir, { recursive: true, force: true });
203
+ }
204
+
205
+ // ── MSVC env activation (Windows + non-VS generator) ───────────────────
206
+ // When the preset's generator is Ninja / Ninja Multi-Config / Make / etc.
207
+ // and the active profile is MSVC, we need `vcvarsall.bat` to have been
208
+ // sourced — otherwise cmake's compiler detection fails with "CXX compiler
209
+ // identification is unknown". VS generators sidestep this because MSBuild
210
+ // finds MSVC via its own toolset resolution.
211
+ //
212
+ // Same mechanism the source-build path already uses (src/build/msvc-env.js).
213
+ let cmakeEnv = null;
214
+ try {
215
+ const { profile: activeProfile } = loadProfile(profileName);
216
+ // Which generator will the preset actually use? Read it from the
217
+ // project's own recipe if one exists. If not, default to null and let
218
+ // cmake/CMakePresets decide — in which case MSVC env activation is
219
+ // skipped (because VS is usually the default on Windows).
220
+ const manifest = readManifest(manifestPath);
221
+ let presetGenerator = null;
222
+ if (hasBuildRecipe(manifest)) {
223
+ try {
224
+ const recipe = normalizeRecipe(manifest.build, null);
225
+ if (recipe.generators.length > 0) [presetGenerator] = recipe.generators;
226
+ } catch { /* templates need options → skip env activation */ }
227
+ }
228
+ cmakeEnv = await loadMsvcEnv({
229
+ profile: activeProfile,
230
+ skipForGenerator: presetGenerator,
231
+ });
232
+ } catch (err) {
233
+ log.warn(`Could not resolve MSVC env activation: ${err.message}`);
234
+ }
235
+
236
+ // ── Configure ─────────────────────────────────────────────────────────────
237
+ const configureArgs = buildConfigureArgs({ presetName });
238
+ log.info(`cmake ${configureArgs.join(' ')}`);
239
+ await runCmake(configureArgs, cmakeEnv);
240
+
241
+ // ── Build ─────────────────────────────────────────────────────────────────
242
+ const buildArgs = buildBuildArgs({
243
+ binaryDir,
244
+ config: buildConfig,
245
+ target: argv.target,
246
+ verbose: argv.verbose,
247
+ passthru: argv['--'] ?? [],
248
+ });
249
+ log.info(`cmake ${buildArgs.join(' ')}`);
250
+ await runCmake(buildArgs, cmakeEnv);
251
+
252
+ // ── Install (optional) ────────────────────────────────────────────────────
253
+ // `--install` runs `cmake --install <binaryDir>` after the build. Prefix
254
+ // defaults to whatever was baked into the cache at configure time; pass
255
+ // `--install-prefix` to override without reconfiguring.
256
+ if (argv.install) {
257
+ const installArgs = buildInstallArgs({
258
+ binaryDir,
259
+ config: buildConfig,
260
+ prefix: argv.installPrefix,
261
+ });
262
+ log.info(`cmake ${installArgs.join(' ')}`);
263
+ await runCmake(installArgs, cmakeEnv);
264
+ }
265
+
266
+ const suffix = argv.install
267
+ ? ` + installed${argv.installPrefix ? ` to ${argv.installPrefix}` : ''}`
268
+ : '';
269
+ log.success(`Build complete: ${binaryDir} (${buildConfig})${suffix}`);
270
+ }
271
+
272
+ module.exports = build;
273
+ module.exports.default = build;
274
+
275
+ // Exported for unit tests
276
+ module.exports._helpers = {
277
+ isToolchainStale,
278
+ resolvePresetName,
279
+ resolveBinaryDir,
280
+ buildConfigureArgs,
281
+ buildBuildArgs,
282
+ buildInstallArgs,
283
+ };
@@ -3,37 +3,77 @@
3
3
  const fs = require('fs');
4
4
  const path = require('path');
5
5
 
6
+ const {
7
+ clearBuildCache,
8
+ getBuildCacheRoot,
9
+ clearUploadSidecars,
10
+ } = require('../build/cache');
11
+ const log = require('../logger');
12
+
6
13
  /**
7
- * Removes the wyvrn_internal/ package cache directory and the wyvrn.lock file.
14
+ * Removes the project's `wyvrn_internal/` package cache and `wyvrn.lock` file.
15
+ * With `--build-cache`, also clears the machine-wide source-build cache
16
+ * under `%LOCALAPPDATA%\wyvrnpm\bc\` (shared across projects).
17
+ *
18
+ * `--uploaded-built` wipes just the consumer's local RECORD of what they
19
+ * have uploaded via `wyvrnpm install --build=missing --upload-built` (the
20
+ * `.uploaded-to.json` sidecars under the build cache). It does NOT touch
21
+ * the registry, the artefact zips, or the source clones. Privacy / local
22
+ * audit cleanup only.
8
23
  *
9
- * @param {object} argv - Parsed yargs arguments.
10
- * @param {string} argv.root - Project root directory.
11
- * @param {string} argv.manifest - Path to the manifest file (used to locate wyvrn.lock).
24
+ * @param {object} argv
25
+ * @param {string} argv.root Project root directory.
26
+ * @param {string} argv.manifest Path to the manifest file (used to locate wyvrn.lock).
27
+ * @param {boolean} [argv.buildCache] When true, also nukes the source-build cache.
28
+ * @param {boolean} [argv.uploadedBuilt] When true, wipes upload sidecars only.
12
29
  * @returns {Promise<void>}
13
30
  */
14
31
  async function clean(argv) {
15
- const rootDir = path.resolve(argv.root);
32
+ const rootDir = path.resolve(argv.root);
16
33
  const razerDir = path.join(rootDir, 'wyvrn_internal');
17
34
  const lockPath = path.join(path.dirname(path.resolve(argv.manifest)), 'wyvrn.lock');
18
35
 
36
+ // --uploaded-built is a targeted cleanup mode. When passed on its own,
37
+ // skip the default wyvrn_internal/ + wyvrn.lock scrub so a user can
38
+ // "forget uploads" without also wiping their project-local install.
39
+ const targetedOnly = argv.uploadedBuilt && !argv.buildCache;
40
+
19
41
  let removedAnything = false;
20
42
 
21
- if (fs.existsSync(razerDir)) {
22
- fs.rmSync(razerDir, { recursive: true, force: true });
23
- console.log(`[wyvrn] Removed ${razerDir}`);
24
- removedAnything = true;
25
- } else {
26
- console.log(`[wyvrn] Nothing to clean at ${razerDir}`);
43
+ if (!targetedOnly) {
44
+ if (fs.existsSync(razerDir)) {
45
+ fs.rmSync(razerDir, { recursive: true, force: true });
46
+ log.info(`Removed ${razerDir}`);
47
+ removedAnything = true;
48
+ } else {
49
+ log.info(`Nothing to clean at ${razerDir}`);
50
+ }
51
+
52
+ if (fs.existsSync(lockPath)) {
53
+ fs.unlinkSync(lockPath);
54
+ log.info(`Removed ${lockPath}`);
55
+ removedAnything = true;
56
+ }
27
57
  }
28
58
 
29
- if (fs.existsSync(lockPath)) {
30
- fs.unlinkSync(lockPath);
31
- console.log(`[wyvrn] Removed ${lockPath}`);
32
- removedAnything = true;
59
+ if (argv.buildCache) {
60
+ const removed = clearBuildCache();
61
+ if (removed) {
62
+ log.info(`Removed source-build cache ${removed}`);
63
+ removedAnything = true;
64
+ } else {
65
+ log.info(`No source-build cache at ${getBuildCacheRoot()}`);
66
+ }
67
+ } else if (argv.uploadedBuilt) {
68
+ // clearBuildCache() would have nuked the sidecars as a side-effect, so
69
+ // only bother with the sidecar sweep when the cache itself is surviving.
70
+ const removed = clearUploadSidecars();
71
+ log.info(`Removed ${removed} upload record${removed === 1 ? '' : 's'} from the build cache`);
72
+ if (removed > 0) removedAnything = true;
33
73
  }
34
74
 
35
75
  if (!removedAnything) {
36
- console.log('[wyvrn] Nothing to clean.');
76
+ log.info('Nothing to clean.');
37
77
  }
38
78
  }
39
79
 
@@ -1,6 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  const { readConfig, writeConfig, getConfigPath } = require('../config');
4
+ const log = require('../logger');
4
5
 
5
6
  /** wyvrnpm configure list */
6
7
  function list() {
@@ -47,14 +48,14 @@ function addSource(argv) {
47
48
  const idx = config[key].findIndex((s) => s.name === name);
48
49
  if (idx !== -1) {
49
50
  config[key][idx] = entry;
50
- console.log(`[wyvrn] Updated ${kind} source "${name}"`);
51
+ log.info(`Updated ${kind} source "${name}"`);
51
52
  } else {
52
53
  config[key].push(entry);
53
- console.log(`[wyvrn] Added ${kind} source "${name}"`);
54
+ log.info(`Added ${kind} source "${name}"`);
54
55
  }
55
56
 
56
57
  writeConfig(config);
57
- console.log(`[wyvrn] Saved to ${getConfigPath()}`);
58
+ log.info(`Saved to ${getConfigPath()}`);
58
59
  }
59
60
 
60
61
  /**
@@ -69,12 +70,12 @@ function removeSource(argv) {
69
70
  config[key] = config[key].filter((s) => s.name !== name);
70
71
 
71
72
  if (config[key].length === before) {
72
- console.error(`[wyvrn] Error: No ${kind} source named "${name}"`);
73
+ log.error(`No ${kind} source named "${name}"`);
73
74
  process.exit(1);
74
75
  }
75
76
 
76
77
  writeConfig(config);
77
- console.log(`[wyvrn] Removed ${kind} source "${name}"`);
78
+ log.info(`Removed ${kind} source "${name}"`);
78
79
  }
79
80
 
80
81
  module.exports = { list, addSource, removeSource };
@@ -3,6 +3,7 @@
3
3
  const fs = require('fs');
4
4
  const path = require('path');
5
5
  const { defaultManifest, writeManifest } = require('../manifest');
6
+ const log = require('../logger');
6
7
 
7
8
  /**
8
9
  * Initialises a new wyvrn.json manifest in the project root.
@@ -18,7 +19,7 @@ async function init(argv) {
18
19
  const rootDir = path.resolve(argv.root);
19
20
 
20
21
  if (fs.existsSync(manifestPath)) {
21
- console.log(`[wyvrn] Manifest already exists at ${manifestPath}`);
22
+ log.info(`Manifest already exists at ${manifestPath}`);
22
23
  return;
23
24
  }
24
25
 
@@ -27,7 +28,7 @@ async function init(argv) {
27
28
 
28
29
  writeManifest(manifestPath, manifest);
29
30
 
30
- console.log(`[wyvrn] Created manifest at ${manifestPath}`);
31
+ log.success(`Created manifest at ${manifestPath}`);
31
32
  }
32
33
 
33
34
  module.exports = init;
@@ -0,0 +1,107 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const os = require('os');
5
+ const path = require('path');
6
+ const AdmZip = require('adm-zip');
7
+
8
+ const log = require('../logger');
9
+
10
+ // ---------------------------------------------------------------------------
11
+ // `wyvrnpm install-skill --claude`
12
+ //
13
+ // Copies the bundled wyvrnpm Agent Skill (claude/skills/wyvrnpm.skill) into
14
+ // the user's Claude Code skills directory so Claude picks it up automatically.
15
+ //
16
+ // Works for both install shapes:
17
+ // - `npm i -g wyvrnpm` → skill zip ships under <pkg>/claude/skills/
18
+ // - `git clone` of this repo → same path, just not from node_modules
19
+ //
20
+ // Destination, per target:
21
+ // --claude → %USERPROFILE%\.claude\skills\wyvrnpm\ (Windows)
22
+ // ~/.claude/skills/wyvrnpm/ (macOS / Linux)
23
+ // ---------------------------------------------------------------------------
24
+
25
+ const SKILL_NAME = 'wyvrnpm';
26
+
27
+ function bundledSkillPath() {
28
+ // __dirname = <pkg>/src/commands; the .skill zip lives at
29
+ // <pkg>/claude/skills/wyvrnpm.skill.
30
+ return path.resolve(__dirname, '..', '..', 'claude', 'skills', `${SKILL_NAME}.skill`);
31
+ }
32
+
33
+ function claudeSkillsDir() {
34
+ const home = os.homedir();
35
+ if (!home) throw new Error('cannot resolve user home directory');
36
+ return path.join(home, '.claude', 'skills');
37
+ }
38
+
39
+ function rmrf(target) {
40
+ if (!fs.existsSync(target)) return;
41
+ fs.rmSync(target, { recursive: true, force: true });
42
+ }
43
+
44
+ function installForClaude({ force, dryRun }) {
45
+ const zipPath = bundledSkillPath();
46
+ if (!fs.existsSync(zipPath)) {
47
+ log.error(`bundled skill not found at ${zipPath}`);
48
+ log.error('this is a packaging bug — please file an issue');
49
+ process.exit(1);
50
+ }
51
+
52
+ const skillsDir = claudeSkillsDir();
53
+ const destDir = path.join(skillsDir, SKILL_NAME);
54
+ const destZip = path.join(skillsDir, `${SKILL_NAME}.skill`);
55
+
56
+ log.info(`Source : ${zipPath}`);
57
+ log.info(`Destination : ${destDir}`);
58
+
59
+ if (fs.existsSync(destDir) && !force) {
60
+ log.warn(`${destDir} already exists`);
61
+ log.warn('re-run with --force to overwrite');
62
+ process.exit(1);
63
+ }
64
+
65
+ if (dryRun) {
66
+ log.info('(--dry-run) would remove existing skill dir and zip, then extract fresh copy');
67
+ return;
68
+ }
69
+
70
+ fs.mkdirSync(skillsDir, { recursive: true });
71
+ rmrf(destDir);
72
+ rmrf(destZip);
73
+
74
+ const zip = new AdmZip(zipPath);
75
+ zip.extractAllTo(skillsDir, /* overwrite */ true);
76
+ fs.copyFileSync(zipPath, destZip);
77
+
78
+ if (!fs.existsSync(path.join(destDir, 'SKILL.md'))) {
79
+ log.error(`extraction produced no SKILL.md under ${destDir}`);
80
+ log.error('the bundled .skill zip may be malformed');
81
+ process.exit(1);
82
+ }
83
+
84
+ log.success(`Installed ${SKILL_NAME} skill to ${destDir}`);
85
+ log.info('Claude Code will pick it up on next session start.');
86
+ }
87
+
88
+ async function installSkill(argv) {
89
+ const targets = [];
90
+ if (argv.claude) targets.push('claude');
91
+
92
+ if (targets.length === 0) {
93
+ log.error('no target specified — pass --claude');
94
+ log.error('(more targets may be added in future releases)');
95
+ process.exit(1);
96
+ }
97
+
98
+ for (const target of targets) {
99
+ if (target === 'claude') {
100
+ installForClaude({ force: !!argv.force, dryRun: !!argv['dry-run'] });
101
+ }
102
+ }
103
+ }
104
+
105
+ module.exports = installSkill;
106
+ module.exports.bundledSkillPath = bundledSkillPath;
107
+ module.exports.claudeSkillsDir = claudeSkillsDir;