hudson 1.0.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.
@@ -0,0 +1,114 @@
1
+ import chalk from "chalk";
2
+ import chokidar from "chokidar";
3
+ import { loadConfig } from "../core/config.js";
4
+ import { runCommand } from "../core/runner.js";
5
+ import { log, BANNER, formatDuration } from "../utils/display.js";
6
+ import ora from "ora";
7
+
8
+ interface DevOptions {
9
+ verbose?: boolean;
10
+ }
11
+
12
+ export async function devCommand(options: DevOptions = {}): Promise<void> {
13
+ const cwd = process.cwd();
14
+ const config = loadConfig(cwd);
15
+ const verbose = options.verbose ?? config.output.verbose;
16
+
17
+ if (config.output.banner) {
18
+ console.log(BANNER());
19
+ }
20
+
21
+ log.section("Dev Mode");
22
+ log.step(`Watching ${chalk.cyan("src/**/*.ts")} for changes...`);
23
+ log.dim("Press Ctrl+C to stop");
24
+ log.blank();
25
+
26
+ let building = false;
27
+ let queued = false;
28
+
29
+ async function build() {
30
+ if (building) {
31
+ queued = true;
32
+ return;
33
+ }
34
+
35
+ building = true;
36
+ const start = Date.now();
37
+
38
+ const spinner = ora({
39
+ text: chalk.dim("Compiling..."),
40
+ prefixText: " ",
41
+ }).start();
42
+
43
+ const result = await runCommand("tsc", ["-p", config.build.tsconfig, "--noEmitOnError"], {
44
+ cwd,
45
+ verbose,
46
+ });
47
+
48
+ const duration = Date.now() - start;
49
+
50
+ if (result.exitCode === 0) {
51
+ const time = new Date().toLocaleTimeString("en-US", { hour12: false });
52
+ spinner.succeed(
53
+ `${chalk.dim(time)} Compiled ${chalk.dim("in")} ${chalk.cyan(formatDuration(duration))}`
54
+ );
55
+ } else {
56
+ const time = new Date().toLocaleTimeString("en-US", { hour12: false });
57
+ spinner.fail(`${chalk.dim(time)} ${chalk.red("Compile error")}`);
58
+
59
+ if (result.stderr) {
60
+ log.blank();
61
+ result.stderr
62
+ .split("\n")
63
+ .filter(Boolean)
64
+ .slice(0, 10)
65
+ .forEach((l) => {
66
+ console.log(` ${chalk.red("·")} ${chalk.dim(l)}`);
67
+ });
68
+ log.blank();
69
+ }
70
+ }
71
+
72
+ building = false;
73
+
74
+ if (queued) {
75
+ queued = false;
76
+ await build();
77
+ }
78
+ }
79
+
80
+ // Initial build
81
+ await build();
82
+
83
+ // Watch for changes
84
+ const watcher = chokidar.watch("src/**/*.ts", {
85
+ cwd,
86
+ ignoreInitial: true,
87
+ persistent: true,
88
+ });
89
+
90
+ watcher.on("change", (file) => {
91
+ log.step(`Changed: ${chalk.cyan(file)}`);
92
+ build();
93
+ });
94
+
95
+ watcher.on("add", (file) => {
96
+ log.step(`Added: ${chalk.cyan(file)}`);
97
+ build();
98
+ });
99
+
100
+ watcher.on("unlink", (file) => {
101
+ log.step(`Removed: ${chalk.cyan(file)}`);
102
+ build();
103
+ });
104
+
105
+ process.on("SIGINT", () => {
106
+ log.blank();
107
+ log.info("Stopping dev mode...");
108
+ watcher.close();
109
+ process.exit(0);
110
+ });
111
+
112
+ // Keep alive
113
+ await new Promise(() => {});
114
+ }
@@ -0,0 +1,78 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import chalk from "chalk";
4
+ import { loadConfig, getConfigPath, getHudsonDir } from "../core/config.js";
5
+ import { discoverWorkspaces } from "../core/workspace.js";
6
+ import { log, MASCOT_FULL, BANNER } from "../utils/display.js";
7
+
8
+ export async function infoCommand(): Promise<void> {
9
+ const cwd = process.cwd();
10
+ const config = loadConfig(cwd);
11
+
12
+ console.log(BANNER());
13
+ console.log(MASCOT_FULL);
14
+
15
+ log.section("Project");
16
+ console.log(` ${chalk.dim("Name")} ${chalk.white(config.project.name)}`);
17
+ console.log(` ${chalk.dim("Version")} ${chalk.cyan(config.project.version)}`);
18
+ if (config.project.description) {
19
+ console.log(` ${chalk.dim("Description")} ${chalk.dim(config.project.description)}`);
20
+ }
21
+ console.log(` ${chalk.dim("Pkg Manager")} ${chalk.cyan(config.packageManager)}`);
22
+
23
+ log.section("Build");
24
+ console.log(` ${chalk.dim("Output Dir")} ${chalk.cyan(config.build.outDir)}`);
25
+ console.log(` ${chalk.dim("tsconfig")} ${chalk.cyan(config.build.tsconfig)}`);
26
+ console.log(` ${chalk.dim("Target")} ${chalk.cyan(config.build.target)}`);
27
+ console.log(` ${chalk.dim("Sourcemap")} ${boolDisplay(config.build.sourcemap)}`);
28
+ console.log(` ${chalk.dim("Minify")} ${boolDisplay(config.build.minify)}`);
29
+ console.log(` ${chalk.dim("Clean")} ${boolDisplay(config.build.clean)}`);
30
+
31
+ if (config.workspaces.enabled) {
32
+ log.section("Workspaces");
33
+ const workspaces = await discoverWorkspaces(config, cwd);
34
+ console.log(` ${chalk.dim("Packages")} ${chalk.cyan(workspaces.length)} found`);
35
+ console.log(` ${chalk.dim("Parallel")} ${boolDisplay(config.workspaces.parallel)}`);
36
+ console.log(` ${chalk.dim("Concurrency")} ${chalk.cyan(config.workspaces.maxConcurrency)}`);
37
+
38
+ if (workspaces.length > 0) {
39
+ log.blank();
40
+ workspaces.forEach((ws) => {
41
+ const hasTs = fs.existsSync(path.join(ws.path, "tsconfig.json"));
42
+ console.log(
43
+ ` ${chalk.dim("·")} ${chalk.cyan(ws.name)} ${chalk.dim("v" + ws.version)} ${hasTs ? chalk.green("[ts]") : ""}`
44
+ );
45
+ });
46
+ }
47
+ }
48
+
49
+ if (Object.keys(config.scripts).length > 0) {
50
+ log.section("Scripts");
51
+ for (const [name, script] of Object.entries(config.scripts)) {
52
+ console.log(` ${chalk.cyan(name.padEnd(12))} ${chalk.dim(script)}`);
53
+ }
54
+ }
55
+
56
+ if (Object.values(config.hooks).some(Boolean)) {
57
+ log.section("Hooks");
58
+ for (const [name, hook] of Object.entries(config.hooks)) {
59
+ if (hook) {
60
+ console.log(` ${chalk.cyan(name.padEnd(12))} ${chalk.dim(hook)}`);
61
+ }
62
+ }
63
+ }
64
+
65
+ log.section("Output Settings");
66
+ console.log(` ${chalk.dim("Verbose")} ${boolDisplay(config.output.verbose)}`);
67
+ console.log(` ${chalk.dim("Timestamps")} ${boolDisplay(config.output.timestamps)}`);
68
+ console.log(` ${chalk.dim("Color")} ${boolDisplay(config.output.color)}`);
69
+ console.log(` ${chalk.dim("Banner")} ${boolDisplay(config.output.banner)}`);
70
+
71
+ log.blank();
72
+ log.dim(`Config: ${chalk.cyan(getConfigPath(cwd))}`);
73
+ log.blank();
74
+ }
75
+
76
+ function boolDisplay(val: boolean): string {
77
+ return val ? chalk.green("true") : chalk.red("false");
78
+ }
@@ -0,0 +1,80 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import { saveConfig, DEFAULT_CONFIG, ensureHudsonDirs, getConfigPath } from "../core/config.js";
4
+ import { log, BANNER, MASCOT_FULL, symbols } from "../utils/display.js";
5
+ import chalk from "chalk";
6
+
7
+ export async function initCommand(options: { force?: boolean } = {}): Promise<void> {
8
+ const cwd = process.cwd();
9
+ const configPath = getConfigPath(cwd);
10
+
11
+ if (process.stdout.isTTY) {
12
+ console.log(BANNER());
13
+ console.log(MASCOT_FULL);
14
+ }
15
+
16
+ if (fs.existsSync(configPath) && !options.force) {
17
+ log.warn("Hudson is already initialized in this directory.");
18
+ log.dim(`Config: ${chalk.cyan(configPath)}`);
19
+ log.dim(`Use ${chalk.cyan("--force")} to reinitialize.`);
20
+ return;
21
+ }
22
+
23
+ let projectName = path.basename(cwd);
24
+ let packageManager: "pnpm" | "yarn" | "npm" = "pnpm";
25
+
26
+ const pkgJsonPath = path.join(cwd, "package.json");
27
+ if (fs.existsSync(pkgJsonPath)) {
28
+ try {
29
+ const pkg = JSON.parse(fs.readFileSync(pkgJsonPath, "utf-8"));
30
+ if (pkg.name) projectName = pkg.name;
31
+ } catch {}
32
+ }
33
+
34
+ // Detect package manager
35
+ if (fs.existsSync(path.join(cwd, "pnpm-lock.yaml"))) packageManager = "pnpm";
36
+ else if (fs.existsSync(path.join(cwd, "yarn.lock"))) packageManager = "yarn";
37
+
38
+ ensureHudsonDirs(cwd);
39
+
40
+ const config = {
41
+ ...DEFAULT_CONFIG,
42
+ project: {
43
+ ...DEFAULT_CONFIG.project,
44
+ name: projectName,
45
+ },
46
+ packageManager,
47
+ workspaces: {
48
+ ...DEFAULT_CONFIG.workspaces,
49
+ enabled: fs.existsSync(path.join(cwd, "packages")) || fs.existsSync(path.join(cwd, "apps")),
50
+ },
51
+ };
52
+
53
+ saveConfig(config, cwd);
54
+
55
+ // Create .gitignore entry for .hudson/temp
56
+ const gitignorePath = path.join(cwd, ".gitignore");
57
+ const hudsonGitignore = "\n# Hudson Build System\n.hudson/temp/\n";
58
+ if (fs.existsSync(gitignorePath)) {
59
+ const content = fs.readFileSync(gitignorePath, "utf-8");
60
+ if (!content.includes(".hudson/temp")) {
61
+ fs.appendFileSync(gitignorePath, hudsonGitignore);
62
+ }
63
+ }
64
+
65
+ log.blank();
66
+ log.success(chalk.bold("Hudson initialized successfully!"));
67
+ log.blank();
68
+
69
+ console.log(` ${chalk.dim("Project")} ${chalk.cyan(projectName)}`);
70
+ console.log(` ${chalk.dim("Config")} ${chalk.cyan(".hudson/hudson.yml")}`);
71
+ console.log(` ${chalk.dim("Pkg manager")} ${chalk.cyan(packageManager)}`);
72
+ console.log(` ${chalk.dim("Workspaces")} ${chalk.cyan(config.workspaces.enabled ? "enabled" : "disabled")}`);
73
+
74
+ log.blank();
75
+ log.section("Next steps");
76
+ log.step(`Edit your config: ${chalk.cyan(".hudson/hudson.yml")}`);
77
+ log.step(`Or configure via: ${chalk.cyan("hudson configure build.outDir dist")}`);
78
+ log.step(`Build your project: ${chalk.cyan("hudson build")}`);
79
+ log.blank();
80
+ }
@@ -0,0 +1,114 @@
1
+ import ora from "ora";
2
+ import chalk from "chalk";
3
+ import { loadConfig } from "../core/config.js";
4
+ import { runScript, runCommand } from "../core/runner.js";
5
+ import { discoverWorkspaces } from "../core/workspace.js";
6
+ import { log, formatDuration, BANNER } from "../utils/display.js";
7
+
8
+ interface RunOptions {
9
+ workspace?: string;
10
+ all?: boolean;
11
+ verbose?: boolean;
12
+ }
13
+
14
+ export async function runScriptCommand(scriptName: string, options: RunOptions = {}): Promise<void> {
15
+ const cwd = process.cwd();
16
+ const config = loadConfig(cwd);
17
+ const verbose = options.verbose ?? config.output.verbose;
18
+
19
+ if (config.output.banner) {
20
+ console.log(BANNER());
21
+ }
22
+
23
+ log.section(`Script: ${scriptName}`);
24
+
25
+ const start = Date.now();
26
+
27
+ if (options.all && config.workspaces.enabled) {
28
+ const workspaces = await discoverWorkspaces(config, cwd);
29
+
30
+ if (workspaces.length === 0) {
31
+ log.warn("No workspaces found");
32
+ return;
33
+ }
34
+
35
+ log.step(`Running ${chalk.cyan(scriptName)} in ${chalk.cyan(workspaces.length)} workspaces`);
36
+ log.blank();
37
+
38
+ const promises = workspaces.map(async (ws) => {
39
+ if (!ws.scripts[scriptName]) {
40
+ log.dim(` ${chalk.dim(ws.name)} — no ${scriptName} script`);
41
+ return;
42
+ }
43
+
44
+ const spinner = ora({
45
+ text: chalk.dim(`${ws.name}...`),
46
+ prefixText: " ",
47
+ }).start();
48
+
49
+ const parts = ws.scripts[scriptName].split(" ");
50
+ const result = await runCommand(parts[0], parts.slice(1), {
51
+ cwd: ws.path,
52
+ verbose,
53
+ });
54
+
55
+ if (result.exitCode === 0) {
56
+ spinner.succeed(`${chalk.cyan(ws.name)} ${chalk.dim(formatDuration(result.duration))}`);
57
+ } else {
58
+ spinner.fail(`${chalk.red(ws.name)} failed`);
59
+ if (result.stderr && verbose) {
60
+ result.stderr.split("\n").filter(Boolean).forEach((l) => {
61
+ console.log(` ${chalk.dim(l)}`);
62
+ });
63
+ }
64
+ }
65
+ });
66
+
67
+ if (config.workspaces.parallel) {
68
+ const batchSize = config.workspaces.maxConcurrency;
69
+ for (let i = 0; i < promises.length; i += batchSize) {
70
+ await Promise.all(promises.slice(i, i + batchSize));
71
+ }
72
+ } else {
73
+ for (const p of promises) await p;
74
+ }
75
+
76
+ } else {
77
+ const script = config.scripts[scriptName];
78
+
79
+ if (!script) {
80
+ log.error(`Script '${scriptName}' not found in .hudson/hudson.yml`);
81
+ log.blank();
82
+
83
+ const available = Object.keys(config.scripts);
84
+ if (available.length > 0) {
85
+ log.step("Available scripts:");
86
+ available.forEach((s) => {
87
+ console.log(` ${chalk.dim("·")} ${chalk.cyan(s)} ${chalk.dim("→")} ${chalk.dim(config.scripts[s])}`);
88
+ });
89
+ }
90
+ process.exit(1);
91
+ }
92
+
93
+ log.step(`${chalk.dim("Running:")} ${chalk.white(script)}`);
94
+ log.blank();
95
+
96
+ const result = await runScript(scriptName, config, { cwd, verbose });
97
+
98
+ if (result.exitCode !== 0) {
99
+ log.error(`Script '${scriptName}' failed with exit code ${result.exitCode}`);
100
+ if (result.stderr) {
101
+ log.blank();
102
+ result.stderr.split("\n").filter(Boolean).forEach((l) => {
103
+ console.log(` ${chalk.dim(l)}`);
104
+ });
105
+ }
106
+ process.exit(1);
107
+ }
108
+ }
109
+
110
+ const duration = Date.now() - start;
111
+ log.blank();
112
+ log.success(`Done ${chalk.dim("in")} ${chalk.cyan(formatDuration(duration))}`);
113
+ log.blank();
114
+ }
@@ -0,0 +1,49 @@
1
+ import chalk from "chalk";
2
+ import { loadConfig } from "../core/config.js";
3
+ import { discoverWorkspaces } from "../core/workspace.js";
4
+ import { log, BANNER } from "../utils/display.js";
5
+
6
+ export async function workspacesCommand(subcommand?: string, wsName?: string): Promise<void> {
7
+ const cwd = process.cwd();
8
+ const config = loadConfig(cwd);
9
+
10
+ if (!config.workspaces.enabled) {
11
+ log.warn("Workspaces are disabled.");
12
+ log.dim(`Enable with: ${chalk.cyan("hudson configure workspaces.enabled true")}`);
13
+ return;
14
+ }
15
+
16
+ const workspaces = await discoverWorkspaces(config, cwd);
17
+
18
+ if (config.output.banner) {
19
+ console.log(BANNER());
20
+ }
21
+
22
+ if (!subcommand || subcommand === "list") {
23
+ log.section(`Workspaces (${workspaces.length})`);
24
+
25
+ if (workspaces.length === 0) {
26
+ log.warn("No workspaces found.");
27
+ log.dim(`Check your workspaces.packages patterns in .hudson/hudson.yml`);
28
+ return;
29
+ }
30
+
31
+ for (const ws of workspaces) {
32
+ const scripts = Object.keys(ws.scripts);
33
+ console.log();
34
+ console.log(` ${chalk.bold.cyan(ws.name)} ${chalk.dim("v" + ws.version)}`);
35
+ console.log(` ${chalk.dim("path:")} ${chalk.dim(ws.path)}`);
36
+
37
+ if (scripts.length > 0) {
38
+ console.log(` ${chalk.dim("scripts:")} ${scripts.map((s) => chalk.cyan(s)).join(chalk.dim(", "))}`);
39
+ }
40
+ }
41
+
42
+ log.blank();
43
+ return;
44
+ }
45
+
46
+ log.error(`Unknown workspaces subcommand: ${subcommand}`);
47
+ log.dim("Available: list");
48
+ process.exit(1);
49
+ }
@@ -0,0 +1,214 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import yaml from "js-yaml";
4
+
5
+ export interface WorkspaceConfig {
6
+ name: string;
7
+ path: string;
8
+ scripts?: Record<string, string>;
9
+ }
10
+
11
+ export interface HudsonConfig {
12
+ project: {
13
+ name: string;
14
+ version: string;
15
+ description?: string;
16
+ };
17
+ build: {
18
+ outDir: string;
19
+ tsconfig: string;
20
+ sourcemap: boolean;
21
+ minify: boolean;
22
+ target: string;
23
+ clean: boolean;
24
+ };
25
+ workspaces: {
26
+ enabled: boolean;
27
+ packages: string[];
28
+ parallel: boolean;
29
+ maxConcurrency: number;
30
+ };
31
+ dev: {
32
+ watch: boolean;
33
+ port?: number;
34
+ open: boolean;
35
+ };
36
+ scripts: Record<string, string>;
37
+ hooks: {
38
+ preBuild?: string;
39
+ postBuild?: string;
40
+ preTest?: string;
41
+ postTest?: string;
42
+ };
43
+ packageManager: "pnpm" | "yarn" | "npm";
44
+ output: {
45
+ verbose: boolean;
46
+ timestamps: boolean;
47
+ color: boolean;
48
+ banner: boolean;
49
+ };
50
+ }
51
+
52
+ export const CONFIG_HEADER = `#####################################################################
53
+ #
54
+ # ██╗ ██╗██╗ ██╗██████╗ ███████╗ ██████╗ ███╗ ██╗
55
+ # ██║ ██║██║ ██║██╔══██╗██╔════╝██╔═══██╗████╗ ██║
56
+ # ███████║██║ ██║██║ ██║███████╗██║ ██║██╔██╗ ██║
57
+ # ██╔══██║██║ ██║██║ ██║╚════██║██║ ██║██║╚██╗██║
58
+ # ██║ ██║╚██████╔╝██████╔╝███████║╚██████╔╝██║ ╚████║
59
+ # ╚═╝ ╚═╝ ╚═════╝ ╚═════╝ ╚══════╝ ╚═════╝ ╚═╝ ╚═══╝
60
+ #
61
+ # Hudson Build System — v1.0.0
62
+ # The blazing-fast TypeScript build system
63
+ #
64
+ # ─────────────────────────────────────────────────────
65
+ # Configure via CLI (without editing this file):
66
+ #
67
+ # hudson configure <key> <value>
68
+ #
69
+ # Examples:
70
+ # hudson configure build.outDir dist
71
+ # hudson configure output.verbose true
72
+ # hudson configure packageManager pnpm
73
+ #
74
+ # ─────────────────────────────────────────────────────
75
+ # Docs: https://github.com/hudson-build/hudson
76
+ #
77
+ #####################################################################
78
+
79
+ `;
80
+
81
+ export const DEFAULT_CONFIG: HudsonConfig = {
82
+ project: {
83
+ name: "my-project",
84
+ version: "1.0.0",
85
+ description: "A Hudson-managed project",
86
+ },
87
+ build: {
88
+ outDir: "dist",
89
+ tsconfig: "tsconfig.json",
90
+ sourcemap: true,
91
+ minify: false,
92
+ target: "ES2022",
93
+ clean: true,
94
+ },
95
+ workspaces: {
96
+ enabled: false,
97
+ packages: ["packages/*"],
98
+ parallel: true,
99
+ maxConcurrency: 4,
100
+ },
101
+ dev: {
102
+ watch: true,
103
+ open: false,
104
+ },
105
+ scripts: {
106
+ build: "tsc",
107
+ dev: "tsc --watch",
108
+ test: "node --test",
109
+ lint: "eslint .",
110
+ clean: "rm -rf dist",
111
+ },
112
+ hooks: {},
113
+ packageManager: "pnpm",
114
+ output: {
115
+ verbose: false,
116
+ timestamps: true,
117
+ color: true,
118
+ banner: true,
119
+ },
120
+ };
121
+
122
+ export function getHudsonDir(cwd = process.cwd()): string {
123
+ return path.join(cwd, ".hudson");
124
+ }
125
+
126
+ export function getConfigPath(cwd = process.cwd()): string {
127
+ return path.join(getHudsonDir(cwd), "hudson.yml");
128
+ }
129
+
130
+ export function getTempDir(cwd = process.cwd()): string {
131
+ return path.join(getHudsonDir(cwd), "temp");
132
+ }
133
+
134
+ export function ensureHudsonDirs(cwd = process.cwd()): void {
135
+ const hudsonDir = getHudsonDir(cwd);
136
+ const tempDir = getTempDir(cwd);
137
+ if (!fs.existsSync(hudsonDir)) fs.mkdirSync(hudsonDir, { recursive: true });
138
+ if (!fs.existsSync(tempDir)) fs.mkdirSync(tempDir, { recursive: true });
139
+ }
140
+
141
+ export function loadConfig(cwd = process.cwd()): HudsonConfig {
142
+ const configPath = getConfigPath(cwd);
143
+ if (!fs.existsSync(configPath)) {
144
+ return DEFAULT_CONFIG;
145
+ }
146
+ const raw = fs.readFileSync(configPath, "utf-8");
147
+ const parsed = yaml.load(raw) as Partial<HudsonConfig>;
148
+ return deepMerge(DEFAULT_CONFIG, parsed) as HudsonConfig;
149
+ }
150
+
151
+ export function saveConfig(config: HudsonConfig, cwd = process.cwd()): void {
152
+ ensureHudsonDirs(cwd);
153
+ const configPath = getConfigPath(cwd);
154
+ const yamlContent = yaml.dump(config, {
155
+ indent: 2,
156
+ lineWidth: 80,
157
+ quotingType: '"',
158
+ });
159
+ fs.writeFileSync(configPath, CONFIG_HEADER + yamlContent, "utf-8");
160
+ }
161
+
162
+ export function setConfigValue(keyPath: string, value: string, cwd = process.cwd()): void {
163
+ const config = loadConfig(cwd);
164
+ const keys = keyPath.split(".");
165
+ let obj: Record<string, unknown> = config as unknown as Record<string, unknown>;
166
+
167
+ for (let i = 0; i < keys.length - 1; i++) {
168
+ const k = keys[i];
169
+ if (!(k in obj) || typeof obj[k] !== "object") {
170
+ obj[k] = {};
171
+ }
172
+ obj = obj[k] as Record<string, unknown>;
173
+ }
174
+
175
+ const lastKey = keys[keys.length - 1];
176
+ const parsed = parseValue(value);
177
+ obj[lastKey] = parsed;
178
+
179
+ saveConfig(config, cwd);
180
+ }
181
+
182
+ export function getConfigValue(keyPath: string, cwd = process.cwd()): unknown {
183
+ const config = loadConfig(cwd);
184
+ const keys = keyPath.split(".");
185
+ let obj: unknown = config;
186
+
187
+ for (const k of keys) {
188
+ if (obj == null || typeof obj !== "object") return undefined;
189
+ obj = (obj as Record<string, unknown>)[k];
190
+ }
191
+
192
+ return obj;
193
+ }
194
+
195
+ function parseValue(value: string): unknown {
196
+ if (value === "true") return true;
197
+ if (value === "false") return false;
198
+ const num = Number(value);
199
+ if (!isNaN(num) && value.trim() !== "") return num;
200
+ return value;
201
+ }
202
+
203
+ function deepMerge(target: unknown, source: unknown): unknown {
204
+ if (typeof source !== "object" || source === null) return source ?? target;
205
+ if (typeof target !== "object" || target === null) return source;
206
+
207
+ const result = { ...(target as Record<string, unknown>) };
208
+ for (const key of Object.keys(source as Record<string, unknown>)) {
209
+ const srcVal = (source as Record<string, unknown>)[key];
210
+ const tgtVal = (target as Record<string, unknown>)[key];
211
+ result[key] = deepMerge(tgtVal, srcVal);
212
+ }
213
+ return result;
214
+ }