@dommaker/harness 0.16.0 → 0.16.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/bin/harness.js +73 -1
- package/dist/cli/commands/doc-freshness-check.d.ts +53 -0
- package/dist/cli/commands/doc-freshness-check.d.ts.map +1 -0
- package/dist/cli/commands/doc-freshness-check.js +366 -0
- package/dist/cli/commands/doc-freshness-check.js.map +1 -0
- package/dist/cli/commands/index.d.ts +4 -1
- package/dist/cli/commands/index.d.ts.map +1 -1
- package/dist/cli/commands/index.js +13 -1
- package/dist/cli/commands/index.js.map +1 -1
- package/dist/cli/commands/knowledge.d.ts +12 -0
- package/dist/cli/commands/knowledge.d.ts.map +1 -1
- package/dist/cli/commands/knowledge.js +134 -0
- package/dist/cli/commands/knowledge.js.map +1 -1
- package/dist/cli/commands/release.d.ts +11 -0
- package/dist/cli/commands/release.d.ts.map +1 -1
- package/dist/cli/commands/release.js +43 -12
- package/dist/cli/commands/release.js.map +1 -1
- package/dist/cli/commands/sdd.d.ts +8 -0
- package/dist/cli/commands/sdd.d.ts.map +1 -0
- package/dist/cli/commands/sdd.js +68 -0
- package/dist/cli/commands/sdd.js.map +1 -0
- package/dist/cli/commands/spec-baseline-check.d.ts +30 -0
- package/dist/cli/commands/spec-baseline-check.d.ts.map +1 -0
- package/dist/cli/commands/spec-baseline-check.js +366 -0
- package/dist/cli/commands/spec-baseline-check.js.map +1 -0
- package/dist/cli/commands/sync-docs.d.ts +2 -0
- package/dist/cli/commands/sync-docs.d.ts.map +1 -1
- package/dist/cli/commands/sync-docs.js +320 -1
- package/dist/cli/commands/sync-docs.js.map +1 -1
- package/dist/knowledge/audit.d.ts +1 -1
- package/dist/knowledge/audit.d.ts.map +1 -1
- package/dist/knowledge/audit.js +59 -7
- package/dist/knowledge/audit.js.map +1 -1
- package/dist/knowledge/index-generator.d.ts +36 -0
- package/dist/knowledge/index-generator.d.ts.map +1 -0
- package/dist/knowledge/index-generator.js +241 -0
- package/dist/knowledge/index-generator.js.map +1 -0
- package/dist/knowledge/index.d.ts +2 -0
- package/dist/knowledge/index.d.ts.map +1 -1
- package/dist/knowledge/index.js +3 -1
- package/dist/knowledge/index.js.map +1 -1
- package/dist/knowledge/ingest.js +2 -2
- package/dist/knowledge/ingest.js.map +1 -1
- package/dist/knowledge/lifecycle.d.ts +15 -0
- package/dist/knowledge/lifecycle.d.ts.map +1 -1
- package/dist/knowledge/lifecycle.js +70 -0
- package/dist/knowledge/lifecycle.js.map +1 -1
- package/dist/knowledge/lint.js +3 -3
- package/dist/knowledge/lint.js.map +1 -1
- package/dist/knowledge/primitives/code-structure.d.ts +37 -0
- package/dist/knowledge/primitives/code-structure.d.ts.map +1 -0
- package/dist/knowledge/primitives/code-structure.js +168 -0
- package/dist/knowledge/primitives/code-structure.js.map +1 -0
- package/dist/knowledge/types.d.ts +3 -1
- package/dist/knowledge/types.d.ts.map +1 -1
- package/dist/knowledge/types.js.map +1 -1
- package/dist/sdd/index-generator.d.ts +22 -0
- package/dist/sdd/index-generator.d.ts.map +1 -0
- package/dist/sdd/index-generator.js +118 -0
- package/dist/sdd/index-generator.js.map +1 -0
- package/package.json +1 -1
- package/src/cli/commands/CONTEXT.md +3 -3
- package/src/cli/commands/__tests__/doc-freshness-check.test.ts +168 -0
- package/src/cli/commands/__tests__/knowledge.test.ts +69 -1
- package/src/cli/commands/__tests__/spec-baseline-check.test.ts +235 -0
- package/src/cli/commands/__tests__/sync-docs-agents.test.ts +226 -0
- package/src/cli/commands/doc-freshness-check.ts +397 -0
- package/src/cli/commands/index.ts +4 -1
- package/src/cli/commands/knowledge.ts +150 -0
- package/src/cli/commands/release.ts +48 -12
- package/src/cli/commands/sdd.ts +44 -0
- package/src/cli/commands/spec-baseline-check.ts +384 -0
- package/src/cli/commands/sync-docs.ts +355 -1
- package/src/knowledge/__tests__/audit.test.ts +56 -0
- package/src/knowledge/__tests__/index-generator.test.ts +335 -0
- package/src/knowledge/audit.ts +64 -8
- package/src/knowledge/index-generator.ts +233 -0
- package/src/knowledge/index.ts +2 -0
- package/src/knowledge/ingest.ts +2 -2
- package/src/knowledge/lifecycle.ts +71 -0
- package/src/knowledge/lint.ts +3 -3
- package/src/knowledge/primitives/__tests__/code-structure.test.ts +120 -0
- package/src/knowledge/primitives/code-structure.ts +161 -0
- package/src/knowledge/types.ts +4 -1
- package/src/sdd/__tests__/index-generator.test.ts +150 -0
- package/src/sdd/index-generator.ts +109 -0
|
@@ -14,6 +14,7 @@ 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';
|
|
@@ -114,8 +125,7 @@ export async function release(options: ReleaseOptions): Promise<void> {
|
|
|
114
125
|
process.exit(0);
|
|
115
126
|
}
|
|
116
127
|
|
|
117
|
-
// ── 5. Bump version (npm version creates git commit + tag atomically
|
|
118
|
-
// including package-lock.json — no desync possible) ──
|
|
128
|
+
// ── 5. Bump version (npm version creates git commit + tag atomically) ──
|
|
119
129
|
console.log(chalk.cyan('🔢 Bumping version...'));
|
|
120
130
|
const [major, minor, patch] = oldVersion.split('.').map(Number);
|
|
121
131
|
let expectedNew: string;
|
|
@@ -138,9 +148,40 @@ export async function release(options: ReleaseOptions): Promise<void> {
|
|
|
138
148
|
const tag = `v${newVersion}`;
|
|
139
149
|
console.log(chalk.green(`✅ version: ${oldVersion} → ${newVersion} (committed + tagged)`));
|
|
140
150
|
|
|
141
|
-
// ── 6. Push ──
|
|
151
|
+
// ── 6. Push (handle protected branch via PR) ──
|
|
142
152
|
console.log(chalk.cyan('⬆️ Pushing...'));
|
|
143
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
|
+
|
|
144
185
|
if (push.stderr && push.stderr.includes('error')) {
|
|
145
186
|
console.error(chalk.red('❌ git push failed:'), push.stderr.slice(0, 500));
|
|
146
187
|
process.exit(1);
|
|
@@ -148,7 +189,7 @@ export async function release(options: ReleaseOptions): Promise<void> {
|
|
|
148
189
|
await run(`git push origin ${tag}`, pkgPath, 30_000);
|
|
149
190
|
console.log(chalk.green('✅ git: pushed'));
|
|
150
191
|
|
|
151
|
-
// ──
|
|
192
|
+
// ── 7. Check private flag ──
|
|
152
193
|
if (pkgJson.private) {
|
|
153
194
|
console.error(chalk.red('❌ package.json has "private": true. Remove it before publishing.'));
|
|
154
195
|
console.error(chalk.gray(' The release tool will NOT auto-remove private flag.'));
|
|
@@ -156,25 +197,21 @@ export async function release(options: ReleaseOptions): Promise<void> {
|
|
|
156
197
|
}
|
|
157
198
|
console.log(chalk.green('✅ package: not private'));
|
|
158
199
|
|
|
159
|
-
// ──
|
|
160
|
-
// Switch to npmjs.org for publishing (npmmirror is read-only mirror)
|
|
200
|
+
// ── 8. npm publish ──
|
|
161
201
|
const origRegistry = await run('npm config get registry', pkgPath);
|
|
162
202
|
await run('npm config set registry https://registry.npmjs.org/', pkgPath);
|
|
163
203
|
console.log(chalk.cyan('📤 Publishing to npm...'));
|
|
164
|
-
// npm publish to npmjs.org (the write registry — npmmirror is read-only)
|
|
165
204
|
const pub = await run('npm publish --registry https://registry.npmjs.org/', pkgPath, 180_000);
|
|
166
|
-
// Restore original registry
|
|
167
205
|
await run(`npm config set registry ${origRegistry.stdout}`, pkgPath);
|
|
168
|
-
// Don't string-match npm output — verify by querying the registry
|
|
169
206
|
const verify = await run(`npm view ${pkgName} version --registry https://registry.npmjs.org/`, pkgPath);
|
|
170
207
|
if (verify.stdout.trim() === newVersion) {
|
|
171
208
|
console.log(chalk.green(`✅ npm: published ${pkgName}@${newVersion}`));
|
|
172
209
|
} else {
|
|
173
|
-
console.
|
|
210
|
+
console.error(chalk.red(`❌ npm publish verification failed. Expected ${newVersion}, registry has ${verify.stdout.trim() || '???'}`));
|
|
174
211
|
process.exit(1);
|
|
175
212
|
}
|
|
176
213
|
|
|
177
|
-
// ──
|
|
214
|
+
// ── 9. GitHub Release ──
|
|
178
215
|
console.log(chalk.cyan('🐙 Creating GitHub Release...'));
|
|
179
216
|
const gh = await run(`gh release create ${tag} --generate-notes`, pkgPath, 30_000);
|
|
180
217
|
if (gh.stderr && gh.stderr.includes('already exists')) {
|
|
@@ -182,7 +219,6 @@ export async function release(options: ReleaseOptions): Promise<void> {
|
|
|
182
219
|
} else if (gh.stderr) {
|
|
183
220
|
console.log(chalk.yellow(`⚠️ gh: release may have failed (non-fatal): ${gh.stderr.slice(0, 200)}`));
|
|
184
221
|
} else {
|
|
185
|
-
// Derive repo URL from git remote
|
|
186
222
|
let repoUrl = '';
|
|
187
223
|
try {
|
|
188
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
|
+
}
|