pi-extmgr 0.1.21 → 0.1.23
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 -17
- package/package.json +2 -2
- package/src/index.ts +6 -1
- package/src/packages/discovery.ts +17 -8
- package/src/packages/extensions.ts +252 -31
- package/src/packages/install.ts +10 -23
- package/src/packages/management.ts +60 -70
- package/src/types/index.ts +7 -6
- package/src/ui/footer.ts +3 -6
- package/src/ui/help.ts +1 -0
- package/src/ui/package-config.ts +361 -0
- package/src/ui/remote.ts +2 -2
- package/src/ui/unified.ts +88 -111
- package/src/utils/auto-update.ts +2 -2
- package/src/utils/command.ts +77 -1
- package/src/utils/format.ts +23 -3
- package/src/utils/npm-exec.ts +47 -0
- package/src/utils/package-source.ts +12 -0
package/src/ui/unified.ts
CHANGED
|
@@ -14,22 +14,14 @@ import {
|
|
|
14
14
|
matchesKey,
|
|
15
15
|
Key,
|
|
16
16
|
} from "@mariozechner/pi-tui";
|
|
17
|
-
import type {
|
|
18
|
-
UnifiedItem,
|
|
19
|
-
State,
|
|
20
|
-
UnifiedAction,
|
|
21
|
-
InstalledPackage,
|
|
22
|
-
PackageExtensionEntry,
|
|
23
|
-
} from "../types/index.js";
|
|
17
|
+
import type { UnifiedItem, State, UnifiedAction, InstalledPackage } from "../types/index.js";
|
|
24
18
|
import {
|
|
25
19
|
discoverExtensions,
|
|
26
20
|
removeLocalExtension,
|
|
27
21
|
setExtensionState,
|
|
28
22
|
} from "../extensions/discovery.js";
|
|
29
23
|
import { getInstalledPackages } from "../packages/discovery.js";
|
|
30
|
-
import { discoverPackageExtensions, setPackageExtensionState } from "../packages/extensions.js";
|
|
31
24
|
import {
|
|
32
|
-
showPackageActions,
|
|
33
25
|
updatePackageWithOutcome,
|
|
34
26
|
removePackageWithOutcome,
|
|
35
27
|
updatePackagesWithOutcome,
|
|
@@ -51,6 +43,7 @@ import { updateExtmgrStatus } from "../utils/status.js";
|
|
|
51
43
|
import { parseChoiceByLabel } from "../utils/command.js";
|
|
52
44
|
import { getPackageSourceKind } from "../utils/package-source.js";
|
|
53
45
|
import { UI } from "../constants.js";
|
|
46
|
+
import { configurePackageExtensions } from "./package-config.js";
|
|
54
47
|
|
|
55
48
|
// Type guard for SettingsList with selectedIndex
|
|
56
49
|
interface SelectableList {
|
|
@@ -87,16 +80,15 @@ async function showInteractiveOnce(
|
|
|
87
80
|
ctx: ExtensionCommandContext,
|
|
88
81
|
pi: ExtensionAPI
|
|
89
82
|
): Promise<boolean> {
|
|
90
|
-
// Load local extensions and installed packages
|
|
83
|
+
// Load local extensions and installed packages.
|
|
91
84
|
const [localEntries, installedPackages] = await Promise.all([
|
|
92
85
|
discoverExtensions(ctx.cwd),
|
|
93
86
|
getInstalledPackages(ctx, pi),
|
|
94
87
|
]);
|
|
95
|
-
const packageExtensions = await discoverPackageExtensions(installedPackages, ctx.cwd);
|
|
96
88
|
|
|
97
|
-
// Build unified items list
|
|
89
|
+
// Build unified items list.
|
|
98
90
|
const knownUpdates = getKnownUpdates(ctx);
|
|
99
|
-
const items = buildUnifiedItems(localEntries, installedPackages,
|
|
91
|
+
const items = buildUnifiedItems(localEntries, installedPackages, knownUpdates);
|
|
100
92
|
|
|
101
93
|
// If nothing found, show quick actions
|
|
102
94
|
if (items.length === 0) {
|
|
@@ -112,7 +104,7 @@ async function showInteractiveOnce(
|
|
|
112
104
|
return true;
|
|
113
105
|
}
|
|
114
106
|
|
|
115
|
-
// Staged changes tracking for
|
|
107
|
+
// Staged changes tracking for local extensions.
|
|
116
108
|
const staged = new Map<string, State>();
|
|
117
109
|
const byId = new Map(items.map((item) => [item.id, item]));
|
|
118
110
|
|
|
@@ -126,7 +118,7 @@ async function showInteractiveOnce(
|
|
|
126
118
|
new Text(
|
|
127
119
|
theme.fg(
|
|
128
120
|
"muted",
|
|
129
|
-
`${items.length} item${items.length === 1 ? "" : "s"} • Space/Enter toggle
|
|
121
|
+
`${items.length} item${items.length === 1 ? "" : "s"} • Space/Enter toggle local • Enter/A actions • c configure pkg extensions • u update pkg • x remove selected`
|
|
130
122
|
),
|
|
131
123
|
2,
|
|
132
124
|
0
|
|
@@ -150,7 +142,7 @@ async function showInteractiveOnce(
|
|
|
150
142
|
getSettingsListTheme(),
|
|
151
143
|
(id: string, newValue: string) => {
|
|
152
144
|
const item = byId.get(id);
|
|
153
|
-
if (!item ||
|
|
145
|
+
if (!item || item.type !== "local") return;
|
|
154
146
|
|
|
155
147
|
const state = newValue as State;
|
|
156
148
|
staged.set(id, state);
|
|
@@ -236,6 +228,10 @@ async function showInteractiveOnce(
|
|
|
236
228
|
done({ type: "action", itemId: selectedId, action: "details" });
|
|
237
229
|
return;
|
|
238
230
|
}
|
|
231
|
+
if (data === "c" || data === "C") {
|
|
232
|
+
done({ type: "action", itemId: selectedId, action: "configure" });
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
239
235
|
}
|
|
240
236
|
|
|
241
237
|
if (selectedId && selectedItem?.type === "local") {
|
|
@@ -266,35 +262,25 @@ async function showInteractiveOnce(
|
|
|
266
262
|
return await handleUnifiedAction(result, items, staged, byId, ctx, pi);
|
|
267
263
|
}
|
|
268
264
|
|
|
265
|
+
function normalizePathForDuplicateCheck(value: string): string {
|
|
266
|
+
const normalized = value.replace(/\\/g, "/");
|
|
267
|
+
const looksWindowsPath =
|
|
268
|
+
/^[a-zA-Z]:\//.test(normalized) || normalized.startsWith("//") || value.includes("\\");
|
|
269
|
+
|
|
270
|
+
return looksWindowsPath ? normalized.toLowerCase() : normalized;
|
|
271
|
+
}
|
|
272
|
+
|
|
269
273
|
export function buildUnifiedItems(
|
|
270
274
|
localEntries: Awaited<ReturnType<typeof discoverExtensions>>,
|
|
271
275
|
installedPackages: InstalledPackage[],
|
|
272
|
-
packageExtensions: PackageExtensionEntry[],
|
|
273
276
|
knownUpdates: Set<string>
|
|
274
277
|
): UnifiedItem[] {
|
|
275
278
|
const items: UnifiedItem[] = [];
|
|
276
279
|
const localPaths = new Set<string>();
|
|
277
280
|
|
|
278
|
-
const packageExtensionGroups = new Map<string, PackageExtensionEntry[]>();
|
|
279
|
-
for (const entry of packageExtensions) {
|
|
280
|
-
const key = `${entry.packageScope}:${entry.packageSource.toLowerCase()}`;
|
|
281
|
-
const group = packageExtensionGroups.get(key) ?? [];
|
|
282
|
-
group.push(entry);
|
|
283
|
-
packageExtensionGroups.set(key, group);
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
const visiblePackageExtensions = packageExtensions.filter((entry) => {
|
|
287
|
-
const key = `${entry.packageScope}:${entry.packageSource.toLowerCase()}`;
|
|
288
|
-
const group = packageExtensionGroups.get(key) ?? [];
|
|
289
|
-
|
|
290
|
-
// Avoid duplicate-looking rows for packages that expose a single enabled extension entrypoint.
|
|
291
|
-
// Keep extension rows when there are multiple entrypoints, or when an entry is disabled so it can be re-enabled.
|
|
292
|
-
return group.length > 1 || entry.state === "disabled";
|
|
293
|
-
});
|
|
294
|
-
|
|
295
281
|
// Add local extensions
|
|
296
282
|
for (const entry of localEntries) {
|
|
297
|
-
localPaths.add(entry.activePath
|
|
283
|
+
localPaths.add(normalizePathForDuplicateCheck(entry.activePath));
|
|
298
284
|
items.push({
|
|
299
285
|
type: "local",
|
|
300
286
|
id: entry.id,
|
|
@@ -308,39 +294,28 @@ export function buildUnifiedItems(
|
|
|
308
294
|
});
|
|
309
295
|
}
|
|
310
296
|
|
|
311
|
-
for (const entry of visiblePackageExtensions) {
|
|
312
|
-
items.push({
|
|
313
|
-
type: "package-extension",
|
|
314
|
-
id: entry.id,
|
|
315
|
-
displayName: entry.displayName,
|
|
316
|
-
summary: entry.summary,
|
|
317
|
-
scope: entry.packageScope,
|
|
318
|
-
state: entry.state,
|
|
319
|
-
originalState: entry.state,
|
|
320
|
-
packageSource: entry.packageSource,
|
|
321
|
-
extensionPath: entry.extensionPath,
|
|
322
|
-
});
|
|
323
|
-
}
|
|
324
|
-
|
|
325
297
|
for (const pkg of installedPackages) {
|
|
326
|
-
const
|
|
327
|
-
const
|
|
298
|
+
const pkgSourceNormalized = normalizePathForDuplicateCheck(pkg.source);
|
|
299
|
+
const pkgResolvedNormalized = pkg.resolvedPath
|
|
300
|
+
? normalizePathForDuplicateCheck(pkg.resolvedPath)
|
|
301
|
+
: "";
|
|
328
302
|
|
|
329
303
|
let isDuplicate = false;
|
|
330
304
|
for (const localPath of localPaths) {
|
|
331
|
-
if (
|
|
305
|
+
if (pkgSourceNormalized === localPath || pkgResolvedNormalized === localPath) {
|
|
332
306
|
isDuplicate = true;
|
|
333
307
|
break;
|
|
334
308
|
}
|
|
335
309
|
if (
|
|
336
|
-
|
|
337
|
-
(localPath.startsWith(
|
|
310
|
+
pkgResolvedNormalized &&
|
|
311
|
+
(localPath.startsWith(`${pkgResolvedNormalized}/`) ||
|
|
312
|
+
pkgResolvedNormalized.startsWith(localPath))
|
|
338
313
|
) {
|
|
339
314
|
isDuplicate = true;
|
|
340
315
|
break;
|
|
341
316
|
}
|
|
342
|
-
const localDir = localPath.
|
|
343
|
-
if (
|
|
317
|
+
const localDir = localPath.split("/").slice(0, -1).join("/");
|
|
318
|
+
if (pkgResolvedNormalized && pkgResolvedNormalized === localDir) {
|
|
344
319
|
isDuplicate = true;
|
|
345
320
|
break;
|
|
346
321
|
}
|
|
@@ -365,8 +340,7 @@ export function buildUnifiedItems(
|
|
|
365
340
|
items.sort((a, b) => {
|
|
366
341
|
const rank = (type: UnifiedItem["type"]): number => {
|
|
367
342
|
if (type === "local") return 0;
|
|
368
|
-
|
|
369
|
-
return 2;
|
|
343
|
+
return 1;
|
|
370
344
|
};
|
|
371
345
|
|
|
372
346
|
const diff = rank(a.type) - rank(b.type);
|
|
@@ -383,7 +357,7 @@ function buildSettingsItems(
|
|
|
383
357
|
theme: Theme
|
|
384
358
|
): SettingItem[] {
|
|
385
359
|
return items.map((item) => {
|
|
386
|
-
if (item.type === "local"
|
|
360
|
+
if (item.type === "local") {
|
|
387
361
|
const currentState = staged.get(item.id) ?? item.state!;
|
|
388
362
|
const changed = staged.has(item.id) && staged.get(item.id) !== item.originalState;
|
|
389
363
|
return {
|
|
@@ -411,34 +385,19 @@ function formatUnifiedItemLabel(
|
|
|
411
385
|
): string {
|
|
412
386
|
if (item.type === "local") {
|
|
413
387
|
const statusIcon = getStatusIcon(theme, state === "enabled" ? "enabled" : "disabled");
|
|
414
|
-
const scopeIcon = getScopeIcon(theme, item.scope
|
|
388
|
+
const scopeIcon = getScopeIcon(theme, item.scope);
|
|
415
389
|
const changeMarker = getChangeMarker(theme, changed);
|
|
416
390
|
const name = theme.bold(item.displayName);
|
|
417
391
|
const summary = theme.fg("dim", item.summary);
|
|
418
392
|
return `${statusIcon} [${scopeIcon}] ${name} - ${summary}${changeMarker}`;
|
|
419
393
|
}
|
|
420
394
|
|
|
421
|
-
if (item.type === "package-extension") {
|
|
422
|
-
const statusIcon = getStatusIcon(theme, state === "enabled" ? "enabled" : "disabled");
|
|
423
|
-
const scopeIcon = getScopeIcon(theme, item.scope as "global" | "project");
|
|
424
|
-
const sourceKind = getPackageSourceKind(item.packageSource ?? "");
|
|
425
|
-
const pkgIcon = getPackageIcon(
|
|
426
|
-
theme,
|
|
427
|
-
sourceKind === "npm" || sourceKind === "git" || sourceKind === "local" ? sourceKind : "local"
|
|
428
|
-
);
|
|
429
|
-
const sourceLabel = sourceKind === "unknown" ? "package" : `${sourceKind} package`;
|
|
430
|
-
const changeMarker = getChangeMarker(theme, changed);
|
|
431
|
-
const name = theme.bold(item.displayName);
|
|
432
|
-
const summary = theme.fg("dim", `${item.summary} • ${sourceLabel}`);
|
|
433
|
-
return `${statusIcon} ${pkgIcon} [${scopeIcon}] ${name} - ${summary}${changeMarker}`;
|
|
434
|
-
}
|
|
435
|
-
|
|
436
395
|
const sourceKind = getPackageSourceKind(item.source ?? "");
|
|
437
396
|
const pkgIcon = getPackageIcon(
|
|
438
397
|
theme,
|
|
439
398
|
sourceKind === "npm" || sourceKind === "git" || sourceKind === "local" ? sourceKind : "local"
|
|
440
399
|
);
|
|
441
|
-
const scopeIcon = getScopeIcon(theme, item.scope
|
|
400
|
+
const scopeIcon = getScopeIcon(theme, item.scope);
|
|
442
401
|
const name = theme.bold(item.displayName);
|
|
443
402
|
const version = item.version ? theme.fg("dim", `@${item.version}`) : "";
|
|
444
403
|
const updateBadge = item.updateAvailable ? ` ${theme.fg("warning", "[update]")}` : "";
|
|
@@ -468,7 +427,7 @@ function formatUnifiedItemLabel(
|
|
|
468
427
|
}
|
|
469
428
|
|
|
470
429
|
function getToggleItemsForApply(items: UnifiedItem[]): UnifiedItem[] {
|
|
471
|
-
return items.filter((item) => item.type === "local"
|
|
430
|
+
return items.filter((item) => item.type === "local");
|
|
472
431
|
}
|
|
473
432
|
|
|
474
433
|
async function applyToggleChangesFromManager(
|
|
@@ -479,7 +438,7 @@ async function applyToggleChangesFromManager(
|
|
|
479
438
|
options?: { promptReload?: boolean }
|
|
480
439
|
): Promise<{ changed: number; reloaded: boolean }> {
|
|
481
440
|
const toggleItems = getToggleItemsForApply(items);
|
|
482
|
-
const apply = await applyStagedChanges(toggleItems, staged, pi
|
|
441
|
+
const apply = await applyStagedChanges(toggleItems, staged, pi);
|
|
483
442
|
|
|
484
443
|
if (apply.errors.length > 0) {
|
|
485
444
|
ctx.ui.notify(
|
|
@@ -489,7 +448,7 @@ async function applyToggleChangesFromManager(
|
|
|
489
448
|
} else if (apply.changed === 0) {
|
|
490
449
|
ctx.ui.notify("No changes to apply.", "info");
|
|
491
450
|
} else {
|
|
492
|
-
ctx.ui.notify(`Applied ${apply.changed} extension change(s).`, "info");
|
|
451
|
+
ctx.ui.notify(`Applied ${apply.changed} local extension change(s).`, "info");
|
|
493
452
|
}
|
|
494
453
|
|
|
495
454
|
if (apply.changed > 0) {
|
|
@@ -498,7 +457,7 @@ async function applyToggleChangesFromManager(
|
|
|
498
457
|
if (shouldPromptReload) {
|
|
499
458
|
const shouldReload = await ctx.ui.confirm(
|
|
500
459
|
"Reload Required",
|
|
501
|
-
"
|
|
460
|
+
"Local extensions changed. Reload pi now?"
|
|
502
461
|
);
|
|
503
462
|
|
|
504
463
|
if (shouldReload) {
|
|
@@ -570,6 +529,34 @@ const QUICK_DESTINATION_LABELS: Record<QuickDestination, string> = {
|
|
|
570
529
|
help: "Help",
|
|
571
530
|
};
|
|
572
531
|
|
|
532
|
+
const PACKAGE_ACTION_OPTIONS = {
|
|
533
|
+
configure: "Configure extensions",
|
|
534
|
+
update: "Update package",
|
|
535
|
+
remove: "Remove package",
|
|
536
|
+
details: "View details",
|
|
537
|
+
back: "Back to manager",
|
|
538
|
+
} as const;
|
|
539
|
+
|
|
540
|
+
type PackageActionKey = keyof typeof PACKAGE_ACTION_OPTIONS;
|
|
541
|
+
|
|
542
|
+
type PackageActionSelection = Exclude<PackageActionKey, "back"> | "cancel";
|
|
543
|
+
|
|
544
|
+
async function promptPackageActionSelection(
|
|
545
|
+
pkg: InstalledPackage,
|
|
546
|
+
ctx: ExtensionCommandContext
|
|
547
|
+
): Promise<PackageActionSelection> {
|
|
548
|
+
const selection = parseChoiceByLabel(
|
|
549
|
+
PACKAGE_ACTION_OPTIONS,
|
|
550
|
+
await ctx.ui.select(pkg.name, Object.values(PACKAGE_ACTION_OPTIONS))
|
|
551
|
+
);
|
|
552
|
+
|
|
553
|
+
if (!selection || selection === "back") {
|
|
554
|
+
return "cancel";
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
return selection;
|
|
558
|
+
}
|
|
559
|
+
|
|
573
560
|
async function navigateWithPendingGuard(
|
|
574
561
|
destination: QuickDestination,
|
|
575
562
|
items: UnifiedItem[],
|
|
@@ -754,20 +741,29 @@ async function handleUnifiedAction(
|
|
|
754
741
|
return false;
|
|
755
742
|
}
|
|
756
743
|
|
|
757
|
-
if (item.type === "package-extension") {
|
|
758
|
-
return false;
|
|
759
|
-
}
|
|
760
|
-
|
|
761
744
|
const pkg: InstalledPackage = {
|
|
762
745
|
source: item.source!,
|
|
763
746
|
name: item.displayName,
|
|
764
747
|
...(item.version ? { version: item.version } : {}),
|
|
765
|
-
scope: item.scope
|
|
748
|
+
scope: item.scope,
|
|
766
749
|
...(item.description ? { description: item.description } : {}),
|
|
767
750
|
...(item.size !== undefined ? { size: item.size } : {}),
|
|
768
751
|
};
|
|
769
752
|
|
|
770
|
-
|
|
753
|
+
const selection =
|
|
754
|
+
!result.action || result.action === "menu"
|
|
755
|
+
? await promptPackageActionSelection(pkg, ctx)
|
|
756
|
+
: result.action;
|
|
757
|
+
|
|
758
|
+
if (selection === "cancel") {
|
|
759
|
+
return false;
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
switch (selection) {
|
|
763
|
+
case "configure": {
|
|
764
|
+
const outcome = await configurePackageExtensions(pkg, ctx, pi);
|
|
765
|
+
return outcome.reloaded;
|
|
766
|
+
}
|
|
771
767
|
case "update": {
|
|
772
768
|
const outcome = await updatePackageWithOutcome(pkg.source, ctx, pi);
|
|
773
769
|
return outcome.reloaded;
|
|
@@ -784,11 +780,6 @@ async function handleUnifiedAction(
|
|
|
784
780
|
);
|
|
785
781
|
return false;
|
|
786
782
|
}
|
|
787
|
-
case "menu":
|
|
788
|
-
default: {
|
|
789
|
-
const exitManager = await showPackageActions(pkg, ctx, pi);
|
|
790
|
-
return exitManager;
|
|
791
|
-
}
|
|
792
783
|
}
|
|
793
784
|
}
|
|
794
785
|
|
|
@@ -799,37 +790,23 @@ async function handleUnifiedAction(
|
|
|
799
790
|
async function applyStagedChanges(
|
|
800
791
|
items: UnifiedItem[],
|
|
801
792
|
staged: Map<string, State>,
|
|
802
|
-
pi: ExtensionAPI
|
|
803
|
-
cwd: string
|
|
793
|
+
pi: ExtensionAPI
|
|
804
794
|
) {
|
|
805
795
|
let changed = 0;
|
|
806
796
|
const errors: string[] = [];
|
|
807
797
|
|
|
808
798
|
for (const item of items) {
|
|
809
|
-
if (
|
|
799
|
+
if (item.type !== "local" || !item.originalState || !item.activePath || !item.disabledPath) {
|
|
810
800
|
continue;
|
|
811
801
|
}
|
|
812
802
|
|
|
813
803
|
const target = staged.get(item.id) ?? item.originalState;
|
|
814
804
|
if (target === item.originalState) continue;
|
|
815
805
|
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
{ activePath: item.activePath, disabledPath: item.disabledPath },
|
|
821
|
-
target
|
|
822
|
-
);
|
|
823
|
-
} else {
|
|
824
|
-
if (!item.packageSource || !item.extensionPath) continue;
|
|
825
|
-
result = await setPackageExtensionState(
|
|
826
|
-
item.packageSource,
|
|
827
|
-
item.extensionPath,
|
|
828
|
-
item.scope as "global" | "project",
|
|
829
|
-
target,
|
|
830
|
-
cwd
|
|
831
|
-
);
|
|
832
|
-
}
|
|
806
|
+
const result = await setExtensionState(
|
|
807
|
+
{ activePath: item.activePath, disabledPath: item.disabledPath },
|
|
808
|
+
target
|
|
809
|
+
);
|
|
833
810
|
|
|
834
811
|
if (result.ok) {
|
|
835
812
|
changed++;
|
package/src/utils/auto-update.ts
CHANGED
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
type AutoUpdateConfig,
|
|
19
19
|
} from "./settings.js";
|
|
20
20
|
import { parseNpmSource } from "./format.js";
|
|
21
|
+
import { execNpm } from "./npm-exec.js";
|
|
21
22
|
import { TIMEOUTS } from "../constants.js";
|
|
22
23
|
|
|
23
24
|
import { startTimer, stopTimer, isTimerRunning } from "./timer.js";
|
|
@@ -134,9 +135,8 @@ async function checkPackageUpdate(
|
|
|
134
135
|
if (!pkgName) return false;
|
|
135
136
|
|
|
136
137
|
try {
|
|
137
|
-
const res = await pi
|
|
138
|
+
const res = await execNpm(pi, ["view", pkgName, "version", "--json"], ctx, {
|
|
138
139
|
timeout: TIMEOUTS.npmView,
|
|
139
|
-
cwd: ctx.cwd,
|
|
140
140
|
});
|
|
141
141
|
|
|
142
142
|
if (res.code !== 0) return false;
|
package/src/utils/command.ts
CHANGED
|
@@ -3,7 +3,83 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
export function tokenizeArgs(input: string): string[] {
|
|
6
|
-
|
|
6
|
+
const tokens: string[] = [];
|
|
7
|
+
let current = "";
|
|
8
|
+
let inSingleQuote = false;
|
|
9
|
+
let inDoubleQuote = false;
|
|
10
|
+
let tokenStarted = false;
|
|
11
|
+
|
|
12
|
+
const pushCurrent = () => {
|
|
13
|
+
if (tokenStarted) {
|
|
14
|
+
tokens.push(current);
|
|
15
|
+
current = "";
|
|
16
|
+
tokenStarted = false;
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
for (let i = 0; i < input.length; i++) {
|
|
21
|
+
const char = input[i]!;
|
|
22
|
+
const next = input[i + 1];
|
|
23
|
+
|
|
24
|
+
if (inSingleQuote) {
|
|
25
|
+
if (char === "'") {
|
|
26
|
+
inSingleQuote = false;
|
|
27
|
+
} else {
|
|
28
|
+
current += char;
|
|
29
|
+
}
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (inDoubleQuote) {
|
|
34
|
+
if (char === '"') {
|
|
35
|
+
inDoubleQuote = false;
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (char === "\\" && next === '"') {
|
|
40
|
+
current += next;
|
|
41
|
+
i++;
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
current += char;
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (/\s/.test(char)) {
|
|
50
|
+
pushCurrent();
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (char === "'") {
|
|
55
|
+
inSingleQuote = true;
|
|
56
|
+
tokenStarted = true;
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (char === '"') {
|
|
61
|
+
inDoubleQuote = true;
|
|
62
|
+
tokenStarted = true;
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (char === "\\" && (next === '"' || next === "'" || /\s/.test(next ?? ""))) {
|
|
67
|
+
tokenStarted = true;
|
|
68
|
+
if (next) {
|
|
69
|
+
current += next;
|
|
70
|
+
i++;
|
|
71
|
+
} else {
|
|
72
|
+
current += char;
|
|
73
|
+
}
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
tokenStarted = true;
|
|
78
|
+
current += char;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
pushCurrent();
|
|
82
|
+
return tokens;
|
|
7
83
|
}
|
|
8
84
|
|
|
9
85
|
export function splitCommandArgs(input: string): { subcommand: string; args: string[] } {
|
package/src/utils/format.ts
CHANGED
|
@@ -59,6 +59,9 @@ export function formatBytes(bytes: number): string {
|
|
|
59
59
|
|
|
60
60
|
const GIT_PATTERNS = {
|
|
61
61
|
gitPrefix: /^git:/,
|
|
62
|
+
gitPlusHttpPrefix: /^git\+https?:\/\//,
|
|
63
|
+
gitPlusSshPrefix: /^git\+ssh:\/\//,
|
|
64
|
+
gitPlusGitPrefix: /^git\+git:\/\//,
|
|
62
65
|
httpPrefix: /^https?:\/\//,
|
|
63
66
|
sshPrefix: /^ssh:\/\//,
|
|
64
67
|
gitProtoPrefix: /^git:\/\//,
|
|
@@ -78,6 +81,9 @@ const LOCAL_PATH_PATTERNS = {
|
|
|
78
81
|
function isGitLikeSource(source: string): boolean {
|
|
79
82
|
return (
|
|
80
83
|
GIT_PATTERNS.gitPrefix.test(source) ||
|
|
84
|
+
GIT_PATTERNS.gitPlusHttpPrefix.test(source) ||
|
|
85
|
+
GIT_PATTERNS.gitPlusSshPrefix.test(source) ||
|
|
86
|
+
GIT_PATTERNS.gitPlusGitPrefix.test(source) ||
|
|
81
87
|
GIT_PATTERNS.httpPrefix.test(source) ||
|
|
82
88
|
GIT_PATTERNS.sshPrefix.test(source) ||
|
|
83
89
|
GIT_PATTERNS.gitProtoPrefix.test(source) ||
|
|
@@ -97,22 +103,36 @@ function isLocalPathSource(source: string): boolean {
|
|
|
97
103
|
);
|
|
98
104
|
}
|
|
99
105
|
|
|
106
|
+
function unwrapQuotedSource(source: string): string {
|
|
107
|
+
const trimmed = source.trim();
|
|
108
|
+
if (trimmed.length < 2) return trimmed;
|
|
109
|
+
|
|
110
|
+
const first = trimmed[0];
|
|
111
|
+
const last = trimmed[trimmed.length - 1];
|
|
112
|
+
|
|
113
|
+
if ((first === '"' && last === '"') || (first === "'" && last === "'")) {
|
|
114
|
+
return trimmed.slice(1, -1).trim();
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return trimmed;
|
|
118
|
+
}
|
|
119
|
+
|
|
100
120
|
export function isPackageSource(str: string): boolean {
|
|
101
|
-
const source = str
|
|
121
|
+
const source = unwrapQuotedSource(str);
|
|
102
122
|
if (!source) return false;
|
|
103
123
|
|
|
104
124
|
return source.startsWith("npm:") || isGitLikeSource(source) || isLocalPathSource(source);
|
|
105
125
|
}
|
|
106
126
|
|
|
107
127
|
export function normalizePackageSource(source: string): string {
|
|
108
|
-
const trimmed = source
|
|
128
|
+
const trimmed = unwrapQuotedSource(source);
|
|
109
129
|
if (!trimmed) return trimmed;
|
|
110
130
|
|
|
111
131
|
if (GIT_PATTERNS.gitSsh.test(trimmed)) {
|
|
112
132
|
return `git:${trimmed}`;
|
|
113
133
|
}
|
|
114
134
|
|
|
115
|
-
if (
|
|
135
|
+
if (trimmed.startsWith("npm:") || isGitLikeSource(trimmed) || isLocalPathSource(trimmed)) {
|
|
116
136
|
return trimmed;
|
|
117
137
|
}
|
|
118
138
|
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { execPath, platform } from "node:process";
|
|
3
|
+
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
4
|
+
|
|
5
|
+
interface NpmCommandResolutionOptions {
|
|
6
|
+
platform?: NodeJS.Platform;
|
|
7
|
+
nodeExecPath?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface NpmExecOptions {
|
|
11
|
+
timeout: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function getNpmCliPath(nodeExecPath: string, runtimePlatform: NodeJS.Platform): string {
|
|
15
|
+
const pathImpl = runtimePlatform === "win32" ? path.win32 : path;
|
|
16
|
+
return pathImpl.join(pathImpl.dirname(nodeExecPath), "node_modules", "npm", "bin", "npm-cli.js");
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function resolveNpmCommand(
|
|
20
|
+
npmArgs: string[],
|
|
21
|
+
options?: NpmCommandResolutionOptions
|
|
22
|
+
): { command: string; args: string[] } {
|
|
23
|
+
const runtimePlatform = options?.platform ?? platform;
|
|
24
|
+
|
|
25
|
+
if (runtimePlatform === "win32") {
|
|
26
|
+
const nodeBinary = options?.nodeExecPath ?? execPath;
|
|
27
|
+
return {
|
|
28
|
+
command: nodeBinary,
|
|
29
|
+
args: [getNpmCliPath(nodeBinary, runtimePlatform), ...npmArgs],
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return { command: "npm", args: npmArgs };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export async function execNpm(
|
|
37
|
+
pi: ExtensionAPI,
|
|
38
|
+
npmArgs: string[],
|
|
39
|
+
ctx: { cwd: string },
|
|
40
|
+
options: NpmExecOptions
|
|
41
|
+
): Promise<{ code: number; stdout: string; stderr: string; killed: boolean }> {
|
|
42
|
+
const resolved = resolveNpmCommand(npmArgs);
|
|
43
|
+
return pi.exec(resolved.command, resolved.args, {
|
|
44
|
+
timeout: options.timeout,
|
|
45
|
+
cwd: ctx.cwd,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
@@ -18,6 +18,10 @@ export function getPackageSourceKind(source: string): PackageSourceKind {
|
|
|
18
18
|
|
|
19
19
|
if (
|
|
20
20
|
normalized.startsWith("git:") ||
|
|
21
|
+
normalized.startsWith("git+http://") ||
|
|
22
|
+
normalized.startsWith("git+https://") ||
|
|
23
|
+
normalized.startsWith("git+ssh://") ||
|
|
24
|
+
normalized.startsWith("git+git://") ||
|
|
21
25
|
normalized.startsWith("http://") ||
|
|
22
26
|
normalized.startsWith("https://") ||
|
|
23
27
|
normalized.startsWith("ssh://") ||
|
|
@@ -43,6 +47,14 @@ export function getPackageSourceKind(source: string): PackageSourceKind {
|
|
|
43
47
|
return "unknown";
|
|
44
48
|
}
|
|
45
49
|
|
|
50
|
+
export function normalizeLocalSourceIdentity(source: string): string {
|
|
51
|
+
const normalized = source.replace(/\\/g, "/");
|
|
52
|
+
const looksWindowsPath =
|
|
53
|
+
/^[a-zA-Z]:\//.test(normalized) || normalized.startsWith("//") || source.includes("\\");
|
|
54
|
+
|
|
55
|
+
return looksWindowsPath ? normalized.toLowerCase() : normalized;
|
|
56
|
+
}
|
|
57
|
+
|
|
46
58
|
export function splitGitRepoAndRef(gitSpec: string): { repo: string; ref?: string | undefined } {
|
|
47
59
|
const lastAt = gitSpec.lastIndexOf("@");
|
|
48
60
|
if (lastAt <= 0) {
|