dsh-codex-port 0.1.1 → 0.2.1

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 stardustlc
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.en.md CHANGED
@@ -1,5 +1,7 @@
1
1
  [中文](README.md)
2
2
 
3
+ ![npm](https://img.shields.io/npm/v/dsh-codex-port) ![downloads](https://img.shields.io/npm/dm/dsh-codex-port) ![license](https://img.shields.io/github/license/STARDUSTLC666/dsh-codex-port) ![stars](https://img.shields.io/github/stars/STARDUSTLC666/dsh-codex-port?style=social)
4
+
3
5
  # dsh-codex-port
4
6
 
5
7
  [![Awesome DSH Plugin](https://awesome-dsh-plugin.com/badge.svg)](https://awesome-dsh-plugin.com)
@@ -8,6 +10,10 @@ Move the whole **official Codex plugin family** into DSH: scan `~/.codex` unpack
8
10
 
9
11
  > Measured on a real machine: 186 official Codex plugins, 583 skills — one port run moved 577 successfully, 0 failures.
10
12
 
13
+ ## Compatibility
14
+
15
+ Verified against `@deepseek-ai/dsh@0.1.2-alpha.2` on 2026-08-31. Built for the cordis patch-bundle plugin model (`cordis.patch.yml` + `dsh.bundle.patch`). No runtime imports of `@deepseek-ai/*` internals.
16
+
11
17
  ## Installation
12
18
 
13
19
  ```bash
@@ -16,6 +22,15 @@ dsh plugin --profile web add dsh-codex-port
16
22
 
17
23
  Requires the Codex CLI to be installed (the `~/.codex` directory must exist).
18
24
 
25
+ ## Uninstall
26
+
27
+ ```bash
28
+ dsh plugin --profile web remove dsh-codex-port
29
+ ```
30
+
31
+ Then restart the web service. To clean up fully, also remove the plugin entry from your profile `cordis.patch.yml` if you overrode it.
32
+
33
+
19
34
  ## Configuration
20
35
 
21
36
  Everything is optional; defaults just work:
@@ -55,14 +70,17 @@ Ported skills are immediately usable from the DSH skills directory; the agent tr
55
70
  - **Frontmatter conversion**: Codex `name/description/metadata` → DSH `name/description/compatibility/allowed-tools`, multi-line descriptions preserved
56
71
  - **Codex-only files stripped**: `agents/*.yaml` and friends stay behind
57
72
  - **Name sanitization**: invalid characters become underscores; `..` traversal rejected outright
73
+ - **Host loadability checks**: before replacement, the converted name must follow DSH's lowercase kebab-case grammar, exactly match its target directory, and have a nonempty description. Names with uppercase letters, underscores, dots, or a different sanitized directory name cannot replace an existing skill.
58
74
  - **Idempotent**: same-name skills are skipped by default; `overwrite=true` to replace
75
+ - **Recoverable replacement**: copying, conversion, and read-back validation finish inside a unique `.dsh-port-<skill>-*` directory under the target root before the old skill moves to `previous/` and the new skill takes its place. Copy/conversion failures leave the old skill untouched. A failed switch attempts rollback; if rollback also fails, both copies remain and the error includes their recovery location. Content created by another writer is never deleted.
76
+ - **Backups and path checks**: successful replacements retain `previous/` and `recovery.json`, using additional disk space; archive them manually when recovery is no longer needed. No `SKILL.md` sits at the transaction directory's top level, so current DSH one-level skill discovery does not register backups. Overlapping source/target paths, target symlinks/junctions, paths outside the target root, and Windows device names are rejected. Multiple renames are not one atomic Windows transaction; after process or system interruption, inspect `recovery.json` to restore the appropriate directory.
59
77
  - **Safe**: pure filesystem operations, zero runtime dependencies (yaml parsing only)
60
78
 
61
79
  ## Development
62
80
 
63
81
  ```bash
64
82
  pnpm install
65
- pnpm test # build + 33 tests
83
+ pnpm test # build + offline tests; fixtures stay under workspace .harness-validation/codex-port-tests
66
84
  ```
67
85
 
68
86
  ## License
package/README.md CHANGED
@@ -13,6 +13,10 @@
13
13
 
14
14
  > 本机实测:186 个 Codex 官方插件、583 个技能,一次移植 577 个成功、0 失败。
15
15
 
16
+ ## 兼容性
17
+
18
+ 在 `@deepseek-ai/dsh@0.1.2-alpha.2` 上验证(2026-08-31)。遵循 cordis 组合包补丁模型(`cordis.patch.yml` + `dsh.bundle.patch`),运行时不 import 任何 `@deepseek-ai/*` 内部模块。
19
+
16
20
  ## 安装
17
21
 
18
22
  ```bash
@@ -21,6 +25,15 @@ dsh plugin --profile web add dsh-codex-port
21
25
 
22
26
  需要本机装有 Codex CLI(`~/.codex` 目录存在即可)。
23
27
 
28
+ ## 卸载
29
+
30
+ ```bash
31
+ dsh plugin --profile web remove dsh-codex-port
32
+ ```
33
+
34
+ 卸载后重启 Web 服务。如需彻底清理,可再手动删除自己 profile `cordis.patch.yml` 中覆盖的插件行。
35
+
36
+
24
37
  ## 配置
25
38
 
26
39
  全部可选,默认即可用:
@@ -76,18 +89,19 @@ codex_port { targetDir: ~/.claude/skills }
76
89
  - **frontmatter 转换**:Codex 的 `name/description/metadata` → DSH 的 `name/description/compatibility/allowed-tools`,多行描述完整保留
77
90
  - **剔除 codex 专属文件**:`agents/*.yaml` 等子代理描述不带走
78
91
  - **名称清洗**:非法字符替换为下划线,`..` 路径穿越直接拒绝
92
+ - **宿主可加载性校验**:覆盖前要求转换后的技能名符合 DSH 的小写短横线命名规则、与目标目录同名,且描述为非空字符串;大写、下划线、点号或清洗后不同名的技能不会替换旧版。
79
93
  - **幂等**:同名技能默认跳过,`overwrite=true` 才覆盖
94
+ - **覆盖失败可恢复**:先在目标根内的唯一 `.dsh-port-<技能名>-*` 目录中复制、转换并回读校验;成功后才把旧技能移到事务目录的 `previous/`,再切换新技能。复制或转换失败不会改动旧技能,切换失败会尝试恢复旧技能;恢复失败时保留两份资料,并在失败原因中给出恢复路径。不会删除已存在的其他写入。
95
+ - **备份与目录边界**:成功覆盖也保留 `previous/` 及 `recovery.json`,会额外占用磁盘空间;确认不需要恢复后可自行归档。事务目录顶层不含 `SKILL.md`,不会被当前 DSH 的单层技能发现重复注册。拒绝源/目标重叠、目标符号链接或目录联接、路径越界及 Windows 设备名称。多次目录重命名不构成 Windows 上全程原子的事务;进程或系统中断后可按 `recovery.json` 检查和恢复。
80
96
  - **安全**:纯文件系统操作,零运行时依赖(仅 yaml 解析)
81
97
 
82
98
  ## 开发
83
99
 
84
100
  ```bash
85
101
  pnpm install
86
- pnpm test # 构建 + 33 个测试
102
+ pnpm test # 构建 + 离线测试;fixture 只写入工作区 .harness-validation/codex-port-tests
87
103
  ```
88
104
 
89
105
  ## License
90
106
 
91
107
  MIT
92
-
93
-
package/lib/discover.js CHANGED
@@ -17,6 +17,25 @@ function isDir(path) {
17
17
  return false;
18
18
  }
19
19
  }
20
+ /** 语义化版本比较:按数字段逐段比较,忽略 v 前缀与 pre-release 后缀。 */
21
+ function isNewerVersion(a, b) {
22
+ const parse = (v) => {
23
+ const match = /v?(\d+(?:\.\d+)*)/.exec(v);
24
+ if (match === null)
25
+ return [];
26
+ return match[1].split('.').map((n) => Number(n));
27
+ };
28
+ const pa = parse(a);
29
+ const pb = parse(b);
30
+ const len = Math.max(pa.length, pb.length);
31
+ for (let i = 0; i < len; i += 1) {
32
+ const x = pa[i] ?? 0;
33
+ const y = pb[i] ?? 0;
34
+ if (x !== y)
35
+ return x > y;
36
+ }
37
+ return false;
38
+ }
20
39
  /** 读取 plugin.json;缺失或非法返回 null。 */
21
40
  function readPluginManifest(pluginDir) {
22
41
  const manifestPath = join(pluginDir, '.codex-plugin', 'plugin.json');
@@ -124,7 +143,7 @@ export function discoverPlugins(codexHome) {
124
143
  }
125
144
  else {
126
145
  const betterPriority = candidate.priority > (candidates.find((c) => c.dir === existing.sourceDir)?.priority ?? -1);
127
- const newerVersion = manifest.version !== 'unknown' && manifest.version !== existing.version && manifest.version > existing.version;
146
+ const newerVersion = manifest.version !== 'unknown' && manifest.version !== existing.version && isNewerVersion(manifest.version, existing.version);
128
147
  if (betterPriority || newerVersion)
129
148
  found.set(key, info);
130
149
  }
package/lib/index.d.ts CHANGED
@@ -10,6 +10,7 @@
10
10
  import { type CodexPortConfig } from './config.js';
11
11
  import { type CodexPortToolDefinition } from './tools.js';
12
12
  /** cordis 服务注入:apply 里要用 ctx.tools,必须显式声明。 */
13
+ export declare const name = "codex-port";
13
14
  export declare const inject: string[];
14
15
  /** 插件所需的最小 ctx 面。 */
15
16
  export interface CodexPortPluginContext {
package/lib/index.js CHANGED
@@ -10,6 +10,7 @@
10
10
  import { resolveConfig } from './config.js';
11
11
  import { buildCodexPortTools } from './tools.js';
12
12
  /** cordis 服务注入:apply 里要用 ctx.tools,必须显式声明。 */
13
+ export const name = 'codex-port';
13
14
  export const inject = ['tools'];
14
15
  /**
15
16
  * 插件入口:解析配置并注册三个移植工具。
package/lib/port.d.ts CHANGED
@@ -8,6 +8,7 @@ export interface PortResult {
8
8
  files: number;
9
9
  }
10
10
  /**
11
- * 移植单个技能到目标目录。
11
+ * Port one skill without deleting its previous installation. Each rename is a
12
+ * filesystem operation; the complete replacement is not atomic on Windows.
12
13
  */
13
14
  export declare function portSkill(skill: CodexSkillSource, plugin: CodexPluginInfo, targetDir: string, overwrite: boolean): PortResult;
package/lib/port.js CHANGED
@@ -1,57 +1,198 @@
1
1
  /**
2
- * 移植执行:目录拷贝(剔除 agents codex 专属文件)+ SKILL.md 转换 + 幂等跳过。
3
- *
2
+ * Stage and validate skill copies before replacing an installation; retain backups for recovery.
4
3
  * @module dsh-codex-port/port
5
4
  */
6
- import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync, rmSync } from 'node:fs';
7
- import { join } from 'node:path';
8
- import { convertSkillFile, sanitizeSkillName } from './frontmatter.js';
9
- /** codex 专属目录,移植时剔除。 */
5
+ import fs from 'node:fs';
6
+ import { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
7
+ import { convertSkillFile, parseSkillFile, sanitizeSkillName } from './frontmatter.js';
10
8
  const EXCLUDED_DIRS = new Set(['agents']);
11
- /** 递归拷贝,跳过排除目录;返回拷贝文件数。 */
12
- function copyTree(source, target) {
13
- mkdirSync(target, { recursive: true });
9
+ // Matches Harness skill.isSkillName; keep the public filename sanitizer unchanged.
10
+ const DSH_SKILL_NAME = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
11
+ function isWithin(root, path) {
12
+ const child = relative(root, path);
13
+ return child !== '' && child !== '..' && !child.startsWith('..' + sep) && !isAbsolute(child);
14
+ }
15
+ function errorText(error) {
16
+ return error instanceof Error ? error.message : String(error);
17
+ }
18
+ /** Anchor every move to the resolved target root, rejecting symlinks and changed parents. */
19
+ class InstallRoot {
20
+ path;
21
+ constructor(targetDir) {
22
+ if (targetDir.trim() === '')
23
+ throw new Error('目标技能目录不能为空');
24
+ const requested = resolve(targetDir);
25
+ if (dirname(requested) === requested)
26
+ throw new Error('不能把文件系统根目录作为技能目录');
27
+ const existing = fs.lstatSync(requested, { throwIfNoEntry: false });
28
+ if (existing?.isSymbolicLink())
29
+ throw new Error('目标技能根目录不能是符号链接或目录联接');
30
+ // Resolve absent target roots through their existing ancestor without creating
31
+ // anything yet: overlap with the source must fail before filesystem writes.
32
+ let ancestor = requested;
33
+ while (fs.lstatSync(ancestor, { throwIfNoEntry: false }) === undefined)
34
+ ancestor = dirname(ancestor);
35
+ this.path = resolve(fs.realpathSync(ancestor), relative(ancestor, requested));
36
+ }
37
+ check(path) {
38
+ const absolute = resolve(path);
39
+ if (!isWithin(this.path, absolute))
40
+ throw new Error('安装路径越出目标技能根目录:' + absolute);
41
+ const root = fs.lstatSync(this.path);
42
+ if (!root.isDirectory() || root.isSymbolicLink() || fs.realpathSync(this.path) !== this.path) {
43
+ throw new Error('目标技能根目录在安装期间发生变化');
44
+ }
45
+ const parent = fs.realpathSync(dirname(absolute));
46
+ if (parent !== this.path && !isWithin(this.path, parent))
47
+ throw new Error('安装路径的父目录越界:' + absolute);
48
+ if (parent !== dirname(absolute))
49
+ throw new Error('安装路径的父目录包含符号链接或目录联接:' + absolute);
50
+ const entry = fs.lstatSync(absolute, { throwIfNoEntry: false });
51
+ if (entry?.isSymbolicLink())
52
+ throw new Error('安装路径不能是符号链接或目录联接:' + absolute);
53
+ if (entry !== undefined && !isWithin(this.path, fs.realpathSync(absolute))) {
54
+ throw new Error('安装路径解析后越界:' + absolute);
55
+ }
56
+ return absolute;
57
+ }
58
+ move(source, target) {
59
+ const from = this.check(source);
60
+ const to = this.check(target);
61
+ if (!fs.lstatSync(from).isDirectory())
62
+ throw new Error('只能切换技能目录:' + from);
63
+ if (fs.lstatSync(to, { throwIfNoEntry: false }) !== undefined)
64
+ throw new Error('切换目标已存在,保留现场:' + to);
65
+ fs.renameSync(from, to);
66
+ }
67
+ }
68
+ /** Copy only regular files/directories; the destination is a new private staging tree. */
69
+ function copyTree(source, target, sourceRoot, root) {
70
+ if (source !== sourceRoot && !isWithin(sourceRoot, fs.realpathSync(source)))
71
+ throw new Error('技能源目录越界');
72
+ if (fs.lstatSync(source).isSymbolicLink())
73
+ throw new Error('技能源目录不能是符号链接');
74
+ fs.mkdirSync(root.check(target));
14
75
  let count = 0;
15
- for (const entry of readdirSync(source, { withFileTypes: true })) {
16
- if (entry.isDirectory() && EXCLUDED_DIRS.has(entry.name))
17
- continue;
76
+ for (const entry of fs.readdirSync(source, { withFileTypes: true })) {
18
77
  const from = join(source, entry.name);
19
- const to = join(target, entry.name);
20
- if (entry.isDirectory()) {
21
- count += copyTree(from, to);
22
- }
23
- else {
24
- writeFileSync(to, readFileSync(from));
78
+ const to = root.check(join(target, entry.name));
79
+ const stat = fs.lstatSync(from);
80
+ if (stat.isSymbolicLink())
81
+ continue;
82
+ if (stat.isDirectory() && EXCLUDED_DIRS.has(entry.name))
83
+ continue;
84
+ if (!isWithin(sourceRoot, fs.realpathSync(from)))
85
+ throw new Error('技能源文件解析后越界:' + from);
86
+ if (stat.isDirectory())
87
+ count += copyTree(from, to, sourceRoot, root);
88
+ else if (stat.isFile()) {
89
+ fs.copyFileSync(from, to, fs.constants.COPYFILE_EXCL);
25
90
  count += 1;
26
91
  }
92
+ else
93
+ throw new Error('技能目录含非普通文件:' + from);
27
94
  }
28
95
  return count;
29
96
  }
30
97
  /**
31
- * 移植单个技能到目标目录。
98
+ * Port one skill without deleting its previous installation. Each rename is a
99
+ * filesystem operation; the complete replacement is not atomic on Windows.
32
100
  */
33
101
  export function portSkill(skill, plugin, targetDir, overwrite) {
34
102
  const safeName = sanitizeSkillName(skill.skillName !== '' ? skill.skillName : skill.skillDirName);
35
- if (safeName === null) {
103
+ if (safeName === null || safeName.endsWith('.') || /^(?:con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\.|$)/i.test(safeName)) {
36
104
  return { skill: skill.skillDirName, plugin: plugin.name, status: 'failed', reason: '技能名清洗后为空或不合法,已跳过(可能是纯中文或含危险字符的名称)', files: 0 };
37
105
  }
38
- const target = join(targetDir, safeName);
39
- if (existsSync(target) && !overwrite) {
40
- return { skill: safeName, plugin: plugin.name, status: 'skipped', reason: '目标技能已存在(overwrite=false),跳过以免覆盖', files: 0 };
41
- }
106
+ let root;
107
+ let target = '';
108
+ let transaction = '';
109
+ let incoming = '';
110
+ let previous = '';
111
+ let backedUp = false;
112
+ let installed = false;
113
+ const record = (phase, error) => {
114
+ if (root === undefined || transaction === '')
115
+ return;
116
+ const file = root.check(join(transaction, 'recovery.json'));
117
+ fs.writeFileSync(file, JSON.stringify({ version: 1, skill: safeName, target, incoming, previous, phase, ...(error === undefined ? {} : { error }) }, null, 2) + '\n', 'utf8');
118
+ };
42
119
  try {
43
- if (existsSync(target))
44
- rmSync(target, { recursive: true, force: true });
45
- let files = copyTree(skill.sourceDir, target);
46
- // 重写 SKILL.md 的 frontmatter
47
- const skillFile = join(target, 'SKILL.md');
48
- const original = readFileSync(skillFile, 'utf8');
49
- const converted = convertSkillFile(original, { pluginName: plugin.name, homepage: plugin.homepage, license: plugin.license }, safeName);
50
- writeFileSync(skillFile, converted, 'utf8');
51
- files = Math.max(files, 1);
52
- return { skill: safeName, plugin: plugin.name, status: 'ported', reason: '', files };
120
+ root = new InstallRoot(targetDir);
121
+ target = join(root.path, safeName);
122
+ if (fs.lstatSync(root.path, { throwIfNoEntry: false }) !== undefined)
123
+ root.check(target);
124
+ const existing = fs.lstatSync(target, { throwIfNoEntry: false });
125
+ if (existing !== undefined && !overwrite) {
126
+ return { skill: safeName, plugin: plugin.name, status: 'skipped', reason: '目标技能已存在(overwrite=false),跳过以免覆盖', files: 0 };
127
+ }
128
+ if (existing !== undefined && !existing.isDirectory())
129
+ throw new Error('目标技能不是目录,拒绝覆盖:' + target);
130
+ const sourceEntry = fs.lstatSync(skill.sourceDir);
131
+ if (!sourceEntry.isDirectory() || sourceEntry.isSymbolicLink())
132
+ throw new Error('技能源必须是普通目录');
133
+ const source = fs.realpathSync(skill.sourceDir);
134
+ if (source === root.path || isWithin(source, root.path) || source === target || isWithin(target, source)) {
135
+ throw new Error('技能源与安装目标重叠,拒绝递归复制或移动源目录');
136
+ }
137
+ fs.mkdirSync(root.path, { recursive: true });
138
+ root.check(target);
139
+ transaction = fs.mkdtempSync(root.check(join(root.path, '.dsh-port-' + safeName + '-')));
140
+ incoming = root.check(join(transaction, 'incoming'));
141
+ previous = root.check(join(transaction, 'previous'));
142
+ record('preparing');
143
+ const files = copyTree(source, incoming, source, root);
144
+ const skillFile = root.check(join(incoming, 'SKILL.md'));
145
+ const converted = convertSkillFile(fs.readFileSync(skillFile, 'utf8'), {
146
+ pluginName: plugin.name, homepage: plugin.homepage, license: plugin.license,
147
+ }, safeName);
148
+ fs.writeFileSync(skillFile, converted, 'utf8');
149
+ // Validate the bytes that were actually written, before moving the old skill.
150
+ const parsed = parseSkillFile(fs.readFileSync(skillFile, 'utf8'));
151
+ if (!parsed.hasFrontmatter || typeof parsed.frontmatter.name !== 'string'
152
+ || !DSH_SKILL_NAME.test(parsed.frontmatter.name) || parsed.frontmatter.name !== safeName
153
+ || typeof parsed.frontmatter.description !== 'string' || parsed.frontmatter.description.length === 0) {
154
+ throw new Error('转换后的 SKILL.md 未通过 DSH 技能名称、目录一致性或非空描述校验');
155
+ }
156
+ record('prepared');
157
+ if (existing !== undefined) {
158
+ root.move(target, previous);
159
+ backedUp = true;
160
+ record('previous-backed-up');
161
+ }
162
+ root.move(incoming, target);
163
+ installed = true;
164
+ let reason = '';
165
+ try {
166
+ record('installed');
167
+ }
168
+ catch (error) {
169
+ reason = '技能已安装,但恢复记录更新失败:' + errorText(error) + ';恢复目录:' + transaction;
170
+ }
171
+ return { skill: safeName, plugin: plugin.name, status: 'ported', reason, files: Math.max(files, 1) };
53
172
  }
54
173
  catch (error) {
55
- return { skill: safeName, plugin: plugin.name, status: 'failed', reason: error instanceof Error ? error.message : String(error), files: 0 };
174
+ let reason = errorText(error);
175
+ let phase = 'failed-before-switch';
176
+ if (backedUp && !installed) {
177
+ try {
178
+ root.move(previous, target);
179
+ phase = 'rolled-back';
180
+ reason += ';已恢复原技能';
181
+ }
182
+ catch (rollbackError) {
183
+ phase = 'rollback-failed';
184
+ reason += ';自动恢复失败:' + errorText(rollbackError) + ';原技能备份:' + previous;
185
+ }
186
+ }
187
+ if (transaction !== '') {
188
+ try {
189
+ record(phase, reason);
190
+ }
191
+ catch (recordError) {
192
+ reason += ';恢复记录写入失败:' + errorText(recordError);
193
+ }
194
+ reason += ';恢复资料保留在:' + transaction;
195
+ }
196
+ return { skill: safeName, plugin: plugin.name, status: 'failed', reason, files: 0 };
56
197
  }
57
198
  }
package/lib/tools.js CHANGED
@@ -263,5 +263,44 @@ export function buildCodexPortTools(config) {
263
263
  };
264
264
  },
265
265
  };
266
- return [codexList, codexPort, codexStatus];
266
+ const codexHealth = {
267
+ name: 'codex_health',
268
+ description: 'dsh-codex-port 自检:检查 Codex 主目录(~/.codex 或自定义)是否存在、插件目录是否可见(不执行任何移植)。遇到问题时先运行本工具定位。',
269
+ parameters: compileParameters({
270
+ codexHome: { type: 'string', description: 'Codex 主目录(可选,缺省用配置或 ~/.codex)。' },
271
+ }),
272
+ output: {
273
+ schema: { type: 'object', additionalProperties: true },
274
+ render: (_args, value) => {
275
+ const rec = asRecord(value);
276
+ const checks = Array.isArray(rec.checks) ? rec.checks : [];
277
+ const lines = ['dsh-codex-port 自检' + (rec.ok === true ? ':正常。' : ':发现问题。')];
278
+ for (const item of checks) {
279
+ const c = asRecord(item);
280
+ lines.push('- ' + c.name + ':' + (c.ok === true ? '✅ ' + String(c.detail ?? '') : '❌ ' + String(c.detail ?? '')));
281
+ }
282
+ return [{ type: 'text', text: lines.join('\n') }];
283
+ },
284
+ },
285
+ async execute(rawArgs) {
286
+ const args = asRecord(rawArgs);
287
+ const home = optionalString(args, 'codexHome') ?? config.codexHome;
288
+ const checks = [];
289
+ try {
290
+ assertCodexHome(home);
291
+ checks.push({ name: 'Codex 主目录', ok: true, detail: home });
292
+ }
293
+ catch (error) {
294
+ checks.push({ name: 'Codex 主目录', ok: false, detail: error instanceof Error ? error.message : String(error) });
295
+ return { ok: false, plugin: 'dsh-codex-port', codexHome: home, checks };
296
+ }
297
+ let ok = true;
298
+ const pluginsOk = existsSync(home + '/plugins');
299
+ checks.push({ name: '插件目录', ok: pluginsOk, detail: pluginsOk ? home + '/plugins' : 'plugins/ 不存在(Codex 尚未安装任何插件)' });
300
+ if (!pluginsOk)
301
+ ok = false;
302
+ return { ok, plugin: 'dsh-codex-port', codexHome: home, checks };
303
+ },
304
+ };
305
+ return [codexList, codexPort, codexStatus, codexHealth];
267
306
  }
package/package.json CHANGED
@@ -1,64 +1,63 @@
1
- {
2
- "name": "dsh-codex-port",
3
- "version": "0.1.1",
4
- "description": "DSH 技能移植工具插件:把 ~/.codex 里的 Codex 官方插件(170+ 个)一键移植为 DSH 技能(codex_list / codex_port / codex_status),frontmatter 自动转换、名称清洗、幂等跳过。",
5
- "type": "module",
6
- "main": "lib/index.js",
7
- "types": "lib/index.d.ts",
8
- "exports": {
9
- ".": {
10
- "types": "./lib/index.d.ts",
11
- "default": "./lib/index.js"
12
- },
13
- "./cordis.patch.yml": "./cordis.patch.yml",
14
- "./package.json": "./package.json"
15
- },
16
- "files": [
17
- "lib",
18
- "cordis.patch.yml",
19
- "README.md",
20
- "README.en.md"
21
- ],
22
- "scripts": {
23
- "build": "tsc -p tsconfig.json",
24
- "prepare": "tsc -p tsconfig.json",
25
- "typecheck": "tsc -p tsconfig.json --noEmit",
26
- "test": "pnpm run build && node --test \"test/*.test.mjs\"",
27
- "prepublishOnly": "pnpm run build"
28
- },
29
- "dsh": {
30
- "bundle": {
31
- "patch": "./cordis.patch.yml"
32
- }
33
- },
34
- "keywords": [
35
- "dsh",
36
- "deepseek-harness",
37
- "plugin",
38
- "codex",
39
- "skills",
40
- "migration",
41
- "dsh-plugin"
42
- ],
43
- "license": "MIT",
44
- "engines": {
45
- "node": ">=20"
46
- },
47
- "dependencies": {
48
- "yaml": "^2.9.0"
49
- },
50
- "devDependencies": {
51
- "@types/node": "^24.0.0",
52
- "typescript": "^5.6.0"
53
- },
54
- "repository": {
55
- "type": "git",
56
- "url": "git+https://github.com/STARDUSTLC666/dsh-codex-port.git"
57
- },
58
- "bugs": {
59
- "url": "https://github.com/STARDUSTLC666/dsh-codex-port/issues"
60
- },
61
- "homepage": "https://github.com/STARDUSTLC666/dsh-codex-port#readme",
62
- "author": "stardustlc",
63
- "packageManager": "pnpm@11.7.0"
64
- }
1
+ {
2
+ "name": "dsh-codex-port",
3
+ "version": "0.2.1",
4
+ "description": "DSH 技能移植工具插件:把 ~/.codex 里的 Codex 官方插件(170+ 个)一键移植为 DSH 技能(codex_list / codex_port / codex_status),frontmatter 自动转换、名称清洗、幂等跳过。",
5
+ "type": "module",
6
+ "main": "lib/index.js",
7
+ "types": "lib/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./lib/index.d.ts",
11
+ "default": "./lib/index.js"
12
+ },
13
+ "./cordis.patch.yml": "./cordis.patch.yml",
14
+ "./package.json": "./package.json"
15
+ },
16
+ "files": [
17
+ "lib",
18
+ "cordis.patch.yml",
19
+ "README.md",
20
+ "README.en.md"
21
+ ],
22
+ "scripts": {
23
+ "build": "tsc -p tsconfig.json",
24
+ "typecheck": "tsc -p tsconfig.json --noEmit",
25
+ "test": "pnpm run build && node --test \"test/*.test.mjs\"",
26
+ "prepublishOnly": "pnpm run build"
27
+ },
28
+ "dsh": {
29
+ "bundle": {
30
+ "patch": "./cordis.patch.yml"
31
+ }
32
+ },
33
+ "keywords": [
34
+ "dsh",
35
+ "deepseek-harness",
36
+ "plugin",
37
+ "codex",
38
+ "skills",
39
+ "migration",
40
+ "dsh-plugin"
41
+ ],
42
+ "license": "MIT",
43
+ "engines": {
44
+ "node": ">=22"
45
+ },
46
+ "dependencies": {
47
+ "yaml": "^2.9.0"
48
+ },
49
+ "devDependencies": {
50
+ "@types/node": "^24.0.0",
51
+ "typescript": "^5.6.0"
52
+ },
53
+ "repository": {
54
+ "type": "git",
55
+ "url": "git+https://github.com/STARDUSTLC666/dsh-codex-port.git"
56
+ },
57
+ "bugs": {
58
+ "url": "https://github.com/STARDUSTLC666/dsh-codex-port/issues"
59
+ },
60
+ "homepage": "https://github.com/STARDUSTLC666/dsh-codex-port#readme",
61
+ "author": "stardustlc",
62
+ "packageManager": "pnpm@11.7.0"
63
+ }