create-yss-spec 1.0.0 → 1.1.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/README.md +25 -1
- package/package.json +1 -1
- package/src/cli.js +324 -4
- package/template/CONTEXT.md +3 -0
- package/template/docs/user-guide/create-yss-spec-cli-guide.md +77 -2
- package/template.manifest.json +5 -0
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# create-yss-spec
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
用于初始化和同步 `yss-spec-project-template` 模板实例仓库的 npm CLI。
|
|
4
4
|
|
|
5
5
|
## 用法
|
|
6
6
|
|
|
@@ -24,6 +24,30 @@ npx create-yss-spec@latest
|
|
|
24
24
|
- `--issue-tracker github|gitlab`
|
|
25
25
|
- `--include-example-docs`
|
|
26
26
|
- `--no-example-docs`
|
|
27
|
+
- `sync` 子命令
|
|
28
|
+
- 基于 `.yss-template.json` 的模板版本基线
|
|
29
|
+
- 已有模板实例仓库的受管模板资产同步
|
|
30
|
+
|
|
31
|
+
## 同步已有模板实例仓库
|
|
32
|
+
|
|
33
|
+
当项目仓库已经由 `create-yss-spec` 初始化,并且根目录存在 `.yss-template.json` 时,可以执行:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
npx create-yss-spec@latest sync
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
只预演,不真实写入:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
npx create-yss-spec@latest sync --dry-run
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
当前同步能力的边界:
|
|
46
|
+
|
|
47
|
+
- 只支持带模板元数据的模板实例仓库
|
|
48
|
+
- 默认只更新未被本地修改的受管模板文件
|
|
49
|
+
- 对本地已修改文件只提示和跳过,不自动覆盖
|
|
50
|
+
- 对模板已删除文件只报告,不自动删除
|
|
27
51
|
|
|
28
52
|
## 开发验证
|
|
29
53
|
|
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
const fs = require("node:fs");
|
|
2
2
|
const path = require("node:path");
|
|
3
3
|
const readline = require("node:readline/promises");
|
|
4
|
+
const crypto = require("node:crypto");
|
|
4
5
|
const { spawnSync } = require("node:child_process");
|
|
5
6
|
|
|
6
7
|
const PACKAGE_ROOT = path.resolve(__dirname, "..");
|
|
8
|
+
const PACKAGE_MANIFEST = JSON.parse(
|
|
9
|
+
fs.readFileSync(path.join(PACKAGE_ROOT, "package.json"), "utf8"),
|
|
10
|
+
);
|
|
7
11
|
const BUNDLED_TEMPLATE_ROOT = path.join(PACKAGE_ROOT, "template");
|
|
8
12
|
const REPO_TEMPLATE_ROOT = path.resolve(PACKAGE_ROOT, "../..");
|
|
9
13
|
const REPO_MANIFEST_PATH = path.join(REPO_TEMPLATE_ROOT, "template.manifest.json");
|
|
@@ -17,18 +21,33 @@ const TEMPLATE_ROOT = IS_REPO_DEVELOPMENT
|
|
|
17
21
|
const TEMPLATE_MANIFEST_PATH = IS_REPO_DEVELOPMENT
|
|
18
22
|
? REPO_MANIFEST_PATH
|
|
19
23
|
: BUNDLED_MANIFEST_PATH;
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
);
|
|
24
|
+
const TEMPLATE_MANIFEST_TEXT = fs.readFileSync(TEMPLATE_MANIFEST_PATH, "utf8");
|
|
25
|
+
const TEMPLATE_MANIFEST = JSON.parse(TEMPLATE_MANIFEST_TEXT);
|
|
23
26
|
const ROOT_EXCLUDED_ENTRIES = new Set(TEMPLATE_MANIFEST.excludeRootEntries);
|
|
24
27
|
const ROOT_EXCLUDED_FILES = new Set(TEMPLATE_MANIFEST.excludeRootFiles);
|
|
25
28
|
const EXCLUDED_RELATIVE_PATHS = new Set(TEMPLATE_MANIFEST.excludePaths);
|
|
26
29
|
const RENDERED_RELATIVE_PATHS = new Set(TEMPLATE_MANIFEST.renderPaths);
|
|
27
30
|
const EXAMPLE_DOC_PATHS = new Set(TEMPLATE_MANIFEST.exampleDocPaths);
|
|
31
|
+
const TEMPLATE_METADATA_FILENAME = ".yss-template.json";
|
|
32
|
+
const TEMPLATE_MANIFEST_VERSION = sha256(TEMPLATE_MANIFEST_TEXT);
|
|
28
33
|
const REPO_TRACKED_STATE = IS_REPO_DEVELOPMENT
|
|
29
34
|
? loadRepoTrackedState(REPO_TEMPLATE_ROOT)
|
|
30
35
|
: null;
|
|
31
36
|
|
|
37
|
+
function sha256(value) {
|
|
38
|
+
return crypto.createHash("sha256").update(value).digest("hex");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function nowIsoString() {
|
|
42
|
+
return new Date().toISOString();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function getTemplateSource() {
|
|
46
|
+
return IS_REPO_DEVELOPMENT
|
|
47
|
+
? "repo-development"
|
|
48
|
+
: `npm:${PACKAGE_MANIFEST.name}@${PACKAGE_MANIFEST.version}`;
|
|
49
|
+
}
|
|
50
|
+
|
|
32
51
|
function loadRepoTrackedState(repoRoot) {
|
|
33
52
|
const result = spawnSync("git", ["ls-files"], {
|
|
34
53
|
cwd: repoRoot,
|
|
@@ -259,6 +278,49 @@ function renderTemplateFile(relativePath, content, variables) {
|
|
|
259
278
|
return content;
|
|
260
279
|
}
|
|
261
280
|
|
|
281
|
+
function collectManagedFileHashes(operations) {
|
|
282
|
+
const managedFiles = {};
|
|
283
|
+
|
|
284
|
+
for (const operation of operations) {
|
|
285
|
+
if (operation.type !== "copy" && operation.type !== "render") {
|
|
286
|
+
continue;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
managedFiles[operation.relativePath] = {
|
|
290
|
+
type: operation.type,
|
|
291
|
+
contentHash: sha256(fs.readFileSync(operation.targetPath)),
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
return managedFiles;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function writeTemplateMetadata(targetDir, metadata) {
|
|
299
|
+
const metadataPath = path.join(targetDir, TEMPLATE_METADATA_FILENAME);
|
|
300
|
+
fs.writeFileSync(metadataPath, `${JSON.stringify(metadata, null, 2)}\n`, "utf8");
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function buildTemplateMetadata(targetDir, variables, operations) {
|
|
304
|
+
const timestamp = nowIsoString();
|
|
305
|
+
|
|
306
|
+
return {
|
|
307
|
+
templateName: PACKAGE_MANIFEST.name,
|
|
308
|
+
templateVersion: PACKAGE_MANIFEST.version,
|
|
309
|
+
templateSource: getTemplateSource(),
|
|
310
|
+
initializedAt: timestamp,
|
|
311
|
+
lastSyncedAt: timestamp,
|
|
312
|
+
managedFilesManifestVersion: TEMPLATE_MANIFEST_VERSION,
|
|
313
|
+
variables: {
|
|
314
|
+
projectName: variables.projectName,
|
|
315
|
+
businessDomain: variables.businessDomain,
|
|
316
|
+
teamSize: variables.teamSize,
|
|
317
|
+
issueTracker: variables.issueTracker,
|
|
318
|
+
includeExampleDocs: variables.includeExampleDocs,
|
|
319
|
+
},
|
|
320
|
+
managedFiles: collectManagedFileHashes(operations),
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
|
|
262
324
|
function buildCopyPlan(sourceDir, targetDir, variables, relativeDir = "") {
|
|
263
325
|
const operations = [];
|
|
264
326
|
const entries = fs.readdirSync(sourceDir, { withFileTypes: true });
|
|
@@ -405,7 +467,245 @@ function initializeGitRepository(targetDir) {
|
|
|
405
467
|
}
|
|
406
468
|
}
|
|
407
469
|
|
|
408
|
-
|
|
470
|
+
function loadTemplateMetadata(targetDir) {
|
|
471
|
+
const metadataPath = path.join(targetDir, TEMPLATE_METADATA_FILENAME);
|
|
472
|
+
|
|
473
|
+
if (!fs.existsSync(metadataPath)) {
|
|
474
|
+
throw new Error(
|
|
475
|
+
`当前目录不是受支持的模板实例仓库,缺少模板元数据文件 ${TEMPLATE_METADATA_FILENAME}`,
|
|
476
|
+
);
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
return {
|
|
480
|
+
metadataPath,
|
|
481
|
+
metadata: JSON.parse(fs.readFileSync(metadataPath, "utf8")),
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
function buildSyncVariables(metadata) {
|
|
486
|
+
const variables = metadata.variables || {};
|
|
487
|
+
|
|
488
|
+
return {
|
|
489
|
+
projectName: variables.projectName,
|
|
490
|
+
businessDomain: variables.businessDomain,
|
|
491
|
+
teamSize: variables.teamSize || "待补充",
|
|
492
|
+
issueTracker: variables.issueTracker || "github",
|
|
493
|
+
includeExampleDocs:
|
|
494
|
+
variables.includeExampleDocs === undefined
|
|
495
|
+
? true
|
|
496
|
+
: Boolean(variables.includeExampleDocs),
|
|
497
|
+
};
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
function buildDesiredManagedOperations(targetDir, metadata) {
|
|
501
|
+
const variables = buildSyncVariables(metadata);
|
|
502
|
+
|
|
503
|
+
return buildCopyPlan(TEMPLATE_ROOT, targetDir, variables).filter(
|
|
504
|
+
(operation) => operation.type === "copy" || operation.type === "render",
|
|
505
|
+
);
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
function buildDesiredManagedFile(operation, metadata) {
|
|
509
|
+
const variables = buildSyncVariables(metadata);
|
|
510
|
+
|
|
511
|
+
if (operation.type === "render") {
|
|
512
|
+
const renderedContent = renderTemplateFile(
|
|
513
|
+
operation.relativePath,
|
|
514
|
+
fs.readFileSync(operation.sourcePath, "utf8"),
|
|
515
|
+
variables,
|
|
516
|
+
);
|
|
517
|
+
|
|
518
|
+
return {
|
|
519
|
+
...operation,
|
|
520
|
+
desiredContent: renderedContent,
|
|
521
|
+
desiredHash: sha256(renderedContent),
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
return {
|
|
526
|
+
...operation,
|
|
527
|
+
desiredHash: sha256(fs.readFileSync(operation.sourcePath)),
|
|
528
|
+
};
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
function classifySyncPlan(targetDir, metadata) {
|
|
532
|
+
const managedFiles = metadata.managedFiles || {};
|
|
533
|
+
const desiredOperations = buildDesiredManagedOperations(targetDir, metadata).map(
|
|
534
|
+
(operation) => buildDesiredManagedFile(operation, metadata),
|
|
535
|
+
);
|
|
536
|
+
const desiredPathSet = new Set(
|
|
537
|
+
desiredOperations.map((operation) => operation.relativePath),
|
|
538
|
+
);
|
|
539
|
+
|
|
540
|
+
const updated = [];
|
|
541
|
+
const added = [];
|
|
542
|
+
const unchanged = [];
|
|
543
|
+
const skipped = [];
|
|
544
|
+
|
|
545
|
+
for (const operation of desiredOperations) {
|
|
546
|
+
const existingRecord = managedFiles[operation.relativePath];
|
|
547
|
+
const existsOnDisk = fs.existsSync(operation.targetPath);
|
|
548
|
+
|
|
549
|
+
if (!existingRecord) {
|
|
550
|
+
if (!existsOnDisk) {
|
|
551
|
+
added.push(operation);
|
|
552
|
+
continue;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
const currentHash = sha256(fs.readFileSync(operation.targetPath));
|
|
556
|
+
if (currentHash === operation.desiredHash) {
|
|
557
|
+
unchanged.push(operation);
|
|
558
|
+
} else {
|
|
559
|
+
skipped.push({
|
|
560
|
+
...operation,
|
|
561
|
+
reason: "文件已存在,但不在受管模板文件基线中",
|
|
562
|
+
});
|
|
563
|
+
}
|
|
564
|
+
continue;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
if (!existsOnDisk) {
|
|
568
|
+
added.push(operation);
|
|
569
|
+
continue;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
const currentHash = sha256(fs.readFileSync(operation.targetPath));
|
|
573
|
+
if (currentHash !== existingRecord.contentHash) {
|
|
574
|
+
skipped.push({
|
|
575
|
+
...operation,
|
|
576
|
+
reason: "检测到本地已修改的受管文件",
|
|
577
|
+
});
|
|
578
|
+
continue;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
if (currentHash === operation.desiredHash) {
|
|
582
|
+
unchanged.push(operation);
|
|
583
|
+
continue;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
updated.push(operation);
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
const removed = Object.keys(managedFiles).filter(
|
|
590
|
+
(relativePath) => !desiredPathSet.has(relativePath),
|
|
591
|
+
);
|
|
592
|
+
|
|
593
|
+
return {
|
|
594
|
+
updated,
|
|
595
|
+
added,
|
|
596
|
+
unchanged,
|
|
597
|
+
skipped,
|
|
598
|
+
removed,
|
|
599
|
+
desiredOperations,
|
|
600
|
+
};
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
function printSyncDryRun(targetDir, metadata, syncPlan) {
|
|
604
|
+
console.log("sync dry-run 预览");
|
|
605
|
+
console.log(`目标目录:${targetDir}`);
|
|
606
|
+
console.log(
|
|
607
|
+
`模板版本:${metadata.templateVersion || "unknown"} -> ${PACKAGE_MANIFEST.version}`,
|
|
608
|
+
);
|
|
609
|
+
|
|
610
|
+
for (const operation of syncPlan.updated) {
|
|
611
|
+
console.log(`update: ${operation.relativePath}`);
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
for (const operation of syncPlan.added) {
|
|
615
|
+
console.log(`add: ${operation.relativePath}`);
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
for (const operation of syncPlan.skipped) {
|
|
619
|
+
console.log(`skip: ${operation.relativePath} (${operation.reason})`);
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
for (const relativePath of syncPlan.removed) {
|
|
623
|
+
console.log(`remove-report: ${relativePath}`);
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
function applyManagedFileOperation(operation) {
|
|
628
|
+
fs.mkdirSync(path.dirname(operation.targetPath), { recursive: true });
|
|
629
|
+
|
|
630
|
+
if (operation.type === "render") {
|
|
631
|
+
fs.writeFileSync(operation.targetPath, operation.desiredContent, "utf8");
|
|
632
|
+
return;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
fs.copyFileSync(operation.sourcePath, operation.targetPath);
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
function syncTemplateInstance(targetDir, metadata, dryRun) {
|
|
639
|
+
const syncPlan = classifySyncPlan(targetDir, metadata);
|
|
640
|
+
|
|
641
|
+
if (dryRun) {
|
|
642
|
+
printSyncDryRun(targetDir, metadata, syncPlan);
|
|
643
|
+
return;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
for (const operation of [...syncPlan.updated, ...syncPlan.added]) {
|
|
647
|
+
applyManagedFileOperation(operation);
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
const nextManagedFiles = { ...(metadata.managedFiles || {}) };
|
|
651
|
+
for (const operation of syncPlan.desiredOperations) {
|
|
652
|
+
if (!fs.existsSync(operation.targetPath)) {
|
|
653
|
+
continue;
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
const currentHash = sha256(fs.readFileSync(operation.targetPath));
|
|
657
|
+
if (currentHash !== operation.desiredHash) {
|
|
658
|
+
continue;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
nextManagedFiles[operation.relativePath] = {
|
|
662
|
+
type: operation.type,
|
|
663
|
+
contentHash: operation.desiredHash,
|
|
664
|
+
};
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
const nextMetadata = {
|
|
668
|
+
...metadata,
|
|
669
|
+
templateName: PACKAGE_MANIFEST.name,
|
|
670
|
+
templateVersion: PACKAGE_MANIFEST.version,
|
|
671
|
+
templateSource: getTemplateSource(),
|
|
672
|
+
lastSyncedAt: nowIsoString(),
|
|
673
|
+
managedFilesManifestVersion: TEMPLATE_MANIFEST_VERSION,
|
|
674
|
+
managedFiles: nextManagedFiles,
|
|
675
|
+
};
|
|
676
|
+
|
|
677
|
+
writeTemplateMetadata(targetDir, nextMetadata);
|
|
678
|
+
|
|
679
|
+
console.log("同步完成");
|
|
680
|
+
console.log(
|
|
681
|
+
`模板版本:${metadata.templateVersion || "unknown"} -> ${PACKAGE_MANIFEST.version}`,
|
|
682
|
+
);
|
|
683
|
+
console.log(`自动更新:${syncPlan.updated.length}`);
|
|
684
|
+
console.log(`新增文件:${syncPlan.added.length}`);
|
|
685
|
+
console.log(`跳过文件:${syncPlan.skipped.length}`);
|
|
686
|
+
console.log(`删除差异:${syncPlan.removed.length}`);
|
|
687
|
+
|
|
688
|
+
if (syncPlan.skipped.length > 0) {
|
|
689
|
+
console.log("本地已修改,已跳过:");
|
|
690
|
+
for (const operation of syncPlan.skipped) {
|
|
691
|
+
console.log(`- ${operation.relativePath}: ${operation.reason}`);
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
if (syncPlan.removed.length > 0) {
|
|
696
|
+
console.log("模板已移除但未自动删除:");
|
|
697
|
+
for (const relativePath of syncPlan.removed) {
|
|
698
|
+
console.log(`- ${relativePath}`);
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
console.log("下一步建议:");
|
|
703
|
+
console.log("1. 运行 git diff 或 git status 检查同步结果");
|
|
704
|
+
console.log("2. 人工处理被跳过文件和删除差异(如有)");
|
|
705
|
+
console.log("3. 确认无误后提交本次模板同步结果");
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
async function runInit(argv = []) {
|
|
409
709
|
const promptedOptions = await promptForMissingOptions(parseArgs(argv));
|
|
410
710
|
assertRequiredOptions(promptedOptions);
|
|
411
711
|
|
|
@@ -421,6 +721,10 @@ async function runCli(argv = []) {
|
|
|
421
721
|
|
|
422
722
|
prepareTargetDir(targetDir, targetState);
|
|
423
723
|
executePlan(operations, promptedOptions);
|
|
724
|
+
writeTemplateMetadata(
|
|
725
|
+
targetDir,
|
|
726
|
+
buildTemplateMetadata(targetDir, promptedOptions, operations),
|
|
727
|
+
);
|
|
424
728
|
|
|
425
729
|
if (promptedOptions.gitInit) {
|
|
426
730
|
initializeGitRepository(targetDir);
|
|
@@ -438,6 +742,22 @@ async function runCli(argv = []) {
|
|
|
438
742
|
console.log("3. 检查 AGENTS.md、README 和 docs 目录是否符合预期");
|
|
439
743
|
}
|
|
440
744
|
|
|
745
|
+
function runSync(argv = []) {
|
|
746
|
+
const options = parseArgs(argv);
|
|
747
|
+
const targetDir = normalizeTargetDir(options.targetDir || ".");
|
|
748
|
+
const { metadata } = loadTemplateMetadata(targetDir);
|
|
749
|
+
syncTemplateInstance(targetDir, metadata, Boolean(options.dryRun));
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
async function runCli(argv = []) {
|
|
753
|
+
if (argv[0] === "sync") {
|
|
754
|
+
runSync(argv.slice(1));
|
|
755
|
+
return;
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
await runInit(argv);
|
|
759
|
+
}
|
|
760
|
+
|
|
441
761
|
module.exports = {
|
|
442
762
|
runCli,
|
|
443
763
|
};
|
package/template/CONTEXT.md
CHANGED
|
@@ -22,5 +22,8 @@
|
|
|
22
22
|
| 模板源仓库 | 承载 `yss-spec-project-template` 权威模板内容及其演进规则的仓库。 | 不等同于实例化后的具体项目仓库。 |
|
|
23
23
|
| 模板实例仓库 | 由模板初始化后生成、面向某个具体项目使用的仓库。 | 不要与模板源仓库混用。 |
|
|
24
24
|
| 模板初始化 CLI | 用于把模板源仓库实例化为新项目初始仓库的命令行工具。 | 不等同于业务运行时代码脚手架。 |
|
|
25
|
+
| 模板同步 | 将已有模板实例仓库与某个更新后的模板快照进行对齐的过程。 | 不等同于重新初始化;通常要考虑本地改动保留、冲突提示和可回滚性。 |
|
|
26
|
+
| 模板快照版本 | 某次发布时模板内容的可识别版本标识,通常对应 npm 包版本、Git tag 或 commit。 | 需要用于判断实例仓库当前基线和目标同步版本。 |
|
|
27
|
+
| 受管模板文件 | 被 CLI 明确纳入同步策略的文件或目录集合。 | 只有受管文件才应被自动更新、跳过或冲突提示。 |
|
|
25
28
|
|
|
26
29
|
只有在计划、分诊、调试或架构讨论中明确沉淀出稳定语言时,才新增术语。
|
|
@@ -17,7 +17,8 @@
|
|
|
17
17
|
- 生成前端 / 后端运行时代码工程
|
|
18
18
|
- 自动安装依赖
|
|
19
19
|
- 自动创建远端 Git 仓库、CI 或 Issue Board
|
|
20
|
-
-
|
|
20
|
+
- 直接接管没有模板元数据的历史老项目
|
|
21
|
+
- 自动解决本地已修改受管文件的冲突
|
|
21
22
|
|
|
22
23
|
## 快速开始
|
|
23
24
|
|
|
@@ -78,6 +79,22 @@ npx create-yss-spec@latest \
|
|
|
78
79
|
|
|
79
80
|
`--dry-run` 只展示计划,不会创建目录,也不会删除已有文件。
|
|
80
81
|
|
|
82
|
+
### 同步已有模板实例仓库
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
npx create-yss-spec@latest sync
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
适合已经由 `create-yss-spec` 初始化过、并且根目录带有 `.yss-template.json` 的模板实例仓库。
|
|
89
|
+
|
|
90
|
+
### 只预演同步,不真正写入
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
npx create-yss-spec@latest sync --dry-run
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
适合在升级前先查看版本变化、将要更新的文件、将被跳过的本地改动文件,以及模板已删除但不会自动删除的文件。
|
|
97
|
+
|
|
81
98
|
## 参数说明
|
|
82
99
|
|
|
83
100
|
| 参数 | 含义 | 默认行为 |
|
|
@@ -93,6 +110,12 @@ npx create-yss-spec@latest \
|
|
|
93
110
|
| `--include-example-docs` | 显式保留示例文档 | 默认开启 |
|
|
94
111
|
| `--no-example-docs` | 不生成示例文档 | 默认关闭 |
|
|
95
112
|
|
|
113
|
+
`sync` 子命令当前只支持:
|
|
114
|
+
|
|
115
|
+
- 在模板实例仓库根目录执行
|
|
116
|
+
- 仓库内已存在 `.yss-template.json`
|
|
117
|
+
- 以当前 npm 已发布包内置模板快照作为同步源
|
|
118
|
+
|
|
96
119
|
## 输出内容说明
|
|
97
120
|
|
|
98
121
|
CLI 会根据模板清单把源仓库内容分成三类处理:
|
|
@@ -114,6 +137,12 @@ CLI 会根据模板清单把源仓库内容分成三类处理:
|
|
|
114
137
|
|
|
115
138
|
如果启用了 `--git-init`,目标目录下还会生成 `.git/`。
|
|
116
139
|
|
|
140
|
+
初始化完成后,CLI 还会额外生成:
|
|
141
|
+
|
|
142
|
+
- `.yss-template.json`
|
|
143
|
+
|
|
144
|
+
这个文件用于记录模板名称、模板版本、模板来源、最近同步时间、受管模板文件基线和关键渲染变量。后续 `sync` 能否安全工作,依赖这份模板元数据。
|
|
145
|
+
|
|
117
146
|
## 默认安全策略
|
|
118
147
|
|
|
119
148
|
为了避免误覆盖,CLI 采用默认安全策略:
|
|
@@ -123,6 +152,13 @@ CLI 会根据模板清单把源仓库内容分成三类处理:
|
|
|
123
152
|
- 目标目录不能位于模板源仓库内部
|
|
124
153
|
- `--dry-run` 没有副作用
|
|
125
154
|
|
|
155
|
+
对于 `sync`,默认安全策略还包括:
|
|
156
|
+
|
|
157
|
+
- 当前目录缺少 `.yss-template.json` 时,直接拒绝同步
|
|
158
|
+
- 只自动更新未被本地修改的受管模板文件
|
|
159
|
+
- 本地已修改的受管文件会被跳过并报告
|
|
160
|
+
- 模板新版本已删除的受管文件只报告,不自动删除
|
|
161
|
+
|
|
126
162
|
## 示例结果
|
|
127
163
|
|
|
128
164
|
一次典型执行完成后,你会看到类似输出:
|
|
@@ -138,6 +174,25 @@ CLI 会根据模板清单把源仓库内容分成三类处理:
|
|
|
138
174
|
|
|
139
175
|
如果已经传了 `--git-init`,第二步会提示执行 `git status` 检查初始化结果。
|
|
140
176
|
|
|
177
|
+
一次典型同步完成后,你会看到类似输出:
|
|
178
|
+
|
|
179
|
+
```text
|
|
180
|
+
同步完成
|
|
181
|
+
模板版本:0.9.0 -> 1.0.0
|
|
182
|
+
自动更新:2
|
|
183
|
+
新增文件:1
|
|
184
|
+
跳过文件:1
|
|
185
|
+
删除差异:1
|
|
186
|
+
本地已修改,已跳过:
|
|
187
|
+
- README.md: 检测到本地已修改的受管文件
|
|
188
|
+
模板已移除但未自动删除:
|
|
189
|
+
- docs/legacy-note.md
|
|
190
|
+
下一步建议:
|
|
191
|
+
1. 运行 git diff 或 git status 检查同步结果
|
|
192
|
+
2. 人工处理被跳过文件和删除差异(如有)
|
|
193
|
+
3. 确认无误后提交本次模板同步结果
|
|
194
|
+
```
|
|
195
|
+
|
|
141
196
|
## 常见问题
|
|
142
197
|
|
|
143
198
|
### 1. 提示“目标目录非空,当前主路径不支持覆盖已有内容”
|
|
@@ -170,12 +225,32 @@ printf 'Acme Spec Repo\nInvestment Research\n12\n/tmp/acme-spec-repo\n' \
|
|
|
170
225
|
|
|
171
226
|
这是设计上的非目标范围。当前 CLI 只负责初始化研发管理模板实例仓库,组织级权限操作和后续 bootstrap 仍由人工控制。
|
|
172
227
|
|
|
228
|
+
### 5. 为什么 `sync` 提示缺少模板元数据
|
|
229
|
+
|
|
230
|
+
说明当前目录不是受支持的模板实例仓库,或者它是一个早期初始化的历史项目,还没有 `.yss-template.json` 基线。
|
|
231
|
+
|
|
232
|
+
处理方式:
|
|
233
|
+
|
|
234
|
+
- 先确认当前目录是否真的是由 `create-yss-spec` 初始化出来的项目
|
|
235
|
+
- 当前版本的 `sync` 只支持带模板元数据的项目
|
|
236
|
+
- 历史老项目的接管 / attach 不在本版范围内
|
|
237
|
+
|
|
238
|
+
### 6. 为什么 `sync` 没有覆盖我改过的文件
|
|
239
|
+
|
|
240
|
+
这是刻意的默认安全策略。CLI 会把这类文件识别为“本地已修改的受管文件”,只报告、跳过,不自动覆盖。
|
|
241
|
+
|
|
242
|
+
处理方式:
|
|
243
|
+
|
|
244
|
+
- 查看输出中的跳过文件列表
|
|
245
|
+
- 用 `git diff` 比较当前项目版本和模板版本的差异
|
|
246
|
+
- 人工决定是否合并模板变更
|
|
247
|
+
|
|
173
248
|
## 维护与验证
|
|
174
249
|
|
|
175
250
|
如果你在维护这个 CLI,本地验证命令是:
|
|
176
251
|
|
|
177
252
|
```bash
|
|
178
|
-
|
|
253
|
+
node --test packages/create-yss-spec/tests/init-cli.test.js
|
|
179
254
|
```
|
|
180
255
|
|
|
181
256
|
发布前可检查打包内容:
|
package/template.manifest.json
CHANGED
|
@@ -22,13 +22,18 @@
|
|
|
22
22
|
".codex/skills/.DS_Store",
|
|
23
23
|
".pi/settings.json",
|
|
24
24
|
"docs/discovery/yss-spec-cli-init-discovery.md",
|
|
25
|
+
"docs/discovery/yss-spec-cli-template-sync-discovery.md",
|
|
25
26
|
"docs/implementation/yss-spec-cli-init-build-checklist.md",
|
|
26
27
|
"docs/implementation/yss-spec-cli-init-routing.md",
|
|
27
28
|
"docs/requirements/issues/yss-spec-cli-init-slice-01-main-path.md",
|
|
28
29
|
"docs/requirements/issues/yss-spec-cli-init-slice-02-safety-controls.md",
|
|
29
30
|
"docs/requirements/issues/yss-spec-cli-init-slice-03-template-manifest.md",
|
|
30
31
|
"docs/requirements/issues/yss-spec-cli-init-slice-04-delivery-verification.md",
|
|
32
|
+
"docs/requirements/issues/yss-spec-cli-template-sync-slice-01-main-path.md",
|
|
33
|
+
"docs/requirements/issues/yss-spec-cli-template-sync-slice-02-safety-controls.md",
|
|
34
|
+
"docs/requirements/issues/yss-spec-cli-template-sync-slice-03-delivery-verification.md",
|
|
31
35
|
"docs/requirements/yss-spec-cli-init-prd.md",
|
|
36
|
+
"docs/requirements/yss-spec-cli-template-sync-prd.md",
|
|
32
37
|
"scripts/sync-cli-template.js"
|
|
33
38
|
],
|
|
34
39
|
"renderPaths": [
|