@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.
- package/LICENSE +21 -0
- package/README.md +351 -0
- package/README.zh-CN.md +351 -0
- package/package.json +58 -0
- package/src/cli.ts +2843 -0
- package/src/envConfig.ts +110 -0
- package/src/runLock.ts +20 -0
package/src/envConfig.ts
ADDED
|
@@ -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
|
+
}
|