pi-blackhole 0.4.2 → 0.4.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -0
- package/dist/index.js +11660 -0
- package/dist/index.js.map +1 -0
- package/example-config.json +1 -1
- package/index.ts +37 -63
- package/package.json +21 -9
- package/src/commands/cleanup.ts +279 -240
- package/src/commands/memory.ts +236 -184
- package/src/commands/pi-vcc.ts +202 -152
- package/src/commands/vcc-recall.ts +126 -95
- package/src/core/brief.ts +167 -33
- package/src/core/build-sections.ts +8 -2
- package/src/core/config-env.ts +117 -0
- package/src/core/content.ts +31 -7
- package/src/core/drill-down.ts +41 -11
- package/src/core/filter-noise.ts +9 -3
- package/src/core/format-recall.ts +15 -6
- package/src/core/format.ts +14 -4
- package/src/core/lineage.ts +9 -3
- package/src/core/load-messages.ts +24 -5
- package/src/core/normalize.ts +38 -14
- package/src/core/recall-scope.ts +11 -3
- package/src/core/render-entries.ts +22 -6
- package/src/core/sanitize.ts +5 -1
- package/src/core/search-entries.ts +111 -19
- package/src/core/settings.ts +1 -3
- package/src/core/summarize.ts +42 -21
- package/src/core/unified-config.ts +549 -411
- package/src/extract/commits.ts +4 -2
- package/src/extract/files.ts +10 -5
- package/src/extract/goals.ts +7 -2
- package/src/hooks/before-compact.ts +210 -88
- package/src/om/agents/dropper/agent.ts +380 -265
- package/src/om/agents/dropper/coverage.ts +102 -82
- package/src/om/agents/observer/agent.ts +242 -206
- package/src/om/agents/reflector/agent.ts +212 -153
- package/src/om/cleanup.ts +239 -218
- package/src/om/clipboard.ts +59 -51
- package/src/om/compaction-trigger.ts +448 -333
- package/src/om/config.ts +13 -6
- package/src/om/configure-overlay.ts +518 -355
- package/src/om/consolidation.ts +1460 -953
- package/src/om/cooldown.ts +75 -65
- package/src/om/debug-log.ts +86 -68
- package/src/om/ids.ts +1 -1
- package/src/om/ledger/fold.ts +89 -78
- package/src/om/ledger/progress.ts +181 -153
- package/src/om/ledger/projection.ts +248 -185
- package/src/om/ledger/recall.ts +247 -196
- package/src/om/ledger/render-summary.ts +79 -50
- package/src/om/ledger/types.ts +146 -117
- package/src/om/model-budget.ts +23 -13
- package/src/om/pending.ts +243 -179
- package/src/om/provider-stream.ts +52 -7
- package/src/om/retryable-error.ts +12 -16
- package/src/om/reverse-recall.ts +97 -91
- package/src/om/runtime.ts +474 -375
- package/src/om/serialize.ts +190 -166
- package/src/om/status-overlay.ts +246 -195
- package/src/om/tokens.ts +28 -21
- package/src/pi-base/blackhole-settings.ts +437 -0
- package/src/pi-base/config-manager.ts +440 -0
- package/src/pi-base/config.ts +469 -0
- package/src/pi-base/env.ts +43 -0
- package/src/pi-base/paths.ts +47 -0
- package/src/pi-base/settings/body.ts +1648 -0
- package/src/pi-base/settings/fields/action.ts +43 -0
- package/src/pi-base/settings/fields/boolean.ts +47 -0
- package/src/pi-base/settings/fields/custom.ts +72 -0
- package/src/pi-base/settings/fields/enum.ts +310 -0
- package/src/pi-base/settings/fields/index.ts +46 -0
- package/src/pi-base/settings/fields/model.ts +452 -0
- package/src/pi-base/settings/fields/string.ts +527 -0
- package/src/pi-base/settings/fields/text.ts +115 -0
- package/src/pi-base/settings/frame.ts +197 -0
- package/src/pi-base/settings/index.ts +77 -0
- package/src/pi-base/settings/inline-edit.ts +313 -0
- package/src/pi-base/settings/modal.ts +152 -0
- package/src/pi-base/settings/types.ts +500 -0
- package/src/pi-base/settings/validate-field.ts +113 -0
- package/src/pi-base/shell.ts +117 -0
- package/src/pi-base/types.ts +6 -0
- package/src/pi-base/ui.ts +32 -0
- package/src/tools/recall.ts +347 -225
- package/src/types.ts +20 -3
- package/tsup.config.ts +23 -0
- package/vitest.config.ts +15 -15
|
@@ -0,0 +1,440 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ConfigManager — declarative config management for pi extensions.
|
|
3
|
+
*
|
|
4
|
+
* Packages configure a single `ConfigManager<T>` with their config shape,
|
|
5
|
+
* defaults, field definitions, optional validation, and optional env-var
|
|
6
|
+
* overrides. The manager handles loading, saving, and modal wiring.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { join } from "node:path";
|
|
10
|
+
import { existsSync } from "node:fs";
|
|
11
|
+
import {
|
|
12
|
+
applyEnvOverrides as applyEnvOverridesGeneric,
|
|
13
|
+
type EnvParser,
|
|
14
|
+
} from "../core/config-env.js";
|
|
15
|
+
export type { EnvParser } from "../core/config-env.js";
|
|
16
|
+
import {
|
|
17
|
+
loadConfig,
|
|
18
|
+
writeConfig,
|
|
19
|
+
readConfig,
|
|
20
|
+
deleteConfig,
|
|
21
|
+
getExtensionsDir,
|
|
22
|
+
deepEqual,
|
|
23
|
+
checkConfigFile,
|
|
24
|
+
} from "./config.ts";
|
|
25
|
+
import { openSettingsModal } from "./settings/index.ts";
|
|
26
|
+
import { validateFieldValue } from "./settings/validate-field.ts";
|
|
27
|
+
import type { Field } from "./settings/types.ts";
|
|
28
|
+
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
29
|
+
|
|
30
|
+
// ── Types ──────────────────────────────────────────────────────────────
|
|
31
|
+
|
|
32
|
+
export interface ConfigManagerOptions<T extends object = object> {
|
|
33
|
+
/** Extension identifier — e.g. "bash-timeout" */
|
|
34
|
+
id: string;
|
|
35
|
+
/** Human-readable label shown in the UI */
|
|
36
|
+
label: string;
|
|
37
|
+
/** Config file name — e.g. "bash-timeout-config.json" */
|
|
38
|
+
filename: string;
|
|
39
|
+
/** Default config values (the complete object) */
|
|
40
|
+
defaults: T;
|
|
41
|
+
/** Build Field[] from current config values */
|
|
42
|
+
fields: (config: T) => Field[];
|
|
43
|
+
/**
|
|
44
|
+
* Optional validator. Receives raw merged data and returns a fully
|
|
45
|
+
* coerced config. If omitted, the manager does a simple spread merge.
|
|
46
|
+
*/
|
|
47
|
+
validate?: (raw: Record<string, unknown>) => T;
|
|
48
|
+
/**
|
|
49
|
+
* Optional env-var overrides. Maps config keys to env var names.
|
|
50
|
+
* Boolean types use `readBooleanEnv`, numeric types use `readPositiveIntEnv`.
|
|
51
|
+
* For custom parsing, use a parser object with a `parse` function.
|
|
52
|
+
*/
|
|
53
|
+
env?: Partial<Record<keyof T, string | EnvParser>>;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface ConfigLoadWarning {
|
|
57
|
+
/** Scope where the warning originated */
|
|
58
|
+
scope: "global" | "project";
|
|
59
|
+
/** Human-readable warning message */
|
|
60
|
+
message: string;
|
|
61
|
+
/** Config key involved (if applicable) */
|
|
62
|
+
key?: string;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface ConfigLoadResult<T> {
|
|
66
|
+
/** Fully resolved config value */
|
|
67
|
+
config: T;
|
|
68
|
+
/** Non-fatal warnings discovered during loading (bad values, unknown keys, etc.) */
|
|
69
|
+
warnings: ConfigLoadWarning[];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// ── ConfigManager ───────────────────────────────────────────────────────
|
|
73
|
+
|
|
74
|
+
export class ConfigManager<T extends object> {
|
|
75
|
+
private opts: ConfigManagerOptions<T>;
|
|
76
|
+
|
|
77
|
+
constructor(opts: ConfigManagerOptions<T>) {
|
|
78
|
+
this.opts = opts;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Load config with layered resolution:
|
|
83
|
+
* defaults ← global file ← project file ← env overrides
|
|
84
|
+
*
|
|
85
|
+
* @param cwd Working directory for project-local override
|
|
86
|
+
* @param configDir Global config directory (defaults to getExtensionsDir())
|
|
87
|
+
*/
|
|
88
|
+
load(cwd?: string, configDir?: string): T {
|
|
89
|
+
return this.loadWithWarnings(cwd, configDir).config;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Load config with warnings. Same as `load()` but also returns per-field
|
|
94
|
+
* validation warnings and unknown-key warnings discovered during loading.
|
|
95
|
+
*
|
|
96
|
+
* Extensions that open a modal can surface these warnings in the UI;
|
|
97
|
+
* extensions that load config programmatically can log or inspect them.
|
|
98
|
+
*/
|
|
99
|
+
loadWithWarnings(cwd?: string, configDir?: string): ConfigLoadResult<T> {
|
|
100
|
+
const warnings: ConfigLoadWarning[] = [];
|
|
101
|
+
|
|
102
|
+
const loaded = loadConfig(this.opts.filename, this.opts.defaults, {
|
|
103
|
+
cwd,
|
|
104
|
+
configDir,
|
|
105
|
+
merge: "deep",
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
// Run validate if provided
|
|
109
|
+
const config = this.opts.validate
|
|
110
|
+
? this.opts.validate(loaded as Record<string, unknown>)
|
|
111
|
+
: (loaded as T);
|
|
112
|
+
|
|
113
|
+
// Validate each field value and collect warnings
|
|
114
|
+
try {
|
|
115
|
+
const fields = this.opts.fields(config);
|
|
116
|
+
const scope = "global";
|
|
117
|
+
for (const field of fields) {
|
|
118
|
+
if (field.type === "action" || field.type === "custom") continue;
|
|
119
|
+
const warning = validateFieldValue(field, field.value);
|
|
120
|
+
if (warning) {
|
|
121
|
+
warnings.push({
|
|
122
|
+
scope: scope as "global" | "project",
|
|
123
|
+
key: String(field.key),
|
|
124
|
+
message: `"${field.label}": ${warning}`,
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
} catch {
|
|
129
|
+
// Fields function may fail for partial configs; skip validation
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Apply env overrides
|
|
133
|
+
const final = this.applyEnvOverrides(config);
|
|
134
|
+
|
|
135
|
+
return { config: final, warnings };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
private applyEnvOverrides(config: T): T {
|
|
139
|
+
const env = this.opts.env;
|
|
140
|
+
if (!env) return config;
|
|
141
|
+
return applyEnvOverridesGeneric(
|
|
142
|
+
config,
|
|
143
|
+
env as Record<string, EnvParser | string>,
|
|
144
|
+
this.opts.defaults as Record<string, unknown>,
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Save config scoped to global or project directory.
|
|
150
|
+
*
|
|
151
|
+
* Writes only the fields that differ from the existing file content
|
|
152
|
+
* (diff-based save against the file). This means:
|
|
153
|
+
*
|
|
154
|
+
* - **First save** (no file exists): writes ALL fields — the file is
|
|
155
|
+
* fully populated with every value the user confirmed.
|
|
156
|
+
* - **Subsequent saves**: only the fields that actually changed in the
|
|
157
|
+
* current session are written. Everything else stays untouched.
|
|
158
|
+
* - **Unknown keys** (hand-edited extras outside the schema) are
|
|
159
|
+
* automatically preserved by the read-patch-write cycle.
|
|
160
|
+
* - **No automatic removal**: only explicit reset/delete removes keys
|
|
161
|
+
* from the file.
|
|
162
|
+
*
|
|
163
|
+
* Reads the existing file, patches it with the deltas, and writes the
|
|
164
|
+
* merged result. This prevents accidental overwrites of fields the user
|
|
165
|
+
* never touched while keeping the file consistent with the UI.
|
|
166
|
+
*
|
|
167
|
+
* @param config Config to persist
|
|
168
|
+
* @param scope Target scope
|
|
169
|
+
* @param cwd Working directory (required for "project" scope)
|
|
170
|
+
* @param configDir Override the global config dir (for tests)
|
|
171
|
+
*/
|
|
172
|
+
save(
|
|
173
|
+
config: T,
|
|
174
|
+
scope: "global" | "project",
|
|
175
|
+
cwd?: string,
|
|
176
|
+
configDir?: string,
|
|
177
|
+
): void {
|
|
178
|
+
if (scope === "project" && !cwd) {
|
|
179
|
+
throw new Error("cwd is required for project-scoped config save");
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const dir =
|
|
183
|
+
scope === "project" && cwd
|
|
184
|
+
? join(cwd, ".pi")
|
|
185
|
+
: (configDir ?? getExtensionsDir());
|
|
186
|
+
const knownKeys = new Set(Object.keys(this.opts.defaults));
|
|
187
|
+
|
|
188
|
+
// Read the existing file. If none exists, start from an empty object.
|
|
189
|
+
const existing =
|
|
190
|
+
readConfig<Record<string, unknown>>(this.opts.filename, dir) ?? {};
|
|
191
|
+
|
|
192
|
+
// Build field map for validation
|
|
193
|
+
const fieldMap = new Map<string, Field>();
|
|
194
|
+
try {
|
|
195
|
+
const fields = this.opts.fields(config);
|
|
196
|
+
for (const f of fields) fieldMap.set(String(f.key), f);
|
|
197
|
+
} catch {
|
|
198
|
+
// Fields function may fail for partial configs; skip validation
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Compute diff: compare each known key's modal value against the
|
|
202
|
+
// existing file value. Only fields that actually changed are written.
|
|
203
|
+
const diff: Record<string, unknown> = {};
|
|
204
|
+
let hasDiff = false;
|
|
205
|
+
|
|
206
|
+
for (const key of Object.keys(this.opts.defaults) as (keyof T)[]) {
|
|
207
|
+
const modalVal = config[key];
|
|
208
|
+
const fileVal = existing[String(key)];
|
|
209
|
+
|
|
210
|
+
if (deepEqual(modalVal, fileVal)) continue;
|
|
211
|
+
|
|
212
|
+
// Validate before persisting — skip invalid values to repair
|
|
213
|
+
// config files (invalid values fall back to defaults next load).
|
|
214
|
+
const field = fieldMap.get(String(key));
|
|
215
|
+
if (field && !(field.type === "action" || field.type === "custom")) {
|
|
216
|
+
const warning = validateFieldValue(field, modalVal);
|
|
217
|
+
if (warning) continue;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
diff[String(key)] = modalVal;
|
|
221
|
+
hasDiff = true;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Preserve unknown keys from the existing file.
|
|
225
|
+
for (const [key, val] of Object.entries(existing)) {
|
|
226
|
+
if (!knownKeys.has(key)) {
|
|
227
|
+
diff[key] = val;
|
|
228
|
+
hasDiff = true;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (!hasDiff) return;
|
|
233
|
+
|
|
234
|
+
// Merge: start from the existing file, overlay the diff. This keeps
|
|
235
|
+
// every untouched key exactly where it was while updating only the
|
|
236
|
+
// fields the user actually changed in this session.
|
|
237
|
+
const merged = { ...existing, ...diff };
|
|
238
|
+
const wrote = writeConfig(this.opts.filename, merged as Partial<T>, dir);
|
|
239
|
+
if (!wrote) {
|
|
240
|
+
throw new Error(
|
|
241
|
+
`Failed to save ${this.opts.filename} — the config file may be read-only (e.g., managed by Nix). ` +
|
|
242
|
+
`Runtime state was updated for this session only.`,
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Open the settings modal with scope tabs, auto-generated onSave, etc.
|
|
249
|
+
*
|
|
250
|
+
* Before opening the modal, checks global and project-local config files
|
|
251
|
+
* for malformed JSON. If either is invalid, a warning notification is
|
|
252
|
+
* shown so the user knows their config couldn't be fully loaded.
|
|
253
|
+
*
|
|
254
|
+
* @param ctx Extension context
|
|
255
|
+
* @param cwd Working directory for project-local override
|
|
256
|
+
* @param onSave Called with the validated config after the user saves
|
|
257
|
+
* @param configDir Override the global config dir (for tests)
|
|
258
|
+
*/
|
|
259
|
+
/**
|
|
260
|
+
* Open the settings modal with scope tabs, auto-generated onSave, scope
|
|
261
|
+
* actions (reset/delete), etc.
|
|
262
|
+
*
|
|
263
|
+
* Before opening the modal, checks global and project-local config files
|
|
264
|
+
* for malformed JSON. If either is invalid, a warning notification is
|
|
265
|
+
* shown so the user knows their config couldn't be fully loaded.
|
|
266
|
+
*
|
|
267
|
+
* @param ctx Extension context
|
|
268
|
+
* @param cwd Working directory for project-local override
|
|
269
|
+
* @param onSave Called with the validated config after the user saves
|
|
270
|
+
* @param configDir Override the global config dir (for tests)
|
|
271
|
+
* @param onChange Optional per-field change handler passed through to
|
|
272
|
+
* the settings modal. Called synchronously when any
|
|
273
|
+
* field value changes (including in buffered mode).
|
|
274
|
+
*/
|
|
275
|
+
async openSettings(
|
|
276
|
+
ctx: ExtensionContext,
|
|
277
|
+
cwd: string,
|
|
278
|
+
onSave: (updated: T) => void,
|
|
279
|
+
configDir?: string,
|
|
280
|
+
onChange?: (key: string, value: unknown) => void,
|
|
281
|
+
): Promise<void> {
|
|
282
|
+
// Check for malformed config files before opening the modal
|
|
283
|
+
this.warnOnMalformedConfig(ctx, cwd, configDir);
|
|
284
|
+
|
|
285
|
+
const config = this.load(cwd, configDir);
|
|
286
|
+
const fields = this.opts.fields(config);
|
|
287
|
+
|
|
288
|
+
// Auto-populate field-level defaults from the config defaults so
|
|
289
|
+
// field-level reset (ctrl+r/ctrl+d) works without every extension
|
|
290
|
+
// manually setting `default` on each field definition.
|
|
291
|
+
const configDefaults = this.opts.defaults as Record<string, unknown>;
|
|
292
|
+
for (const field of fields) {
|
|
293
|
+
if (field.type === "action" || field.type === "custom") continue;
|
|
294
|
+
if (field.default === undefined) {
|
|
295
|
+
const key = String(field.key);
|
|
296
|
+
if (key in configDefaults) {
|
|
297
|
+
(field as any).default = configDefaults[key];
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// Validate loaded field values and warn about inconsistencies.
|
|
303
|
+
// Per-field warnings are also shown inline in the modal UI.
|
|
304
|
+
const loadWarnings: string[] = [];
|
|
305
|
+
for (const field of fields) {
|
|
306
|
+
if (field.type === "action" || field.type === "custom") continue;
|
|
307
|
+
const warning = validateFieldValue(field, field.value);
|
|
308
|
+
if (warning) loadWarnings.push(`"${field.label}": ${warning}`);
|
|
309
|
+
}
|
|
310
|
+
if (loadWarnings.length > 0) {
|
|
311
|
+
const msg =
|
|
312
|
+
loadWarnings.length === 1
|
|
313
|
+
? `Config warning: ${loadWarnings[0]}`
|
|
314
|
+
: `Config has ${loadWarnings.length} warnings. First: ${loadWarnings[0]}`;
|
|
315
|
+
ctx.ui.notify(msg, "warning");
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
await openSettingsModal(ctx, {
|
|
319
|
+
title: this.opts.label,
|
|
320
|
+
configFilename: this.opts.filename,
|
|
321
|
+
mode: "buffered",
|
|
322
|
+
// MUST match the dir used by load()/save() — without this the modal
|
|
323
|
+
// initializes its rows from the wrong file (extensions dir fallback),
|
|
324
|
+
// so an untouched field shows DEFAULTS and a save writes those
|
|
325
|
+
// defaults over the real config.
|
|
326
|
+
globalConfigDir: configDir,
|
|
327
|
+
defaults: this.opts.defaults as unknown as Record<string, unknown>,
|
|
328
|
+
inferDefaultScope: () =>
|
|
329
|
+
existsSync(join(cwd, ".pi", this.opts.filename)) ? "project" : "global",
|
|
330
|
+
fields,
|
|
331
|
+
onChange,
|
|
332
|
+
onSave: async (
|
|
333
|
+
values: Record<string, unknown>,
|
|
334
|
+
scope: "global" | "project",
|
|
335
|
+
) => {
|
|
336
|
+
const merged = { ...config, ...values };
|
|
337
|
+
const updated = this.opts.validate
|
|
338
|
+
? this.opts.validate(merged as Record<string, unknown>)
|
|
339
|
+
: (merged as T);
|
|
340
|
+
this.save(updated, scope, cwd, configDir);
|
|
341
|
+
onSave(updated);
|
|
342
|
+
},
|
|
343
|
+
onResetScope: async (scope) => {
|
|
344
|
+
this.resetScope(scope, cwd, configDir);
|
|
345
|
+
},
|
|
346
|
+
onDeleteScope: async (scope) => {
|
|
347
|
+
this.deleteScope(scope, cwd, configDir);
|
|
348
|
+
},
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Reset a scope's configuration to defaults. Known config keys
|
|
354
|
+
* (those defined in the schema) are removed from the file; unknown
|
|
355
|
+
* keys (from future versions or user additions) are preserved so
|
|
356
|
+
* forward-compatibility is maintained. Next load will use defaults
|
|
357
|
+
* for all known fields.
|
|
358
|
+
*
|
|
359
|
+
* After this call, the file contains only unknown keys. If no
|
|
360
|
+
* unknown keys exist, the file is deleted entirely.
|
|
361
|
+
*/
|
|
362
|
+
resetScope(
|
|
363
|
+
scope: "global" | "project",
|
|
364
|
+
cwd?: string,
|
|
365
|
+
configDir?: string,
|
|
366
|
+
): void {
|
|
367
|
+
if (scope === "project" && !cwd) {
|
|
368
|
+
throw new Error("cwd is required for project-scoped config reset");
|
|
369
|
+
}
|
|
370
|
+
const dir =
|
|
371
|
+
scope === "project" && cwd
|
|
372
|
+
? join(cwd, ".pi")
|
|
373
|
+
: (configDir ?? getExtensionsDir());
|
|
374
|
+
const knownKeys = new Set(Object.keys(this.opts.defaults));
|
|
375
|
+
const existing = readConfig<Record<string, unknown>>(
|
|
376
|
+
this.opts.filename,
|
|
377
|
+
dir,
|
|
378
|
+
);
|
|
379
|
+
const unknownKeys: Record<string, unknown> = {};
|
|
380
|
+
if (existing && typeof existing === "object") {
|
|
381
|
+
for (const [key, val] of Object.entries(existing)) {
|
|
382
|
+
if (!knownKeys.has(key)) unknownKeys[key] = val;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
if (Object.keys(unknownKeys).length > 0) {
|
|
386
|
+
writeConfig(this.opts.filename, unknownKeys as T, dir);
|
|
387
|
+
} else {
|
|
388
|
+
deleteConfig(this.opts.filename, dir);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* Delete the entire config file for a scope. Unlike `resetScope`,
|
|
394
|
+
* this removes unknown keys as well — the file is completely gone.
|
|
395
|
+
* Next load will use nothing but defaults.
|
|
396
|
+
*/
|
|
397
|
+
deleteScope(
|
|
398
|
+
scope: "global" | "project",
|
|
399
|
+
cwd?: string,
|
|
400
|
+
configDir?: string,
|
|
401
|
+
): void {
|
|
402
|
+
if (scope === "project" && !cwd) {
|
|
403
|
+
throw new Error("cwd is required for project-scoped config delete");
|
|
404
|
+
}
|
|
405
|
+
const dir =
|
|
406
|
+
scope === "project" && cwd
|
|
407
|
+
? join(cwd, ".pi")
|
|
408
|
+
: (configDir ?? getExtensionsDir());
|
|
409
|
+
deleteConfig(this.opts.filename, dir);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* Check global and project-local config files for malformed JSON.
|
|
414
|
+
* Warns via ctx.ui.notify if any are found.
|
|
415
|
+
*/
|
|
416
|
+
private warnOnMalformedConfig(
|
|
417
|
+
ctx: ExtensionContext,
|
|
418
|
+
cwd: string,
|
|
419
|
+
configDir?: string,
|
|
420
|
+
): void {
|
|
421
|
+
const filename = this.opts.filename;
|
|
422
|
+
|
|
423
|
+
const globalStatus = checkConfigFile(filename, configDir);
|
|
424
|
+
if (globalStatus.exists && !globalStatus.valid) {
|
|
425
|
+
ctx.ui.notify(
|
|
426
|
+
`Config file "${filename}" is ${globalStatus.error}. Using defaults.`,
|
|
427
|
+
"warning",
|
|
428
|
+
);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
const projectDir = join(cwd, ".pi");
|
|
432
|
+
const projectStatus = checkConfigFile(filename, projectDir);
|
|
433
|
+
if (projectStatus.exists && !projectStatus.valid) {
|
|
434
|
+
ctx.ui.notify(
|
|
435
|
+
`Project config file ".pi/${filename}" is ${projectStatus.error}. Using defaults.`,
|
|
436
|
+
"warning",
|
|
437
|
+
);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
}
|