@skillctl/link-manager 0.2.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 skillctl contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,2 @@
1
+ export * from './link-manager.js';
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,mBAAmB,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ export * from './link-manager.js';
2
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,mBAAmB,CAAC"}
@@ -0,0 +1,36 @@
1
+ /**
2
+ * LinkManager (from PR5 patterns, implemented for PR6).
3
+ * Handles cross-platform linking: symlink (dir), junction (win), copy fallback.
4
+ * Safety: realpath verification to ensure targets point inside canonical store.
5
+ * Used by AgentAdapters' ensureTarget/removeTarget.
6
+ */
7
+ import type { LinkMode } from '@skillctl/core';
8
+ export interface LinkOptions {
9
+ mode?: LinkMode;
10
+ dryRun?: boolean;
11
+ force?: boolean;
12
+ }
13
+ export declare class LinkManager {
14
+ /**
15
+ * Ensure a link (or copy) from canonical skill dir to a target path (agent's skills dir entry).
16
+ * Creates parent dirs. Overwrites existing if force.
17
+ * Verifies after creation that realpath(target) resolves to canonical.
18
+ */
19
+ ensureLink(canonical: string, target: string, options?: LinkOptions): Promise<void>;
20
+ removeLink(target: string): Promise<void>;
21
+ /**
22
+ * Check if target currently points (via realpath) to the canonical.
23
+ */
24
+ isLinkedTo(canonical: string, target: string): Promise<boolean>;
25
+ /**
26
+ * Verify that a target (if exists) resolves inside the canonical store (prevent attacks).
27
+ * Returns true if safe or does not exist.
28
+ */
29
+ verifyTargetSafe(target: string, storeRoot: string): Promise<boolean>;
30
+ private copyDir;
31
+ }
32
+ export declare const linkManager: LinkManager;
33
+ export declare function pathExists(p: string): Promise<boolean>;
34
+ export { join, resolve as pathResolve } from 'node:path';
35
+ export { homedir } from 'node:os';
36
+ //# sourceMappingURL=link-manager.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"link-manager.d.ts","sourceRoot":"","sources":["../src/link-manager.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAKH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAG/C,MAAM,WAAW,WAAW;IAC1B,IAAI,CAAC,EAAE,QAAQ,CAAC;IAChB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAED,qBAAa,WAAW;IACtB;;;;OAIG;IACG,UAAU,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,GAAE,WAAgB,GAAG,OAAO,CAAC,IAAI,CAAC;IAmEvF,UAAU,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAW/C;;OAEG;IACG,UAAU,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAWrE;;;OAGG;IACG,gBAAgB,CAAC,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;YAY7D,OAAO;CAKtB;AAGD,eAAO,MAAM,WAAW,aAAoB,CAAC;AAG7C,wBAAsB,UAAU,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAO5D;AAED,OAAO,EAAE,IAAI,EAAE,OAAO,IAAI,WAAW,EAAE,MAAM,WAAW,CAAC;AACzD,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC"}
@@ -0,0 +1,143 @@
1
+ /**
2
+ * LinkManager (from PR5 patterns, implemented for PR6).
3
+ * Handles cross-platform linking: symlink (dir), junction (win), copy fallback.
4
+ * Safety: realpath verification to ensure targets point inside canonical store.
5
+ * Used by AgentAdapters' ensureTarget/removeTarget.
6
+ */
7
+ import { symlink, rm, stat, realpath, access, constants, cp as fsCp } from 'node:fs/promises';
8
+ import { dirname, resolve as pathResolve } from 'node:path';
9
+ import { ensureDir } from '@skillctl/core';
10
+ export class LinkManager {
11
+ /**
12
+ * Ensure a link (or copy) from canonical skill dir to a target path (agent's skills dir entry).
13
+ * Creates parent dirs. Overwrites existing if force.
14
+ * Verifies after creation that realpath(target) resolves to canonical.
15
+ */
16
+ async ensureLink(canonical, target, options = {}) {
17
+ const mode = options.mode || (process.platform === 'win32' ? 'junction' : 'symlink');
18
+ const dry = !!options.dryRun;
19
+ if (dry) {
20
+ console.log(`[dry-run] would link ${canonical} -> ${target} (${mode})`);
21
+ return;
22
+ }
23
+ await ensureDir(dirname(target));
24
+ // cleanup existing
25
+ if (options.force) {
26
+ try {
27
+ await rm(target, { recursive: true, force: true });
28
+ }
29
+ catch {
30
+ // ignore
31
+ }
32
+ }
33
+ // Check if already correctly linked
34
+ if (await this.isLinkedTo(canonical, target)) {
35
+ return;
36
+ }
37
+ try {
38
+ if (mode === 'copy') {
39
+ await this.copyDir(canonical, target);
40
+ return;
41
+ }
42
+ if (mode === 'junction' || process.platform === 'win32') {
43
+ // Prefer junction on win for broader compat (no dev mode needed for junctions)
44
+ try {
45
+ await symlink(canonical, target, 'junction');
46
+ }
47
+ catch (e) {
48
+ if (e.code === 'EPERM' || e.code === 'EEXIST') {
49
+ // fallback to copy with warning
50
+ console.warn(`[link-manager] junction failed for ${target}, falling back to copy`);
51
+ await this.copyDir(canonical, target);
52
+ return;
53
+ }
54
+ throw e;
55
+ }
56
+ }
57
+ else {
58
+ // Unix dir symlink
59
+ await symlink(canonical, target, 'dir');
60
+ }
61
+ // verify
62
+ if (!(await this.isLinkedTo(canonical, target))) {
63
+ // cleanup bad link and fallback
64
+ await rm(target, { recursive: true, force: true }).catch(() => { });
65
+ console.warn(`[link-manager] link verification failed, falling back to copy for ${target}`);
66
+ await this.copyDir(canonical, target);
67
+ }
68
+ }
69
+ catch (err) {
70
+ // general fallback to copy on error (e.g. EPERM no dev mode)
71
+ if (mode !== 'copy') {
72
+ console.warn(`[link-manager] link error (${err.code || err.message}), fallback copy: ${target}`);
73
+ await this.copyDir(canonical, target);
74
+ }
75
+ else {
76
+ throw err;
77
+ }
78
+ }
79
+ }
80
+ async removeLink(target) {
81
+ try {
82
+ const st = await stat(target).catch(() => null);
83
+ if (!st)
84
+ return;
85
+ // remove regardless of symlink/junction/dir
86
+ await rm(target, { recursive: true, force: true });
87
+ }
88
+ catch {
89
+ // best effort
90
+ }
91
+ }
92
+ /**
93
+ * Check if target currently points (via realpath) to the canonical.
94
+ */
95
+ async isLinkedTo(canonical, target) {
96
+ try {
97
+ // realpath follows symlinks/junctions
98
+ const targetReal = await realpath(target);
99
+ const canonReal = await realpath(canonical).catch(() => canonical);
100
+ return pathResolve(targetReal) === pathResolve(canonReal);
101
+ }
102
+ catch {
103
+ return false;
104
+ }
105
+ }
106
+ /**
107
+ * Verify that a target (if exists) resolves inside the canonical store (prevent attacks).
108
+ * Returns true if safe or does not exist.
109
+ */
110
+ async verifyTargetSafe(target, storeRoot) {
111
+ try {
112
+ const st = await stat(target);
113
+ if (!st)
114
+ return true;
115
+ const tReal = await realpath(target);
116
+ const storeReal = await realpath(storeRoot);
117
+ return tReal.startsWith(storeReal);
118
+ }
119
+ catch {
120
+ return true; // non existing ok
121
+ }
122
+ }
123
+ async copyDir(src, dest) {
124
+ await ensureDir(dest);
125
+ // Use recursive cp (node 16.7+)
126
+ await fsCp(src, dest, { recursive: true, force: true });
127
+ }
128
+ }
129
+ // Singleton for convenience
130
+ export const linkManager = new LinkManager();
131
+ // Also export low level helpers used by adapters if needed
132
+ export async function pathExists(p) {
133
+ try {
134
+ await access(p, constants.F_OK);
135
+ return true;
136
+ }
137
+ catch {
138
+ return false;
139
+ }
140
+ }
141
+ export { join, resolve as pathResolve } from 'node:path';
142
+ export { homedir } from 'node:os';
143
+ //# sourceMappingURL=link-manager.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"link-manager.js","sourceRoot":"","sources":["../src/link-manager.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "@skillctl/link-manager",
3
+ "version": "0.2.0",
4
+ "description": "Link manager for symlinks, junctions, copies across agent adapters for skillctl",
5
+ "license": "MIT",
6
+ "author": "skillctl contributors",
7
+ "type": "module",
8
+ "main": "./dist/index.js",
9
+ "types": "./dist/index.d.ts",
10
+ "files": [
11
+ "dist"
12
+ ],
13
+ "dependencies": {
14
+ "@skillctl/core": "0.2.0"
15
+ },
16
+ "devDependencies": {
17
+ "@types/node": "^20.14.0",
18
+ "rimraf": "^5.0.7",
19
+ "typescript": "^5.6.3"
20
+ },
21
+ "engines": {
22
+ "node": ">=22.13"
23
+ },
24
+ "repository": {
25
+ "type": "git",
26
+ "url": "https://github.com/xFurti/skillctl.git",
27
+ "directory": "packages/link-manager"
28
+ },
29
+ "keywords": [
30
+ "link-manager",
31
+ "symlink",
32
+ "junction",
33
+ "skills",
34
+ "agent-skills"
35
+ ],
36
+ "scripts": {
37
+ "build": "tsc -b",
38
+ "dev": "tsc -b --watch",
39
+ "clean": "rimraf dist",
40
+ "lint": "tsc --noEmit",
41
+ "test": "node --test ./dist/test/*.test.js 2>&1 || echo 'run after build'"
42
+ }
43
+ }