[ini]
This commit is contained in:
commit
b109c6777b
13 changed files with 1541 additions and 0 deletions
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
/temp/
|
||||
/build/
|
391
source/api.ts
Normal file
391
source/api.ts
Normal file
|
@ -0,0 +1,391 @@
|
|||
namespace _wiki_js_cli.api
|
||||
{
|
||||
|
||||
/**
|
||||
*/
|
||||
var _login_token : (null | string) = null;
|
||||
|
||||
|
||||
/**
|
||||
*/
|
||||
var _url_base : (null | string) = null;
|
||||
|
||||
|
||||
/**
|
||||
*/
|
||||
export function init(
|
||||
url_base : string
|
||||
) : Promise<void>
|
||||
{
|
||||
_url_base = url_base;
|
||||
return Promise.resolve<void>(undefined);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
*/
|
||||
async function call_generic_graphql(
|
||||
graphql_query,
|
||||
options
|
||||
)
|
||||
{
|
||||
options = Object.assign(
|
||||
{
|
||||
"variables": {},
|
||||
},
|
||||
options
|
||||
);
|
||||
const http_request = {
|
||||
"target": (_url_base + "/graphql"),
|
||||
"method": "POST",
|
||||
"headers": Object.assign(
|
||||
{
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
(
|
||||
(_login_token === null)
|
||||
?
|
||||
{}
|
||||
:
|
||||
{"Cookie": ("jwt=" + _login_token)}
|
||||
)
|
||||
),
|
||||
"body": JSON.stringify(
|
||||
[
|
||||
{
|
||||
"operationName": null,
|
||||
"variables": options.variables,
|
||||
"extensions": {},
|
||||
"query": graphql_query,
|
||||
}
|
||||
]
|
||||
),
|
||||
};
|
||||
const http_response = await _wiki_js_cli.helpers.http.call(http_request);
|
||||
const data = JSON.parse(http_response.body);
|
||||
return Promise.resolve(data[0]["data"]);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
*/
|
||||
export async function call_finalize(
|
||||
admin_email : string,
|
||||
admin_password : string,
|
||||
site_url : string,
|
||||
telemetry : boolean
|
||||
) : Promise<void>
|
||||
{
|
||||
const http_request = {
|
||||
"target": (_wiki_js_cli.conf.get().api.url_base + "/finalize"),
|
||||
"method": "POST",
|
||||
"headers": {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
"body": JSON.stringify(
|
||||
{
|
||||
"adminEmail": admin_email,
|
||||
"adminPassword": admin_password,
|
||||
"adminPasswordConfirm": admin_password,
|
||||
"siteUrl": site_url,
|
||||
"telemetry": telemetry,
|
||||
}
|
||||
),
|
||||
};
|
||||
const http_response = await _wiki_js_cli.helpers.http.call(http_request);
|
||||
const data = JSON.parse(http_response.body);
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* executes a local login
|
||||
*/
|
||||
export async function call_login_local(
|
||||
username : string,
|
||||
password : string
|
||||
) : Promise<void>
|
||||
{
|
||||
_wiki_js_cli.helpers.log.write(
|
||||
_wiki_js_cli.helpers.log.enum_level.info,
|
||||
"api_call_login_local",
|
||||
{
|
||||
}
|
||||
);
|
||||
return (
|
||||
call_generic_graphql(
|
||||
"mutation ($username: String!, $password: String!, $strategy: String!) {authentication {login(username: $username, password: $password, strategy: $strategy) {responseResult {succeeded errorCode slug message __typename} jwt mustChangePwd mustProvideTFA mustSetupTFA continuationToken redirect tfaQRImage __typename} __typename}}",
|
||||
{
|
||||
"variables": {
|
||||
"strategy": "local",
|
||||
"username": username,
|
||||
"password": password,
|
||||
}
|
||||
}
|
||||
)
|
||||
.then(
|
||||
(data) => {
|
||||
if (data["authentication"]["login"]["responseResult"]["succeeded"]) {
|
||||
_login_token = data["authentication"]["login"]["jwt"];
|
||||
return Promise.resolve<void>(undefined);
|
||||
}
|
||||
else {
|
||||
return Promise.reject(new Error("login failed"));
|
||||
}
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
*/
|
||||
export function call_email_settings_set(
|
||||
settings
|
||||
)
|
||||
{
|
||||
_wiki_js_cli.helpers.log.write(
|
||||
_wiki_js_cli.helpers.log.enum_level.info,
|
||||
"api_call_email_settings_set",
|
||||
{
|
||||
"settings": settings,
|
||||
}
|
||||
);
|
||||
return (
|
||||
call_generic_graphql(
|
||||
"mutation ($senderName: String!, $senderEmail: String!, $host: String!, $port: Int!, $name: String!, $secure: Boolean!, $verifySSL: Boolean!, $user: String!, $pass: String!, $useDKIM: Boolean!, $dkimDomainName: String!, $dkimKeySelector: String!, $dkimPrivateKey: String!) {mail {updateConfig(senderName: $senderName, senderEmail: $senderEmail, host: $host, port: $port, name: $name, secure: $secure, verifySSL: $verifySSL, user: $user, pass: $pass, useDKIM: $useDKIM, dkimDomainName: $dkimDomainName, dkimKeySelector: $dkimKeySelector, dkimPrivateKey: $dkimPrivateKey) {responseResult {succeeded errorCode slug message __typename} __typename} __typename}}",
|
||||
{
|
||||
"variables": {
|
||||
"senderName": settings.sender_name,
|
||||
"senderEmail": settings.sender_email_address,
|
||||
"host": settings.smtp_host,
|
||||
"port": settings.smtp_port,
|
||||
"name": settings.name,
|
||||
"secure": settings.secure,
|
||||
"verifySSL": settings.verify_ssl,
|
||||
"user": settings.smtp_username,
|
||||
"pass": settings.smtp_password,
|
||||
"useDKIM": settings.use_dkim,
|
||||
"dkimDomainName": settings.dkim_domain_name,
|
||||
"dkimKeySelector": settings.dkim_key_selector,
|
||||
"dkimPrivateKey": settings.dkim_private_key,
|
||||
}
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
*/
|
||||
export function call_locale_download(
|
||||
locale
|
||||
)
|
||||
{
|
||||
_wiki_js_cli.helpers.log.write(
|
||||
_wiki_js_cli.helpers.log.enum_level.info,
|
||||
"api_call_locale_download",
|
||||
{
|
||||
"locale": locale,
|
||||
}
|
||||
);
|
||||
return (
|
||||
call_generic_graphql(
|
||||
"mutation ($locale: String!) {localization {downloadLocale(locale: $locale) {responseResult {succeeded errorCode slug message __typename} __typename}__typename}}",
|
||||
{
|
||||
"variables": {
|
||||
"locale": locale,
|
||||
}
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
*/
|
||||
export function call_group_list(
|
||||
name : string
|
||||
) : Promise<any>
|
||||
{
|
||||
_wiki_js_cli.helpers.log.write(
|
||||
_wiki_js_cli.helpers.log.enum_level.info,
|
||||
"api_call_group_list",
|
||||
{
|
||||
}
|
||||
);
|
||||
return (
|
||||
call_generic_graphql(
|
||||
"{groups {list {id name isSystem userCount createdAt updatedAt __typename} __typename}}",
|
||||
{
|
||||
"variables": {
|
||||
}
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
*/
|
||||
export function call_group_create(
|
||||
name : string
|
||||
) : Promise<int>
|
||||
{
|
||||
_wiki_js_cli.helpers.log.write(
|
||||
_wiki_js_cli.helpers.log.enum_level.info,
|
||||
"api_call_group_create",
|
||||
{
|
||||
"name": name,
|
||||
}
|
||||
);
|
||||
return (
|
||||
call_generic_graphql(
|
||||
"mutation ($name: String!) {groups {create(name: $name) {responseResult {succeeded errorCode slug message __typename} group {id name createdAt updatedAt __typename} __typename} __typename}}",
|
||||
{
|
||||
"variables": {
|
||||
"name": name,
|
||||
}
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param permissions_general {
|
||||
* Array<
|
||||
* string
|
||||
* >
|
||||
* }
|
||||
* @param permissions_page_specific {
|
||||
* Array<
|
||||
* {
|
||||
* id:string;
|
||||
* path:string;
|
||||
* roles:Array<
|
||||
* string
|
||||
* >;
|
||||
* match:string;
|
||||
* deny:boolean;
|
||||
* locales:Array<
|
||||
* string
|
||||
* >;
|
||||
* }
|
||||
* >
|
||||
* }
|
||||
*/
|
||||
export function call_group_update(
|
||||
group_id,
|
||||
name,
|
||||
permissions_general,
|
||||
permissions_page_specific
|
||||
)
|
||||
{
|
||||
_wiki_js_cli.helpers.log.write(
|
||||
_wiki_js_cli.helpers.log.enum_level.info,
|
||||
"api_call_group_update",
|
||||
{
|
||||
}
|
||||
);
|
||||
return (
|
||||
call_generic_graphql(
|
||||
"mutation ($id: Int!, $name: String!, $redirectOnLogin: String!, $permissions: [String]!, $pageRules: [PageRuleInput]!) {groups {update(id: $id, name: $name, redirectOnLogin: $redirectOnLogin, permissions: $permissions, pageRules: $pageRules) {responseResult {succeeded errorCode slug message __typename} __typename} __typename}}",
|
||||
{
|
||||
"variables": {
|
||||
"id": group_id,
|
||||
"name": name,
|
||||
"redirectOnLogin": "/",
|
||||
"permissions": permissions_general,
|
||||
"pageRules": permissions_page_specific,
|
||||
}
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
*/
|
||||
export function call_authentication_strategy_list(
|
||||
)
|
||||
{
|
||||
_wiki_js_cli.helpers.log.write(
|
||||
_wiki_js_cli.helpers.log.enum_level.info,
|
||||
"api_call_authentication_strategy_list",
|
||||
{
|
||||
}
|
||||
);
|
||||
return (
|
||||
call_generic_graphql(
|
||||
"{authentication {activeStrategies {key strategy {key title description useForm logo website __typename} config {key value __typename} order isEnabled displayName selfRegistration domainWhitelist autoEnrollGroups __typename} __typename}}",
|
||||
{
|
||||
}
|
||||
)
|
||||
.then(
|
||||
(data) => Promise.resolve(data["authentication"]["activeStrategies"])
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
*/
|
||||
export function call_authentication_strategy_set(
|
||||
strategies
|
||||
)
|
||||
{
|
||||
_wiki_js_cli.helpers.log.write(
|
||||
_wiki_js_cli.helpers.log.enum_level.info,
|
||||
"api_call_authentication_strategy_set",
|
||||
{
|
||||
"strategies": strategies,
|
||||
}
|
||||
);
|
||||
return (
|
||||
call_generic_graphql(
|
||||
"mutation ($strategies: [AuthenticationStrategyInput]!) {authentication {updateStrategies(strategies: $strategies) {responseResult {succeeded errorCode slug message __typename} __typename} __typename}}",
|
||||
{
|
||||
"variables": {
|
||||
"strategies": strategies,
|
||||
}
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
*/
|
||||
export function call_theming_set(
|
||||
dark_mode : boolean,
|
||||
toc_position : ("left" | "right" | "off")
|
||||
)
|
||||
{
|
||||
_wiki_js_cli.helpers.log.write(
|
||||
_wiki_js_cli.helpers.log.enum_level.info,
|
||||
"api_call_theming_set",
|
||||
{
|
||||
"dark_mode": dark_mode,
|
||||
"toc_position": toc_position,
|
||||
}
|
||||
);
|
||||
return (
|
||||
call_generic_graphql(
|
||||
"mutation ($theme: String!, $iconset: String!, $darkMode: Boolean!, $tocPosition: String, $injectCSS: String, $injectHead: String, $injectBody: String) {theming {setConfig(theme: $theme, iconset: $iconset, darkMode: $darkMode, tocPosition: $tocPosition, injectCSS: $injectCSS, injectHead: $injectHead, injectBody: $injectBody) {responseResult {succeeded errorCode slug message __typename} __typename} __typename}}",
|
||||
{
|
||||
"variables": {
|
||||
"theme": "default",
|
||||
"iconset": "mdi",
|
||||
"darkMode": dark_mode,
|
||||
"tocPosition": toc_position,
|
||||
"injectCSS": "",
|
||||
"injectHead": "",
|
||||
"injectBody": ""
|
||||
}
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
}
|
6
source/base.ts
Normal file
6
source/base.ts
Normal file
|
@ -0,0 +1,6 @@
|
|||
declare var process;
|
||||
|
||||
declare var require;
|
||||
|
||||
type int = number;
|
||||
|
79
source/conf.ts
Normal file
79
source/conf.ts
Normal file
|
@ -0,0 +1,79 @@
|
|||
namespace _wiki_js_cli.conf
|
||||
{
|
||||
|
||||
/**
|
||||
*/
|
||||
var _data = null;
|
||||
|
||||
|
||||
/**
|
||||
*/
|
||||
export async function load(
|
||||
path : string
|
||||
) : Promise<void>
|
||||
{
|
||||
let data_raw : any;
|
||||
if (path === null) {
|
||||
data_raw = {};
|
||||
}
|
||||
else {
|
||||
const content : string = await _wiki_js_cli.helpers.file.read(path);
|
||||
data_raw = JSON.parse(content);
|
||||
}
|
||||
_wiki_js_cli.helpers.log.write(
|
||||
_wiki_js_cli.helpers.log.enum_level.debug,
|
||||
"conf_raw",
|
||||
data_raw
|
||||
);
|
||||
_data = {
|
||||
"api": {
|
||||
"url_base": (
|
||||
data_raw?.api?.url_base
|
||||
??
|
||||
"http://localhost:3000"
|
||||
),
|
||||
},
|
||||
"login": {
|
||||
"username": (
|
||||
data_raw?.login?.username
|
||||
??
|
||||
"admin"
|
||||
),
|
||||
"password": (
|
||||
data_raw?.login?.password
|
||||
??
|
||||
"admin"
|
||||
),
|
||||
},
|
||||
"log": {
|
||||
"threshold": (
|
||||
data_raw?.log?.threshold
|
||||
??
|
||||
"info"
|
||||
),
|
||||
}
|
||||
};
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
*/
|
||||
export function set(
|
||||
data : any
|
||||
) : void
|
||||
{
|
||||
_data = data;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
*/
|
||||
export function get(
|
||||
) : any
|
||||
{
|
||||
return _data;
|
||||
}
|
||||
|
||||
}
|
||||
|
72
source/helpers/args.ts
Normal file
72
source/helpers/args.ts
Normal file
|
@ -0,0 +1,72 @@
|
|||
namespace _wiki_js_cli.helpers.args
|
||||
{
|
||||
|
||||
/**
|
||||
*/
|
||||
export function parse(
|
||||
args_raw : Array<string>
|
||||
)
|
||||
{
|
||||
let result : {
|
||||
positional : Array<
|
||||
string
|
||||
>;
|
||||
volatile : Record<
|
||||
string,
|
||||
Array<
|
||||
string
|
||||
>
|
||||
>;
|
||||
} = {
|
||||
"positional": [],
|
||||
"volatile": {},
|
||||
};
|
||||
let state : ("free" | "bound") = "free";
|
||||
let key : (null | string) = null;
|
||||
args_raw.forEach(
|
||||
(arg_raw) => {
|
||||
switch (state) {
|
||||
case "free": {
|
||||
if (arg_raw.startsWith("-")) {
|
||||
key = arg_raw.slice(1);
|
||||
state = "bound";
|
||||
}
|
||||
else {
|
||||
if (key === null) {
|
||||
result.positional.push(arg_raw);
|
||||
key = null;
|
||||
state = "free";
|
||||
}
|
||||
else {
|
||||
_wiki_js_cli.helpers.log.write(
|
||||
_wiki_js_cli.helpers.log.enum_level.warning,
|
||||
"arg_discarded",
|
||||
{
|
||||
"arg_raw": arg_raw,
|
||||
}
|
||||
);
|
||||
key = null;
|
||||
state = "free";
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "bound": {
|
||||
if (! (key in result["volatile"])) {
|
||||
result["volatile"][key] = [];
|
||||
}
|
||||
else {
|
||||
// do nothing
|
||||
}
|
||||
result["volatile"][key].push(arg_raw);
|
||||
key = null;
|
||||
state = "free";
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
return result;
|
||||
}
|
||||
|
||||
}
|
||||
|
32
source/helpers/file.ts
Normal file
32
source/helpers/file.ts
Normal file
|
@ -0,0 +1,32 @@
|
|||
namespace _wiki_js_cli.helpers.file
|
||||
{
|
||||
|
||||
/**
|
||||
*/
|
||||
export function read(
|
||||
path : string
|
||||
) : Promise<string>
|
||||
{
|
||||
const nm_fs = require("fs");
|
||||
return (
|
||||
new Promise(
|
||||
(resolve, reject) => {
|
||||
nm_fs.readFile(
|
||||
path,
|
||||
{
|
||||
},
|
||||
(err, data) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
}
|
||||
else {
|
||||
resolve(data.toString());
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
}
|
57
source/helpers/http.ts
Normal file
57
source/helpers/http.ts
Normal file
|
@ -0,0 +1,57 @@
|
|||
namespace _wiki_js_cli.helpers.http
|
||||
{
|
||||
|
||||
/**
|
||||
*/
|
||||
export type type_http_request = {
|
||||
target : string;
|
||||
method : string;
|
||||
headers : Record<string, string>;
|
||||
body : string;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
*/
|
||||
export type type_http_response = {
|
||||
status_code : int;
|
||||
headers : Record<string, string>;
|
||||
body : string;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
*/
|
||||
export async function call(
|
||||
http_request : type_http_request
|
||||
) : Promise<type_http_response>
|
||||
{
|
||||
_wiki_js_cli.helpers.log.write(
|
||||
_wiki_js_cli.helpers.log.enum_level.debug,
|
||||
"http_call_request",
|
||||
http_request
|
||||
);
|
||||
const fetch_request = new Request(
|
||||
http_request.target,
|
||||
{
|
||||
"method": http_request.method,
|
||||
"headers": http_request.headers,
|
||||
"body": http_request.body,
|
||||
}
|
||||
);
|
||||
const fetch_response = await fetch(fetch_request);
|
||||
const http_response = {
|
||||
"status_code": fetch_response.status,
|
||||
"headers": Object.fromEntries(fetch_response.headers.entries()),
|
||||
"body": await fetch_response.text(),
|
||||
};
|
||||
_wiki_js_cli.helpers.log.write(
|
||||
_wiki_js_cli.helpers.log.enum_level.debug,
|
||||
"http_call_response",
|
||||
http_response
|
||||
);
|
||||
return http_response;
|
||||
}
|
||||
|
||||
}
|
||||
|
69
source/helpers/log.ts
Normal file
69
source/helpers/log.ts
Normal file
|
@ -0,0 +1,69 @@
|
|||
namespace _wiki_js_cli.helpers.log
|
||||
{
|
||||
|
||||
/**
|
||||
*/
|
||||
export enum enum_level {
|
||||
debug = "debug",
|
||||
info = "info",
|
||||
notice = "notice",
|
||||
warning = "warning",
|
||||
error = "error",
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
*/
|
||||
const _level_order : Array<enum_level> = [
|
||||
enum_level.debug,
|
||||
enum_level.info,
|
||||
enum_level.notice,
|
||||
enum_level.warning,
|
||||
enum_level.error,
|
||||
];
|
||||
|
||||
|
||||
/**
|
||||
*/
|
||||
var _threshold : enum_level = enum_level.info;
|
||||
|
||||
|
||||
/**
|
||||
*/
|
||||
export function setup(
|
||||
threshold : enum_level
|
||||
) : Promise<void>
|
||||
{
|
||||
_threshold = threshold;
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
*/
|
||||
export function write(
|
||||
level : enum_level,
|
||||
incident : string,
|
||||
details : any
|
||||
) : void
|
||||
{
|
||||
if (_level_order.indexOf(level) < _level_order.indexOf(_threshold)) {
|
||||
// do nothing
|
||||
}
|
||||
else {
|
||||
process.stderr.write(
|
||||
_wiki_js_cli.helpers.string.coin(
|
||||
"\n<{{datetime}}> [{{level}}] {{incident}}\n{{details}}\n\n",
|
||||
{
|
||||
"datetime": (new Date()).toISOString(),
|
||||
"level": level,
|
||||
"incident": incident,
|
||||
"details": JSON.stringify(details, undefined, "\t"),
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
20
source/helpers/string.ts
Normal file
20
source/helpers/string.ts
Normal file
|
@ -0,0 +1,20 @@
|
|||
namespace _wiki_js_cli.helpers.string
|
||||
{
|
||||
|
||||
/**
|
||||
*/
|
||||
export function coin(
|
||||
template : string,
|
||||
arguments_ : Record<string, string>
|
||||
) : string
|
||||
{
|
||||
let result : string = template;
|
||||
Object.entries(arguments_).forEach(
|
||||
([key, value]) => {
|
||||
result = result.replace(new RegExp("{{" + key + "}}", "g"), value);
|
||||
}
|
||||
);
|
||||
return result;
|
||||
}
|
||||
|
||||
}
|
387
source/logic.ts
Normal file
387
source/logic.ts
Normal file
|
@ -0,0 +1,387 @@
|
|||
namespace _wiki_js_cli.logic
|
||||
{
|
||||
|
||||
/**
|
||||
*/
|
||||
export async function login(
|
||||
options : {
|
||||
username ?: string;
|
||||
password ?: string;
|
||||
} = {}
|
||||
) : Promise<void>
|
||||
{
|
||||
options = Object.assign(
|
||||
{
|
||||
"username": _wiki_js_cli.conf.get().login.username,
|
||||
"password": _wiki_js_cli.conf.get().login.password,
|
||||
},
|
||||
options
|
||||
);
|
||||
await _wiki_js_cli.api.call_login_local(
|
||||
options.username,
|
||||
options.password
|
||||
);
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
*/
|
||||
export async function initialize(
|
||||
admin_email_address : string,
|
||||
admin_password : string,
|
||||
options : {
|
||||
site_url ?: string;
|
||||
allow_telemetry ?: boolean;
|
||||
} = {}
|
||||
) : Promise<void>
|
||||
{
|
||||
options = Object.assign(
|
||||
{
|
||||
"site_url": "http://localhost:3000",
|
||||
"allow_telemetry": false,
|
||||
},
|
||||
options
|
||||
);
|
||||
await _wiki_js_cli.api.call_finalize(
|
||||
admin_email_address,
|
||||
admin_password,
|
||||
options.site_url,
|
||||
options.allow_telemetry
|
||||
);
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
*/
|
||||
export async function email_settings_set(
|
||||
smtp_host : string,
|
||||
smtp_port : int,
|
||||
smtp_username : string,
|
||||
smtp_password : string,
|
||||
sender_name : string,
|
||||
sender_email_address : string,
|
||||
options : {
|
||||
} = {}
|
||||
) : Promise<void>
|
||||
{
|
||||
options = Object.assign(
|
||||
{
|
||||
},
|
||||
options
|
||||
);
|
||||
const result = await _wiki_js_cli.api.call_email_settings_set(
|
||||
{
|
||||
"sender_name": sender_name,
|
||||
"sender_email_address": sender_email_address,
|
||||
"smtp_host": smtp_host,
|
||||
"smtp_port": smtp_port,
|
||||
"secure": true,
|
||||
"verify_ssl": true,
|
||||
"smtp_username": smtp_username,
|
||||
"smtp_password": smtp_password,
|
||||
"name": "",
|
||||
"use_dkim": false,
|
||||
"dkim_domain_name": "",
|
||||
"dkim_key_selector": "",
|
||||
"dkim_private_key": "",
|
||||
}
|
||||
);
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
*/
|
||||
export async function locale_add(
|
||||
locale_code : string
|
||||
) : Promise<void>
|
||||
{
|
||||
_wiki_js_cli.api.call_locale_download(
|
||||
locale_code
|
||||
);
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
*/
|
||||
export async function group_identify(
|
||||
name : string
|
||||
) : Promise<int>
|
||||
{
|
||||
const data = await _wiki_js_cli.api.call_group_list(
|
||||
name
|
||||
);
|
||||
const hits : Array<any> = (
|
||||
data["groups"]["list"]
|
||||
.filter(
|
||||
(entry) => (entry["name"] === name)
|
||||
)
|
||||
);
|
||||
if (hits.length !== 1) {
|
||||
return Promise.reject(new Error("not found or ambiguous"));
|
||||
}
|
||||
else {
|
||||
return Promise.resolve(hits[0]["id"]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* returns the ID of the generated group
|
||||
*/
|
||||
export async function group_add(
|
||||
name : string,
|
||||
options : {
|
||||
permissions_general ?: Array<
|
||||
string
|
||||
>;
|
||||
permissions_specific ?: Array<
|
||||
{
|
||||
id : string;
|
||||
path : string;
|
||||
roles : Array<
|
||||
string
|
||||
>;
|
||||
match : string;
|
||||
deny : boolean;
|
||||
locales : Array<
|
||||
string
|
||||
>;
|
||||
}
|
||||
>;
|
||||
} = {}
|
||||
) : Promise<int>
|
||||
{
|
||||
options = Object.assign(
|
||||
{
|
||||
"permissions_general": [],
|
||||
"permissions_specific": [],
|
||||
},
|
||||
options
|
||||
);
|
||||
const result_1 = await _wiki_js_cli.api.call_group_create(
|
||||
name
|
||||
);
|
||||
const id : int = result_1["groups"]["create"]["group"]["id"];
|
||||
const result_2 = await _wiki_js_cli.api.call_group_update(
|
||||
id,
|
||||
name,
|
||||
options.permissions_general,
|
||||
options.permissions_specific
|
||||
);
|
||||
return Promise.resolve(id);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
*/
|
||||
export async function group_modify(
|
||||
id : int,
|
||||
options : {
|
||||
permissions_general ?: Array<
|
||||
string
|
||||
>;
|
||||
permissions_specific ?: Array<
|
||||
{
|
||||
id : string;
|
||||
path : string;
|
||||
roles : Array<
|
||||
string
|
||||
>;
|
||||
match : string;
|
||||
deny : boolean;
|
||||
locales : Array<
|
||||
string
|
||||
>;
|
||||
}
|
||||
>;
|
||||
} = {}
|
||||
) : Promise<void>
|
||||
{
|
||||
options = Object.assign(
|
||||
{
|
||||
"permissions_general": [],
|
||||
"permissions_specific": [],
|
||||
},
|
||||
options
|
||||
);
|
||||
const result = await _wiki_js_cli.api.call_group_update(
|
||||
id,
|
||||
name,
|
||||
options.permissions_general,
|
||||
options.permissions_specific
|
||||
);
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
*/
|
||||
export async function authentication_strategy_list(
|
||||
) : Promise<Array<any>>
|
||||
{
|
||||
const result = await _wiki_js_cli.api.call_authentication_strategy_list(
|
||||
);
|
||||
return Promise.resolve(result);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
*/
|
||||
export async function authentication_strategy_add(
|
||||
strategy : {
|
||||
key:string;
|
||||
name:string;
|
||||
client_id:string;
|
||||
client_secret:string;
|
||||
authorization_url:string;
|
||||
token_url:string;
|
||||
user_info_url:string;
|
||||
group_assignments:Array<string>;
|
||||
}
|
||||
) : Promise<void>
|
||||
{
|
||||
const current = await _wiki_js_cli.api.call_authentication_strategy_list(
|
||||
);
|
||||
const result = await _wiki_js_cli.api.call_authentication_strategy_set(
|
||||
(
|
||||
(
|
||||
current
|
||||
.map(
|
||||
(entry) => ({
|
||||
"key": entry["key"],
|
||||
"strategyKey": entry["strategy"]["key"],
|
||||
"displayName": entry["displayName"],
|
||||
"order": entry["order"],
|
||||
"isEnabled": entry["isEnabled"],
|
||||
"config": (
|
||||
entry["config"]
|
||||
.map(
|
||||
(item) => ({
|
||||
"key": item["key"],
|
||||
"value": JSON.stringify({"v": JSON.parse(item["value"])["value"]}),
|
||||
})
|
||||
)
|
||||
),
|
||||
"selfRegistration": entry["selfRegistration"],
|
||||
"domainWhitelist": entry["domainWhitelist"],
|
||||
"autoEnrollGroups": entry["autoEnrollGroups"],
|
||||
})
|
||||
)
|
||||
)
|
||||
.concat(
|
||||
[
|
||||
{
|
||||
"key": strategy.key,
|
||||
"strategyKey": "oauth2",
|
||||
"displayName": strategy.name,
|
||||
"order": (
|
||||
(
|
||||
current
|
||||
.map(x => x["order"])
|
||||
.reduce((x,y) => (((x === null) || (x < y)) ? y : x), null)
|
||||
)
|
||||
+
|
||||
1
|
||||
),
|
||||
"isEnabled": true,
|
||||
"config": [
|
||||
{
|
||||
"key": "clientId",
|
||||
"value": JSON.stringify({"v": strategy.client_id}),
|
||||
},
|
||||
{
|
||||
"key": "clientSecret",
|
||||
"value": JSON.stringify({"v": strategy.client_secret}),
|
||||
},
|
||||
{
|
||||
"key": "authorizationURL",
|
||||
"value": JSON.stringify({"v": strategy.authorization_url}),
|
||||
},
|
||||
{
|
||||
"key": "tokenURL",
|
||||
"value": JSON.stringify({"v": strategy.token_url}),
|
||||
},
|
||||
{
|
||||
"key": "userInfoURL",
|
||||
"value": JSON.stringify({"v": strategy.user_info_url}),
|
||||
},
|
||||
{
|
||||
"key": "userIdClaim",
|
||||
"value": JSON.stringify({"v": "id"}),
|
||||
},
|
||||
{
|
||||
"key": "displayNameClaim",
|
||||
"value": JSON.stringify({"v": "name"}),
|
||||
},
|
||||
{
|
||||
"key": "emailClaim",
|
||||
"value": JSON.stringify({"v": "email"}),
|
||||
},
|
||||
{
|
||||
"key": "mapGroups",
|
||||
"value": JSON.stringify({"v": false}),
|
||||
},
|
||||
{
|
||||
"key": "groupsClaim",
|
||||
"value": JSON.stringify({"v": "groups"}),
|
||||
},
|
||||
{
|
||||
"key": "logoutURL",
|
||||
"value": JSON.stringify({"v": ""}),
|
||||
},
|
||||
{
|
||||
"key": "scope",
|
||||
"value": JSON.stringify({"v": "openid profile email"}),
|
||||
},
|
||||
{
|
||||
"key": "useQueryStringForAccessToken",
|
||||
"value": JSON.stringify({"v": false}),
|
||||
},
|
||||
{
|
||||
"key": "enableCSRFProtection",
|
||||
"value": JSON.stringify({"v": true}),
|
||||
}
|
||||
],
|
||||
"selfRegistration": true,
|
||||
"domainWhitelist": [],
|
||||
"autoEnrollGroups": await Promise.all(
|
||||
strategy.group_assignments
|
||||
.map(x => group_identify(x))
|
||||
),
|
||||
},
|
||||
]
|
||||
)
|
||||
)
|
||||
);
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
*/
|
||||
export async function theming_set(
|
||||
options : {
|
||||
dark_mode ?: boolean;
|
||||
toc_position ?: ("left" | "right" | "off");
|
||||
} = {}
|
||||
) : Promise<void>
|
||||
{
|
||||
options = Object.assign(
|
||||
{
|
||||
"dark_mode": false,
|
||||
"toc_position": "left",
|
||||
},
|
||||
options
|
||||
);
|
||||
const result = await _wiki_js_cli.api.call_theming_set(
|
||||
options.dark_mode,
|
||||
options.toc_position
|
||||
);
|
||||
return Promise.resolve(result);
|
||||
}
|
||||
|
||||
}
|
377
source/main.ts
Normal file
377
source/main.ts
Normal file
|
@ -0,0 +1,377 @@
|
|||
namespace _wiki_js_cli
|
||||
{
|
||||
|
||||
/**
|
||||
*/
|
||||
function parse_boolean(
|
||||
indicator : string
|
||||
) : boolean
|
||||
{
|
||||
if (
|
||||
(indicator === "1")
|
||||
||
|
||||
(indicator === "yes")
|
||||
||
|
||||
(indicator === "on")
|
||||
||
|
||||
(indicator === "true")
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
else {
|
||||
if (
|
||||
(indicator === "0")
|
||||
||
|
||||
(indicator === "no")
|
||||
||
|
||||
(indicator === "off")
|
||||
||
|
||||
(indicator === "false")
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
else {
|
||||
throw (new Error("invalid boolean indicator: " + indicator));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
*/
|
||||
export async function main(
|
||||
args_raw : Array<string>
|
||||
) : Promise<void>
|
||||
{
|
||||
// args
|
||||
const args = _wiki_js_cli.helpers.args.parse(args_raw);
|
||||
const override_url_base : (null | string) = (
|
||||
(
|
||||
("b" in args.volatile)
|
||||
&&
|
||||
(args.volatile["b"].length >= 0)
|
||||
)
|
||||
?
|
||||
args.volatile["b"][0]
|
||||
:
|
||||
null
|
||||
);
|
||||
const override_username : (null | string) = (
|
||||
(
|
||||
("u" in args.volatile)
|
||||
&&
|
||||
(args.volatile["u"].length >= 0)
|
||||
)
|
||||
?
|
||||
args.volatile["u"][0]
|
||||
:
|
||||
null
|
||||
);
|
||||
const override_password : (null | string) = (
|
||||
(
|
||||
("p" in args.volatile)
|
||||
&&
|
||||
(args.volatile["p"].length >= 0)
|
||||
)
|
||||
?
|
||||
args.volatile["p"][0]
|
||||
:
|
||||
null
|
||||
);
|
||||
const override_log_level : (null | string) = (
|
||||
(
|
||||
("l" in args.volatile)
|
||||
&&
|
||||
(args.volatile["l"].length >= 0)
|
||||
)
|
||||
?
|
||||
args.volatile["l"][0]
|
||||
:
|
||||
null
|
||||
);
|
||||
|
||||
// conf
|
||||
const conf_path : (null | string) = (
|
||||
(
|
||||
("c" in args.volatile)
|
||||
&&
|
||||
(args.volatile["c"].length >= 0)
|
||||
)
|
||||
?
|
||||
args.volatile["c"][0]
|
||||
:
|
||||
null
|
||||
);
|
||||
await _wiki_js_cli.conf.load(conf_path);
|
||||
_wiki_js_cli.helpers.log.write(
|
||||
_wiki_js_cli.helpers.log.enum_level.debug,
|
||||
"conf",
|
||||
_wiki_js_cli.conf.get()
|
||||
);
|
||||
|
||||
// init
|
||||
await _wiki_js_cli.helpers.log.setup(
|
||||
override_log_level ?? _wiki_js_cli.conf.get().log.threshold
|
||||
);
|
||||
await _wiki_js_cli.api.init(
|
||||
(override_url_base ?? _wiki_js_cli.conf.get().api.url_base)
|
||||
);
|
||||
|
||||
// exec
|
||||
if (args.positional.length < 1) {
|
||||
return Promise.reject("SYNTAX: [node] cli.js [-c <conf-path>] [-b <api-url-base>] [-u <login-username>] [-p <login-password>] <action> [<arg-1> [<arg-2> […]]]\n\n\t<action> = init | email-settings-set | locale-add | group-add | group-modify | auth-strat-add-oauth2 | theming-set");
|
||||
}
|
||||
else {
|
||||
const action : string = args.positional[0];
|
||||
switch (action) {
|
||||
default: {
|
||||
return Promise.reject("invalid action: " + action);
|
||||
break;
|
||||
}
|
||||
case "init": {
|
||||
if (args.positional.length <= 2) {
|
||||
return Promise.reject("SYNTAX: … init <admin-email-address> <admin-password> [<site-url> [<allow-telemetry>]]");
|
||||
}
|
||||
else {
|
||||
const admin_email_address : string = args.positional[1];
|
||||
const admin_password : string = args.positional[2];
|
||||
const site_url : (undefined | string) = (
|
||||
(args.positional.length >= 4)
|
||||
?
|
||||
args.positional[3]
|
||||
:
|
||||
undefined
|
||||
);
|
||||
const allow_telemetry : (undefined | boolean) = (
|
||||
(args.positional.length >= 5)
|
||||
?
|
||||
parse_boolean(args.positional[4])
|
||||
:
|
||||
undefined
|
||||
);
|
||||
await _wiki_js_cli.logic.initialize(
|
||||
admin_email_address,
|
||||
admin_password,
|
||||
{
|
||||
"site_url": site_url,
|
||||
"allow_telemetry": allow_telemetry
|
||||
}
|
||||
);
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "email-settings-set": {
|
||||
if (args.positional.length <= 6) {
|
||||
return Promise.reject("SYNTAX: … email-settings-set <smtp-host> <smtp-port> <smtp-username> <smtp-password> <sender-name> <sender-email-address>");
|
||||
}
|
||||
else {
|
||||
const smtp_host : string = args.positional[1];
|
||||
const smtp_port : int = parseInt(args.positional[2]);
|
||||
const smtp_username : string = args.positional[3];
|
||||
const smtp_password : string = args.positional[4];
|
||||
const sender_name : string = args.positional[5];
|
||||
const sender_email_address : string = args.positional[6];
|
||||
await _wiki_js_cli.logic.login(
|
||||
{
|
||||
"username": override_username,
|
||||
"password": override_password,
|
||||
}
|
||||
);
|
||||
await _wiki_js_cli.logic.email_settings_set(
|
||||
smtp_host,
|
||||
smtp_port,
|
||||
smtp_username,
|
||||
smtp_password,
|
||||
sender_name,
|
||||
sender_email_address,
|
||||
{
|
||||
}
|
||||
);
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "locale-add": {
|
||||
if (args.positional.length <= 1) {
|
||||
return Promise.reject("SYNTAX: … locale-add <locale-code>");
|
||||
}
|
||||
else {
|
||||
const locale_code : string = args.positional[1];
|
||||
await _wiki_js_cli.logic.login(
|
||||
{
|
||||
"username": override_username,
|
||||
"password": override_password,
|
||||
}
|
||||
);
|
||||
await _wiki_js_cli.logic.locale_add(
|
||||
locale_code
|
||||
);
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "group-add": {
|
||||
if (args.positional.length <= 2) {
|
||||
return Promise.reject("SYNTAX: … group-add <name> <permissions>");
|
||||
}
|
||||
else {
|
||||
const name = args.positional[1];
|
||||
const permissions = args.positional[2].split(",");
|
||||
await _wiki_js_cli.logic.login(
|
||||
{
|
||||
"username": override_username,
|
||||
"password": override_password,
|
||||
}
|
||||
);
|
||||
const result = await _wiki_js_cli.logic.group_add(
|
||||
name,
|
||||
{
|
||||
"permissions_general": permissions,
|
||||
"permissions_specific": [
|
||||
{
|
||||
"id": "default",
|
||||
"path": "",
|
||||
"roles": permissions,
|
||||
"match": "START",
|
||||
"deny": false,
|
||||
"locales": []
|
||||
}
|
||||
],
|
||||
}
|
||||
);
|
||||
process.stdout.write(
|
||||
JSON.stringify(
|
||||
result,
|
||||
undefined,
|
||||
"\t"
|
||||
)
|
||||
+
|
||||
"\n"
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "group-modify": {
|
||||
if (args.positional.length <= 2) {
|
||||
return Promise.reject("SYNTAX: … group-modify <name> <permissions>");
|
||||
}
|
||||
else {
|
||||
const name = args.positional[1];
|
||||
const permissions = args.positional[2].split(",");
|
||||
const id : int = await _wiki_js_cli.logic.group_identify(name);
|
||||
await _wiki_js_cli.logic.login(
|
||||
{
|
||||
"username": override_username,
|
||||
"password": override_password,
|
||||
}
|
||||
);
|
||||
const result = await _wiki_js_cli.logic.group_modify(
|
||||
id,
|
||||
{
|
||||
"permissions_general": permissions,
|
||||
"permissions_specific": [
|
||||
{
|
||||
"id": "default",
|
||||
"path": "",
|
||||
"roles": permissions,
|
||||
"match": "START",
|
||||
"deny": false,
|
||||
"locales": []
|
||||
}
|
||||
],
|
||||
}
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "auth-strat-add-oauth2": {
|
||||
if (args.positional.length <= 8) {
|
||||
return Promise.reject("SYNTAX: … auth-strat-add-oauth2 <strategy-key> <strategy-name> <strategy-client-id> <strategy-client-secret> <strategy-authorization-url> <strategy-token-url> <strategy-user-info-url> <group-assignments>");
|
||||
}
|
||||
else {
|
||||
const strategy = {
|
||||
"key": args.positional[1],
|
||||
"name": args.positional[2],
|
||||
"client_id": args.positional[3],
|
||||
"client_secret": args.positional[4],
|
||||
"authorization_url": args.positional[5],
|
||||
"token_url": args.positional[6],
|
||||
"user_info_url": args.positional[7],
|
||||
"group_assignments": args.positional[8].split(","),
|
||||
}
|
||||
await _wiki_js_cli.logic.login(
|
||||
{
|
||||
"username": override_username,
|
||||
"password": override_password,
|
||||
}
|
||||
);
|
||||
await _wiki_js_cli.logic.authentication_strategy_add(
|
||||
strategy
|
||||
);
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "theming-set": {
|
||||
if (args.positional.length <= 1) {
|
||||
return Promise.reject("SYNTAX: … theming-set <dark-mode> [<toc-position>]");
|
||||
}
|
||||
else {
|
||||
const dark_mode : boolean = parse_boolean(args.positional[1]);
|
||||
const toc_position_raw : string = (
|
||||
(args.positional.length <= 2)
|
||||
?
|
||||
"left"
|
||||
:
|
||||
args.positional[2]
|
||||
);
|
||||
const toc_position : (null | ("left" | "right" | "off")) = (() => {
|
||||
switch (toc_position_raw) {
|
||||
case "left": return "left";
|
||||
case "right": return "right";
|
||||
case "hidden": return "off";
|
||||
default: {return null;}
|
||||
}
|
||||
}) ();
|
||||
if (toc_position === null) {
|
||||
return Promise.reject("invalid toc-position: " + toc_position_raw + "; valid values are: left,right,hidden");
|
||||
}
|
||||
else {
|
||||
await _wiki_js_cli.logic.login(
|
||||
{
|
||||
"username": override_username,
|
||||
"password": override_password,
|
||||
}
|
||||
);
|
||||
await _wiki_js_cli.logic.theming_set(
|
||||
{
|
||||
"dark_mode": dark_mode,
|
||||
"toc_position": toc_position,
|
||||
}
|
||||
);
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
(
|
||||
_wiki_js_cli.main(process.argv.slice(2))
|
||||
.then(
|
||||
() => {
|
||||
}
|
||||
)
|
||||
.catch(
|
||||
(reason) => {
|
||||
process.stderr.write("-- " + String(reason) + "\n");
|
||||
}
|
||||
)
|
||||
);
|
||||
|
4
tools/build
Executable file
4
tools/build
Executable file
|
@ -0,0 +1,4 @@
|
|||
#!/usr/bin/env sh
|
||||
|
||||
make -f tools/makefile
|
||||
|
45
tools/makefile
Normal file
45
tools/makefile
Normal file
|
@ -0,0 +1,45 @@
|
|||
## vars
|
||||
|
||||
dir_source := source
|
||||
dir_temp := temp
|
||||
dir_build := build
|
||||
|
||||
|
||||
## commands
|
||||
|
||||
cmd_mkdir := mkdir -p
|
||||
cmd_tsc := tsc
|
||||
cmd_cat := cat
|
||||
cmd_chmod := chmod
|
||||
cmd_echo := echo
|
||||
cmd_log := echo "--"
|
||||
|
||||
|
||||
## rules
|
||||
|
||||
.PHONY: _default
|
||||
_default: ${dir_build}/wiki-js-cli
|
||||
|
||||
${dir_temp}/unlinked.js: \
|
||||
${dir_source}/base.ts \
|
||||
${dir_source}/helpers/string.ts \
|
||||
${dir_source}/helpers/file.ts \
|
||||
${dir_source}/helpers/log.ts \
|
||||
${dir_source}/helpers/http.ts \
|
||||
${dir_source}/helpers/args.ts \
|
||||
${dir_source}/conf.ts \
|
||||
${dir_source}/api.ts \
|
||||
${dir_source}/logic.ts \
|
||||
${dir_source}/main.ts
|
||||
@ ${cmd_log} "compile …"
|
||||
@ ${cmd_mkdir} $(dir $@)
|
||||
@ ${cmd_tsc} --target es2020 $^ --outFile $@
|
||||
|
||||
${dir_build}/wiki-js-cli: \
|
||||
${dir_temp}/unlinked.js
|
||||
@ ${cmd_log} "link …"
|
||||
@ ${cmd_mkdir} $(dir $@)
|
||||
@ ${cmd_echo} "#!/usr/bin/env node" > $@
|
||||
@ ${cmd_cat} $^ >> $@
|
||||
@ ${cmd_chmod} +x $@
|
||||
|
Loading…
Add table
Reference in a new issue