@cakemail-org/cakemail-cli 1.5.0 → 2.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.
- package/.claude/settings.local.json +12 -0
- package/.env.example +40 -0
- package/.env.test.example +45 -0
- package/CHANGELOG.md +1031 -0
- package/README.md +319 -15
- package/audit-formats.js +128 -0
- package/cakemail.rb +20 -0
- package/dist/cli.js +27 -10
- package/dist/cli.js.map +1 -1
- package/dist/client.d.ts +2 -0
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +16 -6
- package/dist/client.js.map +1 -1
- package/dist/commands/account.js +1 -1
- package/dist/commands/account.js.map +1 -1
- package/dist/commands/attributes.js +1 -1
- package/dist/commands/attributes.js.map +1 -1
- package/dist/commands/campaigns.d.ts.map +1 -1
- package/dist/commands/campaigns.js +103 -8
- package/dist/commands/campaigns.js.map +1 -1
- package/dist/commands/config.d.ts.map +1 -1
- package/dist/commands/config.js +63 -4
- package/dist/commands/config.js.map +1 -1
- package/dist/commands/contacts.d.ts.map +1 -1
- package/dist/commands/contacts.js +91 -12
- package/dist/commands/contacts.js.map +1 -1
- package/dist/commands/emails.js +1 -1
- package/dist/commands/emails.js.map +1 -1
- package/dist/commands/interests.d.ts +5 -0
- package/dist/commands/interests.d.ts.map +1 -0
- package/dist/commands/interests.js +172 -0
- package/dist/commands/interests.js.map +1 -0
- package/dist/commands/lists.d.ts.map +1 -1
- package/dist/commands/lists.js +6 -8
- package/dist/commands/lists.js.map +1 -1
- package/dist/commands/logs.d.ts +5 -0
- package/dist/commands/logs.d.ts.map +1 -0
- package/dist/commands/logs.js +237 -0
- package/dist/commands/logs.js.map +1 -0
- package/dist/commands/reports.js +1 -1
- package/dist/commands/reports.js.map +1 -1
- package/dist/commands/segments.js +1 -1
- package/dist/commands/segments.js.map +1 -1
- package/dist/commands/senders.d.ts.map +1 -1
- package/dist/commands/senders.js +11 -8
- package/dist/commands/senders.js.map +1 -1
- package/dist/commands/suppressed.js +1 -1
- package/dist/commands/suppressed.js.map +1 -1
- package/dist/commands/tags.d.ts +5 -0
- package/dist/commands/tags.d.ts.map +1 -0
- package/dist/commands/tags.js +124 -0
- package/dist/commands/tags.js.map +1 -0
- package/dist/commands/templates.js +1 -1
- package/dist/commands/templates.js.map +1 -1
- package/dist/commands/transactional-templates.d.ts +5 -0
- package/dist/commands/transactional-templates.d.ts.map +1 -0
- package/dist/commands/transactional-templates.js +354 -0
- package/dist/commands/transactional-templates.js.map +1 -0
- package/dist/commands/webhooks.js +1 -1
- package/dist/commands/webhooks.js.map +1 -1
- package/dist/utils/auth.d.ts +8 -1
- package/dist/utils/auth.d.ts.map +1 -1
- package/dist/utils/auth.js +39 -11
- package/dist/utils/auth.js.map +1 -1
- package/dist/utils/config-file.d.ts +7 -0
- package/dist/utils/config-file.d.ts.map +1 -1
- package/dist/utils/config-file.js +15 -0
- package/dist/utils/config-file.js.map +1 -1
- package/dist/utils/config.d.ts +2 -0
- package/dist/utils/config.d.ts.map +1 -1
- package/dist/utils/config.js +12 -4
- package/dist/utils/config.js.map +1 -1
- package/dist/utils/errors.js +1 -1
- package/dist/utils/errors.js.map +1 -1
- package/dist/utils/list-defaults.d.ts +33 -0
- package/dist/utils/list-defaults.d.ts.map +1 -0
- package/dist/utils/list-defaults.js +52 -0
- package/dist/utils/list-defaults.js.map +1 -0
- package/dist/utils/output.d.ts.map +1 -1
- package/dist/utils/output.js +36 -13
- package/dist/utils/output.js.map +1 -1
- package/dist/utils/progress.d.ts.map +1 -1
- package/dist/utils/progress.js +32 -4
- package/dist/utils/progress.js.map +1 -1
- package/dist/utils/spinner.d.ts +17 -0
- package/dist/utils/spinner.d.ts.map +1 -0
- package/dist/utils/spinner.js +43 -0
- package/dist/utils/spinner.js.map +1 -0
- package/docs/DOCUMENTATION-STANDARD.md +1068 -0
- package/docs/README.md +161 -0
- package/docs/developer/ARCHITECTURE.md +516 -0
- package/docs/developer/AUTH.md +204 -0
- package/docs/developer/CONTRIBUTING.md +227 -0
- package/docs/developer/DOCUMENTATION_SUMMARY.md +346 -0
- package/docs/developer/PROJECT_INDEX.md +365 -0
- package/docs/planning/API_COVERAGE.md +1045 -0
- package/docs/planning/BACKLOG.md +1159 -0
- package/docs/planning/PROFILE_SYSTEM_TASKS.md +287 -0
- package/docs/planning/UX_IMPLEMENTATION_PLAN.md +691 -0
- package/docs/planning/archive/RELEASE_CHECKLIST_v1.3.0.md +332 -0
- package/docs/planning/archive/RELEASE_v1.3.0.md +428 -0
- package/docs/planning/archive/cakemail-cli-ux-improvements.md +438 -0
- package/docs/planning/cakemail-profile-system-plan.md +1121 -0
- package/docs/testing/AI_USER_SIMULATION_DESIGN.md +1342 -0
- package/docs/testing/KENOGAMI_BIDIRECTIONAL_FLOW.md +1517 -0
- package/docs/testing/KENOGAMI_TRUTH_RECONCILIATION_SYSTEM.md +1369 -0
- package/docs/user-manual/.obsidian/app.json +1 -0
- package/docs/user-manual/.obsidian/appearance.json +1 -0
- package/docs/user-manual/.obsidian/core-plugins.json +33 -0
- package/docs/user-manual/.obsidian/workspace.json +167 -0
- package/docs/user-manual/01-getting-started/01-installation.md +214 -0
- package/docs/user-manual/01-getting-started/02-quick-start.md +432 -0
- package/docs/user-manual/01-getting-started/03-authentication.md +448 -0
- package/docs/user-manual/01-getting-started/04-configuration.md +430 -0
- package/docs/user-manual/01-getting-started/05-output-formats.md +447 -0
- package/docs/user-manual/02-core-concepts/01-accounts.md +514 -0
- package/docs/user-manual/02-core-concepts/02-profile-system.md +771 -0
- package/docs/user-manual/02-core-concepts/03-smart-defaults.md +485 -0
- package/docs/user-manual/02-core-concepts/04-authentication-methods.md +435 -0
- package/docs/user-manual/02-core-concepts/05-pagination-filtering.md +600 -0
- package/docs/user-manual/02-core-concepts/06-error-handling.md +718 -0
- package/docs/user-manual/02-core-concepts/07-api-coverage.md +483 -0
- package/docs/user-manual/03-email-operations/01-senders.md +490 -0
- package/docs/user-manual/03-email-operations/02-templates.md +444 -0
- package/docs/user-manual/03-email-operations/03-transactional-emails.md +706 -0
- package/docs/user-manual/03-email-operations/04-email-tracking.md +407 -0
- package/docs/user-manual/04-campaign-management/01-campaigns-basics.md +394 -0
- package/docs/user-manual/04-campaign-management/02-campaign-scheduling.md +630 -0
- package/docs/user-manual/04-campaign-management/03-campaign-testing.md +997 -0
- package/docs/user-manual/04-campaign-management/04-campaign-lifecycle.md +709 -0
- package/docs/user-manual/04-campaign-management/05-campaign-links.md +934 -0
- package/docs/user-manual/05-contact-management/01-lists.md +836 -0
- package/docs/user-manual/05-contact-management/02-contacts.md +1035 -0
- package/docs/user-manual/05-contact-management/03-custom-attributes.md +788 -0
- package/docs/user-manual/05-contact-management/04-segments.md +1028 -0
- package/docs/user-manual/05-contact-management/05-contact-import-export.md +1031 -0
- package/docs/user-manual/06-analytics-reporting/01-campaign-analytics.md +867 -0
- package/docs/user-manual/06-analytics-reporting/02-account-reports.md +227 -0
- package/docs/user-manual/07-integrations/01-webhooks-integration.md +259 -0
- package/docs/user-manual/07-integrations/02-automation.md +326 -0
- package/docs/user-manual/08-advanced-usage/01-scripting-patterns.md +672 -0
- package/docs/user-manual/08-advanced-usage/02-bulk-operations.md +932 -0
- package/docs/user-manual/08-advanced-usage/03-ci-cd-integration.md +892 -0
- package/docs/user-manual/08-advanced-usage/04-performance-optimization.md +766 -0
- package/docs/user-manual/09-command-reference/01-config.md +776 -0
- package/docs/user-manual/09-command-reference/02-account.md +652 -0
- package/docs/user-manual/09-command-reference/03-lists.md +958 -0
- package/docs/user-manual/09-command-reference/04-contacts.md +1408 -0
- package/docs/user-manual/09-command-reference/05-attributes.md +617 -0
- package/docs/user-manual/09-command-reference/06-segments.md +894 -0
- package/docs/user-manual/09-command-reference/07-senders.md +803 -0
- package/docs/user-manual/09-command-reference/08-templates.md +818 -0
- package/docs/user-manual/09-command-reference/09-campaigns.md +1250 -0
- package/docs/user-manual/09-command-reference/10-emails.md +807 -0
- package/docs/user-manual/09-command-reference/11-reports.md +1135 -0
- package/docs/user-manual/09-command-reference/12-webhooks.md +773 -0
- package/docs/user-manual/09-command-reference/13-suppressed.md +797 -0
- package/docs/user-manual/09-command-reference/14-interests.md +630 -0
- package/docs/user-manual/09-command-reference/15-tags.md +584 -0
- package/docs/user-manual/09-command-reference/16-logs.md +656 -0
- package/docs/user-manual/09-command-reference/17-transactional-templates.md +850 -0
- package/docs/user-manual/10-troubleshooting/01-common-errors.md +457 -0
- package/docs/user-manual/10-troubleshooting/02-authentication-issues.md +558 -0
- package/docs/user-manual/10-troubleshooting/03-connection-problems.md +634 -0
- package/docs/user-manual/10-troubleshooting/04-debugging.md +725 -0
- package/docs/user-manual/11-appendix/04-faq.md +484 -0
- package/docs/user-manual/11-appendix/05-glossary.md +250 -0
- package/docs/user-manual/README.md +0 -0
- package/package.json +13 -47
- package/src/cli.ts +125 -0
- package/src/client.ts +16 -0
- package/src/commands/account.ts +267 -0
- package/src/commands/accounts.ts +78 -0
- package/src/commands/actions.ts +249 -0
- package/src/commands/attributes.ts +139 -0
- package/src/commands/campaign-blueprints.ts +106 -0
- package/src/commands/campaigns.ts +469 -0
- package/src/commands/config.ts +77 -0
- package/src/commands/contacts.ts +612 -0
- package/src/commands/custom-attributes.ts +127 -0
- package/src/commands/dkims.ts +117 -0
- package/src/commands/domains.ts +82 -0
- package/src/commands/email-apis.ts +569 -0
- package/src/commands/emails.ts +197 -0
- package/src/commands/forms.ts +283 -0
- package/src/commands/interests.ts +155 -0
- package/src/commands/links.ts +38 -0
- package/src/commands/lists.ts +406 -0
- package/src/commands/logos.ts +71 -0
- package/src/commands/logs.ts +386 -0
- package/src/commands/reports.ts +306 -0
- package/src/commands/segments.ts +158 -0
- package/src/commands/senders.ts +204 -0
- package/src/commands/sub-accounts.ts +271 -0
- package/src/commands/suppressed-emails.ts +234 -0
- package/src/commands/suppressed.ts +198 -0
- package/src/commands/system-emails.ts +85 -0
- package/src/commands/tags.ts +146 -0
- package/src/commands/tasks.ts +116 -0
- package/src/commands/templates.ts +189 -0
- package/src/commands/tokens.ts +83 -0
- package/src/commands/transactional-emails.ts +374 -0
- package/src/commands/transactional-templates.ts +385 -0
- package/src/commands/users.ts +506 -0
- package/src/commands/webhooks.ts +172 -0
- package/src/commands/workflow-blueprints.ts +123 -0
- package/src/commands/workflows.ts +265 -0
- package/src/types/profile.ts +93 -0
- package/src/utils/auth.ts +272 -0
- package/src/utils/config-file.ts +96 -0
- package/src/utils/config.ts +134 -0
- package/src/utils/confirm.ts +32 -0
- package/src/utils/defaults.ts +99 -0
- package/src/utils/errors.ts +116 -0
- package/src/utils/interactive.ts +91 -0
- package/src/utils/list-defaults.ts +74 -0
- package/src/utils/output.ts +190 -0
- package/src/utils/progress.ts +320 -0
- package/src/utils/spinner.ts +22 -0
- package/tests/IMPLEMENTATION_STATUS.md +258 -0
- package/tests/PTY_SETUP.md +118 -0
- package/tests/PTY_TESTING_GUIDE.md +507 -0
- package/tests/README.md +244 -0
- package/tests/fixtures/api-responses/campaigns.json +34 -0
- package/tests/fixtures/test-config.json +13 -0
- package/tests/helpers/cli-runner.ts +128 -0
- package/tests/helpers/mock-server.ts +301 -0
- package/tests/helpers/pty-runner.ts +181 -0
- package/tests/integration/campaigns-real-api.test.ts +196 -0
- package/tests/integration/setup-integration.ts +50 -0
- package/tests/pty/campaigns.test.ts +241 -0
- package/tests/setup.ts +34 -0
- package/tsconfig.json +15 -0
- package/vitest.config.ts +28 -0
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
// ABOUTME: Context-aware error handling with profile-dependent display.
|
|
2
|
+
// ABOUTME: Maps HTTP errors to user-friendly messages with actionable suggestions.
|
|
3
|
+
|
|
4
|
+
import type { ProfileConfig } from '../types/profile.js';
|
|
5
|
+
|
|
6
|
+
export interface ErrorContext {
|
|
7
|
+
command: string;
|
|
8
|
+
resource?: string;
|
|
9
|
+
resourceId?: string | number;
|
|
10
|
+
operation?: string;
|
|
11
|
+
profileConfig?: ProfileConfig;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface EnhancedError {
|
|
15
|
+
message: string;
|
|
16
|
+
suggestion?: string;
|
|
17
|
+
help?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function enhanceError(error: any, context: ErrorContext): EnhancedError {
|
|
21
|
+
const statusCode = error.statusCode || error.status || error.response?.status;
|
|
22
|
+
const message = error.message || String(error);
|
|
23
|
+
|
|
24
|
+
if (statusCode) {
|
|
25
|
+
return enhanceHttpError(statusCode, message, context);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (message.includes("ECONNREFUSED") || message.includes("ENOTFOUND") || message.includes("fetch failed")) {
|
|
29
|
+
return {
|
|
30
|
+
message: "Cannot connect to the API server.",
|
|
31
|
+
suggestion: "Check your internet connection and API base URL.",
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (message.includes("Missing credentials") || message.includes("accessToken")) {
|
|
36
|
+
return {
|
|
37
|
+
message: "No credentials provided.",
|
|
38
|
+
suggestion: `Set CAKEMAIL_ACCESS_TOKEN or use --access-token.`,
|
|
39
|
+
help: "Run 'cakemail --help' for authentication options.",
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return { message };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function enhanceHttpError(statusCode: number, message: string, context: ErrorContext): EnhancedError {
|
|
47
|
+
switch (statusCode) {
|
|
48
|
+
case 400:
|
|
49
|
+
return {
|
|
50
|
+
message: `Invalid request: ${message}`,
|
|
51
|
+
suggestion: "Check your parameters. Use --help to see available options.",
|
|
52
|
+
};
|
|
53
|
+
case 401:
|
|
54
|
+
return {
|
|
55
|
+
message: "Authentication failed.",
|
|
56
|
+
suggestion: "Check your credentials. Your token may have expired.",
|
|
57
|
+
help: `Set CAKEMAIL_ACCESS_TOKEN or use --access-token.`,
|
|
58
|
+
};
|
|
59
|
+
case 403:
|
|
60
|
+
return {
|
|
61
|
+
message: `Access denied: ${message}`,
|
|
62
|
+
suggestion: "You may not have permission to access this resource.",
|
|
63
|
+
};
|
|
64
|
+
case 404: {
|
|
65
|
+
const resource = context.resource || "Resource";
|
|
66
|
+
const id = context.resourceId ? ` (${context.resourceId})` : "";
|
|
67
|
+
return {
|
|
68
|
+
message: `${resource}${id} not found.`,
|
|
69
|
+
suggestion: `Verify the ID is correct. Use 'cakemail ${context.resource || ""} list' to see available items.`,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
case 409:
|
|
73
|
+
return {
|
|
74
|
+
message: `Conflict: ${message}`,
|
|
75
|
+
suggestion: "The resource may already exist or be in a conflicting state.",
|
|
76
|
+
};
|
|
77
|
+
case 422:
|
|
78
|
+
return {
|
|
79
|
+
message: `Validation error: ${message}`,
|
|
80
|
+
suggestion: "Check that all required fields are provided with valid values.",
|
|
81
|
+
};
|
|
82
|
+
case 429:
|
|
83
|
+
return {
|
|
84
|
+
message: "Rate limit exceeded.",
|
|
85
|
+
suggestion: "Wait a moment before retrying.",
|
|
86
|
+
};
|
|
87
|
+
case 500:
|
|
88
|
+
case 502:
|
|
89
|
+
case 503:
|
|
90
|
+
return {
|
|
91
|
+
message: "The API server encountered an error.",
|
|
92
|
+
suggestion: "Try again later. If the problem persists, contact support.",
|
|
93
|
+
};
|
|
94
|
+
default:
|
|
95
|
+
return {
|
|
96
|
+
message: `API error (${statusCode}): ${message}`,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function displayError(error: any, context: ErrorContext): void {
|
|
102
|
+
const enhanced = enhanceError(error, context);
|
|
103
|
+
const verbose = context.profileConfig?.display.verbose_errors ?? false;
|
|
104
|
+
|
|
105
|
+
if (verbose) {
|
|
106
|
+
console.error(`Error: ${enhanced.message}`);
|
|
107
|
+
if (error.statusCode) console.error(` Status: ${error.statusCode}`);
|
|
108
|
+
if (error.response?.data) console.error(` Response: ${JSON.stringify(error.response.data)}`);
|
|
109
|
+
if (enhanced.suggestion) console.error(` Suggestion: ${enhanced.suggestion}`);
|
|
110
|
+
if (enhanced.help) console.error(` Help: ${enhanced.help}`);
|
|
111
|
+
} else {
|
|
112
|
+
console.error(`Error: ${enhanced.message}`);
|
|
113
|
+
if (enhanced.suggestion) console.error(` ${enhanced.suggestion}`);
|
|
114
|
+
if (enhanced.help) console.error(` ${enhanced.help}`);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
// ABOUTME: Profile-aware interactive prompts that skip in scripting mode.
|
|
2
|
+
// ABOUTME: Detects TTY, CI, and batch mode to decide whether to prompt.
|
|
3
|
+
|
|
4
|
+
import * as readline from 'node:readline';
|
|
5
|
+
import type { ProfileConfig } from '../types/profile.js';
|
|
6
|
+
|
|
7
|
+
export function isInteractive(): boolean {
|
|
8
|
+
return !!(process.stdin.isTTY && process.stdout.isTTY);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function isScriptingMode(): boolean {
|
|
12
|
+
return !!(
|
|
13
|
+
process.env.CI ||
|
|
14
|
+
process.env.CAKEMAIL_BATCH_MODE === 'true' ||
|
|
15
|
+
process.argv.includes('--batch') ||
|
|
16
|
+
!isInteractive()
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function shouldPrompt(profileConfig?: ProfileConfig): boolean {
|
|
21
|
+
const mode = profileConfig?.behavior.interactive_prompts ?? 'auto';
|
|
22
|
+
if (mode === false) return false;
|
|
23
|
+
if (mode === true) return isInteractive();
|
|
24
|
+
return isInteractive() && !isScriptingMode();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function promptText(
|
|
28
|
+
message: string,
|
|
29
|
+
options: { default?: string; required?: boolean; profileConfig?: ProfileConfig } = {},
|
|
30
|
+
): Promise<string | undefined> {
|
|
31
|
+
if (!shouldPrompt(options.profileConfig)) return options.default;
|
|
32
|
+
|
|
33
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
34
|
+
const defaultHint = options.default ? ` (${options.default})` : '';
|
|
35
|
+
|
|
36
|
+
return new Promise((resolve) => {
|
|
37
|
+
rl.question(`${message}${defaultHint}: `, (answer) => {
|
|
38
|
+
rl.close();
|
|
39
|
+
const value = answer.trim() || options.default;
|
|
40
|
+
if (options.required && !value) {
|
|
41
|
+
console.error('This field is required.');
|
|
42
|
+
resolve(undefined);
|
|
43
|
+
} else {
|
|
44
|
+
resolve(value);
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export async function promptSelect<T extends string>(
|
|
51
|
+
message: string,
|
|
52
|
+
choices: { name: string; value: T }[],
|
|
53
|
+
options: { profileConfig?: ProfileConfig } = {},
|
|
54
|
+
): Promise<T | undefined> {
|
|
55
|
+
if (!shouldPrompt(options.profileConfig)) return undefined;
|
|
56
|
+
|
|
57
|
+
console.log(`\n${message}`);
|
|
58
|
+
choices.forEach((c, i) => console.log(` ${i + 1}. ${c.name}`));
|
|
59
|
+
|
|
60
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
61
|
+
|
|
62
|
+
return new Promise((resolve) => {
|
|
63
|
+
rl.question(`Choice (1-${choices.length}): `, (answer) => {
|
|
64
|
+
rl.close();
|
|
65
|
+
const idx = parseInt(answer) - 1;
|
|
66
|
+
if (idx >= 0 && idx < choices.length) {
|
|
67
|
+
resolve(choices[idx].value);
|
|
68
|
+
} else {
|
|
69
|
+
resolve(undefined);
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export async function promptConfirm(
|
|
76
|
+
message: string,
|
|
77
|
+
options: { default?: boolean; profileConfig?: ProfileConfig } = {},
|
|
78
|
+
): Promise<boolean> {
|
|
79
|
+
if (!shouldPrompt(options.profileConfig)) return options.default ?? true;
|
|
80
|
+
|
|
81
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
82
|
+
const hint = options.default ? '[Y/n]' : '[y/N]';
|
|
83
|
+
|
|
84
|
+
return new Promise((resolve) => {
|
|
85
|
+
rl.question(`${message} ${hint} `, (answer) => {
|
|
86
|
+
rl.close();
|
|
87
|
+
if (!answer.trim()) resolve(options.default ?? false);
|
|
88
|
+
else resolve(answer.trim().toLowerCase() === 'y');
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Apply profile-based defaults for list commands
|
|
3
|
+
*
|
|
4
|
+
* For marketer profile (compact format):
|
|
5
|
+
* - Default to 20 items per page
|
|
6
|
+
* - Default to descending order by ID (newest first)
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { ProfileConfig } from '../types/profile.js';
|
|
10
|
+
|
|
11
|
+
export interface ListOptions {
|
|
12
|
+
limit?: string;
|
|
13
|
+
page?: string;
|
|
14
|
+
sort?: string;
|
|
15
|
+
[key: string]: any;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface ListParams {
|
|
19
|
+
per_page?: number;
|
|
20
|
+
page?: number;
|
|
21
|
+
sort?: string;
|
|
22
|
+
[key: string]: any;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Apply profile-based defaults to list parameters
|
|
27
|
+
*
|
|
28
|
+
* @param options - Command line options
|
|
29
|
+
* @param profileConfig - Current profile configuration
|
|
30
|
+
* @returns API parameters with defaults applied
|
|
31
|
+
*/
|
|
32
|
+
export function applyListDefaults(
|
|
33
|
+
options: ListOptions,
|
|
34
|
+
profileConfig?: ProfileConfig
|
|
35
|
+
): ListParams {
|
|
36
|
+
const params: ListParams = {};
|
|
37
|
+
|
|
38
|
+
// Apply limit with profile default
|
|
39
|
+
if (options.limit) {
|
|
40
|
+
params.per_page = parseInt(options.limit);
|
|
41
|
+
} else if (profileConfig?.output.format === 'compact') {
|
|
42
|
+
// Marketer profile default: 25 items (half of API default of 50)
|
|
43
|
+
params.per_page = 25;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Apply page if provided
|
|
47
|
+
if (options.page) {
|
|
48
|
+
params.page = parseInt(options.page);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Apply sort with profile default
|
|
52
|
+
if (options.sort) {
|
|
53
|
+
params.sort = options.sort;
|
|
54
|
+
} else if (profileConfig?.output.format === 'compact') {
|
|
55
|
+
// Marketer profile default: descending by created_on (newest first)
|
|
56
|
+
// Note: -id doesn't work, but -created_on does
|
|
57
|
+
params.sort = '-created_on';
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return params;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Get next page hint for pagination footer
|
|
65
|
+
*/
|
|
66
|
+
export function getNextPageCommand(currentCommand: string, currentPage?: number): string {
|
|
67
|
+
const page = (currentPage || 1) + 1;
|
|
68
|
+
// If command already has --page, replace it
|
|
69
|
+
if (currentCommand.includes('--page')) {
|
|
70
|
+
return currentCommand.replace(/--page\s+\d+/, `--page ${page}`);
|
|
71
|
+
}
|
|
72
|
+
// Otherwise append it
|
|
73
|
+
return `${currentCommand} --page ${page}`;
|
|
74
|
+
}
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
// ABOUTME: Multi-format output formatter with profile-aware styling.
|
|
2
|
+
// ABOUTME: Supports JSON, table, and compact formats with colors and date formatting.
|
|
3
|
+
|
|
4
|
+
import Table from 'cli-table3';
|
|
5
|
+
import chalk from 'chalk';
|
|
6
|
+
import type { ProfileConfig, OutputFormat, ColorScheme, DateFormat } from '../types/profile.js';
|
|
7
|
+
|
|
8
|
+
export class OutputFormatter {
|
|
9
|
+
constructor(
|
|
10
|
+
private format: OutputFormat = 'json',
|
|
11
|
+
private profileConfig?: ProfileConfig,
|
|
12
|
+
) {}
|
|
13
|
+
|
|
14
|
+
output(data: unknown, fields?: string[]): void {
|
|
15
|
+
if (data == null) return;
|
|
16
|
+
|
|
17
|
+
switch (this.format) {
|
|
18
|
+
case 'table':
|
|
19
|
+
this.outputTable(data, fields);
|
|
20
|
+
break;
|
|
21
|
+
case 'compact':
|
|
22
|
+
this.outputCompact(data);
|
|
23
|
+
break;
|
|
24
|
+
case 'json':
|
|
25
|
+
default:
|
|
26
|
+
this.outputJson(data);
|
|
27
|
+
break;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
success(message: string): void {
|
|
32
|
+
console.log(this.applyColor(message, chalk.green));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
error(message: string): void {
|
|
36
|
+
console.error(this.applyColor(`Error: ${message}`, chalk.red));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
info(message: string): void {
|
|
40
|
+
console.log(this.applyColor(message, chalk.cyan));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
warning(message: string): void {
|
|
44
|
+
console.log(this.applyColor(message, chalk.yellow));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
private outputJson(data: unknown): void {
|
|
48
|
+
const pretty = this.profileConfig?.output.pretty_print ?? true;
|
|
49
|
+
console.log(JSON.stringify(data, null, pretty ? 2 : undefined));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
private outputTable(data: unknown, fields?: string[]): void {
|
|
53
|
+
const items = this.extractItems(data);
|
|
54
|
+
if (items.length === 0) {
|
|
55
|
+
this.info('No results found.');
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const keys = fields || this.inferColumns(items[0]);
|
|
60
|
+
const filteredKeys = this.filterColumns(keys);
|
|
61
|
+
|
|
62
|
+
const table = new Table({
|
|
63
|
+
head: filteredKeys.map((k) => this.applyColor(this.formatHeader(k), chalk.bold)),
|
|
64
|
+
style: { head: [], border: [] },
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
for (const item of items) {
|
|
68
|
+
table.push(filteredKeys.map((key) => this.formatValue(item[key], key)));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
console.log(table.toString());
|
|
72
|
+
this.displayPagination(data, items.length);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
private outputCompact(data: unknown): void {
|
|
76
|
+
const items = this.extractItems(data);
|
|
77
|
+
if (items.length === 0) {
|
|
78
|
+
this.info('No results found.');
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
for (const item of items) {
|
|
83
|
+
const id = item.id ?? item.name ?? '';
|
|
84
|
+
const name = item.name ?? item.email ?? item.tag ?? '';
|
|
85
|
+
const status = item.status ? ` [${this.formatStatus(item.status)}]` : '';
|
|
86
|
+
const idStr = this.profileConfig?.display.show_ids !== false && id ? `#${id} ` : '';
|
|
87
|
+
console.log(`${idStr}${name}${status}`);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
this.displayPagination(data, items.length);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
private extractItems(data: unknown): Record<string, any>[] {
|
|
94
|
+
if (Array.isArray(data)) return data;
|
|
95
|
+
if (data && typeof data === 'object') {
|
|
96
|
+
const obj = data as Record<string, unknown>;
|
|
97
|
+
if (Array.isArray(obj.data)) return obj.data;
|
|
98
|
+
return [obj as Record<string, any>];
|
|
99
|
+
}
|
|
100
|
+
return [];
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
private inferColumns(item: Record<string, any>): string[] {
|
|
104
|
+
return Object.keys(item);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
private filterColumns(keys: string[]): string[] {
|
|
108
|
+
const showIds = this.profileConfig?.display.show_ids ?? true;
|
|
109
|
+
const exclude = new Set(['__metadata', 'links']);
|
|
110
|
+
if (!showIds) exclude.add('id');
|
|
111
|
+
return keys.filter((k) => !exclude.has(k));
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
private formatHeader(key: string): string {
|
|
115
|
+
return key.replace(/_/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase());
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
private formatValue(value: unknown, key: string): string {
|
|
119
|
+
if (value == null) return '';
|
|
120
|
+
if (typeof value === 'boolean') return value ? 'Yes' : 'No';
|
|
121
|
+
if (key === 'status') return this.formatStatus(String(value));
|
|
122
|
+
if (this.isDateField(key) && typeof value === 'string') return this.formatDate(value);
|
|
123
|
+
if (typeof value === 'object') return JSON.stringify(value);
|
|
124
|
+
return String(value);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
private formatStatus(status: string): string {
|
|
128
|
+
const colors: Record<string, typeof chalk.green> = {
|
|
129
|
+
active: chalk.green,
|
|
130
|
+
draft: chalk.yellow,
|
|
131
|
+
scheduled: chalk.blue,
|
|
132
|
+
delivering: chalk.cyan,
|
|
133
|
+
delivered: chalk.green,
|
|
134
|
+
suspended: chalk.red,
|
|
135
|
+
archived: chalk.gray,
|
|
136
|
+
};
|
|
137
|
+
const colorFn = colors[status.toLowerCase()] || chalk.white;
|
|
138
|
+
return this.applyColor(status, colorFn);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
private formatDate(dateString: string): string {
|
|
142
|
+
const dateFormat = this.profileConfig?.display.date_format ?? 'friendly';
|
|
143
|
+
const date = new Date(dateString);
|
|
144
|
+
if (isNaN(date.getTime())) return dateString;
|
|
145
|
+
|
|
146
|
+
switch (dateFormat) {
|
|
147
|
+
case 'iso8601':
|
|
148
|
+
return date.toISOString();
|
|
149
|
+
case 'relative':
|
|
150
|
+
return this.relativeDate(date);
|
|
151
|
+
case 'friendly':
|
|
152
|
+
default:
|
|
153
|
+
return date.toLocaleDateString('en-US', {
|
|
154
|
+
year: 'numeric', month: 'short', day: 'numeric',
|
|
155
|
+
hour: '2-digit', minute: '2-digit',
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
private relativeDate(date: Date): string {
|
|
161
|
+
const diff = Date.now() - date.getTime();
|
|
162
|
+
const minutes = Math.floor(diff / 60000);
|
|
163
|
+
if (minutes < 1) return 'just now';
|
|
164
|
+
if (minutes < 60) return `${minutes}m ago`;
|
|
165
|
+
const hours = Math.floor(minutes / 60);
|
|
166
|
+
if (hours < 24) return `${hours}h ago`;
|
|
167
|
+
const days = Math.floor(hours / 24);
|
|
168
|
+
if (days < 30) return `${days}d ago`;
|
|
169
|
+
return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' });
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
private isDateField(key: string): boolean {
|
|
173
|
+
return /(_at|_on|_date|created|updated|scheduled|sent)$/i.test(key);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
private applyColor(text: string, colorFn: (s: string) => string): string {
|
|
177
|
+
const scheme = this.profileConfig?.output.colors ?? 'moderate';
|
|
178
|
+
if (scheme === 'none') return text;
|
|
179
|
+
return colorFn(text);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
private displayPagination(data: unknown, count: number): void {
|
|
183
|
+
if (data && typeof data === 'object' && 'pagination' in (data as any)) {
|
|
184
|
+
const p = (data as any).pagination;
|
|
185
|
+
if (p?.page && p?.per_page) {
|
|
186
|
+
this.info(`Page ${p.page} (${count} items)`);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|