uvako-cli 0.4.0 → 0.6.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/main.js CHANGED
@@ -6,39 +6,56 @@ import { checkTheme, printReport } from "./check.js";
6
6
  import { session, siteOrigin } from "./config.js";
7
7
  import { initTheme } from "./init.js";
8
8
  import { packageTheme } from "./package.js";
9
- import { dev, list, login, logout, pull, push, remove, submit, whoami } from "./remote.js";
10
- import { getLocale, t } from "./i18n.js";
9
+ import { dev, list, login, logout, pull, push, remoteCheck, remove, submit, whoami } from "./remote.js";
10
+ import { appCommand } from "./app.js";
11
+ import { partnerLogin, partnerLogout, partnerWhoami } from "./partner.js";
12
+ import { getLocale, t, tm } from "./i18n.js";
11
13
  import { help as helpEn } from "./locales/en.js";
12
14
 
13
15
  const version = JSON.parse(readFileSync(path.join(path.dirname(fileURLToPath(import.meta.url)), "..", "package.json"), "utf8")).version;
14
16
 
15
- const helpTr = `uvako-cli ${version} — Uvako tema geliştirme aracı
17
+ const helpTr = `uvako-cli ${version} — Uvako tema ve uygulama geliştirme aracı
16
18
  https://uvako.com
17
19
 
18
20
  Giriş:
21
+ uvako login Partner hesabıyla giriş (tarayıcıda onaylanır; uygulama komutları)
19
22
  uvako login --store <mağaza> Mağazaya tema erişim anahtarıyla bağlanır (anahtar sorulur)
20
- uvako logout [--store <mağaza>] Kayıtlı girişi siler
21
- uvako whoami Bağlı mağazayı gösterir
23
+ uvako logout [--store <mağaza>] [--partner] Kayıtlı girişi siler
24
+ uvako whoami Bağlı mağazayı ve partner girişini gösterir
25
+
26
+ Uygulama (herkese açık uygulamalar, partner girişiyle):
27
+ uvako app init <ad> [--dir klasör] [--offline]
28
+ Örnek uygulama (OAuth kurulumu, imzalı webhook'lar) ve uvako.app.json
29
+ uvako app dev [klasör] --store <mağaza> [--port 3000] [--no-serve] [--no-update]
30
+ Uygulamayı yerelde çalıştırır; geliştirme mağazası yerel adrese bağlanır,
31
+ o mağazanın webhook'ları bilgisayarınızdaki alıcıya aktarılır
32
+ uvako app deploy [klasör] [--submit] uvako.app.json'u yeni sürüm olarak dağıtır (önce yalnızca kendi mağazalarınızda)
33
+ uvako app submit [klasör] Uygulamayı herkese açılması için Uvako incelemesine gönderir
34
+ uvako app list [--json] Kayıtlı uygulamalarınızı listeler
22
35
 
23
36
  Tema:
24
- uvako theme init <anahtar> [--base linea] [--name "Ad"] [--author "Yazar"]
37
+ uvako theme init <anahtar> [--base linea] [--name "Ad"] [--author "Yazar"] [--engine sections]
25
38
  Bir katalog temasını temel alan yeni tema iskeleti
26
- uvako theme check [klasör] Temayı denetler (yapı, ayarlar, güvenlik)
27
- uvako theme dev [klasör] Canlı geliştirme: kaydettikçe mağazaya yükler, önizleme yenilenir
39
+ --engine sections: kendi bölümlerini getiren tema (örnek bölümlerle)
40
+ uvako theme check [klasör] [--offline] Temayı denetler (yapı, ayarlar, güvenlik, bölüm şablonları);
41
+ giriş yapılmışsa sunucu da aynı kurallarla denetler
42
+ uvako theme dev [klasör] Canlı geliştirme: yalnızca değişen dosyayı yükler, önizleme yenilenir
28
43
  Durdurunca mağaza eski hâline döner (--keep: kütüphanede bırak)
29
- uvako theme push [klasör] Temayı mağazanın tema kütüphanesine yükler
30
- uvako theme pull <anahtar> [--dir klasör] Yüklediğiniz temayı indirir
44
+ uvako theme push [klasör] [--force] Temayı mağazanın tema kütüphanesine yükler (SHA-256 ve revizyonla;
45
+ paket sizden sonra değiştiyse reddedilir, --force üzerine yazar)
46
+ uvako theme pull <anahtar> [--dir klasör] Yüklediğiniz temayı indirir (sections/, snippets/, templates/ dahil)
31
47
  uvako theme list Mağazadaki temaları ve yüklediğiniz paketleri listeler
32
48
  uvako theme submit <anahtar> Temayı themes.uvako.com için incelemeye gönderir
33
49
  uvako theme delete <anahtar> --yes Yüklediğiniz paketi siler
34
50
  uvako theme package [klasör] [--out x.zip] Denetleyip zip paketi üretir
35
51
  uvako version Sürümü yazar
36
52
 
37
- Tema, kod çalıştırmayan bir pakettir: ayarlar, bölüm sırası, SCSS ve görseller.
38
- JavaScript, şablon ya da sunucu kodu içeren paketler reddedilir. Yüklenen tema yalnızca kendi
39
- mağazanızda görünür; tema mağazasında yayınlanması Uvako incelemesine bağlıdır.
53
+ Tema, kod çalıştırmayan bir pakettir: ayarlar, sayfa şablonları, SCSS ve görseller. "engine": "sections"
54
+ temaları kendi bölümlerini (sections/*.html.tmpl) getirebilir; bunlar yalnızca sunucuda, kısıtlı bir
55
+ şablon dilinde çizilir. JavaScript ya da sunucu kodu içeren paketler reddedilir. Yüklenen tema yalnızca
56
+ kendi mağazanızda görünür; tema mağazasında yayınlanması Uvako incelemesine bağlıdır.
40
57
 
41
- CI için: UVAKO_STORE ve UVAKO_TOKEN ortam değişkenleri girişin yerini tutar.
58
+ CI için: UVAKO_STORE ve UVAKO_TOKEN (tema), UVAKO_PARTNER_TOKEN (uygulama) ortam değişkenleri girişin yerini tutar.
42
59
  Dil: --lang en ya da UVAKO_LANG=en çıktıyı İngilizce verir.
43
60
 
44
61
  Uvako — https://uvako.com
@@ -93,9 +110,31 @@ export async function main(argv) {
93
110
  console.log(version);
94
111
  return 0;
95
112
  }
96
- if (group === "login") return login(options);
97
- if (group === "logout") return logout(options);
98
- if (group === "whoami") return whoami(options);
113
+ // Tek giriş: mağaza belirtilmezse partner hesabıyla (uygulama komutları), belirtilirse tema anahtarıyla.
114
+ if (group === "login") return options.store ? login(options) : partnerLogin(options);
115
+ if (group === "logout") {
116
+ if (options.partner) return partnerLogout();
117
+ const code = await logout(options);
118
+ if (options.all) await partnerLogout();
119
+ return code;
120
+ }
121
+ if (group === "whoami") {
122
+ const partner = await partnerWhoami();
123
+ if (partner && !options.store && !process.env.UVAKO_STORE) {
124
+ try {
125
+ session("");
126
+ } catch {
127
+ return 0;
128
+ }
129
+ }
130
+ return whoami(options);
131
+ }
132
+ if (group === "app") {
133
+ const code = await appCommand(command, target, options);
134
+ if (code !== null) return code;
135
+ console.error(t("Bilinmeyen uygulama komutu: {0}\n\n{1}", command ?? "(yok)", help));
136
+ return 1;
137
+ }
99
138
  if (group !== "theme") {
100
139
  console.error(t("Bilinmeyen komut: {0}\n\n{1}", group, help));
101
140
  return 1;
@@ -103,16 +142,33 @@ export async function main(argv) {
103
142
  if (command === "init") {
104
143
  if (!target) throw new Error(t("Tema anahtarı gerekli: uvako theme init <anahtar>"));
105
144
  const base = typeof options.base === "string" ? options.base : "linea";
106
- const dir = initTheme(target, { name: options.name, author: options.author, cwd: process.cwd(), base, starter: await fetchStarter(base, options) });
145
+ const engine = typeof options.engine === "string" ? options.engine : "";
146
+ const dir = initTheme(target, { name: options.name, author: options.author, cwd: process.cwd(), base, engine, starter: await fetchStarter(base, options) });
107
147
  const rel = path.relative(process.cwd(), dir) || ".";
108
148
  console.log(t("Tema oluşturuldu: {0} (temel: {1})", rel, base));
109
149
  console.log(t("Sıradaki adım: uvako login --store <mağaza>, sonra: uvako theme dev {0}", rel));
110
150
  return 0;
111
151
  }
112
152
  if (command === "check") {
113
- const report = checkTheme(path.resolve(target ?? "."));
153
+ const dir = path.resolve(target ?? ".");
154
+ const report = checkTheme(dir);
114
155
  printReport(report);
115
- return report.errors.length ? 1 : 0;
156
+ if (report.errors.length) return 1;
157
+ // Giriş yapılmışsa sunucu da aynı paketi kuru olarak denetler: yerelde geçen sunucuda da geçer.
158
+ if (options.offline || report.layout !== "package") return 0;
159
+ const remote = await remoteCheck(dir, options);
160
+ if (!remote) {
161
+ console.log(t("Sunucu denetimi için giriş yapın (uvako login --store <mağaza>) ya da --offline ekleyin."));
162
+ return 0;
163
+ }
164
+ if (!remote.ok) {
165
+ console.error(t("✖ Sunucu denetimi ({0}): {1}", remote.origin, tm(remote.message)));
166
+ for (const problem of remote.problems) console.error(` ✖ ${tm(problem)}`);
167
+ return 1;
168
+ }
169
+ for (const warning of remote.warnings) console.warn(` ! ${tm(warning)}`);
170
+ console.log(t("Sunucu denetimi ({0}): geçti.", remote.origin));
171
+ return 0;
116
172
  }
117
173
  if (command === "package") {
118
174
  const result = packageTheme(path.resolve(target ?? "."), { out: typeof options.out === "string" ? options.out : undefined });
package/src/partner.js ADDED
@@ -0,0 +1,135 @@
1
+ import { spawn } from "node:child_process";
2
+ import os from "node:os";
3
+ import { dropPartner, partnerOrigin, partnerSaved, partnerSession, savePartner } from "./config.js";
4
+ import { ApiError } from "./api.js";
5
+ import { t, tm } from "./i18n.js";
6
+
7
+ // Partner paneliyle konuşan istemci (/api/partner/cli). Giriş cihaz akışıyla yapılır: CLI bir kod
8
+ // alır, partner tarayıcıda partner paneline (şifre + iki adımlı doğrulama) girip onaylar, CLI uvp_
9
+ // anahtarını alır. Anahtar ekrana yazılmaz, yalnızca partner adresine gönderilir; yönlendirme izlenmez.
10
+
11
+ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
12
+
13
+ async function request(origin, method, route, { token, body, timeout = 60000 } = {}) {
14
+ let response;
15
+ try {
16
+ response = await fetch(`${origin}/api/partner/cli${route}`, {
17
+ method,
18
+ redirect: "error",
19
+ headers: { Accept: "application/json", "User-Agent": "uvako-cli", ...(token ? { Authorization: `Bearer ${token}` } : {}), ...(body !== undefined ? { "Content-Type": "application/json" } : {}) },
20
+ body: body !== undefined ? JSON.stringify(body) : undefined,
21
+ signal: AbortSignal.timeout(timeout),
22
+ });
23
+ } catch (error) {
24
+ throw new ApiError(t("Partner paneline bağlanılamadı ({0}): {1}", origin, error.cause?.code ?? error.message));
25
+ }
26
+ let payload = null;
27
+ try {
28
+ payload = await response.json();
29
+ } catch {
30
+ // JSON olmayan yanıt (ör. ağ geçidi hatası).
31
+ }
32
+ if (response.ok) return payload;
33
+ // Cihaz girişinin RFC 8628 yanıtları {"error": "kod", "error_description": "…"} biçimindedir.
34
+ if (typeof payload?.error === "string") throw new ApiError(payload.error_description || payload.error, { status: response.status, code: payload.error });
35
+ const problem = payload?.error ?? {};
36
+ const fallback = response.status === 404 ? t("Bu adreste Uvako partner uç noktası yok (UVAKO_PARTNER_URL?).") : t("Sunucu {0} yanıtı verdi.", response.status);
37
+ throw new ApiError(problem.message || fallback, { status: response.status, code: problem.field || "", problems: [] });
38
+ }
39
+
40
+ /** Giriş yapmış partner olarak uç noktayı çağırır. */
41
+ export function partnerCall(method, route, options = {}) {
42
+ const current = partnerSession();
43
+ return request(current.origin, method, route, { ...options, token: current.token });
44
+ }
45
+
46
+ export function explainPartner(error) {
47
+ if (!(error instanceof ApiError)) throw error;
48
+ console.error(`✖ ${tm(error.message)}${error.status === 422 && error.code ? ` (${error.code})` : ""}`);
49
+ if (error.status === 401) console.error(t(" Yeniden giriş yapın: uvako login"));
50
+ return 1;
51
+ }
52
+
53
+ function openBrowser(url) {
54
+ const [command, args] = process.platform === "win32" ? ["cmd", ["/c", "start", "", url]] : process.platform === "darwin" ? ["open", [url]] : ["xdg-open", [url]];
55
+ try {
56
+ const child = spawn(command, args, { stdio: "ignore", detached: true, windowsHide: true });
57
+ child.on("error", () => undefined);
58
+ child.unref();
59
+ } catch {
60
+ // Tarayıcı açılamazsa adres zaten ekranda.
61
+ }
62
+ }
63
+
64
+ /** `uvako login` (mağaza belirtilmeden): partner hesabıyla cihaz girişi. */
65
+ export async function partnerLogin(options) {
66
+ const origin = partnerOrigin();
67
+ let device;
68
+ try {
69
+ device = await request(origin, "POST", "/device", { body: { label: `uvako-cli · ${os.hostname()}`.slice(0, 80) } });
70
+ } catch (error) {
71
+ return explainPartner(error);
72
+ }
73
+ // Sunucunun verdiği adres yalnızca partner panelinin kendi alan adındaysa açılır (yerel geliştirmede
74
+ // panel ile API farklı portlarda olabilir; şifresiz bağlantı yalnızca yerel adreste kabul edilir).
75
+ const verify = new URL(device.verification_uri_complete);
76
+ const api = new URL(origin);
77
+ const sameHost = verify.hostname === api.hostname && (verify.protocol === "https:" || (verify.protocol === "http:" && api.protocol === "http:"));
78
+ if (!sameHost) throw new Error(t("Sunucu beklenmeyen bir doğrulama adresi verdi: {0}", verify.origin));
79
+ console.log(t("Partner paneline giriş: tarayıcıda açılan sayfada kodu onaylayın."));
80
+ console.log(t(" Adres: {0}", verify.href));
81
+ console.log(t(" Kod: {0}\n", device.user_code));
82
+ if (!options["no-browser"]) openBrowser(verify.href);
83
+ let interval = Math.max(1, Number(device.interval) || 3) * 1000;
84
+ const deadline = Date.now() + (Number(device.expires_in) || 600) * 1000;
85
+ while (Date.now() < deadline) {
86
+ await sleep(interval);
87
+ try {
88
+ const result = await request(origin, "POST", "/token", { body: { device_code: device.device_code } });
89
+ const me = await request(origin, "GET", "/me", { token: result.access_token });
90
+ const name = `${me.member?.firstName ?? ""} ${me.member?.lastName ?? ""}`.trim();
91
+ savePartner(origin, result.access_token, me.company?.name ?? "", me.member?.email ?? "");
92
+ console.log(t("Giriş yapıldı: {0} ({1}) — {2}", name || me.member?.email, me.company?.name ?? "", origin));
93
+ if (me.company?.status !== "approved") console.warn(t("! Partner başvurunuz henüz onaylanmadı; uygulama kaydı onaydan sonra açılır."));
94
+ return 0;
95
+ } catch (error) {
96
+ if (!(error instanceof ApiError)) throw error;
97
+ if (error.code === "authorization_pending") continue;
98
+ if (error.code === "slow_down") {
99
+ interval += 2000;
100
+ continue;
101
+ }
102
+ return explainPartner(error);
103
+ }
104
+ }
105
+ console.error(t("✖ Giriş kodunun süresi doldu. `uvako login` komutunu yeniden çalıştırın."));
106
+ return 1;
107
+ }
108
+
109
+ export async function partnerLogout() {
110
+ const saved = partnerSaved();
111
+ if (!saved) {
112
+ console.log(t("Kayıtlı partner girişi yok."));
113
+ return 0;
114
+ }
115
+ try {
116
+ await partnerCall("POST", "/logout", { body: {} });
117
+ } catch {
118
+ // Sunucuya ulaşılamasa da yerel giriş silinir; anahtar partner panelinden de kapatılabilir.
119
+ }
120
+ dropPartner();
121
+ console.log(t("Partner girişinden çıkıldı: {0}", saved.origin));
122
+ return 0;
123
+ }
124
+
125
+ export async function partnerWhoami() {
126
+ if (!partnerSaved() && !process.env.UVAKO_PARTNER_TOKEN) return false;
127
+ try {
128
+ const me = await partnerCall("GET", "/me");
129
+ console.log(t("Partner: {0} — {1} ({2})", me.company?.name ?? "", me.member?.email ?? "", partnerSession().origin));
130
+ } catch (error) {
131
+ if (error instanceof ApiError) console.error(t("Partner: {0}", tm(error.message)));
132
+ else throw error;
133
+ }
134
+ return true;
135
+ }
package/src/remote.js CHANGED
@@ -1,7 +1,7 @@
1
- import { existsSync, mkdirSync, readFileSync, watch, writeFileSync } from "node:fs";
1
+ import { existsSync, mkdirSync, readFileSync, statSync, watch, writeFileSync } from "node:fs";
2
2
  import path from "node:path";
3
3
  import { ApiError, call } from "./api.js";
4
- import { printReport } from "./check.js";
4
+ import { checkTheme, localOnly, printReport } from "./check.js";
5
5
  import { dropSession, saveSession, session, sessions, storeOrigin } from "./config.js";
6
6
  import { buildArchive } from "./package.js";
7
7
  import { unzip } from "./zip.js";
@@ -50,9 +50,31 @@ function explain(error) {
50
50
  console.error(`✖ ${tm(error.message)}`);
51
51
  for (const problem of error.problems) console.error(` ✖ ${tm(problem)}`);
52
52
  if (error.status === 401) console.error(t(" Yeni anahtar: mağaza paneli → Ayarlar → Temalar → Geliştirici erişimi, sonra `uvako login`."));
53
+ if (error.status === 429) console.error(t(" Yükleme sınırı aşıldı. {0} saniye sonra tekrar deneyin.", error.retryAfter || 5));
54
+ if (error.code === "conflict") console.error(t(" Sunucudaki sürümü almak için: uvako theme pull <anahtar> --force. Üzerine yazmak için: uvako theme push --force"));
53
55
  return 1;
54
56
  }
55
57
 
58
+ // Yerel durum: son push/pull'un revizyonu. Push bu revizyonu gönderir; paket arada başka biri
59
+ // tarafından değiştirildiyse sunucu yüklemeyi reddeder (409). Dosya pakete girmez.
60
+ function readState(dir) {
61
+ try {
62
+ return JSON.parse(readFileSync(path.join(dir, ".uvako", "state.json"), "utf8"));
63
+ } catch {
64
+ return null;
65
+ }
66
+ }
67
+
68
+ function writeState(dir, state) {
69
+ mkdirSync(path.join(dir, ".uvako"), { recursive: true });
70
+ writeFileSync(path.join(dir, ".uvako", "state.json"), `${JSON.stringify(state, null, 2)}\n`);
71
+ }
72
+
73
+ function baseRevision(dir, current, key, force) {
74
+ const state = readState(dir);
75
+ return !force && state && state.origin === current.origin && state.key === key && Number.isInteger(state.revision) ? { "X-Uvako-Base-Revision": String(state.revision) } : {};
76
+ }
77
+
56
78
  const previewUrl = (origin, installationId) => `${origin}/?preview=${installationId}`;
57
79
  const editorUrl = (origin, installationId) => `${origin}/admin/ayarlar/temalar/${installationId}/editor`;
58
80
 
@@ -115,23 +137,46 @@ export async function list(options) {
115
137
  }
116
138
  }
117
139
 
118
- async function upload(current, dir, quiet = false) {
140
+ async function upload(current, dir, quiet = false, force = false) {
119
141
  const built = buildArchive(dir);
120
142
  if (!built.ok) {
121
143
  printReport(built.report);
122
144
  return null;
123
145
  }
124
146
  if (!quiet) printReport(built.report);
125
- const result = await call(current, "PUT", "/themes", { body: built.buffer });
147
+ // SHA-256: paket yolda bozulursa sunucu reddeder. Revizyon: başkasının yüklemesinin üstüne yazılmaz.
148
+ const headers = { "X-Uvako-Package-Sha256": built.sha256, ...baseRevision(dir, current, built.meta.key, force) };
149
+ const result = await call(current, "PUT", "/themes", { body: built.buffer, headers });
150
+ writeState(dir, { origin: current.origin, key: result.package.key, revision: result.package.revision });
126
151
  for (const warning of result.warnings ?? []) console.warn(` ! ${warning}`);
127
152
  return result;
128
153
  }
129
154
 
155
+ /** Kuru denetim: sunucu paketi yükleme kurallarıyla denetler, hiçbir şey kaydetmez. */
156
+ export async function remoteCheck(dir, options) {
157
+ let current;
158
+ try {
159
+ current = session(typeof options.store === "string" ? options.store : "");
160
+ } catch {
161
+ return null;
162
+ }
163
+ const built = buildArchive(dir);
164
+ if (!built.ok) return null;
165
+ try {
166
+ const result = await call(current, "POST", "/themes/check", { body: built.buffer });
167
+ return { ok: true, origin: current.origin, warnings: result.warnings ?? [] };
168
+ } catch (error) {
169
+ if (!(error instanceof ApiError)) throw error;
170
+ if (error.status === 422 || error.status === 403) return { ok: false, origin: current.origin, message: error.message, problems: error.problems };
171
+ return { ok: true, origin: current.origin, warnings: [t("Sunucu denetimi yapılamadı: {0}", tm(error.message))] };
172
+ }
173
+ }
174
+
130
175
  export async function push(target, options) {
131
176
  const dir = path.resolve(target ?? ".");
132
177
  try {
133
178
  const current = session(typeof options.store === "string" ? options.store : "");
134
- const result = await upload(current, dir);
179
+ const result = await upload(current, dir, false, Boolean(options.force));
135
180
  if (!result) {
136
181
  console.error(t("Yüklenmedi: önce hataları düzeltin."));
137
182
  return 1;
@@ -185,7 +230,29 @@ export async function dev(target, options) {
185
230
  let timer = null;
186
231
  let running = false;
187
232
  let again = false;
233
+ const changed = new Set();
188
234
  const stamp = () => new Date().toLocaleTimeString("tr-TR");
235
+ // Yalnızca değişen dosyalar gönderilir; sunucu paketin tamamını yine aynı kurallarla denetler.
236
+ const sendChanges = async () => {
237
+ const files = [...changed];
238
+ changed.clear();
239
+ const report = checkTheme(dir);
240
+ if (report.errors.length) {
241
+ printReport(report);
242
+ return null;
243
+ }
244
+ if (files.length > 10 || files.includes("theme.json")) return upload(current, dir, true);
245
+ let result = null;
246
+ for (const rel of files) {
247
+ const full = path.join(dir, rel);
248
+ const exists = existsSync(full) && statSync(full).isFile();
249
+ const route = `/themes/${encodeURIComponent(first.package.key)}/files/${rel.split("/").map(encodeURIComponent).join("/")}`;
250
+ const headers = baseRevision(dir, current, first.package.key, false);
251
+ result = await call(current, exists ? "PUT" : "DELETE", route, exists ? { body: readFileSync(full), headers, contentType: "application/octet-stream" } : { headers });
252
+ writeState(dir, { origin: current.origin, key: result.package.key, revision: result.package.revision });
253
+ }
254
+ return result;
255
+ };
189
256
  const sync = async () => {
190
257
  if (running) {
191
258
  again = true;
@@ -193,13 +260,14 @@ export async function dev(target, options) {
193
260
  }
194
261
  running = true;
195
262
  try {
196
- const result = await upload(current, dir, true);
263
+ const result = await sendChanges();
197
264
  if (result) console.log(t("[{0}] yüklendi (sürüm no {1})", stamp(), result.package.revision));
198
265
  else console.error(t("[{0}] yüklenmedi: hataları düzeltip kaydedin.", stamp()));
199
266
  } catch (error) {
200
267
  if (error instanceof ApiError) {
201
268
  console.error(`[${stamp()}] ✖ ${tm(error.message)}`);
202
269
  for (const problem of error.problems) console.error(` ✖ ${tm(problem)}`);
270
+ if (error.status === 429) console.error(t(" Yükleme sınırı aşıldı. {0} saniye sonra tekrar deneyin.", error.retryAfter || 5));
203
271
  if (error.status === 401 || error.status === 403) process.exit(1);
204
272
  } else {
205
273
  console.error(`[${stamp()}] ✖ ${tm(error.message)}`);
@@ -214,7 +282,9 @@ export async function dev(target, options) {
214
282
  };
215
283
  const watcher = watch(dir, { recursive: true }, (_event, file) => {
216
284
  const rel = String(file ?? "").replaceAll("\\", "/");
217
- if (!rel || rel.startsWith("reference/") || rel.startsWith(".git/") || rel.includes("node_modules/") || rel.endsWith(".zip") || rel.endsWith("~") || rel.split("/").some((part) => part.startsWith("."))) return;
285
+ if (!rel || localOnly(rel) || rel.startsWith(".git/") || rel.includes("node_modules/") || rel.endsWith("~") || rel.split("/").some((part) => part.startsWith("."))) return;
286
+ if (existsSync(path.join(dir, rel)) && statSync(path.join(dir, rel)).isDirectory()) return;
287
+ changed.add(rel);
218
288
  clearTimeout(timer);
219
289
  timer = setTimeout(() => void sync(), 300);
220
290
  });
@@ -264,6 +334,9 @@ export async function pull(key, options) {
264
334
  writeFileSync(file, entry.data);
265
335
  count++;
266
336
  }
337
+ const state = await call(current, "GET", "/themes");
338
+ const pkg = (state.packages ?? []).find((item) => item.key === key);
339
+ if (pkg) writeState(dir, { origin: current.origin, key, revision: pkg.revision });
267
340
  console.log(t("İndirildi: {0} ({1} dosya)", path.relative(process.cwd(), dir) || ".", count));
268
341
  return 0;
269
342
  } catch (error) {