@dommaker/harness 0.14.0 → 0.16.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.
Files changed (168) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/bin/harness.js +71 -0
  3. package/dist/cli/commands/doc-freshness-check.d.ts +53 -0
  4. package/dist/cli/commands/doc-freshness-check.d.ts.map +1 -0
  5. package/dist/cli/commands/doc-freshness-check.js +366 -0
  6. package/dist/cli/commands/doc-freshness-check.js.map +1 -0
  7. package/dist/cli/commands/index.d.ts +4 -1
  8. package/dist/cli/commands/index.d.ts.map +1 -1
  9. package/dist/cli/commands/index.js +13 -1
  10. package/dist/cli/commands/index.js.map +1 -1
  11. package/dist/cli/commands/knowledge.d.ts +12 -0
  12. package/dist/cli/commands/knowledge.d.ts.map +1 -1
  13. package/dist/cli/commands/knowledge.js +140 -6
  14. package/dist/cli/commands/knowledge.js.map +1 -1
  15. package/dist/cli/commands/release.d.ts +11 -0
  16. package/dist/cli/commands/release.d.ts.map +1 -1
  17. package/dist/cli/commands/release.js +80 -12
  18. package/dist/cli/commands/release.js.map +1 -1
  19. package/dist/cli/commands/sdd.d.ts +8 -0
  20. package/dist/cli/commands/sdd.d.ts.map +1 -0
  21. package/dist/cli/commands/sdd.js +68 -0
  22. package/dist/cli/commands/sdd.js.map +1 -0
  23. package/dist/cli/commands/spec-baseline-check.d.ts +30 -0
  24. package/dist/cli/commands/spec-baseline-check.d.ts.map +1 -0
  25. package/dist/cli/commands/spec-baseline-check.js +366 -0
  26. package/dist/cli/commands/spec-baseline-check.js.map +1 -0
  27. package/dist/core/constraints/checker.js +1 -1
  28. package/dist/core/constraints/checker.js.map +1 -1
  29. package/dist/core/constraints/definitions.d.ts.map +1 -1
  30. package/dist/core/constraints/definitions.js +30 -24
  31. package/dist/core/constraints/definitions.js.map +1 -1
  32. package/dist/knowledge/audit.d.ts +1 -1
  33. package/dist/knowledge/audit.d.ts.map +1 -1
  34. package/dist/knowledge/audit.js +60 -8
  35. package/dist/knowledge/audit.js.map +1 -1
  36. package/dist/knowledge/doctor.d.ts +1 -1
  37. package/dist/knowledge/doctor.d.ts.map +1 -1
  38. package/dist/knowledge/import.d.ts +1 -1
  39. package/dist/knowledge/import.d.ts.map +1 -1
  40. package/dist/knowledge/index-generator.d.ts +36 -0
  41. package/dist/knowledge/index-generator.d.ts.map +1 -0
  42. package/dist/knowledge/index-generator.js +241 -0
  43. package/dist/knowledge/index-generator.js.map +1 -0
  44. package/dist/knowledge/index.d.ts +4 -1
  45. package/dist/knowledge/index.d.ts.map +1 -1
  46. package/dist/knowledge/index.js +4 -2
  47. package/dist/knowledge/index.js.map +1 -1
  48. package/dist/knowledge/ingest.d.ts +1 -1
  49. package/dist/knowledge/ingest.d.ts.map +1 -1
  50. package/dist/knowledge/ingest.js +2 -2
  51. package/dist/knowledge/ingest.js.map +1 -1
  52. package/dist/knowledge/lifecycle-hooks.d.ts +1 -1
  53. package/dist/knowledge/lifecycle-hooks.d.ts.map +1 -1
  54. package/dist/knowledge/lifecycle.d.ts +16 -1
  55. package/dist/knowledge/lifecycle.d.ts.map +1 -1
  56. package/dist/knowledge/lifecycle.js +70 -0
  57. package/dist/knowledge/lifecycle.js.map +1 -1
  58. package/dist/knowledge/lint.d.ts +1 -1
  59. package/dist/knowledge/lint.d.ts.map +1 -1
  60. package/dist/knowledge/lint.js +4 -4
  61. package/dist/knowledge/lint.js.map +1 -1
  62. package/dist/knowledge/primitives/code-structure.d.ts +37 -0
  63. package/dist/knowledge/primitives/code-structure.d.ts.map +1 -0
  64. package/dist/knowledge/primitives/code-structure.js +168 -0
  65. package/dist/knowledge/primitives/code-structure.js.map +1 -0
  66. package/dist/knowledge/query.d.ts +1 -1
  67. package/dist/knowledge/query.d.ts.map +1 -1
  68. package/dist/knowledge/reference-tracker.d.ts +1 -1
  69. package/dist/knowledge/reference-tracker.d.ts.map +1 -1
  70. package/dist/knowledge/store.d.ts +29 -2
  71. package/dist/knowledge/store.d.ts.map +1 -1
  72. package/dist/knowledge/store.js +10 -5
  73. package/dist/knowledge/store.js.map +1 -1
  74. package/dist/knowledge/types.d.ts +17 -1
  75. package/dist/knowledge/types.d.ts.map +1 -1
  76. package/dist/knowledge/types.js.map +1 -1
  77. package/dist/monitoring/knowledge-evolver.d.ts +1 -1
  78. package/dist/monitoring/knowledge-evolver.d.ts.map +1 -1
  79. package/dist/sdd/index-generator.d.ts +22 -0
  80. package/dist/sdd/index-generator.d.ts.map +1 -0
  81. package/dist/sdd/index-generator.js +118 -0
  82. package/dist/sdd/index-generator.js.map +1 -0
  83. package/dist/types/constraint.d.ts +1 -1
  84. package/dist/types/constraint.d.ts.map +1 -1
  85. package/package.json +1 -1
  86. package/src/__tests__/checker.test.ts +4 -4
  87. package/src/cli/commands/__tests__/doc-freshness-check.test.ts +168 -0
  88. package/src/cli/commands/__tests__/knowledge.test.ts +70 -2
  89. package/src/cli/commands/__tests__/spec-baseline-check.test.ts +235 -0
  90. package/src/cli/commands/doc-freshness-check.ts +397 -0
  91. package/src/cli/commands/index.ts +4 -1
  92. package/src/cli/commands/knowledge.ts +151 -1
  93. package/src/cli/commands/release.ts +86 -12
  94. package/src/cli/commands/sdd.ts +44 -0
  95. package/src/cli/commands/spec-baseline-check.ts +384 -0
  96. package/src/context/__tests__/knowledge-injector.test.ts +1 -1
  97. package/src/core/constraints/checker.ts +1 -1
  98. package/src/core/constraints/definitions.ts +31 -25
  99. package/src/knowledge/__tests__/audit.test.ts +57 -1
  100. package/src/knowledge/__tests__/doctor.test.ts +1 -1
  101. package/src/knowledge/__tests__/import.test.ts +1 -1
  102. package/src/knowledge/__tests__/index-generator.test.ts +335 -0
  103. package/src/knowledge/__tests__/ingest.test.ts +1 -1
  104. package/src/knowledge/__tests__/lifecycle-hooks.test.ts +1 -1
  105. package/src/knowledge/__tests__/lifecycle.test.ts +1 -1
  106. package/src/knowledge/__tests__/lint.test.ts +1 -1
  107. package/src/knowledge/__tests__/query.test.ts +1 -1
  108. package/src/knowledge/__tests__/reference-tracker.test.ts +1 -1
  109. package/src/knowledge/__tests__/store.test.ts +1 -1
  110. package/src/knowledge/audit.ts +67 -10
  111. package/src/knowledge/doctor.ts +1 -1
  112. package/src/knowledge/import.ts +1 -1
  113. package/src/knowledge/index-generator.ts +233 -0
  114. package/src/knowledge/index.ts +4 -1
  115. package/src/knowledge/ingest.ts +3 -3
  116. package/src/knowledge/lifecycle-hooks.ts +1 -1
  117. package/src/knowledge/lifecycle.ts +72 -1
  118. package/src/knowledge/lint.ts +5 -5
  119. package/src/knowledge/primitives/__tests__/code-structure.test.ts +120 -0
  120. package/src/knowledge/primitives/code-structure.ts +161 -0
  121. package/src/knowledge/query.ts +1 -1
  122. package/src/knowledge/reference-tracker.ts +1 -1
  123. package/src/knowledge/store.ts +27 -4
  124. package/src/knowledge/types.ts +21 -1
  125. package/src/monitoring/knowledge-evolver.ts +1 -1
  126. package/src/sdd/__tests__/index-generator.test.ts +150 -0
  127. package/src/sdd/index-generator.ts +109 -0
  128. package/src/types/constraint.ts +1 -1
  129. package/dist/cli/commands/diagnose.d.ts +0 -16
  130. package/dist/cli/commands/diagnose.d.ts.map +0 -1
  131. package/dist/cli/commands/diagnose.js +0 -195
  132. package/dist/cli/commands/diagnose.js.map +0 -1
  133. package/dist/cli/commands/propose.d.ts +0 -18
  134. package/dist/cli/commands/propose.d.ts.map +0 -1
  135. package/dist/cli/commands/propose.js +0 -331
  136. package/dist/cli/commands/propose.js.map +0 -1
  137. package/dist/cli/commands/traces.d.ts +0 -15
  138. package/dist/cli/commands/traces.d.ts.map +0 -1
  139. package/dist/cli/commands/traces.js +0 -167
  140. package/dist/cli/commands/traces.js.map +0 -1
  141. package/dist/constraints/definitions.d.ts +0 -39
  142. package/dist/constraints/definitions.d.ts.map +0 -1
  143. package/dist/constraints/definitions.js +0 -71
  144. package/dist/constraints/definitions.js.map +0 -1
  145. package/dist/constraints/quality.d.ts +0 -11
  146. package/dist/constraints/quality.d.ts.map +0 -1
  147. package/dist/constraints/quality.js +0 -257
  148. package/dist/constraints/quality.js.map +0 -1
  149. package/dist/constraints/safety.d.ts +0 -13
  150. package/dist/constraints/safety.d.ts.map +0 -1
  151. package/dist/constraints/safety.js +0 -103
  152. package/dist/constraints/safety.js.map +0 -1
  153. package/dist/extensions/long-running/constraints.d.ts +0 -38
  154. package/dist/extensions/long-running/constraints.d.ts.map +0 -1
  155. package/dist/extensions/long-running/constraints.js +0 -153
  156. package/dist/extensions/long-running/constraints.js.map +0 -1
  157. package/dist/extensions/long-running/index.d.ts +0 -30
  158. package/dist/extensions/long-running/index.d.ts.map +0 -1
  159. package/dist/extensions/long-running/index.js +0 -50
  160. package/dist/extensions/long-running/index.js.map +0 -1
  161. package/dist/extensions/long-running/types.d.ts +0 -154
  162. package/dist/extensions/long-running/types.d.ts.map +0 -1
  163. package/dist/extensions/long-running/types.js +0 -9
  164. package/dist/extensions/long-running/types.js.map +0 -1
  165. package/dist/presets/long-running.d.ts +0 -33
  166. package/dist/presets/long-running.d.ts.map +0 -1
  167. package/dist/presets/long-running.js +0 -52
  168. package/dist/presets/long-running.js.map +0 -1
@@ -8,12 +8,13 @@ import chalk from 'chalk';
8
8
  import * as fs from 'fs';
9
9
  import * as os from 'os';
10
10
  import * as path from 'path';
11
- import { KnowledgeStore } from '../../knowledge/store';
11
+ import { FileKnowledgeStore as KnowledgeStore } from '../../knowledge/store';
12
12
  import { KnowledgeQuery } from '../../knowledge/query';
13
13
  import { KnowledgeLifecycle } from '../../knowledge/lifecycle';
14
14
  import { ColdStartImporter } from '../../knowledge/import';
15
15
  import { KnowledgeAudit } from '../../knowledge/audit';
16
16
  import { migrateKnowledgeEntries } from '../../knowledge/migration';
17
+ import { KnowledgeIndexGenerator } from '../../knowledge/index-generator';
17
18
  import type { KnowledgeType, MaturityLevel, QueryFilter } from '../../knowledge/types';
18
19
 
19
20
  export interface KnowledgeOptions {
@@ -413,6 +414,106 @@ export async function knowledgeSyncStatus(options: KnowledgeOptions): Promise<vo
413
414
  }
414
415
  }
415
416
 
417
+ /**
418
+ * 飞轮健康检查 — 零 token 检测知识飞轮数据流状态
419
+ */
420
+ export async function knowledgeHealth(options: KnowledgeOptions & { dir?: string }): Promise<void> {
421
+ const baseDir = options.dir || getKnowledgeDir(options.projectPath);
422
+ const store = new KnowledgeStore({ baseDir });
423
+ const entries = store.list({ excludeArchived: true });
424
+
425
+ const issues: Array<{ severity: 'error' | 'warn' | 'info'; entry: string; detail: string }> = [];
426
+
427
+ // D1: 引用密度检查(低引用 = 可能孤立)
428
+ let lowRefEntries = 0;
429
+ for (const entry of entries) {
430
+ if (entry.referencedBy.length === 0 && entry.maturity === 'verified') {
431
+ lowRefEntries++;
432
+ issues.push({ severity: 'info', entry: entry.id, detail: `verified 条目零引用(可能孤立)` });
433
+ }
434
+ }
435
+
436
+ // D2: 新鲜度检查
437
+ const now = Date.now();
438
+ const staleThreshold = 90 * 24 * 60 * 60 * 1000; // 90 days
439
+ let staleEntries = 0;
440
+ for (const entry of entries) {
441
+ const created = new Date(entry.created).getTime();
442
+ if (now - created > staleThreshold && entry.maturity === 'draft') {
443
+ staleEntries++;
444
+ issues.push({ severity: 'warn', entry: entry.id, detail: `draft 超过 90 天未推进` });
445
+ }
446
+ }
447
+
448
+ // D3: 消费数据检查
449
+ let consumptionData = false;
450
+ const statsPath = path.join(baseDir, '.consumption-stats.json');
451
+ if (fs.existsSync(statsPath)) {
452
+ consumptionData = true;
453
+ } else {
454
+ issues.push({ severity: 'info', entry: '-', detail: `消费追踪数据不存在(${statsPath})` });
455
+ }
456
+
457
+ // D4: 飞轮指标
458
+ const withRefs = entries.filter(e => e.referencedBy.length > 0).length;
459
+ const refCoverage = entries.length > 0 ? Math.round(withRefs / entries.length * 100) : 0;
460
+ const avgRefs = entries.length > 0
461
+ ? Math.round(entries.reduce((sum, e) => sum + e.referencedBy.length, 0) / entries.length * 10) / 10
462
+ : 0;
463
+
464
+ // 健康分计算
465
+ const totalIssues = issues.filter(i => i.severity === 'error').length * 3
466
+ + issues.filter(i => i.severity === 'warn').length * 1;
467
+ const healthScore = Math.max(0, 100 - totalIssues);
468
+
469
+ if (options.json) {
470
+ console.log(JSON.stringify({
471
+ healthScore,
472
+ summary: {
473
+ total: entries.length,
474
+ lowRefEntries,
475
+ staleEntries,
476
+ consumptionData,
477
+ refCoverage,
478
+ avgRefs,
479
+ },
480
+ issues: issues.slice(0, 50),
481
+ }, null, 2));
482
+ return;
483
+ }
484
+
485
+ console.log(chalk.blue('🏥 飞轮健康检查\n'));
486
+ console.log(chalk.bold(` 健康分: ${healthScore >= 80 ? chalk.green(healthScore) : healthScore >= 60 ? chalk.yellow(healthScore) : chalk.red(healthScore)}/100`));
487
+ console.log(chalk.bold(` 活跃条目: ${entries.length}`));
488
+ console.log();
489
+
490
+ console.log(chalk.bold(' 数据流状态:'));
491
+ console.log(` 引用密度: ${lowRefEntries === 0 ? chalk.green('✓') : chalk.yellow(`⚠ ${lowRefEntries} 个零引用 verified`)}`);
492
+ console.log(` 新鲜度: ${staleEntries === 0 ? chalk.green('✓') : chalk.yellow(`⚠ ${staleEntries} 个过期 draft`)}`);
493
+ console.log(` 消费追踪: ${consumptionData ? chalk.green('✓') : chalk.gray('○ 未启用')}`);
494
+ console.log();
495
+
496
+ console.log(chalk.bold(' 飞轮指标:'));
497
+ console.log(` 引用覆盖: ${refCoverage}%`);
498
+ console.log(` 平均引用: ${avgRefs}`);
499
+ console.log();
500
+
501
+ if (issues.length > 0) {
502
+ console.log(chalk.bold(` 问题 (前 20):`));
503
+ for (const issue of issues.slice(0, 20)) {
504
+ const sevColor = issue.severity === 'error' ? chalk.red
505
+ : issue.severity === 'warn' ? chalk.yellow
506
+ : chalk.gray;
507
+ console.log(` ${sevColor(`[${issue.severity}]`)} ${issue.entry}: ${issue.detail}`);
508
+ }
509
+ if (issues.length > 20) {
510
+ console.log(chalk.gray(` ... 还有 ${issues.length - 20} 条`));
511
+ }
512
+ } else {
513
+ console.log(chalk.green(' ✓ 无问题'));
514
+ }
515
+ }
516
+
416
517
  function getKnowledgeDir(projectPath?: string): string {
417
518
  if (projectPath) return `${projectPath}/.harness/knowledge`;
418
519
  if (process.env.KNOWLEDGE_BASE_DIR) return process.env.KNOWLEDGE_BASE_DIR;
@@ -482,6 +583,7 @@ export async function knowledgeAudit(options: KnowledgeOptions & {
482
583
  'maturity-inflation': '成熟度虚高',
483
584
  'title-duplicate': '标题重复',
484
585
  'source-refs-bloat': 'sourceReferences 膨胀',
586
+ 'fragment-cluster': '碎片集群',
485
587
  'promotion-blocked': 'promotion 受阻',
486
588
  'orphan-draft': '孤儿 draft',
487
589
  'stale-entry': '过期条目',
@@ -511,6 +613,14 @@ export async function knowledgeAudit(options: KnowledgeOptions & {
511
613
  if (!options.fix && report.issues.length > 0) {
512
614
  console.log(chalk.yellow(`\n 使用 --fix 自动修复`));
513
615
  }
616
+
617
+ // Auto-rebuild index after audit (files may have changed)
618
+ const baseDir = options.dir || getKnowledgeDir(options.projectPath);
619
+ const idxGen = new KnowledgeIndexGenerator(baseDir);
620
+ idxGen.regenerate();
621
+ if (!options.json) {
622
+ console.log(chalk.gray(` 📇 索引已重建`));
623
+ }
514
624
  }
515
625
 
516
626
  /**
@@ -560,3 +670,43 @@ export function knowledgeMigrate(options: KnowledgeOptions & { dir?: string }):
560
670
  console.log(chalk.green(`\n✅ 迁移完成`));
561
671
  }
562
672
  }
673
+
674
+ /**
675
+ * 知识库索引重建 — 生成 _index.md 供 Agent grep 使用,同时同步 index.json
676
+ */
677
+ export function knowledgeIndex(options: KnowledgeOptions & { dir?: string }): void {
678
+ const baseDir = options.dir || getKnowledgeDir(options.projectPath);
679
+ const gen = new KnowledgeIndexGenerator(baseDir);
680
+
681
+ const beforeSize = (() => {
682
+ const indexPath = path.join(baseDir, '_index.md');
683
+ return fs.existsSync(indexPath) ? fs.statSync(indexPath).size : 0;
684
+ })();
685
+
686
+ const output = gen.regenerate();
687
+
688
+ // Sync index.json from disk files (removes ghost entries from manual mv/rm)
689
+ const store = new KnowledgeStore({ baseDir });
690
+ store.rebuildIndex();
691
+
692
+ const afterSize = Buffer.byteLength(output, 'utf-8');
693
+ const lineCount = output.split('\n').filter(l => !l.startsWith('#')).length;
694
+
695
+ if (options.json) {
696
+ console.log(JSON.stringify({
697
+ path: path.join(baseDir, '_index.md'),
698
+ entries: lineCount,
699
+ size: afterSize,
700
+ previousSize: beforeSize,
701
+ }));
702
+ return;
703
+ }
704
+
705
+ console.log(chalk.blue(`📇 索引已重建`));
706
+ console.log(chalk.gray(` 路径: ${path.join(baseDir, '_index.md')}`));
707
+ console.log(chalk.green(` 条目: ${lineCount}`));
708
+ console.log(chalk.green(` 大小: ${(afterSize / 1024).toFixed(1)} KB`));
709
+ if (beforeSize > 0) {
710
+ console.log(chalk.gray(` 旧大小: ${(beforeSize / 1024).toFixed(1)} KB`));
711
+ }
712
+ }
@@ -4,6 +4,17 @@
4
4
  * 完整 npm 发布流水线:tsc → dist 验证 → npm version → git push → npm publish → gh release。
5
5
  * 与 studio MCP tool `publishPackage` 共享同一逻辑,但此 CLI 命令不依赖 Studio API 运行。
6
6
  * 当 studio API 因 harness 包损坏而无法启动时,此 CLI 命令仍可用。
7
+ *
8
+ * 流程:
9
+ * 1. Verify package
10
+ * 2. Check branch (must be master/main) + remote sync + clean tree
11
+ * 3. tsc build
12
+ * 4. Verify dist files
13
+ * 5. Bump version (npm version → commit + tag)
14
+ * 6. Push (protected branch → create release PR instead)
15
+ * 7. Check private flag
16
+ * 8. npm publish (or skip if PR flow)
17
+ * 9. GitHub Release
7
18
  */
8
19
 
9
20
  import { execSync } from 'child_process';
@@ -44,7 +55,26 @@ export async function release(options: ReleaseOptions): Promise<void> {
44
55
  console.log(chalk.cyan(`📦 ${pkgName}@${oldVersion}`));
45
56
  console.log(chalk.cyan(` bump: ${bumpType}${dryRun ? ' (dry-run)' : ''}`));
46
57
 
47
- // ── 2. Check clean working tree ──
58
+ // ── 2. Check branch (must be on master/main) ──
59
+ const branchResult = await run('git rev-parse --abbrev-ref HEAD', pkgPath);
60
+ const currentBranch = branchResult.stdout.trim();
61
+ if (currentBranch !== 'master' && currentBranch !== 'main') {
62
+ console.error(chalk.red(`❌ Must be on master or main branch to release. Currently on: ${currentBranch}`));
63
+ console.error(chalk.gray(' Run: git checkout master && git merge ' + currentBranch));
64
+ process.exit(1);
65
+ }
66
+ console.log(chalk.green(`✅ git: on ${currentBranch}`));
67
+
68
+ // ── 2b. Check remote sync ──
69
+ await run('git fetch origin', pkgPath, 15_000);
70
+ const behind = await run(`git rev-list --count HEAD..origin/${currentBranch}`, pkgPath);
71
+ if (behind.stdout.trim() !== '0') {
72
+ console.error(chalk.red(`❌ Local ${currentBranch} is ${behind.stdout.trim()} commits behind origin/${currentBranch}. Pull first.`));
73
+ process.exit(1);
74
+ }
75
+ console.log(chalk.green('✅ git: synced with remote'));
76
+
77
+ // ── 2c. Check clean working tree ──
48
78
  const stat = await run('git status --porcelain -uno', pkgPath);
49
79
  if (stat.stdout.length > 0) {
50
80
  console.error(chalk.red('❌ Uncommitted changes. Commit or stash before releasing.'));
@@ -95,9 +125,20 @@ export async function release(options: ReleaseOptions): Promise<void> {
95
125
  process.exit(0);
96
126
  }
97
127
 
98
- // ── 5. Bump version (npm version creates git commit + tag atomically,
99
- // including package-lock.json — no desync possible) ──
128
+ // ── 5. Bump version (npm version creates git commit + tag atomically) ──
100
129
  console.log(chalk.cyan('🔢 Bumping version...'));
130
+ const [major, minor, patch] = oldVersion.split('.').map(Number);
131
+ let expectedNew: string;
132
+ if (bumpType === 'major') expectedNew = `${major + 1}.0.0`;
133
+ else if (bumpType === 'minor') expectedNew = `${major}.${minor + 1}.0`;
134
+ else expectedNew = `${major}.${minor}.${patch + 1}`;
135
+ const expectedTag = `v${expectedNew}`;
136
+ const tagExists = await run(`git tag -l "${expectedTag}"`, pkgPath);
137
+ if (tagExists.stdout.trim() === expectedTag) {
138
+ console.error(chalk.red(`❌ Tag ${expectedTag} already exists. Version may have been released already.`));
139
+ process.exit(1);
140
+ }
141
+
101
142
  const bump = await run(`npm version ${bumpType} -m "release: %s"`, pkgPath);
102
143
  if (bump.stderr && !bump.stdout) {
103
144
  console.error(chalk.red('❌ npm version failed:'), bump.stderr);
@@ -107,10 +148,40 @@ export async function release(options: ReleaseOptions): Promise<void> {
107
148
  const tag = `v${newVersion}`;
108
149
  console.log(chalk.green(`✅ version: ${oldVersion} → ${newVersion} (committed + tagged)`));
109
150
 
110
- // ── 7. Push ──
151
+ // ── 6. Push (handle protected branch via PR) ──
111
152
  console.log(chalk.cyan('⬆️ Pushing...'));
112
- const branch = await run('git rev-parse --abbrev-ref HEAD', pkgPath);
113
- const push = await run(`git push origin ${branch.stdout}`, pkgPath, 30_000);
153
+ const push = await run(`git push origin ${currentBranch}`, pkgPath, 30_000);
154
+ const isProtected = push.stderr.includes('GH006') || push.stderr.includes('protected branch');
155
+
156
+ if (isProtected) {
157
+ // Protected branch: create release branch + PR
158
+ const releaseBranch = `release/v${newVersion}`;
159
+ console.log(chalk.yellow(`⚠️ ${currentBranch} is protected. Creating PR via ${releaseBranch}...`));
160
+
161
+ await run(`git checkout -b ${releaseBranch}`, pkgPath);
162
+ await run(`git push origin ${releaseBranch}`, pkgPath, 30_000);
163
+
164
+ // Create PR
165
+ const prBody = `## Release ${pkgName}@${newVersion}\n\n- Bump version: ${oldVersion} → ${newVersion}\n- Changelog updated\n\nAfter merge, push tag to trigger npm publish:\n\`\`\`\ngit push origin ${tag}\n\`\`\``;
166
+ const pr = await run(`gh pr create --title "release: ${pkgName}@${newVersion}" --body "${prBody}" --base ${currentBranch}`, pkgPath, 30_000);
167
+ if (pr.stdout) {
168
+ console.log(chalk.green(`✅ PR created: ${pr.stdout.trim()}`));
169
+ } else {
170
+ console.error(chalk.red('❌ PR creation failed:'), pr.stderr.slice(0, 500));
171
+ process.exit(1);
172
+ }
173
+
174
+ // Switch back to original branch
175
+ await run(`git checkout ${currentBranch}`, pkgPath);
176
+
177
+ console.log(chalk.cyan('\n📋 Next steps:'));
178
+ console.log(chalk.gray(` 1. Merge the PR: ${pr.stdout.trim()}`));
179
+ console.log(chalk.gray(` 2. After merge, push tag to trigger npm publish:`));
180
+ console.log(chalk.gray(` git push origin ${tag}`));
181
+ console.log(chalk.gray(` 3. Or run: gh release create ${tag} --generate-notes`));
182
+ return;
183
+ }
184
+
114
185
  if (push.stderr && push.stderr.includes('error')) {
115
186
  console.error(chalk.red('❌ git push failed:'), push.stderr.slice(0, 500));
116
187
  process.exit(1);
@@ -118,21 +189,25 @@ export async function release(options: ReleaseOptions): Promise<void> {
118
189
  await run(`git push origin ${tag}`, pkgPath, 30_000);
119
190
  console.log(chalk.green('✅ git: pushed'));
120
191
 
192
+ // ── 7. Check private flag ──
193
+ if (pkgJson.private) {
194
+ console.error(chalk.red('❌ package.json has "private": true. Remove it before publishing.'));
195
+ console.error(chalk.gray(' The release tool will NOT auto-remove private flag.'));
196
+ process.exit(1);
197
+ }
198
+ console.log(chalk.green('✅ package: not private'));
199
+
121
200
  // ── 8. npm publish ──
122
- // Switch to npmjs.org for publishing (npmmirror is read-only mirror)
123
201
  const origRegistry = await run('npm config get registry', pkgPath);
124
202
  await run('npm config set registry https://registry.npmjs.org/', pkgPath);
125
203
  console.log(chalk.cyan('📤 Publishing to npm...'));
126
- // npm publish to npmjs.org (the write registry — npmmirror is read-only)
127
204
  const pub = await run('npm publish --registry https://registry.npmjs.org/', pkgPath, 180_000);
128
- // Restore original registry
129
205
  await run(`npm config set registry ${origRegistry.stdout}`, pkgPath);
130
- // Don't string-match npm output — verify by querying the registry
131
206
  const verify = await run(`npm view ${pkgName} version --registry https://registry.npmjs.org/`, pkgPath);
132
207
  if (verify.stdout.trim() === newVersion) {
133
208
  console.log(chalk.green(`✅ npm: published ${pkgName}@${newVersion}`));
134
209
  } else {
135
- console.log(chalk.red(`❌ npm publish verification failed. Expected ${newVersion}, registry has ${verify.stdout.trim() || '???'}`));
210
+ console.error(chalk.red(`❌ npm publish verification failed. Expected ${newVersion}, registry has ${verify.stdout.trim() || '???'}`));
136
211
  process.exit(1);
137
212
  }
138
213
 
@@ -144,7 +219,6 @@ export async function release(options: ReleaseOptions): Promise<void> {
144
219
  } else if (gh.stderr) {
145
220
  console.log(chalk.yellow(`⚠️ gh: release may have failed (non-fatal): ${gh.stderr.slice(0, 200)}`));
146
221
  } else {
147
- // Derive repo URL from git remote
148
222
  let repoUrl = '';
149
223
  try {
150
224
  const remoteUrl = execSync('git remote get-url origin', { cwd: pkgPath, encoding: 'utf-8', stdio: 'pipe', timeout: 5_000 }).trim();
@@ -0,0 +1,44 @@
1
+ // SDD CLI commands
2
+ import * as fs from 'fs';
3
+ import * as path from 'path';
4
+ import { SDDIndexGenerator } from '../../sdd/index-generator';
5
+
6
+ export interface SDDOptions {
7
+ projectPath?: string;
8
+ json?: boolean;
9
+ }
10
+
11
+ function getSDDDir(projectPath?: string): string {
12
+ // Default: projectPath/docs or cwd/docs
13
+ const base = projectPath || process.cwd();
14
+ return base;
15
+ }
16
+
17
+ export function sddIndex(options: SDDOptions & { dir?: string }): void {
18
+ const baseDir = options.dir || getSDDDir(options.projectPath);
19
+ const gen = new SDDIndexGenerator(baseDir);
20
+
21
+ const sddDir = path.join(baseDir, 'docs', 'sdd');
22
+ const indexPath = path.join(sddDir, '_index.md');
23
+ const beforeSize = fs.existsSync(indexPath) ? fs.statSync(indexPath).size : 0;
24
+
25
+ const result = gen.regenerate();
26
+
27
+ const afterSize = Buffer.byteLength(
28
+ result.entries.map(e => `${e.slug}|${e.pmoNumber}|${e.status}|${e.title}|${e.tags}`).join('\n'),
29
+ 'utf-8',
30
+ );
31
+
32
+ if (options.json) {
33
+ console.log(JSON.stringify({
34
+ path: indexPath,
35
+ entries: result.count,
36
+ size: afterSize,
37
+ previousSize: beforeSize,
38
+ }));
39
+ } else {
40
+ console.log(`SDD Index regenerated: ${indexPath}`);
41
+ console.log(` Entries: ${result.count}`);
42
+ console.log(` Size: ${beforeSize} → ${afterSize} bytes`);
43
+ }
44
+ }