@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,160 @@
1
+ import { Command } from 'commander';
2
+ import { select, checkbox, confirm, Separator } from '@inquirer/prompts';
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 { ProjectTracker } from '../core/config/project-tracker.js';
8
+ import { formatDate } from '../utils/date.js';
9
+
10
+ interface ExploreOptions {
11
+ project?: string;
12
+ }
13
+
14
+ interface ProjectChoice {
15
+ name: string;
16
+ value: any;
17
+ short?: string;
18
+ }
19
+
20
+ export function createExploreCommand(
21
+ configManager: ConfigManager,
22
+ apiClient: ApiClient
23
+ ): Command {
24
+ const projectTracker = new ProjectTracker(configManager);
25
+
26
+ return new Command('explore')
27
+ .description('Explore and select projects/agents to track')
28
+ .option('-p, --project <id>', 'Start with specific project')
29
+ .action(async (options: ExploreOptions) => {
30
+ const spinner = ora('Fetching projects...').start();
31
+
32
+ try {
33
+ // Fetch all projects
34
+ const response = await apiClient.getProjectsBrief();
35
+ const projects = response.projects;
36
+ spinner.stop();
37
+
38
+ if (projects.length === 0) {
39
+ console.log(pc.yellow('No projects found. Create one in the web app first.'));
40
+ return;
41
+ }
42
+
43
+ // Project selection loop
44
+ let exploring = true;
45
+ while (exploring) {
46
+ const projectChoices: ProjectChoice[] = projects.map((p) => ({
47
+ name: `${p.name} ${pc.gray(`(${p.agentCount} agents, last modified ${formatDate(p.lastModifiedAt)})`)}`,
48
+ value: p,
49
+ short: p.name
50
+ }));
51
+
52
+ let selectedProject: any;
53
+ if (options.project) {
54
+ selectedProject = projects.find((p) => p.id === options.project || p.slug === options.project);
55
+ } else {
56
+ const choices: any[] = [
57
+ ...projectChoices,
58
+ new Separator(),
59
+ { name: pc.yellow('Done exploring'), value: 'DONE' }
60
+ ];
61
+ selectedProject = await select({
62
+ message: 'Select a project to explore:',
63
+ choices
64
+ });
65
+ }
66
+
67
+ if (selectedProject === 'DONE') {
68
+ exploring = false;
69
+ break;
70
+ }
71
+
72
+ if (!selectedProject) {
73
+ console.log(pc.red('Project not found'));
74
+ continue;
75
+ }
76
+
77
+ // Fetch agents for selected project
78
+ const agentSpinner = ora(`Fetching agents for ${selectedProject.name}...`).start();
79
+ const agentsResponse = await apiClient.getProjectAgents(selectedProject.id);
80
+ const agents = agentsResponse;
81
+ agentSpinner.succeed(`Found ${agents.agents.length} agents`);
82
+
83
+ // Display agents with folder paths
84
+ console.log(pc.bold(`\nšŸ“ ${selectedProject.name}\n`));
85
+
86
+ agents.agents.forEach((agent) => {
87
+ const isPublished = agent.isPublished
88
+ ? pc.green('ā—')
89
+ : pc.gray('ā—‹');
90
+ const versionInfo = `${agent.version} ${pc.gray(`(${agents.versionResolution.source})`)}`;
91
+
92
+ console.log(` ${isPublished} ${pc.cyan(agent.name)}`);
93
+ console.log(` ${pc.gray('Path:')} ${agent.folderPath}`);
94
+ console.log(` ${pc.gray('Version:')} ${versionInfo}`);
95
+ console.log(` ${pc.gray('Tools:')} ${agent.toolCount}`);
96
+ if (agents.versionResolution.source !== 'PRODUCTION') {
97
+ console.log(` ${pc.yellow('⚠')} ${agents.versionResolution.note}`);
98
+ }
99
+ console.log();
100
+ });
101
+
102
+ // Agent selection
103
+ const currentlyTracked = await projectTracker.getTrackedAgents(selectedProject.id);
104
+ const trackedIds = new Set(currentlyTracked.map(a => a.agentId));
105
+
106
+ const selectedAgentIds = await checkbox({
107
+ message: 'Select agents to track (space to toggle, enter to confirm):',
108
+ choices: agents.agents.map((agent) => ({
109
+ name: `${agent.name} ${pc.gray(`(${agent.folderPath})`)}`,
110
+ value: agent.id,
111
+ checked: trackedIds.has(agent.id)
112
+ }))
113
+ });
114
+
115
+ // Update tracking - IMMEDIATE AUTO-SAVE
116
+ if (selectedAgentIds.length > 0) {
117
+ const selectedAgents = agents.agents.filter((a) => selectedAgentIds.includes(a.id));
118
+ try {
119
+ await projectTracker.trackProject(selectedProject, selectedAgents);
120
+ console.log(pc.green(`\nāœ“ Auto-saved: Tracking ${selectedAgents.length} agents from ${selectedProject.name}`));
121
+ } catch (error) {
122
+ console.error(pc.red(`\nāœ— Failed to save tracking: ${(error as Error).message}`));
123
+ throw error; // RETHROW - don't silently fail
124
+ }
125
+ } else {
126
+ try {
127
+ await projectTracker.untrackProject(selectedProject.id);
128
+ console.log(pc.yellow(`\nāœ“ Auto-saved: No longer tracking ${selectedProject.name}`));
129
+ } catch (error) {
130
+ console.error(pc.red(`\nāœ— Failed to untrack: ${(error as Error).message}`));
131
+ throw error; // RETHROW - don't silently fail
132
+ }
133
+ }
134
+
135
+ // Continue?
136
+ if (!options.project) {
137
+ const continueExploring = await confirm({
138
+ message: 'Explore another project?',
139
+ default: true
140
+ });
141
+ if (!continueExploring) exploring = false;
142
+ } else {
143
+ exploring = false;
144
+ }
145
+ }
146
+
147
+ // Summary
148
+ const summary = await projectTracker.getSummary();
149
+ console.log(pc.bold('\nšŸ“Š Tracking Summary\n'));
150
+ console.log(` Projects: ${summary.projectCount}`);
151
+ console.log(` Agents: ${summary.agentCount}`);
152
+ console.log(pc.gray('\nRun `porchestra pull` to generate code for tracked agents\n'));
153
+
154
+ } catch (error) {
155
+ spinner.fail('Failed to fetch projects');
156
+ console.error(pc.red(`\nāœ— ${(error as Error).message}`));
157
+ process.exit(1);
158
+ }
159
+ });
160
+ }
@@ -0,0 +1,101 @@
1
+ import { Command } from 'commander';
2
+ import pc from 'picocolors';
3
+ import { input, password } from '@inquirer/prompts';
4
+ import { ConfigManager } from '../core/config/config-manager.js';
5
+ import { AuthService } from '../core/auth/auth-service.js';
6
+
7
+ interface LoginOptions {
8
+ apiUrl?: string;
9
+ skipTlsVerify?: boolean;
10
+ }
11
+
12
+ export function createLoginCommand(
13
+ configManager: ConfigManager,
14
+ authService: AuthService
15
+ ): Command {
16
+ return new Command('login')
17
+ .description('Authenticate with your Porchestra account')
18
+ .option('--api-url <url>', 'Override API URL')
19
+ .option('--skip-tls-verify', 'Skip TLS certificate verification (insecure)')
20
+ .action(async (options: LoginOptions) => {
21
+ console.log(pc.bold('\n \uD83D\uDD10 Porchestra CLI Login\n'));
22
+
23
+ const apiUrl = options.apiUrl || process.env.PORCHESTRA_API_URL;
24
+
25
+ if (options.skipTlsVerify) {
26
+ console.log(pc.yellow('\n \u26A0\uFE0F WARNING: TLS verification disabled. Not recommended for production.\n'));
27
+ }
28
+
29
+ const email = await input({
30
+ message: ' Email:',
31
+ validate: (value) => value.includes('@') || 'Please enter a valid email'
32
+ });
33
+
34
+ const pwd = await password({
35
+ message: ' Password:',
36
+ mask: '*'
37
+ });
38
+
39
+ try {
40
+ const result = await authService.login({
41
+ email,
42
+ password: pwd,
43
+ apiUrl,
44
+ skipTlsVerify: options.skipTlsVerify
45
+ });
46
+
47
+ // DEBUG: Log raw API response
48
+ console.log(pc.yellow('\n [DEBUG] Raw API response:'));
49
+ console.log(pc.yellow(` token: ${result.token?.substring(0, 20)}...`));
50
+ console.log(pc.yellow(` tokenId: ${result.tokenId}`));
51
+ console.log(pc.yellow(` expiresAt: ${result.expiresAt}`));
52
+ console.log(pc.yellow(` issuedAt: ${result.issuedAt}`));
53
+ console.log(pc.yellow(` deviceName: ${result.deviceName}`));
54
+
55
+ try {
56
+ await configManager.update((cfg) => ({
57
+ ...cfg,
58
+ auth: {
59
+ token: result.token,
60
+ tokenId: result.tokenId,
61
+ expiresAt: result.expiresAt,
62
+ deviceName: result.deviceName
63
+ },
64
+ api: {
65
+ baseUrl: apiUrl || cfg.api?.baseUrl || 'https://api.porchestra.io/v1',
66
+ skipTlsVerify: options.skipTlsVerify || false
67
+ },
68
+ cli: {
69
+ ...cfg.cli,
70
+ lastLoginAt: new Date().toISOString()
71
+ }
72
+ }));
73
+
74
+ // DEBUG: Verify config was saved
75
+ const savedConfig = await configManager.get();
76
+ console.log(pc.yellow('\n [DEBUG] Saved config auth:'));
77
+ console.log(pc.yellow(` ${JSON.stringify(savedConfig.auth, null, 2)}`));
78
+ } catch (saveError) {
79
+ console.error(pc.red('\n [DEBUG] Save error:'), saveError);
80
+ throw saveError;
81
+ }
82
+
83
+ const expiresAt = new Date(result.expiresAt);
84
+ const daysUntilExpiry = Math.ceil((expiresAt.getTime() - Date.now()) / (1000 * 60 * 60 * 24));
85
+
86
+ console.log(pc.green(`
87
+ āœ“ Login successful
88
+ `));
89
+ console.log(pc.gray(` Token expires in ${daysUntilExpiry} days (${result.expiresAt})`));
90
+ console.log(pc.gray(` Device: ${result.deviceName}`));
91
+ if (apiUrl) {
92
+ console.log(pc.gray(` API: ${apiUrl}`));
93
+ }
94
+ console.log(pc.gray(` Auto-refresh: 7 days before expiry`));
95
+ console.log();
96
+ } catch (error) {
97
+ console.error(pc.red(`\n \u2717 Login failed: ${(error as Error).message}`));
98
+ process.exit(1);
99
+ }
100
+ });
101
+ }
@@ -0,0 +1,52 @@
1
+ import { Command } from 'commander';
2
+ import pc from 'picocolors';
3
+ import { confirm } from '@inquirer/prompts';
4
+ import { AuthService } from '../core/auth/auth-service.js';
5
+ import { ConfigManager } from '../core/config/config-manager.js';
6
+
7
+ interface LogoutOptions {
8
+ all?: boolean;
9
+ }
10
+
11
+ export function createLogoutCommand(
12
+ configManager: ConfigManager,
13
+ authService: AuthService
14
+ ): Command {
15
+ return new Command('logout')
16
+ .description('Logout and revoke CLI token')
17
+ .option('--all', 'Revoke all device tokens (logout everywhere)')
18
+ .action(async (options: LogoutOptions) => {
19
+ const config = await configManager.get();
20
+
21
+ if (!config.auth?.token) {
22
+ console.log(pc.yellow('Not currently logged in'));
23
+ return;
24
+ }
25
+
26
+ if (options.all) {
27
+ const confirmed = await confirm({
28
+ message: 'This will revoke ALL your CLI tokens on ALL devices. Continue?',
29
+ default: false
30
+ });
31
+ if (!confirmed) return;
32
+ }
33
+
34
+ try {
35
+ // Revoke on server
36
+ await authService.revokeToken(config.auth.token, options.all);
37
+
38
+ // Clear local config
39
+ await configManager.clear();
40
+
41
+ if (options.all) {
42
+ console.log(pc.green('āœ“ Revoked all tokens and logged out all devices'));
43
+ } else {
44
+ console.log(pc.green('āœ“ Logged out successfully'));
45
+ }
46
+ } catch (error) {
47
+ // Even if server revoke fails, clear local config
48
+ await configManager.clear();
49
+ console.log(pc.yellow('āœ“ Cleared local credentials (server revoke may have failed)'));
50
+ }
51
+ });
52
+ }
@@ -0,0 +1,220 @@
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
+ }
@@ -0,0 +1,78 @@
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
+ }
@@ -0,0 +1,56 @@
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
+ }