@timurproko/a1 0.2.7-dev.746 → 0.2.7-dev.767
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/bin/tab-launch-context.d.ts +6 -1
- package/bin/tab-launch-context.js +11 -2
- package/bin/ui.js +7 -3
- package/dist/app/session-shell/session-shell-root.d.ts +23 -0
- package/dist/app/session-shell/session-shell.js +112 -6
- package/dist/composition/owned-ui.d.ts +4 -1
- package/dist/composition/owned-ui.js +54 -8
- package/dist/contracts/owned-ui/session-backend.d.ts +1 -0
- package/dist/features/owned-ui/settings-app.js +8 -35
- package/dist/foundation/release/bootstrap.d.ts +1 -1
- package/dist/foundation/release/bootstrap.js +15 -4
- package/dist/foundation/release/dependency-certification.js +54 -19
- package/dist/foundation/release/progress-palette.generated.d.ts +2 -2
- package/dist/foundation/release/progress-palette.generated.js +2 -2
- package/dist/foundation/resident-tabs/bridge-client.d.ts +7 -2
- package/dist/foundation/resident-tabs/bridge-client.js +58 -16
- package/dist/foundation/resident-tabs/draft-checkpoint.d.ts +27 -0
- package/dist/foundation/resident-tabs/draft-checkpoint.js +145 -0
- package/dist/foundation/resident-tabs/ensure-terminal-host.d.ts +2 -0
- package/dist/foundation/resident-tabs/ensure-terminal-host.js +7 -1
- package/dist/foundation/resident-tabs/fault-injection.d.ts +8 -0
- package/dist/foundation/resident-tabs/fault-injection.js +26 -0
- package/dist/foundation/resident-tabs/index.d.ts +2 -1
- package/dist/foundation/resident-tabs/index.js +1 -1
- package/dist/foundation/resident-tabs/prompt-journal.d.ts +65 -0
- package/dist/foundation/resident-tabs/prompt-journal.js +214 -0
- package/dist/foundation/resident-tabs/recovery.d.ts +26 -0
- package/dist/foundation/resident-tabs/recovery.js +205 -0
- package/dist/foundation/resident-tabs/session-sync.d.ts +14 -0
- package/dist/foundation/resident-tabs/session-sync.js +37 -0
- package/dist/foundation/resident-tabs/settings.d.ts +9 -2
- package/dist/foundation/resident-tabs/settings.js +55 -7
- package/dist/foundation/session-writer-lock/index.d.ts +27 -0
- package/dist/foundation/session-writer-lock/index.js +197 -0
- package/dist/foundation/startup/startup-descriptor.js +2 -2
- package/dist/integrations/pi/components/resources/builtin-themes.d.ts +4 -4
- package/dist/integrations/pi/components/resources/builtin-themes.js +4 -4
- package/dist/integrations/pi/components/upstream/adjacent/core/keybindings.d.ts +1 -1
- package/dist/integrations/pi/components/upstream/adjacent/core/keybindings.js +17 -4
- package/dist/integrations/pi/components/upstream/assets/earendil-image.json +2 -2
- package/dist/integrations/pi/components/upstream/components/countdown-timer.d.ts +1 -1
- package/dist/integrations/pi/components/upstream/components/earendil-announcement.js +1 -1
- package/dist/integrations/pi/components/upstream/components/extension-editor.d.ts +1 -1
- package/dist/integrations/pi/components/upstream/components/extension-editor.js +1 -1
- package/dist/integrations/pi/components/upstream/components/extension-input.js +1 -1
- package/dist/integrations/pi/components/upstream/components/extension-selector.js +1 -1
- package/dist/integrations/pi/components/upstream/components/mermaid.js +1 -1
- package/dist/integrations/pi/components/upstream/components/owned-editor.d.ts +1 -1
- package/dist/integrations/pi/components/upstream/components/owned-editor.js +1 -1
- package/dist/integrations/pi/components/upstream/components/pi-logo.js +1 -1
- package/dist/integrations/pi/components/upstream/components/scoped-models-selector.d.ts +1 -1
- package/dist/integrations/pi/components/upstream/components/session-footer.js +1 -1
- package/dist/integrations/pi/components/upstream/components/session-selector-search.js +1 -1
- package/dist/integrations/pi/components/upstream/components/session-selector.js +1 -1
- package/dist/integrations/pi/components/upstream/components/skill-invocation-message.d.ts +1 -1
- package/dist/integrations/pi/components/upstream/components/skill-invocation-message.js +1 -1
- package/dist/integrations/pi/components/upstream/components/status-indicator.d.ts +2 -2
- package/dist/integrations/pi/components/upstream/components/status-indicator.js +3 -3
- package/dist/integrations/pi/components/upstream/components/themed-text.d.ts +1 -1
- package/dist/integrations/pi/components/upstream/components/themed-text.js +1 -1
- package/dist/integrations/pi/components/upstream/components/thinking-selector.d.ts +1 -1
- package/dist/integrations/pi/components/upstream/components/thinking-selector.js +1 -1
- package/dist/integrations/pi/components/upstream/components/tool-execution.d.ts +0 -2
- package/dist/integrations/pi/components/upstream/components/tool-execution.js +41 -20
- package/dist/integrations/pi/components/upstream/components/tree-selector.d.ts +1 -1
- package/dist/integrations/pi/components/upstream/components/tree-selector.js +1 -1
- package/dist/integrations/pi/components/upstream/components/trust-selector.js +1 -1
- package/dist/integrations/pi/components/upstream/external-editor.js +1 -1
- package/dist/integrations/pi/components/upstream/history/editor-core.d.ts +2 -2
- package/dist/integrations/pi/components/upstream/history/editor-core.js +5 -5
- package/dist/integrations/pi/components/upstream/history/kill-ring.d.ts +1 -1
- package/dist/integrations/pi/components/upstream/history/kill-ring.js +1 -1
- package/dist/integrations/pi/components/upstream/history/printable-key.js +1 -1
- package/dist/integrations/pi/components/upstream/history/text-helpers.js +1 -1
- package/dist/integrations/pi/components/upstream/history/undo-stack.d.ts +1 -1
- package/dist/integrations/pi/components/upstream/history/undo-stack.js +1 -1
- package/dist/integrations/pi/components/upstream/history/word-navigation.js +1 -1
- package/dist/integrations/pi/components/upstream/model-search.d.ts +1 -1
- package/dist/integrations/pi/components/upstream/program-status-reporter.d.ts +1 -1
- package/dist/integrations/pi/components/upstream/theme/theme-controller.d.ts +1 -1
- package/dist/integrations/pi/components/upstream/theme/theme.d.ts +3 -0
- package/dist/integrations/pi/components/upstream/theme/theme.js +24 -1
- package/dist/integrations/pi/engine/adapter.js +1 -0
- package/dist/integrations/pi/engine/pi-settings-metadata.json +2 -2
- package/dist/integrations/pi/engine/resources/changelog.json +4 -4
- package/dist/integrations/pi/engine/runtime-integration.js +102 -29
- package/dist/integrations/pi/engine/session-runtime.d.ts +0 -6
- package/dist/integrations/pi/engine/session-runtime.js +8 -3
- package/dist/integrations/pi/engine/session-selection.d.ts +3 -0
- package/dist/integrations/pi/engine/session-selection.js +45 -9
- package/dist/integrations/pi/engine/workflow-contexts.js +10 -1
- package/dist/integrations/pi/startup-public.js +152 -222
- package/dist/integrations/pi/startup-public.manifest.json +266 -177
- package/dist/integrations/pi/tui-runtime/damage-aware-terminal.d.ts +1 -1
- package/dist/integrations/pi/tui-runtime/damage-aware-terminal.js +1 -1
- package/dist/native/darwin-arm64/manifest.json +1 -1
- package/dist/native/linux-x64/manifest.json +1 -1
- package/dist/native/win32-x64/manifest.json +2 -2
- package/dist/native/win32-x64/process-guardian.exe +0 -0
- package/dist/runtime-payload-inventory.json +5349 -6035
- package/dist/ui/components/line-input.d.ts +1 -1
- package/dist/ui/components/prompt-input.d.ts +7 -5
- package/dist/ui/components/prompt-input.js +7 -7
- package/dist/ui/settings/declarations.d.ts +38 -2
- package/dist/ui/settings/declarations.js +29 -2
- package/dist/ui/settings/index.d.ts +4 -2
- package/dist/ui/settings/index.js +2 -1
- package/dist/ui/settings/manager.d.ts +9 -3
- package/dist/ui/settings/manager.js +81 -29
- package/dist/ui/settings/migrations.d.ts +6 -0
- package/dist/ui/settings/migrations.js +25 -0
- package/dist/ui/settings/profile-document.d.ts +26 -0
- package/dist/ui/settings/profile-document.js +164 -0
- package/dist/ui/settings/sections.js +4 -0
- package/docs/architecture/resident-tabs-requirements.md +1 -234
- package/docs/architecture/resident-tabs.md +30 -10
- package/docs/openspec-archive-automation.md +7 -7
- package/package.json +6 -4
|
@@ -56,7 +56,7 @@ export declare function caretCell(text: string): string;
|
|
|
56
56
|
export interface InputRowOptions {
|
|
57
57
|
/** Shown quietly while nothing has been typed, with the caret on its first cell. */
|
|
58
58
|
readonly placeholder?: string;
|
|
59
|
-
/**
|
|
59
|
+
/** Theme-border rules. */
|
|
60
60
|
readonly ruled?: boolean;
|
|
61
61
|
/** Prompt marker including any following spacing. Defaults to the shared prompt glyph. */
|
|
62
62
|
readonly promptGlyph?: string;
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { type UiTheme } from "./theme.js";
|
|
2
2
|
export declare const PROMPT_GLYPH = "\u276F ";
|
|
3
3
|
export declare const PROMPT_PREFIX_WIDTH = 2;
|
|
4
|
-
|
|
5
|
-
export declare function
|
|
6
|
-
export declare function
|
|
4
|
+
type RuleTheme = Pick<UiTheme, "fg">;
|
|
5
|
+
export declare function promptRuleText(text: string, theme: RuleTheme): string;
|
|
6
|
+
export declare function promptRule(width: number, theme: RuleTheme): string;
|
|
7
|
+
export declare function promptArrow(text: string, theme: RuleTheme): string;
|
|
7
8
|
export interface PromptInputBody {
|
|
8
9
|
readonly rows: readonly string[];
|
|
9
10
|
/** Semantic border fragments retain editor history and scroll annotations. */
|
|
@@ -19,9 +20,9 @@ export interface PromptInputMetrics {
|
|
|
19
20
|
}
|
|
20
21
|
/** Composes shared input chrome around controller-owned body rows without owning editing state. */
|
|
21
22
|
export declare class PromptInput {
|
|
22
|
-
readonly theme:
|
|
23
|
+
readonly theme: RuleTheme;
|
|
23
24
|
readonly metrics: PromptInputMetrics;
|
|
24
|
-
constructor(theme?:
|
|
25
|
+
constructor(theme?: RuleTheme, metrics?: PromptInputMetrics);
|
|
25
26
|
styleRule(text: string): string;
|
|
26
27
|
geometry(width: number, padding?: number, promptGlyph?: string): {
|
|
27
28
|
prefixWidth: number;
|
|
@@ -32,3 +33,4 @@ export declare class PromptInput {
|
|
|
32
33
|
};
|
|
33
34
|
render(width: number, renderBody: (innerWidth: number) => PromptInputBody, ruled?: boolean, padding?: number, promptGlyph?: string): string[];
|
|
34
35
|
}
|
|
36
|
+
export {};
|
|
@@ -2,11 +2,11 @@ import { displayWidth, truncateToWidth } from "./text.js";
|
|
|
2
2
|
import { PLAIN_THEME } from "./theme.js";
|
|
3
3
|
export const PROMPT_GLYPH = "❯ ";
|
|
4
4
|
export const PROMPT_PREFIX_WIDTH = 2;
|
|
5
|
-
export function promptRuleText(text) {
|
|
6
|
-
return
|
|
5
|
+
export function promptRuleText(text, theme) {
|
|
6
|
+
return theme.fg("border", text);
|
|
7
7
|
}
|
|
8
|
-
export function promptRule(width) {
|
|
9
|
-
return promptRuleText("─".repeat(Math.max(0, width)));
|
|
8
|
+
export function promptRule(width, theme) {
|
|
9
|
+
return promptRuleText("─".repeat(Math.max(0, width)), theme);
|
|
10
10
|
}
|
|
11
11
|
export function promptArrow(text, theme) {
|
|
12
12
|
return theme.fg("muted", text);
|
|
@@ -16,7 +16,7 @@ export class PromptInput {
|
|
|
16
16
|
theme;
|
|
17
17
|
metrics;
|
|
18
18
|
constructor(theme = PLAIN_THEME, metrics = { measure: displayWidth, truncate: truncateToWidth }) { this.theme = theme; this.metrics = metrics; }
|
|
19
|
-
styleRule(text) { return promptRuleText(text); }
|
|
19
|
+
styleRule(text) { return promptRuleText(text, this.theme); }
|
|
20
20
|
geometry(width, padding = 0, promptGlyph = PROMPT_GLYPH) {
|
|
21
21
|
const prefixWidth = Math.min(this.metrics.measure(promptGlyph), Math.max(0, width));
|
|
22
22
|
// Compatibility: editor wrapping needs room for a two-cell grapheme plus its cursor cell.
|
|
@@ -43,8 +43,8 @@ export class PromptInput {
|
|
|
43
43
|
? promptArrow(promptGlyph, this.theme)
|
|
44
44
|
: " ".repeat(prefixWidth)}${row}`));
|
|
45
45
|
const rule = (fragment) => fragment === undefined
|
|
46
|
-
? promptRule(width)
|
|
47
|
-
: fit(fragment + promptRule(Math.max(0, width - this.metrics.measure(fragment))));
|
|
46
|
+
? promptRule(width, this.theme)
|
|
47
|
+
: fit(fragment + promptRule(Math.max(0, width - this.metrics.measure(fragment)), this.theme));
|
|
48
48
|
return [
|
|
49
49
|
...(ruled ? [rule(body.topRule)] : []),
|
|
50
50
|
...rows,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export declare const OWNED_UI_SETTINGS_VERSION =
|
|
1
|
+
export declare const OWNED_UI_SETTINGS_VERSION = 15;
|
|
2
2
|
export type OwnedUiSettingValue = string | number | boolean;
|
|
3
3
|
export type OwnedUiSettingApplication = "live" | "restart";
|
|
4
4
|
export interface OwnedUiSettingDeclaration {
|
|
@@ -171,11 +171,47 @@ export declare const OWNED_SETTING_DECLARATIONS: Readonly<{
|
|
|
171
171
|
id: "generic";
|
|
172
172
|
title: "Generic";
|
|
173
173
|
}>;
|
|
174
|
-
description: "Keep bare A1 running when its terminal closes and reattach on the next launch.
|
|
174
|
+
description: "Keep bare A1 running when its terminal closes and reattach on the next launch. Turning this off prevents new resident launches but does not stop existing tabs.";
|
|
175
175
|
application: "restart";
|
|
176
176
|
defaultValue: false;
|
|
177
177
|
allowedValues: readonly [true, false];
|
|
178
178
|
}>;
|
|
179
|
+
readonly tabsMaxConcurrentStarts: Readonly<{
|
|
180
|
+
id: "tabsMaxConcurrentStarts";
|
|
181
|
+
label: "Concurrent tab starts";
|
|
182
|
+
section: Readonly<{
|
|
183
|
+
id: "generic";
|
|
184
|
+
title: "Generic";
|
|
185
|
+
}>;
|
|
186
|
+
description: "Maximum A1 tabs started together. Starts remain spaced to protect shared profile files.";
|
|
187
|
+
application: "restart";
|
|
188
|
+
defaultValue: 2;
|
|
189
|
+
allowedValues: readonly [1, 2, 3, 4, 5, 6, 7, 8];
|
|
190
|
+
}>;
|
|
191
|
+
readonly tabsUnresponsiveRestartSeconds: Readonly<{
|
|
192
|
+
id: "tabsUnresponsiveRestartSeconds";
|
|
193
|
+
label: "Unresponsive tab restart";
|
|
194
|
+
section: Readonly<{
|
|
195
|
+
id: "generic";
|
|
196
|
+
title: "Generic";
|
|
197
|
+
}>;
|
|
198
|
+
description: "Seconds of continuously healthy supervision without a tab heartbeat before A1 restarts that tab.";
|
|
199
|
+
application: "restart";
|
|
200
|
+
defaultValue: 120;
|
|
201
|
+
allowedValues: readonly [60, 120, 300, 600];
|
|
202
|
+
}>;
|
|
203
|
+
readonly tabsStallNoticeSeconds: Readonly<{
|
|
204
|
+
id: "tabsStallNoticeSeconds";
|
|
205
|
+
label: "Tab stall notice";
|
|
206
|
+
section: Readonly<{
|
|
207
|
+
id: "generic";
|
|
208
|
+
title: "Generic";
|
|
209
|
+
}>;
|
|
210
|
+
description: "Seconds without model or tool progress before a working tab shows a stall notice; A1 never kills a stalled tab automatically.";
|
|
211
|
+
application: "restart";
|
|
212
|
+
defaultValue: 300;
|
|
213
|
+
allowedValues: readonly [120, 300, 600, 1800];
|
|
214
|
+
}>;
|
|
179
215
|
}>;
|
|
180
216
|
export type OwnedSettingId = keyof typeof OWNED_SETTING_DECLARATIONS;
|
|
181
217
|
/** The value type a setting's declaration allows: a literal union for a choice, `number` for a stepped range. */
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export const OWNED_UI_SETTINGS_VERSION =
|
|
1
|
+
export const OWNED_UI_SETTINGS_VERSION = 15;
|
|
2
2
|
const UI_ACCENT_COLOR_CHOICES = Object.freeze([
|
|
3
3
|
"purple", "blue", "cyan", "green", "orange", "pink",
|
|
4
4
|
]);
|
|
@@ -128,11 +128,38 @@ export const OWNED_SETTING_DECLARATIONS = Object.freeze({
|
|
|
128
128
|
id: "residentTabs",
|
|
129
129
|
label: "Resident tabs",
|
|
130
130
|
section: GENERIC_SECTION,
|
|
131
|
-
description: "Keep bare A1 running when its terminal closes and reattach on the next launch.
|
|
131
|
+
description: "Keep bare A1 running when its terminal closes and reattach on the next launch. Turning this off prevents new resident launches but does not stop existing tabs.",
|
|
132
132
|
application: "restart",
|
|
133
133
|
defaultValue: false,
|
|
134
134
|
allowedValues: Object.freeze([true, false]),
|
|
135
135
|
}),
|
|
136
|
+
tabsMaxConcurrentStarts: Object.freeze({
|
|
137
|
+
id: "tabsMaxConcurrentStarts",
|
|
138
|
+
label: "Concurrent tab starts",
|
|
139
|
+
section: GENERIC_SECTION,
|
|
140
|
+
description: "Maximum A1 tabs started together. Starts remain spaced to protect shared profile files.",
|
|
141
|
+
application: "restart",
|
|
142
|
+
defaultValue: 2,
|
|
143
|
+
allowedValues: Object.freeze([1, 2, 3, 4, 5, 6, 7, 8]),
|
|
144
|
+
}),
|
|
145
|
+
tabsUnresponsiveRestartSeconds: Object.freeze({
|
|
146
|
+
id: "tabsUnresponsiveRestartSeconds",
|
|
147
|
+
label: "Unresponsive tab restart",
|
|
148
|
+
section: GENERIC_SECTION,
|
|
149
|
+
description: "Seconds of continuously healthy supervision without a tab heartbeat before A1 restarts that tab.",
|
|
150
|
+
application: "restart",
|
|
151
|
+
defaultValue: 120,
|
|
152
|
+
allowedValues: Object.freeze([60, 120, 300, 600]),
|
|
153
|
+
}),
|
|
154
|
+
tabsStallNoticeSeconds: Object.freeze({
|
|
155
|
+
id: "tabsStallNoticeSeconds",
|
|
156
|
+
label: "Tab stall notice",
|
|
157
|
+
section: GENERIC_SECTION,
|
|
158
|
+
description: "Seconds without model or tool progress before a working tab shows a stall notice; A1 never kills a stalled tab automatically.",
|
|
159
|
+
application: "restart",
|
|
160
|
+
defaultValue: 300,
|
|
161
|
+
allowedValues: Object.freeze([120, 300, 600, 1800]),
|
|
162
|
+
}),
|
|
136
163
|
});
|
|
137
164
|
/** The declarations in declaration order, for resolution, persistence, and the settings screen. */
|
|
138
165
|
export const OWNED_UI_SETTING_DECLARATIONS = Object.freeze(Object.values(OWNED_SETTING_DECLARATIONS));
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
export { OWNED_SETTING_DECLARATIONS, OWNED_UI_SETTINGS_VERSION, OWNED_UI_SETTING_DECLARATIONS, assertOwnedUiSettingDeclarations, findOwnedUiSettingDeclaration, isOwnedSettingId, } from "./declarations.js";
|
|
2
2
|
export type { OwnedSettingId, OwnedSettingValueOf, OwnedUiSettingApplication, OwnedUiSettingDeclaration, OwnedUiSettingValue } from "./declarations.js";
|
|
3
|
-
export { OWNED_UI_SETTINGS_MIGRATIONS, assertOwnedUiSettingsMigrations, migrationsFrom } from "./migrations.js";
|
|
4
|
-
export type { OwnedUiSettingsMigration } from "./migrations.js";
|
|
3
|
+
export { OWNED_UI_SETTINGS_MIGRATIONS, assertOwnedUiSettingsMigrations, migrationsFrom, normalizeOwnedUiSettingsValues, } from "./migrations.js";
|
|
4
|
+
export type { OwnedUiSettingsCompatibilityResult, OwnedUiSettingsMigration } from "./migrations.js";
|
|
5
5
|
export { documentFrom, parseOwnedUiSettingsDocument, resolveOwnedUiSettings, resolveResidentTabsSetting, settingValue } from "./resolution.js";
|
|
6
6
|
export type { OwnedUiResolvedSetting, OwnedUiSettingSource, OwnedUiSettingsDocument, OwnedUiSettingsNotice, OwnedUiSettingsNoticeCode, OwnedUiSettingsResolution, ResolveOwnedUiSettingsInput, } from "./resolution.js";
|
|
7
7
|
export { AGENT_SECTION_ID, buildOwnedUiSettingsSections, findOwnedUiSettingsEntry } from "./sections.js";
|
|
8
8
|
export type { AgentSettingsSnapshot, BuildOwnedUiSettingsSectionsInput, OwnedUiSettingsBackend, OwnedUiSettingsEntry, OwnedUiSettingsSection, } from "./sections.js";
|
|
9
9
|
export { OwnedSettingsManager } from "./manager.js";
|
|
10
10
|
export type { OwnedSettingsManagerOptions, OwnedUiSettingsChangeOutcome, OwnedUiSettingsListener } from "./manager.js";
|
|
11
|
+
export { assertProfileSettingsNamespaceAvailable, readProfileSettingsNamespace, repairProfileSettingsDocument, updateProfileSettingsNamespace, } from "./profile-document.js";
|
|
12
|
+
export type { ProfileSettingsNamespaceRead, ProfileSettingsNamespaceUpdate } from "./profile-document.js";
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export { OWNED_SETTING_DECLARATIONS, OWNED_UI_SETTINGS_VERSION, OWNED_UI_SETTING_DECLARATIONS, assertOwnedUiSettingDeclarations, findOwnedUiSettingDeclaration, isOwnedSettingId, } from "./declarations.js";
|
|
2
|
-
export { OWNED_UI_SETTINGS_MIGRATIONS, assertOwnedUiSettingsMigrations, migrationsFrom } from "./migrations.js";
|
|
2
|
+
export { OWNED_UI_SETTINGS_MIGRATIONS, assertOwnedUiSettingsMigrations, migrationsFrom, normalizeOwnedUiSettingsValues, } from "./migrations.js";
|
|
3
3
|
export { documentFrom, parseOwnedUiSettingsDocument, resolveOwnedUiSettings, resolveResidentTabsSetting, settingValue } from "./resolution.js";
|
|
4
4
|
export { AGENT_SECTION_ID, buildOwnedUiSettingsSections, findOwnedUiSettingsEntry } from "./sections.js";
|
|
5
5
|
export { OwnedSettingsManager } from "./manager.js";
|
|
6
|
+
export { assertProfileSettingsNamespaceAvailable, readProfileSettingsNamespace, repairProfileSettingsDocument, updateProfileSettingsNamespace, } from "./profile-document.js";
|
|
@@ -4,9 +4,15 @@ import { type OwnedUiSettingsMigration } from "./migrations.js";
|
|
|
4
4
|
import { type OwnedUiSettingsResolution } from "./resolution.js";
|
|
5
5
|
import { type OwnedUiSettingsBackend, type OwnedUiSettingsSection } from "./sections.js";
|
|
6
6
|
export interface OwnedSettingsManagerOptions {
|
|
7
|
-
/**
|
|
8
|
-
readonly
|
|
7
|
+
/** Selected global agent profile containing the shared `settings.json`. */
|
|
8
|
+
readonly profileDir?: string;
|
|
9
|
+
/** Compatibility alias for SDK/tests that supplied the former root option. */
|
|
10
|
+
readonly configDir?: string;
|
|
9
11
|
readonly profileId: string;
|
|
12
|
+
/** Product namespace; defaults to profileId for generic and hermetic consumers. */
|
|
13
|
+
readonly namespace?: string;
|
|
14
|
+
/** Former product configuration root, consulted only while the namespace is absent. */
|
|
15
|
+
readonly legacyConfigDir?: string;
|
|
10
16
|
readonly declarations?: readonly OwnedUiSettingDeclaration[];
|
|
11
17
|
readonly migrations?: readonly OwnedUiSettingsMigration[];
|
|
12
18
|
readonly agent?: AgentSettingsPort | null;
|
|
@@ -37,7 +43,7 @@ export declare class OwnedSettingsManager {
|
|
|
37
43
|
constructor(options: OwnedSettingsManagerOptions);
|
|
38
44
|
get file(): string;
|
|
39
45
|
get resolution(): OwnedUiSettingsResolution;
|
|
40
|
-
/** Reads and resolves the
|
|
46
|
+
/** Reads and resolves the product namespace. Never throws: every failure becomes a notice. */
|
|
41
47
|
read(): OwnedUiSettingsResolution;
|
|
42
48
|
load(): Promise<void>;
|
|
43
49
|
sections(): readonly OwnedUiSettingsSection[];
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { assertAgentSettingDescriptor } from "../../contracts/agent-engine/index.js";
|
|
4
4
|
import { OWNED_SETTING_DECLARATIONS, OWNED_UI_SETTINGS_VERSION, OWNED_UI_SETTING_DECLARATIONS, } from "./declarations.js";
|
|
5
|
-
import { OWNED_UI_SETTINGS_MIGRATIONS } from "./migrations.js";
|
|
6
|
-
import {
|
|
5
|
+
import { OWNED_UI_SETTINGS_MIGRATIONS, normalizeOwnedUiSettingsValues, } from "./migrations.js";
|
|
6
|
+
import { parseOwnedUiSettingsDocument, resolveOwnedUiSettings, } from "./resolution.js";
|
|
7
7
|
import { buildOwnedUiSettingsSections, findOwnedUiSettingsEntry, } from "./sections.js";
|
|
8
|
+
import { readProfileSettingsNamespace, repairProfileSettingsDocument, updateProfileSettingsNamespace, } from "./profile-document.js";
|
|
8
9
|
const PROFILE_ID_PATTERN = /^[a-z][a-z0-9-]{0,63}$/;
|
|
9
10
|
const MAX_DOCUMENT_BYTES = 256 * 1024;
|
|
10
11
|
/**
|
|
@@ -15,6 +16,8 @@ const MAX_DOCUMENT_BYTES = 256 * 1024;
|
|
|
15
16
|
*/
|
|
16
17
|
export class OwnedSettingsManager {
|
|
17
18
|
#file;
|
|
19
|
+
#legacyFile;
|
|
20
|
+
#namespace;
|
|
18
21
|
#declarations;
|
|
19
22
|
#migrations;
|
|
20
23
|
#agent;
|
|
@@ -30,7 +33,14 @@ export class OwnedSettingsManager {
|
|
|
30
33
|
if (!PROFILE_ID_PATTERN.test(options.profileId)) {
|
|
31
34
|
throw new Error(`owned UI settings profile id is not a bounded slug: ${options.profileId}`);
|
|
32
35
|
}
|
|
33
|
-
|
|
36
|
+
const profileDir = options.profileDir ?? options.configDir;
|
|
37
|
+
if (profileDir === undefined)
|
|
38
|
+
throw new Error("owned UI settings require a profile directory");
|
|
39
|
+
this.#file = path.join(path.resolve(profileDir), "settings.json");
|
|
40
|
+
this.#legacyFile = options.legacyConfigDir === undefined
|
|
41
|
+
? null
|
|
42
|
+
: path.join(path.resolve(options.legacyConfigDir), "settings", `${options.profileId}.json`);
|
|
43
|
+
this.#namespace = options.namespace ?? options.profileId;
|
|
34
44
|
this.#declarations = options.declarations ?? OWNED_UI_SETTING_DECLARATIONS;
|
|
35
45
|
this.#migrations = options.migrations ?? OWNED_UI_SETTINGS_MIGRATIONS;
|
|
36
46
|
this.#agent = options.agent ?? null;
|
|
@@ -49,24 +59,59 @@ export class OwnedSettingsManager {
|
|
|
49
59
|
get resolution() {
|
|
50
60
|
return this.#resolution;
|
|
51
61
|
}
|
|
52
|
-
/** Reads and resolves the
|
|
62
|
+
/** Reads and resolves the product namespace. Never throws: every failure becomes a notice. */
|
|
53
63
|
read() {
|
|
64
|
+
try {
|
|
65
|
+
repairProfileSettingsDocument(this.#file);
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
// Compatibility: recovery is opportunistic; unreadable-document handling stays authoritative.
|
|
69
|
+
}
|
|
70
|
+
const shared = readProfileSettingsNamespace(this.#file, this.#namespace);
|
|
71
|
+
if (shared.status === "available") {
|
|
72
|
+
if (Buffer.byteLength(JSON.stringify(shared.value), "utf8") > MAX_DOCUMENT_BYTES) {
|
|
73
|
+
return this.#resolveNamespace(null, `${this.#file} contains a ${this.#namespace} namespace exceeding the ${MAX_DOCUMENT_BYTES}-byte settings limit`);
|
|
74
|
+
}
|
|
75
|
+
return this.#resolveCompatibleNamespace(shared.value);
|
|
76
|
+
}
|
|
77
|
+
if (shared.status === "invalid")
|
|
78
|
+
return this.#resolveNamespace(null, shared.detail);
|
|
79
|
+
return this.#readLegacy();
|
|
80
|
+
}
|
|
81
|
+
#resolveCompatibleNamespace(values) {
|
|
82
|
+
const compatibility = normalizeOwnedUiSettingsValues(values);
|
|
83
|
+
return compatibility.values === null
|
|
84
|
+
? this.#resolveNamespace(null, compatibility.failure ?? "product settings compatibility failed")
|
|
85
|
+
: this.#resolveNamespace(compatibility.values);
|
|
86
|
+
}
|
|
87
|
+
#resolveNamespace(values, unreadableDetail) {
|
|
88
|
+
return resolveOwnedUiSettings({
|
|
89
|
+
declarations: this.#declarations,
|
|
90
|
+
migrations: [],
|
|
91
|
+
document: values === null ? null : { version: OWNED_UI_SETTINGS_VERSION, values },
|
|
92
|
+
...(unreadableDetail === undefined ? {} : { unreadableDetail }),
|
|
93
|
+
currentVersion: OWNED_UI_SETTINGS_VERSION,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
#readLegacy() {
|
|
97
|
+
if (this.#legacyFile === null)
|
|
98
|
+
return this.#resolveNamespace(null);
|
|
54
99
|
let raw = null;
|
|
55
100
|
let unreadable = null;
|
|
56
101
|
try {
|
|
57
|
-
raw = readFileSync(this.#
|
|
102
|
+
raw = readFileSync(this.#legacyFile, "utf8");
|
|
58
103
|
}
|
|
59
104
|
catch (error) {
|
|
60
105
|
if (!isMissing(error))
|
|
61
|
-
unreadable = `${this.#
|
|
106
|
+
unreadable = `${this.#legacyFile} could not be read: ${describe(error)}`;
|
|
62
107
|
}
|
|
63
108
|
if (raw !== null && Buffer.byteLength(raw, "utf8") > MAX_DOCUMENT_BYTES) {
|
|
64
|
-
unreadable = `${this.#
|
|
109
|
+
unreadable = `${this.#legacyFile} exceeds the ${MAX_DOCUMENT_BYTES}-byte settings limit and was ignored`;
|
|
65
110
|
raw = null;
|
|
66
111
|
}
|
|
67
112
|
const document = raw === null ? null : parseOwnedUiSettingsDocument(raw);
|
|
68
113
|
if (raw !== null && document === null && unreadable === null) {
|
|
69
|
-
unreadable = `${this.#
|
|
114
|
+
unreadable = `${this.#legacyFile} is not a valid settings document and was ignored`;
|
|
70
115
|
}
|
|
71
116
|
return resolveOwnedUiSettings({
|
|
72
117
|
declarations: this.#declarations,
|
|
@@ -126,7 +171,7 @@ export class OwnedSettingsManager {
|
|
|
126
171
|
return backend === "a1" ? this.#changeOwned(id, value) : await this.#changeAgentValue(id, value);
|
|
127
172
|
}
|
|
128
173
|
#changeOwned(id, value) {
|
|
129
|
-
const outcome = this.#write(
|
|
174
|
+
const outcome = this.#write(id, value);
|
|
130
175
|
if (!outcome.stored)
|
|
131
176
|
return failed(outcome.failure ?? `${id} could not be stored`);
|
|
132
177
|
const previous = this.#resolution;
|
|
@@ -141,35 +186,34 @@ export class OwnedSettingsManager {
|
|
|
141
186
|
this.#notify();
|
|
142
187
|
return changed("applied", "live", value, value);
|
|
143
188
|
}
|
|
144
|
-
// Invariant:
|
|
145
|
-
// Concurrency:
|
|
146
|
-
|
|
147
|
-
#write(resolution, id, value) {
|
|
189
|
+
// Invariant: only non-default declared values and unknown product properties are stored.
|
|
190
|
+
// Concurrency: the namespace updater rereads current JSON while holding Pi's compatible lock.
|
|
191
|
+
#write(id, value) {
|
|
148
192
|
const declaration = this.#declarations.find(candidate => candidate.id === id);
|
|
149
193
|
if (!declaration)
|
|
150
194
|
return { stored: false, failure: `unknown owned UI setting: ${id}` };
|
|
151
195
|
if (!declaration.allowedValues.includes(value)) {
|
|
152
196
|
return { stored: false, failure: `value ${JSON.stringify(value)} is not allowed for ${id}` };
|
|
153
197
|
}
|
|
154
|
-
const current = documentFrom(resolution);
|
|
155
|
-
const next = {
|
|
156
|
-
version: OWNED_UI_SETTINGS_VERSION,
|
|
157
|
-
values: { ...current.values, [id]: value },
|
|
158
|
-
};
|
|
159
|
-
const temporary = `${this.#file}.${process.pid}.tmp`;
|
|
160
198
|
try {
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
199
|
+
updateProfileSettingsNamespace(this.#file, this.#namespace, current => {
|
|
200
|
+
let latest = this.#readLegacy();
|
|
201
|
+
if (current !== null) {
|
|
202
|
+
const compatibility = normalizeOwnedUiSettingsValues(current);
|
|
203
|
+
if (compatibility.values === null)
|
|
204
|
+
throw new Error(compatibility.failure ?? "product settings compatibility failed");
|
|
205
|
+
latest = this.#resolveNamespace(compatibility.values);
|
|
206
|
+
}
|
|
207
|
+
const next = compactValues(latest);
|
|
208
|
+
if (value === declaration.defaultValue)
|
|
209
|
+
delete next[id];
|
|
210
|
+
else
|
|
211
|
+
next[id] = value;
|
|
212
|
+
return Object.keys(next).length === 0 ? null : next;
|
|
213
|
+
});
|
|
164
214
|
return { stored: true, failure: null };
|
|
165
215
|
}
|
|
166
216
|
catch (error) {
|
|
167
|
-
try {
|
|
168
|
-
rmSync(temporary, { force: true });
|
|
169
|
-
}
|
|
170
|
-
catch {
|
|
171
|
-
// Security: a leftover temporary file is inert: read() only ever opens the target path.
|
|
172
|
-
}
|
|
173
217
|
return { stored: false, failure: `${this.#file} could not be written: ${describe(error)}` };
|
|
174
218
|
}
|
|
175
219
|
}
|
|
@@ -254,6 +298,14 @@ function fromAgentOutcome(result) {
|
|
|
254
298
|
failure: result.failure,
|
|
255
299
|
};
|
|
256
300
|
}
|
|
301
|
+
function compactValues(resolution) {
|
|
302
|
+
const values = { ...resolution.preserved };
|
|
303
|
+
for (const setting of resolution.settings) {
|
|
304
|
+
if (setting.value !== setting.declaration.defaultValue)
|
|
305
|
+
values[setting.declaration.id] = setting.value;
|
|
306
|
+
}
|
|
307
|
+
return values;
|
|
308
|
+
}
|
|
257
309
|
function isMissing(error) {
|
|
258
310
|
return typeof error === "object" && error !== null && error.code === "ENOENT";
|
|
259
311
|
}
|
|
@@ -9,5 +9,11 @@ export interface OwnedUiSettingsMigration {
|
|
|
9
9
|
* is the first stored shape.
|
|
10
10
|
*/
|
|
11
11
|
export declare const OWNED_UI_SETTINGS_MIGRATIONS: readonly OwnedUiSettingsMigration[];
|
|
12
|
+
export interface OwnedUiSettingsCompatibilityResult {
|
|
13
|
+
readonly values: Readonly<Record<string, unknown>> | null;
|
|
14
|
+
readonly failure: string | null;
|
|
15
|
+
}
|
|
16
|
+
/** Shape-selected compatibility for the metadata-free product object. */
|
|
17
|
+
export declare function normalizeOwnedUiSettingsValues(source: Readonly<Record<string, unknown>>): OwnedUiSettingsCompatibilityResult;
|
|
12
18
|
export declare function assertOwnedUiSettingsMigrations(migrations: readonly OwnedUiSettingsMigration[], currentVersion?: number): void;
|
|
13
19
|
export declare function migrationsFrom(migrations: readonly OwnedUiSettingsMigration[], storedVersion: number): readonly OwnedUiSettingsMigration[];
|
|
@@ -111,7 +111,32 @@ export const OWNED_UI_SETTINGS_MIGRATIONS = Object.freeze([
|
|
|
111
111
|
return { ...values };
|
|
112
112
|
},
|
|
113
113
|
}),
|
|
114
|
+
Object.freeze({
|
|
115
|
+
to: 15,
|
|
116
|
+
description: "Introduce the bounded concurrent resident-tab start limit.",
|
|
117
|
+
migrate(values) {
|
|
118
|
+
return { ...values };
|
|
119
|
+
},
|
|
120
|
+
}),
|
|
114
121
|
]);
|
|
122
|
+
/** Shape-selected compatibility for the metadata-free product object. */
|
|
123
|
+
export function normalizeOwnedUiSettingsValues(source) {
|
|
124
|
+
const values = { ...source };
|
|
125
|
+
// Compatibility: "high" is valid again, so the old speed meaning is ambiguous without legacy metadata.
|
|
126
|
+
if (values.scrollbarAppearance === "hover")
|
|
127
|
+
values.scrollbarAppearance = "auto";
|
|
128
|
+
if (values.accentColor === "default")
|
|
129
|
+
values.accentColor = "purple";
|
|
130
|
+
if (values.quitEffect === "off") {
|
|
131
|
+
if (Object.hasOwn(values, "quitAnimation") && values.quitAnimation !== false) {
|
|
132
|
+
return { values: null, failure: "legacy quitEffect conflicts with quitAnimation" };
|
|
133
|
+
}
|
|
134
|
+
values.quitAnimation = false;
|
|
135
|
+
}
|
|
136
|
+
delete values.quitEffect;
|
|
137
|
+
delete values.quitEffectDurationMs;
|
|
138
|
+
return { values, failure: null };
|
|
139
|
+
}
|
|
115
140
|
export function assertOwnedUiSettingsMigrations(migrations, currentVersion = OWNED_UI_SETTINGS_VERSION) {
|
|
116
141
|
const firstProduced = currentVersion - migrations.length + 1;
|
|
117
142
|
if (migrations.length > 0 && firstProduced < 2) {
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export type ProfileSettingsNamespaceRead = {
|
|
2
|
+
readonly status: "missing";
|
|
3
|
+
readonly value: null;
|
|
4
|
+
} | {
|
|
5
|
+
readonly status: "available";
|
|
6
|
+
readonly value: Readonly<Record<string, unknown>>;
|
|
7
|
+
} | {
|
|
8
|
+
readonly status: "invalid";
|
|
9
|
+
readonly value: null;
|
|
10
|
+
readonly detail: string;
|
|
11
|
+
};
|
|
12
|
+
export type ProfileSettingsNamespaceUpdate = (current: Readonly<Record<string, unknown>> | null) => Readonly<Record<string, unknown>> | null;
|
|
13
|
+
/** Refuse an injected namespace already claimed by the surrounding root schema. */
|
|
14
|
+
export declare function assertProfileSettingsNamespaceAvailable(namespace: string, rootSettingKeys: readonly string[]): void;
|
|
15
|
+
/** Read one product-owned root object without exposing unrelated profile settings. */
|
|
16
|
+
export declare function readProfileSettingsNamespace(file: string, namespace: string): ProfileSettingsNamespaceRead;
|
|
17
|
+
/**
|
|
18
|
+
* Remove bounded leading noise only when the remaining bytes are one complete root object.
|
|
19
|
+
* The repair is silent and uses the same lock and atomic replacement path as namespace writes.
|
|
20
|
+
*/
|
|
21
|
+
export declare function repairProfileSettingsDocument(file: string): boolean;
|
|
22
|
+
/**
|
|
23
|
+
* Replace one root namespace through the lock protocol used by pinned Pi.
|
|
24
|
+
* The updater receives bytes reread while locked, so unrelated concurrent writes survive.
|
|
25
|
+
*/
|
|
26
|
+
export declare function updateProfileSettingsNamespace(file: string, namespace: string, update: ProfileSettingsNamespaceUpdate): void;
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { dirname } from "node:path";
|
|
4
|
+
import lockfile from "proper-lockfile";
|
|
5
|
+
const NAMESPACE_PATTERN = /^[a-z][a-z0-9-]*$/;
|
|
6
|
+
const MAX_RECOVERABLE_PREFIX_BYTES = 256;
|
|
7
|
+
/** Refuse an injected namespace already claimed by the surrounding root schema. */
|
|
8
|
+
export function assertProfileSettingsNamespaceAvailable(namespace, rootSettingKeys) {
|
|
9
|
+
assertNamespace(namespace);
|
|
10
|
+
if (rootSettingKeys.includes(namespace))
|
|
11
|
+
throw new Error(`profile settings namespace collides with a root setting: ${namespace}`);
|
|
12
|
+
}
|
|
13
|
+
/** Read one product-owned root object without exposing unrelated profile settings. */
|
|
14
|
+
export function readProfileSettingsNamespace(file, namespace) {
|
|
15
|
+
assertNamespace(namespace);
|
|
16
|
+
let raw;
|
|
17
|
+
try {
|
|
18
|
+
raw = readFileSync(file, "utf8");
|
|
19
|
+
}
|
|
20
|
+
catch (error) {
|
|
21
|
+
if (isMissing(error))
|
|
22
|
+
return { status: "missing", value: null };
|
|
23
|
+
return { status: "invalid", value: null, detail: `${file} could not be read: ${describe(error)}` };
|
|
24
|
+
}
|
|
25
|
+
const root = parseRoot(raw);
|
|
26
|
+
if (root === null)
|
|
27
|
+
return { status: "invalid", value: null, detail: `${file} is not a valid settings object` };
|
|
28
|
+
if (!Object.hasOwn(root, namespace))
|
|
29
|
+
return { status: "missing", value: null };
|
|
30
|
+
const value = root[namespace];
|
|
31
|
+
if (!isRecord(value)) {
|
|
32
|
+
return { status: "invalid", value: null, detail: `${file} contains a non-object ${namespace} settings namespace` };
|
|
33
|
+
}
|
|
34
|
+
return { status: "available", value: Object.freeze({ ...value }) };
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Remove bounded leading noise only when the remaining bytes are one complete root object.
|
|
38
|
+
* The repair is silent and uses the same lock and atomic replacement path as namespace writes.
|
|
39
|
+
*/
|
|
40
|
+
export function repairProfileSettingsDocument(file) {
|
|
41
|
+
if (!existsSync(file))
|
|
42
|
+
return false;
|
|
43
|
+
const release = acquireLock(file);
|
|
44
|
+
try {
|
|
45
|
+
if (!existsSync(file))
|
|
46
|
+
return false;
|
|
47
|
+
const raw = readFileSync(file, "utf8");
|
|
48
|
+
if (parseRoot(raw) !== null)
|
|
49
|
+
return false;
|
|
50
|
+
const root = parseRootAfterLeadingNoise(raw);
|
|
51
|
+
if (root === null)
|
|
52
|
+
return false;
|
|
53
|
+
replaceRoot(file, root);
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
56
|
+
finally {
|
|
57
|
+
release();
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Replace one root namespace through the lock protocol used by pinned Pi.
|
|
62
|
+
* The updater receives bytes reread while locked, so unrelated concurrent writes survive.
|
|
63
|
+
*/
|
|
64
|
+
export function updateProfileSettingsNamespace(file, namespace, update) {
|
|
65
|
+
assertNamespace(namespace);
|
|
66
|
+
mkdirSync(dirname(file), { recursive: true, mode: 0o700 });
|
|
67
|
+
const release = acquireLock(file);
|
|
68
|
+
try {
|
|
69
|
+
const root = existsSync(file) ? parseRoot(readFileSync(file, "utf8")) : {};
|
|
70
|
+
if (root === null)
|
|
71
|
+
throw new Error(`${file} is not a valid settings object`);
|
|
72
|
+
const existing = root[namespace];
|
|
73
|
+
if (existing !== undefined && !isRecord(existing)) {
|
|
74
|
+
throw new Error(`${file} contains a non-object ${namespace} settings namespace`);
|
|
75
|
+
}
|
|
76
|
+
const nextNamespace = update(existing === undefined ? null : Object.freeze({ ...existing }));
|
|
77
|
+
const nextRoot = { ...root };
|
|
78
|
+
if (nextNamespace === null || Object.keys(nextNamespace).length === 0)
|
|
79
|
+
delete nextRoot[namespace];
|
|
80
|
+
else
|
|
81
|
+
nextRoot[namespace] = { ...nextNamespace };
|
|
82
|
+
replaceRoot(file, nextRoot);
|
|
83
|
+
}
|
|
84
|
+
finally {
|
|
85
|
+
release();
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
function acquireLock(file) {
|
|
89
|
+
for (let attempt = 0;; attempt += 1) {
|
|
90
|
+
try {
|
|
91
|
+
return lockfile.lockSync(file, { realpath: false });
|
|
92
|
+
}
|
|
93
|
+
catch (error) {
|
|
94
|
+
if (attempt >= 9 || !isLocked(error))
|
|
95
|
+
throw error;
|
|
96
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 20);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
function replaceRoot(file, root) {
|
|
101
|
+
let temporary = `${file}.${process.pid}.${randomUUID()}.tmp`;
|
|
102
|
+
try {
|
|
103
|
+
writeFileSync(temporary, `${JSON.stringify(root, null, 2)}\n`, { encoding: "utf8", mode: 0o600 });
|
|
104
|
+
renameSync(temporary, file);
|
|
105
|
+
temporary = null;
|
|
106
|
+
}
|
|
107
|
+
finally {
|
|
108
|
+
if (temporary !== null) {
|
|
109
|
+
try {
|
|
110
|
+
rmSync(temporary, { force: true });
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
// Invariant: failed temporary cleanup cannot make the target document authoritative.
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
function parseRoot(raw) {
|
|
119
|
+
return parseRootValue(stripBom(raw));
|
|
120
|
+
}
|
|
121
|
+
function parseRootAfterLeadingNoise(raw) {
|
|
122
|
+
const withoutBom = stripBom(raw);
|
|
123
|
+
const start = withoutBom.indexOf("{");
|
|
124
|
+
if (start <= 0)
|
|
125
|
+
return null;
|
|
126
|
+
const prefix = withoutBom.slice(0, start);
|
|
127
|
+
if (Buffer.byteLength(prefix, "utf8") > MAX_RECOVERABLE_PREFIX_BYTES || prefix.trim().length === 0)
|
|
128
|
+
return null;
|
|
129
|
+
try {
|
|
130
|
+
JSON.parse(prefix);
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
return parseRootValue(withoutBom.slice(start));
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
function parseRootValue(raw) {
|
|
138
|
+
try {
|
|
139
|
+
const value = JSON.parse(raw);
|
|
140
|
+
return isRecord(value) ? value : null;
|
|
141
|
+
}
|
|
142
|
+
catch {
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
function stripBom(raw) {
|
|
147
|
+
return raw.charCodeAt(0) === 0xfeff ? raw.slice(1) : raw;
|
|
148
|
+
}
|
|
149
|
+
function isRecord(value) {
|
|
150
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
151
|
+
}
|
|
152
|
+
function assertNamespace(namespace) {
|
|
153
|
+
if (!NAMESPACE_PATTERN.test(namespace))
|
|
154
|
+
throw new Error(`profile settings namespace is invalid: ${namespace}`);
|
|
155
|
+
}
|
|
156
|
+
function isMissing(error) {
|
|
157
|
+
return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
|
|
158
|
+
}
|
|
159
|
+
function isLocked(error) {
|
|
160
|
+
return typeof error === "object" && error !== null && "code" in error && error.code === "ELOCKED";
|
|
161
|
+
}
|
|
162
|
+
function describe(error) {
|
|
163
|
+
return error instanceof Error ? error.message : String(error);
|
|
164
|
+
}
|