@canonical/webarchitect 0.10.0 → 0.12.0-experimental.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.
Files changed (102) hide show
  1. package/README.md +35 -9
  2. package/package.json +17 -15
  3. package/rulesets/library.ruleset.json +19 -0
  4. package/rulesets/package.ruleset.json +1 -5
  5. package/rulesets/tool-ts.ruleset.json +107 -0
  6. package/rulesets/tool.ruleset.json +19 -0
  7. package/src/cli.ts +322 -0
  8. package/src/constants.ts +8 -0
  9. package/{dist/esm/index.js → src/index.ts} +0 -1
  10. package/{dist/esm/lib/ajv.js → src/lib/ajv.ts} +47 -24
  11. package/src/lib/describeSchema.ts +59 -0
  12. package/src/lib/discoverAllRulesets.ts +18 -0
  13. package/src/lib/discoverRulesetsInDir.ts +41 -0
  14. package/src/lib/executeValidationRules.ts +54 -0
  15. package/{dist/esm/lib/index.js → src/lib/index.ts} +1 -1
  16. package/{dist/esm/lib/listDirectory.js → src/lib/listDirectory.ts} +12 -10
  17. package/{dist/esm/lib/loadFullSchema.js → src/lib/loadFullSchema.ts} +16 -12
  18. package/src/lib/resolveSchema.ts +128 -0
  19. package/src/lib/validateDirectoryRule.ts +158 -0
  20. package/src/lib/validateFileRule.ts +105 -0
  21. package/{dist/esm/lib/validateRule.js → src/lib/validateRule.ts} +18 -11
  22. package/src/schema.json +61 -0
  23. package/src/schemas/draft-2020-12.json +58 -0
  24. package/src/schemas/meta/applicator.json +45 -0
  25. package/src/schemas/meta/content.json +14 -0
  26. package/src/schemas/meta/core.json +48 -0
  27. package/src/schemas/meta/format-annotation.json +11 -0
  28. package/src/schemas/meta/meta-data.json +34 -0
  29. package/src/schemas/meta/unevaluated.json +12 -0
  30. package/src/schemas/meta/validation.json +95 -0
  31. package/src/types.ts +49 -0
  32. package/{dist/esm/validate.js → src/validate.ts} +8 -4
  33. package/dist/esm/cli.js +0 -277
  34. package/dist/esm/cli.js.map +0 -1
  35. package/dist/esm/constants.js +0 -3
  36. package/dist/esm/constants.js.map +0 -1
  37. package/dist/esm/index.js.map +0 -1
  38. package/dist/esm/lib/ajv.js.map +0 -1
  39. package/dist/esm/lib/describeSchema.js +0 -53
  40. package/dist/esm/lib/describeSchema.js.map +0 -1
  41. package/dist/esm/lib/discoverAllRulesets.js +0 -13
  42. package/dist/esm/lib/discoverAllRulesets.js.map +0 -1
  43. package/dist/esm/lib/discoverRulesetsInDir.js +0 -35
  44. package/dist/esm/lib/discoverRulesetsInDir.js.map +0 -1
  45. package/dist/esm/lib/executeValidationRules.js +0 -41
  46. package/dist/esm/lib/executeValidationRules.js.map +0 -1
  47. package/dist/esm/lib/index.js.map +0 -1
  48. package/dist/esm/lib/listDirectory.js.map +0 -1
  49. package/dist/esm/lib/loadFullSchema.js.map +0 -1
  50. package/dist/esm/lib/resolveSchema.js +0 -113
  51. package/dist/esm/lib/resolveSchema.js.map +0 -1
  52. package/dist/esm/lib/validateDirectoryRule.js +0 -138
  53. package/dist/esm/lib/validateDirectoryRule.js.map +0 -1
  54. package/dist/esm/lib/validateFileRule.js +0 -92
  55. package/dist/esm/lib/validateFileRule.js.map +0 -1
  56. package/dist/esm/lib/validateRule.js.map +0 -1
  57. package/dist/esm/schema.json +0 -61
  58. package/dist/esm/schemas/draft-2020-12.json +0 -57
  59. package/dist/esm/schemas/meta/applicator.json +0 -44
  60. package/dist/esm/schemas/meta/content.json +0 -12
  61. package/dist/esm/schemas/meta/core.json +0 -47
  62. package/dist/esm/schemas/meta/format-annotation.json +0 -10
  63. package/dist/esm/schemas/meta/meta-data.json +0 -32
  64. package/dist/esm/schemas/meta/unevaluated.json +0 -11
  65. package/dist/esm/schemas/meta/validation.json +0 -94
  66. package/dist/esm/types.js +0 -2
  67. package/dist/esm/types.js.map +0 -1
  68. package/dist/esm/validate.js.map +0 -1
  69. package/dist/types/cli.d.ts +0 -3
  70. package/dist/types/cli.d.ts.map +0 -1
  71. package/dist/types/constants.d.ts +0 -2
  72. package/dist/types/constants.d.ts.map +0 -1
  73. package/dist/types/index.d.ts +0 -2
  74. package/dist/types/index.d.ts.map +0 -1
  75. package/dist/types/lib/ajv.d.ts +0 -4
  76. package/dist/types/lib/ajv.d.ts.map +0 -1
  77. package/dist/types/lib/describeSchema.d.ts +0 -22
  78. package/dist/types/lib/describeSchema.d.ts.map +0 -1
  79. package/dist/types/lib/discoverAllRulesets.d.ts +0 -9
  80. package/dist/types/lib/discoverAllRulesets.d.ts.map +0 -1
  81. package/dist/types/lib/discoverRulesetsInDir.d.ts +0 -6
  82. package/dist/types/lib/discoverRulesetsInDir.d.ts.map +0 -1
  83. package/dist/types/lib/executeValidationRules.d.ts +0 -22
  84. package/dist/types/lib/executeValidationRules.d.ts.map +0 -1
  85. package/dist/types/lib/index.d.ts +0 -15
  86. package/dist/types/lib/index.d.ts.map +0 -1
  87. package/dist/types/lib/listDirectory.d.ts +0 -20
  88. package/dist/types/lib/listDirectory.d.ts.map +0 -1
  89. package/dist/types/lib/loadFullSchema.d.ts +0 -20
  90. package/dist/types/lib/loadFullSchema.d.ts.map +0 -1
  91. package/dist/types/lib/resolveSchema.d.ts +0 -29
  92. package/dist/types/lib/resolveSchema.d.ts.map +0 -1
  93. package/dist/types/lib/validateDirectoryRule.d.ts +0 -26
  94. package/dist/types/lib/validateDirectoryRule.d.ts.map +0 -1
  95. package/dist/types/lib/validateFileRule.d.ts +0 -29
  96. package/dist/types/lib/validateFileRule.d.ts.map +0 -1
  97. package/dist/types/lib/validateRule.d.ts +0 -21
  98. package/dist/types/lib/validateRule.d.ts.map +0 -1
  99. package/dist/types/types.d.ts +0 -42
  100. package/dist/types/types.d.ts.map +0 -1
  101. package/dist/types/validate.d.ts +0 -23
  102. package/dist/types/validate.d.ts.map +0 -1
package/README.md CHANGED
@@ -1,22 +1,47 @@
1
1
  # Webarchitect
2
2
 
3
- A powerful command-line tool for validating project architecture and configuration consistency across development teams and organizations.
3
+ Validates project architecture and configuration against rulesets. Run it to ensure packages follow organizational standards.
4
4
 
5
- ## What is this tool for?
5
+ ```bash
6
+ webarchitect library
7
+ ```
8
+
9
+ ## Built-in Rulesets
10
+
11
+ Pragma uses three rulesets:
12
+
13
+ | Ruleset | License | Use Case |
14
+ |---------|---------|----------|
15
+ | `library` | LGPL-3.0 | Packages consumed by other packages or applications |
16
+ | `tool` | GPL-3.0 | Compiled CLI tools with a build step |
17
+ | `tool-ts` | GPL-3.0 | TypeScript tools that run directly with Bun (no build) |
6
18
 
7
- Webarchitect helps development teams maintain consistent project structures and configuration standards. Think of it as a quality gate that ensures every project in your organization follows the same architectural patterns, uses the same build tools, and maintains the same configuration standards.
19
+ Each ruleset validates package.json structure, required scripts, TypeScript configuration, Biome setup, and license compliance.
8
20
 
9
- ### The Problem It Solves
21
+ ## Quick Example
10
22
 
11
- In large organizations with multiple development teams, projects tend to drift from established standards over time. One team might use different TypeScript configurations, another might have inconsistent package.json structures, and a third might be missing essential development scripts. This inconsistency creates several problems: developers waste time figuring out how each project works, code quality varies between teams, and maintaining projects becomes more difficult as each one follows slightly different patterns.
23
+ A library package includes this in its check script:
24
+
25
+ ```json
26
+ {
27
+ "scripts": {
28
+ "check:webarchitect": "webarchitect library"
29
+ }
30
+ }
31
+ ```
12
32
 
13
- Webarchitect addresses this challenge by providing automated validation of project architecture. Instead of relying on documentation that might become outdated or manual code reviews that might miss configuration details, you can define your organization's standards as executable rulesets and validate projects automatically.
33
+ Running `bun run check:webarchitect` validates that the package has correct exports, required fields, and LGPL-3.0 license.
14
34
 
15
- ### How It Works
35
+ ## What It Validates
16
36
 
17
- The tool operates on the concept of "rulesets" - JSON Schema-based definitions that describe what files should exist in a project and what those files should contain. For example, a ruleset might specify that every package must have a `package.json` file with specific required fields, a `biome.json` file that extends your organization's linting configuration, and a TypeScript configuration that follows your team's standards.
37
+ Webarchitect checks that:
38
+ - `package.json` has required fields (name, version, type, module, types, exports)
39
+ - `biome.json` extends `@canonical/biome-config`
40
+ - `tsconfig.json` exists with proper configuration
41
+ - License matches the ruleset requirement
42
+ - Required scripts exist (build, check, test)
18
43
 
19
- When you run webarchitect against a project, it loads the specified ruleset, checks whether the required files exist, and validates that their contents match the expected structure and values. This approach gives you confidence that projects follow your established patterns without requiring manual inspection.
44
+ When validation fails, you get specific error messages explaining what's wrong and what's expected.
20
45
 
21
46
  ## How to install and run?
22
47
 
@@ -249,3 +274,4 @@ The programmatic API makes it easy to integrate webarchitect into existing build
249
274
  - **IDE Integration** - No real-time validation feedback is available yet in code editors. Developers must run webarchitect manually or through build scripts to see validation results. Real-time diagnostics would require Language Server Protocol implementation or editor-specific plugins.
250
275
  - **Svelte support** - No svelte-specific ruleset has been developed yet.
251
276
  - **Error Code Granularity** - All validation failures return the same exit code (1) regardless of failure type. Missing files, invalid JSON syntax, and schema validation failures are not distinguished programmatically. This limits automated error handling and reporting capabilities.
277
+
package/package.json CHANGED
@@ -1,15 +1,15 @@
1
1
  {
2
2
  "name": "@canonical/webarchitect",
3
3
  "description": "A tool to test the compliance with architecture specifications for packages and applications.",
4
- "version": "0.10.0",
4
+ "version": "0.12.0-experimental.0",
5
5
  "type": "module",
6
- "module": "dist/esm/index.js",
7
- "types": "dist/types/index.d.ts",
6
+ "module": "src/index.ts",
7
+ "types": "src/index.ts",
8
8
  "bin": {
9
- "webarchitect": "dist/esm/cli.js"
9
+ "webarchitect": "src/cli.ts"
10
10
  },
11
11
  "files": [
12
- "dist",
12
+ "src",
13
13
  "rulesets"
14
14
  ],
15
15
  "author": {
@@ -26,25 +26,27 @@
26
26
  },
27
27
  "homepage": "https://github.com/canonical/ds25#readme",
28
28
  "scripts": {
29
- "build": "tsc -p tsconfig.build.json",
30
- "check": "bun run check:biome && bun run check:ts",
29
+ "build": "echo 'No build needed - runs directly from TypeScript'",
30
+ "build:all": "bun run build",
31
+ "check": "bun run check:biome && bun run check:ts && bun run check:webarchitect",
31
32
  "check:fix": "bun run check:biome:fix && bun run check:ts",
32
33
  "check:biome": "biome check",
33
34
  "check:biome:fix": "biome check --write",
34
35
  "check:ts": "tsc --noEmit",
35
- "test": "echo 'No tests defined yet'"
36
+ "test": "echo 'No tests defined yet'",
37
+ "check:webarchitect": "webarchitect tool-ts"
36
38
  },
37
39
  "devDependencies": {
38
- "@biomejs/biome": "2.2.4",
39
- "@canonical/biome-config": "^0.10.0",
40
- "@canonical/typescript-config-base": "^0.10.0",
40
+ "@biomejs/biome": "2.3.11",
41
+ "@canonical/biome-config": "^0.11.0",
42
+ "@canonical/typescript-config-base": "^0.11.0",
41
43
  "@types/json-schema": "^7.0.15",
42
- "typescript": "^5.8.2"
44
+ "typescript": "^5.9.3"
43
45
  },
44
46
  "dependencies": {
45
47
  "ajv": "^8.17.1",
46
- "chalk": "^5.4.1",
47
- "commander": "^14.0.0"
48
+ "chalk": "^5.6.2",
49
+ "commander": "^14.0.2"
48
50
  },
49
- "gitHead": "49cd1f8973f269cc6ced61c27c137e379ccb032c"
51
+ "gitHead": "0e56eee8b9ff804c676811e1fe55e2716d11db5b"
50
52
  }
@@ -0,0 +1,19 @@
1
+ {
2
+ "$schema": "https://github.com/webarchitect/schemas/main/schema.json",
3
+ "name": "library",
4
+ "extends": ["package"],
5
+ "package-license": {
6
+ "file": {
7
+ "name": "package.json",
8
+ "contains": {
9
+ "type": "object",
10
+ "properties": {
11
+ "license": {
12
+ "const": "LGPL-3.0"
13
+ }
14
+ },
15
+ "required": ["license"]
16
+ }
17
+ }
18
+ }
19
+ }
@@ -34,7 +34,7 @@
34
34
  }
35
35
  }
36
36
  },
37
- "package": {
37
+ "package-structure": {
38
38
  "file": {
39
39
  "name": "package.json",
40
40
  "contains": {
@@ -60,9 +60,6 @@
60
60
  "type": "array",
61
61
  "contains": { "const": "dist" }
62
62
  },
63
- "license": {
64
- "const": "GPL-3.0"
65
- },
66
63
  "scripts": {
67
64
  "type": "object",
68
65
  "properties": {
@@ -88,7 +85,6 @@
88
85
  "module",
89
86
  "types",
90
87
  "files",
91
- "license",
92
88
  "scripts"
93
89
  ]
94
90
  }
@@ -0,0 +1,107 @@
1
+ {
2
+ "$schema": "https://github.com/webarchitect/schemas/main/schema.json",
3
+ "name": "tool-ts",
4
+ "extends": ["base"],
5
+ "biome": {
6
+ "file": {
7
+ "name": "biome.json",
8
+ "contains": {
9
+ "type": "object",
10
+ "properties": {
11
+ "$schema": {
12
+ "type": "string"
13
+ },
14
+ "extends": {
15
+ "type": "array",
16
+ "items": { "const": "@canonical/biome-config" },
17
+ "minItems": 1,
18
+ "maxItems": 1
19
+ },
20
+ "files": {
21
+ "type": "object",
22
+ "properties": {
23
+ "includes": {
24
+ "type": "array",
25
+ "items": { "type": "string" }
26
+ }
27
+ },
28
+ "additionalProperties": false,
29
+ "description": "File inclusion patterns only"
30
+ }
31
+ },
32
+ "required": ["extends"],
33
+ "additionalProperties": false
34
+ }
35
+ }
36
+ },
37
+ "package-structure": {
38
+ "file": {
39
+ "name": "package.json",
40
+ "contains": {
41
+ "type": "object",
42
+ "properties": {
43
+ "name": {
44
+ "type": "string",
45
+ "pattern": "^@canonical/"
46
+ },
47
+ "version": {
48
+ "type": "string"
49
+ },
50
+ "type": {
51
+ "const": "module"
52
+ },
53
+ "module": {
54
+ "const": "src/index.ts"
55
+ },
56
+ "types": {
57
+ "const": "src/index.ts"
58
+ },
59
+ "files": {
60
+ "type": "array",
61
+ "contains": { "const": "src" }
62
+ },
63
+ "scripts": {
64
+ "type": "object",
65
+ "properties": {
66
+ "build": {
67
+ "type": "string",
68
+ "description": "Build command (can be no-op for TS-only packages)"
69
+ },
70
+ "test": {
71
+ "type": "string",
72
+ "description": "Test command (implementation flexible)"
73
+ },
74
+ "check:ts": {
75
+ "const": "tsc --noEmit"
76
+ }
77
+ },
78
+ "required": ["build", "test", "check:ts"]
79
+ }
80
+ },
81
+ "required": [
82
+ "name",
83
+ "version",
84
+ "type",
85
+ "module",
86
+ "types",
87
+ "files",
88
+ "scripts"
89
+ ]
90
+ }
91
+ }
92
+ },
93
+ "package-license": {
94
+ "file": {
95
+ "name": "package.json",
96
+ "contains": {
97
+ "type": "object",
98
+ "properties": {
99
+ "license": {
100
+ "const": "GPL-3.0"
101
+ }
102
+ },
103
+ "required": ["license"]
104
+ }
105
+ }
106
+ }
107
+ }
@@ -0,0 +1,19 @@
1
+ {
2
+ "$schema": "https://github.com/webarchitect/schemas/main/schema.json",
3
+ "name": "tool",
4
+ "extends": ["package"],
5
+ "package-license": {
6
+ "file": {
7
+ "name": "package.json",
8
+ "contains": {
9
+ "type": "object",
10
+ "properties": {
11
+ "license": {
12
+ "const": "GPL-3.0"
13
+ }
14
+ },
15
+ "required": ["license"]
16
+ }
17
+ }
18
+ }
19
+ }
package/src/cli.ts ADDED
@@ -0,0 +1,322 @@
1
+ #!/usr/bin/env bun
2
+ import type { ErrorObject } from "ajv";
3
+ import chalk from "chalk";
4
+ import { Command } from "commander";
5
+ import { discoverAllRulesets } from "./lib/index.js";
6
+ import type { ValidationResult } from "./types.js";
7
+ import validate from "./validate.js";
8
+
9
+ const program = new Command();
10
+
11
+ /**
12
+ * Formats AJV validation errors into human-readable messages.
13
+ * Groups errors by their property path and removes duplicate messages.
14
+ *
15
+ * @param errorsJson - JSON string containing an array of AJV ErrorObject instances
16
+ * @returns Array of formatted error messages, one per property path
17
+ *
18
+ * @example
19
+ * ```typescript
20
+ * const errors = '[{"instancePath": "/name", "message": "must be string"}]';
21
+ * const formatted = formatAjvErrors(errors);
22
+ * // Returns: ["name: must be string"]
23
+ * ```
24
+ */
25
+ function formatAjvErrors(errorsJson: string): string[] {
26
+ try {
27
+ const errors = JSON.parse(errorsJson) as ErrorObject[];
28
+ const errorsByPath = new Map<string, Set<string>>();
29
+
30
+ for (const error of errors) {
31
+ const path = error.instancePath || "root";
32
+ const message = error.message || "validation failed";
33
+
34
+ if (!errorsByPath.has(path)) {
35
+ errorsByPath.set(path, new Set());
36
+ }
37
+ // Use optional chaining for safety
38
+ errorsByPath.get(path)?.add(message);
39
+ }
40
+
41
+ return Array.from(errorsByPath.entries()).map(([path, messages]) => {
42
+ const property = path === "root" ? "file" : path.replace("/", "");
43
+ const messageList = Array.from(messages).join(", ");
44
+ return `${property}: ${messageList}`;
45
+ });
46
+ } catch {
47
+ // Fallback if JSON parsing fails
48
+ return [errorsJson];
49
+ }
50
+ }
51
+
52
+ /**
53
+ * Formats validation results for terminal output with color coding.
54
+ * Displays failed validations first, followed by passed validations,
55
+ * and ends with a summary. In verbose mode, shows additional context
56
+ * including file contents and validation rules.
57
+ *
58
+ * @param results - Array of validation results to format
59
+ * @param verbose - Whether to show detailed information including file contents and rules
60
+ *
61
+ * @example
62
+ * ```typescript
63
+ * const results = [
64
+ * { rule: "package.json", passed: true },
65
+ * { rule: "biome.json", passed: false, message: "File not found" }
66
+ * ];
67
+ * formatTerminalOutput(results, true);
68
+ * ```
69
+ */
70
+ function formatTerminalOutput(
71
+ results: ValidationResult[],
72
+ verbose = false,
73
+ ): void {
74
+ console.log(); // Empty line for spacing
75
+
76
+ const passedResults = results.filter((r) => r.passed);
77
+ const failedResults = results.filter((r) => !r.passed);
78
+
79
+ if (failedResults.length > 0) {
80
+ console.log(chalk.red.bold("✗ FAILED VALIDATIONS"));
81
+ console.log(chalk.gray("─".repeat(50)));
82
+
83
+ for (const result of failedResults) {
84
+ console.log(chalk.red(`✗ ${result.rule}`));
85
+
86
+ if (verbose && result.context) {
87
+ console.log(chalk.gray(` Target: ${result.context.target}`));
88
+ if (result.context.description) {
89
+ console.log(chalk.gray(` Rule: ${result.context.description}`));
90
+ }
91
+ }
92
+
93
+ if (result.message) {
94
+ // Check if message contains JSON (AJV errors)
95
+ if (result.message.includes("Validation failed: [")) {
96
+ const jsonStart = result.message.indexOf("[");
97
+ const prefix = result.message.substring(0, jsonStart).trim();
98
+ const jsonPart = result.message.substring(jsonStart);
99
+
100
+ if (prefix) {
101
+ console.log(chalk.gray(` ${prefix}`));
102
+ }
103
+
104
+ const formattedErrors = formatAjvErrors(jsonPart);
105
+ for (const error of formattedErrors) {
106
+ console.log(chalk.gray(` • ${error}`));
107
+ }
108
+ } else {
109
+ console.log(chalk.gray(` ${result.message}`));
110
+ }
111
+ }
112
+
113
+ if (
114
+ verbose &&
115
+ result.context?.value &&
116
+ result.context.value !== "[File not found]"
117
+ ) {
118
+ // Use regular string instead of template literal where no interpolation needed
119
+ console.log(chalk.gray(" Found:"));
120
+ if (typeof result.context.value === "object") {
121
+ const preview = JSON.stringify(result.context.value, null, 2)
122
+ .split("\n")
123
+ .slice(0, 10) // Show first 10 lines
124
+ .map((line) => ` ${line}`)
125
+ .join("\n");
126
+ console.log(chalk.gray(preview));
127
+ if (JSON.stringify(result.context.value).split("\n").length > 10) {
128
+ console.log(chalk.gray(" ... (truncated)"));
129
+ }
130
+ } else {
131
+ console.log(chalk.gray(` ${result.context.value}`));
132
+ }
133
+ }
134
+ console.log(); // Empty line between failures
135
+ }
136
+ }
137
+
138
+ if (passedResults.length > 0) {
139
+ console.log(chalk.green.bold("✓ PASSED VALIDATIONS"));
140
+ console.log(chalk.gray("─".repeat(50)));
141
+
142
+ for (const result of passedResults) {
143
+ console.log(chalk.green(`✓ ${result.rule}`));
144
+
145
+ if (verbose && result.context) {
146
+ console.log(chalk.gray(` Target: ${result.context.target}`));
147
+ if (result.context.description) {
148
+ console.log(chalk.gray(` Rule: ${result.context.description}`));
149
+ }
150
+
151
+ if (result.context.value && typeof result.context.value === "object") {
152
+ // Use regular string instead of template literal
153
+ console.log(chalk.gray(" Validated content:"));
154
+ const preview = JSON.stringify(result.context.value, null, 2)
155
+ .split("\n")
156
+ .slice(0, 5) // Show fewer lines for passed validations
157
+ .map((line) => ` ${line}`)
158
+ .join("\n");
159
+ console.log(chalk.gray(preview));
160
+ if (JSON.stringify(result.context.value).split("\n").length > 5) {
161
+ console.log(chalk.gray(" ... (content validated successfully)"));
162
+ }
163
+ }
164
+ }
165
+ }
166
+ console.log();
167
+ }
168
+
169
+ // Summary
170
+ const total = results.length;
171
+ const failed = failedResults.length;
172
+ const passed = passedResults.length;
173
+
174
+ if (failed > 0) {
175
+ console.log(
176
+ chalk.red.bold(
177
+ `Summary: ${failed}/${total} validations failed (${passed} passed)`,
178
+ ),
179
+ );
180
+ } else {
181
+ console.log(chalk.green.bold(`Summary: All ${total} validations passed`));
182
+ }
183
+ }
184
+
185
+ /**
186
+ * Formats validation results as JSON for programmatic consumption.
187
+ * Outputs a structured object with summary statistics and detailed results.
188
+ *
189
+ * @param results - Array of validation results to format
190
+ *
191
+ * @example
192
+ * ```typescript
193
+ * const results = [
194
+ * { rule: "package.json", passed: true },
195
+ * { rule: "biome.json", passed: false, message: "File not found" }
196
+ * ];
197
+ * formatJsonOutput(results);
198
+ * // Outputs: {
199
+ * // "summary": { "total": 2, "passed": 1, "failed": 1 },
200
+ * // "results": [...]
201
+ * // }
202
+ * ```
203
+ */
204
+ function formatJsonOutput(results: ValidationResult[]): void {
205
+ const output = {
206
+ summary: {
207
+ total: results.length,
208
+ passed: results.filter((r) => r.passed).length,
209
+ failed: results.filter((r) => !r.passed).length,
210
+ },
211
+ results: results.map((result) => ({
212
+ rule: result.rule,
213
+ passed: result.passed,
214
+ ...(result.message && { message: result.message }),
215
+ })),
216
+ };
217
+
218
+ console.log(JSON.stringify(output, null, 2));
219
+ }
220
+
221
+ /**
222
+ * Displays available rulesets in a tree-like format.
223
+ * Groups rulesets by their location (bundled vs current directory).
224
+ */
225
+ async function displayRulesetTree(): Promise<void> {
226
+ const { bundled, local } = await discoverAllRulesets();
227
+
228
+ if (bundled.length === 0 && local.length === 0) {
229
+ console.log(chalk.yellow("No rulesets found"));
230
+ return;
231
+ }
232
+
233
+ console.log(chalk.bold("\nAvailable Webarchitect Rulesets:"));
234
+ console.log(chalk.gray("─".repeat(50)));
235
+
236
+ // Display bundled rulesets
237
+ if (bundled.length > 0) {
238
+ console.log(chalk.cyan("Bundled Rulesets:"));
239
+ console.log(chalk.gray(`└── ${bundled[0].path.replace(/\/[^/]+$/, "")}/`));
240
+ for (let i = 0; i < bundled.length; i++) {
241
+ const isLast = i === bundled.length - 1;
242
+ const prefix = isLast ? "└──" : "├──";
243
+ const filename = `${bundled[i].name}.ruleset.json`;
244
+ console.log(
245
+ ` ${prefix} ${chalk.bold(bundled[i].name)} ${chalk.gray(`(${filename})`)}`,
246
+ );
247
+ }
248
+ }
249
+
250
+ // Display current directory rulesets
251
+ if (bundled.length > 0) console.log(); // Add spacing between sections
252
+ console.log(chalk.green("Current Directory:"));
253
+ console.log(chalk.gray(`└── ${process.cwd()}/`));
254
+ if (local.length > 0) {
255
+ for (let i = 0; i < local.length; i++) {
256
+ const isLast = i === local.length - 1;
257
+ const prefix = isLast ? "└──" : "├──";
258
+ const filename = `${local[i].name}.ruleset.json`;
259
+ console.log(
260
+ ` ${prefix} ${chalk.bold(local[i].name)} ${chalk.gray(`(${filename})`)}`,
261
+ );
262
+ }
263
+ } else {
264
+ console.log(chalk.gray(` └── (no rulesets found at this path)`));
265
+ }
266
+ }
267
+
268
+ program
269
+ .name("webarchitect")
270
+ .argument("[ruleset]", "ruleset identifier, local path, or URL")
271
+ .option("-v, --verbose", "show all validation results")
272
+ .option("--json", "output results in JSON format")
273
+ .option("--list", "list all available rulesets")
274
+ .action(async (schemaArg, options) => {
275
+ // Handle --list option
276
+ if (options.list) {
277
+ await displayRulesetTree();
278
+ return;
279
+ }
280
+
281
+ // Require ruleset argument if not listing
282
+ if (!schemaArg) {
283
+ console.error(chalk.red("Error: Missing required argument 'ruleset'"));
284
+ console.error(
285
+ chalk.gray("Use 'webarchitect --list' to see available rulesets"),
286
+ );
287
+ process.exit(1);
288
+ }
289
+ try {
290
+ const results = await validate(process.cwd(), schemaArg);
291
+
292
+ if (options.json) {
293
+ formatJsonOutput(results);
294
+ } else {
295
+ formatTerminalOutput(results, options.verbose);
296
+
297
+ // Exit with error code if any validations failed
298
+ const hasFailures = results.some((r) => !r.passed);
299
+ if (hasFailures) {
300
+ process.exit(1);
301
+ }
302
+ }
303
+ } catch (e) {
304
+ if (options.json) {
305
+ console.log(
306
+ JSON.stringify(
307
+ {
308
+ error: (e as Error).message,
309
+ success: false,
310
+ },
311
+ null,
312
+ 2,
313
+ ),
314
+ );
315
+ } else {
316
+ console.error(chalk.red(`Error: ${(e as Error).message}`));
317
+ }
318
+ process.exit(1);
319
+ }
320
+ });
321
+
322
+ program.parse();
@@ -0,0 +1,8 @@
1
+ import { existsSync } from "node:fs";
2
+ import { join } from "node:path";
3
+
4
+ // Handle both source (src/) and built (dist/esm/) paths
5
+ const srcPath = join(import.meta.dirname, "../rulesets");
6
+ const distPath = join(import.meta.dirname, "../../rulesets");
7
+
8
+ export const BUNDLED_RULESETS_DIR = existsSync(srcPath) ? srcPath : distPath;
@@ -1,2 +1 @@
1
1
  export { default as validate } from "./validate.js";
2
- //# sourceMappingURL=index.js.map