From f724fc0ecb660a369393faafb8f5f58ed088934a Mon Sep 17 00:00:00 2001 From: grngxd <36968271+grngxd@users.noreply.github.com> Date: Tue, 24 Jun 2025 13:56:56 +0100 Subject: [PATCH] init command --- README.md | 10 +- cmd/index.ts | 4 +- cmd/init.ts | 24 ++ cmd/{sync/index.ts => sync.ts} | 427 ++++++++++++++++----------------- index.ts | 6 +- 5 files changed, 251 insertions(+), 220 deletions(-) create mode 100644 cmd/init.ts rename cmd/{sync/index.ts => sync.ts} (96%) diff --git a/README.md b/README.md index 3c828d9..db5bf78 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # 🍜 mix -> ` a declerative file-based package manager for windows.` +> ` a declerative file-based package manager for windows. ` ## features @@ -8,4 +8,10 @@ ## requirements - windows 10+ ofc - recent version of [Bun](https://bun.sh/) -- [winget](https://github.com/microsoft/winget-cli) \ No newline at end of file +- [winget](https://github.com/microsoft/winget-cli) + +## usage +- `bun i mix -g`, then `mix ` + - OR `bunx mix ` +- `mix init` to create your mixfile & lock file +- `mix sync` to install/remove/update/config packages \ No newline at end of file diff --git a/cmd/index.ts b/cmd/index.ts index 952fa19..f6dac59 100644 --- a/cmd/index.ts +++ b/cmd/index.ts @@ -1,6 +1,8 @@ import type { Command } from "commander"; +import { registerInitCommand } from "./init"; import { registerSyncCommand } from "./sync"; export const registerCommands = (p: Command) => { - registerSyncCommand(p); + registerSyncCommand(p); + registerInitCommand(p); } \ No newline at end of file diff --git a/cmd/init.ts b/cmd/init.ts new file mode 100644 index 0000000..18da2a0 --- /dev/null +++ b/cmd/init.ts @@ -0,0 +1,24 @@ +import type { Command } from "commander"; +import { readdirSync, writeFileSync } from "fs"; +import * as h from "hjson"; + +export const registerInitCommand = (p: Command) => { + p + .command("init") + .action(() => { + const dir = readdirSync(process.cwd(), { withFileTypes: true }) + .filter((dirent) => dirent.isFile() && (dirent.name.endsWith("mix.hjson") || dirent.name === "mix.lock")); + if (dir.length > 0) { + console.error("Error: Mix project already initialized. Please remove existing .mix.hjson or mix.lock files."); + process.exit(1); + } + + writeFileSync("mix.hjson", h.stringify({ + default: { + packages: [] + } + })); + + writeFileSync("mix.lock", "[]"); + }); +} \ No newline at end of file diff --git a/cmd/sync/index.ts b/cmd/sync.ts similarity index 96% rename from cmd/sync/index.ts rename to cmd/sync.ts index dbe1334..6ac554b 100644 --- a/cmd/sync/index.ts +++ b/cmd/sync.ts @@ -1,215 +1,214 @@ -import { exec } from "child_process"; -import type { Command } from "commander"; -import { createHash } from "crypto"; -import { readdirSync, readFileSync, writeFileSync } from "fs"; -import * as h from "hjson"; - -export const registerSyncCommand = (p: Command) => { - p - .command("sync") - .action((string: string) => { - const getMixFiles = (): { name: string, content: MixFile}[] => { - const files = readdirSync(process.cwd(), { withFileTypes: true }) - .filter((dirent) => dirent.isFile() && dirent.name.endsWith(".hjson") && !dirent.name.startsWith("_")) - .map((dirent) => ({ - name: dirent.name, - content: h.parse(readFileSync(dirent.name, "utf-8")) - })); - - return files - } - - const mergeMixFiles = (files: MixFile[]): MixFile => { - const merged: MixFile = {}; - - for (const file of files) { - for (const group in file) { - if (!merged[group]) { - merged[group] = { packages: [] }; - } - - // if 2 packages with the same id exist, error out - 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 => { - return h.parse(readFileSync("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(); - - const [ installing, removing, updating, configuring ] = diff(mixfile, lock); - - for (const pkg of installing) { - console.log(`Installing ${pkg.id} version ${pkg.version}`); - exec(`winget install ${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); - writeFileSync("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); - writeFileSync("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); - writeFileSync("mix.lock", content, "utf-8"); - }); - } - }); -} - -type MixFile = { - [key: string]: { - packages: MixPackage[]; - }; -}; - -type MixPackage = { - id: string; - version: string; - config: { - type: "raw" | "json"; - path: string; - data: unknown; - }[] -} - -type MixLockFile = MixLockPackage[] - -type MixLockPackage = { - id: string; - version: string; - config: { - type: "raw" | "json"; - path: string; - data: string; - }[]; +import { exec } from "child_process"; +import type { Command } from "commander"; +import { createHash } from "crypto"; +import { readdirSync, readFileSync, writeFileSync } from "fs"; +import * as h from "hjson"; + +export const registerSyncCommand = (p: Command) => { + p + .command("sync") + .action(() => { + const getMixFiles = (): { name: string, content: MixFile}[] => { + const files = readdirSync(process.cwd(), { withFileTypes: true }) + .filter((dirent) => dirent.isFile() && dirent.name.endsWith(".hjson") && !dirent.name.startsWith("_")) + .map((dirent) => ({ + name: dirent.name, + content: h.parse(readFileSync(dirent.name, "utf-8")) + })); + + return files + } + + 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 => { + return h.parse(readFileSync("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(); + + const [ installing, removing, updating, configuring ] = diff(mixfile, lock); + + for (const pkg of installing) { + console.log(`Installing ${pkg.id} version ${pkg.version}`); + exec(`winget install ${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); + writeFileSync("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); + writeFileSync("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); + writeFileSync("mix.lock", content, "utf-8"); + }); + } + }); +} + +type MixFile = { + [key: string]: { + packages: MixPackage[]; + }; +}; + +type MixPackage = { + id: string; + version: string; + config: { + type: "raw" | "json"; + path: string; + data: unknown; + }[] +} + +type MixLockFile = MixLockPackage[] + +type MixLockPackage = { + id: string; + version: string; + config: { + type: "raw" | "json"; + path: string; + data: string; + }[]; } \ No newline at end of file diff --git a/index.ts b/index.ts index fcaf67f..699b5f4 100644 --- a/index.ts +++ b/index.ts @@ -4,9 +4,9 @@ import { registerCommands } from './cmd'; const program = new Command(); program - .name('string-util') - .description('CLI to some JavaScript string utilities') - .version('0.8.0'); + .name('mix') + .description('a declerative file-based package manager for windows.') + .version('0.0.1', '-v, --version', 'output the current version'); registerCommands(program);