any-skills 0.1.1 → 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 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
@@ -38,11 +62,32 @@ Example:
38
62
  npm install any-skills --save-dev
39
63
  ```
40
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
+
41
86
  ## Git ignore
42
87
 
43
88
  Add the tool-specific shared skill directories to your `.gitignore`:
44
89
 
45
90
  ```
46
- .claude.skills
47
- .codex.skills
91
+ .claude/skills
92
+ .codex/skills
48
93
  ```
package/package.json CHANGED
@@ -1,9 +1,12 @@
1
1
  {
2
2
  "name": "any-skills",
3
- "version": "0.1.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
+ };
@@ -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,262 +17,17 @@ function getInstallRoot() {
17
17
  return process.cwd();
18
18
  }
19
19
 
20
- const rootDir = getInstallRoot();
21
- const targetName = '.skills';
22
- const configFileName = '.skillsrc';
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
- return true;
24
+ if (isGlobalInstall) {
25
+ console.log('[any-skills] Global install detected; skipping postinstall.');
26
+ process.exit(0);
48
27
  }
49
28
 
50
- function getSymlinkType() {
51
- return process.platform === 'win32' ? 'junction' : 'dir';
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(`[any-skills] ${linkPath} resolves to the target; skipping.`);
68
- return false;
69
- }
70
-
71
- try {
72
- const stat = fs.lstatSync(linkPath);
73
- if (stat.isSymbolicLink()) {
74
- const resolvedTarget = resolveSymlinkTarget(linkPath);
75
- if (resolvedTarget === expectedTarget) {
76
- return true;
77
- }
78
- fs.unlinkSync(linkPath);
79
- } else {
80
- console.warn(
81
- `[any-skills] ${linkPath} exists and is not a symlink; skipping.`
82
- );
83
- return false;
84
- }
85
- } catch (err) {
86
- if (err.code !== 'ENOENT') {
87
- throw err;
88
- }
89
- }
90
-
91
- const relativeTarget = path.relative(linkParent, targetPath) || '.';
92
- fs.symlinkSync(relativeTarget, linkPath, getSymlinkType());
93
- return true;
94
- }
95
-
96
- function readSkillsConfig(configRoot) {
97
- const configPath = path.join(configRoot, configFileName);
98
- let rawConfig = null;
99
-
100
- try {
101
- rawConfig = fs.readFileSync(configPath, 'utf8');
102
- } catch (err) {
103
- if (err.code === 'ENOENT') {
104
- return { config: null, configPath, exists: false, error: null };
105
- }
106
- throw err;
107
- }
108
-
109
- let parsed = null;
110
- try {
111
- parsed = JSON.parse(rawConfig);
112
- } catch (err) {
113
- return { config: null, configPath, exists: true, error: err };
114
- }
115
-
116
- if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
117
- return {
118
- config: null,
119
- configPath,
120
- exists: true,
121
- error: new Error('Config must be a JSON object.'),
122
- };
123
- }
124
-
125
- return { config: parsed, configPath, exists: true, error: null };
126
- }
127
-
128
- function resolveRootPath(value, baseDir) {
129
- return path.isAbsolute(value) ? value : path.join(baseDir, value);
130
- }
131
-
132
- function resolveTarget(config) {
133
- if (
134
- config &&
135
- typeof config.target === 'string' &&
136
- config.target.trim() !== ''
137
- ) {
138
- return resolveRootPath(config.target, rootDir);
139
- }
140
-
141
- return path.join(rootDir, targetName);
142
- }
143
-
144
- function normalizeLinkEntry(entry, target, configPath) {
145
- if (typeof entry === 'string') {
146
- return { link: entry, error: false };
147
- }
148
-
149
- if (entry && typeof entry === 'object') {
150
- const link = entry.link;
151
- if (typeof link !== 'string' || link.trim() === '') {
152
- console.warn(
153
- `[any-skills] Invalid link entry in ${configPath}; skipping.`
154
- );
155
- return null;
156
- }
157
-
158
- if (Object.prototype.hasOwnProperty.call(entry, 'target')) {
159
- const targetValue = entry.target;
160
- if (typeof targetValue !== 'string' || targetValue.trim() === '') {
161
- console.error(
162
- `[any-skills] Invalid target for ${link} in ${configPath}.`
163
- );
164
- return { error: true };
165
- }
166
-
167
- const resolvedTarget = resolveRootPath(targetValue, rootDir);
168
- if (resolvedTarget !== target) {
169
- console.error(
170
- `[any-skills] Link entry in ${configPath} must not set a different target.`
171
- );
172
- return { error: true };
173
- }
174
-
175
- console.warn(
176
- `[any-skills] Ignoring redundant target for ${link} in ${configPath}.`
177
- );
178
- }
179
-
180
- return { link, error: false };
181
- }
182
-
183
- console.warn(
184
- `[any-skills] Unsupported link entry in ${configPath}; skipping.`
185
- );
186
- return null;
187
- }
188
-
189
- function getConfigEntries(config) {
190
- if (!config) {
191
- return null;
192
- }
193
-
194
- if (Array.isArray(config.links)) {
195
- return config.links;
196
- }
197
-
198
- if (Array.isArray(config.linkTargets)) {
199
- return config.linkTargets;
200
- }
201
-
202
- return null;
203
- }
204
-
205
- function buildLinkMappings({ config, configPath, exists, target }) {
206
- const entries = getConfigEntries(config);
207
- if (!entries) {
208
- if (exists) {
209
- console.warn(
210
- `[any-skills] No link configuration found in ${configPath}; using defaults.`
211
- );
212
- }
213
- return {
214
- mappings: defaultLinkTargets.map((linkTarget) => ({
215
- linkPath: resolveRootPath(linkTarget, rootDir),
216
- targetPath: target,
217
- })),
218
- error: false,
219
- };
220
- }
221
-
222
- if (entries.length === 0) {
223
- return { mappings: [], error: false };
224
- }
225
-
226
- const mappings = [];
227
- for (const entry of entries) {
228
- const normalized = normalizeLinkEntry(entry, target, configPath);
229
- if (!normalized) {
230
- continue;
231
- }
232
- if (normalized.error) {
233
- return { mappings: [], error: true };
234
- }
235
- mappings.push({
236
- linkPath: resolveRootPath(normalized.link, rootDir),
237
- targetPath: target,
238
- });
239
- }
240
-
241
- return { mappings, error: false };
242
- }
243
-
244
- function linkSkills() {
245
- const { config, configPath, exists, error } = readSkillsConfig(rootDir);
246
- if (error) {
247
- console.error(
248
- `[any-skills] Failed to parse ${configPath}: ${error.message}`
249
- );
250
- return 1;
251
- }
252
-
253
- const target = resolveTarget(config);
254
- if (!ensureTarget(target)) {
255
- return 1;
256
- }
257
-
258
- const { mappings, error: mappingError } = buildLinkMappings({
259
- config,
260
- configPath,
261
- exists,
262
- target,
263
- });
264
- if (mappingError) {
265
- return 1;
266
- }
267
-
268
- for (const mapping of mappings) {
269
- ensureSymlink(mapping.linkPath, mapping.targetPath);
270
- }
271
-
272
- return 0;
273
- }
274
-
275
- const exitCode = linkSkills();
29
+ const rootDir = getInstallRoot();
30
+ const exitCode = linkSkills(rootDir);
276
31
  if (exitCode) {
277
32
  process.exit(exitCode);
278
33
  }