@boyingliu01/xp-gate 0.8.2 → 0.8.5

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/lib/uninstall.js CHANGED
@@ -260,6 +260,68 @@ function buildPlan(mode) {
260
260
  return plan;
261
261
  }
262
262
 
263
+ /**
264
+ * Remove a single file item with ownership verification.
265
+ * @returns {boolean} true on success, false on skip (not xp-gate owned)
266
+ */
267
+ function removeFile(item, config) {
268
+ const manifestEntry = config.manifest && config.manifest.files
269
+ ? config.manifest.files[item.manifestKey]
270
+ : null;
271
+
272
+ if (!verifyFileOwnership(item.path, manifestEntry, item.signature)) {
273
+ if (fs.existsSync(item.path)) {
274
+ console.warn(` Warning: ${item.label} does not contain xp-gate signature — skipping`);
275
+ }
276
+ return false;
277
+ }
278
+
279
+ fs.unlinkSync(item.path);
280
+ console.log(` Removed ${item.label}`);
281
+ return true;
282
+ }
283
+
284
+ /**
285
+ * Remove a directory item.
286
+ * @returns {boolean} true if dir existed and was removed, false otherwise
287
+ */
288
+ function removeDir(item) {
289
+ if (!fs.existsSync(item.path)) return false;
290
+ fs.rmSync(item.path, { recursive: true, force: true });
291
+ console.log(` Removed ${item.label}`);
292
+ return true;
293
+ }
294
+
295
+ /**
296
+ * Unset core.hooksPath if it matches the expected xp-gate path.
297
+ * @returns {boolean} true if unset was performed or already unset, false on mismatch
298
+ */
299
+ function unsetGitConfigIfMatch(item) {
300
+ const currentPath = getCurrentHooksPath();
301
+ if (currentPath === null || currentPath === '') {
302
+ console.log(` ${item.label} — not set`);
303
+ return true;
304
+ }
305
+ if (currentPath === item.expectedPath) {
306
+ unsetHooksPath();
307
+ console.log(` Unset ${item.label}`);
308
+ return true;
309
+ }
310
+ console.warn(` Warning: core.hooksPath (${currentPath}) does not match xp-gate path — skipping unset`);
311
+ return false;
312
+ }
313
+
314
+ /**
315
+ * Execute a single plan item (file/dir/gitconfig).
316
+ * @returns {boolean} true on success, false on skip/failure
317
+ */
318
+ function executePlanItem(item, config) {
319
+ if (item.type === 'file') return removeFile(item, config);
320
+ if (item.type === 'dir') return removeDir(item);
321
+ if (item.type === 'gitconfig' && item.action === 'unset-hooks-path') return unsetGitConfigIfMatch(item);
322
+ return false;
323
+ }
324
+
263
325
  /**
264
326
  * @param {string[]} args CLI arguments
265
327
  * @returns {number} exit code (0 = success, 1 = error)
@@ -337,37 +399,7 @@ async function uninstall(args) {
337
399
 
338
400
  for (const item of plan) {
339
401
  try {
340
- if (item.type === 'file') {
341
- const manifestEntry = config.manifest && config.manifest.files
342
- ? config.manifest.files[item.manifestKey]
343
- : null;
344
-
345
- if (!verifyFileOwnership(item.path, manifestEntry, item.signature)) {
346
- if (fs.existsSync(item.path)) {
347
- console.warn(` Warning: ${item.label} does not contain xp-gate signature — skipping`);
348
- }
349
- continue;
350
- }
351
-
352
- fs.unlinkSync(item.path);
353
- console.log(` Removed ${item.label}`);
354
- } else if (item.type === 'dir') {
355
- if (fs.existsSync(item.path)) {
356
- fs.rmSync(item.path, { recursive: true, force: true });
357
- console.log(` Removed ${item.label}`);
358
- }
359
- } else if (item.type === 'gitconfig' && item.action === 'unset-hooks-path') {
360
- const currentPath = getCurrentHooksPath();
361
- if (currentPath === null || currentPath === '') {
362
- // Already unset
363
- console.log(` ${item.label} — not set`);
364
- } else if (currentPath === item.expectedPath) {
365
- unsetHooksPath();
366
- console.log(` Unset ${item.label}`);
367
- } else {
368
- console.warn(` Warning: core.hooksPath (${currentPath}) does not match xp-gate path — skipping unset`);
369
- }
370
- }
402
+ hadErrors = !executePlanItem(item, config) || hadErrors;
371
403
  } catch (e) {
372
404
  console.warn(` Warning: Could not remove ${item.label}: ${e.message}`);
373
405
  hadErrors = true;
@@ -8,57 +8,55 @@ const HOME = process.env.HOME || process.env.USERPROFILE || os.homedir();
8
8
  const CONFIG_DIR = path.join(HOME, '.config', 'xp-gate');
9
9
  const SKILLS_DIR = path.join(HOME, '.config', 'opencode', 'skills');
10
10
 
11
- async function updateSkill(name, options = {}) {
12
- const { all = false, check = false, verbose = false } = options;
13
-
14
- if (check) {
15
- console.log('Checking for updates...');
16
- const config = getConfig();
17
- const skills = config.installedSkills || {};
18
-
19
- for (const [skillName, info] of Object.entries(skills)) {
20
- console.log(` ${skillName}: ${info.version || 'unknown'}`);
21
- }
22
- console.log('Update check complete');
23
- return 0;
11
+ function handleCheckMode() {
12
+ console.log('Checking for updates...');
13
+ const config = getConfig();
14
+ const skills = config.installedSkills || {};
15
+ for (const [skillName, info] of Object.entries(skills)) {
16
+ console.log(` ${skillName}: ${info.version || 'unknown'}`);
24
17
  }
25
-
26
- if (all) {
27
- const config = getConfig();
28
- const skills = config.installedSkills || {};
29
-
30
- console.log('Updating all skills...');
31
- let hasErrors = false;
32
-
33
- for (const skillName of Object.keys(skills)) {
34
- try {
35
- await updateSingleSkill(skillName, verbose);
36
- } catch (err) {
37
- console.error(`Failed to update ${skillName}: ${err.message}`);
38
- hasErrors = true;
39
- }
18
+ console.log('Update check complete');
19
+ return 0;
20
+ }
21
+
22
+ async function handleAllMode(verbose) {
23
+ const config = getConfig();
24
+ const skills = config.installedSkills || {};
25
+ console.log('Updating all skills...');
26
+ let hasErrors = false;
27
+ for (const skillName of Object.keys(skills)) {
28
+ try {
29
+ await updateSingleSkill(skillName, verbose);
30
+ } catch (err) {
31
+ console.error(`Failed to update ${skillName}: ${err.message}`);
32
+ hasErrors = true;
40
33
  }
41
-
42
- return hasErrors ? 1 : 0;
43
34
  }
44
-
35
+ return hasErrors ? 1 : 0;
36
+ }
37
+
38
+ function handleSingleMode(name, verbose) {
45
39
  if (!name) {
46
40
  console.error('Error: Skill name required');
47
41
  console.error('Usage: xp-gate update-skill <name> or --all');
48
42
  return 1;
49
43
  }
50
-
51
44
  const config = getConfig();
52
45
  const skills = config.installedSkills || {};
53
-
54
46
  if (!skills[name]) {
55
47
  console.error(`Error: ${name} is not installed`);
56
48
  return 1;
57
49
  }
58
-
59
50
  return updateSingleSkill(name, verbose);
60
51
  }
61
52
 
53
+ async function updateSkill(name, options = {}) {
54
+ const { all = false, check = false, verbose = false } = options;
55
+ if (check) return handleCheckMode();
56
+ if (all) return handleAllMode(verbose);
57
+ return handleSingleMode(name, verbose);
58
+ }
59
+
62
60
  async function updateSingleSkill(name, verbose) {
63
61
  console.log(`Updating ${name}...`);
64
62
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@boyingliu01/xp-gate",
3
- "version": "0.8.2",
3
+ "version": "0.8.5",
4
4
  "description": "AI-driven development workflow: 6 quality gates + Delphi review + Sprint Flow",
5
5
  "bin": {
6
6
  "xp-gate": "./bin/xp-gate.js"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "xp-gate",
3
- "version": "0.8.2",
3
+ "version": "0.8.5",
4
4
  "displayName": "XP-Gate",
5
5
  "description": "Extreme Programming quality gates + AI workflow skills for Claude Code. Includes 6 quality gates, Sprint Flow, and Delphi multi-expert review.",
6
6
  "author": {
@@ -12,6 +12,7 @@ description: >
12
12
  - "start sprint"
13
13
  - "一键开发"
14
14
  - "/sprint-flow"
15
+ 触发后第一行输出: `Sprint Flow: ISOLATE → AUTO-ESTIMATE → THINK → PLAN → BUILD → REVIEW → USER ACCEPTANCE → FEEDBACK → SHIP → LAND → CLEANUP`
15
16
  用法: /sprint-flow "[需求描述]"
16
17
  示例: /sprint-flow "开发访谈机器人,支持多轮对话"
17
18
  可选参数:
@@ -236,6 +237,36 @@ ISOLATE → AUTO-ESTIMATE → THINK → PLAN → [GITHOOKS-GATE] → BUILD → R
236
237
 
237
238
  ## 各 Phase 调用的 Skills
238
239
 
240
+ ### ⚠️ 强制输出格式规范(Mandatory Output Format)
241
+
242
+ **执行每个 Phase 时,必须以以下固定格式输出阶段标题**,不可省略、不可合并、不可替换:
243
+
244
+ ```markdown
245
+ ## Phase -1: ISOLATE (隔离)
246
+ ## Phase -0.5: AUTO-ESTIMATE (规模评估)
247
+ ## Phase 0: THINK (思考)
248
+ ## Phase 1: PLAN (规划)
249
+ ## Phase 2: BUILD (构建)
250
+ ## Phase 3: REVIEW (评审)
251
+ ## Phase 4: USER ACCEPTANCE (用户验收)
252
+ ## Phase 5: FEEDBACK (反馈)
253
+ ## Phase 6: SHIP (发布)
254
+ ## Phase 7: LAND (部署)
255
+ ## Phase 8: CLEANUP (清理)
256
+ ```
257
+
258
+ **规则**:
259
+ 1. 每个 Phase **开始执行时必须首先输出**对应的 `## Phase X: NAME` 标题行(作为该 Phase 输出的第一行)
260
+ 2. **禁止省略** "Phase" 关键词(如不能只写 "ISOLATE" 或 "## -1")
261
+ 3. **禁止合并**多个 Phase 的输出(每个 Phase 必须有独立标题)
262
+ 4. **格式必须精确匹配**:`## Phase ` + 数字 + `: ` + 大写英文名 + ` (中文名)`
263
+ 5. 跳过某个 Phase(如 `--resume-from build` 跳过了 -1, -0.5, 0, 1)时,不输出被跳过 Phase 的标题
264
+ 6. 触发 `/sprint-flow` 后,**第一行输出应包含工作流阶段概览**:
265
+
266
+ ```
267
+ Sprint Flow: ISOLATE → AUTO-ESTIMATE → THINK → PLAN → BUILD → REVIEW → USER ACCEPTANCE → FEEDBACK → SHIP → LAND → CLEANUP
268
+ ```
269
+
239
270
  ### Phase -1: ISOLATE(git worktree 隔离)
240
271
 
241
272
  **执行时机**: `/sprint-flow` 启动后、Phase 0 THINK 之前。**自动执行**。
@@ -14,7 +14,7 @@ In your `opencode.json`:
14
14
 
15
15
  ```json
16
16
  {
17
- "plugin": ["@xp-gate/opencode-plugin"]
17
+ "plugin": ["@boyingliu01/opencode-plugin"]
18
18
  }
19
19
  ```
20
20
 
@@ -1,12 +1,26 @@
1
1
  {
2
- "name": "@xp-gate/opencode-plugin",
3
- "version": "0.8.2",
4
- "private": true,
2
+ "name": "@boyingliu01/opencode-plugin",
3
+ "version": "0.8.5",
5
4
  "type": "module",
6
5
  "main": "index.ts",
7
6
  "description": "XP-Gate quality gates + AI workflow skills for OpenCode",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/boyingliu01/xp-gate",
10
+ "directory": "plugins/opencode"
11
+ },
12
+ "publishConfig": {
13
+ "registry": "https://registry.npmjs.org",
14
+ "access": "public"
15
+ },
16
+ "files": [
17
+ "index.ts",
18
+ "skills/",
19
+ "tsconfig.json",
20
+ "README.md"
21
+ ],
8
22
  "scripts": {
9
- "build": "bun build index.ts --outdir dist --target bun",
23
+ "prepack": "node scripts/prepack.cjs",
10
24
  "check": "tsc --noEmit"
11
25
  },
12
26
  "dependencies": {
@@ -14,5 +28,15 @@
14
28
  },
15
29
  "devDependencies": {
16
30
  "typescript": "^5.4.0"
17
- }
31
+ },
32
+ "keywords": [
33
+ "opencode",
34
+ "plugin",
35
+ "xp-gate",
36
+ "quality-gates",
37
+ "code-review",
38
+ "sprint-flow"
39
+ ],
40
+ "author": "boyingliu01",
41
+ "license": "MIT"
18
42
  }
@@ -0,0 +1,85 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /**
5
+ * prepack.js — Bundle skills into @boyingliu01/opencode-plugin before npm publish.
6
+ *
7
+ * Skills live in repo-root `skills/` and are gitignored in `plugins/opencode/skills/`.
8
+ * This script copies them into the plugin package so the published tarball is self-contained.
9
+ */
10
+
11
+ const fs = require('fs');
12
+ const path = require('path');
13
+
14
+ const PLUGIN_ROOT = path.resolve(__dirname, '..');
15
+ const REPO_ROOT = path.resolve(PLUGIN_ROOT, '..', '..');
16
+
17
+ const CORE_SKILLS = [
18
+ 'admin-template-guidelines',
19
+ 'delphi-review',
20
+ 'improve-codebase-architecture',
21
+ 'ralph-loop',
22
+ 'sprint-flow',
23
+ 'test-driven-development',
24
+ 'test-specification-alignment',
25
+ 'to-issues',
26
+ ];
27
+
28
+ const SKIP_DIRS = new Set(['node_modules', '.git', 'dist', 'coverage', '.opencode']);
29
+ const SKIP_FILES = new Set(['package-lock.json']);
30
+ const SKIP_FILE_SUFFIXES = ['.lock', '.js.map'];
31
+
32
+ function shouldSkipFile(name) {
33
+ if (SKIP_FILES.has(name)) return true;
34
+ return SKIP_FILE_SUFFIXES.some(suffix => name.endsWith(suffix));
35
+ }
36
+
37
+ function copyDir(src, dest) {
38
+ if (!fs.existsSync(src)) {
39
+ console.error(`[prepack] SKIP (missing): ${src}`);
40
+ return false;
41
+ }
42
+ fs.mkdirSync(dest, { recursive: true });
43
+ const entries = fs.readdirSync(src, { withFileTypes: true });
44
+ for (const entry of entries) {
45
+ if (SKIP_DIRS.has(entry.name)) continue;
46
+ const srcPath = path.join(src, entry.name);
47
+ const destPath = path.join(dest, entry.name);
48
+ if (entry.isDirectory()) {
49
+ copyDir(srcPath, destPath);
50
+ } else if (entry.isFile()) {
51
+ if (shouldSkipFile(entry.name)) continue;
52
+ fs.copyFileSync(srcPath, destPath);
53
+ }
54
+ }
55
+ return true;
56
+ }
57
+
58
+ function main() {
59
+ const skillsDest = path.join(PLUGIN_ROOT, 'skills');
60
+
61
+ // Clean existing skills
62
+ if (fs.existsSync(skillsDest)) {
63
+ fs.rmSync(skillsDest, { recursive: true, force: true });
64
+ }
65
+ fs.mkdirSync(skillsDest, { recursive: true });
66
+
67
+ let copied = 0;
68
+ for (const name of CORE_SKILLS) {
69
+ const src = path.join(REPO_ROOT, 'skills', name);
70
+ const dest = path.join(skillsDest, name);
71
+ if (copyDir(src, dest)) {
72
+ copied += 1;
73
+ console.error(`[prepack] skills/${name}`);
74
+ }
75
+ }
76
+
77
+ if (copied !== CORE_SKILLS.length) {
78
+ console.error(`[prepack] ERROR: expected ${CORE_SKILLS.length} skills, copied ${copied}`);
79
+ process.exit(1);
80
+ }
81
+
82
+ console.error(`[prepack] done: ${copied} skills bundled for @boyingliu01/opencode-plugin`);
83
+ }
84
+
85
+ main();
@@ -12,6 +12,7 @@ description: >
12
12
  - "start sprint"
13
13
  - "一键开发"
14
14
  - "/sprint-flow"
15
+ 触发后第一行输出: `Sprint Flow: ISOLATE → AUTO-ESTIMATE → THINK → PLAN → BUILD → REVIEW → USER ACCEPTANCE → FEEDBACK → SHIP → LAND → CLEANUP`
15
16
  用法: /sprint-flow "[需求描述]"
16
17
  示例: /sprint-flow "开发访谈机器人,支持多轮对话"
17
18
  可选参数:
@@ -236,6 +237,36 @@ ISOLATE → AUTO-ESTIMATE → THINK → PLAN → [GITHOOKS-GATE] → BUILD → R
236
237
 
237
238
  ## 各 Phase 调用的 Skills
238
239
 
240
+ ### ⚠️ 强制输出格式规范(Mandatory Output Format)
241
+
242
+ **执行每个 Phase 时,必须以以下固定格式输出阶段标题**,不可省略、不可合并、不可替换:
243
+
244
+ ```markdown
245
+ ## Phase -1: ISOLATE (隔离)
246
+ ## Phase -0.5: AUTO-ESTIMATE (规模评估)
247
+ ## Phase 0: THINK (思考)
248
+ ## Phase 1: PLAN (规划)
249
+ ## Phase 2: BUILD (构建)
250
+ ## Phase 3: REVIEW (评审)
251
+ ## Phase 4: USER ACCEPTANCE (用户验收)
252
+ ## Phase 5: FEEDBACK (反馈)
253
+ ## Phase 6: SHIP (发布)
254
+ ## Phase 7: LAND (部署)
255
+ ## Phase 8: CLEANUP (清理)
256
+ ```
257
+
258
+ **规则**:
259
+ 1. 每个 Phase **开始执行时必须首先输出**对应的 `## Phase X: NAME` 标题行(作为该 Phase 输出的第一行)
260
+ 2. **禁止省略** "Phase" 关键词(如不能只写 "ISOLATE" 或 "## -1")
261
+ 3. **禁止合并**多个 Phase 的输出(每个 Phase 必须有独立标题)
262
+ 4. **格式必须精确匹配**:`## Phase ` + 数字 + `: ` + 大写英文名 + ` (中文名)`
263
+ 5. 跳过某个 Phase(如 `--resume-from build` 跳过了 -1, -0.5, 0, 1)时,不输出被跳过 Phase 的标题
264
+ 6. 触发 `/sprint-flow` 后,**第一行输出应包含工作流阶段概览**:
265
+
266
+ ```
267
+ Sprint Flow: ISOLATE → AUTO-ESTIMATE → THINK → PLAN → BUILD → REVIEW → USER ACCEPTANCE → FEEDBACK → SHIP → LAND → CLEANUP
268
+ ```
269
+
239
270
  ### Phase -1: ISOLATE(git worktree 隔离)
240
271
 
241
272
  **执行时机**: `/sprint-flow` 启动后、Phase 0 THINK 之前。**自动执行**。
@@ -12,6 +12,7 @@ description: >
12
12
  - "start sprint"
13
13
  - "一键开发"
14
14
  - "/sprint-flow"
15
+ 触发后第一行输出: `Sprint Flow: ISOLATE → AUTO-ESTIMATE → THINK → PLAN → BUILD → REVIEW → USER ACCEPTANCE → FEEDBACK → SHIP → LAND → CLEANUP`
15
16
  用法: /sprint-flow "[需求描述]"
16
17
  示例: /sprint-flow "开发访谈机器人,支持多轮对话"
17
18
  可选参数:
@@ -236,6 +237,36 @@ ISOLATE → AUTO-ESTIMATE → THINK → PLAN → [GITHOOKS-GATE] → BUILD → R
236
237
 
237
238
  ## 各 Phase 调用的 Skills
238
239
 
240
+ ### ⚠️ 强制输出格式规范(Mandatory Output Format)
241
+
242
+ **执行每个 Phase 时,必须以以下固定格式输出阶段标题**,不可省略、不可合并、不可替换:
243
+
244
+ ```markdown
245
+ ## Phase -1: ISOLATE (隔离)
246
+ ## Phase -0.5: AUTO-ESTIMATE (规模评估)
247
+ ## Phase 0: THINK (思考)
248
+ ## Phase 1: PLAN (规划)
249
+ ## Phase 2: BUILD (构建)
250
+ ## Phase 3: REVIEW (评审)
251
+ ## Phase 4: USER ACCEPTANCE (用户验收)
252
+ ## Phase 5: FEEDBACK (反馈)
253
+ ## Phase 6: SHIP (发布)
254
+ ## Phase 7: LAND (部署)
255
+ ## Phase 8: CLEANUP (清理)
256
+ ```
257
+
258
+ **规则**:
259
+ 1. 每个 Phase **开始执行时必须首先输出**对应的 `## Phase X: NAME` 标题行(作为该 Phase 输出的第一行)
260
+ 2. **禁止省略** "Phase" 关键词(如不能只写 "ISOLATE" 或 "## -1")
261
+ 3. **禁止合并**多个 Phase 的输出(每个 Phase 必须有独立标题)
262
+ 4. **格式必须精确匹配**:`## Phase ` + 数字 + `: ` + 大写英文名 + ` (中文名)`
263
+ 5. 跳过某个 Phase(如 `--resume-from build` 跳过了 -1, -0.5, 0, 1)时,不输出被跳过 Phase 的标题
264
+ 6. 触发 `/sprint-flow` 后,**第一行输出应包含工作流阶段概览**:
265
+
266
+ ```
267
+ Sprint Flow: ISOLATE → AUTO-ESTIMATE → THINK → PLAN → BUILD → REVIEW → USER ACCEPTANCE → FEEDBACK → SHIP → LAND → CLEANUP
268
+ ```
269
+
239
270
  ### Phase -1: ISOLATE(git worktree 隔离)
240
271
 
241
272
  **执行时机**: `/sprint-flow` 启动后、Phase 0 THINK 之前。**自动执行**。