@orxataguy/tyr 1.0.0 → 1.0.2

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.
@@ -1,165 +1,213 @@
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
- }
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import yaml from 'js-yaml';
4
+ import dotenv from 'dotenv';
5
+ import { fileURLToPath, pathToFileURL } from 'url';
6
+ import { homedir } from 'os';
7
+ import { Container } from './Container';
8
+
9
+ import gen from './sys/gen';
10
+ import rem from './sys/rem';
11
+ import doc from './sys/doc';
12
+ import ai from './sys/ai';
13
+ import config from './sys/config';
14
+
15
+ import { TyrError } from './TyrError';
16
+
17
+ interface TyrConfig {
18
+ commands: Record<string, string>;
19
+ aliases?: Record<string, string>;
20
+ }
21
+
22
+ export interface TyrContext {
23
+ frameworkRoot: string;
24
+ userRoot: string;
25
+ logger: any;
26
+ shell: any;
27
+ fs: any;
28
+ docker?: any;
29
+ run: (commandName: string, args?: string[]) => Promise<void>;
30
+ task: <T>(description: string, action: () => Promise<T> | T, next?: boolean, onFail?: () => void) => Promise<T | undefined>;
31
+ fail: (msg: string, suggestion?: string) => never;
32
+ [key: string]: any;
33
+ }
34
+
35
+ type CommandFunction = (args: string[]) => Promise<void>;
36
+ type CommandFactory = (context: TyrContext) => CommandFunction;
37
+
38
+ const __filename = fileURLToPath(import.meta.url);
39
+ const __dirname = path.dirname(__filename);
40
+
41
+ export class Kernel {
42
+ private container: Container;
43
+ private config: TyrConfig | null;
44
+ private frameworkRoot: string;
45
+ private userRoot: string;
46
+
47
+ constructor() {
48
+ this.container = new Container();
49
+ this.config = null;
50
+ this.frameworkRoot = path.resolve(__dirname, '../../');
51
+ this.userRoot = path.join(homedir(), '.tyr');
52
+ }
53
+
54
+ public async boot(args: string[]): Promise<void> {
55
+ const isDebug = args.includes('--debug');
56
+
57
+ // Load all env vars from ~/.tyr/.env once, before anything else
58
+ (dotenv as any).config({ path: path.join(this.userRoot, '.env'), quiet: true });
59
+
60
+ await this.container.init(isDebug);
61
+
62
+ // All commands live in ~/.tyr/map.yml — the framework ships no runtime commands
63
+ this.config = { commands: {}, aliases: {} };
64
+
65
+ const userConfigPath = path.join(this.userRoot, 'map.yml');
66
+ if (fs.existsSync(userConfigPath)) {
67
+ try {
68
+ const raw = yaml.load(fs.readFileSync(userConfigPath, 'utf8')) as TyrConfig;
69
+ for (const [name, cmdPath] of Object.entries(raw.commands ?? {})) {
70
+ // Absolute paths used as-is; relative paths resolved from userRoot
71
+ this.config.commands[name] = path.isAbsolute(cmdPath)
72
+ ? cmdPath
73
+ : path.resolve(this.userRoot, cmdPath);
74
+ }
75
+ this.config.aliases = raw.aliases ?? {};
76
+ } catch {
77
+ console.error(`Warning: could not load user config at ${userConfigPath}`);
78
+ }
79
+ }
80
+ }
81
+
82
+ public async handle(args: string[]): Promise<void> {
83
+ const commandName = args[0];
84
+
85
+ if (!commandName) {
86
+ console.log('Usage: tyr <command> [args...]');
87
+ console.log(' tyr --config Configure Tyr for the first time');
88
+ console.log(' tyr --version Show version');
89
+ console.log(' tyr --update Update Tyr to the latest version');
90
+ return;
91
+ }
92
+
93
+ // --version / -v
94
+ if (commandName === '--version' || commandName === '-v') {
95
+ const pkgPath = path.resolve(this.frameworkRoot, 'package.json');
96
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
97
+ console.log(`tyr v${pkg.version}`);
98
+ return;
99
+ }
100
+
101
+ // --update
102
+ if (commandName === '--update') {
103
+ const pkgPath = path.resolve(this.frameworkRoot, 'package.json');
104
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
105
+ const shell = this.container.get().shell;
106
+ console.log(`Updating ${pkg.name}...`);
107
+ await shell.exec(`npm update -g ${pkg.name}`);
108
+ console.log('Update complete. Run tyr --version to confirm.');
109
+ return;
110
+ }
111
+
112
+ const runInternal = async (cmd: string, cmdArgs: string[] = []) => {
113
+ await this.handle([cmd, ...cmdArgs]);
114
+ };
115
+
116
+ const task = async <T>(description: string, action: () => Promise<T> | T, next: boolean = false, onFail?: () => void): Promise<T | undefined> => {
117
+ try {
118
+ return await action();
119
+ } catch (e) {
120
+ if (onFail) onFail();
121
+ if (!next) {
122
+ throw new TyrError(
123
+ `Task failed: "${description}"`,
124
+ e,
125
+ 'Check the previous logs or the configuration.'
126
+ );
127
+ }
128
+ }
129
+ };
130
+
131
+ const context: TyrContext = {
132
+ ...this.container.get(),
133
+ frameworkRoot: this.frameworkRoot,
134
+ userRoot: this.userRoot,
135
+ run: runInternal,
136
+ task,
137
+ fail: (msg: string, suggestion?: string) => { throw new TyrError(msg, null, suggestion, commandName); }
138
+ };
139
+
140
+ // --config (needs context for fs/logger)
141
+ if (commandName === '--config') {
142
+ await config(context)(args.slice(1));
143
+ return;
144
+ }
145
+
146
+ const systemCommands: Record<string, CommandFactory> = {
147
+ gen,
148
+ rem,
149
+ doc,
150
+ ai,
151
+ };
152
+
153
+ if (systemCommands[commandName]) {
154
+ await systemCommands[commandName](context)(args.slice(1));
155
+ return;
156
+ }
157
+
158
+ if (!this.config) {
159
+ throw new Error('Kernel has not been initialized (run boot first).');
160
+ }
161
+
162
+ let scriptPath = this.config.commands[commandName];
163
+
164
+ if (!scriptPath && this.config.aliases?.[commandName]) {
165
+ const aliasTarget = this.config.aliases[commandName];
166
+ scriptPath = this.config.commands[aliasTarget];
167
+ }
168
+
169
+ if (!scriptPath) {
170
+ context.logger?.error(`Command '${commandName}' not found.`);
171
+ return;
172
+ }
173
+
174
+ try {
175
+ // Absolute paths (user commands) are used directly; relative paths resolve from frameworkRoot
176
+ const absolutePath = path.isAbsolute(scriptPath)
177
+ ? scriptPath
178
+ : path.resolve(this.frameworkRoot, scriptPath);
179
+
180
+ // Convert to file:// URL — required by ESM on Windows for absolute paths
181
+ const moduleUrl = pathToFileURL(absolutePath).href;
182
+ const module = await import(moduleUrl);
183
+
184
+ if (typeof module.default !== 'function') {
185
+ throw new Error(`File ${scriptPath} does not export a default function.`);
186
+ }
187
+
188
+ const commandFactory: CommandFactory = module.default;
189
+ const command = commandFactory(context);
190
+ await command(args.slice(1));
191
+
192
+ } catch (error: any) {
193
+ this.handleError(error, args);
194
+ }
195
+ }
196
+
197
+ private handleError(error: unknown, args: string[]): void {
198
+ const isDebug = args.includes('--debug');
199
+ const logger = this.container.get().logger;
200
+ const commandName = args[0];
201
+
202
+ if (error instanceof TyrError) {
203
+ const enriched = error.commandName
204
+ ? error
205
+ : new TyrError(error.message, error.originalError, error.suggestion, commandName);
206
+ enriched.handle(isDebug, logger);
207
+ } else {
208
+ (new TyrError('Unhandled critical error', error, undefined, commandName)).handle(isDebug, logger);
209
+ }
210
+
211
+ process.exit(1);
212
+ }
213
+ }
@@ -1,48 +1,51 @@
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
- }
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
+ const writeToFile = (level: string, msg: any) => {
19
+ try {
20
+ fs.mkdirSync(logDir, { recursive: true });
21
+ const timestamp = new Date().toISOString();
22
+ const line = `[${timestamp}] [${level}] ${String(msg)}\n`;
23
+ fs.appendFileSync(logFile, line, 'utf-8');
24
+ } catch {
25
+ // Logging failures must never crash the application
26
+ }
27
+ };
28
+
29
+ return {
30
+ log: (msg) => {
31
+ console.log(msg);
32
+ writeToFile('LOG', msg);
33
+ },
34
+ info: (msg) => {
35
+ console.log(chalk.blue('ℹ'), msg);
36
+ writeToFile('INFO', msg);
37
+ },
38
+ success: (msg) => {
39
+ console.log(chalk.green('✔'), msg);
40
+ writeToFile('SUCCESS', msg);
41
+ },
42
+ error: (msg) => {
43
+ if (isDebug) console.error(chalk.red('✖'), msg);
44
+ writeToFile('ERROR', msg);
45
+ },
46
+ warn: (msg) => {
47
+ if (isDebug) console.warn(chalk.yellow('⚠'), msg);
48
+ writeToFile('WARN', msg);
49
+ },
50
+ };
51
+ }
@@ -1,57 +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
- }
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
+ }