@soleri/cli 0.0.1 → 1.0.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.
- package/README.md +98 -0
- package/dist/commands/add-domain.d.ts +2 -0
- package/dist/commands/add-domain.js +41 -0
- package/dist/commands/add-domain.js.map +1 -0
- package/dist/commands/create.d.ts +2 -0
- package/dist/commands/create.js +68 -0
- package/dist/commands/create.js.map +1 -0
- package/dist/commands/dev.d.ts +2 -0
- package/dist/commands/dev.js +34 -0
- package/dist/commands/dev.js.map +1 -0
- package/dist/commands/doctor.d.ts +2 -0
- package/dist/commands/doctor.js +31 -0
- package/dist/commands/doctor.js.map +1 -0
- package/dist/commands/hooks.d.ts +2 -0
- package/dist/commands/hooks.js +76 -0
- package/dist/commands/hooks.js.map +1 -0
- package/dist/commands/install-knowledge.d.ts +2 -0
- package/dist/commands/install-knowledge.js +43 -0
- package/dist/commands/install-knowledge.js.map +1 -0
- package/dist/commands/list.d.ts +2 -0
- package/dist/commands/list.js +33 -0
- package/dist/commands/list.js.map +1 -0
- package/dist/hooks/generator.d.ts +15 -0
- package/dist/hooks/generator.js +58 -0
- package/dist/hooks/generator.js.map +1 -0
- package/dist/hooks/templates.d.ts +3 -0
- package/dist/hooks/templates.js +156 -0
- package/dist/hooks/templates.js.map +1 -0
- package/dist/main.d.ts +2 -0
- package/dist/main.js +23 -0
- package/dist/main.js.map +1 -0
- package/dist/prompts/create-wizard.d.ts +6 -0
- package/dist/prompts/create-wizard.js +126 -0
- package/dist/prompts/create-wizard.js.map +1 -0
- package/dist/utils/agent-context.d.ts +12 -0
- package/dist/utils/agent-context.js +31 -0
- package/dist/utils/agent-context.js.map +1 -0
- package/dist/utils/checks.d.ts +12 -0
- package/dist/utils/checks.js +116 -0
- package/dist/utils/checks.js.map +1 -0
- package/dist/utils/logger.d.ts +10 -0
- package/dist/utils/logger.js +33 -0
- package/dist/utils/logger.js.map +1 -0
- package/package.json +40 -4
- package/src/__tests__/add-domain.test.ts +117 -0
- package/src/__tests__/create.test.ts +92 -0
- package/src/__tests__/dev.test.ts +62 -0
- package/src/__tests__/doctor.test.ts +121 -0
- package/src/__tests__/hooks.test.ts +133 -0
- package/src/__tests__/install-knowledge.test.ts +117 -0
- package/src/__tests__/list.test.ts +80 -0
- package/src/commands/add-domain.ts +46 -0
- package/src/commands/create.ts +73 -0
- package/src/commands/dev.ts +39 -0
- package/src/commands/doctor.ts +33 -0
- package/src/commands/hooks.ts +86 -0
- package/src/commands/install-knowledge.ts +49 -0
- package/src/commands/list.ts +42 -0
- package/src/hooks/generator.ts +65 -0
- package/src/hooks/templates.ts +185 -0
- package/src/main.ts +27 -0
- package/src/prompts/create-wizard.ts +129 -0
- package/src/utils/agent-context.ts +38 -0
- package/src/utils/checks.ts +127 -0
- package/src/utils/logger.ts +39 -0
- package/tsconfig.json +21 -0
- package/vitest.config.ts +8 -0
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { mkdirSync, rmSync, writeFileSync, existsSync, readFileSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { addDomain } from '@soleri/forge/lib';
|
|
6
|
+
|
|
7
|
+
describe('add-domain command', () => {
|
|
8
|
+
let tempDir: string;
|
|
9
|
+
let agentDir: string;
|
|
10
|
+
|
|
11
|
+
function createMinimalAgent(): void {
|
|
12
|
+
mkdirSync(join(agentDir, 'src', 'intelligence', 'data'), { recursive: true });
|
|
13
|
+
mkdirSync(join(agentDir, 'src', 'facades'), { recursive: true });
|
|
14
|
+
mkdirSync(join(agentDir, 'src', 'activation'), { recursive: true });
|
|
15
|
+
writeFileSync(
|
|
16
|
+
join(agentDir, 'package.json'),
|
|
17
|
+
JSON.stringify({
|
|
18
|
+
name: 'test-agent-mcp',
|
|
19
|
+
scripts: { build: 'echo build' },
|
|
20
|
+
}),
|
|
21
|
+
);
|
|
22
|
+
// Minimal index.ts with anchors
|
|
23
|
+
writeFileSync(
|
|
24
|
+
join(agentDir, 'src', 'index.ts'),
|
|
25
|
+
[
|
|
26
|
+
"import { createCoreFacade } from './facades/core.facade.js';",
|
|
27
|
+
'',
|
|
28
|
+
'const facades = [',
|
|
29
|
+
' createCoreFacade(vault, brain),',
|
|
30
|
+
'];',
|
|
31
|
+
].join('\n'),
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
beforeEach(() => {
|
|
36
|
+
tempDir = join(tmpdir(), `cli-add-domain-test-${Date.now()}`);
|
|
37
|
+
agentDir = join(tempDir, 'test-agent');
|
|
38
|
+
mkdirSync(tempDir, { recursive: true });
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
afterEach(() => {
|
|
42
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('should fail if no package.json', async () => {
|
|
46
|
+
mkdirSync(agentDir, { recursive: true });
|
|
47
|
+
const result = await addDomain({ agentPath: agentDir, domain: 'security', noBuild: true });
|
|
48
|
+
expect(result.success).toBe(false);
|
|
49
|
+
expect(result.summary).toContain('package.json');
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('should fail if package name does not end with -mcp', async () => {
|
|
53
|
+
mkdirSync(agentDir, { recursive: true });
|
|
54
|
+
writeFileSync(join(agentDir, 'package.json'), JSON.stringify({ name: 'not-agent' }));
|
|
55
|
+
const result = await addDomain({ agentPath: agentDir, domain: 'security', noBuild: true });
|
|
56
|
+
expect(result.success).toBe(false);
|
|
57
|
+
expect(result.summary).toContain('-mcp');
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('should fail with invalid domain name', async () => {
|
|
61
|
+
createMinimalAgent();
|
|
62
|
+
const result = await addDomain({ agentPath: agentDir, domain: 'Invalid', noBuild: true });
|
|
63
|
+
expect(result.success).toBe(false);
|
|
64
|
+
expect(result.summary).toContain('kebab-case');
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('should fail if domain already exists', async () => {
|
|
68
|
+
createMinimalAgent();
|
|
69
|
+
writeFileSync(
|
|
70
|
+
join(agentDir, 'src', 'intelligence', 'data', 'security.json'),
|
|
71
|
+
JSON.stringify({ domain: 'security', version: '1.0.0', entries: [] }),
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
const result = await addDomain({ agentPath: agentDir, domain: 'security', noBuild: true });
|
|
75
|
+
expect(result.success).toBe(false);
|
|
76
|
+
expect(result.summary).toContain('already exists');
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('should create empty bundle and facade', async () => {
|
|
80
|
+
createMinimalAgent();
|
|
81
|
+
|
|
82
|
+
const result = await addDomain({ agentPath: agentDir, domain: 'security', noBuild: true });
|
|
83
|
+
expect(result.success).toBe(true);
|
|
84
|
+
|
|
85
|
+
// Bundle
|
|
86
|
+
const bundle = JSON.parse(
|
|
87
|
+
readFileSync(join(agentDir, 'src', 'intelligence', 'data', 'security.json'), 'utf-8'),
|
|
88
|
+
);
|
|
89
|
+
expect(bundle.domain).toBe('security');
|
|
90
|
+
expect(bundle.entries).toEqual([]);
|
|
91
|
+
|
|
92
|
+
// Facade
|
|
93
|
+
expect(existsSync(join(agentDir, 'src', 'facades', 'security.facade.ts'))).toBe(true);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('should patch index.ts with new import and facade', async () => {
|
|
97
|
+
createMinimalAgent();
|
|
98
|
+
|
|
99
|
+
await addDomain({ agentPath: agentDir, domain: 'security', noBuild: true });
|
|
100
|
+
|
|
101
|
+
const indexContent = readFileSync(join(agentDir, 'src', 'index.ts'), 'utf-8');
|
|
102
|
+
expect(indexContent).toContain('createSecurityFacade');
|
|
103
|
+
expect(indexContent).toContain('import { createSecurityFacade }');
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('should handle agents without brain directory', async () => {
|
|
107
|
+
createMinimalAgent();
|
|
108
|
+
// No brain dir → vault-only facade
|
|
109
|
+
|
|
110
|
+
const result = await addDomain({ agentPath: agentDir, domain: 'api-design', noBuild: true });
|
|
111
|
+
expect(result.success).toBe(true);
|
|
112
|
+
|
|
113
|
+
const facade = readFileSync(join(agentDir, 'src', 'facades', 'api-design.facade.ts'), 'utf-8');
|
|
114
|
+
// Vault-only facades import from vault directly, not from @soleri/core
|
|
115
|
+
expect(facade).toContain('vault');
|
|
116
|
+
});
|
|
117
|
+
});
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { mkdirSync, rmSync, existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { previewScaffold, scaffold } from '@soleri/forge/lib';
|
|
6
|
+
import type { AgentConfig } from '@soleri/forge/lib';
|
|
7
|
+
|
|
8
|
+
describe('create command', () => {
|
|
9
|
+
let tempDir: string;
|
|
10
|
+
|
|
11
|
+
const testConfig: AgentConfig = {
|
|
12
|
+
id: 'test-agent',
|
|
13
|
+
name: 'TestAgent',
|
|
14
|
+
role: 'A test agent',
|
|
15
|
+
description: 'This agent is used for testing the CLI create command.',
|
|
16
|
+
domains: ['testing', 'quality'],
|
|
17
|
+
principles: ['Test everything', 'Quality first'],
|
|
18
|
+
greeting: 'Hello! I am TestAgent, here to help with testing.',
|
|
19
|
+
outputDir: '',
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
beforeEach(() => {
|
|
23
|
+
tempDir = join(tmpdir(), `cli-create-test-${Date.now()}`);
|
|
24
|
+
mkdirSync(tempDir, { recursive: true });
|
|
25
|
+
testConfig.outputDir = tempDir;
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
afterEach(() => {
|
|
29
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('should preview scaffold without creating files', () => {
|
|
33
|
+
const preview = previewScaffold(testConfig);
|
|
34
|
+
|
|
35
|
+
expect(preview.agentDir).toBe(join(tempDir, 'test-agent'));
|
|
36
|
+
expect(preview.persona.name).toBe('TestAgent');
|
|
37
|
+
expect(preview.domains).toEqual(['testing', 'quality']);
|
|
38
|
+
expect(preview.files.length).toBeGreaterThan(10);
|
|
39
|
+
expect(existsSync(preview.agentDir)).toBe(false);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('should scaffold agent successfully', () => {
|
|
43
|
+
const result = scaffold(testConfig);
|
|
44
|
+
|
|
45
|
+
expect(result.success).toBe(true);
|
|
46
|
+
expect(result.agentDir).toBe(join(tempDir, 'test-agent'));
|
|
47
|
+
expect(result.filesCreated.length).toBeGreaterThan(10);
|
|
48
|
+
expect(existsSync(join(tempDir, 'test-agent', 'package.json'))).toBe(true);
|
|
49
|
+
expect(existsSync(join(tempDir, 'test-agent', 'src', 'index.ts'))).toBe(true);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('should fail if directory already exists', () => {
|
|
53
|
+
scaffold(testConfig);
|
|
54
|
+
const result = scaffold(testConfig);
|
|
55
|
+
|
|
56
|
+
expect(result.success).toBe(false);
|
|
57
|
+
expect(result.summary).toContain('already exists');
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('should create domain facades for each domain', () => {
|
|
61
|
+
scaffold(testConfig);
|
|
62
|
+
|
|
63
|
+
expect(existsSync(join(tempDir, 'test-agent', 'src', 'facades', 'testing.facade.ts'))).toBe(
|
|
64
|
+
true,
|
|
65
|
+
);
|
|
66
|
+
expect(existsSync(join(tempDir, 'test-agent', 'src', 'facades', 'quality.facade.ts'))).toBe(
|
|
67
|
+
true,
|
|
68
|
+
);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('should create intelligence data files for each domain', () => {
|
|
72
|
+
scaffold(testConfig);
|
|
73
|
+
|
|
74
|
+
const testingBundle = JSON.parse(
|
|
75
|
+
readFileSync(
|
|
76
|
+
join(tempDir, 'test-agent', 'src', 'intelligence', 'data', 'testing.json'),
|
|
77
|
+
'utf-8',
|
|
78
|
+
),
|
|
79
|
+
);
|
|
80
|
+
expect(testingBundle.domain).toBe('testing');
|
|
81
|
+
expect(testingBundle.entries).toEqual([]);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('should read config from file for non-interactive mode', () => {
|
|
85
|
+
const configPath = join(tempDir, 'agent.json');
|
|
86
|
+
writeFileSync(configPath, JSON.stringify(testConfig), 'utf-8');
|
|
87
|
+
|
|
88
|
+
const raw = JSON.parse(readFileSync(configPath, 'utf-8'));
|
|
89
|
+
expect(raw.id).toBe('test-agent');
|
|
90
|
+
expect(raw.domains).toEqual(['testing', 'quality']);
|
|
91
|
+
});
|
|
92
|
+
});
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { mkdirSync, rmSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { detectAgent } from '../utils/agent-context.js';
|
|
6
|
+
|
|
7
|
+
describe('dev command', () => {
|
|
8
|
+
let tempDir: string;
|
|
9
|
+
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
tempDir = join(tmpdir(), `cli-dev-test-${Date.now()}`);
|
|
12
|
+
mkdirSync(tempDir, { recursive: true });
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
afterEach(() => {
|
|
16
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('should detect agent in directory', () => {
|
|
20
|
+
const agentDir = join(tempDir, 'my-agent');
|
|
21
|
+
mkdirSync(agentDir, { recursive: true });
|
|
22
|
+
writeFileSync(join(agentDir, 'package.json'), JSON.stringify({ name: 'my-agent-mcp' }));
|
|
23
|
+
|
|
24
|
+
const ctx = detectAgent(agentDir);
|
|
25
|
+
expect(ctx).not.toBeNull();
|
|
26
|
+
expect(ctx!.agentId).toBe('my-agent');
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('should return null for non-agent directory', () => {
|
|
30
|
+
const dir = join(tempDir, 'not-agent');
|
|
31
|
+
mkdirSync(dir, { recursive: true });
|
|
32
|
+
writeFileSync(join(dir, 'package.json'), JSON.stringify({ name: 'some-lib' }));
|
|
33
|
+
|
|
34
|
+
const ctx = detectAgent(dir);
|
|
35
|
+
expect(ctx).toBeNull();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('should return null for directory without package.json', () => {
|
|
39
|
+
const ctx = detectAgent(tempDir);
|
|
40
|
+
expect(ctx).toBeNull();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('should detect brain presence', () => {
|
|
44
|
+
const agentDir = join(tempDir, 'brain-agent');
|
|
45
|
+
mkdirSync(join(agentDir, 'src', 'brain'), { recursive: true });
|
|
46
|
+
writeFileSync(join(agentDir, 'package.json'), JSON.stringify({ name: 'brain-agent-mcp' }));
|
|
47
|
+
|
|
48
|
+
const ctx = detectAgent(agentDir);
|
|
49
|
+
expect(ctx).not.toBeNull();
|
|
50
|
+
expect(ctx!.hasBrain).toBe(true);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('should report no brain when absent', () => {
|
|
54
|
+
const agentDir = join(tempDir, 'no-brain');
|
|
55
|
+
mkdirSync(agentDir, { recursive: true });
|
|
56
|
+
writeFileSync(join(agentDir, 'package.json'), JSON.stringify({ name: 'no-brain-mcp' }));
|
|
57
|
+
|
|
58
|
+
const ctx = detectAgent(agentDir);
|
|
59
|
+
expect(ctx).not.toBeNull();
|
|
60
|
+
expect(ctx!.hasBrain).toBe(false);
|
|
61
|
+
});
|
|
62
|
+
});
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { mkdirSync, rmSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import {
|
|
6
|
+
checkNodeVersion,
|
|
7
|
+
checkNpm,
|
|
8
|
+
checkAgentProject,
|
|
9
|
+
checkAgentBuild,
|
|
10
|
+
checkNodeModules,
|
|
11
|
+
runAllChecks,
|
|
12
|
+
} from '../utils/checks.js';
|
|
13
|
+
|
|
14
|
+
describe('doctor command', () => {
|
|
15
|
+
let tempDir: string;
|
|
16
|
+
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
tempDir = join(tmpdir(), `cli-doctor-test-${Date.now()}`);
|
|
19
|
+
mkdirSync(tempDir, { recursive: true });
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
afterEach(() => {
|
|
23
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
describe('checkNodeVersion', () => {
|
|
27
|
+
it('should pass for current Node version', () => {
|
|
28
|
+
const result = checkNodeVersion();
|
|
29
|
+
expect(result.status).toBe('pass');
|
|
30
|
+
expect(result.detail).toContain(process.versions.node);
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
describe('checkNpm', () => {
|
|
35
|
+
it('should pass when npm is available', () => {
|
|
36
|
+
const result = checkNpm();
|
|
37
|
+
expect(result.status).toBe('pass');
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
describe('checkAgentProject', () => {
|
|
42
|
+
it('should warn for non-agent directory', () => {
|
|
43
|
+
const result = checkAgentProject(tempDir);
|
|
44
|
+
expect(result.status).toBe('warn');
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('should pass for agent directory', () => {
|
|
48
|
+
const agentDir = join(tempDir, 'agent');
|
|
49
|
+
mkdirSync(agentDir, { recursive: true });
|
|
50
|
+
writeFileSync(join(agentDir, 'package.json'), JSON.stringify({ name: 'agent-mcp' }));
|
|
51
|
+
|
|
52
|
+
const result = checkAgentProject(agentDir);
|
|
53
|
+
expect(result.status).toBe('pass');
|
|
54
|
+
expect(result.detail).toContain('agent-mcp');
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
describe('checkAgentBuild', () => {
|
|
59
|
+
it('should warn if no agent detected', () => {
|
|
60
|
+
const result = checkAgentBuild(tempDir);
|
|
61
|
+
expect(result.status).toBe('warn');
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('should fail if dist is missing', () => {
|
|
65
|
+
const agentDir = join(tempDir, 'agent');
|
|
66
|
+
mkdirSync(agentDir, { recursive: true });
|
|
67
|
+
writeFileSync(join(agentDir, 'package.json'), JSON.stringify({ name: 'agent-mcp' }));
|
|
68
|
+
|
|
69
|
+
const result = checkAgentBuild(agentDir);
|
|
70
|
+
expect(result.status).toBe('fail');
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('should pass if dist/index.js exists', () => {
|
|
74
|
+
const agentDir = join(tempDir, 'agent');
|
|
75
|
+
mkdirSync(join(agentDir, 'dist'), { recursive: true });
|
|
76
|
+
writeFileSync(join(agentDir, 'package.json'), JSON.stringify({ name: 'agent-mcp' }));
|
|
77
|
+
writeFileSync(join(agentDir, 'dist', 'index.js'), '');
|
|
78
|
+
|
|
79
|
+
const result = checkAgentBuild(agentDir);
|
|
80
|
+
expect(result.status).toBe('pass');
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
describe('checkNodeModules', () => {
|
|
85
|
+
it('should fail if node_modules is missing', () => {
|
|
86
|
+
const agentDir = join(tempDir, 'agent');
|
|
87
|
+
mkdirSync(agentDir, { recursive: true });
|
|
88
|
+
writeFileSync(join(agentDir, 'package.json'), JSON.stringify({ name: 'agent-mcp' }));
|
|
89
|
+
|
|
90
|
+
const result = checkNodeModules(agentDir);
|
|
91
|
+
expect(result.status).toBe('fail');
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('should pass if node_modules exists', () => {
|
|
95
|
+
const agentDir = join(tempDir, 'agent');
|
|
96
|
+
mkdirSync(join(agentDir, 'node_modules'), { recursive: true });
|
|
97
|
+
writeFileSync(join(agentDir, 'package.json'), JSON.stringify({ name: 'agent-mcp' }));
|
|
98
|
+
|
|
99
|
+
const result = checkNodeModules(agentDir);
|
|
100
|
+
expect(result.status).toBe('pass');
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
describe('runAllChecks', () => {
|
|
105
|
+
it('should return array of check results', () => {
|
|
106
|
+
const results = runAllChecks(tempDir);
|
|
107
|
+
expect(results.length).toBeGreaterThan(0);
|
|
108
|
+
for (const r of results) {
|
|
109
|
+
expect(['pass', 'fail', 'warn']).toContain(r.status);
|
|
110
|
+
expect(r.label).toBeTruthy();
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('should include Node and npm checks regardless of directory', () => {
|
|
115
|
+
const results = runAllChecks(tempDir);
|
|
116
|
+
const labels = results.map((r) => r.label);
|
|
117
|
+
expect(labels).toContain('Node.js');
|
|
118
|
+
expect(labels).toContain('npm');
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
});
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { mkdirSync, rmSync, writeFileSync, existsSync, readFileSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { installHooks, removeHooks, detectInstalledHooks } from '../hooks/generator.js';
|
|
6
|
+
import { SUPPORTED_EDITORS } from '../hooks/templates.js';
|
|
7
|
+
|
|
8
|
+
describe('hooks commands', () => {
|
|
9
|
+
let tempDir: string;
|
|
10
|
+
let agentDir: string;
|
|
11
|
+
|
|
12
|
+
function createMinimalAgent(): void {
|
|
13
|
+
mkdirSync(agentDir, { recursive: true });
|
|
14
|
+
writeFileSync(join(agentDir, 'package.json'), JSON.stringify({ name: 'hook-test-mcp' }));
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
tempDir = join(tmpdir(), `cli-hooks-test-${Date.now()}`);
|
|
19
|
+
agentDir = join(tempDir, 'hook-test');
|
|
20
|
+
mkdirSync(tempDir, { recursive: true });
|
|
21
|
+
createMinimalAgent();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
afterEach(() => {
|
|
25
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
describe('installHooks', () => {
|
|
29
|
+
it('should create claude-code settings.json', () => {
|
|
30
|
+
const files = installHooks('claude-code', agentDir);
|
|
31
|
+
|
|
32
|
+
expect(files).toContain('.claude/settings.json');
|
|
33
|
+
expect(existsSync(join(agentDir, '.claude', 'settings.json'))).toBe(true);
|
|
34
|
+
|
|
35
|
+
const settings = JSON.parse(
|
|
36
|
+
readFileSync(join(agentDir, '.claude', 'settings.json'), 'utf-8'),
|
|
37
|
+
);
|
|
38
|
+
expect(settings.hooks).toBeDefined();
|
|
39
|
+
expect(settings.hooks.PreToolUse).toBeDefined();
|
|
40
|
+
expect(settings.hooks.SessionStart).toBeDefined();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('should create cursor rules file', () => {
|
|
44
|
+
const files = installHooks('cursor', agentDir);
|
|
45
|
+
|
|
46
|
+
expect(files).toContain('.cursorrules');
|
|
47
|
+
const content = readFileSync(join(agentDir, '.cursorrules'), 'utf-8');
|
|
48
|
+
expect(content).toContain('hook-test');
|
|
49
|
+
expect(content).toContain('Cursor Rules');
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('should create windsurf rules file', () => {
|
|
53
|
+
const files = installHooks('windsurf', agentDir);
|
|
54
|
+
|
|
55
|
+
expect(files).toContain('.windsurfrules');
|
|
56
|
+
const content = readFileSync(join(agentDir, '.windsurfrules'), 'utf-8');
|
|
57
|
+
expect(content).toContain('Windsurf Rules');
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('should create copilot instructions', () => {
|
|
61
|
+
const files = installHooks('copilot', agentDir);
|
|
62
|
+
|
|
63
|
+
expect(files).toContain('.github/copilot-instructions.md');
|
|
64
|
+
expect(existsSync(join(agentDir, '.github', 'copilot-instructions.md'))).toBe(true);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('should include agent ID in generated files', () => {
|
|
68
|
+
installHooks('cursor', agentDir);
|
|
69
|
+
const content = readFileSync(join(agentDir, '.cursorrules'), 'utf-8');
|
|
70
|
+
expect(content).toContain('hook-test');
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
describe('removeHooks', () => {
|
|
75
|
+
it('should remove installed hook files', () => {
|
|
76
|
+
installHooks('cursor', agentDir);
|
|
77
|
+
expect(existsSync(join(agentDir, '.cursorrules'))).toBe(true);
|
|
78
|
+
|
|
79
|
+
const removed = removeHooks('cursor', agentDir);
|
|
80
|
+
expect(removed).toContain('.cursorrules');
|
|
81
|
+
expect(existsSync(join(agentDir, '.cursorrules'))).toBe(false);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('should return empty array if no hooks installed', () => {
|
|
85
|
+
const removed = removeHooks('cursor', agentDir);
|
|
86
|
+
expect(removed).toEqual([]);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('should remove claude-code settings', () => {
|
|
90
|
+
installHooks('claude-code', agentDir);
|
|
91
|
+
expect(existsSync(join(agentDir, '.claude', 'settings.json'))).toBe(true);
|
|
92
|
+
|
|
93
|
+
const removed = removeHooks('claude-code', agentDir);
|
|
94
|
+
expect(removed).toContain('.claude/settings.json');
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
describe('detectInstalledHooks', () => {
|
|
99
|
+
it('should detect no hooks when none installed', () => {
|
|
100
|
+
const installed = detectInstalledHooks(agentDir);
|
|
101
|
+
expect(installed).toEqual([]);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('should detect installed hooks', () => {
|
|
105
|
+
installHooks('cursor', agentDir);
|
|
106
|
+
installHooks('claude-code', agentDir);
|
|
107
|
+
|
|
108
|
+
const installed = detectInstalledHooks(agentDir);
|
|
109
|
+
expect(installed).toContain('cursor');
|
|
110
|
+
expect(installed).toContain('claude-code');
|
|
111
|
+
expect(installed).not.toContain('windsurf');
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('should detect all 4 editors when all installed', () => {
|
|
115
|
+
for (const editor of SUPPORTED_EDITORS) {
|
|
116
|
+
installHooks(editor, agentDir);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const installed = detectInstalledHooks(agentDir);
|
|
120
|
+
expect(installed).toHaveLength(4);
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
describe('SUPPORTED_EDITORS', () => {
|
|
125
|
+
it('should have 4 editors', () => {
|
|
126
|
+
expect(SUPPORTED_EDITORS).toHaveLength(4);
|
|
127
|
+
expect(SUPPORTED_EDITORS).toContain('claude-code');
|
|
128
|
+
expect(SUPPORTED_EDITORS).toContain('cursor');
|
|
129
|
+
expect(SUPPORTED_EDITORS).toContain('windsurf');
|
|
130
|
+
expect(SUPPORTED_EDITORS).toContain('copilot');
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
});
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { mkdirSync, rmSync, writeFileSync, existsSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { installKnowledge } from '@soleri/forge/lib';
|
|
6
|
+
|
|
7
|
+
describe('install-knowledge command', () => {
|
|
8
|
+
let tempDir: string;
|
|
9
|
+
let agentDir: string;
|
|
10
|
+
let bundleDir: string;
|
|
11
|
+
|
|
12
|
+
function createMinimalAgent(): void {
|
|
13
|
+
mkdirSync(join(agentDir, 'src', 'intelligence', 'data'), { recursive: true });
|
|
14
|
+
mkdirSync(join(agentDir, 'src', 'facades'), { recursive: true });
|
|
15
|
+
writeFileSync(
|
|
16
|
+
join(agentDir, 'package.json'),
|
|
17
|
+
JSON.stringify({
|
|
18
|
+
name: 'test-agent-mcp',
|
|
19
|
+
scripts: { build: 'echo build' },
|
|
20
|
+
}),
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function createBundle(domain: string, entries: unknown[] = []): string {
|
|
25
|
+
const bundlePath = join(bundleDir, `${domain}.json`);
|
|
26
|
+
writeFileSync(
|
|
27
|
+
bundlePath,
|
|
28
|
+
JSON.stringify({
|
|
29
|
+
domain,
|
|
30
|
+
version: '1.0.0',
|
|
31
|
+
entries: entries.length
|
|
32
|
+
? entries
|
|
33
|
+
: [
|
|
34
|
+
{
|
|
35
|
+
id: `${domain}-pattern-1`,
|
|
36
|
+
type: 'pattern',
|
|
37
|
+
domain,
|
|
38
|
+
title: `${domain} Pattern`,
|
|
39
|
+
severity: 'suggestion',
|
|
40
|
+
description: `A ${domain} pattern for testing.`,
|
|
41
|
+
tags: [domain],
|
|
42
|
+
},
|
|
43
|
+
],
|
|
44
|
+
}),
|
|
45
|
+
);
|
|
46
|
+
return bundlePath;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
beforeEach(() => {
|
|
50
|
+
tempDir = join(tmpdir(), `cli-knowledge-test-${Date.now()}`);
|
|
51
|
+
agentDir = join(tempDir, 'test-agent');
|
|
52
|
+
bundleDir = join(tempDir, 'bundles');
|
|
53
|
+
mkdirSync(bundleDir, { recursive: true });
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
afterEach(() => {
|
|
57
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('should fail if agent path is invalid', async () => {
|
|
61
|
+
const result = await installKnowledge({
|
|
62
|
+
agentPath: join(tempDir, 'nonexistent'),
|
|
63
|
+
bundlePath: bundleDir,
|
|
64
|
+
});
|
|
65
|
+
expect(result.success).toBe(false);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('should fail if no bundles found', async () => {
|
|
69
|
+
createMinimalAgent();
|
|
70
|
+
const emptyDir = join(tempDir, 'empty-bundles');
|
|
71
|
+
mkdirSync(emptyDir);
|
|
72
|
+
|
|
73
|
+
const result = await installKnowledge({
|
|
74
|
+
agentPath: agentDir,
|
|
75
|
+
bundlePath: emptyDir,
|
|
76
|
+
});
|
|
77
|
+
expect(result.success).toBe(false);
|
|
78
|
+
expect(result.summary).toContain('No .json bundle files');
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('should install a single bundle', async () => {
|
|
82
|
+
createMinimalAgent();
|
|
83
|
+
createBundle('security');
|
|
84
|
+
|
|
85
|
+
const result = await installKnowledge({
|
|
86
|
+
agentPath: agentDir,
|
|
87
|
+
bundlePath: bundleDir,
|
|
88
|
+
generateFacades: false,
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
expect(result.success).toBe(true);
|
|
92
|
+
expect(result.bundlesInstalled).toBe(1);
|
|
93
|
+
expect(result.domainsAdded).toContain('security');
|
|
94
|
+
expect(existsSync(join(agentDir, 'src', 'intelligence', 'data', 'security.json'))).toBe(true);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('should classify domains as added vs updated', async () => {
|
|
98
|
+
createMinimalAgent();
|
|
99
|
+
// Pre-existing domain
|
|
100
|
+
writeFileSync(
|
|
101
|
+
join(agentDir, 'src', 'intelligence', 'data', 'security.json'),
|
|
102
|
+
JSON.stringify({ domain: 'security', version: '0.5.0', entries: [] }),
|
|
103
|
+
);
|
|
104
|
+
// New + updated bundles
|
|
105
|
+
createBundle('security');
|
|
106
|
+
createBundle('api-design');
|
|
107
|
+
|
|
108
|
+
const result = await installKnowledge({
|
|
109
|
+
agentPath: agentDir,
|
|
110
|
+
bundlePath: bundleDir,
|
|
111
|
+
generateFacades: false,
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
expect(result.domainsUpdated).toContain('security');
|
|
115
|
+
expect(result.domainsAdded).toContain('api-design');
|
|
116
|
+
});
|
|
117
|
+
});
|