@orxataguy/tyr 1.0.4 → 1.0.7

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@orxataguy/tyr",
3
- "version": "1.0.4",
3
+ "version": "1.0.7",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "tyr": "./bin/tyr.js"
@@ -10,8 +10,7 @@
10
10
  },
11
11
  "files": [
12
12
  "bin/",
13
- "src/",
14
- "config/"
13
+ "src/"
15
14
  ],
16
15
  "scripts": {
17
16
  "test": "vitest run",
@@ -6,6 +6,8 @@ import { GitManager } from '../lib/GitManager.js';
6
6
  import { SystemManager } from '../lib/SystemManager.js';
7
7
  import { SQLManager } from '../lib/SQLManager.js';
8
8
  import { WebManager } from '../lib/WebManager.js';
9
+ import { WorkspaceManager } from '../lib/WorkspaceManager.js';
10
+ import { JiraManager } from '../lib/JiraManager.js';
9
11
  import { Logger, createLogger } from './Logger.js';
10
12
 
11
13
  export type { Logger };
@@ -20,6 +22,8 @@ export interface ServiceContainer {
20
22
  sys: SystemManager;
21
23
  db: SQLManager;
22
24
  web: WebManager;
25
+ workspace: WorkspaceManager;
26
+ jira: JiraManager;
23
27
  }
24
28
 
25
29
  export class Container {
@@ -33,17 +37,21 @@ export class Container {
33
37
  const logger = createLogger(isDebug);
34
38
  const shell = new ShellManager();
35
39
  const db = new SQLManager();
40
+ const web = new WebManager(logger);
41
+ const fs = new FileSystemManager(logger);
36
42
 
37
43
  this.services = {
38
44
  logger,
39
45
  shell,
40
46
  db,
41
- web: new WebManager(logger),
42
- fs: new FileSystemManager(logger),
47
+ web,
48
+ fs,
43
49
  pkg: new PackageManager(shell, logger),
44
50
  docker: new DockerManager(shell, logger),
45
51
  git: new GitManager(shell, logger),
46
52
  sys: new SystemManager(shell, logger),
53
+ workspace: new WorkspaceManager(shell, fs, logger),
54
+ jira: new JiraManager(web, shell, logger),
47
55
  };
48
56
  }
49
57
 
@@ -86,7 +86,8 @@ export class Kernel {
86
86
  console.log('Usage: tyr <command> [args...]');
87
87
  console.log(' tyr --config Configure Tyr for the first time');
88
88
  console.log(' tyr --version Show version');
89
- console.log(' tyr --update Update Tyr to the latest version');
89
+ console.log(' tyr --update Pull latest changes from the linked ~/.tyr repo');
90
+ console.log(' tyr --upgrade Upgrade Tyr to the latest npm version');
90
91
  return;
91
92
  }
92
93
 
@@ -98,14 +99,30 @@ export class Kernel {
98
99
  return;
99
100
  }
100
101
 
101
- // --update
102
+ // --update: pull latest changes from the linked ~/.tyr git repo
102
103
  if (commandName === '--update') {
104
+ const shell = this.container.get().shell;
105
+ const gitDir = path.join(this.userRoot, '.git');
106
+ if (!fs.existsSync(gitDir)) {
107
+ console.log('~/.tyr no está vinculado a ningún repositorio git.');
108
+ console.log('Ejecuta: tyr --config --repo <url> para vincularlo.');
109
+ return;
110
+ }
111
+ console.log('Actualizando ~/.tyr desde el repositorio...');
112
+ shell.cd(this.userRoot);
113
+ await shell.exec('git pull');
114
+ console.log('Actualización completada.');
115
+ return;
116
+ }
117
+
118
+ // --upgrade: update the Tyr npm package itself
119
+ if (commandName === '--upgrade') {
103
120
  const pkgPath = path.resolve(this.frameworkRoot, 'package.json');
104
121
  const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
105
122
  const shell = this.container.get().shell;
106
- console.log(`Updating ${pkg.name}...`);
123
+ console.log(`Actualizando ${pkg.name}...`);
107
124
  await shell.exec(`npm update -g ${pkg.name}`);
108
- console.log('Update complete. Run tyr --version to confirm.');
125
+ console.log('Actualización completada. Ejecuta tyr --version para confirmar.');
109
126
  return;
110
127
  }
111
128
 
@@ -111,7 +111,7 @@ REGLAS:export default;async(args:string[]);task() p/errores;fail() p/validar;Tes
111
111
  Responde SOLO código TS sin explicaciones ni backticks.`;
112
112
  }
113
113
 
114
- export default function ai({ logger, fs: tyrFs, frameworkRoot, run, fail }: TyrContext) {
114
+ export default function ai({ logger, fs: tyrFs, userRoot, run, fail }: TyrContext) {
115
115
  return async (args: string[]) => {
116
116
  const commandName = args[0];
117
117
  const prompt = args.slice(1).join(' ');
@@ -153,8 +153,8 @@ export default function ai({ logger, fs: tyrFs, frameworkRoot, run, fail }: TyrC
153
153
  );
154
154
  }
155
155
 
156
- const filePath = path.resolve(frameworkRoot, 'src/commands', `${commandName}.tyr.ts`);
156
+ const filePath = path.join(userRoot, 'commands', `${commandName}.tyr.ts`);
157
157
  await tyrFs.write(filePath, code);
158
- logger.success(`'${commandName}' -> src/commands/${commandName}.tyr.ts`);
158
+ logger.success(`'${commandName}' -> ~/.tyr/commands/${commandName}.tyr.ts`);
159
159
  };
160
160
  }
@@ -59,6 +59,33 @@ function detectShellRcFile(homeDir: string): string | null {
59
59
  return fallbacks.find(p => existsSync(p)) ?? path.join(homeDir, '.bashrc');
60
60
  }
61
61
 
62
+ const PACKAGE_JSON_TEMPLATE = `{
63
+ "name": "tyr-commands",
64
+ "version": "1.0.0",
65
+ "type": "module",
66
+ "private": true,
67
+ "description": "Comandos personalizados de Tyr (~/.tyr/)",
68
+ "dependencies": {
69
+ "@orxataguy/tyr": "latest"
70
+ }
71
+ }
72
+ `;
73
+
74
+ const TSCONFIG_TEMPLATE = `{
75
+ "compilerOptions": {
76
+ "target": "ESNext",
77
+ "module": "ESNext",
78
+ "moduleResolution": "node",
79
+ "esModuleInterop": true,
80
+ "strict": true,
81
+ "allowSyntheticDefaultImports": true,
82
+ "skipLibCheck": true,
83
+ "noEmit": true
84
+ },
85
+ "include": ["commands/**/*.ts"]
86
+ }
87
+ `;
88
+
62
89
  const ENV_TEMPLATE = `# ~/.tyr/.env
63
90
  # Variables de entorno para Tyr. Este archivo nunca debe subirse a git.
64
91
  #
@@ -220,6 +247,27 @@ export default function config({ logger, fs: tyrFs, frameworkRoot, shell }: TyrC
220
247
  logger.success(`Archivo creado: ${envPath}`);
221
248
  }
222
249
 
250
+ const packageJsonPath = path.join(userRoot, 'package.json');
251
+ if (!tyrFs.exists(packageJsonPath)) {
252
+ await tyrFs.write(packageJsonPath, PACKAGE_JSON_TEMPLATE);
253
+ logger.success(`Archivo creado: ${packageJsonPath}`);
254
+ }
255
+
256
+ const tsconfigPath = path.join(userRoot, 'tsconfig.json');
257
+ if (!tyrFs.exists(tsconfigPath)) {
258
+ await tyrFs.write(tsconfigPath, TSCONFIG_TEMPLATE);
259
+ logger.success(`Archivo creado: ${tsconfigPath}`);
260
+ }
261
+
262
+ logger.info('\nInstalando dependencias de tipos en ~/.tyr...');
263
+ shell.cd(userRoot);
264
+ try {
265
+ await shell.exec('npm install');
266
+ logger.success('Dependencias instaladas correctamente.');
267
+ } catch {
268
+ logger.warn('No se pudo ejecutar npm install en ~/.tyr. Hazlo manualmente.');
269
+ }
270
+
223
271
  if (repoUrl) {
224
272
  logger.info('\nSubiendo configuración inicial al repositorio...');
225
273
  shell.cd(userRoot);
@@ -234,6 +282,35 @@ export default function config({ logger, fs: tyrFs, frameworkRoot, shell }: TyrC
234
282
  }
235
283
  }
236
284
 
285
+ // Garantizar package.json + tsconfig.json + npm install siempre,
286
+ // tanto en init fresh como al clonar un repo existente.
287
+ const packageJsonPath = path.join(userRoot, 'package.json');
288
+ const tsconfigPath = path.join(userRoot, 'tsconfig.json');
289
+ let needsInstall = false;
290
+
291
+ if (!tyrFs.exists(packageJsonPath)) {
292
+ await tyrFs.write(packageJsonPath, PACKAGE_JSON_TEMPLATE);
293
+ logger.success(`Archivo creado: ${packageJsonPath}`);
294
+ needsInstall = true;
295
+ }
296
+
297
+ if (!tyrFs.exists(tsconfigPath)) {
298
+ await tyrFs.write(tsconfigPath, TSCONFIG_TEMPLATE);
299
+ logger.success(`Archivo creado: ${tsconfigPath}`);
300
+ needsInstall = true;
301
+ }
302
+
303
+ if (needsInstall) {
304
+ logger.info('\nInstalando dependencias de tipos en ~/.tyr...');
305
+ shell.cd(userRoot);
306
+ try {
307
+ await shell.exec('npm install');
308
+ logger.success('Dependencias instaladas.');
309
+ } catch {
310
+ logger.warn('No se pudo ejecutar npm install en ~/.tyr. Hazlo manualmente.');
311
+ }
312
+ }
313
+
237
314
  const aliasesPath = path.join(userRoot, `aliases${ext}`);
238
315
  const pluginsPath = path.join(userRoot, `plugins${ext}`);
239
316
 
@@ -304,7 +304,7 @@ if (!fs.existsSync('./package.json')) {
304
304
  await run('ai', [name, prompt]);
305
305
 
306
306
  res.writeHead(200, { 'Content-Type': 'application/json' });
307
- res.end(JSON.stringify({ success: true, message: `Comando '${name}' generado correctamente en src/commands/${name}.tyr.ts` }));
307
+ res.end(JSON.stringify({ success: true, message: `Comando '${name}' generado correctamente en ~/.tyr/commands/${name}.tyr.ts` }));
308
308
  } catch (e: any) {
309
309
  res.writeHead(500, { 'Content-Type': 'application/json' });
310
310
  res.end(JSON.stringify({ success: false, message: e.message || 'Error al generar el comando.' }));
@@ -19,9 +19,28 @@ export class FileSystemManager {
19
19
  }
20
20
 
21
21
  private resolvePath(filePath: string): string {
22
- return filePath.startsWith('~/')
23
- ? path.join(homedir(), filePath.slice(2))
24
- : filePath;
22
+ return this.expandPath(filePath);
23
+ }
24
+
25
+ /**
26
+ * @method expandPath
27
+ * @description Expands `~` and `$HOME` in a path string to the actual home directory.
28
+ * Useful for reading paths from environment variables (dotenv does not expand shell variables).
29
+ * @param {string} filePath - The path to expand.
30
+ * @returns {string} The expanded absolute path.
31
+ * @example
32
+ * const dir = fs.expandPath(process.env.INTEGRATIONS_DIR!);
33
+ * // "~/dev/datosBroker" → "/Users/mandreu/dev/datosBroker"
34
+ */
35
+ public expandPath(filePath: string): string {
36
+ const home = homedir();
37
+ if (filePath.startsWith('~/') || filePath === '~') {
38
+ return path.join(home, filePath.slice(1));
39
+ }
40
+ if (filePath.startsWith('$HOME/') || filePath === '$HOME') {
41
+ return path.join(home, filePath.slice(5));
42
+ }
43
+ return filePath;
25
44
  }
26
45
 
27
46
  /**
@@ -64,6 +64,65 @@ export class GitManager {
64
64
  throw new TyrError(`Could not find the repository ` + repoUrl, e, 'Check if the repository exists or if you have the right permissions to clone it.');
65
65
  }
66
66
  }
67
+
68
+ /**
69
+ * @method cloneTo
70
+ * @description Clones a remote repository into a specific target directory.
71
+ * @param {string} repoUrl - The HTTPS or SSH URL of the repository.
72
+ * @param {string} destDir - The absolute path of the destination directory.
73
+ * @example
74
+ * await git.cloneTo('git@github.com:org/repo.git', '/path/to/dest');
75
+ */
76
+ public async cloneTo(repoUrl: string, destDir: string): Promise<void> {
77
+ this.logger.info(`Clonando ${repoUrl}...`);
78
+ const loader = this.shell.showLoader('Clonando repositorio...');
79
+ try {
80
+ await this.shell.exec(`git clone "${repoUrl}" "${destDir}"`);
81
+ await this.shell.exec(`git -C "${destDir}" config --add core.filemode false`);
82
+ loader.stop();
83
+ this.logger.success('Clonación completada.');
84
+ } catch (e) {
85
+ loader.stop();
86
+ throw new TyrError(`No se pudo clonar el repositorio: ${repoUrl}`, e, 'Comprueba que el repositorio existe y que tienes permisos para clonarlo.');
87
+ }
88
+ }
89
+
90
+ /**
91
+ * @method checkRepoExists
92
+ * @description Checks if a remote Git repository is accessible via ls-remote.
93
+ * @param {string} repoUrl - The URL of the repository to check.
94
+ * @returns {Promise<boolean>} True if the repository is reachable.
95
+ * @example
96
+ * const exists = await git.checkRepoExists('git@github.com:org/repo.git');
97
+ */
98
+ public async checkRepoExists(repoUrl: string): Promise<boolean> {
99
+ try {
100
+ await this.shell.exec(`git ls-remote "${repoUrl}" HEAD`);
101
+ return true;
102
+ } catch {
103
+ return false;
104
+ }
105
+ }
106
+
107
+ /**
108
+ * @method initWithRemote
109
+ * @description Removes an existing .git folder if present, then initialises a new Git repository
110
+ * in the given directory and configures a remote origin.
111
+ * @param {string} dir - The absolute path of the directory to initialise.
112
+ * @param {string} remoteUrl - The remote URL to set as origin.
113
+ * @example
114
+ * await git.initWithRemote('/path/to/dir', 'git@github.com:org/repo.git');
115
+ */
116
+ public async initWithRemote(dir: string, remoteUrl: string): Promise<void> {
117
+ try {
118
+ await this.shell.exec(
119
+ `cd "${dir}" && rm -rf .git && git init -b master && git remote add origin "${remoteUrl}" && git config --add core.filemode false && echo 'node_modules' >> .gitignore`
120
+ );
121
+ this.logger.success(`Repositorio Git inicializado en ${dir}`);
122
+ } catch (e) {
123
+ throw new TyrError(`No se pudo inicializar el repositorio en ${dir}`, e);
124
+ }
125
+ }
67
126
  }
68
127
 
69
128
  /**
@@ -0,0 +1,103 @@
1
+ import { WebManager } from './WebManager.js';
2
+ import { ShellManager } from './ShellManager.js';
3
+ import { Logger } from '../core/Logger.js';
4
+
5
+ interface JiraIssue {
6
+ key: string;
7
+ summary: string;
8
+ status: string;
9
+ }
10
+
11
+ /**
12
+ * @class JiraManager
13
+ * @description Integration with the Jira REST API. Allows fetching and selecting issues
14
+ * assigned to the current user. Falls back to manual branch input if Jira is unavailable.
15
+ *
16
+ * Required environment variables:
17
+ * JIRA_URL – e.g. https://yourcompany.atlassian.net
18
+ * JIRA_TOKEN – base64 of "email:api_token" (Basic Auth)
19
+ */
20
+ export class JiraManager {
21
+ private web: WebManager;
22
+ private shell: ShellManager;
23
+ private logger: Logger;
24
+
25
+ constructor(web: WebManager, shell: ShellManager, logger: Logger) {
26
+ this.web = web;
27
+ this.shell = shell;
28
+ this.logger = logger;
29
+ }
30
+
31
+ private get jiraUrl(): string | undefined {
32
+ return process.env.JIRA_URL;
33
+ }
34
+
35
+ private get jiraToken(): string | undefined {
36
+ return process.env.JIRA_TOKEN;
37
+ }
38
+
39
+ private async fetchMyIssues(): Promise<JiraIssue[]> {
40
+ if (!this.jiraUrl || !this.jiraToken) return [];
41
+
42
+ const jql = 'assignee = currentUser() AND status IN ("To develop","TO BE DONE",Backlog,"Pending Info",Developing,DEVELOPMENT,"In development",TEST,DESIGN,"To Do","In Progress")';
43
+ const url = `${this.jiraUrl}/rest/api/3/search?jql=${encodeURIComponent(jql)}`;
44
+
45
+ const data = await this.web.get(url, {
46
+ headers: { Authorization: `Basic ${this.jiraToken}` },
47
+ });
48
+
49
+ return (data.issues ?? []).map((issue: any) => ({
50
+ key: issue.key,
51
+ summary: issue.fields.summary,
52
+ status: issue.fields.status.name,
53
+ }));
54
+ }
55
+
56
+ /**
57
+ * @method selectIssue
58
+ * @description Presents the user's open Jira issues as an interactive list and returns
59
+ * the selected issue key (e.g. "PROJ-123"). If Jira is not configured or unreachable,
60
+ * falls back to a free-text prompt for a branch name.
61
+ * Returns null if the user chooses to skip.
62
+ * @returns {Promise<string | null>} The selected Jira key, a branch name, or null.
63
+ * @example
64
+ * const branch = await jira.selectIssue();
65
+ * if (branch) await workspace.tagWorkspace(dir, branch);
66
+ */
67
+ public async selectIssue(): Promise<string | null> {
68
+ // Try Jira API
69
+ if (this.jiraUrl && this.jiraToken) {
70
+ try {
71
+ const issues = await this.fetchMyIssues();
72
+ const choices = [
73
+ ...issues.map(i => ({
74
+ name: `${i.key} - ${i.summary} [${i.status}]`,
75
+ value: i.key,
76
+ })),
77
+ { name: 'Sin ticket (introducir rama manualmente)', value: '__manual__' },
78
+ { name: 'Omitir (no crear rama)', value: '__skip__' },
79
+ ];
80
+
81
+ const selected = await this.shell.select(choices, 'Selecciona un ticket de Jira:');
82
+
83
+ if (selected === '__skip__') return null;
84
+ if (selected === '__manual__') return this.askForBranch();
85
+
86
+ return selected;
87
+ } catch {
88
+ this.logger.warn('No se ha podido conectar con Jira. Introducción manual de rama.');
89
+ }
90
+ }
91
+
92
+ return this.askForBranch();
93
+ }
94
+
95
+ private async askForBranch(): Promise<string | null> {
96
+ const raw = await this.shell.input('Introduce el nombre de la nueva rama (vacío para omitir):');
97
+ if (!raw.trim()) return null;
98
+ // Normalise: take the last segment separated by '/'
99
+ return raw.trim().split('/').pop()?.toUpperCase() ?? raw.trim();
100
+ }
101
+ }
102
+
103
+ export const JiraManagerTests = {};
@@ -104,6 +104,86 @@ export class ShellManager {
104
104
 
105
105
  this.cwd = resolve(this.cwd, expandedPath);
106
106
  }
107
+
108
+ /**
109
+ * @method getCwd
110
+ * @description Returns the current working directory used by this instance.
111
+ * @returns {string} The current working directory.
112
+ * @example
113
+ * const dir = shell.getCwd();
114
+ */
115
+ public getCwd(): string {
116
+ return this.cwd;
117
+ }
118
+
119
+ /**
120
+ * @method confirm
121
+ * @description Prompts the user with a yes/no question via CLI.
122
+ * @param {string} question - The question to display.
123
+ * @param {boolean} defaultValue - Default answer if user presses Enter (default: false).
124
+ * @returns {Promise<boolean>} True if the user confirmed.
125
+ * @example
126
+ * const ok = await shell.confirm('¿Continuar?', false);
127
+ */
128
+ public async confirm(question: string, defaultValue: boolean = false): Promise<boolean> {
129
+ try {
130
+ const result = await inquirer.prompt([{
131
+ type: 'confirm',
132
+ name: 'value',
133
+ message: question,
134
+ default: defaultValue,
135
+ }]);
136
+ return result.value;
137
+ } catch (e) {
138
+ throw new TyrError(`Error al mostrar la confirmación: ${question}`, e);
139
+ }
140
+ }
141
+
142
+ /**
143
+ * @method select
144
+ * @description Prompts the user to select one option from a list.
145
+ * @param {Array<{name: string, value: string}>} choices - The available options.
146
+ * @param {string} question - The question to display.
147
+ * @returns {Promise<string>} The selected value.
148
+ * @example
149
+ * const branch = await shell.select([{ name: 'main', value: 'main' }], '¿Qué rama?');
150
+ */
151
+ public async select(choices: { name: string; value: string }[], question: string): Promise<string> {
152
+ try {
153
+ const result = await inquirer.prompt([{
154
+ type: 'list',
155
+ name: 'value',
156
+ message: question,
157
+ choices,
158
+ }]);
159
+ return result.value;
160
+ } catch (e) {
161
+ throw new TyrError(`Error al mostrar la selección: ${question}`, e);
162
+ }
163
+ }
164
+
165
+ /**
166
+ * @method checkbox
167
+ * @description Prompts the user to select multiple options from a list.
168
+ * @param {Array<{name: string, value: string}>} choices - The available options.
169
+ * @param {string} question - The question to display.
170
+ * @returns {Promise<string[]>} The selected values.
171
+ * @example
172
+ * const widgets = await shell.checkbox(choices, '¿Qué widgets incluir?');
173
+ */
174
+ public async checkbox(choices: { name: string; value: string }[], question: string): Promise<string[]> {
175
+ try {
176
+ const result = await inquirer.prompt([{
177
+ type: 'checkbox',
178
+ name: 'value',
179
+ message: question,
180
+ choices,
181
+ }]);
182
+ return result.value;
183
+ } catch (e) {
184
+ throw new TyrError(`Error al mostrar las opciones: ${question}`, e);
185
+ }
186
+ }
107
187
  }
108
188
 
109
189
  /**
@@ -114,5 +194,8 @@ export const ShellManagerTests = {
114
194
  exec: { command: 'node -v' },
115
195
  cd: { path: '/tmp' },
116
196
  input: { question: 'Enter a test value:' },
117
- showLoader: { message: 'Loading test...' }
197
+ showLoader: { message: 'Loading test...' },
198
+ confirm: { question: '¿Continuar?', defaultValue: false },
199
+ select: { choices: [{ name: 'Opción A', value: 'a' }, { name: 'Opción B', value: 'b' }], question: '¿Cuál eliges?' },
200
+ checkbox: { choices: [{ name: 'Item 1', value: '1' }, { name: 'Item 2', value: '2' }], question: '¿Cuáles quieres?' },
118
201
  };
@@ -24,6 +24,46 @@ export class WebManager {
24
24
  }
25
25
  }
26
26
 
27
+ /**
28
+ * @method get
29
+ * @description Makes an HTTP GET request and returns the raw response data. Useful for REST APIs.
30
+ * @param {string} url - The URL to request.
31
+ * @param {AxiosRequestConfig} config - Optional Axios config (headers, params, etc.).
32
+ * @returns {Promise<any>} The response data.
33
+ * @example
34
+ * const data = await web.get('https://api.example.com/items', { headers: { Authorization: 'Bearer token' } });
35
+ */
36
+ public async get(url: string, config?: AxiosRequestConfig): Promise<any> {
37
+ try {
38
+ const response = await axios.get(url, config);
39
+ return response.data;
40
+ } catch (e: any) {
41
+ const status = e?.response?.status;
42
+ const err = new TyrError(`HTTP GET failed (${status ?? 'unknown'}): ${url}`, e, 'Check your network connection and the URL.');
43
+ (err as any).status = status;
44
+ throw err;
45
+ }
46
+ }
47
+
48
+ /**
49
+ * @method getMetaTag
50
+ * @description Fetches a page and returns the content of a specific meta tag.
51
+ * @param {string} url - The URL of the page to scrape.
52
+ * @param {string} metaName - The name attribute of the meta tag.
53
+ * @returns {Promise<string|null>} The content of the meta tag, or null if not found.
54
+ * @example
55
+ * const webname = await web.getMetaTag('https://client.example.com', 'webname');
56
+ */
57
+ public async getMetaTag(url: string, metaName: string): Promise<string | null> {
58
+ try {
59
+ const $ = await this.fetch(url);
60
+ return $(`meta[name="${metaName}"]`).attr('content') ?? null;
61
+ } catch (e) {
62
+ if (e instanceof TyrError) throw e;
63
+ throw new TyrError(`Could not read meta tag '${metaName}' from: ${url}`, e);
64
+ }
65
+ }
66
+
27
67
  /**
28
68
  * @method selectFromWeb
29
69
  * @description Selects elements from a web page using a CSS selector.
@@ -0,0 +1,87 @@
1
+ import { ShellManager } from './ShellManager.js';
2
+ import { FileSystemManager } from './FileSystemManager.js';
3
+ import { Logger } from '../core/Logger.js';
4
+ import { TyrError } from '../core/TyrError.js';
5
+
6
+ /**
7
+ * @class WorkspaceManager
8
+ * @description Manages local workspace directories: checks for existing repos,
9
+ * creates branches and opens the project in VSCode.
10
+ */
11
+ export class WorkspaceManager {
12
+ private shell: ShellManager;
13
+ private fs: FileSystemManager;
14
+ private logger: Logger;
15
+
16
+ constructor(shell: ShellManager, fs: FileSystemManager, logger: Logger) {
17
+ this.shell = shell;
18
+ this.fs = fs;
19
+ this.logger = logger;
20
+ }
21
+
22
+ /**
23
+ * @method checkExisting
24
+ * @description Checks whether a workspace directory already exists.
25
+ * If it does, asks the user if they want to replace it (deleting it first).
26
+ * Returns true if the caller should proceed with creating the workspace.
27
+ * @param {string} dirPath - Absolute path to the workspace directory.
28
+ * @param {string} type - Human-readable type name shown in messages (e.g. 'integración', 'web').
29
+ * @returns {Promise<boolean>} True if the workspace can be created/overwritten.
30
+ * @example
31
+ * const proceed = await workspace.checkExisting('/path/to/repo', 'integración');
32
+ * if (!proceed) return;
33
+ */
34
+ public async checkExisting(dirPath: string, type: string = 'directorio'): Promise<boolean> {
35
+ if (!this.fs.exists(dirPath)) return true;
36
+
37
+ this.logger.warn(`Este ${type} ya existe: ${dirPath}`);
38
+ const replace = await this.shell.confirm('¿Quieres reemplazarlo?', false);
39
+
40
+ if (!replace) {
41
+ this.logger.info('Operación cancelada.');
42
+ return false;
43
+ }
44
+
45
+ try {
46
+ await this.shell.exec(`rm -rf "${dirPath}"`);
47
+ this.logger.info('Directorio existente eliminado.');
48
+ return true;
49
+ } catch (e) {
50
+ throw new TyrError(`No se pudo eliminar el directorio existente: ${dirPath}`, e);
51
+ }
52
+ }
53
+
54
+ /**
55
+ * @method tagWorkspace
56
+ * @description Tags a workspace by creating and checking out a new Git branch,
57
+ * and optionally opening it in VSCode.
58
+ * @param {string} dir - Absolute path to the workspace directory.
59
+ * @param {string | null} branch - Branch name to create (null = skip branch creation).
60
+ * @param {boolean} openCode - Whether to open the directory in VSCode (default: true).
61
+ * @example
62
+ * await workspace.tagWorkspace('/path/to/repo', 'PROJ-123', true);
63
+ */
64
+ public async tagWorkspace(dir: string, branch: string | null, openCode: boolean = true): Promise<void> {
65
+ if (branch) {
66
+ try {
67
+ await this.shell.exec(`git -C "${dir}" checkout -b ${branch}`);
68
+ this.logger.success(`Rama '${branch}' creada.`);
69
+ } catch (e) {
70
+ this.logger.warn(`No se pudo crear la rama '${branch}'. Puede que ya exista.`);
71
+ }
72
+ }
73
+
74
+ if (openCode) {
75
+ try {
76
+ await this.shell.exec(`code "${dir}"`);
77
+ } catch {
78
+ this.logger.warn('No se pudo abrir VSCode. Asegúrate de tener el comando "code" instalado.');
79
+ }
80
+ }
81
+ }
82
+ }
83
+
84
+ export const WorkspaceManagerTests = {
85
+ checkExisting: { dirPath: '/tmp/tyr-workspace-test', type: 'integración' },
86
+ tagWorkspace: { dir: '/tmp/tyr-workspace-test', branch: null, openCode: false },
87
+ };
package/config/map.yml DELETED
@@ -1,4 +0,0 @@
1
- settings:
2
- appName: Tyr CLI
3
- version: 0.1.0
4
- commands: {}
@@ -1,113 +0,0 @@
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/online-booking/htdocs/datosBroker');
31
-
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/online-booking/htdocs/datosBroker/${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
-
111
- // export const Test = {
112
- // args: []
113
- // }
@@ -1,116 +0,0 @@
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
- // }
@@ -1,61 +0,0 @@
1
- import { TyrContext } from '../core/Kernel';
2
- import path from 'path';
3
- import { homedir } from 'os';
4
-
5
- export default ({ task, fail, logger, fs }: TyrContext) => {
6
- return async (_args: string[]) => {
7
- const homeDir = homedir();
8
- const userRoot = path.join(homeDir, '.tyr');
9
-
10
- const aliasesTemplatePath = path.join(homeDir, 'avantio', 'framework', 'core', 'include', 'bin', 'aliases.template.sh');
11
- const pluginsTemplatePath = path.join(homeDir, 'avantio', 'framework', 'core', 'include', 'bin', 'plugins.template.sh');
12
-
13
- const aliasesTarget = path.join(userRoot, 'aliases');
14
- const pluginsTarget = path.join(userRoot, 'plugins');
15
-
16
- await task('Verificando configuración de Tyr', async () => {
17
- if (!fs.exists(userRoot)) {
18
- fail(
19
- 'El directorio ~/.tyr no existe.',
20
- "Ejecuta 'tyr --config' antes de continuar."
21
- );
22
- }
23
- logger.success(`Directorio ~/.tyr encontrado: ${userRoot}`);
24
- });
25
-
26
- await task('Verificando templates de Avantio', async () => {
27
- if (!fs.exists(aliasesTemplatePath)) {
28
- fail(
29
- `Template de aliases no encontrado: ${aliasesTemplatePath}`,
30
- 'Verifica que el framework de Avantio esté correctamente instalado.'
31
- );
32
- }
33
- if (!fs.exists(pluginsTemplatePath)) {
34
- fail(
35
- `Template de plugins no encontrado: ${pluginsTemplatePath}`,
36
- 'Verifica que el framework de Avantio esté correctamente instalado.'
37
- );
38
- }
39
- logger.success('Templates de Avantio encontrados.');
40
- });
41
-
42
- await task('Copiando aliases de Avantio', async () => {
43
- const content = await fs.read(aliasesTemplatePath);
44
- if (!content) fail('No se pudo leer aliases.template.sh');
45
- await fs.write(aliasesTarget, content!);
46
- logger.success(`Aliases copiados a: ${aliasesTarget}`);
47
- });
48
-
49
- await task('Copiando plugins de Avantio', async () => {
50
- const content = await fs.read(pluginsTemplatePath);
51
- if (!content) fail('No se pudo leer plugins.template.sh');
52
- await fs.write(pluginsTarget, content!);
53
- logger.success(`Plugins copiados a: ${pluginsTarget}`);
54
- });
55
-
56
- logger.success('\nInstalación de Avantio completada.');
57
- logger.info(` aliases → ${aliasesTarget}`);
58
- logger.info(` plugins → ${pluginsTarget}`);
59
- logger.warn('\nRecuerda recargar tu shell para aplicar los cambios.');
60
- };
61
- };