UNPKG

alwaysai

Version:

The alwaysAI command-line interface (CLI)

220 lines (190 loc) 6.04 kB
import { ConfigFileSchema } from '@alwaysai/config-nodejs'; import Ajv, { JSONSchemaType } from 'ajv'; import { join, posix } from 'path'; import { SystemId } from '../constants'; import { ALWAYSAI_SYSTEM_ID } from '../environment'; import { ALWAYSAI_CONFIG_FILE_NAME, LOCAL_AAI_CFG_DIR, REMOTE_AAI_CFG_DIR_LINUX } from '../paths'; import { SshSpawner, logger } from '../util'; export const SYSTEM_IDS = Object.keys(SystemId) as SystemId[]; const path = join(LOCAL_AAI_CFG_DIR, ALWAYSAI_CONFIG_FILE_NAME); export type AaiConfig = { systemId: SystemId; }; export const aaiConfigSchema: JSONSchemaType<AaiConfig> = { type: 'object', properties: { systemId: { type: 'string', enum: SYSTEM_IDS } }, required: ['systemId'] }; const ajv = new Ajv(); const validateAaiConfig = ajv.compile(aaiConfigSchema); function AaiConfigFile(baseDir?: string) { const filePath = join(baseDir ?? path); const configFile = ConfigFileSchema<AaiConfig>({ path: filePath, validateFunction: validateAaiConfig, initialValue: { systemId: 'production' } }); return configFile; } type AaiConfigFileReturnType = ReturnType<typeof AaiConfigFile>; abstract class AaiCfg { aaiConfigFile: AaiConfigFileReturnType; baseDir: string; fullPath: string; constructor(baseDir: string) { this.baseDir = baseDir; } public writeAaiCfgFile(): void { // intentionally empty } public readAaiCfgFile(): void { // intentionally empty } public getFileName(): string { return ALWAYSAI_CONFIG_FILE_NAME; } } export class LocalAaiCfg extends AaiCfg { aaiConfigFile: AaiConfigFileReturnType; baseDir: string; constructor(baseDir: string = LOCAL_AAI_CFG_DIR) { super(baseDir); this.fullPath = join(this.baseDir, this.getFileName()); this.aaiConfigFile = AaiConfigFile(this.fullPath); } public async getValidationErrors() { return validateAaiConfig.errors; } public async writeAaiCfgFile(): Promise<void> { let contents: AaiConfig = { systemId: getSystemId() }; if (this.aaiConfigFile.exists()) { try { // NOTE: readAaiCfgFile will do a validation const origParsed = await this.readAaiCfgFile(); contents = { ...origParsed, ...contents }; } catch (err) { logger.error( `${this.getFileName()} is invalid:\n${JSON.stringify( this.getValidationErrors(), null, 2 )}` ); this.aaiConfigFile.remove(); } } this.aaiConfigFile.write(contents); } public async readAaiCfgFile(): Promise<AaiConfig> { const parsedContents = this.aaiConfigFile.read(); return parsedContents; } public getFileName(): string { return super.getFileName(); } } export class RemoteAaiCfg extends AaiCfg { aaiConfigFile: AaiConfigFileReturnType; baseDir: string; fullPath: string; spawner: SshSpawner; constructor( targetHostName: string, baseDir: string = REMOTE_AAI_CFG_DIR_LINUX ) { super(baseDir); this.fullPath = posix.join(this.baseDir, this.getFileName()); this.spawner = SshSpawner({ targetHostname: targetHostName, targetPath: this.baseDir }); this.aaiConfigFile = AaiConfigFile(this.fullPath); } s; private validate(parsedContents: any) { return this.aaiConfigFile.validate(parsedContents); } public async writeAaiCfgFile(): Promise<void> { let contents = { systemId: getSystemId() }; if (await this.spawner.exists(this.getFileName())) { logger.debug(`${this.getFileName()} already exists, updating file.`); try { // NOTE: readAaiCfgFile will do a validation const origParsed = await this.readAaiCfgFile(); contents = { ...origParsed, ...contents }; } catch (e) { logger.error( `${this.getFileName()} is invalid:\n${JSON.stringify( this.getValidationErrors(), null, 2 )}` ); await this.spawner.rimraf(this.getFileName()); } } await this.spawner.mkdirp(); await this.spawner.writeFile( this.getFileName(), JSON.stringify(contents, null, 2) ); } public async getValidationErrors() { return validateAaiConfig.errors; } public async readAaiCfgFile(): Promise<AaiConfig> { const origContents = await this.spawner.readFile(this.getFileName()); const origParsed = JSON.parse(origContents); if (!this.validate(origParsed)) { throw new Error( `Validation of ${this.getFileName()} failed:\n${JSON.stringify( this.getValidationErrors(), null, 2 )}!` ); } return origParsed; } } /*=================================================================== System ID Usage ===================================================================*/ export function setSystemId(systemId: SystemId) { const localAaiConfigFile = new LocalAaiCfg(); // TODO: replace with pure wrapper functionality return localAaiConfigFile.aaiConfigFile.update((json) => { json.systemId = systemId; }); } export function getSystemId() { const localAaiConfigFile = new LocalAaiCfg(); // TODO: can catch validation errors here and display them const maybeConfig = localAaiConfigFile.aaiConfigFile.readIfExists(); if (ALWAYSAI_SYSTEM_ID) { // When the env var is set return the overridden value without updating the config file. // This prevents one-off commands from having side effects. if (Object.keys(SystemId).includes(ALWAYSAI_SYSTEM_ID)) { if (!maybeConfig?.systemId) { setSystemId(ALWAYSAI_SYSTEM_ID as SystemId); } return ALWAYSAI_SYSTEM_ID as SystemId; } throw new Error(`Invalid ALWAYSAI_SYSTEM_ID: ${ALWAYSAI_SYSTEM_ID}`); } // TODO: replace with pure wrapper functionality if (!maybeConfig?.systemId) { setSystemId('production'); } const systemId = maybeConfig?.systemId ?? 'production'; return systemId as SystemId; }