162 lines
3.5 KiB
TypeScript
162 lines
3.5 KiB
TypeScript
/**
|
|
* @fileoverview Runtime State Manager Service
|
|
* Manages runtime state separate from configuration
|
|
*/
|
|
|
|
import { promises as fs } from 'node:fs';
|
|
import path from 'node:path';
|
|
import {
|
|
ERROR_CODES,
|
|
TaskMasterError
|
|
} from '../../errors/task-master-error.js';
|
|
import { DEFAULT_CONFIG_VALUES } from '../../interfaces/configuration.interface.js';
|
|
|
|
/**
|
|
* Runtime state data structure
|
|
*/
|
|
export interface RuntimeState {
|
|
/** Currently active tag */
|
|
currentTag: string;
|
|
/** Last updated timestamp */
|
|
lastUpdated?: string;
|
|
/** Additional metadata */
|
|
metadata?: Record<string, unknown>;
|
|
}
|
|
|
|
/**
|
|
* RuntimeStateManager handles runtime state persistence
|
|
* Single responsibility: Runtime state management (separate from config)
|
|
*/
|
|
export class RuntimeStateManager {
|
|
private stateFilePath: string;
|
|
private currentState: RuntimeState;
|
|
|
|
constructor(projectRoot: string) {
|
|
this.stateFilePath = path.join(projectRoot, '.taskmaster', 'state.json');
|
|
this.currentState = {
|
|
currentTag: DEFAULT_CONFIG_VALUES.TAGS.DEFAULT_TAG
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Load runtime state from disk
|
|
*/
|
|
async loadState(): Promise<RuntimeState> {
|
|
try {
|
|
const stateData = await fs.readFile(this.stateFilePath, 'utf-8');
|
|
const rawState = JSON.parse(stateData);
|
|
|
|
// Map legacy field names to current interface
|
|
const state: RuntimeState = {
|
|
currentTag:
|
|
rawState.currentTag ||
|
|
rawState.activeTag ||
|
|
DEFAULT_CONFIG_VALUES.TAGS.DEFAULT_TAG,
|
|
lastUpdated: rawState.lastUpdated,
|
|
metadata: rawState.metadata
|
|
};
|
|
|
|
// Apply environment variable override for current tag
|
|
if (process.env.TASKMASTER_TAG) {
|
|
state.currentTag = process.env.TASKMASTER_TAG;
|
|
}
|
|
|
|
this.currentState = state;
|
|
return state;
|
|
} catch (error: any) {
|
|
if (error.code === 'ENOENT') {
|
|
// State file doesn't exist, use defaults
|
|
console.debug('No state.json found, using default state');
|
|
|
|
// Check environment variable
|
|
if (process.env.TASKMASTER_TAG) {
|
|
this.currentState.currentTag = process.env.TASKMASTER_TAG;
|
|
}
|
|
|
|
return this.currentState;
|
|
}
|
|
|
|
console.warn('Failed to load state file:', error.message);
|
|
return this.currentState;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Save runtime state to disk
|
|
*/
|
|
async saveState(): Promise<void> {
|
|
const stateDir = path.dirname(this.stateFilePath);
|
|
|
|
try {
|
|
await fs.mkdir(stateDir, { recursive: true });
|
|
|
|
const stateToSave = {
|
|
...this.currentState,
|
|
lastUpdated: new Date().toISOString()
|
|
};
|
|
|
|
await fs.writeFile(
|
|
this.stateFilePath,
|
|
JSON.stringify(stateToSave, null, 2),
|
|
'utf-8'
|
|
);
|
|
} catch (error) {
|
|
throw new TaskMasterError(
|
|
'Failed to save runtime state',
|
|
ERROR_CODES.CONFIG_ERROR,
|
|
{ statePath: this.stateFilePath },
|
|
error as Error
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get the currently active tag
|
|
*/
|
|
getCurrentTag(): string {
|
|
return this.currentState.currentTag;
|
|
}
|
|
|
|
/**
|
|
* Set the current tag
|
|
*/
|
|
async setCurrentTag(tag: string): Promise<void> {
|
|
this.currentState.currentTag = tag;
|
|
await this.saveState();
|
|
}
|
|
|
|
/**
|
|
* Get current state
|
|
*/
|
|
getState(): RuntimeState {
|
|
return { ...this.currentState };
|
|
}
|
|
|
|
/**
|
|
* Update metadata
|
|
*/
|
|
async updateMetadata(metadata: Record<string, unknown>): Promise<void> {
|
|
this.currentState.metadata = {
|
|
...this.currentState.metadata,
|
|
...metadata
|
|
};
|
|
await this.saveState();
|
|
}
|
|
|
|
/**
|
|
* Clear state file
|
|
*/
|
|
async clearState(): Promise<void> {
|
|
try {
|
|
await fs.unlink(this.stateFilePath);
|
|
} catch (error: any) {
|
|
if (error.code !== 'ENOENT') {
|
|
throw error;
|
|
}
|
|
}
|
|
this.currentState = {
|
|
currentTag: DEFAULT_CONFIG_VALUES.TAGS.DEFAULT_TAG
|
|
};
|
|
}
|
|
}
|