orch-mini 0.1.1 → 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 +1 -1
- package/src/info.ts +35 -14
- package/src/parser.ts +45 -1
- package/src/schema.ts +10 -0
package/package.json
CHANGED
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
|
-
|
|
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 = {
|
|
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,
|
|
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
|
-
|
|
183
|
-
|
|
184
|
-
|
|
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 (
|
|
188
|
-
|
|
189
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(),
|