skillpkg-core 0.2.0 → 0.3.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 (78) hide show
  1. package/dist/config/config-manager.d.ts +83 -0
  2. package/dist/config/config-manager.d.ts.map +1 -0
  3. package/dist/config/config-manager.js +220 -0
  4. package/dist/config/config-manager.js.map +1 -0
  5. package/dist/config/index.d.ts +23 -0
  6. package/dist/config/index.d.ts.map +1 -0
  7. package/dist/config/index.js +23 -0
  8. package/dist/config/index.js.map +1 -0
  9. package/dist/config/schemas/skillpkg.schema.json +103 -0
  10. package/dist/config/types.d.ts +69 -0
  11. package/dist/config/types.d.ts.map +1 -0
  12. package/dist/config/types.js +29 -0
  13. package/dist/config/types.js.map +1 -0
  14. package/dist/index.d.ts +6 -1
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +11 -1
  17. package/dist/index.js.map +1 -1
  18. package/dist/installer/index.d.ts +33 -0
  19. package/dist/installer/index.d.ts.map +1 -0
  20. package/dist/installer/index.js +33 -0
  21. package/dist/installer/index.js.map +1 -0
  22. package/dist/installer/installer.d.ts +54 -0
  23. package/dist/installer/installer.d.ts.map +1 -0
  24. package/dist/installer/installer.js +371 -0
  25. package/dist/installer/installer.js.map +1 -0
  26. package/dist/installer/types.d.ts +138 -0
  27. package/dist/installer/types.d.ts.map +1 -0
  28. package/dist/installer/types.js +2 -0
  29. package/dist/installer/types.js.map +1 -0
  30. package/dist/parser/schema.d.ts +26 -5
  31. package/dist/parser/schema.d.ts.map +1 -1
  32. package/dist/parser/schema.js +25 -3
  33. package/dist/parser/schema.js.map +1 -1
  34. package/dist/resolver/dependency-resolver.d.ts +59 -0
  35. package/dist/resolver/dependency-resolver.d.ts.map +1 -0
  36. package/dist/resolver/dependency-resolver.js +217 -0
  37. package/dist/resolver/dependency-resolver.js.map +1 -0
  38. package/dist/resolver/index.d.ts +35 -0
  39. package/dist/resolver/index.d.ts.map +1 -0
  40. package/dist/resolver/index.js +34 -0
  41. package/dist/resolver/index.js.map +1 -0
  42. package/dist/resolver/install-plan.d.ts +107 -0
  43. package/dist/resolver/install-plan.d.ts.map +1 -0
  44. package/dist/resolver/install-plan.js +137 -0
  45. package/dist/resolver/install-plan.js.map +1 -0
  46. package/dist/resolver/types.d.ts +76 -0
  47. package/dist/resolver/types.d.ts.map +1 -0
  48. package/dist/resolver/types.js +5 -0
  49. package/dist/resolver/types.js.map +1 -0
  50. package/dist/state/index.d.ts +30 -0
  51. package/dist/state/index.d.ts.map +1 -0
  52. package/dist/state/index.js +30 -0
  53. package/dist/state/index.js.map +1 -0
  54. package/dist/state/state-manager.d.ts +105 -0
  55. package/dist/state/state-manager.d.ts.map +1 -0
  56. package/dist/state/state-manager.js +242 -0
  57. package/dist/state/state-manager.js.map +1 -0
  58. package/dist/state/types.d.ts +86 -0
  59. package/dist/state/types.d.ts.map +1 -0
  60. package/dist/state/types.js +21 -0
  61. package/dist/state/types.js.map +1 -0
  62. package/dist/syncer/index.d.ts +28 -0
  63. package/dist/syncer/index.d.ts.map +1 -0
  64. package/dist/syncer/index.js +28 -0
  65. package/dist/syncer/index.js.map +1 -0
  66. package/dist/syncer/syncer.d.ts +74 -0
  67. package/dist/syncer/syncer.d.ts.map +1 -0
  68. package/dist/syncer/syncer.js +405 -0
  69. package/dist/syncer/syncer.js.map +1 -0
  70. package/dist/syncer/types.d.ts +143 -0
  71. package/dist/syncer/types.d.ts.map +1 -0
  72. package/dist/syncer/types.js +84 -0
  73. package/dist/syncer/types.js.map +1 -0
  74. package/dist/types.d.ts +27 -2
  75. package/dist/types.d.ts.map +1 -1
  76. package/dist/types.js +25 -0
  77. package/dist/types.js.map +1 -1
  78. package/package.json +9 -9
@@ -0,0 +1,86 @@
1
+ /**
2
+ * State types (state.json)
3
+ *
4
+ * Tracks installation state, dependencies, and sync history
5
+ */
6
+ import type { SyncTarget } from '../config/types.js';
7
+ /**
8
+ * Schema version for state.json
9
+ */
10
+ export declare const STATE_SCHEMA_VERSION = "skillpkg-state-v1";
11
+ /**
12
+ * Who installed a skill
13
+ */
14
+ export type InstalledBy = 'user' | string;
15
+ /**
16
+ * Skill installation state
17
+ */
18
+ export interface SkillState {
19
+ /** Installed version */
20
+ version: string;
21
+ /** Source (github:user/repo, URL, or local path) */
22
+ source: string;
23
+ /** Who installed this skill ('user' or skill name that depends on it) */
24
+ installed_by: InstalledBy;
25
+ /** Installation timestamp (ISO 8601) */
26
+ installed_at: string;
27
+ /** Skills that depend on this skill */
28
+ depended_by: string[];
29
+ }
30
+ /**
31
+ * MCP installation state
32
+ */
33
+ export interface McpState {
34
+ /** npm package name */
35
+ package: string;
36
+ /** Skill that required this MCP (null if manually installed) */
37
+ installed_by_skill: string | null;
38
+ /** Installation timestamp (ISO 8601) */
39
+ installed_at: string;
40
+ }
41
+ /**
42
+ * Sync history for each target
43
+ */
44
+ export type SyncHistory = Partial<Record<SyncTarget, string>>;
45
+ /**
46
+ * Complete state (state.json)
47
+ */
48
+ export interface State {
49
+ /** Schema version */
50
+ $schema: string;
51
+ /** Installed skills */
52
+ skills: Record<string, SkillState>;
53
+ /** Installed MCP servers */
54
+ mcp: Record<string, McpState>;
55
+ /** Last sync timestamp for each target */
56
+ sync_history: SyncHistory;
57
+ }
58
+ /**
59
+ * Skill install info (for recording)
60
+ */
61
+ export interface SkillInstallInfo {
62
+ version: string;
63
+ source: string;
64
+ installed_by: InstalledBy;
65
+ }
66
+ /**
67
+ * MCP install info (for recording)
68
+ */
69
+ export interface McpInstallInfo {
70
+ package: string;
71
+ installed_by_skill: string | null;
72
+ }
73
+ /**
74
+ * Uninstall check result
75
+ */
76
+ export interface UninstallCheck {
77
+ /** Can the skill be safely uninstalled */
78
+ canUninstall: boolean;
79
+ /** Skills that depend on this skill */
80
+ dependents: string[];
81
+ }
82
+ /**
83
+ * Create an empty state
84
+ */
85
+ export declare function createEmptyState(): State;
86
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/state/types.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAErD;;GAEG;AACH,eAAO,MAAM,oBAAoB,sBAAsB,CAAC;AAExD;;GAEG;AACH,MAAM,MAAM,WAAW,GAAG,MAAM,GAAG,MAAM,CAAC;AAE1C;;GAEG;AACH,MAAM,WAAW,UAAU;IACzB,wBAAwB;IACxB,OAAO,EAAE,MAAM,CAAC;IAChB,oDAAoD;IACpD,MAAM,EAAE,MAAM,CAAC;IACf,yEAAyE;IACzE,YAAY,EAAE,WAAW,CAAC;IAC1B,wCAAwC;IACxC,YAAY,EAAE,MAAM,CAAC;IACrB,uCAAuC;IACvC,WAAW,EAAE,MAAM,EAAE,CAAC;CACvB;AAED;;GAEG;AACH,MAAM,WAAW,QAAQ;IACvB,uBAAuB;IACvB,OAAO,EAAE,MAAM,CAAC;IAChB,gEAAgE;IAChE,kBAAkB,EAAE,MAAM,GAAG,IAAI,CAAC;IAClC,wCAAwC;IACxC,YAAY,EAAE,MAAM,CAAC;CACtB;AAED;;GAEG;AACH,MAAM,MAAM,WAAW,GAAG,OAAO,CAAC,MAAM,CAAC,UAAU,EAAE,MAAM,CAAC,CAAC,CAAC;AAE9D;;GAEG;AACH,MAAM,WAAW,KAAK;IACpB,qBAAqB;IACrB,OAAO,EAAE,MAAM,CAAC;IAChB,uBAAuB;IACvB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;IACnC,4BAA4B;IAC5B,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;IAC9B,0CAA0C;IAC1C,YAAY,EAAE,WAAW,CAAC;CAC3B;AAED;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,YAAY,EAAE,WAAW,CAAC;CAC3B;AAED;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE,MAAM,CAAC;IAChB,kBAAkB,EAAE,MAAM,GAAG,IAAI,CAAC;CACnC;AAED;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B,0CAA0C;IAC1C,YAAY,EAAE,OAAO,CAAC;IACtB,uCAAuC;IACvC,UAAU,EAAE,MAAM,EAAE,CAAC;CACtB;AAED;;GAEG;AACH,wBAAgB,gBAAgB,IAAI,KAAK,CAOxC"}
@@ -0,0 +1,21 @@
1
+ /**
2
+ * State types (state.json)
3
+ *
4
+ * Tracks installation state, dependencies, and sync history
5
+ */
6
+ /**
7
+ * Schema version for state.json
8
+ */
9
+ export const STATE_SCHEMA_VERSION = 'skillpkg-state-v1';
10
+ /**
11
+ * Create an empty state
12
+ */
13
+ export function createEmptyState() {
14
+ return {
15
+ $schema: STATE_SCHEMA_VERSION,
16
+ skills: {},
17
+ mcp: {},
18
+ sync_history: {},
19
+ };
20
+ }
21
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/state/types.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAIH;;GAEG;AACH,MAAM,CAAC,MAAM,oBAAoB,GAAG,mBAAmB,CAAC;AAiFxD;;GAEG;AACH,MAAM,UAAU,gBAAgB;IAC9B,OAAO;QACL,OAAO,EAAE,oBAAoB;QAC7B,MAAM,EAAE,EAAE;QACV,GAAG,EAAE,EAAE;QACP,YAAY,EAAE,EAAE;KACjB,CAAC;AACJ,CAAC"}
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Syncer module - Sync skills to AI tool directories
3
+ *
4
+ * @example
5
+ * ```typescript
6
+ * import { createSyncer, loadSkillsFromDirectory } from '@skillpkg/core';
7
+ *
8
+ * // Load skills
9
+ * const skills = await loadSkillsFromDirectory('.skillpkg/skills');
10
+ *
11
+ * // Create syncer
12
+ * const syncer = createSyncer();
13
+ *
14
+ * // Sync to all enabled targets
15
+ * const result = await syncer.syncAll(projectPath, skills, config);
16
+ *
17
+ * // Or sync to a specific target
18
+ * const targetResult = await syncer.syncToTarget(
19
+ * projectPath,
20
+ * skills,
21
+ * getTargetConfig('claude-code')
22
+ * );
23
+ * ```
24
+ */
25
+ export type { SyncFormat, FrontmatterHandling, TargetConfig, SkillContent, SyncerOptions, FileSyncResult, TargetSyncResult, SyncerResult, McpJsonConfig, } from './types.js';
26
+ export { TARGET_CONFIGS, getTargetConfig, getImplementedTargets, getAllTargets, } from './types.js';
27
+ export { Syncer, createSyncer, loadSkillContent, loadSkillsFromDirectory, } from './syncer.js';
28
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/syncer/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAGH,YAAY,EACV,UAAU,EACV,mBAAmB,EACnB,YAAY,EACZ,YAAY,EACZ,aAAa,EACb,cAAc,EACd,gBAAgB,EAChB,YAAY,EACZ,aAAa,GACd,MAAM,YAAY,CAAC;AAEpB,OAAO,EACL,cAAc,EACd,eAAe,EACf,qBAAqB,EACrB,aAAa,GACd,MAAM,YAAY,CAAC;AAGpB,OAAO,EACL,MAAM,EACN,YAAY,EACZ,gBAAgB,EAChB,uBAAuB,GACxB,MAAM,aAAa,CAAC"}
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Syncer module - Sync skills to AI tool directories
3
+ *
4
+ * @example
5
+ * ```typescript
6
+ * import { createSyncer, loadSkillsFromDirectory } from '@skillpkg/core';
7
+ *
8
+ * // Load skills
9
+ * const skills = await loadSkillsFromDirectory('.skillpkg/skills');
10
+ *
11
+ * // Create syncer
12
+ * const syncer = createSyncer();
13
+ *
14
+ * // Sync to all enabled targets
15
+ * const result = await syncer.syncAll(projectPath, skills, config);
16
+ *
17
+ * // Or sync to a specific target
18
+ * const targetResult = await syncer.syncToTarget(
19
+ * projectPath,
20
+ * skills,
21
+ * getTargetConfig('claude-code')
22
+ * );
23
+ * ```
24
+ */
25
+ export { TARGET_CONFIGS, getTargetConfig, getImplementedTargets, getAllTargets, } from './types.js';
26
+ // Syncer
27
+ export { Syncer, createSyncer, loadSkillContent, loadSkillsFromDirectory, } from './syncer.js';
28
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/syncer/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAeH,OAAO,EACL,cAAc,EACd,eAAe,EACf,qBAAqB,EACrB,aAAa,GACd,MAAM,YAAY,CAAC;AAEpB,SAAS;AACT,OAAO,EACL,MAAM,EACN,YAAY,EACZ,gBAAgB,EAChB,uBAAuB,GACxB,MAAM,aAAa,CAAC"}
@@ -0,0 +1,74 @@
1
+ import type { SkillpkgConfig, McpConfig } from '../config/types.js';
2
+ import type { StateManager } from '../state/index.js';
3
+ import type { TargetConfig, SkillContent, SyncerOptions, SyncerResult, TargetSyncResult } from './types.js';
4
+ /**
5
+ * Syncer class - syncs skills to AI tool directories
6
+ */
7
+ export declare class Syncer {
8
+ private stateManager?;
9
+ constructor(stateManager?: StateManager);
10
+ /**
11
+ * Sync all skills to all enabled targets
12
+ */
13
+ syncAll(projectPath: string, skills: Map<string, SkillContent>, config: SkillpkgConfig, options?: SyncerOptions): Promise<SyncerResult>;
14
+ /**
15
+ * Sync skills to a single target
16
+ */
17
+ syncToTarget(projectPath: string, skills: Map<string, SkillContent>, targetConfig: TargetConfig, options?: SyncerOptions): Promise<TargetSyncResult>;
18
+ /**
19
+ * Sync in directory format (each skill in its own directory)
20
+ */
21
+ private syncDirectoryFormat;
22
+ /**
23
+ * Sync in single-file format (all skills merged)
24
+ */
25
+ private syncSingleFileFormat;
26
+ /**
27
+ * Transform skill content for a specific target
28
+ */
29
+ transformForTarget(skill: SkillContent, targetConfig: TargetConfig): string;
30
+ /**
31
+ * Merge multiple skills into a single file
32
+ */
33
+ private mergeSkillsForTarget;
34
+ /**
35
+ * Cleanup orphaned skill directories
36
+ */
37
+ private cleanupOrphans;
38
+ /**
39
+ * Sync MCP configuration to .mcp.json
40
+ */
41
+ syncMcpConfig(projectPath: string, mcpConfigs: Record<string, McpConfig>, options?: SyncerOptions): Promise<{
42
+ path: string;
43
+ action: 'created' | 'updated' | 'unchanged';
44
+ }>;
45
+ /**
46
+ * Determine what action is needed for a file
47
+ */
48
+ private getFileAction;
49
+ /**
50
+ * Hash content for comparison
51
+ */
52
+ private hashContent;
53
+ /**
54
+ * Get list of enabled targets from config
55
+ */
56
+ private getEnabledTargets;
57
+ }
58
+ /**
59
+ * Create a Syncer instance
60
+ */
61
+ export declare function createSyncer(stateManager?: StateManager): Syncer;
62
+ /**
63
+ * Load skill content from a SKILL.md file
64
+ *
65
+ * Supports two formats:
66
+ * 1. YAML files (skill.yaml)
67
+ * 2. Markdown with YAML frontmatter (SKILL.md)
68
+ */
69
+ export declare function loadSkillContent(filePath: string): Promise<SkillContent>;
70
+ /**
71
+ * Load all skills from a directory
72
+ */
73
+ export declare function loadSkillsFromDirectory(skillsDir: string): Promise<Map<string, SkillContent>>;
74
+ //# sourceMappingURL=syncer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"syncer.d.ts","sourceRoot":"","sources":["../../src/syncer/syncer.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAA2B,cAAc,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAC7F,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACtD,OAAO,KAAK,EACV,YAAY,EACZ,YAAY,EACZ,aAAa,EACb,YAAY,EACZ,gBAAgB,EAEjB,MAAM,YAAY,CAAC;AAIpB;;GAEG;AACH,qBAAa,MAAM;IACjB,OAAO,CAAC,YAAY,CAAC,CAAe;gBAExB,YAAY,CAAC,EAAE,YAAY;IAIvC;;OAEG;IACG,OAAO,CACX,WAAW,EAAE,MAAM,EACnB,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,YAAY,CAAC,EACjC,MAAM,EAAE,cAAc,EACtB,OAAO,GAAE,aAAkB,GAC1B,OAAO,CAAC,YAAY,CAAC;IAoFxB;;OAEG;IACG,YAAY,CAChB,WAAW,EAAE,MAAM,EACnB,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,YAAY,CAAC,EACjC,YAAY,EAAE,YAAY,EAC1B,OAAO,GAAE,aAAkB,GAC1B,OAAO,CAAC,gBAAgB,CAAC;IAmD5B;;OAEG;YACW,mBAAmB;IAiCjC;;OAEG;YACW,oBAAoB;IA8BlC;;OAEG;IACH,kBAAkB,CAAC,KAAK,EAAE,YAAY,EAAE,YAAY,EAAE,YAAY,GAAG,MAAM;IAc3E;;OAEG;IACH,OAAO,CAAC,oBAAoB;IAyB5B;;OAEG;YACW,cAAc;IAgD5B;;OAEG;IACG,aAAa,CACjB,WAAW,EAAE,MAAM,EACnB,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,EACrC,OAAO,GAAE,aAAkB,GAC1B,OAAO,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,SAAS,GAAG,SAAS,GAAG,WAAW,CAAA;KAAE,CAAC;IA2BzE;;OAEG;YACW,aAAa;IAyB3B;;OAEG;IACH,OAAO,CAAC,WAAW;IAInB;;OAEG;IACH,OAAO,CAAC,iBAAiB;CAe1B;AAED;;GAEG;AACH,wBAAgB,YAAY,CAAC,YAAY,CAAC,EAAE,YAAY,GAAG,MAAM,CAEhE;AAED;;;;;;GAMG;AACH,wBAAsB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,CA2B9E;AAkCD;;GAEG;AACH,wBAAsB,uBAAuB,CAC3C,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC,CA0BpC"}
@@ -0,0 +1,405 @@
1
+ /**
2
+ * Syncer - Sync skills to AI tool directories
3
+ */
4
+ import { readFile, writeFile, mkdir, readdir, unlink } from 'fs/promises';
5
+ import { existsSync } from 'fs';
6
+ import { join, relative } from 'path';
7
+ import { createHash } from 'crypto';
8
+ import { getTargetConfig } from './types.js';
9
+ import { parse } from '../parser/index.js';
10
+ /**
11
+ * Syncer class - syncs skills to AI tool directories
12
+ */
13
+ export class Syncer {
14
+ stateManager;
15
+ constructor(stateManager) {
16
+ this.stateManager = stateManager;
17
+ }
18
+ /**
19
+ * Sync all skills to all enabled targets
20
+ */
21
+ async syncAll(projectPath, skills, config, options = {}) {
22
+ const result = {
23
+ targets: [],
24
+ success: true,
25
+ stats: {
26
+ skillsSynced: 0,
27
+ filesCreated: 0,
28
+ filesUpdated: 0,
29
+ filesUnchanged: 0,
30
+ filesDeleted: 0,
31
+ },
32
+ };
33
+ // Get enabled targets
34
+ const enabledTargets = this.getEnabledTargets(config.sync_targets);
35
+ // Sync to each target
36
+ for (const target of enabledTargets) {
37
+ const targetConfig = getTargetConfig(target);
38
+ // Skip unimplemented targets
39
+ if (!targetConfig.implemented) {
40
+ result.targets.push({
41
+ target,
42
+ success: false,
43
+ files: [],
44
+ errors: [`Target '${target}' is not yet implemented`],
45
+ warnings: [],
46
+ });
47
+ continue;
48
+ }
49
+ const targetResult = await this.syncToTarget(projectPath, skills, targetConfig, options);
50
+ result.targets.push(targetResult);
51
+ if (!targetResult.success) {
52
+ result.success = false;
53
+ }
54
+ // Aggregate stats
55
+ for (const file of targetResult.files) {
56
+ switch (file.action) {
57
+ case 'created':
58
+ result.stats.filesCreated++;
59
+ break;
60
+ case 'updated':
61
+ result.stats.filesUpdated++;
62
+ break;
63
+ case 'unchanged':
64
+ result.stats.filesUnchanged++;
65
+ break;
66
+ case 'deleted':
67
+ result.stats.filesDeleted++;
68
+ break;
69
+ }
70
+ }
71
+ }
72
+ result.stats.skillsSynced = skills.size;
73
+ // Sync MCP config if applicable
74
+ if (config.mcp && Object.keys(config.mcp).length > 0) {
75
+ const mcpResult = await this.syncMcpConfig(projectPath, config.mcp, options);
76
+ result.mcpConfig = mcpResult;
77
+ }
78
+ // Update sync history in state
79
+ if (this.stateManager && !options.dryRun) {
80
+ for (const targetResult of result.targets) {
81
+ if (targetResult.success) {
82
+ await this.stateManager.recordSync(projectPath, targetResult.target);
83
+ }
84
+ }
85
+ }
86
+ return result;
87
+ }
88
+ /**
89
+ * Sync skills to a single target
90
+ */
91
+ async syncToTarget(projectPath, skills, targetConfig, options = {}) {
92
+ const result = {
93
+ target: targetConfig.id,
94
+ success: true,
95
+ files: [],
96
+ errors: [],
97
+ warnings: [],
98
+ };
99
+ try {
100
+ const outputDir = join(projectPath, targetConfig.outputPath);
101
+ // Ensure output directory exists
102
+ if (!options.dryRun) {
103
+ await mkdir(outputDir, { recursive: true });
104
+ }
105
+ if (targetConfig.format === 'directory') {
106
+ // Directory format: each skill in its own directory
107
+ await this.syncDirectoryFormat(projectPath, outputDir, skills, targetConfig, result, options);
108
+ }
109
+ else {
110
+ // Single-file format: all skills merged into one file
111
+ await this.syncSingleFileFormat(projectPath, outputDir, skills, targetConfig, result, options);
112
+ }
113
+ // Clean up orphaned skill directories (only for directory format)
114
+ if (targetConfig.format === 'directory' && !options.dryRun) {
115
+ await this.cleanupOrphans(outputDir, skills, targetConfig, result);
116
+ }
117
+ }
118
+ catch (error) {
119
+ result.success = false;
120
+ result.errors.push(error instanceof Error ? error.message : String(error));
121
+ }
122
+ return result;
123
+ }
124
+ /**
125
+ * Sync in directory format (each skill in its own directory)
126
+ */
127
+ async syncDirectoryFormat(projectPath, outputDir, skills, targetConfig, result, options) {
128
+ for (const [name, skill] of skills) {
129
+ const skillDir = join(outputDir, name);
130
+ const skillFile = join(skillDir, targetConfig.skillFileName);
131
+ const relativePath = relative(projectPath, skillFile);
132
+ // Transform content for target
133
+ const content = this.transformForTarget(skill, targetConfig);
134
+ // Check if file needs updating
135
+ const action = await this.getFileAction(skillFile, content, options);
136
+ if (action === 'unchanged') {
137
+ result.files.push({ path: relativePath, action, skillName: name });
138
+ continue;
139
+ }
140
+ if (!options.dryRun) {
141
+ await mkdir(skillDir, { recursive: true });
142
+ await writeFile(skillFile, content, 'utf-8');
143
+ }
144
+ result.files.push({ path: relativePath, action, skillName: name });
145
+ }
146
+ }
147
+ /**
148
+ * Sync in single-file format (all skills merged)
149
+ */
150
+ async syncSingleFileFormat(projectPath, outputDir, skills, targetConfig, result, options) {
151
+ const outputFile = join(outputDir, targetConfig.skillFileName);
152
+ const relativePath = relative(projectPath, outputFile);
153
+ // Merge all skills into one file
154
+ const content = this.mergeSkillsForTarget(skills, targetConfig);
155
+ // Check if file needs updating
156
+ const action = await this.getFileAction(outputFile, content, options);
157
+ if (action === 'unchanged') {
158
+ result.files.push({ path: relativePath, action });
159
+ return;
160
+ }
161
+ if (!options.dryRun) {
162
+ await mkdir(outputDir, { recursive: true });
163
+ await writeFile(outputFile, content, 'utf-8');
164
+ }
165
+ result.files.push({ path: relativePath, action });
166
+ }
167
+ /**
168
+ * Transform skill content for a specific target
169
+ */
170
+ transformForTarget(skill, targetConfig) {
171
+ switch (targetConfig.frontmatter) {
172
+ case 'keep':
173
+ return skill.rawContent;
174
+ case 'remove':
175
+ return skill.bodyContent;
176
+ case 'convert':
177
+ // Future: convert frontmatter to target-specific format
178
+ return skill.bodyContent;
179
+ default:
180
+ return skill.rawContent;
181
+ }
182
+ }
183
+ /**
184
+ * Merge multiple skills into a single file
185
+ */
186
+ mergeSkillsForTarget(skills, targetConfig) {
187
+ const sections = [];
188
+ // Add header based on target
189
+ if (targetConfig.id === 'codex') {
190
+ sections.push('# AGENTS\n');
191
+ sections.push('This file contains AI agent skills for this project.\n');
192
+ }
193
+ else if (targetConfig.id === 'copilot') {
194
+ sections.push('# GitHub Copilot Instructions\n');
195
+ sections.push('These are custom instructions for GitHub Copilot.\n');
196
+ }
197
+ // Add each skill as a section
198
+ for (const [name, skill] of skills) {
199
+ sections.push(`\n---\n`);
200
+ sections.push(`## ${name} (v${skill.version})\n`);
201
+ sections.push(skill.bodyContent);
202
+ }
203
+ return sections.join('\n');
204
+ }
205
+ /**
206
+ * Cleanup orphaned skill directories
207
+ */
208
+ async cleanupOrphans(outputDir, skills, targetConfig, result) {
209
+ if (!existsSync(outputDir))
210
+ return;
211
+ try {
212
+ const entries = await readdir(outputDir, { withFileTypes: true });
213
+ for (const entry of entries) {
214
+ if (!entry.isDirectory())
215
+ continue;
216
+ const skillName = entry.name;
217
+ // Skip if skill exists in current list
218
+ if (skills.has(skillName))
219
+ continue;
220
+ // This is an orphan - delete it
221
+ const skillDir = join(outputDir, skillName);
222
+ const skillFile = join(skillDir, targetConfig.skillFileName);
223
+ if (existsSync(skillFile)) {
224
+ await unlink(skillFile);
225
+ result.files.push({
226
+ path: relative(outputDir, skillFile),
227
+ action: 'deleted',
228
+ skillName,
229
+ });
230
+ }
231
+ // Try to remove empty directory
232
+ try {
233
+ const remaining = await readdir(skillDir);
234
+ if (remaining.length === 0) {
235
+ const { rmdir } = await import('fs/promises');
236
+ await rmdir(skillDir);
237
+ }
238
+ }
239
+ catch {
240
+ // Ignore errors when removing directory
241
+ }
242
+ }
243
+ }
244
+ catch {
245
+ // Ignore errors during cleanup
246
+ }
247
+ }
248
+ /**
249
+ * Sync MCP configuration to .mcp.json
250
+ */
251
+ async syncMcpConfig(projectPath, mcpConfigs, options = {}) {
252
+ const mcpJsonPath = join(projectPath, '.mcp.json');
253
+ const relativePath = '.mcp.json';
254
+ // Build MCP JSON config
255
+ const mcpJson = {
256
+ mcpServers: {},
257
+ };
258
+ for (const [name, config] of Object.entries(mcpConfigs)) {
259
+ mcpJson.mcpServers[name] = {
260
+ command: config.command || `npx ${config.package}`,
261
+ args: config.args,
262
+ env: config.env,
263
+ };
264
+ }
265
+ const content = JSON.stringify(mcpJson, null, 2);
266
+ const action = await this.getFileAction(mcpJsonPath, content, options);
267
+ if (action !== 'unchanged' && !options.dryRun) {
268
+ await writeFile(mcpJsonPath, content, 'utf-8');
269
+ }
270
+ return { path: relativePath, action };
271
+ }
272
+ /**
273
+ * Determine what action is needed for a file
274
+ */
275
+ async getFileAction(filePath, newContent, options) {
276
+ if (!existsSync(filePath)) {
277
+ return 'created';
278
+ }
279
+ if (options.force) {
280
+ return 'updated';
281
+ }
282
+ // Compare content hash
283
+ try {
284
+ const existingContent = await readFile(filePath, 'utf-8');
285
+ const existingHash = this.hashContent(existingContent);
286
+ const newHash = this.hashContent(newContent);
287
+ return existingHash === newHash ? 'unchanged' : 'updated';
288
+ }
289
+ catch {
290
+ return 'updated';
291
+ }
292
+ }
293
+ /**
294
+ * Hash content for comparison
295
+ */
296
+ hashContent(content) {
297
+ return createHash('md5').update(content).digest('hex');
298
+ }
299
+ /**
300
+ * Get list of enabled targets from config
301
+ */
302
+ getEnabledTargets(syncTargets) {
303
+ if (!syncTargets) {
304
+ // Default to claude-code only
305
+ return ['claude-code'];
306
+ }
307
+ const enabled = [];
308
+ for (const [target, isEnabled] of Object.entries(syncTargets)) {
309
+ if (isEnabled) {
310
+ enabled.push(target);
311
+ }
312
+ }
313
+ return enabled.length > 0 ? enabled : ['claude-code'];
314
+ }
315
+ }
316
+ /**
317
+ * Create a Syncer instance
318
+ */
319
+ export function createSyncer(stateManager) {
320
+ return new Syncer(stateManager);
321
+ }
322
+ /**
323
+ * Load skill content from a SKILL.md file
324
+ *
325
+ * Supports two formats:
326
+ * 1. YAML files (skill.yaml)
327
+ * 2. Markdown with YAML frontmatter (SKILL.md)
328
+ */
329
+ export async function loadSkillContent(filePath) {
330
+ const rawContent = await readFile(filePath, 'utf-8');
331
+ // Check if it's a YAML file
332
+ if (filePath.endsWith('.yaml') || filePath.endsWith('.yml')) {
333
+ const result = parse(rawContent, { validate: false });
334
+ if (result.success && result.data) {
335
+ return {
336
+ name: result.data.name || 'unknown',
337
+ version: result.data.version || '1.0.0',
338
+ rawContent,
339
+ bodyContent: result.data.instructions || rawContent,
340
+ frontmatter: result.data,
341
+ };
342
+ }
343
+ }
344
+ // Parse markdown with YAML frontmatter
345
+ const { frontmatter, body } = parseMarkdownFrontmatter(rawContent);
346
+ return {
347
+ name: frontmatter.name || 'unknown',
348
+ version: frontmatter.version || '1.0.0',
349
+ rawContent,
350
+ bodyContent: body || rawContent,
351
+ frontmatter,
352
+ };
353
+ }
354
+ /**
355
+ * Parse YAML frontmatter from markdown content
356
+ */
357
+ function parseMarkdownFrontmatter(content) {
358
+ const frontmatterRegex = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/;
359
+ const match = content.match(frontmatterRegex);
360
+ if (!match) {
361
+ // No frontmatter, return empty frontmatter and full content as body
362
+ return { frontmatter: {}, body: content };
363
+ }
364
+ const [, yamlContent, body] = match;
365
+ try {
366
+ const result = parse(yamlContent, { validate: false });
367
+ if (result.success && result.data) {
368
+ return {
369
+ frontmatter: result.data,
370
+ body: body.trim(),
371
+ };
372
+ }
373
+ }
374
+ catch {
375
+ // Parse error, return empty frontmatter
376
+ }
377
+ return { frontmatter: {}, body: content };
378
+ }
379
+ /**
380
+ * Load all skills from a directory
381
+ */
382
+ export async function loadSkillsFromDirectory(skillsDir) {
383
+ const skills = new Map();
384
+ if (!existsSync(skillsDir)) {
385
+ return skills;
386
+ }
387
+ const entries = await readdir(skillsDir, { withFileTypes: true });
388
+ for (const entry of entries) {
389
+ if (!entry.isDirectory())
390
+ continue;
391
+ const skillDir = join(skillsDir, entry.name);
392
+ const skillFile = join(skillDir, 'SKILL.md');
393
+ if (!existsSync(skillFile))
394
+ continue;
395
+ try {
396
+ const skill = await loadSkillContent(skillFile);
397
+ skills.set(entry.name, skill);
398
+ }
399
+ catch {
400
+ // Skip invalid skill files
401
+ }
402
+ }
403
+ return skills;
404
+ }
405
+ //# sourceMappingURL=syncer.js.map