Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
127 changes: 55 additions & 72 deletions .github/workflows/changeset-bot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -79,78 +79,61 @@ jobs:
id: generate-changeset
uses: actions/github-script@v7
with:
script: |
const prTitle = `${{ steps.pr-info.outputs.pr_title }}`;
const prBody = `${{ steps.pr-info.outputs.pr_body }}`;
const commits = `${{ steps.pr-info.outputs.commits }}`;

// Get PR labels for manual override
const pr = context.payload.pull_request;
const labels = pr.labels.map(label => label.name.toLowerCase());

let versionType = 'patch'; // Conservative default
let description = prTitle;

// Manual override via labels (highest priority)
if (labels.includes('major') || labels.includes('breaking')) {
versionType = 'major';
} else if (labels.includes('minor') || labels.includes('feature')) {
versionType = 'minor';
} else if (labels.includes('patch') || labels.includes('bugfix')) {
versionType = 'patch';
} else {
// Manual override via PR title syntax: [major], [minor], [patch]
const titleMatch = prTitle.match(/^\[(major|minor|patch)\]/i);
if (titleMatch) {
versionType = titleMatch[1].toLowerCase();
description = prTitle.replace(/^\[(major|minor|patch)\]\s*/i, '');
} else {
// Automatic detection (much more conservative)
const combined = (prTitle + ' ' + prBody + ' ' + commits).toLowerCase();

// Major: Only with very explicit breaking change indicators
if ((combined.includes('breaking change') ||
combined.includes('breaking:') ||
combined.includes('major:') ||
combined.includes('!breaking') ||
(combined.includes('remove') && combined.includes('api')) ||
(combined.includes('delete') && combined.includes('command')))) {
versionType = 'major';
}
// Minor: New features, commands, enhancements
else if (combined.includes('feat:') ||
combined.includes('feature:') ||
combined.includes('add command') ||
combined.includes('new command') ||
combined.includes('add feature') ||
combined.includes('new feature') ||
combined.includes('minor:') ||
combined.includes('enhance') ||
(combined.includes('add') && (combined.includes('option') || combined.includes('flag')))) {
versionType = 'minor';
}
// Everything else defaults to patch
}
}

// Clean up description
description = description
.replace(/^(feat|fix|docs|style|refactor|test|chore|perf)(\([^)]+\))?: /i, '')
.replace(/^(add|fix|update|remove|delete|improve|enhance): /i, '')
.replace(/^\[(major|minor|patch)\]\s*/i, '')
.trim();

if (!description || description.length < 5) {
description = prTitle;
}

description = description.charAt(0).toUpperCase() + description.slice(1);

core.setOutput('version_type', versionType);
core.setOutput('description', description);

console.log(`Generated changeset: ${versionType} - ${description}`);
console.log(`Detection method: ${labels.length > 0 ? 'labels' : titleMatch ? 'title syntax' : 'automatic'}`);
script: |
const prTitle = `${{ steps.pr-info.outputs.pr_title }}`;
const commits = `${{ steps.pr-info.outputs.commits }}`;

let versionType = 'patch';
let description = prTitle;

const combined = (prTitle + ' ' + commits).toLowerCase();

if (combined.includes('breaking') ||
combined.includes('major') ||
combined.includes('remove') && !combined.includes('bug') ||
combined.includes('delete') && !combined.includes('test') ||
combined.includes('incompatible')) {
versionType = 'major';
}
else if (combined.includes('feat') ||
combined.includes('feature') ||
combined.includes('add') ||
combined.includes('new') ||
combined.includes('minor') ||
combined.includes('enhance') ||
combined.includes('improve') && !combined.includes('fix') ||
combined.includes('command') ||
combined.includes('option')) {
versionType = 'minor';
}
// Patch version indicators (default)
else if (combined.includes('fix') ||
combined.includes('bug') ||
combined.includes('patch') ||
combined.includes('update') ||
combined.includes('refactor') ||
combined.includes('perf') ||
combined.includes('docs') ||
combined.includes('style') ||
combined.includes('test')) {
versionType = 'patch';
}

description = prTitle
.replace(/^(feat|fix|docs|style|refactor|test|chore|perf)(\([^)]+\))?: /i, '')
.replace(/^(add|fix|update|remove|delete|improve|enhance): /i, '')
.trim();

if (!description || description.length < 5) {
description = prTitle;
}

description = description.charAt(0).toUpperCase() + description.slice(1);

core.setOutput('version_type', versionType);
core.setOutput('description', description);

console.log(`Generated changeset: ${versionType} - ${description}`);

- name: Create changeset file
if: steps.pr-info.outputs.skip != 'true'
Expand Down
25 changes: 0 additions & 25 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -294,31 +294,6 @@ src/

This is currently a personal learning project, but feedback and suggestions are welcome!

How to control version bumps:

### Method 1: Use GitHub Labels
Add these labels to your repository and apply them to PRs:
- `major` or `breaking` → Major version bump
- `minor` or `feature` → Minor version bump
- `patch` or `bugfix` → Patch version bump

### Method 2: Use PR Title Syntax
Start your PR title with the version type in brackets:
- `[major] Remove deprecated API endpoints`
- `[minor] Add new search command`
- `[patch] Fix memory leak in dump command`

### Method 3: Automatic Detection (Conservative)
The system will now only auto-detect major bumps with very explicit indicators like:
- "breaking change"
- "breaking:"
- "major:"
- "!breaking"
- "remove api"
- "delete command"

**Everything else defaults to patch unless you have clear feature indicators for minor.**

## License

MIT
Expand Down
55 changes: 5 additions & 50 deletions src/commands/dump.command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,50 +2,10 @@ import { randomUUID } from "crypto";
import type { BrainDump, FluxConfig } from "../types";
import { FLUX_BRAIN_DUMP_PATH, FLUX_CONFIG_PATH } from "../utils/constants";
import { createBrainDumpFileIfNotExists, getConfigFile, getCurrentBranch, getFluxPath, getGitUncommittedChanges, getMonthString, getWorkingDir } from "../utils/";
import { editor } from '@inquirer/prompts';


export async function handleBrainDump(message: string[], options: { multiline?: boolean }) {
try {
let finalMessage: string;

if (options.multiline) {
console.log('Opening editor for multiline input...');
const initialText = message ? message.join(' ') : '';

const multilineInput = await editor({
message: 'Enter your brain dump (save & exit when done):',
default: initialText,
waitForUserInput: false
});

if (!multilineInput.trim()) {
console.log('Brain dump cancelled - no content provided');
return;
}

finalMessage = multilineInput.trim();
} else {
if (!message || message.length === 0) {
console.log('Please provide a message: flux dump "your message"');
return;
}
finalMessage = message.join(' ');
}

await brainDumpAddCommand(finalMessage, { multiline: options.multiline });

} catch (error) {
console.error('Error creating brain dump:', error instanceof Error ? error.message : 'Unknown error');
process.exit(1);
}
}


export async function brainDumpAddCommand(message: string, options: { multiline?: boolean } = {}) {
export async function brainDumpAddCommand(message: string[]) {
const fluxPath = await getFluxPath()
const fs = await import("fs");

console.log("Creating brain dump...");

const monthString = getMonthString();
Expand All @@ -59,25 +19,20 @@ export async function brainDumpAddCommand(message: string, options: { multiline?
const newDump: BrainDump = {
id: randomUUID(),
timestamp: new Date().toISOString(),
message: message,
message: message.join(' '),
workingDir,
branch,
hasUncommittedChanges
};


const data: { dumps: BrainDump[] } = JSON.parse(fs.readFileSync(`${fluxPath}${FLUX_BRAIN_DUMP_PATH}/${monthString}.json`, 'utf8'));

config.sorted ? data.dumps.unshift(newDump) : data.dumps.push(newDump);

fs.writeFileSync(`${fluxPath}${FLUX_BRAIN_DUMP_PATH}/${monthString}.json`, JSON.stringify(data, null, 2));

const displayMessage = message.length > 50
? message.substring(0, 47) + "..."
: message;

const preview = message.includes('\n')
? `${message.split('\n')[0]}... (multiline)`
: displayMessage;
// After writeFileSync, add:
console.log(`✅ Brain dump saved: "${message.join(' ')}"`);

console.log(`✅ Brain dump saved: "${preview}"`);
}
72 changes: 39 additions & 33 deletions src/commands/search.command.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import Fuse from "fuse.js"
import { displaySearchResults, FLUX_BRAIN_DUMP_PATH, getAllBrainDumpFilePaths, getConfigFile, getFluxPath, getMonthString, searchResultFormat } from "../utils";
import { FLUX_BRAIN_DUMP_PATH, getAllBrainDumpFilePaths, getConfigFile, getFluxPath, getMonthString, searchResultFormat } from "../utils";
import { createFuseInstance } from "../utils/fuse.instance";
import type { BrainDump } from "../types";
import fs from "fs";
Expand All @@ -8,46 +8,52 @@ export async function searchBrainDumpCommand(query: string[]) {
console.log("Searching all brain dumps...");
const fluxPath = await getFluxPath()
const config = await getConfigFile(fluxPath);
const searchQuery = query.join(' ').trim();
const monthString = getMonthString();

let searchResults: Array<{ item: BrainDump, score?: number }> = [];
let searchResults = []
const allFilePaths = await getAllBrainDumpFilePaths(fluxPath);

for await (const filePath of allFilePaths) {
const fileData: { dumps: BrainDump[] } = JSON.parse(fs.readFileSync(filePath, 'utf8'));

if (searchQuery) {
const fuse = createFuseInstance(fileData.dumps, config);
const results = fuse.search(searchQuery);
searchResults.push(...results);
} else {
const recentDumps = fileData.dumps
.filter(dump => dump && dump.message && dump.message.trim() !== '')
.map(dump => ({
item: dump,
score: 0,
timestamp: new Date(dump.timestamp).getTime()
}));

searchResults.push(...recentDumps);
const fuse = createFuseInstance(fileData.dumps, config);
const results = fuse.search(query.join(' '));
searchResults.push(...results);
if (searchResults.length > 30) {
break;
}
}
if (searchQuery) {
searchResults.sort((a, b) => (a.score || 0) - (b.score || 0));
} else {
searchResults.sort((a, b) => {
const timeA = new Date(a.item.timestamp).getTime();
const timeB = new Date(b.item.timestamp).getTime();
return timeB - timeA;
});
}

const resultLimit = config?.search?.resultLimit || (searchQuery ? 10 : 5);
const limitedResults = searchResults.slice(0, resultLimit);
if (query.length > 0) {
if (searchResults.length === 0) {
console.log("No brain dumps found matching the query.");
return;
}

if (searchResults.length > limitedResults.length) {
console.log(`\n(Showing ${limitedResults.length} of ${searchResults.length} results)`);
const resultLimit = config?.search?.resultLimit || 10;
const limitedResults = searchResults.slice(0, resultLimit);
console.log(`Found ${searchResults.length} brain dumps matching the query${searchResults.length > resultLimit ? ` (showing first ${resultLimit})` : ''}:`);
limitedResults.forEach((result, index) => {
const dump = result.item;
console.log(searchResultFormat({ index: index, timestamp: dump.timestamp, message: dump.message, score: result.score?.toFixed(2) }))
});
} else {
const resultLimit = config?.search?.resultLimit || 3;
let totalCount = 0;

for await (const filePath of allFilePaths) {
if (totalCount >= resultLimit) {
break;
}

const fileData: { dumps: BrainDump[] } = JSON.parse(fs.readFileSync(filePath, 'utf8'));
for (let i = 0; i < fileData.dumps.length && totalCount < resultLimit; i++) {
const dump = fileData.dumps[i];
if (!dump || !dump.message || dump.message.trim() === '') {
continue;
}
totalCount += 1;
console.log(searchResultFormat({ index: totalCount, timestamp: dump.timestamp, message: dump.message, score: '0.00' }))
}
}
}

displaySearchResults(limitedResults, searchQuery || undefined);
}
15 changes: 7 additions & 8 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
#!/usr/bin/env node
import { Command } from "commander";
import { initFluxCommand, resetFluxCommand } from "./commands/init.command";
import { brainDumpAddCommand, handleBrainDump } from "./commands/dump.command";
import { brainDumpAddCommand } from "./commands/dump.command";
import { searchBrainDumpCommand } from "./commands/search.command";
import { configCommand } from "./commands/config.command";
import { helpOption } from "./commands/flux.option";
import packageJson from "../package.json"
import { getFluxPath } from "./utils";

const program = new Command()

program.name(`flux`).description('Git-aware CLI context manager for ADHD developers').version(packageJson.version);
Expand All @@ -20,12 +19,9 @@ program.command('reset')
.description('Resets flux in the current repository')
.action(resetFluxCommand)

program.command('dump [message...]')
.option('-m, --multiline', 'Enable multiline input mode')
.description('Add a brain dump with a message. Use --multiline for multi-line input.')
.action(async (message, options) => {
await handleBrainDump(message, options);
})
program.command('dump <message...>')
.description('Add a brain dump with a message. You can also include tags by using #tag in the message.')
.action(brainDumpAddCommand)

program.command('search [query...]')
.description('Search brain dumps with a query. If no query is provided, lists all brain dumps for the current month.')
Expand All @@ -37,5 +33,8 @@ program.command('config <fields...>')
.description('Update configuration fields. Example: flux config search.limit 10')
.action(configCommand)

program.command("test").action(async () => {
console.log(`Path is - ${await getFluxPath()}`)
})

program.parse(process.argv);
Loading