any-skills 0.1.0 → 0.1.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 +56 -2
- package/package.json +4 -1
- package/scripts/cli.js +58 -0
- package/scripts/link-skills.js +384 -0
- package/scripts/postinstall.js +9 -256
package/README.md
CHANGED
|
@@ -7,7 +7,9 @@ When installed as a dependency, this package creates a shared skills directory (
|
|
|
7
7
|
|
|
8
8
|
## How it works
|
|
9
9
|
|
|
10
|
-
The `postinstall` script runs at install time. It uses the user's install working directory as the root (prefers `INIT_CWD`, then `npm_config_local_prefix`, and falls back to `process.cwd()`). If the shared skills directory does not exist, it is created, then the tool-specific symlinks are generated (unless overridden by configuration).
|
|
10
|
+
The `postinstall` script runs at install time. It uses the user's install working directory as the root (prefers `INIT_CWD`, then `npm_config_local_prefix`, and falls back to `process.cwd()`). If the shared skills directory does not exist, it is created, then the tool-specific symlinks are generated (unless overridden by configuration). By default it auto-detects the `claude`, `codex`, and `gemini` commands on your PATH.
|
|
11
|
+
|
|
12
|
+
When installed globally, the postinstall step is skipped. Instead, run the CLI in the project you want to link.
|
|
11
13
|
|
|
12
14
|
## Configuration
|
|
13
15
|
|
|
@@ -17,6 +19,8 @@ Supported fields:
|
|
|
17
19
|
|
|
18
20
|
- `target`: optional string. Overrides where skills are stored (default: `.skills`).
|
|
19
21
|
- `links`: array of link definitions. Strings map to `target` by default.
|
|
22
|
+
- `tools`: optional object mapping command names to link paths, used for auto-detection.
|
|
23
|
+
- `linkRoot`: optional string. Controls where links are created (`cwd`, `home`, `~`, or a path).
|
|
20
24
|
|
|
21
25
|
Example:
|
|
22
26
|
|
|
@@ -27,6 +31,26 @@ Example:
|
|
|
27
31
|
}
|
|
28
32
|
```
|
|
29
33
|
|
|
34
|
+
Example (auto-detect commands):
|
|
35
|
+
|
|
36
|
+
```json
|
|
37
|
+
{
|
|
38
|
+
"tools": {
|
|
39
|
+
"claude": ".claude/skills",
|
|
40
|
+
"codex": ".codex/skills",
|
|
41
|
+
"gemini": ".gemini/skills"
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Example (link into home directory):
|
|
47
|
+
|
|
48
|
+
```json
|
|
49
|
+
{
|
|
50
|
+
"linkRoot": "home"
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
30
54
|
## Cross-platform behavior
|
|
31
55
|
|
|
32
56
|
- macOS / Linux uses `dir` symlinks
|
|
@@ -35,5 +59,35 @@ Example:
|
|
|
35
59
|
## Usage
|
|
36
60
|
|
|
37
61
|
```sh
|
|
38
|
-
npm install any-skills
|
|
62
|
+
npm install any-skills --save-dev
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Global usage
|
|
66
|
+
|
|
67
|
+
```sh
|
|
68
|
+
npm install -g any-skills
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Then, in the project directory where you want links created:
|
|
72
|
+
|
|
73
|
+
```sh
|
|
74
|
+
npx any-skills
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
If the project does not list `any-skills` as a dependency, the CLI defaults to linking into your home directory (for example `~/.claude/skills`). Use `linkRoot` in `.skillsrc` to override.
|
|
78
|
+
|
|
79
|
+
You can also pass tool names to only create those links:
|
|
80
|
+
|
|
81
|
+
```sh
|
|
82
|
+
npx any-skills claude
|
|
83
|
+
npx any-skills deepseek
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Git ignore
|
|
87
|
+
|
|
88
|
+
Add the tool-specific shared skill directories to your `.gitignore`:
|
|
89
|
+
|
|
90
|
+
```
|
|
91
|
+
.claude/skills
|
|
92
|
+
.codex/skills
|
|
39
93
|
```
|
package/package.json
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "any-skills",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Share skills between Claude, Codex, and similar tools via a shared .skills directory.",
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"scripts": {
|
|
7
7
|
"postinstall": "node scripts/postinstall.js"
|
|
8
|
+
},
|
|
9
|
+
"bin": {
|
|
10
|
+
"any-skills": "scripts/cli.js"
|
|
8
11
|
}
|
|
9
12
|
}
|
package/scripts/cli.js
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const os = require('os');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
const { linkSkills } = require('./link-skills');
|
|
8
|
+
|
|
9
|
+
function readPackageJson(filePath) {
|
|
10
|
+
try {
|
|
11
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
12
|
+
} catch (err) {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function hasAnySkillsDependency(pkg) {
|
|
18
|
+
const dependencyFields = [
|
|
19
|
+
'dependencies',
|
|
20
|
+
'devDependencies',
|
|
21
|
+
'optionalDependencies',
|
|
22
|
+
'peerDependencies',
|
|
23
|
+
];
|
|
24
|
+
for (const field of dependencyFields) {
|
|
25
|
+
if (pkg && pkg[field] && pkg[field]['any-skills']) {
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function hasLocalInstall(startDir) {
|
|
33
|
+
let current = path.resolve(startDir);
|
|
34
|
+
while (true) {
|
|
35
|
+
const pkgPath = path.join(current, 'package.json');
|
|
36
|
+
if (fs.existsSync(pkgPath)) {
|
|
37
|
+
const pkg = readPackageJson(pkgPath);
|
|
38
|
+
if (hasAnySkillsDependency(pkg)) {
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const parent = path.dirname(current);
|
|
44
|
+
if (parent === current) {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
current = parent;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const rootDir = process.cwd();
|
|
52
|
+
const linkRoot = hasLocalInstall(rootDir) ? rootDir : os.homedir();
|
|
53
|
+
const args = process.argv.slice(2).filter(Boolean);
|
|
54
|
+
const explicitTools = args.length ? args : null;
|
|
55
|
+
const exitCode = linkSkills(rootDir, { linkRoot, explicitTools });
|
|
56
|
+
if (exitCode) {
|
|
57
|
+
process.exit(exitCode);
|
|
58
|
+
}
|
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const os = require('os');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
|
|
7
|
+
const targetName = '.skills';
|
|
8
|
+
const configFileName = '.skillsrc';
|
|
9
|
+
const defaultToolTargets = {
|
|
10
|
+
claude: '.claude/skills',
|
|
11
|
+
codex: '.codex/skills',
|
|
12
|
+
gemini: '.gemini/skills',
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
function ensureDir(dirPath) {
|
|
16
|
+
if (!fs.existsSync(dirPath)) {
|
|
17
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function ensureTarget(dirPath) {
|
|
22
|
+
if (!fs.existsSync(dirPath)) {
|
|
23
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const stat = fs.lstatSync(dirPath);
|
|
28
|
+
if (stat.isSymbolicLink()) {
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (!stat.isDirectory()) {
|
|
33
|
+
console.error(`[any-skills] ${dirPath} exists and is not a directory.`);
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function getSymlinkType() {
|
|
41
|
+
return process.platform === 'win32' ? 'junction' : 'dir';
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function resolveSymlinkTarget(linkPath) {
|
|
45
|
+
const linkParent = path.dirname(linkPath);
|
|
46
|
+
const currentTarget = fs.readlinkSync(linkPath);
|
|
47
|
+
return path.resolve(linkParent, currentTarget);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function ensureSymlink(linkPath, targetPath) {
|
|
51
|
+
const linkParent = path.dirname(linkPath);
|
|
52
|
+
ensureDir(linkParent);
|
|
53
|
+
|
|
54
|
+
const resolvedLinkPath = path.resolve(linkPath);
|
|
55
|
+
const expectedTarget = path.resolve(targetPath);
|
|
56
|
+
if (resolvedLinkPath === expectedTarget) {
|
|
57
|
+
console.warn(`[any-skills] ${linkPath} resolves to the target; skipping.`);
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
const stat = fs.lstatSync(linkPath);
|
|
63
|
+
if (stat.isSymbolicLink()) {
|
|
64
|
+
const resolvedTarget = resolveSymlinkTarget(linkPath);
|
|
65
|
+
if (resolvedTarget === expectedTarget) {
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
fs.unlinkSync(linkPath);
|
|
69
|
+
} else {
|
|
70
|
+
console.warn(
|
|
71
|
+
`[any-skills] ${linkPath} exists and is not a symlink; skipping.`
|
|
72
|
+
);
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
} catch (err) {
|
|
76
|
+
if (err.code !== 'ENOENT') {
|
|
77
|
+
throw err;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const relativeTarget = path.relative(linkParent, targetPath) || '.';
|
|
82
|
+
fs.symlinkSync(relativeTarget, linkPath, getSymlinkType());
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function readSkillsConfig(configRoot) {
|
|
87
|
+
const configPath = path.join(configRoot, configFileName);
|
|
88
|
+
let rawConfig = null;
|
|
89
|
+
|
|
90
|
+
try {
|
|
91
|
+
rawConfig = fs.readFileSync(configPath, 'utf8');
|
|
92
|
+
} catch (err) {
|
|
93
|
+
if (err.code === 'ENOENT') {
|
|
94
|
+
return { config: null, configPath, exists: false, error: null };
|
|
95
|
+
}
|
|
96
|
+
throw err;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
let parsed = null;
|
|
100
|
+
try {
|
|
101
|
+
parsed = JSON.parse(rawConfig);
|
|
102
|
+
} catch (err) {
|
|
103
|
+
return { config: null, configPath, exists: true, error: err };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
107
|
+
return {
|
|
108
|
+
config: null,
|
|
109
|
+
configPath,
|
|
110
|
+
exists: true,
|
|
111
|
+
error: new Error('Config must be a JSON object.'),
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return { config: parsed, configPath, exists: true, error: null };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function resolveRootPath(value, baseDir) {
|
|
119
|
+
return path.isAbsolute(value) ? value : path.join(baseDir, value);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function resolveLinkRoot(config, rootDir, defaultLinkRoot) {
|
|
123
|
+
if (
|
|
124
|
+
config &&
|
|
125
|
+
typeof config.linkRoot === 'string' &&
|
|
126
|
+
config.linkRoot.trim() !== ''
|
|
127
|
+
) {
|
|
128
|
+
const raw = config.linkRoot.trim();
|
|
129
|
+
if (raw === 'home') {
|
|
130
|
+
return os.homedir();
|
|
131
|
+
}
|
|
132
|
+
if (raw === 'cwd') {
|
|
133
|
+
return rootDir;
|
|
134
|
+
}
|
|
135
|
+
if (raw === '~' || raw.startsWith('~/')) {
|
|
136
|
+
const suffix = raw.length > 2 ? raw.slice(2) : '';
|
|
137
|
+
return path.join(os.homedir(), suffix);
|
|
138
|
+
}
|
|
139
|
+
return resolveRootPath(raw, rootDir);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return defaultLinkRoot || rootDir;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function resolveTarget(config, rootDir) {
|
|
146
|
+
if (
|
|
147
|
+
config &&
|
|
148
|
+
typeof config.target === 'string' &&
|
|
149
|
+
config.target.trim() !== ''
|
|
150
|
+
) {
|
|
151
|
+
return resolveRootPath(config.target, rootDir);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return path.join(rootDir, targetName);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function normalizeLinkEntry(entry, target, configPath, rootDir) {
|
|
158
|
+
if (typeof entry === 'string') {
|
|
159
|
+
return { link: entry, error: false };
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (entry && typeof entry === 'object') {
|
|
163
|
+
const link = entry.link;
|
|
164
|
+
if (typeof link !== 'string' || link.trim() === '') {
|
|
165
|
+
console.warn(
|
|
166
|
+
`[any-skills] Invalid link entry in ${configPath}; skipping.`
|
|
167
|
+
);
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (Object.prototype.hasOwnProperty.call(entry, 'target')) {
|
|
172
|
+
const targetValue = entry.target;
|
|
173
|
+
if (typeof targetValue !== 'string' || targetValue.trim() === '') {
|
|
174
|
+
console.error(
|
|
175
|
+
`[any-skills] Invalid target for ${link} in ${configPath}.`
|
|
176
|
+
);
|
|
177
|
+
return { error: true };
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const resolvedTarget = resolveRootPath(targetValue, rootDir);
|
|
181
|
+
if (resolvedTarget !== target) {
|
|
182
|
+
console.error(
|
|
183
|
+
`[any-skills] Link entry in ${configPath} must not set a different target.`
|
|
184
|
+
);
|
|
185
|
+
return { error: true };
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
console.warn(
|
|
189
|
+
`[any-skills] Ignoring redundant target for ${link} in ${configPath}.`
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return { link, error: false };
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
console.warn(
|
|
197
|
+
`[any-skills] Unsupported link entry in ${configPath}; skipping.`
|
|
198
|
+
);
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function getConfigEntries(config) {
|
|
203
|
+
if (!config) {
|
|
204
|
+
return null;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (Array.isArray(config.links)) {
|
|
208
|
+
return config.links;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (Array.isArray(config.linkTargets)) {
|
|
212
|
+
return config.linkTargets;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return null;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function commandExists(command) {
|
|
219
|
+
const pathEnv = process.env.PATH || '';
|
|
220
|
+
const pathEntries = pathEnv.split(path.delimiter).filter(Boolean);
|
|
221
|
+
const isWindows = process.platform === 'win32';
|
|
222
|
+
const extensions = isWindows
|
|
223
|
+
? (process.env.PATHEXT || '.EXE;.CMD;.BAT;.COM')
|
|
224
|
+
.split(';')
|
|
225
|
+
.filter(Boolean)
|
|
226
|
+
: [''];
|
|
227
|
+
|
|
228
|
+
for (const entry of pathEntries) {
|
|
229
|
+
for (const ext of extensions) {
|
|
230
|
+
const candidate = path.join(entry, `${command}${ext}`);
|
|
231
|
+
try {
|
|
232
|
+
fs.accessSync(candidate, fs.constants.X_OK);
|
|
233
|
+
return true;
|
|
234
|
+
} catch (err) {
|
|
235
|
+
if (err.code !== 'ENOENT') {
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return false;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function detectLinkTargets(config) {
|
|
246
|
+
const toolTargets =
|
|
247
|
+
config && config.tools && typeof config.tools === 'object'
|
|
248
|
+
? config.tools
|
|
249
|
+
: defaultToolTargets;
|
|
250
|
+
|
|
251
|
+
const targets = [];
|
|
252
|
+
for (const [command, linkTarget] of Object.entries(toolTargets)) {
|
|
253
|
+
if (typeof linkTarget !== 'string' || linkTarget.trim() === '') {
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
if (commandExists(command)) {
|
|
257
|
+
targets.push(linkTarget);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
return targets;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function resolveExplicitTargets(config, toolNames) {
|
|
265
|
+
const toolTargets =
|
|
266
|
+
config && config.tools && typeof config.tools === 'object'
|
|
267
|
+
? config.tools
|
|
268
|
+
: defaultToolTargets;
|
|
269
|
+
|
|
270
|
+
const targets = [];
|
|
271
|
+
for (const name of toolNames) {
|
|
272
|
+
const mappedTarget = toolTargets[name];
|
|
273
|
+
if (typeof mappedTarget === 'string' && mappedTarget.trim() !== '') {
|
|
274
|
+
targets.push(mappedTarget);
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
277
|
+
const safeName = name.trim();
|
|
278
|
+
if (safeName) {
|
|
279
|
+
targets.push(`.${safeName}/skills`);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
return targets;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function buildLinkMappings({
|
|
287
|
+
config,
|
|
288
|
+
configPath,
|
|
289
|
+
exists,
|
|
290
|
+
target,
|
|
291
|
+
rootDir,
|
|
292
|
+
linkRoot,
|
|
293
|
+
explicitTools,
|
|
294
|
+
}) {
|
|
295
|
+
const resolvedLinkRoot = resolveLinkRoot(config, rootDir, linkRoot);
|
|
296
|
+
const explicitTargets = Array.isArray(explicitTools)
|
|
297
|
+
? resolveExplicitTargets(config, explicitTools)
|
|
298
|
+
: null;
|
|
299
|
+
const entries = getConfigEntries(config);
|
|
300
|
+
if (!entries) {
|
|
301
|
+
if (explicitTargets) {
|
|
302
|
+
return {
|
|
303
|
+
mappings: explicitTargets.map((linkTarget) => ({
|
|
304
|
+
linkPath: resolveRootPath(linkTarget, resolvedLinkRoot),
|
|
305
|
+
targetPath: target,
|
|
306
|
+
})),
|
|
307
|
+
error: false,
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
if (exists) {
|
|
312
|
+
console.warn(
|
|
313
|
+
`[any-skills] No link configuration found in ${configPath}; using auto-detected tools.`
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
const detectedTargets = detectLinkTargets(config);
|
|
317
|
+
return {
|
|
318
|
+
mappings: detectedTargets.map((linkTarget) => ({
|
|
319
|
+
linkPath: resolveRootPath(linkTarget, resolvedLinkRoot),
|
|
320
|
+
targetPath: target,
|
|
321
|
+
})),
|
|
322
|
+
error: false,
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
if (entries.length === 0) {
|
|
327
|
+
return { mappings: [], error: false };
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const mappings = [];
|
|
331
|
+
for (const entry of entries) {
|
|
332
|
+
const normalized = normalizeLinkEntry(entry, target, configPath, rootDir);
|
|
333
|
+
if (!normalized) {
|
|
334
|
+
continue;
|
|
335
|
+
}
|
|
336
|
+
if (normalized.error) {
|
|
337
|
+
return { mappings: [], error: true };
|
|
338
|
+
}
|
|
339
|
+
mappings.push({
|
|
340
|
+
linkPath: resolveRootPath(normalized.link, resolvedLinkRoot),
|
|
341
|
+
targetPath: target,
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
return { mappings, error: false };
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function linkSkills(rootDir, options = {}) {
|
|
349
|
+
const { config, configPath, exists, error } = readSkillsConfig(rootDir);
|
|
350
|
+
if (error) {
|
|
351
|
+
console.error(
|
|
352
|
+
`[any-skills] Failed to parse ${configPath}: ${error.message}`
|
|
353
|
+
);
|
|
354
|
+
return 1;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
const target = resolveTarget(config, rootDir);
|
|
358
|
+
if (!ensureTarget(target)) {
|
|
359
|
+
return 1;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
const { mappings, error: mappingError } = buildLinkMappings({
|
|
363
|
+
config,
|
|
364
|
+
configPath,
|
|
365
|
+
exists,
|
|
366
|
+
target,
|
|
367
|
+
rootDir,
|
|
368
|
+
linkRoot: options.linkRoot,
|
|
369
|
+
explicitTools: options.explicitTools,
|
|
370
|
+
});
|
|
371
|
+
if (mappingError) {
|
|
372
|
+
return 1;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
for (const mapping of mappings) {
|
|
376
|
+
ensureSymlink(mapping.linkPath, mapping.targetPath);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
return 0;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
module.exports = {
|
|
383
|
+
linkSkills,
|
|
384
|
+
};
|
package/scripts/postinstall.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const fs = require('fs');
|
|
4
3
|
const path = require('path');
|
|
4
|
+
const { linkSkills } = require('./link-skills');
|
|
5
5
|
|
|
6
6
|
function getInstallRoot() {
|
|
7
7
|
const initCwd = process.env.INIT_CWD;
|
|
@@ -17,264 +17,17 @@ function getInstallRoot() {
|
|
|
17
17
|
return process.cwd();
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
const defaultLinkTargets = ['.claude/skills', '.codex/skills'];
|
|
24
|
-
|
|
25
|
-
function ensureDir(dirPath) {
|
|
26
|
-
if (!fs.existsSync(dirPath)) {
|
|
27
|
-
fs.mkdirSync(dirPath, { recursive: true });
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
function ensureTarget(dirPath) {
|
|
32
|
-
if (!fs.existsSync(dirPath)) {
|
|
33
|
-
fs.mkdirSync(dirPath, { recursive: true });
|
|
34
|
-
return true;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
const stat = fs.lstatSync(dirPath);
|
|
38
|
-
if (stat.isSymbolicLink()) {
|
|
39
|
-
return true;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
if (!stat.isDirectory()) {
|
|
43
|
-
console.error(`[any-skills] ${dirPath} exists and is not a directory.`);
|
|
44
|
-
return false;
|
|
45
|
-
}
|
|
20
|
+
const isGlobalInstall =
|
|
21
|
+
process.env.npm_config_global === 'true' ||
|
|
22
|
+
process.env.npm_config_global === '1';
|
|
46
23
|
|
|
47
|
-
|
|
24
|
+
if (isGlobalInstall) {
|
|
25
|
+
console.log('[any-skills] Global install detected; skipping postinstall.');
|
|
26
|
+
process.exit(0);
|
|
48
27
|
}
|
|
49
28
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
function resolveSymlinkTarget(linkPath) {
|
|
55
|
-
const linkParent = path.dirname(linkPath);
|
|
56
|
-
const currentTarget = fs.readlinkSync(linkPath);
|
|
57
|
-
return path.resolve(linkParent, currentTarget);
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
function ensureSymlink(linkPath, targetPath) {
|
|
61
|
-
const linkParent = path.dirname(linkPath);
|
|
62
|
-
ensureDir(linkParent);
|
|
63
|
-
|
|
64
|
-
const resolvedLinkPath = path.resolve(linkPath);
|
|
65
|
-
const expectedTarget = path.resolve(targetPath);
|
|
66
|
-
if (resolvedLinkPath === expectedTarget) {
|
|
67
|
-
console.warn(
|
|
68
|
-
`[any-skills] ${linkPath} resolves to the target; skipping.`
|
|
69
|
-
);
|
|
70
|
-
return false;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
try {
|
|
74
|
-
const stat = fs.lstatSync(linkPath);
|
|
75
|
-
if (stat.isSymbolicLink()) {
|
|
76
|
-
const resolvedTarget = resolveSymlinkTarget(linkPath);
|
|
77
|
-
if (resolvedTarget === expectedTarget) {
|
|
78
|
-
return true;
|
|
79
|
-
}
|
|
80
|
-
fs.unlinkSync(linkPath);
|
|
81
|
-
} else {
|
|
82
|
-
console.warn(
|
|
83
|
-
`[any-skills] ${linkPath} exists and is not a symlink; skipping.`
|
|
84
|
-
);
|
|
85
|
-
return false;
|
|
86
|
-
}
|
|
87
|
-
} catch (err) {
|
|
88
|
-
if (err.code !== 'ENOENT') {
|
|
89
|
-
throw err;
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
const relativeTarget = path.relative(linkParent, targetPath) || '.';
|
|
94
|
-
fs.symlinkSync(relativeTarget, linkPath, getSymlinkType());
|
|
95
|
-
return true;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
function readSkillsConfig(configRoot) {
|
|
99
|
-
const configPath = path.join(configRoot, configFileName);
|
|
100
|
-
let rawConfig = null;
|
|
101
|
-
|
|
102
|
-
try {
|
|
103
|
-
rawConfig = fs.readFileSync(configPath, 'utf8');
|
|
104
|
-
} catch (err) {
|
|
105
|
-
if (err.code === 'ENOENT') {
|
|
106
|
-
return { config: null, configPath, exists: false, error: null };
|
|
107
|
-
}
|
|
108
|
-
throw err;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
let parsed = null;
|
|
112
|
-
try {
|
|
113
|
-
parsed = JSON.parse(rawConfig);
|
|
114
|
-
} catch (err) {
|
|
115
|
-
return { config: null, configPath, exists: true, error: err };
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
119
|
-
return {
|
|
120
|
-
config: null,
|
|
121
|
-
configPath,
|
|
122
|
-
exists: true,
|
|
123
|
-
error: new Error('Config must be a JSON object.'),
|
|
124
|
-
};
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
return { config: parsed, configPath, exists: true, error: null };
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
function resolveRootPath(value, baseDir) {
|
|
131
|
-
return path.isAbsolute(value) ? value : path.join(baseDir, value);
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
function resolveTarget(config) {
|
|
135
|
-
if (
|
|
136
|
-
config &&
|
|
137
|
-
typeof config.target === 'string' &&
|
|
138
|
-
config.target.trim() !== ''
|
|
139
|
-
) {
|
|
140
|
-
return resolveRootPath(config.target, rootDir);
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
return path.join(rootDir, targetName);
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
function normalizeLinkEntry(entry, target, configPath) {
|
|
147
|
-
if (typeof entry === 'string') {
|
|
148
|
-
return { link: entry, error: false };
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
if (entry && typeof entry === 'object') {
|
|
152
|
-
const link = entry.link;
|
|
153
|
-
if (typeof link !== 'string' || link.trim() === '') {
|
|
154
|
-
console.warn(
|
|
155
|
-
`[any-skills] Invalid link entry in ${configPath}; skipping.`
|
|
156
|
-
);
|
|
157
|
-
return null;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
if (Object.prototype.hasOwnProperty.call(entry, 'target')) {
|
|
161
|
-
const targetValue = entry.target;
|
|
162
|
-
if (typeof targetValue !== 'string' || targetValue.trim() === '') {
|
|
163
|
-
console.error(
|
|
164
|
-
`[any-skills] Invalid target for ${link} in ${configPath}.`
|
|
165
|
-
);
|
|
166
|
-
return { error: true };
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
const resolvedTarget = resolveRootPath(targetValue, rootDir);
|
|
170
|
-
if (resolvedTarget !== target) {
|
|
171
|
-
console.error(
|
|
172
|
-
`[any-skills] Link entry in ${configPath} must not set a different target.`
|
|
173
|
-
);
|
|
174
|
-
return { error: true };
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
console.warn(
|
|
178
|
-
`[any-skills] Ignoring redundant target for ${link} in ${configPath}.`
|
|
179
|
-
);
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
return { link, error: false };
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
console.warn(
|
|
186
|
-
`[any-skills] Unsupported link entry in ${configPath}; skipping.`
|
|
187
|
-
);
|
|
188
|
-
return null;
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
function getConfigEntries(config) {
|
|
192
|
-
if (!config) {
|
|
193
|
-
return null;
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
if (Array.isArray(config.links)) {
|
|
197
|
-
return config.links;
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
if (Array.isArray(config.linkTargets)) {
|
|
201
|
-
return config.linkTargets;
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
return null;
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
function buildLinkMappings({ config, configPath, exists, target }) {
|
|
208
|
-
const entries = getConfigEntries(config);
|
|
209
|
-
if (!entries) {
|
|
210
|
-
if (exists) {
|
|
211
|
-
console.warn(
|
|
212
|
-
`[any-skills] No link configuration found in ${configPath}; using defaults.`
|
|
213
|
-
);
|
|
214
|
-
}
|
|
215
|
-
return {
|
|
216
|
-
mappings: defaultLinkTargets.map((target) => ({
|
|
217
|
-
linkPath: path.join(rootDir, target),
|
|
218
|
-
targetPath: target,
|
|
219
|
-
})),
|
|
220
|
-
error: false,
|
|
221
|
-
};
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
if (entries.length === 0) {
|
|
225
|
-
return { mappings: [], error: false };
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
const mappings = [];
|
|
229
|
-
for (const entry of entries) {
|
|
230
|
-
const normalized = normalizeLinkEntry(entry, target, configPath);
|
|
231
|
-
if (!normalized) {
|
|
232
|
-
continue;
|
|
233
|
-
}
|
|
234
|
-
if (normalized.error) {
|
|
235
|
-
return { mappings: [], error: true };
|
|
236
|
-
}
|
|
237
|
-
mappings.push({
|
|
238
|
-
linkPath: resolveRootPath(normalized.link, rootDir),
|
|
239
|
-
targetPath: target,
|
|
240
|
-
});
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
return { mappings, error: false };
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
function linkSkills() {
|
|
247
|
-
const { config, configPath, exists, error } = readSkillsConfig(rootDir);
|
|
248
|
-
if (error) {
|
|
249
|
-
console.error(
|
|
250
|
-
`[any-skills] Failed to parse ${configPath}: ${error.message}`
|
|
251
|
-
);
|
|
252
|
-
return 1;
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
const target = resolveTarget(config);
|
|
256
|
-
if (!ensureTarget(target)) {
|
|
257
|
-
return 1;
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
const { mappings, error: mappingError } = buildLinkMappings({
|
|
261
|
-
config,
|
|
262
|
-
configPath,
|
|
263
|
-
exists,
|
|
264
|
-
target,
|
|
265
|
-
});
|
|
266
|
-
if (mappingError) {
|
|
267
|
-
return 1;
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
for (const mapping of mappings) {
|
|
271
|
-
ensureSymlink(mapping.linkPath, mapping.targetPath);
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
return 0;
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
const exitCode = linkSkills();
|
|
29
|
+
const rootDir = getInstallRoot();
|
|
30
|
+
const exitCode = linkSkills(rootDir);
|
|
278
31
|
if (exitCode) {
|
|
279
32
|
process.exit(exitCode);
|
|
280
33
|
}
|