@cencori/scan 0.3.8 → 0.4.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/cli.mjs CHANGED
@@ -950,10 +950,218 @@ function buildTelemetryData(result, version, hasApiKey) {
950
950
  };
951
951
  }
952
952
 
953
+ // src/changelog/index.ts
954
+ import { execSync } from "child_process";
955
+ var CENCORI_API_URL2 = "https://api.cencori.com/v1";
956
+ function parseCommitType(message) {
957
+ const lower = message.toLowerCase();
958
+ if (lower.startsWith("feat") || lower.startsWith("feature")) return "feat";
959
+ if (lower.startsWith("fix") || lower.startsWith("bugfix")) return "fix";
960
+ if (lower.startsWith("chore")) return "chore";
961
+ if (lower.startsWith("docs")) return "docs";
962
+ if (lower.startsWith("style")) return "style";
963
+ if (lower.startsWith("refactor")) return "refactor";
964
+ if (lower.startsWith("test")) return "test";
965
+ return "other";
966
+ }
967
+ function getCommits(since = "1 week ago", cwd) {
968
+ try {
969
+ const output = execSync(
970
+ `git log --since="${since}" --pretty=format:"%H|%aI|%an|%s" --no-merges`,
971
+ {
972
+ cwd: cwd || process.cwd(),
973
+ encoding: "utf-8",
974
+ stdio: ["pipe", "pipe", "pipe"]
975
+ }
976
+ );
977
+ if (!output.trim()) {
978
+ return [];
979
+ }
980
+ return output.trim().split("\n").map((line) => {
981
+ const [hash, date, author, ...messageParts] = line.split("|");
982
+ const message = messageParts.join("|");
983
+ return {
984
+ hash: hash.substring(0, 7),
985
+ date,
986
+ author,
987
+ message,
988
+ type: parseCommitType(message)
989
+ };
990
+ });
991
+ } catch (error) {
992
+ return [];
993
+ }
994
+ }
995
+ function groupCommitsByType(commits) {
996
+ const groups = {
997
+ feat: [],
998
+ fix: [],
999
+ docs: [],
1000
+ other: []
1001
+ };
1002
+ for (const commit of commits) {
1003
+ if (commit.type === "feat") {
1004
+ groups.feat.push(commit);
1005
+ } else if (commit.type === "fix") {
1006
+ groups.fix.push(commit);
1007
+ } else if (commit.type === "docs") {
1008
+ groups.docs.push(commit);
1009
+ } else {
1010
+ groups.other.push(commit);
1011
+ }
1012
+ }
1013
+ const entries = [];
1014
+ if (groups.feat.length > 0) {
1015
+ entries.push({
1016
+ type: "feature",
1017
+ title: "Features",
1018
+ commits: groups.feat.map((c) => c.message)
1019
+ });
1020
+ }
1021
+ if (groups.fix.length > 0) {
1022
+ entries.push({
1023
+ type: "fix",
1024
+ title: "Bug Fixes",
1025
+ commits: groups.fix.map((c) => c.message)
1026
+ });
1027
+ }
1028
+ if (groups.docs.length > 0) {
1029
+ entries.push({
1030
+ type: "docs",
1031
+ title: "Documentation",
1032
+ commits: groups.docs.map((c) => c.message)
1033
+ });
1034
+ }
1035
+ if (groups.other.length > 0) {
1036
+ entries.push({
1037
+ type: "other",
1038
+ title: "Other Changes",
1039
+ commits: groups.other.map((c) => c.message)
1040
+ });
1041
+ }
1042
+ return entries;
1043
+ }
1044
+ function generateMarkdownFree(entries, period) {
1045
+ const startDate = new Date(period.start).toLocaleDateString("en-US", {
1046
+ month: "short",
1047
+ day: "numeric",
1048
+ year: "numeric"
1049
+ });
1050
+ const endDate = new Date(period.end).toLocaleDateString("en-US", {
1051
+ month: "short",
1052
+ day: "numeric",
1053
+ year: "numeric"
1054
+ });
1055
+ let md = `## Changelog (${startDate} - ${endDate})
1056
+
1057
+ `;
1058
+ const typeEmojis = {
1059
+ feature: "\u2728",
1060
+ fix: "\u{1F41B}",
1061
+ docs: "\u{1F4DA}",
1062
+ improvement: "\u{1F527}",
1063
+ other: "\u{1F4E6}"
1064
+ };
1065
+ for (const entry of entries) {
1066
+ const emoji = typeEmojis[entry.type] || "\u{1F4E6}";
1067
+ md += `### ${emoji} ${entry.title}
1068
+
1069
+ `;
1070
+ for (const commit of entry.commits) {
1071
+ const cleanMessage = commit.replace(/^(feat|fix|chore|docs|style|refactor|test)(\(.+?\))?:\s*/i, "").trim();
1072
+ md += `- ${cleanMessage}
1073
+ `;
1074
+ }
1075
+ md += "\n";
1076
+ }
1077
+ return md;
1078
+ }
1079
+ async function generateChangelogAI(commits, apiKey, period) {
1080
+ const commitMessages = commits.map((c) => c.message).join("\n");
1081
+ try {
1082
+ const response = await fetch(`${CENCORI_API_URL2}/changelog/generate`, {
1083
+ method: "POST",
1084
+ headers: {
1085
+ "Content-Type": "application/json",
1086
+ Authorization: `Bearer ${apiKey}`
1087
+ },
1088
+ body: JSON.stringify({
1089
+ commits: commitMessages,
1090
+ period
1091
+ })
1092
+ });
1093
+ if (!response.ok) {
1094
+ throw new Error("API request failed");
1095
+ }
1096
+ const data = await response.json();
1097
+ return {
1098
+ entries: data.entries || [],
1099
+ markdown: data.markdown || ""
1100
+ };
1101
+ } catch {
1102
+ const entries = groupCommitsByType(commits);
1103
+ return {
1104
+ entries,
1105
+ markdown: generateMarkdownFree(entries, period)
1106
+ };
1107
+ }
1108
+ }
1109
+ async function generateChangelog(since = "1 week ago", apiKey, cwd) {
1110
+ const commits = getCommits(since, cwd);
1111
+ const now = /* @__PURE__ */ new Date();
1112
+ const sinceDate = /* @__PURE__ */ new Date();
1113
+ const match = since.match(/(\d+)\s*(week|day|month)s?\s*ago/i);
1114
+ if (match) {
1115
+ const amount = parseInt(match[1], 10);
1116
+ const unit = match[2].toLowerCase();
1117
+ if (unit === "week") {
1118
+ sinceDate.setDate(sinceDate.getDate() - amount * 7);
1119
+ } else if (unit === "day") {
1120
+ sinceDate.setDate(sinceDate.getDate() - amount);
1121
+ } else if (unit === "month") {
1122
+ sinceDate.setMonth(sinceDate.getMonth() - amount);
1123
+ }
1124
+ } else {
1125
+ sinceDate.setDate(sinceDate.getDate() - 7);
1126
+ }
1127
+ const period = {
1128
+ start: sinceDate.toISOString(),
1129
+ end: now.toISOString()
1130
+ };
1131
+ if (commits.length === 0) {
1132
+ return {
1133
+ period,
1134
+ entries: [],
1135
+ markdown: `## Changelog
1136
+
1137
+ No commits found in the specified period.
1138
+ `,
1139
+ commitCount: 0
1140
+ };
1141
+ }
1142
+ if (apiKey) {
1143
+ const { entries, markdown } = await generateChangelogAI(commits, apiKey, period);
1144
+ return {
1145
+ period,
1146
+ entries,
1147
+ markdown,
1148
+ commitCount: commits.length
1149
+ };
1150
+ } else {
1151
+ const entries = groupCommitsByType(commits);
1152
+ return {
1153
+ period,
1154
+ entries,
1155
+ markdown: generateMarkdownFree(entries, period),
1156
+ commitCount: commits.length
1157
+ };
1158
+ }
1159
+ }
1160
+
953
1161
  // src/cli.ts
954
1162
  import * as fs3 from "fs";
955
1163
  import * as path3 from "path";
956
- var VERSION = "0.3.8";
1164
+ var VERSION = "0.4.0";
957
1165
  var scoreStyles = {
958
1166
  A: { color: chalk.green },
959
1167
  B: { color: chalk.blue },
@@ -1259,6 +1467,58 @@ async function main() {
1259
1467
  process.exit(1);
1260
1468
  }
1261
1469
  });
1470
+ program.command("changelog").description("Generate AI-powered changelog from git commits").option("-s, --since <time>", "Time range for commits", "1 week ago").option("-o, --output <file>", "Output to file instead of stdout").option("-f, --format <format>", "Output format (markdown or json)", "markdown").action(async (options) => {
1471
+ printBanner();
1472
+ const spinner = ora({
1473
+ text: "Reading git history...",
1474
+ color: "cyan"
1475
+ }).start();
1476
+ try {
1477
+ const apiKey = getApiKey();
1478
+ const result = await generateChangelog(options.since, apiKey || void 0);
1479
+ if (result.commitCount === 0) {
1480
+ spinner.warn("No commits found in the specified period");
1481
+ console.log(chalk.yellow(`
1482
+ Try a larger time range with --since "2 weeks ago"
1483
+ `));
1484
+ process.exit(0);
1485
+ return;
1486
+ }
1487
+ spinner.succeed(`Found ${result.commitCount} commits`);
1488
+ if (options.format === "json") {
1489
+ const output = JSON.stringify(result, null, 2);
1490
+ if (options.output) {
1491
+ fs3.writeFileSync(options.output, output);
1492
+ console.log(chalk.green(`
1493
+ \u2714 Changelog saved to ${options.output}
1494
+ `));
1495
+ } else {
1496
+ console.log(output);
1497
+ }
1498
+ } else {
1499
+ if (options.output) {
1500
+ fs3.writeFileSync(options.output, result.markdown);
1501
+ console.log(chalk.green(`
1502
+ \u2714 Changelog saved to ${options.output}
1503
+ `));
1504
+ } else {
1505
+ console.log("\n" + result.markdown);
1506
+ }
1507
+ }
1508
+ if (!apiKey) {
1509
+ console.log(chalk.gray(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
1510
+ console.log(chalk.cyan("\n \u{1F4A1} Want AI-enhanced changelogs?"));
1511
+ console.log(chalk.gray(" Get human-readable summaries with a Cencori API key"));
1512
+ console.log(chalk.gray(" Sign up free at https://cencori.com\n"));
1513
+ }
1514
+ process.exit(0);
1515
+ } catch (error) {
1516
+ spinner.fail("Changelog generation failed");
1517
+ console.error(chalk.red(`
1518
+ Error: ${error instanceof Error ? error.message : "Unknown error"}`));
1519
+ process.exit(1);
1520
+ }
1521
+ });
1262
1522
  program.parse();
1263
1523
  }
1264
1524
  main();