From ca12ec8a9c519e366dbe5d65f434993fc9d6f66c Mon Sep 17 00:00:00 2001 From: grngxd <36968271+grngxd@users.noreply.github.com> Date: Tue, 24 Jun 2025 18:56:20 +0100 Subject: [PATCH] yes! --- cmd/sync.ts | 133 ------------------------------------- cmd/sync/index.ts | 163 ++++++++++++++++++++++++++++++++++++++++++++++ cmd/sync/types.ts | 60 +++++++++++++++++ index.ts | 2 +- package.json | 2 +- 5 files changed, 225 insertions(+), 135 deletions(-) delete mode 100644 cmd/sync.ts create mode 100644 cmd/sync/index.ts create mode 100644 cmd/sync/types.ts diff --git a/cmd/sync.ts b/cmd/sync.ts deleted file mode 100644 index eb05e5e..0000000 --- a/cmd/sync.ts +++ /dev/null @@ -1,133 +0,0 @@ -import type { Command } from "commander"; -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 files = getMixFiles(); - if (files.length === 0) { - console.log("No mix files found."); - return; - } - - const merged = mergeMixFiles(files.map(f => f.content)); - const lock = getLockFile(); - - console.log("files", h.stringify(merged)); - console.log("lock", h.stringify(lock)); - - }); -} - -type MixFile = { - [key: string]: { - packages: MixPackage[]; - }; -}; - -type MixPackage = { - id: string; - version: string; - config: { - type: "raw" | "json"; - path: string; - data: unknown; - }[] -} - -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 = { - id: string; - version: string; - config: { - type: "raw" | "json"; - 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 diff --git a/cmd/sync/index.ts b/cmd/sync/index.ts new file mode 100644 index 0000000..645d208 --- /dev/null +++ b/cmd/sync/index.ts @@ -0,0 +1,163 @@ +import { exec } from "child_process"; +import type { Command } from "commander"; +import { readdirSync, readFileSync, writeFileSync } from "fs"; +import * as h from "hjson"; +import { homedir } from "os"; +import path from "path"; +import { isDev } from "../../lib"; +import { type MixFile, MixFileSchema, type MixLockFile, MixLockFileSchema, type MixLockPackage, type MixPackage } from "./types"; + +export const registerSyncCommand = (p: Command) => { + p + .command("sync") + .action(() => { + const files = getMixFiles(); + if (files.length === 0) { + console.log("No mix files found."); + return; + } + + const merged = mergeMixFiles(files.map(f => f.content)); + const lock = getLockFile(); + + const { toInstall, toRemove } = diff(merged, lock) + + if (toInstall.length === 0 && toRemove.length === 0) { + console.log("No changes detected. Everything is up to date."); + return; + } + + if (toInstall.length > 0) { + toInstall.forEach(async (pkg) =>{ + console.log(`Installing ${pkg.id}@${pkg.version}`); + await installPackage(lock, pkg.id, pkg.version); + }); + } + + if (toRemove.length > 0) { + toRemove.forEach(async (pkg) => { + console.log(`Removing ${pkg.id}@${pkg.version}`); + await removePackage(lock, pkg.id, pkg.version); + }); + } + }); +} + +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") + )); +}; + +const saveLockFile = (lock: MixLockFile) => { + const lockFilePath = path.join(mixDir, "mix.lock"); + const lockContent = h.stringify(lock, { space: 2 }); + writeFileSync(lockFilePath, lockContent, "utf-8"); +} + +const diff = (mix: MixFile, lock: MixLockFile) => { + const mixPkgs = Object.values(mix).flatMap(g => g.packages) + + const toInstall: MixPackage[] = []; + const toRemove: MixLockPackage[] = []; + + for (const pkg of mixPkgs) { + if (!lock.find(l => l.id === pkg.id && l.version === pkg.version)) { + toInstall.push(pkg); + } + } + + for (const pkg of lock) { + if (!mixPkgs.find(m => m.id === pkg.id && m.version === pkg.version)) { + toRemove.push(pkg); + } + } + + return { + toInstall, + toRemove + } +} + +const installPackage = async (lock: MixLockFile, id: string, version: string) => { + await exec(`winget install --id ${id} --version ${version} --exact --silent --force --disable-interactivity`, (error, _, e) => { + if (error) { + console.error(`Error installing package ${id}@${version}:`, error); + return; + } + + const pkg: MixLockPackage = { + id, + version, + config: [] + } + + lock.push(pkg); + saveLockFile(lock); + + console.log(`Successfully installed ${id}@${version}`); + e && console.error(e); + }); +}; + +const removePackage = async (lock: MixLockFile, id: string, version: string) => { + exec(`winget remove --id ${id} --version ${version} --exact --silent --force`, (error, _, e) => { + if (error) { + console.error(`Error removing package ${id}@${version}:`, error); + return; + } + + const index = lock.findIndex(pkg => pkg.id === id && pkg.version === version); + if (index !== -1) { + lock.splice(index, 1); + saveLockFile(lock); + console.log(`Successfully removed ${id}@${version}`); + } else { + console.warn(`Package ${id}@${version} not found in lock file.`); + } + + e && console.error(e); + }); +} \ No newline at end of file diff --git a/cmd/sync/types.ts b/cmd/sync/types.ts new file mode 100644 index 0000000..f76f616 --- /dev/null +++ b/cmd/sync/types.ts @@ -0,0 +1,60 @@ +import * as z from "zod/v4"; + +export type MixFile = { + [key: string]: { + packages: MixPackage[]; + }; +}; + +export type MixPackage = { + id: string; + version: string; + config: { + type: "raw" | "json"; + path: string; + data: unknown; + }[] +} + +export 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([]), +}); + +export const MixFileSchema = z.record( + z.string(), + z.object({ + packages: z.array(MixPackageSchema) + }) +); + +export type MixLockFile = MixLockPackage[] + +export type MixLockPackage = { + id: string; + version: string; + config: { + type: "raw" | "json"; + path: string; + data: string; + }[]; +} + +export 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() + })) +}); + +export const MixLockFileSchema = z.array(MixLockPackageSchema); \ No newline at end of file diff --git a/index.ts b/index.ts index a55b4b6..408b8e0 100644 --- a/index.ts +++ b/index.ts @@ -6,7 +6,7 @@ const program = new Command(); program .name('mix') .description('a declerative file-based package manager for windows.') - .version('0.0.6', '-v, --version', 'output the current version'); + .version('0.0.7', '-v, --version', 'output the current version'); registerCommands(program); program.parse(); \ No newline at end of file diff --git a/package.json b/package.json index d802e23..0fd07c7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@grng/mix", - "version": "0.0.6", + "version": "0.0.7", "publishConfig": { "access": "public" },