@thinremote/thinr-cli 1.2.0-alpha.1 → 1.3.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 +63 -1
  2. package/README.md +235 -30
  3. package/bin/thinr.js +43 -25
  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 +68 -20
  47. package/lib/bucket.js +87 -0
  48. package/lib/command-stream.js +213 -0
  49. package/lib/config.js +5 -3
  50. package/lib/console.js +19 -5
  51. package/lib/constants.js +83 -0
  52. package/lib/dashboard/event-stream.js +239 -0
  53. package/lib/dashboard/metric-aggregate.js +64 -0
  54. package/lib/dashboard/metric-filter.js +39 -0
  55. package/lib/debug-log.js +93 -0
  56. package/lib/device-api.js +131 -148
  57. package/lib/devices.js +18 -21
  58. package/lib/errors.js +12 -0
  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 +1226 -105
  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 +14 -3
  92. package/commands/product.js +0 -407
@@ -0,0 +1,106 @@
1
+ import { createDeviceAPI } from './device-api.js';
2
+ import { TIMEOUTS } from './constants.js';
3
+
4
+ /**
5
+ * Roll a fleet-wide agent upgrade. Strategy:
6
+ * 1. If `canary` is set and there's more than one device, upgrade one
7
+ * device in isolation first. A canary failure always aborts — the
8
+ * point of the canary is to catch arch / channel / connectivity
9
+ * mistakes before we touch the rest.
10
+ * 2. The remainder is upgraded in parallel batches of `batchSize`. If
11
+ * `abortOnFailure` is set, any failure inside a batch stops the run
12
+ * after that batch completes (we never leave requests dangling).
13
+ * 3. Results come back as { ok, failed, aborted } plus the per-device
14
+ * records. Progress is reported through `onProgress` so the UI can
15
+ * show `updating N/M…` live.
16
+ *
17
+ * We intentionally treat any HTTP rejection as a failure and any resolved
18
+ * call as success. The agent's `update` resource doesn't expose a stable
19
+ * machine-readable status across every "nothing to do" case, so finer
20
+ * classification would be fragile here — a resolved call means the agent
21
+ * accepted the request; whether it then swapped binaries is visible in
22
+ * the next sample's version field.
23
+ */
24
+ export async function runFleetUpgrade({
25
+ deviceIds,
26
+ channel = 'latest',
27
+ user,
28
+ canary = true,
29
+ abortOnFailure = true,
30
+ batchSize = 5,
31
+ onProgress,
32
+ onDeviceResult,
33
+ signal,
34
+ }) {
35
+ const queue = [...deviceIds];
36
+ const total = queue.length;
37
+ const results = [];
38
+ let done = 0;
39
+
40
+ const emitProgress = (phase) => {
41
+ onProgress?.({ done, total, phase });
42
+ };
43
+
44
+ async function upgradeOne(id) {
45
+ if (signal?.aborted) {
46
+ const r = { deviceId: id, ok: false, error: 'cancelled' };
47
+ results.push(r);
48
+ onDeviceResult?.(r);
49
+ return r;
50
+ }
51
+ let r;
52
+ try {
53
+ const api = createDeviceAPI(id, user ? { user } : {});
54
+ const result = await api.callResource(
55
+ 'update',
56
+ { action: 'apply', channel },
57
+ { timeout: TIMEOUTS.DEVICE_UPDATE_APPLY_MS },
58
+ );
59
+ r = { deviceId: id, ok: true, result };
60
+ } catch (e) {
61
+ r = {
62
+ deviceId: id,
63
+ ok: false,
64
+ error: e?.message || String(e),
65
+ };
66
+ }
67
+ results.push(r);
68
+ done += 1;
69
+ onDeviceResult?.(r);
70
+ emitProgress('running');
71
+ return r;
72
+ }
73
+
74
+ emitProgress('running');
75
+
76
+ // Canary first — a single device, synchronously. Treat failure as
77
+ // terminal regardless of `abortOnFailure`: if the canary fails we have
78
+ // no signal that a wider rollout would succeed, and blindly firing at
79
+ // 95 more devices would only amplify whatever went wrong.
80
+ if (canary && queue.length > 1) {
81
+ const canaryId = queue.shift();
82
+ const r = await upgradeOne(canaryId);
83
+ if (!r.ok) {
84
+ return { results, aborted: true, reason: 'canary-failed' };
85
+ }
86
+ }
87
+
88
+ while (queue.length > 0) {
89
+ if (signal?.aborted) {
90
+ return { results, aborted: true, reason: 'cancelled' };
91
+ }
92
+ const batch = queue.splice(0, batchSize);
93
+ const batchResults = await Promise.all(batch.map(upgradeOne));
94
+ if (abortOnFailure && batchResults.some((r) => !r.ok)) {
95
+ return { results, aborted: true, reason: 'batch-failed' };
96
+ }
97
+ }
98
+
99
+ return { results, aborted: false };
100
+ }
101
+
102
+ export function summarize(results) {
103
+ const ok = results.filter((r) => r.ok).length;
104
+ const failed = results.length - ok;
105
+ return { ok, failed, total: results.length };
106
+ }
package/lib/format.js ADDED
@@ -0,0 +1,114 @@
1
+ // @ts-check
2
+ import chalk from 'chalk';
3
+
4
+ // ─────────────────────────────────────────────────────────────────────
5
+ // Theme — semantic wrappers over chalk so every CLI surface speaks
6
+ // the same visual language. New colours / a `--no-color` switch / a
7
+ // dark-on-light adjustment only have to be touched here.
8
+ // ─────────────────────────────────────────────────────────────────────
9
+
10
+ /** Title / header text. */
11
+ export const label = (s) => chalk.bold(s);
12
+ /** Secondary / disabled / "—" text. */
13
+ export const muted = (s) => chalk.dim(s);
14
+ /** Hints, trailing metadata, footers, timestamps. */
15
+ export const hint = (s) => chalk.gray(s);
16
+ /** Positive states (online, ok, success). */
17
+ export const success = (s) => chalk.green(s);
18
+ /** Negative states (offline, failed, errored). */
19
+ export const error = (s) => chalk.red(s);
20
+ /** Caution states (timeout, at-risk thresholds). */
21
+ export const warning = (s) => chalk.yellow(s);
22
+ /** Decorative accents (separators, prompts). */
23
+ export const accent = (s) => chalk.cyan(s);
24
+ /** Neutral-but-noteworthy identifiers (device ids, resource names). */
25
+ export const info = (s) => chalk.blue(s);
26
+
27
+ /**
28
+ * Format a duration in seconds as a human-readable string: `309d16h`,
29
+ * `42d15h`, `17m`, `42s`, etc. Returns `—` for invalid/missing values.
30
+ *
31
+ * @param {number | null | undefined} seconds
32
+ * @returns {string}
33
+ */
34
+ export function formatUptime(seconds) {
35
+ if (seconds == null || !Number.isFinite(seconds) || seconds < 0) return '—';
36
+ const d = Math.floor(seconds / 86400);
37
+ const h = Math.floor((seconds % 86400) / 3600);
38
+ const m = Math.floor((seconds % 3600) / 60);
39
+ if (d > 0) return `${d}d${h}h`;
40
+ if (h > 0) return `${h}h${m}m`;
41
+ if (m > 0) return `${m}m`;
42
+ return `${Math.floor(seconds)}s`;
43
+ }
44
+
45
+ /**
46
+ * Format seconds elapsed as a "time ago" label: `5s ago`, `3m ago`,
47
+ * `2h ago`, `4d ago`. Returns `—` for invalid/missing values.
48
+ *
49
+ * @param {number | null | undefined} seconds
50
+ * @returns {string}
51
+ */
52
+ export function formatAgo(seconds) {
53
+ if (seconds == null || !Number.isFinite(seconds) || seconds < 0) return '—';
54
+ if (seconds < 60) return `${Math.floor(seconds)}s ago`;
55
+ if (seconds < 3600) return `${Math.floor(seconds / 60)}m ago`;
56
+ if (seconds < 86400) return `${Math.floor(seconds / 3600)}h ago`;
57
+ return `${Math.floor(seconds / 86400)}d ago`;
58
+ }
59
+
60
+ /**
61
+ * Render a percentage value coloured by threshold: green below 70 %,
62
+ * yellow below 90 %, red at or above 90 %. Returns a dim `—` when the
63
+ * value is missing. Formatted with no decimal places so columns in
64
+ * cli-table3 stay aligned.
65
+ *
66
+ * @param {number | null | undefined} v
67
+ * @returns {string}
68
+ */
69
+ export function colorPct(v) {
70
+ if (v == null || !Number.isFinite(v)) return muted('—');
71
+ const s = `${v.toFixed(0)}%`;
72
+ if (v >= 90) return error(s);
73
+ if (v >= 70) return warning(s);
74
+ return success(s);
75
+ }
76
+
77
+ /**
78
+ * Format a duration in seconds as a short ETA label: `12s`, `3m15s`,
79
+ * `2h30m`. Returns `—` for non-finite / missing values.
80
+ *
81
+ * @param {number | null | undefined} seconds
82
+ * @returns {string}
83
+ */
84
+ export function formatETA(seconds) {
85
+ if (seconds == null || !Number.isFinite(seconds) || seconds < 0) return '—';
86
+ if (seconds < 60) return `${Math.max(1, Math.ceil(seconds))}s`;
87
+ if (seconds < 3600) {
88
+ const m = Math.floor(seconds / 60);
89
+ const s = Math.ceil(seconds % 60);
90
+ return s > 0 ? `${m}m${s}s` : `${m}m`;
91
+ }
92
+ const h = Math.floor(seconds / 3600);
93
+ const m = Math.ceil((seconds % 3600) / 60);
94
+ return m > 0 ? `${h}h${m}m` : `${h}h`;
95
+ }
96
+
97
+ /**
98
+ * Format a byte count as a human-readable string picking the largest
99
+ * unit where the value is ≥ 1. `2 bytes`, `12.3 KB`, `145 MB`,
100
+ * `2.4 GB`. Returns `—` for invalid/missing values.
101
+ *
102
+ * @param {number | null | undefined} bytes
103
+ * @returns {string}
104
+ */
105
+ export function formatBytes(bytes) {
106
+ if (bytes == null || !Number.isFinite(bytes) || bytes < 0) return '—';
107
+ if (bytes < 1024) return `${bytes} B`;
108
+ const kb = bytes / 1024;
109
+ if (kb < 1024) return `${kb.toFixed(1)} KB`;
110
+ const mb = kb / 1024;
111
+ if (mb < 1024) return `${mb.toFixed(mb >= 100 ? 0 : 1)} MB`;
112
+ const gb = mb / 1024;
113
+ return `${gb.toFixed(gb >= 100 ? 0 : 2)} GB`;
114
+ }
@@ -6,7 +6,14 @@ import { tools as resourcesTools } from './tools-resources.js';
6
6
  import { tools as propertiesTools } from './tools-properties.js';
7
7
  import { tools as scriptsTools } from './tools-scripts.js';
8
8
  import { tools as productsTools } from './tools-products.js';
9
+ import { tools as playbookTools } from './tools-playbook.js';
9
10
  import { tools as profilesTools } from './tools-profiles.js';
11
+ import { tools as installTools } from './tools-install.js';
12
+ import { tools as bucketsTools } from './tools-buckets.js';
13
+ import { tools as logsTools } from './tools-logs.js';
14
+ import { tools as productProfileTools } from './tools-profile.js';
15
+ import { tools as tokensTools } from './tools-tokens.js';
16
+ import { tools as alarmsTools } from './tools-alarms.js';
10
17
 
11
18
  export const tools = [
12
19
  ...devicesTools,
@@ -16,7 +23,14 @@ export const tools = [
16
23
  ...propertiesTools,
17
24
  ...scriptsTools,
18
25
  ...productsTools,
26
+ ...playbookTools,
19
27
  ...profilesTools,
28
+ ...installTools,
29
+ ...bucketsTools,
30
+ ...logsTools,
31
+ ...productProfileTools,
32
+ ...tokensTools,
33
+ ...alarmsTools,
20
34
  ];
21
35
 
22
36
  // Inject the optional `profile` parameter on every tool so callers can