@linxiraos/pi-utils 1.1.4 → 1.1.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/CHANGELOG.md +6 -3
- package/README.md +1 -0
- package/dist/types/acp/transport.d.ts +7 -0
- package/dist/types/async.d.ts +2 -0
- package/dist/types/color.d.ts +34 -0
- package/dist/types/dirs.d.ts +48 -24
- package/dist/types/index.d.ts +1 -0
- package/dist/types/json.d.ts +6 -0
- package/dist/types/math-delimiters.d.ts +45 -0
- package/dist/types/postmortem.d.ts +46 -9
- package/dist/types/ptree.d.ts +15 -1
- package/dist/types/sqlite.d.ts +3 -0
- package/package.json +2 -2
- package/src/acp/transport.ts +9 -0
- package/src/async.ts +20 -6
- package/src/browsers.ts +14 -2
- package/src/color.ts +173 -0
- package/src/dirs.ts +129 -59
- package/src/env.ts +2 -2
- package/src/index.ts +1 -0
- package/src/json.ts +23 -0
- package/src/math-delimiters.ts +143 -0
- package/src/postmortem.ts +212 -63
- package/src/ptree.ts +308 -39
- package/src/sqlite.ts +6 -0
package/src/color.ts
CHANGED
|
@@ -272,6 +272,179 @@ function linearizeChannel(channel: number): number {
|
|
|
272
272
|
const c = channel / 255;
|
|
273
273
|
return c <= 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4;
|
|
274
274
|
}
|
|
275
|
+
/** Gamma-encode a linear 0..1 channel back to 0..255 sRGB. */
|
|
276
|
+
function delinearizeChannel(linear: number): number {
|
|
277
|
+
const c = linear <= 0.0031308 ? linear * 12.92 : 1.055 * linear ** (1 / 2.4) - 0.055;
|
|
278
|
+
return c * 255;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
export interface OKLCH {
|
|
282
|
+
/** Perceptual lightness (0-1) */
|
|
283
|
+
l: number;
|
|
284
|
+
/** Chroma (0 = gray; sRGB peaks around 0.37) */
|
|
285
|
+
c: number;
|
|
286
|
+
/** Hue in degrees (0-360) */
|
|
287
|
+
h: number;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/** Convert linear sRGB (0..1 channels) to OKLab (Björn Ottosson's reference matrices). */
|
|
291
|
+
function linearRgbToOklab(r: number, g: number, b: number): { L: number; a: number; b: number } {
|
|
292
|
+
const l = Math.cbrt(0.4122214708 * r + 0.5363325363 * g + 0.0514459929 * b);
|
|
293
|
+
const m = Math.cbrt(0.2119034982 * r + 0.6806995451 * g + 0.1073969566 * b);
|
|
294
|
+
const s = Math.cbrt(0.0883024619 * r + 0.2817188376 * g + 0.6299787005 * b);
|
|
295
|
+
return {
|
|
296
|
+
L: 0.2104542553 * l + 0.793617785 * m - 0.0040720468 * s,
|
|
297
|
+
a: 1.9779984951 * l - 2.428592205 * m + 0.4505937099 * s,
|
|
298
|
+
b: 0.0259040371 * l + 0.7827717662 * m - 0.808675766 * s,
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/** Convert OKLab back to linear sRGB; channels may fall outside 0..1 when out of gamut. */
|
|
303
|
+
function oklabToLinearRgb(L: number, a: number, b: number): { r: number; g: number; b: number } {
|
|
304
|
+
const l = (L + 0.3963377774 * a + 0.2158037573 * b) ** 3;
|
|
305
|
+
const m = (L - 0.1055613458 * a - 0.0638541728 * b) ** 3;
|
|
306
|
+
const s = (L - 0.0894841775 * a - 1.291485548 * b) ** 3;
|
|
307
|
+
return {
|
|
308
|
+
r: 4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s,
|
|
309
|
+
g: -1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s,
|
|
310
|
+
b: -0.0041960863 * l - 0.7034186147 * m + 1.707614701 * s,
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Convert a hex color to OKLCH (perceptual lightness/chroma/hue).
|
|
316
|
+
*
|
|
317
|
+
* Unlike HSL, equal `l`/`c` values look equally bright and colorful across
|
|
318
|
+
* hues, so carrying them between colors preserves the palette's "weight".
|
|
319
|
+
*/
|
|
320
|
+
export function hexToOklch(hex: string): OKLCH {
|
|
321
|
+
const rgb = hexToRgb(hex);
|
|
322
|
+
const lab = linearRgbToOklab(linearizeChannel(rgb.r), linearizeChannel(rgb.g), linearizeChannel(rgb.b));
|
|
323
|
+
const c = Math.hypot(lab.a, lab.b);
|
|
324
|
+
let h = (Math.atan2(lab.b, lab.a) * 180) / Math.PI;
|
|
325
|
+
if (h < 0) h += 360;
|
|
326
|
+
return { l: lab.L, c, h };
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Max OKLab saturation (C/L) that stays inside sRGB for the hue direction
|
|
331
|
+
* `(a, b)`, via Björn Ottosson's polynomial fit plus one Halley refinement.
|
|
332
|
+
*/
|
|
333
|
+
function computeMaxSaturation(a: number, b: number): number {
|
|
334
|
+
// Select the channel that clips first for this hue direction.
|
|
335
|
+
let k0: number, k1: number, k2: number, k3: number, k4: number;
|
|
336
|
+
let wl: number, wm: number, ws: number;
|
|
337
|
+
if (-1.88170328 * a - 0.80936493 * b > 1) {
|
|
338
|
+
// red channel
|
|
339
|
+
k0 = 1.19086277;
|
|
340
|
+
k1 = 1.76576728;
|
|
341
|
+
k2 = 0.59662641;
|
|
342
|
+
k3 = 0.75515197;
|
|
343
|
+
k4 = 0.56771245;
|
|
344
|
+
wl = 4.0767416621;
|
|
345
|
+
wm = -3.3077115913;
|
|
346
|
+
ws = 0.2309699292;
|
|
347
|
+
} else if (1.81444104 * a - 1.19445276 * b > 1) {
|
|
348
|
+
// green channel
|
|
349
|
+
k0 = 0.73956515;
|
|
350
|
+
k1 = -0.45954404;
|
|
351
|
+
k2 = 0.08285427;
|
|
352
|
+
k3 = 0.1254107;
|
|
353
|
+
k4 = 0.14503204;
|
|
354
|
+
wl = -1.2684380046;
|
|
355
|
+
wm = 2.6097574011;
|
|
356
|
+
ws = -0.3413193965;
|
|
357
|
+
} else {
|
|
358
|
+
// blue channel
|
|
359
|
+
k0 = 1.35733652;
|
|
360
|
+
k1 = -0.00915799;
|
|
361
|
+
k2 = -1.1513021;
|
|
362
|
+
k3 = -0.50559606;
|
|
363
|
+
k4 = 0.00692167;
|
|
364
|
+
wl = -0.0041960863;
|
|
365
|
+
wm = -0.7034186147;
|
|
366
|
+
ws = 1.707614701;
|
|
367
|
+
}
|
|
368
|
+
const S = k0 + k1 * a + k2 * b + k3 * a * a + k4 * a * b;
|
|
369
|
+
|
|
370
|
+
// One Halley step against f = (channel at S) - 0: polishes the fit to
|
|
371
|
+
// well below hex quantization error.
|
|
372
|
+
const kl = 0.3963377774 * a + 0.2158037573 * b;
|
|
373
|
+
const km = -0.1055613458 * a - 0.0638541728 * b;
|
|
374
|
+
const ks = -0.0894841775 * a - 1.291485548 * b;
|
|
375
|
+
const l_ = 1 + S * kl;
|
|
376
|
+
const m_ = 1 + S * km;
|
|
377
|
+
const s_ = 1 + S * ks;
|
|
378
|
+
const f = wl * l_ ** 3 + wm * m_ ** 3 + ws * s_ ** 3;
|
|
379
|
+
const f1 = wl * 3 * kl * l_ * l_ + wm * 3 * km * m_ * m_ + ws * 3 * ks * s_ * s_;
|
|
380
|
+
const f2 = wl * 6 * kl * kl * l_ + wm * 6 * km * km * m_ + ws * 6 * ks * ks * s_;
|
|
381
|
+
return S - (f * f1) / (f1 * f1 - 0.5 * f * f2);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* The sRGB gamut cusp for an OKLCH hue: the lightness/chroma point where the
|
|
386
|
+
* hue reaches its maximum chroma inside sRGB.
|
|
387
|
+
*
|
|
388
|
+
* The cusp lightness varies wildly per hue (yellow ≈ 0.97, blue ≈ 0.45), so
|
|
389
|
+
* transferring absolute OKLCH lightness/chroma between hues distorts
|
|
390
|
+
* vividness; normalize against the cusp instead. See `getSessionAccentHex`.
|
|
391
|
+
*/
|
|
392
|
+
export function oklchCusp(h: number): { l: number; c: number } {
|
|
393
|
+
const hRad = (h * Math.PI) / 180;
|
|
394
|
+
const a = Math.cos(hRad);
|
|
395
|
+
const b = Math.sin(hRad);
|
|
396
|
+
const sCusp = computeMaxSaturation(a, b);
|
|
397
|
+
const rgb = oklabToLinearRgb(1, sCusp * a, sCusp * b);
|
|
398
|
+
const lCusp = Math.cbrt(1 / Math.max(rgb.r, rgb.g, rgb.b));
|
|
399
|
+
return { l: lCusp, c: lCusp * sCusp };
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/** Slack allowed on linear channels before a color counts as out of sRGB gamut. */
|
|
403
|
+
const GAMUT_EPSILON = 1e-4;
|
|
404
|
+
|
|
405
|
+
/** True when all linear channels sit inside sRGB (within {@link GAMUT_EPSILON}). */
|
|
406
|
+
function inSrgbGamut(rgb: { r: number; g: number; b: number }): boolean {
|
|
407
|
+
return (
|
|
408
|
+
rgb.r >= -GAMUT_EPSILON &&
|
|
409
|
+
rgb.r <= 1 + GAMUT_EPSILON &&
|
|
410
|
+
rgb.g >= -GAMUT_EPSILON &&
|
|
411
|
+
rgb.g <= 1 + GAMUT_EPSILON &&
|
|
412
|
+
rgb.b >= -GAMUT_EPSILON &&
|
|
413
|
+
rgb.b <= 1 + GAMUT_EPSILON
|
|
414
|
+
);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* Convert OKLCH to a CSS hex string, gamut-mapping by chroma reduction.
|
|
419
|
+
*
|
|
420
|
+
* Out-of-gamut inputs keep their lightness and hue while chroma is bisected
|
|
421
|
+
* down until the color fits sRGB, matching CSS Color 4's recommended intent.
|
|
422
|
+
*/
|
|
423
|
+
export function oklchToHex(oklch: OKLCH): string {
|
|
424
|
+
const l = Math.max(0, Math.min(1, oklch.l));
|
|
425
|
+
const hRad = (oklch.h * Math.PI) / 180;
|
|
426
|
+
const cos = Math.cos(hRad);
|
|
427
|
+
const sin = Math.sin(hRad);
|
|
428
|
+
const at = (c: number) => oklabToLinearRgb(l, c * cos, c * sin);
|
|
429
|
+
|
|
430
|
+
let rgb = at(oklch.c);
|
|
431
|
+
if (!inSrgbGamut(rgb)) {
|
|
432
|
+
// `lo` always fits (chroma 0 is the gray axis), `hi` never does.
|
|
433
|
+
let lo = 0;
|
|
434
|
+
let hi = oklch.c;
|
|
435
|
+
for (let i = 0; i < 20; i++) {
|
|
436
|
+
const mid = (lo + hi) / 2;
|
|
437
|
+
if (inSrgbGamut(at(mid))) lo = mid;
|
|
438
|
+
else hi = mid;
|
|
439
|
+
}
|
|
440
|
+
rgb = at(lo);
|
|
441
|
+
}
|
|
442
|
+
return rgbToHex({
|
|
443
|
+
r: Math.max(0, Math.min(255, delinearizeChannel(rgb.r))),
|
|
444
|
+
g: Math.max(0, Math.min(255, delinearizeChannel(rgb.g))),
|
|
445
|
+
b: Math.max(0, Math.min(255, delinearizeChannel(rgb.b))),
|
|
446
|
+
});
|
|
447
|
+
}
|
|
275
448
|
|
|
276
449
|
/**
|
|
277
450
|
* Perceptual luma (gamma-encoded BT.709 weights over raw sRGB), normalized to 0..1.
|
package/src/dirs.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Centralized path helpers for
|
|
2
|
+
* Centralized path helpers for zeta config directories.
|
|
3
3
|
*
|
|
4
4
|
* Uses PI_CONFIG_DIR (default ".zeta") for the config root and
|
|
5
5
|
* PI_CODING_AGENT_DIR to override the agent directory.
|
|
6
6
|
*
|
|
7
7
|
* On Linux, if XDG_DATA_HOME / XDG_STATE_HOME / XDG_CACHE_HOME environment
|
|
8
8
|
* variables are set, paths are redirected to XDG-compliant locations under
|
|
9
|
-
* $XDG_*_HOME/
|
|
9
|
+
* $XDG_*_HOME/zeta/. This requires running `zeta config migrate` first to
|
|
10
10
|
* move data to the new locations. No filesystem existence checks are performed
|
|
11
11
|
* — if the env var is set, omp trusts that the migration has been done.
|
|
12
12
|
*/
|
|
@@ -15,8 +15,9 @@ import * as fs from "node:fs";
|
|
|
15
15
|
import * as os from "node:os";
|
|
16
16
|
import * as path from "node:path";
|
|
17
17
|
import { engines, version } from "../package.json" with { type: "json" };
|
|
18
|
+
import { isEnoent, isEnotdir } from "./fs-error";
|
|
18
19
|
|
|
19
|
-
/** App name (e.g. "
|
|
20
|
+
/** App name (e.g. "omp") */
|
|
20
21
|
export const APP_NAME: string = "zeta";
|
|
21
22
|
|
|
22
23
|
/** Config directory name (e.g. ".zeta") */
|
|
@@ -28,7 +29,7 @@ export const MAIN_CONFIG_FILENAMES = ["config.yml", "config.yaml"] as const;
|
|
|
28
29
|
/** Version (e.g. "1.0.0") */
|
|
29
30
|
export const VERSION: string = version;
|
|
30
31
|
|
|
31
|
-
/** Default User-Agent header string (e.g. "zeta/1.
|
|
32
|
+
/** Default User-Agent header string (e.g. "zeta/1.1.5") */
|
|
32
33
|
export const USER_AGENT = `zeta/${VERSION}`;
|
|
33
34
|
|
|
34
35
|
/** Minimum Bun version */
|
|
@@ -178,17 +179,50 @@ export function relativePathWithinRoot(root: string, candidate: string): string
|
|
|
178
179
|
return relative || null;
|
|
179
180
|
}
|
|
180
181
|
|
|
181
|
-
let projectDir
|
|
182
|
+
let projectDir: string | undefined;
|
|
182
183
|
|
|
183
184
|
/** Get the project directory. */
|
|
184
185
|
export function getProjectDir(): string {
|
|
186
|
+
if (projectDir === undefined) {
|
|
187
|
+
try {
|
|
188
|
+
projectDir = standardizeMacOSPath(process.cwd());
|
|
189
|
+
} catch {
|
|
190
|
+
const candidates = [process.env.PWD, os.homedir(), os.tmpdir()];
|
|
191
|
+
for (const candidate of candidates) {
|
|
192
|
+
if (!candidate || !path.isAbsolute(candidate)) continue;
|
|
193
|
+
try {
|
|
194
|
+
process.chdir(candidate);
|
|
195
|
+
projectDir = standardizeMacOSPath(candidate);
|
|
196
|
+
break;
|
|
197
|
+
} catch {}
|
|
198
|
+
}
|
|
199
|
+
if (projectDir === undefined) {
|
|
200
|
+
throw new Error("Unable to determine an accessible working directory");
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
185
204
|
return projectDir;
|
|
186
205
|
}
|
|
187
206
|
|
|
188
207
|
/** Set the project directory. */
|
|
189
208
|
export function setProjectDir(dir: string): void {
|
|
190
|
-
|
|
191
|
-
process.chdir(
|
|
209
|
+
const resolved = standardizeMacOSPath(path.resolve(dir));
|
|
210
|
+
process.chdir(resolved);
|
|
211
|
+
projectDir = resolved;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/** Reset the cached project directory (test seam). */
|
|
215
|
+
export function __resetProjectDirCacheForTests(): void {
|
|
216
|
+
projectDir = undefined;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/** Whether a path is absent or not a directory. Other stat failures return false. */
|
|
220
|
+
export async function directoryIsMissing(dir: string): Promise<boolean> {
|
|
221
|
+
try {
|
|
222
|
+
return !(await fs.promises.stat(dir)).isDirectory();
|
|
223
|
+
} catch (error) {
|
|
224
|
+
return isEnoent(error) || isEnotdir(error);
|
|
225
|
+
}
|
|
192
226
|
}
|
|
193
227
|
|
|
194
228
|
/**
|
|
@@ -205,33 +239,49 @@ export async function directoryExists(dir: string): Promise<boolean> {
|
|
|
205
239
|
}
|
|
206
240
|
}
|
|
207
241
|
|
|
208
|
-
/** Get the config directory name relative to home (e.g. ".zeta" or PI_CONFIG_DIR override). */
|
|
209
|
-
export function getConfigDirName(): string {
|
|
210
|
-
if (process.env.PI_CONFIG_DIR) return process.env.PI_CONFIG_DIR;
|
|
211
|
-
return ".zeta";
|
|
212
|
-
}
|
|
213
|
-
|
|
214
242
|
/**
|
|
215
|
-
*
|
|
216
|
-
*
|
|
217
|
-
*
|
|
218
|
-
*
|
|
219
|
-
*
|
|
243
|
+
* Whether `dir` both exists and can be entered. POSIX `stat` succeeds for a
|
|
244
|
+
* directory whose own search/execute permission is denied (it only needs
|
|
245
|
+
* +x on the parent chain), so existence alone does not imply `chdir` works.
|
|
246
|
+
* Callers that adopt a directory as a working directory must check this
|
|
247
|
+
* rather than {@link directoryExists} alone.
|
|
220
248
|
*/
|
|
221
|
-
export function
|
|
222
|
-
|
|
223
|
-
|
|
249
|
+
export async function directoryIsEnterable(dir: string): Promise<boolean> {
|
|
250
|
+
try {
|
|
251
|
+
const [stats] = await Promise.all([fs.promises.stat(dir), fs.promises.access(dir, fs.constants.X_OK)]);
|
|
252
|
+
return stats.isDirectory();
|
|
253
|
+
} catch {
|
|
254
|
+
return false;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/** Whether `dir` is enterable, synchronous variant. See {@link directoryIsEnterable}. */
|
|
259
|
+
export function directoryIsEnterableSync(dir: string): boolean {
|
|
224
260
|
try {
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
const ompDir = path.join(home, ".omp");
|
|
228
|
-
if (!fs.existsSync(ompDir)) return;
|
|
229
|
-
fs.renameSync(ompDir, zetaDir);
|
|
261
|
+
fs.accessSync(dir, fs.constants.X_OK);
|
|
262
|
+
return fs.statSync(dir).isDirectory();
|
|
230
263
|
} catch {
|
|
231
|
-
|
|
264
|
+
return false;
|
|
232
265
|
}
|
|
233
266
|
}
|
|
234
267
|
|
|
268
|
+
/**
|
|
269
|
+
* Project directory when it is enterable, otherwise a safe fallback.
|
|
270
|
+
* Used by spawns that must preserve project-relative behavior when healthy.
|
|
271
|
+
*/
|
|
272
|
+
export function getSafeProjectCwd(): string {
|
|
273
|
+
try {
|
|
274
|
+
const dir = getProjectDir();
|
|
275
|
+
if (directoryIsEnterableSync(dir)) return dir;
|
|
276
|
+
} catch {}
|
|
277
|
+
return os.homedir();
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/** Get the config directory name relative to home (e.g. ".zeta" or PI_CONFIG_DIR override). */
|
|
281
|
+
export function getConfigDirName(): string {
|
|
282
|
+
return process.env.PI_CONFIG_DIR || CONFIG_DIR_NAME;
|
|
283
|
+
}
|
|
284
|
+
|
|
235
285
|
/** Get the config agent directory name relative to home (e.g. ".zeta/agent" or PI_CONFIG_DIR + "/agent"). */
|
|
236
286
|
export function getConfigAgentDirName(): string {
|
|
237
287
|
const profile = getActiveProfile();
|
|
@@ -246,7 +296,7 @@ type XdgCategory = "data" | "state" | "cache";
|
|
|
246
296
|
|
|
247
297
|
/**
|
|
248
298
|
* Resolves and caches all omp directory paths. On Linux, when XDG environment
|
|
249
|
-
* variables are set, paths are redirected under $XDG_*_HOME/
|
|
299
|
+
* variables are set, paths are redirected under $XDG_*_HOME/zeta/. A new
|
|
250
300
|
* instance is created whenever the agent directory changes, which naturally
|
|
251
301
|
* invalidates all cached paths.
|
|
252
302
|
*/
|
|
@@ -255,7 +305,7 @@ class DirResolver {
|
|
|
255
305
|
readonly agentDir: string;
|
|
256
306
|
|
|
257
307
|
// Per-category base dirs. Without XDG, all three equal configRoot / agentDir.
|
|
258
|
-
// With XDG on Linux, they point to $XDG_*_HOME/
|
|
308
|
+
// With XDG on Linux, they point to $XDG_*_HOME/zeta/.
|
|
259
309
|
readonly #rootDirs: Record<XdgCategory, string>;
|
|
260
310
|
readonly #agentDirs: Record<XdgCategory, string>;
|
|
261
311
|
|
|
@@ -272,14 +322,14 @@ class DirResolver {
|
|
|
272
322
|
const isDefault = this.agentDir === defaultAgent;
|
|
273
323
|
|
|
274
324
|
// XDG is a Linux convention. On supported platforms, default profile state
|
|
275
|
-
// resolves under $XDG_*_HOME/
|
|
325
|
+
// resolves under $XDG_*_HOME/zeta once `zeta config init-xdg` has migrated
|
|
276
326
|
// the user's data. Named profiles follow a stricter rule: the XDG choice
|
|
277
327
|
// is keyed on the profile-specific XDG path, never the base app root.
|
|
278
328
|
//
|
|
279
329
|
// Why: if we consulted the base app root for named profiles too, the same
|
|
280
330
|
// profile could resolve to `~/.zeta/profiles/<name>` on first activation
|
|
281
|
-
// (when no $XDG_*_HOME/
|
|
282
|
-
// `$XDG_*_HOME/
|
|
331
|
+
// (when no $XDG_*_HOME/zeta exists yet) and then silently move to
|
|
332
|
+
// `$XDG_*_HOME/zeta/profiles/<name>` the moment the base appeared, orphaning
|
|
283
333
|
// the earlier state. Pinning on the profile path means a profile's location
|
|
284
334
|
// is decided at first activation and stays put until the user explicitly
|
|
285
335
|
// migrates it (e.g. by mkdir'ing the XDG profile dir).
|
|
@@ -521,7 +571,7 @@ export function getAgentDir(): string {
|
|
|
521
571
|
|
|
522
572
|
/** Get the project-local config directory (.zeta). */
|
|
523
573
|
export function getProjectAgentDir(cwd: string = getProjectDir()): string {
|
|
524
|
-
return path.join(cwd,
|
|
574
|
+
return path.join(cwd, CONFIG_DIR_NAME);
|
|
525
575
|
}
|
|
526
576
|
|
|
527
577
|
// =============================================================================
|
|
@@ -538,7 +588,7 @@ export function getLogsDir(): string {
|
|
|
538
588
|
return dirs.rootSubdir("logs", "state");
|
|
539
589
|
}
|
|
540
590
|
|
|
541
|
-
/** Get this process's dated log path (~/.zeta/logs/
|
|
591
|
+
/** Get this process's dated log path (~/.zeta/logs/omp.YYYY-MM-DD.PID.log). */
|
|
542
592
|
export function getLogPath(date = new Date(), pid = process.pid): string {
|
|
543
593
|
return path.join(getLogsDir(), `${APP_NAME}.${date.toISOString().slice(0, 10)}.${pid}.log`);
|
|
544
594
|
}
|
|
@@ -555,7 +605,7 @@ export function getLogPath(date = new Date(), pid = process.pid): string {
|
|
|
555
605
|
*/
|
|
556
606
|
export function getPluginsDir(home?: string): string {
|
|
557
607
|
if (home !== undefined && home !== RESOLVER_HOME) {
|
|
558
|
-
return path.join(home,
|
|
608
|
+
return path.join(home, getConfigDirName(), "plugins");
|
|
559
609
|
}
|
|
560
610
|
return dirs.rootSubdir("plugins", "data");
|
|
561
611
|
}
|
|
@@ -570,9 +620,9 @@ export function getPluginsPackageJson(home?: string): string {
|
|
|
570
620
|
return path.join(getPluginsDir(home), "package.json");
|
|
571
621
|
}
|
|
572
622
|
|
|
573
|
-
/** Plugin lock file (~/.zeta/plugins/
|
|
623
|
+
/** Plugin lock file (~/.zeta/plugins/omp-plugins.lock.json). */
|
|
574
624
|
export function getPluginsLockfile(home?: string): string {
|
|
575
|
-
return path.join(getPluginsDir(home), "
|
|
625
|
+
return path.join(getPluginsDir(home), "omp-plugins.lock.json");
|
|
576
626
|
}
|
|
577
627
|
|
|
578
628
|
/** Get the remote mount directory (~/.zeta/remote). */
|
|
@@ -585,7 +635,7 @@ export function getRemoteDir(): string {
|
|
|
585
635
|
* empty/whitespace input or a path that is still relative after expansion.
|
|
586
636
|
*
|
|
587
637
|
* A worktree base is process-global and consumed by both creation
|
|
588
|
-
* (PR checkout, task isolation) and cleanup (`
|
|
638
|
+
* (PR checkout, task isolation) and cleanup (`omp worktree`). A relative value
|
|
589
639
|
* would resolve against whatever cwd happened to launch `omp`, so checkout and
|
|
590
640
|
* cleanup could disagree — we refuse it rather than silently bind it to cwd.
|
|
591
641
|
*/
|
|
@@ -602,7 +652,7 @@ let worktreesDirOverride: string | undefined;
|
|
|
602
652
|
|
|
603
653
|
/**
|
|
604
654
|
* Relocate the base directory for agent-managed worktrees (PR checkouts, task
|
|
605
|
-
* isolation, and `
|
|
655
|
+
* isolation, and `omp worktree` cleanup all read the same base). Driven by the
|
|
606
656
|
* `worktree.base` setting in coding-agent; pass `undefined`/empty to clear and
|
|
607
657
|
* fall back to `OMP_WORKTREE_DIR` or the `~/.zeta/wt` default.
|
|
608
658
|
*
|
|
@@ -699,6 +749,15 @@ export function getGithubCacheDbPath(): string {
|
|
|
699
749
|
if (override) return override;
|
|
700
750
|
return dirs.rootSubdir(path.join("cache", "github-cache.db"), "cache");
|
|
701
751
|
}
|
|
752
|
+
/**
|
|
753
|
+
* Get the conventional commit inference cache database path (~/.zeta/cache/commit-inference.db).
|
|
754
|
+
* Honors `OMP_COMMIT_CACHE_DB` so tests and operators can isolate the cache.
|
|
755
|
+
*/
|
|
756
|
+
export function getCommitCacheDbPath(): string {
|
|
757
|
+
const override = process.env.OMP_COMMIT_CACHE_DB;
|
|
758
|
+
if (override) return override;
|
|
759
|
+
return dirs.rootSubdir(path.join("cache", "commit-inference.db"), "cache");
|
|
760
|
+
}
|
|
702
761
|
|
|
703
762
|
/** Get the legacy Pi extension parse cache database path. */
|
|
704
763
|
export function getLegacyPiExtensionCacheDbPath(): string {
|
|
@@ -716,12 +775,12 @@ export function getAuthBrokerSnapshotCachePath(): string {
|
|
|
716
775
|
return dirs.rootSubdir(path.join("cache", "auth-broker-snapshot.enc"), "cache");
|
|
717
776
|
}
|
|
718
777
|
|
|
719
|
-
/** Get the
|
|
720
|
-
/** Get the commit-author avatar cache directory (~/.omp/cache/avatars). */
|
|
778
|
+
/** Get the commit-author avatar cache directory (~/.zeta/cache/avatars). */
|
|
721
779
|
export function getAvatarCacheDir(): string {
|
|
722
780
|
return dirs.rootSubdir(path.join("cache", "avatars"), "cache");
|
|
723
781
|
}
|
|
724
|
-
|
|
782
|
+
|
|
783
|
+
/** Get the local FastEmbed model cache directory (~/.zeta/cache/fastembed). */
|
|
725
784
|
export function getFastembedCacheDir(): string {
|
|
726
785
|
return dirs.rootSubdir(path.join("cache", "fastembed"), "cache");
|
|
727
786
|
}
|
|
@@ -804,6 +863,10 @@ export function getTinyModelsCacheDir(agentDir?: string): string {
|
|
|
804
863
|
export function getDocumentConversionCacheDir(agentDir?: string): string {
|
|
805
864
|
return dirs.agentSubdir(agentDir, path.join("cache", "document-conversions"), "cache");
|
|
806
865
|
}
|
|
866
|
+
/** Get the per-project composer speculative cache directory (~/.zeta/agent/cache/composer; XDG default: $XDG_CACHE_HOME/zeta/cache/composer). */
|
|
867
|
+
export function getComposerCacheDir(agentDir?: string): string {
|
|
868
|
+
return dirs.agentSubdir(agentDir, path.join("cache", "composer"), "cache");
|
|
869
|
+
}
|
|
807
870
|
|
|
808
871
|
/** Get the sessions directory (~/.zeta/agent/sessions). */
|
|
809
872
|
export function getSessionsDir(agentDir?: string): string {
|
|
@@ -850,12 +913,12 @@ export function getTerminalSessionsDir(agentDir?: string): string {
|
|
|
850
913
|
return dirs.agentSubdir(agentDir, "terminal-sessions", "state");
|
|
851
914
|
}
|
|
852
915
|
|
|
853
|
-
/** Get the crash log path (~/.zeta/agent/
|
|
916
|
+
/** Get the crash log path (~/.zeta/agent/omp-crash.log). */
|
|
854
917
|
export function getCrashLogPath(agentDir?: string): string {
|
|
855
|
-
return dirs.agentSubdir(agentDir, "
|
|
918
|
+
return dirs.agentSubdir(agentDir, "omp-crash.log", "state");
|
|
856
919
|
}
|
|
857
920
|
|
|
858
|
-
/** Get the debug log path (~/.zeta/agent/
|
|
921
|
+
/** Get the debug log path (~/.zeta/agent/omp-debug.log). */
|
|
859
922
|
export function getDebugLogPath(agentDir?: string): string {
|
|
860
923
|
return dirs.agentSubdir(agentDir, `${APP_NAME}-debug.log`, "state");
|
|
861
924
|
}
|
|
@@ -961,20 +1024,6 @@ export function getSSHConfigPath(scope: "user" | "project", cwd: string = getPro
|
|
|
961
1024
|
return path.join(getProjectAgentDir(cwd), "ssh.json");
|
|
962
1025
|
}
|
|
963
1026
|
|
|
964
|
-
// =============================================================================
|
|
965
|
-
// Project tracking
|
|
966
|
-
// =============================================================================
|
|
967
|
-
|
|
968
|
-
/** Get the project-level tracking directory (<project>/.zeta/tracking). */
|
|
969
|
-
export function getProjectTrackingDir(cwd: string = getProjectDir()): string {
|
|
970
|
-
return path.join(getProjectAgentDir(cwd), "tracking");
|
|
971
|
-
}
|
|
972
|
-
|
|
973
|
-
/** Get the global tracking index path (~/.zeta/agent/tracking-index.json). */
|
|
974
|
-
export function getTrackingIndexPath(agentDir?: string): string {
|
|
975
|
-
return path.join(agentDir ?? getAgentDir(), "tracking-index.json");
|
|
976
|
-
}
|
|
977
|
-
|
|
978
1027
|
// =============================================================================
|
|
979
1028
|
// Install identity
|
|
980
1029
|
// =============================================================================
|
|
@@ -982,6 +1031,17 @@ export function getTrackingIndexPath(agentDir?: string): string {
|
|
|
982
1031
|
let cachedInstallId: string | null = null;
|
|
983
1032
|
|
|
984
1033
|
const INSTALL_ID_FILE = "install-id";
|
|
1034
|
+
/**
|
|
1035
|
+
* Application label for usage attribution (`OMP_APP_NAME`), defaulting to
|
|
1036
|
+
* `omp`. Embedders that drive omp programmatically (robomp, CI bots, …) set
|
|
1037
|
+
* the env var so broker-side per-client burn tracking can answer "what did
|
|
1038
|
+
* app X use" instead of folding everything into one install-wide bucket.
|
|
1039
|
+
*/
|
|
1040
|
+
export function getAppName(): string {
|
|
1041
|
+
const value = process.env.OMP_APP_NAME?.trim();
|
|
1042
|
+
return value ? value : "omp";
|
|
1043
|
+
}
|
|
1044
|
+
|
|
985
1045
|
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
986
1046
|
|
|
987
1047
|
/**
|
|
@@ -1052,3 +1112,13 @@ export function getInstallId(): string {
|
|
|
1052
1112
|
export function __resetInstallIdCacheForTests(): void {
|
|
1053
1113
|
cachedInstallId = null;
|
|
1054
1114
|
}
|
|
1115
|
+
|
|
1116
|
+
/** Get the project-level tracking directory (<project>/.zeta/tracking). */
|
|
1117
|
+
export function getProjectTrackingDir(cwd: string = getProjectDir()): string {
|
|
1118
|
+
return path.join(getProjectAgentDir(cwd), "tracking");
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
/** Get the global tracking index path (~/.zeta/agent/tracking-index.json). */
|
|
1122
|
+
export function getTrackingIndexPath(agentDir?: string): string {
|
|
1123
|
+
return path.join(agentDir ?? getAgentDir(), "tracking-index.json");
|
|
1124
|
+
}
|
package/src/env.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as os from "node:os";
|
|
3
3
|
import * as path from "node:path";
|
|
4
|
-
import { getAgentDir, getConfigRootDir, refreshDirsFromEnv } from "./dirs";
|
|
4
|
+
import { getAgentDir, getConfigRootDir, getProjectDir, refreshDirsFromEnv } from "./dirs";
|
|
5
5
|
|
|
6
6
|
export * from "./worker-host";
|
|
7
7
|
|
|
@@ -228,7 +228,7 @@ export function parseEnvFile(filePath: string): Record<string, string> {
|
|
|
228
228
|
const homeEnv = parseEnvFile(path.join(os.homedir(), ".env"));
|
|
229
229
|
const piEnv = parseEnvFile(path.join(getConfigRootDir(), ".env"));
|
|
230
230
|
const agentEnv = parseEnvFile(path.join(getAgentDir(), ".env"));
|
|
231
|
-
const projectEnv = parseEnvFile(path.join(
|
|
231
|
+
const projectEnv = parseEnvFile(path.join(getProjectDir(), ".env"));
|
|
232
232
|
|
|
233
233
|
for (const key of Object.keys(Bun.env)) {
|
|
234
234
|
const value = Bun.env[key];
|
package/src/index.ts
CHANGED
package/src/json.ts
CHANGED
|
@@ -21,3 +21,26 @@ export function tryParseJson<T = unknown>(content: string): T | null {
|
|
|
21
21
|
export function stringifyJson(value: unknown, space?: string | number): string | undefined {
|
|
22
22
|
return JSON.stringify(value, (_key, item) => (typeof item === "bigint" ? item.toString() : item), space);
|
|
23
23
|
}
|
|
24
|
+
|
|
25
|
+
function stableJsonClone(value: unknown): unknown {
|
|
26
|
+
if (Array.isArray(value)) return value.map(stableJsonClone);
|
|
27
|
+
if (value !== null && typeof value === "object") {
|
|
28
|
+
const sorted = Object.create(null) as Record<string, unknown>;
|
|
29
|
+
for (const key of Object.keys(value).sort()) {
|
|
30
|
+
sorted[key] = stableJsonClone(Reflect.get(value, key));
|
|
31
|
+
}
|
|
32
|
+
return sorted;
|
|
33
|
+
}
|
|
34
|
+
return value;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Deterministically serialize JSON-shaped data by sorting object keys at every
|
|
39
|
+
* depth while preserving array order. Throws for values JSON cannot represent
|
|
40
|
+
* as a top-level value instead of returning an easy-to-misuse undefined.
|
|
41
|
+
*/
|
|
42
|
+
export function stableStringifyJson(value: unknown): string {
|
|
43
|
+
const serialized = JSON.stringify(stableJsonClone(value));
|
|
44
|
+
if (serialized === undefined) throw new TypeError("Value is not JSON-serializable");
|
|
45
|
+
return serialized;
|
|
46
|
+
}
|