@cullit/core 0.5.0 → 1.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/index.d.ts +38 -8
- package/dist/index.js +210 -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, CullConfig, EnrichmentType, JiraConfig, LinearConfig, 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 = "
|
|
74
|
+
declare const VERSION = "1.4.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"];
|
|
79
|
-
declare const PUBLISHER_TYPES: readonly ["stdout", "file", "slack", "discord", "github-release"];
|
|
78
|
+
declare const OUTPUT_FORMATS: readonly ["markdown", "html", "html-dark", "html-minimal", "html-edgy", "json"];
|
|
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"];
|
|
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.
|
|
@@ -234,6 +251,19 @@ declare function isEnrichmentAllowed(license: LicenseStatus): boolean;
|
|
|
234
251
|
* Build a human-readable upgrade message for a gated feature.
|
|
235
252
|
*/
|
|
236
253
|
declare function upgradeMessage(feature: string): string;
|
|
254
|
+
interface UsageLimits {
|
|
255
|
+
generationsPerMonth: number;
|
|
256
|
+
maxProjects: number;
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* Get usage limits for a license tier.
|
|
260
|
+
*/
|
|
261
|
+
declare function getTierLimits(tier: string): UsageLimits;
|
|
262
|
+
/**
|
|
263
|
+
* Report a generation event to the metering service.
|
|
264
|
+
* Non-blocking — failures are logged but never block the pipeline.
|
|
265
|
+
*/
|
|
266
|
+
declare function reportUsage(project?: string): Promise<void>;
|
|
237
267
|
|
|
238
268
|
/**
|
|
239
269
|
* Plugin Registry — the seam between free (core) and pro features.
|
|
@@ -280,4 +310,4 @@ declare function runPipeline(from: string, to: string, config: CullConfig, optio
|
|
|
280
310
|
logger?: Logger;
|
|
281
311
|
}): Promise<PipelineResult>;
|
|
282
312
|
|
|
283
|
-
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, VERSION, analyzeReleaseReadiness, createLogger, fetchWithTimeout, formatNotes, getCollector, getEnricher, getFormatter, getGenerator, getLatestTag, getPublisher, getRecentTags, hasCollector, hasEnricher, hasGenerator, hasPublisher, isEnrichmentAllowed, isProviderAllowed, isPublisherAllowed, listCollectors, listEnrichers, listFormatters, listGenerators, listPublishers, registerCollector, registerEnricher, registerFormatter, registerGenerator, registerPublisher, 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 = "
|
|
2
|
+
var VERSION = "1.4.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"];
|
|
13
|
-
var PUBLISHER_TYPES = ["stdout", "file", "slack", "discord", "github-release"];
|
|
12
|
+
var OUTPUT_FORMATS = ["markdown", "html", "html-dark", "html-minimal", "html-edgy", "json"];
|
|
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"];
|
|
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) {
|
|
@@ -634,6 +787,35 @@ function upgradeMessage(feature) {
|
|
|
634
787
|
Get your API key at https://cullit.io/pricing
|
|
635
788
|
Then set CULLIT_API_KEY in your environment.`;
|
|
636
789
|
}
|
|
790
|
+
var TIER_LIMITS = {
|
|
791
|
+
free: { generationsPerMonth: 3, maxProjects: 1 },
|
|
792
|
+
pro: { generationsPerMonth: 500, maxProjects: 5 },
|
|
793
|
+
team: { generationsPerMonth: 2e3, maxProjects: 25 },
|
|
794
|
+
enterprise: { generationsPerMonth: Infinity, maxProjects: Infinity }
|
|
795
|
+
};
|
|
796
|
+
function getTierLimits(tier) {
|
|
797
|
+
return TIER_LIMITS[tier] || TIER_LIMITS.free;
|
|
798
|
+
}
|
|
799
|
+
async function reportUsage(project = "default") {
|
|
800
|
+
const key = process.env.CULLIT_API_KEY?.trim();
|
|
801
|
+
const meterUrl = process.env.CULLIT_METER_URL?.trim();
|
|
802
|
+
if (!meterUrl || !key) return;
|
|
803
|
+
try {
|
|
804
|
+
await fetchWithTimeout(meterUrl, {
|
|
805
|
+
method: "POST",
|
|
806
|
+
headers: {
|
|
807
|
+
"Content-Type": "application/json",
|
|
808
|
+
"Authorization": `Bearer ${key}`
|
|
809
|
+
},
|
|
810
|
+
body: JSON.stringify({
|
|
811
|
+
event: "generation",
|
|
812
|
+
project,
|
|
813
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
814
|
+
})
|
|
815
|
+
}, 5e3);
|
|
816
|
+
} catch {
|
|
817
|
+
}
|
|
818
|
+
}
|
|
637
819
|
|
|
638
820
|
// src/registry.ts
|
|
639
821
|
var collectors = /* @__PURE__ */ new Map();
|
|
@@ -690,7 +872,11 @@ function listPublishers() {
|
|
|
690
872
|
}
|
|
691
873
|
|
|
692
874
|
// src/index.ts
|
|
693
|
-
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
|
+
});
|
|
694
880
|
registerGenerator("none", () => new TemplateGenerator());
|
|
695
881
|
registerPublisher("stdout", (_target) => new StdoutPublisher());
|
|
696
882
|
registerPublisher("file", (target) => new FilePublisher(target.path));
|
|
@@ -805,6 +991,7 @@ export {
|
|
|
805
991
|
ENRICHMENT_TYPES,
|
|
806
992
|
FilePublisher,
|
|
807
993
|
GitCollector,
|
|
994
|
+
MultiRepoCollector,
|
|
808
995
|
OUTPUT_FORMATS,
|
|
809
996
|
PUBLISHER_TYPES,
|
|
810
997
|
SOURCE_TYPES,
|
|
@@ -823,6 +1010,7 @@ export {
|
|
|
823
1010
|
getLatestTag,
|
|
824
1011
|
getPublisher,
|
|
825
1012
|
getRecentTags,
|
|
1013
|
+
getTierLimits,
|
|
826
1014
|
hasCollector,
|
|
827
1015
|
hasEnricher,
|
|
828
1016
|
hasGenerator,
|
|
@@ -840,6 +1028,7 @@ export {
|
|
|
840
1028
|
registerFormatter,
|
|
841
1029
|
registerGenerator,
|
|
842
1030
|
registerPublisher,
|
|
1031
|
+
reportUsage,
|
|
843
1032
|
resolveLicense,
|
|
844
1033
|
runPipeline,
|
|
845
1034
|
upgradeMessage,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cullit/core",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "1.4.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": "
|
|
34
|
+
"@cullit/config": "1.4.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
|
}
|