moluoxixi-ai-rules 0.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.
Files changed (49) hide show
  1. package/LICENSE +21 -0
  2. package/README-zh.md +246 -0
  3. package/README.md +246 -0
  4. package/dist/constants/hosts.d.ts +34 -0
  5. package/dist/constants/hosts.js +71 -0
  6. package/dist/constants/skills.d.ts +91 -0
  7. package/dist/constants/skills.js +156 -0
  8. package/dist/scripts/cli.d.ts +2 -0
  9. package/dist/scripts/cli.js +145 -0
  10. package/dist/scripts/host-setup.d.ts +2 -0
  11. package/dist/scripts/host-setup.js +147 -0
  12. package/dist/scripts/lib/install.d.ts +78 -0
  13. package/dist/scripts/lib/install.js +367 -0
  14. package/dist/scripts/lib/links.d.ts +8 -0
  15. package/dist/scripts/lib/links.js +18 -0
  16. package/dist/scripts/lib/skill-projection.d.ts +14 -0
  17. package/dist/scripts/lib/skill-projection.js +54 -0
  18. package/dist/scripts/lib/tool.d.ts +40 -0
  19. package/dist/scripts/lib/tool.js +125 -0
  20. package/dist/scripts/lib/vendor-sync.d.ts +3 -0
  21. package/dist/scripts/lib/vendor-sync.js +129 -0
  22. package/dist/scripts/lib/vendors.d.ts +29 -0
  23. package/dist/scripts/lib/vendors.js +155 -0
  24. package/dist/scripts/lib/verify.d.ts +7 -0
  25. package/dist/scripts/lib/verify.js +84 -0
  26. package/dist/scripts/sync-vendors.d.ts +1 -0
  27. package/dist/scripts/sync-vendors.js +173 -0
  28. package/dist/scripts/verify-host.d.ts +2 -0
  29. package/dist/scripts/verify-host.js +42 -0
  30. package/package.json +60 -0
  31. package/rules/AGENTS.md +30 -0
  32. package/skills/skill-validation-standard/SKILL.md +45 -0
  33. package/skills/skill-validation-standard/__test__/verify-rules.test.ts +108 -0
  34. package/skills/skill-validation-standard/scripts/verify-rules.mjs +186 -0
  35. package/skills/workflow/frontend-code-standard/SKILL.md +58 -0
  36. package/skills/workflow/frontend-code-standard/__test__/verify-rules.test.ts +227 -0
  37. package/skills/workflow/frontend-code-standard/scripts/verify-rules.mjs +537 -0
  38. package/skills/workflow/java-code-standard/SKILL.md +58 -0
  39. package/skills/workflow/java-code-standard/__test__/verify-rules.test.ts +48 -0
  40. package/skills/workflow/java-code-standard/scripts/verify-rules.mjs +215 -0
  41. package/skills/workflow/nestjs-code-standard/SKILL.md +61 -0
  42. package/skills/workflow/nestjs-code-standard/__test__/verify-rules.test.ts +48 -0
  43. package/skills/workflow/nestjs-code-standard/scripts/verify-rules.mjs +219 -0
  44. package/skills/workflow/node-code-standard/SKILL.md +62 -0
  45. package/skills/workflow/node-code-standard/__test__/verify-rules.test.ts +48 -0
  46. package/skills/workflow/node-code-standard/scripts/verify-rules.mjs +213 -0
  47. package/skills/workflow/project-knowledge-read-workflow/SKILL.md +102 -0
  48. package/skills/workflow/project-knowledge-refresh-workflow/SKILL.md +149 -0
  49. package/skills/workflow/software-development-workflow/SKILL.md +85 -0
@@ -0,0 +1,129 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import { existsSync, mkdirSync } from 'node:fs';
3
+ import path from 'node:path';
4
+ function runGit(args, cwd, options = {}) {
5
+ const result = spawnSync('git', args, {
6
+ cwd,
7
+ encoding: 'utf8',
8
+ stdio: options.stdio ?? 'pipe',
9
+ });
10
+ if (result.error) {
11
+ throw result.error;
12
+ }
13
+ if (result.status !== 0) {
14
+ const stderr = (result.stderr ?? '').trim();
15
+ throw new Error(`git ${args.join(' ')} failed with exit code ${result.status}${stderr ? `: ${stderr}` : ''}`);
16
+ }
17
+ return (result.stdout ?? '').trim();
18
+ }
19
+ function listOriginHeadBranches(cloneDir) {
20
+ return runGit(['-C', cloneDir, 'ls-remote', '--heads', 'origin'], process.cwd())
21
+ .split(/\r?\n/)
22
+ .map(line => line.trim())
23
+ .filter(Boolean)
24
+ .map((line) => {
25
+ const ref = line.split('\t')[1];
26
+ if (!ref?.startsWith('refs/heads/')) {
27
+ throw new Error(`Unexpected origin branch format for ${cloneDir}: ${line}`);
28
+ }
29
+ return ref.slice('refs/heads/'.length);
30
+ });
31
+ }
32
+ export function getRemoteDefaultBranch(cloneDir) {
33
+ let symbolicRefError;
34
+ const originHeadBranches = listOriginHeadBranches(cloneDir);
35
+ try {
36
+ const ref = runGit(['-C', cloneDir, 'symbolic-ref', 'refs/remotes/origin/HEAD'], process.cwd());
37
+ const branch = ref.replace('refs/remotes/origin/', '');
38
+ if (originHeadBranches.includes(branch)) {
39
+ return branch;
40
+ }
41
+ }
42
+ catch (error) {
43
+ symbolicRefError = error;
44
+ }
45
+ const lines = runGit(['-C', cloneDir, 'ls-remote', '--symref', 'origin', 'HEAD'], process.cwd())
46
+ .split(/\r?\n/)
47
+ .map(line => line.trim())
48
+ .filter(Boolean);
49
+ const headLine = lines.find(line => line.startsWith('ref: ') && line.endsWith('HEAD'));
50
+ if (headLine) {
51
+ const prefix = 'ref: refs/heads/';
52
+ const suffix = ' HEAD';
53
+ if (!headLine.startsWith(prefix) || !headLine.endsWith(suffix)) {
54
+ throw new Error(`Unexpected origin HEAD format for ${cloneDir}: ${headLine}`);
55
+ }
56
+ const branch = headLine.slice(prefix.length, -suffix.length);
57
+ if (originHeadBranches.includes(branch)) {
58
+ return branch;
59
+ }
60
+ }
61
+ const remoteBranches = runGit(['-C', cloneDir, 'branch', '-r', '--format=%(refname:lstrip=3)'], process.cwd())
62
+ .split(/\r?\n/)
63
+ .map(line => line.trim())
64
+ .filter(line => line && line !== 'origin/HEAD');
65
+ if (remoteBranches.length === 1) {
66
+ return remoteBranches[0].replace(/^origin\//, '');
67
+ }
68
+ // Sparse file:// clones can lack local origin/* refs when remote HEAD is unset.
69
+ if (originHeadBranches.length === 1) {
70
+ return originHeadBranches[0];
71
+ }
72
+ throw new Error(`Unable to determine origin default branch for ${cloneDir}; symbolic-ref failed with: ${String(symbolicRefError)}`);
73
+ }
74
+ function getSparsePatterns(vendor) {
75
+ if (!vendor.links?.length)
76
+ return [];
77
+ const topDirs = new Set();
78
+ for (const link of vendor.links) {
79
+ const first = link.source.split('/')[0];
80
+ if (first)
81
+ topDirs.add(first);
82
+ }
83
+ return [...topDirs];
84
+ }
85
+ function isLocalRepo(repoUrl) {
86
+ return !repoUrl.includes('://');
87
+ }
88
+ export function ensureVendorRepo(homeDir, vendor) {
89
+ const cloneDir = path.resolve(homeDir, vendor.cloneDir);
90
+ mkdirSync(path.dirname(cloneDir), { recursive: true });
91
+ const sparsePatterns = getSparsePatterns(vendor);
92
+ if (!existsSync(path.join(cloneDir, '.git'))) {
93
+ if (isLocalRepo(vendor.repo)) {
94
+ runGit(['clone', vendor.repo, cloneDir], process.cwd(), { stdio: 'inherit' });
95
+ }
96
+ else {
97
+ runGit(['clone', '--depth', '1', '--filter=blob:none', '--sparse', vendor.repo, cloneDir], process.cwd(), { stdio: 'inherit' });
98
+ }
99
+ if (sparsePatterns.length > 0) {
100
+ if (isLocalRepo(vendor.repo)) {
101
+ runGit(['-C', cloneDir, 'sparse-checkout', 'init', '--cone'], process.cwd(), { stdio: 'inherit' });
102
+ }
103
+ runGit(['-C', cloneDir, 'sparse-checkout', 'set', ...sparsePatterns], process.cwd(), { stdio: 'inherit' });
104
+ }
105
+ }
106
+ const defaultBranch = getRemoteDefaultBranch(cloneDir);
107
+ const remoteRef = `origin/${defaultBranch}`;
108
+ const fetchRefSpec = `refs/heads/${defaultBranch}:refs/remotes/${remoteRef}`;
109
+ if (!isLocalRepo(vendor.repo)) {
110
+ runGit(['-C', cloneDir, 'fetch', '--depth', '1', '--prune', 'origin', fetchRefSpec], process.cwd(), { stdio: 'inherit' });
111
+ }
112
+ else {
113
+ runGit(['-C', cloneDir, 'fetch', '--prune', 'origin', fetchRefSpec], process.cwd(), { stdio: 'inherit' });
114
+ }
115
+ if (sparsePatterns.length > 0) {
116
+ if (isLocalRepo(vendor.repo)) {
117
+ runGit(['-C', cloneDir, 'sparse-checkout', 'set', ...sparsePatterns], process.cwd(), { stdio: 'inherit' });
118
+ }
119
+ else {
120
+ runGit(['-C', cloneDir, 'sparse-checkout', 'reapply'], process.cwd(), { stdio: 'inherit' });
121
+ }
122
+ }
123
+ runGit(['-C', cloneDir, 'reset', '--hard'], process.cwd(), { stdio: 'inherit' });
124
+ runGit(['-C', cloneDir, 'clean', '-fd'], process.cwd(), { stdio: 'inherit' });
125
+ runGit(['-C', cloneDir, 'checkout', '-B', defaultBranch, remoteRef], process.cwd(), { stdio: 'inherit' });
126
+ runGit(['-C', cloneDir, 'reset', '--hard', remoteRef], process.cwd(), { stdio: 'inherit' });
127
+ runGit(['-C', cloneDir, 'clean', '-fd'], process.cwd(), { stdio: 'inherit' });
128
+ return cloneDir;
129
+ }
@@ -0,0 +1,29 @@
1
+ import type { SetupCommand } from '../../constants/skills.js';
2
+ export declare function normalizePath(value: string): string;
3
+ export interface VendorLink {
4
+ kind: string;
5
+ source: string;
6
+ target: string;
7
+ /** 该 skill 的安装前置命令(来自 SkillConfig.setup) */
8
+ setup?: SetupCommand[];
9
+ }
10
+ export interface Vendor {
11
+ official?: boolean;
12
+ repo: string;
13
+ cloneDir: string;
14
+ links: VendorLink[];
15
+ }
16
+ export interface VendorManifest {
17
+ version: number;
18
+ vendors: Record<string, Vendor>;
19
+ }
20
+ /**
21
+ * 递归遍历供应商定义树,支持混合数组和对象结构
22
+ * @param node 当前处理的节点 (VendorRepo | Record | Array)
23
+ * @param namespaceParts 当前递归深度对应的分类路径
24
+ * @param vendors 全局积累的供应商对象映射
25
+ */
26
+ export declare function walkVendorTree(node: any, namespaceParts: string[], vendors: Record<string, Vendor>): void;
27
+ export declare function loadVendorManifest(manifestPath: string): Promise<VendorManifest>;
28
+ export declare function getRepoRoot(fromFileUrl: string): string;
29
+ export declare function resolveHomePath(homeDir: string, relativePath: string): string;
@@ -0,0 +1,155 @@
1
+ import path from 'node:path';
2
+ import { fileURLToPath, pathToFileURL } from 'node:url';
3
+ import { flattenedSkillName, flattenedVendorSkillTarget } from './skill-projection.js';
4
+ export function normalizePath(value) {
5
+ return value.replace(/\\/g, '/');
6
+ }
7
+ /**
8
+ * 判断是否为有效的 VendorRepo 定义
9
+ */
10
+ function isVendorEntry(value) {
11
+ return Boolean(value
12
+ && typeof value === 'object'
13
+ && typeof value.name === 'string'
14
+ && typeof value.source === 'string');
15
+ }
16
+ /**
17
+ * 构造 vendor 侧技能目标路径。
18
+ * 源配置允许多级分类或嵌套源路径,但 vendor/skills 始终以叶子 skill 名称展平。
19
+ * @param outputName 最终的技能目录名
20
+ */
21
+ function buildTargetPath(outputName) {
22
+ return flattenedVendorSkillTarget(outputName);
23
+ }
24
+ /**
25
+ * 构建 skills projection 中单个 skill 的链接计划
26
+ * @param sourceBaseDir 仓库内技能基准目录
27
+ * @param skillDef 字符串简写或带输出名/setup 的 skill 配置
28
+ */
29
+ function buildSkillLink(sourceBaseDir, skillDef) {
30
+ if (typeof skillDef === 'string') {
31
+ return {
32
+ kind: 'skill',
33
+ source: path.posix.join(sourceBaseDir, skillDef),
34
+ target: buildTargetPath(skillDef),
35
+ };
36
+ }
37
+ const sourceName = skillDef.name;
38
+ const outputName = (skillDef.output ?? flattenedSkillName(sourceName));
39
+ return {
40
+ kind: 'skill',
41
+ source: path.posix.join(sourceBaseDir, sourceName),
42
+ target: buildTargetPath(outputName),
43
+ setup: skillDef.setup,
44
+ };
45
+ }
46
+ /**
47
+ * 构建单个供应商实体的链接计划
48
+ * @param entry 供应商定义实体
49
+ */
50
+ function buildLinksForEntry(entry) {
51
+ if (entry.sourceDir || entry.sourceBaseDir || entry.skills || entry.setup) {
52
+ throw new Error(`供应商 "${entry.name}" 必须使用 projections 配置`);
53
+ }
54
+ if (!Array.isArray(entry.projections)) {
55
+ throw new TypeError(`供应商 "${entry.name}" 必须使用 projections 配置`);
56
+ }
57
+ return entry.projections.flatMap((projection) => {
58
+ if (projection.kind === 'namespace') {
59
+ return [{
60
+ kind: 'namespace-dir',
61
+ source: projection.sourceDir,
62
+ target: buildTargetPath(projection.output),
63
+ setup: projection.setup,
64
+ }];
65
+ }
66
+ if (projection.kind === 'skills') {
67
+ return projection.skills.map((skillDef) => buildSkillLink(projection.sourceBaseDir, skillDef));
68
+ }
69
+ throw new Error(`供应商 "${entry.name}" 存在未知 projection 类型: ${projection.kind}`);
70
+ });
71
+ }
72
+ /**
73
+ * 合并供应商定义到全局清单
74
+ */
75
+ function mergeVendor(vendors, vendorName, entry) {
76
+ if (entry.local === true) {
77
+ throw new Error('暂不支持本地供应商实体 (Local vendor entries)');
78
+ }
79
+ const cloneDir = path.posix.join('vendor', 'repos', vendorName);
80
+ const links = buildLinksForEntry(entry);
81
+ if (!vendors[vendorName]) {
82
+ vendors[vendorName] = {
83
+ official: entry.official,
84
+ repo: entry.source,
85
+ cloneDir,
86
+ links,
87
+ };
88
+ return;
89
+ }
90
+ const existing = vendors[vendorName];
91
+ if (existing.repo !== entry.source
92
+ || existing.cloneDir !== cloneDir) {
93
+ throw new Error(`供应商 "${vendorName}" 在不同模块中的定义不一致`);
94
+ }
95
+ existing.official = existing.official || entry.official;
96
+ existing.links.push(...links);
97
+ }
98
+ /**
99
+ * 递归遍历供应商定义树,支持混合数组和对象结构
100
+ * @param node 当前处理的节点 (VendorRepo | Record | Array)
101
+ * @param namespaceParts 当前递归深度对应的分类路径
102
+ * @param vendors 全局积累的供应商对象映射
103
+ */
104
+ export function walkVendorTree(node, namespaceParts, vendors) {
105
+ if (!node)
106
+ return;
107
+ if (Array.isArray(node)) {
108
+ for (const entry of node) {
109
+ if (isVendorEntry(entry)) {
110
+ mergeVendor(vendors, entry.name, entry);
111
+ }
112
+ else if (entry && typeof entry === 'object') {
113
+ // 如果数组元素是普通对象,则视为分类节点(例如 { "frontend": [...] })
114
+ for (const [key, value] of Object.entries(entry)) {
115
+ walkVendorTree(value, [...namespaceParts, key], vendors);
116
+ }
117
+ }
118
+ else {
119
+ throw new Error(`在分类 "${namespaceParts.join('/') || '根目录'}" 下发现无效的供应商节点定义`);
120
+ }
121
+ }
122
+ }
123
+ else if (typeof node === 'object') {
124
+ // 处理直接传入的对象结构(用于递归或旧版兼容)
125
+ for (const [key, value] of Object.entries(node)) {
126
+ walkVendorTree(value, [...namespaceParts, key], vendors);
127
+ }
128
+ }
129
+ }
130
+ export async function loadVendorManifest(manifestPath) {
131
+ const manifestUrl = pathToFileURL(path.resolve(manifestPath)).href;
132
+ const module = await import(manifestUrl);
133
+ const vendorTree = module.vendors ?? module.default?.vendors ?? module.default;
134
+ if (!vendorTree || typeof vendorTree !== 'object') {
135
+ throw new Error(`Vendor manifest "${manifestPath}" must export a "vendors" object`);
136
+ }
137
+ const vendors = {};
138
+ walkVendorTree(vendorTree, [], vendors);
139
+ return {
140
+ version: 1,
141
+ vendors,
142
+ };
143
+ }
144
+ export function getRepoRoot(fromFileUrl) {
145
+ return path.resolve(fileURLToPath(new URL('../..', fromFileUrl)));
146
+ }
147
+ export function resolveHomePath(homeDir, relativePath) {
148
+ // Preserve caller-provided absolute path style so tests and generated plans stay OS-neutral.
149
+ const pathApi = path.win32.isAbsolute(homeDir)
150
+ ? path.win32
151
+ : path.posix.isAbsolute(homeDir)
152
+ ? path.posix
153
+ : path;
154
+ return normalizePath(pathApi.resolve(homeDir, relativePath));
155
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * 验证指定宿主的技能链接完整性
3
+ * @param host 宿主名称
4
+ * @param moluoHome AIRules 的本地安装目录
5
+ * @returns 是否验证通过
6
+ */
7
+ export declare function verifyHost(host: string, moluoHome: string, userHome?: string): Promise<boolean>;
@@ -0,0 +1,84 @@
1
+ import { existsSync, lstatSync, readdirSync, realpathSync } from 'node:fs';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import kleur from 'kleur';
5
+ import { findHostConfig, resolveHostPaths } from '../../constants/hosts.js';
6
+ /**
7
+ * 验证指定宿主的技能链接完整性
8
+ * @param host 宿主名称
9
+ * @param moluoHome AIRules 的本地安装目录
10
+ * @returns 是否验证通过
11
+ */
12
+ export async function verifyHost(host, moluoHome, userHome = os.homedir()) {
13
+ console.log(`\n--- 正在验证宿主: ${host} ---`);
14
+ const config = findHostConfig(host);
15
+ if (!config)
16
+ return false;
17
+ const { hostHome, skillsDirName } = resolveHostPaths(config, userHome);
18
+ const resolvedHostHome = path.resolve(hostHome);
19
+ if (!existsSync(resolvedHostHome)) {
20
+ console.warn(`[SKIP] 宿主目录不存在: ${resolvedHostHome}`);
21
+ return true; // 跳过但不视为失败
22
+ }
23
+ const targetSkillsDir = path.join(resolvedHostHome, skillsDirName);
24
+ if (!existsSync(targetSkillsDir)) {
25
+ console.error(`[FAIL] 技能目录缺失: ${targetSkillsDir}`);
26
+ return false;
27
+ }
28
+ // 1. 获取预期技能列表 (从 vendor/skills)
29
+ const expectedSkills = new Set();
30
+ const vendorSkillsDir = path.join(moluoHome, 'vendor', 'skills');
31
+ if (existsSync(vendorSkillsDir)) {
32
+ readdirSync(vendorSkillsDir).forEach((name) => {
33
+ if (name !== '.gitignore')
34
+ expectedSkills.add(name);
35
+ });
36
+ }
37
+ console.log(`[info] 预期技能总数: ${expectedSkills.size}`);
38
+ // 2. 检查实际存在的链接
39
+ let missingCount = 0;
40
+ let brokenCount = 0;
41
+ let validCount = 0;
42
+ const targetSkillNames = new Set(readdirSync(targetSkillsDir));
43
+ for (const skill of expectedSkills) {
44
+ const skillPath = path.join(targetSkillsDir, skill);
45
+ if (!targetSkillNames.has(skill)) {
46
+ console.error(`[FAIL] 缺失技能链接 (Missing): ${skill}`);
47
+ missingCount++;
48
+ continue;
49
+ }
50
+ const stats = lstatSync(skillPath);
51
+ if (stats.isSymbolicLink() && !existsSync(skillPath)) {
52
+ console.error(`[FAIL] 损坏的软链接 (Broken Link): ${skill}`);
53
+ brokenCount++;
54
+ continue;
55
+ }
56
+ if (!stats.isSymbolicLink()) {
57
+ console.warn(`[WARN] 技能不是软链接 (可能是物理拷贝): ${skill}`);
58
+ validCount++;
59
+ }
60
+ else {
61
+ const real = realpathSync(skillPath);
62
+ const normalizedReal = path.resolve(real);
63
+ const normalizedMoluo = path.resolve(moluoHome);
64
+ const normalizedRepo = path.resolve(process.cwd());
65
+ // 验证指向是否在 moluoxixi 内部或仓库根目录
66
+ if (normalizedReal.startsWith(normalizedMoluo) || normalizedReal.startsWith(normalizedRepo)) {
67
+ validCount++;
68
+ }
69
+ else {
70
+ console.warn(`[WARN] 链接指向外部路径: ${skill} -> ${real}`);
71
+ validCount++;
72
+ }
73
+ }
74
+ }
75
+ console.log(`[result] 有效=${validCount}, 缺失=${missingCount}, 损坏=${brokenCount}`);
76
+ const success = missingCount === 0 && brokenCount === 0;
77
+ if (success) {
78
+ console.log(kleur.green(`✅ ${host} 验证通过`));
79
+ }
80
+ else {
81
+ console.log(kleur.red(`❌ ${host} 验证失败`));
82
+ }
83
+ return success;
84
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,173 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import { createHash } from 'node:crypto';
3
+ import { cpSync, existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
4
+ import { dirname, join, resolve } from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+ import { vendors as vendorsConfig } from '../constants/skills.js';
7
+ import { collectFlattenedSkillSources, flattenedSkillName } from './lib/skill-projection.js';
8
+ import { walkVendorTree } from './lib/vendors.js';
9
+ const __filename = fileURLToPath(import.meta.url);
10
+ const __dirname = dirname(__filename);
11
+ const PROJECT_ROOT = resolve(__dirname, '..');
12
+ const VENDOR_SKILLS_DIR = join(PROJECT_ROOT, 'vendor', 'skills');
13
+ const CACHE_DIR = join(PROJECT_ROOT, '.cache-git');
14
+ const CONSTANTS_DIR = join(PROJECT_ROOT, 'constants');
15
+ const REMOVE_DIR_OPTIONS = { recursive: true, force: true, maxRetries: 5, retryDelay: 200 };
16
+ /** 同步状态文件:记录上次同步时 constants/ 的内容指纹 */
17
+ const SYNC_FINGERPRINT_FILE = join(PROJECT_ROOT, 'vendor', '.sync-fingerprint');
18
+ /**
19
+ * 计算 constants/ 目录下所有文件的内容指纹(SHA-256)。
20
+ * 排序保证结果稳定,与文件系统列出顺序无关。
21
+ */
22
+ function computeConstantsFingerprint() {
23
+ const hash = createHash('sha256');
24
+ const files = readdirSync(CONSTANTS_DIR)
25
+ .filter(f => !f.startsWith('.'))
26
+ .sort();
27
+ for (const file of files) {
28
+ const content = readFileSync(join(CONSTANTS_DIR, file));
29
+ hash.update(file); // 文件名也纳入哈希,防止重命名被忽略
30
+ hash.update(content);
31
+ }
32
+ return hash.digest('hex');
33
+ }
34
+ /**
35
+ * 读取上次同步时保存的指纹,不存在则返回空字符串。
36
+ */
37
+ function readSavedFingerprint() {
38
+ if (!existsSync(SYNC_FINGERPRINT_FILE))
39
+ return '';
40
+ return readFileSync(SYNC_FINGERPRINT_FILE, 'utf8').trim();
41
+ }
42
+ /**
43
+ * 将当前指纹写入 lockfile。
44
+ */
45
+ function saveSyncFingerprint(fingerprint) {
46
+ mkdirSync(dirname(SYNC_FINGERPRINT_FILE), { recursive: true });
47
+ writeFileSync(SYNC_FINGERPRINT_FILE, fingerprint, 'utf8');
48
+ }
49
+ /**
50
+ * 删除 Git 临时目录。Windows 上 Git 进程退出后文件句柄可能短暂滞留,
51
+ * 这里使用 Node 的受限重试机制;重试耗尽后仍会抛出原始文件系统错误。
52
+ */
53
+ function removeWorkingDir(targetDir) {
54
+ rmSync(targetDir, REMOVE_DIR_OPTIONS);
55
+ }
56
+ /**
57
+ * 以参数数组执行 Git,避免把 vendor 配置拼进 shell 命令字符串。
58
+ */
59
+ function runGit(args, cwd) {
60
+ const printableCommand = ['git', ...args].join(' ');
61
+ console.log(`[GIT] ${printableCommand} (in ${cwd})`);
62
+ const result = spawnSync('git', args, { cwd, stdio: 'inherit' });
63
+ if (result.error) {
64
+ throw result.error;
65
+ }
66
+ if (result.status !== 0) {
67
+ throw new Error(`Git command failed with exit code ${result.status}: ${printableCommand}`);
68
+ }
69
+ }
70
+ function rememberTarget(targets, target, source) {
71
+ const key = resolve(target).toLowerCase();
72
+ const existingSource = targets.get(key);
73
+ if (existingSource && resolve(existingSource) !== resolve(source)) {
74
+ throw new Error(`Flattened vendor skill target collision "${target}": ${existingSource} conflicts with ${source}`);
75
+ }
76
+ targets.set(key, source);
77
+ }
78
+ /**
79
+ * 供应商技能同步脚本 (核心逻辑)
80
+ *
81
+ * 缓存策略:
82
+ * - 计算 constants/ 目录的内容指纹
83
+ * - 与上次同步时保存的指纹对比
84
+ * - 指纹相同 → 跳过同步(constants 未变化)
85
+ * - 指纹不同 / 无 lockfile → 执行完整同步并更新指纹
86
+ *
87
+ * 使用方式:
88
+ * npx tsx scripts/sync-vendors.ts # 自动判断是否需要同步
89
+ * npx tsx scripts/sync-vendors.ts --force # 忽略缓存,强制重新克隆
90
+ */
91
+ async function main() {
92
+ const force = process.argv.includes('--force');
93
+ const currentFingerprint = computeConstantsFingerprint();
94
+ const savedFingerprint = readSavedFingerprint();
95
+ if (!force && currentFingerprint === savedFingerprint) {
96
+ console.log('[SYNC] constants/ 未发生变化,跳过供应商同步。');
97
+ console.log('[SYNC] 使用 --force 可忽略缓存强制重新克隆。');
98
+ return;
99
+ }
100
+ if (force) {
101
+ console.log('[SYNC] --force 模式:忽略缓存,强制重新同步。');
102
+ }
103
+ else {
104
+ console.log('[SYNC] 检测到 constants/ 已变化,开始重新同步...');
105
+ }
106
+ // 1. 清理环境
107
+ if (existsSync(VENDOR_SKILLS_DIR)) {
108
+ removeWorkingDir(VENDOR_SKILLS_DIR);
109
+ }
110
+ if (existsSync(CACHE_DIR)) {
111
+ removeWorkingDir(CACHE_DIR);
112
+ }
113
+ mkdirSync(VENDOR_SKILLS_DIR, { recursive: true });
114
+ mkdirSync(CACHE_DIR, { recursive: true });
115
+ // 2. 构造同步清单
116
+ const vendorsMap = {};
117
+ walkVendorTree(vendorsConfig, [], vendorsMap);
118
+ // 3. 处理每个供应商
119
+ const copiedTargets = new Map();
120
+ for (const [vendorName, vendor] of Object.entries(vendorsMap)) {
121
+ if (!vendor.repo)
122
+ continue;
123
+ const cacheTarget = join(CACHE_DIR, vendorName);
124
+ console.log(`\n--- 正在拉取远程供应商: ${vendorName} ---`);
125
+ try {
126
+ runGit(['clone', '--filter=blob:none', '--no-checkout', vendor.repo, vendorName], CACHE_DIR);
127
+ const checkoutPaths = new Set();
128
+ for (const link of vendor.links) {
129
+ checkoutPaths.add(link.source);
130
+ }
131
+ if (checkoutPaths.size > 0) {
132
+ runGit(['sparse-checkout', 'set', ...checkoutPaths], cacheTarget);
133
+ }
134
+ runGit(['checkout'], cacheTarget);
135
+ for (const link of vendor.links) {
136
+ const sourcePath = join(cacheTarget, link.source);
137
+ if (!existsSync(sourcePath)) {
138
+ throw new Error(`供应商 ${vendorName} 缺失配置的源目录: ${link.source}`);
139
+ }
140
+ const sources = link.kind === 'namespace-dir'
141
+ ? collectFlattenedSkillSources(sourcePath).map(skill => ({
142
+ source: skill.source,
143
+ target: join(PROJECT_ROOT, 'vendor', 'skills', skill.name),
144
+ }))
145
+ : [{
146
+ source: sourcePath,
147
+ target: join(PROJECT_ROOT, 'vendor', 'skills', flattenedSkillName(link.target)),
148
+ }];
149
+ for (const source of sources) {
150
+ rememberTarget(copiedTargets, source.target, source.source);
151
+ mkdirSync(dirname(source.target), { recursive: true });
152
+ cpSync(source.source, source.target, { recursive: true });
153
+ console.log(`[COPIED] ${vendorName} -> ${source.target.replace(PROJECT_ROOT, '').replace(/^[/\\]/, '')}`);
154
+ }
155
+ }
156
+ }
157
+ catch (err) {
158
+ console.error(`[ERROR] 供应商 ${vendorName} 同步失败:`, err);
159
+ throw err;
160
+ }
161
+ }
162
+ // 4. 清除 Git 缓存
163
+ console.log('\n[SYNC] 正在清理远程缓存...');
164
+ removeWorkingDir(CACHE_DIR);
165
+ // 5. 保存本次同步的指纹
166
+ saveSyncFingerprint(currentFingerprint);
167
+ console.log(`[SYNC] 已更新同步指纹: ${currentFingerprint.slice(0, 12)}...`);
168
+ console.log('\n[SYNC] 供应商技能同步完成。');
169
+ }
170
+ main().catch((error) => {
171
+ console.error(error);
172
+ process.exitCode = 1;
173
+ });
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env npx tsx
2
+ export {};
@@ -0,0 +1,42 @@
1
+ #!/usr/bin/env npx tsx
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import { parseArgs as nodeParseArgs } from 'node:util';
5
+ import { ALL_HOST_IDS } from '../constants/hosts.js';
6
+ import { verifyHost } from './lib/verify.js';
7
+ async function main() {
8
+ const result = nodeParseArgs({
9
+ options: {
10
+ 'host': { type: 'string' },
11
+ 'home': { type: 'string' },
12
+ 'user-home': { type: 'string' },
13
+ },
14
+ strict: false,
15
+ });
16
+ const { values } = result;
17
+ const { host, home } = values;
18
+ const moluoHome = home ? path.resolve(home) : path.join(os.homedir(), '.moluoxixi');
19
+ const userHome = values['user-home'] === undefined ? os.homedir() : path.resolve(String(values['user-home']));
20
+ if (!host || (host !== 'all' && !ALL_HOST_IDS.includes(host))) {
21
+ console.error(`Usage: npx tsx scripts/verify-host.ts --host <name|all> [--home <dir>] [--user-home <dir>]`);
22
+ process.exit(1);
23
+ }
24
+ const targets = host === 'all' ? ALL_HOST_IDS : [host];
25
+ let allPerfect = true;
26
+ for (const targetHost of targets) {
27
+ const success = await verifyHost(targetHost, moluoHome, userHome);
28
+ if (!success)
29
+ allPerfect = false;
30
+ }
31
+ if (allPerfect) {
32
+ console.log('\n✅ [SUCCESS] 所有的验证检查均通过!所有链接均正确且有效。');
33
+ }
34
+ else {
35
+ console.log('\n❌ [FAILURE] 部分验证检查未通过,请检查上述错误信息。');
36
+ process.exit(1);
37
+ }
38
+ }
39
+ main().catch((error) => {
40
+ console.error(error);
41
+ process.exit(1);
42
+ });