expxagents 0.25.4 → 0.26.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/dist/cli/src/__tests__/cli.test.d.ts +1 -0
- package/dist/cli/src/__tests__/cli.test.js +23 -0
- package/dist/cli/src/commands/__tests__/outdated.test.d.ts +1 -0
- package/dist/cli/src/commands/__tests__/outdated.test.js +76 -0
- package/dist/cli/src/mcp/__tests__/catalog.test.d.ts +1 -0
- package/dist/cli/src/mcp/__tests__/catalog.test.js +101 -0
- package/dist/cli/src/mcp/__tests__/detect.test.d.ts +1 -0
- package/dist/cli/src/mcp/__tests__/detect.test.js +84 -0
- package/dist/cli/src/mcp/__tests__/setup.test.d.ts +1 -0
- package/dist/cli/src/mcp/__tests__/setup.test.js +75 -0
- package/dist/cli/src/mcp/__tests__/validate.test.d.ts +1 -0
- package/dist/cli/src/mcp/__tests__/validate.test.js +42 -0
- package/dist/cli/src/pencil/__tests__/detect.test.d.ts +1 -0
- package/dist/cli/src/pencil/__tests__/detect.test.js +71 -0
- package/dist/cli/src/pencil/__tests__/property-mapper.test.d.ts +1 -0
- package/dist/cli/src/pencil/__tests__/property-mapper.test.js +120 -0
- package/dist/cli/src/pencil/__tests__/template-sync.test.d.ts +1 -0
- package/dist/cli/src/pencil/__tests__/template-sync.test.js +95 -0
- package/dist/cli/src/runners/__tests__/provider-registry.test.d.ts +1 -0
- package/dist/cli/src/runners/__tests__/provider-registry.test.js +31 -0
- package/dist/cli/src/runners/__tests__/provider-runner.test.d.ts +1 -0
- package/dist/cli/src/runners/__tests__/provider-runner.test.js +86 -0
- package/dist/cli/src/utils/__tests__/command-prefix.test.d.ts +1 -0
- package/dist/cli/src/utils/__tests__/command-prefix.test.js +35 -0
- package/dist/cli/src/utils/__tests__/global-install.test.d.ts +1 -0
- package/dist/cli/src/utils/__tests__/global-install.test.js +25 -0
- package/dist/core/squad-loader.d.ts +1 -1
- package/dist/core/squad-loader.js +1 -1
- package/dist/dashboard/assets/{BufferResource-D79vaoFm.js → BufferResource-BJ1t-sf3.js} +1 -1
- package/dist/dashboard/assets/{CanvasRenderer-BUoxTNKV.js → CanvasRenderer-DAuw67YY.js} +1 -1
- package/dist/dashboard/assets/JarvisView-B3HjbQH2.js +1 -0
- package/dist/dashboard/assets/{RenderTargetSystem-B7rwTXA1.js → RenderTargetSystem-DncnoCMh.js} +1 -1
- package/dist/dashboard/assets/ThreeBackground-DuuQxD67.js +1 -0
- package/dist/dashboard/assets/{WebGLRenderer-DgdVNsZ9.js → WebGLRenderer-BwXONrs5.js} +1 -1
- package/dist/dashboard/assets/{WebGPURenderer-DnQNvjEQ.js → WebGPURenderer-CMwTQECM.js} +1 -1
- package/dist/dashboard/assets/{browserAll-Cbsk7DE4.js → browserAll-JZnlISmr.js} +1 -1
- package/dist/dashboard/assets/index-BvbW2SdX.js +1195 -0
- package/dist/dashboard/assets/{webworkerAll-BLmfReEj.js → webworkerAll-D2LBc_2J.js} +1 -1
- package/dist/dashboard/index.html +1 -1
- package/dist/data/opensquad.db +0 -0
- package/dist/data/opensquad.db-shm +0 -0
- package/dist/data/opensquad.db-wal +0 -0
- package/dist/server/bridge/engine.d.ts +2 -2
- package/dist/server/bridge/engine.d.ts.map +1 -1
- package/dist/server/bridge/factory.d.ts.map +1 -1
- package/dist/server/bridge/factory.js +2 -0
- package/dist/server/bridge/factory.js.map +1 -1
- package/dist/server/bridge/gemini-bridge.d.ts +21 -0
- package/dist/server/bridge/gemini-bridge.d.ts.map +1 -0
- package/dist/server/bridge/gemini-bridge.js +248 -0
- package/dist/server/bridge/gemini-bridge.js.map +1 -0
- package/dist/server/config/engine-config.d.ts +1 -1
- package/dist/server/config/engine-config.d.ts.map +1 -1
- package/dist/server/config/engine-config.js +2 -2
- package/dist/server/config/engine-config.js.map +1 -1
- package/package.json +1 -1
- package/dist/dashboard/assets/JarvisView-DSN7xWMz.js +0 -1
- package/dist/dashboard/assets/ThreeBackground-BQTdScX-.js +0 -1
- package/dist/dashboard/assets/index-CrlhoBta.js +0 -783
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { execSync } from 'child_process';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
describe('CLI', () => {
|
|
5
|
+
const cliPath = path.resolve(__dirname, '../../bin/expxagents.js');
|
|
6
|
+
it('shows help output with all commands', () => {
|
|
7
|
+
execSync('npm run build', { cwd: path.resolve(__dirname, '../..') });
|
|
8
|
+
const output = execSync(`node ${cliPath} --help`, { encoding: 'utf-8' });
|
|
9
|
+
expect(output).toContain('expxagents');
|
|
10
|
+
expect(output).toContain('init');
|
|
11
|
+
expect(output).toContain('create');
|
|
12
|
+
expect(output).toContain('run');
|
|
13
|
+
expect(output).toContain('stop');
|
|
14
|
+
expect(output).toContain('list');
|
|
15
|
+
expect(output).toContain('install');
|
|
16
|
+
expect(output).toContain('uninstall');
|
|
17
|
+
expect(output).toContain('server');
|
|
18
|
+
});
|
|
19
|
+
it('shows version', () => {
|
|
20
|
+
const output = execSync(`node ${cliPath} --version`, { encoding: 'utf-8' });
|
|
21
|
+
expect(output.trim()).toMatch(/^\d+\.\d+\.\d+$/);
|
|
22
|
+
});
|
|
23
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { mkdirSync, writeFileSync, rmSync, mkdtempSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { scanInstalledSquads } from '../outdated.js';
|
|
6
|
+
describe('scanInstalledSquads', () => {
|
|
7
|
+
let tempDir;
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
tempDir = mkdtempSync(join(tmpdir(), 'squads-test-'));
|
|
10
|
+
});
|
|
11
|
+
afterEach(() => {
|
|
12
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
13
|
+
});
|
|
14
|
+
it('returns empty array when directory does not exist', () => {
|
|
15
|
+
const result = scanInstalledSquads('/nonexistent/path');
|
|
16
|
+
expect(result).toEqual([]);
|
|
17
|
+
});
|
|
18
|
+
it('returns empty array when no @scope directories exist', () => {
|
|
19
|
+
mkdirSync(join(tempDir, 'local-squad'), { recursive: true });
|
|
20
|
+
writeFileSync(join(tempDir, 'local-squad', 'squad.yaml'), 'squad:\n version: "1.0.0"');
|
|
21
|
+
const result = scanInstalledSquads(tempDir);
|
|
22
|
+
expect(result).toEqual([]);
|
|
23
|
+
});
|
|
24
|
+
it('finds squads under @scope directories', () => {
|
|
25
|
+
const squadDir = join(tempDir, '@thulio', 'brand-squad');
|
|
26
|
+
mkdirSync(squadDir, { recursive: true });
|
|
27
|
+
writeFileSync(join(squadDir, 'squad.yaml'), 'squad:\n code: brand-squad\n version: "1.2.0"');
|
|
28
|
+
const result = scanInstalledSquads(tempDir);
|
|
29
|
+
expect(result).toEqual([
|
|
30
|
+
{ fullName: '@thulio/brand-squad', currentVersion: '1.2.0' },
|
|
31
|
+
]);
|
|
32
|
+
});
|
|
33
|
+
it('skips squads without squad.yaml', () => {
|
|
34
|
+
const squadDir = join(tempDir, '@thulio', 'no-yaml');
|
|
35
|
+
mkdirSync(squadDir, { recursive: true });
|
|
36
|
+
const result = scanInstalledSquads(tempDir);
|
|
37
|
+
expect(result).toEqual([]);
|
|
38
|
+
});
|
|
39
|
+
it('skips squads with invalid yaml', () => {
|
|
40
|
+
const squadDir = join(tempDir, '@thulio', 'bad-yaml');
|
|
41
|
+
mkdirSync(squadDir, { recursive: true });
|
|
42
|
+
writeFileSync(join(squadDir, 'squad.yaml'), '}{invalid');
|
|
43
|
+
const result = scanInstalledSquads(tempDir);
|
|
44
|
+
expect(result).toEqual([]);
|
|
45
|
+
});
|
|
46
|
+
it('skips squads without version field', () => {
|
|
47
|
+
const squadDir = join(tempDir, '@thulio', 'no-version');
|
|
48
|
+
mkdirSync(squadDir, { recursive: true });
|
|
49
|
+
writeFileSync(join(squadDir, 'squad.yaml'), 'squad:\n code: no-version');
|
|
50
|
+
const result = scanInstalledSquads(tempDir);
|
|
51
|
+
expect(result).toEqual([]);
|
|
52
|
+
});
|
|
53
|
+
it('finds squads with nested tarball structure (@scope/name/name/squad.yaml)', () => {
|
|
54
|
+
const nestedDir = join(tempDir, '@community', 'copy-squad', 'copy-squad');
|
|
55
|
+
mkdirSync(nestedDir, { recursive: true });
|
|
56
|
+
writeFileSync(join(nestedDir, 'squad.yaml'), 'squad:\n code: copy-squad\n version: "2.0.0"');
|
|
57
|
+
const result = scanInstalledSquads(tempDir);
|
|
58
|
+
expect(result).toEqual([
|
|
59
|
+
{ fullName: '@community/copy-squad', currentVersion: '2.0.0' },
|
|
60
|
+
]);
|
|
61
|
+
});
|
|
62
|
+
it('finds multiple squads across scopes', () => {
|
|
63
|
+
const squad1 = join(tempDir, '@thulio', 'squad-a');
|
|
64
|
+
const squad2 = join(tempDir, '@community', 'squad-b');
|
|
65
|
+
mkdirSync(squad1, { recursive: true });
|
|
66
|
+
mkdirSync(squad2, { recursive: true });
|
|
67
|
+
writeFileSync(join(squad1, 'squad.yaml'), 'squad:\n version: "1.0.0"');
|
|
68
|
+
writeFileSync(join(squad2, 'squad.yaml'), 'squad:\n version: "2.0.0"');
|
|
69
|
+
const result = scanInstalledSquads(tempDir);
|
|
70
|
+
expect(result).toHaveLength(2);
|
|
71
|
+
expect(result).toEqual(expect.arrayContaining([
|
|
72
|
+
{ fullName: '@thulio/squad-a', currentVersion: '1.0.0' },
|
|
73
|
+
{ fullName: '@community/squad-b', currentVersion: '2.0.0' },
|
|
74
|
+
]));
|
|
75
|
+
});
|
|
76
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import os from 'os';
|
|
5
|
+
import { loadMcpCatalog, loadMcpDefinition, listAllMcpIds } from '../catalog.js';
|
|
6
|
+
let tmpDir;
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'mcp-catalog-test-'));
|
|
9
|
+
});
|
|
10
|
+
afterEach(() => {
|
|
11
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
12
|
+
});
|
|
13
|
+
function writeCatalog(content) {
|
|
14
|
+
fs.writeFileSync(path.join(tmpDir, '_catalog.yaml'), content, 'utf-8');
|
|
15
|
+
}
|
|
16
|
+
function writeDefinition(id, content) {
|
|
17
|
+
fs.writeFileSync(path.join(tmpDir, `${id}.mcp.yaml`), content, 'utf-8');
|
|
18
|
+
}
|
|
19
|
+
describe('loadMcpCatalog', () => {
|
|
20
|
+
it('loads and parses _catalog.yaml', () => {
|
|
21
|
+
writeCatalog(`
|
|
22
|
+
catalog:
|
|
23
|
+
version: "1.0.0"
|
|
24
|
+
categories:
|
|
25
|
+
- name: development
|
|
26
|
+
mcps: [github]
|
|
27
|
+
- name: communication
|
|
28
|
+
mcps: [slack]
|
|
29
|
+
`);
|
|
30
|
+
const catalog = loadMcpCatalog(tmpDir);
|
|
31
|
+
expect(catalog.version).toBe('1.0.0');
|
|
32
|
+
expect(catalog.categories).toHaveLength(2);
|
|
33
|
+
expect(catalog.categories[0].name).toBe('development');
|
|
34
|
+
expect(catalog.categories[0].mcps).toEqual(['github']);
|
|
35
|
+
});
|
|
36
|
+
it('returns empty categories when catalog is missing', () => {
|
|
37
|
+
const catalog = loadMcpCatalog(tmpDir);
|
|
38
|
+
expect(catalog.categories).toEqual([]);
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
describe('listAllMcpIds', () => {
|
|
42
|
+
it('flattens all MCP IDs from categories', () => {
|
|
43
|
+
writeCatalog(`
|
|
44
|
+
catalog:
|
|
45
|
+
version: "1.0.0"
|
|
46
|
+
categories:
|
|
47
|
+
- name: dev
|
|
48
|
+
mcps: [github, postgresql]
|
|
49
|
+
- name: comm
|
|
50
|
+
mcps: [slack]
|
|
51
|
+
`);
|
|
52
|
+
const ids = listAllMcpIds(tmpDir);
|
|
53
|
+
expect(ids).toEqual(['github', 'postgresql', 'slack']);
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
describe('loadMcpDefinition', () => {
|
|
57
|
+
it('loads individual MCP definition by ID', () => {
|
|
58
|
+
writeDefinition('github', `
|
|
59
|
+
id: github
|
|
60
|
+
name: GitHub
|
|
61
|
+
description: PRs and issues
|
|
62
|
+
category: development
|
|
63
|
+
detection:
|
|
64
|
+
method: npm
|
|
65
|
+
npm:
|
|
66
|
+
package: "@modelcontextprotocol/server-github"
|
|
67
|
+
server:
|
|
68
|
+
command: npx
|
|
69
|
+
args: ["-y", "@modelcontextprotocol/server-github"]
|
|
70
|
+
env:
|
|
71
|
+
GITHUB_PERSONAL_ACCESS_TOKEN: "\${GITHUB_PERSONAL_ACCESS_TOKEN}"
|
|
72
|
+
auth:
|
|
73
|
+
- key: GITHUB_PERSONAL_ACCESS_TOKEN
|
|
74
|
+
label: "GitHub Token"
|
|
75
|
+
hint: "Create at github.com/settings/tokens"
|
|
76
|
+
required: true
|
|
77
|
+
tools:
|
|
78
|
+
- name: list_issues
|
|
79
|
+
description: List issues
|
|
80
|
+
relevant_sectors:
|
|
81
|
+
- development
|
|
82
|
+
`);
|
|
83
|
+
const def = loadMcpDefinition(tmpDir, 'github');
|
|
84
|
+
expect(def).not.toBeNull();
|
|
85
|
+
expect(def.id).toBe('github');
|
|
86
|
+
expect(def.name).toBe('GitHub');
|
|
87
|
+
expect(def.detection.method).toBe('npm');
|
|
88
|
+
expect(def.auth).toHaveLength(1);
|
|
89
|
+
expect(def.auth[0].key).toBe('GITHUB_PERSONAL_ACCESS_TOKEN');
|
|
90
|
+
expect(def.server.args).toContain('-y');
|
|
91
|
+
});
|
|
92
|
+
it('returns null for unknown MCP ID', () => {
|
|
93
|
+
const def = loadMcpDefinition(tmpDir, 'nonexistent');
|
|
94
|
+
expect(def).toBeNull();
|
|
95
|
+
});
|
|
96
|
+
it('handles malformed YAML gracefully', () => {
|
|
97
|
+
writeDefinition('broken', '{{{{not yaml');
|
|
98
|
+
const def = loadMcpDefinition(tmpDir, 'broken');
|
|
99
|
+
expect(def).toBeNull();
|
|
100
|
+
});
|
|
101
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { detectMcp, resolvePlatformBinary, detectExtension } from '../detect.js';
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import os from 'os';
|
|
6
|
+
function makeDef(overrides) {
|
|
7
|
+
return {
|
|
8
|
+
name: overrides.id,
|
|
9
|
+
description: '',
|
|
10
|
+
category: 'test',
|
|
11
|
+
server: { command: 'npx', args: [], env: {} },
|
|
12
|
+
auth: [],
|
|
13
|
+
tools: [],
|
|
14
|
+
relevant_sectors: [],
|
|
15
|
+
...overrides,
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
describe('resolvePlatformBinary', () => {
|
|
19
|
+
it('resolves darwin + arm64', () => {
|
|
20
|
+
expect(resolvePlatformBinary('darwin', 'arm64')).toBe('mcp-server-darwin-arm64');
|
|
21
|
+
});
|
|
22
|
+
it('resolves darwin + x64', () => {
|
|
23
|
+
expect(resolvePlatformBinary('darwin', 'x64')).toBe('mcp-server-darwin-x64');
|
|
24
|
+
});
|
|
25
|
+
it('resolves linux + x64', () => {
|
|
26
|
+
expect(resolvePlatformBinary('linux', 'x64')).toBe('mcp-server-linux-x64');
|
|
27
|
+
});
|
|
28
|
+
it('resolves win32 + x64 with .exe', () => {
|
|
29
|
+
expect(resolvePlatformBinary('win32', 'x64')).toBe('mcp-server-win32-x64.exe');
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
describe('detectExtension', () => {
|
|
33
|
+
let tmpDir;
|
|
34
|
+
beforeEach(() => {
|
|
35
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ext-detect-test-'));
|
|
36
|
+
});
|
|
37
|
+
afterEach(() => {
|
|
38
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
39
|
+
});
|
|
40
|
+
it('returns null when no extension found', () => {
|
|
41
|
+
const result = detectExtension('nonexistent-prefix-', tmpDir);
|
|
42
|
+
expect(result).toBeNull();
|
|
43
|
+
});
|
|
44
|
+
it('finds latest version', () => {
|
|
45
|
+
fs.mkdirSync(path.join(tmpDir, 'highagency.pencildev-0.6.30'));
|
|
46
|
+
fs.mkdirSync(path.join(tmpDir, 'highagency.pencildev-0.6.35'));
|
|
47
|
+
const result = detectExtension('highagency.pencildev-', tmpDir);
|
|
48
|
+
expect(result).not.toBeNull();
|
|
49
|
+
expect(result.version).toBe('0.6.35');
|
|
50
|
+
expect(result.extensionPath).toContain('0.6.35');
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
describe('detectMcp', () => {
|
|
54
|
+
it('npm detection — always returns available', () => {
|
|
55
|
+
const def = makeDef({
|
|
56
|
+
id: 'slack',
|
|
57
|
+
detection: { method: 'npm', npm: { package: '@modelcontextprotocol/server-slack' } },
|
|
58
|
+
});
|
|
59
|
+
const result = detectMcp(def);
|
|
60
|
+
expect(result.status).toBe('available');
|
|
61
|
+
expect(result.detail).toContain('npm');
|
|
62
|
+
});
|
|
63
|
+
it('cli detection — returns unavailable when command not found', () => {
|
|
64
|
+
const def = makeDef({
|
|
65
|
+
id: 'test-cli',
|
|
66
|
+
detection: { method: 'cli', cli: { command: 'nonexistent-binary-xyz', version_flag: '--version' } },
|
|
67
|
+
});
|
|
68
|
+
const result = detectMcp(def);
|
|
69
|
+
expect(result.status).toBe('unavailable');
|
|
70
|
+
});
|
|
71
|
+
it('hybrid detection — falls back to npm when CLI not found', () => {
|
|
72
|
+
const def = makeDef({
|
|
73
|
+
id: 'test-hybrid',
|
|
74
|
+
detection: {
|
|
75
|
+
method: 'hybrid',
|
|
76
|
+
cli: { command: 'nonexistent-binary-xyz', version_flag: '--version' },
|
|
77
|
+
npm: { package: 'some-package' },
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
const result = detectMcp(def);
|
|
81
|
+
expect(result.status).toBe('available');
|
|
82
|
+
expect(result.detail).toContain('npm');
|
|
83
|
+
});
|
|
84
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import os from 'os';
|
|
5
|
+
import { writeMcpConfig, removeMcpConfig, readMcpJson } from '../setup.js';
|
|
6
|
+
let tmpDir;
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'mcp-setup-test-'));
|
|
9
|
+
});
|
|
10
|
+
afterEach(() => {
|
|
11
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
12
|
+
});
|
|
13
|
+
function makeDef(id) {
|
|
14
|
+
return {
|
|
15
|
+
id,
|
|
16
|
+
name: id,
|
|
17
|
+
description: '',
|
|
18
|
+
category: 'test',
|
|
19
|
+
detection: { method: 'npm', npm: { package: `@test/${id}` } },
|
|
20
|
+
server: { command: 'npx', args: ['-y', `@test/${id}`], env: { TOKEN: '${TOKEN}' } },
|
|
21
|
+
auth: [{ key: 'TOKEN', label: 'Token', hint: 'hint', required: true }],
|
|
22
|
+
tools: [],
|
|
23
|
+
relevant_sectors: [],
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
describe('writeMcpConfig', () => {
|
|
27
|
+
it('creates .mcp.json with server entry', () => {
|
|
28
|
+
writeMcpConfig(tmpDir, makeDef('github'), { TOKEN: 'abc123' });
|
|
29
|
+
const json = readMcpJson(tmpDir);
|
|
30
|
+
expect(json.mcpServers.github).toBeDefined();
|
|
31
|
+
expect(json.mcpServers.github.command).toBe('npx');
|
|
32
|
+
expect(json.mcpServers.github.args).toContain('-y');
|
|
33
|
+
});
|
|
34
|
+
it('merges into existing .mcp.json without overwriting', () => {
|
|
35
|
+
const existing = { mcpServers: { pencil: { command: '/bin/pencil', args: [], env: {} } } };
|
|
36
|
+
fs.writeFileSync(path.join(tmpDir, '.mcp.json'), JSON.stringify(existing, null, 2));
|
|
37
|
+
writeMcpConfig(tmpDir, makeDef('github'), { TOKEN: 'abc' });
|
|
38
|
+
const json = readMcpJson(tmpDir);
|
|
39
|
+
expect(json.mcpServers.pencil).toBeDefined();
|
|
40
|
+
expect(json.mcpServers.github).toBeDefined();
|
|
41
|
+
});
|
|
42
|
+
it('adds env vars to .env file', () => {
|
|
43
|
+
writeMcpConfig(tmpDir, makeDef('github'), { TOKEN: 'mytoken' });
|
|
44
|
+
const env = fs.readFileSync(path.join(tmpDir, '.env'), 'utf-8');
|
|
45
|
+
expect(env).toContain('TOKEN=mytoken');
|
|
46
|
+
});
|
|
47
|
+
it('adds placeholders to .env.example', () => {
|
|
48
|
+
writeMcpConfig(tmpDir, makeDef('github'), { TOKEN: 'mytoken' });
|
|
49
|
+
const example = fs.readFileSync(path.join(tmpDir, '.env.example'), 'utf-8');
|
|
50
|
+
expect(example).toContain('TOKEN=');
|
|
51
|
+
expect(example).not.toContain('mytoken');
|
|
52
|
+
});
|
|
53
|
+
it('handles extension-based MCP with resolved command', () => {
|
|
54
|
+
const def = makeDef('pencil');
|
|
55
|
+
def.server.command = null;
|
|
56
|
+
writeMcpConfig(tmpDir, def, {}, '/resolved/path/to/binary');
|
|
57
|
+
const json = readMcpJson(tmpDir);
|
|
58
|
+
expect(json.mcpServers.pencil.command).toBe('/resolved/path/to/binary');
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
describe('removeMcpConfig', () => {
|
|
62
|
+
it('removes MCP entry from .mcp.json', () => {
|
|
63
|
+
const existing = {
|
|
64
|
+
mcpServers: {
|
|
65
|
+
github: { command: 'npx', args: [], env: {} },
|
|
66
|
+
slack: { command: 'npx', args: [], env: {} },
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
fs.writeFileSync(path.join(tmpDir, '.mcp.json'), JSON.stringify(existing, null, 2));
|
|
70
|
+
removeMcpConfig(tmpDir, 'github');
|
|
71
|
+
const json = readMcpJson(tmpDir);
|
|
72
|
+
expect(json.mcpServers.github).toBeUndefined();
|
|
73
|
+
expect(json.mcpServers.slack).toBeDefined();
|
|
74
|
+
});
|
|
75
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { validateSquadMcps, getConfiguredMcpIds } from '../validate.js';
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import os from 'os';
|
|
6
|
+
let tmpDir;
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'mcp-validate-test-'));
|
|
9
|
+
});
|
|
10
|
+
afterEach(() => {
|
|
11
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
12
|
+
});
|
|
13
|
+
describe('getConfiguredMcpIds', () => {
|
|
14
|
+
it('returns MCP IDs from .mcp.json', () => {
|
|
15
|
+
fs.writeFileSync(path.join(tmpDir, '.mcp.json'), JSON.stringify({
|
|
16
|
+
mcpServers: { github: { command: 'x', args: [], env: {} }, slack: { command: 'x', args: [], env: {} } },
|
|
17
|
+
}));
|
|
18
|
+
expect(getConfiguredMcpIds(tmpDir)).toEqual(['github', 'slack']);
|
|
19
|
+
});
|
|
20
|
+
it('returns empty array when .mcp.json missing', () => {
|
|
21
|
+
expect(getConfiguredMcpIds(tmpDir)).toEqual([]);
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
describe('validateSquadMcps', () => {
|
|
25
|
+
it('passes when squad has no mcps field', () => {
|
|
26
|
+
expect(() => validateSquadMcps(undefined, ['github'])).not.toThrow();
|
|
27
|
+
});
|
|
28
|
+
it('passes when squad has empty mcps', () => {
|
|
29
|
+
expect(() => validateSquadMcps([], ['github'])).not.toThrow();
|
|
30
|
+
});
|
|
31
|
+
it('passes when all required MCPs are configured', () => {
|
|
32
|
+
expect(() => validateSquadMcps(['github', 'slack'], ['github', 'slack', 'sentry'])).not.toThrow();
|
|
33
|
+
});
|
|
34
|
+
it('throws when required MCP is not configured', () => {
|
|
35
|
+
expect(() => validateSquadMcps(['github', 'linear'], ['github']))
|
|
36
|
+
.toThrow(/linear/i);
|
|
37
|
+
});
|
|
38
|
+
it('includes setup command in error message', () => {
|
|
39
|
+
expect(() => validateSquadMcps(['linear'], []))
|
|
40
|
+
.toThrow(/expxagents mcp setup/i);
|
|
41
|
+
});
|
|
42
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import os from 'os';
|
|
5
|
+
import { detectPencilExtension, resolvePlatformBinary, buildMcpConfig, } from '../detect.js';
|
|
6
|
+
describe('pencil/detect', () => {
|
|
7
|
+
let tmpDir;
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pencil-detect-'));
|
|
10
|
+
});
|
|
11
|
+
afterEach(() => {
|
|
12
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
13
|
+
});
|
|
14
|
+
describe('detectPencilExtension()', () => {
|
|
15
|
+
it('returns null when no extension directories exist', () => {
|
|
16
|
+
const result = detectPencilExtension(tmpDir);
|
|
17
|
+
expect(result).toBeNull();
|
|
18
|
+
});
|
|
19
|
+
it('returns the latest version when multiple versions exist', () => {
|
|
20
|
+
fs.mkdirSync(path.join(tmpDir, 'highagency.pencildev-0.6.33', 'out'), { recursive: true });
|
|
21
|
+
fs.mkdirSync(path.join(tmpDir, 'highagency.pencildev-0.6.35', 'out'), { recursive: true });
|
|
22
|
+
fs.mkdirSync(path.join(tmpDir, 'highagency.pencildev-0.6.34', 'out'), { recursive: true });
|
|
23
|
+
const result = detectPencilExtension(tmpDir);
|
|
24
|
+
expect(result).not.toBeNull();
|
|
25
|
+
expect(result.version).toBe('0.6.35');
|
|
26
|
+
expect(result.extensionPath).toContain('highagency.pencildev-0.6.35');
|
|
27
|
+
});
|
|
28
|
+
it('returns info for a single installed version', () => {
|
|
29
|
+
fs.mkdirSync(path.join(tmpDir, 'highagency.pencildev-0.6.33', 'out'), { recursive: true });
|
|
30
|
+
const result = detectPencilExtension(tmpDir);
|
|
31
|
+
expect(result).not.toBeNull();
|
|
32
|
+
expect(result.version).toBe('0.6.33');
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
describe('resolvePlatformBinary()', () => {
|
|
36
|
+
it('returns darwin-arm64 for macOS ARM', () => {
|
|
37
|
+
const binary = resolvePlatformBinary('darwin', 'arm64');
|
|
38
|
+
expect(binary).toBe('mcp-server-darwin-arm64');
|
|
39
|
+
});
|
|
40
|
+
it('returns darwin-x64 for macOS Intel', () => {
|
|
41
|
+
const binary = resolvePlatformBinary('darwin', 'x64');
|
|
42
|
+
expect(binary).toBe('mcp-server-darwin-x64');
|
|
43
|
+
});
|
|
44
|
+
it('returns linux-x64 for Linux', () => {
|
|
45
|
+
const binary = resolvePlatformBinary('linux', 'x64');
|
|
46
|
+
expect(binary).toBe('mcp-server-linux-x64');
|
|
47
|
+
});
|
|
48
|
+
it('returns win32-x64.exe for Windows', () => {
|
|
49
|
+
const binary = resolvePlatformBinary('win32', 'x64');
|
|
50
|
+
expect(binary).toBe('mcp-server-win32-x64.exe');
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
describe('buildMcpConfig()', () => {
|
|
54
|
+
it('generates correct config object', () => {
|
|
55
|
+
const info = {
|
|
56
|
+
version: '0.6.35',
|
|
57
|
+
extensionPath: '/Users/test/.vscode/extensions/highagency.pencildev-0.6.35',
|
|
58
|
+
};
|
|
59
|
+
const config = buildMcpConfig(info, 'mcp-server-darwin-arm64');
|
|
60
|
+
expect(config).toEqual({
|
|
61
|
+
mcpServers: {
|
|
62
|
+
pencil: {
|
|
63
|
+
command: '/Users/test/.vscode/extensions/highagency.pencildev-0.6.35/out/mcp-server-darwin-arm64',
|
|
64
|
+
args: ['--app', 'visual_studio_code'],
|
|
65
|
+
env: {},
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { mapNodeToCSS } from '../property-mapper.js';
|
|
3
|
+
describe('pencil/property-mapper', () => {
|
|
4
|
+
it('maps fill to background-color for frame nodes', () => {
|
|
5
|
+
const node = { type: 'frame', fill: '#FF5733' };
|
|
6
|
+
const css = mapNodeToCSS(node);
|
|
7
|
+
expect(css).toContain('background: #FF5733');
|
|
8
|
+
});
|
|
9
|
+
it('maps fill to color for text nodes', () => {
|
|
10
|
+
const node = { type: 'text', fill: '#333333' };
|
|
11
|
+
const css = mapNodeToCSS(node);
|
|
12
|
+
expect(css).toContain('color: #333333');
|
|
13
|
+
});
|
|
14
|
+
it('maps font properties', () => {
|
|
15
|
+
const node = {
|
|
16
|
+
type: 'text',
|
|
17
|
+
fontSize: 24,
|
|
18
|
+
fontWeight: '700',
|
|
19
|
+
fontFamily: 'Inter',
|
|
20
|
+
lineHeight: 1.5,
|
|
21
|
+
letterSpacing: 0.5,
|
|
22
|
+
};
|
|
23
|
+
const css = mapNodeToCSS(node);
|
|
24
|
+
expect(css).toContain('font-size: 24px');
|
|
25
|
+
expect(css).toContain('font-weight: 700');
|
|
26
|
+
expect(css).toContain('font-family: Inter');
|
|
27
|
+
expect(css).toContain('line-height: 1.5');
|
|
28
|
+
expect(css).toContain('letter-spacing: 0.5px');
|
|
29
|
+
});
|
|
30
|
+
it('maps padding array [T,R,B,L]', () => {
|
|
31
|
+
const node = { type: 'frame', padding: [10, 20, 10, 20] };
|
|
32
|
+
const css = mapNodeToCSS(node);
|
|
33
|
+
expect(css).toContain('padding: 10px 20px 10px 20px');
|
|
34
|
+
});
|
|
35
|
+
it('maps padding array [V,H]', () => {
|
|
36
|
+
const node = { type: 'frame', padding: [10, 20] };
|
|
37
|
+
const css = mapNodeToCSS(node);
|
|
38
|
+
expect(css).toContain('padding: 10px 20px');
|
|
39
|
+
});
|
|
40
|
+
it('maps single padding value', () => {
|
|
41
|
+
const node = { type: 'frame', padding: 16 };
|
|
42
|
+
const css = mapNodeToCSS(node);
|
|
43
|
+
expect(css).toContain('padding: 16px');
|
|
44
|
+
});
|
|
45
|
+
it('maps layout vertical to flex-direction column', () => {
|
|
46
|
+
const node = { type: 'frame', layout: 'vertical' };
|
|
47
|
+
const css = mapNodeToCSS(node);
|
|
48
|
+
expect(css).toContain('display: flex');
|
|
49
|
+
expect(css).toContain('flex-direction: column');
|
|
50
|
+
});
|
|
51
|
+
it('maps layout horizontal to flex-direction row', () => {
|
|
52
|
+
const node = { type: 'frame', layout: 'horizontal' };
|
|
53
|
+
const css = mapNodeToCSS(node);
|
|
54
|
+
expect(css).toContain('display: flex');
|
|
55
|
+
expect(css).toContain('flex-direction: row');
|
|
56
|
+
});
|
|
57
|
+
it('maps gap', () => {
|
|
58
|
+
const node = { type: 'frame', gap: 12 };
|
|
59
|
+
const css = mapNodeToCSS(node);
|
|
60
|
+
expect(css).toContain('gap: 12px');
|
|
61
|
+
});
|
|
62
|
+
it('maps width fill_container to width 100%', () => {
|
|
63
|
+
const node = { type: 'frame', width: 'fill_container' };
|
|
64
|
+
const css = mapNodeToCSS(node);
|
|
65
|
+
expect(css).toContain('width: 100%');
|
|
66
|
+
});
|
|
67
|
+
it('maps width fit_content', () => {
|
|
68
|
+
const node = { type: 'frame', width: 'fit_content' };
|
|
69
|
+
const css = mapNodeToCSS(node);
|
|
70
|
+
expect(css).toContain('width: fit-content');
|
|
71
|
+
});
|
|
72
|
+
it('maps numeric width to px', () => {
|
|
73
|
+
const node = { type: 'frame', width: 300 };
|
|
74
|
+
const css = mapNodeToCSS(node);
|
|
75
|
+
expect(css).toContain('width: 300px');
|
|
76
|
+
});
|
|
77
|
+
it('maps cornerRadius to border-radius', () => {
|
|
78
|
+
const node = { type: 'frame', cornerRadius: 8 };
|
|
79
|
+
const css = mapNodeToCSS(node);
|
|
80
|
+
expect(css).toContain('border-radius: 8px');
|
|
81
|
+
});
|
|
82
|
+
it('maps cornerRadius array', () => {
|
|
83
|
+
const node = { type: 'frame', cornerRadius: [8, 8, 0, 0] };
|
|
84
|
+
const css = mapNodeToCSS(node);
|
|
85
|
+
expect(css).toContain('border-radius: 8px 8px 0px 0px');
|
|
86
|
+
});
|
|
87
|
+
it('maps clip true to overflow hidden', () => {
|
|
88
|
+
const node = { type: 'frame', clip: true };
|
|
89
|
+
const css = mapNodeToCSS(node);
|
|
90
|
+
expect(css).toContain('overflow: hidden');
|
|
91
|
+
});
|
|
92
|
+
it('maps opacity', () => {
|
|
93
|
+
const node = { type: 'frame', opacity: 0.5 };
|
|
94
|
+
const css = mapNodeToCSS(node);
|
|
95
|
+
expect(css).toContain('opacity: 0.5');
|
|
96
|
+
});
|
|
97
|
+
it('maps justifyContent and alignItems', () => {
|
|
98
|
+
const node = {
|
|
99
|
+
type: 'frame',
|
|
100
|
+
justifyContent: 'center',
|
|
101
|
+
alignItems: 'flex-start',
|
|
102
|
+
};
|
|
103
|
+
const css = mapNodeToCSS(node);
|
|
104
|
+
expect(css).toContain('justify-content: center');
|
|
105
|
+
expect(css).toContain('align-items: flex-start');
|
|
106
|
+
});
|
|
107
|
+
it('maps stroke to border', () => {
|
|
108
|
+
const node = {
|
|
109
|
+
type: 'frame',
|
|
110
|
+
stroke: { thickness: 2, fill: '#000000' },
|
|
111
|
+
};
|
|
112
|
+
const css = mapNodeToCSS(node);
|
|
113
|
+
expect(css).toContain('border: 2px solid #000000');
|
|
114
|
+
});
|
|
115
|
+
it('returns empty string for empty node', () => {
|
|
116
|
+
const node = { type: 'frame' };
|
|
117
|
+
const css = mapNodeToCSS(node);
|
|
118
|
+
expect(css).toBe('');
|
|
119
|
+
});
|
|
120
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|