orch-mini 0.1.3 → 0.1.4

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/info.ts +70 -40
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "orch-mini",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
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
@@ -3,7 +3,6 @@ import { resolve } from 'node:path';
3
3
  import type { LoadedStack } from './parser.js';
4
4
  import { hasRepo, type Service, type Stack } from './schema.js';
5
5
 
6
- // Patrones que sugieren "el usuario debería completar esto antes de prod".
7
6
  const PLACEHOLDER_PATTERNS = [
8
7
  /^changeit-/i,
9
8
  /^dev-default-/i,
@@ -13,6 +12,9 @@ const PLACEHOLDER_PATTERNS = [
13
12
  /^your-/i,
14
13
  ];
15
14
 
15
+ const WRAP_WIDTH = 78;
16
+ const DESC_INDENT = ' '; // 7 espacios — alineado con texto post-marker
17
+
16
18
  const C = supportsColor()
17
19
  ? {
18
20
  reset: '\x1b[0m',
@@ -22,8 +24,9 @@ const C = supportsColor()
22
24
  yellow: '\x1b[33m',
23
25
  green: '\x1b[32m',
24
26
  red: '\x1b[31m',
27
+ magenta: '\x1b[35m',
25
28
  }
26
- : { reset: '', dim: '', bold: '', cyan: '', yellow: '', green: '', red: '' };
29
+ : { reset: '', dim: '', bold: '', cyan: '', yellow: '', green: '', red: '', magenta: '' };
27
30
 
28
31
  function supportsColor(): boolean {
29
32
  return process.stdout.isTTY === true && process.env.NO_COLOR === undefined;
@@ -37,8 +40,9 @@ export function renderInfo(loaded: LoadedStack): string {
37
40
  const gw = stack.gateway;
38
41
 
39
42
  out.push(
40
- header(`${stack.name}`) +
41
- ` ${C.dim}${services.length} services${gw ? ` · gateway en :${gw.port}` : ''}${C.reset}`,
43
+ `${C.bold}${stack.name}${C.reset} ${C.dim}${services.length} services${
44
+ gw ? ` · gateway en :${gw.port}` : ''
45
+ }${C.reset}`,
42
46
  );
43
47
 
44
48
  out.push('');
@@ -73,9 +77,7 @@ export function renderInfo(loaded: LoadedStack): string {
73
77
  const sorted = [...gw.routes].sort(
74
78
  (a, b) => effectiveLen(b.path) - effectiveLen(a.path),
75
79
  );
76
- const maxUrl = Math.max(
77
- ...sorted.map((r) => urlFor(gw.port, r.path).length),
78
- );
80
+ const maxUrl = Math.max(...sorted.map((r) => urlFor(gw.port, r.path).length));
79
81
  for (const route of sorted) {
80
82
  const url = urlFor(gw.port, route.path);
81
83
  const note = route.strip_prefix ? ` ${C.dim}(strip prefix)${C.reset}` : '';
@@ -86,24 +88,30 @@ export function renderInfo(loaded: LoadedStack): string {
86
88
  const debugConfigs = services.filter(([, s]) => s.debug_port !== undefined);
87
89
  if (debugConfigs.length > 0) {
88
90
  out.push('');
89
- out.push(section('Debug (VS Code attach via om vscode)'));
91
+ out.push(section('Debug') + ` ${C.dim}(om vscode genera launch.json)${C.reset}`);
92
+ const maxName = Math.max(...debugConfigs.map(([n]) => n.length));
90
93
  for (const [name, svc] of debugConfigs) {
91
- out.push(` ${name.padEnd(30)} attach → localhost:${svc.debug_port}`);
94
+ out.push(
95
+ ` ${name.padEnd(maxName + 2)}${C.dim}attach →${C.reset} localhost:${svc.debug_port}`,
96
+ );
92
97
  }
93
98
  }
94
99
 
95
- const concerns = collectConcerns(stack, loaded.workDir);
100
+ const concerns = collectConcerns(stack);
96
101
  if (concerns.length > 0) {
97
102
  out.push('');
98
103
  out.push(`${C.yellow}⚠${C.reset} ${C.bold}Probablemente quieras tocar${C.reset}`);
99
- const maxKey = Math.max(...concerns.map((c) => `${c.service}.${c.key}`.length));
100
- for (const c of concerns) {
101
- const k = `${c.service}.${c.key}`.padEnd(maxKey + 2);
102
- const tag = c.required ? `${C.red}[required]${C.reset} ` : '';
103
- out.push(` ${tag}${k}${C.dim}${c.reason}${C.reset}`);
104
- if (c.description) {
105
- // Indenta la descripción debajo, alineada con el nombre del key.
106
- out.push(` ${' '.repeat(maxKey + 2 + (c.required ? 11 : 0))}${C.dim}↳ ${c.description}${C.reset}`);
104
+ const grouped = groupByService(concerns);
105
+ for (const [svcName, items] of grouped) {
106
+ out.push('');
107
+ out.push(` ${C.cyan}${svcName}${C.reset}`);
108
+ for (const c of items) {
109
+ out.push(` ${renderConcernLine(c)}`);
110
+ if (c.description) {
111
+ for (const line of wrapText(c.description, WRAP_WIDTH - DESC_INDENT.length)) {
112
+ out.push(` ${DESC_INDENT}${C.dim}${line}${C.reset}`);
113
+ }
114
+ }
107
115
  }
108
116
  }
109
117
  } else {
@@ -114,8 +122,45 @@ export function renderInfo(loaded: LoadedStack): string {
114
122
  return out.join('\n') + '\n';
115
123
  }
116
124
 
117
- function header(text: string): string {
118
- return `${C.bold}${text}${C.reset}`;
125
+ function renderConcernLine(c: Concern): string {
126
+ const marker = c.required
127
+ ? `${C.red}✗${C.reset}`
128
+ : c.reason.startsWith('placeholder')
129
+ ? `${C.yellow}!${C.reset}`
130
+ : `${C.dim}·${C.reset}`;
131
+ const tag = c.required ? ` ${C.red}[required]${C.reset}` : '';
132
+ return `${marker}${tag} ${C.bold}${c.key}${C.reset} ${C.dim}— ${c.reason}${C.reset}`;
133
+ }
134
+
135
+ function groupByService(concerns: Concern[]): Map<string, Concern[]> {
136
+ const grouped = new Map<string, Concern[]>();
137
+ for (const c of concerns) {
138
+ const list = grouped.get(c.service) ?? [];
139
+ list.push(c);
140
+ grouped.set(c.service, list);
141
+ }
142
+ // Ordenar items de cada service: required arriba.
143
+ for (const list of grouped.values()) {
144
+ list.sort((a, b) => (b.required ? 1 : 0) - (a.required ? 1 : 0));
145
+ }
146
+ return grouped;
147
+ }
148
+
149
+ function wrapText(text: string, width: number): string[] {
150
+ if (text.length <= width) return [text];
151
+ const words = text.split(/\s+/);
152
+ const lines: string[] = [];
153
+ let current = '';
154
+ for (const word of words) {
155
+ if ((current + ' ' + word).trim().length > width) {
156
+ if (current) lines.push(current);
157
+ current = word;
158
+ } else {
159
+ current = (current + ' ' + word).trim();
160
+ }
161
+ }
162
+ if (current) lines.push(current);
163
+ return lines;
119
164
  }
120
165
 
121
166
  function section(text: string): string {
@@ -134,11 +179,7 @@ function formatServiceRow(name: string, svc: Service): string {
134
179
  }
135
180
 
136
181
  function shortenRepo(repo: string): string {
137
- // github.com/logieinc/foo.git foo, /local → local
138
- return repo
139
- .replace(/\.git$/, '')
140
- .split(/[/:]/)
141
- .pop() ?? repo;
182
+ return repo.replace(/\.git$/, '').split(/[/:]/).pop() ?? repo;
142
183
  }
143
184
 
144
185
  function urlFor(port: number, path: string): string {
@@ -156,10 +197,7 @@ function collectRepos(stack: Stack): RepoEntry[] {
156
197
  const seen = new Map<string, RepoEntry>();
157
198
  for (const svc of Object.values(stack.services)) {
158
199
  if (!hasRepo(svc)) continue;
159
- const slug = svc.repo
160
- .replace(/\.git$/, '')
161
- .split(/[/:]/)
162
- .pop()!;
200
+ const slug = svc.repo.replace(/\.git$/, '').split(/[/:]/).pop()!;
163
201
  if (!seen.has(slug)) seen.set(slug, { slug, ref: svc.ref });
164
202
  }
165
203
  return [...seen.values()];
@@ -183,7 +221,7 @@ type Concern = {
183
221
  required?: boolean;
184
222
  };
185
223
 
186
- function collectConcerns(stack: Stack, _workDir: string): Concern[] {
224
+ function collectConcerns(stack: Stack): Concern[] {
187
225
  const out: Concern[] = [];
188
226
 
189
227
  for (const [svcName, svc] of Object.entries(stack.services)) {
@@ -195,9 +233,7 @@ function collectConcerns(stack: Stack, _workDir: string): Concern[] {
195
233
  let reason: string | null = null;
196
234
  if (v === '') reason = 'vacío';
197
235
  else if (PLACEHOLDER_PATTERNS.some((re) => re.test(v))) {
198
- reason = `placeholder: "${truncate(v, 30)}"`;
199
- } else if (m.required === true) {
200
- // Marcado como required en la metadata, pero ya tiene valor — no es concern.
236
+ reason = `placeholder "${truncate(v, 30)}"`;
201
237
  }
202
238
 
203
239
  if (reason !== null) {
@@ -209,13 +245,7 @@ function collectConcerns(stack: Stack, _workDir: string): Concern[] {
209
245
  }
210
246
  }
211
247
 
212
- // Required arriba (los más urgentes), después el resto.
213
- return out.sort((a, b) => {
214
- if ((b.required ? 1 : 0) - (a.required ? 1 : 0) !== 0) {
215
- return (b.required ? 1 : 0) - (a.required ? 1 : 0);
216
- }
217
- return 0;
218
- });
248
+ return out;
219
249
  }
220
250
 
221
251
  function truncate(s: string, max: number): string {