uvako-cli 0.1.2 → 0.2.1

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # uvako-cli
2
2
 
3
- Uvako mağazaları için tema geliştirme aracı. Temayı başlatır, denetler ve yüklenebilir pakete çevirir.
3
+ Uvako mağazaları için tema geliştirme aracı. Temayı başlatır, denetler, mağazanıza yükler ve kaydettikçe canlı önizletir.
4
4
 
5
5
  **[Uvako](https://uvako.com)** — Rox Software ürünü.
6
6
 
@@ -13,45 +13,94 @@ npm install -g uvako-cli
13
13
  uvako version
14
14
  ```
15
15
 
16
+ ## Hızlı başlangıç
17
+
18
+ 1. Mağaza panelinde **Ayarlar → Temalar → Geliştirici erişimi** bölümünden bir erişim anahtarı oluşturun.
19
+ 2. Giriş yapın (anahtar sorulur, ekrana yazılmaz):
20
+
21
+ ```bash
22
+ uvako login --store magazam
23
+ ```
24
+
25
+ 3. Bir katalog temasını temel alan yeni tema başlatın:
26
+
27
+ ```bash
28
+ uvako theme init atolye --base linea
29
+ ```
30
+
31
+ 4. Canlı geliştirin: dosyaları kaydettikçe tema mağazanıza yüklenir, açık önizleme sayfası kendini yeniler.
32
+
33
+ ```bash
34
+ uvako theme dev atolye
35
+ ```
36
+
37
+ Önizleme adresi mağazanızın kendi adresidir (`https://magazam.uvoo.store/?preview=…`); tarayıcıda mağaza
38
+ paneline giriş yapmış olmanız gerekir. Tema, panelden yayınlanana kadar vitrininizi değiştirmez.
39
+
16
40
  ## Komutlar
17
41
 
18
42
  | Komut | Ne yapar |
19
43
  |---|---|
20
- | `uvako theme init <anahtar>` | Yeni tema iskeleti oluşturur (`--name`, `--author`). |
44
+ | `uvako login --store <mağaza>` | Mağazaya tema erişim anahtarıyla bağlanır. |
45
+ | `uvako logout` · `uvako whoami` | Kayıtlı girişi siler · bağlı mağazayı gösterir. |
46
+ | `uvako theme init <anahtar>` | Yeni tema iskeleti (`--base`, `--name`, `--author`). |
21
47
  | `uvako theme check [klasör]` | Temayı denetler: yapı, ayarlar, bölümler, görseller, güvenlik. |
48
+ | `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
+ | `uvako theme push [klasör]` | Temayı mağazanın tema kütüphanesine yükler. |
50
+ | `uvako theme pull <anahtar>` | Yüklediğiniz temayı indirir (`--dir`, `--force`). |
51
+ | `uvako theme list` | Mağazadaki temaları ve yüklediğiniz paketleri listeler. |
52
+ | `uvako theme submit <anahtar>` | Temayı themes.uvako.com için incelemeye gönderir. |
53
+ | `uvako theme delete <anahtar> --yes` | Yüklediğiniz paketi siler. |
22
54
  | `uvako theme package [klasör]` | Denetimden geçen temayı zip paketine çevirir (`--out`). |
23
55
  | `uvako version` | Sürümü yazar. |
24
56
 
25
- `uvako-cli` aynı komutların uzun adıdır. `check`, hem bağımsız tema klasörlerinde hem platform deposundaki temalarda (`themes/<anahtar>`) çalışır; depodayken stil ve görselleri `frontend/storefront` altındaki yerlerinden okur. Hata varsa çıkış kodu 1'dir.
57
+ `uvako-cli` aynı komutların uzun adıdır. Hata varsa çıkış kodu 1'dir. CI için `UVAKO_STORE` ve
58
+ `UVAKO_TOKEN` ortam değişkenleri girişin yerini tutar.
26
59
 
27
60
  ## Tema paketi
28
61
 
29
- Tema kod içermez. Paket şunlardan oluşur:
62
+ Tema kod içermez. Bir katalog temasını (`base`) temel alır ve onun üstüne şunları getirir:
30
63
 
31
64
  ```
32
- theme.json kimlik: key, name, version
33
- config/settings_schema.json panelde görünen ayarlar
34
- config/settings_data.json ayarların varsayılan değerleri
35
- config/sections.json ana sayfa bölüm türleri
36
- templates/index.json ana sayfadaki bölüm sırası
37
- styles/ SCSS (renk / yazı tipi değişkenleri ve temaya özgü kurallar)
65
+ theme.json kimlik: key, name, version, base; mağaza vitrini bilgileri
66
+ config/settings_data.json temel temadan farklı olan ayar değerleri
67
+ config/settings_schema.json (isteğe bağlı) panelde görünen ayarlar
68
+ templates/index.json (isteğe bağlı) ana sayfadaki bölüm sırası
69
+ styles/_tokens.scss renk, yazı tipi, köşe değişkenleri (temanın tamamına yayılır)
70
+ styles/theme.scss temaya özgü SCSS
38
71
  assets/ logo, slayt ve banner görselleri, yazı tipleri
72
+ reference/ temel temanın ayarları ve bölüm türleri (pakete girmez)
39
73
  ```
40
74
 
75
+ Bölümlerin kendisi (slayt, banner, ürün vitrini…) temel temadan gelir; paket hangilerinin hangi sırayla
76
+ ve hangi görünümle kullanılacağını söyler. Katalog temaları: [themes.uvako.com](https://themes.uvako.com).
77
+
78
+ ## Yayınlama
79
+
80
+ - `push` ile yüklenen tema **yalnızca kendi mağazanızda** görünür.
81
+ - `submit` temayı Uvako incelemesine gönderir. Onaylanan sürüm themes.uvako.com'da listelenir ve diğer
82
+ mağazalar kurabilir. Fiyatı (`theme.json` içindeki `price` öneridir) Uvako onaylar.
83
+ - Onaydan sonra yüklediğiniz yeni sürümler yeniden onaylanana kadar yalnızca kendi mağazanızı etkiler.
84
+
41
85
  ## Güvenlik kuralları
42
86
 
43
- `check` şunları reddeder:
87
+ Mağaza, yüklenen paketi aynı kurallarla **kendisi yeniden denetler**; `check` hatayı önceden görmeniz içindir.
44
88
 
45
- - JSON, SCSS/CSS, görsel ve yazı tipi dışındaki her dosya türü (JavaScript, TypeScript, HTML, PHP, çalıştırılabilir dosyalar).
89
+ - Yalnızca yukarıdaki dosyalar kabul edilir: JSON, SCSS, görsel ve yazı tipi. JavaScript, şablon ve
90
+ çalıştırılabilir dosyalar reddedilir.
46
91
  - `<script>`, `javascript:` adresleri, `onclick` gibi olay öznitelikleri, `<iframe>` (SVG ve ayar metinleri dahil).
47
92
  - Kod çalıştıran CSS (`expression`, `behavior`, `-moz-binding`).
48
- - SCSS içinden yazı tipi sağlayıcıları dışındaki adreslere yapılan çağrılar.
93
+ - SCSS içinde döngü, fonksiyon ve mixin tanımı (`@while`, `@for`, `@each`, `@function`, `@mixin`, `@use`).
94
+ Temel temanın mixin'leri `@include` ile kullanılabilir.
95
+ - Paketin kendi dosyaları dışındaki `@import`'lar ve yazı tipi sağlayıcıları dışındaki adresler.
49
96
  - Sembolik bağlar, gizli dosyalar, 5 MB'tan büyük dosyalar, 60 MB'tan büyük ya da 600 dosyadan fazla paketler.
50
97
  - Gizli anahtar ya da parola gibi görünen değerler.
51
98
 
52
- Araç platformun kaynak kodunu içermez ve ağa bağlanmaz.
99
+ Erişim anahtarı yalnızca tema işlemlerine yetkilidir; sipariş, müşteri ve ayarlara erişemez. Bilgisayarınızda
100
+ kullanıcı klasörünüzde saklanır, tema klasörüne yazılmaz, yalnızca giriş yaptığınız mağazaya ve yalnızca
101
+ https üzerinden gönderilir. Kaybederseniz panelden iptal edin.
53
102
 
54
- Yerel önizleme (`theme dev`) ve mağazaya yükleme (`login`, `theme push`, `theme pull`) bu sürümde yoktur.
103
+ Araç platformun kaynak kodunu içermez.
55
104
 
56
105
  ## Uvako
57
106
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "uvako-cli",
3
- "version": "0.1.2",
4
- "description": "Uvako tema geliştirme aracı. Tema başlatır, denetler ve paketler. https://uvako.com",
3
+ "version": "0.2.1",
4
+ "description": "Uvako tema geliştirme aracı. Tema başlatır, denetler, mağazaya yükler ve canlı önizletir. https://uvako.com",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",
7
7
  "author": {
package/src/api.js ADDED
@@ -0,0 +1,49 @@
1
+ // Mağazanın tema uç noktası (/api/theme-cli). Erişim anahtarı yalnızca girişte seçilen mağazaya
2
+ // gönderilir: yönlendirmeler izlenmez, böylece anahtar başka bir adrese taşınmaz.
3
+
4
+ export class ApiError extends Error {
5
+ constructor(message, { status = 0, code = "", problems = [] } = {}) {
6
+ super(message);
7
+ this.status = status;
8
+ this.code = code;
9
+ this.problems = problems;
10
+ }
11
+ }
12
+
13
+ export async function call({ origin, token }, method, route, { body, raw = false, timeout = 120000 } = {}) {
14
+ let response;
15
+ try {
16
+ response = await fetch(`${origin}/api/theme-cli${route}`, {
17
+ method,
18
+ redirect: "error",
19
+ headers: { Authorization: `Bearer ${token}`, Accept: "application/json", "User-Agent": "uvako-cli", ...(body ? { "Content-Type": "application/zip" } : {}) },
20
+ body,
21
+ signal: AbortSignal.timeout(timeout),
22
+ });
23
+ } catch (error) {
24
+ throw new ApiError(`Mağazaya bağlanılamadı (${origin}): ${error.cause?.code ?? error.message}`);
25
+ }
26
+ if (response.ok) return raw ? Buffer.from(await response.arrayBuffer()) : response.json();
27
+ let payload = null;
28
+ try {
29
+ payload = await response.json();
30
+ } catch {
31
+ // JSON olmayan yanıt (ör. ağ geçidi hatası).
32
+ }
33
+ const problem = payload?.error ?? {};
34
+ const fallback = response.status === 404 ? "Bu adreste Uvako tema uç noktası yok. Mağaza adresini kontrol edin." : `Sunucu ${response.status} yanıtı verdi.`;
35
+ throw new ApiError(problem.message || fallback, { status: response.status, code: problem.code || "", problems: Array.isArray(problem.problems) ? problem.problems : [] });
36
+ }
37
+
38
+ /** Herkese açık uç nokta: temel temanın başlangıç ayarları (giriş gerektirmez). */
39
+ export async function starter(site, base) {
40
+ let response;
41
+ try {
42
+ response = await fetch(`${site}/api/site/themes/${encodeURIComponent(base)}/starter`, { headers: { Accept: "application/json", "User-Agent": "uvako-cli" }, signal: AbortSignal.timeout(30000) });
43
+ } catch (error) {
44
+ throw new ApiError(`uvako.com'a bağlanılamadı: ${error.cause?.code ?? error.message}`);
45
+ }
46
+ if (response.status === 404) throw new ApiError(`"${base}" adında bir temel tema yok. Temalar: https://themes.uvako.com`, { status: 404 });
47
+ if (!response.ok) throw new ApiError(`Temel tema okunamadı (${response.status}).`, { status: response.status });
48
+ return response.json();
49
+ }
package/src/check.js CHANGED
@@ -2,20 +2,27 @@ import { existsSync, readFileSync } from "node:fs";
2
2
  import path from "node:path";
3
3
  import { walk } from "./files.js";
4
4
 
5
- // Tema paketinin denetimi. Kurallar platformun tema yükleyicisiyle (backend/internal/theme) aynı
6
- // yapıyı arar; üstüne güvenlik kuralları eklenir: tema kod çalıştıramaz, dışarıdan betik çağıramaz.
5
+ // Tema denetimi. Mağazaya yüklenen paketi platform aynı kurallarla yeniden denetler; buradaki
6
+ // denetim geliştiriciye hatayı yüklemeden önce göstermek içindir, güvenlik kararı sunucudadır.
7
+ //
8
+ // İki düzen vardır:
9
+ // paket — bağımsız tema klasörü: theme.json içindeki "base" bir katalog temasıdır; paket onun
10
+ // üstüne ayar, bölüm sırası, SCSS ve görsel getirir.
11
+ // depo — platform deposundaki katalog teması (themes/<anahtar>).
7
12
 
8
- export const limits = { fileBytes: 5 * 1024 * 1024, totalBytes: 60 * 1024 * 1024, files: 600 };
13
+ export const limits = { fileBytes: 5 * 1024 * 1024, totalBytes: 60 * 1024 * 1024, files: 600, styleBytes: 512 * 1024, archiveBytes: 40 * 1024 * 1024 };
9
14
 
10
15
  const keyPattern = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
11
16
  const versionPattern = /^\d+\.\d+\.\d+$/;
17
+ const stylePattern = /^_?[a-z0-9]+(?:[-_][a-z0-9]+)*\.scss$/;
18
+ const segmentPattern = /^[A-Za-z0-9_][A-Za-z0-9._-]*$/;
12
19
 
13
- // Pakette bulunabilecek dosya türleri. Betik, şablon ve çalıştırılabilir dosyalar yoktur.
14
- const allowedExtensions = new Set([
15
- ".json", ".scss", ".css", ".md", ".txt",
16
- ".png", ".jpg", ".jpeg", ".webp", ".avif", ".gif", ".svg", ".ico",
17
- ".woff", ".woff2", ".ttf", ".otf", ".eot",
18
- ]);
20
+ const assetExtensions = new Set([".png", ".jpg", ".jpeg", ".webp", ".avif", ".gif", ".svg", ".ico", ".woff", ".woff2", ".ttf", ".otf"]);
21
+ // Depodaki katalog temalarında bulunabilecek dosya türleri.
22
+ const repoExtensions = new Set([...assetExtensions, ".json", ".scss", ".css", ".md", ".txt", ".eot"]);
23
+
24
+ /** Pakete girmeyen, yalnızca geliştiriciye yardımcı dosyalar. */
25
+ export const localOnly = (rel) => rel === "README.md" || rel.startsWith("reference/") || rel.endsWith(".zip");
19
26
 
20
27
  // SCSS içinden çağrılabilecek dış adresler (yazı tipleri).
21
28
  const allowedStyleHosts = new Set(["fonts.googleapis.com", "fonts.gstatic.com", "fonts.bunny.net"]);
@@ -24,24 +31,25 @@ const scriptPatterns = [
24
31
  { pattern: /<script\b/i, text: "<script> etiketi" },
25
32
  { pattern: /javascript:/i, text: "javascript: adresi" },
26
33
  { pattern: /\son[a-z]+\s*=/i, text: "olay özniteliği (onclick, onload…)" },
27
- { pattern: /<iframe\b|<object\b|<embed\b/i, text: "gömülü içerik etiketi" },
34
+ { pattern: /<iframe\b|<object\b|<embed\b|<foreignObject\b/i, text: "gömülü içerik etiketi" },
28
35
  ];
29
36
 
30
37
  const secretPatterns = [
31
38
  /-----BEGIN [A-Z ]*PRIVATE KEY-----/,
32
39
  /\b(sk|pk)_(live|test)_[A-Za-z0-9]{16,}\b/,
33
40
  /\bAKIA[0-9A-Z]{16}\b/,
41
+ /\buvt_[A-Za-z0-9_-]{20,}\b/,
34
42
  // JSON içinde tırnaklar ters eğik çizgiyle kaçışlı olabilir.
35
43
  /\b(api[_-]?key|secret|password|token)\\?["']?\s*[:=]\s*\\?["'][^"'\\]{12,}/i,
36
44
  ];
37
45
 
38
- function readJson(file, report, label) {
46
+ function readJson(file, report, label, required = true) {
39
47
  if (!existsSync(file)) {
40
- report.errors.push(`${label} yok.`);
48
+ if (required) report.errors.push(`${label} yok.`);
41
49
  return null;
42
50
  }
43
51
  try {
44
- return JSON.parse(readFileSync(file, "utf8").replace(/^/, ""));
52
+ return JSON.parse(readFileSync(file, "utf8").replace(/^\uFEFF/, ""));
45
53
  } catch (error) {
46
54
  report.errors.push(`${label} geçerli JSON değil: ${error.message}`);
47
55
  return null;
@@ -82,22 +90,175 @@ function scanText(rel, text, report) {
82
90
  }
83
91
  }
84
92
 
85
- function scanStyle(rel, text, report) {
86
- if (/expression\s*\(|behavior\s*:|-moz-binding/i.test(text)) report.errors.push(`${rel}: tarayıcıda kod çalıştıran CSS (expression, behavior, -moz-binding) içeriyor.`);
87
- if (/javascript:/i.test(text)) report.errors.push(`${rel}: javascript: adresi içeriyor.`);
88
- for (const match of text.matchAll(/(?:@import\s+(?:url\()?\s*|url\(\s*)["']?((?:https?:)?\/\/[^"')\s]+)/gi)) {
89
- let host = "";
90
- try {
91
- host = new URL(match[1].startsWith("//") ? `https:${match[1]}` : match[1]).hostname;
92
- } catch {
93
- host = "?";
93
+ function hostOf(target) {
94
+ try {
95
+ return new URL(target.startsWith("//") ? `https:${target}` : target).hostname;
96
+ } catch {
97
+ return "?";
98
+ }
99
+ }
100
+
101
+ const allowedUrl = (target) => /^(https:)?\/\//i.test(target) && allowedStyleHosts.has(hostOf(target));
102
+
103
+ /**
104
+ * Stil denetimi. `strict` paketlerde açıktır: döngü, fonksiyon ve mixin tanımı yoktur (paket
105
+ * derleyiciyi kilitleyemez), yalnızca paketin kendi dosyaları içe aktarılır.
106
+ */
107
+ function scanStyle(rel, source, report, { strict = false, styles = new Set(), tokens = false } = {}) {
108
+ let text = source.replace(/\/\*[\s\S]*?\*\//g, " ").replace(/(^|[\s;{}])\/\/[^\n]*/gm, "$1");
109
+ const held = [];
110
+ text = text.replace(/"(?:[^"\\\n]|\\.)*"|'(?:[^'\\\n]|\\.)*'/g, (quoted) => `"~${held.push(quoted.slice(1, -1)) - 1}~"`);
111
+ const resolve = (value) => {
112
+ const match = /^"~(\d+)~"$/.exec(value.trim());
113
+ return (match ? held[Number(match[1])] ?? "" : value).trim();
114
+ };
115
+ const all = `${text} ${held.join(" ")}`;
116
+ if (/expression\s*\(|behavior\s*:|-moz-binding|<\/style/i.test(all)) report.errors.push(`${rel}: tarayıcıda kod çalıştıran CSS (expression, behavior, -moz-binding) içeriyor.`);
117
+ if (/javascript:/i.test(all)) report.errors.push(`${rel}: javascript: adresi içeriyor.`);
118
+ if (strict) {
119
+ const rule = /@(while|for|each|function|mixin|use|forward|debug|warn|error|charset)\b/i.exec(text);
120
+ if (rule) report.errors.push(`${rel}: @${rule[1].toLowerCase()} kullanılamaz. Tema stilleri döngü, fonksiyon ve mixin tanımı içeremez; temel temanın mixin'leri @include ile kullanılabilir.`);
121
+ if (text.includes("\\")) report.errors.push(`${rel}: tırnak dışında ters eğik çizgi kullanılamaz.`);
122
+ }
123
+ text = text.replace(/url\(([^)]*)\)/gi, (_, inner) => {
124
+ const target = resolve(inner);
125
+ if (/^data:/i.test(target)) {
126
+ if (strict && !/^data:(image\/(png|jpe?g|gif|webp)|font\/woff2?|application\/font-woff2?);base64,/i.test(target)) report.errors.push(`${rel}: yalnızca görsel ve yazı tipi data: adresleri kullanılabilir.`);
127
+ } else if ((target.includes("//") || (strict && target.includes(":"))) && !allowedUrl(target)) {
128
+ report.errors.push(`${rel}: izin verilmeyen dış adres (${hostOf(target)}). Görsel ve yazı tiplerini assets/ altına koyun; yalnızca şu adresler çağrılabilir: ${[...allowedStyleHosts].join(", ")}.`);
129
+ }
130
+ return "url()";
131
+ });
132
+ for (const match of text.matchAll(/@import\s+([^;]+);/gi)) {
133
+ for (const part of match[1].split(",")) {
134
+ if (/^\s*url\(\)/i.test(part)) continue;
135
+ const target = resolve(part);
136
+ if (target.includes("//") || target.includes(":")) {
137
+ if (!allowedUrl(target)) report.errors.push(`${rel}: izin verilmeyen dış adres (${hostOf(target)}).`);
138
+ } else if (strict && !(/^[a-z0-9]+(?:[-_][a-z0-9]+)*$/.test(target) && (styles.has(`${target}.scss`) || styles.has(`_${target}.scss`)))) {
139
+ report.errors.push(`${rel}: @import "${target}" pakette yok. Yalnızca paketin kendi styles/ dosyaları içe aktarılabilir.`);
140
+ }
141
+ }
142
+ }
143
+ if (tokens) {
144
+ for (const raw of text.split(";")) {
145
+ const statement = raw.trim();
146
+ if (statement === "" || /^\$[A-Za-z0-9_-]+\s*:\s*[^{}@]+$/.test(statement) || /^@import/i.test(statement)) continue;
147
+ report.errors.push(`${rel}: yalnızca değişken ataması ($ad: değer;) ve yazı tipi @import'u içerebilir.`);
148
+ break;
149
+ }
150
+ }
151
+ }
152
+
153
+ function checkIdentity(dir, meta, report) {
154
+ const key = typeof meta?.key === "string" ? meta.key.trim() : "";
155
+ report.key = key;
156
+ if (!meta) return key;
157
+ if (!keyPattern.test(key) || key.length > 40) report.errors.push(`theme.json: "key" küçük harf, rakam ve tireden oluşmalı (ör. "linea").`);
158
+ else if (key !== path.basename(dir)) report.errors.push(`theme.json: "key" (${key}) klasör adıyla (${path.basename(dir)}) aynı olmalı.`);
159
+ if (!String(meta.name ?? "").trim()) report.errors.push(`theme.json: "name" gerekli.`);
160
+ if (!versionPattern.test(String(meta.version ?? ""))) report.errors.push(`theme.json: "version" 1.0.0 biçiminde olmalı.`);
161
+ if (!String(meta.description ?? "").trim()) report.warnings.push(`theme.json: "description" boş; tema listesinde açıklama görünmez.`);
162
+ return key;
163
+ }
164
+
165
+ function checkTemplate(index, types, report, source) {
166
+ if (!index) return;
167
+ if (!Array.isArray(index.order) || index.order.length === 0) report.errors.push(`templates/index.json: "order" boş; ana sayfada bölüm görünmez.`);
168
+ for (const id of index.order ?? []) {
169
+ const section = index.sections?.[id];
170
+ if (!section) report.errors.push(`templates/index.json: "order" içindeki "${id}" bölümü "sections" altında tanımlı değil.`);
171
+ else if (types && !types.has(section.type)) report.errors.push(`templates/index.json: "${id}" bölümünün türü (${section.type}) ${source} yok.`);
172
+ }
173
+ }
174
+
175
+ function checkImages(data, assets, report) {
176
+ if (!data || typeof data !== "object") return;
177
+ for (const [name, value] of Object.entries(data)) {
178
+ if (typeof value !== "string" || !/^uploads\/[^\s]+\.(png|jpe?g|webp|avif|gif|svg|ico)$/i.test(value)) continue;
179
+ if (!existsSync(path.join(assets, value))) report.errors.push(`config/settings_data.json: "${name}" ayarındaki görsel bulunamadı (assets/${value}).`);
180
+ }
181
+ }
182
+
183
+ // ── paket ──────────────────────────────────────────────────────────────────────────────────────
184
+
185
+ function checkPackage(dir, meta, report) {
186
+ const base = String(meta?.base ?? "").trim();
187
+ if (meta && !keyPattern.test(base)) report.errors.push(`theme.json: "base" gerekli: paketin temel aldığı katalog teması (ör. "linea"). Temalar: https://themes.uvako.com`);
188
+ if (meta && (Number(meta.price ?? 0) < 0 || Number(meta.price ?? 0) > 100000 || Number.isNaN(Number(meta.price ?? 0)))) report.errors.push(`theme.json: "price" 0 ile 100.000 arasında olmalı.`);
189
+ if (meta?.demo_url && !/^https:\/\/[^\s]+$/.test(String(meta.demo_url))) report.errors.push(`theme.json: "demo_url" https:// ile başlayan bir adres olmalı.`);
190
+
191
+ const { files, links } = walk(dir);
192
+ for (const link of links) report.errors.push(`${link}: sembolik bağ pakete alınamaz.`);
193
+ const styles = new Set(files.filter((file) => file.rel.startsWith("styles/")).map((file) => file.rel.slice("styles/".length)));
194
+ const totals = { files: 0, bytes: 0, style: 0 };
195
+ let schema = null;
196
+ let data = null;
197
+ let index = null;
198
+ for (const file of files) {
199
+ const rel = file.rel;
200
+ if (localOnly(rel)) continue;
201
+ totals.files += 1;
202
+ totals.bytes += file.size;
203
+ const parts = rel.split("/");
204
+ if (parts.some((part) => !segmentPattern.test(part))) {
205
+ report.errors.push(`${rel}: dosya adında yalnızca harf, rakam, nokta, tire ve alt çizgi olabilir; gizli dosya olamaz.`);
206
+ continue;
94
207
  }
95
- if (!allowedStyleHosts.has(host)) report.errors.push(`${rel}: izin verilmeyen dış adres (${host}). Görsel ve yazı tiplerini assets/ altına koyun; yalnızca şu adresler çağrılabilir: ${[...allowedStyleHosts].join(", ")}.`);
208
+ const ext = path.extname(rel).toLowerCase();
209
+ const text = () => readFileSync(file.full, "utf8");
210
+ if (rel === "theme.json") scanText(rel, text(), report);
211
+ else if (rel === "config/settings_schema.json" || rel === "config/settings_data.json" || rel === "templates/index.json") {
212
+ if (file.size > 1024 * 1024) report.errors.push(`${rel}: 1 MB sınırını aşıyor.`);
213
+ else scanText(rel, text(), report);
214
+ } else if (parts[0] === "styles" && parts.length === 2 && stylePattern.test(parts[1])) {
215
+ totals.style += file.size;
216
+ if (file.size <= limits.styleBytes) {
217
+ const source = text();
218
+ scanText(rel, source, report);
219
+ scanStyle(rel, source, report, { strict: true, styles, tokens: parts[1] === "_tokens.scss" });
220
+ }
221
+ } else if (parts[0] === "assets" && parts.length >= 2) {
222
+ if (!assetExtensions.has(ext)) {
223
+ report.errors.push(`${rel}: bu dosya türü (${ext || "uzantısız"}) temada bulunamaz.`);
224
+ continue;
225
+ }
226
+ if (file.size > limits.fileBytes) report.errors.push(`${rel}: ${(file.size / 1024 / 1024).toFixed(1)} MB; tek dosya sınırı ${limits.fileBytes / 1024 / 1024} MB.`);
227
+ else if (file.size > 1024 * 1024 && /\.(png|jpe?g|gif)$/i.test(ext)) report.warnings.push(`${rel}: ${(file.size / 1024 / 1024).toFixed(1)} MB. WebP ya da sıkıştırılmış JPEG sayfayı hızlandırır.`);
228
+ if (ext === ".svg" && file.size <= limits.fileBytes) scanText(rel, text(), report);
229
+ } else {
230
+ report.errors.push(`${rel}: bu dosya pakette bulunamaz. İzin verilenler: theme.json, config/settings_schema.json, config/settings_data.json, templates/index.json, styles/*.scss, assets/**.`);
231
+ }
232
+ }
233
+ if (totals.style > limits.styleBytes) report.errors.push(`Stil dosyaları toplam ${limits.styleBytes / 1024} KB sınırını aşıyor.`);
234
+ if (styles.size > 0 && !styles.has("theme.scss") && !(styles.size === 1 && styles.has("_tokens.scss"))) report.errors.push("styles/theme.scss yok: stil dosyaları oradan içe aktarılır.");
235
+
236
+ schema = readJson(path.join(dir, "config", "settings_schema.json"), report, "config/settings_schema.json", false);
237
+ if (schema) {
238
+ if (schema.group?.type !== "tab" || !Array.isArray(schema.group?.values) || schema.group.values.length === 0) report.errors.push(`config/settings_schema.json: "group" sekmeli olmalı ({"type":"tab","values":[…]}).`);
239
+ schemaKeys(schema.group, new Set(), report);
240
+ }
241
+ data = readJson(path.join(dir, "config", "settings_data.json"), report, "config/settings_data.json", false);
242
+ if (data && (typeof data !== "object" || Array.isArray(data))) report.errors.push("config/settings_data.json: anahtar-değer nesnesi olmalı.");
243
+ index = readJson(path.join(dir, "templates", "index.json"), report, "templates/index.json", false);
244
+ // Bölüm türleri temel temadan gelir; `theme init` bunları reference/sections.json dosyasına yazar.
245
+ const reference = existsSync(path.join(dir, "reference", "sections.json")) ? readJson(path.join(dir, "reference", "sections.json"), { errors: [] }, "") : null;
246
+ checkTemplate(index, reference?.sections ? new Set(Object.keys(reference.sections)) : null, report, `${base} temasında`);
247
+ checkImages(data, path.join(dir, "assets"), report);
248
+ for (const shot of [meta?.screenshot, ...(Array.isArray(meta?.screenshots) ? meta.screenshots : [])]) {
249
+ const rel = String(shot ?? "").trim().replace(/^\//, "").replace(/^assets\//, "");
250
+ if (rel && !existsSync(path.join(dir, "assets", rel))) report.errors.push(`theme.json: ekran görüntüsü pakette yok (assets/${rel}).`);
96
251
  }
252
+ report.files = totals.files;
253
+ report.bytes = totals.bytes;
254
+ if (totals.bytes > limits.totalBytes) report.errors.push(`Tema ${(totals.bytes / 1024 / 1024).toFixed(0)} MB; sınır ${limits.totalBytes / 1024 / 1024} MB.`);
255
+ if (totals.files > limits.files) report.errors.push(`Tema ${totals.files} dosya içeriyor; sınır ${limits.files}.`);
97
256
  }
98
257
 
99
- function scanFiles(root, label, report, totals) {
100
- if (!existsSync(root)) return [];
258
+ // ── platform deposundaki katalog teması ────────────────────────────────────────────────────────
259
+
260
+ function scanRepoFiles(root, label, report, totals) {
261
+ if (!existsSync(root)) return;
101
262
  const { files, links } = walk(root);
102
263
  for (const link of links) report.errors.push(`${label}/${link}: sembolik bağ pakete alınamaz.`);
103
264
  for (const file of files) {
@@ -106,7 +267,7 @@ function scanFiles(root, label, report, totals) {
106
267
  totals.files += 1;
107
268
  totals.bytes += file.size;
108
269
  if (file.rel.split("/").some((part) => part === ".." || part.startsWith("."))) report.errors.push(`${rel}: gizli dosya ya da üst klasöre çıkan yol.`);
109
- if (!allowedExtensions.has(ext)) {
270
+ if (!repoExtensions.has(ext)) {
110
271
  report.errors.push(`${rel}: bu dosya türü (${ext || "uzantısız"}) temada bulunamaz.`);
111
272
  continue;
112
273
  }
@@ -114,92 +275,60 @@ function scanFiles(root, label, report, totals) {
114
275
  else if (file.size > 1024 * 1024 && /\.(png|jpe?g|gif)$/i.test(ext)) report.warnings.push(`${rel}: ${(file.size / 1024 / 1024).toFixed(1)} MB. WebP ya da sıkıştırılmış JPEG sayfayı hızlandırır.`);
115
276
  if ([".json", ".scss", ".css", ".svg", ".md", ".txt"].includes(ext) && file.size <= limits.fileBytes) {
116
277
  const text = readFileSync(file.full, "utf8");
117
- // Ayar verisindeki HTML alanları (alt bilgi menüsü gibi) platformda temizlenir; betik yine de reddedilir.
118
278
  scanText(rel, text, report);
119
279
  if (ext === ".scss" || ext === ".css") scanStyle(rel, text, report);
120
280
  }
121
281
  }
122
- return files;
123
282
  }
124
283
 
125
- export function checkTheme(dir) {
126
- const report = { dir, key: "", layout: "", errors: [], warnings: [], files: 0, bytes: 0 };
127
- if (!existsSync(path.join(dir, "theme.json"))) {
128
- report.errors.push(`${dir} bir tema klasörü değil: theme.json bulunamadı.`);
129
- return report;
130
- }
131
- const where = locate(dir);
132
- report.layout = where.layout;
133
-
134
- // ── kimlik ───────────────────────────────────────────────────────────
135
- const meta = readJson(path.join(dir, "theme.json"), report, "theme.json");
136
- const key = typeof meta?.key === "string" ? meta.key.trim() : "";
137
- report.key = key;
138
- if (meta) {
139
- if (!keyPattern.test(key) || key.length > 40) report.errors.push(`theme.json: "key" küçük harf, rakam ve tireden oluşmalı (ör. "linea").`);
140
- else if (key !== path.basename(dir)) report.errors.push(`theme.json: "key" (${key}) klasör adıyla (${path.basename(dir)}) aynı olmalı.`);
141
- if (!String(meta.name ?? "").trim()) report.errors.push(`theme.json: "name" gerekli.`);
142
- if (!versionPattern.test(String(meta.version ?? ""))) report.errors.push(`theme.json: "version" 1.0.0 biçiminde olmalı.`);
143
- if (!String(meta.description ?? "").trim()) report.warnings.push(`theme.json: "description" boş; tema listesinde açıklama görünmez.`);
144
- }
145
-
146
- // ── ayar şeması ve değerler ──────────────────────────────────────────
284
+ function checkRepo(dir, meta, key, where, report) {
147
285
  const schema = readJson(path.join(dir, "config", "settings_schema.json"), report, "config/settings_schema.json");
148
- const known = new Set();
149
286
  if (schema) {
150
287
  if (schema.group?.type !== "tab" || !Array.isArray(schema.group?.values)) report.errors.push(`config/settings_schema.json: "group" sekmeli olmalı ({"type":"tab","values":[…]}).`);
151
- schemaKeys(schema.group, known, report);
288
+ schemaKeys(schema.group, new Set(), report);
152
289
  }
153
290
  const data = readJson(path.join(dir, "config", "settings_data.json"), report, "config/settings_data.json");
154
291
  if (data && (typeof data !== "object" || Array.isArray(data))) report.errors.push("config/settings_data.json: anahtar-değer nesnesi olmalı.");
155
292
 
156
- // ── bölümler ve ana sayfa şablonu ────────────────────────────────────
157
293
  const sections = readJson(path.join(dir, "config", "sections.json"), report, "config/sections.json");
158
294
  const types = new Set(Object.keys(sections?.sections ?? {}));
159
295
  if (sections && types.size === 0) report.errors.push(`config/sections.json: "sections" altında en az bir bölüm türü olmalı.`);
160
296
  for (const [type, section] of Object.entries(sections?.sections ?? {})) {
161
297
  if (key && !type.startsWith(`${key}-`)) report.warnings.push(`config/sections.json: "${type}" bölüm türü tema anahtarıyla ("${key}-") başlamıyor.`);
162
298
  if (!String(section?.name ?? "").trim()) report.errors.push(`config/sections.json: "${type}" bölümünün adı yok.`);
163
- // "keys" isteğe bağlıdır (ayarı olmayan ya da ayarları başka biçimde bağlanan bölümler vardır); verilmişse liste olmalı.
299
+ // "keys" isteğe bağlıdır; verilmişse liste olmalı.
164
300
  if (section?.keys !== undefined && !Array.isArray(section.keys)) report.errors.push(`config/sections.json: "${type}" bölümünün "keys" alanı liste olmalı.`);
165
301
  }
166
- const index = readJson(path.join(dir, "templates", "index.json"), report, "templates/index.json (ana sayfa şablonu)");
167
- if (index) {
168
- if (!Array.isArray(index.order) || index.order.length === 0) report.errors.push(`templates/index.json: "order" boş; ana sayfada bölüm görünmez.`);
169
- for (const id of index.order ?? []) {
170
- const section = index.sections?.[id];
171
- if (!section) report.errors.push(`templates/index.json: "order" içindeki "${id}" bölümü "sections" altında tanımlı değil.`);
172
- else if (sections && !types.has(section.type)) report.errors.push(`templates/index.json: "${id}" bölümünün türü (${section.type}) config/sections.json içinde yok.`);
173
- }
174
- }
302
+ checkTemplate(readJson(path.join(dir, "templates", "index.json"), report, "templates/index.json (ana sayfa şablonu)"), sections ? types : null, report, "config/sections.json içinde");
175
303
 
176
- // ── dosyalar: tür, boyut, güvenlik ───────────────────────────────────
177
304
  const totals = { files: 0, bytes: 0 };
178
- if (where.layout === "package") {
179
- const { files } = { files: scanFiles(dir, ".", report, totals) };
180
- if (!files.some((file) => file.rel === "styles/theme.scss")) report.warnings.push("styles/theme.scss yok; tema platformun temel stilleriyle görünür.");
181
- } else {
182
- for (const sub of ["config", "templates"]) scanFiles(path.join(dir, sub), sub, report, totals);
183
- scanFiles(where.styles, "styles", report, totals);
184
- scanFiles(where.assets, "assets", report, totals);
185
- if (!existsSync(path.join(where.styles, "theme.scss"))) report.errors.push(`Stil dosyası yok: ${path.join(where.styles, "theme.scss")}`);
186
- }
305
+ for (const sub of ["config", "templates"]) scanRepoFiles(path.join(dir, sub), sub, report, totals);
306
+ scanRepoFiles(where.styles, "styles", report, totals);
307
+ scanRepoFiles(where.assets, "assets", report, totals);
308
+ if (!existsSync(path.join(where.styles, "theme.scss"))) report.errors.push(`Stil dosyası yok: ${path.join(where.styles, "theme.scss")}`);
187
309
  report.files = totals.files;
188
310
  report.bytes = totals.bytes;
189
311
  if (totals.bytes > limits.totalBytes) report.errors.push(`Tema ${(totals.bytes / 1024 / 1024).toFixed(0)} MB; sınır ${limits.totalBytes / 1024 / 1024} MB.`);
190
312
  if (totals.files > limits.files) report.errors.push(`Tema ${totals.files} dosya içeriyor; sınır ${limits.files}.`);
191
-
192
- // ── ayarlardaki görseller yerinde mi ─────────────────────────────────
193
- if (data && typeof data === "object") {
194
- for (const [name, value] of Object.entries(data)) {
195
- if (typeof value !== "string" || !/^uploads\/[^\s]+\.(png|jpe?g|webp|avif|gif|svg|ico)$/i.test(value)) continue;
196
- if (!existsSync(path.join(where.assets, value))) report.errors.push(`config/settings_data.json: "${name}" ayarındaki görsel bulunamadı (assets/${value}).`);
197
- }
198
- }
313
+ checkImages(data, where.assets, report);
199
314
  if (meta?.screenshot) {
200
315
  const shot = String(meta.screenshot).replace(new RegExp(`^/themes/${key}/assets/`), "");
201
316
  if (!existsSync(path.join(where.assets, shot))) report.warnings.push(`theme.json: ekran görüntüsü bulunamadı (assets/${shot}).`);
202
317
  }
318
+ }
319
+
320
+ export function checkTheme(dir) {
321
+ const report = { dir, key: "", layout: "", errors: [], warnings: [], files: 0, bytes: 0 };
322
+ if (!existsSync(path.join(dir, "theme.json"))) {
323
+ report.errors.push(`${dir} bir tema klasörü değil: theme.json bulunamadı.`);
324
+ return report;
325
+ }
326
+ const where = locate(dir);
327
+ report.layout = where.layout;
328
+ const meta = readJson(path.join(dir, "theme.json"), report, "theme.json");
329
+ const key = checkIdentity(dir, meta, report);
330
+ if (where.layout === "package") checkPackage(dir, meta, report);
331
+ else checkRepo(dir, meta, key, where, report);
203
332
  return report;
204
333
  }
205
334