create-yss-spec 1.0.0 → 1.1.0

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # create-yss-spec
2
2
 
3
- 用于初始化 `yss-spec-project-template` 模板实例仓库的 npm CLI。
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-yss-spec",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "Initialize a YSS spec project template repository",
5
5
  "files": [
6
6
  "bin",
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 TEMPLATE_MANIFEST = JSON.parse(
21
- fs.readFileSync(TEMPLATE_MANIFEST_PATH, "utf8"),
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
- async function runCli(argv = []) {
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
  };
@@ -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
  只有在计划、分诊、调试或架构讨论中明确沉淀出稳定语言时,才新增术语。
@@ -0,0 +1,265 @@
1
+ ---
2
+ pipeline: yss-spec-cli-template-sync
3
+ stage: discovery
4
+ status: draft
5
+ owner: ai
6
+ ---
7
+
8
+ # yss-spec 模板同步 CLI Discovery 收敛文档
9
+
10
+ > 目标:围绕“CLI 支持持续同步模板演进到已有模板实例仓库”完成需求澄清,输出可直接进入 `to-prd` 的上游结论。
11
+
12
+ ## 1. 先澄清术语
13
+
14
+ 本轮最容易混淆的是“同步”。
15
+
16
+ - 不是“新项目初始化”。
17
+ - 不是“把 GitHub 最新代码重新发布到 npm”。
18
+ - 本文默认的“模板同步”是:
19
+ - 让一个已经由 `yss-spec-project-template` 初始化出来的“模板实例仓库”,在后续某个时点对齐到更新后的模板快照版本。
20
+
21
+ 如果后续用户真正想要的是“只要重新发 npm 包即可”,那属于模板发布流程,不属于这里的“实例仓库同步”能力。
22
+
23
+ ## 2. 输入材料
24
+
25
+ | 材料 | 路径 / 链接 | 状态 | 备注 |
26
+ |------|-------------|------|------|
27
+ | 模板初始化现状 | `packages/create-yss-spec/` | 已确认 | 当前 CLI 已支持初始化,不支持已有仓库升级 |
28
+ | 上游初始化澄清 | `docs/discovery/yss-spec-cli-init-discovery.md` | 已确认 | 已定义模板源仓库 / 模板实例仓库 / 初始化 CLI |
29
+ | 上游初始化 PRD | `docs/requirements/yss-spec-cli-init-prd.md` | 已确认 | 当前 MVP 明确排除了“更新已有模板实例仓库到最新模板版本” |
30
+ | 当前会话用户指令 | 当前会话 | 已确认 | 用户希望 CLI 支持持续同步模板演进 |
31
+
32
+ ## 3. 目标用户与真实场景
33
+
34
+ | 用户 / 角色 | 真实场景 | 当前痛点 | 影响范围 |
35
+ |-------------|----------|----------|----------|
36
+ | 模板维护者 | 模板源仓库持续新增流程、文档、脚本、规范后,希望已有项目能跟进 | 只能人工比对目录和文件,容易漏同步、误覆盖 | 模板演进效率、推广成本 |
37
+ | 项目初始化负责人 / Tech Lead | 已用 CLI 初始化过项目,后续希望补齐模板新增资产 | 不知道哪些文件该更新、哪些本地改动不能碰 | 项目仓库一致性、升级风险 |
38
+ | 研发管理使用者 | 希望项目仓库不断跟进新的流程基线 | 模板升级没有标准路径,导致每个项目分叉越来越大 | 过程规范统一性 |
39
+
40
+ ## 4. 核心问题
41
+
42
+ - 当前 CLI 只解决“第一次初始化”,没有解决“模板演进后如何让既有实例仓库跟进”。
43
+ - 模板实例仓库一旦落地,就会出现本地修改;后续同步不是简单覆盖,而是“受管资产升级 + 本地变更保护 + 冲突可见化”。
44
+ - 如果没有模板版本基线,CLI 无法判断:
45
+ - 当前实例仓库是从哪个模板快照创建的
46
+ - 这次准备同步到哪个目标版本
47
+ - 哪些文件是模板新增、模板修改、用户本地修改
48
+
49
+ ## 5. 关键分歧点
50
+
51
+ ### 5.1 “同步”到底同步什么
52
+
53
+ 推荐默认只同步以下“研发管理模板资产”:
54
+
55
+ - `docs/templates/`
56
+ - `docs/process/`
57
+ - `docs/agents/`
58
+ - `docs/user-guide/` 中明确标记为模板资产的部分
59
+ - `AGENTS.md`
60
+ - `CONTEXT.md` 中模板占位初始化后的公共段落
61
+ - 辅助脚本(如 `scripts/verify-template`、`scripts/gitworks`)
62
+
63
+ 不建议默认自动同步:
64
+
65
+ - 项目团队已经开始填写的 PRD 正文
66
+ - 项目自己的 OpenAPI 草稿
67
+ - 已创建的 issue 内容
68
+ - 项目特有 README 大段业务说明
69
+ - 用户新建的非模板文件
70
+
71
+ 结论:模板同步应是“受管模板资产同步”,不是“整个仓库重置”。
72
+
73
+ ### 5.2 命令形态是否继续沿用 `create-*`
74
+
75
+ 这里有一个真实 trade-off:
76
+
77
+ - 如果继续沿用 `create-yss-spec`
78
+ - 优点:沿用现有 npm 包,最短路径可做 `npx create-yss-spec@latest sync`
79
+ - 风险:`create-*` 从语义上更像一次性初始化,不像长期运维命令
80
+ - 如果升级为稳定 CLI(如 `yss-spec init` / `yss-spec sync`)
81
+ - 优点:命令语义更完整,更适合长期维护
82
+ - 风险:需要额外处理包迁移、别名兼容、文档迁移
83
+
84
+ 建议:
85
+
86
+ - MVP 先不拆包,仍在当前仓库和当前 npm 包中实现
87
+ - 命令层采用过渡方案:
88
+ - `npx create-yss-spec@latest sync`
89
+ - 中期再评估稳定命令:
90
+ - `yss-spec init`
91
+ - `yss-spec sync`
92
+
93
+ ### 5.3 同步源来自哪里
94
+
95
+ 候选有三个:
96
+
97
+ 1. 当前执行的 npm 包内置模板快照
98
+ 2. GitHub 仓库远程拉取最新模板
99
+ 3. 独立模板清单 / 模板版本服务
100
+
101
+ 建议 MVP 选 1:
102
+
103
+ - 以当前执行的 npm 包版本作为“目标模板快照版本”
104
+ - 好处是发布、复现、回滚最简单
105
+ - 用户执行 `npx create-yss-spec@latest sync` 时,自然拿到最新已发布模板
106
+
107
+ 不建议 MVP 直接读 GitHub 最新默认分支:
108
+
109
+ - 会让“源码最新”与“已验证可发布模板”混在一起
110
+ - 难以复现问题
111
+ - 不利于回滚
112
+
113
+ ## 6. 推荐的 MVP 边界
114
+
115
+ | 能力 | 是否进入 MVP | 理由 | 成功信号 |
116
+ |------|---------------|------|----------|
117
+ | 支持在已有模板实例仓库中执行 `sync` | 是 | 这是本轮核心价值 | 用户能标准化升级模板资产 |
118
+ | 为实例仓库写入模板元数据文件 | 是 | 没有基线就无法判断同步关系 | CLI 能识别当前模板版本 |
119
+ | 基于受管文件清单执行同步 | 是 | 避免把整个仓库当成可覆盖对象 | 自动更新范围可控 |
120
+ | 默认只更新“未被本地修改”的受管文件 | 是 | 保护项目局部自定义 | 无意外覆盖 |
121
+ | 对本地已改动的受管文件给出冲突 / 跳过报告 | 是 | 同步必须 fail-closed | 用户知道哪些地方需要人工处理 |
122
+ | 支持 `--dry-run` 预览同步计划 | 是 | 同步比初始化更需要预演 | 用户先看变化再执行 |
123
+ | 支持 `--from` / `--to` 版本识别或展示 | 是 | 便于理解本次升级跨度 | 输出中能看到版本变更 |
124
+ | 支持 `--force` 强制覆盖冲突文件 | 否 | 风险过高,容易伤害项目自定义 | 先人工确认冲突处理 |
125
+ | 支持自动三方合并 | 否 | 实现复杂,且容易产出错误合并 | 后续版本再评估 |
126
+ | 支持同步项目已填写内容(如 PRD 正文) | 否 | 这些内容已脱离模板资产范畴 | 保持边界清晰 |
127
+ | 支持直接从 GitHub 拉取未发布模板 | 否 | 破坏可复现性与发布门禁 | 以后仅作高级模式再评估 |
128
+
129
+ ## 7. 非目标范围
130
+
131
+ | 非目标 | 排除原因 | 后续观察条件 |
132
+ |--------|----------|--------------|
133
+ | 整仓库覆盖式升级 | 风险极高,会破坏项目本地资产 | 除非未来引入可靠三方合并和备份机制 |
134
+ | 自动解决所有冲突 | 很难保证正确性 | 后续若只针对少量结构化文件可再局部自动化 |
135
+ | 同步项目自定义内容 | 已超出模板资产边界 | 仅处理明确受管文件 |
136
+ | 在线拉取 GitHub 最新源码直接升级 | 不利于发布治理 | 若未来内部需要 canary 模式再评估 |
137
+ | 多模板、多分支、多环境升级平台 | 会显著抬高复杂度 | 等单模板同步路径稳定后再扩展 |
138
+
139
+ ## 8. 推荐的最小产品流
140
+
141
+ 1. 用户在模板实例仓库根目录执行 `npx create-yss-spec@latest sync`
142
+ 2. CLI 检查当前目录是否存在模板元数据文件,例如 `.yss-template.json`
143
+ 3. CLI 读取当前实例仓库记录的 `currentTemplateVersion`
144
+ 4. CLI 以当前 npm 包内模板作为 `targetTemplateVersion`
145
+ 5. CLI 计算受管文件差异,分类为:
146
+ - `unchanged-managed`:本地未改,可自动更新
147
+ - `modified-managed`:本地已改,需跳过并提示
148
+ - `new-managed`:模板新增,可安全落盘
149
+ - `removed-managed`:模板已删除,MVP 先只提示,不自动删
150
+ - `unmanaged`:不在同步范围内,忽略
151
+ 6. 若是 `--dry-run`,只输出计划
152
+ 7. 若正式执行,则:
153
+ - 自动写入 `unchanged-managed`
154
+ - 新增 `new-managed`
155
+ - 跳过 `modified-managed`
156
+ - 报告 `removed-managed`
157
+ 8. CLI 输出同步摘要和需人工处理的文件清单
158
+ 9. CLI 更新模板元数据中的当前版本
159
+
160
+ ## 9. 模板元数据建议
161
+
162
+ 建议引入一个实例仓库级元数据文件,例如:
163
+
164
+ - `.yss-template.json`
165
+
166
+ 建议记录:
167
+
168
+ - `templateName`
169
+ - `templateVersion`
170
+ - `templateSource`
171
+ - `initializedAt`
172
+ - `lastSyncedAt`
173
+ - `managedFilesManifestVersion`
174
+ - 初始化时采集的核心渲染变量摘要
175
+
176
+ 这个文件的价值是:
177
+
178
+ - 让 CLI 能判断“这个仓库是不是由本模板初始化出来的”
179
+ - 让 CLI 知道“当前仓库上次同步到哪个模板版本”
180
+ - 为后续同步报告、回滚和支持问题定位提供依据
181
+
182
+ ## 10. 风险与待澄清
183
+
184
+ | 风险 / 问题 | 影响 | 建议处理方式 | 截止点 |
185
+ |-------------|------|--------------|--------|
186
+ | 受管文件范围过大 | 误覆盖项目自定义 | MVP 先只纳入公共模板资产 | PRD 前 |
187
+ | 没有模板元数据基线 | 无法可靠识别同步关系 | 把元数据文件纳入 MVP | PRD 前 |
188
+ | 模板删除策略不清 | 可能误删用户文件 | MVP 对删除只提示不自动执行 | PRD 前 |
189
+ | `create-*` 命令形态长期不适合 sync | UX 心智不清 | PRD 中明确“过渡方案 + 演进路线” | PRD 前 |
190
+ | 老项目没有元数据文件 | 无法直接升级 | 需要设计 onboarding / attach 模式 | PRD 前 |
191
+
192
+ ## 11. 对“老项目”的特殊判断
193
+
194
+ 这是一个必须单独说明的边界:
195
+
196
+ - 对于已经由早期 CLI 初始化、但还没有 `.yss-template.json` 的项目
197
+ - CLI 不能直接假设它安全可同步
198
+
199
+ 建议:
200
+
201
+ - MVP 先只支持“带模板元数据的实例仓库”
202
+ - 对老项目提示:
203
+ - 当前仓库缺少模板基线
204
+ - 需要先执行显式 `attach` / `adopt` 流程,或人工确认后再纳入同步
205
+
206
+ 这类“老项目补挂模板基线”的能力,不建议直接塞进第一版同步主路径。
207
+
208
+ ## 12. 成功标准
209
+
210
+ | 指标 / 结果 | 目标 | 验证方式 |
211
+ |-------------|------|----------|
212
+ | 同步安全性 | 默认不覆盖本地已改文件 | 行为测试 + 冲突场景测试 |
213
+ | 升级可见性 | 用户能在执行前看到同步计划 | `--dry-run` 验证 |
214
+ | 同步范围可控 | 仅受管模板资产被自动处理 | 目录快照校验 |
215
+ | 版本可追踪 | 每次同步前后都有模板版本标识 | 元数据文件校验 |
216
+ | 可回滚性 | 用户至少可通过 Git 看见同步改动 | CLI 输出建议 + 验证文档 |
217
+
218
+ ## 13. 实现位置建议
219
+
220
+ - 建议继续放在当前仓库中实现,不另起独立仓库。
221
+ - 原因:
222
+ - 模板同步比模板初始化更依赖模板权威内容
223
+ - 模板、受管清单、同步规则分仓后更容易漂移
224
+ - 当前仓库本身就是模板源仓库,最适合作为同步规则的单一事实源
225
+
226
+ 目录建议:
227
+
228
+ - `packages/create-yss-spec/`
229
+ - 新增 `sync` 子命令实现
230
+ - 新增同步相关测试
231
+ - `template.manifest.json`
232
+ - 从“初始化复制 / 渲染清单”演进为“初始化 + 同步共用清单”
233
+ - 可新增:
234
+ - `docs/implementation/yss-spec-cli-template-sync-routing.md`
235
+ - `docs/requirements/yss-spec-cli-template-sync-prd.md`
236
+
237
+ ## 14. 推荐结论
238
+
239
+ ### 14.1 推荐的产品定义
240
+
241
+ - 把这件事定义为:
242
+ - “模板实例仓库的受管模板资产同步”
243
+ - 不要定义为:
244
+ - “重新生成整个仓库”
245
+ - “从 GitHub 拉最新代码强制覆盖”
246
+
247
+ ### 14.2 推荐的命令方向
248
+
249
+ - MVP:`npx create-yss-spec@latest sync`
250
+ - 中期:演进到 `yss-spec sync`
251
+
252
+ ### 14.3 推荐的同步策略
253
+
254
+ - 以 npm 已发布版本为同步源
255
+ - 以模板元数据作为实例仓库同步基线
256
+ - 以受管文件清单控制同步范围
257
+ - 默认只更新未被本地修改的受管文件
258
+ - 对删除与冲突先提示,不自动处理
259
+
260
+ ## 15. 下一步门禁
261
+
262
+ - 结论:Approved with clarifications
263
+ - 下一步:`to-prd`
264
+ - 进入 PRD 前必须明确的唯一高风险问题:
265
+ - MVP 是否只支持“已有模板元数据的项目”,还是必须兼容历史老项目直接升级
@@ -0,0 +1,69 @@
1
+ # 垂直切片 Issue:模板元数据门禁与 sync 成功主路径
2
+
3
+ ## 同步信息
4
+
5
+ - 平台:GitHub
6
+ - Issue:[#19](https://github.com/iloveZzz/yss-spec-project-template/issues/19)
7
+ - 同步时间:2026-07-05 20:47:53 +0800
8
+
9
+ ## 父级
10
+
11
+ - [#18 PRD: yss-spec 模板同步 CLI](https://github.com/iloveZzz/yss-spec-project-template/issues/18)
12
+
13
+ ## 要构建什么
14
+
15
+ 交付模板同步 CLI 的第一条完整主路径:在一个已经由 `create-yss-spec` 初始化、且带有模板元数据的模板实例仓库中,用户可以执行 `npx create-yss-spec@latest sync`,CLI 能识别当前模板快照版本与目标模板快照版本,加载受管模板文件清单,自动更新未被本地修改的受管文件,补齐模板新增的受管文件,并在成功后回写模板元数据。
16
+
17
+ 本切片只覆盖带模板元数据项目的成功路径,不处理历史老项目接管,也不处理本地已修改受管文件的冲突保护细节。它必须是一个可单独验证的纵向路径:用户通过公开 CLI 接口执行同步,看到版本信息、落盘结果和更新后的模板元数据。
18
+
19
+ ## 覆盖的用户故事
20
+
21
+ - 2, 3, 8, 9, 10, 11, 12, 21, 23
22
+
23
+ ## OpenAPI 影响
24
+
25
+ - [x] 无
26
+ - [ ] 基于冻结 OpenAPI:`docs/api/specs/<feature>.yaml`
27
+
28
+ 受影响端点:
29
+
30
+ | 方法 | 路径 | 变更 |
31
+ |---|---|---|
32
+ | 无 | 无 | 无 |
33
+
34
+ ## 验收标准
35
+
36
+ - [ ] 在带模板元数据的模板实例仓库根目录执行 `sync` 时,CLI 能识别当前模板版本和目标模板版本,并启动同步流程
37
+ - [ ] CLI 能基于受管模板文件清单自动更新未修改的受管文件,并补齐模板新增的受管文件
38
+ - [ ] 同步成功后,模板元数据会更新到新的模板版本并记录最近同步信息
39
+ - [ ] 以上成功主路径具备可重复执行的 CLI 行为测试覆盖
40
+
41
+ ## 测试 Seam
42
+
43
+ - 主要公共接口:CLI 端到端执行入口
44
+ - 必需测试:
45
+ - [x] 行为 / 领域测试
46
+ - [ ] API / 契约测试
47
+ - [ ] UI / 组件测试
48
+ - [x] E2E 测试
49
+
50
+ ## 阻塞关系
51
+
52
+ - 无,可立即开始
53
+
54
+ ## AI / 人工审查点
55
+
56
+ - [x] 未触碰安全红线
57
+ - [ ] 支付逻辑:`TODO-HUMAN-REVIEW`
58
+ - [ ] 数据库迁移:`TODO-HUMAN-REVIEW`
59
+ - [ ] 认证 / 授权:`TODO-HUMAN-REVIEW`
60
+ - [ ] 加密算法:禁止实现
61
+ - [ ] 原生 SQL:仅生成草案
62
+ - [ ] 公共基础库 API:仅生成草案
63
+
64
+ ## 完成定义
65
+
66
+ - [ ] sync 主路径实现完成
67
+ - [ ] 模板元数据读取与回写行为稳定
68
+ - [ ] 受管文件自动更新与新增补齐行为可被验证
69
+ - [ ] 已移除临时调试 / 原型代码
@@ -0,0 +1,69 @@
1
+ # 垂直切片 Issue:dry-run 预演与本地改动保护
2
+
3
+ ## 同步信息
4
+
5
+ - 平台:GitHub
6
+ - Issue:[#21](https://github.com/iloveZzz/yss-spec-project-template/issues/21)
7
+ - 同步时间:2026-07-05 20:47:53 +0800
8
+
9
+ ## 父级
10
+
11
+ - [#18 PRD: yss-spec 模板同步 CLI](https://github.com/iloveZzz/yss-spec-project-template/issues/18)
12
+
13
+ ## 要构建什么
14
+
15
+ 在模板同步主路径已打通的前提下,为 `sync` 命令补齐默认安全的执行控制:支持 `--dry-run` 预演同步计划,并对本地已修改的受管文件采用 fail-closed 的跳过与提示策略。
16
+
17
+ 本切片必须让用户在真正落盘前看到本次同步会处理哪些文件、跳过哪些文件、原因是什么;当受管文件已经在实例仓库中被本地修改时,CLI 不自动覆盖,而是明确报告冲突并继续保护其余安全可更新的文件。
18
+
19
+ ## 覆盖的用户故事
20
+
21
+ - 13, 14, 15, 17, 18, 28
22
+
23
+ ## OpenAPI 影响
24
+
25
+ - [x] 无
26
+ - [ ] 基于冻结 OpenAPI:`docs/api/specs/<feature>.yaml`
27
+
28
+ 受影响端点:
29
+
30
+ | 方法 | 路径 | 变更 |
31
+ |---|---|---|
32
+ | 无 | 无 | 无 |
33
+
34
+ ## 验收标准
35
+
36
+ - [ ] 当用户执行 `sync --dry-run` 时,CLI 只展示同步计划,不进行真实写入,也不更新模板元数据
37
+ - [ ] 当受管文件存在本地修改时,CLI 会将其识别为需跳过项,并给出可理解的原因提示
38
+ - [ ] 正式执行同步时,CLI 不会自动覆盖本地已修改的受管文件,但仍会继续处理安全可更新的部分
39
+ - [ ] 上述预演与保护路径具备可重复执行的 CLI 行为测试覆盖
40
+
41
+ ## 测试 Seam
42
+
43
+ - 主要公共接口:CLI 端到端执行入口
44
+ - 必需测试:
45
+ - [x] 行为 / 领域测试
46
+ - [ ] API / 契约测试
47
+ - [ ] UI / 组件测试
48
+ - [x] E2E 测试
49
+
50
+ ## 阻塞关系
51
+
52
+ - [#19 Slice 1: 模板元数据门禁与 sync 成功主路径](https://github.com/iloveZzz/yss-spec-project-template/issues/19)
53
+
54
+ ## AI / 人工审查点
55
+
56
+ - [x] 未触碰安全红线
57
+ - [ ] 支付逻辑:`TODO-HUMAN-REVIEW`
58
+ - [ ] 数据库迁移:`TODO-HUMAN-REVIEW`
59
+ - [ ] 认证 / 授权:`TODO-HUMAN-REVIEW`
60
+ - [ ] 加密算法:禁止实现
61
+ - [ ] 原生 SQL:仅生成草案
62
+ - [ ] 公共基础库 API:仅生成草案
63
+
64
+ ## 完成定义
65
+
66
+ - [ ] `--dry-run` 预演行为实现完成
67
+ - [ ] 本地已修改受管文件的跳过与提示逻辑实现完成
68
+ - [ ] 安全路径均有自动化验证
69
+ - [ ] 已移除临时调试 / 原型代码
@@ -0,0 +1,69 @@
1
+ # 垂直切片 Issue:删除差异报告与同步交付收口
2
+
3
+ ## 同步信息
4
+
5
+ - 平台:GitHub
6
+ - Issue:[#20](https://github.com/iloveZzz/yss-spec-project-template/issues/20)
7
+ - 同步时间:2026-07-05 20:47:53 +0800
8
+
9
+ ## 父级
10
+
11
+ - [#18 PRD: yss-spec 模板同步 CLI](https://github.com/iloveZzz/yss-spec-project-template/issues/18)
12
+
13
+ ## 要构建什么
14
+
15
+ 补齐模板同步 CLI 的最后一公里交付能力:当目标模板版本中移除了某些受管文件时,CLI 能报告这些删除差异而不自动删除;同步完成后输出清晰的结果摘要、人工后续动作建议和 Git 检查提示,并将这套行为沉淀到自动化验证与使用说明中。
16
+
17
+ 本切片确保模板同步不仅能“执行”,还能被团队安全理解和推广使用。它聚焦的是删除差异可见化、同步结果可审查、用户下一步可行动。
18
+
19
+ ## 覆盖的用户故事
20
+
21
+ - 7, 16, 19, 20, 24, 29, 30
22
+
23
+ ## OpenAPI 影响
24
+
25
+ - [x] 无
26
+ - [ ] 基于冻结 OpenAPI:`docs/api/specs/<feature>.yaml`
27
+
28
+ 受影响端点:
29
+
30
+ | 方法 | 路径 | 变更 |
31
+ |---|---|---|
32
+ | 无 | 无 | 无 |
33
+
34
+ ## 验收标准
35
+
36
+ - [ ] 当模板新版本移除受管文件时,CLI 会将这些文件报告为删除差异,但 MVP 中不会自动删除它们
37
+ - [ ] 同步完成后,CLI 输出包含版本变化、更新数量、跳过数量、删除差异数量以及建议的 Git 后续动作
38
+ - [ ] 使用手册或等价用户说明已补充 `sync` 的执行方式、输出解释和限制边界
39
+ - [ ] 删除差异报告与交付收口路径具备可重复执行的 CLI 行为测试或文档验证覆盖
40
+
41
+ ## 测试 Seam
42
+
43
+ - 主要公共接口:CLI 端到端执行入口
44
+ - 必需测试:
45
+ - [x] 行为 / 领域测试
46
+ - [ ] API / 契约测试
47
+ - [ ] UI / 组件测试
48
+ - [x] E2E 测试
49
+
50
+ ## 阻塞关系
51
+
52
+ - [#19 Slice 1: 模板元数据门禁与 sync 成功主路径](https://github.com/iloveZzz/yss-spec-project-template/issues/19)
53
+
54
+ ## AI / 人工审查点
55
+
56
+ - [x] 未触碰安全红线
57
+ - [ ] 支付逻辑:`TODO-HUMAN-REVIEW`
58
+ - [ ] 数据库迁移:`TODO-HUMAN-REVIEW`
59
+ - [ ] 认证 / 授权:`TODO-HUMAN-REVIEW`
60
+ - [ ] 加密算法:禁止实现
61
+ - [ ] 原生 SQL:仅生成草案
62
+ - [ ] 公共基础库 API:仅生成草案
63
+
64
+ ## 完成定义
65
+
66
+ - [ ] 删除差异报告能力实现完成
67
+ - [ ] 同步结果摘要与 Git 后续提示实现完成
68
+ - [ ] 使用手册 / 等价说明完成更新
69
+ - [ ] 已移除临时调试 / 原型代码
@@ -0,0 +1,109 @@
1
+ ---
2
+ pipeline: yss-spec-cli-template-sync
3
+ stage: open
4
+ status: draft
5
+ owner: ai
6
+ tracker_platform: github
7
+ tracker_issue_number: 18
8
+ tracker_issue_url: https://github.com/iloveZzz/yss-spec-project-template/issues/18
9
+ tracker_sync_status: published
10
+ tracker_synced_at: 2026-07-05 19:02:00 +0800
11
+ source_discovery: docs/discovery/yss-spec-cli-template-sync-discovery.md
12
+ ---
13
+
14
+ # 产品需求文档:yss-spec 模板同步 CLI
15
+
16
+ ## Problem Statement
17
+
18
+ 当前 `create-yss-spec` 已经能把 `yss-spec-project-template` 初始化为新的模板实例仓库,但模板源仓库后续持续演进时,已有实例仓库缺少标准化的升级路径。
19
+
20
+ 对于模板维护者、项目初始化负责人和 Tech Lead 来说,问题不在“如何再初始化一次”,而在“如何把已有项目安全地同步到新的模板快照版本”。一旦实例仓库已经开始被真实项目使用,就会存在本地修改、项目自定义文档和新增文件。此时如果没有模板基线、受管文件范围和冲突保护机制,升级动作就会退化为人工 diff、手工拷贝和经验判断,容易漏同步、误覆盖,最终导致模板实例仓库不断分叉。
21
+
22
+ ## Solution
23
+
24
+ 在现有 npm 包 `create-yss-spec` 中增加模板同步能力,使用户能够在已有模板实例仓库根目录执行 `npx create-yss-spec@latest sync`,以“受管模板资产同步”的方式,把仓库对齐到新的模板快照版本。
25
+
26
+ MVP 采用以下原则:
27
+
28
+ - 以当前 npm 已发布包内置的模板快照作为同步源,而不是直接读取 GitHub 最新源码。
29
+ - 只支持“带模板元数据的模板实例仓库”进入同步主路径。
30
+ - 只自动处理受管模板文件,不把整个仓库当成可覆盖对象。
31
+ - 默认只更新本地未修改的受管文件;对本地已修改文件、模板删除文件和其他风险点给出跳过或提示,不自动强制覆盖。
32
+ - 保留 `--dry-run` 预览能力,让用户先看同步计划再决定是否执行。
33
+
34
+ ## User Stories
35
+
36
+ 1. 作为模板维护者,我希望已有模板实例仓库可以通过统一命令跟进模板演进,这样我不用再逐个项目手工通知和比对。
37
+ 2. 作为模板维护者,我希望同步源来自已发布的 npm 包版本,而不是 GitHub 默认分支,这样升级结果可复现、可回滚。
38
+ 3. 作为模板维护者,我希望实例仓库记录自己的模板快照版本,这样 CLI 才能知道当前仓库从哪个基线开始同步。
39
+ 4. 作为模板维护者,我希望 CLI 记录模板清单版本,这样未来受管范围变化时可以解释升级行为。
40
+ 5. 作为模板维护者,我希望受管模板文件范围是显式定义的,这样模板维护不会演变成整仓库覆盖。
41
+ 6. 作为模板维护者,我希望模板新增的公共资产可以被自动补齐到实例仓库,这样项目能跟上新的流程基线。
42
+ 7. 作为模板维护者,我希望模板删除的文件先被报告而不是自动删除,这样不会误删项目仍在使用的内容。
43
+ 8. 作为模板维护者,我希望同步后模板元数据能更新到新的模板版本,这样下一次升级仍然有可靠基线。
44
+ 9. 作为项目初始化负责人,我希望在已有模板实例仓库中执行一个统一的 `sync` 命令,这样我不需要记住手工升级步骤。
45
+ 10. 作为项目初始化负责人,我希望 CLI 在执行前先检查当前目录是不是一个受支持的模板实例仓库,这样不会在错误目录里误操作。
46
+ 11. 作为项目初始化负责人,我希望 CLI 识别仓库缺少模板元数据时直接拒绝进入同步主路径,这样我不会误以为老项目已被安全升级。
47
+ 12. 作为项目初始化负责人,我希望 CLI 能告诉我当前模板版本和目标模板版本,这样我能理解这次升级跨度。
48
+ 13. 作为项目初始化负责人,我希望先运行 `--dry-run` 看同步计划,这样我可以在落盘前预判风险。
49
+ 14. 作为项目初始化负责人,我希望 CLI 告诉我哪些文件会自动更新,这样我可以评估对当前项目的影响。
50
+ 15. 作为项目初始化负责人,我希望 CLI 告诉我哪些文件因为本地已改而被跳过,这样我知道后续要人工处理哪些地方。
51
+ 16. 作为项目初始化负责人,我希望 CLI 告诉我哪些模板文件已在新版本中被移除,这样我可以决定是否手工清理。
52
+ 17. 作为项目初始化负责人,我希望 CLI 默认不要覆盖我已经改过的受管文件,这样项目自定义不会被悄悄抹掉。
53
+ 18. 作为项目初始化负责人,我希望 CLI 不会碰我新建的非模板文件,这样项目自己的业务资产始终安全。
54
+ 19. 作为项目初始化负责人,我希望同步完成后拿到一个简洁的结果摘要,这样我知道这次升级成功了多少、还剩哪些人工动作。
55
+ 20. 作为项目初始化负责人,我希望 CLI 给出建议的 Git 操作提示,这样我可以把同步结果以可审查的方式提交。
56
+ 21. 作为内部开发者,我希望同步命令仍然挂在当前 `create-yss-spec` 包下,这样我不用等待新的稳定 CLI 包体系再开始使用。
57
+ 22. 作为内部开发者,我希望命令语义清晰地区分初始化和同步,这样我不会把升级误当成重新生成项目。
58
+ 23. 作为内部开发者,我希望 CLI 在同步前失败时给出明确原因,比如“缺少模板元数据”或“当前目录不是模板实例仓库”,这样我能快速调整操作。
59
+ 24. 作为内部开发者,我希望同步逻辑基于清单驱动,而不是硬编码在多处分支里,这样后续模板扩展时行为更可预测。
60
+ 25. 作为内部开发者,我希望模板元数据记录初始化时的关键变量摘要,这样后续需要重新渲染部分受管文件时有依据。
61
+ 26. 作为内部开发者,我希望同步只覆盖受管模板资产,不自动改动我已经在 PRD、OpenAPI、Issue 中填写的项目内容,这样业务上下文不会丢失。
62
+ 27. 作为内部开发者,我希望 README 这类可能已被项目大幅改写的文件不会被轻率自动覆盖,这样协作入口文档不会被模板版本冲掉。
63
+ 28. 作为内部开发者,我希望同步对本地已修改文件采用 fail-closed 策略,这样风险暴露在前面而不是事后发现。
64
+ 29. 作为内部开发者,我希望同步行为可以通过现有 CLI 行为测试覆盖,这样工具升级后我能持续相信它。
65
+ 30. 作为未来维护者,我希望“老项目补挂模板基线”被明确排除在本版之外,这样 MVP 的目标不会在一开始就失焦。
66
+
67
+ ## Implementation Decisions
68
+
69
+ - 同步能力继续在现有 `create-yss-spec` npm 包内演进,不为本功能单独新建独立仓库或第二个 CLI 包。
70
+ - 命令入口采用过渡形态 `sync` 子命令,保持与现有初始化命令同包共存;是否演进为 `yss-spec sync` 留待后续版本评估。
71
+ - 模板同步被定义为“受管模板资产同步”,不是整仓库重置,也不是覆盖式重新初始化。
72
+ - 同步源以当前执行的 npm 包内置模板快照为准,目标模板快照版本与 npm 包版本绑定。
73
+ - 模板实例仓库必须具备模板元数据文件才能进入同步主路径。没有元数据的历史项目不在本版支持范围内。
74
+ - 模板元数据至少需要表达模板名称、当前模板快照版本、模板来源、最近一次同步时间、受管清单版本,以及初始化时的关键变量摘要。
75
+ - 受管模板文件清单需要从现有模板清单演进而来,支持同时服务初始化与同步两类场景。
76
+ - MVP 自动处理的文件范围仅限公共模板资产;项目已填写的 PRD 正文、OpenAPI 草稿、Issue 内容、项目特有 README 大段内容和用户新增文件不进入自动同步范围。
77
+ - 同步差异至少分为五类:可安全更新的未修改受管文件、本地已修改的受管文件、模板新增文件、模板已删除文件、非受管文件。
78
+ - CLI 默认只自动更新未修改的受管文件,并新增模板新引入的受管文件。
79
+ - 对本地已修改的受管文件,CLI 仅报告并跳过,不自动覆盖。
80
+ - 对模板已删除文件,CLI 在 MVP 中只提示,不自动删除。
81
+ - `--dry-run` 在同步场景中仍然是必需能力,且应输出版本变化、文件分类和预计执行动作。
82
+ - 同步完成后,CLI 需要更新模板元数据中的当前模板快照版本和最近同步时间。
83
+ - 同步输出中应包含人工后续动作建议,至少包括查看冲突 / 跳过文件、检查 Git diff 和提交同步结果。
84
+
85
+ ## Testing Decisions
86
+
87
+ - 最高测试 seam 继续采用 CLI 外部行为测试,而不是先拆分大量内部实现级测试。
88
+ - 好测试的标准是:只验证用户可观察行为,例如命令返回码、输出摘要、落盘后的文件集合、元数据变化、跳过与提示行为,不断言内部函数调用或差异算法细节。
89
+ - 首要测试对象是 `sync` 命令的端到端行为,包括成功同步、`--dry-run` 预览、缺少模板元数据拒绝、存在本地已修改受管文件时跳过、模板新增文件补齐、模板删除文件仅提示不删除。
90
+ - 次级测试仅在 CLI 行为测试难以稳定覆盖边界条件时补充,例如模板元数据解析或受管文件分类逻辑,但仍应优先围绕输入输出结果构建。
91
+ - 现有代码库已经有初始化 CLI 的行为测试基线,可延续这种以命令行为核心的测试策略,而不重新建立一套完全不同的 seam。
92
+
93
+ ## Out of Scope
94
+
95
+ - 支持没有模板元数据的历史老项目直接升级。
96
+ - 提供 `attach`、`adopt` 或其他为老项目补挂模板基线的能力。
97
+ - 自动三方合并或自动解决本地已修改受管文件的冲突。
98
+ - 强制覆盖本地已修改文件的 `--force` 升级模式。
99
+ - 自动删除模板新版本中已移除的受管文件。
100
+ - 直接从 GitHub 最新默认分支或任意 commit 拉取未发布模板进行同步。
101
+ - 同步项目自定义内容、业务文档正文、项目特有 README 段落、OpenAPI 草稿或 Issue 正文。
102
+ - 将该功能扩展成多模板、多分支、多环境的统一升级平台。
103
+
104
+ ## Further Notes
105
+
106
+ - 上游澄清记录见 `docs/discovery/yss-spec-cli-template-sync-discovery.md`。
107
+ - 本版已经明确采用你确认的边界:MVP 只支持“带模板元数据的模板实例仓库”。
108
+ - 如果后续确实需要覆盖历史老项目,建议把“模板基线接管 / attach”单独立项,不与本次 sync 主路径混做一个需求。
109
+ - 进入 `to-issues` 前,最好补一个实现路由记录,明确现有 `template.manifest.json` 将如何演进为初始化与同步共用的受管清单。
@@ -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
- npm test
253
+ node --test packages/create-yss-spec/tests/init-cli.test.js
179
254
  ```
180
255
 
181
256
  发布前可检查打包内容: