@livingdata/pipex 0.0.8 → 0.0.10

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 (90) hide show
  1. package/README.md +186 -16
  2. package/dist/__tests__/errors.js +162 -0
  3. package/dist/__tests__/helpers.js +41 -0
  4. package/dist/__tests__/types.js +8 -0
  5. package/dist/cli/__tests__/condition.js +23 -0
  6. package/dist/cli/__tests__/dag.js +154 -0
  7. package/dist/cli/__tests__/pipeline-loader.js +267 -0
  8. package/dist/cli/__tests__/pipeline-runner.js +257 -0
  9. package/dist/cli/__tests__/state-persistence.js +80 -0
  10. package/dist/cli/__tests__/state.js +58 -0
  11. package/dist/cli/__tests__/step-runner.js +116 -0
  12. package/dist/cli/commands/bundle.js +35 -0
  13. package/dist/cli/commands/cat.js +54 -0
  14. package/dist/cli/commands/clean.js +22 -0
  15. package/dist/cli/commands/exec.js +89 -0
  16. package/dist/cli/commands/export.js +32 -0
  17. package/dist/cli/commands/inspect.js +58 -0
  18. package/dist/cli/commands/list.js +39 -0
  19. package/dist/cli/commands/logs.js +54 -0
  20. package/dist/cli/commands/prune.js +26 -0
  21. package/dist/cli/commands/rm-step.js +41 -0
  22. package/dist/cli/commands/rm.js +27 -0
  23. package/dist/cli/commands/run-bundle.js +59 -0
  24. package/dist/cli/commands/run.js +44 -0
  25. package/dist/cli/commands/show.js +108 -0
  26. package/dist/cli/condition.js +11 -0
  27. package/dist/cli/dag.js +143 -0
  28. package/dist/cli/index.js +24 -105
  29. package/dist/cli/interactive-reporter.js +227 -0
  30. package/dist/cli/pipeline-loader.js +10 -110
  31. package/dist/cli/pipeline-runner.js +256 -111
  32. package/dist/cli/reporter.js +2 -107
  33. package/dist/cli/state.js +30 -9
  34. package/dist/cli/step-loader.js +25 -0
  35. package/dist/cli/step-resolver.js +111 -0
  36. package/dist/cli/step-runner.js +226 -0
  37. package/dist/cli/utils.js +3 -0
  38. package/dist/core/__tests__/bundle.js +663 -0
  39. package/dist/core/__tests__/condition.js +23 -0
  40. package/dist/core/__tests__/dag.js +154 -0
  41. package/dist/core/__tests__/env-file.test.js +41 -0
  42. package/dist/core/__tests__/event-aggregator.js +244 -0
  43. package/dist/core/__tests__/pipeline-loader.js +267 -0
  44. package/dist/core/__tests__/pipeline-runner.js +257 -0
  45. package/dist/core/__tests__/state-persistence.js +80 -0
  46. package/dist/core/__tests__/state.js +58 -0
  47. package/dist/core/__tests__/step-runner.js +118 -0
  48. package/dist/core/__tests__/stream-reporter.js +142 -0
  49. package/dist/core/__tests__/transport.js +50 -0
  50. package/dist/core/__tests__/utils.js +40 -0
  51. package/dist/core/bundle.js +130 -0
  52. package/dist/core/condition.js +11 -0
  53. package/dist/core/dag.js +143 -0
  54. package/dist/core/env-file.js +6 -0
  55. package/dist/core/event-aggregator.js +114 -0
  56. package/dist/core/index.js +14 -0
  57. package/dist/core/pipeline-loader.js +81 -0
  58. package/dist/core/pipeline-runner.js +360 -0
  59. package/dist/core/reporter.js +11 -0
  60. package/dist/core/state.js +110 -0
  61. package/dist/core/step-loader.js +25 -0
  62. package/dist/core/step-resolver.js +117 -0
  63. package/dist/core/step-runner.js +225 -0
  64. package/dist/core/stream-reporter.js +41 -0
  65. package/dist/core/transport.js +9 -0
  66. package/dist/core/utils.js +56 -0
  67. package/dist/engine/__tests__/workspace.js +288 -0
  68. package/dist/engine/docker-executor.js +32 -6
  69. package/dist/engine/index.js +1 -0
  70. package/dist/engine/workspace.js +164 -66
  71. package/dist/errors.js +122 -0
  72. package/dist/index.js +3 -0
  73. package/dist/kits/__tests__/index.js +23 -0
  74. package/dist/kits/builtin/__tests__/node.js +74 -0
  75. package/dist/kits/builtin/__tests__/python.js +67 -0
  76. package/dist/kits/builtin/__tests__/shell.js +74 -0
  77. package/dist/kits/builtin/node.js +10 -5
  78. package/dist/kits/builtin/python.js +10 -5
  79. package/dist/kits/builtin/shell.js +2 -1
  80. package/dist/kits/index.js +2 -1
  81. package/package.json +6 -3
  82. package/dist/cli/types.js +0 -3
  83. package/dist/engine/docker-runtime.js +0 -65
  84. package/dist/engine/runtime.js +0 -2
  85. package/dist/kits/bash.js +0 -19
  86. package/dist/kits/builtin/bash.js +0 -19
  87. package/dist/kits/node.js +0 -56
  88. package/dist/kits/python.js +0 -51
  89. package/dist/kits/types.js +0 -1
  90. package/dist/reporter.js +0 -13
@@ -0,0 +1,288 @@
1
+ import { access, readdir, readlink, writeFile } from 'node:fs/promises';
2
+ import { join } from 'node:path';
3
+ import test from 'ava';
4
+ import { Workspace } from '../workspace.js';
5
+ import { WorkspaceError } from '../../errors.js';
6
+ import { createTmpDir } from '../../__tests__/helpers.js';
7
+ // -- create & open -----------------------------------------------------------
8
+ test('create makes staging/, runs/, caches/ directories', async (t) => {
9
+ const root = await createTmpDir();
10
+ const ws = await Workspace.create(root, 'ws-1');
11
+ const entries = await readdir(ws.root);
12
+ t.true(entries.includes('staging'));
13
+ t.true(entries.includes('runs'));
14
+ t.true(entries.includes('caches'));
15
+ });
16
+ test('create with custom ID uses that ID', async (t) => {
17
+ const root = await createTmpDir();
18
+ const ws = await Workspace.create(root, 'my-workspace');
19
+ t.is(ws.id, 'my-workspace');
20
+ });
21
+ test('create without ID auto-generates one', async (t) => {
22
+ const root = await createTmpDir();
23
+ const ws = await Workspace.create(root);
24
+ t.truthy(ws.id);
25
+ t.true(ws.id.length > 0);
26
+ });
27
+ test('open returns workspace with correct id and root', async (t) => {
28
+ const root = await createTmpDir();
29
+ await Workspace.create(root, 'existing');
30
+ const ws = await Workspace.open(root, 'existing');
31
+ t.is(ws.id, 'existing');
32
+ t.is(ws.root, join(root, 'existing'));
33
+ });
34
+ test('open throws if workspace missing', async (t) => {
35
+ const root = await createTmpDir();
36
+ await t.throwsAsync(async () => Workspace.open(root, 'nonexistent'));
37
+ });
38
+ // -- list & remove -----------------------------------------------------------
39
+ test('list returns sorted workspace IDs', async (t) => {
40
+ const root = await createTmpDir();
41
+ await Workspace.create(root, 'beta');
42
+ await Workspace.create(root, 'alpha');
43
+ await Workspace.create(root, 'gamma');
44
+ const ids = await Workspace.list(root);
45
+ t.deepEqual(ids, ['alpha', 'beta', 'gamma']);
46
+ });
47
+ test('list returns empty array if root does not exist', async (t) => {
48
+ const ids = await Workspace.list('/tmp/nonexistent-pipex-root-' + Date.now());
49
+ t.deepEqual(ids, []);
50
+ });
51
+ test('remove deletes workspace', async (t) => {
52
+ const root = await createTmpDir();
53
+ await Workspace.create(root, 'to-delete');
54
+ await Workspace.remove(root, 'to-delete');
55
+ const ids = await Workspace.list(root);
56
+ t.false(ids.includes('to-delete'));
57
+ });
58
+ test('remove throws WorkspaceError on path traversal with ..', async (t) => {
59
+ const root = await createTmpDir();
60
+ const error = await t.throwsAsync(async () => Workspace.remove(root, '../etc'));
61
+ t.true(error instanceof WorkspaceError);
62
+ });
63
+ test('remove throws WorkspaceError on path traversal with /', async (t) => {
64
+ const error = await t.throwsAsync(async () => Workspace.remove('/tmp', 'a/b'));
65
+ t.true(error instanceof WorkspaceError);
66
+ });
67
+ // -- run lifecycle: commit path ----------------------------------------------
68
+ test('prepareRun creates staging/{runId}/artifacts/', async (t) => {
69
+ const root = await createTmpDir();
70
+ const ws = await Workspace.create(root, 'run-test');
71
+ const runId = ws.generateRunId();
72
+ await ws.prepareRun(runId);
73
+ const stagingPath = ws.runStagingPath(runId);
74
+ await t.notThrowsAsync(async () => access(stagingPath));
75
+ await t.notThrowsAsync(async () => access(join(stagingPath, 'artifacts')));
76
+ });
77
+ test('commitRun moves staging to runs', async (t) => {
78
+ const root = await createTmpDir();
79
+ const ws = await Workspace.create(root, 'commit-test');
80
+ const runId = ws.generateRunId();
81
+ await ws.prepareRun(runId);
82
+ // Write a file into staging artifacts
83
+ await writeFile(join(ws.runStagingArtifactsPath(runId), 'file.txt'), 'hello');
84
+ await ws.commitRun(runId);
85
+ // Committed run should exist
86
+ await t.notThrowsAsync(async () => access(ws.runPath(runId)));
87
+ await t.notThrowsAsync(async () => access(join(ws.runArtifactsPath(runId), 'file.txt')));
88
+ });
89
+ test('committed run appears in listRuns', async (t) => {
90
+ const root = await createTmpDir();
91
+ const ws = await Workspace.create(root, 'list-runs');
92
+ const runId = ws.generateRunId();
93
+ await ws.prepareRun(runId);
94
+ await ws.commitRun(runId);
95
+ const runs = await ws.listRuns();
96
+ t.true(runs.includes(runId));
97
+ });
98
+ test('staging is empty after commit', async (t) => {
99
+ const root = await createTmpDir();
100
+ const ws = await Workspace.create(root, 'staging-empty');
101
+ const runId = ws.generateRunId();
102
+ await ws.prepareRun(runId);
103
+ await ws.commitRun(runId);
104
+ const staging = await readdir(join(ws.root, 'staging'));
105
+ t.deepEqual(staging, []);
106
+ });
107
+ // -- run lifecycle: discard path ---------------------------------------------
108
+ test('discardRun removes staging directory', async (t) => {
109
+ const root = await createTmpDir();
110
+ const ws = await Workspace.create(root, 'discard-test');
111
+ const runId = ws.generateRunId();
112
+ await ws.prepareRun(runId);
113
+ await ws.discardRun(runId);
114
+ const staging = await readdir(join(ws.root, 'staging'));
115
+ t.deepEqual(staging, []);
116
+ });
117
+ test('discarded run does not appear in listRuns', async (t) => {
118
+ const root = await createTmpDir();
119
+ const ws = await Workspace.create(root, 'discard-list');
120
+ const runId = ws.generateRunId();
121
+ await ws.prepareRun(runId);
122
+ await ws.discardRun(runId);
123
+ const runs = await ws.listRuns();
124
+ t.false(runs.includes(runId));
125
+ });
126
+ // -- cleanupStaging ----------------------------------------------------------
127
+ test('cleanupStaging removes all staging dirs', async (t) => {
128
+ const root = await createTmpDir();
129
+ const ws = await Workspace.create(root, 'cleanup-test');
130
+ await ws.prepareRun(ws.generateRunId());
131
+ await ws.prepareRun(ws.generateRunId());
132
+ await ws.cleanupStaging();
133
+ const staging = await readdir(join(ws.root, 'staging'));
134
+ t.deepEqual(staging, []);
135
+ });
136
+ test('cleanupStaging is no-op when empty', async (t) => {
137
+ const root = await createTmpDir();
138
+ const ws = await Workspace.create(root, 'cleanup-noop');
139
+ await t.notThrowsAsync(async () => ws.cleanupStaging());
140
+ });
141
+ // -- linkRun -----------------------------------------------------------------
142
+ test('linkRun creates step-runs/{stepId} symlink', async (t) => {
143
+ const root = await createTmpDir();
144
+ const ws = await Workspace.create(root, 'link-test');
145
+ const runId = ws.generateRunId();
146
+ await ws.prepareRun(runId);
147
+ await ws.commitRun(runId);
148
+ await ws.linkRun('my-step', runId);
149
+ const linkDir = join(ws.root, 'step-runs');
150
+ const entries = await readdir(linkDir);
151
+ t.true(entries.includes('my-step'));
152
+ });
153
+ test('linkRun replaces existing symlink with new target', async (t) => {
154
+ const root = await createTmpDir();
155
+ const ws = await Workspace.create(root, 'link-replace');
156
+ const runId1 = ws.generateRunId();
157
+ await ws.prepareRun(runId1);
158
+ await ws.commitRun(runId1);
159
+ await ws.linkRun('step-a', runId1);
160
+ const runId2 = ws.generateRunId();
161
+ await ws.prepareRun(runId2);
162
+ await ws.commitRun(runId2);
163
+ await ws.linkRun('step-a', runId2);
164
+ // Symlink must point to the second run, not the first
165
+ const target = await readlink(join(ws.root, 'step-runs', 'step-a'));
166
+ t.true(target.includes(runId2));
167
+ t.false(target.includes(runId1));
168
+ });
169
+ // -- pruneRuns ---------------------------------------------------------------
170
+ test('pruneRuns removes runs not in active set', async (t) => {
171
+ const root = await createTmpDir();
172
+ const ws = await Workspace.create(root, 'prune-test');
173
+ const keep = ws.generateRunId();
174
+ await ws.prepareRun(keep);
175
+ await ws.commitRun(keep);
176
+ const remove = ws.generateRunId();
177
+ await ws.prepareRun(remove);
178
+ await ws.commitRun(remove);
179
+ const removed = await ws.pruneRuns(new Set([keep]));
180
+ t.is(removed, 1);
181
+ const runs = await ws.listRuns();
182
+ t.true(runs.includes(keep));
183
+ t.false(runs.includes(remove));
184
+ });
185
+ test('pruneRuns keeps runs in active set', async (t) => {
186
+ const root = await createTmpDir();
187
+ const ws = await Workspace.create(root, 'prune-keep');
188
+ const run1 = ws.generateRunId();
189
+ await ws.prepareRun(run1);
190
+ await ws.commitRun(run1);
191
+ const run2 = ws.generateRunId();
192
+ await ws.prepareRun(run2);
193
+ await ws.commitRun(run2);
194
+ const removed = await ws.pruneRuns(new Set([run1, run2]));
195
+ t.is(removed, 0);
196
+ });
197
+ // -- caches ------------------------------------------------------------------
198
+ test('prepareCache creates cache directory', async (t) => {
199
+ const root = await createTmpDir();
200
+ const ws = await Workspace.create(root, 'cache-test');
201
+ const path = await ws.prepareCache('npm-cache');
202
+ await t.notThrowsAsync(async () => access(path));
203
+ });
204
+ test('listCaches lists cache names', async (t) => {
205
+ const root = await createTmpDir();
206
+ const ws = await Workspace.create(root, 'cache-list');
207
+ await ws.prepareCache('cache-a');
208
+ await ws.prepareCache('cache-b');
209
+ const caches = await ws.listCaches();
210
+ t.true(caches.includes('cache-a'));
211
+ t.true(caches.includes('cache-b'));
212
+ });
213
+ test('invalid cache name throws WorkspaceError', async (t) => {
214
+ const root = await createTmpDir();
215
+ const ws = await Workspace.create(root, 'cache-invalid');
216
+ const error = t.throws(() => ws.cachePath('invalid/name'));
217
+ t.true(error instanceof WorkspaceError);
218
+ t.true(error.message.includes('invalid/name'));
219
+ });
220
+ // -- running step markers ----------------------------------------------------
221
+ test('markStepRunning creates running/{stepId} file', async (t) => {
222
+ const root = await createTmpDir();
223
+ const ws = await Workspace.create(root, 'running-mark');
224
+ await ws.markStepRunning('build', { startedAt: '2024-01-01T00:00:00Z', pid: 12345 });
225
+ const entries = await readdir(join(ws.root, 'running'));
226
+ t.deepEqual(entries, ['build']);
227
+ });
228
+ test('markStepDone removes the running marker', async (t) => {
229
+ const root = await createTmpDir();
230
+ const ws = await Workspace.create(root, 'running-done');
231
+ await ws.markStepRunning('build', { startedAt: '2024-01-01T00:00:00Z', pid: 12345 });
232
+ await ws.markStepDone('build');
233
+ const entries = await readdir(join(ws.root, 'running'));
234
+ t.deepEqual(entries, []);
235
+ });
236
+ test('markStepDone is no-op for non-existent marker', async (t) => {
237
+ const root = await createTmpDir();
238
+ const ws = await Workspace.create(root, 'running-done-noop');
239
+ await t.notThrowsAsync(async () => ws.markStepDone('nonexistent'));
240
+ });
241
+ test('listRunningSteps returns all running steps with metadata', async (t) => {
242
+ const root = await createTmpDir();
243
+ const ws = await Workspace.create(root, 'running-list');
244
+ await ws.markStepRunning('build', { startedAt: '2024-01-01T00:00:00Z', pid: 111, stepName: 'Build' });
245
+ await ws.markStepRunning('test', { startedAt: '2024-01-01T00:01:00Z', pid: 222 });
246
+ const running = await ws.listRunningSteps();
247
+ t.is(running.length, 2);
248
+ const build = running.find(r => r.stepId === 'build');
249
+ t.is(build.pid, 111);
250
+ t.is(build.stepName, 'Build');
251
+ t.is(build.startedAt, '2024-01-01T00:00:00Z');
252
+ const testStep = running.find(r => r.stepId === 'test');
253
+ t.is(testStep.pid, 222);
254
+ t.is(testStep.stepName, undefined);
255
+ });
256
+ test('listRunningSteps returns empty array when no running directory', async (t) => {
257
+ const root = await createTmpDir();
258
+ const ws = await Workspace.create(root, 'running-empty');
259
+ const running = await ws.listRunningSteps();
260
+ t.deepEqual(running, []);
261
+ });
262
+ test('cleanupRunning removes all running markers', async (t) => {
263
+ const root = await createTmpDir();
264
+ const ws = await Workspace.create(root, 'running-cleanup');
265
+ await ws.markStepRunning('a', { startedAt: '2024-01-01T00:00:00Z', pid: 1 });
266
+ await ws.markStepRunning('b', { startedAt: '2024-01-01T00:00:00Z', pid: 2 });
267
+ await ws.cleanupRunning();
268
+ const running = await ws.listRunningSteps();
269
+ t.deepEqual(running, []);
270
+ });
271
+ test('cleanupRunning is no-op when no running directory', async (t) => {
272
+ const root = await createTmpDir();
273
+ const ws = await Workspace.create(root, 'running-cleanup-noop');
274
+ await t.notThrowsAsync(async () => ws.cleanupRunning());
275
+ });
276
+ // -- validation --------------------------------------------------------------
277
+ test('invalid run ID in runStagingPath throws WorkspaceError', async (t) => {
278
+ const root = await createTmpDir();
279
+ const ws = await Workspace.create(root, 'validate-staging');
280
+ const error = t.throws(() => ws.runStagingPath('bad/id'));
281
+ t.true(error instanceof WorkspaceError);
282
+ });
283
+ test('invalid run ID in runPath throws WorkspaceError', async (t) => {
284
+ const root = await createTmpDir();
285
+ const ws = await Workspace.create(root, 'validate-run');
286
+ const error = t.throws(() => ws.runPath('bad id'));
287
+ t.true(error instanceof WorkspaceError);
288
+ });
@@ -1,5 +1,6 @@
1
1
  import process from 'node:process';
2
2
  import { execa } from 'execa';
3
+ import { DockerNotAvailableError, ImagePullError, ContainerTimeoutError } from '../errors.js';
3
4
  import { ContainerExecutor } from './executor.js';
4
5
  /**
5
6
  * Build a minimal environment for the Docker CLI process.
@@ -22,8 +23,26 @@ export class DockerCliExecutor extends ContainerExecutor {
22
23
  try {
23
24
  await execa('docker', ['--version'], { env: this.env });
24
25
  }
26
+ catch (error) {
27
+ throw new DockerNotAvailableError({ cause: error });
28
+ }
29
+ }
30
+ /**
31
+ * Remove any leftover pipex containers for the given workspace.
32
+ * Called before pipeline execution to clean up after crashes.
33
+ */
34
+ async cleanupContainers(workspaceId) {
35
+ try {
36
+ const { stdout } = await execa('docker', [
37
+ 'ps', '-a', '--filter', `label=pipex.workspace=${workspaceId}`, '-q'
38
+ ], { env: this.env });
39
+ const ids = stdout.trim().split('\n').filter(Boolean);
40
+ if (ids.length > 0) {
41
+ await execa('docker', ['rm', '-f', ...ids], { env: this.env, reject: false });
42
+ }
43
+ }
25
44
  catch {
26
- throw new Error('Docker CLI not found. Please install Docker.');
45
+ // Best effort
27
46
  }
28
47
  }
29
48
  async run(workspace, request, onLogLine) {
@@ -31,15 +50,15 @@ export class DockerCliExecutor extends ContainerExecutor {
31
50
  // Use create+start instead of run: docker run cannot create mountpoints
32
51
  // for anonymous volumes inside read-only bind mounts (shadow paths).
33
52
  // docker create sets up the filesystem layer before readonly applies.
34
- const args = ['create', '--name', request.name, '--network', request.network];
53
+ const args = ['create', '--name', request.name, '--network', request.network, '--label', 'pipex=true', '--label', `pipex.workspace=${workspace.id}`];
35
54
  if (request.env) {
36
55
  for (const [key, value] of Object.entries(request.env)) {
37
56
  args.push('-e', `${key}=${value}`);
38
57
  }
39
58
  }
40
- // Mount inputs (committed artifacts, read-only)
59
+ // Mount inputs (committed run artifacts, read-only)
41
60
  for (const input of request.inputs) {
42
- const hostPath = workspace.artifactPath(input.artifactId);
61
+ const hostPath = workspace.runArtifactsPath(input.runId);
43
62
  args.push('-v', `${hostPath}:${input.containerPath}:ro`);
44
63
  }
45
64
  // Mount caches (persistent, read-write)
@@ -55,8 +74,8 @@ export class DockerCliExecutor extends ContainerExecutor {
55
74
  args.push('-v', `${mount.hostPath}:${mount.containerPath}:ro`);
56
75
  }
57
76
  }
58
- // Mount output (staging artifact, read-write)
59
- const outputHostPath = workspace.stagingPath(request.output.stagingArtifactId);
77
+ // Mount output (staging run artifacts, read-write)
78
+ const outputHostPath = workspace.runStagingArtifactsPath(request.output.stagingRunId);
60
79
  args.push('-v', `${outputHostPath}:${request.output.containerPath}:rw`, request.image, ...request.cmd);
61
80
  let exitCode = 0;
62
81
  let error;
@@ -90,6 +109,13 @@ export class DockerCliExecutor extends ContainerExecutor {
90
109
  exitCode = result.exitCode ?? 0;
91
110
  }
92
111
  catch (error_) {
112
+ if (error_ instanceof Error && 'timedOut' in error_ && error_.timedOut) {
113
+ throw new ContainerTimeoutError(request.timeoutSec ?? 0, { cause: error_ });
114
+ }
115
+ const stderr = error_ instanceof Error && 'stderr' in error_ ? String(error_.stderr) : '';
116
+ if (/unable to find image|pull access denied|manifest unknown/i.test(stderr)) {
117
+ throw new ImagePullError(request.image, { cause: error_ });
118
+ }
93
119
  exitCode = 1;
94
120
  error = error_ instanceof Error ? error_.message : String(error_);
95
121
  }
@@ -1,3 +1,4 @@
1
1
  export { Workspace } from './workspace.js';
2
2
  export { ContainerExecutor } from './executor.js';
3
3
  export { DockerCliExecutor } from './docker-executor.js';
4
+ export { PipexError, DockerError, DockerNotAvailableError, ImagePullError, ContainerTimeoutError, ContainerCrashError, ContainerCleanupError, WorkspaceError, ArtifactNotFoundError, StagingError, PipelineError, ValidationError, CyclicDependencyError, StepNotFoundError, KitError, MissingParameterError } from '../errors.js';