pdauth 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.
Files changed (60) hide show
  1. package/PROXY-SPEC.md +159 -0
  2. package/README.md +142 -0
  3. package/bin/ga.mjs +329 -0
  4. package/dist/cli.d.ts +3 -0
  5. package/dist/cli.d.ts.map +1 -0
  6. package/dist/cli.js +88 -0
  7. package/dist/cli.js.map +1 -0
  8. package/dist/commands/apps.d.ts +8 -0
  9. package/dist/commands/apps.d.ts.map +1 -0
  10. package/dist/commands/apps.js +76 -0
  11. package/dist/commands/apps.js.map +1 -0
  12. package/dist/commands/config.d.ts +5 -0
  13. package/dist/commands/config.d.ts.map +1 -0
  14. package/dist/commands/config.js +58 -0
  15. package/dist/commands/config.js.map +1 -0
  16. package/dist/commands/connect.d.ts +6 -0
  17. package/dist/commands/connect.d.ts.map +1 -0
  18. package/dist/commands/connect.js +67 -0
  19. package/dist/commands/connect.js.map +1 -0
  20. package/dist/commands/index.d.ts +7 -0
  21. package/dist/commands/index.d.ts.map +1 -0
  22. package/dist/commands/index.js +7 -0
  23. package/dist/commands/index.js.map +1 -0
  24. package/dist/commands/proxy.d.ts +12 -0
  25. package/dist/commands/proxy.d.ts.map +1 -0
  26. package/dist/commands/proxy.js +127 -0
  27. package/dist/commands/proxy.js.map +1 -0
  28. package/dist/commands/status.d.ts +10 -0
  29. package/dist/commands/status.d.ts.map +1 -0
  30. package/dist/commands/status.js +117 -0
  31. package/dist/commands/status.js.map +1 -0
  32. package/dist/commands/tools.d.ts +10 -0
  33. package/dist/commands/tools.d.ts.map +1 -0
  34. package/dist/commands/tools.js +123 -0
  35. package/dist/commands/tools.js.map +1 -0
  36. package/dist/config.d.ts +21 -0
  37. package/dist/config.d.ts.map +1 -0
  38. package/dist/config.js +59 -0
  39. package/dist/config.js.map +1 -0
  40. package/dist/index.d.ts +4 -0
  41. package/dist/index.d.ts.map +1 -0
  42. package/dist/index.js +6 -0
  43. package/dist/index.js.map +1 -0
  44. package/dist/pipedream.d.ts +52 -0
  45. package/dist/pipedream.d.ts.map +1 -0
  46. package/dist/pipedream.js +142 -0
  47. package/dist/pipedream.js.map +1 -0
  48. package/package.json +46 -0
  49. package/src/cli.ts +111 -0
  50. package/src/commands/apps.ts +88 -0
  51. package/src/commands/config.ts +68 -0
  52. package/src/commands/connect.ts +76 -0
  53. package/src/commands/index.ts +6 -0
  54. package/src/commands/proxy.ts +155 -0
  55. package/src/commands/status.ts +143 -0
  56. package/src/commands/tools.ts +150 -0
  57. package/src/config.ts +78 -0
  58. package/src/index.ts +6 -0
  59. package/src/pipedream.ts +216 -0
  60. package/tsconfig.json +20 -0
package/src/cli.ts ADDED
@@ -0,0 +1,111 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { Command } from 'commander';
4
+ import {
5
+ configCommand,
6
+ appsCommand,
7
+ appInfoCommand,
8
+ connectCommand,
9
+ statusCommand,
10
+ disconnectCommand,
11
+ toolsCommand,
12
+ callCommand,
13
+ proxyCommand,
14
+ } from './commands/index.js';
15
+
16
+ const program = new Command();
17
+
18
+ program
19
+ .name('pdauth')
20
+ .description('šŸ” Pipedream OAuth CLI - Give AI agents access to 2500+ APIs via dynamic OAuth')
21
+ .version('1.0.0');
22
+
23
+ // Config command
24
+ program
25
+ .command('config')
26
+ .description('Configure Pipedream credentials')
27
+ .option('-s, --show', 'Show current configuration')
28
+ .option('--set <key=value>', 'Set a configuration value')
29
+ .action(configCommand);
30
+
31
+ // Apps commands
32
+ program
33
+ .command('apps')
34
+ .description('List available apps (2500+ APIs)')
35
+ .option('-s, --search <query>', 'Search for apps')
36
+ .option('-j, --json', 'Output as JSON')
37
+ .action(appsCommand);
38
+
39
+ program
40
+ .command('app <slug>')
41
+ .description('Get info about a specific app')
42
+ .option('-j, --json', 'Output as JSON')
43
+ .action(appInfoCommand);
44
+
45
+ // Connect command
46
+ program
47
+ .command('connect <app>')
48
+ .description('Generate OAuth link for an app')
49
+ .option('-u, --user <id>', 'User ID (default: "default")')
50
+ .option('-j, --json', 'Output as JSON')
51
+ .option('-c, --copy', 'Copy link to clipboard')
52
+ .action(connectCommand);
53
+
54
+ // Status command
55
+ program
56
+ .command('status')
57
+ .description('List connected accounts')
58
+ .option('-u, --user <id>', 'User ID (default: "default")')
59
+ .option('-a, --all', 'Show all users')
60
+ .option('-j, --json', 'Output as JSON')
61
+ .action(statusCommand);
62
+
63
+ // Disconnect command
64
+ program
65
+ .command('disconnect <app>')
66
+ .description('Disconnect an app')
67
+ .option('-u, --user <id>', 'User ID')
68
+ .option('-f, --force', 'Skip confirmation')
69
+ .action(disconnectCommand);
70
+
71
+ // Tools command
72
+ program
73
+ .command('tools <app>')
74
+ .description('List available MCP tools for an app')
75
+ .option('-u, --user <id>', 'User ID')
76
+ .option('-j, --json', 'Output as JSON')
77
+ .action(toolsCommand);
78
+
79
+ // Call command
80
+ program
81
+ .command('call <tool>')
82
+ .description('Invoke an MCP tool (format: app.tool_name)')
83
+ .argument('[args...]', 'Tool arguments as key=value pairs')
84
+ .option('-u, --user <id>', 'User ID')
85
+ .option('-j, --json', 'Output as JSON')
86
+ .option('-a, --args <json>', 'Arguments as JSON')
87
+ .action(callCommand);
88
+
89
+ // Proxy command - make authenticated API requests
90
+ program
91
+ .command('proxy <app> <path>')
92
+ .description('Make authenticated API request via Pipedream proxy')
93
+ .option('-X, --method <method>', 'HTTP method (GET|POST|PUT|DELETE|PATCH)', 'GET')
94
+ .option('-u, --user <id>', 'User ID')
95
+ .option('-d, --data <json>', 'Request body as JSON string')
96
+ .option('-H, --header <header>', 'Custom header (repeatable)', (v, p: string[]) => p.concat([v]), [] as string[])
97
+ .option('-q, --query <param>', 'Query parameter (repeatable)', (v, p: string[]) => p.concat([v]), [] as string[])
98
+ .option('-j, --json', 'Output response as JSON (compact)')
99
+ .option('-v, --verbose', 'Show request details')
100
+ .action(proxyCommand);
101
+
102
+ // Quick auth link helper
103
+ program
104
+ .command('link <app>')
105
+ .description('Alias for connect - generate OAuth link')
106
+ .option('-u, --user <id>', 'User ID')
107
+ .option('-j, --json', 'Output as JSON')
108
+ .option('-c, --copy', 'Copy to clipboard')
109
+ .action(connectCommand);
110
+
111
+ program.parse();
@@ -0,0 +1,88 @@
1
+ import chalk from 'chalk';
2
+ import ora from 'ora';
3
+ import { searchApps, getApp } from '../pipedream.js';
4
+ import { isConfigured } from '../config.js';
5
+
6
+ export async function appsCommand(options: { search?: string; json?: boolean }): Promise<void> {
7
+ if (!isConfigured()) {
8
+ console.error(chalk.red('Not configured. Run: pdauth config'));
9
+ process.exit(1);
10
+ }
11
+
12
+ const spinner = ora('Fetching apps...').start();
13
+
14
+ try {
15
+ const apps = await searchApps(options.search);
16
+ spinner.stop();
17
+
18
+ if (options.json) {
19
+ console.log(JSON.stringify(apps, null, 2));
20
+ return;
21
+ }
22
+
23
+ if (apps.length === 0) {
24
+ console.log(chalk.yellow(`No apps found${options.search ? ` for "${options.search}"` : ''}`));
25
+ return;
26
+ }
27
+
28
+ console.log(chalk.bold(`\nšŸ“¦ Available Apps${options.search ? ` matching "${options.search}"` : ''}\n`));
29
+
30
+ for (const app of apps.slice(0, 50)) {
31
+ console.log(` ${chalk.cyan(app.nameSlug.padEnd(30))} ${chalk.gray(app.name)}`);
32
+ if (app.description) {
33
+ console.log(` ${' '.repeat(30)} ${chalk.dim(app.description.slice(0, 60))}${app.description.length > 60 ? '...' : ''}`);
34
+ }
35
+ }
36
+
37
+ if (apps.length > 50) {
38
+ console.log(chalk.gray(`\n ... and ${apps.length - 50} more. Use --search to filter.`));
39
+ }
40
+
41
+ console.log(chalk.gray(`\n Total: ${apps.length} apps\n`));
42
+ console.log(chalk.gray(' Browse all: https://mcp.pipedream.com\n'));
43
+ } catch (error) {
44
+ spinner.fail('Failed to fetch apps');
45
+ console.error(chalk.red(error instanceof Error ? error.message : String(error)));
46
+ process.exit(1);
47
+ }
48
+ }
49
+
50
+ export async function appInfoCommand(appSlug: string, options: { json?: boolean }): Promise<void> {
51
+ if (!isConfigured()) {
52
+ console.error(chalk.red('Not configured. Run: pdauth config'));
53
+ process.exit(1);
54
+ }
55
+
56
+ const spinner = ora(`Fetching ${appSlug}...`).start();
57
+
58
+ try {
59
+ const app = await getApp(appSlug);
60
+ spinner.stop();
61
+
62
+ if (!app) {
63
+ console.error(chalk.red(`App not found: ${appSlug}`));
64
+ process.exit(1);
65
+ }
66
+
67
+ if (options.json) {
68
+ console.log(JSON.stringify(app, null, 2));
69
+ return;
70
+ }
71
+
72
+ console.log(chalk.bold(`\nšŸ“¦ ${app.name}\n`));
73
+ console.log(` ${chalk.gray('Slug:')} ${chalk.cyan(app.nameSlug)}`);
74
+ console.log(` ${chalk.gray('Auth:')} ${app.authType}`);
75
+ if (app.description) {
76
+ console.log(` ${chalk.gray('Description:')} ${app.description}`);
77
+ }
78
+ if (app.categories?.length) {
79
+ console.log(` ${chalk.gray('Categories:')} ${app.categories.join(', ')}`);
80
+ }
81
+ console.log(` ${chalk.gray('MCP Server:')} https://mcp.pipedream.com/app/${app.nameSlug}`);
82
+ console.log();
83
+ } catch (error) {
84
+ spinner.fail('Failed to fetch app');
85
+ console.error(chalk.red(error instanceof Error ? error.message : String(error)));
86
+ process.exit(1);
87
+ }
88
+ }
@@ -0,0 +1,68 @@
1
+ import chalk from 'chalk';
2
+ import { getConfig, setConfig, isConfigured, getConfigPath, type PdAuthConfig } from '../config.js';
3
+ import { createInterface } from 'readline';
4
+
5
+ async function prompt(question: string, defaultValue?: string): Promise<string> {
6
+ const rl = createInterface({
7
+ input: process.stdin,
8
+ output: process.stdout,
9
+ });
10
+
11
+ return new Promise((resolve) => {
12
+ const displayDefault = defaultValue ? ` [${defaultValue}]` : '';
13
+ rl.question(`${question}${displayDefault}: `, (answer) => {
14
+ rl.close();
15
+ resolve(answer.trim() || defaultValue || '');
16
+ });
17
+ });
18
+ }
19
+
20
+ export async function configCommand(options: { show?: boolean; set?: string }): Promise<void> {
21
+ if (options.show) {
22
+ const cfg = getConfig();
23
+ console.log(chalk.bold('\nšŸ“‹ Current Configuration\n'));
24
+ console.log(` ${chalk.gray('Path:')} ${getConfigPath()}`);
25
+ console.log(` ${chalk.gray('Client ID:')} ${cfg.clientId ? chalk.green(cfg.clientId.slice(0, 8) + '...') : chalk.red('not set')}`);
26
+ console.log(` ${chalk.gray('Client Secret:')} ${cfg.clientSecret ? chalk.green('********') : chalk.red('not set')}`);
27
+ console.log(` ${chalk.gray('Project ID:')} ${cfg.projectId ? chalk.green(cfg.projectId) : chalk.red('not set')}`);
28
+ console.log(` ${chalk.gray('Environment:')} ${chalk.cyan(cfg.environment)}`);
29
+ console.log(` ${chalk.gray('Status:')} ${isConfigured() ? chalk.green('āœ“ Configured') : chalk.red('āœ— Not configured')}\n`);
30
+ return;
31
+ }
32
+
33
+ if (options.set) {
34
+ // Format: key=value
35
+ const [key, ...valueParts] = options.set.split('=');
36
+ const value = valueParts.join('=');
37
+
38
+ const validKeys = ['clientId', 'clientSecret', 'projectId', 'environment'];
39
+ if (!validKeys.includes(key)) {
40
+ console.error(chalk.red(`Invalid key: ${key}. Valid keys: ${validKeys.join(', ')}`));
41
+ process.exit(1);
42
+ }
43
+
44
+ setConfig({ [key]: value } as Partial<PdAuthConfig>);
45
+ console.log(chalk.green(`āœ“ Set ${key}`));
46
+ return;
47
+ }
48
+
49
+ // Interactive configuration
50
+ console.log(chalk.bold('\nšŸ” Pipedream OAuth Configuration\n'));
51
+ console.log(chalk.gray('Get your credentials at: https://pipedream.com/settings/api\n'));
52
+
53
+ const current = getConfig();
54
+
55
+ const clientId = await prompt('Client ID', current.clientId || undefined);
56
+ const clientSecret = await prompt('Client Secret', current.clientSecret ? '********' : undefined);
57
+ const projectId = await prompt('Project ID', current.projectId || undefined);
58
+ const environment = await prompt('Environment (development/production)', current.environment || 'development');
59
+
60
+ setConfig({
61
+ clientId,
62
+ clientSecret: clientSecret === '********' ? current.clientSecret : clientSecret,
63
+ projectId,
64
+ environment: environment as 'development' | 'production',
65
+ });
66
+
67
+ console.log(chalk.green('\nāœ“ Configuration saved!\n'));
68
+ }
@@ -0,0 +1,76 @@
1
+ import chalk from 'chalk';
2
+ import ora from 'ora';
3
+ import { createConnectLink, getApp } from '../pipedream.js';
4
+ import { isConfigured, getDefaultUser, addUser } from '../config.js';
5
+
6
+ export async function connectCommand(
7
+ appSlug: string,
8
+ options: { user?: string; json?: boolean; copy?: boolean }
9
+ ): Promise<void> {
10
+ if (!isConfigured()) {
11
+ console.error(chalk.red('Not configured. Run: pdauth config'));
12
+ process.exit(1);
13
+ }
14
+
15
+ const userId = options.user || getDefaultUser();
16
+ addUser(userId);
17
+
18
+ const spinner = ora(`Generating OAuth link for ${appSlug}...`).start();
19
+
20
+ try {
21
+ // Verify app exists
22
+ const app = await getApp(appSlug);
23
+ if (!app) {
24
+ spinner.fail(`App not found: ${appSlug}`);
25
+ console.log(chalk.gray('Run: pdauth apps --search <query> to find available apps'));
26
+ process.exit(1);
27
+ }
28
+
29
+ const result = await createConnectLink(userId, appSlug);
30
+ spinner.stop();
31
+
32
+ if (options.json) {
33
+ console.log(JSON.stringify({
34
+ app: appSlug,
35
+ appName: app.name,
36
+ user: userId,
37
+ connectLinkUrl: result.connectLinkUrl,
38
+ expiresAt: result.expiresAt,
39
+ }, null, 2));
40
+ return;
41
+ }
42
+
43
+ console.log(chalk.bold(`\nšŸ”— OAuth Link for ${chalk.cyan(app.name)}\n`));
44
+ console.log(` ${chalk.gray('User:')} ${userId}`);
45
+ console.log(` ${chalk.gray('Expires:')} ${new Date(result.expiresAt).toLocaleString()}\n`);
46
+
47
+ console.log(chalk.bold(' Link:\n'));
48
+ console.log(` ${chalk.underline.blue(result.connectLinkUrl)}\n`);
49
+
50
+ console.log(chalk.gray(' Share this link to authorize access.\n'));
51
+ console.log(chalk.gray(' After authorization, run: pdauth status\n'));
52
+
53
+ // Try to copy to clipboard (cross-platform)
54
+ if (options.copy) {
55
+ try {
56
+ const { execSync } = await import('child_process');
57
+ const platform = process.platform;
58
+
59
+ if (platform === 'darwin') {
60
+ execSync(`echo "${result.connectLinkUrl}" | pbcopy`);
61
+ } else if (platform === 'linux') {
62
+ execSync(`echo "${result.connectLinkUrl}" | xclip -selection clipboard 2>/dev/null || echo "${result.connectLinkUrl}" | xsel --clipboard 2>/dev/null`);
63
+ } else if (platform === 'win32') {
64
+ execSync(`echo ${result.connectLinkUrl} | clip`);
65
+ }
66
+ console.log(chalk.green(' āœ“ Copied to clipboard!\n'));
67
+ } catch {
68
+ // Clipboard not available, skip silently
69
+ }
70
+ }
71
+ } catch (error) {
72
+ spinner.fail('Failed to generate link');
73
+ console.error(chalk.red(error instanceof Error ? error.message : String(error)));
74
+ process.exit(1);
75
+ }
76
+ }
@@ -0,0 +1,6 @@
1
+ export { configCommand } from './config.js';
2
+ export { appsCommand, appInfoCommand } from './apps.js';
3
+ export { connectCommand } from './connect.js';
4
+ export { statusCommand, disconnectCommand } from './status.js';
5
+ export { toolsCommand, callCommand } from './tools.js';
6
+ export { proxyCommand } from './proxy.js';
@@ -0,0 +1,155 @@
1
+ import chalk from 'chalk';
2
+ import ora from 'ora';
3
+ import { getClient, listAccounts } from '../pipedream.js';
4
+ import { isConfigured, getDefaultUser } from '../config.js';
5
+
6
+ interface ProxyOptions {
7
+ method?: string;
8
+ user?: string;
9
+ data?: string;
10
+ header?: string[];
11
+ query?: string[];
12
+ json?: boolean;
13
+ verbose?: boolean;
14
+ }
15
+
16
+ function parseParams(params: string[]): Record<string, string> {
17
+ const result: Record<string, string> = {};
18
+ for (const p of params) {
19
+ const [key, ...rest] = p.split('=');
20
+ if (key) {
21
+ result[key] = rest.join('=');
22
+ }
23
+ }
24
+ return result;
25
+ }
26
+
27
+ function parseHeaders(headers: string[]): Record<string, string> {
28
+ const result: Record<string, string> = {};
29
+ for (const h of headers) {
30
+ const colonIndex = h.indexOf(':');
31
+ if (colonIndex > 0) {
32
+ const key = h.slice(0, colonIndex).trim();
33
+ const value = h.slice(colonIndex + 1).trim();
34
+ result[key] = value;
35
+ }
36
+ }
37
+ return result;
38
+ }
39
+
40
+ export async function proxyCommand(
41
+ app: string,
42
+ path: string,
43
+ options: ProxyOptions
44
+ ): Promise<void> {
45
+ if (!isConfigured()) {
46
+ console.error(chalk.red('Not configured. Run: pdauth config'));
47
+ process.exit(1);
48
+ }
49
+
50
+ const userId = options.user || getDefaultUser();
51
+ const method = (options.method || 'GET').toUpperCase();
52
+
53
+ if (options.verbose) {
54
+ console.log(chalk.gray(`User: ${userId}`));
55
+ console.log(chalk.gray(`App: ${app}`));
56
+ console.log(chalk.gray(`Path: ${path}`));
57
+ console.log(chalk.gray(`Method: ${method}`));
58
+ }
59
+
60
+ const spinner = ora('Finding account...').start();
61
+
62
+ try {
63
+ // 1. Get account ID for app + user
64
+ const accounts = await listAccounts(userId);
65
+ const account = accounts.find(a => a.app.nameSlug === app);
66
+
67
+ if (!account) {
68
+ spinner.fail(`No ${app} account connected for user ${userId}`);
69
+ const connectedApps = accounts.map(a => a.app.nameSlug).join(', ');
70
+ console.log(chalk.gray(`Connected apps: ${connectedApps || 'none'}`));
71
+ console.log(chalk.gray(`\nRun: pdauth connect ${app} --user ${userId}`));
72
+ process.exit(1);
73
+ }
74
+
75
+ if (options.verbose) {
76
+ spinner.succeed(`Using account: ${account.name} (${account.id})`);
77
+ }
78
+
79
+ // 2. Build request
80
+ const client = getClient();
81
+ const params = parseParams(options.query || []);
82
+ const headers = parseHeaders(options.header || []);
83
+
84
+ let body: Record<string, unknown> | undefined;
85
+ if (options.data) {
86
+ try {
87
+ body = JSON.parse(options.data);
88
+ } catch (e) {
89
+ spinner.fail('Invalid JSON in --data');
90
+ process.exit(1);
91
+ }
92
+ }
93
+
94
+ const request = {
95
+ url: path,
96
+ externalUserId: userId,
97
+ accountId: account.id,
98
+ params: Object.keys(params).length > 0 ? params : undefined,
99
+ headers: Object.keys(headers).length > 0 ? headers : undefined,
100
+ body,
101
+ };
102
+
103
+ if (options.verbose) {
104
+ console.log(chalk.gray('\nRequest:'));
105
+ console.log(chalk.gray(JSON.stringify(request, null, 2)));
106
+ console.log();
107
+ }
108
+
109
+ spinner.text = `${method} ${path}...`;
110
+
111
+ // 3. Call proxy based on method
112
+ let response: unknown;
113
+
114
+ switch (method) {
115
+ case 'GET':
116
+ response = await client.proxy.get(request);
117
+ break;
118
+ case 'POST':
119
+ response = await client.proxy.post(request as Parameters<typeof client.proxy.post>[0]);
120
+ break;
121
+ case 'PUT':
122
+ response = await client.proxy.put(request as Parameters<typeof client.proxy.put>[0]);
123
+ break;
124
+ case 'DELETE':
125
+ response = await client.proxy.delete(request);
126
+ break;
127
+ case 'PATCH':
128
+ response = await client.proxy.patch(request as Parameters<typeof client.proxy.patch>[0]);
129
+ break;
130
+ default:
131
+ spinner.fail(`Unsupported method: ${method}`);
132
+ process.exit(1);
133
+ }
134
+
135
+ spinner.stop();
136
+
137
+ // 4. Output response
138
+ if (options.json) {
139
+ console.log(JSON.stringify(response));
140
+ } else if (typeof response === 'object' && response !== null) {
141
+ console.log(JSON.stringify(response, null, 2));
142
+ } else {
143
+ console.log(response);
144
+ }
145
+ } catch (error) {
146
+ spinner.fail('Request failed');
147
+ const message = error instanceof Error ? error.message : String(error);
148
+ console.error(chalk.red(message));
149
+
150
+ if (options.verbose && error instanceof Error && error.stack) {
151
+ console.error(chalk.gray(error.stack));
152
+ }
153
+ process.exit(1);
154
+ }
155
+ }
@@ -0,0 +1,143 @@
1
+ import chalk from 'chalk';
2
+ import ora from 'ora';
3
+ import { listAccounts, deleteAccount } from '../pipedream.js';
4
+ import { isConfigured, getDefaultUser, getUsers } from '../config.js';
5
+
6
+ export async function statusCommand(options: { user?: string; json?: boolean; all?: boolean }): Promise<void> {
7
+ if (!isConfigured()) {
8
+ console.error(chalk.red('Not configured. Run: pdauth config'));
9
+ process.exit(1);
10
+ }
11
+
12
+ const spinner = ora('Fetching connected accounts...').start();
13
+
14
+ try {
15
+ if (options.all) {
16
+ // Show all users
17
+ const users = getUsers();
18
+ const userIds = Object.keys(users);
19
+
20
+ if (userIds.length === 0) {
21
+ spinner.stop();
22
+ console.log(chalk.yellow('No users registered. Run: pdauth connect <app> --user <id>'));
23
+ return;
24
+ }
25
+
26
+ const allAccounts: Array<{ userId: string; accounts: Awaited<ReturnType<typeof listAccounts>> }> = [];
27
+
28
+ for (const userId of userIds) {
29
+ const accounts = await listAccounts(userId);
30
+ allAccounts.push({ userId, accounts });
31
+ }
32
+
33
+ spinner.stop();
34
+
35
+ if (options.json) {
36
+ console.log(JSON.stringify(allAccounts, null, 2));
37
+ return;
38
+ }
39
+
40
+ console.log(chalk.bold('\nšŸ“Š All Connected Accounts\n'));
41
+
42
+ for (const { userId, accounts } of allAccounts) {
43
+ const userMeta = users[userId];
44
+ const label = userMeta?.label ? ` (${userMeta.label})` : '';
45
+ console.log(chalk.bold(` šŸ‘¤ ${userId}${label}`));
46
+
47
+ if (accounts.length === 0) {
48
+ console.log(chalk.gray(' No connected accounts\n'));
49
+ } else {
50
+ for (const account of accounts) {
51
+ const status = account.dead ? chalk.red('āœ— dead') :
52
+ account.healthy ? chalk.green('āœ“ healthy') : chalk.yellow('⚠ unhealthy');
53
+ console.log(` ${chalk.cyan(account.app.nameSlug.padEnd(25))} ${status}`);
54
+ }
55
+ console.log();
56
+ }
57
+ }
58
+ return;
59
+ }
60
+
61
+ const userId = options.user || getDefaultUser();
62
+ const accounts = await listAccounts(userId);
63
+ spinner.stop();
64
+
65
+ if (options.json) {
66
+ console.log(JSON.stringify({ user: userId, accounts }, null, 2));
67
+ return;
68
+ }
69
+
70
+ console.log(chalk.bold(`\nšŸ“Š Connected Accounts for ${chalk.cyan(userId)}\n`));
71
+
72
+ if (accounts.length === 0) {
73
+ console.log(chalk.yellow(' No connected accounts.\n'));
74
+ console.log(chalk.gray(' Run: pdauth connect <app> to authorize an app\n'));
75
+ return;
76
+ }
77
+
78
+ for (const account of accounts) {
79
+ const status = account.dead ? chalk.red('āœ— dead') :
80
+ account.healthy ? chalk.green('āœ“ healthy') : chalk.yellow('⚠ unhealthy');
81
+
82
+ console.log(` ${chalk.cyan(account.app.nameSlug.padEnd(25))} ${account.app.name}`);
83
+ console.log(` ${' '.repeat(25)} ${chalk.gray('Status:')} ${status}`);
84
+ console.log(` ${' '.repeat(25)} ${chalk.gray('Account ID:')} ${account.id}`);
85
+ console.log(` ${' '.repeat(25)} ${chalk.gray('Connected:')} ${new Date(account.createdAt).toLocaleDateString()}`);
86
+ console.log();
87
+ }
88
+
89
+ console.log(chalk.gray(` Total: ${accounts.length} connected app(s)\n`));
90
+ } catch (error) {
91
+ spinner.fail('Failed to fetch accounts');
92
+ console.error(chalk.red(error instanceof Error ? error.message : String(error)));
93
+ process.exit(1);
94
+ }
95
+ }
96
+
97
+ export async function disconnectCommand(
98
+ appSlugOrAccountId: string,
99
+ options: { user?: string; force?: boolean }
100
+ ): Promise<void> {
101
+ if (!isConfigured()) {
102
+ console.error(chalk.red('Not configured. Run: pdauth config'));
103
+ process.exit(1);
104
+ }
105
+
106
+ const userId = options.user || getDefaultUser();
107
+ const spinner = ora('Finding account...').start();
108
+
109
+ try {
110
+ const accounts = await listAccounts(userId);
111
+
112
+ // Find account by app slug or account ID
113
+ const account = accounts.find(
114
+ a => a.app.nameSlug === appSlugOrAccountId || a.id === appSlugOrAccountId
115
+ );
116
+
117
+ if (!account) {
118
+ spinner.fail(`No connected account found: ${appSlugOrAccountId}`);
119
+ process.exit(1);
120
+ }
121
+
122
+ if (!options.force) {
123
+ spinner.stop();
124
+ console.log(chalk.yellow(`\nāš ļø This will disconnect ${chalk.cyan(account.app.name)} for user ${userId}`));
125
+ console.log(chalk.gray(' Run with --force to confirm\n'));
126
+ process.exit(0);
127
+ }
128
+
129
+ spinner.text = `Disconnecting ${account.app.name}...`;
130
+ const success = await deleteAccount(account.id);
131
+
132
+ if (success) {
133
+ spinner.succeed(`Disconnected ${account.app.name}`);
134
+ } else {
135
+ spinner.fail(`Failed to disconnect ${account.app.name}`);
136
+ process.exit(1);
137
+ }
138
+ } catch (error) {
139
+ spinner.fail('Failed to disconnect');
140
+ console.error(chalk.red(error instanceof Error ? error.message : String(error)));
141
+ process.exit(1);
142
+ }
143
+ }