import { spawnSync } from 'node:child_process'; import { packwizContext } from './build.mjs'; export const usage = `Usage: ./mcpack build [--config FILE] [--release] ./mcpack release VERSION [--config FILE] ./mcpack add [--yes] [--config FILE] ./mcpack remove [--yes] [--config FILE] ./mcpack update [--yes] [--config FILE] ./mcpack update --all [--yes] [--config FILE] ./mcpack deploy [--artifact RELEASE_DIR] [--config FILE] [--ssh-config FILE] [--remote-node FILE] ./mcpack rollback [--backup ID --accept-progress-loss] [--config FILE] [--ssh-config FILE] [--remote-node FILE] ./mcpack rollback-pack VERSION [--confirm-compatible] [--config FILE] [--ssh-config FILE] [--remote-node FILE] ./mcpack backup [--target-release DIR] [--wait-timeout SECONDS] ./mcpack restore --backup SNAPSHOT_DIR --destination NEW_SERVER_ROOT Mod commands edit packwiz source and retain packwiz prompts/output. Development builds may be dirty; --release requires a clean Git worktree.`; export function parseArgs(args) { args = [...args]; const command = args.shift(); if (!['build', 'release', 'add', 'remove', 'update', 'backup', 'restore', 'deploy', 'rollback', 'rollback-pack'].includes(command)) throw new Error(usage); const options = { command, configFile: 'platform/config.yaml', releaseOnly: false, yes: false, all: false }; const seen = new Set(); while (args.length) { const argument = args.shift(); const option = argument === '-y' ? '--yes' : argument; if (['--config', '--release', '--yes', '--all', '--target-release', '--wait-timeout', '--backup', '--destination', '--artifact', '--ssh-config', '--remote-node', '--confirm-compatible', '--accept-progress-loss'].includes(option)) { if (seen.has(option)) throw new Error(`Repeated argument: ${option}`); seen.add(option); if (['--config', '--target-release', '--wait-timeout', '--backup', '--destination', '--artifact', '--ssh-config', '--remote-node'].includes(option)) { const value = args.shift(); if (!value || value.startsWith('-') || /[\0\r\n]/.test(value)) throw new Error(`Missing or invalid value: ${option}`); options[{'--config':'configFile','--target-release':'targetRelease','--wait-timeout':'waitTimeout','--backup':'backupDirectory','--destination':'destination','--artifact':'artifact','--ssh-config':'sshConfig','--remote-node':'remoteNode'}[option]] = value; } else options[option === '--release' ? 'releaseOnly' : option.slice(2)] = true; } else { if (argument.startsWith('-') || options.mod || /[\0\r\n]/.test(argument)) throw new Error(`Unexpected argument: ${argument}`); options.mod = argument; } } if (command === 'rollback' || command === 'rollback-pack') { if (options.yes || options.all || options.releaseOnly || options.artifact || options.targetRelease || options.waitTimeout || options.destination) throw new Error('Invalid rollback arguments'); if (command === 'rollback-pack') { if (!options.mod || options.backupDirectory || options['accept-progress-loss']) throw new Error('rollback-pack requires a version and optional --confirm-compatible'); options.version = options.mod; options.confirmCompatible = Boolean(options['confirm-compatible']); } else { if (options.mod || options['confirm-compatible'] || (options['accept-progress-loss'] && !options.backupDirectory)) throw new Error('rollback accepts only an optional snapshot ID and progress-loss acknowledgment'); options.backupId = options.backupDirectory; options.acceptProgressLoss = Boolean(options['accept-progress-loss']); } return options; } if (options['confirm-compatible'] || options['accept-progress-loss']) throw new Error('Confirmation option is only for rollback'); if (command === 'deploy') { if (options.mod || options.yes || options.all || options.releaseOnly || options.targetRelease || options.waitTimeout || options.backupDirectory || options.destination) throw new Error('deploy accepts only --artifact and --config'); return options; } if (options.artifact || options.sshConfig || options.remoteNode) throw new Error('--artifact is only valid for deploy'); const backupOptions = ['--target-release', '--wait-timeout', '--backup', '--destination']; if (command === 'backup' || command === 'restore') { if (options.mod || options.yes || options.all || options.releaseOnly || seen.has('--config')) throw new Error('Invalid backup/restore arguments'); if (command === 'backup') { if (options.backupDirectory || options.destination) throw new Error('backup accepts only --target-release and --wait-timeout'); if (options.waitTimeout && (!/^[1-9]\d*$/.test(options.waitTimeout) || !Number.isSafeInteger(Number(options.waitTimeout)))) throw new Error('Invalid --wait-timeout'); options.waitTimeout = Number(options.waitTimeout ?? 120); } else if (!options.backupDirectory || !options.destination || options.targetRelease || options.waitTimeout) throw new Error('restore requires --backup and --destination'); return options; } if (backupOptions.some(option => seen.has(option))) throw new Error('Backup/restore option used with another command'); if(command==='release'){ if(!options.mod||options.yes||options.all||options.releaseOnly)throw new Error('release requires VERSION and accepts only --config'); options.version=options.mod;return options; } if (command === 'build') { if (options.mod || options.yes || options.all) throw new Error('build accepts only --config and --release'); } else { if (options.releaseOnly) throw new Error('--release is only valid for build'); if (command !== 'update' && options.all) throw new Error('--all is only valid for update'); if (options.all && options.mod) throw new Error('Choose update or update --all'); if (!options.all && !options.mod) throw new Error(`${command} requires a mod`); } return options; } export function packwizArguments(options) { const arguments_ = options.yes ? ['--yes'] : []; if (options.command === 'add') { arguments_.push('modrinth', 'add', options.mod); } else if (options.command === 'remove') arguments_.push('remove', options.mod); else if (options.command === 'update') arguments_.push('update', ...(options.all ? ['--all'] : [options.mod])); else throw new Error('Expected a mod command'); return arguments_; } export function manageMod({ root, ...options }) { const context = packwizContext({ root, configFile: options.configFile }); const result = spawnSync(context.executable, [...context.args, ...packwizArguments(options)], { cwd: context.source, stdio: 'inherit' }); if (result.error) throw result.error; if (result.signal) throw new Error(`packwiz terminated by ${result.signal}`); return result.status ?? 1; }