diff --git a/exec/install-xtrn.js b/exec/install-xtrn.js
new file mode 100644
index 0000000000000000000000000000000000000000..af3efc89d1ce31ff8c3a69c4305dc080539768e3
--- /dev/null
+++ b/exec/install-xtrn.js
@@ -0,0 +1,213 @@
+// $Id$
+
+// Installer for Synchronet External Programs
+
+// This script parses a .ini file (default filename is install-xtrn.ini)
+// and installs the external programs defined within into the Synchronet BBS
+// configuration file: ctrl/xtrn.cnf.  The programs defined within this file
+// can by online programs (a.k.a. Doors), editors, or events.
+//
+// This script is intended for use by sysops using JSexec, for example:
+//   $ jsexec install-xtrn ../xtrn/minesweeper
+//
+// This script can aso be invoked using the ;EXEC sysop command while logged
+// into the terminal server, for example:
+//   ;exec ?install-xtrn ../xtrn/minesweeper
+//
+// The .ini sections and keys supported (zero or more of each may be included):
+//
+// [prog:<code>]
+// 		name 			= Program name or description (40 chars max)
+//		cmd 			= command-line to execute (63 chars max)
+//		clean_cmd 		= clean-up command-line, if needed (63 chars max)
+//		settings 		= bit-flags (see XTRN_* in sbbsdefs.js)
+//		ars				= access requirements string (40 chars max)
+//		execution_ars	= execution requirements string (40 chars max)
+//		type			= drop-file type (see XTRN_* in sbbsdefs.js)
+//		event			= event-type (see EVENT_* in sbbsdefs.j)
+//		cost			= cost to run, in credits
+//		startup_dir		= directory to make current before execution
+//		textra			= extra time (minutes) to allow to run this program
+//		max_time		= maximum time (minutes) allowed to run this program
+//
+// [event:<code>]
+//		cmd 			= command-line to execute (63 chars max)
+//		days			= bit-field representing days of the week to execute
+//		time			= time of day to run this event
+//		node_num		= node number to run this event
+//		settings		= bit-flags (see XTRN_* in sbbsdefs.js)
+//		startup_dir		= directory to make current before execution
+//		freq			= frequency of execution
+//		mdays			= days of month (if non-zero) for execution
+//		months			= bit-field representing which months to execute
+//
+// [editor:<code>]
+//		name 			= Editr name or description (40 chars max)
+//		cmd 			= command-line to execute (63 chars max)
+//		type			= drop-file type (see XTRN_* in sbbsdefs.js)
+//		settings 		= bit-flags (see XTRN_* in sbbsdefs.js)
+//		ars				= access requirements string (40 chars max)
+
+// Additionally, each object can have the following optional keys that are only
+// used by this script
+//		note			= Note to sysop displayed before installation
+//		required		= If true, this item must be installed to continue
+//
+// Notes:
+//
+// - The startup_dir will default to the location of the .ini file if this
+//   key is not defined within the .ini file.
+//
+// - The only required values are the <code> (internal code, 8 chars max) and
+//   cmd; all other keys will have functional default values if not defined in
+//   the .ini file.
+
+"use strict";
+
+load("sbbsdefs.js");
+const ini_fname = "install-xtrn.ini"
+var debug = false;
+
+function install_xtrn_item(cnf, type, desc, item)
+{
+	if (!item.code)
+		return false;
+
+	if (!item.name)
+		item.name = item.code;
+	
+	if (item.note)
+		print(item.note);
+	
+	if (!confirm("Install " + desc + ": " + item.name)) {
+		if (item.required == true)
+			return "Installation of " + item.name + " is required to continue";
+		return false;
+	}
+	
+	if(type == "xtrn") {
+		if (!xtrn_area.sec_list.length)
+			return "No external program sections have been created";
+		
+		for (var i = 0; i < xtrn_area.sec_list.length; i++)
+			print(format("%2u: ", i + 1) + xtrn_area.sec_list[i].name);
+
+		var which;
+		while (!which || which > xtrn_area.sec_list.length)
+			which = prompt("Install " + item.name  + " into which External Program Section");
+		which = parseInt(which, 10);
+		if (!which)
+			return false;
+		
+		item.sec = xtrn_area.sec_list[which - 1].number;
+	}
+	
+	function find_code(objs, code)
+	{
+		for(var i=0; i < objs.length; i++)
+			if(objs[i].code.toLowerCase() == code.toLowerCase())
+				return i;
+		return -1;
+	}
+	
+	while (!item.code 
+		|| (find_code(cnf[type], item.code) >= 0
+			&& print(desc + " Internal Code (" + item.code + ") already exists!")))
+		item.code = prompt(desc + " Internal code");
+
+	try {
+		item.code = item.code.toUpperCase();
+		if (item.settings)
+			item.settings = eval(item.settings);
+		if (item.event)
+			item.event = eval(item.event);
+		if (item.type)
+			item.type = eval(item.type);
+	} catch(e) {
+		return e;
+	}
+	cnf[type].push(item);
+	if(debug)
+		print(JSON.stringify(cnf[type], null, 4));
+	
+	print(desc + " (" + item.name + ") installed successfully");
+	return true;
+}
+
+function main(ini_fname)
+{
+	var installed = 0;
+	var banner = "* Installing " + ini_fname + " use Ctrl-C to abort *";
+	var line = "";
+	for (var i = 0; i < banner.length; i++)
+		line += "*";
+	print(line);
+	print(banner);
+	print(line);
+	
+	var ini_file = new File(ini_fname);
+	if (!ini_file.open("r"))
+		return ini_file.name + " open error " + ini_file.error;
+
+	var cnflib = load({}, "cnflib.js");
+	var xtrn_cnf = cnflib.read("xtrn.cnf");
+	if (!xtrn_cnf)
+		return "Failed to read xtrn.cnf";
+	
+	var startup_dir = ini_fname.substr(0, Math.max(ini_fname.lastIndexOf("/"), ini_fname.lastIndexOf("\\"), 0));
+
+	const types = {
+		prog:	{ desc: "External Program", 	struct: "xtrn" },
+		event:	{ desc: "External Timed Event", struct: "event" },
+		editor:	{ desc: "External Editor",		struct: "xedit" }
+	};
+	
+	for(var t in types) {
+		var list = ini_file.iniGetAllObjects("code", t + ":");
+		for (var i = 0; i < list.length; i++) {
+			if (list[i].startup_dir === undefined)
+				list[i].startup_dir = startup_dir;
+			var result = install_xtrn_item(xtrn_cnf, types[t].struct, types[t].desc, list[i]);
+			if (typeof result !== 'boolean')
+				return result;
+			if (result === true)
+				installed++;
+		}
+	}
+	
+	if (installed) {
+		if (!debug && !cnflib.write("xtrn.cnf", undefined, xtrn_cnf))
+			return "Failed to write xtrn.cnf";
+		print("Installed " + installed + " items from " + ini_fname + " successfully");
+	}
+
+	return installed >= 1; // success
+}
+
+// Command-line argument parsing
+var ini_path;
+for (var i = 0; i < argc; i++) {
+	switch (argv[i]) {
+		case "-debug":
+			debug = true;
+			break;
+		default:
+			ini_path = argv[i];
+			break;
+	}
+}
+// Locate the .ini file
+if (file_isdir(ini_path))
+	ini_path = backslash(ini_path) + ini_fname;
+while (!ini_path || !file_exists(ini_path)) {
+	ini_path = prompt("Location of " + ini_fname);
+	if (file_isdir(ini_path))
+		ini_path = backslash(ini_path) + ini_fname;
+}
+		
+var result = main(ini_path);
+if (result !== true) {
+	if (typeof result !== 'boolean')
+		alert(result);
+	exit(1);
+}