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.
Files changed (2) hide show
  1. package/dist/index.js +190 -44
  2. 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(`cull v${VERSION}`);
71
+ console.log(`cullit v${VERSION}`);
102
72
  process.exit(0);
103
73
  }
104
74
  if (command === "init") {
105
- writeFileSync(".cullit.yml", DEFAULT_YML, "utf-8");
106
- console.log("\u2713 Created .cullit.yml");
107
- console.log(" Edit it to configure your AI provider, integrations, and publish targets.");
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
- const config = loadConfig(opts.config || opts.c || process.cwd());
145
- if (opts.provider) config.ai.provider = opts.provider;
146
- if (opts.audience) config.ai.audience = opts.audience;
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) config.source.type = 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.1.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.1.0",
30
- "@cullit/config": "0.1.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",