@porchestra/cli 1.0.0 → 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/bin/porchestra.js +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1602 -0
- package/dist/index.js.map +1 -0
- package/package.json +11 -1
- package/src/agents/testPrompt/ast.json +0 -71
- package/src/agents/testPrompt/config.ts +0 -18
- package/src/agents/testPrompt/index.ts +0 -64
- package/src/agents/testPrompt/schemas.ts +0 -45
- package/src/agents/testPrompt/tools.ts +0 -88
- package/src/commands/agents.ts +0 -173
- package/src/commands/config.ts +0 -97
- package/src/commands/explore.ts +0 -160
- package/src/commands/login.ts +0 -101
- package/src/commands/logout.ts +0 -52
- package/src/commands/pull.ts +0 -220
- package/src/commands/status.ts +0 -78
- package/src/commands/whoami.ts +0 -56
- package/src/core/api/client.ts +0 -133
- package/src/core/auth/auth-service.ts +0 -176
- package/src/core/auth/token-manager.ts +0 -47
- package/src/core/config/config-manager.ts +0 -107
- package/src/core/config/config-schema.ts +0 -56
- package/src/core/config/project-tracker.ts +0 -158
- package/src/core/generators/code-generator.ts +0 -329
- package/src/core/generators/schema-generator.ts +0 -59
- package/src/index.ts +0 -85
- package/src/types/index.ts +0 -214
- package/src/utils/date.ts +0 -23
- package/src/utils/errors.ts +0 -38
- package/src/utils/logger.ts +0 -11
- package/src/utils/path-utils.ts +0 -47
- package/tests/unit/config-manager.test.ts +0 -74
- package/tests/unit/config-schema.test.ts +0 -61
- package/tests/unit/path-utils.test.ts +0 -53
- package/tests/unit/schema-generator.test.ts +0 -82
- package/tsconfig.json +0 -30
- package/tsup.config.ts +0 -19
- package/vitest.config.ts +0 -20
package/src/commands/pull.ts
DELETED
|
@@ -1,220 +0,0 @@
|
|
|
1
|
-
import { Command } from 'commander';
|
|
2
|
-
import path from 'path';
|
|
3
|
-
import pc from 'picocolors';
|
|
4
|
-
import ora from 'ora';
|
|
5
|
-
import { ApiClient } from '../core/api/client.js';
|
|
6
|
-
import { ConfigManager } from '../core/config/config-manager.js';
|
|
7
|
-
import { CodeGenerator } from '../core/generators/code-generator.js';
|
|
8
|
-
import { ProjectTracker } from '../core/config/project-tracker.js';
|
|
9
|
-
|
|
10
|
-
interface PullOptions {
|
|
11
|
-
project?: string;
|
|
12
|
-
agent?: string;
|
|
13
|
-
version?: string;
|
|
14
|
-
env?: 'PRODUCTION' | 'STAGING' | 'DEVELOPMENT';
|
|
15
|
-
output?: string;
|
|
16
|
-
force?: boolean;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export function createPullCommand(
|
|
20
|
-
configManager: ConfigManager,
|
|
21
|
-
apiClient: ApiClient,
|
|
22
|
-
codeGenerator: CodeGenerator
|
|
23
|
-
): Command {
|
|
24
|
-
const projectTracker = new ProjectTracker(configManager);
|
|
25
|
-
|
|
26
|
-
return new Command('pull')
|
|
27
|
-
.description('Generate tool code for tracked agents')
|
|
28
|
-
.option('-p, --project <id>', 'Pull specific project only')
|
|
29
|
-
.option('-a, --agent <id>', 'Pull specific agent only')
|
|
30
|
-
.option('-e, --env <environment>', 'Target environment (production|staging|development)')
|
|
31
|
-
.option('-o, --output <path>', 'Override output directory')
|
|
32
|
-
.option('--force', 'Overwrite implementation files (WARNING: may lose code)')
|
|
33
|
-
.action(async (options: PullOptions) => {
|
|
34
|
-
// Check if logged in
|
|
35
|
-
const config = await configManager.get();
|
|
36
|
-
if (!config.auth?.token) {
|
|
37
|
-
console.log(pc.red('Not logged in. Run `porchestra login` first.'));
|
|
38
|
-
process.exit(1);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
// Get tracked projects
|
|
42
|
-
let trackedProjects = await projectTracker.getTrackedProjects();
|
|
43
|
-
|
|
44
|
-
// Auto-trigger explore if no projects tracked
|
|
45
|
-
if (trackedProjects.length === 0) {
|
|
46
|
-
console.log(pc.yellow('\n⚠ No projects selected for tracking.\n'));
|
|
47
|
-
console.log(pc.gray('Starting project explorer...\n'));
|
|
48
|
-
|
|
49
|
-
// Import and run explore dynamically
|
|
50
|
-
const { createExploreCommand } = await import('./explore.js');
|
|
51
|
-
const exploreCmd = createExploreCommand(configManager, apiClient);
|
|
52
|
-
// Execute the explore command to let user select projects
|
|
53
|
-
await exploreCmd.parseAsync(['node', 'script', 'explore']);
|
|
54
|
-
|
|
55
|
-
// Re-check tracking after explore
|
|
56
|
-
trackedProjects = await projectTracker.getTrackedProjects();
|
|
57
|
-
|
|
58
|
-
if (trackedProjects.length === 0) {
|
|
59
|
-
console.log(pc.red('\n❌ No projects selected. Run `porchestra explore` to select projects.'));
|
|
60
|
-
process.exit(1);
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
// Filter by options
|
|
65
|
-
if (options.project) {
|
|
66
|
-
trackedProjects = trackedProjects.filter(
|
|
67
|
-
p => p.projectId === options.project || p.projectSlug === options.project
|
|
68
|
-
);
|
|
69
|
-
if (trackedProjects.length === 0) {
|
|
70
|
-
console.log(pc.red(`Project "${options.project}" not found in tracked projects`));
|
|
71
|
-
process.exit(1);
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
// Pull each project
|
|
76
|
-
const baseOutputDir = options.output || config.output?.baseDir || path.resolve(process.cwd(), 'src/porchestra/agents');
|
|
77
|
-
let totalAgents = 0;
|
|
78
|
-
let successCount = 0;
|
|
79
|
-
|
|
80
|
-
for (const project of trackedProjects) {
|
|
81
|
-
// Filter agents if specified
|
|
82
|
-
let agents = project.agents;
|
|
83
|
-
if (options.agent) {
|
|
84
|
-
agents = agents.filter(
|
|
85
|
-
a => a.agentId === options.agent || a.agentSlug === options.agent
|
|
86
|
-
);
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
if (agents.length === 0) continue;
|
|
90
|
-
|
|
91
|
-
console.log(pc.bold(`\n📦 ${project.projectName}\n`));
|
|
92
|
-
totalAgents += agents.length;
|
|
93
|
-
|
|
94
|
-
// Process each agent with progress
|
|
95
|
-
for (let i = 0; i < agents.length; i++) {
|
|
96
|
-
const agent = agents[i];
|
|
97
|
-
const progress = `[${i + 1}/${agents.length}]`;
|
|
98
|
-
const spinner = ora(`${progress} Fetching ${agent.agentName}...`).start();
|
|
99
|
-
|
|
100
|
-
try {
|
|
101
|
-
// Fetch agent tools
|
|
102
|
-
const toolsResponse = await apiClient.getAgentTools(
|
|
103
|
-
project.projectId,
|
|
104
|
-
agent.agentId,
|
|
105
|
-
options.env
|
|
106
|
-
);
|
|
107
|
-
|
|
108
|
-
spinner.text = `${progress} Generating code...`;
|
|
109
|
-
|
|
110
|
-
const normalizeToolset = (toolset: any[] = []) =>
|
|
111
|
-
toolset.map((t, idx) => ({
|
|
112
|
-
id: t.id ?? `toolset-${idx}`,
|
|
113
|
-
name: t.name,
|
|
114
|
-
description: t.description ?? '',
|
|
115
|
-
parameters: t.parameters ?? {},
|
|
116
|
-
returns: t.returns ?? null,
|
|
117
|
-
isBuiltin: t.isBuiltin ?? false,
|
|
118
|
-
builtinType: t.builtinType,
|
|
119
|
-
}));
|
|
120
|
-
|
|
121
|
-
const rawToolConfig = toolsResponse?.components?.toolConfig?.content as any;
|
|
122
|
-
const parsedToolConfig = (() => {
|
|
123
|
-
if (typeof rawToolConfig === 'string') {
|
|
124
|
-
try {
|
|
125
|
-
return JSON.parse(rawToolConfig);
|
|
126
|
-
} catch {
|
|
127
|
-
return undefined;
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
return rawToolConfig;
|
|
131
|
-
})();
|
|
132
|
-
|
|
133
|
-
const findToolArray = (value: any): any[] | undefined => {
|
|
134
|
-
if (Array.isArray(value)) {
|
|
135
|
-
const hasNamedObjects = value.every(v => v && typeof v === 'object' && 'name' in v);
|
|
136
|
-
return hasNamedObjects ? value : undefined;
|
|
137
|
-
}
|
|
138
|
-
if (value && typeof value === 'object') {
|
|
139
|
-
if (Array.isArray(value.toolset)) return value.toolset;
|
|
140
|
-
if (Array.isArray(value.tools)) return value.tools;
|
|
141
|
-
for (const key of Object.keys(value)) {
|
|
142
|
-
const found = findToolArray((value as any)[key]);
|
|
143
|
-
if (found) return found;
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
return undefined;
|
|
147
|
-
};
|
|
148
|
-
|
|
149
|
-
const toolsetFromConfig = findToolArray(parsedToolConfig) ?? [];
|
|
150
|
-
|
|
151
|
-
const resolvedTools = (toolsResponse?.tools && toolsResponse.tools.length > 0)
|
|
152
|
-
? toolsResponse.tools
|
|
153
|
-
: (Array.isArray(toolsResponse?.toolset)
|
|
154
|
-
? normalizeToolset(toolsResponse.toolset)
|
|
155
|
-
: normalizeToolset(toolsetFromConfig));
|
|
156
|
-
|
|
157
|
-
if (!toolsResponse?.agent || !toolsResponse?.components) {
|
|
158
|
-
throw new Error('Invalid tools response from API');
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
// Generate code
|
|
162
|
-
const outputDir = codeGenerator.calculateOutputPath(
|
|
163
|
-
baseOutputDir,
|
|
164
|
-
agent.folderPath,
|
|
165
|
-
toolsResponse.agent.name
|
|
166
|
-
);
|
|
167
|
-
|
|
168
|
-
await codeGenerator.generate({
|
|
169
|
-
agent: toolsResponse.agent,
|
|
170
|
-
tools: resolvedTools,
|
|
171
|
-
components: toolsResponse.components,
|
|
172
|
-
outputDir,
|
|
173
|
-
forceOverwrite: options.force || false
|
|
174
|
-
});
|
|
175
|
-
|
|
176
|
-
spinner.succeed(`${progress} ${agent.agentName} → ${pc.gray(outputDir)}`);
|
|
177
|
-
successCount++;
|
|
178
|
-
|
|
179
|
-
// Update last pulled info
|
|
180
|
-
await projectTracker.updateLastPulled(
|
|
181
|
-
project.projectId,
|
|
182
|
-
agent.agentId,
|
|
183
|
-
toolsResponse.agent.version
|
|
184
|
-
);
|
|
185
|
-
|
|
186
|
-
} catch (error) {
|
|
187
|
-
spinner.fail(`${progress} ${agent.agentName}`);
|
|
188
|
-
console.error(pc.red(` ✗ ${(error as Error).message}`));
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
// Summary
|
|
194
|
-
console.log(pc.bold('\n✨ Pull Complete\n'));
|
|
195
|
-
|
|
196
|
-
if (successCount === totalAgents) {
|
|
197
|
-
console.log(` ${pc.green('✓')} Generated: ${successCount} / ${totalAgents} agents`);
|
|
198
|
-
} else if (successCount > 0) {
|
|
199
|
-
console.log(` ${pc.yellow('⚠')} Generated: ${successCount} / ${totalAgents} agents`);
|
|
200
|
-
console.log(` ${pc.red('✗')} Failed: ${totalAgents - successCount} agents`);
|
|
201
|
-
} else {
|
|
202
|
-
console.log(` ${pc.red('✗')} Failed: All ${totalAgents} agents`);
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
console.log(` Output: ${pc.gray(baseOutputDir)}`);
|
|
206
|
-
|
|
207
|
-
if (successCount > 0) {
|
|
208
|
-
console.log(pc.gray('\nNext steps:'));
|
|
209
|
-
console.log(pc.gray(' 1. Implement tool functions in tool-impl.ts files'));
|
|
210
|
-
console.log(pc.gray(' 2. Import and use the tool dispatcher in your code'));
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
console.log();
|
|
214
|
-
|
|
215
|
-
// Exit with error if any agents failed
|
|
216
|
-
if (successCount < totalAgents) {
|
|
217
|
-
process.exit(1);
|
|
218
|
-
}
|
|
219
|
-
});
|
|
220
|
-
}
|
package/src/commands/status.ts
DELETED
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
import { Command } from 'commander';
|
|
2
|
-
import pc from 'picocolors';
|
|
3
|
-
import { ConfigManager } from '../core/config/config-manager.js';
|
|
4
|
-
import { ProjectTracker } from '../core/config/project-tracker.js';
|
|
5
|
-
import { formatDistanceToNow } from '../utils/date.js';
|
|
6
|
-
|
|
7
|
-
export function createStatusCommand(
|
|
8
|
-
configManager: ConfigManager
|
|
9
|
-
): Command {
|
|
10
|
-
const projectTracker = new ProjectTracker(configManager);
|
|
11
|
-
|
|
12
|
-
return new Command('status')
|
|
13
|
-
.description('Show CLI status and tracked projects')
|
|
14
|
-
.action(async () => {
|
|
15
|
-
const config = await configManager.get();
|
|
16
|
-
|
|
17
|
-
console.log(pc.bold('\n📊 Porchestra CLI Status\n'));
|
|
18
|
-
|
|
19
|
-
// Auth status
|
|
20
|
-
if (config.auth?.token) {
|
|
21
|
-
const expiresAt = new Date(config.auth.expiresAt!);
|
|
22
|
-
const daysUntilExpiry = Math.ceil(
|
|
23
|
-
(expiresAt.getTime() - Date.now()) / (1000 * 60 * 60 * 24)
|
|
24
|
-
);
|
|
25
|
-
const expiryColor = daysUntilExpiry < 7 ? pc.yellow : pc.green;
|
|
26
|
-
|
|
27
|
-
console.log(pc.bold('🔑 Authentication'));
|
|
28
|
-
console.log(` Status: ${pc.green('✓ Logged in')}`);
|
|
29
|
-
console.log(` Device: ${config.auth.deviceName}`);
|
|
30
|
-
console.log(` Expires: ${expiryColor(daysUntilExpiry + ' days')} (${config.auth.expiresAt})`);
|
|
31
|
-
} else {
|
|
32
|
-
console.log(pc.bold('🔑 Authentication'));
|
|
33
|
-
console.log(` Status: ${pc.yellow('✗ Not logged in')}`);
|
|
34
|
-
console.log(pc.gray(' Run `porchestra login` to authenticate'));
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
// API Configuration
|
|
38
|
-
console.log(pc.bold('\n🔗 API Configuration'));
|
|
39
|
-
console.log(` URL: ${config.api?.baseUrl || pc.gray('(default)')}`);
|
|
40
|
-
if (config.api?.skipTlsVerify) {
|
|
41
|
-
console.log(pc.yellow(` ⚠️ TLS verification disabled`));
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
// Tracked Projects
|
|
45
|
-
const projects = await projectTracker.getTrackedProjects();
|
|
46
|
-
console.log(pc.bold('\n📁 Tracked Projects'));
|
|
47
|
-
|
|
48
|
-
if (projects.length === 0) {
|
|
49
|
-
console.log(pc.gray(' No projects tracked'));
|
|
50
|
-
console.log(pc.gray(' Run `porchestra explore` to select projects'));
|
|
51
|
-
} else {
|
|
52
|
-
console.log(` Projects: ${projects.length}`);
|
|
53
|
-
console.log(` Agents: ${projects.reduce((sum, p) => sum + p.agents.length, 0)}`);
|
|
54
|
-
|
|
55
|
-
projects.forEach(project => {
|
|
56
|
-
const lastPulled = project.lastPulledAt
|
|
57
|
-
? formatDistanceToNow(new Date(project.lastPulledAt))
|
|
58
|
-
: pc.gray('never');
|
|
59
|
-
|
|
60
|
-
console.log(pc.gray(`\n ${project.projectName}`));
|
|
61
|
-
project.agents.forEach(agent => {
|
|
62
|
-
const agentPulled = agent.lastPulledAt
|
|
63
|
-
? formatDistanceToNow(new Date(agent.lastPulledAt))
|
|
64
|
-
: pc.gray('pending');
|
|
65
|
-
console.log(pc.gray(` └─ ${agent.agentName} ${pc.cyan(agent.folderPath)} (${agentPulled})`));
|
|
66
|
-
});
|
|
67
|
-
console.log(pc.gray(` Last pulled: ${lastPulled}`));
|
|
68
|
-
});
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
// Output Configuration
|
|
72
|
-
console.log(pc.bold('\n📂 Output Configuration'));
|
|
73
|
-
console.log(` Base dir: ${config.output?.baseDir || './src/agents'}`);
|
|
74
|
-
console.log(` Index files: ${config.output?.createIndexFiles ? 'yes' : 'no'}`);
|
|
75
|
-
|
|
76
|
-
console.log();
|
|
77
|
-
});
|
|
78
|
-
}
|
package/src/commands/whoami.ts
DELETED
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
import { Command } from 'commander';
|
|
2
|
-
import pc from 'picocolors';
|
|
3
|
-
import { AuthService } from '../core/auth/auth-service.js';
|
|
4
|
-
import { ConfigManager } from '../core/config/config-manager.js';
|
|
5
|
-
import { ProjectTracker } from '../core/config/project-tracker.js';
|
|
6
|
-
|
|
7
|
-
export function createWhoamiCommand(
|
|
8
|
-
configManager: ConfigManager,
|
|
9
|
-
authService: AuthService
|
|
10
|
-
): Command {
|
|
11
|
-
const projectTracker = new ProjectTracker(configManager);
|
|
12
|
-
|
|
13
|
-
return new Command('whoami')
|
|
14
|
-
.description('Show current user and token information')
|
|
15
|
-
.action(async () => {
|
|
16
|
-
const config = await configManager.get();
|
|
17
|
-
|
|
18
|
-
if (!config.auth?.token) {
|
|
19
|
-
console.log(pc.yellow('Not logged in. Run `porchestra login`'));
|
|
20
|
-
return;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
try {
|
|
24
|
-
const userInfo = await authService.getCurrentUser(config.auth.token);
|
|
25
|
-
const expiresAt = new Date(config.auth.expiresAt!);
|
|
26
|
-
const isExpiringSoon = expiresAt.getTime() - Date.now() < 7 * 24 * 60 * 60 * 1000;
|
|
27
|
-
|
|
28
|
-
console.log(pc.bold('\n👤 User Information\n'));
|
|
29
|
-
console.log(` Email: ${pc.cyan(userInfo.email)}`);
|
|
30
|
-
console.log(` Name: ${userInfo.name || 'N/A'}`);
|
|
31
|
-
|
|
32
|
-
console.log(pc.bold('\n🔑 Token Information\n'));
|
|
33
|
-
console.log(` Device: ${config.auth.deviceName || 'N/A'}`);
|
|
34
|
-
console.log(` Expires: ${isExpiringSoon ? pc.yellow(expiresAt.toISOString()) : pc.green(expiresAt.toISOString())}`);
|
|
35
|
-
|
|
36
|
-
if (isExpiringSoon) {
|
|
37
|
-
console.log(pc.yellow(' ⚠️ Expires within 7 days - will auto-refresh'));
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
if (config.api?.baseUrl !== 'https://api.porchestra.io/v1') {
|
|
41
|
-
console.log(pc.bold('\n🔗 API Configuration\n'));
|
|
42
|
-
console.log(` URL: ${config.api?.baseUrl}`);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
// Show tracked projects summary
|
|
46
|
-
const summary = await projectTracker.getSummary();
|
|
47
|
-
console.log(pc.bold('\n📁 Tracked Projects\n'));
|
|
48
|
-
console.log(` Projects: ${summary.projectCount}`);
|
|
49
|
-
console.log(` Agents: ${summary.agentCount}`);
|
|
50
|
-
|
|
51
|
-
} catch (error) {
|
|
52
|
-
console.log(pc.red('Failed to fetch user info. Token may be invalid.'));
|
|
53
|
-
console.log(pc.gray('Run `porchestra login` to re-authenticate.'));
|
|
54
|
-
}
|
|
55
|
-
});
|
|
56
|
-
}
|
package/src/core/api/client.ts
DELETED
|
@@ -1,133 +0,0 @@
|
|
|
1
|
-
import got from 'got';
|
|
2
|
-
import { ConfigManager } from '../config/config-manager.js';
|
|
3
|
-
import { NetworkError, AuthenticationError } from '../../utils/errors.js';
|
|
4
|
-
import {
|
|
5
|
-
ProjectsBriefResponse,
|
|
6
|
-
AgentsListResponse,
|
|
7
|
-
AgentToolsResponse,
|
|
8
|
-
UserInfo,
|
|
9
|
-
CliConfigResponse,
|
|
10
|
-
ApiResponse,
|
|
11
|
-
} from '../../types/index.js';
|
|
12
|
-
|
|
13
|
-
const MAX_RETRIES = 3;
|
|
14
|
-
const RETRY_DELAY = 1000;
|
|
15
|
-
|
|
16
|
-
interface RequestOptions {
|
|
17
|
-
method: 'GET' | 'POST' | 'DELETE';
|
|
18
|
-
headers?: Record<string, string>;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export class ApiClient {
|
|
22
|
-
private configManager: ConfigManager;
|
|
23
|
-
|
|
24
|
-
constructor(configManager: ConfigManager) {
|
|
25
|
-
this.configManager = configManager;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
private async getBaseUrl(): Promise<string> {
|
|
29
|
-
const config = await this.configManager.get();
|
|
30
|
-
return config.api?.baseUrl || 'https://api.porchestra.io/v1';
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
private async getToken(): Promise<string | undefined> {
|
|
34
|
-
const config = await this.configManager.get();
|
|
35
|
-
return config.auth?.token;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
private async requestWithRetry<T>(
|
|
39
|
-
url: string,
|
|
40
|
-
options: RequestOptions,
|
|
41
|
-
attempt = 1
|
|
42
|
-
): Promise<T> {
|
|
43
|
-
try {
|
|
44
|
-
const token = await this.getToken();
|
|
45
|
-
const headers: Record<string, string> = {
|
|
46
|
-
'Content-Type': 'application/json',
|
|
47
|
-
...(token && { Authorization: `Bearer ${token}` }),
|
|
48
|
-
...(options.headers || {}),
|
|
49
|
-
};
|
|
50
|
-
|
|
51
|
-
const response = await got(url, {
|
|
52
|
-
method: options.method,
|
|
53
|
-
headers,
|
|
54
|
-
retry: { limit: 0 },
|
|
55
|
-
}).json<ApiResponse<T>>();
|
|
56
|
-
|
|
57
|
-
return response.data;
|
|
58
|
-
} catch (error: any) {
|
|
59
|
-
if (error.response?.statusCode === 401) {
|
|
60
|
-
throw new AuthenticationError('Authentication failed. Please login again.');
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
const isRetryable =
|
|
64
|
-
error.code === 'ECONNRESET' ||
|
|
65
|
-
error.code === 'ETIMEDOUT' ||
|
|
66
|
-
error.code === 'ENOTFOUND' ||
|
|
67
|
-
(error.response?.statusCode && error.response.statusCode >= 500);
|
|
68
|
-
|
|
69
|
-
if (isRetryable && attempt < MAX_RETRIES) {
|
|
70
|
-
await new Promise(resolve => setTimeout(resolve, RETRY_DELAY * attempt));
|
|
71
|
-
return this.requestWithRetry(url, options, attempt + 1);
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
throw new NetworkError(
|
|
75
|
-
`API request failed: ${error.message}`,
|
|
76
|
-
error.response?.statusCode,
|
|
77
|
-
isRetryable
|
|
78
|
-
);
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
async getProjectsBrief(): Promise<ProjectsBriefResponse> {
|
|
83
|
-
const baseUrl = await this.getBaseUrl();
|
|
84
|
-
return this.requestWithRetry<ProjectsBriefResponse>(
|
|
85
|
-
`${baseUrl}/projects/brief`,
|
|
86
|
-
{ method: 'GET' }
|
|
87
|
-
);
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
async getProjectAgents(
|
|
91
|
-
projectId: string,
|
|
92
|
-
environment?: 'PRODUCTION' | 'STAGING' | 'DEVELOPMENT'
|
|
93
|
-
): Promise<AgentsListResponse> {
|
|
94
|
-
const baseUrl = await this.getBaseUrl();
|
|
95
|
-
const query = environment ? `?environment=${environment}` : '';
|
|
96
|
-
return this.requestWithRetry<AgentsListResponse>(
|
|
97
|
-
`${baseUrl}/projects/${projectId}/agents${query}`,
|
|
98
|
-
{ method: 'GET' }
|
|
99
|
-
);
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
async getAgentTools(
|
|
103
|
-
projectId: string,
|
|
104
|
-
agentId: string,
|
|
105
|
-
environment?: 'PRODUCTION' | 'STAGING' | 'DEVELOPMENT'
|
|
106
|
-
): Promise<AgentToolsResponse> {
|
|
107
|
-
const baseUrl = await this.getBaseUrl();
|
|
108
|
-
const query = environment ? `?environment=${environment}` : '';
|
|
109
|
-
return this.requestWithRetry<AgentToolsResponse>(
|
|
110
|
-
`${baseUrl}/projects/${projectId}/agents/${agentId}/tools${query}`,
|
|
111
|
-
{ method: 'GET' }
|
|
112
|
-
);
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
async getCurrentUser(token: string): Promise<UserInfo> {
|
|
116
|
-
const baseUrl = await this.getBaseUrl();
|
|
117
|
-
return this.requestWithRetry<UserInfo>(
|
|
118
|
-
`${baseUrl}/auth/me`,
|
|
119
|
-
{
|
|
120
|
-
method: 'GET',
|
|
121
|
-
headers: { Authorization: `Bearer ${token}` },
|
|
122
|
-
}
|
|
123
|
-
);
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
async getCliConfig(): Promise<CliConfigResponse> {
|
|
127
|
-
const baseUrl = await this.getBaseUrl();
|
|
128
|
-
return this.requestWithRetry<CliConfigResponse>(
|
|
129
|
-
`${baseUrl}/cli/config`,
|
|
130
|
-
{ method: 'GET' }
|
|
131
|
-
);
|
|
132
|
-
}
|
|
133
|
-
}
|
|
@@ -1,176 +0,0 @@
|
|
|
1
|
-
import { ConfigManager } from '../config/config-manager.js';
|
|
2
|
-
import { AuthenticationError, NetworkError } from '../../utils/errors.js';
|
|
3
|
-
import {
|
|
4
|
-
ApiResponse,
|
|
5
|
-
CliTokenResponse,
|
|
6
|
-
CliTokenRefreshResponse,
|
|
7
|
-
CliTokenRevokeResponse,
|
|
8
|
-
UserInfo,
|
|
9
|
-
} from '../../types/index.js';
|
|
10
|
-
import got from 'got';
|
|
11
|
-
import os from 'os';
|
|
12
|
-
|
|
13
|
-
interface LoginCredentials {
|
|
14
|
-
email: string;
|
|
15
|
-
password: string;
|
|
16
|
-
apiUrl?: string;
|
|
17
|
-
deviceName?: string;
|
|
18
|
-
skipTlsVerify?: boolean;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export class AuthService {
|
|
22
|
-
private configManager: ConfigManager;
|
|
23
|
-
|
|
24
|
-
constructor(configManager: ConfigManager) {
|
|
25
|
-
this.configManager = configManager;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
async login(credentials: LoginCredentials): Promise<CliTokenResponse> {
|
|
29
|
-
const config = await this.configManager.get();
|
|
30
|
-
const baseUrl = credentials.apiUrl || config.api?.baseUrl || 'https://api.porchestra.io/v1';
|
|
31
|
-
|
|
32
|
-
const deviceInfo = {
|
|
33
|
-
os: os.platform(),
|
|
34
|
-
version: os.release(),
|
|
35
|
-
cliVersion: '1.0.0',
|
|
36
|
-
};
|
|
37
|
-
|
|
38
|
-
const deviceName = credentials.deviceName || `${os.hostname()} - ${deviceInfo.os}`;
|
|
39
|
-
|
|
40
|
-
try {
|
|
41
|
-
const response = await got.post(`${baseUrl}/cli/login`, {
|
|
42
|
-
json: {
|
|
43
|
-
email: credentials.email,
|
|
44
|
-
password: credentials.password,
|
|
45
|
-
deviceName,
|
|
46
|
-
deviceInfo,
|
|
47
|
-
},
|
|
48
|
-
headers: {
|
|
49
|
-
'Content-Type': 'application/json',
|
|
50
|
-
},
|
|
51
|
-
https: {
|
|
52
|
-
rejectUnauthorized: !credentials.skipTlsVerify,
|
|
53
|
-
},
|
|
54
|
-
}).json<ApiResponse<CliTokenResponse>>();
|
|
55
|
-
|
|
56
|
-
return response.data;
|
|
57
|
-
} catch (error: any) {
|
|
58
|
-
if (error.response?.statusCode) {
|
|
59
|
-
if (error.response.statusCode === 401) {
|
|
60
|
-
throw new AuthenticationError('Invalid email or password');
|
|
61
|
-
}
|
|
62
|
-
throw new AuthenticationError(`Login failed: ${error.message}`);
|
|
63
|
-
}
|
|
64
|
-
if (error.code === 'ECONNREFUSED' || error.code === 'ENOTFOUND' || error.code === 'ETIMEDOUT') {
|
|
65
|
-
throw new NetworkError(`Network error: ${error.message}`);
|
|
66
|
-
}
|
|
67
|
-
throw error;
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
async refreshToken(tokenId: string, currentToken: string): Promise<CliTokenRefreshResponse> {
|
|
72
|
-
const config = await this.configManager.get();
|
|
73
|
-
const baseUrl = config.api?.baseUrl || 'https://api.porchestra.io/v1';
|
|
74
|
-
|
|
75
|
-
try {
|
|
76
|
-
const response = await got.post(`${baseUrl}/cli/token/refresh`, {
|
|
77
|
-
json: {
|
|
78
|
-
tokenId,
|
|
79
|
-
currentToken,
|
|
80
|
-
},
|
|
81
|
-
headers: {
|
|
82
|
-
'Authorization': `Bearer ${currentToken}`,
|
|
83
|
-
'Content-Type': 'application/json',
|
|
84
|
-
},
|
|
85
|
-
}).json<ApiResponse<CliTokenRefreshResponse>>();
|
|
86
|
-
|
|
87
|
-
return response.data;
|
|
88
|
-
} catch (error: any) {
|
|
89
|
-
if (error.response?.statusCode === 401) {
|
|
90
|
-
throw new AuthenticationError('Token refresh failed. Please login again.');
|
|
91
|
-
}
|
|
92
|
-
throw error;
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
async revokeToken(token: string, revokeAll = false): Promise<CliTokenRevokeResponse> {
|
|
97
|
-
const config = await this.configManager.get();
|
|
98
|
-
const baseUrl = config.api?.baseUrl || 'https://api.porchestra.io/v1';
|
|
99
|
-
|
|
100
|
-
try {
|
|
101
|
-
const response = await got.delete(`${baseUrl}/cli/token`, {
|
|
102
|
-
json: {
|
|
103
|
-
revokeAll,
|
|
104
|
-
},
|
|
105
|
-
headers: {
|
|
106
|
-
'Authorization': `Bearer ${token}`,
|
|
107
|
-
'Content-Type': 'application/json',
|
|
108
|
-
},
|
|
109
|
-
}).json<ApiResponse<CliTokenRevokeResponse>>();
|
|
110
|
-
|
|
111
|
-
return response.data;
|
|
112
|
-
} catch (error: any) {
|
|
113
|
-
if (error.response?.statusCode === 401) {
|
|
114
|
-
throw new AuthenticationError('Token already revoked or invalid');
|
|
115
|
-
}
|
|
116
|
-
throw error;
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
async getCurrentUser(token: string): Promise<UserInfo> {
|
|
121
|
-
const config = await this.configManager.get();
|
|
122
|
-
const baseUrl = config.api?.baseUrl || 'https://api.porchestra.io/v1';
|
|
123
|
-
|
|
124
|
-
try {
|
|
125
|
-
const response = await got.get(`${baseUrl}/auth/me`, {
|
|
126
|
-
headers: {
|
|
127
|
-
'Authorization': `Bearer ${token}`,
|
|
128
|
-
},
|
|
129
|
-
}).json<ApiResponse<UserInfo>>();
|
|
130
|
-
|
|
131
|
-
return response.data;
|
|
132
|
-
} catch (error: any) {
|
|
133
|
-
if (error.response?.statusCode === 401) {
|
|
134
|
-
throw new AuthenticationError('Token is invalid or expired');
|
|
135
|
-
}
|
|
136
|
-
throw error;
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
async checkAndRefreshTokenIfNeeded(): Promise<boolean> {
|
|
141
|
-
const config = await this.configManager.get();
|
|
142
|
-
|
|
143
|
-
if (!config.auth?.token || !config.auth?.tokenId || !config.auth?.expiresAt) {
|
|
144
|
-
return false;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
const expiresAt = new Date(config.auth.expiresAt);
|
|
148
|
-
const now = new Date();
|
|
149
|
-
const sevenDaysFromNow = new Date(now.getTime() + 7 * 24 * 60 * 60 * 1000);
|
|
150
|
-
|
|
151
|
-
// If token expires within 7 days, refresh it
|
|
152
|
-
if (expiresAt < sevenDaysFromNow) {
|
|
153
|
-
try {
|
|
154
|
-
const refreshed = await this.refreshToken(
|
|
155
|
-
config.auth.tokenId,
|
|
156
|
-
config.auth.token
|
|
157
|
-
);
|
|
158
|
-
|
|
159
|
-
await this.configManager.update((cfg) => ({
|
|
160
|
-
...cfg,
|
|
161
|
-
auth: {
|
|
162
|
-
...cfg.auth,
|
|
163
|
-
token: refreshed.token,
|
|
164
|
-
expiresAt: refreshed.expiresAt,
|
|
165
|
-
},
|
|
166
|
-
}));
|
|
167
|
-
|
|
168
|
-
return true;
|
|
169
|
-
} catch {
|
|
170
|
-
return false;
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
return true;
|
|
175
|
-
}
|
|
176
|
-
}
|