skill-linker 4.1.1 → 4.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
@@ -21,7 +21,7 @@
21
21
 
22
22
  ```bash
23
23
  # 安裝技能(需要 --skill 或 --from)
24
- npx /app/workspace/projects/skill-linker install --skill <路徑> --agent opencode --scope both --yes
24
+ npx skill-linker install --skill <路徑> --agent opencode --scope both --yes
25
25
  npx skill-linker install --from https://github.com/anthropics/skills --agent claude --scope both
26
26
 
27
27
  # 列出已安裝的 Repos
@@ -159,6 +159,8 @@ npx skill-linker install --from https://github.com/obra/superpowers --agent clau
159
159
 
160
160
  1. **權限問題**:在建立 Symlink 時,請確保您有對應目錄的寫入權限。
161
161
  2. **環境需求**:需安裝 Node.js 18.0.0 以上版本。
162
+ 3. **Windows**:建立 Symlink 需開啟「開發者模式」或以系統管理員權限執行,否則 `fs.symlinkSync` 會失敗。
163
+ 4. **覆寫保護**:`--yes` 只會覆寫既有的 Symlink;若目標是「真實目錄/檔案」,工具會拒絕刪除以保護資料。
162
164
 
163
165
  ## 授權
164
166
 
package/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "skill-linker",
3
- "version": "4.1.1",
3
+ "version": "4.1.2",
4
4
  "description": "CLI to link AI Agent Skills to various agents (Claude, Copilot, Antigravity, Cursor, etc.)",
5
5
  "main": "bin/cli.js",
6
6
  "bin": {
7
7
  "skill-linker": "bin/cli.js"
8
8
  },
9
9
  "scripts": {
10
- "test": "echo \"No tests yet\""
10
+ "test": "node --test"
11
11
  },
12
12
  "keywords": [
13
13
  "ai",
package/src/cli.js CHANGED
@@ -19,10 +19,7 @@ program
19
19
  program
20
20
  .command("install")
21
21
  .description("Install a skill to specified agents")
22
- .requiredOption(
23
- "--skill <path>",
24
- "Path to skill directory or --from clone URL",
25
- )
22
+ .option("--skill <path>", "Path to a local skill directory")
26
23
  .option("--from <github-url>", "Clone skill from GitHub URL first, then link")
27
24
  .option(
28
25
  "-a, --agent <names...>",
@@ -75,6 +75,15 @@ async function install(options) {
75
75
  skillPaths = [options.skill];
76
76
  }
77
77
 
78
+ // Require at least one source: --skill or --from
79
+ if (skillPaths.length === 0) {
80
+ console.error(
81
+ chalk.red("[ERROR]"),
82
+ "No skill source provided. Use --skill <path> or --from <github-url>.",
83
+ );
84
+ process.exit(1);
85
+ }
86
+
78
87
  // Validate skill paths
79
88
  for (const p of skillPaths) {
80
89
  if (!dirExists(p)) {
@@ -147,6 +156,9 @@ async function install(options) {
147
156
  }
148
157
  console.log(chalk.blue("[INFO]"), `Scope: ${scope}`);
149
158
 
159
+ let linkedCount = 0;
160
+ let failedCount = 0;
161
+
150
162
  // Process each selected agent
151
163
  for (const agentIndex of selectedAgents) {
152
164
  const agent = agents[agentIndex];
@@ -189,15 +201,27 @@ async function install(options) {
189
201
 
190
202
  if (createSymlink(sPath, targetLink)) {
191
203
  console.log(chalk.green("[SUCCESS]"), `Linked ${sName}`);
204
+ linkedCount++;
192
205
  } else {
193
206
  console.error(chalk.red("[ERROR]"), `Failed to link ${sName}`);
207
+ failedCount++;
194
208
  }
195
209
  }
196
210
  }
197
211
  }
198
212
 
199
213
  console.log("");
200
- console.log(chalk.green("[SUCCESS]"), "All operations completed.");
214
+ if (failedCount > 0) {
215
+ console.error(
216
+ chalk.red("[ERROR]"),
217
+ `Completed with errors: ${linkedCount} linked, ${failedCount} failed.`,
218
+ );
219
+ process.exit(1);
220
+ }
221
+ console.log(
222
+ chalk.green("[SUCCESS]"),
223
+ `All operations completed (${linkedCount} linked).`,
224
+ );
201
225
  }
202
226
 
203
227
  module.exports = install;
@@ -35,9 +35,27 @@ function createSymlink(source, target) {
35
35
  // Ensure parent directory exists
36
36
  ensureDir(path.dirname(target));
37
37
 
38
- // Remove existing link/file/directory if present
39
- // force: true makes it ignore the error if file doesn't exist
40
- fs.rmSync(target, { recursive: true, force: true });
38
+ // Inspect the target itself without following symlinks.
39
+ let stat = null;
40
+ try {
41
+ stat = fs.lstatSync(target);
42
+ } catch {
43
+ // Target does not exist — nothing to remove.
44
+ }
45
+
46
+ if (stat) {
47
+ if (stat.isSymbolicLink()) {
48
+ // Safe: we are replacing a link, not real content.
49
+ fs.rmSync(target, { force: true });
50
+ } else {
51
+ // A real file or directory lives here. Refuse to delete it so
52
+ // we never destroy user data that we did not create.
53
+ console.error(
54
+ `Refusing to overwrite non-symlink target: ${target}`,
55
+ );
56
+ return false;
57
+ }
58
+ }
41
59
 
42
60
  fs.symlinkSync(source, target, 'dir');
43
61
  return true;