update-new-templates

This commit is contained in:
hhftechnologies
2025-11-21 18:09:42 +05:30
parent a0d98e8b50
commit 3c684213b0
12 changed files with 6669 additions and 211 deletions

View File

@@ -1,5 +1,5 @@
name: Template Bug Report
description: Report a bug in one of our open source templates (Adguard, Appwrite, etc.)
description: Report a bug in one of our open source templates (Supabase, Appwrite, etc.)
labels: ["bug"]
body:
- type: markdown
@@ -7,24 +7,13 @@ body:
value: |
Before opening a new issue, please search existing issues to see if this bug has already been reported.
This template is specifically for bugs in our open source templates like Adguard, Appwrite, and others.
This template is specifically for bugs in our open source templates like Supabase, Appwrite, and others.
- type: input
attributes:
label: Service/Template Name
description: Which service or template is experiencing the bug?
placeholder: "e.g: nginx, redis, adguard, etc."
validations:
required: true
- type: dropdown
attributes:
label: Structure Type
description: Which structure format is this service using?
options:
- "Flat file (compose-files/service-name.yml)"
- "Folder structure (compose-files/services/service-name/)"
- "Not sure"
label: Template Name
description: Which template is experiencing the bug?
placeholder: "e.g: Supabase, Appwrite, Plausible, etc."
validations:
required: true
@@ -32,7 +21,7 @@ body:
attributes:
label: Relevant Logs of the Error
description: |
**IMPORTANT:** Go to the logs tab in your Compose Logs and take a screenshot of the error.
**IMPORTANT:** Please provide clear error logs from your deployment environment.
Please be clear and include the full error message. You can also paste text logs here.
placeholder: |
Please attach a clear screenshot of the error logs from the logs tab.
@@ -69,7 +58,8 @@ body:
render: bash
placeholder: |
Operating System: Ubuntu 20.04
Compose version: 2.40.3
Docker Version: [e.g., 24.0.0]
Docker Compose Version: [e.g., 2.20.0]
VPS Provider: DigitalOcean, Hetzner, etc.
Template Version: [if known]
Browser: Chrome, Firefox, etc. [if relevant]

View File

@@ -1,11 +0,0 @@
blank_issues_enabled: false
contact_links:
- name: New Feature request
url: https://github.com/hhftechnology/Dock-Dploy/discussions/new?category=request-feature
about: For feature/script requests, please use the Discussions section.
- name: 🤔 Questions and Help
url: https://forum.hhf.technology/c/help/85
about: For suggestions or questions, please use the Our forums.
- name: Discord
url: https://discord.gg/HDCt9MjyMJ
about: Join our Discord server to chat with other users in the hhf community.

View File

@@ -2,136 +2,48 @@ name: New Template Request
description: Suggest a new template for the project
labels: ["template"]
body:
- type: markdown
attributes:
value: |
Before submitting, please review the [README.md](https://github.com/hhftechnology/Marketplace/blob/main/README.md) to understand the repository structure and submission guidelines.
- type: input
attributes:
label: Service Name
description: Provide a clear and descriptive name for the service/template
placeholder: "e.g: nginx, redis, postgres, etc."
label: Template Name
description: Provide a clear and descriptive name for the template
placeholder: "e.g: Supabase, Appwrite, etc."
validations:
required: true
- type: dropdown
- type: input
attributes:
label: Structure Preference
description: Choose the structure format for this service
options:
- "Flat file (compose-files/service-name.yml)"
- "Folder structure (compose-files/services/service-name/)"
- "No preference"
label: Template URL
description: Link to the repository or resource where the template is located
placeholder: "https://github.com/username/repo"
validations:
required: true
- type: textarea
- type: input
attributes:
label: Docker Compose Configuration
description: |
Paste your complete `docker-compose.yml` configuration here.
**Guidelines:**
- Include inline comments (ScaleTail style) for clarity
- Use `${VARIABLE_NAME}` syntax for configurable values
- Use descriptive volume placeholders (e.g., `/WORK_DIR` instead of absolute paths)
- Set timezone to `Etc/UTC` by default
placeholder: |
```yaml
services:
service-name:
image: service:latest
container_name: service-name
restart: unless-stopped
ports:
- "${PORT:-8080}:8080" # Service port
environment:
- SERVICE_VAR=${SERVICE_VAR}
```
validations:
required: true
- type: textarea
attributes:
label: Template Metadata (Optional - Folder Structure Only)
description: |
If using folder structure and the service needs template variables, domain configuration, or custom mounts, provide the `template.toml` content here.
See examples in `compose-files/services/nginx/template.toml` or `compose-files/services/redis/template.toml`
placeholder: |
```toml
[variables]
main_domain = "${domain}"
service_port = "80"
[config]
[[config.domains]]
serviceName = "service-name"
port = 80
host = "${main_domain}"
path = "/"
```
label: Docker Compose Link
description: Link to docker-compose.yml file or Docker documentation (optional) but would be helpful
placeholder: "https://github.com/username/repo/blob/main/docker-compose.yml"
validations:
required: false
- type: textarea
attributes:
label: Environment Variables Example (Optional - Folder Structure Only)
description: |
If using folder structure and the service has environment variables, provide the `.env.example` content here.
See examples in the service folders for the format.
label: Resources
description: List the resources, links, or any other information that would be helpful to know about the template
placeholder: |
```bash
# Service Configuration
SERVICE=service-name
IMAGE_URL=service:latest
SERVICEPORT=8080
# Template Variables
MAIN_DOMAIN=example.com
```
- Link to the template
- Link to the documentation
- Link to the repository
- Link to the website
validations:
required: false
- type: textarea
attributes:
label: Service Documentation (Optional - Folder Structure Only)
description: |
If using folder structure, provide a README.md content with setup instructions, features, and usage notes.
label: Template Description
description: Provide a detailed description of what this template does and when to use it
placeholder: |
# Service Name
Brief description of the service.
## Features
- Feature 1
- Feature 2
## Configuration
Setup instructions...
validations:
required: false
- type: textarea
attributes:
label: Resources and References
description: List the resources, links, or any other information that would be helpful
placeholder: |
- Link to official documentation
- Link to Docker Hub page
- Link to GitHub repository
- Link to website
validations:
required: false
- type: textarea
attributes:
label: Service Description
description: Provide a detailed description of what this service does and when to use it
placeholder: |
This service is perfect for [use case]...
This template is perfect for creating modern web applications with React and TypeScript...
validations:
required: false

View File

@@ -1,63 +1,16 @@
## What is this PR about?
<!-- Describe what this PR adds or changes -->
- [ ] New service/template
- [ ] Update to existing service
- [ ] Bug fix
- [ ] Documentation update
- [ ] Other (please describe)
**Service Name:** [e.g., nginx, redis, postgres]
**Structure Type:**
- [ ] Flat file (`compose-files/service-name.yml`)
- [ ] Folder structure (`compose-files/services/service-name/`)
New PR of [Template Name]
## Checklist
Before submitting this PR, please make sure that:
### General Requirements
- [ ] I have read the [README.md](https://github.com/hhftechnology/Marketplace/blob/main/README.md) and followed the submission guidelines
- [ ] I have tested the template/service in my instance
- [ ] The Docker image(s) are publicly accessible (no private registries)
- [ ] I have used `${VARIABLE_NAME}` syntax for configurable values
- [ ] I have used descriptive volume placeholders (e.g., `/WORK_DIR` instead of absolute paths)
- [ ] Timezone is set to `Etc/UTC` (if applicable)
- [ ] I have read the suggestions in the README.md file https://github.com/hhftechnology/Marketplace?tab=readme-ov-file#general-suggestions-when-creating-a-template
- [ ] I have tested the template in my instance, so the maintainers don't spend time trying to figure out what's wrong.
- [ ] I have added tests that demonstrate that my correction works or that my new feature works.
### Docker Compose File
- [ ] Includes inline comments (ScaleTail style) for clarity
- [ ] Minimized unnecessary ports, environment variables, or configurations
- [ ] Uses `latest` tag where possible (or specific version if required)
- [ ] Multi-service support is properly configured (if applicable)
### Folder Structure (If Applicable)
- [ ] `docker-compose.yml` is included and properly formatted
- [ ] `template.toml` is included (if template variables/domain config needed)
- [ ] `.env.example` is included (if environment variables are needed)
- [ ] `README.md` is included (if detailed setup instructions are needed)
### Testing
- [ ] I have tested the service deployment
- [ ] I have verified all environment variables work correctly
- [ ] I have checked that volumes and ports are correctly configured
- [ ] I have verified the service starts and runs without errors
## Files Changed
<!-- List the files you've added or modified -->
- `compose-files/[service-name].yml` (flat file structure)
- OR
- `compose-files/services/[service-name]/docker-compose.yml`
- `compose-files/services/[service-name]/template.toml` (if applicable)
- `compose-files/services/[service-name]/.env.example` (if applicable)
- `compose-files/services/[service-name]/README.md` (if applicable)
## Additional Notes
<!-- Any additional context, screenshots, or information that would help reviewers -->
## Issues Related (if applicable)
## Issues related (if applicable)
Close automatically the related issues using the keywords: `closes #ISSUE_NUMBER`, `fixes #ISSUE_NUMBER`, `resolves #ISSUE_NUMBER`

64
.github/copilot-instructions.md vendored Normal file
View File

@@ -0,0 +1,64 @@
# Marketplace Repository - Copilot Instructions
## Project Overview
This repository maintains Docker Compose templates for deploying open-source applications. The core structure revolves around the `compose-files/` directory, where each subdirectory represents a deployable service (e.g., `compose-files/nginx/` for Nginx web server).
Key components:
- **Compose Files**: Self-contained templates with `docker-compose.yml` (service definitions).
- **meta.json**: Centralized index of all templates. Entries include `id`, `name`, `version`, `description`, `logo`, `links`, and `tags`.
- **Scripts**: Node.js tools in root and `build-scripts/` for maintaining `meta.json` (deduplication, sorting, validation).
Data flow: New templates added to `compose-files/` → Metadata updated in `meta.json` → Processing scripts ensure consistency → Templates ready for deployment.
The "why": Enables rapid, standardized deployment of 300+ OSS apps without manual config. Structure prioritizes simplicity—each template is independent, no shared state or complex interdependencies.
## Key Files and Directories
- `meta.json`: Array of template objects. Always process after edits using `node dedupe-and-sort-meta.js` to remove duplicates (by `id`) and sort alphabetically.
- `compose-files/<service>/`:
- `docker-compose.yml`: Standard Docker Compose v3.8. May include ports, volumes, and environment variables as needed.
- `logo.svg/png`: Service icon, referenced in `meta.json`.
- `dedupe-and-sort-meta.js`: Standalone script—reads `meta.json`, removes duplicate `id`s (keeps first), sorts by `id` (case-insensitive), creates timestamped backup.
- `build-scripts/process-meta.js`: Advanced processor with CLI options (`--verbose`, `--no-backup`, `--input`/`--output`), JSON schema validation (required: `id`, `name`, `version`, `description`, `links.github`, `logo`, `tags` array).
Exemplary template: `compose-files/nginx/``docker-compose.yml` defines Nginx service; meta entry tags as ["proxy", "web-server"].
## Development Workflow
1. **Add/Update Template**:
- Create `compose-files/<id>/` (e.g., `nginx`).
- Implement `docker-compose.yml` (single or multiple services; use volumes for persistence).
- Add/update `meta.json` entry with exact `id` matching folder.
- Run `node dedupe-and-sort-meta.js --backup` to validate/sort.
- Commit and push changes.
2. **Local Development**:
- Meta processing: `node build-scripts/process-meta.js --verbose` or `node dedupe-and-sort-meta.js --backup`.
- Test template: Use `docker-compose up` in the service directory to test locally.
3. **CI/CD**:
- `.github/workflows/validate-meta.yml`: Runs validation on push/PR—fails on duplicates, invalid JSON, missing fields.
- Integrate processing: Add `node build-scripts/process-meta.js` to build steps; use `--no-backup` in CI.
No tests in repo—focus on manual validation via scripts and Docker Compose testing. Debug: Check console output from processing scripts for warnings (e.g., missing `id`).
## Conventions and Patterns
- **Template IDs**: Lowercase, kebab-case (e.g., `active-pieces`); unique across repo—enforced by dedupe script.
- **Docker Compose**: Standard Docker Compose v3.8 format. Include `restart: unless-stopped`, persistent volumes (e.g., `- db-data:/var/lib/postgresql/data`). Services typically named after folder (e.g., `nginx` service).
- **Meta.json**: Entries as JSON objects; tags array of lowercase strings (e.g., ["monitoring", "database"]); links object with `github`, `website`, `docs`.
- **Versions**: Use `latest` tag or pin to specific versions in `docker-compose.yml` (e.g., `nginx:1.25-alpine`); match in `meta.json.version`.
- **Logos**: SVG preferred; size ~128x128; file name in `meta.json.logo` (e.g., "nginx.svg").
Cross-component: No runtime communication—templates independent. Each template can be deployed standalone using Docker Compose.
## Integration Points
- **Docker Compose**: Templates can be deployed directly using `docker-compose up`. Test deploys validate env interpolation and service configuration.
- **External Deps**: Docker Compose (v3.8+). No runtime deps beyond Node.js for meta processing scripts.
When editing, always re-run meta processing and validate template deployment.

80
.github/workflows/validate-meta.yml vendored Normal file
View File

@@ -0,0 +1,80 @@
name: Validate and Process Meta.json
on:
push:
branches: [main, master, develop]
paths: ["meta.json"]
pull_request:
branches: [main, master]
paths: ["meta.json"]
workflow_dispatch:
jobs:
validate-meta:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "18"
- name: Validate meta.json structure
run: |
echo "🔍 Validating meta.json structure..."
node -e "
const fs = require('fs');
const data = JSON.parse(fs.readFileSync('meta.json', 'utf8'));
if (!Array.isArray(data)) throw new Error('meta.json must be an array');
console.log('✅ meta.json structure is valid');
console.log('📊 Found', data.length, 'entries');
"
- name: Check for duplicates and sort order
run: |
echo "🔍 Checking for duplicates and sort order..."
node build-scripts/process-meta.js --verbose --output /tmp/meta-test.json
- name: Compare with original
run: |
echo "🔍 Comparing processed file with original..."
if ! diff -q meta.json /tmp/meta-test.json > /dev/null; then
echo "⚠️ meta.json needs processing (duplicates found or not sorted)"
echo "Original entries:"
node -e "console.log(JSON.parse(require('fs').readFileSync('meta.json', 'utf8')).length)"
echo "Processed entries:"
node -e "console.log(JSON.parse(require('fs').readFileSync('/tmp/meta-test.json', 'utf8')).length)"
echo ""
echo "To fix this, run: npm run process-meta"
exit 1
else
echo "✅ meta.json is properly deduplicated and sorted"
fi
- name: Validate required fields
run: |
echo "🔍 Validating required fields..."
node -e "
const fs = require('fs');
const data = JSON.parse(fs.readFileSync('meta.json', 'utf8'));
const required = ['id', 'name', 'version', 'description', 'links', 'logo', 'tags'];
let issues = 0;
data.forEach((item, index) => {
const missing = required.filter(field => !item[field]);
if (missing.length > 0) {
console.log('❌ Entry', index, '(' + item.id + '):', 'Missing fields:', missing.join(', '));
issues++;
}
});
if (issues > 0) {
console.log('🚨 Found', issues, 'entries with missing required fields');
process.exit(1);
} else {
console.log('✅ All entries have required fields');
}
"

181
.github/workflows/validate.yml vendored Normal file
View File

@@ -0,0 +1,181 @@
name: Validate Compose Files Structure and Meta
on:
pull_request:
branches:
- main
push:
branches:
- main
jobs:
validate:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Validate compose-files folders and files
run: |
echo "🔍 Validating compose-files folder structure..."
ERROR=0
# Loop through each service folder
for dir in compose-files/*; do
if [ -d "$dir" ]; then
TEMPLATE_NAME=$(basename "$dir")
COMPOSE_FILE="$dir/docker-compose.yml"
if [ ! -f "$COMPOSE_FILE" ]; then
echo "❌ Missing docker-compose.yml in $TEMPLATE_NAME"
ERROR=1
fi
fi
done
if [ $ERROR -eq 1 ]; then
echo "❌ Compose files folder validation failed."
exit 1
else
echo "✅ Compose files folders validated successfully."
fi
- name: Validate meta.json structure and required fields
run: |
echo "🔍 Validating meta.json structure and required fields..."
# First check if meta.json exists and is valid JSON
if [ ! -f "meta.json" ]; then
echo "❌ meta.json file not found"
exit 1
fi
if ! jq empty meta.json 2>/dev/null; then
echo "❌ meta.json is not a valid JSON file"
exit 1
fi
ERROR=0
ERRORS_FOUND=""
# Debug: Show total number of entries
TOTAL_ENTRIES=$(jq '. | length' meta.json)
echo "📊 Total entries in meta.json: $TOTAL_ENTRIES"
# Get all entries at once and process them
TOTAL_INDEX=$(($TOTAL_ENTRIES - 1))
for i in $(seq 0 $TOTAL_INDEX); do
entry=$(jq -c ".[$i]" meta.json)
INDEX=$((i + 1))
echo "-------------------------------------------"
echo "🔍 Checking entry #$INDEX..."
# Get the ID for better error reporting
ID=$(echo "$entry" | jq -r '.id // "UNKNOWN"')
echo "📝 Processing entry with ID: $ID"
# Validate required top-level fields
for field in "id" "name" "version" "description" "logo" "links" "tags"; do
if [ "$(echo "$entry" | jq "has(\"$field\")")" != "true" ]; then
ERROR_MSG="❌ Entry #$INDEX (ID: $ID) is missing required field: $field"
echo "$ERROR_MSG"
ERRORS_FOUND="${ERRORS_FOUND}${ERROR_MSG}\n"
ERROR=1
fi
done
# Validate links object required fields
if [ "$(echo "$entry" | jq 'has("links")')" == "true" ]; then
for link_field in "github" "website" "docs"; do
if [ "$(echo "$entry" | jq ".links | has(\"$link_field\")")" != "true" ]; then
ERROR_MSG="❌ Entry #$INDEX (ID: $ID): links object is missing required field: $link_field"
echo "$ERROR_MSG"
ERRORS_FOUND="${ERRORS_FOUND}${ERROR_MSG}\n"
ERROR=1
fi
done
fi
# Validate tags array is not empty
if [ "$(echo "$entry" | jq '.tags | length')" -eq 0 ]; then
ERROR_MSG="❌ Entry #$INDEX (ID: $ID): tags array cannot be empty"
echo "$ERROR_MSG"
ERRORS_FOUND="${ERRORS_FOUND}${ERROR_MSG}\n"
ERROR=1
fi
done
echo "-------------------------------------------"
if [ $ERROR -eq 1 ]; then
echo "❌ meta.json structure validation failed."
echo -e "Summary of all errors found:${ERRORS_FOUND}"
exit 1
else
echo "✅ meta.json structure validated successfully."
fi
- name: Validate meta.json matches compose-files folders and logo files
run: |
echo "🔍 Validating meta.json against compose-files folders and logos..."
ERROR=0
# Read all compose-files folder names into an array
FOLDERS=($(ls -1 compose-files))
# Extract ids and logos from meta.json
IDS_AND_LOGOS=$(jq -c '.[] | {id, logo}' meta.json)
# Validate each id in meta.json exists as a folder
for item in $IDS_AND_LOGOS; do
ID=$(echo "$item" | jq -r '.id')
LOGO=$(echo "$item" | jq -r '.logo')
# Check if folder exists
if [ ! -d "compose-files/$ID" ]; then
echo "❌ meta.json id \"$ID\" does not have a matching folder in compose-files/"
ERROR=1
continue
fi
# Check if logo file exists inside its folder (if logo is specified)
if [ "$LOGO" != "null" ] && [ -n "$LOGO" ]; then
if [ ! -f "compose-files/$ID/$LOGO" ]; then
echo "⚠️ Logo \"$LOGO\" defined for \"$ID\" does not exist in compose-files/$ID/ (warning only)"
# This is a warning, not an error, as logos are optional
fi
fi
done
# Validate each folder has a matching id in meta.json
META_IDS=$(jq -r '.[].id' meta.json)
for FOLDER in "${FOLDERS[@]}"; do
# Skip the services folder if it exists
if [ "$FOLDER" == "services" ]; then
continue
fi
FOUND=0
for ID in $META_IDS; do
if [ "$FOLDER" == "$ID" ]; then
FOUND=1
break
fi
done
if [ "$FOUND" -eq 0 ]; then
echo "❌ Folder \"$FOLDER\" has no matching id in meta.json"
ERROR=1
fi
done
if [ $ERROR -eq 1 ]; then
echo "❌ meta.json validation failed."
exit 1
else
echo "✅ meta.json validated successfully."
fi

View File

@@ -13,20 +13,17 @@ compose-files/
└── services/
├── nginx/
│ ├── docker-compose.yml # Main compose file
│ ├── template.toml # Template metadata (optional)
│ ├── .env.example # Example env file (optional)
│ └── README.md # Service docs (optional)
└── redis/
├── docker-compose.yml
├── template.toml
└── .env.example
```
### File Formats
- **docker-compose.yml**: Standard Docker Compose file with comments (ScaleTail style)
- **template.toml**: Dokploy-style template metadata for template variables and domain configuration (optional)
- **.env.example**: ScaleTail-style environment file with comments showing required variables (optional)
- **README.md**: Service-specific documentation (optional)
@@ -62,24 +59,6 @@ Use this structure in your issue description:
If using folder structure, you may include:
**template.toml** (if template variables are needed):
```toml
[variables]
main_domain = "${domain}"
service_port = "80"
[config]
[[config.domains]]
serviceName = "nginx"
port = 80
host = "${main_domain}"
path = "/"
[config.env]
NGINX_HOST = "${main_domain}"
```
**.env.example** (if environment variables are needed):
```bash
@@ -121,7 +100,6 @@ Before opening your issue, please verify that your submission meets these standa
### Folder Structure Requirements (If Using)
- **docker-compose.yml**: Required - Main compose file
- **template.toml**: Optional - Include if service needs template variables, domain configuration, or custom mounts
- **.env.example**: Optional - Include if service has environment variables that users need to configure
- **README.md**: Optional - Include for services that need detailed setup instructions or have special requirements

View File

@@ -0,0 +1,292 @@
#!/usr/bin/env node
/**
* Production build script for processing meta.json
* This script is designed to be run during CI/CD or build processes
*/
const fs = require("fs");
const path = require("path");
class MetaProcessor {
constructor(options = {}) {
this.options = {
inputFile: options.inputFile || "meta.json",
outputFile: options.outputFile || null, // If null, overwrites input
createBackup: options.createBackup || false, // Default false
verbose: options.verbose || false,
validateSchema: options.validateSchema !== false, // Default true
exitOnError: options.exitOnError !== false, // Default true
...options,
};
}
log(message, level = "info") {
if (!this.options.verbose && level === "debug") return;
const timestamp = new Date().toISOString();
const prefix =
{
info: "🔧",
success: "✅",
warning: "⚠️",
error: "❌",
debug: "🔍",
}[level] || "";
console.log(`[${timestamp}] ${prefix} ${message}`);
}
validateSchema(item, index) {
const requiredFields = [
"id",
"name",
"version",
"description",
"links",
"logo",
"tags",
];
const missing = requiredFields.filter((field) => !item[field]);
if (missing.length > 0) {
this.log(
`Item at index ${index} missing required fields: ${missing.join(", ")}`,
"warning"
);
return false;
}
// Validate links structure
if (typeof item.links !== "object" || !item.links.github) {
this.log(`Item "${item.id}" has invalid links structure`, "warning");
}
// Validate tags is array
if (!Array.isArray(item.tags)) {
this.log(
`Item "${item.id}" has invalid tags (should be array)`,
"warning"
);
}
return true;
}
async process() {
const startTime = Date.now();
this.log(`Starting meta.json processing...`);
try {
// Read input file
if (!fs.existsSync(this.options.inputFile)) {
throw new Error(`Input file not found: ${this.options.inputFile}`);
}
const fileContent = fs.readFileSync(this.options.inputFile, "utf8");
let data;
try {
data = JSON.parse(fileContent);
} catch (parseError) {
throw new Error(
`Invalid JSON in ${this.options.inputFile}: ${parseError.message}`
);
}
if (!Array.isArray(data)) {
throw new Error(
`Expected array in ${this.options.inputFile}, got ${typeof data}`
);
}
this.log(`Found ${data.length} total entries`);
// Process data
const results = this.dedupeAndSort(data);
// Create backup if requested
if (this.options.createBackup) {
const backupPath = `${this.options.inputFile}.backup.${Date.now()}`;
fs.writeFileSync(backupPath, fileContent, "utf8");
this.log(`Backup created: ${backupPath}`, "debug");
}
// Write output
const outputFile = this.options.outputFile || this.options.inputFile;
const newContent = this.formatJSON(results.unique) + "\n";
fs.writeFileSync(outputFile, newContent, "utf8");
// Report results
const duration = Date.now() - startTime;
this.log(`Processing completed in ${duration}ms`, "success");
this.log(`Statistics:`, "info");
this.log(` • Original entries: ${results.original}`, "info");
this.log(` • Duplicates removed: ${results.duplicatesRemoved}`, "info");
this.log(` • Final entries: ${results.final}`, "info");
this.log(` • Schema violations: ${results.schemaViolations}`, "info");
if (results.duplicates.length > 0) {
this.log(`Removed duplicates:`, "warning");
results.duplicates.forEach((dup) => {
this.log(` • "${dup.id}" (${dup.name})`, "warning");
});
}
return results;
} catch (error) {
this.log(`Processing failed: ${error.message}`, "error");
if (this.options.exitOnError) {
process.exit(1);
}
throw error;
}
}
dedupeAndSort(data) {
const seenIds = new Set();
const duplicates = [];
const unique = [];
let schemaViolations = 0;
data.forEach((item, index) => {
if (!item || typeof item !== "object") {
this.log(`Skipping invalid item at index ${index}`, "warning");
schemaViolations++;
return;
}
if (!item.id) {
this.log(
`Skipping item without ID at index ${index}: ${
item.name || "Unknown"
}`,
"warning"
);
schemaViolations++;
return;
}
// Validate schema if enabled
if (this.options.validateSchema) {
if (!this.validateSchema(item, index)) {
schemaViolations++;
}
}
// Check for duplicates
if (seenIds.has(item.id)) {
duplicates.push({
id: item.id,
name: item.name || "Unknown",
originalIndex: index,
});
this.log(
`Duplicate ID found: "${item.id}" (${item.name || "Unknown"})`,
"warning"
);
} else {
seenIds.add(item.id);
unique.push(item);
}
});
// Sort alphabetically by ID (ASCII order)
unique.sort((a, b) => {
const idA = a.id.toLowerCase();
const idB = b.id.toLowerCase();
return idA < idB ? -1 : idA > idB ? 1 : 0;
});
return {
original: data.length,
duplicatesRemoved: duplicates.length,
final: unique.length,
duplicates,
unique,
schemaViolations,
};
}
formatJSON(data) {
// Custom JSON formatter that keeps small arrays compact
return JSON.stringify(
data,
(key, value) => {
if (Array.isArray(value)) {
// Keep arrays compact if they're small and contain only strings
if (
value.length <= 5 &&
value.every((item) => typeof item === "string" && item.length < 50)
) {
return value;
}
}
return value;
},
2
);
}
}
// CLI usage
if (require.main === module) {
const args = process.argv.slice(2);
const options = {};
// Parse command line arguments
for (let i = 0; i < args.length; i++) {
const arg = args[i];
switch (arg) {
case "--input":
case "-i":
options.inputFile = args[++i];
break;
case "--output":
case "-o":
options.outputFile = args[++i];
break;
case "--backup":
options.createBackup = true;
break;
case "--no-backup":
options.createBackup = false;
break;
case "--verbose":
case "-v":
options.verbose = true;
break;
case "--no-schema-validation":
options.validateSchema = false;
break;
case "--help":
case "-h":
console.log(`
Usage: node process-meta.js [options]
Options:
-i, --input <file> Input file path (default: meta.json)
-o, --output <file> Output file path (default: same as input)
--backup Create backup file (disabled by default)
-v, --verbose Verbose output
--no-schema-validation Skip schema validation
-h, --help Show this help message
Examples:
node process-meta.js
node process-meta.js --input data/meta.json --output dist/meta.json
node process-meta.js --verbose --no-backup
`);
process.exit(0);
break;
}
}
const processor = new MetaProcessor(options);
processor.process().catch((error) => {
console.error("Process failed:", error.message);
process.exit(1);
});
}
module.exports = MetaProcessor;

View File

182
dedupe-and-sort-meta.js Normal file
View File

@@ -0,0 +1,182 @@
#!/usr/bin/env node
const fs = require("fs");
const path = require("path");
/**
* Remove duplicate IDs from meta.json and arrange them alphabetically
* Usage: node dedupe-and-sort-meta.js [options] [meta.json path]
* Options:
* --backup Create backup before processing
* --help Show help message
*/
function dedupeAndSortMeta(filePath = "meta.json", options = {}) {
console.log(`🔧 Processing ${filePath}...`);
try {
// Check if file exists
if (!fs.existsSync(filePath)) {
throw new Error(`File not found: ${filePath}`);
}
// Read and parse the JSON file
const fileContent = fs.readFileSync(filePath, "utf8");
let data;
try {
data = JSON.parse(fileContent);
} catch (parseError) {
throw new Error(`Invalid JSON in ${filePath}: ${parseError.message}`);
}
// Validate that data is an array
if (!Array.isArray(data)) {
throw new Error(`Expected an array in ${filePath}, got ${typeof data}`);
}
console.log(`📊 Found ${data.length} total entries`);
// Track duplicates and stats
const seenIds = new Set();
const duplicates = [];
const unique = [];
// Remove duplicates (keep first occurrence)
data.forEach((item, index) => {
if (!item || typeof item !== "object") {
console.warn(`⚠️ Skipping invalid item at index ${index}:`, item);
return;
}
if (!item.id) {
console.warn(
`⚠️ Skipping item without ID at index ${index}:`,
item.name || "Unknown"
);
return;
}
if (seenIds.has(item.id)) {
duplicates.push({
id: item.id,
name: item.name || "Unknown",
originalIndex: index,
});
console.warn(
`🔍 Duplicate ID found: "${item.id}" (${item.name || "Unknown"})`
);
} else {
seenIds.add(item.id);
unique.push(item);
}
});
// Sort alphabetically by ID (ASCII order)
unique.sort((a, b) => {
const idA = a.id.toLowerCase();
const idB = b.id.toLowerCase();
return idA < idB ? -1 : idA > idB ? 1 : 0;
});
// Create backup if requested
if (options.createBackup) {
const backupPath = `${filePath}.backup.${Date.now()}`;
fs.writeFileSync(backupPath, fileContent, "utf8");
console.log(`💾 Backup created: ${backupPath}`);
}
// Custom JSON formatter that keeps small arrays compact
function formatJSON(data) {
return JSON.stringify(
data,
(key, value) => {
if (Array.isArray(value)) {
// Keep arrays compact if they're small and contain only strings
if (
value.length <= 5 &&
value.every(
(item) => typeof item === "string" && item.length < 50
)
) {
return value;
}
}
return value;
},
2
);
}
// Write the cleaned and sorted data
const newContent = formatJSON(unique) + "\n";
fs.writeFileSync(filePath, newContent, "utf8");
// Report results
console.log("\n✅ Processing completed successfully!");
console.log(`📈 Statistics:`);
console.log(` • Original entries: ${data.length}`);
console.log(` • Duplicates removed: ${duplicates.length}`);
console.log(` • Final entries: ${unique.length}`);
console.log(` • Entries sorted alphabetically by ID`);
if (duplicates.length > 0) {
console.log(`\n🗑️ Removed duplicates:`);
duplicates.forEach((dup) => {
console.log(` • "${dup.id}" (${dup.name})`);
});
}
// Verify the result
const firstFew = unique.slice(0, 5).map((item) => item.id);
const lastFew = unique.slice(-5).map((item) => item.id);
console.log(
`\n🔤 ID range: ${firstFew[0]} ... ${lastFew[lastFew.length - 1]}`
);
return {
original: data.length,
duplicatesRemoved: duplicates.length,
final: unique.length,
duplicates: duplicates,
};
} catch (error) {
console.error(`❌ Error processing ${filePath}:`, error.message);
process.exit(1);
}
}
// CLI usage
if (require.main === module) {
const args = process.argv.slice(2);
const options = { createBackup: false };
let filePath = "meta.json";
// Parse command line arguments
for (let i = 0; i < args.length; i++) {
const arg = args[i];
if (arg === "--backup") {
options.createBackup = true;
} else if (arg === "--help" || arg === "-h") {
console.log(`
Usage: node dedupe-and-sort-meta.js [options] [file]
Options:
--backup Create backup before processing (disabled by default)
--help Show this help message
Examples:
node dedupe-and-sort-meta.js # Process meta.json without backup
node dedupe-and-sort-meta.js --backup # Process meta.json with backup
node dedupe-and-sort-meta.js --backup data.json # Process data.json with backup
`);
process.exit(0);
} else if (!arg.startsWith("--")) {
filePath = arg;
}
}
dedupeAndSortMeta(filePath, options);
}
module.exports = dedupeAndSortMeta;

5837
meta.json Normal file

File diff suppressed because it is too large Load Diff