@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.
Files changed (42) hide show
  1. package/LICENSE +20 -0
  2. package/README.md +177 -0
  3. package/dist/build-2LO7GD2S.js +63 -0
  4. package/dist/check-ZTKGGF4M.js +44 -0
  5. package/dist/chunk-3E5LBU3O.js +180 -0
  6. package/dist/chunk-4LLK2S4R.js +630 -0
  7. package/dist/chunk-6J4XDG44.js +14 -0
  8. package/dist/chunk-CXNXZLDG.js +13 -0
  9. package/dist/chunk-DRMASVYN.js +25 -0
  10. package/dist/chunk-GDSZFIHM.js +29 -0
  11. package/dist/chunk-ISRF5F3Q.js +68 -0
  12. package/dist/chunk-KSHWC2XN.js +78 -0
  13. package/dist/chunk-LJDDS4RR.js +91 -0
  14. package/dist/chunk-LXKLCLL6.js +91 -0
  15. package/dist/chunk-MZZOE7L4.js +26 -0
  16. package/dist/chunk-NSNZOH5P.js +31 -0
  17. package/dist/chunk-O4LZKXNZ.js +301 -0
  18. package/dist/chunk-RGD422OM.js +108 -0
  19. package/dist/chunk-RVXUCJYA.js +81 -0
  20. package/dist/chunk-SWPVR2DF.js +468 -0
  21. package/dist/chunk-V22IPJ7A.js +64 -0
  22. package/dist/chunk-V5CTGXMM.js +256 -0
  23. package/dist/chunk-VTJ67TT5.js +238 -0
  24. package/dist/chunk-XTYVVUWW.js +279 -0
  25. package/dist/chunk-XUBWCTJZ.js +100 -0
  26. package/dist/chunk-YYGCSKIQ.js +29 -0
  27. package/dist/cli.d.ts +1 -0
  28. package/dist/cli.js +225 -0
  29. package/dist/diff-GFI6RAEA.js +60 -0
  30. package/dist/doctor-GH4RZVEH.js +127 -0
  31. package/dist/env-QORDD6FQ.js +24 -0
  32. package/dist/impact-AF453PLK.js +702 -0
  33. package/dist/index.d.ts +56 -0
  34. package/dist/index.js +7 -0
  35. package/dist/init-KJIUNGAN.js +158 -0
  36. package/dist/insight-DZ5NQY6D.js +29 -0
  37. package/dist/pr-UFZRZX6S.js +152 -0
  38. package/dist/run-6PNH4QRA.js +40 -0
  39. package/dist/symbol-graph-YYUEMZGR.js +22 -0
  40. package/dist/tsconfig-paths-E3UEDGXZ.js +10 -0
  41. package/dist/workspace-X5FUBTY3.js +186 -0
  42. 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
+ };