@williamthorsen/nmr-core 0.3.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 (50) hide show
  1. package/LICENSE +7 -0
  2. package/README.md +21 -0
  3. package/dist/esm/.cache +1 -0
  4. package/dist/esm/cli-report-overrides.d.ts +1 -0
  5. package/dist/esm/cli-report-overrides.js +9 -0
  6. package/dist/esm/cli-sync-pnpm-version.d.ts +1 -0
  7. package/dist/esm/cli-sync-pnpm-version.js +9 -0
  8. package/dist/esm/cli.d.ts +1 -0
  9. package/dist/esm/cli.js +110 -0
  10. package/dist/esm/commands/report-overrides.d.ts +1 -0
  11. package/dist/esm/commands/report-overrides.js +15 -0
  12. package/dist/esm/commands/sync-pnpm-version.d.ts +3 -0
  13. package/dist/esm/commands/sync-pnpm-version.js +42 -0
  14. package/dist/esm/config.d.ts +6 -0
  15. package/dist/esm/config.js +21 -0
  16. package/dist/esm/context.d.ts +11 -0
  17. package/dist/esm/context.js +84 -0
  18. package/dist/esm/findPackageRoot.d.ts +1 -0
  19. package/dist/esm/findPackageRoot.js +17 -0
  20. package/dist/esm/help.d.ts +2 -0
  21. package/dist/esm/help.js +31 -0
  22. package/dist/esm/helpers/code-quality-pnpm-action.d.ts +12 -0
  23. package/dist/esm/helpers/code-quality-pnpm-action.js +17 -0
  24. package/dist/esm/helpers/package-json.d.ts +11 -0
  25. package/dist/esm/helpers/package-json.js +45 -0
  26. package/dist/esm/helpers/type-guards.d.ts +1 -0
  27. package/dist/esm/helpers/type-guards.js +6 -0
  28. package/dist/esm/helpers/yaml-utils.d.ts +1 -0
  29. package/dist/esm/helpers/yaml-utils.js +14 -0
  30. package/dist/esm/index.d.ts +7 -0
  31. package/dist/esm/index.js +17 -0
  32. package/dist/esm/parseArgs.d.ts +15 -0
  33. package/dist/esm/parseArgs.js +93 -0
  34. package/dist/esm/registries.d.ts +5 -0
  35. package/dist/esm/registries.js +67 -0
  36. package/dist/esm/resolver.d.ts +11 -0
  37. package/dist/esm/resolver.js +64 -0
  38. package/dist/esm/runner.d.ts +4 -0
  39. package/dist/esm/runner.js +38 -0
  40. package/dist/esm/terminal.d.ts +6 -0
  41. package/dist/esm/terminal.js +55 -0
  42. package/dist/esm/tests/consistency.d.ts +1 -0
  43. package/dist/esm/tests/consistency.js +70 -0
  44. package/dist/esm/tests/helpers/get-runtime-version-from-asdf.d.ts +1 -0
  45. package/dist/esm/tests/helpers/get-runtime-version-from-asdf.js +15 -0
  46. package/dist/esm/tests/helpers/get-value-at-path.d.ts +1 -0
  47. package/dist/esm/tests/helpers/get-value-at-path.js +34 -0
  48. package/dist/esm/writeFileWithCheck.d.ts +10 -0
  49. package/dist/esm/writeFileWithCheck.js +41 -0
  50. package/package.json +33 -0
package/LICENSE ADDED
@@ -0,0 +1,7 @@
1
+ ISC License
2
+
3
+ Copyright (c) 2026 William Thorsen
4
+
5
+ Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies.
6
+
7
+ The software is provided "as is" and the author disclaims all warranties with regard to this software including all implied warranties of merchantability and fitness. In no event shall the author be liable for any special, direct, indirect, or consequential damages or any damages whatsoever resulting from loss of use, data or profits, whether in an action of contract, negligence or other tortious action, arising out of or in connection with the use or performance of this software.
package/README.md ADDED
@@ -0,0 +1,21 @@
1
+ # @williamthorsen/nmr-core
2
+
3
+ Shared utilities for node-monorepo-tools packages.
4
+
5
+ This package serves as the shared library foundation for the monorepo. For the nmr CLI tool, see [`@williamthorsen/nmr`](../nmr).
6
+
7
+ <!-- section:release-notes -->
8
+ ## Release notes — v0.3.0 (2026-04-23)
9
+
10
+ ### Features
11
+
12
+ - Scaffold audit.yaml workflow from audit-deps init (#277)
13
+
14
+ Adds GitHub Actions workflow scaffolding to `audit-deps init`. Running the command now writes both `.config/audit-deps.config.json` and `.github/workflows/audit.yaml` in the target repo, so that consumers no longer have to copy the canonical caller workflow by hand from this repo. The workflow content is shipped as a bundled template that ships to npm, and the repo's own workflow is kept byte-identical to that template via a consistency test — the canonical workflow cannot silently drift from what is published.
15
+ <!-- /section:release-notes -->
16
+
17
+ ## Installation
18
+
19
+ ```bash
20
+ pnpm add -D @williamthorsen/nmr-core
21
+ ```
@@ -0,0 +1 @@
1
+ 45293b659d2a5fd6eb962de2df0c6baec80dcb02ed10f63893831df600a7a39b
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,9 @@
1
+ import { reportOverrides } from "./commands/report-overrides.js";
2
+ import { findMonorepoRoot } from "./context.js";
3
+ try {
4
+ const monorepoRoot = findMonorepoRoot();
5
+ reportOverrides(monorepoRoot);
6
+ } catch (error) {
7
+ console.error(error instanceof Error ? error.message : String(error));
8
+ process.exit(1);
9
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,9 @@
1
+ import { syncPnpmVersion } from "./commands/sync-pnpm-version.js";
2
+ import { findMonorepoRoot } from "./context.js";
3
+ try {
4
+ const monorepoRoot = findMonorepoRoot();
5
+ syncPnpmVersion(monorepoRoot);
6
+ } catch (error) {
7
+ console.error(error instanceof Error ? error.message : String(error));
8
+ process.exit(1);
9
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,110 @@
1
+ import process from "node:process";
2
+ import { resolveContext } from "./context.js";
3
+ import { generateHelp } from "./help.js";
4
+ import { buildRootRegistry, buildWorkspaceRegistry, resolveScript } from "./resolver.js";
5
+ import { runCommand } from "./runner.js";
6
+ function shellQuote(arg) {
7
+ return "'" + arg.replace(/'/g, String.raw`'\''`) + "'";
8
+ }
9
+ function parseArgs(argv) {
10
+ const args = argv.slice(2);
11
+ const result = {
12
+ quiet: false,
13
+ recursive: false,
14
+ workspaceRoot: false,
15
+ help: false,
16
+ intTest: false,
17
+ passthrough: []
18
+ };
19
+ let i = 0;
20
+ while (i < args.length) {
21
+ const arg = args[i];
22
+ if (arg === void 0) break;
23
+ if (arg === "-F" || arg === "--filter") {
24
+ i++;
25
+ const filterValue = args[i];
26
+ if (filterValue === void 0) {
27
+ console.error("Error: -F/--filter requires a pattern argument");
28
+ process.exit(1);
29
+ }
30
+ result.filter = filterValue;
31
+ i++;
32
+ continue;
33
+ }
34
+ if (arg === "-R" || arg === "--recursive") {
35
+ result.recursive = true;
36
+ i++;
37
+ continue;
38
+ }
39
+ if (arg === "-w" || arg === "--workspace-root") {
40
+ result.workspaceRoot = true;
41
+ i++;
42
+ continue;
43
+ }
44
+ if (arg === "-?" || arg === "--help") {
45
+ result.help = true;
46
+ i++;
47
+ continue;
48
+ }
49
+ if (arg === "-q" || arg === "--quiet") {
50
+ result.quiet = true;
51
+ i++;
52
+ continue;
53
+ }
54
+ if (arg === "--int-test") {
55
+ result.intTest = true;
56
+ i++;
57
+ continue;
58
+ }
59
+ result.command = arg;
60
+ result.passthrough = args.slice(i + 1);
61
+ break;
62
+ }
63
+ return result;
64
+ }
65
+ async function main() {
66
+ const parsed = parseArgs(process.argv);
67
+ const context = await resolveContext();
68
+ if (parsed.help || !parsed.command) {
69
+ console.info(generateHelp(context.config));
70
+ process.exit(0);
71
+ }
72
+ const { command } = parsed;
73
+ const passthrough = parsed.passthrough.length > 0 ? " " + parsed.passthrough.map(shellQuote).join(" ") : "";
74
+ const runOptions = { quiet: parsed.quiet };
75
+ if (parsed.filter) {
76
+ const delegateCmd = `pnpm --filter ${shellQuote(parsed.filter)} exec nmr ${command}${passthrough}`;
77
+ const code2 = runCommand(delegateCmd, context.monorepoRoot, runOptions);
78
+ process.exit(code2);
79
+ }
80
+ if (parsed.recursive) {
81
+ const delegateCmd = `pnpm --recursive exec nmr ${command}${passthrough}`;
82
+ const code2 = runCommand(delegateCmd, context.monorepoRoot, runOptions);
83
+ process.exit(code2);
84
+ }
85
+ const useRoot = parsed.workspaceRoot || context.isRoot;
86
+ const registry = useRoot ? buildRootRegistry(context.config) : buildWorkspaceRegistry(context.config, parsed.intTest);
87
+ const resolved = resolveScript(command, registry, context.packageDir);
88
+ if (!resolved) {
89
+ console.error(`Unknown command: ${command}`);
90
+ process.exit(1);
91
+ }
92
+ if (resolved.command === "") {
93
+ if (!parsed.quiet) {
94
+ console.info("Override script is defined but empty. Skipping.");
95
+ }
96
+ process.exit(0);
97
+ }
98
+ if (resolved.source === "package" && !parsed.quiet) {
99
+ console.info(`Using override script: ${resolved.command}`);
100
+ }
101
+ const fullCommand = resolved.command + passthrough;
102
+ const code = runCommand(fullCommand, void 0, runOptions);
103
+ process.exit(code);
104
+ }
105
+ try {
106
+ await main();
107
+ } catch (error) {
108
+ console.error(error);
109
+ process.exit(1);
110
+ }
@@ -0,0 +1 @@
1
+ export declare function reportOverrides(monorepoRoot: string): void;
@@ -0,0 +1,15 @@
1
+ import { getPnpmOverrides, readPackageJson } from "../helpers/package-json.js";
2
+ function reportOverrides(monorepoRoot) {
3
+ const pkg = readPackageJson(monorepoRoot);
4
+ const overrides = getPnpmOverrides(pkg);
5
+ if (!overrides || Object.keys(overrides).length === 0) {
6
+ return;
7
+ }
8
+ console.warn("\u{1F512} WARN: pnpm overrides are active! Check whether these are still needed:");
9
+ for (const [name, version] of Object.entries(overrides)) {
10
+ console.warn(`- ${name} \u2192 ${version}`);
11
+ }
12
+ }
13
+ export {
14
+ reportOverrides
15
+ };
@@ -0,0 +1,3 @@
1
+ declare function extractPnpmVersion(packageManager: string | undefined): string | null;
2
+ export declare function syncPnpmVersion(monorepoRoot: string): void;
3
+ export { extractPnpmVersion };
@@ -0,0 +1,42 @@
1
+ import { readFileSync, writeFileSync } from "node:fs";
2
+ import path from "node:path";
3
+ import { CodeQualityPnpmWorkflowSchema, getPnpmVersion } from "../helpers/code-quality-pnpm-action.js";
4
+ import { readPackageJson } from "../helpers/package-json.js";
5
+ import { readYamlFile } from "../helpers/yaml-utils.js";
6
+ const WORKFLOW_RELATIVE_PATH = ".github/workflows/code-quality.yaml";
7
+ function extractPnpmVersion(packageManager) {
8
+ if (!packageManager) {
9
+ return null;
10
+ }
11
+ const match = /^pnpm@(.+)$/.exec(packageManager);
12
+ return match?.[1] ?? null;
13
+ }
14
+ function syncPnpmVersion(monorepoRoot) {
15
+ console.info("Synchronizing pnpm version in GitHub workflow...");
16
+ const pkg = readPackageJson(monorepoRoot);
17
+ const pnpmVersion = extractPnpmVersion(pkg.packageManager);
18
+ if (!pnpmVersion) {
19
+ throw new Error(
20
+ `Could not extract pnpm version from package.json packageManager field
21
+ packageManager field: ${pkg.packageManager ?? "(not set)"}`
22
+ );
23
+ }
24
+ console.info(`Package.json pnpm version: ${pnpmVersion}`);
25
+ const workflowPath = path.join(monorepoRoot, WORKFLOW_RELATIVE_PATH);
26
+ const workflowData = readYamlFile(workflowPath);
27
+ const workflow = CodeQualityPnpmWorkflowSchema.parse(workflowData);
28
+ const currentWorkflowVersion = getPnpmVersion(workflow);
29
+ console.info(`Current workflow pnpm version: ${currentWorkflowVersion}`);
30
+ if (currentWorkflowVersion === pnpmVersion) {
31
+ console.info("Workflow pnpm version is already up to date");
32
+ return;
33
+ }
34
+ const originalContent = readFileSync(workflowPath, "utf8");
35
+ const updatedContent = originalContent.replace(/(\s+pnpm-version:\s+)(['"]?)[\d.]+\2/, `$1$2${pnpmVersion}$2`);
36
+ writeFileSync(workflowPath, updatedContent, "utf8");
37
+ console.info(`\u2713 Updated workflow pnpm version: ${currentWorkflowVersion} \u2192 ${pnpmVersion}`);
38
+ }
39
+ export {
40
+ extractPnpmVersion,
41
+ syncPnpmVersion
42
+ };
@@ -0,0 +1,6 @@
1
+ export interface NmrConfig {
2
+ workspaceScripts?: Record<string, string | string[]>;
3
+ rootScripts?: Record<string, string | string[]>;
4
+ }
5
+ export declare function defineConfig(config: NmrConfig): NmrConfig;
6
+ export declare function loadConfig(monorepoRoot: string): Promise<NmrConfig>;
@@ -0,0 +1,21 @@
1
+ import { existsSync } from "node:fs";
2
+ import path from "node:path";
3
+ import { createJiti } from "jiti";
4
+ const CONFIG_FILENAME = "nmr.config.ts";
5
+ const CONFIG_DIR = ".config";
6
+ function defineConfig(config) {
7
+ return config;
8
+ }
9
+ async function loadConfig(monorepoRoot) {
10
+ const configPath = path.join(monorepoRoot, CONFIG_DIR, CONFIG_FILENAME);
11
+ if (!existsSync(configPath)) {
12
+ return {};
13
+ }
14
+ const jiti = createJiti(path.join(monorepoRoot, "package.json"));
15
+ const loaded = await jiti.import(configPath, { default: true });
16
+ return loaded;
17
+ }
18
+ export {
19
+ defineConfig,
20
+ loadConfig
21
+ };
@@ -0,0 +1,11 @@
1
+ import type { NmrConfig } from './config.js';
2
+ export interface ResolvedContext {
3
+ monorepoRoot: string;
4
+ isRoot: boolean;
5
+ packageDir?: string;
6
+ config: NmrConfig;
7
+ }
8
+ export declare function findMonorepoRoot(startDir?: string): string;
9
+ export declare function getWorkspacePackageDirs(monorepoRoot: string): string[];
10
+ export declare function findContainingPackageDir(dir: string, workspacePackageDirs: string[]): string | undefined;
11
+ export declare function resolveContext(cwd?: string): Promise<ResolvedContext>;
@@ -0,0 +1,84 @@
1
+ import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
2
+ import path from "node:path";
3
+ import yaml from "js-yaml";
4
+ import { loadConfig } from "./config.js";
5
+ import { isObject } from "./helpers/type-guards.js";
6
+ function findMonorepoRoot(startDir) {
7
+ let dir = path.resolve(startDir ?? process.cwd());
8
+ for (; ; ) {
9
+ if (existsSync(path.join(dir, "pnpm-workspace.yaml"))) {
10
+ return dir;
11
+ }
12
+ const parent = path.dirname(dir);
13
+ if (parent === dir) {
14
+ throw new Error("Could not find monorepo root: no pnpm-workspace.yaml found in any parent directory");
15
+ }
16
+ dir = parent;
17
+ }
18
+ }
19
+ function getWorkspacePackageDirs(monorepoRoot) {
20
+ const workspaceFile = path.join(monorepoRoot, "pnpm-workspace.yaml");
21
+ const content = readFileSync(workspaceFile, "utf8");
22
+ const parsed = yaml.load(content);
23
+ const packages = getPackagesFromParsedYaml(parsed);
24
+ if (!packages) {
25
+ return [];
26
+ }
27
+ const dirs = [];
28
+ for (const pattern of packages) {
29
+ if (pattern.endsWith("/*")) {
30
+ const prefix = pattern.slice(0, -2);
31
+ const prefixDir = path.resolve(monorepoRoot, prefix);
32
+ if (existsSync(prefixDir)) {
33
+ for (const entry of readdirSync(prefixDir)) {
34
+ const fullPath = path.join(prefixDir, entry);
35
+ if (statSync(fullPath).isDirectory() && existsSync(path.join(fullPath, "package.json"))) {
36
+ dirs.push(fullPath);
37
+ }
38
+ }
39
+ }
40
+ } else if (!pattern.includes("*")) {
41
+ const fullPath = path.resolve(monorepoRoot, pattern);
42
+ if (existsSync(fullPath) && existsSync(path.join(fullPath, "package.json"))) {
43
+ dirs.push(fullPath);
44
+ }
45
+ }
46
+ }
47
+ return dirs;
48
+ }
49
+ function findContainingPackageDir(dir, workspacePackageDirs) {
50
+ const resolved = path.resolve(dir);
51
+ for (const pkgDir of workspacePackageDirs) {
52
+ const resolvedPkgDir = path.resolve(pkgDir);
53
+ if (resolved === resolvedPkgDir || resolved.startsWith(resolvedPkgDir + path.sep)) {
54
+ return resolvedPkgDir;
55
+ }
56
+ }
57
+ return void 0;
58
+ }
59
+ function getPackagesFromParsedYaml(parsed) {
60
+ if (!isObject(parsed)) return void 0;
61
+ const packages = parsed.packages;
62
+ if (!Array.isArray(packages)) return void 0;
63
+ if (!packages.every((p) => typeof p === "string")) return void 0;
64
+ return packages;
65
+ }
66
+ async function resolveContext(cwd) {
67
+ const resolvedCwd = path.resolve(cwd ?? process.cwd());
68
+ const monorepoRoot = findMonorepoRoot(resolvedCwd);
69
+ const config = await loadConfig(monorepoRoot);
70
+ const workspaceDirs = getWorkspacePackageDirs(monorepoRoot);
71
+ const packageDir = findContainingPackageDir(resolvedCwd, workspaceDirs);
72
+ return {
73
+ monorepoRoot,
74
+ isRoot: packageDir === void 0,
75
+ ...packageDir === void 0 ? {} : { packageDir },
76
+ config
77
+ };
78
+ }
79
+ export {
80
+ findContainingPackageDir,
81
+ findMonorepoRoot,
82
+ getWorkspacePackageDirs,
83
+ resolveContext
84
+ };
@@ -0,0 +1 @@
1
+ export declare function findPackageRoot(fromUrl: string): string;
@@ -0,0 +1,17 @@
1
+ import { existsSync } from "node:fs";
2
+ import { dirname, resolve } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ function findPackageRoot(fromUrl) {
5
+ let dir = dirname(fileURLToPath(fromUrl));
6
+ while (!existsSync(resolve(dir, "package.json"))) {
7
+ const parent = dirname(dir);
8
+ if (parent === dir) {
9
+ throw new Error("Could not find package root from " + fromUrl);
10
+ }
11
+ dir = parent;
12
+ }
13
+ return dir;
14
+ }
15
+ export {
16
+ findPackageRoot
17
+ };
@@ -0,0 +1,2 @@
1
+ import type { NmrConfig } from './config.js';
2
+ export declare function generateHelp(config: NmrConfig): string;
@@ -0,0 +1,31 @@
1
+ import { buildRootRegistry, buildWorkspaceRegistry } from "./resolver.js";
2
+ import { describeScript } from "./resolver.js";
3
+ function generateHelp(config) {
4
+ const lines = [
5
+ "Usage: nmr [flags] <command> [args...]",
6
+ "",
7
+ "Flags:",
8
+ " -F, --filter <pattern> Run command in matching packages",
9
+ " -R, --recursive Run command in all packages",
10
+ " -w, --workspace-root Run root command regardless of cwd",
11
+ " -q, --quiet Suppress output on success; show full output on failure",
12
+ " -?, --help Show this help",
13
+ " --int-test Use integration test scripts",
14
+ "",
15
+ "Workspace commands:"
16
+ ];
17
+ formatRegistry(buildWorkspaceRegistry(config, false), lines);
18
+ lines.push("", "Root commands:");
19
+ formatRegistry(buildRootRegistry(config), lines);
20
+ return lines.join("\n");
21
+ }
22
+ function formatRegistry(registry, lines) {
23
+ const maxKeyLen = Math.max(...Object.keys(registry).map((k) => k.length));
24
+ const pad = Math.max(maxKeyLen + 2, 20);
25
+ for (const [key, value] of Object.entries(registry)) {
26
+ lines.push(` ${key.padEnd(pad)} ${describeScript(value)}`);
27
+ }
28
+ }
29
+ export {
30
+ generateHelp
31
+ };
@@ -0,0 +1,12 @@
1
+ import { z } from 'zod';
2
+ export declare const CodeQualityPnpmWorkflowSchema: z.ZodObject<{
3
+ jobs: z.ZodObject<{
4
+ 'code-quality': z.ZodObject<{
5
+ with: z.ZodObject<{
6
+ 'pnpm-version': z.ZodString;
7
+ }, z.core.$loose>;
8
+ }, z.core.$loose>;
9
+ }, z.core.$loose>;
10
+ }, z.core.$loose>;
11
+ export type CodeQualityPnpmWorkflow = z.infer<typeof CodeQualityPnpmWorkflowSchema>;
12
+ export declare function getPnpmVersion(workflow: CodeQualityPnpmWorkflow): string;
@@ -0,0 +1,17 @@
1
+ import { z } from "zod";
2
+ const CodeQualityPnpmWorkflowSchema = z.looseObject({
3
+ jobs: z.looseObject({
4
+ "code-quality": z.looseObject({
5
+ with: z.looseObject({
6
+ "pnpm-version": z.string()
7
+ })
8
+ })
9
+ })
10
+ });
11
+ function getPnpmVersion(workflow) {
12
+ return workflow.jobs["code-quality"].with["pnpm-version"];
13
+ }
14
+ export {
15
+ CodeQualityPnpmWorkflowSchema,
16
+ getPnpmVersion
17
+ };
@@ -0,0 +1,11 @@
1
+ export interface PackageJson {
2
+ name?: string;
3
+ version?: string;
4
+ packageManager?: string;
5
+ scripts?: Record<string, string>;
6
+ pnpm?: {
7
+ overrides?: Record<string, string>;
8
+ };
9
+ }
10
+ export declare function readPackageJson(dir: string): PackageJson;
11
+ export declare function getPnpmOverrides(pkg: PackageJson): Record<string, string> | undefined;
@@ -0,0 +1,45 @@
1
+ import { readFileSync } from "node:fs";
2
+ import path from "node:path";
3
+ import { isObject } from "./type-guards.js";
4
+ function readPackageJson(dir) {
5
+ const content = readFileSync(path.join(dir, "package.json"), "utf8");
6
+ const parsed = JSON.parse(content);
7
+ if (!isObject(parsed)) {
8
+ throw new TypeError(`Invalid package.json in ${dir}: expected an object`);
9
+ }
10
+ const pkg = {};
11
+ if (typeof parsed.name === "string") pkg.name = parsed.name;
12
+ if (typeof parsed.version === "string") pkg.version = parsed.version;
13
+ if (typeof parsed.packageManager === "string") pkg.packageManager = parsed.packageManager;
14
+ if (isObject(parsed.scripts)) {
15
+ const scripts = {};
16
+ for (const [key, val] of Object.entries(parsed.scripts)) {
17
+ if (typeof val === "string") scripts[key] = val;
18
+ }
19
+ pkg.scripts = scripts;
20
+ }
21
+ if (isObject(parsed.pnpm)) {
22
+ const pnpm = parsed.pnpm;
23
+ if (isObject(pnpm.overrides)) {
24
+ const overrides = {};
25
+ for (const [key, val] of Object.entries(pnpm.overrides)) {
26
+ if (typeof val === "string") overrides[key] = val;
27
+ }
28
+ pkg.pnpm = { overrides };
29
+ }
30
+ }
31
+ return pkg;
32
+ }
33
+ function getPnpmOverrides(pkg) {
34
+ if (!isObject(pkg.pnpm)) return void 0;
35
+ const overrides = pkg.pnpm.overrides;
36
+ if (!isObject(overrides)) return void 0;
37
+ for (const value of Object.values(overrides)) {
38
+ if (typeof value !== "string") return void 0;
39
+ }
40
+ return overrides;
41
+ }
42
+ export {
43
+ getPnpmOverrides,
44
+ readPackageJson
45
+ };
@@ -0,0 +1 @@
1
+ export declare function isObject(value: unknown): value is Record<string, unknown>;
@@ -0,0 +1,6 @@
1
+ function isObject(value) {
2
+ return typeof value === "object" && value !== null;
3
+ }
4
+ export {
5
+ isObject
6
+ };
@@ -0,0 +1 @@
1
+ export declare function readYamlFile(filepath: string): unknown;
@@ -0,0 +1,14 @@
1
+ import { readFileSync } from "node:fs";
2
+ import yaml from "js-yaml";
3
+ function readYamlFile(filepath) {
4
+ try {
5
+ const content = readFileSync(filepath, "utf8");
6
+ return yaml.load(content);
7
+ } catch (error) {
8
+ throw new Error(`Failed to read YAML file: ${filepath}
9
+ ${error instanceof Error ? error.message : String(error)}`);
10
+ }
11
+ }
12
+ export {
13
+ readYamlFile
14
+ };
@@ -0,0 +1,7 @@
1
+ export declare const PACKAGE_NAME = "@williamthorsen/nmr-core";
2
+ export { findPackageRoot } from './findPackageRoot.js';
3
+ export type { FlagDefinition, FlagSchema, ParsedArgs, ParsedFlags } from './parseArgs.js';
4
+ export { parseArgs, translateParseError } from './parseArgs.js';
5
+ export { printError, printSkip, printStep, printSuccess, reportWriteResult } from './terminal.js';
6
+ export type { WriteOutcome, WriteResult } from './writeFileWithCheck.js';
7
+ export { writeFileWithCheck } from './writeFileWithCheck.js';
@@ -0,0 +1,17 @@
1
+ const PACKAGE_NAME = "@williamthorsen/nmr-core";
2
+ import { findPackageRoot } from "./findPackageRoot.js";
3
+ import { parseArgs, translateParseError } from "./parseArgs.js";
4
+ import { printError, printSkip, printStep, printSuccess, reportWriteResult } from "./terminal.js";
5
+ import { writeFileWithCheck } from "./writeFileWithCheck.js";
6
+ export {
7
+ PACKAGE_NAME,
8
+ findPackageRoot,
9
+ parseArgs,
10
+ printError,
11
+ printSkip,
12
+ printStep,
13
+ printSuccess,
14
+ reportWriteResult,
15
+ translateParseError,
16
+ writeFileWithCheck
17
+ };
@@ -0,0 +1,15 @@
1
+ export interface FlagDefinition {
2
+ long: string;
3
+ type: 'boolean' | 'string';
4
+ short?: string;
5
+ }
6
+ export type FlagSchema = Record<string, FlagDefinition>;
7
+ export type ParsedFlags<S extends FlagSchema> = {
8
+ [K in keyof S]: S[K]['type'] extends 'boolean' ? boolean : string | undefined;
9
+ };
10
+ export interface ParsedArgs<S extends FlagSchema> {
11
+ flags: ParsedFlags<S>;
12
+ positionals: string[];
13
+ }
14
+ export declare function parseArgs<S extends FlagSchema>(argv: string[], schema: S): ParsedArgs<S>;
15
+ export declare function translateParseError(error: unknown): string;
@@ -0,0 +1,93 @@
1
+ function handleEqualsForm(arg, eqIndex, longToKey, definitions) {
2
+ const longFlag = arg.slice(0, eqIndex);
3
+ const key = longToKey.get(longFlag);
4
+ if (key === void 0) {
5
+ throw new Error(`unknown flag '${longFlag}'`);
6
+ }
7
+ const def = definitions.get(key);
8
+ if (def?.type === "boolean") {
9
+ throw new Error(`flag '${longFlag}' does not accept a value`);
10
+ }
11
+ const value = arg.slice(eqIndex + 1);
12
+ if (value === "") {
13
+ throw new Error(`${longFlag} requires a value`);
14
+ }
15
+ return { key, value };
16
+ }
17
+ function handleBareFlag(arg, index, argv, longToKey, shortToKey, definitions) {
18
+ const key = longToKey.get(arg) ?? shortToKey.get(arg);
19
+ if (key === void 0) {
20
+ throw new Error(`unknown flag '${arg}'`);
21
+ }
22
+ const def = definitions.get(key);
23
+ if (def?.type === "boolean") {
24
+ return { key, value: true, advance: 0 };
25
+ }
26
+ const next = argv[index + 1];
27
+ if (next === void 0 || next.startsWith("-") && next !== "-") {
28
+ throw new Error(`${def?.long} requires a value`);
29
+ }
30
+ return { key, value: next, advance: 1 };
31
+ }
32
+ function buildLookupTables(schema) {
33
+ const longToKey = /* @__PURE__ */ new Map();
34
+ const shortToKey = /* @__PURE__ */ new Map();
35
+ const definitions = /* @__PURE__ */ new Map();
36
+ for (const [key, def] of Object.entries(schema)) {
37
+ longToKey.set(def.long, key);
38
+ definitions.set(key, def);
39
+ if (def.short !== void 0) {
40
+ shortToKey.set(def.short, key);
41
+ }
42
+ }
43
+ return { longToKey, shortToKey, definitions };
44
+ }
45
+ function parseArgsInternal(argv, schema) {
46
+ const { longToKey, shortToKey, definitions } = buildLookupTables(schema);
47
+ const flags = {};
48
+ for (const [key, def] of Object.entries(schema)) {
49
+ flags[key] = def.type === "boolean" ? false : void 0;
50
+ }
51
+ const positionals = [];
52
+ let pastDelimiter = false;
53
+ for (let i = 0; i < argv.length; i++) {
54
+ const arg = argv[i] ?? "";
55
+ if (pastDelimiter) {
56
+ positionals.push(arg);
57
+ continue;
58
+ }
59
+ if (arg === "--") {
60
+ pastDelimiter = true;
61
+ continue;
62
+ }
63
+ const eqIndex = arg.indexOf("=");
64
+ if (eqIndex !== -1 && arg.startsWith("--")) {
65
+ const { key, value } = handleEqualsForm(arg, eqIndex, longToKey, definitions);
66
+ flags[key] = value;
67
+ continue;
68
+ }
69
+ if (arg.startsWith("-") && arg !== "-") {
70
+ const { key, value, advance } = handleBareFlag(arg, i, argv, longToKey, shortToKey, definitions);
71
+ flags[key] = value;
72
+ i += advance;
73
+ continue;
74
+ }
75
+ positionals.push(arg);
76
+ }
77
+ return { flags, positionals };
78
+ }
79
+ function parseArgs(argv, schema) {
80
+ return parseArgsInternal(argv, schema);
81
+ }
82
+ function translateParseError(error) {
83
+ const message = error instanceof Error ? error.message : String(error);
84
+ const flagMatch = message.match(/^unknown flag '(.+)'$/);
85
+ if (flagMatch?.[1] !== void 0) {
86
+ return `Unknown option: ${flagMatch[1]}`;
87
+ }
88
+ return message;
89
+ }
90
+ export {
91
+ parseArgs,
92
+ translateParseError
93
+ };
@@ -0,0 +1,5 @@
1
+ export type ScriptValue = string | string[];
2
+ export type ScriptRegistry = Record<string, ScriptValue>;
3
+ declare function getDefaultWorkspaceScripts(useIntTests: boolean): ScriptRegistry;
4
+ declare function getDefaultRootScripts(): ScriptRegistry;
5
+ export { getDefaultRootScripts, getDefaultWorkspaceScripts };
@@ -0,0 +1,67 @@
1
+ function getDefaultWorkspaceScripts(useIntTests) {
2
+ const commonScripts = {
3
+ build: ["compile", "generate-typings"],
4
+ check: ["typecheck", "fmt:check", "lint:check", "test"],
5
+ "check:strict": ["typecheck", "fmt:check", "lint:strict", "test:coverage"],
6
+ clean: "pnpm exec rimraf dist/*",
7
+ compile: "tsx ../../config/build.ts",
8
+ fmt: "prettier --list-different --write .",
9
+ "fmt:check": "prettier --check .",
10
+ "generate-typings": "tsc --project tsconfig.generate-typings.json",
11
+ lint: "eslint --fix .",
12
+ "lint:check": "eslint .",
13
+ "lint:strict": "strict-lint",
14
+ typecheck: "tsgo --noEmit",
15
+ "view-coverage": "open coverage/index.html"
16
+ };
17
+ const integrationTestOverrides = {
18
+ test: "pnpm exec vitest --config=vitest.standalone.config.ts",
19
+ "test:coverage": "pnpm exec vitest --config=vitest.standalone.config.ts --coverage",
20
+ "test:integration": "pnpm exec vitest --config=vitest.integration.config.ts",
21
+ "test:watch": "pnpm exec vitest --config=vitest.standalone.config.ts --watch"
22
+ };
23
+ const standardTestOverrides = {
24
+ test: "pnpm exec vitest",
25
+ "test:coverage": "pnpm exec vitest --coverage",
26
+ "test:watch": "pnpm exec vitest --watch"
27
+ };
28
+ return {
29
+ ...commonScripts,
30
+ ...useIntTests ? integrationTestOverrides : standardTestOverrides
31
+ };
32
+ }
33
+ function getDefaultRootScripts() {
34
+ return {
35
+ audit: ["audit:prod", "audit:dev"],
36
+ "audit:dev": "pnpm dlx audit-ci@^6 --config .audit-ci/config.dev.json5",
37
+ "audit:prod": "pnpm dlx audit-ci@^6 --config .audit-ci/config.prod.json5",
38
+ build: "pnpm --recursive exec nmr build",
39
+ check: ["typecheck", "fmt:check", "lint:check", "test"],
40
+ "check:strict": ["typecheck", "fmt:check", "lint:strict", "test:coverage", "audit"],
41
+ ci: ["check:strict", "build"],
42
+ fmt: `sh -c 'prettier --list-different --write "\${@:-.}"' --`,
43
+ "fmt:check": `sh -c 'prettier --check "\${@:-.}"' --`,
44
+ lint: "nmr root:lint && pnpm --recursive exec nmr lint",
45
+ "lint:check": "nmr root:lint:check && pnpm --recursive exec nmr lint:check",
46
+ "lint:strict": "nmr root:lint:strict && pnpm --recursive exec nmr lint:strict",
47
+ outdated: "pnpm outdated --compatible --recursive",
48
+ "outdated:latest": "pnpm outdated --recursive",
49
+ "report-overrides": "nmr-report-overrides",
50
+ "root:lint": "eslint --fix --ignore-pattern 'packages/**' .",
51
+ "root:lint:check": "eslint --ignore-pattern 'packages/**' .",
52
+ "root:lint:strict": "strict-lint --ignore-pattern 'packages/**' .",
53
+ "root:test": "vitest --config ./vitest.root.config.ts",
54
+ "root:typecheck": "tsgo --noEmit",
55
+ "sync-pnpm-version": "nmr-sync-pnpm-version",
56
+ test: "nmr root:test && pnpm --recursive exec nmr test",
57
+ "test:coverage": "nmr root:test && pnpm --recursive exec nmr test:coverage",
58
+ "test:watch": "vitest --watch",
59
+ typecheck: "nmr root:typecheck && pnpm --recursive exec nmr typecheck",
60
+ update: "pnpm update --recursive",
61
+ "update:latest": "pnpm update --latest --recursive"
62
+ };
63
+ }
64
+ export {
65
+ getDefaultRootScripts,
66
+ getDefaultWorkspaceScripts
67
+ };
@@ -0,0 +1,11 @@
1
+ import type { NmrConfig } from './config.js';
2
+ import type { ScriptRegistry, ScriptValue } from './registries.js';
3
+ export interface ResolvedScript {
4
+ command: string;
5
+ source: 'default' | 'package';
6
+ }
7
+ export declare function expandScript(script: ScriptValue): string;
8
+ export declare function describeScript(script: ScriptValue): string;
9
+ export declare function buildWorkspaceRegistry(config: NmrConfig, useIntTests: boolean): ScriptRegistry;
10
+ export declare function buildRootRegistry(config: NmrConfig): ScriptRegistry;
11
+ export declare function resolveScript(commandName: string, registry: ScriptRegistry, packageDir?: string): ResolvedScript | undefined;
@@ -0,0 +1,64 @@
1
+ import { readFileSync } from "node:fs";
2
+ import path from "node:path";
3
+ import { isObject } from "./helpers/type-guards.js";
4
+ import { getDefaultRootScripts, getDefaultWorkspaceScripts } from "./registries.js";
5
+ function expandScript(script) {
6
+ if (typeof script === "string") {
7
+ return script;
8
+ }
9
+ return script.map((s) => `nmr ${s}`).join(" && ");
10
+ }
11
+ function describeScript(script) {
12
+ return typeof script === "string" ? script : `[${script.join(", ")}]`;
13
+ }
14
+ function readPackageJsonScripts(packageDir) {
15
+ try {
16
+ const raw = readFileSync(path.join(packageDir, "package.json"), "utf8");
17
+ const parsed = JSON.parse(raw);
18
+ if (!isObject(parsed)) return void 0;
19
+ const scripts = parsed.scripts;
20
+ if (!isObject(scripts)) return void 0;
21
+ const result = {};
22
+ for (const [key, val] of Object.entries(scripts)) {
23
+ if (typeof val === "string") result[key] = val;
24
+ }
25
+ return result;
26
+ } catch {
27
+ return void 0;
28
+ }
29
+ }
30
+ function buildWorkspaceRegistry(config, useIntTests) {
31
+ return {
32
+ ...getDefaultWorkspaceScripts(useIntTests),
33
+ ...config.workspaceScripts
34
+ };
35
+ }
36
+ function buildRootRegistry(config) {
37
+ return {
38
+ ...getDefaultRootScripts(),
39
+ ...config.rootScripts
40
+ };
41
+ }
42
+ function resolveScript(commandName, registry, packageDir) {
43
+ if (packageDir) {
44
+ const pkgScripts = readPackageJsonScripts(packageDir);
45
+ if (pkgScripts && commandName in pkgScripts) {
46
+ const override = pkgScripts[commandName];
47
+ if (override !== void 0) {
48
+ return { command: override, source: "package" };
49
+ }
50
+ }
51
+ }
52
+ const registryEntry = registry[commandName];
53
+ if (registryEntry === void 0) {
54
+ return void 0;
55
+ }
56
+ return { command: expandScript(registryEntry), source: "default" };
57
+ }
58
+ export {
59
+ buildRootRegistry,
60
+ buildWorkspaceRegistry,
61
+ describeScript,
62
+ expandScript,
63
+ resolveScript
64
+ };
@@ -0,0 +1,4 @@
1
+ export interface RunCommandOptions {
2
+ quiet?: boolean;
3
+ }
4
+ export declare function runCommand(command: string, cwd?: string, options?: RunCommandOptions): number;
@@ -0,0 +1,38 @@
1
+ import { execSync } from "node:child_process";
2
+ import process from "node:process";
3
+ function runCommand(command, cwd, options) {
4
+ const quiet = options?.quiet === true;
5
+ const stdio = quiet ? "pipe" : "inherit";
6
+ try {
7
+ execSync(command, { stdio, cwd });
8
+ return 0;
9
+ } catch (error) {
10
+ if (error !== null && typeof error === "object") {
11
+ if (quiet) {
12
+ writeErrorOutput(error);
13
+ }
14
+ if ("status" in error) {
15
+ const { status } = error;
16
+ return typeof status === "number" ? status : 1;
17
+ }
18
+ }
19
+ return 1;
20
+ }
21
+ }
22
+ function writeErrorOutput(error) {
23
+ if ("stdout" in error) {
24
+ const { stdout } = error;
25
+ if (Buffer.isBuffer(stdout) && stdout.length > 0) {
26
+ process.stderr.write(stdout);
27
+ }
28
+ }
29
+ if ("stderr" in error) {
30
+ const { stderr } = error;
31
+ if (Buffer.isBuffer(stderr) && stderr.length > 0) {
32
+ process.stderr.write(stderr);
33
+ }
34
+ }
35
+ }
36
+ export {
37
+ runCommand
38
+ };
@@ -0,0 +1,6 @@
1
+ import type { WriteResult } from './writeFileWithCheck.ts';
2
+ export declare function printStep(message: string): void;
3
+ export declare function printSuccess(message: string): void;
4
+ export declare function printSkip(message: string): void;
5
+ export declare function printError(message: string): void;
6
+ export declare function reportWriteResult(result: WriteResult, dryRun: boolean): void;
@@ -0,0 +1,55 @@
1
+ function printStep(message) {
2
+ console.info(`
3
+ > ${message}`);
4
+ }
5
+ function printSuccess(message) {
6
+ console.info(` \u2705 ${message}`);
7
+ }
8
+ function printSkip(message) {
9
+ console.info(` \u26A0\uFE0F ${message}`);
10
+ }
11
+ function printError(message) {
12
+ console.error(` \u274C ${message}`);
13
+ }
14
+ function reportWriteResult(result, dryRun) {
15
+ switch (result.outcome) {
16
+ case "created":
17
+ if (dryRun) {
18
+ printSuccess(`[dry-run] Would create ${result.filePath}`);
19
+ } else {
20
+ printSuccess(`Created ${result.filePath}`);
21
+ }
22
+ break;
23
+ case "overwritten":
24
+ if (dryRun) {
25
+ printSuccess(`[dry-run] Would overwrite ${result.filePath}`);
26
+ } else {
27
+ printSuccess(`Overwrote ${result.filePath}`);
28
+ }
29
+ break;
30
+ case "up-to-date":
31
+ printSuccess(`${result.filePath} (up to date)`);
32
+ break;
33
+ case "skipped":
34
+ if (result.error) {
35
+ printSkip(`${result.filePath} (could not read for comparison: ${result.error})`);
36
+ } else {
37
+ printSkip(`${result.filePath} (already exists)`);
38
+ }
39
+ break;
40
+ case "failed":
41
+ if (result.error) {
42
+ printError(`Failed to write ${result.filePath}: ${result.error}`);
43
+ } else {
44
+ printError(`Failed to write ${result.filePath}`);
45
+ }
46
+ break;
47
+ }
48
+ }
49
+ export {
50
+ printError,
51
+ printSkip,
52
+ printStep,
53
+ printSuccess,
54
+ reportWriteResult
55
+ };
@@ -0,0 +1 @@
1
+ export declare function runConsistencyChecks(): void;
@@ -0,0 +1,70 @@
1
+ import assert from "node:assert";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import yaml from "js-yaml";
5
+ import { describe, expect, it } from "vitest";
6
+ import { findMonorepoRoot } from "../context.js";
7
+ import { getRuntimeVersionFromAsdf } from "./helpers/get-runtime-version-from-asdf.js";
8
+ import { getValueAtPathOrThrow } from "./helpers/get-value-at-path.js";
9
+ const GITHUB_ACTION_FILE_PATH = ".github/workflows/code-quality.yaml";
10
+ function checkPnpmVersionConsistency(monorepoRoot) {
11
+ describe("pnpm version consistency", () => {
12
+ it("pnpm version is the same in GitHub action and package.json", async () => {
13
+ const actionVersion = await getPnpmVersionFromAction(monorepoRoot);
14
+ const packageJsonVersion = getPnpmVersionFromPackageJson(monorepoRoot);
15
+ expect(actionVersion).toBe(packageJsonVersion);
16
+ });
17
+ });
18
+ }
19
+ function checkNodeVersionConsistency(monorepoRoot) {
20
+ describe("Node.js version consistency", () => {
21
+ it("version is the same in GitHub action and .tool-versions", async () => {
22
+ const toolVersion = await getRuntimeVersionFromAsdf("nodejs", monorepoRoot);
23
+ const actionVersion = await getNodeVersionFromAction(monorepoRoot);
24
+ expect(toolVersion).toBe(actionVersion);
25
+ });
26
+ });
27
+ }
28
+ async function getPnpmVersionFromAction(monorepoRoot) {
29
+ const actionPath = path.join(monorepoRoot, GITHUB_ACTION_FILE_PATH);
30
+ const actionYaml = await fs.promises.readFile(actionPath, { encoding: "utf8" });
31
+ const action = yaml.load(actionYaml);
32
+ assert.ok(action, "Action YAML not found");
33
+ const version = getValueAtPathOrThrow(action, "jobs.code-quality.with.pnpm-version");
34
+ assert.ok(typeof version === "string" && version.length > 0, "pnpm version not found in action");
35
+ return version;
36
+ }
37
+ function getPnpmVersionFromPackageJson(monorepoRoot) {
38
+ const pkgPath = path.join(monorepoRoot, "package.json");
39
+ const raw = fs.readFileSync(pkgPath, "utf8");
40
+ const pkg = JSON.parse(raw);
41
+ const pm = getValueAtPathOrThrow(pkg, "packageManager");
42
+ if (typeof pm !== "string") {
43
+ throw new TypeError('"packageManager" field missing or not a string in package.json.');
44
+ }
45
+ const [name, version] = pm.split("@");
46
+ if (name !== "pnpm") {
47
+ throw new Error("packageManager is not pnpm.");
48
+ }
49
+ if (!version) {
50
+ throw new Error("pnpm version missing in package.json.");
51
+ }
52
+ return version;
53
+ }
54
+ async function getNodeVersionFromAction(monorepoRoot) {
55
+ const actionPath = path.join(monorepoRoot, GITHUB_ACTION_FILE_PATH);
56
+ const actionYaml = await fs.promises.readFile(actionPath, { encoding: "utf8" });
57
+ const action = yaml.load(actionYaml);
58
+ assert.ok(action, "Action YAML not found");
59
+ const version = getValueAtPathOrThrow(action, "jobs.code-quality.with.node-version");
60
+ assert.ok(typeof version === "string" && version.length > 0, "Node.js version not found in action");
61
+ return version;
62
+ }
63
+ function runConsistencyChecks() {
64
+ const monorepoRoot = findMonorepoRoot();
65
+ checkPnpmVersionConsistency(monorepoRoot);
66
+ checkNodeVersionConsistency(monorepoRoot);
67
+ }
68
+ export {
69
+ runConsistencyChecks
70
+ };
@@ -0,0 +1 @@
1
+ export declare function getRuntimeVersionFromAsdf(runtime: string, monorepoRoot: string): Promise<string>;
@@ -0,0 +1,15 @@
1
+ import assert from "node:assert";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ async function getRuntimeVersionFromAsdf(runtime, monorepoRoot) {
5
+ const toolVersionsPath = path.join(monorepoRoot, ".tool-versions");
6
+ const toolVersions = await fs.promises.readFile(toolVersionsPath, { encoding: "utf8" });
7
+ const versionLine = toolVersions.split("\n").find((line) => line.trim().startsWith(runtime));
8
+ assert.ok(versionLine, `${runtime} not found in .tool-versions`);
9
+ const [, version] = versionLine.trim().split(/\s+/);
10
+ assert.ok(version, `${runtime} version missing in .tool-versions`);
11
+ return version;
12
+ }
13
+ export {
14
+ getRuntimeVersionFromAsdf
15
+ };
@@ -0,0 +1 @@
1
+ export declare function getValueAtPathOrThrow(obj: unknown, objPath: string): unknown;
@@ -0,0 +1,34 @@
1
+ function isObject(value) {
2
+ return typeof value === "object" && value !== null && !Array.isArray(value);
3
+ }
4
+ function getValueAtPathOrThrow(obj, objPath) {
5
+ if (!isObject(obj)) {
6
+ throw new Error("Expected an object as root value.");
7
+ }
8
+ const keys = objPath.split(".");
9
+ let current = obj;
10
+ for (const key of keys) {
11
+ if (Array.isArray(current)) {
12
+ const index = Number(key);
13
+ if (Number.isNaN(index)) {
14
+ throw new TypeError(`Expected array index at segment "${key}" in path "${objPath}"`);
15
+ }
16
+ if (index >= 0 && index < current.length) {
17
+ current = current[index];
18
+ } else {
19
+ throw new Error(`Array index out of bounds: "${key}" in path "${objPath}"`);
20
+ }
21
+ } else if (isObject(current)) {
22
+ if (!(key in current)) {
23
+ throw new Error(`Missing key "${key}" in path "${objPath}"`);
24
+ }
25
+ current = current[key];
26
+ } else {
27
+ throw new Error(`Unexpected non-object/non-array at segment "${key}" in path "${objPath}"`);
28
+ }
29
+ }
30
+ return current;
31
+ }
32
+ export {
33
+ getValueAtPathOrThrow
34
+ };
@@ -0,0 +1,10 @@
1
+ export type WriteOutcome = 'created' | 'overwritten' | 'up-to-date' | 'skipped' | 'failed';
2
+ export interface WriteResult {
3
+ filePath: string;
4
+ outcome: WriteOutcome;
5
+ error?: string;
6
+ }
7
+ export declare function writeFileWithCheck(filePath: string, content: string, options: {
8
+ dryRun: boolean;
9
+ overwrite: boolean;
10
+ }): WriteResult;
@@ -0,0 +1,41 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { dirname } from "node:path";
3
+ function normalizeTrailingWhitespace(content) {
4
+ return content.split("\n").map((line) => line.trimEnd()).join("\n").trimEnd();
5
+ }
6
+ function writeFileWithCheck(filePath, content, options) {
7
+ const { dryRun, overwrite } = options;
8
+ const fileExists = existsSync(filePath);
9
+ if (fileExists && !overwrite) {
10
+ try {
11
+ const existing = readFileSync(filePath, "utf8");
12
+ if (normalizeTrailingWhitespace(existing) === normalizeTrailingWhitespace(content)) {
13
+ return { filePath, outcome: "up-to-date" };
14
+ }
15
+ } catch (error) {
16
+ const message = error instanceof Error ? error.message : String(error);
17
+ return { filePath, outcome: "skipped", error: message };
18
+ }
19
+ return { filePath, outcome: "skipped" };
20
+ }
21
+ const outcome = fileExists ? "overwritten" : "created";
22
+ if (dryRun) {
23
+ return { filePath, outcome };
24
+ }
25
+ try {
26
+ mkdirSync(dirname(filePath), { recursive: true });
27
+ } catch (error) {
28
+ const message = error instanceof Error ? error.message : String(error);
29
+ return { filePath, outcome: "failed", error: message };
30
+ }
31
+ try {
32
+ writeFileSync(filePath, content, "utf8");
33
+ } catch (error) {
34
+ const message = error instanceof Error ? error.message : String(error);
35
+ return { filePath, outcome: "failed", error: message };
36
+ }
37
+ return { filePath, outcome };
38
+ }
39
+ export {
40
+ writeFileWithCheck
41
+ };
package/package.json ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "@williamthorsen/nmr-core",
3
+ "version": "0.3.0",
4
+ "private": false,
5
+ "description": "Shared utilities for node-monorepo-tools packages",
6
+ "keywords": [
7
+ "monorepo",
8
+ "pnpm",
9
+ "utilities"
10
+ ],
11
+ "homepage": "https://github.com/williamthorsen/node-monorepo-tools/tree/main/packages/core#readme",
12
+ "bugs": {
13
+ "url": "https://github.com/williamthorsen/node-monorepo-tools/issues"
14
+ },
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "https://github.com/williamthorsen/node-monorepo-tools.git",
18
+ "directory": "packages/core"
19
+ },
20
+ "license": "ISC",
21
+ "author": "William Thorsen <william@thorsen.dev> (https://github.com/williamthorsen)",
22
+ "type": "module",
23
+ "exports": {
24
+ ".": "./dist/esm/index.js"
25
+ },
26
+ "files": [
27
+ "dist"
28
+ ],
29
+ "publishConfig": {
30
+ "access": "public"
31
+ },
32
+ "scripts": {}
33
+ }