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.
- package/dist/config/config-manager.d.ts +83 -0
- package/dist/config/config-manager.d.ts.map +1 -0
- package/dist/config/config-manager.js +220 -0
- package/dist/config/config-manager.js.map +1 -0
- package/dist/config/index.d.ts +23 -0
- package/dist/config/index.d.ts.map +1 -0
- package/dist/config/index.js +23 -0
- package/dist/config/index.js.map +1 -0
- package/dist/config/schemas/skillpkg.schema.json +103 -0
- package/dist/config/types.d.ts +69 -0
- package/dist/config/types.d.ts.map +1 -0
- package/dist/config/types.js +29 -0
- package/dist/config/types.js.map +1 -0
- package/dist/index.d.ts +6 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +11 -1
- package/dist/index.js.map +1 -1
- package/dist/installer/index.d.ts +33 -0
- package/dist/installer/index.d.ts.map +1 -0
- package/dist/installer/index.js +33 -0
- package/dist/installer/index.js.map +1 -0
- package/dist/installer/installer.d.ts +54 -0
- package/dist/installer/installer.d.ts.map +1 -0
- package/dist/installer/installer.js +371 -0
- package/dist/installer/installer.js.map +1 -0
- package/dist/installer/types.d.ts +138 -0
- package/dist/installer/types.d.ts.map +1 -0
- package/dist/installer/types.js +2 -0
- package/dist/installer/types.js.map +1 -0
- package/dist/parser/schema.d.ts +26 -5
- package/dist/parser/schema.d.ts.map +1 -1
- package/dist/parser/schema.js +25 -3
- package/dist/parser/schema.js.map +1 -1
- package/dist/resolver/dependency-resolver.d.ts +59 -0
- package/dist/resolver/dependency-resolver.d.ts.map +1 -0
- package/dist/resolver/dependency-resolver.js +217 -0
- package/dist/resolver/dependency-resolver.js.map +1 -0
- package/dist/resolver/index.d.ts +35 -0
- package/dist/resolver/index.d.ts.map +1 -0
- package/dist/resolver/index.js +34 -0
- package/dist/resolver/index.js.map +1 -0
- package/dist/resolver/install-plan.d.ts +107 -0
- package/dist/resolver/install-plan.d.ts.map +1 -0
- package/dist/resolver/install-plan.js +137 -0
- package/dist/resolver/install-plan.js.map +1 -0
- package/dist/resolver/types.d.ts +76 -0
- package/dist/resolver/types.d.ts.map +1 -0
- package/dist/resolver/types.js +5 -0
- package/dist/resolver/types.js.map +1 -0
- package/dist/state/index.d.ts +30 -0
- package/dist/state/index.d.ts.map +1 -0
- package/dist/state/index.js +30 -0
- package/dist/state/index.js.map +1 -0
- package/dist/state/state-manager.d.ts +105 -0
- package/dist/state/state-manager.d.ts.map +1 -0
- package/dist/state/state-manager.js +242 -0
- package/dist/state/state-manager.js.map +1 -0
- package/dist/state/types.d.ts +86 -0
- package/dist/state/types.d.ts.map +1 -0
- package/dist/state/types.js +21 -0
- package/dist/state/types.js.map +1 -0
- package/dist/syncer/index.d.ts +28 -0
- package/dist/syncer/index.d.ts.map +1 -0
- package/dist/syncer/index.js +28 -0
- package/dist/syncer/index.js.map +1 -0
- package/dist/syncer/syncer.d.ts +74 -0
- package/dist/syncer/syncer.d.ts.map +1 -0
- package/dist/syncer/syncer.js +405 -0
- package/dist/syncer/syncer.js.map +1 -0
- package/dist/syncer/types.d.ts +143 -0
- package/dist/syncer/types.d.ts.map +1 -0
- package/dist/syncer/types.js +84 -0
- package/dist/syncer/types.js.map +1 -0
- package/dist/types.d.ts +27 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +25 -0
- package/dist/types.js.map +1 -1
- 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
|