uvako-cli 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,3 +1,58 @@
1
- # Temporary Holding Version
1
+ # uvako-cli
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Uvako tema geliştirme aracı. Node.js 22 ve üzeri gerekir; ek bağımlılığı yoktur.
4
+
5
+ ```bash
6
+ node cli/bin/uvako-cli.js help
7
+ ```
8
+
9
+ ## Komutlar
10
+
11
+ | Komut | Ne yapar |
12
+ |---|---|
13
+ | `uvako-cli theme init <anahtar>` | Yeni tema iskeleti oluşturur (`--name`, `--author`). |
14
+ | `uvako-cli theme check [klasör]` | Temayı denetler: yapı, ayarlar, bölümler, görseller, güvenlik. |
15
+ | `uvako-cli theme package [klasör]` | Denetimden geçen temayı zip paketine çevirir (`--out`). |
16
+ | `uvako-cli version` | Sürümü yazar. |
17
+
18
+ `check`, hem bağımsız tema klasörlerinde hem platform deposundaki temalarda (`themes/<anahtar>`) çalışır;
19
+ depodayken stil ve görselleri `frontend/storefront` altındaki yerlerinden okur. Hata varsa çıkış kodu 1'dir.
20
+
21
+ ## Tema paketi
22
+
23
+ Tema kod içermez. Paket şunlardan oluşur:
24
+
25
+ ```
26
+ theme.json kimlik: key, name, version
27
+ config/settings_schema.json panelde görünen ayarlar
28
+ config/settings_data.json ayarların varsayılan değerleri
29
+ config/sections.json ana sayfa bölüm türleri
30
+ templates/index.json ana sayfadaki bölüm sırası
31
+ styles/ SCSS (renk / yazı tipi değişkenleri ve temaya özgü kurallar)
32
+ assets/ logo, slayt ve banner görselleri, yazı tipleri
33
+ ```
34
+
35
+ ## Güvenlik kuralları
36
+
37
+ `check` şunları reddeder:
38
+
39
+ - JSON, SCSS/CSS, görsel ve yazı tipi dışındaki her dosya türü (JavaScript, TypeScript, HTML, PHP, çalıştırılabilir dosyalar).
40
+ - `<script>`, `javascript:` adresleri, `onclick` gibi olay öznitelikleri, `<iframe>` (SVG ve ayar metinleri dahil).
41
+ - Kod çalıştıran CSS (`expression`, `behavior`, `-moz-binding`).
42
+ - SCSS içinden yazı tipi sağlayıcıları dışındaki adreslere yapılan çağrılar.
43
+ - Sembolik bağlar, gizli dosyalar, 5 MB'tan büyük dosyalar, 60 MB'tan büyük ya da 600 dosyadan fazla paketler.
44
+ - Gizli anahtar ya da parola gibi görünen değerler.
45
+
46
+ Araç platformun kaynak kodunu içermez ve ağa bağlanmaz.
47
+
48
+ ## Henüz olmayanlar
49
+
50
+ Yerel önizleme (`theme dev`) ve mağazaya yükleme (`login`, `theme push`, `theme pull`) yoktur. Bunlar
51
+ platformda tema yükleme uç noktası ve paket biçimindeki temaların vitrinde çizilmesini gerektirir;
52
+ tasarım `docs/uvako-cli.md` dosyasındadır.
53
+
54
+ ## Test
55
+
56
+ ```bash
57
+ node --test
58
+ ```
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env node
2
+ import { main } from "../src/main.js";
3
+
4
+ main(process.argv.slice(2)).then(
5
+ (code) => process.exit(code),
6
+ (error) => {
7
+ console.error(`Hata: ${error?.message ?? error}`);
8
+ process.exit(1);
9
+ },
10
+ );
package/package.json CHANGED
@@ -1,6 +1,22 @@
1
1
  {
2
2
  "name": "uvako-cli",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "description": "Uvako tema geliştirme aracı: tema başlat, denetle, paketle.",
5
+ "type": "module",
6
+ "license": "UNLICENSED",
7
+ "bin": {
8
+ "uvako-cli": "bin/uvako-cli.js",
9
+ "uvako": "bin/uvako-cli.js"
10
+ },
11
+ "files": [
12
+ "bin",
13
+ "src",
14
+ "README.md"
15
+ ],
16
+ "engines": {
17
+ "node": ">=22"
18
+ },
19
+ "scripts": {
20
+ "test": "node --test"
21
+ }
22
+ }
package/src/check.js ADDED
@@ -0,0 +1,213 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import path from "node:path";
3
+ import { walk } from "./files.js";
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.
7
+
8
+ export const limits = { fileBytes: 5 * 1024 * 1024, totalBytes: 60 * 1024 * 1024, files: 600 };
9
+
10
+ const keyPattern = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
11
+ const versionPattern = /^\d+\.\d+\.\d+$/;
12
+
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
+ ]);
19
+
20
+ // SCSS içinden çağrılabilecek dış adresler (yazı tipleri).
21
+ const allowedStyleHosts = new Set(["fonts.googleapis.com", "fonts.gstatic.com", "fonts.bunny.net"]);
22
+
23
+ const scriptPatterns = [
24
+ { pattern: /<script\b/i, text: "<script> etiketi" },
25
+ { pattern: /javascript:/i, text: "javascript: adresi" },
26
+ { 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" },
28
+ ];
29
+
30
+ const secretPatterns = [
31
+ /-----BEGIN [A-Z ]*PRIVATE KEY-----/,
32
+ /\b(sk|pk)_(live|test)_[A-Za-z0-9]{16,}\b/,
33
+ /\bAKIA[0-9A-Z]{16}\b/,
34
+ // JSON içinde tırnaklar ters eğik çizgiyle kaçışlı olabilir.
35
+ /\b(api[_-]?key|secret|password|token)\\?["']?\s*[:=]\s*\\?["'][^"'\\]{12,}/i,
36
+ ];
37
+
38
+ function readJson(file, report, label) {
39
+ if (!existsSync(file)) {
40
+ report.errors.push(`${label} yok.`);
41
+ return null;
42
+ }
43
+ try {
44
+ return JSON.parse(readFileSync(file, "utf8").replace(/^/, ""));
45
+ } catch (error) {
46
+ report.errors.push(`${label} geçerli JSON değil: ${error.message}`);
47
+ return null;
48
+ }
49
+ }
50
+
51
+ // Şemadaki ayar adlarını toplar ({"type":"tab","values":[{name, settings, group}]}).
52
+ function schemaKeys(node, into, report, trail = "") {
53
+ if (!node || typeof node !== "object") return;
54
+ for (const tab of Array.isArray(node.values) ? node.values : []) {
55
+ if (!tab || typeof tab !== "object") continue;
56
+ const where = trail ? `${trail} › ${tab.name ?? "?"}` : String(tab.name ?? "?");
57
+ for (const [key, setting] of Object.entries(tab.settings ?? {})) {
58
+ into.add(key);
59
+ if (!setting || typeof setting !== "object" || !setting.type) report.errors.push(`Ayar şeması: "${key}" ayarının türü yok (${where}).`);
60
+ }
61
+ schemaKeys(tab.group, into, report, where);
62
+ }
63
+ }
64
+
65
+ /** Platform deposundaki tema mı (themes/<anahtar> + frontend/storefront/...), bağımsız paket mi? */
66
+ function locate(dir) {
67
+ const key = path.basename(dir);
68
+ const repo = path.resolve(dir, "..", "..");
69
+ const storefront = path.join(repo, "frontend", "storefront");
70
+ if (path.basename(path.dirname(dir)) === "themes" && existsSync(storefront)) {
71
+ return { layout: "repo", styles: path.join(storefront, "themes", key, "styles"), assets: path.join(storefront, "public", "themes", key, "assets") };
72
+ }
73
+ return { layout: "package", styles: path.join(dir, "styles"), assets: path.join(dir, "assets") };
74
+ }
75
+
76
+ function scanText(rel, text, report) {
77
+ for (const { pattern, text: what } of scriptPatterns) {
78
+ if (pattern.test(text)) report.errors.push(`${rel}: ${what} içeriyor. Tema içinde betik çalıştırılamaz.`);
79
+ }
80
+ for (const pattern of secretPatterns) {
81
+ if (pattern.test(text)) report.errors.push(`${rel}: gizli anahtar ya da parola gibi görünen bir değer içeriyor.`);
82
+ }
83
+ }
84
+
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 = "?";
94
+ }
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(", ")}.`);
96
+ }
97
+ }
98
+
99
+ function scanFiles(root, label, report, totals) {
100
+ if (!existsSync(root)) return [];
101
+ const { files, links } = walk(root);
102
+ for (const link of links) report.errors.push(`${label}/${link}: sembolik bağ pakete alınamaz.`);
103
+ for (const file of files) {
104
+ const rel = `${label}/${file.rel}`;
105
+ const ext = path.extname(file.rel).toLowerCase();
106
+ totals.files += 1;
107
+ totals.bytes += file.size;
108
+ 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)) {
110
+ report.errors.push(`${rel}: bu dosya türü (${ext || "uzantısız"}) temada bulunamaz.`);
111
+ continue;
112
+ }
113
+ 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.`);
114
+ 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
+ if ([".json", ".scss", ".css", ".svg", ".md", ".txt"].includes(ext) && file.size <= limits.fileBytes) {
116
+ const text = readFileSync(file.full, "utf8");
117
+ // Ayar verisindeki HTML alanları (alt bilgi menüsü gibi) platformda temizlenir; betik yine de reddedilir.
118
+ scanText(rel, text, report);
119
+ if (ext === ".scss" || ext === ".css") scanStyle(rel, text, report);
120
+ }
121
+ }
122
+ return files;
123
+ }
124
+
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 ──────────────────────────────────────────
147
+ const schema = readJson(path.join(dir, "config", "settings_schema.json"), report, "config/settings_schema.json");
148
+ const known = new Set();
149
+ if (schema) {
150
+ 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);
152
+ }
153
+ const data = readJson(path.join(dir, "config", "settings_data.json"), report, "config/settings_data.json");
154
+ if (data && (typeof data !== "object" || Array.isArray(data))) report.errors.push("config/settings_data.json: anahtar-değer nesnesi olmalı.");
155
+
156
+ // ── bölümler ve ana sayfa şablonu ────────────────────────────────────
157
+ const sections = readJson(path.join(dir, "config", "sections.json"), report, "config/sections.json");
158
+ const types = new Set(Object.keys(sections?.sections ?? {}));
159
+ if (sections && types.size === 0) report.errors.push(`config/sections.json: "sections" altında en az bir bölüm türü olmalı.`);
160
+ for (const [type, section] of Object.entries(sections?.sections ?? {})) {
161
+ 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
+ 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ı.
164
+ 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
+ }
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
+ }
175
+
176
+ // ── dosyalar: tür, boyut, güvenlik ───────────────────────────────────
177
+ 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
+ }
187
+ report.files = totals.files;
188
+ report.bytes = totals.bytes;
189
+ 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
+ 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
+ }
199
+ if (meta?.screenshot) {
200
+ const shot = String(meta.screenshot).replace(new RegExp(`^/themes/${key}/assets/`), "");
201
+ if (!existsSync(path.join(where.assets, shot))) report.warnings.push(`theme.json: ekran görüntüsü bulunamadı (assets/${shot}).`);
202
+ }
203
+ return report;
204
+ }
205
+
206
+ export function printReport(report) {
207
+ const title = report.key ? `${report.key} (${report.layout === "repo" ? "platform deposu" : "paket"})` : report.dir;
208
+ for (const error of report.errors) console.error(` ✖ ${error}`);
209
+ for (const warning of report.warnings) console.warn(` ! ${warning}`);
210
+ const size = report.files ? ` · ${report.files} dosya, ${(report.bytes / 1024 / 1024).toFixed(1)} MB` : "";
211
+ if (report.errors.length) console.error(`${title}: ${report.errors.length} hata, ${report.warnings.length} uyarı${size}`);
212
+ else console.log(`${title}: denetim geçti${report.warnings.length ? `, ${report.warnings.length} uyarı` : ""}${size}`);
213
+ }
package/src/files.js ADDED
@@ -0,0 +1,24 @@
1
+ import { lstatSync, readdirSync } from "node:fs";
2
+ import path from "node:path";
3
+
4
+ // Pakete hiç girmeyen adlar (sürüm kontrolü, bağımlılıklar, işletim sistemi dosyaları).
5
+ const skipped = new Set([".git", "node_modules", ".DS_Store", "Thumbs.db", ".idea", ".vscode"]);
6
+
7
+ /** Klasördeki dosyaları (alt klasörlerle) "a/b.c" biçiminde, sıralı döndürür. Sembolik bağlar ayrıca bildirilir. */
8
+ export function walk(root) {
9
+ const files = [];
10
+ const links = [];
11
+ const visit = (dir, prefix) => {
12
+ for (const name of readdirSync(dir).sort()) {
13
+ if (skipped.has(name)) continue;
14
+ const full = path.join(dir, name);
15
+ const rel = prefix ? `${prefix}/${name}` : name;
16
+ const stat = lstatSync(full);
17
+ if (stat.isSymbolicLink()) links.push(rel);
18
+ else if (stat.isDirectory()) visit(full, rel);
19
+ else if (stat.isFile()) files.push({ rel, full, size: stat.size });
20
+ }
21
+ };
22
+ visit(root, "");
23
+ return { files, links };
24
+ }
package/src/init.js ADDED
@@ -0,0 +1,129 @@
1
+ import { existsSync, mkdirSync, writeFileSync } from "node:fs";
2
+ import path from "node:path";
3
+
4
+ // Yeni tema iskeleti. Tema kod içermez: ayar şeması, bölüm sırası, SCSS ve görsellerden oluşur.
5
+ const keyPattern = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
6
+
7
+ const json = (value) => `${JSON.stringify(value, null, 2)}\n`;
8
+
9
+ function logo(name) {
10
+ return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 240 48" width="240" height="48">
11
+ <text x="0" y="34" font-family="Georgia, 'Times New Roman', serif" font-size="30" letter-spacing="4" fill="#111111">${name.toUpperCase().replace(/[<>&"']/g, "")}</text>
12
+ </svg>
13
+ `;
14
+ }
15
+
16
+ export function initTheme(key, { name, author, cwd }) {
17
+ if (!keyPattern.test(key) || key.length > 40) throw new Error(`Tema anahtarı küçük harf, rakam ve tireden oluşmalı (ör. "atolye"): ${key}`);
18
+ const dir = path.join(cwd, key);
19
+ if (existsSync(dir)) throw new Error(`Klasör zaten var: ${dir}`);
20
+ const title = String(name || key.replace(/(^|-)([a-z])/g, (_, dash, letter) => (dash ? " " : "") + letter.toUpperCase()));
21
+ const write = (rel, content) => {
22
+ const file = path.join(dir, rel);
23
+ mkdirSync(path.dirname(file), { recursive: true });
24
+ writeFileSync(file, content, "utf8");
25
+ };
26
+
27
+ write("theme.json", json({ key, name: title, version: "0.1.0", author: String(author || ""), description: `${title} teması.`, base: "editorial", screenshot: "" }));
28
+
29
+ write("config/settings_schema.json", json({
30
+ theme_version: 2,
31
+ group: {
32
+ type: "tab",
33
+ values: [
34
+ {
35
+ name: "Genel Ayarlar",
36
+ settings: {
37
+ logo: { type: "image", default_value: "uploads/logo.svg", filename: "logo", label: "Logo", info: "" },
38
+ color_primary: { type: "color", default_value: "#111111", label: "Ana renk", info: "Düğme ve vurgu rengi." },
39
+ header_top_bar_text: { type: "text", default_value: "", label: "Üst şerit yazısı", info: "" },
40
+ },
41
+ },
42
+ {
43
+ name: "Ana Sayfa",
44
+ settings: {
45
+ slide_status: { type: "checkbox", default_value: 1, label: "Slaytı göster", info: "" },
46
+ slider_status_1: { type: "checkbox", default_value: 1, label: "1. slayt açık", info: "" },
47
+ slider_picture_1: { type: "image", default_value: "", filename: "slider_1", label: "1. slayt görseli", info: "Önerilen: 1920×1080" },
48
+ slider_title_1: { type: "text", default_value: "", label: "1. slayt başlığı", info: "" },
49
+ slider_button_1: { type: "text", default_value: "", label: "1. slayt düğme yazısı", info: "" },
50
+ slider_link_1: { type: "text", default_value: "/search", label: "1. slayt bağlantısı", info: "" },
51
+ home_products_title: { type: "text", default_value: "Yeni gelenler", label: "Ürün vitrini başlığı", info: "" },
52
+ },
53
+ },
54
+ ],
55
+ },
56
+ }));
57
+
58
+ write("config/settings_data.json", json({
59
+ logo: "uploads/logo.svg",
60
+ color_primary: "#111111",
61
+ header_top_bar_text: "",
62
+ slide_status: 1,
63
+ slider_status_1: 1,
64
+ slider_title_1: "Yeni koleksiyon",
65
+ slider_button_1: "Keşfet",
66
+ slider_link_1: "/search",
67
+ home_products_title: "Yeni gelenler",
68
+ }));
69
+
70
+ write("config/sections.json", json({
71
+ sections: {
72
+ [`${key}-slider`]: { name: "Slayt", icon: "image", keys: ["slide_status", "slider_status_1", "slider_picture_1", "slider_title_1", "slider_button_1", "slider_link_1"] },
73
+ [`${key}-home-products`]: { name: "Ürün vitrini", icon: "grid", keys: ["home_products_title"] },
74
+ },
75
+ }));
76
+
77
+ write("templates/index.json", json({
78
+ order: ["slider", "home_products"],
79
+ sections: {
80
+ slider: { type: `${key}-slider`, disabled: false, settings: {} },
81
+ home_products: { type: `${key}-home-products`, disabled: false, settings: {} },
82
+ },
83
+ }));
84
+
85
+ write("styles/_tokens.scss", `// Temanın renk ve yazı tipi değişkenleri. Platformun temel stilleri bu değerlerle derlenir.
86
+ $font-family: 'Manrope', sans-serif;
87
+ $font-family-sub: 'Bodoni Moda', 'Times New Roman', serif;
88
+
89
+ $color-text: #111111;
90
+ $color-sub-text: #6b6b6b;
91
+ $color-border: #e5e5e5;
92
+ $color-background: #ffffff;
93
+ $border-radius: 0;
94
+ `);
95
+
96
+ write("styles/theme.scss", `// Temaya özgü kurallar. Bütün seçiciler .theme-${key} altında yazılır.
97
+ @import 'tokens';
98
+
99
+ .theme-${key} {
100
+ .products-header span {
101
+ font-family: $font-family-sub;
102
+ }
103
+ }
104
+ `);
105
+
106
+ write("assets/uploads/logo.svg", logo(title));
107
+
108
+ write("README.md", `# ${title}
109
+
110
+ Uvako teması. Bu klasör kod içermez: ayar şeması, ana sayfa bölüm sırası, SCSS ve görseller.
111
+
112
+ - \`theme.json\` — kimlik (anahtar, ad, sürüm).
113
+ - \`config/settings_schema.json\` — mağaza sahibinin panelde göreceği ayarlar.
114
+ - \`config/settings_data.json\` — ayarların varsayılan değerleri.
115
+ - \`config/sections.json\` — ana sayfa bölüm türleri ve bağlı ayarları.
116
+ - \`templates/index.json\` — ana sayfadaki bölümlerin sırası.
117
+ - \`styles/\` — renk / yazı tipi değişkenleri ve temaya özgü SCSS.
118
+ - \`assets/\` — logo, slayt ve banner görselleri.
119
+
120
+ \`\`\`bash
121
+ uvako-cli theme check .
122
+ \`\`\`
123
+
124
+ \`\`\`bash
125
+ uvako-cli theme package .
126
+ \`\`\`
127
+ `);
128
+ return dir;
129
+ }
package/src/main.js ADDED
@@ -0,0 +1,80 @@
1
+ import { readFileSync } from "node:fs";
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { checkTheme, printReport } from "./check.js";
5
+ import { initTheme } from "./init.js";
6
+ import { packageTheme } from "./package.js";
7
+
8
+ const version = JSON.parse(readFileSync(path.join(path.dirname(fileURLToPath(import.meta.url)), "..", "package.json"), "utf8")).version;
9
+
10
+ const help = `uvako-cli ${version} — Uvako tema geliştirme aracı
11
+
12
+ Kullanım:
13
+ uvako-cli theme init <anahtar> [--name "Ad"] [--author "Yazar"] Yeni tema iskeleti oluşturur
14
+ uvako-cli theme check [klasör] Temayı denetler (yapı, ayarlar, güvenlik)
15
+ uvako-cli theme package [klasör] [--out dosya.zip] Denetleyip yüklenebilir zip paketi üretir
16
+ uvako-cli version Sürümü yazar
17
+
18
+ Tema, kod çalıştırmayan bir pakettir: ayar şeması, bölüm sırası, SCSS ve görseller.
19
+ JavaScript, TypeScript ya da sunucu kodu içeren paketler reddedilir.`;
20
+
21
+ // "--ad değer" ve "--ad=değer" biçimli seçenekleri ayırır; kalanlar konumsal argümandır.
22
+ export function parseArgs(args) {
23
+ const options = {};
24
+ const rest = [];
25
+ for (let i = 0; i < args.length; i++) {
26
+ const arg = args[i];
27
+ if (!arg.startsWith("--")) {
28
+ rest.push(arg);
29
+ continue;
30
+ }
31
+ const [name, inline] = arg.slice(2).split(/=(.*)/s);
32
+ if (inline !== undefined) options[name] = inline;
33
+ else if (i + 1 < args.length && !args[i + 1].startsWith("--")) options[name] = args[++i];
34
+ else options[name] = true;
35
+ }
36
+ return { options, rest };
37
+ }
38
+
39
+ export async function main(argv) {
40
+ const { options, rest } = parseArgs(argv);
41
+ const [group, command, target] = rest;
42
+ if (!group || group === "help" || options.help) {
43
+ console.log(help);
44
+ return 0;
45
+ }
46
+ if (group === "version" || options.version) {
47
+ console.log(version);
48
+ return 0;
49
+ }
50
+ if (group !== "theme") {
51
+ console.error(`Bilinmeyen komut: ${group}\n\n${help}`);
52
+ return 1;
53
+ }
54
+ if (command === "init") {
55
+ if (!target) throw new Error("Tema anahtarı gerekli: uvako-cli theme init <anahtar>");
56
+ const dir = initTheme(target, { name: options.name, author: options.author, cwd: process.cwd() });
57
+ console.log(`Tema oluşturuldu: ${path.relative(process.cwd(), dir) || "."}`);
58
+ console.log(`Sıradaki adım: uvako-cli theme check ${path.relative(process.cwd(), dir)}`);
59
+ return 0;
60
+ }
61
+ if (command === "check") {
62
+ const report = checkTheme(path.resolve(target ?? "."));
63
+ printReport(report);
64
+ return report.errors.length ? 1 : 0;
65
+ }
66
+ if (command === "package") {
67
+ const result = packageTheme(path.resolve(target ?? "."), { out: typeof options.out === "string" ? options.out : undefined });
68
+ if (!result.ok) {
69
+ printReport(result.report);
70
+ console.error("Paket oluşturulmadı: önce hataları düzeltin.");
71
+ return 1;
72
+ }
73
+ printReport(result.report);
74
+ console.log(`Paket: ${result.file} (${result.files} dosya, ${(result.bytes / 1024).toFixed(0)} KB)`);
75
+ console.log(`SHA-256: ${result.sha256}`);
76
+ return 0;
77
+ }
78
+ console.error(`Bilinmeyen tema komutu: ${command ?? "(yok)"}\n\n${help}`);
79
+ return 1;
80
+ }
package/src/package.js ADDED
@@ -0,0 +1,75 @@
1
+ import { createHash } from "node:crypto";
2
+ import { readFileSync, writeFileSync } from "node:fs";
3
+ import path from "node:path";
4
+ import zlib from "node:zlib";
5
+ import { checkTheme } from "./check.js";
6
+ import { walk } from "./files.js";
7
+
8
+ // Denetimden geçen temayı zip paketine çevirir. Zip burada yazılır (ek bağımlılık yok); dosya
9
+ // tarihleri sabit tutulur, böylece aynı içerik her zaman aynı SHA-256 özetini verir.
10
+ const DOS_DATE = ((2026 - 1980) << 9) | (1 << 5) | 1;
11
+
12
+ function zip(entries) {
13
+ const parts = [];
14
+ const central = [];
15
+ let offset = 0;
16
+ for (const { name, data } of entries) {
17
+ const nameBytes = Buffer.from(name, "utf8");
18
+ const compressed = zlib.deflateRawSync(data, { level: 9 });
19
+ const crc = zlib.crc32(data);
20
+ const local = Buffer.alloc(30);
21
+ local.writeUInt32LE(0x04034b50, 0);
22
+ local.writeUInt16LE(20, 4);
23
+ local.writeUInt16LE(0x0800, 6); // UTF-8 dosya adları
24
+ local.writeUInt16LE(8, 8);
25
+ local.writeUInt16LE(0, 10);
26
+ local.writeUInt16LE(DOS_DATE, 12);
27
+ local.writeUInt32LE(crc, 14);
28
+ local.writeUInt32LE(compressed.length, 18);
29
+ local.writeUInt32LE(data.length, 22);
30
+ local.writeUInt16LE(nameBytes.length, 26);
31
+ parts.push(local, nameBytes, compressed);
32
+
33
+ const header = Buffer.alloc(46);
34
+ header.writeUInt32LE(0x02014b50, 0);
35
+ header.writeUInt16LE(20, 4);
36
+ header.writeUInt16LE(20, 6);
37
+ header.writeUInt16LE(0x0800, 8);
38
+ header.writeUInt16LE(8, 10);
39
+ header.writeUInt16LE(0, 12);
40
+ header.writeUInt16LE(DOS_DATE, 14);
41
+ header.writeUInt32LE(crc, 16);
42
+ header.writeUInt32LE(compressed.length, 20);
43
+ header.writeUInt32LE(data.length, 24);
44
+ header.writeUInt16LE(nameBytes.length, 28);
45
+ header.writeUInt32LE(offset, 42);
46
+ central.push(header, nameBytes);
47
+ offset += local.length + nameBytes.length + compressed.length;
48
+ }
49
+ const centralSize = central.reduce((sum, part) => sum + part.length, 0);
50
+ const end = Buffer.alloc(22);
51
+ end.writeUInt32LE(0x06054b50, 0);
52
+ end.writeUInt16LE(entries.length, 8);
53
+ end.writeUInt16LE(entries.length, 10);
54
+ end.writeUInt32LE(centralSize, 12);
55
+ end.writeUInt32LE(offset, 16);
56
+ return Buffer.concat([...parts, ...central, end]);
57
+ }
58
+
59
+ export function packageTheme(dir, { out } = {}) {
60
+ const report = checkTheme(dir);
61
+ if (report.errors.length) return { ok: false, report };
62
+ if (report.layout !== "package") {
63
+ report.errors.push("Bu klasör platform deposundaki bir tema; paketleme yalnızca bağımsız tema klasörlerinde yapılır.");
64
+ return { ok: false, report };
65
+ }
66
+ const meta = JSON.parse(readFileSync(path.join(dir, "theme.json"), "utf8"));
67
+ const { files } = walk(dir);
68
+ const entries = files
69
+ .filter((file) => !file.rel.endsWith(".zip"))
70
+ .map((file) => ({ name: `${meta.key}/${file.rel}`, data: readFileSync(file.full) }));
71
+ const buffer = zip(entries);
72
+ const file = path.resolve(out ?? path.join(path.dirname(dir), `${meta.key}-${meta.version}.zip`));
73
+ writeFileSync(file, buffer);
74
+ return { ok: true, report, file, files: entries.length, bytes: buffer.length, sha256: createHash("sha256").update(buffer).digest("hex") };
75
+ }