--- /dev/null
+#
+# Copyright (C) 2025 OpenWrt.org
+#
+# This is free software, licensed under the GNU General Public License v2.
+# See /LICENSE for more information.
+#
+
+include $(TOPDIR)/rules.mk
+include $(INCLUDE_DIR)/kernel.mk
+
+PKG_NAME:=cli
+PKG_VERSION:=1
+
+PKG_LICENSE:=GPL-2.0
+PKG_MAINTAINER:=Felix Fietkau <nbd@nbd.name>
+
+PKG_BUILD_DEPENDS:=bpf-headers
+
+include $(INCLUDE_DIR)/package.mk
+
+define Package/cli
+ SECTION:=utils
+ CATEGORY:=Utilities
+ TITLE:=OpenWrt CLI
+ DEPENDS:=+ucode +ucode-mod-ubus +ucode-mod-uloop
+endef
+
+define Build/Compile
+ :
+endef
+
+define Package/cli/install
+ $(CP) ./files/* $(1)/
+endef
+
+$(eval $(call BuildPackage,cli))
--- /dev/null
+#!/usr/bin/env ucode
+'use strict';
+import * as datamodel from "cli.datamodel";
+import { bold, color_fg } from "cli.color";
+import * as uline from "uline";
+
+let history = [];
+let history_edit;
+let history_idx = -1;
+let cur_line;
+
+let el;
+let model = datamodel.new();
+let uloop = model.uloop;
+model.add_modules();
+let ctx = model.context();
+let parser = uline.arg_parser({
+ line_separator: ";"
+});
+
+model.add_types({
+ Root: {
+ exit: {
+ help: "Exit the CLI",
+ call: function(ctx) {
+ el.close();
+ uloop.end();
+ }
+ }
+ }
+});
+
+
+function update_prompt() {
+ el.set_state({
+ prompt: bold(join(" ", [ "cli", ...ctx.prompt ]) + "> "),
+ });
+}
+
+let cur_completion, tab_prefix, tab_suffix, tab_prefix_len, tab_quote, tab_ctx;
+
+function max_len(list, len)
+{
+ for (let entry in list)
+ if (length(entry) > len)
+ len = length(entry);
+ return len + 3;
+}
+
+function helptext(cur) {
+ if (!cur) {
+ el.set_hint(`\n No help information available\n`);
+ return true;
+ }
+
+ let data = cur.data;
+ switch (cur.type) {
+ case "field":
+ let str = `\n Input field: ${data[1]}`;
+ if (type(data[2]) != "array") {
+ str += "\n";
+ } else if (length(data[2]) > 0) {
+ str += ", values:\n";
+ let len = max_len(map(data[2], (v) => v[0]), 10);
+ for (let val in data[2]) {
+ str += sprintf(" - %-" + len + "s", val[0]);
+ if (val[1])
+ str += ": " + val[1];
+ str += "\n";
+ }
+ } else {
+ str += " (no match)\n";
+ }
+ el.set_hint(str);
+ return true;
+ default:
+ break;
+ }
+
+ let str = `\n Available ${cur.type}:\n\n`;
+ let len = max_len(map(data, (e) => e[0]), 20);
+ for (let entry in data)
+ str += sprintf(" %-" + len + "s %s\n", entry[0], entry[1]);
+
+ el.set_hint(str);
+ return true;
+}
+
+function completion_ctx(arg_info)
+{
+ let cur_ctx = ctx;
+ for (let args in arg_info.args) {
+ let sel = cur_ctx.select(args);
+ if (!length(args))
+ cur_ctx = sel;
+ if (!sel)
+ return;
+ }
+
+ return cur_ctx;
+}
+
+function completion_check_prefix(data)
+{
+ let prefix = data[0][0];
+ let prefix_len = length(prefix);
+
+ for (let entry in data) {
+ entry = entry[0];
+ if (prefix_len > length(entry))
+ prefix_len = length(entry);
+ }
+ prefix = substr(prefix, 0, prefix_len);
+
+ for (let entry in data) {
+ entry = substr(entry[0], 0, prefix_len);
+ while (entry != prefix) {
+ prefix_len--;
+ prefix = substr(prefix, 0, prefix_len);
+ entry = substr(entry, 0, prefix_len);
+ }
+ }
+
+ tab_prefix += substr(prefix, tab_prefix_len);
+ tab_prefix_len = prefix_len;
+
+ let line = tab_prefix + tab_quote;
+ let pos = length(line);
+ line += tab_suffix;
+ el.set_state({ line, pos });
+}
+
+function completion(count) {
+ if (count < 2) {
+ let line_data = el.get_line();
+ let line = line_data.line;
+ let pos = line_data.pos;
+ tab_suffix = substr(line, pos);
+ if (length(tab_suffix) > 0 &&
+ substr(tab_suffix, 0, 1) != " ") {
+ let idx = index(tab_suffix, " ");
+ if (idx < 0 || !idx)
+ pos += length(tab_suffix);
+ else
+ pos += idx;
+
+ tab_suffix = substr(line, pos);
+ }
+ tab_prefix = substr(line, 0, pos);
+
+ let arg_info = parser.parse(tab_prefix);
+ let is_open = arg_info.missing != null;
+ if (arg_info.missing == "\\\"")
+ tab_quote = "\"";
+ else
+ tab_quote = arg_info.missing ?? "";
+ let args = pop(arg_info.args);
+
+ if (!is_open && substr(tab_prefix, -1) == " ")
+ push(args, "");
+ let last = args[length(args) - 1];
+ tab_prefix_len = length(last);
+ tab_ctx = completion_ctx(arg_info);
+ if (!tab_ctx)
+ return;
+
+ cur_completion = tab_ctx.complete([...args]);
+ }
+
+ if (!tab_ctx)
+ return;
+
+ if (count < 0)
+ return helptext(cur_completion);
+
+ let cur = cur_completion;
+ if (cur && cur.type == "field" && !cur.data[2])
+ cur = null;
+ if (!cur) {
+ el.set_hint("");
+ return;
+ }
+
+ let data = cur.data;
+ if (cur.type == "field")
+ data = data[2];
+
+ if (length(data) == 0) {
+ el.set_hint(` (no match)`);
+ return;
+ }
+
+ if (length(data) == 1) {
+ let line = tab_prefix + substr(data[0][0], tab_prefix_len) + tab_quote + " ";
+ let pos = length(line);
+ line += tab_suffix;
+ el.set_state({ line, pos });
+ el.set_hint("");
+ el.reset_key_input();
+ return;
+ }
+
+ if (count == 1)
+ completion_check_prefix(data);
+
+ if (count > 1) {
+ let idx = (count - 2) % length(data);
+ let line = tab_prefix + substr(data[idx][0], tab_prefix_len) + tab_quote;
+ let pos = length(line);
+ line += tab_suffix;
+ el.set_state({ line, pos });
+ }
+
+ let win = el.get_window();
+ let str = "";
+ let x = 0;
+
+ let len = max_len(map(data, (v) => v[0]));
+ for (let entry in data) {
+ let add = sprintf(" %-"+len+"s", entry[0]);
+
+ str += add;
+ x += length(add);
+
+ if (x + length(add) < win.x)
+ continue;
+
+ str += "\n";
+ x = 0;
+ }
+ el.set_hint(str);
+}
+
+function format_result(res)
+{
+ if (!res) {
+ warn(color_fg("red", "Unknown command") + "\n");
+ return;
+ }
+ if (!res.ok) {
+ for (let err in res.errors) {
+ warn(color_fg("red", "Error: "+ err.msg) + "\n");
+ }
+ if (!res.errors)
+ warn(color_fg("red", "Failed") + "\n");
+ return;
+ }
+
+ if (res.status_msg)
+ warn(color_fg("green", res.status_msg) + "\n");
+
+ if (res.name)
+ warn(res.name + ": ");
+
+ switch (res.type) {
+ case "table":
+ let len = max_len(res.data, 8);
+ warn("\n");
+ for (let name, val in res.data) {
+ if (type(val) == "bool")
+ val = val ? "yes" : "no";
+ val = replace(val, /\n/g, "\n ");
+ warn(sprintf(" %-" + len + "s %s\n", name + ":", val));
+ }
+ break;
+ case "list":
+ warn("\n");
+ for (let entry in res.data)
+ warn(" - " + entry + "\n");
+ break;
+ case "string":
+ warn(res.data + "\n");
+ break;
+ }
+}
+
+function line_history_reset()
+{
+ history_idx = -1;
+ history_edit = null;
+ cur_line = null;
+}
+
+function line_history(dir)
+{
+ let min_idx = cur_line == null ? 0 : -1;
+ let new_idx = history_idx + dir;
+
+ if (new_idx < min_idx || new_idx >= length(history))
+ return;
+
+ let line = el.get_line().line;
+ let cur_history = history_edit ?? history;
+ if (history_idx == -1)
+ cur_line = line;
+ else if (cur_history[history_idx] != line) {
+ history_edit ??= [ ...history ];
+ history_edit[history_idx] = line;
+ cur_history = history_edit;
+ }
+
+ history_idx = new_idx;
+ if (history_idx < 0)
+ line = cur_line;
+ else
+ line = cur_history[history_idx];
+ let pos = length(line);
+ el.set_state({ line, pos });
+
+}
+let rev_search, rev_search_results, rev_search_index;
+
+function reverse_search_update(line)
+{
+ if (line) {
+ rev_search = line;
+ rev_search_results = filter(history, (l) => index(l, line) >= 0);
+ rev_search_index = 0;
+ }
+
+ let prompt = "reverse-search: ";
+ if (line && !length(rev_search_results))
+ prompt = "failing " + prompt;
+
+ el.set_state({
+ line2_prompt: prompt,
+ });
+
+ if (line && length(rev_search_results)) {
+ line = rev_search_results[0];
+ let pos = length(line);
+ el.set_state({ line, pos });
+ }
+}
+
+function reverse_search_reset() {
+ if (rev_search == null)
+ return;
+ rev_search = null;
+ rev_search_results = null;
+ rev_search_index = 0;
+ el.set_state({
+ line2_prompt: null
+ });
+}
+
+function reverse_search()
+{
+ if (rev_search == null) {
+ reverse_search_update("");
+ return;
+ }
+
+ if (!length(rev_search_results))
+ return;
+
+ rev_search_index = (rev_search_index + 1) % length(rev_search_results);
+ let line = rev_search_results[rev_search_index];
+ let pos = length(line);
+ el.set_state({ line, pos });
+}
+
+function line_cb(line)
+{
+ reverse_search_reset();
+ line_history_reset();
+ unshift(history, line);
+
+ let arg_info = parser.parse(line);
+ for (let cmd in arg_info.args) {
+ let orig_cmd = [ ...cmd ];
+ let cur_ctx = ctx.select(cmd);
+ if (!cur_ctx) {
+ warn(`Invalid command: ${orig_cmd[0]}\n`);
+ break;
+ }
+
+ if (!length(cmd)) {
+ ctx = cur_ctx;
+ update_prompt();
+ continue;
+ }
+
+ let res = cur_ctx.call(cmd);
+ format_result(res);
+ }
+}
+
+const cb = {
+ eof: () => { uloop.end(); },
+ interrupt: () => { uloop.end(); },
+ line_check: (line) => parser.check(line) == null,
+ line2_cursor: () => {
+ reverse_search_reset();
+ return false;
+ },
+ line2_update: reverse_search_update,
+ key_input: (c, count) => {
+ try {
+ switch(c) {
+ case "?":
+ if (parser.check(el.get_line().line) != null)
+ return false;
+ completion(-1);
+ return true;
+ case "\t":
+ reverse_search_reset();
+ completion(count);
+ return true;
+ case "\x12":
+ reverse_search();
+ return true;
+ }
+ } catch (e) {
+ el.set_hint(`${e}\n${e.stacktrace[0].context}`);
+ }
+ },
+ cursor_up: () => {
+ try {
+ line_history(1);
+ } catch (e) {
+ el.set_hint(`${e}\n${e.stacktrace[0].context}`);
+ }
+ },
+ cursor_down: () => {
+ try {
+ line_history(-1);
+ } catch (e) {
+ el.set_hint(`${e}\n${e.stacktrace[0].context}`);
+ }
+ },
+};
+el = uline.new({
+ utf8: true,
+ cb,
+ key_input_list: [ "?", "\t", "\x12" ]
+});
+
+warn("Welcome to the OpenWrt CLI. Press '?' for help on commands/arguments\n");
+update_prompt();
+el.set_uloop(line_cb);
+uloop.run();
--- /dev/null
+'use strict';
+
+const CACHE_DEFAULT_TIMEOUT = 5;
+
+function cache_get(key, fn, timeout)
+{
+ let now = time();
+ let entry = this.entries[key];
+ if (entry) {
+ if (now < entry.timeout)
+ return entry.data;
+
+ if (!fn)
+ delete this.entries[key];
+ }
+
+ if (!fn)
+ return;
+
+ let data = fn();
+ if (!entry)
+ this.entries[key] = entry = {};
+ timeout ??= CACHE_DEFAULT_TIMEOUT;
+ entry.timeout = now + timeout;
+ entry.data = data;
+
+ return data;
+}
+
+function cache_remove(key)
+{
+ delete this.entries[key];
+}
+
+function cache_gc() {
+ let now = time();
+ for (let key, entry in this.entries)
+ if (now > entry.timeout)
+ delete this.entries[key];
+}
+
+const cache_proto = {
+ get: cache_get,
+ remove: cache_remove,
+ gc: cache_gc,
+};
+
+export function new(model) {
+ model.cache_proto ??= { model, ...cache_proto };
+ let cache = proto({
+ entries: {},
+ }, model.cache_proto);
+ cache.gc_interval = model.uloop.interval(10000, () => {
+ cache.gc();
+ });
+
+ return cache;
+};
--- /dev/null
+'use strict';
+
+const color_codes = {
+ black: 30,
+ red: 31,
+ green: 32,
+ yellow: 33,
+ blue: 34,
+ magenta: 35,
+ cyan: 36,
+ white: 37,
+ default: 39
+};
+
+function color_str(n)
+{
+ return "\e["+n+"m";
+}
+
+function color_code(str)
+{
+ let n = 0;
+ if (substr(str, 0, 7) == "bright_") {
+ str = substr(str, 7);
+ n += 60;
+ }
+ if (!color_codes[str])
+ return;
+
+ n += color_codes[str];
+ return n;
+}
+
+export function color_fg(name, str)
+{
+ let n = color_code(name);
+ if (!n)
+ return str;
+
+ let ret = color_str(n);
+ if (str != null)
+ ret += str + color_str(39);
+
+ return ret;
+};
+
+export function color_bg(name, str)
+{
+ let n = color_code(name);
+ if (!n)
+ return str;
+
+ let ret = color_str(n + 10);
+ if (str != null)
+ ret += str + color_str(49);
+
+ return ret;
+};
+
+export function bold(str)
+{
+ return color_str(1) + str + color_str(22);
+};
--- /dev/null
+'use strict';
+
+function call_ok(msg)
+{
+ this.result.ok = true;
+ if (msg)
+ this.result.status_msg = msg;
+ return true;
+}
+
+function call_error(code, ...args)
+{
+ let msg = this.model.errors[code] ?? "Unknown error";
+ msg = sprintf(msg, ...args);
+ let error = {
+ code, msg
+ };
+ push(this.result.errors, error);
+}
+
+function call_generic(ctx, name, type, val)
+{
+ ctx.result.type = type;
+ ctx.result.name = name;
+ ctx.result.data = val;
+ return ctx.ok();
+}
+
+function call_table(name, val)
+{
+ return call_generic(this, name, "table", val);
+}
+
+function call_list(name, val)
+{
+ return call_generic(this, name, "list", val);
+}
+
+function call_string(name, val)
+{
+ return call_generic(this, name, "string", val);
+}
+
+const callctx_proto = {
+ ok: call_ok,
+ error: call_error,
+ list: call_list,
+ table: call_table,
+ string: call_string,
+};
+
+export function new(model, data) {
+ model.callctx_proto ??= { model, ...callctx_proto };
+ let result = {
+ errors: [],
+ ok: false
+ };
+ return proto({ data, result }, model.callctx_proto);
+};
--- /dev/null
+'use strict';
+
+import * as callctx from "cli.context-call";
+
+function prefix_match(prefix, str)
+{
+ return substr(str, 0, length(prefix)) == prefix;
+}
+
+function context_clone()
+{
+ let ret = { ...this };
+ ret.prompt = [ ...ret.prompt ];
+ ret.data = { ...ret.data };
+ return ret;
+}
+
+function context_entries()
+{
+ return keys(this.type)
+}
+
+function context_help(entry)
+{
+ if (entry)
+ return this.type[entry].help;
+
+ let ret = {};
+ for (let name, val in this.type)
+ ret[name] = val.help ?? "";
+
+ return ret;
+}
+
+function context_set(type, prompt, data)
+{
+ let new_type = this.model.types[type];
+ if (!new_type)
+ return;
+
+ this.type = new_type;
+ if (prompt)
+ this.cur_prompt = prompt;
+ if (data)
+ this.data = { ...this.data, ...data };
+ return true;
+}
+
+const context_select_proto = {
+ set: context_set
+};
+
+function __context_select(ctx, name, args)
+{
+ let entry = ctx.type[name];
+ if (!entry || !entry.select)
+ return;
+
+ let ret = proto(ctx.clone(), ctx.model.context_select_proto);
+ ret.cur_prompt = name;
+ try {
+ if (!entry.select(ret, args))
+ return;
+ } catch (e) {
+ ctx.model.exception(e);
+ return;
+ }
+
+ push(ret.prompt, ret.cur_prompt);
+ ret.prev = ctx;
+ proto(ret, proto(ctx));
+
+ return ret;
+}
+
+function context_select(args)
+{
+ let ctx = this;
+
+ if (args[0] == "exit" && !this.type.exit) {
+ while (length(args) > 0)
+ pop(args);
+ return this.prev;
+ }
+
+ while (length(args) > 0) {
+ let name = args[0];
+ let entry = ctx.type[name];
+
+ if (!entry || !entry.select)
+ return ctx;
+
+ shift(args);
+ ctx = __context_select(ctx, name, args);
+ if (!ctx)
+ return;
+ }
+
+ return ctx;
+}
+
+function complete_object(obj, name)
+{
+ let data = [];
+ for (let cur_name in sort(keys(obj))) {
+ let val = obj[cur_name];
+
+ if (!prefix_match(name, cur_name))
+ continue;
+
+ push(data, [cur_name, val]);
+ }
+ return { type: "keywords", data };
+}
+
+function default_complete(ctx, args)
+{
+ if (!this.args)
+ return;
+
+ let cur_args = [ ...this.args ];
+ let orig_args = [ ...args ];
+ let cur;
+
+ while (length(args) > 0) {
+ let val = shift(args);
+
+ cur = cur_args[0];
+ if (type(cur) != "object")
+ shift(cur_args);
+
+ if (type(cur) == "object") {
+ if (length(args)) {
+ cur = cur[val];
+ if (!cur)
+ return;
+ }
+
+ return complete_object(cur, val);
+ }
+
+ if (length(args))
+ continue;
+
+ if (type(cur) != "array")
+ return;
+
+ cur = [ ...cur ];
+ if (type(cur[2]) == "function")
+ cur[2] = call(cur[2], this, {}, ctx, orig_args);
+
+ if (cur[2] == "object") {
+ let ret = [];
+ for (let key in sort(keys(cur[2])))
+ if (prefix_match(val, k))
+ push(ret, [ key, cur[2][key] ]);
+ cur[2] = ret;
+ } else if (type(cur[2]) == "array") {
+ cur[2] = map(sort(filter(cur[2], (v) => prefix_match(val, v))), (v) => [ v ]);
+ }
+
+ return { type: "field", data: cur };
+ }
+}
+
+function context_complete(args)
+{
+ let last = pop(args);
+ let ctx = this.select(args);
+ if (last != null)
+ push(args, last);
+
+ if (!ctx)
+ return;
+
+ if (length(args) > 1) {
+ let name = shift(args);
+ let entry = ctx.type[name];
+ if (!entry)
+ return;
+
+ try {
+ return call(entry.complete ?? default_complete, entry, {}, ctx, args);
+ } catch (e) {
+ this.model.exception(e);
+ return;
+ }
+ }
+
+ let name = shift(args) ?? "";
+ let prefix_len = length(name);
+ let data = [];
+ let exit_data;
+ for (let cur_name in sort(keys(ctx.type))) {
+ let val = ctx.type[cur_name];
+
+ if (substr(cur_name, 0, prefix_len) != name)
+ continue;
+
+ let cur = [cur_name, val.help];
+ if (cur_name == "exit")
+ exit_data = cur;
+ else
+ push(data, cur);
+ }
+
+ if (ctx == this && this.prev && !this.type.exit &&
+ prefix_match(name, "exit"))
+ exit_data ??= [ "exit", "Return to previous menu" ];
+
+ if (exit_data)
+ push(data, exit_data);
+
+ return { type: "commands", data };
+}
+
+function context_call(args)
+{
+ let ctx = this;
+
+ while (length(args) > 0) {
+ let name = shift(args);
+ let entry = ctx.type[name];
+ if (!entry)
+ return;
+
+ if (entry.select) {
+ ctx = ctx.select(name, args);
+ if (!ctx)
+ return;
+ continue;
+ }
+
+ if (!entry.call)
+ return;
+
+ ctx = callctx.new(this.model, ctx.data);
+ if (!entry.validate || call(entry.validate, entry, {}, ctx, args)) {
+ try {
+ call(entry.call, entry, {}, ctx, args);
+ } catch (e) {
+ this.model.exception(e);
+ return;
+ }
+ }
+ return ctx.result;
+ }
+}
+
+const context_proto = {
+ clone: context_clone,
+ entries: context_entries,
+ help: context_help,
+ select: context_select,
+ call: context_call,
+ complete: context_complete,
+};
+
+export function new(model) {
+ model.context_proto ??= {
+ model,
+ ...context_proto
+ };
+ model.context_select_proto ??= {
+ model,
+ ...context_select_proto
+ };
+ return proto({
+ prompt: [],
+ type: model.types.Root,
+ data: {}
+ }, model.context_proto);
+};
--- /dev/null
+'use strict';
+
+import * as context from "cli.context";
+import * as cache from "cli.cache";
+import * as libubus from "ubus";
+import * as uloop from "uloop";
+import { glob, dirname } from "fs";
+
+uloop.init();
+let ubus = libubus.connect();
+
+function merge_object(obj, add)
+{
+ for (let name, entry in add)
+ obj[name] = entry;
+}
+
+function merge_types(obj, add)
+{
+ for (let name, val in add) {
+ if (obj[name])
+ merge_object(obj[name], val);
+ else
+ obj[name] = { ...val };
+ }
+}
+
+function add_types(add)
+{
+ merge_types(this.types, add);
+}
+
+function merge_hooks(obj, add)
+{
+ for (let name, val in add) {
+ obj[name] ??= [];
+ push(obj[name], ...val);
+ }
+}
+
+function add_module(path)
+{
+ let mod;
+ try {
+ mod = loadfile(path)();
+ } catch (e) {
+ this.warn(`${e}\n${e.stacktrace[0].context}\nFailed to open module ${path}.\n`);
+ return;
+ }
+
+ merge_hooks(this.hooks, mod.hooks);
+ this.add_types(mod.types);
+ merge_object(this.errors, mod.errors);
+ merge_object(this.warnings, mod.warnings);
+}
+
+function add_modules(path)
+{
+ path ??= dirname(sourcepath()) + "/modules/*.uc";
+ for (let mod in glob(path))
+ this.add_module(mod);
+}
+
+function context_new()
+{
+ return context.new(this);
+}
+
+function exception(e)
+{
+ this.warn(`${e}\n${e.stacktrace[0].context}`);
+}
+
+const data_proto = {
+ warn, exception,
+ add_modules,
+ add_module,
+ add_types,
+ context: context_new,
+};
+
+export function new() {
+ let model = proto({
+ libubus, ubus, uloop,
+ hooks: {},
+ types: {
+ Root: {}
+ },
+ errors: {
+ MISSING_ARGUMENT: "Missing argument: %s",
+ COMMAND_FAILED: "Command failed",
+ },
+ warnings: {},
+ }, data_proto);
+ model.cache = cache.new(model);
+ return model;
+};
--- /dev/null
+import { glob, access, basename } from "fs";
+
+function get_services(model)
+{
+ return model.cache.get("init_service_list", () => {
+ let services = glob("/etc/init.d/*");
+ services = filter(services, (v) => !system([ "grep", "-q", "start_service()", v ]));
+ services = map(services, basename);
+ return sort(services);
+ });
+}
+
+function get_service_status(model, name)
+{
+ return model.ubus.call("service", "list", (name ? { name } : null));
+}
+
+function service_running(model, name)
+{
+ let status = get_service_status(model, name);
+ return !!(status && status[name]);
+}
+
+function __service_cmd(name, cmd)
+{
+ return system([ "/etc/init.d/" + name, cmd ]) == 0;
+}
+
+function service_cmd(ctx, name, cmd, msg)
+{
+ if (__service_cmd(name, cmd))
+ return ctx.ok(msg);
+ else
+ return ctx.error("COMMAND_FAILED");
+}
+
+function service_validate(ctx, argv)
+{
+ let name = argv[0];
+ if (!name)
+ return ctx.error("MISSING_ARGUMENT", "name");
+
+ if (index(get_services(ctx.model), name) < 0)
+ return ctx.error("SERVICE_NOT_FOUND", name);
+
+ return true;
+}
+
+const service_args = [
+ [ "name", "Service name", (ctx) => get_services(ctx.model) ]
+];
+
+const Service = {
+ list: {
+ help: "List services",
+ call: function(ctx, argv) {
+ return ctx.list("Services", get_services(ctx.model));
+ }
+ },
+ reload: {
+ help: "Reload service",
+ validate: service_validate,
+ args: service_args,
+ call: function(ctx, argv) {
+ return service_cmd(ctx, shift(argv), "reload", "Service reloaded");
+ }
+ },
+ restart: {
+ help: "Restart service",
+ validate: service_validate,
+ args: service_args,
+ call: function(ctx, argv) {
+ return service_cmd(ctx, shift(argv), "restart", "Service restarted");
+ }
+ },
+ enable: {
+ help: "Enable service autostart",
+ validate: service_validate,
+ args: service_args,
+ call: function(ctx, argv) {
+ return service_cmd(ctx, shift(argv), "enable", "Service enabled");
+ }
+ },
+ disable: {
+ help: "Disable service autostart",
+ validate: service_validate,
+ args: service_args,
+ call: function(ctx, argv) {
+ return service_cmd(ctx, shift(argv), "disable", "Service disabled");
+ }
+ },
+ start: {
+ help: "Start service",
+ validate: service_validate,
+ args: service_args,
+ call: function(ctx, argv) {
+ let name = shift(argv);
+
+ if (service_running(ctx.model, name))
+ return ctx.error("SERVICE_ALREADY_RUNNING");
+
+ return service_cmd(ctx, name, "start", "Service started");
+ }
+ },
+ stop: {
+ help: "Stop service",
+ validate: service_validate,
+ args: service_args,
+ call: function(ctx, argv) {
+ let name = shift(argv);
+
+ if (!service_running(ctx.model, name))
+ return ctx.error("SERVICE_NOT_RUNNING");
+
+ return service_cmd(ctx, name, "stop", "Service stopped");
+ }
+ },
+ status: {
+ help: "Service status",
+ args: service_args,
+ call: function(ctx, argv) {
+ let name = shift(argv);
+ if (!name) {
+ let data = {};
+ for (let service in get_services(ctx.model)) {
+ let running = service_running(ctx.model, service);
+ data[service] = running ? "running" : "not running";
+ }
+ return ctx.table("Status", data);
+ }
+
+ if (index(get_services(ctx.model), name) < 0)
+ return ctx.error("SERVICE_NOT_FOUND", name);
+
+ let data = {
+ "Running": service_running(ctx.model, name),
+ "Enabled": __service_cmd(name, "enabled"),
+ };
+ return ctx.table("Status", data);
+ }
+ }
+};
+
+const Root = {
+ service: {
+ help: "System service configuration",
+ select: function(ctx, argv) {
+ return ctx.set("Service");
+ },
+ }
+};
+
+return {
+ types: { Root, Service },
+ errors: {
+ SERVICE_NOT_FOUND: "Service not found: %s",
+ SERVICE_ALREADY_RUNNING: "Service already running",
+ SERVICE_NOT_RUNNING: "Service not running",
+ },
+};