Luigit
repositories / pi-ext

pi-ext

bugabingas pi extensions

owned by admin

extensions/nushell/src/pi-ext-settings.ts

Raw
// Generated by scripts/codegen.mjs. DO NOT EDIT.
// ============================================================================
// THIS FILE IS GENERATED. DO NOT EDIT IT DIRECTLY.
// Instead, edit scripts/codegen/templates/pi-ext-settings.ts.
// Then run: mise run //:codegen
// ============================================================================
import {
	type ExtensionAPI,
	type ExtensionContext,
	SettingsManager,
} from "@earendil-works/pi-coding-agent";

/**
 * Extension-owned setting declaration.
 * Resolution order per key: flag, env, trusted project settings, user settings, default.
 * When project and user values are both plain objects, they deep-merge with project
 * winning per key; arrays and scalars replace.
 */
export interface SettingDeclaration<T> {
	/** Dotted path inside Pi settings.json, for example `nushell.enable`. */
	key: string;
	/**
	 * Convert a raw flag, env, or settings value.
	 * Return undefined or throw an Error to reject it; thrown messages are reported.
	 */
	parse: (raw: unknown, source: SettingSource) => T | undefined;
	default: T;
	/** Pi CLI flag. Pi flags are boolean or string; boolean flags can only express true. */
	flag?: { name: string; type: "boolean" | "string"; description: string };
	/** Namespaced environment variable. An empty value is present, not missing. */
	env?: string;
}

export type SettingSource = "flag" | "env" | "project" | "user" | "default";

/** Invalid input never falls through to a lower-priority source. */
export type SettingResult<T> =
	| { ok: true; value: T; source: SettingSource }
	| { ok: false; source: SettingSource; error: string };

const MISSING = Symbol("missing");

/** Boolean parser: booleans anywhere; `true|false|1|0` strings from flags or env only. */
export function parseBooleanSetting(
	raw: unknown,
	source: SettingSource,
): boolean | undefined {
	if (typeof raw === "boolean") return raw;
	if (source !== "flag" && source !== "env") return undefined;
	if (raw === "true" || raw === "1") return true;
	if (raw === "false" || raw === "0") return false;
	return undefined;
}

/** Largest delay Node timers and execFile timeouts accept; larger values fire after 1 ms. */
export const MAX_TIMER_MS = 2_147_483_647;

/** Finite number parser: numbers anywhere; numeric strings from flags or env only. */
export function parseNumberSetting(
	raw: unknown,
	source: SettingSource,
): number | undefined {
	const value =
		typeof raw === "number"
			? raw
			: typeof raw === "string" &&
					(source === "flag" || source === "env") &&
					raw.trim() !== ""
				? Number(raw)
				: Number.NaN;
	return Number.isFinite(value) ? value : undefined;
}

/** Register the declared flag without a default, keeping omission distinct from values. */
export function registerSettingFlag<T>(
	pi: Pick<ExtensionAPI, "registerFlag">,
	declaration: SettingDeclaration<T>,
): void {
	const { flag } = declaration;
	if (flag)
		pi.registerFlag(flag.name, {
			description: flag.description,
			type: flag.type,
		});
}

function isPlainObject(value: unknown): value is Record<string, unknown> {
	return typeof value === "object" && value !== null && !Array.isArray(value);
}

function deepMerge(
	base: Record<string, unknown>,
	override: Record<string, unknown>,
): Record<string, unknown> {
	const merged: Record<string, unknown> = { ...base };
	for (const [key, value] of Object.entries(override)) {
		const current = Object.hasOwn(merged, key) ? merged[key] : undefined;
		// defineProperty keeps JSON keys such as __proto__ as own data properties.
		Object.defineProperty(merged, key, {
			value:
				isPlainObject(current) && isPlainObject(value)
					? deepMerge(current, value)
					: value,
			enumerable: true,
			writable: true,
			configurable: true,
		});
	}
	return merged;
}

function settingAt(settings: unknown, key: string): unknown {
	let value = settings;
	for (const part of key.split(".")) {
		if (
			typeof value !== "object" ||
			value === null ||
			Array.isArray(value) ||
			!Object.hasOwn(value, part)
		)
			return MISSING;
		value = (value as Record<string, unknown>)[part];
	}
	return value;
}

/** Resolve one setting when both flags and project trust are available. */
export function resolveSetting<T>(
	pi: Pick<ExtensionAPI, "getFlag">,
	ctx: Pick<ExtensionContext, "cwd" | "isProjectTrusted">,
	declaration: SettingDeclaration<T>,
): SettingResult<T> {
	const { key, flag, env } = declaration;
	const candidates: [SettingSource, string, () => unknown][] = [];
	if (flag)
		candidates.push([
			"flag",
			`flag --${flag.name}`,
			() => pi.getFlag(flag.name) ?? MISSING,
		]);
	if (env)
		candidates.push([
			"env",
			`environment variable ${env}`,
			() => (Object.hasOwn(process.env, env) ? process.env[env] : MISSING),
		]);
	let settings: SettingsManager | undefined;
	const load = () => {
		settings ??= SettingsManager.create(ctx.cwd, undefined, {
			projectTrusted: ctx.isProjectTrusted(),
		});
		return settings;
	};
	candidates.push(
		[
			"project",
			"project settings",
			() => settingAt(load().getProjectSettings(), key),
		],
		["user", "user settings", () => settingAt(load().getGlobalSettings(), key)],
	);
	for (const [index, [source, label, read]] of candidates.entries()) {
		let raw = read();
		if (raw === MISSING) continue;
		let from = label;
		if (source === "project") {
			const user = candidates[index + 1]?.[2]();
			if (isPlainObject(raw) && isPlainObject(user)) {
				raw = deepMerge(user, raw);
				from = "merged project and user settings";
			}
		}
		let value: T | undefined;
		try {
			value = declaration.parse(raw, source);
		} catch (error) {
			if (!(error instanceof Error)) throw error;
			return {
				ok: false,
				source,
				error: `Invalid ${key} from ${from}: ${error.message}`,
			};
		}
		if (value === undefined)
			return { ok: false, source, error: `Invalid ${key} from ${from}` };
		return { ok: true, value, source };
	}
	return { ok: true, value: declaration.default, source: "default" };
}