@thinger-io/cli-core 0.4.2 → 0.5.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 +195 -0
- package/README.md +82 -31
- package/lib/bucket-export.js +91 -0
- package/lib/cli.js +106 -50
- package/lib/commands/_shared.js +89 -1
- package/lib/commands/api.js +90 -0
- package/lib/commands/arguments.js +62 -0
- package/lib/commands/bucket.js +52 -44
- package/lib/commands/dashboard.js +5 -2
- package/lib/commands/data.js +539 -0
- package/lib/commands/device.js +128 -241
- package/lib/commands/grammar.js +279 -0
- package/lib/commands/product.js +104 -214
- package/lib/commands/profile.js +1 -0
- package/lib/commands/resources.js +975 -0
- package/lib/dashboard-init.js +4 -3
- package/lib/dashboard.js +10 -2
- package/lib/mcp/catalog.js +301 -63
- package/lib/mcp/core-types.js +36 -0
- package/lib/mcp/helpers.js +23 -2
- package/lib/mcp/naming.js +20 -0
- package/lib/mcp/registry.js +18 -67
- package/lib/mcp/resource-hooks.js +19 -5
- package/lib/mcp/server.js +43 -11
- package/lib/mcp/tools-resource.js +1232 -330
- package/lib/mcp/types/bucket-data.js +151 -0
- package/lib/mcp/{tools-dashboards.js → types/dashboard-apps.js} +99 -37
- package/lib/mcp/types/device-resource.js +117 -0
- package/lib/mcp/types/doc.js +35 -0
- package/lib/mcp/types/plugin-call.js +75 -0
- package/lib/mcp/types/product-api.js +44 -0
- package/lib/mcp/types/profile.js +37 -0
- package/lib/mcp/types/storage-file.js +170 -0
- package/lib/openapi.js +18 -0
- package/lib/product-apply.js +121 -0
- package/lib/raw-api.js +111 -0
- package/lib/storage.js +41 -13
- package/lib/tokens.js +16 -2
- package/package.json +1 -1
- package/lib/mcp/tools-buckets.js +0 -130
- package/lib/mcp/tools-devices.js +0 -134
- package/lib/mcp/tools-misc.js +0 -48
- package/lib/mcp/tools-plugins.js +0 -63
- package/lib/mcp/tools-products.js +0 -52
- package/lib/mcp/tools-storage.js +0 -173
package/lib/commands/_shared.js
CHANGED
|
@@ -34,6 +34,16 @@ export function getGlobalUser(cmd) {
|
|
|
34
34
|
return root.opts().user || null;
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
export function parseNonNegativeInt(label) {
|
|
38
|
+
return (value) => {
|
|
39
|
+
const n = Number(value);
|
|
40
|
+
if (!Number.isInteger(n) || n < 0) {
|
|
41
|
+
throw new InvalidArgumentError(`${label} must be a non-negative integer`);
|
|
42
|
+
}
|
|
43
|
+
return n;
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
37
47
|
export function parsePositiveInt(label) {
|
|
38
48
|
return (value) => {
|
|
39
49
|
const n = Number(value);
|
|
@@ -54,6 +64,37 @@ export function collectKeyValue(value, previous = {}) {
|
|
|
54
64
|
return { ...previous, [value.slice(0, idx)]: parseJsonValue(value.slice(idx + 1)) };
|
|
55
65
|
}
|
|
56
66
|
|
|
67
|
+
// Like collectKeyValue, values kept as written: query parameters, where
|
|
68
|
+
// "007" must not become 7.
|
|
69
|
+
export function collectKeyString(value, previous = {}) {
|
|
70
|
+
const idx = value.indexOf('=');
|
|
71
|
+
if (idx === -1) throw new InvalidArgumentError('must be key=value');
|
|
72
|
+
return { ...previous, [value.slice(0, idx)]: value.slice(idx + 1) };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Set `value` at a dot path of `target` (`data.source` → target.data.source),
|
|
77
|
+
* creating the objects on the way. Keys that would reach the prototype are
|
|
78
|
+
* refused.
|
|
79
|
+
*
|
|
80
|
+
* @param {Record<string, any>} target
|
|
81
|
+
* @param {string} path
|
|
82
|
+
* @param {any} value
|
|
83
|
+
*/
|
|
84
|
+
export function setPath(target, path, value) {
|
|
85
|
+
const keys = path.split('.');
|
|
86
|
+
if (keys.some((k) => !k || k === '__proto__' || k === 'constructor' || k === 'prototype')) {
|
|
87
|
+
throw new InvalidArgumentError(`invalid field: ${path}`);
|
|
88
|
+
}
|
|
89
|
+
let node = target;
|
|
90
|
+
for (const k of keys.slice(0, -1)) {
|
|
91
|
+
if (!node[k] || typeof node[k] !== 'object' || Array.isArray(node[k])) node[k] = {};
|
|
92
|
+
node = node[k];
|
|
93
|
+
}
|
|
94
|
+
node[keys[keys.length - 1]] = value;
|
|
95
|
+
return target;
|
|
96
|
+
}
|
|
97
|
+
|
|
57
98
|
// Parse a command-line value as JSON, falling back to the raw string so
|
|
58
99
|
// `property set dev name foo` works without shell-escaping quotes.
|
|
59
100
|
export function parseJsonValue(raw) {
|
|
@@ -66,7 +107,9 @@ export function parseJsonValue(raw) {
|
|
|
66
107
|
}
|
|
67
108
|
|
|
68
109
|
// Read a JSON document from a file path, or from stdin when the path is `-`.
|
|
69
|
-
|
|
110
|
+
// With `lines`, a file of one JSON document per line (NDJSON) is read as
|
|
111
|
+
// the array of them.
|
|
112
|
+
export async function readJsonArgument(source, { lines = false } = {}) {
|
|
70
113
|
let text;
|
|
71
114
|
if (source === '-') {
|
|
72
115
|
const chunks = [];
|
|
@@ -78,10 +121,55 @@ export async function readJsonArgument(source) {
|
|
|
78
121
|
try {
|
|
79
122
|
return JSON.parse(text);
|
|
80
123
|
} catch (error) {
|
|
124
|
+
if (lines) {
|
|
125
|
+
try {
|
|
126
|
+
return text
|
|
127
|
+
.split('\n')
|
|
128
|
+
.filter((l) => l.trim())
|
|
129
|
+
.map((l) => JSON.parse(l));
|
|
130
|
+
} catch {
|
|
131
|
+
// Report the error of the whole document.
|
|
132
|
+
}
|
|
133
|
+
}
|
|
81
134
|
throw new InvalidArgumentError(`${source} is not valid JSON: ${error.message}`);
|
|
82
135
|
}
|
|
83
136
|
}
|
|
84
137
|
|
|
138
|
+
/**
|
|
139
|
+
* The body a command builds from `--from <file|->` and repeatable
|
|
140
|
+
* `--set key=value` (dot paths nest) on top of it. Undefined when neither
|
|
141
|
+
* is given.
|
|
142
|
+
*
|
|
143
|
+
* @param {{ from?: string, set?: Record<string, any> }} opts
|
|
144
|
+
* @param {{ lines?: boolean }} [read] Passed to readJsonArgument.
|
|
145
|
+
*/
|
|
146
|
+
export async function bodyFromOptions(opts, read) {
|
|
147
|
+
let body;
|
|
148
|
+
if (opts.from) body = await readJsonArgument(opts.from, read);
|
|
149
|
+
if (opts.set) {
|
|
150
|
+
if (
|
|
151
|
+
body !== undefined &&
|
|
152
|
+
(typeof body !== 'object' || body === null || Array.isArray(body))
|
|
153
|
+
) {
|
|
154
|
+
throw new InvalidArgumentError('--set needs the --from body to be a JSON object');
|
|
155
|
+
}
|
|
156
|
+
body = body ?? {};
|
|
157
|
+
for (const [k, v] of Object.entries(opts.set)) setPath(body, k, v);
|
|
158
|
+
}
|
|
159
|
+
return body;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** `--from` and `--set`, the options of bodyFromOptions. */
|
|
163
|
+
export function bodyOptions(cmd, setFlag = '-s, --set <key=value>') {
|
|
164
|
+
return cmd
|
|
165
|
+
.option('--from <file>', 'JSON body from a file, or - for stdin')
|
|
166
|
+
.option(
|
|
167
|
+
setFlag,
|
|
168
|
+
'Body field (repeatable; values parsed as JSON when they are; dots nest: data.source=resource)',
|
|
169
|
+
collectKeyValue,
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
|
|
85
173
|
/**
|
|
86
174
|
* Ask before an irreversible action unless `--yes` was given. Refuses in a
|
|
87
175
|
* non-interactive shell (or JSON mode) without `--yes` rather than
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
import { writeFile } from 'node:fs/promises';
|
|
3
|
+
import { rawApiCall } from '../raw-api.js';
|
|
4
|
+
import { inputError } from '../errors.js';
|
|
5
|
+
import { classifyError, isJsonMode, printErr, printOk } from '../output.js';
|
|
6
|
+
import {
|
|
7
|
+
applyJsonFlag,
|
|
8
|
+
collectKeyString,
|
|
9
|
+
ensureConfigured,
|
|
10
|
+
getGlobalUser,
|
|
11
|
+
bodyFromOptions,
|
|
12
|
+
bodyOptions,
|
|
13
|
+
} from './_shared.js';
|
|
14
|
+
|
|
15
|
+
/** `<cli> api <path>` — any request to the server, as `gh api` does for GitHub. */
|
|
16
|
+
export function registerApiCommand(program) {
|
|
17
|
+
bodyOptions(
|
|
18
|
+
program
|
|
19
|
+
.command('api <path>')
|
|
20
|
+
.helpGroup('Integration:')
|
|
21
|
+
.description('Send a request to the server API (anything the commands do not cover)')
|
|
22
|
+
.option(
|
|
23
|
+
'-X, --method <method>',
|
|
24
|
+
'GET, POST, PUT, PATCH or DELETE (default: GET, or POST with a body)',
|
|
25
|
+
),
|
|
26
|
+
)
|
|
27
|
+
.option('-q, --query <key=value>', 'Query parameter (repeatable)', collectKeyString)
|
|
28
|
+
.option('-o, --output <file>', 'Write the answer to a file (downloads, binary answers)')
|
|
29
|
+
.option('-j, --json', 'Output as JSON ({ ok, data })')
|
|
30
|
+
.addHelpText(
|
|
31
|
+
'after',
|
|
32
|
+
`
|
|
33
|
+
The path is relative to the profile's server; {user} stands for the account
|
|
34
|
+
(the profile's, or --user). The answer is printed as it comes: JSON pretty
|
|
35
|
+
printed, text as is.
|
|
36
|
+
|
|
37
|
+
Examples:
|
|
38
|
+
$ <cli> api /v1/users/{user}/endpoints
|
|
39
|
+
$ <cli> api /v1/users/{user}/buckets -s bucket=temps -s name=Temperatures -s source=write
|
|
40
|
+
$ <cli> api -X PUT /v1/users/{user}/devices/sensor1 --from device.json
|
|
41
|
+
$ <cli> api /v1/users/{user}/buckets/temps/data -q items=100 -q sort=desc
|
|
42
|
+
$ <cli> api /v2/users/{user}/storages/web/files/index.html -o index.html`.replaceAll(
|
|
43
|
+
'<cli>',
|
|
44
|
+
program.name(),
|
|
45
|
+
),
|
|
46
|
+
)
|
|
47
|
+
.action(async (path, opts, cmd) => {
|
|
48
|
+
applyJsonFlag(opts);
|
|
49
|
+
ensureConfigured();
|
|
50
|
+
try {
|
|
51
|
+
const body = await bodyFromOptions(opts);
|
|
52
|
+
if (
|
|
53
|
+
body !== undefined &&
|
|
54
|
+
['GET', 'DELETE'].includes(String(opts.method).toUpperCase())
|
|
55
|
+
) {
|
|
56
|
+
throw inputError(
|
|
57
|
+
`${opts.method.toUpperCase()} takes no body: pass query parameters with -q`,
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
const res = await rawApiCall({
|
|
61
|
+
method: opts.method,
|
|
62
|
+
path,
|
|
63
|
+
query: opts.query,
|
|
64
|
+
body,
|
|
65
|
+
user: getGlobalUser(cmd),
|
|
66
|
+
});
|
|
67
|
+
if (opts.output) {
|
|
68
|
+
await writeFile(opts.output, res.raw);
|
|
69
|
+
if (isJsonMode()) printOk({ status: res.status, file: opts.output });
|
|
70
|
+
else process.stderr.write(`Saved to ${opts.output}\n`);
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
if (res.kind === 'binary') {
|
|
74
|
+
if (isJsonMode() || process.stdout.isTTY) {
|
|
75
|
+
throw inputError(
|
|
76
|
+
`the answer is binary (${res.contentType || 'unknown type'}, ${res.body.length} bytes): save it with -o <file>`,
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
process.stdout.write(res.body);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
if (isJsonMode()) printOk(res.body);
|
|
83
|
+
else if (res.kind === 'json') console.log(JSON.stringify(res.body, null, 2));
|
|
84
|
+
else if (res.kind === 'text') process.stdout.write(res.raw);
|
|
85
|
+
} catch (error) {
|
|
86
|
+
const { message, code } = classifyError(error);
|
|
87
|
+
printErr(message, { code });
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Help for the positional arguments of the commands written by hand, kept
|
|
5
|
+
* in one table instead of spread over every `.command('x <id>')`. A command
|
|
6
|
+
* path ('dashboard init') wins over the argument name alone; a CLI adds its
|
|
7
|
+
* own tables (runCli's `argumentHelp`).
|
|
8
|
+
*/
|
|
9
|
+
const BY_COMMAND = {
|
|
10
|
+
api: { path: 'API path, e.g. /v1/users/{user}/devices ({user}: your account)' },
|
|
11
|
+
'device set-product': { product: 'Product ID ("" removes the device from its product)' },
|
|
12
|
+
'device resource call': { resource: 'Resource of the device to call' },
|
|
13
|
+
'product profile api call': { resource: 'API resource of the product profile' },
|
|
14
|
+
'doc get': { doc: 'ID of the guide (doc list gives them)' },
|
|
15
|
+
|
|
16
|
+
'dashboard init': { dir: 'New or empty folder for the app' },
|
|
17
|
+
'dashboard dev': {
|
|
18
|
+
url: 'Address of the app on its dev server (default: dev.url of the manifest)',
|
|
19
|
+
},
|
|
20
|
+
'dashboard publish': { dir: 'Folder to upload (default: dir of the manifest)' },
|
|
21
|
+
'storage file list': { folder: 'Folder of the storage to list (default: all the files)' },
|
|
22
|
+
'storage file upload': { local: 'Local file or folder' },
|
|
23
|
+
'storage file download': { path: 'Path of the file in the storage' },
|
|
24
|
+
'storage file delete': { path: 'Path of the file in the storage' },
|
|
25
|
+
'plugin call': { path: 'Path in the API of the plugin, e.g. /api/status' },
|
|
26
|
+
'profile add': { name: 'Profile name (asked when missing)' },
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const BY_NAME = {
|
|
30
|
+
device: 'Device ID',
|
|
31
|
+
product: 'Product ID',
|
|
32
|
+
bucket: 'Data Bucket ID',
|
|
33
|
+
property: 'Property ID',
|
|
34
|
+
value: 'Value, parsed as JSON (text when it is not JSON)',
|
|
35
|
+
storage: 'File storage ID',
|
|
36
|
+
plugin: 'Plugin ID',
|
|
37
|
+
name: 'Profile name',
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Describe the arguments that have no description yet, in the whole tree.
|
|
42
|
+
*
|
|
43
|
+
* @param {import('commander').Command} program
|
|
44
|
+
* @param {{ byCommand?: Record<string, Record<string, string>>, byName?: Record<string, string> }} [extra]
|
|
45
|
+
* A CLI's own tables, read before the core's.
|
|
46
|
+
*/
|
|
47
|
+
export function describeArguments(program, { byCommand = {}, byName = {} } = {}) {
|
|
48
|
+
const walk = (cmd, path) => {
|
|
49
|
+
for (const arg of cmd.registeredArguments) {
|
|
50
|
+
if (arg.description) continue;
|
|
51
|
+
const name = arg.name();
|
|
52
|
+
arg.description =
|
|
53
|
+
byCommand[path]?.[name] ??
|
|
54
|
+
BY_COMMAND[path]?.[name] ??
|
|
55
|
+
byName[name] ??
|
|
56
|
+
BY_NAME[name] ??
|
|
57
|
+
'';
|
|
58
|
+
}
|
|
59
|
+
for (const sub of cmd.commands) walk(sub, path ? `${path} ${sub.name()}` : sub.name());
|
|
60
|
+
};
|
|
61
|
+
walk(program, '');
|
|
62
|
+
}
|
package/lib/commands/bucket.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { getBucketData, getBuckets } from '../bucket.js';
|
|
3
3
|
import { hint, info, label, success } from '../format.js';
|
|
4
4
|
import { isJsonMode, printOk } from '../output.js';
|
|
5
|
+
import { hasListOptions, listOptions, listRecords } from './resources.js';
|
|
5
6
|
import {
|
|
6
7
|
applyJsonFlag,
|
|
7
8
|
ensureConfigured,
|
|
@@ -10,71 +11,78 @@ import {
|
|
|
10
11
|
runCommand,
|
|
11
12
|
} from './_shared.js';
|
|
12
13
|
|
|
13
|
-
/**
|
|
14
|
+
/**
|
|
15
|
+
* `<cli> bucket list|read` — data buckets as a table and their time
|
|
16
|
+
* series; the rest of the group (write, export, records, tags…) comes from
|
|
17
|
+
* the tool and resource commands.
|
|
18
|
+
*/
|
|
14
19
|
export function registerBucketCommands(program) {
|
|
15
20
|
const bucket = program
|
|
16
21
|
.command('bucket')
|
|
17
|
-
.helpGroup('
|
|
18
|
-
.description('Data buckets:
|
|
22
|
+
.helpGroup('Resources:')
|
|
23
|
+
.description('Data buckets: records, time series, exports and tags');
|
|
19
24
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
25
|
+
listOptions(
|
|
26
|
+
bucket
|
|
27
|
+
.command('list')
|
|
28
|
+
.helpGroup('Records:')
|
|
29
|
+
.description('List the data buckets of the account')
|
|
30
|
+
.option('-j, --json', 'Output as JSON'),
|
|
31
|
+
).action(async (opts, cmd) => {
|
|
32
|
+
applyJsonFlag(opts);
|
|
33
|
+
ensureConfigured();
|
|
34
|
+
const user = getGlobalUser(cmd);
|
|
35
|
+
await runCommand({
|
|
36
|
+
start: 'Fetching buckets...',
|
|
37
|
+
fn: () => (hasListOptions(opts) ? listRecords('bucket', opts, user) : getBuckets(user)),
|
|
38
|
+
success: (buckets) => `Found ${buckets.length} bucket(s)`,
|
|
39
|
+
failure: 'Failed to list buckets',
|
|
40
|
+
onSuccess: (buckets) => {
|
|
41
|
+
if (isJsonMode()) {
|
|
42
|
+
printOk(buckets);
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
const idWidth = buckets.reduce(
|
|
46
|
+
(w, b) => Math.max(w, String(b.bucket ?? '').length),
|
|
47
|
+
0,
|
|
48
|
+
);
|
|
49
|
+
for (const b of buckets) {
|
|
50
|
+
const flag = b.enabled === false ? hint('disabled') : success('enabled ');
|
|
51
|
+
const extras = [
|
|
52
|
+
b.name && b.name !== b.bucket ? hint(`(${b.name})`) : '',
|
|
53
|
+
b.product ? info(`[${b.product}]`) : '',
|
|
54
|
+
b.config?.source ? hint(`source: ${b.config.source}`) : '',
|
|
55
|
+
].filter(Boolean);
|
|
56
|
+
console.log(
|
|
57
|
+
` ${flag} ${label(String(b.bucket ?? '(no id)').padEnd(idWidth))}${extras.length ? ' ' + extras.join(' ') : ''}`,
|
|
41
58
|
);
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
const extras = [
|
|
45
|
-
b.name && b.name !== b.bucket ? hint(`(${b.name})`) : '',
|
|
46
|
-
b.product ? info(`[${b.product}]`) : '',
|
|
47
|
-
b.config?.source ? hint(`source: ${b.config.source}`) : '',
|
|
48
|
-
].filter(Boolean);
|
|
49
|
-
console.log(
|
|
50
|
-
` ${flag} ${label(String(b.bucket ?? '(no id)').padEnd(idWidth))}${extras.length ? ' ' + extras.join(' ') : ''}`,
|
|
51
|
-
);
|
|
52
|
-
}
|
|
53
|
-
},
|
|
54
|
-
});
|
|
59
|
+
}
|
|
60
|
+
},
|
|
55
61
|
});
|
|
62
|
+
});
|
|
56
63
|
|
|
57
64
|
bucket
|
|
58
|
-
.command('read <
|
|
65
|
+
.command('read <bucket>')
|
|
66
|
+
.helpGroup('Data:')
|
|
59
67
|
.description('Read samples from a bucket (latest first by default)')
|
|
60
|
-
.option('-d, --device <
|
|
61
|
-
.option('-n, --items <n>', 'Number of samples
|
|
68
|
+
.option('-d, --device <device>', 'Series a product bucket keeps for this device')
|
|
69
|
+
.option('-n, --items <n>', 'Number of samples', parsePositiveInt('items'), 10)
|
|
62
70
|
.option('-m, --minutes <n>', 'Only the last N minutes', parsePositiveInt('minutes'))
|
|
63
|
-
.option('--sort <order>', 'asc or desc
|
|
71
|
+
.option('--sort <order>', 'asc or desc', 'desc')
|
|
64
72
|
.option('--agg <window>', 'Aggregation window, e.g. 5m, 1h')
|
|
65
73
|
.option('--agg-type <reducer>', 'Reducer: mean, max, min, sum, count, first, last…')
|
|
66
74
|
.option('--fields <list>', 'Comma-separated fields to return')
|
|
67
75
|
.option('--group-by <key>', 'Group key, e.g. device')
|
|
68
76
|
.option('-j, --json', 'Output as JSON')
|
|
69
|
-
.action(async (
|
|
77
|
+
.action(async (id, opts, cmd) => {
|
|
70
78
|
applyJsonFlag(opts);
|
|
71
79
|
ensureConfigured();
|
|
72
80
|
const user = getGlobalUser(cmd);
|
|
73
81
|
await runCommand({
|
|
74
|
-
start: `Reading ${
|
|
82
|
+
start: `Reading ${id}...`,
|
|
75
83
|
fn: () =>
|
|
76
84
|
getBucketData({
|
|
77
|
-
bucket:
|
|
85
|
+
bucket: id,
|
|
78
86
|
device: opts.device,
|
|
79
87
|
user,
|
|
80
88
|
items: opts.items,
|
|
@@ -35,9 +35,9 @@ export function registerDashboardCommands(program) {
|
|
|
35
35
|
const cli = identity().name;
|
|
36
36
|
const dashboard = program
|
|
37
37
|
.command('dashboard')
|
|
38
|
-
.helpGroup('
|
|
38
|
+
.helpGroup('Resources:')
|
|
39
39
|
.description(
|
|
40
|
-
'
|
|
40
|
+
'Dashboards: apps on the dashboard SDK (create, develop and publish them) and their records',
|
|
41
41
|
)
|
|
42
42
|
.addHelpText(
|
|
43
43
|
'after',
|
|
@@ -52,6 +52,7 @@ In CI, ${identity().envPrefix}_TOKEN and ${identity().envPrefix}_SERVER replace
|
|
|
52
52
|
|
|
53
53
|
dashboard
|
|
54
54
|
.command('init <dir>')
|
|
55
|
+
.helpGroup('Apps:')
|
|
55
56
|
.description(
|
|
56
57
|
'Create a dashboard app (TypeScript, on @thinger-io/dashboard-sdk) from a template of the SDK',
|
|
57
58
|
)
|
|
@@ -111,6 +112,7 @@ In CI, ${identity().envPrefix}_TOKEN and ${identity().envPrefix}_SERVER replace
|
|
|
111
112
|
|
|
112
113
|
dashboard
|
|
113
114
|
.command('dev [url]')
|
|
115
|
+
.helpGroup('Apps:')
|
|
114
116
|
.description(
|
|
115
117
|
'Link that opens the app on its own dev server (ng serve, Vite…) against the server, with a temporary token',
|
|
116
118
|
)
|
|
@@ -200,6 +202,7 @@ token (one per project).`,
|
|
|
200
202
|
|
|
201
203
|
dashboard
|
|
202
204
|
.command('publish [dir]')
|
|
205
|
+
.helpGroup('Apps:')
|
|
203
206
|
.description('Upload an app to a public storage and point dashboards at it')
|
|
204
207
|
.option('-s, --storage <id>', 'Storage of the app (created public if missing)')
|
|
205
208
|
.option(
|