@cullit/core 1.8.0 → 1.10.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 +17 -5
- package/dist/index.js +159 -24
- package/package.json +10 -3
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
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';
|
|
2
|
+
export { AIConfig, AIProvider, Audience, BitbucketConfig, ConfluenceConfig, CullConfig, EnrichmentType, GitLabConfig, JiraConfig, LinearConfig, NotionConfig, OpenClawConfig, OutputFormat, PublishTarget, PublisherType, RepoSource, SourceConfig, TemplateConfig, TemplateProfile, Tone } from '@cullit/config';
|
|
3
3
|
|
|
4
4
|
interface GitCommit {
|
|
5
5
|
hash: string;
|
|
@@ -71,7 +71,7 @@ interface PipelineResult {
|
|
|
71
71
|
duration: number;
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
-
declare const VERSION = "1.
|
|
74
|
+
declare const VERSION = "1.10.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"];
|
|
@@ -159,6 +159,7 @@ declare function registerFormatter(format: string, fn: FormatterFn): void;
|
|
|
159
159
|
declare function getFormatter(format: string): FormatterFn | undefined;
|
|
160
160
|
declare function listFormatters(): string[];
|
|
161
161
|
declare function formatNotes(notes: ReleaseNotes, format: OutputFormat): string;
|
|
162
|
+
declare function escapeHtml(str: string): string;
|
|
162
163
|
|
|
163
164
|
/**
|
|
164
165
|
* Outputs release notes to stdout (default).
|
|
@@ -218,7 +219,7 @@ declare function analyzeReleaseReadiness(cwd?: string): ReleaseAdvisory;
|
|
|
218
219
|
* validateLicense() performs async remote validation with caching.
|
|
219
220
|
* resolveLicense() remains sync for quick format-only checks (display).
|
|
220
221
|
*/
|
|
221
|
-
type LicenseTier = 'free' | 'pro';
|
|
222
|
+
type LicenseTier = 'free' | 'pro' | 'team' | 'enterprise';
|
|
222
223
|
interface LicenseStatus {
|
|
223
224
|
tier: LicenseTier;
|
|
224
225
|
valid: boolean;
|
|
@@ -241,6 +242,7 @@ declare function validateLicense(): Promise<LicenseStatus>;
|
|
|
241
242
|
declare function isProviderAllowed(provider: string, license: LicenseStatus): boolean;
|
|
242
243
|
/**
|
|
243
244
|
* Check whether the current license allows the requested publisher.
|
|
245
|
+
* Confluence, Notion, and Teams require Team tier or above.
|
|
244
246
|
*/
|
|
245
247
|
declare function isPublisherAllowed(publisherType: string, license: LicenseStatus): boolean;
|
|
246
248
|
/**
|
|
@@ -259,6 +261,15 @@ interface UsageLimits {
|
|
|
259
261
|
* Get usage limits for a license tier.
|
|
260
262
|
*/
|
|
261
263
|
declare function getTierLimits(tier: string): UsageLimits;
|
|
264
|
+
type TeamFeature = 'drafts' | 'approvals' | 'shared_history' | 'project_templates' | 'hosted_changelog' | 'branded_widget' | 'team_publishers' | 'org_settings' | 'audit_logs' | 'sso';
|
|
265
|
+
/**
|
|
266
|
+
* Check whether a license tier grants access to a Team/Enterprise feature.
|
|
267
|
+
*/
|
|
268
|
+
declare function isFeatureAllowed(feature: TeamFeature, tier: string): boolean;
|
|
269
|
+
/**
|
|
270
|
+
* Build a gating summary for a tier — which features are unlocked.
|
|
271
|
+
*/
|
|
272
|
+
declare function getFeatureGating(tier: string): Record<TeamFeature, boolean>;
|
|
262
273
|
/**
|
|
263
274
|
* Report a generation event to the metering service.
|
|
264
275
|
* Non-blocking — failures are logged but never block the pipeline.
|
|
@@ -271,7 +282,7 @@ declare function reportUsage(project?: string): Promise<void>;
|
|
|
271
282
|
* Core registers: git collector, template generator, stdout/file publishers.
|
|
272
283
|
* Pro registers: AI generator, Jira/Linear collectors + enrichers, Slack/Discord/GitHub publishers.
|
|
273
284
|
*
|
|
274
|
-
*
|
|
285
|
+
* Paid plugin registration is preloaded by the licensed distribution package.
|
|
275
286
|
*/
|
|
276
287
|
|
|
277
288
|
type CollectorFactory = (...args: any[]) => Collector;
|
|
@@ -308,6 +319,7 @@ declare function runPipeline(from: string, to: string, config: CullConfig, optio
|
|
|
308
319
|
format?: OutputFormat;
|
|
309
320
|
dryRun?: boolean;
|
|
310
321
|
logger?: Logger;
|
|
322
|
+
templateProfile?: string;
|
|
311
323
|
}): Promise<PipelineResult>;
|
|
312
324
|
|
|
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 };
|
|
325
|
+
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, type TeamFeature, TemplateGenerator, type UsageLimits, VERSION, analyzeReleaseReadiness, createLogger, escapeHtml, fetchWithTimeout, formatNotes, getCollector, getEnricher, getFeatureGating, getFormatter, getGenerator, getLatestTag, getPublisher, getRecentTags, getTierLimits, hasCollector, hasEnricher, hasGenerator, hasPublisher, isEnrichmentAllowed, isFeatureAllowed, 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.10.0";
|
|
3
3
|
var DEFAULT_CATEGORIES = ["features", "fixes", "breaking", "improvements", "chores"];
|
|
4
4
|
var DEFAULT_MODELS = {
|
|
5
5
|
anthropic: "claude-sonnet-4-20250514",
|
|
@@ -72,7 +72,8 @@ var GitCollector = class {
|
|
|
72
72
|
{ cwd: this.cwd, encoding: "utf-8", maxBuffer: 10 * 1024 * 1024 }
|
|
73
73
|
);
|
|
74
74
|
} catch (error) {
|
|
75
|
-
const
|
|
75
|
+
const errWithStderr = typeof error === "object" && error !== null && "stderr" in error ? error : void 0;
|
|
76
|
+
const stderr = errWithStderr?.stderr?.toString?.() || "";
|
|
76
77
|
const hint = stderr.includes("unknown revision") ? 'Check that both refs exist (run "cullit tags" to see tags).' : stderr.includes("not a git repository") ? "Run this command inside a git repository." : `Make sure both refs exist and you're in a git repository.`;
|
|
77
78
|
throw new Error(
|
|
78
79
|
`Failed to read git log between ${from} and ${to}. ${hint}`
|
|
@@ -417,12 +418,15 @@ function formatNotes(notes, format) {
|
|
|
417
418
|
const fn = formatters.get(format) || formatters.get("markdown");
|
|
418
419
|
return fn(notes);
|
|
419
420
|
}
|
|
421
|
+
function sanitizeForMarkdown(text) {
|
|
422
|
+
return escapeHtml(text).replace(/\r?\n/g, " ");
|
|
423
|
+
}
|
|
420
424
|
function formatMarkdown(notes) {
|
|
421
425
|
const lines = [];
|
|
422
|
-
lines.push(`## ${notes.version} \u2014 ${notes.date}`);
|
|
426
|
+
lines.push(`## ${sanitizeForMarkdown(notes.version)} \u2014 ${sanitizeForMarkdown(notes.date)}`);
|
|
423
427
|
lines.push("");
|
|
424
428
|
if (notes.summary) {
|
|
425
|
-
lines.push(notes.summary);
|
|
429
|
+
lines.push(sanitizeForMarkdown(notes.summary));
|
|
426
430
|
lines.push("");
|
|
427
431
|
}
|
|
428
432
|
const grouped = groupByCategory(notes);
|
|
@@ -432,8 +436,8 @@ function formatMarkdown(notes) {
|
|
|
432
436
|
lines.push(`### ${CATEGORY_LABELS[category]}`);
|
|
433
437
|
lines.push("");
|
|
434
438
|
for (const entry of entries) {
|
|
435
|
-
let line = `- ${entry.description}`;
|
|
436
|
-
if (entry.ticketKey) line += ` (${entry.ticketKey})`;
|
|
439
|
+
let line = `- ${sanitizeForMarkdown(entry.description)}`;
|
|
440
|
+
if (entry.ticketKey) line += ` (${sanitizeForMarkdown(entry.ticketKey)})`;
|
|
437
441
|
lines.push(line);
|
|
438
442
|
}
|
|
439
443
|
lines.push("");
|
|
@@ -441,7 +445,7 @@ function formatMarkdown(notes) {
|
|
|
441
445
|
if (notes.contributors?.length) {
|
|
442
446
|
lines.push("### Contributors");
|
|
443
447
|
lines.push("");
|
|
444
|
-
lines.push(notes.contributors.map((c) => `@${c}`).join(", "));
|
|
448
|
+
lines.push(notes.contributors.map((c) => `@${sanitizeForMarkdown(c)}`).join(", "));
|
|
445
449
|
lines.push("");
|
|
446
450
|
}
|
|
447
451
|
if (notes.metadata) {
|
|
@@ -713,6 +717,7 @@ async function fetchWithTimeout(url, init, timeoutMs = DEFAULT_TIMEOUT) {
|
|
|
713
717
|
// src/gate.ts
|
|
714
718
|
var FREE_PROVIDERS = /* @__PURE__ */ new Set(["none"]);
|
|
715
719
|
var FREE_PUBLISHERS = /* @__PURE__ */ new Set(["stdout", "file"]);
|
|
720
|
+
var TEAM_ONLY_PUBLISHERS = /* @__PURE__ */ new Set(["confluence", "notion", "teams"]);
|
|
716
721
|
var LICENSE_CACHE_TTL = 24 * 60 * 60 * 1e3;
|
|
717
722
|
var LICENSE_FAILURE_CACHE_TTL = 5 * 60 * 1e3;
|
|
718
723
|
var cachedValidation = null;
|
|
@@ -750,7 +755,7 @@ async function validateLicense() {
|
|
|
750
755
|
if (res.ok) {
|
|
751
756
|
const data = await res.json();
|
|
752
757
|
const status2 = {
|
|
753
|
-
tier: data.tier === "pro" ? "pro" : "free",
|
|
758
|
+
tier: data.tier === "team" || data.tier === "enterprise" ? data.tier : data.tier === "pro" ? "pro" : "free",
|
|
754
759
|
valid: data.valid !== false,
|
|
755
760
|
message: data.message
|
|
756
761
|
};
|
|
@@ -768,19 +773,22 @@ async function validateLicense() {
|
|
|
768
773
|
if (cachedValidation && cachedValidation.key === key) {
|
|
769
774
|
return cachedValidation.status;
|
|
770
775
|
}
|
|
771
|
-
return { tier: "
|
|
776
|
+
return { tier: "free", valid: true, message: "License validation unavailable offline. Run while connected to activate your Pro license." };
|
|
772
777
|
}
|
|
773
778
|
}
|
|
774
779
|
function isProviderAllowed(provider, license) {
|
|
775
|
-
if (license.tier
|
|
780
|
+
if (license.tier !== "free" && license.valid) return true;
|
|
776
781
|
return FREE_PROVIDERS.has(provider);
|
|
777
782
|
}
|
|
778
783
|
function isPublisherAllowed(publisherType, license) {
|
|
779
|
-
if (
|
|
784
|
+
if (TEAM_ONLY_PUBLISHERS.has(publisherType)) {
|
|
785
|
+
return (license.tier === "team" || license.tier === "enterprise") && license.valid;
|
|
786
|
+
}
|
|
787
|
+
if (license.tier !== "free" && license.valid) return true;
|
|
780
788
|
return FREE_PUBLISHERS.has(publisherType);
|
|
781
789
|
}
|
|
782
790
|
function isEnrichmentAllowed(license) {
|
|
783
|
-
return license.tier
|
|
791
|
+
return license.tier !== "free" && license.valid;
|
|
784
792
|
}
|
|
785
793
|
function upgradeMessage(feature) {
|
|
786
794
|
return `\u{1F512} ${feature} requires a Cullit Pro license.
|
|
@@ -788,14 +796,37 @@ function upgradeMessage(feature) {
|
|
|
788
796
|
Then set CULLIT_API_KEY in your environment.`;
|
|
789
797
|
}
|
|
790
798
|
var TIER_LIMITS = {
|
|
791
|
-
free: { generationsPerMonth:
|
|
792
|
-
pro: { generationsPerMonth: 500, maxProjects:
|
|
793
|
-
team: { generationsPerMonth: 2e3, maxProjects:
|
|
799
|
+
free: { generationsPerMonth: 5, maxProjects: 3 },
|
|
800
|
+
pro: { generationsPerMonth: 500, maxProjects: 100 },
|
|
801
|
+
team: { generationsPerMonth: 2e3, maxProjects: 250 },
|
|
794
802
|
enterprise: { generationsPerMonth: Infinity, maxProjects: Infinity }
|
|
795
803
|
};
|
|
796
804
|
function getTierLimits(tier) {
|
|
797
805
|
return TIER_LIMITS[tier] || TIER_LIMITS.free;
|
|
798
806
|
}
|
|
807
|
+
var FEATURE_TIERS = {
|
|
808
|
+
drafts: /* @__PURE__ */ new Set(["team", "enterprise"]),
|
|
809
|
+
approvals: /* @__PURE__ */ new Set(["team", "enterprise"]),
|
|
810
|
+
shared_history: /* @__PURE__ */ new Set(["team", "enterprise"]),
|
|
811
|
+
project_templates: /* @__PURE__ */ new Set(["team", "enterprise"]),
|
|
812
|
+
hosted_changelog: /* @__PURE__ */ new Set(["pro", "team", "enterprise"]),
|
|
813
|
+
branded_widget: /* @__PURE__ */ new Set(["team", "enterprise"]),
|
|
814
|
+
team_publishers: /* @__PURE__ */ new Set(["team", "enterprise"]),
|
|
815
|
+
org_settings: /* @__PURE__ */ new Set(["team", "enterprise"]),
|
|
816
|
+
audit_logs: /* @__PURE__ */ new Set(["enterprise"]),
|
|
817
|
+
sso: /* @__PURE__ */ new Set(["enterprise"])
|
|
818
|
+
};
|
|
819
|
+
function isFeatureAllowed(feature, tier) {
|
|
820
|
+
const allowed = FEATURE_TIERS[feature];
|
|
821
|
+
return allowed ? allowed.has(tier) : false;
|
|
822
|
+
}
|
|
823
|
+
function getFeatureGating(tier) {
|
|
824
|
+
const result = {};
|
|
825
|
+
for (const feature of Object.keys(FEATURE_TIERS)) {
|
|
826
|
+
result[feature] = isFeatureAllowed(feature, tier);
|
|
827
|
+
}
|
|
828
|
+
return result;
|
|
829
|
+
}
|
|
799
830
|
async function reportUsage(project = "default") {
|
|
800
831
|
const key = process.env.CULLIT_API_KEY?.trim();
|
|
801
832
|
const meterUrl = process.env.CULLIT_METER_URL?.trim();
|
|
@@ -880,13 +911,77 @@ registerCollector("multi-repo", (config) => {
|
|
|
880
911
|
registerGenerator("none", () => new TemplateGenerator());
|
|
881
912
|
registerPublisher("stdout", (_target) => new StdoutPublisher());
|
|
882
913
|
registerPublisher("file", (target) => new FilePublisher(target.path));
|
|
914
|
+
function normalizeSectionOrder(value) {
|
|
915
|
+
if (!Array.isArray(value)) return void 0;
|
|
916
|
+
const order = value.filter((v) => typeof v === "string").map((v) => v.trim()).filter(Boolean);
|
|
917
|
+
return order.length ? order : void 0;
|
|
918
|
+
}
|
|
919
|
+
function toResolvedTemplate(profile) {
|
|
920
|
+
if (!profile) return {};
|
|
921
|
+
const p = profile;
|
|
922
|
+
return {
|
|
923
|
+
format: typeof p.format === "string" ? p.format : void 0,
|
|
924
|
+
sectionOrder: normalizeSectionOrder(p.sectionOrder),
|
|
925
|
+
includeContributors: typeof p.includeContributors === "boolean" ? p.includeContributors : void 0,
|
|
926
|
+
includeMetadata: typeof p.includeMetadata === "boolean" ? p.includeMetadata : void 0,
|
|
927
|
+
summaryPrefix: typeof p.summaryPrefix === "string" ? p.summaryPrefix : void 0
|
|
928
|
+
};
|
|
929
|
+
}
|
|
930
|
+
function mergeTemplates(...templates) {
|
|
931
|
+
const merged = {};
|
|
932
|
+
for (const template of templates) {
|
|
933
|
+
if (!template) continue;
|
|
934
|
+
if (template.name) merged.name = template.name;
|
|
935
|
+
if (template.format) merged.format = template.format;
|
|
936
|
+
if (template.sectionOrder) merged.sectionOrder = template.sectionOrder;
|
|
937
|
+
if (typeof template.includeContributors === "boolean") merged.includeContributors = template.includeContributors;
|
|
938
|
+
if (typeof template.includeMetadata === "boolean") merged.includeMetadata = template.includeMetadata;
|
|
939
|
+
if (typeof template.summaryPrefix === "string") merged.summaryPrefix = template.summaryPrefix;
|
|
940
|
+
}
|
|
941
|
+
return merged;
|
|
942
|
+
}
|
|
943
|
+
function getTemplateProfileByName(config, name) {
|
|
944
|
+
if (!name) return void 0;
|
|
945
|
+
const profile = config.templates?.find((t) => t.name === name);
|
|
946
|
+
if (!profile) return void 0;
|
|
947
|
+
return { name, ...toResolvedTemplate(profile) };
|
|
948
|
+
}
|
|
949
|
+
function applyTemplateToNotes(notes, template) {
|
|
950
|
+
const next = {
|
|
951
|
+
...notes,
|
|
952
|
+
changes: [...notes.changes]
|
|
953
|
+
};
|
|
954
|
+
if (template.sectionOrder?.length) {
|
|
955
|
+
const index = /* @__PURE__ */ new Map();
|
|
956
|
+
template.sectionOrder.forEach((category, i) => index.set(category, i));
|
|
957
|
+
next.changes = next.changes.map((change, i) => ({ change, i })).sort((a, b) => {
|
|
958
|
+
const ai = index.has(a.change.category) ? index.get(a.change.category) : Number.MAX_SAFE_INTEGER;
|
|
959
|
+
const bi = index.has(b.change.category) ? index.get(b.change.category) : Number.MAX_SAFE_INTEGER;
|
|
960
|
+
if (ai !== bi) return ai - bi;
|
|
961
|
+
return a.i - b.i;
|
|
962
|
+
}).map(({ change }) => change);
|
|
963
|
+
}
|
|
964
|
+
if (template.summaryPrefix) {
|
|
965
|
+
const currentSummary = next.summary || "";
|
|
966
|
+
next.summary = `${template.summaryPrefix}${currentSummary ? ` ${currentSummary}` : ""}`.trim();
|
|
967
|
+
}
|
|
968
|
+
if (template.includeContributors === false) {
|
|
969
|
+
delete next.contributors;
|
|
970
|
+
}
|
|
971
|
+
if (template.includeMetadata === false) {
|
|
972
|
+
delete next.metadata;
|
|
973
|
+
}
|
|
974
|
+
return next;
|
|
975
|
+
}
|
|
883
976
|
async function runPipeline(from, to, config, options = {}) {
|
|
884
977
|
const startTime = Date.now();
|
|
885
|
-
const format = options.format || "markdown";
|
|
886
978
|
const log = options.logger || createLogger("normal");
|
|
887
979
|
const license = await validateLicense();
|
|
888
980
|
if (!license.valid) {
|
|
889
|
-
|
|
981
|
+
if (!isProviderAllowed(config.ai.provider, license)) {
|
|
982
|
+
throw new Error(license.message || "Invalid CULLIT_API_KEY");
|
|
983
|
+
}
|
|
984
|
+
log.warn(`\u26A0 ${license.message || "Invalid CULLIT_API_KEY \u2014 running in free mode."}`);
|
|
890
985
|
}
|
|
891
986
|
if (!isProviderAllowed(config.ai.provider, license)) {
|
|
892
987
|
throw new Error(upgradeMessage(`AI provider "${config.ai.provider}"`));
|
|
@@ -894,7 +989,7 @@ async function runPipeline(from, to, config, options = {}) {
|
|
|
894
989
|
const collectorFactory = getCollector(config.source.type);
|
|
895
990
|
if (!collectorFactory) {
|
|
896
991
|
throw new Error(
|
|
897
|
-
`Source type "${config.source.type}" is not available. ` + (config.source.type !== "local" ? "Install @cullit/
|
|
992
|
+
`Source type "${config.source.type}" is not available. ` + (config.source.type !== "local" ? "Install @cullit/licensed (private distribution) to use this source." : "Valid sources: local")
|
|
898
993
|
);
|
|
899
994
|
}
|
|
900
995
|
const sourceLabel = config.source.type === "local" ? `commits between ${from}..${to}` : `items from ${config.source.type}`;
|
|
@@ -916,7 +1011,7 @@ async function runPipeline(from, to, config, options = {}) {
|
|
|
916
1011
|
}
|
|
917
1012
|
const enricherFactory = getEnricher(source);
|
|
918
1013
|
if (!enricherFactory) {
|
|
919
|
-
log.info(`\xBB Skipping ${source} enrichment \u2014 install @cullit/
|
|
1014
|
+
log.info(`\xBB Skipping ${source} enrichment \u2014 install @cullit/licensed to enable`);
|
|
920
1015
|
continue;
|
|
921
1016
|
}
|
|
922
1017
|
log.info(`\xBB Enriching from ${source}...`);
|
|
@@ -940,7 +1035,7 @@ async function runPipeline(from, to, config, options = {}) {
|
|
|
940
1035
|
const generatorFactory = getGenerator(config.ai.provider);
|
|
941
1036
|
if (!generatorFactory) {
|
|
942
1037
|
throw new Error(
|
|
943
|
-
`AI provider "${config.ai.provider}" is not available. ` + (config.ai.provider !== "none" ? "Install @cullit/
|
|
1038
|
+
`AI provider "${config.ai.provider}" is not available. ` + (config.ai.provider !== "none" ? "Install @cullit/licensed (private distribution) to use AI providers." : "")
|
|
944
1039
|
);
|
|
945
1040
|
}
|
|
946
1041
|
let generator;
|
|
@@ -951,8 +1046,16 @@ async function runPipeline(from, to, config, options = {}) {
|
|
|
951
1046
|
}
|
|
952
1047
|
const notes = await generator.generate(context, config.ai);
|
|
953
1048
|
log.info(`\xBB Generated ${notes.changes.length} change entries`);
|
|
954
|
-
const
|
|
1049
|
+
const selectedTemplateName = options.templateProfile || config.template?.default;
|
|
1050
|
+
const baseTemplate = mergeTemplates(
|
|
1051
|
+
toResolvedTemplate(config.template),
|
|
1052
|
+
getTemplateProfileByName(config, selectedTemplateName)
|
|
1053
|
+
);
|
|
1054
|
+
const format = options.format || baseTemplate.format || "markdown";
|
|
1055
|
+
const templatedNotes = applyTemplateToNotes(notes, baseTemplate);
|
|
1056
|
+
const formatted = formatNotes(templatedNotes, format);
|
|
955
1057
|
const publishedTo = [];
|
|
1058
|
+
const renderedCache = /* @__PURE__ */ new Map();
|
|
956
1059
|
if (!options.dryRun) {
|
|
957
1060
|
for (const target of config.publish) {
|
|
958
1061
|
try {
|
|
@@ -962,11 +1065,40 @@ async function runPipeline(from, to, config, options = {}) {
|
|
|
962
1065
|
}
|
|
963
1066
|
const publisherFactory = getPublisher(target.type);
|
|
964
1067
|
if (!publisherFactory) {
|
|
965
|
-
log.info(`\xBB Skipping ${target.type} \u2014 install @cullit/
|
|
1068
|
+
log.info(`\xBB Skipping ${target.type} \u2014 install @cullit/licensed to enable`);
|
|
966
1069
|
continue;
|
|
967
1070
|
}
|
|
968
1071
|
const publisher = publisherFactory(target);
|
|
969
|
-
|
|
1072
|
+
const targetTemplate = mergeTemplates(
|
|
1073
|
+
baseTemplate,
|
|
1074
|
+
getTemplateProfileByName(config, typeof target.templateProfile === "string" ? target.templateProfile : void 0),
|
|
1075
|
+
{
|
|
1076
|
+
format: typeof target.format === "string" ? target.format : void 0,
|
|
1077
|
+
sectionOrder: normalizeSectionOrder(target.sectionOrder)
|
|
1078
|
+
}
|
|
1079
|
+
);
|
|
1080
|
+
const targetFormat = targetTemplate.format || format;
|
|
1081
|
+
const cacheKey = JSON.stringify({
|
|
1082
|
+
f: targetFormat,
|
|
1083
|
+
o: targetTemplate.sectionOrder || null,
|
|
1084
|
+
c: targetTemplate.includeContributors,
|
|
1085
|
+
m: targetTemplate.includeMetadata,
|
|
1086
|
+
s: targetTemplate.summaryPrefix
|
|
1087
|
+
});
|
|
1088
|
+
let cached = renderedCache.get(cacheKey);
|
|
1089
|
+
if (!cached) {
|
|
1090
|
+
const targetNotes = applyTemplateToNotes(notes, targetTemplate);
|
|
1091
|
+
const targetOutput = cacheKey === JSON.stringify({
|
|
1092
|
+
f: format,
|
|
1093
|
+
o: baseTemplate.sectionOrder || null,
|
|
1094
|
+
c: baseTemplate.includeContributors,
|
|
1095
|
+
m: baseTemplate.includeMetadata,
|
|
1096
|
+
s: baseTemplate.summaryPrefix
|
|
1097
|
+
}) ? formatted : formatNotes(targetNotes, targetFormat);
|
|
1098
|
+
cached = { notes: targetNotes, output: targetOutput, format: targetFormat };
|
|
1099
|
+
renderedCache.set(cacheKey, cached);
|
|
1100
|
+
}
|
|
1101
|
+
await publisher.publish(cached.notes, cached.format, cached.output);
|
|
970
1102
|
publishedTo.push(target.type === "file" ? `file:${target.path}` : target.type);
|
|
971
1103
|
} catch (err) {
|
|
972
1104
|
log.error(`\u2717 Failed to publish to ${target.type}: ${err.message}`);
|
|
@@ -980,7 +1112,7 @@ async function runPipeline(from, to, config, options = {}) {
|
|
|
980
1112
|
const duration = Date.now() - startTime;
|
|
981
1113
|
log.info(`
|
|
982
1114
|
\u2713 Done in ${(duration / 1e3).toFixed(1)}s`);
|
|
983
|
-
return { notes, formatted, publishedTo, duration };
|
|
1115
|
+
return { notes: templatedNotes, formatted, publishedTo, duration };
|
|
984
1116
|
}
|
|
985
1117
|
export {
|
|
986
1118
|
AI_PROVIDERS,
|
|
@@ -1001,10 +1133,12 @@ export {
|
|
|
1001
1133
|
VERSION,
|
|
1002
1134
|
analyzeReleaseReadiness,
|
|
1003
1135
|
createLogger,
|
|
1136
|
+
escapeHtml,
|
|
1004
1137
|
fetchWithTimeout,
|
|
1005
1138
|
formatNotes,
|
|
1006
1139
|
getCollector,
|
|
1007
1140
|
getEnricher,
|
|
1141
|
+
getFeatureGating,
|
|
1008
1142
|
getFormatter,
|
|
1009
1143
|
getGenerator,
|
|
1010
1144
|
getLatestTag,
|
|
@@ -1016,6 +1150,7 @@ export {
|
|
|
1016
1150
|
hasGenerator,
|
|
1017
1151
|
hasPublisher,
|
|
1018
1152
|
isEnrichmentAllowed,
|
|
1153
|
+
isFeatureAllowed,
|
|
1019
1154
|
isProviderAllowed,
|
|
1020
1155
|
isPublisherAllowed,
|
|
1021
1156
|
listCollectors,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cullit/core",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.10.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Core engine for Cullit — AI-powered release note generation.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -16,6 +16,10 @@
|
|
|
16
16
|
"ai",
|
|
17
17
|
"automation"
|
|
18
18
|
],
|
|
19
|
+
"homepage": "https://cullit.io",
|
|
20
|
+
"bugs": {
|
|
21
|
+
"url": "https://github.com/mttaylor/cullit/issues"
|
|
22
|
+
},
|
|
19
23
|
"main": "./dist/index.js",
|
|
20
24
|
"types": "./dist/index.d.ts",
|
|
21
25
|
"exports": {
|
|
@@ -28,10 +32,13 @@
|
|
|
28
32
|
"dist"
|
|
29
33
|
],
|
|
30
34
|
"engines": {
|
|
31
|
-
"node": ">=
|
|
35
|
+
"node": ">=22"
|
|
36
|
+
},
|
|
37
|
+
"publishConfig": {
|
|
38
|
+
"access": "public"
|
|
32
39
|
},
|
|
33
40
|
"dependencies": {
|
|
34
|
-
"@cullit/config": "1.
|
|
41
|
+
"@cullit/config": "1.10.0"
|
|
35
42
|
},
|
|
36
43
|
"scripts": {
|
|
37
44
|
"build": "tsup src/index.ts --format esm --dts --clean",
|