uvako-cli 0.4.0 → 0.6.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/README.md +37 -1
- package/bin/uvako-cli.js +12 -7
- package/package.json +9 -3
- package/src/api.js +7 -4
- package/src/app.js +412 -0
- package/src/check.js +78 -4
- package/src/config.js +44 -2
- package/src/files.js +2 -1
- package/src/init.js +97 -6
- package/src/locales/en.js +165 -12
- package/src/main.js +76 -20
- package/src/partner.js +135 -0
- package/src/remote.js +80 -7
- package/src/rules.generated.js +452 -0
- package/src/sections.js +333 -0
- package/src/svg.js +152 -0
- package/templates/app/README.md +65 -0
- package/templates/app/blocks/yildiz-puani/block.css +5 -0
- package/templates/app/blocks/yildiz-puani/block.html +8 -0
- package/templates/app/blocks/yildiz-puani/block.json +11 -0
- package/templates/app/server.js +357 -0
package/src/main.js
CHANGED
|
@@ -6,39 +6,56 @@ import { checkTheme, printReport } from "./check.js";
|
|
|
6
6
|
import { session, siteOrigin } from "./config.js";
|
|
7
7
|
import { initTheme } from "./init.js";
|
|
8
8
|
import { packageTheme } from "./package.js";
|
|
9
|
-
import { dev, list, login, logout, pull, push, remove, submit, whoami } from "./remote.js";
|
|
10
|
-
import {
|
|
9
|
+
import { dev, list, login, logout, pull, push, remoteCheck, remove, submit, whoami } from "./remote.js";
|
|
10
|
+
import { appCommand } from "./app.js";
|
|
11
|
+
import { partnerLogin, partnerLogout, partnerWhoami } from "./partner.js";
|
|
12
|
+
import { getLocale, t, tm } from "./i18n.js";
|
|
11
13
|
import { help as helpEn } from "./locales/en.js";
|
|
12
14
|
|
|
13
15
|
const version = JSON.parse(readFileSync(path.join(path.dirname(fileURLToPath(import.meta.url)), "..", "package.json"), "utf8")).version;
|
|
14
16
|
|
|
15
|
-
const helpTr = `uvako-cli ${version} — Uvako tema geliştirme aracı
|
|
17
|
+
const helpTr = `uvako-cli ${version} — Uvako tema ve uygulama geliştirme aracı
|
|
16
18
|
https://uvako.com
|
|
17
19
|
|
|
18
20
|
Giriş:
|
|
21
|
+
uvako login Partner hesabıyla giriş (tarayıcıda onaylanır; uygulama komutları)
|
|
19
22
|
uvako login --store <mağaza> Mağazaya tema erişim anahtarıyla bağlanır (anahtar sorulur)
|
|
20
|
-
uvako logout [--store <mağaza>]
|
|
21
|
-
uvako whoami Bağlı mağazayı gösterir
|
|
23
|
+
uvako logout [--store <mağaza>] [--partner] Kayıtlı girişi siler
|
|
24
|
+
uvako whoami Bağlı mağazayı ve partner girişini gösterir
|
|
25
|
+
|
|
26
|
+
Uygulama (herkese açık uygulamalar, partner girişiyle):
|
|
27
|
+
uvako app init <ad> [--dir klasör] [--offline]
|
|
28
|
+
Örnek uygulama (OAuth kurulumu, imzalı webhook'lar) ve uvako.app.json
|
|
29
|
+
uvako app dev [klasör] --store <mağaza> [--port 3000] [--no-serve] [--no-update]
|
|
30
|
+
Uygulamayı yerelde çalıştırır; geliştirme mağazası yerel adrese bağlanır,
|
|
31
|
+
o mağazanın webhook'ları bilgisayarınızdaki alıcıya aktarılır
|
|
32
|
+
uvako app deploy [klasör] [--submit] uvako.app.json'u yeni sürüm olarak dağıtır (önce yalnızca kendi mağazalarınızda)
|
|
33
|
+
uvako app submit [klasör] Uygulamayı herkese açılması için Uvako incelemesine gönderir
|
|
34
|
+
uvako app list [--json] Kayıtlı uygulamalarınızı listeler
|
|
22
35
|
|
|
23
36
|
Tema:
|
|
24
|
-
uvako theme init <anahtar> [--base linea] [--name "Ad"] [--author "Yazar"]
|
|
37
|
+
uvako theme init <anahtar> [--base linea] [--name "Ad"] [--author "Yazar"] [--engine sections]
|
|
25
38
|
Bir katalog temasını temel alan yeni tema iskeleti
|
|
26
|
-
|
|
27
|
-
uvako theme
|
|
39
|
+
--engine sections: kendi bölümlerini getiren tema (örnek bölümlerle)
|
|
40
|
+
uvako theme check [klasör] [--offline] Temayı denetler (yapı, ayarlar, güvenlik, bölüm şablonları);
|
|
41
|
+
giriş yapılmışsa sunucu da aynı kurallarla denetler
|
|
42
|
+
uvako theme dev [klasör] Canlı geliştirme: yalnızca değişen dosyayı yükler, önizleme yenilenir
|
|
28
43
|
Durdurunca mağaza eski hâline döner (--keep: kütüphanede bırak)
|
|
29
|
-
uvako theme push [klasör]
|
|
30
|
-
|
|
44
|
+
uvako theme push [klasör] [--force] Temayı mağazanın tema kütüphanesine yükler (SHA-256 ve revizyonla;
|
|
45
|
+
paket sizden sonra değiştiyse reddedilir, --force üzerine yazar)
|
|
46
|
+
uvako theme pull <anahtar> [--dir klasör] Yüklediğiniz temayı indirir (sections/, snippets/, templates/ dahil)
|
|
31
47
|
uvako theme list Mağazadaki temaları ve yüklediğiniz paketleri listeler
|
|
32
48
|
uvako theme submit <anahtar> Temayı themes.uvako.com için incelemeye gönderir
|
|
33
49
|
uvako theme delete <anahtar> --yes Yüklediğiniz paketi siler
|
|
34
50
|
uvako theme package [klasör] [--out x.zip] Denetleyip zip paketi üretir
|
|
35
51
|
uvako version Sürümü yazar
|
|
36
52
|
|
|
37
|
-
Tema, kod çalıştırmayan bir pakettir: ayarlar,
|
|
38
|
-
|
|
39
|
-
|
|
53
|
+
Tema, kod çalıştırmayan bir pakettir: ayarlar, sayfa şablonları, SCSS ve görseller. "engine": "sections"
|
|
54
|
+
temaları kendi bölümlerini (sections/*.html.tmpl) getirebilir; bunlar yalnızca sunucuda, kısıtlı bir
|
|
55
|
+
şablon dilinde çizilir. JavaScript ya da sunucu kodu içeren paketler reddedilir. Yüklenen tema yalnızca
|
|
56
|
+
kendi mağazanızda görünür; tema mağazasında yayınlanması Uvako incelemesine bağlıdır.
|
|
40
57
|
|
|
41
|
-
CI için: UVAKO_STORE ve UVAKO_TOKEN ortam değişkenleri girişin yerini tutar.
|
|
58
|
+
CI için: UVAKO_STORE ve UVAKO_TOKEN (tema), UVAKO_PARTNER_TOKEN (uygulama) ortam değişkenleri girişin yerini tutar.
|
|
42
59
|
Dil: --lang en ya da UVAKO_LANG=en çıktıyı İngilizce verir.
|
|
43
60
|
|
|
44
61
|
Uvako — https://uvako.com
|
|
@@ -93,9 +110,31 @@ export async function main(argv) {
|
|
|
93
110
|
console.log(version);
|
|
94
111
|
return 0;
|
|
95
112
|
}
|
|
96
|
-
|
|
97
|
-
if (group === "
|
|
98
|
-
if (group === "
|
|
113
|
+
// Tek giriş: mağaza belirtilmezse partner hesabıyla (uygulama komutları), belirtilirse tema anahtarıyla.
|
|
114
|
+
if (group === "login") return options.store ? login(options) : partnerLogin(options);
|
|
115
|
+
if (group === "logout") {
|
|
116
|
+
if (options.partner) return partnerLogout();
|
|
117
|
+
const code = await logout(options);
|
|
118
|
+
if (options.all) await partnerLogout();
|
|
119
|
+
return code;
|
|
120
|
+
}
|
|
121
|
+
if (group === "whoami") {
|
|
122
|
+
const partner = await partnerWhoami();
|
|
123
|
+
if (partner && !options.store && !process.env.UVAKO_STORE) {
|
|
124
|
+
try {
|
|
125
|
+
session("");
|
|
126
|
+
} catch {
|
|
127
|
+
return 0;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return whoami(options);
|
|
131
|
+
}
|
|
132
|
+
if (group === "app") {
|
|
133
|
+
const code = await appCommand(command, target, options);
|
|
134
|
+
if (code !== null) return code;
|
|
135
|
+
console.error(t("Bilinmeyen uygulama komutu: {0}\n\n{1}", command ?? "(yok)", help));
|
|
136
|
+
return 1;
|
|
137
|
+
}
|
|
99
138
|
if (group !== "theme") {
|
|
100
139
|
console.error(t("Bilinmeyen komut: {0}\n\n{1}", group, help));
|
|
101
140
|
return 1;
|
|
@@ -103,16 +142,33 @@ export async function main(argv) {
|
|
|
103
142
|
if (command === "init") {
|
|
104
143
|
if (!target) throw new Error(t("Tema anahtarı gerekli: uvako theme init <anahtar>"));
|
|
105
144
|
const base = typeof options.base === "string" ? options.base : "linea";
|
|
106
|
-
const
|
|
145
|
+
const engine = typeof options.engine === "string" ? options.engine : "";
|
|
146
|
+
const dir = initTheme(target, { name: options.name, author: options.author, cwd: process.cwd(), base, engine, starter: await fetchStarter(base, options) });
|
|
107
147
|
const rel = path.relative(process.cwd(), dir) || ".";
|
|
108
148
|
console.log(t("Tema oluşturuldu: {0} (temel: {1})", rel, base));
|
|
109
149
|
console.log(t("Sıradaki adım: uvako login --store <mağaza>, sonra: uvako theme dev {0}", rel));
|
|
110
150
|
return 0;
|
|
111
151
|
}
|
|
112
152
|
if (command === "check") {
|
|
113
|
-
const
|
|
153
|
+
const dir = path.resolve(target ?? ".");
|
|
154
|
+
const report = checkTheme(dir);
|
|
114
155
|
printReport(report);
|
|
115
|
-
|
|
156
|
+
if (report.errors.length) return 1;
|
|
157
|
+
// Giriş yapılmışsa sunucu da aynı paketi kuru olarak denetler: yerelde geçen sunucuda da geçer.
|
|
158
|
+
if (options.offline || report.layout !== "package") return 0;
|
|
159
|
+
const remote = await remoteCheck(dir, options);
|
|
160
|
+
if (!remote) {
|
|
161
|
+
console.log(t("Sunucu denetimi için giriş yapın (uvako login --store <mağaza>) ya da --offline ekleyin."));
|
|
162
|
+
return 0;
|
|
163
|
+
}
|
|
164
|
+
if (!remote.ok) {
|
|
165
|
+
console.error(t("✖ Sunucu denetimi ({0}): {1}", remote.origin, tm(remote.message)));
|
|
166
|
+
for (const problem of remote.problems) console.error(` ✖ ${tm(problem)}`);
|
|
167
|
+
return 1;
|
|
168
|
+
}
|
|
169
|
+
for (const warning of remote.warnings) console.warn(` ! ${tm(warning)}`);
|
|
170
|
+
console.log(t("Sunucu denetimi ({0}): geçti.", remote.origin));
|
|
171
|
+
return 0;
|
|
116
172
|
}
|
|
117
173
|
if (command === "package") {
|
|
118
174
|
const result = packageTheme(path.resolve(target ?? "."), { out: typeof options.out === "string" ? options.out : undefined });
|
package/src/partner.js
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import { dropPartner, partnerOrigin, partnerSaved, partnerSession, savePartner } from "./config.js";
|
|
4
|
+
import { ApiError } from "./api.js";
|
|
5
|
+
import { t, tm } from "./i18n.js";
|
|
6
|
+
|
|
7
|
+
// Partner paneliyle konuşan istemci (/api/partner/cli). Giriş cihaz akışıyla yapılır: CLI bir kod
|
|
8
|
+
// alır, partner tarayıcıda partner paneline (şifre + iki adımlı doğrulama) girip onaylar, CLI uvp_
|
|
9
|
+
// anahtarını alır. Anahtar ekrana yazılmaz, yalnızca partner adresine gönderilir; yönlendirme izlenmez.
|
|
10
|
+
|
|
11
|
+
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
12
|
+
|
|
13
|
+
async function request(origin, method, route, { token, body, timeout = 60000 } = {}) {
|
|
14
|
+
let response;
|
|
15
|
+
try {
|
|
16
|
+
response = await fetch(`${origin}/api/partner/cli${route}`, {
|
|
17
|
+
method,
|
|
18
|
+
redirect: "error",
|
|
19
|
+
headers: { Accept: "application/json", "User-Agent": "uvako-cli", ...(token ? { Authorization: `Bearer ${token}` } : {}), ...(body !== undefined ? { "Content-Type": "application/json" } : {}) },
|
|
20
|
+
body: body !== undefined ? JSON.stringify(body) : undefined,
|
|
21
|
+
signal: AbortSignal.timeout(timeout),
|
|
22
|
+
});
|
|
23
|
+
} catch (error) {
|
|
24
|
+
throw new ApiError(t("Partner paneline bağlanılamadı ({0}): {1}", origin, error.cause?.code ?? error.message));
|
|
25
|
+
}
|
|
26
|
+
let payload = null;
|
|
27
|
+
try {
|
|
28
|
+
payload = await response.json();
|
|
29
|
+
} catch {
|
|
30
|
+
// JSON olmayan yanıt (ör. ağ geçidi hatası).
|
|
31
|
+
}
|
|
32
|
+
if (response.ok) return payload;
|
|
33
|
+
// Cihaz girişinin RFC 8628 yanıtları {"error": "kod", "error_description": "…"} biçimindedir.
|
|
34
|
+
if (typeof payload?.error === "string") throw new ApiError(payload.error_description || payload.error, { status: response.status, code: payload.error });
|
|
35
|
+
const problem = payload?.error ?? {};
|
|
36
|
+
const fallback = response.status === 404 ? t("Bu adreste Uvako partner uç noktası yok (UVAKO_PARTNER_URL?).") : t("Sunucu {0} yanıtı verdi.", response.status);
|
|
37
|
+
throw new ApiError(problem.message || fallback, { status: response.status, code: problem.field || "", problems: [] });
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Giriş yapmış partner olarak uç noktayı çağırır. */
|
|
41
|
+
export function partnerCall(method, route, options = {}) {
|
|
42
|
+
const current = partnerSession();
|
|
43
|
+
return request(current.origin, method, route, { ...options, token: current.token });
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function explainPartner(error) {
|
|
47
|
+
if (!(error instanceof ApiError)) throw error;
|
|
48
|
+
console.error(`✖ ${tm(error.message)}${error.status === 422 && error.code ? ` (${error.code})` : ""}`);
|
|
49
|
+
if (error.status === 401) console.error(t(" Yeniden giriş yapın: uvako login"));
|
|
50
|
+
return 1;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function openBrowser(url) {
|
|
54
|
+
const [command, args] = process.platform === "win32" ? ["cmd", ["/c", "start", "", url]] : process.platform === "darwin" ? ["open", [url]] : ["xdg-open", [url]];
|
|
55
|
+
try {
|
|
56
|
+
const child = spawn(command, args, { stdio: "ignore", detached: true, windowsHide: true });
|
|
57
|
+
child.on("error", () => undefined);
|
|
58
|
+
child.unref();
|
|
59
|
+
} catch {
|
|
60
|
+
// Tarayıcı açılamazsa adres zaten ekranda.
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** `uvako login` (mağaza belirtilmeden): partner hesabıyla cihaz girişi. */
|
|
65
|
+
export async function partnerLogin(options) {
|
|
66
|
+
const origin = partnerOrigin();
|
|
67
|
+
let device;
|
|
68
|
+
try {
|
|
69
|
+
device = await request(origin, "POST", "/device", { body: { label: `uvako-cli · ${os.hostname()}`.slice(0, 80) } });
|
|
70
|
+
} catch (error) {
|
|
71
|
+
return explainPartner(error);
|
|
72
|
+
}
|
|
73
|
+
// Sunucunun verdiği adres yalnızca partner panelinin kendi alan adındaysa açılır (yerel geliştirmede
|
|
74
|
+
// panel ile API farklı portlarda olabilir; şifresiz bağlantı yalnızca yerel adreste kabul edilir).
|
|
75
|
+
const verify = new URL(device.verification_uri_complete);
|
|
76
|
+
const api = new URL(origin);
|
|
77
|
+
const sameHost = verify.hostname === api.hostname && (verify.protocol === "https:" || (verify.protocol === "http:" && api.protocol === "http:"));
|
|
78
|
+
if (!sameHost) throw new Error(t("Sunucu beklenmeyen bir doğrulama adresi verdi: {0}", verify.origin));
|
|
79
|
+
console.log(t("Partner paneline giriş: tarayıcıda açılan sayfada kodu onaylayın."));
|
|
80
|
+
console.log(t(" Adres: {0}", verify.href));
|
|
81
|
+
console.log(t(" Kod: {0}\n", device.user_code));
|
|
82
|
+
if (!options["no-browser"]) openBrowser(verify.href);
|
|
83
|
+
let interval = Math.max(1, Number(device.interval) || 3) * 1000;
|
|
84
|
+
const deadline = Date.now() + (Number(device.expires_in) || 600) * 1000;
|
|
85
|
+
while (Date.now() < deadline) {
|
|
86
|
+
await sleep(interval);
|
|
87
|
+
try {
|
|
88
|
+
const result = await request(origin, "POST", "/token", { body: { device_code: device.device_code } });
|
|
89
|
+
const me = await request(origin, "GET", "/me", { token: result.access_token });
|
|
90
|
+
const name = `${me.member?.firstName ?? ""} ${me.member?.lastName ?? ""}`.trim();
|
|
91
|
+
savePartner(origin, result.access_token, me.company?.name ?? "", me.member?.email ?? "");
|
|
92
|
+
console.log(t("Giriş yapıldı: {0} ({1}) — {2}", name || me.member?.email, me.company?.name ?? "", origin));
|
|
93
|
+
if (me.company?.status !== "approved") console.warn(t("! Partner başvurunuz henüz onaylanmadı; uygulama kaydı onaydan sonra açılır."));
|
|
94
|
+
return 0;
|
|
95
|
+
} catch (error) {
|
|
96
|
+
if (!(error instanceof ApiError)) throw error;
|
|
97
|
+
if (error.code === "authorization_pending") continue;
|
|
98
|
+
if (error.code === "slow_down") {
|
|
99
|
+
interval += 2000;
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
return explainPartner(error);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
console.error(t("✖ Giriş kodunun süresi doldu. `uvako login` komutunu yeniden çalıştırın."));
|
|
106
|
+
return 1;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export async function partnerLogout() {
|
|
110
|
+
const saved = partnerSaved();
|
|
111
|
+
if (!saved) {
|
|
112
|
+
console.log(t("Kayıtlı partner girişi yok."));
|
|
113
|
+
return 0;
|
|
114
|
+
}
|
|
115
|
+
try {
|
|
116
|
+
await partnerCall("POST", "/logout", { body: {} });
|
|
117
|
+
} catch {
|
|
118
|
+
// Sunucuya ulaşılamasa da yerel giriş silinir; anahtar partner panelinden de kapatılabilir.
|
|
119
|
+
}
|
|
120
|
+
dropPartner();
|
|
121
|
+
console.log(t("Partner girişinden çıkıldı: {0}", saved.origin));
|
|
122
|
+
return 0;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export async function partnerWhoami() {
|
|
126
|
+
if (!partnerSaved() && !process.env.UVAKO_PARTNER_TOKEN) return false;
|
|
127
|
+
try {
|
|
128
|
+
const me = await partnerCall("GET", "/me");
|
|
129
|
+
console.log(t("Partner: {0} — {1} ({2})", me.company?.name ?? "", me.member?.email ?? "", partnerSession().origin));
|
|
130
|
+
} catch (error) {
|
|
131
|
+
if (error instanceof ApiError) console.error(t("Partner: {0}", tm(error.message)));
|
|
132
|
+
else throw error;
|
|
133
|
+
}
|
|
134
|
+
return true;
|
|
135
|
+
}
|
package/src/remote.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, readFileSync, watch, writeFileSync } from "node:fs";
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, statSync, watch, writeFileSync } from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { ApiError, call } from "./api.js";
|
|
4
|
-
import { printReport } from "./check.js";
|
|
4
|
+
import { checkTheme, localOnly, printReport } from "./check.js";
|
|
5
5
|
import { dropSession, saveSession, session, sessions, storeOrigin } from "./config.js";
|
|
6
6
|
import { buildArchive } from "./package.js";
|
|
7
7
|
import { unzip } from "./zip.js";
|
|
@@ -50,9 +50,31 @@ function explain(error) {
|
|
|
50
50
|
console.error(`✖ ${tm(error.message)}`);
|
|
51
51
|
for (const problem of error.problems) console.error(` ✖ ${tm(problem)}`);
|
|
52
52
|
if (error.status === 401) console.error(t(" Yeni anahtar: mağaza paneli → Ayarlar → Temalar → Geliştirici erişimi, sonra `uvako login`."));
|
|
53
|
+
if (error.status === 429) console.error(t(" Yükleme sınırı aşıldı. {0} saniye sonra tekrar deneyin.", error.retryAfter || 5));
|
|
54
|
+
if (error.code === "conflict") console.error(t(" Sunucudaki sürümü almak için: uvako theme pull <anahtar> --force. Üzerine yazmak için: uvako theme push --force"));
|
|
53
55
|
return 1;
|
|
54
56
|
}
|
|
55
57
|
|
|
58
|
+
// Yerel durum: son push/pull'un revizyonu. Push bu revizyonu gönderir; paket arada başka biri
|
|
59
|
+
// tarafından değiştirildiyse sunucu yüklemeyi reddeder (409). Dosya pakete girmez.
|
|
60
|
+
function readState(dir) {
|
|
61
|
+
try {
|
|
62
|
+
return JSON.parse(readFileSync(path.join(dir, ".uvako", "state.json"), "utf8"));
|
|
63
|
+
} catch {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function writeState(dir, state) {
|
|
69
|
+
mkdirSync(path.join(dir, ".uvako"), { recursive: true });
|
|
70
|
+
writeFileSync(path.join(dir, ".uvako", "state.json"), `${JSON.stringify(state, null, 2)}\n`);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function baseRevision(dir, current, key, force) {
|
|
74
|
+
const state = readState(dir);
|
|
75
|
+
return !force && state && state.origin === current.origin && state.key === key && Number.isInteger(state.revision) ? { "X-Uvako-Base-Revision": String(state.revision) } : {};
|
|
76
|
+
}
|
|
77
|
+
|
|
56
78
|
const previewUrl = (origin, installationId) => `${origin}/?preview=${installationId}`;
|
|
57
79
|
const editorUrl = (origin, installationId) => `${origin}/admin/ayarlar/temalar/${installationId}/editor`;
|
|
58
80
|
|
|
@@ -115,23 +137,46 @@ export async function list(options) {
|
|
|
115
137
|
}
|
|
116
138
|
}
|
|
117
139
|
|
|
118
|
-
async function upload(current, dir, quiet = false) {
|
|
140
|
+
async function upload(current, dir, quiet = false, force = false) {
|
|
119
141
|
const built = buildArchive(dir);
|
|
120
142
|
if (!built.ok) {
|
|
121
143
|
printReport(built.report);
|
|
122
144
|
return null;
|
|
123
145
|
}
|
|
124
146
|
if (!quiet) printReport(built.report);
|
|
125
|
-
|
|
147
|
+
// SHA-256: paket yolda bozulursa sunucu reddeder. Revizyon: başkasının yüklemesinin üstüne yazılmaz.
|
|
148
|
+
const headers = { "X-Uvako-Package-Sha256": built.sha256, ...baseRevision(dir, current, built.meta.key, force) };
|
|
149
|
+
const result = await call(current, "PUT", "/themes", { body: built.buffer, headers });
|
|
150
|
+
writeState(dir, { origin: current.origin, key: result.package.key, revision: result.package.revision });
|
|
126
151
|
for (const warning of result.warnings ?? []) console.warn(` ! ${warning}`);
|
|
127
152
|
return result;
|
|
128
153
|
}
|
|
129
154
|
|
|
155
|
+
/** Kuru denetim: sunucu paketi yükleme kurallarıyla denetler, hiçbir şey kaydetmez. */
|
|
156
|
+
export async function remoteCheck(dir, options) {
|
|
157
|
+
let current;
|
|
158
|
+
try {
|
|
159
|
+
current = session(typeof options.store === "string" ? options.store : "");
|
|
160
|
+
} catch {
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
const built = buildArchive(dir);
|
|
164
|
+
if (!built.ok) return null;
|
|
165
|
+
try {
|
|
166
|
+
const result = await call(current, "POST", "/themes/check", { body: built.buffer });
|
|
167
|
+
return { ok: true, origin: current.origin, warnings: result.warnings ?? [] };
|
|
168
|
+
} catch (error) {
|
|
169
|
+
if (!(error instanceof ApiError)) throw error;
|
|
170
|
+
if (error.status === 422 || error.status === 403) return { ok: false, origin: current.origin, message: error.message, problems: error.problems };
|
|
171
|
+
return { ok: true, origin: current.origin, warnings: [t("Sunucu denetimi yapılamadı: {0}", tm(error.message))] };
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
130
175
|
export async function push(target, options) {
|
|
131
176
|
const dir = path.resolve(target ?? ".");
|
|
132
177
|
try {
|
|
133
178
|
const current = session(typeof options.store === "string" ? options.store : "");
|
|
134
|
-
const result = await upload(current, dir);
|
|
179
|
+
const result = await upload(current, dir, false, Boolean(options.force));
|
|
135
180
|
if (!result) {
|
|
136
181
|
console.error(t("Yüklenmedi: önce hataları düzeltin."));
|
|
137
182
|
return 1;
|
|
@@ -185,7 +230,29 @@ export async function dev(target, options) {
|
|
|
185
230
|
let timer = null;
|
|
186
231
|
let running = false;
|
|
187
232
|
let again = false;
|
|
233
|
+
const changed = new Set();
|
|
188
234
|
const stamp = () => new Date().toLocaleTimeString("tr-TR");
|
|
235
|
+
// Yalnızca değişen dosyalar gönderilir; sunucu paketin tamamını yine aynı kurallarla denetler.
|
|
236
|
+
const sendChanges = async () => {
|
|
237
|
+
const files = [...changed];
|
|
238
|
+
changed.clear();
|
|
239
|
+
const report = checkTheme(dir);
|
|
240
|
+
if (report.errors.length) {
|
|
241
|
+
printReport(report);
|
|
242
|
+
return null;
|
|
243
|
+
}
|
|
244
|
+
if (files.length > 10 || files.includes("theme.json")) return upload(current, dir, true);
|
|
245
|
+
let result = null;
|
|
246
|
+
for (const rel of files) {
|
|
247
|
+
const full = path.join(dir, rel);
|
|
248
|
+
const exists = existsSync(full) && statSync(full).isFile();
|
|
249
|
+
const route = `/themes/${encodeURIComponent(first.package.key)}/files/${rel.split("/").map(encodeURIComponent).join("/")}`;
|
|
250
|
+
const headers = baseRevision(dir, current, first.package.key, false);
|
|
251
|
+
result = await call(current, exists ? "PUT" : "DELETE", route, exists ? { body: readFileSync(full), headers, contentType: "application/octet-stream" } : { headers });
|
|
252
|
+
writeState(dir, { origin: current.origin, key: result.package.key, revision: result.package.revision });
|
|
253
|
+
}
|
|
254
|
+
return result;
|
|
255
|
+
};
|
|
189
256
|
const sync = async () => {
|
|
190
257
|
if (running) {
|
|
191
258
|
again = true;
|
|
@@ -193,13 +260,14 @@ export async function dev(target, options) {
|
|
|
193
260
|
}
|
|
194
261
|
running = true;
|
|
195
262
|
try {
|
|
196
|
-
const result = await
|
|
263
|
+
const result = await sendChanges();
|
|
197
264
|
if (result) console.log(t("[{0}] yüklendi (sürüm no {1})", stamp(), result.package.revision));
|
|
198
265
|
else console.error(t("[{0}] yüklenmedi: hataları düzeltip kaydedin.", stamp()));
|
|
199
266
|
} catch (error) {
|
|
200
267
|
if (error instanceof ApiError) {
|
|
201
268
|
console.error(`[${stamp()}] ✖ ${tm(error.message)}`);
|
|
202
269
|
for (const problem of error.problems) console.error(` ✖ ${tm(problem)}`);
|
|
270
|
+
if (error.status === 429) console.error(t(" Yükleme sınırı aşıldı. {0} saniye sonra tekrar deneyin.", error.retryAfter || 5));
|
|
203
271
|
if (error.status === 401 || error.status === 403) process.exit(1);
|
|
204
272
|
} else {
|
|
205
273
|
console.error(`[${stamp()}] ✖ ${tm(error.message)}`);
|
|
@@ -214,7 +282,9 @@ export async function dev(target, options) {
|
|
|
214
282
|
};
|
|
215
283
|
const watcher = watch(dir, { recursive: true }, (_event, file) => {
|
|
216
284
|
const rel = String(file ?? "").replaceAll("\\", "/");
|
|
217
|
-
if (!rel || rel
|
|
285
|
+
if (!rel || localOnly(rel) || rel.startsWith(".git/") || rel.includes("node_modules/") || rel.endsWith("~") || rel.split("/").some((part) => part.startsWith("."))) return;
|
|
286
|
+
if (existsSync(path.join(dir, rel)) && statSync(path.join(dir, rel)).isDirectory()) return;
|
|
287
|
+
changed.add(rel);
|
|
218
288
|
clearTimeout(timer);
|
|
219
289
|
timer = setTimeout(() => void sync(), 300);
|
|
220
290
|
});
|
|
@@ -264,6 +334,9 @@ export async function pull(key, options) {
|
|
|
264
334
|
writeFileSync(file, entry.data);
|
|
265
335
|
count++;
|
|
266
336
|
}
|
|
337
|
+
const state = await call(current, "GET", "/themes");
|
|
338
|
+
const pkg = (state.packages ?? []).find((item) => item.key === key);
|
|
339
|
+
if (pkg) writeState(dir, { origin: current.origin, key, revision: pkg.revision });
|
|
267
340
|
console.log(t("İndirildi: {0} ({1} dosya)", path.relative(process.cwd(), dir) || ".", count));
|
|
268
341
|
return 0;
|
|
269
342
|
} catch (error) {
|