@narumitw/pi-statusline 0.49.15 → 0.50.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/src/directory.ts CHANGED
@@ -5,50 +5,46 @@ const DEFAULT_TRUNCATION_LENGTH = 3;
5
5
  const DEFAULT_HOME_SYMBOL = "~";
6
6
 
7
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);
8
+ export function formatDirectoryPath(cwd: string, homeDir: string | undefined, gitRoot: string | undefined): string {
9
+ const contracted =
10
+ gitRoot && (!homeDir || !samePath(gitRoot, homeDir))
11
+ ? (contractRepositoryPath(cwd, gitRoot) ?? contractPath(cwd, homeDir, DEFAULT_HOME_SYMBOL))
12
+ : contractPath(cwd, homeDir, DEFAULT_HOME_SYMBOL);
13
+ const safeContracted = sanitizeTerminalText(contracted);
14
+ const components = safeContracted.split("/").filter((component) => component.length > 0);
15
+ const displayed =
16
+ components.length > DEFAULT_TRUNCATION_LENGTH
17
+ ? components.slice(-DEFAULT_TRUNCATION_LENGTH).join("/")
18
+ : safeContracted;
19
+ const native = sep === "/" ? displayed : displayed.replaceAll("/", sep);
20
+ return sanitizeTerminalText(native || basename(cwd) || cwd);
25
21
  }
26
22
 
27
23
  function toSlashPath(value: string): string {
28
- return sep === "\\" ? value.replaceAll("\\", "/") : value;
24
+ return sep === "\\" ? value.replaceAll("\\", "/") : value;
29
25
  }
30
26
 
31
27
  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;
28
+ if (!root) return toSlashPath(path);
29
+ const child = relativeWithin(path, root);
30
+ if (child === undefined) return toSlashPath(path);
31
+ return child ? `${replacement}/${child}` : replacement;
36
32
  }
37
33
 
38
34
  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;
35
+ const child = relativeWithin(path, root);
36
+ if (child === undefined) return undefined;
37
+ const name = basename(root) || toSlashPath(root);
38
+ return child ? `${name}/${child}` : name;
43
39
  }
44
40
 
45
41
  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);
42
+ const child = relative(root, path);
43
+ if (child === "") return "";
44
+ if (child === ".." || child.startsWith(`..${sep}`) || isAbsolute(child)) return undefined;
45
+ return toSlashPath(child);
50
46
  }
51
47
 
52
48
  function samePath(left: string, right: string): boolean {
53
- return relative(left, right) === "";
49
+ return relative(left, right) === "";
54
50
  }
@@ -8,314 +8,286 @@ import type { StatuslineConfig } from "./types.js";
8
8
 
9
9
  export type ExtensionStatusIconAliasMap = ReadonlyMap<string, readonly string[]>;
10
10
  export interface ExtensionStatusRuntime {
11
- duplicateExtensions: string[];
12
- extensionStatusIconAliases: ExtensionStatusIconAliasMap;
11
+ duplicateExtensions: string[];
12
+ extensionStatusIconAliases: ExtensionStatusIconAliasMap;
13
13
  }
14
14
 
15
15
  const STATUSLINE_KEY = "statusline";
16
16
  const COMPATIBLE_STATUS_ICON_KEYS: Readonly<Record<string, string>> = {
17
- retry: "unknown-error-retry",
18
- sync: "pisync",
19
- "unknown-error-retry": "retry",
20
- pisync: "sync",
17
+ retry: "unknown-error-retry",
18
+ sync: "pisync",
19
+ "unknown-error-retry": "retry",
20
+ pisync: "sync",
21
21
  };
22
22
  const EMPTY_EXTENSION_STATUS_ICON_ALIASES: ExtensionStatusIconAliasMap = new Map();
23
- function extensionStatusSeparator(config: StatuslineConfig, theme: Theme): string {
24
- return powerlineExtensionSeparator(theme, config.palettePreset);
23
+ function extensionStatusSeparator(config: StatuslineConfig, theme: Theme, trueColor: boolean): string {
24
+ return powerlineExtensionSeparator(theme, config.palettePreset, trueColor);
25
25
  }
26
26
 
27
27
  export function formatExtensionStatuses(
28
- statuses: ReadonlyMap<string, string>,
29
- theme: Theme,
30
- config: StatuslineConfig,
31
- runtime: ExtensionStatusRuntime,
32
- hiddenKeys: ReadonlySet<string> = new Set(),
28
+ statuses: ReadonlyMap<string, string>,
29
+ theme: Theme,
30
+ config: StatuslineConfig,
31
+ runtime: ExtensionStatusRuntime,
32
+ hiddenKeys: ReadonlySet<string> = new Set(),
33
+ trueColor = true,
33
34
  ): string {
34
- const separator = extensionStatusSeparator(config, theme);
35
- const visibleStatuses = [
36
- ...formatDuplicateExtensionStatus(runtime, theme),
37
- ...[...statuses.entries()]
38
- .filter(
39
- ([key, value]) => key !== STATUSLINE_KEY && !hiddenKeys.has(key) && value.trim().length > 0,
40
- )
41
- .map(([key, value]) =>
42
- formatExtensionStatus(key, value, theme, config, runtime.extensionStatusIconAliases),
43
- ),
44
- ].slice(0, 5);
45
-
46
- return visibleStatuses.join(separator);
35
+ const separator = extensionStatusSeparator(config, theme, trueColor);
36
+ const visibleStatuses = [
37
+ ...formatDuplicateExtensionStatus(runtime, theme),
38
+ ...[...statuses.entries()]
39
+ .filter(([key, value]) => key !== STATUSLINE_KEY && !hiddenKeys.has(key) && value.trim().length > 0)
40
+ .map(([key, value]) => formatExtensionStatus(key, value, theme, config, runtime.extensionStatusIconAliases)),
41
+ ].slice(0, 5);
42
+
43
+ return visibleStatuses.join(separator);
47
44
  }
48
45
 
49
46
  export function formatExtensionStatus(
50
- key: string,
51
- value: string,
52
- theme: Theme,
53
- config: Pick<StatuslineConfig, "extensionStatusIcons">,
54
- extensionStatusIconAliases: ExtensionStatusIconAliasMap = EMPTY_EXTENSION_STATUS_ICON_ALIASES,
47
+ key: string,
48
+ value: string,
49
+ theme: Theme,
50
+ config: Pick<StatuslineConfig, "extensionStatusIcons">,
51
+ extensionStatusIconAliases: ExtensionStatusIconAliasMap = EMPTY_EXTENSION_STATUS_ICON_ALIASES,
55
52
  ): string {
56
- const status = splitExtensionStatusIcon(stripExtensionStatusPrefix(key, value));
57
- const text = simplifyExtensionStatusText(status.text);
58
- const color = extensionColor(key, value);
59
- const textColor = color === "warning" ? "warning" : "muted";
60
- const icon = extensionStatusIcon(
61
- key,
62
- status.icon,
63
- config.extensionStatusIcons,
64
- extensionStatusIconAliases,
65
- );
66
- const renderedText = theme.fg(textColor, text);
67
- return icon ? `${theme.fg(color, icon)} ${renderedText}` : renderedText;
53
+ const status = splitExtensionStatusIcon(stripExtensionStatusPrefix(key, value));
54
+ const text = simplifyExtensionStatusText(status.text);
55
+ const color = extensionColor(key, value);
56
+ const textColor = color === "warning" ? "warning" : "muted";
57
+ const icon = extensionStatusIcon(key, status.icon, config.extensionStatusIcons, extensionStatusIconAliases);
58
+ const renderedText = theme.fg(textColor, text);
59
+ return icon ? `${theme.fg(color, icon)} ${renderedText}` : renderedText;
68
60
  }
69
61
 
70
62
  function extensionStatusIcon(
71
- key: string,
72
- leadingIcon: string | undefined,
73
- configuredIcons: Record<string, string>,
74
- extensionStatusIconAliases: ExtensionStatusIconAliasMap,
63
+ key: string,
64
+ leadingIcon: string | undefined,
65
+ configuredIcons: Record<string, string>,
66
+ extensionStatusIconAliases: ExtensionStatusIconAliasMap,
75
67
  ) {
76
- if (Object.hasOwn(configuredIcons, key)) return configuredIcons[key];
77
- const namespaceIcon = configuredNamespaceIcon(key, configuredIcons);
78
- if (namespaceIcon !== undefined) return namespaceIcon;
79
- const compatibleKey = COMPATIBLE_STATUS_ICON_KEYS[key];
80
- if (compatibleKey && Object.hasOwn(configuredIcons, compatibleKey)) {
81
- return configuredIcons[compatibleKey];
82
- }
83
- for (const alias of extensionStatusAliasesForKey(key, extensionStatusIconAliases)) {
84
- if (Object.hasOwn(configuredIcons, alias)) return configuredIcons[alias];
85
- }
86
- return leadingIcon ?? DEFAULT_EXTENSION_STATUS_ICONS[key] ?? "🔌";
68
+ if (Object.hasOwn(configuredIcons, key)) return configuredIcons[key];
69
+ const namespaceIcon = configuredNamespaceIcon(key, configuredIcons);
70
+ if (namespaceIcon !== undefined) return namespaceIcon;
71
+ const compatibleKey = COMPATIBLE_STATUS_ICON_KEYS[key];
72
+ if (compatibleKey && Object.hasOwn(configuredIcons, compatibleKey)) {
73
+ return configuredIcons[compatibleKey];
74
+ }
75
+ for (const alias of extensionStatusAliasesForKey(key, extensionStatusIconAliases)) {
76
+ if (Object.hasOwn(configuredIcons, alias)) return configuredIcons[alias];
77
+ }
78
+ return leadingIcon ?? DEFAULT_EXTENSION_STATUS_ICONS[key] ?? "🔌";
87
79
  }
88
80
 
89
- function configuredNamespaceIcon(
90
- key: string,
91
- configuredIcons: Readonly<Record<string, string>>,
92
- ): string | undefined {
93
- let match: { baseLength: number; icon: string } | undefined;
94
- for (const [selector, icon] of Object.entries(configuredIcons)) {
95
- if (!selector.endsWith(":*")) continue;
96
- const base = selector.slice(0, -2);
97
- if (!base || !key.startsWith(`${base}:`)) continue;
98
- if (!match || base.length > match.baseLength) match = { baseLength: base.length, icon };
99
- }
100
- return match?.icon;
81
+ function configuredNamespaceIcon(key: string, configuredIcons: Readonly<Record<string, string>>): string | undefined {
82
+ let match: { baseLength: number; icon: string } | undefined;
83
+ for (const [selector, icon] of Object.entries(configuredIcons)) {
84
+ if (!selector.endsWith(":*")) continue;
85
+ const base = selector.slice(0, -2);
86
+ if (!base || !key.startsWith(`${base}:`)) continue;
87
+ if (!match || base.length > match.baseLength) match = { baseLength: base.length, icon };
88
+ }
89
+ return match?.icon;
101
90
  }
102
91
 
103
92
  function extensionStatusAliasesForKey(
104
- key: string,
105
- extensionStatusIconAliases: ExtensionStatusIconAliasMap,
93
+ key: string,
94
+ extensionStatusIconAliases: ExtensionStatusIconAliasMap,
106
95
  ): readonly string[] {
107
- for (const [statusBase, aliases] of extensionStatusIconAliases) {
108
- if (statusKeyMatchesStatusBase(key, statusBase)) return aliases;
109
- }
110
- return [];
96
+ for (const [statusBase, aliases] of extensionStatusIconAliases) {
97
+ if (statusKeyMatchesStatusBase(key, statusBase)) return aliases;
98
+ }
99
+ return [];
111
100
  }
112
101
 
113
102
  function statusKeyMatchesStatusBase(key: string, statusBase: string): boolean {
114
- return key === statusBase || key.startsWith(`${statusBase}:`) || key.startsWith(`${statusBase}/`);
103
+ return key === statusBase || key.startsWith(`${statusBase}:`) || key.startsWith(`${statusBase}/`);
115
104
  }
116
105
 
117
106
  export function wrapExtensionStatusline(status: string, width: number): string[] {
118
- if (!status || width <= 0) return [];
119
- return wrapTextWithAnsi(status, width);
107
+ if (!status || width <= 0) return [];
108
+ return wrapTextWithAnsi(status, width);
120
109
  }
121
110
 
122
111
  function formatDuplicateExtensionStatus(runtime: ExtensionStatusRuntime, theme: Theme): string[] {
123
- if (runtime.duplicateExtensions.length === 0) return [];
124
- const names = runtime.duplicateExtensions.slice(0, 2).join(", ");
125
- const suffix =
126
- runtime.duplicateExtensions.length > 2 ? ` +${runtime.duplicateExtensions.length - 2}` : "";
127
- return [`${theme.fg("warning", "⚠️")} ${theme.fg("warning", `dup ${names}${suffix}`)}`];
112
+ if (runtime.duplicateExtensions.length === 0) return [];
113
+ const names = runtime.duplicateExtensions.slice(0, 2).join(", ");
114
+ const suffix = runtime.duplicateExtensions.length > 2 ? ` +${runtime.duplicateExtensions.length - 2}` : "";
115
+ return [`${theme.fg("warning", "⚠️")} ${theme.fg("warning", `dup ${names}${suffix}`)}`];
128
116
  }
129
117
 
130
118
  export function splitExtensionStatusIcon(value: string): { icon?: string; text: string } {
131
- const trimmed = value.trim();
132
- const [firstToken, ...restTokens] = trimmed.split(/\s+/);
133
- if (firstToken && isEmojiOnlyToken(firstToken)) {
134
- return { icon: firstToken, text: restTokens.join(" ") };
135
- }
136
- return { text: trimmed };
119
+ const trimmed = value.trim();
120
+ const [firstToken, ...restTokens] = trimmed.split(/\s+/);
121
+ if (firstToken && isEmojiOnlyToken(firstToken)) {
122
+ return { icon: firstToken, text: restTokens.join(" ") };
123
+ }
124
+ return { text: trimmed };
137
125
  }
138
126
 
139
127
  function isEmojiOnlyToken(value: string): boolean {
140
- 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(
141
- value,
142
- );
128
+ 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(
129
+ value,
130
+ );
143
131
  }
144
132
 
145
133
  export function extensionColor(key: string, value: string): ThemeColor {
146
- const normalized = `${key} ${value}`.toLowerCase();
147
- if (/missing|error|fail|conflict|duplicate|unavailable/.test(normalized)) return "warning";
148
- if (normalized.includes("codex")) return "accent";
149
- if (/ready|active|running|enabled|awake|ok/.test(normalized)) return "success";
150
- return "muted";
134
+ const normalized = `${key} ${value}`.toLowerCase();
135
+ if (/missing|error|fail|conflict|duplicate|unavailable/.test(normalized)) return "warning";
136
+ if (normalized.includes("codex")) return "accent";
137
+ if (/ready|active|running|enabled|awake|ok/.test(normalized)) return "success";
138
+ return "muted";
151
139
  }
152
140
 
153
141
  export function stripExtensionStatusPrefix(key: string, value: string): string {
154
- return value.trim().replace(new RegExp(`^${escapeRegExp(key)}\\s*:\\s*`, "iu"), "");
142
+ return value.trim().replace(new RegExp(`^${escapeRegExp(key)}\\s*:\\s*`, "iu"), "");
155
143
  }
156
144
 
157
145
  export function simplifyExtensionStatusText(value: string): string {
158
- return value
159
- .trim()
160
- .replace(/\bready\b/giu, "✓")
161
- .replace(/\bmissing\b/giu, "✗")
162
- .replace(/,\s*/g, " ")
163
- .replace(/\s+\([^)]*\)\s*$/, "")
164
- .replace(/\s+/g, " ");
146
+ return value
147
+ .trim()
148
+ .replace(/\bready\b/giu, "✓")
149
+ .replace(/\bmissing\b/giu, "✗")
150
+ .replace(/,\s*/g, " ")
151
+ .replace(/\s+\([^)]*\)\s*$/, "")
152
+ .replace(/\s+/g, " ");
165
153
  }
166
154
 
167
155
  function escapeRegExp(value: string): string {
168
- return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
156
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
169
157
  }
170
158
 
171
159
  interface InstalledExtensionPackage {
172
- packageName: string;
173
- source: string;
174
- identity: string;
160
+ packageName: string;
161
+ source: string;
162
+ identity: string;
175
163
  }
176
164
 
177
165
  export function readInstalledExtensionPackages(cwd: string): InstalledExtensionPackage[] {
178
- const packages: InstalledExtensionPackage[] = [];
179
- const settingsFiles = extensionSettingsFiles(cwd);
180
-
181
- for (const settingsFile of settingsFiles) {
182
- const baseDirectory = dirname(settingsFile);
183
- for (const rawSource of readPackageSources(settingsFile)) {
184
- const source = rawSource.trim();
185
- if (!source) continue;
186
- const packageName = packageNameForSource(source, baseDirectory);
187
- if (!packageName) continue;
188
- packages.push({ packageName, source, identity: sourceIdentity(source, baseDirectory) });
189
- }
190
- }
191
-
192
- return packages;
166
+ const packages: InstalledExtensionPackage[] = [];
167
+ const settingsFiles = extensionSettingsFiles(cwd);
168
+
169
+ for (const settingsFile of settingsFiles) {
170
+ const baseDirectory = dirname(settingsFile);
171
+ for (const rawSource of readPackageSources(settingsFile)) {
172
+ const source = rawSource.trim();
173
+ if (!source) continue;
174
+ const packageName = packageNameForSource(source, baseDirectory);
175
+ if (!packageName) continue;
176
+ packages.push({ packageName, source, identity: sourceIdentity(source, baseDirectory) });
177
+ }
178
+ }
179
+
180
+ return packages;
193
181
  }
194
182
 
195
183
  function extensionSettingsFiles(cwd: string): string[] {
196
- return [join(getAgentDir(), "settings.json"), join(cwd, ".pi", "settings.json")].filter((file) =>
197
- existsSync(file),
198
- );
184
+ return [join(getAgentDir(), "settings.json"), join(cwd, ".pi", "settings.json")].filter((file) => existsSync(file));
199
185
  }
200
186
 
201
- export function findDuplicateExtensions(
202
- installedPackages: readonly InstalledExtensionPackage[],
203
- ): string[] {
204
- const sourcesByPackage = new Map<string, Set<string>>();
187
+ export function findDuplicateExtensions(installedPackages: readonly InstalledExtensionPackage[]): string[] {
188
+ const sourcesByPackage = new Map<string, Set<string>>();
205
189
 
206
- for (const extensionPackage of installedPackages) {
207
- const sources = sourcesByPackage.get(extensionPackage.packageName) ?? new Set<string>();
208
- sources.add(extensionPackage.identity);
209
- sourcesByPackage.set(extensionPackage.packageName, sources);
210
- }
190
+ for (const extensionPackage of installedPackages) {
191
+ const sources = sourcesByPackage.get(extensionPackage.packageName) ?? new Set<string>();
192
+ sources.add(extensionPackage.identity);
193
+ sourcesByPackage.set(extensionPackage.packageName, sources);
194
+ }
211
195
 
212
- return [...sourcesByPackage.entries()]
213
- .filter(([, sources]) => sources.size > 1)
214
- .map(([packageName]) => packageName.replace(/^@[^/]+\//, "").replace(/^pi-/, ""));
196
+ return [...sourcesByPackage.entries()]
197
+ .filter(([, sources]) => sources.size > 1)
198
+ .map(([packageName]) => packageName.replace(/^@[^/]+\//, "").replace(/^pi-/, ""));
215
199
  }
216
200
 
217
201
  export function buildExtensionStatusIconAliases(
218
- installedPackages: readonly { packageName: string; source?: string }[],
202
+ installedPackages: readonly { packageName: string; source?: string }[],
219
203
  ): Map<string, string[]> {
220
- const packageAliasesByStatusBase = new Map<string, Map<string, string[]>>();
221
-
222
- for (const extensionPackage of installedPackages) {
223
- const candidate = extensionStatusIconAliasCandidate(
224
- extensionPackage.packageName,
225
- extensionPackage.source,
226
- );
227
- if (!candidate) continue;
228
- const aliasesByPackage =
229
- packageAliasesByStatusBase.get(candidate.statusBase) ?? new Map<string, string[]>();
230
- const existingAliases = aliasesByPackage.get(extensionPackage.packageName) ?? [];
231
- aliasesByPackage.set(
232
- extensionPackage.packageName,
233
- uniqueStrings([...existingAliases, ...candidate.aliases]),
234
- );
235
- packageAliasesByStatusBase.set(candidate.statusBase, aliasesByPackage);
236
- }
237
-
238
- const aliases = new Map<string, string[]>();
239
- for (const [statusBase, aliasesByPackage] of packageAliasesByStatusBase) {
240
- if (aliasesByPackage.size === 1)
241
- aliases.set(statusBase, [...aliasesByPackage.values()][0] ?? []);
242
- }
243
- return aliases;
204
+ const packageAliasesByStatusBase = new Map<string, Map<string, string[]>>();
205
+
206
+ for (const extensionPackage of installedPackages) {
207
+ const candidate = extensionStatusIconAliasCandidate(extensionPackage.packageName, extensionPackage.source);
208
+ if (!candidate) continue;
209
+ const aliasesByPackage = packageAliasesByStatusBase.get(candidate.statusBase) ?? new Map<string, string[]>();
210
+ const existingAliases = aliasesByPackage.get(extensionPackage.packageName) ?? [];
211
+ aliasesByPackage.set(extensionPackage.packageName, uniqueStrings([...existingAliases, ...candidate.aliases]));
212
+ packageAliasesByStatusBase.set(candidate.statusBase, aliasesByPackage);
213
+ }
214
+
215
+ const aliases = new Map<string, string[]>();
216
+ for (const [statusBase, aliasesByPackage] of packageAliasesByStatusBase) {
217
+ if (aliasesByPackage.size === 1) aliases.set(statusBase, [...aliasesByPackage.values()][0] ?? []);
218
+ }
219
+ return aliases;
244
220
  }
245
221
 
246
222
  function extensionStatusIconAliasCandidate(
247
- packageName: string,
248
- source?: string,
223
+ packageName: string,
224
+ source?: string,
249
225
  ): { statusBase: string; aliases: string[] } | undefined {
250
- const packageBase = packageBaseName(packageName);
251
- const statusBase = statusBaseFromPackageBase(packageBase);
252
- if (!statusBase) return undefined;
253
-
254
- const sourceAliases = source?.startsWith("npm:") ? [source, `npm:${npmPackageName(source)}`] : [];
255
- return {
256
- statusBase,
257
- aliases: uniqueStrings([...sourceAliases, packageName, packageBase, statusBase]),
258
- };
226
+ const packageBase = packageBaseName(packageName);
227
+ const statusBase = statusBaseFromPackageBase(packageBase);
228
+ if (!statusBase) return undefined;
229
+
230
+ const sourceAliases = source?.startsWith("npm:") ? [source, `npm:${npmPackageName(source)}`] : [];
231
+ return {
232
+ statusBase,
233
+ aliases: uniqueStrings([...sourceAliases, packageName, packageBase, statusBase]),
234
+ };
259
235
  }
260
236
 
261
237
  function packageBaseName(packageName: string): string {
262
- const slashIndex = packageName.lastIndexOf("/");
263
- return slashIndex === -1 ? packageName : packageName.slice(slashIndex + 1);
238
+ const slashIndex = packageName.lastIndexOf("/");
239
+ return slashIndex === -1 ? packageName : packageName.slice(slashIndex + 1);
264
240
  }
265
241
 
266
242
  function statusBaseFromPackageBase(packageBase: string): string {
267
- return packageBase.startsWith("pi-") && packageBase.length > "pi-".length
268
- ? packageBase.slice("pi-".length)
269
- : packageBase;
243
+ return packageBase.startsWith("pi-") && packageBase.length > "pi-".length
244
+ ? packageBase.slice("pi-".length)
245
+ : packageBase;
270
246
  }
271
247
 
272
248
  function uniqueStrings(values: readonly string[]): string[] {
273
- return [...new Set(values.filter((value) => value.length > 0))];
249
+ return [...new Set(values.filter((value) => value.length > 0))];
274
250
  }
275
251
 
276
252
  function readPackageSources(settingsFile: string): string[] {
277
- try {
278
- const settings = JSON.parse(readFileSync(settingsFile, "utf8")) as { packages?: unknown[] };
279
- return (settings.packages ?? [])
280
- .map((entry) => {
281
- if (typeof entry === "string") return entry;
282
- if (
283
- entry &&
284
- typeof entry === "object" &&
285
- typeof (entry as { source?: unknown }).source === "string"
286
- ) {
287
- return (entry as { source: string }).source;
288
- }
289
- return undefined;
290
- })
291
- .filter((source): source is string => source !== undefined);
292
- } catch {
293
- return [];
294
- }
253
+ try {
254
+ const settings = JSON.parse(readFileSync(settingsFile, "utf8")) as { packages?: unknown[] };
255
+ return (settings.packages ?? [])
256
+ .map((entry) => {
257
+ if (typeof entry === "string") return entry;
258
+ if (entry && typeof entry === "object" && typeof (entry as { source?: unknown }).source === "string") {
259
+ return (entry as { source: string }).source;
260
+ }
261
+ return undefined;
262
+ })
263
+ .filter((source): source is string => source !== undefined);
264
+ } catch {
265
+ return [];
266
+ }
295
267
  }
296
268
 
297
269
  function packageNameForSource(source: string, baseDirectory: string): string | undefined {
298
- if (source.startsWith("npm:")) return npmPackageName(source);
299
- const packageJson = join(resolveSourcePath(source, baseDirectory), "package.json");
300
- try {
301
- const packageData = JSON.parse(readFileSync(packageJson, "utf8")) as { name?: unknown };
302
- return typeof packageData.name === "string" ? packageData.name : undefined;
303
- } catch {
304
- return undefined;
305
- }
270
+ if (source.startsWith("npm:")) return npmPackageName(source);
271
+ const packageJson = join(resolveSourcePath(source, baseDirectory), "package.json");
272
+ try {
273
+ const packageData = JSON.parse(readFileSync(packageJson, "utf8")) as { name?: unknown };
274
+ return typeof packageData.name === "string" ? packageData.name : undefined;
275
+ } catch {
276
+ return undefined;
277
+ }
306
278
  }
307
279
 
308
280
  export function npmPackageName(source: string): string {
309
- const spec = source.slice("npm:".length);
310
- if (spec.startsWith("@")) return spec.split("@").slice(0, 2).join("@");
311
- return spec.split("@")[0] ?? spec;
281
+ const spec = source.slice("npm:".length);
282
+ if (spec.startsWith("@")) return spec.split("@").slice(0, 2).join("@");
283
+ return spec.split("@")[0] ?? spec;
312
284
  }
313
285
 
314
286
  function sourceIdentity(source: string, baseDirectory: string): string {
315
- if (source.startsWith("npm:")) return `npm:${npmPackageName(source)}`;
316
- return resolveSourcePath(source, baseDirectory);
287
+ if (source.startsWith("npm:")) return `npm:${npmPackageName(source)}`;
288
+ return resolveSourcePath(source, baseDirectory);
317
289
  }
318
290
 
319
291
  function resolveSourcePath(source: string, baseDirectory: string): string {
320
- return isAbsolute(source) ? source : resolve(baseDirectory, source);
292
+ return isAbsolute(source) ? source : resolve(baseDirectory, source);
321
293
  }