@astrofoundry/pi-astro 0.12.2 → 0.13.1
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/README.md +1 -1
- package/extensions/astro-agents/index.test.ts +7 -1
- package/extensions/astro-agents/index.ts +41 -26
- package/extensions/astro-footer/README.md +49 -27
- package/extensions/astro-footer/format.test.ts +61 -1
- package/extensions/astro-footer/format.ts +48 -0
- package/extensions/astro-footer/git-status.test.ts +45 -0
- package/extensions/astro-footer/git-status.ts +82 -0
- package/extensions/astro-footer/icons.ts +24 -3
- package/extensions/astro-footer/index.test.ts +217 -45
- package/extensions/astro-footer/index.ts +181 -33
- package/extensions/astro-footer/segments.test.ts +116 -24
- package/extensions/astro-footer/segments.ts +76 -17
- package/extensions/astro-footer/settings.test.ts +85 -0
- package/extensions/astro-footer/settings.ts +43 -0
- package/package.json +1 -1
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { describe, expect, it } from "vitest";
|
|
2
2
|
import { iconsFor } from "./icons.ts";
|
|
3
|
+
import { ZERO_COUNTS } from "./git-status.ts";
|
|
3
4
|
import {
|
|
5
|
+
renderCache,
|
|
4
6
|
renderContext,
|
|
5
7
|
renderCost,
|
|
6
8
|
renderExtensionStatus,
|
|
@@ -8,6 +10,9 @@ import {
|
|
|
8
10
|
renderModel,
|
|
9
11
|
renderPath,
|
|
10
12
|
renderPi,
|
|
13
|
+
renderSessionName,
|
|
14
|
+
renderSessionTime,
|
|
15
|
+
renderSubagents,
|
|
11
16
|
renderThinking,
|
|
12
17
|
renderTokens,
|
|
13
18
|
type ThemeFn,
|
|
@@ -48,69 +53,156 @@ describe("segments", () => {
|
|
|
48
53
|
});
|
|
49
54
|
|
|
50
55
|
it("renderThinking falls back gracefully on unknown level", () => {
|
|
51
|
-
|
|
52
|
-
expect(out).toContain("think:weird");
|
|
56
|
+
expect(renderThinking(fakeTheme, ICONS, "weird")).toContain("think:weird");
|
|
53
57
|
});
|
|
54
58
|
|
|
55
|
-
it("renderPath shows the basename of cwd", () => {
|
|
56
|
-
const out = renderPath(fakeTheme, ICONS, "/Users/astro/proj");
|
|
59
|
+
it("renderPath shows the basename of cwd in basename mode", () => {
|
|
60
|
+
const out = renderPath(fakeTheme, ICONS, "/Users/astro/proj", "basename");
|
|
57
61
|
expect(out).toContain("proj");
|
|
58
62
|
});
|
|
59
63
|
|
|
60
64
|
it("renderGit returns null when no branch", () => {
|
|
61
|
-
expect(renderGit(fakeTheme, ICONS, null)).toBeNull();
|
|
65
|
+
expect(renderGit(fakeTheme, ICONS, null, ZERO_COUNTS)).toBeNull();
|
|
62
66
|
});
|
|
63
67
|
|
|
64
|
-
it("renderGit shows branch
|
|
65
|
-
const out = renderGit(fakeTheme, ICONS, "
|
|
66
|
-
expect(out).toContain("<success>
|
|
68
|
+
it("renderGit shows branch in success colour when clean", () => {
|
|
69
|
+
const out = renderGit(fakeTheme, ICONS, "main", ZERO_COUNTS);
|
|
70
|
+
expect(out).toContain("<success>main</success>");
|
|
71
|
+
expect(out).not.toContain("+");
|
|
72
|
+
expect(out).not.toContain("*");
|
|
67
73
|
});
|
|
68
74
|
|
|
69
|
-
it("
|
|
70
|
-
|
|
75
|
+
it("renderGit shows ± counts and warning colour when dirty", () => {
|
|
76
|
+
const out = renderGit(fakeTheme, ICONS, "feat", { staged: 2, unstaged: 3, untracked: 1 });
|
|
77
|
+
expect(out).toContain("<warning>feat</warning>");
|
|
78
|
+
expect(out).toContain("<success>+2</success>");
|
|
79
|
+
expect(out).toContain("<warning>*3</warning>");
|
|
80
|
+
expect(out).toContain("<muted>?1</muted>");
|
|
71
81
|
});
|
|
72
82
|
|
|
73
|
-
it("renderTokens
|
|
74
|
-
expect(renderTokens(fakeTheme, ICONS,
|
|
83
|
+
it("renderTokens hides when both totals are zero", () => {
|
|
84
|
+
expect(renderTokens(fakeTheme, ICONS, 0, 0)).toBeNull();
|
|
75
85
|
});
|
|
76
86
|
|
|
77
|
-
it("
|
|
78
|
-
|
|
87
|
+
it("renderTokens splits input/output with arrows", () => {
|
|
88
|
+
const out = renderTokens(fakeTheme, ICONS, 1234, 567);
|
|
89
|
+
expect(out).toContain("↑");
|
|
90
|
+
expect(out).toContain("1.2k");
|
|
91
|
+
expect(out).toContain("↓");
|
|
92
|
+
expect(out).toContain("567");
|
|
79
93
|
});
|
|
80
94
|
|
|
81
|
-
it("renderCost
|
|
82
|
-
|
|
95
|
+
it("renderCost shows (sub) when subscription-covered", () => {
|
|
96
|
+
const out = renderCost(fakeTheme, ICONS, { costUsd: 0, isSubscription: true });
|
|
97
|
+
expect(out).toContain("<success>(sub)</success>");
|
|
83
98
|
});
|
|
84
99
|
|
|
85
|
-
it("
|
|
100
|
+
it("renderCost hides when zero and not subscription", () => {
|
|
101
|
+
expect(renderCost(fakeTheme, ICONS, { costUsd: 0, isSubscription: false })).toBeNull();
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("renderCost formats sub-dollar costs when not subscription", () => {
|
|
105
|
+
const out = renderCost(fakeTheme, ICONS, { costUsd: 0.123, isSubscription: false });
|
|
106
|
+
expect(out).toContain("$0.123");
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("renderContext hides when input is null or percent null", () => {
|
|
86
110
|
expect(renderContext(fakeTheme, ICONS, null)).toBeNull();
|
|
87
|
-
expect(
|
|
111
|
+
expect(
|
|
112
|
+
renderContext(fakeTheme, ICONS, { percent: null, contextWindow: 256_000, autoCompact: true }),
|
|
113
|
+
).toBeNull();
|
|
88
114
|
});
|
|
89
115
|
|
|
90
116
|
it("renderContext uses dim colour below 70%", () => {
|
|
91
|
-
const out = renderContext(fakeTheme, ICONS, {
|
|
117
|
+
const out = renderContext(fakeTheme, ICONS, {
|
|
118
|
+
percent: 50,
|
|
119
|
+
contextWindow: 256_000,
|
|
120
|
+
autoCompact: false,
|
|
121
|
+
});
|
|
92
122
|
expect(out).toContain("<dim>");
|
|
93
123
|
expect(out).toContain("50.0%/256k");
|
|
94
124
|
});
|
|
95
125
|
|
|
96
|
-
it("renderContext uses warning
|
|
97
|
-
const out = renderContext(fakeTheme, ICONS, {
|
|
126
|
+
it("renderContext uses warning at 70%", () => {
|
|
127
|
+
const out = renderContext(fakeTheme, ICONS, {
|
|
128
|
+
percent: 70,
|
|
129
|
+
contextWindow: 200_000,
|
|
130
|
+
autoCompact: false,
|
|
131
|
+
});
|
|
98
132
|
expect(out).toContain("<warning>");
|
|
99
133
|
expect(out).toContain("70.0%/200k");
|
|
100
134
|
});
|
|
101
135
|
|
|
102
|
-
it("renderContext uses error
|
|
103
|
-
const out = renderContext(fakeTheme, ICONS, {
|
|
136
|
+
it("renderContext uses error at 90%+", () => {
|
|
137
|
+
const out = renderContext(fakeTheme, ICONS, {
|
|
138
|
+
percent: 95.5,
|
|
139
|
+
contextWindow: 1_000_000,
|
|
140
|
+
autoCompact: false,
|
|
141
|
+
});
|
|
104
142
|
expect(out).toContain("<error>");
|
|
105
143
|
expect(out).toContain("95.5%/1.0M");
|
|
106
144
|
});
|
|
107
145
|
|
|
146
|
+
it("renderContext appends AC indicator when auto-compact is on", () => {
|
|
147
|
+
const out = renderContext(fakeTheme, ICONS, {
|
|
148
|
+
percent: 25,
|
|
149
|
+
contextWindow: 256_000,
|
|
150
|
+
autoCompact: true,
|
|
151
|
+
});
|
|
152
|
+
expect(out).toContain("AC");
|
|
153
|
+
});
|
|
154
|
+
|
|
108
155
|
it("renderContext omits the /total suffix when contextWindow is missing", () => {
|
|
109
|
-
const out = renderContext(fakeTheme, ICONS, {
|
|
156
|
+
const out = renderContext(fakeTheme, ICONS, {
|
|
157
|
+
percent: 25,
|
|
158
|
+
contextWindow: 0,
|
|
159
|
+
autoCompact: false,
|
|
160
|
+
});
|
|
110
161
|
expect(out).toContain("25.0%");
|
|
111
162
|
expect(out).not.toContain("25.0%/");
|
|
112
163
|
});
|
|
113
164
|
|
|
165
|
+
it("renderSubagents hides when count is 0", () => {
|
|
166
|
+
expect(renderSubagents(fakeTheme, ICONS, 0)).toBeNull();
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it("renderSubagents shows the count in accent colour", () => {
|
|
170
|
+
const out = renderSubagents(fakeTheme, ICONS, 3);
|
|
171
|
+
expect(out).toContain("<accent>3</accent>");
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it("renderSessionTime hides under 60 seconds", () => {
|
|
175
|
+
expect(renderSessionTime(fakeTheme, ICONS, 0)).toBeNull();
|
|
176
|
+
expect(renderSessionTime(fakeTheme, ICONS, 30_000)).toBeNull();
|
|
177
|
+
expect(renderSessionTime(fakeTheme, ICONS, 59_999)).toBeNull();
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it("renderSessionTime formats minutes and seconds at and above 1 minute", () => {
|
|
181
|
+
const out = renderSessionTime(fakeTheme, ICONS, 12 * 60_000 + 34_000);
|
|
182
|
+
expect(out).toContain("12m34s");
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it("renderSessionName hides when undefined", () => {
|
|
186
|
+
expect(renderSessionName(fakeTheme, ICONS, undefined)).toBeNull();
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it("renderSessionName shows the name", () => {
|
|
190
|
+
const out = renderSessionName(fakeTheme, ICONS, "before-rewrite");
|
|
191
|
+
expect(out).toContain("before-rewrite");
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it("renderCache hides when both are 0", () => {
|
|
195
|
+
expect(renderCache(fakeTheme, ICONS, 0, 0)).toBeNull();
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it("renderCache shows both counts with cR/cW prefixes", () => {
|
|
199
|
+
const out = renderCache(fakeTheme, ICONS, 8500, 2100);
|
|
200
|
+
expect(out).toContain("cR");
|
|
201
|
+
expect(out).toContain("8.5k");
|
|
202
|
+
expect(out).toContain("cW");
|
|
203
|
+
expect(out).toContain("2.1k");
|
|
204
|
+
});
|
|
205
|
+
|
|
114
206
|
it("renderExtensionStatus uses muted colour", () => {
|
|
115
207
|
expect(renderExtensionStatus(fakeTheme, "🪨 caveman:full")).toBe("<muted>🪨 caveman:full</muted>");
|
|
116
208
|
});
|
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
import type { ThemeColor } from "@mariozechner/pi-coding-agent";
|
|
2
2
|
import type { IconSet } from "./icons.ts";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
formatCost,
|
|
5
|
+
formatDuration,
|
|
6
|
+
formatPath,
|
|
7
|
+
formatPercent,
|
|
8
|
+
formatTokens,
|
|
9
|
+
type PathMode,
|
|
10
|
+
} from "./format.ts";
|
|
11
|
+
import type { GitCounts } from "./git-status.ts";
|
|
4
12
|
import { formatModelDisplay } from "./model-name.ts";
|
|
5
13
|
|
|
6
14
|
export interface ThemeFn {
|
|
@@ -42,41 +50,92 @@ export function renderThinking(theme: ThemeFn, icons: IconSet, level: string | u
|
|
|
42
50
|
return `${theme.fg(color, icons.thinking)} ${theme.fg(color, `think:${label}`)}`.trim();
|
|
43
51
|
}
|
|
44
52
|
|
|
45
|
-
export function renderPath(theme: ThemeFn, icons: IconSet, cwd: string): string {
|
|
46
|
-
const name =
|
|
53
|
+
export function renderPath(theme: ThemeFn, icons: IconSet, cwd: string, mode: PathMode): string {
|
|
54
|
+
const name = formatPath(cwd, mode);
|
|
47
55
|
return `${theme.fg("dim", icons.path)} ${theme.fg("accent", name)}`.trim();
|
|
48
56
|
}
|
|
49
57
|
|
|
50
|
-
export function renderGit(
|
|
58
|
+
export function renderGit(
|
|
59
|
+
theme: ThemeFn,
|
|
60
|
+
icons: IconSet,
|
|
61
|
+
branch: string | null,
|
|
62
|
+
counts: GitCounts,
|
|
63
|
+
): string | null {
|
|
51
64
|
if (!branch) return null;
|
|
52
|
-
|
|
65
|
+
const dirty = counts.staged > 0 || counts.unstaged > 0 || counts.untracked > 0;
|
|
66
|
+
const role: ThemeColor = dirty ? "warning" : "success";
|
|
67
|
+
const parts: string[] = [];
|
|
68
|
+
parts.push(theme.fg(role, icons.git));
|
|
69
|
+
parts.push(theme.fg(role, branch));
|
|
70
|
+
if (counts.staged > 0) parts.push(theme.fg("success", `+${counts.staged}`));
|
|
71
|
+
if (counts.unstaged > 0) parts.push(theme.fg("warning", `*${counts.unstaged}`));
|
|
72
|
+
if (counts.untracked > 0) parts.push(theme.fg("muted", `?${counts.untracked}`));
|
|
73
|
+
return parts.join(" ");
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function renderTokens(
|
|
77
|
+
theme: ThemeFn,
|
|
78
|
+
icons: IconSet,
|
|
79
|
+
inputTokens: number,
|
|
80
|
+
outputTokens: number,
|
|
81
|
+
): string | null {
|
|
82
|
+
if (inputTokens === 0 && outputTokens === 0) return null;
|
|
83
|
+
return `${theme.fg("muted", icons.tokensIn)}${theme.fg("text", formatTokens(inputTokens))} ${theme.fg("muted", icons.tokensOut)}${theme.fg("text", formatTokens(outputTokens))}`;
|
|
53
84
|
}
|
|
54
85
|
|
|
55
|
-
export
|
|
56
|
-
|
|
57
|
-
|
|
86
|
+
export interface CostInput {
|
|
87
|
+
costUsd: number;
|
|
88
|
+
isSubscription: boolean;
|
|
58
89
|
}
|
|
59
90
|
|
|
60
|
-
export function renderCost(theme: ThemeFn, icons: IconSet,
|
|
61
|
-
if (
|
|
62
|
-
|
|
91
|
+
export function renderCost(theme: ThemeFn, icons: IconSet, input: CostInput): string | null {
|
|
92
|
+
if (input.isSubscription) {
|
|
93
|
+
return `${theme.fg("muted", icons.cost)} ${theme.fg("success", "(sub)")}`.trim();
|
|
94
|
+
}
|
|
95
|
+
if (input.costUsd === 0) return null;
|
|
96
|
+
return `${theme.fg("muted", icons.cost)} ${theme.fg("text", formatCost(input.costUsd))}`.trim();
|
|
63
97
|
}
|
|
64
98
|
|
|
65
99
|
export interface ContextSegmentInput {
|
|
66
100
|
percent: number | null;
|
|
67
101
|
contextWindow: number | null;
|
|
102
|
+
autoCompact: boolean;
|
|
68
103
|
}
|
|
69
104
|
|
|
70
|
-
export function renderContext(
|
|
71
|
-
theme: ThemeFn,
|
|
72
|
-
icons: IconSet,
|
|
73
|
-
input: ContextSegmentInput | null,
|
|
74
|
-
): string | null {
|
|
105
|
+
export function renderContext(theme: ThemeFn, icons: IconSet, input: ContextSegmentInput | null): string | null {
|
|
75
106
|
if (!input || input.percent === null) return null;
|
|
76
107
|
const role: ThemeColor = input.percent >= 90 ? "error" : input.percent >= 70 ? "warning" : "dim";
|
|
77
108
|
const total = input.contextWindow && input.contextWindow > 0 ? `/${formatTokens(input.contextWindow)}` : "";
|
|
78
109
|
const value = `${formatPercent(input.percent)}${total}`;
|
|
79
|
-
|
|
110
|
+
const ac = input.autoCompact ? ` ${theme.fg("muted", icons.autoCompact)}` : "";
|
|
111
|
+
return `${theme.fg(role, icons.context)} ${theme.fg(role, value)}${ac}`;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function renderSubagents(theme: ThemeFn, icons: IconSet, count: number): string | null {
|
|
115
|
+
if (count <= 0) return null;
|
|
116
|
+
return `${theme.fg("muted", icons.subagents)} ${theme.fg("accent", `${count}`)}`.trim();
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const SESSION_TIME_MIN_MS = 60_000;
|
|
120
|
+
|
|
121
|
+
export function renderSessionTime(theme: ThemeFn, icons: IconSet, ms: number): string | null {
|
|
122
|
+
if (ms < SESSION_TIME_MIN_MS) return null;
|
|
123
|
+
return `${theme.fg("muted", icons.sessionTime)} ${theme.fg("text", formatDuration(ms))}`.trim();
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function renderSessionName(theme: ThemeFn, icons: IconSet, name: string | undefined): string | null {
|
|
127
|
+
if (!name) return null;
|
|
128
|
+
return `${theme.fg("muted", icons.sessionName)} ${theme.fg("text", name)}`.trim();
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function renderCache(
|
|
132
|
+
theme: ThemeFn,
|
|
133
|
+
icons: IconSet,
|
|
134
|
+
cacheRead: number,
|
|
135
|
+
cacheWrite: number,
|
|
136
|
+
): string | null {
|
|
137
|
+
if (cacheRead === 0 && cacheWrite === 0) return null;
|
|
138
|
+
return `${theme.fg("muted", icons.cacheRead)}${theme.fg("text", formatTokens(cacheRead))} ${theme.fg("muted", icons.cacheWrite)}${theme.fg("text", formatTokens(cacheWrite))}`;
|
|
80
139
|
}
|
|
81
140
|
|
|
82
141
|
export function renderExtensionStatus(theme: ThemeFn, value: string): string {
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as os from "node:os";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
5
|
+
import {
|
|
6
|
+
DEFAULT_AUTO_COMPACT,
|
|
7
|
+
globalSettingsPath,
|
|
8
|
+
isAutoCompactEnabled,
|
|
9
|
+
projectSettingsPath,
|
|
10
|
+
} from "./settings.ts";
|
|
11
|
+
|
|
12
|
+
describe("settings paths", () => {
|
|
13
|
+
it("globalSettingsPath uses PI_CODING_AGENT_DIR when set", () => {
|
|
14
|
+
const out = globalSettingsPath({ PI_CODING_AGENT_DIR: "/custom/dir" });
|
|
15
|
+
expect(out).toBe(path.join("/custom/dir", "settings.json"));
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("globalSettingsPath defaults to ~/.pi/agent/settings.json", () => {
|
|
19
|
+
const out = globalSettingsPath({});
|
|
20
|
+
expect(out).toBe(path.join(os.homedir(), ".pi", "agent", "settings.json"));
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("projectSettingsPath joins .pi/settings.json under cwd", () => {
|
|
24
|
+
expect(projectSettingsPath("/tmp/proj")).toBe(path.join("/tmp/proj", ".pi", "settings.json"));
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
describe("isAutoCompactEnabled", () => {
|
|
29
|
+
let tmpDir: string;
|
|
30
|
+
let projectDir: string;
|
|
31
|
+
let globalDir: string;
|
|
32
|
+
|
|
33
|
+
beforeEach(() => {
|
|
34
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "astro-footer-settings-"));
|
|
35
|
+
projectDir = path.join(tmpDir, "proj");
|
|
36
|
+
globalDir = path.join(tmpDir, "global");
|
|
37
|
+
fs.mkdirSync(path.join(projectDir, ".pi"), { recursive: true });
|
|
38
|
+
fs.mkdirSync(globalDir, { recursive: true });
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
afterEach(() => {
|
|
42
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
function env(): NodeJS.ProcessEnv {
|
|
46
|
+
return { PI_CODING_AGENT_DIR: globalDir };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
it("returns the documented default when no settings exist", () => {
|
|
50
|
+
expect(isAutoCompactEnabled(projectDir, env())).toBe(DEFAULT_AUTO_COMPACT);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("project setting wins over global", () => {
|
|
54
|
+
fs.writeFileSync(
|
|
55
|
+
path.join(projectDir, ".pi", "settings.json"),
|
|
56
|
+
JSON.stringify({ compaction: { enabled: false } }),
|
|
57
|
+
);
|
|
58
|
+
fs.writeFileSync(
|
|
59
|
+
path.join(globalDir, "settings.json"),
|
|
60
|
+
JSON.stringify({ compaction: { enabled: true } }),
|
|
61
|
+
);
|
|
62
|
+
expect(isAutoCompactEnabled(projectDir, env())).toBe(false);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("falls back to global when project absent", () => {
|
|
66
|
+
fs.writeFileSync(
|
|
67
|
+
path.join(globalDir, "settings.json"),
|
|
68
|
+
JSON.stringify({ compaction: { enabled: false } }),
|
|
69
|
+
);
|
|
70
|
+
expect(isAutoCompactEnabled(projectDir, env())).toBe(false);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("ignores non-boolean values and uses default", () => {
|
|
74
|
+
fs.writeFileSync(
|
|
75
|
+
path.join(globalDir, "settings.json"),
|
|
76
|
+
JSON.stringify({ compaction: { enabled: "yes" } }),
|
|
77
|
+
);
|
|
78
|
+
expect(isAutoCompactEnabled(projectDir, env())).toBe(DEFAULT_AUTO_COMPACT);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("returns the default when settings.json is malformed", () => {
|
|
82
|
+
fs.writeFileSync(path.join(globalDir, "settings.json"), "{not-json");
|
|
83
|
+
expect(isAutoCompactEnabled(projectDir, env())).toBe(DEFAULT_AUTO_COMPACT);
|
|
84
|
+
});
|
|
85
|
+
});
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as os from "node:os";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
|
|
5
|
+
export const DEFAULT_AUTO_COMPACT = true;
|
|
6
|
+
|
|
7
|
+
interface CompactionShape {
|
|
8
|
+
compaction?: { enabled?: unknown };
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function readJsonOrNull(filePath: string): CompactionShape | null {
|
|
12
|
+
if (!fs.existsSync(filePath)) return null;
|
|
13
|
+
const raw = fs.readFileSync(filePath, "utf8");
|
|
14
|
+
return JSON.parse(raw) as CompactionShape;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function globalSettingsPath(env: NodeJS.ProcessEnv = process.env): string {
|
|
18
|
+
const override = env.PI_CODING_AGENT_DIR;
|
|
19
|
+
const dir = override && override.length > 0 ? override : path.join(os.homedir(), ".pi", "agent");
|
|
20
|
+
return path.join(dir, "settings.json");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function projectSettingsPath(cwd: string): string {
|
|
24
|
+
return path.join(cwd, ".pi", "settings.json");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function isAutoCompactEnabled(cwd: string, env: NodeJS.ProcessEnv = process.env): boolean {
|
|
28
|
+
try {
|
|
29
|
+
const project = readJsonOrNull(projectSettingsPath(cwd));
|
|
30
|
+
const projectValue = project?.compaction?.enabled;
|
|
31
|
+
if (typeof projectValue === "boolean") return projectValue;
|
|
32
|
+
} catch {
|
|
33
|
+
/* fall through */
|
|
34
|
+
}
|
|
35
|
+
try {
|
|
36
|
+
const global = readJsonOrNull(globalSettingsPath(env));
|
|
37
|
+
const globalValue = global?.compaction?.enabled;
|
|
38
|
+
if (typeof globalValue === "boolean") return globalValue;
|
|
39
|
+
} catch {
|
|
40
|
+
/* fall through */
|
|
41
|
+
}
|
|
42
|
+
return DEFAULT_AUTO_COMPACT;
|
|
43
|
+
}
|