@f5xc-salesdemos/pi-utils 14.0.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 +60 -0
- package/src/abortable.ts +85 -0
- package/src/async.ts +50 -0
- package/src/cli.ts +432 -0
- package/src/color.ts +204 -0
- package/src/dirs.ts +425 -0
- package/src/env.ts +84 -0
- package/src/format.ts +106 -0
- package/src/frontmatter.ts +118 -0
- package/src/fs-error.ts +56 -0
- package/src/glob.ts +189 -0
- package/src/hook-fetch.ts +30 -0
- package/src/index.ts +47 -0
- package/src/json.ts +10 -0
- package/src/logger.ts +204 -0
- package/src/mermaid-ascii.ts +31 -0
- package/src/mime.ts +159 -0
- package/src/peek-file.ts +114 -0
- package/src/postmortem.ts +197 -0
- package/src/procmgr.ts +326 -0
- package/src/prompt.ts +401 -0
- package/src/ptree.ts +386 -0
- package/src/ring.ts +169 -0
- package/src/snowflake.ts +136 -0
- package/src/stream.ts +316 -0
- package/src/temp.ts +77 -0
- package/src/type-guards.ts +11 -0
- package/src/which.ts +230 -0
package/src/color.ts
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Color manipulation utilities for hex colors.
|
|
3
|
+
*
|
|
4
|
+
* @example
|
|
5
|
+
* ```ts
|
|
6
|
+
* import { hexToHsv, hsvToHex } from "@f5xc-salesdemos/pi-utils";
|
|
7
|
+
*
|
|
8
|
+
* // Work with HSV directly
|
|
9
|
+
*
|
|
10
|
+
* // Or work with HSV directly
|
|
11
|
+
* const hsv = hexToHsv("#4ade80");
|
|
12
|
+
* hsv.h = (hsv.h + 90) % 360;
|
|
13
|
+
* const newHex = hsvToHex(hsv);
|
|
14
|
+
* ```
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
export interface HSV {
|
|
18
|
+
/** Hue in degrees (0-360) */
|
|
19
|
+
h: number;
|
|
20
|
+
/** Saturation (0-1) */
|
|
21
|
+
s: number;
|
|
22
|
+
/** Value/brightness (0-1) */
|
|
23
|
+
v: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface RGB {
|
|
27
|
+
/** Red (0-255) */
|
|
28
|
+
r: number;
|
|
29
|
+
/** Green (0-255) */
|
|
30
|
+
g: number;
|
|
31
|
+
/** Blue (0-255) */
|
|
32
|
+
b: number;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Parse a hex color string to RGB.
|
|
37
|
+
* Supports #RGB, #RRGGBB formats.
|
|
38
|
+
*/
|
|
39
|
+
export function hexToRgb(hex: string): RGB {
|
|
40
|
+
const h = hex.startsWith("#") ? hex.slice(1) : hex;
|
|
41
|
+
if (h.length === 3) {
|
|
42
|
+
return {
|
|
43
|
+
r: parseInt(h[0] + h[0], 16),
|
|
44
|
+
g: parseInt(h[1] + h[1], 16),
|
|
45
|
+
b: parseInt(h[2] + h[2], 16),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
return {
|
|
49
|
+
r: parseInt(h.slice(0, 2), 16),
|
|
50
|
+
g: parseInt(h.slice(2, 4), 16),
|
|
51
|
+
b: parseInt(h.slice(4, 6), 16),
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Convert RGB to hex color string.
|
|
57
|
+
*/
|
|
58
|
+
export function rgbToHex(rgb: RGB): string {
|
|
59
|
+
const toHex = (n: number) =>
|
|
60
|
+
Math.max(0, Math.min(255, Math.round(n)))
|
|
61
|
+
.toString(16)
|
|
62
|
+
.padStart(2, "0");
|
|
63
|
+
return `#${toHex(rgb.r)}${toHex(rgb.g)}${toHex(rgb.b)}`;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Convert RGB to HSV.
|
|
68
|
+
*/
|
|
69
|
+
export function rgbToHsv(rgb: RGB): HSV {
|
|
70
|
+
const r = rgb.r / 255;
|
|
71
|
+
const g = rgb.g / 255;
|
|
72
|
+
const b = rgb.b / 255;
|
|
73
|
+
|
|
74
|
+
const max = Math.max(r, g, b);
|
|
75
|
+
const min = Math.min(r, g, b);
|
|
76
|
+
const d = max - min;
|
|
77
|
+
|
|
78
|
+
let h = 0;
|
|
79
|
+
if (d !== 0) {
|
|
80
|
+
if (max === r) h = ((g - b) / d + (g < b ? 6 : 0)) / 6;
|
|
81
|
+
else if (max === g) h = ((b - r) / d + 2) / 6;
|
|
82
|
+
else h = ((r - g) / d + 4) / 6;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return {
|
|
86
|
+
h: h * 360,
|
|
87
|
+
s: max === 0 ? 0 : d / max,
|
|
88
|
+
v: max,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Convert HSV to RGB.
|
|
94
|
+
*/
|
|
95
|
+
export function hsvToRgb(hsv: HSV): RGB {
|
|
96
|
+
const { s, v } = hsv;
|
|
97
|
+
const h = ((hsv.h % 360) + 360) % 360; // Normalize to 0-360
|
|
98
|
+
|
|
99
|
+
const i = Math.floor(h / 60);
|
|
100
|
+
const f = h / 60 - i;
|
|
101
|
+
const p = v * (1 - s);
|
|
102
|
+
const q = v * (1 - f * s);
|
|
103
|
+
const t = v * (1 - (1 - f) * s);
|
|
104
|
+
|
|
105
|
+
let r: number, g: number, b: number;
|
|
106
|
+
switch (i % 6) {
|
|
107
|
+
case 0:
|
|
108
|
+
r = v;
|
|
109
|
+
g = t;
|
|
110
|
+
b = p;
|
|
111
|
+
break;
|
|
112
|
+
case 1:
|
|
113
|
+
r = q;
|
|
114
|
+
g = v;
|
|
115
|
+
b = p;
|
|
116
|
+
break;
|
|
117
|
+
case 2:
|
|
118
|
+
r = p;
|
|
119
|
+
g = v;
|
|
120
|
+
b = t;
|
|
121
|
+
break;
|
|
122
|
+
case 3:
|
|
123
|
+
r = p;
|
|
124
|
+
g = q;
|
|
125
|
+
b = v;
|
|
126
|
+
break;
|
|
127
|
+
case 4:
|
|
128
|
+
r = t;
|
|
129
|
+
g = p;
|
|
130
|
+
b = v;
|
|
131
|
+
break;
|
|
132
|
+
default:
|
|
133
|
+
r = v;
|
|
134
|
+
g = p;
|
|
135
|
+
b = q;
|
|
136
|
+
break;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return {
|
|
140
|
+
r: Math.round(r * 255),
|
|
141
|
+
g: Math.round(g * 255),
|
|
142
|
+
b: Math.round(b * 255),
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Convert hex color to HSV.
|
|
148
|
+
*/
|
|
149
|
+
export function hexToHsv(hex: string): HSV {
|
|
150
|
+
return rgbToHsv(hexToRgb(hex));
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Convert HSV to hex color.
|
|
155
|
+
*/
|
|
156
|
+
export function hsvToHex(hsv: HSV): string {
|
|
157
|
+
return rgbToHex(hsvToRgb(hsv));
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Shift the hue of a hex color by a given number of degrees.
|
|
162
|
+
*/
|
|
163
|
+
export function shiftHue(hex: string, degrees: number): string {
|
|
164
|
+
const hsv = hexToHsv(hex);
|
|
165
|
+
hsv.h = (hsv.h + degrees) % 360;
|
|
166
|
+
if (hsv.h < 0) hsv.h += 360;
|
|
167
|
+
return hsvToHex(hsv);
|
|
168
|
+
}
|
|
169
|
+
export interface HSVAdjustment {
|
|
170
|
+
/** Hue shift in degrees (additive) */
|
|
171
|
+
h?: number;
|
|
172
|
+
/** Saturation multiplier */
|
|
173
|
+
s?: number;
|
|
174
|
+
/** Value/brightness multiplier */
|
|
175
|
+
v?: number;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Adjust HSV components of a hex color.
|
|
180
|
+
*
|
|
181
|
+
* @param hex - Hex color string (#RGB or #RRGGBB)
|
|
182
|
+
* @param adj - Adjustments: h is additive degrees, s and v are multipliers
|
|
183
|
+
* @returns New hex color string
|
|
184
|
+
*
|
|
185
|
+
* @example
|
|
186
|
+
* ```ts
|
|
187
|
+
* // Shift hue +60°, reduce saturation to 71%
|
|
188
|
+
* adjustHsv("#00ff88", { h: 60, s: 0.71 }) // "#4a9eff"
|
|
189
|
+
* ```
|
|
190
|
+
*/
|
|
191
|
+
export function adjustHsv(hex: string, adj: HSVAdjustment): string {
|
|
192
|
+
const hsv = hexToHsv(hex);
|
|
193
|
+
if (adj.h !== undefined) {
|
|
194
|
+
hsv.h = (hsv.h + adj.h) % 360;
|
|
195
|
+
if (hsv.h < 0) hsv.h += 360;
|
|
196
|
+
}
|
|
197
|
+
if (adj.s !== undefined) {
|
|
198
|
+
hsv.s = Math.max(0, Math.min(1, hsv.s * adj.s));
|
|
199
|
+
}
|
|
200
|
+
if (adj.v !== undefined) {
|
|
201
|
+
hsv.v = Math.max(0, Math.min(1, hsv.v * adj.v));
|
|
202
|
+
}
|
|
203
|
+
return hsvToHex(hsv);
|
|
204
|
+
}
|
package/src/dirs.ts
ADDED
|
@@ -0,0 +1,425 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Centralized path helpers for xcsh config directories.
|
|
3
|
+
*
|
|
4
|
+
* Uses PI_CONFIG_DIR (default ".xcsh") for the config root and
|
|
5
|
+
* PI_CODING_AGENT_DIR to override the agent directory.
|
|
6
|
+
*
|
|
7
|
+
* On Linux, if XDG_DATA_HOME / XDG_STATE_HOME / XDG_CACHE_HOME environment
|
|
8
|
+
* variables are set, paths are redirected to XDG-compliant locations under
|
|
9
|
+
* $XDG_*_HOME/xcsh/. This requires running `xcsh config migrate` first to
|
|
10
|
+
* move data to the new locations. No filesystem existence checks are performed
|
|
11
|
+
* — if the env var is set, xcsh trusts that the migration has been done.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import * as fs from "node:fs";
|
|
15
|
+
import * as os from "node:os";
|
|
16
|
+
import * as path from "node:path";
|
|
17
|
+
import { engines, version } from "../package.json" with { type: "json" };
|
|
18
|
+
|
|
19
|
+
/** App name (e.g. "xcsh") */
|
|
20
|
+
export const APP_NAME: string = "xcsh";
|
|
21
|
+
|
|
22
|
+
/** Config directory name (e.g. ".xcsh") */
|
|
23
|
+
export const CONFIG_DIR_NAME: string = ".xcsh";
|
|
24
|
+
|
|
25
|
+
/** Version (e.g. "1.0.0") */
|
|
26
|
+
export const VERSION: string = version;
|
|
27
|
+
|
|
28
|
+
/** Minimum Bun version */
|
|
29
|
+
export const MIN_BUN_VERSION: string = engines.bun.replace(/[^0-9.]/g, "");
|
|
30
|
+
|
|
31
|
+
// =============================================================================
|
|
32
|
+
// Project directory
|
|
33
|
+
// =============================================================================
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* On macOS, strip /private prefix only when both paths resolve to the same location.
|
|
37
|
+
* This preserves aliases like /private/tmp -> /tmp without rewriting unrelated paths.
|
|
38
|
+
*/
|
|
39
|
+
function standardizeMacOSPath(p: string): string {
|
|
40
|
+
if (process.platform !== "darwin" || !p.startsWith("/private/")) return p;
|
|
41
|
+
const stripped = p.slice("/private".length);
|
|
42
|
+
try {
|
|
43
|
+
if (fs.realpathSync(p) === fs.realpathSync(stripped)) {
|
|
44
|
+
return stripped;
|
|
45
|
+
}
|
|
46
|
+
} catch {}
|
|
47
|
+
return p;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function resolveEquivalentPath(inputPath: string): string {
|
|
51
|
+
const resolvedPath = path.resolve(inputPath);
|
|
52
|
+
try {
|
|
53
|
+
return fs.realpathSync(resolvedPath);
|
|
54
|
+
} catch {
|
|
55
|
+
return resolvedPath;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function normalizePathForComparison(inputPath: string): string {
|
|
60
|
+
const resolvedPath = resolveEquivalentPath(inputPath);
|
|
61
|
+
return process.platform === "win32" ? resolvedPath.toLowerCase() : resolvedPath;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function pathIsWithin(root: string, candidate: string): boolean {
|
|
65
|
+
const normalizedRoot = normalizePathForComparison(root);
|
|
66
|
+
const normalizedCandidate = normalizePathForComparison(candidate);
|
|
67
|
+
const relative = path.relative(normalizedRoot, normalizedCandidate);
|
|
68
|
+
return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function relativePathWithinRoot(root: string, candidate: string): string | null {
|
|
72
|
+
if (!pathIsWithin(root, candidate)) return null;
|
|
73
|
+
const normalizedRoot = normalizePathForComparison(root);
|
|
74
|
+
const normalizedCandidate = normalizePathForComparison(candidate);
|
|
75
|
+
const relative = path.relative(normalizedRoot, normalizedCandidate);
|
|
76
|
+
return relative || null;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
let projectDir = standardizeMacOSPath(process.cwd());
|
|
80
|
+
|
|
81
|
+
/** Get the project directory. */
|
|
82
|
+
export function getProjectDir(): string {
|
|
83
|
+
return projectDir;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Set the project directory. */
|
|
87
|
+
export function setProjectDir(dir: string): void {
|
|
88
|
+
projectDir = standardizeMacOSPath(path.resolve(dir));
|
|
89
|
+
process.chdir(projectDir);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Get the config directory name relative to home (e.g. ".xcsh" or PI_CONFIG_DIR override). */
|
|
93
|
+
export function getConfigDirName(): string {
|
|
94
|
+
return process.env.PI_CONFIG_DIR || CONFIG_DIR_NAME;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Get the config agent directory name relative to home (e.g. ".xcsh/agent" or PI_CONFIG_DIR + "/agent"). */
|
|
98
|
+
export function getConfigAgentDirName(): string {
|
|
99
|
+
return `${getConfigDirName()}/agent`;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// =============================================================================
|
|
103
|
+
// DirResolver — cached, XDG-aware path resolution
|
|
104
|
+
// =============================================================================
|
|
105
|
+
|
|
106
|
+
type XdgCategory = "data" | "state" | "cache";
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Resolves and caches all xcsh directory paths. On Linux, when XDG environment
|
|
110
|
+
* variables are set, paths are redirected under $XDG_*_HOME/xcsh/. A new
|
|
111
|
+
* instance is created whenever the agent directory changes, which naturally
|
|
112
|
+
* invalidates all cached paths.
|
|
113
|
+
*/
|
|
114
|
+
class DirResolver {
|
|
115
|
+
readonly configRoot: string;
|
|
116
|
+
readonly agentDir: string;
|
|
117
|
+
|
|
118
|
+
// Per-category base dirs. Without XDG, all three equal configRoot / agentDir.
|
|
119
|
+
// With XDG on Linux, they point to $XDG_*_HOME/xcsh/.
|
|
120
|
+
readonly #rootDirs: Record<XdgCategory, string>;
|
|
121
|
+
readonly #agentDirs: Record<XdgCategory, string>;
|
|
122
|
+
|
|
123
|
+
readonly #rootCache = new Map<string, string>();
|
|
124
|
+
readonly #agentCache = new Map<string, string>();
|
|
125
|
+
|
|
126
|
+
constructor(agentDirOverride?: string) {
|
|
127
|
+
this.configRoot = path.join(os.homedir(), getConfigDirName());
|
|
128
|
+
|
|
129
|
+
const defaultAgent = path.join(this.configRoot, "agent");
|
|
130
|
+
this.agentDir = agentDirOverride ? path.resolve(agentDirOverride) : defaultAgent;
|
|
131
|
+
const isDefault = this.agentDir === defaultAgent;
|
|
132
|
+
|
|
133
|
+
// XDG is a Linux convention. On other platforms, or for non-default
|
|
134
|
+
// profiles, all categories resolve to the legacy paths.
|
|
135
|
+
let xdgData: string | undefined;
|
|
136
|
+
let xdgState: string | undefined;
|
|
137
|
+
let xdgCache: string | undefined;
|
|
138
|
+
if ((process.platform === "linux" || process.platform === "darwin") && isDefault) {
|
|
139
|
+
const resolveIf = (envVar: string) => {
|
|
140
|
+
const value = process.env[envVar];
|
|
141
|
+
if (value) {
|
|
142
|
+
try {
|
|
143
|
+
const joined = path.join(value, APP_NAME);
|
|
144
|
+
if (fs.existsSync(joined)) {
|
|
145
|
+
return joined;
|
|
146
|
+
}
|
|
147
|
+
} catch {}
|
|
148
|
+
}
|
|
149
|
+
return undefined;
|
|
150
|
+
};
|
|
151
|
+
xdgData = resolveIf("XDG_DATA_HOME");
|
|
152
|
+
xdgState = resolveIf("XDG_STATE_HOME");
|
|
153
|
+
xdgCache = resolveIf("XDG_CACHE_HOME");
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
this.#rootDirs = {
|
|
157
|
+
data: xdgData ?? this.configRoot,
|
|
158
|
+
state: xdgState ?? this.configRoot,
|
|
159
|
+
cache: xdgCache ?? this.configRoot,
|
|
160
|
+
};
|
|
161
|
+
// XDG flattens the agent/ prefix: ~/.xcsh/agent/sessions → $XDG_DATA_HOME/xcsh/sessions
|
|
162
|
+
this.#agentDirs = {
|
|
163
|
+
data: xdgData ?? this.agentDir,
|
|
164
|
+
state: xdgState ?? this.agentDir,
|
|
165
|
+
cache: xdgCache ?? this.agentDir,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** Config-root subdirectory, with optional XDG override. */
|
|
170
|
+
rootSubdir(subdir: string, xdg?: XdgCategory): string {
|
|
171
|
+
const cached = this.#rootCache.get(subdir);
|
|
172
|
+
if (cached) return cached;
|
|
173
|
+
const base = xdg ? this.#rootDirs[xdg] : this.configRoot;
|
|
174
|
+
const result = path.join(base, subdir);
|
|
175
|
+
this.#rootCache.set(subdir, result);
|
|
176
|
+
return result;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** Agent subdirectory, with optional XDG override. */
|
|
180
|
+
agentSubdir(userAgentDir: string | undefined, subdir: string, xdg?: XdgCategory): string {
|
|
181
|
+
if (!userAgentDir || userAgentDir === this.agentDir) {
|
|
182
|
+
const cached = this.#agentCache.get(subdir);
|
|
183
|
+
if (cached) return cached;
|
|
184
|
+
const base = xdg ? this.#agentDirs[xdg] : this.agentDir;
|
|
185
|
+
const result = path.join(base, subdir);
|
|
186
|
+
this.#agentCache.set(subdir, result);
|
|
187
|
+
return result;
|
|
188
|
+
}
|
|
189
|
+
return path.join(userAgentDir, subdir);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
let dirs = new DirResolver(process.env.PI_CODING_AGENT_DIR);
|
|
194
|
+
|
|
195
|
+
// =============================================================================
|
|
196
|
+
// Root directories
|
|
197
|
+
// =============================================================================
|
|
198
|
+
|
|
199
|
+
/** Get the config root directory (~/.xcsh). */
|
|
200
|
+
export function getConfigRootDir(): string {
|
|
201
|
+
return dirs.configRoot;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/** Set the coding agent directory. Creates a fresh resolver, invalidating all cached paths. */
|
|
205
|
+
export function setAgentDir(dir: string): void {
|
|
206
|
+
dirs = new DirResolver(dir);
|
|
207
|
+
process.env.PI_CODING_AGENT_DIR = dir;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/** Get the agent config directory (~/.xcsh/agent). */
|
|
211
|
+
export function getAgentDir(): string {
|
|
212
|
+
return dirs.agentDir;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/** Get the project-local config directory (.xcsh). */
|
|
216
|
+
export function getProjectAgentDir(cwd: string = getProjectDir()): string {
|
|
217
|
+
return path.join(cwd, CONFIG_DIR_NAME);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// =============================================================================
|
|
221
|
+
// Config-root subdirectories (~/.xcsh/*)
|
|
222
|
+
// =============================================================================
|
|
223
|
+
|
|
224
|
+
/** Get the reports directory (~/.xcsh/reports). */
|
|
225
|
+
export function getReportsDir(): string {
|
|
226
|
+
return dirs.rootSubdir("reports", "state");
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/** Get the logs directory (~/.xcsh/logs). */
|
|
230
|
+
export function getLogsDir(): string {
|
|
231
|
+
return dirs.rootSubdir("logs", "state");
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/** Get the path to a dated log file (~/.xcsh/logs/xcsh.YYYY-MM-DD.log). */
|
|
235
|
+
export function getLogPath(date = new Date()): string {
|
|
236
|
+
return path.join(getLogsDir(), `${APP_NAME}.${date.toISOString().slice(0, 10)}.log`);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/** Get the plugins directory (~/.xcsh/plugins). */
|
|
240
|
+
export function getPluginsDir(): string {
|
|
241
|
+
return dirs.rootSubdir("plugins", "data");
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/** Where npm installs packages (~/.xcsh/plugins/node_modules). */
|
|
245
|
+
export function getPluginsNodeModules(): string {
|
|
246
|
+
return path.join(getPluginsDir(), "node_modules");
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/** Plugin manifest (~/.xcsh/plugins/package.json). */
|
|
250
|
+
export function getPluginsPackageJson(): string {
|
|
251
|
+
return path.join(getPluginsDir(), "package.json");
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/** Plugin lock file (~/.xcsh/plugins/xcsh-plugins.lock.json). */
|
|
255
|
+
export function getPluginsLockfile(): string {
|
|
256
|
+
return path.join(getPluginsDir(), "xcsh-plugins.lock.json");
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/** Get the remote mount directory (~/.xcsh/remote). */
|
|
260
|
+
export function getRemoteDir(): string {
|
|
261
|
+
return dirs.rootSubdir("remote", "data");
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/** Get the SSH control socket directory (~/.xcsh/ssh-control). */
|
|
265
|
+
export function getSshControlDir(): string {
|
|
266
|
+
return dirs.rootSubdir("ssh-control", "state");
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/** Get the remote host info directory (~/.xcsh/remote-host). */
|
|
270
|
+
export function getRemoteHostDir(): string {
|
|
271
|
+
return dirs.rootSubdir("remote-host", "data");
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/** Get the managed Python venv directory (~/.xcsh/python-env). */
|
|
275
|
+
export function getPythonEnvDir(): string {
|
|
276
|
+
return dirs.rootSubdir("python-env", "data");
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/** Get the puppeteer sandbox directory (~/.xcsh/puppeteer). */
|
|
280
|
+
export function getPuppeteerDir(): string {
|
|
281
|
+
return dirs.rootSubdir("puppeteer", "cache");
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/** Get the worktree base directory (~/.xcsh/wt). */
|
|
285
|
+
export function getWorktreeBaseDir(): string {
|
|
286
|
+
return dirs.rootSubdir("wt", "data");
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/** Get the path to a worktree directory (~/.xcsh/wt/<project>/<id>). */
|
|
290
|
+
export function getWorktreeDir(encodedProject: string, id: string): string {
|
|
291
|
+
return path.join(getWorktreeBaseDir(), encodedProject, id);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/** Get the GPU cache path (~/.xcsh/gpu_cache.json). */
|
|
295
|
+
export function getGpuCachePath(): string {
|
|
296
|
+
return dirs.rootSubdir("gpu_cache.json", "cache");
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/** Get the natives directory (~/.xcsh/natives). */
|
|
300
|
+
export function getNativesDir(): string {
|
|
301
|
+
return dirs.rootSubdir("natives", "cache");
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/** Get the stats database path (~/.xcsh/stats.db). */
|
|
305
|
+
export function getStatsDbPath(): string {
|
|
306
|
+
return dirs.rootSubdir("stats.db", "data");
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// =============================================================================
|
|
310
|
+
// Agent subdirectories (~/.xcsh/agent/*)
|
|
311
|
+
// =============================================================================
|
|
312
|
+
|
|
313
|
+
/** Get the path to agent.db (SQLite database for settings and auth storage). */
|
|
314
|
+
export function getAgentDbPath(agentDir?: string): string {
|
|
315
|
+
return dirs.agentSubdir(agentDir, "agent.db", "data");
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/** Get the path to history.db (SQLite database for session history). */
|
|
319
|
+
export function getHistoryDbPath(agentDir?: string): string {
|
|
320
|
+
return dirs.agentSubdir(agentDir, "history.db", "data");
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/** Get the path to models.db (model cache database). */
|
|
324
|
+
export function getModelDbPath(agentDir?: string): string {
|
|
325
|
+
return dirs.agentSubdir(agentDir, "models.db", "data");
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/** Get the directory path for the shared search DB state (~/.xcsh/agent/search-db). */
|
|
329
|
+
export function getSearchDbDir(agentDir?: string): string {
|
|
330
|
+
return dirs.agentSubdir(agentDir, "search-db", "data");
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/** Get the sessions directory (~/.xcsh/agent/sessions). */
|
|
334
|
+
export function getSessionsDir(agentDir?: string): string {
|
|
335
|
+
return dirs.agentSubdir(agentDir, "sessions", "data");
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/** Get the content-addressed blob store directory (~/.xcsh/agent/blobs). */
|
|
339
|
+
export function getBlobsDir(agentDir?: string): string {
|
|
340
|
+
return dirs.agentSubdir(agentDir, "blobs", "data");
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/** Get the custom themes directory (~/.xcsh/agent/themes). */
|
|
344
|
+
export function getCustomThemesDir(agentDir?: string): string {
|
|
345
|
+
return dirs.agentSubdir(agentDir, "themes");
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/** Get the tools directory (~/.xcsh/agent/tools). */
|
|
349
|
+
export function getToolsDir(agentDir?: string): string {
|
|
350
|
+
return dirs.agentSubdir(agentDir, "tools");
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/** Get the slash commands directory (~/.xcsh/agent/commands). */
|
|
354
|
+
export function getCommandsDir(agentDir?: string): string {
|
|
355
|
+
return dirs.agentSubdir(agentDir, "commands");
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/** Get the prompts directory (~/.xcsh/agent/prompts). */
|
|
359
|
+
export function getPromptsDir(agentDir?: string): string {
|
|
360
|
+
return dirs.agentSubdir(agentDir, "prompts");
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/** Get the user-level Python modules directory (~/.xcsh/agent/modules). */
|
|
364
|
+
export function getAgentModulesDir(agentDir?: string): string {
|
|
365
|
+
return dirs.agentSubdir(agentDir, "modules");
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/** Get the memories directory (~/.xcsh/agent/memories). */
|
|
369
|
+
export function getMemoriesDir(agentDir?: string): string {
|
|
370
|
+
return dirs.agentSubdir(agentDir, "memories", "state");
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/** Get the terminal sessions directory (~/.xcsh/agent/terminal-sessions). */
|
|
374
|
+
export function getTerminalSessionsDir(agentDir?: string): string {
|
|
375
|
+
return dirs.agentSubdir(agentDir, "terminal-sessions", "state");
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/** Get the crash log path (~/.xcsh/agent/xcsh-crash.log). */
|
|
379
|
+
export function getCrashLogPath(agentDir?: string): string {
|
|
380
|
+
return dirs.agentSubdir(agentDir, "xcsh-crash.log", "state");
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/** Get the debug log path (~/.xcsh/agent/xcsh-debug.log). */
|
|
384
|
+
export function getDebugLogPath(agentDir?: string): string {
|
|
385
|
+
return dirs.agentSubdir(agentDir, `${APP_NAME}-debug.log`, "state");
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// =============================================================================
|
|
389
|
+
// Project subdirectories (.xcsh/*)
|
|
390
|
+
// =============================================================================
|
|
391
|
+
|
|
392
|
+
/** Get the project-level Python modules directory (.xcsh/modules). */
|
|
393
|
+
export function getProjectModulesDir(cwd: string = getProjectDir()): string {
|
|
394
|
+
return path.join(getProjectAgentDir(cwd), "modules");
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/** Get the project-level prompts directory (.xcsh/prompts). */
|
|
398
|
+
export function getProjectPromptsDir(cwd: string = getProjectDir()): string {
|
|
399
|
+
return path.join(getProjectAgentDir(cwd), "prompts");
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/** Get the project-level plugin overrides path (.xcsh/plugin-overrides.json). */
|
|
403
|
+
export function getProjectPluginOverridesPath(cwd: string = getProjectDir()): string {
|
|
404
|
+
return path.join(getProjectAgentDir(cwd), "plugin-overrides.json");
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// =============================================================================
|
|
408
|
+
// MCP config paths
|
|
409
|
+
// =============================================================================
|
|
410
|
+
|
|
411
|
+
/** Get the primary MCP config file path (first candidate). */
|
|
412
|
+
export function getMCPConfigPath(scope: "user" | "project", cwd: string = getProjectDir()): string {
|
|
413
|
+
if (scope === "user") {
|
|
414
|
+
return path.join(getAgentDir(), "mcp.json");
|
|
415
|
+
}
|
|
416
|
+
return path.join(getProjectAgentDir(cwd), "mcp.json");
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/** Get the SSH config file path. */
|
|
420
|
+
export function getSSHConfigPath(scope: "user" | "project", cwd: string = getProjectDir()): string {
|
|
421
|
+
if (scope === "user") {
|
|
422
|
+
return path.join(getAgentDir(), "ssh.json");
|
|
423
|
+
}
|
|
424
|
+
return path.join(getProjectAgentDir(cwd), "ssh.json");
|
|
425
|
+
}
|
package/src/env.ts
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as os from "node:os";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import { getAgentDir, getConfigRootDir } from "./dirs";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Parses a .env file synchronously and extracts key-value string pairs.
|
|
8
|
+
* Ignores lines that are empty or start with '#'. Trims whitespace.
|
|
9
|
+
* Allows values to be quoted with single or double quotes.
|
|
10
|
+
* Returns an object of key-value pairs.
|
|
11
|
+
*/
|
|
12
|
+
function parseEnvFile(filePath: string): Record<string, string> {
|
|
13
|
+
const result: Record<string, string> = {};
|
|
14
|
+
try {
|
|
15
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
16
|
+
for (const line of content.split("\n")) {
|
|
17
|
+
const trimmed = line.trim();
|
|
18
|
+
// Skip comments and blank lines
|
|
19
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
20
|
+
|
|
21
|
+
const eqIndex = trimmed.indexOf("=");
|
|
22
|
+
if (eqIndex === -1) continue;
|
|
23
|
+
|
|
24
|
+
const key = trimmed.slice(0, eqIndex).trim();
|
|
25
|
+
let value = trimmed.slice(eqIndex + 1).trim();
|
|
26
|
+
|
|
27
|
+
// Remove surrounding quotes (" or ')
|
|
28
|
+
if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
|
|
29
|
+
value = value.slice(1, -1);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
result[key] = value;
|
|
33
|
+
}
|
|
34
|
+
} catch {
|
|
35
|
+
// File doesn't exist or can't be read - return empty result
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// XCSH_ overrides PI_
|
|
39
|
+
for (const k in result) {
|
|
40
|
+
if (k.startsWith("XCSH_")) {
|
|
41
|
+
result[`PI_${k.slice(5)}`] = result[k];
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return result;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Eagerly parse the user's $HOME/.env and the current project's .env (from cwd)
|
|
49
|
+
const homeEnv = parseEnvFile(path.join(os.homedir(), ".env"));
|
|
50
|
+
const piEnv = parseEnvFile(path.join(getConfigRootDir(), ".env"));
|
|
51
|
+
const agentEnv = parseEnvFile(path.join(getAgentDir(), ".env"));
|
|
52
|
+
const projectEnv = parseEnvFile(path.join(process.cwd(), ".env"));
|
|
53
|
+
|
|
54
|
+
for (const file of [projectEnv, agentEnv, piEnv, homeEnv]) {
|
|
55
|
+
for (const [key, value] of Object.entries(file)) {
|
|
56
|
+
if (!Bun.env[key]) {
|
|
57
|
+
Bun.env[key] = value;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Intentional re-export of Bun.env.
|
|
64
|
+
*
|
|
65
|
+
* All users should import this env module (import { $env } from "@f5xc-salesdemos/pi-utils")
|
|
66
|
+
* before using environment variables. This ensures that .env files have been loaded and
|
|
67
|
+
* overrides (project, home) have been applied, so $env always reflects the correct values.
|
|
68
|
+
*/
|
|
69
|
+
export const $env: Record<string, string> = Bun.env as Record<string, string>;
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Resolve the first environment variable value from the given keys.
|
|
73
|
+
* @param keys - The keys to resolve.
|
|
74
|
+
* @returns The first environment variable value, or undefined if no value is found.
|
|
75
|
+
*/
|
|
76
|
+
export function $pickenv(...keys: string[]): string | undefined {
|
|
77
|
+
for (const key of keys) {
|
|
78
|
+
const value = Bun.env[key]?.trim();
|
|
79
|
+
if (value) {
|
|
80
|
+
return value;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return undefined;
|
|
84
|
+
}
|