import * as plugins from './mod.plugins.js'; import * as paths from '../paths.js'; import { logger } from '../gitzone.logging.js'; import * as ui from './mod.ui.js'; export type ProjectType = 'npm' | 'deno' | 'both' | 'none'; export type VersionType = 'patch' | 'minor' | 'major'; /** * Detects the current git branch * @returns The current branch name, defaults to 'master' if detection fails */ export async function detectCurrentBranch(): Promise { try { const smartshellInstance = new plugins.smartshell.Smartshell({ executor: 'bash', sourceFilePaths: [], }); const result = await smartshellInstance.exec('git branch --show-current'); const branchName = result.stdout.trim(); if (!branchName) { logger.log('warn', 'Could not detect current branch, falling back to "master"'); return 'master'; } logger.log('info', `Detected current branch: ${branchName}`); return branchName; } catch (error) { logger.log('warn', `Failed to detect branch: ${error.message}, falling back to "master"`); return 'master'; } } /** * Detects the project type based on presence of package.json and/or deno.json * @returns The project type */ export async function detectProjectType(): Promise { const packageJsonPath = plugins.path.join(paths.cwd, 'package.json'); const denoJsonPath = plugins.path.join(paths.cwd, 'deno.json'); const hasPackageJson = await plugins.smartfile.fs.fileExists(packageJsonPath); const hasDenoJson = await plugins.smartfile.fs.fileExists(denoJsonPath); if (hasPackageJson && hasDenoJson) { logger.log('info', 'Detected dual project (npm + deno)'); return 'both'; } else if (hasPackageJson) { logger.log('info', 'Detected npm project'); return 'npm'; } else if (hasDenoJson) { logger.log('info', 'Detected deno project'); return 'deno'; } else { throw new Error('No package.json or deno.json found in current directory'); } } /** * Parses a semantic version string and bumps it according to the version type * @param currentVersion Current version string (e.g., "1.2.3") * @param versionType Type of version bump * @returns New version string */ function calculateNewVersion(currentVersion: string, versionType: VersionType): string { const versionMatch = currentVersion.match(/^(\d+)\.(\d+)\.(\d+)/); if (!versionMatch) { throw new Error(`Invalid version format: ${currentVersion}`); } let [, major, minor, patch] = versionMatch.map(Number); switch (versionType) { case 'major': major += 1; minor = 0; patch = 0; break; case 'minor': minor += 1; patch = 0; break; case 'patch': patch += 1; break; } return `${major}.${minor}.${patch}`; } /** * Bumps the version in deno.json, commits the change, and creates a tag * @param versionType Type of version bump * @returns The new version string */ export async function bumpDenoVersion(versionType: VersionType): Promise { const denoJsonPath = plugins.path.join(paths.cwd, 'deno.json'); const smartshellInstance = new plugins.smartshell.Smartshell({ executor: 'bash', sourceFilePaths: [], }); try { // Read deno.json const denoConfig = plugins.smartfile.fs.toObjectSync( denoJsonPath ) as { version?: string }; if (!denoConfig.version) { throw new Error('deno.json does not contain a version field'); } const currentVersion = denoConfig.version; const newVersion = calculateNewVersion(currentVersion, versionType); logger.log('info', `Bumping deno.json version: ${currentVersion} → ${newVersion}`); // Update version denoConfig.version = newVersion; // Write back to disk await plugins.smartfile.memory.toFs( JSON.stringify(denoConfig, null, 2) + '\n', denoJsonPath ); // Stage the deno.json file await smartshellInstance.exec('git add deno.json'); // Commit the version bump await smartshellInstance.exec(`git commit -m "v${newVersion}"`); // Create the version tag await smartshellInstance.exec(`git tag v${newVersion} -m "v${newVersion}"`); logger.log('info', `Created commit and tag v${newVersion}`); return newVersion; } catch (error) { throw new Error(`Failed to bump deno.json version: ${error.message}`); } } /** * Bumps the version in package.json using npm version command * @param versionType Type of version bump * @returns The new version string */ async function bumpNpmVersion(versionType: VersionType): Promise { const smartshellInstance = new plugins.smartshell.Smartshell({ executor: 'bash', sourceFilePaths: [], }); logger.log('info', `Bumping package.json version using npm version ${versionType}`); const result = await smartshellInstance.exec(`npm version ${versionType}`); // npm version returns the new version with a 'v' prefix, e.g., "v1.2.3" const newVersion = result.stdout.trim().replace(/^v/, ''); return newVersion; } /** * Syncs the version from package.json to deno.json and amends the npm commit * @param version The version to sync */ async function syncVersionToDenoJson(version: string): Promise { const denoJsonPath = plugins.path.join(paths.cwd, 'deno.json'); const smartshellInstance = new plugins.smartshell.Smartshell({ executor: 'bash', sourceFilePaths: [], }); try { const denoConfig = plugins.smartfile.fs.toObjectSync( denoJsonPath ) as { version?: string }; logger.log('info', `Syncing version to deno.json: ${version}`); denoConfig.version = version; await plugins.smartfile.memory.toFs( JSON.stringify(denoConfig, null, 2) + '\n', denoJsonPath ); // Stage the deno.json file await smartshellInstance.exec('git add deno.json'); // Amend the npm version commit to include deno.json await smartshellInstance.exec('git commit --amend --no-edit'); // Re-create the tag with force to update it await smartshellInstance.exec(`git tag -fa v${version} -m "v${version}"`); logger.log('info', `Amended commit to include deno.json and updated tag v${version}`); } catch (error) { throw new Error(`Failed to sync version to deno.json: ${error.message}`); } } /** * Bumps the project version based on project type * @param projectType The detected project type * @param versionType The type of version bump * @param currentStep The current step number for progress display * @param totalSteps The total number of steps for progress display * @returns The new version string */ export async function bumpProjectVersion( projectType: ProjectType, versionType: VersionType, currentStep?: number, totalSteps?: number ): Promise { const projectEmoji = projectType === 'npm' ? '📦' : projectType === 'deno' ? '🦕' : '🔀'; const description = `🏷️ Bumping version (${projectEmoji} ${projectType})`; if (currentStep && totalSteps) { ui.printStep(currentStep, totalSteps, description, 'in-progress'); } let newVersion: string; switch (projectType) { case 'npm': newVersion = await bumpNpmVersion(versionType); break; case 'deno': newVersion = await bumpDenoVersion(versionType); break; case 'both': { // Bump npm version first (it handles git tags) newVersion = await bumpNpmVersion(versionType); // Then sync to deno.json await syncVersionToDenoJson(newVersion); break; } case 'none': throw new Error('Cannot bump version: no package.json or deno.json found'); default: throw new Error(`Unknown project type: ${projectType}`); } if (currentStep && totalSteps) { ui.printStep(currentStep, totalSteps, description, 'done'); } return newVersion; }