@umbral/cli 0.0.1

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/dist/index.js ADDED
@@ -0,0 +1,1223 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { Command } from "commander";
5
+
6
+ // src/detect/node.ts
7
+ import { existsSync, readFileSync } from "fs";
8
+ import { join } from "path";
9
+ var NodeDetector = class {
10
+ detect(projectPath) {
11
+ const pkgPath = join(projectPath, "package.json");
12
+ if (!existsSync(pkgPath)) return [];
13
+ const results = [];
14
+ let pkg;
15
+ try {
16
+ pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
17
+ } catch {
18
+ return [];
19
+ }
20
+ const engines = pkg.engines;
21
+ const nodeVersion = engines?.node ?? "";
22
+ results.push({
23
+ category: "runtime",
24
+ name: `Node.js${nodeVersion ? ` (${nodeVersion})` : ""}`,
25
+ slug: "nodejs",
26
+ confidence: 1,
27
+ evidence: ["package.json existe"],
28
+ metadata: { nodeVersion }
29
+ });
30
+ if (existsSync(join(projectPath, "pnpm-lock.yaml"))) {
31
+ results.push({ category: "package-manager", name: "pnpm", slug: "pnpm", confidence: 1, evidence: ["pnpm-lock.yaml"], metadata: {} });
32
+ } else if (existsSync(join(projectPath, "yarn.lock"))) {
33
+ results.push({ category: "package-manager", name: "Yarn", slug: "yarn", confidence: 1, evidence: ["yarn.lock"], metadata: {} });
34
+ } else if (existsSync(join(projectPath, "bun.lockb"))) {
35
+ results.push({ category: "package-manager", name: "Bun", slug: "bun", confidence: 1, evidence: ["bun.lockb"], metadata: {} });
36
+ } else if (existsSync(join(projectPath, "package-lock.json"))) {
37
+ results.push({ category: "package-manager", name: "npm", slug: "npm", confidence: 1, evidence: ["package-lock.json"], metadata: {} });
38
+ }
39
+ return results;
40
+ }
41
+ };
42
+
43
+ // src/detect/framework.ts
44
+ import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
45
+ import { join as join2 } from "path";
46
+ function readDeps(projectPath) {
47
+ try {
48
+ const pkg = JSON.parse(readFileSync2(join2(projectPath, "package.json"), "utf-8"));
49
+ return {
50
+ deps: pkg.dependencies ?? {},
51
+ devDeps: pkg.devDependencies ?? {}
52
+ };
53
+ } catch {
54
+ return { deps: {}, devDeps: {} };
55
+ }
56
+ }
57
+ var FrameworkDetector = class {
58
+ detect(projectPath) {
59
+ const { deps, devDeps } = readDeps(projectPath);
60
+ const all = { ...devDeps, ...deps };
61
+ const results = [];
62
+ if (all["next"]) {
63
+ const version = deps["next"] ?? devDeps["next"] ?? "";
64
+ const hasAppDir = existsSync2(join2(projectPath, "app")) || existsSync2(join2(projectPath, "src", "app"));
65
+ const hasPagesDir = existsSync2(join2(projectPath, "pages")) || existsSync2(join2(projectPath, "src", "pages"));
66
+ const router = hasAppDir ? "App Router" : hasPagesDir ? "Pages Router" : "App Router";
67
+ const slug = hasAppDir || !hasPagesDir ? "nextjs-app-router" : "nextjs-pages";
68
+ results.push({
69
+ category: "framework",
70
+ name: `Next.js ${version.replace("^", "")} (${router})`,
71
+ slug,
72
+ confidence: 1,
73
+ evidence: [`next@${version} en dependencies`, `${router} detectado`],
74
+ metadata: { version: version.replace("^", ""), router: router.toLowerCase() }
75
+ });
76
+ } else if (all["nuxt"]) {
77
+ results.push({ category: "framework", name: "Nuxt", slug: "nuxt", confidence: 1, evidence: ["nuxt en dependencies"], metadata: {} });
78
+ } else if (all["@remix-run/node"] || all["@remix-run/react"]) {
79
+ results.push({ category: "framework", name: "Remix", slug: "remix", confidence: 1, evidence: ["@remix-run en dependencies"], metadata: {} });
80
+ } else if (all["express"]) {
81
+ results.push({ category: "framework", name: "Express.js", slug: "express", confidence: 1, evidence: ["express en dependencies"], metadata: {} });
82
+ } else if (all["hono"]) {
83
+ results.push({ category: "framework", name: "Hono", slug: "hono", confidence: 1, evidence: ["hono en dependencies"], metadata: {} });
84
+ } else if (all["fastify"]) {
85
+ results.push({ category: "framework", name: "Fastify", slug: "fastify", confidence: 1, evidence: ["fastify en dependencies"], metadata: {} });
86
+ }
87
+ return results;
88
+ }
89
+ };
90
+
91
+ // src/detect/database.ts
92
+ import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
93
+ import { join as join3 } from "path";
94
+ function readDeps2(projectPath) {
95
+ try {
96
+ const pkg = JSON.parse(readFileSync3(join3(projectPath, "package.json"), "utf-8"));
97
+ return { ...pkg.devDependencies ?? {}, ...pkg.dependencies ?? {} };
98
+ } catch {
99
+ return {};
100
+ }
101
+ }
102
+ function detectPrismaProvider(projectPath) {
103
+ const schemaPath = join3(projectPath, "prisma", "schema.prisma");
104
+ if (!existsSync3(schemaPath)) return null;
105
+ try {
106
+ const content = readFileSync3(schemaPath, "utf-8");
107
+ const match = content.match(/provider\s*=\s*"(\w+)"/);
108
+ return match?.[1] ?? null;
109
+ } catch {
110
+ return null;
111
+ }
112
+ }
113
+ var DatabaseDetector = class {
114
+ detect(projectPath) {
115
+ const deps = readDeps2(projectPath);
116
+ const results = [];
117
+ if (deps["@prisma/client"] || deps["prisma"]) {
118
+ const provider = detectPrismaProvider(projectPath);
119
+ const dbName = provider === "postgresql" ? "PostgreSQL" : provider === "mysql" ? "MySQL" : provider === "sqlite" ? "SQLite" : provider === "mongodb" ? "MongoDB" : "SQL";
120
+ const slug = `${(provider ?? "sql").toLowerCase()}-prisma`;
121
+ results.push({
122
+ category: "database",
123
+ name: `${dbName} (Prisma)`,
124
+ slug,
125
+ confidence: provider ? 1 : 0.7,
126
+ evidence: [
127
+ "@prisma/client en dependencies",
128
+ ...provider ? [`provider: ${provider} en prisma/schema.prisma`] : []
129
+ ],
130
+ metadata: { orm: "prisma", provider: provider ?? "unknown" }
131
+ });
132
+ } else if (deps["drizzle-orm"]) {
133
+ const hasPostgres = !!deps["pg"] || !!deps["postgres"] || !!deps["@neondatabase/serverless"];
134
+ const slug = hasPostgres ? "postgresql-drizzle" : "sql-drizzle";
135
+ results.push({
136
+ category: "database",
137
+ name: `${hasPostgres ? "PostgreSQL" : "SQL"} (Drizzle)`,
138
+ slug,
139
+ confidence: 0.9,
140
+ evidence: ["drizzle-orm en dependencies"],
141
+ metadata: { orm: "drizzle" }
142
+ });
143
+ } else if (deps["mongoose"]) {
144
+ results.push({
145
+ category: "database",
146
+ name: "MongoDB (Mongoose)",
147
+ slug: "mongodb-mongoose",
148
+ confidence: 1,
149
+ evidence: ["mongoose en dependencies"],
150
+ metadata: { orm: "mongoose" }
151
+ });
152
+ } else if (deps["better-sqlite3"]) {
153
+ results.push({
154
+ category: "database",
155
+ name: "SQLite (better-sqlite3)",
156
+ slug: "sqlite-bettersqlite3",
157
+ confidence: 1,
158
+ evidence: ["better-sqlite3 en dependencies"],
159
+ metadata: { orm: "better-sqlite3" }
160
+ });
161
+ } else if (deps["pg"]) {
162
+ results.push({
163
+ category: "database",
164
+ name: "PostgreSQL (pg)",
165
+ slug: "postgresql-pg",
166
+ confidence: 0.9,
167
+ evidence: ["pg en dependencies"],
168
+ metadata: { orm: "pg" }
169
+ });
170
+ }
171
+ return results;
172
+ }
173
+ };
174
+
175
+ // src/detect/testing.ts
176
+ import { readFileSync as readFileSync4 } from "fs";
177
+ import { join as join4 } from "path";
178
+ var TestingDetector = class {
179
+ detect(projectPath) {
180
+ let deps = {};
181
+ try {
182
+ const pkg = JSON.parse(readFileSync4(join4(projectPath, "package.json"), "utf-8"));
183
+ deps = { ...pkg.devDependencies ?? {}, ...pkg.dependencies ?? {} };
184
+ } catch {
185
+ return [];
186
+ }
187
+ if (deps["vitest"]) {
188
+ return [{ category: "testing", name: "Vitest", slug: "vitest", confidence: 1, evidence: ["vitest en devDependencies"], metadata: {} }];
189
+ }
190
+ if (deps["jest"]) {
191
+ return [{ category: "testing", name: "Jest", slug: "jest", confidence: 1, evidence: ["jest en devDependencies"], metadata: {} }];
192
+ }
193
+ if (deps["@playwright/test"]) {
194
+ return [{ category: "testing", name: "Playwright", slug: "playwright", confidence: 1, evidence: ["@playwright/test en devDependencies"], metadata: {} }];
195
+ }
196
+ if (deps["cypress"]) {
197
+ return [{ category: "testing", name: "Cypress", slug: "cypress", confidence: 1, evidence: ["cypress en devDependencies"], metadata: {} }];
198
+ }
199
+ return [];
200
+ }
201
+ };
202
+
203
+ // src/detect/build.ts
204
+ import { existsSync as existsSync4, readFileSync as readFileSync5 } from "fs";
205
+ import { join as join5 } from "path";
206
+ var BuildDetector = class {
207
+ detect(projectPath) {
208
+ const results = [];
209
+ if (existsSync4(join5(projectPath, "turbo.json"))) {
210
+ results.push({ category: "build", name: "Turborepo", slug: "turborepo", confidence: 1, evidence: ["turbo.json existe"], metadata: {} });
211
+ } else if (existsSync4(join5(projectPath, "nx.json"))) {
212
+ results.push({ category: "build", name: "Nx", slug: "nx", confidence: 1, evidence: ["nx.json existe"], metadata: {} });
213
+ }
214
+ let deps = {};
215
+ try {
216
+ const pkg = JSON.parse(readFileSync5(join5(projectPath, "package.json"), "utf-8"));
217
+ deps = { ...pkg.devDependencies ?? {}, ...pkg.dependencies ?? {} };
218
+ } catch {
219
+ return results;
220
+ }
221
+ if (!results.length && deps["vite"]) {
222
+ results.push({ category: "build", name: "Vite", slug: "vite", confidence: 0.8, evidence: ["vite en devDependencies"], metadata: {} });
223
+ }
224
+ return results;
225
+ }
226
+ };
227
+
228
+ // src/detect/styling.ts
229
+ import { existsSync as existsSync5, readFileSync as readFileSync6 } from "fs";
230
+ import { join as join6 } from "path";
231
+ var StylingDetector = class {
232
+ detect(projectPath) {
233
+ let deps = {};
234
+ try {
235
+ const pkg = JSON.parse(readFileSync6(join6(projectPath, "package.json"), "utf-8"));
236
+ deps = { ...pkg.devDependencies ?? {}, ...pkg.dependencies ?? {} };
237
+ } catch {
238
+ return [];
239
+ }
240
+ if (deps["tailwindcss"]) {
241
+ const hasConfig = existsSync5(join6(projectPath, "tailwind.config.ts")) || existsSync5(join6(projectPath, "tailwind.config.js"));
242
+ return [{
243
+ category: "styling",
244
+ name: "Tailwind CSS",
245
+ slug: "tailwindcss",
246
+ confidence: hasConfig ? 1 : 0.8,
247
+ evidence: ["tailwindcss en devDependencies", ...hasConfig ? ["tailwind.config.* existe"] : []],
248
+ metadata: {}
249
+ }];
250
+ }
251
+ if (deps["styled-components"]) {
252
+ return [{ category: "styling", name: "Styled Components", slug: "styled-components", confidence: 1, evidence: ["styled-components en dependencies"], metadata: {} }];
253
+ }
254
+ if (deps["sass"] || deps["node-sass"]) {
255
+ return [{ category: "styling", name: "Sass", slug: "sass", confidence: 1, evidence: ["sass en devDependencies"], metadata: {} }];
256
+ }
257
+ return [];
258
+ }
259
+ };
260
+
261
+ // src/analyze.ts
262
+ var DETECTORS = [
263
+ new NodeDetector(),
264
+ new FrameworkDetector(),
265
+ new DatabaseDetector(),
266
+ new TestingDetector(),
267
+ new BuildDetector(),
268
+ new StylingDetector()
269
+ ];
270
+ function analyzeProject(projectPath) {
271
+ const detections = DETECTORS.flatMap((d) => d.detect(projectPath));
272
+ detections.sort((a, b) => b.confidence - a.confidence);
273
+ return { projectPath, detections };
274
+ }
275
+
276
+ // src/templates.ts
277
+ var TEMPLATES = [
278
+ {
279
+ slugs: ["nextjs-app-router"],
280
+ cognitiveLevel: "navigator",
281
+ complexityTier: 2,
282
+ title: () => "Next.js App Router como framework frontend",
283
+ decision: (d) => `El frontend utiliza Next.js ${d.metadata.version ?? ""} con App Router para renderizado y routing.`.trim(),
284
+ mechanism: () => "App Router con Server Components por defecto. Rutas en app/. API routes en app/api/. Client Components marcados explicitamente con 'use client'.",
285
+ rationale: () => "Next.js App Router provee Server Components, streaming y layouts anidados. Simplifica el data-fetching server-side y mejora el rendimiento versus client-side rendering puro.",
286
+ alternatives: () => [
287
+ { option: "Pages Router", rejectedBecause: "Patron legacy; App Router es el futuro de Next.js." },
288
+ { option: "Remix", rejectedBecause: "Ecosistema mas reducido; Next.js tiene mayor adopcion y soporte." }
289
+ ],
290
+ antiPatterns: () => [
291
+ "No usar 'use client' en componentes que pueden ser Server Components.",
292
+ "No hacer fetch de datos en componentes cliente cuando un Server Component puede hacerlo.",
293
+ "No importar modulos de servidor (fs, db) en componentes cliente."
294
+ ]
295
+ },
296
+ {
297
+ slugs: ["nextjs-pages"],
298
+ cognitiveLevel: "navigator",
299
+ complexityTier: 2,
300
+ title: () => "Next.js Pages Router como framework frontend",
301
+ decision: (d) => `El frontend utiliza Next.js ${d.metadata.version ?? ""} con Pages Router.`.trim(),
302
+ mechanism: () => "Pages Router con getServerSideProps/getStaticProps para data-fetching. Rutas basadas en la estructura de pages/.",
303
+ rationale: () => "Pages Router es el modelo estable y probado de Next.js. El proyecto ya esta estructurado con este patron.",
304
+ alternatives: () => [
305
+ { option: "App Router", rejectedBecause: "Requiere migracion significativa del codebase existente." }
306
+ ],
307
+ antiPatterns: () => [
308
+ "No mezclar patrones de App Router (server components) con Pages Router.",
309
+ "No hacer fetching de datos en el cliente si getServerSideProps puede proveerlos."
310
+ ]
311
+ },
312
+ {
313
+ slugs: ["express"],
314
+ cognitiveLevel: "navigator",
315
+ complexityTier: 1,
316
+ title: () => "Express.js como servidor HTTP",
317
+ decision: () => "El backend utiliza Express.js como servidor HTTP principal.",
318
+ mechanism: () => "Middlewares encadenados para request processing. Rutas definidas con app.get/post/etc. Error handling centralizado.",
319
+ rationale: () => "Express es el framework HTTP mas adoptado en Node.js. Ecosistema maduro de middlewares y amplia documentacion.",
320
+ alternatives: () => [
321
+ { option: "Fastify", rejectedBecause: "Menor base de middlewares existentes." },
322
+ { option: "Hono", rejectedBecause: "Framework mas nuevo con menor adopcion." }
323
+ ],
324
+ antiPatterns: () => [
325
+ "No manejar errores async sin try-catch o middleware de error.",
326
+ "No almacenar estado mutable en variables globales del servidor."
327
+ ]
328
+ },
329
+ {
330
+ slugs: ["postgresql-prisma"],
331
+ cognitiveLevel: "anchor",
332
+ complexityTier: 2,
333
+ title: () => "PostgreSQL via Prisma ORM como persistencia relacional",
334
+ decision: () => "La persistencia relacional utiliza PostgreSQL con Prisma ORM para type-safety y migraciones.",
335
+ mechanism: () => "Prisma schema en prisma/schema.prisma define los modelos. Migraciones via prisma migrate. Cliente generado provee queries type-safe.",
336
+ rationale: () => "Prisma genera tipos TypeScript desde el schema, eliminando divergencia entre modelo de datos y codigo. Las migraciones son declarativas y reproducibles.",
337
+ alternatives: () => [
338
+ { option: "Drizzle ORM", rejectedBecause: "Menor madurez en tooling de migraciones." },
339
+ { option: "TypeORM", rejectedBecause: "Decoradores y patrones Active Record anaden complejidad." },
340
+ { option: "SQL crudo", rejectedBecause: "Sin type-safety; propenso a errores en queries complejas." }
341
+ ],
342
+ antiPatterns: () => [
343
+ "No ejecutar queries SQL crudas fuera de Prisma si el ORM lo soporta.",
344
+ "No modificar el schema de la DB sin crear una migracion.",
345
+ "No ignorar las validaciones de tipos generados por Prisma."
346
+ ]
347
+ },
348
+ {
349
+ slugs: ["postgresql-drizzle", "sql-drizzle"],
350
+ cognitiveLevel: "anchor",
351
+ complexityTier: 2,
352
+ title: () => "PostgreSQL via Drizzle ORM",
353
+ decision: () => "La persistencia utiliza Drizzle ORM para queries SQL type-safe con control granular.",
354
+ mechanism: () => "Schema definido en TypeScript con drizzle-kit para migraciones. Queries builder type-safe sin magic strings.",
355
+ rationale: () => "Drizzle ofrece control SQL granular con type-safety completo. Menor overhead que ORMs tradicionales.",
356
+ alternatives: () => [
357
+ { option: "Prisma", rejectedBecause: "Mayor abstraccion que puede ocultar queries ineficientes." }
358
+ ],
359
+ antiPatterns: () => [
360
+ "No mezclar queries raw SQL con el query builder de Drizzle.",
361
+ "No modificar tablas sin generar migraciones con drizzle-kit."
362
+ ]
363
+ },
364
+ {
365
+ slugs: ["mongodb-mongoose"],
366
+ cognitiveLevel: "anchor",
367
+ complexityTier: 2,
368
+ title: () => "MongoDB via Mongoose como persistencia documental",
369
+ decision: () => "La persistencia utiliza MongoDB con Mongoose para modelado de documentos.",
370
+ mechanism: () => "Schemas de Mongoose definen la estructura de documentos. Validacion en la capa de aplicacion. Indices definidos en el schema.",
371
+ rationale: () => "MongoDB permite esquemas flexibles para datos no-relacionales. Mongoose agrega validacion y tipado sobre la API nativa.",
372
+ alternatives: () => [
373
+ { option: "PostgreSQL", rejectedBecause: "Requiere esquema rigido para datos con estructura variable." }
374
+ ],
375
+ antiPatterns: () => [
376
+ "No almacenar relaciones complejas que requieren joins frecuentes.",
377
+ "No omitir indices en campos usados en queries frecuentes."
378
+ ]
379
+ },
380
+ {
381
+ slugs: ["sqlite-bettersqlite3"],
382
+ cognitiveLevel: "anchor",
383
+ complexityTier: 1,
384
+ title: () => "SQLite via better-sqlite3 como persistencia local",
385
+ decision: () => "La persistencia utiliza SQLite con better-sqlite3 para almacenamiento local sin servidor.",
386
+ mechanism: () => "Base de datos en un unico archivo. Queries sincronas via better-sqlite3. WAL mode para lecturas concurrentes.",
387
+ rationale: () => "SQLite elimina la necesidad de un servidor de base de datos externo. Ideal para aplicaciones local-first y embebidas.",
388
+ alternatives: () => [
389
+ { option: "PostgreSQL", rejectedBecause: "Requiere servidor externo; over-engineering para uso local." }
390
+ ],
391
+ antiPatterns: () => [
392
+ "No abrir multiples conexiones de escritura concurrentes.",
393
+ "No usar SQLite para cargas de escritura masivas concurrentes."
394
+ ]
395
+ },
396
+ {
397
+ slugs: ["mysql-prisma"],
398
+ cognitiveLevel: "anchor",
399
+ complexityTier: 2,
400
+ title: () => "MySQL via Prisma ORM como persistencia relacional",
401
+ decision: () => "La persistencia relacional utiliza MySQL con Prisma ORM.",
402
+ mechanism: () => "Prisma schema define los modelos con provider mysql. Migraciones via prisma migrate.",
403
+ rationale: () => "MySQL es un motor relacional maduro y ampliamente soportado. Prisma agrega type-safety y migraciones declarativas.",
404
+ alternatives: () => [
405
+ { option: "PostgreSQL", rejectedBecause: "Proyecto ya configurado con MySQL." }
406
+ ],
407
+ antiPatterns: () => [
408
+ "No ejecutar queries crudas fuera de Prisma.",
409
+ "No modificar el schema sin generar migraciones."
410
+ ]
411
+ },
412
+ {
413
+ slugs: ["turborepo"],
414
+ cognitiveLevel: "navigator",
415
+ complexityTier: 2,
416
+ title: () => "Turborepo como orquestador de monorepo",
417
+ decision: () => "El monorepo utiliza Turborepo para orquestacion de builds, tests y tareas.",
418
+ mechanism: () => "turbo.json define el pipeline de tareas con dependsOn y cache. Cada paquete declara sus scripts en package.json.",
419
+ rationale: () => "Turborepo provee caching incremental y ejecucion paralela. Reduce tiempos de build significativamente en monorepos.",
420
+ alternatives: () => [
421
+ { option: "Nx", rejectedBecause: "Mayor complejidad de configuracion para proyectos medianos." },
422
+ { option: "Lerna", rejectedBecause: "Menor rendimiento sin caching nativo." }
423
+ ],
424
+ antiPatterns: () => [
425
+ "No saltarse el pipeline de Turbo ejecutando scripts directamente en paquetes interdependientes.",
426
+ "No ignorar los outputs de cache en turbo.json."
427
+ ]
428
+ }
429
+ ];
430
+ function getTemplate(detection) {
431
+ return TEMPLATES.find((t) => t.slugs.includes(detection.slug)) ?? null;
432
+ }
433
+
434
+ // src/generate.ts
435
+ function generateProposals(detections) {
436
+ const proposals = [];
437
+ let counter = 1;
438
+ for (const detection of detections) {
439
+ const template = getTemplate(detection);
440
+ if (!template) continue;
441
+ const now = (/* @__PURE__ */ new Date()).toISOString();
442
+ const id = `EDE-${String(counter).padStart(3, "0")}-${detection.slug}`;
443
+ proposals.push({
444
+ id,
445
+ title: template.title(detection),
446
+ version: 1,
447
+ status: "proposed",
448
+ cognitiveLevel: template.cognitiveLevel,
449
+ complexityTier: template.complexityTier,
450
+ whatAndHow: {
451
+ decision: template.decision(detection),
452
+ mechanism: template.mechanism(detection)
453
+ },
454
+ why: {
455
+ rationale: template.rationale(detection),
456
+ alternativesConsidered: template.alternatives(detection),
457
+ references: []
458
+ },
459
+ whatNotToDo: {
460
+ antiPatterns: template.antiPatterns(detection)
461
+ },
462
+ whatsNext: {
463
+ continuations: [],
464
+ openQuestions: []
465
+ },
466
+ contracts: {
467
+ layerContracts: [],
468
+ verifiedBy: []
469
+ },
470
+ tests: {
471
+ unitTests: [],
472
+ sadPaths: [],
473
+ coverageTarget: 0.8
474
+ },
475
+ provenance: {
476
+ phase: "onboarding",
477
+ slice: null,
478
+ createdBy: "umbral-cli",
479
+ createdAt: now,
480
+ lastUpdated: now
481
+ }
482
+ });
483
+ counter++;
484
+ }
485
+ return proposals;
486
+ }
487
+
488
+ // src/ui.ts
489
+ import { createInterface } from "readline/promises";
490
+ import { stdin, stdout } from "process";
491
+ async function reviewEde(ede, index, total) {
492
+ const rl = createInterface({ input: stdin, output: stdout });
493
+ stdout.write(`
494
+ ${index}/${total} ${ede.id}
495
+ `);
496
+ stdout.write(` ${ede.title}
497
+ `);
498
+ stdout.write(` Decision: ${ede.whatAndHow.decision}
499
+ `);
500
+ stdout.write(` [A]ceptar [E]ditar [S]altar
501
+ `);
502
+ const answer = await rl.question(" > ");
503
+ const choice = answer.trim().toLowerCase();
504
+ if (choice === "e" || choice === "editar") {
505
+ const title = await rl.question(` Titulo [enter = mantener]: `);
506
+ const decision = await rl.question(` Decision [enter = mantener]: `);
507
+ const rationale = await rl.question(` Rationale [enter = mantener]: `);
508
+ rl.close();
509
+ return {
510
+ action: "accept",
511
+ edits: {
512
+ title: title.trim() || void 0,
513
+ decision: decision.trim() || void 0,
514
+ rationale: rationale.trim() || void 0
515
+ }
516
+ };
517
+ }
518
+ rl.close();
519
+ if (choice === "s" || choice === "saltar") return { action: "skip" };
520
+ return { action: "accept" };
521
+ }
522
+
523
+ // src/setup/database.ts
524
+ import { homedir as homedir2 } from "os";
525
+
526
+ // ../persistence/src/db.ts
527
+ import Database from "better-sqlite3";
528
+ import * as sqliteVec from "sqlite-vec";
529
+ import { mkdirSync } from "fs";
530
+ import { homedir } from "os";
531
+ import { dirname } from "path";
532
+
533
+ // ../persistence/src/migrate.ts
534
+ var migrations = [
535
+ {
536
+ id: "001_edes",
537
+ up: `CREATE TABLE IF NOT EXISTS edes (
538
+ id TEXT PRIMARY KEY,
539
+ data TEXT NOT NULL,
540
+ title TEXT NOT NULL,
541
+ status TEXT NOT NULL,
542
+ cognitive_level TEXT NOT NULL,
543
+ created_at TEXT,
544
+ updated_at TEXT
545
+ )`
546
+ },
547
+ {
548
+ id: "002_grill_sessions",
549
+ up: `CREATE TABLE IF NOT EXISTS grill_sessions (
550
+ id TEXT PRIMARY KEY,
551
+ ede_id TEXT NOT NULL,
552
+ data TEXT NOT NULL,
553
+ status TEXT NOT NULL,
554
+ alignment_score REAL NOT NULL DEFAULT 0,
555
+ created_at TEXT NOT NULL,
556
+ updated_at TEXT NOT NULL
557
+ );
558
+ CREATE TABLE IF NOT EXISTS cognitive_debts (
559
+ id TEXT PRIMARY KEY,
560
+ session_id TEXT NOT NULL,
561
+ ede_id TEXT NOT NULL,
562
+ gap REAL NOT NULL,
563
+ reason TEXT NOT NULL,
564
+ created_at TEXT NOT NULL
565
+ )`
566
+ },
567
+ {
568
+ id: "003_semantic_embeddings",
569
+ up: `CREATE TABLE IF NOT EXISTS session_nodes (
570
+ id TEXT PRIMARY KEY,
571
+ session_id TEXT NOT NULL,
572
+ type TEXT NOT NULL,
573
+ content TEXT NOT NULL,
574
+ metadata TEXT,
575
+ source_doc_node TEXT NOT NULL,
576
+ created_at TEXT NOT NULL
577
+ );
578
+ CREATE VIRTUAL TABLE IF NOT EXISTS session_nodes_fts USING fts5(
579
+ node_id UNINDEXED, content, session_id UNINDEXED, type UNINDEXED
580
+ );
581
+ CREATE TABLE IF NOT EXISTS doc_chunks (
582
+ rowid INTEGER PRIMARY KEY AUTOINCREMENT,
583
+ id TEXT UNIQUE NOT NULL,
584
+ doc_id TEXT NOT NULL,
585
+ content TEXT NOT NULL,
586
+ source_doc_node TEXT NOT NULL,
587
+ created_at TEXT NOT NULL
588
+ );
589
+ CREATE VIRTUAL TABLE IF NOT EXISTS doc_chunks_vec USING vec0(
590
+ embedding float[384]
591
+ )`
592
+ },
593
+ {
594
+ id: "004_terminal_sessions",
595
+ up: `CREATE TABLE IF NOT EXISTS terminal_sessions (
596
+ id TEXT PRIMARY KEY,
597
+ ede_id TEXT,
598
+ pid INTEGER NOT NULL,
599
+ status TEXT NOT NULL DEFAULT 'active',
600
+ working_directory TEXT NOT NULL,
601
+ claude_md_path TEXT,
602
+ created_at TEXT NOT NULL,
603
+ last_activity_at TEXT NOT NULL,
604
+ terminated_at TEXT
605
+ )`
606
+ }
607
+ ];
608
+ function runMigrations(db) {
609
+ db.exec(
610
+ `CREATE TABLE IF NOT EXISTS _migrations (
611
+ id TEXT PRIMARY KEY,
612
+ applied_at TEXT NOT NULL
613
+ )`
614
+ );
615
+ const applied = new Set(
616
+ db.prepare("SELECT id FROM _migrations").pluck().all()
617
+ );
618
+ for (const m of migrations) {
619
+ if (!applied.has(m.id)) {
620
+ db.exec(m.up);
621
+ db.prepare("INSERT INTO _migrations (id, applied_at) VALUES (?, ?)").run(
622
+ m.id,
623
+ (/* @__PURE__ */ new Date()).toISOString()
624
+ );
625
+ }
626
+ }
627
+ }
628
+
629
+ // ../persistence/src/db.ts
630
+ var DEFAULT_DB_PATH = `${homedir()}/.umbral/umbral.db`;
631
+ function openDb(path = DEFAULT_DB_PATH) {
632
+ mkdirSync(dirname(path), { recursive: true });
633
+ const db = new Database(path);
634
+ try {
635
+ sqliteVec.load(db);
636
+ } catch (err) {
637
+ db.close();
638
+ throw new Error(
639
+ `[S13] sqlite-vec extension failed to load \u2014 the app will not start. Cause: ${err instanceof Error ? err.message : err}`
640
+ );
641
+ }
642
+ db.pragma("journal_mode = WAL");
643
+ const ok = db.pragma("integrity_check", { simple: true });
644
+ if (ok !== "ok") {
645
+ db.close();
646
+ throw new Error("[S13] DB integrity check failed \u2014 the app will not start.");
647
+ }
648
+ runMigrations(db);
649
+ return db;
650
+ }
651
+
652
+ // ../persistence/src/ede-store.ts
653
+ import { z } from "zod";
654
+ var edeSchema = z.object({
655
+ id: z.string(),
656
+ title: z.string(),
657
+ version: z.number(),
658
+ status: z.enum(["proposed", "accepted", "deprecated"]),
659
+ cognitiveLevel: z.enum(["explorer", "navigator", "anchor"]),
660
+ complexityTier: z.union([z.literal(1), z.literal(2), z.literal(3)]),
661
+ whatAndHow: z.object({ decision: z.string(), mechanism: z.string() }),
662
+ why: z.object({
663
+ rationale: z.string(),
664
+ alternativesConsidered: z.array(
665
+ z.object({ option: z.string(), rejectedBecause: z.string() })
666
+ ),
667
+ references: z.array(z.string())
668
+ }),
669
+ whatNotToDo: z.object({ antiPatterns: z.array(z.string()) }),
670
+ whatsNext: z.object({
671
+ continuations: z.array(z.string()),
672
+ openQuestions: z.array(z.string())
673
+ }),
674
+ contracts: z.object({
675
+ layerContracts: z.array(z.string()),
676
+ verifiedBy: z.array(z.string())
677
+ }),
678
+ tests: z.object({
679
+ unitTests: z.array(z.string()),
680
+ sadPaths: z.array(z.string()),
681
+ coverageTarget: z.number()
682
+ }),
683
+ provenance: z.object({
684
+ phase: z.string(),
685
+ slice: z.number().nullable(),
686
+ createdBy: z.string(),
687
+ createdAt: z.string().nullable(),
688
+ lastUpdated: z.string().nullable()
689
+ })
690
+ });
691
+ function loadEde(json) {
692
+ const result = edeSchema.safeParse(json);
693
+ if (!result.success)
694
+ throw new Error(`[S13] EDE inv\xE1lida: ${result.error.message}`);
695
+ if (!result.data.why.rationale.trim())
696
+ throw new Error("[S13] EDE sin rationale no es una EDE.");
697
+ return result.data;
698
+ }
699
+ function createEdeStore(db) {
700
+ return {
701
+ save(ede) {
702
+ db.prepare(
703
+ `INSERT OR REPLACE INTO edes (id, data, title, status, cognitive_level, created_at, updated_at)
704
+ VALUES (?, ?, ?, ?, ?, ?, ?)`
705
+ ).run(
706
+ ede.id,
707
+ JSON.stringify(ede),
708
+ ede.title,
709
+ ede.status,
710
+ ede.cognitiveLevel,
711
+ ede.provenance.createdAt,
712
+ ede.provenance.lastUpdated
713
+ );
714
+ },
715
+ getById(id) {
716
+ const row = db.prepare("SELECT data FROM edes WHERE id = ?").get(id);
717
+ return row ? JSON.parse(row.data) : null;
718
+ },
719
+ getAll() {
720
+ const rows = db.prepare("SELECT data FROM edes ORDER BY id").all();
721
+ return rows.map((r) => JSON.parse(r.data));
722
+ }
723
+ };
724
+ }
725
+
726
+ // src/setup/database.ts
727
+ function setupDatabase(edes) {
728
+ const db = openDb();
729
+ const store = createEdeStore(db);
730
+ let saved = 0;
731
+ for (const ede of edes) {
732
+ const validated = loadEde(ede);
733
+ store.save(validated);
734
+ saved++;
735
+ }
736
+ db.close();
737
+ return { saved, dbPath: `${homedir2()}/.umbral/umbral.db` };
738
+ }
739
+
740
+ // src/setup/hooks.ts
741
+ import { existsSync as existsSync6, readFileSync as readFileSync7, writeFileSync, mkdirSync as mkdirSync2 } from "fs";
742
+ import { join as join7 } from "path";
743
+ function setupHooks(projectPath) {
744
+ const claudeDir = join7(projectPath, ".claude");
745
+ mkdirSync2(claudeDir, { recursive: true });
746
+ const settingsPath = join7(claudeDir, "settings.json");
747
+ let existing = {};
748
+ if (existsSync6(settingsPath)) {
749
+ try {
750
+ existing = JSON.parse(readFileSync7(settingsPath, "utf-8"));
751
+ } catch {
752
+ }
753
+ }
754
+ const settings = {
755
+ ...existing,
756
+ hooks: {
757
+ SessionStart: [
758
+ {
759
+ matcher: "",
760
+ hooks: [
761
+ {
762
+ type: "command",
763
+ command: "npx @umbral/cli hook session-start",
764
+ timeout: 10,
765
+ statusMessage: "Cargando contexto de gobernanza Umbral..."
766
+ }
767
+ ]
768
+ }
769
+ ],
770
+ PreToolUse: [
771
+ {
772
+ matcher: "Edit|Write",
773
+ hooks: [
774
+ {
775
+ type: "command",
776
+ command: "npx @umbral/cli hook pre-tool-use",
777
+ timeout: 10,
778
+ statusMessage: "Verificando gates Umbral..."
779
+ }
780
+ ]
781
+ }
782
+ ]
783
+ },
784
+ mcpServers: {
785
+ ...typeof existing.mcpServers === "object" && existing.mcpServers !== null ? existing.mcpServers : {},
786
+ umbral: {
787
+ command: "npx",
788
+ args: ["@umbral/cli", "mcp"]
789
+ }
790
+ }
791
+ };
792
+ writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf-8");
793
+ }
794
+
795
+ // src/setup/context.ts
796
+ import { writeFileSync as writeFileSync2, mkdirSync as mkdirSync3 } from "fs";
797
+ import { join as join8 } from "path";
798
+
799
+ // ../orchestrator/src/claude-context.ts
800
+ function assembleClaudeContext(edes) {
801
+ const accepted = edes.filter((e) => e.status === "accepted");
802
+ const sections = [];
803
+ sections.push("# Umbral \u2014 Contexto del Proyecto\n");
804
+ sections.push("> Auto-generado por Umbral. No editar manualmente.\n");
805
+ if (accepted.length === 0) {
806
+ sections.push("No hay EDEs activas.\n");
807
+ return sections.join("\n");
808
+ }
809
+ sections.push("## Decisiones Activas (EDEs)\n");
810
+ sections.push("Estas decisiones son vinculantes. Resp\xE9talas en todo c\xF3digo que generes.\n");
811
+ for (const ede of accepted) {
812
+ sections.push(`### ${ede.id}: ${ede.title}
813
+ `);
814
+ sections.push(`**Decisi\xF3n:** ${ede.whatAndHow.decision}
815
+ `);
816
+ sections.push(`**Mecanismo:** ${ede.whatAndHow.mechanism}
817
+ `);
818
+ sections.push(`**Rationale:** ${ede.why.rationale}
819
+ `);
820
+ if (ede.whatNotToDo.antiPatterns.length > 0) {
821
+ sections.push("**Anti-patterns (PROHIBIDO):**");
822
+ for (const ap of ede.whatNotToDo.antiPatterns) {
823
+ sections.push(`- ${ap}`);
824
+ }
825
+ sections.push("");
826
+ }
827
+ if (ede.contracts.layerContracts.length > 0) {
828
+ sections.push(
829
+ `**Contratos de capa:** ${ede.contracts.layerContracts.join(", ")}
830
+ `
831
+ );
832
+ }
833
+ }
834
+ sections.push("## Reglas Generales\n");
835
+ sections.push("- Fail-fast (S13): todo error de validaci\xF3n debe lanzar inmediatamente, nunca degradar en silencio.");
836
+ sections.push("- Fuente \xFAnica (S2): solo S2 detecta cambios. Otros subsistemas reaccionan al DocRegenEvent.");
837
+ sections.push("- Local-first: la base de datos vive en ~/.umbral/umbral.db (SQLite con FTS5 + sqlite-vec).");
838
+ sections.push("- No agregar dependencias externas sin justificaci\xF3n en una EDE.");
839
+ sections.push("");
840
+ return sections.join("\n");
841
+ }
842
+
843
+ // ../orchestrator/src/hook-dispatch.ts
844
+ var READ_ONLY_DIRS = ["umbral-docs/", "umbral-docs\\"];
845
+ function isReadOnlyPath(filePath) {
846
+ const normalized = filePath.replace(/\\/g, "/");
847
+ return READ_ONLY_DIRS.some((dir) => normalized.includes(dir));
848
+ }
849
+ function dispatchSessionStart(edes) {
850
+ const context = assembleClaudeContext(edes);
851
+ return { action: "context", context };
852
+ }
853
+ function dispatchPreToolUse(input, edes) {
854
+ const toolName = input.toolName ?? "";
855
+ if (toolName !== "Edit" && toolName !== "Write") {
856
+ return { action: "allow" };
857
+ }
858
+ const filePath = input.toolInput?.file_path ?? "";
859
+ if (!filePath) return { action: "allow" };
860
+ if (isReadOnlyPath(filePath)) {
861
+ return {
862
+ action: "deny",
863
+ reason: `[S14] umbral-docs/ es read-only. Las decisiones se gestionan via EDEs en la DB.`
864
+ };
865
+ }
866
+ const accepted = edes.filter((e) => e.status === "accepted");
867
+ const antiPatterns = accepted.flatMap(
868
+ (e) => e.whatNotToDo.antiPatterns.map((ap) => ({ edeId: e.id, pattern: ap }))
869
+ );
870
+ if (antiPatterns.length > 0) {
871
+ return {
872
+ action: "allow",
873
+ context: formatAntiPatternReminder(antiPatterns)
874
+ };
875
+ }
876
+ return { action: "allow" };
877
+ }
878
+ function formatAntiPatternReminder(patterns) {
879
+ const lines = patterns.map((p) => `- [${p.edeId}] ${p.pattern}`);
880
+ return `Anti-patterns activos (no violar):
881
+ ${lines.join("\n")}`;
882
+ }
883
+ function dispatchHook(input, edes) {
884
+ switch (input.hookEventName) {
885
+ case "SessionStart":
886
+ return dispatchSessionStart(edes);
887
+ case "PreToolUse":
888
+ return dispatchPreToolUse(input, edes);
889
+ default:
890
+ return { action: "allow" };
891
+ }
892
+ }
893
+
894
+ // src/setup/context.ts
895
+ function setupContext(projectPath) {
896
+ const db = openDb();
897
+ const edes = createEdeStore(db).getAll();
898
+ db.close();
899
+ const content = assembleClaudeContext(edes);
900
+ const claudeDir = join8(projectPath, ".claude");
901
+ mkdirSync3(claudeDir, { recursive: true });
902
+ writeFileSync2(join8(claudeDir, "CLAUDE.md"), content, "utf-8");
903
+ }
904
+
905
+ // src/commands/init.ts
906
+ async function initCommand(options) {
907
+ const projectPath = options.path ?? process.cwd();
908
+ const w = (s) => process.stdout.write(s);
909
+ w("\n Umbral \u2014 Inicializacion de gobernanza\n\n");
910
+ w(" Analizando proyecto...\n");
911
+ const analysis = analyzeProject(projectPath);
912
+ if (analysis.detections.length === 0) {
913
+ w(" No se detectaron tecnologias en este directorio.\n");
914
+ w(" Puedes crear EDEs manualmente despues.\n\n");
915
+ } else {
916
+ for (const d of analysis.detections) {
917
+ w(` + ${d.name}
918
+ `);
919
+ }
920
+ }
921
+ const proposals = generateProposals(analysis.detections);
922
+ if (proposals.length === 0) {
923
+ w("\n No hay propuestas de EDEs para generar.\n");
924
+ w(" Configurando infraestructura base...\n\n");
925
+ } else {
926
+ w(`
927
+ Propuestas de EDEs (${proposals.length}):
928
+ `);
929
+ }
930
+ const accepted = [];
931
+ for (let i = 0; i < proposals.length; i++) {
932
+ const ede = proposals[i];
933
+ if (options.yes) {
934
+ ede.status = "accepted";
935
+ accepted.push(ede);
936
+ w(` + ${ede.id}: ${ede.title}
937
+ `);
938
+ continue;
939
+ }
940
+ const result = await reviewEde(ede, i + 1, proposals.length);
941
+ if (result.action === "skip") {
942
+ w(" -- saltado\n");
943
+ continue;
944
+ }
945
+ if (result.edits?.title) ede.title = result.edits.title;
946
+ if (result.edits?.decision) ede.whatAndHow.decision = result.edits.decision;
947
+ if (result.edits?.rationale) ede.why.rationale = result.edits.rationale;
948
+ ede.status = "accepted";
949
+ accepted.push(ede);
950
+ w(" + aceptado\n");
951
+ }
952
+ w("\n Configurando infraestructura...\n");
953
+ if (accepted.length > 0) {
954
+ const dbResult = setupDatabase(accepted);
955
+ w(` + ${dbResult.dbPath}
956
+ `);
957
+ w(` + ${dbResult.saved} EDEs guardadas
958
+ `);
959
+ } else {
960
+ w(" - Sin EDEs para guardar\n");
961
+ }
962
+ setupHooks(projectPath);
963
+ w(" + .claude/settings.json (hooks + MCP)\n");
964
+ setupContext(projectPath);
965
+ w(" + .claude/CLAUDE.md (contexto para Claude)\n");
966
+ w("\n Umbral listo. Claude Code ahora tiene gobernanza.\n\n");
967
+ }
968
+
969
+ // src/commands/hook.ts
970
+ async function readStdin() {
971
+ const chunks = [];
972
+ for await (const chunk of process.stdin) {
973
+ chunks.push(chunk);
974
+ }
975
+ return Buffer.concat(chunks).toString("utf-8");
976
+ }
977
+ async function hookCommand(_event) {
978
+ const raw = await readStdin();
979
+ let input;
980
+ try {
981
+ input = JSON.parse(raw);
982
+ } catch {
983
+ process.exit(0);
984
+ }
985
+ let edes;
986
+ try {
987
+ const db = openDb();
988
+ edes = createEdeStore(db).getAll();
989
+ db.close();
990
+ } catch {
991
+ process.exit(0);
992
+ }
993
+ const result = dispatchHook(
994
+ {
995
+ hookEventName: input.hook_event_name,
996
+ toolName: input.tool_name,
997
+ toolInput: input.tool_input,
998
+ cwd: input.cwd,
999
+ source: input.source
1000
+ },
1001
+ edes
1002
+ );
1003
+ if (input.hook_event_name === "SessionStart" && result.action === "context") {
1004
+ process.stdout.write(
1005
+ JSON.stringify({
1006
+ hookSpecificOutput: {
1007
+ hookEventName: "SessionStart",
1008
+ additionalContext: result.context
1009
+ }
1010
+ })
1011
+ );
1012
+ return;
1013
+ }
1014
+ if (input.hook_event_name === "PreToolUse") {
1015
+ if (result.action === "deny") {
1016
+ process.stdout.write(
1017
+ JSON.stringify({
1018
+ hookSpecificOutput: {
1019
+ hookEventName: "PreToolUse",
1020
+ permissionDecision: "deny",
1021
+ permissionDecisionReason: result.reason
1022
+ }
1023
+ })
1024
+ );
1025
+ return;
1026
+ }
1027
+ if (result.context) {
1028
+ process.stdout.write(
1029
+ JSON.stringify({
1030
+ hookSpecificOutput: {
1031
+ hookEventName: "PreToolUse",
1032
+ permissionDecision: "allow"
1033
+ },
1034
+ additionalContext: result.context
1035
+ })
1036
+ );
1037
+ }
1038
+ }
1039
+ }
1040
+
1041
+ // src/commands/mcp.ts
1042
+ import { execFileSync } from "child_process";
1043
+ import { join as join9, dirname as dirname2 } from "path";
1044
+ import { fileURLToPath } from "url";
1045
+ async function mcpCommand() {
1046
+ const __dirname = dirname2(fileURLToPath(import.meta.url));
1047
+ const mcpEntry = join9(__dirname, "mcp-entry.js");
1048
+ execFileSync(process.execPath, [mcpEntry], {
1049
+ stdio: "inherit"
1050
+ });
1051
+ }
1052
+
1053
+ // src/commands/start.ts
1054
+ import { execSync, spawn } from "child_process";
1055
+ import { writeFileSync as writeFileSync3, mkdirSync as mkdirSync4 } from "fs";
1056
+ import { join as join10 } from "path";
1057
+ import { homedir as homedir3 } from "os";
1058
+ import { createServer } from "net";
1059
+ var UMBRAL_DIR = join10(homedir3(), ".umbral");
1060
+ var COMPOSE_PATH = join10(UMBRAL_DIR, "docker-compose.yml");
1061
+ var DOCKER_IMAGE = process.env.UMBRAL_IMAGE ?? "umbral/web:latest";
1062
+ function dockerInstalled() {
1063
+ try {
1064
+ execSync("docker --version", { stdio: "ignore" });
1065
+ return true;
1066
+ } catch {
1067
+ return false;
1068
+ }
1069
+ }
1070
+ function composeInstalled() {
1071
+ try {
1072
+ execSync("docker compose version", { stdio: "ignore" });
1073
+ return true;
1074
+ } catch {
1075
+ return false;
1076
+ }
1077
+ }
1078
+ function findFreePort(start) {
1079
+ return new Promise((resolve, reject) => {
1080
+ const server = createServer();
1081
+ server.listen(start, "127.0.0.1", () => {
1082
+ const addr = server.address();
1083
+ const port = typeof addr === "object" && addr ? addr.port : start;
1084
+ server.close(() => resolve(port));
1085
+ });
1086
+ server.on("error", () => {
1087
+ if (start < 65535) resolve(findFreePort(start + 1));
1088
+ else reject(new Error("No free port found"));
1089
+ });
1090
+ });
1091
+ }
1092
+ function generateCompose(webPort, wsPort) {
1093
+ const dbPath = join10(UMBRAL_DIR, "umbral.db").replace(/\\/g, "/");
1094
+ const dbDir = UMBRAL_DIR.replace(/\\/g, "/");
1095
+ return `# Auto-generated by Umbral CLI \u2014 do not edit manually
1096
+ name: umbral
1097
+
1098
+ services:
1099
+ neo4j:
1100
+ image: neo4j:5-community
1101
+ ports:
1102
+ - "7474:7474"
1103
+ - "7687:7687"
1104
+ environment:
1105
+ NEO4J_AUTH: neo4j/umbral-dev
1106
+ volumes:
1107
+ - neo4j-data:/data
1108
+ healthcheck:
1109
+ test: ["CMD", "neo4j", "status"]
1110
+ interval: 10s
1111
+ timeout: 5s
1112
+ retries: 5
1113
+
1114
+ web:
1115
+ image: ${DOCKER_IMAGE}
1116
+ ports:
1117
+ - "${webPort}:3000"
1118
+ - "${wsPort}:3099"
1119
+ environment:
1120
+ NEO4J_URI: bolt://neo4j:7687
1121
+ NEO4J_USER: neo4j
1122
+ NEO4J_PASSWORD: umbral-dev
1123
+ UMBRAL_WS_PORT: "3099"
1124
+ UMBRAL_DB_PATH: /data/umbral.db
1125
+ volumes:
1126
+ - "${dbDir}:/data"
1127
+ depends_on:
1128
+ neo4j:
1129
+ condition: service_healthy
1130
+
1131
+ volumes:
1132
+ neo4j-data:
1133
+ `;
1134
+ }
1135
+ async function startCommand(options) {
1136
+ const w = (s) => process.stdout.write(s);
1137
+ w("\n Umbral \u2014 Iniciando plataforma\n\n");
1138
+ if (!dockerInstalled()) {
1139
+ w(" \u2717 Docker no esta instalado.\n");
1140
+ w(" Instala Docker Desktop: https://docs.docker.com/get-docker/\n\n");
1141
+ process.exit(1);
1142
+ }
1143
+ if (!composeInstalled()) {
1144
+ w(" \u2717 Docker Compose no esta disponible.\n");
1145
+ w(" Actualiza Docker Desktop o instala el plugin Compose.\n\n");
1146
+ process.exit(1);
1147
+ }
1148
+ w(" \u2713 Docker detectado\n");
1149
+ const webPort = options.port ? parseInt(options.port, 10) : await findFreePort(3e3);
1150
+ const wsPort = await findFreePort(webPort + 99);
1151
+ mkdirSync4(UMBRAL_DIR, { recursive: true });
1152
+ const compose = generateCompose(webPort, wsPort);
1153
+ writeFileSync3(COMPOSE_PATH, compose, "utf-8");
1154
+ w(` \u2713 docker-compose.yml generado
1155
+ `);
1156
+ w(" \u27F3 Descargando imagenes y levantando servicios...\n\n");
1157
+ const detach = options.detach !== false;
1158
+ try {
1159
+ if (detach) {
1160
+ execSync(`docker compose -f "${COMPOSE_PATH}" up -d`, {
1161
+ stdio: "inherit"
1162
+ });
1163
+ } else {
1164
+ const child = spawn("docker", ["compose", "-f", COMPOSE_PATH, "up"], {
1165
+ stdio: "inherit"
1166
+ });
1167
+ child.on("close", (code) => process.exit(code ?? 0));
1168
+ return;
1169
+ }
1170
+ } catch {
1171
+ w("\n \u2717 Error al iniciar los servicios.\n");
1172
+ w(" Revisa que Docker Desktop este corriendo.\n\n");
1173
+ process.exit(1);
1174
+ }
1175
+ w(`
1176
+ \u2713 Umbral corriendo
1177
+ `);
1178
+ w(` Dashboard: http://localhost:${webPort}
1179
+ `);
1180
+ w(` Neo4j: http://localhost:7474
1181
+ `);
1182
+ w(` WebSocket: ws://localhost:${wsPort}
1183
+
1184
+ `);
1185
+ w(` Para detener: umbral stop
1186
+
1187
+ `);
1188
+ }
1189
+
1190
+ // src/commands/stop.ts
1191
+ import { execSync as execSync2 } from "child_process";
1192
+ import { existsSync as existsSync8 } from "fs";
1193
+ import { join as join11 } from "path";
1194
+ import { homedir as homedir4 } from "os";
1195
+ var COMPOSE_PATH2 = join11(homedir4(), ".umbral", "docker-compose.yml");
1196
+ function stopCommand() {
1197
+ const w = (s) => process.stdout.write(s);
1198
+ w("\n Umbral \u2014 Deteniendo plataforma\n\n");
1199
+ if (!existsSync8(COMPOSE_PATH2)) {
1200
+ w(" \u2717 No se encontro docker-compose.yml en ~/.umbral/\n");
1201
+ w(" Ejecuta 'umbral start' primero.\n\n");
1202
+ process.exit(1);
1203
+ }
1204
+ try {
1205
+ execSync2(`docker compose -f "${COMPOSE_PATH2}" down`, {
1206
+ stdio: "inherit"
1207
+ });
1208
+ w("\n \u2713 Servicios detenidos.\n\n");
1209
+ } catch {
1210
+ w("\n \u2717 Error al detener los servicios.\n\n");
1211
+ process.exit(1);
1212
+ }
1213
+ }
1214
+
1215
+ // src/index.ts
1216
+ var program = new Command();
1217
+ program.name("umbral").description("Umbral \u2014 Framework de gobernanza para proyectos con Claude Code").version("0.0.1");
1218
+ program.command("init").description("Inicializar Umbral en el proyecto actual").option("--yes", "Aceptar todas las propuestas sin preguntar").option("--path <path>", "Ruta al proyecto (default: directorio actual)").action(initCommand);
1219
+ program.command("start").description("Levantar la plataforma Umbral (Neo4j + Dashboard web)").option("--port <port>", "Puerto para el dashboard (default: auto)").option("--no-detach", "Correr en primer plano (sin -d)").action(startCommand);
1220
+ program.command("stop").description("Detener la plataforma Umbral").action(stopCommand);
1221
+ program.command("hook <event>").description("Despachar un hook event de Claude Code (stdin/stdout)").action(hookCommand);
1222
+ program.command("mcp").description("Iniciar el servidor MCP de Umbral (transporte stdio)").action(mcpCommand);
1223
+ program.parse();