@hydraharness/harness-settings-file 0.1.1-rc.6

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 DeepSeek
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,43 @@
1
+ # @hydraharness/harness-settings-file
2
+
3
+ File-backed settings provider. One YAML or JSON document carries every namespace section; external edits hot-publish through `ctx.settings`, and `update()` re-reads the document under a writer lock before writing back atomically, preserving the user's YAML comments, any section owned by a plugin that is not currently loaded, and any on-disk change this process has not observed yet.
4
+
5
+ ## Config
6
+
7
+ | Field | Meaning | Default |
8
+ |---|---|---|
9
+ | `path` | Settings document path; extension picks the format (`.yaml`/`.yml`/`.json`) | `settings.yaml` under the harness home |
10
+ | `hydraHome` | Harness home used when `path` is omitted | `$HYDRA_HOME` or `~/.hydra` |
11
+ | `watch` | Watch the document and hot-publish external edits | `true` |
12
+ | `debounceMs` | Watcher write-settle window in milliseconds | `100` |
13
+
14
+ Defaulting is one explicit `resolveSpec(config)` step; an unsupported extension fails at load.
15
+
16
+ ## Behavior
17
+
18
+ - **Boot fails loud, reload keeps last-good.** An existing-but-invalid document fails plugin load; once live, an unreadable or unparsable edit warns and keeps the last good sections. A missing document resolves every namespace from defaults and `base`; deleting it publishes the same empty state.
19
+ - **Every write derives from the current file.** Under the writer lock, the provider reads and publishes any unseen document change before checking `expectedRevision`, applying `update`/`mutate` edits, and validating the resolved candidate. Editing a sibling field cannot restore a deleted provider from an old cache. A stale revision rejects without writing; an unversioned `replace` deliberately replaces the complete section. An invalid on-disk document rejects the write instead of being overwritten.
20
+ - **Writes hold a cross-process writer lock.** Reading, validation, rename, and in-memory commit run under a `wx`-created `<file>.lock` sibling with exponential backoff and a 2 s acquisition deadline. A contender times out without removing the existing lock because age cannot distinguish a crashed owner from a paused live writer; orphan recovery is an operator action. Readers never take the lock: the rename commit is atomic, so reloads are always consistent.
21
+ - **Write-back is atomic, owner-only, and symlink-proof.** The render exclusive-creates a random-suffix temp sibling with mode `0600` (`wx` refuses to follow a planted symlink) and renames over the target, cleaning the temp up on failure.
22
+ - **YAML edits are leaf-level diffs.** A write sets only the values that changed and deletes only the keys that were removed, so comments, anchors, and formatting survive on every untouched node and on the key of every changed pair; a changed array (or other non-map value) replaces wholesale, taking comments inside it along. JSON re-serializes without comments.
23
+ - **Reloads and writes share one operation chain.** Watcher refreshes and complete write transactions from every namespace queue run one at a time; a refresh cannot interleave between persistence and the corresponding in-memory commit.
24
+ - **The watcher's ready signal reconciles once.** The initial load races the watcher's own setup, so a change written in between never fires an event; the reconcile at ready closes that startup gap.
25
+ - **The native watcher receives a canonical path.** Before Chokidar opens the target, the provider realpaths its deepest existing ancestor and restores any missing suffix. File access and user-facing diagnostics retain the configured path, while Windows cannot mix an 8.3 alias with long-form event paths inside libuv.
26
+ - **Dispose quiesces in every watch mode.** Teardown marks the provider closed, closes the watcher when present, then waits out every queued or in-flight document operation, so nothing publishes after disposal.
27
+ - **Self-write suppression by content.** The provider caches the last good text; a watcher event whose content equals the cache (its own write included) is a no-op.
28
+ - **Host configuration adapters receive the resolved path.** `ctx.settings.documentPath` is the absolute `resolveSpec()` filename, including a custom YAML/JSON path; `prepareDocument()` preserves an existing file or exclusively creates an absent empty file with owner-only permissions before the Host opens it. The browser receives only an availability flag, never reconstructs `$HYDRA_HOME`, and never submits a filesystem target.
29
+
30
+ ## Model Experience
31
+
32
+ Indirectly, through consumers of `ctx.settings`: this provider only stores and publishes namespace sections, and each consumer's own surface documents any model effect.
33
+
34
+ #### KV Cache effect
35
+
36
+ No direct invalidation; the consuming plugin owns any request-prefix changes.
37
+
38
+ ## Known Limitations and Deferred Work
39
+
40
+ - **Unversioned edits to the same field stay last-write-wins** — send the descriptor's `expectedRevision` to reject an intervening observed change. Revisions belong to the running registration, not a durable cross-process version; arbitrary editors that do not honor the lock can still race the read-rename interval.
41
+ - **A missed watcher event stays unseen until the next signal** — reads never re-stat the file, so a change the watcher fails to report is only folded in by the next event, the next write, or a restart.
42
+ - **Comment preservation is YAML-only and map-shaped** — JSON documents re-serialize without comments (JSON has none), and comments inside a changed array (or attached inline to a changed scalar value) go with the value they described.
43
+ - **No value indirection** — sections hold literal values; `${env:VAR}`-style references for secrets are a deferred seam-level feature.
package/lib/index.js ADDED
@@ -0,0 +1,286 @@
1
+ import { Service } from "@hydraharness/cordis";
2
+ import z from "@hydraharness/schemastery";
3
+ import { watch } from "chokidar";
4
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
5
+ import { dirname, extname, join, resolve } from "node:path";
6
+ import { Document, parseDocument } from "yaml";
7
+ import { withFileLock, writeFileAtomic } from "@hydraharness/harness-atomic-write";
8
+ import { canonicalizeWatchPath, resolveHydraHome } from "@hydraharness/harness-home-paths";
9
+ import { SettingsProvider, deepEqualJson } from "@hydraharness/harness-settings";
10
+ //#region lib/types/index.js
11
+ /**
12
+ * File-backed settings provider. One YAML or JSON document under the user's
13
+ * harness home carries every namespace section; external edits hot-publish
14
+ * through the seam, and every write re-reads the document under a
15
+ * cross-process writer lock before patching it as a comment-preserving
16
+ * leaf-level diff.
17
+ * @module @hydraharness/harness-settings-file
18
+ */
19
+ const FORMATS = {
20
+ ".yaml": "yaml",
21
+ ".yml": "yaml",
22
+ ".json": "json"
23
+ };
24
+ /**
25
+ * Resolve the runtime spec from plugin config: an explicit `path` wins,
26
+ * otherwise the document lives at `<harness home>/settings.yaml`.
27
+ * @param config - raw plugin config.
28
+ * @returns the resolved file location, format, and watch behavior.
29
+ */
30
+ function resolveSpec(config) {
31
+ const filename = resolve(config.path ?? join(resolveHydraHome(config.hydraHome), "settings.yaml"));
32
+ const format = FORMATS[extname(filename)];
33
+ if (format === void 0) throw new Error(`settings-file: extension "${extname(filename)}" is not supported (use .yaml, .yml, or .json)`);
34
+ return {
35
+ filename,
36
+ format,
37
+ watch: config.watch ?? true,
38
+ debounceMs: config.debounceMs ?? 100
39
+ };
40
+ }
41
+ /** Whether a parsed YAML value is a map for diffing purposes. */
42
+ function isMapLike(value) {
43
+ return typeof value === "object" && value !== null && !Array.isArray(value);
44
+ }
45
+ /**
46
+ * Apply the difference between one node's stored and next value as minimal
47
+ * `setIn`/`deleteIn` edits, recursing through maps, so every untouched node —
48
+ * and the key node of every changed pair — keeps its comments, anchors, and
49
+ * formatting. Non-map values (arrays and scalars) replace wholesale when
50
+ * unequal, taking any comments inside them along.
51
+ */
52
+ function patchNode(document, path, current, next) {
53
+ if (isMapLike(current) && isMapLike(next)) {
54
+ for (const key of Object.keys(current)) if (!(key in next)) document.deleteIn([...path, key]);
55
+ for (const [key, value] of Object.entries(next)) patchNode(document, [...path, key], current[key], value);
56
+ return;
57
+ }
58
+ if (!deepEqualJson(current, next)) document.setIn([...path], next);
59
+ }
60
+ /** Whether a filesystem error means absence; every non-ENOENT failure must surface. */
61
+ function isENOENT(error) {
62
+ return error?.code === "ENOENT";
63
+ }
64
+ /** Whether an exclusive file create found an existing document. */
65
+ function isEEXIST(error) {
66
+ return error?.code === "EEXIST";
67
+ }
68
+ /** File-backed settings provider (`settings.yaml`/`.json`). */
69
+ var FileSettingsProvider = class extends SettingsProvider {
70
+ config;
71
+ static Config = z.object({
72
+ path: z.string(),
73
+ hydraHome: z.string(),
74
+ watch: z.boolean().default(true),
75
+ debounceMs: z.number().min(0).default(100)
76
+ });
77
+ spec;
78
+ /**
79
+ * Raw text of the last successfully parsed or persisted document;
80
+ * `undefined` while the file is absent. Watcher events whose content equals
81
+ * this cache are no-ops, which is also the self-write suppression.
82
+ */
83
+ text;
84
+ /**
85
+ * Single exclusive operation chain: watcher reloads and document writes run
86
+ * one at a time in queue order (settled tail), so a write can never render
87
+ * from text a concurrent reload is busy replacing, and a reload can never
88
+ * read a half-committed write.
89
+ */
90
+ operations = Promise.resolve();
91
+ /** Set at dispose: refuse new watcher events and let in-flight work no-op. */
92
+ closed = false;
93
+ /** Opaque read of {@link closed}: control flow cannot narrow it across awaits. */
94
+ isClosed() {
95
+ return this.closed;
96
+ }
97
+ constructor(ctx, config) {
98
+ super(ctx);
99
+ this.config = config;
100
+ this.spec = resolveSpec(config);
101
+ }
102
+ /** The local document is always writable through {@link SettingsProvider.update}. */
103
+ get writable() {
104
+ return true;
105
+ }
106
+ /** The resolved YAML/JSON document path exposed to local configuration surfaces. */
107
+ get documentPath() {
108
+ return this.spec.filename;
109
+ }
110
+ /** Materialize an absent owner-only document, then return its resolved path. */
111
+ prepareDocument() {
112
+ return this.enqueue(async () => {
113
+ await mkdir(dirname(this.spec.filename), {
114
+ recursive: true,
115
+ mode: 448
116
+ });
117
+ await withFileLock(this.spec.filename, async () => {
118
+ try {
119
+ await writeFile(this.spec.filename, "", {
120
+ flag: "wx",
121
+ mode: 384
122
+ });
123
+ } catch (error) {
124
+ if (isEEXIST(error)) return;
125
+ throw error;
126
+ }
127
+ this.text = "";
128
+ if (!this.isClosed()) this.publish({});
129
+ });
130
+ return this.spec.filename;
131
+ });
132
+ }
133
+ async load() {
134
+ let text;
135
+ try {
136
+ text = await readFile(this.spec.filename, "utf8");
137
+ } catch (error) {
138
+ if (!isENOENT(error)) throw error;
139
+ this.text = void 0;
140
+ return {};
141
+ }
142
+ const doc = this.parse(text);
143
+ this.text = text;
144
+ return doc;
145
+ }
146
+ withWriteTransaction(operation) {
147
+ return this.enqueue(async () => {
148
+ await mkdir(dirname(this.spec.filename), {
149
+ recursive: true,
150
+ mode: 448
151
+ });
152
+ await withFileLock(this.spec.filename, async () => {
153
+ await this.reconcileFromDisk();
154
+ await operation();
155
+ });
156
+ });
157
+ }
158
+ /** Queue one exclusive document operation behind every earlier one. */
159
+ enqueue(operation) {
160
+ const task = this.operations.then(operation);
161
+ this.operations = task.then(() => void 0, () => void 0);
162
+ return task;
163
+ }
164
+ /** Queue a reload; only an invariant violation escaping a commit can reject it. */
165
+ queueRefresh() {
166
+ this.enqueue(() => this.refresh()).catch((error) => {
167
+ this.ctx.logger.error("settings-file: reload commit failed at %s", this.spec.filename);
168
+ this.ctx.logger.error(error);
169
+ });
170
+ }
171
+ async persist(ns, section) {
172
+ const output = this.spec.format === "yaml" ? this.renderYaml(ns, section) : this.renderJson(ns, section);
173
+ await writeFileAtomic(this.spec.filename, output, {
174
+ mode: 384,
175
+ dirMode: 448
176
+ });
177
+ this.text = output;
178
+ }
179
+ async *[Service.init]() {
180
+ yield* super[Service.init]();
181
+ const watcher = this.spec.watch ? watch(await canonicalizeWatchPath(this.spec.filename), {
182
+ ignoreInitial: true,
183
+ awaitWriteFinish: {
184
+ stabilityThreshold: this.spec.debounceMs,
185
+ pollInterval: Math.max(1, Math.min(this.spec.debounceMs, 10))
186
+ }
187
+ }) : void 0;
188
+ if (watcher !== void 0) {
189
+ watcher.on("all", () => {
190
+ if (this.closed) return;
191
+ this.queueRefresh();
192
+ });
193
+ watcher.on("ready", () => {
194
+ if (this.closed) return;
195
+ this.queueRefresh();
196
+ });
197
+ watcher.on("error", (error) => {
198
+ this.ctx.logger.warn("settings-file: watcher error on %s", this.spec.filename);
199
+ this.ctx.logger.warn(error);
200
+ });
201
+ }
202
+ yield async () => {
203
+ this.closed = true;
204
+ await watcher?.close();
205
+ await this.operations;
206
+ };
207
+ }
208
+ /** Parse one document text into raw sections, failing on a non-map root. */
209
+ parse(text) {
210
+ let root;
211
+ if (this.spec.format === "yaml") {
212
+ const document = parseDocument(text, { prettyErrors: true });
213
+ if (document.errors.length > 0) throw new Error(`settings-file: invalid document at ${this.spec.filename}: ${document.errors.map((error) => {
214
+ const at = error.linePos?.[0];
215
+ /* v8 ignore next -- `prettyErrors` populates linePos on every error; the guard answers its optional type */
216
+ return `${error.code}${at === void 0 ? "" : ` at line ${String(at.line)}, column ${String(at.col)}`}`;
217
+ }).join("; ")}`);
218
+ root = document.toJS() ?? {};
219
+ } else root = text.trim().length === 0 ? {} : JSON.parse(text);
220
+ if (typeof root !== "object" || root === null || Array.isArray(root)) throw new TypeError(`settings-file: ${this.spec.filename} must be a map of namespace sections`);
221
+ return root;
222
+ }
223
+ /**
224
+ * Re-read the document after a watcher event. Unchanged content (including
225
+ * this provider's own writes) is a no-op; an unreadable or unparsable
226
+ * document keeps the last good sections and warns — a live hot-reload must
227
+ * never take the process down. An invariant violation escaping a commit is
228
+ * not a reload failure and propagates to the queue's error surface.
229
+ */
230
+ async refresh() {
231
+ if (this.closed) return;
232
+ try {
233
+ await this.reconcileFromDisk();
234
+ } catch (error) {
235
+ if (error?.code === "INVARIANT") throw error;
236
+ this.ctx.logger.warn("settings-file: reload failed at %s; keeping the last good document", this.spec.filename);
237
+ this.ctx.logger.warn(error);
238
+ }
239
+ }
240
+ /**
241
+ * Compare the on-disk text against the cache and publish any difference
242
+ * into the seam. Absence publishes the empty document; an unreadable or
243
+ * unparsable file throws, so each caller picks its policy — a reload warns
244
+ * and keeps the last good document, a write fails loud.
245
+ */
246
+ async reconcileFromDisk() {
247
+ let text;
248
+ try {
249
+ text = await readFile(this.spec.filename, "utf8");
250
+ } catch (error) {
251
+ if (!isENOENT(error)) throw error;
252
+ text = void 0;
253
+ }
254
+ if (text === this.text || this.isClosed()) return;
255
+ if (text === void 0) {
256
+ this.text = void 0;
257
+ this.publish({});
258
+ return;
259
+ }
260
+ const doc = this.parse(text);
261
+ this.text = text;
262
+ this.publish(doc);
263
+ }
264
+ /**
265
+ * Render the next YAML text by patching one namespace in the
266
+ * comment-preserving document. The next section lands as a leaf-level diff
267
+ * against the stored one — only changed values set, only removed keys
268
+ * delete — so comments inside the section survive edits to their siblings,
269
+ * not just comments outside it.
270
+ */
271
+ renderYaml(ns, section) {
272
+ if (this.text === void 0) return new Document({ [ns]: section }).toString();
273
+ const document = parseDocument(this.text);
274
+ const root = document.toJS();
275
+ patchNode(document, [ns], isMapLike(root) ? root[ns] : void 0, section);
276
+ return document.toString();
277
+ }
278
+ /** Render the next JSON text by replacing one namespace key. */
279
+ renderJson(ns, section) {
280
+ const root = this.text === void 0 ? {} : this.parse(this.text);
281
+ root[ns] = section;
282
+ return `${JSON.stringify(root, null, 2)}\n`;
283
+ }
284
+ };
285
+ //#endregion
286
+ export { FileSettingsProvider, FileSettingsProvider as default, resolveSpec };
@@ -0,0 +1,24 @@
1
+ //#region lib/types/invariant.js
2
+ /**
3
+ * Package-owned invariant companion for `@hydraharness/harness-settings-file`.
4
+ * @module @hydraharness/harness-settings-file/invariant
5
+ */
6
+ const PACKAGE_NAME = "@hydraharness/harness-settings-file";
7
+ /** Cordis companion plugin name. */
8
+ const name = "settings-file-invariant";
9
+ /** Service required before the companion can reserve package ownership. */
10
+ const inject = ["invariants"];
11
+ /**
12
+ * No runtime invariant: this provider's contracts are file round-trip,
13
+ * watcher timing, and atomic-write behavior — IO effects proven by package
14
+ * tests; the in-process commit relation is owned by `@hydraharness/harness-settings`.
15
+ */
16
+ const install = () => {};
17
+ /**
18
+ * Register this package's invariant companion.
19
+ * @param ctx - Cordis context carrying the invariant service.
20
+ * @returns the installed registration's disposer after setup succeeds.
21
+ */
22
+ const apply = (ctx) => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install));
23
+ //#endregion
24
+ export { apply, inject, name };
@@ -0,0 +1,105 @@
1
+ /**
2
+ * File-backed settings provider. One YAML or JSON document under the user's
3
+ * harness home carries every namespace section; external edits hot-publish
4
+ * through the seam, and every write re-reads the document under a
5
+ * cross-process writer lock before patching it as a comment-preserving
6
+ * leaf-level diff.
7
+ * @module @hydraharness/harness-settings-file
8
+ */
9
+ import { Context, Service } from '@hydraharness/cordis';
10
+ import z from '@hydraharness/schemastery';
11
+ import { SettingsProvider, type SettingsNamespace } from '@hydraharness/harness-settings';
12
+ /** Plugin config: file location and hot-reload behavior. */
13
+ export interface Config {
14
+ /** Settings document path; defaults to `settings.yaml` under the harness home. */
15
+ path?: string;
16
+ /** Harness home used when `path` is omitted; defaults to `$HYDRA_HOME` or `~/.hydra`. */
17
+ hydraHome?: string;
18
+ /** Watch the document and hot-publish external edits; defaults to true. */
19
+ watch?: boolean;
20
+ /** Watcher write-settle window in milliseconds; defaults to 100. */
21
+ debounceMs?: number;
22
+ }
23
+ /** Document format derived from the configured file extension. */
24
+ type SettingsFormat = 'yaml' | 'json';
25
+ /** Fully resolved provider parameters; defaulting happens here, never inline. */
26
+ interface ResolvedSpec {
27
+ filename: string;
28
+ format: SettingsFormat;
29
+ watch: boolean;
30
+ debounceMs: number;
31
+ }
32
+ /**
33
+ * Resolve the runtime spec from plugin config: an explicit `path` wins,
34
+ * otherwise the document lives at `<harness home>/settings.yaml`.
35
+ * @param config - raw plugin config.
36
+ * @returns the resolved file location, format, and watch behavior.
37
+ */
38
+ export declare function resolveSpec(config: Config): ResolvedSpec;
39
+ /** File-backed settings provider (`settings.yaml`/`.json`). */
40
+ export declare class FileSettingsProvider extends SettingsProvider {
41
+ config: Config;
42
+ static Config: z<Config>;
43
+ private readonly spec;
44
+ /**
45
+ * Raw text of the last successfully parsed or persisted document;
46
+ * `undefined` while the file is absent. Watcher events whose content equals
47
+ * this cache are no-ops, which is also the self-write suppression.
48
+ */
49
+ private text;
50
+ /**
51
+ * Single exclusive operation chain: watcher reloads and document writes run
52
+ * one at a time in queue order (settled tail), so a write can never render
53
+ * from text a concurrent reload is busy replacing, and a reload can never
54
+ * read a half-committed write.
55
+ */
56
+ private operations;
57
+ /** Set at dispose: refuse new watcher events and let in-flight work no-op. */
58
+ private closed;
59
+ /** Opaque read of {@link closed}: control flow cannot narrow it across awaits. */
60
+ private isClosed;
61
+ constructor(ctx: Context, config: Config);
62
+ /** The local document is always writable through {@link SettingsProvider.update}. */
63
+ get writable(): boolean;
64
+ /** The resolved YAML/JSON document path exposed to local configuration surfaces. */
65
+ get documentPath(): string;
66
+ /** Materialize an absent owner-only document, then return its resolved path. */
67
+ prepareDocument(): Promise<string>;
68
+ protected load(): Promise<Record<string, unknown>>;
69
+ protected withWriteTransaction(operation: () => Promise<void>): Promise<void>;
70
+ /** Queue one exclusive document operation behind every earlier one. */
71
+ private enqueue;
72
+ /** Queue a reload; only an invariant violation escaping a commit can reject it. */
73
+ private queueRefresh;
74
+ protected persist(ns: SettingsNamespace, section: Record<string, unknown>): Promise<void>;
75
+ [Service.init](): AsyncGenerator<() => Promise<void> | void, void, void>;
76
+ /** Parse one document text into raw sections, failing on a non-map root. */
77
+ private parse;
78
+ /**
79
+ * Re-read the document after a watcher event. Unchanged content (including
80
+ * this provider's own writes) is a no-op; an unreadable or unparsable
81
+ * document keeps the last good sections and warns — a live hot-reload must
82
+ * never take the process down. An invariant violation escaping a commit is
83
+ * not a reload failure and propagates to the queue's error surface.
84
+ */
85
+ private refresh;
86
+ /**
87
+ * Compare the on-disk text against the cache and publish any difference
88
+ * into the seam. Absence publishes the empty document; an unreadable or
89
+ * unparsable file throws, so each caller picks its policy — a reload warns
90
+ * and keeps the last good document, a write fails loud.
91
+ */
92
+ private reconcileFromDisk;
93
+ /**
94
+ * Render the next YAML text by patching one namespace in the
95
+ * comment-preserving document. The next section lands as a leaf-level diff
96
+ * against the stored one — only changed values set, only removed keys
97
+ * delete — so comments inside the section survive edits to their siblings,
98
+ * not just comments outside it.
99
+ */
100
+ private renderYaml;
101
+ /** Render the next JSON text by replacing one namespace key. */
102
+ private renderJson;
103
+ }
104
+ export default FileSettingsProvider;
105
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Package-owned invariant companion for `@hydraharness/harness-settings-file`.
3
+ * @module @hydraharness/harness-settings-file/invariant
4
+ */
5
+ import type { Context } from '@hydraharness/cordis';
6
+ /** Cordis companion plugin name. */
7
+ export declare const name = "settings-file-invariant";
8
+ /** Service required before the companion can reserve package ownership. */
9
+ export declare const inject: string[];
10
+ /**
11
+ * Register this package's invariant companion.
12
+ * @param ctx - Cordis context carrying the invariant service.
13
+ * @returns the installed registration's disposer after setup succeeds.
14
+ */
15
+ export declare const apply: (ctx: Context) => Promise<() => void>;
16
+ //# sourceMappingURL=invariant.d.ts.map
package/package.json ADDED
@@ -0,0 +1,58 @@
1
+ {
2
+ "name": "@hydraharness/harness-settings-file",
3
+ "description": "File-backed settings provider (settings.yaml) for the Hydra harness",
4
+ "hydra": {
5
+ "plugin": {
6
+ "application": "Persist preferences in settings.yaml and make saved changes available to settings consumers."
7
+ }
8
+ },
9
+ "version": "0.1.1-rc.6",
10
+ "publishConfig": {
11
+ "access": "public"
12
+ },
13
+ "repository": {
14
+ "type": "git",
15
+ "url": "git+https://github.com/MaiHongPhong1902/Hydra-Harness.git",
16
+ "directory": "packages/settings/settings-file"
17
+ },
18
+ "type": "module",
19
+ "main": "lib/index.js",
20
+ "types": "lib/types/index.d.ts",
21
+ "exports": {
22
+ ".": {
23
+ "types": "./lib/types/index.d.ts",
24
+ "default": "./lib/index.js"
25
+ },
26
+ "./invariant": {
27
+ "types": "./lib/types/invariant.d.ts",
28
+ "default": "./lib/invariant.js"
29
+ },
30
+ "./src/*": "./src/*",
31
+ "./package.json": "./package.json"
32
+ },
33
+ "files": [
34
+ "lib/index.js",
35
+ "lib/invariant.js",
36
+ "lib/types/**/*.d.ts"
37
+ ],
38
+ "license": "MIT",
39
+ "peerDependencies": {
40
+ "@hydraharness/harness-atomic-write": "^0.1.1-rc.6",
41
+ "@hydraharness/harness-settings": "^0.1.1-rc.6",
42
+ "@hydraharness/cordis": "^4.0.2",
43
+ "@hydraharness/harness-home-paths": "^0.1.1-rc.6",
44
+ "@hydraharness/harness-invariants": "^0.1.1-rc.6"
45
+ },
46
+ "dependencies": {
47
+ "chokidar": "^4.0.3",
48
+ "yaml": "^2.9.0",
49
+ "@hydraharness/schemastery": "^3.18.2"
50
+ },
51
+ "devDependencies": {
52
+ "@hydraharness/harness-atomic-write": "^0.1.1-rc.6",
53
+ "@hydraharness/harness-invariants": "^0.1.1-rc.6",
54
+ "@hydraharness/harness-settings": "^0.1.1-rc.6",
55
+ "@hydraharness/harness-home-paths": "^0.1.1-rc.6",
56
+ "@hydraharness/cordis": "^4.0.2"
57
+ }
58
+ }