my-pi 0.0.2 → 0.0.4
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/README.md +24 -5
- package/dist/api-B6KnhtN9.js +1893 -0
- package/dist/api-B6KnhtN9.js.map +1 -0
- package/dist/api.js +1 -49
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/package.json +2 -1
- package/src/extensions/config.test.ts +88 -0
- package/src/extensions/config.ts +189 -0
- package/src/extensions/extensions.ts +366 -0
- package/src/extensions/recall.ts +29 -226
- package/src/extensions/skills.ts +496 -75
- package/src/skills/importer.test.ts +301 -0
- package/src/skills/importer.ts +221 -0
- package/src/skills/manager.ts +129 -30
- package/src/skills/scanner.ts +172 -72
- package/dist/api.js.map +0 -1
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
import { randomBytes } from 'node:crypto';
|
|
2
|
+
import {
|
|
3
|
+
existsSync,
|
|
4
|
+
mkdirSync,
|
|
5
|
+
readFileSync,
|
|
6
|
+
rmSync,
|
|
7
|
+
writeFileSync,
|
|
8
|
+
} from 'node:fs';
|
|
9
|
+
import { tmpdir } from 'node:os';
|
|
10
|
+
import { join } from 'node:path';
|
|
11
|
+
import {
|
|
12
|
+
afterEach,
|
|
13
|
+
beforeEach,
|
|
14
|
+
describe,
|
|
15
|
+
expect,
|
|
16
|
+
it,
|
|
17
|
+
vi,
|
|
18
|
+
} from 'vitest';
|
|
19
|
+
|
|
20
|
+
function tmp_test_dir(): string {
|
|
21
|
+
const dir = join(
|
|
22
|
+
tmpdir(),
|
|
23
|
+
`my-pi-skills-test-${randomBytes(4).toString('hex')}`,
|
|
24
|
+
);
|
|
25
|
+
mkdirSync(dir, { recursive: true });
|
|
26
|
+
return dir;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function write_skill(
|
|
30
|
+
base_dir: string,
|
|
31
|
+
name: string,
|
|
32
|
+
description: string,
|
|
33
|
+
): void {
|
|
34
|
+
mkdirSync(base_dir, { recursive: true });
|
|
35
|
+
writeFileSync(
|
|
36
|
+
join(base_dir, 'SKILL.md'),
|
|
37
|
+
`---\nname: ${name}\ndescription: ${description}\n---\n\n# ${name}\n`,
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function write_plugin_registry(
|
|
42
|
+
home_dir: string,
|
|
43
|
+
plugins: Record<
|
|
44
|
+
string,
|
|
45
|
+
{
|
|
46
|
+
installPath: string;
|
|
47
|
+
version: string;
|
|
48
|
+
gitCommitSha?: string;
|
|
49
|
+
}
|
|
50
|
+
>,
|
|
51
|
+
): void {
|
|
52
|
+
const plugins_dir = join(home_dir, '.claude', 'plugins');
|
|
53
|
+
mkdirSync(plugins_dir, { recursive: true });
|
|
54
|
+
writeFileSync(
|
|
55
|
+
join(plugins_dir, 'installed_plugins.json'),
|
|
56
|
+
JSON.stringify(
|
|
57
|
+
{
|
|
58
|
+
version: 2,
|
|
59
|
+
plugins: Object.fromEntries(
|
|
60
|
+
Object.entries(plugins).map(([key, value]) => [
|
|
61
|
+
key,
|
|
62
|
+
[
|
|
63
|
+
{
|
|
64
|
+
scope: 'user',
|
|
65
|
+
installPath: value.installPath,
|
|
66
|
+
version: value.version,
|
|
67
|
+
...(value.gitCommitSha
|
|
68
|
+
? { gitCommitSha: value.gitCommitSha }
|
|
69
|
+
: {}),
|
|
70
|
+
},
|
|
71
|
+
],
|
|
72
|
+
]),
|
|
73
|
+
),
|
|
74
|
+
},
|
|
75
|
+
null,
|
|
76
|
+
2,
|
|
77
|
+
),
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
describe('skills importing and syncing', () => {
|
|
82
|
+
let home_dir: string;
|
|
83
|
+
let original_home: string | undefined;
|
|
84
|
+
let original_xdg: string | undefined;
|
|
85
|
+
|
|
86
|
+
beforeEach(() => {
|
|
87
|
+
home_dir = tmp_test_dir();
|
|
88
|
+
original_home = process.env.HOME;
|
|
89
|
+
original_xdg = process.env.XDG_CONFIG_HOME;
|
|
90
|
+
process.env.HOME = home_dir;
|
|
91
|
+
process.env.XDG_CONFIG_HOME = join(home_dir, '.config');
|
|
92
|
+
vi.resetModules();
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
afterEach(() => {
|
|
96
|
+
if (original_home === undefined) {
|
|
97
|
+
delete process.env.HOME;
|
|
98
|
+
} else {
|
|
99
|
+
process.env.HOME = original_home;
|
|
100
|
+
}
|
|
101
|
+
if (original_xdg === undefined) {
|
|
102
|
+
delete process.env.XDG_CONFIG_HOME;
|
|
103
|
+
} else {
|
|
104
|
+
process.env.XDG_CONFIG_HOME = original_xdg;
|
|
105
|
+
}
|
|
106
|
+
rmSync(home_dir, { recursive: true, force: true });
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('imports an external plugin skill into pi-native storage with tracking metadata', async () => {
|
|
110
|
+
const install_path = join(
|
|
111
|
+
home_dir,
|
|
112
|
+
'plugin-cache',
|
|
113
|
+
'frontend-design',
|
|
114
|
+
'1.0.0',
|
|
115
|
+
);
|
|
116
|
+
write_skill(
|
|
117
|
+
join(install_path, 'skills', 'frontend-design'),
|
|
118
|
+
'frontend-design',
|
|
119
|
+
'Build beautiful interfaces',
|
|
120
|
+
);
|
|
121
|
+
write_plugin_registry(home_dir, {
|
|
122
|
+
'frontend-design@claude-plugins-official': {
|
|
123
|
+
installPath: install_path,
|
|
124
|
+
version: '1.0.0',
|
|
125
|
+
gitCommitSha: 'abc123def456',
|
|
126
|
+
},
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
const { scan_importable_skills, IMPORT_METADATA_FILE } =
|
|
130
|
+
await import('./scanner.js');
|
|
131
|
+
const { import_external_skill } = await import('./importer.js');
|
|
132
|
+
|
|
133
|
+
const skill = scan_importable_skills().find(
|
|
134
|
+
(skill) => skill.name === 'frontend-design',
|
|
135
|
+
);
|
|
136
|
+
expect(skill).toBeDefined();
|
|
137
|
+
expect(skill?.kind).toBe('external');
|
|
138
|
+
|
|
139
|
+
const result = import_external_skill(skill!);
|
|
140
|
+
expect(result.skillDir).toBe(
|
|
141
|
+
join(home_dir, '.pi', 'agent', 'skills', 'frontend-design'),
|
|
142
|
+
);
|
|
143
|
+
expect(existsSync(join(result.skillDir, 'SKILL.md'))).toBe(true);
|
|
144
|
+
|
|
145
|
+
const metadata = JSON.parse(
|
|
146
|
+
readFileSync(
|
|
147
|
+
join(result.skillDir, IMPORT_METADATA_FILE),
|
|
148
|
+
'utf-8',
|
|
149
|
+
),
|
|
150
|
+
) as Record<string, string>;
|
|
151
|
+
expect(metadata.source).toBe(
|
|
152
|
+
'plugin:frontend-design@claude-plugins-official',
|
|
153
|
+
);
|
|
154
|
+
expect(metadata.upstream_skill_path).toContain(
|
|
155
|
+
'skills/frontend-design/SKILL.md',
|
|
156
|
+
);
|
|
157
|
+
expect(metadata.upstream_version).toBe('1.0.0');
|
|
158
|
+
expect(metadata.upstream_git_commit_sha).toBe('abc123def456');
|
|
159
|
+
expect(metadata.imported_hash).toBeTruthy();
|
|
160
|
+
expect(metadata.upstream_hash).toBeTruthy();
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it('syncs an imported skill when the upstream source changes', async () => {
|
|
164
|
+
const install_path = join(
|
|
165
|
+
home_dir,
|
|
166
|
+
'plugin-cache',
|
|
167
|
+
'toolkit-skills',
|
|
168
|
+
'0.0.1',
|
|
169
|
+
);
|
|
170
|
+
const upstream_dir = join(install_path, 'skills', 'github-prs');
|
|
171
|
+
write_skill(
|
|
172
|
+
upstream_dir,
|
|
173
|
+
'github-prs',
|
|
174
|
+
'Find and manage pull requests',
|
|
175
|
+
);
|
|
176
|
+
write_plugin_registry(home_dir, {
|
|
177
|
+
'toolkit-skills@claude-code-toolkit': {
|
|
178
|
+
installPath: install_path,
|
|
179
|
+
version: '0.0.1',
|
|
180
|
+
},
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
const scanner = await import('./scanner.js');
|
|
184
|
+
const importer = await import('./importer.js');
|
|
185
|
+
|
|
186
|
+
const external = scanner
|
|
187
|
+
.scan_importable_skills()
|
|
188
|
+
.find((skill) => skill.name === 'github-prs');
|
|
189
|
+
importer.import_external_skill(external!);
|
|
190
|
+
|
|
191
|
+
writeFileSync(
|
|
192
|
+
join(upstream_dir, 'helper.txt'),
|
|
193
|
+
'new upstream helper content',
|
|
194
|
+
);
|
|
195
|
+
|
|
196
|
+
const managed = scanner
|
|
197
|
+
.scan_managed_skills()
|
|
198
|
+
.find((skill) => skill.name === 'github-prs');
|
|
199
|
+
const result = importer.sync_imported_skill(managed!);
|
|
200
|
+
|
|
201
|
+
expect(result.changed).toBe(true);
|
|
202
|
+
expect(
|
|
203
|
+
readFileSync(join(result.skillDir, 'helper.txt'), 'utf-8'),
|
|
204
|
+
).toBe('new upstream helper content');
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it('refuses to sync when the managed copy has local changes', async () => {
|
|
208
|
+
const install_path = join(
|
|
209
|
+
home_dir,
|
|
210
|
+
'plugin-cache',
|
|
211
|
+
'linear',
|
|
212
|
+
'2.0.0',
|
|
213
|
+
);
|
|
214
|
+
const upstream_dir = join(install_path, 'skills', 'linear');
|
|
215
|
+
write_skill(upstream_dir, 'linear', 'Work with Linear issues');
|
|
216
|
+
write_plugin_registry(home_dir, {
|
|
217
|
+
'linear@vendor': {
|
|
218
|
+
installPath: install_path,
|
|
219
|
+
version: '2.0.0',
|
|
220
|
+
},
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
const scanner = await import('./scanner.js');
|
|
224
|
+
const importer = await import('./importer.js');
|
|
225
|
+
|
|
226
|
+
const external = scanner
|
|
227
|
+
.scan_importable_skills()
|
|
228
|
+
.find((skill) => skill.name === 'linear');
|
|
229
|
+
const imported = importer.import_external_skill(external!);
|
|
230
|
+
|
|
231
|
+
writeFileSync(
|
|
232
|
+
join(imported.skillDir, 'notes.md'),
|
|
233
|
+
'local customization that should block sync',
|
|
234
|
+
);
|
|
235
|
+
writeFileSync(
|
|
236
|
+
join(upstream_dir, 'notes.md'),
|
|
237
|
+
'upstream changed too',
|
|
238
|
+
);
|
|
239
|
+
|
|
240
|
+
const managed = scanner
|
|
241
|
+
.scan_managed_skills()
|
|
242
|
+
.find((skill) => skill.name === 'linear');
|
|
243
|
+
expect(() => importer.sync_imported_skill(managed!)).toThrow(
|
|
244
|
+
/local changes detected/i,
|
|
245
|
+
);
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it('manager separates managed and importable skills and enables imported skills', async () => {
|
|
249
|
+
write_skill(
|
|
250
|
+
join(home_dir, '.claude', 'skills', 'github-prs'),
|
|
251
|
+
'github-prs',
|
|
252
|
+
'Local managed GitHub PR skill',
|
|
253
|
+
);
|
|
254
|
+
|
|
255
|
+
const install_path = join(
|
|
256
|
+
home_dir,
|
|
257
|
+
'plugin-cache',
|
|
258
|
+
'frontend-design',
|
|
259
|
+
'3.0.0',
|
|
260
|
+
);
|
|
261
|
+
write_skill(
|
|
262
|
+
join(install_path, 'skills', 'frontend-design'),
|
|
263
|
+
'frontend-design',
|
|
264
|
+
'Plugin frontend design skill',
|
|
265
|
+
);
|
|
266
|
+
write_plugin_registry(home_dir, {
|
|
267
|
+
'frontend-design@claude-plugins-official': {
|
|
268
|
+
installPath: install_path,
|
|
269
|
+
version: '3.0.0',
|
|
270
|
+
},
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
const { create_skills_manager } = await import('./manager.js');
|
|
274
|
+
const mgr = create_skills_manager();
|
|
275
|
+
|
|
276
|
+
expect(mgr.discover().map((skill) => skill.name)).toEqual([
|
|
277
|
+
'github-prs',
|
|
278
|
+
]);
|
|
279
|
+
expect(
|
|
280
|
+
mgr.discover_importable().map((skill) => skill.name),
|
|
281
|
+
).toEqual(['frontend-design']);
|
|
282
|
+
|
|
283
|
+
const imported = mgr.import_skill('frontend-design');
|
|
284
|
+
expect(imported.key).toBe('frontend-design@pi-native');
|
|
285
|
+
|
|
286
|
+
const managed_names = mgr
|
|
287
|
+
.discover()
|
|
288
|
+
.map((skill) => skill.name)
|
|
289
|
+
.sort();
|
|
290
|
+
expect(managed_names).toEqual(['frontend-design', 'github-prs']);
|
|
291
|
+
expect(mgr.get_enabled_skill_paths()).toContain(
|
|
292
|
+
imported.skillDir,
|
|
293
|
+
);
|
|
294
|
+
expect(
|
|
295
|
+
mgr.is_enabled_by_skill(
|
|
296
|
+
'frontend-design',
|
|
297
|
+
join(imported.skillDir, 'SKILL.md'),
|
|
298
|
+
),
|
|
299
|
+
).toBe(true);
|
|
300
|
+
});
|
|
301
|
+
});
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import {
|
|
3
|
+
cpSync,
|
|
4
|
+
existsSync,
|
|
5
|
+
mkdirSync,
|
|
6
|
+
readdirSync,
|
|
7
|
+
readFileSync,
|
|
8
|
+
rmSync,
|
|
9
|
+
statSync,
|
|
10
|
+
writeFileSync,
|
|
11
|
+
} from 'node:fs';
|
|
12
|
+
import { homedir } from 'node:os';
|
|
13
|
+
import { dirname, join, relative, resolve } from 'node:path';
|
|
14
|
+
import {
|
|
15
|
+
IMPORT_METADATA_FILE,
|
|
16
|
+
type DiscoveredSkill,
|
|
17
|
+
type ImportedSkillMetadata,
|
|
18
|
+
} from './scanner.js';
|
|
19
|
+
|
|
20
|
+
const IMPORT_METADATA_VERSION = 1;
|
|
21
|
+
|
|
22
|
+
function get_managed_skills_dir(): string {
|
|
23
|
+
return join(homedir(), '.pi', 'agent', 'skills');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function ensure_dir(path: string): void {
|
|
27
|
+
mkdirSync(path, { recursive: true, mode: 0o700 });
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function list_files_recursively(dir: string): string[] {
|
|
31
|
+
const files: string[] = [];
|
|
32
|
+
|
|
33
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
34
|
+
const full_path = join(dir, entry.name);
|
|
35
|
+
if (entry.name === IMPORT_METADATA_FILE) continue;
|
|
36
|
+
if (entry.isDirectory()) {
|
|
37
|
+
files.push(...list_files_recursively(full_path));
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
if (entry.isFile()) {
|
|
41
|
+
files.push(full_path);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return files.sort((a, b) => a.localeCompare(b));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function hash_directory(dir: string): string {
|
|
49
|
+
const hash = createHash('sha256');
|
|
50
|
+
for (const file of list_files_recursively(dir)) {
|
|
51
|
+
hash.update(relative(dir, file));
|
|
52
|
+
hash.update('\0');
|
|
53
|
+
hash.update(readFileSync(file));
|
|
54
|
+
hash.update('\0');
|
|
55
|
+
}
|
|
56
|
+
return hash.digest('hex');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function read_metadata(
|
|
60
|
+
base_dir: string,
|
|
61
|
+
): ImportedSkillMetadata | undefined {
|
|
62
|
+
const path = join(base_dir, IMPORT_METADATA_FILE);
|
|
63
|
+
if (!existsSync(path)) return undefined;
|
|
64
|
+
|
|
65
|
+
try {
|
|
66
|
+
return JSON.parse(
|
|
67
|
+
readFileSync(path, 'utf-8'),
|
|
68
|
+
) as ImportedSkillMetadata;
|
|
69
|
+
} catch {
|
|
70
|
+
return undefined;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function write_metadata(
|
|
75
|
+
base_dir: string,
|
|
76
|
+
metadata: ImportedSkillMetadata,
|
|
77
|
+
): void {
|
|
78
|
+
writeFileSync(
|
|
79
|
+
join(base_dir, IMPORT_METADATA_FILE),
|
|
80
|
+
JSON.stringify(metadata, null, '\t') + '\n',
|
|
81
|
+
{ mode: 0o600 },
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function replace_directory(
|
|
86
|
+
source_dir: string,
|
|
87
|
+
dest_dir: string,
|
|
88
|
+
): void {
|
|
89
|
+
const parent_dir = dirname(dest_dir);
|
|
90
|
+
ensure_dir(parent_dir);
|
|
91
|
+
const tmp_dir = join(
|
|
92
|
+
parent_dir,
|
|
93
|
+
`.${resolve(dest_dir).split('/').pop()}.tmp-${Date.now()}`,
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
rmSync(tmp_dir, { recursive: true, force: true });
|
|
97
|
+
cpSync(source_dir, tmp_dir, {
|
|
98
|
+
recursive: true,
|
|
99
|
+
preserveTimestamps: true,
|
|
100
|
+
verbatimSymlinks: false,
|
|
101
|
+
});
|
|
102
|
+
rmSync(dest_dir, { recursive: true, force: true });
|
|
103
|
+
cpSync(tmp_dir, dest_dir, {
|
|
104
|
+
recursive: true,
|
|
105
|
+
preserveTimestamps: true,
|
|
106
|
+
verbatimSymlinks: false,
|
|
107
|
+
});
|
|
108
|
+
rmSync(tmp_dir, { recursive: true, force: true });
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export interface ImportSkillResult {
|
|
112
|
+
skillDir: string;
|
|
113
|
+
metadata: ImportedSkillMetadata;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function import_external_skill(
|
|
117
|
+
skill: DiscoveredSkill,
|
|
118
|
+
): ImportSkillResult {
|
|
119
|
+
if (skill.kind !== 'external') {
|
|
120
|
+
throw new Error(`Skill ${skill.name} is not importable`);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const managed_root = get_managed_skills_dir();
|
|
124
|
+
ensure_dir(managed_root);
|
|
125
|
+
|
|
126
|
+
const skill_dir = join(managed_root, skill.name);
|
|
127
|
+
const existing = existsSync(skill_dir);
|
|
128
|
+
if (existing) {
|
|
129
|
+
const existing_stat = statSync(skill_dir);
|
|
130
|
+
if (!existing_stat.isDirectory()) {
|
|
131
|
+
throw new Error(`${skill_dir} exists and is not a directory`);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const existing_metadata = read_metadata(skill_dir);
|
|
135
|
+
if (!existing_metadata) {
|
|
136
|
+
throw new Error(
|
|
137
|
+
`Refusing to overwrite existing unmanaged skill at ${skill_dir}`,
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
replace_directory(skill.baseDir, skill_dir);
|
|
143
|
+
|
|
144
|
+
const upstream_hash = hash_directory(skill.baseDir);
|
|
145
|
+
const imported_hash = hash_directory(skill_dir);
|
|
146
|
+
const now = new Date().toISOString();
|
|
147
|
+
const metadata: ImportedSkillMetadata = {
|
|
148
|
+
version: IMPORT_METADATA_VERSION,
|
|
149
|
+
source: skill.source,
|
|
150
|
+
upstream_skill_path: skill.skillPath,
|
|
151
|
+
upstream_base_dir: skill.baseDir,
|
|
152
|
+
upstream_install_path: skill.plugin?.installPath,
|
|
153
|
+
upstream_version: skill.plugin?.version,
|
|
154
|
+
upstream_git_commit_sha: skill.plugin?.gitCommitSha,
|
|
155
|
+
imported_at: now,
|
|
156
|
+
last_synced_at: now,
|
|
157
|
+
imported_hash,
|
|
158
|
+
upstream_hash,
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
write_metadata(skill_dir, metadata);
|
|
162
|
+
return {
|
|
163
|
+
skillDir: skill_dir,
|
|
164
|
+
metadata,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export interface SyncSkillResult {
|
|
169
|
+
skillDir: string;
|
|
170
|
+
metadata: ImportedSkillMetadata;
|
|
171
|
+
changed: boolean;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export function sync_imported_skill(
|
|
175
|
+
skill: DiscoveredSkill,
|
|
176
|
+
): SyncSkillResult {
|
|
177
|
+
if (skill.kind !== 'managed' || !skill.import_meta) {
|
|
178
|
+
throw new Error(
|
|
179
|
+
`Skill ${skill.name} is not managed by my-pi sync`,
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const metadata = skill.import_meta;
|
|
184
|
+
if (!existsSync(metadata.upstream_base_dir)) {
|
|
185
|
+
throw new Error(
|
|
186
|
+
`Upstream source no longer exists: ${metadata.upstream_base_dir}`,
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const current_hash = hash_directory(skill.baseDir);
|
|
191
|
+
if (current_hash !== metadata.imported_hash) {
|
|
192
|
+
throw new Error(
|
|
193
|
+
`Refusing to sync ${skill.name}; local changes detected in ${skill.baseDir}`,
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const upstream_hash = hash_directory(metadata.upstream_base_dir);
|
|
198
|
+
if (upstream_hash === metadata.upstream_hash) {
|
|
199
|
+
return {
|
|
200
|
+
skillDir: skill.baseDir,
|
|
201
|
+
metadata,
|
|
202
|
+
changed: false,
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
replace_directory(metadata.upstream_base_dir, skill.baseDir);
|
|
207
|
+
const imported_hash = hash_directory(skill.baseDir);
|
|
208
|
+
const updated: ImportedSkillMetadata = {
|
|
209
|
+
...metadata,
|
|
210
|
+
last_synced_at: new Date().toISOString(),
|
|
211
|
+
imported_hash,
|
|
212
|
+
upstream_hash,
|
|
213
|
+
};
|
|
214
|
+
write_metadata(skill.baseDir, updated);
|
|
215
|
+
|
|
216
|
+
return {
|
|
217
|
+
skillDir: skill.baseDir,
|
|
218
|
+
metadata: updated,
|
|
219
|
+
changed: true,
|
|
220
|
+
};
|
|
221
|
+
}
|