@porchestra/cli 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 +625 -0
- package/bin/porchestra.js +2 -0
- package/package.json +51 -0
- package/src/agents/testPrompt/ast.json +71 -0
- package/src/agents/testPrompt/config.ts +18 -0
- package/src/agents/testPrompt/index.ts +64 -0
- package/src/agents/testPrompt/schemas.ts +45 -0
- package/src/agents/testPrompt/tools.ts +88 -0
- package/src/commands/agents.ts +173 -0
- package/src/commands/config.ts +97 -0
- package/src/commands/explore.ts +160 -0
- package/src/commands/login.ts +101 -0
- package/src/commands/logout.ts +52 -0
- package/src/commands/pull.ts +220 -0
- package/src/commands/status.ts +78 -0
- package/src/commands/whoami.ts +56 -0
- package/src/core/api/client.ts +133 -0
- package/src/core/auth/auth-service.ts +176 -0
- package/src/core/auth/token-manager.ts +47 -0
- package/src/core/config/config-manager.ts +107 -0
- package/src/core/config/config-schema.ts +56 -0
- package/src/core/config/project-tracker.ts +158 -0
- package/src/core/generators/code-generator.ts +329 -0
- package/src/core/generators/schema-generator.ts +59 -0
- package/src/index.ts +85 -0
- package/src/types/index.ts +214 -0
- package/src/utils/date.ts +23 -0
- package/src/utils/errors.ts +38 -0
- package/src/utils/logger.ts +11 -0
- package/src/utils/path-utils.ts +47 -0
- package/tests/unit/config-manager.test.ts +74 -0
- package/tests/unit/config-schema.test.ts +61 -0
- package/tests/unit/path-utils.test.ts +53 -0
- package/tests/unit/schema-generator.test.ts +82 -0
- package/tsconfig.json +30 -0
- package/tsup.config.ts +19 -0
- package/vitest.config.ts +20 -0
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export class PorchestraError extends Error {
|
|
2
|
+
constructor(message: string, public code: string) {
|
|
3
|
+
super(message);
|
|
4
|
+
this.name = 'PorchestraError';
|
|
5
|
+
}
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export class AuthenticationError extends PorchestraError {
|
|
9
|
+
constructor(message: string, public retryable = false) {
|
|
10
|
+
super(message, 'AUTH_ERROR');
|
|
11
|
+
this.name = 'AuthenticationError';
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export class NetworkError extends PorchestraError {
|
|
16
|
+
constructor(
|
|
17
|
+
message: string,
|
|
18
|
+
public statusCode?: number,
|
|
19
|
+
public retryable = true
|
|
20
|
+
) {
|
|
21
|
+
super(message, 'NETWORK_ERROR');
|
|
22
|
+
this.name = 'NetworkError';
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export class ValidationError extends PorchestraError {
|
|
27
|
+
constructor(message: string, public field?: string) {
|
|
28
|
+
super(message, 'VALIDATION_ERROR');
|
|
29
|
+
this.name = 'ValidationError';
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export class FileSystemError extends PorchestraError {
|
|
34
|
+
constructor(message: string, public path: string) {
|
|
35
|
+
super(message, 'FS_ERROR');
|
|
36
|
+
this.name = 'FileSystemError';
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import pc from 'picocolors';
|
|
2
|
+
|
|
3
|
+
export const logger = {
|
|
4
|
+
info: (msg: string) => console.log(msg),
|
|
5
|
+
success: (msg: string) => console.log(pc.green(msg)),
|
|
6
|
+
error: (msg: string) => console.error(pc.red(msg)),
|
|
7
|
+
warn: (msg: string) => console.log(pc.yellow(msg)),
|
|
8
|
+
gray: (msg: string) => console.log(pc.gray(msg)),
|
|
9
|
+
bold: (msg: string) => console.log(pc.bold(msg)),
|
|
10
|
+
cyan: (msg: string) => console.log(pc.cyan(msg)),
|
|
11
|
+
};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import { promises as fs } from 'fs';
|
|
3
|
+
|
|
4
|
+
export function normalizeFolderPath(folderPath: string): string {
|
|
5
|
+
let normalized = folderPath.replace(/^\//, '');
|
|
6
|
+
normalized = normalized.replace(/\/+/g, '/');
|
|
7
|
+
normalized = normalized
|
|
8
|
+
.split('/')
|
|
9
|
+
.map(segment => sanitizePathSegment(segment))
|
|
10
|
+
.join('/');
|
|
11
|
+
normalized = normalized.replace(/\/$/, '');
|
|
12
|
+
return normalized;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function sanitizePathSegment(segment: string): string {
|
|
16
|
+
return segment
|
|
17
|
+
.replace(/[^a-zA-Z0-9-_]/g, '-')
|
|
18
|
+
.replace(/^-+|-+$/g, '')
|
|
19
|
+
.replace(/-+/g, '-');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function calculateOutputPath(
|
|
23
|
+
baseDir: string,
|
|
24
|
+
folderPath: string,
|
|
25
|
+
filename?: string
|
|
26
|
+
): string {
|
|
27
|
+
const normalized = normalizeFolderPath(folderPath);
|
|
28
|
+
const fullPath = path.join(baseDir, normalized);
|
|
29
|
+
return filename ? path.join(fullPath, filename) : fullPath;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function calculateAgentOutputPath(
|
|
33
|
+
baseDir: string,
|
|
34
|
+
folderPath: string,
|
|
35
|
+
agentName: string,
|
|
36
|
+
filename?: string
|
|
37
|
+
): string {
|
|
38
|
+
const normalizedFolder = normalizeFolderPath(folderPath);
|
|
39
|
+
const normalizedAgent = sanitizePathSegment(agentName);
|
|
40
|
+
const fullPath = path.join(baseDir, normalizedFolder, normalizedAgent);
|
|
41
|
+
return filename ? path.join(fullPath, filename) : fullPath;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export async function ensureDirectory(filePath: string): Promise<void> {
|
|
45
|
+
const dir = path.dirname(filePath);
|
|
46
|
+
await fs.mkdir(dir, { recursive: true });
|
|
47
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { promises as fs } from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import os from 'os';
|
|
5
|
+
import { ConfigManager } from '../../src/core/config/config-manager.js';
|
|
6
|
+
|
|
7
|
+
describe('ConfigManager', () => {
|
|
8
|
+
let tempDir: string;
|
|
9
|
+
let configManager: ConfigManager;
|
|
10
|
+
|
|
11
|
+
beforeEach(async () => {
|
|
12
|
+
tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'porchestra-test-'));
|
|
13
|
+
process.env.PORCHESTRA_CONFIG_DIR = tempDir;
|
|
14
|
+
configManager = new ConfigManager();
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
afterEach(async () => {
|
|
18
|
+
await fs.rm(tempDir, { recursive: true, force: true });
|
|
19
|
+
delete process.env.PORCHESTRA_CONFIG_DIR;
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('should create default config when none exists', async () => {
|
|
23
|
+
const config = await configManager.load();
|
|
24
|
+
expect(config.version).toBe('1.0.0');
|
|
25
|
+
expect(config.trackedProjects).toEqual([]);
|
|
26
|
+
expect(config.api?.baseUrl).toBe('https://api.porchestra.io/v1');
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('should save and load config', async () => {
|
|
30
|
+
const config = await configManager.load();
|
|
31
|
+
config.auth = {
|
|
32
|
+
token: 'test-token',
|
|
33
|
+
tokenId: '550e8400-e29b-41d4-a716-446655440000',
|
|
34
|
+
expiresAt: '2025-12-31T23:59:59Z',
|
|
35
|
+
deviceName: 'Test Device'
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
await configManager.save(config);
|
|
39
|
+
|
|
40
|
+
// Create new manager to test loading from disk
|
|
41
|
+
const newManager = new ConfigManager();
|
|
42
|
+
const loaded = await newManager.load();
|
|
43
|
+
|
|
44
|
+
expect(loaded.auth?.token).toBe('test-token');
|
|
45
|
+
expect(loaded.auth?.deviceName).toBe('Test Device');
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('should update config with updater function', async () => {
|
|
49
|
+
await configManager.load();
|
|
50
|
+
|
|
51
|
+
await configManager.update((cfg) => ({
|
|
52
|
+
...cfg,
|
|
53
|
+
api: {
|
|
54
|
+
...cfg.api,
|
|
55
|
+
baseUrl: 'https://custom.api.com/v1'
|
|
56
|
+
}
|
|
57
|
+
}));
|
|
58
|
+
|
|
59
|
+
const config = await configManager.get();
|
|
60
|
+
expect(config.api?.baseUrl).toBe('https://custom.api.com/v1');
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('should clear config', async () => {
|
|
64
|
+
const config = await configManager.load();
|
|
65
|
+
config.auth = { token: 'test' };
|
|
66
|
+
await configManager.save(config);
|
|
67
|
+
|
|
68
|
+
await configManager.clear();
|
|
69
|
+
|
|
70
|
+
// After clear, should return default config
|
|
71
|
+
const newConfig = await configManager.load();
|
|
72
|
+
expect(newConfig.auth).toBeUndefined();
|
|
73
|
+
});
|
|
74
|
+
});
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { CliConfigSchema, TrackedProjectSchema } from '../../src/core/config/config-schema.js';
|
|
3
|
+
|
|
4
|
+
describe('config-schema', () => {
|
|
5
|
+
it('should validate a valid config', () => {
|
|
6
|
+
const validConfig = {
|
|
7
|
+
version: '1.0.0',
|
|
8
|
+
auth: {
|
|
9
|
+
token: 'test-token',
|
|
10
|
+
tokenId: '550e8400-e29b-41d4-a716-446655440000',
|
|
11
|
+
expiresAt: '2025-12-31T23:59:59Z',
|
|
12
|
+
deviceName: 'Test Device'
|
|
13
|
+
},
|
|
14
|
+
api: {
|
|
15
|
+
baseUrl: 'https://api.porchestra.io/v1',
|
|
16
|
+
skipTlsVerify: false
|
|
17
|
+
},
|
|
18
|
+
trackedProjects: [],
|
|
19
|
+
output: {
|
|
20
|
+
baseDir: './src/agents',
|
|
21
|
+
createIndexFiles: true
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const result = CliConfigSchema.safeParse(validConfig);
|
|
26
|
+
expect(result.success).toBe(true);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('should apply defaults for missing fields', () => {
|
|
30
|
+
const minimalConfig = {
|
|
31
|
+
version: '1.0.0'
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const result = CliConfigSchema.parse(minimalConfig);
|
|
35
|
+
expect(result.api?.baseUrl).toBe('https://api.porchestra.io/v1');
|
|
36
|
+
expect(result.output?.baseDir).toBe('./src/agents');
|
|
37
|
+
expect(result.trackedProjects).toEqual([]);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('should reject invalid version', () => {
|
|
41
|
+
const invalidConfig = {
|
|
42
|
+
version: '2.0.0'
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const result = CliConfigSchema.safeParse(invalidConfig);
|
|
46
|
+
expect(result.success).toBe(false);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('should validate tracked project schema', () => {
|
|
50
|
+
const validProject = {
|
|
51
|
+
projectId: '550e8400-e29b-41d4-a716-446655440000',
|
|
52
|
+
projectName: 'Test Project',
|
|
53
|
+
projectSlug: 'test-project',
|
|
54
|
+
selectedAt: '2025-01-30T12:00:00Z',
|
|
55
|
+
agents: []
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const result = TrackedProjectSchema.safeParse(validProject);
|
|
59
|
+
expect(result.success).toBe(true);
|
|
60
|
+
});
|
|
61
|
+
});
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
normalizeFolderPath,
|
|
4
|
+
calculateOutputPath,
|
|
5
|
+
} from '../../src/utils/path-utils.js';
|
|
6
|
+
|
|
7
|
+
describe('path-utils', () => {
|
|
8
|
+
describe('normalizeFolderPath', () => {
|
|
9
|
+
it('should remove leading slash', () => {
|
|
10
|
+
expect(normalizeFolderPath('/main/x-agent/')).toBe('main/x-agent');
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it('should handle paths without leading slash', () => {
|
|
14
|
+
expect(normalizeFolderPath('y-agent')).toBe('y-agent');
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('should replace multiple slashes', () => {
|
|
18
|
+
expect(normalizeFolderPath('//legacy//agents//')).toBe('/legacy/agents');
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('should sanitize special characters', () => {
|
|
22
|
+
expect(normalizeFolderPath('My Agent (v2)')).toBe('My-Agent-v2');
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('should handle empty path', () => {
|
|
26
|
+
expect(normalizeFolderPath('/')).toBe('');
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('should prevent directory traversal', () => {
|
|
30
|
+
expect(normalizeFolderPath('../malicious')).toBe('/malicious');
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
describe('calculateOutputPath', () => {
|
|
35
|
+
it('should calculate correct output path', () => {
|
|
36
|
+
const result = calculateOutputPath(
|
|
37
|
+
'./src/agents',
|
|
38
|
+
'main/x-agent',
|
|
39
|
+
'tool-schemas.ts'
|
|
40
|
+
);
|
|
41
|
+
expect(result).toMatch(/src[\\/]agents[\\/]main[\\/]x-agent[\\/]tool-schemas\.ts/);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('should handle empty folder path', () => {
|
|
45
|
+
const result = calculateOutputPath(
|
|
46
|
+
'./src/agents',
|
|
47
|
+
'/',
|
|
48
|
+
'tool-schemas.ts'
|
|
49
|
+
);
|
|
50
|
+
expect(result).toMatch(/src[\\/]agents[\\/]tool-schemas\.ts$/);
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
});
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { jsonSchemaToZod } from '../../src/core/generators/schema-generator.js';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
|
|
5
|
+
describe('schema-generator', () => {
|
|
6
|
+
it('should convert string schema', () => {
|
|
7
|
+
const schema = { type: 'string' };
|
|
8
|
+
const result = jsonSchemaToZod(schema);
|
|
9
|
+
expect(() => result.parse('hello')).not.toThrow();
|
|
10
|
+
expect(() => result.parse(123)).toThrow();
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it('should convert number schema', () => {
|
|
14
|
+
const schema = { type: 'number' };
|
|
15
|
+
const result = jsonSchemaToZod(schema);
|
|
16
|
+
expect(() => result.parse(42)).not.toThrow();
|
|
17
|
+
expect(() => result.parse('hello')).toThrow();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('should convert boolean schema', () => {
|
|
21
|
+
const schema = { type: 'boolean' };
|
|
22
|
+
const result = jsonSchemaToZod(schema);
|
|
23
|
+
expect(() => result.parse(true)).not.toThrow();
|
|
24
|
+
expect(() => result.parse('true')).toThrow();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('should convert array schema', () => {
|
|
28
|
+
const schema = {
|
|
29
|
+
type: 'array',
|
|
30
|
+
items: { type: 'string' }
|
|
31
|
+
};
|
|
32
|
+
const result = jsonSchemaToZod(schema);
|
|
33
|
+
expect(() => result.parse(['a', 'b'])).not.toThrow();
|
|
34
|
+
expect(() => result.parse([1, 2])).toThrow();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('should convert object schema', () => {
|
|
38
|
+
const schema = {
|
|
39
|
+
type: 'object',
|
|
40
|
+
properties: {
|
|
41
|
+
name: { type: 'string' },
|
|
42
|
+
age: { type: 'number' }
|
|
43
|
+
},
|
|
44
|
+
required: ['name']
|
|
45
|
+
};
|
|
46
|
+
const result = jsonSchemaToZod(schema);
|
|
47
|
+
expect(() => result.parse({ name: 'John' })).not.toThrow();
|
|
48
|
+
expect(() => result.parse({ age: 30 })).toThrow();
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('should handle enum strings', () => {
|
|
52
|
+
const schema = {
|
|
53
|
+
type: 'string',
|
|
54
|
+
enum: ['red', 'green', 'blue']
|
|
55
|
+
};
|
|
56
|
+
const result = jsonSchemaToZod(schema);
|
|
57
|
+
expect(() => result.parse('red')).not.toThrow();
|
|
58
|
+
expect(() => result.parse('yellow')).toThrow();
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('should handle nullable types', () => {
|
|
62
|
+
const schema = {
|
|
63
|
+
type: 'string',
|
|
64
|
+
nullable: true
|
|
65
|
+
};
|
|
66
|
+
const result = jsonSchemaToZod(schema);
|
|
67
|
+
expect(() => result.parse('hello')).not.toThrow();
|
|
68
|
+
expect(() => result.parse(null)).not.toThrow();
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('should handle min/max constraints', () => {
|
|
72
|
+
const schema = {
|
|
73
|
+
type: 'string',
|
|
74
|
+
minLength: 2,
|
|
75
|
+
maxLength: 10
|
|
76
|
+
};
|
|
77
|
+
const result = jsonSchemaToZod(schema);
|
|
78
|
+
expect(() => result.parse('hi')).not.toThrow();
|
|
79
|
+
expect(() => result.parse('a')).toThrow();
|
|
80
|
+
expect(() => result.parse('a'.repeat(20))).toThrow();
|
|
81
|
+
});
|
|
82
|
+
});
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "bundler",
|
|
6
|
+
"lib": ["ES2022"],
|
|
7
|
+
"paths": {
|
|
8
|
+
"@porchestra/core": ["../core/src/index.ts"],
|
|
9
|
+
"@porchestra/core/*": ["../core/src/*"]
|
|
10
|
+
},
|
|
11
|
+
"outDir": "./dist",
|
|
12
|
+
"rootDir": ".",
|
|
13
|
+
"strict": true,
|
|
14
|
+
"esModuleInterop": true,
|
|
15
|
+
"skipLibCheck": true,
|
|
16
|
+
"forceConsistentCasingInFileNames": true,
|
|
17
|
+
"resolveJsonModule": true,
|
|
18
|
+
"declaration": true,
|
|
19
|
+
"declarationMap": true,
|
|
20
|
+
"sourceMap": true,
|
|
21
|
+
"noUnusedLocals": true,
|
|
22
|
+
"noUnusedParameters": true,
|
|
23
|
+
"noImplicitReturns": true,
|
|
24
|
+
"noFallthroughCasesInSwitch": true,
|
|
25
|
+
"isolatedModules": true,
|
|
26
|
+
"allowSyntheticDefaultImports": true
|
|
27
|
+
},
|
|
28
|
+
"include": ["src/**/*"],
|
|
29
|
+
"exclude": ["node_modules", "dist", "tests", "../**/*"]
|
|
30
|
+
}
|
package/tsup.config.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { defineConfig } from 'tsup';
|
|
2
|
+
|
|
3
|
+
export default defineConfig({
|
|
4
|
+
entry: ['src/index.ts'],
|
|
5
|
+
format: ['esm'],
|
|
6
|
+
target: 'node22',
|
|
7
|
+
platform: 'node',
|
|
8
|
+
bundle: true,
|
|
9
|
+
splitting: false,
|
|
10
|
+
sourcemap: true,
|
|
11
|
+
clean: true,
|
|
12
|
+
minify: false,
|
|
13
|
+
outDir: 'dist',
|
|
14
|
+
// No banner - shebang is in bin/porchestra.js instead
|
|
15
|
+
external: [],
|
|
16
|
+
loader: {
|
|
17
|
+
'.hbs': 'text'
|
|
18
|
+
}
|
|
19
|
+
});
|
package/vitest.config.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { defineConfig } from 'vitest/config';
|
|
2
|
+
|
|
3
|
+
export default defineConfig({
|
|
4
|
+
test: {
|
|
5
|
+
globals: true,
|
|
6
|
+
environment: 'node',
|
|
7
|
+
include: ['tests/**/*.{test,spec}.{js,ts}'],
|
|
8
|
+
coverage: {
|
|
9
|
+
provider: 'v8',
|
|
10
|
+
reporter: ['text', 'json', 'html'],
|
|
11
|
+
exclude: [
|
|
12
|
+
'node_modules/',
|
|
13
|
+
'dist/',
|
|
14
|
+
'tests/',
|
|
15
|
+
'**/*.d.ts',
|
|
16
|
+
'**/*.config.{js,ts}'
|
|
17
|
+
]
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
});
|