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/src/remote.js ADDED
@@ -0,0 +1,297 @@
1
+ import { existsSync, mkdirSync, readFileSync, watch, writeFileSync } from "node:fs";
2
+ import path from "node:path";
3
+ import { ApiError, call } from "./api.js";
4
+ import { printReport } from "./check.js";
5
+ import { dropSession, saveSession, session, sessions, storeOrigin } from "./config.js";
6
+ import { buildArchive } from "./package.js";
7
+ import { unzip } from "./zip.js";
8
+
9
+ // Mağazayla konuşan komutlar: giriş, yükleme, canlı geliştirme, indirme, incelemeye gönderme.
10
+
11
+ /** Erişim anahtarını ekrana yazdırmadan okur. Anahtar komut satırına yazılmaz (kabuk geçmişinde kalır). */
12
+ function askSecret(question) {
13
+ const input = process.stdin;
14
+ if (!input.isTTY) {
15
+ return new Promise((resolve) => {
16
+ let data = "";
17
+ input.setEncoding("utf8");
18
+ input.on("data", (chunk) => (data += chunk));
19
+ input.on("end", () => resolve(data.trim()));
20
+ });
21
+ }
22
+ return new Promise((resolve, reject) => {
23
+ process.stdout.write(question);
24
+ let value = "";
25
+ input.setRawMode(true);
26
+ input.resume();
27
+ input.setEncoding("utf8");
28
+ const done = (fn, arg) => {
29
+ input.setRawMode(false);
30
+ input.pause();
31
+ input.off("data", onData);
32
+ process.stdout.write("\n");
33
+ fn(arg);
34
+ };
35
+ const onData = (chunk) => {
36
+ for (const char of chunk) {
37
+ if (char === "\r" || char === "\n") return done(resolve, value.trim());
38
+ if (char === "\u0003") return done(reject, new Error("İptal edildi."));
39
+ if (char === "\u007f" || char === "\b") value = value.slice(0, -1);
40
+ else if (char >= " ") value += char;
41
+ }
42
+ };
43
+ input.on("data", onData);
44
+ });
45
+ }
46
+
47
+ function explain(error) {
48
+ if (!(error instanceof ApiError)) throw error;
49
+ console.error(`✖ ${error.message}`);
50
+ for (const problem of error.problems) console.error(` ✖ ${problem}`);
51
+ if (error.status === 401) console.error(" Yeni anahtar: mağaza paneli → Ayarlar → Temalar → Geliştirici erişimi, sonra `uvako login`.");
52
+ return 1;
53
+ }
54
+
55
+ const previewUrl = (origin, installationId) => `${origin}/?preview=${installationId}`;
56
+ const editorUrl = (origin, installationId) => `${origin}/admin/ayarlar/temalar/${installationId}/editor`;
57
+
58
+ export async function login(options) {
59
+ if (!options.store || options.store === true) throw new Error("Mağaza adresi gerekli: uvako login --store <mağaza>");
60
+ if (options.token) throw new Error("Erişim anahtarı komut satırına yazılmaz. Komutu --token olmadan çalıştırın; anahtar sorulacak (ya da UVAKO_TOKEN ortam değişkenini kullanın).");
61
+ const origin = storeOrigin(options.store);
62
+ const token = (process.env.UVAKO_TOKEN || (await askSecret(`${origin} için tema erişim anahtarı (panel → Ayarlar → Temalar → Geliştirici erişimi): `))).trim();
63
+ if (!/^uvt_[A-Za-z0-9_-]{20,}$/.test(token)) throw new Error("Bu bir tema erişim anahtarı değil (uvt_ ile başlar).");
64
+ try {
65
+ const me = await call({ origin, token }, "GET", "/me");
66
+ saveSession(origin, token, me.store?.name ?? "");
67
+ console.log(`Giriş yapıldı: ${me.store?.name ?? origin} (${origin})`);
68
+ if (me.uploadBlocked) console.warn("! Bu mağaza için tema yükleme kapatılmış. Uvako desteğine yazın.");
69
+ return 0;
70
+ } catch (error) {
71
+ return explain(error);
72
+ }
73
+ }
74
+
75
+ export function logout(options) {
76
+ const origin = dropSession(typeof options.store === "string" ? options.store : "");
77
+ console.log(origin ? `Çıkış yapıldı: ${origin}` : "Kayıtlı giriş yok.");
78
+ console.log("Anahtarı tamamen geçersiz kılmak için mağaza panelinden iptal edin.");
79
+ return 0;
80
+ }
81
+
82
+ export async function whoami(options) {
83
+ try {
84
+ const current = session(typeof options.store === "string" ? options.store : "");
85
+ const me = await call(current, "GET", "/me");
86
+ console.log(`${me.store?.name ?? ""} — ${current.origin} (anahtar: ${me.token})`);
87
+ for (const other of sessions()) if (!other.current) console.log(` diğer giriş: ${other.name || other.origin} — ${other.origin}`);
88
+ return 0;
89
+ } catch (error) {
90
+ return explain(error);
91
+ }
92
+ }
93
+
94
+ const statusLabels = { private: "yalnızca bu mağazada", pending: "incelemede", approved: "tema mağazasında", rejected: "reddedildi" };
95
+
96
+ export async function list(options) {
97
+ try {
98
+ const current = session(typeof options.store === "string" ? options.store : "");
99
+ const data = await call(current, "GET", "/themes");
100
+ if (options.json) {
101
+ console.log(JSON.stringify(data, null, 2));
102
+ return 0;
103
+ }
104
+ console.log(`Mağazadaki temalar (${current.origin}):`);
105
+ for (const item of data.installations) console.log(` ${item.role === "published" ? "● yayında " : "○ kütüphane"} ${item.name} [${item.key}]${item.custom ? " (paket)" : ""}`);
106
+ console.log(data.packages.length ? "\nYüklediğiniz paketler:" : "\nYüklediğiniz paket yok.");
107
+ for (const pkg of data.packages) {
108
+ console.log(` ${pkg.key} ${pkg.version} (temel: ${pkg.base}) ${statusLabels[pkg.status] ?? pkg.status}${pkg.publishedVersion ? ` · mağazadaki sürüm ${pkg.publishedVersion}` : ""}`);
109
+ if (pkg.status === "rejected" && pkg.reviewNote) console.log(` not: ${pkg.reviewNote}`);
110
+ }
111
+ return 0;
112
+ } catch (error) {
113
+ return explain(error);
114
+ }
115
+ }
116
+
117
+ async function upload(current, dir, quiet = false) {
118
+ const built = buildArchive(dir);
119
+ if (!built.ok) {
120
+ printReport(built.report);
121
+ return null;
122
+ }
123
+ if (!quiet) printReport(built.report);
124
+ const result = await call(current, "PUT", "/themes", { body: built.buffer });
125
+ for (const warning of result.warnings ?? []) console.warn(` ! ${warning}`);
126
+ return result;
127
+ }
128
+
129
+ export async function push(target, options) {
130
+ const dir = path.resolve(target ?? ".");
131
+ try {
132
+ const current = session(typeof options.store === "string" ? options.store : "");
133
+ const result = await upload(current, dir);
134
+ if (!result) {
135
+ console.error("Yüklenmedi: önce hataları düzeltin.");
136
+ return 1;
137
+ }
138
+ const pkg = result.package;
139
+ console.log(`${result.created ? "Yüklendi" : "Güncellendi"}: ${pkg.name} ${pkg.version} (sürüm no ${pkg.revision}) → ${current.origin}`);
140
+ console.log(`Önizleme: ${previewUrl(current.origin, result.installationId)}`);
141
+ console.log(`Düzenleme: ${editorUrl(current.origin, result.installationId)}`);
142
+ console.log("Önizleme için tarayıcıda mağaza paneline giriş yapmış olmalısınız. Tema, panelden yayınlanana kadar vitrini değiştirmez.");
143
+ if (pkg.publishedVersion) console.log(`Tema mağazasındaki sürüm (${pkg.publishedVersion}) değişmedi. Yeni sürümü incelemeye göndermek için: uvako theme submit ${pkg.key}`);
144
+ return 0;
145
+ } catch (error) {
146
+ return explain(error);
147
+ }
148
+ }
149
+
150
+ /** Canlı geliştirme: klasörü izler, her kayıtta temayı mağazaya yükler; açık önizleme sayfası kendini yeniler. */
151
+ export async function dev(target, options) {
152
+ const dir = path.resolve(target ?? ".");
153
+ let current;
154
+ let first;
155
+ // Geliştirme bitince mağaza başladığı hâle döner: paket önceden yüklüyse o sürüm geri yüklenir,
156
+ // değilse geliştirme kopyası silinir. `--keep` son hâli kütüphanede bırakır.
157
+ let before = null;
158
+ let existed = false;
159
+ try {
160
+ current = session(typeof options.store === "string" ? options.store : "");
161
+ const key = String(JSON.parse(readFileSync(path.join(dir, "theme.json"), "utf8")).key ?? "");
162
+ if (!options.keep && key) {
163
+ const state = await call(current, "GET", "/themes");
164
+ existed = state.packages.some((pkg) => pkg.key === key);
165
+ if (existed) before = await call(current, "GET", `/themes/${encodeURIComponent(key)}/package`, { raw: true });
166
+ }
167
+ first = await upload(current, dir);
168
+ } catch (error) {
169
+ if (error instanceof ApiError) return explain(error);
170
+ console.error(`✖ ${error.message}`);
171
+ return 1;
172
+ }
173
+ if (!first) {
174
+ console.error("Başlatılamadı: önce hataları düzeltin.");
175
+ return 1;
176
+ }
177
+ console.log(`\nCanlı geliştirme: ${first.package.name} → ${current.origin}`);
178
+ console.log(`Önizleme: ${previewUrl(current.origin, first.installationId)}`);
179
+ console.log(`Düzenleme: ${editorUrl(current.origin, first.installationId)}`);
180
+ console.log("Önizleme için tarayıcıda mağaza paneline giriş yapın. Dosyaları kaydettikçe sayfa yenilenir. Durdurmak için Ctrl+C.\n");
181
+ console.log(options.keep ? "Durdurduğunuzda tema son hâliyle kütüphanede kalır (--keep)." : existed ? "Durdurduğunuzda mağazadaki paket geliştirme öncesindeki sürümüne döner. Kalıcı yüklemek için: uvako theme push" : "Durdurduğunuzda geliştirme kopyası mağazadan silinir. Kalıcı yüklemek için: uvako theme push");
182
+ console.log("");
183
+
184
+ let timer = null;
185
+ let running = false;
186
+ let again = false;
187
+ const stamp = () => new Date().toLocaleTimeString("tr-TR");
188
+ const sync = async () => {
189
+ if (running) {
190
+ again = true;
191
+ return;
192
+ }
193
+ running = true;
194
+ try {
195
+ const result = await upload(current, dir, true);
196
+ if (result) console.log(`[${stamp()}] yüklendi (sürüm no ${result.package.revision})`);
197
+ else console.error(`[${stamp()}] yüklenmedi: hataları düzeltip kaydedin.`);
198
+ } catch (error) {
199
+ if (error instanceof ApiError) {
200
+ console.error(`[${stamp()}] ✖ ${error.message}`);
201
+ for (const problem of error.problems) console.error(` ✖ ${problem}`);
202
+ if (error.status === 401 || error.status === 403) process.exit(1);
203
+ } else {
204
+ console.error(`[${stamp()}] ✖ ${error.message}`);
205
+ }
206
+ } finally {
207
+ running = false;
208
+ if (again) {
209
+ again = false;
210
+ void sync();
211
+ }
212
+ }
213
+ };
214
+ const watcher = watch(dir, { recursive: true }, (_event, file) => {
215
+ const rel = String(file ?? "").replaceAll("\\", "/");
216
+ if (!rel || rel.startsWith("reference/") || rel.startsWith(".git/") || rel.includes("node_modules/") || rel.endsWith(".zip") || rel.endsWith("~") || rel.split("/").some((part) => part.startsWith("."))) return;
217
+ clearTimeout(timer);
218
+ timer = setTimeout(() => void sync(), 300);
219
+ });
220
+ return new Promise((resolve) => {
221
+ let stopping = false;
222
+ process.on("SIGINT", async () => {
223
+ if (stopping) return;
224
+ stopping = true;
225
+ watcher.close();
226
+ clearTimeout(timer);
227
+ const key = first.package.key;
228
+ try {
229
+ if (options.keep) {
230
+ console.log("\nCanlı geliştirme durdu. Tema son hâliyle mağaza kütüphanesinde duruyor.");
231
+ } else if (before) {
232
+ await call(current, "PUT", "/themes", { body: before });
233
+ console.log("\nCanlı geliştirme durdu. Mağazadaki paket geliştirme öncesindeki sürümüne döndü.");
234
+ } else {
235
+ await call(current, "DELETE", `/themes/${encodeURIComponent(key)}`);
236
+ console.log("\nCanlı geliştirme durdu. Geliştirme kopyası mağazadan silindi; vitrin ve kütüphane eski hâlinde.");
237
+ }
238
+ resolve(0);
239
+ } catch (error) {
240
+ console.error(`\nCanlı geliştirme durdu ama mağaza eski hâline döndürülemedi: ${error.message}`);
241
+ console.error(`Elle kaldırmak için: uvako theme delete ${key} --yes`);
242
+ resolve(1);
243
+ }
244
+ });
245
+ });
246
+ }
247
+
248
+ export async function pull(key, options) {
249
+ if (!key) throw new Error("Tema anahtarı gerekli: uvako theme pull <anahtar>");
250
+ const parent = path.resolve(typeof options.dir === "string" ? options.dir : ".");
251
+ const dir = path.join(parent, key);
252
+ if (existsSync(dir) && !options.force) throw new Error(`Klasör zaten var: ${dir}\nÜstüne yazmak için --force ekleyin.`);
253
+ try {
254
+ const current = session(typeof options.store === "string" ? options.store : "");
255
+ const archive = await call(current, "GET", `/themes/${encodeURIComponent(key)}/package`, { raw: true });
256
+ let count = 0;
257
+ for (const entry of unzip(archive)) {
258
+ // Paket yalnızca kendi klasörünün içine açılır.
259
+ const rel = entry.name.split("/").slice(1).join("/");
260
+ const file = path.resolve(dir, rel);
261
+ if (!rel || !entry.name.startsWith(`${key}/`) || rel.split("/").includes("..") || !file.startsWith(dir + path.sep)) throw new Error(`Paketteki yol güvenli değil: ${entry.name}`);
262
+ mkdirSync(path.dirname(file), { recursive: true });
263
+ writeFileSync(file, entry.data);
264
+ count++;
265
+ }
266
+ console.log(`İndirildi: ${path.relative(process.cwd(), dir) || "."} (${count} dosya)`);
267
+ return 0;
268
+ } catch (error) {
269
+ return explain(error);
270
+ }
271
+ }
272
+
273
+ export async function submit(key, options) {
274
+ if (!key) throw new Error("Tema anahtarı gerekli: uvako theme submit <anahtar>");
275
+ try {
276
+ const current = session(typeof options.store === "string" ? options.store : "");
277
+ const pkg = await call(current, "POST", `/themes/${encodeURIComponent(key)}/submit`);
278
+ console.log(`İncelemeye gönderildi: ${pkg.name} ${pkg.version}. Uvako ekibi onayladığında themes.uvako.com'da yayınlanır.`);
279
+ console.log("İnceleme sürerken yeni sürüm yüklerseniz onay o sürüm için yeniden istenir.");
280
+ return 0;
281
+ } catch (error) {
282
+ return explain(error);
283
+ }
284
+ }
285
+
286
+ export async function remove(key, options) {
287
+ if (!key) throw new Error("Tema anahtarı gerekli: uvako theme delete <anahtar>");
288
+ if (!options.yes) throw new Error(`"${key}" paketi ve mağazadaki kopyaları silinir; geri alınamaz. Onaylamak için --yes ekleyin.`);
289
+ try {
290
+ const current = session(typeof options.store === "string" ? options.store : "");
291
+ await call(current, "DELETE", `/themes/${encodeURIComponent(key)}`);
292
+ console.log(`Silindi: ${key}`);
293
+ return 0;
294
+ } catch (error) {
295
+ return explain(error);
296
+ }
297
+ }
package/src/zip.js ADDED
@@ -0,0 +1,91 @@
1
+ import zlib from "node:zlib";
2
+
3
+ // Bağımlılıksız zip yazıcı ve okuyucu. Dosya tarihleri sabit tutulur; aynı içerik her zaman aynı
4
+ // özeti verir.
5
+ const DOS_DATE = ((2026 - 1980) << 9) | (1 << 5) | 1;
6
+
7
+ export function zip(entries) {
8
+ const parts = [];
9
+ const central = [];
10
+ let offset = 0;
11
+ for (const { name, data } of entries) {
12
+ const nameBytes = Buffer.from(name, "utf8");
13
+ const compressed = zlib.deflateRawSync(data, { level: 9 });
14
+ const crc = zlib.crc32(data);
15
+ const local = Buffer.alloc(30);
16
+ local.writeUInt32LE(0x04034b50, 0);
17
+ local.writeUInt16LE(20, 4);
18
+ local.writeUInt16LE(0x0800, 6); // UTF-8 dosya adları
19
+ local.writeUInt16LE(8, 8);
20
+ local.writeUInt16LE(0, 10);
21
+ local.writeUInt16LE(DOS_DATE, 12);
22
+ local.writeUInt32LE(crc, 14);
23
+ local.writeUInt32LE(compressed.length, 18);
24
+ local.writeUInt32LE(data.length, 22);
25
+ local.writeUInt16LE(nameBytes.length, 26);
26
+ parts.push(local, nameBytes, compressed);
27
+
28
+ const header = Buffer.alloc(46);
29
+ header.writeUInt32LE(0x02014b50, 0);
30
+ header.writeUInt16LE(20, 4);
31
+ header.writeUInt16LE(20, 6);
32
+ header.writeUInt16LE(0x0800, 8);
33
+ header.writeUInt16LE(8, 10);
34
+ header.writeUInt16LE(0, 12);
35
+ header.writeUInt16LE(DOS_DATE, 14);
36
+ header.writeUInt32LE(crc, 16);
37
+ header.writeUInt32LE(compressed.length, 20);
38
+ header.writeUInt32LE(data.length, 24);
39
+ header.writeUInt16LE(nameBytes.length, 28);
40
+ header.writeUInt32LE(offset, 42);
41
+ central.push(header, nameBytes);
42
+ offset += local.length + nameBytes.length + compressed.length;
43
+ }
44
+ const centralSize = central.reduce((sum, part) => sum + part.length, 0);
45
+ const end = Buffer.alloc(22);
46
+ end.writeUInt32LE(0x06054b50, 0);
47
+ end.writeUInt16LE(entries.length, 8);
48
+ end.writeUInt16LE(entries.length, 10);
49
+ end.writeUInt32LE(centralSize, 12);
50
+ end.writeUInt32LE(offset, 16);
51
+ return Buffer.concat([...parts, ...central, end]);
52
+ }
53
+
54
+ const maxEntryBytes = 16 * 1024 * 1024;
55
+
56
+ /** Zip içindeki dosyaları {name, data} olarak döndürür. Yalnızca "stored" ve "deflate" desteklenir. */
57
+ export function unzip(buffer) {
58
+ let end = -1;
59
+ for (let i = buffer.length - 22; i >= Math.max(0, buffer.length - 65557); i--) {
60
+ if (buffer.readUInt32LE(i) === 0x06054b50) {
61
+ end = i;
62
+ break;
63
+ }
64
+ }
65
+ if (end < 0) throw new Error("Geçerli bir zip dosyası değil.");
66
+ const count = buffer.readUInt16LE(end + 10);
67
+ let cursor = buffer.readUInt32LE(end + 16);
68
+ const entries = [];
69
+ for (let i = 0; i < count; i++) {
70
+ if (buffer.readUInt32LE(cursor) !== 0x02014b50) throw new Error("Zip dizini okunamadı.");
71
+ const method = buffer.readUInt16LE(cursor + 10);
72
+ const compressedSize = buffer.readUInt32LE(cursor + 20);
73
+ const size = buffer.readUInt32LE(cursor + 24);
74
+ const nameLength = buffer.readUInt16LE(cursor + 28);
75
+ const extraLength = buffer.readUInt16LE(cursor + 30);
76
+ const commentLength = buffer.readUInt16LE(cursor + 32);
77
+ const localOffset = buffer.readUInt32LE(cursor + 42);
78
+ const name = buffer.subarray(cursor + 46, cursor + 46 + nameLength).toString("utf8");
79
+ cursor += 46 + nameLength + extraLength + commentLength;
80
+ if (name.endsWith("/")) continue;
81
+ if (size > maxEntryBytes) throw new Error(`${name}: dosya çok büyük.`);
82
+ const dataStart = localOffset + 30 + buffer.readUInt16LE(localOffset + 26) + buffer.readUInt16LE(localOffset + 28);
83
+ const raw = buffer.subarray(dataStart, dataStart + compressedSize);
84
+ let data;
85
+ if (method === 0) data = Buffer.from(raw);
86
+ else if (method === 8) data = zlib.inflateRawSync(raw, { maxOutputLength: maxEntryBytes });
87
+ else throw new Error(`${name}: desteklenmeyen sıkıştırma yöntemi.`);
88
+ entries.push({ name, data });
89
+ }
90
+ return entries;
91
+ }