@augment-vir/node 31.0.1 → 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.
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,55 @@
1
+ /**
2
+ * The three major operating system types.
3
+ *
4
+ * @category Node : OS
5
+ * @category Package : @augment-vir/node
6
+ * @package [`@augment-vir/node`](https://www.npmjs.com/package/@augment-vir/node)
7
+ */
8
+ export enum OperatingSystem {
9
+ Linux = 'linux',
10
+ Mac = 'mac',
11
+ Windows = 'windows',
12
+ }
13
+
14
+ /**
15
+ * The current operating system type, as deduced from
16
+ * [`process.platform`](https://nodejs.org/api/process.html#processplatform).
17
+ *
18
+ * @category Node : OS
19
+ * @category Package : @augment-vir/node
20
+ * @package [`@augment-vir/node`](https://www.npmjs.com/package/@augment-vir/node)
21
+ */
22
+ export const currentOperatingSystem: OperatingSystem = getOperatingSystem();
23
+
24
+ /**
25
+ * Checks if the current operating system is the requested one.
26
+ *
27
+ * @category Node : OS
28
+ * @category Package : @augment-vir/node
29
+ * @example
30
+ *
31
+ * ```ts
32
+ * import {isOperatingSystem, OperatingSystem} from '@augment-vir/node';
33
+ *
34
+ * if (isOperatingSystem(OperatingSystem.Mac)) {
35
+ * // do something
36
+ * }
37
+ * ```
38
+ *
39
+ * @package [`@augment-vir/node`](https://www.npmjs.com/package/@augment-vir/node)
40
+ */
41
+ export function isOperatingSystem(operatingSystem: OperatingSystem): boolean {
42
+ return currentOperatingSystem === operatingSystem;
43
+ }
44
+
45
+ function getOperatingSystem(): OperatingSystem {
46
+ /** We can't test all of these on a single system. */
47
+ /* node:coverage ignore next 7 */
48
+ if (process.platform === 'win32') {
49
+ return OperatingSystem.Windows;
50
+ } else if (process.platform === 'darwin') {
51
+ return OperatingSystem.Mac;
52
+ } else {
53
+ return OperatingSystem.Linux;
54
+ }
55
+ }
@@ -0,0 +1,78 @@
1
+ import {check} from '@augment-vir/assert';
2
+ import {type MaybePromise} from '@augment-vir/common';
3
+ import {dirname, join} from 'node:path';
4
+ import {systemRootPath} from './root.js';
5
+
6
+ export function findAncestor(
7
+ currentPath: string,
8
+ callback: (path: string) => Promise<boolean>,
9
+ ): Promise<string | undefined>;
10
+ export function findAncestor(
11
+ currentPath: string,
12
+ callback: (path: string) => boolean,
13
+ ): string | undefined;
14
+ export function findAncestor(
15
+ currentPath: string,
16
+ callback: (path: string) => MaybePromise<boolean>,
17
+ ): MaybePromise<string | undefined>;
18
+ /**
19
+ * Find an ancestor file path that matches the given `callback`. If no matches are found all the way
20
+ * up until the system root, this returns `undefined`.
21
+ *
22
+ * @category Path : Node
23
+ * @category Package : @augment-vir/node
24
+ * @returns `undefined` if no matches are found.
25
+ * @package [`@augment-vir/node`](https://www.npmjs.com/package/@augment-vir/node)
26
+ */
27
+ export function findAncestor(
28
+ currentPath: string,
29
+ callback: (path: string) => MaybePromise<boolean>,
30
+ ): MaybePromise<string | undefined> {
31
+ if (currentPath === systemRootPath) {
32
+ return undefined;
33
+ }
34
+
35
+ const result = callback(currentPath);
36
+
37
+ if (check.isPromise(result)) {
38
+ return new Promise<string | undefined>(async (resolve) => {
39
+ const awaitedResult = await result;
40
+
41
+ if (awaitedResult) {
42
+ resolve(currentPath);
43
+ } else {
44
+ resolve(await findAncestor(dirname(currentPath), callback));
45
+ }
46
+ });
47
+ } else if (result) {
48
+ return currentPath;
49
+ } else {
50
+ return findAncestor(dirname(currentPath), callback);
51
+ }
52
+ }
53
+
54
+ /**
55
+ * Join a list of paths to the given `parentDirPath`. This is particularly useful for getting full
56
+ * paths from the output of
57
+ * [`readdir`](https://nodejs.org/api/fs.html#fspromisesreaddirpath-options).
58
+ *
59
+ * @category Path : Node
60
+ * @category Package : @augment-vir/node
61
+ * @example
62
+ *
63
+ * ```ts
64
+ * import {readdir} from 'node:fs/promises';
65
+ * import {join} from 'node:path';
66
+ *
67
+ * const parentDir = join('my', 'path');
68
+ * const dirs = joinFilesToDir(parentDir, await readdir(parentDir));
69
+ * ```
70
+ *
71
+ * @package [`@augment-vir/node`](https://www.npmjs.com/package/@augment-vir/node)
72
+ */
73
+ export function joinFilesToDir(
74
+ parentDirPath: string,
75
+ childNames: ReadonlyArray<string>,
76
+ ): Array<string> {
77
+ return childNames.map((childName) => join(parentDirPath, childName));
78
+ }
@@ -0,0 +1,45 @@
1
+ import {sep} from 'node:path';
2
+
3
+ /**
4
+ * Convert a given path to a windows path if the current system doesn't use `/`.
5
+ *
6
+ * @category Path : Node
7
+ * @category Package : @augment-vir/node
8
+ * @package [`@augment-vir/node`](https://www.npmjs.com/package/@augment-vir/node)
9
+ */
10
+ export function replaceWithWindowsPathIfNeeded(input: string): string {
11
+ /** No single system will test all of these lines so we must ignore them all. */
12
+ /* node:coverage ignore next 5 */
13
+ if (sep === '/') {
14
+ return input;
15
+ } else {
16
+ return input.replace(/\//g, sep);
17
+ }
18
+ }
19
+
20
+ /**
21
+ * Convert a Windows path to a posix path.
22
+ *
23
+ * @category Path : Node
24
+ * @category Package : @augment-vir/node
25
+ * @package [`@augment-vir/node`](https://www.npmjs.com/package/@augment-vir/node)
26
+ */
27
+ export function toPosixPath(maybeWindowsPath: string): string {
28
+ return maybeWindowsPath
29
+ .replace(/^(.+?):\\+/, (match, captureGroup) => {
30
+ return `/${captureGroup.toLowerCase()}/`;
31
+ })
32
+ .replace(/\\+/g, '/');
33
+ }
34
+
35
+ /**
36
+ * Use this to interpolate paths into bash commands. If the given path is not a Windows path, the
37
+ * path structure will not be modified.
38
+ *
39
+ * @category Path : Node
40
+ * @category Package : @augment-vir/node
41
+ * @package [`@augment-vir/node`](https://www.npmjs.com/package/@augment-vir/node)
42
+ */
43
+ export function interpolationSafeWindowsPath(input: string): string {
44
+ return input.replace(/\\/g, '\\\\\\\\');
45
+ }
@@ -0,0 +1,14 @@
1
+ import {parse} from 'node:path';
2
+
3
+ function getSystemRootPath() {
4
+ return parse(process.cwd()).root;
5
+ }
6
+
7
+ /**
8
+ * The root path of the system containing the cwd.
9
+ *
10
+ * @category Path : Node
11
+ * @category Package : @augment-vir/node
12
+ * @package [`@augment-vir/node`](https://www.npmjs.com/package/@augment-vir/node)
13
+ */
14
+ export const systemRootPath = getSystemRootPath();
@@ -0,0 +1,174 @@
1
+ import {addData, dumpData, getAllPrismaModelNames} from '../prisma/model-data.js';
2
+ import {generatePrismaClient, isGeneratedPrismaClientCurrent} from '../prisma/prisma-client.js';
3
+ import {
4
+ doesPrismaDiffExist,
5
+ getPrismaDiff,
6
+ resetDevPrismaDatabase,
7
+ } from '../prisma/prisma-database.js';
8
+ import {
9
+ applyPrismaMigrationsToDev,
10
+ applyPrismaMigrationsToProd,
11
+ createPrismaMigration,
12
+ getMigrationStatus,
13
+ } from '../prisma/prisma-migrations.js';
14
+
15
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
16
+ import type {PrismaMigrationNeededError, PrismaResetNeededError} from '../prisma/prisma-errors.js';
17
+
18
+ export type {
19
+ PrismaAddDataData as PrismaAddModelData,
20
+ PrismaDataDumpOptions,
21
+ } from '../prisma/model-data.js';
22
+ export * from '../prisma/prisma-errors.js';
23
+ export type {PrismaMigrationStatus} from '../prisma/prisma-migrations.js';
24
+
25
+ /**
26
+ * Centralized Prisma API from `@augment-vir/node`.
27
+ *
28
+ * ## Prisma flows
29
+ *
30
+ * - Deploy to production
31
+ *
32
+ * - {@link prisma.migration.applyProd}
33
+ * - Update dev environment
34
+ *
35
+ * - Apply migrations: {@link prisma.migration.applyDev}
36
+ *
37
+ * - If throws {@link PrismaMigrationNeededError}, prompt user for a new migration name and pass it to
38
+ * {@link prisma.migration.create}
39
+ * - If throws {@link PrismaResetNeededError}, reset the database with {@link prisma.database.resetDev}
40
+ * - Generate client: `prisma.client.isCurrent`
41
+ *
42
+ * - If `false`, run {@link prisma.client.generate}
43
+ *
44
+ * @category Prisma : Node
45
+ * @category Package : @augment-vir/node
46
+ * @package [`@augment-vir/node`](https://www.npmjs.com/package/@augment-vir/node)
47
+ */
48
+ export const prisma = {
49
+ migration: {
50
+ /**
51
+ * Get current migration status.
52
+ *
53
+ * @see https://www.prisma.io/docs/orm/reference/prisma-cli-reference#migrate-status
54
+ */
55
+ status: getMigrationStatus,
56
+ /**
57
+ * Creates a new migration.
58
+ *
59
+ * @see https://www.prisma.io/docs/orm/reference/prisma-cli-reference#migrate-dev
60
+ */
61
+ create: createPrismaMigration,
62
+ /**
63
+ * Apply all migrations. Meant for a production environment.
64
+ *
65
+ * @see https://www.prisma.io/docs/orm/reference/prisma-cli-reference#migrate-deploy
66
+ */
67
+ applyProd: applyPrismaMigrationsToProd,
68
+ /**
69
+ * Apply all migrations. Meant for a development environment, with less protections than
70
+ * {@link prisma.migration.applyProd}.
71
+ *
72
+ * @throws `PrismaMigrationNeededError` when a new migration is required so the user needs
73
+ * to input a name.
74
+ * @throws `PrismaResetNeededError` when there's a migration mismatch with the database and
75
+ * it needs to be reset.
76
+ * @see https://www.prisma.io/docs/orm/reference/prisma-cli-reference#migrate-dev
77
+ */
78
+ applyDev: applyPrismaMigrationsToDev,
79
+ },
80
+ database: {
81
+ /**
82
+ * Force resets a dev database to match the current Prisma schema and migrations.
83
+ *
84
+ * **This will destroy all data. Do not use in production.**
85
+ *
86
+ * @see https://www.prisma.io/docs/orm/reference/prisma-cli-reference#migrate-reset
87
+ */
88
+ resetDev: resetDevPrismaDatabase,
89
+ /**
90
+ * Uses {@link prisma.database.diff} to detect if there are any differences between the
91
+ * current database and the Prisma schema that should control it.
92
+ */
93
+ hasDiff: doesPrismaDiffExist,
94
+ /**
95
+ * Gets a string list of all differences between the current database and the Prisma schema
96
+ * that should control it.
97
+ *
98
+ * @see https://www.prisma.io/docs/orm/reference/prisma-cli-reference#migrate-diff
99
+ */
100
+ diff: getPrismaDiff,
101
+ },
102
+ client: {
103
+ /**
104
+ * Runs Prisma generators included in the given Prisma schema (which usually includes the
105
+ * Prisma JS client). This will work even if the database doesn't exist yet.
106
+ *
107
+ * @example
108
+ *
109
+ * ```ts
110
+ * import {prisma} from '@augment-vir/node';
111
+ *
112
+ * prisma.client.generate('../../prisma/schema.prisma');
113
+ * ```
114
+ */
115
+ generate: generatePrismaClient,
116
+ /**
117
+ * Detects if the current generated Prisma JS Client was generated from the current Prisma
118
+ * schema.
119
+ */
120
+ isCurrent: isGeneratedPrismaClientCurrent,
121
+ /**
122
+ * Adds a collection of create data to a database through a `PrismaClient` instance. This is
123
+ * particularly useful for setting up mocks in a mock PrismaClient.
124
+ *
125
+ * @example
126
+ *
127
+ * ```ts
128
+ * import {addPrismaModelData} from '@augment-vir/common';
129
+ * import {PrismaClient} from '@prisma/client';
130
+ *
131
+ * await addPrismaModelData(new PrismaClient(), [
132
+ * {
133
+ * user: {
134
+ * mockUser1: {
135
+ * first_name: 'one',
136
+ * id: 123,
137
+ * // etc.
138
+ * },
139
+ * mockUser2: {
140
+ * first_name: 'two',
141
+ * id: 124,
142
+ * authRole: 'user',
143
+ * // etc.
144
+ * },
145
+ * },
146
+ * },
147
+ * {
148
+ * region: [
149
+ * {
150
+ * id: 1,
151
+ * name: 'North America',
152
+ * // etc.
153
+ * },
154
+ * {
155
+ * id: 2,
156
+ * name: 'Europe',
157
+ * // etc.
158
+ * },
159
+ * ],
160
+ * },
161
+ * ]);
162
+ * ```
163
+ */
164
+ addData: addData,
165
+ /**
166
+ * Dump data from the current database through a `PrismaClient` instance.
167
+ *
168
+ * @see {@link PrismaDataDumpOptions}
169
+ */
170
+ dumpData: dumpData,
171
+ /** List all model names in the given Prisma client. */
172
+ listModelNames: getAllPrismaModelNames,
173
+ },
174
+ };
@@ -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
+ }