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,367 @@
1
+ import { execFileSync } from 'node:child_process';
2
+ import { cpSync, existsSync, lstatSync, mkdirSync, readdirSync, realpathSync, rmSync, symlinkSync, writeFileSync, } from 'node:fs';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+ import { findHostConfig, resolveHostPaths } from '../../constants/hosts.js';
6
+ import { buildLinkPlan } from './links.js';
7
+ import { collectFlattenedSkillSources, discoverSkillDirectories, flattenedSkillName } from './skill-projection.js';
8
+ import { loadVendorManifest } from './vendors.js';
9
+ // ─── 路径辅助函数:集中管理重复路径模式 ──────────────────────────────────────
10
+ /** 基线文件文件名(宿主与 vendor 目录下均使用此名) */
11
+ const BASELINE_FILE_NAME = 'AGENTS.md';
12
+ /** 仓库内基线源文件位于 rules/ 目录 */
13
+ const BASELINE_SOURCE_PATH = path.join('rules', BASELINE_FILE_NAME);
14
+ /** 获取基线文件在 vendor 目录下的绝对路径(所有宿主软链接的统一源) */
15
+ function vendorBaselinePath(moluoHome) {
16
+ return path.join(moluoHome, 'vendor', BASELINE_FILE_NAME);
17
+ }
18
+ /** 获取 vendor 技能目录的绝对路径 */
19
+ function vendorSkillsPath(homeDir) {
20
+ return path.join(homeDir, 'vendor', 'skills');
21
+ }
22
+ /** 获取全局 .agents/skills 目录的绝对路径 */
23
+ function agentsSkillsPath(userHome) {
24
+ return path.join(userHome, '.agents', 'skills');
25
+ }
26
+ function resetDir(targetDir) {
27
+ rmSync(targetDir, { recursive: true, force: true });
28
+ mkdirSync(targetDir, { recursive: true });
29
+ }
30
+ /**
31
+ * 执行各 skill 的安装前置命令(per-skill 精度)。
32
+ * 遍历 manifest 中所有 vendor 的 links,对具有 setup 字段的 link 执行其命令。
33
+ * 任一命令失败都会抛出错误,避免安装流程伪装成功。
34
+ * @param manifest 已解析的 VendorManifest
35
+ */
36
+ export function runSkillSetupCommands(manifest) {
37
+ for (const [vendorName, vendor] of Object.entries(manifest.vendors)) {
38
+ for (const link of vendor.links) {
39
+ if (!link.setup || link.setup.length === 0)
40
+ continue;
41
+ const skillName = path.basename(link.target);
42
+ console.log(`\n[setup] 执行 ${vendorName}/${skillName} 的安装前置命令...`);
43
+ for (const command of link.setup) {
44
+ const commandText = setupCommandText(command);
45
+ console.log(`[setup] > ${commandText}`);
46
+ try {
47
+ execFileSync(command.command, command.args ?? [], { stdio: 'inherit' });
48
+ }
49
+ catch (error) {
50
+ throw new Error(`[setup] ${vendorName}/${skillName} 安装前置命令失败: ${commandText}\n${String(error)}`);
51
+ }
52
+ }
53
+ }
54
+ }
55
+ }
56
+ function setupCommandText(command) {
57
+ return [command.command, ...(command.args ?? [])].join(' ');
58
+ }
59
+ function copyDirContents(sourceDir, targetDir, options = {}) {
60
+ mkdirSync(targetDir, { recursive: true });
61
+ for (const entry of readdirSync(sourceDir, { withFileTypes: true })) {
62
+ const source = path.join(sourceDir, entry.name);
63
+ const target = path.join(targetDir, entry.name);
64
+ const sourceStats = lstatSync(source);
65
+ if (options.skipSymlinks && sourceStats.isSymbolicLink()) {
66
+ continue;
67
+ }
68
+ const copySource = sourceStats.isSymbolicLink() ? realpathSync(source) : source;
69
+ rmSync(target, { recursive: true, force: true });
70
+ cpSync(copySource, target, { recursive: true });
71
+ }
72
+ }
73
+ function copyRequiredFile(sourceFile, targetFile) {
74
+ mkdirSync(path.dirname(targetFile), { recursive: true });
75
+ rmSync(targetFile, { recursive: true, force: true });
76
+ cpSync(sourceFile, targetFile);
77
+ }
78
+ function syncOptionalDir(sourceDir, targetDir) {
79
+ if (!existsSync(sourceDir)) {
80
+ rmSync(targetDir, { recursive: true, force: true });
81
+ return;
82
+ }
83
+ resetDir(targetDir);
84
+ copyDirContents(sourceDir, targetDir);
85
+ }
86
+ function rememberFlattenedTarget(targets, target, source) {
87
+ const targetKey = path.resolve(target).toLowerCase();
88
+ const previousSource = targets.get(targetKey);
89
+ if (previousSource && !isSamePath(previousSource, source)) {
90
+ throw new Error(`Flattened skill target collision "${target}": ${previousSource} conflicts with ${source}`);
91
+ }
92
+ targets.set(targetKey, source);
93
+ }
94
+ export function isSamePath(p1, p2) {
95
+ if (!p1 || !p2)
96
+ return false;
97
+ const n1 = path.resolve(p1).toLowerCase().replace(/\\/g, '/').replace(/\/$/, '');
98
+ const n2 = path.resolve(p2).toLowerCase().replace(/\\/g, '/').replace(/\/$/, '');
99
+ return n1 === n2;
100
+ }
101
+ function linkTypeForCurrentPlatform() {
102
+ return process.platform === 'win32' ? 'junction' : 'dir';
103
+ }
104
+ function linkFileForCurrentPlatform() {
105
+ return 'file';
106
+ }
107
+ export function replaceWithSymlink(source, target, type) {
108
+ if (isSamePath(source, target)) {
109
+ return;
110
+ }
111
+ // 如果目标已经是一个软链接并指向了源码,则无需重复创建
112
+ if (existsSync(target) && lstatSync(target).isSymbolicLink()) {
113
+ if (isSamePath(realpathSync(target), source)) {
114
+ return;
115
+ }
116
+ }
117
+ mkdirSync(path.dirname(target), { recursive: true });
118
+ rmSync(target, { recursive: true, force: true });
119
+ try {
120
+ symlinkSync(source, target, type);
121
+ }
122
+ catch (error) {
123
+ const fileSystemError = error;
124
+ if (type === 'file' && fileSystemError.code === 'EPERM' && process.platform === 'win32') {
125
+ cpSync(source, target);
126
+ return;
127
+ }
128
+ throw error;
129
+ }
130
+ }
131
+ export function getDefaultInstallPaths(userHome = os.homedir()) {
132
+ const moluoHome = path.join(userHome, '.moluoxixi');
133
+ return {
134
+ userHome,
135
+ moluoHome,
136
+ repoRoot: moluoHome,
137
+ moluoBaselineFile: vendorBaselinePath(moluoHome),
138
+ globalAgentSkillsHome: agentsSkillsPath(userHome),
139
+ };
140
+ }
141
+ export function ensureInstallRoot(paths) {
142
+ for (const dir of [
143
+ paths.moluoHome,
144
+ path.join(paths.moluoHome, 'vendor'),
145
+ path.join(paths.moluoHome, 'vendor', 'repos'),
146
+ vendorSkillsPath(paths.moluoHome),
147
+ paths.globalAgentSkillsHome,
148
+ ]) {
149
+ mkdirSync(dir, { recursive: true });
150
+ }
151
+ }
152
+ /**
153
+ * 确保全局 Agent 技能目录 (~/.agents/skills) 的链接正确。
154
+ * ~/.agents 是行业标准共享层,始终存在。
155
+ * 直接从 vendor/skills 映射,无中间层。
156
+ * 遵循层级自愈同步逻辑。
157
+ */
158
+ export function ensureGlobalSkillLink(paths) {
159
+ const sourceSkillsDir = vendorSkillsPath(paths.moluoHome);
160
+ const targetLinkDir = paths.globalAgentSkillsHome;
161
+ syncFlattenedSkills(sourceSkillsDir, targetLinkDir, paths.moluoHome);
162
+ }
163
+ /**
164
+ * 同步展平的技能软链接,并清理过时链接。
165
+ * @param sourceDir 源技能目录
166
+ * @param targetDir 目标链接目录
167
+ * @param moluoHome moluoxixi 根目录(用于识别自愈时需要删除的内部链接)
168
+ */
169
+ export function syncFlattenedSkills(sourceDir, targetDir, moluoHome) {
170
+ if (!existsSync(sourceDir)) {
171
+ return;
172
+ }
173
+ mkdirSync(targetDir, { recursive: true });
174
+ const skillSources = collectFlattenedSkillSources(sourceDir);
175
+ const currentSkills = new Set(skillSources.map(skill => skill.name));
176
+ // 自愈式同步:清理目标目录中不再需要的技能链接
177
+ if (existsSync(targetDir)) {
178
+ for (const entry of readdirSync(targetDir, { withFileTypes: true })) {
179
+ const targetPath = path.join(targetDir, entry.name);
180
+ if (entry.isSymbolicLink()) {
181
+ const isBroken = !existsSync(targetPath);
182
+ if (isBroken) {
183
+ rmSync(targetPath, { recursive: true, force: true });
184
+ console.log(`[cleanup] 已移除失效的死链接: ${entry.name}`);
185
+ continue;
186
+ }
187
+ const resolvedPath = realpathSync(targetPath);
188
+ const normalizedResolved = path.resolve(resolvedPath);
189
+ const normalizedMoluo = path.resolve(moluoHome);
190
+ const normalizedRepo = path.resolve(process.cwd()); // 仓库根目录
191
+ const isInternal = normalizedResolved.startsWith(normalizedMoluo)
192
+ || normalizedResolved.startsWith(normalizedRepo);
193
+ // 如果该链接指向我们的项目,但不在当前技能集合中,则视为过时并移除
194
+ if (isInternal && !currentSkills.has(entry.name)) {
195
+ rmSync(targetPath, { recursive: true, force: true });
196
+ }
197
+ }
198
+ }
199
+ }
200
+ // 为所有当前有效的技能创建或更新软链接
201
+ for (const skill of skillSources) {
202
+ const source = skill.source;
203
+ const target = path.join(targetDir, skill.name);
204
+ replaceWithSymlink(source, target, linkTypeForCurrentPlatform());
205
+ }
206
+ }
207
+ /**
208
+ * 同步第一方(当前仓库内)的 agents 和基线文件到本地 moluoxixi 主目录。
209
+ * skills 统一走 clone → vendor/skills 流程,不在此处理。
210
+ *
211
+ * rules/AGENTS.md 始终复制到 vendor/ 目录下,作为所有宿主基线软链接的统一源。
212
+ * 即使 repoRoot === moluoHome(仓库本身就是安装目录),也需要执行此步骤,
213
+ * 因为软链接最终指向的是 vendor/AGENTS.md。
214
+ */
215
+ export function syncFirstPartyToHome(repoRoot, moluoHome) {
216
+ // rules/AGENTS.md 始终同步到 vendor/ 下(所有宿主基线的软链接源)
217
+ copyRequiredFile(path.join(repoRoot, BASELINE_SOURCE_PATH), vendorBaselinePath(moluoHome));
218
+ if (isSamePath(repoRoot, moluoHome)) {
219
+ return;
220
+ }
221
+ syncOptionalDir(path.join(repoRoot, 'agents'), path.join(moluoHome, 'agents'));
222
+ }
223
+ /**
224
+ * 将第一方 skills 源目录投影到 vendor/skills,作为第三方 vendor 后的本地覆盖层。
225
+ * 该函数只清理曾经指向同一 source skills 根目录的过时链接,不会删除第三方 vendor 技能。
226
+ */
227
+ export function syncFirstPartySkillsToVendor(sourceRoot, moluoHome) {
228
+ const sourceSkillsDir = path.join(sourceRoot, 'skills');
229
+ if (!existsSync(sourceSkillsDir)) {
230
+ return;
231
+ }
232
+ const vendorSkillsDir = vendorSkillsPath(moluoHome);
233
+ mkdirSync(vendorSkillsDir, { recursive: true });
234
+ const seenSkillNames = new Map();
235
+ const skillSources = discoverSkillDirectories(sourceSkillsDir).map((source) => {
236
+ const name = flattenedSkillName(path.basename(source));
237
+ const nameKey = name.toLowerCase();
238
+ const previousSource = seenSkillNames.get(nameKey);
239
+ if (previousSource && path.resolve(previousSource) !== path.resolve(source)) {
240
+ throw new Error(`First-party skill name collision "${name}": ${previousSource} conflicts with ${source}`);
241
+ }
242
+ seenSkillNames.set(nameKey, source);
243
+ return { name, source };
244
+ });
245
+ const currentSkillNames = new Set(skillSources.map(skill => skill.name));
246
+ const normalizedSourceSkillsDir = path.resolve(sourceSkillsDir);
247
+ for (const entry of readdirSync(vendorSkillsDir, { withFileTypes: true })) {
248
+ const targetPath = path.join(vendorSkillsDir, entry.name);
249
+ if (!entry.isSymbolicLink()) {
250
+ continue;
251
+ }
252
+ if (!existsSync(targetPath)) {
253
+ rmSync(targetPath, { recursive: true, force: true });
254
+ continue;
255
+ }
256
+ const resolvedPath = path.resolve(realpathSync(targetPath));
257
+ if (resolvedPath.startsWith(normalizedSourceSkillsDir) && !currentSkillNames.has(entry.name)) {
258
+ rmSync(targetPath, { recursive: true, force: true });
259
+ }
260
+ }
261
+ for (const skill of skillSources) {
262
+ replaceWithSymlink(skill.source, path.join(vendorSkillsDir, skill.name), linkTypeForCurrentPlatform());
263
+ }
264
+ }
265
+ export async function rebuildVendorSkillLinks({ homeDir, manifestPath }) {
266
+ const manifest = await loadVendorManifest(manifestPath);
267
+ const plan = buildLinkPlan(manifest, homeDir);
268
+ const vendorSkillsDir = vendorSkillsPath(homeDir);
269
+ const targetSources = new Map();
270
+ resetDir(vendorSkillsDir);
271
+ for (const entry of plan) {
272
+ if (!existsSync(entry.source)) {
273
+ continue;
274
+ }
275
+ const linkSources = entry.kind === 'namespace-dir'
276
+ ? collectFlattenedSkillSources(entry.source).map(skill => ({
277
+ source: skill.source,
278
+ target: path.join(vendorSkillsDir, skill.name),
279
+ }))
280
+ : [{
281
+ source: entry.source,
282
+ target: path.join(vendorSkillsDir, flattenedSkillName(path.basename(entry.target))),
283
+ }];
284
+ for (const linkSource of linkSources) {
285
+ rememberFlattenedTarget(targetSources, linkSource.target, linkSource.source);
286
+ if (isSamePath(linkSource.source, linkSource.target)) {
287
+ console.log(`[link] Skip (source === target): ${linkSource.target}`);
288
+ continue;
289
+ }
290
+ mkdirSync(path.dirname(linkSource.target), { recursive: true });
291
+ replaceWithSymlink(linkSource.source, linkSource.target, linkTypeForCurrentPlatform());
292
+ }
293
+ }
294
+ // 为 vendor/skills 生成 .gitignore
295
+ const projectedSkillNames = readdirSync(vendorSkillsDir).filter(n => !n.startsWith('.'));
296
+ const gitignoreContent = [
297
+ '# 由 rebuildVendorSkillLinks 自动生成,请勿手动编辑',
298
+ '# 这些 vendor skill 软链接应被 git 忽略',
299
+ ...projectedSkillNames,
300
+ '',
301
+ ].join('\n');
302
+ mkdirSync(vendorSkillsDir, { recursive: true });
303
+ writeFileSync(path.join(vendorSkillsDir, '.gitignore'), gitignoreContent, 'utf8');
304
+ return plan;
305
+ }
306
+ /**
307
+ * 将所有技能投影到宿主软件目录(如 .claude 或 .cursor)。
308
+ * 链路:vendor/skills → ~/.agents/skills → 宿主/skills
309
+ * ~/.agents 是行业标准共享层,始终存在(不存在则创建)。
310
+ */
311
+ export function projectSkillsToHost(userHome, moluoHome, hostSkillsHome) {
312
+ const sourceSkillsDir = vendorSkillsPath(moluoHome);
313
+ const agentsSkillsDir = agentsSkillsPath(userHome);
314
+ // 1. vendor/skills → ~/.agents/skills
315
+ mkdirSync(path.join(userHome, '.agents'), { recursive: true });
316
+ syncFlattenedSkills(sourceSkillsDir, agentsSkillsDir, moluoHome);
317
+ // 2. ~/.agents/skills → 宿主 skills 目录
318
+ syncFlattenedSkills(agentsSkillsDir, hostSkillsHome, moluoHome);
319
+ }
320
+ function projectSharedSkillsHost(userHome, hostHome, moluoHome, customSkillsDirName = 'skills') {
321
+ mkdirSync(hostHome, { recursive: true });
322
+ rmSync(path.join(hostHome, 'rules'), { recursive: true, force: true });
323
+ rmSync(path.join(hostHome, 'agents'), { recursive: true, force: true });
324
+ projectSkillsToHost(userHome, moluoHome, path.join(hostHome, customSkillsDirName));
325
+ if (existsSync(path.join(moluoHome, 'agents'))) {
326
+ const agentsSource = path.join(moluoHome, 'agents');
327
+ const agentsTarget = path.join(hostHome, 'agents');
328
+ replaceWithSymlink(agentsSource, agentsTarget, linkTypeForCurrentPlatform());
329
+ }
330
+ }
331
+ export function projectToHost({ userHome, moluoHome, hostHome, hostBaselineFile, customSkillsDirName = 'skills', }) {
332
+ projectSharedSkillsHost(userHome, hostHome, moluoHome, customSkillsDirName);
333
+ replaceWithSymlink(vendorBaselinePath(moluoHome), hostBaselineFile, linkFileForCurrentPlatform());
334
+ }
335
+ export function linkHostBaseline({ moluoHome, host, userHome = os.homedir() }) {
336
+ const source = vendorBaselinePath(moluoHome);
337
+ const config = findHostConfig(host);
338
+ if (!config) {
339
+ throw new Error(`Unknown host: ${host}`);
340
+ }
341
+ const { hostBaselineFile } = resolveHostPaths(config, userHome);
342
+ replaceWithSymlink(source, hostBaselineFile, linkFileForCurrentPlatform());
343
+ return hostBaselineFile;
344
+ }
345
+ /**
346
+ * 将技能和基线投影到指定宿主,并返回是否成功(宿主目录不存在则跳过)。
347
+ */
348
+ export function projectHostById(host, userHome, moluoHome) {
349
+ const config = findHostConfig(host);
350
+ if (!config) {
351
+ throw new Error(`Unknown host: ${host}`);
352
+ }
353
+ const { hostHome, hostBaselineFile, skillsDirName } = resolveHostPaths(config, userHome);
354
+ const hostHomePath = path.resolve(hostHome);
355
+ if (!existsSync(hostHomePath)) {
356
+ console.warn(`[skip] 宿主目录不存在,跳过投影: ${host} (${hostHomePath})`);
357
+ return { success: false, hostBaselineFile };
358
+ }
359
+ projectToHost({
360
+ userHome,
361
+ moluoHome,
362
+ hostHome,
363
+ hostBaselineFile,
364
+ customSkillsDirName: skillsDirName,
365
+ });
366
+ return { success: true, hostBaselineFile };
367
+ }
@@ -0,0 +1,8 @@
1
+ import type { VendorManifest } from './vendors.js';
2
+ export interface LinkEntry {
3
+ kind: string;
4
+ vendorId: string;
5
+ source: string;
6
+ target: string;
7
+ }
8
+ export declare function buildLinkPlan(manifest: VendorManifest, homeDir: string): LinkEntry[];
@@ -0,0 +1,18 @@
1
+ import path from 'node:path';
2
+ import { resolveHomePath } from './vendors.js';
3
+ export function buildLinkPlan(manifest, homeDir) {
4
+ const plan = [];
5
+ for (const [vendorId, vendor] of Object.entries(manifest.vendors ?? {})) {
6
+ for (const link of vendor.links ?? []) {
7
+ const source = resolveHomePath(homeDir, path.posix.join(vendor.cloneDir, link.source));
8
+ const target = resolveHomePath(homeDir, link.target);
9
+ plan.push({
10
+ kind: link.kind,
11
+ vendorId,
12
+ source,
13
+ target,
14
+ });
15
+ }
16
+ }
17
+ return plan.sort((left, right) => left.target.localeCompare(right.target));
18
+ }
@@ -0,0 +1,14 @@
1
+ export interface FlattenedSkillSource {
2
+ name: string;
3
+ source: string;
4
+ }
5
+ /**
6
+ * vendor/skills 是分发边界,只暴露叶子 skill 名称,不继承源仓库分类路径。
7
+ */
8
+ export declare function flattenedSkillName(value: string): string;
9
+ export declare function flattenedVendorSkillTarget(outputName: string): string;
10
+ /**
11
+ * 递归收集真实 skill 根目录;包含 SKILL.md 的目录即为叶子 skill,内部子目录不再继续展开。
12
+ */
13
+ export declare function discoverSkillDirectories(rootDir: string): string[];
14
+ export declare function collectFlattenedSkillSources(sourceDir: string): FlattenedSkillSource[];
@@ -0,0 +1,54 @@
1
+ import { existsSync, readdirSync, statSync } from 'node:fs';
2
+ import path from 'node:path';
3
+ const SKILL_FILE_NAME = 'SKILL.md';
4
+ /**
5
+ * vendor/skills 是分发边界,只暴露叶子 skill 名称,不继承源仓库分类路径。
6
+ */
7
+ export function flattenedSkillName(value) {
8
+ return path.posix.basename(value.replace(/\\/g, '/'));
9
+ }
10
+ export function flattenedVendorSkillTarget(outputName) {
11
+ return path.posix.join('vendor', 'skills', flattenedSkillName(outputName));
12
+ }
13
+ /**
14
+ * 递归收集真实 skill 根目录;包含 SKILL.md 的目录即为叶子 skill,内部子目录不再继续展开。
15
+ */
16
+ export function discoverSkillDirectories(rootDir) {
17
+ const skillDirs = [];
18
+ function visit(currentDir) {
19
+ if (existsSync(path.join(currentDir, SKILL_FILE_NAME))) {
20
+ skillDirs.push(currentDir);
21
+ return;
22
+ }
23
+ const entries = readdirSync(currentDir, { withFileTypes: true })
24
+ .sort((left, right) => left.name.localeCompare(right.name));
25
+ for (const entry of entries) {
26
+ const childDir = path.join(currentDir, entry.name);
27
+ if (entry.isDirectory() || (entry.isSymbolicLink() && statSync(childDir).isDirectory())) {
28
+ visit(childDir);
29
+ }
30
+ }
31
+ }
32
+ visit(rootDir);
33
+ return skillDirs.sort((left, right) => left.localeCompare(right));
34
+ }
35
+ export function collectFlattenedSkillSources(sourceDir) {
36
+ const discoveredSources = discoverSkillDirectories(sourceDir);
37
+ const sources = discoveredSources.length > 0
38
+ ? discoveredSources
39
+ : readdirSync(sourceDir)
40
+ .filter(name => name !== '.gitignore')
41
+ .sort((left, right) => left.localeCompare(right))
42
+ .map(name => path.join(sourceDir, name));
43
+ const seenNames = new Map();
44
+ return sources.map((source) => {
45
+ const name = flattenedSkillName(path.basename(source));
46
+ const nameKey = name.toLowerCase();
47
+ const previousSource = seenNames.get(nameKey);
48
+ if (previousSource && path.resolve(previousSource) !== path.resolve(source)) {
49
+ throw new Error(`Flattened skill name collision "${name}": ${previousSource} conflicts with ${source}`);
50
+ }
51
+ seenNames.set(nameKey, source);
52
+ return { name, source };
53
+ });
54
+ }
@@ -0,0 +1,40 @@
1
+ export interface ToolPaths {
2
+ repoRoot: string;
3
+ moluoHome: string;
4
+ userHome: string;
5
+ manifestPath: string;
6
+ }
7
+ export interface SyncOptions {
8
+ repoRoot: string;
9
+ home: string;
10
+ userHome?: string;
11
+ host: string;
12
+ skipVendors: boolean;
13
+ verify: boolean;
14
+ }
15
+ export interface SyncResult {
16
+ moluoHome: string;
17
+ projectedHosts: string[];
18
+ skippedHosts: string[];
19
+ }
20
+ export interface AddSkillOptions {
21
+ sourceDir: string;
22
+ moluoHome: string;
23
+ name?: string;
24
+ overwrite: boolean;
25
+ }
26
+ export interface AddSkillResult {
27
+ skillName: string;
28
+ targetDir: string;
29
+ }
30
+ export interface VerifyOptions {
31
+ home: string;
32
+ userHome?: string;
33
+ host: string;
34
+ }
35
+ export declare function getDefaultMoluoHome(): string;
36
+ export declare function resolveToolPaths(repoRoot: string, home: string, userHome?: string): ToolPaths;
37
+ export declare function resolveHostTargets(host: string): string[];
38
+ export declare function addLocalSkill(options: AddSkillOptions): AddSkillResult;
39
+ export declare function syncToHosts(options: SyncOptions): Promise<SyncResult>;
40
+ export declare function verifyHosts(options: VerifyOptions): Promise<string[]>;
@@ -0,0 +1,125 @@
1
+ import { cpSync, existsSync, mkdirSync, rmSync } from 'node:fs';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import { ALL_HOST_IDS } from '../../constants/hosts.js';
5
+ import { ensureInstallRoot, getDefaultInstallPaths, isSamePath, linkHostBaseline, projectHostById, rebuildVendorSkillLinks, runSkillSetupCommands, syncFirstPartySkillsToVendor, syncFirstPartyToHome, } from './install.js';
6
+ import { flattenedSkillName } from './skill-projection.js';
7
+ import { ensureVendorRepo } from './vendor-sync.js';
8
+ import { loadVendorManifest } from './vendors.js';
9
+ import { verifyHost } from './verify.js';
10
+ export function getDefaultMoluoHome() {
11
+ return path.join(os.homedir(), '.moluoxixi');
12
+ }
13
+ function resolveManifestPath(repoRoot) {
14
+ const sourceManifestTs = path.join(repoRoot, 'constants', 'skills.ts');
15
+ const sourceManifestJs = path.join(repoRoot, 'constants', 'skills.js');
16
+ if (existsSync(sourceManifestTs) || existsSync(sourceManifestJs)) {
17
+ return sourceManifestJs;
18
+ }
19
+ return path.join(repoRoot, 'dist', 'constants', 'skills.js');
20
+ }
21
+ export function resolveToolPaths(repoRoot, home, userHome = os.homedir()) {
22
+ const moluoHome = path.resolve(home);
23
+ const resolvedRepoRoot = path.resolve(repoRoot);
24
+ return {
25
+ repoRoot: resolvedRepoRoot,
26
+ moluoHome,
27
+ userHome: path.resolve(userHome),
28
+ manifestPath: resolveManifestPath(resolvedRepoRoot),
29
+ };
30
+ }
31
+ export function resolveHostTargets(host) {
32
+ return host === 'all' ? ALL_HOST_IDS : [host];
33
+ }
34
+ async function syncVendorsIfNeeded(paths, skipVendors) {
35
+ if (skipVendors) {
36
+ return;
37
+ }
38
+ const manifest = await loadVendorManifest(paths.manifestPath);
39
+ for (const vendor of Object.values(manifest.vendors)) {
40
+ ensureVendorRepo(paths.moluoHome, vendor);
41
+ }
42
+ runSkillSetupCommands(manifest);
43
+ }
44
+ function syncLocalSkillLayers(paths) {
45
+ syncFirstPartySkillsToVendor(paths.repoRoot, paths.moluoHome);
46
+ if (!isSamePath(paths.repoRoot, paths.moluoHome)) {
47
+ syncFirstPartySkillsToVendor(paths.moluoHome, paths.moluoHome);
48
+ }
49
+ }
50
+ export function addLocalSkill(options) {
51
+ const sourceDir = path.resolve(options.sourceDir);
52
+ const skillName = flattenedSkillName(options.name ?? path.basename(sourceDir));
53
+ const targetDir = path.join(path.resolve(options.moluoHome), 'skills', skillName);
54
+ if (!existsSync(path.join(sourceDir, 'SKILL.md'))) {
55
+ throw new Error(`Skill source must contain SKILL.md: ${sourceDir}`);
56
+ }
57
+ if (existsSync(targetDir) && !options.overwrite && !isSamePath(sourceDir, targetDir)) {
58
+ throw new Error(`Skill already exists: ${targetDir}. Re-run with --overwrite to replace it.`);
59
+ }
60
+ if (isSamePath(sourceDir, targetDir)) {
61
+ return { skillName, targetDir };
62
+ }
63
+ mkdirSync(path.dirname(targetDir), { recursive: true });
64
+ rmSync(targetDir, { recursive: true, force: true });
65
+ cpSync(sourceDir, targetDir, { recursive: true });
66
+ return { skillName, targetDir };
67
+ }
68
+ export async function syncToHosts(options) {
69
+ const paths = resolveToolPaths(options.repoRoot, options.home, options.userHome);
70
+ const installPaths = getDefaultInstallPaths(paths.userHome);
71
+ installPaths.moluoHome = paths.moluoHome;
72
+ installPaths.repoRoot = paths.repoRoot;
73
+ ensureInstallRoot(installPaths);
74
+ syncFirstPartyToHome(paths.repoRoot, paths.moluoHome);
75
+ await syncVendorsIfNeeded(paths, options.skipVendors);
76
+ await rebuildVendorSkillLinks({
77
+ homeDir: paths.moluoHome,
78
+ manifestPath: paths.manifestPath,
79
+ });
80
+ syncLocalSkillLayers(paths);
81
+ const projectedHosts = [];
82
+ const skippedHosts = [];
83
+ const failedHosts = [];
84
+ for (const host of resolveHostTargets(options.host)) {
85
+ const { success } = projectHostById(host, paths.userHome, paths.moluoHome);
86
+ if (!success) {
87
+ skippedHosts.push(host);
88
+ continue;
89
+ }
90
+ projectedHosts.push(host);
91
+ linkHostBaseline({
92
+ moluoHome: paths.moluoHome,
93
+ host,
94
+ userHome: paths.userHome,
95
+ });
96
+ if (options.verify) {
97
+ const verified = await verifyHost(host, paths.moluoHome, paths.userHome);
98
+ if (!verified) {
99
+ failedHosts.push(host);
100
+ }
101
+ }
102
+ }
103
+ if (failedHosts.length > 0) {
104
+ throw new Error(`Host verification failed: ${failedHosts.join(', ')}`);
105
+ }
106
+ return {
107
+ moluoHome: paths.moluoHome,
108
+ projectedHosts,
109
+ skippedHosts,
110
+ };
111
+ }
112
+ export async function verifyHosts(options) {
113
+ const paths = resolveToolPaths(process.cwd(), options.home, options.userHome);
114
+ const failedHosts = [];
115
+ for (const host of resolveHostTargets(options.host)) {
116
+ const verified = await verifyHost(host, paths.moluoHome, paths.userHome);
117
+ if (!verified) {
118
+ failedHosts.push(host);
119
+ }
120
+ }
121
+ if (failedHosts.length > 0) {
122
+ throw new Error(`Host verification failed: ${failedHosts.join(', ')}`);
123
+ }
124
+ return resolveHostTargets(options.host);
125
+ }
@@ -0,0 +1,3 @@
1
+ import type { Vendor } from './vendors.js';
2
+ export declare function getRemoteDefaultBranch(cloneDir: string): string;
3
+ export declare function ensureVendorRepo(homeDir: string, vendor: Vendor): string;