@augment-vir/node 31.0.1 → 31.1.1

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 (45) hide show
  1. package/dist/scripts/fix-ts-bin.script.d.ts +1 -0
  2. package/dist/scripts/fix-ts-bin.script.js +48 -0
  3. package/package.json +4 -4
  4. package/src/augments/docker.ts +118 -0
  5. package/src/augments/fs/dir-contents.ts +116 -0
  6. package/src/augments/fs/download.ts +31 -0
  7. package/src/augments/fs/json.ts +87 -0
  8. package/src/augments/fs/read-dir.ts +60 -0
  9. package/src/augments/fs/read-file.ts +18 -0
  10. package/src/augments/fs/symlink.ts +37 -0
  11. package/src/augments/fs/write.ts +18 -0
  12. package/src/augments/npm/query-workspace.ts +47 -0
  13. package/src/augments/npm/read-package-json.ts +28 -0
  14. package/src/augments/os/operating-system.ts +55 -0
  15. package/src/augments/path/ancestor.ts +78 -0
  16. package/src/augments/path/os-path.ts +45 -0
  17. package/src/augments/path/root.ts +14 -0
  18. package/src/augments/prisma.ts +174 -0
  19. package/src/augments/terminal/question.ts +142 -0
  20. package/src/augments/terminal/relevant-args.ts +81 -0
  21. package/src/augments/terminal/run-cli-script.ts +60 -0
  22. package/src/augments/terminal/shell.ts +312 -0
  23. package/src/docker/containers/container-info.ts +83 -0
  24. package/src/docker/containers/container-status.ts +110 -0
  25. package/src/docker/containers/copy-to-container.ts +34 -0
  26. package/src/docker/containers/docker-command-inputs.ts +119 -0
  27. package/src/docker/containers/kill-container.ts +25 -0
  28. package/src/docker/containers/run-command.ts +51 -0
  29. package/src/docker/containers/run-container.mock.ts +17 -0
  30. package/src/docker/containers/run-container.ts +92 -0
  31. package/src/docker/containers/try-or-kill-container.ts +18 -0
  32. package/src/docker/docker-image.ts +56 -0
  33. package/src/docker/docker-startup.ts +49 -0
  34. package/src/docker/run-docker-test.mock.ts +26 -0
  35. package/src/file-paths.mock.ts +29 -0
  36. package/src/index.ts +19 -0
  37. package/src/prisma/disable-ci-env.mock.ts +88 -0
  38. package/src/prisma/model-data.ts +213 -0
  39. package/src/prisma/prisma-client.ts +43 -0
  40. package/src/prisma/prisma-database.mock.ts +31 -0
  41. package/src/prisma/prisma-database.ts +35 -0
  42. package/src/prisma/prisma-errors.ts +45 -0
  43. package/src/prisma/prisma-migrations.ts +149 -0
  44. package/src/prisma/run-prisma-command.ts +59 -0
  45. package/src/scripts/fix-ts-bin.script.ts +60 -0
@@ -0,0 +1,312 @@
1
+ import {combineErrors, log, type Logger} from '@augment-vir/common';
2
+ import {type MaybePromise, PartialWithUndefined, RequiredAndNotNull} from '@augment-vir/core';
3
+ import {ChildProcess, ExecException, spawn} from 'node:child_process';
4
+ import {defineTypedCustomEvent, ListenTarget} from 'typed-event-target';
5
+
6
+ /**
7
+ * All output from {@link runShellCommand}.
8
+ *
9
+ * @category Node : Terminal : Util
10
+ * @category Package : @augment-vir/node
11
+ * @package [`@augment-vir/node`](https://www.npmjs.com/package/@augment-vir/node)
12
+ */
13
+ export type ShellOutput = {
14
+ error: undefined | Error;
15
+ stderr: string;
16
+ stdout: string;
17
+ exitCode: number | undefined;
18
+ exitSignal: NodeJS.Signals | undefined;
19
+ };
20
+
21
+ /**
22
+ * An event that indicates that the shell command just wrote to stdout.
23
+ *
24
+ * @category Node : Terminal : Util
25
+ * @category Package : @augment-vir/node
26
+ * @package [`@augment-vir/node`](https://www.npmjs.com/package/@augment-vir/node)
27
+ */
28
+ export class ShellStdoutEvent extends defineTypedCustomEvent<string | Buffer>()('shell-stdout') {}
29
+ /**
30
+ * An event that indicates that the shell command just wrote to stderr.
31
+ *
32
+ * @category Node : Terminal : Util
33
+ * @category Package : @augment-vir/node
34
+ * @package [`@augment-vir/node`](https://www.npmjs.com/package/@augment-vir/node)
35
+ */
36
+ export class ShellStderrEvent extends defineTypedCustomEvent<string | Buffer>()('shell-stderr') {}
37
+ /**
38
+ * An event that indicates that the shell command is finished. This contains an exit code or an exit
39
+ * signal. Based on the Node.js documentation, either one or the other is defined, never both at the
40
+ * same time.
41
+ *
42
+ * @category Node : Terminal : Util
43
+ * @category Package : @augment-vir/node
44
+ * @package [`@augment-vir/node`](https://www.npmjs.com/package/@augment-vir/node)
45
+ */
46
+ export class ShellDoneEvent extends defineTypedCustomEvent<{
47
+ exitCode: number | undefined;
48
+ exitSignal: NodeJS.Signals | undefined;
49
+ }>()('shell-done') {}
50
+ /**
51
+ * An event that indicates that the shell command errored.
52
+ *
53
+ * @category Node : Terminal : Util
54
+ * @category Package : @augment-vir/node
55
+ * @package [`@augment-vir/node`](https://www.npmjs.com/package/@augment-vir/node)
56
+ */
57
+ export class ShellErrorEvent extends defineTypedCustomEvent<Error>()('shell-error') {}
58
+
59
+ /**
60
+ * A shell command listen target that emits events.
61
+ *
62
+ * @category Node : Terminal : Util
63
+ * @category Package : @augment-vir/node
64
+ * @package [`@augment-vir/node`](https://www.npmjs.com/package/@augment-vir/node)
65
+ */
66
+ export class ShellTarget extends ListenTarget<
67
+ ShellStdoutEvent | ShellStderrEvent | ShellDoneEvent | ShellErrorEvent
68
+ > {
69
+ constructor(public readonly childProcess: ChildProcess) {
70
+ super();
71
+ }
72
+ }
73
+
74
+ /**
75
+ * Runs a shell command and returns a {@link ShellTarget} instance for directly hooking into shell
76
+ * events. This allows instant reactions to shell events but in a less convenient API compared to
77
+ * {@link runShellCommand}.
78
+ *
79
+ * @category Node : Terminal
80
+ * @category Package : @augment-vir/node
81
+ * @package [`@augment-vir/node`](https://www.npmjs.com/package/@augment-vir/node)
82
+ * @see
83
+ * - {@link runShellCommand}: a higher level and more succinct way of running a shell command.
84
+ */
85
+ export function streamShellCommand(
86
+ command: string,
87
+ cwd?: string,
88
+ shell = 'bash',
89
+ env: NodeJS.ProcessEnv = process.env,
90
+ hookUpToConsole = false,
91
+ ): ShellTarget {
92
+ const stdio = hookUpToConsole ? [process.stdin] : undefined;
93
+
94
+ const childProcess = spawn(command, {shell, cwd, env, stdio});
95
+ const shellTarget = new ShellTarget(childProcess);
96
+
97
+ /** Type guards. */
98
+ /* node:coverage ignore next 5 */
99
+ if (!childProcess.stdout) {
100
+ throw new Error(`stdout emitter was not created by exec for some reason.`);
101
+ } else if (!childProcess.stderr) {
102
+ throw new Error(`stderr emitter was not created by exec for some reason.`);
103
+ }
104
+
105
+ childProcess.stdout.on('data', (chunk) => {
106
+ shellTarget.dispatch(new ShellStdoutEvent({detail: chunk}));
107
+ });
108
+ childProcess.stderr.on('data', (chunk) => {
109
+ shellTarget.dispatch(new ShellStderrEvent({detail: chunk}));
110
+ });
111
+
112
+ /** Idk how to trigger the 'error' event. */
113
+ /* node:coverage ignore next 3 */
114
+ childProcess.on('error', (error) => {
115
+ shellTarget.dispatch(new ShellErrorEvent({detail: error}));
116
+ });
117
+ /**
118
+ * Based on the Node.js documentation, we should listen to "close" instead of "exit" because the
119
+ * io streams will be finished when "close" emits. Also "close" always emits after "exit"
120
+ * anyway.
121
+ */
122
+ childProcess.on('close', (inputExitCode, inputExitSignal) => {
123
+ /** Idk how to control exitCode or exitSignal being null or not-null. */
124
+ /* node:coverage ignore next 2 */
125
+ const exitCode: number | undefined = inputExitCode ?? undefined;
126
+ const exitSignal: NodeJS.Signals | undefined = inputExitSignal ?? undefined;
127
+
128
+ if ((exitCode !== undefined && exitCode !== 0) || exitSignal !== undefined) {
129
+ const execException: ExecException & {cwd?: string | undefined} = new Error(
130
+ `Command failed: ${command}`,
131
+ );
132
+ execException.code = exitCode;
133
+ execException.signal = exitSignal;
134
+ execException.cmd = command;
135
+ execException.killed = childProcess.killed;
136
+ execException.cwd = cwd;
137
+ shellTarget.dispatch(new ShellErrorEvent({detail: execException}));
138
+ }
139
+ shellTarget.dispatch(
140
+ new ShellDoneEvent({
141
+ detail: {
142
+ exitCode,
143
+ exitSignal,
144
+ },
145
+ }),
146
+ );
147
+ });
148
+
149
+ return shellTarget;
150
+ }
151
+
152
+ /**
153
+ * Options for {@link runShellCommand}.
154
+ *
155
+ * @category Node : Terminal : Util
156
+ * @category Package : @augment-vir/node
157
+ * @package [`@augment-vir/node`](https://www.npmjs.com/package/@augment-vir/node)
158
+ */
159
+ export type RunShellCommandOptions = {
160
+ cwd?: string | undefined;
161
+ env?: NodeJS.ProcessEnv | undefined;
162
+ shell?: string | undefined;
163
+ /** Automatically hook up stdout and stderr printing to the caller's console methods. */
164
+ hookUpToConsole?: boolean | undefined;
165
+ rejectOnError?: boolean | undefined;
166
+ /** Callback to call whenever the shell logs to stdout. */
167
+ stdoutCallback?: (stdout: string, childProcess: ChildProcess) => MaybePromise<void> | undefined;
168
+ /** Callback to call whenever the shell logs to stderr. */
169
+ stderrCallback?: (stderr: string, childProcess: ChildProcess) => MaybePromise<void> | undefined;
170
+ };
171
+
172
+ function prepareChunkForLogging(chunk: string | Buffer, trimEndingLine: boolean): string {
173
+ const stringified = chunk.toString();
174
+
175
+ return trimEndingLine ? stringified.replace(/\n$/, '') : stringified;
176
+ }
177
+
178
+ /**
179
+ * Runs a shell command and returns its output.
180
+ *
181
+ * @category Node : Terminal
182
+ * @category Package : @augment-vir/node
183
+ * @package [`@augment-vir/node`](https://www.npmjs.com/package/@augment-vir/node)
184
+ * @see
185
+ * - {@link streamShellCommand}: a lower level way of running a shell command that allows instant reactions to shell events.
186
+ */
187
+ export async function runShellCommand(
188
+ command: string,
189
+ options: RunShellCommandOptions = {},
190
+ ): Promise<ShellOutput> {
191
+ return new Promise<ShellOutput>((resolve, reject) => {
192
+ let stdout = '';
193
+ let stderr = '';
194
+ const errors: Error[] = [];
195
+
196
+ const shellTarget = streamShellCommand(
197
+ command,
198
+ options.cwd,
199
+ options.shell,
200
+ options.env,
201
+ options.hookUpToConsole,
202
+ );
203
+
204
+ shellTarget.listen(ShellStdoutEvent, ({detail: chunk}) => {
205
+ if (options.stdoutCallback) {
206
+ void options.stdoutCallback(
207
+ prepareChunkForLogging(chunk, false),
208
+ shellTarget.childProcess,
209
+ );
210
+ }
211
+ if (options.hookUpToConsole) {
212
+ process.stdout.write(prepareChunkForLogging(chunk, true) + '\n');
213
+ }
214
+ stdout += String(chunk);
215
+ });
216
+ shellTarget.listen(ShellStderrEvent, ({detail: chunk}) => {
217
+ if (options.stderrCallback) {
218
+ void options.stderrCallback(
219
+ prepareChunkForLogging(chunk, false),
220
+ shellTarget.childProcess,
221
+ );
222
+ }
223
+ if (options.hookUpToConsole) {
224
+ process.stderr.write(prepareChunkForLogging(chunk, true) + '\n');
225
+ }
226
+ stderr += String(chunk);
227
+ });
228
+ shellTarget.listen(ShellErrorEvent, ({detail: error}) => {
229
+ errors.push(error);
230
+ if (!options.rejectOnError) {
231
+ return;
232
+ }
233
+
234
+ /** Covering edge cases. */
235
+ /* node:coverage disable */
236
+ if (shellTarget.childProcess.connected) {
237
+ shellTarget.childProcess.disconnect();
238
+ }
239
+ if (
240
+ shellTarget.childProcess.exitCode == null &&
241
+ shellTarget.childProcess.signalCode == null &&
242
+ !shellTarget.childProcess.killed
243
+ ) {
244
+ shellTarget.childProcess.kill();
245
+ }
246
+ /* node:coverage enable */
247
+
248
+ shellTarget.destroy();
249
+
250
+ const rejectionErrorMessage: Error = combineErrors([new Error(stderr), ...errors]);
251
+ /** Reject now because the "done" listener won't get fired after killing the process. */
252
+ reject(rejectionErrorMessage);
253
+ });
254
+ shellTarget.listen(ShellDoneEvent, ({detail: {exitCode, exitSignal}}) => {
255
+ shellTarget.destroy();
256
+ resolve({
257
+ error: errors.length ? combineErrors(errors) : undefined,
258
+ stdout,
259
+ stderr,
260
+ exitCode,
261
+ exitSignal,
262
+ });
263
+ });
264
+ });
265
+ }
266
+
267
+ /**
268
+ * Options for {@link logShellOutput}.
269
+ *
270
+ * @category Node : Terminal : Util
271
+ * @category Package : @augment-vir/node
272
+ * @package [`@augment-vir/node`](https://www.npmjs.com/package/@augment-vir/node)
273
+ */
274
+ export type LogShellOutputOptions = PartialWithUndefined<{
275
+ logger: Logger;
276
+ withLabels: boolean;
277
+ ignoreError: boolean;
278
+ }>;
279
+
280
+ const defaultLogShellOutputOptions: RequiredAndNotNull<LogShellOutputOptions> = {
281
+ ignoreError: false,
282
+ logger: log,
283
+ withLabels: false,
284
+ };
285
+
286
+ /**
287
+ * Log the output of running a shell command. This is useful for quick debugging of shell commands.
288
+ *
289
+ * @category Node : Terminal : Util
290
+ * @category Package : @augment-vir/node
291
+ * @package [`@augment-vir/node`](https://www.npmjs.com/package/@augment-vir/node)
292
+ */
293
+ export function logShellOutput(
294
+ shellOutput: PartialWithUndefined<Omit<ShellOutput, 'exitSignal'>>,
295
+ {
296
+ ignoreError = defaultLogShellOutputOptions.ignoreError,
297
+ logger = defaultLogShellOutputOptions.logger,
298
+ withLabels = defaultLogShellOutputOptions.withLabels,
299
+ }: LogShellOutputOptions = defaultLogShellOutputOptions,
300
+ ) {
301
+ logger.if(withLabels).info('exit code');
302
+ logger.if(shellOutput.exitCode != undefined || withLabels).plain(shellOutput.exitCode || 0);
303
+
304
+ logger.if(withLabels).info('stdout');
305
+ logger.if(!!shellOutput.stdout || withLabels).plain(shellOutput.stdout || '');
306
+
307
+ logger.if(withLabels).info('stderr');
308
+ logger.if(!!shellOutput.stderr || withLabels).error(shellOutput.stderr || '');
309
+
310
+ logger.if(withLabels && !ignoreError).info('error');
311
+ logger.if((!!shellOutput.error || withLabels) && !ignoreError).error(shellOutput.error);
312
+ }
@@ -0,0 +1,83 @@
1
+ import type {JsonCompatibleArray, JsonCompatibleObject} from '@augment-vir/common';
2
+ import {runShellCommand} from '../../augments/terminal/shell.js';
3
+ import type {DockerContainerStatus} from './container-status.js';
4
+
5
+ /**
6
+ * Properties on {@link DockerContainerInfo}.State, retrieved from {@link getContainerInfo}.
7
+ *
8
+ * @category Node : Docker : Util
9
+ * @category Package : @augment-vir/node
10
+ * @package [`@augment-vir/node`](https://www.npmjs.com/package/@augment-vir/node)
11
+ */
12
+ export type DockerContainerInfoState = {
13
+ Status: DockerContainerStatus;
14
+ Running: boolean;
15
+ Paused: boolean;
16
+ Restarting: boolean;
17
+ OOMKilled: boolean;
18
+ Dead: boolean;
19
+ Pid: number;
20
+ ExitCode: number;
21
+ Error: string;
22
+ StartedAt: string;
23
+ FinishedAt: string;
24
+ };
25
+
26
+ /** This type signature is incomplete. Add to it as necessary. */
27
+
28
+ /**
29
+ * Properties on the output from {@link getContainerInfo}. Not all these properties are filled in all
30
+ * the way, particularly most of properties with nested objects.
31
+ *
32
+ * @category Node : Docker : Util
33
+ * @category Package : @augment-vir/node
34
+ * @package [`@augment-vir/node`](https://www.npmjs.com/package/@augment-vir/node)
35
+ */
36
+ export type DockerContainerInfo = Readonly<{
37
+ Id: string;
38
+ Created: string;
39
+ Path: string;
40
+ Args: ReadonlyArray<string>;
41
+ State: DockerContainerInfoState;
42
+ Image: string;
43
+ ResolvConfPath: string;
44
+ HostnamePath: string;
45
+ HostsPath: string;
46
+ LogPath: string;
47
+ Name: string;
48
+ RestartCount: number;
49
+ Driver: string;
50
+ Platform: string;
51
+ MountLabel: string;
52
+ ProcessLabel: string;
53
+ AppArmorProfile: string;
54
+ ExecIDs: unknown;
55
+ HostConfig: JsonCompatibleObject;
56
+ GraphDriver: JsonCompatibleObject;
57
+ Mounts: JsonCompatibleArray;
58
+ Config: JsonCompatibleObject;
59
+ NetworkSettings: JsonCompatibleObject;
60
+ }>;
61
+
62
+ export async function getContainerInfo(
63
+ containerNameOrId: string,
64
+ ): Promise<DockerContainerInfo | undefined> {
65
+ const command = `docker inspect '${containerNameOrId}'`;
66
+ const output = await runShellCommand(command);
67
+
68
+ if (output.stderr.includes('Error: No such object')) {
69
+ return undefined;
70
+ }
71
+
72
+ const parsedOutput = JSON.parse(output.stdout) as ReadonlyArray<DockerContainerInfo>;
73
+
74
+ /** Edge cases that I don't know how to intentionally trigger. */
75
+ /* node:coverage ignore next 5 */
76
+ if (parsedOutput.length === 0) {
77
+ throw new Error(`Got no output from "${command}"`);
78
+ } else if (parsedOutput.length > 1) {
79
+ throw new Error(`Got more than one output from "${command}"`);
80
+ }
81
+
82
+ return parsedOutput[0];
83
+ }
@@ -0,0 +1,110 @@
1
+ import {assert, waitUntil} from '@augment-vir/assert';
2
+ import {runShellCommand} from '../../augments/terminal/shell.js';
3
+
4
+ export async function getContainerLogs(
5
+ containerNameOrId: string,
6
+ latestLineCount?: number,
7
+ ): Promise<string> {
8
+ const latestLinesArg = latestLineCount == undefined ? '' : `--tail ${latestLineCount}`;
9
+ const logResult = await runShellCommand(
10
+ `docker logs ${latestLinesArg} '${containerNameOrId}'`,
11
+ {rejectOnError: true},
12
+ );
13
+ return logResult.stdout;
14
+ }
15
+
16
+ /**
17
+ * All possible statuses for an existing container.
18
+ *
19
+ * @category Node : Docker : Util
20
+ * @category Package : @augment-vir/node
21
+ * @package [`@augment-vir/node`](https://www.npmjs.com/package/@augment-vir/node)
22
+ */
23
+ export enum DockerContainerStatus {
24
+ Created = 'created',
25
+ Running = 'running',
26
+ Paused = 'paused',
27
+ Restarting = 'restarting',
28
+ Exited = 'exited',
29
+ Removing = 'removing',
30
+ Dead = 'dead',
31
+
32
+ /** This is not a native Docker status but indicates that the container does not exist. */
33
+ Removed = 'removed',
34
+ }
35
+
36
+ /**
37
+ * Statuses from {@link DockerContainerStatus} that indicate that a container has been exited in some
38
+ * way.
39
+ *
40
+ * @category Node : Docker : Util
41
+ * @category Package : @augment-vir/node
42
+ * @package [`@augment-vir/node`](https://www.npmjs.com/package/@augment-vir/node)
43
+ */
44
+ export const exitedDockerContainerStatuses = [
45
+ DockerContainerStatus.Dead,
46
+ DockerContainerStatus.Removed,
47
+ DockerContainerStatus.Exited,
48
+ ];
49
+
50
+ export async function getContainerStatus(
51
+ containerNameOrId: string,
52
+ ): Promise<DockerContainerStatus> {
53
+ const statusResult = await runShellCommand(
54
+ `docker container inspect -f '{{.State.Status}}' '${containerNameOrId}'`,
55
+ );
56
+
57
+ if (statusResult.exitCode !== 0) {
58
+ return DockerContainerStatus.Removed;
59
+ }
60
+
61
+ const status = statusResult.stdout.trim();
62
+ assert.isEnumValue(status, DockerContainerStatus, 'Unexpected container status value');
63
+
64
+ return status;
65
+ }
66
+
67
+ export async function waitUntilContainerRunning(
68
+ containerNameOrId: string,
69
+ failureMessage?: string | undefined,
70
+ ): Promise<void> {
71
+ await waitUntil.isTrue(
72
+ async (): Promise<boolean> => {
73
+ /** Check if logs can be accessed yet. */
74
+ await getContainerLogs(containerNameOrId, 1);
75
+ const status = await getContainerStatus(containerNameOrId);
76
+
77
+ return status === DockerContainerStatus.Running;
78
+ },
79
+ {},
80
+ failureMessage,
81
+ );
82
+ }
83
+
84
+ export async function waitUntilContainerRemoved(
85
+ containerNameOrId: string,
86
+ failureMessage?: string | undefined,
87
+ ): Promise<void> {
88
+ await waitUntil.isTrue(
89
+ async (): Promise<boolean> => {
90
+ const status = await getContainerStatus(containerNameOrId);
91
+ return status === DockerContainerStatus.Removed;
92
+ },
93
+ {},
94
+ failureMessage,
95
+ );
96
+ }
97
+
98
+ export async function waitUntilContainerExited(
99
+ containerNameOrId: string,
100
+ failureMessage?: string | undefined,
101
+ ): Promise<void> {
102
+ await waitUntil.isTrue(
103
+ async (): Promise<boolean> => {
104
+ const status = await getContainerStatus(containerNameOrId);
105
+ return exitedDockerContainerStatuses.includes(status);
106
+ },
107
+ {},
108
+ failureMessage,
109
+ );
110
+ }
@@ -0,0 +1,34 @@
1
+ import {addSuffix} from '@augment-vir/common';
2
+ import {stat} from 'node:fs/promises';
3
+ import {runShellCommand} from '../../augments/terminal/shell.js';
4
+
5
+ /**
6
+ * Parameters for `docker.container.copyTo`.
7
+ *
8
+ * @category Node : Docker : Util
9
+ * @category Package : @augment-vir/node
10
+ * @package [`@augment-vir/node`](https://www.npmjs.com/package/@augment-vir/node)
11
+ */
12
+ export type CopyToDockerContainerParams = {
13
+ hostPath: string;
14
+ containerAbsolutePath: string;
15
+ containerNameOrId: string;
16
+ dockerFlags?: ReadonlyArray<string>;
17
+ };
18
+
19
+ export async function copyToContainer({
20
+ containerAbsolutePath,
21
+ hostPath,
22
+ containerNameOrId,
23
+ dockerFlags = [],
24
+ }: CopyToDockerContainerParams): Promise<void> {
25
+ const isDir = (await stat(hostPath)).isDirectory();
26
+ const suffix = isDir ? '/.' : '';
27
+ const fullHostPath = addSuffix({value: hostPath, suffix});
28
+ const extraInputs: string = dockerFlags.join(' ');
29
+
30
+ await runShellCommand(
31
+ `docker cp ${extraInputs} '${fullHostPath}' '${containerNameOrId}:${containerAbsolutePath}'`,
32
+ {rejectOnError: true},
33
+ );
34
+ }
@@ -0,0 +1,119 @@
1
+ import {wrapString} from '@augment-vir/common';
2
+
3
+ /**
4
+ * Used for `type` in {@link DockerVolumeMap}. These types are apparently only relevant for running
5
+ * Docker on macOS and are potentially irrelevant now. It's likely best to leave the `type` property
6
+ * empty (`undefined`).
7
+ *
8
+ * @category Node : Docker : Util
9
+ * @category Package : @augment-vir/node
10
+ * @package [`@augment-vir/node`](https://www.npmjs.com/package/@augment-vir/node)
11
+ */
12
+ export enum DockerVolumeMappingType {
13
+ Cached = 'cached',
14
+ Delegated = 'delegated',
15
+ }
16
+
17
+ /**
18
+ * A mapping of a single docker volume for mounting host files to a container.
19
+ *
20
+ * @category Node : Docker : Util
21
+ * @category Package : @augment-vir/node
22
+ * @package [`@augment-vir/node`](https://www.npmjs.com/package/@augment-vir/node)
23
+ */
24
+ export type DockerVolumeMap = {
25
+ hostAbsolutePath: string;
26
+ containerAbsolutePath: string;
27
+ type?: DockerVolumeMappingType | undefined;
28
+ };
29
+
30
+ export function makeVolumeFlags(volumeMapping?: ReadonlyArray<DockerVolumeMap>): string {
31
+ if (!volumeMapping) {
32
+ return '';
33
+ }
34
+
35
+ const parts = volumeMapping.map((volume) => {
36
+ const mountType = volume.type ? `:${volume.type}` : '';
37
+ return `-v '${volume.hostAbsolutePath}':'${volume.containerAbsolutePath}'${mountType}`;
38
+ });
39
+
40
+ return parts.join(' ');
41
+ }
42
+ /**
43
+ * A single docker container port mapping. This is usually used in an array.
44
+ *
45
+ * @category Node : Docker : Util
46
+ * @category Package : @augment-vir/node
47
+ * @package [`@augment-vir/node`](https://www.npmjs.com/package/@augment-vir/node)
48
+ */
49
+ export type DockerPortMap = {
50
+ hostPort: number;
51
+ containerPort: number;
52
+ };
53
+
54
+ /**
55
+ * A set of environment mappings for a docker container.
56
+ *
57
+ * - Each key in this object represents the env var name within the Docker container.
58
+ * - Each `value` property can be either the value that the env var should be set to _or_ an existing
59
+ * env var's interpolation into the value.
60
+ * - If the value string is meant to be interpolated within the shell context, make sure to set
61
+ * `allowInterpolation` to `true`. Otherwise, it's best to leave it as `false`.
62
+ *
63
+ * @category Node : Docker : Util
64
+ * @category Package : @augment-vir/node
65
+ * @example
66
+ *
67
+ * ```ts
68
+ * const envMapping: DockerEnvMap = {
69
+ * VAR_1: {
70
+ * value: 'hi',
71
+ * // set to false because this is a raw string value that is not meant to be interpolated
72
+ * allowInterpolation: false,
73
+ * },
74
+ * VAR_2: {
75
+ * // the value here will be interpolated from the current shell's value for `EXISTING_VAR`
76
+ * value: '$EXISTING_VAR',
77
+ * // set to true to allow '$EXISTING_VAR' to be interpolated by the shell
78
+ * allowInterpolation: true,
79
+ * },
80
+ * };
81
+ * ```
82
+ *
83
+ * @package [`@augment-vir/node`](https://www.npmjs.com/package/@augment-vir/node)
84
+ */
85
+ export type DockerEnvMap<RequiredKeys extends string = string> = Readonly<
86
+ Record<
87
+ RequiredKeys | string,
88
+ {
89
+ value: string;
90
+ allowInterpolation: boolean;
91
+ }
92
+ >
93
+ >;
94
+
95
+ export function makePortMapFlags(portMapping?: ReadonlyArray<DockerPortMap> | undefined): string {
96
+ if (!portMapping) {
97
+ return '';
98
+ }
99
+
100
+ return portMapping
101
+ .map((portMap) => {
102
+ return `-p ${portMap.hostPort}:${portMap.containerPort}`;
103
+ })
104
+ .join(' ');
105
+ }
106
+
107
+ export function makeEnvFlags(envMapping?: DockerEnvMap | undefined): string {
108
+ if (!envMapping) {
109
+ return '';
110
+ }
111
+ const flags: ReadonlyArray<string> = Object.entries(envMapping).map(
112
+ ([key, {value, allowInterpolation}]) => {
113
+ const quote = allowInterpolation ? '"' : "'";
114
+ return `-e ${key}=${wrapString({value, wrapper: quote})}`;
115
+ },
116
+ );
117
+
118
+ return flags.join(' ');
119
+ }
@@ -0,0 +1,25 @@
1
+ import type {PartialWithUndefined} from '@augment-vir/core';
2
+ import {runShellCommand} from '../../augments/terminal/shell.js';
3
+ import {waitUntilContainerExited, waitUntilContainerRemoved} from './container-status.js';
4
+
5
+ export async function killContainer(
6
+ containerNameOrId: string,
7
+ options: PartialWithUndefined<{
8
+ /**
9
+ * If set to `true`, the container will be killed but won't be removed. (You'll still see it
10
+ * in your Docker Dashboard but it'll have a status of exited.)
11
+ *
12
+ * @default false
13
+ */
14
+ keepContainer: boolean;
15
+ }> = {},
16
+ ): Promise<void> {
17
+ await runShellCommand(`docker kill '${containerNameOrId}'`);
18
+
19
+ if (options.keepContainer) {
20
+ await waitUntilContainerExited(containerNameOrId);
21
+ } else {
22
+ await runShellCommand(`docker rm '${containerNameOrId}'`);
23
+ await waitUntilContainerRemoved(containerNameOrId);
24
+ }
25
+ }