@nimbus-sh/core 0.4.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (156) hide show
  1. package/README.md +35 -7
  2. package/dist/_shared/cache-stats.d.ts +23 -1
  3. package/dist/_shared/cache-stats.d.ts.map +1 -1
  4. package/dist/_shared/cache-stats.js +13 -1
  5. package/dist/_shared/error-text.d.ts +15 -0
  6. package/dist/_shared/error-text.d.ts.map +1 -0
  7. package/dist/_shared/error-text.js +21 -0
  8. package/dist/_shared/exports-resolver.d.ts +19 -7
  9. package/dist/_shared/exports-resolver.d.ts.map +1 -1
  10. package/dist/_shared/exports-resolver.js +1 -1
  11. package/dist/_shared/retry.d.ts.map +1 -1
  12. package/dist/_shared/retry.js +4 -2
  13. package/dist/constants.d.ts +5 -11
  14. package/dist/constants.d.ts.map +1 -1
  15. package/dist/constants.js +23 -49
  16. package/dist/runtime/assets-loader.js +1 -1
  17. package/dist/runtime/esbuild-service.d.ts +4 -3
  18. package/dist/runtime/esbuild-service.d.ts.map +1 -1
  19. package/dist/runtime/esbuild-service.js +9 -4
  20. package/dist/runtime/installed-runtimes.d.ts +4 -3
  21. package/dist/runtime/installed-runtimes.d.ts.map +1 -1
  22. package/dist/runtime/long-running-handle.d.ts +3 -3
  23. package/dist/runtime/long-running-handle.d.ts.map +1 -1
  24. package/dist/runtime/os-contracts.d.ts +7 -5
  25. package/dist/runtime/os-contracts.d.ts.map +1 -1
  26. package/dist/runtime/port-registry.d.ts +28 -0
  27. package/dist/runtime/port-registry.d.ts.map +1 -1
  28. package/dist/runtime/port-registry.js +79 -9
  29. package/dist/runtime/process-logs.d.ts +9 -1
  30. package/dist/runtime/process-logs.d.ts.map +1 -1
  31. package/dist/runtime/project-detect.d.ts +6 -1
  32. package/dist/runtime/project-detect.d.ts.map +1 -1
  33. package/dist/runtime/require-resolver.d.ts.map +1 -1
  34. package/dist/runtime/require-resolver.js +3 -4
  35. package/dist/runtime/runtime-registry.d.ts +4 -3
  36. package/dist/runtime/runtime-registry.d.ts.map +1 -1
  37. package/dist/runtime/runtime-registry.js +2 -1
  38. package/dist/runtime/sqlite-runtime-fs-bridge.js +1 -5
  39. package/dist/runtime/virtual-socket-kernel.d.ts.map +1 -1
  40. package/dist/runtime/virtual-socket-kernel.generated.d.ts +2 -2
  41. package/dist/runtime/virtual-socket-kernel.generated.d.ts.map +1 -1
  42. package/dist/runtime/virtual-socket-kernel.generated.js +3 -3
  43. package/dist/runtime/virtual-socket-kernel.js +10 -5
  44. package/dist/runtime/wasm-runner.d.ts.map +1 -1
  45. package/dist/runtime/wasm-runner.js +22 -14
  46. package/dist/shell/features.d.ts.map +1 -1
  47. package/dist/shell/features.js +5 -4
  48. package/dist/shell/unix-commands.d.ts +14 -1
  49. package/dist/shell/unix-commands.d.ts.map +1 -1
  50. package/dist/shell/unix-commands.js +115 -69
  51. package/dist/substrate/lifo/commands/net/ip.d.ts.map +1 -1
  52. package/dist/substrate/lifo/commands/system/ps.d.ts +2 -1
  53. package/dist/substrate/lifo/commands/system/ps.d.ts.map +1 -1
  54. package/dist/substrate/lifo/kernel/ServiceManager.d.ts.map +1 -1
  55. package/dist/substrate/lifo/kernel/ServiceManager.js +0 -2
  56. package/dist/substrate/lifo/kernel/network/NetworkStack.d.ts +22 -11
  57. package/dist/substrate/lifo/kernel/network/NetworkStack.d.ts.map +1 -1
  58. package/dist/substrate/lifo/kernel/network/NetworkStack.js +0 -2
  59. package/dist/substrate/lifo/kernel/network/tunnel/WebSocketTunnel.d.ts +2 -1
  60. package/dist/substrate/lifo/kernel/network/tunnel/WebSocketTunnel.d.ts.map +1 -1
  61. package/dist/substrate/lifo/kernel/network/tunnel/WebSocketTunnel.js +3 -4
  62. package/dist/substrate/lifo/kernel/vfs/VFS.d.ts +17 -0
  63. package/dist/substrate/lifo/kernel/vfs/VFS.d.ts.map +1 -1
  64. package/dist/substrate/lifo/kernel/vfs/VFS.js +33 -0
  65. package/dist/substrate/lifo/node-compat/esbuild.d.ts.map +1 -1
  66. package/dist/substrate/lifo/node-compat/esbuild.js +2 -4
  67. package/dist/substrate/lifo/node-compat/index.d.ts.map +1 -1
  68. package/dist/substrate/lifo/node-compat/index.js +3 -2
  69. package/dist/substrate/lifo/node-compat/loose-equality.d.ts +19 -0
  70. package/dist/substrate/lifo/node-compat/loose-equality.d.ts.map +1 -0
  71. package/dist/substrate/lifo/node-compat/loose-equality.js +91 -0
  72. package/dist/substrate/lifo/node-compat/process.d.ts.map +1 -1
  73. package/dist/substrate/lifo/node-compat/process.js +6 -3
  74. package/dist/substrate/lifo/shell/Shell.d.ts +19 -0
  75. package/dist/substrate/lifo/shell/Shell.d.ts.map +1 -1
  76. package/dist/substrate/lifo/shell/Shell.js +86 -75
  77. package/dist/substrate/lifo/shell/lexer.d.ts +9 -0
  78. package/dist/substrate/lifo/shell/lexer.d.ts.map +1 -1
  79. package/dist/substrate/lifo/shell/lexer.js +40 -0
  80. package/dist/vfs/seed-project.d.ts.map +1 -1
  81. package/dist/vfs/seed-project.js +9 -3
  82. package/dist/vfs/sqlite-vfs.d.ts +63 -31
  83. package/dist/vfs/sqlite-vfs.d.ts.map +1 -1
  84. package/dist/vfs/sqlite-vfs.js +206 -41
  85. package/package.json +3 -2
  86. package/src/_shared/cache-stats.ts +27 -1
  87. package/src/_shared/error-text.ts +21 -0
  88. package/src/_shared/exports-resolver.ts +24 -4
  89. package/src/_shared/retry.ts +5 -3
  90. package/src/constants.ts +37 -53
  91. package/src/runtime/assets-loader.ts +1 -1
  92. package/src/runtime/esbuild-service.ts +16 -11
  93. package/src/runtime/installed-runtimes.ts +4 -3
  94. package/src/runtime/long-running-handle.ts +3 -3
  95. package/src/runtime/os-contracts.ts +7 -5
  96. package/src/runtime/port-registry.ts +97 -9
  97. package/src/runtime/process-logs.ts +12 -3
  98. package/src/runtime/project-detect.ts +7 -1
  99. package/src/runtime/require-resolver.ts +7 -7
  100. package/src/runtime/runtime-registry.ts +9 -7
  101. package/src/runtime/sqlite-runtime-fs-bridge.ts +9 -6
  102. package/src/runtime/virtual-socket-kernel.generated.ts +3 -3
  103. package/src/runtime/virtual-socket-kernel.ts +10 -5
  104. package/src/runtime/wasm-runner.ts +55 -29
  105. package/src/shell/features.ts +5 -4
  106. package/src/shell/unix-commands.ts +273 -128
  107. package/src/substrate/lifo/commands/net/ip.ts +15 -15
  108. package/src/substrate/lifo/commands/system/ps.ts +2 -1
  109. package/src/substrate/lifo/kernel/ServiceManager.ts +0 -2
  110. package/src/substrate/lifo/kernel/network/NetworkStack.ts +24 -17
  111. package/src/substrate/lifo/kernel/network/tunnel/WebSocketTunnel.ts +37 -12
  112. package/src/substrate/lifo/kernel/vfs/VFS.ts +34 -0
  113. package/src/substrate/lifo/node-compat/esbuild.ts +16 -8
  114. package/src/substrate/lifo/node-compat/index.ts +3 -2
  115. package/src/substrate/lifo/node-compat/loose-equality.ts +81 -0
  116. package/src/substrate/lifo/node-compat/process.ts +5 -2
  117. package/src/substrate/lifo/shell/Shell.ts +92 -70
  118. package/src/substrate/lifo/shell/lexer.ts +38 -0
  119. package/src/vfs/seed-project.ts +13 -6
  120. package/src/vfs/sqlite-vfs.ts +233 -77
  121. package/dist/_shared/install-phase.d.ts +0 -47
  122. package/dist/_shared/install-phase.d.ts.map +0 -1
  123. package/dist/_shared/install-phase.js +0 -1
  124. package/dist/_shared/rpc-dispose.d.ts +0 -4
  125. package/dist/_shared/rpc-dispose.d.ts.map +0 -1
  126. package/dist/_shared/rpc-dispose.js +0 -30
  127. package/dist/_shared/w7-frame.d.ts +0 -79
  128. package/dist/_shared/w7-frame.d.ts.map +0 -1
  129. package/dist/_shared/w7-frame.js +0 -839
  130. package/dist/_shared/weighted-credit-pool.d.ts +0 -61
  131. package/dist/_shared/weighted-credit-pool.d.ts.map +0 -1
  132. package/dist/_shared/weighted-credit-pool.js +0 -196
  133. package/dist/observability/diag-counters.d.ts +0 -236
  134. package/dist/observability/diag-counters.d.ts.map +0 -1
  135. package/dist/observability/diag-counters.js +0 -195
  136. package/dist/observability/heap-estimate.d.ts +0 -225
  137. package/dist/observability/heap-estimate.d.ts.map +0 -1
  138. package/dist/observability/heap-estimate.js +0 -185
  139. package/dist/observability/heavy-alloc-coord.d.ts +0 -93
  140. package/dist/observability/heavy-alloc-coord.d.ts.map +0 -1
  141. package/dist/observability/heavy-alloc-coord.js +0 -181
  142. package/dist/observability/oom-classify.d.ts +0 -138
  143. package/dist/observability/oom-classify.d.ts.map +0 -1
  144. package/dist/observability/oom-classify.js +0 -261
  145. package/dist/observability/oom-discriminator.d.ts +0 -176
  146. package/dist/observability/oom-discriminator.d.ts.map +0 -1
  147. package/dist/observability/oom-discriminator.js +0 -272
  148. package/src/_shared/install-phase.ts +0 -56
  149. package/src/_shared/rpc-dispose.ts +0 -31
  150. package/src/_shared/w7-frame.ts +0 -1012
  151. package/src/_shared/weighted-credit-pool.ts +0 -244
  152. package/src/observability/diag-counters.ts +0 -343
  153. package/src/observability/heap-estimate.ts +0 -331
  154. package/src/observability/heavy-alloc-coord.ts +0 -241
  155. package/src/observability/oom-classify.ts +0 -253
  156. package/src/observability/oom-discriminator.ts +0 -405
@@ -18,7 +18,7 @@ import {
18
18
  type TerminalFdState,
19
19
  assignScalar,
20
20
  } from './interpreter.js';
21
- import { lex } from './lexer.js';
21
+ import { continuationState, lex } from './lexer.js';
22
22
  import { TokenKind } from './types.js';
23
23
  import { HistoryManager } from './history.js';
24
24
  import { JobTable } from './jobs.js';
@@ -54,6 +54,9 @@ export function formatShellPrompt(env: Record<string, string>, cwd: string): str
54
54
  return `${BOLD}${GREEN}${user}@${host}${RESET}:${BOLD}${BLUE}${displayPath}${RESET}$ `;
55
55
  }
56
56
 
57
+ /** PS2 — shown while an accepted line has not closed into a command yet. */
58
+ const CONTINUATION_PROMPT = '> ';
59
+
57
60
  export interface ExecuteOptions {
58
61
  cwd?: string;
59
62
  env?: Record<string, string>;
@@ -136,6 +139,13 @@ export class Shell {
136
139
  // Paste queue for multiline paste support
137
140
  pasteQueue: string[] = [];
138
141
 
142
+ /**
143
+ * Accepted lines that do not form a complete command yet: an unclosed
144
+ * quote or a trailing `\` keeps the shell reading under PS2, as bash
145
+ * does, instead of executing a truncated command.
146
+ */
147
+ private pendingLine: string | null = null;
148
+
139
149
  /**
140
150
  * Keystrokes that arrived while a foreground command owned the terminal and
141
151
  * nothing was reading stdin. A tty buffers type-ahead and hands it to the
@@ -267,6 +277,15 @@ export class Shell {
267
277
  return this.vfs;
268
278
  }
269
279
 
280
+ /**
281
+ * The `runAs` host this shell re-credentials through. A caller building a
282
+ * second Shell over the same kernel needs it, or its commands lose the
283
+ * identity-transition path `sudo` and `su` are dispatched on.
284
+ */
285
+ getRunAsHost(): CommandRunAsHost | undefined {
286
+ return this.commandIdentity.runAs;
287
+ }
288
+
270
289
  getRegistry(): CommandRegistry {
271
290
  return this.registry;
272
291
  }
@@ -442,6 +461,11 @@ export class Shell {
442
461
  }
443
462
 
444
463
  printPrompt(): void {
464
+ if (this.pendingLine !== null) {
465
+ this.terminal.write(CONTINUATION_PROMPT);
466
+ return;
467
+ }
468
+
445
469
  // Report finished background jobs from JobTable (legacy)
446
470
  const doneJobs = this.jobTable.collectDone();
447
471
  for (const job of doneJobs) {
@@ -462,57 +486,32 @@ export class Shell {
462
486
  return;
463
487
  }
464
488
 
465
- // Multiline paste detection: if data contains newlines and has multiple chars,
466
- // split into lines and execute them sequentially via the paste queue
489
+ // Multiline paste detection: if data contains newlines and has multiple
490
+ // chars, split into lines and accept them sequentially via the paste
491
+ // queue. Middle segments are queued even when empty — a blank pasted
492
+ // line is a newline inside an open quote; only the tail after the final
493
+ // newline is dropped when empty, because no Enter followed it.
467
494
  if (!this.running && data.length > 1 && /[\r\n]/.test(data)) {
468
495
  const lines = data.split(/\r\n|\r|\n/);
469
- for (let i = 0; i < lines.length; i++) {
470
- const segment = lines[i];
471
- if (i === 0) {
472
- // First segment: append to current lineBuffer and execute
473
- if (segment) {
474
- this.lineBuffer = this.lineBuffer.slice(0, this.cursorPos) + segment + this.lineBuffer.slice(this.cursorPos);
475
- this.cursorPos += segment.length;
476
- }
477
- if (i < lines.length - 1) {
478
- // There's a newline after this segment, execute it
479
- this.redrawLine();
480
- this.terminal.write('\r\n');
481
- const line = this.lineBuffer.trim();
482
- this.lineBuffer = '';
483
- this.cursorPos = 0;
484
- this.historyIndex = -1;
485
- if (line) {
486
- this.history.push(line);
487
- // Queue remaining lines before executing
488
- for (let j = 1; j < lines.length - 1; j++) {
489
- if (lines[j]) this.pasteQueue.push(lines[j]);
490
- }
491
- // Last segment (after final newline) goes into lineBuffer without executing
492
- const lastSegment = lines[lines.length - 1];
493
- if (lastSegment) {
494
- this.pasteQueue.push(lastSegment);
495
- }
496
- this.executeLine(line);
497
- return;
498
- } else {
499
- // Empty first line, queue the rest
500
- for (let j = 1; j < lines.length - 1; j++) {
501
- if (lines[j]) this.pasteQueue.push(lines[j]);
502
- }
503
- const lastSegment = lines[lines.length - 1];
504
- if (lastSegment) {
505
- this.pasteQueue.push(lastSegment);
506
- }
507
- this.drainPasteQueue();
508
- return;
509
- }
510
- }
511
- }
496
+ const first = lines[0];
497
+ if (first) {
498
+ this.lineBuffer = this.lineBuffer.slice(0, this.cursorPos) + first + this.lineBuffer.slice(this.cursorPos);
499
+ this.cursorPos += first.length;
500
+ }
501
+ this.redrawLine();
502
+ this.terminal.write('\r\n');
503
+ const line = this.lineBuffer;
504
+ this.lineBuffer = '';
505
+ this.cursorPos = 0;
506
+ this.historyIndex = -1;
507
+ for (let j = 1; j < lines.length - 1; j++) {
508
+ this.pasteQueue.push(lines[j]);
512
509
  }
513
- // If we get here, data had newlines but only one segment (shouldn't happen)
514
- // Fall through to normal handling
515
- if (this.lineBuffer) this.redrawLine();
510
+ const lastSegment = lines[lines.length - 1];
511
+ if (lastSegment) {
512
+ this.pasteQueue.push(lastSegment);
513
+ }
514
+ this.acceptLine(line);
516
515
  return;
517
516
  }
518
517
 
@@ -541,6 +540,7 @@ export class Shell {
541
540
  this.abortController.abort(signalAbortReason(signal));
542
541
  } else if (signal === 'INT') {
543
542
  this.terminal.write('^C\r\n');
543
+ this.pendingLine = null;
544
544
  this.lineBuffer = '';
545
545
  this.cursorPos = 0;
546
546
  this.screenCursorRow = 0;
@@ -606,18 +606,12 @@ export class Shell {
606
606
  // Enter
607
607
  if (data === '\r') {
608
608
  this.terminal.write('\r\n');
609
- const line = this.lineBuffer.trim();
609
+ const line = this.lineBuffer;
610
610
  this.lineBuffer = '';
611
611
  this.cursorPos = 0;
612
612
  this.screenCursorRow = 0;
613
613
  this.historyIndex = -1;
614
-
615
- if (line) {
616
- this.history.push(line);
617
- this.executeLine(line);
618
- } else {
619
- this.printPrompt();
620
- }
614
+ this.acceptLine(line);
621
615
  return;
622
616
  }
623
617
 
@@ -802,6 +796,7 @@ export class Shell {
802
796
  }
803
797
 
804
798
  private getPromptWidth(): number {
799
+ if (this.pendingLine !== null) return CONTINUATION_PROMPT.length;
805
800
  const { displayPath, user, host } = shellPromptParts(this.env, this.cwd);
806
801
  // "user@host:path$ " — count visible chars only (no ANSI codes)
807
802
  return user.length + 1 + host.length + 1 + displayPath.length + 2;
@@ -822,7 +817,9 @@ export class Shell {
822
817
  this.terminal.write('\x1b[J');
823
818
 
824
819
  // Rewrite prompt + buffer
825
- this.terminal.write(formatShellPrompt(this.env, this.cwd));
820
+ this.terminal.write(
821
+ this.pendingLine === null ? formatShellPrompt(this.env, this.cwd) : CONTINUATION_PROMPT,
822
+ );
826
823
  this.terminal.write(this.lineBuffer);
827
824
 
828
825
  // After writing all content, figure out which row the cursor is on.
@@ -868,18 +865,10 @@ export class Shell {
868
865
 
869
866
  drainPasteQueue(): void {
870
867
  const next = this.pasteQueue.shift();
871
- if (next !== undefined) {
872
- const line = next.trim();
873
- if (line) {
874
- this.terminal.write(line);
875
- this.terminal.write('\r\n');
876
- this.history.push(line);
877
- this.executeLine(line);
878
- } else {
879
- // Skip empty lines, try next
880
- this.drainPasteQueue();
881
- }
882
- }
868
+ if (next === undefined) return;
869
+ this.terminal.write(next);
870
+ this.terminal.write('\r\n');
871
+ this.acceptLine(next);
883
872
  }
884
873
 
885
874
  private moveCursorLeft(): void {
@@ -943,6 +932,39 @@ export class Shell {
943
932
  this.redrawLine();
944
933
  }
945
934
 
935
+ /**
936
+ * A line the user finished with Enter. If it leaves a quote open or ends
937
+ * in a line continuation it is not a command yet: bash buffers it, shows
938
+ * PS2 and keeps reading, and so does this shell. A `\<newline>` join drops
939
+ * both characters; a quoted join keeps the newline in the string.
940
+ */
941
+ private acceptLine(rawLine: string): void {
942
+ let command: string;
943
+ if (this.pendingLine === null) {
944
+ command = rawLine.trim();
945
+ if (!command) {
946
+ this.printPrompt();
947
+ this.drainPasteQueue();
948
+ return;
949
+ }
950
+ } else {
951
+ command = continuationState(this.pendingLine) === 'backslash'
952
+ ? this.pendingLine.slice(0, -1) + rawLine
953
+ : this.pendingLine + '\n' + rawLine;
954
+ }
955
+
956
+ if (continuationState(command) !== null) {
957
+ this.pendingLine = command;
958
+ this.printPrompt();
959
+ this.drainPasteQueue();
960
+ return;
961
+ }
962
+
963
+ this.pendingLine = null;
964
+ this.history.push(command);
965
+ this.executeLine(command);
966
+ }
967
+
946
968
  async executeLine(line: string): Promise<void> {
947
969
  // History expansion
948
970
  const expanded = this.historyManager.expand(line);
@@ -69,6 +69,44 @@ export function lex(input: string): Token[] {
69
69
  return tokens;
70
70
  }
71
71
 
72
+ export type ContinuationState = 'single' | 'double' | 'backslash';
73
+
74
+ /**
75
+ * How `input` leaves the lexer at end of input: inside an unclosed single or
76
+ * double quote, or ending in a `\` line continuation (which bash also honors
77
+ * inside double quotes, where `\<newline>` is removed). The interactive shell
78
+ * uses this to keep reading (PS2) instead of executing a truncated command.
79
+ * Comments are skipped exactly where lex() treats `#` as one.
80
+ */
81
+ export function continuationState(input: string): ContinuationState | null {
82
+ let quote: '"' | "'" | null = null;
83
+ for (let i = 0; i < input.length; i++) {
84
+ const ch = input[i];
85
+ if (quote === "'") {
86
+ if (ch === "'") quote = null;
87
+ continue;
88
+ }
89
+ if (ch === '\\') {
90
+ if (i === input.length - 1) return 'backslash';
91
+ i++;
92
+ continue;
93
+ }
94
+ if (quote === '"') {
95
+ if (ch === '"') quote = null;
96
+ continue;
97
+ }
98
+ if (ch === "'" || ch === '"') {
99
+ quote = ch;
100
+ continue;
101
+ }
102
+ if (ch === '#') {
103
+ while (i + 1 < input.length && input[i + 1] !== '\n') i++;
104
+ }
105
+ }
106
+ if (quote === null) return null;
107
+ return quote === '"' ? 'double' : 'single';
108
+ }
109
+
72
110
  function tryOperator(input: string, pos: number): { token: Token; end: number } | null {
73
111
  const ch = input[pos];
74
112
  const next = input[pos + 1];
@@ -18,13 +18,14 @@
18
18
  * - Basename injection (commit 2) handles the /preview/ basepath wiring.
19
19
  */
20
20
 
21
+ import type { SqliteVFS } from './sqlite-vfs.js';
21
22
  import type {
22
- SqliteVFS,
23
23
  BatchInodeEntry,
24
24
  BatchChunkEntry,
25
- } from './sqlite-vfs.js';
26
- import { CHUNK_SIZE } from '../constants.js';
25
+ } from '@nimbus-sh/platform/w7-frame.js';
26
+ import { CHUNK_SIZE } from '@nimbus-sh/platform/limits.js';
27
27
  import { enc } from '../_shared/bytes.js';
28
+ import { errorText } from '../_shared/error-text.js';
28
29
  import { CRED_KERNEL } from '../runtime/os-contracts.js';
29
30
 
30
31
  /** Sentinel: if present, seed never runs again (until user deletes it). */
@@ -1085,9 +1086,15 @@ export function seedProject(
1085
1086
  chunks: [{ path: SEED_SENTINEL_PATH, chunkId: 0, data: sentinelData }],
1086
1087
  });
1087
1088
  log?.(`[seed] sentinel written → ${SEED_SENTINEL_PATH}`);
1088
- } catch (e: any) {
1089
- log?.(`[seed] failed: ${e?.message || e}`);
1090
- return { seeded: false, files: fileCount, reason: e?.message || 'write-failed' };
1089
+ } catch (e) {
1090
+ log?.(`[seed] failed: ${errorText(e)}`);
1091
+ // `reason` keeps its own fallback: a caught value carrying no message
1092
+ // must still report 'write-failed', not the value's text.
1093
+ const reason =
1094
+ typeof e === 'object' && e !== null && 'message' in e && e.message
1095
+ ? String(e.message)
1096
+ : 'write-failed';
1097
+ return { seeded: false, files: fileCount, reason };
1091
1098
  }
1092
1099
 
1093
1100
  return { seeded: true, files: fileCount };