@zhuan-ai/zhuanspec 2.2.0 → 2.2.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.
- package/dist/cli/index.js +2 -0
- package/dist/core/archive.d.ts +22 -12
- package/dist/core/archive.js +241 -7
- package/dist/core/init.d.ts +5 -0
- package/dist/core/init.js +143 -5
- package/dist/core/specs-apply.js +19 -8
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -189,6 +189,7 @@ program
|
|
|
189
189
|
.option('-y, --yes', 'Skip confirmation prompts')
|
|
190
190
|
.option('--skip-specs', 'Skip spec update operations (useful for infrastructure, tooling, or doc-only changes)')
|
|
191
191
|
.option('--no-validate', 'Skip validation (not recommended, requires confirmation)')
|
|
192
|
+
.option('--business-direction <direction>', 'Business direction used for template refresh and remote sync (e.g., oms)')
|
|
192
193
|
.action(async (changeName, options) => {
|
|
193
194
|
try {
|
|
194
195
|
const archiveCommand = new ArchiveCommand();
|
|
@@ -206,6 +207,7 @@ program
|
|
|
206
207
|
.option('-y, --yes', 'Skip confirmation prompts and archive all active changes')
|
|
207
208
|
.option('--skip-specs', 'Skip spec update operations')
|
|
208
209
|
.option('--no-validate', 'Skip validation (not recommended)')
|
|
210
|
+
.option('--business-direction <direction>', 'Business direction used for template refresh and remote sync (e.g., oms)')
|
|
209
211
|
.action(async (options) => {
|
|
210
212
|
try {
|
|
211
213
|
const archiveCommand = new ArchiveCommand();
|
package/dist/core/archive.d.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
|
+
type ArchiveOptions = {
|
|
2
|
+
yes?: boolean;
|
|
3
|
+
skipSpecs?: boolean;
|
|
4
|
+
noValidate?: boolean;
|
|
5
|
+
validate?: boolean;
|
|
6
|
+
businessDirection?: string;
|
|
7
|
+
};
|
|
1
8
|
export declare class ArchiveCommand {
|
|
2
|
-
execute(changeName?: string, options?:
|
|
3
|
-
yes?: boolean;
|
|
4
|
-
skipSpecs?: boolean;
|
|
5
|
-
noValidate?: boolean;
|
|
6
|
-
validate?: boolean;
|
|
7
|
-
}): Promise<void>;
|
|
9
|
+
execute(changeName?: string, options?: ArchiveOptions): Promise<void>;
|
|
8
10
|
/**
|
|
9
11
|
* Move a directory, falling back to copy+remove when rename fails
|
|
10
12
|
* (e.g., across devices, network drives, or permission issues).
|
|
@@ -20,11 +22,19 @@ export declare class ArchiveCommand {
|
|
|
20
22
|
* Bulk archive multiple completed changes at once.
|
|
21
23
|
* Lists all active changes, lets user select multiple, then archives them sequentially.
|
|
22
24
|
*/
|
|
23
|
-
bulkArchive(options?:
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
25
|
+
bulkArchive(options?: ArchiveOptions): Promise<void>;
|
|
26
|
+
private resolveBusinessDirection;
|
|
27
|
+
private refreshBusinessTemplateFromRemote;
|
|
28
|
+
private pushArchiveResultToRemote;
|
|
29
|
+
private cloneRemoteRepoToTemp;
|
|
30
|
+
private git;
|
|
31
|
+
private hasGitChanges;
|
|
32
|
+
private copyDirectoryContentsIfExists;
|
|
33
|
+
private mirrorDirectory;
|
|
34
|
+
private findFirstDirectoryByName;
|
|
35
|
+
private resolveBusinessTemplateRoot;
|
|
36
|
+
private resolveRemoteBusinessRoot;
|
|
37
|
+
private toErrorMessage;
|
|
29
38
|
}
|
|
39
|
+
export {};
|
|
30
40
|
//# sourceMappingURL=archive.d.ts.map
|
package/dist/core/archive.js
CHANGED
|
@@ -1,15 +1,21 @@
|
|
|
1
|
-
import { promises as fs } from 'fs';
|
|
1
|
+
import { promises as fs, existsSync, readdirSync } from 'fs';
|
|
2
2
|
import path from 'path';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
import { mkdtempSync, rmSync } from 'fs';
|
|
5
|
+
import { execSync } from 'child_process';
|
|
3
6
|
import { getTaskProgressForChange, formatTaskStatus } from '../utils/task-progress.js';
|
|
4
7
|
import { Validator } from './validation/validator.js';
|
|
5
8
|
import chalk from 'chalk';
|
|
6
9
|
import { findSpecUpdates, buildUpdatedSpec, writeUpdatedSpec, } from './specs-apply.js';
|
|
10
|
+
const ARCH_REPO_URL = 'http://gitlab.zhuanspirit.com/zz-kf/spec_repo.git';
|
|
11
|
+
const ARCH_REPO_BRANCH = 'spec_repo-feature-6612-2';
|
|
7
12
|
export class ArchiveCommand {
|
|
8
13
|
async execute(changeName, options = {}) {
|
|
9
14
|
const targetPath = '.';
|
|
10
15
|
const changesDir = path.join(targetPath, 'zhuanspec', 'changes');
|
|
11
16
|
const archiveDir = path.join(changesDir, 'archive');
|
|
12
17
|
const mainSpecsDir = path.join(targetPath, 'zhuanspec', 'specs');
|
|
18
|
+
const zhuanspecDir = path.join(targetPath, 'zhuanspec');
|
|
13
19
|
// Check if changes directory exists
|
|
14
20
|
try {
|
|
15
21
|
await fs.access(changesDir);
|
|
@@ -17,6 +23,11 @@ export class ArchiveCommand {
|
|
|
17
23
|
catch {
|
|
18
24
|
throw new Error("未找到 ZhuanSpec 变更目录。请先运行 'zhuanspec init'。");
|
|
19
25
|
}
|
|
26
|
+
const businessDirection = await this.resolveBusinessDirection(zhuanspecDir, options.businessDirection);
|
|
27
|
+
if (!businessDirection) {
|
|
28
|
+
throw new Error('未解析到业务方向,无法执行归档前拉取与归档后回推。请使用 --business-direction 或在 zhuanspec/.business-direction、zhuanspec/project.md 中配置。');
|
|
29
|
+
}
|
|
30
|
+
await this.refreshBusinessTemplateFromRemote(zhuanspecDir, businessDirection);
|
|
20
31
|
// Get change name interactively if not provided
|
|
21
32
|
if (!changeName) {
|
|
22
33
|
const selectedChange = await this.selectChange(changesDir);
|
|
@@ -214,20 +225,27 @@ export class ArchiveCommand {
|
|
|
214
225
|
const archiveName = `${this.getArchiveDate()}-${changeName}`;
|
|
215
226
|
const archivePath = path.join(archiveDir, archiveName);
|
|
216
227
|
// Check if archive already exists
|
|
228
|
+
let archiveExists = false;
|
|
217
229
|
try {
|
|
218
230
|
await fs.access(archivePath);
|
|
219
|
-
|
|
231
|
+
archiveExists = true;
|
|
220
232
|
}
|
|
221
233
|
catch (error) {
|
|
222
234
|
if (error.code !== 'ENOENT') {
|
|
223
235
|
throw error;
|
|
224
236
|
}
|
|
225
237
|
}
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
238
|
+
if (archiveExists) {
|
|
239
|
+
console.log(chalk.yellow(`归档 '${archiveName}' 已存在,跳过归档移动。`));
|
|
240
|
+
}
|
|
241
|
+
else {
|
|
242
|
+
// Create archive directory if needed
|
|
243
|
+
await fs.mkdir(archiveDir, { recursive: true });
|
|
244
|
+
// Move change to archive (with cross-device fallback)
|
|
245
|
+
await this.moveDirectory(changeDir, archivePath);
|
|
246
|
+
console.log(`变更 '${changeName}' 已归档为 '${archiveName}'。`);
|
|
247
|
+
}
|
|
248
|
+
await this.pushArchiveResultToRemote(zhuanspecDir, businessDirection);
|
|
231
249
|
}
|
|
232
250
|
/**
|
|
233
251
|
* Move a directory, falling back to copy+remove when rename fails
|
|
@@ -434,5 +452,221 @@ export class ArchiveCommand {
|
|
|
434
452
|
console.log(chalk.cyan(`\n━━━ 批量归档完成 ━━━`));
|
|
435
453
|
console.log(`成功: ${successCount}, 失败: ${failCount}, 总计: ${selectedChanges.length}`);
|
|
436
454
|
}
|
|
455
|
+
async resolveBusinessDirection(zhuanspecDir, explicitDirection) {
|
|
456
|
+
if (explicitDirection && explicitDirection.trim()) {
|
|
457
|
+
return explicitDirection.trim().toLowerCase();
|
|
458
|
+
}
|
|
459
|
+
const directionFilePath = path.join(zhuanspecDir, '.business-direction');
|
|
460
|
+
try {
|
|
461
|
+
const fromFile = (await fs.readFile(directionFilePath, 'utf-8')).trim();
|
|
462
|
+
if (fromFile) {
|
|
463
|
+
return fromFile.toLowerCase();
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
catch {
|
|
467
|
+
// Continue to project.md fallback.
|
|
468
|
+
}
|
|
469
|
+
const projectPath = path.join(zhuanspecDir, 'project.md');
|
|
470
|
+
try {
|
|
471
|
+
const content = await fs.readFile(projectPath, 'utf-8');
|
|
472
|
+
const patterns = [
|
|
473
|
+
/业务方向\s*[::]\s*([A-Za-z0-9_-]+)/i,
|
|
474
|
+
/business\s*direction\s*[::]\s*([A-Za-z0-9_-]+)/i,
|
|
475
|
+
];
|
|
476
|
+
for (const pattern of patterns) {
|
|
477
|
+
const match = content.match(pattern);
|
|
478
|
+
if (match?.[1]) {
|
|
479
|
+
return match[1].trim().toLowerCase();
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
catch {
|
|
484
|
+
// Ignore parsing errors and fall back to null.
|
|
485
|
+
}
|
|
486
|
+
return null;
|
|
487
|
+
}
|
|
488
|
+
async refreshBusinessTemplateFromRemote(zhuanspecDir, businessDirection) {
|
|
489
|
+
const tempRepo = this.cloneRemoteRepoToTemp();
|
|
490
|
+
try {
|
|
491
|
+
const remoteBusiness = this.resolveRemoteBusinessRoot(tempRepo, businessDirection);
|
|
492
|
+
if (!remoteBusiness) {
|
|
493
|
+
throw new Error(`未在远端 specs 目录中找到业务方向 '${businessDirection}',归档已中止。`);
|
|
494
|
+
}
|
|
495
|
+
const { businessRoot } = remoteBusiness;
|
|
496
|
+
const templateRoot = this.resolveBusinessTemplateRoot(businessRoot);
|
|
497
|
+
if (!templateRoot) {
|
|
498
|
+
throw new Error(`未在业务方向 '${businessDirection}' 目录下递归找到可用的 zhuanspec 目录,归档已中止。`);
|
|
499
|
+
}
|
|
500
|
+
await this.copyDirectoryContentsIfExists(path.join(templateRoot, 'specs'), path.join(zhuanspecDir, 'specs'));
|
|
501
|
+
await this.copyDirectoryContentsIfExists(path.join(templateRoot, 'changes'), path.join(zhuanspecDir, 'changes'));
|
|
502
|
+
console.log(chalk.gray(`已拉取 ${businessDirection} 业务模板(specs/changes)。`));
|
|
503
|
+
}
|
|
504
|
+
finally {
|
|
505
|
+
rmSync(tempRepo, { recursive: true, force: true });
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
async pushArchiveResultToRemote(zhuanspecDir, businessDirection) {
|
|
509
|
+
const tempRepo = this.cloneRemoteRepoToTemp();
|
|
510
|
+
try {
|
|
511
|
+
const remoteBusiness = this.resolveRemoteBusinessRoot(tempRepo, businessDirection);
|
|
512
|
+
if (!remoteBusiness) {
|
|
513
|
+
throw new Error(`未在远端 specs 目录中找到业务方向 '${businessDirection}',归档结果回推已中止。`);
|
|
514
|
+
}
|
|
515
|
+
const { businessRoot } = remoteBusiness;
|
|
516
|
+
const templateRoot = this.resolveBusinessTemplateRoot(businessRoot);
|
|
517
|
+
if (!templateRoot) {
|
|
518
|
+
throw new Error(`未在业务方向 '${businessDirection}' 目录下递归找到可用的 zhuanspec 目录,归档结果回推已中止。`);
|
|
519
|
+
}
|
|
520
|
+
const remoteBase = templateRoot;
|
|
521
|
+
const remoteSpecsPath = path.join(remoteBase, 'specs');
|
|
522
|
+
const remoteArchivePath = path.join(remoteBase, 'changes', 'archive');
|
|
523
|
+
const localSpecsPath = path.join(zhuanspecDir, 'specs');
|
|
524
|
+
const localArchivePath = path.join(zhuanspecDir, 'changes', 'archive');
|
|
525
|
+
await this.mirrorDirectory(localSpecsPath, remoteSpecsPath);
|
|
526
|
+
await this.mirrorDirectory(localArchivePath, remoteArchivePath);
|
|
527
|
+
const changed = this.hasGitChanges(tempRepo);
|
|
528
|
+
if (!changed) {
|
|
529
|
+
console.log(chalk.gray('远端模板仓库无变更,跳过 push。'));
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
this.git(tempRepo, 'git add .');
|
|
533
|
+
const commitMsg = `chore: sync archive outputs for ${businessDirection}`;
|
|
534
|
+
this.git(tempRepo, `git commit -m "${commitMsg}"`);
|
|
535
|
+
this.git(tempRepo, `git push origin ${ARCH_REPO_BRANCH}`);
|
|
536
|
+
console.log(chalk.green(`已推送归档结果到远端 ${ARCH_REPO_BRANCH}(${businessDirection})。`));
|
|
537
|
+
}
|
|
538
|
+
finally {
|
|
539
|
+
rmSync(tempRepo, { recursive: true, force: true });
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
cloneRemoteRepoToTemp() {
|
|
543
|
+
const tempDir = mkdtempSync(path.join(os.tmpdir(), 'zhuanspec-archive-'));
|
|
544
|
+
try {
|
|
545
|
+
this.git(process.cwd(), `git clone --depth 1 --single-branch --branch ${ARCH_REPO_BRANCH} ${ARCH_REPO_URL} "${tempDir}"`);
|
|
546
|
+
return tempDir;
|
|
547
|
+
}
|
|
548
|
+
catch (error) {
|
|
549
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
550
|
+
throw new Error(`拉取远端模板仓库失败:${this.toErrorMessage(error)}`);
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
git(cwd, command) {
|
|
554
|
+
return execSync(command, {
|
|
555
|
+
cwd,
|
|
556
|
+
encoding: 'utf-8',
|
|
557
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
558
|
+
});
|
|
559
|
+
}
|
|
560
|
+
hasGitChanges(repoDir) {
|
|
561
|
+
try {
|
|
562
|
+
const output = this.git(repoDir, 'git status --porcelain');
|
|
563
|
+
return output.trim().length > 0;
|
|
564
|
+
}
|
|
565
|
+
catch {
|
|
566
|
+
return false;
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
async copyDirectoryContentsIfExists(sourceDir, targetDir) {
|
|
570
|
+
try {
|
|
571
|
+
const stat = await fs.stat(sourceDir);
|
|
572
|
+
if (!stat.isDirectory()) {
|
|
573
|
+
return false;
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
catch {
|
|
577
|
+
return false;
|
|
578
|
+
}
|
|
579
|
+
await fs.mkdir(targetDir, { recursive: true });
|
|
580
|
+
const entries = await fs.readdir(sourceDir, { withFileTypes: true });
|
|
581
|
+
for (const entry of entries) {
|
|
582
|
+
const src = path.join(sourceDir, entry.name);
|
|
583
|
+
const dest = path.join(targetDir, entry.name);
|
|
584
|
+
if (entry.isDirectory()) {
|
|
585
|
+
await this.copyDirectoryRecursive(src, dest);
|
|
586
|
+
}
|
|
587
|
+
else {
|
|
588
|
+
await fs.copyFile(src, dest);
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
return true;
|
|
592
|
+
}
|
|
593
|
+
async mirrorDirectory(sourceDir, targetDir) {
|
|
594
|
+
const sourceStat = await fs.stat(sourceDir);
|
|
595
|
+
if (!sourceStat.isDirectory()) {
|
|
596
|
+
throw new Error(`源目录不是目录:${sourceDir}`);
|
|
597
|
+
}
|
|
598
|
+
await fs.rm(targetDir, { recursive: true, force: true });
|
|
599
|
+
await fs.mkdir(path.dirname(targetDir), { recursive: true });
|
|
600
|
+
await fs.cp(sourceDir, targetDir, { recursive: true, force: true });
|
|
601
|
+
}
|
|
602
|
+
findFirstDirectoryByName(searchRoot, targetName) {
|
|
603
|
+
if (!existsSync(searchRoot)) {
|
|
604
|
+
return null;
|
|
605
|
+
}
|
|
606
|
+
const queue = [searchRoot];
|
|
607
|
+
const targetLower = targetName.toLowerCase();
|
|
608
|
+
while (queue.length > 0) {
|
|
609
|
+
const current = queue.shift();
|
|
610
|
+
if (!current) {
|
|
611
|
+
continue;
|
|
612
|
+
}
|
|
613
|
+
try {
|
|
614
|
+
const children = readdirSync(current, { withFileTypes: true })
|
|
615
|
+
.filter((entry) => entry.isDirectory())
|
|
616
|
+
.map((entry) => entry.name)
|
|
617
|
+
.sort((a, b) => a.localeCompare(b));
|
|
618
|
+
for (const child of children) {
|
|
619
|
+
const childPath = path.join(current, child);
|
|
620
|
+
if (child.toLowerCase() === targetLower) {
|
|
621
|
+
return childPath;
|
|
622
|
+
}
|
|
623
|
+
queue.push(childPath);
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
catch {
|
|
627
|
+
// Continue searching.
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
return null;
|
|
631
|
+
}
|
|
632
|
+
resolveBusinessTemplateRoot(businessRoot) {
|
|
633
|
+
if (!existsSync(businessRoot)) {
|
|
634
|
+
return null;
|
|
635
|
+
}
|
|
636
|
+
// Business directory selection is recursive (first match).
|
|
637
|
+
// Once selected, template root must be its direct child: <business>/zhuanspec.
|
|
638
|
+
const zhuanspecRoot = path.join(businessRoot, 'zhuanspec');
|
|
639
|
+
if (!existsSync(zhuanspecRoot)) {
|
|
640
|
+
return null;
|
|
641
|
+
}
|
|
642
|
+
const specsDir = path.join(zhuanspecRoot, 'specs');
|
|
643
|
+
const changesDir = path.join(zhuanspecRoot, 'changes');
|
|
644
|
+
if (!existsSync(specsDir) || !existsSync(changesDir)) {
|
|
645
|
+
return null;
|
|
646
|
+
}
|
|
647
|
+
return zhuanspecRoot;
|
|
648
|
+
}
|
|
649
|
+
resolveRemoteBusinessRoot(tempRepo, businessDirection) {
|
|
650
|
+
const candidates = Array.from(new Set([
|
|
651
|
+
path.join(tempRepo, 'spec_repo', 'specs'),
|
|
652
|
+
path.join(tempRepo, 'specs'),
|
|
653
|
+
]));
|
|
654
|
+
for (const specsRoot of candidates) {
|
|
655
|
+
if (!existsSync(specsRoot)) {
|
|
656
|
+
continue;
|
|
657
|
+
}
|
|
658
|
+
const businessRoot = this.findFirstDirectoryByName(specsRoot, businessDirection);
|
|
659
|
+
if (businessRoot) {
|
|
660
|
+
return { specsRoot, businessRoot };
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
return null;
|
|
664
|
+
}
|
|
665
|
+
toErrorMessage(error) {
|
|
666
|
+
if (error instanceof Error) {
|
|
667
|
+
return error.message;
|
|
668
|
+
}
|
|
669
|
+
return String(error);
|
|
670
|
+
}
|
|
437
671
|
}
|
|
438
672
|
//# sourceMappingURL=archive.js.map
|
package/dist/core/init.d.ts
CHANGED
|
@@ -48,6 +48,11 @@ export declare class InitCommand {
|
|
|
48
48
|
private fetchArchitectureFile;
|
|
49
49
|
private fetchRemoteArchitectureFiles;
|
|
50
50
|
private fetchRemoteArchitectureContent;
|
|
51
|
+
private cloneRemoteArchitectureRepoToTemp;
|
|
52
|
+
private syncBusinessSpecTemplate;
|
|
53
|
+
private findFirstDirectoryByName;
|
|
54
|
+
private findFirstTemplateSourceRoot;
|
|
55
|
+
private copyDirectoryContentsIfExists;
|
|
51
56
|
private writeTemplateFiles;
|
|
52
57
|
private configureAITools;
|
|
53
58
|
private configureRootAgentsStub;
|
package/dist/core/init.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import path from 'path';
|
|
2
2
|
import os from 'os';
|
|
3
|
-
import { existsSync, mkdtempSync, readdirSync, readFileSync, rmSync } from 'fs';
|
|
3
|
+
import { cpSync, existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync, } from 'fs';
|
|
4
4
|
import { createPrompt, isBackspaceKey, isDownKey, isEnterKey, isSpaceKey, isUpKey, useKeypress, usePagination, useState, } from '@inquirer/core';
|
|
5
5
|
import chalk from 'chalk';
|
|
6
6
|
import ora from 'ora';
|
|
@@ -297,6 +297,8 @@ export class InitCommand {
|
|
|
297
297
|
// Step 3: Select business direction and apply architecture file
|
|
298
298
|
await this.promptForBusinessDirection();
|
|
299
299
|
if (this.businessDirection) {
|
|
300
|
+
const directionMetaPath = path.join(zhuanspecPath, '.business-direction');
|
|
301
|
+
await FileSystemUtils.writeFile(directionMetaPath, `${this.businessDirection.toLowerCase()}\n`);
|
|
300
302
|
const archSpinner = this.startSpinner(`正在获取 ${this.businessDirection} 业务架构文档...`);
|
|
301
303
|
const architectureContent = await this.fetchArchitectureFile();
|
|
302
304
|
if (architectureContent) {
|
|
@@ -316,6 +318,27 @@ export class InitCommand {
|
|
|
316
318
|
text: PALETTE.midGray(`未找到 ${this.businessDirection} 业务架构文档,使用默认模板`),
|
|
317
319
|
});
|
|
318
320
|
}
|
|
321
|
+
const specTemplateSpinner = this.startSpinner(`正在同步 ${this.businessDirection} 业务规范模板...`);
|
|
322
|
+
const syncResult = this.syncBusinessSpecTemplate(zhuanspecPath);
|
|
323
|
+
if (syncResult.syncedSpecs || syncResult.syncedChanges) {
|
|
324
|
+
const syncedParts = [];
|
|
325
|
+
if (syncResult.syncedSpecs) {
|
|
326
|
+
syncedParts.push('specs');
|
|
327
|
+
}
|
|
328
|
+
if (syncResult.syncedChanges) {
|
|
329
|
+
syncedParts.push('changes');
|
|
330
|
+
}
|
|
331
|
+
specTemplateSpinner.stopAndPersist({
|
|
332
|
+
symbol: PALETTE.white('▌'),
|
|
333
|
+
text: PALETTE.white(`已从远端模板同步 ${syncedParts.join('、')} 目录`),
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
else {
|
|
337
|
+
specTemplateSpinner.stopAndPersist({
|
|
338
|
+
symbol: PALETTE.midGray('▌'),
|
|
339
|
+
text: PALETTE.midGray(`未找到 ${this.businessDirection} 业务规范模板目录,保留默认模板`),
|
|
340
|
+
});
|
|
341
|
+
}
|
|
319
342
|
}
|
|
320
343
|
// Success message
|
|
321
344
|
this.displaySuccessMessage(selectedTools, created, refreshed, skippedExisting, skipped, extendMode, rootStubStatus);
|
|
@@ -679,9 +702,11 @@ export class InitCommand {
|
|
|
679
702
|
return null;
|
|
680
703
|
}
|
|
681
704
|
fetchRemoteArchitectureFiles() {
|
|
682
|
-
const tempDir =
|
|
705
|
+
const tempDir = this.cloneRemoteArchitectureRepoToTemp();
|
|
706
|
+
if (!tempDir) {
|
|
707
|
+
return [];
|
|
708
|
+
}
|
|
683
709
|
try {
|
|
684
|
-
execSync(`git clone --depth 1 --single-branch --branch ${ARCH_REPO_BRANCH} ${ARCH_REPO_URL} "${tempDir}"`, { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'ignore'] });
|
|
685
710
|
const commonDir = path.join(tempDir, 'specs', 'common');
|
|
686
711
|
if (!existsSync(commonDir)) {
|
|
687
712
|
return [];
|
|
@@ -696,9 +721,11 @@ export class InitCommand {
|
|
|
696
721
|
}
|
|
697
722
|
}
|
|
698
723
|
fetchRemoteArchitectureContent(fileName) {
|
|
699
|
-
const tempDir =
|
|
724
|
+
const tempDir = this.cloneRemoteArchitectureRepoToTemp();
|
|
725
|
+
if (!tempDir) {
|
|
726
|
+
return null;
|
|
727
|
+
}
|
|
700
728
|
try {
|
|
701
|
-
execSync(`git clone --depth 1 --single-branch --branch ${ARCH_REPO_BRANCH} ${ARCH_REPO_URL} "${tempDir}"`, { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'ignore'] });
|
|
702
729
|
const fullPath = path.join(tempDir, 'specs', 'common', fileName);
|
|
703
730
|
if (!existsSync(fullPath)) {
|
|
704
731
|
return null;
|
|
@@ -712,6 +739,117 @@ export class InitCommand {
|
|
|
712
739
|
rmSync(tempDir, { recursive: true, force: true });
|
|
713
740
|
}
|
|
714
741
|
}
|
|
742
|
+
cloneRemoteArchitectureRepoToTemp() {
|
|
743
|
+
const tempDir = mkdtempSync(path.join(os.tmpdir(), 'zhuanspec-arch-'));
|
|
744
|
+
try {
|
|
745
|
+
execSync(`git clone --depth 1 --single-branch --branch ${ARCH_REPO_BRANCH} ${ARCH_REPO_URL} "${tempDir}"`, { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'ignore'] });
|
|
746
|
+
return tempDir;
|
|
747
|
+
}
|
|
748
|
+
catch {
|
|
749
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
750
|
+
return null;
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
syncBusinessSpecTemplate(zhuanspecPath) {
|
|
754
|
+
if (!this.businessDirection) {
|
|
755
|
+
return { syncedSpecs: false, syncedChanges: false };
|
|
756
|
+
}
|
|
757
|
+
const tempDir = this.cloneRemoteArchitectureRepoToTemp();
|
|
758
|
+
if (!tempDir) {
|
|
759
|
+
return { syncedSpecs: false, syncedChanges: false };
|
|
760
|
+
}
|
|
761
|
+
try {
|
|
762
|
+
const specsRoot = path.join(tempDir, 'specs');
|
|
763
|
+
const businessRoot = this.findFirstDirectoryByName(specsRoot, this.businessDirection);
|
|
764
|
+
if (!businessRoot) {
|
|
765
|
+
return { syncedSpecs: false, syncedChanges: false };
|
|
766
|
+
}
|
|
767
|
+
const templateRoot = this.findFirstTemplateSourceRoot(businessRoot);
|
|
768
|
+
if (!templateRoot) {
|
|
769
|
+
return { syncedSpecs: false, syncedChanges: false };
|
|
770
|
+
}
|
|
771
|
+
const syncedSpecs = this.copyDirectoryContentsIfExists(path.join(templateRoot, 'specs'), path.join(zhuanspecPath, 'specs'));
|
|
772
|
+
const syncedChanges = this.copyDirectoryContentsIfExists(path.join(templateRoot, 'changes'), path.join(zhuanspecPath, 'changes'));
|
|
773
|
+
return { syncedSpecs, syncedChanges };
|
|
774
|
+
}
|
|
775
|
+
catch {
|
|
776
|
+
return { syncedSpecs: false, syncedChanges: false };
|
|
777
|
+
}
|
|
778
|
+
finally {
|
|
779
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
findFirstDirectoryByName(searchRoot, targetName) {
|
|
783
|
+
if (!existsSync(searchRoot)) {
|
|
784
|
+
return null;
|
|
785
|
+
}
|
|
786
|
+
const queue = [searchRoot];
|
|
787
|
+
while (queue.length > 0) {
|
|
788
|
+
const current = queue.shift();
|
|
789
|
+
if (!current) {
|
|
790
|
+
continue;
|
|
791
|
+
}
|
|
792
|
+
try {
|
|
793
|
+
const children = readdirSync(current, { withFileTypes: true })
|
|
794
|
+
.filter((entry) => entry.isDirectory())
|
|
795
|
+
.map((entry) => entry.name)
|
|
796
|
+
.sort((a, b) => a.localeCompare(b));
|
|
797
|
+
for (const child of children) {
|
|
798
|
+
const childPath = path.join(current, child);
|
|
799
|
+
if (child === targetName) {
|
|
800
|
+
return childPath;
|
|
801
|
+
}
|
|
802
|
+
queue.push(childPath);
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
catch {
|
|
806
|
+
// Continue searching other branches when a directory cannot be read.
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
return null;
|
|
810
|
+
}
|
|
811
|
+
findFirstTemplateSourceRoot(searchRoot) {
|
|
812
|
+
if (!existsSync(searchRoot)) {
|
|
813
|
+
return null;
|
|
814
|
+
}
|
|
815
|
+
const queue = [searchRoot];
|
|
816
|
+
while (queue.length > 0) {
|
|
817
|
+
const current = queue.shift();
|
|
818
|
+
if (!current) {
|
|
819
|
+
continue;
|
|
820
|
+
}
|
|
821
|
+
const specsDir = path.join(current, 'specs');
|
|
822
|
+
const changesDir = path.join(current, 'changes');
|
|
823
|
+
if (existsSync(specsDir) && existsSync(changesDir)) {
|
|
824
|
+
return current;
|
|
825
|
+
}
|
|
826
|
+
try {
|
|
827
|
+
const children = readdirSync(current, { withFileTypes: true })
|
|
828
|
+
.filter((entry) => entry.isDirectory())
|
|
829
|
+
.map((entry) => entry.name)
|
|
830
|
+
.sort((a, b) => a.localeCompare(b));
|
|
831
|
+
children.forEach((child) => queue.push(path.join(current, child)));
|
|
832
|
+
}
|
|
833
|
+
catch {
|
|
834
|
+
// Continue searching other branches when a directory cannot be read.
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
return null;
|
|
838
|
+
}
|
|
839
|
+
copyDirectoryContentsIfExists(sourceDir, targetDir) {
|
|
840
|
+
if (!existsSync(sourceDir)) {
|
|
841
|
+
return false;
|
|
842
|
+
}
|
|
843
|
+
mkdirSync(targetDir, { recursive: true });
|
|
844
|
+
const entries = readdirSync(sourceDir);
|
|
845
|
+
for (const entry of entries) {
|
|
846
|
+
cpSync(path.join(sourceDir, entry), path.join(targetDir, entry), {
|
|
847
|
+
recursive: true,
|
|
848
|
+
force: true,
|
|
849
|
+
});
|
|
850
|
+
}
|
|
851
|
+
return true;
|
|
852
|
+
}
|
|
715
853
|
async writeTemplateFiles(zhuanspecPath, config, skipExisting) {
|
|
716
854
|
const context = {};
|
|
717
855
|
const templates = TemplateManager.getTemplates(context);
|
package/dist/core/specs-apply.js
CHANGED
|
@@ -141,14 +141,19 @@ export async function buildUpdatedSpec(update, changeName) {
|
|
|
141
141
|
targetContent = await fs.readFile(update.target, 'utf-8');
|
|
142
142
|
}
|
|
143
143
|
catch {
|
|
144
|
-
// Target spec does not exist
|
|
145
|
-
//
|
|
146
|
-
if (plan.modified.length > 0
|
|
147
|
-
|
|
144
|
+
// Target spec does not exist: process as new spec creation.
|
|
145
|
+
// Only ADDED operations are applied; other operations are ignored.
|
|
146
|
+
if (plan.modified.length > 0) {
|
|
147
|
+
console.log(chalk.yellow(`⚠️ Warning: ${specName} - ${plan.modified.length} MODIFIED requirement(s) ignored for new spec (only ADDED is applied).`));
|
|
148
|
+
plan.modified.length = 0;
|
|
149
|
+
}
|
|
150
|
+
if (plan.renamed.length > 0) {
|
|
151
|
+
console.log(chalk.yellow(`⚠️ Warning: ${specName} - ${plan.renamed.length} RENAMED operation(s) ignored for new spec (only ADDED is applied).`));
|
|
152
|
+
plan.renamed.length = 0;
|
|
148
153
|
}
|
|
149
|
-
// Warn about REMOVED requirements being ignored for new specs
|
|
150
154
|
if (plan.removed.length > 0) {
|
|
151
155
|
console.log(chalk.yellow(`⚠️ Warning: ${specName} - ${plan.removed.length} REMOVED requirement(s) ignored for new spec (nothing to remove).`));
|
|
156
|
+
plan.removed.length = 0;
|
|
152
157
|
}
|
|
153
158
|
isNewSpec = true;
|
|
154
159
|
targetContent = buildSpecSkeleton(specName, changeName);
|
|
@@ -160,6 +165,7 @@ export async function buildUpdatedSpec(update, changeName) {
|
|
|
160
165
|
nameToBlock.set(normalizeRequirementName(block.name), block);
|
|
161
166
|
}
|
|
162
167
|
// Apply operations in order: RENAMED → REMOVED → MODIFIED → ADDED
|
|
168
|
+
let skippedAdded = 0;
|
|
163
169
|
// RENAMED
|
|
164
170
|
for (const r of plan.renamed) {
|
|
165
171
|
const from = normalizeRequirementName(r.from);
|
|
@@ -200,7 +206,10 @@ export async function buildUpdatedSpec(update, changeName) {
|
|
|
200
206
|
for (const mod of plan.modified) {
|
|
201
207
|
const key = normalizeRequirementName(mod.name);
|
|
202
208
|
if (!nameToBlock.has(key)) {
|
|
203
|
-
|
|
209
|
+
// Lenient mode: if MODIFIED target is missing, treat it as ADDED.
|
|
210
|
+
console.log(chalk.yellow(`⚠️ Warning: ${specName} MODIFIED target not found for "### Requirement: ${mod.name}", auto-applying as ADDED.`));
|
|
211
|
+
nameToBlock.set(key, mod);
|
|
212
|
+
continue;
|
|
204
213
|
}
|
|
205
214
|
// Replace block with provided raw (ensure header line matches key)
|
|
206
215
|
const modHeaderMatch = mod.raw.split('\n')[0].match(/^###\s*Requirement:\s*(.+)\s*$/);
|
|
@@ -213,7 +222,9 @@ export async function buildUpdatedSpec(update, changeName) {
|
|
|
213
222
|
for (const add of plan.added) {
|
|
214
223
|
const key = normalizeRequirementName(add.name);
|
|
215
224
|
if (nameToBlock.has(key)) {
|
|
216
|
-
|
|
225
|
+
console.log(chalk.yellow(`⚠️ Warning: ${specName} ADDED target already exists for "### Requirement: ${add.name}", skipping.`));
|
|
226
|
+
skippedAdded++;
|
|
227
|
+
continue;
|
|
217
228
|
}
|
|
218
229
|
nameToBlock.set(key, add);
|
|
219
230
|
}
|
|
@@ -247,7 +258,7 @@ export async function buildUpdatedSpec(update, changeName) {
|
|
|
247
258
|
return {
|
|
248
259
|
rebuilt,
|
|
249
260
|
counts: {
|
|
250
|
-
added: plan.added.length,
|
|
261
|
+
added: plan.added.length - skippedAdded,
|
|
251
262
|
modified: plan.modified.length,
|
|
252
263
|
removed: plan.removed.length,
|
|
253
264
|
renamed: plan.renamed.length,
|