@ranger1/dx 0.1.102 → 0.1.104

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
@@ -221,8 +221,9 @@ dx test e2e quantify apps/quantify/e2e/health/health.e2e-spec.ts
221
221
 
222
222
  关于 `dx initial`:
223
223
 
224
- - `dx initial` 会把 npm 包内置的 `skills/` 同步到 `~/.codex/skills` 与 `~/.claude/skills`。
225
- - 包内管理的同名 skill 目录会按当前包内容替换;旧版本中已删除的官方 skill 会被清理。
224
+ - `dx initial` 会把 npm 包内置的 `skills/` 覆盖同步到 `~/.agents/skills`。
225
+ - `~/.claude/skills` 中包内管理的同名非软链接 skill 会先删除,再创建指向 `~/.agents/skills` 的软链接。
226
+ - `~/.codex/skills` 中包内管理的同名非软链接 skill 会被清理;已有软链接不会按旧副本删除。
226
227
  - 不属于包内管理的其他用户自有 skill 目录会保留。
227
228
 
228
229
  关于 `help`:
@@ -4,9 +4,13 @@ import os from 'node:os'
4
4
 
5
5
  import { logger } from './logger.js'
6
6
 
7
- const DEPRECATED_SKILL_DIRS = ['pr-review-loop', 'git-commit-and-pr', 'autospec']
8
7
  const TEMP_DIR_PATTERN = /^\..+\.(tmp|backup)-\d+-\d+$/
9
8
 
9
+ async function collectSkillNames(dir) {
10
+ const entries = await fs.readdir(dir, { withFileTypes: true })
11
+ return entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name)
12
+ }
13
+
10
14
  async function collectAllFiles(dir) {
11
15
  const out = []
12
16
 
@@ -65,7 +69,6 @@ async function copyDirMerge({ srcDir, dstDir }) {
65
69
 
66
70
  for (const entry of entries) {
67
71
  if (!entry.isDirectory()) continue
68
- if (DEPRECATED_SKILL_DIRS.includes(entry.name)) continue
69
72
 
70
73
  const srcSkillDir = join(srcDir, entry.name)
71
74
  const dstSkillDir = join(dstDir, entry.name)
@@ -109,9 +112,18 @@ async function copyDirMerge({ srcDir, dstDir }) {
109
112
  return { fileCount }
110
113
  }
111
114
 
112
- async function removeDeprecatedSkillDirs(skillsDir) {
113
- for (const dirName of DEPRECATED_SKILL_DIRS) {
114
- await fs.rm(join(skillsDir, dirName), { recursive: true, force: true })
115
+ async function removeManagedNonSymlinkSkills(skillsDir, skillNames) {
116
+ for (const skillName of skillNames) {
117
+ const target = join(skillsDir, skillName)
118
+ let stat
119
+ try {
120
+ stat = await fs.lstat(target)
121
+ } catch (error) {
122
+ if (error?.code === 'ENOENT') continue
123
+ throw error
124
+ }
125
+ if (stat.isSymbolicLink()) continue
126
+ await fs.rm(target, { recursive: true, force: true })
115
127
  }
116
128
  }
117
129
 
@@ -124,32 +136,57 @@ async function removeStaleTempDirs(skillsDir) {
124
136
  }
125
137
  }
126
138
 
139
+ async function linkSkillsToClaude({ skillNames, agentsSkillsDir, claudeSkillsDir }) {
140
+ for (const skillName of skillNames) {
141
+ const srcSkillDir = join(agentsSkillsDir, skillName)
142
+ const claudeSkillPath = join(claudeSkillsDir, skillName)
143
+
144
+ let stat
145
+ try {
146
+ stat = await fs.lstat(claudeSkillPath)
147
+ } catch (error) {
148
+ if (error?.code !== 'ENOENT') throw error
149
+ }
150
+
151
+ if (stat?.isSymbolicLink()) {
152
+ const currentTarget = await fs.readlink(claudeSkillPath)
153
+ if (currentTarget === srcSkillDir) continue
154
+ await fs.rm(claudeSkillPath, { recursive: true, force: true })
155
+ } else if (stat) {
156
+ await fs.rm(claudeSkillPath, { recursive: true, force: true })
157
+ }
158
+
159
+ await fs.symlink(srcSkillDir, claudeSkillPath, 'dir')
160
+ }
161
+ }
162
+
127
163
  export async function runCodexInitial(options = {}) {
128
164
  const packageRoot = options.packageRoot
129
165
  if (!packageRoot) throw new Error('runCodexInitial: 缺少 packageRoot')
130
166
 
131
167
  const homeDir = options.homeDir || os.homedir()
132
168
  const srcSkillsDir = join(packageRoot, 'skills')
133
- const targets = [
134
- { name: 'codex', dir: join(homeDir, '.codex', 'skills') },
135
- { name: 'claude', dir: join(homeDir, '.claude', 'skills') },
136
- ]
169
+ const agentsSkillsDir = join(homeDir, '.agents', 'skills')
170
+ const claudeSkillsDir = join(homeDir, '.claude', 'skills')
171
+ const codexSkillsDir = join(homeDir, '.codex', 'skills')
137
172
 
138
173
  await assertDirExists(srcSkillsDir, '模板目录 skills')
174
+ const skillNames = await collectSkillNames(srcSkillsDir)
139
175
 
140
- const stats = []
141
- for (const target of targets) {
142
- await ensureDir(target.dir)
143
- await removeStaleTempDirs(target.dir)
144
- await removeDeprecatedSkillDirs(target.dir)
145
- const copyStats = await copyDirMerge({ srcDir: srcSkillsDir, dstDir: target.dir })
146
- await removeStaleTempDirs(target.dir)
147
- await removeDeprecatedSkillDirs(target.dir)
148
- stats.push({ ...target, ...copyStats })
149
- }
176
+ await ensureDir(codexSkillsDir)
177
+ await ensureDir(claudeSkillsDir)
178
+ await ensureDir(agentsSkillsDir)
179
+
180
+ await removeManagedNonSymlinkSkills(codexSkillsDir, skillNames)
181
+ await removeManagedNonSymlinkSkills(claudeSkillsDir, skillNames)
182
+
183
+ await removeStaleTempDirs(agentsSkillsDir)
184
+ const copyStats = await copyDirMerge({ srcDir: srcSkillsDir, dstDir: agentsSkillsDir })
185
+ await removeStaleTempDirs(agentsSkillsDir)
186
+ await linkSkillsToClaude({ skillNames, agentsSkillsDir, claudeSkillsDir })
150
187
 
151
188
  logger.success('已初始化 skills 模板')
152
- for (const target of stats) {
153
- logger.info(`${target.name} skills: 覆盖复制 ${target.fileCount} 个文件 -> ${target.dir}`)
154
- }
189
+ logger.info(`agents skills: 覆盖复制 ${copyStats.fileCount} 个文件 -> ${agentsSkillsDir}`)
190
+ logger.info(`claude skills: 已创建 ${skillNames.length} 个软链接 -> ${claudeSkillsDir}`)
191
+ logger.info(`codex skills: 已清理 ${skillNames.length} 个包内托管 skill 的旧副本 -> ${codexSkillsDir}`)
155
192
  }
package/lib/exec.js CHANGED
@@ -9,6 +9,7 @@ import {
9
9
  resolveTargetRequiredVars,
10
10
  } from './env-policy.js'
11
11
  import { confirmManager } from './confirm.js'
12
+ import { ensureNxIgnoreToolDirs, isNxCommand } from './nx-ignore.js'
12
13
 
13
14
  const execPromise = promisify(nodeExec)
14
15
 
@@ -171,6 +172,17 @@ export class ExecManager {
171
172
  }
172
173
  }
173
174
 
175
+ if (isNxCommand(fullCommand)) {
176
+ try {
177
+ const result = ensureNxIgnoreToolDirs(cwd || process.cwd())
178
+ if (result.changed) {
179
+ logger.info(`已更新 .nxignore,排除工具元数据目录: ${result.added.join(', ')}`)
180
+ }
181
+ } catch (error) {
182
+ logger.warn(`自动更新 .nxignore 失败: ${error?.message || String(error)}`)
183
+ }
184
+ }
185
+
174
186
  logger.command(fullCommand)
175
187
 
176
188
  // 执行命令(可能重试)
@@ -0,0 +1,45 @@
1
+ import { existsSync, readFileSync, writeFileSync } from 'node:fs'
2
+ import { join } from 'node:path'
3
+
4
+ export const NX_IGNORE_TOOL_DIR_PATTERNS = [
5
+ '.cache/',
6
+ '.claude/',
7
+ '.codex/',
8
+ '.idea/',
9
+ '.omc/',
10
+ '.omx/',
11
+ '.opencode/',
12
+ '.pytest_cache/',
13
+ ]
14
+
15
+ const MANAGED_BLOCK_START = '# dx managed tool metadata ignores'
16
+
17
+ export function isNxCommand(command) {
18
+ return /\bnx(?:\.js)?\b/.test(String(command || ''))
19
+ }
20
+
21
+ export function ensureNxIgnoreToolDirs(projectRoot = process.cwd()) {
22
+ const root = String(projectRoot || process.cwd())
23
+ const nxIgnorePath = join(root, '.nxignore')
24
+ const existing = existsSync(nxIgnorePath) ? readFileSync(nxIgnorePath, 'utf8') : ''
25
+ const existingLines = new Set(
26
+ existing
27
+ .split(/\r?\n/)
28
+ .map(line => line.trim())
29
+ .filter(Boolean),
30
+ )
31
+ const missing = NX_IGNORE_TOOL_DIR_PATTERNS.filter(pattern => !existingLines.has(pattern))
32
+
33
+ if (missing.length === 0) {
34
+ return { changed: false, path: nxIgnorePath, added: [] }
35
+ }
36
+
37
+ const prefix = existing.trimEnd()
38
+ const blockLines = existingLines.has(MANAGED_BLOCK_START)
39
+ ? missing
40
+ : [MANAGED_BLOCK_START, ...missing]
41
+ const next = `${prefix ? `${prefix}\n\n` : ''}${blockLines.join('\n')}\n`
42
+
43
+ writeFileSync(nxIgnorePath, next)
44
+ return { changed: true, path: nxIgnorePath, added: missing }
45
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ranger1/dx",
3
- "version": "0.1.102",
3
+ "version": "0.1.104",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -7,7 +7,7 @@ description: 仅在用户显式调用 $doctor 或明确要求使用 doctor 技
7
7
 
8
8
  ## 概览
9
9
 
10
- 本技能用于把当前机器调整到可稳定运行 Codex 开发工作流的状态。
10
+ 本技能用于把当前机器调整到可稳定运行 agent 开发工作流的状态。
11
11
 
12
12
  不要把执行路径写死。模型应先识别当前系统、shell、包管理器、已有工具版本与用户权限,再自行选择最合适的检测和修复方式。完成修复后必须复检,并给出可读报告。
13
13
 
@@ -20,7 +20,8 @@ description: 仅在用户显式调用 $doctor 或明确要求使用 doctor 技
20
20
  - `node`、`npm`、`pnpm` 可用,且满足当前工作流需要。
21
21
  - `dx` 可用,并已完成必要初始化。
22
22
  - `agent-browser` 可用,且 Chromium/浏览器依赖已安装到可运行状态。
23
- - `rg`(ripgrep)可用。
23
+ - `rg`(rip grep)可用。
24
+ - `rtk` 可用。
24
25
  - 常用 PATH 配置在当前 shell 中可生效;若需要持久化,说明写入了哪个 shell 配置文件。
25
26
 
26
27
  ## 执行原则
@@ -38,7 +39,6 @@ description: 仅在用户显式调用 $doctor 或明确要求使用 doctor 技
38
39
  - 操作系统与架构
39
40
  - 当前 shell 与 PATH
40
41
  - `python3`、`python`、`node`、`npm`、`pnpm`、`dx`、`agent-browser`、`rg` 的存在性与版本
41
- - `CODEX_HOME` 与相关配置目录是否存在
42
42
  2. 对照目标状态判断缺口。
43
43
  3. 制定最小修复动作并执行。
44
44
  4. 每次修复后重新验证相关项。