feat: implement S4 versioning system, fix Gemini API integration, and add whoami command
This commit is contained in:
+43
-6
@@ -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')
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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 };
|
||||||
@@ -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 };
|
||||||
@@ -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 };
|
||||||
Reference in New Issue
Block a user