cullit 0.1.0 → 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/index.js +229 -44
- package/package.json +6 -3
package/dist/index.js
CHANGED
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
-
import { runPipeline, VERSION } from "@cullit/core";
|
|
4
|
+
import { runPipeline, VERSION, createLogger, analyzeReleaseReadiness, resolveLicense } from "@cullit/core";
|
|
5
5
|
import { loadConfig } from "@cullit/config";
|
|
6
6
|
import { getRecentTags } from "@cullit/core";
|
|
7
7
|
import { writeFileSync, readFileSync, existsSync } from "fs";
|
|
8
8
|
import { resolve } from "path";
|
|
9
|
+
import { createInterface } from "readline";
|
|
10
|
+
try {
|
|
11
|
+
await import("@cullit/pro");
|
|
12
|
+
} catch {
|
|
13
|
+
}
|
|
9
14
|
function loadEnv() {
|
|
10
15
|
const envPath = resolve(process.cwd(), ".env");
|
|
11
16
|
if (!existsSync(envPath)) return;
|
|
@@ -34,6 +39,7 @@ var HELP = `
|
|
|
34
39
|
|
|
35
40
|
COMMANDS
|
|
36
41
|
generate Generate release notes from git, Jira, or Linear
|
|
42
|
+
status Release readiness check \u2014 should you release?
|
|
37
43
|
init Create a .cullit.yml config file
|
|
38
44
|
tags List recent tags in the current repo
|
|
39
45
|
|
|
@@ -46,6 +52,8 @@ var HELP = `
|
|
|
46
52
|
--provider Override AI provider (anthropic, openai, gemini, ollama, openclaw, none)
|
|
47
53
|
--source Override source type (local, jira, linear)
|
|
48
54
|
--audience Override audience (developer, end-user, executive)
|
|
55
|
+
--verbose Show detailed output
|
|
56
|
+
--quiet Suppress all output except errors
|
|
49
57
|
|
|
50
58
|
EXAMPLES
|
|
51
59
|
$ cullit generate --from v1.0.0 --to v1.1.0
|
|
@@ -56,40 +64,6 @@ var HELP = `
|
|
|
56
64
|
$ cullit generate --source linear --from "team:ENG" --provider openai
|
|
57
65
|
$ cullit init
|
|
58
66
|
`;
|
|
59
|
-
var DEFAULT_YML = `# Cullit Configuration
|
|
60
|
-
# https://cullit.io/docs/config
|
|
61
|
-
|
|
62
|
-
ai:
|
|
63
|
-
provider: anthropic # anthropic | openai | gemini | ollama | openclaw | none
|
|
64
|
-
# model: claude-sonnet-4-20250514 # optional: override default model
|
|
65
|
-
audience: developer # developer | end-user | executive
|
|
66
|
-
tone: professional # professional | casual | terse
|
|
67
|
-
categories: [features, fixes, breaking, improvements, chores]
|
|
68
|
-
|
|
69
|
-
source:
|
|
70
|
-
type: local # local | jira | linear
|
|
71
|
-
# enrichment: [jira, linear] # uncomment to enable enrichment
|
|
72
|
-
|
|
73
|
-
publish:
|
|
74
|
-
- type: stdout # always output to terminal
|
|
75
|
-
# - type: file
|
|
76
|
-
# path: RELEASE_NOTES.md
|
|
77
|
-
# - type: slack
|
|
78
|
-
# webhook_url: $SLACK_WEBHOOK_URL
|
|
79
|
-
# - type: discord
|
|
80
|
-
# webhook_url: $DISCORD_WEBHOOK_URL
|
|
81
|
-
|
|
82
|
-
# jira:
|
|
83
|
-
# domain: yourcompany.atlassian.net
|
|
84
|
-
# # Set JIRA_EMAIL and JIRA_API_TOKEN in your environment
|
|
85
|
-
|
|
86
|
-
# linear:
|
|
87
|
-
# # Set LINEAR_API_KEY in your environment
|
|
88
|
-
|
|
89
|
-
# openclaw:
|
|
90
|
-
# baseUrl: http://localhost:18789 # OpenClaw gateway URL
|
|
91
|
-
# # Set OPENCLAW_TOKEN in your environment
|
|
92
|
-
`;
|
|
93
67
|
async function main() {
|
|
94
68
|
const args = process.argv.slice(2);
|
|
95
69
|
const command = args[0];
|
|
@@ -98,13 +72,19 @@ async function main() {
|
|
|
98
72
|
process.exit(0);
|
|
99
73
|
}
|
|
100
74
|
if (command === "--version" || command === "-v") {
|
|
101
|
-
console.log(`
|
|
75
|
+
console.log(`cullit v${VERSION}`);
|
|
102
76
|
process.exit(0);
|
|
103
77
|
}
|
|
104
78
|
if (command === "init") {
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
79
|
+
if (existsSync(".cullit.yml")) {
|
|
80
|
+
console.log("\u26A0 .cullit.yml already exists. Delete it first to re-initialize.");
|
|
81
|
+
process.exit(1);
|
|
82
|
+
}
|
|
83
|
+
await interactiveInit();
|
|
84
|
+
process.exit(0);
|
|
85
|
+
}
|
|
86
|
+
if (command === "status") {
|
|
87
|
+
printReleaseStatus();
|
|
108
88
|
process.exit(0);
|
|
109
89
|
}
|
|
110
90
|
if (command === "tags") {
|
|
@@ -141,21 +121,226 @@ async function main() {
|
|
|
141
121
|
process.exit(1);
|
|
142
122
|
}
|
|
143
123
|
async function runGenerate(from, to, opts) {
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
124
|
+
let config;
|
|
125
|
+
try {
|
|
126
|
+
config = loadConfig(opts.config || opts.c || process.cwd());
|
|
127
|
+
} catch (err) {
|
|
128
|
+
console.error(`
|
|
129
|
+
\u2717 Config error: ${err.message}`);
|
|
130
|
+
console.error(" Fix your .cullit.yml or delete it to use defaults.");
|
|
131
|
+
process.exitCode = 1;
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
const VALID_PROVIDERS = ["anthropic", "openai", "gemini", "ollama", "openclaw", "none"];
|
|
135
|
+
const VALID_AUDIENCES = ["developer", "end-user", "executive"];
|
|
136
|
+
const VALID_SOURCES = ["local", "jira", "linear"];
|
|
137
|
+
if (opts.provider) {
|
|
138
|
+
if (!VALID_PROVIDERS.includes(opts.provider)) {
|
|
139
|
+
console.error(`
|
|
140
|
+
\u2717 Invalid provider: ${opts.provider}`);
|
|
141
|
+
console.error(` Valid providers: ${VALID_PROVIDERS.join(", ")}`);
|
|
142
|
+
process.exitCode = 1;
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
config.ai.provider = opts.provider;
|
|
146
|
+
}
|
|
147
|
+
if (opts.audience) {
|
|
148
|
+
if (!VALID_AUDIENCES.includes(opts.audience)) {
|
|
149
|
+
console.error(`
|
|
150
|
+
\u2717 Invalid audience: ${opts.audience}`);
|
|
151
|
+
console.error(` Valid audiences: ${VALID_AUDIENCES.join(", ")}`);
|
|
152
|
+
process.exitCode = 1;
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
config.ai.audience = opts.audience;
|
|
156
|
+
}
|
|
147
157
|
if (opts.model) config.ai.model = opts.model;
|
|
148
|
-
if (opts.source)
|
|
158
|
+
if (opts.source) {
|
|
159
|
+
if (!VALID_SOURCES.includes(opts.source)) {
|
|
160
|
+
console.error(`
|
|
161
|
+
\u2717 Invalid source: ${opts.source}`);
|
|
162
|
+
console.error(` Valid sources: ${VALID_SOURCES.join(", ")}`);
|
|
163
|
+
process.exitCode = 1;
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
config.source.type = opts.source;
|
|
167
|
+
}
|
|
149
168
|
const format = opts.format || "markdown";
|
|
150
169
|
const dryRun = "dry-run" in opts || "dryRun" in opts;
|
|
170
|
+
const logLevel = "verbose" in opts ? "verbose" : "quiet" in opts ? "quiet" : "normal";
|
|
171
|
+
const logger = createLogger(logLevel);
|
|
172
|
+
const license = resolveLicense();
|
|
173
|
+
if (logLevel !== "quiet") {
|
|
174
|
+
const tierLabel = license.tier === "pro" ? "\u{1F511} Pro" : "\u{1F193} Free";
|
|
175
|
+
logger.info(`\xBB License: ${tierLabel}`);
|
|
176
|
+
}
|
|
151
177
|
try {
|
|
152
|
-
const result = await runPipeline(from, to, config, { format, dryRun });
|
|
178
|
+
const result = await runPipeline(from, to, config, { format, dryRun, logger });
|
|
179
|
+
if (logLevel !== "quiet") {
|
|
180
|
+
try {
|
|
181
|
+
const advisory = analyzeReleaseReadiness();
|
|
182
|
+
if (advisory.shouldRelease && advisory.nextVersion) {
|
|
183
|
+
console.log(`
|
|
184
|
+
\u{1F4A1} Release advisory: ${advisory.reasons[0]}`);
|
|
185
|
+
console.log(` Suggested: ${advisory.nextVersion} (${advisory.suggestedBump}) \u2014 ${advisory.commitCount} unreleased commit(s)`);
|
|
186
|
+
console.log(` Run "cullit status" for full breakdown.
|
|
187
|
+
`);
|
|
188
|
+
} else if (advisory.commitCount > 0 && advisory.daysSinceRelease !== null && advisory.daysSinceRelease > 7) {
|
|
189
|
+
console.log(`
|
|
190
|
+
\u{1F4A1} ${advisory.commitCount} unreleased commit(s), ${advisory.daysSinceRelease} days since last release. Run "cullit status" to check readiness.
|
|
191
|
+
`);
|
|
192
|
+
}
|
|
193
|
+
} catch {
|
|
194
|
+
}
|
|
195
|
+
}
|
|
153
196
|
} catch (err) {
|
|
154
197
|
console.error(`
|
|
155
198
|
\u2717 Error: ${err.message}`);
|
|
156
199
|
process.exitCode = 1;
|
|
157
200
|
}
|
|
158
201
|
}
|
|
202
|
+
function printReleaseStatus() {
|
|
203
|
+
const advisory = analyzeReleaseReadiness();
|
|
204
|
+
const bar = (count, max, char = "\u2588") => {
|
|
205
|
+
const width = Math.min(Math.round(count / Math.max(max, 1) * 20), 20);
|
|
206
|
+
return char.repeat(width) || "\xB7";
|
|
207
|
+
};
|
|
208
|
+
console.log(`
|
|
209
|
+
\u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557
|
|
210
|
+
\u2551 Cullit \u2014 Release Readiness \u2551
|
|
211
|
+
\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D
|
|
212
|
+
`);
|
|
213
|
+
if (!advisory.currentVersion) {
|
|
214
|
+
console.log(" No tags found. Create your first release with:");
|
|
215
|
+
console.log(" git tag v0.1.0 && git push --tags\n");
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
const { breakdown, commitCount } = advisory;
|
|
219
|
+
const verdict = advisory.shouldRelease ? "\u{1F7E2} Yes \u2014 time to release" : "\u{1F7E1} Not yet \u2014 keep going";
|
|
220
|
+
console.log(` Current version: ${advisory.currentVersion}`);
|
|
221
|
+
if (advisory.nextVersion) {
|
|
222
|
+
console.log(` Suggested next: ${advisory.nextVersion} (${advisory.suggestedBump})`);
|
|
223
|
+
}
|
|
224
|
+
if (advisory.daysSinceRelease !== null) {
|
|
225
|
+
console.log(` Last release: ${advisory.daysSinceRelease} day(s) ago`);
|
|
226
|
+
}
|
|
227
|
+
console.log(` Unreleased commits: ${commitCount}`);
|
|
228
|
+
console.log(` Contributors: ${advisory.contributorCount}`);
|
|
229
|
+
if (commitCount > 0) {
|
|
230
|
+
console.log("\n Commit breakdown:");
|
|
231
|
+
const maxCount = Math.max(breakdown.features, breakdown.fixes, breakdown.breaking, breakdown.chores, breakdown.other, 1);
|
|
232
|
+
if (breakdown.features > 0) console.log(` \u2728 Features: ${bar(breakdown.features, maxCount)} ${breakdown.features}`);
|
|
233
|
+
if (breakdown.fixes > 0) console.log(` \u{1F41B} Fixes: ${bar(breakdown.fixes, maxCount)} ${breakdown.fixes}`);
|
|
234
|
+
if (breakdown.breaking > 0) console.log(` \u26A0\uFE0F Breaking: ${bar(breakdown.breaking, maxCount)} ${breakdown.breaking}`);
|
|
235
|
+
if (breakdown.chores > 0) console.log(` \u{1F9F9} Chores: ${bar(breakdown.chores, maxCount)} ${breakdown.chores}`);
|
|
236
|
+
if (breakdown.other > 0) console.log(` \u{1F4DD} Other: ${bar(breakdown.other, maxCount)} ${breakdown.other}`);
|
|
237
|
+
}
|
|
238
|
+
console.log(`
|
|
239
|
+
Should you release? ${verdict}`);
|
|
240
|
+
if (advisory.reasons.length > 0) {
|
|
241
|
+
console.log("\n Why:");
|
|
242
|
+
for (const reason of advisory.reasons) {
|
|
243
|
+
console.log(` \u2192 ${reason}`);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
if (advisory.shouldRelease && advisory.nextVersion) {
|
|
247
|
+
console.log(`
|
|
248
|
+
Quick release:`);
|
|
249
|
+
console.log(` cullit generate --from ${advisory.currentVersion} --to HEAD`);
|
|
250
|
+
console.log(` git tag ${advisory.nextVersion} && git push --tags`);
|
|
251
|
+
}
|
|
252
|
+
console.log("");
|
|
253
|
+
}
|
|
254
|
+
function ask(rl, question) {
|
|
255
|
+
return new Promise((resolve2) => rl.question(question, resolve2));
|
|
256
|
+
}
|
|
257
|
+
async function interactiveInit() {
|
|
258
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
259
|
+
const VALID_PROVIDERS = ["anthropic", "openai", "gemini", "ollama", "openclaw", "none"];
|
|
260
|
+
const VALID_SOURCES = ["local", "jira", "linear"];
|
|
261
|
+
const VALID_AUDIENCES = ["developer", "end-user", "executive"];
|
|
262
|
+
const VALID_TONES = ["professional", "casual", "terse"];
|
|
263
|
+
const VALID_ENRICHMENTS = ["jira", "linear", "both", "none"];
|
|
264
|
+
console.log("\n Cullit \u2014 Project Setup\n");
|
|
265
|
+
const provider = await ask(rl, " AI provider (anthropic/openai/gemini/ollama/openclaw/none) [anthropic]: ") || "anthropic";
|
|
266
|
+
if (!VALID_PROVIDERS.includes(provider)) {
|
|
267
|
+
console.error(`
|
|
268
|
+
\u2717 Invalid provider: ${provider}. Must be one of: ${VALID_PROVIDERS.join(", ")}`);
|
|
269
|
+
rl.close();
|
|
270
|
+
process.exit(1);
|
|
271
|
+
}
|
|
272
|
+
const source = await ask(rl, " Source type (local/jira/linear) [local]: ") || "local";
|
|
273
|
+
if (!VALID_SOURCES.includes(source)) {
|
|
274
|
+
console.error(`
|
|
275
|
+
\u2717 Invalid source: ${source}. Must be one of: ${VALID_SOURCES.join(", ")}`);
|
|
276
|
+
rl.close();
|
|
277
|
+
process.exit(1);
|
|
278
|
+
}
|
|
279
|
+
const audience = await ask(rl, " Audience (developer/end-user/executive) [developer]: ") || "developer";
|
|
280
|
+
if (!VALID_AUDIENCES.includes(audience)) {
|
|
281
|
+
console.error(`
|
|
282
|
+
\u2717 Invalid audience: ${audience}. Must be one of: ${VALID_AUDIENCES.join(", ")}`);
|
|
283
|
+
rl.close();
|
|
284
|
+
process.exit(1);
|
|
285
|
+
}
|
|
286
|
+
const tone = await ask(rl, " Tone (professional/casual/terse) [professional]: ") || "professional";
|
|
287
|
+
if (!VALID_TONES.includes(tone)) {
|
|
288
|
+
console.error(`
|
|
289
|
+
\u2717 Invalid tone: ${tone}. Must be one of: ${VALID_TONES.join(", ")}`);
|
|
290
|
+
rl.close();
|
|
291
|
+
process.exit(1);
|
|
292
|
+
}
|
|
293
|
+
let enrichment = "";
|
|
294
|
+
if (source === "local") {
|
|
295
|
+
enrichment = await ask(rl, " Enrich from (jira/linear/both/none) [none]: ") || "none";
|
|
296
|
+
if (!VALID_ENRICHMENTS.includes(enrichment)) {
|
|
297
|
+
console.error(`
|
|
298
|
+
\u2717 Invalid enrichment: ${enrichment}. Must be one of: ${VALID_ENRICHMENTS.join(", ")}`);
|
|
299
|
+
rl.close();
|
|
300
|
+
process.exit(1);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
rl.close();
|
|
304
|
+
const enrichmentLine = enrichment === "both" ? "\n enrichment: [jira, linear]" : enrichment === "jira" || enrichment === "linear" ? `
|
|
305
|
+
enrichment: [${enrichment}]` : "";
|
|
306
|
+
const sections = [];
|
|
307
|
+
if (enrichment === "jira" || enrichment === "both" || source === "jira") {
|
|
308
|
+
sections.push(`
|
|
309
|
+
jira:
|
|
310
|
+
domain: yourcompany.atlassian.net
|
|
311
|
+
# Set JIRA_EMAIL and JIRA_API_TOKEN in your environment`);
|
|
312
|
+
}
|
|
313
|
+
if (enrichment === "linear" || enrichment === "both" || source === "linear") {
|
|
314
|
+
sections.push(`
|
|
315
|
+
linear:
|
|
316
|
+
# Set LINEAR_API_KEY in your environment`);
|
|
317
|
+
}
|
|
318
|
+
const yml = `# Cullit Configuration
|
|
319
|
+
# https://cullit.io
|
|
320
|
+
|
|
321
|
+
ai:
|
|
322
|
+
provider: ${provider}
|
|
323
|
+
audience: ${audience}
|
|
324
|
+
tone: ${tone}
|
|
325
|
+
categories: [features, fixes, breaking, improvements, chores]
|
|
326
|
+
|
|
327
|
+
source:
|
|
328
|
+
type: ${source}${enrichmentLine}
|
|
329
|
+
|
|
330
|
+
publish:
|
|
331
|
+
- type: stdout
|
|
332
|
+
# - type: file
|
|
333
|
+
# path: RELEASE_NOTES.md
|
|
334
|
+
# - type: slack
|
|
335
|
+
# webhook_url: $SLACK_WEBHOOK_URL
|
|
336
|
+
# - type: discord
|
|
337
|
+
# webhook_url: $DISCORD_WEBHOOK_URL
|
|
338
|
+
${sections.join("\n")}
|
|
339
|
+
`;
|
|
340
|
+
writeFileSync(".cullit.yml", yml, "utf-8");
|
|
341
|
+
console.log("\n \u2713 Created .cullit.yml");
|
|
342
|
+
console.log(' Run "cullit generate --from <tag>" to generate release notes.\n');
|
|
343
|
+
}
|
|
159
344
|
function parseArgs(args) {
|
|
160
345
|
const result = {};
|
|
161
346
|
for (let i = 0; i < args.length; i++) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cullit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Cull the noise from your releases. AI-powered release notes from the CLI.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -25,9 +25,12 @@
|
|
|
25
25
|
"files": [
|
|
26
26
|
"dist"
|
|
27
27
|
],
|
|
28
|
+
"engines": {
|
|
29
|
+
"node": ">=18"
|
|
30
|
+
},
|
|
28
31
|
"dependencies": {
|
|
29
|
-
"@cullit/
|
|
30
|
-
"@cullit/
|
|
32
|
+
"@cullit/config": "0.4.0",
|
|
33
|
+
"@cullit/core": "0.4.0"
|
|
31
34
|
},
|
|
32
35
|
"scripts": {
|
|
33
36
|
"build": "tsup src/index.ts --format esm --clean",
|