uvako-cli 0.3.0 → 0.5.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 +65 -2
- package/bin/uvako-cli.js +12 -7
- package/package.json +4 -3
- package/src/api.js +6 -5
- package/src/app.js +412 -0
- package/src/check.js +127 -67
- package/src/config.js +50 -7
- package/src/i18n.js +67 -0
- package/src/init.js +74 -17
- package/src/locales/en.js +311 -0
- package/src/main.js +56 -16
- package/src/package.js +3 -2
- package/src/partner.js +135 -0
- package/src/remote.js +51 -50
- package/src/zip.js +5 -4
- package/templates/app/README.md +39 -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/README.md
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# uvako-cli
|
|
2
2
|
|
|
3
|
-
Uvako
|
|
3
|
+
Uvako için tema ve uygulama geliştirme aracı. Temayı başlatır, denetler, mağazanıza yükler ve kaydettikçe
|
|
4
|
+
canlı önizletir. Herkese açık uygulamayı başlatır, yerelde çalıştırıp webhook'ları bilgisayarınıza aktarır
|
|
5
|
+
ve dağıtır.
|
|
4
6
|
|
|
5
7
|
**[Uvako](https://uvako.com)** — Rox Software ürünü.
|
|
6
8
|
|
|
@@ -44,7 +46,7 @@ paneline giriş yapmış olmanız gerekir. Tema, panelden yayınlanana kadar vit
|
|
|
44
46
|
| `uvako login --store <mağaza>` | Mağazaya tema erişim anahtarıyla bağlanır. |
|
|
45
47
|
| `uvako logout` · `uvako whoami` | Kayıtlı girişi siler · bağlı mağazayı gösterir. |
|
|
46
48
|
| `uvako theme init <anahtar>` | Yeni tema iskeleti (`--base`, `--name`, `--author`). |
|
|
47
|
-
| `uvako theme check [klasör]` | Temayı denetler: yapı, ayarlar, bölümler, görseller, güvenlik. |
|
|
49
|
+
| `uvako theme check [klasör]` | Temayı denetler: yapı, ayarlar, bölümler, görseller, çeviriler, güvenlik. |
|
|
48
50
|
| `uvako theme dev [klasör]` | Canlı geliştirme: kaydettikçe yükler, önizleme yenilenir. Durdurunca mağaza eski hâline döner (`--keep` ile kütüphanede kalır). |
|
|
49
51
|
| `uvako theme push [klasör]` | Temayı mağazanın tema kütüphanesine yükler. |
|
|
50
52
|
| `uvako theme pull <anahtar>` | Yüklediğiniz temayı indirir (`--dir`, `--force`). |
|
|
@@ -57,6 +59,35 @@ paneline giriş yapmış olmanız gerekir. Tema, panelden yayınlanana kadar vit
|
|
|
57
59
|
`uvako-cli` aynı komutların uzun adıdır. Hata varsa çıkış kodu 1'dir. CI için `UVAKO_STORE` ve
|
|
58
60
|
`UVAKO_TOKEN` ortam değişkenleri girişin yerini tutar.
|
|
59
61
|
|
|
62
|
+
## Uygulamalar
|
|
63
|
+
|
|
64
|
+
Uvako partnerleri birden çok mağazaya kurulabilen uygulama yazar. Mağaza uygulamayı panelindeki onay
|
|
65
|
+
ekranından kurar (OAuth); uygulama yalnızca onaylanan kapsamlarla çalışır.
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
uvako login # partner hesabı: tarayıcıda partner panelinden onaylanır
|
|
69
|
+
uvako app init stok-senkron # örnek uygulama + uvako.app.json, Uvako'ya kaydeder
|
|
70
|
+
cd stok-senkron
|
|
71
|
+
uvako app dev --store <geliştirme mağazası>
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
`app dev` örnek uygulamayı client id ve secret ile başlatır, geliştirme mağazanızda uygulamanın adresini
|
|
75
|
+
`http://localhost:3000` yapar, o mağazanın webhook'larını bilgisayarınızdaki alıcıya aktarır (imzasıyla) ve
|
|
76
|
+
kurulum bağlantısını yazar. Tünel gerekmez.
|
|
77
|
+
|
|
78
|
+
| Komut | Ne yapar |
|
|
79
|
+
|---|---|
|
|
80
|
+
| `uvako login` | Partner hesabıyla cihaz girişi (`--no-browser`: tarayıcıyı açmaz). |
|
|
81
|
+
| `uvako logout --partner` | Partner girişini kapatır (anahtar sunucuda da iptal edilir). |
|
|
82
|
+
| `uvako app init <ad>` | Örnek uygulama (`--dir`, `--offline`: kaydetmeden). |
|
|
83
|
+
| `uvako app dev [klasör]` | Yerelde çalıştırma ve webhook aktarımı (`--store`, `--port`, `--no-serve`, `--no-update`). |
|
|
84
|
+
| `uvako app deploy [klasör]` | `uvako.app.json`'u yeni sürüm olarak dağıtır (`--submit`). |
|
|
85
|
+
| `uvako app submit [klasör]` | Uvako incelemesine gönderir; onaylanınca tüm mağazalar kurabilir. |
|
|
86
|
+
| `uvako app list` | Uygulamalarınızı listeler (`--json`). |
|
|
87
|
+
|
|
88
|
+
Her uygulama `app/uninstalled` ve üç KVKK/GDPR webhook'unu (`privacy_webhooks`) tanımlamak zorundadır.
|
|
89
|
+
Client secret diske yazılmaz. CI için `UVAKO_PARTNER_TOKEN`. Ayrıntı: Uvako Admin API belgesi.
|
|
90
|
+
|
|
60
91
|
## Tema paketi
|
|
61
92
|
|
|
62
93
|
Tema kod içermez. Bir katalog temasını (`base`) temel alır ve onun üstüne şunları getirir:
|
|
@@ -66,6 +97,7 @@ theme.json kimlik: key, name, version, base; mağaza vitrini
|
|
|
66
97
|
config/settings_data.json temel temadan farklı olan ayar değerleri
|
|
67
98
|
config/settings_schema.json (isteğe bağlı) panelde görünen ayarlar
|
|
68
99
|
templates/index.json (isteğe bağlı) ana sayfadaki bölüm sırası
|
|
100
|
+
locales/en.json, de.json, tr.json (isteğe bağlı) temanın kendi metinlerinin çevirisi
|
|
69
101
|
styles/_tokens.scss renk, yazı tipi, köşe değişkenleri (temanın tamamına yayılır)
|
|
70
102
|
styles/theme.scss temaya özgü SCSS
|
|
71
103
|
assets/ logo, slayt ve banner görselleri, yazı tipleri
|
|
@@ -75,6 +107,37 @@ reference/ temel temanın ayarları ve bölüm türleri (pak
|
|
|
75
107
|
Bölümlerin kendisi (slayt, banner, ürün vitrini…) temel temadan gelir; paket hangilerinin hangi sırayla
|
|
76
108
|
ve hangi görünümle kullanılacağını söyler. Katalog temaları: [themes.uvako.com](https://themes.uvako.com).
|
|
77
109
|
|
|
110
|
+
### Diller
|
|
111
|
+
|
|
112
|
+
Mağazalar vitrinini Türkçe, İngilizce ve Almanca yayınlayabilir. Temanın kendi metinlerini
|
|
113
|
+
(`config/settings_data.json` içindeki başlık ve buton yazıları, `theme.json` içindeki açıklama, sektör ve
|
|
114
|
+
özellikler) çevirmek için pakete sözlük dosyası koyun:
|
|
115
|
+
|
|
116
|
+
```json
|
|
117
|
+
{
|
|
118
|
+
"Yeni sezon geldi": "The new season is here",
|
|
119
|
+
"Alışverişe başla": "Start shopping"
|
|
120
|
+
}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
- Dosyalar: `locales/en.json`, `locales/de.json`, `locales/tr.json`. Anahtar temadaki metnin kendisi,
|
|
124
|
+
değer o dildeki karşılığıdır. Düz bir sözlüktür; iç içe nesne olamaz.
|
|
125
|
+
- Temayı Türkçe yazdıysanız `en.json` ve `de.json` yeterlidir. Başka bir dilde yazdıysanız Türkçe
|
|
126
|
+
karşılıkları `tr.json` dosyasına koyun.
|
|
127
|
+
- Çevirisi olmayan metin yazıldığı gibi gösterilir. `uvako theme check` her dosyada eksik kalan
|
|
128
|
+
metinleri uyarı olarak listeler.
|
|
129
|
+
- Mağaza sahibinin panelden değiştirdiği metin sözlükte yoksa olduğu gibi kalır.
|
|
130
|
+
- Sınırlar: dosya başına 256 KB ve 2.000 metin; metin 500, çevirisi 2.000 karakter.
|
|
131
|
+
|
|
132
|
+
Temel temadan gelen metinler (sepet, ödeme, üyelik, menü) platform tarafından çevrilir; onları
|
|
133
|
+
sözlüğe yazmanız gerekmez.
|
|
134
|
+
|
|
135
|
+
### CLI dili
|
|
136
|
+
|
|
137
|
+
Çıktı Türkçe ya da İngilizcedir. Dil sırasıyla `--lang` seçeneğinden, `UVAKO_LANG` ortam değişkeninden
|
|
138
|
+
ve sistemin dilinden okunur: `uvako theme check --lang en`. İngilizce çalışırken `theme init` iskeleti
|
|
139
|
+
(README, SCSS açıklamaları) İngilizce yazar ve Türkçe karşılığı `locales/tr.json` dosyasına koyar.
|
|
140
|
+
|
|
78
141
|
### Ürün sayfası sekmeleri
|
|
79
142
|
|
|
80
143
|
Ürün detay sayfasındaki sekmeler `config/settings_data.json` içindeki `product_detail_page_tabs` ile açılır
|
package/bin/uvako-cli.js
CHANGED
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { main } from "../src/main.js";
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
4
|
+
// Çıkış kodu atanır ve süreç kendiliğinden kapanır: process.exit, Windows'ta kapanmakta olan fetch
|
|
5
|
+
// bağlantılarının üstüne gelince libuv uyarısıyla düşebiliyor. Açık kalan bir şey olursa kısa süre
|
|
6
|
+
// sonra yine de çıkılır.
|
|
7
|
+
const finish = (code) => {
|
|
8
|
+
process.exitCode = code;
|
|
9
|
+
setTimeout(() => process.exit(code), 3000).unref();
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
main(process.argv.slice(2)).then(finish, (error) => {
|
|
13
|
+
console.error(`Hata: ${error?.message ?? error}`);
|
|
14
|
+
finish(1);
|
|
15
|
+
});
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "uvako-cli",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Uvako tema geliştirme aracı. Tema başlatır, denetler,
|
|
3
|
+
"version": "0.5.0",
|
|
4
|
+
"description": "Uvako tema ve uygulama geliştirme aracı. Tema başlatır, denetler ve canlı önizletir; herkese açık uygulama başlatır, yerelde çalıştırıp webhook aktarır ve dağıtır. https://uvako.com",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "UNLICENSED",
|
|
7
7
|
"author": {
|
|
@@ -42,7 +42,8 @@
|
|
|
42
42
|
"files": [
|
|
43
43
|
"bin",
|
|
44
44
|
"src",
|
|
45
|
-
"README.md"
|
|
45
|
+
"README.md",
|
|
46
|
+
"templates"
|
|
46
47
|
],
|
|
47
48
|
"engines": {
|
|
48
49
|
"node": ">=22"
|
package/src/api.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { t } from "./i18n.js";
|
|
1
2
|
// Mağazanın tema uç noktası (/api/theme-cli). Erişim anahtarı yalnızca girişte seçilen mağazaya
|
|
2
3
|
// gönderilir: yönlendirmeler izlenmez, böylece anahtar başka bir adrese taşınmaz.
|
|
3
4
|
|
|
@@ -21,7 +22,7 @@ export async function call({ origin, token }, method, route, { body, raw = false
|
|
|
21
22
|
signal: AbortSignal.timeout(timeout),
|
|
22
23
|
});
|
|
23
24
|
} catch (error) {
|
|
24
|
-
throw new ApiError(
|
|
25
|
+
throw new ApiError(t("Mağazaya bağlanılamadı ({0}): {1}", origin, error.cause?.code ?? error.message));
|
|
25
26
|
}
|
|
26
27
|
if (response.ok) return raw ? Buffer.from(await response.arrayBuffer()) : response.json();
|
|
27
28
|
let payload = null;
|
|
@@ -31,7 +32,7 @@ export async function call({ origin, token }, method, route, { body, raw = false
|
|
|
31
32
|
// JSON olmayan yanıt (ör. ağ geçidi hatası).
|
|
32
33
|
}
|
|
33
34
|
const problem = payload?.error ?? {};
|
|
34
|
-
const fallback = response.status === 404 ? "Bu adreste Uvako tema uç noktası yok. Mağaza adresini kontrol edin." :
|
|
35
|
+
const fallback = response.status === 404 ? t("Bu adreste Uvako tema uç noktası yok. Mağaza adresini kontrol edin.") : t("Sunucu {0} yanıtı verdi.", response.status);
|
|
35
36
|
throw new ApiError(problem.message || fallback, { status: response.status, code: problem.code || "", problems: Array.isArray(problem.problems) ? problem.problems : [] });
|
|
36
37
|
}
|
|
37
38
|
|
|
@@ -41,9 +42,9 @@ export async function starter(site, base) {
|
|
|
41
42
|
try {
|
|
42
43
|
response = await fetch(`${site}/api/site/themes/${encodeURIComponent(base)}/starter`, { headers: { Accept: "application/json", "User-Agent": "uvako-cli" }, signal: AbortSignal.timeout(30000) });
|
|
43
44
|
} catch (error) {
|
|
44
|
-
throw new ApiError(
|
|
45
|
+
throw new ApiError(t("uvako.com'a bağlanılamadı: {0}", error.cause?.code ?? error.message));
|
|
45
46
|
}
|
|
46
|
-
if (response.status === 404) throw new ApiError(
|
|
47
|
-
if (!response.ok) throw new ApiError(
|
|
47
|
+
if (response.status === 404) throw new ApiError(t("\"{0}\" adında bir temel tema yok. Temalar: https://themes.uvako.com", base), { status: 404 });
|
|
48
|
+
if (!response.ok) throw new ApiError(t("Temel tema okunamadı ({0}).", response.status), { status: response.status });
|
|
48
49
|
return response.json();
|
|
49
50
|
}
|
package/src/app.js
ADDED
|
@@ -0,0 +1,412 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { ApiError } from "./api.js";
|
|
6
|
+
import { partnerSaved } from "./config.js";
|
|
7
|
+
import { t, tm } from "./i18n.js";
|
|
8
|
+
import { explainPartner, partnerCall } from "./partner.js";
|
|
9
|
+
|
|
10
|
+
// `uvako app`: herkese açık uygulamalar. Yapılandırma uygulama klasöründeki uvako.app.json'dur;
|
|
11
|
+
// client secret diske yazılmaz (`app dev` yerel uygulamaya ortam değişkeniyle verir).
|
|
12
|
+
|
|
13
|
+
export const configFile = "uvako.app.json";
|
|
14
|
+
const templateDir = path.join(path.dirname(fileURLToPath(import.meta.url)), "..", "templates", "app");
|
|
15
|
+
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
16
|
+
const stamp = () => new Date().toLocaleTimeString("tr-TR");
|
|
17
|
+
|
|
18
|
+
export function slug(text) {
|
|
19
|
+
return String(text ?? "")
|
|
20
|
+
.toLocaleLowerCase("tr")
|
|
21
|
+
.replace(/ı/g, "i").replace(/ğ/g, "g").replace(/ü/g, "u").replace(/ş/g, "s").replace(/ö/g, "o").replace(/ç/g, "c")
|
|
22
|
+
.replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Yeni uygulamanın yapılandırması. Adresler örnektir; dağıtmadan önce gerçek adresle değiştirilir. */
|
|
26
|
+
export function starterConfig(name) {
|
|
27
|
+
return {
|
|
28
|
+
client_id: "",
|
|
29
|
+
name,
|
|
30
|
+
application_url: "https://example.com",
|
|
31
|
+
redirect_urls: ["https://example.com/auth/callback"],
|
|
32
|
+
scopes: ["read_orders", "read_products"],
|
|
33
|
+
// Panelde iframe içinde açılır (@uvako/app-bridge ve oturum belirteci).
|
|
34
|
+
embedded: true,
|
|
35
|
+
webhooks: [{ topics: ["orders/create", "app/uninstalled", "app_subscriptions/update"], uri: "/webhooks" }],
|
|
36
|
+
// KVKK/GDPR zorunlu webhook'ları: üçü de tanımlı olmalı.
|
|
37
|
+
privacy_webhooks: {
|
|
38
|
+
customer_data_request_url: "/webhooks/customers/data_request",
|
|
39
|
+
customer_deletion_url: "/webhooks/customers/redact",
|
|
40
|
+
shop_deletion_url: "/webhooks/shop/redact",
|
|
41
|
+
},
|
|
42
|
+
listing: { tagline: "", description: "", category: "", pricing: "", website: "", support_email: "", privacy_url: "" },
|
|
43
|
+
dev: { command: "node server.js", port: 3000 },
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function readConfig(dir) {
|
|
48
|
+
const file = path.join(dir, configFile);
|
|
49
|
+
if (!existsSync(file)) throw new Error(t("{0} bir uygulama klasörü değil: {1} bulunamadı. Yeni uygulama için: uvako app init <ad>", dir, configFile));
|
|
50
|
+
let config;
|
|
51
|
+
try {
|
|
52
|
+
config = JSON.parse(readFileSync(file, "utf8"));
|
|
53
|
+
} catch (error) {
|
|
54
|
+
throw new Error(t("{0} geçerli JSON değil: {1}", configFile, error.message));
|
|
55
|
+
}
|
|
56
|
+
const problems = checkConfig(config);
|
|
57
|
+
if (problems.length) throw new Error(problems.map((p) => `${configFile}: ${p}`).join("\n"));
|
|
58
|
+
return config;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function writeConfig(dir, config) {
|
|
62
|
+
writeFileSync(path.join(dir, configFile), `${JSON.stringify(config, null, 2)}\n`);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Yerel denetim (kolaylık içindir; asıl denetimi sunucu yapar). */
|
|
66
|
+
export function checkConfig(config) {
|
|
67
|
+
const problems = [];
|
|
68
|
+
if (!config || typeof config !== "object" || Array.isArray(config)) return [t("yapılandırma bir nesne olmalı.")];
|
|
69
|
+
if (typeof config.name !== "string" || !config.name.trim() || config.name.length > 60) problems.push(t("\"name\" gerekli (en çok 60 karakter)."));
|
|
70
|
+
if (config.client_id !== undefined && config.client_id !== "" && !/^[0-9a-f]{32}$/.test(config.client_id)) problems.push(t("\"client_id\" 32 karakterlik bir kimlik olmalı (ya da boş)."));
|
|
71
|
+
const https = (value) => {
|
|
72
|
+
try {
|
|
73
|
+
const u = new URL(value);
|
|
74
|
+
return u.protocol === "https:" && !u.username && !u.password;
|
|
75
|
+
} catch {
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
if (!https(config.application_url)) problems.push(t("\"application_url\" https:// ile başlayan bir adres olmalı."));
|
|
80
|
+
if (!Array.isArray(config.redirect_urls) || config.redirect_urls.length === 0 || config.redirect_urls.some((u) => !https(u))) problems.push(t("\"redirect_urls\" https adreslerinden oluşan bir liste olmalı."));
|
|
81
|
+
if (!Array.isArray(config.scopes) || config.scopes.length === 0 || config.scopes.some((s) => !/^(read|write)_[a-z_]+$/.test(s))) problems.push(t("\"scopes\" kapsam listesi olmalı (ör. [\"read_products\"])."));
|
|
82
|
+
if (config.webhooks !== undefined && (!Array.isArray(config.webhooks) || config.webhooks.some((w) => !Array.isArray(w?.topics) || typeof w?.uri !== "string"))) problems.push(t("\"webhooks\" [{\"topics\": [...], \"uri\": \"/webhooks\"}] biçiminde olmalı."));
|
|
83
|
+
const privacy = config.privacy_webhooks;
|
|
84
|
+
for (const key of ["customer_data_request_url", "customer_deletion_url", "shop_deletion_url"]) {
|
|
85
|
+
const value = privacy?.[key];
|
|
86
|
+
if (typeof value !== "string" || !(value.startsWith("/") ? !value.startsWith("//") : https(value))) problems.push(t("\"privacy_webhooks.{0}\" zorunlu: /webhooks/... yolu ya da https adresi (KVKK/GDPR).", key));
|
|
87
|
+
}
|
|
88
|
+
if (Array.isArray(config.webhooks) && !config.webhooks.some((w) => Array.isArray(w?.topics) && w.topics.includes("app/uninstalled"))) problems.push(t("\"webhooks\" içinde zorunlu app/uninstalled konusu yok."));
|
|
89
|
+
if (config.dev !== undefined &&(typeof config.dev !== "object" || (config.dev.port !== undefined && !(Number.isInteger(config.dev.port) && config.dev.port > 0 && config.dev.port < 65536)))) problems.push(t("\"dev.port\" geçerli bir port olmalı."));
|
|
90
|
+
return problems;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Sunucuya giden yapılandırma: client_id ve dev yerel kalır. */
|
|
94
|
+
export function remoteConfig(config) {
|
|
95
|
+
const { client_id: _id, dev: _dev, ...rest } = config;
|
|
96
|
+
return { webhooks: [], listing: {}, ...rest };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export const blocksDir = "blocks";
|
|
100
|
+
const blockFiles = new Set(["block.json", "block.html", "block.css"]);
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Vitrin blokları (theme app extension): blocks/<anahtar>/block.json (ad ve ayar şeması), block.html
|
|
104
|
+
* (şablon) ve isteğe bağlı block.css. Klasör varsa yapılandırmanın theme_blocks alanı buradan kurulur.
|
|
105
|
+
* Blokta kod çalışmaz; asıl denetimi sunucu yapar (tema paketleriyle aynı kurallar).
|
|
106
|
+
*/
|
|
107
|
+
export function readBlocks(dir) {
|
|
108
|
+
const root = path.join(dir, blocksDir);
|
|
109
|
+
if (!existsSync(root)) return null;
|
|
110
|
+
const blocks = [];
|
|
111
|
+
for (const handle of readdirSync(root).sort()) {
|
|
112
|
+
const folder = path.join(root, handle);
|
|
113
|
+
if (!statSync(folder).isDirectory()) continue;
|
|
114
|
+
for (const name of readdirSync(folder)) {
|
|
115
|
+
if (!blockFiles.has(name)) throw new Error(t("{0}: bilinmeyen dosya {1}. Blokta yalnızca block.json, block.html ve block.css olabilir.", `${blocksDir}/${handle}`, name));
|
|
116
|
+
}
|
|
117
|
+
const read = (name) => (existsSync(path.join(folder, name)) ? readFileSync(path.join(folder, name), "utf8") : "");
|
|
118
|
+
let meta;
|
|
119
|
+
try {
|
|
120
|
+
meta = JSON.parse(read("block.json") || "{}");
|
|
121
|
+
} catch (error) {
|
|
122
|
+
throw new Error(t("{0} geçerli JSON değil: {1}", `${blocksDir}/${handle}/block.json`, error.message));
|
|
123
|
+
}
|
|
124
|
+
blocks.push({ handle, name: meta.name ?? handle, template: read("block.html"), css: read("block.css"), settings: meta.settings ?? [] });
|
|
125
|
+
}
|
|
126
|
+
return blocks;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Yerel denetim: sunucunun reddedeceği belirgin hatalar (betik, olay özniteliği, döngü). */
|
|
130
|
+
export function checkBlocks(blocks) {
|
|
131
|
+
const problems = [];
|
|
132
|
+
for (const b of blocks ?? []) {
|
|
133
|
+
const where = `${blocksDir}/${b.handle}`;
|
|
134
|
+
if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(b.handle)) problems.push(t("{0}: klasör adı küçük harf, rakam ve tire olmalı.", where));
|
|
135
|
+
if (!b.template.trim()) problems.push(t("{0}: block.html boş.", where));
|
|
136
|
+
if (/<script\b|\son[a-z]+\s*=|javascript:/i.test(b.template)) problems.push(t("{0}: blokta betik çalıştırılamaz (<script>, onclick=, javascript:).", where));
|
|
137
|
+
if (/\{\{-?\s*(range|with|define|template|block)\b|\{\{-?\s*\$/.test(b.template)) problems.push(t("{0}: şablonda yalnızca {{ .settings.ad }} ve if/else kullanılabilir.", where));
|
|
138
|
+
if (/url\s*\(|@import/i.test(b.css)) problems.push(t("{0}: block.css içinde url() ve @import kullanılamaz.", where));
|
|
139
|
+
if (!Array.isArray(b.settings)) problems.push(t("{0}: block.json \"settings\" bir liste olmalı.", where));
|
|
140
|
+
}
|
|
141
|
+
return problems;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Bloklarla birlikte yapılandırma (blocks/ klasörü varsa theme_blocks onunla değiştirilir). */
|
|
145
|
+
export function withBlocks(dir, config) {
|
|
146
|
+
const blocks = readBlocks(dir);
|
|
147
|
+
if (!blocks) return config;
|
|
148
|
+
const problems = checkBlocks(blocks);
|
|
149
|
+
if (problems.length) throw new Error(problems.join("\n"));
|
|
150
|
+
return { ...config, theme_blocks: blocks };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function fill(text, vars) {
|
|
154
|
+
return text.replace(/\{\{(\w+)\}\}/g, (_, key) => vars[key] ?? "");
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** `uvako app init <ad>`: örnek uygulama klasörü. Partner girişi varsa uygulama Uvako'ya da kaydedilir. */
|
|
158
|
+
export async function initApp(name, options = {}) {
|
|
159
|
+
if (!name || name === true) throw new Error(t("Uygulama adı gerekli: uvako app init <ad>"));
|
|
160
|
+
const handle = slug(name);
|
|
161
|
+
if (!handle) throw new Error(t("Uygulama adı harf ya da rakam içermeli."));
|
|
162
|
+
const dir = path.resolve(options.cwd ?? process.cwd(), typeof options.dir === "string" ? options.dir : handle);
|
|
163
|
+
if (existsSync(path.join(dir, configFile))) throw new Error(t("Klasörde zaten bir uygulama var: {0}", dir));
|
|
164
|
+
mkdirSync(dir, { recursive: true });
|
|
165
|
+
const config = starterConfig(String(name));
|
|
166
|
+
writeFileSync(path.join(dir, "server.js"), readFileSync(path.join(templateDir, "server.js"), "utf8"));
|
|
167
|
+
writeFileSync(path.join(dir, "README.md"), fill(readFileSync(path.join(templateDir, "README.md"), "utf8"), { name: config.name }));
|
|
168
|
+
writeFileSync(path.join(dir, "package.json"), `${JSON.stringify({ name: handle, private: true, type: "module", scripts: { dev: "uvako app dev", start: "node server.js" }, engines: { node: ">=22" } }, null, 2)}\n`);
|
|
169
|
+
writeFileSync(path.join(dir, ".gitignore"), ".uvako/\nnode_modules/\n.env\n");
|
|
170
|
+
// Örnek vitrin bloğu: tema editöründen ana sayfaya eklenir.
|
|
171
|
+
const example = path.join(templateDir, blocksDir, "yildiz-puani");
|
|
172
|
+
mkdirSync(path.join(dir, blocksDir, "yildiz-puani"), { recursive: true });
|
|
173
|
+
for (const name of blockFiles) {
|
|
174
|
+
if (existsSync(path.join(example, name))) writeFileSync(path.join(dir, blocksDir, "yildiz-puani", name), readFileSync(path.join(example, name), "utf8"));
|
|
175
|
+
}
|
|
176
|
+
writeConfig(dir, config);
|
|
177
|
+
let registered = false;
|
|
178
|
+
if (!options.offline && (partnerSaved() || process.env.UVAKO_PARTNER_TOKEN)) {
|
|
179
|
+
const created = await partnerCall("POST", "/apps", { body: remoteConfig(withBlocks(dir, config)) });
|
|
180
|
+
config.client_id = created.app.clientId;
|
|
181
|
+
writeConfig(dir, config);
|
|
182
|
+
registered = true;
|
|
183
|
+
}
|
|
184
|
+
return { dir, config, registered };
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** Kayıtlı değilse kaydeder, değilse yapılandırmayı yeni sürüm olarak dağıtır. */
|
|
188
|
+
async function syncRemote(dir, config) {
|
|
189
|
+
const body = remoteConfig(withBlocks(dir, config));
|
|
190
|
+
if (!config.client_id) {
|
|
191
|
+
const created = await partnerCall("POST", "/apps", { body });
|
|
192
|
+
config.client_id = created.app.clientId;
|
|
193
|
+
writeConfig(dir, config);
|
|
194
|
+
return { app: created.app, created: true, changed: true };
|
|
195
|
+
}
|
|
196
|
+
const before = await partnerCall("GET", `/apps/${config.client_id}`);
|
|
197
|
+
const app = await partnerCall("PUT", `/apps/${config.client_id}/config`, { body });
|
|
198
|
+
return { app, created: false, changed: app.revision !== before.revision };
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const statusText = (app) =>
|
|
202
|
+
app.suspendedAt ? t("askıda") : { draft: t("taslak"), pending: t("incelemede"), approved: t("yayında"), rejected: t("reddedildi") }[app.status] ?? app.status;
|
|
203
|
+
|
|
204
|
+
export async function deployApp(target, options = {}) {
|
|
205
|
+
const dir = path.resolve(target ?? ".");
|
|
206
|
+
try {
|
|
207
|
+
const config = readConfig(dir);
|
|
208
|
+
const { app, created, changed } = await syncRemote(dir, config);
|
|
209
|
+
if (created) console.log(t("Uygulama kaydedildi: {0} (client_id {1})", app.config.name, app.clientId));
|
|
210
|
+
console.log(changed ? t("Dağıtıldı: {0} sürüm {1}", app.config.name, app.revision) : t("Değişiklik yok: {0} sürüm {1}", app.config.name, app.revision));
|
|
211
|
+
console.log(t("Durum: {0}{1}", statusText(app), app.publishedRevision ? t(" · mağazalardaki sürüm {0}", app.publishedRevision) : ""));
|
|
212
|
+
if (app.publishedRevision !== app.revision) console.log(t("Bu sürüm yalnızca kendi geliştirme mağazalarınızda geçerli. Herkese açmak için: uvako app submit"));
|
|
213
|
+
if (options.submit) return submitApp(target);
|
|
214
|
+
return 0;
|
|
215
|
+
} catch (error) {
|
|
216
|
+
if (error instanceof ApiError) return explainPartner(error);
|
|
217
|
+
throw error;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export async function submitApp(target) {
|
|
222
|
+
const dir = path.resolve(target ?? ".");
|
|
223
|
+
try {
|
|
224
|
+
const config = readConfig(dir);
|
|
225
|
+
if (!config.client_id) throw new Error(t("Uygulama henüz kayıtlı değil. Önce: uvako app deploy"));
|
|
226
|
+
const app = await partnerCall("POST", `/apps/${config.client_id}/submit`, { body: {} });
|
|
227
|
+
console.log(t("İncelemeye gönderildi: {0} sürüm {1}. Uvako onayladığında tüm mağazalar bu sürümü kurabilir.", app.config.name, app.revision));
|
|
228
|
+
return 0;
|
|
229
|
+
} catch (error) {
|
|
230
|
+
if (error instanceof ApiError) return explainPartner(error);
|
|
231
|
+
throw error;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
export async function listApps(options = {}) {
|
|
236
|
+
try {
|
|
237
|
+
const { apps } = await partnerCall("GET", "/apps");
|
|
238
|
+
if (options.json) {
|
|
239
|
+
console.log(JSON.stringify(apps, null, 2));
|
|
240
|
+
return 0;
|
|
241
|
+
}
|
|
242
|
+
if (!apps.length) console.log(t("Kayıtlı uygulamanız yok. Başlamak için: uvako app init <ad>"));
|
|
243
|
+
for (const a of apps) console.log(` ${a.config.name} [${a.clientId}] ${t("sürüm {0}", a.revision)} · ${statusText(a)} · ${t("{0} kurulum", a.installs)}`);
|
|
244
|
+
return 0;
|
|
245
|
+
} catch (error) {
|
|
246
|
+
if (error instanceof ApiError) return explainPartner(error);
|
|
247
|
+
throw error;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Aktarılan bir webhook teslimatını yerel alıcıya gönderir. Başlıklar ve gövde Uvako'nun imzaladığı
|
|
253
|
+
* hâliyle gider; yol teslimat adresinin yoludur (https://uygulama.example.com/webhooks → /webhooks).
|
|
254
|
+
*/
|
|
255
|
+
export async function forward(delivery, localOrigin, timeout = 5000) {
|
|
256
|
+
const source = new URL(delivery.address);
|
|
257
|
+
const target = `${localOrigin}${source.pathname}${source.search}`;
|
|
258
|
+
const started = Date.now();
|
|
259
|
+
try {
|
|
260
|
+
const response = await fetch(target, { method: "POST", headers: delivery.headers, body: delivery.body, redirect: "manual", signal: AbortSignal.timeout(timeout) });
|
|
261
|
+
const text = (await response.text()).slice(0, 2048);
|
|
262
|
+
return { target, outcome: { code: response.status, body: text, error: "", durationMs: Date.now() - started } };
|
|
263
|
+
} catch (error) {
|
|
264
|
+
return { target, outcome: { code: 0, body: "", error: String(error.cause?.code ?? error.message ?? error), durationMs: Date.now() - started } };
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/** Bir oturumun bekleyen teslimatlarını alıp yerel alıcıya iletir; iletilen sayısını döner. */
|
|
269
|
+
export async function relayOnce(sessionId, localOrigin, log = console.log) {
|
|
270
|
+
const { deliveries } = await partnerCall("GET", `/dev/${sessionId}/deliveries`, { timeout: 20000 });
|
|
271
|
+
for (const d of deliveries) {
|
|
272
|
+
const { target, outcome } = await forward(d, localOrigin);
|
|
273
|
+
await partnerCall("POST", `/dev/${sessionId}/deliveries/${d.id}`, { body: outcome });
|
|
274
|
+
const result = outcome.code ? `${outcome.code}` : t("ulaşılamadı ({0})", outcome.error);
|
|
275
|
+
log(t("[{0}] {1} → {2} {3} ({4} ms)", stamp(), d.topic, new URL(target).pathname, result, outcome.durationMs));
|
|
276
|
+
}
|
|
277
|
+
return deliveries.length;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/** `uvako app dev`: yerel uygulamayı başlatır, geliştirme mağazasını ona bağlar ve webhook'ları aktarır. */
|
|
281
|
+
export async function devApp(target, options = {}) {
|
|
282
|
+
const dir = path.resolve(target ?? ".");
|
|
283
|
+
let config;
|
|
284
|
+
let session;
|
|
285
|
+
let child = null;
|
|
286
|
+
try {
|
|
287
|
+
config = readConfig(dir);
|
|
288
|
+
const port = Number(options.port ?? config.dev?.port ?? 3000);
|
|
289
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) throw new Error(t("Geçersiz port: {0}", options.port));
|
|
290
|
+
const local = `http://localhost:${port}`;
|
|
291
|
+
|
|
292
|
+
if (!options["no-update"]) {
|
|
293
|
+
const { app, created, changed } = await syncRemote(dir, config);
|
|
294
|
+
if (created) console.log(t("Uygulama kaydedildi: {0} (client_id {1})", app.config.name, app.clientId));
|
|
295
|
+
else if (changed) console.log(t("Yapılandırma dağıtıldı: sürüm {0} (yalnızca kendi mağazalarınızda)", app.revision));
|
|
296
|
+
} else if (!config.client_id) {
|
|
297
|
+
throw new Error(t("Uygulama henüz kayıtlı değil. --no-update olmadan çalıştırın ya da: uvako app deploy"));
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
let store = typeof options.store === "string" ? options.store : "";
|
|
301
|
+
if (!store) {
|
|
302
|
+
const { stores } = await partnerCall("GET", "/stores");
|
|
303
|
+
if (stores.length !== 1) {
|
|
304
|
+
console.error(stores.length ? t("Hangi mağazada geliştireceğinizi seçin: --store <mağaza>") : t("Partner hesabınızda geliştirme mağazası yok. partner.uvako.com → Mağazalar → Geliştirme mağazası aç."));
|
|
305
|
+
for (const s of stores) console.error(` ${s.slug} ${s.host}`);
|
|
306
|
+
return 1;
|
|
307
|
+
}
|
|
308
|
+
store = stores[0].slug;
|
|
309
|
+
}
|
|
310
|
+
const redirects = config.redirect_urls.map((u) => {
|
|
311
|
+
const parsed = new URL(u);
|
|
312
|
+
return `${local}${parsed.pathname}`;
|
|
313
|
+
});
|
|
314
|
+
session = await partnerCall("POST", `/apps/${config.client_id}/dev`, { body: { store, app_url: local, redirect_urls: [...new Set(redirects)] } });
|
|
315
|
+
const { clientSecret } = await partnerCall("POST", `/apps/${config.client_id}/secret`, { body: {} });
|
|
316
|
+
|
|
317
|
+
if (!options["no-serve"]) {
|
|
318
|
+
const command = String(config.dev?.command ?? "node server.js");
|
|
319
|
+
const adminOrigin = session.store.admin.replace(/\/admin$/, "");
|
|
320
|
+
child = spawn(command, {
|
|
321
|
+
cwd: dir,
|
|
322
|
+
shell: true,
|
|
323
|
+
stdio: "inherit",
|
|
324
|
+
env: {
|
|
325
|
+
...process.env,
|
|
326
|
+
PORT: String(port),
|
|
327
|
+
UVAKO_CLIENT_ID: config.client_id,
|
|
328
|
+
UVAKO_CLIENT_SECRET: clientSecret,
|
|
329
|
+
UVAKO_APP_URL: local,
|
|
330
|
+
UVAKO_SCOPES: config.scopes.join(","),
|
|
331
|
+
UVAKO_API_VERSION: "2026-10",
|
|
332
|
+
// Yerel mağaza (http) için; gerçek mağazalarda uygulama https://<shop> kullanır.
|
|
333
|
+
UVAKO_ADMIN_ORIGIN: adminOrigin.startsWith("http://") ? adminOrigin : "",
|
|
334
|
+
// Yerelde oturum belirtecinin dest'i bu mağazadır (örnek uygulama: shopFromOrigin).
|
|
335
|
+
UVAKO_SHOP: session.store.host,
|
|
336
|
+
},
|
|
337
|
+
});
|
|
338
|
+
child.on("exit", (code) => {
|
|
339
|
+
if (code !== null && code !== 0) console.error(t("! Yerel uygulama {0} koduyla kapandı.", code));
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
console.log(t("\nGeliştirme: {0} → {1} ({2})", config.name, session.store.name, session.store.host));
|
|
344
|
+
console.log(t("Yerel uygulama: {0}", local));
|
|
345
|
+
console.log(t("Kur / aç: {0}", session.installUrl));
|
|
346
|
+
console.log(t("Bu mağazada uygulamanın adresi yerel adres oldu ve webhook'ları buraya aktarılıyor. Durdurmak için Ctrl+C.\n"));
|
|
347
|
+
} catch (error) {
|
|
348
|
+
child?.kill();
|
|
349
|
+
if (error instanceof ApiError) return explainPartner(error);
|
|
350
|
+
console.error(`✖ ${tm(error.message)}`);
|
|
351
|
+
return 1;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
let stopping = false;
|
|
355
|
+
const stop = async () => {
|
|
356
|
+
if (stopping) return;
|
|
357
|
+
stopping = true;
|
|
358
|
+
try {
|
|
359
|
+
await partnerCall("DELETE", `/dev/${session.id}`);
|
|
360
|
+
} catch {
|
|
361
|
+
// Oturum 45 saniye sonra kendiliğinden düşer.
|
|
362
|
+
}
|
|
363
|
+
child?.kill();
|
|
364
|
+
console.log(t("\nGeliştirme durdu. Mağaza dağıtılan adresleri kullanıyor; webhook'lar yeniden internetten gidiyor."));
|
|
365
|
+
};
|
|
366
|
+
return new Promise((resolve) => {
|
|
367
|
+
process.on("SIGINT", () => void stop().then(() => resolve(0)));
|
|
368
|
+
const local = `http://localhost:${Number(options.port ?? config.dev?.port ?? 3000)}`;
|
|
369
|
+
(async () => {
|
|
370
|
+
let failures = 0;
|
|
371
|
+
while (!stopping) {
|
|
372
|
+
try {
|
|
373
|
+
const count = await relayOnce(session.id, local);
|
|
374
|
+
failures = 0;
|
|
375
|
+
if (count === 0) await sleep(1500);
|
|
376
|
+
} catch (error) {
|
|
377
|
+
if (stopping) break;
|
|
378
|
+
if (error instanceof ApiError && (error.status === 401 || error.status === 422 || error.status === 404)) {
|
|
379
|
+
console.error(`✖ ${tm(error.message)}`);
|
|
380
|
+
await stop();
|
|
381
|
+
resolve(1);
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
failures++;
|
|
385
|
+
if (failures === 1 || failures % 10 === 0) console.error(t("[{0}] ! Partner paneline ulaşılamadı: {1}", stamp(), tm(error.message)));
|
|
386
|
+
await sleep(3000);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
})();
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
export async function appCommand(command, target, options) {
|
|
394
|
+
if (command === "init") {
|
|
395
|
+
try {
|
|
396
|
+
const { dir, config, registered } = await initApp(target, options);
|
|
397
|
+
const rel = path.relative(process.cwd(), dir) || ".";
|
|
398
|
+
console.log(t("Uygulama oluşturuldu: {0}", rel));
|
|
399
|
+
console.log(registered ? t("Uvako'ya kaydedildi: client_id {0}", config.client_id) : t("Henüz kayıtlı değil (giriş yok ya da --offline). Kayıt için: uvako login, sonra uvako app deploy {0}", rel));
|
|
400
|
+
console.log(t("Sıradaki adım: cd {0} && uvako app dev --store <geliştirme mağazası>", rel));
|
|
401
|
+
return 0;
|
|
402
|
+
} catch (error) {
|
|
403
|
+
if (error instanceof ApiError) return explainPartner(error);
|
|
404
|
+
throw error;
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
if (command === "dev") return devApp(target, options);
|
|
408
|
+
if (command === "deploy") return deployApp(target, options);
|
|
409
|
+
if (command === "submit") return submitApp(target);
|
|
410
|
+
if (command === "list") return listApps(options);
|
|
411
|
+
return null;
|
|
412
|
+
}
|