@fnndsc/chili 3.2.5 → 3.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/README.md CHANGED
@@ -42,27 +42,34 @@ Designed for developers and power-users who want to script and control a ChRIS i
42
42
  2. **[`salsa`](https://github.com/FNNDSC/salsa) (Logic)**: Shared Application Logic and Service Assets — high-level business intents.
43
43
  3. **[`cumin`](https://github.com/FNNDSC/cumin) (Infrastructure)**: State and connection layer — authentication, context persistence, low-level API.
44
44
 
45
- ## Installation & Development
45
+ ## Development
46
46
 
47
- ### Full build (all layers)
47
+ `chili` lives in the [`mise` monorepo](https://github.com/FNNDSC/mise) alongside
48
+ `cumin`, `salsa`, and `chell`. Build the whole stack from the repository root —
49
+ npm workspaces links the four packages together, so there is nothing to clone or
50
+ hand-link:
48
51
 
49
52
  ```bash
50
- cd chili
51
- make meal
53
+ git clone https://github.com/FNNDSC/mise
54
+ cd mise
55
+ make taco # scrub → prep → cook → taste → serve (the full course)
52
56
  ```
53
57
 
54
- ### Individual steps
58
+ ### Individual steps (run from the repo root)
55
59
 
56
60
  | Target | Action |
57
61
  |--------|--------|
58
- | `make shop` | Clone `cumin` and `salsa` if missing |
59
- | `make prep` | `npm install` across all packages |
60
- | `make cook` | Build (compile TypeScript) all packages |
61
- | `make taste` | Run tests |
62
- | `make serve` | Link packages globally |
62
+ | `make prep` | `npm install` — install deps and link all four workspaces |
63
+ | `make cook` | Build (compile TypeScript) all packages in dependency order |
64
+ | `make taste` | Run the full test suite |
65
+ | `make serve` | Link `chell` globally |
63
66
  | `make scrub` | Clean build artifacts and `node_modules` |
64
67
 
65
- > Use [NVM](https://github.com/nvm-sh/nvm) to avoid needing `sudo` for global links.
68
+ To build or test just this package: `npm run build -w @fnndsc/chili` /
69
+ `npm test -w @fnndsc/chili`.
70
+
71
+ > Use [NVM](https://github.com/nvm-sh/nvm) and Node 22.x to avoid needing `sudo`
72
+ > for the global link in `make serve`.
66
73
 
67
74
  ## Core Features
68
75
 
@@ -23,7 +23,7 @@ export async function manPage_display(options) {
23
23
  return;
24
24
  }
25
25
  if (options.browser) {
26
- browser_open(docPath);
26
+ await browser_open(docPath);
27
27
  }
28
28
  else {
29
29
  const content = fs.readFileSync(docPath, "utf-8");
@@ -11,6 +11,9 @@ import { marked } from "marked";
11
11
  import TerminalRenderer from "marked-terminal";
12
12
  import chalk from "chalk";
13
13
  marked.setOptions({
14
+ // @ts-expect-error marked-terminal@6 ships .d.ts that drift from marked@9's
15
+ // Renderer type at this runtime-compatible version pair. (Tracked: align the
16
+ // marked / marked-terminal versions across chili & chell.)
14
17
  renderer: new TerminalRenderer({
15
18
  code: chalk.yellow,
16
19
  blockquote: chalk.gray.italic,
@@ -25,11 +25,6 @@ export declare class FileGroupHandler {
25
25
  * @returns A Promise resolving to a new FileGroupHandler instance.
26
26
  */
27
27
  static handler_create(assetName: string, path?: string): Promise<FileGroupHandler>;
28
- /**
29
- * Removes duplicate column headers from FilteredResourceData results.
30
- * Copied from BaseGroupHandler to support local listing logic.
31
- */
32
- private columns_removeDuplicates;
33
28
  /**
34
29
  * Lists files, links, or directories using the new command logic.
35
30
  */
@@ -4,6 +4,7 @@ import { path_resolveChrisFs } from "../utils/cli.js";
4
4
  import { FileController } from "../controllers/fileController.js";
5
5
  import { files_create } from "../commands/fs/create.js";
6
6
  import { errorStack } from "@fnndsc/cumin";
7
+ import { resourceColumns_removeDuplicates } from "../utils/resourceData.js";
7
8
  import { table_display } from "../screen/screen.js";
8
9
  import { files_fetchList } from "../commands/files/list.js";
9
10
  import { fileFields_fetch } from "../commands/files/fields.js";
@@ -41,24 +42,6 @@ export class FileGroupHandler {
41
42
  throw new Error(`Failed to initialize FileGroupHandler for ${assetName}: ${errorMessage}`);
42
43
  }
43
44
  }
44
- /**
45
- * Removes duplicate column headers from FilteredResourceData results.
46
- * Copied from BaseGroupHandler to support local listing logic.
47
- */
48
- columns_removeDuplicates(results) {
49
- const uniqueHeaders = Array.from(new Set(results.selectedFields));
50
- const uniqueTableData = results.tableData.map((row) => uniqueHeaders.reduce((acc, header) => {
51
- if (typeof header === "string" && header in row) {
52
- acc[header] = row[header];
53
- }
54
- return acc;
55
- }, {}));
56
- return {
57
- ...results,
58
- selectedFields: uniqueHeaders,
59
- tableData: uniqueTableData,
60
- };
61
- }
62
45
  /**
63
46
  * Lists files, links, or directories using the new command logic.
64
47
  */
@@ -73,7 +56,7 @@ export class FileGroupHandler {
73
56
  console.log(`No ${this.assetName} found matching the criteria.`);
74
57
  }
75
58
  else {
76
- const uniqueResults = this.columns_removeDuplicates(results);
59
+ const uniqueResults = resourceColumns_removeDuplicates(results);
77
60
  // cumin returns dynamic table rows (Record<string, unknown>[]);
78
61
  // narrow to the FileResource view model at this boundary.
79
62
  console.log(fileList_render(uniqueResults.tableData, uniqueResults.selectedFields, { table: options.table, csv: options.csv }));
@@ -16,13 +16,6 @@ export declare class BaseGroupHandler {
16
16
  displayOptions: TableOptions;
17
17
  chrisObject: ChRISPluginGroup | ChRISFeedGroup | ChRISEmbeddedResourceGroup<unknown> | ChRISPACSGroup;
18
18
  constructor(assetName: string, chrisObject: ChRISPluginGroup | ChRISFeedGroup | ChRISEmbeddedResourceGroup<unknown> | ChRISPACSGroup);
19
- /**
20
- * Removes duplicate column headers from FilteredResourceData results.
21
- *
22
- * @param results - The FilteredResourceData to process.
23
- * @returns FilteredResourceData with unique headers and corresponding data.
24
- */
25
- private columns_removeDuplicates;
26
19
  /**
27
20
  * Lists ChRIS resources based on provided CLI options.
28
21
  *
@@ -6,6 +6,7 @@
6
6
  import { Command } from "commander";
7
7
  import { ChRISFeedGroup, record_extract, errorStack, } from "@fnndsc/cumin";
8
8
  import { options_toParams } from "../utils/cli.js";
9
+ import { resourceColumns_removeDuplicates } from "../utils/resourceData.js";
9
10
  import { table_display, border_draw } from "../screen/screen.js";
10
11
  import * as readline from "readline";
11
12
  /**
@@ -21,26 +22,6 @@ export class BaseGroupHandler {
21
22
  title: { title: this.assetName, justification: "center" },
22
23
  };
23
24
  }
24
- /**
25
- * Removes duplicate column headers from FilteredResourceData results.
26
- *
27
- * @param results - The FilteredResourceData to process.
28
- * @returns FilteredResourceData with unique headers and corresponding data.
29
- */
30
- columns_removeDuplicates(results) {
31
- const uniqueHeaders = Array.from(new Set(results.selectedFields));
32
- const uniqueTableData = results.tableData.map((row) => uniqueHeaders.reduce((acc, header) => {
33
- if (typeof header === "string" && header in row) {
34
- acc[header] = row[header];
35
- }
36
- return acc;
37
- }, {}));
38
- return {
39
- ...results,
40
- selectedFields: uniqueHeaders,
41
- tableData: uniqueTableData,
42
- };
43
- }
44
25
  /**
45
26
  * Lists ChRIS resources based on provided CLI options.
46
27
  *
@@ -77,7 +58,7 @@ export class BaseGroupHandler {
77
58
  console.log(`No ${this.assetName} found matching the criteria.`);
78
59
  }
79
60
  else {
80
- const uniqueResults = this.columns_removeDuplicates(results);
61
+ const uniqueResults = resourceColumns_removeDuplicates(results);
81
62
  // Construct column options based on parsed widths
82
63
  const columns = uniqueResults.selectedFields.map(field => {
83
64
  const width = columnWidths[field];
package/dist/index.d.ts CHANGED
@@ -2,6 +2,11 @@
2
2
  /**
3
3
  * @file Command-line entry point for the chili (ChILI) CLI.
4
4
  *
5
+ * This is a thin wrapper: all command wiring and execution live in {@link run}
6
+ * (./run.js) so the same logic can be driven in-process by a host such as chell
7
+ * without spawning a separate `node` subprocess. This file only adds the
8
+ * bin-only concerns — shell completion and fatal-exit handling.
9
+ *
5
10
  * @module
6
11
  */
7
- export { logical_toPhysical } from './utils/cli.js';
12
+ export { logical_toPhysical } from "./utils/cli.js";
package/dist/index.js CHANGED
@@ -2,62 +2,18 @@
2
2
  /**
3
3
  * @file Command-line entry point for the chili (ChILI) CLI.
4
4
  *
5
- * @module
6
- */
7
- /**
8
- * Suppress DEP0169 warning from axios/proxy-from-env dependency.
9
- *
10
- * This warning originates from the `proxy-from-env` package (version 1.1.0)
11
- * used by axios for proxy configuration. The package uses the legacy `url.parse()`
12
- * API which is deprecated in favor of the WHATWG URL API.
5
+ * This is a thin wrapper: all command wiring and execution live in {@link run}
6
+ * (./run.js) so the same logic can be driven in-process by a host such as chell
7
+ * without spawning a separate `node` subprocess. This file only adds the
8
+ * bin-only concerns shell completion and fatal-exit handling.
13
9
  *
14
- * Why we suppress it:
15
- * - It's a transitive dependency (axios → proxy-from-env) we don't control
16
- * - proxy-from-env 1.1.0 is the latest version and hasn't been updated
17
- * - According to the warning: "CVEs are not issued for url.parse() vulnerabilities"
18
- * - The warning doesn't affect functionality, only console output
19
- * - This is a well-known issue in the Node.js ecosystem
20
- *
21
- * This can be removed once:
22
- * - proxy-from-env migrates to WHATWG URL API, OR
23
- * - axios replaces proxy-from-env with an alternative
24
- *
25
- * Tracking: https://github.com/Rob--W/proxy-from-env/issues/51
10
+ * @module
26
11
  */
27
- const originalEmitWarning = process.emitWarning;
28
- process.emitWarning = function (warning, ...args) {
29
- // Suppress only the specific DEP0169 warning
30
- if (typeof warning === 'string' &&
31
- (warning.includes('DEP0169') || warning.includes('url.parse()'))) {
32
- return;
33
- }
34
- // Allow all other warnings through
35
- return originalEmitWarning.call(process, warning, ...args);
36
- };
37
- import { Command } from "commander";
38
- import figlet from "figlet";
39
12
  import omelette from "omelette";
40
- import { connectCommand_setup } from "./connect/connectHandler.js";
41
- import { FeedGroupHandler, FeedMemberHandler } from "./feeds/feedHandler.js";
42
- import { PluginGroupHandler, PluginMemberHandler, } from "./plugins/pluginHandler.js";
43
- import { PluginMetaGroupHandler } from "./plugins/pluginMetaHandler.js";
44
- import { PluginContextGroupHandler } from "./plugins/pluginGroupHandler.js";
45
- import { inodeCommand_setup } from "./filesystem/inodeCommand.js";
46
- import { contextCommand_setup } from "./context/contextCommand.js";
47
- import { pathCommand_setup } from "./path/pathCommand.js";
48
- import { fileBrowserCommand_setup } from "./filesystem/filesystemHandler.js";
49
- import { manCommand_setup } from "./man/man.js";
50
- import { chefsCommand_setup } from "./chefs/chefs.js";
51
- import { PACSServerGroupHandler } from "./pacs/pacsServerHandler.js";
52
- import { PACSQueryGroupHandler } from "./pacs/pacsQueryHandler.js";
53
- import { PACSRetrieveGroupHandler } from "./pacs/pacsRetrieveHandler.js";
54
- import { chrisConnection_init, NodeStorageProvider, errorStack_getAllOfType } from "@fnndsc/cumin";
55
- import { FileGroupHandler } from "./filesystem/fileGroupHandler.js";
56
- const program = new Command();
57
- program.version("1.0.1").description("A CLI for ChRIS");
13
+ import { run } from "./run.js";
58
14
  /**
59
- * Sets up command completion for the ChILI CLI using omelette.
60
- * This provides auto-completion suggestions in the shell.
15
+ * Sets up shell command completion for the standalone chili CLI using omelette.
16
+ * This is a bin-only concern; in-process hosts provide their own completion.
61
17
  */
62
18
  function commandCompletion_setup() {
63
19
  const completion = omelette(`chili|chili`);
@@ -95,7 +51,6 @@ function commandCompletion_setup() {
95
51
  });
96
52
  completion.on("man.doc", ({ reply }) => {
97
53
  console.log("Autocomplete triggered for man doc command");
98
- // We'll implement actual topic retrieval here later
99
54
  reply([]);
100
55
  });
101
56
  completion.init();
@@ -104,136 +59,15 @@ function commandCompletion_setup() {
104
59
  }
105
60
  }
106
61
  /**
107
- * Initializes and sets up all command handlers for the ChILI CLI.
108
- *
109
- * @param program - The Commander.js program instance.
110
- */
111
- async function handlers_initialize() {
112
- // const client = await chrisConnection.client_get();
113
- // We don't enforce connection here to allow --help to work.
114
- // Commands will fail individually if not connected.
115
- fileBrowserCommand_setup(program);
116
- contextCommand_setup(program);
117
- pathCommand_setup(program);
118
- manCommand_setup(program);
119
- chefsCommand_setup(program);
120
- const pacsServerHandler = new PACSServerGroupHandler();
121
- pacsServerHandler.pacsServerCommand_setup(program);
122
- const pacsQueryHandler = new PACSQueryGroupHandler();
123
- pacsQueryHandler.pacsQueryCommand_setup(program);
124
- const pacsRetrieveHandler = new PACSRetrieveGroupHandler();
125
- pacsRetrieveHandler.pacsRetrieveCommand_setup(program);
126
- await inodeCommand_setup(program);
127
- const pluginGroupHandler = new PluginGroupHandler();
128
- pluginGroupHandler.pluginGroupCommand_setup(program);
129
- const pluginMetaGroupHandler = new PluginMetaGroupHandler();
130
- pluginMetaGroupHandler.pluginMetaGroupCommand_setup(program);
131
- const feedGroupHandler = new FeedGroupHandler();
132
- feedGroupHandler.feedGroupCommand_setup(program);
133
- const feedMemberHandler = new FeedMemberHandler();
134
- feedMemberHandler.feedCommand_setup(program);
135
- const pluginMemberHandler = new PluginMemberHandler();
136
- pluginMemberHandler.pluginCommand_setup(program);
137
- try {
138
- const filesGroupHandler = await FileGroupHandler.handler_create("files");
139
- filesGroupHandler.fileGroupCommand_setup(program);
140
- const linksGroupHandler = await FileGroupHandler.handler_create("links");
141
- linksGroupHandler.fileGroupCommand_setup(program);
142
- const dirsGroupHandler = await FileGroupHandler.handler_create("dirs");
143
- dirsGroupHandler.fileGroupCommand_setup(program);
144
- }
145
- catch (e) {
146
- const err = e instanceof Error ? e.message : String(e);
147
- const errors = errorStack_getAllOfType("error");
148
- const warnings = errorStack_getAllOfType("warning");
149
- console.log(`Note: File group commands (files, dirs, links) are unavailable. Reason: ${err}`);
150
- if (errors.length > 0) {
151
- console.log("Errors:");
152
- errors.forEach(msg => console.log(` - ${msg}`));
153
- }
154
- if (warnings.length > 0) {
155
- console.log("Warnings:");
156
- warnings.forEach(msg => console.log(` - ${msg}`));
157
- }
158
- }
159
- try {
160
- const computesOfPluginHandler = await PluginContextGroupHandler.handler_create("computesofplugin");
161
- computesOfPluginHandler.pluginContextGroupCommand_setup(program);
162
- const pluginInstancesHandler = await PluginContextGroupHandler.handler_create("instancesofplugin");
163
- pluginInstancesHandler.pluginContextGroupCommand_setup(program);
164
- const pluginParametersHandler = await PluginContextGroupHandler.handler_create("parametersofplugin");
165
- pluginParametersHandler.pluginContextGroupCommand_setup(program);
166
- }
167
- catch (e) {
168
- const err = e instanceof Error ? e.message : String(e);
169
- const errors = errorStack_getAllOfType("error");
170
- const warnings = errorStack_getAllOfType("warning");
171
- console.log(`Note: Plugin context commands are unavailable. Reason: ${err}`);
172
- if (errors.length > 0) {
173
- console.log("Errors:");
174
- errors.forEach(msg => console.log(` - ${msg}`));
175
- }
176
- if (warnings.length > 0) {
177
- console.log("Warnings:");
178
- warnings.forEach(msg => console.log(` - ${msg}`));
179
- }
180
- }
181
- commandCompletion_setup();
182
- }
183
- /**
184
- * Parses the command line arguments to extract context information.
185
- *
186
- * @param args - The command line arguments array (process.argv).
187
- * @returns A tuple containing the context string (if found) and the remaining arguments.
188
- */
189
- function context_parse(args) {
190
- // Find the first argument that looks like a context string (contains "=" but doesn't start with "-")
191
- for (let i = 2; i < args.length; i++) {
192
- const arg = args[i];
193
- if (arg.includes("=") && !arg.startsWith("-")) {
194
- const context = arg;
195
- // Reconstruct args without the context string
196
- const newArgs = [...args.slice(0, i), ...args.slice(i + 1)];
197
- return [context, newArgs];
198
- }
199
- }
200
- return [undefined, args];
201
- }
202
- /**
203
- * Main function to run the ChILI CLI.
204
- * Handles initialization, context parsing, and command execution.
62
+ * Bin entry: register shell completion, then run the requested chili command.
205
63
  */
206
64
  async function main() {
207
- const nodeStorageProvider = new NodeStorageProvider();
208
- const connection = await chrisConnection_init(nodeStorageProvider);
209
- const [context, newArgs] = context_parse(process.argv);
210
- if (context) {
211
- const contextSetSuccess = await connection.context_set(context);
212
- if (!contextSetSuccess) {
213
- console.error("Failed to set context. Exiting.");
214
- process.exit(1);
215
- }
216
- process.argv = newArgs;
217
- }
218
- program
219
- .name("chili")
220
- .description("ChILI handles Intelligent Line Interactions")
221
- .option("-v, --verbose", "enable verbose output")
222
- .option("-c, --config <path>", "path to config file")
223
- .option("-s, --nosplash", "disable splash screen");
224
- connectCommand_setup(program);
225
- await handlers_initialize(); // Call handlers_initialize here
226
- program.parseOptions(process.argv);
227
- const options = program.opts();
228
- if (!options.nosplash) {
229
- console.log(figlet.textSync("ChILI"));
230
- console.log("ChILI handles Intelligent Line Interactions");
231
- }
232
- program.parse(process.argv);
65
+ commandCompletion_setup();
66
+ await run(process.argv.slice(2));
233
67
  }
234
68
  main().catch((error) => {
235
69
  console.error("An error occurred:", error);
236
70
  process.exit(1);
237
71
  });
238
- // Export utilities for use by other packages (e.g., chell)
239
- export { logical_toPhysical } from './utils/cli.js';
72
+ // Re-export library utilities for consumers that import the package root.
73
+ export { logical_toPhysical } from "./utils/cli.js";
@@ -23,4 +23,4 @@ export declare function asciidoc_render(content: string, style: "figlet" | "asci
23
23
  *
24
24
  * @param filePath - The path to the file to open.
25
25
  */
26
- export declare function browser_open(filePath: string): void;
26
+ export declare function browser_open(filePath: string): Promise<void>;
@@ -6,7 +6,7 @@
6
6
  import fs from "fs";
7
7
  import path from "path";
8
8
  import chalk from "chalk";
9
- import asciidoctor from "asciidoctor";
9
+ import { convert as adoc_convert } from "asciidoctor";
10
10
  import { exec } from "child_process";
11
11
  import os from "os";
12
12
  const headingStyles = [
@@ -63,9 +63,8 @@ export function projectDir_get() {
63
63
  * @param content - The AsciiDoc content string.
64
64
  * @returns The converted HTML string.
65
65
  */
66
- function adoc_htmlConvert(content) {
67
- const ascii = asciidoctor();
68
- let result = ascii.convert(content, {
66
+ async function adoc_htmlConvert(content) {
67
+ let result = await adoc_convert(content, {
69
68
  standalone: false,
70
69
  attributes: {
71
70
  showtitle: "",
@@ -107,7 +106,7 @@ function text_wrap(text, width) {
107
106
  * @returns A Promise resolving to the formatted string.
108
107
  */
109
108
  export async function asciidoc_render(content, style, width) {
110
- let result = adoc_htmlConvert(content);
109
+ let result = await adoc_htmlConvert(content);
111
110
  function ASCII_create(text, style) {
112
111
  const transformedText = style.textTransform(text);
113
112
  return style.color(transformedText);
@@ -159,12 +158,11 @@ export async function asciidoc_render(content, style, width) {
159
158
  *
160
159
  * @param filePath - The path to the file to open.
161
160
  */
162
- export function browser_open(filePath) {
163
- const ascii = asciidoctor();
161
+ export async function browser_open(filePath) {
164
162
  const tempHtmlPath = path.join(os.tmpdir(), path.basename(filePath).replace(".adoc", ".html"));
165
163
  try {
166
164
  const content = fs.readFileSync(filePath, "utf-8");
167
- const html = ascii.convert(content, {
165
+ const html = await adoc_convert(content, {
168
166
  safe: "safe",
169
167
  standalone: true,
170
168
  attributes: { showtitle: true },
@@ -13,7 +13,7 @@ import { table_display, border_draw } from "../screen/screen.js";
13
13
  import { options_toParams } from "../utils/cli.js";
14
14
  import archy from "archy";
15
15
  import open from "open";
16
- import { exec } from "child_process";
16
+ import { execFile } from "child_process";
17
17
  import os from "os";
18
18
  import { files_downloadWithProgress } from "../commands/fs/download.js";
19
19
  /**
@@ -160,10 +160,13 @@ async function mermaid_renderServerSide(mermaidDefinition, outputFile) {
160
160
  const tempDir = os.tmpdir();
161
161
  const inputFile = path.join(tempDir, "input.mmd");
162
162
  fs.writeFileSync(inputFile, mermaidDefinition);
163
+ // Pass args as an array (execFile, no shell) so a user-supplied --save path
164
+ // cannot inject shell commands. Requires `mmdc` (@mermaid-js/mermaid-cli),
165
+ // which is fetched on demand via npx and is not a hard dependency.
163
166
  return new Promise((resolve, reject) => {
164
- exec(`npx mmdc -i ${inputFile} -o ${outputFile}`, (error, stdout, stderr) => {
167
+ execFile("npx", ["mmdc", "-i", inputFile, "-o", outputFile], (error) => {
165
168
  if (error) {
166
- console.error(`exec error: ${error}`);
169
+ console.error(`Mermaid render failed. Ensure @mermaid-js/mermaid-cli is available (npx mmdc): ${error.message}`);
167
170
  reject(error);
168
171
  return;
169
172
  }
package/dist/run.d.ts ADDED
@@ -0,0 +1,24 @@
1
+ /**
2
+ * @file In-process library entry for the chili (ChILI) CLI.
3
+ *
4
+ * Exposes {@link run}, which builds a fresh Commander program and executes a
5
+ * single chili command, returning when it completes. Unlike the bin entry
6
+ * (`index.ts`), importing this module has no side effect of running the CLI and
7
+ * never calls `process.exit`, so a host process such as chell can drive chili
8
+ * in-process instead of spawning a separate `node` subprocess.
9
+ *
10
+ * @module
11
+ */
12
+ /**
13
+ * Executes a single chili command in the current process.
14
+ *
15
+ * Builds a fresh Commander program (so repeated calls do not share parser
16
+ * state), wires all command handlers, and parses the supplied arguments. Help,
17
+ * version and usage errors are handled gracefully (Commander's message is
18
+ * already written) instead of terminating the process; genuine errors thrown by
19
+ * a command action propagate to the caller.
20
+ *
21
+ * @param argv - The chili arguments, e.g. `["feeds", "list", "-s"]` (without
22
+ * the leading `node`/script entries).
23
+ */
24
+ export declare function run(argv: string[]): Promise<void>;
package/dist/run.js ADDED
@@ -0,0 +1,192 @@
1
+ /**
2
+ * @file In-process library entry for the chili (ChILI) CLI.
3
+ *
4
+ * Exposes {@link run}, which builds a fresh Commander program and executes a
5
+ * single chili command, returning when it completes. Unlike the bin entry
6
+ * (`index.ts`), importing this module has no side effect of running the CLI and
7
+ * never calls `process.exit`, so a host process such as chell can drive chili
8
+ * in-process instead of spawning a separate `node` subprocess.
9
+ *
10
+ * @module
11
+ */
12
+ import { Command, CommanderError } from "commander";
13
+ import figlet from "figlet";
14
+ import { connectCommand_setup } from "./connect/connectHandler.js";
15
+ import { FeedGroupHandler, FeedMemberHandler } from "./feeds/feedHandler.js";
16
+ import { PluginGroupHandler, PluginMemberHandler, } from "./plugins/pluginHandler.js";
17
+ import { PluginMetaGroupHandler } from "./plugins/pluginMetaHandler.js";
18
+ import { PluginContextGroupHandler } from "./plugins/pluginGroupHandler.js";
19
+ import { inodeCommand_setup } from "./filesystem/inodeCommand.js";
20
+ import { contextCommand_setup } from "./context/contextCommand.js";
21
+ import { pathCommand_setup } from "./path/pathCommand.js";
22
+ import { fileBrowserCommand_setup } from "./filesystem/filesystemHandler.js";
23
+ import { manCommand_setup } from "./man/man.js";
24
+ import { chefsCommand_setup } from "./chefs/chefs.js";
25
+ import { PACSServerGroupHandler } from "./pacs/pacsServerHandler.js";
26
+ import { PACSQueryGroupHandler } from "./pacs/pacsQueryHandler.js";
27
+ import { PACSRetrieveGroupHandler } from "./pacs/pacsRetrieveHandler.js";
28
+ import { chrisConnection_init, NodeStorageProvider, errorStack_getAllOfType, } from "@fnndsc/cumin";
29
+ import { FileGroupHandler } from "./filesystem/fileGroupHandler.js";
30
+ /**
31
+ * Suppress the DEP0169 (`url.parse()`) warning emitted transitively by
32
+ * axios -> proxy-from-env, which we do not control. Installed once at import.
33
+ * See the tracking note in the original bin entry for context.
34
+ */
35
+ const originalEmitWarning = process.emitWarning;
36
+ process.emitWarning = function (warning, ...args) {
37
+ if (typeof warning === "string" &&
38
+ (warning.includes("DEP0169") || warning.includes("url.parse()"))) {
39
+ return;
40
+ }
41
+ return originalEmitWarning.call(process, warning, ...args);
42
+ };
43
+ /**
44
+ * Registers every chili command handler on the supplied program. The handler
45
+ * setup functions are program-parameterised, so a fresh program can be wired on
46
+ * each call without sharing Commander state across invocations.
47
+ *
48
+ * @param program - The Commander program to register commands on.
49
+ */
50
+ async function handlers_initialize(program) {
51
+ // Connection is intentionally not enforced here so that `--help` works; each
52
+ // command fails individually if it needs a connection that is not present.
53
+ fileBrowserCommand_setup(program);
54
+ contextCommand_setup(program);
55
+ pathCommand_setup(program);
56
+ manCommand_setup(program);
57
+ chefsCommand_setup(program);
58
+ const pacsServerHandler = new PACSServerGroupHandler();
59
+ pacsServerHandler.pacsServerCommand_setup(program);
60
+ const pacsQueryHandler = new PACSQueryGroupHandler();
61
+ pacsQueryHandler.pacsQueryCommand_setup(program);
62
+ const pacsRetrieveHandler = new PACSRetrieveGroupHandler();
63
+ pacsRetrieveHandler.pacsRetrieveCommand_setup(program);
64
+ await inodeCommand_setup(program);
65
+ const pluginGroupHandler = new PluginGroupHandler();
66
+ pluginGroupHandler.pluginGroupCommand_setup(program);
67
+ const pluginMetaGroupHandler = new PluginMetaGroupHandler();
68
+ pluginMetaGroupHandler.pluginMetaGroupCommand_setup(program);
69
+ const feedGroupHandler = new FeedGroupHandler();
70
+ feedGroupHandler.feedGroupCommand_setup(program);
71
+ const feedMemberHandler = new FeedMemberHandler();
72
+ feedMemberHandler.feedCommand_setup(program);
73
+ const pluginMemberHandler = new PluginMemberHandler();
74
+ pluginMemberHandler.pluginCommand_setup(program);
75
+ try {
76
+ const filesGroupHandler = await FileGroupHandler.handler_create("files");
77
+ filesGroupHandler.fileGroupCommand_setup(program);
78
+ const linksGroupHandler = await FileGroupHandler.handler_create("links");
79
+ linksGroupHandler.fileGroupCommand_setup(program);
80
+ const dirsGroupHandler = await FileGroupHandler.handler_create("dirs");
81
+ dirsGroupHandler.fileGroupCommand_setup(program);
82
+ }
83
+ catch (e) {
84
+ const err = e instanceof Error ? e.message : String(e);
85
+ const errors = errorStack_getAllOfType("error");
86
+ const warnings = errorStack_getAllOfType("warning");
87
+ console.log(`Note: File group commands (files, dirs, links) are unavailable. Reason: ${err}`);
88
+ if (errors.length > 0) {
89
+ console.log("Errors:");
90
+ errors.forEach((msg) => console.log(` - ${msg}`));
91
+ }
92
+ if (warnings.length > 0) {
93
+ console.log("Warnings:");
94
+ warnings.forEach((msg) => console.log(` - ${msg}`));
95
+ }
96
+ }
97
+ try {
98
+ const computesOfPluginHandler = await PluginContextGroupHandler.handler_create("computesofplugin");
99
+ computesOfPluginHandler.pluginContextGroupCommand_setup(program);
100
+ const pluginInstancesHandler = await PluginContextGroupHandler.handler_create("instancesofplugin");
101
+ pluginInstancesHandler.pluginContextGroupCommand_setup(program);
102
+ const pluginParametersHandler = await PluginContextGroupHandler.handler_create("parametersofplugin");
103
+ pluginParametersHandler.pluginContextGroupCommand_setup(program);
104
+ }
105
+ catch (e) {
106
+ const err = e instanceof Error ? e.message : String(e);
107
+ const errors = errorStack_getAllOfType("error");
108
+ const warnings = errorStack_getAllOfType("warning");
109
+ console.log(`Note: Plugin context commands are unavailable. Reason: ${err}`);
110
+ if (errors.length > 0) {
111
+ console.log("Errors:");
112
+ errors.forEach((msg) => console.log(` - ${msg}`));
113
+ }
114
+ if (warnings.length > 0) {
115
+ console.log("Warnings:");
116
+ warnings.forEach((msg) => console.log(` - ${msg}`));
117
+ }
118
+ }
119
+ }
120
+ /**
121
+ * Extracts a leading `key=value` context string from an argv array.
122
+ *
123
+ * @param args - An argv array shaped like `process.argv` (exec, script, ...).
124
+ * @returns A tuple of the context string (if any) and argv with it removed.
125
+ */
126
+ function context_parse(args) {
127
+ for (let i = 2; i < args.length; i++) {
128
+ const arg = args[i];
129
+ if (arg.includes("=") && !arg.startsWith("-")) {
130
+ const context = arg;
131
+ const newArgs = [...args.slice(0, i), ...args.slice(i + 1)];
132
+ return [context, newArgs];
133
+ }
134
+ }
135
+ return [undefined, args];
136
+ }
137
+ /**
138
+ * Executes a single chili command in the current process.
139
+ *
140
+ * Builds a fresh Commander program (so repeated calls do not share parser
141
+ * state), wires all command handlers, and parses the supplied arguments. Help,
142
+ * version and usage errors are handled gracefully (Commander's message is
143
+ * already written) instead of terminating the process; genuine errors thrown by
144
+ * a command action propagate to the caller.
145
+ *
146
+ * @param argv - The chili arguments, e.g. `["feeds", "list", "-s"]` (without
147
+ * the leading `node`/script entries).
148
+ */
149
+ export async function run(argv) {
150
+ const nodeStorageProvider = new NodeStorageProvider();
151
+ const connection = await chrisConnection_init(nodeStorageProvider);
152
+ // Commander's default parsing expects argv[0]/argv[1] to be exec/script.
153
+ let fullArgv = ["node", "chili", ...argv];
154
+ const [context, newArgs] = context_parse(fullArgv);
155
+ if (context) {
156
+ const contextSetSuccess = await connection.context_set(context);
157
+ if (!contextSetSuccess) {
158
+ console.error("Failed to set context.");
159
+ return;
160
+ }
161
+ fullArgv = newArgs;
162
+ }
163
+ const program = new Command();
164
+ program
165
+ .name("chili")
166
+ .description("ChILI handles Intelligent Line Interactions")
167
+ .version("1.0.1")
168
+ .option("-v, --verbose", "enable verbose output")
169
+ .option("-c, --config <path>", "path to config file")
170
+ .option("-s, --nosplash", "disable splash screen")
171
+ .exitOverride();
172
+ connectCommand_setup(program);
173
+ await handlers_initialize(program);
174
+ program.parseOptions(fullArgv);
175
+ const options = program.opts();
176
+ if (!options.nosplash) {
177
+ console.log(figlet.textSync("ChILI"));
178
+ console.log("ChILI handles Intelligent Line Interactions");
179
+ }
180
+ try {
181
+ await program.parseAsync(fullArgv);
182
+ }
183
+ catch (err) {
184
+ // Under exitOverride, --help/--version/usage errors throw a CommanderError
185
+ // whose message Commander has already written. Swallow these so the host
186
+ // (e.g. a chell REPL) is not torn down; rethrow anything else.
187
+ if (err instanceof CommanderError) {
188
+ return;
189
+ }
190
+ throw err;
191
+ }
192
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * @file Shared shaping helpers for FilteredResourceData prior to display.
3
+ *
4
+ * @module
5
+ */
6
+ import { FilteredResourceData } from "@fnndsc/cumin";
7
+ /**
8
+ * Removes duplicate column headers from a FilteredResourceData result.
9
+ *
10
+ * Keeps each selected field once (preserving first-seen order) and projects
11
+ * every table row onto that unique header set.
12
+ *
13
+ * @param results - The resource data to de-duplicate.
14
+ * @returns A new FilteredResourceData with unique selectedFields and matching rows.
15
+ */
16
+ export declare function resourceColumns_removeDuplicates(results: FilteredResourceData): FilteredResourceData;
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Removes duplicate column headers from a FilteredResourceData result.
3
+ *
4
+ * Keeps each selected field once (preserving first-seen order) and projects
5
+ * every table row onto that unique header set.
6
+ *
7
+ * @param results - The resource data to de-duplicate.
8
+ * @returns A new FilteredResourceData with unique selectedFields and matching rows.
9
+ */
10
+ export function resourceColumns_removeDuplicates(results) {
11
+ const uniqueHeaders = Array.from(new Set(results.selectedFields));
12
+ const uniqueTableData = results.tableData.map((row) => uniqueHeaders.reduce((acc, header) => {
13
+ if (typeof header === "string" && header in row) {
14
+ acc[header] = row[header];
15
+ }
16
+ return acc;
17
+ }, {}));
18
+ return {
19
+ ...results,
20
+ selectedFields: uniqueHeaders,
21
+ tableData: uniqueTableData,
22
+ };
23
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fnndsc/chili",
3
- "version": "3.2.5",
3
+ "version": "3.3.0",
4
4
  "description": "ChILI handles Intelligent Line Interactions: A CLI and library for ChRIS, acting as the controller layer for business logic and user presentation.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -25,7 +25,7 @@
25
25
  "scripts": {
26
26
  "build": "tsc && chmod +x ./dist/index.js",
27
27
  "start": "node dist/index.js",
28
- "test": "jest",
28
+ "test": "jest --coverage",
29
29
  "test:coverage": "jest --coverage --coverageProvider=v8",
30
30
  "clean": "rm -rf dist types fnndsc-chili*tgz",
31
31
  "build:cumin": "cd ../cumin && npm run build",
@@ -47,14 +47,13 @@
47
47
  "author": "FNNDSC <dev@babyMRI.org>",
48
48
  "license": "MIT",
49
49
  "dependencies": {
50
- "@fnndsc/cumin": "^3.2.4",
51
- "@fnndsc/salsa": "^3.2.4",
50
+ "@fnndsc/cumin": "^3.4.0",
51
+ "@fnndsc/salsa": "^3.2.5",
52
52
  "@fortawesome/fontawesome-free": "^6.6.0",
53
53
  "@fortawesome/fontawesome-svg-core": "^6.6.0",
54
54
  "@fortawesome/free-solid-svg-icons": "^6.6.0",
55
- "@mermaid-js/mermaid-cli": "^11.12.0",
56
55
  "archy": "^1.0.0",
57
- "asciidoctor": "^3.0.4",
56
+ "asciidoctor": "^4.0.1",
58
57
  "axios": "^1.6.0",
59
58
  "chalk": "^4.1.2",
60
59
  "cli-highlight": "^2.1.11",
@@ -65,7 +64,6 @@
65
64
  "js-yaml": "^4.1.0",
66
65
  "marked": "^9.1.0",
67
66
  "marked-terminal": "^6.1.0",
68
- "node-fetch": "^3.3.2",
69
67
  "omelette": "^0.4.17",
70
68
  "open": "^10.1.0",
71
69
  "table": "^6.8.1"
@@ -77,7 +75,7 @@
77
75
  "@types/jest": "^30.0.0",
78
76
  "@types/js-yaml": "^4.0.5",
79
77
  "@types/marked-terminal": "^6.1.1",
80
- "@types/node": "^24.10.1",
78
+ "@types/node": "^22.0.0",
81
79
  "@types/node-fetch": "^2.6.11",
82
80
  "@types/omelette": "^0.4.4",
83
81
  "jest": "^30.2.0",