diff --git a/.github/workflows/changeset-bot.yml b/.github/workflows/changeset-bot.yml index 9a74aee..f7975f8 100644 --- a/.github/workflows/changeset-bot.yml +++ b/.github/workflows/changeset-bot.yml @@ -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' diff --git a/README.md b/README.md index c278b55..8b9064f 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/src/commands/dump.command.ts b/src/commands/dump.command.ts index 1a5fa1e..9b9cfb0 100644 --- a/src/commands/dump.command.ts +++ b/src/commands/dump.command.ts @@ -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(); @@ -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}"`); } diff --git a/src/commands/search.command.ts b/src/commands/search.command.ts index 0bec7e1..8ed21bf 100644 --- a/src/commands/search.command.ts +++ b/src/commands/search.command.ts @@ -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"; @@ -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); } diff --git a/src/index.ts b/src/index.ts index 185fa62..450bd0e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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); @@ -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 ') + .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.') @@ -37,5 +33,8 @@ program.command('config ') .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); diff --git a/src/utils/helper.ts b/src/utils/helper.ts index 86761d8..2b052ed 100644 --- a/src/utils/helper.ts +++ b/src/utils/helper.ts @@ -1,6 +1,3 @@ -import { brainDumpAddCommand } from "../commands/dump.command"; -import type { BrainDump } from "../types"; - export function getMonthString(): string { const currentDate = new Date(); const year = currentDate.getFullYear(); @@ -8,119 +5,7 @@ export function getMonthString(): string { return `${year}-${month}`; } - export function searchResultFormat({ message, timestamp, score, index }: { message: string, timestamp: string, score?: string, index?: number }): string { const formattedTimestamp = new Date(timestamp).toLocaleString(); - const indexStr = index !== undefined ? `${index + 1}.` : ''; - const scoreStr = `[${score || '0.00'}]`; - const timeStr = `[${formattedTimestamp}]`; - - const formattedMessage = formatMessageForDisplay(message); - - return `${indexStr} ${scoreStr} ${timeStr} ${formattedMessage}`; -} - -function formatMessageForDisplay(message: string): string { - const lines = message.split('\n').map(line => line.trim()).filter(line => line.length > 0); - - if (lines.length <= 1) { - const singleLine = lines[0] || message; - return singleLine.length > 80 ? singleLine.substring(0, 77) + '...' : singleLine; - } - - const firstLine = lines[0]; - const truncatedFirst = firstLine?.length > 60 ? firstLine?.substring(0, 57) + '...' : firstLine; - const remainingCount = lines.length - 1; - - return `${truncatedFirst} (+${remainingCount} more line${remainingCount === 1 ? '' : 's'})`; -} - -export function displaySearchResults(results: Array<{ item: BrainDump, score?: number }>, query?: string) { - if (results.length === 0) { - if (query) { - console.log(`No brain dumps found matching "${query}"`); - } else { - console.log("No brain dumps found. Try 'flux dump' to create your first one!"); - } - return; - } - - const queryText = query ? ` for "${query}"` : ''; - console.log(`\nFound ${results.length} brain dump${results.length === 1 ? '' : 's'}${queryText}:\n`); - - - const terminalWidth = process.stdout.columns || 80; - const maxIndexWidth = results.length.toString().length; - - results.forEach((result, index) => { - const dump = result.item; - const score = result.score?.toFixed(2) || '0.00'; - const shortId = dump.id.substring(0, 8); - - - const indexStr = `${(index + 1).toString().padStart(maxIndexWidth)}`; - const scoreStr = `[${score}]`; - const idStr = `${shortId}`; - - const headerLine = `${indexStr} ${idStr} ${scoreStr}`; - console.log(headerLine); - - const messageIndent = ' '.repeat(maxIndexWidth + 1); - - - const lines = dump.message.split('\n').map(l => l.trim()).filter(l => l.length > 0); - const availableWidth = terminalWidth - messageIndent.length - 2; - - if (lines.length === 0) { - console.log(`${messageIndent}(empty message)`); - } else { - lines.forEach((line, lineIndex) => { - if (lineIndex < 3) { - const truncatedLine = line.length > availableWidth - ? line.substring(0, availableWidth - 3) + '...' - : line; - console.log(`${messageIndent}${truncatedLine}`); - } - }); - - if (lines.length > 3) { - console.log(`${messageIndent}... (+${lines.length - 3} more line${lines.length - 3 === 1 ? '' : 's'})`); - } - } - - const contextInfo = []; - const date = new Date(dump.timestamp); - const timeAgo = getTimeAgo(date); - contextInfo.push('----------------'); - contextInfo.push(`${timeAgo}`); - contextInfo.push('----------------'); - contextInfo.push('\n'); - - if (dump.branch && dump.branch !== 'main') { - contextInfo.push(`${dump.branch}${dump.hasUncommittedChanges ? ' (uncommitted)' : ''}`); - } - - if (contextInfo.length > 0) { - console.log(`${messageIndent}${contextInfo.join(' • ')}`); - } - - console.log(''); - }); - - console.log(`!! Use the 8-character ID (like ${results[0]?.item.id.substring(0, 8)}) to reference specific dumps\n`); -} - - -function getTimeAgo(date: Date): string { - const now = new Date(); - const diffMs = now.getTime() - date.getTime(); - const diffMins = Math.floor(diffMs / (1000 * 60)); - const diffHours = Math.floor(diffMins / 60); - const diffDays = Math.floor(diffHours / 24); - - if (diffMins < 60) return `${diffMins}m ago`; - if (diffHours < 24) return `${diffHours}h ago`; - if (diffDays < 7) return `${diffDays}d ago`; - - return date.toLocaleDateString(); + return `${index !== undefined ? index + 1 : ''}. [${score || '0.00'}] [${formattedTimestamp}] ${message}` } diff --git a/src/utils/lib.ts b/src/utils/lib.ts index b360adf..30593b4 100644 --- a/src/utils/lib.ts +++ b/src/utils/lib.ts @@ -11,6 +11,7 @@ export async function getFluxPath() { let fullPath = cwd.split(path.sep); while (true) { let parentPath = fullPath.join(path.sep) + "/.flux" + console.log(`testing ${parentPath}`) if (fs.existsSync(parentPath)) { return parentPath.split(".flux")[0] break;