@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
@@ -9,12 +9,13 @@ import { options_toParams } from "../utils/cli.js";
9
9
  import { resourceColumns_removeDuplicates } from "../utils/resourceData.js";
10
10
  import { table_display, table_render, border_draw } from "../screen/screen.js";
11
11
  import * as readline from "readline";
12
+ import { chiliErrLog, chiliLog } from "../screen/output.js";
12
13
  /**
13
14
  * Base handler for groups of ChRIS resources.
14
15
  * Provides common functionality for listing and deleting resources.
15
16
  */
16
17
  export class BaseGroupHandler {
17
- constructor(assetName, chrisObject) {
18
+ constructor(assetName, chrisObject = null) {
18
19
  this.assetName = "";
19
20
  this.assetName = assetName;
20
21
  this.chrisObject = chrisObject;
@@ -22,6 +23,17 @@ export class BaseGroupHandler {
22
23
  title: { title: this.assetName, justification: "center" },
23
24
  };
24
25
  }
26
+ /**
27
+ * The resolved ChRIS context, for use inside actions. Throws if read before
28
+ * the context has been set — a lazy handler must resolve its context before
29
+ * an action dereferences it.
30
+ */
31
+ get chrisObjectResolved() {
32
+ if (!this.chrisObject) {
33
+ throw new Error(`ChRIS context for '${this.assetName}' is not available in the current context.`);
34
+ }
35
+ return this.chrisObject;
36
+ }
25
37
  /**
26
38
  * Lists ChRIS resources based on provided CLI options.
27
39
  *
@@ -49,13 +61,13 @@ export class BaseGroupHandler {
49
61
  options.fields = cleanFields.join(',');
50
62
  }
51
63
  const params = options_toParams(options);
52
- const results = await this.chrisObject.asset.resources_listAndFilterByOptions(params);
64
+ const results = await this.chrisObjectResolved.asset.resources_listAndFilterByOptions(params);
53
65
  if (!results) {
54
- console.error(`No ${this.assetName} resources found. Perhaps check your current context?`);
66
+ chiliErrLog(`No ${this.assetName} resources found. Perhaps check your current context?`);
55
67
  return;
56
68
  }
57
69
  if (results.tableData.length === 0) {
58
- console.log(`No ${this.assetName} found matching the criteria.`);
70
+ chiliLog(`No ${this.assetName} found matching the criteria.`);
59
71
  }
60
72
  else {
61
73
  const uniqueResults = resourceColumns_removeDuplicates(results);
@@ -73,7 +85,7 @@ export class BaseGroupHandler {
73
85
  return `"${String(val !== undefined ? val : '').split('"').join('""')}"`;
74
86
  }).join(',');
75
87
  }).join('\n');
76
- console.log(header + '\n' + rows);
88
+ chiliLog(header + '\n' + rows);
77
89
  }
78
90
  else if (options.table) {
79
91
  // Explicit Table format (with borders and title)
@@ -104,7 +116,7 @@ export class BaseGroupHandler {
104
116
  }
105
117
  }
106
118
  catch (error) {
107
- console.log(errorStack.stack_search(this.assetName)[0]);
119
+ chiliLog(errorStack.stack_search(this.assetName)[0]);
108
120
  }
109
121
  }
110
122
  /**
@@ -112,20 +124,20 @@ export class BaseGroupHandler {
112
124
  */
113
125
  async resourceFields_list() {
114
126
  try {
115
- const results = await this.chrisObject.asset.resourceFields_get();
127
+ const results = await this.chrisObjectResolved.asset.resourceFields_get();
116
128
  if (!results) {
117
- console.error(`An error occurred while fetching resource fields for ${this.assetName}.`);
129
+ chiliErrLog(`An error occurred while fetching resource fields for ${this.assetName}.`);
118
130
  return;
119
131
  }
120
132
  if (results.fields.length === 0) {
121
- console.log(`No resource fields found for ${this.assetName}.`);
133
+ chiliLog(`No resource fields found for ${this.assetName}.`);
122
134
  }
123
135
  else {
124
136
  table_display(results.fields, ["fields"]);
125
137
  }
126
138
  }
127
139
  catch (error) {
128
- console.log(errorStack.stack_search(this.assetName)[0]);
140
+ chiliLog(errorStack.stack_search(this.assetName)[0]);
129
141
  }
130
142
  }
131
143
  /**
@@ -138,7 +150,7 @@ export class BaseGroupHandler {
138
150
  */
139
151
  async resourceFields_render() {
140
152
  try {
141
- const results = await this.chrisObject.asset.resourceFields_get();
153
+ const results = await this.chrisObjectResolved.asset.resourceFields_get();
142
154
  if (!results) {
143
155
  return envelope_error('', undefined, `An error occurred while fetching resource fields for ${this.assetName}.\n`);
144
156
  }
@@ -240,21 +252,21 @@ export class BaseGroupHandler {
240
252
  let OKorNot = "";
241
253
  for (const id of IDs) {
242
254
  try {
243
- const searchResults = await this.chrisObject.asset.resources_listAndFilterByOptions({
255
+ const searchResults = await this.chrisObjectResolved.asset.resources_listAndFilterByOptions({
244
256
  id: id,
245
257
  });
246
- console.log(border_draw(`checking ${this.assetName} id ${id} ... ${this.msg_OKorNot(searchResults)}`));
258
+ chiliLog(border_draw(`checking ${this.assetName} id ${id} ... ${this.msg_OKorNot(searchResults)}`));
247
259
  if (!force) {
248
260
  confirm = await this.user_confirmContinuation(id, "delete");
249
261
  if (!confirm) {
250
262
  continue;
251
263
  }
252
264
  }
253
- delop = await this.chrisObject.asset.resourceItem_delete(id);
265
+ delop = await this.chrisObjectResolved.asset.resourceItem_delete(id);
254
266
  border_draw(`deleting ${this.assetName} id ${id} ... ${this.msg_OKorNot(true)}`);
255
267
  }
256
268
  catch (error) {
257
- console.error(`${error}`);
269
+ chiliErrLog(`${error}`);
258
270
  return false;
259
271
  }
260
272
  }
@@ -268,7 +280,7 @@ export class BaseGroupHandler {
268
280
  */
269
281
  async IDs_getFromSearch(options) {
270
282
  const params = options_toParams(options);
271
- const searchResults = await this.chrisObject.asset.resources_listAndFilterByOptions(params);
283
+ const searchResults = await this.chrisObjectResolved.asset.resources_listAndFilterByOptions(params);
272
284
  if (!searchResults) {
273
285
  return null;
274
286
  }
@@ -284,7 +296,7 @@ export class BaseGroupHandler {
284
296
  let nIDs;
285
297
  nIDs = await this.IDs_getFromSearch(options);
286
298
  if (!nIDs) {
287
- console.error(`No ${this.assetName} matched the search criteria.`);
299
+ chiliErrLog(`No ${this.assetName} matched the search criteria.`);
288
300
  return;
289
301
  }
290
302
  if (nIDs) {
@@ -334,7 +346,7 @@ export class BaseGroupHandler {
334
346
  .option("-f, --force", `force the deletion without prompting for user confirmation`)
335
347
  .action(async (searchable, options) => {
336
348
  const searchParts = searchable.split("++").map((part) => part.trim());
337
- // console.log(`searchParts = ${searchParts}`);
349
+ // chiliLog(`searchParts = ${searchParts}`);
338
350
  for (const searchPart of searchParts) {
339
351
  const currentOptions = {
340
352
  ...options,
package/dist/index.js CHANGED
@@ -11,6 +11,7 @@
11
11
  */
12
12
  import omelette from "omelette";
13
13
  import { run } from "./run.js";
14
+ import { chiliErrLog, chiliLog } from "./screen/output.js";
14
15
  /**
15
16
  * Sets up shell command completion for the standalone chili CLI using omelette.
16
17
  * This is a bin-only concern; in-process hosts provide their own completion.
@@ -50,7 +51,7 @@ function commandCompletion_setup() {
50
51
  },
51
52
  });
52
53
  completion.on("man.doc", ({ reply }) => {
53
- console.log("Autocomplete triggered for man doc command");
54
+ chiliLog("Autocomplete triggered for man doc command");
54
55
  reply([]);
55
56
  });
56
57
  completion.init();
@@ -66,7 +67,7 @@ async function main() {
66
67
  await run(process.argv.slice(2));
67
68
  }
68
69
  main().catch((error) => {
69
- console.error("An error occurred:", error);
70
+ chiliErrLog("An error occurred:", error);
70
71
  process.exit(1);
71
72
  });
72
73
  // Re-export library utilities for consumers that import the package root.
package/dist/lfs/lfs.js CHANGED
@@ -8,6 +8,8 @@
8
8
  */
9
9
  import * as fs from 'fs';
10
10
  import * as path from 'path';
11
+ import { table_render } from "../screen/screen.js";
12
+ import { chiliErrLog, chiliLog } from "../screen/output.js";
11
13
  /**
12
14
  * Lists the contents of a directory with details.
13
15
  *
@@ -22,10 +24,10 @@ export async function dirContents_list(filepath) {
22
24
  return { filename: file, "size(KB)": size, created_at: birthtime };
23
25
  });
24
26
  const detailedFiles = await Promise.all(detailedFilesPromises);
25
- console.table(detailedFiles);
27
+ chiliLog(table_render(detailedFiles, ["filename", "size(KB)", "created_at"]));
26
28
  }
27
29
  catch (error) {
28
- console.error("Error occurred while reading the directory!", error);
30
+ chiliErrLog("Error occurred while reading the directory!", error);
29
31
  }
30
32
  }
31
33
  /**
@@ -36,7 +38,7 @@ export async function dirContents_list(filepath) {
36
38
  export function dir_create(filepath) {
37
39
  if (!fs.existsSync(filepath)) {
38
40
  fs.mkdirSync(filepath);
39
- console.log("The directory has been created successfully");
41
+ chiliLog("The directory has been created successfully");
40
42
  }
41
43
  }
42
44
  /**
@@ -46,7 +48,7 @@ export function dir_create(filepath) {
46
48
  */
47
49
  export function file_create(filepath) {
48
50
  fs.openSync(filepath, "w");
49
- console.log("An empty file has been created");
51
+ chiliLog("An empty file has been created");
50
52
  }
51
53
  /**
52
54
  * Sets up the 'lfs' command group in Commander.
package/dist/man/man.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { topics_list } from "../commands/man/topics.js";
2
2
  import { manPage_display } from "../commands/man/doc.js";
3
+ import { chiliLog } from "../screen/output.js";
3
4
  /**
4
5
  * Sets up the 'man' command for displaying ChILI manual and help pages.
5
6
  *
@@ -24,8 +25,8 @@ export function manCommand_setup(program) {
24
25
  .description("List the available manual page topics")
25
26
  .action(async () => {
26
27
  const files = await topics_list();
27
- console.log("\n\nThe following topics are available:");
28
- console.log("(read more with 'chili man doc <topic>')\n");
29
- console.log(files.join("\n"));
28
+ chiliLog("\n\nThe following topics are available:");
29
+ chiliLog("(read more with 'chili man doc <topic>')\n");
30
+ chiliLog(files.join("\n"));
30
31
  });
31
32
  }
@@ -9,6 +9,7 @@ import chalk from "chalk";
9
9
  import { convert as adoc_convert } from "asciidoctor";
10
10
  import { exec } from "child_process";
11
11
  import os from "os";
12
+ import { chiliErrLog } from "../screen/output.js";
12
13
  const headingStyles = [
13
14
  { regex: /<h1.*?>(.*?)<\/h1>/g, font: "Standard", color: chalk.magenta },
14
15
  { regex: /<h2.*?>(.*?)<\/h2>/g, font: "Slant", color: chalk.yellow },
@@ -175,11 +176,11 @@ export async function browser_open(filePath) {
175
176
  : "xdg-open";
176
177
  exec(`${command} ${tempHtmlPath}`, (error) => {
177
178
  if (error) {
178
- console.error("Error opening browser:", error);
179
+ chiliErrLog("Error opening browser:", error);
179
180
  }
180
181
  });
181
182
  }
182
183
  catch (error) {
183
- console.error("Error opening documentation in browser:", error instanceof Error ? error.message : String(error));
184
+ chiliErrLog("Error opening documentation in browser:", error instanceof Error ? error.message : String(error));
184
185
  }
185
186
  }
@@ -9,6 +9,7 @@ import { BaseGroupHandler } from "../handlers/baseGroupHandler.js";
9
9
  import { border_draw } from "../screen/screen.js";
10
10
  import { pacsQueryResult_renderPretty } from "./pacsResultRender.js";
11
11
  import { pacsQueryPayload_build } from "./pacsQueryPayload.js";
12
+ import { chiliLog } from "../screen/output.js";
12
13
  /**
13
14
  * Handler for PACS queries commands.
14
15
  */
@@ -69,7 +70,7 @@ export class PACSQueryGroupHandler {
69
70
  const listCommand = this.baseGroupHandler.baseListCommand_create(async (options) => {
70
71
  const pacsserver = await this.pacsserverContext_resolve(options.pacsserver);
71
72
  if (!pacsserver) {
72
- console.log(border_draw("No PACS server in context. Use --pacsserver or set via context."));
73
+ chiliLog(border_draw("No PACS server in context. Use --pacsserver or set via context."));
73
74
  return;
74
75
  }
75
76
  const mergedOptions = this.options_withPACSFilter(options, pacsserver);
@@ -111,17 +112,17 @@ export class PACSQueryGroupHandler {
111
112
  }
112
113
  const payload = pacsQueryPayload_build(queryInput, options.title, options.description);
113
114
  if (!payload) {
114
- console.log(border_draw("Invalid query format. Provide JSON or comma-separated key:value pairs."));
115
+ chiliLog(border_draw("Invalid query format. Provide JSON or comma-separated key:value pairs."));
115
116
  return;
116
117
  }
117
118
  const result = await pacsQueries_create(pacsserver, payload);
118
119
  if (!result.ok) {
119
120
  const errors = errorStack_getAllOfType("error");
120
121
  if (errors.length) {
121
- errors.forEach((msg) => console.log(border_draw(msg)));
122
+ errors.forEach((msg) => chiliLog(border_draw(msg)));
122
123
  }
123
124
  else {
124
- console.log(border_draw("Failed to create PACS query."));
125
+ chiliLog(border_draw("Failed to create PACS query."));
125
126
  }
126
127
  return;
127
128
  }
@@ -133,7 +134,7 @@ export class PACSQueryGroupHandler {
133
134
  `pacs=${pacsserver}`,
134
135
  `title="${created.title ?? options.title ?? ""}"`,
135
136
  ].join(" ");
136
- console.log(border_draw(msg.trim()));
137
+ chiliLog(border_draw(msg.trim()));
137
138
  });
138
139
  }
139
140
  /**
@@ -149,17 +150,17 @@ export class PACSQueryGroupHandler {
149
150
  .action(async (queryId, options) => {
150
151
  const idNum = Number(queryId);
151
152
  if (Number.isNaN(idNum)) {
152
- console.log(border_draw("queryId must be a number."));
153
+ chiliLog(border_draw("queryId must be a number."));
153
154
  return;
154
155
  }
155
156
  const result = await pacsQuery_resultDecode(idNum);
156
157
  if (!result.ok) {
157
158
  const errors = errorStack_getAllOfType("error");
158
159
  if (errors.length) {
159
- errors.forEach((msg) => console.log(border_draw(msg)));
160
+ errors.forEach((msg) => chiliLog(border_draw(msg)));
160
161
  }
161
162
  else {
162
- console.log(border_draw(`Failed to decode PACS query result for ${idNum}.`));
163
+ chiliLog(border_draw(`Failed to decode PACS query result for ${idNum}.`));
163
164
  }
164
165
  return;
165
166
  }
@@ -167,23 +168,23 @@ export class PACSQueryGroupHandler {
167
168
  // Prefer JSON, then text, else indicate base64 length
168
169
  if (decoded.json !== undefined) {
169
170
  if (options.raw) {
170
- console.log(border_draw(JSON.stringify(decoded.json, null, 2)));
171
+ chiliLog(border_draw(JSON.stringify(decoded.json, null, 2)));
171
172
  return;
172
173
  }
173
174
  const pretty = this.pacsResult_renderPretty(decoded.json);
174
175
  if (pretty) {
175
- console.log(border_draw(pretty));
176
+ chiliLog(border_draw(pretty));
176
177
  }
177
178
  else {
178
- console.log(border_draw(JSON.stringify(decoded.json, null, 2)));
179
+ chiliLog(border_draw(JSON.stringify(decoded.json, null, 2)));
179
180
  }
180
181
  }
181
182
  else if (decoded.text) {
182
- console.log(border_draw(decoded.text));
183
+ chiliLog(border_draw(decoded.text));
183
184
  }
184
185
  else {
185
186
  const len = decoded.raw.length;
186
- console.log(border_draw(`Decoded payload available (base64 length ${len}), but not printable.`));
187
+ chiliLog(border_draw(`Decoded payload available (base64 length ${len}), but not printable.`));
187
188
  }
188
189
  });
189
190
  }
@@ -6,6 +6,7 @@
6
6
  import { errorStack_getAllOfType, } from "@fnndsc/cumin";
7
7
  import { pacsRetrieve_create, pacsRetrieve_delete, pacsRetrieve_statusForQuery, } from "@fnndsc/salsa";
8
8
  import { border_draw } from "../screen/screen.js";
9
+ import { chiliLog } from "../screen/output.js";
9
10
  /**
10
11
  * Handler for PACS retrieve commands.
11
12
  */
@@ -52,17 +53,17 @@ export class PACSRetrieveGroupHandler {
52
53
  async retrieve_pull(queryIdStr) {
53
54
  const queryId = Number(queryIdStr);
54
55
  if (Number.isNaN(queryId)) {
55
- console.log(border_draw("Query ID must be a number."));
56
+ chiliLog(border_draw("Query ID must be a number."));
56
57
  return;
57
58
  }
58
59
  const result = await pacsRetrieve_create(queryId);
59
60
  if (!result.ok) {
60
61
  const errors = errorStack_getAllOfType("error");
61
62
  if (errors.length) {
62
- errors.forEach((msg) => console.log(border_draw(msg)));
63
+ errors.forEach((msg) => chiliLog(border_draw(msg)));
63
64
  }
64
65
  else {
65
- console.log(border_draw(`Failed to create PACS retrieve for query ${queryId}.`));
66
+ chiliLog(border_draw(`Failed to create PACS retrieve for query ${queryId}.`));
66
67
  }
67
68
  return;
68
69
  }
@@ -73,7 +74,7 @@ export class PACSRetrieveGroupHandler {
73
74
  `query=${queryId}`,
74
75
  `status=${record.status || "created"}`,
75
76
  ].join(" ");
76
- console.log(border_draw(msg));
77
+ chiliLog(border_draw(msg));
77
78
  }
78
79
  /**
79
80
  * Handle the 'report' command - show detailed status.
@@ -83,23 +84,23 @@ export class PACSRetrieveGroupHandler {
83
84
  async retrieve_report(queryIdStr) {
84
85
  const queryId = Number(queryIdStr);
85
86
  if (Number.isNaN(queryId)) {
86
- console.log(border_draw("Query ID must be a number."));
87
+ chiliLog(border_draw("Query ID must be a number."));
87
88
  return;
88
89
  }
89
90
  const result = await pacsRetrieve_statusForQuery(queryId);
90
91
  if (!result.ok) {
91
92
  const errors = errorStack_getAllOfType("error");
92
93
  if (errors.length) {
93
- errors.forEach((msg) => console.log(border_draw(msg)));
94
+ errors.forEach((msg) => chiliLog(border_draw(msg)));
94
95
  }
95
96
  else {
96
- console.log(border_draw(`Failed to generate status report for query ${queryId}.`));
97
+ chiliLog(border_draw(`Failed to generate status report for query ${queryId}.`));
97
98
  }
98
99
  return;
99
100
  }
100
101
  const report = result.value;
101
102
  const rendered = this.report_render(report);
102
- console.log(border_draw(rendered));
103
+ chiliLog(border_draw(rendered));
103
104
  }
104
105
  /**
105
106
  * Handle the 'cancel' command - delete a retrieve.
@@ -109,21 +110,21 @@ export class PACSRetrieveGroupHandler {
109
110
  async retrieve_cancel(retrieveIdStr) {
110
111
  const retrieveId = Number(retrieveIdStr);
111
112
  if (Number.isNaN(retrieveId)) {
112
- console.log(border_draw("Retrieve ID must be a number."));
113
+ chiliLog(border_draw("Retrieve ID must be a number."));
113
114
  return;
114
115
  }
115
116
  const result = await pacsRetrieve_delete(retrieveId);
116
117
  if (!result.ok) {
117
118
  const errors = errorStack_getAllOfType("error");
118
119
  if (errors.length) {
119
- errors.forEach((msg) => console.log(border_draw(msg)));
120
+ errors.forEach((msg) => chiliLog(border_draw(msg)));
120
121
  }
121
122
  else {
122
- console.log(border_draw(`Failed to cancel PACS retrieve ${retrieveId}.`));
123
+ chiliLog(border_draw(`Failed to cancel PACS retrieve ${retrieveId}.`));
123
124
  }
124
125
  return;
125
126
  }
126
- console.log(border_draw(`PACS retrieve ${retrieveId} cancelled.`));
127
+ chiliLog(border_draw(`PACS retrieve ${retrieveId} cancelled.`));
127
128
  }
128
129
  /**
129
130
  * Determine overall completion status from series statuses.
@@ -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")
@@ -13,16 +13,27 @@ export declare class PluginContextGroupHandler {
13
13
  private baseGroupHandler;
14
14
  private controller;
15
15
  readonly assetName: string;
16
+ private readonly id?;
16
17
  private constructor();
17
18
  /**
18
- * Factory method to create a new PluginContextGroupHandler instance.
19
+ * Creates a PluginContextGroupHandler for an asset type. This performs no
20
+ * network work and resolves no plugin context — command registration needs
21
+ * only the asset name; the controller is resolved lazily on first action (see
22
+ * {@link controller_ensure}).
19
23
  *
20
- * @param assetName - The type of plugin context to handle ('computesofplugin', 'instancesofplugin', 'parametersofplugin').
21
- * @param id - Optional plugin ID. Defaults to current ChRIS plugin context.
22
- * @returns A Promise resolving to a new PluginContextGroupHandler instance.
23
- * @throws InitializationError if an unsupported asset type is provided.
24
+ * @param assetName - The type of plugin context ('computesofplugin', 'instancesofplugin', 'parametersofplugin').
25
+ * @param id - Optional plugin ID the context binds to; defaults to the current plugin context.
26
+ * @returns A new PluginContextGroupHandler instance.
24
27
  */
25
- static handler_create(assetName: string, id?: number | null): Promise<PluginContextGroupHandler>;
28
+ static handler_create(assetName: string, id?: number | null): PluginContextGroupHandler;
29
+ /**
30
+ * Lazily resolves and memoizes the plugin-context controller against the bound
31
+ * plugin id (or the current ChRIS context), wiring it into the
32
+ * registration-only base handler so its actions can read the context.
33
+ *
34
+ * @returns The controller, or null if the context cannot be resolved here.
35
+ */
36
+ private controller_ensure;
26
37
  /**
27
38
  * Lists plugin parameters in a "man page" style format.
28
39
  * This is a specialized view for parameters that differs from the standard table view.