ts-repo-utils 7.7.3 → 7.8.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 (54) hide show
  1. package/README.md +197 -144
  2. package/dist/cmd/assert-repo-is-clean.mjs +1 -1
  3. package/dist/cmd/assert-repo-is-clean.mjs.map +1 -1
  4. package/dist/cmd/check-should-run-type-checks.mjs +1 -1
  5. package/dist/cmd/check-should-run-type-checks.mjs.map +1 -1
  6. package/dist/cmd/format-diff-from.mjs +1 -1
  7. package/dist/cmd/format-diff-from.mjs.map +1 -1
  8. package/dist/cmd/format-uncommitted.mjs +1 -1
  9. package/dist/cmd/format-uncommitted.mjs.map +1 -1
  10. package/dist/cmd/gen-index-ts.mjs +1 -1
  11. package/dist/cmd/gen-index-ts.mjs.map +1 -1
  12. package/dist/entry-point.mjs +2 -1
  13. package/dist/entry-point.mjs.map +1 -1
  14. package/dist/functions/assert-ext.d.mts +20 -1
  15. package/dist/functions/assert-ext.d.mts.map +1 -1
  16. package/dist/functions/assert-ext.mjs +52 -35
  17. package/dist/functions/assert-ext.mjs.map +1 -1
  18. package/dist/functions/create-result-assert.d.mts +18 -0
  19. package/dist/functions/create-result-assert.d.mts.map +1 -0
  20. package/dist/functions/create-result-assert.mjs +40 -0
  21. package/dist/functions/create-result-assert.mjs.map +1 -0
  22. package/dist/functions/exec-async.d.mts +5 -6
  23. package/dist/functions/exec-async.d.mts.map +1 -1
  24. package/dist/functions/exec-async.mjs +26 -7
  25. package/dist/functions/exec-async.mjs.map +1 -1
  26. package/dist/functions/format.d.mts.map +1 -1
  27. package/dist/functions/format.mjs +8 -2
  28. package/dist/functions/format.mjs.map +1 -1
  29. package/dist/functions/index.d.mts +1 -0
  30. package/dist/functions/index.d.mts.map +1 -1
  31. package/dist/functions/index.mjs +2 -1
  32. package/dist/functions/index.mjs.map +1 -1
  33. package/dist/functions/workspace-utils/execute-parallel.mjs.map +1 -1
  34. package/dist/index.mjs +2 -1
  35. package/dist/index.mjs.map +1 -1
  36. package/dist/node-global.d.mts +2 -0
  37. package/dist/node-global.d.mts.map +1 -1
  38. package/dist/node-global.mjs +3 -1
  39. package/dist/node-global.mjs.map +1 -1
  40. package/package.json +20 -14
  41. package/src/cmd/assert-repo-is-clean.mts +2 -2
  42. package/src/cmd/check-should-run-type-checks.mts +2 -2
  43. package/src/cmd/format-diff-from.mts +2 -2
  44. package/src/cmd/format-uncommitted.mts +2 -2
  45. package/src/cmd/gen-index-ts.mts +3 -3
  46. package/src/functions/assert-ext.mts +78 -52
  47. package/src/functions/create-result-assert.mts +59 -0
  48. package/src/functions/exec-async.mts +71 -14
  49. package/src/functions/exec-async.test.mts +5 -5
  50. package/src/functions/format.mts +11 -3
  51. package/src/functions/index.mts +1 -0
  52. package/src/functions/workspace-utils/execute-parallel.mts +1 -1
  53. package/src/functions/workspace-utils/run-cmd-in-stages.test.mts +5 -8
  54. package/src/node-global.mts +4 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ts-repo-utils",
3
- "version": "7.7.3",
3
+ "version": "7.8.1",
4
4
  "private": false,
5
5
  "keywords": [
6
6
  "typescript"
@@ -84,34 +84,35 @@
84
84
  "fast-glob": "^3.3.3",
85
85
  "micromatch": "^4.0.8",
86
86
  "prettier": "^3.6.2",
87
- "ts-data-forge": "^3.3.0",
87
+ "ts-data-forge": "^3.3.1",
88
88
  "tsx": "^4.20.6"
89
89
  },
90
90
  "devDependencies": {
91
- "@octokit/core": "^7.0.5",
92
- "@rollup/plugin-replace": "^6.0.2",
91
+ "@octokit/core": "7.0.6",
92
+ "@rollup/plugin-replace": "^6.0.3",
93
93
  "@rollup/plugin-strip": "^3.0.4",
94
- "@rollup/plugin-typescript": "^12.1.4",
94
+ "@rollup/plugin-typescript": "^12.3.0",
95
95
  "@semantic-release/changelog": "^6.0.3",
96
96
  "@semantic-release/commit-analyzer": "^13.0.1",
97
97
  "@semantic-release/exec": "^7.1.0",
98
98
  "@semantic-release/git": "^10.0.1",
99
- "@semantic-release/github": "^12.0.0",
99
+ "@semantic-release/github": "^12.0.1",
100
100
  "@semantic-release/npm": "^13.1.1",
101
101
  "@semantic-release/release-notes-generator": "^14.1.0",
102
- "@types/node": "^24.8.1",
103
- "@vitest/coverage-v8": "^3.2.4",
104
- "@vitest/ui": "^3.2.4",
102
+ "@types/node": "^24.10.0",
103
+ "@vitest/coverage-v8": "^4.0.6",
104
+ "@vitest/ui": "^4.0.6",
105
105
  "conventional-changelog-conventionalcommits": "^9.1.0",
106
- "cspell": "^9.2.1",
106
+ "cspell": "^9.2.2",
107
107
  "dedent": "^1.7.0",
108
- "eslint": "^9.37.0",
109
- "eslint-config-typed": "^1.7.1",
108
+ "eslint": "9.38.0",
109
+ "eslint-config-typed": "^3.1.1",
110
110
  "fast-glob": "^3.3.3",
111
+ "jiti": "^2.6.1",
111
112
  "markdownlint": "^0.39.0",
112
113
  "markdownlint-cli2": "^0.18.1",
113
114
  "npm-run-all2": "^8.0.4",
114
- "octokit-safe-types": "^1.1.1",
115
+ "octokit-safe-types": "^1.1.2",
115
116
  "prettier": "^3.6.2",
116
117
  "prettier-plugin-organize-imports": "^4.3.0",
117
118
  "prettier-plugin-packagejson": "^2.5.19",
@@ -120,12 +121,17 @@
120
121
  "ts-fortress": "^5.2.0",
121
122
  "ts-type-forge": "^2.3.0",
122
123
  "tslib": "^2.8.1",
124
+ "tsx": "^4.20.6",
123
125
  "typedoc": "^0.28.14",
124
126
  "typedoc-plugin-markdown": "^4.9.0",
125
127
  "typescript": "^5.9.3",
126
- "vitest": "^3.2.4"
128
+ "vitest": "^4.0.6"
127
129
  },
128
130
  "peerDependencies": {
129
131
  "prettier": "^3.6.2"
132
+ },
133
+ "volta": {
134
+ "node": "25.0.0",
135
+ "npm": "11.6.2"
130
136
  }
131
137
  }
@@ -5,7 +5,7 @@ import { assertRepoIsClean } from '../functions/index.mjs';
5
5
 
6
6
  const cmdDef = cmd.command({
7
7
  name: 'assert-repo-is-clean-cli',
8
- version: '7.7.3',
8
+ version: '7.8.1',
9
9
  args: {
10
10
  silent: cmd.flag({
11
11
  long: 'silent',
@@ -14,7 +14,7 @@ const cmdDef = cmd.command({
14
14
  }),
15
15
  },
16
16
  handler: (args) => {
17
- main(args).catch((error) => {
17
+ main(args).catch((error: unknown) => {
18
18
  console.error('An error occurred:', error);
19
19
  process.exit(1);
20
20
  });
@@ -5,7 +5,7 @@ import { checkShouldRunTypeChecks } from '../functions/index.mjs';
5
5
 
6
6
  const cmdDef = cmd.command({
7
7
  name: 'check-should-run-type-checks-cli',
8
- version: '7.7.3',
8
+ version: '7.8.1',
9
9
  args: {
10
10
  pathsIgnore: cmd.multioption({
11
11
  long: 'paths-ignore',
@@ -21,7 +21,7 @@ const cmdDef = cmd.command({
21
21
  }),
22
22
  },
23
23
  handler: (args) => {
24
- main(args).catch((error) => {
24
+ main(args).catch((error: unknown) => {
25
25
  console.error('An error occurred:', error);
26
26
  process.exit(1);
27
27
  });
@@ -6,7 +6,7 @@ import { formatDiffFrom } from '../functions/index.mjs';
6
6
 
7
7
  const cmdDef = cmd.command({
8
8
  name: 'format-diff-from-cli',
9
- version: '7.7.3',
9
+ version: '7.8.1',
10
10
  args: {
11
11
  base: cmd.positional({
12
12
  type: cmd.string,
@@ -50,7 +50,7 @@ const cmdDef = cmd.command({
50
50
  excludeStaged: args.excludeStaged ?? false,
51
51
  ignoreUnknown: args.ignoreUnknown ?? true,
52
52
  silent: args.silent ?? false,
53
- }).catch((error) => {
53
+ }).catch((error: unknown) => {
54
54
  console.error('An error occurred:', error);
55
55
  process.exit(1);
56
56
  });
@@ -6,7 +6,7 @@ import { formatUncommittedFiles } from '../functions/index.mjs';
6
6
 
7
7
  const cmdDef = cmd.command({
8
8
  name: 'format-uncommitted-cli',
9
- version: '7.7.3',
9
+ version: '7.8.1',
10
10
  args: {
11
11
  excludeUntracked: cmd.flag({
12
12
  long: 'exclude-untracked',
@@ -44,7 +44,7 @@ const cmdDef = cmd.command({
44
44
  excludeStaged: args.excludeStaged ?? false,
45
45
  ignoreUnknown: args.ignoreUnknown ?? true,
46
46
  silent: args.silent ?? false,
47
- }).catch((error) => {
47
+ }).catch((error: unknown) => {
48
48
  console.error('An error occurred:', error);
49
49
  process.exit(1);
50
50
  });
@@ -2,7 +2,7 @@
2
2
 
3
3
  import * as cmd from 'cmd-ts';
4
4
 
5
- // eslint-disable-next-line import/no-internal-modules
5
+ // eslint-disable-next-line import-x/no-internal-modules
6
6
  import { type InputOf, type OutputOf } from 'cmd-ts/dist/esm/from.js';
7
7
  import { expectType } from 'ts-data-forge';
8
8
  import { genIndex } from '../functions/index.mjs';
@@ -40,7 +40,7 @@ const nonEmptyArray = <T extends cmd.Type<any, any>>(
40
40
 
41
41
  const cmdDef = cmd.command({
42
42
  name: 'gen-index-ts-cli',
43
- version: '7.7.3',
43
+ version: '7.8.1',
44
44
  args: {
45
45
  // required args
46
46
  targetDirectory: cmd.positional({
@@ -109,7 +109,7 @@ const cmdDef = cmd.command({
109
109
  expectType<typeof args.formatCommand, string | undefined>('=');
110
110
  expectType<typeof args.silent, boolean | undefined>('=');
111
111
 
112
- main(args).catch((error) => {
112
+ main(args).catch((error: unknown) => {
113
113
  console.error('An error occurred:', error);
114
114
  process.exit(1);
115
115
  });
@@ -1,6 +1,7 @@
1
- import { Arr, type IMap, isString } from 'ts-data-forge';
1
+ import { Arr, type IMap, isString, Result } from 'ts-data-forge';
2
2
  import '../node-global.mjs';
3
3
  import { assertPathExists } from './assert-path-exists.mjs';
4
+ import { createResultAssert } from './create-result-assert.mjs';
4
5
 
5
6
  /** Configuration for directory extension checking. */
6
7
  export type CheckExtConfig = DeepReadonly<{
@@ -23,13 +24,21 @@ export type CheckExtConfig = DeepReadonly<{
23
24
  }[];
24
25
  }>;
25
26
 
27
+ export type CheckExtError = Readonly<{
28
+ message: string;
29
+ files: readonly string[];
30
+ }>;
31
+
26
32
  /**
27
33
  * Validates that all files in specified directories have the correct
28
- * extensions. Exits with code 1 if any files have incorrect extensions.
34
+ * extensions.
29
35
  *
30
36
  * @param config - Configuration specifying directories and expected extensions.
37
+ * @returns Result.ok when all files pass, otherwise Result.err with details.
31
38
  */
32
- export const assertExt = async (config: CheckExtConfig): Promise<void> => {
39
+ export const checkExt = async (
40
+ config: CheckExtConfig,
41
+ ): Promise<Result<undefined, CheckExtError>> => {
33
42
  // Check all directories in parallel
34
43
  const results = await Promise.all(
35
44
  config.directories.map(async ({ path: dir, extension, ignorePatterns }) => {
@@ -49,59 +58,43 @@ export const assertExt = async (config: CheckExtConfig): Promise<void> => {
49
58
  // Collect all incorrect files
50
59
  const allIncorrectFiles: readonly string[] = results.flat();
51
60
 
52
- if (allIncorrectFiles.length > 0) {
53
- const generateErrorMessage = (): string => {
54
- // Group directories by extension for a cleaner message
55
- const extensionGroups: IMap<
56
- string,
57
- readonly Readonly<{
58
- relativePath: string;
59
- extKey: string;
60
- }>[]
61
- > = Arr.groupBy(
62
- config.directories.map(({ path: dirPath, extension }) => {
63
- const relativePath = path.relative(process.cwd(), dirPath);
64
- const extKey = isString(extension)
65
- ? extension
66
- : extension.join(' or ');
67
-
68
- return {
69
- relativePath,
70
- extKey,
71
- };
72
- }),
73
- ({ extKey }) => extKey,
74
- );
75
-
76
- // Generate message parts for each extension
77
- const messageParts = Array.from(
78
- extensionGroups.entries(),
79
- ([ext, dirs]) => {
80
- const dirList =
81
- dirs.length === 1
82
- ? dirs[0]?.relativePath
83
- : dirs.map((d) => d.relativePath).join(', ');
84
- return `${dirList} should have ${ext} extension`;
85
- },
86
- );
87
-
88
- return `All files in ${messageParts.join(' and ')}.`;
89
- };
90
-
91
- const errorMessage = [
92
- 'Files with incorrect extensions found:',
93
- ...allIncorrectFiles.map((file) => ` - ${file}`),
94
- '',
95
- generateErrorMessage(),
96
- ].join('\n');
97
-
98
- echo(errorMessage);
99
- process.exit(1);
61
+ if (allIncorrectFiles.length === 0) {
62
+ return Result.ok(undefined);
100
63
  }
101
64
 
102
- echo('✓ All files have correct extensions');
65
+ const message = [
66
+ 'Files with incorrect extensions found:',
67
+ ...allIncorrectFiles.map((file) => ` - ${file}`),
68
+ '',
69
+ describeExpectedExtensions(config),
70
+ ].join('\n');
71
+
72
+ return Result.err({
73
+ message,
74
+ files: allIncorrectFiles,
75
+ });
103
76
  };
104
77
 
78
+ /**
79
+ * Validates that all files in specified directories have the correct
80
+ * extensions. Exits with code 1 if any files have incorrect extensions.
81
+ *
82
+ * @param config - Configuration specifying directories and expected extensions.
83
+ */
84
+ export const assertExt = createResultAssert<
85
+ CheckExtConfig,
86
+ undefined,
87
+ CheckExtError
88
+ >({
89
+ run: checkExt,
90
+ onError: (error) => {
91
+ echo(error.message);
92
+ },
93
+ onSuccess: () => {
94
+ echo('✓ All files have correct extensions');
95
+ },
96
+ });
97
+
105
98
  /**
106
99
  * Checks if all files in a directory have the expected extension.
107
100
  *
@@ -134,3 +127,36 @@ const getFilesWithIncorrectExtension = async (
134
127
  (file) => !expectedExtensions.some((ext) => file.endsWith(ext)),
135
128
  );
136
129
  };
130
+
131
+ const describeExpectedExtensions = (config: CheckExtConfig): string => {
132
+ // Group directories by extension for a cleaner message
133
+ const extensionGroups: IMap<
134
+ string,
135
+ readonly Readonly<{
136
+ relativePath: string;
137
+ extKey: string;
138
+ }>[]
139
+ > = Arr.groupBy(
140
+ config.directories.map(({ path: dirPath, extension }) => {
141
+ const relativePath = path.relative(process.cwd(), dirPath);
142
+ const extKey = isString(extension) ? extension : extension.join(' or ');
143
+
144
+ return {
145
+ relativePath,
146
+ extKey,
147
+ };
148
+ }),
149
+ ({ extKey }) => extKey,
150
+ );
151
+
152
+ // Generate message parts for each extension
153
+ const messageParts = Array.from(extensionGroups.entries(), ([ext, dirs]) => {
154
+ const dirList =
155
+ dirs.length === 1
156
+ ? dirs[0]?.relativePath
157
+ : dirs.map((d) => d.relativePath).join(', ');
158
+ return `${dirList} should have ${ext} extension`;
159
+ });
160
+
161
+ return `All files in ${messageParts.join(' and ')}.`;
162
+ };
@@ -0,0 +1,59 @@
1
+ import { hasKey, isRecord, isString, Result } from 'ts-data-forge';
2
+ import '../node-global.mjs';
3
+
4
+ type ResultProducer<TConfig, TOk, TErr> = (
5
+ config: TConfig,
6
+ ) => Promise<Result<TOk, TErr>>;
7
+
8
+ export type CreateResultAssertOptions<Config, Ok, Err> = Readonly<{
9
+ run: ResultProducer<Config, Ok, Err>;
10
+ onSuccess?: (value: Ok, config: Config) => void | Promise<void>;
11
+ onError?: (error: Err, config: Config) => void | Promise<void>;
12
+ exitCode?: number;
13
+ }>;
14
+
15
+ /**
16
+ * Converts a function that returns a Result into an assert-style variant that
17
+ * exits the process when the Result is Err. This is useful for building CLI
18
+ * commands that should stop execution on failure but remain composable when a
19
+ * Result is preferred.
20
+ */
21
+ export const createResultAssert = <Config, Ok, Err>({
22
+ run,
23
+ onSuccess,
24
+ onError,
25
+ exitCode = 1,
26
+ }: CreateResultAssertOptions<Config, Ok, Err>): ((
27
+ config: Config,
28
+ ) => Promise<Ok>) => {
29
+ const defaultOnError = (error: Err): void => {
30
+ if (
31
+ isRecord(error) &&
32
+ hasKey(error, 'message') &&
33
+ isString(error.message)
34
+ ) {
35
+ echo(error.message);
36
+ } else {
37
+ console.error(error);
38
+ }
39
+ };
40
+
41
+ return async (config: Config): Promise<Ok> => {
42
+ const result = await run(config);
43
+
44
+ if (Result.isErr(result)) {
45
+ if (onError !== undefined) {
46
+ await onError(result.value, config);
47
+ } else {
48
+ defaultOnError(result.value);
49
+ }
50
+ process.exit(exitCode);
51
+ }
52
+
53
+ if (onSuccess !== undefined) {
54
+ await onSuccess(result.value, config);
55
+ }
56
+
57
+ return result.value;
58
+ };
59
+ };
@@ -5,6 +5,18 @@ type ExecOptionsCustom = Readonly<{
5
5
  silent?: boolean;
6
6
  }>;
7
7
 
8
+ type ExecOptionsWithStringEncoding = Readonly<
9
+ childProcess.ExecOptionsWithStringEncoding & ExecOptionsCustom
10
+ >;
11
+
12
+ type ExecOptionsWithBufferEncoding = Readonly<
13
+ childProcess.ExecOptionsWithBufferEncoding & ExecOptionsCustom
14
+ >;
15
+
16
+ type NormalizedExecOptions = Readonly<
17
+ childProcess.ExecOptions & { encoding?: BufferEncoding | 'buffer' | null }
18
+ >;
19
+
8
20
  export type ExecOptions = childProcess.ExecOptions & ExecOptionsCustom;
9
21
 
10
22
  export type ExecResult<T extends string | Buffer> = Result<
@@ -23,47 +35,92 @@ export type ExecResult<T extends string | Buffer> = Result<
23
35
  export function $(
24
36
  command: string,
25
37
  // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
26
- options?:
27
- | ExecOptionsCustom
28
- | Readonly<{ encoding: BufferEncoding } & ExecOptions>,
38
+ options?: ExecOptionsWithStringEncoding,
29
39
  ): Promise<ExecResult<string>>;
30
40
 
31
41
  export function $(
32
42
  command: string,
33
43
  // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
34
- options?: Readonly<{ encoding: 'buffer' | null } & ExecOptions>,
44
+ options: ExecOptionsWithBufferEncoding,
35
45
  ): Promise<ExecResult<Buffer>>;
36
46
 
47
+ export function $<
48
+ TOptions extends
49
+ | ExecOptionsWithBufferEncoding
50
+ | ExecOptionsWithStringEncoding
51
+ | undefined = undefined,
52
+ >(
53
+ command: string,
54
+ options?: TOptions,
55
+ ): Promise<
56
+ ExecResult<TOptions extends ExecOptionsWithBufferEncoding ? Buffer : string>
57
+ >;
58
+
37
59
  export function $(
38
60
  command: string,
39
61
  // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
40
- options?: Readonly<
41
- { encoding?: BufferEncoding | 'buffer' | null } & ExecOptions
42
- >,
62
+ options?: ExecOptionsWithStringEncoding | ExecOptionsWithBufferEncoding,
43
63
  ): Promise<ExecResult<string | Buffer>> {
44
64
  const { silent = false, ...restOptions } = options ?? {};
65
+ const normalizedOptions: NormalizedExecOptions = restOptions;
45
66
 
46
67
  if (!silent) {
47
68
  echo(`$ ${command}`);
48
69
  }
49
70
 
50
71
  return new Promise((resolve) => {
51
- // eslint-disable-next-line security/detect-child-process
52
- childProcess.exec(command, restOptions, (error, stdout, stderr) => {
72
+ const handleResult = <T extends string | Buffer>(
73
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
74
+ error: childProcess.ExecException | null,
75
+ stdout: T,
76
+ stderr: T,
77
+ ): void => {
53
78
  if (!silent) {
54
- if (stdout !== '') {
79
+ if (!isEmpty(stdout)) {
55
80
  echo(stdout);
56
81
  }
57
- if (stderr !== '') {
82
+ if (!isEmpty(stderr)) {
58
83
  console.error(stderr);
59
84
  }
60
85
  }
61
86
 
62
87
  if (error !== null) {
63
88
  resolve(Result.err(error));
64
- } else {
65
- resolve(Result.ok({ stdout, stderr }));
89
+ return;
66
90
  }
67
- });
91
+
92
+ resolve(
93
+ Result.ok<Readonly<{ stdout: T; stderr: T }>>({ stdout, stderr }),
94
+ );
95
+ };
96
+
97
+ const encoding = normalizedOptions.encoding;
98
+
99
+ if (encoding === 'buffer' || encoding === null) {
100
+ // eslint-disable-next-line security/detect-child-process
101
+ childProcess.exec(
102
+ command,
103
+ // eslint-disable-next-line total-functions/no-unsafe-type-assertion
104
+ normalizedOptions as childProcess.ExecOptionsWithBufferEncoding,
105
+ (error, stdout, stderr) => {
106
+ handleResult(error, stdout, stderr);
107
+ },
108
+ );
109
+ return;
110
+ }
111
+
112
+ // eslint-disable-next-line security/detect-child-process
113
+ childProcess.exec(
114
+ command,
115
+ // eslint-disable-next-line total-functions/no-unsafe-type-assertion
116
+ normalizedOptions as childProcess.ExecOptionsWithStringEncoding,
117
+ (error, stdout, stderr) => {
118
+ handleResult(error, stdout, stderr);
119
+ },
120
+ );
68
121
  });
69
122
  }
123
+
124
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
125
+ const isEmpty = (value: string | Buffer): boolean =>
126
+ typeof value === 'string' ? value === '' : value.length === 0;
@@ -368,8 +368,8 @@ describe('exec-async', () => {
368
368
  // Type check for native exec with buffer encoding
369
369
  exec('echo "test"', { encoding: 'buffer' }, (error, stdout, stderr) => {
370
370
  expectType<ExecException | null, typeof error>('=');
371
- expectType<Buffer, typeof stdout>('=');
372
- expectType<Buffer, typeof stderr>('=');
371
+ expectType<Buffer, typeof stdout>('>=');
372
+ expectType<Buffer, typeof stderr>('>=');
373
373
  });
374
374
 
375
375
  // The $ function should produce the same types wrapped in Result (suppressed for clean output)
@@ -381,15 +381,15 @@ describe('exec-async', () => {
381
381
  Promise<
382
382
  Result<Readonly<{ stdout: Buffer; stderr: Buffer }>, ExecException>
383
383
  >
384
- >('=');
384
+ >('~=');
385
385
  });
386
386
 
387
387
  test('should match exec callback types for null encoding', () => {
388
388
  // Type check for native exec with null encoding
389
389
  exec('echo "test"', { encoding: null }, (error, stdout, stderr) => {
390
390
  expectType<ExecException | null, typeof error>('=');
391
- expectType<Buffer, typeof stdout>('=');
392
- expectType<Buffer, typeof stderr>('=');
391
+ expectType<Buffer, typeof stdout>('>=');
392
+ expectType<Buffer, typeof stderr>('>=');
393
393
  });
394
394
 
395
395
  // The $ function should produce the same types wrapped in Result (suppressed for clean output)
@@ -1,6 +1,6 @@
1
1
  import { type ExecException } from 'node:child_process';
2
2
  import * as prettier from 'prettier';
3
- import { Arr, isNotUndefined, Result } from 'ts-data-forge';
3
+ import { Arr, isNotUndefined, pipe, Result } from 'ts-data-forge';
4
4
  import '../node-global.mjs';
5
5
  import {
6
6
  getDiffFrom,
@@ -127,21 +127,28 @@ const defaultIgnoreFn = (filePath: string): boolean => {
127
127
  ignoreFiles.has(filename) ||
128
128
  filename.startsWith('.env') ||
129
129
  ignoreExtensions.some((ext) => filePath.endsWith(ext)) ||
130
- ignoreDirs.some((dir) => filename.startsWith(dir))
130
+ pipe(filePath.split(path.sep)).map((pathSegments) =>
131
+ pathSegments.some((segment) => ignoreDirs.includes(segment)),
132
+ ).value
131
133
  );
132
134
  };
133
135
 
134
136
  const ignoreFiles: ReadonlySet<string> = new Set([
135
137
  '.DS_Store',
136
138
  'package-lock.json',
139
+ 'yarn.lock',
140
+ 'pnpm-lock.yaml',
137
141
  'LICENSE',
138
142
  '.prettierignore',
139
143
  '.editorconfig',
140
144
  '.gitignore',
141
145
  '.npmignore',
146
+ '.envrc',
147
+ '.nvmrc',
148
+ '.npmrc',
142
149
  ]);
143
150
 
144
- const ignoreExtensions: readonly string[] = [
151
+ const ignoreExtensions: readonly `.${string}`[] = [
145
152
  '.svg',
146
153
  '.png',
147
154
  '.jpg',
@@ -171,6 +178,7 @@ const ignoreDirs: readonly string[] = [
171
178
  '.cache',
172
179
  '.vscode',
173
180
  '.yarn',
181
+ '.wireit',
174
182
  ] as const;
175
183
 
176
184
  /**
@@ -1,6 +1,7 @@
1
1
  export * from './assert-ext.mjs';
2
2
  export * from './assert-path-exists.mjs';
3
3
  export * from './assert-repo-is-clean.mjs';
4
+ export * from './create-result-assert.mjs';
4
5
  export * from './diff.mjs';
5
6
  export * from './exec-async.mjs';
6
7
  export * from './format.mjs';
@@ -57,7 +57,7 @@ export const executeParallel = async (
57
57
 
58
58
  const wrappedPromise = promise
59
59
  // eslint-disable-next-line @typescript-eslint/no-loop-func
60
- .catch((error) => {
60
+ .catch((error: unknown) => {
61
61
  mut_failed = true;
62
62
  throw error; // Re-throw to ensure fail-fast propagation
63
63
  })
@@ -1,4 +1,5 @@
1
1
  /* eslint-disable vitest/no-restricted-vi-methods */
2
+ import { type MockInstance } from 'vitest';
2
3
  import '../../node-global.mjs';
3
4
  import { executeStages } from './execute-parallel.mjs';
4
5
  import { getWorkspacePackages } from './get-workspace-packages.mjs';
@@ -16,9 +17,9 @@ vi.mock('./get-workspace-packages.mjs', () => ({
16
17
 
17
18
  describe('runCmdInStagesAcrossWorkspaces', () => {
18
19
  type MockedSpies = Readonly<{
19
- consoleLogSpy: ReturnType<typeof vi.spyOn>;
20
- consoleErrorSpy: ReturnType<typeof vi.spyOn>;
21
- processExitSpy: ReturnType<typeof vi.spyOn>;
20
+ consoleLogSpy: MockInstance<typeof console.log>;
21
+ consoleErrorSpy: MockInstance<typeof console.error>;
22
+ processExitSpy: MockInstance<typeof process.exit>;
22
23
  }>;
23
24
 
24
25
  const setupSpies = (): MockedSpies => {
@@ -30,14 +31,10 @@ describe('runCmdInStagesAcrossWorkspaces', () => {
30
31
  .spyOn(console, 'error')
31
32
  .mockImplementation((): void => {});
32
33
 
33
- // eslint-disable-next-line total-functions/no-unsafe-type-assertion
34
34
  const processExitSpy = vi
35
35
  .spyOn(process, 'exit')
36
-
37
36
  // eslint-disable-next-line total-functions/no-unsafe-type-assertion
38
- .mockImplementation((): never => undefined as never) as ReturnType<
39
- typeof vi.spyOn
40
- >;
37
+ .mockImplementation((): never => undefined as never);
41
38
 
42
39
  return { consoleLogSpy, consoleErrorSpy, processExitSpy };
43
40
  };
@@ -1,8 +1,9 @@
1
- /* eslint-disable import/no-internal-modules */
1
+ /* eslint-disable import-x/no-internal-modules */
2
2
  import glob_ from 'fast-glob';
3
3
  import * as fs_ from 'node:fs/promises';
4
4
  import * as os_ from 'node:os';
5
5
  import * as path_ from 'node:path';
6
+ import { chdir as chdir_ } from 'node:process';
6
7
  import { Result as Result_ } from 'ts-data-forge';
7
8
  import { $ as $_ } from './functions/exec-async.mjs';
8
9
  import { isDirectlyExecuted as isDirectlyExecuted_ } from './functions/is-directly-executed.mjs';
@@ -19,6 +20,7 @@ const globalsDef = {
19
20
  $: $_,
20
21
  Result: Result_,
21
22
  echo: console.log,
23
+ cd: chdir_,
22
24
  isDirectlyExecuted: isDirectlyExecuted_,
23
25
  } as const;
24
26
 
@@ -35,5 +37,6 @@ declare global {
35
37
  const Result: typeof Result_;
36
38
  type Result<S, E> = Result_<S, E>;
37
39
  const echo: typeof console.log;
40
+ const cd: typeof chdir_;
38
41
  const isDirectlyExecuted: typeof isDirectlyExecuted_;
39
42
  }