@skillsmith/cli 0.5.12 → 0.6.1
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/CHANGELOG.md +16 -0
- package/README.md +92 -2
- package/dist/.tsbuildinfo +1 -1
- package/dist/src/commands/audit-collisions.d.ts +71 -0
- package/dist/src/commands/audit-collisions.d.ts.map +1 -0
- package/dist/src/commands/audit-collisions.js +300 -0
- package/dist/src/commands/audit-collisions.js.map +1 -0
- package/dist/src/commands/audit.d.ts +27 -2
- package/dist/src/commands/audit.d.ts.map +1 -1
- package/dist/src/commands/audit.js +73 -13
- package/dist/src/commands/audit.js.map +1 -1
- package/dist/src/commands/audit.test.js +2 -2
- package/dist/src/commands/audit.test.js.map +1 -1
- package/dist/src/commands/config.d.ts +55 -0
- package/dist/src/commands/config.d.ts.map +1 -0
- package/dist/src/commands/config.js +211 -0
- package/dist/src/commands/config.js.map +1 -0
- package/dist/src/commands/create.d.ts +3 -3
- package/dist/src/commands/create.d.ts.map +1 -1
- package/dist/src/commands/create.js +11 -9
- package/dist/src/commands/create.js.map +1 -1
- package/dist/src/commands/diff.js +2 -2
- package/dist/src/commands/diff.js.map +1 -1
- package/dist/src/commands/import-local.d.ts +21 -0
- package/dist/src/commands/import-local.d.ts.map +1 -0
- package/dist/src/commands/import-local.helpers.d.ts +28 -0
- package/dist/src/commands/import-local.helpers.d.ts.map +1 -0
- package/dist/src/commands/import-local.helpers.js +166 -0
- package/dist/src/commands/import-local.helpers.js.map +1 -0
- package/dist/src/commands/import-local.js +264 -0
- package/dist/src/commands/import-local.js.map +1 -0
- package/dist/src/commands/import-local.types.d.ts +59 -0
- package/dist/src/commands/import-local.types.d.ts.map +1 -0
- package/dist/src/commands/import-local.types.js +2 -0
- package/dist/src/commands/import-local.types.js.map +1 -0
- package/dist/src/{import.d.ts → commands/import.d.ts} +11 -0
- package/dist/src/commands/import.d.ts.map +1 -0
- package/dist/src/{import.js → commands/import.js} +39 -4
- package/dist/src/commands/import.js.map +1 -0
- package/dist/src/commands/index.d.ts +3 -0
- package/dist/src/commands/index.d.ts.map +1 -1
- package/dist/src/commands/index.js +6 -0
- package/dist/src/commands/index.js.map +1 -1
- package/dist/src/commands/info.d.ts.map +1 -1
- package/dist/src/commands/info.js +3 -3
- package/dist/src/commands/info.js.map +1 -1
- package/dist/src/commands/install-skill.d.ts.map +1 -1
- package/dist/src/commands/install-skill.js +6 -3
- package/dist/src/commands/install-skill.js.map +1 -1
- package/dist/src/commands/install.d.ts.map +1 -1
- package/dist/src/commands/install.js +81 -7
- package/dist/src/commands/install.js.map +1 -1
- package/dist/src/commands/login.d.ts.map +1 -1
- package/dist/src/commands/login.js +37 -0
- package/dist/src/commands/login.js.map +1 -1
- package/dist/src/commands/login.post-login-sync.test.d.ts +2 -0
- package/dist/src/commands/login.post-login-sync.test.d.ts.map +1 -0
- package/dist/src/commands/login.post-login-sync.test.js +120 -0
- package/dist/src/commands/login.post-login-sync.test.js.map +1 -0
- package/dist/src/commands/login.test-helpers.d.ts +7 -0
- package/dist/src/commands/login.test-helpers.d.ts.map +1 -0
- package/dist/src/commands/login.test-helpers.js +44 -0
- package/dist/src/commands/login.test-helpers.js.map +1 -0
- package/dist/src/commands/login.test.js +30 -31
- package/dist/src/commands/login.test.js.map +1 -1
- package/dist/src/commands/manage.d.ts.map +1 -1
- package/dist/src/commands/manage.js +17 -4
- package/dist/src/commands/manage.js.map +1 -1
- package/dist/src/commands/recommend.helpers.d.ts +5 -1
- package/dist/src/commands/recommend.helpers.d.ts.map +1 -1
- package/dist/src/commands/recommend.helpers.js +9 -3
- package/dist/src/commands/recommend.helpers.js.map +1 -1
- package/dist/src/commands/run-registry-sync.d.ts +31 -0
- package/dist/src/commands/run-registry-sync.d.ts.map +1 -0
- package/dist/src/commands/run-registry-sync.js +45 -0
- package/dist/src/commands/run-registry-sync.js.map +1 -0
- package/dist/src/commands/search.d.ts.map +1 -1
- package/dist/src/commands/search.helpers.d.ts +23 -0
- package/dist/src/commands/search.helpers.d.ts.map +1 -0
- package/dist/src/commands/search.helpers.js +57 -0
- package/dist/src/commands/search.helpers.js.map +1 -0
- package/dist/src/commands/search.js +12 -5
- package/dist/src/commands/search.js.map +1 -1
- package/dist/src/commands/sync.d.ts.map +1 -1
- package/dist/src/commands/sync.js +10 -21
- package/dist/src/commands/sync.js.map +1 -1
- package/dist/src/config.d.ts +6 -1
- package/dist/src/config.d.ts.map +1 -1
- package/dist/src/config.js +8 -2
- package/dist/src/config.js.map +1 -1
- package/dist/src/index.d.ts +1 -1
- package/dist/src/index.js +10 -28
- package/dist/src/index.js.map +1 -1
- package/dist/src/templates/mcp-server.template.d.ts +1 -1
- package/dist/src/templates/mcp-server.template.d.ts.map +1 -1
- package/dist/src/templates/mcp-server.template.js +13 -13
- package/dist/src/templates/mcp-server.template.js.map +1 -1
- package/dist/src/templates/mcp-server.template.snippets.d.ts +51 -0
- package/dist/src/templates/mcp-server.template.snippets.d.ts.map +1 -0
- package/dist/src/templates/mcp-server.template.snippets.js +162 -0
- package/dist/src/templates/mcp-server.template.snippets.js.map +1 -0
- package/dist/src/templates/readme.md.template.d.ts +1 -1
- package/dist/src/templates/readme.md.template.d.ts.map +1 -1
- package/dist/src/templates/readme.md.template.js +2 -2
- package/dist/src/utils/open-database.d.ts +27 -0
- package/dist/src/utils/open-database.d.ts.map +1 -0
- package/dist/src/utils/open-database.js +31 -0
- package/dist/src/utils/open-database.js.map +1 -0
- package/dist/src/utils/skills-directory.d.ts +26 -7
- package/dist/src/utils/skills-directory.d.ts.map +1 -1
- package/dist/src/utils/skills-directory.js +78 -33
- package/dist/src/utils/skills-directory.js.map +1 -1
- package/dist/tests/audit-collisions.test.d.ts +42 -0
- package/dist/tests/audit-collisions.test.d.ts.map +1 -0
- package/dist/tests/audit-collisions.test.js +370 -0
- package/dist/tests/audit-collisions.test.js.map +1 -0
- package/dist/tests/audit-restructure.test.d.ts +15 -0
- package/dist/tests/audit-restructure.test.d.ts.map +1 -0
- package/dist/tests/audit-restructure.test.js +151 -0
- package/dist/tests/audit-restructure.test.js.map +1 -0
- package/dist/tests/config.test.d.ts +33 -0
- package/dist/tests/config.test.d.ts.map +1 -0
- package/dist/tests/config.test.js +240 -0
- package/dist/tests/config.test.js.map +1 -0
- package/dist/tests/install-multi-client.test.d.ts +2 -0
- package/dist/tests/install-multi-client.test.d.ts.map +1 -0
- package/dist/tests/install-multi-client.test.js +202 -0
- package/dist/tests/install-multi-client.test.js.map +1 -0
- package/dist/tests/integration/import-local.test.d.ts +2 -0
- package/dist/tests/integration/import-local.test.d.ts.map +1 -0
- package/dist/tests/integration/import-local.test.js +158 -0
- package/dist/tests/integration/import-local.test.js.map +1 -0
- package/dist/tests/mcp-server-template.test.js +21 -0
- package/dist/tests/mcp-server-template.test.js.map +1 -1
- package/dist/tests/open-database.test.d.ts +2 -0
- package/dist/tests/open-database.test.d.ts.map +1 -0
- package/dist/tests/open-database.test.js +52 -0
- package/dist/tests/open-database.test.js.map +1 -0
- package/dist/tests/search-helpers.test.d.ts +2 -0
- package/dist/tests/search-helpers.test.d.ts.map +1 -0
- package/dist/tests/search-helpers.test.js +110 -0
- package/dist/tests/search-helpers.test.js.map +1 -0
- package/dist/tests/skills-directory.test.d.ts +2 -0
- package/dist/tests/skills-directory.test.d.ts.map +1 -0
- package/dist/tests/skills-directory.test.js +97 -0
- package/dist/tests/skills-directory.test.js.map +1 -0
- package/dist/tests/unit/commands/install.test.js +75 -1
- package/dist/tests/unit/commands/install.test.js.map +1 -1
- package/dist/tests/unit/import-local.helpers.test.d.ts +2 -0
- package/dist/tests/unit/import-local.helpers.test.d.ts.map +1 -0
- package/dist/tests/unit/import-local.helpers.test.js +96 -0
- package/dist/tests/unit/import-local.helpers.test.js.map +1 -0
- package/package.json +10 -4
- package/dist/src/import.d.ts.map +0 -1
- package/dist/src/import.js.map +0 -1
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview SMI-4590 Wave 4 PR 5/6 — `sklx config get/set audit_mode` tests
|
|
3
|
+
* @module @skillsmith/cli/tests/config
|
|
4
|
+
*
|
|
5
|
+
* Coverage (plan §810-813 — load-bearing tier-revalidation security tests):
|
|
6
|
+
*
|
|
7
|
+
* Tier revalidation matrix — the `set` path MUST refuse a write when
|
|
8
|
+
* `tierAllowsAuditMode(tier, mode)` returns false. No file IO occurs on
|
|
9
|
+
* rejection; the existing config (if any) stays intact.
|
|
10
|
+
*
|
|
11
|
+
* 1. community + power_user → audit.mode.tier_ineligible, NO write
|
|
12
|
+
* 2. community + governance → audit.mode.tier_ineligible, NO write
|
|
13
|
+
* 3. individual + power_user → audit.mode.tier_ineligible, NO write
|
|
14
|
+
* 4. individual + governance → audit.mode.tier_ineligible, NO write
|
|
15
|
+
* 5. team + power_user → SUCCESS, file written
|
|
16
|
+
* 6. team + governance → audit.mode.tier_ineligible (Enterprise-only)
|
|
17
|
+
* 7. enterprise + governance → SUCCESS, file written
|
|
18
|
+
*
|
|
19
|
+
* Value validation:
|
|
20
|
+
* 8. any tier + invalid_value → audit.mode.invalid_value, NO write
|
|
21
|
+
* 9. any tier + preventative → SUCCESS for every tier
|
|
22
|
+
* 10. any tier + off → SUCCESS for every tier
|
|
23
|
+
*
|
|
24
|
+
* Unsupported keys:
|
|
25
|
+
* 11. set with unknown key → config.unsupported_key, NO write
|
|
26
|
+
* 12. get with unknown key → config.unsupported_key
|
|
27
|
+
*
|
|
28
|
+
* Get path:
|
|
29
|
+
* 13. get audit_mode reads existing file value when present
|
|
30
|
+
* 14. get audit_mode falls back to tier default when unset
|
|
31
|
+
*/
|
|
32
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
33
|
+
import * as fs from 'node:fs';
|
|
34
|
+
import * as os from 'node:os';
|
|
35
|
+
import { join } from 'node:path';
|
|
36
|
+
const mocks = vi.hoisted(() => ({
|
|
37
|
+
getLicenseStatus: vi.fn(),
|
|
38
|
+
}));
|
|
39
|
+
vi.mock('../src/utils/license.js', () => ({
|
|
40
|
+
getLicenseStatus: () => mocks.getLicenseStatus(),
|
|
41
|
+
}));
|
|
42
|
+
import { runConfigGet, runConfigSet, ConfigError, configPath } from '../src/commands/config.js';
|
|
43
|
+
// ============================================================================
|
|
44
|
+
// Test harness — sandbox HOME so config writes are isolated.
|
|
45
|
+
// ============================================================================
|
|
46
|
+
let tmpHome;
|
|
47
|
+
let originalHome;
|
|
48
|
+
let stdoutSpy;
|
|
49
|
+
beforeEach(() => {
|
|
50
|
+
tmpHome = fs.mkdtempSync(join(os.tmpdir(), 'sklx-config-test-'));
|
|
51
|
+
originalHome = process.env['HOME'];
|
|
52
|
+
process.env['HOME'] = tmpHome;
|
|
53
|
+
stdoutSpy = vi.spyOn(console, 'log').mockImplementation(() => { });
|
|
54
|
+
vi.clearAllMocks();
|
|
55
|
+
});
|
|
56
|
+
afterEach(() => {
|
|
57
|
+
if (originalHome !== undefined) {
|
|
58
|
+
process.env['HOME'] = originalHome;
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
delete process.env['HOME'];
|
|
62
|
+
}
|
|
63
|
+
fs.rmSync(tmpHome, { recursive: true, force: true });
|
|
64
|
+
stdoutSpy.mockRestore();
|
|
65
|
+
});
|
|
66
|
+
function setTier(tier) {
|
|
67
|
+
mocks.getLicenseStatus.mockResolvedValue({ tier });
|
|
68
|
+
}
|
|
69
|
+
function configFileExists() {
|
|
70
|
+
return fs.existsSync(configPath());
|
|
71
|
+
}
|
|
72
|
+
function readConfigRaw() {
|
|
73
|
+
if (!configFileExists())
|
|
74
|
+
return {};
|
|
75
|
+
return JSON.parse(fs.readFileSync(configPath(), 'utf-8'));
|
|
76
|
+
}
|
|
77
|
+
// ============================================================================
|
|
78
|
+
// Tests
|
|
79
|
+
// ============================================================================
|
|
80
|
+
describe('SMI-4590 Wave 4 PR 5/6 — sklx config tier revalidation (plan §810-813)', () => {
|
|
81
|
+
describe('community tier — power_user / governance forbidden', () => {
|
|
82
|
+
it('community + power_user → tier_ineligible, NO write', async () => {
|
|
83
|
+
setTier('community');
|
|
84
|
+
try {
|
|
85
|
+
await runConfigSet('audit_mode', 'power_user');
|
|
86
|
+
expect.fail('Expected ConfigError');
|
|
87
|
+
}
|
|
88
|
+
catch (error) {
|
|
89
|
+
expect(error).toBeInstanceOf(ConfigError);
|
|
90
|
+
expect(error.code).toBe('audit.mode.tier_ineligible');
|
|
91
|
+
}
|
|
92
|
+
expect(configFileExists()).toBe(false);
|
|
93
|
+
});
|
|
94
|
+
it('community + governance → tier_ineligible, NO write', async () => {
|
|
95
|
+
setTier('community');
|
|
96
|
+
try {
|
|
97
|
+
await runConfigSet('audit_mode', 'governance');
|
|
98
|
+
expect.fail('Expected ConfigError');
|
|
99
|
+
}
|
|
100
|
+
catch (error) {
|
|
101
|
+
expect(error.code).toBe('audit.mode.tier_ineligible');
|
|
102
|
+
}
|
|
103
|
+
expect(configFileExists()).toBe(false);
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
describe('individual tier — power_user / governance forbidden', () => {
|
|
107
|
+
it('individual + power_user → tier_ineligible, NO write', async () => {
|
|
108
|
+
setTier('individual');
|
|
109
|
+
try {
|
|
110
|
+
await runConfigSet('audit_mode', 'power_user');
|
|
111
|
+
expect.fail('Expected ConfigError');
|
|
112
|
+
}
|
|
113
|
+
catch (error) {
|
|
114
|
+
expect(error.code).toBe('audit.mode.tier_ineligible');
|
|
115
|
+
}
|
|
116
|
+
expect(configFileExists()).toBe(false);
|
|
117
|
+
});
|
|
118
|
+
it('individual + governance → tier_ineligible, NO write', async () => {
|
|
119
|
+
setTier('individual');
|
|
120
|
+
try {
|
|
121
|
+
await runConfigSet('audit_mode', 'governance');
|
|
122
|
+
expect.fail('Expected ConfigError');
|
|
123
|
+
}
|
|
124
|
+
catch (error) {
|
|
125
|
+
expect(error.code).toBe('audit.mode.tier_ineligible');
|
|
126
|
+
}
|
|
127
|
+
expect(configFileExists()).toBe(false);
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
describe('team tier — power_user allowed, governance forbidden', () => {
|
|
131
|
+
it('team + power_user → SUCCESS, file written', async () => {
|
|
132
|
+
setTier('team');
|
|
133
|
+
await runConfigSet('audit_mode', 'power_user');
|
|
134
|
+
expect(configFileExists()).toBe(true);
|
|
135
|
+
expect(readConfigRaw()['audit_mode']).toBe('power_user');
|
|
136
|
+
});
|
|
137
|
+
it('team + governance → tier_ineligible (Enterprise-only)', async () => {
|
|
138
|
+
setTier('team');
|
|
139
|
+
try {
|
|
140
|
+
await runConfigSet('audit_mode', 'governance');
|
|
141
|
+
expect.fail('Expected ConfigError');
|
|
142
|
+
}
|
|
143
|
+
catch (error) {
|
|
144
|
+
expect(error.code).toBe('audit.mode.tier_ineligible');
|
|
145
|
+
}
|
|
146
|
+
expect(configFileExists()).toBe(false);
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
describe('enterprise tier — governance allowed', () => {
|
|
150
|
+
it('enterprise + governance → SUCCESS, file written', async () => {
|
|
151
|
+
setTier('enterprise');
|
|
152
|
+
await runConfigSet('audit_mode', 'governance');
|
|
153
|
+
expect(configFileExists()).toBe(true);
|
|
154
|
+
expect(readConfigRaw()['audit_mode']).toBe('governance');
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
describe('SMI-4590 Wave 4 PR 5/6 — sklx config value validation', () => {
|
|
159
|
+
it('invalid_value → audit.mode.invalid_value, NO write', async () => {
|
|
160
|
+
setTier('enterprise');
|
|
161
|
+
try {
|
|
162
|
+
await runConfigSet('audit_mode', 'definitely-not-a-mode');
|
|
163
|
+
expect.fail('Expected ConfigError');
|
|
164
|
+
}
|
|
165
|
+
catch (error) {
|
|
166
|
+
expect(error.code).toBe('audit.mode.invalid_value');
|
|
167
|
+
}
|
|
168
|
+
expect(configFileExists()).toBe(false);
|
|
169
|
+
});
|
|
170
|
+
it('preventative is allowed for every tier', async () => {
|
|
171
|
+
for (const tier of ['community', 'individual', 'team', 'enterprise']) {
|
|
172
|
+
// Reset sandbox between tiers.
|
|
173
|
+
fs.rmSync(configPath(), { force: true });
|
|
174
|
+
setTier(tier);
|
|
175
|
+
await runConfigSet('audit_mode', 'preventative');
|
|
176
|
+
expect(readConfigRaw()['audit_mode']).toBe('preventative');
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
it('off is allowed for every tier', async () => {
|
|
180
|
+
for (const tier of ['community', 'individual', 'team', 'enterprise']) {
|
|
181
|
+
fs.rmSync(configPath(), { force: true });
|
|
182
|
+
setTier(tier);
|
|
183
|
+
await runConfigSet('audit_mode', 'off');
|
|
184
|
+
expect(readConfigRaw()['audit_mode']).toBe('off');
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
describe('SMI-4590 Wave 4 PR 5/6 — sklx config unsupported keys', () => {
|
|
189
|
+
it('set with unknown key → config.unsupported_key, NO write', async () => {
|
|
190
|
+
setTier('enterprise');
|
|
191
|
+
try {
|
|
192
|
+
await runConfigSet('telemetry_endpoint', 'https://example.com');
|
|
193
|
+
expect.fail('Expected ConfigError');
|
|
194
|
+
}
|
|
195
|
+
catch (error) {
|
|
196
|
+
expect(error.code).toBe('config.unsupported_key');
|
|
197
|
+
}
|
|
198
|
+
expect(configFileExists()).toBe(false);
|
|
199
|
+
});
|
|
200
|
+
it('get with unknown key → config.unsupported_key', async () => {
|
|
201
|
+
setTier('enterprise');
|
|
202
|
+
try {
|
|
203
|
+
await runConfigGet('telemetry_endpoint');
|
|
204
|
+
expect.fail('Expected ConfigError');
|
|
205
|
+
}
|
|
206
|
+
catch (error) {
|
|
207
|
+
expect(error.code).toBe('config.unsupported_key');
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
});
|
|
211
|
+
describe('SMI-4590 Wave 4 PR 5/6 — sklx config get audit_mode', () => {
|
|
212
|
+
it('reads existing file value when present', async () => {
|
|
213
|
+
setTier('team');
|
|
214
|
+
await runConfigSet('audit_mode', 'power_user');
|
|
215
|
+
stdoutSpy.mockClear();
|
|
216
|
+
await runConfigGet('audit_mode');
|
|
217
|
+
const out = stdoutSpy.mock.calls.map((c) => String(c[0] ?? '')).join('\n');
|
|
218
|
+
expect(out).toContain('power_user');
|
|
219
|
+
});
|
|
220
|
+
it('falls back to tier default when no config file exists', async () => {
|
|
221
|
+
setTier('community');
|
|
222
|
+
// No prior write, so file does not exist.
|
|
223
|
+
expect(configFileExists()).toBe(false);
|
|
224
|
+
await runConfigGet('audit_mode');
|
|
225
|
+
const out = stdoutSpy.mock.calls.map((c) => String(c[0] ?? '')).join('\n');
|
|
226
|
+
// Output should reference the tier default plus the "(tier default)" hint.
|
|
227
|
+
expect(out).toMatch(/tier default/);
|
|
228
|
+
});
|
|
229
|
+
it('preserves unknown keys on read-modify-write (atomic merge)', async () => {
|
|
230
|
+
setTier('team');
|
|
231
|
+
// Seed the config with extra keys directly.
|
|
232
|
+
fs.mkdirSync(join(tmpHome, '.skillsmith'), { recursive: true });
|
|
233
|
+
fs.writeFileSync(configPath(), JSON.stringify({ extra_key: 'value', audit_mode: 'preventative' }), 'utf-8');
|
|
234
|
+
await runConfigSet('audit_mode', 'power_user');
|
|
235
|
+
const after = readConfigRaw();
|
|
236
|
+
expect(after['audit_mode']).toBe('power_user');
|
|
237
|
+
expect(after['extra_key']).toBe('value');
|
|
238
|
+
});
|
|
239
|
+
});
|
|
240
|
+
//# sourceMappingURL=config.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.test.js","sourceRoot":"","sources":["../../tests/config.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"install-multi-client.test.d.ts","sourceRoot":"","sources":["../../tests/install-multi-client.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SMI-4578 Step 6: install --client / --also-link end-to-end tests.
|
|
3
|
+
*
|
|
4
|
+
* Exercises the parseAlsoLink + assertClientId validation surface in
|
|
5
|
+
* install.ts plus the addLink fan-out behaviour from
|
|
6
|
+
* @skillsmith/core/install. Each test runs in a temp $HOME so we
|
|
7
|
+
* never touch the real filesystem.
|
|
8
|
+
*
|
|
9
|
+
* Skill installation itself (the SkillInstallationService.install
|
|
10
|
+
* call) is NOT exercised here — that's covered by install.test.ts,
|
|
11
|
+
* service unit tests, and the post-merge smoke. These tests focus on
|
|
12
|
+
* the multi-client surface: target-directory resolution, fan-out
|
|
13
|
+
* manifest writes, conflict refuse, cycle refuse.
|
|
14
|
+
*/
|
|
15
|
+
import { mkdtemp, mkdir, readFile, rm, stat, lstat, writeFile } from 'node:fs/promises';
|
|
16
|
+
import { tmpdir } from 'node:os';
|
|
17
|
+
import * as path from 'node:path';
|
|
18
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
19
|
+
const ORIGINAL_HOME = process.env['HOME'];
|
|
20
|
+
const ORIGINAL_USERPROFILE = process.env['USERPROFILE'];
|
|
21
|
+
const ORIGINAL_CLIENT = process.env['SKILLSMITH_CLIENT'];
|
|
22
|
+
let homeDir;
|
|
23
|
+
beforeEach(async () => {
|
|
24
|
+
homeDir = await mkdtemp(path.join(tmpdir(), 'smi4578-multi-'));
|
|
25
|
+
process.env['HOME'] = homeDir;
|
|
26
|
+
process.env['USERPROFILE'] = homeDir;
|
|
27
|
+
delete process.env['SKILLSMITH_CLIENT'];
|
|
28
|
+
// CLIENT_NATIVE_PATHS computes homedir() at module import time —
|
|
29
|
+
// reset modules so each test sees its own $HOME.
|
|
30
|
+
vi.resetModules();
|
|
31
|
+
});
|
|
32
|
+
afterEach(async () => {
|
|
33
|
+
if (ORIGINAL_HOME === undefined)
|
|
34
|
+
delete process.env['HOME'];
|
|
35
|
+
else
|
|
36
|
+
process.env['HOME'] = ORIGINAL_HOME;
|
|
37
|
+
if (ORIGINAL_USERPROFILE === undefined)
|
|
38
|
+
delete process.env['USERPROFILE'];
|
|
39
|
+
else
|
|
40
|
+
process.env['USERPROFILE'] = ORIGINAL_USERPROFILE;
|
|
41
|
+
if (ORIGINAL_CLIENT === undefined)
|
|
42
|
+
delete process.env['SKILLSMITH_CLIENT'];
|
|
43
|
+
else
|
|
44
|
+
process.env['SKILLSMITH_CLIENT'] = ORIGINAL_CLIENT;
|
|
45
|
+
await rm(homeDir, { recursive: true, force: true });
|
|
46
|
+
});
|
|
47
|
+
async function seedSkill(skillId, body = '# test\n') {
|
|
48
|
+
const dir = path.join(homeDir, '.claude', 'skills', skillId);
|
|
49
|
+
await mkdir(dir, { recursive: true });
|
|
50
|
+
await writeFile(path.join(dir, 'SKILL.md'), `---\nname: ${skillId}\n---\n${body}`, 'utf-8');
|
|
51
|
+
return dir;
|
|
52
|
+
}
|
|
53
|
+
describe('install --client / --also-link', () => {
|
|
54
|
+
describe('--client target directory', () => {
|
|
55
|
+
it('getInstallPath returns the cursor directory for --client cursor', async () => {
|
|
56
|
+
const { getInstallPath } = await import('@skillsmith/core/install');
|
|
57
|
+
expect(getInstallPath('cursor')).toBe(path.join(homeDir, '.cursor', 'skills'));
|
|
58
|
+
});
|
|
59
|
+
it('SKILLSMITH_CLIENT env var routes resolveClientPath', async () => {
|
|
60
|
+
process.env['SKILLSMITH_CLIENT'] = 'windsurf';
|
|
61
|
+
const { resolveClientPath } = await import('@skillsmith/core/install');
|
|
62
|
+
expect(resolveClientPath()).toBe(path.join(homeDir, '.codeium', 'windsurf', 'skills'));
|
|
63
|
+
});
|
|
64
|
+
it('explicit override beats env var', async () => {
|
|
65
|
+
process.env['SKILLSMITH_CLIENT'] = 'cursor';
|
|
66
|
+
const { resolveClientPath } = await import('@skillsmith/core/install');
|
|
67
|
+
expect(resolveClientPath('agents')).toBe(path.join(homeDir, '.agents', 'skills'));
|
|
68
|
+
});
|
|
69
|
+
it('rejects invalid SKILLSMITH_CLIENT with a friendly hint', async () => {
|
|
70
|
+
process.env['SKILLSMITH_CLIENT'] = 'codex';
|
|
71
|
+
const { resolveClientPath } = await import('@skillsmith/core/install');
|
|
72
|
+
expect(() => resolveClientPath()).toThrow(/--client agents/);
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
describe('--also-link copy default', () => {
|
|
76
|
+
it('copies the source skill into a secondary client directory', async () => {
|
|
77
|
+
await seedSkill('foo', '# foo\n');
|
|
78
|
+
const { addLink, listLinks } = await import('@skillsmith/core/install');
|
|
79
|
+
const result = await addLink({
|
|
80
|
+
skillId: 'foo',
|
|
81
|
+
fromClient: 'claude-code',
|
|
82
|
+
toClient: 'cursor',
|
|
83
|
+
});
|
|
84
|
+
expect(result.record.kind).toBe('copy');
|
|
85
|
+
const dest = path.join(homeDir, '.cursor', 'skills', 'foo');
|
|
86
|
+
const skillMd = await readFile(path.join(dest, 'SKILL.md'), 'utf-8');
|
|
87
|
+
expect(skillMd).toContain('# foo');
|
|
88
|
+
const links = await listLinks('foo');
|
|
89
|
+
expect(links).toHaveLength(1);
|
|
90
|
+
expect(links[0]?.kind).toBe('copy');
|
|
91
|
+
expect(links[0]?.to).toBe(dest);
|
|
92
|
+
});
|
|
93
|
+
it('writes a manifest entry per fan-out target', async () => {
|
|
94
|
+
await seedSkill('multi');
|
|
95
|
+
const { addLink, getLinkManifestPath, loadManifest } = await import('@skillsmith/core/install');
|
|
96
|
+
await addLink({ skillId: 'multi', fromClient: 'claude-code', toClient: 'cursor' });
|
|
97
|
+
await addLink({ skillId: 'multi', fromClient: 'claude-code', toClient: 'agents' });
|
|
98
|
+
const manifest = await loadManifest();
|
|
99
|
+
expect(manifest.links.filter((l) => l.skillId === 'multi')).toHaveLength(2);
|
|
100
|
+
expect(manifest.version).toBe(1);
|
|
101
|
+
// manifest path is under ~/.skillsmith/links/manifest.json
|
|
102
|
+
const manifestPath = getLinkManifestPath();
|
|
103
|
+
expect(manifestPath).toBe(path.join(homeDir, '.skillsmith', 'links', 'manifest.json'));
|
|
104
|
+
await expect(stat(manifestPath)).resolves.toBeDefined();
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
describe('--also-link --symlink (POSIX opt-in)', () => {
|
|
108
|
+
it('creates a relative symlink instead of a copy', async () => {
|
|
109
|
+
await seedSkill('linkme');
|
|
110
|
+
const { addLink } = await import('@skillsmith/core/install');
|
|
111
|
+
const result = await addLink({
|
|
112
|
+
skillId: 'linkme',
|
|
113
|
+
fromClient: 'claude-code',
|
|
114
|
+
toClient: 'windsurf',
|
|
115
|
+
preferSymlink: true,
|
|
116
|
+
});
|
|
117
|
+
expect(result.record.kind).toBe('symlink');
|
|
118
|
+
const dest = path.join(homeDir, '.codeium', 'windsurf', 'skills', 'linkme');
|
|
119
|
+
const linkStat = await lstat(dest);
|
|
120
|
+
expect(linkStat.isSymbolicLink()).toBe(true);
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
describe('conflict policy', () => {
|
|
124
|
+
it('refuses to overwrite a pre-existing destination without force', async () => {
|
|
125
|
+
await seedSkill('clash');
|
|
126
|
+
// Pre-create destination with different content
|
|
127
|
+
const destDir = path.join(homeDir, '.cursor', 'skills', 'clash');
|
|
128
|
+
await mkdir(destDir, { recursive: true });
|
|
129
|
+
await writeFile(path.join(destDir, 'SKILL.md'), '# DIFFERENT\n', 'utf-8');
|
|
130
|
+
const { addLink } = await import('@skillsmith/core/install');
|
|
131
|
+
await expect(addLink({ skillId: 'clash', fromClient: 'claude-code', toClient: 'cursor' })).rejects.toThrow(/already exists/);
|
|
132
|
+
// Destination contents unchanged
|
|
133
|
+
const after = await readFile(path.join(destDir, 'SKILL.md'), 'utf-8');
|
|
134
|
+
expect(after).toBe('# DIFFERENT\n');
|
|
135
|
+
});
|
|
136
|
+
it('overwrites when force=true', async () => {
|
|
137
|
+
await seedSkill('clash');
|
|
138
|
+
const destDir = path.join(homeDir, '.cursor', 'skills', 'clash');
|
|
139
|
+
await mkdir(destDir, { recursive: true });
|
|
140
|
+
await writeFile(path.join(destDir, 'SKILL.md'), '# OLD\n', 'utf-8');
|
|
141
|
+
const { addLink } = await import('@skillsmith/core/install');
|
|
142
|
+
await addLink({
|
|
143
|
+
skillId: 'clash',
|
|
144
|
+
fromClient: 'claude-code',
|
|
145
|
+
toClient: 'cursor',
|
|
146
|
+
force: true,
|
|
147
|
+
});
|
|
148
|
+
const after = await readFile(path.join(destDir, 'SKILL.md'), 'utf-8');
|
|
149
|
+
expect(after).toContain('# test'); // matches seedSkill default body
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
describe('cycle detection', () => {
|
|
153
|
+
it('refuses A→B when an existing B→A entry would form a cycle', async () => {
|
|
154
|
+
await seedSkill('cycle');
|
|
155
|
+
const agentsDir = path.join(homeDir, '.agents', 'skills', 'cycle');
|
|
156
|
+
await mkdir(agentsDir, { recursive: true });
|
|
157
|
+
await writeFile(path.join(agentsDir, 'SKILL.md'), '# cycle\n', 'utf-8');
|
|
158
|
+
const { addLink } = await import('@skillsmith/core/install');
|
|
159
|
+
// Forward: agents → claude-code
|
|
160
|
+
await addLink({
|
|
161
|
+
skillId: 'cycle',
|
|
162
|
+
fromClient: 'agents',
|
|
163
|
+
toClient: 'claude-code',
|
|
164
|
+
force: true,
|
|
165
|
+
});
|
|
166
|
+
// Reverse: claude-code → agents — should refuse
|
|
167
|
+
await expect(addLink({
|
|
168
|
+
skillId: 'cycle',
|
|
169
|
+
fromClient: 'claude-code',
|
|
170
|
+
toClient: 'agents',
|
|
171
|
+
force: true,
|
|
172
|
+
})).rejects.toThrow(/cycle detected/);
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
describe('uninstall fan-out cleanup', () => {
|
|
176
|
+
it('removeLinks tears down both copies and symlinks', async () => {
|
|
177
|
+
await seedSkill('teardown');
|
|
178
|
+
const { addLink, removeLinks, listLinks } = await import('@skillsmith/core/install');
|
|
179
|
+
await addLink({ skillId: 'teardown', fromClient: 'claude-code', toClient: 'cursor' });
|
|
180
|
+
await addLink({
|
|
181
|
+
skillId: 'teardown',
|
|
182
|
+
fromClient: 'claude-code',
|
|
183
|
+
toClient: 'windsurf',
|
|
184
|
+
preferSymlink: true,
|
|
185
|
+
});
|
|
186
|
+
const removed = await removeLinks('teardown');
|
|
187
|
+
expect(removed).toBe(2);
|
|
188
|
+
expect(await listLinks('teardown')).toEqual([]);
|
|
189
|
+
// Both destinations gone
|
|
190
|
+
await expect(stat(path.join(homeDir, '.cursor', 'skills', 'teardown'))).rejects.toThrow(/ENOENT/);
|
|
191
|
+
await expect(lstat(path.join(homeDir, '.codeium', 'windsurf', 'skills', 'teardown'))).rejects.toThrow(/ENOENT/);
|
|
192
|
+
// Source untouched
|
|
193
|
+
await expect(stat(path.join(homeDir, '.claude', 'skills', 'teardown'))).resolves.toBeDefined();
|
|
194
|
+
});
|
|
195
|
+
it('removeLinks returns 0 when no manifest exists', async () => {
|
|
196
|
+
const { removeLinks } = await import('@skillsmith/core/install');
|
|
197
|
+
const removed = await removeLinks('nothing-installed');
|
|
198
|
+
expect(removed).toBe(0);
|
|
199
|
+
});
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
//# sourceMappingURL=install-multi-client.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"install-multi-client.test.js","sourceRoot":"","sources":["../../tests/install-multi-client.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,QAAQ,EAAE,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAA;AACvF,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAA;AAChC,OAAO,KAAK,IAAI,MAAM,WAAW,CAAA;AACjC,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAA;AAExE,MAAM,aAAa,GAAG,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,CAAA;AACzC,MAAM,oBAAoB,GAAG,OAAO,CAAC,GAAG,CAAC,aAAa,CAAC,CAAA;AACvD,MAAM,eAAe,GAAG,OAAO,CAAC,GAAG,CAAC,mBAAmB,CAAC,CAAA;AAExD,IAAI,OAAe,CAAA;AAEnB,UAAU,CAAC,KAAK,IAAI,EAAE;IACpB,OAAO,GAAG,MAAM,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,EAAE,gBAAgB,CAAC,CAAC,CAAA;IAC9D,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,GAAG,OAAO,CAAA;IAC7B,OAAO,CAAC,GAAG,CAAC,aAAa,CAAC,GAAG,OAAO,CAAA;IACpC,OAAO,OAAO,CAAC,GAAG,CAAC,mBAAmB,CAAC,CAAA;IACvC,iEAAiE;IACjE,iDAAiD;IACjD,EAAE,CAAC,YAAY,EAAE,CAAA;AACnB,CAAC,CAAC,CAAA;AAEF,SAAS,CAAC,KAAK,IAAI,EAAE;IACnB,IAAI,aAAa,KAAK,SAAS;QAAE,OAAO,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,CAAA;;QACtD,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,GAAG,aAAa,CAAA;IACxC,IAAI,oBAAoB,KAAK,SAAS;QAAE,OAAO,OAAO,CAAC,GAAG,CAAC,aAAa,CAAC,CAAA;;QACpE,OAAO,CAAC,GAAG,CAAC,aAAa,CAAC,GAAG,oBAAoB,CAAA;IACtD,IAAI,eAAe,KAAK,SAAS;QAAE,OAAO,OAAO,CAAC,GAAG,CAAC,mBAAmB,CAAC,CAAA;;QACrE,OAAO,CAAC,GAAG,CAAC,mBAAmB,CAAC,GAAG,eAAe,CAAA;IACvD,MAAM,EAAE,CAAC,OAAO,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAA;AACrD,CAAC,CAAC,CAAA;AAEF,KAAK,UAAU,SAAS,CAAC,OAAe,EAAE,OAAe,UAAU;IACjE,MAAM,GAAG,GAAG,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,OAAO,CAAC,CAAA;IAC5D,MAAM,KAAK,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAA;IACrC,MAAM,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,UAAU,CAAC,EAAE,cAAc,OAAO,UAAU,IAAI,EAAE,EAAE,OAAO,CAAC,CAAA;IAC3F,OAAO,GAAG,CAAA;AACZ,CAAC;AAED,QAAQ,CAAC,gCAAgC,EAAE,GAAG,EAAE;IAC9C,QAAQ,CAAC,2BAA2B,EAAE,GAAG,EAAE;QACzC,EAAE,CAAC,iEAAiE,EAAE,KAAK,IAAI,EAAE;YAC/E,MAAM,EAAE,cAAc,EAAE,GAAG,MAAM,MAAM,CAAC,0BAA0B,CAAC,CAAA;YACnE,MAAM,CAAC,cAAc,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,SAAS,EAAE,QAAQ,CAAC,CAAC,CAAA;QAChF,CAAC,CAAC,CAAA;QAEF,EAAE,CAAC,oDAAoD,EAAE,KAAK,IAAI,EAAE;YAClE,OAAO,CAAC,GAAG,CAAC,mBAAmB,CAAC,GAAG,UAAU,CAAA;YAC7C,MAAM,EAAE,iBAAiB,EAAE,GAAG,MAAM,MAAM,CAAC,0BAA0B,CAAC,CAAA;YACtE,MAAM,CAAC,iBAAiB,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,QAAQ,CAAC,CAAC,CAAA;QACxF,CAAC,CAAC,CAAA;QAEF,EAAE,CAAC,iCAAiC,EAAE,KAAK,IAAI,EAAE;YAC/C,OAAO,CAAC,GAAG,CAAC,mBAAmB,CAAC,GAAG,QAAQ,CAAA;YAC3C,MAAM,EAAE,iBAAiB,EAAE,GAAG,MAAM,MAAM,CAAC,0BAA0B,CAAC,CAAA;YACtE,MAAM,CAAC,iBAAiB,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,SAAS,EAAE,QAAQ,CAAC,CAAC,CAAA;QACnF,CAAC,CAAC,CAAA;QAEF,EAAE,CAAC,wDAAwD,EAAE,KAAK,IAAI,EAAE;YACtE,OAAO,CAAC,GAAG,CAAC,mBAAmB,CAAC,GAAG,OAAO,CAAA;YAC1C,MAAM,EAAE,iBAAiB,EAAE,GAAG,MAAM,MAAM,CAAC,0BAA0B,CAAC,CAAA;YACtE,MAAM,CAAC,GAAG,EAAE,CAAC,iBAAiB,EAAE,CAAC,CAAC,OAAO,CAAC,iBAAiB,CAAC,CAAA;QAC9D,CAAC,CAAC,CAAA;IACJ,CAAC,CAAC,CAAA;IAEF,QAAQ,CAAC,0BAA0B,EAAE,GAAG,EAAE;QACxC,EAAE,CAAC,2DAA2D,EAAE,KAAK,IAAI,EAAE;YACzE,MAAM,SAAS,CAAC,KAAK,EAAE,SAAS,CAAC,CAAA;YACjC,MAAM,EAAE,OAAO,EAAE,SAAS,EAAE,GAAG,MAAM,MAAM,CAAC,0BAA0B,CAAC,CAAA;YAEvE,MAAM,MAAM,GAAG,MAAM,OAAO,CAAC;gBAC3B,OAAO,EAAE,KAAK;gBACd,UAAU,EAAE,aAAa;gBACzB,QAAQ,EAAE,QAAQ;aACnB,CAAC,CAAA;YAEF,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAA;YACvC,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,KAAK,CAAC,CAAA;YAC3D,MAAM,OAAO,GAAG,MAAM,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,UAAU,CAAC,EAAE,OAAO,CAAC,CAAA;YACpE,MAAM,CAAC,OAAO,CAAC,CAAC,SAAS,CAAC,OAAO,CAAC,CAAA;YAElC,MAAM,KAAK,GAAG,MAAM,SAAS,CAAC,KAAK,CAAC,CAAA;YACpC,MAAM,CAAC,KAAK,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,CAAA;YAC7B,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAA;YACnC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;QACjC,CAAC,CAAC,CAAA;QAEF,EAAE,CAAC,4CAA4C,EAAE,KAAK,IAAI,EAAE;YAC1D,MAAM,SAAS,CAAC,OAAO,CAAC,CAAA;YACxB,MAAM,EAAE,OAAO,EAAE,mBAAmB,EAAE,YAAY,EAAE,GAClD,MAAM,MAAM,CAAC,0BAA0B,CAAC,CAAA;YAE1C,MAAM,OAAO,CAAC,EAAE,OAAO,EAAE,OAAO,EAAE,UAAU,EAAE,aAAa,EAAE,QAAQ,EAAE,QAAQ,EAAE,CAAC,CAAA;YAClF,MAAM,OAAO,CAAC,EAAE,OAAO,EAAE,OAAO,EAAE,UAAU,EAAE,aAAa,EAAE,QAAQ,EAAE,QAAQ,EAAE,CAAC,CAAA;YAElF,MAAM,QAAQ,GAAG,MAAM,YAAY,EAAE,CAAA;YACrC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,KAAK,OAAO,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,CAAA;YAC3E,MAAM,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;YAEhC,2DAA2D;YAC3D,MAAM,YAAY,GAAG,mBAAmB,EAAE,CAAA;YAC1C,MAAM,CAAC,YAAY,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,aAAa,EAAE,OAAO,EAAE,eAAe,CAAC,CAAC,CAAA;YACtF,MAAM,MAAM,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC,CAAC,QAAQ,CAAC,WAAW,EAAE,CAAA;QACzD,CAAC,CAAC,CAAA;IACJ,CAAC,CAAC,CAAA;IAEF,QAAQ,CAAC,sCAAsC,EAAE,GAAG,EAAE;QACpD,EAAE,CAAC,8CAA8C,EAAE,KAAK,IAAI,EAAE;YAC5D,MAAM,SAAS,CAAC,QAAQ,CAAC,CAAA;YACzB,MAAM,EAAE,OAAO,EAAE,GAAG,MAAM,MAAM,CAAC,0BAA0B,CAAC,CAAA;YAE5D,MAAM,MAAM,GAAG,MAAM,OAAO,CAAC;gBAC3B,OAAO,EAAE,QAAQ;gBACjB,UAAU,EAAE,aAAa;gBACzB,QAAQ,EAAE,UAAU;gBACpB,aAAa,EAAE,IAAI;aACpB,CAAC,CAAA;YAEF,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,CAAA;YAC1C,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,QAAQ,EAAE,QAAQ,CAAC,CAAA;YAC3E,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,IAAI,CAAC,CAAA;YAClC,MAAM,CAAC,QAAQ,CAAC,cAAc,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;QAC9C,CAAC,CAAC,CAAA;IACJ,CAAC,CAAC,CAAA;IAEF,QAAQ,CAAC,iBAAiB,EAAE,GAAG,EAAE;QAC/B,EAAE,CAAC,+DAA+D,EAAE,KAAK,IAAI,EAAE;YAC7E,MAAM,SAAS,CAAC,OAAO,CAAC,CAAA;YACxB,gDAAgD;YAChD,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,OAAO,CAAC,CAAA;YAChE,MAAM,KAAK,CAAC,OAAO,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAA;YACzC,MAAM,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,UAAU,CAAC,EAAE,eAAe,EAAE,OAAO,CAAC,CAAA;YAEzE,MAAM,EAAE,OAAO,EAAE,GAAG,MAAM,MAAM,CAAC,0BAA0B,CAAC,CAAA;YAC5D,MAAM,MAAM,CACV,OAAO,CAAC,EAAE,OAAO,EAAE,OAAO,EAAE,UAAU,EAAE,aAAa,EAAE,QAAQ,EAAE,QAAQ,EAAE,CAAC,CAC7E,CAAC,OAAO,CAAC,OAAO,CAAC,gBAAgB,CAAC,CAAA;YAEnC,iCAAiC;YACjC,MAAM,KAAK,GAAG,MAAM,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,UAAU,CAAC,EAAE,OAAO,CAAC,CAAA;YACrE,MAAM,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,eAAe,CAAC,CAAA;QACrC,CAAC,CAAC,CAAA;QAEF,EAAE,CAAC,4BAA4B,EAAE,KAAK,IAAI,EAAE;YAC1C,MAAM,SAAS,CAAC,OAAO,CAAC,CAAA;YACxB,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,OAAO,CAAC,CAAA;YAChE,MAAM,KAAK,CAAC,OAAO,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAA;YACzC,MAAM,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,UAAU,CAAC,EAAE,SAAS,EAAE,OAAO,CAAC,CAAA;YAEnE,MAAM,EAAE,OAAO,EAAE,GAAG,MAAM,MAAM,CAAC,0BAA0B,CAAC,CAAA;YAC5D,MAAM,OAAO,CAAC;gBACZ,OAAO,EAAE,OAAO;gBAChB,UAAU,EAAE,aAAa;gBACzB,QAAQ,EAAE,QAAQ;gBAClB,KAAK,EAAE,IAAI;aACZ,CAAC,CAAA;YACF,MAAM,KAAK,GAAG,MAAM,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,UAAU,CAAC,EAAE,OAAO,CAAC,CAAA;YACrE,MAAM,CAAC,KAAK,CAAC,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAA,CAAC,iCAAiC;QACrE,CAAC,CAAC,CAAA;IACJ,CAAC,CAAC,CAAA;IAEF,QAAQ,CAAC,iBAAiB,EAAE,GAAG,EAAE;QAC/B,EAAE,CAAC,2DAA2D,EAAE,KAAK,IAAI,EAAE;YACzE,MAAM,SAAS,CAAC,OAAO,CAAC,CAAA;YACxB,MAAM,SAAS,GAAG,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,OAAO,CAAC,CAAA;YAClE,MAAM,KAAK,CAAC,SAAS,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAA;YAC3C,MAAM,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,UAAU,CAAC,EAAE,WAAW,EAAE,OAAO,CAAC,CAAA;YAEvE,MAAM,EAAE,OAAO,EAAE,GAAG,MAAM,MAAM,CAAC,0BAA0B,CAAC,CAAA;YAC5D,gCAAgC;YAChC,MAAM,OAAO,CAAC;gBACZ,OAAO,EAAE,OAAO;gBAChB,UAAU,EAAE,QAAQ;gBACpB,QAAQ,EAAE,aAAa;gBACvB,KAAK,EAAE,IAAI;aACZ,CAAC,CAAA;YACF,gDAAgD;YAChD,MAAM,MAAM,CACV,OAAO,CAAC;gBACN,OAAO,EAAE,OAAO;gBAChB,UAAU,EAAE,aAAa;gBACzB,QAAQ,EAAE,QAAQ;gBAClB,KAAK,EAAE,IAAI;aACZ,CAAC,CACH,CAAC,OAAO,CAAC,OAAO,CAAC,gBAAgB,CAAC,CAAA;QACrC,CAAC,CAAC,CAAA;IACJ,CAAC,CAAC,CAAA;IAEF,QAAQ,CAAC,2BAA2B,EAAE,GAAG,EAAE;QACzC,EAAE,CAAC,iDAAiD,EAAE,KAAK,IAAI,EAAE;YAC/D,MAAM,SAAS,CAAC,UAAU,CAAC,CAAA;YAC3B,MAAM,EAAE,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,GAAG,MAAM,MAAM,CAAC,0BAA0B,CAAC,CAAA;YAEpF,MAAM,OAAO,CAAC,EAAE,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,aAAa,EAAE,QAAQ,EAAE,QAAQ,EAAE,CAAC,CAAA;YACrF,MAAM,OAAO,CAAC;gBACZ,OAAO,EAAE,UAAU;gBACnB,UAAU,EAAE,aAAa;gBACzB,QAAQ,EAAE,UAAU;gBACpB,aAAa,EAAE,IAAI;aACpB,CAAC,CAAA;YAEF,MAAM,OAAO,GAAG,MAAM,WAAW,CAAC,UAAU,CAAC,CAAA;YAC7C,MAAM,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;YACvB,MAAM,CAAC,MAAM,SAAS,CAAC,UAAU,CAAC,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC,CAAA;YAE/C,yBAAyB;YACzB,MAAM,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,UAAU,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,OAAO,CACrF,QAAQ,CACT,CAAA;YACD,MAAM,MAAM,CACV,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,QAAQ,EAAE,UAAU,CAAC,CAAC,CACxE,CAAC,OAAO,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAA;YAE3B,mBAAmB;YACnB,MAAM,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,UAAU,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,WAAW,EAAE,CAAA;QAChG,CAAC,CAAC,CAAA;QAEF,EAAE,CAAC,+CAA+C,EAAE,KAAK,IAAI,EAAE;YAC7D,MAAM,EAAE,WAAW,EAAE,GAAG,MAAM,MAAM,CAAC,0BAA0B,CAAC,CAAA;YAChE,MAAM,OAAO,GAAG,MAAM,WAAW,CAAC,mBAAmB,CAAC,CAAA;YACtD,MAAM,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;QACzB,CAAC,CAAC,CAAA;IACJ,CAAC,CAAC,CAAA;AACJ,CAAC,CAAC,CAAA"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"import-local.test.d.ts","sourceRoot":"","sources":["../../../tests/integration/import-local.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Integration tests for `skillsmith import-local` (SMI-4665)
|
|
3
|
+
*
|
|
4
|
+
* Exercises the action handler against a real SQLite DB on a tmpdir fixture.
|
|
5
|
+
* Round-trip:
|
|
6
|
+
* 1. Import — fixture skill lands in DB with source='local', trust_tier='local'
|
|
7
|
+
* 2. Re-import is idempotent — same id, no duplicates
|
|
8
|
+
* 3. SyncEngine.upsertSkills with force=true does NOT clobber the local row
|
|
9
|
+
*/
|
|
10
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
11
|
+
import { promises as fs } from 'node:fs';
|
|
12
|
+
import { join } from 'node:path';
|
|
13
|
+
import { tmpdir } from 'node:os';
|
|
14
|
+
import { createDatabaseAsync, initializeSchema, SkillRepository, SyncEngine, SyncConfigRepository, SyncHistoryRepository, } from '@skillsmith/core';
|
|
15
|
+
import { runImportLocal } from '../../src/commands/import-local.js';
|
|
16
|
+
import { vi } from 'vitest';
|
|
17
|
+
let workDir;
|
|
18
|
+
let dbPath;
|
|
19
|
+
beforeEach(async () => {
|
|
20
|
+
workDir = await fs.mkdtemp(join(tmpdir(), 'skillsmith-il-'));
|
|
21
|
+
dbPath = join(workDir, 'skills.db');
|
|
22
|
+
});
|
|
23
|
+
afterEach(async () => {
|
|
24
|
+
await fs.rm(workDir, { recursive: true, force: true });
|
|
25
|
+
});
|
|
26
|
+
async function writeSkill(name) {
|
|
27
|
+
const dir = join(workDir, 'skills', name);
|
|
28
|
+
await fs.mkdir(dir, { recursive: true });
|
|
29
|
+
const file = join(dir, 'SKILL.md');
|
|
30
|
+
await fs.writeFile(file, `---\nname: ${name}\ndescription: A test skill named ${name}\ntags:\n - test\n---\n# ${name}\n\nBody.\n`);
|
|
31
|
+
return file;
|
|
32
|
+
}
|
|
33
|
+
describe('skillsmith import-local — round-trip', () => {
|
|
34
|
+
it('imports SKILL.md files into the DB with source=local', async () => {
|
|
35
|
+
await writeSkill('alpha');
|
|
36
|
+
await writeSkill('beta');
|
|
37
|
+
const result = await runImportLocal({
|
|
38
|
+
path: join(workDir, 'skills'),
|
|
39
|
+
dbPath,
|
|
40
|
+
});
|
|
41
|
+
expect(result.errors).toEqual([]);
|
|
42
|
+
expect(result.imported).toBe(2);
|
|
43
|
+
expect(result.scanned).toBe(2);
|
|
44
|
+
const db = await createDatabaseAsync(dbPath);
|
|
45
|
+
initializeSchema(db);
|
|
46
|
+
try {
|
|
47
|
+
const repo = new SkillRepository(db);
|
|
48
|
+
const all = repo.findAll({ limit: 100 });
|
|
49
|
+
expect(all.items).toHaveLength(2);
|
|
50
|
+
for (const skill of all.items) {
|
|
51
|
+
expect(skill.source).toBe('local');
|
|
52
|
+
expect(skill.trustTier).toBe('local');
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
finally {
|
|
56
|
+
db.close();
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
it('re-importing the same directory is idempotent (same id, no duplicates)', async () => {
|
|
60
|
+
await writeSkill('gamma');
|
|
61
|
+
const first = await runImportLocal({ path: join(workDir, 'skills'), dbPath });
|
|
62
|
+
expect(first.imported).toBe(1);
|
|
63
|
+
expect(first.updated).toBe(0);
|
|
64
|
+
const second = await runImportLocal({ path: join(workDir, 'skills'), dbPath });
|
|
65
|
+
expect(second.imported).toBe(0);
|
|
66
|
+
expect(second.updated).toBe(1); // path is the same, id matches → update path
|
|
67
|
+
const db = await createDatabaseAsync(dbPath);
|
|
68
|
+
initializeSchema(db);
|
|
69
|
+
try {
|
|
70
|
+
const repo = new SkillRepository(db);
|
|
71
|
+
expect(repo.count()).toBe(1);
|
|
72
|
+
}
|
|
73
|
+
finally {
|
|
74
|
+
db.close();
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
it('sync --force does NOT clobber locally-imported rows', async () => {
|
|
78
|
+
const skillFile = await writeSkill('delta');
|
|
79
|
+
await runImportLocal({ path: join(workDir, 'skills'), dbPath });
|
|
80
|
+
const db = await createDatabaseAsync(dbPath);
|
|
81
|
+
try {
|
|
82
|
+
const skillRepo = new SkillRepository(db);
|
|
83
|
+
const localSkills = skillRepo.findAll({ limit: 10 }).items;
|
|
84
|
+
expect(localSkills).toHaveLength(1);
|
|
85
|
+
const local = localSkills[0];
|
|
86
|
+
// Build a SyncEngine that returns a colliding registry skill.
|
|
87
|
+
const apiClient = {
|
|
88
|
+
isOffline: vi.fn().mockReturnValue(false),
|
|
89
|
+
checkHealth: vi.fn().mockResolvedValue({ status: 'healthy' }),
|
|
90
|
+
search: vi.fn().mockResolvedValue({
|
|
91
|
+
data: [
|
|
92
|
+
{
|
|
93
|
+
id: local.id,
|
|
94
|
+
name: 'CLOBBERED REGISTRY NAME',
|
|
95
|
+
description: 'should not appear',
|
|
96
|
+
author: 'reg',
|
|
97
|
+
repo_url: 'https://example.com/registry-collision',
|
|
98
|
+
quality_score: 0.5,
|
|
99
|
+
trust_tier: 'community',
|
|
100
|
+
tags: [],
|
|
101
|
+
stars: 0,
|
|
102
|
+
installable: true,
|
|
103
|
+
created_at: new Date().toISOString(),
|
|
104
|
+
updated_at: new Date(Date.now() + 60_000).toISOString(),
|
|
105
|
+
},
|
|
106
|
+
],
|
|
107
|
+
total: 1,
|
|
108
|
+
}),
|
|
109
|
+
getSkill: vi.fn(),
|
|
110
|
+
getHealthStatus: vi.fn(),
|
|
111
|
+
};
|
|
112
|
+
const syncConfigRepo = new SyncConfigRepository(db);
|
|
113
|
+
const syncHistoryRepo = new SyncHistoryRepository(db);
|
|
114
|
+
const skillVersionRepo = {
|
|
115
|
+
recordVersion: vi.fn().mockResolvedValue(undefined),
|
|
116
|
+
pruneVersions: vi.fn().mockResolvedValue(undefined),
|
|
117
|
+
getLatestVersion: vi.fn().mockResolvedValue(null),
|
|
118
|
+
getVersionHistory: vi.fn().mockResolvedValue([]),
|
|
119
|
+
getVersionByHash: vi.fn().mockResolvedValue(null),
|
|
120
|
+
};
|
|
121
|
+
const engine = new SyncEngine(apiClient, skillRepo, syncConfigRepo, syncHistoryRepo, skillVersionRepo);
|
|
122
|
+
const result = await engine.sync({ force: true });
|
|
123
|
+
expect(result.success).toBe(true);
|
|
124
|
+
expect(result.skillsUpdated).toBe(0);
|
|
125
|
+
const after = skillRepo.findById(local.id);
|
|
126
|
+
expect(after).not.toBeNull();
|
|
127
|
+
expect(after.name).toBe('delta');
|
|
128
|
+
expect(after.source).toBe('local');
|
|
129
|
+
}
|
|
130
|
+
finally {
|
|
131
|
+
db.close();
|
|
132
|
+
}
|
|
133
|
+
// Sanity: the fixture file is still there (we didn't accidentally delete it).
|
|
134
|
+
await fs.access(skillFile);
|
|
135
|
+
});
|
|
136
|
+
it('--dry-run does not mutate the DB', async () => {
|
|
137
|
+
await writeSkill('echo');
|
|
138
|
+
const result = await runImportLocal({
|
|
139
|
+
path: join(workDir, 'skills'),
|
|
140
|
+
dbPath,
|
|
141
|
+
dryRun: true,
|
|
142
|
+
});
|
|
143
|
+
expect(result.dryRun).toBe(true);
|
|
144
|
+
expect(result.imported).toBe(1);
|
|
145
|
+
// The dry-run pass opens the DB only to read; it still initializes the
|
|
146
|
+
// schema so subsequent reads work. Verify no skill rows landed.
|
|
147
|
+
const db = await createDatabaseAsync(dbPath);
|
|
148
|
+
initializeSchema(db);
|
|
149
|
+
try {
|
|
150
|
+
const repo = new SkillRepository(db);
|
|
151
|
+
expect(repo.count()).toBe(0);
|
|
152
|
+
}
|
|
153
|
+
finally {
|
|
154
|
+
db.close();
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
//# sourceMappingURL=import-local.test.js.map
|