@zhuan-ai/zhuanspec 2.2.2 → 2.2.3

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.
@@ -50,9 +50,23 @@ export declare class InitCommand {
50
50
  private fetchRemoteArchitectureContent;
51
51
  private cloneRemoteArchitectureRepoToTemp;
52
52
  private syncBusinessSpecTemplate;
53
+ private syncBusinessClaudeAssets;
54
+ private resolveBusinessDirectionRoot;
53
55
  private findFirstDirectoryByName;
54
56
  private findFirstTemplateSourceRoot;
55
57
  private copyDirectoryContentsIfExists;
58
+ private resolveClaudeAssetSource;
59
+ private copyClaudeFileIfNeeded;
60
+ private copyClaudeDirectoryIfNeeded;
61
+ private copyClaudeDirectoryWithFallback;
62
+ private copyClaudeDirectoryFromSingleSource;
63
+ private directoryHasAtLeastOneFile;
64
+ private copyClaudeDirectoryByFileSystem;
65
+ private findGitRootForPath;
66
+ private safeRealpath;
67
+ private copyGitTrackedDirectoryFiles;
68
+ private mergeDirectoryWithoutOverwrite;
69
+ private describeClaudeAssetResult;
56
70
  private writeTemplateFiles;
57
71
  private configureAITools;
58
72
  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 { cpSync, existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync, } from 'fs';
3
+ import { cpSync, existsSync, mkdirSync, mkdtempSync, realpathSync, writeFileSync, 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';
@@ -339,6 +339,14 @@ export class InitCommand {
339
339
  text: PALETTE.midGray(`未找到 ${this.businessDirection} 业务规范模板目录,保留默认模板`),
340
340
  });
341
341
  }
342
+ const claudeAssetSpinner = this.startSpinner(`正在同步 ${this.businessDirection} 业务 Claude 规范资产...`);
343
+ const claudeAssetResult = this.syncBusinessClaudeAssets(projectPath);
344
+ const claudeMdText = this.describeClaudeAssetResult('CLAUDE.md', claudeAssetResult.claudeMd);
345
+ const dotClaudeText = this.describeClaudeAssetResult('.claude', claudeAssetResult.dotClaude);
346
+ claudeAssetSpinner.stopAndPersist({
347
+ symbol: PALETTE.white('▌'),
348
+ text: PALETTE.white(`Claude 规范资产同步完成:${claudeMdText};${dotClaudeText}`),
349
+ });
342
350
  }
343
351
  // Success message
344
352
  this.displaySuccessMessage(selectedTools, created, refreshed, skippedExisting, skipped, extendMode, rootStubStatus);
@@ -760,7 +768,7 @@ export class InitCommand {
760
768
  }
761
769
  try {
762
770
  const specsRoot = path.join(tempDir, 'specs');
763
- const businessRoot = this.findFirstDirectoryByName(specsRoot, this.businessDirection);
771
+ const businessRoot = this.resolveBusinessDirectionRoot(specsRoot);
764
772
  if (!businessRoot) {
765
773
  return { syncedSpecs: false, syncedChanges: false };
766
774
  }
@@ -779,6 +787,53 @@ export class InitCommand {
779
787
  rmSync(tempDir, { recursive: true, force: true });
780
788
  }
781
789
  }
790
+ syncBusinessClaudeAssets(projectPath) {
791
+ const fallbackResult = {
792
+ claudeMd: { source: 'none', status: 'missing' },
793
+ dotClaude: { source: 'none', status: 'missing' },
794
+ };
795
+ if (!this.businessDirection) {
796
+ return fallbackResult;
797
+ }
798
+ const tempDir = this.cloneRemoteArchitectureRepoToTemp();
799
+ if (!tempDir) {
800
+ return fallbackResult;
801
+ }
802
+ try {
803
+ const specsRoot = path.join(tempDir, 'specs');
804
+ const businessRoot = this.resolveBusinessDirectionRoot(specsRoot);
805
+ const commonClaudeRoot = path.join(specsRoot, 'common', 'claude');
806
+ const businessClaudeMd = businessRoot
807
+ ? path.join(businessRoot, 'CLAUDE.md')
808
+ : null;
809
+ const commonClaudeMd = path.join(commonClaudeRoot, 'CLAUDE.md');
810
+ const resolvedClaudeMdSource = this.resolveClaudeAssetSource(businessClaudeMd, commonClaudeMd);
811
+ const businessDotClaude = businessRoot
812
+ ? path.join(businessRoot, '.claude')
813
+ : null;
814
+ const commonDotClaude = path.join(commonClaudeRoot, '.claude');
815
+ return {
816
+ claudeMd: this.copyClaudeFileIfNeeded(resolvedClaudeMdSource, path.join(projectPath, 'CLAUDE.md')),
817
+ dotClaude: this.copyClaudeDirectoryWithFallback(businessDotClaude, commonDotClaude, path.join(projectPath, '.claude')),
818
+ };
819
+ }
820
+ catch {
821
+ return fallbackResult;
822
+ }
823
+ finally {
824
+ rmSync(tempDir, { recursive: true, force: true });
825
+ }
826
+ }
827
+ resolveBusinessDirectionRoot(specsRoot) {
828
+ if (!this.businessDirection) {
829
+ return null;
830
+ }
831
+ const directRoot = path.join(specsRoot, this.businessDirection);
832
+ if (existsSync(directRoot)) {
833
+ return directRoot;
834
+ }
835
+ return this.findFirstDirectoryByName(specsRoot, this.businessDirection);
836
+ }
782
837
  findFirstDirectoryByName(searchRoot, targetName) {
783
838
  if (!existsSync(searchRoot)) {
784
839
  return null;
@@ -850,6 +905,197 @@ export class InitCommand {
850
905
  }
851
906
  return true;
852
907
  }
908
+ resolveClaudeAssetSource(businessPath, commonPath) {
909
+ if (businessPath && existsSync(businessPath)) {
910
+ return { source: 'business', path: businessPath };
911
+ }
912
+ if (existsSync(commonPath)) {
913
+ return { source: 'common', path: commonPath };
914
+ }
915
+ return null;
916
+ }
917
+ copyClaudeFileIfNeeded(sourceAsset, targetPath) {
918
+ if (!sourceAsset) {
919
+ return { source: 'none', status: 'missing' };
920
+ }
921
+ if (existsSync(targetPath)) {
922
+ return { source: sourceAsset.source, status: 'kept-existing' };
923
+ }
924
+ cpSync(sourceAsset.path, targetPath, { force: false });
925
+ return { source: sourceAsset.source, status: 'copied' };
926
+ }
927
+ copyClaudeDirectoryIfNeeded(sourceAsset, targetPath) {
928
+ if (!sourceAsset) {
929
+ return { source: 'none', status: 'missing' };
930
+ }
931
+ if (!existsSync(sourceAsset.path)) {
932
+ return { source: sourceAsset.source, status: 'missing' };
933
+ }
934
+ const repoRoot = this.findGitRootForPath(sourceAsset.path);
935
+ if (!repoRoot) {
936
+ return this.copyClaudeDirectoryByFileSystem(sourceAsset, targetPath);
937
+ }
938
+ const canonicalRepoRoot = this.safeRealpath(repoRoot);
939
+ const canonicalSourcePath = this.safeRealpath(sourceAsset.path);
940
+ const relativeSourceDir = path
941
+ .relative(canonicalRepoRoot, canonicalSourcePath)
942
+ .split(path.sep)
943
+ .join('/');
944
+ if (relativeSourceDir.startsWith('..') ||
945
+ path.isAbsolute(relativeSourceDir)) {
946
+ return this.copyClaudeDirectoryByFileSystem(sourceAsset, targetPath);
947
+ }
948
+ const targetAlreadyExisted = existsSync(targetPath);
949
+ const copiedAny = this.copyGitTrackedDirectoryFiles(canonicalRepoRoot, relativeSourceDir, targetPath);
950
+ if (!copiedAny) {
951
+ return { source: sourceAsset.source, status: 'missing' };
952
+ }
953
+ return {
954
+ source: sourceAsset.source,
955
+ status: targetAlreadyExisted ? 'kept-existing' : 'copied',
956
+ };
957
+ }
958
+ copyClaudeDirectoryWithFallback(businessSourcePath, commonSourcePath, targetPath) {
959
+ const fromBusiness = this.copyClaudeDirectoryFromSingleSource(businessSourcePath, 'business', targetPath);
960
+ if (fromBusiness) {
961
+ return fromBusiness;
962
+ }
963
+ const fromCommon = this.copyClaudeDirectoryFromSingleSource(commonSourcePath, 'common', targetPath);
964
+ if (fromCommon) {
965
+ return fromCommon;
966
+ }
967
+ return { source: 'none', status: 'missing' };
968
+ }
969
+ copyClaudeDirectoryFromSingleSource(sourcePath, sourceLabel, targetPath) {
970
+ if (!sourcePath || !existsSync(sourcePath)) {
971
+ return null;
972
+ }
973
+ if (!this.directoryHasAtLeastOneFile(sourcePath)) {
974
+ return null;
975
+ }
976
+ return this.copyClaudeDirectoryIfNeeded({ source: sourceLabel, path: sourcePath }, targetPath);
977
+ }
978
+ directoryHasAtLeastOneFile(dirPath) {
979
+ if (!existsSync(dirPath)) {
980
+ return false;
981
+ }
982
+ try {
983
+ const entries = readdirSync(dirPath, { withFileTypes: true });
984
+ for (const entry of entries) {
985
+ const fullPath = path.join(dirPath, entry.name);
986
+ if (entry.isFile()) {
987
+ return true;
988
+ }
989
+ if (entry.isDirectory() && this.directoryHasAtLeastOneFile(fullPath)) {
990
+ return true;
991
+ }
992
+ }
993
+ }
994
+ catch {
995
+ return false;
996
+ }
997
+ return false;
998
+ }
999
+ copyClaudeDirectoryByFileSystem(sourceAsset, targetPath) {
1000
+ if (!existsSync(targetPath)) {
1001
+ cpSync(sourceAsset.path, targetPath, { recursive: true, force: true });
1002
+ return { source: sourceAsset.source, status: 'copied' };
1003
+ }
1004
+ this.mergeDirectoryWithoutOverwrite(sourceAsset.path, targetPath);
1005
+ return { source: sourceAsset.source, status: 'kept-existing' };
1006
+ }
1007
+ findGitRootForPath(sourcePath) {
1008
+ try {
1009
+ const output = execSync('git rev-parse --show-toplevel', {
1010
+ encoding: 'utf-8',
1011
+ stdio: ['pipe', 'pipe', 'ignore'],
1012
+ cwd: sourcePath,
1013
+ });
1014
+ return output.trim() || null;
1015
+ }
1016
+ catch {
1017
+ return null;
1018
+ }
1019
+ }
1020
+ safeRealpath(targetPath) {
1021
+ try {
1022
+ return realpathSync(targetPath);
1023
+ }
1024
+ catch {
1025
+ return targetPath;
1026
+ }
1027
+ }
1028
+ copyGitTrackedDirectoryFiles(gitRoot, sourceDirRelativeToGitRoot, targetDir) {
1029
+ try {
1030
+ const sourcePrefix = sourceDirRelativeToGitRoot.endsWith('/')
1031
+ ? sourceDirRelativeToGitRoot
1032
+ : `${sourceDirRelativeToGitRoot}/`;
1033
+ const output = execSync(`git ls-tree -r --name-only HEAD -- "${sourceDirRelativeToGitRoot}"`, {
1034
+ encoding: 'utf-8',
1035
+ stdio: ['pipe', 'pipe', 'ignore'],
1036
+ cwd: gitRoot,
1037
+ });
1038
+ const trackedFiles = output
1039
+ .split('\n')
1040
+ .map((line) => line.trim())
1041
+ .filter((line) => line.length > 0);
1042
+ if (trackedFiles.length === 0) {
1043
+ return false;
1044
+ }
1045
+ mkdirSync(targetDir, { recursive: true });
1046
+ trackedFiles.forEach((repoFilePath) => {
1047
+ const relativePath = repoFilePath.startsWith(sourcePrefix)
1048
+ ? repoFilePath.slice(sourcePrefix.length)
1049
+ : path.basename(repoFilePath);
1050
+ const targetFilePath = path.join(targetDir, relativePath);
1051
+ if (existsSync(targetFilePath)) {
1052
+ return;
1053
+ }
1054
+ mkdirSync(path.dirname(targetFilePath), { recursive: true });
1055
+ const content = execSync(`git show HEAD:${repoFilePath}`, {
1056
+ encoding: 'buffer',
1057
+ stdio: ['pipe', 'pipe', 'ignore'],
1058
+ cwd: gitRoot,
1059
+ });
1060
+ writeFileSync(targetFilePath, content);
1061
+ });
1062
+ return true;
1063
+ }
1064
+ catch {
1065
+ return false;
1066
+ }
1067
+ }
1068
+ mergeDirectoryWithoutOverwrite(sourceDir, targetDir) {
1069
+ if (!existsSync(sourceDir) || !existsSync(targetDir)) {
1070
+ return;
1071
+ }
1072
+ const copyRecursive = (source, target) => {
1073
+ const entries = readdirSync(source, { withFileTypes: true });
1074
+ for (const entry of entries) {
1075
+ const sourceEntryPath = path.join(source, entry.name);
1076
+ const targetEntryPath = path.join(target, entry.name);
1077
+ if (entry.isDirectory()) {
1078
+ mkdirSync(targetEntryPath, { recursive: true });
1079
+ copyRecursive(sourceEntryPath, targetEntryPath);
1080
+ continue;
1081
+ }
1082
+ if (!existsSync(targetEntryPath)) {
1083
+ cpSync(sourceEntryPath, targetEntryPath, { force: false });
1084
+ }
1085
+ }
1086
+ };
1087
+ copyRecursive(sourceDir, targetDir);
1088
+ }
1089
+ describeClaudeAssetResult(assetName, result) {
1090
+ if (result.status === 'missing') {
1091
+ return `${assetName} 未找到可用模板`;
1092
+ }
1093
+ const sourceText = result.source === 'business' ? '业务目录' : 'common/claude';
1094
+ if (result.status === 'kept-existing') {
1095
+ return `${assetName} 使用${sourceText}模板,目标已存在未覆盖`;
1096
+ }
1097
+ return `${assetName} 已从${sourceText}模板复制`;
1098
+ }
853
1099
  async writeTemplateFiles(zhuanspecPath, config, skipExisting) {
854
1100
  const context = {};
855
1101
  const templates = TemplateManager.getTemplates(context);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhuan-ai/zhuanspec",
3
- "version": "2.2.2",
3
+ "version": "2.2.3",
4
4
  "description": "AI-native system for spec-driven development",
5
5
  "keywords": [
6
6
  "zhuanspec",
@@ -38,6 +38,26 @@
38
38
  "!dist/**/__tests__",
39
39
  "!dist/**/*.map"
40
40
  ],
41
+ "scripts": {
42
+ "lint": "eslint src/",
43
+ "build": "node build.js",
44
+ "dev": "tsc --watch",
45
+ "dev:cli": "pnpm build && node bin/zhuanspec.js",
46
+ "test": "vitest run",
47
+ "test:watch": "vitest",
48
+ "test:ui": "vitest --ui",
49
+ "test:coverage": "vitest --coverage",
50
+ "test:postinstall": "node scripts/postinstall.js",
51
+ "prepare": "npm run build",
52
+ "prepublishOnly": "npm run build",
53
+ "postinstall": "node scripts/postinstall.js",
54
+ "check:pack-version": "node scripts/pack-version-check.mjs",
55
+ "diagnose:cursor": "node scripts/diagnose-cursor-commands.js",
56
+ "release": "pnpm run release:ci",
57
+ "release:ci": "pnpm run check:pack-version && pnpm exec changeset publish",
58
+ "release:local": "pnpm exec changeset version && pnpm run check:pack-version && pnpm exec changeset publish",
59
+ "changeset": "changeset"
60
+ },
41
61
  "engines": {
42
62
  "node": ">=20.19.0"
43
63
  },
@@ -59,23 +79,5 @@
59
79
  "ora": "^8.2.0",
60
80
  "yaml": "^2.8.2",
61
81
  "zod": "^4.0.17"
62
- },
63
- "scripts": {
64
- "lint": "eslint src/",
65
- "build": "node build.js",
66
- "dev": "tsc --watch",
67
- "dev:cli": "pnpm build && node bin/zhuanspec.js",
68
- "test": "vitest run",
69
- "test:watch": "vitest",
70
- "test:ui": "vitest --ui",
71
- "test:coverage": "vitest --coverage",
72
- "test:postinstall": "node scripts/postinstall.js",
73
- "postinstall": "node scripts/postinstall.js",
74
- "check:pack-version": "node scripts/pack-version-check.mjs",
75
- "diagnose:cursor": "node scripts/diagnose-cursor-commands.js",
76
- "release": "pnpm run release:ci",
77
- "release:ci": "pnpm run check:pack-version && pnpm exec changeset publish",
78
- "release:local": "pnpm exec changeset version && pnpm run check:pack-version && pnpm exec changeset publish",
79
- "changeset": "changeset"
80
82
  }
81
- }
83
+ }