@narumitw/pi-statusline 0.49.0 → 0.49.3
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 +10 -5
- package/dist/chunks/chunk-6BDIM4OT.ts +776 -0
- package/dist/chunks/chunk-6BDIM4OT.ts.map +7 -0
- package/dist/chunks/commands-FUT5RTFK.ts +774 -0
- package/dist/chunks/commands-FUT5RTFK.ts.map +7 -0
- package/dist/statusline.ts +1144 -0
- package/dist/statusline.ts.map +7 -0
- package/package.json +10 -6
- package/src/index.ts +1 -1
- package/src/ansi.ts +0 -26
- package/src/commands.ts +0 -929
- package/src/directory.ts +0 -54
- package/src/extension-status.ts +0 -323
- package/src/git-status.ts +0 -148
- package/src/information-profiles.ts +0 -40
- package/src/powerline.ts +0 -170
- package/src/presets/candy.ts +0 -9
- package/src/presets/create-ramp.ts +0 -41
- package/src/presets/custom.ts +0 -11
- package/src/presets/forest.ts +0 -9
- package/src/presets/index.ts +0 -55
- package/src/presets/mono.ts +0 -9
- package/src/presets/neon.ts +0 -9
- package/src/presets/ocean.ts +0 -9
- package/src/presets/sunset.ts +0 -9
- package/src/presets/tokyo-night.ts +0 -13
- package/src/presets/types.ts +0 -12
- package/src/render.ts +0 -357
- package/src/settings.ts +0 -702
- package/src/statusline.ts +0 -353
- package/src/terminal.ts +0 -60
- package/src/types.ts +0 -88
- package/src/usage.ts +0 -53
package/src/directory.ts
DELETED
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
import { basename, isAbsolute, relative, sep } from "node:path";
|
|
2
|
-
import { sanitizeTerminalText } from "./terminal.js";
|
|
3
|
-
|
|
4
|
-
const DEFAULT_TRUNCATION_LENGTH = 3;
|
|
5
|
-
const DEFAULT_HOME_SYMBOL = "~";
|
|
6
|
-
|
|
7
|
-
/** Apply Starship's default directory presentation without exposing a format language. */
|
|
8
|
-
export function formatDirectoryPath(
|
|
9
|
-
cwd: string,
|
|
10
|
-
homeDir: string | undefined,
|
|
11
|
-
gitRoot: string | undefined,
|
|
12
|
-
): string {
|
|
13
|
-
const contracted =
|
|
14
|
-
gitRoot && (!homeDir || !samePath(gitRoot, homeDir))
|
|
15
|
-
? (contractRepositoryPath(cwd, gitRoot) ?? contractPath(cwd, homeDir, DEFAULT_HOME_SYMBOL))
|
|
16
|
-
: contractPath(cwd, homeDir, DEFAULT_HOME_SYMBOL);
|
|
17
|
-
const safeContracted = sanitizeTerminalText(contracted);
|
|
18
|
-
const components = safeContracted.split("/").filter((component) => component.length > 0);
|
|
19
|
-
const displayed =
|
|
20
|
-
components.length > DEFAULT_TRUNCATION_LENGTH
|
|
21
|
-
? components.slice(-DEFAULT_TRUNCATION_LENGTH).join("/")
|
|
22
|
-
: safeContracted;
|
|
23
|
-
const native = sep === "/" ? displayed : displayed.replaceAll("/", sep);
|
|
24
|
-
return sanitizeTerminalText(native || basename(cwd) || cwd);
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function toSlashPath(value: string): string {
|
|
28
|
-
return sep === "\\" ? value.replaceAll("\\", "/") : value;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
function contractPath(path: string, root: string | undefined, replacement: string): string {
|
|
32
|
-
if (!root) return toSlashPath(path);
|
|
33
|
-
const child = relativeWithin(path, root);
|
|
34
|
-
if (child === undefined) return toSlashPath(path);
|
|
35
|
-
return child ? `${replacement}/${child}` : replacement;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
function contractRepositoryPath(path: string, root: string): string | undefined {
|
|
39
|
-
const child = relativeWithin(path, root);
|
|
40
|
-
if (child === undefined) return undefined;
|
|
41
|
-
const name = basename(root) || toSlashPath(root);
|
|
42
|
-
return child ? `${name}/${child}` : name;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function relativeWithin(path: string, root: string): string | undefined {
|
|
46
|
-
const child = relative(root, path);
|
|
47
|
-
if (child === "") return "";
|
|
48
|
-
if (child === ".." || child.startsWith(`..${sep}`) || isAbsolute(child)) return undefined;
|
|
49
|
-
return toSlashPath(child);
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
function samePath(left: string, right: string): boolean {
|
|
53
|
-
return relative(left, right) === "";
|
|
54
|
-
}
|
package/src/extension-status.ts
DELETED
|
@@ -1,323 +0,0 @@
|
|
|
1
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
-
import { dirname, isAbsolute, join, resolve } from "node:path";
|
|
3
|
-
import process from "node:process";
|
|
4
|
-
import type { Theme, ThemeColor } from "@earendil-works/pi-coding-agent";
|
|
5
|
-
import { wrapTextWithAnsi } from "@earendil-works/pi-tui";
|
|
6
|
-
import { powerlineExtensionSeparator } from "./powerline.js";
|
|
7
|
-
import { DEFAULT_EXTENSION_STATUS_ICONS } from "./settings.js";
|
|
8
|
-
import type { StatuslineConfig } from "./types.js";
|
|
9
|
-
|
|
10
|
-
export type ExtensionStatusIconAliasMap = ReadonlyMap<string, readonly string[]>;
|
|
11
|
-
export interface ExtensionStatusRuntime {
|
|
12
|
-
duplicateExtensions: string[];
|
|
13
|
-
extensionStatusIconAliases: ExtensionStatusIconAliasMap;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
const STATUSLINE_KEY = "statusline";
|
|
17
|
-
const COMPATIBLE_STATUS_ICON_KEYS: Readonly<Record<string, string>> = {
|
|
18
|
-
retry: "unknown-error-retry",
|
|
19
|
-
sync: "pisync",
|
|
20
|
-
"unknown-error-retry": "retry",
|
|
21
|
-
pisync: "sync",
|
|
22
|
-
};
|
|
23
|
-
const EMPTY_EXTENSION_STATUS_ICON_ALIASES: ExtensionStatusIconAliasMap = new Map();
|
|
24
|
-
function extensionStatusSeparator(config: StatuslineConfig, theme: Theme): string {
|
|
25
|
-
return powerlineExtensionSeparator(theme, config.palettePreset);
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export function formatExtensionStatuses(
|
|
29
|
-
statuses: ReadonlyMap<string, string>,
|
|
30
|
-
theme: Theme,
|
|
31
|
-
config: StatuslineConfig,
|
|
32
|
-
runtime: ExtensionStatusRuntime,
|
|
33
|
-
hiddenKeys: ReadonlySet<string> = new Set(),
|
|
34
|
-
): string {
|
|
35
|
-
const separator = extensionStatusSeparator(config, theme);
|
|
36
|
-
const visibleStatuses = [
|
|
37
|
-
...formatDuplicateExtensionStatus(runtime, theme),
|
|
38
|
-
...[...statuses.entries()]
|
|
39
|
-
.filter(
|
|
40
|
-
([key, value]) => key !== STATUSLINE_KEY && !hiddenKeys.has(key) && value.trim().length > 0,
|
|
41
|
-
)
|
|
42
|
-
.map(([key, value]) =>
|
|
43
|
-
formatExtensionStatus(key, value, theme, config, runtime.extensionStatusIconAliases),
|
|
44
|
-
),
|
|
45
|
-
].slice(0, 5);
|
|
46
|
-
|
|
47
|
-
return visibleStatuses.join(separator);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
export function formatExtensionStatus(
|
|
51
|
-
key: string,
|
|
52
|
-
value: string,
|
|
53
|
-
theme: Theme,
|
|
54
|
-
config: Pick<StatuslineConfig, "extensionStatusIcons">,
|
|
55
|
-
extensionStatusIconAliases: ExtensionStatusIconAliasMap = EMPTY_EXTENSION_STATUS_ICON_ALIASES,
|
|
56
|
-
): string {
|
|
57
|
-
const status = splitExtensionStatusIcon(stripExtensionStatusPrefix(key, value));
|
|
58
|
-
const text = simplifyExtensionStatusText(status.text);
|
|
59
|
-
const color = extensionColor(key, value);
|
|
60
|
-
const textColor = color === "warning" ? "warning" : "muted";
|
|
61
|
-
const icon = extensionStatusIcon(
|
|
62
|
-
key,
|
|
63
|
-
status.icon,
|
|
64
|
-
config.extensionStatusIcons,
|
|
65
|
-
extensionStatusIconAliases,
|
|
66
|
-
);
|
|
67
|
-
const renderedText = theme.fg(textColor, text);
|
|
68
|
-
return icon ? `${theme.fg(color, icon)} ${renderedText}` : renderedText;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
function extensionStatusIcon(
|
|
72
|
-
key: string,
|
|
73
|
-
leadingIcon: string | undefined,
|
|
74
|
-
configuredIcons: Record<string, string>,
|
|
75
|
-
extensionStatusIconAliases: ExtensionStatusIconAliasMap,
|
|
76
|
-
) {
|
|
77
|
-
if (Object.hasOwn(configuredIcons, key)) return configuredIcons[key];
|
|
78
|
-
const namespaceIcon = configuredNamespaceIcon(key, configuredIcons);
|
|
79
|
-
if (namespaceIcon !== undefined) return namespaceIcon;
|
|
80
|
-
const compatibleKey = COMPATIBLE_STATUS_ICON_KEYS[key];
|
|
81
|
-
if (compatibleKey && Object.hasOwn(configuredIcons, compatibleKey)) {
|
|
82
|
-
return configuredIcons[compatibleKey];
|
|
83
|
-
}
|
|
84
|
-
for (const alias of extensionStatusAliasesForKey(key, extensionStatusIconAliases)) {
|
|
85
|
-
if (Object.hasOwn(configuredIcons, alias)) return configuredIcons[alias];
|
|
86
|
-
}
|
|
87
|
-
return leadingIcon ?? DEFAULT_EXTENSION_STATUS_ICONS[key] ?? "🔌";
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
function configuredNamespaceIcon(
|
|
91
|
-
key: string,
|
|
92
|
-
configuredIcons: Readonly<Record<string, string>>,
|
|
93
|
-
): string | undefined {
|
|
94
|
-
let match: { baseLength: number; icon: string } | undefined;
|
|
95
|
-
for (const [selector, icon] of Object.entries(configuredIcons)) {
|
|
96
|
-
if (!selector.endsWith(":*")) continue;
|
|
97
|
-
const base = selector.slice(0, -2);
|
|
98
|
-
if (!base || !key.startsWith(`${base}:`)) continue;
|
|
99
|
-
if (!match || base.length > match.baseLength) match = { baseLength: base.length, icon };
|
|
100
|
-
}
|
|
101
|
-
return match?.icon;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
function extensionStatusAliasesForKey(
|
|
105
|
-
key: string,
|
|
106
|
-
extensionStatusIconAliases: ExtensionStatusIconAliasMap,
|
|
107
|
-
): readonly string[] {
|
|
108
|
-
for (const [statusBase, aliases] of extensionStatusIconAliases) {
|
|
109
|
-
if (statusKeyMatchesStatusBase(key, statusBase)) return aliases;
|
|
110
|
-
}
|
|
111
|
-
return [];
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
function statusKeyMatchesStatusBase(key: string, statusBase: string): boolean {
|
|
115
|
-
return key === statusBase || key.startsWith(`${statusBase}:`) || key.startsWith(`${statusBase}/`);
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
export function wrapExtensionStatusline(status: string, width: number): string[] {
|
|
119
|
-
if (!status || width <= 0) return [];
|
|
120
|
-
return wrapTextWithAnsi(status, width);
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
function formatDuplicateExtensionStatus(runtime: ExtensionStatusRuntime, theme: Theme): string[] {
|
|
124
|
-
if (runtime.duplicateExtensions.length === 0) return [];
|
|
125
|
-
const names = runtime.duplicateExtensions.slice(0, 2).join(", ");
|
|
126
|
-
const suffix =
|
|
127
|
-
runtime.duplicateExtensions.length > 2 ? ` +${runtime.duplicateExtensions.length - 2}` : "";
|
|
128
|
-
return [`${theme.fg("warning", "⚠️")} ${theme.fg("warning", `dup ${names}${suffix}`)}`];
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
export function splitExtensionStatusIcon(value: string): { icon?: string; text: string } {
|
|
132
|
-
const trimmed = value.trim();
|
|
133
|
-
const [firstToken, ...restTokens] = trimmed.split(/\s+/);
|
|
134
|
-
if (firstToken && isEmojiOnlyToken(firstToken)) {
|
|
135
|
-
return { icon: firstToken, text: restTokens.join(" ") };
|
|
136
|
-
}
|
|
137
|
-
return { text: trimmed };
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
function isEmojiOnlyToken(value: string): boolean {
|
|
141
|
-
return /^(?=.*(?:\p{Extended_Pictographic}|\p{Regional_Indicator}|[0-9#*]\ufe0f?\u20e3))(?:\p{Extended_Pictographic}|\p{Emoji_Modifier}|\p{Regional_Indicator}|\u200d|\ufe0f|[0-9#*]\ufe0f?\u20e3)+$/u.test(
|
|
142
|
-
value,
|
|
143
|
-
);
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
export function extensionColor(key: string, value: string): ThemeColor {
|
|
147
|
-
const normalized = `${key} ${value}`.toLowerCase();
|
|
148
|
-
if (/missing|error|fail|conflict|duplicate|unavailable/.test(normalized)) return "warning";
|
|
149
|
-
if (normalized.includes("codex")) return "accent";
|
|
150
|
-
if (/ready|active|running|enabled|awake|ok/.test(normalized)) return "success";
|
|
151
|
-
return "muted";
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
export function stripExtensionStatusPrefix(key: string, value: string): string {
|
|
155
|
-
return value.trim().replace(new RegExp(`^${escapeRegExp(key)}\\s*:\\s*`, "iu"), "");
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
export function simplifyExtensionStatusText(value: string): string {
|
|
159
|
-
return value
|
|
160
|
-
.trim()
|
|
161
|
-
.replace(/\bready\b/giu, "✓")
|
|
162
|
-
.replace(/\bmissing\b/giu, "✗")
|
|
163
|
-
.replace(/,\s*/g, " ")
|
|
164
|
-
.replace(/\s+\([^)]*\)\s*$/, "")
|
|
165
|
-
.replace(/\s+/g, " ");
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
function escapeRegExp(value: string): string {
|
|
169
|
-
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
interface InstalledExtensionPackage {
|
|
173
|
-
packageName: string;
|
|
174
|
-
source: string;
|
|
175
|
-
identity: string;
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
export function readInstalledExtensionPackages(cwd: string): InstalledExtensionPackage[] {
|
|
179
|
-
const packages: InstalledExtensionPackage[] = [];
|
|
180
|
-
const settingsFiles = extensionSettingsFiles(cwd);
|
|
181
|
-
|
|
182
|
-
for (const settingsFile of settingsFiles) {
|
|
183
|
-
const baseDirectory = dirname(settingsFile);
|
|
184
|
-
for (const rawSource of readPackageSources(settingsFile)) {
|
|
185
|
-
const source = rawSource.trim();
|
|
186
|
-
if (!source) continue;
|
|
187
|
-
const packageName = packageNameForSource(source, baseDirectory);
|
|
188
|
-
if (!packageName) continue;
|
|
189
|
-
packages.push({ packageName, source, identity: sourceIdentity(source, baseDirectory) });
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
return packages;
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
function extensionSettingsFiles(cwd: string): string[] {
|
|
197
|
-
return [
|
|
198
|
-
join(process.env.HOME ?? "", ".pi", "agent", "settings.json"),
|
|
199
|
-
join(cwd, ".pi", "settings.json"),
|
|
200
|
-
].filter((file) => existsSync(file));
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
export function findDuplicateExtensions(
|
|
204
|
-
installedPackages: readonly InstalledExtensionPackage[],
|
|
205
|
-
): string[] {
|
|
206
|
-
const sourcesByPackage = new Map<string, Set<string>>();
|
|
207
|
-
|
|
208
|
-
for (const extensionPackage of installedPackages) {
|
|
209
|
-
const sources = sourcesByPackage.get(extensionPackage.packageName) ?? new Set<string>();
|
|
210
|
-
sources.add(extensionPackage.identity);
|
|
211
|
-
sourcesByPackage.set(extensionPackage.packageName, sources);
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
return [...sourcesByPackage.entries()]
|
|
215
|
-
.filter(([, sources]) => sources.size > 1)
|
|
216
|
-
.map(([packageName]) => packageName.replace(/^@[^/]+\//, "").replace(/^pi-/, ""));
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
export function buildExtensionStatusIconAliases(
|
|
220
|
-
installedPackages: readonly { packageName: string; source?: string }[],
|
|
221
|
-
): Map<string, string[]> {
|
|
222
|
-
const packageAliasesByStatusBase = new Map<string, Map<string, string[]>>();
|
|
223
|
-
|
|
224
|
-
for (const extensionPackage of installedPackages) {
|
|
225
|
-
const candidate = extensionStatusIconAliasCandidate(
|
|
226
|
-
extensionPackage.packageName,
|
|
227
|
-
extensionPackage.source,
|
|
228
|
-
);
|
|
229
|
-
if (!candidate) continue;
|
|
230
|
-
const aliasesByPackage =
|
|
231
|
-
packageAliasesByStatusBase.get(candidate.statusBase) ?? new Map<string, string[]>();
|
|
232
|
-
const existingAliases = aliasesByPackage.get(extensionPackage.packageName) ?? [];
|
|
233
|
-
aliasesByPackage.set(
|
|
234
|
-
extensionPackage.packageName,
|
|
235
|
-
uniqueStrings([...existingAliases, ...candidate.aliases]),
|
|
236
|
-
);
|
|
237
|
-
packageAliasesByStatusBase.set(candidate.statusBase, aliasesByPackage);
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
const aliases = new Map<string, string[]>();
|
|
241
|
-
for (const [statusBase, aliasesByPackage] of packageAliasesByStatusBase) {
|
|
242
|
-
if (aliasesByPackage.size === 1)
|
|
243
|
-
aliases.set(statusBase, [...aliasesByPackage.values()][0] ?? []);
|
|
244
|
-
}
|
|
245
|
-
return aliases;
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
function extensionStatusIconAliasCandidate(
|
|
249
|
-
packageName: string,
|
|
250
|
-
source?: string,
|
|
251
|
-
): { statusBase: string; aliases: string[] } | undefined {
|
|
252
|
-
const packageBase = packageBaseName(packageName);
|
|
253
|
-
const statusBase = statusBaseFromPackageBase(packageBase);
|
|
254
|
-
if (!statusBase) return undefined;
|
|
255
|
-
|
|
256
|
-
const sourceAliases = source?.startsWith("npm:") ? [source, `npm:${npmPackageName(source)}`] : [];
|
|
257
|
-
return {
|
|
258
|
-
statusBase,
|
|
259
|
-
aliases: uniqueStrings([...sourceAliases, packageName, packageBase, statusBase]),
|
|
260
|
-
};
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
function packageBaseName(packageName: string): string {
|
|
264
|
-
const slashIndex = packageName.lastIndexOf("/");
|
|
265
|
-
return slashIndex === -1 ? packageName : packageName.slice(slashIndex + 1);
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
function statusBaseFromPackageBase(packageBase: string): string {
|
|
269
|
-
return packageBase.startsWith("pi-") && packageBase.length > "pi-".length
|
|
270
|
-
? packageBase.slice("pi-".length)
|
|
271
|
-
: packageBase;
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
function uniqueStrings(values: readonly string[]): string[] {
|
|
275
|
-
return [...new Set(values.filter((value) => value.length > 0))];
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
function readPackageSources(settingsFile: string): string[] {
|
|
279
|
-
try {
|
|
280
|
-
const settings = JSON.parse(readFileSync(settingsFile, "utf8")) as { packages?: unknown[] };
|
|
281
|
-
return (settings.packages ?? [])
|
|
282
|
-
.map((entry) => {
|
|
283
|
-
if (typeof entry === "string") return entry;
|
|
284
|
-
if (
|
|
285
|
-
entry &&
|
|
286
|
-
typeof entry === "object" &&
|
|
287
|
-
typeof (entry as { source?: unknown }).source === "string"
|
|
288
|
-
) {
|
|
289
|
-
return (entry as { source: string }).source;
|
|
290
|
-
}
|
|
291
|
-
return undefined;
|
|
292
|
-
})
|
|
293
|
-
.filter((source): source is string => source !== undefined);
|
|
294
|
-
} catch {
|
|
295
|
-
return [];
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
function packageNameForSource(source: string, baseDirectory: string): string | undefined {
|
|
300
|
-
if (source.startsWith("npm:")) return npmPackageName(source);
|
|
301
|
-
const packageJson = join(resolveSourcePath(source, baseDirectory), "package.json");
|
|
302
|
-
try {
|
|
303
|
-
const packageData = JSON.parse(readFileSync(packageJson, "utf8")) as { name?: unknown };
|
|
304
|
-
return typeof packageData.name === "string" ? packageData.name : undefined;
|
|
305
|
-
} catch {
|
|
306
|
-
return undefined;
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
export function npmPackageName(source: string): string {
|
|
311
|
-
const spec = source.slice("npm:".length);
|
|
312
|
-
if (spec.startsWith("@")) return spec.split("@").slice(0, 2).join("@");
|
|
313
|
-
return spec.split("@")[0] ?? spec;
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
function sourceIdentity(source: string, baseDirectory: string): string {
|
|
317
|
-
if (source.startsWith("npm:")) return `npm:${npmPackageName(source)}`;
|
|
318
|
-
return resolveSourcePath(source, baseDirectory);
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
function resolveSourcePath(source: string, baseDirectory: string): string {
|
|
322
|
-
return isAbsolute(source) ? source : resolve(baseDirectory, source);
|
|
323
|
-
}
|
package/src/git-status.ts
DELETED
|
@@ -1,148 +0,0 @@
|
|
|
1
|
-
import { isAbsolute } from "node:path";
|
|
2
|
-
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
3
|
-
|
|
4
|
-
const GIT_STATUS_TIMEOUT_MS = 3_000;
|
|
5
|
-
|
|
6
|
-
export interface GitStatusSummary {
|
|
7
|
-
root?: string;
|
|
8
|
-
ahead: number;
|
|
9
|
-
behind: number;
|
|
10
|
-
staged: number;
|
|
11
|
-
modified: number;
|
|
12
|
-
untracked: number;
|
|
13
|
-
conflicts: number;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export async function readGitStatus(
|
|
17
|
-
pi: ExtensionAPI,
|
|
18
|
-
cwd: string,
|
|
19
|
-
signal?: AbortSignal,
|
|
20
|
-
): Promise<GitStatusSummary | undefined> {
|
|
21
|
-
const [statusResult, rootResult] = await Promise.all([
|
|
22
|
-
pi.exec(
|
|
23
|
-
"git",
|
|
24
|
-
["--no-optional-locks", "status", "--porcelain=v1", "--branch", "--untracked-files=normal"],
|
|
25
|
-
{ cwd, timeout: GIT_STATUS_TIMEOUT_MS, signal },
|
|
26
|
-
),
|
|
27
|
-
pi
|
|
28
|
-
.exec("git", ["rev-parse", "--path-format=absolute", "--show-toplevel"], {
|
|
29
|
-
cwd,
|
|
30
|
-
timeout: GIT_STATUS_TIMEOUT_MS,
|
|
31
|
-
signal,
|
|
32
|
-
})
|
|
33
|
-
.catch(() => undefined),
|
|
34
|
-
]);
|
|
35
|
-
if (statusResult.code !== 0 || statusResult.killed) return undefined;
|
|
36
|
-
const summary = parseGitStatusPorcelain(statusResult.stdout);
|
|
37
|
-
if (rootResult && rootResult.code === 0 && !rootResult.killed) {
|
|
38
|
-
const root = parseGitRoot(rootResult.stdout);
|
|
39
|
-
if (root) summary.root = root;
|
|
40
|
-
}
|
|
41
|
-
return summary;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export function parseGitStatusPorcelain(output: string): GitStatusSummary {
|
|
45
|
-
const summary: GitStatusSummary = {
|
|
46
|
-
ahead: 0,
|
|
47
|
-
behind: 0,
|
|
48
|
-
staged: 0,
|
|
49
|
-
modified: 0,
|
|
50
|
-
untracked: 0,
|
|
51
|
-
conflicts: 0,
|
|
52
|
-
};
|
|
53
|
-
for (const line of output.split(/\r?\n/)) {
|
|
54
|
-
if (!line) continue;
|
|
55
|
-
if (line.startsWith("## ")) {
|
|
56
|
-
const ahead = line.match(/\bahead (\d+)/u);
|
|
57
|
-
const behind = line.match(/\bbehind (\d+)/u);
|
|
58
|
-
summary.ahead = ahead ? Number(ahead[1]) : 0;
|
|
59
|
-
summary.behind = behind ? Number(behind[1]) : 0;
|
|
60
|
-
continue;
|
|
61
|
-
}
|
|
62
|
-
const indexStatus = line[0] ?? " ";
|
|
63
|
-
const worktreeStatus = line[1] ?? " ";
|
|
64
|
-
if (indexStatus === "?" && worktreeStatus === "?") {
|
|
65
|
-
summary.untracked += 1;
|
|
66
|
-
continue;
|
|
67
|
-
}
|
|
68
|
-
if (isConflictStatus(indexStatus, worktreeStatus)) {
|
|
69
|
-
summary.conflicts += 1;
|
|
70
|
-
continue;
|
|
71
|
-
}
|
|
72
|
-
if (isChangedStatus(indexStatus)) summary.staged += 1;
|
|
73
|
-
if (isChangedStatus(worktreeStatus)) summary.modified += 1;
|
|
74
|
-
}
|
|
75
|
-
return summary;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
function isConflictStatus(indexStatus: string, worktreeStatus: string): boolean {
|
|
79
|
-
return (
|
|
80
|
-
(indexStatus === "D" && worktreeStatus === "D") ||
|
|
81
|
-
(indexStatus === "A" && worktreeStatus === "A") ||
|
|
82
|
-
indexStatus === "U" ||
|
|
83
|
-
worktreeStatus === "U"
|
|
84
|
-
);
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
function isChangedStatus(status: string): boolean {
|
|
88
|
-
return status !== " " && status !== "?" && status !== "!";
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
export function parseGitRoot(output: string): string | undefined {
|
|
92
|
-
const root = output.split(/\r?\n/u)[0]?.trim();
|
|
93
|
-
return root && isAbsolute(root) ? root : undefined;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
export function formatGitStatusSummary(summary: GitStatusSummary | undefined): string {
|
|
97
|
-
if (!summary) return "";
|
|
98
|
-
const tokens = [
|
|
99
|
-
["⇡", summary.ahead],
|
|
100
|
-
["⇣", summary.behind],
|
|
101
|
-
["+", summary.staged],
|
|
102
|
-
["~", summary.modified],
|
|
103
|
-
["?", summary.untracked],
|
|
104
|
-
["!", summary.conflicts],
|
|
105
|
-
] as const;
|
|
106
|
-
return tokens
|
|
107
|
-
.filter(([, count]) => count > 0)
|
|
108
|
-
.map(([prefix, count]) => `${prefix}${formatCount(count)}`)
|
|
109
|
-
.join(" ");
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
export function formatGitBranchValue(
|
|
113
|
-
branch: string | null,
|
|
114
|
-
status: GitStatusSummary | undefined,
|
|
115
|
-
pr?: string,
|
|
116
|
-
): string {
|
|
117
|
-
if (!branch) return "no-git";
|
|
118
|
-
const suffixes = [formatGitStatusSummary(status), pr ? `(${pr})` : ""].filter(Boolean);
|
|
119
|
-
return suffixes.length > 0 ? `${branch} ${suffixes.join(" ")}` : branch;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
export function formatGitBranchText(
|
|
123
|
-
branch: string | null,
|
|
124
|
-
status: GitStatusSummary | undefined,
|
|
125
|
-
pr?: string,
|
|
126
|
-
): string {
|
|
127
|
-
return `🌿 ${formatGitBranchValue(branch, status, pr)}`;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
export function gitStatusSummaryEqual(
|
|
131
|
-
left: GitStatusSummary | undefined,
|
|
132
|
-
right: GitStatusSummary | undefined,
|
|
133
|
-
): boolean {
|
|
134
|
-
if (!left || !right) return left === right;
|
|
135
|
-
return (
|
|
136
|
-
left.root === right.root &&
|
|
137
|
-
left.ahead === right.ahead &&
|
|
138
|
-
left.behind === right.behind &&
|
|
139
|
-
left.staged === right.staged &&
|
|
140
|
-
left.modified === right.modified &&
|
|
141
|
-
left.untracked === right.untracked &&
|
|
142
|
-
left.conflicts === right.conflicts
|
|
143
|
-
);
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
function formatCount(value: number): string {
|
|
147
|
-
return Intl.NumberFormat("en", { notation: "compact", maximumFractionDigits: 1 }).format(value);
|
|
148
|
-
}
|
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
import type { ConfigSegmentName, SegmentName } from "./types.js";
|
|
2
|
-
|
|
3
|
-
export const INFORMATION_PROFILE_NAMES = ["minimal", "balanced", "detailed"] as const;
|
|
4
|
-
export type InformationProfileName = (typeof INFORMATION_PROFILE_NAMES)[number];
|
|
5
|
-
export type InformationProfile = InformationProfileName | "custom";
|
|
6
|
-
|
|
7
|
-
export const INFORMATION_PROFILES: Readonly<
|
|
8
|
-
Record<InformationProfileName, readonly SegmentName[]>
|
|
9
|
-
> = {
|
|
10
|
-
minimal: ["model", "cwd", "branch", "context"],
|
|
11
|
-
balanced: ["model", "thinking", "cwd", "branch", "tools", "context", "time"],
|
|
12
|
-
detailed: [
|
|
13
|
-
"provider",
|
|
14
|
-
"model",
|
|
15
|
-
"thinking",
|
|
16
|
-
"cwd",
|
|
17
|
-
"branch",
|
|
18
|
-
"tools",
|
|
19
|
-
"context",
|
|
20
|
-
"tokens",
|
|
21
|
-
"cache",
|
|
22
|
-
"cost",
|
|
23
|
-
"time",
|
|
24
|
-
],
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
export function inferInformationProfile(
|
|
28
|
-
segments: readonly ConfigSegmentName[],
|
|
29
|
-
): InformationProfile {
|
|
30
|
-
for (const name of INFORMATION_PROFILE_NAMES) {
|
|
31
|
-
const profile = INFORMATION_PROFILES[name];
|
|
32
|
-
if (
|
|
33
|
-
segments.length === profile.length &&
|
|
34
|
-
segments.every((segment, index) => segment === profile[index])
|
|
35
|
-
) {
|
|
36
|
-
return name;
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
return "custom";
|
|
40
|
-
}
|