@xynogen/pix-runtime 0.8.0 → 0.8.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xynogen/pix-runtime",
3
- "version": "0.8.0",
3
+ "version": "0.8.2",
4
4
  "description": "Pix shared runtime — versioned pix.json config, atomic persistence, typed change events",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -9,7 +9,7 @@
9
9
  },
10
10
  "files": [
11
11
  "src",
12
- "!src/**/*.test.ts",
12
+ "!src/**/*.test.*",
13
13
  "README.md",
14
14
  "DESIGN.md",
15
15
  "LICENSE"
@@ -21,6 +21,7 @@
21
21
  },
22
22
  "exports": {
23
23
  ".": "./src/index.ts",
24
+ "./atomic-write": "./src/atomic-write.ts",
24
25
  "./config": "./src/runtime.ts",
25
26
  "./sections": "./src/sections/index.ts",
26
27
  "./collapse": "./src/collapse.ts",
@@ -0,0 +1,35 @@
1
+ /**
2
+ * atomic-write.ts — write a file so readers never see a partial result.
3
+ *
4
+ * Writes to a unique sibling temp file, then renames over the target (rename is
5
+ * atomic on the same filesystem). A crash mid-write leaves the old file intact.
6
+ * Parent dirs are created.
7
+ *
8
+ * NOT a replacement for pix-runtime's ConfigStorage.writeAtomic, which adds a
9
+ * cross-process lock. Use this for plain single-writer files (per-tool JSON
10
+ * state, caches).
11
+ */
12
+
13
+ import { mkdirSync, renameSync, rmSync, writeFileSync } from "node:fs";
14
+ import { dirname } from "node:path";
15
+
16
+ /** Atomically write `contents` to `target`. Mode defaults to 0o600. */
17
+ export function writeFileAtomicSync(
18
+ target: string,
19
+ contents: string | Uint8Array,
20
+ mode = 0o600,
21
+ ): void {
22
+ mkdirSync(dirname(target), { recursive: true });
23
+ const tmp = `${target}.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2)}.tmp`;
24
+ try {
25
+ writeFileSync(tmp, contents, { mode });
26
+ renameSync(tmp, target);
27
+ } catch (err) {
28
+ try {
29
+ rmSync(tmp, { force: true });
30
+ } catch {
31
+ /* best-effort cleanup */
32
+ }
33
+ throw err;
34
+ }
35
+ }
package/src/index.ts CHANGED
@@ -1,3 +1,4 @@
1
+ export { writeFileAtomicSync } from "./atomic-write.ts";
1
2
  export { collapseDelayMs, shouldCollapse } from "./collapse.ts";
2
3
  export type {
3
4
  ConfigChange,
@@ -205,6 +205,7 @@ export function registerPixCommand(pi: ExtensionAPI, runtime: PixRuntime): void
205
205
  handler: async (_args: string, ctx: ExtensionCommandContext) => {
206
206
  // Re-parse pix.json so section parsers remain the single validation source.
207
207
  await runtime.reload({ origin: "command", source: "pix" });
208
+ // SAFETY: Runtime UI supports custom overlays beyond the published command context type.
208
209
  const ui = ctx.ui as unknown as {
209
210
  custom?: (f: unknown, opts?: unknown) => Promise<unknown>;
210
211
  notify(m: string, t?: "info" | "warning" | "error"): void;
@@ -227,8 +228,8 @@ export function registerPixCommand(pi: ExtensionAPI, runtime: PixRuntime): void
227
228
  done: (v: null) => void,
228
229
  ) => {
229
230
  const guide = (key: string, action: string) =>
230
- theme.fg("text", key) + theme.fg("dim", ` ${action}`);
231
- const guideSep = theme.fg("dim", " · ");
231
+ theme.fg("text", key) + theme.fg("muted", ` ${action}`);
232
+ const guideSep = theme.fg("muted", " · ");
232
233
  let selected = 0;
233
234
  let bodyOffset = 0;
234
235
  let visibleBodyLines = 1;
@@ -241,8 +242,11 @@ export function registerPixCommand(pi: ExtensionAPI, runtime: PixRuntime): void
241
242
  // Functional updater: the runtime queue serializes writes, and the
242
243
  // callback sees the latest committed value — so rapid presses each
243
244
  // advance exactly one step instead of collapsing on a stale snapshot.
244
- const step = (current: unknown): unknown => {
245
- const curVal = row.read(current);
245
+ type SettingsState = Record<string, unknown>;
246
+ const step = (current: unknown): SettingsState => {
247
+ const state: SettingsState =
248
+ current !== null && typeof current === "object" ? { ...(current as object) } : {};
249
+ const curVal = row.read(state);
246
250
  let cur = row.values.indexOf(curVal);
247
251
  // ponytail: unsupported custom values snap to section default on first change.
248
252
  if (cur === -1) {
@@ -252,8 +256,8 @@ export function registerPixCommand(pi: ExtensionAPI, runtime: PixRuntime): void
252
256
  }
253
257
  const next = (cur + direction + row.values.length) % row.values.length;
254
258
  const val = row.values[next];
255
- if (val === undefined) return current;
256
- return { ...(current as object), ...(row.patch(val) as object) };
259
+ if (val === undefined) return state;
260
+ return { ...state, ...(row.patch(val) as object) };
257
261
  };
258
262
  // Re-render AFTER the commit lands; the synchronous render in
259
263
  // handleInput fires before the queued write and shows a stale value.