paymongo-cli 1.4.11 → 1.4.13

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 (46) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/README.md +4 -3
  3. package/biome.json +72 -0
  4. package/dist/.tsbuildinfo +1 -1
  5. package/dist/commands/config/actions.js +13 -5
  6. package/dist/commands/config/analytics.js +75 -0
  7. package/dist/commands/config/helpers.js +14 -25
  8. package/dist/commands/config/rate-limit.js +3 -3
  9. package/dist/commands/config.js +7 -1
  10. package/dist/commands/dev/logs.js +13 -4
  11. package/dist/commands/dev/status.js +1 -1
  12. package/dist/commands/dev/stop.js +2 -2
  13. package/dist/commands/dev.js +10 -258
  14. package/dist/commands/doctor.js +241 -0
  15. package/dist/commands/env.js +10 -19
  16. package/dist/commands/generate/templates/index.js +3 -3
  17. package/dist/commands/generate.js +6 -6
  18. package/dist/commands/init.js +22 -36
  19. package/dist/commands/login.js +18 -29
  20. package/dist/commands/payments/actions.js +15 -15
  21. package/dist/commands/payments/helpers.js +6 -24
  22. package/dist/commands/payments.js +1 -1
  23. package/dist/commands/shared/auth.js +23 -0
  24. package/dist/commands/shared/runtime.js +35 -0
  25. package/dist/commands/team/index.js +3 -3
  26. package/dist/commands/trigger/actions.js +2 -2
  27. package/dist/commands/trigger/helpers.js +13 -9
  28. package/dist/commands/trigger.js +2 -2
  29. package/dist/commands/webhooks/actions.js +11 -11
  30. package/dist/commands/webhooks/helpers.js +5 -23
  31. package/dist/commands/webhooks.js +1 -1
  32. package/dist/index.js +37 -14
  33. package/dist/services/analytics/service.js +3 -3
  34. package/dist/services/api/client.js +8 -4
  35. package/dist/services/config/manager.js +3 -3
  36. package/dist/services/dev/process-manager.js +4 -4
  37. package/dist/services/dev/server.js +4 -6
  38. package/dist/services/dev/session.js +353 -0
  39. package/dist/services/team/service.js +1 -1
  40. package/dist/utils/bulk.js +11 -11
  41. package/dist/utils/cache.js +5 -5
  42. package/dist/utils/constants.js +1 -1
  43. package/dist/utils/webhook-store.js +3 -3
  44. package/package.json +11 -25
  45. package/vitest.config.ts +18 -0
  46. package/eslint.config.ts +0 -70
@@ -1,6 +1,6 @@
1
+ import * as fs from 'node:fs';
2
+ import * as path from 'node:path';
1
3
  import chalk from 'chalk';
2
- import * as fs from 'fs';
3
- import * as path from 'path';
4
4
  import { CommandError } from '../../utils/errors.js';
5
5
  import { checkConfigConflicts, createConfigContext, handleCommandFailure, loadRequiredConfig, setConfigValue, validateImportedConfig, validateImportedConfigWithSchema, } from './helpers.js';
6
6
  export async function showAction(options) {
@@ -19,7 +19,7 @@ export async function showAction(options) {
19
19
  console.log(JSON.stringify(config, null, 2));
20
20
  return;
21
21
  }
22
- console.log('\n' + chalk.bold('Configuration (.paymongo)'));
22
+ console.log(`\n${chalk.bold('Configuration (.paymongo)')}`);
23
23
  console.log('');
24
24
  console.log(chalk.bold('Project:'), config.projectName);
25
25
  console.log(chalk.bold('Environment:'), config.environment);
@@ -32,6 +32,10 @@ export async function showAction(options) {
32
32
  console.log(chalk.bold('Auto Register Webhook:'), config.dev.autoRegisterWebhook ? 'Yes' : 'No');
33
33
  console.log(chalk.bold('Verify Webhook Signatures:'), config.dev.verifyWebhookSignatures ? 'Yes' : 'No');
34
34
  console.log('');
35
+ if (config.analytics) {
36
+ console.log(chalk.bold('Analytics:'), config.analytics.enabled ? 'Enabled' : 'Disabled');
37
+ console.log('');
38
+ }
35
39
  if (config.rateLimiting) {
36
40
  console.log(chalk.bold('Rate Limiting:'));
37
41
  console.log(chalk.bold(' Enabled:'), config.rateLimiting.enabled ? chalk.green('Yes') : chalk.red('No'));
@@ -190,7 +194,9 @@ export async function importAction(filePath, options) {
190
194
  if (validationErrors.length > 0) {
191
195
  spinner.fail('Invalid configuration');
192
196
  console.error(chalk.red('❌ Configuration validation failed:'));
193
- validationErrors.forEach((err) => console.error(chalk.gray(` • ${err}`)));
197
+ validationErrors.forEach((err) => {
198
+ console.error(chalk.gray(` • ${err}`));
199
+ });
194
200
  throw new CommandError();
195
201
  }
196
202
  spinner.succeed('Configuration validated');
@@ -201,7 +207,9 @@ export async function importAction(filePath, options) {
201
207
  if (conflicts.length > 0) {
202
208
  spinner.stop();
203
209
  console.log(chalk.yellow('⚠️ Configuration conflicts detected:'));
204
- conflicts.forEach((conflict) => console.log(chalk.gray(` • ${conflict}`)));
210
+ conflicts.forEach((conflict) => {
211
+ console.log(chalk.gray(` • ${conflict}`));
212
+ });
205
213
  console.log('');
206
214
  console.log(chalk.bold('Use --force to overwrite existing configuration'));
207
215
  throw new CommandError();
@@ -0,0 +1,75 @@
1
+ import chalk from 'chalk';
2
+ import { CommandError } from '../../utils/errors.js';
3
+ import { createConfigContext, ensureAnalyticsConfig, loadRequiredConfig } from './helpers.js';
4
+ export async function analyticsEnableAction() {
5
+ const { spinner, configManager } = createConfigContext();
6
+ try {
7
+ const config = await loadRequiredConfig(spinner, configManager);
8
+ if (!config) {
9
+ return;
10
+ }
11
+ ensureAnalyticsConfig(config).enabled = true;
12
+ spinner.start('Enabling analytics...');
13
+ await configManager.save(config);
14
+ spinner.succeed('Analytics enabled');
15
+ console.log(chalk.green('✓ Analytics enabled'));
16
+ console.log(chalk.gray('Webhook events will now be tracked locally on this machine'));
17
+ }
18
+ catch (error) {
19
+ spinner.stop();
20
+ const err = error;
21
+ console.error(chalk.red('❌ Failed to enable analytics:'), err.message);
22
+ throw new CommandError();
23
+ }
24
+ }
25
+ export async function analyticsDisableAction() {
26
+ const { spinner, configManager } = createConfigContext();
27
+ try {
28
+ const config = await loadRequiredConfig(spinner, configManager);
29
+ if (!config) {
30
+ return;
31
+ }
32
+ ensureAnalyticsConfig(config).enabled = false;
33
+ spinner.start('Disabling analytics...');
34
+ await configManager.save(config);
35
+ spinner.succeed('Analytics disabled');
36
+ console.log(chalk.green('✓ Analytics disabled'));
37
+ console.log(chalk.gray('Existing analytics data remains local until you remove it manually'));
38
+ }
39
+ catch (error) {
40
+ spinner.stop();
41
+ const err = error;
42
+ console.error(chalk.red('❌ Failed to disable analytics:'), err.message);
43
+ throw new CommandError();
44
+ }
45
+ }
46
+ export async function analyticsStatusAction() {
47
+ const { spinner, configManager } = createConfigContext();
48
+ try {
49
+ const config = await loadRequiredConfig(spinner, configManager);
50
+ if (!config) {
51
+ return;
52
+ }
53
+ console.log(`\n${chalk.bold('Analytics Status')}`);
54
+ console.log('');
55
+ if (!config.analytics?.enabled) {
56
+ console.log(chalk.yellow('Status: Disabled'));
57
+ console.log(chalk.gray('Webhook analytics are currently opt-out for this project'));
58
+ console.log('');
59
+ console.log(chalk.gray("Run 'paymongo config analytics enable' to enable"));
60
+ return;
61
+ }
62
+ console.log(chalk.green('Status: Enabled'));
63
+ console.log(chalk.gray('Webhook analytics are stored locally and never sent externally'));
64
+ console.log('');
65
+ console.log(chalk.gray('Commands:'));
66
+ console.log(chalk.gray("• 'paymongo config analytics disable' - Disable analytics"));
67
+ console.log(chalk.gray("• 'paymongo config show' - View current analytics setting"));
68
+ }
69
+ catch (error) {
70
+ spinner.stop();
71
+ const err = error;
72
+ console.error(chalk.red('❌ Failed to check analytics status:'), err.message);
73
+ throw new CommandError();
74
+ }
75
+ }
@@ -1,8 +1,5 @@
1
- import chalk from 'chalk';
2
1
  import { validateConfig as zodValidateConfig } from '../../types/schemas.js';
3
- import ConfigManager from '../../services/config/manager.js';
4
- import Spinner from '../../utils/spinner.js';
5
- import { CommandError } from '../../utils/errors.js';
2
+ import { createCommandContext, failCommand, loadCommandConfig } from '../shared/runtime.js';
6
3
  export const CONFIG_KEY_MAPPINGS = {
7
4
  'project.name': 'projectName',
8
5
  'webhook.url': 'webhooks.url',
@@ -10,35 +7,19 @@ export const CONFIG_KEY_MAPPINGS = {
10
7
  'dev.port': 'dev.port',
11
8
  'dev.autoRegister': 'dev.autoRegisterWebhook',
12
9
  'dev.verifySignatures': 'dev.verifyWebhookSignatures',
10
+ 'analytics.enabled': 'analytics.enabled',
13
11
  'rateLimit.enabled': 'rateLimiting.enabled',
14
12
  'rateLimit.maxRequests': 'rateLimiting.maxRequests',
15
13
  'rateLimit.windowMs': 'rateLimiting.windowMs',
16
14
  };
17
15
  export function createConfigContext() {
18
- return {
19
- spinner: new Spinner(),
20
- configManager: new ConfigManager(),
21
- };
22
- }
23
- export function showNoConfigMessage(message = "Run 'paymongo init' to set up your project first.") {
24
- console.log(chalk.yellow('No PayMongo configuration found.'));
25
- console.log(chalk.gray(message));
16
+ return createCommandContext();
26
17
  }
27
18
  export async function loadRequiredConfig(spinner, configManager, loadingText = 'Loading configuration...') {
28
- spinner.start(loadingText);
29
- const config = await configManager.load();
30
- if (!config) {
31
- spinner.fail('No configuration found');
32
- showNoConfigMessage();
33
- return null;
34
- }
35
- spinner.succeed('Configuration loaded');
36
- return config;
19
+ return loadCommandConfig(spinner, configManager, loadingText);
37
20
  }
38
21
  export function handleCommandFailure(prefix, error) {
39
- const err = error;
40
- console.error(chalk.red(prefix), err.message);
41
- throw new CommandError();
22
+ return failCommand(prefix, error);
42
23
  }
43
24
  export function validateImportedConfig(config) {
44
25
  if (typeof config !== 'object' || config === null) {
@@ -135,7 +116,7 @@ export function coerceConfigValue(value) {
135
116
  if (value === 'false') {
136
117
  return false;
137
118
  }
138
- if (!isNaN(Number(value))) {
119
+ if (!Number.isNaN(Number(value))) {
139
120
  return Number(value);
140
121
  }
141
122
  return value;
@@ -151,3 +132,11 @@ export function ensureRateLimitingConfig(config) {
151
132
  }
152
133
  return config.rateLimiting;
153
134
  }
135
+ export function ensureAnalyticsConfig(config) {
136
+ if (!config.analytics) {
137
+ config.analytics = {
138
+ enabled: false,
139
+ };
140
+ }
141
+ return config.analytics;
142
+ }
@@ -3,7 +3,7 @@ import { CommandError } from '../../utils/errors.js';
3
3
  import { createConfigContext, ensureRateLimitingConfig, loadRequiredConfig } from './helpers.js';
4
4
  function parsePositiveInt(value, message) {
5
5
  const parsed = parseInt(value, 10);
6
- if (isNaN(parsed) || parsed < 1) {
6
+ if (Number.isNaN(parsed) || parsed < 1) {
7
7
  console.error(chalk.red(message));
8
8
  throw new CommandError();
9
9
  }
@@ -102,9 +102,9 @@ export async function rateLimitStatusAction() {
102
102
  if (!config) {
103
103
  return;
104
104
  }
105
- console.log('\n' + chalk.bold('Rate Limiting Status'));
105
+ console.log(`\n${chalk.bold('Rate Limiting Status')}`);
106
106
  console.log('');
107
- if (!config.rateLimiting || !config.rateLimiting.enabled) {
107
+ if (!config.rateLimiting?.enabled) {
108
108
  console.log(chalk.yellow('Status: Disabled'));
109
109
  console.log(chalk.gray('Rate limiting is not currently active'));
110
110
  console.log('');
@@ -1,5 +1,6 @@
1
1
  import { Command } from 'commander';
2
2
  import { backupAction, importAction, resetAction, setAction, showAction, } from './config/actions.js';
3
+ import { analyticsDisableAction, analyticsEnableAction, analyticsStatusAction, } from './config/analytics.js';
3
4
  import { rateLimitDisableAction, rateLimitEnableAction, rateLimitSetMaxRequestsAction, rateLimitSetWindowAction, rateLimitStatusAction, } from './config/rate-limit.js';
4
5
  const command = new Command('config');
5
6
  command
@@ -23,6 +24,11 @@ command
23
24
  .arguments('<file>')
24
25
  .option('-f, --force', 'Overwrite existing configuration without confirmation')
25
26
  .action(importAction))
27
+ .addCommand(new Command('analytics')
28
+ .description('Configure local webhook analytics')
29
+ .addCommand(new Command('enable').description('Enable analytics').action(analyticsEnableAction))
30
+ .addCommand(new Command('disable').description('Disable analytics').action(analyticsDisableAction))
31
+ .addCommand(new Command('status').description('Show analytics status').action(analyticsStatusAction)))
26
32
  .addCommand(new Command('rate-limit')
27
33
  .description('Configure rate limiting settings')
28
34
  .addCommand(new Command('enable').description('Enable rate limiting').action(rateLimitEnableAction))
@@ -38,5 +44,5 @@ command
38
44
  .addCommand(new Command('status')
39
45
  .description('Show current rate limiting settings')
40
46
  .action(rateLimitStatusAction)));
41
- export { showAction, setAction, backupAction, resetAction, importAction, rateLimitEnableAction, rateLimitDisableAction, rateLimitSetMaxRequestsAction, rateLimitSetWindowAction, rateLimitStatusAction, };
47
+ export { analyticsDisableAction, analyticsEnableAction, analyticsStatusAction, backupAction, importAction, rateLimitDisableAction, rateLimitEnableAction, rateLimitSetMaxRequestsAction, rateLimitSetWindowAction, rateLimitStatusAction, resetAction, setAction, showAction, };
42
48
  export default command;
@@ -1,6 +1,6 @@
1
- import { Command } from 'commander';
2
- import * as fs from 'fs';
1
+ import * as fs from 'node:fs';
3
2
  import chalk from 'chalk';
3
+ import { Command } from 'commander';
4
4
  import { DevProcessManager } from '../../services/dev/process-manager.js';
5
5
  const logsCommand = new Command('logs')
6
6
  .description('View dev server logs')
@@ -14,7 +14,7 @@ const logsCommand = new Command('logs')
14
14
  return;
15
15
  }
16
16
  const logFile = await DevProcessManager.getLogFile();
17
- const lines = await DevProcessManager.readLogs(parseInt(options.lines));
17
+ const lines = await DevProcessManager.readLogs(parseInt(options.lines, 10));
18
18
  if (lines.length === 0) {
19
19
  console.log(chalk.yellow('No logs available.'));
20
20
  console.log(chalk.gray('Log file:'), logFile);
@@ -24,11 +24,18 @@ const logsCommand = new Command('logs')
24
24
  console.log(chalk.gray(`(Last ${lines.length} lines from ${logFile})`));
25
25
  console.log(chalk.gray('─'.repeat(60)));
26
26
  console.log('');
27
- lines.forEach((line) => console.log(line));
27
+ lines.forEach((line) => {
28
+ console.log(line);
29
+ });
28
30
  if (options.follow) {
29
31
  console.log('');
30
32
  console.log(chalk.gray('Following logs... Press Ctrl+C to stop'));
31
33
  let lastSize = fs.statSync(logFile).size;
34
+ const stopWatching = () => {
35
+ fs.unwatchFile(logFile);
36
+ process.removeListener('SIGINT', stopWatching);
37
+ process.removeListener('SIGTERM', stopWatching);
38
+ };
32
39
  fs.watchFile(logFile, { interval: 500 }, () => {
33
40
  const newSize = fs.statSync(logFile).size;
34
41
  if (newSize > lastSize) {
@@ -40,6 +47,8 @@ const logsCommand = new Command('logs')
40
47
  lastSize = newSize;
41
48
  }
42
49
  });
50
+ process.on('SIGINT', stopWatching);
51
+ process.on('SIGTERM', stopWatching);
43
52
  await new Promise(() => { });
44
53
  }
45
54
  });
@@ -1,5 +1,5 @@
1
- import { Command } from 'commander';
2
1
  import chalk from 'chalk';
2
+ import { Command } from 'commander';
3
3
  import { DevProcessManager } from '../../services/dev/process-manager.js';
4
4
  const statusCommand = new Command('status')
5
5
  .description('Check if dev server is running in background')
@@ -1,7 +1,7 @@
1
- import { Command } from 'commander';
2
1
  import chalk from 'chalk';
3
- import Spinner from '../../utils/spinner.js';
2
+ import { Command } from 'commander';
4
3
  import { DevProcessManager } from '../../services/dev/process-manager.js';
4
+ import Spinner from '../../utils/spinner.js';
5
5
  const stopCommand = new Command('stop')
6
6
  .description('Stop the background dev server')
7
7
  .action(async () => {
@@ -1,16 +1,10 @@
1
1
  import { Command } from 'commander';
2
- import * as fs from 'fs';
3
- import { spawn } from 'child_process';
4
- import chalk from 'chalk';
5
- import ConfigManager from '../services/config/manager.js';
6
- import ApiClient from '../services/api/client.js';
7
- import Spinner from '../utils/spinner.js';
8
- import { withRetry, CommandError } from '../utils/errors.js';
9
- import { DevProcessManager } from '../services/dev/process-manager.js';
10
2
  import { DevServer } from '../services/dev/server.js';
3
+ import DevSessionService from '../services/dev/session.js';
4
+ import logsCommand from './dev/logs.js';
11
5
  import statusCommand from './dev/status.js';
12
6
  import stopCommand from './dev/stop.js';
13
- import logsCommand from './dev/logs.js';
7
+ import { createCommandContext, showNoConfigMessage } from './shared/runtime.js';
14
8
  const command = new Command('dev');
15
9
  command
16
10
  .description('Start local development server')
@@ -20,255 +14,13 @@ command
20
14
  .option('--ngrok-token <token>', 'ngrok authtoken (if not set in environment)')
21
15
  .option('-d, --detach', 'Run server in background (detached mode)')
22
16
  .action(async (options) => {
23
- const spinner = new Spinner();
24
- const configManager = new ConfigManager();
25
- let tunnel;
26
- if (options.detach) {
27
- const existingState = await DevProcessManager.loadState();
28
- if (existingState && DevProcessManager.isProcessRunning(existingState.pid)) {
29
- console.log(chalk.yellow('⚠️ Dev server is already running in background'));
30
- console.log('');
31
- console.log(chalk.bold('Status:'));
32
- console.log(chalk.gray(' PID:'), existingState.pid);
33
- console.log(chalk.gray(' Port:'), existingState.port);
34
- console.log(chalk.gray(' Tunnel:'), existingState.tunnelUrl);
35
- console.log(chalk.gray(' Uptime:'), DevProcessManager.formatUptime(existingState.startedAt));
36
- console.log('');
37
- console.log(chalk.gray('Use "paymongo dev stop" to stop the server first.'));
38
- return;
39
- }
40
- const args = ['dist/index.js', 'dev', '--port', options.port || '3000'];
41
- if (options.noRegister) {
42
- args.push('--no-register');
43
- }
44
- if (options.events) {
45
- args.push('--events', options.events);
46
- }
47
- if (options.ngrokToken) {
48
- args.push('--ngrok-token', options.ngrokToken);
49
- }
50
- const logFile = await DevProcessManager.getLogFile();
51
- const out = fs.openSync(logFile, 'a');
52
- const err = fs.openSync(logFile, 'a');
53
- const child = spawn(process.execPath, args, {
54
- detached: true,
55
- stdio: ['ignore', out, err],
56
- cwd: process.cwd(),
57
- env: { ...process.env, FORCE_COLOR: '1' },
58
- });
59
- child.unref();
60
- console.log(chalk.green('✓'), 'Dev server starting in background...');
61
- console.log(chalk.gray(' PID:'), child.pid);
62
- console.log(chalk.gray(' Logs:'), logFile);
63
- console.log('');
64
- console.log(chalk.gray('Use "paymongo dev status" to check server status'));
65
- console.log(chalk.gray('Use "paymongo dev stop" to stop the server'));
66
- console.log(chalk.gray('Use "paymongo dev logs" to view server logs'));
67
- await new Promise((resolve) => setTimeout(resolve, 500));
68
- return;
69
- }
70
- try {
71
- spinner.start('Loading configuration...');
72
- const config = await configManager.load();
73
- if (!config) {
74
- spinner.fail('No configuration found');
75
- console.log(chalk.yellow('No PayMongo configuration found.'));
76
- console.log(chalk.gray("Run 'paymongo init' to set up your project first."));
77
- return;
78
- }
79
- spinner.succeed('Configuration loaded');
80
- spinner.start('Creating tunnel...');
81
- const port = parseInt(options.port || '3000');
82
- const { default: ngrok } = await import('@ngrok/ngrok');
83
- const tunnelUrl = await withRetry(async () => {
84
- try {
85
- const authtoken = options.ngrokToken || process.env.NGROK_AUTHTOKEN;
86
- if (!authtoken) {
87
- throw new Error('ngrok authtoken not found. Please either:\n' +
88
- ' 1. Set NGROK_AUTHTOKEN environment variable, or\n' +
89
- ' 2. Use --ngrok-token option: paymongo dev --ngrok-token YOUR_TOKEN\n' +
90
- ' Get your token from: https://dashboard.ngrok.com/get-started/your-authtoken');
91
- }
92
- tunnel = await ngrok.forward({
93
- addr: port,
94
- authtoken: authtoken,
95
- });
96
- return tunnel.url();
97
- }
98
- catch (error) {
99
- console.log(chalk.yellow('Debug: ngrok error details:'), error.message);
100
- throw error;
101
- }
102
- }, {
103
- maxRetries: 3,
104
- delayMs: 2000,
105
- retryCondition: (error) => {
106
- return (error.message.includes('connection') ||
107
- error.message.includes('timeout') ||
108
- error.message.includes('tunnel') ||
109
- error.message.includes('ngrok') ||
110
- error.message.includes('authtoken'));
111
- },
112
- });
113
- spinner.succeed('Tunnel created');
114
- const devServer = new DevServer(port, config);
115
- await devServer.start();
116
- if (config.registeredWebhooks && config.registeredWebhooks.length > 0) {
117
- spinner.start('Cleaning up stale webhooks...');
118
- const apiClient = new ApiClient({ config });
119
- let cleanedCount = 0;
120
- for (const webhook of config.registeredWebhooks) {
121
- try {
122
- await apiClient.disableWebhook(webhook.id);
123
- cleanedCount++;
124
- }
125
- catch {
126
- }
127
- }
128
- config.registeredWebhooks = [];
129
- await configManager.save(config);
130
- if (cleanedCount > 0) {
131
- spinner.succeed(`Cleaned up ${cleanedCount} stale webhook(s)`);
132
- }
133
- else {
134
- spinner.succeed('No stale webhooks to clean up');
135
- }
136
- }
137
- let webhookId;
138
- const shouldRegister = !options.noRegister && config.dev.autoRegisterWebhook !== false;
139
- if (shouldRegister) {
140
- spinner.start('Registering webhook...');
141
- const events = (options.events || 'payment.paid,payment.failed').split(',');
142
- const projectSlug = config.projectName.toLowerCase().replace(/[^a-z0-9]/g, '-');
143
- const webhookUrl = `${tunnelUrl}/webhook/${projectSlug}`;
144
- try {
145
- const webhook = await new ApiClient({ config }).createWebhook(webhookUrl, events);
146
- webhookId = webhook.id;
147
- if (webhook.attributes?.secret) {
148
- config.webhookSecrets = config.webhookSecrets || {};
149
- config.webhookSecrets[webhook.id] = webhook.attributes.secret;
150
- }
151
- config.registeredWebhooks = config.registeredWebhooks || [];
152
- config.registeredWebhooks.push({
153
- id: webhook.id,
154
- url: webhookUrl,
155
- createdAt: Date.now(),
156
- });
157
- await configManager.save(config);
158
- if (webhook.attributes?.secret) {
159
- spinner.succeed(`Webhook registered: ${webhookId} (with signature verification)`);
160
- }
161
- else {
162
- spinner.succeed(`Webhook registered: ${webhookId}`);
163
- }
164
- }
165
- catch (error) {
166
- const err = error;
167
- spinner.warn('Webhook registration failed - server will start without webhook');
168
- console.log(chalk.yellow('⚠️'), 'Webhook registration failed:', err.message);
169
- console.log('');
170
- console.log(chalk.blue('ℹ️'), 'You can still test webhooks manually:');
171
- console.log(chalk.gray(` Webhook URL: ${webhookUrl}`));
172
- console.log(chalk.gray(' Copy this URL to your PayMongo dashboard'));
173
- if (config.dev.verifyWebhookSignatures) {
174
- console.log(chalk.gray(' Signature verification is currently enabled'));
175
- console.log(chalk.gray(' For manual unsigned testing, run: paymongo config set dev.verifySignatures false'));
176
- }
177
- console.log('');
178
- if (err.message.includes('API key') || err.message.includes('unauthorized')) {
179
- console.log(chalk.yellow('💡 To fix webhook registration:'));
180
- console.log(chalk.gray(' 1. Run "paymongo login" to update your API keys'));
181
- console.log(chalk.gray(' 2. Restart the development server'));
182
- }
183
- }
184
- }
185
- const projectSlug = config.projectName.toLowerCase().replace(/[^a-z0-9]/g, '-');
186
- const localWebhookUrl = `http://localhost:${port}/webhook/${projectSlug}`;
187
- const externalWebhookUrl = `${tunnelUrl}/webhook/${projectSlug}`;
188
- console.log('\n' + chalk.green('🚀 PayMongo Development Server'));
189
- console.log('');
190
- console.log(chalk.bold('URLs:'));
191
- console.log(chalk.gray(' ├─'), chalk.cyan('External (PayMongo sends here):'));
192
- console.log(chalk.gray(' │ '), chalk.yellow(externalWebhookUrl));
193
- console.log(chalk.gray(' │'));
194
- console.log(chalk.gray(' └─'), chalk.cyan('Local (Your server receives here):'));
195
- console.log(chalk.gray(' '), chalk.green(localWebhookUrl));
196
- console.log('');
197
- console.log(chalk.bold('Forwarding:'));
198
- console.log(chalk.gray(' '), `${chalk.yellow(tunnelUrl)} ${chalk.gray('→')} ${chalk.green(`http://localhost:${port}`)}`);
199
- console.log('');
200
- if (webhookId) {
201
- console.log(chalk.bold('Webhook ID:'), chalk.gray(webhookId));
202
- }
203
- console.log(chalk.bold('Events:'), (options.events || 'payment.paid,payment.failed').split(',').join(', '));
204
- console.log('');
205
- console.log(chalk.gray('💡 Tip: Use the External URL in PayMongo dashboard, requests will forward to your local server'));
206
- console.log(chalk.gray('Press Ctrl+C to stop'));
207
- await DevProcessManager.saveState({
208
- pid: process.pid,
209
- port,
210
- tunnelUrl: tunnelUrl ?? '',
211
- webhookId,
212
- webhookUrl: externalWebhookUrl,
213
- localUrl: localWebhookUrl,
214
- events: (options.events || 'payment.paid,payment.failed').split(','),
215
- startedAt: Date.now(),
216
- projectName: config.projectName,
217
- });
218
- const cleanup = async () => {
219
- console.log('\n' + chalk.yellow('Shutting down...'));
220
- await DevProcessManager.clearState();
221
- try {
222
- if (tunnel) {
223
- await tunnel.close();
224
- console.log(chalk.yellow('✓'), 'Tunnel closed');
225
- }
226
- await devServer.stop();
227
- if (webhookId) {
228
- spinner.start('Cleaning up webhook...');
229
- await new ApiClient({ config }).disableWebhook(webhookId);
230
- if (config.registeredWebhooks) {
231
- config.registeredWebhooks = config.registeredWebhooks.filter((w) => w.id !== webhookId);
232
- delete config.webhookSecrets[webhookId];
233
- await configManager.save(config);
234
- }
235
- spinner.succeed('Webhook disabled');
236
- }
237
- }
238
- catch (error) {
239
- console.error(chalk.red('Error during cleanup:'), error.message);
240
- console.log(chalk.yellow('⚠️'), 'Some cleanup tasks may not have completed');
241
- }
242
- process.exit(0);
243
- };
244
- process.on('SIGINT', cleanup);
245
- process.on('SIGTERM', cleanup);
246
- await new Promise(() => { });
247
- }
248
- catch (error) {
249
- spinner.stop();
250
- const err = error;
251
- if (err.message.includes('ngrok') || err.message.includes('tunnel')) {
252
- console.error(chalk.red('❌ Failed to create tunnel:'), err.message);
253
- console.log('');
254
- console.log(chalk.yellow('💡 Troubleshooting suggestions:'));
255
- console.log(chalk.gray('• Check your internet connection'));
256
- console.log(chalk.gray('• Make sure ngrok is not blocked by firewall/antivirus'));
257
- console.log(chalk.gray('• Set up ngrok authentication: export NGROK_AUTHTOKEN=your_token'));
258
- console.log(chalk.gray('• Get your authtoken from: https://dashboard.ngrok.com/get-started/your-authtoken'));
259
- console.log(chalk.gray('• Try a different port: paymongo dev --port 3001'));
260
- console.log(chalk.gray('• Visit https://ngrok.com for status updates'));
261
- }
262
- await DevProcessManager.clearState();
263
- try {
264
- if (tunnel) {
265
- await tunnel.close();
266
- }
267
- }
268
- catch {
269
- }
270
- throw new CommandError();
271
- }
17
+ const { spinner, configManager } = createCommandContext();
18
+ const session = new DevSessionService({
19
+ spinner,
20
+ configManager,
21
+ onMissingConfig: showNoConfigMessage,
22
+ });
23
+ await session.run(options);
272
24
  });
273
25
  command.addCommand(statusCommand);
274
26
  command.addCommand(stopCommand);