@thinremote/thinr-cli 1.0.2-alpha.2 → 1.1.0-alpha.1

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 (46) hide show
  1. package/CHANGELOG.md +90 -0
  2. package/README.md +362 -87
  3. package/bin/thinr.js +51 -11
  4. package/commands/device/_shared.js +48 -0
  5. package/commands/device/console.js +15 -0
  6. package/commands/device/exec.js +76 -0
  7. package/commands/device/index.js +31 -0
  8. package/commands/device/list.js +43 -0
  9. package/commands/device/property.js +50 -0
  10. package/commands/device/proxy.js +42 -0
  11. package/commands/device/resource.js +51 -0
  12. package/commands/device/status.js +26 -0
  13. package/commands/device/update.js +53 -0
  14. package/commands/logout.js +12 -5
  15. package/commands/product.js +146 -177
  16. package/commands/profile.js +105 -0
  17. package/lib/api.js +54 -31
  18. package/lib/auth.js +225 -309
  19. package/lib/config.js +176 -45
  20. package/lib/console.js +87 -108
  21. package/lib/device-api.js +257 -0
  22. package/lib/devices.js +69 -0
  23. package/lib/errors.js +53 -0
  24. package/lib/mcp/helpers.js +34 -0
  25. package/lib/mcp/registry.js +32 -0
  26. package/lib/mcp/server.js +63 -0
  27. package/lib/mcp/tools-devices.js +194 -0
  28. package/lib/mcp/tools-exec.js +213 -0
  29. package/lib/mcp/tools-filesystem.js +163 -0
  30. package/lib/mcp/tools-products.js +232 -0
  31. package/lib/mcp/tools-profiles.js +30 -0
  32. package/lib/mcp/tools-properties.js +79 -0
  33. package/lib/mcp/tools-resources.js +76 -0
  34. package/lib/mcp/tools-scripts.js +172 -0
  35. package/lib/monitoring.js +72 -0
  36. package/lib/output.js +114 -0
  37. package/lib/product.js +373 -0
  38. package/lib/property.js +41 -57
  39. package/lib/proxy.js +58 -90
  40. package/lib/resource.js +106 -72
  41. package/lib/status.js +7 -33
  42. package/lib/storage.js +128 -0
  43. package/package.json +68 -37
  44. package/.github/workflows/publish.yml +0 -79
  45. package/commands/device.js +0 -268
  46. package/lib/device.js +0 -76
package/bin/thinr.js CHANGED
@@ -1,23 +1,48 @@
1
1
  #!/usr/bin/env node
2
2
 
3
+ import { readFileSync } from 'fs';
4
+ import { fileURLToPath } from 'url';
5
+ import { dirname, resolve } from 'path';
3
6
  import { Command } from 'commander';
4
7
  import chalk from 'chalk';
5
8
  import figlet from 'figlet';
6
9
  import ora from 'ora';
7
- import { configExists, readConfig } from '../lib/config.js';
10
+ import { configExists, readConfig, setActiveProfile } from '../lib/config.js';
11
+ import { detectJsonModeFromArgv } from '../lib/output.js';
12
+
13
+ // Single source of truth for the version: package.json. Avoids the drift
14
+ // between `package.json#version` and a hardcoded string in this file.
15
+ const pkg = JSON.parse(
16
+ readFileSync(resolve(dirname(fileURLToPath(import.meta.url)), '../package.json'), 'utf8'),
17
+ );
18
+
19
+ // Pick up --profile early so subsequent config reads use the right one.
20
+ // Commander parses options later, but the config is touched before that.
21
+ for (let i = 2; i < process.argv.length; i++) {
22
+ const a = process.argv[i];
23
+ if (a === '--profile' && process.argv[i + 1]) {
24
+ setActiveProfile(process.argv[i + 1]);
25
+ break;
26
+ }
27
+ if (a.startsWith('--profile=')) {
28
+ setActiveProfile(a.slice('--profile='.length));
29
+ break;
30
+ }
31
+ }
32
+
33
+ // Set JSON mode before any command runs so spinners/logs can opt out.
34
+ detectJsonModeFromArgv();
8
35
  import { authenticate } from '../lib/auth.js';
9
- import { deviceCommand } from '../commands/device.js';
36
+ import { deviceCommand } from '../commands/device/index.js';
10
37
  import { logoutCommand } from '../commands/logout.js';
11
- import { productCommand } from "../commands/product.js";
38
+ import { productCommand } from '../commands/product.js';
39
+ import { profileCommand } from '../commands/profile.js';
12
40
  import { setBaseURL } from '../lib/api.js';
41
+ import { startMCPServer } from '../lib/mcp/server.js';
13
42
 
14
43
  // Function to display the banner
15
44
  function displayBanner() {
16
- console.log(
17
- chalk.cyan(
18
- figlet.textSync('thinr-cli', { horizontalLayout: 'full' })
19
- )
20
- );
45
+ console.log(chalk.cyan(figlet.textSync('thinr-cli', { horizontalLayout: 'full' })));
21
46
  console.log(chalk.blue('Thin Remote CLI - Remote management for IoT devices'));
22
47
  console.log();
23
48
  }
@@ -28,16 +53,31 @@ const program = new Command();
28
53
  program
29
54
  .name('thinr')
30
55
  .description('CLI for ThinRemote - Remote management for IoT devices')
31
- .version('1.0.2');
56
+ .version(pkg.version)
57
+ .option('-u, --user <username>', 'API user override (admin impersonation)')
58
+ .option('--profile <name>', 'Configuration profile to use (defaults to the saved default)');
32
59
 
33
60
  // Register commands
34
61
  deviceCommand(program);
35
62
  productCommand(program);
63
+ profileCommand(program);
36
64
  logoutCommand(program);
37
65
 
66
+ program
67
+ .command('mcp')
68
+ .description('Start the MCP server (stdio) for AI tool integration')
69
+ .action(async () => {
70
+ try {
71
+ await startMCPServer();
72
+ } catch (error) {
73
+ console.error(`Error: ${error.message}`);
74
+ process.exit(1);
75
+ }
76
+ });
77
+
38
78
  // Handle help
39
79
  const originalHelp = program.help;
40
- program.help = function(cb) {
80
+ program.help = function (cb) {
41
81
  displayBanner();
42
82
  return originalHelp.call(this, cb);
43
83
  };
@@ -75,4 +115,4 @@ if (process.argv.length <= 2) {
75
115
  } else {
76
116
  // Parse arguments - don't show banner for regular commands
77
117
  program.parse(process.argv);
78
- }
118
+ }
@@ -0,0 +1,48 @@
1
+ // @ts-check
2
+ import chalk from 'chalk';
3
+ import { InvalidArgumentError } from 'commander';
4
+ import { configExists } from '../../lib/config.js';
5
+ import { setJsonMode, classifyError, printErr } from '../../lib/output.js';
6
+
7
+ export function ensureConfigured() {
8
+ if (!configExists()) {
9
+ printErr('Not configured. Run thinr without parameters to set up.', {
10
+ code: 'not_configured',
11
+ });
12
+ }
13
+ }
14
+
15
+ export function applyJsonFlag(opts) {
16
+ if (opts.json) setJsonMode(true);
17
+ }
18
+
19
+ // Walks up to the root program so subcommands can read `--user` regardless
20
+ // of nesting depth.
21
+ export function getGlobalUser(cmd) {
22
+ let root = cmd;
23
+ while (root.parent) root = root.parent;
24
+ return root.opts().user || null;
25
+ }
26
+
27
+ // Collect repeatable `-i key=value` pairs into a single object. Uses
28
+ // Commander's InvalidArgumentError so a bad value prints a clean
29
+ // "error: option … argument is invalid" line instead of a stack trace.
30
+ export function collectInput(value, previous = {}) {
31
+ const idx = value.indexOf('=');
32
+ if (idx === -1) throw new InvalidArgumentError('must be key=value');
33
+ return { ...previous, [value.slice(0, idx)]: value.slice(idx + 1) };
34
+ }
35
+
36
+ // Wrapper for the proxy / console handlers, which return a Promise that
37
+ // resolves with an exit code or rejects with a tagged Error (instead of
38
+ // calling process.exit themselves).
39
+ export const runInteractive = async (fn) => {
40
+ try {
41
+ const exitCode = await fn();
42
+ process.exit(exitCode ?? 0);
43
+ } catch (error) {
44
+ const { message, code } = classifyError(error);
45
+ console.error(chalk.red(`Error [${code}]: ${message}`));
46
+ process.exit(1);
47
+ }
48
+ };
@@ -0,0 +1,15 @@
1
+ // @ts-check
2
+ import { connectToDeviceConsole } from '../../lib/console.js';
3
+ import { ensureConfigured, runInteractive } from './_shared.js';
4
+
5
+ export function registerConsoleCommand(device) {
6
+ device
7
+ .command('console <deviceId>')
8
+ .description('Open an interactive terminal on the device')
9
+ .action((deviceId) =>
10
+ runInteractive(() => {
11
+ ensureConfigured();
12
+ return connectToDeviceConsole(deviceId);
13
+ }),
14
+ );
15
+ }
@@ -0,0 +1,76 @@
1
+ // @ts-check
2
+ import chalk from 'chalk';
3
+ import { createDeviceAPI } from '../../lib/device-api.js';
4
+ import { isJsonMode, printOk, printErr, classifyError } from '../../lib/output.js';
5
+ import { applyJsonFlag, ensureConfigured, getGlobalUser } from './_shared.js';
6
+
7
+ export function registerExecCommand(device) {
8
+ device
9
+ .command('exec <deviceId> <command...>')
10
+ .description('Execute a shell command on the device')
11
+ .option('-j, --json', 'Buffer stdout/stderr and emit a single JSON envelope')
12
+ .option('--legacy', 'Use the non-streaming one-shot API (older agents)')
13
+ .action(async (deviceId, commandParts, opts, cmd) => {
14
+ applyJsonFlag(opts);
15
+ ensureConfigured();
16
+ const user = getGlobalUser(cmd);
17
+ const commandStr = commandParts.join(' ');
18
+ try {
19
+ const api = createDeviceAPI(deviceId, { user });
20
+ if (isJsonMode()) {
21
+ let stdout = '';
22
+ let stderr = '';
23
+ const { exitCode, timedOut, cancelled } = await api.execStream(commandStr, {
24
+ onStdout: (s) => {
25
+ stdout += s;
26
+ },
27
+ onStderr: (s) => {
28
+ stderr += s;
29
+ },
30
+ });
31
+ if (cancelled) printErr('Interrupted', { code: 'cancelled', exitCode: 130 });
32
+ if (timedOut) printErr('Command timed out on the device', { code: 'timeout' });
33
+ printOk({ stdout, stderr, exitCode: exitCode ?? null });
34
+ process.exit(exitCode ?? 1);
35
+ }
36
+ if (opts.legacy) {
37
+ const result = await api.exec(commandStr, 120);
38
+ if (result.stdout) process.stdout.write(result.stdout);
39
+ if (result.stderr) process.stderr.write(result.stderr);
40
+ process.exit(result.retcode || 0);
41
+ }
42
+ let cancelFn = null;
43
+ const onSigint = () => cancelFn?.();
44
+ process.on('SIGINT', onSigint);
45
+ const { exitCode, timedOut, cancelled } = await api.execStream(commandStr, {
46
+ onStdout: (s) => process.stdout.write(s),
47
+ onStderr: (s) => process.stderr.write(s),
48
+ onCancel: (fn) => {
49
+ cancelFn = fn;
50
+ },
51
+ stdin: process.stdin,
52
+ });
53
+ process.off('SIGINT', onSigint);
54
+ if (timedOut) {
55
+ console.error(chalk.red('\nError: command timed out on the device.'));
56
+ }
57
+ if (cancelled) {
58
+ console.error(chalk.yellow('\n[interrupted]'));
59
+ process.exit(130);
60
+ }
61
+ process.exit(exitCode ?? 1);
62
+ } catch (error) {
63
+ const { message, code } = classifyError(error);
64
+ if (isJsonMode()) printErr(message, { code });
65
+ // Human path: prefix `[code]` matches the MCP error format
66
+ // and the rest of the CLI; the response dump only surfaces
67
+ // when the failure carries an HTTP body.
68
+ console.error(chalk.red(`Error [${code}]: ${message}`));
69
+ if (error.response) {
70
+ console.error(chalk.red(`Status: ${error.response.status}`));
71
+ console.error(chalk.red(`Body: ${JSON.stringify(error.response.data)}`));
72
+ }
73
+ process.exit(1);
74
+ }
75
+ });
76
+ }
@@ -0,0 +1,31 @@
1
+ // @ts-check
2
+ import { registerListCommand } from './list.js';
3
+ import { registerProxyCommands } from './proxy.js';
4
+ import { registerConsoleCommand } from './console.js';
5
+ import { registerStatusCommand } from './status.js';
6
+ import { registerPropertyCommand } from './property.js';
7
+ import { registerResourceCommand } from './resource.js';
8
+ import { registerExecCommand } from './exec.js';
9
+ import { registerUpdateCommand } from './update.js';
10
+
11
+ /**
12
+ * `thinr device <subcommand> [<deviceId>] …` — all device-scoped actions
13
+ * as proper Commander subcommands. Most subcommands declare `<deviceId>`
14
+ * as their first positional so help, option parsing and validation are
15
+ * autogenerated per-action; `list` is the exception (it operates on the
16
+ * fleet, not a single device).
17
+ */
18
+ export function deviceCommand(program) {
19
+ const device = program
20
+ .command('device')
21
+ .description('Device commands (subcommand-first: thinr device <action> <deviceId>)');
22
+
23
+ registerListCommand(device);
24
+ registerProxyCommands(device);
25
+ registerConsoleCommand(device);
26
+ registerStatusCommand(device);
27
+ registerPropertyCommand(device);
28
+ registerResourceCommand(device);
29
+ registerExecCommand(device);
30
+ registerUpdateCommand(device);
31
+ }
@@ -0,0 +1,43 @@
1
+ // @ts-check
2
+ import chalk from 'chalk';
3
+ import { getDevices } from '../../lib/devices.js';
4
+ import { isJsonMode, printOk, printErr, createSpinner, classifyError } from '../../lib/output.js';
5
+ import { applyJsonFlag, ensureConfigured, getGlobalUser } from './_shared.js';
6
+
7
+ export function registerListCommand(device) {
8
+ device
9
+ .command('list [pattern]')
10
+ .description(
11
+ 'List devices, optionally filtered by name/id pattern (case-insensitive regex)',
12
+ )
13
+ .option('-j, --json', 'Output as JSON')
14
+ .action(async (pattern, opts, cmd) => {
15
+ applyJsonFlag(opts);
16
+ ensureConfigured();
17
+ const user = getGlobalUser(cmd);
18
+ const filter = pattern ? { name: pattern } : {};
19
+ const label = pattern
20
+ ? `Searching devices matching "${pattern}"...`
21
+ : 'Fetching devices...';
22
+ const spinner = createSpinner(label).start();
23
+ try {
24
+ const devices = await getDevices(filter, user);
25
+ spinner.succeed(`Found ${devices.length} device(s)`);
26
+ if (isJsonMode()) {
27
+ printOk(devices);
28
+ } else {
29
+ for (const d of devices) {
30
+ const online = d.connection?.active
31
+ ? chalk.green('online ')
32
+ : chalk.gray('offline');
33
+ const name = d.name ? chalk.gray(` (${d.name})`) : '';
34
+ console.log(` ${online} ${chalk.bold(d.device)}${name}`);
35
+ }
36
+ }
37
+ } catch (error) {
38
+ spinner.fail('Failed to list devices');
39
+ const { message, code } = classifyError(error);
40
+ printErr(message, { code });
41
+ }
42
+ });
43
+ }
@@ -0,0 +1,50 @@
1
+ // @ts-check
2
+ import {
3
+ formatDeviceProperty,
4
+ getDeviceProperty,
5
+ getDeviceProperties,
6
+ formatDeviceProperties,
7
+ } from '../../lib/property.js';
8
+ import { isJsonMode, printOk, printErr, createSpinner, classifyError } from '../../lib/output.js';
9
+ import { applyJsonFlag, ensureConfigured } from './_shared.js';
10
+
11
+ export function registerPropertyCommand(device) {
12
+ device
13
+ .command('property <deviceId> [propertyId]')
14
+ .description('List device properties, or read one by ID')
15
+ .option('-j, --json', 'Output as JSON')
16
+ .option('-f, --field <field>', 'Extract a sub-field (dot path, e.g. data.value)')
17
+ .action(async (deviceId, propertyId, opts) => {
18
+ applyJsonFlag(opts);
19
+ ensureConfigured();
20
+ if (!propertyId) {
21
+ const spinner = createSpinner(`Getting properties for ${deviceId}...`).start();
22
+ try {
23
+ const result = await getDeviceProperties(deviceId);
24
+ spinner.succeed(`Properties for ${deviceId} retrieved successfully`);
25
+ if (isJsonMode()) printOk(result);
26
+ else console.log(formatDeviceProperties(deviceId, result));
27
+ } catch (error) {
28
+ spinner.fail('Property retrieval failed');
29
+ const { message, code } = classifyError(error);
30
+ printErr(message, { code });
31
+ }
32
+ return;
33
+ }
34
+ const spinner = createSpinner(`Retrieving property for ${deviceId}...`).start();
35
+ try {
36
+ const property = await getDeviceProperty(deviceId, propertyId);
37
+ spinner.succeed(`Property ${propertyId} for ${deviceId} found`);
38
+ const value = opts.field
39
+ ? opts.field.split('.').reduce((obj, key) => obj && obj[key], property)
40
+ : property;
41
+ if (isJsonMode()) printOk(value);
42
+ else if (opts.field) console.log(value);
43
+ else console.log(formatDeviceProperty(deviceId, propertyId, property));
44
+ } catch (error) {
45
+ spinner.fail('Property retrieval failed');
46
+ const { message, code } = classifyError(error);
47
+ printErr(message, { code });
48
+ }
49
+ });
50
+ }
@@ -0,0 +1,42 @@
1
+ // @ts-check
2
+ import { handleProxyAction } from '../../lib/proxy.js';
3
+ import { ensureConfigured, runInteractive } from './_shared.js';
4
+
5
+ export function registerProxyCommands(device) {
6
+ device
7
+ .command('tcp <deviceId> [target]')
8
+ .description('Create a TCP proxy (no TLS). Default target: 22.')
9
+ .option('-p, --port <port>', 'Local port to use (default: random)')
10
+ .action((deviceId, target, opts) =>
11
+ runInteractive(() => {
12
+ ensureConfigured();
13
+ return handleProxyAction(deviceId, 'tcp', target, { port: opts.port });
14
+ }),
15
+ );
16
+
17
+ device
18
+ .command('tls <deviceId> [target]')
19
+ .description('Create a TLS proxy. Default target: 443.')
20
+ .option('-p, --port <port>', 'Local port to use (default: random)')
21
+ .action((deviceId, target, opts) =>
22
+ runInteractive(() => {
23
+ ensureConfigured();
24
+ return handleProxyAction(deviceId, 'tls', target, { port: opts.port });
25
+ }),
26
+ );
27
+
28
+ device
29
+ .command('http <deviceId> [target]')
30
+ .description('Create an HTTP proxy. Default target: 80.')
31
+ .option('-p, --port <port>', 'Local port to use (default: random)')
32
+ .option('--no-open', 'Do not open the browser')
33
+ .action((deviceId, target, opts) =>
34
+ runInteractive(() => {
35
+ ensureConfigured();
36
+ return handleProxyAction(deviceId, 'http', target, {
37
+ port: opts.port,
38
+ openBrowser: opts.open,
39
+ });
40
+ }),
41
+ );
42
+ }
@@ -0,0 +1,51 @@
1
+ // @ts-check
2
+ import {
3
+ callDeviceResource,
4
+ listDeviceResourcesWithSchemas,
5
+ formatDeviceResourcesWithSchemas,
6
+ } from '../../lib/resource.js';
7
+ import { isJsonMode, printOk, printErr, createSpinner, classifyError } from '../../lib/output.js';
8
+ import { applyJsonFlag, collectInput, ensureConfigured } from './_shared.js';
9
+
10
+ export function registerResourceCommand(device) {
11
+ device
12
+ .command('resource <deviceId> [resource]')
13
+ .description('List device resources, or call one by name')
14
+ .option('-j, --json', 'Output as JSON')
15
+ .option('-f, --field <field>', 'Extract a sub-field from the result (dot path)')
16
+ .option('-i, --input <key=value>', 'Resource input (repeatable)', collectInput, {})
17
+ .action(async (deviceId, resource, opts) => {
18
+ applyJsonFlag(opts);
19
+ ensureConfigured();
20
+ if (!resource) {
21
+ const spinner = createSpinner(`Getting resources for ${deviceId}...`).start();
22
+ try {
23
+ const entries = await listDeviceResourcesWithSchemas(deviceId);
24
+ spinner.succeed(`Successfully retrieved resources for ${deviceId}`);
25
+ if (isJsonMode()) printOk(entries);
26
+ else console.log(formatDeviceResourcesWithSchemas(deviceId, entries));
27
+ } catch (error) {
28
+ spinner.fail('Resource retrieval failed');
29
+ const { message, code } = classifyError(error);
30
+ printErr(message, { code });
31
+ }
32
+ return;
33
+ }
34
+ const spinner = createSpinner(
35
+ `Calling resource ${resource} for ${deviceId}...`,
36
+ ).start();
37
+ try {
38
+ const result = await callDeviceResource(deviceId, resource, opts.input);
39
+ spinner.succeed(`Resource ${resource} returned successfully for ${deviceId}`);
40
+ const value = opts.field
41
+ ? opts.field.split('.').reduce((obj, key) => obj && obj[key], result)
42
+ : result;
43
+ if (isJsonMode()) printOk(value);
44
+ else console.log(value);
45
+ } catch (error) {
46
+ spinner.fail('Resource call failed');
47
+ const { message, code } = classifyError(error);
48
+ printErr(message, { code });
49
+ }
50
+ });
51
+ }
@@ -0,0 +1,26 @@
1
+ // @ts-check
2
+ import { getDeviceStatus, formatDeviceStatus } from '../../lib/status.js';
3
+ import { isJsonMode, printOk, printErr, createSpinner, classifyError } from '../../lib/output.js';
4
+ import { applyJsonFlag, ensureConfigured } from './_shared.js';
5
+
6
+ export function registerStatusCommand(device) {
7
+ device
8
+ .command('status <deviceId>')
9
+ .description('Check device status and stats')
10
+ .option('-j, --json', 'Output as JSON')
11
+ .action(async (deviceId, opts) => {
12
+ applyJsonFlag(opts);
13
+ ensureConfigured();
14
+ const spinner = createSpinner(`Checking status of ${deviceId}...`).start();
15
+ try {
16
+ const status = await getDeviceStatus(deviceId);
17
+ spinner.succeed(`Device ${deviceId} found`);
18
+ if (isJsonMode()) printOk(status);
19
+ else console.log(formatDeviceStatus(deviceId, status));
20
+ } catch (error) {
21
+ spinner.fail('Status check failed');
22
+ const { message, code } = classifyError(error);
23
+ printErr(message, { code });
24
+ }
25
+ });
26
+ }
@@ -0,0 +1,53 @@
1
+ // @ts-check
2
+ import chalk from 'chalk';
3
+ import { createDeviceAPI } from '../../lib/device-api.js';
4
+ import { isJsonMode, printOk, printErr, createSpinner, classifyError } from '../../lib/output.js';
5
+ import { applyJsonFlag, ensureConfigured, getGlobalUser } from './_shared.js';
6
+
7
+ const runUpdate = async (action, deviceId, opts, cmd) => {
8
+ applyJsonFlag(opts);
9
+ ensureConfigured();
10
+ const user = getGlobalUser(cmd);
11
+ const channel = opts.channel || 'latest';
12
+ const label = action === 'check' ? 'Checking for updates' : 'Applying update';
13
+ const spinner = createSpinner(`${label} on ${deviceId}...`).start();
14
+ try {
15
+ const api = createDeviceAPI(deviceId, { user });
16
+ const timeout = action === 'apply' ? 300000 : 30000;
17
+ const result = await api.callResource('update', { action, channel }, { timeout });
18
+ spinner.succeed(`${label} finished`);
19
+ if (isJsonMode()) {
20
+ printOk(result);
21
+ } else if (result && typeof result === 'object') {
22
+ if (result.current) console.log(` current: ${chalk.bold(result.current)}`);
23
+ if (result.latest) console.log(` latest: ${chalk.bold(result.latest)}`);
24
+ if (result.arch) console.log(` arch: ${result.arch}`);
25
+ if (result.status) console.log(` status: ${chalk.cyan(result.status)}`);
26
+ if (result.message) console.log(` message: ${result.message}`);
27
+ } else {
28
+ console.log(result);
29
+ }
30
+ } catch (error) {
31
+ spinner.fail(`${label} failed`);
32
+ const { message, code } = classifyError(error);
33
+ printErr(message, { code });
34
+ }
35
+ };
36
+
37
+ export function registerUpdateCommand(device) {
38
+ const update = device.command('update').description('Check for or apply agent updates');
39
+
40
+ update
41
+ .command('check <deviceId>')
42
+ .description('Check if an agent update is available')
43
+ .option('-j, --json', 'Output as JSON')
44
+ .option('--channel <name>', 'Update channel (default: latest)')
45
+ .action((deviceId, opts, cmd) => runUpdate('check', deviceId, opts, cmd));
46
+
47
+ update
48
+ .command('apply <deviceId>')
49
+ .description('Apply an agent update')
50
+ .option('-j, --json', 'Output as JSON')
51
+ .option('--channel <name>', 'Update channel (default: latest)')
52
+ .action((deviceId, opts, cmd) => runUpdate('apply', deviceId, opts, cmd));
53
+ }
@@ -1,7 +1,6 @@
1
1
  import chalk from 'chalk';
2
2
  import ora from 'ora';
3
3
  import { deleteConfig, configExists } from '../lib/config.js';
4
- import { deleteToken } from '../lib/auth.js';
5
4
 
6
5
  /**
7
6
  * Register the logout command
@@ -29,13 +28,21 @@ export function logoutCommand(program) {
29
28
  spinner.succeed('Logged out successfully. Configuration removed.');
30
29
  } else {
31
30
  spinner.fail('Failed to remove configuration.');
32
- console.error(chalk.red('Error: Unable to delete configuration file. You may need to remove it manually.'));
33
- console.error(chalk.gray('Configuration is stored in ~/.config/thinr-cli/config.json'));
31
+ console.error(
32
+ chalk.red(
33
+ 'Error: Unable to delete configuration file. You may need to remove it manually.',
34
+ ),
35
+ );
36
+ console.error(
37
+ chalk.gray('Configuration is stored in ~/.config/thinr-cli/config.json'),
38
+ );
34
39
  }
35
40
  } catch (error) {
36
41
  spinner.fail('Failed to remove configuration.');
37
42
  console.error(chalk.red(`Error: ${error.message}`));
38
- console.error(chalk.gray('Configuration is stored in ~/.config/thinr-cli/config.json'));
43
+ console.error(
44
+ chalk.gray('Configuration is stored in ~/.config/thinr-cli/config.json'),
45
+ );
39
46
  }
40
47
  });
41
- }
48
+ }