@altronix/cli 0.7.13 → 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.js CHANGED
@@ -379,13 +379,11 @@ function extraBootConfs(board, extraConfs) {
379
379
  function copy() {
380
380
  return (obs$) => obs$.pipe(mergeMap((opts) => {
381
381
  const { src, dst } = opts;
382
- return new Observable((subscriber) => {
383
- fs.promises
384
- .copyFile(src, dst)
385
- .then(() => subscriber.next(opts))
386
- .catch(() => subscriber.next({ ...opts, err: dst }))
387
- .finally(() => subscriber.complete());
388
- });
382
+ const promise = fs.promises
383
+ .copyFile(src, dst)
384
+ .then(() => opts)
385
+ .catch(() => ({ ...opts, err: dst }));
386
+ return from(promise);
389
387
  }));
390
388
  }
391
389
  function concatFiles(src, dest) {
@@ -18,10 +18,10 @@ export interface BuildProgress<K extends string = string> {
18
18
  export interface Options {
19
19
  items: BuildItem[];
20
20
  progress$: Observable<BuildProgress>;
21
- onComplete: UseCompleteEffectCallback;
21
+ onComplete: BuildEffectCallback;
22
22
  }
23
23
  export default function ({ items, progress$, onComplete }: Options): React.JSX.Element;
24
- interface UseCompleteEffectCallback<E extends Error = Error> {
24
+ interface BuildEffectCallback<E extends Error = Error> {
25
25
  (e?: E): void;
26
26
  }
27
27
  export {};
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);
@@ -21,8 +22,7 @@ export class BuildError extends Error {
21
22
  export default function ({ items, progress$, onComplete }) {
22
23
  const [col, _rows] = useStdoutDimensions();
23
24
  const [width, setWidth] = useState(0);
24
- const progress = useBuildEffect(progress$, items.map(({ item: i }) => i));
25
- useCompleteEffect(progress$, onComplete);
25
+ const progress = useBuildEffect(progress$, items.map(({ item: i }) => i), onComplete);
26
26
  useLayoutEffect(() => {
27
27
  const width = items.map(({ item }) => item).reduce(calculateItemWidth, 0);
28
28
  setWidth(width);
@@ -81,34 +81,15 @@ function progressReducer(acc, next) {
81
81
  : progressInc(acc[next.item]);
82
82
  return acc;
83
83
  }
84
- function useCompleteEffect(obs$, cb) {
85
- useLayoutEffect(() => {
86
- const s = obs$.subscribe({ complete: cb, error: cb });
87
- return s.unsubscribe();
88
- });
89
- }
90
- function useBuildEffect(obs$, items) {
84
+ function useBuildEffect(obs$, items, cb) {
91
85
  const [progress, setProgress] = useState(initProgress(items));
92
86
  useLayoutEffect(() => {
93
87
  const s = obs$.pipe(scan(progressReducer, progress)).subscribe({
94
- next: (progress) => setProgress({ ...progress })
88
+ next: (progress) => setProgress({ ...progress }),
89
+ complete: cb,
90
+ error: cb
95
91
  });
96
92
  return () => s.unsubscribe();
97
93
  }, [obs$]);
98
94
  return progress;
99
95
  }
100
- function useStdoutDimensions() {
101
- const { stdout } = useStdout();
102
- const [dimensions, setDimensions] = useState([
103
- stdout.columns,
104
- stdout.rows
105
- ]);
106
- useEffect(() => {
107
- const handler = () => setDimensions([stdout.columns, stdout.rows]);
108
- stdout.on('resize', handler);
109
- return () => {
110
- stdout.off('resize', handler);
111
- };
112
- }, [stdout]);
113
- return dimensions;
114
- }
@@ -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,49 +3,119 @@ 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
- (async function main() {
11
- // Parse package.json to get version
12
- const pkg = path.resolve(__dirname, '..', 'package.json');
13
- const ver = JSON.parse(await fs.promises.readFile(pkg, 'ascii')).version;
14
- // const cwd = path.resolve('./');
16
+ // Parse package.json to get version
17
+ const pkg = path.resolve(__dirname, '..', 'package.json');
18
+ const ver = JSON.parse(fs.readFileSync(pkg, 'ascii')).version;
19
+ // const cwd = path.resolve('./');
20
+ program
21
+ .name('atx')
22
+ .description('build atx zdk projects')
23
+ .version(ver)
24
+ .option('-Q, --quiet', 'no logs')
25
+ .option('-VV, --verbose', 'extra logging');
26
+ // Scan command
27
+ /*
28
+ const ignore = 'node_modules;target;build;.git';
15
29
  program
16
- .name('atx')
17
- .description('build atx zdk projects')
18
- .version(ver)
19
- .option('-VV, --verbose', 'extra logging');
20
- // Scan command
21
- /*
22
- const ignore = 'node_modules;target;build;.git';
23
- program
24
- .command('scan')
25
- .description('scan for *.cddl files')
26
- .option('-p, --path <PATH>', 'root directory to start scan', cwd)
27
- .option('-m, --matches <REGEX>', 'match expression', '.*cddl$')
28
- .option('-i, --ignores <REGEX>', 'ignore directories', ignore)
29
- .action(seedle.scan);
30
- */
31
- // Build command
32
- // TODO - detect if west is available and fail early
33
- program
34
- .command('build')
35
- .description('build atx zdk application')
36
- .option('-c, --config <CONFIG>', 'workspace config file')
37
- .option('-C, --concurrent <NUMBER>', 'how many builds to run concurrently')
38
- .option('-A, --application', 'build applications')
39
- .option('-B, --bootloader', 'build bootloaders')
40
- .option('-W, --wasm', 'build wasm')
41
- .option('-y, --yes', 'answer yes automatically')
42
- .action(build);
43
- // Load plugins
44
- // (await plugins()).forEach(({ plugin: _, path: __, description: ___ }) => {});
45
- try {
46
- await program.parseAsync();
47
- }
48
- catch (e) {
49
- console.error(e);
50
- }
51
- })();
30
+ .command('scan')
31
+ .description('scan for *.cddl files')
32
+ .option('-p, --path <PATH>', 'root directory to start scan', cwd)
33
+ .option('-m, --matches <REGEX>', 'match expression', '.*cddl$')
34
+ .option('-i, --ignores <REGEX>', 'ignore directories', ignore)
35
+ .action(seedle.scan);
36
+ */
37
+ // Build command
38
+ // TODO - detect if west is available and fail early
39
+ program
40
+ .command('build')
41
+ .description('build atx zdk application')
42
+ .option('-c, --config <CONFIG>', 'workspace config file')
43
+ .option('-C, --concurrent <NUMBER>', 'how many builds to run concurrently')
44
+ .option('-A, --application', 'build applications')
45
+ .option('-B, --bootloader', 'build bootloaders')
46
+ .option('-W, --wasm', 'build wasm')
47
+ .option('-y, --yes', 'answer yes automatically')
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);
119
+ // Load plugins
120
+ // (await plugins()).forEach(({ plugin: _, path: __, description: ___ }) => {});
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.13",
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",