aboutsummaryrefslogtreecommitdiff
path: root/validators
diff options
context:
space:
mode:
Diffstat (limited to 'validators')
-rw-r--r--validators/bash/README.md60
-rw-r--r--validators/bash/validate.sh172
-rw-r--r--validators/nodejs/README.md37
-rw-r--r--validators/nodejs/validate.js234
-rw-r--r--validators/powershell/README.md46
-rw-r--r--validators/powershell/Validate-GitInfo.ps1248
6 files changed, 797 insertions, 0 deletions
diff --git a/validators/bash/README.md b/validators/bash/README.md
new file mode 100644
index 0000000..3692a1d
--- /dev/null
+++ b/validators/bash/README.md
@@ -0,0 +1,60 @@
+# Bash Validator
+
+A Bash script for validating `.gitinfo` files.
+
+## Requirements
+
+- Bash 4+
+- [jq](https://jqlang.github.io/jq/) (JSON processor)
+
+Install jq:
+
+```bash
+# Debian/Ubuntu
+apt install jq
+
+# macOS
+brew install jq
+
+# Windows (via Chocolatey)
+choco install jq
+```
+
+## Usage
+
+```bash
+# Make executable (first time only)
+chmod +x validate.sh
+
+# Validate .gitinfo in current directory
+./validate.sh
+
+# Validate a specific file
+./validate.sh path/to/.gitinfo
+```
+
+## Features
+
+- Parses JSONC (strips `//` and `/* */` comments)
+- Validates against the gitinfo JSON Schema
+- Checks types and formats (URI, email)
+- Enforces `additionalProperties: false`
+- Returns exit code 0 on success, 1 on failure
+- Color-coded output (green for success, red for errors)
+
+## Example Output
+
+```
+✓ .gitinfo is valid
+```
+
+```
+Validation failed for .gitinfo:
+ - .root: invalid URI "not-a-url"
+ - root: unknown property "invalid_field"
+```
+
+## Limitations
+
+- Comment stripping is simplified and may not handle edge cases with comments inside strings
+- For complex validation, consider using the Node.js validator
diff --git a/validators/bash/validate.sh b/validators/bash/validate.sh
new file mode 100644
index 0000000..55324f1
--- /dev/null
+++ b/validators/bash/validate.sh
@@ -0,0 +1,172 @@
+#!/usr/bin/env bash
+
+# CLI validator for .gitinfo files
+# Usage: ./validate.sh [file]
+# ./validate.sh # validates .gitinfo in current directory
+# ./validate.sh path/to/.gitinfo
+
+set -e
+
+# Schema path (two levels up from validators/bash/)
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+SCHEMA_PATH="$SCRIPT_DIR/../../gitinfo.schema.json"
+
+# Colors
+RED='\033[0;31m'
+GREEN='\033[0;32m'
+NC='\033[0m' # No Color
+
+# Check for jq
+if ! command -v jq &> /dev/null; then
+ echo -e "${RED}Error: jq is required but not installed.${NC}" >&2
+ echo "Install with: apt install jq / brew install jq / choco install jq" >&2
+ exit 1
+fi
+
+# Strip JSONC comments using sed
+strip_comments() {
+ # Remove single-line comments (// ...) and multi-line comments (/* ... */)
+ # This is a simplified version - doesn't handle comments inside strings perfectly
+ sed -e 's|//.*$||g' -e ':a;s|/\*.*\*/||g;ta' -e '/\/\*/,/\*\//d' "$1"
+}
+
+# Validate URI format
+validate_uri() {
+ local uri="$1"
+ if [[ "$uri" =~ ^https?:// ]]; then
+ return 0
+ fi
+ return 1
+}
+
+# Validate email format
+validate_email() {
+ local email="$1"
+ if [[ "$email" =~ ^[^[:space:]@]+@[^[:space:]@]+\.[^[:space:]@]+$ ]]; then
+ return 0
+ fi
+ return 1
+}
+
+# Main validation function
+validate() {
+ local file="$1"
+ local errors=()
+
+ # Parse JSON
+ local json
+ if ! json=$(strip_comments "$file" | jq -c . 2>&1); then
+ echo -e "${RED}Error parsing JSONC: $json${NC}" >&2
+ exit 1
+ fi
+
+ # Load schema
+ local schema
+ if ! schema=$(jq -c . "$SCHEMA_PATH" 2>&1); then
+ echo -e "${RED}Error parsing schema: $schema${NC}" >&2
+ exit 1
+ fi
+
+ # Check if root is an object
+ local type
+ type=$(echo "$json" | jq -r 'type')
+ if [[ "$type" != "object" ]]; then
+ errors+=("root: expected object, got $type")
+ fi
+
+ # Get allowed properties from schema
+ local allowed_props
+ allowed_props=$(echo "$schema" | jq -r '.properties | keys[]')
+
+ # Check for unknown properties
+ local actual_props
+ actual_props=$(echo "$json" | jq -r 'keys[]')
+ for prop in $actual_props; do
+ if ! echo "$allowed_props" | grep -qx "$prop"; then
+ errors+=("root: unknown property \"$prop\"")
+ fi
+ done
+
+ # Validate each property
+ local schema_props
+ schema_props=$(echo "$schema" | jq -r '.properties | to_entries[] | @base64')
+
+ for entry in $schema_props; do
+ local key format prop_type
+ key=$(echo "$entry" | base64 -d | jq -r '.key')
+ format=$(echo "$entry" | base64 -d | jq -r '.value.format // empty')
+ prop_type=$(echo "$entry" | base64 -d | jq -r '.value.type')
+
+ # Check if property exists
+ local value
+ value=$(echo "$json" | jq -r --arg k "$key" '.[$k] // empty')
+
+ if [[ -n "$value" && "$value" != "null" ]]; then
+ local actual_type
+ actual_type=$(echo "$json" | jq -r --arg k "$key" '.[$k] | type')
+
+ # Type check
+ if [[ "$prop_type" == "string" && "$actual_type" != "string" ]]; then
+ errors+=(".$key: expected string")
+ elif [[ "$prop_type" == "array" && "$actual_type" != "array" ]]; then
+ errors+=(".$key: expected array")
+ fi
+
+ # Format validation for strings
+ if [[ "$actual_type" == "string" ]]; then
+ if [[ "$format" == "uri" ]]; then
+ if ! validate_uri "$value"; then
+ errors+=(".$key: invalid URI \"$value\"")
+ fi
+ elif [[ "$format" == "email" ]]; then
+ if ! validate_email "$value"; then
+ errors+=(".$key: invalid email \"$value\"")
+ fi
+ fi
+ fi
+
+ # Validate array items
+ if [[ "$actual_type" == "array" ]]; then
+ local item_format
+ item_format=$(echo "$entry" | base64 -d | jq -r '.value.items.format // empty')
+
+ if [[ "$item_format" == "uri" ]]; then
+ local i=0
+ while IFS= read -r item; do
+ if ! validate_uri "$item"; then
+ errors+=(".${key}[$i]: invalid URI \"$item\"")
+ fi
+ ((i++))
+ done < <(echo "$json" | jq -r --arg k "$key" '.[$k][]?')
+ fi
+ fi
+ fi
+ done
+
+ # Output results
+ if [[ ${#errors[@]} -gt 0 ]]; then
+ echo -e "${RED}Validation failed for $file:${NC}" >&2
+ for error in "${errors[@]}"; do
+ echo -e " - $error" >&2
+ done
+ exit 1
+ fi
+
+ echo -e "${GREEN}✓ $file is valid${NC}"
+ exit 0
+}
+
+# Main
+FILE="${1:-.gitinfo}"
+
+if [[ ! -f "$FILE" ]]; then
+ echo -e "${RED}Error: File not found: $FILE${NC}" >&2
+ exit 1
+fi
+
+if [[ ! -f "$SCHEMA_PATH" ]]; then
+ echo -e "${RED}Error: Schema not found: $SCHEMA_PATH${NC}" >&2
+ exit 1
+fi
+
+validate "$FILE"
diff --git a/validators/nodejs/README.md b/validators/nodejs/README.md
new file mode 100644
index 0000000..4fe9fa6
--- /dev/null
+++ b/validators/nodejs/README.md
@@ -0,0 +1,37 @@
+# Node.js Validator
+
+A zero-dependency Node.js CLI tool for validating `.gitinfo` files.
+
+## Requirements
+
+- Node.js 14+
+
+## Usage
+
+```bash
+# Validate .gitinfo in current directory
+node validate.js
+
+# Validate a specific file
+node validate.js path/to/.gitinfo
+```
+
+## Features
+
+- Parses JSONC (strips `//` and `/* */` comments)
+- Validates against the gitinfo JSON Schema
+- Checks types, formats (URI, email), and patterns
+- Enforces `additionalProperties: false`
+- Returns exit code 0 on success, 1 on failure
+
+## Example Output
+
+```
+✓ .gitinfo is valid
+```
+
+```
+Validation failed for .gitinfo:
+ - .root: invalid URI "not-a-url"
+ - root: unknown property "invalid_field"
+```
diff --git a/validators/nodejs/validate.js b/validators/nodejs/validate.js
new file mode 100644
index 0000000..8c230c4
--- /dev/null
+++ b/validators/nodejs/validate.js
@@ -0,0 +1,234 @@
+#!/usr/bin/env node
+
+/**
+ * CLI validator for .gitinfo files
+ * Usage: node validate.js [file]
+ * node validate.js # validates .gitinfo in current directory
+ * node validate.js path/to/.gitinfo
+ */
+
+const fs = require("fs");
+const path = require("path");
+
+// Load the schema (two levels up from validators/nodejs/)
+const SCHEMA_PATH = path.join(__dirname, "..", "..", "gitinfo.schema.json");
+
+/**
+ * Strip JSONC comments (single-line // and multi-line /* *\/)
+ */
+function stripJsonComments(jsonc) {
+ let result = "";
+ let i = 0;
+ let inString = false;
+ let stringChar = null;
+
+ while (i < jsonc.length) {
+ const char = jsonc[i];
+ const next = jsonc[i + 1];
+
+ // Track string state
+ if (!inString && (char === '"' || char === "'")) {
+ inString = true;
+ stringChar = char;
+ result += char;
+ i++;
+ continue;
+ }
+
+ if (inString) {
+ if (char === "\\" && i + 1 < jsonc.length) {
+ // Escape sequence
+ result += char + jsonc[i + 1];
+ i += 2;
+ continue;
+ }
+ if (char === stringChar) {
+ inString = false;
+ stringChar = null;
+ }
+ result += char;
+ i++;
+ continue;
+ }
+
+ // Single-line comment
+ if (char === "/" && next === "/") {
+ while (i < jsonc.length && jsonc[i] !== "\n") {
+ i++;
+ }
+ continue;
+ }
+
+ // Multi-line comment
+ if (char === "/" && next === "*") {
+ i += 2;
+ while (
+ i < jsonc.length - 1 &&
+ !(jsonc[i] === "*" && jsonc[i + 1] === "/")
+ ) {
+ i++;
+ }
+ i += 2;
+ continue;
+ }
+
+ result += char;
+ i++;
+ }
+
+ return result;
+}
+
+/**
+ * Simple JSON Schema validator (subset of draft 2020-12)
+ */
+function validateSchema(data, schema, path = "") {
+ const errors = [];
+
+ if (schema.type === "object") {
+ if (typeof data !== "object" || data === null || Array.isArray(data)) {
+ errors.push(`${path || "root"}: expected object`);
+ return errors;
+ }
+
+ // Check additionalProperties
+ if (schema.additionalProperties === false && schema.properties) {
+ const allowed = new Set(Object.keys(schema.properties));
+ for (const key of Object.keys(data)) {
+ if (!allowed.has(key)) {
+ errors.push(`${path || "root"}: unknown property "${key}"`);
+ }
+ }
+ }
+
+ // Validate each property
+ if (schema.properties) {
+ for (const [key, propSchema] of Object.entries(schema.properties)) {
+ if (data[key] !== undefined) {
+ errors.push(
+ ...validateSchema(data[key], propSchema, `${path}.${key}`),
+ );
+ }
+ }
+ }
+ } else if (schema.type === "array") {
+ if (!Array.isArray(data)) {
+ errors.push(`${path}: expected array`);
+ return errors;
+ }
+
+ if (schema.items) {
+ if (Array.isArray(schema.items)) {
+ // Tuple validation
+ for (let i = 0; i < data.length; i++) {
+ const itemSchema = schema.items[i] || {};
+ errors.push(...validateSchema(data[i], itemSchema, `${path}[${i}]`));
+ }
+ if (schema.minItems && data.length < schema.minItems) {
+ errors.push(`${path}: expected at least ${schema.minItems} items`);
+ }
+ if (schema.maxItems && data.length > schema.maxItems) {
+ errors.push(`${path}: expected at most ${schema.maxItems} items`);
+ }
+ } else {
+ // Array of same type
+ for (let i = 0; i < data.length; i++) {
+ errors.push(
+ ...validateSchema(data[i], schema.items, `${path}[${i}]`),
+ );
+ }
+ }
+ }
+ } else if (schema.type === "string") {
+ if (typeof data !== "string") {
+ errors.push(`${path}: expected string`);
+ return errors;
+ }
+
+ if (schema.minLength && data.length < schema.minLength) {
+ errors.push(`${path}: string too short (min ${schema.minLength})`);
+ }
+
+ if (schema.format === "uri") {
+ try {
+ new URL(data);
+ } catch {
+ errors.push(`${path}: invalid URI "${data}"`);
+ }
+ }
+
+ if (schema.format === "email") {
+ if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(data)) {
+ errors.push(`${path}: invalid email "${data}"`);
+ }
+ }
+
+ if (schema.pattern) {
+ if (!new RegExp(schema.pattern).test(data)) {
+ errors.push(`${path}: does not match pattern ${schema.pattern}`);
+ }
+ }
+ }
+
+ return errors;
+}
+
+function main() {
+ const args = process.argv.slice(2);
+ const filePath = args[0] || ".gitinfo";
+
+ // Check if file exists
+ if (!fs.existsSync(filePath)) {
+ console.error(`Error: File not found: ${filePath}`);
+ process.exit(1);
+ }
+
+ // Check if schema exists
+ if (!fs.existsSync(SCHEMA_PATH)) {
+ console.error(`Error: Schema not found: ${SCHEMA_PATH}`);
+ process.exit(1);
+ }
+
+ // Read and parse schema
+ let schema;
+ try {
+ schema = JSON.parse(fs.readFileSync(SCHEMA_PATH, "utf-8"));
+ } catch (err) {
+ console.error(`Error parsing schema: ${err.message}`);
+ process.exit(1);
+ }
+
+ // Read and parse .gitinfo file
+ let content;
+ try {
+ content = fs.readFileSync(filePath, "utf-8");
+ } catch (err) {
+ console.error(`Error reading file: ${err.message}`);
+ process.exit(1);
+ }
+
+ let data;
+ try {
+ const stripped = stripJsonComments(content);
+ data = JSON.parse(stripped);
+ } catch (err) {
+ console.error(`Error parsing JSONC: ${err.message}`);
+ process.exit(1);
+ }
+
+ // Validate against schema
+ const errors = validateSchema(data, schema);
+
+ if (errors.length > 0) {
+ console.error(`Validation failed for ${filePath}:`);
+ for (const error of errors) {
+ console.error(` - ${error}`);
+ }
+ process.exit(1);
+ }
+
+ console.log(`✓ ${filePath} is valid`);
+ process.exit(0);
+}
+
+main();
diff --git a/validators/powershell/README.md b/validators/powershell/README.md
new file mode 100644
index 0000000..822659e
--- /dev/null
+++ b/validators/powershell/README.md
@@ -0,0 +1,46 @@
+# PowerShell Validator
+
+A PowerShell script for validating `.gitinfo` files.
+
+## Requirements
+
+- PowerShell 5.1+ (Windows) or PowerShell Core 7+ (cross-platform)
+
+## Usage
+
+```powershell
+# Validate .gitinfo in current directory
+.\Validate-GitInfo.ps1
+
+# Validate a specific file
+.\Validate-GitInfo.ps1 -Path "path/to/.gitinfo"
+```
+
+## Features
+
+- Parses JSONC (strips `//` and `/* */` comments)
+- Validates against the gitinfo JSON Schema
+- Checks types, formats (URI, email), and patterns
+- Enforces `additionalProperties: false`
+- Returns exit code 0 on success, 1 on failure
+- Color-coded output (green for success, red for errors)
+
+## Example Output
+
+```
+✓ .gitinfo is valid
+```
+
+```
+Validation failed for .gitinfo:
+ - .root: invalid URI "not-a-url"
+ - root: unknown property "invalid_field"
+```
+
+## Notes
+
+On Windows, you may need to adjust the execution policy:
+
+```powershell
+Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser
+```
diff --git a/validators/powershell/Validate-GitInfo.ps1 b/validators/powershell/Validate-GitInfo.ps1
new file mode 100644
index 0000000..e84606c
--- /dev/null
+++ b/validators/powershell/Validate-GitInfo.ps1
@@ -0,0 +1,248 @@
+<#
+.SYNOPSIS
+ Validates .gitinfo files against the gitinfo JSON Schema.
+
+.DESCRIPTION
+ A PowerShell validator for .gitinfo files. Parses JSONC (strips comments)
+ and validates against the schema.
+
+.PARAMETER Path
+ Path to the .gitinfo file. Defaults to ".gitinfo" in the current directory.
+
+.EXAMPLE
+ .\Validate-GitInfo.ps1
+ Validates .gitinfo in the current directory.
+
+.EXAMPLE
+ .\Validate-GitInfo.ps1 -Path "path/to/.gitinfo"
+ Validates a specific .gitinfo file.
+#>
+
+param(
+ [Parameter(Position = 0)]
+ [string]$Path = ".gitinfo"
+)
+
+$ErrorActionPreference = "Stop"
+
+# Schema path (two levels up from validators/powershell/)
+$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
+$SchemaPath = Join-Path $ScriptDir "..\..\gitinfo.schema.json"
+
+function Remove-JsonComments {
+ param([string]$Jsonc)
+
+ $result = New-Object System.Text.StringBuilder
+ $i = 0
+ $inString = $false
+ $stringChar = $null
+
+ while ($i -lt $Jsonc.Length) {
+ $char = $Jsonc[$i]
+ $next = if ($i + 1 -lt $Jsonc.Length) { $Jsonc[$i + 1] } else { $null }
+
+ # Track string state
+ if (-not $inString -and ($char -eq '"' -or $char -eq "'")) {
+ $inString = $true
+ $stringChar = $char
+ [void]$result.Append($char)
+ $i++
+ continue
+ }
+
+ if ($inString) {
+ if ($char -eq '\' -and $i + 1 -lt $Jsonc.Length) {
+ [void]$result.Append($char)
+ [void]$result.Append($Jsonc[$i + 1])
+ $i += 2
+ continue
+ }
+ if ($char -eq $stringChar) {
+ $inString = $false
+ $stringChar = $null
+ }
+ [void]$result.Append($char)
+ $i++
+ continue
+ }
+
+ # Single-line comment
+ if ($char -eq '/' -and $next -eq '/') {
+ while ($i -lt $Jsonc.Length -and $Jsonc[$i] -ne "`n") {
+ $i++
+ }
+ continue
+ }
+
+ # Multi-line comment
+ if ($char -eq '/' -and $next -eq '*') {
+ $i += 2
+ while ($i -lt $Jsonc.Length - 1 -and -not ($Jsonc[$i] -eq '*' -and $Jsonc[$i + 1] -eq '/')) {
+ $i++
+ }
+ $i += 2
+ continue
+ }
+
+ [void]$result.Append($char)
+ $i++
+ }
+
+ return $result.ToString()
+}
+
+function Test-Uri {
+ param([string]$Value)
+ try {
+ $uri = [System.Uri]::new($Value)
+ return $uri.Scheme -eq "http" -or $uri.Scheme -eq "https"
+ }
+ catch {
+ return $false
+ }
+}
+
+function Test-Email {
+ param([string]$Value)
+ return $Value -match '^[^\s@]+@[^\s@]+\.[^\s@]+$'
+}
+
+function Test-Schema {
+ param(
+ $Data,
+ $Schema,
+ [string]$JsonPath = ""
+ )
+
+ $errors = @()
+
+ if ($Schema.type -eq "object") {
+ if ($Data -isnot [System.Collections.IDictionary] -and $Data.GetType().Name -ne "PSCustomObject") {
+ $errors += "$(if ($JsonPath) { $JsonPath } else { 'root' }): expected object"
+ return $errors
+ }
+
+ # Convert PSCustomObject to hashtable for easier handling
+ $dataHash = @{}
+ $Data.PSObject.Properties | ForEach-Object { $dataHash[$_.Name] = $_.Value }
+
+ # Check additionalProperties
+ if ($Schema.additionalProperties -eq $false -and $Schema.properties) {
+ $allowed = $Schema.properties.PSObject.Properties.Name
+ foreach ($key in $dataHash.Keys) {
+ if ($key -notin $allowed) {
+ $errors += "$(if ($JsonPath) { $JsonPath } else { 'root' }): unknown property `"$key`""
+ }
+ }
+ }
+
+ # Validate each property
+ if ($Schema.properties) {
+ foreach ($prop in $Schema.properties.PSObject.Properties) {
+ $key = $prop.Name
+ $propSchema = $prop.Value
+ if ($dataHash.ContainsKey($key)) {
+ $errors += Test-Schema -Data $dataHash[$key] -Schema $propSchema -JsonPath "$JsonPath.$key"
+ }
+ }
+ }
+ }
+ elseif ($Schema.type -eq "array") {
+ if ($Data -isnot [System.Array]) {
+ $errors += "${JsonPath}: expected array"
+ return $errors
+ }
+
+ if ($Schema.items) {
+ for ($i = 0; $i -lt $Data.Count; $i++) {
+ $itemSchema = if ($Schema.items -is [System.Array]) { $Schema.items[$i] } else { $Schema.items }
+ if ($itemSchema) {
+ $errors += Test-Schema -Data $Data[$i] -Schema $itemSchema -JsonPath "$JsonPath[$i]"
+ }
+ }
+
+ if ($Schema.minItems -and $Data.Count -lt $Schema.minItems) {
+ $errors += "${JsonPath}: expected at least $($Schema.minItems) items"
+ }
+ if ($Schema.maxItems -and $Data.Count -gt $Schema.maxItems) {
+ $errors += "${JsonPath}: expected at most $($Schema.maxItems) items"
+ }
+ }
+ }
+ elseif ($Schema.type -eq "string") {
+ if ($Data -isnot [string]) {
+ $errors += "${JsonPath}: expected string"
+ return $errors
+ }
+
+ if ($Schema.minLength -and $Data.Length -lt $Schema.minLength) {
+ $errors += "${JsonPath}: string too short (min $($Schema.minLength))"
+ }
+
+ if ($Schema.format -eq "uri") {
+ if (-not (Test-Uri $Data)) {
+ $errors += "${JsonPath}: invalid URI `"$Data`""
+ }
+ }
+
+ if ($Schema.format -eq "email") {
+ if (-not (Test-Email $Data)) {
+ $errors += "${JsonPath}: invalid email `"$Data`""
+ }
+ }
+
+ if ($Schema.pattern) {
+ if ($Data -notmatch $Schema.pattern) {
+ $errors += "${JsonPath}: does not match pattern $($Schema.pattern)"
+ }
+ }
+ }
+
+ return $errors
+}
+
+# Main
+if (-not (Test-Path $Path)) {
+ Write-Error "Error: File not found: $Path"
+ exit 1
+}
+
+if (-not (Test-Path $SchemaPath)) {
+ Write-Error "Error: Schema not found: $SchemaPath"
+ exit 1
+}
+
+# Read and parse schema
+try {
+ $schemaContent = Get-Content -Path $SchemaPath -Raw
+ $schema = $schemaContent | ConvertFrom-Json
+}
+catch {
+ Write-Error "Error parsing schema: $_"
+ exit 1
+}
+
+# Read and parse .gitinfo file
+try {
+ $content = Get-Content -Path $Path -Raw
+ $stripped = Remove-JsonComments -Jsonc $content
+ $data = $stripped | ConvertFrom-Json
+}
+catch {
+ Write-Error "Error parsing JSONC: $_"
+ exit 1
+}
+
+# Validate against schema
+$errors = Test-Schema -Data $data -Schema $schema
+
+if ($errors.Count -gt 0) {
+ Write-Host "Validation failed for ${Path}:" -ForegroundColor Red
+ foreach ($error in $errors) {
+ Write-Host " - $error" -ForegroundColor Red
+ }
+ exit 1
+}
+
+Write-Host "✓ $Path is valid" -ForegroundColor Green
+exit 0