@cullit/core 1.0.0 → 1.5.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/index.d.ts +24 -7
- package/dist/index.js +179 -21
- package/package.json +4 -3
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { EnrichmentType, OutputFormat, AIConfig, CullConfig } from '@cullit/config';
|
|
2
|
-
export { AIConfig, AIProvider, Audience, BitbucketConfig, ConfluenceConfig, CullConfig, EnrichmentType, GitLabConfig, JiraConfig, LinearConfig, NotionConfig, OpenClawConfig, OutputFormat, PublishTarget, PublisherType, SourceConfig, Tone } from '@cullit/config';
|
|
1
|
+
import { EnrichmentType, OutputFormat, AIConfig, RepoSource, CullConfig } from '@cullit/config';
|
|
2
|
+
export { AIConfig, AIProvider, Audience, BitbucketConfig, ConfluenceConfig, CullConfig, EnrichmentType, GitLabConfig, JiraConfig, LinearConfig, NotionConfig, OpenClawConfig, OutputFormat, PublishTarget, PublisherType, RepoSource, SourceConfig, Tone } from '@cullit/config';
|
|
3
3
|
|
|
4
4
|
interface GitCommit {
|
|
5
5
|
hash: string;
|
|
@@ -71,17 +71,17 @@ interface PipelineResult {
|
|
|
71
71
|
duration: number;
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
-
declare const VERSION = "1.
|
|
74
|
+
declare const VERSION = "1.5.0";
|
|
75
75
|
declare const DEFAULT_CATEGORIES: string[];
|
|
76
76
|
declare const DEFAULT_MODELS: Record<string, string>;
|
|
77
77
|
declare const AI_PROVIDERS: readonly ["anthropic", "openai", "gemini", "ollama", "openclaw", "none"];
|
|
78
|
-
declare const OUTPUT_FORMATS: readonly ["markdown", "html", "json"];
|
|
78
|
+
declare const OUTPUT_FORMATS: readonly ["markdown", "html", "html-dark", "html-minimal", "html-edgy", "json"];
|
|
79
79
|
declare const PUBLISHER_TYPES: readonly ["stdout", "file", "slack", "discord", "github-release", "teams", "confluence", "notion", "gitlab-release", "changelog"];
|
|
80
80
|
declare const ENRICHMENT_TYPES: readonly ["jira", "linear"];
|
|
81
81
|
declare const CHANGE_CATEGORIES: readonly ["features", "fixes", "breaking", "improvements", "chores", "other"];
|
|
82
82
|
declare const AUDIENCES: readonly ["developer", "end-user", "executive"];
|
|
83
|
-
declare const TONES: readonly ["professional", "casual", "terse"];
|
|
84
|
-
declare const SOURCE_TYPES: readonly ["local", "jira", "linear", "gitlab", "bitbucket"];
|
|
83
|
+
declare const TONES: readonly ["professional", "casual", "terse", "edgy", "hype", "snarky"];
|
|
84
|
+
declare const SOURCE_TYPES: readonly ["local", "jira", "linear", "gitlab", "bitbucket", "multi-repo"];
|
|
85
85
|
|
|
86
86
|
type LogLevel = 'quiet' | 'normal' | 'verbose';
|
|
87
87
|
interface Logger {
|
|
@@ -123,6 +123,23 @@ declare function getRecentTags(cwd?: string, count?: number): string[];
|
|
|
123
123
|
*/
|
|
124
124
|
declare function getLatestTag(cwd?: string): string | null;
|
|
125
125
|
|
|
126
|
+
/**
|
|
127
|
+
* Collects commits from multiple repositories and merges them into a single GitDiff.
|
|
128
|
+
* Supports both local paths and remote URLs (shallow-cloned to temp dirs).
|
|
129
|
+
*
|
|
130
|
+
* Commits are tagged with `[repo-name]` prefix in the message for traceability.
|
|
131
|
+
* Results are sorted by date (newest first) across all repos.
|
|
132
|
+
*/
|
|
133
|
+
declare class MultiRepoCollector implements Collector {
|
|
134
|
+
private repos;
|
|
135
|
+
private tempDirs;
|
|
136
|
+
constructor(repos: RepoSource[]);
|
|
137
|
+
collect(from: string, to: string): Promise<GitDiff>;
|
|
138
|
+
private resolveRepoPath;
|
|
139
|
+
private inferName;
|
|
140
|
+
private cleanup;
|
|
141
|
+
}
|
|
142
|
+
|
|
126
143
|
/**
|
|
127
144
|
* Template-based release notes generator — no AI required.
|
|
128
145
|
* Groups commits by conventional commit prefix and ticket type.
|
|
@@ -293,4 +310,4 @@ declare function runPipeline(from: string, to: string, config: CullConfig, optio
|
|
|
293
310
|
logger?: Logger;
|
|
294
311
|
}): Promise<PipelineResult>;
|
|
295
312
|
|
|
296
|
-
export { AI_PROVIDERS, AUDIENCES, CHANGE_CATEGORIES, type ChangeCategory, type ChangeEntry, type Collector, type CollectorFactory, DEFAULT_CATEGORIES, DEFAULT_MODELS, ENRICHMENT_TYPES, type EnrichedContext, type EnrichedTicket, type Enricher, type EnricherFactory, FilePublisher, type Generator, type GeneratorFactory, GitCollector, type GitCommit, type GitDiff, type LicenseStatus, type LicenseTier, type LogLevel, type Logger, OUTPUT_FORMATS, PUBLISHER_TYPES, type PipelineResult, type Publisher, type PublisherFactory, type ReleaseAdvisory, type ReleaseNotes, SOURCE_TYPES, type SemverBump, StdoutPublisher, TONES, TemplateGenerator, type UsageLimits, VERSION, analyzeReleaseReadiness, createLogger, fetchWithTimeout, formatNotes, getCollector, getEnricher, getFormatter, getGenerator, getLatestTag, getPublisher, getRecentTags, getTierLimits, hasCollector, hasEnricher, hasGenerator, hasPublisher, isEnrichmentAllowed, isProviderAllowed, isPublisherAllowed, listCollectors, listEnrichers, listFormatters, listGenerators, listPublishers, registerCollector, registerEnricher, registerFormatter, registerGenerator, registerPublisher, reportUsage, resolveLicense, runPipeline, upgradeMessage, validateLicense };
|
|
313
|
+
export { AI_PROVIDERS, AUDIENCES, CHANGE_CATEGORIES, type ChangeCategory, type ChangeEntry, type Collector, type CollectorFactory, DEFAULT_CATEGORIES, DEFAULT_MODELS, ENRICHMENT_TYPES, type EnrichedContext, type EnrichedTicket, type Enricher, type EnricherFactory, FilePublisher, type Generator, type GeneratorFactory, GitCollector, type GitCommit, type GitDiff, type LicenseStatus, type LicenseTier, type LogLevel, type Logger, MultiRepoCollector, OUTPUT_FORMATS, PUBLISHER_TYPES, type PipelineResult, type Publisher, type PublisherFactory, type ReleaseAdvisory, type ReleaseNotes, SOURCE_TYPES, type SemverBump, StdoutPublisher, TONES, TemplateGenerator, type UsageLimits, VERSION, analyzeReleaseReadiness, createLogger, fetchWithTimeout, formatNotes, getCollector, getEnricher, getFormatter, getGenerator, getLatestTag, getPublisher, getRecentTags, getTierLimits, hasCollector, hasEnricher, hasGenerator, hasPublisher, isEnrichmentAllowed, isProviderAllowed, isPublisherAllowed, listCollectors, listEnrichers, listFormatters, listGenerators, listPublishers, registerCollector, registerEnricher, registerFormatter, registerGenerator, registerPublisher, reportUsage, resolveLicense, runPipeline, upgradeMessage, validateLicense };
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// src/constants.ts
|
|
2
|
-
var VERSION = "1.
|
|
2
|
+
var VERSION = "1.5.0";
|
|
3
3
|
var DEFAULT_CATEGORIES = ["features", "fixes", "breaking", "improvements", "chores"];
|
|
4
4
|
var DEFAULT_MODELS = {
|
|
5
5
|
anthropic: "claude-sonnet-4-20250514",
|
|
@@ -9,13 +9,13 @@ var DEFAULT_MODELS = {
|
|
|
9
9
|
openclaw: "anthropic/claude-sonnet-4-6"
|
|
10
10
|
};
|
|
11
11
|
var AI_PROVIDERS = ["anthropic", "openai", "gemini", "ollama", "openclaw", "none"];
|
|
12
|
-
var OUTPUT_FORMATS = ["markdown", "html", "json"];
|
|
12
|
+
var OUTPUT_FORMATS = ["markdown", "html", "html-dark", "html-minimal", "html-edgy", "json"];
|
|
13
13
|
var PUBLISHER_TYPES = ["stdout", "file", "slack", "discord", "github-release", "teams", "confluence", "notion", "gitlab-release", "changelog"];
|
|
14
14
|
var ENRICHMENT_TYPES = ["jira", "linear"];
|
|
15
15
|
var CHANGE_CATEGORIES = ["features", "fixes", "breaking", "improvements", "chores", "other"];
|
|
16
16
|
var AUDIENCES = ["developer", "end-user", "executive"];
|
|
17
|
-
var TONES = ["professional", "casual", "terse"];
|
|
18
|
-
var SOURCE_TYPES = ["local", "jira", "linear", "gitlab", "bitbucket"];
|
|
17
|
+
var TONES = ["professional", "casual", "terse", "edgy", "hype", "snarky"];
|
|
18
|
+
var SOURCE_TYPES = ["local", "jira", "linear", "gitlab", "bitbucket", "multi-repo"];
|
|
19
19
|
|
|
20
20
|
// src/logger.ts
|
|
21
21
|
function createLogger(level = "normal") {
|
|
@@ -36,7 +36,7 @@ function createLogger(level = "normal") {
|
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
// src/collectors/git.ts
|
|
39
|
-
import {
|
|
39
|
+
import { execFileSync } from "child_process";
|
|
40
40
|
function validateRef(ref) {
|
|
41
41
|
if (!ref || ref.length > 256) {
|
|
42
42
|
throw new Error(`Invalid git ref: too ${ref ? "long" : "short"}`);
|
|
@@ -66,8 +66,9 @@ var GitCollector = class {
|
|
|
66
66
|
const format = "%H|%h|%an|%aI|%s|%b";
|
|
67
67
|
const separator = "---CULLIT_COMMIT---";
|
|
68
68
|
try {
|
|
69
|
-
return
|
|
70
|
-
|
|
69
|
+
return execFileSync(
|
|
70
|
+
"git",
|
|
71
|
+
["log", `${from}..${to}`, `--format=${format}${separator}`, "--no-merges"],
|
|
71
72
|
{ cwd: this.cwd, encoding: "utf-8", maxBuffer: 10 * 1024 * 1024 }
|
|
72
73
|
);
|
|
73
74
|
} catch (error) {
|
|
@@ -130,8 +131,9 @@ ${body}` : message;
|
|
|
130
131
|
}
|
|
131
132
|
getFilesChanged(from, to) {
|
|
132
133
|
try {
|
|
133
|
-
const output =
|
|
134
|
-
|
|
134
|
+
const output = execFileSync(
|
|
135
|
+
"git",
|
|
136
|
+
["diff", "--shortstat", `${from}..${to}`],
|
|
135
137
|
{ cwd: this.cwd, encoding: "utf-8" }
|
|
136
138
|
);
|
|
137
139
|
const match = output.match(/(\d+) files? changed/);
|
|
@@ -143,8 +145,9 @@ ${body}` : message;
|
|
|
143
145
|
};
|
|
144
146
|
function getRecentTags(cwd = process.cwd(), count = 10) {
|
|
145
147
|
try {
|
|
146
|
-
const output =
|
|
147
|
-
|
|
148
|
+
const output = execFileSync(
|
|
149
|
+
"git",
|
|
150
|
+
["tag", "--sort=-v:refname"],
|
|
148
151
|
{ cwd, encoding: "utf-8" }
|
|
149
152
|
);
|
|
150
153
|
return output.trim().split("\n").filter(Boolean).slice(0, count);
|
|
@@ -154,7 +157,7 @@ function getRecentTags(cwd = process.cwd(), count = 10) {
|
|
|
154
157
|
}
|
|
155
158
|
function getLatestTag(cwd = process.cwd()) {
|
|
156
159
|
try {
|
|
157
|
-
return
|
|
160
|
+
return execFileSync("git", ["describe", "--tags", "--abbrev=0"], { cwd, encoding: "utf-8" }).trim();
|
|
158
161
|
} catch {
|
|
159
162
|
return null;
|
|
160
163
|
}
|
|
@@ -164,8 +167,9 @@ function getCommitsSince(from, to, cwd = process.cwd()) {
|
|
|
164
167
|
validateRef(to);
|
|
165
168
|
const format = "%H|%h|%an|%aI|%s";
|
|
166
169
|
const separator = "---CULLIT_COMMIT---";
|
|
167
|
-
const log =
|
|
168
|
-
|
|
170
|
+
const log = execFileSync(
|
|
171
|
+
"git",
|
|
172
|
+
["log", `${from}..${to}`, `--format=${format}${separator}`, "--no-merges"],
|
|
169
173
|
{ cwd, encoding: "utf-8", maxBuffer: 10 * 1024 * 1024 }
|
|
170
174
|
);
|
|
171
175
|
if (!log.trim()) return [];
|
|
@@ -181,6 +185,80 @@ function getCommitsSince(from, to, cwd = process.cwd()) {
|
|
|
181
185
|
});
|
|
182
186
|
}
|
|
183
187
|
|
|
188
|
+
// src/collectors/multi-repo.ts
|
|
189
|
+
import { execFileSync as execFileSync2 } from "child_process";
|
|
190
|
+
import { mkdtempSync, rmSync } from "fs";
|
|
191
|
+
import { join } from "path";
|
|
192
|
+
import { tmpdir } from "os";
|
|
193
|
+
var MultiRepoCollector = class {
|
|
194
|
+
repos;
|
|
195
|
+
tempDirs = [];
|
|
196
|
+
constructor(repos) {
|
|
197
|
+
if (!repos.length) throw new Error("Multi-repo collector requires at least one repo");
|
|
198
|
+
this.repos = repos;
|
|
199
|
+
}
|
|
200
|
+
async collect(from, to) {
|
|
201
|
+
const allCommits = [];
|
|
202
|
+
let totalFilesChanged = 0;
|
|
203
|
+
try {
|
|
204
|
+
for (const repo of this.repos) {
|
|
205
|
+
const repoPath = await this.resolveRepoPath(repo);
|
|
206
|
+
const repoName = repo.name || this.inferName(repo);
|
|
207
|
+
const repoFrom = repo.from || from;
|
|
208
|
+
const repoTo = repo.to || to;
|
|
209
|
+
const collector = new GitCollector(repoPath);
|
|
210
|
+
const diff = await collector.collect(repoFrom, repoTo);
|
|
211
|
+
const taggedCommits = diff.commits.map((c) => ({
|
|
212
|
+
...c,
|
|
213
|
+
message: `[${repoName}] ${c.message}`
|
|
214
|
+
}));
|
|
215
|
+
allCommits.push(...taggedCommits);
|
|
216
|
+
totalFilesChanged += diff.filesChanged || 0;
|
|
217
|
+
}
|
|
218
|
+
} finally {
|
|
219
|
+
this.cleanup();
|
|
220
|
+
}
|
|
221
|
+
allCommits.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime());
|
|
222
|
+
return {
|
|
223
|
+
from,
|
|
224
|
+
to,
|
|
225
|
+
commits: allCommits,
|
|
226
|
+
filesChanged: totalFilesChanged
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
async resolveRepoPath(repo) {
|
|
230
|
+
if (repo.path) return repo.path;
|
|
231
|
+
if (!repo.url) {
|
|
232
|
+
throw new Error('Each repo must have either "url" or "path"');
|
|
233
|
+
}
|
|
234
|
+
if (!/^(https?:\/\/|git@|ssh:\/\/)/.test(repo.url)) {
|
|
235
|
+
throw new Error(`Invalid repo URL: ${repo.url}`);
|
|
236
|
+
}
|
|
237
|
+
const tempDir = mkdtempSync(join(tmpdir(), "cullit-repo-"));
|
|
238
|
+
this.tempDirs.push(tempDir);
|
|
239
|
+
execFileSync2(
|
|
240
|
+
"git",
|
|
241
|
+
["clone", "--depth=500", "--single-branch", repo.url, tempDir],
|
|
242
|
+
{ encoding: "utf-8", timeout: 6e4, stdio: "pipe" }
|
|
243
|
+
);
|
|
244
|
+
return tempDir;
|
|
245
|
+
}
|
|
246
|
+
inferName(repo) {
|
|
247
|
+
const source = repo.url || repo.path || "unknown";
|
|
248
|
+
const basename = source.replace(/\.git$/, "").split(/[/\\]/).pop();
|
|
249
|
+
return basename || "unknown";
|
|
250
|
+
}
|
|
251
|
+
cleanup() {
|
|
252
|
+
for (const dir of this.tempDirs) {
|
|
253
|
+
try {
|
|
254
|
+
rmSync(dir, { recursive: true, force: true });
|
|
255
|
+
} catch {
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
this.tempDirs = [];
|
|
259
|
+
}
|
|
260
|
+
};
|
|
261
|
+
|
|
184
262
|
// src/generators/template.ts
|
|
185
263
|
var TemplateGenerator = class {
|
|
186
264
|
async generate(context, config) {
|
|
@@ -292,6 +370,18 @@ var TemplateGenerator = class {
|
|
|
292
370
|
if (parts.length === 0) return `A quick update with ${commitCount} commits \u2014 nothing too wild.`;
|
|
293
371
|
return `We've got ${parts.join(", ")} packed into ${commitCount} commits. Let's go!`;
|
|
294
372
|
}
|
|
373
|
+
if (tone === "edgy") {
|
|
374
|
+
if (parts.length === 0) return `${commitCount} commits. No fluff. Just code that needed to exist.`;
|
|
375
|
+
return `Shipped: ${parts.join(", ")}. ${commitCount} commits. Zero apologies.`;
|
|
376
|
+
}
|
|
377
|
+
if (tone === "hype") {
|
|
378
|
+
if (parts.length === 0) return `\u{1F525} ${commitCount} commits just dropped and they're INCREDIBLE!`;
|
|
379
|
+
return `\u{1F680} HUGE release! ${parts.join(", ")} across ${commitCount} commits! This changes EVERYTHING!`;
|
|
380
|
+
}
|
|
381
|
+
if (tone === "snarky") {
|
|
382
|
+
if (parts.length === 0) return `${commitCount} commits. We were bored, okay?`;
|
|
383
|
+
return `Oh look, ${parts.join(", ")} from ${commitCount} commits. You're welcome.`;
|
|
384
|
+
}
|
|
295
385
|
return parts.length > 0 ? `This release includes ${parts.join(", ")} across ${commitCount} commits.` : `This release includes ${commitCount} commits.`;
|
|
296
386
|
}
|
|
297
387
|
};
|
|
@@ -396,12 +486,64 @@ function groupByCategory(notes) {
|
|
|
396
486
|
}
|
|
397
487
|
return grouped;
|
|
398
488
|
}
|
|
489
|
+
var THEME_DARK = `<style>
|
|
490
|
+
.cull-release{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;background:#0f1117;color:#d4d4e0;padding:2rem;border-radius:12px;max-width:720px;line-height:1.7}
|
|
491
|
+
.cull-release h2{color:#f0f0f5;font-size:1.6rem;border-bottom:2px solid #5eead4;padding-bottom:.5rem;margin-bottom:1rem}
|
|
492
|
+
.cull-release h3{color:#5eead4;font-size:1.1rem;margin:1.5rem 0 .5rem}
|
|
493
|
+
.cull-release .summary{color:#7e819a;font-size:.95rem;margin-bottom:1.5rem}
|
|
494
|
+
.cull-release ul{list-style:none;padding:0}
|
|
495
|
+
.cull-release li{padding:.4rem 0;border-bottom:1px solid #1e2030;font-size:.9rem}
|
|
496
|
+
.cull-release li:last-child{border-bottom:none}
|
|
497
|
+
.cull-release code{background:#161822;color:#5eead4;padding:.15rem .4rem;border-radius:4px;font-size:.8rem}
|
|
498
|
+
.cull-release footer{margin-top:2rem;padding-top:1rem;border-top:1px solid #282a3a;color:#7e819a;font-size:.75rem}
|
|
499
|
+
.cull-release a{color:#5eead4;text-decoration:none}
|
|
500
|
+
</style>
|
|
501
|
+
`;
|
|
502
|
+
var THEME_MINIMAL = `<style>
|
|
503
|
+
.cull-release{font-family:'Georgia',serif;background:#fafafa;color:#222;padding:2.5rem;max-width:680px;line-height:1.8}
|
|
504
|
+
.cull-release h2{font-weight:400;font-size:1.4rem;letter-spacing:-.02em;border-bottom:1px solid #ddd;padding-bottom:.75rem;margin-bottom:1.25rem}
|
|
505
|
+
.cull-release h3{font-weight:600;font-size:.9rem;text-transform:uppercase;letter-spacing:.1em;color:#888;margin:1.5rem 0 .5rem}
|
|
506
|
+
.cull-release .summary{color:#555;font-size:.95rem;font-style:italic;margin-bottom:1.5rem}
|
|
507
|
+
.cull-release ul{list-style:none;padding:0}
|
|
508
|
+
.cull-release li{padding:.5rem 0;border-bottom:1px solid #eee;font-size:.9rem}
|
|
509
|
+
.cull-release li:last-child{border-bottom:none}
|
|
510
|
+
.cull-release code{background:#f0f0f0;color:#c7254e;padding:.1rem .35rem;border-radius:3px;font-size:.8rem}
|
|
511
|
+
.cull-release footer{margin-top:2rem;color:#aaa;font-size:.75rem}
|
|
512
|
+
.cull-release a{color:#222}
|
|
513
|
+
</style>
|
|
514
|
+
`;
|
|
515
|
+
var THEME_EDGY = `<style>
|
|
516
|
+
.cull-release{font-family:'Courier New',monospace;background:#000;color:#0f0;padding:2rem;border:1px solid #0f0;border-radius:0;max-width:720px;line-height:1.6;text-shadow:0 0 4px rgba(0,255,0,.3)}
|
|
517
|
+
.cull-release h2{color:#0f0;font-size:1.5rem;text-transform:uppercase;letter-spacing:.15em;border-bottom:2px solid #0f0;padding-bottom:.5rem;margin-bottom:1rem}
|
|
518
|
+
.cull-release h3{color:#0f0;font-size:1rem;text-transform:uppercase;letter-spacing:.1em;margin:1.5rem 0 .5rem;opacity:.8}
|
|
519
|
+
.cull-release h3::before{content:'> '}
|
|
520
|
+
.cull-release .summary{color:#0c0;font-size:.9rem;margin-bottom:1.5rem;opacity:.7}
|
|
521
|
+
.cull-release ul{list-style:none;padding:0}
|
|
522
|
+
.cull-release li{padding:.3rem 0;font-size:.85rem}
|
|
523
|
+
.cull-release li::before{content:'$ ';color:#0a0;opacity:.6}
|
|
524
|
+
.cull-release code{background:#001100;color:#5f5;padding:.15rem .4rem;border:1px solid #0a0;font-size:.8rem}
|
|
525
|
+
.cull-release footer{margin-top:2rem;padding-top:1rem;border-top:1px solid #0a0;color:#0a0;font-size:.7rem;opacity:.5}
|
|
526
|
+
.cull-release a{color:#0f0}
|
|
527
|
+
</style>
|
|
528
|
+
`;
|
|
529
|
+
function formatThemedHTML(theme) {
|
|
530
|
+
const themeCSS = {
|
|
531
|
+
dark: THEME_DARK,
|
|
532
|
+
minimal: THEME_MINIMAL,
|
|
533
|
+
edgy: THEME_EDGY
|
|
534
|
+
};
|
|
535
|
+
return (notes) => (themeCSS[theme] || "") + formatHTML(notes);
|
|
536
|
+
}
|
|
399
537
|
registerFormatter("markdown", formatMarkdown);
|
|
400
538
|
registerFormatter("html", formatHTML);
|
|
539
|
+
registerFormatter("html-dark", formatThemedHTML("dark"));
|
|
540
|
+
registerFormatter("html-minimal", formatThemedHTML("minimal"));
|
|
541
|
+
registerFormatter("html-edgy", formatThemedHTML("edgy"));
|
|
401
542
|
registerFormatter("json", (notes) => JSON.stringify(notes, null, 2));
|
|
402
543
|
|
|
403
544
|
// src/publishers/index.ts
|
|
404
545
|
import { writeFileSync } from "fs";
|
|
546
|
+
import { resolve, relative, isAbsolute } from "path";
|
|
405
547
|
var StdoutPublisher = class {
|
|
406
548
|
async publish(notes, format, preformatted) {
|
|
407
549
|
console.log(preformatted || formatNotes(notes, format));
|
|
@@ -410,6 +552,12 @@ var StdoutPublisher = class {
|
|
|
410
552
|
var FilePublisher = class {
|
|
411
553
|
constructor(path) {
|
|
412
554
|
this.path = path;
|
|
555
|
+
const resolved = resolve(path);
|
|
556
|
+
const cwd = resolve(".");
|
|
557
|
+
const rel = relative(cwd, resolved);
|
|
558
|
+
if (rel.startsWith("..") || isAbsolute(rel)) {
|
|
559
|
+
throw new Error(`File output path must be within the project directory. Got: ${path}`);
|
|
560
|
+
}
|
|
413
561
|
}
|
|
414
562
|
async publish(notes, format, preformatted) {
|
|
415
563
|
const output = preformatted || formatNotes(notes, format);
|
|
@@ -419,7 +567,7 @@ var FilePublisher = class {
|
|
|
419
567
|
};
|
|
420
568
|
|
|
421
569
|
// src/advisor.ts
|
|
422
|
-
import {
|
|
570
|
+
import { execFileSync as execFileSync3 } from "child_process";
|
|
423
571
|
var COMMIT_PATTERNS = [
|
|
424
572
|
{ pattern: /^breaking[(!:]|^BREAKING CHANGE/i, category: "breaking" },
|
|
425
573
|
{ pattern: /!:/, category: "breaking" },
|
|
@@ -456,8 +604,9 @@ function getCommitsSinceTag(tag, cwd) {
|
|
|
456
604
|
}
|
|
457
605
|
function getTagDate(tag, cwd) {
|
|
458
606
|
try {
|
|
459
|
-
const dateStr =
|
|
460
|
-
|
|
607
|
+
const dateStr = execFileSync3(
|
|
608
|
+
"git",
|
|
609
|
+
["log", "-1", "--format=%aI", tag],
|
|
461
610
|
{ cwd, encoding: "utf-8" }
|
|
462
611
|
).trim();
|
|
463
612
|
return new Date(dateStr);
|
|
@@ -565,6 +714,7 @@ async function fetchWithTimeout(url, init, timeoutMs = DEFAULT_TIMEOUT) {
|
|
|
565
714
|
var FREE_PROVIDERS = /* @__PURE__ */ new Set(["none"]);
|
|
566
715
|
var FREE_PUBLISHERS = /* @__PURE__ */ new Set(["stdout", "file"]);
|
|
567
716
|
var LICENSE_CACHE_TTL = 24 * 60 * 60 * 1e3;
|
|
717
|
+
var LICENSE_FAILURE_CACHE_TTL = 5 * 60 * 1e3;
|
|
568
718
|
var cachedValidation = null;
|
|
569
719
|
function resolveLicense() {
|
|
570
720
|
const key = process.env.CULLIT_API_KEY?.trim();
|
|
@@ -612,10 +762,13 @@ async function validateLicense() {
|
|
|
612
762
|
valid: false,
|
|
613
763
|
message: "License validation failed. Check your API key at https://cullit.io/pricing"
|
|
614
764
|
};
|
|
615
|
-
cachedValidation = { status, key, expiresAt: Date.now() +
|
|
765
|
+
cachedValidation = { status, key, expiresAt: Date.now() + LICENSE_FAILURE_CACHE_TTL };
|
|
616
766
|
return status;
|
|
617
767
|
} catch {
|
|
618
|
-
|
|
768
|
+
if (cachedValidation && cachedValidation.key === key) {
|
|
769
|
+
return cachedValidation.status;
|
|
770
|
+
}
|
|
771
|
+
return { tier: "pro", valid: true, message: "Offline validation \u2014 using cached license." };
|
|
619
772
|
}
|
|
620
773
|
}
|
|
621
774
|
function isProviderAllowed(provider, license) {
|
|
@@ -635,7 +788,7 @@ function upgradeMessage(feature) {
|
|
|
635
788
|
Then set CULLIT_API_KEY in your environment.`;
|
|
636
789
|
}
|
|
637
790
|
var TIER_LIMITS = {
|
|
638
|
-
free: { generationsPerMonth:
|
|
791
|
+
free: { generationsPerMonth: 3, maxProjects: 1 },
|
|
639
792
|
pro: { generationsPerMonth: 500, maxProjects: 5 },
|
|
640
793
|
team: { generationsPerMonth: 2e3, maxProjects: 25 },
|
|
641
794
|
enterprise: { generationsPerMonth: Infinity, maxProjects: Infinity }
|
|
@@ -719,7 +872,11 @@ function listPublishers() {
|
|
|
719
872
|
}
|
|
720
873
|
|
|
721
874
|
// src/index.ts
|
|
722
|
-
registerCollector("local", () => new GitCollector());
|
|
875
|
+
registerCollector("local", (config) => new GitCollector(config.source?.repoPath));
|
|
876
|
+
registerCollector("multi-repo", (config) => {
|
|
877
|
+
if (!config.repos?.length) throw new Error('Multi-repo source requires "repos" array in config');
|
|
878
|
+
return new MultiRepoCollector(config.repos);
|
|
879
|
+
});
|
|
723
880
|
registerGenerator("none", () => new TemplateGenerator());
|
|
724
881
|
registerPublisher("stdout", (_target) => new StdoutPublisher());
|
|
725
882
|
registerPublisher("file", (target) => new FilePublisher(target.path));
|
|
@@ -834,6 +991,7 @@ export {
|
|
|
834
991
|
ENRICHMENT_TYPES,
|
|
835
992
|
FilePublisher,
|
|
836
993
|
GitCollector,
|
|
994
|
+
MultiRepoCollector,
|
|
837
995
|
OUTPUT_FORMATS,
|
|
838
996
|
PUBLISHER_TYPES,
|
|
839
997
|
SOURCE_TYPES,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cullit/core",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Core engine for Cullit — AI-powered release note generation.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -31,10 +31,11 @@
|
|
|
31
31
|
"node": ">=18"
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@cullit/config": "1.
|
|
34
|
+
"@cullit/config": "1.5.0"
|
|
35
35
|
},
|
|
36
36
|
"scripts": {
|
|
37
37
|
"build": "tsup src/index.ts --format esm --dts --clean",
|
|
38
|
-
"dev": "tsup src/index.ts --format esm --dts --watch"
|
|
38
|
+
"dev": "tsup src/index.ts --format esm --dts --watch",
|
|
39
|
+
"test": "vitest run"
|
|
39
40
|
}
|
|
40
41
|
}
|