commit 3a0b1a80c85c32b99a2f21a205eda313f369ec7e Author: grngxd <36968271+grngxd@users.noreply.github.com> Date: Tue Jun 24 13:32:46 2025 +0100 chore: reset history diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6470dcc --- /dev/null +++ b/.gitignore @@ -0,0 +1,39 @@ +# dependencies (bun install) +node_modules + +# output +out +dist +*.tgz + +# code coverage +coverage +*.lcov + +# logs +logs +_.log +report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json + +# dotenv environment variable files +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# caches +.eslintcache +.cache +*.tsbuildinfo + +# IntelliJ based IDEs +.idea + +# Finder (MacOS) folder config +.DS_Store + +########### + +*.mix.hjson +*mix.lock \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..3c828d9 --- /dev/null +++ b/README.md @@ -0,0 +1,11 @@ +# 🍜 mix +> ` a declerative file-based package manager for windows.` + + +## features +- uses winget 😭 + +## requirements +- windows 10+ ofc +- recent version of [Bun](https://bun.sh/) +- [winget](https://github.com/microsoft/winget-cli) \ No newline at end of file diff --git a/bun.lock b/bun.lock new file mode 100644 index 0000000..6957b27 --- /dev/null +++ b/bun.lock @@ -0,0 +1,36 @@ +{ + "lockfileVersion": 1, + "workspaces": { + "": { + "name": "wem", + "dependencies": { + "@types/hjson": "^2.4.6", + "commander": "^14.0.0", + "hjson": "^3.2.2", + }, + "devDependencies": { + "@types/bun": "^1.2.17", + }, + "peerDependencies": { + "typescript": "^5", + }, + }, + }, + "packages": { + "@types/bun": ["@types/bun@1.2.17", "", { "dependencies": { "bun-types": "1.2.17" } }, "sha512-l/BYs/JYt+cXA/0+wUhulYJB6a6p//GTPiJ7nV+QHa8iiId4HZmnu/3J/SowP5g0rTiERY2kfGKXEK5Ehltx4Q=="], + + "@types/hjson": ["@types/hjson@2.4.6", "", {}, "sha512-tEQ4hlyKfsb9WWeueUY5eRnU2eK+KdE0eofSpQ05v9Aah4VvWwIRIid/ZN1zZZ0TfeVTRDgabKKqKZXEkfD3Sw=="], + + "@types/node": ["@types/node@24.0.3", "", { "dependencies": { "undici-types": "~7.8.0" } }, "sha512-R4I/kzCYAdRLzfiCabn9hxWfbuHS573x+r0dJMkkzThEa7pbrcDWK+9zu3e7aBOouf+rQAciqPFMnxwr0aWgKg=="], + + "bun-types": ["bun-types@1.2.17", "", { "dependencies": { "@types/node": "*" } }, "sha512-ElC7ItwT3SCQwYZDYoAH+q6KT4Fxjl8DtZ6qDulUFBmXA8YB4xo+l54J9ZJN+k2pphfn9vk7kfubeSd5QfTVJQ=="], + + "commander": ["commander@14.0.0", "", {}, "sha512-2uM9rYjPvyq39NwLRqaiLtWHyDC1FvryJDa2ATTVims5YAS4PupsEQsDvP14FqhFr0P49CYDugi59xaxJlTXRA=="], + + "hjson": ["hjson@3.2.2", "", { "bin": { "hjson": "bin/hjson" } }, "sha512-MkUeB0cTIlppeSsndgESkfFD21T2nXPRaBStLtf3cAYA2bVEFdXlodZB0TukwZiobPD1Ksax5DK4RTZeaXCI3Q=="], + + "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=="], + } +} diff --git a/cmd/index.ts b/cmd/index.ts new file mode 100644 index 0000000..952fa19 --- /dev/null +++ b/cmd/index.ts @@ -0,0 +1,6 @@ +import type { Command } from "commander"; +import { registerSyncCommand } from "./sync"; + +export const registerCommands = (p: Command) => { + registerSyncCommand(p); +} \ No newline at end of file diff --git a/cmd/sync/index.ts b/cmd/sync/index.ts new file mode 100644 index 0000000..9ef492c --- /dev/null +++ b/cmd/sync/index.ts @@ -0,0 +1,184 @@ +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, { space: 2 }); + 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, { space: 2 }); + 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 new file mode 100644 index 0000000..fcaf67f --- /dev/null +++ b/index.ts @@ -0,0 +1,13 @@ +import { Command } from 'commander'; +import { registerCommands } from './cmd'; + +const program = new Command(); + +program + .name('string-util') + .description('CLI to some JavaScript string utilities') + .version('0.8.0'); + +registerCommands(program); + +program.parse(); \ No newline at end of file diff --git a/mix.hjson.example b/mix.hjson.example new file mode 100644 index 0000000..1cff6ac --- /dev/null +++ b/mix.hjson.example @@ -0,0 +1,21 @@ +default: { + packages: [ + { + id: "wez.wezterm" + version: "20240203-110809-5046fc22" + config: [ + { + type: "raw" + path: "~/.wezterm.lua" + data: + ''' + local wezterm = require 'wezterm' + local config = {} + config.color_scheme = 'Batman' + return config + ''' + } + ] + } + ] +} \ No newline at end of file diff --git a/mix.lock.example b/mix.lock.example new file mode 100644 index 0000000..0637a08 --- /dev/null +++ b/mix.lock.example @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..d5524b7 --- /dev/null +++ b/package.json @@ -0,0 +1,16 @@ +{ + "name": "wem", + "module": "index.ts", + "type": "module", + "devDependencies": { + "@types/bun": "^1.2.17" + }, + "peerDependencies": { + "typescript": "^5" + }, + "dependencies": { + "@types/hjson": "^2.4.6", + "commander": "^14.0.0", + "hjson": "^3.2.2" + } +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..9c62f74 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,28 @@ +{ + "compilerOptions": { + // Environment setup & latest features + "lib": ["ESNext"], + "target": "ESNext", + "module": "ESNext", + "moduleDetection": "force", + "jsx": "react-jsx", + "allowJs": true, + + // Bundler mode + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "noEmit": true, + + // Best practices + "strict": true, + "skipLibCheck": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedIndexedAccess": true, + + // Some stricter flags (disabled by default) + "noUnusedLocals": false, + "noUnusedParameters": false, + "noPropertyAccessFromIndexSignature": false + } +}