diff --git a/bun.lock b/bun.lock index 0718d26..6957b27 100644 --- a/bun.lock +++ b/bun.lock @@ -7,7 +7,6 @@ "@types/hjson": "^2.4.6", "commander": "^14.0.0", "hjson": "^3.2.2", - "zod": "^3.25.67", }, "devDependencies": { "@types/bun": "^1.2.17", @@ -33,7 +32,5 @@ "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 eb05e5e..f23d698 100644 --- a/cmd/sync.ts +++ b/cmd/sync.ts @@ -1,75 +1,194 @@ +import { exec } from "child_process"; import type { Command } from "commander"; -import { readdirSync, readFileSync } from "fs"; +import { createHash } from "crypto"; +import { readdirSync, readFileSync, writeFileSync } 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 files = getMixFiles(); - if (files.length === 0) { - console.log("No mix files found."); - return; + const getMixFiles = (): { name: string, content: MixFile}[] => { + const files = readdirSync(isDev ? process.cwd() : homedir(), { withFileTypes: true }) + .filter((dirent) => dirent.isFile() && dirent.name.endsWith("mix.hjson") && !dirent.name.startsWith("_")) + .map((dirent) => ({ + name: dirent.name, + content: h.parse(readFileSync(dirent.name, "utf-8")) + })); + + return files } - const merged = mergeMixFiles(files.map(f => f.content)); + 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 getLockFile = (): MixLockFile => { + const mixDir = isDev ? "" : path.join(homedir(), ".mix"); + 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 lock = getLockFile(); - console.log("files", h.stringify(merged)); - console.log("lock", h.stringify(lock)); - + 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"); + }); + } }); } @@ -89,25 +208,6 @@ 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 = { @@ -118,16 +218,4 @@ type MixLockPackage = { path: string; data: string; }[]; -} - -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 +} \ No newline at end of file diff --git a/package.json b/package.json index d802e23..901c53a 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,6 @@ "dependencies": { "@types/hjson": "^2.4.6", "commander": "^14.0.0", - "hjson": "^3.2.2", - "zod": "^3.25.67" + "hjson": "^3.2.2" } }