@soleri/core 9.13.0 → 9.14.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/dist/engine/bin/soleri-engine.js +7 -2
- package/dist/engine/bin/soleri-engine.js.map +1 -1
- package/dist/flows/types.d.ts +34 -30
- package/dist/flows/types.d.ts.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/knowledge-packs/community/.gitkeep +0 -0
- package/dist/knowledge-packs/salvador/salvador-craft/soleri-pack.json +10 -0
- package/dist/knowledge-packs/salvador/salvador-craft/vault/accessibility.json +53 -0
- package/dist/knowledge-packs/salvador/salvador-craft/vault/design-tokens.json +26 -0
- package/dist/knowledge-packs/salvador/salvador-craft/vault/design.json +33 -0
- package/dist/knowledge-packs/salvador/salvador-craft/vault/styling.json +44 -0
- package/dist/knowledge-packs/salvador/salvador-craft/vault/ux-laws.json +36 -0
- package/dist/knowledge-packs/salvador/salvador-craft/vault/ux.json +36 -0
- package/dist/knowledge-packs/salvador/salvador-engineering/soleri-pack.json +10 -0
- package/dist/knowledge-packs/salvador/salvador-engineering/vault/architecture.json +143 -0
- package/dist/knowledge-packs/salvador/salvador-engineering/vault/commercial.json +16 -0
- package/dist/knowledge-packs/salvador/salvador-engineering/vault/communication.json +33 -0
- package/dist/knowledge-packs/salvador/salvador-engineering/vault/component.json +16 -0
- package/dist/knowledge-packs/salvador/salvador-engineering/vault/express.json +34 -0
- package/dist/knowledge-packs/salvador/salvador-engineering/vault/leadership.json +33 -0
- package/dist/knowledge-packs/salvador/salvador-engineering/vault/methodology.json +33 -0
- package/dist/knowledge-packs/salvador/salvador-engineering/vault/monorepo.json +33 -0
- package/dist/knowledge-packs/salvador/salvador-engineering/vault/other.json +73 -0
- package/dist/knowledge-packs/salvador/salvador-engineering/vault/performance.json +35 -0
- package/dist/knowledge-packs/salvador/salvador-engineering/vault/prisma.json +33 -0
- package/dist/knowledge-packs/salvador/salvador-engineering/vault/product-strategy.json +42 -0
- package/dist/knowledge-packs/salvador/salvador-engineering/vault/react.json +47 -0
- package/dist/knowledge-packs/salvador/salvador-engineering/vault/security.json +34 -0
- package/dist/knowledge-packs/salvador/salvador-engineering/vault/testing.json +33 -0
- package/dist/knowledge-packs/salvador/salvador-engineering/vault/tooling.json +85 -0
- package/dist/knowledge-packs/salvador/salvador-engineering/vault/typescript.json +34 -0
- package/dist/knowledge-packs/salvador/salvador-engineering/vault/workflow.json +46 -0
- package/dist/knowledge-packs/salvador/salvador-uipro/soleri-pack.json +10 -0
- package/dist/knowledge-packs/salvador/salvador-uipro/vault/design.json +2589 -0
- package/dist/knowledge-packs/starter/api-design/soleri-pack.json +9 -0
- package/dist/knowledge-packs/starter/api-design/vault/patterns.json +137 -0
- package/dist/knowledge-packs/starter/architecture/soleri-pack.json +10 -0
- package/dist/knowledge-packs/starter/architecture/vault/patterns.json +137 -0
- package/dist/knowledge-packs/starter/design/soleri-pack.json +10 -0
- package/dist/knowledge-packs/starter/design/vault/patterns.json +137 -0
- package/dist/knowledge-packs/starter/nodejs/soleri-pack.json +9 -0
- package/dist/knowledge-packs/starter/nodejs/vault/patterns.json +137 -0
- package/dist/knowledge-packs/starter/react/soleri-pack.json +9 -0
- package/dist/knowledge-packs/starter/react/vault/patterns.json +164 -0
- package/dist/knowledge-packs/starter/security/soleri-pack.json +10 -0
- package/dist/knowledge-packs/starter/security/vault/patterns.json +137 -0
- package/dist/knowledge-packs/starter/testing/soleri-pack.json +9 -0
- package/dist/knowledge-packs/starter/testing/vault/patterns.json +128 -0
- package/dist/knowledge-packs/starter/typescript/soleri-pack.json +9 -0
- package/dist/knowledge-packs/starter/typescript/vault/patterns.json +164 -0
- package/dist/packs/index.d.ts +1 -1
- package/dist/packs/index.d.ts.map +1 -1
- package/dist/packs/index.js +1 -1
- package/dist/packs/index.js.map +1 -1
- package/dist/packs/resolver.d.ts +6 -0
- package/dist/packs/resolver.d.ts.map +1 -1
- package/dist/packs/resolver.js +20 -1
- package/dist/packs/resolver.js.map +1 -1
- package/dist/runtime/admin-setup-ops.js +1 -1
- package/dist/runtime/admin-setup-ops.js.map +1 -1
- package/dist/runtime/capture-ops.d.ts.map +1 -1
- package/dist/runtime/capture-ops.js +2 -1
- package/dist/runtime/capture-ops.js.map +1 -1
- package/dist/runtime/intake-ops.d.ts.map +1 -1
- package/dist/runtime/intake-ops.js +5 -5
- package/dist/runtime/intake-ops.js.map +1 -1
- package/dist/runtime/orchestrate-ops.d.ts.map +1 -1
- package/dist/runtime/orchestrate-ops.js +26 -2
- package/dist/runtime/orchestrate-ops.js.map +1 -1
- package/dist/runtime/planning-extra-ops.d.ts.map +1 -1
- package/dist/runtime/planning-extra-ops.js +5 -7
- package/dist/runtime/planning-extra-ops.js.map +1 -1
- package/dist/runtime/playbook-ops.d.ts.map +1 -1
- package/dist/runtime/playbook-ops.js +2 -1
- package/dist/runtime/playbook-ops.js.map +1 -1
- package/dist/runtime/schema-helpers.d.ts +7 -0
- package/dist/runtime/schema-helpers.d.ts.map +1 -0
- package/dist/runtime/schema-helpers.js +21 -0
- package/dist/runtime/schema-helpers.js.map +1 -0
- package/dist/runtime/sync-ops.d.ts.map +1 -1
- package/dist/runtime/sync-ops.js +3 -4
- package/dist/runtime/sync-ops.js.map +1 -1
- package/dist/runtime/vault-extra-ops.d.ts.map +1 -1
- package/dist/runtime/vault-extra-ops.js +5 -4
- package/dist/runtime/vault-extra-ops.js.map +1 -1
- package/dist/skills/sync-skills.d.ts +26 -7
- package/dist/skills/sync-skills.d.ts.map +1 -1
- package/dist/skills/sync-skills.js +132 -32
- package/dist/skills/sync-skills.js.map +1 -1
- package/dist/skills/validate-skill-docs.d.ts +24 -0
- package/dist/skills/validate-skill-docs.d.ts.map +1 -0
- package/dist/skills/validate-skill-docs.js +476 -0
- package/dist/skills/validate-skill-docs.js.map +1 -0
- package/package.json +2 -2
- package/src/__tests__/deviation-detection.test.ts +49 -0
- package/src/enforcement/adapters/claude-code.test.ts +9 -9
- package/src/engine/bin/soleri-engine.ts +7 -2
- package/src/flows/types.ts +4 -0
- package/src/index.ts +15 -2
- package/src/packs/index.ts +6 -1
- package/src/packs/resolver.ts +24 -1
- package/src/runtime/admin-setup-ops.test.ts +2 -0
- package/src/runtime/admin-setup-ops.ts +1 -1
- package/src/runtime/capture-ops.ts +2 -1
- package/src/runtime/intake-ops.ts +7 -7
- package/src/runtime/orchestrate-ops.ts +29 -2
- package/src/runtime/planning-extra-ops.ts +35 -37
- package/src/runtime/playbook-ops.ts +2 -1
- package/src/runtime/schema-helpers.test.ts +45 -0
- package/src/runtime/schema-helpers.ts +19 -0
- package/src/runtime/sync-ops.ts +8 -9
- package/src/runtime/vault-extra-ops.ts +5 -4
- package/src/skills/__tests__/sync-skills.test.ts +102 -29
- package/src/skills/__tests__/validate-skill-docs.test.ts +58 -0
- package/src/skills/sync-skills.ts +152 -32
- package/src/skills/validate-skill-docs.ts +562 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
2
|
-
import { mkdirSync, writeFileSync, rmSync, existsSync } from 'node:fs';
|
|
2
|
+
import { mkdirSync, writeFileSync, rmSync, existsSync, lstatSync } from 'node:fs';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
import { tmpdir } from 'node:os';
|
|
5
5
|
|
|
@@ -9,6 +9,7 @@ import { tmpdir } from 'node:os';
|
|
|
9
9
|
|
|
10
10
|
let sourceDir: string;
|
|
11
11
|
let fakeHome: string;
|
|
12
|
+
let fakeProject: string;
|
|
12
13
|
|
|
13
14
|
function setup(): void {
|
|
14
15
|
const base = join(tmpdir(), `soleri-sync-test-${Date.now()}`);
|
|
@@ -19,6 +20,9 @@ function setup(): void {
|
|
|
19
20
|
|
|
20
21
|
fakeHome = join(base, 'fake-home');
|
|
21
22
|
mkdirSync(join(fakeHome, '.claude', 'skills'), { recursive: true });
|
|
23
|
+
|
|
24
|
+
fakeProject = join(base, 'fake-project');
|
|
25
|
+
mkdirSync(join(fakeProject, '.claude', 'skills'), { recursive: true });
|
|
22
26
|
}
|
|
23
27
|
|
|
24
28
|
function teardown(): void {
|
|
@@ -40,29 +44,36 @@ function createSourceSkill(name: string, content?: string): string {
|
|
|
40
44
|
}
|
|
41
45
|
|
|
42
46
|
/** Create a directory in the fake ~/.claude/skills/ target */
|
|
43
|
-
function
|
|
47
|
+
function createGlobalSkillDir(name: string): string {
|
|
44
48
|
const dir = join(fakeHome, '.claude', 'skills', name);
|
|
45
49
|
mkdirSync(dir, { recursive: true });
|
|
46
50
|
writeFileSync(join(dir, 'SKILL.md'), `---\nname: ${name}\n---\n\nStale skill.\n`);
|
|
47
51
|
return dir;
|
|
48
52
|
}
|
|
49
53
|
|
|
50
|
-
function
|
|
54
|
+
function globalSkillsDir(): string {
|
|
51
55
|
return join(fakeHome, '.claude', 'skills');
|
|
52
56
|
}
|
|
53
57
|
|
|
54
|
-
function
|
|
55
|
-
return existsSync(join(
|
|
58
|
+
function globalDirExists(name: string): boolean {
|
|
59
|
+
return existsSync(join(globalSkillsDir(), name));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function projectSkillsDir(): string {
|
|
63
|
+
return join(fakeProject, '.claude', 'skills');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function projectDirExists(name: string): boolean {
|
|
67
|
+
return existsSync(join(projectSkillsDir(), name));
|
|
56
68
|
}
|
|
57
69
|
|
|
58
70
|
// =============================================================================
|
|
59
|
-
// TESTS
|
|
71
|
+
// TESTS — Global install (orphan cleanup)
|
|
60
72
|
// =============================================================================
|
|
61
73
|
|
|
62
|
-
describe('syncSkillsToClaudeCode — orphan cleanup', () => {
|
|
74
|
+
describe('syncSkillsToClaudeCode — global orphan cleanup', () => {
|
|
63
75
|
beforeEach(() => {
|
|
64
76
|
setup();
|
|
65
|
-
// Mock homedir() so syncSkillsToClaudeCode writes to our temp directory
|
|
66
77
|
vi.mock('node:os', async (importOriginal) => {
|
|
67
78
|
const original = await importOriginal<typeof import('node:os')>();
|
|
68
79
|
return {
|
|
@@ -77,56 +88,118 @@ describe('syncSkillsToClaudeCode — orphan cleanup', () => {
|
|
|
77
88
|
teardown();
|
|
78
89
|
});
|
|
79
90
|
|
|
80
|
-
it('removes orphan directories that match the agent prefix', async () => {
|
|
81
|
-
// Source has "my-skill", target has stale "test-agent-old-skill"
|
|
91
|
+
it('removes orphan directories that match the agent prefix (global)', async () => {
|
|
82
92
|
createSourceSkill('my-skill');
|
|
83
|
-
|
|
93
|
+
createGlobalSkillDir('test-agent-old-skill');
|
|
84
94
|
|
|
85
95
|
const { syncSkillsToClaudeCode } = await import('../sync-skills.js');
|
|
86
|
-
const result = syncSkillsToClaudeCode([sourceDir], 'Test Agent');
|
|
96
|
+
const result = syncSkillsToClaudeCode([sourceDir], 'Test Agent', { global: true });
|
|
87
97
|
|
|
88
|
-
// The orphan should be reported as removed
|
|
89
98
|
expect(result.removed).toContain('test-agent-old-skill');
|
|
90
|
-
|
|
91
|
-
expect(targetDirExists('test-agent-old-skill')).toBe(false);
|
|
99
|
+
expect(globalDirExists('test-agent-old-skill')).toBe(false);
|
|
92
100
|
});
|
|
93
101
|
|
|
94
|
-
it('does NOT remove directories that do not match the agent prefix', async () => {
|
|
102
|
+
it('does NOT remove directories that do not match the agent prefix (global)', async () => {
|
|
95
103
|
createSourceSkill('my-skill');
|
|
96
|
-
|
|
97
|
-
createTargetSkillDir('other-agent-skill');
|
|
104
|
+
createGlobalSkillDir('other-agent-skill');
|
|
98
105
|
|
|
99
106
|
const { syncSkillsToClaudeCode } = await import('../sync-skills.js');
|
|
100
|
-
const result = syncSkillsToClaudeCode([sourceDir], 'Test Agent');
|
|
107
|
+
const result = syncSkillsToClaudeCode([sourceDir], 'Test Agent', { global: true });
|
|
101
108
|
|
|
102
|
-
|
|
103
|
-
expect(targetDirExists('other-agent-skill')).toBe(true);
|
|
109
|
+
expect(globalDirExists('other-agent-skill')).toBe(true);
|
|
104
110
|
expect(result.removed).not.toContain('other-agent-skill');
|
|
105
111
|
});
|
|
106
112
|
|
|
107
|
-
it('does NOT remove a skill directory that was just synced', async () => {
|
|
113
|
+
it('does NOT remove a skill directory that was just synced (global)', async () => {
|
|
108
114
|
createSourceSkill('active-skill');
|
|
109
|
-
|
|
110
|
-
createTargetSkillDir('test-agent-active-skill');
|
|
115
|
+
createGlobalSkillDir('test-agent-active-skill');
|
|
111
116
|
|
|
112
117
|
const { syncSkillsToClaudeCode } = await import('../sync-skills.js');
|
|
113
|
-
const result = syncSkillsToClaudeCode([sourceDir], 'Test Agent');
|
|
118
|
+
const result = syncSkillsToClaudeCode([sourceDir], 'Test Agent', { global: true });
|
|
114
119
|
|
|
115
|
-
// "active-skill" should be synced (installed/updated/skipped), not removed
|
|
116
120
|
const synced = [...result.installed, ...result.updated, ...result.skipped];
|
|
117
121
|
expect(synced).toContain('active-skill');
|
|
118
122
|
expect(result.removed).not.toContain('test-agent-active-skill');
|
|
119
|
-
expect(
|
|
123
|
+
expect(globalDirExists('test-agent-active-skill')).toBe(true);
|
|
120
124
|
});
|
|
121
125
|
|
|
122
|
-
it('returns an empty removed array when there are no orphans', async () => {
|
|
126
|
+
it('returns an empty removed array when there are no orphans (global)', async () => {
|
|
123
127
|
createSourceSkill('only-skill');
|
|
124
128
|
|
|
125
129
|
const { syncSkillsToClaudeCode } = await import('../sync-skills.js');
|
|
126
|
-
const result = syncSkillsToClaudeCode([sourceDir], 'Test Agent');
|
|
130
|
+
const result = syncSkillsToClaudeCode([sourceDir], 'Test Agent', { global: true });
|
|
127
131
|
|
|
128
132
|
expect(result.removed).toBeDefined();
|
|
129
133
|
expect(Array.isArray(result.removed)).toBe(true);
|
|
130
134
|
expect(result.removed).toHaveLength(0);
|
|
131
135
|
});
|
|
132
136
|
});
|
|
137
|
+
|
|
138
|
+
// =============================================================================
|
|
139
|
+
// TESTS — Project-local install (symlinks + canonical names)
|
|
140
|
+
// =============================================================================
|
|
141
|
+
|
|
142
|
+
describe('syncSkillsToClaudeCode — project-local install', () => {
|
|
143
|
+
beforeEach(() => {
|
|
144
|
+
setup();
|
|
145
|
+
vi.mock('node:os', async (importOriginal) => {
|
|
146
|
+
const original = await importOriginal<typeof import('node:os')>();
|
|
147
|
+
return {
|
|
148
|
+
...original,
|
|
149
|
+
homedir: () => fakeHome,
|
|
150
|
+
};
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
afterEach(() => {
|
|
155
|
+
vi.restoreAllMocks();
|
|
156
|
+
teardown();
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it('creates symlinks with canonical (unprefixed) names', async () => {
|
|
160
|
+
createSourceSkill('soleri-vault-capture');
|
|
161
|
+
|
|
162
|
+
const { syncSkillsToClaudeCode } = await import('../sync-skills.js');
|
|
163
|
+
const result = syncSkillsToClaudeCode([sourceDir], 'Ernesto', {
|
|
164
|
+
projectRoot: fakeProject,
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
// Should use canonical name, not "ernesto-soleri-vault-capture"
|
|
168
|
+
expect(result.installed).toContain('soleri-vault-capture');
|
|
169
|
+
expect(projectDirExists('soleri-vault-capture')).toBe(true);
|
|
170
|
+
|
|
171
|
+
// Should be a symlink
|
|
172
|
+
const stat = lstatSync(join(projectSkillsDir(), 'soleri-vault-capture'));
|
|
173
|
+
expect(stat.isSymbolicLink()).toBe(true);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it('cleans stale global entries with agent-soleri- prefix', async () => {
|
|
177
|
+
createSourceSkill('soleri-vault-capture');
|
|
178
|
+
// Simulate old global duplicates
|
|
179
|
+
createGlobalSkillDir('ernesto-soleri-vault-capture');
|
|
180
|
+
createGlobalSkillDir('ernesto-soleri-vault-navigator');
|
|
181
|
+
|
|
182
|
+
const { syncSkillsToClaudeCode } = await import('../sync-skills.js');
|
|
183
|
+
const result = syncSkillsToClaudeCode([sourceDir], 'Ernesto', {
|
|
184
|
+
projectRoot: fakeProject,
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
expect(result.cleanedGlobal).toContain('ernesto-soleri-vault-capture');
|
|
188
|
+
expect(result.cleanedGlobal).toContain('ernesto-soleri-vault-navigator');
|
|
189
|
+
expect(globalDirExists('ernesto-soleri-vault-capture')).toBe(false);
|
|
190
|
+
expect(globalDirExists('ernesto-soleri-vault-navigator')).toBe(false);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it('does not clean global entries that do not match agent-soleri- prefix', async () => {
|
|
194
|
+
createSourceSkill('soleri-vault-capture');
|
|
195
|
+
createGlobalSkillDir('other-agent-skill');
|
|
196
|
+
|
|
197
|
+
const { syncSkillsToClaudeCode } = await import('../sync-skills.js');
|
|
198
|
+
const result = syncSkillsToClaudeCode([sourceDir], 'Ernesto', {
|
|
199
|
+
projectRoot: fakeProject,
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
expect(result.cleanedGlobal).not.toContain('other-agent-skill');
|
|
203
|
+
expect(globalDirExists('other-agent-skill')).toBe(true);
|
|
204
|
+
});
|
|
205
|
+
});
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for the SKILL.md validator.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, it, expect } from 'vitest';
|
|
6
|
+
import { validateSkillDocs } from '../validate-skill-docs.js';
|
|
7
|
+
|
|
8
|
+
// Resolve monorepo root from this file's location (packages/core/src/skills/__tests__)
|
|
9
|
+
import { dirname, resolve } from 'node:path';
|
|
10
|
+
import { fileURLToPath } from 'node:url';
|
|
11
|
+
const __dirname2 = dirname(fileURLToPath(import.meta.url));
|
|
12
|
+
const ROOT_DIR = resolve(__dirname2, '../../../../..');
|
|
13
|
+
|
|
14
|
+
describe('validateSkillDocs', () => {
|
|
15
|
+
it('builds a non-empty schema registry', () => {
|
|
16
|
+
const result = validateSkillDocs(ROOT_DIR);
|
|
17
|
+
expect(result.registrySize).toBeGreaterThan(100);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('discovers SKILL.md files', () => {
|
|
21
|
+
const result = validateSkillDocs(ROOT_DIR);
|
|
22
|
+
expect(result.totalFiles).toBeGreaterThan(10);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('extracts op examples from SKILL.md files', () => {
|
|
26
|
+
const result = validateSkillDocs(ROOT_DIR);
|
|
27
|
+
expect(result.totalExamples).toBeGreaterThan(20);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('returns structured error objects with file, line, opName, message', () => {
|
|
31
|
+
const result = validateSkillDocs(ROOT_DIR);
|
|
32
|
+
// We expect some errors in the current state of docs
|
|
33
|
+
if (result.errors.length > 0) {
|
|
34
|
+
const err = result.errors[0];
|
|
35
|
+
expect(err).toHaveProperty('file');
|
|
36
|
+
expect(err).toHaveProperty('line');
|
|
37
|
+
expect(err).toHaveProperty('opName');
|
|
38
|
+
expect(err).toHaveProperty('message');
|
|
39
|
+
expect(typeof err.line).toBe('number');
|
|
40
|
+
expect(err.line).toBeGreaterThan(0);
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('detects the create_plan scope mismatch', () => {
|
|
45
|
+
const result = validateSkillDocs(ROOT_DIR);
|
|
46
|
+
const scopeError = result.errors.find(
|
|
47
|
+
(e) => e.opName === 'create_plan' && e.message.includes('scope'),
|
|
48
|
+
);
|
|
49
|
+
expect(scopeError).toBeDefined();
|
|
50
|
+
expect(scopeError!.message).toContain('Expected string');
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('detects unknown ops', () => {
|
|
54
|
+
const result = validateSkillDocs(ROOT_DIR);
|
|
55
|
+
const unknownOps = result.errors.filter((e) => e.message.includes('unknown op'));
|
|
56
|
+
expect(unknownOps.length).toBeGreaterThan(0);
|
|
57
|
+
});
|
|
58
|
+
});
|
|
@@ -1,22 +1,29 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Skill sync — discovers SKILL.md files in agent skills directories
|
|
3
|
-
* and
|
|
3
|
+
* and installs them for Claude Code discovery.
|
|
4
|
+
*
|
|
5
|
+
* Default: project-local `.claude/skills/` with canonical (unprefixed) names
|
|
6
|
+
* and symlinks so source edits propagate automatically.
|
|
7
|
+
*
|
|
8
|
+
* Optional `global: true` installs to `~/.claude/skills/` with agent-prefixed
|
|
9
|
+
* names and file copies (symlinks to project paths don't work globally).
|
|
4
10
|
*
|
|
5
|
-
* Injects agent branding so users know which agent owns the skill.
|
|
6
11
|
* Called automatically at engine startup and by admin_setup_global.
|
|
7
12
|
*/
|
|
8
13
|
|
|
9
14
|
import {
|
|
10
15
|
cpSync,
|
|
11
16
|
existsSync,
|
|
17
|
+
lstatSync,
|
|
12
18
|
mkdirSync,
|
|
13
19
|
readdirSync,
|
|
14
20
|
readFileSync,
|
|
15
21
|
rmSync,
|
|
16
22
|
statSync,
|
|
23
|
+
symlinkSync,
|
|
17
24
|
writeFileSync,
|
|
18
25
|
} from 'node:fs';
|
|
19
|
-
import { join, dirname } from 'node:path';
|
|
26
|
+
import { join, dirname, relative } from 'node:path';
|
|
20
27
|
import { homedir } from 'node:os';
|
|
21
28
|
import type { SkillMetadata, SourceType } from '../packs/types.js';
|
|
22
29
|
import { classifyTrust } from './trust-classifier.js';
|
|
@@ -35,6 +42,15 @@ export interface SyncResult {
|
|
|
35
42
|
skipped: string[];
|
|
36
43
|
failed: string[];
|
|
37
44
|
removed: string[];
|
|
45
|
+
/** Stale global entries cleaned up during project-local install */
|
|
46
|
+
cleanedGlobal: string[];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface SyncOptions {
|
|
50
|
+
/** Install globally to ~/.claude/skills/ with agent-prefixed names (default: false) */
|
|
51
|
+
global?: boolean;
|
|
52
|
+
/** Project root for project-local installs (default: process.cwd()) */
|
|
53
|
+
projectRoot?: string;
|
|
38
54
|
}
|
|
39
55
|
|
|
40
56
|
/** Error thrown when a skill requires approval due to scripts trust level */
|
|
@@ -96,42 +112,97 @@ function brandSkillContent(content: string, agentName: string, prefixedName?: st
|
|
|
96
112
|
}
|
|
97
113
|
|
|
98
114
|
/**
|
|
99
|
-
* Sync skills
|
|
100
|
-
*
|
|
101
|
-
* -
|
|
102
|
-
*
|
|
115
|
+
* Sync skills to Claude Code's skills directory.
|
|
116
|
+
*
|
|
117
|
+
* Default (project-local): installs to `<projectRoot>/.claude/skills/` using
|
|
118
|
+
* canonical (unprefixed) names and symlinks for automatic propagation.
|
|
119
|
+
*
|
|
120
|
+
* Global (`options.global: true`): installs to `~/.claude/skills/` using
|
|
121
|
+
* agent-prefixed names and file copies (symlinks to project paths won't work).
|
|
122
|
+
*
|
|
123
|
+
* Project-local installs also clean up stale `{agent}-soleri-*` entries from
|
|
124
|
+
* the global `~/.claude/skills/` directory to eliminate duplication.
|
|
103
125
|
*/
|
|
104
|
-
export function syncSkillsToClaudeCode(
|
|
105
|
-
|
|
126
|
+
export function syncSkillsToClaudeCode(
|
|
127
|
+
skillsDirs: string[],
|
|
128
|
+
agentName?: string,
|
|
129
|
+
options: SyncOptions = {},
|
|
130
|
+
): SyncResult {
|
|
131
|
+
const isGlobal = options.global === true;
|
|
132
|
+
const projectRoot = options.projectRoot ?? process.cwd();
|
|
133
|
+
|
|
134
|
+
const skillsDir = isGlobal
|
|
135
|
+
? join(homedir(), '.claude', 'skills')
|
|
136
|
+
: join(projectRoot, '.claude', 'skills');
|
|
137
|
+
|
|
106
138
|
const skills = discoverSkills(skillsDirs);
|
|
107
|
-
const result: SyncResult = {
|
|
139
|
+
const result: SyncResult = {
|
|
140
|
+
installed: [],
|
|
141
|
+
updated: [],
|
|
142
|
+
skipped: [],
|
|
143
|
+
failed: [],
|
|
144
|
+
removed: [],
|
|
145
|
+
cleanedGlobal: [],
|
|
146
|
+
};
|
|
108
147
|
|
|
109
148
|
if (skills.length === 0) return result;
|
|
110
149
|
|
|
111
150
|
for (const skill of skills) {
|
|
112
|
-
|
|
113
|
-
const skillName =
|
|
151
|
+
// Global installs use agent-prefixed names; local uses canonical names
|
|
152
|
+
const skillName =
|
|
153
|
+
isGlobal && agentName
|
|
154
|
+
? `${agentName.toLowerCase().replace(/\s+/g, '-')}-${skill.name}`
|
|
155
|
+
: skill.name;
|
|
114
156
|
const targetDir = join(skillsDir, skillName);
|
|
115
157
|
const targetPath = join(targetDir, 'SKILL.md');
|
|
158
|
+
|
|
116
159
|
try {
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
} else {
|
|
127
|
-
const sourceMtime = statSync(skill.sourcePath).mtimeMs;
|
|
128
|
-
const targetMtime = statSync(targetPath).mtimeMs;
|
|
129
|
-
if (sourceMtime > targetMtime) {
|
|
160
|
+
if (isGlobal) {
|
|
161
|
+
// Global: copy with branding (original behavior)
|
|
162
|
+
const sourceContent = readFileSync(skill.sourcePath, 'utf-8');
|
|
163
|
+
const branded = agentName
|
|
164
|
+
? brandSkillContent(sourceContent, agentName, skillName)
|
|
165
|
+
: sourceContent;
|
|
166
|
+
|
|
167
|
+
if (!existsSync(targetPath)) {
|
|
168
|
+
mkdirSync(targetDir, { recursive: true });
|
|
130
169
|
writeFileSync(targetPath, branded);
|
|
131
|
-
result.
|
|
170
|
+
result.installed.push(skill.name);
|
|
132
171
|
} else {
|
|
133
|
-
|
|
172
|
+
const sourceMtime = statSync(skill.sourcePath).mtimeMs;
|
|
173
|
+
const targetMtime = statSync(targetPath).mtimeMs;
|
|
174
|
+
if (sourceMtime > targetMtime) {
|
|
175
|
+
writeFileSync(targetPath, branded);
|
|
176
|
+
result.updated.push(skill.name);
|
|
177
|
+
} else {
|
|
178
|
+
result.skipped.push(skill.name);
|
|
179
|
+
}
|
|
134
180
|
}
|
|
181
|
+
} else {
|
|
182
|
+
// Project-local: symlink the skill directory
|
|
183
|
+
const sourceSkillDir = dirname(skill.sourcePath);
|
|
184
|
+
|
|
185
|
+
if (existsSync(targetDir)) {
|
|
186
|
+
// Check if it's already a symlink pointing to the right place
|
|
187
|
+
try {
|
|
188
|
+
const stat = lstatSync(targetDir);
|
|
189
|
+
if (stat.isSymbolicLink()) {
|
|
190
|
+
// Already a symlink — skip
|
|
191
|
+
result.skipped.push(skill.name);
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
} catch {
|
|
195
|
+
// lstat failed — remove and recreate
|
|
196
|
+
}
|
|
197
|
+
// Not a symlink — remove the copy and replace with symlink
|
|
198
|
+
rmSync(targetDir, { recursive: true, force: true });
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
mkdirSync(skillsDir, { recursive: true });
|
|
202
|
+
// Use relative symlink so the project stays portable
|
|
203
|
+
const relPath = relative(skillsDir, sourceSkillDir);
|
|
204
|
+
symlinkSync(relPath, targetDir);
|
|
205
|
+
result.installed.push(skill.name);
|
|
135
206
|
}
|
|
136
207
|
} catch {
|
|
137
208
|
result.failed.push(skill.name);
|
|
@@ -142,16 +213,18 @@ export function syncSkillsToClaudeCode(skillsDirs: string[], agentName?: string)
|
|
|
142
213
|
if (agentName) {
|
|
143
214
|
const prefix = `${agentName.toLowerCase().replace(/\s+/g, '-')}-`;
|
|
144
215
|
const syncedNames = new Set<string>(
|
|
145
|
-
[...result.installed, ...result.updated, ...result.skipped, ...result.failed].map(
|
|
146
|
-
|
|
216
|
+
[...result.installed, ...result.updated, ...result.skipped, ...result.failed].map((name) =>
|
|
217
|
+
isGlobal ? `${prefix}${name}` : name,
|
|
147
218
|
),
|
|
148
219
|
);
|
|
149
220
|
|
|
150
221
|
try {
|
|
151
222
|
const entries = readdirSync(skillsDir, { withFileTypes: true });
|
|
152
223
|
for (const entry of entries) {
|
|
153
|
-
if (!entry.isDirectory())
|
|
154
|
-
|
|
224
|
+
if (!entry.isDirectory() && !lstatSync(join(skillsDir, entry.name)).isSymbolicLink())
|
|
225
|
+
continue;
|
|
226
|
+
const matchPrefix = isGlobal ? entry.name.startsWith(prefix) : true;
|
|
227
|
+
if (!matchPrefix) continue;
|
|
155
228
|
if (syncedNames.has(entry.name)) continue;
|
|
156
229
|
|
|
157
230
|
// Orphan detected — stage backup then remove
|
|
@@ -160,7 +233,10 @@ export function syncSkillsToClaudeCode(skillsDirs: string[], agentName?: string)
|
|
|
160
233
|
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
161
234
|
const stagingDir = join(homedir(), '.soleri', 'staging', timestamp);
|
|
162
235
|
mkdirSync(stagingDir, { recursive: true });
|
|
163
|
-
|
|
236
|
+
const orphanStat = lstatSync(orphanPath);
|
|
237
|
+
if (!orphanStat.isSymbolicLink()) {
|
|
238
|
+
cpSync(orphanPath, join(stagingDir, entry.name), { recursive: true });
|
|
239
|
+
}
|
|
164
240
|
rmSync(orphanPath, { recursive: true, force: true });
|
|
165
241
|
result.removed.push(entry.name);
|
|
166
242
|
} catch {
|
|
@@ -172,9 +248,53 @@ export function syncSkillsToClaudeCode(skillsDirs: string[], agentName?: string)
|
|
|
172
248
|
}
|
|
173
249
|
}
|
|
174
250
|
|
|
251
|
+
// Task 3: Clean up stale global entries when doing a project-local install
|
|
252
|
+
if (!isGlobal && agentName) {
|
|
253
|
+
cleanStaleGlobalSkills(agentName, result);
|
|
254
|
+
}
|
|
255
|
+
|
|
175
256
|
return result;
|
|
176
257
|
}
|
|
177
258
|
|
|
259
|
+
/**
|
|
260
|
+
* Remove ALL `{agent}-soleri-*` entries from ~/.claude/skills/
|
|
261
|
+
* to clean up duplicates left by the old global-install behavior.
|
|
262
|
+
*
|
|
263
|
+
* Cleans entries from ALL agents, not just the current one — any
|
|
264
|
+
* `*-soleri-*` entry in the global dir is a stale copy from a previous
|
|
265
|
+
* global install. Canonical skills now live in project-local .claude/skills/.
|
|
266
|
+
*/
|
|
267
|
+
function cleanStaleGlobalSkills(agentName: string, result: SyncResult): void {
|
|
268
|
+
const globalSkillsDir = join(homedir(), '.claude', 'skills');
|
|
269
|
+
if (!existsSync(globalSkillsDir)) return;
|
|
270
|
+
|
|
271
|
+
// Match any agent-prefixed soleri skill: <anything>-soleri-<skillname>
|
|
272
|
+
// Canonical project-local names look like "soleri-*" (no agent prefix).
|
|
273
|
+
const stalePattern = /^.+-soleri-.+$/;
|
|
274
|
+
|
|
275
|
+
try {
|
|
276
|
+
const entries = readdirSync(globalSkillsDir, { withFileTypes: true });
|
|
277
|
+
for (const entry of entries) {
|
|
278
|
+
if (!entry.isDirectory()) continue;
|
|
279
|
+
if (!stalePattern.test(entry.name)) continue;
|
|
280
|
+
|
|
281
|
+
const staleDir = join(globalSkillsDir, entry.name);
|
|
282
|
+
try {
|
|
283
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
284
|
+
const stagingDir = join(homedir(), '.soleri', 'staging', timestamp);
|
|
285
|
+
mkdirSync(stagingDir, { recursive: true });
|
|
286
|
+
cpSync(staleDir, join(stagingDir, entry.name), { recursive: true });
|
|
287
|
+
rmSync(staleDir, { recursive: true, force: true });
|
|
288
|
+
result.cleanedGlobal.push(entry.name);
|
|
289
|
+
} catch {
|
|
290
|
+
// Best-effort cleanup — don't fail the sync
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
} catch {
|
|
294
|
+
// Global skills dir unreadable — nothing to clean
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
178
298
|
// =============================================================================
|
|
179
299
|
// TRUST CLASSIFICATION & SOURCE TRACKING
|
|
180
300
|
// =============================================================================
|