any-skills 0.1.0

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 ADDED
@@ -0,0 +1,39 @@
1
+ # any-skills
2
+
3
+ When installed as a dependency, this package creates a shared skills directory (default: `.skills`) so Claude, Codex, and similar tools can reuse the same skills.
4
+
5
+ - `.claude/skills`
6
+ - `.codex/skills`
7
+
8
+ ## How it works
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).
11
+
12
+ ## Configuration
13
+
14
+ You can customize which links are created by adding `.skillsrc` (JSON) in your project root.
15
+
16
+ Supported fields:
17
+
18
+ - `target`: optional string. Overrides where skills are stored (default: `.skills`).
19
+ - `links`: array of link definitions. Strings map to `target` by default.
20
+
21
+ Example:
22
+
23
+ ```json
24
+ {
25
+ "target": ".skills",
26
+ "links": [".codex/skills", ".claude/skills"]
27
+ }
28
+ ```
29
+
30
+ ## Cross-platform behavior
31
+
32
+ - macOS / Linux uses `dir` symlinks
33
+ - Windows uses `junction` for better compatibility
34
+
35
+ ## Usage
36
+
37
+ ```sh
38
+ npm install any-skills
39
+ ```
package/package.json ADDED
@@ -0,0 +1,9 @@
1
+ {
2
+ "name": "any-skills",
3
+ "version": "0.1.0",
4
+ "description": "Share skills between Claude, Codex, and similar tools via a shared .skills directory.",
5
+ "type": "commonjs",
6
+ "scripts": {
7
+ "postinstall": "node scripts/postinstall.js"
8
+ }
9
+ }
@@ -0,0 +1,280 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+
6
+ function getInstallRoot() {
7
+ const initCwd = process.env.INIT_CWD;
8
+ if (initCwd) {
9
+ return path.resolve(initCwd);
10
+ }
11
+
12
+ const npmPrefix = process.env.npm_config_local_prefix;
13
+ if (npmPrefix) {
14
+ return path.resolve(npmPrefix);
15
+ }
16
+
17
+ return process.cwd();
18
+ }
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
+ }
46
+
47
+ return true;
48
+ }
49
+
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(
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();
278
+ if (exitCode) {
279
+ process.exit(exitCode);
280
+ }