qsys-cli 0.1.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/LICENSE +21 -0
- package/README.md +56 -0
- package/dist/bin.d.ts +2 -0
- package/dist/bin.js +24 -0
- package/dist/cli.d.ts +20 -0
- package/dist/cli.js +287 -0
- package/dist/format.d.ts +17 -0
- package/dist/format.js +47 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/package.json +53 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Robert Owens
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# qsys-cli
|
|
2
|
+
|
|
3
|
+
Control a running Q-SYS Core from the shell, over QSC's QRC protocol —
|
|
4
|
+
works against real Core hardware and against **Q-SYS Designer in Emulate
|
|
5
|
+
mode** (validated on a live Designer emulation). The human/scriptable
|
|
6
|
+
sibling of [`q-sys-mcp`](https://www.npmjs.com/package/q-sys-mcp), built on
|
|
7
|
+
the same [`qsys-qrc`](https://www.npmjs.com/package/qsys-qrc) client
|
|
8
|
+
(keepalive + transparent auto-reconnect included).
|
|
9
|
+
|
|
10
|
+
## Install
|
|
11
|
+
|
|
12
|
+
```sh
|
|
13
|
+
npm install -g qsys-cli # installs the `qsys` command
|
|
14
|
+
npx qsys-cli status # or one-shot, no install
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
(The npm package is `qsys-cli` — npm reserves the bare name `qsys` — but the
|
|
18
|
+
installed command is plain `qsys`.)
|
|
19
|
+
|
|
20
|
+
## Usage
|
|
21
|
+
|
|
22
|
+
```sh
|
|
23
|
+
export QSYS_HOST=192.168.1.10 # or pass --host on every call
|
|
24
|
+
|
|
25
|
+
qsys status # engine/design status
|
|
26
|
+
qsys ls --type gain # list components, filtered by type
|
|
27
|
+
qsys get MainGain # read a named control
|
|
28
|
+
qsys set MainGain -6 --ramp 2 # set it (negative values just work)
|
|
29
|
+
qsys get-component Gain1 # all controls of a component
|
|
30
|
+
qsys set-component Gain1 mute true
|
|
31
|
+
qsys watch MainGain --interval 0.2 # stream changes until Ctrl-C
|
|
32
|
+
qsys snapshot load Bank 1 --ramp 1
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Every command takes `--json` for machine-readable output (`watch` emits JSON lines).
|
|
36
|
+
Connection: `--host/--port/--user/--password` or `QSYS_HOST/QSYS_PORT/QSYS_USER/QSYS_PASSWORD`.
|
|
37
|
+
|
|
38
|
+
Values coerce naturally: `true`/`false` → boolean, numeric → number, anything else → string.
|
|
39
|
+
|
|
40
|
+
## Notes
|
|
41
|
+
|
|
42
|
+
- QRC has no way to enumerate named controls or snapshot banks — you need to
|
|
43
|
+
know their names from the design (`ls` lists components that have script
|
|
44
|
+
access enabled).
|
|
45
|
+
- `watch` uses QRC change groups; momentary trigger controls don't emit
|
|
46
|
+
change events (they never hold a value).
|
|
47
|
+
|
|
48
|
+
## Disclaimer
|
|
49
|
+
|
|
50
|
+
This is an independent open-source project, **not affiliated with, endorsed
|
|
51
|
+
by, or supported by QSC, LLC**. "Q-SYS" is a trademark of QSC. The CLI
|
|
52
|
+
speaks the publicly documented QRC protocol and contains no QSC code.
|
|
53
|
+
|
|
54
|
+
## License
|
|
55
|
+
|
|
56
|
+
MIT
|
package/dist/bin.d.ts
ADDED
package/dist/bin.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* qsys — control a running Q-SYS Core (or qsys-emulator) over QRC from the
|
|
4
|
+
* shell. The human/scriptable sibling of qsys-mcp, over the same QrcClient.
|
|
5
|
+
*
|
|
6
|
+
* qsys status --host 192.168.1.10
|
|
7
|
+
* qsys set MainGain -6 --ramp 2
|
|
8
|
+
* qsys watch MainGain --interval 0.2
|
|
9
|
+
*
|
|
10
|
+
* The published bin is the compiled dist/bin.js; in the workspace, run it
|
|
11
|
+
* via `npm -w qsys run qsys` (tsx) instead of executing this file directly.
|
|
12
|
+
*/
|
|
13
|
+
import { runCli } from './cli.js';
|
|
14
|
+
const abort = new AbortController();
|
|
15
|
+
process.on('SIGINT', () => abort.abort());
|
|
16
|
+
process.on('SIGTERM', () => abort.abort());
|
|
17
|
+
runCli(process.argv.slice(2), {
|
|
18
|
+
out: console.log,
|
|
19
|
+
err: console.error,
|
|
20
|
+
signal: abort.signal,
|
|
21
|
+
}).then((code) => process.exit(code), (err) => {
|
|
22
|
+
console.error('qsys: fatal:', err);
|
|
23
|
+
process.exit(1);
|
|
24
|
+
});
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export declare class UsageError extends Error {
|
|
2
|
+
}
|
|
3
|
+
export interface CliIo {
|
|
4
|
+
out: (line: string) => void;
|
|
5
|
+
err: (line: string) => void;
|
|
6
|
+
/** Aborting stops a `watch` stream (bin wires SIGINT/SIGTERM here). */
|
|
7
|
+
signal?: AbortSignal;
|
|
8
|
+
}
|
|
9
|
+
export declare const USAGE = "usage: qsys <command> [args...] [flags]\n\ncommands:\n status engine/design status\n ls [--type <substr>] [--filter <substr>] list components\n get <name...> read named control(s)\n set <name> <value> [--ramp <s>] set a named control\n get-component <comp> [ctrl...] read component controls (all if none given)\n set-component <comp> <ctrl> <value> [--ramp <s>] set a component control\n watch [--component <comp>] <ctrl...> [--interval <s>] stream changes until Ctrl-C\n snapshot load <bank> <slot> [--ramp <s>] recall a snapshot\n snapshot save <bank> <slot> save a snapshot\n\nconnection:\n --host <ip> Core/emulator address (or QSYS_HOST)\n --port <n> QRC port, default 1710 (or QSYS_PORT)\n --user/--password logon if the Core requires it (or QSYS_USER/QSYS_PASSWORD)\n --timeout <s> per-request timeout, default 10\n\noutput:\n --json machine-readable output (watch emits JSON lines)\n\nvalues: true/false \u2192 boolean, numeric \u2192 number, anything else \u2192 string.\nNegative values work as-is: qsys set MainGain -6";
|
|
10
|
+
interface ParsedArgs {
|
|
11
|
+
flags: Record<string, string | boolean>;
|
|
12
|
+
positionals: string[];
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Hand-rolled instead of node:util parseArgs so bare negative numbers stay
|
|
16
|
+
* positional — `qsys set MainGain -6` must not read `-6` as an option.
|
|
17
|
+
*/
|
|
18
|
+
export declare function parseCliArgs(argv: string[]): ParsedArgs;
|
|
19
|
+
export declare function runCli(argv: string[], io?: CliIo): Promise<number>;
|
|
20
|
+
export {};
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
import { QrcClient } from 'qsys-qrc';
|
|
2
|
+
import { CONTROL_HEADER, coerceValue, controlRow, renderKv, renderTable, } from './format.js';
|
|
3
|
+
export class UsageError extends Error {
|
|
4
|
+
}
|
|
5
|
+
export const USAGE = `usage: qsys <command> [args...] [flags]
|
|
6
|
+
|
|
7
|
+
commands:
|
|
8
|
+
status engine/design status
|
|
9
|
+
ls [--type <substr>] [--filter <substr>] list components
|
|
10
|
+
get <name...> read named control(s)
|
|
11
|
+
set <name> <value> [--ramp <s>] set a named control
|
|
12
|
+
get-component <comp> [ctrl...] read component controls (all if none given)
|
|
13
|
+
set-component <comp> <ctrl> <value> [--ramp <s>] set a component control
|
|
14
|
+
watch [--component <comp>] <ctrl...> [--interval <s>] stream changes until Ctrl-C
|
|
15
|
+
snapshot load <bank> <slot> [--ramp <s>] recall a snapshot
|
|
16
|
+
snapshot save <bank> <slot> save a snapshot
|
|
17
|
+
|
|
18
|
+
connection:
|
|
19
|
+
--host <ip> Core/emulator address (or QSYS_HOST)
|
|
20
|
+
--port <n> QRC port, default 1710 (or QSYS_PORT)
|
|
21
|
+
--user/--password logon if the Core requires it (or QSYS_USER/QSYS_PASSWORD)
|
|
22
|
+
--timeout <s> per-request timeout, default 10
|
|
23
|
+
|
|
24
|
+
output:
|
|
25
|
+
--json machine-readable output (watch emits JSON lines)
|
|
26
|
+
|
|
27
|
+
values: true/false → boolean, numeric → number, anything else → string.
|
|
28
|
+
Negative values work as-is: qsys set MainGain -6`;
|
|
29
|
+
/** Flags taking a value; everything else that starts with `--` must be boolean. */
|
|
30
|
+
const STRING_FLAGS = new Set([
|
|
31
|
+
'host', 'port', 'user', 'password', 'timeout',
|
|
32
|
+
'type', 'filter', 'ramp', 'component', 'interval',
|
|
33
|
+
]);
|
|
34
|
+
const BOOL_FLAGS = new Set(['json', 'help']);
|
|
35
|
+
/**
|
|
36
|
+
* Hand-rolled instead of node:util parseArgs so bare negative numbers stay
|
|
37
|
+
* positional — `qsys set MainGain -6` must not read `-6` as an option.
|
|
38
|
+
*/
|
|
39
|
+
export function parseCliArgs(argv) {
|
|
40
|
+
const flags = {};
|
|
41
|
+
const positionals = [];
|
|
42
|
+
for (let i = 0; i < argv.length; i++) {
|
|
43
|
+
const a = argv[i];
|
|
44
|
+
if (a === '--') {
|
|
45
|
+
positionals.push(...argv.slice(i + 1));
|
|
46
|
+
break;
|
|
47
|
+
}
|
|
48
|
+
if (a.startsWith('--')) {
|
|
49
|
+
const eq = a.indexOf('=');
|
|
50
|
+
const name = eq === -1 ? a.slice(2) : a.slice(2, eq);
|
|
51
|
+
if (BOOL_FLAGS.has(name)) {
|
|
52
|
+
if (eq !== -1)
|
|
53
|
+
throw new UsageError(`--${name} takes no value`);
|
|
54
|
+
flags[name] = true;
|
|
55
|
+
}
|
|
56
|
+
else if (STRING_FLAGS.has(name)) {
|
|
57
|
+
const value = eq === -1 ? argv[++i] : a.slice(eq + 1);
|
|
58
|
+
if (value === undefined)
|
|
59
|
+
throw new UsageError(`--${name} requires a value`);
|
|
60
|
+
flags[name] = value;
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
throw new UsageError(`unknown option --${name}`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
positionals.push(a);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return { flags, positionals };
|
|
71
|
+
}
|
|
72
|
+
function numFlag(raw, name) {
|
|
73
|
+
if (raw === undefined)
|
|
74
|
+
return undefined;
|
|
75
|
+
const n = Number(raw);
|
|
76
|
+
if (!Number.isFinite(n))
|
|
77
|
+
throw new UsageError(`invalid --${name}: ${String(raw)}`);
|
|
78
|
+
return n;
|
|
79
|
+
}
|
|
80
|
+
function need(positionals, count, shape) {
|
|
81
|
+
if (positionals.length < count)
|
|
82
|
+
throw new UsageError(`expected: ${shape}`);
|
|
83
|
+
}
|
|
84
|
+
export async function runCli(argv, io = { out: console.log, err: console.error }) {
|
|
85
|
+
let flags;
|
|
86
|
+
let positionals;
|
|
87
|
+
try {
|
|
88
|
+
({ flags, positionals } = parseCliArgs(argv));
|
|
89
|
+
}
|
|
90
|
+
catch (err) {
|
|
91
|
+
if (err instanceof UsageError) {
|
|
92
|
+
io.err(`qsys: ${err.message}`);
|
|
93
|
+
io.err(USAGE);
|
|
94
|
+
return 2;
|
|
95
|
+
}
|
|
96
|
+
throw err;
|
|
97
|
+
}
|
|
98
|
+
if (flags.help) {
|
|
99
|
+
io.out(USAGE);
|
|
100
|
+
return 0;
|
|
101
|
+
}
|
|
102
|
+
const cmd = positionals[0];
|
|
103
|
+
const rest = positionals.slice(1);
|
|
104
|
+
if (!cmd) {
|
|
105
|
+
io.err(USAGE);
|
|
106
|
+
return 2;
|
|
107
|
+
}
|
|
108
|
+
const host = flags.host ?? process.env.QSYS_HOST;
|
|
109
|
+
if (!host) {
|
|
110
|
+
io.err('qsys: --host (or QSYS_HOST) is required');
|
|
111
|
+
return 2;
|
|
112
|
+
}
|
|
113
|
+
let client;
|
|
114
|
+
try {
|
|
115
|
+
const port = numFlag(flags.port ?? process.env.QSYS_PORT, 'port') ?? 1710;
|
|
116
|
+
const timeoutS = numFlag(flags.timeout, 'timeout') ?? 10;
|
|
117
|
+
client = new QrcClient({
|
|
118
|
+
host,
|
|
119
|
+
port,
|
|
120
|
+
requestTimeoutMs: timeoutS * 1000,
|
|
121
|
+
// One-shot commands should fail fast; only a watch stream rides out drops.
|
|
122
|
+
reconnect: cmd === 'watch',
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
catch (err) {
|
|
126
|
+
if (err instanceof UsageError) {
|
|
127
|
+
io.err(`qsys: ${err.message}`);
|
|
128
|
+
return 2;
|
|
129
|
+
}
|
|
130
|
+
throw err;
|
|
131
|
+
}
|
|
132
|
+
const json = flags.json === true;
|
|
133
|
+
try {
|
|
134
|
+
await client.connect();
|
|
135
|
+
const user = flags.user ?? process.env.QSYS_USER;
|
|
136
|
+
const password = flags.password ?? process.env.QSYS_PASSWORD;
|
|
137
|
+
if (user !== undefined && password !== undefined)
|
|
138
|
+
await client.logon(user, password);
|
|
139
|
+
switch (cmd) {
|
|
140
|
+
case 'status': {
|
|
141
|
+
const s = await client.statusGet();
|
|
142
|
+
if (json) {
|
|
143
|
+
io.out(JSON.stringify(s, null, 2));
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
io.out(renderKv([
|
|
147
|
+
['Design', `${s.DesignName} (code ${s.DesignCode})`],
|
|
148
|
+
['Platform', s.Platform + (s.IsEmulator ? ' (emulator)' : '')],
|
|
149
|
+
['State', s.State],
|
|
150
|
+
['Status', `${s.Status.Code} ${s.Status.String}`],
|
|
151
|
+
['Redundant', String(s.IsRedundant)],
|
|
152
|
+
]));
|
|
153
|
+
}
|
|
154
|
+
return 0;
|
|
155
|
+
}
|
|
156
|
+
case 'ls': {
|
|
157
|
+
let comps = await client.getComponents();
|
|
158
|
+
const type = flags.type?.toLowerCase();
|
|
159
|
+
const filter = flags.filter?.toLowerCase();
|
|
160
|
+
if (type)
|
|
161
|
+
comps = comps.filter((c) => c.Type.toLowerCase().includes(type));
|
|
162
|
+
if (filter)
|
|
163
|
+
comps = comps.filter((c) => c.Name.toLowerCase().includes(filter));
|
|
164
|
+
if (json)
|
|
165
|
+
io.out(JSON.stringify(comps, null, 2));
|
|
166
|
+
else
|
|
167
|
+
io.out(renderTable(['NAME', 'TYPE'], comps.map((c) => [c.Name, c.Type])));
|
|
168
|
+
return 0;
|
|
169
|
+
}
|
|
170
|
+
case 'get': {
|
|
171
|
+
need(rest, 1, 'qsys get <name...>');
|
|
172
|
+
const controls = await client.getControl(rest);
|
|
173
|
+
printControls(io, controls, json);
|
|
174
|
+
return 0;
|
|
175
|
+
}
|
|
176
|
+
case 'set': {
|
|
177
|
+
need(rest, 2, 'qsys set <name> <value> [--ramp <s>]');
|
|
178
|
+
const [name, raw] = rest;
|
|
179
|
+
await client.setControl(name, coerceValue(raw), numFlag(flags.ramp, 'ramp'));
|
|
180
|
+
printControls(io, await client.getControl([name]), json); // echo the readback
|
|
181
|
+
return 0;
|
|
182
|
+
}
|
|
183
|
+
case 'get-component': {
|
|
184
|
+
need(rest, 1, 'qsys get-component <comp> [ctrl...]');
|
|
185
|
+
const [comp, ...ctrls] = rest;
|
|
186
|
+
const r = ctrls.length > 0
|
|
187
|
+
? await client.getComponent(comp, ctrls)
|
|
188
|
+
: await client.getComponentControls(comp);
|
|
189
|
+
printControls(io, r.Controls, json);
|
|
190
|
+
return 0;
|
|
191
|
+
}
|
|
192
|
+
case 'set-component': {
|
|
193
|
+
need(rest, 3, 'qsys set-component <comp> <ctrl> <value> [--ramp <s>]');
|
|
194
|
+
const [comp, ctrl, raw] = rest;
|
|
195
|
+
await client.setComponent(comp, [
|
|
196
|
+
{ Name: ctrl, Value: coerceValue(raw), Ramp: numFlag(flags.ramp, 'ramp') },
|
|
197
|
+
]);
|
|
198
|
+
printControls(io, (await client.getComponent(comp, [ctrl])).Controls, json);
|
|
199
|
+
return 0;
|
|
200
|
+
}
|
|
201
|
+
case 'snapshot': {
|
|
202
|
+
need(rest, 3, 'qsys snapshot <load|save> <bank> <slot>');
|
|
203
|
+
const [sub, bank, slotRaw] = rest;
|
|
204
|
+
const slot = Number(slotRaw);
|
|
205
|
+
if (!Number.isInteger(slot))
|
|
206
|
+
throw new UsageError(`invalid snapshot slot: ${slotRaw}`);
|
|
207
|
+
if (sub === 'load')
|
|
208
|
+
await client.snapshotLoad(bank, slot, numFlag(flags.ramp, 'ramp'));
|
|
209
|
+
else if (sub === 'save')
|
|
210
|
+
await client.snapshotSave(bank, slot);
|
|
211
|
+
else
|
|
212
|
+
throw new UsageError(`unknown snapshot subcommand: ${sub}`);
|
|
213
|
+
io.out(`snapshot ${sub}: ${bank} #${slot}`);
|
|
214
|
+
return 0;
|
|
215
|
+
}
|
|
216
|
+
case 'watch':
|
|
217
|
+
return await watch(client, rest, flags, json, io);
|
|
218
|
+
default:
|
|
219
|
+
throw new UsageError(`unknown command: ${cmd}`);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
catch (err) {
|
|
223
|
+
if (err instanceof UsageError) {
|
|
224
|
+
io.err(`qsys: ${err.message}`);
|
|
225
|
+
io.err(USAGE);
|
|
226
|
+
return 2;
|
|
227
|
+
}
|
|
228
|
+
io.err(`qsys: ${err.message}`);
|
|
229
|
+
return 1;
|
|
230
|
+
}
|
|
231
|
+
finally {
|
|
232
|
+
client.close();
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
function printControls(io, controls, json) {
|
|
236
|
+
if (json)
|
|
237
|
+
io.out(JSON.stringify(controls, null, 2));
|
|
238
|
+
else
|
|
239
|
+
io.out(renderTable(CONTROL_HEADER, controls.map(controlRow)));
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* Change-group stream: register the controls, print current values, then let
|
|
243
|
+
* the Core push changes via ChangeGroup.AutoPoll until the abort signal fires.
|
|
244
|
+
*/
|
|
245
|
+
async function watch(client, names, flags, json, io) {
|
|
246
|
+
need(names, 1, 'qsys watch [--component <comp>] <ctrl...>');
|
|
247
|
+
const component = flags.component;
|
|
248
|
+
const interval = numFlag(flags.interval, 'interval') ?? 0.5;
|
|
249
|
+
const id = `qsys-cli-${process.pid}`;
|
|
250
|
+
if (component)
|
|
251
|
+
await client.changeGroupAddComponentControl(id, component, names);
|
|
252
|
+
else
|
|
253
|
+
await client.changeGroupAddControl(id, names);
|
|
254
|
+
const emit = (c) => {
|
|
255
|
+
if (json)
|
|
256
|
+
io.out(JSON.stringify(c));
|
|
257
|
+
else
|
|
258
|
+
io.out(`${new Date().toISOString().slice(11, 23)} ${controlRow(c).join(' ').trimEnd()}`);
|
|
259
|
+
};
|
|
260
|
+
// First poll returns current values — the baseline before the stream starts.
|
|
261
|
+
for (const c of (await client.changeGroupPoll(id)).Changes)
|
|
262
|
+
emit(c);
|
|
263
|
+
await client.send('ChangeGroup.AutoPoll', { Id: id, Rate: interval });
|
|
264
|
+
const onNotification = (msg) => {
|
|
265
|
+
if (msg.method !== 'ChangeGroup.Poll' || msg.params?.Id !== id)
|
|
266
|
+
return;
|
|
267
|
+
for (const c of msg.params.Changes ?? [])
|
|
268
|
+
emit(c);
|
|
269
|
+
};
|
|
270
|
+
client.on('notification', onNotification);
|
|
271
|
+
await new Promise((resolve) => {
|
|
272
|
+
if (io.signal) {
|
|
273
|
+
// Only the abort signal ends the stream — a socket drop just reconnects.
|
|
274
|
+
if (io.signal.aborted)
|
|
275
|
+
return resolve();
|
|
276
|
+
io.signal.addEventListener('abort', () => resolve(), { once: true });
|
|
277
|
+
}
|
|
278
|
+
else {
|
|
279
|
+
// No signal (programmatic use): stream until the caller closes the client.
|
|
280
|
+
client.once('close', () => resolve());
|
|
281
|
+
}
|
|
282
|
+
});
|
|
283
|
+
client.off('notification', onNotification);
|
|
284
|
+
if (client.isConnected())
|
|
285
|
+
await client.changeGroupDestroy(id).catch(() => { });
|
|
286
|
+
return 0;
|
|
287
|
+
}
|
package/dist/format.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { ControlValue, QrcControl } from 'qsys-qrc';
|
|
2
|
+
/** A poll/get row; change-group polls tag component controls with their owner. */
|
|
3
|
+
export type ControlRow = QrcControl & {
|
|
4
|
+
Component?: string;
|
|
5
|
+
};
|
|
6
|
+
export declare const CONTROL_HEADER: string[];
|
|
7
|
+
export declare function controlRow(c: ControlRow): string[];
|
|
8
|
+
export declare function fmtValue(v: ControlValue | undefined): string;
|
|
9
|
+
/** Right-pad cells so columns align. No wrapping — terminals scroll. */
|
|
10
|
+
export declare function renderTable(header: string[], rows: string[][]): string;
|
|
11
|
+
/** Two-column key/value block (status output). */
|
|
12
|
+
export declare function renderKv(pairs: Array<[string, string]>): string;
|
|
13
|
+
/**
|
|
14
|
+
* CLI arg → QRC control value: `true`/`false` → boolean, numeric → number,
|
|
15
|
+
* anything else stays a string (e.g. a combo-box item name).
|
|
16
|
+
*/
|
|
17
|
+
export declare function coerceValue(raw: string): ControlValue;
|
package/dist/format.js
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
export const CONTROL_HEADER = ['NAME', 'VALUE', 'STRING', 'POSITION'];
|
|
2
|
+
export function controlRow(c) {
|
|
3
|
+
return [
|
|
4
|
+
c.Component ? `${c.Component}.${c.Name}` : c.Name,
|
|
5
|
+
fmtValue(c.Value),
|
|
6
|
+
c.String ?? '',
|
|
7
|
+
c.Position == null ? '' : c.Position.toFixed(3),
|
|
8
|
+
];
|
|
9
|
+
}
|
|
10
|
+
export function fmtValue(v) {
|
|
11
|
+
if (v === undefined)
|
|
12
|
+
return '';
|
|
13
|
+
if (typeof v === 'number' && !Number.isInteger(v)) {
|
|
14
|
+
return String(Number(v.toFixed(4)));
|
|
15
|
+
}
|
|
16
|
+
return String(v);
|
|
17
|
+
}
|
|
18
|
+
/** Right-pad cells so columns align. No wrapping — terminals scroll. */
|
|
19
|
+
export function renderTable(header, rows) {
|
|
20
|
+
const all = header.length > 0 ? [header, ...rows] : rows;
|
|
21
|
+
const cols = Math.max(...all.map((r) => r.length));
|
|
22
|
+
const widths = [];
|
|
23
|
+
for (let c = 0; c < cols; c++) {
|
|
24
|
+
widths.push(Math.max(...all.map((r) => (r[c] ?? '').length)));
|
|
25
|
+
}
|
|
26
|
+
return all
|
|
27
|
+
.map((r) => r.map((cell, c) => (cell ?? '').padEnd(widths[c])).join(' ').trimEnd())
|
|
28
|
+
.join('\n');
|
|
29
|
+
}
|
|
30
|
+
/** Two-column key/value block (status output). */
|
|
31
|
+
export function renderKv(pairs) {
|
|
32
|
+
return renderTable([], pairs.map(([k, v]) => [k, v]));
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* CLI arg → QRC control value: `true`/`false` → boolean, numeric → number,
|
|
36
|
+
* anything else stays a string (e.g. a combo-box item name).
|
|
37
|
+
*/
|
|
38
|
+
export function coerceValue(raw) {
|
|
39
|
+
if (raw === 'true')
|
|
40
|
+
return true;
|
|
41
|
+
if (raw === 'false')
|
|
42
|
+
return false;
|
|
43
|
+
const n = Number(raw);
|
|
44
|
+
if (raw.trim() !== '' && Number.isFinite(n))
|
|
45
|
+
return n;
|
|
46
|
+
return raw;
|
|
47
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "qsys-cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Human/scriptable CLI to control a Q-SYS Core over QRC — status, component inventory, get/set controls, watch, snapshots.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"default": "./dist/index.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"bin": {
|
|
15
|
+
"qsys": "dist/bin.js"
|
|
16
|
+
},
|
|
17
|
+
"scripts": {
|
|
18
|
+
"build": "tsc -p tsconfig.build.json",
|
|
19
|
+
"postbuild": "node -e \"require('fs').chmodSync('dist/bin.js', 0o755)\"",
|
|
20
|
+
"prepare": "npm run build",
|
|
21
|
+
"qsys": "tsx src/bin.ts --",
|
|
22
|
+
"test": "node ../../scripts/run-if-emulator.mjs \"tsx test/cli.ts\""
|
|
23
|
+
},
|
|
24
|
+
"files": [
|
|
25
|
+
"dist",
|
|
26
|
+
"README.md",
|
|
27
|
+
"LICENSE"
|
|
28
|
+
],
|
|
29
|
+
"repository": {
|
|
30
|
+
"type": "git",
|
|
31
|
+
"url": "git+https://github.com/reowens/qsys-tools.git",
|
|
32
|
+
"directory": "packages/qsys"
|
|
33
|
+
},
|
|
34
|
+
"bugs": {
|
|
35
|
+
"url": "https://github.com/reowens/qsys-tools/issues"
|
|
36
|
+
},
|
|
37
|
+
"homepage": "https://github.com/reowens/qsys-tools/tree/main/packages/qsys#readme",
|
|
38
|
+
"engines": {
|
|
39
|
+
"node": ">=18"
|
|
40
|
+
},
|
|
41
|
+
"license": "MIT",
|
|
42
|
+
"keywords": [
|
|
43
|
+
"q-sys",
|
|
44
|
+
"qsys",
|
|
45
|
+
"qsc",
|
|
46
|
+
"qrc",
|
|
47
|
+
"cli",
|
|
48
|
+
"av"
|
|
49
|
+
],
|
|
50
|
+
"dependencies": {
|
|
51
|
+
"qsys-qrc": "^0.1.0"
|
|
52
|
+
}
|
|
53
|
+
}
|