orch-mini 0.1.2 → 0.1.3

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.2",
3
+ "version": "0.1.3",
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
@@ -99,7 +99,12 @@ export function renderInfo(loaded: LoadedStack): string {
99
99
  const maxKey = Math.max(...concerns.map((c) => `${c.service}.${c.key}`.length));
100
100
  for (const c of concerns) {
101
101
  const k = `${c.service}.${c.key}`.padEnd(maxKey + 2);
102
- out.push(` ${k}${C.dim}${c.reason}${C.reset}`);
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}`);
107
+ }
103
108
  }
104
109
  } else {
105
110
  out.push('');
@@ -170,31 +175,47 @@ function collectDatabases(stack: Stack): Array<{ service: string; names: string[
170
175
  return out;
171
176
  }
172
177
 
173
- type Concern = { service: string; key: string; reason: string };
178
+ type Concern = {
179
+ service: string;
180
+ key: string;
181
+ reason: string;
182
+ description?: string;
183
+ required?: boolean;
184
+ };
174
185
 
175
- function collectConcerns(stack: Stack, workDir: string): Concern[] {
186
+ function collectConcerns(stack: Stack, _workDir: string): Concern[] {
176
187
  const out: Concern[] = [];
177
188
 
178
189
  for (const [svcName, svc] of Object.entries(stack.services)) {
190
+ const meta = svc.env_meta ?? {};
179
191
  for (const [k, vRaw] of Object.entries(svc.env ?? {})) {
180
192
  const v = String(vRaw);
181
-
182
- if (v === '') {
183
- out.push({ service: svcName, key: k, reason: 'vacío' });
184
- continue;
193
+ const m = meta[k] ?? {};
194
+
195
+ let reason: string | null = null;
196
+ if (v === '') reason = 'vacío';
197
+ 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.
185
201
  }
186
202
 
187
- if (PLACEHOLDER_PATTERNS.some((re) => re.test(v))) {
188
- out.push({ service: svcName, key: k, reason: `placeholder: "${truncate(v, 30)}"` });
189
- continue;
203
+ if (reason !== null) {
204
+ const concern: Concern = { service: svcName, key: k, reason };
205
+ if (m.description) concern.description = m.description;
206
+ if (m.required) concern.required = m.required;
207
+ out.push(concern);
190
208
  }
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
209
  }
195
210
  }
196
211
 
197
- return out;
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
+ });
198
219
  }
199
220
 
200
221
  function truncate(s: string, max: number): string {
package/src/parser.ts CHANGED
@@ -49,7 +49,11 @@ export function loadStack(stackPath?: string): LoadedStack {
49
49
  // Expandir ${file:path} antes de validar — paths se resuelven relativo al workDir.
50
50
  const expanded = expandFileRefs(doc, workDir);
51
51
 
52
- const result = stackSchema.safeParse(expanded);
52
+ // Normalizar el formato mixed de env (string | {value, description, required})
53
+ // a dos campos planos: env (Record<string,string>) + env_meta (descripciones).
54
+ const normalized = normalizeEnvMetadata(expanded);
55
+
56
+ const result = stackSchema.safeParse(normalized);
53
57
  if (!result.success) {
54
58
  throw new Error(formatZodError(result.error, absPath));
55
59
  }
@@ -74,6 +78,46 @@ function computeWorkspaceRoot(workDir: string): string {
74
78
  return workDir;
75
79
  }
76
80
 
81
+ // Recorre stack.services.*.env y si algún valor es un objeto con shape
82
+ // { value, description?, required? }, splittea: env[k] = String(value),
83
+ // env_meta[k] = { description?, required? }. Los valores que ya son string,
84
+ // number o boolean pasan tal cual.
85
+ function normalizeEnvMetadata(doc: unknown): unknown {
86
+ if (!isPlainObject(doc)) return doc;
87
+ const services = isPlainObject(doc.services) ? doc.services : undefined;
88
+ if (!services) return doc;
89
+
90
+ const newServices: Record<string, unknown> = {};
91
+ for (const [svcName, svc] of Object.entries(services)) {
92
+ if (!isPlainObject(svc) || !isPlainObject(svc.env)) {
93
+ newServices[svcName] = svc;
94
+ continue;
95
+ }
96
+ const env: Record<string, unknown> = {};
97
+ const env_meta: Record<string, { description?: string; required?: boolean }> = {};
98
+ for (const [k, v] of Object.entries(svc.env)) {
99
+ if (isPlainObject(v) && ('value' in v || 'description' in v || 'required' in v)) {
100
+ env[k] = v.value ?? '';
101
+ const meta: { description?: string; required?: boolean } = {};
102
+ if (typeof v.description === 'string') meta.description = v.description;
103
+ if (typeof v.required === 'boolean') meta.required = v.required;
104
+ if (Object.keys(meta).length > 0) env_meta[k] = meta;
105
+ } else {
106
+ env[k] = v;
107
+ }
108
+ }
109
+ const newSvc: Record<string, unknown> = { ...svc, env };
110
+ if (Object.keys(env_meta).length > 0) newSvc.env_meta = env_meta;
111
+ newServices[svcName] = newSvc;
112
+ }
113
+
114
+ return { ...doc, services: newServices };
115
+ }
116
+
117
+ function isPlainObject(value: unknown): value is Record<string, unknown> {
118
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
119
+ }
120
+
77
121
  function formatZodError(err: z.ZodError, source: string): string {
78
122
  const lines = err.issues.map((issue) => {
79
123
  const path = issue.path.length > 0 ? issue.path.join('.') : '(root)';
package/src/schema.ts CHANGED
@@ -8,6 +8,15 @@ const serviceNameSchema = z
8
8
  const envValueSchema = z.union([z.string(), z.number(), z.boolean()]).transform(String);
9
9
  const envMapSchema = z.record(z.string(), envValueSchema);
10
10
 
11
+ // Metadata opcional por env var: description + required.
12
+ // El parser pre-normaliza el formato mixed (string vs {value,description,required})
13
+ // dejando `env` como Record<string,string> y `env_meta` como esta estructura.
14
+ const envMetaEntrySchema = z.object({
15
+ description: z.string().optional(),
16
+ required: z.boolean().optional(),
17
+ });
18
+ const envMetaMapSchema = z.record(z.string(), envMetaEntrySchema);
19
+
11
20
  const routeSchema = z.object({
12
21
  path: z.string().min(1),
13
22
  service: serviceNameSchema,
@@ -44,6 +53,7 @@ const serviceSchema = z
44
53
  port: z.number().int().positive().optional(),
45
54
  debug_port: z.number().int().positive().optional(),
46
55
  env: envMapSchema.optional(),
56
+ env_meta: envMetaMapSchema.optional(),
47
57
  needs: z.array(serviceNameSchema).optional(),
48
58
  expose_host: z.number().int().positive().optional(),
49
59
  volumes: z.array(z.string()).optional(),