codarch 0.1.4 → 0.2.0

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/index.js CHANGED
@@ -2095,8 +2095,8 @@ var MIGRATION_EXTENSIONS = Object.keys(EXTENSION_LANGUAGE);
2095
2095
  var LANGUAGE_ORDER = ["ts", "js", "rust", "wasm", "node"];
2096
2096
  var RUST_BRIDGE = /#\[\s*(?:napi|wasm_bindgen)(?:\(|\])|\bnapi_derive\b|\bnapi::|\bwasm_bindgen(?:::|\b)|\bwasm-pack\b/;
2097
2097
  var JS_FFI = /\bWebAssembly\s*\.\s*(?:instantiate|compile|Module|Instance)\b|\bprocess\.dlopen\b/;
2098
- function languageForPath(relativePath2) {
2099
- const lower = relativePath2.toLowerCase();
2098
+ function languageForPath(relativePath3) {
2099
+ const lower = relativePath3.toLowerCase();
2100
2100
  const dot = lower.lastIndexOf(".");
2101
2101
  if (dot === -1) return null;
2102
2102
  return EXTENSION_LANGUAGE[lower.slice(dot)] ?? null;
@@ -2519,8 +2519,8 @@ function buildSnapshot(input) {
2519
2519
  }
2520
2520
  function migrationLensFrom(migration) {
2521
2521
  const nodes = {};
2522
- for (const [relativePath2, node] of migration.nodes) {
2523
- nodes[fileId(relativePath2)] = node;
2522
+ for (const [relativePath3, node] of migration.nodes) {
2523
+ nodes[fileId(relativePath3)] = node;
2524
2524
  }
2525
2525
  const edges = migration.edges.map((edge) => ({
2526
2526
  fromId: fileId(edge.from),
@@ -2616,17 +2616,17 @@ function walkSourceFiles(fileSystem, options = {}) {
2616
2616
  (left, right) => left.name < right.name ? -1 : left.name > right.name ? 1 : 0
2617
2617
  );
2618
2618
  for (const entry of entries) {
2619
- const relativePath2 = directory.relativePath ? `${directory.relativePath}/${entry.name}` : entry.name;
2619
+ const relativePath3 = directory.relativePath ? `${directory.relativePath}/${entry.name}` : entry.name;
2620
2620
  const absolutePath = directory.absolutePath === "/" ? `/${entry.name}` : `${directory.absolutePath}/${entry.name}`;
2621
2621
  if (entry.isDirectory) {
2622
- if (isIgnored(directory.layers, relativePath2, true)) continue;
2623
- const nested = readGitignore(fileSystem, absolutePath, relativePath2, warnings);
2624
- const layers = nested ? [...directory.layers, { base: relativePath2, matcher: ignore().add(nested) }] : directory.layers;
2625
- stack.push({ absolutePath, relativePath: relativePath2, layers });
2622
+ if (isIgnored(directory.layers, relativePath3, true)) continue;
2623
+ const nested = readGitignore(fileSystem, absolutePath, relativePath3, warnings);
2624
+ const layers = nested ? [...directory.layers, { base: relativePath3, matcher: ignore().add(nested) }] : directory.layers;
2625
+ stack.push({ absolutePath, relativePath: relativePath3, layers });
2626
2626
  } else if (entry.isFile) {
2627
- if (isIgnored(directory.layers, relativePath2, false)) continue;
2627
+ if (isIgnored(directory.layers, relativePath3, false)) continue;
2628
2628
  if (!matchesExtension(entry.name, extensions)) continue;
2629
- files.push({ absolutePath, relativePath: relativePath2 });
2629
+ files.push({ absolutePath, relativePath: relativePath3 });
2630
2630
  }
2631
2631
  }
2632
2632
  }
@@ -2647,10 +2647,10 @@ function readGitignore(fileSystem, absoluteDirectory, relativeBase, warnings) {
2647
2647
  return null;
2648
2648
  }
2649
2649
  }
2650
- function isIgnored(layers, relativePath2, isDirectory) {
2650
+ function isIgnored(layers, relativePath3, isDirectory) {
2651
2651
  let ignored = false;
2652
2652
  for (const layer of layers) {
2653
- const localPath = layer.base ? relativePath2.slice(layer.base.length + 1) : relativePath2;
2653
+ const localPath = layer.base ? relativePath3.slice(layer.base.length + 1) : relativePath3;
2654
2654
  if (!localPath) continue;
2655
2655
  const probe = isDirectory ? `${localPath}/` : localPath;
2656
2656
  const result = layer.matcher.test(probe);
@@ -2679,14 +2679,21 @@ function analyzeFiles(options) {
2679
2679
  warnings.push(...resolver.warnings);
2680
2680
  const parser = options.parser;
2681
2681
  const parsedFiles = [];
2682
- const migrationFiles = [];
2682
+ const migrationFiles2 = [];
2683
2683
  const migrationLinks = [];
2684
2684
  const links = [];
2685
2685
  const unresolved = [];
2686
2686
  let externalImports = 0;
2687
2687
  let builtinImports = 0;
2688
2688
  let typeOnlyImports = 0;
2689
+ let processed = 0;
2689
2690
  for (const file of walked.files) {
2691
+ processed += 1;
2692
+ options.onProgress?.({
2693
+ done: processed,
2694
+ total: walked.files.length,
2695
+ current: file.relativePath
2696
+ });
2690
2697
  const language = languageForPath(file.relativePath);
2691
2698
  if (!language) continue;
2692
2699
  if (isNativeLanguage(language)) {
@@ -2694,7 +2701,7 @@ function analyzeFiles(options) {
2694
2701
  relativePath: file.relativePath,
2695
2702
  metrics: { loc: 0, rloc: 0 }
2696
2703
  });
2697
- migrationFiles.push({
2704
+ migrationFiles2.push({
2698
2705
  relativePath: file.relativePath,
2699
2706
  language,
2700
2707
  loc: 0,
@@ -2720,7 +2727,7 @@ function analyzeFiles(options) {
2720
2727
  relativePath: file.relativePath,
2721
2728
  metrics: { loc: measured.loc, rloc: measured.rloc }
2722
2729
  });
2723
- migrationFiles.push({
2730
+ migrationFiles2.push({
2724
2731
  relativePath: file.relativePath,
2725
2732
  language,
2726
2733
  loc: measured.loc,
@@ -2790,7 +2797,7 @@ function analyzeFiles(options) {
2790
2797
  unresolvedImports: fileUnresolvedImports
2791
2798
  }
2792
2799
  });
2793
- migrationFiles.push({
2800
+ migrationFiles2.push({
2794
2801
  relativePath: file.relativePath,
2795
2802
  language,
2796
2803
  loc: parsed.metrics.loc ?? 0,
@@ -2828,10 +2835,10 @@ function analyzeFiles(options) {
2828
2835
  file.metrics.dependencyLevel = metrics.dependencyLevel;
2829
2836
  }
2830
2837
  }
2831
- for (const file of migrationFiles) {
2838
+ for (const file of migrationFiles2) {
2832
2839
  file.fanIn = graph.metrics.get(file.relativePath)?.fanIn ?? 0;
2833
2840
  }
2834
- const migration = buildMigration(migrationFiles, migrationLinks);
2841
+ const migration = buildMigration(migrationFiles2, migrationLinks);
2835
2842
  for (const file of parsedFiles) {
2836
2843
  const node = migration.nodes.get(file.relativePath);
2837
2844
  if (!node) continue;
@@ -3180,6 +3187,318 @@ function clamp01(value) {
3180
3187
  return Math.max(0, Math.min(1, value));
3181
3188
  }
3182
3189
 
3190
+ // packages/metrics/src/profiles.ts
3191
+ var DOMAIN_PROFILES = [
3192
+ {
3193
+ id: "capas",
3194
+ title: "Capas por convenci\xF3n",
3195
+ description: "Tres capas: el dominio no depende de nada, la aplicaci\xF3n no depende de la infraestructura y la infraestructura no depende de la aplicaci\xF3n.",
3196
+ when: "Para repos con src/domain, src/application y src/infra.",
3197
+ layers: [
3198
+ {
3199
+ name: "domain",
3200
+ patterns: [
3201
+ "src/domain/**",
3202
+ "src/core/**",
3203
+ "src/dominio/**",
3204
+ "src/nucleo/**",
3205
+ "domain/**"
3206
+ ]
3207
+ },
3208
+ {
3209
+ name: "application",
3210
+ patterns: [
3211
+ "src/application/**",
3212
+ "src/app/**",
3213
+ "src/use-cases/**",
3214
+ "src/usecases/**",
3215
+ "src/aplicacion/**",
3216
+ "src/casos-de-uso/**",
3217
+ "application/**"
3218
+ ]
3219
+ },
3220
+ {
3221
+ name: "infrastructure",
3222
+ patterns: [
3223
+ "src/infrastructure/**",
3224
+ "src/infra/**",
3225
+ "src/adapters/**",
3226
+ "src/persistence/**",
3227
+ "src/infraestructura/**",
3228
+ "src/adaptadores/**",
3229
+ "infrastructure/**"
3230
+ ]
3231
+ }
3232
+ ],
3233
+ rules: [
3234
+ {
3235
+ type: "forbidden",
3236
+ name: "dominio-sin-capas-exteriores",
3237
+ from: "domain",
3238
+ to: "application",
3239
+ severity: "warn",
3240
+ message: "El dominio no debe depender de la aplicaci\xF3n"
3241
+ },
3242
+ {
3243
+ type: "forbidden",
3244
+ name: "dominio-sin-infraestructura",
3245
+ from: "domain",
3246
+ to: "infrastructure",
3247
+ severity: "warn",
3248
+ message: "El dominio no debe depender de la infraestructura"
3249
+ },
3250
+ {
3251
+ type: "forbidden",
3252
+ name: "aplicacion-sin-infraestructura",
3253
+ from: "application",
3254
+ to: "infrastructure",
3255
+ severity: "warn",
3256
+ message: "La aplicaci\xF3n no debe depender de la infraestructura"
3257
+ }
3258
+ ]
3259
+ },
3260
+ {
3261
+ id: "hexagonal",
3262
+ title: "Ports & adapters",
3263
+ description: "El dominio en el centro, la aplicaci\xF3n con los puertos y los adaptadores por fuera: la aplicaci\xF3n y el dominio no dependen de los adaptadores.",
3264
+ when: "Para repos con puertos (interfaces) en el centro y adaptadores fuera.",
3265
+ layers: [
3266
+ {
3267
+ name: "domain",
3268
+ patterns: [
3269
+ "src/domain/**",
3270
+ "src/core/**",
3271
+ "src/model/**",
3272
+ "src/dominio/**",
3273
+ "src/modelo/**",
3274
+ "domain/**"
3275
+ ]
3276
+ },
3277
+ {
3278
+ name: "application",
3279
+ patterns: [
3280
+ "src/application/**",
3281
+ "src/app/**",
3282
+ "src/use-cases/**",
3283
+ "src/usecases/**",
3284
+ "src/ports/**",
3285
+ "src/puertos/**",
3286
+ "application/**"
3287
+ ]
3288
+ },
3289
+ {
3290
+ name: "adapters",
3291
+ patterns: [
3292
+ "src/adapters/**",
3293
+ "src/infra/**",
3294
+ "src/infrastructure/**",
3295
+ "src/persistence/**",
3296
+ "src/adaptadores/**",
3297
+ "src/infraestructura/**",
3298
+ "adapters/**"
3299
+ ]
3300
+ }
3301
+ ],
3302
+ rules: [
3303
+ {
3304
+ type: "forbidden",
3305
+ name: "dominio-sin-adaptadores",
3306
+ from: "domain",
3307
+ to: "adapters",
3308
+ severity: "warn",
3309
+ message: "El dominio no debe depender de los adaptadores"
3310
+ },
3311
+ {
3312
+ type: "forbidden",
3313
+ name: "aplicacion-sin-adaptadores",
3314
+ from: "application",
3315
+ to: "adapters",
3316
+ severity: "warn",
3317
+ message: "La aplicaci\xF3n habla con los adaptadores por puertos, no directamente"
3318
+ }
3319
+ ]
3320
+ },
3321
+ {
3322
+ id: "onion",
3323
+ title: "Clean Architecture (cebolla)",
3324
+ description: "Cuatro anillos: el dominio en el centro, luego la aplicaci\xF3n, la infraestructura y la presentaci\xF3n fuera. Las dependencias van hacia dentro y el anillo exterior no se importa desde dentro.",
3325
+ when: "Para repos con una capa de presentaci\xF3n o de interfaz propia.",
3326
+ layers: [
3327
+ {
3328
+ name: "domain",
3329
+ patterns: [
3330
+ "src/domain/**",
3331
+ "src/entities/**",
3332
+ "src/value-objects/**",
3333
+ "src/dominio/**",
3334
+ "src/entidades/**",
3335
+ "domain/**"
3336
+ ]
3337
+ },
3338
+ {
3339
+ name: "application",
3340
+ patterns: [
3341
+ "src/application/**",
3342
+ "src/use-cases/**",
3343
+ "src/usecases/**",
3344
+ "src/services/**",
3345
+ "src/aplicacion/**",
3346
+ "src/casos-de-uso/**",
3347
+ "application/**"
3348
+ ]
3349
+ },
3350
+ {
3351
+ name: "infrastructure",
3352
+ patterns: [
3353
+ "src/infrastructure/**",
3354
+ "src/infra/**",
3355
+ "src/persistence/**",
3356
+ "src/repositories/**",
3357
+ "src/infraestructura/**",
3358
+ "infrastructure/**"
3359
+ ]
3360
+ },
3361
+ {
3362
+ name: "presentation",
3363
+ patterns: [
3364
+ "src/presentation/**",
3365
+ "src/ui/**",
3366
+ "src/controllers/**",
3367
+ "src/pages/**",
3368
+ "src/components/**",
3369
+ "src/presentacion/**",
3370
+ "src/interfaz/**",
3371
+ "presentation/**"
3372
+ ]
3373
+ }
3374
+ ],
3375
+ rules: [
3376
+ {
3377
+ type: "forbidden",
3378
+ name: "dominio-sin-capas-exteriores",
3379
+ from: "domain",
3380
+ to: "application",
3381
+ severity: "warn",
3382
+ message: "El dominio no debe depender de la aplicaci\xF3n"
3383
+ },
3384
+ {
3385
+ type: "forbidden",
3386
+ name: "dominio-sin-infraestructura",
3387
+ from: "domain",
3388
+ to: "infrastructure",
3389
+ severity: "warn",
3390
+ message: "El dominio no debe depender de la infraestructura"
3391
+ },
3392
+ {
3393
+ type: "forbidden",
3394
+ name: "dominio-sin-presentacion",
3395
+ from: "domain",
3396
+ to: "presentation",
3397
+ severity: "warn",
3398
+ message: "El dominio no debe depender de la presentaci\xF3n"
3399
+ },
3400
+ {
3401
+ type: "forbidden",
3402
+ name: "aplicacion-sin-infraestructura",
3403
+ from: "application",
3404
+ to: "infrastructure",
3405
+ severity: "warn",
3406
+ message: "La aplicaci\xF3n no debe depender de la infraestructura"
3407
+ },
3408
+ {
3409
+ type: "forbidden",
3410
+ name: "aplicacion-sin-presentacion",
3411
+ from: "application",
3412
+ to: "presentation",
3413
+ severity: "warn",
3414
+ message: "La aplicaci\xF3n no debe depender de la presentaci\xF3n"
3415
+ },
3416
+ {
3417
+ type: "forbidden",
3418
+ name: "infraestructura-sin-presentacion",
3419
+ from: "infrastructure",
3420
+ to: "presentation",
3421
+ severity: "warn",
3422
+ message: "La infraestructura y la presentaci\xF3n se hablan por los puertos, no entre ellas"
3423
+ }
3424
+ ]
3425
+ },
3426
+ {
3427
+ id: "ddd",
3428
+ title: "DDD por carpetas",
3429
+ description: "Las carpetas del dominio driven design: el modelo dentro, los servicios de aplicaci\xF3n y los repositorios en la infraestructura.",
3430
+ when: "Para repos con modelo, servicios y repositorios como carpetas.",
3431
+ layers: [
3432
+ {
3433
+ name: "domain",
3434
+ patterns: [
3435
+ "src/domain/**",
3436
+ "src/model/**",
3437
+ "src/entities/**",
3438
+ "src/value-objects/**",
3439
+ "src/dominio/**",
3440
+ "src/modelo/**",
3441
+ "domain/**"
3442
+ ]
3443
+ },
3444
+ {
3445
+ name: "application",
3446
+ patterns: [
3447
+ "src/application/**",
3448
+ "src/services/**",
3449
+ "src/use-cases/**",
3450
+ "src/aplicacion/**",
3451
+ "src/servicios/**",
3452
+ "application/**"
3453
+ ]
3454
+ },
3455
+ {
3456
+ name: "infrastructure",
3457
+ patterns: [
3458
+ "src/infrastructure/**",
3459
+ "src/repositories/**",
3460
+ "src/persistence/**",
3461
+ "src/infraestructura/**",
3462
+ "src/repositorios/**",
3463
+ "infrastructure/**"
3464
+ ]
3465
+ }
3466
+ ],
3467
+ rules: [
3468
+ {
3469
+ type: "forbidden",
3470
+ name: "dominio-sin-capas-exteriores",
3471
+ from: "domain",
3472
+ to: "application",
3473
+ severity: "warn",
3474
+ message: "El modelo de dominio no debe depender de los servicios"
3475
+ },
3476
+ {
3477
+ type: "forbidden",
3478
+ name: "dominio-sin-infraestructura",
3479
+ from: "domain",
3480
+ to: "infrastructure",
3481
+ severity: "warn",
3482
+ message: "El modelo de dominio no debe depender de los repositorios"
3483
+ },
3484
+ {
3485
+ type: "forbidden",
3486
+ name: "aplicacion-sin-infraestructura",
3487
+ from: "application",
3488
+ to: "infrastructure",
3489
+ severity: "warn",
3490
+ message: "Los servicios hablan con los repositorios por puertos, no directamente"
3491
+ }
3492
+ ]
3493
+ }
3494
+ ];
3495
+ function findDomainProfile(id) {
3496
+ return DOMAIN_PROFILES.find((profile) => profile.id === id);
3497
+ }
3498
+ function domainProfileIds() {
3499
+ return DOMAIN_PROFILES.map((profile) => profile.id);
3500
+ }
3501
+
3183
3502
  // packages/render/src/model.ts
3184
3503
  var RenderError = class extends Error {
3185
3504
  constructor(message) {
@@ -4342,10 +4661,103 @@ function parseRuleConfig(input) {
4342
4661
  if (root.version !== 1) {
4343
4662
  throw new ConfigError("config.version debe ser 1");
4344
4663
  }
4345
- const layers = parseLayers(root.layers);
4664
+ const own = parseInlineProfiles(root.profiles);
4665
+ const profile = parseProfile(root.profile, own);
4666
+ const profileSeverity = parseProfileSeverity(root.profile_severity);
4667
+ const layers = mergeLayers(profile, parseLayers(root.layers));
4346
4668
  const layerNames = new Set(layers.map((layer) => layer.name));
4347
- const rules = parseRules(root.rules, layerNames);
4348
- return { version: 1, layers, rules };
4669
+ const rules = mergeRules(
4670
+ profile,
4671
+ profileSeverity,
4672
+ parseRules(root.rules, layerNames),
4673
+ layerNames
4674
+ );
4675
+ return { version: 1, profile: profile?.id ?? null, layers, rules };
4676
+ }
4677
+ function parseInlineProfiles(value) {
4678
+ const profiles = /* @__PURE__ */ new Map();
4679
+ if (value === void 0 || value === null) return profiles;
4680
+ if (!Array.isArray(value)) {
4681
+ throw new ConfigError("config.profiles debe ser una lista");
4682
+ }
4683
+ value.forEach((raw, index) => {
4684
+ const entry = asObject(raw, `config.profiles[${index}]`);
4685
+ const id = asString(entry.id, `config.profiles[${index}].id`);
4686
+ const title = asString(entry.title, `config.profiles[${index}].title`);
4687
+ const description = entry.description === void 0 ? "" : asString(entry.description, `config.profiles[${index}].description`);
4688
+ const layers = parseLayers(entry.layers).map((layer) => ({
4689
+ name: layer.name,
4690
+ patterns: layer.patterns
4691
+ }));
4692
+ if (layers.length === 0) {
4693
+ throw new ConfigError(
4694
+ `config.profiles[${index}] necesita al menos una capa`
4695
+ );
4696
+ }
4697
+ const layerNames = new Set(layers.map((layer) => layer.name));
4698
+ const rules = parseRules(entry.rules, layerNames).filter(
4699
+ (rule) => rule.type === "forbidden"
4700
+ );
4701
+ profiles.set(id, {
4702
+ id,
4703
+ title,
4704
+ description,
4705
+ when: `Perfil propio del equipo, definido en ${"el archivo de reglas"}.`,
4706
+ layers,
4707
+ rules
4708
+ });
4709
+ });
4710
+ return profiles;
4711
+ }
4712
+ function parseProfile(value, own) {
4713
+ if (value === void 0 || value === null) return null;
4714
+ const id = asString(value, "config.profile");
4715
+ const profile = own.get(id) ?? findDomainProfile(id);
4716
+ if (!profile) {
4717
+ const available = [...own.keys(), ...domainProfileIds()].join(", ");
4718
+ throw new ConfigError(
4719
+ `config.profile desconocido: ${id}. Disponibles: ${available}`
4720
+ );
4721
+ }
4722
+ return profile;
4723
+ }
4724
+ function parseProfileSeverity(value) {
4725
+ if (value === void 0 || value === null) return "warn";
4726
+ if (typeof value !== "string" || !SEVERITIES.has(value)) {
4727
+ throw new ConfigError(
4728
+ "config.profile_severity debe ser info, warn o error"
4729
+ );
4730
+ }
4731
+ return value;
4732
+ }
4733
+ function mergeLayers(profile, own) {
4734
+ const layers = /* @__PURE__ */ new Map();
4735
+ for (const layer of profile?.layers ?? []) layers.set(layer.name, layer);
4736
+ for (const layer of own) layers.set(layer.name, layer);
4737
+ return [...layers.values()];
4738
+ }
4739
+ function mergeRules(profile, profileSeverity, own, layerNames) {
4740
+ const rules = /* @__PURE__ */ new Map();
4741
+ for (const rule of profile?.rules ?? []) {
4742
+ rules.set(rule.name, {
4743
+ ...rule,
4744
+ severity: profileSeverity,
4745
+ source: "profile"
4746
+ });
4747
+ }
4748
+ for (const rule of own) {
4749
+ rules.set(rule.name, { ...rule, source: "config" });
4750
+ }
4751
+ for (const rule of rules.values()) {
4752
+ if (rule.type !== "forbidden") continue;
4753
+ if (layerNames.has(rule.from) && layerNames.has(rule.to)) continue;
4754
+ if (rule.source === "profile") {
4755
+ throw new ConfigError(
4756
+ `la regla "${rule.name}" del perfil apunta a una capa que no existe: ${rule.from} \u2192 ${rule.to}`
4757
+ );
4758
+ }
4759
+ }
4760
+ return [...rules.values()];
4349
4761
  }
4350
4762
  function loadRuleConfig(file) {
4351
4763
  let text;
@@ -4512,6 +4924,7 @@ import { minimatch } from "minimatch";
4512
4924
  function emptyRuleReport() {
4513
4925
  return {
4514
4926
  configured: false,
4927
+ profile: null,
4515
4928
  summary: {
4516
4929
  layers: 0,
4517
4930
  rules: 0,
@@ -4548,6 +4961,7 @@ function evaluateRules(base, head, config) {
4548
4961
  resolved.sort(compareViolations);
4549
4962
  return {
4550
4963
  configured: true,
4964
+ profile: config.profile ?? null,
4551
4965
  summary: {
4552
4966
  layers: config.layers.length,
4553
4967
  rules: config.rules.length,
@@ -4603,6 +5017,7 @@ function evaluateForbidden(rule, input, layers, violations) {
4603
5017
  rule: rule.name,
4604
5018
  type: "forbidden",
4605
5019
  severity: rule.severity,
5020
+ source: rule.source ?? "config",
4606
5021
  message: rule.message ?? `${edge.from} no puede depender de ${edge.to}`,
4607
5022
  files: [edge.from, edge.to],
4608
5023
  edge: {
@@ -4627,6 +5042,7 @@ function evaluateThreshold(rule, input, violations) {
4627
5042
  rule: rule.name,
4628
5043
  type: "threshold",
4629
5044
  severity: rule.severity,
5045
+ source: rule.source ?? "config",
4630
5046
  message: rule.message ?? `${file.path}: ${rule.metric} = ${value} (${limit})`,
4631
5047
  files: [file.path],
4632
5048
  metric: {
@@ -4646,6 +5062,7 @@ function evaluateCycles(rule, cycles, violations) {
4646
5062
  rule: rule.name,
4647
5063
  type: "cycles",
4648
5064
  severity: rule.severity,
5065
+ source: rule.source ?? "config",
4649
5066
  message: rule.message ?? cycleMessage(paths),
4650
5067
  files: paths,
4651
5068
  cycle: {
@@ -4692,7 +5109,7 @@ function diffSnapshots(base, head, options = {}) {
4692
5109
  options.rules
4693
5110
  ) : emptyRuleReport();
4694
5111
  return {
4695
- schemaVersion: "1.0",
5112
+ schemaVersion: "1.2",
4696
5113
  base: summarize(baseIndex),
4697
5114
  head: summarize(headIndex),
4698
5115
  summary: {
@@ -4712,9 +5129,94 @@ function diffSnapshots(base, head, options = {}) {
4712
5129
  files: { added, removed, changed },
4713
5130
  edges: edgeDiff,
4714
5131
  cycles: { introduced: introducedCycles, fixed: fixedCycles },
4715
- hotspots
5132
+ hotspots,
5133
+ migration: buildMigrationReport(baseIndex, headIndex, top)
4716
5134
  };
4717
5135
  }
5136
+ function buildMigrationReport(baseIndex, headIndex, top) {
5137
+ const baseLens = baseIndex.cc.lenses?.migration;
5138
+ const headLens = headIndex.cc.lenses?.migration;
5139
+ if (!baseLens && !headLens) return null;
5140
+ const baseNodes = baseLens?.nodes ?? {};
5141
+ const headNodes = headLens?.nodes ?? {};
5142
+ const changes = [];
5143
+ for (const id of /* @__PURE__ */ new Set([...Object.keys(baseNodes), ...Object.keys(headNodes)])) {
5144
+ const base = baseNodes[id];
5145
+ const head = headNodes[id];
5146
+ if (!base && !head) continue;
5147
+ const reference = head ?? base;
5148
+ const baseDebt = base ? base.debt ?? 0 : null;
5149
+ const headDebt = head ? head.debt ?? 0 : null;
5150
+ const delta = baseDebt === null || headDebt === null ? null : headDebt - baseDebt;
5151
+ if (delta === null || delta === 0) continue;
5152
+ changes.push({
5153
+ id,
5154
+ path: headIndex.paths.get(id) ?? baseIndex.paths.get(id) ?? id,
5155
+ language: reference.language,
5156
+ status: reference.status,
5157
+ base: baseDebt,
5158
+ head: headDebt,
5159
+ delta
5160
+ });
5161
+ }
5162
+ const increased = changes.filter((change) => (change.delta ?? 0) > 0).sort(
5163
+ (left, right) => (right.delta ?? 0) - (left.delta ?? 0) || compareByPath(left, right)
5164
+ );
5165
+ const decreased = changes.filter((change) => (change.delta ?? 0) < 0).sort(
5166
+ (left, right) => (left.delta ?? 0) - (right.delta ?? 0) || compareByPath(left, right)
5167
+ );
5168
+ const topDebt = [];
5169
+ for (const [id, node] of Object.entries(headNodes)) {
5170
+ if (!node.debt) continue;
5171
+ topDebt.push({
5172
+ id,
5173
+ path: headIndex.paths.get(id) ?? id,
5174
+ language: node.language,
5175
+ status: node.status,
5176
+ debt: node.debt,
5177
+ rloc: node.rloc,
5178
+ crossings: node.crossings
5179
+ });
5180
+ }
5181
+ topDebt.sort(
5182
+ (left, right) => right.debt - left.debt || compareByPath(left, right)
5183
+ );
5184
+ const baseSummary = baseLens?.summary;
5185
+ const headSummary = headLens?.summary;
5186
+ return {
5187
+ languages: {
5188
+ base: baseSummary?.languages ?? {},
5189
+ head: headSummary?.languages ?? {}
5190
+ },
5191
+ status: {
5192
+ base: baseSummary?.status ?? emptyStatus(),
5193
+ head: headSummary?.status ?? emptyStatus()
5194
+ },
5195
+ debt: {
5196
+ base: baseSummary?.migrationDebt ?? 0,
5197
+ head: headSummary?.migrationDebt ?? 0,
5198
+ delta: (headSummary?.migrationDebt ?? 0) - (baseSummary?.migrationDebt ?? 0)
5199
+ },
5200
+ crossings: {
5201
+ base: baseSummary?.ffiCrossings ?? 0,
5202
+ head: headSummary?.ffiCrossings ?? 0,
5203
+ delta: (headSummary?.ffiCrossings ?? 0) - (baseSummary?.ffiCrossings ?? 0)
5204
+ },
5205
+ bridgeFiles: {
5206
+ base: baseSummary?.bridgeFiles ?? 0,
5207
+ head: headSummary?.bridgeFiles ?? 0,
5208
+ delta: (headSummary?.bridgeFiles ?? 0) - (baseSummary?.bridgeFiles ?? 0)
5209
+ },
5210
+ files: {
5211
+ increased: increased.slice(0, top),
5212
+ decreased: decreased.slice(0, top)
5213
+ },
5214
+ topDebt: topDebt.slice(0, top)
5215
+ };
5216
+ }
5217
+ function emptyStatus() {
5218
+ return { js: 0, rust: 0, bridge: 0, native: 0 };
5219
+ }
4718
5220
  function indexSnapshot(cc) {
4719
5221
  const paths = /* @__PURE__ */ new Map();
4720
5222
  for (const root of cc.files) {
@@ -4776,7 +5278,7 @@ function difference(present, absent) {
4776
5278
  metrics: sortedMetrics(present.metrics[id])
4777
5279
  });
4778
5280
  }
4779
- return files.sort(compareFiles);
5281
+ return files.sort(compareByPath);
4780
5282
  }
4781
5283
  function changedFiles(base, head) {
4782
5284
  const changed = [];
@@ -4990,7 +5492,7 @@ function finiteNumber(value) {
4990
5492
  function edgeKey(fromId, toId) {
4991
5493
  return `${fromId}\0${toId}`;
4992
5494
  }
4993
- function compareFiles(left, right) {
5495
+ function compareByPath(left, right) {
4994
5496
  return compareStrings5(left.path, right.path) || compareStrings5(left.id, right.id);
4995
5497
  }
4996
5498
  function compareChangedFiles(left, right) {
@@ -5009,6 +5511,209 @@ function compareStrings5(left, right) {
5009
5511
  return left < right ? -1 : left > right ? 1 : 0;
5010
5512
  }
5011
5513
 
5514
+ // packages/metrics/src/suggest.ts
5515
+ var ROOT_FOLDERS = /* @__PURE__ */ new Set([
5516
+ "src",
5517
+ "lib",
5518
+ "libs",
5519
+ "source",
5520
+ "app",
5521
+ "apps",
5522
+ "packages",
5523
+ "pkg"
5524
+ ]);
5525
+ function suggestLayers(cc) {
5526
+ const paths = /* @__PURE__ */ new Map();
5527
+ for (const root of cc.files) {
5528
+ walkFiles([root], (entry) => {
5529
+ paths.set(entry.node.id, relativePath2(root, entry.path));
5530
+ });
5531
+ }
5532
+ const edges = cc.lenses?.dependency?.edges ?? [];
5533
+ const groups = /* @__PURE__ */ new Map();
5534
+ let ungrouped = 0;
5535
+ const groupOf = (path7) => {
5536
+ const parts = path7.split("/").filter(Boolean);
5537
+ if (parts.length === 0) return null;
5538
+ const directory = parts.slice(0, -1);
5539
+ if (directory.length === 0) {
5540
+ ungrouped += 1;
5541
+ return null;
5542
+ }
5543
+ if (directory.length === 1 && ROOT_FOLDERS.has(directory[0])) {
5544
+ ungrouped += 1;
5545
+ return null;
5546
+ }
5547
+ const name = directory.slice(0, 2).join("/");
5548
+ let group = groups.get(name);
5549
+ if (!group) {
5550
+ group = {
5551
+ name,
5552
+ pattern: `${name}/**`,
5553
+ files: 0,
5554
+ dependsOn: /* @__PURE__ */ new Set(),
5555
+ usedBy: /* @__PURE__ */ new Set()
5556
+ };
5557
+ groups.set(name, group);
5558
+ }
5559
+ group.files += 1;
5560
+ return group;
5561
+ };
5562
+ for (const path7 of paths.values()) groupOf(path7);
5563
+ const groupEdges = [];
5564
+ for (const edge of edges) {
5565
+ const fromPath = paths.get(edge.fromId);
5566
+ const toPath = paths.get(edge.toId);
5567
+ if (!fromPath || !toPath) continue;
5568
+ const from = groupOf(fromPath);
5569
+ const to = groupOf(toPath);
5570
+ if (!from || !to || from.name === to.name) continue;
5571
+ from.dependsOn.add(to.name);
5572
+ to.usedBy.add(from.name);
5573
+ groupEdges.push({ from, to });
5574
+ }
5575
+ const depthOf = (name, visiting = /* @__PURE__ */ new Set()) => {
5576
+ const group = groups.get(name);
5577
+ if (!group) return 0;
5578
+ if (visiting.has(name)) return 0;
5579
+ visiting.add(name);
5580
+ let depth = 0;
5581
+ for (const dependency of group.dependsOn) {
5582
+ depth = Math.max(depth, depthOf(dependency, visiting) + 1);
5583
+ }
5584
+ visiting.delete(name);
5585
+ return depth;
5586
+ };
5587
+ const layers = [...groups.values()].map((group) => ({
5588
+ name: group.name,
5589
+ patterns: [group.pattern],
5590
+ files: group.files,
5591
+ dependsOn: [...group.dependsOn].sort(),
5592
+ usedBy: [...group.usedBy].sort(),
5593
+ depth: depthOf(group.name)
5594
+ })).sort((left, right) => left.depth - right.depth || left.name.localeCompare(right.name));
5595
+ const rules = [];
5596
+ for (const layer of layers) {
5597
+ for (const outer of layer.usedBy) {
5598
+ rules.push({
5599
+ name: `${slug(layer.name)}-sin-${slug(outer)}`,
5600
+ type: "forbidden",
5601
+ from: layer.name,
5602
+ to: outer,
5603
+ severity: "warn",
5604
+ message: `${layer.name} no debe depender de ${outer}`
5605
+ });
5606
+ }
5607
+ }
5608
+ rules.sort((left, right) => left.name.localeCompare(right.name));
5609
+ const notes = [];
5610
+ if (ungrouped > 0) {
5611
+ notes.push(
5612
+ `${ungrouped} ficheros en la ra\xEDz o sin carpeta: no forman capa (suelen ser puntos de entrada).`
5613
+ );
5614
+ }
5615
+ if (layers.length === 0) {
5616
+ notes.push("No se ha encontrado ninguna carpeta con c\xF3digo.");
5617
+ }
5618
+ if (layers.length === 1) {
5619
+ notes.push(
5620
+ "Solo hay una capa: no se puede deducir una estructura. Prueba con un perfil del cat\xE1logo."
5621
+ );
5622
+ }
5623
+ const fits = matchProfiles(groups, groupEdges);
5624
+ const best = fits.find((fit) => fit.matched >= 2) ?? null;
5625
+ const profile = best ? findDomainProfile(best.id) ?? null : null;
5626
+ const proposedLayers = profile ? profile.layers.map((layer) => {
5627
+ const group = groupForLayer(layer, groups);
5628
+ return {
5629
+ name: layer.name,
5630
+ patterns: [...layer.patterns],
5631
+ files: group?.files ?? 0,
5632
+ dependsOn: [],
5633
+ usedBy: [],
5634
+ depth: 0
5635
+ };
5636
+ }) : layers;
5637
+ const proposedRules = profile ? profile.rules.map((rule) => ({
5638
+ name: rule.name,
5639
+ type: "forbidden",
5640
+ from: rule.from,
5641
+ to: rule.to,
5642
+ severity: "warn",
5643
+ message: rule.message ?? `${rule.from} no debe depender de ${rule.to}`
5644
+ })) : rules;
5645
+ return {
5646
+ detected: layers,
5647
+ layers: proposedLayers,
5648
+ rules: proposedRules,
5649
+ proposal: profile ? "profile" : "deduced",
5650
+ profile: profile?.id ?? null,
5651
+ ungrouped,
5652
+ notes,
5653
+ fits
5654
+ };
5655
+ }
5656
+ function groupForLayer(layer, groups) {
5657
+ for (const candidate of groups.values()) {
5658
+ if (layer.patterns.some((pattern) => covers(pattern, candidate.name))) {
5659
+ return candidate;
5660
+ }
5661
+ }
5662
+ return void 0;
5663
+ }
5664
+ function matchProfiles(groups, groupEdges) {
5665
+ const totalFiles = [...groups.values()].reduce(
5666
+ (sum, group) => sum + group.files,
5667
+ 0
5668
+ );
5669
+ return DOMAIN_PROFILES.map((profile) => {
5670
+ const layerOf = /* @__PURE__ */ new Map();
5671
+ for (const layer of profile.layers) {
5672
+ const group = [...groups.values()].find(
5673
+ (candidate) => layer.patterns.some((pattern) => covers(pattern, candidate.name))
5674
+ );
5675
+ if (group) layerOf.set(layer.name, group.name);
5676
+ }
5677
+ const covered = [...layerOf.values()].reduce(
5678
+ (sum, name) => sum + (groups.get(name)?.files ?? 0),
5679
+ 0
5680
+ );
5681
+ const broken = [];
5682
+ for (const rule of profile.rules) {
5683
+ const fromGroup = layerOf.get(rule.from);
5684
+ const toGroup = layerOf.get(rule.to);
5685
+ if (!fromGroup || !toGroup) continue;
5686
+ const violates = groupEdges.some(
5687
+ (edge) => edge.from.name === fromGroup && edge.to.name === toGroup
5688
+ );
5689
+ if (violates) broken.push(rule.message ?? `${rule.from} no puede depender de ${rule.to}`);
5690
+ }
5691
+ return {
5692
+ id: profile.id,
5693
+ title: profile.title,
5694
+ matched: layerOf.size,
5695
+ total: profile.layers.length,
5696
+ coverage: totalFiles === 0 ? 0 : Math.round(covered / totalFiles * 100),
5697
+ broken
5698
+ };
5699
+ }).sort(
5700
+ (left, right) => right.matched - left.matched || right.coverage - left.coverage || left.id.localeCompare(right.id)
5701
+ );
5702
+ }
5703
+ function covers(pattern, name) {
5704
+ const prefix = pattern.endsWith("/**") ? pattern.slice(0, -3) : pattern;
5705
+ return name === prefix || name.startsWith(`${prefix}/`);
5706
+ }
5707
+ function slug(name) {
5708
+ return name.replaceAll("/", "-");
5709
+ }
5710
+ function relativePath2(root, fullPath) {
5711
+ if (fullPath === root.name) return "";
5712
+ const prefix = `${root.name}/`;
5713
+ const stripped = fullPath.startsWith(prefix) ? fullPath.slice(prefix.length) : fullPath;
5714
+ return stripped.replace(/^\/+/, "");
5715
+ }
5716
+
5012
5717
  // apps/cli/src/commands/diff.ts
5013
5718
  var numberFormat2 = new Intl.NumberFormat("es-ES");
5014
5719
  function num2(value) {
@@ -5651,7 +6356,11 @@ function runServe(options) {
5651
6356
  if (pathname === "/data/report.json") {
5652
6357
  const report = readReport(state.root);
5653
6358
  if (report === null) {
5654
- sendError(response, 404, "no hay .codarch/report.json");
6359
+ response.writeHead(204, {
6360
+ "Cache-Control": "no-store",
6361
+ "X-Content-Type-Options": "nosniff"
6362
+ });
6363
+ response.end();
5655
6364
  return;
5656
6365
  }
5657
6366
  sendJson(response, report);
@@ -5881,8 +6590,170 @@ data: ${JSON.stringify(data)}
5881
6590
  }
5882
6591
  }
5883
6592
 
6593
+ // apps/cli/src/commands/profiles.ts
6594
+ function runProfiles(json) {
6595
+ if (json) {
6596
+ console.log(
6597
+ JSON.stringify(
6598
+ {
6599
+ profiles: DOMAIN_PROFILES.map((profile) => ({
6600
+ id: profile.id,
6601
+ title: profile.title,
6602
+ description: profile.description,
6603
+ when: profile.when,
6604
+ layers: profile.layers.map((layer) => ({
6605
+ name: layer.name,
6606
+ patterns: layer.patterns
6607
+ })),
6608
+ rules: profile.rules.map((rule) => ({
6609
+ name: rule.name,
6610
+ from: rule.from,
6611
+ to: rule.to,
6612
+ severity: rule.severity,
6613
+ message: rule.message ?? null
6614
+ }))
6615
+ }))
6616
+ },
6617
+ null,
6618
+ 2
6619
+ )
6620
+ );
6621
+ return 0;
6622
+ }
6623
+ console.log("");
6624
+ console.log(" CodArch \xB7 perfiles de arquitectura");
6625
+ console.log("");
6626
+ for (const profile of DOMAIN_PROFILES) {
6627
+ console.log(` ${profile.id} \xB7 ${profile.title}`);
6628
+ console.log(` ${profile.description}`);
6629
+ console.log(` para: ${profile.when}`);
6630
+ console.log(
6631
+ ` capas: ${profile.layers.map((layer) => layer.name).join(", ")}`
6632
+ );
6633
+ for (const rule of profile.rules) {
6634
+ console.log(
6635
+ ` \xB7 ${rule.name}: ${rule.from} no puede depender de ${rule.to} (${rule.severity})`
6636
+ );
6637
+ }
6638
+ console.log("");
6639
+ }
6640
+ console.log(" Para usarlo, en .codarch.yml:");
6641
+ console.log("");
6642
+ console.log(" version: 1");
6643
+ console.log(" profile: capas");
6644
+ console.log("");
6645
+ console.log(' Las reglas del perfil entran como aviso: suben a "error" cuando el c\xF3digo est\xE9 listo.');
6646
+ console.log("");
6647
+ return 0;
6648
+ }
6649
+
6650
+ // apps/cli/src/commands/suggest.ts
6651
+ import { stringify as toYaml } from "yaml";
6652
+ function runSuggest(options) {
6653
+ if (!options.file) {
6654
+ console.error(
6655
+ "falta la ruta del fichero cc.json\n\n codarch suggest <fichero.cc.json[.gz]>"
6656
+ );
6657
+ return 1;
6658
+ }
6659
+ let loaded;
6660
+ try {
6661
+ loaded = readJsonFile2(options.file);
6662
+ } catch (error) {
6663
+ console.error(`no se pudo leer ${options.file}: ${error.message}`);
6664
+ return 1;
6665
+ }
6666
+ let suggestion;
6667
+ try {
6668
+ const cc = parseCcJson(loaded.json);
6669
+ suggestion = suggestLayers(cc);
6670
+ } catch (error) {
6671
+ console.error(`no se pudo leer ${options.file}: ${error.message}`);
6672
+ return 1;
6673
+ }
6674
+ if (options.json) {
6675
+ console.log(
6676
+ JSON.stringify(
6677
+ {
6678
+ detected: suggestion.detected,
6679
+ layers: suggestion.layers,
6680
+ rules: suggestion.rules,
6681
+ proposal: suggestion.proposal,
6682
+ profile: suggestion.profile,
6683
+ ungrouped: suggestion.ungrouped,
6684
+ notes: suggestion.notes,
6685
+ fits: suggestion.fits
6686
+ },
6687
+ null,
6688
+ 2
6689
+ )
6690
+ );
6691
+ return 0;
6692
+ }
6693
+ console.log("");
6694
+ console.log(" CodArch \xB7 propuesta de capas");
6695
+ console.log("");
6696
+ if (suggestion.layers.length === 0) {
6697
+ console.log(" No se ha encontrado ninguna carpeta con c\xF3digo.");
6698
+ for (const note of suggestion.notes) console.log(` ${note}`);
6699
+ console.log("");
6700
+ return 0;
6701
+ }
6702
+ for (const layer of suggestion.detected) {
6703
+ console.log(
6704
+ ` ${layer.name} \xB7 ${layer.files} ficheros \xB7 nivel ${layer.depth}` + (layer.usedBy.length > 0 ? ` \xB7 de aqu\xED dependen: ${layer.usedBy.join(", ")}` : "")
6705
+ );
6706
+ }
6707
+ console.log("");
6708
+ const yaml = toYaml(
6709
+ {
6710
+ version: 1,
6711
+ layers: suggestion.layers.map((layer) => ({
6712
+ name: layer.name,
6713
+ paths: layer.patterns
6714
+ })),
6715
+ rules: suggestion.rules
6716
+ },
6717
+ { lineWidth: 0 }
6718
+ );
6719
+ console.log(" Propuesta para .codarch.yml:");
6720
+ console.log("");
6721
+ console.log(
6722
+ suggestion.proposal === "profile" && suggestion.profile ? ` # Propuesta a partir del perfil "${suggestion.profile}" del cat\xE1logo.` : " # Propuesta deducida del grafo de dependencias del repo."
6723
+ );
6724
+ console.log(
6725
+ yaml.split("\n").map((line) => ` ${line}`).join("\n").trimEnd()
6726
+ );
6727
+ console.log("");
6728
+ const best = suggestion.fits[0];
6729
+ if (best && best.matched > 0) {
6730
+ console.log(" Encaje con el cat\xE1logo de perfiles:");
6731
+ console.log("");
6732
+ for (const fit of suggestion.fits.filter((entry) => entry.matched > 0)) {
6733
+ console.log(
6734
+ ` ${fit.id.padEnd(12)} ${fit.matched} de ${fit.total} capas \xB7 cubre el ${fit.coverage}% del c\xF3digo`
6735
+ );
6736
+ }
6737
+ console.log("");
6738
+ if (best.broken.length > 0) {
6739
+ console.log(
6740
+ ` Con el perfil "${best.id}", el c\xF3digo incumple hoy ${best.broken.length} ${best.broken.length === 1 ? "regla" : "reglas"}:`
6741
+ );
6742
+ for (const message of best.broken) console.log(` \xB7 ${message}`);
6743
+ console.log("");
6744
+ }
6745
+ }
6746
+ for (const note of suggestion.notes) console.log(` ${note}`);
6747
+ console.log("");
6748
+ console.log(
6749
+ ' Las reglas salen como aviso: consultan, no bloquean. Rev\xEDsalas y s\xFAbelas a "error" si encajan.'
6750
+ );
6751
+ console.log("");
6752
+ return 0;
6753
+ }
6754
+
5884
6755
  // apps/cli/src/index.ts
5885
- var VERSION = true ? "0.1.4" : "0.0.0";
6756
+ var VERSION = true ? "0.2.0" : "0.0.0";
5886
6757
  var USAGE = `
5887
6758
  CodArch \xB7 mapa vivo de la arquitectura de tu software
5888
6759
 
@@ -5892,6 +6763,8 @@ Uso:
5892
6763
  codarch render <fichero.cc.json[.gz]> [opciones]
5893
6764
  codarch diff <base.cc.json[.gz]> <head.cc.json[.gz]> [opciones]
5894
6765
  codarch serve [ruta] [opciones]
6766
+ codarch profiles [--json]
6767
+ codarch suggest <fichero.cc.json[.gz]> [--json]
5895
6768
 
5896
6769
  Opciones de analyze:
5897
6770
  -o, --out <dir> directorio de salida (por defecto <ruta>/.codarch)
@@ -5933,6 +6806,14 @@ Opciones de serve:
5933
6806
  --no-web modo API sin servir la app (para Vite u otro frontend)
5934
6807
  -h, --help esta ayuda
5935
6808
 
6809
+ Opciones de profiles:
6810
+ --json salida machine-readable
6811
+ -h, --help esta ayuda
6812
+
6813
+ Opciones de suggest:
6814
+ --json salida machine-readable
6815
+ -h, --help esta ayuda
6816
+
5936
6817
  Ejemplos:
5937
6818
  codarch analyze .
5938
6819
  codarch analyze . -o .codarch --title "Mi proyecto"
@@ -5942,6 +6823,8 @@ Ejemplos:
5942
6823
  codarch diff base.cc.json head.cc.json -o report.json --fail-on cycles
5943
6824
  codarch serve .
5944
6825
  codarch serve . --no-watch
6826
+ codarch profiles
6827
+ codarch suggest .codarch/snapshot.cc.json
5945
6828
  `;
5946
6829
  function parseAnalyze(args) {
5947
6830
  const options = { help: false, ignore: [] };
@@ -6006,6 +6889,30 @@ function parseAnalyze(args) {
6006
6889
  options.root = positional[0] ?? ".";
6007
6890
  return options;
6008
6891
  }
6892
+ function parseSuggest(args) {
6893
+ const options = { json: false };
6894
+ const positional = [];
6895
+ for (let index = 0; index < args.length; index++) {
6896
+ const arg = args[index];
6897
+ if (arg === "--json") {
6898
+ options.json = true;
6899
+ } else if (arg === "--help" || arg === "-h") {
6900
+ console.log(USAGE);
6901
+ return 0;
6902
+ } else if (arg.startsWith("-")) {
6903
+ console.error(`opci\xF3n desconocida: ${arg}`);
6904
+ return 1;
6905
+ } else {
6906
+ positional.push(arg);
6907
+ }
6908
+ }
6909
+ if (positional.length > 1) {
6910
+ console.error(`sobra un argumento: ${positional[1]}`);
6911
+ return 1;
6912
+ }
6913
+ options.file = positional[0];
6914
+ return options;
6915
+ }
6009
6916
  function parseInspect(args) {
6010
6917
  const options = { json: false, top: 5, help: false };
6011
6918
  const positional = [];
@@ -6301,6 +7208,14 @@ function main(argv) {
6301
7208
  }
6302
7209
  return runServe(parsed);
6303
7210
  }
7211
+ if (command === "profiles") {
7212
+ return runProfiles(rest.includes("--json"));
7213
+ }
7214
+ if (command === "suggest") {
7215
+ const parsed = parseSuggest(rest);
7216
+ if (typeof parsed === "number") return parsed;
7217
+ return runSuggest(parsed);
7218
+ }
6304
7219
  console.error(`comando desconocido: ${command}
6305
7220
  ${USAGE}`);
6306
7221
  return 1;