pi-extmgr 0.1.22 → 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/package.json +1 -1
- package/src/index.ts +6 -1
- package/src/packages/discovery.ts +17 -8
- package/src/packages/install.ts +2 -2
- package/src/packages/management.ts +59 -13
- package/src/ui/remote.ts +2 -2
- package/src/ui/unified.ts +5 -1
- 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/package.json
CHANGED
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,12 @@ 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
|
+
normalizeLocalSourceIdentity,
|
|
18
|
+
splitGitRepoAndRef,
|
|
19
|
+
} from "../utils/package-source.js";
|
|
20
|
+
import { execNpm } from "../utils/npm-exec.js";
|
|
16
21
|
|
|
17
22
|
let searchCache: SearchCache | null = null;
|
|
18
23
|
|
|
@@ -67,9 +72,8 @@ export async function searchNpmPackages(
|
|
|
67
72
|
ctx.ui.notify(`Searching npm for "${query}"...`, "info");
|
|
68
73
|
}
|
|
69
74
|
|
|
70
|
-
const res = await pi
|
|
75
|
+
const res = await execNpm(pi, ["search", "--json", `--searchlimit=${searchLimit}`, query], ctx, {
|
|
71
76
|
timeout: TIMEOUTS.npmSearch,
|
|
72
|
-
cwd: ctx.cwd,
|
|
73
77
|
});
|
|
74
78
|
|
|
75
79
|
if (res.code !== 0) {
|
|
@@ -118,7 +122,14 @@ function sanitizeListSourceSuffix(source: string): string {
|
|
|
118
122
|
}
|
|
119
123
|
|
|
120
124
|
function normalizeSourceIdentity(source: string): string {
|
|
121
|
-
|
|
125
|
+
const sanitized = sanitizeListSourceSuffix(source);
|
|
126
|
+
const kind = getPackageSourceKind(sanitized);
|
|
127
|
+
|
|
128
|
+
if (kind === "local") {
|
|
129
|
+
return normalizeLocalSourceIdentity(sanitized);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return sanitized.replace(/\\/g, "/").toLowerCase();
|
|
122
133
|
}
|
|
123
134
|
|
|
124
135
|
function isScopeHeader(lowerTrimmed: string, scope: "global" | "project"): boolean {
|
|
@@ -375,9 +386,8 @@ async function fetchPackageSize(
|
|
|
375
386
|
|
|
376
387
|
try {
|
|
377
388
|
// Try to get unpacked size from npm view
|
|
378
|
-
const res = await pi
|
|
389
|
+
const res = await execNpm(pi, ["view", pkgName, "dist.unpackedSize", "--json"], ctx, {
|
|
379
390
|
timeout: TIMEOUTS.npmView,
|
|
380
|
-
cwd: ctx.cwd,
|
|
381
391
|
});
|
|
382
392
|
if (res.code === 0) {
|
|
383
393
|
try {
|
|
@@ -441,9 +451,8 @@ async function addPackageMetadata(
|
|
|
441
451
|
if (cached?.description) {
|
|
442
452
|
pkg.description = cached.description;
|
|
443
453
|
} else {
|
|
444
|
-
const res = await pi
|
|
454
|
+
const res = await execNpm(pi, ["view", pkgName, "description", "--json"], ctx, {
|
|
445
455
|
timeout: TIMEOUTS.npmView,
|
|
446
|
-
cwd: ctx.cwd,
|
|
447
456
|
});
|
|
448
457
|
if (res.code === 0) {
|
|
449
458
|
try {
|
package/src/packages/install.ts
CHANGED
|
@@ -16,6 +16,7 @@ import { notify, error as notifyError, success } from "../utils/notify.js";
|
|
|
16
16
|
import { confirmAction, confirmReload, showProgress } from "../utils/ui-helpers.js";
|
|
17
17
|
import { tryOperation } from "../utils/mode.js";
|
|
18
18
|
import { updateExtmgrStatus } from "../utils/status.js";
|
|
19
|
+
import { execNpm } from "../utils/npm-exec.js";
|
|
19
20
|
import { TIMEOUTS } from "../constants.js";
|
|
20
21
|
|
|
21
22
|
export type InstallScope = "global" | "project";
|
|
@@ -291,9 +292,8 @@ export async function installPackageLocally(
|
|
|
291
292
|
await mkdir(extensionDir, { recursive: true });
|
|
292
293
|
showProgress(ctx, "Fetching", packageName);
|
|
293
294
|
|
|
294
|
-
const viewRes = await pi
|
|
295
|
+
const viewRes = await execNpm(pi, ["view", packageName, "--json"], ctx, {
|
|
295
296
|
timeout: TIMEOUTS.fetchPackageInfo,
|
|
296
|
-
cwd: ctx.cwd,
|
|
297
297
|
});
|
|
298
298
|
|
|
299
299
|
if (viewRes.code !== 0) {
|
|
@@ -11,7 +11,11 @@ import {
|
|
|
11
11
|
} from "./discovery.js";
|
|
12
12
|
import { waitForCondition } from "../utils/retry.js";
|
|
13
13
|
import { formatInstalledPackageLabel, parseNpmSource } from "../utils/format.js";
|
|
14
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
getPackageSourceKind,
|
|
16
|
+
normalizeLocalSourceIdentity,
|
|
17
|
+
splitGitRepoAndRef,
|
|
18
|
+
} from "../utils/package-source.js";
|
|
15
19
|
import { logPackageUpdate, logPackageRemove } from "../utils/history.js";
|
|
16
20
|
import { clearUpdatesAvailable } from "../utils/settings.js";
|
|
17
21
|
import { notify, error as notifyError, success } from "../utils/notify.js";
|
|
@@ -33,12 +37,19 @@ const NO_PACKAGE_MUTATION_OUTCOME: PackageMutationOutcome = {
|
|
|
33
37
|
reloaded: false,
|
|
34
38
|
};
|
|
35
39
|
|
|
40
|
+
const BULK_UPDATE_LABEL = "all packages";
|
|
41
|
+
|
|
36
42
|
function packageMutationOutcome(
|
|
37
43
|
overrides: Partial<PackageMutationOutcome>
|
|
38
44
|
): PackageMutationOutcome {
|
|
39
45
|
return { ...NO_PACKAGE_MUTATION_OUTCOME, ...overrides };
|
|
40
46
|
}
|
|
41
47
|
|
|
48
|
+
function isUpToDateOutput(stdout: string): boolean {
|
|
49
|
+
const pinnedAsStatus = /^\s*pinned\b(?!\s+dependency\b)(?:\s*$|\s*[:(-])/im.test(stdout);
|
|
50
|
+
return /already\s+up\s+to\s+date/i.test(stdout) || pinnedAsStatus;
|
|
51
|
+
}
|
|
52
|
+
|
|
42
53
|
async function updatePackageInternal(
|
|
43
54
|
source: string,
|
|
44
55
|
ctx: ExtensionCommandContext,
|
|
@@ -60,9 +71,10 @@ async function updatePackageInternal(
|
|
|
60
71
|
}
|
|
61
72
|
|
|
62
73
|
const stdout = res.stdout || "";
|
|
63
|
-
if (
|
|
74
|
+
if (isUpToDateOutput(stdout)) {
|
|
64
75
|
notify(ctx, `${source} is already up to date (or pinned).`, "info");
|
|
65
76
|
logPackageUpdate(pi, source, source, undefined, true);
|
|
77
|
+
clearUpdatesAvailable(pi, ctx);
|
|
66
78
|
void updateExtmgrStatus(ctx, pi);
|
|
67
79
|
return NO_PACKAGE_MUTATION_OUTCOME;
|
|
68
80
|
}
|
|
@@ -87,18 +99,23 @@ async function updatePackagesInternal(
|
|
|
87
99
|
const res = await pi.exec("pi", ["update"], { timeout: TIMEOUTS.packageUpdateAll, cwd: ctx.cwd });
|
|
88
100
|
|
|
89
101
|
if (res.code !== 0) {
|
|
90
|
-
|
|
102
|
+
const errorMsg = `Update failed: ${res.stderr || res.stdout || `exit ${res.code}`}`;
|
|
103
|
+
logPackageUpdate(pi, BULK_UPDATE_LABEL, BULK_UPDATE_LABEL, undefined, false, errorMsg);
|
|
104
|
+
notifyError(ctx, errorMsg);
|
|
91
105
|
void updateExtmgrStatus(ctx, pi);
|
|
92
106
|
return NO_PACKAGE_MUTATION_OUTCOME;
|
|
93
107
|
}
|
|
94
108
|
|
|
95
109
|
const stdout = res.stdout || "";
|
|
96
|
-
if (stdout
|
|
110
|
+
if (isUpToDateOutput(stdout) || stdout.trim() === "") {
|
|
97
111
|
notify(ctx, "All packages are already up to date.", "info");
|
|
112
|
+
logPackageUpdate(pi, BULK_UPDATE_LABEL, BULK_UPDATE_LABEL, undefined, true);
|
|
113
|
+
clearUpdatesAvailable(pi, ctx);
|
|
98
114
|
void updateExtmgrStatus(ctx, pi);
|
|
99
115
|
return NO_PACKAGE_MUTATION_OUTCOME;
|
|
100
116
|
}
|
|
101
117
|
|
|
118
|
+
logPackageUpdate(pi, BULK_UPDATE_LABEL, BULK_UPDATE_LABEL, undefined, true);
|
|
102
119
|
success(ctx, "Packages updated");
|
|
103
120
|
clearUpdatesAvailable(pi, ctx);
|
|
104
121
|
|
|
@@ -145,12 +162,18 @@ function packageIdentity(source: string, fallbackName?: string): string {
|
|
|
145
162
|
return `npm:${npm.name}`;
|
|
146
163
|
}
|
|
147
164
|
|
|
148
|
-
|
|
165
|
+
const sourceKind = getPackageSourceKind(source);
|
|
166
|
+
|
|
167
|
+
if (sourceKind === "git") {
|
|
149
168
|
const gitSpec = source.startsWith("git:") ? source.slice(4) : source;
|
|
150
169
|
const { repo } = splitGitRepoAndRef(gitSpec);
|
|
151
170
|
return `git:${repo}`;
|
|
152
171
|
}
|
|
153
172
|
|
|
173
|
+
if (sourceKind === "local") {
|
|
174
|
+
return `src:${normalizeLocalSourceIdentity(source)}`;
|
|
175
|
+
}
|
|
176
|
+
|
|
154
177
|
if (fallbackName) {
|
|
155
178
|
return `name:${fallbackName}`;
|
|
156
179
|
}
|
|
@@ -238,12 +261,18 @@ function formatRemovalTargets(targets: RemovalTarget[]): string {
|
|
|
238
261
|
return targets.map((t) => `${t.scope}: ${t.source}`).join("\n");
|
|
239
262
|
}
|
|
240
263
|
|
|
264
|
+
interface RemovalExecutionResult {
|
|
265
|
+
target: RemovalTarget;
|
|
266
|
+
success: boolean;
|
|
267
|
+
error?: string;
|
|
268
|
+
}
|
|
269
|
+
|
|
241
270
|
async function executeRemovalTargets(
|
|
242
271
|
targets: RemovalTarget[],
|
|
243
272
|
ctx: ExtensionCommandContext,
|
|
244
273
|
pi: ExtensionAPI
|
|
245
|
-
): Promise<
|
|
246
|
-
const
|
|
274
|
+
): Promise<RemovalExecutionResult[]> {
|
|
275
|
+
const results: RemovalExecutionResult[] = [];
|
|
247
276
|
|
|
248
277
|
for (const target of targets) {
|
|
249
278
|
showProgress(ctx, "Removing", `${target.source} (${target.scope})`);
|
|
@@ -254,14 +283,15 @@ async function executeRemovalTargets(
|
|
|
254
283
|
if (res.code !== 0) {
|
|
255
284
|
const errorMsg = `Remove failed (${target.scope}): ${res.stderr || res.stdout || `exit ${res.code}`}`;
|
|
256
285
|
logPackageRemove(pi, target.source, target.name, false, errorMsg);
|
|
257
|
-
|
|
286
|
+
results.push({ target, success: false, error: errorMsg });
|
|
258
287
|
continue;
|
|
259
288
|
}
|
|
260
289
|
|
|
261
290
|
logPackageRemove(pi, target.source, target.name, true);
|
|
291
|
+
results.push({ target, success: true });
|
|
262
292
|
}
|
|
263
293
|
|
|
264
|
-
return
|
|
294
|
+
return results;
|
|
265
295
|
}
|
|
266
296
|
|
|
267
297
|
function notifyRemovalSummary(
|
|
@@ -326,9 +356,18 @@ async function removePackageInternal(
|
|
|
326
356
|
return NO_PACKAGE_MUTATION_OUTCOME;
|
|
327
357
|
}
|
|
328
358
|
|
|
329
|
-
const
|
|
359
|
+
const results = await executeRemovalTargets(targets, ctx, pi);
|
|
330
360
|
clearSearchCache();
|
|
331
361
|
|
|
362
|
+
const failures = results
|
|
363
|
+
.filter((result): result is RemovalExecutionResult & { success: false; error: string } =>
|
|
364
|
+
Boolean(!result.success && result.error)
|
|
365
|
+
)
|
|
366
|
+
.map((result) => result.error);
|
|
367
|
+
const successfulTargets = results
|
|
368
|
+
.filter((result) => result.success)
|
|
369
|
+
.map((result) => result.target);
|
|
370
|
+
|
|
332
371
|
const remaining = (await getInstalledPackagesAllScopes(ctx, pi)).filter(
|
|
333
372
|
(p) => packageIdentity(p.source, p.name) === identity
|
|
334
373
|
);
|
|
@@ -338,13 +377,15 @@ async function removePackageInternal(
|
|
|
338
377
|
clearUpdatesAvailable(pi, ctx);
|
|
339
378
|
}
|
|
340
379
|
|
|
341
|
-
|
|
342
|
-
|
|
380
|
+
const successfulRemovalCount = successfulTargets.length;
|
|
381
|
+
|
|
382
|
+
// Wait for successfully removed targets to disappear from their target scopes before reloading.
|
|
383
|
+
if (successfulTargets.length > 0) {
|
|
343
384
|
notify(ctx, "Waiting for removal to complete...", "info");
|
|
344
385
|
const isRemoved = await waitForCondition(
|
|
345
386
|
async () => {
|
|
346
387
|
const installedChecks = await Promise.all(
|
|
347
|
-
|
|
388
|
+
successfulTargets.map((target) =>
|
|
348
389
|
isSourceInstalled(target.source, ctx, pi, {
|
|
349
390
|
scope: target.scope,
|
|
350
391
|
})
|
|
@@ -360,6 +401,11 @@ async function removePackageInternal(
|
|
|
360
401
|
}
|
|
361
402
|
}
|
|
362
403
|
|
|
404
|
+
if (successfulRemovalCount === 0) {
|
|
405
|
+
void updateExtmgrStatus(ctx, pi);
|
|
406
|
+
return NO_PACKAGE_MUTATION_OUTCOME;
|
|
407
|
+
}
|
|
408
|
+
|
|
363
409
|
const reloaded = await confirmReload(ctx, "Removal complete.");
|
|
364
410
|
if (!reloaded) {
|
|
365
411
|
void updateExtmgrStatus(ctx, pi);
|
package/src/ui/remote.ts
CHANGED
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
isCacheValid,
|
|
16
16
|
} from "../packages/discovery.js";
|
|
17
17
|
import { installPackage, installPackageLocally } from "../packages/install.js";
|
|
18
|
+
import { execNpm } from "../utils/npm-exec.js";
|
|
18
19
|
import { notify } from "../utils/notify.js";
|
|
19
20
|
|
|
20
21
|
interface PackageInfoCacheEntry {
|
|
@@ -143,9 +144,8 @@ async function buildPackageInfoText(
|
|
|
143
144
|
}
|
|
144
145
|
|
|
145
146
|
const [infoRes, weeklyDownloads] = await Promise.all([
|
|
146
|
-
pi
|
|
147
|
+
execNpm(pi, ["view", packageName, "--json"], ctx, {
|
|
147
148
|
timeout: TIMEOUTS.npmView,
|
|
148
|
-
cwd: ctx.cwd,
|
|
149
149
|
}),
|
|
150
150
|
fetchWeeklyDownloads(packageName),
|
|
151
151
|
]);
|
package/src/ui/unified.ts
CHANGED
|
@@ -263,7 +263,11 @@ async function showInteractiveOnce(
|
|
|
263
263
|
}
|
|
264
264
|
|
|
265
265
|
function normalizePathForDuplicateCheck(value: string): string {
|
|
266
|
-
|
|
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;
|
|
267
271
|
}
|
|
268
272
|
|
|
269
273
|
export function buildUnifiedItems(
|
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) {
|