@fnndsc/chili 3.3.0 → 3.5.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.
Files changed (40) hide show
  1. package/dist/chefs/chefs.js +12 -11
  2. package/dist/commands/fs/download.d.ts +19 -4
  3. package/dist/commands/fs/download.js +75 -65
  4. package/dist/commands/fs/upload.d.ts +20 -4
  5. package/dist/commands/fs/upload.js +46 -34
  6. package/dist/commands/man/doc.js +3 -2
  7. package/dist/commands/plugins/add.js +35 -34
  8. package/dist/config/colorConfig.js +2 -1
  9. package/dist/connect/connectHandler.js +7 -6
  10. package/dist/context/contextCommand.js +3 -2
  11. package/dist/controllers/fileController.js +4 -3
  12. package/dist/controllers/pluginController.js +2 -1
  13. package/dist/feeds/feedHandler.js +19 -18
  14. package/dist/filesystem/fileGroupHandler.js +28 -27
  15. package/dist/filesystem/filesystemHandler.js +13 -11
  16. package/dist/filesystem/inodeCommand.js +2 -1
  17. package/dist/handlers/baseGroupHandler.d.ts +10 -1
  18. package/dist/handlers/baseGroupHandler.js +38 -13
  19. package/dist/index.js +3 -2
  20. package/dist/lfs/lfs.js +6 -4
  21. package/dist/man/man.js +4 -3
  22. package/dist/man/renderer.js +3 -2
  23. package/dist/pacs/pacsQueryHandler.js +14 -13
  24. package/dist/pacs/pacsRetrieveHandler.js +13 -12
  25. package/dist/path/pathCommand.js +23 -22
  26. package/dist/plugins/pluginGroupHandler.d.ts +18 -0
  27. package/dist/plugins/pluginGroupHandler.js +44 -4
  28. package/dist/plugins/pluginHandler.js +23 -22
  29. package/dist/run.d.ts +13 -0
  30. package/dist/run.js +28 -13
  31. package/dist/screen/output.d.ts +88 -0
  32. package/dist/screen/output.js +95 -0
  33. package/dist/screen/screen.d.ts +13 -0
  34. package/dist/screen/screen.js +53 -8
  35. package/dist/utils/admin_prompt.js +10 -9
  36. package/dist/utils/docker.js +10 -9
  37. package/dist/views/pluginParameters.d.ts +10 -0
  38. package/dist/views/pluginParameters.js +25 -17
  39. package/docs/pacs.adoc +3 -3
  40. package/package.json +1 -1
@@ -16,6 +16,7 @@ import open from "open";
16
16
  import { execFile } from "child_process";
17
17
  import os from "os";
18
18
  import { files_downloadWithProgress } from "../commands/fs/download.js";
19
+ import { chiliErrLog, chiliLog } from "../screen/output.js";
19
20
  /**
20
21
  * Creates a summary table for file transfer details.
21
22
  *
@@ -166,7 +167,7 @@ async function mermaid_renderServerSide(mermaidDefinition, outputFile) {
166
167
  return new Promise((resolve, reject) => {
167
168
  execFile("npx", ["mmdc", "-i", inputFile, "-o", outputFile], (error) => {
168
169
  if (error) {
169
- console.error(`Mermaid render failed. Ensure @mermaid-js/mermaid-cli is available (npx mmdc): ${error.message}`);
170
+ chiliErrLog(`Mermaid render failed. Ensure @mermaid-js/mermaid-cli is available (npx mmdc): ${error.message}`);
170
171
  reject(error);
171
172
  return;
172
173
  }
@@ -412,10 +413,10 @@ async function scanResult_render(scanResult, options) {
412
413
  try {
413
414
  const outputFile = path.resolve(options.save);
414
415
  const savedFilePath = await mermaid_renderServerSide(mermaidDefinition, outputFile);
415
- console.log(`Mermaid diagram saved to: ${savedFilePath}`);
416
+ chiliLog(`Mermaid diagram saved to: ${savedFilePath}`);
416
417
  }
417
418
  catch (error) {
418
- console.error(`Failed to save Mermaid diagram: ${error}`);
419
+ chiliErrLog(`Failed to save Mermaid diagram: ${error}`);
419
420
  }
420
421
  }
421
422
  else {
@@ -424,16 +425,16 @@ async function scanResult_render(scanResult, options) {
424
425
  return;
425
426
  }
426
427
  if (options.tree) {
427
- console.log(archyTree_create(scanResult.fileInfo));
428
+ chiliLog(archyTree_create(scanResult.fileInfo));
428
429
  return;
429
430
  }
430
431
  if (!options.silent) {
431
432
  for (const file of scanResult.fileInfo) {
432
433
  if (file.isLink && !options.follow) {
433
- console.log(`${file.chrisPath} -> ${file.linkTarget}`);
434
+ chiliLog(`${file.chrisPath} -> ${file.linkTarget}`);
434
435
  }
435
436
  else {
436
- console.log(`${file.chrisPath}`);
437
+ chiliLog(`${file.chrisPath}`);
437
438
  }
438
439
  }
439
440
  }
@@ -447,24 +448,24 @@ async function scanResult_render(scanResult, options) {
447
448
  export async function scan_do(options) {
448
449
  const chrisFolder = await chrisContext.current_get(Context.ChRISfolder);
449
450
  if (!chrisFolder) {
450
- console.error(chalk.red("No ChRIS folder context set. Use 'folder=' to set a context."));
451
+ chiliErrLog(chalk.red("No ChRIS folder context set. Use 'folder=' to set a context."));
451
452
  return null;
452
453
  }
453
454
  if (!options.silent) {
454
- console.log(chalk.cyan(`Scanning for ${options.dirsOnly ? "directories" : "all files"} recursively from ${chrisFolder}`));
455
+ chiliLog(chalk.cyan(`Scanning for ${options.dirsOnly ? "directories" : "all files"} recursively from ${chrisFolder}`));
455
456
  }
456
457
  const hostBasePath = options.hostpath || process.cwd();
457
458
  const scanResult = await chrisFS_scan(chrisFolder, hostBasePath, options.follow, options.dirsOnly);
458
459
  if (!scanResult) {
459
- console.error(chalk.red("Failed to scan ChRIS filesystem."));
460
+ chiliErrLog(chalk.red("Failed to scan ChRIS filesystem."));
460
461
  return null;
461
462
  }
462
463
  const filtered = scanResult_filter(scanResult, options);
463
464
  await scanResult_render(filtered, options);
464
465
  if (!options.silent) {
465
- console.log(chalk.green(`Total size: ${bytes_format(filtered.totalSize)}`));
466
+ chiliLog(chalk.green(`Total size: ${bytes_format(filtered.totalSize)}`));
466
467
  if (options.filter || options.endsWith) {
467
- console.log(chalk.cyan(`Filtered results: ${filtered.fileInfo.length} items`));
468
+ chiliLog(chalk.cyan(`Filtered results: ${filtered.fileInfo.length} items`));
468
469
  }
469
470
  }
470
471
  return filtered;
@@ -517,25 +518,25 @@ async function localFS_scan(options) {
517
518
  await chrisContext.currentContext_update();
518
519
  const folder = chrisContext.singleContext.folder;
519
520
  if (!folder) {
520
- console.error("ChRIS folder context is undefined, cannot initialize chrisIO.");
521
+ chiliErrLog("ChRIS folder context is undefined, cannot initialize chrisIO.");
521
522
  return null;
522
523
  }
523
524
  // Validate that the folder context exists in CUBE before proceeding
524
525
  try {
525
526
  const testGroup = await objContext_create("ChRISDirsContext", `folder:${folder}`);
526
527
  if (!testGroup) {
527
- console.error(chalk.red(`Folder context '${folder}' does not exist in CUBE. Please specify an existing directory.`));
528
+ chiliErrLog(chalk.red(`Folder context '${folder}' does not exist in CUBE. Please specify an existing directory.`));
528
529
  return null;
529
530
  }
530
531
  }
531
532
  catch (error) {
532
- console.error(chalk.red(`Folder context '${folder}' does not exist in CUBE. Please specify an existing directory.`));
533
+ chiliErrLog(chalk.red(`Folder context '${folder}' does not exist in CUBE. Please specify an existing directory.`));
533
534
  return null;
534
535
  }
535
536
  // Folder exists, so just set chrisFolder without calling init()
536
537
  // (init() tries to CREATE the folder which will fail if it already exists)
537
538
  chrisIO.chrisFolder = folder;
538
- console.log(border_draw(chalk.cyan("Scanning files to upload...")));
539
+ chiliLog(border_draw(chalk.cyan("Scanning files to upload...")));
539
540
  const filesToUpload = await filesToUpload_get(options.hostpath, folder);
540
541
  let totalSize = 0;
541
542
  for (const file of filesToUpload) {
@@ -643,12 +644,12 @@ async function chris_pull(scanRecord, progressBar) {
643
644
  }
644
645
  else {
645
646
  summary.failedCount++;
646
- console.log(chalk.yellow(`Failed to download: ${file.hostPath}`));
647
+ chiliLog(chalk.yellow(`Failed to download: ${file.hostPath}`));
647
648
  }
648
649
  }
649
650
  catch (error) {
650
651
  summary.failedCount++;
651
- console.log(chalk.red(`Error downloading ${file.hostPath}: ${error instanceof Error ? error.message : String(error)}`));
652
+ chiliLog(chalk.red(`Error downloading ${file.hostPath}: ${error instanceof Error ? error.message : String(error)}`));
652
653
  }
653
654
  progressBar.update(index + 1, {
654
655
  bytes: bytes_format(summary.transferSize), // Renamed
@@ -685,12 +686,12 @@ async function chris_push(scanRecord, progressBar) {
685
686
  }
686
687
  else {
687
688
  summary.failedCount++;
688
- console.log(chalk.yellow(`Failed to upload: ${file.hostPath}`));
689
+ chiliLog(chalk.yellow(`Failed to upload: ${file.hostPath}`));
689
690
  }
690
691
  }
691
692
  catch (error) {
692
693
  summary.failedCount++;
693
- console.log(chalk.red(`Error uploading ${file.hostPath}: ${error instanceof Error ? error.message : String(error)}`));
694
+ chiliLog(chalk.red(`Error uploading ${file.hostPath}: ${error instanceof Error ? error.message : String(error)}`));
694
695
  }
695
696
  progressBar.update(index + 1, {
696
697
  bytes: bytes_format(summary.transferSize), // Renamed
@@ -713,7 +714,7 @@ async function download_handle(options) {
713
714
  await chrisContext.currentContext_update();
714
715
  const folder = chrisContext.singleContext.folder;
715
716
  if (!folder) {
716
- console.error(chalk.red("No ChRIS folder context set. Use 'connect' to establish a session."));
717
+ chiliErrLog(chalk.red("No ChRIS folder context set. Use 'connect' to establish a session."));
717
718
  return false;
718
719
  }
719
720
  const localTarget = path.resolve(options.hostpath || process.cwd());
@@ -728,7 +729,7 @@ async function download_handle(options) {
728
729
  }
729
730
  catch (error) {
730
731
  const msg = error instanceof Error ? error.message : String(error);
731
- console.error(chalk.red(`Download failed: ${msg}`));
732
+ chiliErrLog(chalk.red(`Download failed: ${msg}`));
732
733
  return false;
733
734
  }
734
735
  }
@@ -788,7 +789,7 @@ export async function pathCommand_setup(program) {
788
789
  hostpath: hostpath || process.cwd(),
789
790
  force: options.force,
790
791
  });
791
- console.log(result);
792
+ chiliLog(result);
792
793
  });
793
794
  pathCommand
794
795
  .command("scan")
@@ -5,6 +5,7 @@
5
5
  */
6
6
  import { Command } from "commander";
7
7
  import { CLIoptions } from "../utils/cli.js";
8
+ import { type CommandEnvelope } from "@fnndsc/cumin";
8
9
  /**
9
10
  * Handles commands related to groups of plugin contexts (computes, instances, parameters).
10
11
  */
@@ -33,6 +34,23 @@ export declare class PluginContextGroupHandler {
33
34
  * Lists available fields for the current plugin context resource.
34
35
  */
35
36
  parameters_fieldsList(): Promise<void>;
37
+ /**
38
+ * Renders plugin parameters in "man page" style as an envelope.
39
+ *
40
+ * Sans-I/O counterpart to {@link parameters_listMan}.
41
+ *
42
+ * @param options - CLI options for filtering.
43
+ * @returns An envelope carrying the rendered parameters or an error.
44
+ */
45
+ parameters_listManRender(options: CLIoptions): Promise<CommandEnvelope>;
46
+ /**
47
+ * Renders available fields for the current plugin context resource as an envelope.
48
+ *
49
+ * Sans-I/O counterpart to {@link parameters_fieldsList}.
50
+ *
51
+ * @returns An envelope carrying the field listing.
52
+ */
53
+ parameters_fieldsRender(): Promise<CommandEnvelope>;
36
54
  /**
37
55
  * Sets up the Commander.js commands for plugin context group operations.
38
56
  *
@@ -6,7 +6,9 @@
6
6
  import { BaseGroupHandler } from "../handlers/baseGroupHandler.js";
7
7
  import { options_toParams } from "../utils/cli.js";
8
8
  import { PluginContextController } from "../controllers/pluginContextController.js";
9
- import { pluginParameters_renderMan } from "../views/pluginParameters.js";
9
+ import { pluginParameters_renderMan, pluginParameters_manRender } from "../views/pluginParameters.js";
10
+ import { envelope_ok, envelope_error } from "@fnndsc/cumin";
11
+ import { chiliErrLog, chiliLog } from "../screen/output.js";
10
12
  class InitializationError extends Error {
11
13
  constructor(message) {
12
14
  super(message);
@@ -52,7 +54,7 @@ export class PluginContextGroupHandler {
52
54
  // We access the asset directly from the controller
53
55
  const asset = this.controller.chrisObject.asset;
54
56
  if (!asset || typeof asset.resources_listAndFilterByOptions !== 'function') {
55
- console.error("Underlying resource does not support listing.");
57
+ chiliErrLog("Underlying resource does not support listing.");
56
58
  return;
57
59
  }
58
60
  const params = options_toParams(options);
@@ -61,11 +63,11 @@ export class PluginContextGroupHandler {
61
63
  pluginParameters_renderMan(results);
62
64
  }
63
65
  else {
64
- console.log("No parameters found.");
66
+ chiliLog("No parameters found.");
65
67
  }
66
68
  }
67
69
  catch (error) {
68
- console.error(`Error listing parameters: ${error}`);
70
+ chiliErrLog(`Error listing parameters: ${error}`);
69
71
  }
70
72
  }
71
73
  /**
@@ -76,6 +78,44 @@ export class PluginContextGroupHandler {
76
78
  await this.baseGroupHandler.resourceFields_list();
77
79
  }
78
80
  }
81
+ /**
82
+ * Renders plugin parameters in "man page" style as an envelope.
83
+ *
84
+ * Sans-I/O counterpart to {@link parameters_listMan}.
85
+ *
86
+ * @param options - CLI options for filtering.
87
+ * @returns An envelope carrying the rendered parameters or an error.
88
+ */
89
+ async parameters_listManRender(options) {
90
+ try {
91
+ const asset = this.controller.chrisObject.asset;
92
+ if (!asset || typeof asset.resources_listAndFilterByOptions !== 'function') {
93
+ return envelope_error('', undefined, "Underlying resource does not support listing.\n");
94
+ }
95
+ const params = options_toParams(options);
96
+ const results = await asset.resources_listAndFilterByOptions(params);
97
+ if (results) {
98
+ return envelope_ok(`${pluginParameters_manRender(results)}\n`);
99
+ }
100
+ return envelope_ok("No parameters found.\n");
101
+ }
102
+ catch (error) {
103
+ return envelope_error('', undefined, `Error listing parameters: ${error}\n`);
104
+ }
105
+ }
106
+ /**
107
+ * Renders available fields for the current plugin context resource as an envelope.
108
+ *
109
+ * Sans-I/O counterpart to {@link parameters_fieldsList}.
110
+ *
111
+ * @returns An envelope carrying the field listing.
112
+ */
113
+ async parameters_fieldsRender() {
114
+ if (this.baseGroupHandler) {
115
+ return this.baseGroupHandler.resourceFields_render();
116
+ }
117
+ return envelope_ok('');
118
+ }
79
119
  /**
80
120
  * Sets up the Commander.js commands for plugin context group operations.
81
121
  *
@@ -13,6 +13,7 @@ import { pluginIds_resolve } from "../commands/plugin/search.js";
13
13
  import { pluginRun_render } from "../views/plugin.js"; // Still needed for pluginRun_render
14
14
  import chalk from "chalk";
15
15
  import { pluginReadme_render } from "../commands/plugin/readme.js";
16
+ import { chiliErrLog, chiliLog } from "../screen/output.js";
16
17
  /**
17
18
  * Handles commands related to groups of ChRIS plugins.
18
19
  */
@@ -34,10 +35,10 @@ export class PluginGroupHandler {
34
35
  // async plugins_list(options: CLIoptions): Promise<void> {
35
36
  // try {
36
37
  // const { plugins, selectedFields } = await plugins_fetchList(options);
37
- // console.log(pluginList_render(plugins, selectedFields, { table: options.table, csv: options.csv }));
38
+ // chiliLog(pluginList_render(plugins, selectedFields, { table: options.table, csv: options.csv }));
38
39
  // } catch (error: unknown) {
39
40
  // const msg = error instanceof Error ? error.message : String(error);
40
- // console.error(msg);
41
+ // chiliErrLog(msg);
41
42
  // }
42
43
  // }
43
44
  /**
@@ -50,11 +51,11 @@ export class PluginGroupHandler {
50
51
  table_display(fields, ["fields"]);
51
52
  }
52
53
  else {
53
- console.log(`No resource fields found for ${this.assetName}.`);
54
+ chiliLog(`No resource fields found for ${this.assetName}.`);
54
55
  }
55
56
  }
56
57
  catch (error) {
57
- console.log(errorStack.stack_search(this.assetName)[0]);
58
+ chiliLog(errorStack.stack_search(this.assetName)[0]);
58
59
  }
59
60
  }
60
61
  /**
@@ -67,12 +68,12 @@ export class PluginGroupHandler {
67
68
  for (const search of searchables) {
68
69
  const items = await plugins_searchByTerm(search.raw);
69
70
  if (items.length === 0) {
70
- console.log(`No plugins found matching: ${search.raw}`);
71
+ chiliLog(`No plugins found matching: ${search.raw}`);
71
72
  continue;
72
73
  }
73
74
  for (const item of items) {
74
75
  // Show item info - reusing table_display for single item details if possible, or simple log
75
- console.log(`Preparing to delete Plugin: ID=${item.id}, Name=${item.name}, Version=${item.version}`);
76
+ chiliLog(`Preparing to delete Plugin: ID=${item.id}, Name=${item.name}, Version=${item.version}`);
76
77
  if (!options.force) {
77
78
  const confirmed = await prompt_confirm(`Are you sure you want to delete plugin ${item.name} (ID: ${item.id})?`);
78
79
  if (!confirmed)
@@ -80,10 +81,10 @@ export class PluginGroupHandler {
80
81
  }
81
82
  const success = await plugin_deleteById(item.id);
82
83
  if (success) {
83
- console.log(`Deleted plugin ${item.id}`);
84
+ chiliLog(`Deleted plugin ${item.id}`);
84
85
  }
85
86
  else {
86
- console.error(`Failed to delete plugin ${item.id}`);
87
+ chiliErrLog(`Failed to delete plugin ${item.id}`);
87
88
  }
88
89
  }
89
90
  }
@@ -161,11 +162,11 @@ export class PluginMemberHandler {
161
162
  async readme_print(repoUrl) {
162
163
  const content = await this.controller.readmeContent_fetch(repoUrl);
163
164
  if (content) {
164
- console.log(chalk.green.bold("\nREADME Content:"));
165
- console.log(pluginReadme_render(content));
165
+ chiliLog(chalk.green.bold("\nREADME Content:"));
166
+ chiliLog(pluginReadme_render(content));
166
167
  }
167
168
  else {
168
- console.log(chalk.red("README not found in the repository."));
169
+ chiliLog(chalk.red("README not found in the repository."));
169
170
  }
170
171
  }
171
172
  /**
@@ -176,21 +177,21 @@ export class PluginMemberHandler {
176
177
  */
177
178
  async plugin_readme(pluginId) {
178
179
  try {
179
- console.log(`Fetching info for plugin with ID: ${pluginId}`);
180
+ chiliLog(`Fetching info for plugin with ID: ${pluginId}`);
180
181
  const documentation = await this.controller.documentationUrl_get(pluginId);
181
182
  if (!documentation) {
182
183
  return null;
183
184
  }
184
- console.log(documentation);
185
+ chiliLog(documentation);
185
186
  await this.readme_print(documentation);
186
187
  return documentation;
187
188
  }
188
189
  catch (error) {
189
190
  if (error instanceof Error) {
190
- console.error(`Error fetching plugin info: ${error.message}`);
191
+ chiliErrLog(`Error fetching plugin info: ${error.message}`);
191
192
  }
192
193
  else {
193
- console.error("An unknown error occurred while fetching plugin info");
194
+ chiliErrLog("An unknown error occurred while fetching plugin info");
194
195
  }
195
196
  }
196
197
  return null;
@@ -214,15 +215,15 @@ export class PluginMemberHandler {
214
215
  try {
215
216
  const instance = await plugin_execute(searchable, params);
216
217
  if (!instance) {
217
- console.log(errorStack.messagesOfType_search("error", "plugin"));
218
+ chiliLog(errorStack.messagesOfType_search("error", "plugin"));
218
219
  return null;
219
220
  }
220
- console.log(pluginRun_render(instance));
221
+ chiliLog(pluginRun_render(instance));
221
222
  return instance.id;
222
223
  }
223
224
  catch (e) {
224
225
  const message = e instanceof Error ? e.message : String(e);
225
- console.error(message);
226
+ chiliErrLog(message);
226
227
  return null;
227
228
  }
228
229
  }
@@ -234,7 +235,7 @@ export class PluginMemberHandler {
234
235
  if (!hits) {
235
236
  return null;
236
237
  }
237
- console.log(hits);
238
+ chiliLog(hits);
238
239
  return hits;
239
240
  }
240
241
  /**
@@ -270,10 +271,10 @@ export class PluginMemberHandler {
270
271
  else {
271
272
  const warnings = errorStack_getAllOfType('warning');
272
273
  if (warnings && warnings.length > 0) {
273
- warnings.forEach(warning => console.error(chalk.yellow(warning)));
274
+ warnings.forEach(warning => chiliErrLog(chalk.yellow(warning)));
274
275
  }
275
276
  else {
276
- console.error(chalk.red("Plugin not found."));
277
+ chiliErrLog(chalk.red("Plugin not found."));
277
278
  }
278
279
  }
279
280
  });
@@ -297,7 +298,7 @@ export class PluginMemberHandler {
297
298
  });
298
299
  }
299
300
  else {
300
- console.error(`Failed to find '${this.assetName}' command. The 'readme' subcommand was not added.`);
301
+ chiliErrLog(`Failed to find '${this.assetName}' command. The 'readme' subcommand was not added.`);
301
302
  }
302
303
  }
303
304
  }
package/dist/run.d.ts CHANGED
@@ -9,6 +9,7 @@
9
9
  *
10
10
  * @module
11
11
  */
12
+ import { type ChiliCaptured } from "./screen/output.js";
12
13
  /**
13
14
  * Executes a single chili command in the current process.
14
15
  *
@@ -22,3 +23,15 @@
22
23
  * the leading `node`/script entries).
23
24
  */
24
25
  export declare function run(argv: string[]): Promise<void>;
26
+ /**
27
+ * Executes a single chili command with its output captured instead of printed.
28
+ *
29
+ * Runs {@link run} inside {@link chili_capture}, so everything the command
30
+ * would have written to the output and error channels is collected and
31
+ * returned. This is how an in-process host (the brasa engine) drives chili
32
+ * headless and carries the result in an envelope, with no console monkeypatch.
33
+ *
34
+ * @param argv - The chili arguments, e.g. `["feeds", "list", "-s"]`.
35
+ * @returns The captured output and error text.
36
+ */
37
+ export declare function run_capture(argv: string[]): Promise<ChiliCaptured>;
package/dist/run.js CHANGED
@@ -27,6 +27,7 @@ import { PACSQueryGroupHandler } from "./pacs/pacsQueryHandler.js";
27
27
  import { PACSRetrieveGroupHandler } from "./pacs/pacsRetrieveHandler.js";
28
28
  import { chrisConnection_init, NodeStorageProvider, errorStack_getAllOfType, } from "@fnndsc/cumin";
29
29
  import { FileGroupHandler } from "./filesystem/fileGroupHandler.js";
30
+ import { chiliErrLog, chiliLog, chili_capture } from "./screen/output.js";
30
31
  /**
31
32
  * Suppress the DEP0169 (`url.parse()`) warning emitted transitively by
32
33
  * axios -> proxy-from-env, which we do not control. Installed once at import.
@@ -84,14 +85,14 @@ async function handlers_initialize(program) {
84
85
  const err = e instanceof Error ? e.message : String(e);
85
86
  const errors = errorStack_getAllOfType("error");
86
87
  const warnings = errorStack_getAllOfType("warning");
87
- console.log(`Note: File group commands (files, dirs, links) are unavailable. Reason: ${err}`);
88
+ chiliLog(`Note: File group commands (files, dirs, links) are unavailable. Reason: ${err}`);
88
89
  if (errors.length > 0) {
89
- console.log("Errors:");
90
- errors.forEach((msg) => console.log(` - ${msg}`));
90
+ chiliLog("Errors:");
91
+ errors.forEach((msg) => chiliLog(` - ${msg}`));
91
92
  }
92
93
  if (warnings.length > 0) {
93
- console.log("Warnings:");
94
- warnings.forEach((msg) => console.log(` - ${msg}`));
94
+ chiliLog("Warnings:");
95
+ warnings.forEach((msg) => chiliLog(` - ${msg}`));
95
96
  }
96
97
  }
97
98
  try {
@@ -106,14 +107,14 @@ async function handlers_initialize(program) {
106
107
  const err = e instanceof Error ? e.message : String(e);
107
108
  const errors = errorStack_getAllOfType("error");
108
109
  const warnings = errorStack_getAllOfType("warning");
109
- console.log(`Note: Plugin context commands are unavailable. Reason: ${err}`);
110
+ chiliLog(`Note: Plugin context commands are unavailable. Reason: ${err}`);
110
111
  if (errors.length > 0) {
111
- console.log("Errors:");
112
- errors.forEach((msg) => console.log(` - ${msg}`));
112
+ chiliLog("Errors:");
113
+ errors.forEach((msg) => chiliLog(` - ${msg}`));
113
114
  }
114
115
  if (warnings.length > 0) {
115
- console.log("Warnings:");
116
- warnings.forEach((msg) => console.log(` - ${msg}`));
116
+ chiliLog("Warnings:");
117
+ warnings.forEach((msg) => chiliLog(` - ${msg}`));
117
118
  }
118
119
  }
119
120
  }
@@ -155,7 +156,7 @@ export async function run(argv) {
155
156
  if (context) {
156
157
  const contextSetSuccess = await connection.context_set(context);
157
158
  if (!contextSetSuccess) {
158
- console.error("Failed to set context.");
159
+ chiliErrLog("Failed to set context.");
159
160
  return;
160
161
  }
161
162
  fullArgv = newArgs;
@@ -174,8 +175,8 @@ export async function run(argv) {
174
175
  program.parseOptions(fullArgv);
175
176
  const options = program.opts();
176
177
  if (!options.nosplash) {
177
- console.log(figlet.textSync("ChILI"));
178
- console.log("ChILI handles Intelligent Line Interactions");
178
+ chiliLog(figlet.textSync("ChILI"));
179
+ chiliLog("ChILI handles Intelligent Line Interactions");
179
180
  }
180
181
  try {
181
182
  await program.parseAsync(fullArgv);
@@ -190,3 +191,17 @@ export async function run(argv) {
190
191
  throw err;
191
192
  }
192
193
  }
194
+ /**
195
+ * Executes a single chili command with its output captured instead of printed.
196
+ *
197
+ * Runs {@link run} inside {@link chili_capture}, so everything the command
198
+ * would have written to the output and error channels is collected and
199
+ * returned. This is how an in-process host (the brasa engine) drives chili
200
+ * headless and carries the result in an envelope, with no console monkeypatch.
201
+ *
202
+ * @param argv - The chili arguments, e.g. `["feeds", "list", "-s"]`.
203
+ * @returns The captured output and error text.
204
+ */
205
+ export async function run_capture(argv) {
206
+ return chili_capture(() => run(argv));
207
+ }
@@ -0,0 +1,88 @@
1
+ /**
2
+ * @file chili's output seam.
3
+ *
4
+ * chili historically printed straight to `console.log`/`console.error`, which
5
+ * fused its command layer to a terminal and forced any in-process host (the
6
+ * brasa engine, which drives chili headless) to capture output with a console
7
+ * monkeypatch. This seam inverts that: command output is written to whatever
8
+ * writer is installed, and by default that writer simply delegates to the
9
+ * process console — so chili's standalone CLI behaves exactly as it always has.
10
+ *
11
+ * A host captures a run's output with {@link chili_capture}, which swaps in a
12
+ * buffering writer for the duration of a call and returns the collected text.
13
+ * This is explicit dependency injection through a module seam, not a runtime
14
+ * override of the global `console`: the default writer calls `console.log`
15
+ * normally, and capture bypasses the console entirely.
16
+ *
17
+ * @module
18
+ */
19
+ /**
20
+ * Destination for chili's command output.
21
+ */
22
+ export interface ChiliWriter {
23
+ /**
24
+ * Writes to the output channel with `console.log` semantics.
25
+ *
26
+ * @param args - The values to format, exactly as passed to `console.log`.
27
+ */
28
+ log(...args: unknown[]): void;
29
+ /**
30
+ * Writes to the error channel with `console.error` semantics.
31
+ *
32
+ * @param args - The values to format, exactly as passed to `console.error`.
33
+ */
34
+ errLog(...args: unknown[]): void;
35
+ /**
36
+ * Writes raw text or bytes to the output channel without adding a newline.
37
+ *
38
+ * @param chunk - The text or bytes to write.
39
+ */
40
+ write(chunk: string | Buffer): void;
41
+ }
42
+ /**
43
+ * Installs a writer as the active output destination.
44
+ *
45
+ * @param writer - The writer to install.
46
+ * @returns The previously installed writer, so callers can restore it.
47
+ */
48
+ export declare function chiliWriter_set(writer: ChiliWriter): ChiliWriter;
49
+ /**
50
+ * Writes to the output channel with `console.log` semantics.
51
+ *
52
+ * @param args - The values to format, exactly as passed to `console.log`.
53
+ */
54
+ export declare function chiliLog(...args: unknown[]): void;
55
+ /**
56
+ * Writes to the error channel with `console.error` semantics.
57
+ *
58
+ * @param args - The values to format, exactly as passed to `console.error`.
59
+ */
60
+ export declare function chiliErrLog(...args: unknown[]): void;
61
+ /**
62
+ * Writes raw text or bytes to the output channel without adding a newline —
63
+ * the replacement for direct `process.stdout.write` calls.
64
+ *
65
+ * @param chunk - The text or bytes to write.
66
+ */
67
+ export declare function chiliWrite(chunk: string | Buffer): void;
68
+ /**
69
+ * The text captured from a run, one string per channel.
70
+ */
71
+ export interface ChiliCaptured {
72
+ /** Everything written to the output channel. */
73
+ out: string;
74
+ /** Everything written to the error channel. */
75
+ err: string;
76
+ }
77
+ /**
78
+ * Runs `fn` with both channels captured into memory and returns the collected
79
+ * text, restoring the previous writer afterwards even if `fn` throws.
80
+ *
81
+ * Formatting matches `console.log`: arguments are rendered with `util.format`
82
+ * and each `log`/`errLog` call contributes a trailing newline, exactly as the
83
+ * console would have written.
84
+ *
85
+ * @param fn - The work whose output should be captured.
86
+ * @returns The captured output and error text.
87
+ */
88
+ export declare function chili_capture(fn: () => Promise<void>): Promise<ChiliCaptured>;