@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,279 @@
|
|
|
1
|
+
import {
|
|
2
|
+
matchPathAlias
|
|
3
|
+
} from "./chunk-KSHWC2XN.js";
|
|
4
|
+
|
|
5
|
+
// src/core/graph/import-graph.ts
|
|
6
|
+
import path2 from "path";
|
|
7
|
+
|
|
8
|
+
// src/core/graph/package-exports.ts
|
|
9
|
+
import path from "path";
|
|
10
|
+
var BUILD_DIRS = ["dist", "lib", "build", "out", "es", "esm", "cjs"];
|
|
11
|
+
var SOURCE_DIRS = ["src", "lib", "source", ""];
|
|
12
|
+
var CONDITIONS = [
|
|
13
|
+
"source",
|
|
14
|
+
"development",
|
|
15
|
+
"import",
|
|
16
|
+
"module",
|
|
17
|
+
"require",
|
|
18
|
+
"types",
|
|
19
|
+
"node",
|
|
20
|
+
"default"
|
|
21
|
+
];
|
|
22
|
+
var MAX_CONDITION_DEPTH = 8;
|
|
23
|
+
function targetsOf(value, depth = 0) {
|
|
24
|
+
if (depth > MAX_CONDITION_DEPTH) return [];
|
|
25
|
+
if (typeof value === "string") return [value];
|
|
26
|
+
if (Array.isArray(value)) {
|
|
27
|
+
return value.flatMap((entry) => targetsOf(entry, depth + 1));
|
|
28
|
+
}
|
|
29
|
+
if (typeof value !== "object" || value === null) return [];
|
|
30
|
+
const record = value;
|
|
31
|
+
const out = [];
|
|
32
|
+
for (const condition of CONDITIONS) {
|
|
33
|
+
if (condition in record) {
|
|
34
|
+
out.push(...targetsOf(record[condition], depth + 1));
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
for (const [key, nested] of Object.entries(record)) {
|
|
38
|
+
if (!CONDITIONS.includes(key) && !key.startsWith(".")) {
|
|
39
|
+
out.push(...targetsOf(nested, depth + 1));
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return out;
|
|
43
|
+
}
|
|
44
|
+
function clean(target) {
|
|
45
|
+
if (!target.startsWith("./")) return void 0;
|
|
46
|
+
return target.slice(2);
|
|
47
|
+
}
|
|
48
|
+
function resolveExportsSubpath(exportsField, subpath) {
|
|
49
|
+
if (exportsField === void 0 || exportsField === null) return [];
|
|
50
|
+
const request = subpath === "" ? "." : `./${subpath}`;
|
|
51
|
+
const isSubpathMap = typeof exportsField === "object" && !Array.isArray(exportsField) && Object.keys(exportsField).some(
|
|
52
|
+
(k) => k.startsWith(".")
|
|
53
|
+
);
|
|
54
|
+
if (!isSubpathMap) {
|
|
55
|
+
return request === "." ? targetsOf(exportsField).map(clean).filter((t) => t !== void 0) : [];
|
|
56
|
+
}
|
|
57
|
+
const map = exportsField;
|
|
58
|
+
if (request in map) {
|
|
59
|
+
return targetsOf(map[request]).map(clean).filter((t) => t !== void 0);
|
|
60
|
+
}
|
|
61
|
+
const patterns = Object.keys(map).filter((key) => key.startsWith("./") && key.includes("*")).sort((a, b) => b.length - a.length);
|
|
62
|
+
for (const pattern of patterns) {
|
|
63
|
+
const [prefix = "", suffix = ""] = pattern.split("*");
|
|
64
|
+
if (request.length < prefix.length + suffix.length || !request.startsWith(prefix) || !request.endsWith(suffix)) {
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
const captured = request.slice(
|
|
68
|
+
prefix.length,
|
|
69
|
+
request.length - suffix.length
|
|
70
|
+
);
|
|
71
|
+
const targets = targetsOf(map[pattern]).map((t) => clean(t.replace("*", captured))).filter((t) => t !== void 0);
|
|
72
|
+
if (targets.length > 0) return targets;
|
|
73
|
+
}
|
|
74
|
+
return [];
|
|
75
|
+
}
|
|
76
|
+
function toSourceCandidates(target) {
|
|
77
|
+
const withoutExtension = stripExtension(target);
|
|
78
|
+
const segments = withoutExtension.split("/");
|
|
79
|
+
const head = segments[0];
|
|
80
|
+
const bodies = head !== void 0 && BUILD_DIRS.includes(head) && segments.length > 1 ? [segments.slice(1).join("/")] : [withoutExtension];
|
|
81
|
+
const out = [];
|
|
82
|
+
for (const body of bodies) {
|
|
83
|
+
for (const dir of SOURCE_DIRS) {
|
|
84
|
+
out.push(dir === "" ? body : `${dir}/${body}`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return [...new Set(out)];
|
|
88
|
+
}
|
|
89
|
+
function stripExtension(target) {
|
|
90
|
+
if (target.endsWith(".d.ts")) return target.slice(0, -".d.ts".length);
|
|
91
|
+
const ext = path.posix.extname(target);
|
|
92
|
+
return ext === "" ? target : target.slice(0, -ext.length);
|
|
93
|
+
}
|
|
94
|
+
function exportsCandidates(exportsField, subpath) {
|
|
95
|
+
const targets = resolveExportsSubpath(exportsField, subpath);
|
|
96
|
+
if (targets.length === 0) return [];
|
|
97
|
+
const out = [];
|
|
98
|
+
for (const target of targets) out.push(stripExtension(target));
|
|
99
|
+
for (const target of targets) out.push(...toSourceCandidates(target));
|
|
100
|
+
return [...new Set(out)];
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// src/core/graph/import-graph.ts
|
|
104
|
+
var TS_EXTENSIONS = [".ts", ".tsx", ".mts", ".cts"];
|
|
105
|
+
var JS_TO_TS = {
|
|
106
|
+
".js": [".ts", ".tsx"],
|
|
107
|
+
".mjs": [".mts"],
|
|
108
|
+
".cjs": [".cts"],
|
|
109
|
+
".jsx": [".tsx"]
|
|
110
|
+
};
|
|
111
|
+
function buildImportGraph(symbolGraph, options = {}) {
|
|
112
|
+
const files = new Set(Object.keys(symbolGraph.files));
|
|
113
|
+
const packageDirs = options.packageDirs ?? {};
|
|
114
|
+
const pathAliases = options.pathAliases ?? [];
|
|
115
|
+
const packageExports = options.packageExports ?? {};
|
|
116
|
+
const imports = /* @__PURE__ */ new Map();
|
|
117
|
+
const dependents = /* @__PURE__ */ new Map();
|
|
118
|
+
const externals = /* @__PURE__ */ new Map();
|
|
119
|
+
const unresolved = /* @__PURE__ */ new Map();
|
|
120
|
+
const edges = /* @__PURE__ */ new Map();
|
|
121
|
+
for (const file of [...files].sort()) {
|
|
122
|
+
imports.set(file, /* @__PURE__ */ new Set());
|
|
123
|
+
dependents.set(file, /* @__PURE__ */ new Set());
|
|
124
|
+
externals.set(file, /* @__PURE__ */ new Set());
|
|
125
|
+
unresolved.set(file, /* @__PURE__ */ new Set());
|
|
126
|
+
edges.set(file, /* @__PURE__ */ new Map());
|
|
127
|
+
}
|
|
128
|
+
const addEdge = (from, to, imp) => {
|
|
129
|
+
imports.get(from)?.add(to);
|
|
130
|
+
const perFile = edges.get(from);
|
|
131
|
+
if (perFile === void 0) return;
|
|
132
|
+
let edge = perFile.get(to);
|
|
133
|
+
if (edge === void 0) {
|
|
134
|
+
edge = { names: /* @__PURE__ */ new Set(), typeOnly: true, dynamic: false };
|
|
135
|
+
perFile.set(to, edge);
|
|
136
|
+
}
|
|
137
|
+
for (const name of imp.names) edge.names.add(name);
|
|
138
|
+
if (imp.kind === "dynamic") edge.dynamic = true;
|
|
139
|
+
edge.typeOnly = edge.typeOnly && imp.typeOnly;
|
|
140
|
+
};
|
|
141
|
+
for (const file of [...files].sort()) {
|
|
142
|
+
const index = symbolGraph.files[file];
|
|
143
|
+
if (index === void 0) continue;
|
|
144
|
+
for (const imp of index.imports) {
|
|
145
|
+
const spec = imp.module;
|
|
146
|
+
if (spec.startsWith("./") || spec.startsWith("../")) {
|
|
147
|
+
const resolved = resolveRelativeImport(file, spec, files);
|
|
148
|
+
if (resolved === void 0) unresolved.get(file)?.add(spec);
|
|
149
|
+
else addEdge(file, resolved, imp);
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
if (spec.startsWith("node:")) {
|
|
153
|
+
externals.get(file)?.add(spec);
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
const pkg = matchWorkspacePackage(spec, packageDirs);
|
|
157
|
+
if (pkg !== void 0) {
|
|
158
|
+
const resolved = resolvePackageImport(
|
|
159
|
+
pkg.dir,
|
|
160
|
+
pkg.subpath,
|
|
161
|
+
files,
|
|
162
|
+
packageExports[pkg.name]
|
|
163
|
+
);
|
|
164
|
+
if (resolved === void 0) unresolved.get(file)?.add(spec);
|
|
165
|
+
else addEdge(file, resolved, imp);
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
const aliased = resolveAliasImport(spec, pathAliases, files);
|
|
169
|
+
if (aliased !== void 0) {
|
|
170
|
+
addEdge(file, aliased, imp);
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
if (matchPathAlias(spec, pathAliases).length > 0) {
|
|
174
|
+
unresolved.get(file)?.add(spec);
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
externals.get(file)?.add(spec);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
for (const [file, targets] of imports) {
|
|
181
|
+
for (const target of targets) {
|
|
182
|
+
dependents.get(target)?.add(file);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return { imports, dependents, externals, unresolved, edges };
|
|
186
|
+
}
|
|
187
|
+
function computeAffectedFiles(changed, graph) {
|
|
188
|
+
const affected = /* @__PURE__ */ new Set();
|
|
189
|
+
for (const file of changed) affected.add(toPosix(file));
|
|
190
|
+
let frontier = new Set(affected);
|
|
191
|
+
while (frontier.size > 0) {
|
|
192
|
+
const next = /* @__PURE__ */ new Set();
|
|
193
|
+
for (const file of frontier) {
|
|
194
|
+
for (const dependent of graph.dependents.get(file) ?? []) {
|
|
195
|
+
if (!affected.has(dependent)) {
|
|
196
|
+
affected.add(dependent);
|
|
197
|
+
next.add(dependent);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
frontier = next;
|
|
202
|
+
}
|
|
203
|
+
return affected;
|
|
204
|
+
}
|
|
205
|
+
function resolveRelativeImport(fromFile, spec, files) {
|
|
206
|
+
const base = path2.posix.normalize(
|
|
207
|
+
path2.posix.join(path2.posix.dirname(fromFile), spec)
|
|
208
|
+
);
|
|
209
|
+
if (base.startsWith("../")) return void 0;
|
|
210
|
+
return firstExisting(candidatePaths(base), files);
|
|
211
|
+
}
|
|
212
|
+
function resolvePackageImport(pkgDir, subpath, files, exportsField) {
|
|
213
|
+
const fromExports = exportsCandidates(exportsField, subpath);
|
|
214
|
+
const candidates = fromExports.flatMap(
|
|
215
|
+
(base) => candidatePaths(path2.posix.join(pkgDir, base))
|
|
216
|
+
);
|
|
217
|
+
if (subpath === "") {
|
|
218
|
+
candidates.push(
|
|
219
|
+
...candidatePaths(`${pkgDir}/src/index`),
|
|
220
|
+
...candidatePaths(`${pkgDir}/index`)
|
|
221
|
+
);
|
|
222
|
+
} else {
|
|
223
|
+
candidates.push(
|
|
224
|
+
...candidatePaths(path2.posix.join(pkgDir, subpath)),
|
|
225
|
+
...candidatePaths(path2.posix.join(pkgDir, "src", subpath))
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
return firstExisting(candidates, files);
|
|
229
|
+
}
|
|
230
|
+
function resolveAliasImport(spec, aliases, files) {
|
|
231
|
+
for (const base of matchPathAlias(spec, aliases)) {
|
|
232
|
+
const resolved = firstExisting(candidatePaths(base), files);
|
|
233
|
+
if (resolved !== void 0) return resolved;
|
|
234
|
+
}
|
|
235
|
+
return void 0;
|
|
236
|
+
}
|
|
237
|
+
function matchWorkspacePackage(spec, packageDirs) {
|
|
238
|
+
const segments = spec.split("/");
|
|
239
|
+
const nameLength = spec.startsWith("@") ? 2 : 1;
|
|
240
|
+
if (segments.length < nameLength) return void 0;
|
|
241
|
+
const name = segments.slice(0, nameLength).join("/");
|
|
242
|
+
const dir = packageDirs[name];
|
|
243
|
+
if (dir === void 0) return void 0;
|
|
244
|
+
return {
|
|
245
|
+
name,
|
|
246
|
+
dir: toPosix(dir),
|
|
247
|
+
subpath: segments.slice(nameLength).join("/")
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
function candidatePaths(base) {
|
|
251
|
+
const ext = path2.posix.extname(base);
|
|
252
|
+
if (TS_EXTENSIONS.includes(ext)) {
|
|
253
|
+
return [base];
|
|
254
|
+
}
|
|
255
|
+
const tsExts = JS_TO_TS[ext];
|
|
256
|
+
if (tsExts !== void 0) {
|
|
257
|
+
const stem = base.slice(0, -ext.length);
|
|
258
|
+
return tsExts.map((e) => stem + e);
|
|
259
|
+
}
|
|
260
|
+
return [
|
|
261
|
+
...TS_EXTENSIONS.map((e) => base + e),
|
|
262
|
+
...TS_EXTENSIONS.map((e) => `${base}/index${e}`)
|
|
263
|
+
];
|
|
264
|
+
}
|
|
265
|
+
function firstExisting(candidates, files) {
|
|
266
|
+
for (const c of candidates) {
|
|
267
|
+
if (files.has(c)) return c;
|
|
268
|
+
}
|
|
269
|
+
return void 0;
|
|
270
|
+
}
|
|
271
|
+
function toPosix(p) {
|
|
272
|
+
return p.replace(/\\/g, "/");
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
export {
|
|
276
|
+
buildImportGraph,
|
|
277
|
+
computeAffectedFiles,
|
|
278
|
+
resolveRelativeImport
|
|
279
|
+
};
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
// src/core/vcs/git.ts
|
|
2
|
+
function toPosix(relPath) {
|
|
3
|
+
return relPath.replace(/\\/g, "/");
|
|
4
|
+
}
|
|
5
|
+
async function git(cwd, args) {
|
|
6
|
+
try {
|
|
7
|
+
const { execa } = await import("execa");
|
|
8
|
+
const { stdout } = await execa("git", args, { cwd });
|
|
9
|
+
return stdout;
|
|
10
|
+
} catch {
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
function toFileList(stdout) {
|
|
15
|
+
if (stdout === null) return null;
|
|
16
|
+
return stdout.split("\n").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
17
|
+
}
|
|
18
|
+
async function readFileAtRef(cwd, ref, relPath) {
|
|
19
|
+
return git(cwd, ["show", `${ref}:${toPosix(relPath)}`]);
|
|
20
|
+
}
|
|
21
|
+
var CAT_FILE_BATCH_SIZE = 256;
|
|
22
|
+
async function readFilesAtRef(cwd, ref, relPaths) {
|
|
23
|
+
const out = /* @__PURE__ */ new Map();
|
|
24
|
+
if (relPaths.length === 0) return out;
|
|
25
|
+
if (relPaths.some((p) => p.includes("\n") || p.includes("\0"))) return null;
|
|
26
|
+
const { execa } = await import("execa");
|
|
27
|
+
for (let start = 0; start < relPaths.length; start += CAT_FILE_BATCH_SIZE) {
|
|
28
|
+
const chunk = relPaths.slice(start, start + CAT_FILE_BATCH_SIZE);
|
|
29
|
+
let stdout;
|
|
30
|
+
try {
|
|
31
|
+
const result = await execa("git", ["cat-file", "--batch"], {
|
|
32
|
+
cwd,
|
|
33
|
+
input: `${chunk.map((p) => `${ref}:${toPosix(p)}`).join("\n")}
|
|
34
|
+
`,
|
|
35
|
+
encoding: "buffer",
|
|
36
|
+
// The response framing is byte-exact; execa's default of
|
|
37
|
+
// trimming the final newline would truncate the last record.
|
|
38
|
+
stripFinalNewline: false
|
|
39
|
+
});
|
|
40
|
+
stdout = result.stdout;
|
|
41
|
+
} catch {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
if (!parseCatFileBatch(stdout, chunk, out)) return null;
|
|
45
|
+
}
|
|
46
|
+
return out;
|
|
47
|
+
}
|
|
48
|
+
var NEWLINE = 10;
|
|
49
|
+
function stripFinalNewline(text) {
|
|
50
|
+
if (text.endsWith("\r\n")) return text.slice(0, -2);
|
|
51
|
+
if (text.endsWith("\n")) return text.slice(0, -1);
|
|
52
|
+
return text;
|
|
53
|
+
}
|
|
54
|
+
function parseCatFileBatch(stdout, requested, out) {
|
|
55
|
+
const decoder = new TextDecoder();
|
|
56
|
+
let offset = 0;
|
|
57
|
+
for (const relPath of requested) {
|
|
58
|
+
const lineEnd = stdout.indexOf(NEWLINE, offset);
|
|
59
|
+
if (lineEnd === -1) return false;
|
|
60
|
+
const header = decoder.decode(stdout.subarray(offset, lineEnd));
|
|
61
|
+
offset = lineEnd + 1;
|
|
62
|
+
const parts = header.split(" ");
|
|
63
|
+
const type = parts[1];
|
|
64
|
+
if (type !== "blob") {
|
|
65
|
+
out.set(relPath, null);
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
const size = Number(parts[2]);
|
|
69
|
+
if (!Number.isInteger(size) || size < 0) return false;
|
|
70
|
+
const end = offset + size;
|
|
71
|
+
if (end > stdout.length) return false;
|
|
72
|
+
out.set(
|
|
73
|
+
relPath,
|
|
74
|
+
stripFinalNewline(decoder.decode(stdout.subarray(offset, end)))
|
|
75
|
+
);
|
|
76
|
+
offset = end + 1;
|
|
77
|
+
}
|
|
78
|
+
return true;
|
|
79
|
+
}
|
|
80
|
+
async function readHeadSha(cwd) {
|
|
81
|
+
const stdout = await git(cwd, ["rev-parse", "HEAD"]);
|
|
82
|
+
const sha = stdout?.trim();
|
|
83
|
+
return sha === void 0 || sha === "" ? null : sha;
|
|
84
|
+
}
|
|
85
|
+
async function listChangedFiles(cwd, ref) {
|
|
86
|
+
return toFileList(await git(cwd, ["diff", "--name-only", ref, "--"]));
|
|
87
|
+
}
|
|
88
|
+
async function listChangedFilesSinceMergeBase(cwd, ref) {
|
|
89
|
+
return toFileList(
|
|
90
|
+
await git(cwd, ["diff", "--name-only", `${ref}...HEAD`, "--"])
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export {
|
|
95
|
+
readFileAtRef,
|
|
96
|
+
readFilesAtRef,
|
|
97
|
+
readHeadSha,
|
|
98
|
+
listChangedFiles,
|
|
99
|
+
listChangedFilesSinceMergeBase
|
|
100
|
+
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// src/cli/visuals/color.ts
|
|
2
|
+
import pc from "picocolors";
|
|
3
|
+
function envFlagSet(name) {
|
|
4
|
+
const value = process.env[name];
|
|
5
|
+
return value !== void 0 && value !== "";
|
|
6
|
+
}
|
|
7
|
+
function resolveColorChoice(flags = {}) {
|
|
8
|
+
const { color } = flags;
|
|
9
|
+
if (color === "auto" || color === "always" || color === "never") return color;
|
|
10
|
+
if (color === false) return "never";
|
|
11
|
+
if (envFlagSet("NO_COLOR")) return "never";
|
|
12
|
+
if (envFlagSet("FORCE_COLOR") || envFlagSet("CLICOLOR_FORCE")) {
|
|
13
|
+
return "always";
|
|
14
|
+
}
|
|
15
|
+
return "auto";
|
|
16
|
+
}
|
|
17
|
+
var globalColors = pc;
|
|
18
|
+
function writeGlobalColorChoice(choice) {
|
|
19
|
+
globalColors = choice === "auto" ? pc.createColors(pc.isColorSupported) : pc.createColors(choice === "always");
|
|
20
|
+
}
|
|
21
|
+
function getColors() {
|
|
22
|
+
return globalColors;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export {
|
|
26
|
+
resolveColorChoice,
|
|
27
|
+
writeGlobalColorChoice,
|
|
28
|
+
getColors
|
|
29
|
+
};
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
getColors,
|
|
4
|
+
resolveColorChoice,
|
|
5
|
+
writeGlobalColorChoice
|
|
6
|
+
} from "./chunk-YYGCSKIQ.js";
|
|
7
|
+
import {
|
|
8
|
+
CliUsageError,
|
|
9
|
+
VariantError
|
|
10
|
+
} from "./chunk-RVXUCJYA.js";
|
|
11
|
+
import {
|
|
12
|
+
Printer,
|
|
13
|
+
setGlobalPrinter
|
|
14
|
+
} from "./chunk-V22IPJ7A.js";
|
|
15
|
+
|
|
16
|
+
// src/cli/index.ts
|
|
17
|
+
import { readFileSync } from "fs";
|
|
18
|
+
import { dirname, join } from "path";
|
|
19
|
+
import { fileURLToPath } from "url";
|
|
20
|
+
import { Command, Option } from "commander";
|
|
21
|
+
|
|
22
|
+
// src/cli/parse-opts.ts
|
|
23
|
+
function parseConcurrency(opts) {
|
|
24
|
+
if (opts.concurrency === void 0) return void 0;
|
|
25
|
+
const n = Number.parseInt(opts.concurrency, 10);
|
|
26
|
+
if (!Number.isFinite(n) || n < 1 || String(n) !== opts.concurrency.trim()) {
|
|
27
|
+
throw new CliUsageError(
|
|
28
|
+
`--concurrency must be a positive integer, got "${opts.concurrency}"`
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
return n;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// src/cli/render/provenance.ts
|
|
35
|
+
var HASH_PREFIX_LENGTH = 7;
|
|
36
|
+
var MAX_LISTED_FILES = 3;
|
|
37
|
+
var MAX_LISTED_DEPENDENTS = 5;
|
|
38
|
+
function shortHash(hash) {
|
|
39
|
+
return hash.slice(0, HASH_PREFIX_LENGTH);
|
|
40
|
+
}
|
|
41
|
+
function listWithOverflow(items, max) {
|
|
42
|
+
const shown = items.slice(0, max).join(", ");
|
|
43
|
+
const hidden = items.length - max;
|
|
44
|
+
return hidden > 0 ? `${shown} and ${hidden} more` : shown;
|
|
45
|
+
}
|
|
46
|
+
function describeReason(reason) {
|
|
47
|
+
if (reason.kind === "always") {
|
|
48
|
+
return "this task is never cached, so it runs every time";
|
|
49
|
+
}
|
|
50
|
+
if (reason.kind === "cache-miss") {
|
|
51
|
+
return reason.expectedHash === null ? "cache miss \u2014 nothing cached for this task yet" : `cache miss \u2014 inputs hash ${shortHash(reason.actualHash)}, cached ${shortHash(reason.expectedHash)}`;
|
|
52
|
+
}
|
|
53
|
+
if (reason.changedFiles.length === 0) return "affected by the current diff";
|
|
54
|
+
return `${listWithOverflow(reason.changedFiles, MAX_LISTED_FILES)} changed`;
|
|
55
|
+
}
|
|
56
|
+
function provenanceLines(provenance) {
|
|
57
|
+
const block = [`ran because: ${describeReason(provenance.reason)}`];
|
|
58
|
+
const { dirtyDependents } = provenance;
|
|
59
|
+
if (dirtyDependents.length > 0) {
|
|
60
|
+
block.push(
|
|
61
|
+
`also affected: ${listWithOverflow(dirtyDependents, MAX_LISTED_DEPENDENTS)}`
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
return block;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// src/cli/render/error.ts
|
|
68
|
+
var writeStderr = (text) => {
|
|
69
|
+
process.stderr.write(text);
|
|
70
|
+
};
|
|
71
|
+
function renderError(err, write = writeStderr) {
|
|
72
|
+
const colors = getColors();
|
|
73
|
+
write(`${colors.red(`[${err.code}]`)} ${err.message}
|
|
74
|
+
`);
|
|
75
|
+
if (err.provenance) {
|
|
76
|
+
for (const line of provenanceLines(err.provenance)) {
|
|
77
|
+
write(`${colors.dim(` ${line}`)}
|
|
78
|
+
`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
if (err.hint) write(`${colors.dim(` Hint: ${err.hint}`)}
|
|
82
|
+
`);
|
|
83
|
+
}
|
|
84
|
+
function renderUnexpectedError(err, write = writeStderr) {
|
|
85
|
+
const detail = err instanceof Error ? err.stack ?? err.message : String(err);
|
|
86
|
+
write(`Unexpected error \u2014 please file a bug:
|
|
87
|
+
${detail}
|
|
88
|
+
`);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// src/cli/index.ts
|
|
92
|
+
var _dir = dirname(fileURLToPath(import.meta.url));
|
|
93
|
+
var _pkg = JSON.parse(
|
|
94
|
+
readFileSync(join(_dir, "..", "package.json"), "utf8")
|
|
95
|
+
);
|
|
96
|
+
var countFlag = (_value, previous) => previous + 1;
|
|
97
|
+
var program = new Command().name("variant").description(
|
|
98
|
+
"Change intelligence and task orchestration for TypeScript monorepos"
|
|
99
|
+
).version(_pkg.version).option(
|
|
100
|
+
"-q, --quiet",
|
|
101
|
+
"use quiet output (repeat, e.g. -qq, for silent)",
|
|
102
|
+
countFlag,
|
|
103
|
+
0
|
|
104
|
+
).option("-v, --verbose", "use verbose output", countFlag, 0).option("--no-progress", "hide progress bars and spinners").addOption(
|
|
105
|
+
new Option("--color <when>", "control colored output").choices([
|
|
106
|
+
"auto",
|
|
107
|
+
"always",
|
|
108
|
+
"never"
|
|
109
|
+
])
|
|
110
|
+
).option("--no-color", "disable colored output (alias for --color never)").hook("preAction", (thisCommand) => {
|
|
111
|
+
const opts = thisCommand.opts();
|
|
112
|
+
writeGlobalColorChoice(resolveColorChoice({ color: opts.color }));
|
|
113
|
+
setGlobalPrinter(
|
|
114
|
+
Printer.fromFlags({
|
|
115
|
+
quiet: opts.quiet,
|
|
116
|
+
verbose: opts.verbose,
|
|
117
|
+
noProgress: !opts.progress
|
|
118
|
+
})
|
|
119
|
+
);
|
|
120
|
+
});
|
|
121
|
+
var impactCmd = program.command("impact").description(
|
|
122
|
+
"Predict which tests a change requires \u2014 report-only; the full suite should still run"
|
|
123
|
+
).option("--base <ref>", "git ref to compare against", "HEAD~1").option("--json", "output the report as JSON").action(async (opts) => {
|
|
124
|
+
const { registerImpactAction } = await import("./impact-AF453PLK.js");
|
|
125
|
+
await registerImpactAction({
|
|
126
|
+
base: opts.base,
|
|
127
|
+
...opts.json === true && { json: true }
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
impactCmd.command("verify <report>").description(
|
|
131
|
+
"Reconcile a logged prediction against a Vitest/Jest JSON report \u2014 reports false skips"
|
|
132
|
+
).option("--head-sha <sha>", "reconcile the prediction made at this commit").option("--json", "output the reconciliation as JSON").action(
|
|
133
|
+
async (report, opts) => {
|
|
134
|
+
const { registerImpactVerifyAction } = await import("./impact-AF453PLK.js");
|
|
135
|
+
await registerImpactVerifyAction(report, {
|
|
136
|
+
...opts.headSha !== void 0 && { headSha: opts.headSha },
|
|
137
|
+
...opts.json === true && { json: true }
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
);
|
|
141
|
+
program.command("diff <file>").description(
|
|
142
|
+
"Classify a TypeScript file change as non-impacting / internal / breaking"
|
|
143
|
+
).option("--base <ref>", "git ref to compare against", "HEAD~1").action(async (file, opts) => {
|
|
144
|
+
const { registerDiffAction } = await import("./diff-GFI6RAEA.js");
|
|
145
|
+
await registerDiffAction(file, { base: opts.base });
|
|
146
|
+
});
|
|
147
|
+
var workspaceCmd = program.command("workspace").description("Workspace-level analysis commands");
|
|
148
|
+
workspaceCmd.command("check").description(
|
|
149
|
+
"Detect packages importing dependencies they do not declare (CI gate: exits 1 on violations)"
|
|
150
|
+
).option("--json", "output the result as JSON").action(async (opts) => {
|
|
151
|
+
const { registerWorkspaceCheckAction } = await import("./workspace-X5FUBTY3.js");
|
|
152
|
+
await registerWorkspaceCheckAction({
|
|
153
|
+
...opts.json === true && { json: true }
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
var prCmd = program.command("pr").description("PR-aware analysis commands");
|
|
157
|
+
prCmd.command("check").description(
|
|
158
|
+
"Classify all TypeScript changes in this PR and report a build verdict"
|
|
159
|
+
).option("--base <ref>", "base branch or ref for the PR diff", "main").action(async (opts) => {
|
|
160
|
+
const { registerPrCheckAction } = await import("./pr-UFZRZX6S.js");
|
|
161
|
+
await registerPrCheckAction({ base: opts.base });
|
|
162
|
+
});
|
|
163
|
+
prCmd.command("report").description("Render `pr check` as a structured JSON or markdown report").option("--base <ref>", "base branch or ref for the PR diff", "main").option("--markdown", "output a markdown summary instead of JSON").option("--output <file>", "write report to file instead of stdout").action(
|
|
164
|
+
async (opts) => {
|
|
165
|
+
const { registerPrReportAction } = await import("./pr-UFZRZX6S.js");
|
|
166
|
+
await registerPrReportAction(opts);
|
|
167
|
+
}
|
|
168
|
+
);
|
|
169
|
+
program.command("build").description("Run the build task").option("-c, --concurrency <n>", "max tasks per DAG level").option(
|
|
170
|
+
"--affected",
|
|
171
|
+
"run only packages affected by changes since baseRef (includes cascade dependents)"
|
|
172
|
+
).option("--dry-run", "print the task plan without executing").action(
|
|
173
|
+
async (opts) => {
|
|
174
|
+
const { registerBuildAction } = await import("./build-2LO7GD2S.js");
|
|
175
|
+
const concurrency = parseConcurrency(opts);
|
|
176
|
+
await registerBuildAction({
|
|
177
|
+
...concurrency !== void 0 && { concurrency },
|
|
178
|
+
...opts.affected && { affected: true },
|
|
179
|
+
...opts.dryRun && { dryRun: true }
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
);
|
|
183
|
+
program.command("run <task>").description("Run a named task").option(
|
|
184
|
+
"-c, --concurrency <n>",
|
|
185
|
+
"max tasks to run concurrently per DAG level"
|
|
186
|
+
).option("--dry-run", "print the task plan without executing").action(
|
|
187
|
+
async (taskName, opts) => {
|
|
188
|
+
const { registerRunAction } = await import("./run-6PNH4QRA.js");
|
|
189
|
+
const concurrency = parseConcurrency(opts);
|
|
190
|
+
await registerRunAction(taskName, {
|
|
191
|
+
...concurrency !== void 0 && { concurrency },
|
|
192
|
+
...opts.dryRun && { dryRun: true }
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
);
|
|
196
|
+
program.command("init").description("Scaffold variant.config.ts in the current directory").action(async () => {
|
|
197
|
+
const { registerInitAction } = await import("./init-KJIUNGAN.js");
|
|
198
|
+
await registerInitAction();
|
|
199
|
+
});
|
|
200
|
+
program.command("doctor").description("Validate config and diagnose common issues").action(async () => {
|
|
201
|
+
const { registerDoctorAction } = await import("./doctor-GH4RZVEH.js");
|
|
202
|
+
await registerDoctorAction();
|
|
203
|
+
});
|
|
204
|
+
program.command("insight").description("Show task timing and cache hit stats").action(async () => {
|
|
205
|
+
const { registerInsightAction } = await import("./insight-DZ5NQY6D.js");
|
|
206
|
+
await registerInsightAction();
|
|
207
|
+
});
|
|
208
|
+
program.command("env").description(
|
|
209
|
+
"Show detected environment (runtime, package manager, framework)"
|
|
210
|
+
).action(async () => {
|
|
211
|
+
const { registerEnvAction } = await import("./env-QORDD6FQ.js");
|
|
212
|
+
await registerEnvAction();
|
|
213
|
+
});
|
|
214
|
+
program.command("check").description("Validate config and task graph (no execution)").action(async () => {
|
|
215
|
+
const { registerCheckAction } = await import("./check-ZTKGGF4M.js");
|
|
216
|
+
await registerCheckAction();
|
|
217
|
+
});
|
|
218
|
+
program.parseAsync(process.argv).catch((err) => {
|
|
219
|
+
if (err instanceof VariantError) {
|
|
220
|
+
renderError(err);
|
|
221
|
+
process.exit(1);
|
|
222
|
+
}
|
|
223
|
+
renderUnexpectedError(err);
|
|
224
|
+
process.exit(2);
|
|
225
|
+
});
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DEFAULT_DIFF_BASE_REF,
|
|
3
|
+
classifyFileAgainstRef,
|
|
4
|
+
toWorkspaceRelative
|
|
5
|
+
} from "./chunk-NSNZOH5P.js";
|
|
6
|
+
import "./chunk-LJDDS4RR.js";
|
|
7
|
+
import "./chunk-XUBWCTJZ.js";
|
|
8
|
+
import {
|
|
9
|
+
lines
|
|
10
|
+
} from "./chunk-6J4XDG44.js";
|
|
11
|
+
import {
|
|
12
|
+
getPrinter
|
|
13
|
+
} from "./chunk-V22IPJ7A.js";
|
|
14
|
+
import "./chunk-O4LZKXNZ.js";
|
|
15
|
+
|
|
16
|
+
// src/cli/render/diff.ts
|
|
17
|
+
var CLASS_MEANING = {
|
|
18
|
+
"non-impacting": "safe to skip build",
|
|
19
|
+
internal: "implementation-only change",
|
|
20
|
+
breaking: "exported API changed"
|
|
21
|
+
};
|
|
22
|
+
function renderClassification(result, baseRef, printer = getPrinter()) {
|
|
23
|
+
const meaning = `${result.classification.padEnd(15)}(${CLASS_MEANING[result.classification]})`;
|
|
24
|
+
lines(
|
|
25
|
+
printer,
|
|
26
|
+
"",
|
|
27
|
+
`File: ${result.filePath}`,
|
|
28
|
+
`Base ref: ${baseRef}`,
|
|
29
|
+
`Classification: ${meaning}`,
|
|
30
|
+
`Confidence: ${Math.round(result.confidence * 100)}%`
|
|
31
|
+
);
|
|
32
|
+
const { added, removed, changed } = result.exportedSymbols;
|
|
33
|
+
if (added.length > 0 || removed.length > 0 || changed.length > 0) {
|
|
34
|
+
lines(printer, "", "Exported symbol changes:");
|
|
35
|
+
if (added.length > 0) lines(printer, ` added: ${added.join(", ")}`);
|
|
36
|
+
if (removed.length > 0) lines(printer, ` removed: ${removed.join(", ")}`);
|
|
37
|
+
if (changed.length > 0) {
|
|
38
|
+
const detail = changed.map((c) => `${c.name} [${c.kind}]`).join(", ");
|
|
39
|
+
lines(printer, ` changed: ${detail}`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
if (result.confidenceNotes.length > 0) {
|
|
43
|
+
lines(printer, "", "Confidence lowered by:");
|
|
44
|
+
for (const note of result.confidenceNotes) lines(printer, ` - ${note}`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// src/cli/commands/diff.ts
|
|
49
|
+
async function registerDiffAction(filePath, opts = {}) {
|
|
50
|
+
const cwd = process.cwd();
|
|
51
|
+
const baseRef = opts.base ?? DEFAULT_DIFF_BASE_REF;
|
|
52
|
+
const relPath = toWorkspaceRelative(cwd, filePath);
|
|
53
|
+
renderClassification(
|
|
54
|
+
await classifyFileAgainstRef(cwd, relPath, baseRef),
|
|
55
|
+
baseRef
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
export {
|
|
59
|
+
registerDiffAction
|
|
60
|
+
};
|