trmnl-cli 0.1.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/README.md +195 -0
- package/bin/trmnl.mjs +3 -0
- package/package.json +49 -0
- package/src/commands/config.ts +210 -0
- package/src/commands/history.ts +160 -0
- package/src/commands/send.ts +115 -0
- package/src/commands/validate.ts +88 -0
- package/src/index.ts +38 -0
- package/src/lib/config.ts +257 -0
- package/src/lib/index.ts +8 -0
- package/src/lib/logger.ts +141 -0
- package/src/lib/validator.ts +116 -0
- package/src/lib/webhook.ts +167 -0
- package/src/types.ts +80 -0
package/README.md
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
# trmnl-cli
|
|
2
|
+
|
|
3
|
+
CLI tool for [TRMNL](https://usetrmnl.com) e-ink displays. Send, validate, and track payloads.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g trmnl-cli
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Requires Node.js 22.6.0 or later.
|
|
12
|
+
|
|
13
|
+
## Quick Start
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
# Add your first plugin (webhook)
|
|
17
|
+
trmnl plugin add home "https://trmnl.com/api/custom_plugins/YOUR_UUID"
|
|
18
|
+
|
|
19
|
+
# Send content
|
|
20
|
+
trmnl send --content '<div class="layout">Hello TRMNL!</div>'
|
|
21
|
+
|
|
22
|
+
# Or from a file
|
|
23
|
+
trmnl send --file ./output.html
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Plugins
|
|
27
|
+
|
|
28
|
+
Manage multiple TRMNL displays with named webhooks:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
# Add plugins
|
|
32
|
+
trmnl plugin add home "https://trmnl.com/api/custom_plugins/abc123"
|
|
33
|
+
trmnl plugin add office "https://trmnl.com/api/custom_plugins/xyz789"
|
|
34
|
+
|
|
35
|
+
# List plugins
|
|
36
|
+
trmnl plugin
|
|
37
|
+
|
|
38
|
+
# Set default
|
|
39
|
+
trmnl plugin default office
|
|
40
|
+
|
|
41
|
+
# Send to specific plugin
|
|
42
|
+
trmnl send --file output.html --plugin home
|
|
43
|
+
|
|
44
|
+
# Update plugin
|
|
45
|
+
trmnl plugin set home --url "https://new-url..."
|
|
46
|
+
|
|
47
|
+
# Remove plugin
|
|
48
|
+
trmnl plugin rm office
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Commands
|
|
52
|
+
|
|
53
|
+
### `trmnl send`
|
|
54
|
+
|
|
55
|
+
Send content to your TRMNL display.
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
# Direct content
|
|
59
|
+
trmnl send --content "<div class=\"layout\">Hello</div>"
|
|
60
|
+
|
|
61
|
+
# From file
|
|
62
|
+
trmnl send --file ./output.html
|
|
63
|
+
|
|
64
|
+
# To specific plugin
|
|
65
|
+
trmnl send --file ./output.html --plugin office
|
|
66
|
+
|
|
67
|
+
# From stdin (piped)
|
|
68
|
+
echo '{"merge_variables":{"content":"..."}}' | trmnl send
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Options:
|
|
72
|
+
- `-c, --content <html>` - HTML content to send
|
|
73
|
+
- `-f, --file <path>` - Read content from file
|
|
74
|
+
- `-p, --plugin <name>` - Plugin to use (default: default plugin)
|
|
75
|
+
- `-w, --webhook <url>` - Override webhook URL directly
|
|
76
|
+
- `--skip-validation` - Skip payload validation
|
|
77
|
+
- `--skip-log` - Don't log to history
|
|
78
|
+
- `--json` - Output result as JSON
|
|
79
|
+
|
|
80
|
+
### `trmnl validate`
|
|
81
|
+
|
|
82
|
+
Validate a payload without sending.
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
trmnl validate --file ./output.html
|
|
86
|
+
trmnl validate --content "..." --tier plus
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Options:
|
|
90
|
+
- `-c, --content <html>` - HTML content to validate
|
|
91
|
+
- `-f, --file <path>` - Read content from file
|
|
92
|
+
- `-t, --tier <tier>` - Override tier (`free` or `plus`)
|
|
93
|
+
- `--json` - Output result as JSON
|
|
94
|
+
|
|
95
|
+
### `trmnl plugin`
|
|
96
|
+
|
|
97
|
+
Manage webhook plugins.
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
# List plugins
|
|
101
|
+
trmnl plugin
|
|
102
|
+
|
|
103
|
+
# Add plugin
|
|
104
|
+
trmnl plugin add <name> <url>
|
|
105
|
+
trmnl plugin add home "https://..." --desc "Living room"
|
|
106
|
+
|
|
107
|
+
# Update plugin
|
|
108
|
+
trmnl plugin set <name> --url "https://..."
|
|
109
|
+
|
|
110
|
+
# Set default
|
|
111
|
+
trmnl plugin default <name>
|
|
112
|
+
|
|
113
|
+
# Remove plugin
|
|
114
|
+
trmnl plugin rm <name>
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### `trmnl config`
|
|
118
|
+
|
|
119
|
+
Show configuration.
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
trmnl config # Show all config
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### `trmnl tier`
|
|
126
|
+
|
|
127
|
+
Get or set the payload size tier.
|
|
128
|
+
|
|
129
|
+
```bash
|
|
130
|
+
trmnl tier # Show current tier
|
|
131
|
+
trmnl tier plus # Set tier to plus (5KB limit)
|
|
132
|
+
trmnl tier free # Set tier to free (2KB limit)
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
### `trmnl history`
|
|
136
|
+
|
|
137
|
+
View send history.
|
|
138
|
+
|
|
139
|
+
```bash
|
|
140
|
+
trmnl history # Last 10 sends
|
|
141
|
+
trmnl history --last 20 # Last N sends
|
|
142
|
+
trmnl history --today # Today's sends
|
|
143
|
+
trmnl history --failed # Failed sends only
|
|
144
|
+
trmnl history --plugin home # Filter by plugin
|
|
145
|
+
trmnl history stats # Statistics
|
|
146
|
+
trmnl history clear --confirm # Clear history
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
## Configuration
|
|
150
|
+
|
|
151
|
+
### Config File (`~/.trmnl/config.json`)
|
|
152
|
+
|
|
153
|
+
```json
|
|
154
|
+
{
|
|
155
|
+
"plugins": {
|
|
156
|
+
"home": {
|
|
157
|
+
"url": "https://trmnl.com/api/custom_plugins/...",
|
|
158
|
+
"description": "Living room display"
|
|
159
|
+
},
|
|
160
|
+
"office": {
|
|
161
|
+
"url": "https://trmnl.com/api/custom_plugins/..."
|
|
162
|
+
}
|
|
163
|
+
},
|
|
164
|
+
"defaultPlugin": "home",
|
|
165
|
+
"tier": "free"
|
|
166
|
+
}
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
### Environment Variables
|
|
170
|
+
|
|
171
|
+
- `TRMNL_WEBHOOK` - Webhook URL (overrides config, highest priority)
|
|
172
|
+
|
|
173
|
+
## Tier Limits
|
|
174
|
+
|
|
175
|
+
| Tier | Payload Limit | Rate Limit |
|
|
176
|
+
|------|---------------|------------|
|
|
177
|
+
| Free | 2 KB (2,048 bytes) | 12 requests/hour |
|
|
178
|
+
| Plus | 5 KB (5,120 bytes) | 30 requests/hour |
|
|
179
|
+
|
|
180
|
+
Set your tier globally:
|
|
181
|
+
```bash
|
|
182
|
+
trmnl tier plus
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
## History
|
|
186
|
+
|
|
187
|
+
Sends are logged to `~/.trmnl/history.jsonl`:
|
|
188
|
+
|
|
189
|
+
```jsonl
|
|
190
|
+
{"timestamp":"2026-02-07T10:00:00Z","plugin":"home","size_bytes":1234,"success":true,...}
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
## License
|
|
194
|
+
|
|
195
|
+
MIT
|
package/bin/trmnl.mjs
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "trmnl-cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "CLI tool for TRMNL e-ink displays - send, validate, and track payloads",
|
|
5
|
+
"author": "peetzweg",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "https://github.com/peetzweg/openclaw-trmnl"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://github.com/peetzweg/openclaw-trmnl#readme",
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/peetzweg/openclaw-trmnl/issues"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [
|
|
16
|
+
"trmnl",
|
|
17
|
+
"e-ink",
|
|
18
|
+
"cli",
|
|
19
|
+
"display",
|
|
20
|
+
"webhook",
|
|
21
|
+
"dashboard"
|
|
22
|
+
],
|
|
23
|
+
"type": "module",
|
|
24
|
+
"bin": {
|
|
25
|
+
"trmnl": "./bin/trmnl.mjs"
|
|
26
|
+
},
|
|
27
|
+
"exports": {
|
|
28
|
+
".": "./src/index.ts"
|
|
29
|
+
},
|
|
30
|
+
"files": [
|
|
31
|
+
"bin",
|
|
32
|
+
"src",
|
|
33
|
+
"README.md"
|
|
34
|
+
],
|
|
35
|
+
"scripts": {
|
|
36
|
+
"start": "node --experimental-strip-types ./src/index.ts",
|
|
37
|
+
"typecheck": "tsc --noEmit"
|
|
38
|
+
},
|
|
39
|
+
"dependencies": {
|
|
40
|
+
"cac": "^6.7.14"
|
|
41
|
+
},
|
|
42
|
+
"devDependencies": {
|
|
43
|
+
"@types/node": "^22.0.0",
|
|
44
|
+
"typescript": "^5.0.0"
|
|
45
|
+
},
|
|
46
|
+
"engines": {
|
|
47
|
+
"node": ">=22.6.0"
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* trmnl config - Manage CLI configuration
|
|
3
|
+
* trmnl plugin - Manage webhook plugins
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { CAC } from 'cac';
|
|
7
|
+
import {
|
|
8
|
+
CONFIG_PATH,
|
|
9
|
+
getTier,
|
|
10
|
+
listPlugins,
|
|
11
|
+
loadConfig,
|
|
12
|
+
removePlugin,
|
|
13
|
+
setDefaultPlugin,
|
|
14
|
+
setPlugin,
|
|
15
|
+
setTier,
|
|
16
|
+
} from '../lib/config.ts';
|
|
17
|
+
import { getHistoryPath } from '../lib/logger.ts';
|
|
18
|
+
import type { WebhookTier } from '../types.ts';
|
|
19
|
+
|
|
20
|
+
export function registerConfigCommand(cli: CAC): void {
|
|
21
|
+
// Config show
|
|
22
|
+
cli
|
|
23
|
+
.command('config', 'Show configuration')
|
|
24
|
+
.action(() => {
|
|
25
|
+
const config = loadConfig();
|
|
26
|
+
const plugins = listPlugins();
|
|
27
|
+
|
|
28
|
+
console.log(`Config file: ${CONFIG_PATH}`);
|
|
29
|
+
console.log('');
|
|
30
|
+
|
|
31
|
+
console.log('Plugins:');
|
|
32
|
+
if (plugins.length === 0) {
|
|
33
|
+
console.log(' (none configured)');
|
|
34
|
+
console.log('');
|
|
35
|
+
console.log(' Add a plugin:');
|
|
36
|
+
console.log(' trmnl plugin add <name> <url>');
|
|
37
|
+
} else {
|
|
38
|
+
for (const { name, plugin, isDefault } of plugins) {
|
|
39
|
+
const defaultMark = isDefault ? ' (default)' : '';
|
|
40
|
+
console.log(` ${name}${defaultMark}`);
|
|
41
|
+
console.log(` url: ${plugin.url}`);
|
|
42
|
+
if (plugin.description) {
|
|
43
|
+
console.log(` desc: ${plugin.description}`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
console.log('');
|
|
49
|
+
console.log(`Tier: ${config.tier || 'free'}`);
|
|
50
|
+
console.log(` Limit: ${config.tier === 'plus' ? '5 KB' : '2 KB'}`);
|
|
51
|
+
|
|
52
|
+
console.log('');
|
|
53
|
+
console.log('History:');
|
|
54
|
+
console.log(` path: ${getHistoryPath()}`);
|
|
55
|
+
|
|
56
|
+
console.log('');
|
|
57
|
+
console.log('Environment:');
|
|
58
|
+
console.log(` TRMNL_WEBHOOK: ${process.env.TRMNL_WEBHOOK || '(not set)'}`);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
// Tier command (separate from config)
|
|
62
|
+
cli
|
|
63
|
+
.command('tier [value]', 'Get or set tier (free or plus)')
|
|
64
|
+
.example('trmnl tier # Show current tier')
|
|
65
|
+
.example('trmnl tier plus # Set to plus')
|
|
66
|
+
.example('trmnl tier free # Set to free')
|
|
67
|
+
.action((value?: string) => {
|
|
68
|
+
if (!value) {
|
|
69
|
+
const tier = getTier();
|
|
70
|
+
console.log(`Tier: ${tier}`);
|
|
71
|
+
console.log(`Limit: ${tier === 'plus' ? '5 KB (5,120 bytes)' : '2 KB (2,048 bytes)'}`);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (value !== 'free' && value !== 'plus') {
|
|
76
|
+
console.error('Invalid tier. Use "free" or "plus".');
|
|
77
|
+
process.exit(1);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
setTier(value as WebhookTier);
|
|
81
|
+
console.log(`✓ Tier set to: ${value}`);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
// Plugin command with action as first arg
|
|
85
|
+
cli
|
|
86
|
+
.command('plugin [action] [name] [url]', 'Manage webhook plugins')
|
|
87
|
+
.option('-d, --desc <description>', 'Plugin description')
|
|
88
|
+
.option('-u, --url <url>', 'Webhook URL (for set action)')
|
|
89
|
+
.option('--default', 'Set as default plugin')
|
|
90
|
+
.example('trmnl plugin # List plugins')
|
|
91
|
+
.example('trmnl plugin add home <url> # Add plugin')
|
|
92
|
+
.example('trmnl plugin rm home # Remove plugin')
|
|
93
|
+
.example('trmnl plugin default home # Set default')
|
|
94
|
+
.example('trmnl plugin set home --url ... # Update plugin')
|
|
95
|
+
.action((action?: string, name?: string, url?: string, options?: { desc?: string; url?: string; default?: boolean }) => {
|
|
96
|
+
// No action = list
|
|
97
|
+
if (!action) {
|
|
98
|
+
showPluginList();
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Handle actions
|
|
103
|
+
switch (action) {
|
|
104
|
+
case 'add':
|
|
105
|
+
if (!name || !url) {
|
|
106
|
+
console.error('Usage: trmnl plugin add <name> <url>');
|
|
107
|
+
process.exit(1);
|
|
108
|
+
}
|
|
109
|
+
setPlugin(name, url, options?.desc);
|
|
110
|
+
console.log(`✓ Added plugin: ${name}`);
|
|
111
|
+
if (options?.default) {
|
|
112
|
+
setDefaultPlugin(name);
|
|
113
|
+
console.log(`✓ Set as default`);
|
|
114
|
+
}
|
|
115
|
+
break;
|
|
116
|
+
|
|
117
|
+
case 'rm':
|
|
118
|
+
case 'remove':
|
|
119
|
+
if (!name) {
|
|
120
|
+
console.error('Usage: trmnl plugin rm <name>');
|
|
121
|
+
process.exit(1);
|
|
122
|
+
}
|
|
123
|
+
if (removePlugin(name)) {
|
|
124
|
+
console.log(`✓ Removed plugin: ${name}`);
|
|
125
|
+
} else {
|
|
126
|
+
console.error(`Plugin not found: ${name}`);
|
|
127
|
+
process.exit(1);
|
|
128
|
+
}
|
|
129
|
+
break;
|
|
130
|
+
|
|
131
|
+
case 'default':
|
|
132
|
+
if (!name) {
|
|
133
|
+
console.error('Usage: trmnl plugin default <name>');
|
|
134
|
+
process.exit(1);
|
|
135
|
+
}
|
|
136
|
+
if (setDefaultPlugin(name)) {
|
|
137
|
+
console.log(`✓ Default plugin: ${name}`);
|
|
138
|
+
} else {
|
|
139
|
+
console.error(`Plugin not found: ${name}`);
|
|
140
|
+
process.exit(1);
|
|
141
|
+
}
|
|
142
|
+
break;
|
|
143
|
+
|
|
144
|
+
case 'set':
|
|
145
|
+
case 'update':
|
|
146
|
+
if (!name) {
|
|
147
|
+
console.error('Usage: trmnl plugin set <name> [options]');
|
|
148
|
+
process.exit(1);
|
|
149
|
+
}
|
|
150
|
+
const plugins = listPlugins();
|
|
151
|
+
const existing = plugins.find(p => p.name === name);
|
|
152
|
+
if (!existing) {
|
|
153
|
+
console.error(`Plugin not found: ${name}`);
|
|
154
|
+
process.exit(1);
|
|
155
|
+
}
|
|
156
|
+
const newUrl = options?.url || existing.plugin.url;
|
|
157
|
+
const newDesc = options?.desc !== undefined ? options.desc : existing.plugin.description;
|
|
158
|
+
setPlugin(name, newUrl, newDesc);
|
|
159
|
+
console.log(`✓ Updated plugin: ${name}`);
|
|
160
|
+
break;
|
|
161
|
+
|
|
162
|
+
case 'list':
|
|
163
|
+
showPluginList();
|
|
164
|
+
break;
|
|
165
|
+
|
|
166
|
+
default:
|
|
167
|
+
console.error(`Unknown action: ${action}`);
|
|
168
|
+
console.log('');
|
|
169
|
+
console.log('Available actions:');
|
|
170
|
+
console.log(' add <name> <url> - Add a plugin');
|
|
171
|
+
console.log(' rm <name> - Remove a plugin');
|
|
172
|
+
console.log(' default <name> - Set default plugin');
|
|
173
|
+
console.log(' set <name> - Update a plugin');
|
|
174
|
+
console.log(' list - List all plugins');
|
|
175
|
+
process.exit(1);
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
// Plugins alias for list
|
|
180
|
+
cli
|
|
181
|
+
.command('plugins', 'List all plugins')
|
|
182
|
+
.action(() => {
|
|
183
|
+
showPluginList();
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function showPluginList(): void {
|
|
188
|
+
const plugins = listPlugins();
|
|
189
|
+
|
|
190
|
+
if (plugins.length === 0) {
|
|
191
|
+
console.log('No plugins configured.');
|
|
192
|
+
console.log('');
|
|
193
|
+
console.log('Add a plugin:');
|
|
194
|
+
console.log(' trmnl plugin add <name> <url>');
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
console.log('Plugins:');
|
|
199
|
+
for (const { name, plugin, isDefault } of plugins) {
|
|
200
|
+
const defaultMark = isDefault ? ' ★' : '';
|
|
201
|
+
console.log(` ${name}${defaultMark}`);
|
|
202
|
+
console.log(` ${plugin.url}`);
|
|
203
|
+
if (plugin.description) {
|
|
204
|
+
console.log(` ${plugin.description}`);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
console.log('');
|
|
209
|
+
console.log('★ = default plugin');
|
|
210
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* trmnl history - View send history
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { unlinkSync } from 'node:fs';
|
|
6
|
+
import type { CAC } from 'cac';
|
|
7
|
+
import { formatEntry, getHistory, getHistoryPath, getHistoryStats, type HistoryFilter } from '../lib/logger.ts';
|
|
8
|
+
|
|
9
|
+
interface HistoryOptions {
|
|
10
|
+
last?: number;
|
|
11
|
+
today?: boolean;
|
|
12
|
+
failed?: boolean;
|
|
13
|
+
success?: boolean;
|
|
14
|
+
plugin?: string;
|
|
15
|
+
json?: boolean;
|
|
16
|
+
verbose?: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function registerHistoryCommand(cli: CAC): void {
|
|
20
|
+
cli
|
|
21
|
+
.command('history', 'View send history')
|
|
22
|
+
.option('-n, --last <n>', 'Show last N entries', { default: 10 })
|
|
23
|
+
.option('--today', 'Show only today\'s entries')
|
|
24
|
+
.option('--failed', 'Show only failed sends')
|
|
25
|
+
.option('--success', 'Show only successful sends')
|
|
26
|
+
.option('-p, --plugin <name>', 'Filter by plugin name')
|
|
27
|
+
.option('--json', 'Output as JSON')
|
|
28
|
+
.option('-v, --verbose', 'Show content preview')
|
|
29
|
+
.example('trmnl history')
|
|
30
|
+
.example('trmnl history --last 20')
|
|
31
|
+
.example('trmnl history --today --failed')
|
|
32
|
+
.example('trmnl history --plugin office')
|
|
33
|
+
.action((options: HistoryOptions) => {
|
|
34
|
+
const filter: HistoryFilter = {
|
|
35
|
+
last: options.last,
|
|
36
|
+
today: options.today,
|
|
37
|
+
failed: options.failed,
|
|
38
|
+
success: options.success,
|
|
39
|
+
plugin: options.plugin,
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const entries = getHistory(filter);
|
|
43
|
+
|
|
44
|
+
if (options.json) {
|
|
45
|
+
console.log(JSON.stringify(entries, null, 2));
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (entries.length === 0) {
|
|
50
|
+
console.log('No history entries found.');
|
|
51
|
+
console.log(`History file: ${getHistoryPath()}`);
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Stats header
|
|
56
|
+
const stats = getHistoryStats();
|
|
57
|
+
if (stats) {
|
|
58
|
+
console.log(`History: ${stats.entries} total entries (${stats.sizeMb} MB)`);
|
|
59
|
+
console.log('');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Filter description
|
|
63
|
+
const filterParts: string[] = [];
|
|
64
|
+
if (options.today) filterParts.push('today');
|
|
65
|
+
if (options.failed) filterParts.push('failed');
|
|
66
|
+
if (options.success) filterParts.push('success');
|
|
67
|
+
if (options.plugin) filterParts.push(`plugin: ${options.plugin}`);
|
|
68
|
+
if (filterParts.length > 0) {
|
|
69
|
+
console.log(`Filter: ${filterParts.join(', ')}`);
|
|
70
|
+
console.log('');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Entries
|
|
74
|
+
console.log(`Showing ${entries.length} entries (most recent first):`);
|
|
75
|
+
console.log('');
|
|
76
|
+
|
|
77
|
+
for (const entry of entries) {
|
|
78
|
+
console.log(formatEntry(entry, options.verbose));
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
// History clear
|
|
83
|
+
cli
|
|
84
|
+
.command('history clear', 'Clear send history')
|
|
85
|
+
.option('--confirm', 'Confirm deletion')
|
|
86
|
+
.action((options: { confirm?: boolean }) => {
|
|
87
|
+
const historyPath = getHistoryPath();
|
|
88
|
+
|
|
89
|
+
if (!options.confirm) {
|
|
90
|
+
console.log('This will delete all history. Use --confirm to proceed.');
|
|
91
|
+
console.log(`History file: ${historyPath}`);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
try {
|
|
96
|
+
unlinkSync(historyPath);
|
|
97
|
+
console.log('✓ History cleared');
|
|
98
|
+
} catch (err) {
|
|
99
|
+
if ((err as NodeJS.ErrnoException).code === 'ENOENT') {
|
|
100
|
+
console.log('History file does not exist.');
|
|
101
|
+
} else {
|
|
102
|
+
console.error('Error clearing history:', err);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
// History stats
|
|
108
|
+
cli
|
|
109
|
+
.command('history stats', 'Show history statistics')
|
|
110
|
+
.action(() => {
|
|
111
|
+
const stats = getHistoryStats();
|
|
112
|
+
|
|
113
|
+
if (!stats) {
|
|
114
|
+
console.log('No history file found.');
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const entries = getHistory({});
|
|
119
|
+
const successCount = entries.filter(e => e.success).length;
|
|
120
|
+
const failedCount = entries.filter(e => !e.success).length;
|
|
121
|
+
const totalBytes = entries.reduce((sum, e) => sum + e.size_bytes, 0);
|
|
122
|
+
const avgBytes = entries.length > 0 ? Math.round(totalBytes / entries.length) : 0;
|
|
123
|
+
const avgDuration = entries.length > 0
|
|
124
|
+
? Math.round(entries.reduce((sum, e) => sum + e.duration_ms, 0) / entries.length)
|
|
125
|
+
: 0;
|
|
126
|
+
|
|
127
|
+
// Plugin breakdown
|
|
128
|
+
const byPlugin = new Map<string, number>();
|
|
129
|
+
for (const entry of entries) {
|
|
130
|
+
byPlugin.set(entry.plugin, (byPlugin.get(entry.plugin) || 0) + 1);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
console.log('History Statistics');
|
|
134
|
+
console.log('');
|
|
135
|
+
console.log(`File: ${getHistoryPath()}`);
|
|
136
|
+
console.log(`Size: ${stats.sizeMb} MB`);
|
|
137
|
+
console.log('');
|
|
138
|
+
console.log(`Total: ${entries.length} sends`);
|
|
139
|
+
console.log(`Success: ${successCount} (${entries.length > 0 ? Math.round(successCount / entries.length * 100) : 0}%)`);
|
|
140
|
+
console.log(`Failed: ${failedCount} (${entries.length > 0 ? Math.round(failedCount / entries.length * 100) : 0}%)`);
|
|
141
|
+
console.log('');
|
|
142
|
+
console.log(`Avg size: ${avgBytes} bytes`);
|
|
143
|
+
console.log(`Avg duration: ${avgDuration}ms`);
|
|
144
|
+
|
|
145
|
+
if (byPlugin.size > 1) {
|
|
146
|
+
console.log('');
|
|
147
|
+
console.log('By plugin:');
|
|
148
|
+
for (const [plugin, count] of byPlugin.entries()) {
|
|
149
|
+
console.log(` ${plugin}: ${count} sends`);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Recent activity
|
|
154
|
+
const today = getHistory({ today: true });
|
|
155
|
+
const thisWeek = getHistory({ since: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000) });
|
|
156
|
+
console.log('');
|
|
157
|
+
console.log(`Today: ${today.length} sends`);
|
|
158
|
+
console.log(`This week: ${thisWeek.length} sends`);
|
|
159
|
+
});
|
|
160
|
+
}
|