@neonwilderness/moveskins 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +2 -0
- package/CHANGELOG.md +6 -0
- package/LICENSE +21 -0
- package/README.md +3 -0
- package/dist/index.js +206 -0
- package/package.json +64 -0
- package/src/_compare.js +130 -0
- package/src/_download.js +51 -0
- package/src/_upload.js +68 -0
- package/src/_utils.js +107 -0
- package/src/create.js +65 -0
- package/src/images.js +55 -0
- package/src/index.js +114 -0
- package/src/template.html +132 -0
- package/src/template.json +361 -0
package/.env.example
ADDED
package/CHANGELOG.md
ADDED
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 NeonWilderness
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import e from "yargs/yargs";
|
|
2
|
+
import t from "chalk";
|
|
3
|
+
import * as n from "@neonwilderness/twoday";
|
|
4
|
+
import { deleteSync as r } from "del";
|
|
5
|
+
import { existsSync as i, mkdirSync as a, readFileSync as o, readdirSync as s, writeFileSync as c } from "node:fs";
|
|
6
|
+
import { resolve as l } from "node:path";
|
|
7
|
+
import { config as u } from "dotenv-safe";
|
|
8
|
+
//#region src/_utils.js
|
|
9
|
+
var d = l(process.cwd(), "skins"), f = l(process.cwd(), "backup"), p = l(d, "skinRegister.json"), m = {
|
|
10
|
+
data: void 0,
|
|
11
|
+
hasChanged: !1,
|
|
12
|
+
load() {
|
|
13
|
+
return this.data = i(p) ? JSON.parse(o(p).toString()) : {}, this.data;
|
|
14
|
+
},
|
|
15
|
+
store() {
|
|
16
|
+
this.hasChanged &&= (c(p, JSON.stringify(this.data, null, 2)), !1);
|
|
17
|
+
},
|
|
18
|
+
hasData(e) {
|
|
19
|
+
return this.data === void 0 && this.load(), this.data.hasOwnProperty(e);
|
|
20
|
+
},
|
|
21
|
+
getData(e) {
|
|
22
|
+
let t = e.endsWith(".skin") ? e.substr(0, e.length - 5) : e;
|
|
23
|
+
if (this.hasData(t)) {
|
|
24
|
+
let { title: e, description: n } = this.data[t];
|
|
25
|
+
return {
|
|
26
|
+
name: t,
|
|
27
|
+
title: e,
|
|
28
|
+
description: n
|
|
29
|
+
};
|
|
30
|
+
} else return {
|
|
31
|
+
name: t,
|
|
32
|
+
title: t,
|
|
33
|
+
description: ""
|
|
34
|
+
};
|
|
35
|
+
},
|
|
36
|
+
setData(e, t, n) {
|
|
37
|
+
this.hasData(e) || (this.data[e] = {
|
|
38
|
+
title: t,
|
|
39
|
+
description: n
|
|
40
|
+
}, this.hasChanged = !0);
|
|
41
|
+
},
|
|
42
|
+
deleteData(e) {
|
|
43
|
+
this.hasData(e) && delete this.data[e];
|
|
44
|
+
}
|
|
45
|
+
}, h = (e, t) => l(t ? f : d, e), g = (e, t) => {
|
|
46
|
+
try {
|
|
47
|
+
r(e.map((e) => l(h(e, t), "*.skin")));
|
|
48
|
+
} catch (e) {
|
|
49
|
+
console.log(`cleanupAliasFolder ended with error: ${e.toString()}.`), process.exit(1);
|
|
50
|
+
}
|
|
51
|
+
}, _ = (e, t) => {
|
|
52
|
+
let n = h(e, t);
|
|
53
|
+
return i(n) ? s(n).filter((e) => e.endsWith(".skin")) : [];
|
|
54
|
+
}, v = (e, t) => e.split(t).filter((e) => e.trim().length).map((e) => e.toLowerCase()), y = async (e, n, r) => {
|
|
55
|
+
try {
|
|
56
|
+
await e.login();
|
|
57
|
+
for (let o of n) {
|
|
58
|
+
console.log(t.green(`\nNow processing alias "${o}"...`));
|
|
59
|
+
let n = h(o, r.backup), s = await e.getModifiedSkins(o);
|
|
60
|
+
s.length && !i(n) && a(n), console.log(t.yellow(`${o} has ${s.length} modified skin${s.length === 1 ? "" : "s"}`)), r.skin && console.log(t.yellow(`Filtering skins for ${r.skin}.`));
|
|
61
|
+
for (let i of s) {
|
|
62
|
+
if (r.skin && !r.skin.test(i.name)) continue;
|
|
63
|
+
console.log(t.gray(`Reading "${i.name}".`));
|
|
64
|
+
let { title: a, description: o, skin: s } = await e.getSkin(i);
|
|
65
|
+
m.setData(i.name, a, o), c(l(n, `${i.name}.skin`), s);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
console.log(t.green("Download completed."));
|
|
69
|
+
} catch (e) {
|
|
70
|
+
console.log(t.red(`Error while downloading skins: ${e}`));
|
|
71
|
+
} finally {
|
|
72
|
+
m.store();
|
|
73
|
+
}
|
|
74
|
+
}, b = async (e, n, r) => {
|
|
75
|
+
try {
|
|
76
|
+
await e.login();
|
|
77
|
+
for (let i of n) {
|
|
78
|
+
console.log(t.green(`\nNow processing alias "${i}"...`));
|
|
79
|
+
let n = _(i, r.backup);
|
|
80
|
+
console.log(t.yellow(`${i} has ${n.length} local skins. `) + t.red(`${n.length === 0 ? "Got nothing to do!" : ""}`)), r.skin && console.log(t.yellow(`Filtering skins for ${r.skin}.`));
|
|
81
|
+
let a = await e.getModifiedSkins(i), s = new Set(a.map((e) => e.name)), c = h(i, r.backup);
|
|
82
|
+
for (let a of n) {
|
|
83
|
+
if (!a.endsWith(".skin") || r.skin && !r.skin.test(a)) continue;
|
|
84
|
+
let n = m.getData(a);
|
|
85
|
+
n.content = o(l(c, a)).toString().replace(/\r\n/g, "\n"), console.log(t.blue(`Now updating skin ${n.name} (len=${n.content.length}).`)), await e.updateSkin(i, n.name, {
|
|
86
|
+
title: n.title,
|
|
87
|
+
description: n.description,
|
|
88
|
+
skin: n.content,
|
|
89
|
+
diff: r.debug
|
|
90
|
+
}), console.log(t.green(`Update request completed for skin: ${n.name}`)), s.has(n.name) && s.delete(n.name);
|
|
91
|
+
}
|
|
92
|
+
s.size > 0 && (console.log(t.blue(`\nFollowing skins exist on "${i}" and were not touched by this upload:`)), s.forEach((e) => console.log(`--> ${t.blue(e)}`)));
|
|
93
|
+
}
|
|
94
|
+
console.log(t.green("Upload completed."));
|
|
95
|
+
} catch (e) {
|
|
96
|
+
console.log(t.red(`Error while uploading skins: ${e}`));
|
|
97
|
+
}
|
|
98
|
+
}, x = {
|
|
99
|
+
different: "cyan",
|
|
100
|
+
identical: "grey",
|
|
101
|
+
new: "green",
|
|
102
|
+
untouched: "grey"
|
|
103
|
+
}, S = (e, t) => e.name < t.name ? -1 : e.name > t.name ? 1 : 0, C = async (e, n) => {
|
|
104
|
+
let r = { aliases: [] };
|
|
105
|
+
try {
|
|
106
|
+
typeof n == "string" && (n = [n]), await e.login();
|
|
107
|
+
for (let i of n) {
|
|
108
|
+
let n = {
|
|
109
|
+
header: "",
|
|
110
|
+
skins: []
|
|
111
|
+
}, a = i, s = i;
|
|
112
|
+
i.includes(":") && ([a, s] = v(i, ":")), n.header = `Now comparing local alias "${a}" to remote ${e.platform.toUpperCase()} alias "${s}@${e.fullDomain}" [${(/* @__PURE__ */ new Date()).toLocaleString()}] ...`, console.log(t.green(`\n${n.header}`)), n.header = n.header.replace("local", "<ins>local</ins>").replace("remote", "<del>remote</del>");
|
|
113
|
+
let c = h(a, !1), u = _(a, !1).map((e) => {
|
|
114
|
+
let t = m.getData(e);
|
|
115
|
+
return t.skin = o(l(c, e)).toString().replace(/\r\n/g, "\n"), t;
|
|
116
|
+
}).sort(S), d = await e.getModifiedSkins(s), f = [];
|
|
117
|
+
for (let t of d) f.push(e.getSkin(t));
|
|
118
|
+
let p = (await Promise.all(f)).map((e) => {
|
|
119
|
+
let { name: t, title: n, description: r, skin: i } = e;
|
|
120
|
+
return {
|
|
121
|
+
name: t,
|
|
122
|
+
title: n,
|
|
123
|
+
description: r,
|
|
124
|
+
skin: i
|
|
125
|
+
};
|
|
126
|
+
}).sort(S), g = 0, y = 0;
|
|
127
|
+
for (; g < u.length && y < p.length;) {
|
|
128
|
+
let r = g < u.length ? u[g].name : "", i = y < p.length ? p[y].name : "", a = {
|
|
129
|
+
open: !1,
|
|
130
|
+
results: []
|
|
131
|
+
};
|
|
132
|
+
if (r === i) {
|
|
133
|
+
console.log(t.white(`Processing skin ${t.italic(r)}`));
|
|
134
|
+
let n = e.diffSkin(r, p[y], u[g]);
|
|
135
|
+
a.class = n.skinChanged ? "different" : "identical", a.open = n.skinChanged, a.text = `${n.skinName} is ${a.class}.`, a.results = n.results, g++, y++;
|
|
136
|
+
} else r < i ? (a.text = `Local skin ${r} missing on remote. Will be created upon update!`, a.class = "new", g++) : (a.text = `Remote skin ${i} without local counterpart. Will remain untouched upon update!`, a.class = "untouched", y++), console.log(t[x[a.class]](a.text));
|
|
137
|
+
n.skins.push(a);
|
|
138
|
+
}
|
|
139
|
+
r.aliases.push(n);
|
|
140
|
+
}
|
|
141
|
+
c(l(process.cwd(), "./src/template.json"), JSON.stringify(r, null, 2));
|
|
142
|
+
let i = o(l(process.cwd(), "./src/template.html")).toString().trim();
|
|
143
|
+
c(l(process.cwd(), "./report.html"), i.replace("$$templateJSON", `(${JSON.stringify(r, null, 2)})`)), console.log(t.green("Local/Remote comparison completed."));
|
|
144
|
+
} catch (e) {
|
|
145
|
+
console.log(t.red(`Error while comparing skins: ${e}`));
|
|
146
|
+
}
|
|
147
|
+
}, w = e(process.argv.slice(2)).options({
|
|
148
|
+
from: {
|
|
149
|
+
alias: "f",
|
|
150
|
+
description: "Platform where to download the skins from: \"dev\" or \"prod\"",
|
|
151
|
+
type: "string"
|
|
152
|
+
},
|
|
153
|
+
to: {
|
|
154
|
+
alias: "t",
|
|
155
|
+
description: "Platform where to compare or upload the skins to: \"dev\" or \"prod\"",
|
|
156
|
+
type: "string"
|
|
157
|
+
},
|
|
158
|
+
alias: {
|
|
159
|
+
alias: "a",
|
|
160
|
+
description: "One or more alias blog name/s delimited by comma (no space)",
|
|
161
|
+
type: "string"
|
|
162
|
+
},
|
|
163
|
+
skin: {
|
|
164
|
+
alias: "s",
|
|
165
|
+
description: "Skin selection string (regex) to filter the skins to upload",
|
|
166
|
+
type: "string"
|
|
167
|
+
},
|
|
168
|
+
backup: {
|
|
169
|
+
alias: "b",
|
|
170
|
+
description: "Backup skins to the local /backup folder to preserve their former status",
|
|
171
|
+
type: "boolean"
|
|
172
|
+
},
|
|
173
|
+
clean: {
|
|
174
|
+
default: !1,
|
|
175
|
+
description: "Cleanup (delete) the alias local skin directory to remove potential old files before new download",
|
|
176
|
+
type: "boolean"
|
|
177
|
+
},
|
|
178
|
+
compare: {
|
|
179
|
+
default: !1,
|
|
180
|
+
description: "Compare the local alias skin directory to a remote location (prod/dev)",
|
|
181
|
+
type: "boolean"
|
|
182
|
+
},
|
|
183
|
+
debug: {
|
|
184
|
+
default: !1,
|
|
185
|
+
description: "Issue more logging messages and diff analysis (for upload function)",
|
|
186
|
+
type: "boolean"
|
|
187
|
+
}
|
|
188
|
+
}).argv;
|
|
189
|
+
u(), !w.from && !w.to && (console.log("Desired action must be specified with --from=dev|prod or --to=dev|prod"), process.exit(1)), w.from && w.to && (console.log("You cannot specify --from=dev|prod AND --to=dev|prod in one run"), process.exit(1)), w.compare && !w.to && (console.log("You must specify a target platform with --to=dev|prod when comparing skins"), process.exit(1)), w.alias || (console.log("You must specify the desired alias(es) with --alias=name1,name2,... OR --alias=*"), process.exit(1));
|
|
190
|
+
var T = (w.from || w.to).toLowerCase(), E = new n.Twoday(T, { delay: 100 }), D = w.alias === "*" ? [
|
|
191
|
+
"www",
|
|
192
|
+
"info",
|
|
193
|
+
"help",
|
|
194
|
+
"top"
|
|
195
|
+
] : v(w.alias, ",");
|
|
196
|
+
if (w.compare) D.length === 1 && D[0].includes(":") && console.log(`Start comparing local skin files of alias${D.length > 1 ? "es" : ""} ${t.yellow(D.join(", "))} to ${t.green(E.fullDomain)}...`), C(E, D);
|
|
197
|
+
else {
|
|
198
|
+
let e = w.from ? "down" : "up", n = w.from ? "from" : "to", r = w.from ? y : b;
|
|
199
|
+
e === "down" && w.clean && g(D, w.backup), console.log(`Start ${e}loading modified skins of alias${D.length > 1 ? "es" : ""} ${t.yellow(D.join(", "))} ${n} remote ${t.green(E.fullDomain)}...`), r(E, D, {
|
|
200
|
+
backup: w.backup,
|
|
201
|
+
debug: w.debug,
|
|
202
|
+
skin: w.skin ? new RegExp(w.skin) : null
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
//#endregion
|
|
206
|
+
export {};
|
package/package.json
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@neonwilderness/moveskins",
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"description": "Download/Upload/Compare modified skins for selected aliases on Twoday",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"antville",
|
|
7
|
+
"migration",
|
|
8
|
+
"skins",
|
|
9
|
+
"twoday"
|
|
10
|
+
],
|
|
11
|
+
"license": "MIT",
|
|
12
|
+
"author": "NeonWilderness",
|
|
13
|
+
"files": [
|
|
14
|
+
"dist",
|
|
15
|
+
"src",
|
|
16
|
+
".env.example",
|
|
17
|
+
"CHANGELOG.md",
|
|
18
|
+
"LICENSE",
|
|
19
|
+
"pack*.json",
|
|
20
|
+
"README.md"
|
|
21
|
+
],
|
|
22
|
+
"type": "module",
|
|
23
|
+
"exports": "./dist/index.js",
|
|
24
|
+
"scripts": {
|
|
25
|
+
"update": "ncu -u && npm i",
|
|
26
|
+
"backup": "node ./src/index --backup --alias=www --from=prod",
|
|
27
|
+
"dev": "vp run node ./src/index.js",
|
|
28
|
+
"build": "vp build",
|
|
29
|
+
"check": "vp check",
|
|
30
|
+
"create": "node ./src/create --skin=Site.testskin --alias=talkstraight",
|
|
31
|
+
"//": "For compare function, live-server module must be installed globally!",
|
|
32
|
+
"compare": "node ./src/index --compare --alias=help --to=dev && npm run report",
|
|
33
|
+
"compare:blogs": "node ./src/index --compare --alias=foundation:info --to=prod && npm run report",
|
|
34
|
+
"download": "node ./src/index --alias=foundation --from=prod",
|
|
35
|
+
"download2": "node ./src/index --alias=info,help --from=prod",
|
|
36
|
+
"download:clean": "node ./src/index --alias=recycle --from=prod --clean",
|
|
37
|
+
"download:core": "node ./src/index --alias=* --from=prod",
|
|
38
|
+
"download:core:clean": "node ./src/index --alias=* --from=prod --clean",
|
|
39
|
+
"report": "live-server --open=./report.html --quiet",
|
|
40
|
+
"restore": "node ./src/index --backup --alias=www --to=dev",
|
|
41
|
+
"upload": "node ./src/index --alias=neonwilderness --to=dev",
|
|
42
|
+
"upload2": "node ./src/index --alias=info,help --to=dev",
|
|
43
|
+
"upload:core": "node ./src/index --alias=* --to=dev",
|
|
44
|
+
"images": "node ./src/images --alias=recycle --to=prod",
|
|
45
|
+
"lonely": "node ./util/buildLonelyBlogs",
|
|
46
|
+
"test": "vp test",
|
|
47
|
+
"test:ui": "vp test --ui"
|
|
48
|
+
},
|
|
49
|
+
"dependencies": {
|
|
50
|
+
"@neonwilderness/twoday": "^0.6.1",
|
|
51
|
+
"chalk": "^5.6.2",
|
|
52
|
+
"del": "^8.0.1",
|
|
53
|
+
"dotenv-safe": "^9.1.0",
|
|
54
|
+
"yargs": "^18.0.0"
|
|
55
|
+
},
|
|
56
|
+
"devDependencies": {
|
|
57
|
+
"@vitest/ui": "4.1.1",
|
|
58
|
+
"slash": "^5.1.0",
|
|
59
|
+
"vite-plus": "^0.1.15"
|
|
60
|
+
},
|
|
61
|
+
"engines": {
|
|
62
|
+
"node": ">=20"
|
|
63
|
+
}
|
|
64
|
+
}
|
package/src/_compare.js
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compare
|
|
3
|
+
* =======
|
|
4
|
+
*
|
|
5
|
+
*/
|
|
6
|
+
import chalk from "chalk";
|
|
7
|
+
import { readFileSync, writeFileSync } from "node:fs";
|
|
8
|
+
import { resolve } from "node:path";
|
|
9
|
+
import { aliasSkinFolder, getLocalSkins, skinRegister, splitAliases } from "./_utils.js";
|
|
10
|
+
|
|
11
|
+
const classToColor = {
|
|
12
|
+
different: "cyan",
|
|
13
|
+
identical: "grey",
|
|
14
|
+
new: "green",
|
|
15
|
+
untouched: "grey",
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Sorting function sorts objects by their name property alphabetically
|
|
20
|
+
* @param {object} skinA
|
|
21
|
+
* @param {object} skinB
|
|
22
|
+
* @returns -1/1/0 depending on name comparison
|
|
23
|
+
*/
|
|
24
|
+
const skinsByName = (skinA, skinB) => {
|
|
25
|
+
if (skinA.name < skinB.name) return -1;
|
|
26
|
+
if (skinA.name > skinB.name) return 1;
|
|
27
|
+
return 0;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Compares all local skins of selected aliases to the target platform skins
|
|
32
|
+
* @param {Object} td Twoday access class object instance
|
|
33
|
+
* @param {String[] | String} aliases One or more target alias(es), can also be a single alias string
|
|
34
|
+
* @returns {Promise}
|
|
35
|
+
*/
|
|
36
|
+
const compareLocalToRemoteSkins = async (td, aliases) => {
|
|
37
|
+
const alpineView = { aliases: [] };
|
|
38
|
+
try {
|
|
39
|
+
if (typeof aliases === "string") aliases = [aliases];
|
|
40
|
+
|
|
41
|
+
await td.login();
|
|
42
|
+
|
|
43
|
+
for (let alias of aliases) {
|
|
44
|
+
let aliasView = { header: "", skins: [] };
|
|
45
|
+
let localAlias = alias;
|
|
46
|
+
let remoteAlias = alias;
|
|
47
|
+
if (alias.includes(":")) [localAlias, remoteAlias] = splitAliases(alias, ":");
|
|
48
|
+
aliasView.header = `Now comparing local alias "${localAlias}" to remote ${td.platform.toUpperCase()} alias "${remoteAlias}@${td.fullDomain}" [${new Date().toLocaleString()}] ...`;
|
|
49
|
+
console.log(chalk.green(`\n${aliasView.header}`));
|
|
50
|
+
aliasView.header = aliasView.header
|
|
51
|
+
.replace("local", "<ins>local</ins>")
|
|
52
|
+
.replace("remote", "<del>remote</del>");
|
|
53
|
+
|
|
54
|
+
const aliasSkinDir = aliasSkinFolder(localAlias, false);
|
|
55
|
+
const localSkinData = getLocalSkins(localAlias, false)
|
|
56
|
+
.map((skinFile) => {
|
|
57
|
+
const localSkin = skinRegister.getData(skinFile);
|
|
58
|
+
localSkin.skin = readFileSync(resolve(aliasSkinDir, skinFile))
|
|
59
|
+
.toString()
|
|
60
|
+
.replace(/\r\n/g, "\n");
|
|
61
|
+
return localSkin;
|
|
62
|
+
})
|
|
63
|
+
.sort(skinsByName);
|
|
64
|
+
|
|
65
|
+
const modifiedSkins = await td.getModifiedSkins(remoteAlias);
|
|
66
|
+
let remoteSkins = [];
|
|
67
|
+
for (let skin of modifiedSkins) {
|
|
68
|
+
remoteSkins.push(td.getSkin(skin));
|
|
69
|
+
}
|
|
70
|
+
const remoteSkinEnriched = await Promise.all(remoteSkins);
|
|
71
|
+
const remoteSkinData = remoteSkinEnriched
|
|
72
|
+
.map((data) => {
|
|
73
|
+
const { name, title, description, skin } = data;
|
|
74
|
+
return { name, title, description, skin };
|
|
75
|
+
})
|
|
76
|
+
.sort(skinsByName);
|
|
77
|
+
|
|
78
|
+
const endOfSkins = String.fromCharCode(0xffff);
|
|
79
|
+
let l = 0;
|
|
80
|
+
let r = 0;
|
|
81
|
+
while (l < localSkinData.length && r < remoteSkinData.length) {
|
|
82
|
+
const lName = l < localSkinData.length ? localSkinData[l].name : endOfSkins;
|
|
83
|
+
const rName = r < remoteSkinData.length ? remoteSkinData[r].name : endOfSkins;
|
|
84
|
+
let skinView = { open: false, results: [] };
|
|
85
|
+
if (lName === rName) {
|
|
86
|
+
console.log(chalk.white(`Processing skin ${chalk.italic(lName)}`));
|
|
87
|
+
let skinDiff = td.diffSkin(lName, remoteSkinData[r], localSkinData[l]);
|
|
88
|
+
skinView.class = skinDiff.skinChanged ? "different" : "identical";
|
|
89
|
+
skinView.open = skinDiff.skinChanged;
|
|
90
|
+
skinView.text = `${skinDiff.skinName} is ${skinView.class}.`;
|
|
91
|
+
skinView.results = skinDiff.results;
|
|
92
|
+
l++;
|
|
93
|
+
r++;
|
|
94
|
+
} else {
|
|
95
|
+
if (lName < rName) {
|
|
96
|
+
skinView.text = `Local skin ${lName} missing on remote. Will be created upon update!`;
|
|
97
|
+
skinView.class = "new";
|
|
98
|
+
l++;
|
|
99
|
+
} else {
|
|
100
|
+
// lName > rName
|
|
101
|
+
skinView.text = `Remote skin ${rName} without local counterpart. Will remain untouched upon update!`;
|
|
102
|
+
skinView.class = "untouched";
|
|
103
|
+
r++;
|
|
104
|
+
}
|
|
105
|
+
console.log(chalk[classToColor[skinView.class]](skinView.text));
|
|
106
|
+
}
|
|
107
|
+
aliasView.skins.push(skinView);
|
|
108
|
+
}
|
|
109
|
+
alpineView.aliases.push(aliasView);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
writeFileSync(
|
|
113
|
+
resolve(process.cwd(), "./src/template.json"),
|
|
114
|
+
JSON.stringify(alpineView, null, 2),
|
|
115
|
+
);
|
|
116
|
+
const alpineTemplate = readFileSync(resolve(process.cwd(), "./src/template.html"))
|
|
117
|
+
.toString()
|
|
118
|
+
.trim();
|
|
119
|
+
writeFileSync(
|
|
120
|
+
resolve(process.cwd(), "./report.html"),
|
|
121
|
+
alpineTemplate.replace("$$templateJSON", `(${JSON.stringify(alpineView, null, 2)})`),
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
console.log(chalk.green("Local/Remote comparison completed."));
|
|
125
|
+
} catch (e) {
|
|
126
|
+
console.log(chalk.red(`Error while comparing skins: ${e}`));
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
export { compareLocalToRemoteSkins };
|
package/src/_download.js
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Download
|
|
3
|
+
* ========
|
|
4
|
+
*
|
|
5
|
+
*/
|
|
6
|
+
import chalk from "chalk";
|
|
7
|
+
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
8
|
+
import { resolve } from "node:path";
|
|
9
|
+
import { aliasSkinFolder, skinRegister } from "./_utils.js";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Downloads all customized skins of one or more aliases and stores them in appropriate
|
|
13
|
+
* subdirectories for subsequent manual edits (or just as backup)
|
|
14
|
+
* @param {Object} td Twoday class object instance
|
|
15
|
+
* @param {String[]} aliases array of aliases (one or more blog names)
|
|
16
|
+
*/
|
|
17
|
+
const downloadModifiedSkins = async (td, aliases, options) => {
|
|
18
|
+
try {
|
|
19
|
+
await td.login();
|
|
20
|
+
|
|
21
|
+
for (let alias of aliases) {
|
|
22
|
+
console.log(chalk.green(`\nNow processing alias "${alias}"...`));
|
|
23
|
+
const aliasSkinDir = aliasSkinFolder(alias, options.backup);
|
|
24
|
+
const modifiedSkins = await td.getModifiedSkins(alias);
|
|
25
|
+
|
|
26
|
+
if (modifiedSkins.length && !existsSync(aliasSkinDir)) mkdirSync(aliasSkinDir);
|
|
27
|
+
console.log(
|
|
28
|
+
chalk.yellow(
|
|
29
|
+
`${alias} has ${modifiedSkins.length} modified skin${modifiedSkins.length !== 1 ? "s" : ""}`,
|
|
30
|
+
),
|
|
31
|
+
);
|
|
32
|
+
if (options.skin) console.log(chalk.yellow(`Filtering skins for ${options.skin}.`));
|
|
33
|
+
|
|
34
|
+
for (let modSkin of modifiedSkins) {
|
|
35
|
+
if (options.skin && !options.skin.test(modSkin.name)) continue;
|
|
36
|
+
console.log(chalk.gray(`Reading "${modSkin.name}".`));
|
|
37
|
+
const { title, description, skin } = await td.getSkin(modSkin);
|
|
38
|
+
|
|
39
|
+
skinRegister.setData(modSkin.name, title, description);
|
|
40
|
+
writeFileSync(resolve(aliasSkinDir, `${modSkin.name}.skin`), skin);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
console.log(chalk.green("Download completed."));
|
|
44
|
+
} catch (e) {
|
|
45
|
+
console.log(chalk.red(`Error while downloading skins: ${e}`));
|
|
46
|
+
} finally {
|
|
47
|
+
skinRegister.store();
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export { downloadModifiedSkins };
|
package/src/_upload.js
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Upload
|
|
3
|
+
* ======
|
|
4
|
+
*
|
|
5
|
+
*/
|
|
6
|
+
import chalk from "chalk";
|
|
7
|
+
import { readFileSync } from "node:fs";
|
|
8
|
+
import { resolve } from "node:path";
|
|
9
|
+
import { aliasSkinFolder, getLocalSkins, skinRegister } from "./_utils.js";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Uploads all local skins of selected aliases to the target platform
|
|
13
|
+
* @param {Object} td Twoday class object instance
|
|
14
|
+
* @param {String[]} aliases One or more target alias(es)
|
|
15
|
+
* @returns {Promise}
|
|
16
|
+
*/
|
|
17
|
+
const uploadModifiedSkins = async (td, aliases, options) => {
|
|
18
|
+
try {
|
|
19
|
+
await td.login();
|
|
20
|
+
|
|
21
|
+
for (let alias of aliases) {
|
|
22
|
+
console.log(chalk.green(`\nNow processing alias "${alias}"...`));
|
|
23
|
+
const localSkinFiles = getLocalSkins(alias, options.backup);
|
|
24
|
+
console.log(
|
|
25
|
+
chalk.yellow(`${alias} has ${localSkinFiles.length} local skins. `) +
|
|
26
|
+
chalk.red(`${localSkinFiles.length === 0 ? "Got nothing to do!" : ""}`),
|
|
27
|
+
);
|
|
28
|
+
if (options.skin) console.log(chalk.yellow(`Filtering skins for ${options.skin}.`));
|
|
29
|
+
|
|
30
|
+
const modSkins = await td.getModifiedSkins(alias);
|
|
31
|
+
const currentSkins = new Set(modSkins.map((el) => el.name));
|
|
32
|
+
const aliasSkinDir = aliasSkinFolder(alias, options.backup);
|
|
33
|
+
|
|
34
|
+
for (let skinFile of localSkinFiles) {
|
|
35
|
+
if (!skinFile.endsWith(".skin")) continue;
|
|
36
|
+
if (options.skin && !options.skin.test(skinFile)) continue;
|
|
37
|
+
|
|
38
|
+
const skin = skinRegister.getData(skinFile);
|
|
39
|
+
skin.content = readFileSync(resolve(aliasSkinDir, skinFile))
|
|
40
|
+
.toString()
|
|
41
|
+
.replace(/\r\n/g, "\n");
|
|
42
|
+
|
|
43
|
+
console.log(chalk.blue(`Now updating skin ${skin.name} (len=${skin.content.length}).`));
|
|
44
|
+
await td.updateSkin(alias, skin.name, {
|
|
45
|
+
title: skin.title,
|
|
46
|
+
description: skin.description,
|
|
47
|
+
skin: skin.content,
|
|
48
|
+
diff: options.debug,
|
|
49
|
+
});
|
|
50
|
+
console.log(chalk.green(`Update request completed for skin: ${skin.name}`));
|
|
51
|
+
|
|
52
|
+
if (currentSkins.has(skin.name)) currentSkins.delete(skin.name);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (currentSkins.size > 0) {
|
|
56
|
+
console.log(
|
|
57
|
+
chalk.blue(`\nFollowing skins exist on "${alias}" and were not touched by this upload:`),
|
|
58
|
+
);
|
|
59
|
+
currentSkins.forEach((skinName) => console.log(`--> ${chalk.blue(skinName)}`));
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
console.log(chalk.green("Upload completed."));
|
|
63
|
+
} catch (e) {
|
|
64
|
+
console.log(chalk.red(`Error while uploading skins: ${e}`));
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
export { uploadModifiedSkins };
|
package/src/_utils.js
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utility functions
|
|
3
|
+
* =================
|
|
4
|
+
*
|
|
5
|
+
*/
|
|
6
|
+
import { deleteSync } from "del";
|
|
7
|
+
import { existsSync, readFileSync, writeFileSync, readdirSync } from "node:fs";
|
|
8
|
+
import { resolve } from "node:path";
|
|
9
|
+
|
|
10
|
+
// Read centrally stored skin header data (name => title, description)
|
|
11
|
+
const skinsFolder = resolve(process.cwd(), "skins");
|
|
12
|
+
const backupFolder = resolve(process.cwd(), "backup");
|
|
13
|
+
const pathToRegister = resolve(skinsFolder, "skinRegister.json");
|
|
14
|
+
const skinRegister = {
|
|
15
|
+
data: undefined,
|
|
16
|
+
hasChanged: false,
|
|
17
|
+
load() {
|
|
18
|
+
this.data = existsSync(pathToRegister)
|
|
19
|
+
? JSON.parse(readFileSync(pathToRegister).toString())
|
|
20
|
+
: {};
|
|
21
|
+
return this.data;
|
|
22
|
+
},
|
|
23
|
+
store() {
|
|
24
|
+
if (this.hasChanged) {
|
|
25
|
+
writeFileSync(pathToRegister, JSON.stringify(this.data, null, 2));
|
|
26
|
+
this.hasChanged = false;
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
hasData(skinName) {
|
|
30
|
+
if (typeof this.data === "undefined") this.load();
|
|
31
|
+
return this.data.hasOwnProperty(skinName);
|
|
32
|
+
},
|
|
33
|
+
getData(skinFileOrName) {
|
|
34
|
+
const name = skinFileOrName.endsWith(".skin")
|
|
35
|
+
? skinFileOrName.substr(0, skinFileOrName.length - 5)
|
|
36
|
+
: skinFileOrName;
|
|
37
|
+
if (this.hasData(name)) {
|
|
38
|
+
const { title, description } = this.data[name];
|
|
39
|
+
return { name, title, description };
|
|
40
|
+
} else return { name, title: name, description: "" };
|
|
41
|
+
},
|
|
42
|
+
setData(skinName, title, description) {
|
|
43
|
+
if (!this.hasData(skinName)) {
|
|
44
|
+
this.data[skinName] = { title, description };
|
|
45
|
+
this.hasChanged = true;
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
deleteData(skinName) {
|
|
49
|
+
if (this.hasData(skinName)) delete this.data[skinName];
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Returns the path to the local alias skin folder
|
|
55
|
+
* @param {String} alias A single blog alias, e.g. info or help or wiki or any other Twoday alias
|
|
56
|
+
* @param {boolean} isBackup true=argv.backup has been activated
|
|
57
|
+
* @returns {String} path
|
|
58
|
+
*/
|
|
59
|
+
const aliasSkinFolder = (alias, isBackup) => resolve(isBackup ? backupFolder : skinsFolder, alias);
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Cleans the local alias' skin folder, i.e. deletes all skin files in the local folder
|
|
63
|
+
* @param {String} aliases Array of blog aliases
|
|
64
|
+
* @returns {void}
|
|
65
|
+
*/
|
|
66
|
+
const cleanupAliasFolder = (aliases, isBackup) => {
|
|
67
|
+
try {
|
|
68
|
+
deleteSync(aliases.map((alias) => resolve(aliasSkinFolder(alias, isBackup), "*.skin")));
|
|
69
|
+
} catch (err) {
|
|
70
|
+
console.log(`cleanupAliasFolder ended with error: ${err.toString()}.`);
|
|
71
|
+
process.exit(1);
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Returns all files in a given alias directory or an empty array if it doesn't exist
|
|
77
|
+
* @param {String} alias Blog alias
|
|
78
|
+
* @returns {String[]} Array of file strings or []
|
|
79
|
+
*/
|
|
80
|
+
const getLocalSkins = (alias, isBackup) => {
|
|
81
|
+
const aliasSkinDir = aliasSkinFolder(alias, isBackup);
|
|
82
|
+
return existsSync(aliasSkinDir)
|
|
83
|
+
? readdirSync(aliasSkinDir).filter((file) => file.endsWith(".skin"))
|
|
84
|
+
: [];
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Splits an alias param string based on a given delimiter
|
|
89
|
+
* @param {String} aliases, e.g. "info,foundation" or "foundation:info"
|
|
90
|
+
* @param {String} delimiter, e.g. "," or ":"
|
|
91
|
+
* @returns {String[]} Array of trimmed, lowercased strings
|
|
92
|
+
*/
|
|
93
|
+
const splitAliases = (aliases, delimiter) => {
|
|
94
|
+
return aliases
|
|
95
|
+
.split(delimiter)
|
|
96
|
+
.filter((alias) => alias.trim().length)
|
|
97
|
+
.map((alias) => alias.toLowerCase());
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
export {
|
|
101
|
+
pathToRegister,
|
|
102
|
+
skinRegister,
|
|
103
|
+
aliasSkinFolder,
|
|
104
|
+
cleanupAliasFolder,
|
|
105
|
+
getLocalSkins,
|
|
106
|
+
splitAliases,
|
|
107
|
+
};
|