@fnndsc/chell 4.3.1 → 4.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.
Files changed (124) hide show
  1. package/dist/builtins/debug.d.ts +3 -1
  2. package/dist/builtins/debug.js +14 -22
  3. package/dist/builtins/debug.js.map +1 -1
  4. package/dist/builtins/fs/cat.d.ts +8 -8
  5. package/dist/builtins/fs/cat.js +37 -20
  6. package/dist/builtins/fs/cat.js.map +1 -1
  7. package/dist/builtins/fs/cd.d.ts +3 -2
  8. package/dist/builtins/fs/cd.js +48 -33
  9. package/dist/builtins/fs/cd.js.map +1 -1
  10. package/dist/builtins/fs/cp.d.ts +4 -1
  11. package/dist/builtins/fs/cp.js +23 -10
  12. package/dist/builtins/fs/cp.js.map +1 -1
  13. package/dist/builtins/fs/download.js +5 -1
  14. package/dist/builtins/fs/download.js.map +1 -1
  15. package/dist/builtins/fs/edit.d.ts +2 -2
  16. package/dist/builtins/fs/edit.js +37 -19
  17. package/dist/builtins/fs/edit.js.map +1 -1
  18. package/dist/builtins/fs/mkdir.d.ts +4 -1
  19. package/dist/builtins/fs/mkdir.js +20 -6
  20. package/dist/builtins/fs/mkdir.js.map +1 -1
  21. package/dist/builtins/fs/mv.d.ts +4 -1
  22. package/dist/builtins/fs/mv.js +23 -10
  23. package/dist/builtins/fs/mv.js.map +1 -1
  24. package/dist/builtins/fs/pull.d.ts +2 -2
  25. package/dist/builtins/fs/pull.js +57 -29
  26. package/dist/builtins/fs/pull.js.map +1 -1
  27. package/dist/builtins/fs/pwd.d.ts +6 -3
  28. package/dist/builtins/fs/pwd.js +8 -10
  29. package/dist/builtins/fs/pwd.js.map +1 -1
  30. package/dist/builtins/fs/rm.d.ts +7 -1
  31. package/dist/builtins/fs/rm.js +61 -12
  32. package/dist/builtins/fs/rm.js.map +1 -1
  33. package/dist/builtins/fs/touch.d.ts +4 -1
  34. package/dist/builtins/fs/touch.js +24 -12
  35. package/dist/builtins/fs/touch.js.map +1 -1
  36. package/dist/builtins/fs/upload.js +4 -1
  37. package/dist/builtins/fs/upload.js.map +1 -1
  38. package/dist/builtins/help.js +3 -3
  39. package/dist/builtins/index.d.ts +1 -0
  40. package/dist/builtins/index.js +1 -0
  41. package/dist/builtins/index.js.map +1 -1
  42. package/dist/builtins/net/cubepath.d.ts +1 -1
  43. package/dist/builtins/net/cubepath.js +1 -1
  44. package/dist/builtins/net/query.d.ts +1 -1
  45. package/dist/builtins/net/query.js +1 -1
  46. package/dist/builtins/sys/physicalmode.d.ts +3 -1
  47. package/dist/builtins/sys/physicalmode.js +13 -20
  48. package/dist/builtins/sys/physicalmode.js.map +1 -1
  49. package/dist/builtins/sys/timing.d.ts +3 -1
  50. package/dist/builtins/sys/timing.js +13 -20
  51. package/dist/builtins/sys/timing.js.map +1 -1
  52. package/dist/builtins/sys/version.d.ts +15 -0
  53. package/dist/builtins/sys/version.js +23 -0
  54. package/dist/builtins/sys/version.js.map +1 -0
  55. package/dist/builtins/sys/whoami.d.ts +9 -4
  56. package/dist/builtins/sys/whoami.js +20 -13
  57. package/dist/builtins/sys/whoami.js.map +1 -1
  58. package/dist/calypso.d.ts +13 -0
  59. package/dist/calypso.js +44 -0
  60. package/dist/calypso.js.map +1 -0
  61. package/dist/chell.d.ts +2 -1
  62. package/dist/chell.js +2 -1
  63. package/dist/chell.js.map +1 -1
  64. package/dist/core/boot.d.ts +3 -1
  65. package/dist/core/boot.js +50 -100
  66. package/dist/core/boot.js.map +1 -1
  67. package/dist/core/cli.d.ts +3 -1
  68. package/dist/core/cli.js +11 -1
  69. package/dist/core/cli.js.map +1 -1
  70. package/dist/core/cliSurface.d.ts +26 -0
  71. package/dist/core/cliSurface.js +152 -0
  72. package/dist/core/cliSurface.js.map +1 -0
  73. package/dist/core/dispatch.d.ts +63 -10
  74. package/dist/core/dispatch.js +326 -234
  75. package/dist/core/dispatch.js.map +1 -1
  76. package/dist/core/engine.d.ts +91 -0
  77. package/dist/core/engine.js +222 -0
  78. package/dist/core/engine.js.map +1 -0
  79. package/dist/core/progress.d.ts +25 -0
  80. package/dist/core/progress.js +10 -0
  81. package/dist/core/progress.js.map +1 -0
  82. package/dist/core/progressRenderer.d.ts +42 -0
  83. package/dist/core/progressRenderer.js +176 -0
  84. package/dist/core/progressRenderer.js.map +1 -0
  85. package/dist/core/prompt/session.d.ts +20 -0
  86. package/dist/core/prompt/session.js +43 -0
  87. package/dist/core/prompt/session.js.map +1 -0
  88. package/dist/core/question.d.ts +11 -18
  89. package/dist/core/question.js +14 -57
  90. package/dist/core/question.js.map +1 -1
  91. package/dist/core/repl.d.ts +25 -5
  92. package/dist/core/repl.js +56 -45
  93. package/dist/core/repl.js.map +1 -1
  94. package/dist/core/sink.d.ts +201 -0
  95. package/dist/core/sink.js +300 -0
  96. package/dist/core/sink.js.map +1 -0
  97. package/dist/core/surface.d.ts +172 -0
  98. package/dist/core/surface.js +105 -0
  99. package/dist/core/surface.js.map +1 -0
  100. package/dist/core/version.d.ts +20 -0
  101. package/dist/core/version.js +82 -0
  102. package/dist/core/version.js.map +1 -0
  103. package/dist/core/warnings.d.ts +14 -0
  104. package/dist/core/warnings.js +25 -0
  105. package/dist/core/warnings.js.map +1 -0
  106. package/dist/daemon/launch.d.ts +9 -0
  107. package/dist/daemon/launch.js +82 -0
  108. package/dist/daemon/launch.js.map +1 -0
  109. package/dist/index.js +2 -12
  110. package/dist/index.js.map +1 -1
  111. package/dist/lib/spinner.js +5 -4
  112. package/dist/lib/spinner.js.map +1 -1
  113. package/dist/lib/vfs/vfs.js +7 -3
  114. package/dist/lib/vfs/vfs.js.map +1 -1
  115. package/dist/remote/client.d.ts +6 -0
  116. package/dist/remote/client.js +71 -0
  117. package/dist/remote/client.js.map +1 -0
  118. package/dist/remote/discovery.d.ts +23 -0
  119. package/dist/remote/discovery.js +55 -0
  120. package/dist/remote/discovery.js.map +1 -0
  121. package/dist/remote/remoteEngine.d.ts +125 -0
  122. package/dist/remote/remoteEngine.js +283 -0
  123. package/dist/remote/remoteEngine.js.map +1 -0
  124. package/package.json +8 -6
@@ -1,31 +1,32 @@
1
1
  /**
2
- * @file Command parsing and dispatch for the ChELL shell.
2
+ * @file Command dispatch for the ChELL shell.
3
3
  *
4
- * Turns a line of user input into an executed command:
5
- * - preprocessing (inline shell escape, semicolon batching, redirects, pipes, wildcards, help)
4
+ * Turns one parsed command into an executed command with an envelope result:
6
5
  * - the built-in command table and dispatch to built-ins, simulated plugin exec, or `chili`
7
6
  * - output capture for the redirect/pipe paths
7
+ * - envelope-producing execution used by the engine facade
8
8
  *
9
- * The startup/connection/REPL glue that drives this layer lives in `./boot.js`.
9
+ * Line-level orchestration (shell escape, semicolon batching, redirect and
10
+ * pipe detection) lives in `./engine.js`; the startup/connection/REPL glue
11
+ * lives in `./boot.js`.
10
12
  *
11
13
  * @module
12
14
  */
13
15
  import { writeFileSync, appendFileSync } from 'fs';
14
16
  import chalk from 'chalk';
15
17
  import { spawn } from 'child_process';
16
- import { session } from '../session/index.js';
17
- import { builtin_cd, builtin_ls, builtin_pwd, builtin_connect, builtin_logout, builtin_cat, builtin_cp, builtin_mv, builtin_upload, builtin_pacs, builtin_pipeline, builtin_pull, builtin_query, builtin_cubepath, builtin_rm, builtin_touch, builtin_mkdir, builtin_plugin, builtin_feed, builtin_compute, builtin_tag, builtin_group, builtin_pluginmeta, builtin_plugininstance, builtin_workflow, builtin_download, builtin_edit, builtin_files, builtin_links, builtin_dirs, builtin_context, builtin_parametersofplugin, builtin_physicalmode, builtin_prompt, builtin_timing, builtin_whoami, builtin_whereami, builtin_debug, builtin_help, builtin_tree, builtin_du, builtin_store, error_stripDebugPrefix } from '../builtins/index.js';
18
+ import { builtin_cd, builtin_ls, builtin_pwd, builtin_connect, builtin_logout, builtin_cat, builtin_cp, builtin_mv, builtin_upload, builtin_pacs, builtin_pipeline, builtin_pull, builtin_query, builtin_cubepath, builtin_rm, builtin_touch, builtin_mkdir, builtin_plugin, builtin_feed, builtin_compute, builtin_tag, builtin_group, builtin_pluginmeta, builtin_plugininstance, builtin_workflow, builtin_download, builtin_edit, builtin_files, builtin_links, builtin_dirs, builtin_context, builtin_parametersofplugin, builtin_physicalmode, builtin_prompt, builtin_timing, builtin_whoami, builtin_whereami, builtin_version, builtin_debug, builtin_help, builtin_tree, builtin_du, builtin_store, error_stripDebugPrefix } from '../builtins/index.js';
18
19
  import { builtin_executePlugin } from '../builtins/pluginExecute.js';
19
20
  import { builtin_proc } from '../builtins/proc.js';
20
21
  import { wildcards_expandAll } from '../builtins/wildcard.js';
21
22
  import { help_show, args_checkHasHelpFlag } from '../builtins/help.js';
22
23
  import { pluginExecutable_handle } from '../builtins/executable.js';
23
24
  import { errorStack, Ok, Err } from '@fnndsc/cumin';
25
+ import { envelopeHandler_wrap, printingHandler_wrap, ansi_strip, envelope_deliver, sink_get } from './sink.js';
24
26
  import { vfs } from '../lib/vfs/vfs.js';
25
27
  import { args_tokenize } from '../lib/parser.js';
26
- import { semicolons_parse } from '../lib/semicolonParser.js';
27
- import { segment_pipeThrough } from '../lib/pipe.js';
28
- import { pipes_parse, redirect_parse, redirectTarget_resolve, wildcards_expandCheck, command_shellEscape_detect, } from './preprocess.js';
28
+ import { surface_get, capability_require } from './surface.js';
29
+ import { redirectTarget_resolve, wildcards_expandCheck, } from './preprocess.js';
29
30
  import { run as chiliRun } from '@fnndsc/chili/run.js';
30
31
  /**
31
32
  * Spawns the `chili` CLI as a child process.
@@ -49,9 +50,9 @@ export async function chiliCommand_run(command, args) {
49
50
  * Executes a shell command on the host system (shell escape with ! prefix).
50
51
  *
51
52
  * @param shellCommand - The command to execute on the host shell.
52
- * @returns A Promise that resolves when the command completes.
53
+ * @returns A Promise resolving to the command's exit code (1 on spawn failure).
53
54
  */
54
- async function shellCommand_execute(shellCommand) {
55
+ export async function shellCommand_execute(shellCommand) {
55
56
  return new Promise((resolve) => {
56
57
  const child = spawn(shellCommand, {
57
58
  shell: true,
@@ -62,74 +63,14 @@ async function shellCommand_execute(shellCommand) {
62
63
  if (code !== null && code !== 0) {
63
64
  console.error(chalk.red(`Shell command exited with code ${code}`));
64
65
  }
65
- resolve();
66
+ resolve(code ?? 0);
66
67
  });
67
68
  child.on('error', (err) => {
68
69
  console.error(chalk.red(`Failed to execute shell command: ${err.message}`));
69
- resolve();
70
+ resolve(1);
70
71
  });
71
72
  });
72
73
  }
73
- /**
74
- * Handles a command entered by the user.
75
- *
76
- * @param line - The input line.
77
- * @returns A Promise that resolves once the command has been processed.
78
- */
79
- export async function command_handle(line) {
80
- const trimmedLine = line.trim();
81
- if (!trimmedLine)
82
- return;
83
- // Start timing if enabled
84
- const timingEnabled = session.timingEnabled_get();
85
- const startTime = timingEnabled ? performance.now() : 0;
86
- if (command_shellEscape_detect(trimmedLine)) {
87
- await shellEscape_handle(trimmedLine, startTime, timingEnabled);
88
- return;
89
- }
90
- const semicolonResult = await semicolons_handle(trimmedLine, startTime, timingEnabled);
91
- if (!semicolonResult.ok) {
92
- return;
93
- }
94
- if (semicolonResult.value)
95
- return;
96
- const redirectResult = await redirect_handle(trimmedLine, startTime, timingEnabled);
97
- if (!redirectResult.ok) {
98
- return;
99
- }
100
- if (redirectResult.value)
101
- return;
102
- const pipeResult = await pipe_handle(trimmedLine, startTime, timingEnabled);
103
- if (!pipeResult.ok) {
104
- return;
105
- }
106
- if (pipeResult.value)
107
- return;
108
- const tokens = args_tokenize(trimmedLine);
109
- if (tokens.length === 0)
110
- return;
111
- let [command, ...args] = tokens;
112
- // Check for --help flag before any processing
113
- const helpResult = help_showMaybe(command, args);
114
- if (helpResult.ok && helpResult.value) {
115
- return;
116
- }
117
- // Expand wildcards for commands that support it
118
- const expandResult = await wildcards_expand(command, args);
119
- if (!expandResult.ok) {
120
- return;
121
- }
122
- args = expandResult.value;
123
- // Attempt to handle as a simulated plugin execution
124
- if (await pluginExecutable_handle(command, args)) {
125
- // Display timing if enabled
126
- command_timingMaybePrint(startTime, timingEnabled);
127
- return;
128
- }
129
- await command_dispatch(command, args);
130
- // Display timing if enabled
131
- command_timingMaybePrint(startTime, timingEnabled);
132
- }
133
74
  /**
134
75
  * Captures console output during command execution.
135
76
  *
@@ -170,65 +111,134 @@ async function output_capture(fn) {
170
111
  const text = buffer.toString('utf-8');
171
112
  return { text, buffer };
172
113
  }
173
- export const COMMAND_HANDLERS = {
174
- connect: builtin_connect,
175
- logout: builtin_logout,
176
- cd: builtin_cd,
177
- ls: builtin_ls,
178
- pwd: builtin_pwd,
114
+ /**
115
+ * Builtins that have been converted to return envelopes, keyed by command
116
+ * name. Entries here are also present in COMMAND_HANDLERS in wrapped form;
117
+ * this registry exists so envelope-aware hosts can bypass the wrapper and
118
+ * receive the structured result.
119
+ */
120
+ /**
121
+ * Composes the capture bridge with envelope delivery, so a legacy printing
122
+ * handler participates in the dispatch table with envelope semantics and
123
+ * unchanged observable output.
124
+ *
125
+ * @param handler - A legacy printing command handler.
126
+ * @returns A wrapped handler for the dispatch table.
127
+ */
128
+ function printingBridge_wrap(handler) {
129
+ return envelopeHandler_wrap(printingHandler_wrap(handler));
130
+ }
131
+ export const ENVELOPE_HANDLERS = {
179
132
  cat: builtin_cat,
180
- rm: builtin_rm,
133
+ cd: builtin_cd,
181
134
  cp: builtin_cp,
182
135
  mv: builtin_mv,
183
- touch: builtin_touch,
136
+ rm: builtin_rm,
184
137
  mkdir: builtin_mkdir,
138
+ touch: builtin_touch,
139
+ pwd: builtin_pwd,
140
+ whoami: builtin_whoami,
141
+ whereami: builtin_whereami,
142
+ timing: builtin_timing,
143
+ physicalmode: builtin_physicalmode,
144
+ debug: builtin_debug,
145
+ version: builtin_version,
146
+ // Bridged (captured) handlers: envelope semantics without typed models.
147
+ // plugin is deliberately absent: its add flow prompts for admin
148
+ // credentials through readline, which capture would make invisible.
149
+ ls: printingHandler_wrap(builtin_ls),
150
+ tree: printingHandler_wrap(builtin_tree),
151
+ du: printingHandler_wrap(builtin_du),
152
+ help: printingHandler_wrap(builtin_help),
153
+ proc: printingHandler_wrap(builtin_proc),
154
+ logout: printingHandler_wrap(builtin_logout),
155
+ cubepath: printingHandler_wrap(builtin_cubepath),
156
+ query: printingHandler_wrap(builtin_query),
157
+ feed: printingHandler_wrap(builtin_feed),
158
+ feeds: printingHandler_wrap(builtin_feed),
159
+ compute: printingHandler_wrap(builtin_compute),
160
+ computes: printingHandler_wrap(builtin_compute),
161
+ tag: printingHandler_wrap(builtin_tag),
162
+ tags: printingHandler_wrap(builtin_tag),
163
+ group: printingHandler_wrap(builtin_group),
164
+ groups: printingHandler_wrap(builtin_group),
165
+ pluginmeta: printingHandler_wrap(builtin_pluginmeta),
166
+ pluginmetas: printingHandler_wrap(builtin_pluginmeta),
167
+ meta: printingHandler_wrap(builtin_pluginmeta),
168
+ metas: printingHandler_wrap(builtin_pluginmeta),
169
+ plugininstance: printingHandler_wrap(builtin_plugininstance),
170
+ plugininstances: printingHandler_wrap(builtin_plugininstance),
171
+ instance: printingHandler_wrap(builtin_plugininstance),
172
+ instances: printingHandler_wrap(builtin_plugininstance),
173
+ job: printingHandler_wrap(builtin_plugininstance),
174
+ jobs: printingHandler_wrap(builtin_plugininstance),
175
+ workflow: printingHandler_wrap(builtin_workflow),
176
+ workflows: printingHandler_wrap(builtin_workflow),
177
+ files: printingHandler_wrap(builtin_files),
178
+ links: printingHandler_wrap(builtin_links),
179
+ dirs: printingHandler_wrap(builtin_dirs),
180
+ context: printingHandler_wrap(builtin_context),
181
+ parametersofplugin: printingHandler_wrap(builtin_parametersofplugin),
182
+ };
183
+ export const COMMAND_HANDLERS = {
184
+ connect: builtin_connect,
185
+ logout: printingBridge_wrap(builtin_logout),
186
+ cd: envelopeHandler_wrap(builtin_cd),
187
+ ls: printingBridge_wrap(builtin_ls),
188
+ pwd: envelopeHandler_wrap(builtin_pwd),
189
+ cat: envelopeHandler_wrap(builtin_cat),
190
+ rm: envelopeHandler_wrap(builtin_rm),
191
+ cp: envelopeHandler_wrap(builtin_cp),
192
+ mv: envelopeHandler_wrap(builtin_mv),
193
+ touch: envelopeHandler_wrap(builtin_touch),
194
+ mkdir: envelopeHandler_wrap(builtin_mkdir),
185
195
  upload: builtin_upload,
186
196
  pacs: builtin_pacs,
187
197
  pipeline: builtin_pipeline,
188
198
  pipelines: builtin_pipeline,
189
199
  pull: builtin_pull,
190
- query: builtin_query,
191
- cubepath: builtin_cubepath,
200
+ query: printingBridge_wrap(builtin_query),
201
+ cubepath: printingBridge_wrap(builtin_cubepath),
192
202
  download: builtin_download,
193
203
  edit: builtin_edit,
194
- context: builtin_context,
195
- parametersofplugin: builtin_parametersofplugin,
196
- physicalmode: builtin_physicalmode,
204
+ context: printingBridge_wrap(builtin_context),
205
+ parametersofplugin: printingBridge_wrap(builtin_parametersofplugin),
206
+ physicalmode: envelopeHandler_wrap(builtin_physicalmode),
197
207
  prompt: builtin_prompt,
198
- timing: builtin_timing,
199
- whoami: builtin_whoami,
200
- whereami: builtin_whereami,
201
- debug: builtin_debug,
202
- help: builtin_help,
203
- proc: builtin_proc,
204
- tree: builtin_tree,
205
- du: builtin_du,
208
+ timing: envelopeHandler_wrap(builtin_timing),
209
+ whoami: envelopeHandler_wrap(builtin_whoami),
210
+ whereami: envelopeHandler_wrap(builtin_whereami),
211
+ debug: envelopeHandler_wrap(builtin_debug),
212
+ help: printingBridge_wrap(builtin_help),
213
+ proc: printingBridge_wrap(builtin_proc),
214
+ tree: printingBridge_wrap(builtin_tree),
215
+ du: printingBridge_wrap(builtin_du),
206
216
  store: builtin_store,
207
217
  plugin: builtin_plugin,
208
218
  plugins: builtin_plugin,
209
- feed: builtin_feed,
210
- feeds: builtin_feed,
211
- compute: builtin_compute,
212
- computes: builtin_compute,
213
- tag: builtin_tag,
214
- tags: builtin_tag,
215
- group: builtin_group,
216
- groups: builtin_group,
217
- pluginmeta: builtin_pluginmeta,
218
- pluginmetas: builtin_pluginmeta,
219
- meta: builtin_pluginmeta,
220
- metas: builtin_pluginmeta,
221
- plugininstance: builtin_plugininstance,
222
- plugininstances: builtin_plugininstance,
223
- instance: builtin_plugininstance,
224
- instances: builtin_plugininstance,
225
- job: builtin_plugininstance,
226
- jobs: builtin_plugininstance,
227
- workflow: builtin_workflow,
228
- workflows: builtin_workflow,
229
- files: builtin_files,
230
- links: builtin_links,
231
- dirs: builtin_dirs,
219
+ feed: printingBridge_wrap(builtin_feed),
220
+ feeds: printingBridge_wrap(builtin_feed),
221
+ compute: printingBridge_wrap(builtin_compute),
222
+ computes: printingBridge_wrap(builtin_compute),
223
+ tag: printingBridge_wrap(builtin_tag),
224
+ tags: printingBridge_wrap(builtin_tag),
225
+ group: printingBridge_wrap(builtin_group),
226
+ groups: printingBridge_wrap(builtin_group),
227
+ pluginmeta: printingBridge_wrap(builtin_pluginmeta),
228
+ pluginmetas: printingBridge_wrap(builtin_pluginmeta),
229
+ meta: printingBridge_wrap(builtin_pluginmeta),
230
+ metas: printingBridge_wrap(builtin_pluginmeta),
231
+ plugininstance: printingBridge_wrap(builtin_plugininstance),
232
+ plugininstances: printingBridge_wrap(builtin_plugininstance),
233
+ instance: printingBridge_wrap(builtin_plugininstance),
234
+ instances: printingBridge_wrap(builtin_plugininstance),
235
+ job: printingBridge_wrap(builtin_plugininstance),
236
+ jobs: printingBridge_wrap(builtin_plugininstance),
237
+ workflow: printingBridge_wrap(builtin_workflow),
238
+ workflows: printingBridge_wrap(builtin_workflow),
239
+ files: printingBridge_wrap(builtin_files),
240
+ links: printingBridge_wrap(builtin_links),
241
+ dirs: printingBridge_wrap(builtin_dirs),
232
242
  pacsservers: async (args) => {
233
243
  await chiliCommand_run('pacsservers', ['-s', ...args]);
234
244
  },
@@ -246,7 +256,7 @@ export { COMMAND_HANDLERS_KEYS } from '../command-keys.js';
246
256
  * @param startTime - Timestamp from `performance.now()` at command start.
247
257
  * @param enabled - Whether timing display is active.
248
258
  */
249
- function command_timingMaybePrint(startTime, enabled) {
259
+ export function command_timingMaybePrint(startTime, enabled) {
250
260
  if (!enabled)
251
261
  return;
252
262
  const elapsed = performance.now() - startTime;
@@ -293,103 +303,155 @@ export function envRefs_expand(token) {
293
303
  });
294
304
  }
295
305
  /**
296
- * Dispatches a parsed command to its handler.
297
- * Expands environment references in the arguments, then checks
298
- * COMMAND_HANDLERS, then /bin plugin names, then falls back to chili.
306
+ * Reads the current process exit code as a number.
307
+ *
308
+ * @returns The exit code, treating an unset code as zero.
309
+ */
310
+ function exitCode_read() {
311
+ return typeof process.exitCode === 'number' ? process.exitCode : 0;
312
+ }
313
+ /**
314
+ * Runs a legacy printing handler uncaptured and derives an envelope from the
315
+ * exit-code delta.
316
+ *
317
+ * This is the passthrough for commands that cannot be captured yet: the
318
+ * interactive holdouts (their prompts must reach the terminal) and the
319
+ * progress writers (their live updates must stay live). The handler prints
320
+ * exactly as it always has; the placeholder envelope records only the
321
+ * outcome, with no rendered text, so envelope consumers never re-print what
322
+ * the terminal already showed.
323
+ *
324
+ * @param handler - A legacy printing command handler.
325
+ * @param args - Parsed arguments.
326
+ * @returns A placeholder envelope carrying the command's outcome.
327
+ */
328
+ async function handler_runDirect(handler, args) {
329
+ const exitCodeBefore = exitCode_read();
330
+ await handler(args);
331
+ const exitCodeAfter = exitCode_read();
332
+ const failed = exitCodeAfter !== 0 && exitCodeAfter !== exitCodeBefore;
333
+ return { status: failed ? 'error' : 'ok', rendered: '' };
334
+ }
335
+ /**
336
+ * Drains the errorStack from a checkpoint into an envelope's structured
337
+ * `errors` field, and escalates status to `error` when an error-type message
338
+ * was drained.
339
+ *
340
+ * This is the per-command error boundary: it captures exactly the messages a
341
+ * command left on the stack, so a remote surface receives full error detail
342
+ * with each result and stale errors cannot bleed into the next command.
343
+ * Escalating status from a drained error is a reliable per-command failure
344
+ * signal, independent of whether the command happened to change
345
+ * `process.exitCode`.
346
+ *
347
+ * @param checkpoint - A checkpoint from `errorStack.checkpoint_mark()`.
348
+ * @param envelope - The envelope to attach drained errors to.
349
+ * @returns The same envelope, with errors and possibly status updated.
350
+ */
351
+ function envelope_drainErrorsInto(checkpoint, envelope) {
352
+ const drained = errorStack.checkpoint_drain(checkpoint);
353
+ if (drained.length > 0) {
354
+ envelope.errors = drained;
355
+ if (envelope.status === 'ok' && drained.some((message) => message.type === 'error')) {
356
+ envelope.status = 'error';
357
+ }
358
+ }
359
+ return envelope;
360
+ }
361
+ /**
362
+ * Dispatches a parsed command to its handler and returns its envelope, with
363
+ * any errors the command left on the stack drained into the envelope.
299
364
  *
300
365
  * @param command - The command name.
301
366
  * @param args - Parsed arguments.
367
+ * @returns The envelope of the executed command.
302
368
  */
303
- export async function command_dispatch(command, args) {
369
+ export async function command_dispatchEnvelope(command, args) {
370
+ const checkpoint = errorStack.checkpoint_mark();
371
+ const envelope = await commandDispatchEnvelope_run(command, args);
372
+ return envelope_drainErrorsInto(checkpoint, envelope);
373
+ }
374
+ /**
375
+ * Runs the dispatch itself (without the error drain). Expands environment
376
+ * references in the arguments, then checks ENVELOPE_HANDLERS, then
377
+ * unconverted COMMAND_HANDLERS, then /bin plugin/pipeline names, then falls
378
+ * back to chili through the capture bridge.
379
+ *
380
+ * Envelope-speaking handlers are delivered through the active sink here, so
381
+ * direct execution prints exactly as it always has; unconverted handlers
382
+ * print for themselves and yield a placeholder envelope.
383
+ *
384
+ * @param command - The command name.
385
+ * @param args - Parsed arguments.
386
+ * @returns The envelope of the executed command.
387
+ */
388
+ async function commandDispatchEnvelope_run(command, args) {
304
389
  if (command === 'exit') {
305
390
  process.exit(0);
306
391
  }
307
392
  args = args.map(envRefs_expand);
393
+ const envelopeHandler = ENVELOPE_HANDLERS[command];
394
+ if (envelopeHandler) {
395
+ // A handler that resolves without an envelope (as stubbed handlers in
396
+ // tests do) is treated as having produced no output.
397
+ const envelope = await envelopeHandler(args);
398
+ if (!envelope) {
399
+ return { status: 'ok', rendered: '' };
400
+ }
401
+ envelope_deliver(envelope);
402
+ return envelope;
403
+ }
308
404
  const handler = COMMAND_HANDLERS[command];
309
405
  if (handler) {
310
- await handler(args);
311
- return;
406
+ return handler_runDirect(handler, args);
312
407
  }
313
408
  const binResult = await vfs.data_get('/bin');
314
409
  if (binResult.ok) {
315
410
  const pluginItem = binResult.value.find(item => item.name === command && item.type === 'plugin');
316
411
  const pipelineItem = binResult.value.find(item => item.name === command && item.type === 'pipeline');
317
412
  if (pluginItem) {
318
- await builtin_executePlugin(command, args);
319
- return;
413
+ return handler_runDirect((pluginArgs) => builtin_executePlugin(command, pluginArgs), args);
320
414
  }
321
415
  if (pipelineItem) {
322
- await pipelineExecutable_handle(command, args);
323
- return;
416
+ return handler_runDirect((pipelineArgs) => pipelineExecutable_handle(command, pipelineArgs), args);
324
417
  }
325
418
  }
326
- console.log(chalk.yellow(`Unknown chell command '${command}' -- delegating to chili`));
327
- await chiliCommand_run(command, ['-s', ...args]);
419
+ const fallback = printingHandler_wrap(async (fallbackArgs) => {
420
+ console.log(chalk.yellow(`Unknown chell command '${command}' -- delegating to chili`));
421
+ await chiliCommand_run(command, ['-s', ...fallbackArgs]);
422
+ });
423
+ const envelope = await fallback(args);
424
+ envelope_deliver(envelope);
425
+ return envelope;
328
426
  }
329
427
  /**
330
- * Executes a shell-escaped command and prints timing if enabled.
428
+ * Dispatches a parsed command to its handler.
331
429
  *
332
- * @param line - Full input line including the leading `!`.
333
- * @param startTime - Timing reference from `performance.now()`.
334
- * @param timingEnabled - Whether to print elapsed time after execution.
335
- */
336
- async function shellEscape_handle(line, startTime, timingEnabled) {
337
- const shellCommand = line.substring(1).trim();
338
- if (!shellCommand)
339
- return;
340
- await shellCommand_execute(shellCommand);
341
- command_timingMaybePrint(startTime, timingEnabled);
342
- }
343
- /**
344
- * Splits a semicolon-separated command line and executes each segment in sequence.
345
- * Returns Ok(true) if the line contained semicolons and was handled, Ok(false) if not.
430
+ * Compatibility shape over {@link command_dispatchEnvelope} for callers that
431
+ * do not consume envelopes.
346
432
  *
347
- * @param line - Full input line.
348
- * @param startTime - Timing reference.
349
- * @param timingEnabled - Whether to print total elapsed time after all segments.
433
+ * @param command - The command name.
434
+ * @param args - Parsed arguments.
435
+ * @returns A Promise that resolves once the command has been dispatched.
350
436
  */
351
- async function semicolons_handle(line, startTime, timingEnabled) {
352
- const commands = semicolons_parse(line);
353
- if (commands.length <= 1) {
354
- return Ok(false);
355
- }
356
- for (const cmd of commands) {
357
- try {
358
- await command_handle(cmd);
359
- }
360
- catch (error) {
361
- const msg = error instanceof Error ? error.message : String(error);
362
- console.error(chalk.red(`Command error: ${msg}`));
363
- if (stopOnError) {
364
- return Err();
365
- }
366
- }
367
- }
368
- if (timingEnabled) {
369
- const elapsed = performance.now() - startTime;
370
- console.log(chalk.gray(`[Total: ${elapsed.toFixed(2)}ms]`));
371
- }
372
- return Ok(true);
437
+ export async function command_dispatch(command, args) {
438
+ await command_dispatchEnvelope(command, args);
373
439
  }
374
440
  /**
375
- * Handles output redirection (`>` / `>>`). Captures command output and writes to the target file.
376
- * Returns Ok(true) if redirection was detected and handled, Ok(false) if not.
441
+ * Executes a redirected command (`>` / `>>`): captures the command's output
442
+ * and writes it to the target file.
377
443
  *
378
- * @param line - Full input line.
379
- * @param startTime - Timing reference.
380
- * @param timingEnabled - Whether to print elapsed time after execution.
444
+ * @param redirectInfo - The parsed redirection (command, operator, target).
445
+ * @returns An envelope recording the outcome; rendered text stays empty
446
+ * because the output went to the file, not the terminal.
381
447
  */
382
- async function redirect_handle(line, startTime, timingEnabled) {
383
- const redirectInfo = redirect_parse(line);
384
- if (!redirectInfo) {
385
- return Ok(false);
386
- }
448
+ export async function redirect_execute(redirectInfo) {
387
449
  const { buffer } = await chellCommand_executeAndCapture(redirectInfo.command);
388
450
  const targetResult = redirectTarget_resolve(redirectInfo.filePath, redirectInfo.command);
389
451
  if (!targetResult.ok) {
390
452
  const lastError = errorStack.stack_pop();
391
453
  console.error(chalk.red(lastError ? lastError.message : 'Redirect error'));
392
- return Err();
454
+ return { status: 'error', rendered: '' };
393
455
  }
394
456
  if (redirectInfo.operator === '>') {
395
457
  writeFileSync(targetResult.value, buffer);
@@ -397,33 +459,7 @@ async function redirect_handle(line, startTime, timingEnabled) {
397
459
  else {
398
460
  appendFileSync(targetResult.value, buffer);
399
461
  }
400
- command_timingMaybePrint(startTime, timingEnabled);
401
- return Ok(true);
402
- }
403
- /**
404
- * Handles pipe chains (`cmd1 | cmd2 | ...`). Executes the first segment in chell and
405
- * pipes the output through subsequent host-shell processes.
406
- * Returns Ok(true) if a pipe was detected and handled, Ok(false) if not.
407
- *
408
- * @param line - Full input line.
409
- * @param startTime - Timing reference.
410
- * @param timingEnabled - Whether to print elapsed time after the chain completes.
411
- */
412
- async function pipe_handle(line, startTime, timingEnabled) {
413
- const segments = pipes_parse(line);
414
- if (segments.length <= 1) {
415
- return Ok(false);
416
- }
417
- try {
418
- await pipe_execute(segments);
419
- }
420
- catch (error) {
421
- const msg = error instanceof Error ? error.message : String(error);
422
- console.error(chalk.red(`Pipe error: ${msg}`));
423
- return Err();
424
- }
425
- command_timingMaybePrint(startTime, timingEnabled);
426
- return Ok(true);
462
+ return { status: 'ok', rendered: '' };
427
463
  }
428
464
  /**
429
465
  * Shows help for a command if `--help` or `-h` is present in args.
@@ -486,10 +522,31 @@ async function chellCommand_executeAndCapture(commandLine) {
486
522
  }
487
523
  args = expandResult.value;
488
524
  }
489
- return output_capture(async () => {
490
- if (command === 'exit') {
491
- process.exit(0);
525
+ if (command === 'exit') {
526
+ process.exit(0);
527
+ }
528
+ // Envelope-speaking commands feed pipes and redirects from their envelope:
529
+ // rendered text with ANSI stripped (plain pipes, the documented deviation),
530
+ // error-stream text passed live to stderr exactly as the inherit behavior
531
+ // always did. Direct stdout writers (binary cat) are still captured; their
532
+ // bytes precede the envelope's rendered text if a command mixes both.
533
+ const envelopeHandler = ENVELOPE_HANDLERS[command];
534
+ if (envelopeHandler) {
535
+ let envelope;
536
+ const { buffer: directBuffer } = await output_capture(async () => {
537
+ envelope = await envelopeHandler(args);
538
+ });
539
+ if (!envelope) {
540
+ return { text: '', buffer: Buffer.alloc(0) };
541
+ }
542
+ if (envelope.renderedErr !== undefined && envelope.renderedErr.length > 0) {
543
+ process.stderr.write(envelope.renderedErr);
492
544
  }
545
+ const plain = ansi_strip(envelope.rendered);
546
+ const buffer = Buffer.concat([directBuffer, Buffer.from(plain, 'utf-8')]);
547
+ return { text: buffer.toString('utf-8'), buffer };
548
+ }
549
+ return output_capture(async () => {
493
550
  if (await pluginExecutable_handle(command, args, { piped: true })) {
494
551
  return;
495
552
  }
@@ -503,42 +560,77 @@ async function chellCommand_executeAndCapture(commandLine) {
503
560
  });
504
561
  }
505
562
  /**
506
- * Executes a pipe chain by running the first command in chell and piping through local tools.
563
+ * Executes a pipe chain by running the first command in chell and piping
564
+ * through local tools, delivering the final output on the data channel.
507
565
  *
508
566
  * @param segments - Array of command segments separated by pipes.
509
- * @returns A Promise that resolves when the pipe chain completes.
567
+ * @returns An envelope whose rendered text is the chain's final output.
510
568
  */
511
- async function pipe_execute(segments) {
512
- if (segments.length === 0)
513
- return;
569
+ export async function pipe_execute(segments) {
570
+ if (segments.length === 0) {
571
+ return { status: 'ok', rendered: '' };
572
+ }
573
+ // The first segment is a chell command run in-engine; the rest run through
574
+ // the surface, so nothing spawns on a daemon host — a surface without the
575
+ // capability (a browser) fails the pipeline with a clear message.
576
+ if (segments.length > 1) {
577
+ capability_require('pipeSegments', 'this surface cannot run pipeline segments');
578
+ }
514
579
  // Execute first segment in chell and capture output
515
580
  const firstCommand = segments[0];
516
581
  const { buffer } = await chellCommand_executeAndCapture(firstCommand);
517
- if (segments.length === 1) {
518
- // No pipes, just output the result
519
- process.stdout.write(buffer);
520
- return;
521
- }
522
- // Chain remaining segments as spawned processes
582
+ // Chain remaining segments through the surface's own tools.
523
583
  let currentInput = buffer;
524
584
  for (let i = 1; i < segments.length; i++) {
525
- currentInput = await segment_pipeThrough(segments[i], currentInput);
585
+ currentInput = await surface_get().pipeSegment(segments[i], currentInput);
526
586
  }
527
587
  // Output final result
528
- process.stdout.write(currentInput);
588
+ sink_get().data_write(currentInput);
589
+ return { status: 'ok', rendered: currentInput.toString('utf-8') };
529
590
  }
530
591
  /**
531
- * Whether a batch (semicolon list, script) should abort on the first error.
532
- * Read by {@link semicolons_handle}; set by the boot layer for `-e` / script modes.
533
- */
534
- let stopOnError = false;
535
- /**
536
- * Sets the shared stop-on-error flag. Exposed so the boot layer (which owns the
537
- * `-e` flag and script execution) can drive the flag that the dispatch layer reads.
592
+ * Executes one plain command line (no shell escape, batch, redirect or pipe)
593
+ * and returns its envelope.
594
+ *
595
+ * Mirrors the historical direct-execution order exactly: help flag
596
+ * short-circuit, wildcard expansion, simulated plugin execution, then table
597
+ * dispatch. Timing is printed at the same points the shell always printed
598
+ * it (after plugin execution or dispatch; never after help or a wildcard
599
+ * failure).
538
600
  *
539
- * @param value - `true` to abort a batch on the first error.
601
+ * @param trimmedLine - The trimmed command line.
602
+ * @param startTime - Timing reference from `performance.now()`.
603
+ * @param timingEnabled - Whether to print elapsed time after execution.
604
+ * @returns The command's envelope, or null when the line held no tokens.
540
605
  */
541
- export function stopOnError_set(value) {
542
- stopOnError = value;
606
+ export async function command_executeToEnvelope(trimmedLine, startTime, timingEnabled) {
607
+ const tokens = args_tokenize(trimmedLine);
608
+ if (tokens.length === 0)
609
+ return null;
610
+ let [command, ...args] = tokens;
611
+ // Check for --help flag before any processing
612
+ const helpResult = help_showMaybe(command, args);
613
+ if (helpResult.ok && helpResult.value) {
614
+ return { status: 'ok', rendered: '' };
615
+ }
616
+ // Expand wildcards for commands that support it
617
+ const expandResult = await wildcards_expand(command, args);
618
+ if (!expandResult.ok) {
619
+ return { status: 'error', rendered: '' };
620
+ }
621
+ args = expandResult.value;
622
+ // Attempt to handle as a simulated plugin execution. This path bypasses
623
+ // command_dispatchEnvelope, so it drains the errorStack itself.
624
+ const checkpoint = errorStack.checkpoint_mark();
625
+ const exitCodeBefore = exitCode_read();
626
+ if (await pluginExecutable_handle(command, args)) {
627
+ command_timingMaybePrint(startTime, timingEnabled);
628
+ const exitCodeAfter = exitCode_read();
629
+ const failed = exitCodeAfter !== 0 && exitCodeAfter !== exitCodeBefore;
630
+ return envelope_drainErrorsInto(checkpoint, { status: failed ? 'error' : 'ok', rendered: '' });
631
+ }
632
+ const envelope = await command_dispatchEnvelope(command, args);
633
+ command_timingMaybePrint(startTime, timingEnabled);
634
+ return envelope;
543
635
  }
544
636
  //# sourceMappingURL=dispatch.js.map