History

Use platform-standard home config paths

Rhys Sullivan committed 8 months ago

Commit 2b571621 parent 9ad2bf82 files · +123 −3Browse files

Unified · Split

2 files
  1. packages/control-plane/src/runtime/local-config.test.ts +46 −0
  2. packages/control-plane/src/runtime/local-config.ts +77 −3
packages/control-plane/src/runtime/local-config.test.ts+46−0
@@ -7,6 +7,8 @@ import * as Effect from "effect/Effect"; · 6 unmodified lines
7 7
8import {8import {
9 loadLocalExecutorConfig,9 loadLocalExecutorConfig,
10 resolveDefaultHomeConfigCandidates,
11 resolveHomeConfigPath,
10 resolveLocalWorkspaceContext,12 resolveLocalWorkspaceContext,
11} from "./local-config";13} from "./local-config";
12 14
@@ -93,4 +95,48 @@ describe("local-config", () => { · 80 unmodified lines
93 expect(failure.message).toContain("line 5, column 7");95 expect(failure.message).toContain("line 5, column 7");
94 }),96 }),
95 );97 );
98
99 it("uses platform-standard home config candidates", () => {
100 const linuxCandidates = resolveDefaultHomeConfigCandidates({
101 platform: "linux",
102 homeDirectory: "/home/alice",
103 env: {},
104 });
105 const macCandidates = resolveDefaultHomeConfigCandidates({
106 platform: "darwin",
107 homeDirectory: "/Users/alice",
108 env: {},
109 });
110
111 expect(linuxCandidates[0]).toBe("/home/alice/.config/executor/executor.jsonc");
112 expect(macCandidates[0]).toBe(
113 "/Users/alice/Library/Application Support/Executor/executor.jsonc",
114 );
115 expect(macCandidates[2]).toBe("/Users/alice/.config/executor/executor.jsonc");
116 });
117
118 it.effect("prefers an existing legacy home config path before the canonical path", () =>
119 Effect.gen(function* () {
120 const homeDirectory = makeWorkspaceRoot();
121 const legacyConfigDirectory = join(homeDirectory, ".config", "executor");
122 yield* Effect.promise(() => mkdir(legacyConfigDirectory, { recursive: true }));
123 yield* Effect.promise(() =>
124 writeFile(
125 join(legacyConfigDirectory, "executor.jsonc"),
126 "{\n \"sources\": {}\n}\n",
127 "utf8",
128 ),
129 );
130
131 const resolvedPath = yield* Effect.promise(() =>
132 resolveHomeConfigPath({
133 platform: "darwin",
134 homeDirectory,
135 env: {},
136 }),
137 );
138
139 expect(resolvedPath).toBe(join(legacyConfigDirectory, "executor.jsonc"));
140 }),
141 );
96});142});
packages/control-plane/src/runtime/local-config.ts+77−3
@@ -22,8 +22,7 @@ const decodeLocalExecutorConfig = Schema.decodeUnknownSync(LocalExecutorConfigSc · 21 unmodified lines
22const PROJECT_CONFIG_BASENAME = "executor.jsonc";22const PROJECT_CONFIG_BASENAME = "executor.jsonc";
23const PROJECT_CONFIG_FALLBACK_BASENAME = "executor.json";23const PROJECT_CONFIG_FALLBACK_BASENAME = "executor.json";
24const PROJECT_CONFIG_DIRECTORY = ".executor";24const PROJECT_CONFIG_DIRECTORY = ".executor";
25const HOME_CONFIG_DIRECTORY = join(homedir(), ".config", "executor");25const EXECUTOR_CONFIG_DIR_ENV = "EXECUTOR_CONFIG_DIR";
26const HOME_CONFIG_PATH = join(HOME_CONFIG_DIRECTORY, PROJECT_CONFIG_BASENAME);
27 26
28const normalizeSlashPath = (value: string): string =>27const normalizeSlashPath = (value: string): string =>
29 value.replaceAll("\\", "/");28 value.replaceAll("\\", "/");
@@ -54,6 +53,80 @@ const trimOrUndefined = (value: string | undefined | null): string | undefined = · 24 unmodified lines
54 return trimmed && trimmed.length > 0 ? trimmed : undefined;53 return trimmed && trimmed.length > 0 ? trimmed : undefined;
55};54};
56 55
56const defaultExecutorConfigDirectory = (input: {
57 env?: NodeJS.ProcessEnv;
58 platform?: NodeJS.Platform;
59 homeDirectory?: string;
60} = {}): string => {
61 const env = input.env ?? process.env;
62 const platform = input.platform ?? process.platform;
63 const homeDirectory = input.homeDirectory ?? homedir();
64 const explicitConfigDirectory = trimOrUndefined(env[EXECUTOR_CONFIG_DIR_ENV]);
65
66 if (explicitConfigDirectory) {
67 return explicitConfigDirectory;
68 }
69
70 if (platform === "win32") {
71 return join(
72 trimOrUndefined(env.LOCALAPPDATA) ?? join(homeDirectory, "AppData", "Local"),
73 "Executor",
74 );
75 }
76
77 if (platform === "darwin") {
78 return join(homeDirectory, "Library", "Application Support", "Executor");
79 }
80
81 return join(
82 trimOrUndefined(env.XDG_CONFIG_HOME) ?? join(homeDirectory, ".config"),
83 "executor",
84 );
85};
86
87const legacyExecutorConfigDirectory = (input: {
88 homeDirectory?: string;
89} = {}): string =>
90 join(input.homeDirectory ?? homedir(), ".config", "executor");
91
92export const resolveDefaultHomeConfigCandidates = (input: {
93 env?: NodeJS.ProcessEnv;
94 platform?: NodeJS.Platform;
95 homeDirectory?: string;
96} = {}): string[] => {
97 const homeDirectory = input.homeDirectory ?? homedir();
98 const canonicalDirectory = defaultExecutorConfigDirectory({
99 env: input.env,
100 platform: input.platform,
101 homeDirectory,
102 });
103 const legacyDirectory = legacyExecutorConfigDirectory({ homeDirectory });
104 const directories = canonicalDirectory === legacyDirectory
105 ? [canonicalDirectory]
106 : [canonicalDirectory, legacyDirectory];
107
108 return directories.flatMap((directory) => [
109 join(directory, PROJECT_CONFIG_BASENAME),
110 join(directory, PROJECT_CONFIG_FALLBACK_BASENAME),
111 ]);
112};
113
114export const resolveHomeConfigPath = async (input: {
115 env?: NodeJS.ProcessEnv;
116 platform?: NodeJS.Platform;
117 homeDirectory?: string;
118} = {}): Promise<string> => {
119 const candidates = resolveDefaultHomeConfigCandidates(input);
120
121 for (const candidate of candidates) {
122 if (await fileExists(candidate)) {
123 return candidate;
124 }
125 }
126
127 return candidates[0]!;
128};
129
57const formatJsoncParseErrors = (content: string, errors: readonly JsoncParseError[]): string => {130const formatJsoncParseErrors = (content: string, errors: readonly JsoncParseError[]): string => {
58 const lines = content.split("\n");131 const lines = content.split("\n");
59 132
@@ -232,6 +305,7 @@ export const resolveLocalWorkspaceContext = async (input: { · 172 unmodified lines
232 );305 );
233 const workspaceName = basename(workspaceRoot) || "workspace";306 const workspaceName = basename(workspaceRoot) || "workspace";
234 const projectConfigPath = await resolveProjectConfigPath(workspaceRoot);307 const projectConfigPath = await resolveProjectConfigPath(workspaceRoot);
308 const homeConfigPath = await resolveHomeConfigPath();
235 309
236 return {310 return {
237 cwd,311 cwd,
@@ -239,7 +313,7 @@ export const resolveLocalWorkspaceContext = async (input: { · 1 unmodified line
239 workspaceName,313 workspaceName,
240 configDirectory: join(workspaceRoot, PROJECT_CONFIG_DIRECTORY),314 configDirectory: join(workspaceRoot, PROJECT_CONFIG_DIRECTORY),
241 projectConfigPath,315 projectConfigPath,
242 homeConfigPath: HOME_CONFIG_PATH,316 homeConfigPath,
243 artifactsDirectory: join(workspaceRoot, PROJECT_CONFIG_DIRECTORY, "artifacts"),317 artifactsDirectory: join(workspaceRoot, PROJECT_CONFIG_DIRECTORY, "artifacts"),
244 stateDirectory: join(workspaceRoot, PROJECT_CONFIG_DIRECTORY, "state"),318 stateDirectory: join(workspaceRoot, PROJECT_CONFIG_DIRECTORY, "state"),
245 installationId: `local_${stableHash(normalizeSlashPath(workspaceRoot))}`,319 installationId: `local_${stableHash(normalizeSlashPath(workspaceRoot))}`,