orch-mini 0.1.4 → 0.1.5

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": "orch-mini",
3
- "version": "0.1.4",
3
+ "version": "0.1.5",
4
4
  "description": "Orquestador declarativo mínimo: un stack.yaml describe la arquitectura, om renderiza docker-compose + nginx + scripts y levanta el stack.",
5
5
  "type": "module",
6
6
  "engines": {
package/src/info.ts CHANGED
@@ -1,5 +1,4 @@
1
- import { existsSync } from 'node:fs';
2
- import { resolve } from 'node:path';
1
+ import { relative } from 'node:path';
3
2
  import type { LoadedStack } from './parser.js';
4
3
  import { hasRepo, type Service, type Stack } from './schema.js';
5
4
 
@@ -35,6 +34,7 @@ function supportsColor(): boolean {
35
34
  export function renderInfo(loaded: LoadedStack): string {
36
35
  const out: string[] = [];
37
36
  const { stack } = loaded;
37
+ const sourceLabel = relative(process.cwd(), loaded.sourcePath) || loaded.sourcePath;
38
38
 
39
39
  const services = Object.entries(stack.services);
40
40
  const gw = stack.gateway;
@@ -42,7 +42,7 @@ export function renderInfo(loaded: LoadedStack): string {
42
42
  out.push(
43
43
  `${C.bold}${stack.name}${C.reset} ${C.dim}${services.length} services${
44
44
  gw ? ` · gateway en :${gw.port}` : ''
45
- }${C.reset}`,
45
+ } · ${sourceLabel}${C.reset}`,
46
46
  );
47
47
 
48
48
  out.push('');
@@ -59,6 +59,7 @@ export function renderInfo(loaded: LoadedStack): string {
59
59
  for (const r of repos) {
60
60
  const refLabel = r.ref ?? `${C.dim}(default branch)${C.reset}`;
61
61
  out.push(` ${r.slug.padEnd(maxName + 2)}${refLabel}`);
62
+ out.push(` ${' '.repeat(maxName + 2)}${C.dim}${r.url}${C.reset}`);
62
63
  }
63
64
  }
64
65
 
@@ -97,7 +98,7 @@ export function renderInfo(loaded: LoadedStack): string {
97
98
  }
98
99
  }
99
100
 
100
- const concerns = collectConcerns(stack);
101
+ const concerns = collectConcerns(loaded);
101
102
  if (concerns.length > 0) {
102
103
  out.push('');
103
104
  out.push(`${C.yellow}⚠${C.reset} ${C.bold}Probablemente quieras tocar${C.reset}`);
@@ -107,6 +108,9 @@ export function renderInfo(loaded: LoadedStack): string {
107
108
  out.push(` ${C.cyan}${svcName}${C.reset}`);
108
109
  for (const c of items) {
109
110
  out.push(` ${renderConcernLine(c)}`);
111
+ if (c.location) {
112
+ out.push(` ${DESC_INDENT}${C.dim}${c.location}${C.reset}`);
113
+ }
110
114
  if (c.description) {
111
115
  for (const line of wrapText(c.description, WRAP_WIDTH - DESC_INDENT.length)) {
112
116
  out.push(` ${DESC_INDENT}${C.dim}${line}${C.reset}`);
@@ -114,6 +118,15 @@ export function renderInfo(loaded: LoadedStack): string {
114
118
  }
115
119
  }
116
120
  }
121
+
122
+ out.push('');
123
+ const requiredCount = concerns.filter((c) => c.required).length;
124
+ const optionalCount = concerns.length - requiredCount;
125
+ out.push(
126
+ ` ${C.dim}Total:${C.reset} ${
127
+ requiredCount > 0 ? `${C.red}${requiredCount} required${C.reset}` : `${C.green}0 required${C.reset}`
128
+ }${C.dim} · ${optionalCount} opcionales${C.reset}`,
129
+ );
117
130
  } else {
118
131
  out.push('');
119
132
  out.push(`${C.green}✓${C.reset} no detecté placeholders ni valores vacíos`);
@@ -191,14 +204,14 @@ function effectiveLen(path: string): number {
191
204
  return path.replace(/^(=|\^~)\s*/, '').length;
192
205
  }
193
206
 
194
- type RepoEntry = { slug: string; ref: string | undefined };
207
+ type RepoEntry = { slug: string; ref: string | undefined; url: string };
195
208
 
196
209
  function collectRepos(stack: Stack): RepoEntry[] {
197
210
  const seen = new Map<string, RepoEntry>();
198
211
  for (const svc of Object.values(stack.services)) {
199
212
  if (!hasRepo(svc)) continue;
200
213
  const slug = svc.repo.replace(/\.git$/, '').split(/[/:]/).pop()!;
201
- if (!seen.has(slug)) seen.set(slug, { slug, ref: svc.ref });
214
+ if (!seen.has(slug)) seen.set(slug, { slug, ref: svc.ref, url: svc.repo });
202
215
  }
203
216
  return [...seen.values()];
204
217
  }
@@ -219,12 +232,14 @@ type Concern = {
219
232
  reason: string;
220
233
  description?: string;
221
234
  required?: boolean;
235
+ location?: string; // "arch/stack.yaml:42" — clickeable en VS Code terminal
222
236
  };
223
237
 
224
- function collectConcerns(stack: Stack): Concern[] {
238
+ function collectConcerns(loaded: LoadedStack): Concern[] {
225
239
  const out: Concern[] = [];
240
+ const sourceRel = relative(process.cwd(), loaded.sourcePath) || loaded.sourcePath;
226
241
 
227
- for (const [svcName, svc] of Object.entries(stack.services)) {
242
+ for (const [svcName, svc] of Object.entries(loaded.stack.services)) {
228
243
  const meta = svc.env_meta ?? {};
229
244
  for (const [k, vRaw] of Object.entries(svc.env ?? {})) {
230
245
  const v = String(vRaw);
@@ -240,6 +255,8 @@ function collectConcerns(stack: Stack): Concern[] {
240
255
  const concern: Concern = { service: svcName, key: k, reason };
241
256
  if (m.description) concern.description = m.description;
242
257
  if (m.required) concern.required = m.required;
258
+ const loc = loaded.locations.get(`${svcName}.${k}`);
259
+ if (loc) concern.location = `${sourceRel}:${loc.line}:${loc.col}`;
243
260
  out.push(concern);
244
261
  }
245
262
  }
package/src/parser.ts CHANGED
@@ -1,17 +1,22 @@
1
1
  import { readFileSync } from 'node:fs';
2
2
  import { basename, dirname, resolve } from 'node:path';
3
- import { parse as parseYaml } from 'yaml';
3
+ import { isMap, isPair, isScalar, LineCounter, parseDocument } from 'yaml';
4
4
  import { z } from 'zod';
5
5
  import { expandFileRefs } from './file-refs.js';
6
6
  import { findStack, STACK_FILENAME, STACK_SUBDIRS } from './discover.js';
7
7
  import { stackSchema, type Stack } from './schema.js';
8
8
 
9
+ // Mapa de "service.envKey" → {line, col} del archivo fuente.
10
+ // Habilita reportes con archivo:línea clickeables en VS Code terminal.
11
+ export type Locations = Map<string, { line: number; col: number }>;
12
+
9
13
  export type LoadedStack = {
10
14
  stack: Stack;
11
15
  sourcePath: string;
12
16
  workDir: string; // dirname(stack.yaml)
13
17
  workspaceRoot: string; // donde van repos/ y .stack/ — padre del workDir si el yaml vive en arch/
14
18
  outDir: string; // workspaceRoot/.stack
19
+ locations: Locations;
15
20
  };
16
21
 
17
22
  export function loadStack(stackPath?: string): LoadedStack {
@@ -36,18 +41,23 @@ export function loadStack(stackPath?: string): LoadedStack {
36
41
  throw new Error(`no se pudo leer el stack en ${absPath}: ${msg}`);
37
42
  }
38
43
 
39
- let doc: unknown;
44
+ const lineCounter = new LineCounter();
45
+ let docNode: ReturnType<typeof parseDocument>;
40
46
  try {
41
- doc = parseYaml(raw);
47
+ docNode = parseDocument(raw, { lineCounter });
42
48
  } catch (err) {
43
49
  const msg = err instanceof Error ? err.message : String(err);
44
50
  throw new Error(`YAML inválido en ${absPath}: ${msg}`);
45
51
  }
46
52
 
47
53
  const workDir = resolve(absPath, '..');
54
+ const locations = collectLocations(docNode, lineCounter);
55
+
56
+ // Plain JS desde el AST para los pasos siguientes.
57
+ const docPlain = docNode.toJSON();
48
58
 
49
59
  // Expandir ${file:path} antes de validar — paths se resuelven relativo al workDir.
50
- const expanded = expandFileRefs(doc, workDir);
60
+ const expanded = expandFileRefs(docPlain, workDir);
51
61
 
52
62
  // Normalizar el formato mixed de env (string | {value, description, required})
53
63
  // a dos campos planos: env (Record<string,string>) + env_meta (descripciones).
@@ -66,9 +76,44 @@ export function loadStack(stackPath?: string): LoadedStack {
66
76
  workDir,
67
77
  workspaceRoot,
68
78
  outDir: resolve(workspaceRoot, '.stack'),
79
+ locations,
69
80
  };
70
81
  }
71
82
 
83
+ // Recorre el AST del YAML y extrae line/col de cada services.<svc>.env.<KEY>.
84
+ // El path es el del KEY del par (donde el usuario debería pararse para editar).
85
+ function collectLocations(
86
+ doc: ReturnType<typeof parseDocument>,
87
+ lineCounter: LineCounter,
88
+ ): Locations {
89
+ const locations: Locations = new Map();
90
+ const services = doc.get('services', true);
91
+ if (!isMap(services)) return locations;
92
+
93
+ for (const svcPair of services.items) {
94
+ if (!isPair(svcPair) || !isScalar(svcPair.key)) continue;
95
+ const svcName = String(svcPair.key.value);
96
+
97
+ const svcNode = svcPair.value;
98
+ if (!isMap(svcNode)) continue;
99
+
100
+ const envNode = svcNode.get('env', true);
101
+ if (!isMap(envNode)) continue;
102
+
103
+ for (const envPair of envNode.items) {
104
+ if (!isPair(envPair) || !isScalar(envPair.key)) continue;
105
+ const keyNode = envPair.key;
106
+ const key = String(keyNode.value);
107
+ const range = keyNode.range;
108
+ if (!range) continue;
109
+ const pos = lineCounter.linePos(range[0]);
110
+ locations.set(`${svcName}.${key}`, { line: pos.line, col: pos.col });
111
+ }
112
+ }
113
+
114
+ return locations;
115
+ }
116
+
72
117
  function computeWorkspaceRoot(workDir: string): string {
73
118
  // Si el stack.yaml vive en un subdir convencional (arch/), el workspaceRoot
74
119
  // es el padre — repos/ y .stack/ no se mezclan con la definición.