@fastagent-sh/voicenote 0.17.9

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.
@@ -0,0 +1,110 @@
1
+ // Pure logic behind cli.ts's env-config provenance. Extracted (no fs, no
2
+ // process.env) so its invariants are testable — see envConfig.test.ts:
3
+ //
4
+ // 1. File precedence: config.json (GUI) wins over ~/.zshrc (legacy CLI);
5
+ // $HOME tokens are expanded in both.
6
+ // 2. Hydration: only keys the real environment does NOT set are filled from
7
+ // files — an explicit empty string in the environment (e.g.
8
+ // VOICENOTE_PI_SUMMARY_TOOLS="") counts as set and is never overridden.
9
+ // 3. Scheduler embedding: a value is embedded only when it is NOT recoverable
10
+ // from the files at run time — i.e. it is a real-environment value that the
11
+ // files either don't provide or provide differently. Hydrated values and
12
+ // real-env values equal to the file value are skipped (vn run re-reads the
13
+ // files each start, and scheduler env outranks config.json, so embedding a
14
+ // recoverable value would freeze it against future config edits).
15
+ // 4. A real-env value that DIFFERS from the file value is embedded as a
16
+ // deliberate override, but reported (frozenOverrides) so the caller can
17
+ // warn: it may equally be a stale shell session shadowing a fresh config
18
+ // edit, and it will keep overriding until the scheduler is reinstalled.
19
+
20
+ /** File-provided values for `keys`: config.json over zshrc, $HOME expanded. */
21
+ export function parseFileEnv(
22
+ keys: readonly string[],
23
+ configData: Record<string, unknown>,
24
+ zshrcContent: string | null,
25
+ home: string,
26
+ ): Record<string, string> {
27
+ const out: Record<string, string> = {}
28
+ const expand = (v: string) => v.replace(/\$\{?HOME\}?/g, home)
29
+ for (const key of keys) {
30
+ const v = configData[key]
31
+ if (typeof v === 'string') out[key] = expand(v)
32
+ }
33
+ if (zshrcContent !== null) {
34
+ for (const key of keys) {
35
+ if (out[key] !== undefined) continue // config.json wins
36
+ const pattern = new RegExp(`(?:^|\\n)\\s*export\\s+${key}=(?:"([^"]*)"|'([^']*)'|([^\\s"'#]+))`)
37
+ const value = zshrcContent.match(pattern)?.slice(1).find(v => v !== undefined)
38
+ if (value !== undefined) out[key] = expand(value)
39
+ }
40
+ }
41
+ return out
42
+ }
43
+
44
+ /** Which keys to copy from fileEnv into an environment (invariant 2). */
45
+ export function hydrateFromFileEnv(
46
+ keys: readonly string[],
47
+ processEnv: Record<string, string | undefined>,
48
+ fileEnv: Record<string, string>,
49
+ ): Record<string, string> {
50
+ const out: Record<string, string> = {}
51
+ for (const key of keys) {
52
+ if (processEnv[key] !== undefined) continue // real env wins, incl. ""
53
+ const v = fileEnv[key]
54
+ if (v !== undefined) out[key] = v
55
+ }
56
+ return out
57
+ }
58
+
59
+ /**
60
+ * Per-key no_proxy/NO_PROXY derivation, made pure so its provenance invariant
61
+ * is testable (it is the whole reason envKeysToEmbed exists). Given the
62
+ * current env value, any previously-captured pre-merge original, whether the
63
+ * key is already marked hydrated, and whether a proxy is active, returns:
64
+ * - runtime: the value to put in the environment (always volcano-merged)
65
+ * - capture: the pre-merge real-env original to remember (or undefined:
66
+ * either we synthesized the value, or it was already captured) — this is
67
+ * what the scheduler embeds, never the merged value
68
+ * - hydrate: true when we synthesized the value from nothing (fully
69
+ * rebuildable at run time, so it must NOT be embedded)
70
+ * Merge is idempotent, so re-running on an already-merged value (reload) is
71
+ * safe; capture-once is preserved by honoring capturedOriginal.
72
+ */
73
+ export function deriveNoProxy(
74
+ current: string | undefined,
75
+ capturedOriginal: string | undefined,
76
+ alreadyHydrated: boolean,
77
+ proxyActive: boolean,
78
+ base: string,
79
+ volcanoHosts: readonly string[],
80
+ ): { runtime: string; capture: string | undefined; hydrate: boolean } {
81
+ const merge = (v: string): string => {
82
+ const items = v.split(',').map(s => s.trim()).filter(Boolean)
83
+ for (const h of volcanoHosts) if (!items.includes(h)) items.push(h)
84
+ return items.join(',')
85
+ }
86
+ if (current === undefined) {
87
+ return { runtime: merge(proxyActive ? base : ''), capture: undefined, hydrate: true }
88
+ }
89
+ const capture = (!alreadyHydrated && capturedOriginal === undefined) ? current : undefined
90
+ return { runtime: merge(current), capture, hydrate: false }
91
+ }
92
+
93
+ /** Which env values the scheduler must snapshot (invariants 3 + 4). */
94
+ export function envKeysToEmbed(
95
+ keys: readonly string[],
96
+ processEnv: Record<string, string | undefined>,
97
+ hydratedKeys: ReadonlySet<string>,
98
+ fileEnv: Record<string, string>,
99
+ ): { embed: Record<string, string>; frozenOverrides: string[] } {
100
+ const embed: Record<string, string> = {}
101
+ const frozenOverrides: string[] = []
102
+ for (const k of keys) {
103
+ const v = processEnv[k]
104
+ if (v === undefined) continue
105
+ if (hydratedKeys.has(k) || fileEnv[k] === v) continue // recoverable at run time
106
+ embed[k] = v
107
+ if (fileEnv[k] !== undefined) frozenOverrides.push(k)
108
+ }
109
+ return { embed, frozenOverrides }
110
+ }
package/src/runLock.ts ADDED
@@ -0,0 +1,20 @@
1
+ // Pure logic behind the Windows run-lock ownership check (cli.ts's
2
+ // acquireRunLockWindows). Extracted (no fs, no process) so its one subtle
3
+ // invariant is tested: a transient READ failure must map to 'unknown', never
4
+ // to 'reclaimed'. The heartbeat and release paths branch on these three
5
+ // states, and collapsing 'unknown' into 'reclaimed' (or 'mine') is exactly
6
+ // the bug that would either hand a live lock away or delete a reclaimer's lock.
7
+ export type LockOwnership = "mine" | "reclaimed" | "unknown";
8
+
9
+ /**
10
+ * @param raw lock-file contents, or null if the file could not be read
11
+ * (ENOENT, EBUSY under AV scan, …)
12
+ * @param ownPid this process's pid
13
+ */
14
+ export function parseLockOwner(raw: string | null, ownPid: number): LockOwnership {
15
+ if (raw === null) return "unknown"; // read failed — do NOT assume reclaimed
16
+ let pid: number;
17
+ try { pid = Number(JSON.parse(raw)?.pid); } catch { return "unknown"; } // corrupt/partial write
18
+ if (!Number.isFinite(pid)) return "unknown";
19
+ return pid === ownPid ? "mine" : "reclaimed";
20
+ }