( path: string, scope: "user" | "project", )
| 95 | } |
| 96 | |
| 97 | function loadConfigFile( |
| 98 | path: string, |
| 99 | scope: "user" | "project", |
| 100 | ): LoadedConfigFile | null { |
| 101 | try { |
| 102 | const rawText = readFileSync(path, "utf-8"); |
| 103 | const substituted = substituteConfigVariables({ |
| 104 | text: rawText, |
| 105 | configPath: path, |
| 106 | // Repo-supplied project configs are untrusted: do not expand |
| 107 | // {env:}/{file:} secret-bearing tokens (parity with OpenCode). |
| 108 | isProjectConfig: scope === "project", |
| 109 | }); |
| 110 | return { |
| 111 | path, |
| 112 | scope, |
| 113 | config: parseJsonc(substituted.text) as Record<string, unknown>, |
| 114 | warnings: substituted.warnings.map((warning) => `${path}: ${warning}`), |
| 115 | loadOutcome: |
| 116 | substituted.warnings.length > 0 ? "substitution-failure" : "ok", |
| 117 | }; |
| 118 | } catch (error) { |
| 119 | const message = error instanceof Error ? error.message : String(error); |
| 120 | return { |
| 121 | path, |
| 122 | scope, |
| 123 | config: {}, |
| 124 | warnings: [ |
| 125 | `${path}: failed to load config: ${message}; using defaults for this file.`, |
| 126 | ], |
| 127 | loadOutcome: |
| 128 | typeof (error as { code?: unknown }).code === "string" |
| 129 | ? "project-file-io-error" |
| 130 | : "project-file-parse-error", |
| 131 | }; |
| 132 | } |
| 133 | } |
| 134 | |
| 135 | function redactConfigValue(value: unknown): string { |
| 136 | if (value === undefined) return "<missing>"; |
no test coverage detected