cc-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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026
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.
package/README.md ADDED
@@ -0,0 +1,165 @@
1
+ # cc-skills
2
+
3
+ [![npm version](https://img.shields.io/npm/v/cc-skills.svg)](https://www.npmjs.com/package/cc-skills)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
+ [![Node.js Version](https://img.shields.io/node/v/cc-skills.svg)](https://nodejs.org)
6
+
7
+ A thin wrapper around `npx skills add` for **CC-MIRROR** Claude Code instances.
8
+
9
+ It discovers one or more CC-MIRROR variants, sets `CLAUDE_CONFIG_DIR` to the target instance's `config/` directory, and then delegates the actual install to `npx skills add`.
10
+
11
+ [中文文档](./README_CN.md)
12
+
13
+ ## Features
14
+
15
+ - **Multi-instance support** - Install skills to one or more CC-MIRROR instances
16
+ - **Automatic discovery** - Detects CC-MIRROR instances automatically
17
+ - **Zero dependencies** - Uses only Node.js built-in modules
18
+ - **Dry-run mode** - Preview commands before execution
19
+ - **Graceful fallback** - Works even without cc-mirror CLI
20
+
21
+ ## Why this exists
22
+
23
+ `skills` already knows how to install into a Claude Code config directory when `CLAUDE_CONFIG_DIR` is set.
24
+
25
+ What this package adds is only the **instance orchestration layer**:
26
+
27
+ - Discover CC-MIRROR instances
28
+ - Target one instance or all instances
29
+ - Loop over targets
30
+ - Set `CLAUDE_CONFIG_DIR` per instance
31
+ - Forward the real install to `npx skills add`
32
+
33
+ ## Installation
34
+
35
+ Run directly with `npx`:
36
+
37
+ ```bash
38
+ npx cc-skills instances
39
+ npx cc-skills add better-auth/skills --instance mclaude-auth --copy -y
40
+ ```
41
+
42
+ Or install globally:
43
+
44
+ ```bash
45
+ npm install -g cc-skills
46
+ cc-skills instances
47
+ cc-skills add better-auth/skills --all-instances --skill "*" --copy -y
48
+ ```
49
+
50
+ ## Usage
51
+
52
+ ### `instances` - List CC-MIRROR instances
53
+
54
+ ```bash
55
+ cc-skills instances
56
+ cc-skills instances --json
57
+ cc-skills instances --cc-root ~/.cc-mirror
58
+ ```
59
+
60
+ ### `add` - Install skills to instances
61
+
62
+ ```bash
63
+ cc-skills add <source> (--instance <name> | --all-instances) [options]
64
+ ```
65
+
66
+ **Examples:**
67
+
68
+ ```bash
69
+ # Single instance
70
+ cc-skills add better-auth/skills --instance mclaude-auth --copy -y
71
+
72
+ # Multiple named instances
73
+ cc-skills add better-auth/skills --instance mclaude-auth --instance mclaude-security --copy -y
74
+
75
+ # All instances
76
+ cc-skills add better-auth/skills --all-instances --skill "*" --copy -y
77
+
78
+ # Dry run (preview without executing)
79
+ cc-skills add better-auth/skills --instance mclaude-auth --dry-run --copy -y
80
+ ```
81
+
82
+ ## Options
83
+
84
+ ### Wrapper options (handled by cc-skills)
85
+
86
+ | Option | Description |
87
+ |--------|-------------|
88
+ | `-i, --instance <name>` | Target a specific CC-MIRROR instance (repeatable) |
89
+ | `--all-instances` | Target all discovered instances |
90
+ | `--cc-root <path>` | Custom CC-MIRROR root directory |
91
+ | `--cc-bin-dir <path>` | Forwarded to `cc-mirror list` |
92
+ | `--dry-run` | Print commands without executing them |
93
+ | `--fail-fast` | Stop after the first failed install |
94
+ | `--verbose` | Print the underlying `npx` commands |
95
+
96
+ ### Forwarded options (passed to `npx skills add`)
97
+
98
+ | Option | Description |
99
+ |--------|-------------|
100
+ | `--copy` | Copy files instead of symlinking |
101
+ | `-y` | Skip confirmation prompts |
102
+ | `--skill <name>` | Install specific skill (use `*` for all) |
103
+ | `--list` | List available skills in source |
104
+
105
+ ## Important Behavior
106
+
107
+ This wrapper always injects:
108
+
109
+ ```bash
110
+ -g -a claude-code
111
+ ```
112
+
113
+ This means every install is done in the target instance's global Claude Code scope.
114
+
115
+ ## Do not use `skills --all`
116
+
117
+ The `skills --all` flag means **all skills to all agents**, which is broader than this wrapper's scope.
118
+
119
+ If you want all skills from a repository, use:
120
+
121
+ ```bash
122
+ cc-skills add owner/repo --all-instances --skill "*" --copy -y
123
+ ```
124
+
125
+ ## How It Works
126
+
127
+ **Discovery phase:**
128
+
129
+ ```bash
130
+ npx -y cc-mirror list --json
131
+ ```
132
+
133
+ **Installation phase (per instance):**
134
+
135
+ ```bash
136
+ CLAUDE_CONFIG_DIR="~/.cc-mirror/<instance>/config" \
137
+ npx -y skills add <source> -g -a claude-code ...
138
+ ```
139
+
140
+ ## Fallback Behavior
141
+
142
+ If `cc-mirror list --json` is unavailable or fails, the wrapper falls back to scanning the CC-MIRROR root directory and reading `variant.json` / `config/` directly.
143
+
144
+ ## Development
145
+
146
+ ```bash
147
+ # Run tests
148
+ npm test
149
+
150
+ # Run CLI locally
151
+ node bin/cc-skills.js help
152
+
153
+ # Create local tarball
154
+ npm run pack:local
155
+ ```
156
+
157
+ ## Publishing
158
+
159
+ ```bash
160
+ npm publish
161
+ ```
162
+
163
+ ## License
164
+
165
+ [MIT](./LICENSE)
package/README_CN.md ADDED
@@ -0,0 +1,165 @@
1
+ # cc-skills
2
+
3
+ [![npm version](https://img.shields.io/npm/v/cc-skills.svg)](https://www.npmjs.com/package/cc-skills)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
+ [![Node.js Version](https://img.shields.io/node/v/cc-skills.svg)](https://nodejs.org)
6
+
7
+ **CC-MIRROR** Claude Code 实例的 `npx skills add` 轻量封装工具。
8
+
9
+ 它可以发现一个或多个 CC-MIRROR 变体,将 `CLAUDE_CONFIG_DIR` 设置为目标实例的 `config/` 目录,然后将实际安装委托给 `npx skills add`。
10
+
11
+ [English](./README.md)
12
+
13
+ ## 特性
14
+
15
+ - **多实例支持** - 将技能安装到一个或多个 CC-MIRROR 实例
16
+ - **自动发现** - 自动检测 CC-MIRROR 实例
17
+ - **零依赖** - 仅使用 Node.js 内置模块
18
+ - **试运行模式** - 执行前预览命令
19
+ - **优雅降级** - 即使没有 cc-mirror CLI 也能工作
20
+
21
+ ## 为什么需要这个工具
22
+
23
+ `skills` 已经知道如何在设置 `CLAUDE_CONFIG_DIR` 时安装到 Claude Code 配置目录。
24
+
25
+ 本工具仅添加了**实例编排层**:
26
+
27
+ - 发现 CC-MIRROR 实例
28
+ - 定位单个或所有实例
29
+ - 遍历目标实例
30
+ - 为每个实例设置 `CLAUDE_CONFIG_DIR`
31
+ - 将实际安装转发给 `npx skills add`
32
+
33
+ ## 安装
34
+
35
+ 直接使用 `npx` 运行:
36
+
37
+ ```bash
38
+ npx cc-skills instances
39
+ npx cc-skills add better-auth/skills --instance mclaude-auth --copy -y
40
+ ```
41
+
42
+ 或全局安装:
43
+
44
+ ```bash
45
+ npm install -g cc-skills
46
+ cc-skills instances
47
+ cc-skills add better-auth/skills --all-instances --skill "*" --copy -y
48
+ ```
49
+
50
+ ## 使用方法
51
+
52
+ ### `instances` - 列出 CC-MIRROR 实例
53
+
54
+ ```bash
55
+ cc-skills instances
56
+ cc-skills instances --json
57
+ cc-skills instances --cc-root ~/.cc-mirror
58
+ ```
59
+
60
+ ### `add` - 向实例安装技能
61
+
62
+ ```bash
63
+ cc-skills add <source> (--instance <name> | --all-instances) [选项]
64
+ ```
65
+
66
+ **示例:**
67
+
68
+ ```bash
69
+ # 单个实例
70
+ cc-skills add better-auth/skills --instance mclaude-auth --copy -y
71
+
72
+ # 多个指定实例
73
+ cc-skills add better-auth/skills --instance mclaude-auth --instance mclaude-security --copy -y
74
+
75
+ # 所有实例
76
+ cc-skills add better-auth/skills --all-instances --skill "*" --copy -y
77
+
78
+ # 试运行(预览而不执行)
79
+ cc-skills add better-auth/skills --instance mclaude-auth --dry-run --copy -y
80
+ ```
81
+
82
+ ## 选项
83
+
84
+ ### 封装工具选项(由 cc-skills 处理)
85
+
86
+ | 选项 | 描述 |
87
+ |------|------|
88
+ | `-i, --instance <name>` | 定位特定的 CC-MIRROR 实例(可重复使用) |
89
+ | `--all-instances` | 定位所有已发现的实例 |
90
+ | `--cc-root <path>` | 自定义 CC-MIRROR 根目录 |
91
+ | `--cc-bin-dir <path>` | 转发给 `cc-mirror list` |
92
+ | `--dry-run` | 仅打印命令,不执行 |
93
+ | `--fail-fast` | 首次安装失败后停止 |
94
+ | `--verbose` | 打印底层的 `npx` 命令 |
95
+
96
+ ### 转发选项(传递给 `npx skills add`)
97
+
98
+ | 选项 | 描述 |
99
+ |------|------|
100
+ | `--copy` | 复制文件而非创建符号链接 |
101
+ | `-y` | 跳过确认提示 |
102
+ | `--skill <name>` | 安装特定技能(使用 `*` 表示全部) |
103
+ | `--list` | 列出源中可用的技能 |
104
+
105
+ ## 重要行为
106
+
107
+ 此封装工具始终会注入:
108
+
109
+ ```bash
110
+ -g -a claude-code
111
+ ```
112
+
113
+ 这意味着每次安装都在目标实例的全局 Claude Code 范围内进行。
114
+
115
+ ## 不要使用 `skills --all`
116
+
117
+ `skills --all` 标志意味着**所有技能安装到所有代理**,这超出了本工具的范围。
118
+
119
+ 如果你想要仓库中的所有技能,请使用:
120
+
121
+ ```bash
122
+ cc-skills add owner/repo --all-instances --skill "*" --copy -y
123
+ ```
124
+
125
+ ## 工作原理
126
+
127
+ **发现阶段:**
128
+
129
+ ```bash
130
+ npx -y cc-mirror list --json
131
+ ```
132
+
133
+ **安装阶段(每个实例):**
134
+
135
+ ```bash
136
+ CLAUDE_CONFIG_DIR="~/.cc-mirror/<instance>/config" \
137
+ npx -y skills add <source> -g -a claude-code ...
138
+ ```
139
+
140
+ ## 降级行为
141
+
142
+ 如果 `cc-mirror list --json` 不可用或失败,封装工具会降级为扫描 CC-MIRROR 根目录并直接读取 `variant.json` / `config/`。
143
+
144
+ ## 开发
145
+
146
+ ```bash
147
+ # 运行测试
148
+ npm test
149
+
150
+ # 本地运行 CLI
151
+ node bin/cc-skills.js help
152
+
153
+ # 创建本地打包文件
154
+ npm run pack:local
155
+ ```
156
+
157
+ ## 发布
158
+
159
+ ```bash
160
+ npm publish
161
+ ```
162
+
163
+ ## 许可证
164
+
165
+ [MIT](./LICENSE)
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env node
2
+
3
+ const { main } = require('../src/cli');
4
+
5
+ try {
6
+ const exitCode = main(process.argv.slice(2));
7
+ if (typeof exitCode === 'number' && exitCode !== 0) {
8
+ process.exitCode = exitCode;
9
+ }
10
+ } catch (error) {
11
+ const message = error && error.message ? error.message : String(error);
12
+ console.error(`Error: ${message}`);
13
+ process.exitCode = 1;
14
+ }
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "cc-skills",
3
+ "version": "0.1.0",
4
+ "description": "Thin wrapper around `npx skills add` that installs skills into one or more CC-MIRROR Claude Code instances by setting CLAUDE_CONFIG_DIR per instance.",
5
+ "license": "MIT",
6
+ "author": "Lyon <lyon.liang12@gmail.com>",
7
+ "type": "commonjs",
8
+ "bin": {
9
+ "cc-skills": "bin/cc-skills.js"
10
+ },
11
+ "files": [
12
+ "bin",
13
+ "src",
14
+ "README.md",
15
+ "README_CN.md",
16
+ "LICENSE"
17
+ ],
18
+ "engines": {
19
+ "node": ">=18.17"
20
+ },
21
+ "keywords": [
22
+ "cc-mirror",
23
+ "claude-code",
24
+ "skills",
25
+ "agent-skills",
26
+ "wrapper",
27
+ "claude_config_dir",
28
+ "cli"
29
+ ],
30
+ "scripts": {
31
+ "test": "node --test",
32
+ "start": "node bin/cc-skills.js",
33
+ "pack:local": "npm pack"
34
+ },
35
+ "repository": {
36
+ "type": "git",
37
+ "url": "git+https://github.com/1WorldCapture/cc-skills.git"
38
+ },
39
+ "homepage": "https://github.com/1WorldCapture/cc-skills#readme",
40
+ "bugs": {
41
+ "url": "https://github.com/1WorldCapture/cc-skills/issues"
42
+ }
43
+ }
package/src/cli.js ADDED
@@ -0,0 +1,238 @@
1
+ const path = require('node:path');
2
+ const pkg = require(path.join(__dirname, '..', 'package.json'));
3
+ const {
4
+ NPX_BIN,
5
+ buildSkillsAddNpxArgs,
6
+ discoverInstances,
7
+ formatCommand,
8
+ hasListFlag,
9
+ normalizeSkillsAddArgs,
10
+ parseAddArgs,
11
+ parseInstancesArgs,
12
+ runCommand,
13
+ selectInstances,
14
+ summarizeResults,
15
+ } = require('./lib');
16
+
17
+ function printMainHelp() {
18
+ console.log(`
19
+ ${pkg.name} v${pkg.version}
20
+
21
+ Thin wrapper around \`npx skills add\` for CC-MIRROR Claude Code instances.
22
+
23
+ Usage:
24
+ cc-skills add <source> (--instance <name> | --all-instances) [skills add args...]
25
+ cc-skills instances [--json]
26
+ cc-skills help [command]
27
+
28
+ Commands:
29
+ add Install one skill package into one or more CC-MIRROR instances
30
+ instances List discovered CC-MIRROR instances
31
+ help Show help
32
+
33
+ Global examples:
34
+ cc-skills instances
35
+ cc-skills add better-auth/skills --instance mclaude-auth --copy -y
36
+ cc-skills add better-auth/skills --all-instances --skill "*" --copy -y
37
+
38
+ Notes:
39
+ - This wrapper always targets Claude Code and forces global install mode.
40
+ - Internally it calls: npx -y cc-mirror list --json
41
+ - Then, per instance, it calls: npx -y skills add <source> -g -a claude-code ...
42
+ - Do not pass \`skills --all\`; use \`--skill "*"\` instead.
43
+ `);
44
+ }
45
+
46
+ function printInstancesHelp() {
47
+ console.log(`
48
+ Usage:
49
+ cc-skills instances [options]
50
+
51
+ Options:
52
+ --cc-root <path> Custom CC-MIRROR root (default: ~/.cc-mirror)
53
+ --cc-bin-dir <path> Forward a custom --bin-dir to cc-mirror list
54
+ --json Print raw JSON
55
+ --verbose Print the underlying cc-mirror command
56
+ -h, --help Show this help
57
+ `);
58
+ }
59
+
60
+ function printAddHelp() {
61
+ console.log(`
62
+ Usage:
63
+ cc-skills add <source> [wrapper options] [skills add args...]
64
+
65
+ Wrapper options:
66
+ -i, --instance <name> Target a specific CC-MIRROR instance (repeatable)
67
+ --all-instances Target every discovered CC-MIRROR instance
68
+ --cc-root <path> Custom CC-MIRROR root (default: ~/.cc-mirror)
69
+ --cc-bin-dir <path> Forward a custom --bin-dir to cc-mirror list
70
+ --dry-run Print commands without executing them
71
+ --fail-fast Stop after the first failed install
72
+ --verbose Print underlying npx commands
73
+ -h, --help Show this help
74
+
75
+ Forwarded to \`skills add\`:
76
+ Any unrecognized args are passed through to \`npx skills add\`.
77
+ Common examples: --copy, -y, --skill <name>, --list
78
+
79
+ Examples:
80
+ cc-skills add better-auth/skills --instance mclaude-auth --copy -y
81
+ cc-skills add better-auth/skills --instance mclaude-auth --skill organization --copy -y
82
+ cc-skills add better-auth/skills --all-instances --skill "*" --copy -y
83
+ cc-skills add better-auth/skills --instance mclaude-auth --dry-run --copy -y
84
+
85
+ Important:
86
+ - The wrapper injects \`-g -a claude-code\` automatically.
87
+ - \`skills --all\` is intentionally blocked because it installs to all agents.
88
+ `);
89
+ }
90
+
91
+ function printInstances(instances, json) {
92
+ if (json) {
93
+ console.log(JSON.stringify(instances, null, 2));
94
+ return;
95
+ }
96
+
97
+ if (instances.length === 0) {
98
+ console.log('No CC-MIRROR instances found.');
99
+ return;
100
+ }
101
+
102
+ for (const instance of instances) {
103
+ const provider = instance.provider || 'unknown';
104
+ console.log(`${instance.name}\t${provider}\t${instance.configDir}`);
105
+ }
106
+ }
107
+
108
+ function handleInstances(argv) {
109
+ const options = parseInstancesArgs(argv);
110
+ if (options.help) {
111
+ printInstancesHelp();
112
+ return 0;
113
+ }
114
+
115
+ const instances = discoverInstances(options);
116
+ printInstances(instances, options.json);
117
+ return 0;
118
+ }
119
+
120
+ function handleAdd(argv) {
121
+ const options = parseAddArgs(argv);
122
+ if (options.help) {
123
+ printAddHelp();
124
+ return 0;
125
+ }
126
+
127
+ const discovered = discoverInstances({
128
+ ccRoot: options.ccRoot,
129
+ ccBinDir: options.ccBinDir,
130
+ verbose: options.verbose,
131
+ });
132
+
133
+ if (discovered.length === 0) {
134
+ throw new Error('No CC-MIRROR instances found. Run `cc-skills instances` to verify discovery.');
135
+ }
136
+
137
+ const selected = selectInstances(discovered, options);
138
+ if (selected.length === 0) {
139
+ throw new Error('No target instances selected.');
140
+ }
141
+
142
+ const normalizedSkillsArgs = normalizeSkillsAddArgs(options.skillsArgs);
143
+ const listOnly = hasListFlag(normalizedSkillsArgs);
144
+ const targets = listOnly && selected.length > 1 ? selected.slice(0, 1) : selected;
145
+ const npxArgs = buildSkillsAddNpxArgs(options.source, normalizedSkillsArgs);
146
+
147
+ if (listOnly && selected.length > 1) {
148
+ console.error('`skills add --list` does not depend on the target instance. Running it once for the first selected instance.');
149
+ }
150
+
151
+ const results = [];
152
+
153
+ for (const target of targets) {
154
+ const envPatch = { CLAUDE_CONFIG_DIR: target.configDir };
155
+ const env = { ...process.env, ...envPatch };
156
+
157
+ console.error(`\n==> [${target.name}] ${target.configDir}`);
158
+
159
+ if (options.dryRun) {
160
+ console.log(formatCommand(NPX_BIN, npxArgs, envPatch));
161
+ results.push({
162
+ instance: target.name,
163
+ configDir: target.configDir,
164
+ ok: true,
165
+ dryRun: true,
166
+ exitCode: 0,
167
+ });
168
+ continue;
169
+ }
170
+
171
+ const result = runCommand(NPX_BIN, npxArgs, {
172
+ env,
173
+ stdio: 'inherit',
174
+ verbose: options.verbose,
175
+ });
176
+
177
+ const ok = result.status === 0;
178
+ results.push({
179
+ instance: target.name,
180
+ configDir: target.configDir,
181
+ ok,
182
+ dryRun: false,
183
+ exitCode: result.status ?? 1,
184
+ });
185
+
186
+ if (!ok && options.failFast) {
187
+ break;
188
+ }
189
+ }
190
+
191
+ const summary = summarizeResults(results);
192
+ console.error(`\nSummary: ${summary.successCount}/${summary.total} succeeded.`);
193
+ for (const result of results) {
194
+ if (result.ok) {
195
+ console.error(` ✓ ${result.instance} -> ${result.configDir}`);
196
+ } else {
197
+ console.error(` ✗ ${result.instance} -> ${result.configDir} (exit ${result.exitCode})`);
198
+ }
199
+ }
200
+
201
+ return summary.failureCount === 0 ? 0 : 1;
202
+ }
203
+
204
+ function main(argv = process.argv.slice(2)) {
205
+ const [command, ...rest] = argv;
206
+
207
+ if (!command || command === 'help' || command === '--help' || command === '-h') {
208
+ const subcommand = rest[0];
209
+ if (subcommand === 'add') {
210
+ printAddHelp();
211
+ return 0;
212
+ }
213
+ if (subcommand === 'instances') {
214
+ printInstancesHelp();
215
+ return 0;
216
+ }
217
+ printMainHelp();
218
+ return 0;
219
+ }
220
+
221
+ if (command === '--version' || command === '-v' || command === 'version') {
222
+ console.log(pkg.version);
223
+ return 0;
224
+ }
225
+
226
+ switch (command) {
227
+ case 'instances':
228
+ return handleInstances(rest);
229
+ case 'add':
230
+ return handleAdd(rest);
231
+ default:
232
+ throw new Error(`Unknown command: ${command}`);
233
+ }
234
+ }
235
+
236
+ module.exports = {
237
+ main,
238
+ };
package/src/lib.js ADDED
@@ -0,0 +1,472 @@
1
+ const fs = require('node:fs');
2
+ const os = require('node:os');
3
+ const path = require('node:path');
4
+ const { spawnSync } = require('node:child_process');
5
+
6
+ const NPX_BIN = process.platform === 'win32' ? 'npx.cmd' : 'npx';
7
+ const DEFAULT_CC_MIRROR_ROOT = path.join(os.homedir(), '.cc-mirror');
8
+
9
+ function expandHome(input) {
10
+ if (!input) return input;
11
+ if (input === '~') return os.homedir();
12
+ if (input.startsWith('~/') || input.startsWith('~\\')) {
13
+ return path.join(os.homedir(), input.slice(2));
14
+ }
15
+ return input;
16
+ }
17
+
18
+ function unique(values) {
19
+ return [...new Set(values.filter(Boolean))];
20
+ }
21
+
22
+ function formatCommand(command, args, envPatch) {
23
+ const envPrefix = envPatch
24
+ ? Object.entries(envPatch)
25
+ .map(([key, value]) => `${key}=${JSON.stringify(String(value))}`)
26
+ .join(' ')
27
+ : '';
28
+ return [envPrefix, command, ...args].filter(Boolean).join(' ');
29
+ }
30
+
31
+ function runCommand(command, args, options = {}) {
32
+ const {
33
+ env,
34
+ cwd,
35
+ stdio = 'pipe',
36
+ verbose = false,
37
+ } = options;
38
+
39
+ if (verbose) {
40
+ console.error(`$ ${formatCommand(command, args)}`);
41
+ }
42
+
43
+ const result = spawnSync(command, args, {
44
+ env,
45
+ cwd,
46
+ stdio,
47
+ encoding: 'utf8',
48
+ });
49
+
50
+ if (result.error) {
51
+ throw result.error;
52
+ }
53
+
54
+ return result;
55
+ }
56
+
57
+ function parseLooseJsonArray(stdout) {
58
+ const text = String(stdout || '').trim();
59
+ if (!text) return [];
60
+
61
+ try {
62
+ return JSON.parse(text);
63
+ } catch (error) {
64
+ const firstBracket = text.indexOf('[');
65
+ const lastBracket = text.lastIndexOf(']');
66
+
67
+ if (firstBracket !== -1 && lastBracket !== -1 && lastBracket > firstBracket) {
68
+ return JSON.parse(text.slice(firstBracket, lastBracket + 1));
69
+ }
70
+
71
+ throw error;
72
+ }
73
+ }
74
+
75
+ function readJsonIfExists(filePath) {
76
+ if (!fs.existsSync(filePath)) return null;
77
+ try {
78
+ return JSON.parse(fs.readFileSync(filePath, 'utf8'));
79
+ } catch {
80
+ return null;
81
+ }
82
+ }
83
+
84
+ function sanitizeDiscoveredInstances(instances, ccRoot) {
85
+ const root = expandHome(ccRoot || DEFAULT_CC_MIRROR_ROOT);
86
+ const seen = new Set();
87
+ const result = [];
88
+
89
+ for (const instance of Array.isArray(instances) ? instances : []) {
90
+ if (!instance || typeof instance !== 'object') continue;
91
+ const name = typeof instance.name === 'string' ? instance.name.trim() : '';
92
+ if (!name || seen.has(name)) continue;
93
+ seen.add(name);
94
+
95
+ const derivedConfigDir = path.join(root, name, 'config');
96
+ result.push({
97
+ name,
98
+ provider: instance.provider ?? null,
99
+ claudeVersion: instance.claudeVersion ?? null,
100
+ wrapperPath: instance.wrapperPath ?? null,
101
+ configDir: instance.configDir ? expandHome(instance.configDir) : derivedConfigDir,
102
+ raw: instance,
103
+ });
104
+ }
105
+
106
+ return result;
107
+ }
108
+
109
+ function discoverInstancesFromFilesystem(ccRoot) {
110
+ const root = expandHome(ccRoot || DEFAULT_CC_MIRROR_ROOT);
111
+ if (!fs.existsSync(root)) return [];
112
+
113
+ const entries = fs
114
+ .readdirSync(root, { withFileTypes: true })
115
+ .filter((entry) => entry.isDirectory())
116
+ .filter((entry) => entry.name !== 'bin' && !entry.name.startsWith('.'));
117
+
118
+ const instances = [];
119
+
120
+ for (const entry of entries) {
121
+ const variantDir = path.join(root, entry.name);
122
+ const variantJsonPath = path.join(variantDir, 'variant.json');
123
+ const configDir = path.join(variantDir, 'config');
124
+ const meta = readJsonIfExists(variantJsonPath);
125
+
126
+ if (!meta && !fs.existsSync(configDir)) {
127
+ continue;
128
+ }
129
+
130
+ instances.push({
131
+ name: entry.name,
132
+ provider: meta?.provider ?? null,
133
+ claudeVersion: meta?.nativeVersion ?? null,
134
+ wrapperPath: meta?.wrapperPath ?? null,
135
+ configDir: meta?.configDir ? expandHome(meta.configDir) : configDir,
136
+ raw: meta,
137
+ });
138
+ }
139
+
140
+ return sanitizeDiscoveredInstances(instances, root);
141
+ }
142
+
143
+ function discoverInstances(options = {}) {
144
+ const { ccRoot, ccBinDir, verbose = false } = options;
145
+ const resolvedRoot = expandHome(ccRoot || DEFAULT_CC_MIRROR_ROOT);
146
+ const resolvedBinDir = ccBinDir ? expandHome(ccBinDir) : undefined;
147
+
148
+ let lastError = null;
149
+
150
+ try {
151
+ const args = ['-y', 'cc-mirror', 'list', '--json'];
152
+ if (ccRoot) args.push('--root', resolvedRoot);
153
+ if (resolvedBinDir) args.push('--bin-dir', resolvedBinDir);
154
+
155
+ const result = runCommand(NPX_BIN, args, {
156
+ stdio: 'pipe',
157
+ verbose,
158
+ });
159
+
160
+ if (result.status === 0) {
161
+ const parsed = parseLooseJsonArray(result.stdout);
162
+ return sanitizeDiscoveredInstances(parsed, resolvedRoot);
163
+ }
164
+
165
+ lastError = new Error(result.stderr || result.stdout || `cc-mirror exited with code ${result.status}`);
166
+ } catch (error) {
167
+ lastError = error;
168
+ }
169
+
170
+ const fallback = discoverInstancesFromFilesystem(resolvedRoot);
171
+ if (fallback.length > 0) {
172
+ if (verbose) {
173
+ console.error('Falling back to filesystem discovery from CC-MIRROR root.');
174
+ }
175
+ return fallback;
176
+ }
177
+
178
+ if (lastError) {
179
+ throw new Error(
180
+ `Unable to discover CC-MIRROR instances via \`npx cc-mirror list --json\`: ${lastError.message}`
181
+ );
182
+ }
183
+
184
+ return [];
185
+ }
186
+
187
+ function selectInstances(discoveredInstances, options) {
188
+ const discovered = Array.isArray(discoveredInstances) ? discoveredInstances : [];
189
+ if (options.allInstances) return discovered;
190
+
191
+ const wanted = unique(options.instances || []);
192
+ const byName = new Map(discovered.map((instance) => [instance.name, instance]));
193
+ const selected = [];
194
+ const missing = [];
195
+
196
+ for (const name of wanted) {
197
+ const instance = byName.get(name);
198
+ if (instance) {
199
+ selected.push(instance);
200
+ } else {
201
+ missing.push(name);
202
+ }
203
+ }
204
+
205
+ if (missing.length > 0) {
206
+ const available = discovered.map((instance) => instance.name).sort();
207
+ const availableText = available.length > 0 ? available.join(', ') : '(none found)';
208
+ throw new Error(
209
+ `Unknown CC-MIRROR instance(s): ${missing.join(', ')}. Available instances: ${availableText}`
210
+ );
211
+ }
212
+
213
+ return selected;
214
+ }
215
+
216
+ function normalizeSkillsAddArgs(args) {
217
+ const forwarded = [];
218
+ let hasGlobal = false;
219
+ let hasClaudeAgent = false;
220
+
221
+ for (let index = 0; index < args.length; index += 1) {
222
+ const arg = args[index];
223
+
224
+ if (arg === '--all') {
225
+ throw new Error(
226
+ 'Do not pass `--all` through cc-skills because `skills --all` targets all agents. Use `--skill "*"` instead.'
227
+ );
228
+ }
229
+
230
+ if (arg === '-g' || arg === '--global') {
231
+ hasGlobal = true;
232
+ forwarded.push(arg);
233
+ continue;
234
+ }
235
+
236
+ if (arg === '--agent') {
237
+ const value = args[index + 1];
238
+ if (!value) {
239
+ throw new Error('Missing value for `--agent`.');
240
+ }
241
+ index += 1;
242
+ if (value !== 'claude-code') {
243
+ throw new Error(
244
+ `cc-skills only targets Claude Code instances. Remove \`--agent ${value}\` or change it to \`--agent claude-code\`.`
245
+ );
246
+ }
247
+ hasClaudeAgent = true;
248
+ forwarded.push('--agent', value);
249
+ continue;
250
+ }
251
+
252
+ if (arg.startsWith('--agent=')) {
253
+ const value = arg.slice('--agent='.length);
254
+ if (value !== 'claude-code') {
255
+ throw new Error(
256
+ `cc-skills only targets Claude Code instances. Remove \`${arg}\` or change it to \`--agent=claude-code\`.`
257
+ );
258
+ }
259
+ hasClaudeAgent = true;
260
+ forwarded.push(arg);
261
+ continue;
262
+ }
263
+
264
+ if (arg === '-a') {
265
+ const value = args[index + 1];
266
+ if (!value) {
267
+ throw new Error('Missing value for `-a`.');
268
+ }
269
+ index += 1;
270
+ if (value !== 'claude-code') {
271
+ throw new Error(
272
+ `cc-skills only targets Claude Code instances. Remove \`-a ${value}\` or change it to \`-a claude-code\`.`
273
+ );
274
+ }
275
+ hasClaudeAgent = true;
276
+ forwarded.push('-a', value);
277
+ continue;
278
+ }
279
+
280
+ forwarded.push(arg);
281
+ }
282
+
283
+ if (!hasGlobal) {
284
+ forwarded.unshift('-g');
285
+ }
286
+
287
+ if (!hasClaudeAgent) {
288
+ forwarded.unshift('claude-code');
289
+ forwarded.unshift('-a');
290
+ }
291
+
292
+ return forwarded;
293
+ }
294
+
295
+ function buildSkillsAddNpxArgs(source, normalizedSkillsArgs) {
296
+ return ['-y', 'skills', 'add', source, ...normalizedSkillsArgs];
297
+ }
298
+
299
+ function hasListFlag(args) {
300
+ return args.includes('--list') || args.includes('-l');
301
+ }
302
+
303
+ function parseInstancesArgs(argv) {
304
+ const options = {
305
+ ccRoot: undefined,
306
+ ccBinDir: undefined,
307
+ json: false,
308
+ verbose: false,
309
+ };
310
+
311
+ for (let index = 0; index < argv.length; index += 1) {
312
+ const arg = argv[index];
313
+
314
+ switch (arg) {
315
+ case '--cc-root': {
316
+ const value = argv[index + 1];
317
+ if (!value) throw new Error('Missing value for `--cc-root`.');
318
+ options.ccRoot = value;
319
+ index += 1;
320
+ break;
321
+ }
322
+ case '--cc-bin-dir': {
323
+ const value = argv[index + 1];
324
+ if (!value) throw new Error('Missing value for `--cc-bin-dir`.');
325
+ options.ccBinDir = value;
326
+ index += 1;
327
+ break;
328
+ }
329
+ case '--json':
330
+ options.json = true;
331
+ break;
332
+ case '--verbose':
333
+ options.verbose = true;
334
+ break;
335
+ case '--help':
336
+ case '-h':
337
+ options.help = true;
338
+ break;
339
+ default:
340
+ throw new Error(`Unknown option for \`instances\`: ${arg}`);
341
+ }
342
+ }
343
+
344
+ return options;
345
+ }
346
+
347
+ function parseAddArgs(argv) {
348
+ const options = {
349
+ source: undefined,
350
+ instances: [],
351
+ allInstances: false,
352
+ ccRoot: undefined,
353
+ ccBinDir: undefined,
354
+ dryRun: false,
355
+ failFast: false,
356
+ verbose: false,
357
+ help: false,
358
+ skillsArgs: [],
359
+ };
360
+
361
+ let passthroughMode = false;
362
+
363
+ for (let index = 0; index < argv.length; index += 1) {
364
+ const arg = argv[index];
365
+
366
+ if (passthroughMode) {
367
+ options.skillsArgs.push(arg);
368
+ continue;
369
+ }
370
+
371
+ if (arg === '--') {
372
+ passthroughMode = true;
373
+ continue;
374
+ }
375
+
376
+ if (!options.source && !arg.startsWith('-')) {
377
+ options.source = arg;
378
+ continue;
379
+ }
380
+
381
+ switch (arg) {
382
+ case '--instance':
383
+ case '-i': {
384
+ const value = argv[index + 1];
385
+ if (!value) throw new Error(`Missing value for \`${arg}\`.`);
386
+ options.instances.push(value);
387
+ index += 1;
388
+ break;
389
+ }
390
+ case '--all-instances':
391
+ options.allInstances = true;
392
+ break;
393
+ case '--cc-root': {
394
+ const value = argv[index + 1];
395
+ if (!value) throw new Error('Missing value for `--cc-root`.');
396
+ options.ccRoot = value;
397
+ index += 1;
398
+ break;
399
+ }
400
+ case '--cc-bin-dir': {
401
+ const value = argv[index + 1];
402
+ if (!value) throw new Error('Missing value for `--cc-bin-dir`.');
403
+ options.ccBinDir = value;
404
+ index += 1;
405
+ break;
406
+ }
407
+ case '--dry-run':
408
+ options.dryRun = true;
409
+ break;
410
+ case '--fail-fast':
411
+ options.failFast = true;
412
+ break;
413
+ case '--verbose':
414
+ options.verbose = true;
415
+ break;
416
+ case '--help':
417
+ case '-h':
418
+ options.help = true;
419
+ break;
420
+ default:
421
+ options.skillsArgs.push(arg);
422
+ break;
423
+ }
424
+ }
425
+
426
+ options.instances = unique(options.instances);
427
+
428
+ if (!options.help) {
429
+ if (!options.source) {
430
+ throw new Error('Missing required <source> for `cc-skills add`.');
431
+ }
432
+ if (options.allInstances && options.instances.length > 0) {
433
+ throw new Error('Use either `--all-instances` or `--instance`, not both.');
434
+ }
435
+ if (!options.allInstances && options.instances.length === 0) {
436
+ throw new Error('Choose a target with `--instance <name>` or `--all-instances`.');
437
+ }
438
+ }
439
+
440
+ return options;
441
+ }
442
+
443
+ function summarizeResults(results) {
444
+ const successes = results.filter((result) => result.ok);
445
+ const failures = results.filter((result) => !result.ok);
446
+ return {
447
+ total: results.length,
448
+ successCount: successes.length,
449
+ failureCount: failures.length,
450
+ successes,
451
+ failures,
452
+ };
453
+ }
454
+
455
+ module.exports = {
456
+ DEFAULT_CC_MIRROR_ROOT,
457
+ NPX_BIN,
458
+ buildSkillsAddNpxArgs,
459
+ discoverInstances,
460
+ discoverInstancesFromFilesystem,
461
+ expandHome,
462
+ formatCommand,
463
+ hasListFlag,
464
+ normalizeSkillsAddArgs,
465
+ parseAddArgs,
466
+ parseInstancesArgs,
467
+ parseLooseJsonArray,
468
+ runCommand,
469
+ selectInstances,
470
+ summarizeResults,
471
+ unique,
472
+ };