@jefferylau/euphony-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,231 @@
1
+ # Euphony Skills
2
+
3
+ [English](README.md) | [简体中文](README_CN.md)
4
+
5
+ Install local Euphony viewer skills for Codex and CodeBuddy.
6
+
7
+ This repository contains two independent skills with one npm-based installer:
8
+
9
+ - `skills/codex-euphony`: opens local Codex session JSONL logs in OpenAI Euphony.
10
+ - `skills/codebuddy-euphony`: converts local CodeBuddy session JSONL logs into an Euphony-compatible shape and opens them in OpenAI Euphony.
11
+
12
+ Each skill directory is self-contained, so it can still be installed by path. The npm CLI is the recommended install path because it handles both host applications consistently.
13
+
14
+ ## Requirements
15
+
16
+ - Node.js 18 or newer.
17
+ - `git`, used when a skill needs to clone the Euphony runtime checkout.
18
+ - `pnpm` or `corepack`, used to install and run Euphony.
19
+ - macOS, Linux, or Windows with a local browser opener.
20
+
21
+ The installer itself does not install Euphony dependencies. The selected skill does that lazily the first time it starts Euphony.
22
+ At runtime the skill prefers an installed `pnpm`; when only Corepack is available, it uses `corepack pnpm` and sets `COREPACK_INTEGRITY_KEYS=0` for that subprocess to avoid known Corepack pnpm signature bootstrap failures.
23
+
24
+ ## Quick Install
25
+
26
+ Install from npm after the package is published:
27
+
28
+ ```bash
29
+ npx @jefferylau/euphony-skills install codex
30
+ npx @jefferylau/euphony-skills install codebuddy
31
+ npx @jefferylau/euphony-skills install all
32
+ ```
33
+
34
+ Replace an existing install:
35
+
36
+ ```bash
37
+ npx @jefferylau/euphony-skills install all --force
38
+ ```
39
+
40
+ Remove installed skills:
41
+
42
+ ```bash
43
+ npx @jefferylau/euphony-skills uninstall all
44
+ ```
45
+
46
+ Check local state:
47
+
48
+ ```bash
49
+ npx @jefferylau/euphony-skills doctor
50
+ ```
51
+
52
+ Restart Codex or CodeBuddy after installing so the host application reloads skills.
53
+
54
+ ## Local Checkout Install
55
+
56
+ From a cloned repository:
57
+
58
+ ```bash
59
+ git clone https://github.com/liiujinfu/euphony-skills.git
60
+ cd euphony-skills
61
+ node bin/euphony-skills.mjs install all --force
62
+ ```
63
+
64
+ For local development, install with symlinks so edits in this repository are picked up immediately:
65
+
66
+ ```bash
67
+ node bin/euphony-skills.mjs install all --force --link
68
+ ```
69
+
70
+ For normal end users, prefer copy install without `--link`.
71
+
72
+ ## Install Targets
73
+
74
+ Codex installs to:
75
+
76
+ ```text
77
+ ${CODEX_HOME:-~/.codex}/skills/codex-euphony
78
+ ```
79
+
80
+ CodeBuddy installs to:
81
+
82
+ ```text
83
+ ${CODEBUDDY_HOME:-~/.codebuddy}/skills/codebuddy-euphony
84
+ ```
85
+
86
+ Override host homes when needed:
87
+
88
+ ```bash
89
+ CODEX_HOME=/custom/.codex npx @jefferylau/euphony-skills install codex
90
+ CODEBUDDY_HOME=/custom/.codebuddy npx @jefferylau/euphony-skills install codebuddy
91
+ ```
92
+
93
+ ## CLI Reference
94
+
95
+ ```bash
96
+ euphony-skills install codex [--force] [--link]
97
+ euphony-skills install codebuddy [--force] [--link]
98
+ euphony-skills install all [--force] [--link]
99
+ euphony-skills uninstall codex
100
+ euphony-skills uninstall codebuddy
101
+ euphony-skills uninstall all
102
+ euphony-skills doctor
103
+ ```
104
+
105
+ Options:
106
+
107
+ - `--force`: replace an existing install.
108
+ - `--link`: create a symlink from the host skill directory to this checkout. Use this for development only.
109
+
110
+ ## Using The Skills
111
+
112
+ After installing and restarting the host app, ask the assistant to open the latest session with Euphony.
113
+
114
+ Codex examples:
115
+
116
+ ```text
117
+ Use codex-euphony to open the latest Codex session.
118
+ Open this Codex conversation in Euphony.
119
+ ```
120
+
121
+ CodeBuddy examples:
122
+
123
+ ```text
124
+ Use codebuddy-euphony to open the latest CodeBuddy session.
125
+ Open this CodeBuddy conversation in Euphony.
126
+ ```
127
+
128
+ You can also run the scripts directly.
129
+
130
+ Codex:
131
+
132
+ ```bash
133
+ node ~/.codex/skills/codex-euphony/scripts/codex-euphony.mjs open
134
+ node ~/.codex/skills/codex-euphony/scripts/codex-euphony.mjs status
135
+ node ~/.codex/skills/codex-euphony/scripts/codex-euphony.mjs stop
136
+ ```
137
+
138
+ CodeBuddy:
139
+
140
+ ```bash
141
+ ~/.codebuddy/skills/codebuddy-euphony/scripts/codebuddy-euphony.mjs open
142
+ ~/.codebuddy/skills/codebuddy-euphony/scripts/codebuddy-euphony.mjs status
143
+ ~/.codebuddy/skills/codebuddy-euphony/scripts/codebuddy-euphony.mjs stop
144
+ ```
145
+
146
+ ## Runtime Behavior
147
+
148
+ The installer only copies or links skill folders. It does not copy session logs, generated JSONL files, local caches, or Euphony checkouts.
149
+
150
+ At runtime:
151
+
152
+ - Codex uses `${CODEX_HOME:-~/.codex}/cache/euphony`.
153
+ - CodeBuddy uses `${CODEBUDDY_HOME:-~/.codebuddy}/cache/euphony`.
154
+ - If the cache is deleted, the skill recreates it on the next command that needs Euphony.
155
+ - The local Euphony server binds to `127.0.0.1`.
156
+ - The default port is `3000`.
157
+ - Codex staging uses a symlink on macOS/Linux and a copy on Windows by default. Set `EUPHONY_STAGE_MODE=copy` to force snapshot staging everywhere.
158
+ - Background servers are tracked with a pid file under the Euphony cache, so `stop` only controls servers started by the same skill script.
159
+
160
+ If port `3000` is already occupied, use another port:
161
+
162
+ ```bash
163
+ EUPHONY_PORT=3001 node ~/.codex/skills/codex-euphony/scripts/codex-euphony.mjs open
164
+ EUPHONY_PORT=3001 ~/.codebuddy/skills/codebuddy-euphony/scripts/codebuddy-euphony.mjs open
165
+ ```
166
+
167
+ Windows PowerShell example:
168
+
169
+ ```powershell
170
+ $env:EUPHONY_PORT = "3001"
171
+ node "$env:USERPROFILE\.codex\skills\codex-euphony\scripts\codex-euphony.mjs" open
172
+ ```
173
+
174
+ ## Privacy
175
+
176
+ Session JSONL files can contain prompts, paths, tool output, and secrets that appeared in conversations. The skills serve staged session data only from a local `127.0.0.1` Euphony instance.
177
+
178
+ Do not commit generated session files, staged JSONL output, `.env` files, or Euphony cache directories. This repository's package contents are limited to the CLI, README files, LICENSE, and skill source files.
179
+
180
+ ## Troubleshooting
181
+
182
+ If a host app does not see the skill, restart Codex or CodeBuddy after installation.
183
+
184
+ If installation says the skill already exists, rerun with `--force`:
185
+
186
+ ```bash
187
+ npx @jefferylau/euphony-skills install codebuddy --force
188
+ ```
189
+
190
+ If Euphony fails to start, check prerequisites:
191
+
192
+ ```bash
193
+ npx @jefferylau/euphony-skills doctor
194
+ ```
195
+
196
+ If `npm` reports cache permission errors, use a temporary cache or repair the npm cache ownership:
197
+
198
+ ```bash
199
+ npm_config_cache=/tmp/euphony-skills-npm-cache npx @jefferylau/euphony-skills doctor
200
+ ```
201
+
202
+ If multiple Euphony servers are running, stop the relevant one or choose a different port:
203
+
204
+ ```bash
205
+ node ~/.codex/skills/codex-euphony/scripts/codex-euphony.mjs stop
206
+ ~/.codebuddy/skills/codebuddy-euphony/scripts/codebuddy-euphony.mjs stop
207
+ ```
208
+
209
+ ## Development
210
+
211
+ Run checks:
212
+
213
+ ```bash
214
+ npm run check
215
+ ```
216
+
217
+ Test install behavior without touching real homes:
218
+
219
+ ```bash
220
+ CODEX_HOME=/tmp/euphony-test-codex \
221
+ CODEBUDDY_HOME=/tmp/euphony-test-codebuddy \
222
+ node bin/euphony-skills.mjs install all --force
223
+ ```
224
+
225
+ Test symlink install behavior:
226
+
227
+ ```bash
228
+ CODEX_HOME=/tmp/euphony-test-codex \
229
+ CODEBUDDY_HOME=/tmp/euphony-test-codebuddy \
230
+ node bin/euphony-skills.mjs install all --force --link
231
+ ```
package/README_CN.md ADDED
@@ -0,0 +1,231 @@
1
+ # Euphony Skills
2
+
3
+ [English](README.md) | [简体中文](README_CN.md)
4
+
5
+ 为 Codex 和 CodeBuddy 安装本地 Euphony 会话查看 skill。
6
+
7
+ 这个仓库包含两个独立 skill,并提供一个 npm 安装器:
8
+
9
+ - `skills/codex-euphony`:用 OpenAI Euphony 打开本地 Codex 会话 JSONL。
10
+ - `skills/codebuddy-euphony`:把本地 CodeBuddy 会话 JSONL 转成 Euphony 兼容格式后打开。
11
+
12
+ 每个 skill 目录都是自包含的,也可以按路径单独安装。推荐使用 npm CLI,因为它能统一处理 Codex 和 CodeBuddy 两种宿主。
13
+
14
+ ## 环境要求
15
+
16
+ - Node.js 18 或更高版本。
17
+ - `git`,skill 首次拉取 Euphony 运行时 checkout 时使用。
18
+ - `pnpm` 或 `corepack`,用于安装和运行 Euphony。
19
+ - macOS、Linux 或 Windows,并支持本地浏览器打开命令。
20
+
21
+ 安装器本身不会安装 Euphony 依赖;对应 skill 会在第一次启动 Euphony 时按需安装。
22
+ 运行时会优先使用已安装的 `pnpm`;如果只有 Corepack,则使用 `corepack pnpm`,并仅对该子进程设置 `COREPACK_INTEGRITY_KEYS=0`,避开常见的 Corepack pnpm 签名 bootstrap 失败。
23
+
24
+ ## 快速安装
25
+
26
+ 发布到 npm 后,可以这样安装:
27
+
28
+ ```bash
29
+ npx @jefferylau/euphony-skills install codex
30
+ npx @jefferylau/euphony-skills install codebuddy
31
+ npx @jefferylau/euphony-skills install all
32
+ ```
33
+
34
+ 替换已有安装:
35
+
36
+ ```bash
37
+ npx @jefferylau/euphony-skills install all --force
38
+ ```
39
+
40
+ 卸载已安装的 skill:
41
+
42
+ ```bash
43
+ npx @jefferylau/euphony-skills uninstall all
44
+ ```
45
+
46
+ 检查本机状态:
47
+
48
+ ```bash
49
+ npx @jefferylau/euphony-skills doctor
50
+ ```
51
+
52
+ 安装后需要重启 Codex 或 CodeBuddy,让宿主重新加载 skill。
53
+
54
+ ## 本地 Checkout 安装
55
+
56
+ 从本地 clone 安装:
57
+
58
+ ```bash
59
+ git clone https://github.com/liiujinfu/euphony-skills.git
60
+ cd euphony-skills
61
+ node bin/euphony-skills.mjs install all --force
62
+ ```
63
+
64
+ 本地开发时可以使用软链接安装,这样修改仓库里的代码会立即生效:
65
+
66
+ ```bash
67
+ node bin/euphony-skills.mjs install all --force --link
68
+ ```
69
+
70
+ 普通用户建议使用默认复制安装,不要加 `--link`。
71
+
72
+ ## 安装位置
73
+
74
+ Codex 安装到:
75
+
76
+ ```text
77
+ ${CODEX_HOME:-~/.codex}/skills/codex-euphony
78
+ ```
79
+
80
+ CodeBuddy 安装到:
81
+
82
+ ```text
83
+ ${CODEBUDDY_HOME:-~/.codebuddy}/skills/codebuddy-euphony
84
+ ```
85
+
86
+ 需要时可以覆盖宿主 home 目录:
87
+
88
+ ```bash
89
+ CODEX_HOME=/custom/.codex npx @jefferylau/euphony-skills install codex
90
+ CODEBUDDY_HOME=/custom/.codebuddy npx @jefferylau/euphony-skills install codebuddy
91
+ ```
92
+
93
+ ## CLI 参考
94
+
95
+ ```bash
96
+ euphony-skills install codex [--force] [--link]
97
+ euphony-skills install codebuddy [--force] [--link]
98
+ euphony-skills install all [--force] [--link]
99
+ euphony-skills uninstall codex
100
+ euphony-skills uninstall codebuddy
101
+ euphony-skills uninstall all
102
+ euphony-skills doctor
103
+ ```
104
+
105
+ 参数:
106
+
107
+ - `--force`:替换已有安装。
108
+ - `--link`:从宿主 skill 目录创建到当前 checkout 的软链接,只建议开发时使用。
109
+
110
+ ## 使用方式
111
+
112
+ 安装并重启宿主后,可以直接让助手用 Euphony 打开最新会话。
113
+
114
+ Codex 示例:
115
+
116
+ ```text
117
+ Use codex-euphony to open the latest Codex session.
118
+ Open this Codex conversation in Euphony.
119
+ ```
120
+
121
+ CodeBuddy 示例:
122
+
123
+ ```text
124
+ Use codebuddy-euphony to open the latest CodeBuddy session.
125
+ Open this CodeBuddy conversation in Euphony.
126
+ ```
127
+
128
+ 也可以直接运行脚本。
129
+
130
+ Codex:
131
+
132
+ ```bash
133
+ node ~/.codex/skills/codex-euphony/scripts/codex-euphony.mjs open
134
+ node ~/.codex/skills/codex-euphony/scripts/codex-euphony.mjs status
135
+ node ~/.codex/skills/codex-euphony/scripts/codex-euphony.mjs stop
136
+ ```
137
+
138
+ CodeBuddy:
139
+
140
+ ```bash
141
+ ~/.codebuddy/skills/codebuddy-euphony/scripts/codebuddy-euphony.mjs open
142
+ ~/.codebuddy/skills/codebuddy-euphony/scripts/codebuddy-euphony.mjs status
143
+ ~/.codebuddy/skills/codebuddy-euphony/scripts/codebuddy-euphony.mjs stop
144
+ ```
145
+
146
+ ## 运行时行为
147
+
148
+ 安装器只复制或链接 skill 目录,不会复制会话日志、生成的 JSONL、本地缓存或 Euphony checkout。
149
+
150
+ 运行时:
151
+
152
+ - Codex 使用 `${CODEX_HOME:-~/.codex}/cache/euphony`。
153
+ - CodeBuddy 使用 `${CODEBUDDY_HOME:-~/.codebuddy}/cache/euphony`。
154
+ - 如果缓存被删除,下一次需要 Euphony 时会自动重建。
155
+ - 本地 Euphony 服务绑定到 `127.0.0.1`。
156
+ - 默认端口是 `3000`。
157
+ - Codex staging 在 macOS/Linux 默认使用软链接,在 Windows 默认复制文件。设置 `EUPHONY_STAGE_MODE=copy` 可以在所有平台强制使用快照复制。
158
+ - 后台服务通过 Euphony cache 下的 pid 文件跟踪,所以 `stop` 只会停止同一个 skill 脚本启动的服务。
159
+
160
+ 如果 `3000` 端口已被占用,可以换端口:
161
+
162
+ ```bash
163
+ EUPHONY_PORT=3001 node ~/.codex/skills/codex-euphony/scripts/codex-euphony.mjs open
164
+ EUPHONY_PORT=3001 ~/.codebuddy/skills/codebuddy-euphony/scripts/codebuddy-euphony.mjs open
165
+ ```
166
+
167
+ Windows PowerShell 示例:
168
+
169
+ ```powershell
170
+ $env:EUPHONY_PORT = "3001"
171
+ node "$env:USERPROFILE\.codex\skills\codex-euphony\scripts\codex-euphony.mjs" open
172
+ ```
173
+
174
+ ## 隐私
175
+
176
+ 会话 JSONL 可能包含提示词、路径、工具输出,以及对话中出现过的敏感信息。skill 只会通过本地 `127.0.0.1` Euphony 实例提供临时 staged 数据。
177
+
178
+ 不要提交生成的会话文件、staged JSONL 输出、`.env` 文件或 Euphony 缓存目录。这个仓库的 npm 包内容只包含 CLI、README、LICENSE 和 skill 源码。
179
+
180
+ ## 排错
181
+
182
+ 如果宿主没有识别到 skill,安装后先重启 Codex 或 CodeBuddy。
183
+
184
+ 如果安装提示 skill 已存在,使用 `--force`:
185
+
186
+ ```bash
187
+ npx @jefferylau/euphony-skills install codebuddy --force
188
+ ```
189
+
190
+ 如果 Euphony 启动失败,先检查依赖和安装状态:
191
+
192
+ ```bash
193
+ npx @jefferylau/euphony-skills doctor
194
+ ```
195
+
196
+ 如果 `npm` 报 cache 权限错误,可以使用临时 cache,或修复 npm cache 目录权限:
197
+
198
+ ```bash
199
+ npm_config_cache=/tmp/euphony-skills-npm-cache npx @jefferylau/euphony-skills doctor
200
+ ```
201
+
202
+ 如果有多个 Euphony 服务在跑,停止对应服务或换端口:
203
+
204
+ ```bash
205
+ node ~/.codex/skills/codex-euphony/scripts/codex-euphony.mjs stop
206
+ ~/.codebuddy/skills/codebuddy-euphony/scripts/codebuddy-euphony.mjs stop
207
+ ```
208
+
209
+ ## 开发
210
+
211
+ 运行检查:
212
+
213
+ ```bash
214
+ npm run check
215
+ ```
216
+
217
+ 不影响真实 home 目录的安装测试:
218
+
219
+ ```bash
220
+ CODEX_HOME=/tmp/euphony-test-codex \
221
+ CODEBUDDY_HOME=/tmp/euphony-test-codebuddy \
222
+ node bin/euphony-skills.mjs install all --force
223
+ ```
224
+
225
+ 测试软链接安装:
226
+
227
+ ```bash
228
+ CODEX_HOME=/tmp/euphony-test-codex \
229
+ CODEBUDDY_HOME=/tmp/euphony-test-codebuddy \
230
+ node bin/euphony-skills.mjs install all --force --link
231
+ ```
@@ -0,0 +1,221 @@
1
+ #!/usr/bin/env node
2
+ import { execFileSync } from 'node:child_process';
3
+ import fs from 'node:fs';
4
+ import os from 'node:os';
5
+ import path from 'node:path';
6
+ import { fileURLToPath } from 'node:url';
7
+
8
+ const rootDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
9
+ const home = os.homedir();
10
+ const isWindows = process.platform === 'win32';
11
+
12
+ const targets = {
13
+ codex: {
14
+ label: 'Codex',
15
+ homeEnv: 'CODEX_HOME',
16
+ defaultHome: path.join(home, '.codex'),
17
+ skillName: 'codex-euphony',
18
+ executables: ['scripts/codex-euphony.mjs', 'scripts/codex-euphony.sh']
19
+ },
20
+ codebuddy: {
21
+ label: 'CodeBuddy',
22
+ homeEnv: 'CODEBUDDY_HOME',
23
+ defaultHome: path.join(home, '.codebuddy'),
24
+ skillName: 'codebuddy-euphony',
25
+ executables: ['scripts/codebuddy-euphony.mjs']
26
+ }
27
+ };
28
+
29
+ function usage() {
30
+ console.log(`Usage: euphony-skills <command> [target] [options]
31
+
32
+ Commands:
33
+ install codex|codebuddy|all Install skill(s).
34
+ uninstall codex|codebuddy|all Remove installed skill(s).
35
+ doctor Check local install prerequisites and state.
36
+ help Show this help.
37
+
38
+ Options:
39
+ --force Replace an existing install.
40
+ --link Symlink from this checkout instead of copying.
41
+
42
+ Environment:
43
+ CODEX_HOME Override Codex home. Default: ~/.codex
44
+ CODEBUDDY_HOME Override CodeBuddy home. Default: ~/.codebuddy`);
45
+ }
46
+
47
+ function fail(message) {
48
+ console.error(message);
49
+ process.exit(1);
50
+ }
51
+
52
+ function commandExists(command) {
53
+ try {
54
+ if (isWindows) {
55
+ execFileSync('where', [command], { stdio: 'ignore', shell: true });
56
+ } else {
57
+ execFileSync('which', [command], { stdio: 'ignore' });
58
+ }
59
+ return true;
60
+ } catch {
61
+ return false;
62
+ }
63
+ }
64
+
65
+ function parseArgs(argv) {
66
+ const args = [...argv];
67
+ const flags = new Set(args.filter(arg => arg.startsWith('--')));
68
+ const positional = args.filter(arg => !arg.startsWith('--'));
69
+ return {
70
+ command: positional[0] || 'help',
71
+ target: positional[1],
72
+ force: flags.has('--force'),
73
+ link: flags.has('--link')
74
+ };
75
+ }
76
+
77
+ function expandTargets(target) {
78
+ if (target === 'all') return Object.keys(targets);
79
+ if (target && targets[target]) return [target];
80
+ fail(`Unknown target: ${target || '(missing)'}
81
+
82
+ Expected one of: codex, codebuddy, all`);
83
+ }
84
+
85
+ function sourceDirFor(targetKey) {
86
+ return path.join(rootDir, 'skills', targets[targetKey].skillName);
87
+ }
88
+
89
+ function homeDirFor(targetKey) {
90
+ const target = targets[targetKey];
91
+ return process.env[target.homeEnv] || target.defaultHome;
92
+ }
93
+
94
+ function installDirFor(targetKey) {
95
+ return path.join(homeDirFor(targetKey), 'skills', targets[targetKey].skillName);
96
+ }
97
+
98
+ function removeExisting(dest) {
99
+ if (!pathExists(dest)) return;
100
+ fs.rmSync(dest, { recursive: true, force: true });
101
+ }
102
+
103
+ function pathExists(file) {
104
+ return fs.lstatSync(file, { throwIfNoEntry: false }) !== undefined;
105
+ }
106
+
107
+ function copyRecursive(src, dest) {
108
+ fs.cpSync(src, dest, {
109
+ recursive: true,
110
+ dereference: false,
111
+ filter: source => !source.split(path.sep).includes('.git')
112
+ });
113
+ }
114
+
115
+ function chmodExecutables(targetKey) {
116
+ if (isWindows) return;
117
+ const target = targets[targetKey];
118
+ const dest = installDirFor(targetKey);
119
+ for (const relative of target.executables) {
120
+ const file = path.join(dest, relative);
121
+ if (fs.existsSync(file)) fs.chmodSync(file, 0o755);
122
+ }
123
+ }
124
+
125
+ function installOne(targetKey, options) {
126
+ const source = sourceDirFor(targetKey);
127
+ const dest = installDirFor(targetKey);
128
+ if (!fs.existsSync(path.join(source, 'SKILL.md'))) {
129
+ fail(`Missing skill source: ${source}`);
130
+ }
131
+ if (pathExists(dest)) {
132
+ if (!options.force) {
133
+ fail(`${targets[targetKey].label} skill already exists: ${dest}
134
+ Use --force to replace it.`);
135
+ }
136
+ removeExisting(dest);
137
+ }
138
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
139
+ if (options.link) {
140
+ fs.symlinkSync(source, dest, 'dir');
141
+ } else {
142
+ copyRecursive(source, dest);
143
+ }
144
+ if (!options.link) chmodExecutables(targetKey);
145
+ console.log(`Installed ${targets[targetKey].label}: ${dest}${options.link ? ` -> ${source}` : ''}`);
146
+ }
147
+
148
+ function uninstallOne(targetKey) {
149
+ const dest = installDirFor(targetKey);
150
+ if (!pathExists(dest)) {
151
+ console.log(`${targets[targetKey].label} skill is not installed: ${dest}`);
152
+ return;
153
+ }
154
+ removeExisting(dest);
155
+ console.log(`Removed ${targets[targetKey].label}: ${dest}`);
156
+ }
157
+
158
+ function statInstall(targetKey) {
159
+ const target = targets[targetKey];
160
+ const source = sourceDirFor(targetKey);
161
+ const dest = installDirFor(targetKey);
162
+ const installed = pathExists(dest);
163
+ const linkTarget = installed && fs.lstatSync(dest).isSymbolicLink() ? fs.readlinkSync(dest) : null;
164
+ const executableStatus = target.executables.map(relative => {
165
+ const file = path.join(dest, relative);
166
+ if (!fs.existsSync(file)) return `${relative}: missing`;
167
+ const mode = fs.statSync(file).mode;
168
+ return `${relative}: ${(mode & 0o111) !== 0 ? 'executable' : 'not executable'}`;
169
+ });
170
+ return {
171
+ target,
172
+ source,
173
+ dest,
174
+ installed,
175
+ linkTarget,
176
+ executableStatus
177
+ };
178
+ }
179
+
180
+ function doctor() {
181
+ console.log(`Node: ${process.version}`);
182
+ for (const command of ['git', 'pnpm', 'corepack']) {
183
+ console.log(`${command}: ${commandExists(command) ? 'found' : 'missing'}`);
184
+ }
185
+ console.log(`package runner: ${commandExists('pnpm') ? 'pnpm' : commandExists('corepack') ? 'corepack pnpm' : 'missing'}`);
186
+ for (const key of Object.keys(targets)) {
187
+ const status = statInstall(key);
188
+ console.log(`\n${status.target.label}`);
189
+ console.log(` source: ${status.source}`);
190
+ console.log(` install: ${status.dest}`);
191
+ console.log(` installed: ${status.installed ? 'yes' : 'no'}`);
192
+ if (status.linkTarget) console.log(` link: ${status.linkTarget}`);
193
+ for (const line of status.executableStatus) console.log(` ${line}`);
194
+ }
195
+ }
196
+
197
+ function main() {
198
+ const args = parseArgs(process.argv.slice(2));
199
+ switch (args.command) {
200
+ case 'install':
201
+ for (const targetKey of expandTargets(args.target)) installOne(targetKey, args);
202
+ console.log('Restart Codex or CodeBuddy to pick up newly installed skills.');
203
+ break;
204
+ case 'uninstall':
205
+ for (const targetKey of expandTargets(args.target)) uninstallOne(targetKey);
206
+ break;
207
+ case 'doctor':
208
+ doctor();
209
+ break;
210
+ case 'help':
211
+ case '--help':
212
+ case '-h':
213
+ usage();
214
+ break;
215
+ default:
216
+ usage();
217
+ process.exitCode = 1;
218
+ }
219
+ }
220
+
221
+ main();