@linxiraos/pi-utils 1.1.22 → 1.1.24
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 +2 -2
- package/THIRD-PARTY-NOTICES.txt +3 -3
- package/dist/types/async.d.ts +6 -1
- package/dist/types/dirs.d.ts +49 -14
- package/dist/types/file-lock.d.ts +2 -0
- package/dist/types/format.d.ts +2 -0
- package/dist/types/logger/rotating-file.d.ts +36 -6
- package/dist/types/logger.d.ts +9 -0
- package/dist/types/marked/core.d.ts +24 -0
- package/dist/types/marked/inline-closers.d.ts +119 -0
- package/dist/types/marked/inline-text.d.ts +70 -0
- package/dist/types/math-delimiters.d.ts +22 -0
- package/dist/types/postmortem.d.ts +4 -0
- package/package.json +2 -2
- package/src/async.ts +11 -1
- package/src/dirs.ts +178 -47
- package/src/file-lock.ts +8 -1
- package/src/format.ts +5 -0
- package/src/logger/rotating-file.ts +105 -81
- package/src/logger.ts +103 -29
- package/src/marked/core.ts +234 -166
- package/src/marked/inline-closers.ts +434 -0
- package/src/marked/inline-text.ts +321 -0
- package/src/math-delimiters.ts +121 -19
- package/src/postmortem.ts +8 -3
package/src/dirs.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Centralized path helpers for zeta config directories.
|
|
2
|
+
* Centralized path helpers for zeta-c config directories.
|
|
3
3
|
*
|
|
4
4
|
* Uses PI_CONFIG_DIR (default ".zeta") for the config root and
|
|
5
5
|
* ZETA_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/zeta/. This requires running `zeta config migrate` first to
|
|
9
|
+
* $XDG_*_HOME/zeta/. This requires running `zeta-c 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, zeta trusts that the migration has been done.
|
|
12
12
|
*/
|
|
@@ -40,7 +40,7 @@ export const MAIN_CONFIG_FILENAMES = ["config.yml", "config.yaml"] as const;
|
|
|
40
40
|
/** Version (e.g. "1.0.0") */
|
|
41
41
|
export const VERSION: string = version;
|
|
42
42
|
|
|
43
|
-
/** Default User-Agent header string (e.g. "zeta/
|
|
43
|
+
/** Default User-Agent header string (e.g. "zeta/17.2.12") */
|
|
44
44
|
export const USER_AGENT = `zeta/${VERSION}`;
|
|
45
45
|
|
|
46
46
|
/** Minimum Bun version */
|
|
@@ -91,8 +91,8 @@ export function normalizeProfileName(profile: string | undefined): string | unde
|
|
|
91
91
|
* validation/normalization to {@link normalizeProfileName} (which throws on a
|
|
92
92
|
* syntactically invalid value).
|
|
93
93
|
*/
|
|
94
|
-
export function resolveProfileEnv(
|
|
95
|
-
return normalizeProfileName(
|
|
94
|
+
export function resolveProfileEnv(zeta: string | undefined): string | undefined {
|
|
95
|
+
return normalizeProfileName(zeta);
|
|
96
96
|
}
|
|
97
97
|
|
|
98
98
|
function getProfileFromEnv(): string | undefined {
|
|
@@ -330,6 +330,7 @@ class DirResolver {
|
|
|
330
330
|
// With XDG on Linux, they point to $XDG_*_HOME/zeta/.
|
|
331
331
|
readonly #rootDirs: Record<XdgCategory, string>;
|
|
332
332
|
readonly #agentDirs: Record<XdgCategory, string>;
|
|
333
|
+
readonly #baseRootDirs: Record<XdgCategory, string>;
|
|
333
334
|
|
|
334
335
|
readonly #rootCache = new Map<string, string>();
|
|
335
336
|
readonly #agentCache = new Map<string, string>();
|
|
@@ -344,7 +345,7 @@ class DirResolver {
|
|
|
344
345
|
const isDefault = this.agentDir === defaultAgent;
|
|
345
346
|
|
|
346
347
|
// XDG is a Linux convention. On supported platforms, default profile state
|
|
347
|
-
// resolves under $XDG_*_HOME/zeta once `zeta config init-xdg` has migrated
|
|
348
|
+
// resolves under $XDG_*_HOME/zeta once `zeta-c config init-xdg` has migrated
|
|
348
349
|
// the user's data. Named profiles follow a stricter rule: the XDG choice
|
|
349
350
|
// is keyed on the profile-specific XDG path, never the base app root.
|
|
350
351
|
//
|
|
@@ -358,7 +359,8 @@ class DirResolver {
|
|
|
358
359
|
let xdgData: string | undefined;
|
|
359
360
|
let xdgState: string | undefined;
|
|
360
361
|
let xdgCache: string | undefined;
|
|
361
|
-
|
|
362
|
+
const xdgPlatform = process.platform === "linux" || process.platform === "darwin";
|
|
363
|
+
if (xdgPlatform && isDefault) {
|
|
362
364
|
const resolveIf = (envVar: string) => {
|
|
363
365
|
const value = process.env[envVar];
|
|
364
366
|
if (!value) return undefined;
|
|
@@ -366,14 +368,10 @@ class DirResolver {
|
|
|
366
368
|
const appRoot = path.join(value, APP_NAME);
|
|
367
369
|
if (profile) {
|
|
368
370
|
const profilePath = path.join(appRoot, "profiles", profile);
|
|
369
|
-
if (fs.existsSync(profilePath))
|
|
370
|
-
return profilePath;
|
|
371
|
-
}
|
|
371
|
+
if (fs.existsSync(profilePath)) return profilePath;
|
|
372
372
|
return undefined;
|
|
373
373
|
}
|
|
374
|
-
|
|
375
|
-
return appRoot;
|
|
376
|
-
}
|
|
374
|
+
return fs.existsSync(appRoot) ? appRoot : undefined;
|
|
377
375
|
} catch {}
|
|
378
376
|
return undefined;
|
|
379
377
|
};
|
|
@@ -382,6 +380,22 @@ class DirResolver {
|
|
|
382
380
|
xdgCache = resolveIf("XDG_CACHE_HOME");
|
|
383
381
|
}
|
|
384
382
|
|
|
383
|
+
// XDG choice for machine-global paths (daemon scopes shared by every
|
|
384
|
+
// process): keyed only on the base app root, independent of both the
|
|
385
|
+
// profile and any agent-dir override, so every zeta process on the machine
|
|
386
|
+
// agrees on one location. These hold process-scoped runtime state
|
|
387
|
+
// (sockets, tokens), so there is no migration to protect.
|
|
388
|
+
const resolveBase = (envVar: string) => {
|
|
389
|
+
if (!xdgPlatform) return undefined;
|
|
390
|
+
const value = process.env[envVar];
|
|
391
|
+
if (!value) return undefined;
|
|
392
|
+
try {
|
|
393
|
+
const appRoot = path.join(value, APP_NAME);
|
|
394
|
+
return fs.existsSync(appRoot) ? appRoot : undefined;
|
|
395
|
+
} catch {}
|
|
396
|
+
return undefined;
|
|
397
|
+
};
|
|
398
|
+
|
|
385
399
|
this.#rootDirs = {
|
|
386
400
|
data: xdgData ?? this.configRoot,
|
|
387
401
|
state: xdgState ?? this.configRoot,
|
|
@@ -393,6 +407,17 @@ class DirResolver {
|
|
|
393
407
|
state: xdgState ?? this.agentDir,
|
|
394
408
|
cache: xdgCache ?? this.agentDir,
|
|
395
409
|
};
|
|
410
|
+
const baseRoot = getBaseConfigRoot();
|
|
411
|
+
this.#baseRootDirs = {
|
|
412
|
+
data: resolveBase("XDG_DATA_HOME") ?? baseRoot,
|
|
413
|
+
state: resolveBase("XDG_STATE_HOME") ?? baseRoot,
|
|
414
|
+
cache: resolveBase("XDG_CACHE_HOME") ?? baseRoot,
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/** Profile-independent config-root subdirectory, with optional XDG override. Shared across profiles. */
|
|
419
|
+
baseRootSubdir(subdir: string, xdg?: XdgCategory): string {
|
|
420
|
+
return path.join(xdg ? this.#baseRootDirs[xdg] : getBaseConfigRoot(), subdir);
|
|
396
421
|
}
|
|
397
422
|
|
|
398
423
|
/** Config-root subdirectory, with optional XDG override. */
|
|
@@ -429,12 +454,8 @@ class DirResolver {
|
|
|
429
454
|
* without exporting it). Returns `undefined` in those cases so reset falls back
|
|
430
455
|
* to the standard `~/.zeta/agent`.
|
|
431
456
|
*/
|
|
432
|
-
function resolvePreProfileAgentDir(
|
|
433
|
-
profile
|
|
434
|
-
agentDirEnv: string | undefined,
|
|
435
|
-
profileAgentDirSource: string | undefined = profile,
|
|
436
|
-
): string | undefined {
|
|
437
|
-
return isProfileDerivedAgentDir(profile ?? profileAgentDirSource, agentDirEnv) ? undefined : agentDirEnv;
|
|
457
|
+
function resolvePreProfileAgentDir(profile: string | undefined, agentDirEnv: string | undefined): string | undefined {
|
|
458
|
+
return isProfileDerivedAgentDir(profile, agentDirEnv) ? undefined : agentDirEnv;
|
|
438
459
|
}
|
|
439
460
|
|
|
440
461
|
let activeProfile = readProfileFromEnvSafe();
|
|
@@ -470,8 +491,9 @@ let dirs = new DirResolver({
|
|
|
470
491
|
* with `ZETA_CODING_AGENT_DIR=/custom` then `setProfile("work")` then
|
|
471
492
|
* `setProfile(undefined)` would silently lose `/custom` and fall back to
|
|
472
493
|
* `~/.zeta/agent`. Captured at module load — ignoring a profile-derived value
|
|
473
|
-
* inherited from a parent's `setProfile` (
|
|
474
|
-
*
|
|
494
|
+
* inherited from a parent's `setProfile` (see {@link resolvePreProfileAgentDir})
|
|
495
|
+
* — and refreshed on `setAgentDir`, since that call is the user explicitly
|
|
496
|
+
* redefining the baseline.
|
|
475
497
|
*/
|
|
476
498
|
let preProfileAgentDirEnv: string | undefined = resolveActiveAgentDirOverride();
|
|
477
499
|
// Anchor home for the resolver. Captured at module load to stay stable across
|
|
@@ -511,6 +533,7 @@ export function setAgentDir(dir: string): void {
|
|
|
511
533
|
activeProfile = undefined;
|
|
512
534
|
dirs = new DirResolver({ agentDirOverride: dir });
|
|
513
535
|
process.env.ZETA_CODING_AGENT_DIR = dir;
|
|
536
|
+
preProfileAgentDirEnv = dir;
|
|
514
537
|
for (const key of PROFILE_ENV_KEYS) {
|
|
515
538
|
delete process.env[key];
|
|
516
539
|
}
|
|
@@ -540,6 +563,7 @@ export function __resetDirsFromEnvForTests(): void {
|
|
|
540
563
|
refreshDirsFromEnv();
|
|
541
564
|
}
|
|
542
565
|
|
|
566
|
+
/** Activate a named profile. Passing undefined or "default" returns to the default profile. */
|
|
543
567
|
export function setProfile(profile: string | undefined): void {
|
|
544
568
|
const next = normalizeProfileName(profile);
|
|
545
569
|
if (next && !activeProfile) {
|
|
@@ -603,7 +627,7 @@ export function getLogsDir(): string {
|
|
|
603
627
|
|
|
604
628
|
/**
|
|
605
629
|
* Local-timezone `YYYY-MM-DD` day key (zero-padded), formatted exactly like
|
|
606
|
-
* the rotating log sink's file naming: log files are named `
|
|
630
|
+
* the rotating log sink's file naming: log files are named `zeta.<day>.<pid>.log`
|
|
607
631
|
* with the LOCAL day, not the UTC day `toISOString()` yields. Anything that
|
|
608
632
|
* computes "today's" log path or matches same-day log files by name must use
|
|
609
633
|
* this key, or between local midnight and UTC midnight it points at files that
|
|
@@ -645,11 +669,89 @@ export function getPluginsPackageJson(home?: string): string {
|
|
|
645
669
|
return path.join(getPluginsDir(home), "package.json");
|
|
646
670
|
}
|
|
647
671
|
|
|
648
|
-
/** Plugin lock file (~/.zeta/plugins/omp-plugins.lock.json).
|
|
672
|
+
/** Plugin lock file (~/.zeta/plugins/omp-plugins.lock.json). The on-disk
|
|
673
|
+
* filename stays upstream-compatible so existing installs keep their runtime
|
|
674
|
+
* state (enabled features, versions) across upgrades — never rebrand it. */
|
|
649
675
|
export function getPluginsLockfile(home?: string): string {
|
|
650
676
|
return path.join(getPluginsDir(home), "omp-plugins.lock.json");
|
|
651
677
|
}
|
|
652
678
|
|
|
679
|
+
/**
|
|
680
|
+
* Normalize a plugin id into a safe single path segment (spec §4.2): lowercased,
|
|
681
|
+
* every character outside `[a-z0-9._-]` (including `/` and `\`, so scoped npm
|
|
682
|
+
* names like `@scope/name` collapse to `-scope-name`) replaced with `-`.
|
|
683
|
+
* Guarantees the result cannot traverse: it never contains a separator, and
|
|
684
|
+
* degenerate segments (`.`, `..`, empty) throw instead of escaping.
|
|
685
|
+
*/
|
|
686
|
+
export function normalizePluginId(pluginId: string): string {
|
|
687
|
+
const normalized = pluginId.toLowerCase().replace(/[^a-z0-9._-]/g, "-");
|
|
688
|
+
if (normalized.length === 0 || normalized === "." || normalized === "..") {
|
|
689
|
+
throw new TypeError(`Invalid plugin id: "${pluginId}"`);
|
|
690
|
+
}
|
|
691
|
+
return normalized;
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
function getPluginTierDir(tier: "data" | "cache" | "state", pluginId: string, home?: string): string {
|
|
695
|
+
return path.join(getPluginsDir(home), tier, normalizePluginId(pluginId));
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
/** Per-plugin persistent data dir (~/.zeta/plugins/data/<id>). User data: cleared on uninstall, kept across upgrades. */
|
|
699
|
+
export function getPluginDataDir(pluginId: string, home?: string): string {
|
|
700
|
+
return getPluginTierDir("data", pluginId, home);
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
/** Per-plugin rebuildable cache dir (~/.zeta/plugins/cache/<id>). May be wiped by GC at any time. */
|
|
704
|
+
export function getPluginCacheDir(pluginId: string, home?: string): string {
|
|
705
|
+
return getPluginTierDir("cache", pluginId, home);
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
/** Per-plugin runtime state dir (~/.zeta/plugins/state/<id>). Locks, cursors, migration markers. */
|
|
709
|
+
export function getPluginStateDir(pluginId: string, home?: string): string {
|
|
710
|
+
return getPluginTierDir("state", pluginId, home);
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
/**
|
|
714
|
+
* Adopt a legacy config file into its new home: one-shot best-effort copy
|
|
715
|
+
* (spec §4.5). Copies only when the target does not exist yet and the legacy
|
|
716
|
+
* file does; the legacy file is left in place for older versions sharing the
|
|
717
|
+
* profile. Returns whether a copy happened this call. Opportunistic: a copy
|
|
718
|
+
* race or unwritable target dir falls back to a fresh file at the new path.
|
|
719
|
+
*/
|
|
720
|
+
export function adoptLegacyFile(legacyPath: string, targetPath: string): boolean {
|
|
721
|
+
if (targetPath === legacyPath) return false;
|
|
722
|
+
try {
|
|
723
|
+
if (fs.existsSync(targetPath) || !fs.existsSync(legacyPath)) return false;
|
|
724
|
+
fs.mkdirSync(path.dirname(targetPath), { recursive: true });
|
|
725
|
+
fs.copyFileSync(legacyPath, targetPath, fs.constants.COPYFILE_EXCL);
|
|
726
|
+
return true;
|
|
727
|
+
} catch {
|
|
728
|
+
// Opportunistic: a copy race or unwritable XDG dir falls back to a fresh
|
|
729
|
+
// file at the new path — the pre-adoption behavior.
|
|
730
|
+
return false;
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
/**
|
|
735
|
+
* One-shot legacy migration with a marker file (spec §4.5, the
|
|
736
|
+
* pi-messenger `migrations/` precedent, generalized): the first call copies
|
|
737
|
+
* `legacyPath` → `targetPath` (when legacy data exists) and writes a
|
|
738
|
+
* `migratedAt` marker at `markerPath`; every later call is a no-op regardless
|
|
739
|
+
* of filesystem state. Returns whether a copy happened this call.
|
|
740
|
+
*/
|
|
741
|
+
export function adoptLegacyFileOnce(legacyPath: string, targetPath: string, markerPath: string): boolean {
|
|
742
|
+
try {
|
|
743
|
+
if (fs.existsSync(markerPath)) return false;
|
|
744
|
+
const adopted = adoptLegacyFile(legacyPath, targetPath);
|
|
745
|
+
fs.mkdirSync(path.dirname(markerPath), { recursive: true });
|
|
746
|
+
fs.writeFileSync(markerPath, `${JSON.stringify({ migratedAt: new Date().toISOString() })}\n`);
|
|
747
|
+
return adopted;
|
|
748
|
+
} catch {
|
|
749
|
+
// Opportunistic: an unwritable marker dir must not crash callers; the
|
|
750
|
+
// next run retries the migration (still idempotent via COPYFILE_EXCL).
|
|
751
|
+
return false;
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
|
|
653
755
|
/** Get the remote mount directory (~/.zeta/remote). */
|
|
654
756
|
export function getRemoteDir(): string {
|
|
655
757
|
return dirs.rootSubdir("remote", "data");
|
|
@@ -660,8 +762,8 @@ export function getRemoteDir(): string {
|
|
|
660
762
|
* empty/whitespace input or a path that is still relative after expansion.
|
|
661
763
|
*
|
|
662
764
|
* A worktree base is process-global and consumed by both creation
|
|
663
|
-
* (PR checkout, task isolation) and cleanup (`
|
|
664
|
-
* would resolve against whatever cwd happened to launch `
|
|
765
|
+
* (PR checkout, task isolation) and cleanup (`zeta worktree`). A relative value
|
|
766
|
+
* would resolve against whatever cwd happened to launch `zeta`, so checkout and
|
|
665
767
|
* cleanup could disagree — we refuse it rather than silently bind it to cwd.
|
|
666
768
|
*/
|
|
667
769
|
function resolveWorktreeBase(value: string | undefined): string | undefined {
|
|
@@ -677,7 +779,7 @@ let worktreesDirOverride: string | undefined;
|
|
|
677
779
|
|
|
678
780
|
/**
|
|
679
781
|
* Relocate the base directory for agent-managed worktrees (PR checkouts, task
|
|
680
|
-
* isolation, and `
|
|
782
|
+
* isolation, and `zeta worktree` cleanup all read the same base). Driven by the
|
|
681
783
|
* `worktree.base` setting in coding-agent; pass `undefined`/empty to clear and
|
|
682
784
|
* fall back to `OMP_WORKTREE_DIR` or the `~/.zeta/wt` default.
|
|
683
785
|
*
|
|
@@ -898,15 +1000,21 @@ export function getTinyModelsCacheDir(agentDir?: string): string {
|
|
|
898
1000
|
export function getDocumentConversionCacheDir(agentDir?: string): string {
|
|
899
1001
|
return dirs.agentSubdir(agentDir, path.join("cache", "document-conversions"), "cache");
|
|
900
1002
|
}
|
|
901
|
-
/** Get the per-project composer speculative cache directory (~/.zeta/agent/cache/composer; XDG default: $XDG_CACHE_HOME/zeta/cache/composer). */
|
|
902
|
-
export function getComposerCacheDir(agentDir?: string): string {
|
|
903
|
-
return dirs.agentSubdir(agentDir, path.join("cache", "composer"), "cache");
|
|
904
|
-
}
|
|
905
|
-
|
|
906
1003
|
/** Get the composer speculative cache database (~/.zeta/agent/cache/composer.db; XDG default: $XDG_CACHE_HOME/zeta/cache/composer.db). */
|
|
907
1004
|
export function getComposerCacheDbPath(agentDir?: string): string {
|
|
908
1005
|
return dirs.agentSubdir(agentDir, path.join("cache", "composer.db"), "cache");
|
|
909
1006
|
}
|
|
1007
|
+
/** Get the skill descriptions database (~/.zeta/agent/skill-descriptions.db; XDG default: $XDG_DATA_HOME/zeta/skill-descriptions.db). */
|
|
1008
|
+
export function getSkillDescriptionsDbPath(agentDir?: string): string {
|
|
1009
|
+
return dirs.agentSubdir(agentDir, "skill-descriptions.db", "data");
|
|
1010
|
+
}
|
|
1011
|
+
/** Get the text-predict engine state directory (~/.zeta/agent/predict/<method>; XDG default: $XDG_DATA_HOME/zeta/predict/<method>). Adopts legacy engine state on first XDG resolution. */
|
|
1012
|
+
export function getPredictStateDir(agentDir: string | undefined, method: string): string {
|
|
1013
|
+
const subdir = path.join("predict", method);
|
|
1014
|
+
const stateDir = dirs.agentSubdir(agentDir, subdir, "data");
|
|
1015
|
+
adoptLegacyDir(path.join(agentDir ?? dirs.agentDir, subdir), stateDir);
|
|
1016
|
+
return stateDir;
|
|
1017
|
+
}
|
|
910
1018
|
|
|
911
1019
|
/** Get the sessions directory (~/.zeta/agent/sessions). */
|
|
912
1020
|
export function getSessionsDir(agentDir?: string): string {
|
|
@@ -977,10 +1085,10 @@ export function getDebugLogPath(agentDir?: string): string {
|
|
|
977
1085
|
* Best-effort one-time copy of a legacy config-root file to its redirected XDG
|
|
978
1086
|
* location. Existing installs that enable XDG after the file was created keep
|
|
979
1087
|
* their data (e.g. a placeholder key whose loss would break deobfuscation of
|
|
980
|
-
* persisted transcripts). The legacy file is left in place for older
|
|
1088
|
+
* persisted transcripts). The legacy file is left in place for older zeta
|
|
981
1089
|
* versions sharing the profile.
|
|
982
1090
|
*/
|
|
983
|
-
function
|
|
1091
|
+
function adoptLegacyFileViaStaging(legacyPath: string, targetPath: string): void {
|
|
984
1092
|
if (targetPath === legacyPath) return;
|
|
985
1093
|
try {
|
|
986
1094
|
if (fs.existsSync(targetPath) || !fs.existsSync(legacyPath)) return;
|
|
@@ -992,10 +1100,33 @@ function adoptLegacyFile(legacyPath: string, targetPath: string): void {
|
|
|
992
1100
|
}
|
|
993
1101
|
}
|
|
994
1102
|
|
|
1103
|
+
/**
|
|
1104
|
+
* Best-effort one-time copy of a legacy directory to its redirected XDG
|
|
1105
|
+
* location, so learned state survives enabling XDG. The copy is staged next to
|
|
1106
|
+
* the target and renamed into place, so a reader never sees a partial tree and
|
|
1107
|
+
* a concurrent adopter cannot clobber a finished one. The legacy directory is
|
|
1108
|
+
* left in place for older zeta versions sharing the profile.
|
|
1109
|
+
*/
|
|
1110
|
+
function adoptLegacyDir(legacyPath: string, targetPath: string): void {
|
|
1111
|
+
if (targetPath === legacyPath) return;
|
|
1112
|
+
const staging = `${targetPath}.adopt-${process.pid}`;
|
|
1113
|
+
try {
|
|
1114
|
+
if (fs.existsSync(targetPath) || !fs.statSync(legacyPath, { throwIfNoEntry: false })?.isDirectory()) return;
|
|
1115
|
+
fs.mkdirSync(path.dirname(targetPath), { recursive: true });
|
|
1116
|
+
fs.rmSync(staging, { recursive: true, force: true });
|
|
1117
|
+
fs.cpSync(legacyPath, staging, { recursive: true });
|
|
1118
|
+
fs.renameSync(staging, targetPath);
|
|
1119
|
+
} catch {
|
|
1120
|
+
// Opportunistic: a lost race or unwritable XDG dir falls back to fresh
|
|
1121
|
+
// state at the new path — the pre-adoption behavior.
|
|
1122
|
+
fs.rmSync(staging, { recursive: true, force: true });
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
|
|
995
1126
|
/** Get the secret placeholder key path (~/.zeta/agent/secret-placeholder.key; XDG default: $XDG_STATE_HOME/zeta/secret-placeholder.key). Adopts a legacy key on first XDG resolution. */
|
|
996
1127
|
export function getSecretPlaceholderKeyPath(): string {
|
|
997
1128
|
const keyPath = dirs.agentSubdir(undefined, "secret-placeholder.key", "state");
|
|
998
|
-
|
|
1129
|
+
adoptLegacyFileViaStaging(path.join(dirs.agentDir, "secret-placeholder.key"), keyPath);
|
|
999
1130
|
return keyPath;
|
|
1000
1131
|
}
|
|
1001
1132
|
|
|
@@ -1015,9 +1146,9 @@ export function getDaemonRuntimeDir(projectDir: string): string {
|
|
|
1015
1146
|
return path.join(getDaemonRuntimeRoot(), key);
|
|
1016
1147
|
}
|
|
1017
1148
|
|
|
1018
|
-
/** Root directory containing every machine-global daemon service scope. */
|
|
1149
|
+
/** Root directory containing every machine-global daemon service scope (~/.zeta/run/daemons/global; XDG default: $XDG_STATE_HOME/zeta/run/daemons/global). Shared across profiles. */
|
|
1019
1150
|
export function getGlobalDaemonRuntimeRoot(): string {
|
|
1020
|
-
return path.join(
|
|
1151
|
+
return dirs.baseRootSubdir(path.join("run", "daemons", "global"), "state");
|
|
1021
1152
|
}
|
|
1022
1153
|
|
|
1023
1154
|
/** Get a profile-independent runtime directory for a machine-global daemon service. */
|
|
@@ -1036,7 +1167,7 @@ export function getProviderInFlightRoot(): string {
|
|
|
1036
1167
|
/** Get the marketplaces registry path (~/.zeta/marketplaces.json; XDG default: $XDG_DATA_HOME/zeta/marketplaces.json). Adopts a legacy registry on first XDG resolution. */
|
|
1037
1168
|
export function getMarketplacesRegistryPath(): string {
|
|
1038
1169
|
const registryPath = dirs.rootSubdir("marketplaces.json", "data");
|
|
1039
|
-
|
|
1170
|
+
adoptLegacyFileViaStaging(path.join(dirs.configRoot, "marketplaces.json"), registryPath);
|
|
1040
1171
|
return registryPath;
|
|
1041
1172
|
}
|
|
1042
1173
|
|
|
@@ -1049,6 +1180,16 @@ export function getProjectModulesDir(cwd: string = getProjectDir()): string {
|
|
|
1049
1180
|
return path.join(getProjectAgentDir(cwd), "modules");
|
|
1050
1181
|
}
|
|
1051
1182
|
|
|
1183
|
+
/** Get the project-level tracking directory (<project>/.zeta/tracking). */
|
|
1184
|
+
export function getProjectTrackingDir(cwd: string = getProjectDir()): string {
|
|
1185
|
+
return path.join(getProjectAgentDir(cwd), "tracking");
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
/** Get the global tracking index path (~/.zeta/agent/tracking-index.json). */
|
|
1189
|
+
export function getTrackingIndexPath(agentDir?: string): string {
|
|
1190
|
+
return path.join(agentDir ?? getAgentDir(), "tracking-index.json");
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1052
1193
|
/** Get the project-level prompts directory (.zeta/prompts). */
|
|
1053
1194
|
export function getProjectPromptsDir(cwd: string = getProjectDir()): string {
|
|
1054
1195
|
return path.join(getProjectAgentDir(cwd), "prompts");
|
|
@@ -1163,16 +1304,6 @@ export function getInstallId(): string {
|
|
|
1163
1304
|
return next;
|
|
1164
1305
|
}
|
|
1165
1306
|
|
|
1166
|
-
/** Get the project-level tracking directory (<project>/.zeta/tracking). */
|
|
1167
|
-
export function getProjectTrackingDir(cwd: string = getProjectDir()): string {
|
|
1168
|
-
return path.join(getProjectAgentDir(cwd), "tracking");
|
|
1169
|
-
}
|
|
1170
|
-
|
|
1171
|
-
/** Get the global tracking index path (~/.zeta/agent/tracking-index.json). */
|
|
1172
|
-
export function getTrackingIndexPath(agentDir?: string): string {
|
|
1173
|
-
return path.join(agentDir ?? getAgentDir(), "tracking-index.json");
|
|
1174
|
-
}
|
|
1175
|
-
|
|
1176
1307
|
/** Test-only: clear cached install id. Never call from production code. */
|
|
1177
1308
|
export function __resetInstallIdCacheForTests(): void {
|
|
1178
1309
|
cachedInstallId = null;
|
package/src/file-lock.ts
CHANGED
|
@@ -35,7 +35,14 @@ function getLockPath(filePath: string): string {
|
|
|
35
35
|
|
|
36
36
|
function tryAcquireLock(lockPath: string): NativeFileLock | null {
|
|
37
37
|
const lock = NativeFileLock.tryAcquire(lockPath);
|
|
38
|
-
|
|
38
|
+
if (lock.acquired) return lock;
|
|
39
|
+
lock.release();
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Acquire an exclusive lease without waiting; `null` while another holder owns it. */
|
|
44
|
+
export function tryAcquireFileLock(filePath: string): FileLockHandle | null {
|
|
45
|
+
return tryAcquireLock(getLockPath(filePath));
|
|
39
46
|
}
|
|
40
47
|
|
|
41
48
|
/** Acquire an exclusive lease; callers must release it when their operation ends. */
|
package/src/format.ts
CHANGED
|
@@ -127,3 +127,8 @@ export function pluralize(label: string, count: number): string {
|
|
|
127
127
|
export function formatPercent(ratio: number): string {
|
|
128
128
|
return `${(ratio * 100).toFixed(1)}%`;
|
|
129
129
|
}
|
|
130
|
+
|
|
131
|
+
/** Round premium-request counters without losing legitimate fractional requests. */
|
|
132
|
+
export function normalizePremiumRequests(value: number): number {
|
|
133
|
+
return Math.round((value + Number.EPSILON) * 100) / 100;
|
|
134
|
+
}
|