pi-extmgr 0.1.23 → 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/packages/discovery.ts +45 -29
- package/src/packages/extensions.ts +171 -63
- package/src/packages/install.ts +116 -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/history.ts +41 -2
- package/src/utils/mode.ts +56 -5
- 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,8 +14,9 @@ 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";
|
|
21
22
|
|
|
@@ -121,15 +122,11 @@ function sanitizeListSourceSuffix(source: string): string {
|
|
|
121
122
|
.trim();
|
|
122
123
|
}
|
|
123
124
|
|
|
124
|
-
function
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
return normalizeLocalSourceIdentity(sanitized);
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
return sanitized.replace(/\\/g, "/").toLowerCase();
|
|
125
|
+
function getInstalledPackageIdentity(pkg: InstalledPackage): string {
|
|
126
|
+
return normalizePackageIdentity(
|
|
127
|
+
pkg.source,
|
|
128
|
+
pkg.resolvedPath ? { resolvedPath: pkg.resolvedPath } : undefined
|
|
129
|
+
);
|
|
133
130
|
}
|
|
134
131
|
|
|
135
132
|
function isScopeHeader(lowerTrimmed: string, scope: "global" | "project"): boolean {
|
|
@@ -181,12 +178,8 @@ function parseResolvedPathLine(line: string): string | undefined {
|
|
|
181
178
|
return undefined;
|
|
182
179
|
}
|
|
183
180
|
|
|
184
|
-
function parseInstalledPackagesOutputInternal(
|
|
185
|
-
text: string,
|
|
186
|
-
options?: { dedupeBySource?: boolean }
|
|
187
|
-
): InstalledPackage[] {
|
|
181
|
+
function parseInstalledPackagesOutputInternal(text: string): InstalledPackage[] {
|
|
188
182
|
const packages: InstalledPackage[] = [];
|
|
189
|
-
const seenSources = new Set<string>();
|
|
190
183
|
|
|
191
184
|
const lines = text.split("\n");
|
|
192
185
|
let currentScope: "global" | "project" = "global";
|
|
@@ -222,15 +215,6 @@ function parseInstalledPackagesOutputInternal(
|
|
|
222
215
|
if (!looksLikePackageSource(candidate)) continue;
|
|
223
216
|
|
|
224
217
|
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
218
|
const { name, version } = parsePackageNameAndVersion(source);
|
|
235
219
|
|
|
236
220
|
const pkg: InstalledPackage = { source, name, scope: currentScope };
|
|
@@ -244,8 +228,34 @@ function parseInstalledPackagesOutputInternal(
|
|
|
244
228
|
return packages;
|
|
245
229
|
}
|
|
246
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
|
+
|
|
247
246
|
export function parseInstalledPackagesOutput(text: string): InstalledPackage[] {
|
|
248
|
-
|
|
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());
|
|
249
259
|
}
|
|
250
260
|
|
|
251
261
|
/**
|
|
@@ -263,10 +273,10 @@ export async function isSourceInstalled(
|
|
|
263
273
|
if (res.code !== 0) return false;
|
|
264
274
|
|
|
265
275
|
const installed = parseInstalledPackagesOutputAllScopes(res.stdout || "");
|
|
266
|
-
const expected =
|
|
276
|
+
const expected = normalizePackageIdentity(source);
|
|
267
277
|
|
|
268
278
|
return installed.some((pkg) => {
|
|
269
|
-
if (
|
|
279
|
+
if (getInstalledPackageIdentity(pkg) !== expected) {
|
|
270
280
|
return false;
|
|
271
281
|
}
|
|
272
282
|
return options?.scope ? pkg.scope === options.scope : true;
|
|
@@ -276,8 +286,14 @@ export async function isSourceInstalled(
|
|
|
276
286
|
}
|
|
277
287
|
}
|
|
278
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
|
+
*/
|
|
279
295
|
export function parseInstalledPackagesOutputAllScopes(text: string): InstalledPackage[] {
|
|
280
|
-
return parseInstalledPackagesOutputInternal(text
|
|
296
|
+
return parseInstalledPackagesOutputInternal(text);
|
|
281
297
|
}
|
|
282
298
|
|
|
283
299
|
function extractGitPackageName(repoSpec: string): string {
|
|
@@ -316,7 +332,7 @@ function parsePackageNameAndVersion(fullSource: string): {
|
|
|
316
332
|
|
|
317
333
|
const sourceKind = getPackageSourceKind(fullSource);
|
|
318
334
|
if (sourceKind === "git") {
|
|
319
|
-
const gitSpec =
|
|
335
|
+
const gitSpec = stripGitSourcePrefix(fullSource);
|
|
320
336
|
const { repo } = splitGitRepoAndRef(gitSpec);
|
|
321
337
|
return { name: extractGitPackageName(repo) };
|
|
322
338
|
}
|
|
@@ -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
|
}
|