@fnndsc/chili 3.3.0 → 3.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/dist/chefs/chefs.js +12 -11
  2. package/dist/commands/fs/download.d.ts +19 -4
  3. package/dist/commands/fs/download.js +75 -65
  4. package/dist/commands/fs/upload.d.ts +20 -4
  5. package/dist/commands/fs/upload.js +46 -34
  6. package/dist/commands/man/doc.js +3 -2
  7. package/dist/commands/plugins/add.js +35 -34
  8. package/dist/config/colorConfig.js +2 -1
  9. package/dist/connect/connectHandler.js +7 -6
  10. package/dist/context/contextCommand.js +3 -2
  11. package/dist/controllers/fileController.js +4 -3
  12. package/dist/controllers/pluginController.js +2 -1
  13. package/dist/feeds/feedHandler.js +19 -18
  14. package/dist/filesystem/fileGroupHandler.js +28 -27
  15. package/dist/filesystem/filesystemHandler.js +13 -11
  16. package/dist/filesystem/inodeCommand.js +2 -1
  17. package/dist/handlers/baseGroupHandler.d.ts +10 -1
  18. package/dist/handlers/baseGroupHandler.js +38 -13
  19. package/dist/index.js +3 -2
  20. package/dist/lfs/lfs.js +6 -4
  21. package/dist/man/man.js +4 -3
  22. package/dist/man/renderer.js +3 -2
  23. package/dist/pacs/pacsQueryHandler.js +14 -13
  24. package/dist/pacs/pacsRetrieveHandler.js +13 -12
  25. package/dist/path/pathCommand.js +23 -22
  26. package/dist/plugins/pluginGroupHandler.d.ts +18 -0
  27. package/dist/plugins/pluginGroupHandler.js +44 -4
  28. package/dist/plugins/pluginHandler.js +23 -22
  29. package/dist/run.d.ts +13 -0
  30. package/dist/run.js +28 -13
  31. package/dist/screen/output.d.ts +88 -0
  32. package/dist/screen/output.js +95 -0
  33. package/dist/screen/screen.d.ts +13 -0
  34. package/dist/screen/screen.js +53 -8
  35. package/dist/utils/admin_prompt.js +10 -9
  36. package/dist/utils/docker.js +10 -9
  37. package/dist/views/pluginParameters.d.ts +10 -0
  38. package/dist/views/pluginParameters.js +25 -17
  39. package/docs/pacs.adoc +3 -3
  40. package/package.json +1 -1
@@ -9,6 +9,7 @@ import { files_rm } from '../commands/fs/rm.js';
9
9
  import { files_cp } from '../commands/fs/cp.js';
10
10
  import { files_mv } from '../commands/fs/mv.js';
11
11
  import { mkdir_render, touch_render, upload_render, cat_render, rm_render, cp_render, mv_render } from '../views/fs.js';
12
+ import { chiliLog } from "../screen/output.js";
12
13
  /**
13
14
  * Lists directory contents.
14
15
  *
@@ -21,10 +22,10 @@ async function chefs_ls(options, pathStr = "") {
21
22
  return;
22
23
  }
23
24
  if (options.long) {
24
- console.log(long_render(items, { human: !!options.human }));
25
+ chiliLog(long_render(items, { human: !!options.human }));
25
26
  }
26
27
  else {
27
- console.log(grid_render(items));
28
+ chiliLog(grid_render(items));
28
29
  }
29
30
  }
30
31
  /**
@@ -36,7 +37,7 @@ async function chefs_ls(options, pathStr = "") {
36
37
  */
37
38
  async function chefs_cp(src, dest, options) {
38
39
  const success = await files_cp(src, dest, options);
39
- console.log(cp_render(src, dest, success));
40
+ chiliLog(cp_render(src, dest, success));
40
41
  }
41
42
  /**
42
43
  * Moves a file or directory.
@@ -47,7 +48,7 @@ async function chefs_cp(src, dest, options) {
47
48
  */
48
49
  async function chefs_mv(src, dest, _options) {
49
50
  const success = await files_mv(src, dest);
50
- console.log(mv_render(src, dest, success));
51
+ chiliLog(mv_render(src, dest, success));
51
52
  }
52
53
  /**
53
54
  * Creates a directory.
@@ -56,7 +57,7 @@ async function chefs_mv(src, dest, _options) {
56
57
  */
57
58
  async function chefs_mkdir(dirPath) {
58
59
  const success = await files_mkdir(dirPath);
59
- console.log(mkdir_render(dirPath, success));
60
+ chiliLog(mkdir_render(dirPath, success));
60
61
  }
61
62
  /**
62
63
  * Creates an empty file.
@@ -65,7 +66,7 @@ async function chefs_mkdir(dirPath) {
65
66
  */
66
67
  async function chefs_touch(filePath) {
67
68
  const success = await files_touch(filePath);
68
- console.log(touch_render(filePath, success));
69
+ chiliLog(touch_render(filePath, success));
69
70
  }
70
71
  /**
71
72
  * Uploads a local file or directory to ChRIS.
@@ -74,9 +75,9 @@ async function chefs_touch(filePath) {
74
75
  * @param remotePath - Remote ChRIS path.
75
76
  */
76
77
  async function chefs_upload(localPath, remotePath) {
77
- console.log(`Uploading ${localPath} to ${remotePath}...`);
78
+ chiliLog(`Uploading ${localPath} to ${remotePath}...`);
78
79
  const success = await files_upload(localPath, remotePath);
79
- console.log(upload_render(localPath, remotePath, success));
80
+ chiliLog(upload_render(localPath, remotePath, success));
80
81
  }
81
82
  /**
82
83
  * Changes the current working directory.
@@ -96,7 +97,7 @@ async function chefs_cd(path) {
96
97
  */
97
98
  async function chefs_pwd() {
98
99
  const current = await chrisContext.current_get(Context.ChRISfolder);
99
- console.log(current || "/");
100
+ chiliLog(current || "/");
100
101
  }
101
102
  /**
102
103
  * Displays file content.
@@ -106,7 +107,7 @@ async function chefs_pwd() {
106
107
  async function chefs_cat(filePath) {
107
108
  const result = await files_cat(filePath);
108
109
  const content = result.ok ? result.value : null;
109
- console.log(cat_render(content, filePath));
110
+ chiliLog(cat_render(content, filePath));
110
111
  }
111
112
  /**
112
113
  * Removes a file or directory.
@@ -116,7 +117,7 @@ async function chefs_cat(filePath) {
116
117
  */
117
118
  async function chefs_rm(targetPath, options) {
118
119
  const result = await files_rm(targetPath, options);
119
- console.log(rm_render(result));
120
+ chiliLog(rm_render(result));
120
121
  }
121
122
  /**
122
123
  * Sets up the 'chefs' command group in Commander.
@@ -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 with a progress bar display.
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,11 +6,11 @@ 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, eta_format, rate_format } from "./upload.js";
11
+ import { bytes_format } from "./upload.js";
13
12
  import { prompt_confirmOrThrow } from "../../utils/input_format.js";
13
+ import { chiliLog } from "../../screen/output.js";
14
14
  export { bytes_format };
15
15
  /**
16
16
  * Recursively creates a directory and all parent directories.
@@ -36,7 +36,7 @@ async function chrisPath_isDirectory(chrisPath) {
36
36
  }
37
37
  }
38
38
  /**
39
- * Downloads files from ChRIS with a progress bar display.
39
+ * Downloads files from ChRIS and optionally emits structured progress events.
40
40
  * @param chrisPath - Remote ChRIS path (file or directory).
41
41
  * @param localPath - Local filesystem path.
42
42
  * @param options - Download options.
@@ -93,61 +93,66 @@ export async function files_downloadWithProgress(chrisPath, localPath, options =
93
93
  // Ensure parent directory exists
94
94
  const parentDir = path.dirname(finalLocalPath);
95
95
  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
96
  let transferred = 0;
111
97
  const writeStream = fs.createWriteStream(finalLocalPath);
112
98
  stream.on("data", (chunk) => {
113
99
  transferred += chunk.length;
114
- if (progressBar) {
115
- const elapsedSeconds = Math.max(1e-6, (Date.now() - summary.startTime) / 1000);
116
- const rateCurrent = transferred / elapsedSeconds;
117
- const remainingBytes = Math.max(0, (size ?? transferred) - transferred);
118
- const etaSeconds = rateCurrent > 0 ? remainingBytes / rateCurrent : null;
119
- progressBar.update(Math.min(transferred, size || transferred), {
120
- etaHuman: eta_format(etaSeconds),
121
- rate: rate_format(rateCurrent),
122
- bytes: bytes_format(transferred),
123
- totalBytes: bytes_format(size ?? transferred),
124
- });
125
- }
100
+ options.onProgress?.({
101
+ operation: "download",
102
+ kind: "transfer",
103
+ phase: "transferring",
104
+ label: `Downloading ${path.basename(finalLocalPath)}`,
105
+ current: transferred,
106
+ total: size,
107
+ percent: typeof size === "number" && size > 0 ? (Math.min(transferred, size) / size) * 100 : undefined,
108
+ unit: "bytes",
109
+ status: "running",
110
+ });
126
111
  });
127
112
  await pipeline(stream, writeStream);
128
- if (progressBar) {
129
- progressBar.update(size || transferred);
130
- progressBar.stop();
131
- }
132
113
  summary.totalFiles = 1;
133
114
  summary.transferredCount = 1;
134
115
  summary.transferSize = transferred;
135
116
  summary.endTime = Date.now();
136
117
  summary.duration = (summary.endTime - summary.startTime) / 1000;
137
118
  summary.speed = summary.duration > 0 ? summary.transferSize / summary.duration : 0;
119
+ options.onProgress?.({
120
+ operation: "download",
121
+ kind: "transfer",
122
+ phase: "complete",
123
+ label: "Download complete",
124
+ current: transferred,
125
+ total: size,
126
+ percent: typeof size === "number" && size > 0 ? 100 : undefined,
127
+ unit: "bytes",
128
+ status: "done",
129
+ });
138
130
  return summary;
139
131
  }
140
132
  catch (error) {
141
133
  summary.failedCount = 1;
134
+ options.onProgress?.({
135
+ operation: "download",
136
+ kind: "transfer",
137
+ phase: "failed",
138
+ label: "Download failed",
139
+ status: "error",
140
+ });
142
141
  throw error;
143
142
  }
144
143
  }
145
144
  // Download directory
146
- console.log(chalk.cyan("Scanning files to download..."));
145
+ chiliLog(chalk.cyan("Scanning files to download..."));
146
+ options.onProgress?.({
147
+ operation: "download",
148
+ kind: "transfer",
149
+ phase: "scanning",
150
+ label: "Scanning files to download",
151
+ status: "running",
152
+ });
147
153
  const items = await files_listRecursive(resolvedChris);
148
154
  const files = items.filter((item) => item.type === 'file');
149
155
  summary.totalFiles = files.length;
150
- const totalSize = files.reduce((sum, f) => sum + (f.size || 0), 0);
151
156
  // Apply Unix semantics: trailing slash determines merge behavior
152
157
  const targetDir = resolvedChris.endsWith('/')
153
158
  ? localPath // Merge contents
@@ -169,27 +174,24 @@ export async function files_downloadWithProgress(chrisPath, localPath, options =
169
174
  }
170
175
  // Ensure target directory exists
171
176
  await directory_ensureExists(targetDir);
172
- const showProgress = !!process.stdout.isTTY;
173
- const progressBar = showProgress
174
- ? new cliProgress.SingleBar({
175
- format: "Downloading [{bar}] {percentage}% | ETA: {etaHuman} | Rate: {rate} | {value}/{total} files | {bytes}/{totalBytes}",
176
- }, cliProgress.Presets.shades_classic)
177
- : null;
178
- if (progressBar) {
179
- progressBar.start(files.length, 0, {
180
- bytes: bytes_format(0),
181
- totalBytes: bytes_format(totalSize),
182
- etaHuman: "--",
183
- rate: "--",
184
- });
185
- }
177
+ options.onProgress?.({
178
+ operation: "download",
179
+ kind: "transfer",
180
+ phase: "transferring",
181
+ label: "Downloading files",
182
+ current: 0,
183
+ total: files.length,
184
+ percent: files.length === 0 ? 100 : 0,
185
+ unit: "files",
186
+ status: "running",
187
+ });
186
188
  // Download each file
187
189
  for (const [index, file] of files.entries()) {
188
190
  try {
189
191
  const result = await fileContent_getBinaryStream(file.path);
190
192
  if (!result.ok) {
191
193
  summary.failedCount++;
192
- console.log(chalk.yellow(`\nFailed to download: ${file.path}`));
194
+ chiliLog(chalk.yellow(`\nFailed to download: ${file.path}`));
193
195
  continue;
194
196
  }
195
197
  const { stream, size } = result.value;
@@ -211,25 +213,33 @@ export async function files_downloadWithProgress(chrisPath, localPath, options =
211
213
  catch (error) {
212
214
  summary.failedCount++;
213
215
  const msg = error instanceof Error ? error.message : String(error);
214
- console.log(chalk.red(`\nError downloading ${file.path}: ${msg}`));
215
- }
216
- const elapsedSeconds = Math.max(1e-6, (Date.now() - summary.startTime) / 1000);
217
- const rateCurrent = summary.transferSize > 0 ? summary.transferSize / elapsedSeconds : 0;
218
- const remainingBytes = Math.max(0, totalSize - summary.transferSize);
219
- const etaSeconds = rateCurrent > 0 ? remainingBytes / rateCurrent : null;
220
- if (progressBar) {
221
- progressBar.update(index + 1, {
222
- bytes: bytes_format(summary.transferSize),
223
- etaHuman: eta_format(etaSeconds),
224
- rate: rate_format(rateCurrent),
225
- });
216
+ chiliLog(chalk.red(`\nError downloading ${file.path}: ${msg}`));
226
217
  }
227
- }
228
- if (progressBar) {
229
- progressBar.stop();
218
+ options.onProgress?.({
219
+ operation: "download",
220
+ kind: "transfer",
221
+ phase: "transferring",
222
+ label: "Downloading files",
223
+ current: index + 1,
224
+ total: files.length,
225
+ percent: files.length === 0 ? 100 : ((index + 1) / files.length) * 100,
226
+ unit: "files",
227
+ status: "running",
228
+ });
230
229
  }
231
230
  summary.endTime = Date.now();
232
231
  summary.duration = (summary.endTime - summary.startTime) / 1000;
233
232
  summary.speed = summary.duration > 0 ? summary.transferSize / summary.duration : 0;
233
+ options.onProgress?.({
234
+ operation: "download",
235
+ kind: "transfer",
236
+ phase: summary.failedCount === 0 ? "complete" : "failed",
237
+ label: "Download complete",
238
+ current: summary.transferredCount,
239
+ total: summary.totalFiles,
240
+ percent: summary.totalFiles === 0 ? 100 : (summary.transferredCount / summary.totalFiles) * 100,
241
+ unit: "files",
242
+ status: summary.failedCount === 0 ? "done" : "error",
243
+ });
234
244
  return summary;
235
245
  }
@@ -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 with a progress bar display.
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,9 +7,9 @@ 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";
12
+ import { chiliLog } from "../../screen/output.js";
13
13
  /**
14
14
  * Formats bytes into human-readable format.
15
15
  * @param bytes - Number of bytes.
@@ -101,17 +101,24 @@ async function localFiles_scan(localPath, remotePath) {
101
101
  return files;
102
102
  }
103
103
  /**
104
- * Uploads files with a progress bar display.
104
+ * Uploads files and optionally emits structured progress events.
105
105
  * @param localPath - Local path (file or directory).
106
106
  * @param remotePath - Remote ChRIS path.
107
+ * @param options - Upload options, including an optional progress callback.
107
108
  * @returns Promise<UploadSummary> with upload statistics.
108
109
  */
109
110
  export async function files_uploadWithProgress(localPath, remotePath, options = {}) {
110
111
  const resolvedRemote = await path_resolveChrisFs(remotePath, {});
111
112
  // Scan files
112
- console.log(chalk.cyan("Scanning files to upload..."));
113
+ chiliLog(chalk.cyan("Scanning files to upload..."));
114
+ options.onProgress?.({
115
+ operation: "upload",
116
+ kind: "transfer",
117
+ phase: "scanning",
118
+ label: "Scanning files to upload",
119
+ status: "running",
120
+ });
113
121
  const fileList = await localFiles_scan(localPath, resolvedRemote);
114
- const totalSize = fileList.reduce((sum, f) => sum + f.size, 0);
115
122
  // Determine actual target path (where files will be uploaded)
116
123
  const stats = await fs.promises.stat(localPath);
117
124
  let actualTarget = resolvedRemote;
@@ -140,20 +147,6 @@ export async function files_uploadWithProgress(localPath, remotePath, options =
140
147
  else {
141
148
  // 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
149
  }
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
150
  // Upload files
158
151
  const summary = {
159
152
  startTime: Date.now(),
@@ -166,6 +159,17 @@ export async function files_uploadWithProgress(localPath, remotePath, options =
166
159
  speed: 0,
167
160
  actualTargetPath: actualTarget,
168
161
  };
162
+ options.onProgress?.({
163
+ operation: "upload",
164
+ kind: "transfer",
165
+ phase: "transferring",
166
+ label: "Uploading files",
167
+ current: 0,
168
+ total: fileList.length,
169
+ percent: fileList.length === 0 ? 100 : 0,
170
+ unit: "files",
171
+ status: "running",
172
+ });
169
173
  for (const [index, file] of fileList.entries()) {
170
174
  try {
171
175
  const fileContent = await fs.promises.readFile(file.hostPath);
@@ -181,31 +185,39 @@ export async function files_uploadWithProgress(localPath, remotePath, options =
181
185
  }
182
186
  else {
183
187
  summary.failedCount++;
184
- console.log(chalk.yellow(`Failed to upload: ${file.hostPath}`));
188
+ chiliLog(chalk.yellow(`Failed to upload: ${file.hostPath}`));
185
189
  }
186
190
  }
187
191
  catch (error) {
188
192
  summary.failedCount++;
189
- console.log(chalk.red(`Error uploading ${file.hostPath}: ${error instanceof Error ? error.message : String(error)}`));
190
- }
191
- const elapsedSeconds = Math.max(1e-6, (Date.now() - summary.startTime) / 1000);
192
- const rateCurrent = summary.transferSize > 0 ? summary.transferSize / elapsedSeconds : 0;
193
- const remainingBytes = Math.max(0, totalSize - summary.transferSize);
194
- const etaSeconds = rateCurrent > 0 ? remainingBytes / rateCurrent : null;
195
- if (progressBar) {
196
- progressBar.update(index + 1, {
197
- bytes: bytes_format(summary.transferSize),
198
- etaHuman: eta_format(etaSeconds),
199
- rate: rate_format(rateCurrent),
200
- });
193
+ chiliLog(chalk.red(`Error uploading ${file.hostPath}: ${error instanceof Error ? error.message : String(error)}`));
201
194
  }
202
- }
203
- if (progressBar) {
204
- progressBar.stop();
195
+ options.onProgress?.({
196
+ operation: "upload",
197
+ kind: "transfer",
198
+ phase: "transferring",
199
+ label: "Uploading files",
200
+ current: index + 1,
201
+ total: fileList.length,
202
+ percent: fileList.length === 0 ? 100 : ((index + 1) / fileList.length) * 100,
203
+ unit: "files",
204
+ status: "running",
205
+ });
205
206
  }
206
207
  summary.endTime = Date.now();
207
208
  summary.duration = (summary.endTime - summary.startTime) / 1000; // seconds
208
209
  summary.speed = summary.transferSize / summary.duration; // bytes per second
210
+ options.onProgress?.({
211
+ operation: "upload",
212
+ kind: "transfer",
213
+ phase: summary.failedCount === 0 ? "complete" : "failed",
214
+ label: "Upload complete",
215
+ current: summary.transferredCount,
216
+ total: summary.totalFiles,
217
+ percent: summary.totalFiles === 0 ? 100 : (summary.transferredCount / summary.totalFiles) * 100,
218
+ unit: "files",
219
+ status: summary.failedCount === 0 ? "done" : "error",
220
+ });
209
221
  return summary;
210
222
  }
211
223
  /**
@@ -9,6 +9,7 @@
9
9
  import fs from "fs";
10
10
  import path from "path";
11
11
  import { projectDir_get, browser_open, asciidoc_render } from "../../man/renderer.js";
12
+ import { chiliErrLog, chiliLog } from "../../screen/output.js";
12
13
  /**
13
14
  * Displays a documentation page for a specific topic.
14
15
  *
@@ -19,7 +20,7 @@ export async function manPage_display(options) {
19
20
  const docDir = path.join(projectDir_get(), "doc");
20
21
  const docPath = path.join(docDir, `${options.topic}.adoc`);
21
22
  if (!fs.existsSync(docPath)) {
22
- console.error(`Documentation for '${options.topic}' not found.`);
23
+ chiliErrLog(`Documentation for '${options.topic}' not found.`);
23
24
  return;
24
25
  }
25
26
  if (options.browser) {
@@ -27,6 +28,6 @@ export async function manPage_display(options) {
27
28
  }
28
29
  else {
29
30
  const content = fs.readFileSync(docPath, "utf-8");
30
- console.log(await asciidoc_render(content, options.style, options.width));
31
+ chiliLog(await asciidoc_render(content, options.style, options.width));
31
32
  }
32
33
  }