@paretools/python 0.8.5 → 0.10.0
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 +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/lib/formatters.d.ts +52 -5
- package/dist/lib/formatters.d.ts.map +1 -1
- package/dist/lib/formatters.js +287 -37
- package/dist/lib/formatters.js.map +1 -1
- package/dist/lib/parsers.d.ts +30 -15
- package/dist/lib/parsers.d.ts.map +1 -1
- package/dist/lib/parsers.js +506 -51
- package/dist/lib/parsers.js.map +1 -1
- package/dist/lib/python-runner.d.ts +2 -0
- package/dist/lib/python-runner.d.ts.map +1 -1
- package/dist/lib/python-runner.js +12 -8
- package/dist/lib/python-runner.js.map +1 -1
- package/dist/schemas/index.d.ts +354 -69
- package/dist/schemas/index.d.ts.map +1 -1
- package/dist/schemas/index.js +266 -32
- package/dist/schemas/index.js.map +1 -1
- package/dist/tools/black.d.ts.map +1 -1
- package/dist/tools/black.js +45 -10
- package/dist/tools/black.js.map +1 -1
- package/dist/tools/conda.d.ts.map +1 -1
- package/dist/tools/conda.js +73 -14
- package/dist/tools/conda.js.map +1 -1
- package/dist/tools/mypy.d.ts.map +1 -1
- package/dist/tools/mypy.js +168 -13
- package/dist/tools/mypy.js.map +1 -1
- package/dist/tools/pip-audit.d.ts.map +1 -1
- package/dist/tools/pip-audit.js +75 -15
- package/dist/tools/pip-audit.js.map +1 -1
- package/dist/tools/pip-install.d.ts.map +1 -1
- package/dist/tools/pip-install.js +88 -14
- package/dist/tools/pip-install.js.map +1 -1
- package/dist/tools/pip-list.d.ts.map +1 -1
- package/dist/tools/pip-list.js +58 -13
- package/dist/tools/pip-list.js.map +1 -1
- package/dist/tools/pip-show.d.ts.map +1 -1
- package/dist/tools/pip-show.js +46 -12
- package/dist/tools/pip-show.js.map +1 -1
- package/dist/tools/poetry.d.ts.map +1 -1
- package/dist/tools/poetry.js +75 -14
- package/dist/tools/poetry.js.map +1 -1
- package/dist/tools/pyenv.d.ts.map +1 -1
- package/dist/tools/pyenv.js +84 -19
- package/dist/tools/pyenv.js.map +1 -1
- package/dist/tools/pytest.d.ts.map +1 -1
- package/dist/tools/pytest.js +77 -11
- package/dist/tools/pytest.js.map +1 -1
- package/dist/tools/ruff-format.d.ts.map +1 -1
- package/dist/tools/ruff-format.js +84 -9
- package/dist/tools/ruff-format.js.map +1 -1
- package/dist/tools/ruff.d.ts.map +1 -1
- package/dist/tools/ruff.js +87 -11
- package/dist/tools/ruff.js.map +1 -1
- package/dist/tools/uv-install.d.ts.map +1 -1
- package/dist/tools/uv-install.js +79 -14
- package/dist/tools/uv-install.js.map +1 -1
- package/dist/tools/uv-run.d.ts.map +1 -1
- package/dist/tools/uv-run.js +81 -13
- package/dist/tools/uv-run.js.map +1 -1
- package/package.json +2 -2
package/dist/lib/parsers.js
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
|
-
/** Parses `pip install` output into structured data with installed packages and satisfaction status.
|
|
1
|
+
/** Parses `pip install` output into structured data with installed packages and satisfaction status.
|
|
2
|
+
* Handles both normal installs ("Successfully installed ...") and dry-run output ("Would install ..."). */
|
|
2
3
|
export function parsePipInstall(stdout, stderr, exitCode) {
|
|
3
4
|
const output = stdout + "\n" + stderr;
|
|
5
|
+
const lines = output.split("\n");
|
|
4
6
|
const alreadySatisfied = output.includes("already satisfied");
|
|
7
|
+
const dryRun = output.includes("Would install") || output.includes("--dry-run");
|
|
5
8
|
const installed = [];
|
|
9
|
+
const warnings = [];
|
|
10
|
+
// Normal mode: "Successfully installed pkg-1.0.0 pkg2-2.0.0"
|
|
6
11
|
const installMatch = output.match(/Successfully installed (.+)/);
|
|
7
12
|
if (installMatch) {
|
|
8
13
|
const packages = installMatch[1].trim().split(/\s+/);
|
|
@@ -16,16 +21,73 @@ export function parsePipInstall(stdout, stderr, exitCode) {
|
|
|
16
21
|
}
|
|
17
22
|
}
|
|
18
23
|
}
|
|
24
|
+
// Dry-run mode: "Would install pkg-1.0.0 pkg2-2.0.0"
|
|
25
|
+
if (installed.length === 0) {
|
|
26
|
+
const dryRunMatch = output.match(/Would install (.+)/);
|
|
27
|
+
if (dryRunMatch) {
|
|
28
|
+
const packages = dryRunMatch[1].trim().split(/\s+/);
|
|
29
|
+
for (const pkg of packages) {
|
|
30
|
+
const lastDash = pkg.lastIndexOf("-");
|
|
31
|
+
if (lastDash > 0) {
|
|
32
|
+
installed.push({
|
|
33
|
+
name: pkg.slice(0, lastDash),
|
|
34
|
+
version: pkg.slice(lastDash + 1),
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
for (const line of lines) {
|
|
41
|
+
const trimmed = line.trim();
|
|
42
|
+
if (/^(WARNING|DEPRECATION):/i.test(trimmed)) {
|
|
43
|
+
warnings.push(trimmed);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
19
46
|
return {
|
|
20
47
|
success: exitCode === 0,
|
|
21
48
|
installed,
|
|
22
49
|
alreadySatisfied,
|
|
50
|
+
warnings: warnings.length > 0 ? warnings : undefined,
|
|
51
|
+
dryRun,
|
|
23
52
|
total: installed.length,
|
|
24
53
|
};
|
|
25
54
|
}
|
|
26
55
|
const MYPY_RE = /^(.+?):(\d+)(?::(\d+))?: (error|warning|note): (.+?)(?:\s+\[([^\]]+)\])?$/;
|
|
27
|
-
/** Parses mypy
|
|
28
|
-
export function
|
|
56
|
+
/** Parses mypy JSON output (from `--output json`) into structured diagnostics. */
|
|
57
|
+
export function parseMypyJsonOutput(stdout, exitCode) {
|
|
58
|
+
let entries;
|
|
59
|
+
try {
|
|
60
|
+
entries = JSON.parse(stdout);
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
// Fall back to text parsing if JSON parsing fails (older mypy without --output json)
|
|
64
|
+
return parseMypyTextOutput(stdout, exitCode);
|
|
65
|
+
}
|
|
66
|
+
if (!Array.isArray(entries)) {
|
|
67
|
+
return parseMypyTextOutput(stdout, exitCode);
|
|
68
|
+
}
|
|
69
|
+
const diagnostics = entries.map((e) => ({
|
|
70
|
+
file: e.file,
|
|
71
|
+
line: e.line,
|
|
72
|
+
column: e.column > 0 ? e.column : undefined,
|
|
73
|
+
severity: e.severity,
|
|
74
|
+
message: e.message,
|
|
75
|
+
code: e.code || undefined,
|
|
76
|
+
}));
|
|
77
|
+
const errors = diagnostics.filter((d) => d.severity === "error").length;
|
|
78
|
+
const warnings = diagnostics.filter((d) => d.severity === "warning").length;
|
|
79
|
+
const notes = diagnostics.filter((d) => d.severity === "note").length;
|
|
80
|
+
return {
|
|
81
|
+
success: exitCode === 0,
|
|
82
|
+
diagnostics,
|
|
83
|
+
total: diagnostics.length,
|
|
84
|
+
errors,
|
|
85
|
+
warnings,
|
|
86
|
+
notes,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
/** Parses mypy text output into structured diagnostics (fallback for older mypy without JSON). */
|
|
90
|
+
export function parseMypyTextOutput(stdout, exitCode) {
|
|
29
91
|
const lines = stdout.split("\n");
|
|
30
92
|
const diagnostics = [];
|
|
31
93
|
for (const line of lines) {
|
|
@@ -42,23 +104,30 @@ export function parseMypyOutput(stdout, exitCode) {
|
|
|
42
104
|
}
|
|
43
105
|
}
|
|
44
106
|
const errors = diagnostics.filter((d) => d.severity === "error").length;
|
|
45
|
-
const warnings = diagnostics.filter((d) => d.severity === "warning"
|
|
107
|
+
const warnings = diagnostics.filter((d) => d.severity === "warning").length;
|
|
108
|
+
const notes = diagnostics.filter((d) => d.severity === "note").length;
|
|
46
109
|
return {
|
|
47
110
|
success: exitCode === 0,
|
|
48
111
|
diagnostics,
|
|
49
112
|
total: diagnostics.length,
|
|
50
113
|
errors,
|
|
51
114
|
warnings,
|
|
115
|
+
notes,
|
|
52
116
|
};
|
|
53
117
|
}
|
|
118
|
+
/** Parses mypy output: tries JSON first, falls back to text parsing.
|
|
119
|
+
* @deprecated Use parseMypyJsonOutput (which auto-falls back) instead. Kept for backward compat. */
|
|
120
|
+
export function parseMypyOutput(stdout, exitCode) {
|
|
121
|
+
return parseMypyTextOutput(stdout, exitCode);
|
|
122
|
+
}
|
|
54
123
|
/** Parses `ruff check --output-format json` output into structured lint diagnostics with fixability info. */
|
|
55
|
-
export function parseRuffJson(stdout) {
|
|
124
|
+
export function parseRuffJson(stdout, exitCode, stderr = "") {
|
|
56
125
|
let entries;
|
|
57
126
|
try {
|
|
58
127
|
entries = JSON.parse(stdout);
|
|
59
128
|
}
|
|
60
129
|
catch {
|
|
61
|
-
return { diagnostics: [], total: 0, fixable: 0 };
|
|
130
|
+
return { success: exitCode === 0, diagnostics: [], total: 0, fixable: 0 };
|
|
62
131
|
}
|
|
63
132
|
const diagnostics = entries.map((e) => ({
|
|
64
133
|
file: e.filename,
|
|
@@ -69,34 +138,83 @@ export function parseRuffJson(stdout) {
|
|
|
69
138
|
code: e.code,
|
|
70
139
|
message: e.message,
|
|
71
140
|
fixable: !!e.fix,
|
|
141
|
+
fixApplicability: extractFixApplicability(e.fix),
|
|
142
|
+
url: e.url || undefined,
|
|
72
143
|
}));
|
|
73
144
|
const fixable = diagnostics.filter((d) => d.fixable).length;
|
|
74
|
-
|
|
145
|
+
const fixedMatch = stderr.match(/\bFixed\s+(\d+)\s+(?:errors?|violations?)\b/i) ??
|
|
146
|
+
stderr.match(/\b(\d+)\s+fixed\b/i);
|
|
147
|
+
const fixedCount = fixedMatch ? parseInt(fixedMatch[1], 10) : undefined;
|
|
148
|
+
return {
|
|
149
|
+
success: exitCode === 0,
|
|
150
|
+
diagnostics,
|
|
151
|
+
total: diagnostics.length,
|
|
152
|
+
fixable,
|
|
153
|
+
fixedCount,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
/** Extracts fix applicability from a ruff fix object. */
|
|
157
|
+
function extractFixApplicability(fix) {
|
|
158
|
+
if (!fix)
|
|
159
|
+
return undefined;
|
|
160
|
+
if (typeof fix === "object" && "applicability" in fix && typeof fix.applicability === "string") {
|
|
161
|
+
const val = fix.applicability.toLowerCase();
|
|
162
|
+
if (val === "safe" || val === "unsafe" || val === "display") {
|
|
163
|
+
return val;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return undefined;
|
|
75
167
|
}
|
|
76
168
|
/** Parses `pip-audit --format json` output into structured vulnerability data with fix versions. */
|
|
77
|
-
export function parsePipAuditJson(stdout) {
|
|
169
|
+
export function parsePipAuditJson(stdout, exitCode) {
|
|
78
170
|
let data;
|
|
79
171
|
try {
|
|
80
172
|
data = JSON.parse(stdout);
|
|
81
173
|
}
|
|
82
174
|
catch {
|
|
83
|
-
return { vulnerabilities: [], total: 0 };
|
|
84
|
-
}
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
175
|
+
return { success: exitCode === 0, vulnerabilities: [], total: 0 };
|
|
176
|
+
}
|
|
177
|
+
const byPackage = [];
|
|
178
|
+
const skipped = [];
|
|
179
|
+
const vulnerabilities = (data.dependencies ?? []).flatMap((dep) => {
|
|
180
|
+
if (dep.skip_reason) {
|
|
181
|
+
skipped.push({ name: dep.name, reason: dep.skip_reason });
|
|
182
|
+
return [];
|
|
183
|
+
}
|
|
184
|
+
const packageVulns = (dep.vulns ?? []).map((v) => ({
|
|
185
|
+
name: dep.name,
|
|
186
|
+
version: dep.version,
|
|
187
|
+
id: v.id,
|
|
188
|
+
description: v.description ?? "",
|
|
189
|
+
fixVersions: v.fix_versions ?? [],
|
|
190
|
+
aliases: v.aliases && v.aliases.length > 0 ? v.aliases : undefined,
|
|
191
|
+
url: v.url || undefined,
|
|
192
|
+
severity: v.severity || undefined,
|
|
193
|
+
cvssScore: v.cvss_score != null ? v.cvss_score : undefined,
|
|
194
|
+
}));
|
|
195
|
+
if (packageVulns.length > 0) {
|
|
196
|
+
byPackage.push({
|
|
197
|
+
name: dep.name,
|
|
198
|
+
version: dep.version,
|
|
199
|
+
vulnerabilities: packageVulns,
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
return packageVulns;
|
|
203
|
+
});
|
|
204
|
+
return {
|
|
205
|
+
success: exitCode === 0,
|
|
206
|
+
vulnerabilities,
|
|
207
|
+
byPackage: byPackage.length > 0 ? byPackage : undefined,
|
|
208
|
+
skipped: skipped.length > 0 ? skipped : undefined,
|
|
209
|
+
total: vulnerabilities.length,
|
|
210
|
+
};
|
|
93
211
|
}
|
|
94
212
|
const PYTEST_DURATION_RE = /in ([\d.]+)s/;
|
|
95
213
|
function extractCount(line, label) {
|
|
96
214
|
const m = line.match(label);
|
|
97
215
|
return m ? parseInt(m[1], 10) : 0;
|
|
98
216
|
}
|
|
99
|
-
/** Parses pytest output into structured test results with pass/fail/error/skip counts and failure details. */
|
|
217
|
+
/** Parses pytest output into structured test results with pass/fail/error/skip/warning counts and failure details. */
|
|
100
218
|
export function parsePytestOutput(stdout, stderr, exitCode) {
|
|
101
219
|
const output = stdout + "\n" + stderr;
|
|
102
220
|
const lines = output.split("\n");
|
|
@@ -105,14 +223,16 @@ export function parsePytestOutput(stdout, stderr, exitCode) {
|
|
|
105
223
|
let failed = 0;
|
|
106
224
|
let errors = 0;
|
|
107
225
|
let skipped = 0;
|
|
226
|
+
let warnings = 0;
|
|
108
227
|
let duration = 0;
|
|
109
228
|
for (const line of lines) {
|
|
110
229
|
// Match lines containing pytest summary markers (== ... in Xs ==) or standalone counts
|
|
111
|
-
if (/\d+ (?:passed|failed|error|skipped)/.test(line) || /in [\d.]+s/.test(line)) {
|
|
230
|
+
if (/\d+ (?:passed|failed|error|skipped|warning)/.test(line) || /in [\d.]+s/.test(line)) {
|
|
112
231
|
passed = Math.max(passed, extractCount(line, /(\d+) passed/));
|
|
113
232
|
failed = Math.max(failed, extractCount(line, /(\d+) failed/));
|
|
114
233
|
errors = Math.max(errors, extractCount(line, /(\d+) errors?/));
|
|
115
234
|
skipped = Math.max(skipped, extractCount(line, /(\d+) skipped/));
|
|
235
|
+
warnings = Math.max(warnings, extractCount(line, /(\d+) warnings?/));
|
|
116
236
|
const durationMatch = line.match(PYTEST_DURATION_RE);
|
|
117
237
|
if (durationMatch) {
|
|
118
238
|
duration = parseFloat(durationMatch[1]);
|
|
@@ -128,6 +248,7 @@ export function parsePytestOutput(stdout, stderr, exitCode) {
|
|
|
128
248
|
failed: 0,
|
|
129
249
|
errors: 0,
|
|
130
250
|
skipped: 0,
|
|
251
|
+
warnings,
|
|
131
252
|
total: 0,
|
|
132
253
|
duration,
|
|
133
254
|
failures: [],
|
|
@@ -168,6 +289,7 @@ export function parsePytestOutput(stdout, stderr, exitCode) {
|
|
|
168
289
|
failed,
|
|
169
290
|
errors,
|
|
170
291
|
skipped,
|
|
292
|
+
warnings,
|
|
171
293
|
total,
|
|
172
294
|
duration,
|
|
173
295
|
failures,
|
|
@@ -176,7 +298,10 @@ export function parsePytestOutput(stdout, stderr, exitCode) {
|
|
|
176
298
|
// Matches uv install output lines like: " + package==version" or "Installed N packages in Ns"
|
|
177
299
|
const UV_INSTALLED_PKG_RE = /^\s*\+\s+(\S+)==(\S+)/;
|
|
178
300
|
const UV_SUMMARY_RE = /Installed (\d+) packages? in ([\d.]+)/;
|
|
179
|
-
|
|
301
|
+
// Matches uv resolution conflict lines like: "`package>=1.0` and `package<1.0`"
|
|
302
|
+
const UV_CONFLICT_RE = /`([a-zA-Z0-9_-]+)\s*([^`]*)`/g;
|
|
303
|
+
/** Parses uv pip install output into structured data with installed packages.
|
|
304
|
+
* On failure, extracts resolution conflict information from stderr. */
|
|
180
305
|
export function parseUvInstall(stdout, stderr, exitCode) {
|
|
181
306
|
const output = stdout + "\n" + stderr;
|
|
182
307
|
const lines = output.split("\n");
|
|
@@ -192,19 +317,90 @@ export function parseUvInstall(stdout, stderr, exitCode) {
|
|
|
192
317
|
duration = parseFloat(summaryMatch[2]);
|
|
193
318
|
}
|
|
194
319
|
}
|
|
320
|
+
// On failure, extract resolution error details from stderr
|
|
321
|
+
let error;
|
|
322
|
+
let resolutionConflicts;
|
|
323
|
+
if (exitCode !== 0) {
|
|
324
|
+
const stderrTrimmed = stderr.trim();
|
|
325
|
+
if (stderrTrimmed) {
|
|
326
|
+
error = stderrTrimmed;
|
|
327
|
+
}
|
|
328
|
+
// Check for resolution/version conflict errors
|
|
329
|
+
if (stderr.includes("version solving failed") ||
|
|
330
|
+
stderr.includes("Cannot install") ||
|
|
331
|
+
stderr.includes("conflict") ||
|
|
332
|
+
stderr.includes("incompatible")) {
|
|
333
|
+
const conflicts = [];
|
|
334
|
+
const seen = new Set();
|
|
335
|
+
for (const line of stderr.split("\n")) {
|
|
336
|
+
// Reset lastIndex for global regex
|
|
337
|
+
UV_CONFLICT_RE.lastIndex = 0;
|
|
338
|
+
let match;
|
|
339
|
+
while ((match = UV_CONFLICT_RE.exec(line)) !== null) {
|
|
340
|
+
const pkg = match[1];
|
|
341
|
+
const constraint = match[2].trim();
|
|
342
|
+
const key = `${pkg}:${constraint}`;
|
|
343
|
+
if (!seen.has(key) && constraint) {
|
|
344
|
+
seen.add(key);
|
|
345
|
+
conflicts.push({ package: pkg, constraint });
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
if (conflicts.length > 0) {
|
|
350
|
+
resolutionConflicts = conflicts;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
const alreadySatisfied = exitCode === 0 &&
|
|
355
|
+
installed.length === 0 &&
|
|
356
|
+
/(already installed|already satisfied|audited \d+ packages|nothing to install)/i.test(output);
|
|
195
357
|
return {
|
|
196
358
|
success: exitCode === 0,
|
|
197
359
|
installed,
|
|
198
360
|
total: installed.length,
|
|
199
361
|
duration,
|
|
362
|
+
alreadySatisfied,
|
|
363
|
+
error,
|
|
364
|
+
resolutionConflicts,
|
|
200
365
|
};
|
|
201
366
|
}
|
|
202
367
|
/** Parses uv run output into structured result with exit code, stdout, stderr. */
|
|
203
|
-
export function parseUvRun(stdout, stderr, exitCode, durationMs) {
|
|
368
|
+
export function parseUvRun(stdout, stderr, exitCode, durationMs, options) {
|
|
369
|
+
const uvDiagnosticLines = [];
|
|
370
|
+
const commandErrLines = [];
|
|
371
|
+
for (const line of stderr.split("\n")) {
|
|
372
|
+
const trimmed = line.trim();
|
|
373
|
+
if (/^(Resolved|Prepared|Installed|Uninstalled|Audited)\b/.test(trimmed) ||
|
|
374
|
+
/^Using (?:Python|CPython)\b/.test(trimmed) ||
|
|
375
|
+
/^Downloading\b/.test(trimmed)) {
|
|
376
|
+
if (trimmed)
|
|
377
|
+
uvDiagnosticLines.push(trimmed);
|
|
378
|
+
}
|
|
379
|
+
else {
|
|
380
|
+
commandErrLines.push(line);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
let outputStdout = stdout;
|
|
384
|
+
let outputStderr = commandErrLines.join("\n");
|
|
385
|
+
let truncated = false;
|
|
386
|
+
const maxOutputChars = options?.maxOutputChars ?? 0;
|
|
387
|
+
if (maxOutputChars > 0) {
|
|
388
|
+
if (outputStdout.length > maxOutputChars) {
|
|
389
|
+
outputStdout = outputStdout.slice(0, maxOutputChars) + "…";
|
|
390
|
+
truncated = true;
|
|
391
|
+
}
|
|
392
|
+
if (outputStderr.length > maxOutputChars) {
|
|
393
|
+
outputStderr = outputStderr.slice(0, maxOutputChars) + "…";
|
|
394
|
+
truncated = true;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
204
397
|
return {
|
|
205
398
|
exitCode,
|
|
206
|
-
stdout,
|
|
207
|
-
stderr,
|
|
399
|
+
stdout: outputStdout,
|
|
400
|
+
stderr: outputStderr,
|
|
401
|
+
commandStderr: outputStderr || undefined,
|
|
402
|
+
uvDiagnostics: uvDiagnosticLines.length > 0 ? uvDiagnosticLines : undefined,
|
|
403
|
+
truncated: truncated || undefined,
|
|
208
404
|
success: exitCode === 0,
|
|
209
405
|
duration: Math.round(durationMs) / 1000,
|
|
210
406
|
};
|
|
@@ -218,6 +414,7 @@ export function parseBlackOutput(stdout, stderr, exitCode) {
|
|
|
218
414
|
const output = stdout + "\n" + stderr;
|
|
219
415
|
const lines = output.split("\n");
|
|
220
416
|
const wouldReformat = [];
|
|
417
|
+
const diagnostics = [];
|
|
221
418
|
let filesChanged = 0;
|
|
222
419
|
let filesUnchanged = 0;
|
|
223
420
|
for (const line of lines) {
|
|
@@ -229,6 +426,15 @@ export function parseBlackOutput(stdout, stderr, exitCode) {
|
|
|
229
426
|
if (reformattedMatch) {
|
|
230
427
|
wouldReformat.push(reformattedMatch[1].trim());
|
|
231
428
|
}
|
|
429
|
+
const parseDiagMatch = line.match(/cannot format (.+?): Cannot parse: (\d+):(\d+): (.+)$/i);
|
|
430
|
+
if (parseDiagMatch) {
|
|
431
|
+
diagnostics.push({
|
|
432
|
+
file: parseDiagMatch[1],
|
|
433
|
+
line: parseInt(parseDiagMatch[2], 10),
|
|
434
|
+
column: parseInt(parseDiagMatch[3], 10),
|
|
435
|
+
message: parseDiagMatch[4],
|
|
436
|
+
});
|
|
437
|
+
}
|
|
232
438
|
}
|
|
233
439
|
// Parse summary line
|
|
234
440
|
for (const line of lines) {
|
|
@@ -251,38 +457,69 @@ export function parseBlackOutput(stdout, stderr, exitCode) {
|
|
|
251
457
|
const filesChecked = filesChanged + filesUnchanged;
|
|
252
458
|
// In check mode, success means no files need reformatting
|
|
253
459
|
// In format mode, success is always true (exitCode 0) unless black errors
|
|
254
|
-
// exitCode 1 in check mode = files would be reformatted
|
|
255
|
-
// exitCode 123 = internal error
|
|
460
|
+
// exitCode 1 in check mode = files would be reformatted ("check_failed")
|
|
461
|
+
// exitCode 123 = internal error (parse error or crash)
|
|
256
462
|
const success = exitCode === 0;
|
|
463
|
+
let errorType;
|
|
464
|
+
if (exitCode === 1) {
|
|
465
|
+
errorType = "check_failed";
|
|
466
|
+
}
|
|
467
|
+
else if (exitCode === 123) {
|
|
468
|
+
errorType = "internal_error";
|
|
469
|
+
}
|
|
257
470
|
return {
|
|
258
471
|
filesChanged,
|
|
259
472
|
filesUnchanged,
|
|
260
473
|
filesChecked,
|
|
261
474
|
success,
|
|
475
|
+
exitCode: exitCode !== 0 ? exitCode : undefined,
|
|
476
|
+
errorType,
|
|
477
|
+
diagnostics: diagnostics.length > 0 ? diagnostics : undefined,
|
|
262
478
|
wouldReformat,
|
|
263
479
|
};
|
|
264
480
|
}
|
|
265
|
-
/** Parses `pip list --format json` output into structured package list.
|
|
266
|
-
|
|
481
|
+
/** Parses `pip list --format json` output into structured package list.
|
|
482
|
+
* When outdated=true, also parses latestVersion and latestFiletype fields.
|
|
483
|
+
* On JSON parse failure, surfaces error with raw output for debugging. */
|
|
484
|
+
export function parsePipListJson(stdout, exitCode, outdated) {
|
|
267
485
|
let entries;
|
|
268
486
|
try {
|
|
269
487
|
entries = JSON.parse(stdout);
|
|
270
488
|
}
|
|
271
|
-
catch {
|
|
272
|
-
|
|
489
|
+
catch (err) {
|
|
490
|
+
const errorMessage = err instanceof Error ? err.message : "Failed to parse pip list JSON output";
|
|
491
|
+
// Truncate raw output to 500 chars to avoid bloating the response
|
|
492
|
+
const rawOutput = stdout.length > 500 ? stdout.slice(0, 500) + "..." : stdout;
|
|
493
|
+
return {
|
|
494
|
+
success: false,
|
|
495
|
+
packages: [],
|
|
496
|
+
total: 0,
|
|
497
|
+
error: errorMessage,
|
|
498
|
+
rawOutput: rawOutput || undefined,
|
|
499
|
+
};
|
|
273
500
|
}
|
|
274
501
|
if (!Array.isArray(entries)) {
|
|
275
|
-
return {
|
|
502
|
+
return {
|
|
503
|
+
success: false,
|
|
504
|
+
packages: [],
|
|
505
|
+
total: 0,
|
|
506
|
+
error: "pip list output is not a JSON array",
|
|
507
|
+
rawOutput: stdout.length > 500 ? stdout.slice(0, 500) + "..." : stdout,
|
|
508
|
+
};
|
|
276
509
|
}
|
|
277
510
|
const packages = entries.map((e) => ({
|
|
278
511
|
name: e.name,
|
|
279
512
|
version: e.version,
|
|
513
|
+
location: e.location || undefined,
|
|
514
|
+
editableProject: e.editable_project != null ? true : undefined,
|
|
515
|
+
...(outdated && e.latest_version ? { latestVersion: e.latest_version } : {}),
|
|
516
|
+
...(outdated && e.latest_filetype ? { latestFiletype: e.latest_filetype } : {}),
|
|
280
517
|
}));
|
|
281
|
-
return { packages, total: packages.length };
|
|
518
|
+
return { success: exitCode === 0, packages, total: packages.length };
|
|
282
519
|
}
|
|
283
|
-
/** Parses `pip show
|
|
284
|
-
|
|
285
|
-
const lines =
|
|
520
|
+
/** Parses a single block of `pip show` key-value output into a package info object. */
|
|
521
|
+
function parseSinglePipShowBlock(block) {
|
|
522
|
+
const lines = block.split("\n");
|
|
286
523
|
const data = {};
|
|
287
524
|
for (const line of lines) {
|
|
288
525
|
const colonIdx = line.indexOf(": ");
|
|
@@ -295,15 +532,62 @@ export function parsePipShowOutput(stdout) {
|
|
|
295
532
|
const requires = data["Requires"] && data["Requires"].trim()
|
|
296
533
|
? data["Requires"].split(",").map((r) => r.trim())
|
|
297
534
|
: [];
|
|
535
|
+
const requiredBy = data["Required-by"] && data["Required-by"].trim()
|
|
536
|
+
? data["Required-by"].split(",").map((r) => r.trim())
|
|
537
|
+
: [];
|
|
538
|
+
const classifiers = data["Classifier"] && data["Classifier"].trim()
|
|
539
|
+
? data["Classifier"].split(",").map((c) => c.trim())
|
|
540
|
+
: [];
|
|
298
541
|
return {
|
|
299
542
|
name: data["Name"] || "",
|
|
300
543
|
version: data["Version"] || "",
|
|
301
544
|
summary: data["Summary"] || "",
|
|
302
545
|
homepage: data["Home-page"] || undefined,
|
|
303
546
|
author: data["Author"] || undefined,
|
|
547
|
+
authorEmail: data["Author-email"] || undefined,
|
|
304
548
|
license: data["License"] || undefined,
|
|
305
549
|
location: data["Location"] || undefined,
|
|
306
550
|
requires,
|
|
551
|
+
requiredBy: requiredBy.length > 0 ? requiredBy : undefined,
|
|
552
|
+
metadataVersion: data["Metadata-Version"] || undefined,
|
|
553
|
+
classifiers: classifiers.length > 0 ? classifiers : undefined,
|
|
554
|
+
};
|
|
555
|
+
}
|
|
556
|
+
/** Parses `pip show <package>` key-value output into structured package metadata.
|
|
557
|
+
* Supports multiple packages separated by `---` delimiter. */
|
|
558
|
+
export function parsePipShowOutput(stdout, exitCode) {
|
|
559
|
+
// Split on `---` separator for multi-package output
|
|
560
|
+
const blocks = stdout
|
|
561
|
+
.split(/^---$/m)
|
|
562
|
+
.map((b) => b.trim())
|
|
563
|
+
.filter(Boolean);
|
|
564
|
+
const packages = blocks.map(parseSinglePipShowBlock);
|
|
565
|
+
// Use first package for backward-compatible top-level fields
|
|
566
|
+
const first = packages.length > 0
|
|
567
|
+
? packages[0]
|
|
568
|
+
: {
|
|
569
|
+
name: "",
|
|
570
|
+
version: "",
|
|
571
|
+
summary: "",
|
|
572
|
+
requires: [],
|
|
573
|
+
};
|
|
574
|
+
const hasName = Boolean(first.name);
|
|
575
|
+
return {
|
|
576
|
+
success: exitCode === 0 && hasName,
|
|
577
|
+
packages,
|
|
578
|
+
// Backward-compat top-level fields from first package
|
|
579
|
+
name: first.name,
|
|
580
|
+
version: first.version,
|
|
581
|
+
summary: first.summary,
|
|
582
|
+
homepage: first.homepage,
|
|
583
|
+
author: first.author,
|
|
584
|
+
authorEmail: first.authorEmail,
|
|
585
|
+
license: first.license,
|
|
586
|
+
location: first.location,
|
|
587
|
+
requires: first.requires,
|
|
588
|
+
requiredBy: first.requiredBy,
|
|
589
|
+
metadataVersion: first.metadataVersion,
|
|
590
|
+
classifiers: first.classifiers,
|
|
307
591
|
};
|
|
308
592
|
}
|
|
309
593
|
/** Parses `pyenv` command output into structured version management data. */
|
|
@@ -347,6 +631,16 @@ export function parsePyenvOutput(stdout, stderr, exitCode, action) {
|
|
|
347
631
|
.trim();
|
|
348
632
|
return { action, success: true, current: ver || undefined };
|
|
349
633
|
}
|
|
634
|
+
case "installList": {
|
|
635
|
+
const availableVersions = [];
|
|
636
|
+
for (const line of stdout.split("\n")) {
|
|
637
|
+
const trimmed = line.trim();
|
|
638
|
+
if (trimmed && !trimmed.startsWith("Available versions:") && !trimmed.startsWith("--")) {
|
|
639
|
+
availableVersions.push(trimmed);
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
return { action, success: true, availableVersions };
|
|
643
|
+
}
|
|
350
644
|
case "install": {
|
|
351
645
|
// pyenv install outputs to stderr typically
|
|
352
646
|
const installed = output.match(/Installed Python-(\S+)/)?.[1] || output.match(/Installing Python-(\S+)/)?.[1];
|
|
@@ -356,34 +650,73 @@ export function parsePyenvOutput(stdout, stderr, exitCode, action) {
|
|
|
356
650
|
installed: installed || undefined,
|
|
357
651
|
};
|
|
358
652
|
}
|
|
653
|
+
case "uninstall": {
|
|
654
|
+
// pyenv uninstall -f <version>
|
|
655
|
+
// On success, output may contain "pyenv: remove <version>" or similar
|
|
656
|
+
const version = output.match(/remove\s+(\S+)/)?.[1] ||
|
|
657
|
+
output.match(/uninstall(?:ed)?\s+(\S+)/i)?.[1] ||
|
|
658
|
+
// If no match, try extracting from stderr which often has the version
|
|
659
|
+
stderr.match(/(\d+\.\d+\.\d+\S*)/)?.[1] ||
|
|
660
|
+
stdout.match(/(\d+\.\d+\.\d+\S*)/)?.[1];
|
|
661
|
+
return {
|
|
662
|
+
action,
|
|
663
|
+
success: true,
|
|
664
|
+
uninstalled: version || undefined,
|
|
665
|
+
};
|
|
666
|
+
}
|
|
359
667
|
case "local": {
|
|
360
668
|
return { action, success: true, localVersion: stdout.trim() || undefined };
|
|
361
669
|
}
|
|
362
670
|
case "global": {
|
|
363
671
|
return { action, success: true, globalVersion: stdout.trim() || undefined };
|
|
364
672
|
}
|
|
673
|
+
case "which": {
|
|
674
|
+
const commandPath = stdout.trim();
|
|
675
|
+
return { action, success: true, commandPath: commandPath || undefined };
|
|
676
|
+
}
|
|
677
|
+
case "rehash": {
|
|
678
|
+
return { action, success: true };
|
|
679
|
+
}
|
|
365
680
|
}
|
|
366
681
|
}
|
|
367
|
-
const
|
|
368
|
-
|
|
682
|
+
const RUFF_FORMAT_WOULD_RE = /^Would reformat: (.+)$/;
|
|
683
|
+
const RUFF_FORMAT_DID_RE = /^reformatted: (.+)$/;
|
|
684
|
+
/** Parses `ruff format` output into structured result with file counts.
|
|
685
|
+
* Distinguishes check mode ("Would reformat") from fix mode ("reformatted"). */
|
|
369
686
|
export function parseRuffFormatOutput(stdout, stderr, exitCode) {
|
|
370
687
|
// ruff format writes file-level output to stderr
|
|
371
688
|
const output = stdout + "\n" + stderr;
|
|
372
689
|
const lines = output.split("\n");
|
|
373
690
|
const files = [];
|
|
691
|
+
let checkMode = false;
|
|
374
692
|
for (const line of lines) {
|
|
375
|
-
const
|
|
376
|
-
if (
|
|
377
|
-
files.push(
|
|
693
|
+
const wouldMatch = line.match(RUFF_FORMAT_WOULD_RE);
|
|
694
|
+
if (wouldMatch) {
|
|
695
|
+
files.push(wouldMatch[1].trim());
|
|
696
|
+
checkMode = true;
|
|
697
|
+
continue;
|
|
698
|
+
}
|
|
699
|
+
const didMatch = line.match(RUFF_FORMAT_DID_RE);
|
|
700
|
+
if (didMatch) {
|
|
701
|
+
files.push(didMatch[1].trim());
|
|
378
702
|
}
|
|
379
703
|
}
|
|
704
|
+
// Also detect check mode from summary line
|
|
705
|
+
if (output.includes("would be reformatted")) {
|
|
706
|
+
checkMode = true;
|
|
707
|
+
}
|
|
380
708
|
// Parse summary line: "N files reformatted" or "N files would be reformatted" or "N files left unchanged"
|
|
381
709
|
const reformattedMatch = output.match(/(\d+) files? (?:would be )?reformatted/);
|
|
382
710
|
const filesChanged = reformattedMatch ? parseInt(reformattedMatch[1], 10) : files.length;
|
|
711
|
+
// Parse unchanged count: "N files left unchanged" or "N files would be left unchanged"
|
|
712
|
+
const unchangedMatch = output.match(/(\d+) files? (?:would be )?left unchanged/);
|
|
713
|
+
const filesUnchanged = unchangedMatch ? parseInt(unchangedMatch[1], 10) : 0;
|
|
383
714
|
return {
|
|
384
715
|
success: exitCode === 0,
|
|
385
716
|
filesChanged: filesChanged || files.length,
|
|
717
|
+
filesUnchanged,
|
|
386
718
|
files: files.length > 0 ? files : undefined,
|
|
719
|
+
checkMode,
|
|
387
720
|
};
|
|
388
721
|
}
|
|
389
722
|
/** Parses `conda list --json` output into structured package list. */
|
|
@@ -392,11 +725,18 @@ export function parseCondaListJson(stdout, envName) {
|
|
|
392
725
|
try {
|
|
393
726
|
entries = JSON.parse(stdout);
|
|
394
727
|
}
|
|
395
|
-
catch {
|
|
396
|
-
|
|
728
|
+
catch (err) {
|
|
729
|
+
const parseError = err instanceof Error ? err.message : "Failed to parse conda list JSON output";
|
|
730
|
+
return { action: "list", packages: [], total: 0, environment: envName, parseError };
|
|
397
731
|
}
|
|
398
732
|
if (!Array.isArray(entries)) {
|
|
399
|
-
return {
|
|
733
|
+
return {
|
|
734
|
+
action: "list",
|
|
735
|
+
packages: [],
|
|
736
|
+
total: 0,
|
|
737
|
+
environment: envName,
|
|
738
|
+
parseError: "conda list output is not a JSON array",
|
|
739
|
+
};
|
|
400
740
|
}
|
|
401
741
|
const packages = entries.map((e) => ({
|
|
402
742
|
name: e.name,
|
|
@@ -412,7 +752,8 @@ export function parseCondaInfoJson(stdout) {
|
|
|
412
752
|
try {
|
|
413
753
|
data = JSON.parse(stdout);
|
|
414
754
|
}
|
|
415
|
-
catch {
|
|
755
|
+
catch (err) {
|
|
756
|
+
const parseError = err instanceof Error ? err.message : "Failed to parse conda info JSON output";
|
|
416
757
|
return {
|
|
417
758
|
action: "info",
|
|
418
759
|
condaVersion: "",
|
|
@@ -422,6 +763,7 @@ export function parseCondaInfoJson(stdout) {
|
|
|
422
763
|
channels: [],
|
|
423
764
|
envsDirs: [],
|
|
424
765
|
pkgsDirs: [],
|
|
766
|
+
parseError,
|
|
425
767
|
};
|
|
426
768
|
}
|
|
427
769
|
return {
|
|
@@ -442,8 +784,9 @@ export function parseCondaEnvListJson(stdout, activePrefix) {
|
|
|
442
784
|
try {
|
|
443
785
|
data = JSON.parse(stdout);
|
|
444
786
|
}
|
|
445
|
-
catch {
|
|
446
|
-
|
|
787
|
+
catch (err) {
|
|
788
|
+
const parseError = err instanceof Error ? err.message : "Failed to parse conda env list JSON output";
|
|
789
|
+
return { action: "env-list", environments: [], total: 0, parseError };
|
|
447
790
|
}
|
|
448
791
|
const envs = (data.envs ?? []).map((envPath) => {
|
|
449
792
|
// Extract name from the last path segment, or "base" for the root env
|
|
@@ -457,9 +800,104 @@ export function parseCondaEnvListJson(stdout, activePrefix) {
|
|
|
457
800
|
});
|
|
458
801
|
return { action: "env-list", environments: envs, total: envs.length };
|
|
459
802
|
}
|
|
803
|
+
/** Parses `conda create/remove/update --json` output into structured mutation summaries. */
|
|
804
|
+
export function parseCondaMutationJson(stdout, stderr, action, environment, prefix) {
|
|
805
|
+
let data;
|
|
806
|
+
try {
|
|
807
|
+
data = stdout.trim() ? JSON.parse(stdout) : {};
|
|
808
|
+
}
|
|
809
|
+
catch (err) {
|
|
810
|
+
const parseError = err instanceof Error ? err.message : `Failed to parse conda ${action} JSON output`;
|
|
811
|
+
if (action === "create") {
|
|
812
|
+
return {
|
|
813
|
+
action,
|
|
814
|
+
success: false,
|
|
815
|
+
environment,
|
|
816
|
+
prefix,
|
|
817
|
+
totalAdded: 0,
|
|
818
|
+
error: stderr.trim() || undefined,
|
|
819
|
+
parseError,
|
|
820
|
+
};
|
|
821
|
+
}
|
|
822
|
+
if (action === "remove") {
|
|
823
|
+
return {
|
|
824
|
+
action,
|
|
825
|
+
success: false,
|
|
826
|
+
environment,
|
|
827
|
+
prefix,
|
|
828
|
+
totalRemoved: 0,
|
|
829
|
+
error: stderr.trim() || undefined,
|
|
830
|
+
parseError,
|
|
831
|
+
};
|
|
832
|
+
}
|
|
833
|
+
return {
|
|
834
|
+
action,
|
|
835
|
+
success: false,
|
|
836
|
+
environment,
|
|
837
|
+
prefix,
|
|
838
|
+
totalUpdated: 0,
|
|
839
|
+
error: stderr.trim() || undefined,
|
|
840
|
+
parseError,
|
|
841
|
+
};
|
|
842
|
+
}
|
|
843
|
+
const linked = (data.actions?.LINK ?? []).map((p) => ({
|
|
844
|
+
name: p.name ?? "",
|
|
845
|
+
version: p.version ?? undefined,
|
|
846
|
+
channel: p.channel ?? undefined,
|
|
847
|
+
buildString: p.build_string ?? undefined,
|
|
848
|
+
}));
|
|
849
|
+
const unlinked = (data.actions?.UNLINK ?? []).map((p) => ({
|
|
850
|
+
name: p.name ?? "",
|
|
851
|
+
version: p.version ?? undefined,
|
|
852
|
+
channel: p.channel ?? undefined,
|
|
853
|
+
buildString: p.build_string ?? undefined,
|
|
854
|
+
}));
|
|
855
|
+
const success = data.success !== false && !data.error;
|
|
856
|
+
const outPrefix = data.prefix ?? prefix;
|
|
857
|
+
const error = data.error ?? (success ? undefined : stderr.trim() || undefined);
|
|
858
|
+
if (action === "create") {
|
|
859
|
+
return {
|
|
860
|
+
action,
|
|
861
|
+
success,
|
|
862
|
+
environment,
|
|
863
|
+
prefix: outPrefix,
|
|
864
|
+
addedPackages: linked.length > 0 ? linked : undefined,
|
|
865
|
+
totalAdded: linked.length,
|
|
866
|
+
error,
|
|
867
|
+
};
|
|
868
|
+
}
|
|
869
|
+
if (action === "remove") {
|
|
870
|
+
return {
|
|
871
|
+
action,
|
|
872
|
+
success,
|
|
873
|
+
environment,
|
|
874
|
+
prefix: outPrefix,
|
|
875
|
+
removedPackages: unlinked.length > 0 ? unlinked : undefined,
|
|
876
|
+
totalRemoved: unlinked.length,
|
|
877
|
+
error,
|
|
878
|
+
};
|
|
879
|
+
}
|
|
880
|
+
const updatedPackages = linked.filter((l) => unlinked.some((u) => u.name === l.name));
|
|
881
|
+
const addedPackages = linked.filter((l) => !unlinked.some((u) => u.name === l.name));
|
|
882
|
+
const removedPackages = unlinked.filter((u) => !linked.some((l) => l.name === u.name));
|
|
883
|
+
return {
|
|
884
|
+
action,
|
|
885
|
+
success,
|
|
886
|
+
environment,
|
|
887
|
+
prefix: outPrefix,
|
|
888
|
+
updatedPackages: updatedPackages.length > 0 ? updatedPackages : undefined,
|
|
889
|
+
addedPackages: addedPackages.length > 0 ? addedPackages : undefined,
|
|
890
|
+
removedPackages: removedPackages.length > 0 ? removedPackages : undefined,
|
|
891
|
+
totalUpdated: updatedPackages.length + addedPackages.length + removedPackages.length,
|
|
892
|
+
error,
|
|
893
|
+
};
|
|
894
|
+
}
|
|
460
895
|
// ─── poetry parsers ──────────────────────────────────────────────────────────
|
|
461
|
-
/**
|
|
462
|
-
|
|
896
|
+
/** Strict regex matching `poetry show --no-ansi` lines: "package-name version description"
|
|
897
|
+
* Anchored to start, requires package name to start with a letter/digit, and version
|
|
898
|
+
* to look like a version string (digit-prefixed). This avoids matching status lines,
|
|
899
|
+
* blank lines, or other non-package output. */
|
|
900
|
+
const POETRY_SHOW_RE = /^([a-zA-Z0-9][a-zA-Z0-9._-]*)\s+(\d[a-zA-Z0-9._-]*)(?:\s+(.+))?$/;
|
|
463
901
|
/** Regex matching `poetry build` artifact lines: " - Built package-1.0.0.tar.gz" or " Built package-1.0.0-py3-none-any.whl" */
|
|
464
902
|
const POETRY_BUILT_RE = /Built\s+(\S+)/;
|
|
465
903
|
/** Regex matching `poetry add/remove` installed/removed lines with version e.g. " - Installing requests (2.31.0)" */
|
|
@@ -471,9 +909,17 @@ export function parsePoetryOutput(stdout, stderr, exitCode, action) {
|
|
|
471
909
|
if (action === "show") {
|
|
472
910
|
const packages = [];
|
|
473
911
|
for (const line of lines) {
|
|
474
|
-
const
|
|
912
|
+
const trimmed = line.trim();
|
|
913
|
+
// Skip empty lines and lines that don't look like package entries
|
|
914
|
+
if (!trimmed)
|
|
915
|
+
continue;
|
|
916
|
+
const match = trimmed.match(POETRY_SHOW_RE);
|
|
475
917
|
if (match) {
|
|
476
|
-
packages.push({
|
|
918
|
+
packages.push({
|
|
919
|
+
name: match[1],
|
|
920
|
+
version: match[2],
|
|
921
|
+
description: match[3]?.trim() || undefined,
|
|
922
|
+
});
|
|
477
923
|
}
|
|
478
924
|
}
|
|
479
925
|
return {
|
|
@@ -498,7 +944,16 @@ export function parsePoetryOutput(stdout, stderr, exitCode, action) {
|
|
|
498
944
|
total: artifacts.length,
|
|
499
945
|
};
|
|
500
946
|
}
|
|
501
|
-
|
|
947
|
+
if (action === "check" || action === "lock" || action === "export") {
|
|
948
|
+
const messages = lines.map((l) => l.trim()).filter(Boolean);
|
|
949
|
+
return {
|
|
950
|
+
success: exitCode === 0,
|
|
951
|
+
action,
|
|
952
|
+
messages: messages.length > 0 ? messages : undefined,
|
|
953
|
+
total: messages.length,
|
|
954
|
+
};
|
|
955
|
+
}
|
|
956
|
+
// install, add, remove, update — parse installed/updated/removed packages
|
|
502
957
|
const packages = [];
|
|
503
958
|
for (const line of lines) {
|
|
504
959
|
const match = line.match(POETRY_INSTALL_LINE_RE);
|