summaryrefslogtreecommitdiffhomepage
path: root/packages/core/src/config/loader.ts
blob: bccbb8f5654a53c7745164591198ad5ec14ebfe6 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
import { readFileSync } from "node:fs";
import { homedir } from "node:os";
import { join } from "node:path";
import { parse } from "smol-toml";
import type { PermissionRule, Ruleset } from "../permission/index.js";
import type { DispatchConfig } from "../types/index.js";
import { validateConfig } from "./schema.js";

const DEFAULT_CONFIG: DispatchConfig = { permissions: {} };

const VALID_ACTIONS = new Set(["allow", "deny", "ask"]);

function validateAction(raw: string): "allow" | "deny" | "ask" {
	if (VALID_ACTIONS.has(raw)) return raw as "allow" | "deny" | "ask";
	console.warn(`dispatch: unrecognized action "${raw}", defaulting to "ask"`);
	return "ask";
}

// Load dispatch.toml from the given directory
export function loadConfig(dir: string): DispatchConfig {
	const tomlPath = join(dir, "dispatch.toml");
	let raw: unknown;
	try {
		const content = readFileSync(tomlPath, "utf-8");
		raw = parse(content);
	} catch (err: unknown) {
		if (err instanceof Error && (err as NodeJS.ErrnoException).code === "ENOENT") {
			// File doesn't exist — return empty default
			return DEFAULT_CONFIG;
		}
		console.warn(
			`dispatch: failed to parse dispatch.toml: ${err instanceof Error ? err.message : String(err)}`,
		);
		throw err;
	}

	const { config, errors } = validateConfig(raw);
	for (const e of errors) {
		console.warn(`dispatch: config warning at ${e.path}: ${e.message}`);
	}
	return config;
}

// Convert the config's permission block to a Ruleset
export function configToRuleset(config: DispatchConfig): Ruleset {
	const home = homedir();
	const rules: PermissionRule[] = [];

	for (const [permission, value] of Object.entries(config.permissions)) {
		if (typeof value === "string") {
			const action = validateAction(value);
			rules.push({ permission, pattern: "*", action });
		} else {
			for (const [rawPattern, rawAction] of Object.entries(value)) {
				const pattern = rawPattern
					.replace(/^\$HOME(?=[/\\]|$)/, home)
					.replace(/^~(?=[/\\]|$)/, home);
				const action = validateAction(rawAction);
				rules.push({ permission, pattern, action });
			}
		}
	}

	return rules;
}