diff --git a/packages/cli/package.json b/packages/cli/package.json index 3ecfec37f..b59fffe89 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -37,6 +37,7 @@ "devDependencies": { "@types/js-yaml": "^4.0.9", "@types/node": "^20.12.7", + "@types/semver": "^7.7.0", "@types/shelljs": "^0.8.16", "ts-node": "^10.9.2" }, @@ -54,6 +55,7 @@ "minimist": "^1.2.8", "pg-cache": "^1.1.1", "pg-env": "^1.1.0", + "semver": "^7.7.2", "shelljs": "^0.9.2" }, "resolutions": { diff --git a/packages/cli/src/commands.ts b/packages/cli/src/commands.ts index 835cb485b..e66064e78 100644 --- a/packages/cli/src/commands.ts +++ b/packages/cli/src/commands.ts @@ -20,6 +20,7 @@ import revert from './commands/revert'; import server from './commands/server'; import tag from './commands/tag'; import verify from './commands/verify'; +import version from './commands/version'; import analyze from './commands/analyze'; import renameCmd from './commands/rename'; import { readAndParsePackageJson } from './package'; @@ -50,6 +51,7 @@ const createCommandMap = (skipPgTeardown: boolean = false): Record Target specific package + --cwd Working directory (default: current directory) + +Examples: + lql version Prompt for version bump type + lql version --package mypackage Version specific package +`; + +export default async ( + argv: Partial>, + prompter: Inquirerer, + _options: CLIOptions +) => { + if (argv.help || argv.h) { + console.log(versionUsageText); + process.exit(0); + } + + const { newArgv } = extractFirst(argv); + + const cwdResult = await prompter.prompt(newArgv, [ + { + type: 'text', + name: 'cwd', + message: 'Working directory', + required: false, + default: process.cwd(), + useDefault: true + } + ]); + const cwd = (cwdResult as any).cwd || process.cwd(); + + const pkg = new LaunchQLPackage(cwd); + + let packageName: string | undefined; + + if (argv.package) { + packageName = argv.package as string; + log.info(`Using specified package: ${packageName}`); + } + else if (pkg.isInModule()) { + packageName = pkg.getModuleName(); + log.info(`Using current module: ${packageName}`); + } + else if (pkg.isInWorkspace()) { + packageName = await selectPackage(newArgv, prompter, cwd, 'version', log); + if (!packageName) { + throw new Error('No package selected. Cannot version without specifying a target package.'); + } + } else { + throw new Error('This command must be run inside a LaunchQL workspace or module.'); + } + + const versionAnswer = await prompter.prompt(newArgv, [ + { + type: 'autocomplete', + name: 'bumpType', + message: 'Select version bump type', + options: ['patch', 'minor', 'major'], + required: true + } + ]) as any; + + const bumpType = versionAnswer.bumpType; + + try { + if (argv.package || !pkg.isInModule()) { + const moduleMap = pkg.getModuleMap(); + const module = moduleMap[packageName]; + if (!module) { + throw errors.MODULE_NOT_FOUND({ name: packageName }); + } + + const workspacePath = pkg.getWorkspacePath()!; + const absoluteModulePath = path.resolve(workspacePath, module.path); + + const originalCwd = process.cwd(); + process.chdir(absoluteModulePath); + + try { + const modulePkg = new LaunchQLPackage(absoluteModulePath); + const newVersion = await updatePackageVersion(absoluteModulePath, bumpType); + modulePkg.addTag(`v${newVersion}`, undefined, `Version bump: ${bumpType}`); + log.info(`Successfully bumped ${packageName} to version ${newVersion} and added tag v${newVersion}`); + } finally { + process.chdir(originalCwd); + } + } else { + const newVersion = await updatePackageVersion(pkg.getModulePath()!, bumpType); + pkg.addTag(`v${newVersion}`, undefined, `Version bump: ${bumpType}`); + log.info(`Successfully bumped to version ${newVersion} and added tag v${newVersion}`); + } + } catch (error) { + log.error(`Failed to version package: ${error instanceof Error ? error.message : String(error)}`); + throw error; + } + + return newArgv; +}; + +async function updatePackageVersion(modulePath: string, bumpType: 'major' | 'minor' | 'patch'): Promise { + const pkgJsonPath = path.join(modulePath, 'package.json'); + + if (!fs.existsSync(pkgJsonPath)) { + throw new Error(`No package.json found at module path: ${modulePath}`); + } + + const pkgData = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf-8')); + const currentVersion = pkgData.version; + + if (!currentVersion) { + throw new Error('No version field found in package.json'); + } + + const newVersion = semver.inc(currentVersion, bumpType); + + if (!newVersion) { + throw new Error(`Failed to calculate new ${bumpType} version from ${currentVersion}`); + } + + pkgData.version = newVersion; + fs.writeFileSync(pkgJsonPath, JSON.stringify(pkgData, null, 2) + '\n'); + + return newVersion; +}