feat: implement S4 versioning system, fix Gemini API integration, and add whoami command

This commit is contained in:
s41r4j
2026-01-28 23:45:14 +05:30
parent 687dd1920f
commit 9eaa68165e
5 changed files with 428 additions and 9 deletions
+43 -6
View File
@@ -16,6 +16,8 @@ const { GitAnalyzer } = require('./lib/git/analyzer');
const { AIProviderManager } = require('./lib/ai/providers'); const { AIProviderManager } = require('./lib/ai/providers');
const { InteractiveCommands } = require('./lib/commands/interactive'); const { InteractiveCommands } = require('./lib/commands/interactive');
const { Intelligence } = require('./lib/utils/intelligence'); const { Intelligence } = require('./lib/utils/intelligence');
const { VersionCommand } = require('./lib/commands/version');
const { WhoAmICommand } = require('./lib/commands/whoami');
const program = new Command(); const program = new Command();
const git = simpleGit(); const git = simpleGit();
@@ -25,6 +27,18 @@ const configManager = new ConfigManager();
const gitAnalyzer = new GitAnalyzer(git); const gitAnalyzer = new GitAnalyzer(git);
let aiProvider; let aiProvider;
let interactive; 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() { function getOpts() {
const cfg = configManager.load(); 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() { async function ensureRepo() {
const isRepo = await git.checkIsRepo(); const isRepo = await git.checkIsRepo();
@@ -138,7 +148,7 @@ async function hasChanges() {
program program
.name('gims') .name('gims')
.alias('g') .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 <name>', 'AI provider: auto|openai|gemini|groq|none') .option('--provider <name>', 'AI provider: auto|openai|gemini|groq|none')
.option('--model <name>', 'Model identifier for provider') .option('--model <name>', 'Model identifier for provider')
.option('--staged-only', 'Use only staged changes (default for suggest)') .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 <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') program.command('amend').alias('a')
.description('Stage all changes and amend last commit (keeps message)') .description('Stage all changes and amend last commit (keeps message)')
.option('--edit', 'Generate new AI commit message') .option('--edit', 'Generate new AI commit message')
+10 -3
View File
@@ -15,7 +15,7 @@ class AIProviderManager {
resolveProvider(preference = 'auto') { resolveProvider(preference = 'auto') {
if (preference === 'none') return 'none'; if (preference === 'none') return 'none';
// Check if preferred provider's key is available // Check if preferred provider's key is available
if (preference === 'openai' && process.env.OPENAI_API_KEY) return 'openai'; if (preference === 'openai' && process.env.OPENAI_API_KEY) return 'openai';
if (preference === 'gemini' && process.env.GEMINI_API_KEY) return 'gemini'; 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.GEMINI_API_KEY) return 'gemini';
if (process.env.OPENAI_API_KEY) return 'openai'; if (process.env.OPENAI_API_KEY) return 'openai';
if (process.env.GROQ_API_KEY) return 'groq'; if (process.env.GROQ_API_KEY) return 'groq';
return 'none'; return 'none';
} }
@@ -90,7 +90,14 @@ class AIProviderManager {
model: actualModel, model: actualModel,
contents: prompt, 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) { async generateWithOpenAI(prompt, model, options) {
+162
View File
@@ -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 };
+44
View File
@@ -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 };
+169
View File
@@ -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 };