alwaysai
Version:
The alwaysAI command-line interface (CLI)
220 lines (190 loc) • 6.04 kB
text/typescript
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;
}