@quandev104/pi-style 0.1.1
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/CHANGELOG.md +35 -0
- package/LICENSE +21 -0
- package/README.md +193 -0
- package/dist/extensions/pi-style.js +7897 -0
- package/dist/extensions/pi-style.js.map +1 -0
- package/extension-src/pi-style/app/command-service.ts +244 -0
- package/extension-src/pi-style/app/commands.ts +12 -0
- package/extension-src/pi-style/app/config-storage.ts +98 -0
- package/extension-src/pi-style/app/doctor.ts +53 -0
- package/extension-src/pi-style/app/index.ts +322 -0
- package/extension-src/pi-style/app/providers.ts +268 -0
- package/extension-src/pi-style/app/render-scheduler.ts +48 -0
- package/extension-src/pi-style/app/runtime.ts +404 -0
- package/extension-src/pi-style/app/snapshot.ts +15 -0
- package/extension-src/pi-style/domain/capabilities.ts +34 -0
- package/extension-src/pi-style/domain/config-authorization.ts +20 -0
- package/extension-src/pi-style/domain/config-diagnostics.ts +20 -0
- package/extension-src/pi-style/domain/config-diff.ts +30 -0
- package/extension-src/pi-style/domain/config-migrations.ts +52 -0
- package/extension-src/pi-style/domain/config-normalization.ts +567 -0
- package/extension-src/pi-style/domain/config-presets.ts +51 -0
- package/extension-src/pi-style/domain/config-types.ts +115 -0
- package/extension-src/pi-style/domain/providers.ts +37 -0
- package/extension-src/pi-style/domain/status-presets.ts +60 -0
- package/extension-src/pi-style/domain/status-renderer.ts +142 -0
- package/extension-src/pi-style/domain/status.ts +430 -0
- package/extension-src/pi-style/domain/theme.ts +232 -0
- package/extension-src/pi-style/features/.gitkeep +0 -0
- package/extension-src/pi-style/features/editor/index.ts +304 -0
- package/extension-src/pi-style/features/messages/boxed-block.ts +74 -0
- package/extension-src/pi-style/features/messages/index.ts +145 -0
- package/extension-src/pi-style/features/messages/special-blocks.ts +281 -0
- package/extension-src/pi-style/features/startup/index.ts +564 -0
- package/extension-src/pi-style/features/startup/logo.ts +151 -0
- package/extension-src/pi-style/features/status-line/index.ts +319 -0
- package/extension-src/pi-style/features/tools/boxed/bash.ts +347 -0
- package/extension-src/pi-style/features/tools/boxed/edit.ts +113 -0
- package/extension-src/pi-style/features/tools/boxed/fallback.ts +67 -0
- package/extension-src/pi-style/features/tools/boxed/find.ts +65 -0
- package/extension-src/pi-style/features/tools/boxed/grep.ts +108 -0
- package/extension-src/pi-style/features/tools/boxed/index.ts +64 -0
- package/extension-src/pi-style/features/tools/boxed/ls.ts +65 -0
- package/extension-src/pi-style/features/tools/boxed/quick-edit.ts +190 -0
- package/extension-src/pi-style/features/tools/boxed/read.ts +204 -0
- package/extension-src/pi-style/features/tools/boxed/session-config.ts +45 -0
- package/extension-src/pi-style/features/tools/boxed/shared.ts +157 -0
- package/extension-src/pi-style/features/tools/boxed/write.ts +89 -0
- package/extension-src/pi-style/features/tools/index.ts +480 -0
- package/extension-src/pi-style/pi/commands.ts +17 -0
- package/extension-src/pi-style/pi/compatibility-coordinator.ts +195 -0
- package/extension-src/pi-style/pi/compatibility-probe.ts +645 -0
- package/extension-src/pi-style/pi/compatibility-registry.ts +329 -0
- package/extension-src/pi-style/pi/config-host.ts +52 -0
- package/extension-src/pi-style/pi/config-session.ts +105 -0
- package/extension-src/pi-style/pi/index.ts +77 -0
- package/extension-src/pi-style/pi/operational-state.ts +29 -0
- package/extension-src/pi-style/pi/session-coordinator.ts +187 -0
- package/extension-src/pi-style/pi/session-usage.ts +62 -0
- package/extension-src/pi-style/pi/startup-resources.ts +70 -0
- package/extension-src/pi-style/shared/.gitkeep +0 -0
- package/extension-src/pi-style/shared/ansi.ts +386 -0
- package/extension-src/pi-style/shared/box.ts +805 -0
- package/extension-src/pi-style/shared/disposable-store.ts +28 -0
- package/extension-src/pi-style/shared/elapsed.ts +88 -0
- package/extension-src/pi-style/shared/render-budget.ts +367 -0
- package/extension-src/pi-style/shared/split-diff.ts +692 -0
- package/extension-src/pi-style/shared/theme-extras.ts +292 -0
- package/package.json +68 -0
- package/themes/.gitkeep +0 -0
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import { validateConfigLayer } from "../domain/config-normalization.js";
|
|
2
|
+
import type { NormalizedPiStyleConfig } from "../domain/config-types.js";
|
|
3
|
+
import { type ConfigFilePort, type ConfigStoragePaths, writeScopedConfig } from "./config-storage.js";
|
|
4
|
+
|
|
5
|
+
export type Mutation = Record<string, unknown>;
|
|
6
|
+
export interface CommandUi {
|
|
7
|
+
select(title: string, options: string[]): Promise<string | undefined>;
|
|
8
|
+
notify(message: string, type?: "info" | "warning" | "error"): void;
|
|
9
|
+
}
|
|
10
|
+
export interface CommandHost {
|
|
11
|
+
readonly ui: CommandUi;
|
|
12
|
+
readonly cwd: string;
|
|
13
|
+
isProjectTrusted(): boolean;
|
|
14
|
+
}
|
|
15
|
+
export interface CommandApp {
|
|
16
|
+
readonly config: NormalizedPiStyleConfig;
|
|
17
|
+
applySession(patch: Mutation): void;
|
|
18
|
+
setProjectTrusted?(trusted: boolean): void;
|
|
19
|
+
reload(): Promise<void>;
|
|
20
|
+
doctor(): Readonly<Record<string, unknown>>;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const surfaces = new Set(["status", "editor", "startup", "messages", "tools"]);
|
|
24
|
+
const presets = ["default", "minimal", "compact", "full", "ascii", "native"];
|
|
25
|
+
const styles = ["compact", "boxed", "dock", "native"];
|
|
26
|
+
const frames = ["auto", "halfblock", "line", "solid", "outline", "native"];
|
|
27
|
+
const startupModes = ["off", "compact", "overlay"];
|
|
28
|
+
const allowedPaths = new Set([
|
|
29
|
+
"enabled",
|
|
30
|
+
"preset",
|
|
31
|
+
"placement",
|
|
32
|
+
"startup.mode",
|
|
33
|
+
"startup.showResources",
|
|
34
|
+
"statusLine.enabled",
|
|
35
|
+
"statusLine.separator",
|
|
36
|
+
"statusLine.layout.left",
|
|
37
|
+
"statusLine.layout.right",
|
|
38
|
+
"statusLine.layout.secondary",
|
|
39
|
+
"statusLine.disabledSegments",
|
|
40
|
+
"statusLine.customItems",
|
|
41
|
+
"statusLine.bottomMargin",
|
|
42
|
+
"statusLine.contextBarWidth",
|
|
43
|
+
"editor.enabled",
|
|
44
|
+
"editor.style",
|
|
45
|
+
"editor.frame",
|
|
46
|
+
"editor.showMetadata",
|
|
47
|
+
"messages.enabled",
|
|
48
|
+
"messages.userPrefix",
|
|
49
|
+
"messages.assistantPrefix",
|
|
50
|
+
"messages.specialBlocks",
|
|
51
|
+
"tools.enabled",
|
|
52
|
+
"tools.style",
|
|
53
|
+
"tools.maxCollapsedLines",
|
|
54
|
+
"tools.maxExpandedLines",
|
|
55
|
+
"tools.dimOutput",
|
|
56
|
+
"tools.showElapsed",
|
|
57
|
+
"theme.nerdFonts",
|
|
58
|
+
"theme.terminalBackgroundSync",
|
|
59
|
+
"theme.colors",
|
|
60
|
+
"theme.glyphs",
|
|
61
|
+
"compatibility.allowSafePatches",
|
|
62
|
+
"compatibility.allowCorePatches",
|
|
63
|
+
"compatibility.preferExistingEditor",
|
|
64
|
+
"compatibility.preferExistingFooter",
|
|
65
|
+
"debug",
|
|
66
|
+
]);
|
|
67
|
+
function validatePathValue(path: string, value: unknown): boolean {
|
|
68
|
+
if (!allowedPaths.has(path)) return false;
|
|
69
|
+
if (
|
|
70
|
+
[
|
|
71
|
+
"enabled",
|
|
72
|
+
"startup.showResources",
|
|
73
|
+
"statusLine.enabled",
|
|
74
|
+
"editor.enabled",
|
|
75
|
+
"editor.showMetadata",
|
|
76
|
+
"messages.enabled",
|
|
77
|
+
"messages.userPrefix",
|
|
78
|
+
"messages.assistantPrefix",
|
|
79
|
+
"messages.specialBlocks",
|
|
80
|
+
"tools.enabled",
|
|
81
|
+
"tools.showElapsed",
|
|
82
|
+
"tools.dimOutput",
|
|
83
|
+
"compatibility.allowSafePatches",
|
|
84
|
+
"compatibility.allowCorePatches",
|
|
85
|
+
"compatibility.preferExistingEditor",
|
|
86
|
+
"compatibility.preferExistingFooter",
|
|
87
|
+
"debug",
|
|
88
|
+
].includes(path)
|
|
89
|
+
)
|
|
90
|
+
return typeof value === "boolean";
|
|
91
|
+
if (path === "preset") return typeof value === "string" && presets.includes(value);
|
|
92
|
+
if (path === "placement") return value === "above" || value === "below";
|
|
93
|
+
if (path === "startup.mode") return typeof value === "string" && startupModes.includes(value);
|
|
94
|
+
if (path === "editor.style") return typeof value === "string" && styles.includes(value);
|
|
95
|
+
if (path === "editor.frame") return typeof value === "string" && frames.includes(value);
|
|
96
|
+
if (["theme.nerdFonts", "theme.terminalBackgroundSync", "statusLine.separator", "tools.style"].includes(path))
|
|
97
|
+
return typeof value === "string";
|
|
98
|
+
if (path === "tools.maxCollapsedLines" || path === "tools.maxExpandedLines")
|
|
99
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0;
|
|
100
|
+
if (path === "statusLine.bottomMargin" || path === "statusLine.contextBarWidth")
|
|
101
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0;
|
|
102
|
+
if (path.endsWith(".colors") || path.endsWith(".glyphs"))
|
|
103
|
+
return (
|
|
104
|
+
typeof value === "object" &&
|
|
105
|
+
value !== null &&
|
|
106
|
+
!Array.isArray(value) &&
|
|
107
|
+
Object.values(value).every((item) => typeof item === "string")
|
|
108
|
+
);
|
|
109
|
+
if (path.includes("layout.") || path === "statusLine.disabledSegments")
|
|
110
|
+
return Array.isArray(value) && value.every((item) => typeof item === "string");
|
|
111
|
+
return (
|
|
112
|
+
path === "statusLine.customItems" &&
|
|
113
|
+
validateConfigLayer({ statusLine: { customItems: value } }).diagnostics.length === 0
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
function validateMutation(patch: Mutation, prefix = ""): boolean {
|
|
117
|
+
return Object.entries(patch).every(([key, value]) => {
|
|
118
|
+
const path = prefix ? `${prefix}.${key}` : key;
|
|
119
|
+
if (
|
|
120
|
+
[
|
|
121
|
+
"theme.colors",
|
|
122
|
+
"theme.glyphs",
|
|
123
|
+
"statusLine.layout.left",
|
|
124
|
+
"statusLine.layout.right",
|
|
125
|
+
"statusLine.layout.secondary",
|
|
126
|
+
"statusLine.disabledSegments",
|
|
127
|
+
"statusLine.customItems",
|
|
128
|
+
].includes(path)
|
|
129
|
+
)
|
|
130
|
+
return validatePathValue(path, value);
|
|
131
|
+
if (typeof value === "object" && value !== null && !Array.isArray(value))
|
|
132
|
+
return Object.keys(value).length > 0 && validateMutation(value as Mutation, path);
|
|
133
|
+
return validatePathValue(path, value);
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
function sanitizeMutation(patch: Mutation): Mutation | undefined {
|
|
137
|
+
return validateMutation(patch) ? patch : undefined;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function mutation(parts: readonly string[]): Mutation | undefined {
|
|
141
|
+
if (parts[0] === "set" && parts[1] && parts[2]) {
|
|
142
|
+
try {
|
|
143
|
+
const value = JSON.parse(parts.slice(2).join(" ")) as unknown;
|
|
144
|
+
const patch: Mutation = {};
|
|
145
|
+
let cursor = patch;
|
|
146
|
+
const keys = parts[1].split(".");
|
|
147
|
+
for (const key of keys.slice(0, -1)) {
|
|
148
|
+
const next = cursor[key];
|
|
149
|
+
if (typeof next === "object" && next !== null && !Array.isArray(next)) cursor = next as Mutation;
|
|
150
|
+
else {
|
|
151
|
+
cursor[key] = {};
|
|
152
|
+
cursor = cursor[key] as Mutation;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
cursor[keys.at(-1) as string] = value;
|
|
156
|
+
return sanitizeMutation(patch);
|
|
157
|
+
} catch {
|
|
158
|
+
return undefined;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
const [action, value, extra] = parts;
|
|
162
|
+
if (action === "on" || action === "off") return { enabled: action === "on" };
|
|
163
|
+
if (action === "preset" && value && presets.includes(value)) return { preset: value };
|
|
164
|
+
if (action === "placement" && (value === "above" || value === "below")) return { placement: value };
|
|
165
|
+
if (action === "editor" && value && styles.includes(value) && (!extra || frames.includes(extra)))
|
|
166
|
+
return { editor: { style: value, ...(extra ? { frame: extra } : {}) } };
|
|
167
|
+
if (action === "startup" && value && startupModes.includes(value)) return { startup: { mode: value } };
|
|
168
|
+
if (action === "surface" && value && surfaces.has(value) && (extra === "on" || extra === "off")) {
|
|
169
|
+
if (value === "startup") return { startup: { mode: extra === "on" ? "compact" : "off" } };
|
|
170
|
+
return { [value === "status" ? "statusLine" : value]: { enabled: extra === "on" } };
|
|
171
|
+
}
|
|
172
|
+
return undefined;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export async function executePiStyleCommand(
|
|
176
|
+
args: string,
|
|
177
|
+
host: CommandHost,
|
|
178
|
+
app: CommandApp,
|
|
179
|
+
storage: { readonly port: ConfigFilePort; readonly paths: ConfigStoragePaths },
|
|
180
|
+
): Promise<void> {
|
|
181
|
+
const parts = args.trim().split(/\s+/).filter(Boolean);
|
|
182
|
+
const [action, scope, ...rest] = parts;
|
|
183
|
+
if (!action) {
|
|
184
|
+
host.ui.notify(
|
|
185
|
+
`pi-style preset ${app.config.preset}; status ${app.config.statusLine.enabled ? "on" : "off"}`,
|
|
186
|
+
"info",
|
|
187
|
+
);
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
if (action === "reload") {
|
|
191
|
+
await app.reload();
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
if (action === "doctor") {
|
|
195
|
+
host.ui.notify(JSON.stringify(app.doctor()), "info");
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
if (action === "persist") {
|
|
199
|
+
if (scope !== "global" && scope !== "project") {
|
|
200
|
+
host.ui.notify("persistence requires explicit global or project scope", "warning");
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
if (scope === "project" && !host.isProjectTrusted()) {
|
|
204
|
+
host.ui.notify("pi-style project persistence requires a trusted project", "warning");
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
const patch = mutation(rest);
|
|
208
|
+
if (!patch) {
|
|
209
|
+
host.ui.notify("invalid pi-style mutation or value", "warning");
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
const sanitized = sanitizeMutation(patch);
|
|
213
|
+
if (!sanitized) {
|
|
214
|
+
host.ui.notify("mutation contains fields that cannot be persisted", "warning");
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
try {
|
|
218
|
+
await writeScopedConfig(
|
|
219
|
+
storage.port,
|
|
220
|
+
scope === "global" ? storage.paths.globalPath : storage.paths.projectPath,
|
|
221
|
+
sanitized,
|
|
222
|
+
);
|
|
223
|
+
} catch (error) {
|
|
224
|
+
host.ui.notify(
|
|
225
|
+
`pi-style settings write failed: ${error instanceof Error ? error.message : "unknown error"}`,
|
|
226
|
+
"error",
|
|
227
|
+
);
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
app.applySession(patch);
|
|
231
|
+
if (
|
|
232
|
+
(patch.messages as Record<string, unknown> | undefined)?.enabled === true ||
|
|
233
|
+
(patch.tools as Record<string, unknown> | undefined)?.enabled === true
|
|
234
|
+
)
|
|
235
|
+
host.ui.notify("desired Tier C state stored; awaiting session authorization", "info");
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
const patch = mutation(parts);
|
|
239
|
+
if (patch) app.applySession(patch);
|
|
240
|
+
else if (action === "preset" && !scope) {
|
|
241
|
+
const selected = await host.ui.select("pi-style preset", presets);
|
|
242
|
+
if (selected) app.applySession({ preset: selected });
|
|
243
|
+
} else host.ui.notify("invalid pi-style command or value", "warning");
|
|
244
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { CommandApp, CommandHost } from "./command-service.js";
|
|
2
|
+
import { executePiStyleCommand } from "./command-service.js";
|
|
3
|
+
import type { ConfigFilePort, ConfigStoragePaths } from "./config-storage.js";
|
|
4
|
+
|
|
5
|
+
export function runCommand(
|
|
6
|
+
args: string,
|
|
7
|
+
host: CommandHost,
|
|
8
|
+
app: CommandApp,
|
|
9
|
+
storage: { readonly port: ConfigFilePort; readonly paths: ConfigStoragePaths },
|
|
10
|
+
): Promise<void> {
|
|
11
|
+
return executePiStyleCommand(args, host, app, storage);
|
|
12
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import type { ConfigDiagnostic } from "../domain/config-diagnostics.js";
|
|
2
|
+
import { migrateConfig } from "../domain/config-migrations.js";
|
|
3
|
+
|
|
4
|
+
export interface ConfigStoragePaths {
|
|
5
|
+
readonly globalPath: string;
|
|
6
|
+
readonly projectPath: string;
|
|
7
|
+
}
|
|
8
|
+
export interface ConfigFilePort {
|
|
9
|
+
read(path: string): Promise<string>;
|
|
10
|
+
writeAtomic(path: string, content: string): Promise<void>;
|
|
11
|
+
}
|
|
12
|
+
export interface ConfigRead {
|
|
13
|
+
readonly value: unknown;
|
|
14
|
+
readonly diagnostics: readonly ConfigDiagnostic[];
|
|
15
|
+
readonly readOnly: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export async function readScopedConfig(
|
|
19
|
+
port: ConfigFilePort,
|
|
20
|
+
path: string,
|
|
21
|
+
selectedNamespace = "piStyle",
|
|
22
|
+
): Promise<ConfigRead> {
|
|
23
|
+
try {
|
|
24
|
+
const parsed = JSON.parse(await port.read(path)) as unknown;
|
|
25
|
+
if (!record(parsed))
|
|
26
|
+
return {
|
|
27
|
+
value: undefined,
|
|
28
|
+
readOnly: true,
|
|
29
|
+
diagnostics: [{ code: "CFG-001", level: "warning", path, message: "settings root must be an object" }],
|
|
30
|
+
};
|
|
31
|
+
if (!Object.hasOwn(parsed, selectedNamespace)) return { value: undefined, readOnly: false, diagnostics: [] };
|
|
32
|
+
const result = migrateConfig(parsed[selectedNamespace], `${path}.${selectedNamespace}`);
|
|
33
|
+
return { value: result.config, diagnostics: result.diagnostics, readOnly: result.readOnly };
|
|
34
|
+
} catch (error) {
|
|
35
|
+
const code =
|
|
36
|
+
typeof error === "object" && error !== null && "code" in error ? (error as { code?: unknown }).code : undefined;
|
|
37
|
+
if (code === "ENOENT") return { value: undefined, readOnly: false, diagnostics: [] };
|
|
38
|
+
return {
|
|
39
|
+
value: undefined,
|
|
40
|
+
readOnly: true,
|
|
41
|
+
diagnostics: [
|
|
42
|
+
{
|
|
43
|
+
code: "CFG-IO",
|
|
44
|
+
level: "warning",
|
|
45
|
+
path,
|
|
46
|
+
message: `settings unreadable; preserved without rewrite (${error instanceof Error ? error.message : "invalid JSON"})`,
|
|
47
|
+
},
|
|
48
|
+
],
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function record(value: unknown): value is Record<string, unknown> {
|
|
54
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function mergePatch(base: unknown, patch: unknown): unknown {
|
|
58
|
+
if (!record(base) || !record(patch)) return patch;
|
|
59
|
+
const result: Record<string, unknown> = { ...base };
|
|
60
|
+
for (const [key, value] of Object.entries(patch)) result[key] = mergePatch(result[key], value);
|
|
61
|
+
return result;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const writeLocks = new Map<string, Promise<void>>();
|
|
65
|
+
|
|
66
|
+
export async function writeScopedConfig(
|
|
67
|
+
port: ConfigFilePort,
|
|
68
|
+
path: string,
|
|
69
|
+
value: unknown,
|
|
70
|
+
selectedNamespace = "piStyle",
|
|
71
|
+
): Promise<void> {
|
|
72
|
+
const previous = writeLocks.get(path) ?? Promise.resolve();
|
|
73
|
+
const operation = previous.then(async () => {
|
|
74
|
+
let document: Record<string, unknown> = {};
|
|
75
|
+
try {
|
|
76
|
+
const parsed = JSON.parse(await port.read(path)) as unknown;
|
|
77
|
+
if (!record(parsed)) throw new Error("settings root must be an object");
|
|
78
|
+
document = parsed;
|
|
79
|
+
} catch (error) {
|
|
80
|
+
const code =
|
|
81
|
+
typeof error === "object" && error !== null && "code" in error ? (error as { code?: unknown }).code : undefined;
|
|
82
|
+
if (code !== "ENOENT") throw new Error(`refusing to rewrite unreadable settings: ${path}`);
|
|
83
|
+
}
|
|
84
|
+
if (Object.hasOwn(document, selectedNamespace)) {
|
|
85
|
+
const existing = migrateConfig(document[selectedNamespace], `${path}.${selectedNamespace}`);
|
|
86
|
+
if (existing.readOnly || !record(document[selectedNamespace]))
|
|
87
|
+
throw new Error(`refusing to rewrite protected ${selectedNamespace} namespace: ${path}`);
|
|
88
|
+
}
|
|
89
|
+
const nextNamespace = mergePatch(document[selectedNamespace], value);
|
|
90
|
+
await port.writeAtomic(path, `${JSON.stringify({ ...document, [selectedNamespace]: nextNamespace }, null, 2)}\n`);
|
|
91
|
+
});
|
|
92
|
+
writeLocks.set(path, operation);
|
|
93
|
+
try {
|
|
94
|
+
await operation;
|
|
95
|
+
} finally {
|
|
96
|
+
if (writeLocks.get(path) === operation) writeLocks.delete(path);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { ConfigDiagnostic } from "../domain/config-diagnostics.js";
|
|
2
|
+
import type { NormalizedPiStyleConfig } from "../domain/config-types.js";
|
|
3
|
+
|
|
4
|
+
export interface DoctorOperationalState {
|
|
5
|
+
readonly compatibility?: Readonly<Record<string, unknown>>;
|
|
6
|
+
readonly provider?: Readonly<Record<string, unknown>>;
|
|
7
|
+
readonly installations?: Readonly<Record<string, unknown>>;
|
|
8
|
+
readonly authorization?: Readonly<Record<string, unknown>>;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface DoctorState {
|
|
12
|
+
readonly config: NormalizedPiStyleConfig;
|
|
13
|
+
readonly diagnostics: readonly ConfigDiagnostic[];
|
|
14
|
+
readonly sources?: Readonly<Record<string, string>>;
|
|
15
|
+
readonly surfaces: Readonly<Record<string, unknown>>;
|
|
16
|
+
readonly piVersion?: string;
|
|
17
|
+
readonly compatibility?: string;
|
|
18
|
+
readonly operational?: DoctorOperationalState;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function createDoctor(state: DoctorState): Readonly<Record<string, unknown>> {
|
|
22
|
+
return Object.freeze({
|
|
23
|
+
config: Object.freeze({
|
|
24
|
+
preset: state.config.preset,
|
|
25
|
+
enabled: state.config.enabled,
|
|
26
|
+
placement: state.config.placement,
|
|
27
|
+
statusLine: state.config.statusLine.enabled ? "enabled" : "disabled",
|
|
28
|
+
editor: state.config.editor.enabled ? "enabled" : "disabled",
|
|
29
|
+
startup: state.config.startup.mode,
|
|
30
|
+
}),
|
|
31
|
+
diagnostics: state.diagnostics,
|
|
32
|
+
sources: state.sources ?? {},
|
|
33
|
+
surfaces: state.surfaces,
|
|
34
|
+
...(state.piVersion ? { piVersion: state.piVersion } : {}),
|
|
35
|
+
...(state.operational?.compatibility && typeof state.operational.compatibility.piVersion === "string"
|
|
36
|
+
? { piVersion: state.operational.compatibility.piVersion }
|
|
37
|
+
: {}),
|
|
38
|
+
...(state.operational?.compatibility && typeof state.operational.compatibility.versionRange === "string"
|
|
39
|
+
? { piVersionRange: state.operational.compatibility.versionRange }
|
|
40
|
+
: {}),
|
|
41
|
+
...(state.compatibility ? { compatibility: state.compatibility } : {}),
|
|
42
|
+
...(state.operational
|
|
43
|
+
? {
|
|
44
|
+
operational: Object.freeze({
|
|
45
|
+
...(state.operational.compatibility ? { compatibility: state.operational.compatibility } : {}),
|
|
46
|
+
...(state.operational.provider ? { provider: state.operational.provider } : {}),
|
|
47
|
+
...(state.operational.installations ? { installations: state.operational.installations } : {}),
|
|
48
|
+
...(state.operational.authorization ? { authorization: state.operational.authorization } : {}),
|
|
49
|
+
}),
|
|
50
|
+
}
|
|
51
|
+
: {}),
|
|
52
|
+
});
|
|
53
|
+
}
|