trmnl-cli 0.1.2 → 0.1.3
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/dist/index.js +25 -9
- package/dist/index.js.map +1 -1
- package/package.json +5 -2
package/dist/index.js
CHANGED
|
@@ -556,17 +556,33 @@ function validatePayload(payload, tier = "free") {
|
|
|
556
556
|
errors
|
|
557
557
|
};
|
|
558
558
|
}
|
|
559
|
-
function
|
|
559
|
+
function minifyHtml(html) {
|
|
560
|
+
return html.replace(/<!--[\s\S]*?-->/g, "").replace(/>\s+</g, "><").replace(/\s{2,}/g, " ").trim();
|
|
561
|
+
}
|
|
562
|
+
function minifyMergeVariables(vars) {
|
|
563
|
+
const result = {};
|
|
564
|
+
for (const [key, value] of Object.entries(vars)) {
|
|
565
|
+
if (typeof value === "string" && value.includes("<") && value.includes(">")) {
|
|
566
|
+
result[key] = minifyHtml(value);
|
|
567
|
+
} else {
|
|
568
|
+
result[key] = value;
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
return result;
|
|
572
|
+
}
|
|
573
|
+
function createPayload(content, options = {}) {
|
|
574
|
+
const shouldMinify = options.minify !== false;
|
|
560
575
|
try {
|
|
561
576
|
const parsed = JSON.parse(content);
|
|
562
|
-
|
|
563
|
-
|
|
577
|
+
const payload = parsed.merge_variables ? parsed : { merge_variables: parsed };
|
|
578
|
+
if (shouldMinify) {
|
|
579
|
+
payload.merge_variables = minifyMergeVariables(payload.merge_variables);
|
|
564
580
|
}
|
|
565
|
-
return
|
|
581
|
+
return payload;
|
|
566
582
|
} catch {
|
|
567
583
|
return {
|
|
568
584
|
merge_variables: {
|
|
569
|
-
content
|
|
585
|
+
content: shouldMinify ? minifyHtml(content) : content
|
|
570
586
|
}
|
|
571
587
|
};
|
|
572
588
|
}
|
|
@@ -700,7 +716,7 @@ function createHistoryEntry(payload, result, tier, pluginName) {
|
|
|
700
716
|
|
|
701
717
|
// src/commands/send.ts
|
|
702
718
|
function registerSendCommand(cli2) {
|
|
703
|
-
cli2.command("send", "Send content to TRMNL display").option("-c, --content <html>", "HTML content to send").option("-f, --file <path>", "Read content from file").option("-p, --plugin <name>", "Plugin to use (default: default plugin)").option("-w, --webhook <url>", "Override webhook URL directly").option("--skip-validation", "Skip payload validation").option("--skip-log", "Skip history logging").option("--json", "Output result as JSON").example('trmnl send --content "<div class=\\"layout\\">Hello</div>"').example("trmnl send --file ./output.html").example("trmnl send --file ./output.html --plugin office").example(`echo '{"merge_variables":{"content":"..."}}' | trmnl send`).action(async (options) => {
|
|
719
|
+
cli2.command("send", "Send content to TRMNL display").option("-c, --content <html>", "HTML content to send").option("-f, --file <path>", "Read content from file").option("-p, --plugin <name>", "Plugin to use (default: default plugin)").option("-w, --webhook <url>", "Override webhook URL directly").option("--skip-validation", "Skip payload validation").option("--skip-log", "Skip history logging").option("--no-minify", "Disable HTML minification (enabled by default)").option("--json", "Output result as JSON").example('trmnl send --content "<div class=\\"layout\\">Hello</div>"').example("trmnl send --file ./output.html").example("trmnl send --file ./output.html --plugin office").example(`echo '{"merge_variables":{"content":"..."}}' | trmnl send`).action(async (options) => {
|
|
704
720
|
let content;
|
|
705
721
|
if (options.content) {
|
|
706
722
|
content = options.content;
|
|
@@ -718,7 +734,7 @@ function registerSendCommand(cli2) {
|
|
|
718
734
|
process.exit(1);
|
|
719
735
|
}
|
|
720
736
|
}
|
|
721
|
-
const payload = createPayload(content);
|
|
737
|
+
const payload = createPayload(content, { minify: !options.noMinify });
|
|
722
738
|
const result = await sendToWebhook(payload, {
|
|
723
739
|
plugin: options.plugin,
|
|
724
740
|
webhookUrl: options.webhook,
|
|
@@ -767,7 +783,7 @@ async function readStdin() {
|
|
|
767
783
|
// src/commands/validate.ts
|
|
768
784
|
import { readFileSync as readFileSync4 } from "fs";
|
|
769
785
|
function registerValidateCommand(cli2) {
|
|
770
|
-
cli2.command("validate", "Validate payload without sending").option("-c, --content <html>", "HTML content to validate").option("-f, --file <path>", "Read content from file").option("-t, --tier <tier>", "Override tier (free or plus)").option("--json", "Output result as JSON").example("trmnl validate --file ./output.html").example('trmnl validate --content "<div>...</div>" --tier plus').action(async (options) => {
|
|
786
|
+
cli2.command("validate", "Validate payload without sending").option("-c, --content <html>", "HTML content to validate").option("-f, --file <path>", "Read content from file").option("-t, --tier <tier>", "Override tier (free or plus)").option("--no-minify", "Disable HTML minification (enabled by default)").option("--json", "Output result as JSON").example("trmnl validate --file ./output.html").example('trmnl validate --content "<div>...</div>" --tier plus').action(async (options) => {
|
|
771
787
|
let content;
|
|
772
788
|
if (options.content) {
|
|
773
789
|
content = options.content;
|
|
@@ -786,7 +802,7 @@ function registerValidateCommand(cli2) {
|
|
|
786
802
|
}
|
|
787
803
|
}
|
|
788
804
|
const tier = options.tier || getTier();
|
|
789
|
-
const payload = createPayload(content);
|
|
805
|
+
const payload = createPayload(content, { minify: !options.noMinify });
|
|
790
806
|
const result = validatePayload(payload, tier);
|
|
791
807
|
if (options.json) {
|
|
792
808
|
console.log(JSON.stringify(result, null, 2));
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/lib/config.ts","../src/types.ts","../src/lib/logger.ts","../src/commands/config.ts","../src/commands/history.ts","../src/commands/send.ts","../src/lib/validator.ts","../src/lib/webhook.ts","../src/commands/validate.ts"],"sourcesContent":["/**\n * trmnl-cli - CLI tool for TRMNL e-ink displays\n *\n * Commands:\n * trmnl send - Send content to TRMNL display\n * trmnl validate - Validate payload without sending\n * trmnl config - Manage CLI configuration\n * trmnl history - View send history\n */\n\nimport { createRequire } from \"node:module\";\nimport cac from \"cac\";\nimport { registerConfigCommand } from \"./commands/config.ts\";\nimport { registerHistoryCommand } from \"./commands/history.ts\";\nimport { registerSendCommand } from \"./commands/send.ts\";\nimport { registerValidateCommand } from \"./commands/validate.ts\";\n\nconst require = createRequire(import.meta.url);\nconst { version } = require(\"../package.json\");\n\nconst cli = cac(\"trmnl\");\n\ncli.version(version);\n\n// Register commands\nregisterSendCommand(cli);\nregisterValidateCommand(cli);\nregisterConfigCommand(cli);\nregisterHistoryCommand(cli);\n\n// Help text\ncli.help();\n\n// Default action (no command)\ncli.command(\"\").action(() => {\n cli.outputHelp();\n});\n\n// Parse and run\ncli.parse();\n","/**\n * Config file management for ~/.trmnl/config.json\n * Supports multiple plugins with a default\n */\n\nimport { existsSync, mkdirSync, readFileSync, writeFileSync, unlinkSync } from 'node:fs';\nimport { homedir } from 'node:os';\nimport { join } from 'node:path';\nimport type { Config, Plugin, WebhookTier } from '../types.ts';\nimport { DEFAULT_CONFIG } from '../types.ts';\n\n/** Config directory path */\nexport const CONFIG_DIR = join(homedir(), '.trmnl');\n\n/** Config file path */\nexport const CONFIG_PATH = join(CONFIG_DIR, 'config.json');\n\n/** Legacy TOML config path (for migration) */\nconst LEGACY_CONFIG_PATH = join(CONFIG_DIR, 'config.toml');\n\n/** History file path */\nexport const HISTORY_PATH = join(CONFIG_DIR, 'history.jsonl');\n\n/**\n * Ensure ~/.trmnl directory exists\n */\nexport function ensureConfigDir(): void {\n if (!existsSync(CONFIG_DIR)) {\n mkdirSync(CONFIG_DIR, { recursive: true });\n }\n}\n\n/**\n * Migrate legacy TOML config to JSON (one-time)\n */\nfunction migrateLegacyConfig(): Config | null {\n if (!existsSync(LEGACY_CONFIG_PATH)) {\n return null;\n }\n\n try {\n const content = readFileSync(LEGACY_CONFIG_PATH, 'utf-8');\n const config: Config = { plugins: {}, tier: 'free' };\n\n // Parse legacy TOML (simple parser for our format)\n let webhookUrl: string | undefined;\n\n for (const line of content.split('\\n')) {\n const trimmed = line.trim();\n const urlMatch = trimmed.match(/^url\\s*=\\s*\"([^\"]+)\"/);\n if (urlMatch) webhookUrl = urlMatch[1];\n }\n\n if (webhookUrl) {\n config.plugins['default'] = { url: webhookUrl };\n config.defaultPlugin = 'default';\n }\n\n // Remove legacy file after migration\n unlinkSync(LEGACY_CONFIG_PATH);\n console.log('Migrated legacy config.toml to config.json');\n\n return config;\n } catch {\n return null;\n }\n}\n\n/**\n * Load config from file\n */\nexport function loadConfig(): Config {\n ensureConfigDir();\n\n // Try to migrate legacy config first\n const migrated = migrateLegacyConfig();\n if (migrated) {\n saveConfig(migrated);\n return migrated;\n }\n\n if (!existsSync(CONFIG_PATH)) {\n return { ...DEFAULT_CONFIG };\n }\n\n try {\n const content = readFileSync(CONFIG_PATH, 'utf-8');\n const parsed = JSON.parse(content) as Partial<Config>;\n return {\n plugins: parsed.plugins || {},\n defaultPlugin: parsed.defaultPlugin,\n tier: parsed.tier || 'free',\n history: parsed.history || DEFAULT_CONFIG.history,\n };\n } catch {\n return { ...DEFAULT_CONFIG };\n }\n}\n\n/**\n * Save config to file\n */\nexport function saveConfig(config: Config): void {\n ensureConfigDir();\n const content = JSON.stringify(config, null, 2);\n writeFileSync(CONFIG_PATH, content, 'utf-8');\n}\n\n/**\n * Get a plugin by name (or default if not specified)\n */\nexport function getPlugin(name?: string): { name: string; plugin: Plugin } | null {\n const config = loadConfig();\n \n // If name specified, use that\n if (name) {\n const plugin = config.plugins[name];\n if (plugin) {\n return { name, plugin };\n }\n return null;\n }\n\n // Use default plugin\n const defaultName = config.defaultPlugin;\n if (defaultName && config.plugins[defaultName]) {\n return { name: defaultName, plugin: config.plugins[defaultName] };\n }\n\n // If only one plugin, use it\n const pluginNames = Object.keys(config.plugins);\n if (pluginNames.length === 1) {\n const name = pluginNames[0];\n return { name, plugin: config.plugins[name] };\n }\n\n return null;\n}\n\n/**\n * Add or update a plugin\n */\nexport function setPlugin(name: string, url: string, description?: string): void {\n const config = loadConfig();\n config.plugins[name] = { url, description };\n \n // If no default and this is the first plugin, make it default\n if (!config.defaultPlugin || Object.keys(config.plugins).length === 1) {\n config.defaultPlugin = name;\n }\n \n saveConfig(config);\n}\n\n/**\n * Remove a plugin\n */\nexport function removePlugin(name: string): boolean {\n const config = loadConfig();\n \n if (!config.plugins[name]) {\n return false;\n }\n \n delete config.plugins[name];\n \n // If we removed the default, pick a new one\n if (config.defaultPlugin === name) {\n const remaining = Object.keys(config.plugins);\n config.defaultPlugin = remaining.length > 0 ? remaining[0] : undefined;\n }\n \n saveConfig(config);\n return true;\n}\n\n/**\n * Set the default plugin\n */\nexport function setDefaultPlugin(name: string): boolean {\n const config = loadConfig();\n \n if (!config.plugins[name]) {\n return false;\n }\n \n config.defaultPlugin = name;\n saveConfig(config);\n return true;\n}\n\n/**\n * List all plugins\n */\nexport function listPlugins(): Array<{ name: string; plugin: Plugin; isDefault: boolean }> {\n const config = loadConfig();\n return Object.entries(config.plugins).map(([name, plugin]) => ({\n name,\n plugin,\n isDefault: name === config.defaultPlugin,\n }));\n}\n\n/**\n * Get global tier setting\n */\nexport function getTier(): WebhookTier {\n const config = loadConfig();\n return config.tier || 'free';\n}\n\n/**\n * Set global tier setting\n */\nexport function setTier(tier: WebhookTier): void {\n const config = loadConfig();\n config.tier = tier;\n saveConfig(config);\n}\n\n/**\n * Get webhook URL from environment, plugin name, or default\n */\nexport function getWebhookUrl(pluginName?: string): { url: string; name: string } | null {\n // Environment variable takes highest precedence\n const envUrl = process.env.TRMNL_WEBHOOK;\n if (envUrl) {\n return { url: envUrl, name: '$TRMNL_WEBHOOK' };\n }\n\n // Try to get plugin\n const result = getPlugin(pluginName);\n if (result) {\n return {\n url: result.plugin.url,\n name: result.name,\n };\n }\n\n return null;\n}\n\n/**\n * Get history config\n */\nexport function getHistoryConfig(): { path: string; maxSizeMb: number } {\n const config = loadConfig();\n const historyPath = config.history?.path || DEFAULT_CONFIG.history!.path!;\n const expandedPath = historyPath.startsWith('~') \n ? historyPath.replace('~', homedir())\n : historyPath;\n \n return {\n path: expandedPath,\n maxSizeMb: config.history?.maxSizeMb || DEFAULT_CONFIG.history!.maxSizeMb!,\n };\n}\n","/**\n * TRMNL CLI Types\n */\n\n/** Webhook tier determines payload size limits */\nexport type WebhookTier = 'free' | 'plus';\n\n/** Size limits per tier in bytes */\nexport const TIER_LIMITS: Record<WebhookTier, number> = {\n free: 2048, // 2 KB\n plus: 5120, // 5 KB\n};\n\n/** Plugin configuration */\nexport interface Plugin {\n url: string;\n description?: string;\n}\n\n/** CLI configuration stored in ~/.trmnl/config.json */\nexport interface Config {\n plugins: Record<string, Plugin>;\n defaultPlugin?: string;\n tier?: WebhookTier; // Global tier setting\n history?: {\n path?: string;\n maxSizeMb?: number;\n };\n}\n\n/** Default config values */\nexport const DEFAULT_CONFIG: Config = {\n plugins: {},\n defaultPlugin: undefined,\n tier: 'free',\n history: {\n path: '~/.trmnl/history.jsonl',\n maxSizeMb: 100,\n },\n};\n\n/** Merge variables payload structure */\nexport interface MergeVariables {\n content?: string;\n title?: string;\n text?: string;\n image?: string;\n [key: string]: string | undefined;\n}\n\n/** Webhook request payload */\nexport interface WebhookPayload {\n merge_variables: MergeVariables;\n}\n\n/** History entry stored in JSONL */\nexport interface HistoryEntry {\n timestamp: string;\n plugin: string;\n size_bytes: number;\n tier: WebhookTier;\n payload: WebhookPayload;\n success: boolean;\n status_code?: number;\n response?: string;\n error?: string;\n duration_ms: number;\n}\n\n/** Validation result */\nexport interface ValidationResult {\n valid: boolean;\n size_bytes: number;\n tier: WebhookTier;\n limit_bytes: number;\n remaining_bytes: number;\n percent_used: number;\n warnings: string[];\n errors: string[];\n}\n","/**\n * JSONL history logger for tracking sent payloads\n */\n\nimport { appendFileSync, existsSync, readFileSync, statSync } from 'node:fs';\nimport { ensureConfigDir, getHistoryConfig } from './config.ts';\nimport type { HistoryEntry } from '../types.ts';\n\n/**\n * Append a history entry to the JSONL file\n */\nexport function logEntry(entry: HistoryEntry): void {\n ensureConfigDir();\n const { path } = getHistoryConfig();\n const line = JSON.stringify(entry) + '\\n';\n appendFileSync(path, line, 'utf-8');\n}\n\n/**\n * Read all history entries\n */\nexport function readHistory(): HistoryEntry[] {\n const { path } = getHistoryConfig();\n \n if (!existsSync(path)) {\n return [];\n }\n\n try {\n const content = readFileSync(path, 'utf-8');\n const lines = content.trim().split('\\n').filter(Boolean);\n return lines.map(line => JSON.parse(line) as HistoryEntry);\n } catch {\n return [];\n }\n}\n\n/**\n * Get history entries with filters\n */\nexport interface HistoryFilter {\n last?: number;\n today?: boolean;\n failed?: boolean;\n success?: boolean;\n plugin?: string;\n since?: Date;\n until?: Date;\n}\n\nexport function getHistory(filter: HistoryFilter = {}): HistoryEntry[] {\n let entries = readHistory();\n\n // Filter by plugin\n if (filter.plugin) {\n entries = entries.filter(e => e.plugin === filter.plugin);\n }\n\n // Filter by success/failed\n if (filter.failed) {\n entries = entries.filter(e => !e.success);\n }\n if (filter.success) {\n entries = entries.filter(e => e.success);\n }\n\n // Filter by date\n if (filter.today) {\n const today = new Date();\n today.setHours(0, 0, 0, 0);\n entries = entries.filter(e => new Date(e.timestamp) >= today);\n }\n if (filter.since) {\n entries = entries.filter(e => new Date(e.timestamp) >= filter.since!);\n }\n if (filter.until) {\n entries = entries.filter(e => new Date(e.timestamp) <= filter.until!);\n }\n\n // Sort by timestamp descending (most recent first)\n entries.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());\n\n // Limit results\n if (filter.last) {\n entries = entries.slice(0, filter.last);\n }\n\n return entries;\n}\n\n/**\n * Format a history entry for display\n */\nexport function formatEntry(entry: HistoryEntry, verbose = false): string {\n const time = new Date(entry.timestamp).toLocaleString();\n const status = entry.success ? '✓' : '✗';\n const sizeKb = (entry.size_bytes / 1024).toFixed(2);\n \n let line = `${status} ${time} | ${entry.plugin} | ${sizeKb} KB | ${entry.duration_ms}ms`;\n \n if (!entry.success && entry.error) {\n line += ` | ${entry.error}`;\n }\n \n if (verbose && entry.payload?.merge_variables?.content) {\n const preview = entry.payload.merge_variables.content.substring(0, 80);\n line += `\\n ${preview}${entry.payload.merge_variables.content.length > 80 ? '...' : ''}`;\n }\n \n return line;\n}\n\n/**\n * Get history file stats\n */\nexport function getHistoryStats(): { entries: number; sizeBytes: number; sizeMb: number } | null {\n const { path } = getHistoryConfig();\n \n if (!existsSync(path)) {\n return null;\n }\n\n try {\n const stats = statSync(path);\n const entries = readHistory().length;\n return {\n entries,\n sizeBytes: stats.size,\n sizeMb: Math.round((stats.size / 1024 / 1024) * 100) / 100,\n };\n } catch {\n return null;\n }\n}\n\n/**\n * Get history file path\n */\nexport function getHistoryPath(): string {\n return getHistoryConfig().path;\n}\n","/**\n * trmnl config - Manage CLI configuration\n * trmnl plugin - Manage webhook plugins\n */\n\nimport type { CAC } from 'cac';\nimport {\n CONFIG_PATH,\n getTier,\n listPlugins,\n loadConfig,\n removePlugin,\n setDefaultPlugin,\n setPlugin,\n setTier,\n} from '../lib/config.ts';\nimport { getHistoryPath } from '../lib/logger.ts';\nimport type { WebhookTier } from '../types.ts';\n\nexport function registerConfigCommand(cli: CAC): void {\n // Config show\n cli\n .command('config', 'Show configuration')\n .action(() => {\n const config = loadConfig();\n const plugins = listPlugins();\n\n console.log(`Config file: ${CONFIG_PATH}`);\n console.log('');\n\n console.log('Plugins:');\n if (plugins.length === 0) {\n console.log(' (none configured)');\n console.log('');\n console.log(' Add a plugin:');\n console.log(' trmnl plugin add <name> <url>');\n } else {\n for (const { name, plugin, isDefault } of plugins) {\n const defaultMark = isDefault ? ' (default)' : '';\n console.log(` ${name}${defaultMark}`);\n console.log(` url: ${plugin.url}`);\n if (plugin.description) {\n console.log(` desc: ${plugin.description}`);\n }\n }\n }\n\n console.log('');\n console.log(`Tier: ${config.tier || 'free'}`);\n console.log(` Limit: ${config.tier === 'plus' ? '5 KB' : '2 KB'}`);\n\n console.log('');\n console.log('History:');\n console.log(` path: ${getHistoryPath()}`);\n\n console.log('');\n console.log('Environment:');\n console.log(` TRMNL_WEBHOOK: ${process.env.TRMNL_WEBHOOK || '(not set)'}`);\n });\n\n // Tier command (separate from config)\n cli\n .command('tier [value]', 'Get or set tier (free or plus)')\n .example('trmnl tier # Show current tier')\n .example('trmnl tier plus # Set to plus')\n .example('trmnl tier free # Set to free')\n .action((value?: string) => {\n if (!value) {\n const tier = getTier();\n console.log(`Tier: ${tier}`);\n console.log(`Limit: ${tier === 'plus' ? '5 KB (5,120 bytes)' : '2 KB (2,048 bytes)'}`);\n return;\n }\n\n if (value !== 'free' && value !== 'plus') {\n console.error('Invalid tier. Use \"free\" or \"plus\".');\n process.exit(1);\n }\n\n setTier(value as WebhookTier);\n console.log(`✓ Tier set to: ${value}`);\n });\n\n // Plugin command with action as first arg\n cli\n .command('plugin [action] [name] [url]', 'Manage webhook plugins')\n .option('-d, --desc <description>', 'Plugin description')\n .option('-u, --url <url>', 'Webhook URL (for set action)')\n .option('--default', 'Set as default plugin')\n .example('trmnl plugin # List plugins')\n .example('trmnl plugin add home <url> # Add plugin')\n .example('trmnl plugin rm home # Remove plugin')\n .example('trmnl plugin default home # Set default')\n .example('trmnl plugin set home --url ... # Update plugin')\n .action((action?: string, name?: string, url?: string, options?: { desc?: string; url?: string; default?: boolean }) => {\n // No action = list\n if (!action) {\n showPluginList();\n return;\n }\n\n // Handle actions\n switch (action) {\n case 'add':\n if (!name || !url) {\n console.error('Usage: trmnl plugin add <name> <url>');\n process.exit(1);\n }\n setPlugin(name, url, options?.desc);\n console.log(`✓ Added plugin: ${name}`);\n if (options?.default) {\n setDefaultPlugin(name);\n console.log(`✓ Set as default`);\n }\n break;\n\n case 'rm':\n case 'remove':\n if (!name) {\n console.error('Usage: trmnl plugin rm <name>');\n process.exit(1);\n }\n if (removePlugin(name)) {\n console.log(`✓ Removed plugin: ${name}`);\n } else {\n console.error(`Plugin not found: ${name}`);\n process.exit(1);\n }\n break;\n\n case 'default':\n if (!name) {\n console.error('Usage: trmnl plugin default <name>');\n process.exit(1);\n }\n if (setDefaultPlugin(name)) {\n console.log(`✓ Default plugin: ${name}`);\n } else {\n console.error(`Plugin not found: ${name}`);\n process.exit(1);\n }\n break;\n\n case 'set':\n case 'update':\n if (!name) {\n console.error('Usage: trmnl plugin set <name> [options]');\n process.exit(1);\n }\n const plugins = listPlugins();\n const existing = plugins.find(p => p.name === name);\n if (!existing) {\n console.error(`Plugin not found: ${name}`);\n process.exit(1);\n }\n const newUrl = options?.url || existing.plugin.url;\n const newDesc = options?.desc !== undefined ? options.desc : existing.plugin.description;\n setPlugin(name, newUrl, newDesc);\n console.log(`✓ Updated plugin: ${name}`);\n break;\n\n case 'list':\n showPluginList();\n break;\n\n default:\n console.error(`Unknown action: ${action}`);\n console.log('');\n console.log('Available actions:');\n console.log(' add <name> <url> - Add a plugin');\n console.log(' rm <name> - Remove a plugin');\n console.log(' default <name> - Set default plugin');\n console.log(' set <name> - Update a plugin');\n console.log(' list - List all plugins');\n process.exit(1);\n }\n });\n\n // Plugins alias for list\n cli\n .command('plugins', 'List all plugins')\n .action(() => {\n showPluginList();\n });\n}\n\nfunction showPluginList(): void {\n const plugins = listPlugins();\n\n if (plugins.length === 0) {\n console.log('No plugins configured.');\n console.log('');\n console.log('Add a plugin:');\n console.log(' trmnl plugin add <name> <url>');\n return;\n }\n\n console.log('Plugins:');\n for (const { name, plugin, isDefault } of plugins) {\n const defaultMark = isDefault ? ' ★' : '';\n console.log(` ${name}${defaultMark}`);\n console.log(` ${plugin.url}`);\n if (plugin.description) {\n console.log(` ${plugin.description}`);\n }\n }\n\n console.log('');\n console.log('★ = default plugin');\n}\n","/**\n * trmnl history - View send history\n */\n\nimport { unlinkSync } from 'node:fs';\nimport type { CAC } from 'cac';\nimport { formatEntry, getHistory, getHistoryPath, getHistoryStats, type HistoryFilter } from '../lib/logger.ts';\n\ninterface HistoryOptions {\n last?: number;\n today?: boolean;\n failed?: boolean;\n success?: boolean;\n plugin?: string;\n json?: boolean;\n verbose?: boolean;\n}\n\nexport function registerHistoryCommand(cli: CAC): void {\n cli\n .command('history', 'View send history')\n .option('-n, --last <n>', 'Show last N entries', { default: 10 })\n .option('--today', 'Show only today\\'s entries')\n .option('--failed', 'Show only failed sends')\n .option('--success', 'Show only successful sends')\n .option('-p, --plugin <name>', 'Filter by plugin name')\n .option('--json', 'Output as JSON')\n .option('-v, --verbose', 'Show content preview')\n .example('trmnl history')\n .example('trmnl history --last 20')\n .example('trmnl history --today --failed')\n .example('trmnl history --plugin office')\n .action((options: HistoryOptions) => {\n const filter: HistoryFilter = {\n last: options.last,\n today: options.today,\n failed: options.failed,\n success: options.success,\n plugin: options.plugin,\n };\n\n const entries = getHistory(filter);\n\n if (options.json) {\n console.log(JSON.stringify(entries, null, 2));\n return;\n }\n\n if (entries.length === 0) {\n console.log('No history entries found.');\n console.log(`History file: ${getHistoryPath()}`);\n return;\n }\n\n // Stats header\n const stats = getHistoryStats();\n if (stats) {\n console.log(`History: ${stats.entries} total entries (${stats.sizeMb} MB)`);\n console.log('');\n }\n\n // Filter description\n const filterParts: string[] = [];\n if (options.today) filterParts.push('today');\n if (options.failed) filterParts.push('failed');\n if (options.success) filterParts.push('success');\n if (options.plugin) filterParts.push(`plugin: ${options.plugin}`);\n if (filterParts.length > 0) {\n console.log(`Filter: ${filterParts.join(', ')}`);\n console.log('');\n }\n\n // Entries\n console.log(`Showing ${entries.length} entries (most recent first):`);\n console.log('');\n\n for (const entry of entries) {\n console.log(formatEntry(entry, options.verbose));\n }\n });\n\n // History clear\n cli\n .command('history clear', 'Clear send history')\n .option('--confirm', 'Confirm deletion')\n .action((options: { confirm?: boolean }) => {\n const historyPath = getHistoryPath();\n \n if (!options.confirm) {\n console.log('This will delete all history. Use --confirm to proceed.');\n console.log(`History file: ${historyPath}`);\n return;\n }\n\n try {\n unlinkSync(historyPath);\n console.log('✓ History cleared');\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === 'ENOENT') {\n console.log('History file does not exist.');\n } else {\n console.error('Error clearing history:', err);\n }\n }\n });\n\n // History stats\n cli\n .command('history stats', 'Show history statistics')\n .action(() => {\n const stats = getHistoryStats();\n \n if (!stats) {\n console.log('No history file found.');\n return;\n }\n\n const entries = getHistory({});\n const successCount = entries.filter(e => e.success).length;\n const failedCount = entries.filter(e => !e.success).length;\n const totalBytes = entries.reduce((sum, e) => sum + e.size_bytes, 0);\n const avgBytes = entries.length > 0 ? Math.round(totalBytes / entries.length) : 0;\n const avgDuration = entries.length > 0 \n ? Math.round(entries.reduce((sum, e) => sum + e.duration_ms, 0) / entries.length) \n : 0;\n\n // Plugin breakdown\n const byPlugin = new Map<string, number>();\n for (const entry of entries) {\n byPlugin.set(entry.plugin, (byPlugin.get(entry.plugin) || 0) + 1);\n }\n\n console.log('History Statistics');\n console.log('');\n console.log(`File: ${getHistoryPath()}`);\n console.log(`Size: ${stats.sizeMb} MB`);\n console.log('');\n console.log(`Total: ${entries.length} sends`);\n console.log(`Success: ${successCount} (${entries.length > 0 ? Math.round(successCount / entries.length * 100) : 0}%)`);\n console.log(`Failed: ${failedCount} (${entries.length > 0 ? Math.round(failedCount / entries.length * 100) : 0}%)`);\n console.log('');\n console.log(`Avg size: ${avgBytes} bytes`);\n console.log(`Avg duration: ${avgDuration}ms`);\n\n if (byPlugin.size > 1) {\n console.log('');\n console.log('By plugin:');\n for (const [plugin, count] of byPlugin.entries()) {\n console.log(` ${plugin}: ${count} sends`);\n }\n }\n\n // Recent activity\n const today = getHistory({ today: true });\n const thisWeek = getHistory({ since: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000) });\n console.log('');\n console.log(`Today: ${today.length} sends`);\n console.log(`This week: ${thisWeek.length} sends`);\n });\n}\n","/**\n * trmnl send - Send content to TRMNL display\n */\n\nimport { readFileSync } from 'node:fs';\nimport type { CAC } from 'cac';\nimport { createPayload, formatValidation } from '../lib/validator.ts';\nimport { sendToWebhook } from '../lib/webhook.ts';\n\ninterface SendOptions {\n content?: string;\n file?: string;\n plugin?: string;\n webhook?: string;\n skipValidation?: boolean;\n skipLog?: boolean;\n json?: boolean;\n}\n\nexport function registerSendCommand(cli: CAC): void {\n cli\n .command('send', 'Send content to TRMNL display')\n .option('-c, --content <html>', 'HTML content to send')\n .option('-f, --file <path>', 'Read content from file')\n .option('-p, --plugin <name>', 'Plugin to use (default: default plugin)')\n .option('-w, --webhook <url>', 'Override webhook URL directly')\n .option('--skip-validation', 'Skip payload validation')\n .option('--skip-log', 'Skip history logging')\n .option('--json', 'Output result as JSON')\n .example('trmnl send --content \"<div class=\\\\\"layout\\\\\">Hello</div>\"')\n .example('trmnl send --file ./output.html')\n .example('trmnl send --file ./output.html --plugin office')\n .example('echo \\'{\"merge_variables\":{\"content\":\"...\"}}\\' | trmnl send')\n .action(async (options: SendOptions) => {\n let content: string;\n\n // Get content from options, file, or stdin\n if (options.content) {\n content = options.content;\n } else if (options.file) {\n try {\n content = readFileSync(options.file, 'utf-8');\n } catch (err) {\n console.error(`Error reading file: ${options.file}`);\n process.exit(1);\n }\n } else {\n // Try reading from stdin\n content = await readStdin();\n if (!content) {\n console.error('No content provided. Use --content, --file, or pipe content via stdin.');\n process.exit(1);\n }\n }\n\n // Create payload\n const payload = createPayload(content);\n\n // Send\n const result = await sendToWebhook(payload, {\n plugin: options.plugin,\n webhookUrl: options.webhook,\n skipValidation: options.skipValidation,\n skipLog: options.skipLog,\n });\n\n // Output\n if (options.json) {\n console.log(JSON.stringify(result, null, 2));\n } else {\n if (result.success) {\n console.log(`✓ Sent to TRMNL (${result.pluginName})`);\n console.log(` Status: ${result.statusCode}`);\n console.log(` Time: ${result.durationMs}ms`);\n console.log(` Size: ${result.validation.size_bytes} bytes (${result.validation.percent_used}% of limit)`);\n } else {\n console.error('✗ Failed to send');\n console.error(` Error: ${result.error}`);\n if (result.pluginName) {\n console.error(` Plugin: ${result.pluginName}`);\n }\n console.log('');\n console.log('Validation:');\n console.log(formatValidation(result.validation));\n }\n }\n\n process.exit(result.success ? 0 : 1);\n });\n}\n\n/**\n * Read content from stdin (non-blocking check)\n */\nasync function readStdin(): Promise<string> {\n // Check if stdin has data (not a TTY)\n if (process.stdin.isTTY) {\n return '';\n }\n\n const chunks: Buffer[] = [];\n \n return new Promise((resolve) => {\n process.stdin.on('data', (chunk) => chunks.push(chunk));\n process.stdin.on('end', () => resolve(Buffer.concat(chunks).toString('utf-8').trim()));\n process.stdin.on('error', () => resolve(''));\n \n // Timeout to avoid hanging\n setTimeout(() => {\n if (chunks.length === 0) {\n resolve('');\n }\n }, 100);\n });\n}\n","/**\n * Payload validation for TRMNL webhooks\n */\n\nimport { TIER_LIMITS, type ValidationResult, type WebhookPayload, type WebhookTier } from '../types.ts';\n\n/**\n * Validate a webhook payload against size limits\n */\nexport function validatePayload(payload: WebhookPayload, tier: WebhookTier = 'free'): ValidationResult {\n const jsonString = JSON.stringify(payload);\n const sizeBytes = new TextEncoder().encode(jsonString).length;\n const limitBytes = TIER_LIMITS[tier];\n const remainingBytes = limitBytes - sizeBytes;\n const percentUsed = Math.round((sizeBytes / limitBytes) * 1000) / 10;\n\n const warnings: string[] = [];\n const errors: string[] = [];\n\n // Size check\n if (sizeBytes > limitBytes) {\n errors.push(`Payload exceeds ${tier} tier limit: ${sizeBytes} bytes > ${limitBytes} bytes`);\n } else if (percentUsed > 90) {\n warnings.push(`Payload is at ${percentUsed}% of ${tier} tier limit`);\n }\n\n // Content check\n if (!payload.merge_variables) {\n errors.push('Missing merge_variables object');\n } else if (!payload.merge_variables.content && !payload.merge_variables.text) {\n warnings.push('No content or text field in merge_variables');\n }\n\n // HTML sanity checks\n const content = payload.merge_variables?.content || '';\n if (content) {\n // Check for unclosed tags (basic check)\n const openDivs = (content.match(/<div/g) || []).length;\n const closeDivs = (content.match(/<\\/div>/g) || []).length;\n if (openDivs !== closeDivs) {\n warnings.push(`Potential unclosed divs: ${openDivs} open, ${closeDivs} close`);\n }\n\n // Check for common TRMNL patterns - look for 'layout' as a class (may have other classes too)\n const hasLayoutClass = /class=[\"'][^\"']*\\blayout\\b[^\"']*[\"']/.test(content);\n if (!hasLayoutClass) {\n warnings.push('Missing .layout class - TRMNL requires a root layout element');\n }\n }\n\n return {\n valid: errors.length === 0,\n size_bytes: sizeBytes,\n tier,\n limit_bytes: limitBytes,\n remaining_bytes: remainingBytes,\n percent_used: percentUsed,\n warnings,\n errors,\n };\n}\n\n/**\n * Parse content into a webhook payload\n */\nexport function createPayload(content: string): WebhookPayload {\n // Try to parse as JSON first\n try {\n const parsed = JSON.parse(content);\n if (parsed.merge_variables) {\n return parsed as WebhookPayload;\n }\n // If it's just merge_variables content\n return { merge_variables: parsed };\n } catch {\n // Treat as raw HTML content\n return {\n merge_variables: {\n content: content,\n },\n };\n }\n}\n\n/**\n * Format validation result for display\n */\nexport function formatValidation(result: ValidationResult): string {\n const lines: string[] = [];\n \n const status = result.valid ? '✓' : '✗';\n const sizeKb = (result.size_bytes / 1024).toFixed(2);\n const limitKb = (result.limit_bytes / 1024).toFixed(2);\n \n lines.push(`${status} Payload: ${result.size_bytes} bytes (${sizeKb} KB)`);\n lines.push(` Tier: ${result.tier} (limit: ${limitKb} KB)`);\n lines.push(` Used: ${result.percent_used}% (${result.remaining_bytes} bytes remaining)`);\n \n if (result.errors.length > 0) {\n lines.push('');\n lines.push('Errors:');\n for (const error of result.errors) {\n lines.push(` ✗ ${error}`);\n }\n }\n \n if (result.warnings.length > 0) {\n lines.push('');\n lines.push('Warnings:');\n for (const warning of result.warnings) {\n lines.push(` ⚠ ${warning}`);\n }\n }\n \n return lines.join('\\n');\n}\n","/**\n * Webhook sending logic\n */\n\nimport { getTier, getWebhookUrl } from './config.ts';\nimport { logEntry } from './logger.ts';\nimport { validatePayload } from './validator.ts';\nimport type { HistoryEntry, WebhookPayload, WebhookTier } from '../types.ts';\n\nexport interface SendResult {\n success: boolean;\n pluginName: string;\n statusCode?: number;\n response?: string;\n error?: string;\n durationMs: number;\n validation: ReturnType<typeof validatePayload>;\n}\n\nexport interface SendOptions {\n plugin?: string; // Plugin name to use\n webhookUrl?: string; // Direct URL override\n skipValidation?: boolean;\n skipLog?: boolean;\n}\n\n/**\n * Send payload to TRMNL webhook\n */\nexport async function sendToWebhook(\n payload: WebhookPayload,\n options: SendOptions = {}\n): Promise<SendResult> {\n const startTime = Date.now();\n const tier = getTier();\n \n // Resolve webhook URL\n let webhookUrl: string;\n let pluginName: string;\n\n if (options.webhookUrl) {\n // Direct URL override\n webhookUrl = options.webhookUrl;\n pluginName = 'custom';\n } else {\n // Get from config/env\n const resolved = getWebhookUrl(options.plugin);\n if (!resolved) {\n const durationMs = Date.now() - startTime;\n const validation = validatePayload(payload, tier);\n \n let error = 'No webhook URL configured.';\n if (options.plugin) {\n error = `Plugin \"${options.plugin}\" not found.`;\n } else {\n error += ' Add a plugin with: trmnl plugin add <name> <url>';\n }\n \n return {\n success: false,\n pluginName: options.plugin || 'unknown',\n error,\n durationMs,\n validation,\n };\n }\n webhookUrl = resolved.url;\n pluginName = resolved.name;\n }\n\n // Validate payload\n const validation = validatePayload(payload, tier);\n \n if (!options.skipValidation && !validation.valid) {\n const durationMs = Date.now() - startTime;\n const result: SendResult = {\n success: false,\n pluginName,\n error: validation.errors.join('; '),\n durationMs,\n validation,\n };\n \n // Log failed validation\n if (!options.skipLog) {\n logEntry(createHistoryEntry(payload, result, tier, pluginName));\n }\n \n return result;\n }\n \n // Send request\n try {\n const response = await fetch(webhookUrl, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(payload),\n });\n \n const durationMs = Date.now() - startTime;\n const responseText = await response.text();\n \n const result: SendResult = {\n success: response.ok,\n pluginName,\n statusCode: response.status,\n response: responseText,\n durationMs,\n validation,\n };\n \n if (!response.ok) {\n result.error = `HTTP ${response.status}: ${responseText}`;\n }\n \n // Log the send\n if (!options.skipLog) {\n logEntry(createHistoryEntry(payload, result, tier, pluginName));\n }\n \n return result;\n \n } catch (err) {\n const durationMs = Date.now() - startTime;\n const error = err instanceof Error ? err.message : 'Unknown error';\n \n const result: SendResult = {\n success: false,\n pluginName,\n error,\n durationMs,\n validation,\n };\n \n // Log the error\n if (!options.skipLog) {\n logEntry(createHistoryEntry(payload, result, tier, pluginName));\n }\n \n return result;\n }\n}\n\n/**\n * Create a history entry from send result\n */\nfunction createHistoryEntry(\n payload: WebhookPayload,\n result: SendResult,\n tier: WebhookTier,\n pluginName: string\n): HistoryEntry {\n return {\n timestamp: new Date().toISOString(),\n plugin: pluginName,\n size_bytes: result.validation.size_bytes,\n tier,\n payload,\n success: result.success,\n status_code: result.statusCode,\n response: result.response,\n error: result.error,\n duration_ms: result.durationMs,\n };\n}\n","/**\n * trmnl validate - Validate payload without sending\n */\n\nimport { readFileSync } from 'node:fs';\nimport type { CAC } from 'cac';\nimport { getTier } from '../lib/config.ts';\nimport { createPayload, formatValidation, validatePayload } from '../lib/validator.ts';\nimport type { WebhookTier } from '../types.ts';\n\ninterface ValidateOptions {\n content?: string;\n file?: string;\n tier?: WebhookTier;\n json?: boolean;\n}\n\nexport function registerValidateCommand(cli: CAC): void {\n cli\n .command('validate', 'Validate payload without sending')\n .option('-c, --content <html>', 'HTML content to validate')\n .option('-f, --file <path>', 'Read content from file')\n .option('-t, --tier <tier>', 'Override tier (free or plus)')\n .option('--json', 'Output result as JSON')\n .example('trmnl validate --file ./output.html')\n .example('trmnl validate --content \"<div>...</div>\" --tier plus')\n .action(async (options: ValidateOptions) => {\n let content: string;\n\n // Get content from options, file, or stdin\n if (options.content) {\n content = options.content;\n } else if (options.file) {\n try {\n content = readFileSync(options.file, 'utf-8');\n } catch (err) {\n console.error(`Error reading file: ${options.file}`);\n process.exit(1);\n }\n } else {\n // Try reading from stdin\n content = await readStdin();\n if (!content) {\n console.error('No content provided. Use --content, --file, or pipe content via stdin.');\n process.exit(1);\n }\n }\n\n // Use explicit tier or global config\n const tier = options.tier || getTier();\n\n // Create payload and validate\n const payload = createPayload(content);\n const result = validatePayload(payload, tier);\n\n // Output\n if (options.json) {\n console.log(JSON.stringify(result, null, 2));\n } else {\n console.log(formatValidation(result));\n }\n\n process.exit(result.valid ? 0 : 1);\n });\n}\n\n/**\n * Read content from stdin (non-blocking check)\n */\nasync function readStdin(): Promise<string> {\n if (process.stdin.isTTY) {\n return '';\n }\n\n const chunks: Buffer[] = [];\n \n return new Promise((resolve) => {\n process.stdin.on('data', (chunk) => chunks.push(chunk));\n process.stdin.on('end', () => resolve(Buffer.concat(chunks).toString('utf-8').trim()));\n process.stdin.on('error', () => resolve(''));\n \n setTimeout(() => {\n if (chunks.length === 0) {\n resolve('');\n }\n }, 100);\n });\n}\n"],"mappings":"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","names":["name","existsSync","readFileSync","existsSync","readFileSync","cli","unlinkSync","cli","unlinkSync","readFileSync","validation","cli","readFileSync","readFileSync","cli","readFileSync","readStdin","require"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/lib/config.ts","../src/types.ts","../src/lib/logger.ts","../src/commands/config.ts","../src/commands/history.ts","../src/commands/send.ts","../src/lib/validator.ts","../src/lib/webhook.ts","../src/commands/validate.ts"],"sourcesContent":["/**\n * trmnl-cli - CLI tool for TRMNL e-ink displays\n *\n * Commands:\n * trmnl send - Send content to TRMNL display\n * trmnl validate - Validate payload without sending\n * trmnl config - Manage CLI configuration\n * trmnl history - View send history\n */\n\nimport { createRequire } from \"node:module\";\nimport cac from \"cac\";\nimport { registerConfigCommand } from \"./commands/config.ts\";\nimport { registerHistoryCommand } from \"./commands/history.ts\";\nimport { registerSendCommand } from \"./commands/send.ts\";\nimport { registerValidateCommand } from \"./commands/validate.ts\";\n\nconst require = createRequire(import.meta.url);\nconst { version } = require(\"../package.json\");\n\nconst cli = cac(\"trmnl\");\n\ncli.version(version);\n\n// Register commands\nregisterSendCommand(cli);\nregisterValidateCommand(cli);\nregisterConfigCommand(cli);\nregisterHistoryCommand(cli);\n\n// Help text\ncli.help();\n\n// Default action (no command)\ncli.command(\"\").action(() => {\n cli.outputHelp();\n});\n\n// Parse and run\ncli.parse();\n","/**\n * Config file management for ~/.trmnl/config.json\n * Supports multiple plugins with a default\n */\n\nimport { existsSync, mkdirSync, readFileSync, writeFileSync, unlinkSync } from 'node:fs';\nimport { homedir } from 'node:os';\nimport { join } from 'node:path';\nimport type { Config, Plugin, WebhookTier } from '../types.ts';\nimport { DEFAULT_CONFIG } from '../types.ts';\n\n/** Config directory path */\nexport const CONFIG_DIR = join(homedir(), '.trmnl');\n\n/** Config file path */\nexport const CONFIG_PATH = join(CONFIG_DIR, 'config.json');\n\n/** Legacy TOML config path (for migration) */\nconst LEGACY_CONFIG_PATH = join(CONFIG_DIR, 'config.toml');\n\n/** History file path */\nexport const HISTORY_PATH = join(CONFIG_DIR, 'history.jsonl');\n\n/**\n * Ensure ~/.trmnl directory exists\n */\nexport function ensureConfigDir(): void {\n if (!existsSync(CONFIG_DIR)) {\n mkdirSync(CONFIG_DIR, { recursive: true });\n }\n}\n\n/**\n * Migrate legacy TOML config to JSON (one-time)\n */\nfunction migrateLegacyConfig(): Config | null {\n if (!existsSync(LEGACY_CONFIG_PATH)) {\n return null;\n }\n\n try {\n const content = readFileSync(LEGACY_CONFIG_PATH, 'utf-8');\n const config: Config = { plugins: {}, tier: 'free' };\n\n // Parse legacy TOML (simple parser for our format)\n let webhookUrl: string | undefined;\n\n for (const line of content.split('\\n')) {\n const trimmed = line.trim();\n const urlMatch = trimmed.match(/^url\\s*=\\s*\"([^\"]+)\"/);\n if (urlMatch) webhookUrl = urlMatch[1];\n }\n\n if (webhookUrl) {\n config.plugins['default'] = { url: webhookUrl };\n config.defaultPlugin = 'default';\n }\n\n // Remove legacy file after migration\n unlinkSync(LEGACY_CONFIG_PATH);\n console.log('Migrated legacy config.toml to config.json');\n\n return config;\n } catch {\n return null;\n }\n}\n\n/**\n * Load config from file\n */\nexport function loadConfig(): Config {\n ensureConfigDir();\n\n // Try to migrate legacy config first\n const migrated = migrateLegacyConfig();\n if (migrated) {\n saveConfig(migrated);\n return migrated;\n }\n\n if (!existsSync(CONFIG_PATH)) {\n return { ...DEFAULT_CONFIG };\n }\n\n try {\n const content = readFileSync(CONFIG_PATH, 'utf-8');\n const parsed = JSON.parse(content) as Partial<Config>;\n return {\n plugins: parsed.plugins || {},\n defaultPlugin: parsed.defaultPlugin,\n tier: parsed.tier || 'free',\n history: parsed.history || DEFAULT_CONFIG.history,\n };\n } catch {\n return { ...DEFAULT_CONFIG };\n }\n}\n\n/**\n * Save config to file\n */\nexport function saveConfig(config: Config): void {\n ensureConfigDir();\n const content = JSON.stringify(config, null, 2);\n writeFileSync(CONFIG_PATH, content, 'utf-8');\n}\n\n/**\n * Get a plugin by name (or default if not specified)\n */\nexport function getPlugin(name?: string): { name: string; plugin: Plugin } | null {\n const config = loadConfig();\n \n // If name specified, use that\n if (name) {\n const plugin = config.plugins[name];\n if (plugin) {\n return { name, plugin };\n }\n return null;\n }\n\n // Use default plugin\n const defaultName = config.defaultPlugin;\n if (defaultName && config.plugins[defaultName]) {\n return { name: defaultName, plugin: config.plugins[defaultName] };\n }\n\n // If only one plugin, use it\n const pluginNames = Object.keys(config.plugins);\n if (pluginNames.length === 1) {\n const name = pluginNames[0];\n return { name, plugin: config.plugins[name] };\n }\n\n return null;\n}\n\n/**\n * Add or update a plugin\n */\nexport function setPlugin(name: string, url: string, description?: string): void {\n const config = loadConfig();\n config.plugins[name] = { url, description };\n \n // If no default and this is the first plugin, make it default\n if (!config.defaultPlugin || Object.keys(config.plugins).length === 1) {\n config.defaultPlugin = name;\n }\n \n saveConfig(config);\n}\n\n/**\n * Remove a plugin\n */\nexport function removePlugin(name: string): boolean {\n const config = loadConfig();\n \n if (!config.plugins[name]) {\n return false;\n }\n \n delete config.plugins[name];\n \n // If we removed the default, pick a new one\n if (config.defaultPlugin === name) {\n const remaining = Object.keys(config.plugins);\n config.defaultPlugin = remaining.length > 0 ? remaining[0] : undefined;\n }\n \n saveConfig(config);\n return true;\n}\n\n/**\n * Set the default plugin\n */\nexport function setDefaultPlugin(name: string): boolean {\n const config = loadConfig();\n \n if (!config.plugins[name]) {\n return false;\n }\n \n config.defaultPlugin = name;\n saveConfig(config);\n return true;\n}\n\n/**\n * List all plugins\n */\nexport function listPlugins(): Array<{ name: string; plugin: Plugin; isDefault: boolean }> {\n const config = loadConfig();\n return Object.entries(config.plugins).map(([name, plugin]) => ({\n name,\n plugin,\n isDefault: name === config.defaultPlugin,\n }));\n}\n\n/**\n * Get global tier setting\n */\nexport function getTier(): WebhookTier {\n const config = loadConfig();\n return config.tier || 'free';\n}\n\n/**\n * Set global tier setting\n */\nexport function setTier(tier: WebhookTier): void {\n const config = loadConfig();\n config.tier = tier;\n saveConfig(config);\n}\n\n/**\n * Get webhook URL from environment, plugin name, or default\n */\nexport function getWebhookUrl(pluginName?: string): { url: string; name: string } | null {\n // Environment variable takes highest precedence\n const envUrl = process.env.TRMNL_WEBHOOK;\n if (envUrl) {\n return { url: envUrl, name: '$TRMNL_WEBHOOK' };\n }\n\n // Try to get plugin\n const result = getPlugin(pluginName);\n if (result) {\n return {\n url: result.plugin.url,\n name: result.name,\n };\n }\n\n return null;\n}\n\n/**\n * Get history config\n */\nexport function getHistoryConfig(): { path: string; maxSizeMb: number } {\n const config = loadConfig();\n const historyPath = config.history?.path || DEFAULT_CONFIG.history!.path!;\n const expandedPath = historyPath.startsWith('~') \n ? historyPath.replace('~', homedir())\n : historyPath;\n \n return {\n path: expandedPath,\n maxSizeMb: config.history?.maxSizeMb || DEFAULT_CONFIG.history!.maxSizeMb!,\n };\n}\n","/**\n * TRMNL CLI Types\n */\n\n/** Webhook tier determines payload size limits */\nexport type WebhookTier = 'free' | 'plus';\n\n/** Size limits per tier in bytes */\nexport const TIER_LIMITS: Record<WebhookTier, number> = {\n free: 2048, // 2 KB\n plus: 5120, // 5 KB\n};\n\n/** Plugin configuration */\nexport interface Plugin {\n url: string;\n description?: string;\n}\n\n/** CLI configuration stored in ~/.trmnl/config.json */\nexport interface Config {\n plugins: Record<string, Plugin>;\n defaultPlugin?: string;\n tier?: WebhookTier; // Global tier setting\n history?: {\n path?: string;\n maxSizeMb?: number;\n };\n}\n\n/** Default config values */\nexport const DEFAULT_CONFIG: Config = {\n plugins: {},\n defaultPlugin: undefined,\n tier: 'free',\n history: {\n path: '~/.trmnl/history.jsonl',\n maxSizeMb: 100,\n },\n};\n\n/** Merge variables payload structure */\nexport interface MergeVariables {\n content?: string;\n title?: string;\n text?: string;\n image?: string;\n [key: string]: string | undefined;\n}\n\n/** Webhook request payload */\nexport interface WebhookPayload {\n merge_variables: MergeVariables;\n}\n\n/** History entry stored in JSONL */\nexport interface HistoryEntry {\n timestamp: string;\n plugin: string;\n size_bytes: number;\n tier: WebhookTier;\n payload: WebhookPayload;\n success: boolean;\n status_code?: number;\n response?: string;\n error?: string;\n duration_ms: number;\n}\n\n/** Validation result */\nexport interface ValidationResult {\n valid: boolean;\n size_bytes: number;\n tier: WebhookTier;\n limit_bytes: number;\n remaining_bytes: number;\n percent_used: number;\n warnings: string[];\n errors: string[];\n}\n","/**\n * JSONL history logger for tracking sent payloads\n */\n\nimport { appendFileSync, existsSync, readFileSync, statSync } from 'node:fs';\nimport { ensureConfigDir, getHistoryConfig } from './config.ts';\nimport type { HistoryEntry } from '../types.ts';\n\n/**\n * Append a history entry to the JSONL file\n */\nexport function logEntry(entry: HistoryEntry): void {\n ensureConfigDir();\n const { path } = getHistoryConfig();\n const line = JSON.stringify(entry) + '\\n';\n appendFileSync(path, line, 'utf-8');\n}\n\n/**\n * Read all history entries\n */\nexport function readHistory(): HistoryEntry[] {\n const { path } = getHistoryConfig();\n \n if (!existsSync(path)) {\n return [];\n }\n\n try {\n const content = readFileSync(path, 'utf-8');\n const lines = content.trim().split('\\n').filter(Boolean);\n return lines.map(line => JSON.parse(line) as HistoryEntry);\n } catch {\n return [];\n }\n}\n\n/**\n * Get history entries with filters\n */\nexport interface HistoryFilter {\n last?: number;\n today?: boolean;\n failed?: boolean;\n success?: boolean;\n plugin?: string;\n since?: Date;\n until?: Date;\n}\n\nexport function getHistory(filter: HistoryFilter = {}): HistoryEntry[] {\n let entries = readHistory();\n\n // Filter by plugin\n if (filter.plugin) {\n entries = entries.filter(e => e.plugin === filter.plugin);\n }\n\n // Filter by success/failed\n if (filter.failed) {\n entries = entries.filter(e => !e.success);\n }\n if (filter.success) {\n entries = entries.filter(e => e.success);\n }\n\n // Filter by date\n if (filter.today) {\n const today = new Date();\n today.setHours(0, 0, 0, 0);\n entries = entries.filter(e => new Date(e.timestamp) >= today);\n }\n if (filter.since) {\n entries = entries.filter(e => new Date(e.timestamp) >= filter.since!);\n }\n if (filter.until) {\n entries = entries.filter(e => new Date(e.timestamp) <= filter.until!);\n }\n\n // Sort by timestamp descending (most recent first)\n entries.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());\n\n // Limit results\n if (filter.last) {\n entries = entries.slice(0, filter.last);\n }\n\n return entries;\n}\n\n/**\n * Format a history entry for display\n */\nexport function formatEntry(entry: HistoryEntry, verbose = false): string {\n const time = new Date(entry.timestamp).toLocaleString();\n const status = entry.success ? '✓' : '✗';\n const sizeKb = (entry.size_bytes / 1024).toFixed(2);\n \n let line = `${status} ${time} | ${entry.plugin} | ${sizeKb} KB | ${entry.duration_ms}ms`;\n \n if (!entry.success && entry.error) {\n line += ` | ${entry.error}`;\n }\n \n if (verbose && entry.payload?.merge_variables?.content) {\n const preview = entry.payload.merge_variables.content.substring(0, 80);\n line += `\\n ${preview}${entry.payload.merge_variables.content.length > 80 ? '...' : ''}`;\n }\n \n return line;\n}\n\n/**\n * Get history file stats\n */\nexport function getHistoryStats(): { entries: number; sizeBytes: number; sizeMb: number } | null {\n const { path } = getHistoryConfig();\n \n if (!existsSync(path)) {\n return null;\n }\n\n try {\n const stats = statSync(path);\n const entries = readHistory().length;\n return {\n entries,\n sizeBytes: stats.size,\n sizeMb: Math.round((stats.size / 1024 / 1024) * 100) / 100,\n };\n } catch {\n return null;\n }\n}\n\n/**\n * Get history file path\n */\nexport function getHistoryPath(): string {\n return getHistoryConfig().path;\n}\n","/**\n * trmnl config - Manage CLI configuration\n * trmnl plugin - Manage webhook plugins\n */\n\nimport type { CAC } from 'cac';\nimport {\n CONFIG_PATH,\n getTier,\n listPlugins,\n loadConfig,\n removePlugin,\n setDefaultPlugin,\n setPlugin,\n setTier,\n} from '../lib/config.ts';\nimport { getHistoryPath } from '../lib/logger.ts';\nimport type { WebhookTier } from '../types.ts';\n\nexport function registerConfigCommand(cli: CAC): void {\n // Config show\n cli\n .command('config', 'Show configuration')\n .action(() => {\n const config = loadConfig();\n const plugins = listPlugins();\n\n console.log(`Config file: ${CONFIG_PATH}`);\n console.log('');\n\n console.log('Plugins:');\n if (plugins.length === 0) {\n console.log(' (none configured)');\n console.log('');\n console.log(' Add a plugin:');\n console.log(' trmnl plugin add <name> <url>');\n } else {\n for (const { name, plugin, isDefault } of plugins) {\n const defaultMark = isDefault ? ' (default)' : '';\n console.log(` ${name}${defaultMark}`);\n console.log(` url: ${plugin.url}`);\n if (plugin.description) {\n console.log(` desc: ${plugin.description}`);\n }\n }\n }\n\n console.log('');\n console.log(`Tier: ${config.tier || 'free'}`);\n console.log(` Limit: ${config.tier === 'plus' ? '5 KB' : '2 KB'}`);\n\n console.log('');\n console.log('History:');\n console.log(` path: ${getHistoryPath()}`);\n\n console.log('');\n console.log('Environment:');\n console.log(` TRMNL_WEBHOOK: ${process.env.TRMNL_WEBHOOK || '(not set)'}`);\n });\n\n // Tier command (separate from config)\n cli\n .command('tier [value]', 'Get or set tier (free or plus)')\n .example('trmnl tier # Show current tier')\n .example('trmnl tier plus # Set to plus')\n .example('trmnl tier free # Set to free')\n .action((value?: string) => {\n if (!value) {\n const tier = getTier();\n console.log(`Tier: ${tier}`);\n console.log(`Limit: ${tier === 'plus' ? '5 KB (5,120 bytes)' : '2 KB (2,048 bytes)'}`);\n return;\n }\n\n if (value !== 'free' && value !== 'plus') {\n console.error('Invalid tier. Use \"free\" or \"plus\".');\n process.exit(1);\n }\n\n setTier(value as WebhookTier);\n console.log(`✓ Tier set to: ${value}`);\n });\n\n // Plugin command with action as first arg\n cli\n .command('plugin [action] [name] [url]', 'Manage webhook plugins')\n .option('-d, --desc <description>', 'Plugin description')\n .option('-u, --url <url>', 'Webhook URL (for set action)')\n .option('--default', 'Set as default plugin')\n .example('trmnl plugin # List plugins')\n .example('trmnl plugin add home <url> # Add plugin')\n .example('trmnl plugin rm home # Remove plugin')\n .example('trmnl plugin default home # Set default')\n .example('trmnl plugin set home --url ... # Update plugin')\n .action((action?: string, name?: string, url?: string, options?: { desc?: string; url?: string; default?: boolean }) => {\n // No action = list\n if (!action) {\n showPluginList();\n return;\n }\n\n // Handle actions\n switch (action) {\n case 'add':\n if (!name || !url) {\n console.error('Usage: trmnl plugin add <name> <url>');\n process.exit(1);\n }\n setPlugin(name, url, options?.desc);\n console.log(`✓ Added plugin: ${name}`);\n if (options?.default) {\n setDefaultPlugin(name);\n console.log(`✓ Set as default`);\n }\n break;\n\n case 'rm':\n case 'remove':\n if (!name) {\n console.error('Usage: trmnl plugin rm <name>');\n process.exit(1);\n }\n if (removePlugin(name)) {\n console.log(`✓ Removed plugin: ${name}`);\n } else {\n console.error(`Plugin not found: ${name}`);\n process.exit(1);\n }\n break;\n\n case 'default':\n if (!name) {\n console.error('Usage: trmnl plugin default <name>');\n process.exit(1);\n }\n if (setDefaultPlugin(name)) {\n console.log(`✓ Default plugin: ${name}`);\n } else {\n console.error(`Plugin not found: ${name}`);\n process.exit(1);\n }\n break;\n\n case 'set':\n case 'update':\n if (!name) {\n console.error('Usage: trmnl plugin set <name> [options]');\n process.exit(1);\n }\n const plugins = listPlugins();\n const existing = plugins.find(p => p.name === name);\n if (!existing) {\n console.error(`Plugin not found: ${name}`);\n process.exit(1);\n }\n const newUrl = options?.url || existing.plugin.url;\n const newDesc = options?.desc !== undefined ? options.desc : existing.plugin.description;\n setPlugin(name, newUrl, newDesc);\n console.log(`✓ Updated plugin: ${name}`);\n break;\n\n case 'list':\n showPluginList();\n break;\n\n default:\n console.error(`Unknown action: ${action}`);\n console.log('');\n console.log('Available actions:');\n console.log(' add <name> <url> - Add a plugin');\n console.log(' rm <name> - Remove a plugin');\n console.log(' default <name> - Set default plugin');\n console.log(' set <name> - Update a plugin');\n console.log(' list - List all plugins');\n process.exit(1);\n }\n });\n\n // Plugins alias for list\n cli\n .command('plugins', 'List all plugins')\n .action(() => {\n showPluginList();\n });\n}\n\nfunction showPluginList(): void {\n const plugins = listPlugins();\n\n if (plugins.length === 0) {\n console.log('No plugins configured.');\n console.log('');\n console.log('Add a plugin:');\n console.log(' trmnl plugin add <name> <url>');\n return;\n }\n\n console.log('Plugins:');\n for (const { name, plugin, isDefault } of plugins) {\n const defaultMark = isDefault ? ' ★' : '';\n console.log(` ${name}${defaultMark}`);\n console.log(` ${plugin.url}`);\n if (plugin.description) {\n console.log(` ${plugin.description}`);\n }\n }\n\n console.log('');\n console.log('★ = default plugin');\n}\n","/**\n * trmnl history - View send history\n */\n\nimport { unlinkSync } from 'node:fs';\nimport type { CAC } from 'cac';\nimport { formatEntry, getHistory, getHistoryPath, getHistoryStats, type HistoryFilter } from '../lib/logger.ts';\n\ninterface HistoryOptions {\n last?: number;\n today?: boolean;\n failed?: boolean;\n success?: boolean;\n plugin?: string;\n json?: boolean;\n verbose?: boolean;\n}\n\nexport function registerHistoryCommand(cli: CAC): void {\n cli\n .command('history', 'View send history')\n .option('-n, --last <n>', 'Show last N entries', { default: 10 })\n .option('--today', 'Show only today\\'s entries')\n .option('--failed', 'Show only failed sends')\n .option('--success', 'Show only successful sends')\n .option('-p, --plugin <name>', 'Filter by plugin name')\n .option('--json', 'Output as JSON')\n .option('-v, --verbose', 'Show content preview')\n .example('trmnl history')\n .example('trmnl history --last 20')\n .example('trmnl history --today --failed')\n .example('trmnl history --plugin office')\n .action((options: HistoryOptions) => {\n const filter: HistoryFilter = {\n last: options.last,\n today: options.today,\n failed: options.failed,\n success: options.success,\n plugin: options.plugin,\n };\n\n const entries = getHistory(filter);\n\n if (options.json) {\n console.log(JSON.stringify(entries, null, 2));\n return;\n }\n\n if (entries.length === 0) {\n console.log('No history entries found.');\n console.log(`History file: ${getHistoryPath()}`);\n return;\n }\n\n // Stats header\n const stats = getHistoryStats();\n if (stats) {\n console.log(`History: ${stats.entries} total entries (${stats.sizeMb} MB)`);\n console.log('');\n }\n\n // Filter description\n const filterParts: string[] = [];\n if (options.today) filterParts.push('today');\n if (options.failed) filterParts.push('failed');\n if (options.success) filterParts.push('success');\n if (options.plugin) filterParts.push(`plugin: ${options.plugin}`);\n if (filterParts.length > 0) {\n console.log(`Filter: ${filterParts.join(', ')}`);\n console.log('');\n }\n\n // Entries\n console.log(`Showing ${entries.length} entries (most recent first):`);\n console.log('');\n\n for (const entry of entries) {\n console.log(formatEntry(entry, options.verbose));\n }\n });\n\n // History clear\n cli\n .command('history clear', 'Clear send history')\n .option('--confirm', 'Confirm deletion')\n .action((options: { confirm?: boolean }) => {\n const historyPath = getHistoryPath();\n \n if (!options.confirm) {\n console.log('This will delete all history. Use --confirm to proceed.');\n console.log(`History file: ${historyPath}`);\n return;\n }\n\n try {\n unlinkSync(historyPath);\n console.log('✓ History cleared');\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === 'ENOENT') {\n console.log('History file does not exist.');\n } else {\n console.error('Error clearing history:', err);\n }\n }\n });\n\n // History stats\n cli\n .command('history stats', 'Show history statistics')\n .action(() => {\n const stats = getHistoryStats();\n \n if (!stats) {\n console.log('No history file found.');\n return;\n }\n\n const entries = getHistory({});\n const successCount = entries.filter(e => e.success).length;\n const failedCount = entries.filter(e => !e.success).length;\n const totalBytes = entries.reduce((sum, e) => sum + e.size_bytes, 0);\n const avgBytes = entries.length > 0 ? Math.round(totalBytes / entries.length) : 0;\n const avgDuration = entries.length > 0 \n ? Math.round(entries.reduce((sum, e) => sum + e.duration_ms, 0) / entries.length) \n : 0;\n\n // Plugin breakdown\n const byPlugin = new Map<string, number>();\n for (const entry of entries) {\n byPlugin.set(entry.plugin, (byPlugin.get(entry.plugin) || 0) + 1);\n }\n\n console.log('History Statistics');\n console.log('');\n console.log(`File: ${getHistoryPath()}`);\n console.log(`Size: ${stats.sizeMb} MB`);\n console.log('');\n console.log(`Total: ${entries.length} sends`);\n console.log(`Success: ${successCount} (${entries.length > 0 ? Math.round(successCount / entries.length * 100) : 0}%)`);\n console.log(`Failed: ${failedCount} (${entries.length > 0 ? Math.round(failedCount / entries.length * 100) : 0}%)`);\n console.log('');\n console.log(`Avg size: ${avgBytes} bytes`);\n console.log(`Avg duration: ${avgDuration}ms`);\n\n if (byPlugin.size > 1) {\n console.log('');\n console.log('By plugin:');\n for (const [plugin, count] of byPlugin.entries()) {\n console.log(` ${plugin}: ${count} sends`);\n }\n }\n\n // Recent activity\n const today = getHistory({ today: true });\n const thisWeek = getHistory({ since: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000) });\n console.log('');\n console.log(`Today: ${today.length} sends`);\n console.log(`This week: ${thisWeek.length} sends`);\n });\n}\n","/**\n * trmnl send - Send content to TRMNL display\n */\n\nimport { readFileSync } from 'node:fs';\nimport type { CAC } from 'cac';\nimport { createPayload, formatValidation } from '../lib/validator.ts';\nimport { sendToWebhook } from '../lib/webhook.ts';\n\ninterface SendOptions {\n content?: string;\n file?: string;\n plugin?: string;\n webhook?: string;\n skipValidation?: boolean;\n skipLog?: boolean;\n noMinify?: boolean;\n json?: boolean;\n}\n\nexport function registerSendCommand(cli: CAC): void {\n cli\n .command('send', 'Send content to TRMNL display')\n .option('-c, --content <html>', 'HTML content to send')\n .option('-f, --file <path>', 'Read content from file')\n .option('-p, --plugin <name>', 'Plugin to use (default: default plugin)')\n .option('-w, --webhook <url>', 'Override webhook URL directly')\n .option('--skip-validation', 'Skip payload validation')\n .option('--skip-log', 'Skip history logging')\n .option('--no-minify', 'Disable HTML minification (enabled by default)')\n .option('--json', 'Output result as JSON')\n .example('trmnl send --content \"<div class=\\\\\"layout\\\\\">Hello</div>\"')\n .example('trmnl send --file ./output.html')\n .example('trmnl send --file ./output.html --plugin office')\n .example('echo \\'{\"merge_variables\":{\"content\":\"...\"}}\\' | trmnl send')\n .action(async (options: SendOptions) => {\n let content: string;\n\n // Get content from options, file, or stdin\n if (options.content) {\n content = options.content;\n } else if (options.file) {\n try {\n content = readFileSync(options.file, 'utf-8');\n } catch (err) {\n console.error(`Error reading file: ${options.file}`);\n process.exit(1);\n }\n } else {\n // Try reading from stdin\n content = await readStdin();\n if (!content) {\n console.error('No content provided. Use --content, --file, or pipe content via stdin.');\n process.exit(1);\n }\n }\n\n // Create payload (minified by default to maximize usable payload space)\n const payload = createPayload(content, { minify: !options.noMinify });\n\n // Send\n const result = await sendToWebhook(payload, {\n plugin: options.plugin,\n webhookUrl: options.webhook,\n skipValidation: options.skipValidation,\n skipLog: options.skipLog,\n });\n\n // Output\n if (options.json) {\n console.log(JSON.stringify(result, null, 2));\n } else {\n if (result.success) {\n console.log(`✓ Sent to TRMNL (${result.pluginName})`);\n console.log(` Status: ${result.statusCode}`);\n console.log(` Time: ${result.durationMs}ms`);\n console.log(` Size: ${result.validation.size_bytes} bytes (${result.validation.percent_used}% of limit)`);\n } else {\n console.error('✗ Failed to send');\n console.error(` Error: ${result.error}`);\n if (result.pluginName) {\n console.error(` Plugin: ${result.pluginName}`);\n }\n console.log('');\n console.log('Validation:');\n console.log(formatValidation(result.validation));\n }\n }\n\n process.exit(result.success ? 0 : 1);\n });\n}\n\n/**\n * Read content from stdin (non-blocking check)\n */\nasync function readStdin(): Promise<string> {\n // Check if stdin has data (not a TTY)\n if (process.stdin.isTTY) {\n return '';\n }\n\n const chunks: Buffer[] = [];\n \n return new Promise((resolve) => {\n process.stdin.on('data', (chunk) => chunks.push(chunk));\n process.stdin.on('end', () => resolve(Buffer.concat(chunks).toString('utf-8').trim()));\n process.stdin.on('error', () => resolve(''));\n \n // Timeout to avoid hanging\n setTimeout(() => {\n if (chunks.length === 0) {\n resolve('');\n }\n }, 100);\n });\n}\n","/**\n * Payload validation for TRMNL webhooks\n */\n\nimport { TIER_LIMITS, type ValidationResult, type WebhookPayload, type WebhookTier } from '../types.ts';\n\n/**\n * Validate a webhook payload against size limits\n */\nexport function validatePayload(payload: WebhookPayload, tier: WebhookTier = 'free'): ValidationResult {\n const jsonString = JSON.stringify(payload);\n const sizeBytes = new TextEncoder().encode(jsonString).length;\n const limitBytes = TIER_LIMITS[tier];\n const remainingBytes = limitBytes - sizeBytes;\n const percentUsed = Math.round((sizeBytes / limitBytes) * 1000) / 10;\n\n const warnings: string[] = [];\n const errors: string[] = [];\n\n // Size check\n if (sizeBytes > limitBytes) {\n errors.push(`Payload exceeds ${tier} tier limit: ${sizeBytes} bytes > ${limitBytes} bytes`);\n } else if (percentUsed > 90) {\n warnings.push(`Payload is at ${percentUsed}% of ${tier} tier limit`);\n }\n\n // Content check\n if (!payload.merge_variables) {\n errors.push('Missing merge_variables object');\n } else if (!payload.merge_variables.content && !payload.merge_variables.text) {\n warnings.push('No content or text field in merge_variables');\n }\n\n // HTML sanity checks\n const content = payload.merge_variables?.content || '';\n if (content) {\n // Check for unclosed tags (basic check)\n const openDivs = (content.match(/<div/g) || []).length;\n const closeDivs = (content.match(/<\\/div>/g) || []).length;\n if (openDivs !== closeDivs) {\n warnings.push(`Potential unclosed divs: ${openDivs} open, ${closeDivs} close`);\n }\n\n // Check for common TRMNL patterns - look for 'layout' as a class (may have other classes too)\n const hasLayoutClass = /class=[\"'][^\"']*\\blayout\\b[^\"']*[\"']/.test(content);\n if (!hasLayoutClass) {\n warnings.push('Missing .layout class - TRMNL requires a root layout element');\n }\n }\n\n return {\n valid: errors.length === 0,\n size_bytes: sizeBytes,\n tier,\n limit_bytes: limitBytes,\n remaining_bytes: remainingBytes,\n percent_used: percentUsed,\n warnings,\n errors,\n };\n}\n\n/**\n * Minify HTML content to reduce payload size.\n *\n * Strips whitespace that has no effect on rendered output:\n * - HTML comments\n * - Whitespace between tags (> ... <)\n * - Leading/trailing whitespace\n * - Runs of whitespace collapsed to a single space\n */\nexport function minifyHtml(html: string): string {\n return html\n // Remove HTML comments\n .replace(/<!--[\\s\\S]*?-->/g, '')\n // Collapse whitespace between tags\n .replace(/>\\s+</g, '><')\n // Collapse remaining runs of whitespace to a single space\n .replace(/\\s{2,}/g, ' ')\n .trim();\n}\n\n/**\n * Minify all HTML string values in a merge_variables object.\n * Only minifies values that look like HTML (contain '<' and '>').\n */\nfunction minifyMergeVariables(vars: Record<string, string | undefined>): Record<string, string | undefined> {\n const result: Record<string, string | undefined> = {};\n for (const [key, value] of Object.entries(vars)) {\n if (typeof value === 'string' && value.includes('<') && value.includes('>')) {\n result[key] = minifyHtml(value);\n } else {\n result[key] = value;\n }\n }\n return result;\n}\n\n/**\n * Parse content into a webhook payload\n */\nexport function createPayload(content: string, options: { minify?: boolean } = {}): WebhookPayload {\n const shouldMinify = options.minify !== false; // default: true\n\n // Try to parse as JSON first\n try {\n const parsed = JSON.parse(content);\n const payload: WebhookPayload = parsed.merge_variables\n ? (parsed as WebhookPayload)\n : { merge_variables: parsed };\n\n if (shouldMinify) {\n payload.merge_variables = minifyMergeVariables(payload.merge_variables) as WebhookPayload['merge_variables'];\n }\n return payload;\n } catch {\n // Treat as raw HTML content\n return {\n merge_variables: {\n content: shouldMinify ? minifyHtml(content) : content,\n },\n };\n }\n}\n\n/**\n * Format validation result for display\n */\nexport function formatValidation(result: ValidationResult): string {\n const lines: string[] = [];\n \n const status = result.valid ? '✓' : '✗';\n const sizeKb = (result.size_bytes / 1024).toFixed(2);\n const limitKb = (result.limit_bytes / 1024).toFixed(2);\n \n lines.push(`${status} Payload: ${result.size_bytes} bytes (${sizeKb} KB)`);\n lines.push(` Tier: ${result.tier} (limit: ${limitKb} KB)`);\n lines.push(` Used: ${result.percent_used}% (${result.remaining_bytes} bytes remaining)`);\n \n if (result.errors.length > 0) {\n lines.push('');\n lines.push('Errors:');\n for (const error of result.errors) {\n lines.push(` ✗ ${error}`);\n }\n }\n \n if (result.warnings.length > 0) {\n lines.push('');\n lines.push('Warnings:');\n for (const warning of result.warnings) {\n lines.push(` ⚠ ${warning}`);\n }\n }\n \n return lines.join('\\n');\n}\n","/**\n * Webhook sending logic\n */\n\nimport { getTier, getWebhookUrl } from './config.ts';\nimport { logEntry } from './logger.ts';\nimport { validatePayload } from './validator.ts';\nimport type { HistoryEntry, WebhookPayload, WebhookTier } from '../types.ts';\n\nexport interface SendResult {\n success: boolean;\n pluginName: string;\n statusCode?: number;\n response?: string;\n error?: string;\n durationMs: number;\n validation: ReturnType<typeof validatePayload>;\n}\n\nexport interface SendOptions {\n plugin?: string; // Plugin name to use\n webhookUrl?: string; // Direct URL override\n skipValidation?: boolean;\n skipLog?: boolean;\n}\n\n/**\n * Send payload to TRMNL webhook\n */\nexport async function sendToWebhook(\n payload: WebhookPayload,\n options: SendOptions = {}\n): Promise<SendResult> {\n const startTime = Date.now();\n const tier = getTier();\n \n // Resolve webhook URL\n let webhookUrl: string;\n let pluginName: string;\n\n if (options.webhookUrl) {\n // Direct URL override\n webhookUrl = options.webhookUrl;\n pluginName = 'custom';\n } else {\n // Get from config/env\n const resolved = getWebhookUrl(options.plugin);\n if (!resolved) {\n const durationMs = Date.now() - startTime;\n const validation = validatePayload(payload, tier);\n \n let error = 'No webhook URL configured.';\n if (options.plugin) {\n error = `Plugin \"${options.plugin}\" not found.`;\n } else {\n error += ' Add a plugin with: trmnl plugin add <name> <url>';\n }\n \n return {\n success: false,\n pluginName: options.plugin || 'unknown',\n error,\n durationMs,\n validation,\n };\n }\n webhookUrl = resolved.url;\n pluginName = resolved.name;\n }\n\n // Validate payload\n const validation = validatePayload(payload, tier);\n \n if (!options.skipValidation && !validation.valid) {\n const durationMs = Date.now() - startTime;\n const result: SendResult = {\n success: false,\n pluginName,\n error: validation.errors.join('; '),\n durationMs,\n validation,\n };\n \n // Log failed validation\n if (!options.skipLog) {\n logEntry(createHistoryEntry(payload, result, tier, pluginName));\n }\n \n return result;\n }\n \n // Send request\n try {\n const response = await fetch(webhookUrl, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(payload),\n });\n \n const durationMs = Date.now() - startTime;\n const responseText = await response.text();\n \n const result: SendResult = {\n success: response.ok,\n pluginName,\n statusCode: response.status,\n response: responseText,\n durationMs,\n validation,\n };\n \n if (!response.ok) {\n result.error = `HTTP ${response.status}: ${responseText}`;\n }\n \n // Log the send\n if (!options.skipLog) {\n logEntry(createHistoryEntry(payload, result, tier, pluginName));\n }\n \n return result;\n \n } catch (err) {\n const durationMs = Date.now() - startTime;\n const error = err instanceof Error ? err.message : 'Unknown error';\n \n const result: SendResult = {\n success: false,\n pluginName,\n error,\n durationMs,\n validation,\n };\n \n // Log the error\n if (!options.skipLog) {\n logEntry(createHistoryEntry(payload, result, tier, pluginName));\n }\n \n return result;\n }\n}\n\n/**\n * Create a history entry from send result\n */\nfunction createHistoryEntry(\n payload: WebhookPayload,\n result: SendResult,\n tier: WebhookTier,\n pluginName: string\n): HistoryEntry {\n return {\n timestamp: new Date().toISOString(),\n plugin: pluginName,\n size_bytes: result.validation.size_bytes,\n tier,\n payload,\n success: result.success,\n status_code: result.statusCode,\n response: result.response,\n error: result.error,\n duration_ms: result.durationMs,\n };\n}\n","/**\n * trmnl validate - Validate payload without sending\n */\n\nimport { readFileSync } from 'node:fs';\nimport type { CAC } from 'cac';\nimport { getTier } from '../lib/config.ts';\nimport { createPayload, formatValidation, validatePayload } from '../lib/validator.ts';\nimport type { WebhookTier } from '../types.ts';\n\ninterface ValidateOptions {\n content?: string;\n file?: string;\n tier?: WebhookTier;\n noMinify?: boolean;\n json?: boolean;\n}\n\nexport function registerValidateCommand(cli: CAC): void {\n cli\n .command('validate', 'Validate payload without sending')\n .option('-c, --content <html>', 'HTML content to validate')\n .option('-f, --file <path>', 'Read content from file')\n .option('-t, --tier <tier>', 'Override tier (free or plus)')\n .option('--no-minify', 'Disable HTML minification (enabled by default)')\n .option('--json', 'Output result as JSON')\n .example('trmnl validate --file ./output.html')\n .example('trmnl validate --content \"<div>...</div>\" --tier plus')\n .action(async (options: ValidateOptions) => {\n let content: string;\n\n // Get content from options, file, or stdin\n if (options.content) {\n content = options.content;\n } else if (options.file) {\n try {\n content = readFileSync(options.file, 'utf-8');\n } catch (err) {\n console.error(`Error reading file: ${options.file}`);\n process.exit(1);\n }\n } else {\n // Try reading from stdin\n content = await readStdin();\n if (!content) {\n console.error('No content provided. Use --content, --file, or pipe content via stdin.');\n process.exit(1);\n }\n }\n\n // Use explicit tier or global config\n const tier = options.tier || getTier();\n\n // Create payload (minified by default to maximize usable payload space)\n const payload = createPayload(content, { minify: !options.noMinify });\n const result = validatePayload(payload, tier);\n\n // Output\n if (options.json) {\n console.log(JSON.stringify(result, null, 2));\n } else {\n console.log(formatValidation(result));\n }\n\n process.exit(result.valid ? 0 : 1);\n });\n}\n\n/**\n * Read content from stdin (non-blocking check)\n */\nasync function readStdin(): Promise<string> {\n if (process.stdin.isTTY) {\n return '';\n }\n\n const chunks: Buffer[] = [];\n \n return new Promise((resolve) => {\n process.stdin.on('data', (chunk) => chunks.push(chunk));\n process.stdin.on('end', () => resolve(Buffer.concat(chunks).toString('utf-8').trim()));\n process.stdin.on('error', () => resolve(''));\n \n setTimeout(() => {\n if (chunks.length === 0) {\n resolve('');\n }\n }, 100);\n });\n}\n"],"mappings":"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","names":["name","existsSync","readFileSync","existsSync","readFileSync","cli","unlinkSync","cli","unlinkSync","readFileSync","validation","cli","readFileSync","readFileSync","cli","readFileSync","readStdin","require"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "trmnl-cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "CLI tool for TRMNL e-ink displays - send, validate, and track payloads",
|
|
5
5
|
"author": "peetzweg",
|
|
6
6
|
"license": "MIT",
|
|
@@ -35,6 +35,8 @@
|
|
|
35
35
|
"dev": "tsx ./src/index.ts",
|
|
36
36
|
"build": "tsup",
|
|
37
37
|
"start": "node ./dist/index.js",
|
|
38
|
+
"test": "vitest run",
|
|
39
|
+
"test:watch": "vitest",
|
|
38
40
|
"typecheck": "tsc --noEmit",
|
|
39
41
|
"prepublishOnly": "pnpm run build"
|
|
40
42
|
},
|
|
@@ -45,7 +47,8 @@
|
|
|
45
47
|
"@types/node": "^22.0.0",
|
|
46
48
|
"tsup": "^8.0.0",
|
|
47
49
|
"tsx": "^4.0.0",
|
|
48
|
-
"typescript": "^5.0.0"
|
|
50
|
+
"typescript": "^5.0.0",
|
|
51
|
+
"vitest": "^4.0.18"
|
|
49
52
|
},
|
|
50
53
|
"engines": {
|
|
51
54
|
"node": ">=22.6.0"
|