diff --git a/.bumpy/fix-add-command.md b/.bumpy/fix-add-command.md new file mode 100644 index 0000000..13fe5ec --- /dev/null +++ b/.bumpy/fix-add-command.md @@ -0,0 +1,5 @@ +--- +'@varlock/bumpy': patch +--- + +Fix `bumpy add` interactive prompt: distinguish skip vs none, respect existing bump files, fix prompt rendering, remove cascade prompt diff --git a/packages/bumpy/src/commands/add.ts b/packages/bumpy/src/commands/add.ts index 356c23c..af22433 100644 --- a/packages/bumpy/src/commands/add.ts +++ b/packages/bumpy/src/commands/add.ts @@ -4,16 +4,15 @@ import { log } from '../utils/logger.ts'; import { p, unwrap } from '../utils/clack.ts'; import { ensureDir, exists } from '../utils/fs.ts'; import { randomName, slugify } from '../utils/names.ts'; -import { writeBumpFile } from '../core/bump-file.ts'; +import { writeBumpFile, readBumpFiles, filterBranchBumpFiles } from '../core/bump-file.ts'; import picomatch from 'picomatch'; -import { getBumpyDir, loadConfig, loadPackageConfig, matchGlob } from '../core/config.ts'; +import { getBumpyDir, loadConfig, loadPackageConfig } from '../core/config.ts'; import { discoverPackages, discoverWorkspace } from '../core/workspace.ts'; import { findChangedPackages } from './check.ts'; -import { DependencyGraph } from '../core/dep-graph.ts'; import { getChangedFiles } from '../core/git.ts'; import { bumpSelectPrompt } from '../prompts/bump-select.ts'; -import type { BumpSelectItem } from '../prompts/bump-select.ts'; -import type { BumpType, BumpTypeWithNone, BumpFileRelease, BumpFileReleaseCascade } from '../types.ts'; +import type { BumpSelectItem, BumpLevel } from '../prompts/bump-select.ts'; +import type { BumpTypeWithNone, BumpFileRelease } from '../types.ts'; interface AddOptions { packages?: string; // "pkg-a:minor,pkg-b:patch" @@ -23,12 +22,6 @@ interface AddOptions { none?: boolean; } -const CASCADE_CHOICES: { label: string; value: BumpType }[] = [ - { label: 'patch', value: 'patch' }, - { label: 'minor', value: 'minor' }, - { label: 'major', value: 'major' }, -]; - export async function addCommand(rootDir: string, opts: AddOptions): Promise { const config = await loadConfig(rootDir); const bumpyDir = getBumpyDir(rootDir); @@ -86,8 +79,6 @@ export async function addCommand(rootDir: string, opts: AddOptions): Promise(); + for (const bf of branchBumpFiles) { + for (const release of bf.releases) { + alreadyCoveredPackages.set(release.name, release.type === 'none' ? 'none' : release.type); + } + } + // Build items for the bump select prompt - const bumpSelectItems: BumpSelectItem[] = [...pkgs.values()].map((pkg) => ({ - name: pkg.name, - version: pkg.version, - changed: changedPackageNames.has(pkg.name), - })); + const bumpSelectItems: BumpSelectItem[] = [...pkgs.values()].map((pkg) => { + const item: BumpSelectItem = { + name: pkg.name, + version: pkg.version, + changed: changedPackageNames.has(pkg.name), + }; + // If already covered by an existing bump file, default to skip + if (alreadyCoveredPackages.has(pkg.name)) { + item.initialLevel = 'skip'; + } + return item; + }); const bumpSelectResult = await bumpSelectPrompt(bumpSelectItems); if (typeof bumpSelectResult === 'symbol') { @@ -136,62 +144,7 @@ export async function addCommand(rootDir: string, opts: AddOptions): Promise 0 || cascadeTargets) { - const wantCascade = unwrap( - await p.confirm({ - message: `${pc.cyan(name)} has ${pc.bold(String(dependents.length))} dependents. Specify explicit cascades?`, - initialValue: false, - }), - ); - - if (wantCascade) { - const allTargets = new Set(); - for (const d of dependents) allTargets.add(d.name); - if (cascadeTargets) { - for (const pattern of Object.keys(cascadeTargets)) { - for (const [pName] of pkgs) { - if (matchGlob(pName, pattern)) allTargets.add(pName); - } - } - } - - const cascadeSelected = unwrap( - await p.multiselect({ - message: 'Which packages should cascade?', - options: [...allTargets].map((n) => ({ label: n, value: n })), - required: false, - }), - ); - - if (cascadeSelected.length > 0) { - const cascadeBump = unwrap( - await p.select({ - message: 'Cascade bump type', - options: CASCADE_CHOICES, - }), - ); - const cascade: Record = {}; - for (const target of cascadeSelected) { - cascade[target] = cascadeBump; - } - (release as BumpFileReleaseCascade).cascade = cascade; - } - } - } - } - - releases.push(release); - } + releases = bumpSelections.map(({ name, type }) => ({ name, type }) as BumpFileRelease); summary = unwrap( await p.text({ diff --git a/packages/bumpy/src/prompts/bump-select.ts b/packages/bumpy/src/prompts/bump-select.ts index 2a7e590..7e9a371 100644 --- a/packages/bumpy/src/prompts/bump-select.ts +++ b/packages/bumpy/src/prompts/bump-select.ts @@ -2,14 +2,17 @@ import * as readline from 'node:readline'; import pc from 'picocolors'; import type { BumpTypeWithNone } from '../types.ts'; -export type BumpLevel = BumpTypeWithNone | 'none'; +/** 'skip' = not included in bump file at all, 'none' = explicitly included with type none */ +export type BumpLevel = 'skip' | BumpTypeWithNone; -const LEVELS: BumpLevel[] = ['none', 'patch', 'minor', 'major']; +const LEVELS: BumpLevel[] = ['skip', 'none', 'patch', 'minor', 'major']; export interface BumpSelectItem { name: string; version: string; changed: boolean; + /** Pre-set level (e.g. from an existing bump file on the branch) */ + initialLevel?: BumpLevel; } export interface BumpSelectResult { @@ -33,7 +36,9 @@ export async function bumpSelectPrompt(items: BumpSelectItem[]): Promise (item.changed ? 'patch' : 'none')); + const levels: BumpLevel[] = items.map((item) => + item.initialLevel !== undefined ? item.initialLevel : item.changed ? 'patch' : 'skip', + ); return new Promise((resolve) => { const { stdin, stdout } = process; @@ -55,9 +60,9 @@ export async function bumpSelectPrompt(items: BumpSelectItem[]): Promise levels[idx] !== 'none'); + const selected = displayOrder.filter(({ idx }) => levels[idx] !== 'skip'); if (selected.length === 0) { - lines.push(`${pc.dim('│')} ${pc.dim('(none)')}`); + lines.push(`${pc.dim('│')} ${pc.dim('(none selected)')}`); } else { for (const { item, idx } of selected) { lines.push(`${pc.dim('│')} ${pc.cyan(item.name)} ${pc.dim('→')} ${pc.bold(levels[idx])}`); @@ -67,7 +72,7 @@ export async function bumpSelectPrompt(items: BumpSelectItem[]): Promise l !== 'none').length; + const selectedCount = levels.filter((l) => l !== 'skip').length; lines.push(`${pc.dim('│')} ${pc.dim(`${selectedCount} package${selectedCount !== 1 ? 's' : ''} selected`)}`); lines.push(`${pc.dim('└')}`); } @@ -103,6 +108,7 @@ export async function bumpSelectPrompt(items: BumpSelectItem[]): Promise { + function onKeypress(_str: string | undefined, key: readline.Key) { if (!key) return; if (key.name === 'escape' || (key.ctrl && key.name === 'c')) { @@ -138,7 +147,7 @@ export async function bumpSelectPrompt(items: BumpSelectItem[]): Promise { if (l === level) { + if (l === 'skip') return pc.bold(pc.dim('[skip]')); if (l === 'none') return pc.bold(pc.dim('[none]')); if (l === 'major') return pc.bold(pc.red(`[${l}]`)); if (l === 'minor') return pc.bold(pc.yellow(`[${l}]`));