From 9eaa68165e08e7bdd0c38b2ba3d5af560ddf7b89 Mon Sep 17 00:00:00 2001 From: s41r4j Date: Wed, 28 Jan 2026 23:45:14 +0530 Subject: [PATCH] feat: implement S4 versioning system, fix Gemini API integration, and add whoami command --- bin/gims.js | 49 +++++++++-- bin/lib/ai/providers.js | 13 ++- bin/lib/commands/version.js | 162 ++++++++++++++++++++++++++++++++++ bin/lib/commands/whoami.js | 44 ++++++++++ bin/lib/utils/s4.js | 169 ++++++++++++++++++++++++++++++++++++ 5 files changed, 428 insertions(+), 9 deletions(-) create mode 100644 bin/lib/commands/version.js create mode 100644 bin/lib/commands/whoami.js create mode 100644 bin/lib/utils/s4.js diff --git a/bin/gims.js b/bin/gims.js index 324a674..9c0b8f6 100755 --- a/bin/gims.js +++ b/bin/gims.js @@ -16,6 +16,8 @@ const { GitAnalyzer } = require('./lib/git/analyzer'); const { AIProviderManager } = require('./lib/ai/providers'); const { InteractiveCommands } = require('./lib/commands/interactive'); const { Intelligence } = require('./lib/utils/intelligence'); +const { VersionCommand } = require('./lib/commands/version'); +const { WhoAmICommand } = require('./lib/commands/whoami'); const program = new Command(); const git = simpleGit(); @@ -25,6 +27,18 @@ const configManager = new ConfigManager(); const gitAnalyzer = new GitAnalyzer(git); let aiProvider; let interactive; +let versionCmd; +let whoAmI; + +// ... (getOpts function remains) ... + +function initializeComponents() { + const config = configManager.load(); + aiProvider = new AIProviderManager(config); + interactive = new InteractiveCommands(git, aiProvider, gitAnalyzer); + versionCmd = new VersionCommand(git, configManager); + whoAmI = new WhoAmICommand(git); +} function getOpts() { const cfg = configManager.load(); @@ -47,11 +61,7 @@ function getOpts() { }; } -function initializeComponents() { - const config = configManager.load(); - aiProvider = new AIProviderManager(config); - interactive = new InteractiveCommands(git, aiProvider, gitAnalyzer); -} + async function ensureRepo() { const isRepo = await git.checkIsRepo(); @@ -138,7 +148,7 @@ async function hasChanges() { program .name('gims') .alias('g') - .version(require('../package.json').version, '-v, --version', 'Output the version number') + .version(require('../package.json').version, '--version', 'Output the version number') // Removed -v .option('--provider ', 'AI provider: auto|openai|gemini|groq|none') .option('--model ', 'Model identifier for provider') .option('--staged-only', 'Use only staged changes (default for suggest)') @@ -772,6 +782,33 @@ program.command('stash') } }); +program.command('version').alias('v') + .description('Manage S4 version (smart bump by default)') + .argument('[type]', 'bump type: major, minor, patch, auto', 'auto') + .option('-s, --stage ', 'Set prerelease stage (dev, alpha, beta, rc, stable)') + .option('-n, --dry-run', 'Show next version without applying') + .option('-i, --info', 'Show info about current version') + .action(async (type, options) => { + await ensureRepo(); + if (!versionCmd) initializeComponents(); + try { + await versionCmd.run(type, options); + } catch (e) { + handleError('Version error', e); + } + }); + +program.command('whoami') + .description('Show system and tool identity status') + .action(async () => { + try { + if (!whoAmI) initializeComponents(); + await whoAmI.run(); + } catch (e) { + handleError('WhoAmI error', e); + } + }); + program.command('amend').alias('a') .description('Stage all changes and amend last commit (keeps message)') .option('--edit', 'Generate new AI commit message') diff --git a/bin/lib/ai/providers.js b/bin/lib/ai/providers.js index 247c1d2..326d879 100644 --- a/bin/lib/ai/providers.js +++ b/bin/lib/ai/providers.js @@ -15,7 +15,7 @@ class AIProviderManager { resolveProvider(preference = 'auto') { if (preference === 'none') return 'none'; - + // Check if preferred provider's key is available if (preference === 'openai' && process.env.OPENAI_API_KEY) return 'openai'; if (preference === 'gemini' && process.env.GEMINI_API_KEY) return 'gemini'; @@ -25,7 +25,7 @@ class AIProviderManager { if (process.env.GEMINI_API_KEY) return 'gemini'; if (process.env.OPENAI_API_KEY) return 'openai'; if (process.env.GROQ_API_KEY) return 'groq'; - + return 'none'; } @@ -90,7 +90,14 @@ class AIProviderManager { model: actualModel, contents: prompt, }); - return (await response.response.text()).trim(); + + // Handle @google/genai v1.5+ raw response structure + if (response && response.candidates && response.candidates.length > 0) { + const parts = response.candidates[0].content.parts; + return parts.map(p => p.text || '').join('').trim(); + } + + return ''; } async generateWithOpenAI(prompt, model, options) { diff --git a/bin/lib/commands/version.js b/bin/lib/commands/version.js new file mode 100644 index 0000000..a8233a0 --- /dev/null +++ b/bin/lib/commands/version.js @@ -0,0 +1,162 @@ +const fs = require('fs'); +const path = require('path'); +const { S4Versioning } = require('../utils/s4'); +const { color } = require('../utils/colors'); +const { Progress } = require('../utils/progress'); + +class VersionCommand { + constructor(git, configManager) { + this.git = git; + this.configManager = configManager; + this.s4 = new S4Versioning(git); + } + + async run(type = 'auto', options = {}) { + const { info, dryRun, stage } = options; + + try { + // 1. Get current version + const currentVersion = await this.getCurrentVersion(); + if (!currentVersion) { + throw new Error('Could not find version in package.json or VERSION file'); + } + + // 2. Info mode + if (info) { + this.showInfo(currentVersion); + return; + } + + // 3. Calculate next version + Progress.start('Calculated next version...'); + const nextVersion = await this.s4.bump(currentVersion, type, stage); + Progress.stop(''); + + // 4. Dry run + if (dryRun) { + console.log(color.bold('\nDry Run Results:')); + console.log(`Current: ${color.dim(currentVersion)}`); + console.log(`Next: ${color.green(nextVersion)}`); + this.showInfo(nextVersion); + return; + } + + // 5. Update files + console.log(`\nBumping version: ${color.dim(currentVersion)} → ${color.green(nextVersion)}`); + + const updatedFiles = []; + + // Update package.json + if (this.updatePackageJson(nextVersion)) updatedFiles.push('package.json'); + + // Update .env + const envUpdates = this.updateEnvFiles(nextVersion); + updatedFiles.push(...envUpdates); + + // Create VERSION file if it doesn't exist or update it + fs.writeFileSync('VERSION', nextVersion); + updatedFiles.push('VERSION'); + + if (updatedFiles.length > 0) { + console.log(`${color.green('✔')} Updated ${updatedFiles.join(', ')}`); + } + + // 6. Git commit and tag + await this.createRelease(nextVersion, updatedFiles); + + } catch (error) { + Progress.error(`Version bump failed: ${error.message}`); + } + } + + async getCurrentVersion() { + // Try package.json + if (fs.existsSync('package.json')) { + const pkg = JSON.parse(fs.readFileSync('package.json', 'utf8')); + if (pkg.version) return pkg.version; + } + + // Try VERSION file + if (fs.existsSync('VERSION')) { + return fs.readFileSync('VERSION', 'utf8').trim(); + } + + return '0.0.0'; // Default start + } + + updatePackageJson(newVersion) { + if (!fs.existsSync('package.json')) return false; + + const pkg = JSON.parse(fs.readFileSync('package.json', 'utf8')); + pkg.version = newVersion; + fs.writeFileSync('package.json', JSON.stringify(pkg, null, 2) + '\n'); + return true; + } + + updateEnvFiles(newVersion) { + const updates = []; + const envFiles = ['.env', '.env.local', '.env.development', '.env.production']; + + envFiles.forEach(file => { + if (fs.existsSync(file)) { + let content = fs.readFileSync(file, 'utf8'); + let changed = false; + + // Match VERSION=... or APP_VERSION=... + const regex = /^(VERSION|APP_VERSION|NEXT_PUBLIC_VERSION)=(.*)$/gm; + + if (regex.test(content)) { + content = content.replace(regex, `$1=${newVersion}`); + changed = true; + } else { + // Optionally append if missing? Maybe too aggressive. + } + + if (changed) { + fs.writeFileSync(file, content); + updates.push(file); + } + } + }); + + return updates; + } + + async createRelease(version, files) { + try { + // Stage files + await this.git.add(files); + + // Commit + const msg = `chore(release): bump to ${version}`; + await this.git.commit(msg); + + // Tag + await this.git.addAnnotatedTag(version, `Release ${version}`); + + console.log(`${color.green('✔')} Created git commit and tag: ${color.cyan(version)}`); + console.log(`\nRun ${color.cyan('g push --follow-tags')} to publish`); + } catch (e) { + console.log(color.yellow(`⚠ Git operations failed: ${e.message}`)); + } + } + + showInfo(versionStr) { + const parsed = S4Versioning.parse(versionStr); + if (!parsed) { + console.log(color.red('Invalid S4 version format')); + return; + } + + console.log(color.bold('\nS4 Version Analysis:')); + console.log(`Major: ${parsed.major}`); + console.log(`Minor: ${parsed.minor}`); + console.log(`Patch: ${parsed.patch}`); + console.log(`Stage: ${color.magenta(parsed.stage)}`); + console.log(`Build: ${parsed.build}`); + console.log(`Time: ${parsed.date} @ ${parsed.time}`); + console.log(`Git: ${parsed.branch} (${parsed.commit})`); + } +} + +module.exports = { VersionCommand }; diff --git a/bin/lib/commands/whoami.js b/bin/lib/commands/whoami.js new file mode 100644 index 0000000..170008f --- /dev/null +++ b/bin/lib/commands/whoami.js @@ -0,0 +1,44 @@ +const { color } = require('../utils/colors'); +const os = require('os'); + +class WhoAmICommand { + constructor(git) { + this.git = git; + } + + async run() { + const pkg = require('../../../package.json'); + const user = os.userInfo().username; + const hostname = os.hostname(); + const platform = os.platform(); + const arch = os.arch(); + + let gitUser = 'unknown'; + let gitEmail = 'unknown'; + try { + gitUser = (await this.git.raw(['config', 'user.name'])).trim(); + gitEmail = (await this.git.raw(['config', 'user.email'])).trim(); + } catch (e) { } + + console.log(color.bold('\n⚡ GIMS SYSTEM STATUS ⚡\n')); + + console.log(`${color.green('SYSTEM_IDENTITY')}`); + console.log(`├─ Host: ${color.cyan(hostname)}`); + console.log(`├─ User: ${color.cyan(user)}`); + console.log(`└─ OS: ${platform} (${arch})`); + + console.log(`\n${color.green('GIT_OPERATOR')}`); + console.log(`├─ Name: ${color.cyan(gitUser)}`); + console.log(`└─ Email: ${color.cyan(gitEmail)}`); + + console.log(`\n${color.green('TOOL_METRICS')}`); + console.log(`├─ Version: ${color.magenta(pkg.version)}`); + console.log(`├─ Engine: Node ${process.version}`); + console.log(`└─ Author: ${pkg.author}`); + + console.log('\n' + color.dim('----------------------------------------')); + console.log(color.dim(`Initialized at: ${new Date().toISOString()}`)); + } +} + +module.exports = { WhoAmICommand }; diff --git a/bin/lib/utils/s4.js b/bin/lib/utils/s4.js new file mode 100644 index 0000000..000101a --- /dev/null +++ b/bin/lib/utils/s4.js @@ -0,0 +1,169 @@ +const simpleGit = require('simple-git'); +const path = require('path'); +const fs = require('fs'); + +class S4Versioning { + constructor(git) { + this.git = git || simpleGit(); + } + + // Regex for parsing S4 version + static get REGEX() { + return /^([0-9]+)\.([0-9]+)\.([0-9]+)-([a-z]+)\.([0-9]+)\+([0-9]{8})\.([0-9]{4})\.([0-9a-f]+)\.([a-zA-Z0-9._\/-]+)$/; + } + + /** + * Parse an S4 version string into components + */ + static parse(versionStr) { + const match = versionStr.match(S4Versioning.REGEX); + if (!match) return null; + + return { + major: parseInt(match[1]), + minor: parseInt(match[2]), + patch: parseInt(match[3]), + stage: match[4], + build: parseInt(match[5]), + date: match[6], + time: match[7], + commit: match[8], + branch: match[9], + original: versionStr + }; + } + + /** + * Generate S4 version string from components + */ + static stringify(components) { + const { major, minor, patch, stage, build, date, time, commit, branch } = components; + return `${major}.${minor}.${patch}-${stage}.${build}+${date}.${time}.${commit}.${branch}`; + } + + /** + * Migrate a standard SemVer or empty version to S4 + */ + async migrate(currentVersion) { + // Basic SemVer parsing (loose) + const semVerMatch = currentVersion.match(/^([0-9]+)\.([0-9]+)\.([0-9]+)(?:-([a-zA-Z0-9.]+))?/); + + let base = { + major: 0, minor: 0, patch: 0, stage: 'dev', build: 1 + }; + + if (semVerMatch) { + base.major = parseInt(semVerMatch[1]); + base.minor = parseInt(semVerMatch[2]); + base.patch = parseInt(semVerMatch[3]); + // Attempt to salvage stage if present, else default to dev + if (semVerMatch[4]) { + const parts = semVerMatch[4].split('.'); + const possibleStage = parts.find(p => /^[a-z]+$/.test(p)); + if (possibleStage) base.stage = possibleStage; + } + } + + const ctx = await this.getContext(); + return S4Versioning.stringify({ ...base, ...ctx }); + } + + /** + * Get current Git and Time context + */ + async getContext() { + const now = new Date(); + const pad = (n) => n.toString().padStart(2, '0'); + const date = `${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}`; + const time = `${pad(now.getHours())}${pad(now.getMinutes())}`; + + let commit = '0000000'; + let branch = 'unknown'; + + try { + const isRepo = await this.git.checkIsRepo(); + if (isRepo) { + commit = (await this.git.revparse(['--short=7', 'HEAD'])).trim(); + branch = (await this.git.revparse(['--abbrev-ref', 'HEAD'])).trim(); + } + } catch (e) { + // Ignore git errors, stick to defaults + } + + // Sanitize branch name for S4 compliance + branch = branch.replace(/[^a-zA-Z0-9._\/-]/g, '-'); + + return { date, time, commit, branch }; + } + + /** + * Calculate next version based on bump type + */ + async bump(currentStr, type = 'auto', stageVal = null) { + let parsed = S4Versioning.parse(currentStr); + + // If not S4, migrate first + if (!parsed) { + const migrated = await this.migrate(currentStr); + parsed = S4Versioning.parse(migrated); + } + + const ctx = await this.getContext(); + const next = { ...parsed, ...ctx }; // Update context (date, time, commit, branch) + + // Handle stage change + if (stageVal && stageVal !== parsed.stage) { + next.stage = stageVal; + next.build = 1; // Stage change resets build usually? S4 "Sequence" implies uniqueness. + // S4 doc: "When to Increment: Each new build of the same version on the same day" + // Changing stage is effectively a new version identity. + } else { + // Keep existing stage if not specified + } + + // Logic for bumping + switch (type) { + case 'major': + next.major++; + next.minor = 0; + next.patch = 0; + next.build = 1; + break; + case 'minor': + next.minor++; + next.patch = 0; + next.build = 1; + break; + case 'patch': + next.patch++; + next.build = 1; + break; + case 'auto': + // Smart Auto Logic + + // 1. If Date Changed, reset build to 1 + if (parsed.date !== ctx.date) { + next.build = 1; + } + // 2. If Date is SAME + else { + // If Commit Changed -> It's a new build of the same day + if (parsed.commit !== ctx.commit) { + next.build = parsed.build + 1; + } + // If Commit is SAME and user requested bump, force build increment? + // "auto" implies we just want the "next" logical version for the current state. + // If nothing changed, maybe we shouldn't bump? + // But usually CI calls this to FORCE a new version. + else { + next.build = parsed.build + 1; + } + } + break; + } + + return S4Versioning.stringify(next); + } +} + +module.exports = { S4Versioning };