From 4fa66a2eb9a94fc908434eda360c7252c3c24284 Mon Sep 17 00:00:00 2001 From: grngxd <36968271+grngxd@users.noreply.github.com> Date: Tue, 24 Jun 2025 17:34:03 +0100 Subject: [PATCH] i love zod --- bun.lock | 3 + cmd/sync.ts | 271 +++++++++++++++++---------------------------------- package.json | 3 +- 3 files changed, 95 insertions(+), 182 deletions(-) diff --git a/bun.lock b/bun.lock index 6957b27..0718d26 100644 --- a/bun.lock +++ b/bun.lock @@ -7,6 +7,7 @@ "@types/hjson": "^2.4.6", "commander": "^14.0.0", "hjson": "^3.2.2", + "zod": "^3.25.67", }, "devDependencies": { "@types/bun": "^1.2.17", @@ -32,5 +33,7 @@ "typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="], "undici-types": ["undici-types@7.8.0", "", {}, "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw=="], + + "zod": ["zod@3.25.67", "", {}, "sha512-idA2YXwpCdqUSKRCACDE6ItZD9TZzy3OZMtpfLoh6oPR47lipysRrJfjzMqFxQ3uJuUPyUeWe1r9vLH33xO/Qw=="], } } diff --git a/cmd/sync.ts b/cmd/sync.ts index af39821..eb05e5e 100644 --- a/cmd/sync.ts +++ b/cmd/sync.ts @@ -1,197 +1,75 @@ -import { exec } from "child_process"; import type { Command } from "commander"; -import { createHash } from "crypto"; -import { readdirSync, readFileSync, writeFileSync } from "fs"; +import { readdirSync, readFileSync } from "fs"; import * as h from "hjson"; import { homedir } from "os"; import path from "path"; +import * as z from "zod/v4"; import { isDev } from "../lib"; +const mixDir = isDev + ? process.cwd() + : path.join(homedir(), ".mix"); + +const getMixFiles = (): { name: string, content: MixFile }[] => { + return readdirSync(mixDir, { withFileTypes: true }) + .filter(d => d.isFile() && d.name.endsWith("mix.hjson") && !d.name.startsWith("_")) + .map(d => ({ + name: d.name, + content: MixFileSchema.parse( + h.parse(readFileSync(path.join(mixDir, d.name), "utf-8")) + ) + })); +}; + +const mergeMixFiles = (files: MixFile[]): MixFile => { + const merged: MixFile = {}; + + for (const file of files) { + for (const group in file) { + if (!merged[group]) { + merged[group] = { packages: [] }; + } + + const packageToCheck = file[group]?.packages?.[0]; + const existingPackage = packageToCheck + ? merged[group].packages.find(pkg => pkg.id === packageToCheck.id) + : undefined; + + if (existingPackage) { + throw new Error(`Duplicate package id found: ${packageToCheck!.id} in group ${group}`); + } + + if (file[group]?.packages) { + merged[group].packages.push(...file[group].packages); + } + } + } + + return MixFileSchema.parse(merged); +} + +const getLockFile = (): MixLockFile => { + return MixLockFileSchema.parse(h.parse( + readFileSync(path.join(mixDir, "mix.lock"), "utf-8") + )); +}; + export const registerSyncCommand = (p: Command) => { p .command("sync") .action(() => { - const mixDir = isDev - ? process.cwd() - : path.join(homedir(), ".mix"); - - const getMixFiles = (): { name: string, content: MixFile }[] => { - return readdirSync(mixDir, { withFileTypes: true }) - .filter(d => d.isFile() && d.name.endsWith("mix.hjson") && !d.name.startsWith("_")) - .map(d => ({ - name: d.name, - content: h.parse(readFileSync(path.join(mixDir, d.name), "utf-8")) - })); - }; - - const mergeMixFiles = (files: MixFile[]): MixFile => { - const merged: MixFile = {}; - - for (const file of files) { - for (const group in file) { - if (!merged[group]) { - merged[group] = { packages: [] }; - } - - const packageToCheck = file[group]?.packages?.[0]; - const existingPackage = packageToCheck - ? merged[group].packages.find(pkg => pkg.id === packageToCheck.id) - : undefined; - - if (existingPackage) { - throw new Error(`Duplicate package id found: ${packageToCheck!.id} in group ${group}`); - } - - if (file[group]?.packages) { - merged[group].packages.push(...file[group].packages); - } - } - } - - return merged; + const files = getMixFiles(); + if (files.length === 0) { + console.log("No mix files found."); + return; } - const getLockFile = (): MixLockFile => { - return h.parse( - readFileSync(path.join(mixDir, "mix.lock"), "utf-8") - ); - }; - - const diff = (file: MixFile, lock: MixLockFile) => { - const toInstall: MixPackage[] = []; - const toRemove: MixLockFile = []; - const toUpdate: MixPackage[] = []; - const toConfig: MixPackage[] = []; - - const pkgs = Object.values(file).flatMap(g => g.packages); - - // install - for (const pkg of pkgs) { - if (!lock.find(l => l.id === pkg.id)) toInstall.push(pkg); - } - - // remove / update / config - for (const existing of lock) { - const pkg = pkgs.find(p => p.id === existing.id); - if (!pkg) { - toRemove.push(existing); - continue; - } - - // version changed? - if (pkg.version !== existing.version) { - toUpdate.push(pkg); - continue; - } - - // config changed? - for (const newCfg of pkg.config) { - const lockCfg = existing.config.find(c => c.path === newCfg.path); - const dataChanged = (() => { - if (!lockCfg) return true; - if (newCfg.type === "raw") { - return createHash("sha3-256").update(newCfg.data as string, "utf8").digest("hex") !== lockCfg.data; - } else { - return JSON.stringify(newCfg.data) !== JSON.stringify(lockCfg.data); - } - })(); - - if (!lockCfg || lockCfg.type !== newCfg.type || dataChanged) { - toConfig.push(pkg); - break; - } - } - } - - return [toInstall, toRemove, toUpdate, toConfig] as const; - }; - - const mixfile = mergeMixFiles(getMixFiles().map(file => file.content)); + const merged = mergeMixFiles(files.map(f => f.content)); const lock = getLockFile(); - const [ installing, removing, updating, configuring ] = diff(mixfile, lock); - - for (const pkg of installing) { - console.log(`Installing ${pkg.id} version ${pkg.version}`); - exec(`winget install --id ${pkg.id} --version ${pkg.version}`, (error, stdout, stderr) => { - if (error) { - console.error(`Error installing ${pkg.id}: ${error.message}`); - return; - } - if (stderr) { - console.error(`Error output for ${pkg.id}: ${stderr}`); - return; - } - console.log(`Installed ${pkg.id} version ${pkg.version}`); - - - const l: MixLockPackage = { - id: pkg.id, - version: pkg.version, - config: [] - } - - lock.push(l); - const content = h.stringify(lock); - const mixDir = isDev ? "" : path.join(homedir(), ".mix"); - writeFileSync(path.join(mixDir, "mix.lock"), content, "utf-8"); - }); - } - - for (const pkg of removing) { - console.log(`Removing ${pkg.id}`); - exec(`winget uninstall ${pkg.id}`, (error, stdout, stderr) => { - if (error) { - console.error(`Error uninstalling ${pkg.id}: ${error.message}`); - return; - } - if (stderr) { - console.error(`Error output for ${pkg.id}: ${stderr}`); - return; - } - console.log(`Uninstalled ${pkg.id}`); - - const index = lock.findIndex(l => l.id === pkg.id); - if (index !== -1) { - lock.splice(index, 1); - const content = h.stringify(lock); - const mixDir = isDev ? "" : path.join(homedir(), ".mix"); - writeFileSync(path.join(mixDir, "mix.lock"), content, "utf-8"); - } - }); - } - - for (const pkg of updating) { - console.log(`Updating ${pkg.id} to version ${pkg.version}`); - exec(`winget upgrade ${pkg.id} --version ${pkg.version}`, (error, stdout, stderr) => { - if (error) { - console.error(`Error updating ${pkg.id}: ${error.message}`); - return; - } - if (stderr) { - console.error(`Error output for ${pkg.id}: ${stderr}`); - return; - } - console.log(`Updated ${pkg.id} to version ${pkg.version}`); - - const index = lock.findIndex(l => l.id === pkg.id); - if (index !== -1 && lock[index]) { - lock[index]!.version = pkg.version; - } else { - const l: MixLockPackage = { - id: pkg.id, - version: pkg.version, - config: [] - }; - - lock.push(l); - } - - const content = h.stringify(lock); - const mixDir = isDev ? "" : path.join(homedir(), ".mix"); - writeFileSync(path.join(mixDir, "mix.lock"), content, "utf-8"); - }); - } + console.log("files", h.stringify(merged)); + console.log("lock", h.stringify(lock)); + }); } @@ -211,6 +89,25 @@ type MixPackage = { }[] } +const MixPackageSchema = z.object({ + id: z.string(), + version: z.string(), + config: z.array(z.object({ + type: z.enum(["raw", "json"]), + path: z.string(), + data: z.unknown() + })) + .optional() + .default([]), +}); + +const MixFileSchema = z.record( + z.string(), + z.object({ + packages: z.array(MixPackageSchema) + }) +); + type MixLockFile = MixLockPackage[] type MixLockPackage = { @@ -221,4 +118,16 @@ type MixLockPackage = { path: string; data: string; }[]; -} \ No newline at end of file +} + +const MixLockPackageSchema = z.object({ + id: z.string(), + version: z.string(), + config: z.array(z.object({ + type: z.enum(["raw", "json"]), + path: z.string(), + data: z.string() + })) +}); + +const MixLockFileSchema = z.array(MixLockPackageSchema); \ No newline at end of file diff --git a/package.json b/package.json index 901c53a..d802e23 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "dependencies": { "@types/hjson": "^2.4.6", "commander": "^14.0.0", - "hjson": "^3.2.2" + "hjson": "^3.2.2", + "zod": "^3.25.67" } }