orch-mini 0.1.0 → 0.1.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.
Files changed (3) hide show
  1. package/package.json +1 -1
  2. package/src/cli.ts +11 -0
  3. package/src/info.ts +202 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "orch-mini",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
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/cli.ts CHANGED
@@ -2,6 +2,7 @@ import { spawnSync } from 'node:child_process';
2
2
  import { chmodSync, existsSync, mkdirSync, writeFileSync } from 'node:fs';
3
3
  import { dirname, join, relative, resolve } from 'node:path';
4
4
  import { initStack } from './init.js';
5
+ import { renderInfo } from './info.js';
5
6
  import { loadStack, type LoadedStack } from './parser.js';
6
7
  import { renderCompose } from './renderer/compose.js';
7
8
  import { renderDbInit } from './renderer/db-init.js';
@@ -16,6 +17,7 @@ const COMMANDS = [
16
17
  'sync',
17
18
  'gen',
18
19
  'validate',
20
+ 'info',
19
21
  'vscode',
20
22
  'up',
21
23
  'down',
@@ -36,6 +38,7 @@ setup:
36
38
  om sync clone/pull de los repos declarados (services.*.repo)
37
39
  om gen [stack.yaml] [--out <dir>] rinde compose + nginx + scripts en .stack/
38
40
  om validate [stack.yaml] solo valida el stack
41
+ om info resumen del stack + qué probablemente quieras tocar
39
42
  om vscode genera .vscode/launch.json (attach + browser)
40
43
 
41
44
  runtime:
@@ -77,6 +80,8 @@ function main(argv: string[]): number {
77
80
  return runValidate(rest);
78
81
  case 'gen':
79
82
  return runGen(rest);
83
+ case 'info':
84
+ return runInfo();
80
85
  case 'vscode':
81
86
  return runVscode();
82
87
  case 'up':
@@ -165,6 +170,12 @@ function runValidate(args: string[]): number {
165
170
  return 0;
166
171
  }
167
172
 
173
+ function runInfo(): number {
174
+ const loaded = loadStack(undefined);
175
+ process.stdout.write(renderInfo(loaded));
176
+ return 0;
177
+ }
178
+
168
179
  function runVscode(): number {
169
180
  const loaded = loadStack(undefined);
170
181
  const vscodeDir = resolve(loaded.workspaceRoot, '.vscode');
package/src/info.ts ADDED
@@ -0,0 +1,202 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { resolve } from 'node:path';
3
+ import type { LoadedStack } from './parser.js';
4
+ import { hasRepo, type Service, type Stack } from './schema.js';
5
+
6
+ // Patrones que sugieren "el usuario debería completar esto antes de prod".
7
+ const PLACEHOLDER_PATTERNS = [
8
+ /^changeit-/i,
9
+ /^dev-default-/i,
10
+ /^replace-me/i,
11
+ /^tbd$/i,
12
+ /^insert-/i,
13
+ /^your-/i,
14
+ ];
15
+
16
+ const C = supportsColor()
17
+ ? {
18
+ reset: '\x1b[0m',
19
+ dim: '\x1b[2m',
20
+ bold: '\x1b[1m',
21
+ cyan: '\x1b[36m',
22
+ yellow: '\x1b[33m',
23
+ green: '\x1b[32m',
24
+ red: '\x1b[31m',
25
+ }
26
+ : { reset: '', dim: '', bold: '', cyan: '', yellow: '', green: '', red: '' };
27
+
28
+ function supportsColor(): boolean {
29
+ return process.stdout.isTTY === true && process.env.NO_COLOR === undefined;
30
+ }
31
+
32
+ export function renderInfo(loaded: LoadedStack): string {
33
+ const out: string[] = [];
34
+ const { stack } = loaded;
35
+
36
+ const services = Object.entries(stack.services);
37
+ const gw = stack.gateway;
38
+
39
+ out.push(
40
+ header(`${stack.name}`) +
41
+ ` ${C.dim}${services.length} services${gw ? ` · gateway en :${gw.port}` : ''}${C.reset}`,
42
+ );
43
+
44
+ out.push('');
45
+ out.push(section('Services'));
46
+ for (const [name, svc] of services) {
47
+ out.push(` ${formatServiceRow(name, svc)}`);
48
+ }
49
+
50
+ const repos = collectRepos(stack);
51
+ if (repos.length > 0) {
52
+ out.push('');
53
+ out.push(section('Repos (om sync)'));
54
+ const maxName = Math.max(...repos.map((r) => r.slug.length));
55
+ for (const r of repos) {
56
+ const refLabel = r.ref ?? `${C.dim}(default branch)${C.reset}`;
57
+ out.push(` ${r.slug.padEnd(maxName + 2)}${refLabel}`);
58
+ }
59
+ }
60
+
61
+ const dbs = collectDatabases(stack);
62
+ if (dbs.length > 0) {
63
+ out.push('');
64
+ out.push(section('Databases'));
65
+ for (const db of dbs) {
66
+ out.push(` ${db.service}: ${db.names.join(', ')}`);
67
+ }
68
+ }
69
+
70
+ if (gw) {
71
+ out.push('');
72
+ out.push(section('Endpoints (gateway)'));
73
+ const sorted = [...gw.routes].sort(
74
+ (a, b) => effectiveLen(b.path) - effectiveLen(a.path),
75
+ );
76
+ const maxUrl = Math.max(
77
+ ...sorted.map((r) => urlFor(gw.port, r.path).length),
78
+ );
79
+ for (const route of sorted) {
80
+ const url = urlFor(gw.port, route.path);
81
+ const note = route.strip_prefix ? ` ${C.dim}(strip prefix)${C.reset}` : '';
82
+ out.push(` ${url.padEnd(maxUrl + 2)}→ ${route.service}${note}`);
83
+ }
84
+ }
85
+
86
+ const debugConfigs = services.filter(([, s]) => s.debug_port !== undefined);
87
+ if (debugConfigs.length > 0) {
88
+ out.push('');
89
+ out.push(section('Debug (VS Code attach via om vscode)'));
90
+ for (const [name, svc] of debugConfigs) {
91
+ out.push(` ${name.padEnd(30)} attach → localhost:${svc.debug_port}`);
92
+ }
93
+ }
94
+
95
+ const concerns = collectConcerns(stack, loaded.workDir);
96
+ if (concerns.length > 0) {
97
+ out.push('');
98
+ 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
+ out.push(` ${k}${C.dim}${c.reason}${C.reset}`);
103
+ }
104
+ } else {
105
+ out.push('');
106
+ out.push(`${C.green}✓${C.reset} no detecté placeholders ni valores vacíos`);
107
+ }
108
+
109
+ return out.join('\n') + '\n';
110
+ }
111
+
112
+ function header(text: string): string {
113
+ return `${C.bold}${text}${C.reset}`;
114
+ }
115
+
116
+ function section(text: string): string {
117
+ return `${C.cyan}${text}${C.reset}`;
118
+ }
119
+
120
+ function formatServiceRow(name: string, svc: Service): string {
121
+ const image = svc.image ?? (hasRepo(svc) ? `build (${svc.build})` : '?');
122
+ const repoLabel = hasRepo(svc) ? ` ${C.dim}(${shortenRepo(svc.repo)})${C.reset}` : '';
123
+ const ports: string[] = [];
124
+ if (svc.expose_host !== undefined) ports.push(`host:${svc.expose_host}`);
125
+ if (svc.debug_port !== undefined) ports.push(`debug:${svc.debug_port}`);
126
+ const portLabel = ports.length > 0 ? ` ${ports.join(' ')}` : '';
127
+ const kind = svc.kind === 'oneshot' ? ` ${C.dim}[oneshot]${C.reset}` : '';
128
+ return `${name.padEnd(30)} ${image}${repoLabel}${kind}${portLabel}`;
129
+ }
130
+
131
+ function shortenRepo(repo: string): string {
132
+ // github.com/logieinc/foo.git → foo, /local → local
133
+ return repo
134
+ .replace(/\.git$/, '')
135
+ .split(/[/:]/)
136
+ .pop() ?? repo;
137
+ }
138
+
139
+ function urlFor(port: number, path: string): string {
140
+ const cleaned = path.replace(/^=\s*/, '');
141
+ return `http://localhost:${port}${cleaned === '/' ? '/' : cleaned}`;
142
+ }
143
+
144
+ function effectiveLen(path: string): number {
145
+ return path.replace(/^(=|\^~)\s*/, '').length;
146
+ }
147
+
148
+ type RepoEntry = { slug: string; ref: string | undefined };
149
+
150
+ function collectRepos(stack: Stack): RepoEntry[] {
151
+ const seen = new Map<string, RepoEntry>();
152
+ for (const svc of Object.values(stack.services)) {
153
+ if (!hasRepo(svc)) continue;
154
+ const slug = svc.repo
155
+ .replace(/\.git$/, '')
156
+ .split(/[/:]/)
157
+ .pop()!;
158
+ if (!seen.has(slug)) seen.set(slug, { slug, ref: svc.ref });
159
+ }
160
+ return [...seen.values()];
161
+ }
162
+
163
+ function collectDatabases(stack: Stack): Array<{ service: string; names: string[] }> {
164
+ const out: Array<{ service: string; names: string[] }> = [];
165
+ for (const [name, svc] of Object.entries(stack.services)) {
166
+ if (svc.databases && svc.databases.length > 0) {
167
+ out.push({ service: name, names: [...svc.databases] });
168
+ }
169
+ }
170
+ return out;
171
+ }
172
+
173
+ type Concern = { service: string; key: string; reason: string };
174
+
175
+ function collectConcerns(stack: Stack, workDir: string): Concern[] {
176
+ const out: Concern[] = [];
177
+
178
+ for (const [svcName, svc] of Object.entries(stack.services)) {
179
+ for (const [k, vRaw] of Object.entries(svc.env ?? {})) {
180
+ const v = String(vRaw);
181
+
182
+ if (v === '') {
183
+ out.push({ service: svcName, key: k, reason: 'vacío' });
184
+ continue;
185
+ }
186
+
187
+ if (PLACEHOLDER_PATTERNS.some((re) => re.test(v))) {
188
+ out.push({ service: svcName, key: k, reason: `placeholder: "${truncate(v, 30)}"` });
189
+ continue;
190
+ }
191
+
192
+ // ${file:...} ya fue expandido en el parse — si llegamos acá y no
193
+ // existe, el parse hubiera tirado error. No re-chequear acá.
194
+ }
195
+ }
196
+
197
+ return out;
198
+ }
199
+
200
+ function truncate(s: string, max: number): string {
201
+ return s.length > max ? `${s.slice(0, max - 1)}…` : s;
202
+ }