From 672587613df90faddfb33561214e74b1d90acbfa Mon Sep 17 00:00:00 2001 From: Richard Schneeman Date: Wed, 3 Jun 2026 10:37:28 -0500 Subject: [PATCH 1/7] chore: vendor node-foreman library files into CLI Copy the 9 library files from node-foreman v3.0.1 (MIT license) into src/lib/local/foreman/ with .cjs extensions and updated internal require paths for the .cjs extension. No other changes to the upstream source. Refs #3736 --- src/lib/local/foreman/colors.cjs | 45 +++++++ src/lib/local/foreman/console.cjs | 123 +++++++++++++++++++ src/lib/local/foreman/envs.cjs | 115 ++++++++++++++++++ src/lib/local/foreman/exporters.cjs | 159 +++++++++++++++++++++++++ src/lib/local/foreman/forward.cjs | 29 +++++ src/lib/local/foreman/proc.cjs | 141 ++++++++++++++++++++++ src/lib/local/foreman/procfile.cjs | 55 +++++++++ src/lib/local/foreman/proxy.cjs | 118 ++++++++++++++++++ src/lib/local/foreman/requirements.cjs | 49 ++++++++ 9 files changed, 834 insertions(+) create mode 100644 src/lib/local/foreman/colors.cjs create mode 100644 src/lib/local/foreman/console.cjs create mode 100644 src/lib/local/foreman/envs.cjs create mode 100644 src/lib/local/foreman/exporters.cjs create mode 100644 src/lib/local/foreman/forward.cjs create mode 100644 src/lib/local/foreman/proc.cjs create mode 100644 src/lib/local/foreman/procfile.cjs create mode 100644 src/lib/local/foreman/proxy.cjs create mode 100644 src/lib/local/foreman/requirements.cjs diff --git a/src/lib/local/foreman/colors.cjs b/src/lib/local/foreman/colors.cjs new file mode 100644 index 0000000000..0c3af07558 --- /dev/null +++ b/src/lib/local/foreman/colors.cjs @@ -0,0 +1,45 @@ +// Copyright IBM Corp. 2012,2016. All Rights Reserved. +// Node module: foreman +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +var reset = '\x1B[0m'; +var colors = { + magenta: '\x1B[35m', + blue: '\x1B[34m', + cyan: '\x1B[36m', + green: '\x1B[32m', + yellow: '\x1B[33m', + red: '\x1B[31m', + bright_magenta: '\x1B[35m', + bright_cyan: '\x1B[36m', + bright_blue: '\x1B[34m', + bright_green: '\x1B[32m', + bright_yellow: '\x1B[33m', + bright_red: '\x1B[31m', +}; + +function identity(self) { + return self; +} + +function colorizer(color) { + if (process.stdout.isTTY) { + return function (str) { + return colors[color] + str + reset; + }; + } else { + return identity; + } +} + +module.exports.colors = []; + +var colorKeys = Object.keys(colors); +colorKeys.forEach(function(name) { + var colorFn = colorizer(name); + module.exports[name] = colorFn; + module.exports.colors.push(colorFn); +}); + +module.exports.colors_max = module.exports.colors.length; diff --git a/src/lib/local/foreman/console.cjs b/src/lib/local/foreman/console.cjs new file mode 100644 index 0000000000..bb543e306b --- /dev/null +++ b/src/lib/local/foreman/console.cjs @@ -0,0 +1,123 @@ +// Copyright IBM Corp. 2012,2016. All Rights Reserved. +// Node module: foreman +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +var util = require('util'); +var colors = require('./colors.cjs'); + +function wrap(log, length, res) { + if(!res) { res = []; } + if(log.length <= length) { + res.push(log); + return res; + } else { + res.push(log.substr(0, length)); + return wrap(log.substr(length), length, res); + } +} + +var trimEnd = '\u2026'; +var ansiEscapes = /\x1b\[(\d+([A-GJKSTm]|;\d+[Hf])|6n|s|u|\?25[lh])/g; + +function stripANSI(str) { + return str.replace(ansiEscapes, ''); +} + +function trim(line, n) { + line = line.replace(/\s+$/, ''); + var stripped = stripANSI(line); + if (stripped.length <= n) { + return line; + } else { + return stripped.substr(0, n) + trimEnd; + } +} + +function Console(logger) { + logger = logger || console; + this.padding = 25; + + this.trimline = 10; + this.wrapline = 500; + + this.fmt = function fmt() { + return util.format.apply(null, arguments); + }; + + this.pad = function pad(string, n) { + var l = string.length; + var o = string; + for(var i = l; i < n; i++) { + o += " "; + } + return o; + }; + + this.trim = trim; + + this.info = function info(key, proc, string) { + var stamp = (new Date().toLocaleTimeString()) + " " + key; + logger.log(proc.color(this.pad(stamp,this.padding)), colors.cyan(string)); + }; + + this.error = function error(key, proc, string) { + var stamp = (new Date().toLocaleTimeString()) + " " + key; + logger.error(proc.color(this.pad(stamp,this.padding)), colors.red(string)); + }; + + this.log = function log(key, proc, string) { + var self = this; + + if(self.raw) { + logger.log(string); + return; + } + + string.split(/\n/).forEach(function(line) { + + if (line.trim().length === 0) { return; } + + var stamp = (new Date().toLocaleTimeString()) + " " + key; + + if(self.trimline>0){ + line = self.trim(line,self.trimline); + } + + var delimiter = " | "; + + var wrapline; + if(self.wrapline === 0) { + wrapline = line.length; + } else { + wrapline = self.wrapline; + } + + wrap(line, wrapline).forEach(function(l) { + logger.log(proc.color(self.pad(stamp,self.padding) + delimiter), l); + delimiter = " | > "; + }); + + }); + }; + + this.Alert = function Alert() { + logger.log(colors.green('[OKAY] '+ this.fmt.apply(null, arguments))); + }; + + this.Done = function Info() { + logger.log(colors.cyan('[DONE] ' + this.fmt.apply(null, arguments))); + }; + + this.Warn = function Warn() { + logger.warn(colors.yellow('[WARN] ' + this.fmt.apply(null, arguments))); + }; + + this.Error = function Error() { + logger.error(colors.bright_red('[FAIL] ' + this.fmt.apply(null,arguments))); + }; + +} + +module.exports = Console; +Console.Console = new Console(); diff --git a/src/lib/local/foreman/envs.cjs b/src/lib/local/foreman/envs.cjs new file mode 100644 index 0000000000..2593f84bb8 --- /dev/null +++ b/src/lib/local/foreman/envs.cjs @@ -0,0 +1,115 @@ +// Copyright IBM Corp. 2012,2016. All Rights Reserved. +// Node module: foreman +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +var fs = require('fs'); +var util = require('util'); +var cons = require('./console.cjs').Console; + +function method(name) { + return function(o) { + return o[name].apply(o); + }; +} + +// Parse a Key=Value File Containing Environmental Variables +function keyValue(data) { + var env = {}; + + data + .toString() + .replace(/^\s*\#.*$/gm,'') + .replace(/^\s*$/gm,'') + .split(/\n/) + .map(method('trim')) + .filter(notBlank) + .forEach(capturePair); + + return env; + + function notBlank(str) { + return str.length > 2; + } + + function capturePair(line) { + var pair = line.split('='); + var key = pair[0].trim(); + var rawVal = pair.slice(1).join('=').trim(); + env[key] = parseValue(rawVal); + } + + function parseValue(val) { + switch (val[0]) { + case '"': return /^"([^"]*)"/.exec(val)[1]; + case "'": return /^'([^']*)'/.exec(val)[1]; + default : return val.replace(/\s*\#.*$/, ''); + } + } +} + +// Flatten nested object structure into KEY_SUBKEY=value pairs +function flattenJSON(json) { + var flattened = {}; + + walk(json, function(path, item) { + flattened[path.join('_').toUpperCase()] = item; + }); + + return flattened; + + function walk(obj, visitor, path) { + var item; + path = path || []; + for (var key in obj) { + item = obj[key]; + if (typeof item === 'object') { + walk(item, visitor, path.concat(key)); + } else { + visitor(path.concat(key), item); + } + } + } +} + +function dumpEnv(conf) { + var output = []; + for (var key in conf) { + output.push(key + '=' + conf[key]); + } + return output.sort().join('\n') + '\n'; +} + +function loadEnvsFile(path) { + var env, data; + + if(!fs.existsSync(path)) { + env = {}; + } else { + data = fs.readFileSync(path); + try { + var envs_json = JSON.parse(data); + env = flattenJSON(envs_json, "", {}); + cons.Alert("Loaded ENV %s File as JSON Format", path); + } catch (e) { + env = keyValue(data); + cons.Alert("Loaded ENV %s File as KEY=VALUE Format", path); + } + } + env.PATH = env.PATH || process.env.PATH; + return env; +} + +function loadEnvs(path) { + var envs = path.split(',').map(loadEnvsFile).reduce(util._extend, {}); + var sorted = Object.create(null); + Object.keys(envs).sort().forEach(function(k) { + sorted[k] = envs[k]; + }); + return sorted; +} + +module.exports.loadEnvs = loadEnvs; +module.exports.flattenJSON = flattenJSON; +module.exports.keyValue = keyValue; +module.exports.dumpEnv = dumpEnv; diff --git a/src/lib/local/foreman/exporters.cjs b/src/lib/local/foreman/exporters.cjs new file mode 100644 index 0000000000..bf93d8347a --- /dev/null +++ b/src/lib/local/foreman/exporters.cjs @@ -0,0 +1,159 @@ +// Copyright IBM Corp. 2014,2016. All Rights Reserved. +// Node module: foreman +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +// NOTE: This module depends on 'mustache' which is not a CLI dependency. +// The `nf export` command is not used by any Heroku CLI command, so this +// dependency is never exercised at runtime. + +var colors = require('./colors.cjs'); +var ppath = require('path'); +var mu = require('mustache'); +var fs = require('fs'); + +var display = require('./console.cjs').Console; + +function render(filename, conf, callback) { + fs.readFile(filename, {encoding: 'utf8'}, function(err, template) { + if (err) { + throw err; + } + callback(mu.render(template, conf)); + }); +} + +function templatePath(conf, type, file) { + if(conf.template) { + return ppath.resolve(conf.template, file); + } else { + return ppath.resolve(__dirname, type, file); + } +} + +function writeout(path) { + return function(data) { + if (fs.existsSync(path)) { + display.Warn(colors.bright_yellow('Replacing: %s'), path); + } + + fs.writeFileSync(path,data); + display.Alert('Wrote :',ppath.normalize(path)); + }; +} + +function upstart(conf, outdir) { + var path = outdir + "/" + conf.application + ".conf"; + render(templatePath(conf, 'upstart', 'foreman.conf'), conf, writeout(path)); +} + +function upstart_app(conf, outdir) { + var path = outdir + "/" + conf.application + "-" + conf.process + ".conf"; + render(templatePath(conf, 'upstart', 'foreman-APP.conf'), conf, writeout(path)); +} + +function upstart_app_n(conf, outdir) { + var path = outdir + "/" + conf.application + "-" + conf.process + "-" + conf.number + ".conf"; + render(templatePath(conf, 'upstart','foreman-APP-N.conf'), conf, writeout(path)); +} + +function upstart_single(conf, outdir) { + var path = outdir + "/" + conf.application + ".conf"; + render(templatePath(conf, 'upstart-single', 'foreman.conf'), conf, writeout(path)); + display.Warn('upstart-single jobs attempt to raise limits and will fail ' + + 'to start if the limits cannot be raised to the desired ' + + 'levels. Some manual editing may be required.'); +} + +function upstart_single_app(conf, outdir) { + var path = outdir + "/" + conf.application + "-" + conf.process + ".conf"; + render(templatePath(conf, 'upstart-single', 'foreman-APP.conf'), conf, writeout(path)); +} + +function systemd(conf, outdir){ + var path = outdir + "/" + conf.application + ".target"; + render(templatePath(conf, 'systemd', 'foreman.target'), conf, writeout(path)); +} + +function systemd_app(conf, outdir) { + var path = outdir + "/" + conf.application + "-" + conf.process + ".target"; + render(templatePath(conf, 'systemd', 'foreman-APP.target'), conf, writeout(path)); +} + +function systemd_app_n(conf, outdir) { + var path = outdir + "/" + conf.application + "-" + conf.process + "-" + conf.number + ".service"; + render(templatePath(conf, 'systemd', 'foreman-APP-N.service'), conf, writeout(path)); +} + +function supervisord(conf, outdir) { + var path = outdir + "/" + conf.application + ".conf"; + var programs = []; + + for(var i = 0; i < conf.processes.length; i++) { + var process = conf.processes[i].process; + var n = conf.processes[i].n; + + for(var j = 1; j <= n; j++) { + programs.push(conf.application + "-" + process + "-" + j); + } + } + + conf.programs = programs.join(','); + + render(templatePath(conf, 'supervisord', 'foreman.conf'), conf, writeout(path)); +} + +function supervisord_app_n(conf, outdir) { + var path = outdir + "/" + conf.application + "-" + conf.process + "-" + conf.number + ".conf"; + var envs = []; + + for(var i in conf.envs) { + var key = conf.envs[i].key; + var value = conf.envs[i].value; + + if(typeof value === 'string') { + value = value.replace(/"/, '\\"'); + } + + envs.push(key + "=" + '"' + value + '"'); + } + + conf.envs = envs.join(','); + + render(templatePath(conf, 'supervisord', 'foreman-APP-N.conf'), conf, writeout(path)); +} + +function smf_app(conf, outdir) { + var path = outdir + "/" + conf.application + "-" + conf.process + ".xml"; + render(templatePath(conf, 'smf', 'foreman-APP.xml'), conf, writeout(path)); +} + +var export_formats = { + "upstart": { + foreman : upstart, + foreman_app : upstart_app, + foreman_app_n : upstart_app_n, + }, + "upstart-single": { + foreman : upstart_single, + foreman_app : upstart_single_app, + foreman_app_n : function noop() {}, + }, + "systemd": { + foreman : systemd, + foreman_app : systemd_app, + foreman_app_n : systemd_app_n, + }, + "supervisord": { + foreman : supervisord, + foreman_app : function noop() {}, + foreman_app_n : supervisord_app_n, + }, + "smf": { + foreman : function noop() {}, + foreman_app : smf_app, + foreman_app_n : function noop() {}, + } +}; + +module.exports = export_formats; diff --git a/src/lib/local/foreman/forward.cjs b/src/lib/local/foreman/forward.cjs new file mode 100644 index 0000000000..aef05183a6 --- /dev/null +++ b/src/lib/local/foreman/forward.cjs @@ -0,0 +1,29 @@ +// Copyright IBM Corp. 2012,2016. All Rights Reserved. +// Node module: foreman +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +var prog = require('child_process'); + +var cons = require('./console.cjs').Console; + +function startForward(port, hostname, emitter) { + var proc = prog.fork(__dirname + '/../forward.js', [], { + env: { + PROXY_PORT: port, + PROXY_HOST: hostname || ' ' + } + }); + cons.Alert('Forward Proxy Started in Port %d', port); + if(hostname) { + cons.Alert('Intercepting requests to %s through forward proxy', hostname); + } else { + cons.Alert('Intercepting ALL requests through forward proxy'); + } + emitter.once('killall', function(signal) { + cons.Done('Killing Forward Proxy Server on Port %d',port); + proc.kill(signal); + }); +} + +module.exports.startForward = startForward; diff --git a/src/lib/local/foreman/proc.cjs b/src/lib/local/foreman/proc.cjs new file mode 100644 index 0000000000..0da3f100db --- /dev/null +++ b/src/lib/local/foreman/proc.cjs @@ -0,0 +1,141 @@ +// Copyright IBM Corp. 2012,2016. All Rights Reserved. +// Node module: foreman +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +var prog = require('child_process'); + +var cons = require('./console.cjs').Console; + +var _colors = require('./colors.cjs'); +var colors_max = _colors.colors_max; +var colors = _colors.colors; + +var os = require('os'); +var platform = os.platform(); + +function run(key, proc, emitter) { + var file, args; + if (platform === 'win32') { + file = process.env.comspec || 'cmd.exe'; + args = ['/s', '/c', proc.command]; + } else { + file = '/bin/sh'; + args = ['-c', proc.command]; + } + var child = prog.spawn(file, args, { env: proc.env }); + var killallReceived = false; + + child.stdout.on('data', function(data) { + cons.log(key, proc, data.toString()); + }); + + child.stderr.on('data', function(data) { + cons.log(key, proc, data.toString()); + }); + + child.on('close', function(code, signal) { + if(code === 0) { + cons.info(key, proc, "Exited Successfully"); + } else { + cons.error(key, proc, "Exited with exit code " + signal || code); + } + }); + + child.on('exit', function(code, signal) { + if (!killallReceived) { + emitter.emit('killall', signal || 'SIGINT'); + } + }); + + emitter.on('killall', function(signal) { + killallReceived = true; + + try { + child.kill(signal); + } + catch (err) { + if (err.code === 'EPERM') { + cons.error(key, proc, "Process has become unkillable; returns EPERM."); + } + } + }); + +} + +function once(input, envs, callback) { + var file, args; + var proc = { + command : input, + env : merge(merge({}, process.env), envs) + }; + + if (platform === 'win32') { + file = process.env.comspec || 'cmd.exe'; + args = ['/s', '/c', proc.command]; + } else { + file = '/bin/sh'; + args = ['-c', proc.command]; + } + + var child = prog.spawn(file, args, { env: proc.env, stdio: 'inherit' }); + + child.on('close', function(code) { + callback(code); + }); +} + +function start(procs, requirements, envs, portarg, emitter){ + + var j = 0; + var k = 0; + var port = parseInt(portarg); + + if(port < 1024) { + return cons.Error('Only Proxies Can Bind to Privileged Ports - '+ + 'Try \'sudo nf start -x %s\'', port); + } + + for(var key in requirements) { + var n = parseInt(requirements[key]); + + for(var i = 0; i < n; i++) { + + var color_val = (j + k) % colors_max; + + if (!procs[key]) { + cons.Warn("Required Key '%s' Does Not Exist in Procfile Definition", key); + continue; + } + + var p = { + command : procs[key], + color : colors[color_val], + env : merge(merge({}, process.env), envs) + }; + + p.env.PORT = port + j + k * 100; + p.env.FOREMAN_WORKER_NAME = p.env.FOREMAN_WORKER_NAME || key + "." + (i + 1); + + run(key + "." + (i + 1), p, emitter); + + j++; + + } + j = 0; + k++; + } +} + +function merge(a, b) { + if (a && b) { + for (var key in b) { + a[key] = b[key]; + } + } + return a; +} + +module.exports.start = start; +module.exports.run = run; +module.exports.once = once; diff --git a/src/lib/local/foreman/procfile.cjs b/src/lib/local/foreman/procfile.cjs new file mode 100644 index 0000000000..1a5871c62e --- /dev/null +++ b/src/lib/local/foreman/procfile.cjs @@ -0,0 +1,55 @@ +// Copyright IBM Corp. 2012,2016. All Rights Reserved. +// Node module: foreman +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +var fs = require('fs'); +var cons = require('./console.cjs').Console; +var path = require('path'); + +function procs(procdata){ + + var processes = {}; + + procdata.toString().split(/\n/).forEach(function(line) { + if(!line || line[0] === '#') { return; } + + var tuple = /^([A-Za-z0-9_-]+):\s*(.+)$/m.exec(line); + + var prockey = tuple[1].trim(); + var command = tuple[2].trim(); + + if(!prockey) { + throw new Error('Syntax Error in Procfile, Line %d: No Prockey Found'); + } + + if(!command) { + throw new Error('Syntax Error in Procfile, Line %d: No Command Found'); + } + + processes[prockey] = command; + }); + + return processes; +} + +function loadProc(filename) { + + try { + var data = fs.readFileSync(filename); + return procs(data); + } catch(e) { + cons.Warn(e.message); + if(fs.existsSync('package.json')) { + cons.Alert("package.json file found - trying 'npm start'"); + return procs("web: npm start"); + } else { + cons.Error("No Procfile and no package.json file found in Current Directory - See " + path.basename(process.argv[1]) + " --help"); + return; + } + } + +} + +module.exports.loadProc = loadProc; +module.exports.procs = procs; diff --git a/src/lib/local/foreman/proxy.cjs b/src/lib/local/foreman/proxy.cjs new file mode 100644 index 0000000000..f372fec633 --- /dev/null +++ b/src/lib/local/foreman/proxy.cjs @@ -0,0 +1,118 @@ +// Copyright IBM Corp. 2012,2016. All Rights Reserved. +// Node module: foreman +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +var fs = require('fs'); +var path = require('path'); +var prog = require('child_process'); +var util = require('util'); + +var cons = require('./console.cjs').Console; + +function f(key, j, ports, proc, reqs, portargs, localhost, emitter, ssl) { + var port = parseInt(ports[j]); + var ssl_port = (port === 80 ? 443 : (port + 443)); + + if(port > 0 && port < 1024 && process.getuid() !== 0) { + return cons.Error('Cannot Bind to Privileged Port %s Without Permission - Try \'sudo\'',port); + } + + if(isNaN(port)) { + return cons.Warn('No Downstream Port Defined for \'%s\' Proxy', key); + } + + if(!(key in proc)) { + return cons.Warn('Proxy Not Started for Undefined Key \'%s\'', key); + } + + var upstream_size = reqs[key]; + var upstream_port = parseInt(portargs) + j * 100; + + var proxy = prog.fork(require.resolve('../proxy'), [], { + env: { + HOST: localhost, + PORT: port, + UPSTREAM_HOST: localhost, + UPSTREAM_PORT: upstream_port, + UPSTREAM_SIZE: upstream_size, + SSL_CERT: ssl.cert, + SSL_KEY: ssl.key, + SSL_PORT: port ? ssl_port : 0 + } + }); + + var port_targets; + + if(upstream_size === 1) { + port_targets = util.format('%d', upstream_port); + } else { + port_targets = util.format('(%d-%d)', upstream_port, upstream_port + upstream_size - 1); + } + + cons.Alert('Starting Proxy Server [%s] %s -> %s', key, port, port_targets); + if (ssl.cert && ssl.key) { + cons.Alert('Starting Secure Proxy Server [%s] %s -> %s', key, ssl_port, port_targets); + } + + proxy.on('message', function(msg) { + if ('http' in msg) { + emitter.emit('http', msg.http); + } + if ('https' in msg) { + emitter.emit('https', msg.https); + } + }); + + emitter.once('killall', function(signal) { + cons.Done('Killing Proxy Server on Port %s', port); + proxy.kill(signal); + }); + + proxy.on('exit', function(code, signal) { + emitter.emit('killall', signal); + }); + +} + +function startProxies(reqs, proc, command, emitter, portargs) { + + if ('proxy' in command) { + + var localhost = 'localhost'; + + var ports = command.proxy.split(','); + + var ssl = { + cert: '', + key: '' + }; + if ((command.sslKey && !command.sslCert) || + (command.sslCert && !command.sslKey)) { + cons.Warn('SSL key and cert must both be supplied for SSL support'); + } + if (command.sslKey && command.sslCert) { + command.sslKey = path.resolve(command.sslKey); + command.sslCert = path.resolve(command.sslCert); + if (!fs.existsSync(command.sslKey)) { + cons.Warn('SSL key (%s) does not exist', command.sslKey); + } + else { + ssl.key = command.sslKey; + } + if (!fs.existsSync(command.sslCert)) { + cons.Warn('SSL cert (%s) does not exist', command.sslCert); + } + else { + ssl.cert = command.sslCert; + } + } + + Object.keys(reqs).forEach(function(key, i) { + f(key, i, ports, proc, reqs, portargs, localhost, emitter, ssl); + }); + } + +} + +module.exports.startProxies = startProxies; diff --git a/src/lib/local/foreman/requirements.cjs b/src/lib/local/foreman/requirements.cjs new file mode 100644 index 0000000000..7a52d93eb9 --- /dev/null +++ b/src/lib/local/foreman/requirements.cjs @@ -0,0 +1,49 @@ +// Copyright IBM Corp. 2012,2016. All Rights Reserved. +// Node module: foreman +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +function parseRequirements(req) { + var requirements = {}; + req.toString().split(',').forEach(function(item) { + var tup = item.trim().split('='); + var key = tup[0]; + var val; + if(tup.length > 1) { + val = parseInt(tup[1]); + } else { + val = 1; + } + + requirements[key] = val; + }); + return requirements; +} + +function getreqs(args, proc) { + var req; + if(args && args.length > 0) { + req = parseRequirements(args); + } else { + req = {}; + for(var key in proc){ + req[key] = 1; + } + } + return req; +} + +function calculatePadding(reqs) { + var padding = 0; + for(var key in reqs){ + var num = reqs[key]; + var len = key.length + num.toString().length; + if(len > padding) { + padding = len; + } + } + return padding + 12; +} + +module.exports.calculatePadding = calculatePadding; +module.exports.getreqs = getreqs; From c7182196c44593e87d054f9145c50cf72d680daf Mon Sep 17 00:00:00 2001 From: Richard Schneeman Date: Wed, 3 Jun 2026 10:38:08 -0500 Subject: [PATCH 2/7] refactor: rewire imports to vendored foreman and remove npm dependency Update run-foreman.cjs to import from ./foreman/*.cjs instead of foreman/lib/*. Hardcode the foreman version (3.0.1) instead of reading it from the package. Remove the foreman npm dependency from package.json and update the tsconfig.json comment to reflect the vendored state. Add eslint and cspell ignore patterns for the vendored foreman directory. Refs #3736 --- cspell.json | 3 ++- eslint.config.js | 2 +- package.json | 2 +- src/lib/local/run-foreman.cjs | 30 +++++++++++++++--------------- tsconfig.json | 2 +- 5 files changed, 20 insertions(+), 19 deletions(-) diff --git a/cspell.json b/cspell.json index bdd7705a41..69c0ea4721 100644 --- a/cspell.json +++ b/cspell.json @@ -14,6 +14,7 @@ "**/package-lock.json", "**/node_modules/**", "**/coverage/**", - "**/fixtures/**" + "**/fixtures/**", + "src/lib/local/foreman/**" ] } diff --git a/eslint.config.js b/eslint.config.js index 27173bfa9a..459500c36a 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -34,6 +34,6 @@ export default [ }, // Ignore patterns (in addition to shared ignores) { - ignores: ['**/test/**/*.js', '**/*.d.ts', '.github/**'], + ignores: ['**/test/**/*.js', '**/*.d.ts', '.github/**', 'src/lib/local/foreman/**'], }, ] diff --git a/package.json b/package.json index 865738eb58..5075dc76b5 100644 --- a/package.json +++ b/package.json @@ -47,7 +47,7 @@ "eventsource": "^4", "execa": "^9.6.1", "filesize": "^10.1", - "foreman": "^3.0.1", + "fs-extra": "^11.3.0", "glob": "^13.0.2", "got": "^13.0.0", diff --git a/src/lib/local/run-foreman.cjs b/src/lib/local/run-foreman.cjs index a46ba1d7b4..24ca220262 100644 --- a/src/lib/local/run-foreman.cjs +++ b/src/lib/local/run-foreman.cjs @@ -3,9 +3,9 @@ // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT -// This file is copied from the Node Foreman package. It was copied in order to fix -// a bug related to the use of port 5000 as a default. It is not meant to be a -// long-term solution, we plan to eventually remove our dependency on Node Foreman. +// Based on the Node Foreman package (v3.0.1). The foreman library is vendored +// in ./foreman/ and is no longer an external npm dependency. The default port +// was changed from 5000 to 5006 to avoid conflicts with macOS AirPlay. // These rules are disabled in order to prevent the need for refactoring /* eslint-disable guard-for-in */ @@ -16,24 +16,24 @@ /* eslint-disable unicorn/no-process-exit */ /* eslint-disable no-new */ /* eslint-disable radix */ +/* eslint-disable perfectionist/sort-imports */ const program = require('commander') -const colors = require('foreman/lib/colors') +const colors = require('./foreman/colors.cjs') const events = require('node:events') const fs = require('node:fs') const path = require('node:path') const {quote} = require('shell-quote') -const display = require('foreman/lib/console').Console -const _envs = require('foreman/lib/envs') -const exporters = require('foreman/lib/exporters') -const {startForward} = require('foreman/lib/forward') -const _proc = require('foreman/lib/proc') -const _procfile = require('foreman/lib/procfile') -const {startProxies} = require('foreman/lib/proxy') -const _requirements = require('foreman/lib/requirements') -const foremanPjson = require('foreman/package.json') - -program.version(foremanPjson.version) +const display = require('./foreman/console.cjs').Console +const _envs = require('./foreman/envs.cjs') +const exporters = require('./foreman/exporters.cjs') +const {startForward} = require('./foreman/forward.cjs') +const _proc = require('./foreman/proc.cjs') +const _procfile = require('./foreman/procfile.cjs') +const {startProxies} = require('./foreman/proxy.cjs') +const _requirements = require('./foreman/requirements.cjs') + +program.version('3.0.1') program.option('-j, --procfile ', 'load procfile FILE', 'Procfile') program.option('-e, --env ', 'load environment from FILE, a comma-separated list', '.env') program.option('-p, --port ', 'start indexing ports at number PORT', 0) diff --git a/tsconfig.json b/tsconfig.json index 75da7be3e1..b6fb8d6cf7 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,6 +1,6 @@ { "compilerOptions": { - "allowJs": true, // allowed to support run-foreman.js. Should be removed when that functionality is refactored. + "allowJs": true, // required to support run-foreman.cjs and vendored foreman .cjs files in src/lib/local/foreman/ "declaration": true, "esModuleInterop": true, "forceConsistentCasingInFileNames": true, From db2a5e58396d66569db05810d2ace7aed9b163b6 Mon Sep 17 00:00:00 2001 From: Richard Schneeman Date: Wed, 3 Jun 2026 10:38:24 -0500 Subject: [PATCH 3/7] fix: replace `util._extend` with `Object.assign` in vendored foreman The deprecated `util._extend` API in foreman's envs module triggers a Node.js DEP0060 deprecation warning when running `heroku local`. Replace it with `Object.assign` and remove the unused `util` require. Fixes #3736 --- src/lib/local/foreman/envs.cjs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/lib/local/foreman/envs.cjs b/src/lib/local/foreman/envs.cjs index 2593f84bb8..625e99e85d 100644 --- a/src/lib/local/foreman/envs.cjs +++ b/src/lib/local/foreman/envs.cjs @@ -4,7 +4,6 @@ // License text available at https://opensource.org/licenses/MIT var fs = require('fs'); -var util = require('util'); var cons = require('./console.cjs').Console; function method(name) { @@ -101,7 +100,9 @@ function loadEnvsFile(path) { } function loadEnvs(path) { - var envs = path.split(',').map(loadEnvsFile).reduce(util._extend, {}); + var envs = path.split(',').map(loadEnvsFile).reduce(function(acc, obj) { + return Object.assign(acc, obj); + }, {}); var sorted = Object.create(null); Object.keys(envs).sort().forEach(function(k) { sorted[k] = envs[k]; From 7dafedc7dc6ab6ea4175b540ff9a91a0f51c2616 Mon Sep 17 00:00:00 2001 From: Richard Schneeman Date: Wed, 3 Jun 2026 10:39:09 -0500 Subject: [PATCH 4/7] test: add unit tests for vendored foreman envs module Cover keyValue parsing (basic, quoted, comments, equals in values), flattenJSON behavior, and loadEnvs with single and comma-separated env files. The multi-file merge test exercises the Object.assign replacement for the former util._extend call. Refs #3736 --- test/unit/lib/local/foreman/envs.unit.test.ts | 130 ++++++++++++++++++ 1 file changed, 130 insertions(+) create mode 100644 test/unit/lib/local/foreman/envs.unit.test.ts diff --git a/test/unit/lib/local/foreman/envs.unit.test.ts b/test/unit/lib/local/foreman/envs.unit.test.ts new file mode 100644 index 0000000000..9b551e8c22 --- /dev/null +++ b/test/unit/lib/local/foreman/envs.unit.test.ts @@ -0,0 +1,130 @@ +import {expect} from 'chai' +import fs from 'node:fs' +import {createRequire} from 'node:module' +import os from 'node:os' +import path from 'node:path' + +const require = createRequire(import.meta.url) +const {flattenJSON, keyValue, loadEnvs} = require('../../../../../src/lib/local/foreman/envs.cjs') + +describe('vendored foreman envs', function () { + let tempDir: string + + beforeEach(function () { + tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'heroku-foreman-envs-')) + }) + + afterEach(function () { + fs.rmSync(tempDir, {force: true, recursive: true}) + }) + + describe('keyValue', function () { + it('parses basic key=value pairs', function () { + const result = keyValue('FOO=bar\nBAZ=qux\n') + expect(result).to.deep.equal({BAZ: 'qux', FOO: 'bar'}) + }) + + it('parses double-quoted values', function () { + const result = keyValue('FOO="hello world"\n') + expect(result).to.deep.equal({FOO: 'hello world'}) + }) + + it('parses single-quoted values', function () { + const result = keyValue("FOO='hello world'\n") + expect(result).to.deep.equal({FOO: 'hello world'}) + }) + + it('strips inline comments from unquoted values', function () { + const result = keyValue('FOO=bar # this is a comment\n') + expect(result).to.deep.equal({FOO: 'bar'}) + }) + + it('ignores comment lines', function () { + const result = keyValue('# a comment\nFOO=bar\n') + expect(result).to.deep.equal({FOO: 'bar'}) + }) + + it('ignores blank lines', function () { + const result = keyValue('\n\nFOO=bar\n\n') + expect(result).to.deep.equal({FOO: 'bar'}) + }) + + it('handles values containing equals signs', function () { + const result = keyValue('DATABASE_URL=postgres://user:pass@host/db?opt=val\n') + expect(result).to.deep.equal({DATABASE_URL: 'postgres://user:pass@host/db?opt=val'}) + }) + }) + + describe('flattenJSON', function () { + it('flattens a simple nested object', function () { + const result = flattenJSON({top: {middle: 'value'}}) + expect(result).to.deep.equal({TOP_MIDDLE: 'value'}) + }) + + it('flattens arrays by index', function () { + const result = flattenJSON({items: ['a', 'b', 'c']}) + expect(result).to.deep.equal({ITEMS_0: 'a', ITEMS_1: 'b', ITEMS_2: 'c'}) + }) + + it('flattens deeply nested structures', function () { + const result = flattenJSON({a: {b: {c: 42}}}) + expect(result).to.deep.equal({A_B_C: 42}) + }) + + it('flattens a flat object', function () { + const result = flattenJSON({KEY: 'val'}) + expect(result).to.deep.equal({KEY: 'val'}) + }) + }) + + describe('loadEnvs', function () { + it('loads a single env file', function () { + const envPath = path.join(tempDir, '.env') + fs.writeFileSync(envPath, 'FOO=bar\nBAZ=qux\n') + + const result = loadEnvs(envPath) + expect(result.FOO).to.equal('bar') + expect(result.BAZ).to.equal('qux') + expect(result.PATH).to.be.a('string') + }) + + it('merges multiple comma-separated env files', function () { + const env1 = path.join(tempDir, 'first.env') + const env2 = path.join(tempDir, 'second.env') + fs.writeFileSync(env1, 'FOO=from_first\nSHARED=first\n') + fs.writeFileSync(env2, 'BAR=from_second\nSHARED=second\n') + + const result = loadEnvs(env1 + ',' + env2) + expect(result.FOO).to.equal('from_first') + expect(result.BAR).to.equal('from_second') + expect(result.SHARED).to.equal('second') + }) + + it('returns sorted keys', function () { + const envPath = path.join(tempDir, '.env') + fs.writeFileSync(envPath, 'ZZZ=last\nAAA=first\nMMM=middle\n') + + const result = loadEnvs(envPath) + const keys = Object.keys(result) + const aIdx = keys.indexOf('AAA') + const mIdx = keys.indexOf('MMM') + const zIdx = keys.indexOf('ZZZ') + expect(aIdx).to.be.lessThan(mIdx) + expect(mIdx).to.be.lessThan(zIdx) + }) + + it('handles a missing env file gracefully', function () { + const missing = path.join(tempDir, 'nonexistent.env') + const result = loadEnvs(missing) + expect(result.PATH).to.be.a('string') + }) + + it('sets PATH from process.env when not in env file', function () { + const envPath = path.join(tempDir, '.env') + fs.writeFileSync(envPath, 'FOO=bar\n') + + const result = loadEnvs(envPath) + expect(result.PATH).to.equal(process.env.PATH) + }) + }) +}) From 064f2a333f0bf0ebc08cc54483b31edc8ec963d0 Mon Sep 17 00:00:00 2001 From: Richard Schneeman Date: Wed, 3 Jun 2026 10:39:26 -0500 Subject: [PATCH 5/7] docs: add CHANGELOG entries for foreman vendoring and deprecation fix Refs #3736 --- CHANGELOG.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8459d32ef6..e927ecfc30 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,18 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## Unreleased + + +### Bug Fixes + +* fix `util._extend` deprecation warning when running `heroku local` ([#3742](https://github.com/heroku/cli/pull/3742)) + + +### Code Refactoring + +* vendor node-foreman, used by `heroku local`, into the CLI as an internal dependency ([#3742](https://github.com/heroku/cli/pull/3742)) + ## [11.4.0](https://github.com/heroku/cli/compare/v11.3.0...v11.4.0) (2026-05-13) From 6ad250e677885487a198a53f0f84a56441dce304 Mon Sep 17 00:00:00 2001 From: Richard Schneeman Date: Thu, 4 Jun 2026 16:40:41 -0500 Subject: [PATCH 6/7] Remove vendored exporter The exporter.cjs depends on mustache, causing an error in CI: ``` node:internal/modules/cjs/loader:1210 throw err; ^ Error: Cannot find module 'mustache' Require stack: - /home/runner/work/cli/cli/dist/lib/local/foreman/exporters.cjs - /home/runner/work/cli/cli/dist/lib/local/run-foreman.cjs at Module._resolveFilename (node:internal/modules/cjs/loader:1207:15) at Module._load (node:internal/modules/cjs/loader:1038:27) at Module.require (node:internal/modules/cjs/loader:1289:19) at require (node:internal/modules/helpers:182:18) at Object. (/home/runner/work/cli/cli/dist/lib/local/foreman/exporters.cjs:11:10) at Module._compile (node:internal/modules/cjs/loader:1521:14) at Module._extensions..js (node:internal/modules/cjs/loader:1623:10) at Module.load (node:internal/modules/cjs/loader:1266:32) at Module._load (node:internal/modules/cjs/loader:1091:12) at Module.require (node:internal/modules/cjs/loader:1289:19) { code: 'MODULE_NOT_FOUND', requireStack: [ '/home/runner/work/cli/cli/dist/lib/local/foreman/exporters.cjs', '/home/runner/work/cli/cli/dist/lib/local/run-foreman.cjs' ] } ``` The `heroku local` command does not use `export` therefore it doesn't need this module, so we can remove it and the need to have mustache. --- package-lock.json | 77 -------------- src/lib/local/foreman/exporters.cjs | 159 ---------------------------- src/lib/local/run-foreman.cjs | 142 ------------------------- 3 files changed, 378 deletions(-) delete mode 100644 src/lib/local/foreman/exporters.cjs diff --git a/package-lock.json b/package-lock.json index 58817e70db..5ff8bab91d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -50,7 +50,6 @@ "eventsource": "^4", "execa": "^9.6.1", "filesize": "^10.1", - "foreman": "^3.0.1", "fs-extra": "^11.3.0", "glob": "^13.0.2", "got": "^13.0.0", @@ -13463,12 +13462,6 @@ "node": ">= 0.6" } }, - "node_modules/eventemitter3": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", - "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", - "license": "MIT" - }, "node_modules/events-universal": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz", @@ -13989,26 +13982,6 @@ "dev": true, "license": "ISC" }, - "node_modules/follow-redirects": { - "version": "1.16.0", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", - "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/RubenVerborgh" - } - ], - "license": "MIT", - "engines": { - "node": ">=4.0" - }, - "peerDependenciesMeta": { - "debug": { - "optional": true - } - } - }, "node_modules/for-each": { "version": "0.3.5", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", @@ -14042,24 +14015,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/foreman": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/foreman/-/foreman-3.0.1.tgz", - "integrity": "sha512-ek/qoM0vVKpxzkBUQN9k4Fs7l0XsHv4bqxuEW6oqIS4s0ouYKsQ19YjBzUJKTFRumFiSpUv7jySkrI6lfbhjlw==", - "license": "MIT", - "dependencies": { - "commander": "^2.15.1", - "http-proxy": "^1.17.0", - "mustache": "^2.2.1", - "shell-quote": "^1.6.1" - }, - "bin": { - "nf": "nf.js" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/form-data": { "version": "4.0.5", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", @@ -15010,20 +14965,6 @@ "url": "https://opencollective.com/express" } }, - "node_modules/http-proxy": { - "version": "1.18.1", - "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", - "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==", - "license": "MIT", - "dependencies": { - "eventemitter3": "^4.0.0", - "follow-redirects": "^1.0.0", - "requires-port": "^1.0.0" - }, - "engines": { - "node": ">=8.0.0" - } - }, "node_modules/http-proxy-agent": { "version": "7.0.2", "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", @@ -17485,18 +17426,6 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, - "node_modules/mustache": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/mustache/-/mustache-2.3.2.tgz", - "integrity": "sha512-KpMNwdQsYz3O/SBS1qJ/o3sqUJ5wSb8gb0pul8CO0S56b9Y2ALm8zCfsjPXsqGFfoNBkDwZuZIAjhsZI03gYVQ==", - "license": "MIT", - "bin": { - "mustache": "bin/mustache" - }, - "engines": { - "npm": ">=1.4.0" - } - }, "node_modules/mute-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-2.0.0.tgz", @@ -21423,12 +21352,6 @@ "node": ">=8.6.0" } }, - "node_modules/requires-port": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", - "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", - "license": "MIT" - }, "node_modules/resolve": { "version": "1.22.11", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", diff --git a/src/lib/local/foreman/exporters.cjs b/src/lib/local/foreman/exporters.cjs deleted file mode 100644 index bf93d8347a..0000000000 --- a/src/lib/local/foreman/exporters.cjs +++ /dev/null @@ -1,159 +0,0 @@ -// Copyright IBM Corp. 2014,2016. All Rights Reserved. -// Node module: foreman -// This file is licensed under the MIT License. -// License text available at https://opensource.org/licenses/MIT - -// NOTE: This module depends on 'mustache' which is not a CLI dependency. -// The `nf export` command is not used by any Heroku CLI command, so this -// dependency is never exercised at runtime. - -var colors = require('./colors.cjs'); -var ppath = require('path'); -var mu = require('mustache'); -var fs = require('fs'); - -var display = require('./console.cjs').Console; - -function render(filename, conf, callback) { - fs.readFile(filename, {encoding: 'utf8'}, function(err, template) { - if (err) { - throw err; - } - callback(mu.render(template, conf)); - }); -} - -function templatePath(conf, type, file) { - if(conf.template) { - return ppath.resolve(conf.template, file); - } else { - return ppath.resolve(__dirname, type, file); - } -} - -function writeout(path) { - return function(data) { - if (fs.existsSync(path)) { - display.Warn(colors.bright_yellow('Replacing: %s'), path); - } - - fs.writeFileSync(path,data); - display.Alert('Wrote :',ppath.normalize(path)); - }; -} - -function upstart(conf, outdir) { - var path = outdir + "/" + conf.application + ".conf"; - render(templatePath(conf, 'upstart', 'foreman.conf'), conf, writeout(path)); -} - -function upstart_app(conf, outdir) { - var path = outdir + "/" + conf.application + "-" + conf.process + ".conf"; - render(templatePath(conf, 'upstart', 'foreman-APP.conf'), conf, writeout(path)); -} - -function upstart_app_n(conf, outdir) { - var path = outdir + "/" + conf.application + "-" + conf.process + "-" + conf.number + ".conf"; - render(templatePath(conf, 'upstart','foreman-APP-N.conf'), conf, writeout(path)); -} - -function upstart_single(conf, outdir) { - var path = outdir + "/" + conf.application + ".conf"; - render(templatePath(conf, 'upstart-single', 'foreman.conf'), conf, writeout(path)); - display.Warn('upstart-single jobs attempt to raise limits and will fail ' + - 'to start if the limits cannot be raised to the desired ' + - 'levels. Some manual editing may be required.'); -} - -function upstart_single_app(conf, outdir) { - var path = outdir + "/" + conf.application + "-" + conf.process + ".conf"; - render(templatePath(conf, 'upstart-single', 'foreman-APP.conf'), conf, writeout(path)); -} - -function systemd(conf, outdir){ - var path = outdir + "/" + conf.application + ".target"; - render(templatePath(conf, 'systemd', 'foreman.target'), conf, writeout(path)); -} - -function systemd_app(conf, outdir) { - var path = outdir + "/" + conf.application + "-" + conf.process + ".target"; - render(templatePath(conf, 'systemd', 'foreman-APP.target'), conf, writeout(path)); -} - -function systemd_app_n(conf, outdir) { - var path = outdir + "/" + conf.application + "-" + conf.process + "-" + conf.number + ".service"; - render(templatePath(conf, 'systemd', 'foreman-APP-N.service'), conf, writeout(path)); -} - -function supervisord(conf, outdir) { - var path = outdir + "/" + conf.application + ".conf"; - var programs = []; - - for(var i = 0; i < conf.processes.length; i++) { - var process = conf.processes[i].process; - var n = conf.processes[i].n; - - for(var j = 1; j <= n; j++) { - programs.push(conf.application + "-" + process + "-" + j); - } - } - - conf.programs = programs.join(','); - - render(templatePath(conf, 'supervisord', 'foreman.conf'), conf, writeout(path)); -} - -function supervisord_app_n(conf, outdir) { - var path = outdir + "/" + conf.application + "-" + conf.process + "-" + conf.number + ".conf"; - var envs = []; - - for(var i in conf.envs) { - var key = conf.envs[i].key; - var value = conf.envs[i].value; - - if(typeof value === 'string') { - value = value.replace(/"/, '\\"'); - } - - envs.push(key + "=" + '"' + value + '"'); - } - - conf.envs = envs.join(','); - - render(templatePath(conf, 'supervisord', 'foreman-APP-N.conf'), conf, writeout(path)); -} - -function smf_app(conf, outdir) { - var path = outdir + "/" + conf.application + "-" + conf.process + ".xml"; - render(templatePath(conf, 'smf', 'foreman-APP.xml'), conf, writeout(path)); -} - -var export_formats = { - "upstart": { - foreman : upstart, - foreman_app : upstart_app, - foreman_app_n : upstart_app_n, - }, - "upstart-single": { - foreman : upstart_single, - foreman_app : upstart_single_app, - foreman_app_n : function noop() {}, - }, - "systemd": { - foreman : systemd, - foreman_app : systemd_app, - foreman_app_n : systemd_app_n, - }, - "supervisord": { - foreman : supervisord, - foreman_app : function noop() {}, - foreman_app_n : supervisord_app_n, - }, - "smf": { - foreman : function noop() {}, - foreman_app : smf_app, - foreman_app_n : function noop() {}, - } -}; - -module.exports = export_formats; diff --git a/src/lib/local/run-foreman.cjs b/src/lib/local/run-foreman.cjs index 24ca220262..21f5131ddd 100644 --- a/src/lib/local/run-foreman.cjs +++ b/src/lib/local/run-foreman.cjs @@ -21,12 +21,9 @@ const program = require('commander') const colors = require('./foreman/colors.cjs') const events = require('node:events') -const fs = require('node:fs') -const path = require('node:path') const {quote} = require('shell-quote') const display = require('./foreman/console.cjs').Console const _envs = require('./foreman/envs.cjs') -const exporters = require('./foreman/exporters.cjs') const {startForward} = require('./foreman/forward.cjs') const _proc = require('./foreman/proc.cjs') const _procfile = require('./foreman/procfile.cjs') @@ -146,145 +143,6 @@ program once(input, envs, callback) }) -program - .command('export [PROCS]') - .option('-a, --app ', 'export upstart application as NAME', 'foreman') - .option('-u, --user ', 'export upstart user as NAME', 'root') - .option('-o, --out ', 'export upstart files to DIR', '.') - .option('-c, --cwd ', 'change current working directory to DIR') - .option('-g, --gid ', 'set gid of upstart config to GID') - .option('-l, --log ', 'specify upstart log directory', '/var/log') - .option('-t, --type ', 'export file to TYPE (default upstart)', 'upstart') - .option('-m, --template ', 'use template folder') - .description('Export to an upstart job independent of foreman') - .action(function (procArgs) { - const envs = loadEnvs(program.env) - - const procs = loadProc(program.procfile) - - if (!procs) { - return - } - - const req = getreqs(procArgs, procs) - - // Variables for Upstart Template - const config = { - application: this.app, - cwd: path.resolve(process.cwd(), this.cwd || ''), - envs, - group: this.gid || this.user, - logs: this.log, - template: this.template, - user: this.user, - } - - config.envfile = path.resolve(program.env) - - let writeout - if (exporters[this.type]) { - writeout = exporters[this.type] - } else { - new display.Error('Unknown Export Format', this.type) - process.exit(1) - } - - // Check for Upstart User - // friendly warning - does not stop export - let userExists = false - for (const line of fs.readFileSync('/etc/passwd') - .toString().split(/\n/)) { - if (line.match(/^[^:]*/)[0] === config.user) { - userExists = true - } - } - - if (!userExists) { - display.Warn(display.fmt('User %s Does Not Exist on System', config.user)) - } - - // using port 5006 because it is not known to be used by other common software - const baseport = Number.parseInt(program.port || envs.PORT || process.env.PORT || 5006) - let baseport_i = 0 - let baseport_j = 0 - let envl = [] - - config.processes = [] - - // This is ugly because of shitty support for array copying - // Cleanup is definitely required - for (let key in req) { - const c = {} - const cmd = procs[key] - - if (!cmd) { - display.Warn("Required Key '%s' Does Not Exist in Procfile Definition", key) - continue - } - - const n = req[key] - - config.processes.push({n, process: key}) - c.process = key - c.command = cmd - - for (const _ in config) { - c[_] = config[_] - } - - c.numbers = [] - for (let i = 1; i <= n; i++) { - const port = (baseport + baseport_i + baseport_j) * 100 - - const envl = [] - for (key in envs) { - envl.push({ - key, - value: envs[key], - }) - } - - envl.push({key: 'PORT', value: conf.port}) - // eslint-disable-next-line unicorn/no-array-push-push - envl.push({key: 'FOREMAN_WORKER_NAME', value: conf.process + '.' + conf.number}) - - conf.envs = envl - - const conf = { - ...c, - envs: envl, - number: i, - port, - } - - // Write the APP-PROCESS-N.conf File - writeout.foreman_app_n(conf, this.out) - - baseport_i++ - c.numbers.push({number: i}) - } - - envl = [] - for (key in envs) { - envl.push({ - key, - value: envs[key], - }) - } - - c.envs = envl - - // Write the APP-Process.conf File - writeout.foreman_app(c, this.out) - - baseport_i = 0 - baseport_j++ - } - - // Write the APP.conf File - writeout.foreman(config, this.out) - }) - program.parse(process.argv) if (process.argv.slice(2).length === 0) { From 65be92e8e461db4b4cf97315eb47d297e044ecba Mon Sep 17 00:00:00 2001 From: Richard Schneeman Date: Thu, 4 Jun 2026 16:55:11 -0500 Subject: [PATCH 7/7] fix: remove unused eslint-disable directives for `no-new` and `radix` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit These directives were left over from the vendored node-foreman code but the rules they suppress are never triggered, causing eslint to report unused-directive errors and fail CI. This was causing errors in CI ``` npx eslint src/lib/local/run-foreman.cjs 2>&1 /Users/rschneeman/Documents/projects/work/tmp/cli/src/lib/local/run-foreman.cjs 17:1 error Unused eslint-disable directive (no problems were reported from 'no-new') 18:1 error Unused eslint-disable directive (no problems were reported from 'radix') ✖ 2 problems (2 errors, 0 warnings) 2 errors and 0 warnings potentially fixable with the `--fix` option. ``` --- src/lib/local/run-foreman.cjs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/lib/local/run-foreman.cjs b/src/lib/local/run-foreman.cjs index 21f5131ddd..704dd2b613 100644 --- a/src/lib/local/run-foreman.cjs +++ b/src/lib/local/run-foreman.cjs @@ -14,8 +14,6 @@ /* eslint-disable no-undef */ /* eslint-disable n/no-process-exit */ /* eslint-disable unicorn/no-process-exit */ -/* eslint-disable no-new */ -/* eslint-disable radix */ /* eslint-disable perfectionist/sort-imports */ const program = require('commander')