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