@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.
Files changed (69) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/LICENSE +21 -0
  3. package/README.md +193 -0
  4. package/dist/extensions/pi-style.js +7897 -0
  5. package/dist/extensions/pi-style.js.map +1 -0
  6. package/extension-src/pi-style/app/command-service.ts +244 -0
  7. package/extension-src/pi-style/app/commands.ts +12 -0
  8. package/extension-src/pi-style/app/config-storage.ts +98 -0
  9. package/extension-src/pi-style/app/doctor.ts +53 -0
  10. package/extension-src/pi-style/app/index.ts +322 -0
  11. package/extension-src/pi-style/app/providers.ts +268 -0
  12. package/extension-src/pi-style/app/render-scheduler.ts +48 -0
  13. package/extension-src/pi-style/app/runtime.ts +404 -0
  14. package/extension-src/pi-style/app/snapshot.ts +15 -0
  15. package/extension-src/pi-style/domain/capabilities.ts +34 -0
  16. package/extension-src/pi-style/domain/config-authorization.ts +20 -0
  17. package/extension-src/pi-style/domain/config-diagnostics.ts +20 -0
  18. package/extension-src/pi-style/domain/config-diff.ts +30 -0
  19. package/extension-src/pi-style/domain/config-migrations.ts +52 -0
  20. package/extension-src/pi-style/domain/config-normalization.ts +567 -0
  21. package/extension-src/pi-style/domain/config-presets.ts +51 -0
  22. package/extension-src/pi-style/domain/config-types.ts +115 -0
  23. package/extension-src/pi-style/domain/providers.ts +37 -0
  24. package/extension-src/pi-style/domain/status-presets.ts +60 -0
  25. package/extension-src/pi-style/domain/status-renderer.ts +142 -0
  26. package/extension-src/pi-style/domain/status.ts +430 -0
  27. package/extension-src/pi-style/domain/theme.ts +232 -0
  28. package/extension-src/pi-style/features/.gitkeep +0 -0
  29. package/extension-src/pi-style/features/editor/index.ts +304 -0
  30. package/extension-src/pi-style/features/messages/boxed-block.ts +74 -0
  31. package/extension-src/pi-style/features/messages/index.ts +145 -0
  32. package/extension-src/pi-style/features/messages/special-blocks.ts +281 -0
  33. package/extension-src/pi-style/features/startup/index.ts +564 -0
  34. package/extension-src/pi-style/features/startup/logo.ts +151 -0
  35. package/extension-src/pi-style/features/status-line/index.ts +319 -0
  36. package/extension-src/pi-style/features/tools/boxed/bash.ts +347 -0
  37. package/extension-src/pi-style/features/tools/boxed/edit.ts +113 -0
  38. package/extension-src/pi-style/features/tools/boxed/fallback.ts +67 -0
  39. package/extension-src/pi-style/features/tools/boxed/find.ts +65 -0
  40. package/extension-src/pi-style/features/tools/boxed/grep.ts +108 -0
  41. package/extension-src/pi-style/features/tools/boxed/index.ts +64 -0
  42. package/extension-src/pi-style/features/tools/boxed/ls.ts +65 -0
  43. package/extension-src/pi-style/features/tools/boxed/quick-edit.ts +190 -0
  44. package/extension-src/pi-style/features/tools/boxed/read.ts +204 -0
  45. package/extension-src/pi-style/features/tools/boxed/session-config.ts +45 -0
  46. package/extension-src/pi-style/features/tools/boxed/shared.ts +157 -0
  47. package/extension-src/pi-style/features/tools/boxed/write.ts +89 -0
  48. package/extension-src/pi-style/features/tools/index.ts +480 -0
  49. package/extension-src/pi-style/pi/commands.ts +17 -0
  50. package/extension-src/pi-style/pi/compatibility-coordinator.ts +195 -0
  51. package/extension-src/pi-style/pi/compatibility-probe.ts +645 -0
  52. package/extension-src/pi-style/pi/compatibility-registry.ts +329 -0
  53. package/extension-src/pi-style/pi/config-host.ts +52 -0
  54. package/extension-src/pi-style/pi/config-session.ts +105 -0
  55. package/extension-src/pi-style/pi/index.ts +77 -0
  56. package/extension-src/pi-style/pi/operational-state.ts +29 -0
  57. package/extension-src/pi-style/pi/session-coordinator.ts +187 -0
  58. package/extension-src/pi-style/pi/session-usage.ts +62 -0
  59. package/extension-src/pi-style/pi/startup-resources.ts +70 -0
  60. package/extension-src/pi-style/shared/.gitkeep +0 -0
  61. package/extension-src/pi-style/shared/ansi.ts +386 -0
  62. package/extension-src/pi-style/shared/box.ts +805 -0
  63. package/extension-src/pi-style/shared/disposable-store.ts +28 -0
  64. package/extension-src/pi-style/shared/elapsed.ts +88 -0
  65. package/extension-src/pi-style/shared/render-budget.ts +367 -0
  66. package/extension-src/pi-style/shared/split-diff.ts +692 -0
  67. package/extension-src/pi-style/shared/theme-extras.ts +292 -0
  68. package/package.json +68 -0
  69. package/themes/.gitkeep +0 -0
@@ -0,0 +1,404 @@
1
+ import type { ExtensionContext, ExtensionUIContext } from "@earendil-works/pi-coding-agent";
2
+ import { diffConfig } from "../domain/config-diff.js";
3
+ import type { NormalizedPiStyleConfig } from "../domain/config-types.js";
4
+ import type { GitCommandRunner } from "../domain/providers.js";
5
+ import type { StatusSnapshot } from "../domain/status.js";
6
+ import { normalizeThinkingLevel } from "../domain/status.js";
7
+ import { installEditor } from "../features/editor/index.js";
8
+ import {
9
+ installStartup,
10
+ type StartupHost,
11
+ type StartupReason,
12
+ type StartupResources,
13
+ type StartupSnapshot,
14
+ } from "../features/startup/index.js";
15
+ import { installStatusLine } from "../features/status-line/index.js";
16
+ import { DisposableStore } from "../shared/disposable-store.js";
17
+ import { CachedGitProvider, InMemoryContextProvider, InMemoryUsageProvider } from "./providers.js";
18
+ import { RenderScheduler } from "./render-scheduler.js";
19
+ import { createSnapshot, replaceSnapshot, type UiSnapshot } from "./snapshot.js";
20
+
21
+ export interface RuntimeInstallationState {
22
+ readonly status: "installed" | "disabled" | "failed";
23
+ readonly editor: "installed" | "preserved" | "disabled" | "failed";
24
+ readonly startup: "installed" | "disabled" | "failed";
25
+ }
26
+
27
+ export interface PiStyleRuntime {
28
+ generation: number;
29
+ readonly providerIdentity: { git: object; context: object; usage: object };
30
+ readonly installationState: RuntimeInstallationState;
31
+ mode: ExtensionContext["mode"];
32
+ hasUI: boolean;
33
+ disposed: boolean;
34
+ snapshot: UiSnapshot;
35
+ disposables: DisposableStore;
36
+ scheduler: RenderScheduler;
37
+ update(values: StatusSnapshot): void;
38
+ updateStartupResources(resources: StartupResources): void;
39
+ dismissStartup(): void;
40
+ invalidateGit(): void;
41
+ configure(config: NormalizedPiStyleConfig): void;
42
+ dispose(): void;
43
+ }
44
+
45
+ export interface RuntimeHost {
46
+ readonly mode: ExtensionContext["mode"];
47
+ readonly hasUI: boolean;
48
+ readonly ui?: ExtensionUIContext;
49
+ readonly cwd?: string;
50
+ readonly model?: { id?: string; name?: string; provider?: string; reasoning?: boolean } | undefined;
51
+ readonly thinkingLevel?: string | undefined;
52
+ readonly config: NormalizedPiStyleConfig;
53
+ readonly startupReason?: StartupReason;
54
+ readonly provider?: string;
55
+ readonly requestRender?: () => void;
56
+ readonly resources?: StartupResources;
57
+ readonly extensionStatusProvider?: () => readonly import("../domain/status.js").ExtensionStatus[] | undefined;
58
+ readonly gitRunner?: GitCommandRunner;
59
+ getContextUsage?:
60
+ | (() => { tokens: number | null; contextWindow: number; percent: number | null } | undefined)
61
+ | undefined;
62
+ }
63
+
64
+ export function createPiStyleRuntime(
65
+ host: RuntimeHost,
66
+ generation: number,
67
+ requestRender: () => void = () => {},
68
+ ): PiStyleRuntime {
69
+ let disposed = false;
70
+ const extensionStatuses = (): readonly import("../domain/status.js").ExtensionStatus[] | undefined => {
71
+ if (!host.extensionStatusProvider) return undefined;
72
+ try {
73
+ return host.extensionStatusProvider();
74
+ } catch {
75
+ host.ui?.notify?.("pi-style extension statuses unavailable; provider recovery required", "warning");
76
+ return [];
77
+ }
78
+ };
79
+ let currentConfig = host.config;
80
+ const disposables = new DisposableStore();
81
+ const scheduler = new RenderScheduler({ requestRender }, generation, () => !disposed);
82
+ const git = new CachedGitProvider(host.gitRunner);
83
+ const contextProvider = new InMemoryContextProvider();
84
+ const usageProvider = new InMemoryUsageProvider();
85
+ const usage = host.getContextUsage?.();
86
+ const initialStatuses = extensionStatuses();
87
+ const initialValues = {
88
+ ...(initialStatuses ? { extensionStatuses: initialStatuses } : {}),
89
+ ...(host.model?.name || host.model?.id ? { model: host.model.name ?? host.model.id } : {}),
90
+ ...(host.model?.provider ? { provider: host.model.provider } : {}),
91
+ ...(host.model?.reasoning !== undefined ? { reasoning: host.model.reasoning } : {}),
92
+ ...(host.thinkingLevel ? { thinkingLevel: normalizeThinkingLevel(host.thinkingLevel) } : {}),
93
+ ...(host.cwd ? { cwd: host.cwd } : {}),
94
+ ...(usage
95
+ ? {
96
+ context: {
97
+ ...(usage.tokens !== null ? { currentTokens: usage.tokens } : {}),
98
+ windowTokens: usage.contextWindow,
99
+ ...(usage.percent !== null ? { percent: usage.percent } : {}),
100
+ },
101
+ }
102
+ : {}),
103
+ };
104
+ let currentSnapshot = createSnapshot(generation, 0, initialValues);
105
+ let statusLine: ReturnType<typeof installStatusLine> | undefined;
106
+ let editor: ReturnType<typeof installEditor> | undefined;
107
+ let startup: ReturnType<typeof installStartup> | undefined;
108
+ let installationState: RuntimeInstallationState = {
109
+ status: "disabled",
110
+ editor: "disabled",
111
+ startup: "disabled",
112
+ };
113
+ const startupSnapshot = (config: NormalizedPiStyleConfig): StartupSnapshot => ({
114
+ ...currentSnapshot,
115
+ reason: host.startupReason ?? "startup",
116
+ ...(host.provider ? { startupProvider: host.provider } : {}),
117
+ ...(host.cwd ? { project: host.cwd.split(/[\\/]/).filter(Boolean).at(-1) } : {}),
118
+ preset: config.preset,
119
+ ...(host.resources ? { resources: host.resources } : {}),
120
+ });
121
+ const installStatus = () => {
122
+ if (!host.hasUI || host.mode !== "tui" || !host.ui || !currentConfig.enabled || !currentConfig.statusLine.enabled) {
123
+ installationState = { ...installationState, status: "disabled" };
124
+ return;
125
+ }
126
+ try {
127
+ statusLine = installStatusLine({
128
+ host: host.ui,
129
+ config: currentConfig,
130
+ generation,
131
+ initialSnapshot: currentSnapshot,
132
+ isCurrent: () => !disposed,
133
+ clearOnStartup: host.startupReason === "startup",
134
+ });
135
+ if (statusLine) {
136
+ disposables.add(statusLine);
137
+ installationState = { ...installationState, status: "installed" };
138
+ } else installationState = { ...installationState, status: "failed" };
139
+ } catch (error) {
140
+ host.ui.notify?.(
141
+ `pi-style status unavailable: ${error instanceof Error ? error.message : "installation failed"}`,
142
+ "warning",
143
+ );
144
+ statusLine = undefined;
145
+ installationState = { ...installationState, status: "failed" };
146
+ }
147
+ };
148
+ const installEditorFeature = () => {
149
+ if (!host.hasUI || host.mode !== "tui" || !host.ui || !currentConfig.enabled || !currentConfig.editor.enabled) {
150
+ installationState = { ...installationState, editor: "disabled" };
151
+ return;
152
+ }
153
+ try {
154
+ editor = installEditor({
155
+ host: host.ui,
156
+ config: currentConfig,
157
+ generation,
158
+ initialSnapshot: currentSnapshot,
159
+ isCurrent: () => !disposed,
160
+ });
161
+ if (editor) {
162
+ disposables.add(editor);
163
+ installationState = { ...installationState, editor: editor.preservedPrevious ? "preserved" : "installed" };
164
+ } else installationState = { ...installationState, editor: "failed" };
165
+ } catch (error) {
166
+ host.ui.notify?.(
167
+ `pi-style editor unavailable: ${error instanceof Error ? error.message : "installation failed"}`,
168
+ "warning",
169
+ );
170
+ editor = undefined;
171
+ installationState = { ...installationState, editor: "failed" };
172
+ }
173
+ };
174
+ const installStartupFeature = () => {
175
+ if (
176
+ !host.hasUI ||
177
+ host.mode !== "tui" ||
178
+ !host.ui ||
179
+ !currentConfig.enabled ||
180
+ currentConfig.startup.mode === "off"
181
+ ) {
182
+ installationState = { ...installationState, startup: "disabled" };
183
+ return;
184
+ }
185
+ try {
186
+ startup = installStartup({
187
+ host: { ...(host.ui as unknown as StartupHost), mode: host.mode, hasUI: host.hasUI },
188
+ config: currentConfig,
189
+ snapshot: startupSnapshot(currentConfig),
190
+ generation,
191
+ requestRender,
192
+ timeoutMs: 3000,
193
+ isCurrent: () => !disposed,
194
+ });
195
+ if (startup) {
196
+ disposables.add(startup);
197
+ installationState = { ...installationState, startup: "installed" };
198
+ } else installationState = { ...installationState, startup: "failed" };
199
+ } catch (error) {
200
+ host.ui.notify?.(
201
+ `pi-style startup unavailable: ${error instanceof Error ? error.message : "installation failed"}`,
202
+ "warning",
203
+ );
204
+ startup = undefined;
205
+ installationState = { ...installationState, startup: "failed" };
206
+ }
207
+ };
208
+ const disposeFeature = (label: string, dispose: (() => void) | undefined): void => {
209
+ if (!dispose) return;
210
+ try {
211
+ dispose();
212
+ } catch (error) {
213
+ host.ui?.notify?.(`pi-style ${label} cleanup failed; preserving the current owner`, "warning");
214
+ void error;
215
+ }
216
+ };
217
+ const disposeStatus = () => {
218
+ disposeFeature("status", statusLine?.dispose);
219
+ statusLine = undefined;
220
+ installationState = { ...installationState, status: "disabled" };
221
+ };
222
+ const disposeEditor = () => {
223
+ disposeFeature("editor", editor?.dispose);
224
+ editor = undefined;
225
+ installationState = { ...installationState, editor: "disabled" };
226
+ };
227
+ const disposeStartup = () => {
228
+ disposeFeature("startup", startup?.dispose);
229
+ startup = undefined;
230
+ installationState = { ...installationState, startup: "disabled" };
231
+ };
232
+ installStatus();
233
+ installEditorFeature();
234
+ installStartupFeature();
235
+ if (host.cwd && currentConfig.enabled && currentConfig.statusLine.enabled) {
236
+ void git.get(host.cwd).then((value) => {
237
+ if (disposed) return;
238
+ currentSnapshot = replaceSnapshot(currentSnapshot, generation, { ...currentSnapshot, git: value });
239
+ statusLine?.update(currentSnapshot);
240
+ editor?.update(currentSnapshot);
241
+ startup?.update({
242
+ ...currentSnapshot,
243
+ reason: host.startupReason ?? "startup",
244
+ ...(host.provider ? { startupProvider: host.provider } : {}),
245
+ ...(host.cwd ? { project: host.cwd.split(/[\\/]/).filter(Boolean).at(-1) } : {}),
246
+ preset: currentConfig.preset,
247
+ ...(host.resources ? { resources: host.resources } : {}),
248
+ });
249
+ requestRender();
250
+ });
251
+ }
252
+ if (usage) {
253
+ contextProvider.set("active", {
254
+ ...(usage.tokens !== null ? { currentTokens: usage.tokens } : {}),
255
+ windowTokens: usage.contextWindow,
256
+ ...(usage.percent !== null ? { percent: usage.percent } : {}),
257
+ });
258
+ }
259
+ return {
260
+ generation,
261
+ providerIdentity: { git, context: contextProvider, usage: usageProvider },
262
+ get installationState() {
263
+ return installationState;
264
+ },
265
+ mode: host.mode,
266
+ hasUI: host.hasUI,
267
+ snapshot: currentSnapshot,
268
+ disposables,
269
+ scheduler,
270
+ updateStartupResources(resources) {
271
+ if (disposed) return;
272
+ startup?.update({
273
+ ...currentSnapshot,
274
+ reason: host.startupReason ?? "startup",
275
+ ...(host.provider ? { startupProvider: host.provider } : {}),
276
+ ...(host.cwd ? { project: host.cwd.split(/[\\/]/).filter(Boolean).at(-1) } : {}),
277
+ preset: currentConfig.preset,
278
+ resources,
279
+ });
280
+ requestRender();
281
+ },
282
+ dismissStartup() {
283
+ startup?.dismiss();
284
+ },
285
+ update(values) {
286
+ if (disposed) return;
287
+ const liveUsage = host.getContextUsage?.();
288
+ const context = liveUsage
289
+ ? {
290
+ ...(liveUsage.tokens !== null ? { currentTokens: liveUsage.tokens } : {}),
291
+ windowTokens: liveUsage.contextWindow,
292
+ ...(liveUsage.percent !== null ? { percent: liveUsage.percent } : {}),
293
+ }
294
+ : undefined;
295
+ const statuses = extensionStatuses();
296
+ currentSnapshot = replaceSnapshot(currentSnapshot, generation, {
297
+ ...currentSnapshot,
298
+ ...values,
299
+ ...(context ? { context } : {}),
300
+ ...(statuses ? { extensionStatuses: statuses } : {}),
301
+ });
302
+ statusLine?.update(currentSnapshot);
303
+ editor?.update(currentSnapshot);
304
+ startup?.update({
305
+ ...currentSnapshot,
306
+ reason: host.startupReason ?? "startup",
307
+ ...(host.provider ? { startupProvider: host.provider } : {}),
308
+ ...(host.cwd ? { project: host.cwd.split(/[\\/]/).filter(Boolean).at(-1) } : {}),
309
+ preset: currentConfig.preset,
310
+ ...(host.resources ? { resources: host.resources } : {}),
311
+ });
312
+ },
313
+ configure(nextConfig) {
314
+ if (disposed) return;
315
+ const previous = currentConfig;
316
+ const impactPlan = diffConfig(previous, nextConfig);
317
+ if (impactPlan.impacts.length === 0) return;
318
+ currentConfig = nextConfig;
319
+ const statusChanged =
320
+ JSON.stringify(previous.statusLine) !== JSON.stringify(nextConfig.statusLine) ||
321
+ previous.placement !== nextConfig.placement ||
322
+ previous.enabled !== nextConfig.enabled;
323
+ const editorChanged =
324
+ JSON.stringify(previous.editor) !== JSON.stringify(nextConfig.editor) ||
325
+ previous.enabled !== nextConfig.enabled;
326
+ const startupChanged =
327
+ JSON.stringify(previous.startup) !== JSON.stringify(nextConfig.startup) ||
328
+ previous.enabled !== nextConfig.enabled;
329
+ if (statusChanged) {
330
+ disposeStatus();
331
+ currentConfig = nextConfig;
332
+ installStatus();
333
+ }
334
+ if (editorChanged) {
335
+ disposeEditor();
336
+ currentConfig = nextConfig;
337
+ installEditorFeature();
338
+ }
339
+ if (startupChanged) {
340
+ disposeStartup();
341
+ currentConfig = nextConfig;
342
+ installStartupFeature();
343
+ }
344
+ statusLine?.configure(nextConfig);
345
+ editor?.configure(nextConfig);
346
+ startup?.configure(nextConfig);
347
+ },
348
+ invalidateGit() {
349
+ if (disposed || !host.cwd || !currentConfig.enabled || !currentConfig.statusLine.enabled) return;
350
+ git.invalidate(host.cwd);
351
+ void git.get(host.cwd).then((value) => {
352
+ if (disposed) return;
353
+ currentSnapshot = replaceSnapshot(currentSnapshot, generation, { ...currentSnapshot, git: value });
354
+ statusLine?.update(currentSnapshot);
355
+ editor?.update(currentSnapshot);
356
+ startup?.update({
357
+ ...currentSnapshot,
358
+ reason: host.startupReason ?? "startup",
359
+ ...(host.provider ? { startupProvider: host.provider } : {}),
360
+ ...(host.cwd ? { project: host.cwd.split(/[\\/]/).filter(Boolean).at(-1) } : {}),
361
+ preset: currentConfig.preset,
362
+ });
363
+ requestRender();
364
+ });
365
+ },
366
+ get disposed() {
367
+ return disposed;
368
+ },
369
+ dispose() {
370
+ if (disposed) return;
371
+ disposed = true;
372
+ scheduler.cancel();
373
+ // UI surfaces are restored synchronously so teardown is deterministic;
374
+ // the store then disposes the (already disposed) feature instances idempotently.
375
+ disposeStatus();
376
+ disposeEditor();
377
+ disposeStartup();
378
+ git.dispose();
379
+ contextProvider.clear();
380
+ usageProvider.reset();
381
+ void disposables.dispose();
382
+ },
383
+ };
384
+ }
385
+
386
+ export class PiStyleRuntimeController {
387
+ private activeRuntime: PiStyleRuntime | undefined;
388
+ private nextGeneration = 0;
389
+
390
+ start(ctx: RuntimeHost): PiStyleRuntime {
391
+ this.stop();
392
+ this.activeRuntime = createPiStyleRuntime(ctx, ++this.nextGeneration);
393
+ return this.activeRuntime;
394
+ }
395
+
396
+ stop(): void {
397
+ this.activeRuntime?.dispose();
398
+ this.activeRuntime = undefined;
399
+ }
400
+
401
+ get current(): PiStyleRuntime | undefined {
402
+ return this.activeRuntime;
403
+ }
404
+ }
@@ -0,0 +1,15 @@
1
+ import type { StatusSnapshot } from "../domain/status.js";
2
+
3
+ export interface UiSnapshot extends StatusSnapshot {
4
+ readonly revision: number;
5
+ readonly generation: number;
6
+ }
7
+
8
+ export function createSnapshot(generation: number, revision = 0, values: StatusSnapshot = {}): UiSnapshot {
9
+ return Object.freeze({ generation, revision, ...values });
10
+ }
11
+
12
+ export function replaceSnapshot(current: UiSnapshot, generation: number, values: StatusSnapshot): UiSnapshot {
13
+ if (current.generation !== generation) return current;
14
+ return createSnapshot(generation, current.revision + 1, values);
15
+ }
@@ -0,0 +1,34 @@
1
+ export type DiagnosticClass = "capability" | "configuration" | "conflict" | "fallback" | "error";
2
+ export interface CapabilityRecord {
3
+ readonly name: string;
4
+ readonly available: boolean;
5
+ readonly tier: "A" | "B" | "C";
6
+ readonly detail?: string;
7
+ }
8
+ export interface DiagnosticRecord {
9
+ readonly classification: DiagnosticClass;
10
+ readonly code: string;
11
+ readonly message: string;
12
+ readonly secretFree: true;
13
+ }
14
+ export interface CapabilityHost {
15
+ ui?: Record<string, unknown>;
16
+ mode?: string;
17
+ hasUI?: boolean;
18
+ }
19
+ export function detectCapabilities(host: CapabilityHost): readonly CapabilityRecord[] {
20
+ const ui = host.ui ?? {};
21
+ return Object.freeze(
22
+ ["setWidget", "setHeader", "setEditorComponent", "setFooter", "setWorkingIndicator"].map((name) =>
23
+ Object.freeze({ name, available: typeof ui[name] === "function", tier: "A" as const }),
24
+ ),
25
+ );
26
+ }
27
+ export function diagnostic(classification: DiagnosticClass, code: string, message: string): DiagnosticRecord {
28
+ return Object.freeze({
29
+ classification,
30
+ code,
31
+ message: message.replace(/(api[_-]?key|token|secret|password)\s*[:=]\s*\S+/gi, "$1=[redacted]"),
32
+ secretFree: true,
33
+ });
34
+ }
@@ -0,0 +1,20 @@
1
+ import type { NormalizedPiStyleConfig } from "./config-types.js";
2
+
3
+ export interface TierCAuthorizationInput {
4
+ readonly certifiedHost: boolean;
5
+ readonly coreFlag: boolean;
6
+ readonly surfaceFlag: boolean;
7
+ readonly surface: "messages" | "tools" | "userMessage" | "assistantMessage" | "specialBlocks";
8
+ readonly config: NormalizedPiStyleConfig;
9
+ }
10
+
11
+ /** Persisted and ordinary command state is intentionally not an input: authorization is session-bound. */
12
+ export function isTierCAuthorized(input: TierCAuthorizationInput): boolean {
13
+ if (!input.certifiedHost || !input.coreFlag || !input.surfaceFlag || !input.config.enabled) return false;
14
+ if (input.surface === "tools") return input.config.tools.enabled;
15
+ if (input.surface === "userMessage") return input.config.messages.enabled && input.config.messages.userPrefix;
16
+ if (input.surface === "assistantMessage")
17
+ return input.config.messages.enabled && input.config.messages.assistantPrefix;
18
+ if (input.surface === "specialBlocks") return input.config.messages.enabled && input.config.messages.specialBlocks;
19
+ return input.config.messages.enabled;
20
+ }
@@ -0,0 +1,20 @@
1
+ export type ConfigDiagnosticLevel = "warning" | "error";
2
+ export interface ConfigDiagnostic {
3
+ readonly code: string;
4
+ readonly level: ConfigDiagnosticLevel;
5
+ readonly path: string;
6
+ readonly message: string;
7
+ }
8
+
9
+ export function boundedDiagnostics(items: readonly ConfigDiagnostic[], limit = 32): readonly ConfigDiagnostic[] {
10
+ const seen = new Set<string>();
11
+ const result: ConfigDiagnostic[] = [];
12
+ for (const item of items) {
13
+ const key = `${item.level}:${item.code}:${item.path}:${item.message}`;
14
+ if (seen.has(key)) continue;
15
+ seen.add(key);
16
+ result.push(Object.freeze(item));
17
+ if (result.length >= limit) break;
18
+ }
19
+ return Object.freeze(result);
20
+ }
@@ -0,0 +1,30 @@
1
+ import type { NormalizedPiStyleConfig } from "./config-types.js";
2
+
3
+ export type ConfigImpact = "none" | "status" | "editor" | "startup" | "compatibility" | "all";
4
+
5
+ export interface ConfigDiff {
6
+ readonly changed: readonly string[];
7
+ readonly impacts: readonly ConfigImpact[];
8
+ }
9
+
10
+ export function diffConfig(previous: NormalizedPiStyleConfig, next: NormalizedPiStyleConfig): ConfigDiff {
11
+ const changed: string[] = [];
12
+ const impacts = new Set<ConfigImpact>();
13
+ const compare = (key: string, before: unknown, after: unknown, impact: ConfigImpact) => {
14
+ if (JSON.stringify(before) === JSON.stringify(after)) return;
15
+ changed.push(key);
16
+ impacts.add(impact);
17
+ };
18
+ compare("enabled", previous.enabled, next.enabled, "all");
19
+ compare("statusLine", previous.statusLine, next.statusLine, "status");
20
+ compare("placement", previous.placement, next.placement, "status");
21
+ compare("editor", previous.editor, next.editor, "editor");
22
+ compare("startup", previous.startup, next.startup, "startup");
23
+ compare("messages", previous.messages, next.messages, "compatibility");
24
+ compare("tools", previous.tools, next.tools, "compatibility");
25
+ compare("theme", previous.theme, next.theme, "all");
26
+ compare("compatibility", previous.compatibility, next.compatibility, "compatibility");
27
+ if (impacts.has("all"))
28
+ return Object.freeze({ changed: Object.freeze(changed), impacts: Object.freeze(["all"] as const) });
29
+ return Object.freeze({ changed: Object.freeze(changed), impacts: Object.freeze([...impacts]) });
30
+ }
@@ -0,0 +1,52 @@
1
+ import { boundedDiagnostics, type ConfigDiagnostic } from "./config-diagnostics.js";
2
+ import { PI_STYLE_SCHEMA_VERSION } from "./config-types.js";
3
+
4
+ function record(value: unknown): value is Record<string, unknown> {
5
+ return typeof value === "object" && value !== null && !Array.isArray(value);
6
+ }
7
+
8
+ export interface MigrationResult {
9
+ readonly config: Record<string, unknown> | undefined;
10
+ readonly diagnostics: readonly ConfigDiagnostic[];
11
+ readonly readOnly: boolean;
12
+ }
13
+
14
+ /** Purely validates the durable envelope; v1 is identity and future data is read-only. */
15
+ export function migrateConfig(input: unknown, source = "config"): MigrationResult {
16
+ if (!record(input))
17
+ return {
18
+ config: undefined,
19
+ readOnly: false,
20
+ diagnostics: boundedDiagnostics([
21
+ { code: "CFG-001", level: "warning", path: source, message: "configuration must be an object; defaults used" },
22
+ ]),
23
+ };
24
+ const version = input.schemaVersion;
25
+ if (version === undefined)
26
+ return {
27
+ config: { ...input, schemaVersion: PI_STYLE_SCHEMA_VERSION },
28
+ readOnly: false,
29
+ diagnostics: boundedDiagnostics([
30
+ {
31
+ code: "CFG-002",
32
+ level: "warning",
33
+ path: `${source}.schemaVersion`,
34
+ message: "missing schemaVersion accepted as v1-shaped input",
35
+ },
36
+ ]),
37
+ };
38
+ if (version !== PI_STYLE_SCHEMA_VERSION)
39
+ return {
40
+ config: undefined,
41
+ readOnly: true,
42
+ diagnostics: boundedDiagnostics([
43
+ {
44
+ code: "CFG-003",
45
+ level: "error",
46
+ path: `${source}.schemaVersion`,
47
+ message: `unsupported schemaVersion ${String(version)}; ignored without rewrite`,
48
+ },
49
+ ]),
50
+ };
51
+ return { config: { ...input }, readOnly: false, diagnostics: [] };
52
+ }