@fnndsc/chili 3.4.0 → 3.6.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 (36) hide show
  1. package/dist/chefs/chefs.js +12 -11
  2. package/dist/commands/fs/download.js +4 -3
  3. package/dist/commands/fs/upload.js +4 -3
  4. package/dist/commands/man/doc.js +3 -2
  5. package/dist/commands/plugins/add.js +35 -34
  6. package/dist/config/colorConfig.js +2 -1
  7. package/dist/connect/connectHandler.js +7 -6
  8. package/dist/context/contextCommand.js +3 -2
  9. package/dist/controllers/fileController.js +4 -3
  10. package/dist/controllers/pluginController.js +2 -1
  11. package/dist/feeds/feedHandler.js +19 -18
  12. package/dist/filesystem/fileGroupHandler.d.ts +18 -5
  13. package/dist/filesystem/fileGroupHandler.js +68 -46
  14. package/dist/filesystem/filesystemHandler.js +13 -11
  15. package/dist/filesystem/inodeCommand.js +3 -2
  16. package/dist/handlers/baseGroupHandler.d.ts +8 -2
  17. package/dist/handlers/baseGroupHandler.js +30 -18
  18. package/dist/index.js +3 -2
  19. package/dist/lfs/lfs.js +6 -4
  20. package/dist/man/man.js +4 -3
  21. package/dist/man/renderer.js +3 -2
  22. package/dist/pacs/pacsQueryHandler.js +14 -13
  23. package/dist/pacs/pacsRetrieveHandler.js +13 -12
  24. package/dist/path/pathCommand.js +23 -22
  25. package/dist/plugins/pluginGroupHandler.d.ts +17 -6
  26. package/dist/plugins/pluginGroupHandler.js +58 -37
  27. package/dist/plugins/pluginHandler.js +23 -22
  28. package/dist/run.d.ts +22 -0
  29. package/dist/run.js +42 -46
  30. package/dist/screen/output.d.ts +88 -0
  31. package/dist/screen/output.js +95 -0
  32. package/dist/screen/screen.js +9 -8
  33. package/dist/utils/admin_prompt.js +10 -9
  34. package/dist/utils/docker.js +10 -9
  35. package/dist/views/pluginParameters.js +2 -1
  36. package/package.json +1 -1
@@ -8,39 +8,50 @@ import { options_toParams } from "../utils/cli.js";
8
8
  import { PluginContextController } from "../controllers/pluginContextController.js";
9
9
  import { pluginParameters_renderMan, pluginParameters_manRender } from "../views/pluginParameters.js";
10
10
  import { envelope_ok, envelope_error } from "@fnndsc/cumin";
11
- class InitializationError extends Error {
12
- constructor(message) {
13
- super(message);
14
- this.name = "InitializationError";
15
- }
16
- }
11
+ import { chiliErrLog, chiliLog } from "../screen/output.js";
17
12
  /**
18
13
  * Handles commands related to groups of plugin contexts (computes, instances, parameters).
19
14
  */
20
15
  export class PluginContextGroupHandler {
21
- constructor(controller, assetName) {
22
- this.baseGroupHandler = null;
23
- this.controller = controller;
16
+ constructor(assetName, id) {
17
+ // Resolved lazily on first action.
18
+ this.controller = null;
24
19
  this.assetName = assetName;
25
- // Cast ChRISEmbeddedResourceGroup to ChRISEmbeddedResourceGroup<unknown> safely.
26
- this.baseGroupHandler = new BaseGroupHandler(this.assetName, this.controller.chrisObject);
20
+ this.id = id;
21
+ this.baseGroupHandler = new BaseGroupHandler(this.assetName);
27
22
  }
28
23
  /**
29
- * Factory method to create a new PluginContextGroupHandler instance.
24
+ * Creates a PluginContextGroupHandler for an asset type. This performs no
25
+ * network work and resolves no plugin context — command registration needs
26
+ * only the asset name; the controller is resolved lazily on first action (see
27
+ * {@link controller_ensure}).
30
28
  *
31
- * @param assetName - The type of plugin context to handle ('computesofplugin', 'instancesofplugin', 'parametersofplugin').
32
- * @param id - Optional plugin ID. Defaults to current ChRIS plugin context.
33
- * @returns A Promise resolving to a new PluginContextGroupHandler instance.
34
- * @throws InitializationError if an unsupported asset type is provided.
29
+ * @param assetName - The type of plugin context ('computesofplugin', 'instancesofplugin', 'parametersofplugin').
30
+ * @param id - Optional plugin ID the context binds to; defaults to the current plugin context.
31
+ * @returns A new PluginContextGroupHandler instance.
35
32
  */
36
- static async handler_create(assetName, id) {
37
- try {
38
- const controller = await PluginContextController.controller_create(assetName, id);
39
- return new PluginContextGroupHandler(controller, assetName);
40
- }
41
- catch (error) {
42
- throw new InitializationError(`Failed to initialize PluginContextGroupHandler: ${error}`);
33
+ static handler_create(assetName, id) {
34
+ return new PluginContextGroupHandler(assetName, id);
35
+ }
36
+ /**
37
+ * Lazily resolves and memoizes the plugin-context controller against the bound
38
+ * plugin id (or the current ChRIS context), wiring it into the
39
+ * registration-only base handler so its actions can read the context.
40
+ *
41
+ * @returns The controller, or null if the context cannot be resolved here.
42
+ */
43
+ async controller_ensure() {
44
+ if (!this.controller) {
45
+ try {
46
+ this.controller = await PluginContextController.controller_create(this.assetName, this.id);
47
+ this.baseGroupHandler.chrisObject =
48
+ this.controller.chrisObject;
49
+ }
50
+ catch {
51
+ this.controller = null;
52
+ }
43
53
  }
54
+ return this.controller;
44
55
  }
45
56
  /**
46
57
  * Lists plugin parameters in a "man page" style format.
@@ -50,10 +61,15 @@ export class PluginContextGroupHandler {
50
61
  */
51
62
  async parameters_listMan(options) {
52
63
  try {
64
+ const controller = await this.controller_ensure();
65
+ if (!controller) {
66
+ chiliErrLog(`No plugin context is available for ${this.assetName} in the current context.`);
67
+ return;
68
+ }
53
69
  // We access the asset directly from the controller
54
- const asset = this.controller.chrisObject.asset;
70
+ const asset = controller.chrisObject.asset;
55
71
  if (!asset || typeof asset.resources_listAndFilterByOptions !== 'function') {
56
- console.error("Underlying resource does not support listing.");
72
+ chiliErrLog("Underlying resource does not support listing.");
57
73
  return;
58
74
  }
59
75
  const params = options_toParams(options);
@@ -62,20 +78,23 @@ export class PluginContextGroupHandler {
62
78
  pluginParameters_renderMan(results);
63
79
  }
64
80
  else {
65
- console.log("No parameters found.");
81
+ chiliLog("No parameters found.");
66
82
  }
67
83
  }
68
84
  catch (error) {
69
- console.error(`Error listing parameters: ${error}`);
85
+ chiliErrLog(`Error listing parameters: ${error}`);
70
86
  }
71
87
  }
72
88
  /**
73
89
  * Lists available fields for the current plugin context resource.
74
90
  */
75
91
  async parameters_fieldsList() {
76
- if (this.baseGroupHandler) {
77
- await this.baseGroupHandler.resourceFields_list();
92
+ const controller = await this.controller_ensure();
93
+ if (!controller) {
94
+ chiliErrLog(`No plugin context is available for ${this.assetName} in the current context.`);
95
+ return;
78
96
  }
97
+ await this.baseGroupHandler.resourceFields_list();
79
98
  }
80
99
  /**
81
100
  * Renders plugin parameters in "man page" style as an envelope.
@@ -87,7 +106,11 @@ export class PluginContextGroupHandler {
87
106
  */
88
107
  async parameters_listManRender(options) {
89
108
  try {
90
- const asset = this.controller.chrisObject.asset;
109
+ const controller = await this.controller_ensure();
110
+ if (!controller) {
111
+ return envelope_error('', undefined, `No plugin context is available for ${this.assetName} in the current context.\n`);
112
+ }
113
+ const asset = controller.chrisObject.asset;
91
114
  if (!asset || typeof asset.resources_listAndFilterByOptions !== 'function') {
92
115
  return envelope_error('', undefined, "Underlying resource does not support listing.\n");
93
116
  }
@@ -110,10 +133,11 @@ export class PluginContextGroupHandler {
110
133
  * @returns An envelope carrying the field listing.
111
134
  */
112
135
  async parameters_fieldsRender() {
113
- if (this.baseGroupHandler) {
114
- return this.baseGroupHandler.resourceFields_render();
136
+ const controller = await this.controller_ensure();
137
+ if (!controller) {
138
+ return envelope_ok('');
115
139
  }
116
- return envelope_ok('');
140
+ return this.baseGroupHandler.resourceFields_render();
117
141
  }
118
142
  /**
119
143
  * Sets up the Commander.js commands for plugin context group operations.
@@ -121,9 +145,6 @@ export class PluginContextGroupHandler {
121
145
  * @param program - The Commander.js program instance.
122
146
  */
123
147
  pluginContextGroupCommand_setup(program) {
124
- if (this.baseGroupHandler) {
125
- this.baseGroupHandler.command_setup(program);
126
- }
127
- const fileGroupCommand = program.commands.find((cmd) => cmd.name() === this.assetName);
148
+ this.baseGroupHandler.command_setup(program);
128
149
  }
129
150
  }
@@ -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,16 @@
9
9
  *
10
10
  * @module
11
11
  */
12
+ import { type ChiliCaptured } from "./screen/output.js";
13
+ /**
14
+ * The set of top-level chili command names. Builds a fresh program and registers
15
+ * every handler — now a cheap, network-free operation — then reads the
16
+ * registered command names. A host (the brasa engine) uses this to decide
17
+ * whether an unknown chell command is worth delegating to chili at all.
18
+ *
19
+ * @returns The registered top-level command names.
20
+ */
21
+ export declare function commandNames_get(): Promise<Set<string>>;
12
22
  /**
13
23
  * Executes a single chili command in the current process.
14
24
  *
@@ -22,3 +32,15 @@
22
32
  * the leading `node`/script entries).
23
33
  */
24
34
  export declare function run(argv: string[]): Promise<void>;
35
+ /**
36
+ * Executes a single chili command with its output captured instead of printed.
37
+ *
38
+ * Runs {@link run} inside {@link chili_capture}, so everything the command
39
+ * would have written to the output and error channels is collected and
40
+ * returned. This is how an in-process host (the brasa engine) drives chili
41
+ * headless and carries the result in an envelope, with no console monkeypatch.
42
+ *
43
+ * @param argv - The chili arguments, e.g. `["feeds", "list", "-s"]`.
44
+ * @returns The captured output and error text.
45
+ */
46
+ export declare function run_capture(argv: string[]): Promise<ChiliCaptured>;
package/dist/run.js CHANGED
@@ -25,8 +25,9 @@ import { chefsCommand_setup } from "./chefs/chefs.js";
25
25
  import { PACSServerGroupHandler } from "./pacs/pacsServerHandler.js";
26
26
  import { PACSQueryGroupHandler } from "./pacs/pacsQueryHandler.js";
27
27
  import { PACSRetrieveGroupHandler } from "./pacs/pacsRetrieveHandler.js";
28
- import { chrisConnection_init, NodeStorageProvider, errorStack_getAllOfType, } from "@fnndsc/cumin";
28
+ import { chrisConnection_init, NodeStorageProvider, } 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.
@@ -72,51 +73,32 @@ async function handlers_initialize(program) {
72
73
  feedMemberHandler.feedCommand_setup(program);
73
74
  const pluginMemberHandler = new PluginMemberHandler();
74
75
  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);
76
+ // File-group and plugin-context commands register with no ChRIS context: each
77
+ // handler resolves its context lazily, only when one of its actions runs. This
78
+ // keeps setup free of network work, so an unrelated command (or a directory
79
+ // that is not a ChRIS folder) pays no file-context cost and produces no
80
+ // context-init noise.
81
+ for (const assetName of ["files", "links", "dirs"]) {
82
+ FileGroupHandler.handler_create(assetName).fileGroupCommand_setup(program);
104
83
  }
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
- }
84
+ for (const assetName of ["computesofplugin", "instancesofplugin", "parametersofplugin"]) {
85
+ PluginContextGroupHandler.handler_create(assetName).pluginContextGroupCommand_setup(program);
118
86
  }
119
87
  }
88
+ /**
89
+ * The set of top-level chili command names. Builds a fresh program and registers
90
+ * every handler — now a cheap, network-free operation — then reads the
91
+ * registered command names. A host (the brasa engine) uses this to decide
92
+ * whether an unknown chell command is worth delegating to chili at all.
93
+ *
94
+ * @returns The registered top-level command names.
95
+ */
96
+ export async function commandNames_get() {
97
+ const program = new Command();
98
+ connectCommand_setup(program);
99
+ await handlers_initialize(program);
100
+ return new Set(program.commands.map((cmd) => cmd.name()));
101
+ }
120
102
  /**
121
103
  * Extracts a leading `key=value` context string from an argv array.
122
104
  *
@@ -155,7 +137,7 @@ export async function run(argv) {
155
137
  if (context) {
156
138
  const contextSetSuccess = await connection.context_set(context);
157
139
  if (!contextSetSuccess) {
158
- console.error("Failed to set context.");
140
+ chiliErrLog("Failed to set context.");
159
141
  return;
160
142
  }
161
143
  fullArgv = newArgs;
@@ -174,8 +156,8 @@ export async function run(argv) {
174
156
  program.parseOptions(fullArgv);
175
157
  const options = program.opts();
176
158
  if (!options.nosplash) {
177
- console.log(figlet.textSync("ChILI"));
178
- console.log("ChILI handles Intelligent Line Interactions");
159
+ chiliLog(figlet.textSync("ChILI"));
160
+ chiliLog("ChILI handles Intelligent Line Interactions");
179
161
  }
180
162
  try {
181
163
  await program.parseAsync(fullArgv);
@@ -190,3 +172,17 @@ export async function run(argv) {
190
172
  throw err;
191
173
  }
192
174
  }
175
+ /**
176
+ * Executes a single chili command with its output captured instead of printed.
177
+ *
178
+ * Runs {@link run} inside {@link chili_capture}, so everything the command
179
+ * would have written to the output and error channels is collected and
180
+ * returned. This is how an in-process host (the brasa engine) drives chili
181
+ * headless and carries the result in an envelope, with no console monkeypatch.
182
+ *
183
+ * @param argv - The chili arguments, e.g. `["feeds", "list", "-s"]`.
184
+ * @returns The captured output and error text.
185
+ */
186
+ export async function run_capture(argv) {
187
+ return chili_capture(() => run(argv));
188
+ }
@@ -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>;
@@ -0,0 +1,95 @@
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
+ import { format } from "util";
20
+ /**
21
+ * The default writer: delegates to the process console. Installed at import so
22
+ * the standalone CLI prints exactly as before.
23
+ */
24
+ const consoleWriter = {
25
+ log: (...args) => { console.log(...args); },
26
+ errLog: (...args) => { console.error(...args); },
27
+ write: (chunk) => { process.stdout.write(chunk); },
28
+ };
29
+ /** The writer in effect. Swapped for the duration of a captured run. */
30
+ let activeWriter = consoleWriter;
31
+ /**
32
+ * Installs a writer as the active output destination.
33
+ *
34
+ * @param writer - The writer to install.
35
+ * @returns The previously installed writer, so callers can restore it.
36
+ */
37
+ export function chiliWriter_set(writer) {
38
+ const previous = activeWriter;
39
+ activeWriter = writer;
40
+ return previous;
41
+ }
42
+ /**
43
+ * Writes to the output channel with `console.log` semantics.
44
+ *
45
+ * @param args - The values to format, exactly as passed to `console.log`.
46
+ */
47
+ export function chiliLog(...args) {
48
+ activeWriter.log(...args);
49
+ }
50
+ /**
51
+ * Writes to the error channel with `console.error` semantics.
52
+ *
53
+ * @param args - The values to format, exactly as passed to `console.error`.
54
+ */
55
+ export function chiliErrLog(...args) {
56
+ activeWriter.errLog(...args);
57
+ }
58
+ /**
59
+ * Writes raw text or bytes to the output channel without adding a newline —
60
+ * the replacement for direct `process.stdout.write` calls.
61
+ *
62
+ * @param chunk - The text or bytes to write.
63
+ */
64
+ export function chiliWrite(chunk) {
65
+ activeWriter.write(chunk);
66
+ }
67
+ /**
68
+ * Runs `fn` with both channels captured into memory and returns the collected
69
+ * text, restoring the previous writer afterwards even if `fn` throws.
70
+ *
71
+ * Formatting matches `console.log`: arguments are rendered with `util.format`
72
+ * and each `log`/`errLog` call contributes a trailing newline, exactly as the
73
+ * console would have written.
74
+ *
75
+ * @param fn - The work whose output should be captured.
76
+ * @returns The captured output and error text.
77
+ */
78
+ export async function chili_capture(fn) {
79
+ const outChunks = [];
80
+ const errChunks = [];
81
+ const previous = chiliWriter_set({
82
+ log: (...args) => { outChunks.push(`${format(...args)}\n`); },
83
+ errLog: (...args) => { errChunks.push(`${format(...args)}\n`); },
84
+ write: (chunk) => {
85
+ outChunks.push(typeof chunk === "string" ? chunk : chunk.toString("utf-8"));
86
+ },
87
+ });
88
+ try {
89
+ await fn();
90
+ }
91
+ finally {
92
+ chiliWriter_set(previous);
93
+ }
94
+ return { out: outChunks.join(""), err: errChunks.join("") };
95
+ }