@augment-vir/node 31.0.0 → 31.1.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.
- package/dist/augments/fs/dir-contents.js +1 -1
- package/dist/augments/terminal/relevant-args.d.ts +1 -7
- package/dist/augments/terminal/relevant-args.js +1 -7
- package/dist/augments/terminal/run-cli-script.js +1 -5
- package/dist/augments/terminal/shell.js +1 -4
- package/dist/docker/containers/docker-command-inputs.js +1 -1
- package/dist/docker/containers/run-container.mock.js +1 -4
- package/dist/docker/docker-image.js +1 -6
- package/dist/prisma/model-data.js +2 -5
- package/dist/prisma/prisma-migrations.js +2 -7
- package/dist/prisma/run-prisma-command.js +3 -15
- package/dist/scripts/fix-ts-bin.script.d.ts +1 -0
- package/dist/scripts/fix-ts-bin.script.js +48 -0
- package/package.json +4 -4
- package/src/augments/docker.ts +118 -0
- package/src/augments/fs/dir-contents.ts +116 -0
- package/src/augments/fs/download.ts +31 -0
- package/src/augments/fs/json.ts +87 -0
- package/src/augments/fs/read-dir.ts +60 -0
- package/src/augments/fs/read-file.ts +18 -0
- package/src/augments/fs/symlink.ts +37 -0
- package/src/augments/fs/write.ts +18 -0
- package/src/augments/npm/query-workspace.ts +47 -0
- package/src/augments/npm/read-package-json.ts +28 -0
- package/src/augments/os/operating-system.ts +55 -0
- package/src/augments/path/ancestor.ts +78 -0
- package/src/augments/path/os-path.ts +45 -0
- package/src/augments/path/root.ts +14 -0
- package/src/augments/prisma.ts +174 -0
- package/src/augments/terminal/question.ts +142 -0
- package/src/augments/terminal/relevant-args.ts +81 -0
- package/src/augments/terminal/run-cli-script.ts +60 -0
- package/src/augments/terminal/shell.ts +312 -0
- package/src/docker/containers/container-info.ts +83 -0
- package/src/docker/containers/container-status.ts +110 -0
- package/src/docker/containers/copy-to-container.ts +34 -0
- package/src/docker/containers/docker-command-inputs.ts +119 -0
- package/src/docker/containers/kill-container.ts +25 -0
- package/src/docker/containers/run-command.ts +51 -0
- package/src/docker/containers/run-container.mock.ts +17 -0
- package/src/docker/containers/run-container.ts +92 -0
- package/src/docker/containers/try-or-kill-container.ts +18 -0
- package/src/docker/docker-image.ts +56 -0
- package/src/docker/docker-startup.ts +49 -0
- package/src/docker/run-docker-test.mock.ts +26 -0
- package/src/file-paths.mock.ts +29 -0
- package/src/index.ts +19 -0
- package/src/prisma/disable-ci-env.mock.ts +88 -0
- package/src/prisma/model-data.ts +213 -0
- package/src/prisma/prisma-client.ts +43 -0
- package/src/prisma/prisma-database.mock.ts +31 -0
- package/src/prisma/prisma-database.ts +35 -0
- package/src/prisma/prisma-errors.ts +45 -0
- package/src/prisma/prisma-migrations.ts +149 -0
- package/src/prisma/run-prisma-command.ts +59 -0
- package/src/scripts/fix-ts-bin.script.ts +60 -0
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import {log} from '@augment-vir/common';
|
|
2
|
+
import {convertDuration, type AnyDuration} from '@date-vir/duration';
|
|
3
|
+
import {createInterface} from 'node:readline';
|
|
4
|
+
|
|
5
|
+
/** Can't test requiring user input. */
|
|
6
|
+
/* node:coverage disable */
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Options for {@link askQuestion}.
|
|
10
|
+
*
|
|
11
|
+
* @category Node : Terminal : Util
|
|
12
|
+
* @category Package : @augment-vir/node
|
|
13
|
+
* @package [`@augment-vir/node`](https://www.npmjs.com/package/@augment-vir/node)
|
|
14
|
+
*/
|
|
15
|
+
export type AskQuestionOptions = {
|
|
16
|
+
timeout: AnyDuration;
|
|
17
|
+
hideUserInput: boolean;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const defaultAskQuestionOptions: AskQuestionOptions = {
|
|
21
|
+
timeout: {seconds: 60},
|
|
22
|
+
hideUserInput: false,
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Asks the user a question in their terminal and then waits for them to type something in response.
|
|
27
|
+
* The response is accepted once the user inputs a new line.
|
|
28
|
+
*
|
|
29
|
+
* @category Node : Terminal
|
|
30
|
+
* @category Package : @augment-vir/node
|
|
31
|
+
* @package [`@augment-vir/node`](https://www.npmjs.com/package/@augment-vir/node)
|
|
32
|
+
* @see
|
|
33
|
+
* - {@link askQuestionUntilConditionMet}: ask a question on loop until the user provides a valid response.
|
|
34
|
+
*/
|
|
35
|
+
export async function askQuestion(
|
|
36
|
+
questionToAsk: string,
|
|
37
|
+
{
|
|
38
|
+
hideUserInput = defaultAskQuestionOptions.hideUserInput,
|
|
39
|
+
timeout = defaultAskQuestionOptions.timeout,
|
|
40
|
+
}: Partial<AskQuestionOptions> = defaultAskQuestionOptions,
|
|
41
|
+
): Promise<string> {
|
|
42
|
+
const cliInterface = createInterface({
|
|
43
|
+
input: process.stdin,
|
|
44
|
+
output: process.stdout,
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
if (hideUserInput) {
|
|
48
|
+
let promptWritten = false;
|
|
49
|
+
/** _writeToOutput is not in the types OR in the Node.js documentation but is a thing. */
|
|
50
|
+
(cliInterface as unknown as {_writeToOutput: (prompt: string) => void})._writeToOutput = (
|
|
51
|
+
prompt,
|
|
52
|
+
) => {
|
|
53
|
+
if (!promptWritten) {
|
|
54
|
+
(
|
|
55
|
+
cliInterface as unknown as {output: {write: (output: string) => void}}
|
|
56
|
+
).output.write(prompt);
|
|
57
|
+
promptWritten = true;
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// handle killing the process
|
|
63
|
+
cliInterface.on('SIGINT', () => {
|
|
64
|
+
cliInterface.close();
|
|
65
|
+
process.stdout.write('\n');
|
|
66
|
+
process.kill(process.pid, 'SIGINT');
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
return new Promise((resolve, reject) => {
|
|
70
|
+
const timeoutMs = convertDuration(timeout, {milliseconds: true}).milliseconds;
|
|
71
|
+
|
|
72
|
+
const timeoutId = timeoutMs
|
|
73
|
+
? setTimeout(() => {
|
|
74
|
+
cliInterface.close();
|
|
75
|
+
reject(
|
|
76
|
+
new Error(
|
|
77
|
+
`Took too long to respond (over "${Math.floor(timeoutMs / 1000)}" seconds)`,
|
|
78
|
+
),
|
|
79
|
+
);
|
|
80
|
+
}, timeoutMs)
|
|
81
|
+
: undefined;
|
|
82
|
+
|
|
83
|
+
process.stdout.write(questionToAsk + '\n');
|
|
84
|
+
cliInterface.question('', (response) => {
|
|
85
|
+
if (timeoutId != undefined) {
|
|
86
|
+
clearTimeout(timeoutId);
|
|
87
|
+
}
|
|
88
|
+
cliInterface.close();
|
|
89
|
+
resolve(response);
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Options for {@link askQuestionUntilConditionMet}.
|
|
96
|
+
*
|
|
97
|
+
* @category Node : Terminal : Util
|
|
98
|
+
* @category Package : @augment-vir/node
|
|
99
|
+
* @package [`@augment-vir/node`](https://www.npmjs.com/package/@augment-vir/node)
|
|
100
|
+
*/
|
|
101
|
+
export type QuestionUntilConditionMetOptions = {
|
|
102
|
+
questionToAsk: string;
|
|
103
|
+
/** Callback to call with the user's response to verify if their response is valid. */
|
|
104
|
+
verifyResponseCallback: (response: string) => boolean | Promise<boolean>;
|
|
105
|
+
invalidInputMessage: string;
|
|
106
|
+
tryCountMax?: number;
|
|
107
|
+
} & Partial<AskQuestionOptions>;
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Asks the user a question in their terminal and then waits for them to type something in response.
|
|
111
|
+
* The response is submitted once the user inputs a new line. If the response fails validation, the
|
|
112
|
+
* question is presented again.
|
|
113
|
+
*
|
|
114
|
+
* @category Node : Terminal
|
|
115
|
+
* @category Package : @augment-vir/node
|
|
116
|
+
* @package [`@augment-vir/node`](https://www.npmjs.com/package/@augment-vir/node)
|
|
117
|
+
* @see
|
|
118
|
+
* - {@link askQuestion}: ask a question and accept any response.
|
|
119
|
+
*/
|
|
120
|
+
export async function askQuestionUntilConditionMet({
|
|
121
|
+
questionToAsk,
|
|
122
|
+
verifyResponseCallback,
|
|
123
|
+
invalidInputMessage,
|
|
124
|
+
tryCountMax = 5,
|
|
125
|
+
...options
|
|
126
|
+
}: QuestionUntilConditionMetOptions): Promise<string> {
|
|
127
|
+
let wasConditionMet = false;
|
|
128
|
+
let retryCount = 0;
|
|
129
|
+
let response = '';
|
|
130
|
+
while (!wasConditionMet && retryCount <= tryCountMax) {
|
|
131
|
+
response = (await askQuestion(questionToAsk, options)).trim();
|
|
132
|
+
wasConditionMet = await verifyResponseCallback(response);
|
|
133
|
+
if (!wasConditionMet) {
|
|
134
|
+
log.error(invalidInputMessage);
|
|
135
|
+
}
|
|
136
|
+
retryCount++;
|
|
137
|
+
}
|
|
138
|
+
if (retryCount > tryCountMax) {
|
|
139
|
+
throw new Error(`Max input attempts (${tryCountMax}) exceeded.`);
|
|
140
|
+
}
|
|
141
|
+
return response;
|
|
142
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import {basename} from 'node:path';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Input for extractRelevantArgs.
|
|
5
|
+
*
|
|
6
|
+
* @category Node : Terminal : Util
|
|
7
|
+
* @category Package : @augment-vir/node
|
|
8
|
+
* @package [`@augment-vir/node`](https://www.npmjs.com/package/@augment-vir/node)
|
|
9
|
+
*/
|
|
10
|
+
export type RelevantArgsInput = {
|
|
11
|
+
/** Raw arguments passed to the CLI. Typically this will simply be process.argv. */
|
|
12
|
+
rawArgs: ReadonlyArray<string>;
|
|
13
|
+
/**
|
|
14
|
+
* Executable bin name for your script. This should be the "bin" name in your package.json, or
|
|
15
|
+
* simply your package name if you have no custom bin name defined.
|
|
16
|
+
*
|
|
17
|
+
* See https://docs.npmjs.com/cli/v10/configuring-npm/package-json#bin for details on the bin
|
|
18
|
+
* field of package.json
|
|
19
|
+
*/
|
|
20
|
+
binName: string | undefined;
|
|
21
|
+
/**
|
|
22
|
+
* The name or path of your script file that will be executed via the CLI. This should almost
|
|
23
|
+
* always simply be __filename in CJS or `import.meta.filename` in ESM.
|
|
24
|
+
*/
|
|
25
|
+
fileName: string;
|
|
26
|
+
/**
|
|
27
|
+
* If set to true, this function with throw an error if the given file or bin name was not found
|
|
28
|
+
* in the given arguments list.
|
|
29
|
+
*/
|
|
30
|
+
errorIfNotFound?: boolean | undefined;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Trims arguments list to remove all arguments that take place before the script's file name or
|
|
35
|
+
* executable bin name.
|
|
36
|
+
*
|
|
37
|
+
* @category Node : Terminal
|
|
38
|
+
* @category Package : @augment-vir/node
|
|
39
|
+
* @example
|
|
40
|
+
*
|
|
41
|
+
* ```ts
|
|
42
|
+
* extractRelevantArgs({
|
|
43
|
+
* rawArgs: ['npx', 'ts-node', './my-script.ts', 'arg1', '--arg2'], // typically will be process.argv
|
|
44
|
+
* binName: 'my-script', // should be your package.json "bin" property name, can be undefined
|
|
45
|
+
* fileName: 'my-script.ts', // should be __filename from the script that will be executed
|
|
46
|
+
* });
|
|
47
|
+
* // will output ['arg1', '--arg2']
|
|
48
|
+
* ```
|
|
49
|
+
*
|
|
50
|
+
* @package [`@augment-vir/node`](https://www.npmjs.com/package/@augment-vir/node)
|
|
51
|
+
*/
|
|
52
|
+
export function extractRelevantArgs({
|
|
53
|
+
rawArgs,
|
|
54
|
+
binName,
|
|
55
|
+
fileName,
|
|
56
|
+
errorIfNotFound,
|
|
57
|
+
}: Readonly<RelevantArgsInput>): string[] {
|
|
58
|
+
const baseFileName = basename(fileName);
|
|
59
|
+
|
|
60
|
+
if (!baseFileName) {
|
|
61
|
+
throw new Error(
|
|
62
|
+
`Given file name produced no base file name (with path.basename()): '${fileName}'`,
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const lastIrrelevantArgIndex = rawArgs.findIndex((arg) => {
|
|
67
|
+
const baseArgName = basename(arg);
|
|
68
|
+
const matchesFileName = baseArgName === baseFileName;
|
|
69
|
+
const matchesBinName = binName ? baseArgName === binName : false;
|
|
70
|
+
return matchesFileName || matchesBinName;
|
|
71
|
+
});
|
|
72
|
+
if (lastIrrelevantArgIndex === -1) {
|
|
73
|
+
if (errorIfNotFound) {
|
|
74
|
+
throw new Error('Failed to find position of file or bin name in provided args list.');
|
|
75
|
+
} else {
|
|
76
|
+
return [...rawArgs];
|
|
77
|
+
}
|
|
78
|
+
} else {
|
|
79
|
+
return rawArgs.slice(lastIrrelevantArgIndex + 1);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/* node:coverage disable */
|
|
2
|
+
/** This file cannot be tested because it calls `process.exit`. */
|
|
3
|
+
|
|
4
|
+
import {extname} from 'node:path';
|
|
5
|
+
import {interpolationSafeWindowsPath} from '../path/os-path.js';
|
|
6
|
+
import {extractRelevantArgs} from './relevant-args.js';
|
|
7
|
+
import {runShellCommand} from './shell.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* A map of file extensions to their known runners for {@link runCliScript}.
|
|
11
|
+
*
|
|
12
|
+
* @category Node : Terminal : Util
|
|
13
|
+
* @category Package : @augment-vir/node
|
|
14
|
+
* @package [`@augment-vir/node`](https://www.npmjs.com/package/@augment-vir/node)
|
|
15
|
+
*/
|
|
16
|
+
export const ExtensionToRunner: Record<string, string> = {
|
|
17
|
+
'.ts': 'tsx',
|
|
18
|
+
'.js': 'node',
|
|
19
|
+
'.sh': 'bash',
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Runs a script path as if it had been run directly, as much as possible.
|
|
24
|
+
*
|
|
25
|
+
* @category Node : Terminal : Util
|
|
26
|
+
* @category Package : @augment-vir/node
|
|
27
|
+
* @package [`@augment-vir/node`](https://www.npmjs.com/package/@augment-vir/node)
|
|
28
|
+
*/
|
|
29
|
+
export async function runCliScript(
|
|
30
|
+
path: string,
|
|
31
|
+
/** This should just be `__filename` (for CJS) or `import.meta.filename` (for ESM). */
|
|
32
|
+
cliScriptFilePath: string,
|
|
33
|
+
/**
|
|
34
|
+
* This should be the bin name of the package that is calling this function. Set to `undefined`
|
|
35
|
+
* if there isn't one.
|
|
36
|
+
*/
|
|
37
|
+
binName: string | undefined,
|
|
38
|
+
) {
|
|
39
|
+
const args = extractRelevantArgs({
|
|
40
|
+
rawArgs: process.argv,
|
|
41
|
+
binName,
|
|
42
|
+
fileName: cliScriptFilePath,
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
const extension = extname(path);
|
|
46
|
+
|
|
47
|
+
const runner = ExtensionToRunner[extension];
|
|
48
|
+
|
|
49
|
+
if (!runner) {
|
|
50
|
+
throw new Error("No runner configured for file extension '${extension}' in '${path}'");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const results = await runShellCommand(
|
|
54
|
+
interpolationSafeWindowsPath([runner, path, ...args].join(' ')),
|
|
55
|
+
{
|
|
56
|
+
hookUpToConsole: true,
|
|
57
|
+
},
|
|
58
|
+
);
|
|
59
|
+
process.exit(results.exitCode || 0);
|
|
60
|
+
}
|
|
@@ -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
|
+
}
|