@thinremote/thinr-cli 1.3.4 → 1.4.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 +86 -0
- package/README.md +117 -123
- package/bin/thinr.js +43 -105
- package/commands/_shared.js +10 -90
- package/commands/device/_shared.js +2 -2
- package/commands/device/exec.js +2 -2
- package/commands/device/fs.js +2 -2
- package/commands/device/index.js +5 -19
- package/commands/device/logs.js +4 -4
- package/commands/device/status.js +1 -1
- package/commands/device/transfer.js +2 -2
- package/commands/device/update.js +8 -2
- package/commands/fleet/upgrade.js +3 -3
- package/commands/playbook/run.js +2 -2
- package/commands/playbook/validate.js +2 -2
- package/commands/product/_shared.js +1 -1
- package/commands/product/exec.js +2 -2
- package/commands/product/fs.js +2 -2
- package/commands/product/index.js +4 -11
- package/commands/product/logs.js +2 -2
- package/commands/product/metric.js +2 -2
- package/commands/product/playbook.js +5 -5
- package/commands/product/property.js +4 -4
- package/commands/product/push.js +2 -2
- package/commands/product/resource.js +4 -4
- package/commands/product/status.js +3 -3
- package/dist/dashboard.js +12 -12
- package/docs/guides/thinremote-metrics-logs.md +59 -0
- package/docs/guides/thinremote-playbooks.md +133 -0
- package/docs/guides/thinremote-scripts.md +115 -0
- package/docs/guides/thinremote.md +116 -0
- package/lib/command-stream.js +3 -2
- package/lib/console.js +6 -6
- package/lib/constants.js +1 -19
- package/lib/dashboard/event-stream.js +5 -5
- package/lib/device-api.js +4 -4
- package/lib/guides.js +41 -0
- package/lib/identity.js +75 -0
- package/lib/local-files.js +54 -0
- package/lib/mcp/{tools-install.js → agent-install.js} +12 -61
- package/lib/mcp/helpers.js +5 -25
- package/lib/mcp/playbook-ops.js +393 -0
- package/lib/mcp/registry.js +9 -43
- package/lib/mcp/resource-hooks.js +53 -0
- package/lib/mcp/resource-types.js +857 -0
- package/lib/mcp/tools-exec.js +217 -177
- package/lib/monitoring.js +3 -3
- package/lib/playbook/actions.js +30 -18
- package/lib/playbook/expression.js +1 -4
- package/lib/playbook/fleet.js +4 -1
- package/lib/playbook/loader.js +16 -8
- package/lib/playbook/runner.js +40 -8
- package/lib/playbook/schema.js +108 -34
- package/lib/product/log-presets.js +1 -1
- package/lib/product/logs.js +2 -2
- package/lib/product-orchestrator.js +14 -9
- package/lib/product.js +29 -227
- package/lib/proxy.js +4 -4
- package/lib/status.js +13 -5
- package/package.json +4 -2
- package/commands/device/delete.js +0 -53
- package/commands/device/list.js +0 -55
- package/commands/device/property.js +0 -52
- package/commands/device/resource.js +0 -52
- package/commands/logout.js +0 -47
- package/commands/product/list.js +0 -86
- package/commands/product/props.js +0 -125
- package/commands/profile.js +0 -173
- package/lib/alarms.js +0 -223
- package/lib/api.js +0 -89
- package/lib/auth.js +0 -428
- package/lib/bucket.js +0 -82
- package/lib/concurrency.js +0 -52
- package/lib/config.js +0 -230
- package/lib/devices.js +0 -60
- package/lib/errors.js +0 -65
- package/lib/format.js +0 -114
- package/lib/mcp/server.js +0 -67
- package/lib/mcp/tools-alarms.js +0 -507
- package/lib/mcp/tools-buckets.js +0 -99
- package/lib/mcp/tools-devices.js +0 -235
- package/lib/mcp/tools-filesystem.js +0 -281
- package/lib/mcp/tools-logs.js +0 -225
- package/lib/mcp/tools-playbook.js +0 -210
- package/lib/mcp/tools-products.js +0 -1557
- package/lib/mcp/tools-profile.js +0 -635
- package/lib/mcp/tools-profiles.js +0 -30
- package/lib/mcp/tools-properties.js +0 -79
- package/lib/mcp/tools-resources.js +0 -78
- package/lib/mcp/tools-scripts.js +0 -212
- package/lib/mcp/tools-tokens.js +0 -369
- package/lib/output.js +0 -114
- package/lib/paths.js +0 -32
- package/lib/profile.js +0 -657
- package/lib/property.js +0 -75
- package/lib/resource.js +0 -151
- package/lib/storage.js +0 -117
- package/lib/tokens.js +0 -366
package/commands/product/list.js
DELETED
|
@@ -1,86 +0,0 @@
|
|
|
1
|
-
// @ts-check
|
|
2
|
-
import { getProducts, getProductApi } from '../../lib/product.js';
|
|
3
|
-
import { runPool } from '../../lib/concurrency.js';
|
|
4
|
-
import { label, hint, success } from '../../lib/format.js';
|
|
5
|
-
import {
|
|
6
|
-
isJsonMode,
|
|
7
|
-
printOk,
|
|
8
|
-
printErr,
|
|
9
|
-
createSpinner,
|
|
10
|
-
classifyError,
|
|
11
|
-
} from '../../lib/output.js';
|
|
12
|
-
import {
|
|
13
|
-
applyJsonFlag,
|
|
14
|
-
ensureConfigured,
|
|
15
|
-
getGlobalUser,
|
|
16
|
-
parsePositiveInt,
|
|
17
|
-
} from '../_shared.js';
|
|
18
|
-
|
|
19
|
-
export function registerProductListCommand(product) {
|
|
20
|
-
product
|
|
21
|
-
.command('list')
|
|
22
|
-
.helpGroup('Discovery:')
|
|
23
|
-
.description('List every product configured on the server')
|
|
24
|
-
.option('-j, --json', 'Output as JSON')
|
|
25
|
-
.option(
|
|
26
|
-
'-c, --concurrency <n>',
|
|
27
|
-
'Max parallel api-resource lookups (default: 10)',
|
|
28
|
-
parsePositiveInt('concurrency'),
|
|
29
|
-
10,
|
|
30
|
-
)
|
|
31
|
-
.action(async (opts, cmd) => {
|
|
32
|
-
applyJsonFlag(opts);
|
|
33
|
-
ensureConfigured();
|
|
34
|
-
const user = getGlobalUser(cmd);
|
|
35
|
-
|
|
36
|
-
const spinner = createSpinner('Fetching products...').start();
|
|
37
|
-
let products;
|
|
38
|
-
try {
|
|
39
|
-
products = await getProducts(user);
|
|
40
|
-
} catch (error) {
|
|
41
|
-
spinner.fail('Failed to fetch products');
|
|
42
|
-
const { message, code } = classifyError(error);
|
|
43
|
-
printErr(message, { code });
|
|
44
|
-
return;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
// Script counts come from each product's api resources map.
|
|
48
|
-
// Parallelise so a large catalogue doesn't crawl serially.
|
|
49
|
-
const scriptResults = await runPool(products, opts.concurrency, async (p) => {
|
|
50
|
-
try {
|
|
51
|
-
const apis = await getProductApi(p.product, user);
|
|
52
|
-
return Object.keys(apis || {}).length;
|
|
53
|
-
} catch {
|
|
54
|
-
return null;
|
|
55
|
-
}
|
|
56
|
-
});
|
|
57
|
-
const rows = products.map((p, i) => ({
|
|
58
|
-
product: p.product,
|
|
59
|
-
name: p.name && p.name !== p.product ? p.name : null,
|
|
60
|
-
enabled: !!p.enabled,
|
|
61
|
-
scripts: scriptResults[i]?.ok ? scriptResults[i].value : null,
|
|
62
|
-
}));
|
|
63
|
-
|
|
64
|
-
spinner.succeed(`Found ${rows.length} product(s)`);
|
|
65
|
-
|
|
66
|
-
if (isJsonMode()) {
|
|
67
|
-
printOk({ products: rows });
|
|
68
|
-
return;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
if (rows.length === 0) return;
|
|
72
|
-
|
|
73
|
-
// Pad the product id column so the trailing name/scripts
|
|
74
|
-
// bits align even when ids have very different widths.
|
|
75
|
-
const idWidth = rows.reduce((w, r) => Math.max(w, r.product.length), 0);
|
|
76
|
-
for (const r of rows) {
|
|
77
|
-
const flag = r.enabled ? success('enabled ') : hint('disabled');
|
|
78
|
-
const id = label(r.product.padEnd(idWidth));
|
|
79
|
-
const name = r.name ? hint(`(${r.name})`) : '';
|
|
80
|
-
const scripts =
|
|
81
|
-
r.scripts != null && r.scripts > 0 ? hint(`${r.scripts} script(s)`) : '';
|
|
82
|
-
const extras = [name, scripts].filter(Boolean).join(' ');
|
|
83
|
-
console.log(` ${flag} ${id}${extras ? ' ' + extras : ''}`);
|
|
84
|
-
}
|
|
85
|
-
});
|
|
86
|
-
}
|
|
@@ -1,125 +0,0 @@
|
|
|
1
|
-
// @ts-check
|
|
2
|
-
import {
|
|
3
|
-
getProductProperties,
|
|
4
|
-
getProductProperty,
|
|
5
|
-
setProductProperty,
|
|
6
|
-
deleteProductProperty,
|
|
7
|
-
} from '../../lib/product.js';
|
|
8
|
-
import { info, success } from '../../lib/format.js';
|
|
9
|
-
import { isJsonMode, printOk, printErr, classifyError } from '../../lib/output.js';
|
|
10
|
-
import { applyJsonFlag, ensureConfigured, getGlobalUser } from '../_shared.js';
|
|
11
|
-
|
|
12
|
-
function parseJsonValue(raw) {
|
|
13
|
-
if (raw == null) return raw;
|
|
14
|
-
try {
|
|
15
|
-
return JSON.parse(raw);
|
|
16
|
-
} catch {
|
|
17
|
-
// Fall back to raw string so `thinr product property-set p x foo` still works
|
|
18
|
-
// for trivial scalar writes without shell-escaping quotes.
|
|
19
|
-
return raw;
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export function registerProductPropertyAdminCommands(product) {
|
|
24
|
-
product
|
|
25
|
-
.command('property-list <productId>')
|
|
26
|
-
.helpGroup('Product config:')
|
|
27
|
-
.description('List the names of properties attached to 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 list = await getProductProperties(productId, user);
|
|
35
|
-
const names = Array.isArray(list) ? list.map((p) => p.property).filter(Boolean) : [];
|
|
36
|
-
if (isJsonMode()) {
|
|
37
|
-
printOk({ product: productId, properties: names });
|
|
38
|
-
return;
|
|
39
|
-
}
|
|
40
|
-
if (names.length === 0) {
|
|
41
|
-
console.log(`No properties on product ${info(productId)}`);
|
|
42
|
-
return;
|
|
43
|
-
}
|
|
44
|
-
console.log(`Properties of product ${info(productId)}:`);
|
|
45
|
-
for (const n of names) console.log(`- ${n}`);
|
|
46
|
-
} catch (error) {
|
|
47
|
-
const { message, code } = classifyError(error);
|
|
48
|
-
printErr(message, { code });
|
|
49
|
-
}
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
product
|
|
53
|
-
.command('property-get <productId> <propertyId>')
|
|
54
|
-
.helpGroup('Product config:')
|
|
55
|
-
.description('Read a property attached to a product')
|
|
56
|
-
.option('-j, --json', 'Output as JSON')
|
|
57
|
-
.action(async (productId, propertyId, opts, cmd) => {
|
|
58
|
-
applyJsonFlag(opts);
|
|
59
|
-
ensureConfigured();
|
|
60
|
-
const user = getGlobalUser(cmd);
|
|
61
|
-
try {
|
|
62
|
-
const value = await getProductProperty(productId, propertyId, user);
|
|
63
|
-
if (isJsonMode()) {
|
|
64
|
-
printOk({ product: productId, property: propertyId, value });
|
|
65
|
-
return;
|
|
66
|
-
}
|
|
67
|
-
console.log(`Value of ${info(propertyId)} on product ${info(productId)}:`);
|
|
68
|
-
console.log(JSON.stringify(value, null, 2));
|
|
69
|
-
} catch (error) {
|
|
70
|
-
const { message, code } = classifyError(error);
|
|
71
|
-
printErr(message, { code });
|
|
72
|
-
}
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
product
|
|
76
|
-
.command('property-set <productId> <propertyId> <value>')
|
|
77
|
-
.helpGroup('Product config:')
|
|
78
|
-
.description('Create or overwrite a product property (value parsed as JSON)')
|
|
79
|
-
.option('-j, --json', 'Output as JSON')
|
|
80
|
-
.action(async (productId, propertyId, value, opts, cmd) => {
|
|
81
|
-
applyJsonFlag(opts);
|
|
82
|
-
ensureConfigured();
|
|
83
|
-
const user = getGlobalUser(cmd);
|
|
84
|
-
const parsed = parseJsonValue(value);
|
|
85
|
-
try {
|
|
86
|
-
const saved = await setProductProperty(productId, propertyId, parsed, user);
|
|
87
|
-
if (isJsonMode()) {
|
|
88
|
-
printOk({ product: productId, property: propertyId, value: saved.value });
|
|
89
|
-
return;
|
|
90
|
-
}
|
|
91
|
-
console.log(
|
|
92
|
-
`Saved property ${info(propertyId)} on product ${info(productId)} → ${success(JSON.stringify(saved.value))}`,
|
|
93
|
-
);
|
|
94
|
-
} catch (error) {
|
|
95
|
-
const { message, code } = classifyError(error);
|
|
96
|
-
printErr(message, { code });
|
|
97
|
-
}
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
product
|
|
101
|
-
.command('property-delete <productId> <propertyId>')
|
|
102
|
-
.helpGroup('Product config:')
|
|
103
|
-
.description('Delete a product property (idempotent)')
|
|
104
|
-
.option('-j, --json', 'Output as JSON')
|
|
105
|
-
.action(async (productId, propertyId, opts, cmd) => {
|
|
106
|
-
applyJsonFlag(opts);
|
|
107
|
-
ensureConfigured();
|
|
108
|
-
const user = getGlobalUser(cmd);
|
|
109
|
-
try {
|
|
110
|
-
const removed = await deleteProductProperty(productId, propertyId, user);
|
|
111
|
-
if (isJsonMode()) {
|
|
112
|
-
printOk({ product: productId, property: propertyId, removed });
|
|
113
|
-
return;
|
|
114
|
-
}
|
|
115
|
-
console.log(
|
|
116
|
-
removed
|
|
117
|
-
? `Deleted ${info(propertyId)} from ${info(productId)}`
|
|
118
|
-
: `Property ${info(propertyId)} was not set on ${info(productId)}`,
|
|
119
|
-
);
|
|
120
|
-
} catch (error) {
|
|
121
|
-
const { message, code } = classifyError(error);
|
|
122
|
-
printErr(message, { code });
|
|
123
|
-
}
|
|
124
|
-
});
|
|
125
|
-
}
|
package/commands/profile.js
DELETED
|
@@ -1,173 +0,0 @@
|
|
|
1
|
-
import chalk from 'chalk';
|
|
2
|
-
import { input } from '@inquirer/prompts';
|
|
3
|
-
import {
|
|
4
|
-
listProfiles,
|
|
5
|
-
getActiveProfile,
|
|
6
|
-
getProfile,
|
|
7
|
-
setActiveProfile,
|
|
8
|
-
setDefaultProfile,
|
|
9
|
-
deleteProfile,
|
|
10
|
-
} from '../lib/config.js';
|
|
11
|
-
import { authenticate } from '../lib/auth.js';
|
|
12
|
-
import { setJsonMode, isJsonMode, printOk, printErr } from '../lib/output.js';
|
|
13
|
-
import { hint, success, label } from '../lib/format.js';
|
|
14
|
-
|
|
15
|
-
function applyJsonFlag(opts) {
|
|
16
|
-
if (opts.json) setJsonMode(true);
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export function profileCommand(program) {
|
|
20
|
-
const profile = program
|
|
21
|
-
.command('profile')
|
|
22
|
-
.helpGroup('Configuration:')
|
|
23
|
-
.description('Manage configuration profiles');
|
|
24
|
-
|
|
25
|
-
profile
|
|
26
|
-
.command('list')
|
|
27
|
-
.helpGroup('Profile:')
|
|
28
|
-
.description('List configured profiles')
|
|
29
|
-
.option('-j, --json', 'Output as JSON')
|
|
30
|
-
.action((opts) => {
|
|
31
|
-
applyJsonFlag(opts);
|
|
32
|
-
const names = listProfiles();
|
|
33
|
-
const active = getActiveProfile();
|
|
34
|
-
if (isJsonMode()) {
|
|
35
|
-
const entries = names.map((name) => {
|
|
36
|
-
const data = getProfile(name) || {};
|
|
37
|
-
return {
|
|
38
|
-
name,
|
|
39
|
-
active: name === active,
|
|
40
|
-
username: data.username || null,
|
|
41
|
-
server: data.server || null,
|
|
42
|
-
};
|
|
43
|
-
});
|
|
44
|
-
printOk({ active, profiles: entries });
|
|
45
|
-
return;
|
|
46
|
-
}
|
|
47
|
-
if (!names.length) {
|
|
48
|
-
console.log(hint('No profiles configured. Run thinr to set one up.'));
|
|
49
|
-
return;
|
|
50
|
-
}
|
|
51
|
-
for (const name of names) {
|
|
52
|
-
const data = getProfile(name) || {};
|
|
53
|
-
const marker = name === active ? success('* ') : ' ';
|
|
54
|
-
const detail = hint(`${data.username || ''}@${data.server || name}`);
|
|
55
|
-
console.log(`${marker}${label(name)} ${detail}`);
|
|
56
|
-
}
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
profile
|
|
60
|
-
.command('current')
|
|
61
|
-
.helpGroup('Profile:')
|
|
62
|
-
.description('Print the active profile')
|
|
63
|
-
.option('-j, --json', 'Output as JSON')
|
|
64
|
-
.action((opts) => {
|
|
65
|
-
applyJsonFlag(opts);
|
|
66
|
-
const name = getActiveProfile();
|
|
67
|
-
if (isJsonMode()) {
|
|
68
|
-
printOk({ active: name });
|
|
69
|
-
return;
|
|
70
|
-
}
|
|
71
|
-
if (!name) {
|
|
72
|
-
console.log(hint('No active profile.'));
|
|
73
|
-
return;
|
|
74
|
-
}
|
|
75
|
-
console.log(name);
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
profile
|
|
79
|
-
.command('use <name>')
|
|
80
|
-
.helpGroup('Profile:')
|
|
81
|
-
.description('Set the default profile')
|
|
82
|
-
.option('-j, --json', 'Output as JSON')
|
|
83
|
-
.action((name, opts) => {
|
|
84
|
-
applyJsonFlag(opts);
|
|
85
|
-
try {
|
|
86
|
-
setDefaultProfile(name);
|
|
87
|
-
} catch (error) {
|
|
88
|
-
printErr(error.message, { code: 'not_found' });
|
|
89
|
-
return;
|
|
90
|
-
}
|
|
91
|
-
if (isJsonMode()) {
|
|
92
|
-
printOk({ active: name });
|
|
93
|
-
return;
|
|
94
|
-
}
|
|
95
|
-
console.log(success(`Default profile set to ${name}`));
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
profile
|
|
99
|
-
.command('add [name]')
|
|
100
|
-
.description('Add a new profile via interactive authentication')
|
|
101
|
-
.option('--no-activate', 'Do not set the new profile as default')
|
|
102
|
-
.option('-j, --json', 'Output as JSON')
|
|
103
|
-
.action(async (nameArg, opts) => {
|
|
104
|
-
applyJsonFlag(opts);
|
|
105
|
-
let name = nameArg;
|
|
106
|
-
if (!name) {
|
|
107
|
-
try {
|
|
108
|
-
name = await input({
|
|
109
|
-
message: 'Profile name:',
|
|
110
|
-
validate: (v) => {
|
|
111
|
-
const trimmed = (v || '').trim();
|
|
112
|
-
if (!trimmed) return 'Profile name is required';
|
|
113
|
-
if (/\s/.test(trimmed)) return 'Profile name cannot contain whitespace';
|
|
114
|
-
return true;
|
|
115
|
-
},
|
|
116
|
-
});
|
|
117
|
-
} catch (error) {
|
|
118
|
-
printErr(error.message || 'Prompt cancelled');
|
|
119
|
-
return;
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
name = name.trim();
|
|
123
|
-
if (getProfile(name)) {
|
|
124
|
-
printErr(`Profile already exists: ${name}`, { code: 'already_exists' });
|
|
125
|
-
return;
|
|
126
|
-
}
|
|
127
|
-
setActiveProfile(name);
|
|
128
|
-
try {
|
|
129
|
-
await authenticate();
|
|
130
|
-
const saved = getProfile(name);
|
|
131
|
-
if (!saved || !saved.token || !saved.username) {
|
|
132
|
-
throw new Error('Authentication did not complete; profile discarded.');
|
|
133
|
-
}
|
|
134
|
-
} catch (error) {
|
|
135
|
-
setActiveProfile(null);
|
|
136
|
-
try {
|
|
137
|
-
deleteProfile(name);
|
|
138
|
-
} catch {
|
|
139
|
-
// Best-effort cleanup — ignore secondary failures
|
|
140
|
-
}
|
|
141
|
-
printErr(error.message || 'Authentication failed');
|
|
142
|
-
return;
|
|
143
|
-
}
|
|
144
|
-
const activate = opts.activate !== false;
|
|
145
|
-
if (activate) setDefaultProfile(name);
|
|
146
|
-
setActiveProfile(null);
|
|
147
|
-
if (isJsonMode()) {
|
|
148
|
-
printOk({ added: name, activated: activate, active: getActiveProfile() });
|
|
149
|
-
return;
|
|
150
|
-
}
|
|
151
|
-
const suffix = activate ? ' and set as default' : '';
|
|
152
|
-
console.log(chalk.green(`Profile ${name} added${suffix}`));
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
profile
|
|
156
|
-
.command('delete <name>')
|
|
157
|
-
.helpGroup('Profile:')
|
|
158
|
-
.description('Remove a profile')
|
|
159
|
-
.option('-j, --json', 'Output as JSON')
|
|
160
|
-
.action((name, opts) => {
|
|
161
|
-
applyJsonFlag(opts);
|
|
162
|
-
const ok = deleteProfile(name);
|
|
163
|
-
if (!ok) {
|
|
164
|
-
printErr(`Profile not found: ${name}`, { code: 'not_found' });
|
|
165
|
-
return;
|
|
166
|
-
}
|
|
167
|
-
if (isJsonMode()) {
|
|
168
|
-
printOk({ removed: name });
|
|
169
|
-
return;
|
|
170
|
-
}
|
|
171
|
-
console.log(success(`Removed profile ${name}`));
|
|
172
|
-
});
|
|
173
|
-
}
|
package/lib/alarms.js
DELETED
|
@@ -1,223 +0,0 @@
|
|
|
1
|
-
import api from './api.js';
|
|
2
|
-
import { apiError } from './errors.js';
|
|
3
|
-
import { v1 } from './paths.js';
|
|
4
|
-
|
|
5
|
-
// Backend enums (see backend/src/thinger/features/alarms/alarm_rule.hpp).
|
|
6
|
-
// Exported so the dashboard can render labels/colors without re-deriving
|
|
7
|
-
// them from string heuristics.
|
|
8
|
-
export const ALARM_STATE = {
|
|
9
|
-
NONE: 0,
|
|
10
|
-
ACTIVATED: 1,
|
|
11
|
-
ACKNOWLEDGED: 2,
|
|
12
|
-
LATCHED: 3,
|
|
13
|
-
SHELVED: 4,
|
|
14
|
-
CLEARED: 5,
|
|
15
|
-
};
|
|
16
|
-
|
|
17
|
-
export const ALARM_SEVERITY = {
|
|
18
|
-
NONE: 0,
|
|
19
|
-
LOW: 1,
|
|
20
|
-
MEDIUM: 2,
|
|
21
|
-
HIGH: 3,
|
|
22
|
-
CRITICAL: 4,
|
|
23
|
-
};
|
|
24
|
-
|
|
25
|
-
export const ALARM_STATE_LABEL = {
|
|
26
|
-
0: 'none',
|
|
27
|
-
1: 'active',
|
|
28
|
-
2: 'ack',
|
|
29
|
-
3: 'latched',
|
|
30
|
-
4: 'shelved',
|
|
31
|
-
5: 'cleared',
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
export const ALARM_SEVERITY_LABEL = {
|
|
35
|
-
0: 'none',
|
|
36
|
-
1: 'low',
|
|
37
|
-
2: 'med',
|
|
38
|
-
3: 'high',
|
|
39
|
-
4: 'crit',
|
|
40
|
-
};
|
|
41
|
-
|
|
42
|
-
// Reverse maps so callers can pass human-readable strings ("ack", "shelved",
|
|
43
|
-
// "high", …) instead of remembering the numeric enum values.
|
|
44
|
-
const STATE_FROM_LABEL = {
|
|
45
|
-
none: ALARM_STATE.NONE,
|
|
46
|
-
active: ALARM_STATE.ACTIVATED,
|
|
47
|
-
activated: ALARM_STATE.ACTIVATED,
|
|
48
|
-
ack: ALARM_STATE.ACKNOWLEDGED,
|
|
49
|
-
acknowledged: ALARM_STATE.ACKNOWLEDGED,
|
|
50
|
-
latched: ALARM_STATE.LATCHED,
|
|
51
|
-
shelved: ALARM_STATE.SHELVED,
|
|
52
|
-
cleared: ALARM_STATE.CLEARED,
|
|
53
|
-
};
|
|
54
|
-
|
|
55
|
-
const SEVERITY_FROM_LABEL = {
|
|
56
|
-
none: ALARM_SEVERITY.NONE,
|
|
57
|
-
low: ALARM_SEVERITY.LOW,
|
|
58
|
-
med: ALARM_SEVERITY.MEDIUM,
|
|
59
|
-
medium: ALARM_SEVERITY.MEDIUM,
|
|
60
|
-
high: ALARM_SEVERITY.HIGH,
|
|
61
|
-
crit: ALARM_SEVERITY.CRITICAL,
|
|
62
|
-
critical: ALARM_SEVERITY.CRITICAL,
|
|
63
|
-
};
|
|
64
|
-
|
|
65
|
-
export function parseAlarmState(value) {
|
|
66
|
-
if (value == null) return undefined;
|
|
67
|
-
if (typeof value === 'number') return value;
|
|
68
|
-
const key = String(value).trim().toLowerCase();
|
|
69
|
-
return STATE_FROM_LABEL[key];
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
export function parseAlarmSeverity(value) {
|
|
73
|
-
if (value == null) return undefined;
|
|
74
|
-
if (typeof value === 'number') return value;
|
|
75
|
-
const key = String(value).trim().toLowerCase();
|
|
76
|
-
return SEVERITY_FROM_LABEL[key];
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
// Repeats a query param as the server expects (`?state=1&state=2`).
|
|
80
|
-
function appendMulti(params, key, values) {
|
|
81
|
-
if (values == null) return;
|
|
82
|
-
const arr = Array.isArray(values) ? values : [values];
|
|
83
|
-
for (const v of arr) params.append(key, String(v));
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
/**
|
|
87
|
-
* Query alarm instances from /v1/users/{user}/alarms/instances.
|
|
88
|
-
*
|
|
89
|
-
* `state` and `severity` are repeated query params — pass numbers or
|
|
90
|
-
* arrays of numbers (matching the ALARM_STATE / ALARM_SEVERITY enums).
|
|
91
|
-
*
|
|
92
|
-
* @param {{
|
|
93
|
-
* user?: string | null,
|
|
94
|
-
* count?: number,
|
|
95
|
-
* index?: number,
|
|
96
|
-
* name?: string,
|
|
97
|
-
* state?: number | number[],
|
|
98
|
-
* severity?: number | number[],
|
|
99
|
-
* sort?: string,
|
|
100
|
-
* order?: string,
|
|
101
|
-
* }} [opts]
|
|
102
|
-
*/
|
|
103
|
-
export async function getAlarmInstances({
|
|
104
|
-
user = null,
|
|
105
|
-
count = 100,
|
|
106
|
-
index = 0,
|
|
107
|
-
name,
|
|
108
|
-
state,
|
|
109
|
-
severity,
|
|
110
|
-
sort,
|
|
111
|
-
order,
|
|
112
|
-
} = {}) {
|
|
113
|
-
const params = new URLSearchParams();
|
|
114
|
-
params.set('count', String(count));
|
|
115
|
-
params.set('index', String(index));
|
|
116
|
-
if (name) params.set('name', name);
|
|
117
|
-
if (sort) params.set('sort', sort);
|
|
118
|
-
if (order) params.set('order', order);
|
|
119
|
-
appendMulti(params, 'state', state);
|
|
120
|
-
appendMulti(params, 'severity', severity);
|
|
121
|
-
try {
|
|
122
|
-
const res = await api.get(`${v1(user)}/alarms/instances?${params.toString()}`);
|
|
123
|
-
return Array.isArray(res.data) ? res.data : [];
|
|
124
|
-
} catch (e) {
|
|
125
|
-
throw apiError(e);
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
// Counter per state, e.g. { activated: 3, acknowledged: 1, ... }. The
|
|
130
|
-
// server seeds zeros for every known state so consumers can render a
|
|
131
|
-
// stable layout without null-checks.
|
|
132
|
-
export async function getAlarmInstanceStats({ user = null } = {}) {
|
|
133
|
-
try {
|
|
134
|
-
const res = await api.get(`${v1(user)}/alarms/instances/stats`);
|
|
135
|
-
return res.data || {};
|
|
136
|
-
} catch (e) {
|
|
137
|
-
throw apiError(e);
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
export async function getAlarmInstance(instance, { user = null } = {}) {
|
|
142
|
-
try {
|
|
143
|
-
const res = await api.get(`${v1(user)}/alarms/instances/${encodeURIComponent(instance)}`);
|
|
144
|
-
return res.data;
|
|
145
|
-
} catch (e) {
|
|
146
|
-
throw apiError(e, { notFound: `Alarm instance not found: ${instance}` });
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
// PUT-style update. The server accepts only a small whitelist of fields
|
|
151
|
-
// (see UPDATE_SCHEMA in alarms_instances.cpp): name, description, state,
|
|
152
|
-
// annotation, reactivation. Anything else is rejected.
|
|
153
|
-
export async function updateAlarmInstance(instance, body, { user = null } = {}) {
|
|
154
|
-
try {
|
|
155
|
-
const res = await api.put(
|
|
156
|
-
`${v1(user)}/alarms/instances/${encodeURIComponent(instance)}`,
|
|
157
|
-
body,
|
|
158
|
-
);
|
|
159
|
-
return res.data;
|
|
160
|
-
} catch (e) {
|
|
161
|
-
throw apiError(e, { notFound: `Alarm instance not found: ${instance}` });
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
export async function deleteAlarmInstance(instance, { user = null } = {}) {
|
|
166
|
-
try {
|
|
167
|
-
await api.delete(`${v1(user)}/alarms/instances/${encodeURIComponent(instance)}`);
|
|
168
|
-
return true;
|
|
169
|
-
} catch (e) {
|
|
170
|
-
throw apiError(e, { notFound: `Alarm instance not found: ${instance}` });
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
export async function getAlarmRules({ user = null, count = 200, index = 0 } = {}) {
|
|
175
|
-
const params = new URLSearchParams();
|
|
176
|
-
params.set('count', String(count));
|
|
177
|
-
params.set('index', String(index));
|
|
178
|
-
try {
|
|
179
|
-
const res = await api.get(`${v1(user)}/alarms/rules?${params.toString()}`);
|
|
180
|
-
return Array.isArray(res.data) ? res.data : [];
|
|
181
|
-
} catch (e) {
|
|
182
|
-
throw apiError(e);
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
export async function getAlarmRule(rule, { user = null } = {}) {
|
|
187
|
-
try {
|
|
188
|
-
const res = await api.get(`${v1(user)}/alarms/rules/${encodeURIComponent(rule)}`);
|
|
189
|
-
return res.data;
|
|
190
|
-
} catch (e) {
|
|
191
|
-
throw apiError(e, { notFound: `Alarm rule not found: ${rule}` });
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
// POST /v1/users/{user}/alarms/rules. The body must include `rule`,
|
|
196
|
-
// `name`, `enabled` and an optional `config` object — see CREATE_SCHEMA
|
|
197
|
-
// in alarms_rules.cpp for the full shape.
|
|
198
|
-
export async function createAlarmRule(body, { user = null } = {}) {
|
|
199
|
-
try {
|
|
200
|
-
const res = await api.post(`${v1(user)}/alarms/rules`, body);
|
|
201
|
-
return res.data;
|
|
202
|
-
} catch (e) {
|
|
203
|
-
throw apiError(e);
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
export async function updateAlarmRule(rule, body, { user = null } = {}) {
|
|
208
|
-
try {
|
|
209
|
-
const res = await api.put(`${v1(user)}/alarms/rules/${encodeURIComponent(rule)}`, body);
|
|
210
|
-
return res.data;
|
|
211
|
-
} catch (e) {
|
|
212
|
-
throw apiError(e, { notFound: `Alarm rule not found: ${rule}` });
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
export async function deleteAlarmRule(rule, { user = null } = {}) {
|
|
217
|
-
try {
|
|
218
|
-
await api.delete(`${v1(user)}/alarms/rules/${encodeURIComponent(rule)}`);
|
|
219
|
-
return true;
|
|
220
|
-
} catch (e) {
|
|
221
|
-
throw apiError(e, { notFound: `Alarm rule not found: ${rule}` });
|
|
222
|
-
}
|
|
223
|
-
}
|
package/lib/api.js
DELETED
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
import axios from 'axios';
|
|
2
|
-
import { refreshToken, getAccessToken } from './auth.js';
|
|
3
|
-
import { readConfig } from './config.js';
|
|
4
|
-
import { TIMEOUTS } from './constants.js';
|
|
5
|
-
import http from 'http';
|
|
6
|
-
import https from 'https';
|
|
7
|
-
|
|
8
|
-
const httpAgent = new http.Agent({ keepAlive: true });
|
|
9
|
-
// Accept self-signed certificates when talking to local dev servers
|
|
10
|
-
// (localhost / 127.0.0.1). Any other host still enforces full TLS
|
|
11
|
-
// validation. Toggleable via THINR_INSECURE=1 for other local setups.
|
|
12
|
-
const httpsAgent = new https.Agent({ keepAlive: true });
|
|
13
|
-
const insecureAgent = new https.Agent({ keepAlive: true, rejectUnauthorized: false });
|
|
14
|
-
|
|
15
|
-
const api = axios.create({
|
|
16
|
-
// You can leave baseURL undefined initially
|
|
17
|
-
timeout: TIMEOUTS.API_DEFAULT_MS,
|
|
18
|
-
httpAgent,
|
|
19
|
-
httpsAgent,
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
export function setBaseURL(url) {
|
|
23
|
-
api.defaults.baseURL = url;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
api.interceptors.request.use(
|
|
27
|
-
(request) => {
|
|
28
|
-
const token = getAccessToken();
|
|
29
|
-
if (token) {
|
|
30
|
-
request.headers['Authorization'] = `Bearer ${token}`;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
// Allow self-signed TLS against localhost / 127.0.0.1 or when the user
|
|
34
|
-
// opts in explicitly. Anything else keeps default certificate validation.
|
|
35
|
-
// A malformed URL here is not worth aborting the request over — the
|
|
36
|
-
// HTTPS layer will surface the real error downstream; we only care
|
|
37
|
-
// about detecting localhost cheaply.
|
|
38
|
-
try {
|
|
39
|
-
const base = request.baseURL || api.defaults.baseURL || '';
|
|
40
|
-
const url = new URL(request.url || '', base || 'https://placeholder/');
|
|
41
|
-
const isLocal = url.hostname === 'localhost' || url.hostname === '127.0.0.1';
|
|
42
|
-
if (url.protocol === 'https:' && (isLocal || process.env.THINR_INSECURE === '1')) {
|
|
43
|
-
request.httpsAgent = insecureAgent;
|
|
44
|
-
}
|
|
45
|
-
} catch {
|
|
46
|
-
// Intentionally swallowed — see note above.
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
return request;
|
|
50
|
-
},
|
|
51
|
-
(error) => {
|
|
52
|
-
return Promise.reject(error);
|
|
53
|
-
},
|
|
54
|
-
);
|
|
55
|
-
|
|
56
|
-
api.interceptors.response.use(
|
|
57
|
-
(response) => response,
|
|
58
|
-
async (error) => {
|
|
59
|
-
const originalRequest = error.config;
|
|
60
|
-
|
|
61
|
-
// Only intervene on 401s; everything else surfaces unchanged.
|
|
62
|
-
if (error.response?.status !== 401) return Promise.reject(error);
|
|
63
|
-
|
|
64
|
-
// Don't recurse: refresh requests themselves and already-retried
|
|
65
|
-
// requests must not trigger another refresh.
|
|
66
|
-
if (originalRequest?._isRefresh || originalRequest?._retry) {
|
|
67
|
-
return Promise.reject(error);
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
// Tokens issued via the username/password flow have no
|
|
71
|
-
// refresh_token. Skip the refresh attempt and let the 401 reach
|
|
72
|
-
// the caller — they can re-run `thinr` to re-authenticate.
|
|
73
|
-
if (!readConfig().refresh_token) return Promise.reject(error);
|
|
74
|
-
|
|
75
|
-
originalRequest._retry = true;
|
|
76
|
-
|
|
77
|
-
try {
|
|
78
|
-
const newToken = await refreshToken();
|
|
79
|
-
// Patch only this request; subsequent requests pick up the new
|
|
80
|
-
// token from config via the request interceptor.
|
|
81
|
-
originalRequest.headers['Authorization'] = `Bearer ${newToken}`;
|
|
82
|
-
return api(originalRequest);
|
|
83
|
-
} catch (refreshError) {
|
|
84
|
-
return Promise.reject(refreshError);
|
|
85
|
-
}
|
|
86
|
-
},
|
|
87
|
-
);
|
|
88
|
-
|
|
89
|
-
export default api;
|