@fnndsc/chili 3.3.0 → 3.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/fs/download.d.ts +19 -4
- package/dist/commands/fs/download.js +71 -62
- package/dist/commands/fs/upload.d.ts +20 -4
- package/dist/commands/fs/upload.js +42 -31
- package/dist/handlers/baseGroupHandler.d.ts +10 -1
- package/dist/handlers/baseGroupHandler.js +26 -2
- package/dist/plugins/pluginGroupHandler.d.ts +18 -0
- package/dist/plugins/pluginGroupHandler.js +40 -1
- package/dist/screen/screen.d.ts +13 -0
- package/dist/screen/screen.js +44 -0
- package/dist/views/pluginParameters.d.ts +10 -0
- package/dist/views/pluginParameters.js +24 -17
- package/docs/pacs.adoc +3 -3
- package/package.json +1 -1
|
@@ -13,13 +13,28 @@ export interface DownloadSummary {
|
|
|
13
13
|
duration: number;
|
|
14
14
|
speed: number;
|
|
15
15
|
}
|
|
16
|
+
/** Download progress fact emitted to callers that want to render progress. */
|
|
17
|
+
export interface DownloadProgressEvent {
|
|
18
|
+
operation: "download";
|
|
19
|
+
kind: "transfer";
|
|
20
|
+
phase: "scanning" | "transferring" | "complete" | "failed";
|
|
21
|
+
label?: string;
|
|
22
|
+
current?: number;
|
|
23
|
+
total?: number;
|
|
24
|
+
percent?: number;
|
|
25
|
+
unit?: "files" | "bytes";
|
|
26
|
+
status?: "running" | "done" | "error";
|
|
27
|
+
}
|
|
28
|
+
/** Options for download execution. */
|
|
29
|
+
export interface DownloadOptions {
|
|
30
|
+
force?: boolean;
|
|
31
|
+
onProgress?: (event: DownloadProgressEvent) => void;
|
|
32
|
+
}
|
|
16
33
|
/**
|
|
17
|
-
* Downloads files from ChRIS
|
|
34
|
+
* Downloads files from ChRIS and optionally emits structured progress events.
|
|
18
35
|
* @param chrisPath - Remote ChRIS path (file or directory).
|
|
19
36
|
* @param localPath - Local filesystem path.
|
|
20
37
|
* @param options - Download options.
|
|
21
38
|
* @returns Promise<DownloadSummary> with download statistics.
|
|
22
39
|
*/
|
|
23
|
-
export declare function files_downloadWithProgress(chrisPath: string, localPath: string, options?:
|
|
24
|
-
force?: boolean;
|
|
25
|
-
}): Promise<DownloadSummary>;
|
|
40
|
+
export declare function files_downloadWithProgress(chrisPath: string, localPath: string, options?: DownloadOptions): Promise<DownloadSummary>;
|
|
@@ -6,10 +6,9 @@ import { fileContent_getBinaryStream, files_listRecursive } from "@fnndsc/salsa"
|
|
|
6
6
|
import { path_resolveChrisFs } from "../../utils/cli.js";
|
|
7
7
|
import fs from "fs";
|
|
8
8
|
import path from "path";
|
|
9
|
-
import cliProgress from "cli-progress";
|
|
10
9
|
import chalk from "chalk";
|
|
11
10
|
import { pipeline } from "stream/promises";
|
|
12
|
-
import { bytes_format
|
|
11
|
+
import { bytes_format } from "./upload.js";
|
|
13
12
|
import { prompt_confirmOrThrow } from "../../utils/input_format.js";
|
|
14
13
|
export { bytes_format };
|
|
15
14
|
/**
|
|
@@ -36,7 +35,7 @@ async function chrisPath_isDirectory(chrisPath) {
|
|
|
36
35
|
}
|
|
37
36
|
}
|
|
38
37
|
/**
|
|
39
|
-
* Downloads files from ChRIS
|
|
38
|
+
* Downloads files from ChRIS and optionally emits structured progress events.
|
|
40
39
|
* @param chrisPath - Remote ChRIS path (file or directory).
|
|
41
40
|
* @param localPath - Local filesystem path.
|
|
42
41
|
* @param options - Download options.
|
|
@@ -93,61 +92,66 @@ export async function files_downloadWithProgress(chrisPath, localPath, options =
|
|
|
93
92
|
// Ensure parent directory exists
|
|
94
93
|
const parentDir = path.dirname(finalLocalPath);
|
|
95
94
|
await directory_ensureExists(parentDir);
|
|
96
|
-
const showProgress = !!process.stdout.isTTY;
|
|
97
|
-
const progressBar = typeof size === "number" && showProgress
|
|
98
|
-
? new cliProgress.SingleBar({
|
|
99
|
-
format: "Downloading [{bar}] {percentage}% | ETA: {etaHuman} | Rate: {rate} | {bytes}/{totalBytes}",
|
|
100
|
-
}, cliProgress.Presets.shades_classic)
|
|
101
|
-
: null;
|
|
102
|
-
if (progressBar) {
|
|
103
|
-
progressBar.start(size, 0, {
|
|
104
|
-
etaHuman: "--",
|
|
105
|
-
rate: "--",
|
|
106
|
-
bytes: bytes_format(0),
|
|
107
|
-
totalBytes: bytes_format(size),
|
|
108
|
-
});
|
|
109
|
-
}
|
|
110
95
|
let transferred = 0;
|
|
111
96
|
const writeStream = fs.createWriteStream(finalLocalPath);
|
|
112
97
|
stream.on("data", (chunk) => {
|
|
113
98
|
transferred += chunk.length;
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
}
|
|
99
|
+
options.onProgress?.({
|
|
100
|
+
operation: "download",
|
|
101
|
+
kind: "transfer",
|
|
102
|
+
phase: "transferring",
|
|
103
|
+
label: `Downloading ${path.basename(finalLocalPath)}`,
|
|
104
|
+
current: transferred,
|
|
105
|
+
total: size,
|
|
106
|
+
percent: typeof size === "number" && size > 0 ? (Math.min(transferred, size) / size) * 100 : undefined,
|
|
107
|
+
unit: "bytes",
|
|
108
|
+
status: "running",
|
|
109
|
+
});
|
|
126
110
|
});
|
|
127
111
|
await pipeline(stream, writeStream);
|
|
128
|
-
if (progressBar) {
|
|
129
|
-
progressBar.update(size || transferred);
|
|
130
|
-
progressBar.stop();
|
|
131
|
-
}
|
|
132
112
|
summary.totalFiles = 1;
|
|
133
113
|
summary.transferredCount = 1;
|
|
134
114
|
summary.transferSize = transferred;
|
|
135
115
|
summary.endTime = Date.now();
|
|
136
116
|
summary.duration = (summary.endTime - summary.startTime) / 1000;
|
|
137
117
|
summary.speed = summary.duration > 0 ? summary.transferSize / summary.duration : 0;
|
|
118
|
+
options.onProgress?.({
|
|
119
|
+
operation: "download",
|
|
120
|
+
kind: "transfer",
|
|
121
|
+
phase: "complete",
|
|
122
|
+
label: "Download complete",
|
|
123
|
+
current: transferred,
|
|
124
|
+
total: size,
|
|
125
|
+
percent: typeof size === "number" && size > 0 ? 100 : undefined,
|
|
126
|
+
unit: "bytes",
|
|
127
|
+
status: "done",
|
|
128
|
+
});
|
|
138
129
|
return summary;
|
|
139
130
|
}
|
|
140
131
|
catch (error) {
|
|
141
132
|
summary.failedCount = 1;
|
|
133
|
+
options.onProgress?.({
|
|
134
|
+
operation: "download",
|
|
135
|
+
kind: "transfer",
|
|
136
|
+
phase: "failed",
|
|
137
|
+
label: "Download failed",
|
|
138
|
+
status: "error",
|
|
139
|
+
});
|
|
142
140
|
throw error;
|
|
143
141
|
}
|
|
144
142
|
}
|
|
145
143
|
// Download directory
|
|
146
144
|
console.log(chalk.cyan("Scanning files to download..."));
|
|
145
|
+
options.onProgress?.({
|
|
146
|
+
operation: "download",
|
|
147
|
+
kind: "transfer",
|
|
148
|
+
phase: "scanning",
|
|
149
|
+
label: "Scanning files to download",
|
|
150
|
+
status: "running",
|
|
151
|
+
});
|
|
147
152
|
const items = await files_listRecursive(resolvedChris);
|
|
148
153
|
const files = items.filter((item) => item.type === 'file');
|
|
149
154
|
summary.totalFiles = files.length;
|
|
150
|
-
const totalSize = files.reduce((sum, f) => sum + (f.size || 0), 0);
|
|
151
155
|
// Apply Unix semantics: trailing slash determines merge behavior
|
|
152
156
|
const targetDir = resolvedChris.endsWith('/')
|
|
153
157
|
? localPath // Merge contents
|
|
@@ -169,20 +173,17 @@ export async function files_downloadWithProgress(chrisPath, localPath, options =
|
|
|
169
173
|
}
|
|
170
174
|
// Ensure target directory exists
|
|
171
175
|
await directory_ensureExists(targetDir);
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
:
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
rate: "--",
|
|
184
|
-
});
|
|
185
|
-
}
|
|
176
|
+
options.onProgress?.({
|
|
177
|
+
operation: "download",
|
|
178
|
+
kind: "transfer",
|
|
179
|
+
phase: "transferring",
|
|
180
|
+
label: "Downloading files",
|
|
181
|
+
current: 0,
|
|
182
|
+
total: files.length,
|
|
183
|
+
percent: files.length === 0 ? 100 : 0,
|
|
184
|
+
unit: "files",
|
|
185
|
+
status: "running",
|
|
186
|
+
});
|
|
186
187
|
// Download each file
|
|
187
188
|
for (const [index, file] of files.entries()) {
|
|
188
189
|
try {
|
|
@@ -213,23 +214,31 @@ export async function files_downloadWithProgress(chrisPath, localPath, options =
|
|
|
213
214
|
const msg = error instanceof Error ? error.message : String(error);
|
|
214
215
|
console.log(chalk.red(`\nError downloading ${file.path}: ${msg}`));
|
|
215
216
|
}
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
if (progressBar) {
|
|
229
|
-
progressBar.stop();
|
|
217
|
+
options.onProgress?.({
|
|
218
|
+
operation: "download",
|
|
219
|
+
kind: "transfer",
|
|
220
|
+
phase: "transferring",
|
|
221
|
+
label: "Downloading files",
|
|
222
|
+
current: index + 1,
|
|
223
|
+
total: files.length,
|
|
224
|
+
percent: files.length === 0 ? 100 : ((index + 1) / files.length) * 100,
|
|
225
|
+
unit: "files",
|
|
226
|
+
status: "running",
|
|
227
|
+
});
|
|
230
228
|
}
|
|
231
229
|
summary.endTime = Date.now();
|
|
232
230
|
summary.duration = (summary.endTime - summary.startTime) / 1000;
|
|
233
231
|
summary.speed = summary.duration > 0 ? summary.transferSize / summary.duration : 0;
|
|
232
|
+
options.onProgress?.({
|
|
233
|
+
operation: "download",
|
|
234
|
+
kind: "transfer",
|
|
235
|
+
phase: summary.failedCount === 0 ? "complete" : "failed",
|
|
236
|
+
label: "Download complete",
|
|
237
|
+
current: summary.transferredCount,
|
|
238
|
+
total: summary.totalFiles,
|
|
239
|
+
percent: summary.totalFiles === 0 ? 100 : (summary.transferredCount / summary.totalFiles) * 100,
|
|
240
|
+
unit: "files",
|
|
241
|
+
status: summary.failedCount === 0 ? "done" : "error",
|
|
242
|
+
});
|
|
234
243
|
return summary;
|
|
235
244
|
}
|
|
@@ -20,6 +20,23 @@ export interface UploadSummary {
|
|
|
20
20
|
speed: number;
|
|
21
21
|
actualTargetPath: string;
|
|
22
22
|
}
|
|
23
|
+
/** Upload progress fact emitted to callers that want to render progress. */
|
|
24
|
+
export interface UploadProgressEvent {
|
|
25
|
+
operation: "upload";
|
|
26
|
+
kind: "transfer";
|
|
27
|
+
phase: "scanning" | "transferring" | "complete" | "failed";
|
|
28
|
+
label?: string;
|
|
29
|
+
current?: number;
|
|
30
|
+
total?: number;
|
|
31
|
+
percent?: number;
|
|
32
|
+
unit?: "files";
|
|
33
|
+
status?: "running" | "done" | "error";
|
|
34
|
+
}
|
|
35
|
+
/** Options for upload execution. */
|
|
36
|
+
export interface UploadOptions {
|
|
37
|
+
force?: boolean;
|
|
38
|
+
onProgress?: (event: UploadProgressEvent) => void;
|
|
39
|
+
}
|
|
23
40
|
/**
|
|
24
41
|
* Formats bytes into human-readable format.
|
|
25
42
|
* @param bytes - Number of bytes.
|
|
@@ -37,14 +54,13 @@ export declare function eta_format(seconds: number | null): string;
|
|
|
37
54
|
*/
|
|
38
55
|
export declare function rate_format(rateBytesPerSec: number): string;
|
|
39
56
|
/**
|
|
40
|
-
* Uploads files
|
|
57
|
+
* Uploads files and optionally emits structured progress events.
|
|
41
58
|
* @param localPath - Local path (file or directory).
|
|
42
59
|
* @param remotePath - Remote ChRIS path.
|
|
60
|
+
* @param options - Upload options, including an optional progress callback.
|
|
43
61
|
* @returns Promise<UploadSummary> with upload statistics.
|
|
44
62
|
*/
|
|
45
|
-
export declare function files_uploadWithProgress(localPath: string, remotePath: string, options?:
|
|
46
|
-
force?: boolean;
|
|
47
|
-
}): Promise<UploadSummary>;
|
|
63
|
+
export declare function files_uploadWithProgress(localPath: string, remotePath: string, options?: UploadOptions): Promise<UploadSummary>;
|
|
48
64
|
/**
|
|
49
65
|
* Uploads a local file or directory to ChRIS.
|
|
50
66
|
*
|
|
@@ -7,7 +7,6 @@ import { chrisIO } from "@fnndsc/cumin";
|
|
|
7
7
|
import { path_resolveChrisFs } from "../../utils/cli.js";
|
|
8
8
|
import fs from "fs";
|
|
9
9
|
import path from "path";
|
|
10
|
-
import cliProgress from "cli-progress";
|
|
11
10
|
import chalk from "chalk";
|
|
12
11
|
import { prompt_confirmOrThrow } from "../../utils/input_format.js";
|
|
13
12
|
/**
|
|
@@ -101,17 +100,24 @@ async function localFiles_scan(localPath, remotePath) {
|
|
|
101
100
|
return files;
|
|
102
101
|
}
|
|
103
102
|
/**
|
|
104
|
-
* Uploads files
|
|
103
|
+
* Uploads files and optionally emits structured progress events.
|
|
105
104
|
* @param localPath - Local path (file or directory).
|
|
106
105
|
* @param remotePath - Remote ChRIS path.
|
|
106
|
+
* @param options - Upload options, including an optional progress callback.
|
|
107
107
|
* @returns Promise<UploadSummary> with upload statistics.
|
|
108
108
|
*/
|
|
109
109
|
export async function files_uploadWithProgress(localPath, remotePath, options = {}) {
|
|
110
110
|
const resolvedRemote = await path_resolveChrisFs(remotePath, {});
|
|
111
111
|
// Scan files
|
|
112
112
|
console.log(chalk.cyan("Scanning files to upload..."));
|
|
113
|
+
options.onProgress?.({
|
|
114
|
+
operation: "upload",
|
|
115
|
+
kind: "transfer",
|
|
116
|
+
phase: "scanning",
|
|
117
|
+
label: "Scanning files to upload",
|
|
118
|
+
status: "running",
|
|
119
|
+
});
|
|
113
120
|
const fileList = await localFiles_scan(localPath, resolvedRemote);
|
|
114
|
-
const totalSize = fileList.reduce((sum, f) => sum + f.size, 0);
|
|
115
121
|
// Determine actual target path (where files will be uploaded)
|
|
116
122
|
const stats = await fs.promises.stat(localPath);
|
|
117
123
|
let actualTarget = resolvedRemote;
|
|
@@ -140,20 +146,6 @@ export async function files_uploadWithProgress(localPath, remotePath, options =
|
|
|
140
146
|
else {
|
|
141
147
|
// Force mode: if target exists and is a directory, no prompt. If it's a file and we're uploading a single file, proceed.
|
|
142
148
|
}
|
|
143
|
-
const showProgress = !!process.stdout.isTTY;
|
|
144
|
-
const progressBar = showProgress
|
|
145
|
-
? new cliProgress.SingleBar({
|
|
146
|
-
format: "Transferring [{bar}] {percentage}% | ETA: {etaHuman} | Rate: {rate} | {value}/{total} files | {bytes}/{totalBytes}",
|
|
147
|
-
}, cliProgress.Presets.shades_classic)
|
|
148
|
-
: null;
|
|
149
|
-
if (progressBar) {
|
|
150
|
-
progressBar.start(fileList.length, 0, {
|
|
151
|
-
bytes: "0 B",
|
|
152
|
-
totalBytes: bytes_format(totalSize),
|
|
153
|
-
etaHuman: "--",
|
|
154
|
-
rate: "--",
|
|
155
|
-
});
|
|
156
|
-
}
|
|
157
149
|
// Upload files
|
|
158
150
|
const summary = {
|
|
159
151
|
startTime: Date.now(),
|
|
@@ -166,6 +158,17 @@ export async function files_uploadWithProgress(localPath, remotePath, options =
|
|
|
166
158
|
speed: 0,
|
|
167
159
|
actualTargetPath: actualTarget,
|
|
168
160
|
};
|
|
161
|
+
options.onProgress?.({
|
|
162
|
+
operation: "upload",
|
|
163
|
+
kind: "transfer",
|
|
164
|
+
phase: "transferring",
|
|
165
|
+
label: "Uploading files",
|
|
166
|
+
current: 0,
|
|
167
|
+
total: fileList.length,
|
|
168
|
+
percent: fileList.length === 0 ? 100 : 0,
|
|
169
|
+
unit: "files",
|
|
170
|
+
status: "running",
|
|
171
|
+
});
|
|
169
172
|
for (const [index, file] of fileList.entries()) {
|
|
170
173
|
try {
|
|
171
174
|
const fileContent = await fs.promises.readFile(file.hostPath);
|
|
@@ -188,24 +191,32 @@ export async function files_uploadWithProgress(localPath, remotePath, options =
|
|
|
188
191
|
summary.failedCount++;
|
|
189
192
|
console.log(chalk.red(`Error uploading ${file.hostPath}: ${error instanceof Error ? error.message : String(error)}`));
|
|
190
193
|
}
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
if (progressBar) {
|
|
204
|
-
progressBar.stop();
|
|
194
|
+
options.onProgress?.({
|
|
195
|
+
operation: "upload",
|
|
196
|
+
kind: "transfer",
|
|
197
|
+
phase: "transferring",
|
|
198
|
+
label: "Uploading files",
|
|
199
|
+
current: index + 1,
|
|
200
|
+
total: fileList.length,
|
|
201
|
+
percent: fileList.length === 0 ? 100 : ((index + 1) / fileList.length) * 100,
|
|
202
|
+
unit: "files",
|
|
203
|
+
status: "running",
|
|
204
|
+
});
|
|
205
205
|
}
|
|
206
206
|
summary.endTime = Date.now();
|
|
207
207
|
summary.duration = (summary.endTime - summary.startTime) / 1000; // seconds
|
|
208
208
|
summary.speed = summary.transferSize / summary.duration; // bytes per second
|
|
209
|
+
options.onProgress?.({
|
|
210
|
+
operation: "upload",
|
|
211
|
+
kind: "transfer",
|
|
212
|
+
phase: summary.failedCount === 0 ? "complete" : "failed",
|
|
213
|
+
label: "Upload complete",
|
|
214
|
+
current: summary.transferredCount,
|
|
215
|
+
total: summary.totalFiles,
|
|
216
|
+
percent: summary.totalFiles === 0 ? 100 : (summary.transferredCount / summary.totalFiles) * 100,
|
|
217
|
+
unit: "files",
|
|
218
|
+
status: summary.failedCount === 0 ? "done" : "error",
|
|
219
|
+
});
|
|
209
220
|
return summary;
|
|
210
221
|
}
|
|
211
222
|
/**
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* @module
|
|
5
5
|
*/
|
|
6
6
|
import { Command } from "commander";
|
|
7
|
-
import { ChRISPluginGroup, ChRISFeedGroup, ChRISEmbeddedResourceGroup, ChRISPACSGroup } from "@fnndsc/cumin";
|
|
7
|
+
import { ChRISPluginGroup, ChRISFeedGroup, ChRISEmbeddedResourceGroup, ChRISPACSGroup, type CommandEnvelope } from "@fnndsc/cumin";
|
|
8
8
|
import { CLIoptions } from "../utils/cli.js";
|
|
9
9
|
import { TableOptions } from "../screen/screen.js";
|
|
10
10
|
/**
|
|
@@ -26,6 +26,15 @@ export declare class BaseGroupHandler {
|
|
|
26
26
|
* Lists available fields for the current ChRIS resource type.
|
|
27
27
|
*/
|
|
28
28
|
resourceFields_list(): Promise<void>;
|
|
29
|
+
/**
|
|
30
|
+
* Renders available fields for the current ChRIS resource type as an envelope.
|
|
31
|
+
*
|
|
32
|
+
* Sans-I/O counterpart to {@link resourceFields_list} — returns the rendered
|
|
33
|
+
* text instead of printing it, so hosted surfaces can carry it over the wire.
|
|
34
|
+
*
|
|
35
|
+
* @returns An envelope carrying the field listing or an error.
|
|
36
|
+
*/
|
|
37
|
+
resourceFields_render(): Promise<CommandEnvelope>;
|
|
29
38
|
/**
|
|
30
39
|
* Prompts the user for confirmation before performing an operation.
|
|
31
40
|
*
|
|
@@ -4,10 +4,10 @@
|
|
|
4
4
|
* @module
|
|
5
5
|
*/
|
|
6
6
|
import { Command } from "commander";
|
|
7
|
-
import { ChRISFeedGroup, record_extract, errorStack, } from "@fnndsc/cumin";
|
|
7
|
+
import { ChRISFeedGroup, record_extract, errorStack, envelope_ok, envelope_error, } from "@fnndsc/cumin";
|
|
8
8
|
import { options_toParams } from "../utils/cli.js";
|
|
9
9
|
import { resourceColumns_removeDuplicates } from "../utils/resourceData.js";
|
|
10
|
-
import { table_display, border_draw } from "../screen/screen.js";
|
|
10
|
+
import { table_display, table_render, border_draw } from "../screen/screen.js";
|
|
11
11
|
import * as readline from "readline";
|
|
12
12
|
/**
|
|
13
13
|
* Base handler for groups of ChRIS resources.
|
|
@@ -128,6 +128,30 @@ export class BaseGroupHandler {
|
|
|
128
128
|
console.log(errorStack.stack_search(this.assetName)[0]);
|
|
129
129
|
}
|
|
130
130
|
}
|
|
131
|
+
/**
|
|
132
|
+
* Renders available fields for the current ChRIS resource type as an envelope.
|
|
133
|
+
*
|
|
134
|
+
* Sans-I/O counterpart to {@link resourceFields_list} — returns the rendered
|
|
135
|
+
* text instead of printing it, so hosted surfaces can carry it over the wire.
|
|
136
|
+
*
|
|
137
|
+
* @returns An envelope carrying the field listing or an error.
|
|
138
|
+
*/
|
|
139
|
+
async resourceFields_render() {
|
|
140
|
+
try {
|
|
141
|
+
const results = await this.chrisObject.asset.resourceFields_get();
|
|
142
|
+
if (!results) {
|
|
143
|
+
return envelope_error('', undefined, `An error occurred while fetching resource fields for ${this.assetName}.\n`);
|
|
144
|
+
}
|
|
145
|
+
if (results.fields.length === 0) {
|
|
146
|
+
return envelope_ok(`No resource fields found for ${this.assetName}.\n`);
|
|
147
|
+
}
|
|
148
|
+
return envelope_ok(table_render(results.fields, ["fields"]));
|
|
149
|
+
}
|
|
150
|
+
catch (error) {
|
|
151
|
+
const stacked = errorStack.stack_search(this.assetName)[0];
|
|
152
|
+
return envelope_error('', undefined, `${stacked ?? String(error)}\n`);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
131
155
|
/**
|
|
132
156
|
* Prompts the user for confirmation before performing an operation.
|
|
133
157
|
*
|
|
@@ -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,8 @@
|
|
|
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";
|
|
10
11
|
class InitializationError extends Error {
|
|
11
12
|
constructor(message) {
|
|
12
13
|
super(message);
|
|
@@ -76,6 +77,44 @@ export class PluginContextGroupHandler {
|
|
|
76
77
|
await this.baseGroupHandler.resourceFields_list();
|
|
77
78
|
}
|
|
78
79
|
}
|
|
80
|
+
/**
|
|
81
|
+
* Renders plugin parameters in "man page" style as an envelope.
|
|
82
|
+
*
|
|
83
|
+
* Sans-I/O counterpart to {@link parameters_listMan}.
|
|
84
|
+
*
|
|
85
|
+
* @param options - CLI options for filtering.
|
|
86
|
+
* @returns An envelope carrying the rendered parameters or an error.
|
|
87
|
+
*/
|
|
88
|
+
async parameters_listManRender(options) {
|
|
89
|
+
try {
|
|
90
|
+
const asset = this.controller.chrisObject.asset;
|
|
91
|
+
if (!asset || typeof asset.resources_listAndFilterByOptions !== 'function') {
|
|
92
|
+
return envelope_error('', undefined, "Underlying resource does not support listing.\n");
|
|
93
|
+
}
|
|
94
|
+
const params = options_toParams(options);
|
|
95
|
+
const results = await asset.resources_listAndFilterByOptions(params);
|
|
96
|
+
if (results) {
|
|
97
|
+
return envelope_ok(`${pluginParameters_manRender(results)}\n`);
|
|
98
|
+
}
|
|
99
|
+
return envelope_ok("No parameters found.\n");
|
|
100
|
+
}
|
|
101
|
+
catch (error) {
|
|
102
|
+
return envelope_error('', undefined, `Error listing parameters: ${error}\n`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Renders available fields for the current plugin context resource as an envelope.
|
|
107
|
+
*
|
|
108
|
+
* Sans-I/O counterpart to {@link parameters_fieldsList}.
|
|
109
|
+
*
|
|
110
|
+
* @returns An envelope carrying the field listing.
|
|
111
|
+
*/
|
|
112
|
+
async parameters_fieldsRender() {
|
|
113
|
+
if (this.baseGroupHandler) {
|
|
114
|
+
return this.baseGroupHandler.resourceFields_render();
|
|
115
|
+
}
|
|
116
|
+
return envelope_ok('');
|
|
117
|
+
}
|
|
79
118
|
/**
|
|
80
119
|
* Sets up the Commander.js commands for plugin context group operations.
|
|
81
120
|
*
|
package/dist/screen/screen.d.ts
CHANGED
|
@@ -75,6 +75,19 @@ export declare function border_draw(text: string, borders?: Borders): string;
|
|
|
75
75
|
* @param options - Optional table display options.
|
|
76
76
|
* @returns The `TableContent` object or null on error.
|
|
77
77
|
*/
|
|
78
|
+
/**
|
|
79
|
+
* Renders a table to a string, the string form of {@link table_display}.
|
|
80
|
+
*
|
|
81
|
+
* Envelope-returning callers use this to return their output rather than
|
|
82
|
+
* printing it; table_display remains the printing form for callers not yet
|
|
83
|
+
* converted. Returns an empty string when there is no table to render.
|
|
84
|
+
*
|
|
85
|
+
* @param tableData - The rows to render.
|
|
86
|
+
* @param headers - Column headers.
|
|
87
|
+
* @param options - Table formatting options.
|
|
88
|
+
* @returns The rendered table (with a trailing newline), or an empty string.
|
|
89
|
+
*/
|
|
90
|
+
export declare function table_render(tableData: TableDataRow[] | string[][] | string[], headers: string[] | string, options?: TableOptions): string;
|
|
78
91
|
export declare function table_display(tableData: TableDataRow[] | string[][] | string[], headers: string[] | string, options?: TableOptions): TableContent | null;
|
|
79
92
|
/**
|
|
80
93
|
* Provides screen utilities for logging and table output.
|
package/dist/screen/screen.js
CHANGED
|
@@ -118,6 +118,50 @@ function firstColumnSettings_apply(columns) {
|
|
|
118
118
|
* @param options - Optional table display options.
|
|
119
119
|
* @returns The `TableContent` object or null on error.
|
|
120
120
|
*/
|
|
121
|
+
/**
|
|
122
|
+
* Renders a table to a string, the string form of {@link table_display}.
|
|
123
|
+
*
|
|
124
|
+
* Envelope-returning callers use this to return their output rather than
|
|
125
|
+
* printing it; table_display remains the printing form for callers not yet
|
|
126
|
+
* converted. Returns an empty string when there is no table to render.
|
|
127
|
+
*
|
|
128
|
+
* @param tableData - The rows to render.
|
|
129
|
+
* @param headers - Column headers.
|
|
130
|
+
* @param options - Table formatting options.
|
|
131
|
+
* @returns The rendered table (with a trailing newline), or an empty string.
|
|
132
|
+
*/
|
|
133
|
+
export function table_render(tableData, headers, options = {}) {
|
|
134
|
+
const { processedTableData, processedHeaders } = tableInput_process(tableData, headers);
|
|
135
|
+
const tableObj = tableContent_pack(processedTableData, processedHeaders);
|
|
136
|
+
if (!tableObj) {
|
|
137
|
+
return "";
|
|
138
|
+
}
|
|
139
|
+
const columns = tableObj.headers.map((_, index) => {
|
|
140
|
+
const existingCol = options.columns?.[index] || {};
|
|
141
|
+
return {
|
|
142
|
+
justification: "left",
|
|
143
|
+
...existingCol,
|
|
144
|
+
};
|
|
145
|
+
});
|
|
146
|
+
const updatedColumns = firstColumnSettings_apply(columns);
|
|
147
|
+
const tableOptions = {
|
|
148
|
+
...options,
|
|
149
|
+
head: processedHeaders,
|
|
150
|
+
columns: updatedColumns,
|
|
151
|
+
typeColors: {
|
|
152
|
+
string: "green",
|
|
153
|
+
number: "yellow",
|
|
154
|
+
boolean: "cyan",
|
|
155
|
+
object: "magenta",
|
|
156
|
+
},
|
|
157
|
+
};
|
|
158
|
+
let out = `${screen.table_output(processedTableData, tableOptions)}\n`;
|
|
159
|
+
if (options.pagination && options.pagination.shown < options.pagination.total) {
|
|
160
|
+
const { shown, total } = options.pagination;
|
|
161
|
+
out += `${chalk.dim(` ↓ showing ${shown} of ${total} · --all to fetch all · --limit <n> for page size`)}\n`;
|
|
162
|
+
}
|
|
163
|
+
return out;
|
|
164
|
+
}
|
|
121
165
|
export function table_display(tableData, headers, options = {}) {
|
|
122
166
|
const { processedTableData, processedHeaders } = tableInput_process(tableData, headers);
|
|
123
167
|
const tableObj = tableContent_pack(processedTableData, processedHeaders);
|
|
@@ -15,3 +15,13 @@ import { FilteredResourceData } from '@fnndsc/cumin';
|
|
|
15
15
|
* @param data - The filtered resource data containing plugin parameters.
|
|
16
16
|
*/
|
|
17
17
|
export declare function pluginParameters_renderMan(data: FilteredResourceData): void;
|
|
18
|
+
/**
|
|
19
|
+
* Builds the "man page" style rendering of plugin parameters as a string.
|
|
20
|
+
*
|
|
21
|
+
* Same format as {@link pluginParameters_renderMan} but returns the text
|
|
22
|
+
* instead of printing it, so hosted surfaces can carry it in an envelope.
|
|
23
|
+
*
|
|
24
|
+
* @param data - The filtered resource data containing plugin parameters.
|
|
25
|
+
* @returns The rendered parameter listing.
|
|
26
|
+
*/
|
|
27
|
+
export declare function pluginParameters_manRender(data: FilteredResourceData): string;
|
|
@@ -15,10 +15,22 @@ import chalk from 'chalk';
|
|
|
15
15
|
* @param data - The filtered resource data containing plugin parameters.
|
|
16
16
|
*/
|
|
17
17
|
export function pluginParameters_renderMan(data) {
|
|
18
|
+
console.log(pluginParameters_manRender(data));
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Builds the "man page" style rendering of plugin parameters as a string.
|
|
22
|
+
*
|
|
23
|
+
* Same format as {@link pluginParameters_renderMan} but returns the text
|
|
24
|
+
* instead of printing it, so hosted surfaces can carry it in an envelope.
|
|
25
|
+
*
|
|
26
|
+
* @param data - The filtered resource data containing plugin parameters.
|
|
27
|
+
* @returns The rendered parameter listing.
|
|
28
|
+
*/
|
|
29
|
+
export function pluginParameters_manRender(data) {
|
|
18
30
|
if (!data.tableData || data.tableData.length === 0) {
|
|
19
|
-
|
|
20
|
-
return;
|
|
31
|
+
return "No parameters found.";
|
|
21
32
|
}
|
|
33
|
+
const lines = [];
|
|
22
34
|
data.tableData.forEach((param) => {
|
|
23
35
|
// 1. Construct the first line: Flags and Value
|
|
24
36
|
let line1Parts = [];
|
|
@@ -52,28 +64,23 @@ export function pluginParameters_renderMan(data) {
|
|
|
52
64
|
line1Parts.push(chalk.gray(`(default: ${param.default})`));
|
|
53
65
|
}
|
|
54
66
|
if (line1Parts.length > 0) {
|
|
55
|
-
|
|
67
|
+
lines.push(line1Parts.join(' '));
|
|
56
68
|
}
|
|
57
|
-
else {
|
|
58
|
-
// Fallback if
|
|
59
|
-
//
|
|
60
|
-
|
|
61
|
-
// If name is 'v', it implies it should probably have a flag '-v'.
|
|
62
|
-
// If not, we might want to show '--v' anyway despite the rule, or '-v'.
|
|
63
|
-
// For safety, if line1 is empty but we have a name, show it as long flag to avoid invisible params.
|
|
64
|
-
if (param.name) {
|
|
65
|
-
console.log(chalk.bold(`--${param.name}`) + ' ' + chalk.gray('(implicit)'));
|
|
66
|
-
}
|
|
69
|
+
else if (param.name) {
|
|
70
|
+
// Fallback: if line1 is empty but we have a name, show it as a long flag
|
|
71
|
+
// to avoid invisible params.
|
|
72
|
+
lines.push(chalk.bold(`--${param.name}`) + ' ' + chalk.gray('(implicit)'));
|
|
67
73
|
}
|
|
68
74
|
// 2. Type line
|
|
69
75
|
if (param.type) {
|
|
70
|
-
|
|
76
|
+
lines.push(`Type: ${chalk.yellow(param.type)}`);
|
|
71
77
|
}
|
|
72
78
|
// 3. Help description
|
|
73
79
|
if (param.help) {
|
|
74
|
-
|
|
80
|
+
lines.push(chalk.white(param.help));
|
|
75
81
|
}
|
|
76
|
-
// Separator (
|
|
77
|
-
|
|
82
|
+
// Separator (blank line)
|
|
83
|
+
lines.push('');
|
|
78
84
|
});
|
|
85
|
+
return lines.join('\n');
|
|
79
86
|
}
|
package/docs/pacs.adoc
CHANGED
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
== PACS Queries
|
|
22
22
|
|
|
23
23
|
- **Create**: `chili pacsqueries create "<query>" [--title <title>] [--description <desc>] [--pacsserver <pacs>]`
|
|
24
|
-
- `<query>` can be JSON (`{"PatientID":"12345"}`) or comma-separated `key:value` pairs (`PatientID:12345,StudyDate:
|
|
24
|
+
- `<query>` can be JSON (`{"PatientID":"12345"}`) or comma-separated `key:value` pairs (`PatientID:12345,StudyDate:20240101`).
|
|
25
25
|
- On success prints: `Created PACS query id=<ID> status=<status> pacs=<pacs> title="<title>"`.
|
|
26
26
|
- **List**: `chili pacsqueries list [--pacsserver <pacs>] [--fields ...] [--table|--csv]`
|
|
27
27
|
- Filters to current/override PACS server. Use `fieldslist` to discover columns.
|
|
@@ -64,7 +64,7 @@
|
|
|
64
64
|
```bash
|
|
65
65
|
chili context set --pacsserver PACSDCM
|
|
66
66
|
chili pacsservers list --table
|
|
67
|
-
chili pacsqueries create "PatientID:
|
|
67
|
+
chili pacsqueries create "PatientID:7654321" --title "PID 7654321"
|
|
68
68
|
chili pacsqueries list --fields id,title,status,pacs_id,query
|
|
69
69
|
chili pacsqueries decode <id>
|
|
70
70
|
```
|
|
@@ -76,7 +76,7 @@ chili pacsqueries decode <id>
|
|
|
76
76
|
chili context set --pacsserver PACSDCM
|
|
77
77
|
|
|
78
78
|
# 2. Create query
|
|
79
|
-
chili pacsqueries create "PatientID:
|
|
79
|
+
chili pacsqueries create "PatientID:7654321" --title "Patient scan"
|
|
80
80
|
# Returns: Created PACS query id=123
|
|
81
81
|
|
|
82
82
|
# 3. See what's available on PACS
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fnndsc/chili",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.4.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",
|