#!/usr/bin/env node const { execFileSync } = require("child_process"); const fs = require("fs"); const path = require("path"); function exitError(message, code = 1) { console.error("ERROR: " + message); process.exit(code); } function run(command, args) { return execFileSync(command, args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }); } function ensureDocker() { try { run("docker", ["version", "--format", "{{.Server.Version}}"]); } catch (e) { exitError("docker command is not executable, or Docker daemon is unavailable"); } } function inspectContainer(container) { try { const text = run("docker", ["inspect", container]); const data = JSON.parse(text); if (!Array.isArray(data) || data.length === 0) { exitError("container not found: " + container); } return data[0]; } catch (e) { if (e instanceof SyntaxError) { exitError("docker inspect output is not valid JSON"); } exitError("docker inspect failed: " + container); } } function quoteSh(value) { if (value === undefined || value === null) { return "''"; } const text = String(value); if (/^[A-Za-z0-9_@%+=:,./-]+$/.test(text)) { return text; } return "'" + text.replace(/'/g, "'\\''") + "'"; } function quoteMountSpec(value) { const text = String(value); if (text.startsWith("$(pwd)")) { const rest = text.slice("$(pwd)".length).replace(/(["\\`$])/g, "\\$1"); return '"$(pwd)' + rest + '"'; } return quoteSh(text); } function normalizeContainerName(name) { return (name || "container").replace(/^\/+/, "") || "container"; } function isLikelyProjectPath(source) { if (!source) return false; const normalized = source.replace(/\\/g, "/").replace(/\/+$/, ""); if (normalized === "/") return false; if (/^[A-Za-z]:$/i.test(normalized)) return false; const specialPrefixes = [ "/bin", "/boot", "/dev", "/etc", "/lib", "/lib64", "/proc", "/run", "/sbin", "/sys", "/tmp", "/usr", "/var", "/opt/docker", "/var/lib/docker", "/var/run/docker.sock", "C:/Program Files", "C:/Program Files (x86)", "C:/ProgramData", "C:/Windows" ]; return !specialPrefixes.some(prefix => normalized === prefix || normalized.startsWith(prefix + "/")); } function relativeToPwdIfProject(source) { if (!isLikelyProjectPath(source)) { return source; } const normalized = source.replace(/\\/g, "/").replace(/\/+$/, ""); const base = path.posix.basename(normalized); if (!base || base === "." || base === "/") { return source; } return "$(pwd)/" + base; } function collectEnv(config) { const env = Array.isArray(config.Env) ? config.Env : []; return env .filter(item => typeof item === "string" && item.length > 0) .map(item => " -e " + quoteSh(item) + " \\"); } function collectPorts(networkSettings, hostConfig) { const lines = []; const seen = new Set(); const ports = Object.assign({}, networkSettings && networkSettings.Ports ? networkSettings.Ports : {}); const bindings = hostConfig && hostConfig.PortBindings ? hostConfig.PortBindings : {}; for (const containerPort of Object.keys(bindings)) { if (!ports[containerPort]) ports[containerPort] = bindings[containerPort]; } for (const containerPort of Object.keys(ports).sort()) { const entries = ports[containerPort]; if (!Array.isArray(entries)) continue; for (const entry of entries) { if (!entry || !entry.HostPort) continue; const hostIp = entry.HostIp && entry.HostIp !== "0.0.0.0" && entry.HostIp !== "::" ? entry.HostIp + ":" : ""; const spec = hostIp + entry.HostPort + ":" + containerPort; if (seen.has(spec)) continue; seen.add(spec); lines.push(" -p " + quoteSh(spec) + " \\"); } } return lines; } function collectMounts(info) { const mounts = Array.isArray(info.Mounts) ? info.Mounts : []; const lines = []; for (const mount of mounts) { if (!mount || !mount.Destination) continue; if (mount.Type === "volume" && mount.Name) { let spec = mount.Name + ":" + mount.Destination; if (mount.RW === false) spec += ":ro"; lines.push(" -v " + quoteMountSpec(spec) + " \\"); continue; } if (mount.Source) { let spec = relativeToPwdIfProject(mount.Source) + ":" + mount.Destination; if (mount.RW === false) spec += ":ro"; lines.push(" -v " + quoteMountSpec(spec) + " \\"); } } return lines; } function collectNetworks(info) { const hostConfig = info.HostConfig || {}; const networkMode = hostConfig.NetworkMode; const lines = []; if (networkMode && networkMode !== "default" && networkMode !== "bridge") { lines.push(" --network " + quoteSh(networkMode) + " \\"); return lines; } const networkSettings = info.NetworkSettings || {}; const networks = networkSettings.Networks ? Object.keys(networkSettings.Networks) : []; if (networks.length === 1 && networks[0] !== "bridge") { lines.push(" --network " + quoteSh(networks[0]) + " \\"); } return lines; } function collectSimpleOptions(info) { const config = info.Config || {}; const hostConfig = info.HostConfig || {}; const lines = []; if (hostConfig.RestartPolicy && hostConfig.RestartPolicy.Name && hostConfig.RestartPolicy.Name !== "no") { let restart = hostConfig.RestartPolicy.Name; if (restart === "on-failure" && hostConfig.RestartPolicy.MaximumRetryCount) { restart += ":" + hostConfig.RestartPolicy.MaximumRetryCount; } lines.push(" --restart " + quoteSh(restart) + " \\"); } if (config.WorkingDir) lines.push(" -w " + quoteSh(config.WorkingDir) + " \\"); if (config.User) lines.push(" -u " + quoteSh(config.User) + " \\"); if (hostConfig.Privileged) lines.push(" --privileged \\"); if (config.Tty) lines.push(" -t \\"); if (config.OpenStdin) lines.push(" -i \\"); return lines; } function commandTail(config) { let entrypoint = []; if (Array.isArray(config.Entrypoint)) { entrypoint = config.Entrypoint.filter(item => item !== null && item !== undefined).map(String); } else if (typeof config.Entrypoint === "string" && config.Entrypoint) { entrypoint = [config.Entrypoint]; } const cmd = Array.isArray(config.Cmd) ? config.Cmd : (typeof config.Cmd === "string" && config.Cmd ? [config.Cmd] : []); return { entrypoint: entrypoint.length > 0 ? entrypoint[0] : null, args: entrypoint.slice(1).concat(cmd) }; } function buildScript(info) { const config = info.Config || {}; const hostConfig = info.HostConfig || {}; const name = normalizeContainerName(info.Name); const image = config.Image || info.Image || ""; const tail = commandTail(config); const lines = [ "#!/usr/bin/env bash", "set -euo pipefail", "", "name=" + quoteSh(name), "img=" + quoteSh(image), "", "docker run -d \\", " --name \"$name\" \\", ]; lines.push(...collectSimpleOptions(info)); lines.push(...collectEnv(config)); lines.push(...collectPorts(info.NetworkSettings || {}, hostConfig)); lines.push(...collectMounts(info)); lines.push(...collectNetworks(info)); if (tail.entrypoint) { lines.push(" --entrypoint " + quoteSh(tail.entrypoint) + " \\"); } const cmd = tail.args.map(quoteSh).join(" "); if (cmd) { lines.push(" \"$img\" " + cmd); } else { lines.push(" \"$img\""); } lines.push(""); return lines.join("\n"); } function outputFile(container, explicitOutput) { if (explicitOutput) return explicitOutput; const safe = container.replace(/[^A-Za-z0-9_.-]+/g, "_").replace(/^_+|_+$/g, "") || "container"; return path.resolve(process.cwd(), "docker-run-" + safe + ".sh"); } function main() { const args = process.argv.slice(2); if (args.length < 1 || args[0] === "-h" || args[0] === "--help") { console.log("Usage: node restore-run.js [output-file]"); process.exit(args.length < 1 ? 1 : 0); } ensureDocker(); const container = args[0]; const info = inspectContainer(container); const script = buildScript(info); const file = outputFile(container, args[1]); fs.writeFileSync(file, script, { mode: 0o755 }); console.log(file); } if (require.main === module) { main(); } module.exports = { buildScript, main };