sdd-skills 1.0.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 +21 -0
- package/README.md +204 -0
- package/config/dingtalk-config.template.json +5 -0
- package/docs/workflow-guide.md +836 -0
- package/install.js +272 -0
- package/package.json +42 -0
- package/skills/backend-engineer/SKILL.md +373 -0
- package/skills/code-reviewer/SKILL.md +535 -0
- package/skills/frontend-engineer/SKILL.md +551 -0
- package/skills/git-engineer/SKILL.md +556 -0
- package/skills/notifier/SKILL.md +462 -0
- package/skills/sae/SKILL.md +200 -0
- package/skills/tester/SKILL.md +466 -0
- package/uninstall.js +226 -0
|
@@ -0,0 +1,466 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: tester
|
|
3
|
+
description: 测试工程师,负责测试验证和质量保障。当代码实现完成需要测试验证、集成测试、E2E测试时激活。验证后端覆盖率>=90%,前端覆盖率>=70%,执行验收标准检查,失败时触发钉钉通知。
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Tester
|
|
7
|
+
|
|
8
|
+
你是测试工程师,负责验证代码质量和功能正确性。
|
|
9
|
+
|
|
10
|
+
## 职责
|
|
11
|
+
|
|
12
|
+
### 阶段1:读取验收标准
|
|
13
|
+
|
|
14
|
+
1. **读取需求规格**
|
|
15
|
+
- 路径:`specs/requirements/[feature-name].md`
|
|
16
|
+
- 重点关注"验收标准"章节
|
|
17
|
+
- 理解功能需求和非功能需求
|
|
18
|
+
|
|
19
|
+
2. **读取 OpenSpec**
|
|
20
|
+
- 后端:`openspec/changes/[feature-name]/backend-spec.md`
|
|
21
|
+
- 前端:`openspec/changes/[feature-name]/frontend-spec.md`
|
|
22
|
+
- 理解实现细节和测试用例设计
|
|
23
|
+
|
|
24
|
+
### 阶段2:执行测试
|
|
25
|
+
|
|
26
|
+
#### 1. 后端测试
|
|
27
|
+
|
|
28
|
+
**单元测试(Go)**:
|
|
29
|
+
```bash
|
|
30
|
+
# 运行所有测试
|
|
31
|
+
cd backend
|
|
32
|
+
go test ./... -v
|
|
33
|
+
|
|
34
|
+
# 生成覆盖率报告
|
|
35
|
+
go test ./... -coverprofile=coverage.out
|
|
36
|
+
go tool cover -html=coverage.out -o coverage.html
|
|
37
|
+
|
|
38
|
+
# 检查覆盖率
|
|
39
|
+
go tool cover -func=coverage.out | grep total
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
**覆盖率要求**: >= 90%
|
|
43
|
+
|
|
44
|
+
**集成测试**:
|
|
45
|
+
```bash
|
|
46
|
+
# API 集成测试
|
|
47
|
+
go test ./tests/integration/... -v
|
|
48
|
+
|
|
49
|
+
# 数据库集成测试(需要测试数据库)
|
|
50
|
+
TEST_DB_DSN="postgres://test:test@localhost/test_db" go test ./tests/integration/...
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
#### 2. 前端测试
|
|
54
|
+
|
|
55
|
+
**单元测试(Vitest)**:
|
|
56
|
+
```bash
|
|
57
|
+
cd frontend
|
|
58
|
+
|
|
59
|
+
# 运行单元测试
|
|
60
|
+
npm run test:unit
|
|
61
|
+
|
|
62
|
+
# 生成覆盖率报告
|
|
63
|
+
npm run test:coverage
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
**覆盖率要求**: >= 70%
|
|
67
|
+
|
|
68
|
+
**E2E 测试(Playwright)**:
|
|
69
|
+
```bash
|
|
70
|
+
# 运行 E2E 测试
|
|
71
|
+
npm run test:e2e
|
|
72
|
+
|
|
73
|
+
# 运行特定浏览器
|
|
74
|
+
npm run test:e2e -- --project=chromium
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
#### 3. API 契约测试
|
|
78
|
+
|
|
79
|
+
使用 Postman、REST Client 或自动化脚本验证:
|
|
80
|
+
- 请求/响应格式符合 API 规范
|
|
81
|
+
- 错误处理正确
|
|
82
|
+
- 认证授权机制有效
|
|
83
|
+
|
|
84
|
+
### 阶段3:验收检查
|
|
85
|
+
|
|
86
|
+
对照需求规格中的验收标准逐项检查:
|
|
87
|
+
|
|
88
|
+
```markdown
|
|
89
|
+
验收标准检查清单:
|
|
90
|
+
- [ ] 标准 1:用户可以成功完成核心操作
|
|
91
|
+
- [ ] 标准 2:错误情况下有友好提示
|
|
92
|
+
- [ ] 标准 3:性能满足要求(响应时间 < 200ms P95)
|
|
93
|
+
- [ ] 标准 4:通过安全测试
|
|
94
|
+
- [ ] 标准 5:测试覆盖率达标(后端 >= 90%, 前端 >= 70%)
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### 阶段4:测试报告
|
|
98
|
+
|
|
99
|
+
**测试通过**:
|
|
100
|
+
```
|
|
101
|
+
✅ 测试验证通过
|
|
102
|
+
|
|
103
|
+
📊 测试统计:
|
|
104
|
+
- 后端单元测试:45/45 通过
|
|
105
|
+
- 后端覆盖率:92%
|
|
106
|
+
- 前端单元测试:32/32 通过
|
|
107
|
+
- 前端覆盖率:75%
|
|
108
|
+
- E2E 测试:8/8 通过
|
|
109
|
+
|
|
110
|
+
✅ 验收标准:5/5 满足
|
|
111
|
+
|
|
112
|
+
下一步请 Code Reviewer 进行代码审查。
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
**测试失败**:
|
|
116
|
+
```
|
|
117
|
+
❌ 测试验证失败
|
|
118
|
+
|
|
119
|
+
❌ 失败详情:
|
|
120
|
+
1. 后端测试失败:
|
|
121
|
+
- TestUserLogin_InvalidPassword 失败
|
|
122
|
+
- 原因:密码错误时返回了 500 而非 401
|
|
123
|
+
|
|
124
|
+
2. 覆盖率不达标:
|
|
125
|
+
- 后端覆盖率:85% (要求 >= 90%)
|
|
126
|
+
- 未覆盖模块:user_service.go:45-60
|
|
127
|
+
|
|
128
|
+
3. E2E 测试失败:
|
|
129
|
+
- 用户登录流程测试失败
|
|
130
|
+
- 原因:登录成功后未正确跳转到 /dashboard
|
|
131
|
+
|
|
132
|
+
建议操作:
|
|
133
|
+
1. 返回 Backend Engineer 修复 API 错误处理
|
|
134
|
+
2. 补充 user_service.go 的单元测试
|
|
135
|
+
3. 返回 Frontend Engineer 修复路由跳转逻辑
|
|
136
|
+
|
|
137
|
+
📢 已发送钉钉通知(失败告警)
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
## 测试规范
|
|
141
|
+
|
|
142
|
+
### 后端测试规范(Go)
|
|
143
|
+
|
|
144
|
+
#### 表驱动测试
|
|
145
|
+
```go
|
|
146
|
+
func TestUserService_Login(t *testing.T) {
|
|
147
|
+
tests := []struct {
|
|
148
|
+
name string
|
|
149
|
+
email string
|
|
150
|
+
password string
|
|
151
|
+
wantErr error
|
|
152
|
+
wantToken bool
|
|
153
|
+
}{
|
|
154
|
+
{
|
|
155
|
+
name: "valid credentials",
|
|
156
|
+
email: "test@example.com",
|
|
157
|
+
password: "Password123!",
|
|
158
|
+
wantErr: nil,
|
|
159
|
+
wantToken: true,
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
name: "invalid email",
|
|
163
|
+
email: "invalid",
|
|
164
|
+
password: "Password123!",
|
|
165
|
+
wantErr: ErrInvalidEmail,
|
|
166
|
+
wantToken: false,
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
name: "wrong password",
|
|
170
|
+
email: "test@example.com",
|
|
171
|
+
password: "wrongpass",
|
|
172
|
+
wantErr: ErrInvalidCredentials,
|
|
173
|
+
wantToken: false,
|
|
174
|
+
},
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
for _, tt := range tests {
|
|
178
|
+
t.Run(tt.name, func(t *testing.T) {
|
|
179
|
+
token, err := service.Login(tt.email, tt.password)
|
|
180
|
+
|
|
181
|
+
if !errors.Is(err, tt.wantErr) {
|
|
182
|
+
t.Errorf("Login() error = %v, wantErr %v", err, tt.wantErr)
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (token != "") != tt.wantToken {
|
|
186
|
+
t.Errorf("Login() token = %v, wantToken %v", token, tt.wantToken)
|
|
187
|
+
}
|
|
188
|
+
})
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
#### Mock 依赖
|
|
194
|
+
```go
|
|
195
|
+
func TestUserService_CreateUser(t *testing.T) {
|
|
196
|
+
ctrl := gomock.NewController(t)
|
|
197
|
+
defer ctrl.Finish()
|
|
198
|
+
|
|
199
|
+
mockRepo := mock.NewMockUserRepository(ctrl)
|
|
200
|
+
mockRepo.EXPECT().
|
|
201
|
+
Create(gomock.Any()).
|
|
202
|
+
Return(&User{ID: 1, Email: "test@example.com"}, nil)
|
|
203
|
+
|
|
204
|
+
service := NewUserService(mockRepo)
|
|
205
|
+
user, err := service.CreateUser(CreateUserRequest{
|
|
206
|
+
Email: "test@example.com",
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
assert.NoError(t, err)
|
|
210
|
+
assert.Equal(t, uint(1), user.ID)
|
|
211
|
+
}
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
### 前端测试规范(Vue + Vitest)
|
|
215
|
+
|
|
216
|
+
#### 组件测试
|
|
217
|
+
```typescript
|
|
218
|
+
import { describe, it, expect, vi } from 'vitest'
|
|
219
|
+
import { render, screen, fireEvent } from '@testing-library/vue'
|
|
220
|
+
import { createPinia } from 'pinia'
|
|
221
|
+
import LoginForm from '@/components/LoginForm.vue'
|
|
222
|
+
|
|
223
|
+
describe('LoginForm', () => {
|
|
224
|
+
it('submits login form with valid credentials', async () => {
|
|
225
|
+
const { emitted } = render(LoginForm, {
|
|
226
|
+
global: {
|
|
227
|
+
plugins: [createPinia()],
|
|
228
|
+
},
|
|
229
|
+
})
|
|
230
|
+
|
|
231
|
+
await fireEvent.update(screen.getByLabelText('Email'), 'test@example.com')
|
|
232
|
+
await fireEvent.update(screen.getByLabelText('Password'), 'password123')
|
|
233
|
+
await fireEvent.click(screen.getByRole('button', { name: /login/i }))
|
|
234
|
+
|
|
235
|
+
expect(emitted()).toHaveProperty('submit')
|
|
236
|
+
expect(emitted().submit[0]).toEqual([{
|
|
237
|
+
email: 'test@example.com',
|
|
238
|
+
password: 'password123',
|
|
239
|
+
}])
|
|
240
|
+
})
|
|
241
|
+
|
|
242
|
+
it('shows validation errors for invalid input', async () => {
|
|
243
|
+
render(LoginForm, {
|
|
244
|
+
global: {
|
|
245
|
+
plugins: [createPinia()],
|
|
246
|
+
},
|
|
247
|
+
})
|
|
248
|
+
|
|
249
|
+
await fireEvent.click(screen.getByRole('button', { name: /login/i }))
|
|
250
|
+
|
|
251
|
+
expect(screen.getByText(/email is required/i)).toBeInTheDocument()
|
|
252
|
+
})
|
|
253
|
+
})
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
#### Store 测试
|
|
257
|
+
```typescript
|
|
258
|
+
import { describe, it, expect, beforeEach } from 'vitest'
|
|
259
|
+
import { setActivePinia, createPinia } from 'pinia'
|
|
260
|
+
import { useAuthStore } from '@/stores/authStore'
|
|
261
|
+
|
|
262
|
+
describe('AuthStore', () => {
|
|
263
|
+
beforeEach(() => {
|
|
264
|
+
setActivePinia(createPinia())
|
|
265
|
+
})
|
|
266
|
+
|
|
267
|
+
it('logs in user successfully', async () => {
|
|
268
|
+
const store = useAuthStore()
|
|
269
|
+
|
|
270
|
+
await store.login('test@example.com', 'password123')
|
|
271
|
+
|
|
272
|
+
expect(store.isAuthenticated).toBe(true)
|
|
273
|
+
expect(store.currentUser).toBeDefined()
|
|
274
|
+
})
|
|
275
|
+
|
|
276
|
+
it('handles login failure', async () => {
|
|
277
|
+
const store = useAuthStore()
|
|
278
|
+
|
|
279
|
+
await expect(
|
|
280
|
+
store.login('test@example.com', 'wrongpassword')
|
|
281
|
+
).rejects.toThrow('Invalid credentials')
|
|
282
|
+
|
|
283
|
+
expect(store.isAuthenticated).toBe(false)
|
|
284
|
+
})
|
|
285
|
+
})
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
### E2E 测试规范(Playwright)
|
|
289
|
+
|
|
290
|
+
```typescript
|
|
291
|
+
import { test, expect } from '@playwright/test'
|
|
292
|
+
|
|
293
|
+
test.describe('User Login Flow', () => {
|
|
294
|
+
test('successful login redirects to dashboard', async ({ page }) => {
|
|
295
|
+
await page.goto('/login')
|
|
296
|
+
|
|
297
|
+
await page.fill('input[name="email"]', 'test@example.com')
|
|
298
|
+
await page.fill('input[name="password"]', 'Password123!')
|
|
299
|
+
await page.click('button[type="submit"]')
|
|
300
|
+
|
|
301
|
+
await expect(page).toHaveURL('/dashboard')
|
|
302
|
+
await expect(page.getByText('Welcome')).toBeVisible()
|
|
303
|
+
})
|
|
304
|
+
|
|
305
|
+
test('failed login shows error message', async ({ page }) => {
|
|
306
|
+
await page.goto('/login')
|
|
307
|
+
|
|
308
|
+
await page.fill('input[name="email"]', 'test@example.com')
|
|
309
|
+
await page.fill('input[name="password"]', 'wrongpassword')
|
|
310
|
+
await page.click('button[type="submit"]')
|
|
311
|
+
|
|
312
|
+
await expect(page.getByText(/invalid credentials/i)).toBeVisible()
|
|
313
|
+
await expect(page).toHaveURL('/login')
|
|
314
|
+
})
|
|
315
|
+
|
|
316
|
+
test('validates required fields', async ({ page }) => {
|
|
317
|
+
await page.goto('/login')
|
|
318
|
+
|
|
319
|
+
await page.click('button[type="submit"]')
|
|
320
|
+
|
|
321
|
+
await expect(page.getByText(/email is required/i)).toBeVisible()
|
|
322
|
+
await expect(page.getByText(/password is required/i)).toBeVisible()
|
|
323
|
+
})
|
|
324
|
+
})
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
## 性能测试
|
|
328
|
+
|
|
329
|
+
### API 性能测试(可选)
|
|
330
|
+
|
|
331
|
+
使用 Apache Bench 或 wrk:
|
|
332
|
+
```bash
|
|
333
|
+
# 100 并发,持续 10 秒
|
|
334
|
+
ab -n 10000 -c 100 http://localhost:8080/api/v1/users
|
|
335
|
+
|
|
336
|
+
# 或使用 wrk
|
|
337
|
+
wrk -t12 -c100 -d10s http://localhost:8080/api/v1/users
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
验证要求:
|
|
341
|
+
- **响应时间**: P95 < 200ms
|
|
342
|
+
- **吞吐量**: >= 1000 QPS
|
|
343
|
+
|
|
344
|
+
### 前端性能测试
|
|
345
|
+
|
|
346
|
+
使用 Lighthouse CI:
|
|
347
|
+
```bash
|
|
348
|
+
npm install -g @lhci/cli
|
|
349
|
+
|
|
350
|
+
# 运行 Lighthouse
|
|
351
|
+
lhci autorun
|
|
352
|
+
|
|
353
|
+
# 检查性能分数
|
|
354
|
+
# 期望分数:>= 90
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
## 安全测试
|
|
358
|
+
|
|
359
|
+
### 基础安全检查
|
|
360
|
+
|
|
361
|
+
1. **SQL 注入测试**:
|
|
362
|
+
- 尝试注入恶意 SQL:`' OR '1'='1`
|
|
363
|
+
- 验证参数化查询有效
|
|
364
|
+
|
|
365
|
+
2. **XSS 测试**:
|
|
366
|
+
- 尝试注入脚本:`<script>alert('XSS')</script>`
|
|
367
|
+
- 验证输出转义有效
|
|
368
|
+
|
|
369
|
+
3. **CSRF 测试**:
|
|
370
|
+
- 验证 CSRF token 机制
|
|
371
|
+
|
|
372
|
+
4. **认证测试**:
|
|
373
|
+
- 未认证访问受保护资源应返回 401
|
|
374
|
+
- Token 过期后应拒绝访问
|
|
375
|
+
|
|
376
|
+
## 钉钉通知集成
|
|
377
|
+
|
|
378
|
+
### 测试失败时发送通知
|
|
379
|
+
|
|
380
|
+
当以下情况发生时触发钉钉通知:
|
|
381
|
+
- 测试用例失败
|
|
382
|
+
- 覆盖率不达标
|
|
383
|
+
- E2E 测试失败
|
|
384
|
+
- 验收标准未满足
|
|
385
|
+
|
|
386
|
+
通知格式(JSON):
|
|
387
|
+
```json
|
|
388
|
+
{
|
|
389
|
+
"msgtype": "markdown",
|
|
390
|
+
"markdown": {
|
|
391
|
+
"title": "SDD 工作流失败",
|
|
392
|
+
"text": "❌ **SDD 工作流失败**\n\n- **工作流**: user-login\n- **失败阶段**: Tester\n- **错误信息**: 3/10 测试用例失败\n- **建议操作**: 返回 Backend Engineer 修复\n\n🕐 **时间**: 2026-01-06 14:30:00"
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
**注意**: 消息必须包含 "SDD" 关键字。
|
|
398
|
+
|
|
399
|
+
## 与其他 Skills 的协作
|
|
400
|
+
|
|
401
|
+
1. **上游**: 接收 Backend Engineer 和 Frontend Engineer 的代码实现
|
|
402
|
+
2. **下游**:
|
|
403
|
+
- 测试通过 → 交给 Code Reviewer
|
|
404
|
+
- 测试失败 → 返回相应的 Engineer 修复
|
|
405
|
+
3. **并行**: 无
|
|
406
|
+
|
|
407
|
+
## 在 Pre-Execution Review 中的角色
|
|
408
|
+
|
|
409
|
+
提供测试策略评估:
|
|
410
|
+
- 测试复杂度评估
|
|
411
|
+
- 是否需要特殊测试环境(如测试数据库)
|
|
412
|
+
- E2E 测试场景是否完整
|
|
413
|
+
- 性能测试是否必要
|
|
414
|
+
- 安全测试关注点
|
|
415
|
+
|
|
416
|
+
## 示例对话
|
|
417
|
+
|
|
418
|
+
```
|
|
419
|
+
我(Tester):
|
|
420
|
+
|
|
421
|
+
已读取需求规格:specs/requirements/user-login.md
|
|
422
|
+
已读取后端 OpenSpec 和前端 OpenSpec
|
|
423
|
+
|
|
424
|
+
开始执行测试验证...
|
|
425
|
+
|
|
426
|
+
📊 后端测试结果:
|
|
427
|
+
- 单元测试:✅ 12/12 通过
|
|
428
|
+
- 覆盖率:✅ 92% (>= 90%)
|
|
429
|
+
- 集成测试:✅ 5/5 通过
|
|
430
|
+
|
|
431
|
+
📊 前端测试结果:
|
|
432
|
+
- 单元测试:✅ 8/8 通过
|
|
433
|
+
- 覆盖率:✅ 75% (>= 70%)
|
|
434
|
+
- E2E 测试:❌ 1/3 失败
|
|
435
|
+
|
|
436
|
+
❌ E2E 测试失败详情:
|
|
437
|
+
- 测试:用户登录成功后跳转到 Dashboard
|
|
438
|
+
- 失败原因:登录成功后停留在 /login,未跳转到 /dashboard
|
|
439
|
+
- 涉及文件:frontend/src/components/LoginForm.vue:45
|
|
440
|
+
|
|
441
|
+
验收标准检查:
|
|
442
|
+
- [x] 用户可以成功完成核心操作
|
|
443
|
+
- [x] 错误情况下有友好提示
|
|
444
|
+
- [x] 性能满足要求
|
|
445
|
+
- [x] 通过安全测试
|
|
446
|
+
- [ ] E2E 测试全部通过 ❌
|
|
447
|
+
|
|
448
|
+
建议操作:
|
|
449
|
+
返回 Frontend Engineer 修复路由跳转逻辑
|
|
450
|
+
|
|
451
|
+
📢 已发送钉钉通知(失败告警)
|
|
452
|
+
|
|
453
|
+
[Frontend Engineer 修复后]
|
|
454
|
+
|
|
455
|
+
重新执行测试...
|
|
456
|
+
|
|
457
|
+
✅ 所有测试通过!
|
|
458
|
+
|
|
459
|
+
📊 最终测试统计:
|
|
460
|
+
- 后端单元测试:12/12 通过,覆盖率 92%
|
|
461
|
+
- 前端单元测试:8/8 通过,覆盖率 75%
|
|
462
|
+
- E2E 测试:3/3 通过
|
|
463
|
+
- 验收标准:5/5 满足
|
|
464
|
+
|
|
465
|
+
下一步请 Code Reviewer 进行代码审查。
|
|
466
|
+
```
|
package/uninstall.js
ADDED
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* SDD Skills Uninstaller for Claude Code
|
|
5
|
+
*
|
|
6
|
+
* This script removes the SDD Skills package from Claude Code's skills directory.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { existsSync, rmSync } from 'fs';
|
|
10
|
+
import { join } from 'path';
|
|
11
|
+
import { homedir } from 'os';
|
|
12
|
+
import inquirer from 'inquirer';
|
|
13
|
+
import chalk from 'chalk';
|
|
14
|
+
|
|
15
|
+
// Paths
|
|
16
|
+
const GLOBAL_SKILLS_DIR = join(homedir(), '.claude', 'skills');
|
|
17
|
+
const LOCAL_SKILLS_DIR = join(process.cwd(), '.claude', 'skills');
|
|
18
|
+
const GLOBAL_CONFIG = join(homedir(), '.claude', 'dingtalk-config.json');
|
|
19
|
+
const LOCAL_CONFIG = join(process.cwd(), '.claude', 'dingtalk-config.json');
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Check if SDD Skills are installed in a location
|
|
23
|
+
*/
|
|
24
|
+
function checkInstallation(skillsDir) {
|
|
25
|
+
if (!existsSync(skillsDir)) {
|
|
26
|
+
return { installed: false, skills: [] };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const sddSkills = [
|
|
30
|
+
'sae',
|
|
31
|
+
'backend-engineer',
|
|
32
|
+
'frontend-engineer',
|
|
33
|
+
'tester',
|
|
34
|
+
'code-reviewer',
|
|
35
|
+
'git-engineer',
|
|
36
|
+
'notifier',
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
const installedSkills = sddSkills.filter(skill =>
|
|
40
|
+
existsSync(join(skillsDir, skill, 'SKILL.md'))
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
return {
|
|
44
|
+
installed: installedSkills.length > 0,
|
|
45
|
+
skills: installedSkills,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Prompt user for uninstallation options
|
|
51
|
+
*/
|
|
52
|
+
async function promptUninstallation() {
|
|
53
|
+
const globalInstallation = checkInstallation(GLOBAL_SKILLS_DIR);
|
|
54
|
+
const localInstallation = checkInstallation(LOCAL_SKILLS_DIR);
|
|
55
|
+
|
|
56
|
+
if (!globalInstallation.installed && !localInstallation.installed) {
|
|
57
|
+
console.log(chalk.yellow('\n⚠️ No SDD Skills installation found.\n'));
|
|
58
|
+
process.exit(0);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
console.log(chalk.bold('\n🗑️ SDD Skills Uninstaller\n'));
|
|
62
|
+
|
|
63
|
+
// Build choices based on what's installed
|
|
64
|
+
const locationChoices = [];
|
|
65
|
+
|
|
66
|
+
if (globalInstallation.installed) {
|
|
67
|
+
locationChoices.push({
|
|
68
|
+
name: `${chalk.green('Global')} - ${globalInstallation.skills.length} Skills (${chalk.gray('~/.claude/skills')})`,
|
|
69
|
+
value: 'global',
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (localInstallation.installed) {
|
|
74
|
+
locationChoices.push({
|
|
75
|
+
name: `${chalk.blue('Local')} - ${localInstallation.skills.length} Skills (${chalk.gray('.claude/skills')})`,
|
|
76
|
+
value: 'local',
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (locationChoices.length === 0) {
|
|
81
|
+
console.log(chalk.yellow('\n⚠️ No SDD Skills installation found.\n'));
|
|
82
|
+
process.exit(0);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const answers = await inquirer.prompt([
|
|
86
|
+
{
|
|
87
|
+
type: 'list',
|
|
88
|
+
name: 'location',
|
|
89
|
+
message: 'Which installation would you like to remove?',
|
|
90
|
+
choices: locationChoices,
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
type: 'confirm',
|
|
94
|
+
name: 'removeConfig',
|
|
95
|
+
message: 'Also remove DingTalk configuration?',
|
|
96
|
+
default: true,
|
|
97
|
+
when: (answers) => {
|
|
98
|
+
const configPath = answers.location === 'global' ? GLOBAL_CONFIG : LOCAL_CONFIG;
|
|
99
|
+
return existsSync(configPath);
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
type: 'confirm',
|
|
104
|
+
name: 'confirm',
|
|
105
|
+
message: (answers) => {
|
|
106
|
+
const location = answers.location;
|
|
107
|
+
const skillsCount = location === 'global'
|
|
108
|
+
? globalInstallation.skills.length
|
|
109
|
+
: localInstallation.skills.length;
|
|
110
|
+
return `${chalk.red('Are you sure you want to remove')} ${chalk.bold.red(skillsCount)} ${chalk.red('Skills?')}`;
|
|
111
|
+
},
|
|
112
|
+
default: false,
|
|
113
|
+
},
|
|
114
|
+
]);
|
|
115
|
+
|
|
116
|
+
return answers;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Remove Skills from target directory
|
|
121
|
+
*/
|
|
122
|
+
function removeSkills(location) {
|
|
123
|
+
const skillsDir = location === 'global' ? GLOBAL_SKILLS_DIR : LOCAL_SKILLS_DIR;
|
|
124
|
+
const installation = checkInstallation(skillsDir);
|
|
125
|
+
|
|
126
|
+
console.log(chalk.bold(`\n🗑️ Removing Skills from ${chalk.cyan(skillsDir)}\n`));
|
|
127
|
+
|
|
128
|
+
let removedCount = 0;
|
|
129
|
+
|
|
130
|
+
for (const skill of installation.skills) {
|
|
131
|
+
const skillPath = join(skillsDir, skill);
|
|
132
|
+
|
|
133
|
+
try {
|
|
134
|
+
rmSync(skillPath, { recursive: true, force: true });
|
|
135
|
+
console.log(chalk.gray(` ✓ Removed ${skill}`));
|
|
136
|
+
removedCount++;
|
|
137
|
+
} catch (error) {
|
|
138
|
+
console.log(chalk.red(` ✗ Failed to remove ${skill}: ${error.message}`));
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
console.log(chalk.bold(`\n✅ Removed ${chalk.green(removedCount)}/${installation.skills.length} Skills\n`));
|
|
143
|
+
|
|
144
|
+
return removedCount;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Remove DingTalk configuration
|
|
149
|
+
*/
|
|
150
|
+
function removeConfig(location) {
|
|
151
|
+
const configPath = location === 'global' ? GLOBAL_CONFIG : LOCAL_CONFIG;
|
|
152
|
+
|
|
153
|
+
if (!existsSync(configPath)) {
|
|
154
|
+
return false;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
try {
|
|
158
|
+
rmSync(configPath, { force: true });
|
|
159
|
+
console.log(chalk.gray(` ✓ Removed DingTalk configuration: ${configPath}\n`));
|
|
160
|
+
return true;
|
|
161
|
+
} catch (error) {
|
|
162
|
+
console.log(chalk.red(` ✗ Failed to remove config: ${error.message}\n`));
|
|
163
|
+
return false;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Display uninstallation summary
|
|
169
|
+
*/
|
|
170
|
+
function displaySummary(location, skillsRemoved, configRemoved) {
|
|
171
|
+
console.log(chalk.bold.green('\n✨ Uninstallation Complete!\n'));
|
|
172
|
+
console.log(chalk.bold('📋 Summary:\n'));
|
|
173
|
+
console.log(` ${chalk.gray('Location:')} ${chalk.cyan(location)}`);
|
|
174
|
+
console.log(` ${chalk.gray('Skills removed:')} ${chalk.cyan(skillsRemoved)}`);
|
|
175
|
+
|
|
176
|
+
if (configRemoved) {
|
|
177
|
+
console.log(` ${chalk.gray('Config removed:')} ${chalk.cyan('Yes')}`);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
console.log(chalk.bold('\n💡 Reinstall anytime:\n'));
|
|
181
|
+
console.log(' npx sdd-skills\n');
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Main uninstallation function
|
|
186
|
+
*/
|
|
187
|
+
export default async function uninstall() {
|
|
188
|
+
try {
|
|
189
|
+
// Prompt for uninstallation options
|
|
190
|
+
const answers = await promptUninstallation();
|
|
191
|
+
|
|
192
|
+
// Check if user confirmed
|
|
193
|
+
if (!answers.confirm) {
|
|
194
|
+
console.log(chalk.yellow('\n⚠️ Uninstallation cancelled by user\n'));
|
|
195
|
+
process.exit(0);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Remove Skills
|
|
199
|
+
const skillsRemoved = removeSkills(answers.location);
|
|
200
|
+
|
|
201
|
+
// Remove config if requested
|
|
202
|
+
let configRemoved = false;
|
|
203
|
+
if (answers.removeConfig) {
|
|
204
|
+
configRemoved = removeConfig(answers.location);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Display summary
|
|
208
|
+
displaySummary(answers.location, skillsRemoved, configRemoved);
|
|
209
|
+
|
|
210
|
+
} catch (error) {
|
|
211
|
+
if (error.isTtyError) {
|
|
212
|
+
console.error(chalk.red('\n❌ Error: Prompt could not be rendered in this environment\n'));
|
|
213
|
+
} else if (error.name === 'ExitPromptError') {
|
|
214
|
+
console.log(chalk.yellow('\n⚠️ Uninstallation cancelled by user\n'));
|
|
215
|
+
process.exit(0);
|
|
216
|
+
} else {
|
|
217
|
+
console.error(chalk.red(`\n❌ Error: ${error.message}\n`));
|
|
218
|
+
process.exit(1);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Allow running this script directly
|
|
224
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
225
|
+
uninstall();
|
|
226
|
+
}
|