@operor/skills 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,162 @@
1
+ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
2
+ import {
3
+ resolveEnvVars,
4
+ resolveEnvRecord,
5
+ loadSkillsConfig,
6
+ saveSkillsConfig,
7
+ } from '../src/config.js';
8
+ import type { SkillsConfig } from '../src/config.js';
9
+ import { mkdtempSync, rmSync, readFileSync, writeFileSync } from 'node:fs';
10
+ import { tmpdir } from 'node:os';
11
+ import { join } from 'node:path';
12
+
13
+ // ─── resolveEnvVars ──────────────────────────────────────────────────────────
14
+
15
+ describe('resolveEnvVars', () => {
16
+ beforeEach(() => {
17
+ vi.stubEnv('TEST_KEY', 'hello');
18
+ vi.stubEnv('OTHER_KEY', 'world');
19
+ });
20
+
21
+ afterEach(() => {
22
+ vi.unstubAllEnvs();
23
+ });
24
+
25
+ it('should resolve a single env var', () => {
26
+ expect(resolveEnvVars('${TEST_KEY}')).toBe('hello');
27
+ });
28
+
29
+ it('should resolve multiple env vars', () => {
30
+ expect(resolveEnvVars('${TEST_KEY}-${OTHER_KEY}')).toBe('hello-world');
31
+ });
32
+
33
+ it('should return empty string for missing env var', () => {
34
+ expect(resolveEnvVars('${MISSING_VAR}')).toBe('');
35
+ });
36
+
37
+ it('should leave plain strings unchanged', () => {
38
+ expect(resolveEnvVars('no-vars-here')).toBe('no-vars-here');
39
+ });
40
+ });
41
+
42
+ describe('resolveEnvRecord', () => {
43
+ beforeEach(() => {
44
+ vi.stubEnv('API_KEY', 'sk_test_123');
45
+ });
46
+
47
+ afterEach(() => {
48
+ vi.unstubAllEnvs();
49
+ });
50
+
51
+ it('should resolve all values in a record', () => {
52
+ const result = resolveEnvRecord({
53
+ KEY: '${API_KEY}',
54
+ PLAIN: 'literal',
55
+ });
56
+ expect(result).toEqual({
57
+ KEY: 'sk_test_123',
58
+ PLAIN: 'literal',
59
+ });
60
+ });
61
+ });
62
+
63
+ // ─── loadSkillsConfig / saveSkillsConfig ─────────────────────────────────────
64
+
65
+ describe('loadSkillsConfig', () => {
66
+ let tmpDir: string;
67
+
68
+ beforeEach(() => {
69
+ tmpDir = mkdtempSync(join(tmpdir(), 'skills-config-'));
70
+ });
71
+
72
+ afterEach(() => {
73
+ rmSync(tmpDir, { recursive: true, force: true });
74
+ });
75
+
76
+ it('should return empty config when mcp.json does not exist', () => {
77
+ const config = loadSkillsConfig(tmpDir);
78
+ expect(config.skills).toEqual([]);
79
+ });
80
+
81
+ it('should load skills from mcp.json', () => {
82
+ const data: SkillsConfig = {
83
+ skills: [
84
+ {
85
+ name: 'shopify',
86
+ transport: 'stdio',
87
+ command: 'npx',
88
+ args: ['-y', 'shopify-mcp'],
89
+ env: { SHOPIFY_TOKEN: '${SHOPIFY_TOKEN}' },
90
+ enabled: true,
91
+ },
92
+ ],
93
+ };
94
+ writeFileSync(join(tmpDir, 'mcp.json'), JSON.stringify(data));
95
+
96
+ const config = loadSkillsConfig(tmpDir);
97
+ expect(config.skills).toHaveLength(1);
98
+ expect(config.skills[0].name).toBe('shopify');
99
+ expect(config.skills[0].transport).toBe('stdio');
100
+ });
101
+
102
+ it('should throw on invalid JSON', () => {
103
+ writeFileSync(join(tmpDir, 'mcp.json'), 'not json{{{');
104
+ expect(() => loadSkillsConfig(tmpDir)).toThrow('Invalid JSON');
105
+ });
106
+
107
+ it('should return empty skills for non-object JSON', () => {
108
+ writeFileSync(join(tmpDir, 'mcp.json'), '"just a string"');
109
+ const config = loadSkillsConfig(tmpDir);
110
+ expect(config.skills).toEqual([]);
111
+ });
112
+
113
+ it('should return empty skills when skills key is missing', () => {
114
+ writeFileSync(join(tmpDir, 'mcp.json'), '{}');
115
+ const config = loadSkillsConfig(tmpDir);
116
+ expect(config.skills).toEqual([]);
117
+ });
118
+ });
119
+
120
+ describe('saveSkillsConfig', () => {
121
+ let tmpDir: string;
122
+
123
+ beforeEach(() => {
124
+ tmpDir = mkdtempSync(join(tmpdir(), 'skills-config-'));
125
+ });
126
+
127
+ afterEach(() => {
128
+ rmSync(tmpDir, { recursive: true, force: true });
129
+ });
130
+
131
+ it('should write mcp.json', () => {
132
+ const config: SkillsConfig = {
133
+ skills: [
134
+ { name: 'stripe', transport: 'stdio', command: 'npx', args: ['-y', '@stripe/mcp'] },
135
+ ],
136
+ };
137
+
138
+ saveSkillsConfig(config, tmpDir);
139
+
140
+ const raw = readFileSync(join(tmpDir, 'mcp.json'), 'utf-8');
141
+ const parsed = JSON.parse(raw);
142
+ expect(parsed.skills).toHaveLength(1);
143
+ expect(parsed.skills[0].name).toBe('stripe');
144
+ });
145
+
146
+ it('should be round-trippable with loadSkillsConfig', () => {
147
+ const config: SkillsConfig = {
148
+ skills: [
149
+ { name: 'a', transport: 'stdio', command: 'npx', args: ['-y', 'a-mcp'], enabled: true },
150
+ { name: 'b', transport: 'http', url: 'https://b.example.com/mcp', enabled: false },
151
+ ],
152
+ };
153
+
154
+ saveSkillsConfig(config, tmpDir);
155
+ const loaded = loadSkillsConfig(tmpDir);
156
+
157
+ expect(loaded.skills).toHaveLength(2);
158
+ expect(loaded.skills[0].name).toBe('a');
159
+ expect(loaded.skills[1].name).toBe('b');
160
+ expect(loaded.skills[1].url).toBe('https://b.example.com/mcp');
161
+ });
162
+ });
package/tsconfig.json ADDED
@@ -0,0 +1,9 @@
1
+ {
2
+ "extends": "../../tsconfig.json",
3
+ "compilerOptions": {
4
+ "outDir": "./dist",
5
+ "rootDir": "./src"
6
+ },
7
+ "include": ["src/**/*"],
8
+ "exclude": ["node_modules", "dist"]
9
+ }
@@ -0,0 +1,10 @@
1
+ import { defineConfig } from 'tsdown';
2
+
3
+ export default defineConfig({
4
+ entry: ['src/index.ts'],
5
+ format: ['esm'],
6
+ dts: true,
7
+ clean: true,
8
+ sourcemap: true,
9
+ outExtensions: () => ({ js: '.js', dts: '.d.ts' }),
10
+ });
@@ -0,0 +1,3 @@
1
+ import { defineConfig } from 'vitest/config';
2
+
3
+ export default defineConfig({});