@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.
- package/CHANGELOG.md +63 -1
- package/README.md +235 -30
- package/bin/thinr.js +43 -25
- package/commands/_shared.js +154 -0
- package/commands/dashboard.js +17 -0
- package/commands/device/_shared.js +17 -34
- package/commands/device/console.js +1 -0
- package/commands/device/delete.js +53 -0
- package/commands/device/exec.js +7 -6
- package/commands/device/fs.js +193 -0
- package/commands/device/index.js +9 -0
- package/commands/device/list.js +35 -23
- package/commands/device/logs.js +178 -0
- package/commands/device/property.js +30 -28
- package/commands/device/proxy.js +3 -0
- package/commands/device/resource.js +30 -29
- package/commands/device/status.js +35 -14
- package/commands/device/transfer.js +236 -0
- package/commands/device/update.js +15 -10
- package/commands/fleet/index.js +17 -0
- package/commands/fleet/upgrade.js +275 -0
- package/commands/logout.js +10 -11
- package/commands/playbook/_shared.js +12 -0
- package/commands/playbook/index.js +18 -0
- package/commands/playbook/run.js +302 -0
- package/commands/playbook/validate.js +38 -0
- package/commands/product/_shared.js +8 -0
- package/commands/product/exec.js +243 -0
- package/commands/product/fs.js +218 -0
- package/commands/product/index.js +35 -0
- package/commands/product/list.js +86 -0
- package/commands/product/logs.js +289 -0
- package/commands/product/metric.js +124 -0
- package/commands/product/playbook.js +911 -0
- package/commands/product/property.js +76 -0
- package/commands/product/props.js +125 -0
- package/commands/product/push.js +184 -0
- package/commands/product/resource.js +76 -0
- package/commands/product/status.js +213 -0
- package/commands/profile.js +76 -8
- package/dist/dashboard.js +5644 -0
- package/lib/agent-releases.js +59 -0
- package/lib/agent-versions.js +73 -0
- package/lib/alarms.js +252 -0
- package/lib/api.js +8 -2
- package/lib/auth.js +68 -20
- package/lib/bucket.js +87 -0
- package/lib/command-stream.js +213 -0
- package/lib/config.js +5 -3
- package/lib/console.js +19 -5
- package/lib/constants.js +83 -0
- package/lib/dashboard/event-stream.js +239 -0
- package/lib/dashboard/metric-aggregate.js +64 -0
- package/lib/dashboard/metric-filter.js +39 -0
- package/lib/debug-log.js +93 -0
- package/lib/device-api.js +131 -148
- package/lib/devices.js +18 -21
- package/lib/errors.js +12 -0
- package/lib/fleet-upgrade.js +106 -0
- package/lib/format.js +114 -0
- package/lib/mcp/registry.js +14 -0
- package/lib/mcp/tools-alarms.js +510 -0
- package/lib/mcp/tools-buckets.js +103 -0
- package/lib/mcp/tools-devices.js +33 -3
- package/lib/mcp/tools-exec.js +30 -17
- package/lib/mcp/tools-filesystem.js +102 -6
- package/lib/mcp/tools-install.js +153 -0
- package/lib/mcp/tools-logs.js +225 -0
- package/lib/mcp/tools-playbook.js +211 -0
- package/lib/mcp/tools-products.js +1226 -105
- package/lib/mcp/tools-profile.js +634 -0
- package/lib/mcp/tools-tokens.js +371 -0
- package/lib/monitoring.js +26 -1
- package/lib/playbook/actions.js +447 -0
- package/lib/playbook/expression.js +302 -0
- package/lib/playbook/fleet.js +175 -0
- package/lib/playbook/loader.js +247 -0
- package/lib/playbook/runner.js +300 -0
- package/lib/playbook/schema.js +209 -0
- package/lib/playbook/vars.js +209 -0
- package/lib/product/log-presets.js +196 -0
- package/lib/product/logs.js +485 -0
- package/lib/product-orchestrator.js +124 -0
- package/lib/product.js +523 -20
- package/lib/profile.js +665 -0
- package/lib/property.js +7 -40
- package/lib/proxy.js +9 -8
- package/lib/resource.js +10 -14
- package/lib/status.js +78 -37
- package/lib/tokens.js +374 -0
- package/package.json +14 -3
- 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
|
+
}
|
package/lib/mcp/registry.js
CHANGED
|
@@ -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
|