@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 +5 -5
- package/package.json +2 -2
- package/skills/specx-archive/SKILL.md +13 -13
- package/skills/specx-clarify/SKILL.md +6 -6
- package/skills/specx-create-rule/SKILL.md +10 -10
- package/skills/specx-demystify/SKILL.md +21 -21
- package/{specx.js → src/specx.js} +192 -9
package/README.md
CHANGED
|
@@ -88,20 +88,20 @@ specx 是一套企业级研发流程 skill 系列,核心理念是**先签合
|
|
|
88
88
|
docs/specx/
|
|
89
89
|
├── spaces/ # 需求空间(每个需求独立目录)
|
|
90
90
|
│ └── {yyyyMMdd}-{需求摘要}/
|
|
91
|
-
│ ├──
|
|
91
|
+
│ ├── raws/ # 原始需求来源(多源分类)
|
|
92
92
|
│ │ ├── P001-{需求名称}.md # 产品需求文档
|
|
93
93
|
│ │ ├── U001-{用户反馈}.md # 用户输入/语料
|
|
94
94
|
│ │ └── ...
|
|
95
|
-
│ ├──
|
|
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
|
-
│ └──
|
|
101
|
+
│ └── t-02-{子需求名}/
|
|
102
102
|
│ └── ...
|
|
103
103
|
├── wiki/ # 项目知识库(归档沉淀)
|
|
104
|
-
├──
|
|
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/
|
|
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.
|
|
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
|
-
**前置判断:** 子需求目录 `
|
|
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
|
-
- `
|
|
25
|
-
- `
|
|
26
|
-
- `
|
|
27
|
-
- `
|
|
28
|
-
- `
|
|
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
|
-
- 文件名:`{
|
|
58
|
-
- 示例:`
|
|
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
|
-
- [
|
|
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
|
-
在 `
|
|
202
|
+
在 `t-xx/` 下的 `README.md`(如果没有就创建)标注归档状态:
|
|
203
203
|
|
|
204
204
|
```markdown
|
|
205
|
-
#
|
|
205
|
+
# t-{xx}-{子需求名称}
|
|
206
206
|
|
|
207
207
|
**状态:** ✅ 已归档
|
|
208
208
|
**最后更新时间:** {YYYY-MM-DD}
|
|
209
209
|
**Wiki 沉淀位置:**
|
|
210
|
-
- [
|
|
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
|
-
[ ] 文件名符合命名规则 {
|
|
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
|
-
**子需求目录:**
|
|
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/
|
|
210
|
+
> ⚠️ 如果 `docs/specx/rules/` 下已有规范,只clarify 框架未定义的部分
|
|
211
211
|
|
|
212
212
|
**动作:**
|
|
213
|
-
1. 扫描 `docs/specx/
|
|
213
|
+
1. 扫描 `docs/specx/rules/` 目录下的规范文件
|
|
214
214
|
2. 在 clarify-prd 中标注哪些是框架定义的(直接引用,不重复问)
|
|
215
215
|
3. 只对「框架未定义」的部分提问
|
|
216
216
|
|
|
217
217
|
**扫描范围:**
|
|
218
218
|
```
|
|
219
|
-
docs/specx/
|
|
219
|
+
docs/specx/rules/ → 项目框架规范(由项目自己组织子目录结构)
|
|
220
220
|
```
|
|
221
221
|
|
|
222
222
|
**标注示例:**
|
|
@@ -252,7 +252,7 @@ history:
|
|
|
252
252
|
|
|
253
253
|
# {子需求名称} — 规格说明书
|
|
254
254
|
|
|
255
|
-
**所属子需求:**
|
|
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/
|
|
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/
|
|
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/
|
|
47
|
+
### 路径 1:项目初始化时(docs/specx/rules 不存在)
|
|
48
48
|
|
|
49
|
-
**前置判断:** `docs/specx/
|
|
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/
|
|
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/
|
|
68
|
+
3. 如用户确认,从纠正内容中提炼规则,写入 `docs/specx/rules/`
|
|
69
69
|
|
|
70
70
|
### 路径 3:用户明确提出规则
|
|
71
71
|
|
|
72
72
|
**前置判断:** 用户说"加上 X 规则"、"规定 Y 这样做"等
|
|
73
73
|
|
|
74
74
|
**动作:**
|
|
75
|
-
1. **先判断** — 读取 `docs/specx/
|
|
75
|
+
1. **先判断** — 读取 `docs/specx/rules/` 下现有规则,确认是否已有相关规则
|
|
76
76
|
2. 按判断结果执行(新增/冲突/过时/细化/替换)
|
|
77
|
-
3. 更新 `docs/specx/
|
|
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/
|
|
91
|
+
### docs/specx/rules/ 目录结构
|
|
92
92
|
|
|
93
93
|
```
|
|
94
|
-
docs/specx/
|
|
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/
|
|
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}-{摘要}/
|
|
25
|
+
| 原始需求来源 | 一个或多个文件,放入 `docs/specx/spaces/{yyyyMMdd}-{摘要}/raws/` 目录,只读不修改 |
|
|
26
26
|
| 项目上下文 | 技术栈(iOS / KMP / Android)、现有框架规范、架构约定 |
|
|
27
27
|
|
|
28
|
-
###
|
|
28
|
+
### raws/ 目录规范
|
|
29
29
|
|
|
30
|
-
所有原始来源文件(PRD、用户反馈、聊天记录、参考文献等)统一放入 `
|
|
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}-{摘要}/
|
|
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
|
-
- 示例:项目已有「商品数据模型」规范 →
|
|
93
|
+
- 示例:项目已有「商品数据模型」规范 → t-01 不再定义商品模型,改为「按项目现有商品模型扩展新字段」
|
|
94
94
|
|
|
95
95
|
**Step 1:识别新增抽象层**
|
|
96
96
|
- 梳理需求涉及的所有「实体」(Entity)—— 有唯一标识、有生命周期的东西
|
|
@@ -107,24 +107,24 @@ Raws/
|
|
|
107
107
|
|
|
108
108
|
```
|
|
109
109
|
抽象层:
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
110
|
+
t-01: 订单数据模型(Order Entity + 商品线 Item Value Object)
|
|
111
|
+
t-02: 订单状态机(待支付→已支付→已发货→已完成→已取消,含流转条件)
|
|
112
|
+
t-03: 订单数据加载/缓存/刷新机制(列表+详情的数据流)
|
|
113
113
|
|
|
114
114
|
实现层:
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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(独立可并行):
|
|
126
|
-
Phase II(依赖 Phase I):
|
|
127
|
-
Phase III(可选):
|
|
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
|
-
每个子需求目录 `/
|
|
152
|
+
每个子需求目录 `/t-{编号}-{名称}/` 下:
|
|
153
153
|
|
|
154
154
|
```
|
|
155
|
-
|
|
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
|
-
##
|
|
163
|
+
## t-{编号}: {子需求名称}
|
|
164
164
|
|
|
165
165
|
### 来源
|
|
166
|
-
> 摘录自 `
|
|
166
|
+
> 摘录自 `raws/{文件名}`
|
|
167
167
|
|
|
168
168
|
{从原始需求中摘录相关段落}
|
|
169
169
|
|
|
@@ -171,8 +171,8 @@ T-001-订单数据模型/
|
|
|
171
171
|
{明确本子需求包含什么、不包含什么}
|
|
172
172
|
|
|
173
173
|
### 依赖
|
|
174
|
-
- 依赖:
|
|
175
|
-
- 被依赖:
|
|
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
|
-
|
|
222
|
-
|
|
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
|
-
|
|
226
|
-
|
|
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 ───────────────────────────────────────────
|