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/src/packages/install.ts
CHANGED
|
@@ -8,7 +8,7 @@ import type { ExtensionAPI, ExtensionCommandContext } from "@mariozechner/pi-cod
|
|
|
8
8
|
import { normalizePackageSource } from "../utils/format.js";
|
|
9
9
|
import { fileExists } from "../utils/fs.js";
|
|
10
10
|
import { clearSearchCache, isSourceInstalled } from "./discovery.js";
|
|
11
|
-
import {
|
|
11
|
+
import { discoverPackageExtensionEntrypoints, readPackageManifest } from "./extensions.js";
|
|
12
12
|
import { waitForCondition } from "../utils/retry.js";
|
|
13
13
|
import { logPackageInstall } from "../utils/history.js";
|
|
14
14
|
import { clearUpdatesAvailable } from "../utils/settings.js";
|
|
@@ -17,6 +17,7 @@ import { confirmAction, confirmReload, showProgress } from "../utils/ui-helpers.
|
|
|
17
17
|
import { tryOperation } from "../utils/mode.js";
|
|
18
18
|
import { updateExtmgrStatus } from "../utils/status.js";
|
|
19
19
|
import { execNpm } from "../utils/npm-exec.js";
|
|
20
|
+
import { normalizePackageIdentity } from "../utils/package-source.js";
|
|
20
21
|
import { TIMEOUTS } from "../constants.js";
|
|
21
22
|
|
|
22
23
|
export type InstallScope = "global" | "project";
|
|
@@ -72,20 +73,93 @@ function safeExtractGithubMatch(match: RegExpMatchArray | null): GithubUrlInfo |
|
|
|
72
73
|
return { owner, repo, branch, filePath };
|
|
73
74
|
}
|
|
74
75
|
|
|
76
|
+
async function fetchWithTimeout(url: string, timeoutMs: number): Promise<Response> {
|
|
77
|
+
const controller = new AbortController();
|
|
78
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
return await fetch(url, { signal: controller.signal });
|
|
82
|
+
} catch (error) {
|
|
83
|
+
if (error instanceof Error && error.name === "AbortError") {
|
|
84
|
+
throw new Error(`Download timed out after ${Math.ceil(timeoutMs / 1000)}s`);
|
|
85
|
+
}
|
|
86
|
+
throw error;
|
|
87
|
+
} finally {
|
|
88
|
+
clearTimeout(timer);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async function ensureTarAvailable(
|
|
93
|
+
pi: ExtensionAPI,
|
|
94
|
+
ctx: ExtensionCommandContext
|
|
95
|
+
): Promise<{ ok: true } | { ok: false; error: string }> {
|
|
96
|
+
const result = await pi.exec("tar", ["--version"], {
|
|
97
|
+
timeout: 5_000,
|
|
98
|
+
cwd: ctx.cwd,
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
if (result.code === 0) {
|
|
102
|
+
return { ok: true };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return {
|
|
106
|
+
ok: false,
|
|
107
|
+
error:
|
|
108
|
+
"Standalone local installs require the `tar` command on PATH. Install tar or use managed package install instead.",
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
75
112
|
async function hasStandaloneEntrypoint(packageRoot: string): Promise<boolean> {
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
113
|
+
const entrypoints = await discoverPackageExtensionEntrypoints(packageRoot, {
|
|
114
|
+
allowConventionDirectory: false,
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
for (const path of entrypoints) {
|
|
118
|
+
if (await fileExists(join(packageRoot, path))) {
|
|
119
|
+
return true;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async function getStandaloneDependencyError(packageRoot: string): Promise<string | undefined> {
|
|
127
|
+
const manifest = await readPackageManifest(packageRoot);
|
|
128
|
+
const dependencies = manifest?.dependencies;
|
|
129
|
+
if (!dependencies || typeof dependencies !== "object") {
|
|
130
|
+
return undefined;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const missingDependencies: string[] = [];
|
|
134
|
+
for (const dependencyName of Object.keys(dependencies)) {
|
|
135
|
+
const dependencyPath = join(packageRoot, "node_modules", dependencyName);
|
|
136
|
+
if (!(await fileExists(dependencyPath))) {
|
|
137
|
+
missingDependencies.push(dependencyName);
|
|
82
138
|
}
|
|
83
|
-
return false;
|
|
84
139
|
}
|
|
85
140
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
141
|
+
if (missingDependencies.length === 0) {
|
|
142
|
+
return undefined;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const packageName = manifest?.name ?? "This package";
|
|
146
|
+
return `${packageName} declares runtime dependencies that are not bundled for standalone install: ${missingDependencies.join(", ")}. Use managed install instead, or bundle dependencies in the package tarball.`;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async function cleanupStandaloneTempArtifacts(tempDir: string, extractDir?: string): Promise<void> {
|
|
150
|
+
const paths = [extractDir, tempDir].filter((path): path is string => Boolean(path));
|
|
151
|
+
|
|
152
|
+
await Promise.allSettled(
|
|
153
|
+
paths.map(async (path) => {
|
|
154
|
+
try {
|
|
155
|
+
await rm(path, { recursive: true, force: true });
|
|
156
|
+
} catch (error) {
|
|
157
|
+
console.warn(
|
|
158
|
+
`[extmgr] Failed to remove temporary standalone install artifact at ${path}:`,
|
|
159
|
+
error
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
})
|
|
89
163
|
);
|
|
90
164
|
}
|
|
91
165
|
|
|
@@ -150,7 +224,7 @@ export async function installPackage(
|
|
|
150
224
|
clearSearchCache();
|
|
151
225
|
logPackageInstall(pi, normalized, normalized, undefined, scope, true);
|
|
152
226
|
success(ctx, `Installed ${normalized} (${scope})`);
|
|
153
|
-
clearUpdatesAvailable(pi, ctx);
|
|
227
|
+
clearUpdatesAvailable(pi, ctx, [normalizePackageIdentity(normalized)]);
|
|
154
228
|
|
|
155
229
|
// Wait for the extension to be discoverable before reloading.
|
|
156
230
|
// This prevents a race condition where ctx.reload() runs before
|
|
@@ -208,7 +282,7 @@ export async function installFromUrl(
|
|
|
208
282
|
await mkdir(extensionDir, { recursive: true });
|
|
209
283
|
notify(ctx, `Downloading ${fileName}...`, "info");
|
|
210
284
|
|
|
211
|
-
const response = await
|
|
285
|
+
const response = await fetchWithTimeout(url, TIMEOUTS.packageInstall);
|
|
212
286
|
if (!response.ok) {
|
|
213
287
|
throw new Error(`Download failed: ${response.status} ${response.statusText}`);
|
|
214
288
|
}
|
|
@@ -231,7 +305,6 @@ export async function installFromUrl(
|
|
|
231
305
|
const { fileName: name, destPath } = result;
|
|
232
306
|
logPackageInstall(pi, url, name, undefined, scope, true);
|
|
233
307
|
success(ctx, `Installed ${name} to:\n${destPath}`);
|
|
234
|
-
clearUpdatesAvailable(pi, ctx);
|
|
235
308
|
|
|
236
309
|
const reloaded = await confirmReload(ctx, "Extension installed.");
|
|
237
310
|
if (!reloaded) {
|
|
@@ -325,17 +398,33 @@ export async function installPackageLocally(
|
|
|
325
398
|
}
|
|
326
399
|
const { version, tarballUrl } = result;
|
|
327
400
|
|
|
401
|
+
const tarAvailability = await ensureTarAvailable(pi, ctx);
|
|
402
|
+
if (!tarAvailability.ok) {
|
|
403
|
+
notifyError(ctx, tarAvailability.error);
|
|
404
|
+
logPackageInstall(
|
|
405
|
+
pi,
|
|
406
|
+
`npm:${packageName}`,
|
|
407
|
+
packageName,
|
|
408
|
+
version,
|
|
409
|
+
scope,
|
|
410
|
+
false,
|
|
411
|
+
tarAvailability.error
|
|
412
|
+
);
|
|
413
|
+
void updateExtmgrStatus(ctx, pi);
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
|
|
328
417
|
// Download and extract
|
|
418
|
+
const tempDir = join(extensionDir, ".temp");
|
|
329
419
|
const extractResult = await tryOperation(
|
|
330
420
|
ctx,
|
|
331
421
|
async () => {
|
|
332
|
-
const tempDir = join(extensionDir, ".temp");
|
|
333
422
|
await mkdir(tempDir, { recursive: true });
|
|
334
423
|
const tarballPath = join(tempDir, `${packageName.replace(/[@/]/g, "-")}-${version}.tgz`);
|
|
335
424
|
|
|
336
425
|
showProgress(ctx, "Downloading", `${packageName}@${version}`);
|
|
337
426
|
|
|
338
|
-
const response = await
|
|
427
|
+
const response = await fetchWithTimeout(tarballUrl, TIMEOUTS.packageInstall);
|
|
339
428
|
if (!response.ok) {
|
|
340
429
|
throw new Error(`Download failed: ${response.status} ${response.statusText}`);
|
|
341
430
|
}
|
|
@@ -343,12 +432,13 @@ export async function installPackageLocally(
|
|
|
343
432
|
const buffer = await response.arrayBuffer();
|
|
344
433
|
await writeFile(tarballPath, new Uint8Array(buffer));
|
|
345
434
|
|
|
346
|
-
return { tarballPath
|
|
435
|
+
return { tarballPath };
|
|
347
436
|
},
|
|
348
437
|
"Download failed"
|
|
349
438
|
);
|
|
350
439
|
|
|
351
440
|
if (!extractResult) {
|
|
441
|
+
await cleanupStandaloneTempArtifacts(tempDir);
|
|
352
442
|
logPackageInstall(
|
|
353
443
|
pi,
|
|
354
444
|
`npm:${packageName}`,
|
|
@@ -361,7 +451,7 @@ export async function installPackageLocally(
|
|
|
361
451
|
void updateExtmgrStatus(ctx, pi);
|
|
362
452
|
return;
|
|
363
453
|
}
|
|
364
|
-
const { tarballPath
|
|
454
|
+
const { tarballPath } = extractResult;
|
|
365
455
|
|
|
366
456
|
// Extract
|
|
367
457
|
const extractDir = join(
|
|
@@ -390,17 +480,22 @@ export async function installPackageLocally(
|
|
|
390
480
|
const hasEntrypoint = await hasStandaloneEntrypoint(extractDir);
|
|
391
481
|
if (!hasEntrypoint) {
|
|
392
482
|
throw new Error(
|
|
393
|
-
`Package ${packageName} does not contain a runnable extension entrypoint (
|
|
483
|
+
`Package ${packageName} does not contain a runnable standalone extension entrypoint (manifest-declared entrypoint, index.ts, or index.js)`
|
|
394
484
|
);
|
|
395
485
|
}
|
|
396
486
|
|
|
487
|
+
const dependencyError = await getStandaloneDependencyError(extractDir);
|
|
488
|
+
if (dependencyError) {
|
|
489
|
+
throw new Error(dependencyError);
|
|
490
|
+
}
|
|
491
|
+
|
|
397
492
|
return true;
|
|
398
493
|
},
|
|
399
494
|
"Extraction failed"
|
|
400
495
|
);
|
|
401
496
|
|
|
402
497
|
if (!extractSuccess) {
|
|
403
|
-
await
|
|
498
|
+
await cleanupStandaloneTempArtifacts(tempDir, extractDir);
|
|
404
499
|
logPackageInstall(
|
|
405
500
|
pi,
|
|
406
501
|
`npm:${packageName}`,
|
|
@@ -429,7 +524,7 @@ export async function installPackageLocally(
|
|
|
429
524
|
"Failed to copy extension"
|
|
430
525
|
);
|
|
431
526
|
|
|
432
|
-
await
|
|
527
|
+
await cleanupStandaloneTempArtifacts(tempDir, extractDir);
|
|
433
528
|
|
|
434
529
|
if (!destResult) {
|
|
435
530
|
logPackageInstall(
|
|
@@ -448,7 +543,6 @@ export async function installPackageLocally(
|
|
|
448
543
|
clearSearchCache();
|
|
449
544
|
logPackageInstall(pi, `npm:${packageName}`, packageName, version, scope, true);
|
|
450
545
|
success(ctx, `Installed ${packageName}@${version} locally to:\n${destResult}`);
|
|
451
|
-
clearUpdatesAvailable(pi, ctx);
|
|
452
546
|
|
|
453
547
|
const reloaded = await confirmReload(ctx, "Extension installed.");
|
|
454
548
|
if (!reloaded) {
|
|
@@ -10,12 +10,8 @@ import {
|
|
|
10
10
|
isSourceInstalled,
|
|
11
11
|
} from "./discovery.js";
|
|
12
12
|
import { waitForCondition } from "../utils/retry.js";
|
|
13
|
-
import { formatInstalledPackageLabel
|
|
14
|
-
import {
|
|
15
|
-
getPackageSourceKind,
|
|
16
|
-
normalizeLocalSourceIdentity,
|
|
17
|
-
splitGitRepoAndRef,
|
|
18
|
-
} from "../utils/package-source.js";
|
|
13
|
+
import { formatInstalledPackageLabel } from "../utils/format.js";
|
|
14
|
+
import { normalizePackageIdentity } from "../utils/package-source.js";
|
|
19
15
|
import { logPackageUpdate, logPackageRemove } from "../utils/history.js";
|
|
20
16
|
import { clearUpdatesAvailable } from "../utils/settings.js";
|
|
21
17
|
import { notify, error as notifyError, success } from "../utils/notify.js";
|
|
@@ -70,18 +66,19 @@ async function updatePackageInternal(
|
|
|
70
66
|
return NO_PACKAGE_MUTATION_OUTCOME;
|
|
71
67
|
}
|
|
72
68
|
|
|
69
|
+
const updateIdentity = normalizePackageIdentity(source);
|
|
73
70
|
const stdout = res.stdout || "";
|
|
74
71
|
if (isUpToDateOutput(stdout)) {
|
|
75
72
|
notify(ctx, `${source} is already up to date (or pinned).`, "info");
|
|
76
73
|
logPackageUpdate(pi, source, source, undefined, true);
|
|
77
|
-
clearUpdatesAvailable(pi, ctx);
|
|
74
|
+
clearUpdatesAvailable(pi, ctx, [updateIdentity]);
|
|
78
75
|
void updateExtmgrStatus(ctx, pi);
|
|
79
76
|
return NO_PACKAGE_MUTATION_OUTCOME;
|
|
80
77
|
}
|
|
81
78
|
|
|
82
79
|
logPackageUpdate(pi, source, source, undefined, true);
|
|
83
80
|
success(ctx, `Updated ${source}`);
|
|
84
|
-
clearUpdatesAvailable(pi, ctx);
|
|
81
|
+
clearUpdatesAvailable(pi, ctx, [updateIdentity]);
|
|
85
82
|
|
|
86
83
|
const reloaded = await confirmReload(ctx, "Package updated.");
|
|
87
84
|
if (!reloaded) {
|
|
@@ -156,29 +153,8 @@ export async function updatePackagesWithOutcome(
|
|
|
156
153
|
return updatePackagesInternal(ctx, pi);
|
|
157
154
|
}
|
|
158
155
|
|
|
159
|
-
function packageIdentity(source: string
|
|
160
|
-
|
|
161
|
-
if (npm?.name) {
|
|
162
|
-
return `npm:${npm.name}`;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
const sourceKind = getPackageSourceKind(source);
|
|
166
|
-
|
|
167
|
-
if (sourceKind === "git") {
|
|
168
|
-
const gitSpec = source.startsWith("git:") ? source.slice(4) : source;
|
|
169
|
-
const { repo } = splitGitRepoAndRef(gitSpec);
|
|
170
|
-
return `git:${repo}`;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
if (sourceKind === "local") {
|
|
174
|
-
return `src:${normalizeLocalSourceIdentity(source)}`;
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
if (fallbackName) {
|
|
178
|
-
return `name:${fallbackName}`;
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
return `src:${source}`;
|
|
156
|
+
function packageIdentity(source: string): string {
|
|
157
|
+
return normalizePackageIdentity(source);
|
|
182
158
|
}
|
|
183
159
|
|
|
184
160
|
async function getInstalledPackagesAllScopes(
|
|
@@ -325,9 +301,8 @@ async function removePackageInternal(
|
|
|
325
301
|
pi: ExtensionAPI
|
|
326
302
|
): Promise<PackageMutationOutcome> {
|
|
327
303
|
const installed = await getInstalledPackagesAllScopes(ctx, pi);
|
|
328
|
-
const
|
|
329
|
-
const
|
|
330
|
-
const matching = installed.filter((p) => packageIdentity(p.source, p.name) === identity);
|
|
304
|
+
const identity = packageIdentity(source);
|
|
305
|
+
const matching = installed.filter((p) => packageIdentity(p.source) === identity);
|
|
331
306
|
|
|
332
307
|
const hasBothScopes =
|
|
333
308
|
matching.some((pkg) => pkg.scope === "global") &&
|
|
@@ -369,12 +344,12 @@ async function removePackageInternal(
|
|
|
369
344
|
.map((result) => result.target);
|
|
370
345
|
|
|
371
346
|
const remaining = (await getInstalledPackagesAllScopes(ctx, pi)).filter(
|
|
372
|
-
(p) => packageIdentity(p.source
|
|
347
|
+
(p) => packageIdentity(p.source) === identity
|
|
373
348
|
);
|
|
374
349
|
notifyRemovalSummary(source, remaining, failures, ctx);
|
|
375
350
|
|
|
376
|
-
if (failures.length === 0) {
|
|
377
|
-
clearUpdatesAvailable(pi, ctx);
|
|
351
|
+
if (failures.length === 0 && remaining.length === 0) {
|
|
352
|
+
clearUpdatesAvailable(pi, ctx, [identity]);
|
|
378
353
|
}
|
|
379
354
|
|
|
380
355
|
const successfulRemovalCount = successfulTargets.length;
|
package/src/ui/package-config.ts
CHANGED
|
@@ -13,19 +13,20 @@ import {
|
|
|
13
13
|
type SettingItem,
|
|
14
14
|
} from "@mariozechner/pi-tui";
|
|
15
15
|
import type { InstalledPackage, PackageExtensionEntry, State } from "../types/index.js";
|
|
16
|
-
import {
|
|
16
|
+
import {
|
|
17
|
+
applyPackageExtensionStateChanges,
|
|
18
|
+
discoverPackageExtensions,
|
|
19
|
+
validatePackageExtensionSettings,
|
|
20
|
+
} from "../packages/extensions.js";
|
|
17
21
|
import { notify } from "../utils/notify.js";
|
|
18
22
|
import { logExtensionToggle } from "../utils/history.js";
|
|
23
|
+
import { requireCustomUI, runCustomUI } from "../utils/mode.js";
|
|
19
24
|
import { getPackageSourceKind } from "../utils/package-source.js";
|
|
25
|
+
import { getSettingsListSelectedIndex } from "../utils/settings-list.js";
|
|
20
26
|
import { fileExists } from "../utils/fs.js";
|
|
21
27
|
import { UI } from "../constants.js";
|
|
22
28
|
import { getChangeMarker, getPackageIcon, getScopeIcon, getStatusIcon } from "./theme.js";
|
|
23
29
|
|
|
24
|
-
interface SelectableList {
|
|
25
|
-
selectedIndex?: number;
|
|
26
|
-
handleInput?(data: string): void;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
30
|
export interface PackageConfigRow {
|
|
30
31
|
id: string;
|
|
31
32
|
extensionPath: string;
|
|
@@ -36,16 +37,6 @@ export interface PackageConfigRow {
|
|
|
36
37
|
|
|
37
38
|
type ConfigurePanelAction = { type: "cancel" } | { type: "save" };
|
|
38
39
|
|
|
39
|
-
function getSelectedIndex(settingsList: unknown): number | undefined {
|
|
40
|
-
if (settingsList && typeof settingsList === "object") {
|
|
41
|
-
const selectable = settingsList as SelectableList;
|
|
42
|
-
if (typeof selectable.selectedIndex === "number") {
|
|
43
|
-
return selectable.selectedIndex;
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
return undefined;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
40
|
export async function buildPackageConfigRows(
|
|
50
41
|
entries: PackageExtensionEntry[]
|
|
51
42
|
): Promise<PackageConfigRow[]> {
|
|
@@ -130,99 +121,116 @@ async function showConfigurePanel(
|
|
|
130
121
|
rows: PackageConfigRow[],
|
|
131
122
|
staged: Map<string, State>,
|
|
132
123
|
ctx: ExtensionCommandContext
|
|
133
|
-
): Promise<ConfigurePanelAction> {
|
|
134
|
-
return ctx
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
new Text(
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
if (!row || !row.available) return;
|
|
163
|
-
|
|
164
|
-
const state = newValue as State;
|
|
165
|
-
staged.set(id, state);
|
|
166
|
-
|
|
167
|
-
const settingsItem = settingsItems.find((item) => item.id === id);
|
|
168
|
-
if (settingsItem) {
|
|
124
|
+
): Promise<ConfigurePanelAction | undefined> {
|
|
125
|
+
return runCustomUI(ctx, "Package extension configuration", () =>
|
|
126
|
+
ctx.ui.custom<ConfigurePanelAction>((tui, theme, _keybindings, done) => {
|
|
127
|
+
const container = new Container();
|
|
128
|
+
const titleText = new Text("", 2, 0);
|
|
129
|
+
const subtitleText = new Text("", 2, 0);
|
|
130
|
+
const footerText = new Text("", 2, 0);
|
|
131
|
+
|
|
132
|
+
container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s)));
|
|
133
|
+
container.addChild(titleText);
|
|
134
|
+
container.addChild(subtitleText);
|
|
135
|
+
container.addChild(new Spacer(1));
|
|
136
|
+
|
|
137
|
+
const settingsItems = buildSettingItems(rows, staged, pkg, theme);
|
|
138
|
+
const rowById = new Map(rows.map((row) => [row.id, row]));
|
|
139
|
+
const syncThemedContent = (): void => {
|
|
140
|
+
titleText.setText(theme.fg("accent", theme.bold(`Configure extensions: ${pkg.name}`)));
|
|
141
|
+
subtitleText.setText(
|
|
142
|
+
theme.fg(
|
|
143
|
+
"muted",
|
|
144
|
+
`${rows.length} extension path${rows.length === 1 ? "" : "s"} • Space/Enter toggle • S save • Esc cancel`
|
|
145
|
+
)
|
|
146
|
+
);
|
|
147
|
+
footerText.setText(theme.fg("dim", "↑↓ Navigate | Space/Enter Toggle | S Save | Esc Back"));
|
|
148
|
+
|
|
149
|
+
for (const settingsItem of settingsItems) {
|
|
150
|
+
const row = rowById.get(settingsItem.id);
|
|
151
|
+
if (!row) continue;
|
|
152
|
+
const currentState = staged.get(row.id) ?? row.originalState;
|
|
169
153
|
settingsItem.label = formatConfigRowLabel(
|
|
170
154
|
row,
|
|
171
|
-
|
|
155
|
+
currentState,
|
|
172
156
|
pkg,
|
|
173
157
|
theme,
|
|
174
|
-
|
|
175
|
-
);
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
tui.requestRender();
|
|
179
|
-
},
|
|
180
|
-
() => done({ type: "cancel" }),
|
|
181
|
-
{ enableSearch: rows.length > UI.searchThreshold }
|
|
182
|
-
);
|
|
183
|
-
|
|
184
|
-
container.addChild(settingsList);
|
|
185
|
-
container.addChild(new Spacer(1));
|
|
186
|
-
container.addChild(
|
|
187
|
-
new Text(theme.fg("dim", "↑↓ Navigate | Space/Enter Toggle | S Save | Esc Back"), 2, 0)
|
|
188
|
-
);
|
|
189
|
-
container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s)));
|
|
190
|
-
|
|
191
|
-
return {
|
|
192
|
-
render(width: number) {
|
|
193
|
-
return container.render(width);
|
|
194
|
-
},
|
|
195
|
-
invalidate() {
|
|
196
|
-
container.invalidate();
|
|
197
|
-
},
|
|
198
|
-
handleInput(data: string) {
|
|
199
|
-
if (matchesKey(data, Key.ctrl("s")) || data === "s" || data === "S") {
|
|
200
|
-
done({ type: "save" });
|
|
201
|
-
return;
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
const selectedIndex = getSelectedIndex(settingsList) ?? 0;
|
|
205
|
-
const selectedId = settingsItems[selectedIndex]?.id ?? settingsItems[0]?.id;
|
|
206
|
-
const selectedRow = selectedId ? rowById.get(selectedId) : undefined;
|
|
207
|
-
|
|
208
|
-
if (
|
|
209
|
-
selectedRow &&
|
|
210
|
-
!selectedRow.available &&
|
|
211
|
-
(data === " " || data === "\r" || data === "\n")
|
|
212
|
-
) {
|
|
213
|
-
notify(
|
|
214
|
-
ctx,
|
|
215
|
-
`${selectedRow.extensionPath} is missing on disk and cannot be toggled.`,
|
|
216
|
-
"warning"
|
|
158
|
+
currentState !== row.originalState
|
|
217
159
|
);
|
|
218
|
-
return;
|
|
219
160
|
}
|
|
161
|
+
};
|
|
162
|
+
syncThemedContent();
|
|
163
|
+
|
|
164
|
+
const settingsList = new SettingsList(
|
|
165
|
+
settingsItems,
|
|
166
|
+
Math.min(rows.length + 2, UI.maxListHeight),
|
|
167
|
+
getSettingsListTheme(),
|
|
168
|
+
(id: string, newValue: string) => {
|
|
169
|
+
const row = rowById.get(id);
|
|
170
|
+
if (!row || !row.available) return;
|
|
171
|
+
|
|
172
|
+
const state = newValue as State;
|
|
173
|
+
staged.set(id, state);
|
|
174
|
+
|
|
175
|
+
const settingsItem = settingsItems.find((item) => item.id === id);
|
|
176
|
+
if (settingsItem) {
|
|
177
|
+
settingsItem.label = formatConfigRowLabel(
|
|
178
|
+
row,
|
|
179
|
+
state,
|
|
180
|
+
pkg,
|
|
181
|
+
theme,
|
|
182
|
+
state !== row.originalState
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
tui.requestRender();
|
|
187
|
+
},
|
|
188
|
+
() => done({ type: "cancel" }),
|
|
189
|
+
{ enableSearch: rows.length > UI.searchThreshold }
|
|
190
|
+
);
|
|
220
191
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
192
|
+
container.addChild(settingsList);
|
|
193
|
+
container.addChild(new Spacer(1));
|
|
194
|
+
container.addChild(footerText);
|
|
195
|
+
container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s)));
|
|
196
|
+
|
|
197
|
+
return {
|
|
198
|
+
render(width: number) {
|
|
199
|
+
return container.render(width);
|
|
200
|
+
},
|
|
201
|
+
invalidate() {
|
|
202
|
+
container.invalidate();
|
|
203
|
+
syncThemedContent();
|
|
204
|
+
},
|
|
205
|
+
handleInput(data: string) {
|
|
206
|
+
if (matchesKey(data, Key.ctrl("s")) || data === "s" || data === "S") {
|
|
207
|
+
done({ type: "save" });
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const selectedIndex = getSettingsListSelectedIndex(settingsList) ?? 0;
|
|
212
|
+
const selectedId = settingsItems[selectedIndex]?.id ?? settingsItems[0]?.id;
|
|
213
|
+
const selectedRow = selectedId ? rowById.get(selectedId) : undefined;
|
|
214
|
+
|
|
215
|
+
if (
|
|
216
|
+
selectedRow &&
|
|
217
|
+
!selectedRow.available &&
|
|
218
|
+
(data === " " || data === "\r" || data === "\n")
|
|
219
|
+
) {
|
|
220
|
+
notify(
|
|
221
|
+
ctx,
|
|
222
|
+
`${selectedRow.extensionPath} is missing on disk and cannot be toggled.`,
|
|
223
|
+
"warning"
|
|
224
|
+
);
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
settingsList.handleInput?.(data);
|
|
229
|
+
tui.requestRender();
|
|
230
|
+
},
|
|
231
|
+
};
|
|
232
|
+
})
|
|
233
|
+
);
|
|
226
234
|
}
|
|
227
235
|
|
|
228
236
|
export async function applyPackageExtensionChanges(
|
|
@@ -232,40 +240,50 @@ export async function applyPackageExtensionChanges(
|
|
|
232
240
|
cwd: string,
|
|
233
241
|
pi: ExtensionAPI
|
|
234
242
|
): Promise<{ changed: number; errors: string[] }> {
|
|
235
|
-
let changed = 0;
|
|
236
243
|
const errors: string[] = [];
|
|
244
|
+
const changedRows = [...rows]
|
|
245
|
+
.sort((a, b) => a.extensionPath.localeCompare(b.extensionPath))
|
|
246
|
+
.flatMap((row) => {
|
|
247
|
+
const target = staged.get(row.id) ?? row.originalState;
|
|
248
|
+
if (target === row.originalState) {
|
|
249
|
+
return [];
|
|
250
|
+
}
|
|
237
251
|
|
|
238
|
-
|
|
252
|
+
if (!row.available) {
|
|
253
|
+
const error = `${row.extensionPath}: extension entrypoint is missing on disk`;
|
|
254
|
+
errors.push(error);
|
|
255
|
+
logExtensionToggle(pi, row.id, row.originalState, target, false, error);
|
|
256
|
+
return [];
|
|
257
|
+
}
|
|
239
258
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
if (target === row.originalState) continue;
|
|
259
|
+
return [{ row, target }];
|
|
260
|
+
});
|
|
243
261
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
logExtensionToggle(pi, row.id, row.originalState, target, false, error);
|
|
248
|
-
continue;
|
|
249
|
-
}
|
|
262
|
+
if (changedRows.length === 0) {
|
|
263
|
+
return { changed: 0, errors };
|
|
264
|
+
}
|
|
250
265
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
);
|
|
266
|
+
const result = await applyPackageExtensionStateChanges(
|
|
267
|
+
pkg.source,
|
|
268
|
+
pkg.scope,
|
|
269
|
+
changedRows.map(({ row, target }) => ({ extensionPath: row.extensionPath, target })),
|
|
270
|
+
cwd
|
|
271
|
+
);
|
|
258
272
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
errors.push(`${row.extensionPath}: ${result.error}`);
|
|
273
|
+
if (!result.ok) {
|
|
274
|
+
for (const { row, target } of changedRows) {
|
|
275
|
+
const error = `${row.extensionPath}: ${result.error}`;
|
|
276
|
+
errors.push(error);
|
|
264
277
|
logExtensionToggle(pi, row.id, row.originalState, target, false, result.error);
|
|
265
278
|
}
|
|
279
|
+
return { changed: 0, errors };
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
for (const { row, target } of changedRows) {
|
|
283
|
+
logExtensionToggle(pi, row.id, row.originalState, target, true);
|
|
266
284
|
}
|
|
267
285
|
|
|
268
|
-
return { changed, errors };
|
|
286
|
+
return { changed: changedRows.length, errors };
|
|
269
287
|
}
|
|
270
288
|
|
|
271
289
|
async function promptRestartForPackageConfig(ctx: ExtensionCommandContext): Promise<boolean> {
|
|
@@ -302,6 +320,16 @@ export async function configurePackageExtensions(
|
|
|
302
320
|
ctx: ExtensionCommandContext,
|
|
303
321
|
pi: ExtensionAPI
|
|
304
322
|
): Promise<{ changed: number; reloaded: boolean }> {
|
|
323
|
+
if (!requireCustomUI(ctx, "Package extension configuration")) {
|
|
324
|
+
return { changed: 0, reloaded: false };
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
const validation = await validatePackageExtensionSettings(pkg.scope, ctx.cwd);
|
|
328
|
+
if (!validation.ok) {
|
|
329
|
+
notify(ctx, validation.error, "error");
|
|
330
|
+
return { changed: 0, reloaded: false };
|
|
331
|
+
}
|
|
332
|
+
|
|
305
333
|
const discovered = await discoverPackageExtensions([pkg], ctx.cwd);
|
|
306
334
|
const rows = await buildPackageConfigRows(discovered);
|
|
307
335
|
|
|
@@ -314,6 +342,9 @@ export async function configurePackageExtensions(
|
|
|
314
342
|
|
|
315
343
|
while (true) {
|
|
316
344
|
const action = await showConfigurePanel(pkg, rows, staged, ctx);
|
|
345
|
+
if (!action) {
|
|
346
|
+
return { changed: 0, reloaded: false };
|
|
347
|
+
}
|
|
317
348
|
|
|
318
349
|
if (action.type === "cancel") {
|
|
319
350
|
const pending = getPendingChangeCount(rows, staged);
|