orch-mini 0.1.4 → 0.1.5
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 +25 -8
- package/src/parser.ts +49 -4
package/package.json
CHANGED
package/src/info.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { resolve } from 'node:path';
|
|
1
|
+
import { relative } from 'node:path';
|
|
3
2
|
import type { LoadedStack } from './parser.js';
|
|
4
3
|
import { hasRepo, type Service, type Stack } from './schema.js';
|
|
5
4
|
|
|
@@ -35,6 +34,7 @@ function supportsColor(): boolean {
|
|
|
35
34
|
export function renderInfo(loaded: LoadedStack): string {
|
|
36
35
|
const out: string[] = [];
|
|
37
36
|
const { stack } = loaded;
|
|
37
|
+
const sourceLabel = relative(process.cwd(), loaded.sourcePath) || loaded.sourcePath;
|
|
38
38
|
|
|
39
39
|
const services = Object.entries(stack.services);
|
|
40
40
|
const gw = stack.gateway;
|
|
@@ -42,7 +42,7 @@ export function renderInfo(loaded: LoadedStack): string {
|
|
|
42
42
|
out.push(
|
|
43
43
|
`${C.bold}${stack.name}${C.reset} ${C.dim}${services.length} services${
|
|
44
44
|
gw ? ` · gateway en :${gw.port}` : ''
|
|
45
|
-
}${C.reset}`,
|
|
45
|
+
} · ${sourceLabel}${C.reset}`,
|
|
46
46
|
);
|
|
47
47
|
|
|
48
48
|
out.push('');
|
|
@@ -59,6 +59,7 @@ export function renderInfo(loaded: LoadedStack): string {
|
|
|
59
59
|
for (const r of repos) {
|
|
60
60
|
const refLabel = r.ref ?? `${C.dim}(default branch)${C.reset}`;
|
|
61
61
|
out.push(` ${r.slug.padEnd(maxName + 2)}${refLabel}`);
|
|
62
|
+
out.push(` ${' '.repeat(maxName + 2)}${C.dim}${r.url}${C.reset}`);
|
|
62
63
|
}
|
|
63
64
|
}
|
|
64
65
|
|
|
@@ -97,7 +98,7 @@ export function renderInfo(loaded: LoadedStack): string {
|
|
|
97
98
|
}
|
|
98
99
|
}
|
|
99
100
|
|
|
100
|
-
const concerns = collectConcerns(
|
|
101
|
+
const concerns = collectConcerns(loaded);
|
|
101
102
|
if (concerns.length > 0) {
|
|
102
103
|
out.push('');
|
|
103
104
|
out.push(`${C.yellow}⚠${C.reset} ${C.bold}Probablemente quieras tocar${C.reset}`);
|
|
@@ -107,6 +108,9 @@ export function renderInfo(loaded: LoadedStack): string {
|
|
|
107
108
|
out.push(` ${C.cyan}${svcName}${C.reset}`);
|
|
108
109
|
for (const c of items) {
|
|
109
110
|
out.push(` ${renderConcernLine(c)}`);
|
|
111
|
+
if (c.location) {
|
|
112
|
+
out.push(` ${DESC_INDENT}${C.dim}${c.location}${C.reset}`);
|
|
113
|
+
}
|
|
110
114
|
if (c.description) {
|
|
111
115
|
for (const line of wrapText(c.description, WRAP_WIDTH - DESC_INDENT.length)) {
|
|
112
116
|
out.push(` ${DESC_INDENT}${C.dim}${line}${C.reset}`);
|
|
@@ -114,6 +118,15 @@ export function renderInfo(loaded: LoadedStack): string {
|
|
|
114
118
|
}
|
|
115
119
|
}
|
|
116
120
|
}
|
|
121
|
+
|
|
122
|
+
out.push('');
|
|
123
|
+
const requiredCount = concerns.filter((c) => c.required).length;
|
|
124
|
+
const optionalCount = concerns.length - requiredCount;
|
|
125
|
+
out.push(
|
|
126
|
+
` ${C.dim}Total:${C.reset} ${
|
|
127
|
+
requiredCount > 0 ? `${C.red}${requiredCount} required${C.reset}` : `${C.green}0 required${C.reset}`
|
|
128
|
+
}${C.dim} · ${optionalCount} opcionales${C.reset}`,
|
|
129
|
+
);
|
|
117
130
|
} else {
|
|
118
131
|
out.push('');
|
|
119
132
|
out.push(`${C.green}✓${C.reset} no detecté placeholders ni valores vacíos`);
|
|
@@ -191,14 +204,14 @@ function effectiveLen(path: string): number {
|
|
|
191
204
|
return path.replace(/^(=|\^~)\s*/, '').length;
|
|
192
205
|
}
|
|
193
206
|
|
|
194
|
-
type RepoEntry = { slug: string; ref: string | undefined };
|
|
207
|
+
type RepoEntry = { slug: string; ref: string | undefined; url: string };
|
|
195
208
|
|
|
196
209
|
function collectRepos(stack: Stack): RepoEntry[] {
|
|
197
210
|
const seen = new Map<string, RepoEntry>();
|
|
198
211
|
for (const svc of Object.values(stack.services)) {
|
|
199
212
|
if (!hasRepo(svc)) continue;
|
|
200
213
|
const slug = svc.repo.replace(/\.git$/, '').split(/[/:]/).pop()!;
|
|
201
|
-
if (!seen.has(slug)) seen.set(slug, { slug, ref: svc.ref });
|
|
214
|
+
if (!seen.has(slug)) seen.set(slug, { slug, ref: svc.ref, url: svc.repo });
|
|
202
215
|
}
|
|
203
216
|
return [...seen.values()];
|
|
204
217
|
}
|
|
@@ -219,12 +232,14 @@ type Concern = {
|
|
|
219
232
|
reason: string;
|
|
220
233
|
description?: string;
|
|
221
234
|
required?: boolean;
|
|
235
|
+
location?: string; // "arch/stack.yaml:42" — clickeable en VS Code terminal
|
|
222
236
|
};
|
|
223
237
|
|
|
224
|
-
function collectConcerns(
|
|
238
|
+
function collectConcerns(loaded: LoadedStack): Concern[] {
|
|
225
239
|
const out: Concern[] = [];
|
|
240
|
+
const sourceRel = relative(process.cwd(), loaded.sourcePath) || loaded.sourcePath;
|
|
226
241
|
|
|
227
|
-
for (const [svcName, svc] of Object.entries(stack.services)) {
|
|
242
|
+
for (const [svcName, svc] of Object.entries(loaded.stack.services)) {
|
|
228
243
|
const meta = svc.env_meta ?? {};
|
|
229
244
|
for (const [k, vRaw] of Object.entries(svc.env ?? {})) {
|
|
230
245
|
const v = String(vRaw);
|
|
@@ -240,6 +255,8 @@ function collectConcerns(stack: Stack): Concern[] {
|
|
|
240
255
|
const concern: Concern = { service: svcName, key: k, reason };
|
|
241
256
|
if (m.description) concern.description = m.description;
|
|
242
257
|
if (m.required) concern.required = m.required;
|
|
258
|
+
const loc = loaded.locations.get(`${svcName}.${k}`);
|
|
259
|
+
if (loc) concern.location = `${sourceRel}:${loc.line}:${loc.col}`;
|
|
243
260
|
out.push(concern);
|
|
244
261
|
}
|
|
245
262
|
}
|
package/src/parser.ts
CHANGED
|
@@ -1,17 +1,22 @@
|
|
|
1
1
|
import { readFileSync } from 'node:fs';
|
|
2
2
|
import { basename, dirname, resolve } from 'node:path';
|
|
3
|
-
import {
|
|
3
|
+
import { isMap, isPair, isScalar, LineCounter, parseDocument } from 'yaml';
|
|
4
4
|
import { z } from 'zod';
|
|
5
5
|
import { expandFileRefs } from './file-refs.js';
|
|
6
6
|
import { findStack, STACK_FILENAME, STACK_SUBDIRS } from './discover.js';
|
|
7
7
|
import { stackSchema, type Stack } from './schema.js';
|
|
8
8
|
|
|
9
|
+
// Mapa de "service.envKey" → {line, col} del archivo fuente.
|
|
10
|
+
// Habilita reportes con archivo:línea clickeables en VS Code terminal.
|
|
11
|
+
export type Locations = Map<string, { line: number; col: number }>;
|
|
12
|
+
|
|
9
13
|
export type LoadedStack = {
|
|
10
14
|
stack: Stack;
|
|
11
15
|
sourcePath: string;
|
|
12
16
|
workDir: string; // dirname(stack.yaml)
|
|
13
17
|
workspaceRoot: string; // donde van repos/ y .stack/ — padre del workDir si el yaml vive en arch/
|
|
14
18
|
outDir: string; // workspaceRoot/.stack
|
|
19
|
+
locations: Locations;
|
|
15
20
|
};
|
|
16
21
|
|
|
17
22
|
export function loadStack(stackPath?: string): LoadedStack {
|
|
@@ -36,18 +41,23 @@ export function loadStack(stackPath?: string): LoadedStack {
|
|
|
36
41
|
throw new Error(`no se pudo leer el stack en ${absPath}: ${msg}`);
|
|
37
42
|
}
|
|
38
43
|
|
|
39
|
-
|
|
44
|
+
const lineCounter = new LineCounter();
|
|
45
|
+
let docNode: ReturnType<typeof parseDocument>;
|
|
40
46
|
try {
|
|
41
|
-
|
|
47
|
+
docNode = parseDocument(raw, { lineCounter });
|
|
42
48
|
} catch (err) {
|
|
43
49
|
const msg = err instanceof Error ? err.message : String(err);
|
|
44
50
|
throw new Error(`YAML inválido en ${absPath}: ${msg}`);
|
|
45
51
|
}
|
|
46
52
|
|
|
47
53
|
const workDir = resolve(absPath, '..');
|
|
54
|
+
const locations = collectLocations(docNode, lineCounter);
|
|
55
|
+
|
|
56
|
+
// Plain JS desde el AST para los pasos siguientes.
|
|
57
|
+
const docPlain = docNode.toJSON();
|
|
48
58
|
|
|
49
59
|
// Expandir ${file:path} antes de validar — paths se resuelven relativo al workDir.
|
|
50
|
-
const expanded = expandFileRefs(
|
|
60
|
+
const expanded = expandFileRefs(docPlain, workDir);
|
|
51
61
|
|
|
52
62
|
// Normalizar el formato mixed de env (string | {value, description, required})
|
|
53
63
|
// a dos campos planos: env (Record<string,string>) + env_meta (descripciones).
|
|
@@ -66,9 +76,44 @@ export function loadStack(stackPath?: string): LoadedStack {
|
|
|
66
76
|
workDir,
|
|
67
77
|
workspaceRoot,
|
|
68
78
|
outDir: resolve(workspaceRoot, '.stack'),
|
|
79
|
+
locations,
|
|
69
80
|
};
|
|
70
81
|
}
|
|
71
82
|
|
|
83
|
+
// Recorre el AST del YAML y extrae line/col de cada services.<svc>.env.<KEY>.
|
|
84
|
+
// El path es el del KEY del par (donde el usuario debería pararse para editar).
|
|
85
|
+
function collectLocations(
|
|
86
|
+
doc: ReturnType<typeof parseDocument>,
|
|
87
|
+
lineCounter: LineCounter,
|
|
88
|
+
): Locations {
|
|
89
|
+
const locations: Locations = new Map();
|
|
90
|
+
const services = doc.get('services', true);
|
|
91
|
+
if (!isMap(services)) return locations;
|
|
92
|
+
|
|
93
|
+
for (const svcPair of services.items) {
|
|
94
|
+
if (!isPair(svcPair) || !isScalar(svcPair.key)) continue;
|
|
95
|
+
const svcName = String(svcPair.key.value);
|
|
96
|
+
|
|
97
|
+
const svcNode = svcPair.value;
|
|
98
|
+
if (!isMap(svcNode)) continue;
|
|
99
|
+
|
|
100
|
+
const envNode = svcNode.get('env', true);
|
|
101
|
+
if (!isMap(envNode)) continue;
|
|
102
|
+
|
|
103
|
+
for (const envPair of envNode.items) {
|
|
104
|
+
if (!isPair(envPair) || !isScalar(envPair.key)) continue;
|
|
105
|
+
const keyNode = envPair.key;
|
|
106
|
+
const key = String(keyNode.value);
|
|
107
|
+
const range = keyNode.range;
|
|
108
|
+
if (!range) continue;
|
|
109
|
+
const pos = lineCounter.linePos(range[0]);
|
|
110
|
+
locations.set(`${svcName}.${key}`, { line: pos.line, col: pos.col });
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return locations;
|
|
115
|
+
}
|
|
116
|
+
|
|
72
117
|
function computeWorkspaceRoot(workDir: string): string {
|
|
73
118
|
// Si el stack.yaml vive en un subdir convencional (arch/), el workspaceRoot
|
|
74
119
|
// es el padre — repos/ y .stack/ no se mezclan con la definición.
|