uvako-cli 0.1.2 → 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/README.md +64 -15
- package/package.json +2 -2
- package/src/api.js +49 -0
- package/src/check.js +210 -81
- package/src/config.js +100 -0
- package/src/init.js +51 -85
- package/src/main.js +59 -13
- package/src/package.js +20 -61
- package/src/remote.js +264 -0
- package/src/zip.js +91 -0
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:
|
|
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
|
-
|
|
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ı.`,
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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.
|
|
97
|
-
@
|
|
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-${
|
|
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ı.
|
|
71
|
+
Uvako teması. \`${base}\` temasını temel alır; kod içermez.
|
|
111
72
|
|
|
112
|
-
- \`theme.json\` — kimlik
|
|
113
|
-
- \`config/
|
|
114
|
-
- \`config/
|
|
115
|
-
- \`
|
|
116
|
-
- \`
|
|
117
|
-
- \`styles
|
|
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
|
|
83
|
+
uvako login --store magazam
|
|
122
84
|
\`\`\`
|
|
123
85
|
|
|
124
86
|
\`\`\`bash
|
|
125
|
-
uvako
|
|
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
|
-
|
|
14
|
-
uvako
|
|
15
|
-
uvako
|
|
16
|
-
uvako
|
|
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
|
|
20
|
-
|
|
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
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
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
|
|
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
|
-
|
|
9
|
-
|
|
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
|
|
68
|
-
|
|
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
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
}
|