@soleri/cli 0.0.1 → 1.0.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/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 +138 -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 +148 -0
- package/src/utils/logger.ts +39 -0
- package/tsconfig.json +21 -0
- package/vitest.config.ts +8 -0
|
@@ -0,0 +1,80 @@
|
|
|
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 { listAgents } from '@soleri/forge/lib';
|
|
6
|
+
|
|
7
|
+
describe('list command', () => {
|
|
8
|
+
let tempDir: string;
|
|
9
|
+
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
tempDir = join(tmpdir(), `cli-list-test-${Date.now()}`);
|
|
12
|
+
mkdirSync(tempDir, { recursive: true });
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
afterEach(() => {
|
|
16
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('should return empty array for directory with no agents', () => {
|
|
20
|
+
const agents = listAgents(tempDir);
|
|
21
|
+
expect(agents).toEqual([]);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('should return empty array for non-existent directory', () => {
|
|
25
|
+
const agents = listAgents(join(tempDir, 'nonexistent'));
|
|
26
|
+
expect(agents).toEqual([]);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('should detect agent directories', () => {
|
|
30
|
+
// Create a minimal agent directory
|
|
31
|
+
const agentDir = join(tempDir, 'test-agent');
|
|
32
|
+
mkdirSync(join(agentDir, 'src', 'intelligence', 'data'), { recursive: true });
|
|
33
|
+
writeFileSync(
|
|
34
|
+
join(agentDir, 'package.json'),
|
|
35
|
+
JSON.stringify({ name: 'test-agent-mcp', description: 'A test agent' }),
|
|
36
|
+
);
|
|
37
|
+
writeFileSync(
|
|
38
|
+
join(agentDir, 'src', 'intelligence', 'data', 'testing.json'),
|
|
39
|
+
JSON.stringify({ domain: 'testing', version: '1.0.0', entries: [] }),
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
const agents = listAgents(tempDir);
|
|
43
|
+
expect(agents).toHaveLength(1);
|
|
44
|
+
expect(agents[0].id).toBe('test-agent');
|
|
45
|
+
expect(agents[0].domains).toContain('testing');
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('should skip directories without -mcp package name', () => {
|
|
49
|
+
const dir = join(tempDir, 'not-agent');
|
|
50
|
+
mkdirSync(dir, { recursive: true });
|
|
51
|
+
writeFileSync(join(dir, 'package.json'), JSON.stringify({ name: 'some-lib' }));
|
|
52
|
+
|
|
53
|
+
const agents = listAgents(tempDir);
|
|
54
|
+
expect(agents).toHaveLength(0);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('should detect multiple agents', () => {
|
|
58
|
+
for (const id of ['alpha', 'beta']) {
|
|
59
|
+
const dir = join(tempDir, id);
|
|
60
|
+
mkdirSync(join(dir, 'src', 'intelligence', 'data'), { recursive: true });
|
|
61
|
+
writeFileSync(join(dir, 'package.json'), JSON.stringify({ name: `${id}-mcp` }));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const agents = listAgents(tempDir);
|
|
65
|
+
expect(agents).toHaveLength(2);
|
|
66
|
+
expect(agents.map((a) => a.id).sort()).toEqual(['alpha', 'beta']);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('should report build and deps status', () => {
|
|
70
|
+
const agentDir = join(tempDir, 'built-agent');
|
|
71
|
+
mkdirSync(join(agentDir, 'src', 'intelligence', 'data'), { recursive: true });
|
|
72
|
+
mkdirSync(join(agentDir, 'dist'), { recursive: true });
|
|
73
|
+
mkdirSync(join(agentDir, 'node_modules'), { recursive: true });
|
|
74
|
+
writeFileSync(join(agentDir, 'package.json'), JSON.stringify({ name: 'built-agent-mcp' }));
|
|
75
|
+
|
|
76
|
+
const agents = listAgents(tempDir);
|
|
77
|
+
expect(agents[0].hasDistDir).toBe(true);
|
|
78
|
+
expect(agents[0].hasNodeModules).toBe(true);
|
|
79
|
+
});
|
|
80
|
+
});
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { Command } from 'commander';
|
|
2
|
+
import * as p from '@clack/prompts';
|
|
3
|
+
import { addDomain } from '@soleri/forge/lib';
|
|
4
|
+
import { detectAgent } from '../utils/agent-context.js';
|
|
5
|
+
|
|
6
|
+
export function registerAddDomain(program: Command): void {
|
|
7
|
+
program
|
|
8
|
+
.command('add-domain')
|
|
9
|
+
.argument('<domain>', 'Domain name in kebab-case (e.g., "security")')
|
|
10
|
+
.option('--no-build', 'Skip the build step after adding the domain')
|
|
11
|
+
.description('Add a new knowledge domain to the agent in the current directory')
|
|
12
|
+
.action(async (domain: string, opts: { build: boolean }) => {
|
|
13
|
+
const ctx = detectAgent();
|
|
14
|
+
if (!ctx) {
|
|
15
|
+
p.log.error('No agent project detected in current directory. Run this from an agent root.');
|
|
16
|
+
process.exit(1);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const s = p.spinner();
|
|
20
|
+
s.start(`Adding domain "${domain}" to ${ctx.agentId}...`);
|
|
21
|
+
|
|
22
|
+
try {
|
|
23
|
+
const result = await addDomain({
|
|
24
|
+
agentPath: ctx.agentPath,
|
|
25
|
+
domain,
|
|
26
|
+
noBuild: !opts.build,
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
s.stop(result.success ? result.summary : 'Failed');
|
|
30
|
+
|
|
31
|
+
if (result.warnings.length > 0) {
|
|
32
|
+
for (const w of result.warnings) {
|
|
33
|
+
p.log.warn(w);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (!result.success) {
|
|
38
|
+
process.exit(1);
|
|
39
|
+
}
|
|
40
|
+
} catch (err) {
|
|
41
|
+
s.stop('Failed');
|
|
42
|
+
p.log.error(err instanceof Error ? err.message : String(err));
|
|
43
|
+
process.exit(1);
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { readFileSync, existsSync } from 'node:fs';
|
|
2
|
+
import { resolve } from 'node:path';
|
|
3
|
+
import type { Command } from 'commander';
|
|
4
|
+
import * as p from '@clack/prompts';
|
|
5
|
+
import { previewScaffold, scaffold, AgentConfigSchema } from '@soleri/forge/lib';
|
|
6
|
+
import { runCreateWizard } from '../prompts/create-wizard.js';
|
|
7
|
+
|
|
8
|
+
export function registerCreate(program: Command): void {
|
|
9
|
+
program
|
|
10
|
+
.command('create')
|
|
11
|
+
.argument('[name]', 'Agent ID (kebab-case)')
|
|
12
|
+
.option('-c, --config <path>', 'Path to JSON config file (skip interactive prompts)')
|
|
13
|
+
.description('Create a new Soleri agent')
|
|
14
|
+
.action(async (name?: string, opts?: { config?: string }) => {
|
|
15
|
+
try {
|
|
16
|
+
let config;
|
|
17
|
+
|
|
18
|
+
if (opts?.config) {
|
|
19
|
+
// Non-interactive: read from config file
|
|
20
|
+
const configPath = resolve(opts.config);
|
|
21
|
+
if (!existsSync(configPath)) {
|
|
22
|
+
p.log.error(`Config file not found: ${configPath}`);
|
|
23
|
+
process.exit(1);
|
|
24
|
+
}
|
|
25
|
+
const raw = JSON.parse(readFileSync(configPath, 'utf-8'));
|
|
26
|
+
const parsed = AgentConfigSchema.safeParse(raw);
|
|
27
|
+
if (!parsed.success) {
|
|
28
|
+
p.log.error(`Invalid config: ${parsed.error.message}`);
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
31
|
+
config = parsed.data;
|
|
32
|
+
} else {
|
|
33
|
+
// Interactive wizard
|
|
34
|
+
config = await runCreateWizard(name);
|
|
35
|
+
if (!config) {
|
|
36
|
+
p.outro('Cancelled.');
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Preview
|
|
42
|
+
const preview = previewScaffold(config);
|
|
43
|
+
|
|
44
|
+
p.log.info(`Will create ${preview.files.length} files in ${preview.agentDir}`);
|
|
45
|
+
p.log.info(`Facades: ${preview.facades.map((f) => f.name).join(', ')}`);
|
|
46
|
+
p.log.info(`Domains: ${preview.domains.join(', ')}`);
|
|
47
|
+
|
|
48
|
+
const confirmed = await p.confirm({ message: 'Create agent?' });
|
|
49
|
+
if (p.isCancel(confirmed) || !confirmed) {
|
|
50
|
+
p.outro('Cancelled.');
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Scaffold
|
|
55
|
+
const s = p.spinner();
|
|
56
|
+
s.start('Scaffolding agent...');
|
|
57
|
+
const result = scaffold(config);
|
|
58
|
+
s.stop(result.success ? 'Agent created!' : 'Scaffolding failed');
|
|
59
|
+
|
|
60
|
+
if (result.success) {
|
|
61
|
+
p.note(result.summary, 'Next steps');
|
|
62
|
+
} else {
|
|
63
|
+
p.log.error(result.summary);
|
|
64
|
+
process.exit(1);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
p.outro('Done!');
|
|
68
|
+
} catch (err) {
|
|
69
|
+
p.log.error(err instanceof Error ? err.message : String(err));
|
|
70
|
+
process.exit(1);
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import type { Command } from 'commander';
|
|
3
|
+
import * as p from '@clack/prompts';
|
|
4
|
+
import { detectAgent } from '../utils/agent-context.js';
|
|
5
|
+
|
|
6
|
+
export function registerDev(program: Command): void {
|
|
7
|
+
program
|
|
8
|
+
.command('dev')
|
|
9
|
+
.description('Run the agent in development mode (stdio MCP server)')
|
|
10
|
+
.action(() => {
|
|
11
|
+
const ctx = detectAgent();
|
|
12
|
+
if (!ctx) {
|
|
13
|
+
p.log.error('No agent project detected in current directory. Run this from an agent root.');
|
|
14
|
+
process.exit(1);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
p.log.info(`Starting ${ctx.agentId} in dev mode...`);
|
|
18
|
+
|
|
19
|
+
const child = spawn('npx', ['tsx', 'src/index.ts'], {
|
|
20
|
+
cwd: ctx.agentPath,
|
|
21
|
+
stdio: 'inherit',
|
|
22
|
+
env: { ...process.env },
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
child.on('error', (err) => {
|
|
26
|
+
p.log.error(`Failed to start: ${err.message}`);
|
|
27
|
+
p.log.info('Make sure tsx is available: npm install -g tsx');
|
|
28
|
+
process.exit(1);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
child.on('exit', (code, signal) => {
|
|
32
|
+
if (signal) {
|
|
33
|
+
p.log.warn(`Process terminated by signal ${signal}`);
|
|
34
|
+
process.exit(1);
|
|
35
|
+
}
|
|
36
|
+
process.exit(code ?? 0);
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { Command } from 'commander';
|
|
2
|
+
import { runAllChecks } from '../utils/checks.js';
|
|
3
|
+
import * as log from '../utils/logger.js';
|
|
4
|
+
|
|
5
|
+
export function registerDoctor(program: Command): void {
|
|
6
|
+
program
|
|
7
|
+
.command('doctor')
|
|
8
|
+
.description('Check system health and agent project status')
|
|
9
|
+
.action(() => {
|
|
10
|
+
log.heading('Soleri Doctor');
|
|
11
|
+
|
|
12
|
+
const results = runAllChecks();
|
|
13
|
+
let hasFailures = false;
|
|
14
|
+
|
|
15
|
+
for (const r of results) {
|
|
16
|
+
if (r.status === 'pass') log.pass(r.label, r.detail);
|
|
17
|
+
else if (r.status === 'warn') log.warn(r.label, r.detail);
|
|
18
|
+
else {
|
|
19
|
+
log.fail(r.label, r.detail);
|
|
20
|
+
hasFailures = true;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
console.log();
|
|
25
|
+
|
|
26
|
+
if (hasFailures) {
|
|
27
|
+
log.info('Some checks failed. Fix the issues above and run soleri doctor again.');
|
|
28
|
+
process.exit(1);
|
|
29
|
+
} else {
|
|
30
|
+
log.info('All checks passed!');
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import type { Command } from 'commander';
|
|
2
|
+
import { SUPPORTED_EDITORS, type EditorId } from '../hooks/templates.js';
|
|
3
|
+
import { installHooks, removeHooks, detectInstalledHooks } from '../hooks/generator.js';
|
|
4
|
+
import { detectAgent } from '../utils/agent-context.js';
|
|
5
|
+
import * as log from '../utils/logger.js';
|
|
6
|
+
|
|
7
|
+
export function registerHooks(program: Command): void {
|
|
8
|
+
const hooks = program.command('hooks').description('Manage editor hooks for this agent');
|
|
9
|
+
|
|
10
|
+
hooks
|
|
11
|
+
.command('add')
|
|
12
|
+
.argument('<editor>', `Editor: ${SUPPORTED_EDITORS.join(', ')}`)
|
|
13
|
+
.description('Generate editor hooks/config files')
|
|
14
|
+
.action((editor: string) => {
|
|
15
|
+
if (!isValidEditor(editor)) {
|
|
16
|
+
log.fail(`Unknown editor "${editor}". Supported: ${SUPPORTED_EDITORS.join(', ')}`);
|
|
17
|
+
process.exit(1);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const ctx = detectAgent();
|
|
21
|
+
if (!ctx) {
|
|
22
|
+
log.fail('No agent project detected in current directory.');
|
|
23
|
+
process.exit(1);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const files = installHooks(editor, ctx.agentPath);
|
|
27
|
+
for (const f of files) {
|
|
28
|
+
log.pass(`Created ${f}`);
|
|
29
|
+
}
|
|
30
|
+
log.info(`${editor} hooks installed for ${ctx.agentId}`);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
hooks
|
|
34
|
+
.command('remove')
|
|
35
|
+
.argument('<editor>', `Editor: ${SUPPORTED_EDITORS.join(', ')}`)
|
|
36
|
+
.description('Remove editor hooks/config files')
|
|
37
|
+
.action((editor: string) => {
|
|
38
|
+
if (!isValidEditor(editor)) {
|
|
39
|
+
log.fail(`Unknown editor "${editor}". Supported: ${SUPPORTED_EDITORS.join(', ')}`);
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const ctx = detectAgent();
|
|
44
|
+
if (!ctx) {
|
|
45
|
+
log.fail('No agent project detected in current directory.');
|
|
46
|
+
process.exit(1);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const removed = removeHooks(editor, ctx.agentPath);
|
|
50
|
+
if (removed.length === 0) {
|
|
51
|
+
log.info(`No ${editor} hooks found to remove.`);
|
|
52
|
+
} else {
|
|
53
|
+
for (const f of removed) {
|
|
54
|
+
log.warn(`Removed ${f}`);
|
|
55
|
+
}
|
|
56
|
+
log.info(`${editor} hooks removed from ${ctx.agentId}`);
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
hooks
|
|
61
|
+
.command('list')
|
|
62
|
+
.description('Show which editor hooks are installed')
|
|
63
|
+
.action(() => {
|
|
64
|
+
const ctx = detectAgent();
|
|
65
|
+
if (!ctx) {
|
|
66
|
+
log.fail('No agent project detected in current directory.');
|
|
67
|
+
process.exit(1);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const installed = detectInstalledHooks(ctx.agentPath);
|
|
71
|
+
|
|
72
|
+
log.heading(`Editor hooks for ${ctx.agentId}`);
|
|
73
|
+
|
|
74
|
+
for (const editor of SUPPORTED_EDITORS) {
|
|
75
|
+
if (installed.includes(editor)) {
|
|
76
|
+
log.pass(editor, 'installed');
|
|
77
|
+
} else {
|
|
78
|
+
log.dim(` ${editor} — not installed`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function isValidEditor(editor: string): editor is EditorId {
|
|
85
|
+
return (SUPPORTED_EDITORS as readonly string[]).includes(editor);
|
|
86
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { resolve } from 'node:path';
|
|
2
|
+
import type { Command } from 'commander';
|
|
3
|
+
import * as p from '@clack/prompts';
|
|
4
|
+
import { installKnowledge } from '@soleri/forge/lib';
|
|
5
|
+
import { detectAgent } from '../utils/agent-context.js';
|
|
6
|
+
|
|
7
|
+
export function registerInstallKnowledge(program: Command): void {
|
|
8
|
+
program
|
|
9
|
+
.command('install-knowledge')
|
|
10
|
+
.argument('<pack>', 'Path to knowledge bundle file or directory')
|
|
11
|
+
.option('--no-facades', 'Skip facade generation for new domains')
|
|
12
|
+
.description('Install knowledge packs into the agent in the current directory')
|
|
13
|
+
.action(async (pack: string, opts: { facades: boolean }) => {
|
|
14
|
+
const ctx = detectAgent();
|
|
15
|
+
if (!ctx) {
|
|
16
|
+
p.log.error('No agent project detected in current directory. Run this from an agent root.');
|
|
17
|
+
process.exit(1);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const bundlePath = resolve(pack);
|
|
21
|
+
|
|
22
|
+
const s = p.spinner();
|
|
23
|
+
s.start(`Installing knowledge from ${bundlePath}...`);
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
const result = await installKnowledge({
|
|
27
|
+
agentPath: ctx.agentPath,
|
|
28
|
+
bundlePath,
|
|
29
|
+
generateFacades: opts.facades,
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
s.stop(result.success ? result.summary : 'Installation failed');
|
|
33
|
+
|
|
34
|
+
if (result.warnings.length > 0) {
|
|
35
|
+
for (const w of result.warnings) {
|
|
36
|
+
p.log.warn(w);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (!result.success) {
|
|
41
|
+
process.exit(1);
|
|
42
|
+
}
|
|
43
|
+
} catch (err) {
|
|
44
|
+
s.stop('Installation failed');
|
|
45
|
+
p.log.error(err instanceof Error ? err.message : String(err));
|
|
46
|
+
process.exit(1);
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { resolve } from 'node:path';
|
|
2
|
+
import type { Command } from 'commander';
|
|
3
|
+
import { listAgents } from '@soleri/forge/lib';
|
|
4
|
+
import * as log from '../utils/logger.js';
|
|
5
|
+
|
|
6
|
+
function pad(s: string, len: number): string {
|
|
7
|
+
return s.length >= len ? s.slice(0, len) : s + ' '.repeat(len - s.length);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function registerList(program: Command): void {
|
|
11
|
+
program
|
|
12
|
+
.command('list')
|
|
13
|
+
.argument('[dir]', 'Directory to scan for agents', process.cwd())
|
|
14
|
+
.description('List all Soleri agents in a directory')
|
|
15
|
+
.action((dir: string) => {
|
|
16
|
+
const targetDir = resolve(dir);
|
|
17
|
+
const agents = listAgents(targetDir);
|
|
18
|
+
|
|
19
|
+
if (agents.length === 0) {
|
|
20
|
+
log.info(`No agents found in ${targetDir}`);
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
log.heading(`Agents in ${targetDir}`);
|
|
25
|
+
|
|
26
|
+
// Table header
|
|
27
|
+
console.log(` ${pad('ID', 16)}${pad('Domains', 26)}${pad('Built', 8)}${pad('Deps', 8)}Path`);
|
|
28
|
+
console.log(' ' + '-'.repeat(80));
|
|
29
|
+
|
|
30
|
+
for (const agent of agents) {
|
|
31
|
+
const built = agent.hasDistDir ? '✓' : '✗';
|
|
32
|
+
const deps = agent.hasNodeModules ? '✓' : '✗';
|
|
33
|
+
const domains = agent.domains.join(', ') || '(none)';
|
|
34
|
+
const truncDomains = domains.length > 25 ? domains.slice(0, 22) + '...' : domains;
|
|
35
|
+
console.log(
|
|
36
|
+
` ${pad(agent.id, 16)}${pad(truncDomains, 26)}${pad(built, 8)}${pad(deps, 8)}${agent.path}`,
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
console.log(`\n ${agents.length} agent(s) found`);
|
|
41
|
+
});
|
|
42
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hook file generator — writes and removes editor hook files.
|
|
3
|
+
*/
|
|
4
|
+
import { existsSync, writeFileSync, unlinkSync, mkdirSync } from 'node:fs';
|
|
5
|
+
import { join, dirname } from 'node:path';
|
|
6
|
+
import { getEditorFiles, SUPPORTED_EDITORS, type EditorId } from './templates.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Install editor hooks for the given editor.
|
|
10
|
+
* Returns list of files written.
|
|
11
|
+
*/
|
|
12
|
+
export function installHooks(editor: EditorId, agentPath: string): string[] {
|
|
13
|
+
const files = getEditorFiles(editor, agentPath);
|
|
14
|
+
const written: string[] = [];
|
|
15
|
+
|
|
16
|
+
const overwritten: string[] = [];
|
|
17
|
+
for (const [relPath, content] of Object.entries(files)) {
|
|
18
|
+
const absPath = join(agentPath, relPath);
|
|
19
|
+
if (existsSync(absPath)) overwritten.push(relPath);
|
|
20
|
+
mkdirSync(dirname(absPath), { recursive: true });
|
|
21
|
+
writeFileSync(absPath, content, 'utf-8');
|
|
22
|
+
written.push(relPath);
|
|
23
|
+
}
|
|
24
|
+
if (overwritten.length > 0) {
|
|
25
|
+
console.warn(`Warning: overwritten existing file(s): ${overwritten.join(', ')}`);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return written;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Remove editor hooks for the given editor.
|
|
33
|
+
* Returns list of files removed.
|
|
34
|
+
*/
|
|
35
|
+
export function removeHooks(editor: EditorId, agentPath: string): string[] {
|
|
36
|
+
const files = getEditorFiles(editor, agentPath);
|
|
37
|
+
const removed: string[] = [];
|
|
38
|
+
|
|
39
|
+
for (const relPath of Object.keys(files)) {
|
|
40
|
+
const absPath = join(agentPath, relPath);
|
|
41
|
+
if (existsSync(absPath)) {
|
|
42
|
+
unlinkSync(absPath);
|
|
43
|
+
removed.push(relPath);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return removed;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Detect which editors have hooks installed.
|
|
52
|
+
*/
|
|
53
|
+
export function detectInstalledHooks(agentPath: string): EditorId[] {
|
|
54
|
+
const installed: EditorId[] = [];
|
|
55
|
+
|
|
56
|
+
for (const editor of SUPPORTED_EDITORS) {
|
|
57
|
+
const files = getEditorFiles(editor, agentPath);
|
|
58
|
+
const allExist = Object.keys(files).every((relPath) => existsSync(join(agentPath, relPath)));
|
|
59
|
+
if (allExist) {
|
|
60
|
+
installed.push(editor);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return installed;
|
|
65
|
+
}
|