@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 ADDED
@@ -0,0 +1,2 @@
1
+ USER=someUserIDWithAdminRights
2
+ PASSWORD=somePassword
package/CHANGELOG.md ADDED
@@ -0,0 +1,6 @@
1
+ # Changelog
2
+
3
+ ## 1.1.0 (31.03.2026)
4
+ - Migrate to Vite+
5
+ - Swap Ava for Vitest
6
+ - Generate minified dist js file
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
@@ -0,0 +1,3 @@
1
+ # moveskins
2
+
3
+ Download/Upload/Compare modified skins for selected aliases on Twoday
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
+ }
@@ -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 };
@@ -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
+ };