@gati-framework/cli
Version:
CLI tool for Gati framework - create, develop, build and deploy cloud-native applications
478 lines ⢠19.4 kB
JavaScript
/**
* @module cli/commands/timescape
* @description Timescape Versioning System CLI commands
*/
import { Command } from 'commander';
import chalk from 'chalk';
import { existsSync } from 'fs';
import { resolve } from 'path';
// @ts-expect-error - Imports are valid after build
import { SQLiteTimelineStore, JSONTimelineStore } from '@gati-framework/runtime/timescape/timeline-store';
// @ts-expect-error - Imports are valid after build
import { VersionRegistry } from '@gati-framework/runtime/timescape/registry';
export const timescapeCommand = new Command('ts')
.description('Manage Timescape Versioning System')
.alias('timescape');
// Log Command
timescapeCommand
.command('log')
.description('View change history')
.option('-n, --limit <number>', 'Limit number of entries', '10')
.action(async (options) => {
console.log('DEBUG: Executing log command');
console.log(chalk.bold.cyan('\nš°ļø Timescape History\n'));
const dbPath = resolve(process.cwd(), '.gati/timeline.db');
const jsonPath = resolve(process.cwd(), '.gati/timeline.json');
let store;
try {
if (existsSync(dbPath)) {
store = new SQLiteTimelineStore(dbPath);
}
else if (existsSync(jsonPath)) {
store = new JSONTimelineStore(jsonPath);
}
else {
console.log(chalk.yellow('No Timescape history found.'));
return;
}
const limit = parseInt(options.limit, 10);
// We need to implement a query method with limit in TimelineStore,
// but for now we can use query() which returns recent items.
// Assuming query() returns items in reverse chronological order (newest first)
const _items = await store.query({ limit });
}
catch (error) {
console.error(chalk.red('Failed to read history:'), error);
}
});
// Diff Command
timescapeCommand
.command('diff')
.description('Show details of a specific change')
.argument('<id>', 'Change ID')
.action(async (id) => {
console.log(chalk.bold.cyan(`\nš Timescape Diff: ${id}\n`));
const dbPath = resolve(process.cwd(), '.gati/timeline.db');
const jsonPath = resolve(process.cwd(), '.gati/timeline.json');
let store;
try {
if (existsSync(dbPath)) {
store = new SQLiteTimelineStore(dbPath);
}
else if (existsSync(jsonPath)) {
store = new JSONTimelineStore(jsonPath);
}
else {
console.log(chalk.yellow('No Timescape history found.'));
return;
}
// We need a way to get a single item.
// Since TimelineStore interface doesn't have getById, we might need to query.
// For now, let's fetch recent items and find it, or implement getById in store.
// Optimization: In a real app, we'd add getById to the store.
// For this demo, we'll just scan recent items.
const items = await store.query({ limit: 100 });
const item = items.find((i) => i.id === id);
if (!item) {
console.error(chalk.red(`Change ${id} not found (checked last 100 items).`));
return;
}
console.log(chalk.bold(`Artifact: ${item.artifactId} (${item.type})`));
console.log(chalk.gray(`Version: ${item.version}`));
if (item.changes && item.changes.length > 0) {
console.log(chalk.bold('\nChanges:'));
item.changes.forEach((change) => {
const opColor = change.op === 'add' ? chalk.green : change.op === 'remove' ? chalk.red : chalk.yellow;
console.log(opColor(` ${change.op.toUpperCase()} ${change.path}`));
if (change.value !== undefined) {
console.log(chalk.gray(` Value: ${JSON.stringify(change.value)}`));
}
if (change.oldValue !== undefined) {
console.log(chalk.gray(` Old: ${JSON.stringify(change.oldValue)}`));
}
});
}
else {
console.log(chalk.gray('No structural changes recorded.'));
}
}
catch (error) {
console.error(chalk.red('Failed to read diff:'), error);
}
});
// Helper function to load registry
function loadRegistry() {
const registryPath = resolve(process.cwd(), '.gati/timescape/registry.json');
if (!existsSync(registryPath)) {
throw new Error('No Timescape registry found. Run your application first to create versions.');
}
const registry = new VersionRegistry();
registry.deserialize(registryPath);
return registry;
}
// Helper function to save registry
function saveRegistry(registry) {
const registryPath = resolve(process.cwd(), '.gati/timescape/registry.json');
registry.serialize(registryPath);
}
// Helper function to format timestamp
function formatTimestamp(timestamp) {
return new Date(timestamp).toISOString();
}
// Helper function to format status with color
function formatStatus(status) {
switch (status) {
case 'hot':
return chalk.red.bold('HOT');
case 'warm':
return chalk.yellow.bold('WARM');
case 'cold':
return chalk.blue.bold('COLD');
case 'deactivated':
return chalk.gray('DEACTIVATED');
default:
return chalk.white(status);
}
}
// List Command - List all versions
timescapeCommand
.command('list')
.description('List all versions')
.option('-h, --handler <path>', 'Filter by handler path')
.option('-s, --status <status>', 'Filter by status (hot/warm/cold/deactivated)')
.option('--tags', 'Show tags for each version')
.action(async (options) => {
console.log(chalk.bold.cyan('\nš Timescape Versions\n'));
try {
const registry = loadRegistry();
const handlers = options.handler
? [options.handler]
: registry.getAllHandlers();
if (handlers.length === 0) {
console.log(chalk.yellow('No versions found.'));
return;
}
for (const handlerPath of handlers) {
const versions = registry.getVersions(handlerPath);
if (!versions || versions.length === 0) {
continue;
}
console.log(chalk.bold.white(`\n${handlerPath}`));
console.log(chalk.gray('ā'.repeat(80)));
for (const version of versions) {
// Filter by status if specified
if (options.status && version.status !== options.status) {
continue;
}
const status = formatStatus(version.status);
const timestamp = formatTimestamp(version.timestamp);
const requests = version.requestCount.toString().padStart(6);
const lastAccessed = version.lastAccessed
? formatTimestamp(version.lastAccessed)
: 'Never';
console.log(`${status} ${chalk.cyan(version.tsv)}`);
console.log(` Created: ${chalk.gray(timestamp)}`);
console.log(` Requests: ${chalk.white(requests)} | Last accessed: ${chalk.gray(lastAccessed)}`);
if (version.dbSchemaVersion) {
console.log(` DB Schema: ${chalk.magenta(version.dbSchemaVersion)}`);
}
if (options.tags && version.tags.length > 0) {
console.log(` Tags: ${version.tags.map((t) => chalk.green(t)).join(', ')}`);
}
console.log('');
}
}
// Summary
const stats = registry.getUsageStatistics();
console.log(chalk.gray('ā'.repeat(80)));
console.log(chalk.bold('Summary:'));
console.log(` Total versions: ${chalk.white(stats.totalVersions)}`);
console.log(` Hot: ${chalk.red(stats.hotVersions)} | Warm: ${chalk.yellow(stats.warmVersions)} | Cold: ${chalk.blue(stats.coldVersions)} | Deactivated: ${chalk.gray(stats.deactivatedVersions)}`);
console.log('');
}
catch (error) {
console.error(chalk.red('Failed to list versions:'), error.message);
process.exit(1);
}
});
// Status Command - Show detailed status of a version
timescapeCommand
.command('status')
.description('Show detailed status of a version')
.argument('<version>', 'Version identifier (TSV, tag, or timestamp)')
.option('-h, --handler <path>', 'Handler path (required for tags/timestamps)')
.action(async (versionId, options) => {
console.log(chalk.bold.cyan(`\nš Version Status: ${versionId}\n`));
try {
const registry = loadRegistry();
let tsv;
let handlerPath;
// Try to resolve version
if (versionId.startsWith('tsv:')) {
// Direct TSV
tsv = versionId;
// Find handler for this TSV
for (const handler of registry.getAllHandlers()) {
const versions = registry.getVersions(handler);
if (versions.some((v) => v.tsv === tsv)) {
handlerPath = handler;
break;
}
}
}
else if (options.handler) {
// Tag or timestamp with handler
handlerPath = options.handler;
// Try as tag first
tsv = registry.getVersionByTag(handlerPath, versionId);
// If not found, try as timestamp
if (!tsv && versionId.includes('T')) {
const timestamp = Date.parse(versionId);
if (!isNaN(timestamp)) {
tsv = registry.getVersionAt(handlerPath, timestamp);
}
}
}
else {
console.error(chalk.red('Error: --handler is required for tags and timestamps'));
process.exit(1);
}
if (!tsv || !handlerPath) {
console.error(chalk.red(`Version not found: ${versionId}`));
process.exit(1);
}
const info = registry.getVersionInfo(handlerPath, tsv);
if (!info) {
console.error(chalk.red(`Version info not found: ${tsv}`));
process.exit(1);
}
// Display version info
console.log(chalk.bold('Version:'), chalk.cyan(info.tsv));
console.log(chalk.bold('Handler:'), chalk.white(handlerPath));
console.log(chalk.bold('Status:'), formatStatus(info.status));
console.log(chalk.bold('Created:'), chalk.gray(formatTimestamp(info.timestamp)));
console.log(chalk.bold('Hash:'), chalk.gray(info.hash));
console.log(chalk.bold('Requests:'), chalk.white(info.requestCount));
if (info.lastAccessed) {
console.log(chalk.bold('Last Accessed:'), chalk.gray(formatTimestamp(info.lastAccessed)));
}
if (info.dbSchemaVersion) {
console.log(chalk.bold('DB Schema:'), chalk.magenta(info.dbSchemaVersion));
}
if (info.tags.length > 0) {
console.log(chalk.bold('Tags:'), info.tags.map((t) => chalk.green(t)).join(', '));
}
console.log('');
}
catch (error) {
console.error(chalk.red('Failed to get version status:'), error.message);
process.exit(1);
}
});
// Deactivate Command - Manually deactivate a version
timescapeCommand
.command('deactivate')
.description('Manually deactivate a version')
.argument('<version>', 'Version identifier (TSV, tag, or timestamp)')
.option('-h, --handler <path>', 'Handler path (required for tags/timestamps)')
.option('-f, --force', 'Force deactivation even if protected')
.action(async (versionId, options) => {
console.log(chalk.bold.cyan(`\nš Deactivating Version: ${versionId}\n`));
try {
const registry = loadRegistry();
let tsv;
let handlerPath;
// Resolve version (same logic as status command)
if (versionId.startsWith('tsv:')) {
tsv = versionId;
for (const handler of registry.getAllHandlers()) {
const versions = registry.getVersions(handler);
if (versions.some((v) => v.tsv === tsv)) {
handlerPath = handler;
break;
}
}
}
else if (options.handler) {
handlerPath = options.handler;
tsv = registry.getVersionByTag(handlerPath, versionId);
if (!tsv && versionId.includes('T')) {
const timestamp = Date.parse(versionId);
if (!isNaN(timestamp)) {
tsv = registry.getVersionAt(handlerPath, timestamp);
}
}
}
else {
console.error(chalk.red('Error: --handler is required for tags and timestamps'));
process.exit(1);
}
if (!tsv || !handlerPath) {
console.error(chalk.red(`Version not found: ${versionId}`));
process.exit(1);
}
const info = registry.getVersionInfo(handlerPath, tsv);
if (!info) {
console.error(chalk.red(`Version info not found: ${tsv}`));
process.exit(1);
}
// Check if already deactivated
if (info.status === 'deactivated') {
console.log(chalk.yellow(`Version ${tsv} is already deactivated.`));
return;
}
// Check for protected tags
if (!options.force && info.tags.some((tag) => ['stable', 'production', 'latest'].includes(tag))) {
console.error(chalk.red(`Version ${tsv} has protected tags: ${info.tags.join(', ')}`));
console.error(chalk.yellow('Use --force to deactivate anyway.'));
process.exit(1);
}
// Deactivate
registry.deactivateVersion(handlerPath, tsv);
saveRegistry(registry);
console.log(chalk.green(`ā Version ${tsv} deactivated successfully.`));
console.log('');
}
catch (error) {
console.error(chalk.red('Failed to deactivate version:'), error.message);
process.exit(1);
}
});
// Tag Command - Create a semantic version tag
timescapeCommand
.command('tag')
.description('Tag a version with a semantic label')
.argument('<tsv>', 'Version identifier (TSV)')
.argument('<label>', 'Tag label (e.g., v1.0.0, stable, production)')
.option('-c, --created-by <name>', 'Creator name', 'cli-user')
.action(async (tsv, label, options) => {
console.log(chalk.bold.cyan(`\nš·ļø Tagging Version: ${tsv} ā ${label}\n`));
try {
const registry = loadRegistry();
// Validate TSV format
if (!tsv.startsWith('tsv:')) {
console.error(chalk.red(`Invalid TSV format: ${tsv}`));
console.error(chalk.yellow('TSV must start with "tsv:"'));
process.exit(1);
}
// Check if version exists
let found = false;
for (const handler of registry.getAllHandlers()) {
const versions = registry.getVersions(handler);
if (versions.some((v) => v.tsv === tsv)) {
found = true;
break;
}
}
if (!found) {
console.error(chalk.red(`Version not found: ${tsv}`));
process.exit(1);
}
// Check if tag already exists
const existingTag = registry.getAllTags().find((t) => t.label === label);
if (existingTag) {
console.error(chalk.red(`Tag "${label}" already exists and points to ${existingTag.tsv}`));
console.error(chalk.yellow('Remove the existing tag first with: gati timescape untag ' + label));
process.exit(1);
}
// Create tag
registry.tagVersion(tsv, label, options.createdBy);
saveRegistry(registry);
console.log(chalk.green(`ā Tag "${label}" created successfully.`));
console.log(chalk.gray(` ${tsv} ā ${label}`));
console.log('');
}
catch (error) {
console.error(chalk.red('Failed to create tag:'), error.message);
process.exit(1);
}
});
// Tags Command - List all tags or tags for a specific version
timescapeCommand
.command('tags')
.description('List all tags or tags for a specific version')
.argument('[tsv]', 'Optional: Version identifier (TSV) to show tags for')
.action(async (tsv) => {
if (tsv) {
console.log(chalk.bold.cyan(`\nš·ļø Tags for Version: ${tsv}\n`));
}
else {
console.log(chalk.bold.cyan('\nš·ļø All Tags\n'));
}
try {
const registry = loadRegistry();
if (tsv) {
// Show tags for specific version
if (!tsv.startsWith('tsv:')) {
console.error(chalk.red(`Invalid TSV format: ${tsv}`));
process.exit(1);
}
const tags = registry.getTagsForVersion(tsv);
if (tags.length === 0) {
console.log(chalk.yellow(`No tags found for version ${tsv}`));
return;
}
console.log(chalk.bold('Tags:'));
for (const tag of tags) {
console.log(` ${chalk.green(tag)}`);
}
console.log('');
}
else {
// Show all tags
const allTags = registry.getAllTags();
if (allTags.length === 0) {
console.log(chalk.yellow('No tags found.'));
return;
}
console.log(chalk.bold('Tag ā Version'));
console.log(chalk.gray('ā'.repeat(80)));
for (const tag of allTags) {
const created = formatTimestamp(tag.createdAt);
console.log(`${chalk.green(tag.label.padEnd(20))} ā ${chalk.cyan(tag.tsv)}`);
console.log(`${' '.repeat(20)} Created: ${chalk.gray(created)} by ${chalk.gray(tag.createdBy)}`);
}
console.log('');
console.log(chalk.bold('Total tags:'), chalk.white(allTags.length));
console.log('');
}
}
catch (error) {
console.error(chalk.red('Failed to list tags:'), error.message);
process.exit(1);
}
});
// Untag Command - Remove a tag
timescapeCommand
.command('untag')
.description('Remove a tag')
.argument('<label>', 'Tag label to remove')
.action(async (label) => {
console.log(chalk.bold.cyan(`\nšļø Removing Tag: ${label}\n`));
try {
const registry = loadRegistry();
// Check if tag exists
const tag = registry.getAllTags().find((t) => t.label === label);
if (!tag) {
console.error(chalk.red(`Tag not found: ${label}`));
process.exit(1);
}
// Remove tag
const removed = registry.untagVersion(label);
if (removed) {
saveRegistry(registry);
console.log(chalk.green(`ā Tag "${label}" removed successfully.`));
console.log(chalk.gray(` Was pointing to: ${tag.tsv}`));
console.log('');
}
else {
console.error(chalk.red(`Failed to remove tag: ${label}`));
process.exit(1);
}
}
catch (error) {
console.error(chalk.red('Failed to remove tag:'), error.message);
process.exit(1);
}
});
//# sourceMappingURL=timescape.js.map