@coastal-programs/notion-cli
Version:
Unofficial Notion CLI optimized for automation and AI agents. Non-interactive interface for Notion API v5.2.1 with intelligent caching, retry logic, structured error handling, and comprehensive testing.
258 lines (257 loc) • 9.25 kB
JavaScript
;
/**
* JSON Envelope Standardization System for Notion CLI
*
* Provides consistent machine-readable output across all commands with:
* - Standard success/error envelopes
* - Metadata tracking (command, timestamp, execution time)
* - Exit code standardization (0=success, 1=API error, 2=CLI error)
* - Proper stdout/stderr separation
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.isErrorEnvelope = exports.isSuccessEnvelope = exports.createEnvelopeFormatter = exports.EnvelopeFormatter = exports.ExitCode = void 0;
const index_1 = require("./errors/index");
/**
* Exit codes for consistent process termination
*/
var ExitCode;
(function (ExitCode) {
/** Command completed successfully */
ExitCode[ExitCode["SUCCESS"] = 0] = "SUCCESS";
/** API/Notion error (auth, not found, rate limit, network, etc.) */
ExitCode[ExitCode["API_ERROR"] = 1] = "API_ERROR";
/** CLI/validation error (invalid args, syntax, config issues) */
ExitCode[ExitCode["CLI_ERROR"] = 2] = "CLI_ERROR";
})(ExitCode = exports.ExitCode || (exports.ExitCode = {}));
/**
* Maps error codes to appropriate exit codes
*/
function getExitCodeForError(errorCode) {
// CLI/validation errors
const cliErrors = [
index_1.NotionCLIErrorCode.VALIDATION_ERROR,
'VALIDATION_ERROR',
'CLI_ERROR',
'CONFIG_ERROR',
'INVALID_ARGUMENT',
];
if (cliErrors.includes(errorCode)) {
return ExitCode.CLI_ERROR;
}
// All other errors are API-related
return ExitCode.API_ERROR;
}
/**
* Suggestion generator based on error codes
*/
function generateSuggestions(errorCode) {
const suggestions = [];
switch (errorCode) {
case index_1.NotionCLIErrorCode.UNAUTHORIZED:
suggestions.push('Verify your NOTION_TOKEN is set correctly');
suggestions.push('Check token at: https://www.notion.so/my-integrations');
break;
case index_1.NotionCLIErrorCode.NOT_FOUND:
suggestions.push('Verify the resource ID is correct');
suggestions.push('Ensure your integration has access to the resource');
suggestions.push('Try running: notion-cli sync');
break;
case index_1.NotionCLIErrorCode.RATE_LIMITED:
suggestions.push('Wait and retry - the CLI will auto-retry with backoff');
suggestions.push('Reduce request frequency if this persists');
break;
case index_1.NotionCLIErrorCode.VALIDATION_ERROR:
suggestions.push('Check command syntax: notion-cli [command] --help');
suggestions.push('Verify all required arguments are provided');
break;
case 'CLI_ERROR':
case 'CONFIG_ERROR':
suggestions.push('Run: notion-cli config set-token');
suggestions.push('Check your .env file configuration');
break;
}
return suggestions;
}
/**
* EnvelopeFormatter - Core utility for creating and outputting envelopes
*/
class EnvelopeFormatter {
/**
* Initialize formatter with command metadata
*
* @param commandName - Full command name (e.g., "page retrieve")
* @param version - CLI version from package.json
*/
constructor(commandName, version) {
this.startTime = Date.now();
this.commandName = commandName;
this.version = version;
}
/**
* Create success envelope with data and metadata
*
* @param data - The actual response data
* @param additionalMetadata - Optional additional metadata fields
* @returns Success envelope ready for output
*/
wrapSuccess(data, additionalMetadata) {
const executionTime = Date.now() - this.startTime;
return {
success: true,
data,
metadata: {
timestamp: new Date().toISOString(),
command: this.commandName,
execution_time_ms: executionTime,
version: this.version,
...additionalMetadata,
},
};
}
/**
* Create error envelope from Error, NotionCLIError, or raw error object
*
* @param error - Error instance or error object
* @param additionalContext - Optional additional error context
* @returns Error envelope ready for output
*/
wrapError(error, additionalContext) {
const executionTime = Date.now() - this.startTime;
let errorDetails;
// Handle NotionCLIError
if (error instanceof index_1.NotionCLIError) {
errorDetails = {
code: error.code,
message: error.message,
details: { ...error.context, ...additionalContext },
suggestions: error.suggestions.map(s => s.description),
notionError: error.context.originalError,
};
}
// Handle standard Error
else if (error instanceof Error) {
errorDetails = {
code: 'UNKNOWN',
message: error.message,
details: { stack: error.stack, ...additionalContext },
suggestions: ['Check the error message for details'],
};
}
// Handle raw error objects
else {
errorDetails = {
code: error.code || 'UNKNOWN',
message: error.message || 'An unknown error occurred',
details: { ...error, ...additionalContext },
suggestions: generateSuggestions(error.code || 'UNKNOWN'),
};
}
return {
success: false,
error: errorDetails,
metadata: {
timestamp: new Date().toISOString(),
command: this.commandName,
execution_time_ms: executionTime,
version: this.version,
},
};
}
/**
* Output envelope to stdout with proper formatting
* Handles flag-based format selection and stdout/stderr separation
*
* @param envelope - Success or error envelope
* @param flags - Output format flags
* @param logFn - Logging function (typically this.log from Command)
*/
outputEnvelope(envelope, flags, logFn = console.log) {
// Raw mode bypasses envelope - outputs data directly
if (flags.raw && envelope.success) {
logFn(JSON.stringify(envelope.data, null, 2));
return;
}
// Compact JSON - single line for piping
if (flags['compact-json']) {
logFn(JSON.stringify(envelope));
return;
}
// Default: Pretty JSON (--json flag or error state)
logFn(JSON.stringify(envelope, null, 2));
}
/**
* Get appropriate exit code for the envelope
*
* @param envelope - Success or error envelope
* @returns Exit code (0, 1, or 2)
*/
getExitCode(envelope) {
if (envelope.success) {
return ExitCode.SUCCESS;
}
// Type narrowing: at this point, envelope is ErrorEnvelope
return getExitCodeForError(envelope.error.code);
}
/**
* Write diagnostic messages to stderr (won't pollute JSON on stdout)
* Useful for retry messages, cache hits, debug info, etc.
*
* @param message - Diagnostic message
* @param level - Message level (info, warn, error)
*/
static writeDiagnostic(message, level = 'info') {
const prefix = {
info: '[INFO]',
warn: '[WARN]',
error: '[ERROR]',
}[level];
// Write to stderr to avoid polluting JSON output on stdout
console.error(`${prefix} ${message}`);
}
/**
* Helper to log retry attempts to stderr (doesn't pollute JSON output)
*
* @param attempt - Retry attempt number
* @param maxRetries - Maximum retry attempts
* @param delay - Delay before next retry in milliseconds
*/
static logRetry(attempt, maxRetries, delay) {
EnvelopeFormatter.writeDiagnostic(`Retry attempt ${attempt}/${maxRetries} after ${delay}ms`, 'warn');
}
/**
* Helper to log cache hits to stderr (for debugging)
*
* @param cacheKey - Cache key that was hit
*/
static logCacheHit(cacheKey) {
if (process.env.DEBUG === 'true') {
EnvelopeFormatter.writeDiagnostic(`Cache hit: ${cacheKey}`, 'info');
}
}
}
exports.EnvelopeFormatter = EnvelopeFormatter;
/**
* Convenience function to create an envelope formatter
*
* @param commandName - Full command name
* @param version - CLI version
* @returns New EnvelopeFormatter instance
*/
function createEnvelopeFormatter(commandName, version) {
return new EnvelopeFormatter(commandName, version);
}
exports.createEnvelopeFormatter = createEnvelopeFormatter;
/**
* Type guard to check if envelope is a success envelope
*/
function isSuccessEnvelope(envelope) {
return envelope.success === true;
}
exports.isSuccessEnvelope = isSuccessEnvelope;
/**
* Type guard to check if envelope is an error envelope
*/
function isErrorEnvelope(envelope) {
return envelope.success === false;
}
exports.isErrorEnvelope = isErrorEnvelope;