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.
- package/package.json +1 -1
- package/src/cli.ts +11 -0
- package/src/info.ts +202 -0
package/package.json
CHANGED
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
|
+
}
|