adspecs 0.1.27 → 0.1.29
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/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.qoder-plugin/plugin.json +1 -2
- package/CLAUDE.md +19 -11
- package/INSTALL.md +90 -3
- package/README.md +32 -10
- package/bin/adspecs.js +8 -5
- package/hooks/commit-queue.js +8 -7
- package/hooks/hooks.json +4 -3
- package/hooks/platform.js +141 -0
- package/hooks/session-logger.js +9 -13
- package/hooks/session-start.js +10 -14
- package/hooks/wiki-queue.js +8 -7
- package/package.json +2 -1
- package/scripts/postinstall.js +14 -11
- package/src/commands/plugin.js +339 -22
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "adspecs",
|
|
3
3
|
"displayName": "adspecs AI 工程规范驱动开发插件",
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.29",
|
|
5
5
|
"description": "AI-first specification-driven development plugin for ECP platform full-lifecycle development",
|
|
6
6
|
"descriptionZh": "AI优先的工程规范驱动开发插件,覆盖需求分析、PRD、系统设计、任务拆解、TDD、代码评审、Wiki同步全流程",
|
|
7
7
|
"author": {
|
|
@@ -25,7 +25,6 @@
|
|
|
25
25
|
"documentation"
|
|
26
26
|
],
|
|
27
27
|
"skills": "./skills/",
|
|
28
|
-
"rules": "./rules/",
|
|
29
28
|
"hooks": "./hooks/hooks.json",
|
|
30
29
|
"preserveUpstreamMetadata": true
|
|
31
30
|
}
|
package/CLAUDE.md
CHANGED
|
@@ -96,22 +96,27 @@ Skills check for `.adspecs/extensions.yml` in the target project root, supportin
|
|
|
96
96
|
|
|
97
97
|
## Hooks System
|
|
98
98
|
|
|
99
|
-
Hooks are configured in `hooks/hooks.json` and run as Node.js scripts:
|
|
99
|
+
Hooks are configured in `hooks/hooks.json` and run as Node.js scripts. A shared platform detection module (`hooks/platform.js`) provides cross-platform compatibility between Claude Code and Qoder:
|
|
100
100
|
|
|
101
101
|
| Event | Script | Purpose |
|
|
102
102
|
| ------------------------------------------------- | ------------------------- | ----------------------------------------------------------------------------------------- |
|
|
103
|
-
| `SessionStart` | `session-start.js` | Injects `skills/using-
|
|
104
|
-
| `SessionStart` | `wiki-queue.js init` | Initialize wiki update queue file in
|
|
105
|
-
| `SessionStart` | `commit-queue.js init` | Initialize commit candidates queue file in
|
|
103
|
+
| `SessionStart` | `session-start.js` | Injects `skills/using-adspecs/SKILL.md` as agent context (no-op if skill doesn't exist) |
|
|
104
|
+
| `SessionStart` | `wiki-queue.js init` | Initialize wiki update queue file in state dir |
|
|
105
|
+
| `SessionStart` | `commit-queue.js init` | Initialize commit candidates queue file in state dir |
|
|
106
106
|
| `SessionStart` | `session-logger.js init` | Detect unsaved previous session → inject recovery reminder or normal guidance |
|
|
107
|
-
| `PreCompact` | `session-logger.js mark` | Context compaction imminent → inject urgent save reminder for
|
|
108
|
-
| `PostToolUse` (Write/Edit on code/doc files)
|
|
109
|
-
| `PostToolUse` (Write on `pipeline-manifest.json`) | `commit-queue.js enqueue` | Track completed features for commit
|
|
107
|
+
| `PreCompact` | `session-logger.js mark` | Context compaction imminent → inject urgent save reminder for agent to write log |
|
|
108
|
+
| `PostToolUse` (Write/Edit/SearchReplace on code/doc files) | `wiki-queue.js enqueue` | Track changed files for wiki sync |
|
|
109
|
+
| `PostToolUse` (Write/SearchReplace on `pipeline-manifest.json`) | `commit-queue.js enqueue` | Track completed features for commit |
|
|
110
110
|
| `Stop` | `wiki-queue.js report` | Suggest wiki sync at session end |
|
|
111
111
|
| `Stop` | `commit-queue.js report` | Suggest commit at session end |
|
|
112
112
|
| `Stop` | `session-logger.js report`| Mark session as missed if log not saved → recovery at next SessionStart |
|
|
113
113
|
|
|
114
|
-
|
|
114
|
+
### Platform Detection (`hooks/platform.js`)
|
|
115
|
+
|
|
116
|
+
- Detects platform via `ADSPECS_PLATFORM` env var, or platform-specific env vars (`CLAUDE_PLUGIN_ROOT` / `QODER_PLUGIN_ROOT`)
|
|
117
|
+
- State directory: `.claude/` for Claude Code, `.qoder/` for Qoder
|
|
118
|
+
- Tool output files: reads `CLAUDE_TOOL_OUTPUT_FILES` or `QODER_TOOL_OUTPUT_FILES`
|
|
119
|
+
- Output protocol: unified `hookSpecificOutput.additionalContext` JSON stdout format
|
|
115
120
|
|
|
116
121
|
## Path Configuration
|
|
117
122
|
|
|
@@ -209,9 +214,12 @@ npm publish --registry=https://registry.npmjs.org
|
|
|
209
214
|
## Installation & Distribution
|
|
210
215
|
|
|
211
216
|
```bash
|
|
212
|
-
# Register marketplace
|
|
217
|
+
# Claude Code: Register marketplace + install
|
|
213
218
|
claude plugins marketplace add <path-to>/.claude-plugin --scope user|project
|
|
214
|
-
|
|
215
|
-
# Install plugin
|
|
216
219
|
claude plugins install adspecs --scope user|project
|
|
220
|
+
|
|
221
|
+
# Qoder: CLI install
|
|
222
|
+
adspecs plugin install --platform qoder --scope user|project
|
|
223
|
+
|
|
224
|
+
# Qoder: Manual — ensure .qoder-plugin/plugin.json is accessible to Qoder
|
|
217
225
|
```
|
package/INSTALL.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
## 环境准备
|
|
4
4
|
|
|
5
|
-
### 1. 安装 superpowers
|
|
5
|
+
### 1. 安装 superpowers 插件(前置依赖,仅 Claude Code)
|
|
6
6
|
|
|
7
7
|
adspecs 的编码阶段依赖 `superpowers` 插件提供的 `subagent-driven-development`、`test-driven-development` 等通用 Skill,必须先安装:
|
|
8
8
|
|
|
@@ -22,7 +22,55 @@ adspecs 的编码阶段依赖 `superpowers` 插件提供的 `subagent-driven-dev
|
|
|
22
22
|
/plugin install superpowers@superpowers-marketplace
|
|
23
23
|
```
|
|
24
24
|
|
|
25
|
-
|
|
25
|
+
> **注意**:Qoder 平台无需安装 superpowers,adspecs 的 Skills 在 Qoder 中可独立运行。
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## 在 Qoder 中安装
|
|
30
|
+
|
|
31
|
+
adspecs 通过 `.qoder-plugin/plugin.json` 提供 Qoder 原生插件支持。
|
|
32
|
+
|
|
33
|
+
### 方式 A:CLI 自动安装(推荐)
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
# 全局安装 adspecs CLI
|
|
37
|
+
npm install -g adspecs
|
|
38
|
+
|
|
39
|
+
# 注册为 Qoder 插件(自动检测平台)
|
|
40
|
+
adspecs plugin install --platform qoder
|
|
41
|
+
|
|
42
|
+
# 或指定项目级安装
|
|
43
|
+
adspecs plugin install --platform qoder --scope project
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### 方式 B:手动安装
|
|
47
|
+
|
|
48
|
+
1. 将 adspecs 插件目录放置到 Qoder 可访问的路径
|
|
49
|
+
2. 确保目录中包含 `.qoder-plugin/plugin.json`
|
|
50
|
+
3. 在 Qoder 插件设置中添加该目录路径
|
|
51
|
+
4. 重启 Qoder 使插件生效
|
|
52
|
+
|
|
53
|
+
### 方式 C:项目级安装(团队协作)
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
# 进入项目根目录
|
|
57
|
+
cd /path/to/your/project
|
|
58
|
+
|
|
59
|
+
# 注册为项目级 Qoder 插件
|
|
60
|
+
adspecs plugin install --platform qoder --scope project
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
此命令会在项目中创建 `.qoder-plugin/` 链接,使插件随项目版本控制。
|
|
64
|
+
|
|
65
|
+
### 卸载
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
adspecs plugin uninstall --platform qoder --scope project
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
## 在 Claude Code 中安装
|
|
26
74
|
|
|
27
75
|
adspecs 插件提供**用户级**和**项目级**两种安装方式,按需选择:
|
|
28
76
|
|
|
@@ -129,6 +177,45 @@ claude plugins update adspecs --scope project
|
|
|
129
177
|
- **团队协作,需要统一插件版本** → 选**项目级**,插件随代码版本控制
|
|
130
178
|
- **同时存在两种需求** → 两种可同时安装,项目级优先于用户级(就近原则)
|
|
131
179
|
|
|
132
|
-
> **注意**:安装完成后,建议重启 Claude Code
|
|
180
|
+
> **注意**:安装完成后,建议重启 AI 平台(Claude Code / Qoder)以确保所有 Skill 被正确加载。如遇到 Skill 未生效,可执行 `/plugins` 查看当前已加载的插件列表。
|
|
133
181
|
|
|
134
182
|
---
|
|
183
|
+
|
|
184
|
+
## 跨平台支持
|
|
185
|
+
|
|
186
|
+
### 平台自动检测
|
|
187
|
+
|
|
188
|
+
adspecs 通过 `hooks/platform.js` 模块自动检测运行环境,无需手动配置:
|
|
189
|
+
|
|
190
|
+
| 检测方式 | 说明 |
|
|
191
|
+
|----------|------|
|
|
192
|
+
| `ADSPECS_PLATFORM` 环境变量 | 显式指定平台(优先级最高) |
|
|
193
|
+
| `CLAUDE_PLUGIN_ROOT` 环境变量 | Claude Code 自动注入 |
|
|
194
|
+
| `QODER_PLUGIN_ROOT` 环境变量 | Qoder 自动注入 |
|
|
195
|
+
|
|
196
|
+
### 环境变量
|
|
197
|
+
|
|
198
|
+
| 环境变量 | 说明 | 可选值 |
|
|
199
|
+
|----------|------|--------|
|
|
200
|
+
| `ADSPECS_PLATFORM` | 显式指定运行平台 | `claude` / `qoder` |
|
|
201
|
+
| `CLAUDE_PLUGIN_ROOT` | Claude Code 注入的插件根路径 | 自动 |
|
|
202
|
+
| `QODER_PLUGIN_ROOT` | Qoder 注入的插件根路径 | 自动 |
|
|
203
|
+
|
|
204
|
+
通常无需手动设置,平台会自动注入。若 hooks 执行异常,可尝试设置 `ADSPECS_PLATFORM=qoder`。
|
|
205
|
+
|
|
206
|
+
### Hooks 系统
|
|
207
|
+
|
|
208
|
+
adspecs 的 hooks 脚本(`hooks/*.js`)已适配双平台,通过统一的 `platform.js` 模块获取:
|
|
209
|
+
|
|
210
|
+
- 平台类型(`claude` / `qoder`)
|
|
211
|
+
- 状态目录(`.claude/` / `.qoder/`)
|
|
212
|
+
- 工具输出文件路径
|
|
213
|
+
|
|
214
|
+
hooks 配置位于 `hooks/hooks.json`,支持以下事件:
|
|
215
|
+
|
|
216
|
+
| 事件 | 说明 |
|
|
217
|
+
|------|------|
|
|
218
|
+
| `SessionStart` | 会话启动时注入上下文 |
|
|
219
|
+
| `PreCompact` | 上下文压缩前保存提醒 |
|
|
220
|
+
| `PostToolUse` | 文件变更后追踪队列 |
|
|
221
|
+
| `Stop` | 会话结束时建议操作 |
|
package/README.md
CHANGED
|
@@ -2,20 +2,24 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/adspecs)
|
|
4
4
|
[](https://www.npmjs.com/package/adspecs)
|
|
5
|
+
[](#安装)
|
|
5
6
|
|
|
6
7
|
---
|
|
7
8
|
|
|
8
9
|
## 简介
|
|
9
10
|
|
|
10
|
-
AdSpecs
|
|
11
|
+
AdSpecs 是一个支持 **Claude Code** 和 **Qoder** 双平台的 AI 工程研发协同插件,提供:
|
|
11
12
|
|
|
12
13
|
- **21 个 Skill**(Slash 命令) — 覆盖需求分析、架构规划、系统设计、任务拆解、单元测试、代码审查、文档导出、Wiki 同步
|
|
13
14
|
- **CLI 工具** — `adspecs init` 一键脚手架、`adspecs plugin install` 插件注册、`adspecs doctor` 环境检查
|
|
14
15
|
- **21 个标准文档模板** — 需求 / 设计 / 任务 / 测试 / 评审 / 架构全覆盖
|
|
15
|
-
- **多技术栈参考** —
|
|
16
|
+
- **多技术栈参考** — Java 后端、Vue 3 前端、Ant Design React 前端、Python 后端等 11 套参考工程
|
|
17
|
+
- **双平台支持** — 原生兼容 Claude Code 和 Qoder,自动检测运行环境
|
|
16
18
|
- **npm 分发** — `npm install -g adspecs` 即装即用
|
|
17
19
|
|
|
18
|
-
|
|
20
|
+
> **平台依赖说明**:
|
|
21
|
+
> - **Claude Code**:需配合 [`superpowers`](https://github.com/anthropics/claude-plugins-official) 插件(提供 TDD、子代理驱动开发等通用 Skill)一起使用
|
|
22
|
+
> - **Qoder**:adspecs 的 Skills 可独立运行,无需额外依赖
|
|
19
23
|
|
|
20
24
|
---
|
|
21
25
|
|
|
@@ -344,12 +348,25 @@ Skill 支持通过 `.adspecs/paths.json` 自定义输出目录:
|
|
|
344
348
|
|
|
345
349
|
详细安装说明见 [INSTALL.md](./INSTALL.md)。
|
|
346
350
|
|
|
351
|
+
### Claude Code
|
|
352
|
+
|
|
353
|
+
```bash
|
|
354
|
+
npm install -g adspecs
|
|
355
|
+
adspecs plugin install # 用户级(默认)
|
|
356
|
+
adspecs plugin install --scope project # 项目级
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
### Qoder
|
|
360
|
+
|
|
347
361
|
```bash
|
|
348
|
-
# 安装
|
|
349
362
|
npm install -g adspecs
|
|
350
|
-
adspecs plugin install #
|
|
351
|
-
adspecs plugin install --scope project # 项目级
|
|
363
|
+
adspecs plugin install --platform qoder # 用户级
|
|
364
|
+
adspecs plugin install --platform qoder --scope project # 项目级
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
### 通用命令
|
|
352
368
|
|
|
369
|
+
```bash
|
|
353
370
|
# 升级
|
|
354
371
|
npm install -g adspecs@latest
|
|
355
372
|
|
|
@@ -363,15 +380,20 @@ adspecs plugin uninstall --scope project
|
|
|
363
380
|
## 常见问题
|
|
364
381
|
|
|
365
382
|
**Q1:adspecs 与 superpowers 的关系?**
|
|
366
|
-
adspecs 提供领域专属 Skill(需求、设计、模板、规范),superpowers 提供通用 AI 工程 Skill(TDD
|
|
383
|
+
adspecs 提供领域专属 Skill(需求、设计、模板、规范),superpowers 提供通用 AI 工程 Skill(TDD、子代理、调试)。
|
|
384
|
+
- **Claude Code**:两者分工协作,编码阶段必须同时安装
|
|
385
|
+
- **Qoder**:adspecs 可独立运行,无需 superpowers
|
|
367
386
|
|
|
368
387
|
**Q2:用户级和项目级安装可以共存吗?**
|
|
369
388
|
可以,项目级优先于用户级(就近原则)。个人开发选用户级,团队协作选项目级。
|
|
370
389
|
|
|
371
|
-
**Q3
|
|
390
|
+
**Q3:Claude Code 和 Qoder 可以同时使用吗?**
|
|
391
|
+
可以。adspecs 通过 `.claude-plugin/` 和 `.qoder-plugin/` 分别提供两个平台的插件配置,互不干扰。使用 `--platform` 参数指定目标平台即可。
|
|
392
|
+
|
|
393
|
+
**Q4:安装后 Skill 未生效?**
|
|
372
394
|
1. 执行 `/plugins` 查看 adspecs 是否在已加载列表
|
|
373
|
-
2. 重启 Claude Code
|
|
374
|
-
3. 检查安装 scope
|
|
395
|
+
2. 重启 Claude Code 或 Qoder
|
|
396
|
+
3. 检查安装 scope 对应的目录是否存在(`.claude-plugin/` 或 `.qoder-plugin/`)
|
|
375
397
|
|
|
376
398
|
**Q4:如何升级?**
|
|
377
399
|
```bash
|
package/bin/adspecs.js
CHANGED
|
@@ -44,12 +44,13 @@ program
|
|
|
44
44
|
// ============ plugin 命令组 ============
|
|
45
45
|
const pluginCmd = program
|
|
46
46
|
.command('plugin')
|
|
47
|
-
.description('Claude Code
|
|
47
|
+
.description('AI 平台插件管理(支持 Claude Code / Qoder)');
|
|
48
48
|
|
|
49
49
|
pluginCmd
|
|
50
50
|
.command('install')
|
|
51
|
-
.description('注册当前 adspecs 为
|
|
51
|
+
.description('注册当前 adspecs 为 AI 平台插件')
|
|
52
52
|
.option('--scope <scope>', '安装范围: user|project', 'user')
|
|
53
|
+
.option('--platform <platform>', '目标平台: claude|qoder(默认自动检测)')
|
|
53
54
|
.action(async (options) => {
|
|
54
55
|
try {
|
|
55
56
|
const pluginCmd = require('../src/commands/plugin');
|
|
@@ -63,8 +64,9 @@ pluginCmd
|
|
|
63
64
|
|
|
64
65
|
pluginCmd
|
|
65
66
|
.command('uninstall')
|
|
66
|
-
.description('从
|
|
67
|
+
.description('从 AI 平台中卸载 adspecs 插件')
|
|
67
68
|
.option('--scope <scope>', '卸载范围: user|project', 'user')
|
|
69
|
+
.option('--platform <platform>', '目标平台: claude|qoder(默认自动检测)')
|
|
68
70
|
.action(async (options) => {
|
|
69
71
|
try {
|
|
70
72
|
const pluginCmd = require('../src/commands/plugin');
|
|
@@ -114,9 +116,10 @@ program.addHelpText('after', `
|
|
|
114
116
|
$ adspecs init --mode simple # 快速初始化(交互选择其余参数)
|
|
115
117
|
$ adspecs init --non-interactive # 完全非交互(使用默认值)
|
|
116
118
|
$ adspecs init --project-name "我的项目" --mode full --front-demo antd
|
|
117
|
-
$ adspecs plugin install # 注册为
|
|
119
|
+
$ adspecs plugin install # 注册为 AI 平台插件(自动检测平台)
|
|
120
|
+
$ adspecs plugin install --platform qoder # 注册为 Qoder 插件
|
|
121
|
+
$ adspecs plugin install --platform claude # 注册为 Claude Code 插件
|
|
118
122
|
$ adspecs plugin install --scope project # 仅注册到当前项目
|
|
119
|
-
$ adspecs plugin install --scope user # 注册到当前用户(全局可用)
|
|
120
123
|
$ adspecs plugin uninstall --scope project # 从当前项目卸载
|
|
121
124
|
$ adspecs doctor # 检查环境依赖
|
|
122
125
|
$ adspecs update # 升级到最新版本
|
package/hooks/commit-queue.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* 在 SessionStart / PostToolUse / Stop 三个 Hook 事件中管理 .commit-candidates 队列文件。
|
|
7
7
|
* 当检测到 pipeline-manifest.json 写入(且 status=completed + mvn_compile=pass),
|
|
8
8
|
* 自动将 feature 信息加入提交候选队列,Stop 时提示用户执行提交。
|
|
9
|
+
* 支持 Claude Code 和 Qoder 双平台。
|
|
9
10
|
*
|
|
10
11
|
* 用法:
|
|
11
12
|
* node hooks/commit-queue.js init — SessionStart: 创建队列文件
|
|
@@ -15,13 +16,13 @@
|
|
|
15
16
|
|
|
16
17
|
const fs = require('fs');
|
|
17
18
|
const path = require('path');
|
|
19
|
+
const platform = require('./platform');
|
|
18
20
|
|
|
19
21
|
// ── 配置 ──────────────────────────────────────────────
|
|
20
|
-
//
|
|
21
|
-
// 使用 process.cwd()
|
|
22
|
-
|
|
23
|
-
const
|
|
24
|
-
const QUEUE_DIR = path.join(PROJECT_ROOT, '.claude');
|
|
22
|
+
// 队列文件放在目标项目根目录的平台状态目录下。
|
|
23
|
+
// 使用 process.cwd() 获取目标项目根目录(平台运行 hook 时 CWD 即为目标项目)。
|
|
24
|
+
const PROJECT_ROOT = platform.getProjectRoot();
|
|
25
|
+
const QUEUE_DIR = platform.getStateDir(PROJECT_ROOT);
|
|
25
26
|
const QUEUE_FILE = path.join(QUEUE_DIR, '.commit-candidates');
|
|
26
27
|
|
|
27
28
|
// ── 工具函数 ──────────────────────────────────────────
|
|
@@ -230,8 +231,8 @@ switch (action) {
|
|
|
230
231
|
break;
|
|
231
232
|
|
|
232
233
|
case 'enqueue':
|
|
233
|
-
//
|
|
234
|
-
enqueueFromManifests(
|
|
234
|
+
// 工具输出文件由平台 Hook 运行时注入为环境变量(兼容 Claude Code / Qoder)
|
|
235
|
+
enqueueFromManifests(platform.getToolOutputFiles());
|
|
235
236
|
break;
|
|
236
237
|
|
|
237
238
|
case 'report':
|
package/hooks/hooks.json
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
+
"_comment": "跨平台 Hook 配置 — 同时支持 Claude Code 和 Qoder。${CLAUDE_PLUGIN_ROOT} 由平台在加载插件时注入(Qoder 兼容此变量或设置 ADSPECS_PLATFORM=qoder)。",
|
|
2
3
|
"hooks": {
|
|
3
4
|
"SessionStart": [
|
|
4
5
|
{
|
|
@@ -35,7 +36,7 @@
|
|
|
35
36
|
"PreToolUse": [],
|
|
36
37
|
"PostToolUse": [
|
|
37
38
|
{
|
|
38
|
-
"matcher": "Write|Edit",
|
|
39
|
+
"matcher": "Write|Edit|SearchReplace",
|
|
39
40
|
"hooks": [
|
|
40
41
|
{
|
|
41
42
|
"type": "command",
|
|
@@ -43,11 +44,11 @@
|
|
|
43
44
|
}
|
|
44
45
|
],
|
|
45
46
|
"conditions": {
|
|
46
|
-
"filePattern": "\\.(java|py|ts|js|go|rs|cs|md)$"
|
|
47
|
+
"filePattern": "\\.(java|py|ts|js|go|rs|cs|md|vue|tsx|jsx)$"
|
|
47
48
|
}
|
|
48
49
|
},
|
|
49
50
|
{
|
|
50
|
-
"matcher": "Write",
|
|
51
|
+
"matcher": "Write|SearchReplace",
|
|
51
52
|
"hooks": [
|
|
52
53
|
{
|
|
53
54
|
"type": "command",
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Platform Detection — 跨 AI 平台兼容工具模块
|
|
5
|
+
*
|
|
6
|
+
* 为 hooks 脚本提供统一的平台检测、路径解析和输出协议,
|
|
7
|
+
* 同时支持 Claude Code 和 Qoder 两种 AI 编码平台。
|
|
8
|
+
*
|
|
9
|
+
* 检测优先级:
|
|
10
|
+
* 1. 环境变量 ADSPECS_PLATFORM 显式指定('claude' | 'qoder')
|
|
11
|
+
* 2. Qoder 特征环境变量(QODER_PLUGIN_ROOT 等)
|
|
12
|
+
* 3. Claude Code 特征环境变量(CLAUDE_PLUGIN_ROOT 等)
|
|
13
|
+
* 4. 默认回退到 claude(向后兼容)
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const path = require('path');
|
|
17
|
+
|
|
18
|
+
// ── 平台检测 ──────────────────────────────────────────────
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* 检测当前运行的 AI 平台
|
|
22
|
+
* @returns {'claude'|'qoder'}
|
|
23
|
+
*/
|
|
24
|
+
function detectPlatform() {
|
|
25
|
+
// 1. 显式指定
|
|
26
|
+
var explicit = (process.env.ADSPECS_PLATFORM || '').toLowerCase();
|
|
27
|
+
if (explicit === 'qoder' || explicit === 'claude') {
|
|
28
|
+
return explicit;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// 2. Qoder 特征
|
|
32
|
+
if (process.env.QODER_PLUGIN_ROOT || process.env.QODER_SESSION_ID) {
|
|
33
|
+
return 'qoder';
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// 3. Claude Code 特征
|
|
37
|
+
if (process.env.CLAUDE_PLUGIN_ROOT || process.env.CLAUDE_SESSION_ID) {
|
|
38
|
+
return 'claude';
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// 4. 默认回退
|
|
42
|
+
return 'claude';
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** 当前平台(模块加载时确定) */
|
|
46
|
+
var PLATFORM = detectPlatform();
|
|
47
|
+
|
|
48
|
+
// ── 路径解析 ──────────────────────────────────────────────
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* 获取插件根目录
|
|
52
|
+
* 优先使用平台环境变量,回退到 __dirname 相对路径推导
|
|
53
|
+
*
|
|
54
|
+
* @param {string} hooksDir - 调用方的 __dirname(即 hooks/ 目录绝对路径)
|
|
55
|
+
* @returns {string} 插件根目录绝对路径
|
|
56
|
+
*/
|
|
57
|
+
function getPluginRoot(hooksDir) {
|
|
58
|
+
// 平台环境变量优先
|
|
59
|
+
var envRoot = process.env.CLAUDE_PLUGIN_ROOT || process.env.QODER_PLUGIN_ROOT;
|
|
60
|
+
if (envRoot) {
|
|
61
|
+
return path.resolve(envRoot);
|
|
62
|
+
}
|
|
63
|
+
// 回退:hooks/ 的上级目录即插件根
|
|
64
|
+
return path.resolve(hooksDir, '..');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* 获取目标项目根目录
|
|
69
|
+
* 两种平台均使用 process.cwd()(运行时 CWD 即为目标项目)
|
|
70
|
+
*
|
|
71
|
+
* @returns {string}
|
|
72
|
+
*/
|
|
73
|
+
function getProjectRoot() {
|
|
74
|
+
return process.cwd();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* 获取项目内的插件状态目录(用于存放队列文件、marker 等)
|
|
79
|
+
* - Claude Code: {project}/.claude/
|
|
80
|
+
* - Qoder: {project}/.qoder/(若不存在则回退 .claude/ 保持兼容)
|
|
81
|
+
*
|
|
82
|
+
* @param {string} projectRoot - 项目根目录
|
|
83
|
+
* @returns {string}
|
|
84
|
+
*/
|
|
85
|
+
function getStateDir(projectRoot) {
|
|
86
|
+
if (PLATFORM === 'qoder') {
|
|
87
|
+
return path.join(projectRoot, '.qoder');
|
|
88
|
+
}
|
|
89
|
+
return path.join(projectRoot, '.claude');
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// ── 环境变量适配 ──────────────────────────────────────────
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* 获取工具输出文件列表(PostToolUse 时由平台注入)
|
|
96
|
+
* - Claude Code: CLAUDE_TOOL_OUTPUT_FILES
|
|
97
|
+
* - Qoder: QODER_TOOL_OUTPUT_FILES 或 CLAUDE_TOOL_OUTPUT_FILES(兼容)
|
|
98
|
+
*
|
|
99
|
+
* @returns {string} 换行分隔的文件路径列表
|
|
100
|
+
*/
|
|
101
|
+
function getToolOutputFiles() {
|
|
102
|
+
return (
|
|
103
|
+
process.env.QODER_TOOL_OUTPUT_FILES ||
|
|
104
|
+
process.env.CLAUDE_TOOL_OUTPUT_FILES ||
|
|
105
|
+
''
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// ── 输出协议 ──────────────────────────────────────────────
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* 输出 Hook 上下文到 stdout(供 AI 平台读取注入 Agent 上下文)
|
|
113
|
+
*
|
|
114
|
+
* 两种平台当前均使用相同的 JSON stdout 协议:
|
|
115
|
+
* { "hookSpecificOutput": { "hookEventName": "...", "additionalContext": "..." } }
|
|
116
|
+
*
|
|
117
|
+
* @param {string} eventName - Hook 事件名称
|
|
118
|
+
* @param {string} contextText - 要注入的上下文文本
|
|
119
|
+
*/
|
|
120
|
+
function outputContext(eventName, contextText) {
|
|
121
|
+
var output = {
|
|
122
|
+
hookSpecificOutput: {
|
|
123
|
+
hookEventName: eventName,
|
|
124
|
+
additionalContext: contextText,
|
|
125
|
+
},
|
|
126
|
+
};
|
|
127
|
+
process.stdout.write(JSON.stringify(output));
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// ── 导出 ──────────────────────────────────────────────────
|
|
131
|
+
|
|
132
|
+
module.exports = {
|
|
133
|
+
/** 当前检测到的平台: 'claude' | 'qoder' */
|
|
134
|
+
platform: PLATFORM,
|
|
135
|
+
detectPlatform: detectPlatform,
|
|
136
|
+
getPluginRoot: getPluginRoot,
|
|
137
|
+
getProjectRoot: getProjectRoot,
|
|
138
|
+
getStateDir: getStateDir,
|
|
139
|
+
getToolOutputFiles: getToolOutputFiles,
|
|
140
|
+
outputContext: outputContext,
|
|
141
|
+
};
|
package/hooks/session-logger.js
CHANGED
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
* Session Logger — AI 会话日志自动保存 Hook
|
|
5
5
|
*
|
|
6
6
|
* 在 SessionStart / PreCompact / Stop 三个事件中管理会话保存状态,
|
|
7
|
-
* 通过 additionalContext 引导
|
|
7
|
+
* 通过 additionalContext 引导 AI Agent 自行生成并写入结构化日志文件。
|
|
8
|
+
* 支持 Claude Code 和 Qoder 双平台。
|
|
8
9
|
*
|
|
9
10
|
* 用法:
|
|
10
11
|
* node hooks/session-logger.js init — SessionStart: 检测恢复 / 注入引导
|
|
@@ -14,12 +15,13 @@
|
|
|
14
15
|
|
|
15
16
|
const fs = require('fs');
|
|
16
17
|
const path = require('path');
|
|
18
|
+
const platform = require('./platform');
|
|
17
19
|
|
|
18
20
|
// ── 路径解析 ──────────────────────────────────────────────
|
|
19
21
|
// 使用 process.cwd() 定位目标项目(不用 __dirname,因为 __dirname 指向插件目录)
|
|
20
|
-
const PROJECT_ROOT =
|
|
21
|
-
const
|
|
22
|
-
const MARKER_FILE = path.join(
|
|
22
|
+
const PROJECT_ROOT = platform.getProjectRoot();
|
|
23
|
+
const STATE_DIR = platform.getStateDir(PROJECT_ROOT);
|
|
24
|
+
const MARKER_FILE = path.join(STATE_DIR, '.session-save-marker');
|
|
23
25
|
const CONFIG_FILE = path.join(PROJECT_ROOT, '.adspecs', 'session-logger.json');
|
|
24
26
|
const PATHS_FILE = path.join(PROJECT_ROOT, '.adspecs', 'paths.json');
|
|
25
27
|
const FEATURE_FILE = path.join(PROJECT_ROOT, '.adspecs', 'feature.json');
|
|
@@ -112,7 +114,7 @@ function readMarker() {
|
|
|
112
114
|
|
|
113
115
|
/** 写入 marker 文件(覆盖写,单行最新态) */
|
|
114
116
|
function writeMarker(data) {
|
|
115
|
-
fs.mkdirSync(
|
|
117
|
+
fs.mkdirSync(STATE_DIR, { recursive: true });
|
|
116
118
|
fs.writeFileSync(MARKER_FILE, JSON.stringify(data) + '\n', 'utf8');
|
|
117
119
|
}
|
|
118
120
|
|
|
@@ -135,15 +137,9 @@ function formatTimestamp(date) {
|
|
|
135
137
|
return yyyy + MM + dd + '-' + HH + mm + ss;
|
|
136
138
|
}
|
|
137
139
|
|
|
138
|
-
/** 输出 additionalContext 到 stdout(供
|
|
140
|
+
/** 输出 additionalContext 到 stdout(供 AI 平台读取,使用平台统一协议) */
|
|
139
141
|
function outputContext(eventName, contextText) {
|
|
140
|
-
|
|
141
|
-
hookSpecificOutput: {
|
|
142
|
-
hookEventName: eventName,
|
|
143
|
-
additionalContext: contextText,
|
|
144
|
-
},
|
|
145
|
-
};
|
|
146
|
-
process.stdout.write(JSON.stringify(output));
|
|
142
|
+
platform.outputContext(eventName, contextText);
|
|
147
143
|
}
|
|
148
144
|
|
|
149
145
|
/** 计算两个 ISO 时间戳之间的分钟数 */
|
package/hooks/session-start.js
CHANGED
|
@@ -4,18 +4,19 @@
|
|
|
4
4
|
* SessionStart Hook — 注入 using-adspecs 技能引导内容
|
|
5
5
|
*
|
|
6
6
|
* 在每次会话启动时,读取 using-adspecs/SKILL.md 并输出为
|
|
7
|
-
*
|
|
7
|
+
* hook additionalContext,确保 Agent 在任何任务开始前
|
|
8
8
|
* 都知道需要检查并使用 ADSpecs 技能。
|
|
9
9
|
*
|
|
10
|
-
*
|
|
10
|
+
* 跨平台兼容:Claude Code / Qoder
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
const fs = require('fs');
|
|
14
14
|
const path = require('path');
|
|
15
|
+
const platform = require('./platform');
|
|
15
16
|
|
|
16
17
|
// ── 路径解析 ──────────────────────────────────────────
|
|
17
|
-
//
|
|
18
|
-
const PLUGIN_ROOT =
|
|
18
|
+
// 使用平台工具解析插件根目录(兼容环境变量和 __dirname 回退)
|
|
19
|
+
const PLUGIN_ROOT = platform.getPluginRoot(__dirname);
|
|
19
20
|
const SKILL_PATH = path.join(PLUGIN_ROOT, 'skills', 'using-adspecs', 'SKILL.md');
|
|
20
21
|
|
|
21
22
|
// ── 读取技能内容 ──────────────────────────────────────
|
|
@@ -32,13 +33,8 @@ if (!skillContent || skillContent.trim() === '') {
|
|
|
32
33
|
}
|
|
33
34
|
|
|
34
35
|
// ── 输出 Hook Context ─────────────────────────────────
|
|
35
|
-
//
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
additionalContext: '<EXTREMELY-IMPORTANT>\n' + skillContent + '\n</EXTREMELY-IMPORTANT>'
|
|
41
|
-
}
|
|
42
|
-
};
|
|
43
|
-
|
|
44
|
-
process.stdout.write(JSON.stringify(output));
|
|
36
|
+
// 使用平台统一输出协议(hookSpecificOutput.additionalContext)
|
|
37
|
+
platform.outputContext(
|
|
38
|
+
'SessionStart',
|
|
39
|
+
'<EXTREMELY-IMPORTANT>\n' + skillContent + '\n</EXTREMELY-IMPORTANT>'
|
|
40
|
+
);
|
package/hooks/wiki-queue.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
*
|
|
6
6
|
* 在 SessionStart / PostToolUse / Stop 三个 Hook 事件中管理 .wiki-update-queue 队列文件。
|
|
7
7
|
* 使用 Node.js(而非平台特定 Shell)确保 Windows / macOS / Linux 三平台兼容。
|
|
8
|
+
* 支持 Claude Code 和 Qoder 双平台。
|
|
8
9
|
*
|
|
9
10
|
* 用法:
|
|
10
11
|
* node hooks/wiki-queue.js init — SessionStart: 创建队列文件
|
|
@@ -14,13 +15,13 @@
|
|
|
14
15
|
|
|
15
16
|
const fs = require('fs');
|
|
16
17
|
const path = require('path');
|
|
18
|
+
const platform = require('./platform');
|
|
17
19
|
|
|
18
20
|
// ── 配置 ──────────────────────────────────────────────
|
|
19
|
-
//
|
|
20
|
-
// 使用 process.cwd()
|
|
21
|
-
|
|
22
|
-
const
|
|
23
|
-
const QUEUE_DIR = path.join(PROJECT_ROOT, '.claude');
|
|
21
|
+
// 队列文件放在目标项目根目录的平台状态目录下。
|
|
22
|
+
// 使用 process.cwd() 获取目标项目根目录(平台运行 hook 时 CWD 即为目标项目)。
|
|
23
|
+
const PROJECT_ROOT = platform.getProjectRoot();
|
|
24
|
+
const QUEUE_DIR = platform.getStateDir(PROJECT_ROOT);
|
|
24
25
|
const QUEUE_FILE = path.join(QUEUE_DIR, '.wiki-update-queue');
|
|
25
26
|
|
|
26
27
|
// ── 工具函数 ──────────────────────────────────────────
|
|
@@ -112,8 +113,8 @@ switch (action) {
|
|
|
112
113
|
break;
|
|
113
114
|
|
|
114
115
|
case 'enqueue':
|
|
115
|
-
//
|
|
116
|
-
appendToQueue(
|
|
116
|
+
// 工具输出文件由平台 Hook 运行时注入为环境变量(兼容 Claude Code / Qoder)
|
|
117
|
+
appendToQueue(platform.getToolOutputFiles());
|
|
117
118
|
break;
|
|
118
119
|
|
|
119
120
|
case 'report':
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "adspecs",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.29",
|
|
4
4
|
"description": "AI first 工程规范驱动开发插件 — 支持 npm 安装和 CLI 初始化",
|
|
5
5
|
"bin": {
|
|
6
6
|
"adspecs": "bin/adspecs.js"
|
|
@@ -42,6 +42,7 @@
|
|
|
42
42
|
"keywords": [
|
|
43
43
|
"adspecs",
|
|
44
44
|
"claude-code",
|
|
45
|
+
"qoder",
|
|
45
46
|
"plugin",
|
|
46
47
|
"scaffold",
|
|
47
48
|
"cli",
|
package/scripts/postinstall.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
'use strict';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
* npm 安装/更新后自动刷新
|
|
5
|
+
* npm 安装/更新后自动刷新 AI 平台插件注册
|
|
6
6
|
*
|
|
7
7
|
* 触发时机:
|
|
8
8
|
* - `npm install -g adspecs` → 首次安装
|
|
@@ -10,9 +10,9 @@
|
|
|
10
10
|
* - `npm install adspecs` (本地) → 作为项目依赖安装
|
|
11
11
|
*
|
|
12
12
|
* 行为:
|
|
13
|
-
* - 检查
|
|
14
|
-
* - 检查 adspecs
|
|
15
|
-
* - 若已注册 →
|
|
13
|
+
* - 检查 AI 平台 CLI 是否可用(Claude Code / Qoder)
|
|
14
|
+
* - 检查 adspecs 插件是否已在平台中注册
|
|
15
|
+
* - 若已注册 → 静默重新安装以刷新版本
|
|
16
16
|
* - 若未注册 → 不做任何事(首次安装应由用户主动 `adspecs plugin install`)
|
|
17
17
|
*
|
|
18
18
|
* 关键约束:
|
|
@@ -33,17 +33,22 @@ const timer = setTimeout(() => {
|
|
|
33
33
|
if (timer.unref) timer.unref();
|
|
34
34
|
|
|
35
35
|
try {
|
|
36
|
-
// 1.
|
|
36
|
+
// 1. 检测可用的 AI 平台
|
|
37
|
+
var hasClaudeCli = false;
|
|
37
38
|
try {
|
|
38
39
|
execSync('claude --version', { stdio: 'ignore', timeout: 1000 });
|
|
40
|
+
hasClaudeCli = true;
|
|
39
41
|
} catch {
|
|
40
|
-
// claude CLI 未安装
|
|
42
|
+
// claude CLI 未安装
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// 如果没有任何平台 CLI 可用,静默退出
|
|
46
|
+
// Qoder 作为 IDE 插件不需要 CLI 刷新(通过 .qoder-plugin/ 自动发现)
|
|
47
|
+
if (!hasClaudeCli) {
|
|
41
48
|
process.exit(0);
|
|
42
49
|
}
|
|
43
50
|
|
|
44
51
|
// 2. 判断是否是全局安装
|
|
45
|
-
// npm 在全局安装时会设置 npm_config_global 或 initInstalled 等环境变量
|
|
46
|
-
// 也可通过检查安装路径是否包含 node_modules/adspecs 判断
|
|
47
52
|
const installDir = path.resolve(__dirname, '..');
|
|
48
53
|
const isGlobal =
|
|
49
54
|
process.env.npm_config_global === 'true' ||
|
|
@@ -75,8 +80,6 @@ try {
|
|
|
75
80
|
}
|
|
76
81
|
|
|
77
82
|
// 4. 已注册 → 重新安装以刷新版本
|
|
78
|
-
// 使用异步 spawn 避免阻塞 npm install 流程
|
|
79
|
-
const pluginDir = installDir;
|
|
80
83
|
const child = spawn(
|
|
81
84
|
'claude',
|
|
82
85
|
['plugins', 'install', 'adspecs', '--scope', 'user'],
|
|
@@ -97,7 +100,7 @@ try {
|
|
|
97
100
|
fs.readFileSync(path.join(installDir, 'package.json'), 'utf8')
|
|
98
101
|
);
|
|
99
102
|
console.log(
|
|
100
|
-
`\n🔄 adspecs@${pkg.version} —
|
|
103
|
+
`\n🔄 adspecs@${pkg.version} — AI 平台插件注册已自动刷新\n`
|
|
101
104
|
);
|
|
102
105
|
}
|
|
103
106
|
} catch {
|
package/src/commands/plugin.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
const { execSync } = require('child_process');
|
|
4
4
|
const path = require('path');
|
|
5
5
|
const fs = require('fs');
|
|
6
|
+
const os = require('os');
|
|
6
7
|
const chalk = require('chalk');
|
|
7
8
|
|
|
8
9
|
/**
|
|
@@ -25,42 +26,326 @@ function checkClaudeCli() {
|
|
|
25
26
|
}
|
|
26
27
|
|
|
27
28
|
/**
|
|
28
|
-
* 读取 plugin.json
|
|
29
|
+
* 读取 plugin.json 获取插件名(根据平台选择对应清单)
|
|
30
|
+
* @param {string} platform - 'claude' | 'qoder'
|
|
29
31
|
*/
|
|
30
|
-
function getPluginName() {
|
|
31
|
-
|
|
32
|
+
function getPluginName(platform) {
|
|
33
|
+
var manifestDir = platform === 'qoder' ? '.qoder-plugin' : '.claude-plugin';
|
|
34
|
+
const pluginJsonPath = path.join(PLUGIN_DIR, manifestDir, 'plugin.json');
|
|
32
35
|
if (!fs.existsSync(pluginJsonPath)) {
|
|
33
|
-
|
|
36
|
+
// 回退到 .claude-plugin
|
|
37
|
+
const fallbackPath = path.join(PLUGIN_DIR, '.claude-plugin', 'plugin.json');
|
|
38
|
+
if (!fs.existsSync(fallbackPath)) {
|
|
39
|
+
throw new Error(`未找到插件清单文件: ${pluginJsonPath}`);
|
|
40
|
+
}
|
|
41
|
+
const pluginJson = JSON.parse(fs.readFileSync(fallbackPath, 'utf8'));
|
|
42
|
+
return pluginJson.name || 'adspecs';
|
|
34
43
|
}
|
|
35
44
|
const pluginJson = JSON.parse(fs.readFileSync(pluginJsonPath, 'utf8'));
|
|
36
45
|
return pluginJson.name || 'adspecs';
|
|
37
46
|
}
|
|
38
47
|
|
|
48
|
+
/**
|
|
49
|
+
* 检测当前可用的 AI 平台
|
|
50
|
+
* @returns {'claude'|'qoder'}
|
|
51
|
+
*/
|
|
52
|
+
function detectInstalledPlatform() {
|
|
53
|
+
// 显式环境变量优先
|
|
54
|
+
var explicit = (process.env.ADSPECS_PLATFORM || '').toLowerCase();
|
|
55
|
+
if (explicit === 'qoder' || explicit === 'claude') {
|
|
56
|
+
return explicit;
|
|
57
|
+
}
|
|
58
|
+
// 检测 claude CLI
|
|
59
|
+
if (checkClaudeCli()) {
|
|
60
|
+
return 'claude';
|
|
61
|
+
}
|
|
62
|
+
// 默认回退 qoder(当前运行环境)
|
|
63
|
+
return 'qoder';
|
|
64
|
+
}
|
|
65
|
+
|
|
39
66
|
/**
|
|
40
67
|
* 安装(注册)插件到 Claude Code
|
|
41
68
|
*
|
|
42
|
-
* Claude Code 的 marketplace 命令期望路径指向包含 `.claude-plugin/marketplace.json`
|
|
43
|
-
* 的目录,因此传入 PLUGIN_DIR(而非 PLUGIN_DIR/.claude-plugin)。
|
|
44
|
-
*
|
|
45
69
|
* @param {object} options
|
|
46
70
|
* @param {string} options.scope - 安装范围: user|project
|
|
71
|
+
* @param {string} options.platform - 目标平台: claude|qoder(可选,自动检测)
|
|
47
72
|
*/
|
|
48
73
|
exports.install = async function install(options) {
|
|
49
74
|
const scope = options.scope || 'user';
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
// marketplace.json 必须存在,否则无法注册
|
|
53
|
-
const marketplaceJson = path.join(PLUGIN_DIR, '.claude-plugin', 'marketplace.json');
|
|
54
|
-
if (!fs.existsSync(marketplaceJson)) {
|
|
55
|
-
throw new Error(`未找到 marketplace 清单: ${marketplaceJson}`);
|
|
56
|
-
}
|
|
75
|
+
const platform = options.platform || detectInstalledPlatform();
|
|
76
|
+
const pluginName = getPluginName(platform);
|
|
57
77
|
|
|
58
78
|
console.log(chalk.cyan('🔌 adspecs 插件注册'));
|
|
59
79
|
console.log('');
|
|
60
80
|
console.log(` 插件目录: ${PLUGIN_DIR}`);
|
|
61
81
|
console.log(` 插件名称: ${pluginName}`);
|
|
62
82
|
console.log(` 安装范围: ${scope}`);
|
|
83
|
+
console.log(` 目标平台: ${platform}`);
|
|
84
|
+
console.log('');
|
|
85
|
+
|
|
86
|
+
if (platform === 'qoder') {
|
|
87
|
+
await installForQoder(pluginName, scope);
|
|
88
|
+
} else {
|
|
89
|
+
await installForClaude(pluginName, scope);
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Qoder 平台安装
|
|
95
|
+
* 自动注册到 Qoder 插件系统:
|
|
96
|
+
* - 用户级:创建 junction 链接 + 写入注册表 + 启用插件
|
|
97
|
+
* - 项目级:在项目目录创建 .qoder-plugin 链接
|
|
98
|
+
*/
|
|
99
|
+
async function installForQoder(pluginName, scope) {
|
|
100
|
+
const qoderManifest = path.join(PLUGIN_DIR, '.qoder-plugin', 'plugin.json');
|
|
101
|
+
if (!fs.existsSync(qoderManifest)) {
|
|
102
|
+
throw new Error(`未找到 Qoder 插件清单: ${qoderManifest}`);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (scope === 'project') {
|
|
106
|
+
await installQoderProject(pluginName, qoderManifest);
|
|
107
|
+
} else {
|
|
108
|
+
await installQoderUser(pluginName, qoderManifest);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
console.log('');
|
|
112
|
+
console.log(chalk.green.bold(`✅ adspecs 插件已配置到 Qoder (scope=${scope})`));
|
|
113
|
+
console.log('');
|
|
114
|
+
printSkillList();
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Qoder 用户级安装 — 自动注册到 ~/.qoder/plugins/
|
|
119
|
+
*/
|
|
120
|
+
async function installQoderUser(pluginName, qoderManifest) {
|
|
121
|
+
const qoderHome = path.join(os.homedir(), '.qoder');
|
|
122
|
+
const pluginsDir = path.join(qoderHome, 'plugins');
|
|
123
|
+
const cacheDir = path.join(pluginsDir, 'cache', 'local');
|
|
124
|
+
const linkPath = path.join(cacheDir, pluginName);
|
|
125
|
+
const registryKey = `${pluginName}@local`;
|
|
126
|
+
|
|
127
|
+
console.log(chalk.gray(' → Qoder 用户级插件自动注册...'));
|
|
128
|
+
console.log('');
|
|
129
|
+
|
|
130
|
+
// 1. 确保目录结构存在
|
|
131
|
+
fs.mkdirSync(cacheDir, { recursive: true });
|
|
132
|
+
|
|
133
|
+
// 2. 创建 junction 链接(已存在则先移除重建,确保指向最新路径)
|
|
134
|
+
if (fs.existsSync(linkPath)) {
|
|
135
|
+
try {
|
|
136
|
+
fs.rmSync(linkPath, { recursive: true, force: true });
|
|
137
|
+
} catch (_err) {
|
|
138
|
+
// junction 可能无法用 rmSync 删除,尝试 unlink
|
|
139
|
+
try { fs.unlinkSync(linkPath); } catch (_e) { /* ignore */ }
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
try {
|
|
144
|
+
fs.symlinkSync(PLUGIN_DIR, linkPath, 'junction');
|
|
145
|
+
console.log(chalk.green(` ✅ 已创建插件链接: ${linkPath}`));
|
|
146
|
+
console.log(chalk.gray(` → ${PLUGIN_DIR}`));
|
|
147
|
+
} catch (err) {
|
|
148
|
+
// junction 创建失败(权限不足等),回退为复制关键文件
|
|
149
|
+
console.log(chalk.yellow(` ⚠ 链接创建失败(${err.message}),回退为文件复制...`));
|
|
150
|
+
fs.mkdirSync(linkPath, { recursive: true });
|
|
151
|
+
copyDirRecursive(PLUGIN_DIR, linkPath, [
|
|
152
|
+
'node_modules', '.git', '.claude', 'assets'
|
|
153
|
+
]);
|
|
154
|
+
console.log(chalk.green(` ✅ 已复制插件文件到: ${linkPath}`));
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// 3. 读取插件版本
|
|
158
|
+
const pluginJson = JSON.parse(fs.readFileSync(qoderManifest, 'utf8'));
|
|
159
|
+
const version = pluginJson.version || '0.0.0';
|
|
160
|
+
const now = new Date().toISOString();
|
|
161
|
+
|
|
162
|
+
// 4. 写入 installed_plugins_v2.json
|
|
163
|
+
const v2Path = path.join(pluginsDir, 'installed_plugins_v2.json');
|
|
164
|
+
let v2Data = { version: 2, plugins: {} };
|
|
165
|
+
if (fs.existsSync(v2Path)) {
|
|
166
|
+
try { v2Data = JSON.parse(fs.readFileSync(v2Path, 'utf8')); } catch (_e) { /* use default */ }
|
|
167
|
+
}
|
|
168
|
+
if (!v2Data.plugins) v2Data.plugins = {};
|
|
169
|
+
|
|
170
|
+
v2Data.plugins[registryKey] = [{
|
|
171
|
+
scope: 'user',
|
|
172
|
+
installPath: linkPath,
|
|
173
|
+
version: version,
|
|
174
|
+
source: 'local',
|
|
175
|
+
installedAt: (v2Data.plugins[registryKey] && v2Data.plugins[registryKey][0] && v2Data.plugins[registryKey][0].installedAt) || now,
|
|
176
|
+
lastUpdated: now,
|
|
177
|
+
enabled: true
|
|
178
|
+
}];
|
|
179
|
+
|
|
180
|
+
fs.writeFileSync(v2Path, JSON.stringify(v2Data, null, 2) + '\n', 'utf8');
|
|
181
|
+
console.log(chalk.green(' ✅ 已写入插件注册表 (installed_plugins_v2.json)'));
|
|
182
|
+
|
|
183
|
+
// 5. 写入 installed_plugins.json(v1 兼容)
|
|
184
|
+
const v1Path = path.join(pluginsDir, 'installed_plugins.json');
|
|
185
|
+
let v1Data = { plugins: {} };
|
|
186
|
+
if (fs.existsSync(v1Path)) {
|
|
187
|
+
try { v1Data = JSON.parse(fs.readFileSync(v1Path, 'utf8')); } catch (_e) { /* use default */ }
|
|
188
|
+
}
|
|
189
|
+
if (!v1Data.plugins) v1Data.plugins = {};
|
|
190
|
+
|
|
191
|
+
v1Data.plugins[registryKey] = { installPath: linkPath };
|
|
192
|
+
|
|
193
|
+
fs.writeFileSync(v1Path, JSON.stringify(v1Data, null, 2) + '\n', 'utf8');
|
|
194
|
+
console.log(chalk.green(' ✅ 已写入插件注册表 (installed_plugins.json)'));
|
|
195
|
+
|
|
196
|
+
// 6. 在 settings.json 中启用插件
|
|
197
|
+
const settingsPath = path.join(qoderHome, 'settings.json');
|
|
198
|
+
let settings = {};
|
|
199
|
+
if (fs.existsSync(settingsPath)) {
|
|
200
|
+
try { settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8')); } catch (_e) { /* use default */ }
|
|
201
|
+
}
|
|
202
|
+
if (!settings.enabledPlugins) settings.enabledPlugins = {};
|
|
203
|
+
|
|
204
|
+
settings.enabledPlugins[registryKey] = true;
|
|
205
|
+
|
|
206
|
+
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n', 'utf8');
|
|
207
|
+
console.log(chalk.green(' ✅ 已在 settings.json 中启用插件'));
|
|
208
|
+
|
|
63
209
|
console.log('');
|
|
210
|
+
console.log(chalk.gray(' 请重启 Qoder 使插件生效。'));
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Qoder 项目级安装
|
|
215
|
+
*/
|
|
216
|
+
async function installQoderProject(pluginName, qoderManifest) {
|
|
217
|
+
// 项目级:在当前项目创建 .qoder-plugin 符号链接或复制
|
|
218
|
+
const targetDir = path.join(process.cwd(), '.qoder-plugin');
|
|
219
|
+
if (fs.existsSync(targetDir)) {
|
|
220
|
+
console.log(chalk.yellow(' ⏭ 当前项目已存在 .qoder-plugin/,跳过'));
|
|
221
|
+
} else {
|
|
222
|
+
try {
|
|
223
|
+
fs.symlinkSync(
|
|
224
|
+
path.join(PLUGIN_DIR, '.qoder-plugin'),
|
|
225
|
+
targetDir,
|
|
226
|
+
'junction'
|
|
227
|
+
);
|
|
228
|
+
console.log(chalk.green(' ✅ 已创建 .qoder-plugin 符号链接'));
|
|
229
|
+
} catch (_err) {
|
|
230
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
231
|
+
fs.copyFileSync(qoderManifest, path.join(targetDir, 'plugin.json'));
|
|
232
|
+
console.log(chalk.green(' ✅ 已复制 .qoder-plugin/plugin.json 到项目'));
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// 确保 skills 可被访问
|
|
237
|
+
const targetSkills = path.join(process.cwd(), 'skills');
|
|
238
|
+
if (!fs.existsSync(targetSkills)) {
|
|
239
|
+
try {
|
|
240
|
+
fs.symlinkSync(path.join(PLUGIN_DIR, 'skills'), targetSkills, 'junction');
|
|
241
|
+
console.log(chalk.green(' ✅ 已创建 skills/ 符号链接'));
|
|
242
|
+
} catch (_err) {
|
|
243
|
+
console.log(chalk.yellow(' ⚠ skills/ 链接创建失败,请手动确保 Qoder 可访问插件 skills 目录'));
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// 确保 hooks 可被访问
|
|
248
|
+
const targetHooks = path.join(process.cwd(), 'hooks');
|
|
249
|
+
if (!fs.existsSync(targetHooks)) {
|
|
250
|
+
try {
|
|
251
|
+
fs.symlinkSync(path.join(PLUGIN_DIR, 'hooks'), targetHooks, 'junction');
|
|
252
|
+
console.log(chalk.green(' ✅ 已创建 hooks/ 符号链接'));
|
|
253
|
+
} catch (_err) {
|
|
254
|
+
console.log(chalk.yellow(' ⚠ hooks/ 链接创建失败,请手动确保 Qoder 可访问插件 hooks 目录'));
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* 递归复制目录(排除指定目录名)
|
|
261
|
+
*/
|
|
262
|
+
function copyDirRecursive(src, dest, excludeDirs) {
|
|
263
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
264
|
+
const entries = fs.readdirSync(src, { withFileTypes: true });
|
|
265
|
+
for (const entry of entries) {
|
|
266
|
+
if (excludeDirs && excludeDirs.includes(entry.name)) continue;
|
|
267
|
+
const srcPath = path.join(src, entry.name);
|
|
268
|
+
const destPath = path.join(dest, entry.name);
|
|
269
|
+
if (entry.isDirectory()) {
|
|
270
|
+
copyDirRecursive(srcPath, destPath, excludeDirs);
|
|
271
|
+
} else {
|
|
272
|
+
fs.copyFileSync(srcPath, destPath);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Qoder 用户级卸载 — 移除链接 + 注册表条目 + 启用配置
|
|
279
|
+
*/
|
|
280
|
+
function uninstallQoderUser(pluginName) {
|
|
281
|
+
const qoderHome = path.join(os.homedir(), '.qoder');
|
|
282
|
+
const pluginsDir = path.join(qoderHome, 'plugins');
|
|
283
|
+
const linkPath = path.join(pluginsDir, 'cache', 'local', pluginName);
|
|
284
|
+
const registryKey = `${pluginName}@local`;
|
|
285
|
+
|
|
286
|
+
// 1. 移除 junction 链接 / 复制的目录
|
|
287
|
+
if (fs.existsSync(linkPath)) {
|
|
288
|
+
try {
|
|
289
|
+
fs.rmSync(linkPath, { recursive: true, force: true });
|
|
290
|
+
console.log(chalk.green(` ✅ 已移除插件目录: ${linkPath}`));
|
|
291
|
+
} catch (err) {
|
|
292
|
+
console.log(chalk.yellow(` ⚠ 移除插件目录失败: ${err.message}`));
|
|
293
|
+
}
|
|
294
|
+
} else {
|
|
295
|
+
console.log(chalk.yellow(' ⏭ 插件目录不存在,跳过'));
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// 2. 从 installed_plugins_v2.json 移除
|
|
299
|
+
const v2Path = path.join(pluginsDir, 'installed_plugins_v2.json');
|
|
300
|
+
if (fs.existsSync(v2Path)) {
|
|
301
|
+
try {
|
|
302
|
+
const v2Data = JSON.parse(fs.readFileSync(v2Path, 'utf8'));
|
|
303
|
+
if (v2Data.plugins && v2Data.plugins[registryKey]) {
|
|
304
|
+
delete v2Data.plugins[registryKey];
|
|
305
|
+
fs.writeFileSync(v2Path, JSON.stringify(v2Data, null, 2) + '\n', 'utf8');
|
|
306
|
+
console.log(chalk.green(' ✅ 已从注册表移除 (installed_plugins_v2.json)'));
|
|
307
|
+
}
|
|
308
|
+
} catch (_err) { /* ignore */ }
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// 3. 从 installed_plugins.json 移除
|
|
312
|
+
const v1Path = path.join(pluginsDir, 'installed_plugins.json');
|
|
313
|
+
if (fs.existsSync(v1Path)) {
|
|
314
|
+
try {
|
|
315
|
+
const v1Data = JSON.parse(fs.readFileSync(v1Path, 'utf8'));
|
|
316
|
+
if (v1Data.plugins && v1Data.plugins[registryKey]) {
|
|
317
|
+
delete v1Data.plugins[registryKey];
|
|
318
|
+
fs.writeFileSync(v1Path, JSON.stringify(v1Data, null, 2) + '\n', 'utf8');
|
|
319
|
+
console.log(chalk.green(' ✅ 已从注册表移除 (installed_plugins.json)'));
|
|
320
|
+
}
|
|
321
|
+
} catch (_err) { /* ignore */ }
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// 4. 从 settings.json 禁用
|
|
325
|
+
const settingsPath = path.join(qoderHome, 'settings.json');
|
|
326
|
+
if (fs.existsSync(settingsPath)) {
|
|
327
|
+
try {
|
|
328
|
+
const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
|
|
329
|
+
if (settings.enabledPlugins && settings.enabledPlugins[registryKey] !== undefined) {
|
|
330
|
+
delete settings.enabledPlugins[registryKey];
|
|
331
|
+
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n', 'utf8');
|
|
332
|
+
console.log(chalk.green(' ✅ 已从 settings.json 移除启用配置'));
|
|
333
|
+
}
|
|
334
|
+
} catch (_err) { /* ignore */ }
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
console.log(chalk.gray(' 请重启 Qoder 使变更生效。'));
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Claude Code 平台安装(原有逻辑)
|
|
342
|
+
*/
|
|
343
|
+
async function installForClaude(pluginName, scope) {
|
|
344
|
+
// marketplace.json 必须存在,否则无法注册
|
|
345
|
+
const marketplaceJson = path.join(PLUGIN_DIR, '.claude-plugin', 'marketplace.json');
|
|
346
|
+
if (!fs.existsSync(marketplaceJson)) {
|
|
347
|
+
throw new Error(`未找到 marketplace 清单: ${marketplaceJson}`);
|
|
348
|
+
}
|
|
64
349
|
|
|
65
350
|
// 1. 检查 claude CLI
|
|
66
351
|
if (!checkClaudeCli()) {
|
|
@@ -74,7 +359,6 @@ exports.install = async function install(options) {
|
|
|
74
359
|
}
|
|
75
360
|
|
|
76
361
|
// 2. 注册 marketplace
|
|
77
|
-
// 注意:传入 PLUGIN_DIR,claude 会去找 <PLUGIN_DIR>/.claude-plugin/marketplace.json
|
|
78
362
|
console.log(chalk.gray(' → 注册 marketplace...'));
|
|
79
363
|
let marketplaceRegistered = false;
|
|
80
364
|
try {
|
|
@@ -90,15 +374,13 @@ exports.install = async function install(options) {
|
|
|
90
374
|
const combined = stderr + stdout;
|
|
91
375
|
if (combined.includes('already exists') || combined.includes('already registered')) {
|
|
92
376
|
console.log(chalk.yellow(' ⏭ marketplace 已存在,跳过'));
|
|
93
|
-
marketplaceRegistered = true;
|
|
377
|
+
marketplaceRegistered = true;
|
|
94
378
|
} else {
|
|
95
|
-
// 把真实错误输出,方便排查,不再静默吞掉
|
|
96
379
|
console.log(chalk.red(` ❌ marketplace 注册失败: ${combined.trim() || err.message}`));
|
|
97
380
|
}
|
|
98
381
|
}
|
|
99
382
|
|
|
100
383
|
// 3. 安装插件
|
|
101
|
-
// 只有 marketplace 注册成功(或已存在)时,才能按名称安装
|
|
102
384
|
console.log(chalk.gray(' → 安装插件...'));
|
|
103
385
|
if (!marketplaceRegistered) {
|
|
104
386
|
throw new Error('marketplace 未注册成功,无法按名称安装插件,请检查上方错误');
|
|
@@ -122,28 +404,63 @@ exports.install = async function install(options) {
|
|
|
122
404
|
console.log('');
|
|
123
405
|
console.log(chalk.green.bold(`✅ adspecs 插件已注册到 Claude Code (scope=${scope})`));
|
|
124
406
|
console.log('');
|
|
125
|
-
|
|
407
|
+
printSkillList();
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* 打印可用技能列表
|
|
412
|
+
*/
|
|
413
|
+
function printSkillList() {
|
|
414
|
+
console.log(' 现在可以使用以下技能:');
|
|
126
415
|
console.log(' /adspecs-prd — 生成 PRD');
|
|
127
416
|
console.log(' /adspecs-plan — 后端系统设计');
|
|
128
417
|
console.log(' /adspecs-tasks — TDD 任务清单');
|
|
129
418
|
console.log(' /adspecs-utest — 生成单元测试');
|
|
130
|
-
console.log(' ...(共
|
|
419
|
+
console.log(' ...(共 30 个技能)');
|
|
131
420
|
console.log('');
|
|
132
|
-
}
|
|
421
|
+
}
|
|
133
422
|
|
|
134
423
|
/**
|
|
135
424
|
* 卸载插件
|
|
136
425
|
*
|
|
137
426
|
* @param {object} options
|
|
138
427
|
* @param {string} options.scope - 卸载范围: user|project
|
|
428
|
+
* @param {string} options.platform - 目标平台: claude|qoder(可选,自动检测)
|
|
139
429
|
*/
|
|
140
430
|
exports.uninstall = async function uninstall(options) {
|
|
141
431
|
const scope = options.scope || 'user';
|
|
142
|
-
const
|
|
432
|
+
const platform = options.platform || detectInstalledPlatform();
|
|
433
|
+
const pluginName = getPluginName(platform);
|
|
143
434
|
|
|
144
435
|
console.log(chalk.cyan('🔌 adspecs 插件卸载'));
|
|
145
436
|
console.log('');
|
|
437
|
+
console.log(` 目标平台: ${platform}`);
|
|
438
|
+
console.log('');
|
|
439
|
+
|
|
440
|
+
if (platform === 'qoder') {
|
|
441
|
+
if (scope === 'project') {
|
|
442
|
+
// Qoder 项目级卸载:移除项目链接
|
|
443
|
+
const targetDir = path.join(process.cwd(), '.qoder-plugin');
|
|
444
|
+
if (fs.existsSync(targetDir)) {
|
|
445
|
+
try {
|
|
446
|
+
fs.rmSync(targetDir, { recursive: true, force: true });
|
|
447
|
+
console.log(chalk.green(' ✅ 已移除项目 .qoder-plugin/'));
|
|
448
|
+
} catch (err) {
|
|
449
|
+
throw new Error(`移除失败: ${err.message}`);
|
|
450
|
+
}
|
|
451
|
+
} else {
|
|
452
|
+
console.log(chalk.yellow(' ⏭ 项目未安装 .qoder-plugin/,无需卸载'));
|
|
453
|
+
}
|
|
454
|
+
} else {
|
|
455
|
+
// Qoder 用户级卸载:移除链接 + 注册表 + 启用配置
|
|
456
|
+
uninstallQoderUser(pluginName);
|
|
457
|
+
}
|
|
458
|
+
console.log('');
|
|
459
|
+
console.log(chalk.green(`✅ ${pluginName} 已从 Qoder 卸载 (scope=${scope})`));
|
|
460
|
+
return;
|
|
461
|
+
}
|
|
146
462
|
|
|
463
|
+
// Claude Code 卸载
|
|
147
464
|
if (!checkClaudeCli()) {
|
|
148
465
|
console.error(chalk.red('❌ 未检测到 claude CLI'));
|
|
149
466
|
process.exit(1);
|