mirror of
https://github.com/morluto/rea.git
synced 2026-11-06 06:46:10 +00:00
fix(setup): honor active client configuration and skill roots (#1504)
* refactor(setup): resolve skill destinations per client * fix(setup): honor active client configuration and skill roots * fix(setup): validate effective client scopes and Gemini trust * fix(setup): treat empty OpenCode overrides as unset An empty OPENCODE_CONFIG_DIR now stays on the XDG directory, and doctor and setup inspect global OpenCode files when a custom directory is absent. Uninstall reports one status per client so a missing earlier layer does not hide a removed registration. --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com>
This commit is contained in:
1 parent
b25922e4e8
commit
7a56fe6260
14 files changed
+1562
-61
No files matched your search
@@ -0,0 +1,108 @@
|
||||
import { err, ok, type Result } from "../domain/result.js";
|
||||
import { readFile } from "node:fs/promises";
|
||||
import {
|
||||
effectiveClientServer,
|
||||
parseClientConfiguration,
|
||||
type ClientConfigurationDocument,
|
||||
} from "./ClientConfigurationDocument.js";
|
||||
import type { SetupClient } from "./SupportedClients.js";
|
||||
|
||||
/** A named source failure that setup can explain without losing the bad path. */
|
||||
export interface ClientConfigurationFileError {
|
||||
readonly kind: "unreadable" | "malformed";
|
||||
readonly path: string;
|
||||
readonly detail: string;
|
||||
}
|
||||
|
||||
/** Preserve named read failures and distinguish a missing source from an empty file. */
|
||||
export const readConfigurationText = async (
|
||||
path: string,
|
||||
): Promise<Result<string | undefined, ClientConfigurationFileError>> => {
|
||||
try {
|
||||
return ok(await readFile(path, "utf8"));
|
||||
} catch (cause: unknown) {
|
||||
if (cause instanceof Error && "code" in cause && cause.code === "ENOENT")
|
||||
return ok(undefined);
|
||||
return err({
|
||||
kind: "unreadable",
|
||||
path,
|
||||
detail: `Configuration ${path} could not be read: ${cause instanceof Error ? cause.message : String(cause)}`,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/** Read and parse one configuration at its source boundary. */
|
||||
export const readConfigurationFile = async (
|
||||
path: string,
|
||||
format: SetupClient["format"],
|
||||
): Promise<
|
||||
Result<ClientConfigurationDocument | undefined, ClientConfigurationFileError>
|
||||
> => {
|
||||
const source = await readConfigurationText(path);
|
||||
if (!source.ok) return source;
|
||||
const content = source.value;
|
||||
if (content === undefined) return ok(undefined);
|
||||
try {
|
||||
return ok(parseClientConfiguration(content, format));
|
||||
} catch (cause: unknown) {
|
||||
return err({
|
||||
kind: "malformed",
|
||||
path,
|
||||
detail: `Configuration ${path} is malformed: ${cause instanceof Error ? cause.message : String(cause)}`,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/** Read every applicable user file; malformed lower-priority files also stop setup. */
|
||||
export const readClientConfigurationFiles = async (
|
||||
client: SetupClient,
|
||||
): Promise<
|
||||
Result<readonly ClientConfigurationDocument[], ClientConfigurationFileError>
|
||||
> => {
|
||||
const documents: ClientConfigurationDocument[] = [];
|
||||
for (const path of client.configPaths ?? [client.configPath]) {
|
||||
const document = await readConfigurationFile(path, client.format);
|
||||
if (!document.ok) return document;
|
||||
if (document.value !== undefined) documents.push(document.value);
|
||||
}
|
||||
return ok(documents);
|
||||
};
|
||||
|
||||
/** OpenCode merges server objects in file order, replacing arrays and scalar values. */
|
||||
const mergeConfigurationValue = (previous: unknown, next: unknown): unknown => {
|
||||
if (!isObject(previous) || !isObject(next)) return next;
|
||||
return Object.fromEntries(
|
||||
[...new Set([...Object.keys(previous), ...Object.keys(next)])].map(
|
||||
(key) => [
|
||||
key,
|
||||
Object.hasOwn(next, key)
|
||||
? mergeConfigurationValue(previous[key], next[key])
|
||||
: previous[key],
|
||||
],
|
||||
),
|
||||
);
|
||||
};
|
||||
const isObject = (value: unknown): value is Record<string, unknown> =>
|
||||
typeof value === "object" && value !== null && !Array.isArray(value);
|
||||
|
||||
/** Project a client's effective user registration without modifying its source files. */
|
||||
export const effectiveClientConfiguration = (
|
||||
documents: readonly ClientConfigurationDocument[],
|
||||
): ClientConfigurationDocument | undefined => {
|
||||
const last = documents.at(-1);
|
||||
if (last === undefined) return undefined;
|
||||
if (documents.length === 1) return last;
|
||||
const servers: Record<string, unknown> = {};
|
||||
for (const document of documents) {
|
||||
const names = new Set([
|
||||
...Object.keys(document.legacyServers),
|
||||
...Object.keys(document.servers),
|
||||
]);
|
||||
for (const name of names)
|
||||
servers[name] = mergeConfigurationValue(
|
||||
servers[name],
|
||||
effectiveClientServer(document, name),
|
||||
);
|
||||
}
|
||||
return { ...last, servers, legacyServers: {} };
|
||||
};
|
||||
Reference in new issue
Block a user