@altronix/cli 0.7.14 → 0.7.15

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.
@@ -0,0 +1,4 @@
1
+ import { Command } from 'commander';
2
+ export declare function getAbout(this: Command): Promise<void>;
3
+ export declare function getSite(this: Command): Promise<void>;
4
+ export declare function setSite(this: Command, site: string): Promise<void>;
package/dist/about.js ADDED
@@ -0,0 +1,46 @@
1
+ import { About, cborDecodeStr } from '@altronix/zdk';
2
+ import { Linq } from '@altronix/device';
3
+ import { EmptyError, finalize, firstValueFrom, map, switchMap } from 'rxjs';
4
+ import logger from './logger.js';
5
+ import { select, first } from './select.js';
6
+ export async function getAbout() {
7
+ logger(this.optsWithGlobals()['verbose'] ? 'silly' : 'info');
8
+ const linq = new Linq();
9
+ const path = '/api/v1/about';
10
+ const obs = linq.connections().pipe(this.optsWithGlobals()['first'] ? first() : select(), switchMap((sid) => linq.get(sid, path)), map((resp) => About.fromCbor(resp.body)), finalize(() => linq.shutdown()));
11
+ return firstValueFrom(obs)
12
+ .then((about) => console.log(JSON.stringify(about.toJson(), null, 2)))
13
+ .catch((e) => {
14
+ if (!(e instanceof EmptyError))
15
+ throw e;
16
+ });
17
+ }
18
+ export async function getSite() {
19
+ const log = logger(this.optsWithGlobals()['verbose'] ? 'silly' : 'info');
20
+ const linq = new Linq();
21
+ const path = '/api/v1/about/site';
22
+ const obs = linq.connections().pipe(this.optsWithGlobals()['first'] ? first() : select(), switchMap((sid) => linq.get(sid, path)), map((resp) => ({ site: cborDecodeStr(resp.body) })), finalize(() => linq.shutdown()));
23
+ return firstValueFrom(obs)
24
+ .then((site) => log.info(`request complete`, { ...site }))
25
+ .then(() => void 0)
26
+ .catch((e) => {
27
+ if (!(e instanceof EmptyError)) {
28
+ throw e;
29
+ }
30
+ });
31
+ }
32
+ export async function setSite(site) {
33
+ const log = logger(this.optsWithGlobals()['verbose'] ? 'silly' : 'info');
34
+ const linq = new Linq();
35
+ const path = '/api/v1/about/site';
36
+ const obs = linq.connections().pipe(this.optsWithGlobals()['first'] ? first() : select(), switchMap((sid) => linq.put(sid, path, site)), finalize(() => linq.shutdown()));
37
+ return firstValueFrom(obs)
38
+ .then(({ meta }) => {
39
+ log.info(`request complete`, { code: meta.code, message: meta.mesg });
40
+ })
41
+ .catch((e) => {
42
+ if (!(e instanceof EmptyError)) {
43
+ throw e;
44
+ }
45
+ });
46
+ }
package/dist/build.ui.js CHANGED
@@ -1,6 +1,7 @@
1
- import React, { useLayoutEffect, useEffect, useState } from 'react';
2
- import { Box, Text, useStdout } from 'ink';
1
+ import React, { useLayoutEffect, useState } from 'react';
2
+ import { Box, Text } from 'ink';
3
3
  import { scan } from 'rxjs';
4
+ import useStdoutDimensions from './useStdoutDimensions.js';
4
5
  export class BuildError extends Error {
5
6
  constructor(item, kind, message) {
6
7
  super(message);
@@ -92,18 +93,3 @@ function useBuildEffect(obs$, items, cb) {
92
93
  }, [obs$]);
93
94
  return progress;
94
95
  }
95
- function useStdoutDimensions() {
96
- const { stdout } = useStdout();
97
- const [dimensions, setDimensions] = useState([
98
- stdout.columns,
99
- stdout.rows
100
- ]);
101
- useEffect(() => {
102
- const handler = () => setDimensions([stdout.columns, stdout.rows]);
103
- stdout.on('resize', handler);
104
- return () => {
105
- stdout.off('resize', handler);
106
- };
107
- }, [stdout]);
108
- return dimensions;
109
- }
@@ -0,0 +1,3 @@
1
+ import { Command } from 'commander';
2
+ export declare function getCloud(this: Command): Promise<void>;
3
+ export declare function setCloud(this: Command, endpoint: string): Promise<void>;
package/dist/cloud.js ADDED
@@ -0,0 +1,47 @@
1
+ import { cborDecodeNum, NetCloud } from '@altronix/zdk';
2
+ import { Linq } from '@altronix/device';
3
+ import { concat, EmptyError, finalize, lastValueFrom, map, reduce, switchMap } from 'rxjs';
4
+ import logger from './logger.js';
5
+ import { select, first } from './select.js';
6
+ export async function getCloud() {
7
+ logger(this.optsWithGlobals()['verbose'] ? 'silly' : 'info');
8
+ const linq = new Linq();
9
+ const path = '/api/v1/net/cloud';
10
+ const status = '/api/v1/net/cloud/status';
11
+ const obs = linq.connections().pipe(this.optsWithGlobals()['first'] ? first() : select(), switchMap((sid) => {
12
+ const o0 = linq
13
+ .get(sid, path)
14
+ .pipe(map((resp) => NetCloud.fromCbor(resp.body).toJson()));
15
+ const o1 = linq
16
+ .get(sid, status)
17
+ .pipe(map((resp) => ({ status: Number(cborDecodeNum(resp.body)) })));
18
+ return concat(o0, o1);
19
+ }), reduce((acc, curr) => ({ ...acc, ...curr })), finalize(() => linq.shutdown()));
20
+ return lastValueFrom(obs)
21
+ .then((resp) => console.log(JSON.stringify(resp, null, 2)))
22
+ .catch((e) => {
23
+ if (!(e instanceof EmptyError))
24
+ throw e;
25
+ });
26
+ }
27
+ export async function setCloud(endpoint) {
28
+ logger(this.optsWithGlobals()['verbose'] ? 'silly' : 'info');
29
+ const [ip, port] = endpoint.split(':');
30
+ if (!port)
31
+ throw new Error('invalid format! enter ${ip}:${port}');
32
+ const body = new NetCloud({ ip, port: parseInt(port), portEn: true });
33
+ const path = '/api/v1/net/cloud';
34
+ const save = '/api/v1/exe/saveAndReboot';
35
+ const linq = new Linq();
36
+ const obs = linq.connections().pipe(this.optsWithGlobals()['first'] ? first() : select(), switchMap((sid) => {
37
+ const o0 = linq.put(sid, path, body);
38
+ const o1 = linq.get(sid, save);
39
+ return concat(o0, o1);
40
+ }), finalize(() => linq.shutdown()));
41
+ return lastValueFrom(obs)
42
+ .then(({ meta }) => console.log(JSON.stringify(meta.toJson(), null, 2)))
43
+ .catch((e) => {
44
+ if (!(e instanceof EmptyError))
45
+ throw e;
46
+ });
47
+ }
package/dist/exe.d.ts ADDED
@@ -0,0 +1,5 @@
1
+ import { Command } from 'commander';
2
+ export declare function reboot(this: Command): Promise<void>;
3
+ export declare function save(this: Command): Promise<void>;
4
+ export declare function saveAndReboot(this: Command): Promise<void>;
5
+ export declare function erase(this: Command): Promise<void>;
package/dist/exe.js ADDED
@@ -0,0 +1,52 @@
1
+ import { Linq } from '@altronix/device';
2
+ import { EmptyError, finalize, firstValueFrom, switchMap } from 'rxjs';
3
+ import logger from './logger.js';
4
+ import { select, first } from './select.js';
5
+ export async function reboot() {
6
+ logger(this.optsWithGlobals()['verbose'] ? 'silly' : 'info');
7
+ const linq = new Linq();
8
+ const path = '/api/v1/exe/reboot';
9
+ const obs = linq.connections().pipe(this.optsWithGlobals()['first'] ? first() : select(), switchMap((sid) => linq.get(sid, path)), finalize(() => linq.shutdown()));
10
+ return firstValueFrom(obs)
11
+ .then(({ meta }) => console.log(JSON.stringify(meta.toJson(), null, 2)))
12
+ .catch((e) => {
13
+ if (!(e instanceof EmptyError))
14
+ throw e;
15
+ });
16
+ }
17
+ export async function save() {
18
+ logger(this.optsWithGlobals()['verbose'] ? 'silly' : 'info');
19
+ const linq = new Linq();
20
+ const path = '/api/v1/exe/save';
21
+ const obs = linq.connections().pipe(this.optsWithGlobals()['first'] ? first() : select(), switchMap((sid) => linq.get(sid, path)), finalize(() => linq.shutdown()));
22
+ return firstValueFrom(obs)
23
+ .then(({ meta }) => console.log(JSON.stringify(meta.toJson(), null, 2)))
24
+ .catch((e) => {
25
+ if (!(e instanceof EmptyError))
26
+ throw e;
27
+ });
28
+ }
29
+ export async function saveAndReboot() {
30
+ logger(this.optsWithGlobals()['verbose'] ? 'silly' : 'info');
31
+ const linq = new Linq();
32
+ const path = '/api/v1/exe/saveAndReboot';
33
+ const obs = linq.connections().pipe(this.optsWithGlobals()['first'] ? first() : select(), switchMap((sid) => linq.get(sid, path)), finalize(() => linq.shutdown()));
34
+ return firstValueFrom(obs)
35
+ .then(({ meta }) => console.log(JSON.stringify(meta.toJson(), null, 2)))
36
+ .catch((e) => {
37
+ if (!(e instanceof EmptyError))
38
+ throw e;
39
+ });
40
+ }
41
+ export async function erase() {
42
+ logger(this.optsWithGlobals()['verbose'] ? 'silly' : 'info');
43
+ const linq = new Linq();
44
+ const path = '/api/v1/exe/erase';
45
+ const obs = linq.connections().pipe(this.optsWithGlobals()['first'] ? first() : select(), switchMap((sid) => linq.get(sid, path)), finalize(() => linq.shutdown()));
46
+ return firstValueFrom(obs)
47
+ .then(({ meta }) => console.log(JSON.stringify(meta.toJson(), null, 2)))
48
+ .catch((e) => {
49
+ if (!(e instanceof EmptyError))
50
+ throw e;
51
+ });
52
+ }
package/dist/index.js CHANGED
@@ -3,8 +3,14 @@ import { program } from 'commander';
3
3
  import path from 'path';
4
4
  import fs from 'fs';
5
5
  import { build } from './build.js';
6
- //import { plugins } from "./plugin.js";
6
+ import { getAbout, getSite, setSite } from './about.js';
7
+ import { reboot, save, saveAndReboot } from './exe.js';
8
+ import { getNet, setNet, setDhcp } from './net.js';
7
9
  import { fileURLToPath } from 'url';
10
+ import { getCloud, setCloud } from './cloud.js';
11
+ import { stress } from './stress.js';
12
+ import { update } from './update.js';
13
+ import { listen } from './listen.js';
8
14
  // https://stackoverflow.com/questions/8817423/why-is-dirname-not-defined-in-node-repl
9
15
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
10
16
  // Parse package.json to get version
@@ -15,6 +21,7 @@ program
15
21
  .name('atx')
16
22
  .description('build atx zdk projects')
17
23
  .version(ver)
24
+ .option('-Q, --quiet', 'no logs')
18
25
  .option('-VV, --verbose', 'extra logging');
19
26
  // Scan command
20
27
  /*
@@ -39,6 +46,76 @@ program
39
46
  .option('-W, --wasm', 'build wasm')
40
47
  .option('-y, --yes', 'answer yes automatically')
41
48
  .action(build);
49
+ const device = program
50
+ .command('device')
51
+ .description('manage zephyr device settings')
52
+ .option('-p, --port <PORT>', 'listen port for incoming device connections')
53
+ .option('-v, --verbose', 'print extra debug informating during request')
54
+ .option('-f, --first', 'perform request on first device we see (any device)');
55
+ device
56
+ .command('get-about')
57
+ .description('get about data on the device')
58
+ .action(getAbout);
59
+ device
60
+ .command('get-site')
61
+ .description('get the site ID of the device')
62
+ .action(getSite);
63
+ device
64
+ .command('set-site')
65
+ .description('set the site ID of the device')
66
+ .argument('<site>', 'new site id')
67
+ .action(setSite);
68
+ device
69
+ .command('save')
70
+ .description('save data to persistant storage. (does not reboot)')
71
+ .action(save);
72
+ device
73
+ .command('reboot')
74
+ .description('reboot the device. (does not save)')
75
+ .action(reboot);
76
+ device
77
+ .command('save-reboot')
78
+ .description('save data to persistant storage and reboot the device')
79
+ .action(saveAndReboot);
80
+ device
81
+ .command('get-net')
82
+ .description('get network configuration from the device')
83
+ .action(getNet);
84
+ device
85
+ .command('set-net')
86
+ .description('set network interface into static ip mode')
87
+ .argument('<ip>', 'the new IP address')
88
+ .argument('<sn>', 'the new SUBNET mask')
89
+ .argument('<gw>', 'the new GATEWAY address')
90
+ .action(setNet);
91
+ device
92
+ .command('set-dhcp')
93
+ .description('set network interface into DHCP mode')
94
+ .action(setDhcp);
95
+ device
96
+ .command('get-cloud')
97
+ .description('get cloud endpoint on the device')
98
+ .action(getCloud);
99
+ device
100
+ .command('set-cloud')
101
+ .description('set cloud endpoint on the device')
102
+ .argument('<endpoint>', 'cloud service location')
103
+ .action(setCloud);
104
+ device
105
+ .command('stress')
106
+ .description('run a stress test on a device')
107
+ .argument('<count>', 'how many requests to make')
108
+ .action(stress);
109
+ device
110
+ .command('update')
111
+ .description('run firmware update from file')
112
+ .argument('<file>', 'file to update device with')
113
+ .action(update);
114
+ device
115
+ .command('listen')
116
+ .description('listen for alerts and heartbeats')
117
+ .argument('<duration>', 'how long to listen')
118
+ .action(listen);
42
119
  // Load plugins
43
120
  // (await plugins()).forEach(({ plugin: _, path: __, description: ___ }) => {});
44
121
  program.parseAsync().catch((e) => console.error(e));
@@ -0,0 +1,2 @@
1
+ import { Command } from 'commander';
2
+ export declare function listen(this: Command, ms: number): Promise<void>;
package/dist/listen.js ADDED
@@ -0,0 +1,23 @@
1
+ import { Linq } from '@altronix/device';
2
+ import { first, map, merge, timer } from 'rxjs';
3
+ import logger from './logger.js';
4
+ import { quit } from './quit.js';
5
+ export async function listen(ms) {
6
+ const log = logger(this.optsWithGlobals()['verbose'] ? 'silly' : 'info');
7
+ const linq = new Linq();
8
+ const abort = ms > 0
9
+ ? merge(timer(ms), quit()).pipe(first(), map(() => void 0))
10
+ : quit();
11
+ abort.subscribe(() => linq.shutdown());
12
+ linq.connections().subscribe(({ about, transport }) => {
13
+ log.info('new connection', { ...about, transport });
14
+ });
15
+ linq.alerts().subscribe(({ about, transport, alert }) => {
16
+ const { sid: serial } = about;
17
+ log.info('new alert', { serial, transport, ...alert.toJson() });
18
+ });
19
+ linq.heartbeats().subscribe(({ about, transport, heartbeat }) => {
20
+ const { sid: serial } = about;
21
+ log.info('new heartbeat', { serial, transport, ...heartbeat.toJson() });
22
+ });
23
+ }
@@ -0,0 +1,2 @@
1
+ declare const _default: (level?: string) => import("@altronix/device/build/ioc/interfaces.js").Logger;
2
+ export default _default;
package/dist/logger.js ADDED
@@ -0,0 +1,8 @@
1
+ import { timestamp, inliner, installLogger } from '@altronix/device';
2
+ import { createLogger, format, transports } from 'winston';
3
+ export default (level) => installLogger(createLogger({
4
+ level,
5
+ silent: level ? false : true,
6
+ format: format.combine(inliner(), timestamp()),
7
+ transports: [new transports.Console()]
8
+ }));
package/dist/net.d.ts ADDED
@@ -0,0 +1,4 @@
1
+ import { Command } from 'commander';
2
+ export declare function getNet(this: Command): Promise<void>;
3
+ export declare function setNet(this: Command, ip: string, sn: string, gw: string): Promise<void>;
4
+ export declare function setDhcp(this: Command): Promise<void>;
package/dist/net.js ADDED
@@ -0,0 +1,53 @@
1
+ import { concat, EmptyError, finalize, firstValueFrom, lastValueFrom, map, switchMap } from 'rxjs';
2
+ import { Linq } from '@altronix/device';
3
+ import { NetIp } from '@altronix/zdk';
4
+ import logger from './logger.js';
5
+ import { select, first } from './select.js';
6
+ export async function getNet() {
7
+ logger(this.optsWithGlobals()['verbose'] ? 'silly' : 'info');
8
+ const linq = new Linq();
9
+ const path = '/api/v1/net/ip';
10
+ const obs = linq.connections().pipe(this.optsWithGlobals()['first'] ? first() : select(), switchMap((sid) => linq.get(sid, path)), map((resp) => NetIp.fromCbor(resp.body)), finalize(() => linq.shutdown()));
11
+ return firstValueFrom(obs)
12
+ .then((resp) => console.log(JSON.stringify(resp.toJson(), null, 2)))
13
+ .catch((e) => {
14
+ if (!(e instanceof EmptyError))
15
+ throw e;
16
+ });
17
+ }
18
+ export async function setNet(ip, sn, gw) {
19
+ logger(this.optsWithGlobals()['verbose'] ? 'silly' : 'info');
20
+ const linq = new Linq();
21
+ const path = '/api/v1/net/ip';
22
+ const save = '/api/v1/exe/saveAndReboot';
23
+ const body = new NetIp({ ip, sn, gw, dhcp: false });
24
+ const obs = linq.connections().pipe(this.optsWithGlobals()['first'] ? first() : select(), switchMap((sid) => {
25
+ const o0 = linq.put(sid, path, body);
26
+ const o1 = linq.get(sid, save);
27
+ return concat(o0, o1);
28
+ }), finalize(() => linq.shutdown()));
29
+ return lastValueFrom(obs)
30
+ .then(({ meta }) => console.log(JSON.stringify(meta.toJson(), null, 2)))
31
+ .catch((e) => {
32
+ if (!(e instanceof EmptyError))
33
+ throw e;
34
+ });
35
+ }
36
+ export async function setDhcp() {
37
+ logger(this.optsWithGlobals()['verbose'] ? 'silly' : 'info');
38
+ const linq = new Linq();
39
+ const path = '/api/v1/net/ip';
40
+ const save = '/api/v1/exe/saveAndReboot';
41
+ const body = new NetIp({ dhcp: true });
42
+ const obs = linq.connections().pipe(this.optsWithGlobals()['first'] ? first() : select(), switchMap((sid) => {
43
+ const o0 = linq.put(sid, path, body);
44
+ const o1 = linq.get(sid, save);
45
+ return concat(o0, o1);
46
+ }), finalize(() => linq.shutdown()));
47
+ return lastValueFrom(obs)
48
+ .then(({ meta }) => console.log(JSON.stringify(meta.toJson(), null, 2)))
49
+ .catch((e) => {
50
+ if (!(e instanceof EmptyError))
51
+ throw e;
52
+ });
53
+ }
@@ -0,0 +1,2 @@
1
+ import { OperatorFunction } from 'rxjs';
2
+ export default function <R>(total: number): OperatorFunction<R, void>;
@@ -0,0 +1,13 @@
1
+ import React from 'react';
2
+ import { Observable } from 'rxjs';
3
+ import { render } from 'ink';
4
+ import Progress from './progress.ui.js';
5
+ export default function (total) {
6
+ return (obs$) => new Observable((subscriber) => {
7
+ const renderer = render(React.createElement(Progress, { total: total, "response$": obs$, onComplete: () => {
8
+ renderer.unmount();
9
+ subscriber.next();
10
+ subscriber.complete();
11
+ } }));
12
+ });
13
+ }
@@ -0,0 +1,12 @@
1
+ import React from 'react';
2
+ import { Observable } from 'rxjs';
3
+ interface OnCompleteCallback {
4
+ (): void;
5
+ }
6
+ interface ProgressOptions<R> {
7
+ total: number;
8
+ response$: Observable<R>;
9
+ onComplete: OnCompleteCallback;
10
+ }
11
+ export default function Progress<R>({ total, response$, onComplete }: ProgressOptions<R>): React.JSX.Element;
12
+ export {};
@@ -0,0 +1,35 @@
1
+ import React, { useLayoutEffect, useState } from 'react';
2
+ import { scan } from 'rxjs';
3
+ import { Box, Text } from 'ink';
4
+ import useDimensions from './useStdoutDimensions.js';
5
+ export default function Progress({ total, response$, onComplete }) {
6
+ const [cols] = useDimensions();
7
+ const [width, setWidth] = useState(Math.min(cols - 4, 76));
8
+ const [progress, empty] = useProgress(response$, total, width, onComplete);
9
+ useLayoutEffect(() => setWidth(Math.min(cols - 4, 80)), [width]);
10
+ return (React.createElement(Box, null,
11
+ React.createElement(Box, null,
12
+ React.createElement(Text, null, "["),
13
+ new Array(progress).fill(0).map((_, idx) => (React.createElement(Text, { key: idx }, "#"))),
14
+ new Array(empty).fill(0).map((_, idx) => (React.createElement(Text, { key: idx }, "-"))),
15
+ React.createElement(Text, null, "]")),
16
+ React.createElement(Box, { marginLeft: 1 },
17
+ React.createElement(Text, null,
18
+ Math.ceil((100 * progress) / width),
19
+ "%"))));
20
+ }
21
+ function useProgress(obs$, total, width, onComplete) {
22
+ const [progress, setProgress] = useState([0, width]);
23
+ useLayoutEffect(() => {
24
+ const s = obs$.pipe(scan((acc) => acc + 1, 0)).subscribe({
25
+ complete: onComplete,
26
+ error: (error) => console.error(error),
27
+ next: (n) => {
28
+ const progress = Math.ceil((width * n) / total);
29
+ setProgress([progress, width - progress]);
30
+ }
31
+ });
32
+ return () => s.unsubscribe();
33
+ }, [obs$, width, total]);
34
+ return progress;
35
+ }
package/dist/quit.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ import { Observable } from 'rxjs';
2
+ export declare function quit(): Observable<void>;
package/dist/quit.js ADDED
@@ -0,0 +1,30 @@
1
+ import { Observable } from 'rxjs';
2
+ import { emitKeypressEvents } from 'node:readline';
3
+ export function quit() {
4
+ return new Observable((subscriber) => {
5
+ if (process.stdin.isTTY) {
6
+ process.stdin.setRawMode(true);
7
+ }
8
+ emitKeypressEvents(process.stdin);
9
+ function onQuit() {
10
+ subscriber.next();
11
+ subscriber.complete();
12
+ }
13
+ function onkeypress(data) {
14
+ if (data.indexOf('q') >= 0)
15
+ onQuit();
16
+ }
17
+ process.stdin.on('keypress', onkeypress);
18
+ process.stdin.on('error', onQuit);
19
+ process.stdin.on('end', onQuit);
20
+ process.stdin.on('close', onQuit);
21
+ return () => {
22
+ // https://stackoverflow.com/questions/59220095/node-doesnt-exit-automatically-once-a-listener-is-set-on-stdin
23
+ process.stdin.unref();
24
+ process.stdin.off('keypress', onkeypress);
25
+ process.stdin.off('error', onQuit);
26
+ process.stdin.off('end', onQuit);
27
+ process.stdin.off('close', onQuit);
28
+ };
29
+ });
30
+ }
@@ -0,0 +1,4 @@
1
+ import { OperatorFunction } from 'rxjs';
2
+ import { NewChannel } from '@altronix/device';
3
+ export declare function select(): OperatorFunction<NewChannel, string>;
4
+ export declare function first(): OperatorFunction<NewChannel, string>;
package/dist/select.js ADDED
@@ -0,0 +1,18 @@
1
+ import React from 'react';
2
+ import { first as rxFirst, map, Observable } from 'rxjs';
3
+ import { render } from 'ink';
4
+ import Select from './select.ui.js';
5
+ export function select() {
6
+ return (obs$) => new Observable((subscriber) => {
7
+ const renderer = render(React.createElement(Select, { "connections$": obs$.pipe(map(({ about }) => about)), onSelect: (result) => {
8
+ renderer.unmount();
9
+ renderer.cleanup();
10
+ if (result)
11
+ subscriber.next(result);
12
+ subscriber.complete();
13
+ } }));
14
+ });
15
+ }
16
+ export function first() {
17
+ return (obs$) => obs$.pipe(rxFirst(), map(({ about }) => about.sid));
18
+ }
@@ -0,0 +1,11 @@
1
+ import { AboutProps } from '@altronix/zdk';
2
+ import React from 'react';
3
+ import { Observable } from 'rxjs';
4
+ export interface OnSelectCallback {
5
+ (serial?: string): void;
6
+ }
7
+ export interface Options {
8
+ connections$: Observable<AboutProps>;
9
+ onSelect: OnSelectCallback;
10
+ }
11
+ export default function ({ connections$, onSelect }: Options): React.JSX.Element;
@@ -0,0 +1,72 @@
1
+ import React, { useLayoutEffect, useState } from 'react';
2
+ import { scan } from 'rxjs';
3
+ import { Box, Text, useInput } from 'ink';
4
+ // Main ui component
5
+ export default function ({ connections$, onSelect }) {
6
+ const connections = useConnections(connections$);
7
+ const dimensions = useTableDimensions(connections);
8
+ const [selected, setSelected] = useState(0);
9
+ useInput((input, key) => {
10
+ if (input === 'q')
11
+ onSelect();
12
+ if (input === 'k' || key.upArrow) {
13
+ setSelected(selected == 0 ? connections.length - 1 : selected - 1);
14
+ }
15
+ else if (input === 'j' || key.downArrow) {
16
+ setSelected(selected == connections.length - 1 ? 0 : selected + 1);
17
+ }
18
+ else if (key.return && connections[selected]) {
19
+ onSelect(connections[selected].sid);
20
+ }
21
+ });
22
+ return (React.createElement(Box, { flexDirection: "column", width: 80, marginBottom: 1 },
23
+ React.createElement(Box, { flexDirection: "column", width: 80, marginBottom: 1 },
24
+ React.createElement(Text, { underline: true }, "Please select device for request"),
25
+ React.createElement(Text, { dimColor: true },
26
+ '\u2191',
27
+ " move cursor up"),
28
+ React.createElement(Text, { dimColor: true },
29
+ '\u2193',
30
+ " move cursor down"),
31
+ React.createElement(Text, { dimColor: true },
32
+ '\u23ce',
33
+ " make request"),
34
+ React.createElement(Text, { dimColor: true }, "Q to quit")),
35
+ connections.map(({ sid, board, site }, idx) => (React.createElement(Box, { key: idx },
36
+ React.createElement(Box, { width: 2, marginRight: 1 },
37
+ React.createElement(Text, { bold: selected == idx, underline: selected == idx },
38
+ idx,
39
+ ":")),
40
+ React.createElement(Box, { width: dimensions[0], marginRight: 1 },
41
+ React.createElement(Text, { wrap: "truncate", color: "yellow", bold: selected == idx, underline: selected == idx }, board)),
42
+ React.createElement(Box, { width: dimensions[1], marginRight: 1 },
43
+ React.createElement(Text, { wrap: "truncate", color: "blue", bold: selected == idx, underline: selected == idx }, site)),
44
+ React.createElement(Box, { width: dimensions[2], marginRight: 1 },
45
+ React.createElement(Text, { wrap: "truncate", color: "magenta", bold: selected == idx, underline: selected == idx }, sid)))))));
46
+ }
47
+ function useConnections(obs$) {
48
+ const [connections, setConnections] = useState([]);
49
+ useLayoutEffect(() => {
50
+ const s = obs$
51
+ .pipe(scan((acc, next) => [...acc, next], connections))
52
+ .subscribe({ next: (connections) => setConnections(connections) });
53
+ return () => s.unsubscribe();
54
+ }, [obs$]);
55
+ return connections;
56
+ }
57
+ function useTableDimensions(connections) {
58
+ const [dimensions, setDimensions] = useState([0, 0, 0]);
59
+ useLayoutEffect(() => {
60
+ const next = connections
61
+ .map((a) => [a.board.length, a.site.length, a.sid.length])
62
+ .reduce((acc, next) => {
63
+ return [
64
+ next[0] > acc[0] ? next[0] : acc[0],
65
+ next[1] > acc[1] ? next[1] : acc[1],
66
+ next[2] > acc[2] ? next[2] : acc[2]
67
+ ];
68
+ }, dimensions);
69
+ setDimensions(next);
70
+ }, [connections]);
71
+ return dimensions;
72
+ }
@@ -0,0 +1,2 @@
1
+ import { Command } from 'commander';
2
+ export declare function stress(this: Command, n: number): Promise<void>;
package/dist/stress.js ADDED
@@ -0,0 +1,23 @@
1
+ import { METH_CONSTANTS } from '@altronix/zdk';
2
+ import { Linq } from '@altronix/device';
3
+ import { EmptyError, finalize, lastValueFrom, switchMap, pipe, repeat } from 'rxjs';
4
+ import progress from './progress.js';
5
+ import logger from './logger.js';
6
+ import { select, first } from './select.js';
7
+ export async function stress(n) {
8
+ logger(this.optsWithGlobals()['verbose'] ? 'silly' : 'info');
9
+ const linq = new Linq();
10
+ const path = '/api/v1/about';
11
+ const request = {
12
+ path,
13
+ meth: METH_CONSTANTS.GET,
14
+ retry: 100,
15
+ timeout: 500
16
+ };
17
+ const progress$ = pipe(repeat(n), progress(n));
18
+ const obs = linq.connections().pipe(this.optsWithGlobals()['first'] ? first() : select(), switchMap((id) => linq.request(id, request).pipe(progress$)), finalize(() => linq.shutdown()));
19
+ return lastValueFrom(obs).catch((e) => {
20
+ if (!(e instanceof EmptyError))
21
+ throw e;
22
+ });
23
+ }
@@ -0,0 +1,3 @@
1
+ import { OperatorFunction } from 'rxjs';
2
+ import { ResponseBody } from '@altronix/device';
3
+ export default function (): OperatorFunction<ResponseBody, void>;
@@ -0,0 +1,31 @@
1
+ import React, { useLayoutEffect, useState } from 'react';
2
+ import { Observable, scan } from 'rxjs';
3
+ import { Box, render, Text } from 'ink';
4
+ export default function () {
5
+ return (obs$) => new Observable((subscriber) => {
6
+ const renderer = render(React.createElement(View, { "response$": obs$, onComplete: () => {
7
+ renderer.unmount();
8
+ subscriber.next();
9
+ subscriber.complete();
10
+ } }));
11
+ });
12
+ }
13
+ function View({ response$, onComplete }) {
14
+ const responses = useResponses(response$, onComplete);
15
+ return (React.createElement(Box, null,
16
+ React.createElement(Text, null, "Response count:"),
17
+ React.createElement(Text, null, responses.length)));
18
+ }
19
+ function useResponses(obs$, onComplete) {
20
+ const [responses, setResponses] = useState([]);
21
+ useLayoutEffect(() => {
22
+ const s = obs$
23
+ .pipe(scan((acc, next) => [...acc, next], responses))
24
+ .subscribe({
25
+ next: (responses) => setResponses(responses),
26
+ complete: onComplete
27
+ });
28
+ return () => s.unsubscribe();
29
+ }, [obs$]);
30
+ return responses;
31
+ }
@@ -0,0 +1,2 @@
1
+ import { Command } from 'commander';
2
+ export declare function update(this: Command, file: string): Promise<void>;
package/dist/update.js ADDED
@@ -0,0 +1,59 @@
1
+ import { Linq } from '@altronix/device';
2
+ import { Update } from '@altronix/zdk';
3
+ import { concat, defer, EmptyError, finalize, from, lastValueFrom, map, mergeScan, switchMap } from 'rxjs';
4
+ import fs from 'fs';
5
+ import logger from './logger.js';
6
+ import { select, first } from './select.js';
7
+ import progress from './progress.js';
8
+ function chunks(data, size) {
9
+ let arr = [];
10
+ let remainder = data.length % size;
11
+ function slicer(n) {
12
+ for (let i = 0; i < n; i++) {
13
+ let idx = i * size;
14
+ arr.push(data.slice(idx, idx + size));
15
+ }
16
+ }
17
+ if (remainder == 0) {
18
+ slicer(data.length / size);
19
+ }
20
+ else {
21
+ let n = Math.ceil(data.length / size);
22
+ let end = size * (n - 1);
23
+ slicer(n - 1);
24
+ let last = new Uint8Array(size);
25
+ last.set(data.slice(end));
26
+ arr.push(last);
27
+ }
28
+ return arr;
29
+ }
30
+ function updateMap(data, idx) {
31
+ let update = new Update({ data, offset: idx * 512 });
32
+ // NOTE: Have to initialize the Uint8Array across binding this way
33
+ // https://gitlab.altronix.com/software-engineering/sdk/atx-zdk/-/issues/1
34
+ update.data = data;
35
+ return update;
36
+ }
37
+ function updater(linq, serial) {
38
+ return (acc, update) => linq
39
+ .put(serial, '/api/v1/update/transfer', update.cbor())
40
+ .pipe(map(({ meta }) => ({ ...meta, remaining: acc.remaining - 1 })));
41
+ }
42
+ export async function update(file) {
43
+ logger(this.optsWithGlobals()['verbose'] ? 'silly' : 'info');
44
+ const linq = new Linq();
45
+ const dev$ = linq
46
+ .connections()
47
+ .pipe(this.optsWithGlobals()['first'] ? first() : select());
48
+ const obs = from(fs.promises.readFile(file)).pipe(switchMap((bin) => dev$.pipe(map((serial) => ({ serial, bin })))), switchMap(({ serial, bin }) => {
49
+ const update = chunks(bin, 512).map(updateMap);
50
+ const start$ = defer(() => linq.get(serial, '/api/v1/update/start'));
51
+ const finish$ = defer(() => linq.get(serial, '/api/v1/update/finish'));
52
+ const transfer$ = from(update).pipe(mergeScan(updater(linq, serial), { remaining: update.length }, 1));
53
+ return concat(start$, transfer$, finish$).pipe(progress(update.length));
54
+ }), finalize(() => linq.shutdown()));
55
+ return lastValueFrom(obs).catch((e) => {
56
+ if (!(e instanceof EmptyError))
57
+ throw e;
58
+ });
59
+ }
@@ -0,0 +1 @@
1
+ export default function useStdoutDimensions(): [number, number];
@@ -0,0 +1,17 @@
1
+ import { useState, useEffect } from 'react';
2
+ import { useStdout } from 'ink';
3
+ export default function useStdoutDimensions() {
4
+ const { stdout } = useStdout();
5
+ const [dimensions, setDimensions] = useState([
6
+ stdout.columns,
7
+ stdout.rows
8
+ ]);
9
+ useEffect(() => {
10
+ const handler = () => setDimensions([stdout.columns, stdout.rows]);
11
+ stdout.on('resize', handler);
12
+ return () => {
13
+ stdout.off('resize', handler);
14
+ };
15
+ }, [stdout]);
16
+ return dimensions;
17
+ }
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "@altronix/cli",
3
- "version": "0.7.14",
3
+ "version": "0.7.15",
4
4
  "license": "MIT",
5
+ "type": "module",
5
6
  "bin": {
6
7
  "atx": "./dist/index.js"
7
8
  },
8
- "type": "module",
9
9
  "engines": {
10
10
  "node": ">=16"
11
11
  },
@@ -20,7 +20,10 @@
20
20
  "ink": "^4.1.0",
21
21
  "jsonc-parser": "^3.3.1",
22
22
  "react": "^18.2.0",
23
- "rxjs": "^7.8.1"
23
+ "rxjs": "^7.8.1",
24
+ "winston": "^3.13.0",
25
+ "@altronix/device": "0.6.13",
26
+ "@altronix/zdk": "0.7.0"
24
27
  },
25
28
  "devDependencies": {
26
29
  "@sindresorhus/tsconfig": "^3.0.1",