@hyphaene/hexa-ts-kit 1.9.0 → 1.10.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.
@@ -852,6 +852,10 @@ var ContractsScaffoldConfigSchema = z.object({
852
852
  var ProjectConfigSchema = z.object({
853
853
  type: z.enum(["contracts-lib", "nestjs-bff", "vue-frontend"])
854
854
  });
855
+ var VaultConfigSchema = z.object({
856
+ schemaPath: z.string()
857
+ // Path to vault.schema.json relative to repo root
858
+ });
855
859
  var HexaTsKitConfigSchema = z.object({
856
860
  project: ProjectConfigSchema,
857
861
  lint: z.object({
@@ -860,7 +864,8 @@ var HexaTsKitConfigSchema = z.object({
860
864
  }).optional(),
861
865
  scaffold: z.object({
862
866
  contracts: ContractsScaffoldConfigSchema.optional()
863
- }).optional()
867
+ }).optional(),
868
+ vault: VaultConfigSchema.optional()
864
869
  });
865
870
  var CONFIG_FILE_NAME = ".hexa-ts-kit.yaml";
866
871
  function loadConfig(repoPath) {
@@ -924,6 +929,9 @@ function getDisabledRules(config) {
924
929
  function getDisabledVaultRules(config) {
925
930
  return config.lint?.vault?.disabledRules ?? [];
926
931
  }
932
+ function getVaultConfig(config) {
933
+ return config.vault ?? null;
934
+ }
927
935
 
928
936
  // src/lib/contracts/permissions-extractor.ts
929
937
  import { readFileSync as readFileSync2, existsSync as existsSync3 } from "fs";
@@ -3316,6 +3324,8 @@ async function scaffoldCommand(type, path, options) {
3316
3324
  }
3317
3325
 
3318
3326
  export {
3327
+ loadConfig,
3328
+ getVaultConfig,
3319
3329
  lintCore,
3320
3330
  lintCommand,
3321
3331
  analyzeCore,
package/dist/cli.js CHANGED
@@ -1,9 +1,11 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  analyzeCommand,
4
+ getVaultConfig,
4
5
  lintCommand,
6
+ loadConfig,
5
7
  scaffoldCommand
6
- } from "./chunk-OI2LN5K5.js";
8
+ } from "./chunk-IBVI6IS2.js";
7
9
 
8
10
  // src/cli.ts
9
11
  import { program } from "commander";
@@ -257,6 +259,533 @@ function padEnd(str, length) {
257
259
  return str.length >= length ? str.slice(0, length - 1) + " " : str + " ".repeat(length - str.length);
258
260
  }
259
261
 
262
+ // src/vault/extractors/json-schema.ts
263
+ import { execSync } from "child_process";
264
+ import { existsSync, readFileSync as readFileSync2 } from "fs";
265
+ import { join } from "path";
266
+ function extractFromJsonSchema(source, version, options) {
267
+ const { schemaPath, cwd = process.cwd() } = options;
268
+ let jsonContent;
269
+ const isGitRef = options.isGitRef ?? isLikelyGitRef(source);
270
+ if (isGitRef) {
271
+ jsonContent = readFromGit(source, schemaPath, cwd);
272
+ } else {
273
+ jsonContent = readFromFilesystem(source, schemaPath);
274
+ }
275
+ const jsonSchema = JSON.parse(jsonContent);
276
+ const root = convertJsonSchemaToSchemaNode(
277
+ jsonSchema,
278
+ jsonSchema.definitions
279
+ );
280
+ return {
281
+ root,
282
+ sourceFile: schemaPath,
283
+ version
284
+ };
285
+ }
286
+ function isLikelyGitRef(source) {
287
+ if (source === "HEAD" || source.startsWith("HEAD")) return true;
288
+ if (source === "main" || source === "master" || source === "develop")
289
+ return true;
290
+ if (source.startsWith("origin/")) return true;
291
+ if (source.startsWith("refs/")) return true;
292
+ if (source.match(/^[a-f0-9]{7,40}$/)) return true;
293
+ if (source.match(/^v?\d+\.\d+/)) return true;
294
+ if (source.startsWith(".") || source.startsWith("/") || source.startsWith("~"))
295
+ return false;
296
+ if (source.includes("/") && !source.startsWith("origin/")) return false;
297
+ return false;
298
+ }
299
+ function readFromGit(gitRef, schemaPath, cwd) {
300
+ let fullRef;
301
+ if (gitRef.includes(":") && gitRef.endsWith(".json")) {
302
+ fullRef = gitRef;
303
+ } else if (gitRef.includes(":")) {
304
+ fullRef = gitRef;
305
+ } else {
306
+ fullRef = `${gitRef}:${schemaPath}`;
307
+ }
308
+ try {
309
+ const content = execSync(`git show "${fullRef}"`, {
310
+ cwd,
311
+ encoding: "utf-8",
312
+ stdio: ["pipe", "pipe", "pipe"]
313
+ });
314
+ return content;
315
+ } catch (error) {
316
+ const errorMsg = error instanceof Error ? error.message : String(error);
317
+ throw new Error(
318
+ `Cannot read schema from git ref "${fullRef}": ${errorMsg}`
319
+ );
320
+ }
321
+ }
322
+ function readFromFilesystem(repoPath, schemaPath) {
323
+ const fullPath = join(repoPath, schemaPath);
324
+ if (!existsSync(fullPath)) {
325
+ throw new Error(
326
+ `Schema file not found: ${fullPath}
327
+ Run 'npm run vault:build' to generate it.`
328
+ );
329
+ }
330
+ return readFileSync2(fullPath, "utf-8");
331
+ }
332
+ function convertJsonSchemaToSchemaNode(schema, definitions) {
333
+ const localDefs = { ...definitions, ...schema.definitions };
334
+ if (schema.$ref) {
335
+ const refPath = schema.$ref.replace("#/definitions/", "");
336
+ const refSchema = localDefs?.[refPath];
337
+ if (refSchema) {
338
+ return convertJsonSchemaToSchemaNode(refSchema, localDefs);
339
+ }
340
+ return { type: "unknown" };
341
+ }
342
+ const type = Array.isArray(schema.type) ? schema.type[0] : schema.type;
343
+ switch (type) {
344
+ case "string":
345
+ if (schema.enum) {
346
+ return {
347
+ type: "enum",
348
+ enumValues: schema.enum.map(String)
349
+ };
350
+ }
351
+ return { type: "string" };
352
+ case "number":
353
+ case "integer":
354
+ return { type: "number" };
355
+ case "boolean":
356
+ return { type: "boolean" };
357
+ case "array":
358
+ if (schema.items) {
359
+ return {
360
+ type: "array",
361
+ arrayItemType: convertJsonSchemaToSchemaNode(schema.items, localDefs)
362
+ };
363
+ }
364
+ return { type: "array" };
365
+ case "object": {
366
+ const children = {};
367
+ const requiredSet = new Set(schema.required ?? []);
368
+ if (schema.properties) {
369
+ for (const [key, propSchema] of Object.entries(schema.properties)) {
370
+ if (key.startsWith("$") || key === "definitions") continue;
371
+ const propDefs = { ...localDefs, ...propSchema.definitions };
372
+ const propNode = convertJsonSchemaToSchemaNode(propSchema, propDefs);
373
+ children[key] = requiredSet.has(key) ? propNode : { ...propNode, optional: true };
374
+ }
375
+ }
376
+ return { type: "object", children };
377
+ }
378
+ default: {
379
+ if (schema.properties) {
380
+ const children = {};
381
+ const requiredSet = new Set(schema.required ?? []);
382
+ for (const [key, propSchema] of Object.entries(schema.properties)) {
383
+ if (key.startsWith("$") || key === "definitions") continue;
384
+ const propDefs = { ...localDefs, ...propSchema.definitions };
385
+ const propNode = convertJsonSchemaToSchemaNode(propSchema, propDefs);
386
+ children[key] = requiredSet.has(key) ? propNode : { ...propNode, optional: true };
387
+ }
388
+ return { type: "object", children };
389
+ }
390
+ return { type: "unknown" };
391
+ }
392
+ }
393
+ }
394
+ function gitRefExists(ref, cwd) {
395
+ try {
396
+ execSync(`git rev-parse --verify "${ref}"`, {
397
+ cwd,
398
+ encoding: "utf-8",
399
+ stdio: ["pipe", "pipe", "pipe"]
400
+ });
401
+ return true;
402
+ } catch {
403
+ return false;
404
+ }
405
+ }
406
+ function getCurrentBranch(cwd) {
407
+ try {
408
+ return execSync("git rev-parse --abbrev-ref HEAD", {
409
+ cwd,
410
+ encoding: "utf-8"
411
+ }).trim();
412
+ } catch {
413
+ return "HEAD";
414
+ }
415
+ }
416
+ function fileExistsAtRef(ref, filePath, cwd) {
417
+ try {
418
+ execSync(`git show "${ref}:${filePath}"`, {
419
+ cwd,
420
+ encoding: "utf-8",
421
+ stdio: ["pipe", "pipe", "pipe"]
422
+ });
423
+ return true;
424
+ } catch {
425
+ return false;
426
+ }
427
+ }
428
+
429
+ // src/vault/diff.ts
430
+ function diffSchemas(source, target, sourceVersion, targetVersion) {
431
+ const changes = [];
432
+ compareNodes(source, target, "", changes);
433
+ const hasBreakingChanges = changes.some(
434
+ (c) => c.type === "removed" || c.type === "changed"
435
+ );
436
+ return {
437
+ sourceVersion,
438
+ targetVersion,
439
+ changes,
440
+ hasBreakingChanges
441
+ };
442
+ }
443
+ function compareNodes(source, target, path, changes) {
444
+ if (!source && target) {
445
+ changes.push({
446
+ path: path || "(root)",
447
+ type: "added",
448
+ newValue: target
449
+ });
450
+ return;
451
+ }
452
+ if (source && !target) {
453
+ changes.push({
454
+ path: path || "(root)",
455
+ type: "removed",
456
+ oldValue: source
457
+ });
458
+ return;
459
+ }
460
+ if (!source || !target) {
461
+ return;
462
+ }
463
+ if (source.type !== target.type) {
464
+ changes.push({
465
+ path: path || "(root)",
466
+ type: "changed",
467
+ oldValue: source,
468
+ newValue: target
469
+ });
470
+ return;
471
+ }
472
+ if (source.type === "enum" && target.type === "enum") {
473
+ const sourceValues = new Set(source.enumValues ?? []);
474
+ const targetValues = new Set(target.enumValues ?? []);
475
+ for (const val of targetValues) {
476
+ if (!sourceValues.has(val)) {
477
+ changes.push({
478
+ path: `${path}[enum:${val}]`,
479
+ type: "added",
480
+ newValue: { type: "enum", enumValues: [val] }
481
+ });
482
+ }
483
+ }
484
+ for (const val of sourceValues) {
485
+ if (!targetValues.has(val)) {
486
+ changes.push({
487
+ path: `${path}[enum:${val}]`,
488
+ type: "removed",
489
+ oldValue: { type: "enum", enumValues: [val] }
490
+ });
491
+ }
492
+ }
493
+ return;
494
+ }
495
+ if (source.optional !== target.optional) {
496
+ changes.push({
497
+ path,
498
+ type: "changed",
499
+ oldValue: source,
500
+ newValue: target
501
+ });
502
+ }
503
+ if (source.type === "array" && target.type === "array") {
504
+ compareNodes(
505
+ source.arrayItemType,
506
+ target.arrayItemType,
507
+ `${path}[]`,
508
+ changes
509
+ );
510
+ return;
511
+ }
512
+ if (source.type === "object" && target.type === "object") {
513
+ const sourceKeys = new Set(Object.keys(source.children ?? {}));
514
+ const targetKeys = new Set(Object.keys(target.children ?? {}));
515
+ for (const key of targetKeys) {
516
+ const childPath = path ? `${path}.${key}` : key;
517
+ if (!sourceKeys.has(key)) {
518
+ changes.push({
519
+ path: childPath,
520
+ type: "added",
521
+ newValue: target.children[key]
522
+ });
523
+ } else {
524
+ compareNodes(
525
+ source.children[key],
526
+ target.children[key],
527
+ childPath,
528
+ changes
529
+ );
530
+ }
531
+ }
532
+ for (const key of sourceKeys) {
533
+ if (!targetKeys.has(key)) {
534
+ const childPath = path ? `${path}.${key}` : key;
535
+ changes.push({
536
+ path: childPath,
537
+ type: "removed",
538
+ oldValue: source.children[key]
539
+ });
540
+ }
541
+ }
542
+ }
543
+ }
544
+ function formatType(node) {
545
+ switch (node.type) {
546
+ case "string":
547
+ return "string";
548
+ case "number":
549
+ return "number";
550
+ case "boolean":
551
+ return "boolean";
552
+ case "array":
553
+ if (node.arrayItemType) {
554
+ return `${formatType(node.arrayItemType)}[]`;
555
+ }
556
+ return "array";
557
+ case "enum":
558
+ if (node.enumValues) {
559
+ return `enum(${node.enumValues.join(" | ")})`;
560
+ }
561
+ return "enum";
562
+ case "object":
563
+ if (node.children) {
564
+ const keys = Object.keys(node.children).filter((k) => k !== "__ref");
565
+ if (keys.length <= 3) {
566
+ return `{ ${keys.join(", ")} }`;
567
+ }
568
+ return `{ ${keys.slice(0, 3).join(", ")}, ... }`;
569
+ }
570
+ return "object";
571
+ default:
572
+ return "unknown";
573
+ }
574
+ }
575
+
576
+ // src/commands/vault-diff.ts
577
+ function getSchemaPath(cwd) {
578
+ const configResult = loadConfig(cwd);
579
+ if (!configResult.success) {
580
+ throw new Error(
581
+ `Cannot load .hexa-ts-kit.yaml: ${configResult.error}
582
+ Add vault.schemaPath to your config file.`
583
+ );
584
+ }
585
+ const vaultConfig = getVaultConfig(configResult.config);
586
+ if (!vaultConfig) {
587
+ throw new Error(
588
+ `Missing vault config in .hexa-ts-kit.yaml
589
+ Add:
590
+ vault:
591
+ schemaPath: path/to/vault.schema.json`
592
+ );
593
+ }
594
+ return vaultConfig.schemaPath;
595
+ }
596
+ async function vaultDiffCommand(sourceRef, targetRef, options) {
597
+ const format = options.format ?? "console";
598
+ const cwd = options.cwd ?? process.cwd();
599
+ let schemaPath;
600
+ try {
601
+ schemaPath = getSchemaPath(cwd);
602
+ } catch (error) {
603
+ console.error(
604
+ `Error: ${error instanceof Error ? error.message : String(error)}`
605
+ );
606
+ process.exit(1);
607
+ }
608
+ if (!gitRefExists(sourceRef, cwd)) {
609
+ console.error(`Error: Git ref "${sourceRef}" does not exist`);
610
+ process.exit(1);
611
+ }
612
+ if (!gitRefExists(targetRef, cwd)) {
613
+ console.error(`Error: Git ref "${targetRef}" does not exist`);
614
+ process.exit(1);
615
+ }
616
+ if (!fileExistsAtRef(sourceRef, schemaPath, cwd)) {
617
+ console.error(
618
+ `Error: vault.schema.json not found at "${sourceRef}:${schemaPath}"
619
+ The schema file may not exist in this version. Run 'npm run vault:build' on that branch first.`
620
+ );
621
+ process.exit(1);
622
+ }
623
+ if (!fileExistsAtRef(targetRef, schemaPath, cwd)) {
624
+ console.error(
625
+ `Error: vault.schema.json not found at "${targetRef}:${schemaPath}"
626
+ The schema file may not exist in this version. Run 'npm run vault:build' on that branch first.`
627
+ );
628
+ process.exit(1);
629
+ }
630
+ try {
631
+ const extractOpts = { schemaPath, isGitRef: true, cwd };
632
+ const sourceSchema = extractFromJsonSchema(
633
+ sourceRef,
634
+ sourceRef,
635
+ extractOpts
636
+ );
637
+ const targetSchema = extractFromJsonSchema(
638
+ targetRef,
639
+ targetRef,
640
+ extractOpts
641
+ );
642
+ const result = diffSchemas(
643
+ sourceSchema.root,
644
+ targetSchema.root,
645
+ sourceSchema.version,
646
+ targetSchema.version
647
+ );
648
+ if (format === "json") {
649
+ console.log(JSON.stringify(result, null, 2));
650
+ } else {
651
+ printConsoleOutput(result);
652
+ }
653
+ if (result.hasBreakingChanges) {
654
+ process.exit(1);
655
+ }
656
+ } catch (error) {
657
+ console.error(
658
+ `Error: ${error instanceof Error ? error.message : String(error)}`
659
+ );
660
+ process.exit(1);
661
+ }
662
+ }
663
+ async function vaultExtractCommand(source, options) {
664
+ const format = options.format ?? "json";
665
+ const cwd = options.cwd ?? process.cwd();
666
+ let schemaPath;
667
+ try {
668
+ schemaPath = getSchemaPath(cwd);
669
+ } catch (error) {
670
+ console.error(
671
+ `Error: ${error instanceof Error ? error.message : String(error)}`
672
+ );
673
+ process.exit(1);
674
+ }
675
+ try {
676
+ let schema;
677
+ if (source === ".") {
678
+ schema = extractFromJsonSchema(cwd, "local", {
679
+ schemaPath,
680
+ isGitRef: false
681
+ });
682
+ } else if (gitRefExists(source, cwd)) {
683
+ if (!fileExistsAtRef(source, schemaPath, cwd)) {
684
+ console.error(
685
+ `Error: vault.schema.json not found at "${source}:${schemaPath}"`
686
+ );
687
+ process.exit(1);
688
+ }
689
+ schema = extractFromJsonSchema(source, source, {
690
+ schemaPath,
691
+ isGitRef: true,
692
+ cwd
693
+ });
694
+ } else {
695
+ schema = extractFromJsonSchema(source, "path", {
696
+ schemaPath,
697
+ isGitRef: false
698
+ });
699
+ }
700
+ if (format === "json") {
701
+ console.log(JSON.stringify(schema, null, 2));
702
+ } else {
703
+ console.log(`Schema from ${schema.sourceFile} (${schema.version}):
704
+ `);
705
+ printSchemaTree(schema.root, 0);
706
+ }
707
+ } catch (error) {
708
+ console.error(
709
+ `Error: ${error instanceof Error ? error.message : String(error)}`
710
+ );
711
+ process.exit(1);
712
+ }
713
+ }
714
+ function printConsoleOutput(result) {
715
+ console.log(`
716
+ Vault Schema Diff`);
717
+ console.log(`Source: ${result.sourceVersion}`);
718
+ console.log(`Target: ${result.targetVersion}`);
719
+ console.log("\u2500".repeat(50));
720
+ if (result.changes.length === 0) {
721
+ console.log("\n\u2713 No changes detected");
722
+ return;
723
+ }
724
+ const added = result.changes.filter((c) => c.type === "added");
725
+ const removed = result.changes.filter((c) => c.type === "removed");
726
+ const changed = result.changes.filter((c) => c.type === "changed");
727
+ if (added.length > 0) {
728
+ console.log("\n+ Added:");
729
+ for (const change of added) {
730
+ const type = change.newValue ? formatType(change.newValue) : "unknown";
731
+ console.log(` ${change.path}: ${type}`);
732
+ }
733
+ }
734
+ if (removed.length > 0) {
735
+ console.log("\n- Removed:");
736
+ for (const change of removed) {
737
+ const type = change.oldValue ? formatType(change.oldValue) : "unknown";
738
+ console.log(` ${change.path}: ${type}`);
739
+ }
740
+ }
741
+ if (changed.length > 0) {
742
+ console.log("\n~ Changed:");
743
+ for (const change of changed) {
744
+ const oldType = change.oldValue ? formatType(change.oldValue) : "unknown";
745
+ const newType = change.newValue ? formatType(change.newValue) : "unknown";
746
+ console.log(` ${change.path}: ${oldType} \u2192 ${newType}`);
747
+ }
748
+ }
749
+ console.log("\n" + "\u2500".repeat(50));
750
+ console.log(
751
+ `Summary: +${added.length} -${removed.length} ~${changed.length}`
752
+ );
753
+ if (result.hasBreakingChanges) {
754
+ console.log("\n\u26A0 Breaking changes detected (removals or type changes)");
755
+ }
756
+ }
757
+ function printSchemaTree(node, indent) {
758
+ const prefix = " ".repeat(indent);
759
+ if (node.type === "object" && node.children) {
760
+ for (const [key, child] of Object.entries(node.children)) {
761
+ if (key === "__ref") continue;
762
+ const opt = child.optional ? "?" : "";
763
+ console.log(`${prefix}${key}${opt}: ${formatType(child)}`);
764
+ if (child.type === "object" && child.children) {
765
+ printSchemaTree(child, indent + 1);
766
+ }
767
+ }
768
+ } else {
769
+ console.log(`${prefix}${formatType(node)}`);
770
+ }
771
+ }
772
+ async function vaultQuickDiffCommand(options) {
773
+ const cwd = options.cwd ?? process.cwd();
774
+ const currentBranch = getCurrentBranch(cwd);
775
+ const baseBranch = gitRefExists("main", cwd) ? "main" : gitRefExists("master", cwd) ? "master" : null;
776
+ if (!baseBranch) {
777
+ console.error("Error: Cannot find main or master branch");
778
+ process.exit(1);
779
+ }
780
+ if (currentBranch === baseBranch) {
781
+ console.log(`Already on ${baseBranch}, nothing to diff`);
782
+ return;
783
+ }
784
+ console.log(`Comparing ${baseBranch} \u2192 ${currentBranch}
785
+ `);
786
+ await vaultDiffCommand(baseBranch, currentBranch, options);
787
+ }
788
+
260
789
  // src/cli.ts
261
790
  var require2 = createRequire(import.meta.url);
262
791
  var { version: VERSION } = require2("../package.json");
@@ -286,4 +815,9 @@ program.command("scaffold <type> <path>").description("Generate a colocated feat
286
815
  program.command("analyze-bff <bff-path>").description(
287
816
  "Analyze BFF to extract endpoint \u2192 permission \u2192 apiService mappings"
288
817
  ).option("-o, --output <file>", "Output JSON file path").option("--format <type>", "Output format: json, table", "json").action(analyzeBffCommand);
818
+ program.command("vault:diff <source-ref> <target-ref>").description(
819
+ "Compare Vault schemas between two git refs (branches, tags, commits)"
820
+ ).option("-f, --format <type>", "Output format: console, json", "console").option("--cwd <path>", "Working directory (git repo)").action(vaultDiffCommand);
821
+ program.command("vault:quick-diff").alias("vault:qd").description("Compare current branch vault schema against main/master").option("-f, --format <type>", "Output format: console, json", "console").option("--cwd <path>", "Working directory (git repo)").action(vaultQuickDiffCommand);
822
+ program.command("vault:extract <source>").description("Extract and display Vault schema from git ref or path").option("-f, --format <type>", "Output format: json, tree", "json").option("--cwd <path>", "Working directory (git repo)").action(vaultExtractCommand);
289
823
  program.parse();
@@ -3,7 +3,7 @@ import {
3
3
  analyzeCore,
4
4
  lintCore,
5
5
  scaffoldCore
6
- } from "./chunk-OI2LN5K5.js";
6
+ } from "./chunk-IBVI6IS2.js";
7
7
 
8
8
  // src/mcp-server.ts
9
9
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hyphaene/hexa-ts-kit",
3
- "version": "1.9.0",
3
+ "version": "1.10.0",
4
4
  "description": "TypeScript dev kit for Claude Code agents: architecture linting, scaffolding, knowledge analysis",
5
5
  "type": "module",
6
6
  "bin": {