@blzsky/variant 0.2.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/LICENSE +20 -0
- package/README.md +177 -0
- package/dist/build-2LO7GD2S.js +63 -0
- package/dist/check-ZTKGGF4M.js +44 -0
- package/dist/chunk-3E5LBU3O.js +180 -0
- package/dist/chunk-4LLK2S4R.js +630 -0
- package/dist/chunk-6J4XDG44.js +14 -0
- package/dist/chunk-CXNXZLDG.js +13 -0
- package/dist/chunk-DRMASVYN.js +25 -0
- package/dist/chunk-GDSZFIHM.js +29 -0
- package/dist/chunk-ISRF5F3Q.js +68 -0
- package/dist/chunk-KSHWC2XN.js +78 -0
- package/dist/chunk-LJDDS4RR.js +91 -0
- package/dist/chunk-LXKLCLL6.js +91 -0
- package/dist/chunk-MZZOE7L4.js +26 -0
- package/dist/chunk-NSNZOH5P.js +31 -0
- package/dist/chunk-O4LZKXNZ.js +301 -0
- package/dist/chunk-RGD422OM.js +108 -0
- package/dist/chunk-RVXUCJYA.js +81 -0
- package/dist/chunk-SWPVR2DF.js +468 -0
- package/dist/chunk-V22IPJ7A.js +64 -0
- package/dist/chunk-V5CTGXMM.js +256 -0
- package/dist/chunk-VTJ67TT5.js +238 -0
- package/dist/chunk-XTYVVUWW.js +279 -0
- package/dist/chunk-XUBWCTJZ.js +100 -0
- package/dist/chunk-YYGCSKIQ.js +29 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +225 -0
- package/dist/diff-GFI6RAEA.js +60 -0
- package/dist/doctor-GH4RZVEH.js +127 -0
- package/dist/env-QORDD6FQ.js +24 -0
- package/dist/impact-AF453PLK.js +702 -0
- package/dist/index.d.ts +56 -0
- package/dist/index.js +7 -0
- package/dist/init-KJIUNGAN.js +158 -0
- package/dist/insight-DZ5NQY6D.js +29 -0
- package/dist/pr-UFZRZX6S.js +152 -0
- package/dist/run-6PNH4QRA.js +40 -0
- package/dist/symbol-graph-YYUEMZGR.js +22 -0
- package/dist/tsconfig-paths-E3UEDGXZ.js +10 -0
- package/dist/workspace-X5FUBTY3.js +186 -0
- package/package.json +86 -0
|
@@ -0,0 +1,630 @@
|
|
|
1
|
+
import {
|
|
2
|
+
computeInsights,
|
|
3
|
+
evictStaleEntries,
|
|
4
|
+
readCache,
|
|
5
|
+
writeCache,
|
|
6
|
+
writeCacheSync
|
|
7
|
+
} from "./chunk-LXKLCLL6.js";
|
|
8
|
+
import {
|
|
9
|
+
renderInsights
|
|
10
|
+
} from "./chunk-ISRF5F3Q.js";
|
|
11
|
+
import {
|
|
12
|
+
recordCacheMiss,
|
|
13
|
+
runScheduled
|
|
14
|
+
} from "./chunk-SWPVR2DF.js";
|
|
15
|
+
import {
|
|
16
|
+
getColors
|
|
17
|
+
} from "./chunk-YYGCSKIQ.js";
|
|
18
|
+
import {
|
|
19
|
+
mapLimit
|
|
20
|
+
} from "./chunk-GDSZFIHM.js";
|
|
21
|
+
import {
|
|
22
|
+
TaskExecutionError,
|
|
23
|
+
VariantError
|
|
24
|
+
} from "./chunk-RVXUCJYA.js";
|
|
25
|
+
import {
|
|
26
|
+
lines
|
|
27
|
+
} from "./chunk-6J4XDG44.js";
|
|
28
|
+
import {
|
|
29
|
+
getPrinter
|
|
30
|
+
} from "./chunk-V22IPJ7A.js";
|
|
31
|
+
|
|
32
|
+
// src/core/execution/runner.ts
|
|
33
|
+
import { createHash as createHash2 } from "crypto";
|
|
34
|
+
import os from "os";
|
|
35
|
+
|
|
36
|
+
// src/core/cache/hashing.ts
|
|
37
|
+
import { createHash } from "crypto";
|
|
38
|
+
import { readFile } from "fs/promises";
|
|
39
|
+
import path from "path";
|
|
40
|
+
import fg from "fast-glob";
|
|
41
|
+
function assertSafePattern(pattern) {
|
|
42
|
+
if (path.isAbsolute(pattern)) {
|
|
43
|
+
throw new Error(
|
|
44
|
+
`Unsafe glob pattern "${pattern}": patterns must be relative and must not traverse outside the project root.`
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
const segments = pattern.replace(/\\/g, "/").split("/");
|
|
48
|
+
if (segments.some((seg) => seg === "..")) {
|
|
49
|
+
throw new Error(
|
|
50
|
+
`Unsafe glob pattern "${pattern}": patterns must be relative and must not traverse outside the project root.`
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
async function hashTaskInputs(cwd, patterns, options = {}) {
|
|
55
|
+
for (const p of patterns) assertSafePattern(p);
|
|
56
|
+
const files = (await fg(patterns, {
|
|
57
|
+
cwd,
|
|
58
|
+
onlyFiles: true,
|
|
59
|
+
ignore: ["node_modules/**", ".git/**", ".variant/**"]
|
|
60
|
+
})).sort();
|
|
61
|
+
const inScope = filterToScopes(cwd, files, options.packageScopes);
|
|
62
|
+
const limit = Math.max(1, options.parallel ?? 32);
|
|
63
|
+
const digests = new Array(inScope.length);
|
|
64
|
+
let next = 0;
|
|
65
|
+
const worker = async () => {
|
|
66
|
+
while (true) {
|
|
67
|
+
const i = next++;
|
|
68
|
+
if (i >= inScope.length) return;
|
|
69
|
+
const relPath = inScope[i];
|
|
70
|
+
if (relPath === void 0) return;
|
|
71
|
+
digests[i] = sha256(await readFile(path.join(cwd, relPath)));
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
await Promise.all(
|
|
75
|
+
Array.from({ length: Math.min(limit, inScope.length || 1) }, worker)
|
|
76
|
+
);
|
|
77
|
+
const hash = createHash("sha256");
|
|
78
|
+
for (let i = 0; i < inScope.length; i++) {
|
|
79
|
+
const relPath = inScope[i];
|
|
80
|
+
const digest = digests[i];
|
|
81
|
+
if (relPath === void 0 || digest === void 0) continue;
|
|
82
|
+
hash.update(relPath);
|
|
83
|
+
hash.update(digest);
|
|
84
|
+
}
|
|
85
|
+
return hash.digest("hex");
|
|
86
|
+
}
|
|
87
|
+
function filterToScopes(cwd, files, packageScopes) {
|
|
88
|
+
if (!packageScopes?.length) return files;
|
|
89
|
+
const prefixes = packageScopes.map((dir) => path.resolve(dir) + path.sep);
|
|
90
|
+
return files.filter((file) => {
|
|
91
|
+
const absolute = path.resolve(cwd, file);
|
|
92
|
+
return prefixes.some((prefix) => absolute.startsWith(prefix));
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
function sha256(content) {
|
|
96
|
+
return createHash("sha256").update(content).digest("hex");
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// src/core/provenance/attach.ts
|
|
100
|
+
function attachProvenance(err, taskId, provenance) {
|
|
101
|
+
const failure = err instanceof VariantError ? err : new TaskExecutionError(
|
|
102
|
+
taskId,
|
|
103
|
+
1,
|
|
104
|
+
`Task "${taskId}" failed: ${String(err)}`,
|
|
105
|
+
{ cause: err }
|
|
106
|
+
);
|
|
107
|
+
const entry = provenance?.get(taskId);
|
|
108
|
+
if (entry !== void 0) failure.provenance = entry;
|
|
109
|
+
return failure;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// src/core/execution/executor.ts
|
|
113
|
+
var executeTask = async (name, cfg, pm, cwd, onOutput) => {
|
|
114
|
+
const { execa, ExecaError } = await import("execa");
|
|
115
|
+
const { default: stringArgv } = await import("string-argv");
|
|
116
|
+
const command = cfg.command ?? `${pm} run ${name}`;
|
|
117
|
+
const [cmd, ...args] = stringArgv(command);
|
|
118
|
+
if (!cmd) {
|
|
119
|
+
throw new TaskExecutionError(name, 1, `Empty command for task "${name}"`);
|
|
120
|
+
}
|
|
121
|
+
try {
|
|
122
|
+
if (onOutput === void 0) {
|
|
123
|
+
await execa(cmd, args, { cwd, stdio: "inherit" });
|
|
124
|
+
} else {
|
|
125
|
+
const subprocess = execa(cmd, args, { cwd, all: true, buffer: false });
|
|
126
|
+
for await (const line of subprocess.iterable({ from: "all" })) {
|
|
127
|
+
onOutput(line);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
} catch (err) {
|
|
131
|
+
if (err instanceof ExecaError) {
|
|
132
|
+
const exitCode = typeof err.exitCode === "number" ? err.exitCode : 1;
|
|
133
|
+
const signal = typeof err.signal === "string" ? err.signal : null;
|
|
134
|
+
const message = signal ? `Task "${name}" killed by ${signal}` : `Task "${name}" failed with exit code ${exitCode}`;
|
|
135
|
+
throw new TaskExecutionError(name, exitCode, message, { cause: err });
|
|
136
|
+
}
|
|
137
|
+
throw new TaskExecutionError(
|
|
138
|
+
name,
|
|
139
|
+
1,
|
|
140
|
+
`Task "${name}" failed: ${String(err)}`,
|
|
141
|
+
{ cause: err }
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
// src/core/execution/runner.ts
|
|
147
|
+
var sha2562 = (s) => createHash2("sha256").update(s).digest("hex");
|
|
148
|
+
function defaultConcurrency() {
|
|
149
|
+
return Math.max(1, os.cpus().length - 1);
|
|
150
|
+
}
|
|
151
|
+
async function runOneTask(taskName, options, cache, executor) {
|
|
152
|
+
if (options.taskFilter && !options.taskFilter(taskName)) {
|
|
153
|
+
return { task: taskName, durationMs: 0, cacheHit: false, skipped: true };
|
|
154
|
+
}
|
|
155
|
+
const taskCfg = options.tasks[taskName] ?? {};
|
|
156
|
+
const patterns = taskCfg.inputs ?? [];
|
|
157
|
+
const isStrict = options.config.strategy === "strict";
|
|
158
|
+
const plugins = options.plugins;
|
|
159
|
+
const { onTaskOutput } = options;
|
|
160
|
+
const onOutput = onTaskOutput !== void 0 ? (line) => onTaskOutput(taskName, line) : void 0;
|
|
161
|
+
if (plugins) {
|
|
162
|
+
const skip = await plugins.runOnBeforeExecute(taskName);
|
|
163
|
+
if (skip) {
|
|
164
|
+
const result2 = {
|
|
165
|
+
task: taskName,
|
|
166
|
+
durationMs: 0,
|
|
167
|
+
cacheHit: true
|
|
168
|
+
};
|
|
169
|
+
options.onTaskEvent?.({ task: taskName, status: "cached" });
|
|
170
|
+
await plugins.runOnAfterExecute(taskName, result2);
|
|
171
|
+
return result2;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
if (!isStrict && patterns.length > 0) {
|
|
175
|
+
const baseHash = await hashTaskInputs(
|
|
176
|
+
options.cwd,
|
|
177
|
+
patterns,
|
|
178
|
+
options.packageScopes !== void 0 ? { packageScopes: options.packageScopes } : {}
|
|
179
|
+
);
|
|
180
|
+
const extra = plugins ? await plugins.runOnHash(taskName, patterns) : [];
|
|
181
|
+
const hash = extra.length === 0 ? baseHash : sha2562(`${baseHash}\0${extra.join("\0")}`);
|
|
182
|
+
const cached = cache.tasks[taskName];
|
|
183
|
+
if (cached !== void 0 && cached.hash === hash) {
|
|
184
|
+
const result3 = {
|
|
185
|
+
task: taskName,
|
|
186
|
+
durationMs: 0,
|
|
187
|
+
cacheHit: true
|
|
188
|
+
};
|
|
189
|
+
options.onTaskEvent?.({ task: taskName, status: "cached" });
|
|
190
|
+
if (plugins) await plugins.runOnAfterExecute(taskName, result3);
|
|
191
|
+
return result3;
|
|
192
|
+
}
|
|
193
|
+
recordCacheMiss(options.provenance, taskName, cached?.hash ?? null, hash);
|
|
194
|
+
options.onTaskEvent?.({ task: taskName, status: "running" });
|
|
195
|
+
const start2 = Date.now();
|
|
196
|
+
try {
|
|
197
|
+
await executor(taskName, taskCfg, options.pm, options.cwd, onOutput);
|
|
198
|
+
} catch (err) {
|
|
199
|
+
options.onTaskEvent?.({ task: taskName, status: "failed" });
|
|
200
|
+
throw attachProvenance(err, taskName, options.provenance);
|
|
201
|
+
}
|
|
202
|
+
const durationMs2 = Date.now() - start2;
|
|
203
|
+
const entry = { hash, lastRun: Date.now(), lastDurationMs: durationMs2 };
|
|
204
|
+
cache.tasks[taskName] = entry;
|
|
205
|
+
const result2 = {
|
|
206
|
+
task: taskName,
|
|
207
|
+
durationMs: durationMs2,
|
|
208
|
+
cacheHit: false
|
|
209
|
+
};
|
|
210
|
+
options.onTaskEvent?.({ task: taskName, status: "done", durationMs: durationMs2 });
|
|
211
|
+
if (plugins) await plugins.runOnAfterExecute(taskName, result2);
|
|
212
|
+
return result2;
|
|
213
|
+
}
|
|
214
|
+
options.onTaskEvent?.({ task: taskName, status: "running" });
|
|
215
|
+
const start = Date.now();
|
|
216
|
+
try {
|
|
217
|
+
await executor(taskName, taskCfg, options.pm, options.cwd, onOutput);
|
|
218
|
+
} catch (err) {
|
|
219
|
+
options.onTaskEvent?.({ task: taskName, status: "failed" });
|
|
220
|
+
throw attachProvenance(err, taskName, options.provenance);
|
|
221
|
+
}
|
|
222
|
+
const durationMs = Date.now() - start;
|
|
223
|
+
cache.tasks[taskName] = {
|
|
224
|
+
lastRun: Date.now(),
|
|
225
|
+
lastDurationMs: durationMs
|
|
226
|
+
};
|
|
227
|
+
const result = {
|
|
228
|
+
task: taskName,
|
|
229
|
+
durationMs,
|
|
230
|
+
cacheHit: false
|
|
231
|
+
};
|
|
232
|
+
options.onTaskEvent?.({ task: taskName, status: "done", durationMs });
|
|
233
|
+
if (plugins) await plugins.runOnAfterExecute(taskName, result);
|
|
234
|
+
return result;
|
|
235
|
+
}
|
|
236
|
+
async function runTasksWithDeps(target, graph, options, executor = executeTask) {
|
|
237
|
+
const levels = graph.toLevels(target);
|
|
238
|
+
const rawCache = await readCache(options.cacheDir);
|
|
239
|
+
const ttlDays = options.config.cache.ttlDays;
|
|
240
|
+
const cache = ttlDays !== void 0 ? evictStaleEntries(rawCache, ttlDays) : rawCache;
|
|
241
|
+
const results = [];
|
|
242
|
+
const concurrency = options.concurrency ?? defaultConcurrency();
|
|
243
|
+
const flushCache = () => writeCacheSync(options.cacheDir, cache);
|
|
244
|
+
process.on("exit", flushCache);
|
|
245
|
+
try {
|
|
246
|
+
if (options.useScheduler) {
|
|
247
|
+
const byTask = /* @__PURE__ */ new Map();
|
|
248
|
+
await runScheduled(
|
|
249
|
+
target,
|
|
250
|
+
graph,
|
|
251
|
+
async (taskName) => {
|
|
252
|
+
byTask.set(
|
|
253
|
+
taskName,
|
|
254
|
+
await runOneTask(taskName, options, cache, executor)
|
|
255
|
+
);
|
|
256
|
+
},
|
|
257
|
+
{
|
|
258
|
+
concurrency,
|
|
259
|
+
...options.priorityOf !== void 0 ? { priorityOf: options.priorityOf } : {}
|
|
260
|
+
}
|
|
261
|
+
);
|
|
262
|
+
for (const name of levels.flat()) {
|
|
263
|
+
const r = byTask.get(name);
|
|
264
|
+
if (r !== void 0) results.push(r);
|
|
265
|
+
}
|
|
266
|
+
} else {
|
|
267
|
+
for (const level of levels) {
|
|
268
|
+
const levelResults = await mapLimit(
|
|
269
|
+
level,
|
|
270
|
+
concurrency,
|
|
271
|
+
async (taskName) => runOneTask(taskName, options, cache, executor)
|
|
272
|
+
);
|
|
273
|
+
results.push(...levelResults);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
} finally {
|
|
277
|
+
try {
|
|
278
|
+
await writeCache(options.cacheDir, cache);
|
|
279
|
+
} catch {
|
|
280
|
+
}
|
|
281
|
+
process.off("exit", flushCache);
|
|
282
|
+
}
|
|
283
|
+
return results;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// src/cli/visuals/ansi.ts
|
|
287
|
+
var ESC = "\x1B";
|
|
288
|
+
var HIDE_CURSOR = `${ESC}[?25l`;
|
|
289
|
+
var SHOW_CURSOR = `${ESC}[?25h`;
|
|
290
|
+
var CLEAR_LINE = `${ESC}[2K`;
|
|
291
|
+
var SYNC_START = `${ESC}[?2026h`;
|
|
292
|
+
var SYNC_END = `${ESC}[?2026l`;
|
|
293
|
+
function cursorUp(lines2) {
|
|
294
|
+
return lines2 > 0 ? `${ESC}[${lines2}A` : "";
|
|
295
|
+
}
|
|
296
|
+
var STYLE_PATTERN = new RegExp(`${ESC}\\[[0-9;]*m`, "g");
|
|
297
|
+
function stripStyles(text) {
|
|
298
|
+
return text.replace(STYLE_PATTERN, "");
|
|
299
|
+
}
|
|
300
|
+
function visibleLength(line) {
|
|
301
|
+
return stripStyles(line).length;
|
|
302
|
+
}
|
|
303
|
+
function fitWidth(line, columns) {
|
|
304
|
+
if (visibleLength(line) <= columns) return line;
|
|
305
|
+
return stripStyles(line).slice(0, Math.max(0, columns - 1));
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// src/cli/visuals/progress.ts
|
|
309
|
+
var SPINNER_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
310
|
+
var TICK_MS = 80;
|
|
311
|
+
var BAR_WIDTH = 30;
|
|
312
|
+
function humanReadableBytes(bytes) {
|
|
313
|
+
const units = ["B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB"];
|
|
314
|
+
if (bytes < 1) return [bytes, "B"];
|
|
315
|
+
const exponent = Math.min(
|
|
316
|
+
Math.floor(Math.log2(bytes) / 10),
|
|
317
|
+
units.length - 1
|
|
318
|
+
);
|
|
319
|
+
return [bytes / 1024 ** exponent, units[exponent] ?? "B"];
|
|
320
|
+
}
|
|
321
|
+
function formatBytes(bytes) {
|
|
322
|
+
const [value, unit] = humanReadableBytes(bytes);
|
|
323
|
+
const rounded = value >= 100 ? value.toFixed(0) : value.toFixed(1);
|
|
324
|
+
return `${rounded} ${unit}`;
|
|
325
|
+
}
|
|
326
|
+
var DIRECTION_LABELS = {
|
|
327
|
+
download: { active: "Downloading", done: "Downloaded" },
|
|
328
|
+
upload: { active: "Uploading", done: "Uploaded" }
|
|
329
|
+
};
|
|
330
|
+
var BlockRenderer = class {
|
|
331
|
+
constructor(stream) {
|
|
332
|
+
this.stream = stream;
|
|
333
|
+
}
|
|
334
|
+
stream;
|
|
335
|
+
renderedLineCount = 0;
|
|
336
|
+
/**
|
|
337
|
+
* Emit one frame as a single write, wrapped in a synchronized update so
|
|
338
|
+
* the terminal paints it atomically. Permanent lines overwrite the top of
|
|
339
|
+
* the previous block and scroll away; the live block is redrawn below.
|
|
340
|
+
*/
|
|
341
|
+
frame(permanentLines, liveLines) {
|
|
342
|
+
const columns = this.stream.columns ?? 80;
|
|
343
|
+
let out = SYNC_START + cursorUp(this.renderedLineCount);
|
|
344
|
+
for (const line of permanentLines) {
|
|
345
|
+
out += `${CLEAR_LINE}${fitWidth(line, columns)}
|
|
346
|
+
`;
|
|
347
|
+
}
|
|
348
|
+
for (const line of liveLines) {
|
|
349
|
+
out += `${CLEAR_LINE}${fitWidth(line, columns)}
|
|
350
|
+
`;
|
|
351
|
+
}
|
|
352
|
+
const leftover = this.renderedLineCount - permanentLines.length - liveLines.length;
|
|
353
|
+
if (leftover > 0) {
|
|
354
|
+
out += `${CLEAR_LINE}
|
|
355
|
+
`.repeat(leftover) + cursorUp(leftover);
|
|
356
|
+
}
|
|
357
|
+
this.stream.write(out + SYNC_END);
|
|
358
|
+
this.renderedLineCount = liveLines.length;
|
|
359
|
+
}
|
|
360
|
+
repaint(lines2) {
|
|
361
|
+
this.frame([], lines2);
|
|
362
|
+
}
|
|
363
|
+
clear() {
|
|
364
|
+
this.frame([], []);
|
|
365
|
+
}
|
|
366
|
+
};
|
|
367
|
+
function detectSingleMode() {
|
|
368
|
+
const session = process.env["JPY_SESSION_NAME"];
|
|
369
|
+
return session !== void 0 && session !== "";
|
|
370
|
+
}
|
|
371
|
+
function testProgressSuppressed() {
|
|
372
|
+
const flag = process.env["VARIANT_TEST_NO_CLI_PROGRESS"];
|
|
373
|
+
return flag !== void 0 && flag !== "";
|
|
374
|
+
}
|
|
375
|
+
var ProgressReporter = class _ProgressReporter {
|
|
376
|
+
printer;
|
|
377
|
+
renderer;
|
|
378
|
+
singleMode;
|
|
379
|
+
/** Pinned header lines (pre-rendered), in insertion order. */
|
|
380
|
+
tasks = /* @__PURE__ */ new Map();
|
|
381
|
+
requests = /* @__PURE__ */ new Map();
|
|
382
|
+
/** Permanent lines queued for the next frame (flushed in one write). */
|
|
383
|
+
pendingLogs = [];
|
|
384
|
+
message;
|
|
385
|
+
nextId = 0;
|
|
386
|
+
maxNameLength = 0;
|
|
387
|
+
frame = 0;
|
|
388
|
+
timer;
|
|
389
|
+
finished = false;
|
|
390
|
+
constructor(printer, message, options) {
|
|
391
|
+
this.printer = printer;
|
|
392
|
+
this.message = message;
|
|
393
|
+
this.singleMode = options.singleMode ?? detectSingleMode();
|
|
394
|
+
if (printer.progressEnabled) {
|
|
395
|
+
this.renderer = new BlockRenderer(printer.progressStream);
|
|
396
|
+
printer.progressStream.write(HIDE_CURSOR);
|
|
397
|
+
this.timer = setInterval(() => this.tick(), TICK_MS);
|
|
398
|
+
this.timer.unref();
|
|
399
|
+
this.repaint();
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
/** Build a reporter with a root spinner, e.g. for a resolve/prepare step. */
|
|
403
|
+
static spinner(printer, message, options = {}) {
|
|
404
|
+
return new _ProgressReporter(printer, message, options);
|
|
405
|
+
}
|
|
406
|
+
/** Update the root spinner's message (e.g. the item currently in flight). */
|
|
407
|
+
setMessage(message) {
|
|
408
|
+
this.message = message;
|
|
409
|
+
this.repaint();
|
|
410
|
+
}
|
|
411
|
+
/** Finish and clear the whole block, restoring the cursor. */
|
|
412
|
+
finish() {
|
|
413
|
+
if (this.finished) return;
|
|
414
|
+
if (this.timer !== void 0) clearInterval(this.timer);
|
|
415
|
+
if (this.renderer !== void 0) {
|
|
416
|
+
this.renderer.frame(this.pendingLogs.splice(0), []);
|
|
417
|
+
this.printer.progressStream.write(SHOW_CURSOR);
|
|
418
|
+
}
|
|
419
|
+
this.finished = true;
|
|
420
|
+
}
|
|
421
|
+
/**
|
|
422
|
+
* Pin a fully custom, pre-rendered header line above the item bars.
|
|
423
|
+
* Returns an ID to pass to {@link finishTask} / {@link completeTask}.
|
|
424
|
+
*/
|
|
425
|
+
pinLine(line) {
|
|
426
|
+
const id = ++this.nextId;
|
|
427
|
+
if (this.singleMode) return id;
|
|
428
|
+
this.tasks.set(id, line);
|
|
429
|
+
if (this.renderer === void 0) {
|
|
430
|
+
this.logLine(line);
|
|
431
|
+
return id;
|
|
432
|
+
}
|
|
433
|
+
this.repaint();
|
|
434
|
+
return id;
|
|
435
|
+
}
|
|
436
|
+
/**
|
|
437
|
+
* Start a labeled header (e.g. "Building foo") pinned above the item bars.
|
|
438
|
+
* Returns an ID to pass to {@link finishTask}.
|
|
439
|
+
*/
|
|
440
|
+
startTask(verb, subject) {
|
|
441
|
+
const colors = getColors();
|
|
442
|
+
return this.pinLine(` ${colors.bold(colors.cyan(verb))} ${subject}`);
|
|
443
|
+
}
|
|
444
|
+
/** Complete a task started with {@link startTask}, leaving a done line. */
|
|
445
|
+
finishTask(verb, subject, id) {
|
|
446
|
+
const colors = getColors();
|
|
447
|
+
this.completeTask(
|
|
448
|
+
id,
|
|
449
|
+
` ${colors.bold(colors.green(verb))} ${subject}`
|
|
450
|
+
);
|
|
451
|
+
}
|
|
452
|
+
/**
|
|
453
|
+
* Complete a task started with {@link startTask} with a fully custom line
|
|
454
|
+
* (e.g. a red failure line): removes the header and logs the line.
|
|
455
|
+
*/
|
|
456
|
+
completeTask(id, line) {
|
|
457
|
+
if (this.singleMode) return;
|
|
458
|
+
this.tasks.delete(id);
|
|
459
|
+
this.logLine(line);
|
|
460
|
+
}
|
|
461
|
+
/**
|
|
462
|
+
* Print a permanent line above the live block (indicatif's `println`) — or
|
|
463
|
+
* as a plain stderr line when bars are hidden.
|
|
464
|
+
*/
|
|
465
|
+
log(line) {
|
|
466
|
+
this.logLine(line);
|
|
467
|
+
}
|
|
468
|
+
/**
|
|
469
|
+
* Start tracking a download/upload with an optional known size. Returns an
|
|
470
|
+
* ID to pass to {@link requestProgress} / {@link requestComplete}.
|
|
471
|
+
*/
|
|
472
|
+
requestStart(direction, name, size) {
|
|
473
|
+
const id = ++this.nextId;
|
|
474
|
+
if (this.singleMode) return id;
|
|
475
|
+
this.requests.set(id, { direction, name, size, transferred: 0 });
|
|
476
|
+
this.maxNameLength = Math.max(this.maxNameLength, name.length);
|
|
477
|
+
this.repaint();
|
|
478
|
+
return id;
|
|
479
|
+
}
|
|
480
|
+
/** Record `bytes` more transferred for a request. */
|
|
481
|
+
requestProgress(id, bytes) {
|
|
482
|
+
const request = this.requests.get(id);
|
|
483
|
+
if (request === void 0) return;
|
|
484
|
+
request.transferred += bytes;
|
|
485
|
+
this.repaint();
|
|
486
|
+
}
|
|
487
|
+
/** Complete and clear a request bar. */
|
|
488
|
+
requestComplete(id) {
|
|
489
|
+
const request = this.requests.get(id);
|
|
490
|
+
if (request === void 0) return;
|
|
491
|
+
this.requests.delete(id);
|
|
492
|
+
if (this.renderer === void 0) {
|
|
493
|
+
const colors = getColors();
|
|
494
|
+
const label = DIRECTION_LABELS[request.direction].done;
|
|
495
|
+
const detail = request.size !== void 0 ? ` (${formatBytes(request.size)})` : "";
|
|
496
|
+
this.logLine(
|
|
497
|
+
`${colors.bold(colors.green(label))} ${request.name}${colors.dim(detail)}`
|
|
498
|
+
);
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
501
|
+
this.repaint();
|
|
502
|
+
}
|
|
503
|
+
/**
|
|
504
|
+
* Print a permanent line above the live block — or as a plain stderr line
|
|
505
|
+
* when bars are hidden, so output degrades gracefully in pipes and CI.
|
|
506
|
+
* Visible-mode lines are queued and flushed inside the next frame's single
|
|
507
|
+
* write, so the terminal never paints an intermediate state.
|
|
508
|
+
*/
|
|
509
|
+
logLine(line) {
|
|
510
|
+
if (this.renderer === void 0) {
|
|
511
|
+
if (!testProgressSuppressed()) this.printer.stderr(`${line}
|
|
512
|
+
`);
|
|
513
|
+
return;
|
|
514
|
+
}
|
|
515
|
+
this.pendingLogs.push(line);
|
|
516
|
+
this.repaint();
|
|
517
|
+
}
|
|
518
|
+
tick() {
|
|
519
|
+
this.frame = (this.frame + 1) % SPINNER_FRAMES.length;
|
|
520
|
+
this.repaint();
|
|
521
|
+
}
|
|
522
|
+
repaint() {
|
|
523
|
+
if (this.renderer === void 0 || this.finished) return;
|
|
524
|
+
this.renderer.frame(this.pendingLogs.splice(0), this.composeLines());
|
|
525
|
+
}
|
|
526
|
+
composeLines() {
|
|
527
|
+
const colors = getColors();
|
|
528
|
+
const lines2 = [...this.tasks.values()];
|
|
529
|
+
const requests = [...this.requests.values()].sort(
|
|
530
|
+
(a, b) => (a.size ?? 0) - (b.size ?? 0)
|
|
531
|
+
);
|
|
532
|
+
for (const request of requests) {
|
|
533
|
+
lines2.push(this.composeRequestLine(request, colors));
|
|
534
|
+
}
|
|
535
|
+
const spinnerFrame = SPINNER_FRAMES[this.frame] ?? "\u280B";
|
|
536
|
+
lines2.push(`${spinnerFrame} ${colors.dim(this.message)}`);
|
|
537
|
+
return lines2;
|
|
538
|
+
}
|
|
539
|
+
composeRequestLine(request, colors) {
|
|
540
|
+
const name = colors.dim(request.name.padEnd(this.maxNameLength));
|
|
541
|
+
if (request.size === void 0) {
|
|
542
|
+
return `${name} ${colors.dim("....")}`;
|
|
543
|
+
}
|
|
544
|
+
const ratio = request.size === 0 ? 1 : Math.min(request.transferred / request.size, 1);
|
|
545
|
+
const filled = Math.round(ratio * BAR_WIDTH);
|
|
546
|
+
const bar = colors.green("\u2588".repeat(filled)) + colors.dim("\u2591".repeat(BAR_WIDTH - filled));
|
|
547
|
+
const transferred = formatBytes(request.transferred).padStart(9);
|
|
548
|
+
return `${name} ${bar} ${transferred}/${formatBytes(request.size)}`;
|
|
549
|
+
}
|
|
550
|
+
};
|
|
551
|
+
|
|
552
|
+
// src/cli/visuals/task-events.ts
|
|
553
|
+
function createTaskEventProgress(message, printer = getPrinter()) {
|
|
554
|
+
const reporter = ProgressReporter.spinner(printer, message);
|
|
555
|
+
const running = /* @__PURE__ */ new Map();
|
|
556
|
+
const completeWith = (task, line) => {
|
|
557
|
+
const id = running.get(task);
|
|
558
|
+
running.delete(task);
|
|
559
|
+
if (id !== void 0) reporter.completeTask(id, line);
|
|
560
|
+
else reporter.log(line);
|
|
561
|
+
};
|
|
562
|
+
const onTaskEvent = ({ task, status, durationMs }) => {
|
|
563
|
+
const colors = getColors();
|
|
564
|
+
switch (status) {
|
|
565
|
+
case "running":
|
|
566
|
+
running.set(
|
|
567
|
+
task,
|
|
568
|
+
reporter.pinLine(` ${colors.bold(colors.cyan("\u2192"))} ${task}`)
|
|
569
|
+
);
|
|
570
|
+
break;
|
|
571
|
+
case "cached":
|
|
572
|
+
reporter.log(colors.dim(` \xB7 ${task} [cached]`));
|
|
573
|
+
break;
|
|
574
|
+
case "done": {
|
|
575
|
+
const duration = durationMs !== void 0 ? colors.dim(` (${durationMs}ms)`) : "";
|
|
576
|
+
completeWith(task, `${colors.green(` \u2713 ${task}`)}${duration}`);
|
|
577
|
+
break;
|
|
578
|
+
}
|
|
579
|
+
case "failed":
|
|
580
|
+
completeWith(task, colors.red(` \u2717 ${task}`));
|
|
581
|
+
break;
|
|
582
|
+
}
|
|
583
|
+
};
|
|
584
|
+
const onTaskOutput = printer.progressEnabled ? (task, line) => {
|
|
585
|
+
const colors = getColors();
|
|
586
|
+
reporter.log(`${colors.dim(` ${task} \u2502`)} ${line}`);
|
|
587
|
+
} : void 0;
|
|
588
|
+
return {
|
|
589
|
+
onTaskEvent,
|
|
590
|
+
onTaskOutput,
|
|
591
|
+
finish: () => {
|
|
592
|
+
reporter.finish();
|
|
593
|
+
}
|
|
594
|
+
};
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
// src/cli/execute.ts
|
|
598
|
+
async function executeTarget(target, ctx, runOptions, message) {
|
|
599
|
+
const progress = createTaskEventProgress(message);
|
|
600
|
+
runOptions.onTaskEvent = progress.onTaskEvent;
|
|
601
|
+
if (progress.onTaskOutput !== void 0) {
|
|
602
|
+
runOptions.onTaskOutput = progress.onTaskOutput;
|
|
603
|
+
}
|
|
604
|
+
try {
|
|
605
|
+
return await runTasksWithDeps(target, ctx.graph, runOptions);
|
|
606
|
+
} finally {
|
|
607
|
+
progress.finish();
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
async function reportRunInsights(ctx, results) {
|
|
611
|
+
const cache = await readCache(ctx.cacheDir);
|
|
612
|
+
renderInsights(computeInsights(results, cache));
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
// src/cli/render/plan.ts
|
|
616
|
+
function renderDryRunPlan(target, levels, printer = getPrinter()) {
|
|
617
|
+
lines(
|
|
618
|
+
printer,
|
|
619
|
+
`[dry-run] Task plan for "${target}" (${levels.flat().length} task(s)):`
|
|
620
|
+
);
|
|
621
|
+
for (const [index, level] of levels.entries()) {
|
|
622
|
+
lines(printer, ` Level ${index + 1}: ${level.join(", ")}`);
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
export {
|
|
627
|
+
executeTarget,
|
|
628
|
+
reportRunInsights,
|
|
629
|
+
renderDryRunPlan
|
|
630
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// src/cli/render/writer.ts
|
|
2
|
+
function lines(printer, ...text) {
|
|
3
|
+
for (const t of text) printer.stdout(`${t}
|
|
4
|
+
`);
|
|
5
|
+
}
|
|
6
|
+
function errorLines(printer, ...text) {
|
|
7
|
+
for (const t of text) printer.stderr(`${t}
|
|
8
|
+
`);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export {
|
|
12
|
+
lines,
|
|
13
|
+
errorLines
|
|
14
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// src/cli/render/labels.ts
|
|
2
|
+
var LABEL_WIDTH = "non-impacting".length;
|
|
3
|
+
function classificationLabel(classification) {
|
|
4
|
+
return classification.padEnd(LABEL_WIDTH);
|
|
5
|
+
}
|
|
6
|
+
function plural(count, noun) {
|
|
7
|
+
return `${count.toLocaleString("en-US")} ${noun}${count === 1 ? "" : "s"}`;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export {
|
|
11
|
+
classificationLabel,
|
|
12
|
+
plural
|
|
13
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// src/core/semantic/verdict.ts
|
|
2
|
+
function deriveVerdict(classifications, options = {}) {
|
|
3
|
+
if (options.forceBuild === true) return "build-required";
|
|
4
|
+
let verdict = "safe-to-skip";
|
|
5
|
+
for (const classification of classifications) {
|
|
6
|
+
if (classification === "breaking") return "build-required";
|
|
7
|
+
if (classification === "internal" || classification === "unanalyzed") {
|
|
8
|
+
verdict = "build-recommended";
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
return verdict;
|
|
12
|
+
}
|
|
13
|
+
var VERDICT_TEXT = {
|
|
14
|
+
"safe-to-skip": "safe to skip build",
|
|
15
|
+
"build-recommended": "build recommended",
|
|
16
|
+
"build-required": "build required"
|
|
17
|
+
};
|
|
18
|
+
function verdictText(verdict) {
|
|
19
|
+
return VERDICT_TEXT[verdict];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export {
|
|
23
|
+
deriveVerdict,
|
|
24
|
+
verdictText
|
|
25
|
+
};
|