@orxataguy/tyr 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +408 -0
- package/bin/tyr.js +25 -0
- package/bin/tyr.ts +14 -0
- package/config/map.yml +7 -0
- package/package.json +60 -0
- package/src/commands/di.tyr.ts +113 -0
- package/src/commands/dw.tyr.ts +116 -0
- package/src/commands/install.tyr.ts +135 -0
- package/src/core/Container.ts +56 -0
- package/src/core/Kernel.ts +165 -0
- package/src/core/Logger.ts +48 -0
- package/src/core/TyrError.ts +57 -0
- package/src/core/sys/ai.ts +162 -0
- package/src/core/sys/doc.ts +325 -0
- package/src/core/sys/gen.ts +72 -0
- package/src/core/sys/rem.ts +57 -0
- package/src/lib/DockerManager.ts +108 -0
- package/src/lib/FileSystemManager.ts +152 -0
- package/src/lib/GitManager.ts +76 -0
- package/src/lib/PackageManager.ts +87 -0
- package/src/lib/SQLManager.ts +121 -0
- package/src/lib/ShellManager.ts +118 -0
- package/src/lib/SystemManager.ts +83 -0
- package/src/lib/WebManager.ts +62 -0
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { TyrContext } from '../core/Kernel';
|
|
2
|
+
|
|
3
|
+
export default ({ task, fail, logger, shell, db, git, fs }: TyrContext) => {
|
|
4
|
+
/**
|
|
5
|
+
* @method extractBranchName
|
|
6
|
+
* @description Extrae el nombre de rama de una URL o devuelve el nombre tal cual
|
|
7
|
+
*/
|
|
8
|
+
const extractBranchName = (input: string): string => {
|
|
9
|
+
if (input.includes('/')) {
|
|
10
|
+
const parts = input.split('/');
|
|
11
|
+
return parts[parts.length - 1];
|
|
12
|
+
}
|
|
13
|
+
return input;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
return async (args: string[]) => {
|
|
17
|
+
|
|
18
|
+
// Validación de argumentos
|
|
19
|
+
if (args.length === 0) {
|
|
20
|
+
fail(
|
|
21
|
+
'No se especificó la URL del cliente',
|
|
22
|
+
'Uso: clone-client <url-cliente> [url-rama-opcional]'
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const clientUrl = args[0];
|
|
27
|
+
const branchUrlOrName = args[1] || null;
|
|
28
|
+
|
|
29
|
+
logger.info('Navegando al directorio de clientes...');
|
|
30
|
+
shell.cd('~/dev/wolbenvironment/dev/websITS/clients');
|
|
31
|
+
console.log("Lanzo con url:", clientUrl)
|
|
32
|
+
const broker = await task('Buscando broker en la base de datos', async () => {
|
|
33
|
+
const result = await db.searchBrokerOnDB(clientUrl);
|
|
34
|
+
|
|
35
|
+
if (!result) {
|
|
36
|
+
fail(
|
|
37
|
+
`No se encontró broker para la URL: ${clientUrl}`,
|
|
38
|
+
'Verifica que la URL sea correcta y esté registrada en la BD'
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
logger.success(`Broker encontrado: ${result}`);
|
|
43
|
+
return result;
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
const brokerPath = `~/dev/wolbenvironment/dev/websITS/clients/${broker}`;
|
|
47
|
+
const dirExists = fs.exists(brokerPath);
|
|
48
|
+
|
|
49
|
+
if (dirExists) {
|
|
50
|
+
logger.warn(`El directorio '${broker}' ya existe`);
|
|
51
|
+
|
|
52
|
+
const choice = await shell.input(
|
|
53
|
+
'¿Qué deseas hacer? (s)obrescribir / (m)antener / (r)enombrar: '
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
if (choice.toLowerCase() === 'm' || choice.toLowerCase() === 'mantener') {
|
|
57
|
+
logger.info('Manteniendo directorio existente. Finalizando...');
|
|
58
|
+
return;
|
|
59
|
+
} else if (choice.toLowerCase() === 'r' || choice.toLowerCase() === 'renombrar') {
|
|
60
|
+
await task('Renombrando directorio existente', async () => {
|
|
61
|
+
await shell.exec(`mv ${broker} ${broker}.bak`);
|
|
62
|
+
logger.success(`Directorio renombrado a: ${broker}.bak`);
|
|
63
|
+
});
|
|
64
|
+
} else if (choice.toLowerCase() === 's' || choice.toLowerCase() === 'sobrescribir') {
|
|
65
|
+
await task('Eliminando directorio existente', async () => {
|
|
66
|
+
await shell.exec(`rm -rf ${broker}`);
|
|
67
|
+
logger.success('Directorio eliminado');
|
|
68
|
+
});
|
|
69
|
+
} else {
|
|
70
|
+
fail('Opción no válida', 'Usa: s (sobrescribir), m (mantener) o r (renombrar)');
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const repoUrl = `git@github.com:Avantio/${broker}`;
|
|
75
|
+
logger.info(`Repositorio: ${repoUrl}`);
|
|
76
|
+
|
|
77
|
+
const loader = shell.showLoader('Clonando repositorio desde GitHub...');
|
|
78
|
+
|
|
79
|
+
await task('Clonando repositorio', async () => {
|
|
80
|
+
await git.clone(repoUrl);
|
|
81
|
+
loader.stop();
|
|
82
|
+
logger.success('Repositorio clonado exitosamente');
|
|
83
|
+
}, false, () => loader.stop());
|
|
84
|
+
|
|
85
|
+
shell.cd(broker);
|
|
86
|
+
|
|
87
|
+
let branchName: string;
|
|
88
|
+
|
|
89
|
+
if (branchUrlOrName) {
|
|
90
|
+
branchName = extractBranchName(branchUrlOrName);
|
|
91
|
+
logger.info(`Rama extraída: ${branchName}`);
|
|
92
|
+
} else {
|
|
93
|
+
const answer = await shell.input('🌿 ¿Qué rama quieres usar? (nombre o URL): ');
|
|
94
|
+
branchName = extractBranchName(answer);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
await task(`Cambiando a la rama: ${branchName}`, async () => {
|
|
98
|
+
if (branchName.length > 0) {
|
|
99
|
+
await shell.exec(`git checkout -b ${branchName}`);
|
|
100
|
+
logger.success(`Ahora estás en la rama: ${branchName}`);
|
|
101
|
+
} else {
|
|
102
|
+
logger.info('No se va a generar ninguna rama nueva')
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
logger.success(`Repositorio ${broker} clonado y configurado exitosamente`);
|
|
106
|
+
|
|
107
|
+
};
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
// export const Test = {
|
|
111
|
+
// args: ['https://www.feelporto.com/'],
|
|
112
|
+
// mockInputs: {
|
|
113
|
+
// 'sobrescribir': 'm',
|
|
114
|
+
// 'rama': ''
|
|
115
|
+
// }
|
|
116
|
+
// }
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { TyrContext } from '../core/Kernel';
|
|
2
|
+
import { homedir, platform } from 'os';
|
|
3
|
+
import { existsSync } from 'fs';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
|
|
6
|
+
function detectShellRcFile(homeDir: string): string | null {
|
|
7
|
+
const shell = process.env.SHELL || '';
|
|
8
|
+
|
|
9
|
+
if (shell.includes('zsh')) return path.join(homeDir, '.zshrc');
|
|
10
|
+
if (shell.includes('fish')) return path.join(homeDir, '.config', 'fish', 'config.fish');
|
|
11
|
+
if (shell.includes('bash')) {
|
|
12
|
+
const candidates = [
|
|
13
|
+
path.join(homeDir, '.bash_profile'),
|
|
14
|
+
path.join(homeDir, '.bashrc'),
|
|
15
|
+
];
|
|
16
|
+
return candidates.find(p => existsSync(p)) ?? path.join(homeDir, '.bashrc');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Fallback: try common files in order
|
|
20
|
+
const fallbacks = [
|
|
21
|
+
path.join(homeDir, '.zshrc'),
|
|
22
|
+
path.join(homeDir, '.bashrc'),
|
|
23
|
+
path.join(homeDir, '.bash_profile'),
|
|
24
|
+
];
|
|
25
|
+
return fallbacks.find(p => existsSync(p)) ?? path.join(homeDir, '.bashrc');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async function getPowerShellProfile(shell: any): Promise<string | null> {
|
|
29
|
+
try {
|
|
30
|
+
const profile = await shell.exec('powershell -NoProfile -Command "$PROFILE"').catch(() => null);
|
|
31
|
+
return profile?.trim() || null;
|
|
32
|
+
} catch {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export default ({ task, fail, logger, fs, shell }: TyrContext) => {
|
|
38
|
+
return async (args: string[]) => {
|
|
39
|
+
const homeDir = homedir();
|
|
40
|
+
const isWindows = platform() === 'win32';
|
|
41
|
+
|
|
42
|
+
const tfgPath = path.join(homeDir, 'Projects', 'TyrFramework');
|
|
43
|
+
const addonsPath = path.join(tfgPath, 'local');
|
|
44
|
+
const aliasesTemplatePath = path.join(homeDir, 'avantio', 'framework', 'core', 'include', 'bin', 'aliases.template.sh');
|
|
45
|
+
const pluginsTemplatePath = path.join(homeDir, 'avantio', 'framework', 'core', 'include', 'bin', 'plugins.template.sh');
|
|
46
|
+
|
|
47
|
+
await task('Verificando directorio TFG', async () => {
|
|
48
|
+
if (!fs.exists(tfgPath)) {
|
|
49
|
+
fail(
|
|
50
|
+
`El directorio ${tfgPath} no existe`,
|
|
51
|
+
'Asegúrate de que la ruta ~/Projects/TyrFramework existe'
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
logger.success(`Directorio TFG encontrado: ${tfgPath}`);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
await task('Verificando templates', async () => {
|
|
58
|
+
if (!fs.exists(aliasesTemplatePath)) {
|
|
59
|
+
fail(
|
|
60
|
+
`Template de aliases no encontrado: ${aliasesTemplatePath}`,
|
|
61
|
+
'Verifica que el framework de Avantio esté correctamente instalado'
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (!fs.exists(pluginsTemplatePath)) {
|
|
66
|
+
fail(
|
|
67
|
+
`Template de plugins no encontrado: ${pluginsTemplatePath}`,
|
|
68
|
+
'Verifica que el framework de Avantio esté correctamente instalado'
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
logger.success('Templates encontrados');
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
await task('Creando carpeta local', async () => {
|
|
76
|
+
await fs.createDir(addonsPath);
|
|
77
|
+
logger.success(`Carpeta creada: ${addonsPath}`);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
await task('Copiando aliases.template.sh', async () => {
|
|
81
|
+
const aliasesContent = await fs.read(aliasesTemplatePath);
|
|
82
|
+
if (!aliasesContent) {
|
|
83
|
+
fail('No se pudo leer el contenido de aliases.template.sh');
|
|
84
|
+
}
|
|
85
|
+
await fs.write(path.join(addonsPath, 'aliases.sh'), aliasesContent!);
|
|
86
|
+
logger.success(`Archivo creado: ${path.join(addonsPath, 'aliases.sh')}`);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
await task('Copiando plugins.template.sh', async () => {
|
|
90
|
+
const pluginsContent = await fs.read(pluginsTemplatePath);
|
|
91
|
+
if (!pluginsContent) {
|
|
92
|
+
fail('No se pudo leer el contenido de plugins.template.sh');
|
|
93
|
+
}
|
|
94
|
+
await fs.write(path.join(addonsPath, 'plugins.sh'), pluginsContent!);
|
|
95
|
+
logger.success(`Archivo creado: ${path.join(addonsPath, 'plugins.sh')}`);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
if (isWindows) {
|
|
99
|
+
await task('Configurando perfil de PowerShell', async () => {
|
|
100
|
+
const psProfile = await getPowerShellProfile(shell);
|
|
101
|
+
if (psProfile) {
|
|
102
|
+
const sourceLine = `. "${path.join(addonsPath, 'aliases.sh')}"`;
|
|
103
|
+
await fs.ensureLine(psProfile, sourceLine);
|
|
104
|
+
logger.success(`Aliases añadidos al perfil de PowerShell: ${psProfile}`);
|
|
105
|
+
logger.info('Reinicia PowerShell para aplicar los cambios');
|
|
106
|
+
} else {
|
|
107
|
+
logger.warn('No se pudo detectar el perfil de PowerShell.');
|
|
108
|
+
logger.info(`Añade manualmente a tu perfil: . "${path.join(addonsPath, 'aliases.sh')}"`);
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
} else {
|
|
112
|
+
await task('Configurando shell', async () => {
|
|
113
|
+
const rcFile = detectShellRcFile(homeDir);
|
|
114
|
+
if (!rcFile) {
|
|
115
|
+
logger.warn('No se pudo detectar el archivo de configuración de shell. Configura manualmente.');
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const sourceLine = `source "${path.join(addonsPath, 'aliases.sh')}"`;
|
|
120
|
+
await fs.ensureLine(rcFile, sourceLine);
|
|
121
|
+
logger.success(`Aliases añadidos a: ${rcFile}`);
|
|
122
|
+
logger.info(`Ejecuta: source ${rcFile} (o abre una nueva terminal)`);
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
logger.success('\nEstructura de addons configurada exitosamente');
|
|
127
|
+
logger.info(`\nArchivos creados en: ${addonsPath}`);
|
|
128
|
+
logger.info(' - aliases.sh');
|
|
129
|
+
logger.info(' - plugins.sh');
|
|
130
|
+
};
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
// export const Test = {
|
|
134
|
+
// args: []
|
|
135
|
+
// }
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { ShellManager } from '../lib/ShellManager.js';
|
|
2
|
+
import { FileSystemManager } from '../lib/FileSystemManager.js';
|
|
3
|
+
import { PackageManager } from '../lib/PackageManager.js';
|
|
4
|
+
import { DockerManager } from '../lib/DockerManager.js';
|
|
5
|
+
import { GitManager } from '../lib/GitManager.js';
|
|
6
|
+
import { SystemManager } from '../lib/SystemManager.js';
|
|
7
|
+
import { SQLManager } from '../lib/SQLManager.js';
|
|
8
|
+
import { WebManager } from '../lib/WebManager.js';
|
|
9
|
+
import { Logger, createLogger } from './Logger.js';
|
|
10
|
+
|
|
11
|
+
export type { Logger };
|
|
12
|
+
|
|
13
|
+
export interface ServiceContainer {
|
|
14
|
+
logger: Logger;
|
|
15
|
+
shell: ShellManager;
|
|
16
|
+
fs: FileSystemManager;
|
|
17
|
+
pkg: PackageManager;
|
|
18
|
+
docker: DockerManager;
|
|
19
|
+
git: GitManager;
|
|
20
|
+
sys: SystemManager;
|
|
21
|
+
db: SQLManager;
|
|
22
|
+
web: WebManager;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export class Container {
|
|
26
|
+
private services: Partial<ServiceContainer>;
|
|
27
|
+
|
|
28
|
+
constructor() {
|
|
29
|
+
this.services = {};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
public init(isDebug: boolean): void {
|
|
33
|
+
const logger = createLogger(isDebug);
|
|
34
|
+
const shell = new ShellManager();
|
|
35
|
+
const db = new SQLManager();
|
|
36
|
+
|
|
37
|
+
this.services = {
|
|
38
|
+
logger,
|
|
39
|
+
shell,
|
|
40
|
+
db,
|
|
41
|
+
web: new WebManager(logger),
|
|
42
|
+
fs: new FileSystemManager(logger),
|
|
43
|
+
pkg: new PackageManager(shell, logger),
|
|
44
|
+
docker: new DockerManager(shell, logger),
|
|
45
|
+
git: new GitManager(shell, logger),
|
|
46
|
+
sys: new SystemManager(shell, logger),
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
public get(): ServiceContainer {
|
|
51
|
+
if (!this.services.logger) {
|
|
52
|
+
throw new Error('Container not initialised. Call .init() first.');
|
|
53
|
+
}
|
|
54
|
+
return this.services as ServiceContainer;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import yaml from 'js-yaml';
|
|
4
|
+
import { fileURLToPath } from 'url';
|
|
5
|
+
import { Container } from './Container';
|
|
6
|
+
|
|
7
|
+
import gen from './sys/gen';
|
|
8
|
+
import rem from './sys/rem';
|
|
9
|
+
import doc from './sys/doc';
|
|
10
|
+
import ai from './sys/ai';
|
|
11
|
+
|
|
12
|
+
import { TyrError } from './TyrError';
|
|
13
|
+
|
|
14
|
+
interface TyrConfig {
|
|
15
|
+
commands: Record<string, string>;
|
|
16
|
+
aliases?: Record<string, string>;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface TyrContext {
|
|
20
|
+
frameworkRoot: string;
|
|
21
|
+
logger: any;
|
|
22
|
+
shell: any;
|
|
23
|
+
fs: any;
|
|
24
|
+
docker?: any;
|
|
25
|
+
run: (commandName: string, args?: string[]) => Promise<void>;
|
|
26
|
+
task: <T>(description: string, action: () => Promise<T> | T, next?: boolean, onFail?: () => void) => Promise<T | undefined>;
|
|
27
|
+
fail: (msg: string, suggestion?: string) => never;
|
|
28
|
+
[key: string]: any;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
type CommandFunction = (args: string[]) => Promise<void>;
|
|
32
|
+
type CommandFactory = (context: TyrContext) => CommandFunction;
|
|
33
|
+
|
|
34
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
35
|
+
const __dirname = path.dirname(__filename);
|
|
36
|
+
|
|
37
|
+
export class Kernel {
|
|
38
|
+
private container: Container;
|
|
39
|
+
private config: TyrConfig | null;
|
|
40
|
+
private frameworkRoot: string;
|
|
41
|
+
|
|
42
|
+
constructor() {
|
|
43
|
+
this.container = new Container();
|
|
44
|
+
this.config = null;
|
|
45
|
+
this.frameworkRoot = path.resolve(__dirname, '../../');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
public async boot(args: string[]): Promise<void> {
|
|
49
|
+
const isDebug = args.includes('--debug');
|
|
50
|
+
await this.container.init(isDebug);
|
|
51
|
+
|
|
52
|
+
const configPath = path.resolve(this.frameworkRoot, 'config/map.yml');
|
|
53
|
+
|
|
54
|
+
try {
|
|
55
|
+
const fileContents = fs.readFileSync(configPath, 'utf8');
|
|
56
|
+
this.config = yaml.load(fileContents) as TyrConfig;
|
|
57
|
+
} catch (error) {
|
|
58
|
+
console.error(`Critical error: configuration not found at ${configPath}`);
|
|
59
|
+
process.exit(1);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
public async handle(args: string[]): Promise<void> {
|
|
64
|
+
const commandName = args[0];
|
|
65
|
+
|
|
66
|
+
if (!commandName) {
|
|
67
|
+
console.log("Please provide a command. Example: tyr help");
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const runInternal = async (cmd: string, cmdArgs: string[] = []) => {
|
|
72
|
+
await this.handle([cmd, ...cmdArgs]);
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const task = async <T>(description: string, action: () => Promise<T> | T, next: boolean = false, onFail?: () => void): Promise<T | undefined> => {
|
|
76
|
+
try {
|
|
77
|
+
return await action();
|
|
78
|
+
} catch (e) {
|
|
79
|
+
|
|
80
|
+
if (onFail) {
|
|
81
|
+
onFail();
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (!next) {
|
|
85
|
+
throw new TyrError(
|
|
86
|
+
`Task failed: "${description}"`,
|
|
87
|
+
e,
|
|
88
|
+
"Check the previous logs or the configuration."
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const context: TyrContext = {
|
|
95
|
+
...this.container.get(),
|
|
96
|
+
frameworkRoot: this.frameworkRoot,
|
|
97
|
+
run: runInternal,
|
|
98
|
+
task,
|
|
99
|
+
fail: (msg: string, suggestion?: string) => { throw new TyrError(msg, null, suggestion, commandName); }
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const systemCommands: Record<string, CommandFactory> = {
|
|
103
|
+
gen,
|
|
104
|
+
rem,
|
|
105
|
+
doc,
|
|
106
|
+
ai,
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
if (systemCommands[commandName]) {
|
|
110
|
+
await systemCommands[commandName](context)(args.slice(1));
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (!this.config) {
|
|
115
|
+
throw new Error("Kernel has not been initialized (run boot first).");
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
let scriptPath = this.config.commands[commandName];
|
|
119
|
+
|
|
120
|
+
if (!scriptPath && this.config.aliases?.[commandName]) {
|
|
121
|
+
const aliasTarget = this.config.aliases[commandName];
|
|
122
|
+
scriptPath = this.config.commands[aliasTarget];
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (!scriptPath) {
|
|
126
|
+
context.logger?.error(`Command '${commandName}' not found.`);
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
try {
|
|
131
|
+
const absolutePath = path.resolve(this.frameworkRoot, scriptPath);
|
|
132
|
+
|
|
133
|
+
const module = await import(absolutePath);
|
|
134
|
+
|
|
135
|
+
if (typeof module.default !== 'function') {
|
|
136
|
+
throw new Error(`File ${scriptPath} does not export a default function.`);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const commandFactory: CommandFactory = module.default;
|
|
140
|
+
const command = commandFactory(context);
|
|
141
|
+
|
|
142
|
+
await command(args.slice(1));
|
|
143
|
+
|
|
144
|
+
} catch (error: any) {
|
|
145
|
+
this.handleError(error, args);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
private handleError(error: unknown, args: string[]): void {
|
|
150
|
+
const isDebug = args.includes('--debug');
|
|
151
|
+
const logger = this.container.get().logger;
|
|
152
|
+
const commandName = args[0];
|
|
153
|
+
|
|
154
|
+
if (error instanceof TyrError) {
|
|
155
|
+
const enriched = error.commandName
|
|
156
|
+
? error
|
|
157
|
+
: new TyrError(error.message, error.originalError, error.suggestion, commandName);
|
|
158
|
+
enriched.handle(isDebug, logger);
|
|
159
|
+
} else {
|
|
160
|
+
(new TyrError('Unhandled critical error', error, undefined, commandName)).handle(isDebug, logger);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
process.exit(1);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { homedir } from 'os';
|
|
5
|
+
|
|
6
|
+
export interface Logger {
|
|
7
|
+
log(msg: any): void;
|
|
8
|
+
info(msg: any): void;
|
|
9
|
+
success(msg: any): void;
|
|
10
|
+
error(msg: any): void;
|
|
11
|
+
warn(msg: any): void;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function createLogger(isDebug: boolean): Logger {
|
|
15
|
+
const logDir = path.join(homedir(), '.tyr', 'logs');
|
|
16
|
+
const logFile = path.join(logDir, `${new Date().toISOString().slice(0, 10)}.log`);
|
|
17
|
+
|
|
18
|
+
fs.mkdirSync(logDir, { recursive: true });
|
|
19
|
+
|
|
20
|
+
const writeToFile = (level: string, msg: any) => {
|
|
21
|
+
const timestamp = new Date().toISOString();
|
|
22
|
+
const line = `[${timestamp}] [${level}] ${String(msg)}\n`;
|
|
23
|
+
fs.appendFileSync(logFile, line, 'utf-8');
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
return {
|
|
27
|
+
log: (msg) => {
|
|
28
|
+
console.log(msg);
|
|
29
|
+
writeToFile('LOG', msg);
|
|
30
|
+
},
|
|
31
|
+
info: (msg) => {
|
|
32
|
+
console.log(chalk.blue('ℹ'), msg);
|
|
33
|
+
writeToFile('INFO', msg);
|
|
34
|
+
},
|
|
35
|
+
success: (msg) => {
|
|
36
|
+
console.log(chalk.green('✔'), msg);
|
|
37
|
+
writeToFile('SUCCESS', msg);
|
|
38
|
+
},
|
|
39
|
+
error: (msg) => {
|
|
40
|
+
if (isDebug) console.error(chalk.red('✖'), msg);
|
|
41
|
+
writeToFile('ERROR', msg);
|
|
42
|
+
},
|
|
43
|
+
warn: (msg) => {
|
|
44
|
+
if (isDebug) console.warn(chalk.yellow('⚠'), msg);
|
|
45
|
+
writeToFile('WARN', msg);
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { Logger, createLogger } from './Logger.js';
|
|
2
|
+
|
|
3
|
+
export class TyrError extends Error {
|
|
4
|
+
public readonly originalError: unknown;
|
|
5
|
+
public readonly isTyrError = true;
|
|
6
|
+
public readonly suggestion?: string;
|
|
7
|
+
public readonly commandName?: string;
|
|
8
|
+
|
|
9
|
+
constructor(message: string, originalError?: unknown, suggestion?: string, commandName?: string) {
|
|
10
|
+
super(message);
|
|
11
|
+
this.originalError = originalError;
|
|
12
|
+
this.suggestion = suggestion;
|
|
13
|
+
this.commandName = commandName;
|
|
14
|
+
Error.captureStackTrace(this, this.constructor);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
private extractErrorMessage(err: unknown): string {
|
|
18
|
+
if (err instanceof Error) return err.message;
|
|
19
|
+
if (typeof err === 'string') return err;
|
|
20
|
+
try {
|
|
21
|
+
return JSON.stringify(err);
|
|
22
|
+
} catch {
|
|
23
|
+
return 'Unknown error (non-serializable)';
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
public handle(isDebug: boolean = false, _logger?: Logger): void {
|
|
28
|
+
const logger = _logger ?? createLogger(isDebug);
|
|
29
|
+
|
|
30
|
+
if (this.commandName) {
|
|
31
|
+
logger.error(`Error in command: ${this.commandName}`);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
logger.error('Oops! An error occurred.');
|
|
35
|
+
logger.error(`↳ ${this.message}`);
|
|
36
|
+
|
|
37
|
+
if (this.originalError) {
|
|
38
|
+
logger.error(` ↳ Caused by: ${this.extractErrorMessage(this.originalError)}`);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (this.suggestion) {
|
|
42
|
+
logger.warn(` Suggestion: ${this.suggestion}`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (isDebug) {
|
|
46
|
+
if (this.originalError instanceof Error) {
|
|
47
|
+
logger.log('\n--- Stack Trace ---');
|
|
48
|
+
logger.log(this.originalError.stack);
|
|
49
|
+
} else {
|
|
50
|
+
logger.log('\n--- Stack Trace ---');
|
|
51
|
+
logger.log(this);
|
|
52
|
+
}
|
|
53
|
+
} else {
|
|
54
|
+
logger.log('\n(Use --debug to see the full stack trace)');
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|