gitpulse-cli 0.1.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/dist/commands/analyze.d.ts +16 -0
- package/dist/commands/analyze.d.ts.map +1 -0
- package/dist/commands/compare.d.ts +11 -0
- package/dist/commands/compare.d.ts.map +1 -0
- package/dist/commands/config.d.ts +8 -0
- package/dist/commands/config.d.ts.map +1 -0
- package/dist/commands/history.d.ts +6 -0
- package/dist/commands/history.d.ts.map +1 -0
- package/dist/commands/report.d.ts +7 -0
- package/dist/commands/report.d.ts.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1202 -0
- package/dist/index.js.map +1 -0
- package/dist/repl/arg-parser.d.ts +33 -0
- package/dist/repl/arg-parser.d.ts.map +1 -0
- package/dist/repl/repl.d.ts +2 -0
- package/dist/repl/repl.d.ts.map +1 -0
- package/dist/ui/logo.d.ts +2 -0
- package/dist/ui/logo.d.ts.map +1 -0
- package/dist/ui/progress.d.ts +3 -0
- package/dist/ui/progress.d.ts.map +1 -0
- package/dist/ui/spinner.d.ts +4 -0
- package/dist/ui/spinner.d.ts.map +1 -0
- package/dist/ui/table.d.ts +4 -0
- package/dist/ui/table.d.ts.map +1 -0
- package/dist/ui/theme.d.ts +15 -0
- package/dist/ui/theme.d.ts.map +1 -0
- package/dist/utils/error-handler.d.ts +3 -0
- package/dist/utils/error-handler.d.ts.map +1 -0
- package/dist/utils/remote-repo.d.ts +5 -0
- package/dist/utils/remote-repo.d.ts.map +1 -0
- package/dist/utils/version.d.ts +2 -0
- package/dist/utils/version.d.ts.map +1 -0
- package/dist/wizard/onboarding.d.ts +3 -0
- package/dist/wizard/onboarding.d.ts.map +1 -0
- package/dist/wizard/setup.d.ts +9 -0
- package/dist/wizard/setup.d.ts.map +1 -0
- package/package.json +45 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1202 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
|
|
6
|
+
// src/ui/logo.ts
|
|
7
|
+
import chalk from "chalk";
|
|
8
|
+
var LOGO = `
|
|
9
|
+
_____ _ _ _____ _
|
|
10
|
+
/ ____(_) | | __ \\ | |
|
|
11
|
+
| | __ _| |_| |__) | _| |___ ___
|
|
12
|
+
| | |_ | | __| ___/ | | | / __|/ _ \\
|
|
13
|
+
| |__| | | |_| | | |_| | \\__ \\ __/
|
|
14
|
+
\\_____|_|\\__|_| \\__,_|_|___/\\___|
|
|
15
|
+
`;
|
|
16
|
+
function printLogo() {
|
|
17
|
+
console.log(chalk.hex("#6366f1")(LOGO));
|
|
18
|
+
console.log(chalk.dim(" AI-Powered Git Contribution Analyzer\n"));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// src/utils/version.ts
|
|
22
|
+
import * as fs from "fs";
|
|
23
|
+
import * as path from "path";
|
|
24
|
+
import { fileURLToPath } from "url";
|
|
25
|
+
function getVersion() {
|
|
26
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
27
|
+
let dir = path.dirname(__filename);
|
|
28
|
+
for (let i = 0; i < 10; i++) {
|
|
29
|
+
const pkgPath = path.join(dir, "package.json");
|
|
30
|
+
if (fs.existsSync(pkgPath)) {
|
|
31
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
|
|
32
|
+
return pkg.version ?? "0.0.0";
|
|
33
|
+
}
|
|
34
|
+
const parent = path.dirname(dir);
|
|
35
|
+
if (parent === dir) break;
|
|
36
|
+
dir = parent;
|
|
37
|
+
}
|
|
38
|
+
return "0.0.0";
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// src/commands/analyze.ts
|
|
42
|
+
import * as path3 from "path";
|
|
43
|
+
import * as fs3 from "fs";
|
|
44
|
+
import { confirm, select, input } from "@inquirer/prompts";
|
|
45
|
+
import {
|
|
46
|
+
Analyzer,
|
|
47
|
+
createProvider,
|
|
48
|
+
generateReport,
|
|
49
|
+
loadConfig,
|
|
50
|
+
HomeManager
|
|
51
|
+
} from "@gitpulse/core";
|
|
52
|
+
|
|
53
|
+
// src/ui/theme.ts
|
|
54
|
+
import chalk2 from "chalk";
|
|
55
|
+
var theme = {
|
|
56
|
+
primary: chalk2.hex("#6366f1"),
|
|
57
|
+
secondary: chalk2.hex("#8b5cf6"),
|
|
58
|
+
success: chalk2.hex("#22c55e"),
|
|
59
|
+
warning: chalk2.hex("#f59e0b"),
|
|
60
|
+
error: chalk2.hex("#ef4444"),
|
|
61
|
+
info: chalk2.hex("#3b82f6"),
|
|
62
|
+
dim: chalk2.dim,
|
|
63
|
+
bold: chalk2.bold,
|
|
64
|
+
heading: chalk2.bold.hex("#6366f1"),
|
|
65
|
+
prompt: chalk2.hex("#6366f1").bold,
|
|
66
|
+
score: (score) => {
|
|
67
|
+
if (score >= 80) return chalk2.hex("#22c55e")(score.toFixed(1));
|
|
68
|
+
if (score >= 60) return chalk2.hex("#3b82f6")(score.toFixed(1));
|
|
69
|
+
if (score >= 40) return chalk2.hex("#f59e0b")(score.toFixed(1));
|
|
70
|
+
return chalk2.hex("#ef4444")(score.toFixed(1));
|
|
71
|
+
},
|
|
72
|
+
trend: (direction) => {
|
|
73
|
+
switch (direction) {
|
|
74
|
+
case "improving":
|
|
75
|
+
return chalk2.hex("#22c55e")("\u25B2 improving");
|
|
76
|
+
case "declining":
|
|
77
|
+
return chalk2.hex("#ef4444")("\u25BC declining");
|
|
78
|
+
case "stable":
|
|
79
|
+
return chalk2.hex("#94a3b8")("\u25BA stable");
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
// src/ui/spinner.ts
|
|
85
|
+
import ora from "ora";
|
|
86
|
+
function createSpinner(text) {
|
|
87
|
+
return ora({
|
|
88
|
+
text: theme.dim(text),
|
|
89
|
+
color: "cyan"
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
function phaseSpinner(phase, totalPhases, text) {
|
|
93
|
+
return createSpinner(`[Phase ${phase}/${totalPhases}] ${text}`);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// src/ui/table.ts
|
|
97
|
+
import Table from "cli-table3";
|
|
98
|
+
function renderScoreTable(report) {
|
|
99
|
+
const table = new Table({
|
|
100
|
+
head: [
|
|
101
|
+
theme.bold("Author"),
|
|
102
|
+
theme.bold("Overall"),
|
|
103
|
+
theme.bold("Code Quality"),
|
|
104
|
+
theme.bold("Complexity"),
|
|
105
|
+
theme.bold("Discipline"),
|
|
106
|
+
theme.bold("Collab"),
|
|
107
|
+
theme.bold("Trend"),
|
|
108
|
+
theme.bold("Commits")
|
|
109
|
+
],
|
|
110
|
+
style: {
|
|
111
|
+
head: [],
|
|
112
|
+
border: ["dim"]
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
for (const author of report.authors) {
|
|
116
|
+
const s = author.score;
|
|
117
|
+
table.push([
|
|
118
|
+
s.authorName,
|
|
119
|
+
theme.score(s.overallScore),
|
|
120
|
+
theme.score(s.dimensionScores.codeQuality.score),
|
|
121
|
+
theme.score(s.dimensionScores.complexityImpact.score),
|
|
122
|
+
theme.score(s.dimensionScores.commitDiscipline.score),
|
|
123
|
+
theme.score(s.dimensionScores.collaboration.score),
|
|
124
|
+
theme.trend(s.trend.direction),
|
|
125
|
+
String(s.scoredCommitCount)
|
|
126
|
+
]);
|
|
127
|
+
}
|
|
128
|
+
console.log(table.toString());
|
|
129
|
+
}
|
|
130
|
+
function renderSummaryTable(report) {
|
|
131
|
+
const table = new Table({
|
|
132
|
+
style: { head: [], border: ["dim"] }
|
|
133
|
+
});
|
|
134
|
+
table.push(
|
|
135
|
+
{ [theme.bold("Total Commits")]: String(report.metadata.totalCommits) },
|
|
136
|
+
{ [theme.bold("Analyzed")]: String(report.metadata.analyzedCommits) },
|
|
137
|
+
{ [theme.bold("Cached")]: String(report.metadata.cachedCommits) },
|
|
138
|
+
{ [theme.bold("Skipped")]: String(report.metadata.skippedCommits) },
|
|
139
|
+
{ [theme.bold("Authors")]: String(report.summary.totalAuthors) },
|
|
140
|
+
{ [theme.bold("Average Score")]: theme.score(report.summary.averageScore) },
|
|
141
|
+
{ [theme.bold("Top Performer")]: report.summary.topPerformer }
|
|
142
|
+
);
|
|
143
|
+
console.log(table.toString());
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// src/ui/progress.ts
|
|
147
|
+
import cliProgress from "cli-progress";
|
|
148
|
+
function createProgressBar(total) {
|
|
149
|
+
const bar = new cliProgress.SingleBar({
|
|
150
|
+
format: ` ${theme.primary("{bar}")} {value}/{total} ({percentage}%) | {message}`,
|
|
151
|
+
barCompleteChar: "\u2588",
|
|
152
|
+
barIncompleteChar: "\u2591",
|
|
153
|
+
hideCursor: true
|
|
154
|
+
});
|
|
155
|
+
bar.start(total, 0, { message: "" });
|
|
156
|
+
return bar;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// src/utils/error-handler.ts
|
|
160
|
+
function handleError(error) {
|
|
161
|
+
if (error instanceof Error) {
|
|
162
|
+
console.error(`
|
|
163
|
+
${theme.error("Error:")} ${error.message}`);
|
|
164
|
+
if (process.env.DEBUG) {
|
|
165
|
+
console.error(theme.dim(error.stack ?? ""));
|
|
166
|
+
}
|
|
167
|
+
} else {
|
|
168
|
+
console.error(`
|
|
169
|
+
${theme.error("Error:")} An unexpected error occurred`);
|
|
170
|
+
}
|
|
171
|
+
process.exit(1);
|
|
172
|
+
}
|
|
173
|
+
function handleReplError(error) {
|
|
174
|
+
if (error instanceof Error) {
|
|
175
|
+
console.error(`
|
|
176
|
+
${theme.error("Error:")} ${error.message}`);
|
|
177
|
+
if (process.env.DEBUG) {
|
|
178
|
+
console.error(theme.dim(error.stack ?? ""));
|
|
179
|
+
}
|
|
180
|
+
} else {
|
|
181
|
+
console.error(`
|
|
182
|
+
${theme.error("Error:")} An unexpected error occurred`);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// src/utils/remote-repo.ts
|
|
187
|
+
import * as path2 from "path";
|
|
188
|
+
import * as fs2 from "fs";
|
|
189
|
+
import * as os from "os";
|
|
190
|
+
import * as crypto from "crypto";
|
|
191
|
+
import simpleGit from "simple-git";
|
|
192
|
+
var REMOTE_URL_RE = /^(?:https?:\/\/|git@|ssh:\/\/|git:\/\/)/;
|
|
193
|
+
function isRemoteUrl(input4) {
|
|
194
|
+
return REMOTE_URL_RE.test(input4);
|
|
195
|
+
}
|
|
196
|
+
function repoSlug(url) {
|
|
197
|
+
const cleaned = url.replace(/^(?:https?:\/\/|git@|ssh:\/\/|git:\/\/)/, "").replace(/\.git\/?$/, "").replace(/:/g, "/").replace(/\/+$/, "");
|
|
198
|
+
const slug = cleaned.replace(/[^a-zA-Z0-9_.-]/g, "-").replace(/-+/g, "-");
|
|
199
|
+
const hash = crypto.createHash("sha256").update(url).digest("hex").slice(0, 8);
|
|
200
|
+
return `${slug}-${hash}`;
|
|
201
|
+
}
|
|
202
|
+
function getCacheDir() {
|
|
203
|
+
return path2.join(os.homedir(), ".gitpulse", "repos");
|
|
204
|
+
}
|
|
205
|
+
async function resolveRepoPath(input4) {
|
|
206
|
+
if (!isRemoteUrl(input4)) {
|
|
207
|
+
return { localPath: input4 };
|
|
208
|
+
}
|
|
209
|
+
const slug = repoSlug(input4);
|
|
210
|
+
const cacheDir = getCacheDir();
|
|
211
|
+
const localPath = path2.join(cacheDir, slug);
|
|
212
|
+
fs2.mkdirSync(cacheDir, { recursive: true });
|
|
213
|
+
if (fs2.existsSync(path2.join(localPath, ".git"))) {
|
|
214
|
+
console.log(theme.dim(`Using cached clone at ${localPath}`));
|
|
215
|
+
console.log(theme.dim("Fetching latest changes..."));
|
|
216
|
+
try {
|
|
217
|
+
const git = simpleGit(localPath);
|
|
218
|
+
await git.fetch(["--all"]);
|
|
219
|
+
console.log(theme.success("Fetch complete.\n"));
|
|
220
|
+
} catch (err) {
|
|
221
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
222
|
+
console.log(theme.warning(`Fetch failed (using cached clone): ${msg}
|
|
223
|
+
`));
|
|
224
|
+
}
|
|
225
|
+
} else {
|
|
226
|
+
console.log(theme.dim(`Cloning ${input4}...`));
|
|
227
|
+
const git = simpleGit();
|
|
228
|
+
await git.clone(input4, localPath);
|
|
229
|
+
console.log(theme.success("Clone complete.\n"));
|
|
230
|
+
}
|
|
231
|
+
return { localPath };
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// src/commands/analyze.ts
|
|
235
|
+
async function analyzeCommandInner(repoPath, options) {
|
|
236
|
+
const { localPath } = await resolveRepoPath(repoPath);
|
|
237
|
+
const absolutePath = path3.resolve(localPath);
|
|
238
|
+
const home = new HomeManager();
|
|
239
|
+
home.ensureInitialized();
|
|
240
|
+
const overrides = {};
|
|
241
|
+
if (options.provider) overrides.provider = options.provider;
|
|
242
|
+
if (options.model) overrides.model = options.model;
|
|
243
|
+
const config = await loadConfig(absolutePath, overrides);
|
|
244
|
+
const format = options.format ?? config.output.format;
|
|
245
|
+
const model = await createProvider({
|
|
246
|
+
type: config.provider,
|
|
247
|
+
model: config.model,
|
|
248
|
+
apiKey: config.apiKey,
|
|
249
|
+
baseURL: config.baseURL
|
|
250
|
+
});
|
|
251
|
+
const analyzer = await Analyzer.create(absolutePath, {
|
|
252
|
+
config,
|
|
253
|
+
model,
|
|
254
|
+
noCache: options.noCache
|
|
255
|
+
});
|
|
256
|
+
const scope = {
|
|
257
|
+
branch: options.branch,
|
|
258
|
+
since: options.since,
|
|
259
|
+
until: options.until,
|
|
260
|
+
authors: options.author,
|
|
261
|
+
maxCommits: options.maxCommits,
|
|
262
|
+
path: absolutePath
|
|
263
|
+
};
|
|
264
|
+
if (!options.since && !options.until && !options.yes) {
|
|
265
|
+
const fmt = (d) => d.toISOString().slice(0, 10);
|
|
266
|
+
const ago = (months) => {
|
|
267
|
+
const d = /* @__PURE__ */ new Date();
|
|
268
|
+
d.setMonth(d.getMonth() - months);
|
|
269
|
+
return fmt(d);
|
|
270
|
+
};
|
|
271
|
+
const range = await select({
|
|
272
|
+
message: "Select time range for analysis:",
|
|
273
|
+
choices: [
|
|
274
|
+
{ name: "Last month", value: "last-1" },
|
|
275
|
+
{ name: "Last 3 months", value: "last-3" },
|
|
276
|
+
{ name: "Last 6 months", value: "last-6" },
|
|
277
|
+
{ name: "Last year", value: "last-12" },
|
|
278
|
+
{ name: "All time (no filter)", value: "all" },
|
|
279
|
+
{ name: "Custom range...", value: "custom" }
|
|
280
|
+
]
|
|
281
|
+
});
|
|
282
|
+
if (range === "custom") {
|
|
283
|
+
const customSince = await input({ message: "Since (YYYY-MM-DD, leave empty for none):" });
|
|
284
|
+
const customUntil = await input({ message: "Until (YYYY-MM-DD, leave empty for none):" });
|
|
285
|
+
if (customSince) scope.since = customSince;
|
|
286
|
+
if (customUntil) scope.until = customUntil;
|
|
287
|
+
} else if (range !== "all") {
|
|
288
|
+
const months = parseInt(range.split("-")[1], 10);
|
|
289
|
+
scope.since = ago(months);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
const estimate = await analyzer.estimate(scope);
|
|
293
|
+
console.log(theme.heading("\nAnalysis Summary:"));
|
|
294
|
+
console.log(` Total commits: ${estimate.totalCommits}`);
|
|
295
|
+
console.log(` Cached (skip): ${estimate.cachedCommits}`);
|
|
296
|
+
console.log(` To analyze: ${estimate.toAnalyze}`);
|
|
297
|
+
console.log(` Est. LLM calls: ~${estimate.estimatedLlmCalls}`);
|
|
298
|
+
console.log(` Est. tokens: ~${(estimate.estimatedTokens / 1e3).toFixed(0)}K`);
|
|
299
|
+
console.log(` Est. cost: ~$${estimate.estimatedCost.toFixed(2)}`);
|
|
300
|
+
console.log("");
|
|
301
|
+
if (estimate.toAnalyze === 0) {
|
|
302
|
+
console.log(theme.success("All commits are cached. Generating report from cache.\n"));
|
|
303
|
+
} else if (!options.yes) {
|
|
304
|
+
const proceed = await confirm({ message: "Proceed with analysis?", default: true });
|
|
305
|
+
if (!proceed) {
|
|
306
|
+
console.log(theme.dim("Analysis cancelled."));
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
const startTime = Date.now();
|
|
311
|
+
let currentSpinner = phaseSpinner(1, 3, "Extracting commits");
|
|
312
|
+
currentSpinner.start();
|
|
313
|
+
const state = { progressBar: null };
|
|
314
|
+
const onProgress = (progress) => {
|
|
315
|
+
if (progress.phase === "extracting") {
|
|
316
|
+
currentSpinner.text = theme.dim(
|
|
317
|
+
`[Phase 1/3] ${progress.message}`
|
|
318
|
+
);
|
|
319
|
+
} else if (progress.phase === "scoring") {
|
|
320
|
+
if (currentSpinner.isSpinning) {
|
|
321
|
+
currentSpinner.succeed(theme.dim("Commits extracted"));
|
|
322
|
+
}
|
|
323
|
+
if (!state.progressBar && progress.total > 0) {
|
|
324
|
+
console.log(theme.dim(`
|
|
325
|
+
Scoring commits [Phase 2/3]:`));
|
|
326
|
+
state.progressBar = createProgressBar(progress.total);
|
|
327
|
+
}
|
|
328
|
+
if (state.progressBar) {
|
|
329
|
+
state.progressBar.update(progress.current, { message: progress.message });
|
|
330
|
+
}
|
|
331
|
+
} else if (progress.phase === "aggregating") {
|
|
332
|
+
if (state.progressBar) {
|
|
333
|
+
state.progressBar.stop();
|
|
334
|
+
state.progressBar = null;
|
|
335
|
+
}
|
|
336
|
+
if (currentSpinner.isSpinning) {
|
|
337
|
+
currentSpinner.succeed(theme.dim("Scoring complete"));
|
|
338
|
+
}
|
|
339
|
+
currentSpinner = phaseSpinner(3, 3, "Aggregating results");
|
|
340
|
+
currentSpinner.start();
|
|
341
|
+
}
|
|
342
|
+
};
|
|
343
|
+
const report = await analyzer.analyze(scope, onProgress);
|
|
344
|
+
if (state.progressBar) state.progressBar.stop();
|
|
345
|
+
if (currentSpinner.isSpinning) {
|
|
346
|
+
currentSpinner.succeed(theme.dim("Report generated"));
|
|
347
|
+
}
|
|
348
|
+
const durationMs = Date.now() - startTime;
|
|
349
|
+
try {
|
|
350
|
+
home.recordAnalysis(report, scope, durationMs, estimate);
|
|
351
|
+
home.updateMemory(report);
|
|
352
|
+
} catch {
|
|
353
|
+
}
|
|
354
|
+
if (format === "terminal") {
|
|
355
|
+
console.log(theme.heading("\nResults:\n"));
|
|
356
|
+
renderSummaryTable(report);
|
|
357
|
+
console.log("");
|
|
358
|
+
renderScoreTable(report);
|
|
359
|
+
for (const author of report.authors) {
|
|
360
|
+
console.log(`
|
|
361
|
+
${theme.bold(author.score.authorName)}`);
|
|
362
|
+
console.log(` Score: ${theme.score(author.score.overallScore)}/100 | Trend: ${theme.trend(author.score.trend.direction)}`);
|
|
363
|
+
if (author.highlights.length > 0) {
|
|
364
|
+
console.log(` Highlights: ${author.highlights.join(", ")}`);
|
|
365
|
+
}
|
|
366
|
+
if (author.recommendations.length > 0) {
|
|
367
|
+
console.log(` Recommendations: ${author.recommendations.join(", ")}`);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
} else {
|
|
371
|
+
const output = generateReport(report, format);
|
|
372
|
+
if (options.output) {
|
|
373
|
+
const outputPath = path3.resolve(options.output);
|
|
374
|
+
fs3.writeFileSync(outputPath, output, "utf-8");
|
|
375
|
+
console.log(theme.success(`
|
|
376
|
+
Report saved to ${outputPath}`));
|
|
377
|
+
} else {
|
|
378
|
+
console.log(output);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
console.log("");
|
|
382
|
+
}
|
|
383
|
+
async function analyzeCommand(repoPath, options) {
|
|
384
|
+
try {
|
|
385
|
+
await analyzeCommandInner(repoPath, options);
|
|
386
|
+
} catch (error) {
|
|
387
|
+
handleError(error);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// src/commands/report.ts
|
|
392
|
+
import * as path4 from "path";
|
|
393
|
+
import * as fs4 from "fs";
|
|
394
|
+
import {
|
|
395
|
+
CacheManager,
|
|
396
|
+
loadConfig as loadConfig2,
|
|
397
|
+
generateReport as generateReport2,
|
|
398
|
+
loadAllRubrics,
|
|
399
|
+
computeRubricHash
|
|
400
|
+
} from "@gitpulse/core";
|
|
401
|
+
async function reportCommandInner(repoPath, options) {
|
|
402
|
+
const { localPath } = await resolveRepoPath(repoPath);
|
|
403
|
+
const absolutePath = path4.resolve(localPath);
|
|
404
|
+
const config = await loadConfig2(absolutePath);
|
|
405
|
+
const format = options.format ?? config.output.format;
|
|
406
|
+
const rubrics = loadAllRubrics(absolutePath);
|
|
407
|
+
const rubricHash = computeRubricHash(rubrics);
|
|
408
|
+
const cache = new CacheManager(absolutePath);
|
|
409
|
+
const cached = cache.getAllCached(rubricHash);
|
|
410
|
+
if (cached.size === 0) {
|
|
411
|
+
console.log(theme.warning("No cached scores found. Run `gitpulse analyze` first."));
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
console.log(theme.dim(`Found ${cached.size} cached commit scores.
|
|
415
|
+
`));
|
|
416
|
+
const commitScores = Array.from(cached.values());
|
|
417
|
+
const report = buildReportFromCache(commitScores, absolutePath, config.provider, config.model, config.scoring.weights);
|
|
418
|
+
if (format === "terminal") {
|
|
419
|
+
renderSummaryTable(report);
|
|
420
|
+
console.log("");
|
|
421
|
+
renderScoreTable(report);
|
|
422
|
+
} else {
|
|
423
|
+
const output = generateReport2(report, format);
|
|
424
|
+
if (options.output) {
|
|
425
|
+
const outputPath = path4.resolve(options.output);
|
|
426
|
+
fs4.writeFileSync(outputPath, output, "utf-8");
|
|
427
|
+
console.log(theme.success(`Report saved to ${outputPath}`));
|
|
428
|
+
} else {
|
|
429
|
+
console.log(output);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
async function reportCommand(repoPath, options) {
|
|
434
|
+
try {
|
|
435
|
+
await reportCommandInner(repoPath, options);
|
|
436
|
+
} catch (error) {
|
|
437
|
+
handleError(error);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
function buildReportFromCache(scores, repoPath, provider, model, weights) {
|
|
441
|
+
return {
|
|
442
|
+
metadata: {
|
|
443
|
+
generatedAt: /* @__PURE__ */ new Date(),
|
|
444
|
+
repositoryPath: repoPath,
|
|
445
|
+
scope: {},
|
|
446
|
+
dimensionWeights: weights,
|
|
447
|
+
totalCommits: scores.length,
|
|
448
|
+
analyzedCommits: scores.length,
|
|
449
|
+
cachedCommits: scores.length,
|
|
450
|
+
skippedCommits: 0,
|
|
451
|
+
llmProvider: provider,
|
|
452
|
+
llmModel: model
|
|
453
|
+
},
|
|
454
|
+
summary: {
|
|
455
|
+
averageScore: scores.reduce((s, c) => s + c.overallScore, 0) / scores.length,
|
|
456
|
+
medianScore: [...scores].sort((a, b) => a.overallScore - b.overallScore)[Math.floor(scores.length / 2)]?.overallScore ?? 0,
|
|
457
|
+
topPerformer: "N/A (from cache)",
|
|
458
|
+
totalAuthors: 0,
|
|
459
|
+
dateRange: { start: /* @__PURE__ */ new Date(), end: /* @__PURE__ */ new Date() }
|
|
460
|
+
},
|
|
461
|
+
authors: [],
|
|
462
|
+
commitScores: scores
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// src/commands/config.ts
|
|
467
|
+
import { loadConfig as loadConfig3, HomeManager as HomeManager2 } from "@gitpulse/core";
|
|
468
|
+
|
|
469
|
+
// src/wizard/setup.ts
|
|
470
|
+
import * as fs5 from "fs";
|
|
471
|
+
import * as path5 from "path";
|
|
472
|
+
import { select as select2, input as input2, confirm as confirm2 } from "@inquirer/prompts";
|
|
473
|
+
async function runSetupWizard(targetDir) {
|
|
474
|
+
console.log(theme.heading("\nGitPulse Configuration Wizard\n"));
|
|
475
|
+
const provider = await select2({
|
|
476
|
+
message: "Select your LLM provider:",
|
|
477
|
+
choices: [
|
|
478
|
+
{ value: "openai", name: "OpenAI (GPT-4o, GPT-4o-mini)" },
|
|
479
|
+
{ value: "anthropic", name: "Anthropic (Claude)" },
|
|
480
|
+
{ value: "google", name: "Google (Gemini)" },
|
|
481
|
+
{ value: "vertex", name: "Google Vertex AI (Gemini via Vertex)" },
|
|
482
|
+
{ value: "custom", name: "Custom (OpenAI-compatible endpoint)" }
|
|
483
|
+
]
|
|
484
|
+
});
|
|
485
|
+
const defaultModels = {
|
|
486
|
+
openai: "gpt-4o",
|
|
487
|
+
anthropic: "claude-sonnet-4-20250514",
|
|
488
|
+
google: "gemini-1.5-pro",
|
|
489
|
+
vertex: "gemini-1.5-pro",
|
|
490
|
+
custom: "default"
|
|
491
|
+
};
|
|
492
|
+
const model = await input2({
|
|
493
|
+
message: "Model name:",
|
|
494
|
+
default: defaultModels[provider] ?? "gpt-4o"
|
|
495
|
+
});
|
|
496
|
+
let baseURL;
|
|
497
|
+
if (provider === "custom") {
|
|
498
|
+
baseURL = await input2({
|
|
499
|
+
message: "Base URL for the API:",
|
|
500
|
+
validate: (v) => {
|
|
501
|
+
try {
|
|
502
|
+
new URL(v);
|
|
503
|
+
return true;
|
|
504
|
+
} catch {
|
|
505
|
+
return "Please enter a valid URL";
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
});
|
|
509
|
+
}
|
|
510
|
+
const envVarNames = {
|
|
511
|
+
openai: "OPENAI_API_KEY",
|
|
512
|
+
anthropic: "ANTHROPIC_API_KEY",
|
|
513
|
+
google: "GOOGLE_GENERATIVE_AI_API_KEY",
|
|
514
|
+
vertex: "GOOGLE_VERTEX_API_KEY",
|
|
515
|
+
custom: "API_KEY"
|
|
516
|
+
};
|
|
517
|
+
const envVar = envVarNames[provider] ?? "API_KEY";
|
|
518
|
+
console.log(
|
|
519
|
+
theme.dim(
|
|
520
|
+
`
|
|
521
|
+
Tip: Set your API key via the ${theme.info(envVar)} environment variable.
|
|
522
|
+
`
|
|
523
|
+
)
|
|
524
|
+
);
|
|
525
|
+
const customize = await confirm2({
|
|
526
|
+
message: "Customize scoring weights?",
|
|
527
|
+
default: false
|
|
528
|
+
});
|
|
529
|
+
let weights = {
|
|
530
|
+
codeQuality: 0.3,
|
|
531
|
+
complexityImpact: 0.25,
|
|
532
|
+
commitDiscipline: 0.25,
|
|
533
|
+
collaboration: 0.2
|
|
534
|
+
};
|
|
535
|
+
if (customize) {
|
|
536
|
+
console.log(theme.dim("Enter weights (must sum to 1.0):"));
|
|
537
|
+
const cq = await input2({ message: "Code Quality weight:", default: "0.30" });
|
|
538
|
+
const ci = await input2({ message: "Complexity & Impact weight:", default: "0.25" });
|
|
539
|
+
const cd = await input2({ message: "Commit Discipline weight:", default: "0.25" });
|
|
540
|
+
const co = await input2({ message: "Collaboration weight:", default: "0.20" });
|
|
541
|
+
weights = {
|
|
542
|
+
codeQuality: parseFloat(cq),
|
|
543
|
+
complexityImpact: parseFloat(ci),
|
|
544
|
+
commitDiscipline: parseFloat(cd),
|
|
545
|
+
collaboration: parseFloat(co)
|
|
546
|
+
};
|
|
547
|
+
}
|
|
548
|
+
const config = buildConfigYaml(provider, model, baseURL, weights);
|
|
549
|
+
const configPath = path5.join(targetDir, ".gitpulse.yml");
|
|
550
|
+
fs5.writeFileSync(configPath, config, "utf-8");
|
|
551
|
+
console.log(theme.success(`
|
|
552
|
+
Configuration saved to ${configPath}`));
|
|
553
|
+
}
|
|
554
|
+
function buildConfigYaml(provider, model, baseURL, weights) {
|
|
555
|
+
let yaml = `# GitPulse Configuration
|
|
556
|
+
provider: ${provider}
|
|
557
|
+
model: ${model}
|
|
558
|
+
`;
|
|
559
|
+
if (baseURL) {
|
|
560
|
+
yaml += `baseURL: ${baseURL}
|
|
561
|
+
`;
|
|
562
|
+
}
|
|
563
|
+
yaml += `
|
|
564
|
+
scoring:
|
|
565
|
+
weights:
|
|
566
|
+
codeQuality: ${weights.codeQuality}
|
|
567
|
+
complexityImpact: ${weights.complexityImpact}
|
|
568
|
+
commitDiscipline: ${weights.commitDiscipline}
|
|
569
|
+
collaboration: ${weights.collaboration}
|
|
570
|
+
timeDecay: false
|
|
571
|
+
maxTokensPerDiff: 8000
|
|
572
|
+
|
|
573
|
+
analysis:
|
|
574
|
+
maxConcurrency: 3
|
|
575
|
+
skipMergeCommits: true
|
|
576
|
+
skipLockFiles: true
|
|
577
|
+
skipAutoGenerated: true
|
|
578
|
+
|
|
579
|
+
output:
|
|
580
|
+
format: terminal
|
|
581
|
+
anonymize: false
|
|
582
|
+
|
|
583
|
+
privacy:
|
|
584
|
+
excludeFileContents: false
|
|
585
|
+
anonymize: false
|
|
586
|
+
`;
|
|
587
|
+
return yaml;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
// src/commands/config.ts
|
|
591
|
+
async function configCommandInner(options, setValue) {
|
|
592
|
+
if (options.init) {
|
|
593
|
+
const home = new HomeManager2();
|
|
594
|
+
const result = home.initialize();
|
|
595
|
+
if (result.created) {
|
|
596
|
+
console.log(theme.success(`Created ${result.homePath}/`));
|
|
597
|
+
} else {
|
|
598
|
+
console.log(theme.dim(`Home directory already exists: ${result.homePath}/`));
|
|
599
|
+
}
|
|
600
|
+
if (result.copiedRubrics.length > 0) {
|
|
601
|
+
console.log(theme.success(`Copied rubrics: ${result.copiedRubrics.join(", ")}`));
|
|
602
|
+
}
|
|
603
|
+
if (result.skippedRubrics.length > 0) {
|
|
604
|
+
console.log(theme.dim(`Skipped existing rubrics: ${result.skippedRubrics.join(", ")}`));
|
|
605
|
+
}
|
|
606
|
+
await runSetupWizard(process.cwd());
|
|
607
|
+
return;
|
|
608
|
+
}
|
|
609
|
+
if (options.show || !options.set && !options.init) {
|
|
610
|
+
const config = await loadConfig3(process.cwd());
|
|
611
|
+
console.log(theme.heading("\nCurrent Configuration:\n"));
|
|
612
|
+
console.log(JSON.stringify(config, null, 2));
|
|
613
|
+
return;
|
|
614
|
+
}
|
|
615
|
+
if (options.set && setValue) {
|
|
616
|
+
console.log(theme.dim(`Setting ${options.set} = ${setValue}`));
|
|
617
|
+
console.log(theme.warning("Direct config setting via CLI not yet implemented. Edit .gitpulse.yml directly."));
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
async function configCommand(options, setValue) {
|
|
621
|
+
try {
|
|
622
|
+
await configCommandInner(options, setValue);
|
|
623
|
+
} catch (error) {
|
|
624
|
+
handleError(error);
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
// src/commands/compare.ts
|
|
629
|
+
import * as path6 from "path";
|
|
630
|
+
import {
|
|
631
|
+
Analyzer as Analyzer2,
|
|
632
|
+
createProvider as createProvider2,
|
|
633
|
+
loadConfig as loadConfig4
|
|
634
|
+
} from "@gitpulse/core";
|
|
635
|
+
async function compareCommandInner(author1, author2, options) {
|
|
636
|
+
const { localPath } = await resolveRepoPath(options.path ?? ".");
|
|
637
|
+
const repoPath = path6.resolve(localPath);
|
|
638
|
+
const config = await loadConfig4(repoPath);
|
|
639
|
+
const model = await createProvider2({
|
|
640
|
+
type: config.provider,
|
|
641
|
+
model: config.model,
|
|
642
|
+
apiKey: config.apiKey,
|
|
643
|
+
baseURL: config.baseURL
|
|
644
|
+
});
|
|
645
|
+
const analyzer = await Analyzer2.create(repoPath, { config, model });
|
|
646
|
+
const scope = {
|
|
647
|
+
since: options.since,
|
|
648
|
+
until: options.until,
|
|
649
|
+
path: repoPath
|
|
650
|
+
};
|
|
651
|
+
console.log(theme.heading(`
|
|
652
|
+
Comparing: ${author1} vs ${author2}
|
|
653
|
+
`));
|
|
654
|
+
console.log(theme.dim("Running analysis...\n"));
|
|
655
|
+
const report = await analyzer.analyze(scope);
|
|
656
|
+
const a1 = report.authors.find(
|
|
657
|
+
(a) => a.score.authorName.toLowerCase().includes(author1.toLowerCase()) || a.score.authorEmail.toLowerCase().includes(author1.toLowerCase())
|
|
658
|
+
);
|
|
659
|
+
const a2 = report.authors.find(
|
|
660
|
+
(a) => a.score.authorName.toLowerCase().includes(author2.toLowerCase()) || a.score.authorEmail.toLowerCase().includes(author2.toLowerCase())
|
|
661
|
+
);
|
|
662
|
+
if (!a1) {
|
|
663
|
+
console.log(theme.error(`Author not found: ${author1}`));
|
|
664
|
+
return;
|
|
665
|
+
}
|
|
666
|
+
if (!a2) {
|
|
667
|
+
console.log(theme.error(`Author not found: ${author2}`));
|
|
668
|
+
return;
|
|
669
|
+
}
|
|
670
|
+
const dims = [
|
|
671
|
+
{ label: "Overall", get: (a) => a.score.overallScore },
|
|
672
|
+
{ label: "Code Quality", get: (a) => a.score.dimensionScores.codeQuality.score },
|
|
673
|
+
{ label: "Complexity", get: (a) => a.score.dimensionScores.complexityImpact.score },
|
|
674
|
+
{ label: "Discipline", get: (a) => a.score.dimensionScores.commitDiscipline.score },
|
|
675
|
+
{ label: "Collaboration", get: (a) => a.score.dimensionScores.collaboration.score }
|
|
676
|
+
];
|
|
677
|
+
const nameWidth = Math.max(a1.score.authorName.length, a2.score.authorName.length, 15);
|
|
678
|
+
console.log(
|
|
679
|
+
`${"Dimension".padEnd(20)} ${a1.score.authorName.padEnd(nameWidth)} ${a2.score.authorName.padEnd(nameWidth)} ${"Winner"}`
|
|
680
|
+
);
|
|
681
|
+
console.log("-".repeat(20 + nameWidth * 2 + 15));
|
|
682
|
+
for (const dim of dims) {
|
|
683
|
+
const v1 = dim.get(a1);
|
|
684
|
+
const v2 = dim.get(a2);
|
|
685
|
+
const winner = v1 > v2 ? a1.score.authorName : v2 > v1 ? a2.score.authorName : "Tie";
|
|
686
|
+
console.log(
|
|
687
|
+
`${dim.label.padEnd(20)} ${theme.score(v1).padEnd(nameWidth + 10)} ${theme.score(v2).padEnd(nameWidth + 10)} ${winner}`
|
|
688
|
+
);
|
|
689
|
+
}
|
|
690
|
+
console.log(`
|
|
691
|
+
${"Commits".padEnd(20)} ${String(a1.score.scoredCommitCount).padEnd(nameWidth)} ${String(a2.score.scoredCommitCount).padEnd(nameWidth)}`);
|
|
692
|
+
console.log(`${"Trend".padEnd(20)} ${theme.trend(a1.score.trend.direction).padEnd(nameWidth + 10)} ${theme.trend(a2.score.trend.direction)}`);
|
|
693
|
+
console.log("");
|
|
694
|
+
}
|
|
695
|
+
async function compareCommand(author1, author2, options) {
|
|
696
|
+
try {
|
|
697
|
+
await compareCommandInner(author1, author2, options);
|
|
698
|
+
} catch (error) {
|
|
699
|
+
handleError(error);
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
// src/repl/repl.ts
|
|
704
|
+
import * as readline from "readline";
|
|
705
|
+
import { select as select4, confirm as confirm3, input as inputPrompt } from "@inquirer/prompts";
|
|
706
|
+
import { HomeManager as HomeManager4 } from "@gitpulse/core";
|
|
707
|
+
|
|
708
|
+
// src/wizard/onboarding.ts
|
|
709
|
+
import * as fs6 from "fs";
|
|
710
|
+
import * as path7 from "path";
|
|
711
|
+
import * as os2 from "os";
|
|
712
|
+
import { select as select3, input as input3, password } from "@inquirer/prompts";
|
|
713
|
+
var HOME_DIR = path7.join(os2.homedir(), ".gitpulse");
|
|
714
|
+
var CONFIG_FILE = path7.join(HOME_DIR, "config.yml");
|
|
715
|
+
function isOnboardingNeeded() {
|
|
716
|
+
return !fs6.existsSync(CONFIG_FILE);
|
|
717
|
+
}
|
|
718
|
+
async function runOnboarding() {
|
|
719
|
+
console.log(theme.heading("\n Welcome to GitPulse!\n"));
|
|
720
|
+
console.log(theme.dim(" Let's set up your global configuration.\n"));
|
|
721
|
+
const provider = await select3({
|
|
722
|
+
message: "Select your LLM provider:",
|
|
723
|
+
choices: [
|
|
724
|
+
{ value: "openai", name: "OpenAI (GPT-4o, GPT-4o-mini)" },
|
|
725
|
+
{ value: "anthropic", name: "Anthropic (Claude)" },
|
|
726
|
+
{ value: "google", name: "Google (Gemini)" },
|
|
727
|
+
{ value: "vertex", name: "Google Vertex AI (Gemini via Vertex)" },
|
|
728
|
+
{ value: "custom", name: "Custom (OpenAI-compatible endpoint)" }
|
|
729
|
+
]
|
|
730
|
+
});
|
|
731
|
+
const defaultModels = {
|
|
732
|
+
openai: "gpt-4o",
|
|
733
|
+
anthropic: "claude-sonnet-4-20250514",
|
|
734
|
+
google: "gemini-1.5-pro",
|
|
735
|
+
vertex: "gemini-1.5-pro",
|
|
736
|
+
custom: "default"
|
|
737
|
+
};
|
|
738
|
+
const model = await input3({
|
|
739
|
+
message: "Model name:",
|
|
740
|
+
default: defaultModels[provider] ?? "gpt-4o"
|
|
741
|
+
});
|
|
742
|
+
let baseURL;
|
|
743
|
+
if (provider === "custom") {
|
|
744
|
+
baseURL = await input3({
|
|
745
|
+
message: "Base URL for the API:",
|
|
746
|
+
validate: (v) => {
|
|
747
|
+
try {
|
|
748
|
+
new URL(v);
|
|
749
|
+
return true;
|
|
750
|
+
} catch {
|
|
751
|
+
return "Please enter a valid URL";
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
});
|
|
755
|
+
}
|
|
756
|
+
const apiKey = await password({
|
|
757
|
+
message: `API Key for ${provider}:`,
|
|
758
|
+
mask: "*"
|
|
759
|
+
});
|
|
760
|
+
fs6.mkdirSync(HOME_DIR, { recursive: true });
|
|
761
|
+
let yaml = `# GitPulse Global Configuration
|
|
762
|
+
provider: ${provider}
|
|
763
|
+
model: ${model}
|
|
764
|
+
`;
|
|
765
|
+
if (baseURL) {
|
|
766
|
+
yaml += `baseURL: ${baseURL}
|
|
767
|
+
`;
|
|
768
|
+
}
|
|
769
|
+
if (apiKey) {
|
|
770
|
+
yaml += `apiKey: ${apiKey}
|
|
771
|
+
`;
|
|
772
|
+
}
|
|
773
|
+
fs6.writeFileSync(CONFIG_FILE, yaml, "utf-8");
|
|
774
|
+
console.log(theme.success(`
|
|
775
|
+
Configuration saved to ${CONFIG_FILE}`));
|
|
776
|
+
console.log("");
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
// src/commands/history.ts
|
|
780
|
+
import Table2 from "cli-table3";
|
|
781
|
+
import { HomeManager as HomeManager3 } from "@gitpulse/core";
|
|
782
|
+
async function historyCommandInner(options) {
|
|
783
|
+
const home = new HomeManager3();
|
|
784
|
+
const entries = home.getHistory(options?.repo);
|
|
785
|
+
if (entries.length === 0) {
|
|
786
|
+
console.log(theme.dim("\n No analysis history found. Run `analyze` to get started.\n"));
|
|
787
|
+
return;
|
|
788
|
+
}
|
|
789
|
+
const limit = options?.limit ?? 20;
|
|
790
|
+
const shown = entries.slice(0, limit);
|
|
791
|
+
const table = new Table2({
|
|
792
|
+
head: [
|
|
793
|
+
theme.bold("Date"),
|
|
794
|
+
theme.bold("Repository"),
|
|
795
|
+
theme.bold("Commits"),
|
|
796
|
+
theme.bold("Avg Score"),
|
|
797
|
+
theme.bold("Provider"),
|
|
798
|
+
theme.bold("Cost")
|
|
799
|
+
],
|
|
800
|
+
style: {
|
|
801
|
+
head: [],
|
|
802
|
+
border: ["dim"]
|
|
803
|
+
}
|
|
804
|
+
});
|
|
805
|
+
for (const entry of shown) {
|
|
806
|
+
const date = new Date(entry.timestamp);
|
|
807
|
+
const dateStr = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}-${String(date.getDate()).padStart(2, "0")} ${String(date.getHours()).padStart(2, "0")}:${String(date.getMinutes()).padStart(2, "0")}`;
|
|
808
|
+
table.push([
|
|
809
|
+
dateStr,
|
|
810
|
+
entry.repoName,
|
|
811
|
+
String(entry.summary.analyzedCommits),
|
|
812
|
+
theme.score(entry.summary.averageScore),
|
|
813
|
+
`${entry.cost.llmProvider}/${entry.cost.llmModel}`,
|
|
814
|
+
`$${entry.cost.estimatedCost.toFixed(2)}`
|
|
815
|
+
]);
|
|
816
|
+
}
|
|
817
|
+
console.log(theme.heading("\n Analysis History\n"));
|
|
818
|
+
console.log(table.toString());
|
|
819
|
+
if (entries.length > limit) {
|
|
820
|
+
console.log(theme.dim(`
|
|
821
|
+
Showing ${limit} of ${entries.length} entries.`));
|
|
822
|
+
}
|
|
823
|
+
console.log("");
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
// src/repl/arg-parser.ts
|
|
827
|
+
function parseArgs(input4) {
|
|
828
|
+
const args2 = [];
|
|
829
|
+
let current = "";
|
|
830
|
+
let inSingle = false;
|
|
831
|
+
let inDouble = false;
|
|
832
|
+
for (let i = 0; i < input4.length; i++) {
|
|
833
|
+
const ch = input4[i];
|
|
834
|
+
if (ch === "'" && !inDouble) {
|
|
835
|
+
inSingle = !inSingle;
|
|
836
|
+
continue;
|
|
837
|
+
}
|
|
838
|
+
if (ch === '"' && !inSingle) {
|
|
839
|
+
inDouble = !inDouble;
|
|
840
|
+
continue;
|
|
841
|
+
}
|
|
842
|
+
if (ch === " " && !inSingle && !inDouble) {
|
|
843
|
+
if (current.length > 0) {
|
|
844
|
+
args2.push(current);
|
|
845
|
+
current = "";
|
|
846
|
+
}
|
|
847
|
+
continue;
|
|
848
|
+
}
|
|
849
|
+
current += ch;
|
|
850
|
+
}
|
|
851
|
+
if (current.length > 0) {
|
|
852
|
+
args2.push(current);
|
|
853
|
+
}
|
|
854
|
+
return args2;
|
|
855
|
+
}
|
|
856
|
+
function consumeFlag(args2, flag, alias) {
|
|
857
|
+
const idx = args2.findIndex((a) => a === flag || alias && a === alias);
|
|
858
|
+
if (idx === -1) return false;
|
|
859
|
+
args2.splice(idx, 1);
|
|
860
|
+
return true;
|
|
861
|
+
}
|
|
862
|
+
function consumeOption(args2, flag, alias) {
|
|
863
|
+
const idx = args2.findIndex((a) => a === flag || alias && a === alias);
|
|
864
|
+
if (idx === -1) return void 0;
|
|
865
|
+
const value = args2[idx + 1];
|
|
866
|
+
args2.splice(idx, value !== void 0 ? 2 : 1);
|
|
867
|
+
return value;
|
|
868
|
+
}
|
|
869
|
+
function consumeMultiOption(args2, flag, alias) {
|
|
870
|
+
const values = [];
|
|
871
|
+
let found = false;
|
|
872
|
+
while (true) {
|
|
873
|
+
const idx = args2.findIndex((a) => a === flag || alias && a === alias);
|
|
874
|
+
if (idx === -1) break;
|
|
875
|
+
found = true;
|
|
876
|
+
const value = args2[idx + 1];
|
|
877
|
+
if (value !== void 0 && !value.startsWith("-")) {
|
|
878
|
+
args2.splice(idx, 2);
|
|
879
|
+
values.push(value);
|
|
880
|
+
} else {
|
|
881
|
+
args2.splice(idx, 1);
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
return found ? values : void 0;
|
|
885
|
+
}
|
|
886
|
+
function parseAnalyzeArgs(args2) {
|
|
887
|
+
const rest = [...args2];
|
|
888
|
+
const options = {};
|
|
889
|
+
options.branch = consumeOption(rest, "--branch", "-b");
|
|
890
|
+
options.since = consumeOption(rest, "--since");
|
|
891
|
+
options.until = consumeOption(rest, "--until");
|
|
892
|
+
options.author = consumeMultiOption(rest, "--author");
|
|
893
|
+
const maxCommits = consumeOption(rest, "--max-commits");
|
|
894
|
+
if (maxCommits) options.maxCommits = parseInt(maxCommits, 10);
|
|
895
|
+
options.format = consumeOption(rest, "--format");
|
|
896
|
+
options.output = consumeOption(rest, "--output", "-o");
|
|
897
|
+
if (consumeFlag(rest, "--no-cache")) options.noCache = true;
|
|
898
|
+
if (consumeFlag(rest, "-y") || consumeFlag(rest, "--yes")) options.yes = true;
|
|
899
|
+
options.provider = consumeOption(rest, "--provider");
|
|
900
|
+
options.model = consumeOption(rest, "--model");
|
|
901
|
+
const repoPath = rest.find((a) => !a.startsWith("-")) ?? ".";
|
|
902
|
+
return { repoPath, options };
|
|
903
|
+
}
|
|
904
|
+
function parseReportArgs(args2) {
|
|
905
|
+
const rest = [...args2];
|
|
906
|
+
const options = {};
|
|
907
|
+
options.format = consumeOption(rest, "--format");
|
|
908
|
+
options.output = consumeOption(rest, "--output", "-o");
|
|
909
|
+
const repoPath = rest.find((a) => !a.startsWith("-")) ?? ".";
|
|
910
|
+
return { repoPath, options };
|
|
911
|
+
}
|
|
912
|
+
function parseConfigArgs(args2) {
|
|
913
|
+
const rest = [...args2];
|
|
914
|
+
const options = {};
|
|
915
|
+
if (consumeFlag(rest, "--init")) options.init = true;
|
|
916
|
+
if (consumeFlag(rest, "--show")) options.show = true;
|
|
917
|
+
options.set = consumeOption(rest, "--set");
|
|
918
|
+
const value = rest.find((a) => !a.startsWith("-"));
|
|
919
|
+
return { options, value };
|
|
920
|
+
}
|
|
921
|
+
function parseCompareArgs(args2) {
|
|
922
|
+
const rest = [...args2];
|
|
923
|
+
const options = {};
|
|
924
|
+
options.path = consumeOption(rest, "--path", "-p");
|
|
925
|
+
options.since = consumeOption(rest, "--since");
|
|
926
|
+
options.until = consumeOption(rest, "--until");
|
|
927
|
+
const positional = rest.filter((a) => !a.startsWith("-"));
|
|
928
|
+
if (positional.length < 2) {
|
|
929
|
+
return null;
|
|
930
|
+
}
|
|
931
|
+
return { author1: positional[0], author2: positional[1], options };
|
|
932
|
+
}
|
|
933
|
+
function parseHistoryArgs(args2) {
|
|
934
|
+
const rest = [...args2];
|
|
935
|
+
const options = {};
|
|
936
|
+
options.repo = consumeOption(rest, "--repo");
|
|
937
|
+
const limit = consumeOption(rest, "--limit", "-n");
|
|
938
|
+
if (limit) options.limit = parseInt(limit, 10);
|
|
939
|
+
return { options };
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
// src/repl/repl.ts
|
|
943
|
+
async function startRepl() {
|
|
944
|
+
printLogo();
|
|
945
|
+
if (isOnboardingNeeded()) {
|
|
946
|
+
await runOnboarding();
|
|
947
|
+
} else {
|
|
948
|
+
await showWelcomeBack();
|
|
949
|
+
}
|
|
950
|
+
await replLoop();
|
|
951
|
+
}
|
|
952
|
+
async function showWelcomeBack() {
|
|
953
|
+
const home = new HomeManager4();
|
|
954
|
+
const memory = home.getMemory();
|
|
955
|
+
if (memory.lastRun) {
|
|
956
|
+
const date = new Date(memory.lastRun.timestamp);
|
|
957
|
+
const dateStr = date.toLocaleDateString("en-US", {
|
|
958
|
+
year: "numeric",
|
|
959
|
+
month: "short",
|
|
960
|
+
day: "numeric",
|
|
961
|
+
hour: "2-digit",
|
|
962
|
+
minute: "2-digit"
|
|
963
|
+
});
|
|
964
|
+
console.log(theme.heading(" Welcome back!\n"));
|
|
965
|
+
console.log(` Last analysis: ${theme.bold(memory.lastRun.repoName)} on ${dateStr}`);
|
|
966
|
+
console.log(` Commits: ${memory.lastRun.totalCommits} | Avg Score: ${theme.score(memory.lastRun.averageScore)}`);
|
|
967
|
+
if (memory.preferences.preferredProvider) {
|
|
968
|
+
console.log(` Provider: ${memory.preferences.preferredProvider}/${memory.preferences.preferredModel ?? "default"}`);
|
|
969
|
+
}
|
|
970
|
+
console.log(` Total analyses: ${memory.totalAnalysisCount}
|
|
971
|
+
`);
|
|
972
|
+
const continueSettings = await confirm3({
|
|
973
|
+
message: "Continue with current settings?",
|
|
974
|
+
default: true
|
|
975
|
+
});
|
|
976
|
+
if (!continueSettings) {
|
|
977
|
+
await runOnboarding();
|
|
978
|
+
}
|
|
979
|
+
} else {
|
|
980
|
+
console.log(theme.heading(" Welcome back!\n"));
|
|
981
|
+
console.log(theme.dim(" No previous analysis found. Run `analyze <path>` to get started.\n"));
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
function promptLine() {
|
|
985
|
+
return new Promise((resolve4) => {
|
|
986
|
+
const rl = readline.createInterface({
|
|
987
|
+
input: process.stdin,
|
|
988
|
+
output: process.stdout,
|
|
989
|
+
terminal: true
|
|
990
|
+
});
|
|
991
|
+
rl.on("SIGINT", () => {
|
|
992
|
+
rl.close();
|
|
993
|
+
resolve4(null);
|
|
994
|
+
});
|
|
995
|
+
rl.on("close", () => {
|
|
996
|
+
resolve4(null);
|
|
997
|
+
});
|
|
998
|
+
rl.question(theme.prompt("gitpulse> "), (answer) => {
|
|
999
|
+
resolve4(answer);
|
|
1000
|
+
rl.close();
|
|
1001
|
+
});
|
|
1002
|
+
});
|
|
1003
|
+
}
|
|
1004
|
+
async function replLoop() {
|
|
1005
|
+
console.log(theme.dim(" Type a command, press Enter for quick menu, or type `help` for usage.\n"));
|
|
1006
|
+
while (true) {
|
|
1007
|
+
const line = await promptLine();
|
|
1008
|
+
if (line === null) {
|
|
1009
|
+
console.log(theme.dim("\n Goodbye!"));
|
|
1010
|
+
return;
|
|
1011
|
+
}
|
|
1012
|
+
const trimmed = line.trim();
|
|
1013
|
+
if (trimmed === "") {
|
|
1014
|
+
try {
|
|
1015
|
+
const shouldExit = await showQuickMenu();
|
|
1016
|
+
if (shouldExit) return;
|
|
1017
|
+
} catch (error) {
|
|
1018
|
+
if (error instanceof Error && error.name === "AbortPromptError") {
|
|
1019
|
+
} else {
|
|
1020
|
+
handleReplError(error);
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
continue;
|
|
1024
|
+
}
|
|
1025
|
+
if (trimmed === "exit" || trimmed === "quit") {
|
|
1026
|
+
console.log(theme.dim("\n Goodbye!"));
|
|
1027
|
+
return;
|
|
1028
|
+
}
|
|
1029
|
+
if (trimmed === "help") {
|
|
1030
|
+
printHelp();
|
|
1031
|
+
continue;
|
|
1032
|
+
}
|
|
1033
|
+
try {
|
|
1034
|
+
await dispatchCommand(trimmed);
|
|
1035
|
+
} catch (error) {
|
|
1036
|
+
handleReplError(error);
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
async function dispatchCommand(input4) {
|
|
1041
|
+
const argv = parseArgs(input4);
|
|
1042
|
+
if (argv.length === 0) return;
|
|
1043
|
+
const command = argv[0].toLowerCase();
|
|
1044
|
+
const args2 = argv.slice(1);
|
|
1045
|
+
switch (command) {
|
|
1046
|
+
case "analyze": {
|
|
1047
|
+
const { repoPath, options } = parseAnalyzeArgs(args2);
|
|
1048
|
+
await analyzeCommandInner(repoPath, options);
|
|
1049
|
+
break;
|
|
1050
|
+
}
|
|
1051
|
+
case "report": {
|
|
1052
|
+
const { repoPath, options } = parseReportArgs(args2);
|
|
1053
|
+
await reportCommandInner(repoPath, options);
|
|
1054
|
+
break;
|
|
1055
|
+
}
|
|
1056
|
+
case "config": {
|
|
1057
|
+
const { options, value } = parseConfigArgs(args2);
|
|
1058
|
+
await configCommandInner(options, value);
|
|
1059
|
+
break;
|
|
1060
|
+
}
|
|
1061
|
+
case "compare": {
|
|
1062
|
+
const parsed = parseCompareArgs(args2);
|
|
1063
|
+
if (!parsed) {
|
|
1064
|
+
console.log(theme.error("Usage: compare <author1> <author2> [--path <path>] [--since <date>] [--until <date>]"));
|
|
1065
|
+
break;
|
|
1066
|
+
}
|
|
1067
|
+
await compareCommandInner(parsed.author1, parsed.author2, parsed.options);
|
|
1068
|
+
break;
|
|
1069
|
+
}
|
|
1070
|
+
case "history": {
|
|
1071
|
+
const { options } = parseHistoryArgs(args2);
|
|
1072
|
+
await historyCommandInner(options);
|
|
1073
|
+
break;
|
|
1074
|
+
}
|
|
1075
|
+
case "help": {
|
|
1076
|
+
printHelp();
|
|
1077
|
+
break;
|
|
1078
|
+
}
|
|
1079
|
+
default: {
|
|
1080
|
+
console.log(theme.warning(`Unknown command: ${command}`));
|
|
1081
|
+
console.log(theme.dim("Type `help` for available commands.\n"));
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
async function showQuickMenu() {
|
|
1086
|
+
const action = await select4({
|
|
1087
|
+
message: "What would you like to do?",
|
|
1088
|
+
choices: [
|
|
1089
|
+
{ value: "analyze", name: "Analyze a repository" },
|
|
1090
|
+
{ value: "report", name: "Generate a report from cache" },
|
|
1091
|
+
{ value: "compare", name: "Compare two developers" },
|
|
1092
|
+
{ value: "history", name: "View analysis history" },
|
|
1093
|
+
{ value: "config", name: "Manage configuration" },
|
|
1094
|
+
{ value: "help", name: "Show help" },
|
|
1095
|
+
{ value: "exit", name: "Exit" }
|
|
1096
|
+
]
|
|
1097
|
+
});
|
|
1098
|
+
switch (action) {
|
|
1099
|
+
case "analyze": {
|
|
1100
|
+
const repoPath = await inputPrompt({
|
|
1101
|
+
message: "Repository path or URL:",
|
|
1102
|
+
default: "."
|
|
1103
|
+
});
|
|
1104
|
+
await analyzeCommandInner(repoPath, {});
|
|
1105
|
+
break;
|
|
1106
|
+
}
|
|
1107
|
+
case "report": {
|
|
1108
|
+
const repoPath = await inputPrompt({
|
|
1109
|
+
message: "Repository path or URL:",
|
|
1110
|
+
default: "."
|
|
1111
|
+
});
|
|
1112
|
+
await reportCommandInner(repoPath, {});
|
|
1113
|
+
break;
|
|
1114
|
+
}
|
|
1115
|
+
case "compare": {
|
|
1116
|
+
const author1 = await inputPrompt({ message: "First author name or email:" });
|
|
1117
|
+
const author2 = await inputPrompt({ message: "Second author name or email:" });
|
|
1118
|
+
await compareCommandInner(author1, author2, {});
|
|
1119
|
+
break;
|
|
1120
|
+
}
|
|
1121
|
+
case "history": {
|
|
1122
|
+
await historyCommandInner();
|
|
1123
|
+
break;
|
|
1124
|
+
}
|
|
1125
|
+
case "config": {
|
|
1126
|
+
await configCommandInner({ show: true });
|
|
1127
|
+
break;
|
|
1128
|
+
}
|
|
1129
|
+
case "help": {
|
|
1130
|
+
printHelp();
|
|
1131
|
+
break;
|
|
1132
|
+
}
|
|
1133
|
+
case "exit": {
|
|
1134
|
+
console.log(theme.dim("\n Goodbye!"));
|
|
1135
|
+
return true;
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
return false;
|
|
1139
|
+
}
|
|
1140
|
+
function printHelp() {
|
|
1141
|
+
console.log(theme.heading("\n Available Commands:\n"));
|
|
1142
|
+
console.log(" analyze [path] Analyze a repository");
|
|
1143
|
+
console.log(" --branch, -b <name> Branch to analyze");
|
|
1144
|
+
console.log(" --since <date> Start date");
|
|
1145
|
+
console.log(" --until <date> End date");
|
|
1146
|
+
console.log(" --author <names...> Filter by author");
|
|
1147
|
+
console.log(" --max-commits <n> Max commits to analyze");
|
|
1148
|
+
console.log(" --format <type> Output: terminal, json, markdown, html");
|
|
1149
|
+
console.log(" -o, --output <path> Output file path");
|
|
1150
|
+
console.log(" --no-cache Force re-scoring");
|
|
1151
|
+
console.log(" -y, --yes Skip confirmation");
|
|
1152
|
+
console.log(" --provider <type> LLM provider override");
|
|
1153
|
+
console.log(" --model <name> LLM model override");
|
|
1154
|
+
console.log("");
|
|
1155
|
+
console.log(" report [path] Generate report from cached scores");
|
|
1156
|
+
console.log(" --format <type> Output format");
|
|
1157
|
+
console.log(" -o, --output <path> Output file path");
|
|
1158
|
+
console.log("");
|
|
1159
|
+
console.log(" compare <a1> <a2> Compare two developers");
|
|
1160
|
+
console.log(" -p, --path <path> Repository path");
|
|
1161
|
+
console.log(" --since <date> Start date");
|
|
1162
|
+
console.log(" --until <date> End date");
|
|
1163
|
+
console.log("");
|
|
1164
|
+
console.log(" history View analysis history");
|
|
1165
|
+
console.log(" --repo <path> Filter by repository");
|
|
1166
|
+
console.log(" -n, --limit <n> Limit entries");
|
|
1167
|
+
console.log("");
|
|
1168
|
+
console.log(" config Show current configuration");
|
|
1169
|
+
console.log(" --init Run setup wizard");
|
|
1170
|
+
console.log(" --show Show configuration");
|
|
1171
|
+
console.log("");
|
|
1172
|
+
console.log(" help Show this help");
|
|
1173
|
+
console.log(" exit / quit Exit REPL");
|
|
1174
|
+
console.log("");
|
|
1175
|
+
console.log(theme.dim(" Press Enter (empty input) for quick menu."));
|
|
1176
|
+
console.log("");
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
// src/index.ts
|
|
1180
|
+
var args = process.argv.slice(2);
|
|
1181
|
+
if (args.length === 0) {
|
|
1182
|
+
startRepl().catch(handleError);
|
|
1183
|
+
} else {
|
|
1184
|
+
const program = new Command();
|
|
1185
|
+
program.name("gitpulse").description("AI-powered Git contribution analyzer").version(getVersion()).hook("preAction", () => {
|
|
1186
|
+
printLogo();
|
|
1187
|
+
});
|
|
1188
|
+
program.command("analyze").description("Analyze repository contributions").argument("[path]", "Path or URL to git repository", ".").option("-b, --branch <branch>", "Branch to analyze").option("--since <date>", "Start date (e.g., 2025-01-01)").option("--until <date>", "End date").option("--author <names...>", "Filter by author name(s)").option("--max-commits <n>", "Maximum number of commits", parseInt).option("--format <type>", "Output format: terminal, json, markdown, html").option("-o, --output <path>", "Output file path").option("--no-cache", "Ignore cache, force re-scoring").option("-y, --yes", "Skip confirmation prompt").option("--provider <type>", "LLM provider override").option("--model <name>", "LLM model override").action(async (repoPath, options) => {
|
|
1189
|
+
await analyzeCommand(repoPath, options);
|
|
1190
|
+
});
|
|
1191
|
+
program.command("report").description("Generate report from cached scores").argument("[path]", "Path or URL to git repository", ".").option("--format <type>", "Output format: terminal, json, markdown, html").option("-o, --output <path>", "Output file path").action(async (repoPath, options) => {
|
|
1192
|
+
await reportCommand(repoPath, options);
|
|
1193
|
+
});
|
|
1194
|
+
program.command("config").description("Manage configuration").option("--init", "Run interactive setup wizard").option("--show", "Show current configuration").option("--set <key>", "Set a config value").argument("[value]", "Value to set").action(async (value, options) => {
|
|
1195
|
+
await configCommand(options, value);
|
|
1196
|
+
});
|
|
1197
|
+
program.command("compare").description("Compare two developers").argument("<author1>", "First author name or email").argument("<author2>", "Second author name or email").option("-p, --path <path>", "Path or URL to git repository", ".").option("--since <date>", "Start date").option("--until <date>", "End date").action(async (author1, author2, options) => {
|
|
1198
|
+
await compareCommand(author1, author2, options);
|
|
1199
|
+
});
|
|
1200
|
+
program.parseAsync(process.argv).catch(handleError);
|
|
1201
|
+
}
|
|
1202
|
+
//# sourceMappingURL=index.js.map
|