@thinremote/thinr-cli 1.1.0 → 1.2.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.
Files changed (92) hide show
  1. package/CHANGELOG.md +71 -0
  2. package/README.md +230 -16
  3. package/bin/thinr.js +23 -24
  4. package/commands/_shared.js +154 -0
  5. package/commands/dashboard.js +17 -0
  6. package/commands/device/_shared.js +17 -34
  7. package/commands/device/console.js +1 -0
  8. package/commands/device/delete.js +53 -0
  9. package/commands/device/exec.js +7 -6
  10. package/commands/device/fs.js +193 -0
  11. package/commands/device/index.js +9 -0
  12. package/commands/device/list.js +35 -23
  13. package/commands/device/logs.js +178 -0
  14. package/commands/device/property.js +30 -28
  15. package/commands/device/proxy.js +3 -0
  16. package/commands/device/resource.js +30 -29
  17. package/commands/device/status.js +35 -14
  18. package/commands/device/transfer.js +236 -0
  19. package/commands/device/update.js +15 -10
  20. package/commands/fleet/index.js +17 -0
  21. package/commands/fleet/upgrade.js +275 -0
  22. package/commands/logout.js +10 -11
  23. package/commands/playbook/_shared.js +12 -0
  24. package/commands/playbook/index.js +18 -0
  25. package/commands/playbook/run.js +302 -0
  26. package/commands/playbook/validate.js +38 -0
  27. package/commands/product/_shared.js +8 -0
  28. package/commands/product/exec.js +243 -0
  29. package/commands/product/fs.js +218 -0
  30. package/commands/product/index.js +35 -0
  31. package/commands/product/list.js +86 -0
  32. package/commands/product/logs.js +289 -0
  33. package/commands/product/metric.js +124 -0
  34. package/commands/product/playbook.js +911 -0
  35. package/commands/product/property.js +76 -0
  36. package/commands/product/props.js +125 -0
  37. package/commands/product/push.js +184 -0
  38. package/commands/product/resource.js +76 -0
  39. package/commands/product/status.js +213 -0
  40. package/commands/profile.js +76 -8
  41. package/dist/dashboard.js +5644 -0
  42. package/lib/agent-releases.js +59 -0
  43. package/lib/agent-versions.js +73 -0
  44. package/lib/alarms.js +252 -0
  45. package/lib/api.js +8 -2
  46. package/lib/auth.js +30 -17
  47. package/lib/bucket.js +87 -0
  48. package/lib/command-stream.js +199 -0
  49. package/lib/concurrency.js +52 -0
  50. package/lib/config.js +5 -3
  51. package/lib/console.js +12 -4
  52. package/lib/constants.js +76 -0
  53. package/lib/dashboard/event-stream.js +239 -0
  54. package/lib/dashboard/metric-aggregate.js +64 -0
  55. package/lib/dashboard/metric-filter.js +39 -0
  56. package/lib/debug-log.js +93 -0
  57. package/lib/device-api.js +109 -147
  58. package/lib/devices.js +18 -21
  59. package/lib/fleet-upgrade.js +106 -0
  60. package/lib/format.js +114 -0
  61. package/lib/mcp/registry.js +14 -0
  62. package/lib/mcp/tools-alarms.js +510 -0
  63. package/lib/mcp/tools-buckets.js +103 -0
  64. package/lib/mcp/tools-devices.js +33 -3
  65. package/lib/mcp/tools-exec.js +30 -17
  66. package/lib/mcp/tools-filesystem.js +102 -6
  67. package/lib/mcp/tools-install.js +153 -0
  68. package/lib/mcp/tools-logs.js +225 -0
  69. package/lib/mcp/tools-playbook.js +211 -0
  70. package/lib/mcp/tools-products.js +1343 -10
  71. package/lib/mcp/tools-profile.js +634 -0
  72. package/lib/mcp/tools-tokens.js +371 -0
  73. package/lib/monitoring.js +26 -1
  74. package/lib/playbook/actions.js +447 -0
  75. package/lib/playbook/expression.js +302 -0
  76. package/lib/playbook/fleet.js +175 -0
  77. package/lib/playbook/loader.js +247 -0
  78. package/lib/playbook/runner.js +300 -0
  79. package/lib/playbook/schema.js +209 -0
  80. package/lib/playbook/vars.js +209 -0
  81. package/lib/product/log-presets.js +196 -0
  82. package/lib/product/logs.js +485 -0
  83. package/lib/product-orchestrator.js +124 -0
  84. package/lib/product.js +523 -20
  85. package/lib/profile.js +665 -0
  86. package/lib/property.js +7 -40
  87. package/lib/proxy.js +9 -8
  88. package/lib/resource.js +10 -14
  89. package/lib/status.js +78 -37
  90. package/lib/tokens.js +374 -0
  91. package/package.json +15 -3
  92. package/commands/product.js +0 -167
@@ -0,0 +1,289 @@
1
+ // @ts-check
2
+ import Table from 'cli-table3';
3
+ import {
4
+ addLogSource,
5
+ getProductLogs,
6
+ listPresets,
7
+ removeLogSource,
8
+ setDefaultLogSource,
9
+ } from '../../lib/product/logs.js';
10
+ import { hint, info, label, muted, success } from '../../lib/format.js';
11
+ import { classifyError, isJsonMode, printErr, printOk } from '../../lib/output.js';
12
+ import { applyJsonFlag, ensureConfigured, getGlobalUser } from '../_shared.js';
13
+
14
+ function truncate(value, max = 60) {
15
+ if (typeof value !== 'string') return '';
16
+ return value.length > max ? `${value.slice(0, max - 1)}…` : value;
17
+ }
18
+
19
+ function registerList(logs) {
20
+ logs
21
+ .command('list <productId>')
22
+ .helpGroup('Log sources:')
23
+ .description('List the log sources configured on a product')
24
+ .option('-j, --json', 'Output as JSON')
25
+ .action(async (productId, opts, cmd) => {
26
+ applyJsonFlag(opts);
27
+ ensureConfigured();
28
+ const user = getGlobalUser(cmd);
29
+ try {
30
+ const cfg = await getProductLogs(productId, user);
31
+ const fallback = !!(/** @type {any} */ (cfg).__fallback);
32
+ const def = cfg.default || cfg.sources[0]?.name || null;
33
+
34
+ if (isJsonMode()) {
35
+ printOk({
36
+ product: productId,
37
+ fallback,
38
+ default: def,
39
+ sources: cfg.sources,
40
+ });
41
+ return;
42
+ }
43
+
44
+ if (fallback) {
45
+ console.log(
46
+ `${muted('No logs property set on')} ${info(productId)}${muted('. Showing the synthetic fallback:')}`,
47
+ );
48
+ } else {
49
+ console.log(`${cfg.sources.length} source(s) on ${info(productId)}:`);
50
+ }
51
+ const table = new Table({
52
+ head: ['Name', 'Command', 'Pattern', 'Default'].map((h) => label(h)),
53
+ style: { head: [], border: ['gray'] },
54
+ });
55
+ for (const s of cfg.sources) {
56
+ const pat = s.preset
57
+ ? muted(`preset:${s.preset}`)
58
+ : s.pattern
59
+ ? truncate(s.pattern, 30)
60
+ : muted('—');
61
+ table.push([
62
+ s.name,
63
+ truncate(s.command),
64
+ pat,
65
+ s.name === def ? success('✓') : muted('—'),
66
+ ]);
67
+ }
68
+ console.log(table.toString());
69
+ } catch (err) {
70
+ const { message, code } = classifyError(err);
71
+ printErr(message, { code });
72
+ }
73
+ });
74
+ }
75
+
76
+ function registerAdd(logs) {
77
+ logs
78
+ .command('add <productId> <name>')
79
+ .helpGroup('Log sources:')
80
+ .description('Add or replace a log source on a product')
81
+ .requiredOption('-c, --command <cmd>', 'Shell command the agent should exec-stream for this source')
82
+ .option('--default', 'Mark this source as the active default')
83
+ .option(
84
+ '--pattern <regex>',
85
+ 'Custom regex with named groups (time, level, msg) for structured rendering',
86
+ )
87
+ .option(
88
+ '--preset <name>',
89
+ 'Named preset for structured rendering (see `thinr product logs presets`)',
90
+ )
91
+ .option('-j, --json', 'Output as JSON')
92
+ .action(async (productId, name, opts, cmd) => {
93
+ applyJsonFlag(opts);
94
+ ensureConfigured();
95
+ const user = getGlobalUser(cmd);
96
+ try {
97
+ const { config, action } = await addLogSource(
98
+ productId,
99
+ {
100
+ name,
101
+ command: opts.command,
102
+ makeDefault: !!opts.default,
103
+ pattern: opts.pattern,
104
+ preset: opts.preset,
105
+ },
106
+ user,
107
+ );
108
+ if (isJsonMode()) {
109
+ printOk({ product: productId, action, name, config });
110
+ return;
111
+ }
112
+ console.log(
113
+ `${success(action === 'added' ? 'Added' : 'Updated')} source ${info(name)} on product ${info(productId)}` +
114
+ (opts.default ? ` ${muted('(default)')}` : ''),
115
+ );
116
+ console.log(` ${muted('command:')} ${truncate(opts.command, 80)}`);
117
+ if (opts.preset) console.log(` ${muted('preset:')} ${opts.preset}`);
118
+ if (opts.pattern) console.log(` ${muted('pattern:')} ${truncate(opts.pattern, 80)}`);
119
+ console.log(
120
+ ` ${muted('total sources:')} ${config.sources.length}${
121
+ config.default ? ` ${muted('default:')} ${config.default}` : ''
122
+ }`,
123
+ );
124
+ } catch (err) {
125
+ const { message, code } = classifyError(err);
126
+ printErr(message, { code: code === 'error' ? 'input_error' : code });
127
+ }
128
+ });
129
+ }
130
+
131
+ function registerRemove(logs) {
132
+ logs
133
+ .command('remove <productId> <name>')
134
+ .alias('rm')
135
+ .helpGroup('Log sources:')
136
+ .description('Remove a log source from a product (idempotent)')
137
+ .option('-j, --json', 'Output as JSON')
138
+ .action(async (productId, name, opts, cmd) => {
139
+ applyJsonFlag(opts);
140
+ ensureConfigured();
141
+ const user = getGlobalUser(cmd);
142
+ try {
143
+ const { removed, config } = await removeLogSource(productId, name, user);
144
+ if (isJsonMode()) {
145
+ printOk({
146
+ product: productId,
147
+ name,
148
+ removed,
149
+ config: config || null,
150
+ cleared: removed && config === null,
151
+ });
152
+ return;
153
+ }
154
+ if (!removed) {
155
+ console.log(
156
+ `Source ${info(name)} was not configured on ${info(productId)}.`,
157
+ );
158
+ return;
159
+ }
160
+ if (config === null) {
161
+ console.log(
162
+ `${success('Removed')} source ${info(name)} (last one). ${muted('Logs property cleared — falling back to system journal.')}`,
163
+ );
164
+ return;
165
+ }
166
+ console.log(
167
+ `${success('Removed')} source ${info(name)}. ${muted(`${config.sources.length} source(s) remain`)}` +
168
+ (config.default ? ` ${muted(`· default: ${config.default}`)}` : ''),
169
+ );
170
+ } catch (err) {
171
+ const { message, code } = classifyError(err);
172
+ printErr(message, { code: code === 'error' ? 'input_error' : code });
173
+ }
174
+ });
175
+ }
176
+
177
+ function registerSetDefault(logs) {
178
+ logs
179
+ .command('set-default <productId> <name>')
180
+ .helpGroup('Log sources:')
181
+ .description('Set the active default source on a product')
182
+ .option('-j, --json', 'Output as JSON')
183
+ .action(async (productId, name, opts, cmd) => {
184
+ applyJsonFlag(opts);
185
+ ensureConfigured();
186
+ const user = getGlobalUser(cmd);
187
+ try {
188
+ const config = await setDefaultLogSource(productId, name, user);
189
+ if (isJsonMode()) {
190
+ printOk({ product: productId, default: config.default, config });
191
+ return;
192
+ }
193
+ console.log(
194
+ `${success('Default set')}: ${info(name)} on product ${info(productId)}.`,
195
+ );
196
+ } catch (err) {
197
+ const { message, code } = classifyError(err);
198
+ printErr(message, { code: code === 'error' ? 'input_error' : code });
199
+ }
200
+ });
201
+ }
202
+
203
+ function registerShow(logs) {
204
+ logs
205
+ .command('show <productId> <name>')
206
+ .helpGroup('Log sources:')
207
+ .description('Show a single log source (full command, untruncated)')
208
+ .option('-j, --json', 'Output as JSON')
209
+ .action(async (productId, name, opts, cmd) => {
210
+ applyJsonFlag(opts);
211
+ ensureConfigured();
212
+ const user = getGlobalUser(cmd);
213
+ try {
214
+ const cfg = await getProductLogs(productId, user);
215
+ const source = cfg.sources.find((s) => s.name === name);
216
+ if (!source) {
217
+ const available = cfg.sources.map((s) => s.name).join(', ') || '(none)';
218
+ printErr(
219
+ `Source "${name}" is not configured on product "${productId}". Available: ${available}`,
220
+ { code: 'not_found' },
221
+ );
222
+ return;
223
+ }
224
+ const fallback = !!(/** @type {any} */ (cfg).__fallback);
225
+ const isDefault = (cfg.default || cfg.sources[0]?.name) === name;
226
+ if (isJsonMode()) {
227
+ printOk({
228
+ product: productId,
229
+ fallback,
230
+ name: source.name,
231
+ command: source.command,
232
+ pattern: source.pattern || null,
233
+ preset: source.preset || null,
234
+ default: isDefault,
235
+ });
236
+ return;
237
+ }
238
+ console.log(`${label('Source')}: ${info(source.name)}${isDefault ? ` ${muted('(default)')}` : ''}`);
239
+ console.log(`${label('Command')}: ${source.command}`);
240
+ if (source.preset) console.log(`${label('Preset')}: ${source.preset}`);
241
+ if (source.pattern) console.log(`${label('Pattern')}: ${source.pattern}`);
242
+ if (fallback) console.log(hint('(synthetic fallback — no logs property set)'));
243
+ } catch (err) {
244
+ const { message, code } = classifyError(err);
245
+ printErr(message, { code });
246
+ }
247
+ });
248
+ }
249
+
250
+ function registerPresets(logs) {
251
+ logs
252
+ .command('presets')
253
+ .helpGroup('Log sources:')
254
+ .description('List the named log line patterns shipped with the CLI')
255
+ .option('-j, --json', 'Output as JSON')
256
+ .action(async (opts) => {
257
+ applyJsonFlag(opts);
258
+ const presets = listPresets();
259
+ if (isJsonMode()) {
260
+ printOk({ presets });
261
+ return;
262
+ }
263
+ const table = new Table({
264
+ head: ['Name', 'Description', 'Pattern'].map((h) => label(h)),
265
+ style: { head: [], border: ['gray'] },
266
+ });
267
+ for (const p of presets) {
268
+ table.push([p.name, p.description, truncate(p.pattern, 60)]);
269
+ }
270
+ console.log(`${presets.length} preset(s) available:`);
271
+ console.log(table.toString());
272
+ });
273
+ }
274
+
275
+ export function registerProductLogsCommand(product) {
276
+ const logs = product
277
+ .command('logs')
278
+ .helpGroup('Log sources:')
279
+ .description(
280
+ `Manage log sources stored on a product. ${hint('Subcommands: list, add, remove, set-default, show, presets.')}`,
281
+ );
282
+
283
+ registerList(logs);
284
+ registerAdd(logs);
285
+ registerRemove(logs);
286
+ registerSetDefault(logs);
287
+ registerShow(logs);
288
+ registerPresets(logs);
289
+ }
@@ -0,0 +1,124 @@
1
+ // @ts-check
2
+ import {
3
+ listDashboardMetrics,
4
+ upsertDashboardMetric,
5
+ removeDashboardMetric,
6
+ } from '../../lib/product.js';
7
+ import { info, success } from '../../lib/format.js';
8
+ import { isJsonMode, printOk, printErr, classifyError } from '../../lib/output.js';
9
+ import { applyJsonFlag, ensureConfigured, getGlobalUser } from '../_shared.js';
10
+
11
+ function stringifyMetricLine(m) {
12
+ const parts = [
13
+ `resource=${m.resource}`,
14
+ m.field ? `field=${m.field}` : null,
15
+ m.aggregation ? `agg=${m.aggregation}` : null,
16
+ m.visualization ? `viz=${m.visualization}` : null,
17
+ m.interval ? `every ${m.interval}s` : null,
18
+ ].filter(Boolean);
19
+ const trail = m.label ? ` · ${m.label}` : '';
20
+ return `${m.name} ${parts.join(' ')}${trail}`;
21
+ }
22
+
23
+ export function registerProductMetricCommands(product) {
24
+ product
25
+ .command('metric-list <productId>')
26
+ .helpGroup('Product config:')
27
+ .description('List dashboard metrics configured on a product')
28
+ .option('-j, --json', 'Output as JSON')
29
+ .action(async (productId, opts, cmd) => {
30
+ applyJsonFlag(opts);
31
+ ensureConfigured();
32
+ const user = getGlobalUser(cmd);
33
+ try {
34
+ const metrics = await listDashboardMetrics(productId, user);
35
+ if (isJsonMode()) {
36
+ printOk({ product: productId, metrics });
37
+ return;
38
+ }
39
+ if (metrics.length === 0) {
40
+ console.log(`No dashboard metrics configured on ${info(productId)}`);
41
+ return;
42
+ }
43
+ console.log(`${metrics.length} metric(s) on ${info(productId)}:`);
44
+ for (const m of metrics) console.log(' - ' + stringifyMetricLine(m));
45
+ } catch (error) {
46
+ const { message, code } = classifyError(error);
47
+ printErr(message, { code });
48
+ }
49
+ });
50
+
51
+ product
52
+ .command('metric-set <productId> <name>')
53
+ .helpGroup('Product config:')
54
+ .description('Add or update a dashboard metric on a product')
55
+ .requiredOption('-r, --resource <resource>', 'Product API resource the dashboard invokes')
56
+ .option('-l, --label <label>', 'Human-readable label for the metric')
57
+ .option('-f, --field <field>', 'Dot-path to the numeric value inside the response')
58
+ .option(
59
+ '-a, --aggregation <agg>',
60
+ 'Aggregation across the fleet (sum, avg, max, min, count, top, none, distribution)',
61
+ )
62
+ .option(
63
+ '-v, --visualization <viz>',
64
+ 'Dashboard rendering hint (kpi, bar, sparkline, list)',
65
+ )
66
+ .option('-i, --interval <seconds>', 'Dashboard refresh interval in seconds', Number)
67
+ .option('--unit <unit>', 'Unit suffix for display (e.g. "%", "devices")')
68
+ .option('--json', 'Output as JSON')
69
+ .action(async (productId, name, opts, cmd) => {
70
+ applyJsonFlag(opts);
71
+ ensureConfigured();
72
+ const user = getGlobalUser(cmd);
73
+ const metric = {
74
+ name,
75
+ label: opts.label,
76
+ resource: opts.resource,
77
+ field: opts.field,
78
+ aggregation: opts.aggregation,
79
+ visualization: opts.visualization,
80
+ interval: opts.interval,
81
+ unit: opts.unit,
82
+ };
83
+ for (const k of Object.keys(metric)) if (metric[k] === undefined) delete metric[k];
84
+ try {
85
+ const { action, count } = await upsertDashboardMetric(productId, metric, user);
86
+ if (isJsonMode()) {
87
+ printOk({ product: productId, action, metric, total: count });
88
+ return;
89
+ }
90
+ console.log(
91
+ `Metric ${info(name)} ${success(action)} on ${info(productId)} (total: ${count}).`,
92
+ );
93
+ } catch (error) {
94
+ const { message, code } = classifyError(error);
95
+ printErr(message, { code });
96
+ }
97
+ });
98
+
99
+ product
100
+ .command('metric-delete <productId> <name>')
101
+ .helpGroup('Product config:')
102
+ .description('Remove a dashboard metric from a product')
103
+ .option('-j, --json', 'Output as JSON')
104
+ .action(async (productId, name, opts, cmd) => {
105
+ applyJsonFlag(opts);
106
+ ensureConfigured();
107
+ const user = getGlobalUser(cmd);
108
+ try {
109
+ const { action, count } = await removeDashboardMetric(productId, name, user);
110
+ if (isJsonMode()) {
111
+ printOk({ product: productId, action, remaining: count });
112
+ return;
113
+ }
114
+ console.log(
115
+ action === 'removed'
116
+ ? `Removed ${info(name)} from ${info(productId)} (remaining: ${count}).`
117
+ : `Metric ${info(name)} was not configured on ${info(productId)}.`,
118
+ );
119
+ } catch (error) {
120
+ const { message, code } = classifyError(error);
121
+ printErr(message, { code });
122
+ }
123
+ });
124
+ }