init command

This commit is contained in:
grngxd 2025-06-24 13:56:56 +01:00
parent 1642272fae
commit f724fc0ecb
5 changed files with 251 additions and 220 deletions

View file

@ -1,5 +1,5 @@
# 🍜 mix # 🍜 mix
> ` a declerative file-based package manager for windows.` > ` a declerative file-based package manager for windows. `
## features ## features
@ -8,4 +8,10 @@
## requirements ## requirements
- windows 10+ ofc - windows 10+ ofc
- recent version of [Bun](https://bun.sh/) - recent version of [Bun](https://bun.sh/)
- [winget](https://github.com/microsoft/winget-cli) - [winget](https://github.com/microsoft/winget-cli)
## usage
- `bun i mix -g`, then `mix <command> <args>`
- OR `bunx mix <command> <args>`
- `mix init` to create your mixfile & lock file
- `mix sync` to install/remove/update/config packages

View file

@ -1,6 +1,8 @@
import type { Command } from "commander"; import type { Command } from "commander";
import { registerInitCommand } from "./init";
import { registerSyncCommand } from "./sync"; import { registerSyncCommand } from "./sync";
export const registerCommands = (p: Command) => { export const registerCommands = (p: Command) => {
registerSyncCommand(p); registerSyncCommand(p);
registerInitCommand(p);
} }

24
cmd/init.ts Normal file
View file

@ -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", "[]");
});
}

View file

@ -1,215 +1,214 @@
import { exec } from "child_process"; import { exec } from "child_process";
import type { Command } from "commander"; import type { Command } from "commander";
import { createHash } from "crypto"; import { createHash } from "crypto";
import { readdirSync, readFileSync, writeFileSync } from "fs"; import { readdirSync, readFileSync, writeFileSync } from "fs";
import * as h from "hjson"; import * as h from "hjson";
export const registerSyncCommand = (p: Command) => { export const registerSyncCommand = (p: Command) => {
p p
.command("sync") .command("sync")
.action((string: string) => { .action(() => {
const getMixFiles = (): { name: string, content: MixFile}[] => { const getMixFiles = (): { name: string, content: MixFile}[] => {
const files = readdirSync(process.cwd(), { withFileTypes: true }) const files = readdirSync(process.cwd(), { withFileTypes: true })
.filter((dirent) => dirent.isFile() && dirent.name.endsWith(".hjson") && !dirent.name.startsWith("_")) .filter((dirent) => dirent.isFile() && dirent.name.endsWith(".hjson") && !dirent.name.startsWith("_"))
.map((dirent) => ({ .map((dirent) => ({
name: dirent.name, name: dirent.name,
content: h.parse(readFileSync(dirent.name, "utf-8")) content: h.parse(readFileSync(dirent.name, "utf-8"))
})); }));
return files return files
} }
const mergeMixFiles = (files: MixFile[]): MixFile => { const mergeMixFiles = (files: MixFile[]): MixFile => {
const merged: MixFile = {}; const merged: MixFile = {};
for (const file of files) { for (const file of files) {
for (const group in file) { for (const group in file) {
if (!merged[group]) { if (!merged[group]) {
merged[group] = { packages: [] }; merged[group] = { packages: [] };
} }
// if 2 packages with the same id exist, error out const packageToCheck = file[group]?.packages?.[0];
const packageToCheck = file[group]?.packages?.[0]; const existingPackage = packageToCheck
const existingPackage = packageToCheck ? merged[group].packages.find(pkg => pkg.id === packageToCheck.id)
? merged[group].packages.find(pkg => pkg.id === packageToCheck.id) : undefined;
: undefined;
if (existingPackage) {
if (existingPackage) { throw new Error(`Duplicate package id found: ${packageToCheck!.id} in group ${group}`);
throw new Error(`Duplicate package id found: ${packageToCheck!.id} in group ${group}`); }
}
if (file[group]?.packages) {
if (file[group]?.packages) { merged[group].packages.push(...file[group].packages);
merged[group].packages.push(...file[group].packages); }
} }
} }
}
return merged;
return merged; }
}
const getLockFile = (): MixLockFile => {
const getLockFile = (): MixLockFile => { return h.parse(readFileSync("mix.lock", "utf-8"));
return h.parse(readFileSync("mix.lock", "utf-8")); }
}
const diff = (file: MixFile, lock: MixLockFile) => {
const diff = (file: MixFile, lock: MixLockFile) => { const toInstall: MixPackage[] = [];
const toInstall: MixPackage[] = []; const toRemove: MixLockFile = [];
const toRemove: MixLockFile = []; const toUpdate: MixPackage[] = [];
const toUpdate: MixPackage[] = []; const toConfig: MixPackage[] = [];
const toConfig: MixPackage[] = [];
const pkgs = Object.values(file).flatMap(g => g.packages);
const pkgs = Object.values(file).flatMap(g => g.packages);
// install
// install for (const pkg of pkgs) {
for (const pkg of pkgs) { if (!lock.find(l => l.id === pkg.id)) toInstall.push(pkg);
if (!lock.find(l => l.id === pkg.id)) toInstall.push(pkg); }
}
// remove / update / config
// remove / update / config for (const existing of lock) {
for (const existing of lock) { const pkg = pkgs.find(p => p.id === existing.id);
const pkg = pkgs.find(p => p.id === existing.id); if (!pkg) {
if (!pkg) { toRemove.push(existing);
toRemove.push(existing); continue;
continue; }
}
// version changed?
// version changed? if (pkg.version !== existing.version) {
if (pkg.version !== existing.version) { toUpdate.push(pkg);
toUpdate.push(pkg); continue;
continue; }
}
// config changed?
// config changed? for (const newCfg of pkg.config) {
for (const newCfg of pkg.config) { const lockCfg = existing.config.find(c => c.path === newCfg.path);
const lockCfg = existing.config.find(c => c.path === newCfg.path); const dataChanged = (() => {
const dataChanged = (() => { if (!lockCfg) return true;
if (!lockCfg) return true; if (newCfg.type === "raw") {
if (newCfg.type === "raw") { return createHash("sha3-256").update(newCfg.data as string, "utf8").digest("hex") !== lockCfg.data;
return createHash("sha3-256").update(newCfg.data as string, "utf8").digest("hex") !== lockCfg.data; } else {
} else { return JSON.stringify(newCfg.data) !== JSON.stringify(lockCfg.data);
return JSON.stringify(newCfg.data) !== JSON.stringify(lockCfg.data); }
} })();
})();
if (!lockCfg || lockCfg.type !== newCfg.type || dataChanged) {
if (!lockCfg || lockCfg.type !== newCfg.type || dataChanged) { toConfig.push(pkg);
toConfig.push(pkg); break;
break; }
} }
} }
}
return [toInstall, toRemove, toUpdate, toConfig] as const;
return [toInstall, toRemove, toUpdate, toConfig] as const; };
};
const mixfile = mergeMixFiles(getMixFiles().map(file => file.content));
const mixfile = mergeMixFiles(getMixFiles().map(file => file.content)); const lock = getLockFile();
const lock = getLockFile();
const [ installing, removing, updating, configuring ] = diff(mixfile, lock);
const [ installing, removing, updating, configuring ] = diff(mixfile, lock);
for (const pkg of installing) {
for (const pkg of installing) { console.log(`Installing ${pkg.id} version ${pkg.version}`);
console.log(`Installing ${pkg.id} version ${pkg.version}`); exec(`winget install ${pkg.id} --version ${pkg.version}`, (error, stdout, stderr) => {
exec(`winget install ${pkg.id} --version ${pkg.version}`, (error, stdout, stderr) => { if (error) {
if (error) { console.error(`Error installing ${pkg.id}: ${error.message}`);
console.error(`Error installing ${pkg.id}: ${error.message}`); return;
return; }
} if (stderr) {
if (stderr) { console.error(`Error output for ${pkg.id}: ${stderr}`);
console.error(`Error output for ${pkg.id}: ${stderr}`); return;
return; }
} console.log(`Installed ${pkg.id} version ${pkg.version}`);
console.log(`Installed ${pkg.id} version ${pkg.version}`);
const l: MixLockPackage = {
const l: MixLockPackage = { id: pkg.id,
id: pkg.id, version: pkg.version,
version: pkg.version, config: []
config: [] }
}
lock.push(l);
lock.push(l); const content = h.stringify(lock);
const content = h.stringify(lock); writeFileSync("mix.lock", content, "utf-8");
writeFileSync("mix.lock", content, "utf-8"); });
}); }
}
for (const pkg of removing) {
for (const pkg of removing) { console.log(`Removing ${pkg.id}`);
console.log(`Removing ${pkg.id}`); exec(`winget uninstall ${pkg.id}`, (error, stdout, stderr) => {
exec(`winget uninstall ${pkg.id}`, (error, stdout, stderr) => { if (error) {
if (error) { console.error(`Error uninstalling ${pkg.id}: ${error.message}`);
console.error(`Error uninstalling ${pkg.id}: ${error.message}`); return;
return; }
} if (stderr) {
if (stderr) { console.error(`Error output for ${pkg.id}: ${stderr}`);
console.error(`Error output for ${pkg.id}: ${stderr}`); return;
return; }
} console.log(`Uninstalled ${pkg.id}`);
console.log(`Uninstalled ${pkg.id}`);
const index = lock.findIndex(l => l.id === pkg.id);
const index = lock.findIndex(l => l.id === pkg.id); if (index !== -1) {
if (index !== -1) { lock.splice(index, 1);
lock.splice(index, 1); const content = h.stringify(lock);
const content = h.stringify(lock); writeFileSync("mix.lock", content, "utf-8");
writeFileSync("mix.lock", content, "utf-8"); }
} });
}); }
}
for (const pkg of updating) {
for (const pkg of updating) { console.log(`Updating ${pkg.id} to version ${pkg.version}`);
console.log(`Updating ${pkg.id} to version ${pkg.version}`); exec(`winget upgrade ${pkg.id} --version ${pkg.version}`, (error, stdout, stderr) => {
exec(`winget upgrade ${pkg.id} --version ${pkg.version}`, (error, stdout, stderr) => { if (error) {
if (error) { console.error(`Error updating ${pkg.id}: ${error.message}`);
console.error(`Error updating ${pkg.id}: ${error.message}`); return;
return; }
} if (stderr) {
if (stderr) { console.error(`Error output for ${pkg.id}: ${stderr}`);
console.error(`Error output for ${pkg.id}: ${stderr}`); return;
return; }
} console.log(`Updated ${pkg.id} to version ${pkg.version}`);
console.log(`Updated ${pkg.id} to version ${pkg.version}`);
const index = lock.findIndex(l => l.id === pkg.id);
const index = lock.findIndex(l => l.id === pkg.id); if (index !== -1 && lock[index]) {
if (index !== -1 && lock[index]) { lock[index]!.version = pkg.version;
lock[index]!.version = pkg.version; } else {
} else { const l: MixLockPackage = {
const l: MixLockPackage = { id: pkg.id,
id: pkg.id, version: pkg.version,
version: pkg.version, config: []
config: [] };
};
lock.push(l);
lock.push(l); }
}
const content = h.stringify(lock);
const content = h.stringify(lock); writeFileSync("mix.lock", content, "utf-8");
writeFileSync("mix.lock", content, "utf-8"); });
}); }
} });
}); }
}
type MixFile = {
type MixFile = { [key: string]: {
[key: string]: { packages: MixPackage[];
packages: MixPackage[]; };
}; };
};
type MixPackage = {
type MixPackage = { id: string;
id: string; version: string;
version: string; config: {
config: { type: "raw" | "json";
type: "raw" | "json"; path: string;
path: string; data: unknown;
data: unknown; }[]
}[] }
}
type MixLockFile = MixLockPackage[]
type MixLockFile = MixLockPackage[]
type MixLockPackage = {
type MixLockPackage = { id: string;
id: string; version: string;
version: string; config: {
config: { type: "raw" | "json";
type: "raw" | "json"; path: string;
path: string; data: string;
data: string; }[];
}[];
} }

View file

@ -4,9 +4,9 @@ import { registerCommands } from './cmd';
const program = new Command(); const program = new Command();
program program
.name('string-util') .name('mix')
.description('CLI to some JavaScript string utilities') .description('a declerative file-based package manager for windows.')
.version('0.8.0'); .version('0.0.1', '-v, --version', 'output the current version');
registerCommands(program); registerCommands(program);