@culeo/specx 0.1.0 → 0.1.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/README.md CHANGED
@@ -88,20 +88,20 @@ specx 是一套企业级研发流程 skill 系列,核心理念是**先签合
88
88
  docs/specx/
89
89
  ├── spaces/ # 需求空间(每个需求独立目录)
90
90
  │ └── {yyyyMMdd}-{需求摘要}/
91
- │ ├── Raws/ # 原始需求来源(多源分类)
91
+ │ ├── raws/ # 原始需求来源(多源分类)
92
92
  │ │ ├── P001-{需求名称}.md # 产品需求文档
93
93
  │ │ ├── U001-{用户反馈}.md # 用户输入/语料
94
94
  │ │ └── ...
95
- │ ├── T-01-{子需求名}/
95
+ │ ├── t-01-{子需求名}/
96
96
  │ │ ├── 00-子需求.md # 子需求原始描述(由 demystify 产出)
97
97
  │ │ ├── 01-clarify.md # 澄清记录
98
98
  │ │ ├── 02-spec.md # 规格说明书
99
99
  │ │ ├── 03-design.md # 设计方案
100
100
  │ │ └── 04-tasks.md # 任务清单
101
- │ └── T-02-{子需求名}/
101
+ │ └── t-02-{子需求名}/
102
102
  │ └── ...
103
103
  ├── wiki/ # 项目知识库(归档沉淀)
104
- ├── rule/ # 框架规范
104
+ ├── rules/ # 框架规范
105
105
  └── templates/ # 项目专属模板
106
106
  ```
107
107
 
@@ -111,7 +111,7 @@ docs/specx/
111
111
 
112
112
  - **文件名规范**:`00-子需求.md` / `01-clarify.md` / `02-spec.md` / `03-design.md` / `04-tasks.md`
113
113
  - **wiki 知识沉淀**:`docs/specx/wiki/`,由 `specx-archive` 维护
114
- - **框架规范**:`docs/specx/rule/`,clarify 时跳过已有规范,不重复提问
114
+ - **框架规范**:`docs/specx/rules/`,clarify 时跳过已有规范,不重复提问
115
115
  - **项目模板**:`docs/specx/templates/`,首次 design 时生成
116
116
 
117
117
  ---
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "@culeo/specx",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Install specx skills to AI coding agents (Claude Code, Codex, etc.)",
5
5
  "type": "module",
6
6
  "bin": {
7
- "specx": "./specx.js"
7
+ "specx": "./src/specx.js"
8
8
  },
9
9
  "dependencies": {
10
10
  "commander": "^12.0.0"
@@ -11,7 +11,7 @@ tags: [specx, archive, wiki, knowledge]
11
11
 
12
12
  当一个子需求完成(clarify → spec → design → code 全部走完)后,用户说"归档"、"沉淀到 wiki"、"完成这个需求"时使用。
13
13
 
14
- **前置判断:** 子需求目录 `T-xx/` 下已有 `clarify.md`、`spec.md`、`design.md`(及可选 `code` 相关文档)。
14
+ **前置判断:** 子需求目录 `t-xx/` 下已有 `clarify.md`、`spec.md`、`design.md`(及可选 `code` 相关文档)。
15
15
 
16
16
  **不是触发的情况:**
17
17
  - 需求还在中途(未完成 code)
@@ -21,11 +21,11 @@ tags: [specx, archive, wiki, knowledge]
21
21
 
22
22
  ## 输入
23
23
 
24
- - `T-xx/00-子需求.md` — 原始需求
25
- - `T-xx/01-clarify.md` — 澄清后的需求
26
- - `T-xx/02-spec.md` — 规格说明书
27
- - `T-xx/03-design.md` — 设计方案
28
- - `T-xx/04-tasks.md` — 任务清单(已完成的)
24
+ - `t-xx/00-子需求.md` — 原始需求
25
+ - `t-xx/01-clarify.md` — 澄清后的需求
26
+ - `t-xx/02-spec.md` — 规格说明书
27
+ - `t-xx/03-design.md` — 设计方案
28
+ - `t-xx/04-tasks.md` — 任务清单(已完成的)
29
29
 
30
30
  ---
31
31
 
@@ -54,8 +54,8 @@ docs/specx/wiki/
54
54
 
55
55
  ### 命名规则
56
56
 
57
- - 文件名:`{T-xx}-{简短英文名}.md`,小写,连字符分隔
58
- - 示例:`T01-stock-card.md`、`T03-redis-cache.md`
57
+ - 文件名:`{t-xx}-{简短英文名}.md`,小写,连字符分隔
58
+ - 示例:`t01-stock-card.md`、`t03-redis-cache.md`
59
59
 
60
60
  > 首次归档时由本 skill 创建 `docs/specx/wiki/` 目录和 `README.md`。
61
61
 
@@ -137,7 +137,7 @@ docs/specx/wiki/
137
137
  **分类:** {entities/features/decisions/components}
138
138
  **最后更新时间:** {YYYY-MM-DD}
139
139
  **关联需求:**
140
- - [T-xx](../{需求目录}/T-xx/00-子需求.md)
140
+ - [t-xx](../{需求目录}/t-xx/00-子需求.md)
141
141
 
142
142
  ---
143
143
 
@@ -199,15 +199,15 @@ history:
199
199
 
200
200
  ## Step 6️⃣ 更新子需求目录状态
201
201
 
202
- 在 `T-xx/` 下的 `README.md`(如果没有就创建)标注归档状态:
202
+ 在 `t-xx/` 下的 `README.md`(如果没有就创建)标注归档状态:
203
203
 
204
204
  ```markdown
205
- # T-{xx}-{子需求名称}
205
+ # t-{xx}-{子需求名称}
206
206
 
207
207
  **状态:** ✅ 已归档
208
208
  **最后更新时间:** {YYYY-MM-DD}
209
209
  **Wiki 沉淀位置:**
210
- - [T-xx-{slug}](../wiki/{分类}/T-xx-{slug}.md)
210
+ - [t-xx-{slug}](../wiki/{分类}/t-xx-{slug}.md)
211
211
  ```
212
212
 
213
213
  ---
@@ -218,7 +218,7 @@ history:
218
218
  [ ] 已读取全部子需求文档(00-子需求/clarify/spec/design/tasks)
219
219
  [ ] 判断了正确的 wiki 分类(entities/features/decisions/components)
220
220
  [ ] 每个提炼内容写成独立文件
221
- [ ] 文件名符合命名规则 {T-xx}-{slug}.md
221
+ [ ] 文件名符合命名规则 {t-xx}-{slug}.md
222
222
  [ ] 更新了 README.md 索引(顶部插入新行)
223
223
  [ ] 标注了子需求目录为已归档
224
224
  [ ] 未污染原始文档(clarify/spec/design.md 未被修改)
@@ -159,7 +159,7 @@ history:
159
159
  # {子需求名称}
160
160
 
161
161
  **所属原始需求:** {需求名称}
162
- **子需求目录:** T-{序号}-{名称}
162
+ **子需求目录:** t-{序号}-{名称}
163
163
 
164
164
  ---
165
165
 
@@ -207,16 +207,16 @@ history:
207
207
 
208
208
  ### Step 2.5️⃣ 框架对齐(跳过已定义的)
209
209
 
210
- > ⚠️ 如果 `docs/specx/rule/` 下已有规范,只clarify 框架未定义的部分
210
+ > ⚠️ 如果 `docs/specx/rules/` 下已有规范,只clarify 框架未定义的部分
211
211
 
212
212
  **动作:**
213
- 1. 扫描 `docs/specx/rule/` 目录下的规范文件
213
+ 1. 扫描 `docs/specx/rules/` 目录下的规范文件
214
214
  2. 在 clarify-prd 中标注哪些是框架定义的(直接引用,不重复问)
215
215
  3. 只对「框架未定义」的部分提问
216
216
 
217
217
  **扫描范围:**
218
218
  ```
219
- docs/specx/rule/ → 项目框架规范(由项目自己组织子目录结构)
219
+ docs/specx/rules/ → 项目框架规范(由项目自己组织子目录结构)
220
220
  ```
221
221
 
222
222
  **标注示例:**
@@ -252,7 +252,7 @@ history:
252
252
 
253
253
  # {子需求名称} — 规格说明书
254
254
 
255
- **所属子需求:** T-{序号}-{名称}
255
+ **所属子需求:** t-{序号}-{名称}
256
256
 
257
257
  ---
258
258
 
@@ -505,7 +505,7 @@ history:
505
505
  - **两个文件同步更新** — clarify-prd 和 spec 是一体的
506
506
  - **clarify 迭代次数不限** — 可能是 1 次,可能是 10 次,都正常
507
507
  - **文档状态** — 新建时 `draft`,完成时 `review`
508
- - **框架已定义的不重复问** — 先扫描 `docs/specx/rule/`,直接引用,只clarify 未定义的部分
508
+ - **框架已定义的不重复问** — 先扫描 `docs/specx/rules/`,直接引用,只clarify 未定义的部分
509
509
  - **避免污染上下文** — 框架规范在 rule/ 目录维护,不进clarify过程
510
510
 
511
511
  ---
@@ -36,7 +36,7 @@ tags: [specx, rule, convention, knowledge]
36
36
  | **替换** | 现有规则方向正确但表述不对 | 替换原文 |
37
37
 
38
38
  **判断步骤:**
39
- 1. 读取 `docs/specx/rule/` 下所有现有规则
39
+ 1. 读取 `docs/specx/rules/` 下所有现有规则
40
40
  2. 与新规则对比:是否同类?是否矛盾?是否重复?
41
41
  3. 确定类型后,再执行对应动作
42
42
 
@@ -44,9 +44,9 @@ tags: [specx, rule, convention, knowledge]
44
44
 
45
45
  ## 规则发现路径
46
46
 
47
- ### 路径 1:项目初始化时(docs/specx/rule 不存在)
47
+ ### 路径 1:项目初始化时(docs/specx/rules 不存在)
48
48
 
49
- **前置判断:** `docs/specx/rule/` 目录不存在
49
+ **前置判断:** `docs/specx/rules/` 目录不存在
50
50
 
51
51
  **扫描步骤:**
52
52
 
@@ -56,7 +56,7 @@ tags: [specx, rule, convention, knowledge]
56
56
  - 目录结构约定
57
57
  - 常用工具库
58
58
  - 分层架构(MVVM / Clean / DDD)
59
- 3. **推断规则** — 从扫描结果推断项目规范,写入 `docs/specx/rule/`
59
+ 3. **推断规则** — 从扫描结果推断项目规范,写入 `docs/specx/rules/`
60
60
 
61
61
  ### 路径 2:对话中途纠正(上下文沉淀)
62
62
 
@@ -65,16 +65,16 @@ tags: [specx, rule, convention, knowledge]
65
65
  **动作:**
66
66
  1. 理解用户纠正的本质(表面修改 vs 原则变更)
67
67
  2. 询问用户:"是否需要把这个规则固化到项目的规范文档里?"
68
- 3. 如用户确认,从纠正内容中提炼规则,写入 `docs/specx/rule/`
68
+ 3. 如用户确认,从纠正内容中提炼规则,写入 `docs/specx/rules/`
69
69
 
70
70
  ### 路径 3:用户明确提出规则
71
71
 
72
72
  **前置判断:** 用户说"加上 X 规则"、"规定 Y 这样做"等
73
73
 
74
74
  **动作:**
75
- 1. **先判断** — 读取 `docs/specx/rule/` 下现有规则,确认是否已有相关规则
75
+ 1. **先判断** — 读取 `docs/specx/rules/` 下现有规则,确认是否已有相关规则
76
76
  2. 按判断结果执行(新增/冲突/过时/细化/替换)
77
- 3. 更新 `docs/specx/rule/README.md`
77
+ 3. 更新 `docs/specx/rules/README.md`
78
78
 
79
79
  ### 路径 4:意图不明确
80
80
 
@@ -88,10 +88,10 @@ tags: [specx, rule, convention, knowledge]
88
88
 
89
89
  ## 输出结构
90
90
 
91
- ### docs/specx/rule/ 目录结构
91
+ ### docs/specx/rules/ 目录结构
92
92
 
93
93
  ```
94
- docs/specx/rule/
94
+ docs/specx/rules/
95
95
  ├── README.md # 规则总览 & 索引
96
96
  ├── naming.md # 命名规范
97
97
  ├── workflow.md # 开发流程规范
@@ -99,7 +99,7 @@ docs/specx/rule/
99
99
  └── architecture.md # 架构约定
100
100
  ```
101
101
 
102
- ### docs/specx/rule/README.md 模板
102
+ ### docs/specx/rules/README.md 模板
103
103
 
104
104
  ```markdown
105
105
  # 项目规范
@@ -22,12 +22,12 @@ history:
22
22
 
23
23
  | 输入 | 说明 |
24
24
  |------|------|
25
- | 原始需求来源 | 一个或多个文件,放入 `docs/specx/spaces/{yyyyMMdd}-{摘要}/Raws/` 目录,只读不修改 |
25
+ | 原始需求来源 | 一个或多个文件,放入 `docs/specx/spaces/{yyyyMMdd}-{摘要}/raws/` 目录,只读不修改 |
26
26
  | 项目上下文 | 技术栈(iOS / KMP / Android)、现有框架规范、架构约定 |
27
27
 
28
- ### Raws/ 目录规范
28
+ ### raws/ 目录规范
29
29
 
30
- 所有原始来源文件(PRD、用户反馈、聊天记录、参考文献等)统一放入 `Raws/` 子目录,按分类前缀命名:
30
+ 所有原始来源文件(PRD、用户反馈、聊天记录、参考文献等)统一放入 `raws/` 子目录,按分类前缀命名:
31
31
 
32
32
  | 分类 | 前缀 | 说明 |
33
33
  |------|------|------|
@@ -51,7 +51,7 @@ Raws/
51
51
 
52
52
  | 输出 | 格式 | 说明 |
53
53
  |------|------|------|
54
- | 子需求目录集合 | `docs/specx/spaces/{yyyyMMdd}-{摘要}/T-{编号}-{名称}/00-子需求.md` | 每个子需求独立目录,含独立分析 |
54
+ | 子需求目录集合 | `docs/specx/spaces/{yyyyMMdd}-{摘要}/t-{编号}-{名称}/00-子需求.md` | 每个子需求独立目录,含独立分析 |
55
55
  | 拆解决策记录 | 同一 docs 目录下的 `00-拆解策略.md`(可选) | 记录选择策略 A/B 的理由 |
56
56
 
57
57
  ## 4. 拆解策略(二选一)
@@ -90,7 +90,7 @@ Raws/
90
90
  - 检查项目是否已有数据模型规范、状态机模式、数据流约定(如已有项目的 Entity 定义、API 规范层)
91
91
  - 对已有抽象层:**不重复拆,直接引用**已有规范的路径/文档
92
92
  - 只拆「新增部分」——当前需求引入的新实体、新状态、新数据流
93
- - 示例:项目已有「商品数据模型」规范 → T-01 不再定义商品模型,改为「按项目现有商品模型扩展新字段」
93
+ - 示例:项目已有「商品数据模型」规范 → t-01 不再定义商品模型,改为「按项目现有商品模型扩展新字段」
94
94
 
95
95
  **Step 1:识别新增抽象层**
96
96
  - 梳理需求涉及的所有「实体」(Entity)—— 有唯一标识、有生命周期的东西
@@ -107,24 +107,24 @@ Raws/
107
107
 
108
108
  ```
109
109
  抽象层:
110
- T-01: 订单数据模型(Order Entity + 商品线 Item Value Object)
111
- T-02: 订单状态机(待支付→已支付→已发货→已完成→已取消,含流转条件)
112
- T-03: 订单数据加载/缓存/刷新机制(列表+详情的数据流)
110
+ t-01: 订单数据模型(Order Entity + 商品线 Item Value Object)
111
+ t-02: 订单状态机(待支付→已支付→已发货→已完成→已取消,含流转条件)
112
+ t-03: 订单数据加载/缓存/刷新机制(列表+详情的数据流)
113
113
 
114
114
  实现层:
115
- T-04: 订单列表页(依赖 T-01, T-03)
116
- T-05: 订单详情页(依赖 T-01, T-02, T-03)
117
- T-06: 退款处理流程(依赖 T-01 订单状态机扩展)
118
- T-07: 数据统计看板(依赖 T-01, T-03)
115
+ t-04: 订单列表页(依赖 t-01, t-03)
116
+ t-05: 订单详情页(依赖 t-01, t-02, t-03)
117
+ t-06: 退款处理流程(依赖 t-01 订单状态机扩展)
118
+ t-07: 数据统计看板(依赖 t-01, t-03)
119
119
  ```
120
120
 
121
121
  #### 依赖关系与执行顺序
122
122
 
123
123
  ```
124
124
  执行顺序:
125
- Phase I(独立可并行):T-01 → T-02 → T-03
126
- Phase II(依赖 Phase I):T-04, T-05, T-06(可并行)
127
- Phase III(可选):T-07
125
+ Phase I(独立可并行):t-01 → t-02 → t-03
126
+ Phase II(依赖 Phase I):t-04, t-05, t-06(可并行)
127
+ Phase III(可选):t-07
128
128
  ```
129
129
 
130
130
  #### 🎯 策略 B:按业务模块拆
@@ -149,10 +149,10 @@ Raws/
149
149
 
150
150
  ## 6. 输出规范
151
151
 
152
- 每个子需求目录 `/T-{编号}-{名称}/` 下:
152
+ 每个子需求目录 `/t-{编号}-{名称}/` 下:
153
153
 
154
154
  ```
155
- T-001-订单数据模型/
155
+ t-001-订单数据模型/
156
156
  ├── 00-子需求.md # 从原始需求摘录,可独立分析
157
157
  └── (后续 clarify/design/writing-plans 产物)
158
158
  ```
@@ -160,10 +160,10 @@ T-001-订单数据模型/
160
160
  `00-子需求.md` 模板:
161
161
 
162
162
  ```markdown
163
- ## T-{编号}: {子需求名称}
163
+ ## t-{编号}: {子需求名称}
164
164
 
165
165
  ### 来源
166
- > 摘录自 `Raws/{文件名}`
166
+ > 摘录自 `raws/{文件名}`
167
167
 
168
168
  {从原始需求中摘录相关段落}
169
169
 
@@ -171,8 +171,8 @@ T-001-订单数据模型/
171
171
  {明确本子需求包含什么、不包含什么}
172
172
 
173
173
  ### 依赖
174
- - 依赖:T-{编号1}, T-{编号2}
175
- - 被依赖:T-{编号3}, T-{编号4}
174
+ - 依赖:t-{编号1}, t-{编号2}
175
+ - 被依赖:t-{编号3}, t-{编号4}
176
176
 
177
177
  ### 类型
178
178
  - [ ] 抽象层(数据模型/状态机/数据流/领域规则)
@@ -6,12 +6,13 @@ import path from "path";
6
6
  import fs from "fs";
7
7
  import os from "os";
8
8
  import readline from "readline";
9
+ import { execSync } from "child_process";
9
10
 
10
11
  const __filename = fileURLToPath(import.meta.url);
11
12
  const __dirname = path.dirname(__filename);
12
13
 
13
14
  // ─── Paths ───────────────────────────────────────────────────────
14
- const SPECX_HOME = process.env.SPECX_HOME || path.resolve(__dirname);
15
+ const SPECX_HOME = process.env.SPECX_HOME || path.resolve(__dirname, "..");
15
16
  const SKILLS_DIR = path.join(SPECX_HOME, "skills");
16
17
 
17
18
  // ─── Registered CLI Targets ──────────────────────────────────────
@@ -28,6 +29,72 @@ const CLI_TARGETS = [
28
29
  },
29
30
  ];
30
31
 
32
+ // ─── Helpers ──────────────────────────────────────────────────────
33
+ function getPackageName() {
34
+ try {
35
+ const pkg = JSON.parse(fs.readFileSync(path.join(SPECX_HOME, "package.json"), "utf-8"));
36
+ return pkg.name;
37
+ } catch {
38
+ return "@culeo/specx";
39
+ }
40
+ }
41
+
42
+ // ─── Detect installed specx skills in a target dir ───────────────
43
+ function findSpecxSkillsInDir(dir) {
44
+ if (!fs.existsSync(dir)) return [];
45
+ return fs.readdirSync(dir)
46
+ .filter((name) => {
47
+ const skillDir = path.join(dir, name);
48
+ return fs.statSync(skillDir).isDirectory() && fs.existsSync(path.join(skillDir, "SKILL.md")) && name.startsWith("specx-");
49
+ })
50
+ .sort();
51
+ }
52
+
53
+ // ─── Detect project-level .{claude,codex}/skills/ ────────────────
54
+ function findProjectSpecxDirs(cwd) {
55
+ const results = [];
56
+ for (const target of CLI_TARGETS) {
57
+ const projectDir = path.join(cwd, `.${target.id}`, "skills");
58
+ const skills = findSpecxSkillsInDir(projectDir);
59
+ if (skills.length > 0) {
60
+ results.push({ targetId: target.id, label: target.label, dir: projectDir, skills });
61
+ } else if (fs.existsSync(projectDir)) {
62
+ // Dir exists but no specx skills — still a candidate for coverage
63
+ results.push({ targetId: target.id, label: target.label, dir: projectDir, skills: [] });
64
+ }
65
+ }
66
+ return results;
67
+ }
68
+
69
+ // ─── Copy skills from source to a destination ────────────────────
70
+ function copySkillsToDir(srcDir, destDir) {
71
+ const skills = getAvailableSkills();
72
+ let installCount = 0;
73
+ let updateCount = 0;
74
+ for (const skillName of skills) {
75
+ const skillSrc = path.join(srcDir, skillName);
76
+ const skillDest = path.join(destDir, skillName);
77
+ if (!fs.existsSync(skillSrc)) {
78
+ console.log(` ✖ ${skillName}: source not found, skipping`);
79
+ continue;
80
+ }
81
+ const existed = fs.existsSync(skillDest);
82
+ if (existed) {
83
+ fs.rmSync(skillDest, { recursive: true, force: true });
84
+ }
85
+ fs.mkdirSync(destDir, { recursive: true });
86
+ copyDirSync(skillSrc, skillDest);
87
+ if (existed) {
88
+ console.log(` ↻ ${skillName} updated`);
89
+ updateCount++;
90
+ } else {
91
+ console.log(` ✓ ${skillName} installed`);
92
+ installCount++;
93
+ }
94
+ }
95
+ return { installCount, updateCount };
96
+ }
97
+
31
98
  // ─── Interactive Multi-Select ────────────────────────────────────
32
99
  function isTTY() {
33
100
  return process.stdin.isTTY;
@@ -205,27 +272,143 @@ program
205
272
  const finalDir = getFinalDir(targetId, isProject, projectDir);
206
273
 
207
274
  let installCount = 0;
275
+ let updateCount = 0;
208
276
  for (const skillName of skillsToInstall) {
209
277
  const destDir = path.join(finalDir, skillName);
210
- if (fs.existsSync(destDir)) {
211
- console.log(` ○ ${target.label}: ${skillName} already installed`);
212
- continue;
213
- }
214
278
  const srcDir = path.join(SKILLS_DIR, skillName);
215
279
  if (!fs.existsSync(srcDir)) {
216
280
  console.log(` ✖ ${skillName}: source not found, skipping`);
217
281
  continue;
218
282
  }
283
+ const existed = fs.existsSync(destDir);
284
+ if (existed) {
285
+ fs.rmSync(destDir, { recursive: true, force: true });
286
+ }
219
287
  fs.mkdirSync(finalDir, { recursive: true });
220
288
  copyDirSync(srcDir, destDir);
221
- console.log(` ✓ ${target.label}: ${skillName}`);
222
- installCount++;
289
+ if (existed) {
290
+ console.log(` ↻ ${target.label}: ${skillName} updated`);
291
+ updateCount++;
292
+ } else {
293
+ console.log(` ✓ ${target.label}: ${skillName} installed`);
294
+ installCount++;
295
+ }
296
+ }
297
+
298
+ if (installCount > 0 || updateCount > 0) {
299
+ const parts = [];
300
+ if (installCount > 0) parts.push(`${installCount} installed`);
301
+ if (updateCount > 0) parts.push(`${updateCount} updated`);
302
+ console.log(`\n✓ ${parts.join(", ")} for ${target.label} (${isProject ? "project" : "user"} level)`);
223
303
  }
304
+ }
305
+ });
306
+
307
+ // ─── Update Command ──────────────────────────────────────────────
308
+ program
309
+ .command("update")
310
+ .description("Update specx CLI (npm) and re-install skills to all installed locations")
311
+ .action(async () => {
312
+ console.log("🔄 Updating specx CLI...");
313
+
314
+ // Step 1: Update the CLI itself via npm
315
+ const pkgName = getPackageName();
316
+ console.log(` Running: npm update -g ${pkgName}...`);
317
+ try {
318
+ execSync(`npm update -g ${pkgName}`, { stdio: "inherit" });
319
+ console.log(" ✓ npm update complete");
320
+ } catch (e) {
321
+ console.error(" ✖ npm update failed. Try with sudo or check your npm setup.");
322
+ process.exit(1);
323
+ }
324
+
325
+ console.log("\n🔁 Re-installing specx skills...");
326
+
327
+ // Step 2: Re-install to all user-level targets that have specx skills installed
328
+ const userTargets = [];
329
+ for (const target of CLI_TARGETS) {
330
+ const targetDir = target.dir();
331
+ const installedSkills = findSpecxSkillsInDir(targetDir);
332
+ if (installedSkills.length > 0) {
333
+ userTargets.push({ target, skills: installedSkills });
334
+ } else {
335
+ console.log(` ○ ${target.label}: no specx skills installed at user level, skipping`);
336
+ }
337
+ }
224
338
 
225
- if (installCount > 0) {
226
- console.log(`\n Installed ${installCount} skill(s) to ${target.label} (${isProject ? "project" : "user"} level)`);
339
+ if (userTargets.length > 0) {
340
+ console.log("\n📦 User-level targets found:");
341
+ for (const ut of userTargets) {
342
+ console.log(` ${ut.target.label} (${ut.skills.length} skills)`);
343
+ }
344
+ console.log("");
345
+
346
+ for (const ut of userTargets) {
347
+ const destDir = ut.target.dir();
348
+ console.log(`→ ${ut.target.label}...`);
349
+ const result = copySkillsToDir(SKILLS_DIR, destDir);
350
+ const parts = [];
351
+ if (result.installCount > 0) parts.push(`${result.installCount} installed`);
352
+ if (result.updateCount > 0) parts.push(`${result.updateCount} updated`);
353
+ console.log(` ✓ ${parts.join(", ")} for ${ut.target.label} (user level)`);
227
354
  }
355
+ } else {
356
+ console.log("\n No user-level specx skills found to update.");
228
357
  }
358
+
359
+ // Step 3: Check current working directory for project-level installations
360
+ const cwd = process.cwd();
361
+ const projectTargets = findProjectSpecxDirs(cwd);
362
+
363
+ if (projectTargets.length > 0) {
364
+ console.log(`\n📁 Project-level specx skills detected in ${cwd}:`);
365
+ for (const pt of projectTargets) {
366
+ console.log(` ${pt.label} (${pt.skills.length} skills) — ${pt.dir}`);
367
+ }
368
+ console.log("");
369
+
370
+ if (isTTY()) {
371
+ // Interactive: let user choose which project locations to update
372
+ const items = projectTargets.map((pt) => ({
373
+ id: pt.targetId,
374
+ label: `${pt.label} (${pt.skills.length || 0} skills installed)`,
375
+ detected: true,
376
+ }));
377
+ const selected = await interactiveSelect(
378
+ items,
379
+ "📁 Select project-level locations to update:",
380
+ projectTargets.map((pt) => pt.targetId),
381
+ false
382
+ );
383
+
384
+ for (const pt of projectTargets) {
385
+ if (!selected.includes(pt.targetId)) {
386
+ console.log(` ○ ${pt.label}: skipped`);
387
+ continue;
388
+ }
389
+ console.log(`→ ${pt.label}...`);
390
+ const result = copySkillsToDir(SKILLS_DIR, pt.dir);
391
+ const parts = [];
392
+ if (result.installCount > 0) parts.push(`${result.installCount} installed`);
393
+ if (result.updateCount > 0) parts.push(`${result.updateCount} updated`);
394
+ console.log(` ✓ ${parts.join(", ")} for ${pt.label} (project level)`);
395
+ }
396
+ } else {
397
+ // Non-interactive: update all
398
+ for (const pt of projectTargets) {
399
+ console.log(`→ ${pt.label}...`);
400
+ const result = copySkillsToDir(SKILLS_DIR, pt.dir);
401
+ const parts = [];
402
+ if (result.installCount > 0) parts.push(`${result.installCount} installed`);
403
+ if (result.updateCount > 0) parts.push(`${result.updateCount} updated`);
404
+ console.log(` ✓ ${parts.join(", ")} for ${pt.label} (project level)`);
405
+ }
406
+ }
407
+ } else {
408
+ console.log("\n No project-level specx skills detected in current directory.");
409
+ }
410
+
411
+ console.log("\n✓ Update complete!");
229
412
  });
230
413
 
231
414
  // ─── Uninstall Command ───────────────────────────────────────────