@orxataguy/tyr 1.5.0 → 1.6.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.
- package/LICENSE +21 -21
- package/README.md +408 -408
- package/bin/tyr.ts +13 -13
- package/config/map.yml +4 -7
- package/package.json +60 -62
- package/src/commands/di.tyr.ts +112 -112
- package/src/commands/dw.tyr.ts +115 -115
- package/src/commands/install.tyr.ts +61 -61
- package/src/core/Container.ts +56 -56
- package/src/core/Kernel.ts +1 -1
- package/src/core/Logger.ts +48 -48
- package/src/core/TyrError.ts +57 -57
- package/src/core/sys/ai.ts +162 -162
- package/src/core/sys/config.ts +121 -83
- package/src/core/sys/doc.ts +324 -324
- package/src/lib/DockerManager.ts +108 -108
- package/src/lib/FileSystemManager.ts +152 -152
- package/src/lib/GitManager.ts +75 -75
- package/src/lib/PackageManager.ts +87 -87
- package/src/lib/SQLManager.ts +117 -117
- package/src/lib/ShellManager.ts +117 -117
- package/src/lib/SystemManager.ts +83 -83
- package/src/lib/WebManager.ts +62 -62
package/src/lib/DockerManager.ts
CHANGED
|
@@ -1,108 +1,108 @@
|
|
|
1
|
-
import { ShellManager } from './ShellManager.js';
|
|
2
|
-
import { Logger } from '../core/Logger.js';
|
|
3
|
-
import { TyrError } from '../core/TyrError.js';
|
|
4
|
-
|
|
5
|
-
export interface DockerRunOptions {
|
|
6
|
-
image: string;
|
|
7
|
-
name: string;
|
|
8
|
-
port?: string;
|
|
9
|
-
env?: string[];
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* @class DockerManager
|
|
14
|
-
* @description Low-level manager for interacting with the Docker Daemon.
|
|
15
|
-
* Allows starting individual containers, checking states, and managing Docker Compose stacks.
|
|
16
|
-
*/
|
|
17
|
-
export class DockerManager {
|
|
18
|
-
private shell: ShellManager;
|
|
19
|
-
private logger: Logger;
|
|
20
|
-
|
|
21
|
-
constructor(shell: ShellManager, logger: Logger) {
|
|
22
|
-
this.shell = shell;
|
|
23
|
-
this.logger = logger;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* @method isRunning
|
|
28
|
-
* @description Checks whether the Docker service is active and responding on the host system.
|
|
29
|
-
* @returns {Promise<boolean>} True if Docker is running.
|
|
30
|
-
* @example
|
|
31
|
-
* const active = await docker.isRunning();
|
|
32
|
-
* if (!active) fail('Start Docker first.');
|
|
33
|
-
*/
|
|
34
|
-
public async isRunning(): Promise<boolean> {
|
|
35
|
-
try {
|
|
36
|
-
await this.shell.exec('docker info');
|
|
37
|
-
return true;
|
|
38
|
-
} catch (e) {
|
|
39
|
-
return false;
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* @method run
|
|
45
|
-
* @description Deploys an individual container in detached mode. If it already exists, restarts it.
|
|
46
|
-
* @param {DockerRunOptions} config - Deployment configuration.
|
|
47
|
-
* @example
|
|
48
|
-
* await docker.run({ name: 'my-db', image: 'mongo:latest', port: '27017:27017' });
|
|
49
|
-
*/
|
|
50
|
-
public async run({ image, name, port, env = [] }: DockerRunOptions): Promise<void> {
|
|
51
|
-
this.logger.info(`Starting container: ${name} (${image})...`);
|
|
52
|
-
try {
|
|
53
|
-
if (await this.containerExists(name)) {
|
|
54
|
-
this.logger.warn(`Container ${name} already exists. Restarting...`);
|
|
55
|
-
await this.shell.exec(`docker rm -f ${name}`);
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
const envFlags = env.map(e => `-e ${e}`).join(' ');
|
|
59
|
-
const portMapping = port ? `-p ${port}` : '';
|
|
60
|
-
const containerId = await this.shell.exec(`docker run -d --name ${name} ${portMapping} ${envFlags} ${image}`);
|
|
61
|
-
this.logger.success(`Container active. ID: ${containerId.substring(0, 12)}`);
|
|
62
|
-
} catch (e) {
|
|
63
|
-
if (e instanceof TyrError) throw e;
|
|
64
|
-
throw new TyrError(`Could not start container: ${name}`, e, 'Check that Docker is running and the image exists.');
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* @method containerExists
|
|
70
|
-
* @description Checks whether a specific container exists (running or stopped).
|
|
71
|
-
* @param {string} name - The container name to look for.
|
|
72
|
-
* @returns {Promise<boolean>} True if it exists.
|
|
73
|
-
* @example
|
|
74
|
-
* if (await docker.containerExists('my-app')) { ... }
|
|
75
|
-
*/
|
|
76
|
-
public async containerExists(name: string): Promise<boolean> {
|
|
77
|
-
try {
|
|
78
|
-
await this.shell.exec(`docker inspect ${name}`);
|
|
79
|
-
return true;
|
|
80
|
-
} catch (e) {
|
|
81
|
-
return false;
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
/**
|
|
86
|
-
* @method composeUp
|
|
87
|
-
* @description Starts a full stack using a docker-compose file.
|
|
88
|
-
* @param {string} file - Relative path to the compose file.
|
|
89
|
-
* @example
|
|
90
|
-
* await docker.composeUp('infrastructure/db-compose.yml');
|
|
91
|
-
*/
|
|
92
|
-
public async composeUp(file: string = 'docker-compose.yml'): Promise<void> {
|
|
93
|
-
this.logger.info(`Starting stack from ${file}...`);
|
|
94
|
-
try {
|
|
95
|
-
await this.shell.exec(`docker-compose -f ${file} up -d`);
|
|
96
|
-
this.logger.success('Stack deployed successfully.');
|
|
97
|
-
} catch (e) {
|
|
98
|
-
if (e instanceof TyrError) throw e;
|
|
99
|
-
throw new TyrError(`Could not start Docker Compose stack from: ${file}`, e, 'Check that the compose file exists and is valid.');
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
export const DockerManagerTests = {
|
|
105
|
-
// isRunning: {},
|
|
106
|
-
// run: { image: 'alpine:latest', name: 'tyr-test-container', env: [] },
|
|
107
|
-
// containerExists: { name: 'tyr-test-container' },
|
|
108
|
-
};
|
|
1
|
+
import { ShellManager } from './ShellManager.js';
|
|
2
|
+
import { Logger } from '../core/Logger.js';
|
|
3
|
+
import { TyrError } from '../core/TyrError.js';
|
|
4
|
+
|
|
5
|
+
export interface DockerRunOptions {
|
|
6
|
+
image: string;
|
|
7
|
+
name: string;
|
|
8
|
+
port?: string;
|
|
9
|
+
env?: string[];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* @class DockerManager
|
|
14
|
+
* @description Low-level manager for interacting with the Docker Daemon.
|
|
15
|
+
* Allows starting individual containers, checking states, and managing Docker Compose stacks.
|
|
16
|
+
*/
|
|
17
|
+
export class DockerManager {
|
|
18
|
+
private shell: ShellManager;
|
|
19
|
+
private logger: Logger;
|
|
20
|
+
|
|
21
|
+
constructor(shell: ShellManager, logger: Logger) {
|
|
22
|
+
this.shell = shell;
|
|
23
|
+
this.logger = logger;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* @method isRunning
|
|
28
|
+
* @description Checks whether the Docker service is active and responding on the host system.
|
|
29
|
+
* @returns {Promise<boolean>} True if Docker is running.
|
|
30
|
+
* @example
|
|
31
|
+
* const active = await docker.isRunning();
|
|
32
|
+
* if (!active) fail('Start Docker first.');
|
|
33
|
+
*/
|
|
34
|
+
public async isRunning(): Promise<boolean> {
|
|
35
|
+
try {
|
|
36
|
+
await this.shell.exec('docker info');
|
|
37
|
+
return true;
|
|
38
|
+
} catch (e) {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* @method run
|
|
45
|
+
* @description Deploys an individual container in detached mode. If it already exists, restarts it.
|
|
46
|
+
* @param {DockerRunOptions} config - Deployment configuration.
|
|
47
|
+
* @example
|
|
48
|
+
* await docker.run({ name: 'my-db', image: 'mongo:latest', port: '27017:27017' });
|
|
49
|
+
*/
|
|
50
|
+
public async run({ image, name, port, env = [] }: DockerRunOptions): Promise<void> {
|
|
51
|
+
this.logger.info(`Starting container: ${name} (${image})...`);
|
|
52
|
+
try {
|
|
53
|
+
if (await this.containerExists(name)) {
|
|
54
|
+
this.logger.warn(`Container ${name} already exists. Restarting...`);
|
|
55
|
+
await this.shell.exec(`docker rm -f ${name}`);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const envFlags = env.map(e => `-e ${e}`).join(' ');
|
|
59
|
+
const portMapping = port ? `-p ${port}` : '';
|
|
60
|
+
const containerId = await this.shell.exec(`docker run -d --name ${name} ${portMapping} ${envFlags} ${image}`);
|
|
61
|
+
this.logger.success(`Container active. ID: ${containerId.substring(0, 12)}`);
|
|
62
|
+
} catch (e) {
|
|
63
|
+
if (e instanceof TyrError) throw e;
|
|
64
|
+
throw new TyrError(`Could not start container: ${name}`, e, 'Check that Docker is running and the image exists.');
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* @method containerExists
|
|
70
|
+
* @description Checks whether a specific container exists (running or stopped).
|
|
71
|
+
* @param {string} name - The container name to look for.
|
|
72
|
+
* @returns {Promise<boolean>} True if it exists.
|
|
73
|
+
* @example
|
|
74
|
+
* if (await docker.containerExists('my-app')) { ... }
|
|
75
|
+
*/
|
|
76
|
+
public async containerExists(name: string): Promise<boolean> {
|
|
77
|
+
try {
|
|
78
|
+
await this.shell.exec(`docker inspect ${name}`);
|
|
79
|
+
return true;
|
|
80
|
+
} catch (e) {
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* @method composeUp
|
|
87
|
+
* @description Starts a full stack using a docker-compose file.
|
|
88
|
+
* @param {string} file - Relative path to the compose file.
|
|
89
|
+
* @example
|
|
90
|
+
* await docker.composeUp('infrastructure/db-compose.yml');
|
|
91
|
+
*/
|
|
92
|
+
public async composeUp(file: string = 'docker-compose.yml'): Promise<void> {
|
|
93
|
+
this.logger.info(`Starting stack from ${file}...`);
|
|
94
|
+
try {
|
|
95
|
+
await this.shell.exec(`docker-compose -f ${file} up -d`);
|
|
96
|
+
this.logger.success('Stack deployed successfully.');
|
|
97
|
+
} catch (e) {
|
|
98
|
+
if (e instanceof TyrError) throw e;
|
|
99
|
+
throw new TyrError(`Could not start Docker Compose stack from: ${file}`, e, 'Check that the compose file exists and is valid.');
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export const DockerManagerTests = {
|
|
105
|
+
// isRunning: {},
|
|
106
|
+
// run: { image: 'alpine:latest', name: 'tyr-test-container', env: [] },
|
|
107
|
+
// containerExists: { name: 'tyr-test-container' },
|
|
108
|
+
};
|
|
@@ -1,152 +1,152 @@
|
|
|
1
|
-
import fs from 'fs/promises';
|
|
2
|
-
import { existsSync } from 'fs';
|
|
3
|
-
import { homedir } from 'os';
|
|
4
|
-
import path from 'path';
|
|
5
|
-
|
|
6
|
-
import { Logger } from '../core/Logger.js';
|
|
7
|
-
import { TyrError } from '../core/TyrError.js';
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* @class FileSystemManager
|
|
11
|
-
* @description Abstraction layer over the file system (fs).
|
|
12
|
-
* Includes safety utilities such as automatic backups when overwriting and idempotent writes.
|
|
13
|
-
*/
|
|
14
|
-
export class FileSystemManager {
|
|
15
|
-
private logger: Logger;
|
|
16
|
-
|
|
17
|
-
constructor(logger: Logger) {
|
|
18
|
-
this.logger = logger;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
private resolvePath(filePath: string): string {
|
|
22
|
-
return filePath.startsWith('~/')
|
|
23
|
-
? path.join(homedir(), filePath.slice(2))
|
|
24
|
-
: filePath;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* @method exists
|
|
29
|
-
* @description Synchronously checks whether a file or directory exists at the given path.
|
|
30
|
-
* @param {string} filePath - Relative or absolute path to check.
|
|
31
|
-
* @returns {boolean} True if the file exists.
|
|
32
|
-
* @example
|
|
33
|
-
* if (fs.exists('./config.json')) {
|
|
34
|
-
* logger.info('Config found.');
|
|
35
|
-
* }
|
|
36
|
-
*/
|
|
37
|
-
public exists(filePath: string): boolean {
|
|
38
|
-
const resolvedPath = this.resolvePath(filePath);
|
|
39
|
-
return existsSync(resolvedPath);
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* @method read
|
|
44
|
-
* @description Reads the content of a file in UTF-8 format. Returns null if the file does not exist.
|
|
45
|
-
* @param {string} filePath - Path to the file.
|
|
46
|
-
* @returns {Promise<string|null>} File content or null if it does not exist.
|
|
47
|
-
* @example
|
|
48
|
-
* const content = await fs.read('.env');
|
|
49
|
-
*/
|
|
50
|
-
public async read(filePath: string): Promise<string | null> {
|
|
51
|
-
const resolvedPath = this.resolvePath(filePath);
|
|
52
|
-
try {
|
|
53
|
-
return await fs.readFile(resolvedPath, 'utf-8');
|
|
54
|
-
} catch (e) {
|
|
55
|
-
if ((e as NodeJS.ErrnoException).code === 'ENOENT') return null;
|
|
56
|
-
throw new TyrError(`Could not read file: ${filePath}`, e, 'Check that the file exists and has read permissions.');
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* @method delete
|
|
62
|
-
* @description Deletes a file if it exists.
|
|
63
|
-
* @param {string} filePath - Path to the file to delete.
|
|
64
|
-
* @example
|
|
65
|
-
* await fs.delete('./temp/cache.log');
|
|
66
|
-
*/
|
|
67
|
-
public async delete(filePath: string): Promise<void> {
|
|
68
|
-
const resolvedPath = this.resolvePath(filePath);
|
|
69
|
-
if (!this.exists(resolvedPath)) {
|
|
70
|
-
throw new TyrError(`Cannot delete: file not found: ${filePath}`, null, 'Check that the path is correct.');
|
|
71
|
-
}
|
|
72
|
-
try {
|
|
73
|
-
await fs.unlink(resolvedPath);
|
|
74
|
-
this.logger.success(`File deleted: ${filePath}`);
|
|
75
|
-
} catch (e) {
|
|
76
|
-
throw new TyrError(`Could not delete file: ${filePath}`, e);
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
/**
|
|
81
|
-
* @method write
|
|
82
|
-
* @description Writes content to a file. If the file already exists, creates a .bak backup before overwriting.
|
|
83
|
-
* @param {string} filePath - Destination path.
|
|
84
|
-
* @param {string} content - Text content to write.
|
|
85
|
-
* @example
|
|
86
|
-
* await fs.write('src/config.js', 'export const port = 3000;');
|
|
87
|
-
*/
|
|
88
|
-
public async write(filePath: string, content: string): Promise<void> {
|
|
89
|
-
const resolvedPath = this.resolvePath(filePath);
|
|
90
|
-
try {
|
|
91
|
-
const dir = path.dirname(resolvedPath);
|
|
92
|
-
await fs.mkdir(dir, { recursive: true });
|
|
93
|
-
|
|
94
|
-
if (this.exists(resolvedPath)) {
|
|
95
|
-
const backupPath = `${resolvedPath}.bak`;
|
|
96
|
-
await fs.copyFile(resolvedPath, backupPath);
|
|
97
|
-
this.logger.info(`Backup created at: ${backupPath}`);
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
await fs.writeFile(resolvedPath, content, 'utf-8');
|
|
101
|
-
this.logger.success(`File written: ${filePath}`);
|
|
102
|
-
} catch (e) {
|
|
103
|
-
if (e instanceof TyrError) throw e;
|
|
104
|
-
throw new TyrError(`Could not write file: ${filePath}`, e, 'Check write permissions on the destination directory.');
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
/**
|
|
109
|
-
* @method createDir
|
|
110
|
-
* @description Creates a directory recursively (like mkdir -p). Does nothing if it already exists.
|
|
111
|
-
* @param {string} dirPath - Path of the directory to create.
|
|
112
|
-
* @example
|
|
113
|
-
* await fs.createDir('src/controllers/api/v1');
|
|
114
|
-
*/
|
|
115
|
-
public async createDir(dirPath: string): Promise<void> {
|
|
116
|
-
const resolvedPath = this.resolvePath(dirPath);
|
|
117
|
-
if (this.exists(resolvedPath)) return;
|
|
118
|
-
try {
|
|
119
|
-
await fs.mkdir(resolvedPath, { recursive: true });
|
|
120
|
-
this.logger.info(`Directory created: ${dirPath}`);
|
|
121
|
-
} catch (e) {
|
|
122
|
-
throw new TyrError(`Could not create directory: ${dirPath}`, e, 'Check write permissions on the parent directory.');
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
/**
|
|
127
|
-
* @method ensureLine
|
|
128
|
-
* @description Ensures that a specific line exists in a file. Useful for adding environment variables or config entries without duplicating them.
|
|
129
|
-
* @param {string} filePath - Path to the file.
|
|
130
|
-
* @param {string} line - The exact line to ensure.
|
|
131
|
-
* @example
|
|
132
|
-
* await fs.ensureLine('.env', 'PORT=8080');
|
|
133
|
-
*/
|
|
134
|
-
public async ensureLine(filePath: string, line: string): Promise<void> {
|
|
135
|
-
const resolvedPath = this.resolvePath(filePath);
|
|
136
|
-
const content = (await this.read(resolvedPath)) || '';
|
|
137
|
-
if (content.includes(line)) {
|
|
138
|
-
this.logger.info(`Line already present in ${filePath}. Skipping.`);
|
|
139
|
-
return;
|
|
140
|
-
}
|
|
141
|
-
const newContent = content.endsWith('\n') ? content + line : content + '\n' + line;
|
|
142
|
-
await this.write(filePath, newContent);
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
export const FileSystemManagerTests = {
|
|
147
|
-
exists: { filePath: '~/Projects/TyrFramework/package.json' },
|
|
148
|
-
read: { filePath: '~/Projects/TyrFramework/package.json' },
|
|
149
|
-
write: { filePath: '~/Projects/TyrFramework/tests/foo.test.txt', content: 'Test content from TyrFramework' },
|
|
150
|
-
delete: { filePath: '~/Projects/TyrFramework/tests/foo.test.txt' },
|
|
151
|
-
ensureLine: { filePath: '~/Projects/TyrFramework/package.json', line: '"type": "module",' },
|
|
152
|
-
};
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import { existsSync } from 'fs';
|
|
3
|
+
import { homedir } from 'os';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
|
|
6
|
+
import { Logger } from '../core/Logger.js';
|
|
7
|
+
import { TyrError } from '../core/TyrError.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @class FileSystemManager
|
|
11
|
+
* @description Abstraction layer over the file system (fs).
|
|
12
|
+
* Includes safety utilities such as automatic backups when overwriting and idempotent writes.
|
|
13
|
+
*/
|
|
14
|
+
export class FileSystemManager {
|
|
15
|
+
private logger: Logger;
|
|
16
|
+
|
|
17
|
+
constructor(logger: Logger) {
|
|
18
|
+
this.logger = logger;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
private resolvePath(filePath: string): string {
|
|
22
|
+
return filePath.startsWith('~/')
|
|
23
|
+
? path.join(homedir(), filePath.slice(2))
|
|
24
|
+
: filePath;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* @method exists
|
|
29
|
+
* @description Synchronously checks whether a file or directory exists at the given path.
|
|
30
|
+
* @param {string} filePath - Relative or absolute path to check.
|
|
31
|
+
* @returns {boolean} True if the file exists.
|
|
32
|
+
* @example
|
|
33
|
+
* if (fs.exists('./config.json')) {
|
|
34
|
+
* logger.info('Config found.');
|
|
35
|
+
* }
|
|
36
|
+
*/
|
|
37
|
+
public exists(filePath: string): boolean {
|
|
38
|
+
const resolvedPath = this.resolvePath(filePath);
|
|
39
|
+
return existsSync(resolvedPath);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* @method read
|
|
44
|
+
* @description Reads the content of a file in UTF-8 format. Returns null if the file does not exist.
|
|
45
|
+
* @param {string} filePath - Path to the file.
|
|
46
|
+
* @returns {Promise<string|null>} File content or null if it does not exist.
|
|
47
|
+
* @example
|
|
48
|
+
* const content = await fs.read('.env');
|
|
49
|
+
*/
|
|
50
|
+
public async read(filePath: string): Promise<string | null> {
|
|
51
|
+
const resolvedPath = this.resolvePath(filePath);
|
|
52
|
+
try {
|
|
53
|
+
return await fs.readFile(resolvedPath, 'utf-8');
|
|
54
|
+
} catch (e) {
|
|
55
|
+
if ((e as NodeJS.ErrnoException).code === 'ENOENT') return null;
|
|
56
|
+
throw new TyrError(`Could not read file: ${filePath}`, e, 'Check that the file exists and has read permissions.');
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* @method delete
|
|
62
|
+
* @description Deletes a file if it exists.
|
|
63
|
+
* @param {string} filePath - Path to the file to delete.
|
|
64
|
+
* @example
|
|
65
|
+
* await fs.delete('./temp/cache.log');
|
|
66
|
+
*/
|
|
67
|
+
public async delete(filePath: string): Promise<void> {
|
|
68
|
+
const resolvedPath = this.resolvePath(filePath);
|
|
69
|
+
if (!this.exists(resolvedPath)) {
|
|
70
|
+
throw new TyrError(`Cannot delete: file not found: ${filePath}`, null, 'Check that the path is correct.');
|
|
71
|
+
}
|
|
72
|
+
try {
|
|
73
|
+
await fs.unlink(resolvedPath);
|
|
74
|
+
this.logger.success(`File deleted: ${filePath}`);
|
|
75
|
+
} catch (e) {
|
|
76
|
+
throw new TyrError(`Could not delete file: ${filePath}`, e);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* @method write
|
|
82
|
+
* @description Writes content to a file. If the file already exists, creates a .bak backup before overwriting.
|
|
83
|
+
* @param {string} filePath - Destination path.
|
|
84
|
+
* @param {string} content - Text content to write.
|
|
85
|
+
* @example
|
|
86
|
+
* await fs.write('src/config.js', 'export const port = 3000;');
|
|
87
|
+
*/
|
|
88
|
+
public async write(filePath: string, content: string): Promise<void> {
|
|
89
|
+
const resolvedPath = this.resolvePath(filePath);
|
|
90
|
+
try {
|
|
91
|
+
const dir = path.dirname(resolvedPath);
|
|
92
|
+
await fs.mkdir(dir, { recursive: true });
|
|
93
|
+
|
|
94
|
+
if (this.exists(resolvedPath)) {
|
|
95
|
+
const backupPath = `${resolvedPath}.bak`;
|
|
96
|
+
await fs.copyFile(resolvedPath, backupPath);
|
|
97
|
+
this.logger.info(`Backup created at: ${backupPath}`);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
await fs.writeFile(resolvedPath, content, 'utf-8');
|
|
101
|
+
this.logger.success(`File written: ${filePath}`);
|
|
102
|
+
} catch (e) {
|
|
103
|
+
if (e instanceof TyrError) throw e;
|
|
104
|
+
throw new TyrError(`Could not write file: ${filePath}`, e, 'Check write permissions on the destination directory.');
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* @method createDir
|
|
110
|
+
* @description Creates a directory recursively (like mkdir -p). Does nothing if it already exists.
|
|
111
|
+
* @param {string} dirPath - Path of the directory to create.
|
|
112
|
+
* @example
|
|
113
|
+
* await fs.createDir('src/controllers/api/v1');
|
|
114
|
+
*/
|
|
115
|
+
public async createDir(dirPath: string): Promise<void> {
|
|
116
|
+
const resolvedPath = this.resolvePath(dirPath);
|
|
117
|
+
if (this.exists(resolvedPath)) return;
|
|
118
|
+
try {
|
|
119
|
+
await fs.mkdir(resolvedPath, { recursive: true });
|
|
120
|
+
this.logger.info(`Directory created: ${dirPath}`);
|
|
121
|
+
} catch (e) {
|
|
122
|
+
throw new TyrError(`Could not create directory: ${dirPath}`, e, 'Check write permissions on the parent directory.');
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* @method ensureLine
|
|
128
|
+
* @description Ensures that a specific line exists in a file. Useful for adding environment variables or config entries without duplicating them.
|
|
129
|
+
* @param {string} filePath - Path to the file.
|
|
130
|
+
* @param {string} line - The exact line to ensure.
|
|
131
|
+
* @example
|
|
132
|
+
* await fs.ensureLine('.env', 'PORT=8080');
|
|
133
|
+
*/
|
|
134
|
+
public async ensureLine(filePath: string, line: string): Promise<void> {
|
|
135
|
+
const resolvedPath = this.resolvePath(filePath);
|
|
136
|
+
const content = (await this.read(resolvedPath)) || '';
|
|
137
|
+
if (content.includes(line)) {
|
|
138
|
+
this.logger.info(`Line already present in ${filePath}. Skipping.`);
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
const newContent = content.endsWith('\n') ? content + line : content + '\n' + line;
|
|
142
|
+
await this.write(filePath, newContent);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export const FileSystemManagerTests = {
|
|
147
|
+
exists: { filePath: '~/Projects/TyrFramework/package.json' },
|
|
148
|
+
read: { filePath: '~/Projects/TyrFramework/package.json' },
|
|
149
|
+
write: { filePath: '~/Projects/TyrFramework/tests/foo.test.txt', content: 'Test content from TyrFramework' },
|
|
150
|
+
delete: { filePath: '~/Projects/TyrFramework/tests/foo.test.txt' },
|
|
151
|
+
ensureLine: { filePath: '~/Projects/TyrFramework/package.json', line: '"type": "module",' },
|
|
152
|
+
};
|