pi-extmgr 0.1.22 → 0.1.24
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/index.ts +6 -1
- package/src/packages/discovery.ts +53 -28
- package/src/packages/extensions.ts +171 -63
- package/src/packages/install.ts +118 -24
- package/src/packages/management.ts +58 -37
- package/src/ui/package-config.ts +157 -126
- package/src/ui/remote.ts +79 -54
- package/src/ui/unified.ts +222 -173
- package/src/utils/auto-update.ts +36 -31
- package/src/utils/command.ts +77 -1
- package/src/utils/format.ts +23 -3
- package/src/utils/history.ts +41 -2
- package/src/utils/mode.ts +56 -5
- package/src/utils/npm-exec.ts +47 -0
- package/src/utils/package-source.ts +43 -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",
|
package/src/index.ts
CHANGED
|
@@ -70,12 +70,17 @@ export default function extensionsManager(pi: ExtensionAPI) {
|
|
|
70
70
|
// Restore persisted auto-update config into session entries so sync lookups are valid.
|
|
71
71
|
await hydrateAutoUpdateConfig(pi, ctx);
|
|
72
72
|
|
|
73
|
-
if (!ctx.hasUI)
|
|
73
|
+
if (!ctx.hasUI) {
|
|
74
|
+
stopAutoUpdateTimer();
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
74
77
|
|
|
75
78
|
const config = getAutoUpdateConfig(ctx);
|
|
76
79
|
if (config.enabled && config.intervalMs > 0) {
|
|
77
80
|
const getCtx: ContextProvider = () => ctx;
|
|
78
81
|
startAutoUpdateTimer(pi, getCtx, createAutoUpdateNotificationHandler(ctx));
|
|
82
|
+
} else {
|
|
83
|
+
stopAutoUpdateTimer();
|
|
79
84
|
}
|
|
80
85
|
|
|
81
86
|
setImmediate(() => {
|
|
@@ -12,7 +12,13 @@ import type { InstalledPackage, NpmPackage, SearchCache } from "../types/index.j
|
|
|
12
12
|
import { CACHE_TTL, TIMEOUTS } from "../constants.js";
|
|
13
13
|
import { readSummary } from "../utils/fs.js";
|
|
14
14
|
import { parseNpmSource } from "../utils/format.js";
|
|
15
|
-
import {
|
|
15
|
+
import {
|
|
16
|
+
getPackageSourceKind,
|
|
17
|
+
normalizePackageIdentity,
|
|
18
|
+
splitGitRepoAndRef,
|
|
19
|
+
stripGitSourcePrefix,
|
|
20
|
+
} from "../utils/package-source.js";
|
|
21
|
+
import { execNpm } from "../utils/npm-exec.js";
|
|
16
22
|
|
|
17
23
|
let searchCache: SearchCache | null = null;
|
|
18
24
|
|
|
@@ -67,9 +73,8 @@ export async function searchNpmPackages(
|
|
|
67
73
|
ctx.ui.notify(`Searching npm for "${query}"...`, "info");
|
|
68
74
|
}
|
|
69
75
|
|
|
70
|
-
const res = await pi
|
|
76
|
+
const res = await execNpm(pi, ["search", "--json", `--searchlimit=${searchLimit}`, query], ctx, {
|
|
71
77
|
timeout: TIMEOUTS.npmSearch,
|
|
72
|
-
cwd: ctx.cwd,
|
|
73
78
|
});
|
|
74
79
|
|
|
75
80
|
if (res.code !== 0) {
|
|
@@ -117,8 +122,11 @@ function sanitizeListSourceSuffix(source: string): string {
|
|
|
117
122
|
.trim();
|
|
118
123
|
}
|
|
119
124
|
|
|
120
|
-
function
|
|
121
|
-
return
|
|
125
|
+
function getInstalledPackageIdentity(pkg: InstalledPackage): string {
|
|
126
|
+
return normalizePackageIdentity(
|
|
127
|
+
pkg.source,
|
|
128
|
+
pkg.resolvedPath ? { resolvedPath: pkg.resolvedPath } : undefined
|
|
129
|
+
);
|
|
122
130
|
}
|
|
123
131
|
|
|
124
132
|
function isScopeHeader(lowerTrimmed: string, scope: "global" | "project"): boolean {
|
|
@@ -170,12 +178,8 @@ function parseResolvedPathLine(line: string): string | undefined {
|
|
|
170
178
|
return undefined;
|
|
171
179
|
}
|
|
172
180
|
|
|
173
|
-
function parseInstalledPackagesOutputInternal(
|
|
174
|
-
text: string,
|
|
175
|
-
options?: { dedupeBySource?: boolean }
|
|
176
|
-
): InstalledPackage[] {
|
|
181
|
+
function parseInstalledPackagesOutputInternal(text: string): InstalledPackage[] {
|
|
177
182
|
const packages: InstalledPackage[] = [];
|
|
178
|
-
const seenSources = new Set<string>();
|
|
179
183
|
|
|
180
184
|
const lines = text.split("\n");
|
|
181
185
|
let currentScope: "global" | "project" = "global";
|
|
@@ -211,15 +215,6 @@ function parseInstalledPackagesOutputInternal(
|
|
|
211
215
|
if (!looksLikePackageSource(candidate)) continue;
|
|
212
216
|
|
|
213
217
|
const source = sanitizeListSourceSuffix(candidate);
|
|
214
|
-
if (options?.dedupeBySource !== false) {
|
|
215
|
-
const sourceIdentity = normalizeSourceIdentity(source);
|
|
216
|
-
if (seenSources.has(sourceIdentity)) {
|
|
217
|
-
currentPackage = undefined;
|
|
218
|
-
continue;
|
|
219
|
-
}
|
|
220
|
-
seenSources.add(sourceIdentity);
|
|
221
|
-
}
|
|
222
|
-
|
|
223
218
|
const { name, version } = parsePackageNameAndVersion(source);
|
|
224
219
|
|
|
225
220
|
const pkg: InstalledPackage = { source, name, scope: currentScope };
|
|
@@ -233,8 +228,34 @@ function parseInstalledPackagesOutputInternal(
|
|
|
233
228
|
return packages;
|
|
234
229
|
}
|
|
235
230
|
|
|
231
|
+
function shouldReplaceInstalledPackage(
|
|
232
|
+
current: InstalledPackage | undefined,
|
|
233
|
+
candidate: InstalledPackage
|
|
234
|
+
): boolean {
|
|
235
|
+
if (!current) {
|
|
236
|
+
return true;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (current.scope !== candidate.scope) {
|
|
240
|
+
return candidate.scope === "project";
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
return false;
|
|
244
|
+
}
|
|
245
|
+
|
|
236
246
|
export function parseInstalledPackagesOutput(text: string): InstalledPackage[] {
|
|
237
|
-
|
|
247
|
+
const parsed = parseInstalledPackagesOutputInternal(text);
|
|
248
|
+
const deduped = new Map<string, InstalledPackage>();
|
|
249
|
+
|
|
250
|
+
for (const pkg of parsed) {
|
|
251
|
+
const identity = getInstalledPackageIdentity(pkg);
|
|
252
|
+
const current = deduped.get(identity);
|
|
253
|
+
if (shouldReplaceInstalledPackage(current, pkg)) {
|
|
254
|
+
deduped.set(identity, pkg);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
return Array.from(deduped.values());
|
|
238
259
|
}
|
|
239
260
|
|
|
240
261
|
/**
|
|
@@ -252,10 +273,10 @@ export async function isSourceInstalled(
|
|
|
252
273
|
if (res.code !== 0) return false;
|
|
253
274
|
|
|
254
275
|
const installed = parseInstalledPackagesOutputAllScopes(res.stdout || "");
|
|
255
|
-
const expected =
|
|
276
|
+
const expected = normalizePackageIdentity(source);
|
|
256
277
|
|
|
257
278
|
return installed.some((pkg) => {
|
|
258
|
-
if (
|
|
279
|
+
if (getInstalledPackageIdentity(pkg) !== expected) {
|
|
259
280
|
return false;
|
|
260
281
|
}
|
|
261
282
|
return options?.scope ? pkg.scope === options.scope : true;
|
|
@@ -265,8 +286,14 @@ export async function isSourceInstalled(
|
|
|
265
286
|
}
|
|
266
287
|
}
|
|
267
288
|
|
|
289
|
+
/**
|
|
290
|
+
* parseInstalledPackagesOutputAllScopes returns the raw parsed entries from
|
|
291
|
+
* parseInstalledPackagesOutputInternal without deduplication or scope merging.
|
|
292
|
+
* Prefer parseInstalledPackagesOutput for user-facing lists, since it applies
|
|
293
|
+
* deduplication and normalized scope selection.
|
|
294
|
+
*/
|
|
268
295
|
export function parseInstalledPackagesOutputAllScopes(text: string): InstalledPackage[] {
|
|
269
|
-
return parseInstalledPackagesOutputInternal(text
|
|
296
|
+
return parseInstalledPackagesOutputInternal(text);
|
|
270
297
|
}
|
|
271
298
|
|
|
272
299
|
function extractGitPackageName(repoSpec: string): string {
|
|
@@ -305,7 +332,7 @@ function parsePackageNameAndVersion(fullSource: string): {
|
|
|
305
332
|
|
|
306
333
|
const sourceKind = getPackageSourceKind(fullSource);
|
|
307
334
|
if (sourceKind === "git") {
|
|
308
|
-
const gitSpec =
|
|
335
|
+
const gitSpec = stripGitSourcePrefix(fullSource);
|
|
309
336
|
const { repo } = splitGitRepoAndRef(gitSpec);
|
|
310
337
|
return { name: extractGitPackageName(repo) };
|
|
311
338
|
}
|
|
@@ -375,9 +402,8 @@ async function fetchPackageSize(
|
|
|
375
402
|
|
|
376
403
|
try {
|
|
377
404
|
// Try to get unpacked size from npm view
|
|
378
|
-
const res = await pi
|
|
405
|
+
const res = await execNpm(pi, ["view", pkgName, "dist.unpackedSize", "--json"], ctx, {
|
|
379
406
|
timeout: TIMEOUTS.npmView,
|
|
380
|
-
cwd: ctx.cwd,
|
|
381
407
|
});
|
|
382
408
|
if (res.code === 0) {
|
|
383
409
|
try {
|
|
@@ -441,9 +467,8 @@ async function addPackageMetadata(
|
|
|
441
467
|
if (cached?.description) {
|
|
442
468
|
pkg.description = cached.description;
|
|
443
469
|
} else {
|
|
444
|
-
const res = await pi
|
|
470
|
+
const res = await execNpm(pi, ["view", pkgName, "description", "--json"], ctx, {
|
|
445
471
|
timeout: TIMEOUTS.npmView,
|
|
446
|
-
cwd: ctx.cwd,
|
|
447
472
|
});
|
|
448
473
|
if (res.code === 0) {
|
|
449
474
|
try {
|
|
@@ -9,6 +9,7 @@ import { getAgentDir } from "@mariozechner/pi-coding-agent";
|
|
|
9
9
|
import type { InstalledPackage, PackageExtensionEntry, Scope, State } from "../types/index.js";
|
|
10
10
|
import { parseNpmSource } from "../utils/format.js";
|
|
11
11
|
import { fileExists, readSummary } from "../utils/fs.js";
|
|
12
|
+
import { resolveNpmCommand } from "../utils/npm-exec.js";
|
|
12
13
|
|
|
13
14
|
interface PackageSettingsObject {
|
|
14
15
|
source: string;
|
|
@@ -19,6 +20,14 @@ interface SettingsFile {
|
|
|
19
20
|
packages?: (string | PackageSettingsObject)[];
|
|
20
21
|
}
|
|
21
22
|
|
|
23
|
+
export interface PackageManifest {
|
|
24
|
+
name?: string;
|
|
25
|
+
dependencies?: Record<string, string>;
|
|
26
|
+
pi?: {
|
|
27
|
+
extensions?: unknown;
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
22
31
|
const execFileAsync = promisify(execFile);
|
|
23
32
|
let globalNpmRootCache: string | null | undefined;
|
|
24
33
|
|
|
@@ -50,7 +59,8 @@ async function getGlobalNpmRoot(): Promise<string | undefined> {
|
|
|
50
59
|
}
|
|
51
60
|
|
|
52
61
|
try {
|
|
53
|
-
const
|
|
62
|
+
const npmCommand = resolveNpmCommand(["root", "-g"]);
|
|
63
|
+
const { stdout } = await execFileAsync(npmCommand.command, npmCommand.args, {
|
|
54
64
|
timeout: 2_000,
|
|
55
65
|
windowsHide: true,
|
|
56
66
|
});
|
|
@@ -206,6 +216,125 @@ async function writeSettingsFile(path: string, settings: SettingsFile): Promise<
|
|
|
206
216
|
}
|
|
207
217
|
}
|
|
208
218
|
|
|
219
|
+
function findPackageSettingsIndex(
|
|
220
|
+
packages: SettingsFile["packages"] extends infer T ? NonNullable<T> : never,
|
|
221
|
+
normalizedSource: string
|
|
222
|
+
): number {
|
|
223
|
+
return packages.findIndex((pkg) => {
|
|
224
|
+
if (typeof pkg === "string") {
|
|
225
|
+
return normalizeSource(pkg) === normalizedSource;
|
|
226
|
+
}
|
|
227
|
+
return normalizeSource(pkg.source) === normalizedSource;
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function toPackageSettingsObject(
|
|
232
|
+
existing: string | PackageSettingsObject | undefined,
|
|
233
|
+
packageSource: string
|
|
234
|
+
): PackageSettingsObject {
|
|
235
|
+
if (typeof existing === "string") {
|
|
236
|
+
return { source: existing, extensions: [] };
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (existing && typeof existing.source === "string") {
|
|
240
|
+
return {
|
|
241
|
+
source: existing.source,
|
|
242
|
+
extensions: Array.isArray(existing.extensions) ? [...existing.extensions] : [],
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
return { source: packageSource, extensions: [] };
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function updateExtensionMarkers(
|
|
250
|
+
existingTokens: string[] | undefined,
|
|
251
|
+
changes: ReadonlyMap<string, State>
|
|
252
|
+
): string[] {
|
|
253
|
+
const nextTokens: string[] = [];
|
|
254
|
+
|
|
255
|
+
for (const token of existingTokens ?? []) {
|
|
256
|
+
if (typeof token !== "string") {
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (token[0] !== "+" && token[0] !== "-") {
|
|
261
|
+
nextTokens.push(token);
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const tokenPath = normalizeRelativePath(token.slice(1));
|
|
266
|
+
if (!changes.has(tokenPath)) {
|
|
267
|
+
nextTokens.push(token);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
for (const [extensionPath, target] of Array.from(changes.entries()).sort((a, b) =>
|
|
272
|
+
a[0].localeCompare(b[0])
|
|
273
|
+
)) {
|
|
274
|
+
nextTokens.push(`${target === "enabled" ? "+" : "-"}${extensionPath}`);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
return nextTokens;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
export async function validatePackageExtensionSettings(
|
|
281
|
+
scope: Scope,
|
|
282
|
+
cwd: string
|
|
283
|
+
): Promise<{ ok: true } | { ok: false; error: string }> {
|
|
284
|
+
try {
|
|
285
|
+
await readSettingsFile(getSettingsPath(scope, cwd), { strict: true });
|
|
286
|
+
return { ok: true };
|
|
287
|
+
} catch (error) {
|
|
288
|
+
return {
|
|
289
|
+
ok: false,
|
|
290
|
+
error: error instanceof Error ? error.message : String(error),
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
export async function applyPackageExtensionStateChanges(
|
|
296
|
+
packageSource: string,
|
|
297
|
+
scope: Scope,
|
|
298
|
+
changes: readonly { extensionPath: string; target: State }[],
|
|
299
|
+
cwd: string
|
|
300
|
+
): Promise<{ ok: true } | { ok: false; error: string }> {
|
|
301
|
+
try {
|
|
302
|
+
if (changes.length === 0) {
|
|
303
|
+
return { ok: true };
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
const settingsPath = getSettingsPath(scope, cwd);
|
|
307
|
+
const settings = await readSettingsFile(settingsPath, { strict: true });
|
|
308
|
+
const normalizedSource = normalizeSource(packageSource);
|
|
309
|
+
const packages = [...(settings.packages ?? [])];
|
|
310
|
+
const index = findPackageSettingsIndex(packages, normalizedSource);
|
|
311
|
+
const packageEntry = toPackageSettingsObject(packages[index], packageSource);
|
|
312
|
+
|
|
313
|
+
const normalizedChanges = new Map<string, State>();
|
|
314
|
+
for (const change of changes) {
|
|
315
|
+
normalizedChanges.set(normalizeRelativePath(change.extensionPath), change.target);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
packageEntry.extensions = updateExtensionMarkers(packageEntry.extensions, normalizedChanges);
|
|
319
|
+
|
|
320
|
+
if (index === -1) {
|
|
321
|
+
packages.push(packageEntry);
|
|
322
|
+
} else {
|
|
323
|
+
packages[index] = packageEntry;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
settings.packages = packages;
|
|
327
|
+
await writeSettingsFile(settingsPath, settings);
|
|
328
|
+
|
|
329
|
+
return { ok: true };
|
|
330
|
+
} catch (error) {
|
|
331
|
+
return {
|
|
332
|
+
ok: false,
|
|
333
|
+
error: error instanceof Error ? error.message : String(error),
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
209
338
|
function safeMatchesGlob(targetPath: string, pattern: string): boolean {
|
|
210
339
|
try {
|
|
211
340
|
return matchesGlob(targetPath, pattern);
|
|
@@ -405,20 +534,29 @@ async function resolveManifestExtensionEntries(
|
|
|
405
534
|
return Array.from(selected).sort((a, b) => a.localeCompare(b));
|
|
406
535
|
}
|
|
407
536
|
|
|
408
|
-
export async function
|
|
537
|
+
export async function readPackageManifest(
|
|
409
538
|
packageRoot: string
|
|
410
|
-
): Promise<
|
|
539
|
+
): Promise<PackageManifest | undefined> {
|
|
411
540
|
const packageJsonPath = join(packageRoot, "package.json");
|
|
412
541
|
|
|
413
|
-
let parsed: { pi?: { extensions?: unknown } };
|
|
414
542
|
try {
|
|
415
543
|
const raw = await readFile(packageJsonPath, "utf8");
|
|
416
|
-
parsed = JSON.parse(raw) as
|
|
544
|
+
const parsed = JSON.parse(raw) as unknown;
|
|
545
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
546
|
+
return undefined;
|
|
547
|
+
}
|
|
548
|
+
return parsed as PackageManifest;
|
|
417
549
|
} catch {
|
|
418
550
|
return undefined;
|
|
419
551
|
}
|
|
552
|
+
}
|
|
420
553
|
|
|
421
|
-
|
|
554
|
+
export async function resolveManifestExtensionEntrypoints(
|
|
555
|
+
packageRoot: string,
|
|
556
|
+
manifest?: PackageManifest
|
|
557
|
+
): Promise<string[] | undefined> {
|
|
558
|
+
const parsed = manifest ?? (await readPackageManifest(packageRoot));
|
|
559
|
+
const extensions = parsed?.pi?.extensions;
|
|
422
560
|
if (!Array.isArray(extensions)) {
|
|
423
561
|
return undefined;
|
|
424
562
|
}
|
|
@@ -427,12 +565,35 @@ export async function resolveManifestExtensionEntrypoints(
|
|
|
427
565
|
return resolveManifestExtensionEntries(packageRoot, entries);
|
|
428
566
|
}
|
|
429
567
|
|
|
430
|
-
async function
|
|
431
|
-
const
|
|
568
|
+
async function resolveConventionExtensionEntrypoints(packageRoot: string): Promise<string[]> {
|
|
569
|
+
const extensionsDir = join(packageRoot, "extensions");
|
|
570
|
+
return collectExtensionFilesFromDir(packageRoot, extensionsDir);
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
export async function discoverPackageExtensionEntrypoints(
|
|
574
|
+
packageRoot: string,
|
|
575
|
+
options?: {
|
|
576
|
+
allowConventionDirectory?: boolean;
|
|
577
|
+
allowRootIndexFallback?: boolean;
|
|
578
|
+
}
|
|
579
|
+
): Promise<string[]> {
|
|
580
|
+
const manifest = await readPackageManifest(packageRoot);
|
|
581
|
+
const manifestEntrypoints = await resolveManifestExtensionEntrypoints(packageRoot, manifest);
|
|
432
582
|
if (manifestEntrypoints !== undefined) {
|
|
433
583
|
return manifestEntrypoints;
|
|
434
584
|
}
|
|
435
585
|
|
|
586
|
+
if (options?.allowConventionDirectory !== false) {
|
|
587
|
+
const conventionEntrypoints = await resolveConventionExtensionEntrypoints(packageRoot);
|
|
588
|
+
if (conventionEntrypoints.length > 0) {
|
|
589
|
+
return conventionEntrypoints.sort((a, b) => a.localeCompare(b));
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
if (options?.allowRootIndexFallback === false) {
|
|
594
|
+
return [];
|
|
595
|
+
}
|
|
596
|
+
|
|
436
597
|
const indexTs = join(packageRoot, "index.ts");
|
|
437
598
|
if (await fileExists(indexTs)) {
|
|
438
599
|
return ["index.ts"];
|
|
@@ -456,7 +617,7 @@ export async function discoverPackageExtensions(
|
|
|
456
617
|
const packageRoot = await toPackageRoot(pkg, cwd);
|
|
457
618
|
if (!packageRoot) continue;
|
|
458
619
|
|
|
459
|
-
const extensionPaths = await
|
|
620
|
+
const extensionPaths = await discoverPackageExtensionEntrypoints(packageRoot);
|
|
460
621
|
for (const extensionPath of extensionPaths) {
|
|
461
622
|
const normalizedPath = normalizeRelativePath(extensionPath);
|
|
462
623
|
const absolutePath = resolve(packageRoot, extensionPath);
|
|
@@ -490,58 +651,5 @@ export async function setPackageExtensionState(
|
|
|
490
651
|
target: State,
|
|
491
652
|
cwd: string
|
|
492
653
|
): Promise<{ ok: true } | { ok: false; error: string }> {
|
|
493
|
-
|
|
494
|
-
const settingsPath = getSettingsPath(scope, cwd);
|
|
495
|
-
const settings = await readSettingsFile(settingsPath, { strict: true });
|
|
496
|
-
|
|
497
|
-
const normalizedSource = normalizeSource(packageSource);
|
|
498
|
-
const normalizedPath = normalizeRelativePath(extensionPath);
|
|
499
|
-
const marker = `${target === "enabled" ? "+" : "-"}${normalizedPath}`;
|
|
500
|
-
|
|
501
|
-
const packages = [...(settings.packages ?? [])];
|
|
502
|
-
let index = packages.findIndex((pkg) => {
|
|
503
|
-
if (typeof pkg === "string") {
|
|
504
|
-
return normalizeSource(pkg) === normalizedSource;
|
|
505
|
-
}
|
|
506
|
-
return normalizeSource(pkg.source) === normalizedSource;
|
|
507
|
-
});
|
|
508
|
-
|
|
509
|
-
let packageEntry: PackageSettingsObject;
|
|
510
|
-
if (index === -1) {
|
|
511
|
-
packageEntry = { source: packageSource, extensions: [marker] };
|
|
512
|
-
packages.push(packageEntry);
|
|
513
|
-
index = packages.length - 1;
|
|
514
|
-
} else {
|
|
515
|
-
const existing = packages[index];
|
|
516
|
-
if (typeof existing === "string") {
|
|
517
|
-
packageEntry = { source: existing, extensions: [] };
|
|
518
|
-
} else if (existing && typeof existing.source === "string") {
|
|
519
|
-
packageEntry = {
|
|
520
|
-
source: existing.source,
|
|
521
|
-
extensions: Array.isArray(existing.extensions) ? [...existing.extensions] : [],
|
|
522
|
-
};
|
|
523
|
-
} else {
|
|
524
|
-
packageEntry = { source: packageSource, extensions: [] };
|
|
525
|
-
}
|
|
526
|
-
|
|
527
|
-
packageEntry.extensions = (packageEntry.extensions ?? []).filter((token) => {
|
|
528
|
-
if (typeof token !== "string") return false;
|
|
529
|
-
if (token[0] !== "+" && token[0] !== "-") return true;
|
|
530
|
-
return normalizeRelativePath(token.slice(1)) !== normalizedPath;
|
|
531
|
-
});
|
|
532
|
-
packageEntry.extensions.push(marker);
|
|
533
|
-
packages[index] = packageEntry;
|
|
534
|
-
}
|
|
535
|
-
|
|
536
|
-
settings.packages = packages;
|
|
537
|
-
|
|
538
|
-
await writeSettingsFile(settingsPath, settings);
|
|
539
|
-
|
|
540
|
-
return { ok: true };
|
|
541
|
-
} catch (error) {
|
|
542
|
-
return {
|
|
543
|
-
ok: false,
|
|
544
|
-
error: error instanceof Error ? error.message : String(error),
|
|
545
|
-
};
|
|
546
|
-
}
|
|
654
|
+
return applyPackageExtensionStateChanges(packageSource, scope, [{ extensionPath, target }], cwd);
|
|
547
655
|
}
|