wyvrnpm 2.11.0 → 2.12.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/README.md +1914 -1905
- package/bin/{wyvrn.js → wyvrnpm.js} +31 -0
- package/cmake/cpp.cmake +9 -9
- package/cmake/functions.cmake +224 -224
- package/cmake/macros.cmake +284 -284
- package/package.json +3 -2
- package/src/auth.js +66 -66
- package/src/binary-dir.js +95 -0
- package/src/bootstrap/cookbook.js +196 -196
- package/src/bootstrap/detect.js +150 -150
- package/src/bootstrap/index.js +220 -220
- package/src/bootstrap/version.js +72 -72
- package/src/build/cache.js +344 -344
- package/src/build/clone.js +170 -170
- package/src/build/cmake.js +342 -342
- package/src/build/index.js +299 -297
- package/src/build/msvc-env.js +260 -260
- package/src/build/recipe.js +188 -188
- package/src/commands/add.js +141 -141
- package/src/commands/bootstrap.js +96 -96
- package/src/commands/build.js +482 -452
- package/src/commands/cache.js +189 -189
- package/src/commands/clean.js +80 -80
- package/src/commands/configure.js +92 -92
- package/src/commands/init.js +70 -70
- package/src/commands/install-skill.js +115 -115
- package/src/commands/install.js +730 -674
- package/src/commands/link.js +320 -320
- package/src/commands/profile.js +237 -237
- package/src/commands/publish.js +584 -555
- package/src/commands/show.js +252 -252
- package/src/commands/version.js +187 -187
- package/src/compat.js +273 -273
- package/src/conf/index.js +415 -391
- package/src/conf/namespaces.js +94 -94
- package/src/context.js +230 -230
- package/src/http-fetch.js +53 -53
- package/src/ignore.js +118 -118
- package/src/logger.js +122 -122
- package/src/options.js +303 -303
- package/src/settings-overrides.js +152 -152
- package/src/toolchain/deps.js +164 -164
- package/src/toolchain/index.js +212 -212
- package/src/toolchain/presets.js +356 -340
- package/src/toolchain/template.cmake +77 -77
- package/src/upload-built.js +265 -265
- package/src/version-range.js +301 -301
- package/src/zip-safe.js +52 -52
- package/src/zip-stream.js +126 -0
|
@@ -1,92 +1,92 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const { readConfig, writeConfig, getConfigPath } = require('../config');
|
|
4
|
-
const log = require('../logger');
|
|
5
|
-
|
|
6
|
-
/** wyvrnpm configure list */
|
|
7
|
-
function list() {
|
|
8
|
-
const config = readConfig();
|
|
9
|
-
|
|
10
|
-
console.log(`Config: ${getConfigPath()}\n`);
|
|
11
|
-
|
|
12
|
-
printSection('Install Sources', config.installSources);
|
|
13
|
-
console.log('');
|
|
14
|
-
printSection('Publish Sources', config.publishSources);
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
function printSection(title, sources) {
|
|
18
|
-
console.log(`${title}:`);
|
|
19
|
-
if (sources.length === 0) {
|
|
20
|
-
console.log(' (none)');
|
|
21
|
-
return;
|
|
22
|
-
}
|
|
23
|
-
for (const src of sources) {
|
|
24
|
-
const auth = authLabel(src);
|
|
25
|
-
console.log(` ${src.name.padEnd(20)} ${src.url}${auth ? ' ' + auth : ''}`);
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function authLabel(src) {
|
|
30
|
-
if (src.profile) return `[profile: ${src.profile}]`;
|
|
31
|
-
if (src.tokenEnv) return `[token-env: ${src.tokenEnv}]`;
|
|
32
|
-
if (src.token) return '[token: ***]';
|
|
33
|
-
return '';
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* wyvrnpm configure add-source --kind install|publish --name <name> --url <url>
|
|
38
|
-
* [--profile <profile>] [--token <token>]
|
|
39
|
-
*/
|
|
40
|
-
function addSource(argv) {
|
|
41
|
-
const { kind, name, url, profile, token } = argv;
|
|
42
|
-
// yargs delivers --token-env as argv['token-env']; accept both shapes
|
|
43
|
-
// so programmatic callers with camelCase also work.
|
|
44
|
-
const tokenEnv = argv.tokenEnv ?? argv['token-env'];
|
|
45
|
-
|
|
46
|
-
if (token && tokenEnv) {
|
|
47
|
-
log.error('Pass --token OR --token-env, not both. --token-env is preferred for CI and shared setups (EVALUATION.md S8).');
|
|
48
|
-
process.exit(1);
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
const config = readConfig();
|
|
52
|
-
const key = kind === 'install' ? 'installSources' : 'publishSources';
|
|
53
|
-
|
|
54
|
-
const entry = { name, url };
|
|
55
|
-
if (profile) entry.profile = profile;
|
|
56
|
-
if (token) entry.token = token;
|
|
57
|
-
if (tokenEnv) entry.tokenEnv = tokenEnv;
|
|
58
|
-
|
|
59
|
-
const idx = config[key].findIndex((s) => s.name === name);
|
|
60
|
-
if (idx !== -1) {
|
|
61
|
-
config[key][idx] = entry;
|
|
62
|
-
log.info(`Updated ${kind} source "${name}"`);
|
|
63
|
-
} else {
|
|
64
|
-
config[key].push(entry);
|
|
65
|
-
log.info(`Added ${kind} source "${name}"`);
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
writeConfig(config);
|
|
69
|
-
log.info(`Saved to ${getConfigPath()}`);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/**
|
|
73
|
-
* wyvrnpm configure remove-source --kind install|publish --name <name>
|
|
74
|
-
*/
|
|
75
|
-
function removeSource(argv) {
|
|
76
|
-
const { kind, name } = argv;
|
|
77
|
-
const config = readConfig();
|
|
78
|
-
const key = kind === 'install' ? 'installSources' : 'publishSources';
|
|
79
|
-
|
|
80
|
-
const before = config[key].length;
|
|
81
|
-
config[key] = config[key].filter((s) => s.name !== name);
|
|
82
|
-
|
|
83
|
-
if (config[key].length === before) {
|
|
84
|
-
log.error(`No ${kind} source named "${name}"`);
|
|
85
|
-
process.exit(1);
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
writeConfig(config);
|
|
89
|
-
log.info(`Removed ${kind} source "${name}"`);
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
module.exports = { list, addSource, removeSource };
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { readConfig, writeConfig, getConfigPath } = require('../config');
|
|
4
|
+
const log = require('../logger');
|
|
5
|
+
|
|
6
|
+
/** wyvrnpm configure list */
|
|
7
|
+
function list() {
|
|
8
|
+
const config = readConfig();
|
|
9
|
+
|
|
10
|
+
console.log(`Config: ${getConfigPath()}\n`);
|
|
11
|
+
|
|
12
|
+
printSection('Install Sources', config.installSources);
|
|
13
|
+
console.log('');
|
|
14
|
+
printSection('Publish Sources', config.publishSources);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function printSection(title, sources) {
|
|
18
|
+
console.log(`${title}:`);
|
|
19
|
+
if (sources.length === 0) {
|
|
20
|
+
console.log(' (none)');
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
for (const src of sources) {
|
|
24
|
+
const auth = authLabel(src);
|
|
25
|
+
console.log(` ${src.name.padEnd(20)} ${src.url}${auth ? ' ' + auth : ''}`);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function authLabel(src) {
|
|
30
|
+
if (src.profile) return `[profile: ${src.profile}]`;
|
|
31
|
+
if (src.tokenEnv) return `[token-env: ${src.tokenEnv}]`;
|
|
32
|
+
if (src.token) return '[token: ***]';
|
|
33
|
+
return '';
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* wyvrnpm configure add-source --kind install|publish --name <name> --url <url>
|
|
38
|
+
* [--profile <profile>] [--token <token>]
|
|
39
|
+
*/
|
|
40
|
+
function addSource(argv) {
|
|
41
|
+
const { kind, name, url, profile, token } = argv;
|
|
42
|
+
// yargs delivers --token-env as argv['token-env']; accept both shapes
|
|
43
|
+
// so programmatic callers with camelCase also work.
|
|
44
|
+
const tokenEnv = argv.tokenEnv ?? argv['token-env'];
|
|
45
|
+
|
|
46
|
+
if (token && tokenEnv) {
|
|
47
|
+
log.error('Pass --token OR --token-env, not both. --token-env is preferred for CI and shared setups (EVALUATION.md S8).');
|
|
48
|
+
process.exit(1);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const config = readConfig();
|
|
52
|
+
const key = kind === 'install' ? 'installSources' : 'publishSources';
|
|
53
|
+
|
|
54
|
+
const entry = { name, url };
|
|
55
|
+
if (profile) entry.profile = profile;
|
|
56
|
+
if (token) entry.token = token;
|
|
57
|
+
if (tokenEnv) entry.tokenEnv = tokenEnv;
|
|
58
|
+
|
|
59
|
+
const idx = config[key].findIndex((s) => s.name === name);
|
|
60
|
+
if (idx !== -1) {
|
|
61
|
+
config[key][idx] = entry;
|
|
62
|
+
log.info(`Updated ${kind} source "${name}"`);
|
|
63
|
+
} else {
|
|
64
|
+
config[key].push(entry);
|
|
65
|
+
log.info(`Added ${kind} source "${name}"`);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
writeConfig(config);
|
|
69
|
+
log.info(`Saved to ${getConfigPath()}`);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* wyvrnpm configure remove-source --kind install|publish --name <name>
|
|
74
|
+
*/
|
|
75
|
+
function removeSource(argv) {
|
|
76
|
+
const { kind, name } = argv;
|
|
77
|
+
const config = readConfig();
|
|
78
|
+
const key = kind === 'install' ? 'installSources' : 'publishSources';
|
|
79
|
+
|
|
80
|
+
const before = config[key].length;
|
|
81
|
+
config[key] = config[key].filter((s) => s.name !== name);
|
|
82
|
+
|
|
83
|
+
if (config[key].length === before) {
|
|
84
|
+
log.error(`No ${kind} source named "${name}"`);
|
|
85
|
+
process.exit(1);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
writeConfig(config);
|
|
89
|
+
log.info(`Removed ${kind} source "${name}"`);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
module.exports = { list, addSource, removeSource };
|
package/src/commands/init.js
CHANGED
|
@@ -1,70 +1,70 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const fs = require('fs');
|
|
4
|
-
const path = require('path');
|
|
5
|
-
const { defaultManifest, writeManifest } = require('../manifest');
|
|
6
|
-
const { LOCAL_OVERLAY_FILENAME } = require('../conf');
|
|
7
|
-
const log = require('../logger');
|
|
8
|
-
|
|
9
|
-
// The wyvrn.local.json overlay must never be committed — it is by design
|
|
10
|
-
// per-developer state. init writes a gitignore entry; existing projects
|
|
11
|
-
// get a one-line nudge from install (see install.js) when the file is
|
|
12
|
-
// present but not ignored.
|
|
13
|
-
const GITIGNORE_MARKER_LINE = '# wyvrnpm — per-developer conf overlay (never commit)';
|
|
14
|
-
|
|
15
|
-
function ensureGitignoreEntry(rootDir) {
|
|
16
|
-
const gitignorePath = path.join(rootDir, '.gitignore');
|
|
17
|
-
const desiredLines = [GITIGNORE_MARKER_LINE, LOCAL_OVERLAY_FILENAME];
|
|
18
|
-
|
|
19
|
-
let existing = '';
|
|
20
|
-
if (fs.existsSync(gitignorePath)) {
|
|
21
|
-
existing = fs.readFileSync(gitignorePath, 'utf8');
|
|
22
|
-
const hasEntry = existing.split(/\r?\n/).some((l) => l.trim() === LOCAL_OVERLAY_FILENAME);
|
|
23
|
-
if (hasEntry) return { action: 'already-present', path: gitignorePath };
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
const needsLeadingNewline = existing.length > 0 && !existing.endsWith('\n');
|
|
27
|
-
const block = (needsLeadingNewline ? '\n' : '') + desiredLines.join('\n') + '\n';
|
|
28
|
-
fs.writeFileSync(gitignorePath, existing + block, 'utf8');
|
|
29
|
-
return {
|
|
30
|
-
action: existing.length > 0 ? 'appended' : 'created',
|
|
31
|
-
path: gitignorePath,
|
|
32
|
-
};
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Initialises a new wyvrn.json manifest in the project root.
|
|
37
|
-
* If a manifest already exists the command exits early without overwriting it.
|
|
38
|
-
*
|
|
39
|
-
* @param {object} argv - Parsed yargs arguments.
|
|
40
|
-
* @param {string} argv.manifest - Path to the manifest file.
|
|
41
|
-
* @param {string} argv.root - Project root directory.
|
|
42
|
-
* @returns {Promise<void>}
|
|
43
|
-
*/
|
|
44
|
-
async function init(argv) {
|
|
45
|
-
const manifestPath = path.resolve(argv.manifest);
|
|
46
|
-
const rootDir = path.resolve(argv.root);
|
|
47
|
-
|
|
48
|
-
if (fs.existsSync(manifestPath)) {
|
|
49
|
-
log.info(`Manifest already exists at ${manifestPath}`);
|
|
50
|
-
return;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
const projectName = path.basename(rootDir);
|
|
54
|
-
const manifest = defaultManifest(projectName);
|
|
55
|
-
|
|
56
|
-
writeManifest(manifestPath, manifest);
|
|
57
|
-
|
|
58
|
-
const gitignore = ensureGitignoreEntry(rootDir);
|
|
59
|
-
if (gitignore.action === 'created') {
|
|
60
|
-
log.info(`Created .gitignore at ${gitignore.path} with ${LOCAL_OVERLAY_FILENAME} exclusion`);
|
|
61
|
-
} else if (gitignore.action === 'appended') {
|
|
62
|
-
log.info(`Added ${LOCAL_OVERLAY_FILENAME} to ${gitignore.path}`);
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
log.success(`Created manifest at ${manifestPath}`);
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
module.exports = init;
|
|
69
|
-
module.exports.ensureGitignoreEntry = ensureGitignoreEntry;
|
|
70
|
-
module.exports.GITIGNORE_MARKER_LINE = GITIGNORE_MARKER_LINE;
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { defaultManifest, writeManifest } = require('../manifest');
|
|
6
|
+
const { LOCAL_OVERLAY_FILENAME } = require('../conf');
|
|
7
|
+
const log = require('../logger');
|
|
8
|
+
|
|
9
|
+
// The wyvrn.local.json overlay must never be committed — it is by design
|
|
10
|
+
// per-developer state. init writes a gitignore entry; existing projects
|
|
11
|
+
// get a one-line nudge from install (see install.js) when the file is
|
|
12
|
+
// present but not ignored.
|
|
13
|
+
const GITIGNORE_MARKER_LINE = '# wyvrnpm — per-developer conf overlay (never commit)';
|
|
14
|
+
|
|
15
|
+
function ensureGitignoreEntry(rootDir) {
|
|
16
|
+
const gitignorePath = path.join(rootDir, '.gitignore');
|
|
17
|
+
const desiredLines = [GITIGNORE_MARKER_LINE, LOCAL_OVERLAY_FILENAME];
|
|
18
|
+
|
|
19
|
+
let existing = '';
|
|
20
|
+
if (fs.existsSync(gitignorePath)) {
|
|
21
|
+
existing = fs.readFileSync(gitignorePath, 'utf8');
|
|
22
|
+
const hasEntry = existing.split(/\r?\n/).some((l) => l.trim() === LOCAL_OVERLAY_FILENAME);
|
|
23
|
+
if (hasEntry) return { action: 'already-present', path: gitignorePath };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const needsLeadingNewline = existing.length > 0 && !existing.endsWith('\n');
|
|
27
|
+
const block = (needsLeadingNewline ? '\n' : '') + desiredLines.join('\n') + '\n';
|
|
28
|
+
fs.writeFileSync(gitignorePath, existing + block, 'utf8');
|
|
29
|
+
return {
|
|
30
|
+
action: existing.length > 0 ? 'appended' : 'created',
|
|
31
|
+
path: gitignorePath,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Initialises a new wyvrn.json manifest in the project root.
|
|
37
|
+
* If a manifest already exists the command exits early without overwriting it.
|
|
38
|
+
*
|
|
39
|
+
* @param {object} argv - Parsed yargs arguments.
|
|
40
|
+
* @param {string} argv.manifest - Path to the manifest file.
|
|
41
|
+
* @param {string} argv.root - Project root directory.
|
|
42
|
+
* @returns {Promise<void>}
|
|
43
|
+
*/
|
|
44
|
+
async function init(argv) {
|
|
45
|
+
const manifestPath = path.resolve(argv.manifest);
|
|
46
|
+
const rootDir = path.resolve(argv.root);
|
|
47
|
+
|
|
48
|
+
if (fs.existsSync(manifestPath)) {
|
|
49
|
+
log.info(`Manifest already exists at ${manifestPath}`);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const projectName = path.basename(rootDir);
|
|
54
|
+
const manifest = defaultManifest(projectName);
|
|
55
|
+
|
|
56
|
+
writeManifest(manifestPath, manifest);
|
|
57
|
+
|
|
58
|
+
const gitignore = ensureGitignoreEntry(rootDir);
|
|
59
|
+
if (gitignore.action === 'created') {
|
|
60
|
+
log.info(`Created .gitignore at ${gitignore.path} with ${LOCAL_OVERLAY_FILENAME} exclusion`);
|
|
61
|
+
} else if (gitignore.action === 'appended') {
|
|
62
|
+
log.info(`Added ${LOCAL_OVERLAY_FILENAME} to ${gitignore.path}`);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
log.success(`Created manifest at ${manifestPath}`);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
module.exports = init;
|
|
69
|
+
module.exports.ensureGitignoreEntry = ensureGitignoreEntry;
|
|
70
|
+
module.exports.GITIGNORE_MARKER_LINE = GITIGNORE_MARKER_LINE;
|
|
@@ -1,115 +1,115 @@
|
|
|
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 { assertAllSafeZipEntryNames } = require('../zip-safe');
|
|
9
|
-
const log = require('../logger');
|
|
10
|
-
|
|
11
|
-
// ---------------------------------------------------------------------------
|
|
12
|
-
// `wyvrnpm install-skill --claude`
|
|
13
|
-
//
|
|
14
|
-
// Copies the bundled wyvrnpm Agent Skill (claude/skills/wyvrnpm.skill) into
|
|
15
|
-
// the user's Claude Code skills directory so Claude picks it up automatically.
|
|
16
|
-
//
|
|
17
|
-
// Works for both install shapes:
|
|
18
|
-
// - `npm i -g wyvrnpm` → skill zip ships under <pkg>/claude/skills/
|
|
19
|
-
// - `git clone` of this repo → same path, just not from node_modules
|
|
20
|
-
//
|
|
21
|
-
// Destination, per target:
|
|
22
|
-
// --claude → %USERPROFILE%\.claude\skills\wyvrnpm\ (Windows)
|
|
23
|
-
// ~/.claude/skills/wyvrnpm/ (macOS / Linux)
|
|
24
|
-
// ---------------------------------------------------------------------------
|
|
25
|
-
|
|
26
|
-
const SKILL_NAME = 'wyvrnpm';
|
|
27
|
-
|
|
28
|
-
function bundledSkillPath() {
|
|
29
|
-
// __dirname = <pkg>/src/commands; the .skill zip lives at
|
|
30
|
-
// <pkg>/claude/skills/wyvrnpm.skill.
|
|
31
|
-
return path.resolve(__dirname, '..', '..', 'claude', 'skills', `${SKILL_NAME}.skill`);
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
function claudeSkillsDir() {
|
|
35
|
-
const home = os.homedir();
|
|
36
|
-
if (!home) throw new Error('cannot resolve user home directory');
|
|
37
|
-
return path.join(home, '.claude', 'skills');
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
function rmrf(target) {
|
|
41
|
-
if (!fs.existsSync(target)) return;
|
|
42
|
-
fs.rmSync(target, { recursive: true, force: true });
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function installForClaude({ force, dryRun }) {
|
|
46
|
-
const zipPath = bundledSkillPath();
|
|
47
|
-
if (!fs.existsSync(zipPath)) {
|
|
48
|
-
log.error(`bundled skill not found at ${zipPath}`);
|
|
49
|
-
log.error('this is a packaging bug — please file an issue');
|
|
50
|
-
process.exit(1);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
const skillsDir = claudeSkillsDir();
|
|
54
|
-
const destDir = path.join(skillsDir, SKILL_NAME);
|
|
55
|
-
const destZip = path.join(skillsDir, `${SKILL_NAME}.skill`);
|
|
56
|
-
|
|
57
|
-
log.info(`Source : ${zipPath}`);
|
|
58
|
-
log.info(`Destination : ${destDir}`);
|
|
59
|
-
|
|
60
|
-
if (fs.existsSync(destDir) && !force) {
|
|
61
|
-
log.warn(`${destDir} already exists`);
|
|
62
|
-
log.warn('re-run with --force to overwrite');
|
|
63
|
-
process.exit(1);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
if (dryRun) {
|
|
67
|
-
log.info('(--dry-run) would remove existing skill dir and zip, then extract fresh copy');
|
|
68
|
-
return;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
fs.mkdirSync(skillsDir, { recursive: true });
|
|
72
|
-
rmrf(destDir);
|
|
73
|
-
rmrf(destZip);
|
|
74
|
-
|
|
75
|
-
const zip = new AdmZip(zipPath);
|
|
76
|
-
// Zip Slip defense-in-depth (EVALUATION.md S2). The shipped .skill zip
|
|
77
|
-
// is trusted-by-provenance, but validating consistently keeps the
|
|
78
|
-
// attack surface flat across all extraction call sites.
|
|
79
|
-
assertAllSafeZipEntryNames(
|
|
80
|
-
zip.getEntries().map((e) => e.entryName),
|
|
81
|
-
`${SKILL_NAME}.skill`,
|
|
82
|
-
);
|
|
83
|
-
zip.extractAllTo(skillsDir, /* overwrite */ true);
|
|
84
|
-
fs.copyFileSync(zipPath, destZip);
|
|
85
|
-
|
|
86
|
-
if (!fs.existsSync(path.join(destDir, 'SKILL.md'))) {
|
|
87
|
-
log.error(`extraction produced no SKILL.md under ${destDir}`);
|
|
88
|
-
log.error('the bundled .skill zip may be malformed');
|
|
89
|
-
process.exit(1);
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
log.success(`Installed ${SKILL_NAME} skill to ${destDir}`);
|
|
93
|
-
log.info('Claude Code will pick it up on next session start.');
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
async function installSkill(argv) {
|
|
97
|
-
const targets = [];
|
|
98
|
-
if (argv.claude) targets.push('claude');
|
|
99
|
-
|
|
100
|
-
if (targets.length === 0) {
|
|
101
|
-
log.error('no target specified — pass --claude');
|
|
102
|
-
log.error('(more targets may be added in future releases)');
|
|
103
|
-
process.exit(1);
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
for (const target of targets) {
|
|
107
|
-
if (target === 'claude') {
|
|
108
|
-
installForClaude({ force: !!argv.force, dryRun: !!argv['dry-run'] });
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
module.exports = installSkill;
|
|
114
|
-
module.exports.bundledSkillPath = bundledSkillPath;
|
|
115
|
-
module.exports.claudeSkillsDir = claudeSkillsDir;
|
|
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 { assertAllSafeZipEntryNames } = require('../zip-safe');
|
|
9
|
+
const log = require('../logger');
|
|
10
|
+
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
// `wyvrnpm install-skill --claude`
|
|
13
|
+
//
|
|
14
|
+
// Copies the bundled wyvrnpm Agent Skill (claude/skills/wyvrnpm.skill) into
|
|
15
|
+
// the user's Claude Code skills directory so Claude picks it up automatically.
|
|
16
|
+
//
|
|
17
|
+
// Works for both install shapes:
|
|
18
|
+
// - `npm i -g wyvrnpm` → skill zip ships under <pkg>/claude/skills/
|
|
19
|
+
// - `git clone` of this repo → same path, just not from node_modules
|
|
20
|
+
//
|
|
21
|
+
// Destination, per target:
|
|
22
|
+
// --claude → %USERPROFILE%\.claude\skills\wyvrnpm\ (Windows)
|
|
23
|
+
// ~/.claude/skills/wyvrnpm/ (macOS / Linux)
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
|
|
26
|
+
const SKILL_NAME = 'wyvrnpm';
|
|
27
|
+
|
|
28
|
+
function bundledSkillPath() {
|
|
29
|
+
// __dirname = <pkg>/src/commands; the .skill zip lives at
|
|
30
|
+
// <pkg>/claude/skills/wyvrnpm.skill.
|
|
31
|
+
return path.resolve(__dirname, '..', '..', 'claude', 'skills', `${SKILL_NAME}.skill`);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function claudeSkillsDir() {
|
|
35
|
+
const home = os.homedir();
|
|
36
|
+
if (!home) throw new Error('cannot resolve user home directory');
|
|
37
|
+
return path.join(home, '.claude', 'skills');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function rmrf(target) {
|
|
41
|
+
if (!fs.existsSync(target)) return;
|
|
42
|
+
fs.rmSync(target, { recursive: true, force: true });
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function installForClaude({ force, dryRun }) {
|
|
46
|
+
const zipPath = bundledSkillPath();
|
|
47
|
+
if (!fs.existsSync(zipPath)) {
|
|
48
|
+
log.error(`bundled skill not found at ${zipPath}`);
|
|
49
|
+
log.error('this is a packaging bug — please file an issue');
|
|
50
|
+
process.exit(1);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const skillsDir = claudeSkillsDir();
|
|
54
|
+
const destDir = path.join(skillsDir, SKILL_NAME);
|
|
55
|
+
const destZip = path.join(skillsDir, `${SKILL_NAME}.skill`);
|
|
56
|
+
|
|
57
|
+
log.info(`Source : ${zipPath}`);
|
|
58
|
+
log.info(`Destination : ${destDir}`);
|
|
59
|
+
|
|
60
|
+
if (fs.existsSync(destDir) && !force) {
|
|
61
|
+
log.warn(`${destDir} already exists`);
|
|
62
|
+
log.warn('re-run with --force to overwrite');
|
|
63
|
+
process.exit(1);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (dryRun) {
|
|
67
|
+
log.info('(--dry-run) would remove existing skill dir and zip, then extract fresh copy');
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
fs.mkdirSync(skillsDir, { recursive: true });
|
|
72
|
+
rmrf(destDir);
|
|
73
|
+
rmrf(destZip);
|
|
74
|
+
|
|
75
|
+
const zip = new AdmZip(zipPath);
|
|
76
|
+
// Zip Slip defense-in-depth (EVALUATION.md S2). The shipped .skill zip
|
|
77
|
+
// is trusted-by-provenance, but validating consistently keeps the
|
|
78
|
+
// attack surface flat across all extraction call sites.
|
|
79
|
+
assertAllSafeZipEntryNames(
|
|
80
|
+
zip.getEntries().map((e) => e.entryName),
|
|
81
|
+
`${SKILL_NAME}.skill`,
|
|
82
|
+
);
|
|
83
|
+
zip.extractAllTo(skillsDir, /* overwrite */ true);
|
|
84
|
+
fs.copyFileSync(zipPath, destZip);
|
|
85
|
+
|
|
86
|
+
if (!fs.existsSync(path.join(destDir, 'SKILL.md'))) {
|
|
87
|
+
log.error(`extraction produced no SKILL.md under ${destDir}`);
|
|
88
|
+
log.error('the bundled .skill zip may be malformed');
|
|
89
|
+
process.exit(1);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
log.success(`Installed ${SKILL_NAME} skill to ${destDir}`);
|
|
93
|
+
log.info('Claude Code will pick it up on next session start.');
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async function installSkill(argv) {
|
|
97
|
+
const targets = [];
|
|
98
|
+
if (argv.claude) targets.push('claude');
|
|
99
|
+
|
|
100
|
+
if (targets.length === 0) {
|
|
101
|
+
log.error('no target specified — pass --claude');
|
|
102
|
+
log.error('(more targets may be added in future releases)');
|
|
103
|
+
process.exit(1);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
for (const target of targets) {
|
|
107
|
+
if (target === 'claude') {
|
|
108
|
+
installForClaude({ force: !!argv.force, dryRun: !!argv['dry-run'] });
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
module.exports = installSkill;
|
|
114
|
+
module.exports.bundledSkillPath = bundledSkillPath;
|
|
115
|
+
module.exports.claudeSkillsDir = claudeSkillsDir;
|