@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.
Files changed (37) hide show
  1. package/README.md +625 -0
  2. package/bin/porchestra.js +2 -0
  3. package/package.json +51 -0
  4. package/src/agents/testPrompt/ast.json +71 -0
  5. package/src/agents/testPrompt/config.ts +18 -0
  6. package/src/agents/testPrompt/index.ts +64 -0
  7. package/src/agents/testPrompt/schemas.ts +45 -0
  8. package/src/agents/testPrompt/tools.ts +88 -0
  9. package/src/commands/agents.ts +173 -0
  10. package/src/commands/config.ts +97 -0
  11. package/src/commands/explore.ts +160 -0
  12. package/src/commands/login.ts +101 -0
  13. package/src/commands/logout.ts +52 -0
  14. package/src/commands/pull.ts +220 -0
  15. package/src/commands/status.ts +78 -0
  16. package/src/commands/whoami.ts +56 -0
  17. package/src/core/api/client.ts +133 -0
  18. package/src/core/auth/auth-service.ts +176 -0
  19. package/src/core/auth/token-manager.ts +47 -0
  20. package/src/core/config/config-manager.ts +107 -0
  21. package/src/core/config/config-schema.ts +56 -0
  22. package/src/core/config/project-tracker.ts +158 -0
  23. package/src/core/generators/code-generator.ts +329 -0
  24. package/src/core/generators/schema-generator.ts +59 -0
  25. package/src/index.ts +85 -0
  26. package/src/types/index.ts +214 -0
  27. package/src/utils/date.ts +23 -0
  28. package/src/utils/errors.ts +38 -0
  29. package/src/utils/logger.ts +11 -0
  30. package/src/utils/path-utils.ts +47 -0
  31. package/tests/unit/config-manager.test.ts +74 -0
  32. package/tests/unit/config-schema.test.ts +61 -0
  33. package/tests/unit/path-utils.test.ts +53 -0
  34. package/tests/unit/schema-generator.test.ts +82 -0
  35. package/tsconfig.json +30 -0
  36. package/tsup.config.ts +19 -0
  37. 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
+ });
@@ -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
+ });