@assemblyline/al-control 0.0.0-stage → 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/CHANGELOG.md +37 -0
- package/LICENSE +201 -0
- package/NOTICE +5 -0
- package/README.md +100 -2
- package/bin/al-control.js +6 -0
- package/package.json +52 -4
- package/src/cli/_helpers.js +14 -0
- package/src/cli/factories/list.js +56 -0
- package/src/cli/hosts.js +59 -0
- package/src/cli/index.js +273 -0
- package/src/cli/lines/list.js +72 -0
- package/src/cli/lines/show.js +76 -0
- package/src/cli/lines/status.js +121 -0
- package/src/cli/procs/actions.js +95 -0
- package/src/cli/procs/list.js +25 -0
- package/src/cli/stations/status.js +79 -0
- package/src/cli/workpieces/_resolve.js +25 -0
- package/src/cli/workpieces/cat.js +20 -0
- package/src/cli/workpieces/clear.js +121 -0
- package/src/cli/workpieces/count.js +73 -0
- package/src/cli/workpieces/errors.js +79 -0
- package/src/cli/workpieces/list.js +140 -0
- package/src/cli/workpieces/open.js +25 -0
- package/src/cli/workpieces/show.js +126 -0
- package/src/cli/workpieces/watch.js +159 -0
- package/src/config.js +213 -0
- package/src/probe.js +591 -0
- package/src/transport.js +184 -0
- package/src/utils/conveyors.js +50 -0
- package/src/utils/factory-data.js +113 -0
- package/src/utils/failures.js +26 -0
- package/src/utils/format.js +160 -0
- package/src/utils/line-state.js +324 -0
- package/src/utils/watch-render.js +40 -0
package/src/cli/index.js
ADDED
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI entry point.
|
|
3
|
+
*
|
|
4
|
+
* Pattern: al-control <resource> <action> [target] [-f <factory>] [--host <host>]
|
|
5
|
+
*
|
|
6
|
+
* One host at a time (`hosts use`, or `--host` per command). Commands about a
|
|
7
|
+
* single factory require `-f`; the current directory is never consulted.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { readFileSync } from 'node:fs';
|
|
11
|
+
import yargs from 'yargs';
|
|
12
|
+
|
|
13
|
+
import { resolveHost, resolveFactory, resolveFactories } from '../config.js';
|
|
14
|
+
import { localOptions } from './_helpers.js';
|
|
15
|
+
|
|
16
|
+
import { listHostsHandler, useHostHandler, currentHostHandler, showConfigHandler } from './hosts.js';
|
|
17
|
+
import { listFactoriesHandler } from './factories/list.js';
|
|
18
|
+
import { listLinesHandler } from './lines/list.js';
|
|
19
|
+
import { showLineHandler } from './lines/show.js';
|
|
20
|
+
import { statusLineHandler } from './lines/status.js';
|
|
21
|
+
import { statusStationHandler } from './stations/status.js';
|
|
22
|
+
import { listWorkpiecesHandler } from './workpieces/list.js';
|
|
23
|
+
import { showWorkpieceHandler } from './workpieces/show.js';
|
|
24
|
+
import { watchHandler } from './workpieces/watch.js';
|
|
25
|
+
import { countWorkpiecesHandler } from './workpieces/count.js';
|
|
26
|
+
import { errorsHandler } from './workpieces/errors.js';
|
|
27
|
+
import { catHandler } from './workpieces/cat.js';
|
|
28
|
+
import { openHandler } from './workpieces/open.js';
|
|
29
|
+
import { clearHandler } from './workpieces/clear.js';
|
|
30
|
+
import { listProcsHandler } from './procs/list.js';
|
|
31
|
+
import {
|
|
32
|
+
startFactoryHandler,
|
|
33
|
+
stopFactoryHandler,
|
|
34
|
+
restartFactoryHandler,
|
|
35
|
+
logsFactoryHandler,
|
|
36
|
+
monitHandler,
|
|
37
|
+
} from './procs/actions.js';
|
|
38
|
+
|
|
39
|
+
const { version } = JSON.parse(readFileSync(new URL('../../package.json', import.meta.url), 'utf8'));
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* On a non-local host, say where the command is about to act — on stderr so
|
|
43
|
+
* `--json` stays parseable. Guards against operating on prod by accident.
|
|
44
|
+
*/
|
|
45
|
+
function banner(host, factory_name) {
|
|
46
|
+
if (!host.is_local) console.error(`→ ${host.name}${factory_name ? ` / ${factory_name}` : ''}`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Wrap a handler that needs a `-f` factory: resolve it, print the banner,
|
|
51
|
+
* optionally frame human output with separator lines.
|
|
52
|
+
*/
|
|
53
|
+
function withFactory(handler, { frame = true } = {}) {
|
|
54
|
+
return async (argv) => {
|
|
55
|
+
if (argv.getYargsCompletions) return;
|
|
56
|
+
const factory = resolveFactory(argv);
|
|
57
|
+
banner(factory.host, factory.name);
|
|
58
|
+
if (!frame || argv.json) return handler(argv, factory);
|
|
59
|
+
console.log('='.repeat(60));
|
|
60
|
+
await handler(argv, factory);
|
|
61
|
+
console.log('='.repeat(60));
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Wrap a host-level handler (no `-f`). */
|
|
66
|
+
function withHost(handler) {
|
|
67
|
+
return async (argv) => {
|
|
68
|
+
if (argv.getYargsCompletions) return;
|
|
69
|
+
const host = resolveHost(argv);
|
|
70
|
+
banner(host);
|
|
71
|
+
return handler(argv, host);
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Wrap a handler whose scope is every factory on the host, or just the `-f`
|
|
77
|
+
* one when given.
|
|
78
|
+
*/
|
|
79
|
+
function withFactories(handler) {
|
|
80
|
+
return async (argv) => {
|
|
81
|
+
if (argv.getYargsCompletions) return;
|
|
82
|
+
const scope = resolveFactories(argv);
|
|
83
|
+
banner(scope.host, argv.factory);
|
|
84
|
+
return handler(argv, scope);
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function optionalFactoryOption(yargs) {
|
|
89
|
+
return localOptions(yargs).option('factory', {
|
|
90
|
+
alias: 'f',
|
|
91
|
+
describe: 'Only this factory (default: every factory on the host)',
|
|
92
|
+
type: 'string',
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function factoryOption(yargs) {
|
|
97
|
+
return localOptions(yargs).option('factory', {
|
|
98
|
+
alias: 'f',
|
|
99
|
+
describe: 'Factory name on the current host (required)',
|
|
100
|
+
type: 'string',
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const jsonOption = { describe: 'Output raw JSON', type: 'boolean' };
|
|
105
|
+
const watchOptions = (yargs) => yargs
|
|
106
|
+
.option('watch', { describe: 'Re-render on an interval. Mutually exclusive with --json.', type: 'boolean' })
|
|
107
|
+
.option('interval', { describe: 'Watch refresh interval in seconds (default 1)', type: 'number' });
|
|
108
|
+
const scopeOptions = (yargs) => yargs
|
|
109
|
+
.option('line', { describe: 'Scope to a single line (e.g. VM)', type: 'string' })
|
|
110
|
+
.option('bin', { describe: 'Scope to a specific bin (STATION/BIN, e.g. VM3/failed)', type: 'string' })
|
|
111
|
+
.option('match', { describe: 'Substring filter on workpiece id', type: 'string' });
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Build and run CLI
|
|
115
|
+
* @param {string[]} args
|
|
116
|
+
*/
|
|
117
|
+
export function run(args) {
|
|
118
|
+
const cli = yargs(args)
|
|
119
|
+
.scriptName('al-control')
|
|
120
|
+
.usage('$0 <resource> <action> [options]')
|
|
121
|
+
.option('host', { describe: 'Target host for this command (overrides current_host)', type: 'string', global: true })
|
|
122
|
+
.command('hosts', 'Configured hosts; pick the one commands target', (yargs) => yargs
|
|
123
|
+
.usage('$0 hosts <action>')
|
|
124
|
+
.command('list', 'List configured hosts', (y) => localOptions(y).option('json', jsonOption), listHostsHandler)
|
|
125
|
+
.command('use <name>', 'Set the current host', (y) => y.positional('name', { describe: 'Host name from the config', type: 'string' }), useHostHandler)
|
|
126
|
+
.command('current', 'Print the current host', {}, currentHostHandler)
|
|
127
|
+
.demandCommand(1, 'Specify an action: list, use, current'))
|
|
128
|
+
.command('factories', 'Factories configured on the current host', (yargs) => yargs
|
|
129
|
+
.usage('$0 factories <action>')
|
|
130
|
+
.command('list', 'Configured factories and whether each is running', (y) => localOptions(y).option('json', jsonOption), withHost(listFactoriesHandler))
|
|
131
|
+
.demandCommand(1, 'Specify an action: list'))
|
|
132
|
+
.command('lines', 'Inspect a factory\'s assembly lines', (yargs) => yargs
|
|
133
|
+
.usage('$0 lines <action> -f <factory> [options]')
|
|
134
|
+
.command('list', 'Lines and their stations (definitional)', (y) => factoryOption(y).option('json', jsonOption), withFactory(listLinesHandler))
|
|
135
|
+
.command(
|
|
136
|
+
'show <line>',
|
|
137
|
+
'Stations in dependency order + conveyor topology (definitional)',
|
|
138
|
+
(y) => factoryOption(y.positional('line', { describe: 'Line code (e.g. VM)', type: 'string' })).option('json', jsonOption),
|
|
139
|
+
withFactory(showLineHandler),
|
|
140
|
+
)
|
|
141
|
+
.command(
|
|
142
|
+
'status [line]',
|
|
143
|
+
'Live bin counts. No line = per-line summary.',
|
|
144
|
+
(y) => watchOptions(factoryOption(y.positional('line', { describe: 'Line code to drill into', type: 'string' })).option('json', jsonOption)),
|
|
145
|
+
withFactory(statusLineHandler),
|
|
146
|
+
)
|
|
147
|
+
.demandCommand(1, 'Specify an action: list, show, status'))
|
|
148
|
+
.command('stations', 'Inspect a factory\'s station bins', (yargs) => yargs
|
|
149
|
+
.usage('$0 stations <action> -f <factory> [options]')
|
|
150
|
+
.command(
|
|
151
|
+
'status <id>',
|
|
152
|
+
'One station\'s bins and the workpieces in them',
|
|
153
|
+
(y) => watchOptions(factoryOption(y.positional('id', { describe: 'Station short_code (e.g. VM3)', type: 'string' })).option('json', jsonOption)),
|
|
154
|
+
withFactory(statusStationHandler),
|
|
155
|
+
)
|
|
156
|
+
.demandCommand(1, 'Specify an action: status'))
|
|
157
|
+
.command('workpieces', 'Inspect a factory\'s workpieces', (yargs) => yargs
|
|
158
|
+
.usage('$0 workpieces <action> -f <factory> [options]')
|
|
159
|
+
.command('list', 'Workpieces on disk: position, last event, folder', (y) => scopeOptions(factoryOption(y)).option('json', jsonOption), withFactory(listWorkpiecesHandler))
|
|
160
|
+
.command(
|
|
161
|
+
'show <id>',
|
|
162
|
+
'One workpiece and its journey (a substring matching several shows the list)',
|
|
163
|
+
(y) => factoryOption(y.positional('id', { describe: 'Workpiece id (exact or substring)', type: 'string' })).option('json', jsonOption),
|
|
164
|
+
withFactory(showWorkpieceHandler),
|
|
165
|
+
)
|
|
166
|
+
.command(
|
|
167
|
+
'watch [id]',
|
|
168
|
+
'Live tail: bin moves and new log events',
|
|
169
|
+
(y) => scopeOptions(factoryOption(y.positional('id', { describe: 'Workpiece id (exact or substring)', type: 'string' })))
|
|
170
|
+
.option('interval', { describe: 'Poll interval in seconds (default 2)', type: 'number' }),
|
|
171
|
+
withFactory(watchHandler, { frame: false }),
|
|
172
|
+
)
|
|
173
|
+
.command(
|
|
174
|
+
'count [bin]',
|
|
175
|
+
'FAILED / DONE / FINISHED counts per factory, line or station (all factories unless -f)',
|
|
176
|
+
(y) => optionalFactoryOption(y.positional('bin', { describe: 'Only this count', choices: ['failed', 'done', 'finished'] }))
|
|
177
|
+
.option('by', { describe: 'Roll up to this level', choices: ['factory', 'line', 'station'], default: 'station' })
|
|
178
|
+
.option('json', jsonOption),
|
|
179
|
+
withFactories(countWorkpiecesHandler),
|
|
180
|
+
)
|
|
181
|
+
.command(
|
|
182
|
+
'errors',
|
|
183
|
+
'Failed workpieces grouped by error, most common first (all factories unless -f)',
|
|
184
|
+
(y) => optionalFactoryOption(y)
|
|
185
|
+
.option('line', { describe: 'Only this line', type: 'string' })
|
|
186
|
+
.option('station', { describe: 'Only this station', type: 'string' })
|
|
187
|
+
.option('json', jsonOption),
|
|
188
|
+
withFactories(errorsHandler),
|
|
189
|
+
)
|
|
190
|
+
.command(
|
|
191
|
+
'cat <id> <file>',
|
|
192
|
+
'Print one file from a workpiece folder (binary-safe when redirected)',
|
|
193
|
+
(y) => factoryOption(y
|
|
194
|
+
.positional('id', { describe: 'Workpiece id (exact or unique substring)', type: 'string' })
|
|
195
|
+
.positional('file', { describe: 'Path inside the workpiece folder (e.g. error.json)', type: 'string' })),
|
|
196
|
+
withFactory(catHandler, { frame: false }),
|
|
197
|
+
)
|
|
198
|
+
.command(
|
|
199
|
+
'open <id>',
|
|
200
|
+
'Open the workpiece folder: Finder locally, a shell in it on a remote host',
|
|
201
|
+
(y) => factoryOption(y.positional('id', { describe: 'Workpiece id (exact or unique substring)', type: 'string' })),
|
|
202
|
+
withFactory(openHandler, { frame: false }),
|
|
203
|
+
)
|
|
204
|
+
.command(
|
|
205
|
+
'clear [id]',
|
|
206
|
+
'Delete workpieces from done bins or terminal outputs (--bin), or one workpiece\'s (id). Dry run unless --yes.',
|
|
207
|
+
(y) => factoryOption(y.positional('id', { describe: 'One workpiece (exact or unique substring): its done and finished copies', type: 'string' }))
|
|
208
|
+
.option('bin', { describe: 'done = every done bin; finished = each line\'s terminal output', choices: ['done', 'finished'] })
|
|
209
|
+
.option('line', { describe: 'Only this line', type: 'string' })
|
|
210
|
+
.option('station', { describe: 'Only this station', type: 'string' })
|
|
211
|
+
.option('older-than', { describe: 'Only workpieces with no activity for this many days', type: 'number' })
|
|
212
|
+
.option('yes', { describe: 'Actually delete (default: dry run)', type: 'boolean' })
|
|
213
|
+
.check((a) => {
|
|
214
|
+
if (!a.id && !a.bin) throw new Error('Give --bin done|finished, or a workpiece id.');
|
|
215
|
+
if (a.olderThan !== undefined && !(a.olderThan >= 0)) throw new Error('--older-than takes a number of days (0 or more).');
|
|
216
|
+
return true;
|
|
217
|
+
}),
|
|
218
|
+
withFactory(clearHandler, { frame: false }),
|
|
219
|
+
)
|
|
220
|
+
.demandCommand(1, 'Specify an action: list, show, watch, count, errors, cat, open, clear'))
|
|
221
|
+
.command('procs', 'Factory processes on the current host (pm2)', (yargs) => yargs
|
|
222
|
+
.usage('$0 procs <action> [-f <factory>]')
|
|
223
|
+
.command('list', 'Every running factory on the host', (y) => localOptions(y).option('json', jsonOption), withHost(listProcsHandler))
|
|
224
|
+
.command('start', 'Start a factory under pm2', factoryOption, withFactory(startFactoryHandler, { frame: false }))
|
|
225
|
+
.command('stop', 'Stop a pm2-managed factory', factoryOption, withFactory(stopFactoryHandler, { frame: false }))
|
|
226
|
+
.command('restart', 'Restart a pm2-managed factory', factoryOption, withFactory(restartFactoryHandler, { frame: false }))
|
|
227
|
+
.command('logs', 'Tail a factory\'s pm2 logs (Ctrl-C to stop)', factoryOption, withFactory(logsFactoryHandler, { frame: false }))
|
|
228
|
+
.command('monit', 'pm2\'s interactive CPU/RAM monitor on the host', {}, withHost(monitHandler))
|
|
229
|
+
.demandCommand(1, 'Specify an action: list, start, stop, restart, logs, monit'))
|
|
230
|
+
.command('config', 'Show the al-control config', (yargs) => yargs
|
|
231
|
+
.usage('$0 config <action>')
|
|
232
|
+
.command('show', 'Config path, current host, hosts and factories', {}, showConfigHandler)
|
|
233
|
+
.demandCommand(1, 'Specify an action: show'))
|
|
234
|
+
.completion('completion', 'Generate shell completion script', (current, argv) => {
|
|
235
|
+
const ACTIONS = {
|
|
236
|
+
hosts: ['list', 'use', 'current'],
|
|
237
|
+
factories: ['list'],
|
|
238
|
+
lines: ['list', 'show', 'status'],
|
|
239
|
+
stations: ['status'],
|
|
240
|
+
workpieces: ['list', 'show', 'watch', 'count', 'errors', 'cat', 'open', 'clear'],
|
|
241
|
+
procs: ['list', 'start', 'stop', 'restart', 'logs', 'monit'],
|
|
242
|
+
config: ['show'],
|
|
243
|
+
};
|
|
244
|
+
// Drop the partial word being completed so `args` holds completed words only.
|
|
245
|
+
const words = argv._.slice(1).filter((a) => a !== '');
|
|
246
|
+
const done = current && words[words.length - 1] === current ? words.slice(0, -1) : words;
|
|
247
|
+
const startsWithCurrent = (list) => (current ? list.filter((c) => c.startsWith(current)) : list);
|
|
248
|
+
if (done.length === 0) return startsWithCurrent(Object.keys(ACTIONS));
|
|
249
|
+
if (done.length === 1) return startsWithCurrent(ACTIONS[done[0]] ?? []);
|
|
250
|
+
return [];
|
|
251
|
+
})
|
|
252
|
+
.demandCommand(1, 'Specify a resource: hosts, factories, lines, stations, workpieces, procs, config')
|
|
253
|
+
.strictCommands()
|
|
254
|
+
.help()
|
|
255
|
+
.version(version)
|
|
256
|
+
.alias('h', 'help')
|
|
257
|
+
.alias('v', 'version')
|
|
258
|
+
.group(['host', 'help', 'version'], 'Global Options:')
|
|
259
|
+
.wrap(process.stdout.columns || 100)
|
|
260
|
+
.fail((msg, err, yargs) => {
|
|
261
|
+
if (err) {
|
|
262
|
+
console.error(`Error: ${err.message}`);
|
|
263
|
+
if (process.env.DEBUG) console.error(err.stack);
|
|
264
|
+
} else {
|
|
265
|
+
console.error(msg);
|
|
266
|
+
console.error('');
|
|
267
|
+
yargs.showHelp();
|
|
268
|
+
}
|
|
269
|
+
process.exit(1);
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
cli.parse();
|
|
273
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { topoSortStations, codeOf } from '../../utils/line-state.js';
|
|
2
|
+
import { loadDefs } from '../../utils/factory-data.js';
|
|
3
|
+
import { formatTable } from '../../utils/format.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* `al-control lines list -f <factory>` — lines derived from the factory's
|
|
7
|
+
* station JSON files: name, location (from `.orchestrator/lines/<CODE>.json`),
|
|
8
|
+
* station count and members. Definitional; `lines status` has live counts.
|
|
9
|
+
*
|
|
10
|
+
* @param {{json?: boolean}} argv
|
|
11
|
+
* @param {import('../../config.js').FactoryContext} factory
|
|
12
|
+
*/
|
|
13
|
+
export async function listLinesHandler(argv, factory) {
|
|
14
|
+
const { json } = argv;
|
|
15
|
+
const { stations, lines: line_defs } = loadDefs(factory);
|
|
16
|
+
|
|
17
|
+
if (stations.length === 0) {
|
|
18
|
+
if (json) {
|
|
19
|
+
console.log(JSON.stringify({}, null, 2));
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
console.log(`No station files in ${factory.name}/.orchestrator/stations/.`);
|
|
23
|
+
console.log('Run "al-orc stations pull --all" in the factory repo to fetch them.');
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Stations without a line field group under '(unassigned)'.
|
|
28
|
+
const groups = new Map();
|
|
29
|
+
for (const s of stations) {
|
|
30
|
+
const key = s.line ?? '(unassigned)';
|
|
31
|
+
if (!groups.has(key)) groups.set(key, []);
|
|
32
|
+
groups.get(key).push(s);
|
|
33
|
+
}
|
|
34
|
+
// A line file with no stations yet still exists as a definition.
|
|
35
|
+
for (const code of Object.keys(line_defs)) {
|
|
36
|
+
if (!groups.has(code)) groups.set(code, []);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (json) {
|
|
40
|
+
const out = {};
|
|
41
|
+
for (const [line, members] of groups) {
|
|
42
|
+
const def = line_defs[line] || null;
|
|
43
|
+
out[line] = {
|
|
44
|
+
name: def?.name ?? null,
|
|
45
|
+
location: def?.location ?? null,
|
|
46
|
+
stations: topoSortStations(members).map((m) => ({ short_code: m.data.short_code, id: m.data.id, name: m.data.name })),
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
console.log(JSON.stringify(out, null, 2));
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const rows = [...groups.keys()].sort().map((line) => {
|
|
54
|
+
const members = groups.get(line);
|
|
55
|
+
const def = line_defs[line];
|
|
56
|
+
return [
|
|
57
|
+
line,
|
|
58
|
+
def?.name && def.name !== line ? def.name : '—',
|
|
59
|
+
def?.location ?? '(no line file)',
|
|
60
|
+
String(members.length),
|
|
61
|
+
topoSortStations(members).map(codeOf).join(', ') || '—',
|
|
62
|
+
];
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
console.log(formatTable(['LINE', 'NAME', 'LOCATION', 'STATIONS', 'MEMBERS'], rows));
|
|
66
|
+
console.log('');
|
|
67
|
+
console.log(`Total: ${groups.size} lines, ${stations.length} stations`);
|
|
68
|
+
if (Object.keys(line_defs).length === 0) {
|
|
69
|
+
console.log('No line files in .orchestrator/lines/ — run "al-orc lines pull --all" in the factory repo.');
|
|
70
|
+
}
|
|
71
|
+
console.log(`Run \`al-control lines status ${factory.flags}\` for live bin counts.`);
|
|
72
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import {
|
|
2
|
+
topoSortStations,
|
|
3
|
+
codeOf,
|
|
4
|
+
dependencyRef,
|
|
5
|
+
resolveStationRef,
|
|
6
|
+
indexStationsByRef,
|
|
7
|
+
} from '../../utils/line-state.js';
|
|
8
|
+
import { loadDefs } from '../../utils/factory-data.js';
|
|
9
|
+
import { formatHeader, formatField, formatTable, formatSection } from '../../utils/format.js';
|
|
10
|
+
import { conveyorMoves } from '../../utils/conveyors.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* `al-control lines show <line> -f <factory>` — definitional view of a line:
|
|
14
|
+
* stations in dependency order plus conveyor topology.
|
|
15
|
+
*
|
|
16
|
+
* @param {{line: string, json?: boolean}} argv
|
|
17
|
+
* @param {import('../../config.js').FactoryContext} factory
|
|
18
|
+
*/
|
|
19
|
+
export async function showLineHandler(argv, factory) {
|
|
20
|
+
const { line: line_arg, json } = argv;
|
|
21
|
+
const { stations, lines: line_defs } = loadDefs(factory);
|
|
22
|
+
const line_def = line_defs[line_arg] || null;
|
|
23
|
+
|
|
24
|
+
const members = stations.filter((s) => s.line === line_arg);
|
|
25
|
+
if (members.length === 0 && !line_def) {
|
|
26
|
+
console.error(`Line not found: ${line_arg}`);
|
|
27
|
+
console.error(`Run "al-control lines list ${factory.flags}" to see available lines.`);
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const ordered = topoSortStations(members);
|
|
32
|
+
|
|
33
|
+
if (json) {
|
|
34
|
+
console.log(JSON.stringify({ line: line_def, stations: ordered.map((m) => m.data) }, null, 2));
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
console.log(formatHeader('Line', line_arg));
|
|
39
|
+
if (line_def?.name && line_def.name !== line_arg) console.log(formatField('Name', line_def.name, 12));
|
|
40
|
+
console.log(formatField('Location', line_def?.location ?? '(no line file — run "al-orc lines pull --all")', 12));
|
|
41
|
+
if (line_def?.description) console.log(formatField('Description', line_def.description, 12));
|
|
42
|
+
console.log(formatField('Stations', String(members.length), 12));
|
|
43
|
+
console.log('');
|
|
44
|
+
|
|
45
|
+
// Resolve dependency refs against every station, not just this line's — a
|
|
46
|
+
// cross-line edge should still render as a short_code.
|
|
47
|
+
const by_ref = indexStationsByRef(stations);
|
|
48
|
+
const rows = ordered.map((m) => {
|
|
49
|
+
const deps = (m.data.dependencies || [])
|
|
50
|
+
.map((d) => resolveStationRef(dependencyRef(d), by_ref))
|
|
51
|
+
.join(', ') || '—';
|
|
52
|
+
return [
|
|
53
|
+
codeOf(m),
|
|
54
|
+
m.data.name || '—',
|
|
55
|
+
String(Array.isArray(m.data.steps) ? m.data.steps.length : 0),
|
|
56
|
+
deps,
|
|
57
|
+
m.data.is_enabled === false ? 'no' : 'yes',
|
|
58
|
+
];
|
|
59
|
+
});
|
|
60
|
+
console.log(formatTable(['STATION', 'NAME', 'STEPS', 'DEPENDS ON', 'ENABLED'], rows));
|
|
61
|
+
|
|
62
|
+
// move_files steps describe the source → target bin flow.
|
|
63
|
+
const conveyors = [];
|
|
64
|
+
for (const m of ordered) {
|
|
65
|
+
for (const move of conveyorMoves(m.data)) {
|
|
66
|
+
conveyors.push([codeOf(m), move.source_bin, move.target_bin || '—', move.mode || '—']);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
if (conveyors.length > 0) {
|
|
70
|
+
console.log(formatSection('Conveyors'));
|
|
71
|
+
console.log(formatTable(['AT STATION', 'FROM BIN', 'TO BIN', 'MODE'], conveyors));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
console.log('');
|
|
75
|
+
console.log(`Run \`al-control lines status ${line_arg} ${factory.flags}\` for live bin counts.`);
|
|
76
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { formatTable, formatHeader, formatField } from '../../utils/format.js';
|
|
2
|
+
import { summarizeLine, LIVE_BINS } from '../../utils/line-state.js';
|
|
3
|
+
import { loadLines } from '../../utils/factory-data.js';
|
|
4
|
+
import { watchRender, DEFAULT_WATCH_INTERVAL_SECS } from '../../utils/watch-render.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* `al-control lines status [line] -f <factory>` — live bin state.
|
|
8
|
+
*
|
|
9
|
+
* No line: per-line IN-FLIGHT / STUCK / FINISHED / HEALTH summary. With a
|
|
10
|
+
* line: station × live-bin table; `done` shown in parens and excluded from
|
|
11
|
+
* totals (a receipt of a forward move, not a position).
|
|
12
|
+
*
|
|
13
|
+
* `--watch [--interval=N]` re-renders in place; exclusive with `--json`.
|
|
14
|
+
*
|
|
15
|
+
* @param {{line?: string, json?: boolean, watch?: boolean, interval?: number}} argv
|
|
16
|
+
* @param {import('../../config.js').FactoryContext} factory
|
|
17
|
+
*/
|
|
18
|
+
export async function statusLineHandler(argv, factory) {
|
|
19
|
+
const { line: line_arg, json, watch, interval } = argv;
|
|
20
|
+
if (watch && json) {
|
|
21
|
+
console.error('--watch and --json are mutually exclusive (watch is a TTY redraw; json is a one-shot snapshot).');
|
|
22
|
+
process.exit(1);
|
|
23
|
+
}
|
|
24
|
+
const interval_secs = typeof interval === 'number' ? interval : DEFAULT_WATCH_INTERVAL_SECS;
|
|
25
|
+
|
|
26
|
+
const renderOnce = () => {
|
|
27
|
+
const { lines } = loadLines(factory);
|
|
28
|
+
|
|
29
|
+
if (Object.keys(lines).length === 0) {
|
|
30
|
+
if (json) console.log(JSON.stringify({}, null, 2));
|
|
31
|
+
else console.log(`No lines in ${factory.name}/.orchestrator/stations/. Run "al-orc stations pull --all" in the factory repo.`);
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (!line_arg) {
|
|
36
|
+
renderLineSummary(lines, json);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const ls = lines[line_arg];
|
|
41
|
+
if (!ls) {
|
|
42
|
+
const msg = `Line "${line_arg}" not found. Available: ${Object.keys(lines).sort().join(', ')}`;
|
|
43
|
+
// In watch mode the line may appear later, so surface and retry.
|
|
44
|
+
if (watch) {
|
|
45
|
+
console.log(msg);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
console.error(msg);
|
|
49
|
+
process.exit(1);
|
|
50
|
+
}
|
|
51
|
+
renderLineDrilldown(ls, json, factory);
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
if (watch) {
|
|
55
|
+
await watchRender({ render: renderOnce, interval_secs });
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
renderOnce();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function renderLineSummary(lines, json) {
|
|
62
|
+
const summaries = {};
|
|
63
|
+
for (const ls of Object.values(lines)) summaries[ls.code] = summarizeLine(ls);
|
|
64
|
+
|
|
65
|
+
if (json) {
|
|
66
|
+
console.log(JSON.stringify(summaries, null, 2));
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const rows = Object.keys(summaries).sort().map((code) => {
|
|
71
|
+
const s = summaries[code];
|
|
72
|
+
let health;
|
|
73
|
+
if (s.stuck > 0) health = `⚠ ${s.stuck_locations.join(', ')}`;
|
|
74
|
+
else if (s.in_flight === 0 && s.finished === 0) health = 'idle';
|
|
75
|
+
else if (s.in_flight === 0) health = `${s.finished} finished, drained`;
|
|
76
|
+
else health = `flowing, biggest at ${s.biggest_flow.at}`;
|
|
77
|
+
return [code, String(s.in_flight), String(s.stuck), String(s.finished), health];
|
|
78
|
+
});
|
|
79
|
+
console.log(formatTable(['LINE', 'IN-FLIGHT', 'STUCK', 'FINISHED', 'HEALTH'], rows));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function renderLineDrilldown(ls, json, factory) {
|
|
83
|
+
if (json) {
|
|
84
|
+
const out = { code: ls.code, terminal: ls.terminal, stations: {} };
|
|
85
|
+
for (const station of ls.stations) {
|
|
86
|
+
out.stations[station] = {};
|
|
87
|
+
for (const bin of [...LIVE_BINS, 'done']) {
|
|
88
|
+
const map = ls.bins[station][bin];
|
|
89
|
+
out.stations[station][bin] = map ? [...map.keys()].sort() : null;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
out.summary = summarizeLine(ls);
|
|
93
|
+
console.log(JSON.stringify(out, null, 2));
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
console.log(formatHeader('Line', ls.code));
|
|
98
|
+
console.log(formatField('Stations', ls.stations.join(' → '), 12));
|
|
99
|
+
console.log(formatField('Terminal', ls.terminal, 12));
|
|
100
|
+
console.log('');
|
|
101
|
+
|
|
102
|
+
const cell = (map) => (map === null ? '—' : String(map.size));
|
|
103
|
+
const rows = ls.stations.map((station) => {
|
|
104
|
+
const b = ls.bins[station];
|
|
105
|
+
return [station, cell(b.input), cell(b.doing), cell(b.output), cell(b.failed), b.done === null ? '—' : `(${b.done.size})`];
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
const totals = { input: 0, doing: 0, output: 0, failed: 0 };
|
|
109
|
+
for (const station of ls.stations) {
|
|
110
|
+
for (const bin of LIVE_BINS) {
|
|
111
|
+
const map = ls.bins[station][bin];
|
|
112
|
+
if (map) totals[bin] += map.size;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
rows.push(['live', String(totals.input), String(totals.doing), String(totals.output), String(totals.failed), '']);
|
|
116
|
+
|
|
117
|
+
console.log(formatTable(['STATION', 'INPUT', 'DOING', 'OUTPUT', 'FAILED', '(DONE)'], rows));
|
|
118
|
+
console.log('');
|
|
119
|
+
console.log('`(done)` shown in parens for audit; excluded from live totals.');
|
|
120
|
+
console.log(`Run \`al-control workpieces list --line ${ls.code} ${factory.flags}\` for per-workpiece positions.`);
|
|
121
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
|
|
3
|
+
import { probe, pm2, pm2Hint } from '../../transport.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The factory's resolved root on its host, its package.json `main`, and the
|
|
7
|
+
* running process for it (if any) — one probe round trip.
|
|
8
|
+
*
|
|
9
|
+
* pm2 process name = directory basename of the factory root, the same name
|
|
10
|
+
* `al-factory procs start` and the control-room app use, so a factory started by
|
|
11
|
+
* one is recognised by the others.
|
|
12
|
+
*
|
|
13
|
+
* @param {import('../../config.js').FactoryContext} factory
|
|
14
|
+
*/
|
|
15
|
+
function inspectFactory(factory) {
|
|
16
|
+
const { factories: [w], procs, pm2_found } = probe(factory.host, { type: 'factories', paths: { [factory.name]: factory.path } });
|
|
17
|
+
if (!w.exists) throw new Error(`Factory directory does not exist on ${factory.host.name}: ${w.root}`);
|
|
18
|
+
if (!pm2_found) throw new Error(`pm2 not found on ${factory.host.name}. ${pm2Hint(factory.host)}`);
|
|
19
|
+
const pm2_name = path.posix.basename(w.root);
|
|
20
|
+
const running = procs.find((r) => r.cwd === w.root || r.pm2 === pm2_name) ?? null;
|
|
21
|
+
return { root: w.root, main: w.main, pm2_name, running };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* The factory's pm2-managed process, or exit with why it can't be managed.
|
|
26
|
+
* @param {import('../../config.js').FactoryContext} factory
|
|
27
|
+
*/
|
|
28
|
+
function requirePm2Process(factory) {
|
|
29
|
+
const { running } = inspectFactory(factory);
|
|
30
|
+
if (!running) {
|
|
31
|
+
console.error(`Factory "${factory.name}" is not running on ${factory.host.name}.`);
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
34
|
+
if (running.mode !== 'pm2') {
|
|
35
|
+
console.error(`Factory "${factory.name}" (pid ${running.pid}) is running directly, not under pm2 — can't manage it this way.`);
|
|
36
|
+
console.error(`Stop it on ${factory.host.name} manually: kill ${running.pid}`);
|
|
37
|
+
process.exit(1);
|
|
38
|
+
}
|
|
39
|
+
return running;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* `al-control procs start -f <factory>` — start under pm2 from the factory root.
|
|
44
|
+
* @param {object} argv
|
|
45
|
+
* @param {import('../../config.js').FactoryContext} factory
|
|
46
|
+
*/
|
|
47
|
+
export async function startFactoryHandler(argv, factory) {
|
|
48
|
+
const { root, main, pm2_name, running } = inspectFactory(factory);
|
|
49
|
+
if (!main) {
|
|
50
|
+
console.error(`${root} on ${factory.host.name} doesn't look like a factory repo (missing @assemblyline/lib-factory dependency or package.json "main").`);
|
|
51
|
+
process.exit(1);
|
|
52
|
+
}
|
|
53
|
+
if (running) {
|
|
54
|
+
console.error(`Factory "${factory.name}" is already running on ${factory.host.name} (pid ${running.pid}, mode ${running.mode}).`);
|
|
55
|
+
console.error(`Use "al-control procs restart ${factory.flags}" instead.`);
|
|
56
|
+
process.exit(1);
|
|
57
|
+
}
|
|
58
|
+
pm2(factory.host, ['start', main, '--name', pm2_name], { cwd: root });
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* `al-control procs stop -f <factory>`
|
|
63
|
+
* @param {object} argv
|
|
64
|
+
* @param {import('../../config.js').FactoryContext} factory
|
|
65
|
+
*/
|
|
66
|
+
export async function stopFactoryHandler(argv, factory) {
|
|
67
|
+
pm2(factory.host, ['stop', requirePm2Process(factory).pm2]);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* `al-control procs restart -f <factory>`
|
|
72
|
+
* @param {object} argv
|
|
73
|
+
* @param {import('../../config.js').FactoryContext} factory
|
|
74
|
+
*/
|
|
75
|
+
export async function restartFactoryHandler(argv, factory) {
|
|
76
|
+
pm2(factory.host, ['restart', requirePm2Process(factory).pm2]);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* `al-control procs logs -f <factory>` — tail until Ctrl-C.
|
|
81
|
+
* @param {object} argv
|
|
82
|
+
* @param {import('../../config.js').FactoryContext} factory
|
|
83
|
+
*/
|
|
84
|
+
export async function logsFactoryHandler(argv, factory) {
|
|
85
|
+
pm2(factory.host, ['logs', requirePm2Process(factory).pm2], { interactive: true });
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* `al-control procs monit` — pm2's interactive CPU/RAM monitor on the host.
|
|
90
|
+
* @param {object} argv
|
|
91
|
+
* @param {import('../../config.js').HostContext} host
|
|
92
|
+
*/
|
|
93
|
+
export async function monitHandler(argv, host) {
|
|
94
|
+
pm2(host, ['monit'], { interactive: true });
|
|
95
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { probeProcesses } from '../../transport.js';
|
|
2
|
+
import { formatTable } from '../../utils/format.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* `al-control procs list` — every factory process on the host, configured
|
|
6
|
+
* or not, pm2-managed or run directly.
|
|
7
|
+
* @param {{json?: boolean}} argv
|
|
8
|
+
* @param {import('../../config.js').HostContext} host
|
|
9
|
+
*/
|
|
10
|
+
export async function listProcsHandler(argv, host) {
|
|
11
|
+
const { procs } = probeProcesses(host, { type: 'procs' });
|
|
12
|
+
|
|
13
|
+
if (argv.json) {
|
|
14
|
+
console.log(JSON.stringify(procs, null, 2));
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
if (procs.length === 0) {
|
|
18
|
+
console.log(`No running factories on ${host.name}.`);
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
console.log(formatTable(
|
|
22
|
+
['PID', 'FACTORY', 'MODE', 'PM2', 'PORT', 'UPTIME', 'STARTED'],
|
|
23
|
+
procs.map((r) => [r.pid, r.factory, r.mode, r.pm2, r.port, r.uptime, r.started]),
|
|
24
|
+
));
|
|
25
|
+
}
|