pi-extmgr 0.1.23 → 0.1.25
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 +25 -12
- package/package.json +1 -1
- package/src/commands/auto-update.ts +1 -1
- package/src/commands/cache.ts +5 -1
- package/src/commands/history.ts +4 -2
- package/src/commands/registry.ts +1 -1
- package/src/packages/discovery.ts +144 -51
- package/src/packages/extensions.ts +171 -63
- package/src/packages/install.ts +101 -22
- package/src/packages/management.ts +12 -37
- package/src/ui/package-config.ts +157 -126
- package/src/ui/remote.ts +77 -52
- package/src/ui/unified.ts +217 -172
- package/src/utils/auto-update.ts +34 -29
- package/src/utils/cache.ts +15 -2
- package/src/utils/history.ts +41 -2
- package/src/utils/mode.ts +56 -5
- package/src/utils/network.ts +15 -0
- package/src/utils/package-source.ts +31 -0
- package/src/utils/settings-list.ts +12 -0
- package/src/utils/settings.ts +35 -7
- package/src/utils/status.ts +10 -2
- package/src/utils/timer.ts +32 -8
- package/src/utils/ui-helpers.ts +2 -1
package/README.md
CHANGED
|
@@ -26,6 +26,7 @@ Requires Node.js `>=22.5.0`.
|
|
|
26
26
|
- Scope indicators (global/project), status indicators, update badges
|
|
27
27
|
- **Package extension configuration panel**
|
|
28
28
|
- Configure individual extension entrypoints inside an installed package (`c` on package row)
|
|
29
|
+
- Works with manifest-declared entrypoints and conventional `extensions/` package layouts
|
|
29
30
|
- Persists to package filters in `settings.json` (no manual JSON editing)
|
|
30
31
|
- **Safe staged local extension toggles**
|
|
31
32
|
- Toggle with `Space/Enter`, apply with `S`
|
|
@@ -36,15 +37,16 @@ Requires Node.js `>=22.5.0`.
|
|
|
36
37
|
- **Remote discovery and install**
|
|
37
38
|
- npm search/browse with pagination
|
|
38
39
|
- Install by source (`npm:`, `git:`, `https://`, `ssh://`, `git@...`, local path)
|
|
39
|
-
- Supports direct GitHub `.ts` installs and local
|
|
40
|
+
- Supports direct GitHub `.ts` installs and standalone local install for self-contained packages
|
|
40
41
|
- **Auto-update**
|
|
41
42
|
- Interactive wizard (`t` in manager, or `/extensions auto-update`)
|
|
42
43
|
- Persistent schedule restored on startup and session switch
|
|
43
|
-
- Background checks + status bar updates
|
|
44
|
+
- Background checks + status bar updates for installed npm packages
|
|
44
45
|
- **Operational visibility**
|
|
45
46
|
- Session history (`/extensions history`)
|
|
46
|
-
- Cache controls (`/extensions clear-cache`)
|
|
47
|
+
- Cache controls (`/extensions clear-cache` clears persistent + runtime extmgr caches)
|
|
47
48
|
- Status line summary (`pkg count • auto-update • known updates`)
|
|
49
|
+
- History now records local extension deletions and auto-update configuration changes
|
|
48
50
|
- **Interactive + non-interactive support**
|
|
49
51
|
- Works in TUI and non-UI modes
|
|
50
52
|
- Non-interactive commands for list/install/remove/update/auto-update
|
|
@@ -91,9 +93,9 @@ Open the manager:
|
|
|
91
93
|
/extensions remove [source] # Remove package
|
|
92
94
|
/extensions uninstall [source] # Alias: remove
|
|
93
95
|
/extensions update [source] # Update one package (or all when omitted)
|
|
94
|
-
/extensions auto-update [every] # No arg opens wizard in UI; accepts 1d, 1w, never, etc.
|
|
96
|
+
/extensions auto-update [every] # No arg opens wizard in UI; accepts 1d, 1w, 1mo, never, etc.
|
|
95
97
|
/extensions history [options] # View change history (supports filters)
|
|
96
|
-
/extensions clear-cache # Clear
|
|
98
|
+
/extensions clear-cache # Clear persistent + runtime extmgr caches
|
|
97
99
|
```
|
|
98
100
|
|
|
99
101
|
### Non-interactive mode
|
|
@@ -107,23 +109,33 @@ When Pi is running without UI, extmgr still supports command-driven workflows:
|
|
|
107
109
|
- `/extensions update [source]`
|
|
108
110
|
- `/extensions history [options]`
|
|
109
111
|
- `/extensions auto-update <duration>`
|
|
112
|
+
- Use `1mo` for monthly schedules (`/extensions history --since <duration>` also accepts `1mo`; `30m`/`24h` are just lookback examples)
|
|
110
113
|
|
|
111
|
-
Remote browsing/search menus require interactive
|
|
114
|
+
Remote browsing/search menus require the full interactive TUI.
|
|
115
|
+
|
|
116
|
+
### RPC / limited-UI mode
|
|
117
|
+
|
|
118
|
+
In RPC mode, dialog-based commands still work, but the custom TUI panels do not:
|
|
119
|
+
|
|
120
|
+
- `/extensions` falls back to read-only local/package lists
|
|
121
|
+
- `/extensions installed` lists packages directly
|
|
122
|
+
- remote browsing/search panels require the full interactive TUI
|
|
123
|
+
- package extension configuration requires the full interactive TUI
|
|
112
124
|
|
|
113
125
|
History options (works in non-interactive mode too):
|
|
114
126
|
|
|
115
127
|
- `--limit <n>`
|
|
116
|
-
- `--action <extension_toggle|package_install|package_update|package_remove|cache_clear>`
|
|
128
|
+
- `--action <extension_toggle|extension_delete|package_install|package_update|package_remove|cache_clear|auto_update_config>`
|
|
117
129
|
- `--success` / `--failed`
|
|
118
130
|
- `--package <query>`
|
|
119
|
-
- `--since <duration>` (e.g. `30m`, `24h`, `7d`, `1mo`)
|
|
120
|
-
- `--global` (non-interactive mode only; reads all persisted sessions)
|
|
131
|
+
- `--since <duration>` (e.g. `30m`, `24h`, `7d`, `1mo`; `1mo` is supported for monthly lookbacks)
|
|
132
|
+
- `--global` (non-interactive mode only; reads all persisted sessions under `~/.pi/agent/sessions`)
|
|
121
133
|
|
|
122
134
|
Examples:
|
|
123
135
|
|
|
124
136
|
- `/extensions history --failed --limit 50`
|
|
125
137
|
- `/extensions history --action package_update --since 7d`
|
|
126
|
-
- `/extensions history --global --package extmgr --since
|
|
138
|
+
- `/extensions history --global --package extmgr --since 1mo`
|
|
127
139
|
|
|
128
140
|
### Install sources
|
|
129
141
|
|
|
@@ -144,9 +156,10 @@ Examples:
|
|
|
144
156
|
- **Package extension config**: Select a package and press `c` (or Enter/A → Configure) to enable/disable individual package entrypoints.
|
|
145
157
|
- After saving package extension config, restart pi to fully apply changes.
|
|
146
158
|
- **Two install modes**:
|
|
147
|
-
- **Managed** (npm): Auto-updates with `pi update`, stored in pi's package cache
|
|
148
|
-
- **Local** (standalone): Copies to `~/.pi/agent/extensions/{package}/`,
|
|
159
|
+
- **Managed** (npm): Auto-updates with `pi update`, stored in pi's package cache, supports Pi package manifest/convention loading
|
|
160
|
+
- **Local** (standalone): Copies to `~/.pi/agent/extensions/{package}/`, so it only accepts runnable standalone layouts (manifest-declared/root entrypoints), requires `tar` on `PATH`, and rejects packages whose runtime `dependencies` are not already bundled with the package contents
|
|
149
161
|
- **Auto-update schedule is persistent**: `/extensions auto-update 1d` stays active across future Pi sessions and is restored when switching sessions.
|
|
162
|
+
- **Auto-update coverage is npm-only today**: extmgr checks update availability for managed npm packages; git/local installs are not included in the background update badge yet.
|
|
150
163
|
- **Settings/cache writes are hardened**: extmgr serializes writes and uses safe file replacement to reduce JSON corruption issues.
|
|
151
164
|
- **Invalid JSON is handled safely**: malformed `auto-update.json` / metadata cache files are backed up and reset; invalid `.pi/settings.json` is not overwritten during package-extension toggles.
|
|
152
165
|
- **Reload is built-in**: When extmgr asks to reload, it calls `ctx.reload()` directly.
|
package/package.json
CHANGED
|
@@ -54,7 +54,7 @@ export async function handleAutoUpdateSubcommand(
|
|
|
54
54
|
" 3d - Check every 3 days",
|
|
55
55
|
" 1w - Check weekly",
|
|
56
56
|
" 2w - Check every 2 weeks",
|
|
57
|
-
"
|
|
57
|
+
" 1mo - Check monthly (1m also works)",
|
|
58
58
|
" daily - Check daily (alias)",
|
|
59
59
|
" weekly - Check weekly (alias)",
|
|
60
60
|
];
|
package/src/commands/cache.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import type { ExtensionAPI, ExtensionCommandContext } from "@mariozechner/pi-coding-agent";
|
|
2
|
+
import { clearSearchCache } from "../packages/discovery.js";
|
|
3
|
+
import { clearRemotePackageInfoCache } from "../ui/remote.js";
|
|
2
4
|
import { clearCache } from "../utils/cache.js";
|
|
3
5
|
import { logCacheClear } from "../utils/history.js";
|
|
4
6
|
import { notify } from "../utils/notify.js";
|
|
@@ -10,8 +12,10 @@ export async function clearMetadataCacheCommand(
|
|
|
10
12
|
): Promise<void> {
|
|
11
13
|
try {
|
|
12
14
|
await clearCache();
|
|
15
|
+
clearSearchCache();
|
|
16
|
+
clearRemotePackageInfoCache();
|
|
13
17
|
logCacheClear(pi, true);
|
|
14
|
-
notify(ctx, "Metadata
|
|
18
|
+
notify(ctx, "Metadata and in-memory extmgr caches cleared.", "info");
|
|
15
19
|
} catch (error) {
|
|
16
20
|
const message = error instanceof Error ? error.message : String(error);
|
|
17
21
|
logCacheClear(pi, false, message);
|
package/src/commands/history.ts
CHANGED
|
@@ -11,10 +11,12 @@ import { formatListOutput } from "../utils/ui-helpers.js";
|
|
|
11
11
|
|
|
12
12
|
const HISTORY_ACTIONS: ChangeAction[] = [
|
|
13
13
|
"extension_toggle",
|
|
14
|
+
"extension_delete",
|
|
14
15
|
"package_install",
|
|
15
16
|
"package_update",
|
|
16
17
|
"package_remove",
|
|
17
18
|
"cache_clear",
|
|
19
|
+
"auto_update_config",
|
|
18
20
|
];
|
|
19
21
|
|
|
20
22
|
interface ParsedHistoryArgs {
|
|
@@ -187,12 +189,12 @@ function showHistoryHelp(ctx: ExtensionCommandContext): void {
|
|
|
187
189
|
"Options:",
|
|
188
190
|
" --limit <n> Maximum entries to show (default: 20)",
|
|
189
191
|
" --action <type> Filter by action",
|
|
190
|
-
"
|
|
192
|
+
` ${HISTORY_ACTIONS.join(" | ")}`,
|
|
191
193
|
" --success Show only successful entries",
|
|
192
194
|
" --failed Show only failed entries",
|
|
193
195
|
" --package <q> Filter by package/source/extension id",
|
|
194
196
|
" --since <d> Show only entries newer than duration (e.g. 30m, 24h, 7d, 1mo)",
|
|
195
|
-
" --global Read all persisted sessions (non-interactive mode only)",
|
|
197
|
+
" --global Read all persisted sessions from ~/.pi/agent/sessions (non-interactive mode only)",
|
|
196
198
|
"",
|
|
197
199
|
"Examples:",
|
|
198
200
|
" /extensions history --failed --limit 50",
|
package/src/commands/registry.ts
CHANGED
|
@@ -34,7 +34,7 @@ function showNonInteractiveHelp(ctx: ExtensionCommandContext): void {
|
|
|
34
34
|
" /extensions remove <source> - Remove a package",
|
|
35
35
|
" /extensions update [source] - Update one package or all packages",
|
|
36
36
|
" /extensions history [opts] - Show history (supports filters)",
|
|
37
|
-
" /extensions auto-update <d> - Configure auto-update (e.g. 1d, 1w, never)",
|
|
37
|
+
" /extensions auto-update <d> - Configure auto-update (e.g. 1d, 1w, 1mo, never)",
|
|
38
38
|
"",
|
|
39
39
|
"History examples:",
|
|
40
40
|
" /extensions history --failed --limit 50",
|
|
@@ -14,10 +14,30 @@ import { readSummary } from "../utils/fs.js";
|
|
|
14
14
|
import { parseNpmSource } from "../utils/format.js";
|
|
15
15
|
import {
|
|
16
16
|
getPackageSourceKind,
|
|
17
|
-
|
|
17
|
+
normalizePackageIdentity,
|
|
18
18
|
splitGitRepoAndRef,
|
|
19
|
+
stripGitSourcePrefix,
|
|
19
20
|
} from "../utils/package-source.js";
|
|
20
21
|
import { execNpm } from "../utils/npm-exec.js";
|
|
22
|
+
import { fetchWithTimeout } from "../utils/network.js";
|
|
23
|
+
|
|
24
|
+
const NPM_SEARCH_API = "https://registry.npmjs.org/-/v1/search";
|
|
25
|
+
const NPM_SEARCH_PAGE_SIZE = 250;
|
|
26
|
+
|
|
27
|
+
interface NpmSearchResultObject {
|
|
28
|
+
package?: {
|
|
29
|
+
name?: string;
|
|
30
|
+
version?: string;
|
|
31
|
+
description?: string;
|
|
32
|
+
keywords?: string[];
|
|
33
|
+
date?: string;
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
interface NpmSearchResponse {
|
|
38
|
+
total?: number;
|
|
39
|
+
objects?: NpmSearchResultObject[];
|
|
40
|
+
}
|
|
21
41
|
|
|
22
42
|
let searchCache: SearchCache | null = null;
|
|
23
43
|
|
|
@@ -50,15 +70,86 @@ import {
|
|
|
50
70
|
setCachedPackageSize,
|
|
51
71
|
} from "../utils/cache.js";
|
|
52
72
|
|
|
73
|
+
function toNpmPackage(entry: NpmSearchResultObject): NpmPackage | undefined {
|
|
74
|
+
const pkg = entry.package;
|
|
75
|
+
if (!pkg) return undefined;
|
|
76
|
+
|
|
77
|
+
const name = pkg.name?.trim();
|
|
78
|
+
if (!name) return undefined;
|
|
79
|
+
|
|
80
|
+
return {
|
|
81
|
+
name,
|
|
82
|
+
version: pkg.version,
|
|
83
|
+
description: pkg.description,
|
|
84
|
+
keywords: Array.isArray(pkg.keywords) ? pkg.keywords : undefined,
|
|
85
|
+
date: pkg.date,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async function fetchNpmSearchPage(
|
|
90
|
+
query: string,
|
|
91
|
+
from: number
|
|
92
|
+
): Promise<{
|
|
93
|
+
total: number;
|
|
94
|
+
resultCount: number;
|
|
95
|
+
packages: NpmPackage[];
|
|
96
|
+
}> {
|
|
97
|
+
const params = new URLSearchParams({
|
|
98
|
+
text: query,
|
|
99
|
+
size: String(NPM_SEARCH_PAGE_SIZE),
|
|
100
|
+
from: String(from),
|
|
101
|
+
});
|
|
102
|
+
const response = await fetchWithTimeout(
|
|
103
|
+
`${NPM_SEARCH_API}?${params.toString()}`,
|
|
104
|
+
TIMEOUTS.npmSearch
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
if (!response.ok) {
|
|
108
|
+
throw new Error(`npm registry search failed: HTTP ${response.status}`);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const data = (await response.json()) as NpmSearchResponse;
|
|
112
|
+
const objects = data.objects ?? [];
|
|
113
|
+
const packages = objects.map(toNpmPackage).filter((pkg): pkg is NpmPackage => !!pkg);
|
|
114
|
+
|
|
115
|
+
return {
|
|
116
|
+
total:
|
|
117
|
+
typeof data.total === "number" && Number.isFinite(data.total) ? data.total : packages.length,
|
|
118
|
+
resultCount: objects.length,
|
|
119
|
+
packages,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export async function fetchNpmRegistrySearchResults(query: string): Promise<NpmPackage[]> {
|
|
124
|
+
const packagesByName = new Map<string, NpmPackage>();
|
|
125
|
+
let from = 0;
|
|
126
|
+
let total = Infinity;
|
|
127
|
+
|
|
128
|
+
while (from < total) {
|
|
129
|
+
const page = await fetchNpmSearchPage(query, from);
|
|
130
|
+
total = page.total;
|
|
131
|
+
|
|
132
|
+
if (page.resultCount === 0) {
|
|
133
|
+
break;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
for (const pkg of page.packages) {
|
|
137
|
+
if (!packagesByName.has(pkg.name)) {
|
|
138
|
+
packagesByName.set(pkg.name, pkg);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
from += page.resultCount;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return [...packagesByName.values()];
|
|
146
|
+
}
|
|
147
|
+
|
|
53
148
|
export async function searchNpmPackages(
|
|
54
149
|
query: string,
|
|
55
150
|
ctx: ExtensionCommandContext,
|
|
56
|
-
|
|
151
|
+
_pi: ExtensionAPI
|
|
57
152
|
): Promise<NpmPackage[]> {
|
|
58
|
-
// Pull more results so browse mode has meaningful pagination.
|
|
59
|
-
// npm search can still cap server-side, but this improves coverage.
|
|
60
|
-
const searchLimit = 250;
|
|
61
|
-
|
|
62
153
|
// Check persistent cache first
|
|
63
154
|
const cached = await getCachedSearch(query);
|
|
64
155
|
if (cached && cached.length > 0) {
|
|
@@ -72,25 +163,12 @@ export async function searchNpmPackages(
|
|
|
72
163
|
ctx.ui.notify(`Searching npm for "${query}"...`, "info");
|
|
73
164
|
}
|
|
74
165
|
|
|
75
|
-
const
|
|
76
|
-
timeout: TIMEOUTS.npmSearch,
|
|
77
|
-
});
|
|
166
|
+
const packages = await fetchNpmRegistrySearchResults(query);
|
|
78
167
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
}
|
|
168
|
+
// Cache the results
|
|
169
|
+
await setCachedSearch(query, packages);
|
|
82
170
|
|
|
83
|
-
|
|
84
|
-
const parsed = JSON.parse(res.stdout || "[]") as NpmPackage[];
|
|
85
|
-
const filtered = parsed.filter((p) => !!p?.name);
|
|
86
|
-
|
|
87
|
-
// Cache the results
|
|
88
|
-
await setCachedSearch(query, filtered);
|
|
89
|
-
|
|
90
|
-
return filtered;
|
|
91
|
-
} catch {
|
|
92
|
-
throw new Error("Failed to parse npm search output");
|
|
93
|
-
}
|
|
171
|
+
return packages;
|
|
94
172
|
}
|
|
95
173
|
|
|
96
174
|
export async function getInstalledPackages(
|
|
@@ -121,15 +199,11 @@ function sanitizeListSourceSuffix(source: string): string {
|
|
|
121
199
|
.trim();
|
|
122
200
|
}
|
|
123
201
|
|
|
124
|
-
function
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
return normalizeLocalSourceIdentity(sanitized);
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
return sanitized.replace(/\\/g, "/").toLowerCase();
|
|
202
|
+
function getInstalledPackageIdentity(pkg: InstalledPackage): string {
|
|
203
|
+
return normalizePackageIdentity(
|
|
204
|
+
pkg.source,
|
|
205
|
+
pkg.resolvedPath ? { resolvedPath: pkg.resolvedPath } : undefined
|
|
206
|
+
);
|
|
133
207
|
}
|
|
134
208
|
|
|
135
209
|
function isScopeHeader(lowerTrimmed: string, scope: "global" | "project"): boolean {
|
|
@@ -181,12 +255,8 @@ function parseResolvedPathLine(line: string): string | undefined {
|
|
|
181
255
|
return undefined;
|
|
182
256
|
}
|
|
183
257
|
|
|
184
|
-
function parseInstalledPackagesOutputInternal(
|
|
185
|
-
text: string,
|
|
186
|
-
options?: { dedupeBySource?: boolean }
|
|
187
|
-
): InstalledPackage[] {
|
|
258
|
+
function parseInstalledPackagesOutputInternal(text: string): InstalledPackage[] {
|
|
188
259
|
const packages: InstalledPackage[] = [];
|
|
189
|
-
const seenSources = new Set<string>();
|
|
190
260
|
|
|
191
261
|
const lines = text.split("\n");
|
|
192
262
|
let currentScope: "global" | "project" = "global";
|
|
@@ -222,15 +292,6 @@ function parseInstalledPackagesOutputInternal(
|
|
|
222
292
|
if (!looksLikePackageSource(candidate)) continue;
|
|
223
293
|
|
|
224
294
|
const source = sanitizeListSourceSuffix(candidate);
|
|
225
|
-
if (options?.dedupeBySource !== false) {
|
|
226
|
-
const sourceIdentity = normalizeSourceIdentity(source);
|
|
227
|
-
if (seenSources.has(sourceIdentity)) {
|
|
228
|
-
currentPackage = undefined;
|
|
229
|
-
continue;
|
|
230
|
-
}
|
|
231
|
-
seenSources.add(sourceIdentity);
|
|
232
|
-
}
|
|
233
|
-
|
|
234
295
|
const { name, version } = parsePackageNameAndVersion(source);
|
|
235
296
|
|
|
236
297
|
const pkg: InstalledPackage = { source, name, scope: currentScope };
|
|
@@ -244,8 +305,34 @@ function parseInstalledPackagesOutputInternal(
|
|
|
244
305
|
return packages;
|
|
245
306
|
}
|
|
246
307
|
|
|
308
|
+
function shouldReplaceInstalledPackage(
|
|
309
|
+
current: InstalledPackage | undefined,
|
|
310
|
+
candidate: InstalledPackage
|
|
311
|
+
): boolean {
|
|
312
|
+
if (!current) {
|
|
313
|
+
return true;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
if (current.scope !== candidate.scope) {
|
|
317
|
+
return candidate.scope === "project";
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
return false;
|
|
321
|
+
}
|
|
322
|
+
|
|
247
323
|
export function parseInstalledPackagesOutput(text: string): InstalledPackage[] {
|
|
248
|
-
|
|
324
|
+
const parsed = parseInstalledPackagesOutputInternal(text);
|
|
325
|
+
const deduped = new Map<string, InstalledPackage>();
|
|
326
|
+
|
|
327
|
+
for (const pkg of parsed) {
|
|
328
|
+
const identity = getInstalledPackageIdentity(pkg);
|
|
329
|
+
const current = deduped.get(identity);
|
|
330
|
+
if (shouldReplaceInstalledPackage(current, pkg)) {
|
|
331
|
+
deduped.set(identity, pkg);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
return Array.from(deduped.values());
|
|
249
336
|
}
|
|
250
337
|
|
|
251
338
|
/**
|
|
@@ -263,10 +350,10 @@ export async function isSourceInstalled(
|
|
|
263
350
|
if (res.code !== 0) return false;
|
|
264
351
|
|
|
265
352
|
const installed = parseInstalledPackagesOutputAllScopes(res.stdout || "");
|
|
266
|
-
const expected =
|
|
353
|
+
const expected = normalizePackageIdentity(source);
|
|
267
354
|
|
|
268
355
|
return installed.some((pkg) => {
|
|
269
|
-
if (
|
|
356
|
+
if (getInstalledPackageIdentity(pkg) !== expected) {
|
|
270
357
|
return false;
|
|
271
358
|
}
|
|
272
359
|
return options?.scope ? pkg.scope === options.scope : true;
|
|
@@ -276,8 +363,14 @@ export async function isSourceInstalled(
|
|
|
276
363
|
}
|
|
277
364
|
}
|
|
278
365
|
|
|
366
|
+
/**
|
|
367
|
+
* parseInstalledPackagesOutputAllScopes returns the raw parsed entries from
|
|
368
|
+
* parseInstalledPackagesOutputInternal without deduplication or scope merging.
|
|
369
|
+
* Prefer parseInstalledPackagesOutput for user-facing lists, since it applies
|
|
370
|
+
* deduplication and normalized scope selection.
|
|
371
|
+
*/
|
|
279
372
|
export function parseInstalledPackagesOutputAllScopes(text: string): InstalledPackage[] {
|
|
280
|
-
return parseInstalledPackagesOutputInternal(text
|
|
373
|
+
return parseInstalledPackagesOutputInternal(text);
|
|
281
374
|
}
|
|
282
375
|
|
|
283
376
|
function extractGitPackageName(repoSpec: string): string {
|
|
@@ -316,7 +409,7 @@ function parsePackageNameAndVersion(fullSource: string): {
|
|
|
316
409
|
|
|
317
410
|
const sourceKind = getPackageSourceKind(fullSource);
|
|
318
411
|
if (sourceKind === "git") {
|
|
319
|
-
const gitSpec =
|
|
412
|
+
const gitSpec = stripGitSourcePrefix(fullSource);
|
|
320
413
|
const { repo } = splitGitRepoAndRef(gitSpec);
|
|
321
414
|
return { name: extractGitPackageName(repo) };
|
|
322
415
|
}
|