@fnndsc/chell 4.2.8 → 4.3.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/dist/builtins/fs/pull.d.ts +2 -2
- package/dist/builtins/fs/pull.js +10 -6
- package/dist/builtins/fs/pull.js.map +1 -1
- package/dist/builtins/help.js +12 -12
- package/dist/builtins/net/query.js +8 -1
- package/dist/builtins/net/query.js.map +1 -1
- package/dist/chell.d.ts +6 -12
- package/dist/chell.js +5 -1187
- package/dist/chell.js.map +1 -1
- package/dist/core/boot.d.ts +7 -0
- package/dist/core/boot.js +597 -0
- package/dist/core/boot.js.map +1 -0
- package/dist/core/dispatch.d.ts +48 -0
- package/dist/core/dispatch.js +544 -0
- package/dist/core/dispatch.js.map +1 -0
- package/dist/core/preprocess.d.ts +46 -0
- package/dist/core/preprocess.js +146 -0
- package/dist/core/preprocess.js.map +1 -0
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/package.json +10 -6
package/dist/chell.js
CHANGED
|
@@ -1,1193 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @file ChELL
|
|
2
|
+
* @file ChELL public module surface.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* - Dispatch built-in commands, simulated plugin exec, and fallback delegation to `chili`.
|
|
8
|
-
* - Provide REPL loop with timing, debug toggles, and logical/physical FS modes.
|
|
9
|
-
*
|
|
10
|
-
* The code is organized into:
|
|
11
|
-
* - Session/bootstrap helpers (init, prefetch, intro rendering)
|
|
12
|
-
* - Command preprocessing helpers (semicolon batching, pipes, redirects, wildcards, help)
|
|
13
|
-
* - Command dispatch table for built-ins and plugin/bin execution
|
|
14
|
-
* - REPL wiring and prompt startup
|
|
4
|
+
* Historically the shell's god file; the implementation now lives in the
|
|
5
|
+
* `core/` modules. This barrel re-exports the command layer so existing
|
|
6
|
+
* importers (`./builtins/res/*`, `./core/boot.js`) keep a stable path.
|
|
15
7
|
*
|
|
16
8
|
* @module
|
|
17
9
|
*/
|
|
18
|
-
|
|
19
|
-
import * as os from 'os';
|
|
20
|
-
import * as path from 'path';
|
|
21
|
-
import { readFileSync, writeFileSync, appendFileSync, statSync, existsSync } from 'fs';
|
|
22
|
-
import { fileURLToPath } from 'url';
|
|
23
|
-
import { createRequire } from 'module';
|
|
24
|
-
import { Writable } from 'stream';
|
|
25
|
-
import chalk from 'chalk';
|
|
26
|
-
import { spawn } from 'child_process';
|
|
27
|
-
import { REPL } from './core/repl.js';
|
|
28
|
-
import { session } from './session/index.js';
|
|
29
|
-
import { builtin_cd, builtin_ls, builtin_pwd, builtin_connect, builtin_logout, builtin_cat, builtin_cp, builtin_mv, builtin_upload, builtin_pacs, builtin_pipeline, builtin_pull, builtin_query, builtin_cubepath, builtin_rm, builtin_touch, builtin_mkdir, builtin_plugin, builtin_feed, builtin_compute, builtin_tag, builtin_group, builtin_pluginmeta, builtin_plugininstance, builtin_workflow, builtin_download, builtin_edit, builtin_files, builtin_links, builtin_dirs, builtin_context, builtin_parametersofplugin, builtin_physicalmode, builtin_prompt, builtin_timing, builtin_whoami, builtin_whereami, builtin_debug, builtin_help, builtin_tree, builtin_du, builtin_store, error_stripDebugPrefix } from './builtins/index.js';
|
|
30
|
-
import { builtin_executePlugin } from './builtins/pluginExecute.js';
|
|
31
|
-
import { builtin_proc } from './builtins/proc.js';
|
|
32
|
-
import { wildcards_expandAll } from './builtins/wildcard.js';
|
|
33
|
-
import { help_show, args_checkHasHelpFlag } from './builtins/help.js';
|
|
34
|
-
import { pluginExecutable_handle } from './builtins/executable.js';
|
|
35
|
-
import { errorStack, Ok, Err } from '@fnndsc/cumin';
|
|
36
|
-
import { vfs } from './lib/vfs/vfs.js';
|
|
37
|
-
import { spinner } from './lib/spinner.js';
|
|
38
|
-
import { args_tokenize } from './lib/parser.js';
|
|
39
|
-
import { semicolons_parse } from './lib/semicolonParser.js';
|
|
40
|
-
import { logo_print, logo_animatePulse, logo_animateStop } from './lib/logo.js';
|
|
41
|
-
import { bootLogger_create, bootsequence_printIntroPanelsStacked } from './lib/bootsequence.js';
|
|
42
|
-
import { settings_load } from './config/settings.js';
|
|
43
|
-
import { cli_parse } from './core/cli.js';
|
|
44
|
-
import { prefetch_path, prefetch_withSpinner } from './lib/prefetch.js';
|
|
45
|
-
import { segment_pipeThrough } from './lib/pipe.js';
|
|
46
|
-
import { bootFlags_compute } from './core/bootFlags.js';
|
|
47
|
-
import { context_getSingle, procCache_refresh, procTopology_warmup } from '@fnndsc/salsa';
|
|
48
|
-
import { chrisContext, Context } from '@fnndsc/cumin';
|
|
49
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
50
|
-
const __dirname = path.dirname(__filename);
|
|
51
|
-
/**
|
|
52
|
-
* Loads a dependency's package.json via node module resolution, so it works
|
|
53
|
-
* whether the dep is nested or hoisted to a workspace-root node_modules.
|
|
54
|
-
*
|
|
55
|
-
* @param name - The package name (e.g. `@fnndsc/cumin`).
|
|
56
|
-
* @returns The parsed package.json, or a fallback `{ name, version: 'unknown' }`.
|
|
57
|
-
*/
|
|
58
|
-
function depPackageJson_load(name) {
|
|
59
|
-
try {
|
|
60
|
-
const req = createRequire(import.meta.url);
|
|
61
|
-
return req(`${name}/package.json`);
|
|
62
|
-
}
|
|
63
|
-
catch {
|
|
64
|
-
return { name, version: 'unknown' };
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
const packageJson = JSON.parse(readFileSync(path.resolve(__dirname, '../package.json'), 'utf-8'));
|
|
68
|
-
const cuminJson = depPackageJson_load('@fnndsc/cumin');
|
|
69
|
-
const salsaJson = depPackageJson_load('@fnndsc/salsa');
|
|
70
|
-
const chiliJson = depPackageJson_load('@fnndsc/chili');
|
|
71
|
-
/**
|
|
72
|
-
* Spawns the `chili` CLI as a child process.
|
|
73
|
-
*
|
|
74
|
-
* @param command - The command to run (e.g., "feeds").
|
|
75
|
-
* @param args - The arguments to pass to the command.
|
|
76
|
-
* @returns A Promise resolving when the child process exits.
|
|
77
|
-
*/
|
|
78
|
-
export async function chiliCommand_run(command, args) {
|
|
79
|
-
// Resolve chili's entry via node module resolution (works when installed from
|
|
80
|
-
// npm); fall back to the sibling dev-tree path for local monorepo runs.
|
|
81
|
-
let chiliPath;
|
|
82
|
-
try {
|
|
83
|
-
chiliPath = createRequire(import.meta.url).resolve('@fnndsc/chili');
|
|
84
|
-
}
|
|
85
|
-
catch {
|
|
86
|
-
chiliPath = path.resolve(__dirname, '../../chili/dist/index.js');
|
|
87
|
-
}
|
|
88
|
-
return new Promise((resolve) => {
|
|
89
|
-
const child = spawn('node', [chiliPath, command, ...args], {
|
|
90
|
-
stdio: 'inherit',
|
|
91
|
-
env: process.env
|
|
92
|
-
});
|
|
93
|
-
child.on('close', () => resolve());
|
|
94
|
-
child.on('error', (err) => {
|
|
95
|
-
console.error(chalk.red(`Failed to start chili: ${err.message}`));
|
|
96
|
-
resolve();
|
|
97
|
-
});
|
|
98
|
-
});
|
|
99
|
-
}
|
|
100
|
-
/**
|
|
101
|
-
* Determines if a command should have its arguments expanded for wildcards.
|
|
102
|
-
*
|
|
103
|
-
* @param command - The command name.
|
|
104
|
-
* @returns True if wildcards should be expanded.
|
|
105
|
-
*/
|
|
106
|
-
function wildcards_expandCheck(command) {
|
|
107
|
-
// Commands that benefit from wildcard expansion
|
|
108
|
-
const expandCommands = ['ls', 'rm', 'cat', 'mv', 'cp', 'du', 'tree'];
|
|
109
|
-
return expandCommands.includes(command);
|
|
110
|
-
}
|
|
111
|
-
/**
|
|
112
|
-
* Executes a shell command on the host system (shell escape with ! prefix).
|
|
113
|
-
*
|
|
114
|
-
* @param shellCommand - The command to execute on the host shell.
|
|
115
|
-
* @returns A Promise that resolves when the command completes.
|
|
116
|
-
*/
|
|
117
|
-
async function shellCommand_execute(shellCommand) {
|
|
118
|
-
return new Promise((resolve) => {
|
|
119
|
-
const child = spawn(shellCommand, {
|
|
120
|
-
shell: true,
|
|
121
|
-
stdio: 'inherit',
|
|
122
|
-
env: process.env
|
|
123
|
-
});
|
|
124
|
-
child.on('close', (code) => {
|
|
125
|
-
if (code !== null && code !== 0) {
|
|
126
|
-
console.error(chalk.red(`Shell command exited with code ${code}`));
|
|
127
|
-
}
|
|
128
|
-
resolve();
|
|
129
|
-
});
|
|
130
|
-
child.on('error', (err) => {
|
|
131
|
-
console.error(chalk.red(`Failed to execute shell command: ${err.message}`));
|
|
132
|
-
resolve();
|
|
133
|
-
});
|
|
134
|
-
});
|
|
135
|
-
}
|
|
136
|
-
/**
|
|
137
|
-
* Handles a command entered by the user.
|
|
138
|
-
*
|
|
139
|
-
* @param line - The input line.
|
|
140
|
-
* @returns A Promise that resolves once the command has been processed.
|
|
141
|
-
*/
|
|
142
|
-
async function command_handle(line) {
|
|
143
|
-
const trimmedLine = line.trim();
|
|
144
|
-
if (!trimmedLine)
|
|
145
|
-
return;
|
|
146
|
-
// Start timing if enabled
|
|
147
|
-
const timingEnabled = session.timingEnabled_get();
|
|
148
|
-
const startTime = timingEnabled ? performance.now() : 0;
|
|
149
|
-
if (command_shellEscape_detect(trimmedLine)) {
|
|
150
|
-
await shellEscape_handle(trimmedLine, startTime, timingEnabled);
|
|
151
|
-
return;
|
|
152
|
-
}
|
|
153
|
-
const semicolonResult = await semicolons_handle(trimmedLine, startTime, timingEnabled);
|
|
154
|
-
if (!semicolonResult.ok) {
|
|
155
|
-
return;
|
|
156
|
-
}
|
|
157
|
-
if (semicolonResult.value)
|
|
158
|
-
return;
|
|
159
|
-
const redirectResult = await redirect_handle(trimmedLine, startTime, timingEnabled);
|
|
160
|
-
if (!redirectResult.ok) {
|
|
161
|
-
return;
|
|
162
|
-
}
|
|
163
|
-
if (redirectResult.value)
|
|
164
|
-
return;
|
|
165
|
-
const pipeResult = await pipe_handle(trimmedLine, startTime, timingEnabled);
|
|
166
|
-
if (!pipeResult.ok) {
|
|
167
|
-
return;
|
|
168
|
-
}
|
|
169
|
-
if (pipeResult.value)
|
|
170
|
-
return;
|
|
171
|
-
const tokens = args_tokenize(trimmedLine);
|
|
172
|
-
if (tokens.length === 0)
|
|
173
|
-
return;
|
|
174
|
-
let [command, ...args] = tokens;
|
|
175
|
-
// Check for --help flag before any processing
|
|
176
|
-
const helpResult = help_showMaybe(command, args);
|
|
177
|
-
if (helpResult.ok && helpResult.value) {
|
|
178
|
-
return;
|
|
179
|
-
}
|
|
180
|
-
// Expand wildcards for commands that support it
|
|
181
|
-
const expandResult = await wildcards_expand(command, args);
|
|
182
|
-
if (!expandResult.ok) {
|
|
183
|
-
return;
|
|
184
|
-
}
|
|
185
|
-
args = expandResult.value;
|
|
186
|
-
// Attempt to handle as a simulated plugin execution
|
|
187
|
-
if (await pluginExecutable_handle(command, args)) {
|
|
188
|
-
// Display timing if enabled
|
|
189
|
-
command_timingMaybePrint(startTime, timingEnabled);
|
|
190
|
-
return;
|
|
191
|
-
}
|
|
192
|
-
await command_dispatch(command, args);
|
|
193
|
-
// Display timing if enabled
|
|
194
|
-
command_timingMaybePrint(startTime, timingEnabled);
|
|
195
|
-
}
|
|
196
|
-
/**
|
|
197
|
-
* Parses a command line for pipe operators and returns segments.
|
|
198
|
-
*
|
|
199
|
-
* @param line - The full command line.
|
|
200
|
-
* @returns An array of command segments.
|
|
201
|
-
*/
|
|
202
|
-
function pipes_parse(line) {
|
|
203
|
-
const segments = [];
|
|
204
|
-
let currentSegment = '';
|
|
205
|
-
let inSingleQuote = false;
|
|
206
|
-
let inDoubleQuote = false;
|
|
207
|
-
for (let i = 0; i < line.length; i++) {
|
|
208
|
-
const char = line[i];
|
|
209
|
-
if (char === "'" && !inDoubleQuote) {
|
|
210
|
-
inSingleQuote = !inSingleQuote;
|
|
211
|
-
currentSegment += char;
|
|
212
|
-
}
|
|
213
|
-
else if (char === '"' && !inSingleQuote) {
|
|
214
|
-
inDoubleQuote = !inDoubleQuote;
|
|
215
|
-
currentSegment += char;
|
|
216
|
-
}
|
|
217
|
-
else if (char === '|' && !inSingleQuote && !inDoubleQuote) {
|
|
218
|
-
segments.push(currentSegment.trim());
|
|
219
|
-
currentSegment = '';
|
|
220
|
-
}
|
|
221
|
-
else {
|
|
222
|
-
currentSegment += char;
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
if (currentSegment.trim()) {
|
|
226
|
-
segments.push(currentSegment.trim());
|
|
227
|
-
}
|
|
228
|
-
return segments;
|
|
229
|
-
}
|
|
230
|
-
function redirect_parse(line) {
|
|
231
|
-
let inSingleQuote = false;
|
|
232
|
-
let inDoubleQuote = false;
|
|
233
|
-
for (let i = 0; i < line.length; i++) {
|
|
234
|
-
const char = line[i];
|
|
235
|
-
if (char === "'" && !inDoubleQuote) {
|
|
236
|
-
inSingleQuote = !inSingleQuote;
|
|
237
|
-
}
|
|
238
|
-
else if (char === '"' && !inSingleQuote) {
|
|
239
|
-
inDoubleQuote = !inDoubleQuote;
|
|
240
|
-
}
|
|
241
|
-
else if (!inSingleQuote && !inDoubleQuote) {
|
|
242
|
-
// Check for >>
|
|
243
|
-
if (i < line.length - 1 && line[i] === '>' && line[i + 1] === '>') {
|
|
244
|
-
const command = line.substring(0, i).trim();
|
|
245
|
-
const filePath = line.substring(i + 2).trim();
|
|
246
|
-
return { command, operator: '>>', filePath };
|
|
247
|
-
}
|
|
248
|
-
// Check for >
|
|
249
|
-
else if (line[i] === '>') {
|
|
250
|
-
const command = line.substring(0, i).trim();
|
|
251
|
-
const filePath = line.substring(i + 1).trim();
|
|
252
|
-
return { command, operator: '>', filePath };
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
return null;
|
|
257
|
-
}
|
|
258
|
-
/**
|
|
259
|
-
* Resolves the final redirect target path. If the target is an existing directory
|
|
260
|
-
* and the source command is `cat <file>`, resolves to `<dir>/<basename>`.
|
|
261
|
-
* Returns Err (with message on errorStack) when the filename cannot be determined.
|
|
262
|
-
*
|
|
263
|
-
* @param filePath - The redirect target path as written by the user.
|
|
264
|
-
* @param commandLine - The command whose output is being redirected.
|
|
265
|
-
* @returns Ok(resolved path) or Err on failure.
|
|
266
|
-
*/
|
|
267
|
-
function redirectTarget_resolve(filePath, commandLine) {
|
|
268
|
-
try {
|
|
269
|
-
const stats = statSync(filePath);
|
|
270
|
-
if (!stats.isDirectory()) {
|
|
271
|
-
return Ok(filePath);
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
catch (err) {
|
|
275
|
-
const nodeErr = err;
|
|
276
|
-
if (nodeErr.code === 'ENOENT') {
|
|
277
|
-
return Ok(filePath);
|
|
278
|
-
}
|
|
279
|
-
errorStack.stack_push('error', `Redirect: filesystem error for '${filePath}': ${String(err)}`);
|
|
280
|
-
return Err();
|
|
281
|
-
}
|
|
282
|
-
const tokens = args_tokenize(commandLine);
|
|
283
|
-
if (tokens.length === 0) {
|
|
284
|
-
errorStack.stack_push('error', `Redirect target '${filePath}' is a directory and no source command was provided.`);
|
|
285
|
-
return Err();
|
|
286
|
-
}
|
|
287
|
-
const [command, ...args] = tokens;
|
|
288
|
-
if (command !== 'cat') {
|
|
289
|
-
errorStack.stack_push('error', `Redirect target '${filePath}' is a directory; cannot infer filename for command '${command}'.`);
|
|
290
|
-
return Err();
|
|
291
|
-
}
|
|
292
|
-
const sourceArgs = args.filter((arg) => arg !== '--binary');
|
|
293
|
-
if (sourceArgs.length === 0) {
|
|
294
|
-
errorStack.stack_push('error', `Redirect target '${filePath}' is a directory; no source file provided to 'cat'.`);
|
|
295
|
-
return Err();
|
|
296
|
-
}
|
|
297
|
-
if (sourceArgs.length > 1) {
|
|
298
|
-
errorStack.stack_push('error', `Redirect target '${filePath}' is a directory; 'cat' with multiple files cannot choose a single output name.`);
|
|
299
|
-
return Err();
|
|
300
|
-
}
|
|
301
|
-
const sourceName = path.basename(sourceArgs[0]);
|
|
302
|
-
return Ok(path.join(filePath, sourceName));
|
|
303
|
-
}
|
|
304
|
-
/**
|
|
305
|
-
* Captures console output during command execution.
|
|
306
|
-
*
|
|
307
|
-
* @param fn - The function to execute while capturing output.
|
|
308
|
-
* @returns The captured output as a string and buffer.
|
|
309
|
-
*/
|
|
310
|
-
async function output_capture(fn) {
|
|
311
|
-
const chunks = [];
|
|
312
|
-
const originalLog = console.log;
|
|
313
|
-
const originalStdoutWrite = process.stdout.write.bind(process.stdout);
|
|
314
|
-
// Override console.log and console.error
|
|
315
|
-
console.log = (...args) => {
|
|
316
|
-
const text = args.map(arg => typeof arg === 'string' ? arg : JSON.stringify(arg)).join(' ') + '\n';
|
|
317
|
-
chunks.push(Buffer.from(text, 'utf-8'));
|
|
318
|
-
};
|
|
319
|
-
// Override process.stdout.write to capture binary data
|
|
320
|
-
process.stdout.write = ((chunk) => {
|
|
321
|
-
if (typeof chunk === 'string') {
|
|
322
|
-
chunks.push(Buffer.from(chunk, 'utf-8'));
|
|
323
|
-
}
|
|
324
|
-
else if (Buffer.isBuffer(chunk)) {
|
|
325
|
-
chunks.push(chunk);
|
|
326
|
-
}
|
|
327
|
-
else if (chunk instanceof Uint8Array) {
|
|
328
|
-
chunks.push(Buffer.from(chunk));
|
|
329
|
-
}
|
|
330
|
-
return true;
|
|
331
|
-
});
|
|
332
|
-
try {
|
|
333
|
-
await fn();
|
|
334
|
-
}
|
|
335
|
-
finally {
|
|
336
|
-
// Restore original console methods and stdout.write
|
|
337
|
-
console.log = originalLog;
|
|
338
|
-
process.stdout.write = originalStdoutWrite;
|
|
339
|
-
}
|
|
340
|
-
const buffer = Buffer.concat(chunks);
|
|
341
|
-
const text = buffer.toString('utf-8');
|
|
342
|
-
return { text, buffer };
|
|
343
|
-
}
|
|
344
|
-
const COMMAND_HANDLERS = {
|
|
345
|
-
connect: builtin_connect,
|
|
346
|
-
logout: builtin_logout,
|
|
347
|
-
cd: builtin_cd,
|
|
348
|
-
ls: builtin_ls,
|
|
349
|
-
pwd: builtin_pwd,
|
|
350
|
-
cat: builtin_cat,
|
|
351
|
-
rm: builtin_rm,
|
|
352
|
-
cp: builtin_cp,
|
|
353
|
-
mv: builtin_mv,
|
|
354
|
-
touch: builtin_touch,
|
|
355
|
-
mkdir: builtin_mkdir,
|
|
356
|
-
upload: builtin_upload,
|
|
357
|
-
pacs: builtin_pacs,
|
|
358
|
-
pipeline: builtin_pipeline,
|
|
359
|
-
pipelines: builtin_pipeline,
|
|
360
|
-
pull: builtin_pull,
|
|
361
|
-
query: builtin_query,
|
|
362
|
-
cubepath: builtin_cubepath,
|
|
363
|
-
download: builtin_download,
|
|
364
|
-
edit: builtin_edit,
|
|
365
|
-
context: builtin_context,
|
|
366
|
-
parametersofplugin: builtin_parametersofplugin,
|
|
367
|
-
physicalmode: builtin_physicalmode,
|
|
368
|
-
prompt: builtin_prompt,
|
|
369
|
-
timing: builtin_timing,
|
|
370
|
-
whoami: builtin_whoami,
|
|
371
|
-
whereami: builtin_whereami,
|
|
372
|
-
debug: builtin_debug,
|
|
373
|
-
help: builtin_help,
|
|
374
|
-
proc: builtin_proc,
|
|
375
|
-
tree: builtin_tree,
|
|
376
|
-
du: builtin_du,
|
|
377
|
-
store: builtin_store,
|
|
378
|
-
plugin: builtin_plugin,
|
|
379
|
-
plugins: builtin_plugin,
|
|
380
|
-
feed: builtin_feed,
|
|
381
|
-
feeds: builtin_feed,
|
|
382
|
-
compute: builtin_compute,
|
|
383
|
-
computes: builtin_compute,
|
|
384
|
-
tag: builtin_tag,
|
|
385
|
-
tags: builtin_tag,
|
|
386
|
-
group: builtin_group,
|
|
387
|
-
groups: builtin_group,
|
|
388
|
-
pluginmeta: builtin_pluginmeta,
|
|
389
|
-
pluginmetas: builtin_pluginmeta,
|
|
390
|
-
meta: builtin_pluginmeta,
|
|
391
|
-
metas: builtin_pluginmeta,
|
|
392
|
-
plugininstance: builtin_plugininstance,
|
|
393
|
-
plugininstances: builtin_plugininstance,
|
|
394
|
-
instance: builtin_plugininstance,
|
|
395
|
-
instances: builtin_plugininstance,
|
|
396
|
-
job: builtin_plugininstance,
|
|
397
|
-
jobs: builtin_plugininstance,
|
|
398
|
-
workflow: builtin_workflow,
|
|
399
|
-
workflows: builtin_workflow,
|
|
400
|
-
files: builtin_files,
|
|
401
|
-
links: builtin_links,
|
|
402
|
-
dirs: builtin_dirs,
|
|
403
|
-
pacsservers: async (args) => {
|
|
404
|
-
await chiliCommand_run('pacsservers', ['-s', ...args]);
|
|
405
|
-
},
|
|
406
|
-
pacsqueries: async (args) => {
|
|
407
|
-
await chiliCommand_run('pacsqueries', ['-s', ...args]);
|
|
408
|
-
},
|
|
409
|
-
pacsretrieve: async (args) => {
|
|
410
|
-
await chiliCommand_run('pacsretrieve', ['-s', ...args]);
|
|
411
|
-
}
|
|
412
|
-
};
|
|
413
|
-
export { COMMAND_HANDLERS_KEYS } from './command-keys.js';
|
|
414
|
-
/**
|
|
415
|
-
* Prints elapsed time since startTime if timing is enabled.
|
|
416
|
-
*
|
|
417
|
-
* @param startTime - Timestamp from `performance.now()` at command start.
|
|
418
|
-
* @param enabled - Whether timing display is active.
|
|
419
|
-
*/
|
|
420
|
-
function command_timingMaybePrint(startTime, enabled) {
|
|
421
|
-
if (!enabled)
|
|
422
|
-
return;
|
|
423
|
-
const elapsed = performance.now() - startTime;
|
|
424
|
-
console.log(chalk.gray(`[${elapsed.toFixed(2)}ms]`));
|
|
425
|
-
}
|
|
426
|
-
/**
|
|
427
|
-
* Handles a pipeline name invoked directly as an executable from /bin.
|
|
428
|
-
* Routes flag combinations to the appropriate pipeline subcommand:
|
|
429
|
-
* --nodes / --parameters → pipeline info
|
|
430
|
-
* --source / --readme → pipeline source
|
|
431
|
-
* (bare or --compute) → pipeline run
|
|
432
|
-
*
|
|
433
|
-
* @param name - The pipeline name as typed.
|
|
434
|
-
* @param args - Arguments following the pipeline name.
|
|
435
|
-
*/
|
|
436
|
-
async function pipelineExecutable_handle(name, args) {
|
|
437
|
-
if (args.includes('--nodes') || args.includes('--parameters')) {
|
|
438
|
-
await builtin_pipeline(['info', name]);
|
|
439
|
-
}
|
|
440
|
-
else if (args.includes('--source') || args.includes('--readme')) {
|
|
441
|
-
await builtin_pipeline(['source', name]);
|
|
442
|
-
}
|
|
443
|
-
else {
|
|
444
|
-
await builtin_pipeline(['run', name, ...args]);
|
|
445
|
-
}
|
|
446
|
-
}
|
|
447
|
-
/**
|
|
448
|
-
* Dispatches a parsed command to its handler.
|
|
449
|
-
* Checks COMMAND_HANDLERS, then /bin plugin names, then falls back to chili.
|
|
450
|
-
*
|
|
451
|
-
* @param command - The command name.
|
|
452
|
-
* @param args - Parsed arguments.
|
|
453
|
-
*/
|
|
454
|
-
async function command_dispatch(command, args) {
|
|
455
|
-
if (command === 'exit') {
|
|
456
|
-
process.exit(0);
|
|
457
|
-
}
|
|
458
|
-
const handler = COMMAND_HANDLERS[command];
|
|
459
|
-
if (handler) {
|
|
460
|
-
await handler(args);
|
|
461
|
-
return;
|
|
462
|
-
}
|
|
463
|
-
const binResult = await vfs.data_get('/bin');
|
|
464
|
-
if (binResult.ok) {
|
|
465
|
-
const pluginItem = binResult.value.find(item => item.name === command && item.type === 'plugin');
|
|
466
|
-
const pipelineItem = binResult.value.find(item => item.name === command && item.type === 'pipeline');
|
|
467
|
-
if (pluginItem) {
|
|
468
|
-
await builtin_executePlugin(command, args);
|
|
469
|
-
return;
|
|
470
|
-
}
|
|
471
|
-
if (pipelineItem) {
|
|
472
|
-
await pipelineExecutable_handle(command, args);
|
|
473
|
-
return;
|
|
474
|
-
}
|
|
475
|
-
}
|
|
476
|
-
console.log(chalk.yellow(`Unknown chell command '${command}' -- delegating to a spawned chili instance (slight delay expected)`));
|
|
477
|
-
await chiliCommand_run(command, ['-s', ...args]);
|
|
478
|
-
}
|
|
479
|
-
/**
|
|
480
|
-
* Returns true if the input line is a shell escape (starts with `!`).
|
|
481
|
-
*
|
|
482
|
-
* @param line - Trimmed input line.
|
|
483
|
-
*/
|
|
484
|
-
function command_shellEscape_detect(line) {
|
|
485
|
-
return line.startsWith('!');
|
|
486
|
-
}
|
|
487
|
-
/**
|
|
488
|
-
* Executes a shell-escaped command and prints timing if enabled.
|
|
489
|
-
*
|
|
490
|
-
* @param line - Full input line including the leading `!`.
|
|
491
|
-
* @param startTime - Timing reference from `performance.now()`.
|
|
492
|
-
* @param timingEnabled - Whether to print elapsed time after execution.
|
|
493
|
-
*/
|
|
494
|
-
async function shellEscape_handle(line, startTime, timingEnabled) {
|
|
495
|
-
const shellCommand = line.substring(1).trim();
|
|
496
|
-
if (!shellCommand)
|
|
497
|
-
return;
|
|
498
|
-
await shellCommand_execute(shellCommand);
|
|
499
|
-
command_timingMaybePrint(startTime, timingEnabled);
|
|
500
|
-
}
|
|
501
|
-
/**
|
|
502
|
-
* Splits a semicolon-separated command line and executes each segment in sequence.
|
|
503
|
-
* Returns Ok(true) if the line contained semicolons and was handled, Ok(false) if not.
|
|
504
|
-
*
|
|
505
|
-
* @param line - Full input line.
|
|
506
|
-
* @param startTime - Timing reference.
|
|
507
|
-
* @param timingEnabled - Whether to print total elapsed time after all segments.
|
|
508
|
-
*/
|
|
509
|
-
async function semicolons_handle(line, startTime, timingEnabled) {
|
|
510
|
-
const commands = semicolons_parse(line);
|
|
511
|
-
if (commands.length <= 1) {
|
|
512
|
-
return Ok(false);
|
|
513
|
-
}
|
|
514
|
-
for (const cmd of commands) {
|
|
515
|
-
try {
|
|
516
|
-
await command_handle(cmd);
|
|
517
|
-
}
|
|
518
|
-
catch (error) {
|
|
519
|
-
const msg = error instanceof Error ? error.message : String(error);
|
|
520
|
-
console.error(chalk.red(`Command error: ${msg}`));
|
|
521
|
-
if (stopOnError) {
|
|
522
|
-
return Err();
|
|
523
|
-
}
|
|
524
|
-
}
|
|
525
|
-
}
|
|
526
|
-
if (timingEnabled) {
|
|
527
|
-
const elapsed = performance.now() - startTime;
|
|
528
|
-
console.log(chalk.gray(`[Total: ${elapsed.toFixed(2)}ms]`));
|
|
529
|
-
}
|
|
530
|
-
return Ok(true);
|
|
531
|
-
}
|
|
532
|
-
/**
|
|
533
|
-
* Handles output redirection (`>` / `>>`). Captures command output and writes to the target file.
|
|
534
|
-
* Returns Ok(true) if redirection was detected and handled, Ok(false) if not.
|
|
535
|
-
*
|
|
536
|
-
* @param line - Full input line.
|
|
537
|
-
* @param startTime - Timing reference.
|
|
538
|
-
* @param timingEnabled - Whether to print elapsed time after execution.
|
|
539
|
-
*/
|
|
540
|
-
async function redirect_handle(line, startTime, timingEnabled) {
|
|
541
|
-
const redirectInfo = redirect_parse(line);
|
|
542
|
-
if (!redirectInfo) {
|
|
543
|
-
return Ok(false);
|
|
544
|
-
}
|
|
545
|
-
const { buffer } = await chellCommand_executeAndCapture(redirectInfo.command);
|
|
546
|
-
const targetResult = redirectTarget_resolve(redirectInfo.filePath, redirectInfo.command);
|
|
547
|
-
if (!targetResult.ok) {
|
|
548
|
-
const lastError = errorStack.stack_pop();
|
|
549
|
-
console.error(chalk.red(lastError ? lastError.message : 'Redirect error'));
|
|
550
|
-
return Err();
|
|
551
|
-
}
|
|
552
|
-
if (redirectInfo.operator === '>') {
|
|
553
|
-
writeFileSync(targetResult.value, buffer);
|
|
554
|
-
}
|
|
555
|
-
else {
|
|
556
|
-
appendFileSync(targetResult.value, buffer);
|
|
557
|
-
}
|
|
558
|
-
command_timingMaybePrint(startTime, timingEnabled);
|
|
559
|
-
return Ok(true);
|
|
560
|
-
}
|
|
561
|
-
/**
|
|
562
|
-
* Handles pipe chains (`cmd1 | cmd2 | ...`). Executes the first segment in chell and
|
|
563
|
-
* pipes the output through subsequent host-shell processes.
|
|
564
|
-
* Returns Ok(true) if a pipe was detected and handled, Ok(false) if not.
|
|
565
|
-
*
|
|
566
|
-
* @param line - Full input line.
|
|
567
|
-
* @param startTime - Timing reference.
|
|
568
|
-
* @param timingEnabled - Whether to print elapsed time after the chain completes.
|
|
569
|
-
*/
|
|
570
|
-
async function pipe_handle(line, startTime, timingEnabled) {
|
|
571
|
-
const segments = pipes_parse(line);
|
|
572
|
-
if (segments.length <= 1) {
|
|
573
|
-
return Ok(false);
|
|
574
|
-
}
|
|
575
|
-
try {
|
|
576
|
-
await pipe_execute(segments);
|
|
577
|
-
}
|
|
578
|
-
catch (error) {
|
|
579
|
-
const msg = error instanceof Error ? error.message : String(error);
|
|
580
|
-
console.error(chalk.red(`Pipe error: ${msg}`));
|
|
581
|
-
return Err();
|
|
582
|
-
}
|
|
583
|
-
command_timingMaybePrint(startTime, timingEnabled);
|
|
584
|
-
return Ok(true);
|
|
585
|
-
}
|
|
586
|
-
/**
|
|
587
|
-
* Shows help for a command if `--help` or `-h` is present in args.
|
|
588
|
-
* Returns Ok(true) if help was displayed, Ok(false) otherwise.
|
|
589
|
-
*
|
|
590
|
-
* @param command - The command name (used to look up help text).
|
|
591
|
-
* @param args - Parsed argument list.
|
|
592
|
-
*/
|
|
593
|
-
function help_showMaybe(command, args) {
|
|
594
|
-
if (args_checkHasHelpFlag(args, command)) {
|
|
595
|
-
help_show(command);
|
|
596
|
-
return Ok(true);
|
|
597
|
-
}
|
|
598
|
-
return Ok(false);
|
|
599
|
-
}
|
|
600
|
-
/**
|
|
601
|
-
* Expands wildcard patterns in args for commands that support it.
|
|
602
|
-
* Returns Ok(expanded args) or Err if expansion fails.
|
|
603
|
-
*
|
|
604
|
-
* @param command - The command name (determines whether expansion applies).
|
|
605
|
-
* @param args - Raw argument list potentially containing glob patterns.
|
|
606
|
-
*/
|
|
607
|
-
async function wildcards_expand(command, args) {
|
|
608
|
-
if (!wildcards_expandCheck(command)) {
|
|
609
|
-
return Ok(args);
|
|
610
|
-
}
|
|
611
|
-
const expandResult = await wildcards_expandAll(args);
|
|
612
|
-
if (!expandResult.ok) {
|
|
613
|
-
const lastError = errorStack.stack_pop();
|
|
614
|
-
if (lastError) {
|
|
615
|
-
console.error(chalk.red(error_stripDebugPrefix(lastError.message)));
|
|
616
|
-
}
|
|
617
|
-
return Err();
|
|
618
|
-
}
|
|
619
|
-
return Ok(expandResult.value);
|
|
620
|
-
}
|
|
621
|
-
/**
|
|
622
|
-
* Executes a chell command and captures its output.
|
|
623
|
-
* Consults COMMAND_HANDLERS — the single source of truth — so the pipe path
|
|
624
|
-
* is always consistent with the direct-execution path.
|
|
625
|
-
*
|
|
626
|
-
* @param commandLine - The command line to execute.
|
|
627
|
-
* @returns The captured output as text and raw buffer.
|
|
628
|
-
*/
|
|
629
|
-
async function chellCommand_executeAndCapture(commandLine) {
|
|
630
|
-
const trimmedLine = commandLine.trim();
|
|
631
|
-
if (!trimmedLine)
|
|
632
|
-
return { text: '', buffer: Buffer.alloc(0) };
|
|
633
|
-
const tokens = args_tokenize(trimmedLine);
|
|
634
|
-
if (tokens.length === 0) {
|
|
635
|
-
return { text: '', buffer: Buffer.alloc(0) };
|
|
636
|
-
}
|
|
637
|
-
let [command, ...args] = tokens;
|
|
638
|
-
if (wildcards_expandCheck(command)) {
|
|
639
|
-
const expandResult = await wildcards_expandAll(args);
|
|
640
|
-
if (!expandResult.ok) {
|
|
641
|
-
const lastError = errorStack.stack_pop();
|
|
642
|
-
const errorMsg = lastError ? error_stripDebugPrefix(lastError.message) : 'Unknown error';
|
|
643
|
-
return { text: chalk.red(`${errorMsg}\n`), buffer: Buffer.from('') };
|
|
644
|
-
}
|
|
645
|
-
args = expandResult.value;
|
|
646
|
-
}
|
|
647
|
-
return output_capture(async () => {
|
|
648
|
-
if (command === 'exit') {
|
|
649
|
-
process.exit(0);
|
|
650
|
-
}
|
|
651
|
-
if (await pluginExecutable_handle(command, args, { piped: true })) {
|
|
652
|
-
return;
|
|
653
|
-
}
|
|
654
|
-
const handler = COMMAND_HANDLERS[command];
|
|
655
|
-
if (handler) {
|
|
656
|
-
await handler(args);
|
|
657
|
-
return;
|
|
658
|
-
}
|
|
659
|
-
console.log(chalk.yellow(`Unknown chell command '${command}' -- delegating to a spawned chili instance (slight delay expected)`));
|
|
660
|
-
await chiliCommand_run(command, ['-s', ...args]);
|
|
661
|
-
});
|
|
662
|
-
}
|
|
663
|
-
/**
|
|
664
|
-
* Executes a pipe chain by running the first command in chell and piping through local tools.
|
|
665
|
-
*
|
|
666
|
-
* @param segments - Array of command segments separated by pipes.
|
|
667
|
-
* @returns A Promise that resolves when the pipe chain completes.
|
|
668
|
-
*/
|
|
669
|
-
async function pipe_execute(segments) {
|
|
670
|
-
if (segments.length === 0)
|
|
671
|
-
return;
|
|
672
|
-
// Execute first segment in chell and capture output
|
|
673
|
-
const firstCommand = segments[0];
|
|
674
|
-
const { buffer } = await chellCommand_executeAndCapture(firstCommand);
|
|
675
|
-
if (segments.length === 1) {
|
|
676
|
-
// No pipes, just output the result
|
|
677
|
-
process.stdout.write(buffer);
|
|
678
|
-
return;
|
|
679
|
-
}
|
|
680
|
-
// Chain remaining segments as spawned processes
|
|
681
|
-
let currentInput = buffer;
|
|
682
|
-
for (let i = 1; i < segments.length; i++) {
|
|
683
|
-
currentInput = await segment_pipeThrough(segments[i], currentInput);
|
|
684
|
-
}
|
|
685
|
-
// Output final result
|
|
686
|
-
process.stdout.write(currentInput);
|
|
687
|
-
}
|
|
688
|
-
/**
|
|
689
|
-
* Prompts the user for a password without echoing input.
|
|
690
|
-
*
|
|
691
|
-
* @param user - The username.
|
|
692
|
-
* @param url - The CUBE URL.
|
|
693
|
-
* @returns A Promise resolving to the password string.
|
|
694
|
-
*/
|
|
695
|
-
async function password_prompt(user, url) {
|
|
696
|
-
return new Promise((resolve) => {
|
|
697
|
-
const mutableStdout = new Writable({
|
|
698
|
-
write: function (chunk, encoding, callback) {
|
|
699
|
-
if (!this.muted)
|
|
700
|
-
process.stdout.write(chunk, encoding);
|
|
701
|
-
callback();
|
|
702
|
-
}
|
|
703
|
-
});
|
|
704
|
-
const rl = readline.createInterface({
|
|
705
|
-
input: process.stdin,
|
|
706
|
-
output: mutableStdout,
|
|
707
|
-
terminal: true
|
|
708
|
-
});
|
|
709
|
-
mutableStdout.muted = false;
|
|
710
|
-
process.stdout.write(`Password for ${user}@${url}: `);
|
|
711
|
-
mutableStdout.muted = true;
|
|
712
|
-
rl.question('', (password) => {
|
|
713
|
-
rl.close();
|
|
714
|
-
console.log(''); // Add newline after entry
|
|
715
|
-
resolve(password);
|
|
716
|
-
});
|
|
717
|
-
});
|
|
718
|
-
}
|
|
719
|
-
let stopOnError = false;
|
|
720
|
-
/**
|
|
721
|
-
* Warm the VFS cache for plugins, feeds, and jobs.
|
|
722
|
-
*
|
|
723
|
-
* @param flags - Which resource types to prefetch.
|
|
724
|
-
* @param user - Authenticated ChRIS username (for feed path construction).
|
|
725
|
-
* @param isInteractive - Whether to show spinners vs plain log lines.
|
|
726
|
-
* @param boot - Boot logger; null in non-interactive modes.
|
|
727
|
-
* @returns Counts for each prefetched resource type.
|
|
728
|
-
*/
|
|
729
|
-
async function cache_prefetch(flags, user, isInteractive, boot) {
|
|
730
|
-
const result = {};
|
|
731
|
-
// --- Plugins & Pipelines ---
|
|
732
|
-
if (!session.offline && flags.plugins) {
|
|
733
|
-
const r = await prefetch_withSpinner('Plugins', 'Prefetching /bin for completions', isInteractive, async () => {
|
|
734
|
-
const vfsResult = await vfs.data_get('/bin');
|
|
735
|
-
if (vfsResult.ok) {
|
|
736
|
-
return {
|
|
737
|
-
ok: true,
|
|
738
|
-
count: vfsResult.value.filter((i) => i.type === 'plugin').length,
|
|
739
|
-
pipelineCount: vfsResult.value.filter((i) => i.type === 'pipeline').length,
|
|
740
|
-
};
|
|
741
|
-
}
|
|
742
|
-
const err = errorStack.stack_pop();
|
|
743
|
-
return { ok: false, message: err ? error_stripDebugPrefix(err.message) : 'Failed to prefetch /bin' };
|
|
744
|
-
});
|
|
745
|
-
if (r.ok) {
|
|
746
|
-
result.plugins = r.count;
|
|
747
|
-
result.pipelines = r.pipelineCount;
|
|
748
|
-
boot?.log('ok', 'Plugins', `Cached ${r.count ?? 0} plugin(s)`);
|
|
749
|
-
boot?.log('ok', 'Pipelines', `Cached ${r.pipelineCount ?? 0} pipeline(s)`);
|
|
750
|
-
}
|
|
751
|
-
else {
|
|
752
|
-
boot?.log('fail', 'Plugins', r.message || 'Failed to prefetch /bin');
|
|
753
|
-
}
|
|
754
|
-
}
|
|
755
|
-
else if (!session.offline) {
|
|
756
|
-
boot?.log('skip', 'Plugins', 'Prefetch disabled');
|
|
757
|
-
boot?.log('skip', 'Pipelines', 'Prefetch disabled');
|
|
758
|
-
}
|
|
759
|
-
else {
|
|
760
|
-
boot?.log('skip', 'Plugins', 'Offline mode');
|
|
761
|
-
boot?.log('skip', 'Pipelines', 'Offline mode');
|
|
762
|
-
}
|
|
763
|
-
// --- Feeds ---
|
|
764
|
-
if (!session.offline && flags.feeds) {
|
|
765
|
-
const feedPath = user ? `/home/${user}/feeds` : undefined;
|
|
766
|
-
if (feedPath) {
|
|
767
|
-
const r = await prefetch_withSpinner('Feeds', 'Prefetching user feeds', isInteractive, () => prefetch_path(feedPath));
|
|
768
|
-
if (r.ok) {
|
|
769
|
-
result.feeds = r.count;
|
|
770
|
-
boot?.log('ok', 'Feeds', `Cached ${r.count ?? 0} item(s) from ${feedPath}`);
|
|
771
|
-
}
|
|
772
|
-
else {
|
|
773
|
-
boot?.log('fail', 'Feeds', r.message || `Prefetch failed for ${feedPath}`);
|
|
774
|
-
}
|
|
775
|
-
}
|
|
776
|
-
else {
|
|
777
|
-
boot?.log('skip', 'Feeds', 'No user context');
|
|
778
|
-
}
|
|
779
|
-
if (flags.publicFeeds) {
|
|
780
|
-
const r = await prefetch_withSpinner('Public', 'Prefetching public feeds', isInteractive, () => prefetch_path('/PUBLIC'));
|
|
781
|
-
if (r.ok) {
|
|
782
|
-
result.public = r.count;
|
|
783
|
-
boot?.log('ok', 'Public', `Cached ${r.count ?? 0} item(s) from /PUBLIC`);
|
|
784
|
-
}
|
|
785
|
-
else {
|
|
786
|
-
boot?.log('fail', 'Public', r.message || 'Prefetch failed for /PUBLIC');
|
|
787
|
-
}
|
|
788
|
-
}
|
|
789
|
-
}
|
|
790
|
-
else if (!session.offline) {
|
|
791
|
-
boot?.log('skip', 'Feeds', 'Prefetch disabled');
|
|
792
|
-
}
|
|
793
|
-
else {
|
|
794
|
-
boot?.log('skip', 'Feeds', 'Offline mode');
|
|
795
|
-
}
|
|
796
|
-
// --- Jobs ---
|
|
797
|
-
if (!session.offline && flags.jobs) {
|
|
798
|
-
const r = await prefetch_withSpinner('Jobs', 'Indexing /proc/jobs (feed list)...', isInteractive, async () => {
|
|
799
|
-
try {
|
|
800
|
-
await procCache_refresh();
|
|
801
|
-
const { procCache_get } = await import('@fnndsc/cumin');
|
|
802
|
-
return { ok: true, count: procCache_get().feedIDs_get().length };
|
|
803
|
-
}
|
|
804
|
-
catch (err) {
|
|
805
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
806
|
-
return { ok: false, message: msg };
|
|
807
|
-
}
|
|
808
|
-
});
|
|
809
|
-
if (r.ok) {
|
|
810
|
-
boot?.log('ok', 'Jobs', `Indexed ${r.count ?? 0} feed(s) — topology warming in background`);
|
|
811
|
-
}
|
|
812
|
-
else {
|
|
813
|
-
boot?.log('fail', 'Jobs', r.message || 'Failed to index /proc/jobs');
|
|
814
|
-
}
|
|
815
|
-
}
|
|
816
|
-
else if (!session.offline) {
|
|
817
|
-
boot?.log('skip', 'Jobs', 'Prefetch disabled');
|
|
818
|
-
}
|
|
819
|
-
else {
|
|
820
|
-
boot?.log('skip', 'Jobs', 'Offline mode');
|
|
821
|
-
}
|
|
822
|
-
return result;
|
|
823
|
-
}
|
|
824
|
-
/**
|
|
825
|
-
* Assemble and render the Neofetch-style boot info panels.
|
|
826
|
-
*
|
|
827
|
-
* @param context - Current ChRIS session context.
|
|
828
|
-
* @param mode - Shell mode string (e.g. 'interactive').
|
|
829
|
-
* @param cache - Prefetch counts for the panel's ChRIS section.
|
|
830
|
-
* @param useAsciiBoot - Render ASCII box-drawing instead of Unicode.
|
|
831
|
-
*/
|
|
832
|
-
function bootPanels_render(context, mode, cache, useAsciiBoot) {
|
|
833
|
-
const headerItems = [
|
|
834
|
-
{ app: 'chell', name: 'ChELL Executes Layered Logic', version: packageJson.version },
|
|
835
|
-
{ app: 'chili', name: 'ChILI handles Intelligent Line Interactions', version: chiliJson.version },
|
|
836
|
-
{ app: 'salsa', name: 'Salsa Abstracts Logic Service Assets', version: salsaJson.version },
|
|
837
|
-
{ app: 'cumin', name: 'Cumin Underpins Management Infrastructure Needs', version: cuminJson.version },
|
|
838
|
-
];
|
|
839
|
-
const localItems = [
|
|
840
|
-
{ label: 'System', value: `${os.platform()} ${os.release()} (${os.arch()})` },
|
|
841
|
-
{ label: 'User', value: `${os.userInfo().username}@${os.hostname()}` },
|
|
842
|
-
...(localIPv4_get() ? [{ label: 'Local', value: localIPv4_get() }] : []),
|
|
843
|
-
{ label: 'Time', value: localTime_withOffset() },
|
|
844
|
-
];
|
|
845
|
-
const maxItemLen = Math.max(30, (process.stdout.columns || 100) - 20);
|
|
846
|
-
const str_truncate = (s) => s.length > maxItemLen ? `…${s.slice(-(maxItemLen - 1))}` : s;
|
|
847
|
-
const chrisItems = [
|
|
848
|
-
{ label: 'ChRIS', value: str_truncate(context.URL || 'offline') },
|
|
849
|
-
{ label: 'User', value: context.user || 'offline' },
|
|
850
|
-
{ label: 'Mode', value: mode },
|
|
851
|
-
];
|
|
852
|
-
if (typeof cache.plugins === 'number')
|
|
853
|
-
chrisItems.push({ label: 'Plugins', value: `${cache.plugins}` });
|
|
854
|
-
if (typeof cache.pipelines === 'number')
|
|
855
|
-
chrisItems.push({ label: 'Pipelines', value: `${cache.pipelines}` });
|
|
856
|
-
if (context.plugin)
|
|
857
|
-
chrisItems.push({ label: 'Active Plugin', value: context.plugin });
|
|
858
|
-
if (typeof cache.feeds === 'number')
|
|
859
|
-
chrisItems.push({ label: 'User Feeds', value: `${cache.feeds}` });
|
|
860
|
-
if (context.feed)
|
|
861
|
-
chrisItems.push({ label: 'Active Feed', value: `${context.feed}` });
|
|
862
|
-
if (typeof cache.public === 'number')
|
|
863
|
-
chrisItems.push({ label: 'Public Feeds', value: `${cache.public}` });
|
|
864
|
-
if (context.pacsserver)
|
|
865
|
-
chrisItems.push({ label: 'PACS Server', value: context.pacsserver });
|
|
866
|
-
chrisItems.push({ label: 'Path', value: str_truncate(context.folder ?? '/') });
|
|
867
|
-
const panels = { header: headerItems, local: localItems, chris: chrisItems };
|
|
868
|
-
bootsequence_printIntroPanelsStacked([], panels, !useAsciiBoot, useAsciiBoot);
|
|
869
|
-
}
|
|
870
|
-
/**
|
|
871
|
-
* Returns the first non-internal IPv4 address of the local machine, or null if none found.
|
|
872
|
-
*/
|
|
873
|
-
function localIPv4_get() {
|
|
874
|
-
const interfaces = os.networkInterfaces();
|
|
875
|
-
for (const key of Object.keys(interfaces)) {
|
|
876
|
-
const iface = interfaces[key];
|
|
877
|
-
if (!iface)
|
|
878
|
-
continue;
|
|
879
|
-
for (const entry of iface) {
|
|
880
|
-
if (entry.family === 'IPv4' && !entry.internal && entry.address) {
|
|
881
|
-
return entry.address;
|
|
882
|
-
}
|
|
883
|
-
}
|
|
884
|
-
}
|
|
885
|
-
return null;
|
|
886
|
-
}
|
|
887
|
-
/**
|
|
888
|
-
* Returns the current local date/time as `YYYY-MM-DD HH:MM:SS ±HH:MM`.
|
|
889
|
-
*/
|
|
890
|
-
function localTime_withOffset() {
|
|
891
|
-
const now = new Date();
|
|
892
|
-
const offsetMinutes = now.getTimezoneOffset();
|
|
893
|
-
const sign = offsetMinutes <= 0 ? '+' : '-';
|
|
894
|
-
const absMinutes = Math.abs(offsetMinutes);
|
|
895
|
-
const hours = Math.floor(absMinutes / 60);
|
|
896
|
-
const minutes = absMinutes % 60;
|
|
897
|
-
const pad = (n) => n.toString().padStart(2, '0');
|
|
898
|
-
const offsetStr = `${sign}${pad(hours)}:${pad(minutes)}`;
|
|
899
|
-
const dateStr = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}`;
|
|
900
|
-
const timeStr = `${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}`;
|
|
901
|
-
return `${dateStr} ${timeStr} ${offsetStr}`;
|
|
902
|
-
}
|
|
903
|
-
/**
|
|
904
|
-
* Executes a chell script file.
|
|
905
|
-
* Reads the file line by line, ignoring comments and blank lines.
|
|
906
|
-
* Supports shebang (#!) on first line.
|
|
907
|
-
*
|
|
908
|
-
* @param scriptPath - Path to the script file.
|
|
909
|
-
* @param stopOnError - Whether to stop execution on first error (default: false).
|
|
910
|
-
* @returns A Promise that resolves when script execution completes.
|
|
911
|
-
*
|
|
912
|
-
* @example
|
|
913
|
-
* ```typescript
|
|
914
|
-
* await script_execute('/path/to/script.chell', false);
|
|
915
|
-
* ```
|
|
916
|
-
*/
|
|
917
|
-
async function script_execute(scriptPath, stopOnError = false) {
|
|
918
|
-
if (!existsSync(scriptPath)) {
|
|
919
|
-
console.error(chalk.red(`Error: Script file not found: ${scriptPath}`));
|
|
920
|
-
process.exit(1);
|
|
921
|
-
}
|
|
922
|
-
// Set global stop-on-error flag
|
|
923
|
-
stopOnError = stopOnError;
|
|
924
|
-
const scriptContent = readFileSync(scriptPath, 'utf8');
|
|
925
|
-
const lines = scriptContent.split('\n');
|
|
926
|
-
for (let i = 0; i < lines.length; i++) {
|
|
927
|
-
let line = lines[i].trim();
|
|
928
|
-
// Skip shebang on first line
|
|
929
|
-
if (i === 0 && line.startsWith('#!')) {
|
|
930
|
-
continue;
|
|
931
|
-
}
|
|
932
|
-
// Skip comment lines and blank lines
|
|
933
|
-
if (line.startsWith('#') || line === '') {
|
|
934
|
-
continue;
|
|
935
|
-
}
|
|
936
|
-
// Execute the line
|
|
937
|
-
try {
|
|
938
|
-
await command_handle(line);
|
|
939
|
-
}
|
|
940
|
-
catch (error) {
|
|
941
|
-
const msg = error instanceof Error ? error.message : String(error);
|
|
942
|
-
console.error(chalk.red(`Error on line ${i + 1}: ${msg}`));
|
|
943
|
-
if (stopOnError) {
|
|
944
|
-
console.error(chalk.red('Stopping execution due to error (use without -e to continue on error)'));
|
|
945
|
-
process.exit(1);
|
|
946
|
-
}
|
|
947
|
-
// Otherwise continue to next line
|
|
948
|
-
}
|
|
949
|
-
}
|
|
950
|
-
// Reset flag
|
|
951
|
-
stopOnError = false;
|
|
952
|
-
}
|
|
953
|
-
/**
|
|
954
|
-
* Connect using explicit CLI credentials. Returns Err() on unrecoverable failure.
|
|
955
|
-
*/
|
|
956
|
-
async function connection_fromArgs(config, boot) {
|
|
957
|
-
let { user, password, url } = config.connectConfig;
|
|
958
|
-
if (!user) {
|
|
959
|
-
console.error(chalk.red('Error: Username required when connecting via CLI args.'));
|
|
960
|
-
return Err();
|
|
961
|
-
}
|
|
962
|
-
if (!password && url && config.mode !== 'execute' && config.mode !== 'script') {
|
|
963
|
-
password = await password_prompt(user, url);
|
|
964
|
-
}
|
|
965
|
-
if (!password && config.mode === 'execute') {
|
|
966
|
-
console.error(chalk.red('Error: Password required for connection in execute mode.'));
|
|
967
|
-
return Err();
|
|
968
|
-
}
|
|
969
|
-
if (!url || !password) {
|
|
970
|
-
return Ok(await context_getSingle());
|
|
971
|
-
}
|
|
972
|
-
spinner.start(`Establishing uplink to ${url}`);
|
|
973
|
-
try {
|
|
974
|
-
await session.connection.connection_connect({ user: user, password, url, debug: false });
|
|
975
|
-
session.offline = false;
|
|
976
|
-
await chrisContext.current_set(Context.ChRISuser, user);
|
|
977
|
-
await chrisContext.current_set(Context.ChRISURL, url);
|
|
978
|
-
await chrisContext.current_set(Context.ChRISfolder, '/');
|
|
979
|
-
await chrisContext.current_set(Context.ChRISfeed, '');
|
|
980
|
-
await chrisContext.current_set(Context.ChRISplugin, '');
|
|
981
|
-
spinner.stop();
|
|
982
|
-
if (config.mode !== 'execute' && config.mode !== 'script') {
|
|
983
|
-
console.log(chalk.green('[+] Connection established.'));
|
|
984
|
-
}
|
|
985
|
-
boot?.log('ok', 'Connect', `Connected to ${url}`);
|
|
986
|
-
return Ok(await context_getSingle());
|
|
987
|
-
}
|
|
988
|
-
catch (error) {
|
|
989
|
-
spinner.stop();
|
|
990
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
991
|
-
console.error(chalk.red(`[!] Connection failed: ${errorMessage}`));
|
|
992
|
-
boot?.log('fail', 'Connect', errorMessage);
|
|
993
|
-
return Err();
|
|
994
|
-
}
|
|
995
|
-
}
|
|
996
|
-
/**
|
|
997
|
-
* Restore a saved session from disk. Always succeeds — falls back to offline mode.
|
|
998
|
-
*/
|
|
999
|
-
async function connection_fromSavedSession(config, boot) {
|
|
1000
|
-
spinner.start('Checking for previous context');
|
|
1001
|
-
spinner.stop();
|
|
1002
|
-
const ctx = await context_getSingle();
|
|
1003
|
-
if (!ctx.user || !ctx.URL) {
|
|
1004
|
-
if (config.mode !== 'execute' && config.mode !== 'script') {
|
|
1005
|
-
console.log(chalk.yellow('[!] No previous context found'));
|
|
1006
|
-
}
|
|
1007
|
-
return ctx;
|
|
1008
|
-
}
|
|
1009
|
-
if (config.mode !== 'execute' && config.mode !== 'script') {
|
|
1010
|
-
console.log(chalk.green('[+] Previous context detected'));
|
|
1011
|
-
console.log(chalk.gray(` User: ${chalk.cyan(ctx.user)}`));
|
|
1012
|
-
console.log(chalk.gray(` URL: ${chalk.cyan(ctx.URL)}`));
|
|
1013
|
-
}
|
|
1014
|
-
spinner.start('Validating existing token');
|
|
1015
|
-
const token = await session.connection.authToken_get(true);
|
|
1016
|
-
spinner.stop();
|
|
1017
|
-
if (!token) {
|
|
1018
|
-
console.log(chalk.yellow('[!] No token found'));
|
|
1019
|
-
console.log(chalk.yellow('[!] Running in disconnected mode'));
|
|
1020
|
-
session.offline = true;
|
|
1021
|
-
console.log(chalk.gray(` Use: connect --user ${ctx.user} --password <pwd> ${ctx.URL}`));
|
|
1022
|
-
boot?.log('skip', 'Session', 'No saved token; offline');
|
|
1023
|
-
return ctx;
|
|
1024
|
-
}
|
|
1025
|
-
spinner.start(`Testing connection to ${ctx.URL}`);
|
|
1026
|
-
try {
|
|
1027
|
-
const client = await session.connection.client_get();
|
|
1028
|
-
if (!client) {
|
|
1029
|
-
spinner.stop();
|
|
1030
|
-
console.log(chalk.yellow('[!] Failed to create client'));
|
|
1031
|
-
console.log(chalk.yellow('[!] Running in disconnected mode'));
|
|
1032
|
-
session.offline = true;
|
|
1033
|
-
console.log(chalk.gray(` Use: connect --user ${ctx.user} --password <pwd> ${ctx.URL}`));
|
|
1034
|
-
return ctx;
|
|
1035
|
-
}
|
|
1036
|
-
await client.getUser();
|
|
1037
|
-
spinner.stop();
|
|
1038
|
-
if (config.mode !== 'execute' && config.mode !== 'script') {
|
|
1039
|
-
console.log(chalk.green('[+] Token validated with server'));
|
|
1040
|
-
console.log(chalk.green('[+] Session restored'));
|
|
1041
|
-
boot?.log('ok', 'Session', `Restored ${ctx.user}@${ctx.URL}`);
|
|
1042
|
-
}
|
|
1043
|
-
}
|
|
1044
|
-
catch (error) {
|
|
1045
|
-
spinner.stop();
|
|
1046
|
-
const msg = error instanceof Error ? error.message : String(error);
|
|
1047
|
-
console.log(chalk.yellow('[!] Token expired or invalid'));
|
|
1048
|
-
console.log(chalk.gray(` Error: ${msg}`));
|
|
1049
|
-
console.log(chalk.yellow('[!] Running in disconnected mode'));
|
|
1050
|
-
session.offline = true;
|
|
1051
|
-
console.log(chalk.gray(` Use: connect --user ${ctx.user} --password <pwd> ${ctx.URL}`));
|
|
1052
|
-
}
|
|
1053
|
-
return ctx;
|
|
1054
|
-
}
|
|
1055
|
-
/**
|
|
1056
|
-
* Establish a ChRIS connection: explicit CLI args or saved session restore.
|
|
1057
|
-
*
|
|
1058
|
-
* @param config - Parsed CLI config.
|
|
1059
|
-
* @param boot - Boot logger; null in non-interactive modes.
|
|
1060
|
-
* @returns Ok(context) on success; Err() on unrecoverable failure (caller should exit).
|
|
1061
|
-
*/
|
|
1062
|
-
async function connection_establish(config, boot) {
|
|
1063
|
-
if (config.connectConfig) {
|
|
1064
|
-
return await connection_fromArgs(config, boot);
|
|
1065
|
-
}
|
|
1066
|
-
const ctx = await connection_fromSavedSession(config, boot);
|
|
1067
|
-
if (!ctx.user || !ctx.URL) {
|
|
1068
|
-
if (!session.offline) {
|
|
1069
|
-
console.log(chalk.yellow('[!] Running in disconnected mode.'));
|
|
1070
|
-
session.offline = true;
|
|
1071
|
-
}
|
|
1072
|
-
}
|
|
1073
|
-
return Ok(ctx);
|
|
1074
|
-
}
|
|
1075
|
-
/**
|
|
1076
|
-
* Registers the static VFS providers and the logical→physical path resolver
|
|
1077
|
-
* hook on the shared salsa vfsDispatcher.
|
|
1078
|
-
*/
|
|
1079
|
-
async function vfsProviders_register() {
|
|
1080
|
-
const { vfsDispatcher } = await import('@fnndsc/salsa');
|
|
1081
|
-
const { StaticVfsProvider } = await import('./lib/vfs/providers/static.js');
|
|
1082
|
-
vfsDispatcher.provider_register(new StaticVfsProvider('/bin'));
|
|
1083
|
-
vfsDispatcher.provider_register(new StaticVfsProvider('/usr'));
|
|
1084
|
-
vfsDispatcher.provider_register(new StaticVfsProvider('/usr/bin'));
|
|
1085
|
-
vfsDispatcher.pathResolver_register(async (logicalPath) => {
|
|
1086
|
-
if (session.physicalMode_get()) {
|
|
1087
|
-
return logicalPath;
|
|
1088
|
-
}
|
|
1089
|
-
const { logical_toPhysical } = await import('@fnndsc/chili/utils');
|
|
1090
|
-
const res = await logical_toPhysical(logicalPath);
|
|
1091
|
-
if (res.ok) {
|
|
1092
|
-
return res.value;
|
|
1093
|
-
}
|
|
1094
|
-
throw new Error(`Logical-to-physical resolution failed for path: ${logicalPath}`);
|
|
1095
|
-
});
|
|
1096
|
-
}
|
|
1097
|
-
/**
|
|
1098
|
-
* Runs the interactive session: prefetches the boot cache, renders the boot
|
|
1099
|
-
* panels and greeting, kicks off non-blocking topology warm-up, and enters the
|
|
1100
|
-
* REPL loop.
|
|
1101
|
-
*
|
|
1102
|
-
* @param config - The parsed CLI config.
|
|
1103
|
-
* @param currentContext - The resolved session context.
|
|
1104
|
-
* @param flags - Prefetch/interactivity boot flags.
|
|
1105
|
-
* @param boot - The boot logger, or null when non-interactive.
|
|
1106
|
-
*/
|
|
1107
|
-
async function interactiveSession_run(config, currentContext, flags, boot) {
|
|
1108
|
-
const cache = await cache_prefetch({ plugins: flags.prefetchPlugins, feeds: flags.prefetchFeeds, publicFeeds: flags.prefetchPublicFeeds, jobs: flags.prefetchJobs }, currentContext.user ?? undefined, flags.isInteractiveSession, boot);
|
|
1109
|
-
if (flags.isInteractiveSession) {
|
|
1110
|
-
logo_animateStop();
|
|
1111
|
-
bootPanels_render(currentContext, config.mode, cache, flags.useAsciiBoot);
|
|
1112
|
-
}
|
|
1113
|
-
console.log(chalk.yellow('Order up! Your Taco Chell is ready! Filled with chili, salsa, and cumin goodness! 🌮'));
|
|
1114
|
-
console.log('');
|
|
1115
|
-
if (flags.isInteractiveSession) {
|
|
1116
|
-
if (session.offline) {
|
|
1117
|
-
console.log(chalk.yellow('You are currently disconnected. Use: connect --user <user> --password <pwd> <url>'));
|
|
1118
|
-
}
|
|
1119
|
-
console.log(chalk.gray("Tip: type 'help' for available commands."));
|
|
1120
|
-
console.log('');
|
|
1121
|
-
}
|
|
1122
|
-
// Fire instance topology warm-up — does NOT block the REPL.
|
|
1123
|
-
// Progress is shown in the prompt as [proc: N/total] until complete.
|
|
1124
|
-
if (!session.offline && flags.prefetchJobs) {
|
|
1125
|
-
procTopology_warmup().catch(() => { });
|
|
1126
|
-
}
|
|
1127
|
-
const repl = new REPL();
|
|
1128
|
-
await repl.start(command_handle);
|
|
1129
|
-
}
|
|
1130
|
-
/**
|
|
1131
|
-
* Starts the ChELL REPL.
|
|
1132
|
-
* Initializes connection and enters the command loop.
|
|
1133
|
-
*
|
|
1134
|
-
* @returns A Promise that resolves when the shell exits.
|
|
1135
|
-
*/
|
|
1136
|
-
export async function chell_start() {
|
|
1137
|
-
const config = await cli_parse(process.argv, packageJson.version);
|
|
1138
|
-
const { isInteractiveSession, useAsciiBoot, prefetchPlugins, prefetchFeeds, prefetchPublicFeeds, prefetchJobs, showLogo, } = bootFlags_compute(config, !!process.stdout.isTTY);
|
|
1139
|
-
const boot = isInteractiveSession ? bootLogger_create('ChELL Boot', useAsciiBoot) : null;
|
|
1140
|
-
if (config.mode === 'help' || config.mode === 'version') {
|
|
1141
|
-
if (config.output)
|
|
1142
|
-
console.log(config.output);
|
|
1143
|
-
return;
|
|
1144
|
-
}
|
|
1145
|
-
// --- Common Initialization ---
|
|
1146
|
-
if (isInteractiveSession) {
|
|
1147
|
-
logo_print(showLogo && !useAsciiBoot);
|
|
1148
|
-
}
|
|
1149
|
-
await settings_load();
|
|
1150
|
-
spinner.start('Initializing session components');
|
|
1151
|
-
await session.init();
|
|
1152
|
-
spinner.stop();
|
|
1153
|
-
await vfsProviders_register();
|
|
1154
|
-
if (isInteractiveSession) {
|
|
1155
|
-
console.log(chalk.green('[+] Session initialized.'));
|
|
1156
|
-
boot?.log('ok', 'Session', 'Components initialized');
|
|
1157
|
-
}
|
|
1158
|
-
// Set physical filesystem mode if requested
|
|
1159
|
-
if (config.physicalFS) {
|
|
1160
|
-
session.physicalMode_set(true);
|
|
1161
|
-
if (config.mode !== 'execute' && config.mode !== 'script') {
|
|
1162
|
-
console.log(chalk.yellow('[!] Physical filesystem mode enabled - logical-to-physical mapping disabled'));
|
|
1163
|
-
}
|
|
1164
|
-
}
|
|
1165
|
-
const ctxResult = await connection_establish(config, boot);
|
|
1166
|
-
if (!ctxResult.ok)
|
|
1167
|
-
process.exit(1);
|
|
1168
|
-
let currentContext = ctxResult.value;
|
|
1169
|
-
if (!currentContext.folder) {
|
|
1170
|
-
currentContext = { ...currentContext, folder: '/' };
|
|
1171
|
-
}
|
|
1172
|
-
// Login phase complete — start the brain activity animation now
|
|
1173
|
-
if (isInteractiveSession && showLogo && !useAsciiBoot) {
|
|
1174
|
-
logo_animatePulse();
|
|
1175
|
-
}
|
|
1176
|
-
// --- Execution Mode ---
|
|
1177
|
-
if (config.mode === 'execute' && config.commandToExecute) {
|
|
1178
|
-
// Set stop-on-error flag if requested
|
|
1179
|
-
if (config.stopOnError) {
|
|
1180
|
-
stopOnError = true;
|
|
1181
|
-
}
|
|
1182
|
-
await command_handle(config.commandToExecute);
|
|
1183
|
-
process.exit(process.exitCode ?? 0);
|
|
1184
|
-
}
|
|
1185
|
-
// --- Script Mode ---
|
|
1186
|
-
if (config.mode === 'script' && config.scriptFile) {
|
|
1187
|
-
await script_execute(config.scriptFile, config.stopOnError || false);
|
|
1188
|
-
process.exit(0);
|
|
1189
|
-
}
|
|
1190
|
-
// --- Interactive Mode ---
|
|
1191
|
-
await interactiveSession_run(config, currentContext, { prefetchPlugins, prefetchFeeds, prefetchPublicFeeds, prefetchJobs, isInteractiveSession, useAsciiBoot }, boot);
|
|
1192
|
-
}
|
|
10
|
+
export { chiliCommand_run, command_handle, command_dispatch, COMMAND_HANDLERS, COMMAND_HANDLERS_KEYS, stopOnError_set, } from './core/dispatch.js';
|
|
1193
11
|
//# sourceMappingURL=chell.js.map
|