@fnndsc/chili 3.2.5

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 (219) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +113 -0
  3. package/dist/chefs/chefs.d.ts +15 -0
  4. package/dist/chefs/chefs.js +195 -0
  5. package/dist/commands/compute/fields.d.ts +6 -0
  6. package/dist/commands/compute/fields.js +13 -0
  7. package/dist/commands/compute/list.d.ts +19 -0
  8. package/dist/commands/compute/list.js +20 -0
  9. package/dist/commands/connect/login.d.ts +16 -0
  10. package/dist/commands/connect/login.js +18 -0
  11. package/dist/commands/connect/logout.d.ts +6 -0
  12. package/dist/commands/connect/logout.js +16 -0
  13. package/dist/commands/feed/comments.d.ts +41 -0
  14. package/dist/commands/feed/comments.js +45 -0
  15. package/dist/commands/feed/create.d.ts +13 -0
  16. package/dist/commands/feed/create.js +40 -0
  17. package/dist/commands/feed/note.d.ts +21 -0
  18. package/dist/commands/feed/note.js +24 -0
  19. package/dist/commands/feeds/delete.d.ts +14 -0
  20. package/dist/commands/feeds/delete.js +34 -0
  21. package/dist/commands/feeds/fields.d.ts +6 -0
  22. package/dist/commands/feeds/fields.js +17 -0
  23. package/dist/commands/feeds/list.d.ts +17 -0
  24. package/dist/commands/feeds/list.js +38 -0
  25. package/dist/commands/feeds/share.d.ts +17 -0
  26. package/dist/commands/feeds/share.js +19 -0
  27. package/dist/commands/file/view.d.ts +8 -0
  28. package/dist/commands/file/view.js +25 -0
  29. package/dist/commands/files/delete.d.ts +16 -0
  30. package/dist/commands/files/delete.js +36 -0
  31. package/dist/commands/files/fields.d.ts +7 -0
  32. package/dist/commands/files/fields.js +18 -0
  33. package/dist/commands/files/list.d.ts +11 -0
  34. package/dist/commands/files/list.js +23 -0
  35. package/dist/commands/fs/cat.d.ts +15 -0
  36. package/dist/commands/fs/cat.js +26 -0
  37. package/dist/commands/fs/cp.d.ts +17 -0
  38. package/dist/commands/fs/cp.js +26 -0
  39. package/dist/commands/fs/create.d.ts +15 -0
  40. package/dist/commands/fs/create.js +83 -0
  41. package/dist/commands/fs/download.d.ts +25 -0
  42. package/dist/commands/fs/download.js +235 -0
  43. package/dist/commands/fs/edit.d.ts +16 -0
  44. package/dist/commands/fs/edit.js +25 -0
  45. package/dist/commands/fs/ls.d.ts +18 -0
  46. package/dist/commands/fs/ls.js +142 -0
  47. package/dist/commands/fs/mkdir.d.ts +8 -0
  48. package/dist/commands/fs/mkdir.js +21 -0
  49. package/dist/commands/fs/mv.d.ts +13 -0
  50. package/dist/commands/fs/mv.js +73 -0
  51. package/dist/commands/fs/rm.d.ts +24 -0
  52. package/dist/commands/fs/rm.js +108 -0
  53. package/dist/commands/fs/touch.d.ts +16 -0
  54. package/dist/commands/fs/touch.js +62 -0
  55. package/dist/commands/fs/upload.d.ts +55 -0
  56. package/dist/commands/fs/upload.js +221 -0
  57. package/dist/commands/groups/fields.d.ts +6 -0
  58. package/dist/commands/groups/fields.js +13 -0
  59. package/dist/commands/groups/list.d.ts +16 -0
  60. package/dist/commands/groups/list.js +26 -0
  61. package/dist/commands/man/doc.d.ts +16 -0
  62. package/dist/commands/man/doc.js +32 -0
  63. package/dist/commands/man/topics.d.ts +6 -0
  64. package/dist/commands/man/topics.js +27 -0
  65. package/dist/commands/pipeline/fields.d.ts +6 -0
  66. package/dist/commands/pipeline/fields.js +13 -0
  67. package/dist/commands/pipeline/run.d.ts +18 -0
  68. package/dist/commands/pipeline/run.js +33 -0
  69. package/dist/commands/plugin/readme.d.ts +14 -0
  70. package/dist/commands/plugin/readme.js +49 -0
  71. package/dist/commands/plugin/run.d.ts +10 -0
  72. package/dist/commands/plugin/run.js +29 -0
  73. package/dist/commands/plugin/search.d.ts +7 -0
  74. package/dist/commands/plugin/search.js +18 -0
  75. package/dist/commands/plugininstances/fields.d.ts +6 -0
  76. package/dist/commands/plugininstances/fields.js +13 -0
  77. package/dist/commands/plugininstances/list.d.ts +16 -0
  78. package/dist/commands/plugininstances/list.js +26 -0
  79. package/dist/commands/pluginmetas/fields.d.ts +6 -0
  80. package/dist/commands/pluginmetas/fields.js +13 -0
  81. package/dist/commands/pluginmetas/list.d.ts +16 -0
  82. package/dist/commands/pluginmetas/list.js +26 -0
  83. package/dist/commands/plugins/add.d.ts +34 -0
  84. package/dist/commands/plugins/add.js +374 -0
  85. package/dist/commands/plugins/delete.d.ts +14 -0
  86. package/dist/commands/plugins/delete.js +34 -0
  87. package/dist/commands/plugins/fields.d.ts +6 -0
  88. package/dist/commands/plugins/fields.js +17 -0
  89. package/dist/commands/plugins/list.d.ts +17 -0
  90. package/dist/commands/plugins/list.js +40 -0
  91. package/dist/commands/plugins/overview.d.ts +6 -0
  92. package/dist/commands/plugins/overview.js +17 -0
  93. package/dist/commands/store/list.d.ts +23 -0
  94. package/dist/commands/store/list.js +48 -0
  95. package/dist/commands/tags/fields.d.ts +6 -0
  96. package/dist/commands/tags/fields.js +13 -0
  97. package/dist/commands/tags/list.d.ts +16 -0
  98. package/dist/commands/tags/list.js +26 -0
  99. package/dist/commands/workflows/fields.d.ts +6 -0
  100. package/dist/commands/workflows/fields.js +13 -0
  101. package/dist/commands/workflows/list.d.ts +16 -0
  102. package/dist/commands/workflows/list.js +26 -0
  103. package/dist/config/colorConfig.d.ts +56 -0
  104. package/dist/config/colorConfig.js +161 -0
  105. package/dist/connect/connectHandler.d.ts +15 -0
  106. package/dist/connect/connectHandler.js +48 -0
  107. package/dist/context/contextCommand.d.ts +13 -0
  108. package/dist/context/contextCommand.js +193 -0
  109. package/dist/controllers/baseController.d.ts +43 -0
  110. package/dist/controllers/baseController.js +67 -0
  111. package/dist/controllers/feedController.d.ts +35 -0
  112. package/dist/controllers/feedController.js +53 -0
  113. package/dist/controllers/fileController.d.ts +56 -0
  114. package/dist/controllers/fileController.js +93 -0
  115. package/dist/controllers/pluginContextController.d.ts +24 -0
  116. package/dist/controllers/pluginContextController.js +49 -0
  117. package/dist/controllers/pluginController.d.ts +70 -0
  118. package/dist/controllers/pluginController.js +98 -0
  119. package/dist/controllers/pluginMetaController.d.ts +41 -0
  120. package/dist/controllers/pluginMetaController.js +52 -0
  121. package/dist/feeds/feedHandler.d.ts +64 -0
  122. package/dist/feeds/feedHandler.js +203 -0
  123. package/dist/filesystem/fileGroupHandler.d.ts +92 -0
  124. package/dist/filesystem/fileGroupHandler.js +318 -0
  125. package/dist/filesystem/filesystemHandler.d.ts +12 -0
  126. package/dist/filesystem/filesystemHandler.js +135 -0
  127. package/dist/filesystem/inodeCommand.d.ts +12 -0
  128. package/dist/filesystem/inodeCommand.js +37 -0
  129. package/dist/handlers/baseGroupHandler.d.ts +103 -0
  130. package/dist/handlers/baseGroupHandler.js +342 -0
  131. package/dist/index.d.ts +7 -0
  132. package/dist/index.js +239 -0
  133. package/dist/lfs/lfs.d.ts +25 -0
  134. package/dist/lfs/lfs.js +74 -0
  135. package/dist/man/man.d.ts +15 -0
  136. package/dist/man/man.js +31 -0
  137. package/dist/man/renderer.d.ts +26 -0
  138. package/dist/man/renderer.js +187 -0
  139. package/dist/models/feed.d.ts +18 -0
  140. package/dist/models/feed.js +8 -0
  141. package/dist/models/listing.d.ts +33 -0
  142. package/dist/models/listing.js +9 -0
  143. package/dist/models/plugin.d.ts +27 -0
  144. package/dist/models/plugin.js +1 -0
  145. package/dist/models/resource.d.ts +39 -0
  146. package/dist/models/resource.js +9 -0
  147. package/dist/pacs/pacsQueryHandler.d.ts +55 -0
  148. package/dist/pacs/pacsQueryHandler.js +196 -0
  149. package/dist/pacs/pacsQueryPayload.d.ts +19 -0
  150. package/dist/pacs/pacsQueryPayload.js +44 -0
  151. package/dist/pacs/pacsResultRender.d.ts +20 -0
  152. package/dist/pacs/pacsResultRender.js +141 -0
  153. package/dist/pacs/pacsRetrieveHandler.d.ts +87 -0
  154. package/dist/pacs/pacsRetrieveHandler.js +272 -0
  155. package/dist/pacs/pacsServerHandler.d.ts +15 -0
  156. package/dist/pacs/pacsServerHandler.js +32 -0
  157. package/dist/path/pathCommand.d.ts +64 -0
  158. package/dist/path/pathCommand.js +805 -0
  159. package/dist/path/pathMapper.d.ts +189 -0
  160. package/dist/path/pathMapper.js +382 -0
  161. package/dist/plugins/pluginGroupHandler.d.ts +42 -0
  162. package/dist/plugins/pluginGroupHandler.js +90 -0
  163. package/dist/plugins/pluginHandler.d.ts +87 -0
  164. package/dist/plugins/pluginHandler.js +303 -0
  165. package/dist/plugins/pluginMetaHandler.d.ts +21 -0
  166. package/dist/plugins/pluginMetaHandler.js +27 -0
  167. package/dist/screen/screen.d.ts +214 -0
  168. package/dist/screen/screen.js +478 -0
  169. package/dist/utils/admin_prompt.d.ts +42 -0
  170. package/dist/utils/admin_prompt.js +105 -0
  171. package/dist/utils/cli.d.ts +67 -0
  172. package/dist/utils/cli.js +112 -0
  173. package/dist/utils/docker.d.ts +74 -0
  174. package/dist/utils/docker.js +155 -0
  175. package/dist/utils/input_format.d.ts +77 -0
  176. package/dist/utils/input_format.js +135 -0
  177. package/dist/utils/sort.d.ts +29 -0
  178. package/dist/utils/sort.js +63 -0
  179. package/dist/utils/ui.d.ts +7 -0
  180. package/dist/utils/ui.js +26 -0
  181. package/dist/utils.d.ts +15 -0
  182. package/dist/utils.js +14 -0
  183. package/dist/views/compute.d.ts +17 -0
  184. package/dist/views/compute.js +40 -0
  185. package/dist/views/connect.d.ts +13 -0
  186. package/dist/views/connect.js +35 -0
  187. package/dist/views/feed.d.ts +44 -0
  188. package/dist/views/feed.js +110 -0
  189. package/dist/views/file.d.ts +25 -0
  190. package/dist/views/file.js +71 -0
  191. package/dist/views/fs.d.ts +51 -0
  192. package/dist/views/fs.js +105 -0
  193. package/dist/views/ls.d.ts +37 -0
  194. package/dist/views/ls.js +190 -0
  195. package/dist/views/plugin.d.ts +28 -0
  196. package/dist/views/plugin.js +71 -0
  197. package/dist/views/pluginParameters.d.ts +17 -0
  198. package/dist/views/pluginParameters.js +79 -0
  199. package/docs/00_intro.adoc +25 -0
  200. package/docs/01_setup.adoc +63 -0
  201. package/docs/02_context.adoc +128 -0
  202. package/docs/03_searchable.adoc +134 -0
  203. package/docs/10_everyday_examples.adoc +9 -0
  204. package/docs/11_upload_data.adoc +146 -0
  205. package/docs/12_create_a_feed.adoc +9 -0
  206. package/docs/13_run_a_plugin.adoc +271 -0
  207. package/docs/20_plugins.adoc +16 -0
  208. package/docs/21_pluginReadme.adoc +123 -0
  209. package/docs/30_filesystem.adoc +93 -0
  210. package/docs/31_fileops.adoc +51 -0
  211. package/docs/32_file_content.adoc +471 -0
  212. package/docs/_architecture.adoc +48 -0
  213. package/docs/_deepContext.adoc +84 -0
  214. package/docs/_roadmap.adoc +42 -0
  215. package/docs/_searchable.adoc +187 -0
  216. package/docs/_state.adoc +68 -0
  217. package/docs/pacs.adoc +101 -0
  218. package/docs/pathMapper.md +246 -0
  219. package/package.json +92 -0
@@ -0,0 +1,235 @@
1
+ /**
2
+ * @file Download command implementation.
3
+ * @module
4
+ */
5
+ import { fileContent_getBinaryStream, files_listRecursive } from "@fnndsc/salsa";
6
+ import { path_resolveChrisFs } from "../../utils/cli.js";
7
+ import fs from "fs";
8
+ import path from "path";
9
+ import cliProgress from "cli-progress";
10
+ import chalk from "chalk";
11
+ import { pipeline } from "stream/promises";
12
+ import { bytes_format, eta_format, rate_format } from "./upload.js";
13
+ import { prompt_confirmOrThrow } from "../../utils/input_format.js";
14
+ export { bytes_format };
15
+ /**
16
+ * Recursively creates a directory and all parent directories.
17
+ * @param dirPath - Directory path to create.
18
+ */
19
+ async function directory_ensureExists(dirPath) {
20
+ await fs.promises.mkdir(dirPath, { recursive: true });
21
+ }
22
+ /**
23
+ * Checks if a ChRIS path is a directory by attempting to list it.
24
+ * @param chrisPath - ChRIS path to check.
25
+ * @returns True if it's a directory, false if it's a file.
26
+ */
27
+ async function chrisPath_isDirectory(chrisPath) {
28
+ try {
29
+ // Try to list it - if it succeeds, it's a directory
30
+ const items = await files_listRecursive(chrisPath);
31
+ return items.length > 0 || chrisPath.endsWith('/');
32
+ }
33
+ catch {
34
+ // If listing fails, assume it's a file
35
+ return false;
36
+ }
37
+ }
38
+ /**
39
+ * Downloads files from ChRIS with a progress bar display.
40
+ * @param chrisPath - Remote ChRIS path (file or directory).
41
+ * @param localPath - Local filesystem path.
42
+ * @param options - Download options.
43
+ * @returns Promise<DownloadSummary> with download statistics.
44
+ */
45
+ export async function files_downloadWithProgress(chrisPath, localPath, options = {}) {
46
+ const resolvedChris = await path_resolveChrisFs(chrisPath, {});
47
+ // Check if local path exists (only block overwriting regular files unless force)
48
+ if (fs.existsSync(localPath)) {
49
+ const localStat = await fs.promises.stat(localPath);
50
+ if (localStat.isFile() && !options.force) {
51
+ throw new Error(`Local path already exists: ${localPath}. Use -f flag to overwrite.`);
52
+ }
53
+ // If it's a directory, allow and handle below.
54
+ }
55
+ const summary = {
56
+ startTime: Date.now(),
57
+ endTime: 0,
58
+ totalFiles: 0,
59
+ transferredCount: 0,
60
+ failedCount: 0,
61
+ transferSize: 0,
62
+ duration: 0,
63
+ speed: 0,
64
+ };
65
+ const isDirectory = await chrisPath_isDirectory(resolvedChris);
66
+ if (!isDirectory) {
67
+ // Download single file
68
+ try {
69
+ const result = await fileContent_getBinaryStream(resolvedChris);
70
+ if (!result.ok) {
71
+ throw new Error("Failed to download file");
72
+ }
73
+ const { stream, size } = result.value;
74
+ // If localPath is a directory, place the file inside using remote basename
75
+ const stats = fs.existsSync(localPath) ? await fs.promises.stat(localPath) : null;
76
+ const finalLocalPath = stats && stats.isDirectory()
77
+ ? path.join(localPath, path.basename(resolvedChris))
78
+ : localPath;
79
+ // Warn if target file already exists (non-overwrite) or directory already exists
80
+ if (fs.existsSync(finalLocalPath)) {
81
+ const existingStat = await fs.promises.stat(finalLocalPath);
82
+ if (existingStat.isFile()) {
83
+ if (!options.force) {
84
+ await prompt_confirmOrThrow(`Local file exists at ${finalLocalPath}. Overwrite? (y/N)`);
85
+ }
86
+ }
87
+ else if (existingStat.isDirectory()) {
88
+ if (!options.force) {
89
+ await prompt_confirmOrThrow(`Target directory exists. Download into existing folder: ${finalLocalPath}? (y/N)`);
90
+ }
91
+ }
92
+ }
93
+ // Ensure parent directory exists
94
+ const parentDir = path.dirname(finalLocalPath);
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
+ let transferred = 0;
111
+ const writeStream = fs.createWriteStream(finalLocalPath);
112
+ stream.on("data", (chunk) => {
113
+ 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
+ }
126
+ });
127
+ await pipeline(stream, writeStream);
128
+ if (progressBar) {
129
+ progressBar.update(size || transferred);
130
+ progressBar.stop();
131
+ }
132
+ summary.totalFiles = 1;
133
+ summary.transferredCount = 1;
134
+ summary.transferSize = transferred;
135
+ summary.endTime = Date.now();
136
+ summary.duration = (summary.endTime - summary.startTime) / 1000;
137
+ summary.speed = summary.duration > 0 ? summary.transferSize / summary.duration : 0;
138
+ return summary;
139
+ }
140
+ catch (error) {
141
+ summary.failedCount = 1;
142
+ throw error;
143
+ }
144
+ }
145
+ // Download directory
146
+ console.log(chalk.cyan("Scanning files to download..."));
147
+ const items = await files_listRecursive(resolvedChris);
148
+ const files = items.filter((item) => item.type === 'file');
149
+ summary.totalFiles = files.length;
150
+ const totalSize = files.reduce((sum, f) => sum + (f.size || 0), 0);
151
+ // Apply Unix semantics: trailing slash determines merge behavior
152
+ const targetDir = resolvedChris.endsWith('/')
153
+ ? localPath // Merge contents
154
+ : path.join(localPath, path.basename(resolvedChris)); // Create subdir
155
+ // Warn/confirm if target directory exists
156
+ if (fs.existsSync(targetDir)) {
157
+ const statTarget = await fs.promises.stat(targetDir);
158
+ if (statTarget.isDirectory()) {
159
+ const entries = await fs.promises.readdir(targetDir);
160
+ if (!options.force) {
161
+ await prompt_confirmOrThrow(`Target directory exists${entries.length ? ' (will merge contents)' : ''}: ${targetDir}. Continue? (y/N)`);
162
+ }
163
+ }
164
+ else if (statTarget.isFile()) {
165
+ if (!options.force) {
166
+ await prompt_confirmOrThrow(`Local file exists at ${targetDir}. Overwrite? (y/N)`);
167
+ }
168
+ }
169
+ }
170
+ // Ensure target directory exists
171
+ 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
+ }
186
+ // Download each file
187
+ for (const [index, file] of files.entries()) {
188
+ try {
189
+ const result = await fileContent_getBinaryStream(file.path);
190
+ if (!result.ok) {
191
+ summary.failedCount++;
192
+ console.log(chalk.yellow(`\nFailed to download: ${file.path}`));
193
+ continue;
194
+ }
195
+ const { stream, size } = result.value;
196
+ // Calculate relative path
197
+ const relativePath = file.path.substring(resolvedChris.length).replace(/^\//, '');
198
+ const localFilePath = path.join(targetDir, relativePath);
199
+ // Ensure parent directory exists
200
+ const parentDir = path.dirname(localFilePath);
201
+ await directory_ensureExists(parentDir);
202
+ const writeStream = fs.createWriteStream(localFilePath);
203
+ let transferred = 0;
204
+ stream.on("data", (chunk) => {
205
+ transferred += chunk.length;
206
+ });
207
+ await pipeline(stream, writeStream);
208
+ summary.transferredCount++;
209
+ summary.transferSize += size ?? transferred;
210
+ }
211
+ catch (error) {
212
+ summary.failedCount++;
213
+ 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
+ });
226
+ }
227
+ }
228
+ if (progressBar) {
229
+ progressBar.stop();
230
+ }
231
+ summary.endTime = Date.now();
232
+ summary.duration = (summary.endTime - summary.startTime) / 1000;
233
+ summary.speed = summary.duration > 0 ? summary.transferSize / summary.duration : 0;
234
+ return summary;
235
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Result of an edit operation.
3
+ */
4
+ export interface EditResult {
5
+ success: boolean;
6
+ error?: string;
7
+ }
8
+ /**
9
+ * Replaces a ChRIS file in-place by deleting the original and uploading
10
+ * a new version from a local path.
11
+ *
12
+ * @param chrisPath - The resolved ChRIS path of the file to replace.
13
+ * @param localPath - The local temp file containing the new content.
14
+ * @returns EditResult indicating success or failure with error message.
15
+ */
16
+ export declare function file_replaceContent(chrisPath: string, localPath: string): Promise<EditResult>;
@@ -0,0 +1,25 @@
1
+ /**
2
+ * @file File edit-in-place: delete original + re-upload modified content.
3
+ * @module
4
+ */
5
+ import { files_rm } from './rm.js';
6
+ import { files_uploadPath } from '@fnndsc/salsa';
7
+ /**
8
+ * Replaces a ChRIS file in-place by deleting the original and uploading
9
+ * a new version from a local path.
10
+ *
11
+ * @param chrisPath - The resolved ChRIS path of the file to replace.
12
+ * @param localPath - The local temp file containing the new content.
13
+ * @returns EditResult indicating success or failure with error message.
14
+ */
15
+ export async function file_replaceContent(chrisPath, localPath) {
16
+ const rmResult = await files_rm(chrisPath, { recursive: false, force: false });
17
+ if (!rmResult.success) {
18
+ return { success: false, error: rmResult.error ?? 'Delete failed' };
19
+ }
20
+ const uploaded = await files_uploadPath(localPath, chrisPath);
21
+ return {
22
+ success: uploaded,
23
+ error: uploaded ? undefined : 'Upload failed after delete — original is gone, check tmp file',
24
+ };
25
+ }
@@ -0,0 +1,18 @@
1
+ import { ListingItem } from "../../models/listing.js";
2
+ /**
3
+ * Options for the list (ls) operation.
4
+ */
5
+ export interface LsOptions {
6
+ path?: string;
7
+ sort?: string;
8
+ reverse?: boolean;
9
+ [key: string]: string | boolean | undefined;
10
+ }
11
+ /**
12
+ * Core logic for the 'ls' command, returning structured data.
13
+ *
14
+ * @param options - Options for the ls command, including path.
15
+ * @param pathStr - The path to list. Defaults to an empty string if not provided.
16
+ * @returns A Promise resolving to an array of ListingItem.
17
+ */
18
+ export declare function files_list(options: LsOptions, pathStr?: string): Promise<ListingItem[]>;
@@ -0,0 +1,142 @@
1
+ /**
2
+ * @file Implements the core logic for the `ls` command in the ChRIS CLI.
3
+ *
4
+ * This module provides functionality to list files, directories, and links
5
+ * from a specified path within the ChRIS file system. It interacts with the
6
+ * `@fnndsc/salsa` and `@fnndsc/cumin` libraries to fetch and filter resource data.
7
+ *
8
+ * @module
9
+ */
10
+ import { files_listAll, feeds_list, pluginInstances_list } from "@fnndsc/salsa";
11
+ import { path_resolveChrisFs } from "../../utils/cli.js";
12
+ import { list_applySort } from "../../utils/sort.js";
13
+ import { errorStack } from "@fnndsc/cumin";
14
+ /**
15
+ * Core logic for the 'ls' command, returning structured data.
16
+ *
17
+ * @param options - Options for the ls command, including path.
18
+ * @param pathStr - The path to list. Defaults to an empty string if not provided.
19
+ * @returns A Promise resolving to an array of ListingItem.
20
+ */
21
+ export async function files_list(options, pathStr = "") {
22
+ const items = [];
23
+ // Note: If pathStr is invalid, this might throw, which is handled by caller
24
+ const resolvedPath = await path_resolveChrisFs(pathStr, {});
25
+ const fetchOpts = { limit: 100, offset: 0 }; // Starting options
26
+ // Fetch all resources in parallel, but allow some to fail
27
+ const results = await Promise.allSettled([
28
+ files_listAll(fetchOpts, 'dirs', resolvedPath),
29
+ files_listAll(fetchOpts, 'files', resolvedPath),
30
+ files_listAll(fetchOpts, 'links', resolvedPath)
31
+ ]);
32
+ // Process results: 0=dirs, 1=files, 2=links
33
+ const [dirsResult, filesResult, linksResult] = results;
34
+ // Helper to map raw API objects to ListingItem
35
+ const mapToItem = (raw, type) => {
36
+ let name = raw.fname || raw.path || "";
37
+ if (name.includes('/')) {
38
+ name = name.split('/').pop() || name;
39
+ }
40
+ const normalizedPath = raw.path
41
+ ? (raw.path.startsWith('/') ? raw.path : `/${raw.path}`)
42
+ : undefined;
43
+ // For links, strip the .chrislink extension from the display name
44
+ if (type === 'link' && name.endsWith('.chrislink')) {
45
+ name = name.slice(0, -10); // Remove '.chrislink' (10 characters)
46
+ }
47
+ // Normalize target for all item types when a path is present
48
+ const target = normalizedPath;
49
+ return {
50
+ name,
51
+ type,
52
+ size: raw.fsize || 0,
53
+ owner: raw.owner_username || 'unknown',
54
+ date: raw.creation_date || '',
55
+ target
56
+ };
57
+ };
58
+ if (dirsResult.status === 'fulfilled') {
59
+ const dirs = dirsResult.value;
60
+ if (dirs && dirs.tableData) {
61
+ dirs.tableData.forEach((d) => items.push(mapToItem(d, 'dir')));
62
+ }
63
+ }
64
+ else {
65
+ errorStack.stack_push('error', `Failed to list directories: ${dirsResult.reason}`);
66
+ }
67
+ if (filesResult.status === 'fulfilled') {
68
+ const files = filesResult.value;
69
+ if (files && files.tableData) {
70
+ files.tableData.forEach((f) => items.push(mapToItem(f, 'file')));
71
+ }
72
+ }
73
+ else {
74
+ errorStack.stack_push('error', `Failed to list files: ${filesResult.reason}`);
75
+ }
76
+ if (linksResult.status === 'fulfilled') {
77
+ const links = linksResult.value;
78
+ if (links && links.tableData) {
79
+ links.tableData.forEach((l) => items.push(mapToItem(l, 'link')));
80
+ }
81
+ }
82
+ else {
83
+ errorStack.stack_push('error', `Failed to list links: ${linksResult.reason}`);
84
+ }
85
+ // Enrich items with titles for feeds and plugin instances
86
+ await listingItems_enrichTitles(items);
87
+ // Apply sorting (default to name if not specified)
88
+ const sortField = options.sort || 'name';
89
+ const sortedItems = list_applySort(items, sortField, options.reverse);
90
+ return sortedItems;
91
+ }
92
+ /**
93
+ * Enriches listing items with titles for feeds and plugin instances.
94
+ * Detects patterns: feed_XXXX and pl-<name>_XXXX
95
+ *
96
+ * @param items - Array of listing items to enrich.
97
+ */
98
+ async function listingItems_enrichTitles(items) {
99
+ const enrichPromises = items.map(async (item) => {
100
+ if (item.type !== 'dir')
101
+ return; // Only process directories
102
+ // Pattern 1: feed_XXXX
103
+ const feedMatch = item.name.match(/^feed_(\d+)$/);
104
+ if (feedMatch) {
105
+ const feedId = parseInt(feedMatch[1], 10);
106
+ try {
107
+ const feedData = await feeds_list({ id: feedId, limit: 1 });
108
+ if (feedData && feedData.tableData && feedData.tableData.length > 0) {
109
+ const feed = feedData.tableData[0];
110
+ if (feed && typeof feed['name'] === 'string') {
111
+ item.title = feed['name'];
112
+ }
113
+ }
114
+ }
115
+ catch (e) {
116
+ // Silently ignore errors
117
+ }
118
+ return;
119
+ }
120
+ // Pattern 2: pl-<name>_XXXX
121
+ const pluginMatch = item.name.match(/^(pl-.+)_(\d+)$/);
122
+ if (pluginMatch) {
123
+ const pluginInstanceId = parseInt(pluginMatch[2], 10);
124
+ try {
125
+ const instanceData = await pluginInstances_list({ id: pluginInstanceId, limit: 1 });
126
+ if (instanceData && instanceData.tableData && instanceData.tableData.length > 0) {
127
+ const instance = instanceData.tableData[0];
128
+ if (instance) {
129
+ const pluginName = typeof instance['plugin_name'] === 'string' ? instance['plugin_name'] : '';
130
+ const pluginVersion = typeof instance['plugin_version'] === 'string' ? instance['plugin_version'] : '';
131
+ item.title = pluginVersion ? `${pluginName} v${pluginVersion}` : pluginName;
132
+ }
133
+ }
134
+ }
135
+ catch (e) {
136
+ // Silently ignore errors
137
+ }
138
+ return;
139
+ }
140
+ });
141
+ await Promise.all(enrichPromises);
142
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Core logic for the 'mkdir' command.
3
+ * This function does not perform any console output.
4
+ *
5
+ * @param dirPath - The full or relative ChRIS path for the new folder.
6
+ * @returns A Promise resolving to true on success, false on failure.
7
+ */
8
+ export declare function files_mkdir(dirPath: string): Promise<boolean>;
@@ -0,0 +1,21 @@
1
+ /**
2
+ * @file Implements the core logic for the `mkdir` command in the ChRIS CLI.
3
+ *
4
+ * This module provides functionality to create new directories
5
+ * within the ChRIS file system. It interacts with the `@fnndsc/salsa` library.
6
+ *
7
+ * @module
8
+ */
9
+ import { files_mkdir as salsaFiles_mkdir } from "@fnndsc/salsa";
10
+ import { path_resolveChrisFs } from "../../utils/cli.js";
11
+ /**
12
+ * Core logic for the 'mkdir' command.
13
+ * This function does not perform any console output.
14
+ *
15
+ * @param dirPath - The full or relative ChRIS path for the new folder.
16
+ * @returns A Promise resolving to true on success, false on failure.
17
+ */
18
+ export async function files_mkdir(dirPath) {
19
+ const resolvedPath = await path_resolveChrisFs(dirPath, {});
20
+ return await salsaFiles_mkdir(resolvedPath);
21
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Options for the move (mv) operation.
3
+ */
4
+ export interface MvOptions {
5
+ }
6
+ /**
7
+ * Moves a file or directory.
8
+ *
9
+ * @param src - Source path.
10
+ * @param dest - Destination path.
11
+ * @returns Promise<boolean> success.
12
+ */
13
+ export declare function files_mv(src: string, dest: string): Promise<boolean>;
@@ -0,0 +1,73 @@
1
+ /**
2
+ * @file Move command logic.
3
+ *
4
+ * Performs server-side moves (renames) for files and directories.
5
+ *
6
+ * @module
7
+ */
8
+ import { files_move as salsa_files_move, files_listAll } from "@fnndsc/salsa";
9
+ import { path_resolveChrisFs } from "../../utils/cli.js";
10
+ import { errorStack } from "@fnndsc/cumin";
11
+ import path from "path";
12
+ /**
13
+ * Moves a file or directory.
14
+ *
15
+ * @param src - Source path.
16
+ * @param dest - Destination path.
17
+ * @returns Promise<boolean> success.
18
+ */
19
+ export async function files_mv(src, dest) {
20
+ const srcPath = await path_resolveChrisFs(src, {});
21
+ const destPath = await path_resolveChrisFs(dest, {});
22
+ const srcIsDir = await path_checkIsDir(srcPath);
23
+ if (!srcIsDir && !(await path_checkExists(srcPath))) {
24
+ errorStack.stack_push("error", `Source not found: ${src}`);
25
+ return false;
26
+ }
27
+ const destIsDir = await path_checkIsDir(destPath);
28
+ const destLooksDir = dest.endsWith("/");
29
+ const finalDest = (destIsDir || destLooksDir)
30
+ ? path.posix.join(destPath, path.posix.basename(srcPath))
31
+ : destPath;
32
+ return await salsa_files_move(srcPath, finalDest);
33
+ }
34
+ /**
35
+ * Determines whether a given ChRIS path refers to a directory.
36
+ *
37
+ * @param targetPath - The absolute ChRIS path to check.
38
+ * @returns Promise<boolean> indicating directory existence.
39
+ */
40
+ async function path_checkIsDir(targetPath) {
41
+ const parent = path.posix.dirname(targetPath);
42
+ const results = await files_listAll({ limit: 1000, offset: 0 }, "dirs", parent);
43
+ if (!results || !results.tableData) {
44
+ return false;
45
+ }
46
+ const name = path.posix.basename(targetPath);
47
+ return results.tableData.some((entry) => {
48
+ const candidate = typeof entry.path === "string" && entry.path.length > 0
49
+ ? entry.path
50
+ : typeof entry.fname === "string"
51
+ ? entry.fname
52
+ : "";
53
+ return candidate === targetPath || path.posix.basename(candidate) === name;
54
+ });
55
+ }
56
+ /**
57
+ * Determines whether a given ChRIS path exists as a file in its parent directory.
58
+ *
59
+ * @param targetPath - The absolute ChRIS path to check.
60
+ * @returns Promise<boolean> indicating file existence.
61
+ */
62
+ async function path_checkExists(targetPath) {
63
+ const parent = path.posix.dirname(targetPath);
64
+ const name = path.posix.basename(targetPath);
65
+ const results = await files_listAll({ limit: 1000, offset: 0 }, "files", parent);
66
+ if (!results || !results.tableData) {
67
+ return false;
68
+ }
69
+ return results.tableData.some((entry) => {
70
+ const candidate = typeof entry.fname === "string" ? entry.fname : "";
71
+ return candidate === targetPath || path.posix.basename(candidate) === name;
72
+ });
73
+ }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Options for the remove (rm) operation.
3
+ */
4
+ export interface RmOptions {
5
+ recursive?: boolean;
6
+ force?: boolean;
7
+ }
8
+ /**
9
+ * Result of a remove (rm) operation.
10
+ */
11
+ export interface RmResult {
12
+ success: boolean;
13
+ path: string;
14
+ type: 'file' | 'dir' | 'link' | null;
15
+ error?: string;
16
+ }
17
+ /**
18
+ * Core logic for the 'rm' command.
19
+ *
20
+ * @param targetPath - The path to delete.
21
+ * @param options - Options including recursive and force flags.
22
+ * @returns A Promise resolving to RmResult.
23
+ */
24
+ export declare function files_rm(targetPath: string, options?: RmOptions): Promise<RmResult>;