uvako-cli 0.1.1 → 0.2.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/src/config.js ADDED
@@ -0,0 +1,100 @@
1
+ import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+
5
+ // Giriş bilgileri kullanıcının ev klasöründe, yalnızca kullanıcının okuyabildiği bir dosyada durur.
6
+ // Tema klasörüne hiçbir zaman yazılmaz; böylece pakete ya da sürüm kontrolüne karışmaz.
7
+
8
+ export const defaultStoreApex = "uvoo.store";
9
+ export const siteOrigin = () => process.env.UVAKO_SITE_URL || "https://uvako.com";
10
+
11
+ export function configDir() {
12
+ if (process.env.UVAKO_CONFIG_DIR) return process.env.UVAKO_CONFIG_DIR;
13
+ if (process.platform === "win32" && process.env.APPDATA) return path.join(process.env.APPDATA, "uvako");
14
+ return path.join(process.env.XDG_CONFIG_HOME || path.join(os.homedir(), ".config"), "uvako");
15
+ }
16
+
17
+ const file = () => path.join(configDir(), "credentials.json");
18
+
19
+ function read() {
20
+ try {
21
+ const data = JSON.parse(readFileSync(file(), "utf8"));
22
+ return { current: typeof data.current === "string" ? data.current : "", stores: data.stores && typeof data.stores === "object" ? data.stores : {} };
23
+ } catch {
24
+ return { current: "", stores: {} };
25
+ }
26
+ }
27
+
28
+ function write(data) {
29
+ mkdirSync(configDir(), { recursive: true, mode: 0o700 });
30
+ writeFileSync(file(), `${JSON.stringify(data, null, 2)}\n`, { mode: 0o600 });
31
+ // Dosya önceden varsa izinleri yazma sırasında değişmez.
32
+ try {
33
+ chmodSync(file(), 0o600);
34
+ } catch {
35
+ // Windows'ta dosya zaten yalnızca kullanıcının profilindedir.
36
+ }
37
+ }
38
+
39
+ const local = (hostname) => hostname === "localhost" || hostname === "127.0.0.1" || hostname.endsWith(".localhost");
40
+
41
+ /**
42
+ * Mağaza adresini mağazanın kök adresine çevirir: "magazam" → https://magazam.uvoo.store,
43
+ * "www.magazam.com" → https://www.magazam.com. Şifresiz (http) bağlantı yalnızca yerel
44
+ * geliştirme adreslerinde kabul edilir: erişim anahtarı açık ağdan gönderilmez.
45
+ */
46
+ export function storeOrigin(input) {
47
+ let value = String(input ?? "").trim().replace(/\/+$/, "");
48
+ if (!value) throw new Error("Mağaza adresi gerekli (ör. magazam ya da magazam.uvoo.store).");
49
+ if (!/^https?:\/\//i.test(value)) {
50
+ if (!value.includes(".") && !value.includes(":")) value = `${value}.${defaultStoreApex}`;
51
+ value = `${local(value.split(":")[0]) ? "http" : "https"}://${value}`;
52
+ }
53
+ let url;
54
+ try {
55
+ url = new URL(value);
56
+ } catch {
57
+ throw new Error(`Mağaza adresi okunamadı: ${input}`);
58
+ }
59
+ if (url.protocol !== "https:" && !local(url.hostname)) throw new Error("Mağazaya yalnızca https ile bağlanılır.");
60
+ if (url.username || url.password) throw new Error("Mağaza adresinde kullanıcı adı ya da parola olamaz.");
61
+ return url.origin;
62
+ }
63
+
64
+ /** Etkin oturum: önce ortam değişkenleri (CI), sonra kayıtlı giriş. */
65
+ export function session(storeOption) {
66
+ if (process.env.UVAKO_TOKEN && (storeOption || process.env.UVAKO_STORE)) {
67
+ return { origin: storeOrigin(storeOption || process.env.UVAKO_STORE), token: process.env.UVAKO_TOKEN.trim() };
68
+ }
69
+ const data = read();
70
+ const origin = storeOption ? storeOrigin(storeOption) : data.current;
71
+ const entry = origin ? data.stores[origin] : null;
72
+ if (!origin || !entry?.token) {
73
+ throw new Error(storeOption ? `Bu mağaza için giriş yapılmamış: ${origin}\nÖnce: uvako login --store ${storeOption}` : "Giriş yapılmamış. Önce: uvako login --store <mağaza>");
74
+ }
75
+ return { origin, token: entry.token, name: entry.name };
76
+ }
77
+
78
+ export function saveSession(origin, token, name) {
79
+ const data = read();
80
+ data.stores[origin] = { token, name, savedAt: new Date().toISOString() };
81
+ data.current = origin;
82
+ write(data);
83
+ }
84
+
85
+ export function dropSession(storeOption) {
86
+ const data = read();
87
+ const origin = storeOption ? storeOrigin(storeOption) : data.current;
88
+ if (!origin || !data.stores[origin]) return "";
89
+ delete data.stores[origin];
90
+ if (data.current === origin) data.current = Object.keys(data.stores)[0] ?? "";
91
+ write(data);
92
+ return origin;
93
+ }
94
+
95
+ export function sessions() {
96
+ const data = read();
97
+ return Object.entries(data.stores).map(([origin, entry]) => ({ origin, name: entry.name ?? "", current: origin === data.current }));
98
+ }
99
+
100
+ export const credentialsPath = () => (existsSync(file()) ? file() : "");
package/src/init.js CHANGED
@@ -1,7 +1,8 @@
1
1
  import { existsSync, mkdirSync, writeFileSync } from "node:fs";
2
2
  import path from "node:path";
3
3
 
4
- // Yeni tema iskeleti. Tema kod içermez: ayar şeması, bölüm sırası, SCSS ve görsellerden oluşur.
4
+ // Yeni tema iskeleti. Tema kod içermez: bir katalog temasını ("base") temel alır ve onun üstüne
5
+ // ayar, ana sayfa bölüm sırası, SCSS ve görsel getirir.
5
6
  const keyPattern = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
6
7
 
7
8
  const json = (value) => `${JSON.stringify(value, null, 2)}\n`;
@@ -13,8 +14,13 @@ function logo(name) {
13
14
  `;
14
15
  }
15
16
 
16
- export function initTheme(key, { name, author, cwd }) {
17
+ /**
18
+ * `starter` temel temanın ayarlarıdır (platformdan okunur: şema, varsayılanlar, bölüm türleri, ana
19
+ * sayfa şablonu). Verilmezse iskelet şablonsuz kurulur; tema temel temanın bölüm sırasını kullanır.
20
+ */
21
+ export function initTheme(key, { name, author, cwd, base = "linea", starter = null }) {
17
22
  if (!keyPattern.test(key) || key.length > 40) throw new Error(`Tema anahtarı küçük harf, rakam ve tireden oluşmalı (ör. "atolye"): ${key}`);
23
+ if (!keyPattern.test(base)) throw new Error(`Temel tema adı geçersiz: ${base}`);
18
24
  const dir = path.join(cwd, key);
19
25
  if (existsSync(dir)) throw new Error(`Klasör zaten var: ${dir}`);
20
26
  const title = String(name || key.replace(/(^|-)([a-z])/g, (_, dash, letter) => (dash ? " " : "") + letter.toUpperCase()));
@@ -24,82 +30,37 @@ export function initTheme(key, { name, author, cwd }) {
24
30
  writeFileSync(file, content, "utf8");
25
31
  };
26
32
 
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;
33
+ write("theme.json", json({ key, name: title, version: "0.1.0", base, author: String(author || ""), description: `${title} teması.`, screenshot: "", industries: [], features: [], price: 0 }));
34
+
35
+ // Yalnızca temel temadan farklı olan değerler yazılır; gerisi temel temadan gelir.
36
+ write("config/settings_data.json", json({ logo: "uploads/logo.svg" }));
37
+
38
+ if (starter?.template) write("templates/index.json", json(starter.template));
39
+ if (starter) {
40
+ // Başvuru dosyaları: pakete girmez. Hangi ayarların ve bölüm türlerinin olduğunu gösterir.
41
+ write("reference/settings_schema.json", json(starter.schema ?? {}));
42
+ write("reference/settings_defaults.json", json(starter.defaults ?? {}));
43
+ write("reference/sections.json", json(starter.sections ?? {}));
44
+ }
45
+
46
+ write("styles/_tokens.scss", `// Temel temanın değişkenlerinin üstüne yazılır: burada verdiğiniz değerler temanın tamamına yayılır.
47
+ // Bu dosyada yalnızca değişken ataması ve yazı tipi @import'u bulunabilir.
48
+ //
49
+ // @import url('https://fonts.googleapis.com/css2?family=Manrope:wght@400;500;600;700&display=swap');
50
+ // $font-family: 'Manrope', sans-serif;
51
+ // $color-text: #111111;
52
+ // $color-sub-text: #6b6b6b;
53
+ // $color-border: #e5e5e5;
54
+ // $color-background: #ffffff;
55
+ // $border-radius: 0;
94
56
  `);
95
57
 
96
- write("styles/theme.scss", `// Temaya özgü kurallar. Bütün seçiciler .theme-${key} altında yazılır.
97
- @import 'tokens';
58
+ write("styles/theme.scss", `// Temaya özgü kurallar. Temel temanın stilinden sonra derlenir; temel temanın değişkenleri
59
+ // ($color-text, $font-family…) ve mixin'leri (@include) kullanılabilir.
60
+ // Görseller: url("#{$theme-assets}/uploads/dosya.webp")
61
+ // Panel ayarları: $s-<ayar_adı> (ör. $s-color_primary)
98
62
 
99
- .theme-${key} {
100
- .products-header span {
101
- font-family: $font-family-sub;
102
- }
63
+ .theme-${base} {
103
64
  }
104
65
  `);
105
66
 
@@ -107,22 +68,27 @@ $border-radius: 0;
107
68
 
108
69
  write("README.md", `# ${title}
109
70
 
110
- Uvako teması. Bu klasör kod içermez: ayar şeması, ana sayfa bölüm sırası, SCSS ve görseller.
71
+ Uvako teması. \`${base}\` temasını temel alır; kod içermez.
111
72
 
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.
73
+ - \`theme.json\` — kimlik: anahtar, ad, sürüm, temel tema, mağaza vitrini bilgileri.
74
+ - \`config/settings_data.json\` — temel temadan farklı olan ayar değerleri.
75
+ - \`config/settings_schema.json\` — (isteğe bağlı) panelde görünen ayarlar; yoksa temel temanınki kullanılır.
76
+ - \`templates/index.json\` — (isteğe bağlı) ana sayfadaki bölümlerin sırası.
77
+ - \`styles/_tokens.scss\` — renk, yazı tipi, köşe gibi değişkenler.
78
+ - \`styles/theme.scss\` — temaya özgü SCSS.
79
+ - \`assets/\` — logo, slayt ve banner görselleri, yazı tipleri.
80
+ - \`reference/\` — temel temanın ayar şeması, varsayılanları ve bölüm türleri. Pakete girmez.
119
81
 
120
82
  \`\`\`bash
121
- uvako-cli theme check .
83
+ uvako login --store magazam
122
84
  \`\`\`
123
85
 
124
86
  \`\`\`bash
125
- uvako-cli theme package .
87
+ uvako theme dev
88
+ \`\`\`
89
+
90
+ \`\`\`bash
91
+ uvako theme push
126
92
  \`\`\`
127
93
  `);
128
94
  return dir;
package/src/main.js CHANGED
@@ -1,23 +1,41 @@
1
1
  import { readFileSync } from "node:fs";
2
2
  import path from "node:path";
3
3
  import { fileURLToPath } from "node:url";
4
+ import { ApiError, call, starter } from "./api.js";
4
5
  import { checkTheme, printReport } from "./check.js";
6
+ import { session, siteOrigin } from "./config.js";
5
7
  import { initTheme } from "./init.js";
6
8
  import { packageTheme } from "./package.js";
9
+ import { dev, list, login, logout, pull, push, remove, submit, whoami } from "./remote.js";
7
10
 
8
11
  const version = JSON.parse(readFileSync(path.join(path.dirname(fileURLToPath(import.meta.url)), "..", "package.json"), "utf8")).version;
9
12
 
10
13
  const help = `uvako-cli ${version} — Uvako tema geliştirme aracı
11
14
  https://uvako.com
12
15
 
13
- Kullanım:
14
- uvako-cli theme init <anahtar> [--name "Ad"] [--author "Yazar"] Yeni tema iskeleti oluşturur
15
- uvako-cli theme check [klasör] Temayı denetler (yapı, ayarlar, güvenlik)
16
- uvako-cli theme package [klasör] [--out dosya.zip] Denetleyip yüklenebilir zip paketi üretir
17
- uvako-cli version Sürümü yazar
16
+ Giriş:
17
+ uvako login --store <mağaza> Mağazaya tema erişim anahtarıyla bağlanır (anahtar sorulur)
18
+ uvako logout [--store <mağaza>] Kayıtlı girişi siler
19
+ uvako whoami Bağlı mağazayı gösterir
18
20
 
19
- Tema, kod çalıştırmayan bir pakettir: ayar şeması, bölüm sırası, SCSS ve görseller.
20
- JavaScript, TypeScript ya da sunucu kodu içeren paketler reddedilir.
21
+ Tema:
22
+ uvako theme init <anahtar> [--base linea] [--name "Ad"] [--author "Yazar"]
23
+ Bir katalog temasını temel alan yeni tema iskeleti
24
+ uvako theme check [klasör] Temayı denetler (yapı, ayarlar, güvenlik)
25
+ uvako theme dev [klasör] Canlı geliştirme: kaydettikçe mağazaya yükler, önizleme yenilenir
26
+ uvako theme push [klasör] Temayı mağazanın tema kütüphanesine yükler
27
+ uvako theme pull <anahtar> [--dir klasör] Yüklediğiniz temayı indirir
28
+ uvako theme list Mağazadaki temaları ve yüklediğiniz paketleri listeler
29
+ uvako theme submit <anahtar> Temayı themes.uvako.com için incelemeye gönderir
30
+ uvako theme delete <anahtar> --yes Yüklediğiniz paketi siler
31
+ uvako theme package [klasör] [--out x.zip] Denetleyip zip paketi üretir
32
+ uvako version Sürümü yazar
33
+
34
+ Tema, kod çalıştırmayan bir pakettir: ayarlar, bölüm sırası, SCSS ve görseller.
35
+ JavaScript, şablon ya da sunucu kodu içeren paketler reddedilir. Yüklenen tema yalnızca kendi
36
+ mağazanızda görünür; tema mağazasında yayınlanması Uvako incelemesine bağlıdır.
37
+
38
+ CI için: UVAKO_STORE ve UVAKO_TOKEN ortam değişkenleri girişin yerini tutar.
21
39
 
22
40
  Uvako — https://uvako.com
23
41
  Rox Software · Rox Group`;
@@ -40,6 +58,24 @@ export function parseArgs(args) {
40
58
  return { options, rest };
41
59
  }
42
60
 
61
+ /** Temel temanın başlangıç ayarları: giriş yapılmışsa mağazadan, değilse uvako.com'dan okunur. */
62
+ async function fetchStarter(base, options) {
63
+ if (options.offline) return null;
64
+ try {
65
+ let current = null;
66
+ try {
67
+ current = session(typeof options.store === "string" ? options.store : "");
68
+ } catch {
69
+ // Giriş yoksa herkese açık uç nokta kullanılır.
70
+ }
71
+ return current ? await call(current, "GET", `/bases/${encodeURIComponent(base)}`) : await starter(siteOrigin(), base);
72
+ } catch (error) {
73
+ if (error instanceof ApiError && error.status === 404) throw new Error(`"${base}" adında bir temel tema yok. Temalar: https://themes.uvako.com`);
74
+ console.warn(`! Temel temanın ayarları okunamadı (${error.message}). İskelet başvuru dosyaları olmadan kuruluyor.`);
75
+ return null;
76
+ }
77
+ }
78
+
43
79
  export async function main(argv) {
44
80
  const { options, rest } = parseArgs(argv);
45
81
  const [group, command, target] = rest;
@@ -51,15 +87,20 @@ export async function main(argv) {
51
87
  console.log(version);
52
88
  return 0;
53
89
  }
90
+ if (group === "login") return login(options);
91
+ if (group === "logout") return logout(options);
92
+ if (group === "whoami") return whoami(options);
54
93
  if (group !== "theme") {
55
94
  console.error(`Bilinmeyen komut: ${group}\n\n${help}`);
56
95
  return 1;
57
96
  }
58
97
  if (command === "init") {
59
- if (!target) throw new Error("Tema anahtarı gerekli: uvako-cli theme init <anahtar>");
60
- const dir = initTheme(target, { name: options.name, author: options.author, cwd: process.cwd() });
61
- console.log(`Tema oluşturuldu: ${path.relative(process.cwd(), dir) || "."}`);
62
- console.log(`Sıradaki adım: uvako-cli theme check ${path.relative(process.cwd(), dir)}`);
98
+ if (!target) throw new Error("Tema anahtarı gerekli: uvako theme init <anahtar>");
99
+ const base = typeof options.base === "string" ? options.base : "linea";
100
+ const dir = initTheme(target, { name: options.name, author: options.author, cwd: process.cwd(), base, starter: await fetchStarter(base, options) });
101
+ const rel = path.relative(process.cwd(), dir) || ".";
102
+ console.log(`Tema oluşturuldu: ${rel} (temel: ${base})`);
103
+ console.log(`Sıradaki adım: uvako login --store <mağaza>, sonra: uvako theme dev ${rel}`);
63
104
  return 0;
64
105
  }
65
106
  if (command === "check") {
@@ -69,16 +110,21 @@ export async function main(argv) {
69
110
  }
70
111
  if (command === "package") {
71
112
  const result = packageTheme(path.resolve(target ?? "."), { out: typeof options.out === "string" ? options.out : undefined });
113
+ printReport(result.report);
72
114
  if (!result.ok) {
73
- printReport(result.report);
74
115
  console.error("Paket oluşturulmadı: önce hataları düzeltin.");
75
116
  return 1;
76
117
  }
77
- printReport(result.report);
78
118
  console.log(`Paket: ${result.file} (${result.files} dosya, ${(result.bytes / 1024).toFixed(0)} KB)`);
79
119
  console.log(`SHA-256: ${result.sha256}`);
80
120
  return 0;
81
121
  }
122
+ if (command === "push") return push(target, options);
123
+ if (command === "dev") return dev(target, options);
124
+ if (command === "pull") return pull(target, options);
125
+ if (command === "list") return list(options);
126
+ if (command === "submit") return submit(target, options);
127
+ if (command === "delete") return remove(target, options);
82
128
  console.error(`Bilinmeyen tema komutu: ${command ?? "(yok)"}\n\n${help}`);
83
129
  return 1;
84
130
  }
package/src/package.js CHANGED
@@ -1,75 +1,34 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { readFileSync, writeFileSync } from "node:fs";
3
3
  import path from "node:path";
4
- import zlib from "node:zlib";
5
- import { checkTheme } from "./check.js";
4
+ import { checkTheme, limits, localOnly } from "./check.js";
6
5
  import { walk } from "./files.js";
6
+ import { zip } from "./zip.js";
7
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 } = {}) {
8
+ /** Denetimden geçen temanın zip paketini bellekte üretir. Yardımcı dosyalar (README, reference/) pakete girmez. */
9
+ export function buildArchive(dir) {
60
10
  const report = checkTheme(dir);
61
11
  if (report.errors.length) return { ok: false, report };
62
12
  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.");
13
+ report.errors.push("Bu klasör platform deposundaki bir tema; paketleme ve yükleme yalnızca bağımsız tema klasörlerinde yapılır.");
64
14
  return { ok: false, report };
65
15
  }
66
16
  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"))
17
+ const entries = walk(dir)
18
+ .files.filter((file) => !localOnly(file.rel))
70
19
  .map((file) => ({ name: `${meta.key}/${file.rel}`, data: readFileSync(file.full) }));
71
20
  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") };
21
+ if (buffer.length > limits.archiveBytes) {
22
+ report.errors.push(`Paket ${(buffer.length / 1024 / 1024).toFixed(0)} MB; yükleme sınırı ${limits.archiveBytes / 1024 / 1024} MB.`);
23
+ return { ok: false, report };
24
+ }
25
+ return { ok: true, report, meta, buffer, files: entries.length, sha256: createHash("sha256").update(buffer).digest("hex") };
26
+ }
27
+
28
+ export function packageTheme(dir, { out } = {}) {
29
+ const built = buildArchive(dir);
30
+ if (!built.ok) return built;
31
+ const file = path.resolve(out ?? path.join(path.dirname(dir), `${built.meta.key}-${built.meta.version}.zip`));
32
+ writeFileSync(file, built.buffer);
33
+ return { ok: true, report: built.report, file, files: built.files, bytes: built.buffer.length, sha256: built.sha256 };
75
34
  }