commit-whisper 1.1.1 → 1.1.2
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/dist/index.js +145 -30
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1171,7 +1171,7 @@ function messageForError(err) {
|
|
|
1171
1171
|
import pc2 from "picocolors";
|
|
1172
1172
|
import { isCancel, multiselect as clackMultiselect, select as clackSelect, text as clackText } from "@clack/prompts";
|
|
1173
1173
|
var LAUNCHPAD_TAGLINE = "commit-whisper \xB7 \u{1F575}\uFE0F I know what you did last commit";
|
|
1174
|
-
var QUIT_MESSAGE = "Case closed.
|
|
1174
|
+
var QUIT_MESSAGE = "Case closed. Until your next commit \u2014 here's the cheatsheet for the road \u{1F6E3}\uFE0F";
|
|
1175
1175
|
var FLAGS_CHEATSHEET = [
|
|
1176
1176
|
"Common commands:",
|
|
1177
1177
|
" commit-whisper . analyze the current repository",
|
|
@@ -1472,12 +1472,12 @@ async function waitForKey(output) {
|
|
|
1472
1472
|
const wasRaw = stdin.isRaw === true;
|
|
1473
1473
|
stdin.setRawMode?.(true);
|
|
1474
1474
|
stdin.resume();
|
|
1475
|
-
return await new Promise((
|
|
1475
|
+
return await new Promise((resolve2) => {
|
|
1476
1476
|
stdin.once("data", (data) => {
|
|
1477
1477
|
stdin.setRawMode?.(wasRaw);
|
|
1478
1478
|
stdin.pause();
|
|
1479
1479
|
const key = data.toString("utf8");
|
|
1480
|
-
|
|
1480
|
+
resolve2(key === "" || key === "\x1B" ? "quit" : "continue");
|
|
1481
1481
|
});
|
|
1482
1482
|
});
|
|
1483
1483
|
}
|
|
@@ -3927,7 +3927,7 @@ function assertNeverProvider(provider) {
|
|
|
3927
3927
|
|
|
3928
3928
|
// src/narrate/narrate.ts
|
|
3929
3929
|
function createNarrate(deps = {}) {
|
|
3930
|
-
const
|
|
3930
|
+
const resolve2 = deps.resolveModel ?? resolveModel;
|
|
3931
3931
|
const generate = deps.generate ?? generateNarrative;
|
|
3932
3932
|
const generateExpl = deps.generateExplanations ?? generateExplanations;
|
|
3933
3933
|
return async (analysis, config) => {
|
|
@@ -3935,7 +3935,7 @@ function createNarrate(deps = {}) {
|
|
|
3935
3935
|
return { kind: "skipped" };
|
|
3936
3936
|
}
|
|
3937
3937
|
try {
|
|
3938
|
-
const model =
|
|
3938
|
+
const model = resolve2(config);
|
|
3939
3939
|
const [parts, explanations] = await Promise.all([
|
|
3940
3940
|
generate(model, analysis),
|
|
3941
3941
|
generateExpl(model, analysis)
|
|
@@ -4725,6 +4725,13 @@ function svgDonut(series, label) {
|
|
|
4725
4725
|
const a0 = (-90 + cum * 360) * Math.PI / 180;
|
|
4726
4726
|
const a1 = (-90 + (cum + frac) * 360) * Math.PI / 180;
|
|
4727
4727
|
cum += frac;
|
|
4728
|
+
if (frac <= 0) {
|
|
4729
|
+
return "";
|
|
4730
|
+
}
|
|
4731
|
+
if (frac >= 0.999) {
|
|
4732
|
+
const ring = `M ${cx} ${cy - rOuter} A ${rOuter} ${rOuter} 0 1 1 ${cx} ${cy + rOuter} A ${rOuter} ${rOuter} 0 1 1 ${cx} ${cy - rOuter} Z M ${cx} ${cy - rInner} A ${rInner} ${rInner} 0 1 0 ${cx} ${cy + rInner} A ${rInner} ${rInner} 0 1 0 ${cx} ${cy - rInner} Z`;
|
|
4733
|
+
return `<path class="donut-seg slice-${i % 6}" d="${ring}"/>`;
|
|
4734
|
+
}
|
|
4728
4735
|
const large = frac > 0.5 ? 1 : 0;
|
|
4729
4736
|
const x0o = r(cx + rOuter * Math.cos(a0));
|
|
4730
4737
|
const y0o = r(cy + rOuter * Math.sin(a0));
|
|
@@ -4773,20 +4780,121 @@ function formatNumber2(value) {
|
|
|
4773
4780
|
}
|
|
4774
4781
|
return Number.isInteger(value) ? String(value) : String(Math.round(value * 100) / 100);
|
|
4775
4782
|
}
|
|
4776
|
-
|
|
4777
|
-
|
|
4778
|
-
|
|
4779
|
-
|
|
4780
|
-
|
|
4781
|
-
|
|
4782
|
-
|
|
4783
|
-
|
|
4784
|
-
|
|
4785
|
-
|
|
4783
|
+
function asRecord(value) {
|
|
4784
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
4785
|
+
}
|
|
4786
|
+
function objectSeries(value) {
|
|
4787
|
+
return Object.entries(asRecord(value)).filter(([, v]) => typeof v === "number" && Number.isFinite(v)).map(([label, v]) => ({ label, value: v }));
|
|
4788
|
+
}
|
|
4789
|
+
function pickFields(value, fields) {
|
|
4790
|
+
const obj = asRecord(value);
|
|
4791
|
+
const out = [];
|
|
4792
|
+
for (const pair of fields) {
|
|
4793
|
+
const key = pair[0];
|
|
4794
|
+
if (typeof key !== "string") {
|
|
4795
|
+
continue;
|
|
4796
|
+
}
|
|
4797
|
+
const v = obj[key];
|
|
4798
|
+
if (typeof v === "number" && Number.isFinite(v)) {
|
|
4799
|
+
out.push({ label: pair[1] ?? key, value: v });
|
|
4800
|
+
}
|
|
4801
|
+
}
|
|
4802
|
+
return out;
|
|
4803
|
+
}
|
|
4804
|
+
function pctField(value, key) {
|
|
4805
|
+
const v = asRecord(value)[key];
|
|
4806
|
+
return typeof v === "number" && Number.isFinite(v) ? { value: v, max: 100 } : void 0;
|
|
4807
|
+
}
|
|
4808
|
+
function baseName(path) {
|
|
4809
|
+
const parts = path.split("/").filter((p) => p !== "");
|
|
4810
|
+
return parts.at(-1) ?? path;
|
|
4811
|
+
}
|
|
4812
|
+
function rowsSeries(value, key, field2, limit) {
|
|
4813
|
+
const rows = asRecord(value)[key];
|
|
4814
|
+
if (!Array.isArray(rows)) {
|
|
4815
|
+
return [];
|
|
4816
|
+
}
|
|
4817
|
+
const out = [];
|
|
4818
|
+
for (const row of rows.slice(0, limit)) {
|
|
4819
|
+
const obj = asRecord(row);
|
|
4820
|
+
const v = obj[field2];
|
|
4821
|
+
if (typeof v === "number" && Number.isFinite(v) && typeof obj.path === "string") {
|
|
4822
|
+
out.push({ label: baseName(obj.path), value: v });
|
|
4823
|
+
}
|
|
4824
|
+
}
|
|
4825
|
+
return out;
|
|
4786
4826
|
}
|
|
4787
|
-
function
|
|
4788
|
-
|
|
4827
|
+
function contributorSplit(value) {
|
|
4828
|
+
const obj = asRecord(value);
|
|
4829
|
+
const active = typeof obj.active === "number" ? obj.active : 0;
|
|
4830
|
+
const total = typeof obj.total === "number" ? obj.total : active;
|
|
4831
|
+
return [
|
|
4832
|
+
{ label: "active", value: active },
|
|
4833
|
+
{ label: "inactive", value: Math.max(0, total - active) }
|
|
4834
|
+
];
|
|
4835
|
+
}
|
|
4836
|
+
function hygieneDimensions(value) {
|
|
4837
|
+
const obj = asRecord(value);
|
|
4838
|
+
const out = [];
|
|
4839
|
+
for (const key of ["strengths", "weaknesses"]) {
|
|
4840
|
+
const arr = obj[key];
|
|
4841
|
+
if (!Array.isArray(arr)) {
|
|
4842
|
+
continue;
|
|
4843
|
+
}
|
|
4844
|
+
for (const entry of arr) {
|
|
4845
|
+
const dim = asRecord(entry);
|
|
4846
|
+
if (typeof dim.name === "string" && typeof dim.subScore === "number" && Number.isFinite(dim.subScore)) {
|
|
4847
|
+
out.push({ label: dim.name, value: dim.subScore });
|
|
4848
|
+
}
|
|
4849
|
+
}
|
|
4850
|
+
}
|
|
4851
|
+
return out;
|
|
4789
4852
|
}
|
|
4853
|
+
function churnByMonth(value) {
|
|
4854
|
+
const out = [];
|
|
4855
|
+
for (const [label, bucket] of Object.entries(asRecord(asRecord(value).perMonth))) {
|
|
4856
|
+
const churn = asRecord(bucket).churn;
|
|
4857
|
+
if (typeof churn === "number" && Number.isFinite(churn)) {
|
|
4858
|
+
out.push({ label, value: churn });
|
|
4859
|
+
}
|
|
4860
|
+
}
|
|
4861
|
+
return out;
|
|
4862
|
+
}
|
|
4863
|
+
function subjectLengthSeries(value) {
|
|
4864
|
+
return pickFields(asRecord(value).subjectLength, [
|
|
4865
|
+
["min", "Min"],
|
|
4866
|
+
["median", "Median"],
|
|
4867
|
+
["mean", "Mean"],
|
|
4868
|
+
["p90", "p90"],
|
|
4869
|
+
["max", "Max"]
|
|
4870
|
+
]);
|
|
4871
|
+
}
|
|
4872
|
+
var CHART_PLAN = {
|
|
4873
|
+
A: [
|
|
4874
|
+
{ title: "Commit volume over time", sourceId: "a-commit-volume", kind: "line", series: (v) => objectSeries(asRecord(v).perMonth) },
|
|
4875
|
+
{ title: "Commit frequency / cadence", sourceId: "a-commit-volume", kind: "bars", series: (v) => objectSeries(asRecord(v).perWeek) }
|
|
4876
|
+
],
|
|
4877
|
+
B: [
|
|
4878
|
+
{ title: "Contributor count", sourceId: "b-contributor-count", kind: "donut", series: contributorSplit },
|
|
4879
|
+
{ title: "Contribution distribution", sourceId: "b-contribution-distribution", kind: "gauge", gauge: (v) => pctField(v, "topCommitSharePct") }
|
|
4880
|
+
],
|
|
4881
|
+
C: [
|
|
4882
|
+
{ title: "Message length distribution", sourceId: "c-message-length-distribution", kind: "bars", series: subjectLengthSeries },
|
|
4883
|
+
{ title: "Conventional Commits adherence", sourceId: "c-conventional-commits", kind: "gauge", gauge: (v) => pctField(v, "adherenceSharePct") }
|
|
4884
|
+
],
|
|
4885
|
+
D: [
|
|
4886
|
+
{ title: "Branch/merge topology summary", sourceId: "d-topology-summary", kind: "bars", series: (v) => pickFields(v, [["regularCommitCount", "Regular"], ["mergeCommitCount", "Merges"], ["rootCommitCount", "Root"]]) },
|
|
4887
|
+
{ title: "Direct-to-default-branch rate", sourceId: "d-direct-to-default", kind: "gauge", gauge: (v) => pctField(v, "directToDefaultSharePct") }
|
|
4888
|
+
],
|
|
4889
|
+
E: [
|
|
4890
|
+
{ title: "Most-changed files / directories", sourceId: "e-most-changed", kind: "hbars", series: (v) => rowsSeries(v, "topFiles", "touchCount", 8) },
|
|
4891
|
+
{ title: "Churn rate over time", sourceId: "e-churn-over-time", kind: "line", series: churnByMonth }
|
|
4892
|
+
],
|
|
4893
|
+
F: [
|
|
4894
|
+
{ title: "Hygiene strengths & weaknesses", sourceId: "f-strengths-weaknesses", kind: "radar", series: hygieneDimensions },
|
|
4895
|
+
{ title: "Overall hygiene score", sourceId: "f-hygiene-score", kind: "gauge", gauge: (v) => pctField(v, "score") }
|
|
4896
|
+
]
|
|
4897
|
+
};
|
|
4790
4898
|
function subFigure(title2, svg, table) {
|
|
4791
4899
|
return `<div class="chart-sub">
|
|
4792
4900
|
<h4>${escapeHtml(title2)}</h4>
|
|
@@ -4794,22 +4902,24 @@ ${svg}
|
|
|
4794
4902
|
${table}
|
|
4795
4903
|
</div>`;
|
|
4796
4904
|
}
|
|
4797
|
-
function
|
|
4798
|
-
const
|
|
4799
|
-
|
|
4800
|
-
if (metric === void 0) {
|
|
4905
|
+
function renderChartSpec(group, spec, byId) {
|
|
4906
|
+
const metric = byId.get(spec.sourceId);
|
|
4907
|
+
if (metric?.status !== "computed") {
|
|
4801
4908
|
return void 0;
|
|
4802
4909
|
}
|
|
4803
|
-
const label = `Group ${group} \u2014 ${
|
|
4910
|
+
const label = `Group ${group} \u2014 ${spec.title}`;
|
|
4804
4911
|
if (spec.kind === "gauge") {
|
|
4805
|
-
const range =
|
|
4912
|
+
const range = spec.gauge?.(metric.value);
|
|
4806
4913
|
if (range === void 0) {
|
|
4807
4914
|
return void 0;
|
|
4808
4915
|
}
|
|
4809
|
-
const table = dataTable([{ label:
|
|
4810
|
-
return subFigure(
|
|
4916
|
+
const table = dataTable([{ label: spec.title, value: range.value }], "Value", spec.title);
|
|
4917
|
+
return subFigure(spec.title, svgRadialGauge(range.value, range.max, label), table);
|
|
4918
|
+
}
|
|
4919
|
+
const series = spec.series?.(metric.value) ?? [];
|
|
4920
|
+
if (series.length === 0) {
|
|
4921
|
+
return void 0;
|
|
4811
4922
|
}
|
|
4812
|
-
const series = extractSeries(metric.value);
|
|
4813
4923
|
let svg;
|
|
4814
4924
|
switch (spec.kind) {
|
|
4815
4925
|
case "line":
|
|
@@ -4827,12 +4937,13 @@ function renderSubChart(group, spec, metrics) {
|
|
|
4827
4937
|
default:
|
|
4828
4938
|
svg = svgDonut(series, label);
|
|
4829
4939
|
}
|
|
4830
|
-
return subFigure(
|
|
4940
|
+
return subFigure(spec.title, svg, dataTable(series, "Value", spec.title));
|
|
4831
4941
|
}
|
|
4832
4942
|
function groupOverviewPanel(group, metrics) {
|
|
4833
4943
|
const description = GROUP_DESCRIPTION[group];
|
|
4834
4944
|
const label = `Group ${group} overview`;
|
|
4835
|
-
const
|
|
4945
|
+
const byId = new Map(metrics.map((m) => [m.id, m]));
|
|
4946
|
+
const subs = CHART_PLAN[group].map((spec) => renderChartSpec(group, spec, byId)).filter((html) => html !== void 0);
|
|
4836
4947
|
if (subs.length === 0) {
|
|
4837
4948
|
return `<figure class="chart-panel" aria-label="${escapeHtml(label)}">
|
|
4838
4949
|
<figcaption>${escapeHtml(description)}</figcaption>
|
|
@@ -6110,7 +6221,7 @@ function createRetrieve(deps = {}) {
|
|
|
6110
6221
|
}
|
|
6111
6222
|
|
|
6112
6223
|
// src/cli/provenance.ts
|
|
6113
|
-
import { basename } from "path";
|
|
6224
|
+
import { basename, resolve } from "path";
|
|
6114
6225
|
function buildProvenance(input) {
|
|
6115
6226
|
const source = isRemoteTarget(input.target) ? "remote" : "local";
|
|
6116
6227
|
const repo = {
|
|
@@ -6183,13 +6294,17 @@ function localName(target) {
|
|
|
6183
6294
|
const trimmed = stripTrailingSlashes(target.trim());
|
|
6184
6295
|
const base = basename(trimmed);
|
|
6185
6296
|
if (base === "" || base === "." || base === "..") {
|
|
6297
|
+
const resolved = basename(resolve(trimmed === "" ? "." : trimmed));
|
|
6298
|
+
if (resolved !== "" && resolved !== "." && resolved !== "..") {
|
|
6299
|
+
return resolved;
|
|
6300
|
+
}
|
|
6186
6301
|
return trimmed === "" ? target.trim() : trimmed;
|
|
6187
6302
|
}
|
|
6188
6303
|
return base;
|
|
6189
6304
|
}
|
|
6190
6305
|
|
|
6191
6306
|
// src/cli/version.ts
|
|
6192
|
-
var VERSION = "1.1.
|
|
6307
|
+
var VERSION = "1.1.2";
|
|
6193
6308
|
|
|
6194
6309
|
// src/cli/write-file.ts
|
|
6195
6310
|
import { writeFile as fsWriteFile } from "fs/promises";
|