@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.
- package/CHANGELOG.md +90 -0
- package/README.md +362 -87
- package/bin/thinr.js +51 -11
- package/commands/device/_shared.js +48 -0
- package/commands/device/console.js +15 -0
- package/commands/device/exec.js +76 -0
- package/commands/device/index.js +31 -0
- package/commands/device/list.js +43 -0
- package/commands/device/property.js +50 -0
- package/commands/device/proxy.js +42 -0
- package/commands/device/resource.js +51 -0
- package/commands/device/status.js +26 -0
- package/commands/device/update.js +53 -0
- package/commands/logout.js +12 -5
- package/commands/product.js +146 -177
- package/commands/profile.js +105 -0
- package/lib/api.js +54 -31
- package/lib/auth.js +225 -309
- package/lib/config.js +176 -45
- package/lib/console.js +87 -108
- package/lib/device-api.js +257 -0
- package/lib/devices.js +69 -0
- package/lib/errors.js +53 -0
- package/lib/mcp/helpers.js +34 -0
- package/lib/mcp/registry.js +32 -0
- package/lib/mcp/server.js +63 -0
- package/lib/mcp/tools-devices.js +194 -0
- package/lib/mcp/tools-exec.js +213 -0
- package/lib/mcp/tools-filesystem.js +163 -0
- package/lib/mcp/tools-products.js +232 -0
- package/lib/mcp/tools-profiles.js +30 -0
- package/lib/mcp/tools-properties.js +79 -0
- package/lib/mcp/tools-resources.js +76 -0
- package/lib/mcp/tools-scripts.js +172 -0
- package/lib/monitoring.js +72 -0
- package/lib/output.js +114 -0
- package/lib/product.js +373 -0
- package/lib/property.js +41 -57
- package/lib/proxy.js +58 -90
- package/lib/resource.js +106 -72
- package/lib/status.js +7 -33
- package/lib/storage.js +128 -0
- package/package.json +68 -37
- package/.github/workflows/publish.yml +0 -79
- package/commands/device.js +0 -268
- package/lib/device.js +0 -76
package/lib/storage.js
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import api from './api.js';
|
|
2
|
+
import { apiError } from './errors.js';
|
|
3
|
+
import { requireConfig } from './config.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Helpers for ThinRemote file storages — independent buckets that the
|
|
7
|
+
* platform exposes alongside products. The product script workflow
|
|
8
|
+
* uses them to host the actual JS files; storages are also useful on
|
|
9
|
+
* their own (config blobs, firmware artefacts, etc.).
|
|
10
|
+
*
|
|
11
|
+
* Functions return raw data and throw `apiError` on failure, except
|
|
12
|
+
* the `*Exists` and `delete*` helpers which use boolean return values
|
|
13
|
+
* so orchestration code can stay branch-light.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
function resolveUser(user) {
|
|
17
|
+
return user || requireConfig().username;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function v1(user) {
|
|
21
|
+
return `/v1/users/${resolveUser(user)}`;
|
|
22
|
+
}
|
|
23
|
+
function v2(user) {
|
|
24
|
+
return `/v2/users/${resolveUser(user)}`;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function getStorage(storageId, user) {
|
|
28
|
+
try {
|
|
29
|
+
const res = await api.get(`${v1(user)}/storages/${storageId}`);
|
|
30
|
+
return res.data || {};
|
|
31
|
+
} catch (e) {
|
|
32
|
+
throw apiError(e, { notFound: `Storage not found: ${storageId}` });
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export async function storageExists(storageId, user) {
|
|
37
|
+
try {
|
|
38
|
+
await api.get(`${v1(user)}/storages/${storageId}`);
|
|
39
|
+
return true;
|
|
40
|
+
} catch (e) {
|
|
41
|
+
if (e.response?.status === 404) return false;
|
|
42
|
+
throw apiError(e);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export async function createStorage(
|
|
47
|
+
storageId,
|
|
48
|
+
user,
|
|
49
|
+
{ name, description, public: isPublic = false } = {},
|
|
50
|
+
) {
|
|
51
|
+
try {
|
|
52
|
+
await api.post(`${v1(user)}/storages`, {
|
|
53
|
+
storage: storageId,
|
|
54
|
+
name: name || storageId,
|
|
55
|
+
description: description || `Storage for ${storageId}`,
|
|
56
|
+
public: isPublic,
|
|
57
|
+
});
|
|
58
|
+
} catch (e) {
|
|
59
|
+
throw apiError(e);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export async function deleteStorage(storageId, user) {
|
|
64
|
+
try {
|
|
65
|
+
await api.delete(`${v1(user)}/storages/${storageId}`);
|
|
66
|
+
return true;
|
|
67
|
+
} catch (e) {
|
|
68
|
+
if (e.response?.status === 404) return false;
|
|
69
|
+
throw apiError(e);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export async function listStorageFiles(storageId, user) {
|
|
74
|
+
try {
|
|
75
|
+
const res = await api.get(`${v1(user)}/storages/${storageId}/files`);
|
|
76
|
+
return res.data || [];
|
|
77
|
+
} catch (e) {
|
|
78
|
+
if (e.response?.status === 404) return [];
|
|
79
|
+
throw apiError(e);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Read a file as raw text (no JSON parsing). The agent stores script
|
|
85
|
+
* bodies as `text/plain` — parsing them as JSON would corrupt them.
|
|
86
|
+
*/
|
|
87
|
+
export async function readStorageFile(storageId, filePath, user) {
|
|
88
|
+
try {
|
|
89
|
+
const res = await api.get(`${v2(user)}/storages/${storageId}/files/${filePath}`, {
|
|
90
|
+
transformResponse: [(x) => x],
|
|
91
|
+
});
|
|
92
|
+
return res.data;
|
|
93
|
+
} catch (e) {
|
|
94
|
+
throw apiError(e, { notFound: `File not found in storage "${storageId}": ${filePath}` });
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export async function storageFileExists(storageId, filePath, user) {
|
|
99
|
+
try {
|
|
100
|
+
await api.get(`${v2(user)}/storages/${storageId}/files/${filePath}`);
|
|
101
|
+
return true;
|
|
102
|
+
} catch (e) {
|
|
103
|
+
if (e.response?.status === 404) return false;
|
|
104
|
+
throw apiError(e);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export async function writeStorageFile(storageId, filePath, content, user) {
|
|
109
|
+
try {
|
|
110
|
+
await api.put(
|
|
111
|
+
`${v2(user)}/storages/${storageId}/files/${filePath}?overwrite=true`,
|
|
112
|
+
content,
|
|
113
|
+
{ headers: { 'Content-Type': 'text/plain' } },
|
|
114
|
+
);
|
|
115
|
+
} catch (e) {
|
|
116
|
+
throw apiError(e);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export async function deleteStorageFile(storageId, filePath, user) {
|
|
121
|
+
try {
|
|
122
|
+
await api.delete(`${v2(user)}/storages/${storageId}/files/${filePath}`);
|
|
123
|
+
return true;
|
|
124
|
+
} catch (e) {
|
|
125
|
+
if (e.response?.status === 404) return false;
|
|
126
|
+
throw apiError(e);
|
|
127
|
+
}
|
|
128
|
+
}
|
package/package.json
CHANGED
|
@@ -1,39 +1,70 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
"
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
"
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
2
|
+
"name": "@thinremote/thinr-cli",
|
|
3
|
+
"version": "1.1.0-alpha.1",
|
|
4
|
+
"description": "CLI for ThinRemote — remote management for IoT devices, with an MCP server for AI clients",
|
|
5
|
+
"main": "bin/thinr.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"thinr": "bin/thinr.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin",
|
|
11
|
+
"commands",
|
|
12
|
+
"lib",
|
|
13
|
+
"CHANGELOG.md",
|
|
14
|
+
"LICENSE",
|
|
15
|
+
"README.md"
|
|
16
|
+
],
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "https://github.com/Thin-Remote/thinr-cli.git"
|
|
20
|
+
},
|
|
21
|
+
"bugs": "https://github.com/Thin-Remote/thinr-cli/issues",
|
|
22
|
+
"homepage": "https://github.com/Thin-Remote/thinr-cli#readme",
|
|
23
|
+
"scripts": {
|
|
24
|
+
"start": "node bin/thinr.js",
|
|
25
|
+
"lint": "eslint .",
|
|
26
|
+
"lint:fix": "eslint . --fix",
|
|
27
|
+
"format": "prettier --write .",
|
|
28
|
+
"format:check": "prettier --check .",
|
|
29
|
+
"typecheck": "tsc",
|
|
30
|
+
"test": "node --test 'tests/**/*.test.js'"
|
|
31
|
+
},
|
|
32
|
+
"keywords": [
|
|
33
|
+
"cli",
|
|
34
|
+
"iot",
|
|
35
|
+
"remote",
|
|
36
|
+
"management",
|
|
37
|
+
"thinremote",
|
|
38
|
+
"thinr",
|
|
39
|
+
"thinger",
|
|
40
|
+
"mcp"
|
|
41
|
+
],
|
|
42
|
+
"author": "ThinRemote",
|
|
43
|
+
"license": "MIT",
|
|
44
|
+
"dependencies": {
|
|
45
|
+
"@inquirer/prompts": "^7.5.3",
|
|
46
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
47
|
+
"axios": "^1.10.0",
|
|
48
|
+
"chalk": "^5.4.1",
|
|
49
|
+
"commander": "^14.0.0",
|
|
50
|
+
"conf": "^14.0.0",
|
|
51
|
+
"figlet": "^1.8.1",
|
|
52
|
+
"jsonwebtoken": "^9.0.2",
|
|
53
|
+
"open": "^10.1.2",
|
|
54
|
+
"ora": "^8.2.0",
|
|
55
|
+
"ws": "^8.18.2"
|
|
56
|
+
},
|
|
57
|
+
"type": "module",
|
|
58
|
+
"engines": {
|
|
59
|
+
"node": ">=18"
|
|
60
|
+
},
|
|
61
|
+
"devDependencies": {
|
|
62
|
+
"@eslint/js": "^10.0.1",
|
|
63
|
+
"@types/node": "^25.6.0",
|
|
64
|
+
"eslint": "^10.2.0",
|
|
65
|
+
"eslint-config-prettier": "^10.1.8",
|
|
66
|
+
"globals": "^17.5.0",
|
|
67
|
+
"prettier": "^3.8.3",
|
|
68
|
+
"typescript": "^6.0.2"
|
|
69
|
+
}
|
|
39
70
|
}
|
|
@@ -1,79 +0,0 @@
|
|
|
1
|
-
name: "publish"
|
|
2
|
-
|
|
3
|
-
on:
|
|
4
|
-
push:
|
|
5
|
-
branches: [ main, develop ]
|
|
6
|
-
|
|
7
|
-
jobs:
|
|
8
|
-
publish:
|
|
9
|
-
name: publish
|
|
10
|
-
runs-on: ubuntu-latest
|
|
11
|
-
|
|
12
|
-
steps:
|
|
13
|
-
|
|
14
|
-
- name: Extract branch name
|
|
15
|
-
shell: bash
|
|
16
|
-
run: echo "branch=${GITHUB_REF#refs/heads/}" >> $GITHUB_ENV
|
|
17
|
-
|
|
18
|
-
- name: checkout
|
|
19
|
-
uses: actions/checkout@v4
|
|
20
|
-
with:
|
|
21
|
-
fetch-depth: 0
|
|
22
|
-
ref: ${{ env.branch }}
|
|
23
|
-
|
|
24
|
-
- name: node
|
|
25
|
-
uses: actions/setup-node@v4
|
|
26
|
-
with:
|
|
27
|
-
node-version: 22
|
|
28
|
-
registry-url: https://registry.npmjs.org
|
|
29
|
-
|
|
30
|
-
- name: Get tag version
|
|
31
|
-
run: |
|
|
32
|
-
echo "tag_version=`cat package.json | jq '.version' | tr -d '"'`" >> $GITHUB_ENV
|
|
33
|
-
|
|
34
|
-
- name: Check if tag is for develop and if it already exists
|
|
35
|
-
run: |
|
|
36
|
-
git fetch --prune --tags
|
|
37
|
-
if [ ${{ env.branch }} == 'develop' ]; then
|
|
38
|
-
for i in {1..100}; do
|
|
39
|
-
if [ `git tag --list | grep ${{ env.tag_version }}"-alpha.${i}" | wc -l` -eq 0 ]; then
|
|
40
|
-
tag_version=${{ env.tag_version }}"-alpha.${i}"
|
|
41
|
-
echo "tag_version=${tag_version}" >> $GITHUB_ENV
|
|
42
|
-
echo "npm_tag_version=develop" >> $GITHUB_ENV
|
|
43
|
-
break;
|
|
44
|
-
fi
|
|
45
|
-
done
|
|
46
|
-
elif [ ${{ env.branch }} == 'main' ]; then
|
|
47
|
-
echo "npm_tag_version=latest" >> $GITHUB_ENV
|
|
48
|
-
elif [ `git tag --list | grep -q ${{ env.tag_version }}` -ne 0 ]; then exit 1; fi
|
|
49
|
-
|
|
50
|
-
- name: update version
|
|
51
|
-
run: |
|
|
52
|
-
npm version --no-git-tag-version --allow-same-version ${{ env.tag_version }}
|
|
53
|
-
|
|
54
|
-
- name: publish
|
|
55
|
-
run: npm publish --tag ${{ env.npm_tag_version }} --access public
|
|
56
|
-
env:
|
|
57
|
-
NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}}
|
|
58
|
-
|
|
59
|
-
- name: Get changelog body
|
|
60
|
-
run: |
|
|
61
|
-
awk '/^## \[[0-9]+/,EOF' CHANGELOG.md | awk '(NR>1)' | awk '1;/^## \[[0-9]+/{exit}' | sed '$d' > release_body.md
|
|
62
|
-
|
|
63
|
-
- name: Set pre-release boolean
|
|
64
|
-
run: |
|
|
65
|
-
prerelease=false
|
|
66
|
-
if [ ${{ env.branch }} == 'develop' ]; then
|
|
67
|
-
prerelease=true
|
|
68
|
-
fi
|
|
69
|
-
echo "prerelease=$prerelease" >> $GITHUB_ENV
|
|
70
|
-
|
|
71
|
-
- name: Create GitHub release
|
|
72
|
-
uses: ncipollo/release-action@v1
|
|
73
|
-
with:
|
|
74
|
-
prerelease: ${{ env.prerelease }}
|
|
75
|
-
commit: ${{ env.branch }}
|
|
76
|
-
tag: ${{ env.tag_version }}
|
|
77
|
-
name: ${{ env.tag_version }}
|
|
78
|
-
token: ${{ secrets.GITHUB_TOKEN }}
|
|
79
|
-
bodyFile: "release_body.md"
|
package/commands/device.js
DELETED
|
@@ -1,268 +0,0 @@
|
|
|
1
|
-
import { handleProxyAction } from '../lib/proxy.js';
|
|
2
|
-
import { connectToDeviceConsole } from '../lib/console.js';
|
|
3
|
-
import { getDeviceStatus, formatDeviceStatus } from '../lib/status.js';
|
|
4
|
-
import { formatDeviceProperty, getDeviceProperty, getDeviceProperties, formatDeviceProperties } from '../lib/property.js';
|
|
5
|
-
import chalk from 'chalk';
|
|
6
|
-
import ora from 'ora';
|
|
7
|
-
import { configExists } from '../lib/config.js';
|
|
8
|
-
import { getDeviceResources, executeDeviceResource, formatDeviceResources } from "../lib/resource.js";
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Register the device command with all device-related subcommands
|
|
13
|
-
* @param {Command} program - Commander program instance
|
|
14
|
-
*/
|
|
15
|
-
export function deviceCommand(program) {
|
|
16
|
-
|
|
17
|
-
const device = program.command('device').description('Device commands');
|
|
18
|
-
|
|
19
|
-
device
|
|
20
|
-
.argument('<deviceId>', 'ID of the device to manage')
|
|
21
|
-
.description('Manage device connections and proxies')
|
|
22
|
-
.allowUnknownOption(true)
|
|
23
|
-
.allowExcessArguments(true)
|
|
24
|
-
.action(async (deviceId, options, command) => {
|
|
25
|
-
// Get additional arguments manually
|
|
26
|
-
const args = command.args.slice(1); // Skip deviceId
|
|
27
|
-
const action = args[0];
|
|
28
|
-
const target = args[1] && !args[1].startsWith('-') ? args[1] : null; // Check if target is provided, skip if it's an option
|
|
29
|
-
|
|
30
|
-
// Check if configured
|
|
31
|
-
if (!configExists()) {
|
|
32
|
-
console.error(chalk.red('Error: Not configured. Run thinr without parameters to set up.'));
|
|
33
|
-
process.exit(1);
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
// If no action provided, show help
|
|
37
|
-
if (!action) {
|
|
38
|
-
printHelp(deviceId);
|
|
39
|
-
return;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
// Parse options manually from process.argv
|
|
43
|
-
const parsedOptions = {
|
|
44
|
-
port: null,
|
|
45
|
-
json: false,
|
|
46
|
-
openBrowser: true,
|
|
47
|
-
inputs: {},
|
|
48
|
-
field: "",
|
|
49
|
-
};
|
|
50
|
-
|
|
51
|
-
const argv = process.argv;
|
|
52
|
-
for (let i = 0; i < argv.length; i++) {
|
|
53
|
-
if (argv[i] === '-p' || argv[i] === '--port') {
|
|
54
|
-
parsedOptions.port = argv[i + 1];
|
|
55
|
-
i++; // Skip the value
|
|
56
|
-
} else if (argv[i] === '-j' || argv[i] === '--json') {
|
|
57
|
-
parsedOptions.json = true;
|
|
58
|
-
} else if (argv[i] === '--no-open') {
|
|
59
|
-
parsedOptions.open = false;
|
|
60
|
-
} else if (argv[i] === '-i' || argv[i] === '--input') {
|
|
61
|
-
let inputPair = argv[i + 1].split('=');
|
|
62
|
-
parsedOptions.inputs[inputPair[0]] = inputPair[1];
|
|
63
|
-
} else if (argv[i] === '-f' || argv[i] === '--field') {
|
|
64
|
-
parsedOptions.field = argv[i + 1];
|
|
65
|
-
} else if (argv[i] === '-h' || argv[i] === '--help') {
|
|
66
|
-
printHelp(deviceId);
|
|
67
|
-
return;
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
switch (action.toLowerCase()) {
|
|
72
|
-
case 'tcp':
|
|
73
|
-
await handleProxyAction(deviceId, 'tcp', target, parsedOptions);
|
|
74
|
-
break;
|
|
75
|
-
|
|
76
|
-
case 'tls':
|
|
77
|
-
await handleProxyAction(deviceId, 'tls', target, parsedOptions);
|
|
78
|
-
break;
|
|
79
|
-
|
|
80
|
-
case 'http':
|
|
81
|
-
await handleProxyAction(deviceId, 'http', target, parsedOptions);
|
|
82
|
-
break;
|
|
83
|
-
|
|
84
|
-
case 'console':
|
|
85
|
-
try {
|
|
86
|
-
await connectToDeviceConsole(deviceId);
|
|
87
|
-
} catch (error) {
|
|
88
|
-
console.error(chalk.red(`Error: ${error.message}`));
|
|
89
|
-
process.exit(1);
|
|
90
|
-
}
|
|
91
|
-
break;
|
|
92
|
-
|
|
93
|
-
case 'status': {
|
|
94
|
-
const spinner = ora(`Checking status of ${deviceId}...`).start();
|
|
95
|
-
try {
|
|
96
|
-
const status = await getDeviceStatus(deviceId);
|
|
97
|
-
spinner.succeed(`Device ${deviceId} found`);
|
|
98
|
-
|
|
99
|
-
if (parsedOptions.json) {
|
|
100
|
-
console.log(JSON.stringify(status, null, 2));
|
|
101
|
-
} else {
|
|
102
|
-
console.log(formatDeviceStatus(deviceId, status));
|
|
103
|
-
}
|
|
104
|
-
} catch (error) {
|
|
105
|
-
spinner.fail(`Status check failed`);
|
|
106
|
-
console.error(chalk.red(`Error: ${error.message}`));
|
|
107
|
-
process.exit(1);
|
|
108
|
-
}
|
|
109
|
-
break;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
case 'property': {
|
|
113
|
-
|
|
114
|
-
if ( !target ) {
|
|
115
|
-
const spinner = ora(`Getting properties for ${deviceId}...`).start();
|
|
116
|
-
try {
|
|
117
|
-
const result = await getDeviceProperties(deviceId);
|
|
118
|
-
spinner.succeed(`Properties for ${deviceId} retrieved successfully`);
|
|
119
|
-
if (parsedOptions.json) {
|
|
120
|
-
console.log(result);
|
|
121
|
-
} else {
|
|
122
|
-
console.log(chalk.green(`Resource result for device ${deviceId}:`));
|
|
123
|
-
console.log(formatDeviceProperties(deviceId, result));
|
|
124
|
-
}
|
|
125
|
-
} catch (error) {
|
|
126
|
-
spinner.fail(`Property retrieval failed`);
|
|
127
|
-
console.error(chalk.red(`Error: ${error.message}`));
|
|
128
|
-
process.exit(1);
|
|
129
|
-
}
|
|
130
|
-
} else {
|
|
131
|
-
const spinner = ora(`Retrieving property for ${deviceId}...`).start();
|
|
132
|
-
try {
|
|
133
|
-
const property = await getDeviceProperty(deviceId, target);
|
|
134
|
-
spinner.succeed(`Property ${target} for ${deviceId} found`);
|
|
135
|
-
|
|
136
|
-
if (parsedOptions.json) {
|
|
137
|
-
if ( parsedOptions.field ) {
|
|
138
|
-
console.log(parsedOptions.field.split('.').reduce((obj, key) => obj && obj[key], property));
|
|
139
|
-
} else {
|
|
140
|
-
console.log(JSON.stringify(property, null, 2));
|
|
141
|
-
}
|
|
142
|
-
} else {
|
|
143
|
-
console.log(formatDeviceProperty(deviceId, target, property));
|
|
144
|
-
}
|
|
145
|
-
} catch (error) {
|
|
146
|
-
spinner.fail(`Property retrieval failed`);
|
|
147
|
-
console.error(chalk.red(`Error: ${error.message}`));
|
|
148
|
-
process.exit(1);
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
break;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
case 'resource': {
|
|
157
|
-
|
|
158
|
-
if (!target) {
|
|
159
|
-
const spinner = ora(`Getting resources for ${deviceId}...`).start();
|
|
160
|
-
try {
|
|
161
|
-
|
|
162
|
-
const result = await getDeviceResources(deviceId);
|
|
163
|
-
spinner.succeed(`Succesfully retrieved resources for ${deviceId}`);
|
|
164
|
-
|
|
165
|
-
if (parsedOptions.json) {
|
|
166
|
-
console.log(result);
|
|
167
|
-
} else {
|
|
168
|
-
//console.log(chalk.green(`Resource ${target} result for device ${deviceId}:`));
|
|
169
|
-
console.log(chalk.green(`Resource result for device ${deviceId}:`));
|
|
170
|
-
console.log(formatDeviceResources(deviceId, result));
|
|
171
|
-
}
|
|
172
|
-
} catch (error) {
|
|
173
|
-
spinner.fail(`Resource retrieval failed`);
|
|
174
|
-
console.error(chalk.red(`Error: ${error.message}`));
|
|
175
|
-
process.exit(1);
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
} else {
|
|
179
|
-
const spinner = ora(`Executing resource ${target} for ${deviceId}...`).start();
|
|
180
|
-
try {
|
|
181
|
-
|
|
182
|
-
const result = await executeDeviceResource(deviceId, target, parsedOptions.inputs);
|
|
183
|
-
spinner.succeed(`Resource ${target} executed successfully for ${deviceId}`);
|
|
184
|
-
|
|
185
|
-
if ( parsedOptions.field ) {
|
|
186
|
-
console.log(parsedOptions.field.split('.').reduce((obj, key) => obj && obj[key], result));
|
|
187
|
-
} else {
|
|
188
|
-
console.log(result);
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
} catch (error) {
|
|
192
|
-
spinner.fail(`Resource execution failed`);
|
|
193
|
-
console.error(chalk.red(`Error: ${error.message}`));
|
|
194
|
-
process.exit(1);
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
break;
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
default:
|
|
202
|
-
console.error(chalk.red(`Error: Unknown action '${action}'. Available actions: tcp, tls, http, console, status, property, resource.\n`));
|
|
203
|
-
printHelp(deviceId);
|
|
204
|
-
process.exit(1);
|
|
205
|
-
}
|
|
206
|
-
});
|
|
207
|
-
|
|
208
|
-
}
|
|
209
|
-
function printHelp(deviceId) {
|
|
210
|
-
console.log(`Usage: thinr device ${deviceId} <command> [options]
|
|
211
|
-
|
|
212
|
-
Available commands for device "${deviceId}":
|
|
213
|
-
|
|
214
|
-
tcp [target] Create a TCP proxy (no TLS)
|
|
215
|
-
Example: thinr device ${deviceId} tcp 22
|
|
216
|
-
|
|
217
|
-
tls [target] Create a TLS proxy
|
|
218
|
-
Example: thinr device ${deviceId} tls 443
|
|
219
|
-
|
|
220
|
-
http [target] Create an HTTP proxy
|
|
221
|
-
Example: thinr device ${deviceId} http 8080
|
|
222
|
-
|
|
223
|
-
console Open terminal console
|
|
224
|
-
Example: thinr device ${deviceId} console
|
|
225
|
-
|
|
226
|
-
status Check device status
|
|
227
|
-
Example: thinr device ${deviceId} status
|
|
228
|
-
|
|
229
|
-
property Get device properties
|
|
230
|
-
Example: thinr device ${deviceId} property
|
|
231
|
-
|
|
232
|
-
property [propertyId] Get a specific property of the device
|
|
233
|
-
Example: thinr device ${deviceId} property uptime
|
|
234
|
-
|
|
235
|
-
resource Get resources of the device
|
|
236
|
-
Example: thinr device ${deviceId} resource
|
|
237
|
-
|
|
238
|
-
resource [resource] Execute a specific resource of the device
|
|
239
|
-
Example: thinr device ${deviceId} resource reboot
|
|
240
|
-
|
|
241
|
-
Arguments:
|
|
242
|
-
|
|
243
|
-
target Target for proxy commands (address:port, port, or address)
|
|
244
|
-
Defaults: tcp=22, tls=443, http=80
|
|
245
|
-
|
|
246
|
-
propertyId ID of the property to retrieve (for property command)
|
|
247
|
-
|
|
248
|
-
resource Name of the resource to execute (for resource command)
|
|
249
|
-
|
|
250
|
-
Options (for proxy commands):
|
|
251
|
-
-p, --port <port> Local port to use (default: random)
|
|
252
|
-
--no-open Do not open browser (http only)
|
|
253
|
-
|
|
254
|
-
Options (for status command):
|
|
255
|
-
-j, --json Output as JSON
|
|
256
|
-
|
|
257
|
-
Options (for property command):
|
|
258
|
-
-j, --json Output as JSON
|
|
259
|
-
-f, --field <field> Field to extract from the property (e.g., -f data.value)
|
|
260
|
-
|
|
261
|
-
Options (for resource command):
|
|
262
|
-
-i, --input <input>=<value> Input for the resource (e.g., -i param1=value1 -i param2=value2)
|
|
263
|
-
-j, --json Output as JSON
|
|
264
|
-
-f, --field <field> Field to extract from the property (e.g., -f data.value)
|
|
265
|
-
|
|
266
|
-
Use "thinr device ${deviceId} <command> --help" for more details on each command.`);
|
|
267
|
-
|
|
268
|
-
}
|
package/lib/device.js
DELETED
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
import chalk from 'chalk';
|
|
2
|
-
import { readConfig } from './config.js';
|
|
3
|
-
import api from './api.js';
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Get devices belonging to a product
|
|
7
|
-
* @param {Object} filter - Filters to query devices
|
|
8
|
-
* @return {Promise<Array>} List of devices
|
|
9
|
-
*/
|
|
10
|
-
export async function getDevices(filter) {
|
|
11
|
-
// Get configuration
|
|
12
|
-
const config = readConfig();
|
|
13
|
-
|
|
14
|
-
if (!config.token || !config.server || !config.username) {
|
|
15
|
-
throw new Error('Not configured. Run thinr without parameters to set up.');
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
let devices = [];
|
|
19
|
-
|
|
20
|
-
// Query parameters to string
|
|
21
|
-
let queryParametersString = "";
|
|
22
|
-
for (const [key, value] of Object.entries(filter)) {
|
|
23
|
-
if ( value ) queryParametersString += key+"="+value+"&";
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
try {
|
|
27
|
-
const count = 50;
|
|
28
|
-
let url = `/v1/users/${config.username}/devices?count=${count}&${queryParametersString}`;
|
|
29
|
-
|
|
30
|
-
let index = 0;
|
|
31
|
-
let res_length = 0;
|
|
32
|
-
|
|
33
|
-
do {
|
|
34
|
-
// Fetch devices for the product
|
|
35
|
-
const response = await api.get(
|
|
36
|
-
`${url}index=${index}`,
|
|
37
|
-
);
|
|
38
|
-
|
|
39
|
-
res_length = response.data.length;
|
|
40
|
-
index += res_length;
|
|
41
|
-
devices = devices.concat(response.data);
|
|
42
|
-
|
|
43
|
-
} while ( res_length === count );
|
|
44
|
-
|
|
45
|
-
return devices;
|
|
46
|
-
|
|
47
|
-
} catch (error) {
|
|
48
|
-
if (error.response) {
|
|
49
|
-
// The request was made and the server responded with a status code
|
|
50
|
-
if (error.response.status === 404) {
|
|
51
|
-
throw new Error(`Product not found: ${productId}`);
|
|
52
|
-
} else if (error.response.status === 401) {
|
|
53
|
-
throw new Error('Unauthorized. Your token may have expired. Please reconfigure.');
|
|
54
|
-
} else {
|
|
55
|
-
throw new Error(`Server error: ${error.response.status} ${error.response.statusText}`);
|
|
56
|
-
}
|
|
57
|
-
} else if (error.request) {
|
|
58
|
-
// The request was made but no response was received
|
|
59
|
-
throw new Error('No response from server. Please check your connection.');
|
|
60
|
-
} else {
|
|
61
|
-
// Something happened in setting up the request that triggered an Error
|
|
62
|
-
throw new Error(`Error: ${error.message}`);
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* Filter active devices from a list of devices
|
|
70
|
-
* @param {Array} devices - List of devices to filter
|
|
71
|
-
* @return {Array} List of active devices
|
|
72
|
-
*/
|
|
73
|
-
export function filterActiveDevices(devices) {
|
|
74
|
-
return devices.filter(device => device.connection && device.connection.active === true);
|
|
75
|
-
}
|
|
76
|
-
|