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.
Files changed (2) hide show
  1. package/dist/index.js +229 -44
  2. 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(`cull v${VERSION}`);
75
+ console.log(`cullit v${VERSION}`);
102
76
  process.exit(0);
103
77
  }
104
78
  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.");
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
- 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;
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) config.source.type = 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.1.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/core": "0.1.0",
30
- "@cullit/config": "0.1.0"
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",