uvako-cli 0.5.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/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) {
@@ -0,0 +1,452 @@
1
+ // Generated by scripts/generate-theme-rules.js. Edit backend/internal/theme/rules/rules.json.
2
+ export const rules = {
3
+ "version": 1,
4
+ "engines": [
5
+ "",
6
+ "sections"
7
+ ],
8
+ "templates": [
9
+ "index",
10
+ "product",
11
+ "collection",
12
+ "cart",
13
+ "search",
14
+ "page",
15
+ "account",
16
+ "login",
17
+ "thank_you"
18
+ ],
19
+ "functions": [
20
+ "money",
21
+ "image_url",
22
+ "asset_url",
23
+ "t",
24
+ "url_for",
25
+ "default",
26
+ "truncate",
27
+ "json",
28
+ "eq",
29
+ "ne",
30
+ "lt",
31
+ "le",
32
+ "gt",
33
+ "ge",
34
+ "and",
35
+ "or",
36
+ "not",
37
+ "len"
38
+ ],
39
+ "actions": [
40
+ "if",
41
+ "else",
42
+ "end",
43
+ "range",
44
+ "with",
45
+ "define",
46
+ "template"
47
+ ],
48
+ "platformSchemas": {
49
+ "product-main": {
50
+ "name": "Ürün (temanın ürün sayfası)",
51
+ "settings": []
52
+ },
53
+ "product-gallery": {
54
+ "name": "Ürün görselleri",
55
+ "settings": [
56
+ {
57
+ "id": "layout",
58
+ "type": "select",
59
+ "label": "Yerleşim",
60
+ "default": "slider",
61
+ "options": [
62
+ {
63
+ "value": "slider",
64
+ "label": "Kaydırmalı"
65
+ },
66
+ {
67
+ "value": "grid",
68
+ "label": "Izgara"
69
+ }
70
+ ]
71
+ },
72
+ {
73
+ "id": "show_thumbnails",
74
+ "type": "checkbox",
75
+ "label": "Küçük görseller",
76
+ "default": true
77
+ }
78
+ ]
79
+ },
80
+ "product-info": {
81
+ "name": "Ürün bilgileri",
82
+ "settings": [
83
+ {
84
+ "id": "show_sku",
85
+ "type": "checkbox",
86
+ "label": "Stok kodu",
87
+ "default": true
88
+ },
89
+ {
90
+ "id": "show_stock",
91
+ "type": "checkbox",
92
+ "label": "Stok durumu",
93
+ "default": true
94
+ },
95
+ {
96
+ "id": "show_unit_price",
97
+ "type": "checkbox",
98
+ "label": "Birim fiyat",
99
+ "default": true
100
+ },
101
+ {
102
+ "id": "shipping_note",
103
+ "type": "text",
104
+ "label": "Kargo notu",
105
+ "default": ""
106
+ }
107
+ ]
108
+ },
109
+ "product-details": {
110
+ "name": "Ürün ayrıntıları",
111
+ "settings": [
112
+ {
113
+ "id": "title",
114
+ "type": "text",
115
+ "label": "Başlık",
116
+ "default": "Ürün ayrıntıları"
117
+ },
118
+ {
119
+ "id": "show_specs",
120
+ "type": "checkbox",
121
+ "label": "Teknik bilgiler tablosu",
122
+ "default": true
123
+ },
124
+ {
125
+ "id": "collapsible",
126
+ "type": "checkbox",
127
+ "label": "Akordeon",
128
+ "default": false
129
+ }
130
+ ]
131
+ },
132
+ "product-related": {
133
+ "name": "İlgili ürünler",
134
+ "settings": [
135
+ {
136
+ "id": "title",
137
+ "type": "text",
138
+ "label": "Başlık",
139
+ "default": "Bununla iyi gider"
140
+ },
141
+ {
142
+ "id": "limit",
143
+ "type": "range",
144
+ "label": "Ürün sayısı",
145
+ "default": 4,
146
+ "min": 2,
147
+ "max": 12,
148
+ "step": 1
149
+ }
150
+ ]
151
+ },
152
+ "collection-main": {
153
+ "name": "Ürün listesi (temanın liste sayfası)",
154
+ "settings": []
155
+ },
156
+ "cart-main": {
157
+ "name": "Sepet (temanın sepet sayfası)",
158
+ "settings": []
159
+ },
160
+ "search-main": {
161
+ "name": "Arama",
162
+ "settings": []
163
+ },
164
+ "page-main": {
165
+ "name": "Sayfa",
166
+ "settings": []
167
+ },
168
+ "account-main": {
169
+ "name": "Hesabım",
170
+ "settings": []
171
+ },
172
+ "login-main": {
173
+ "name": "Giriş",
174
+ "settings": []
175
+ },
176
+ "thank-you-main": {
177
+ "name": "Sipariş onayı",
178
+ "settings": []
179
+ },
180
+ "plate-rack": {
181
+ "name": "Plaka rafı",
182
+ "settings": [
183
+ {
184
+ "id": "title",
185
+ "type": "text",
186
+ "label": "Başlık",
187
+ "default": "Öne çıkanlar"
188
+ },
189
+ {
190
+ "id": "source",
191
+ "type": "select",
192
+ "label": "Ürünler",
193
+ "default": "featured",
194
+ "options": [
195
+ {
196
+ "value": "featured",
197
+ "label": "Öne çıkanlar"
198
+ },
199
+ {
200
+ "value": "newest",
201
+ "label": "Yeni gelenler"
202
+ },
203
+ {
204
+ "value": "discounted",
205
+ "label": "İndirimdekiler"
206
+ },
207
+ {
208
+ "value": "popular",
209
+ "label": "Çok satanlar"
210
+ }
211
+ ]
212
+ },
213
+ {
214
+ "id": "limit",
215
+ "type": "range",
216
+ "label": "Ürün sayısı",
217
+ "default": 8,
218
+ "min": 2,
219
+ "max": 20,
220
+ "step": 1
221
+ }
222
+ ]
223
+ },
224
+ "portion-comparison": {
225
+ "name": "Porsiyon karşılaştırması",
226
+ "settings": [
227
+ {
228
+ "id": "title",
229
+ "type": "text",
230
+ "label": "Başlık",
231
+ "default": "Porsiyon karşılaştırması"
232
+ },
233
+ {
234
+ "id": "source",
235
+ "type": "select",
236
+ "label": "Ürünler",
237
+ "default": "featured",
238
+ "options": [
239
+ {
240
+ "value": "featured",
241
+ "label": "Öne çıkanlar"
242
+ },
243
+ {
244
+ "value": "newest",
245
+ "label": "Yeni gelenler"
246
+ },
247
+ {
248
+ "value": "discounted",
249
+ "label": "İndirimdekiler"
250
+ },
251
+ {
252
+ "value": "popular",
253
+ "label": "Çok satanlar"
254
+ }
255
+ ]
256
+ },
257
+ {
258
+ "id": "limit",
259
+ "type": "range",
260
+ "label": "Ürün sayısı",
261
+ "default": 4,
262
+ "min": 2,
263
+ "max": 8,
264
+ "step": 1
265
+ },
266
+ {
267
+ "id": "portion_label",
268
+ "type": "text",
269
+ "label": "Porsiyon özelliğinin adı",
270
+ "default": "Porsiyon"
271
+ },
272
+ {
273
+ "id": "protein_label",
274
+ "type": "text",
275
+ "label": "Protein özelliğinin adı",
276
+ "default": "Protein"
277
+ }
278
+ ]
279
+ },
280
+ "vehicle-finder": {
281
+ "name": "Araç bulucu",
282
+ "settings": [
283
+ {
284
+ "id": "title",
285
+ "type": "text",
286
+ "label": "Başlık",
287
+ "default": "Önce aracını seç"
288
+ },
289
+ {
290
+ "id": "text",
291
+ "type": "textarea",
292
+ "label": "Açıklama",
293
+ "default": "Sonra sadece aracına uyan parçaları gösterelim."
294
+ },
295
+ {
296
+ "id": "button_label",
297
+ "type": "text",
298
+ "label": "Düğme",
299
+ "default": "Parçaları göster"
300
+ },
301
+ {
302
+ "id": "image",
303
+ "type": "image",
304
+ "label": "Görsel",
305
+ "default": ""
306
+ }
307
+ ]
308
+ },
309
+ "side-cart": {
310
+ "name": "Yan sepet",
311
+ "settings": [
312
+ {
313
+ "id": "title",
314
+ "type": "text",
315
+ "label": "Başlık",
316
+ "default": "Sepetin"
317
+ },
318
+ {
319
+ "id": "free_shipping_threshold",
320
+ "type": "number",
321
+ "label": "Ücretsiz kargo sınırı (TL)",
322
+ "info": "0: kapalı",
323
+ "default": 0,
324
+ "min": 0
325
+ }
326
+ ]
327
+ },
328
+ "tab-menu": {
329
+ "name": "Sekmeli menü",
330
+ "settings": [
331
+ {
332
+ "id": "limit",
333
+ "type": "range",
334
+ "label": "Sekme sayısı",
335
+ "default": 6,
336
+ "min": 2,
337
+ "max": 12,
338
+ "step": 1
339
+ },
340
+ {
341
+ "id": "colored",
342
+ "type": "checkbox",
343
+ "label": "Kategori renkleri",
344
+ "default": true
345
+ }
346
+ ]
347
+ }
348
+ },
349
+ "deniedElements": [
350
+ "script",
351
+ "style",
352
+ "iframe",
353
+ "frame",
354
+ "frameset",
355
+ "object",
356
+ "embed",
357
+ "applet",
358
+ "base",
359
+ "meta",
360
+ "link",
361
+ "template",
362
+ "foreignobject",
363
+ "noembed",
364
+ "noframes",
365
+ "xmp",
366
+ "plaintext",
367
+ "portal",
368
+ "set",
369
+ "animate",
370
+ "handler"
371
+ ],
372
+ "deniedAttributes": [
373
+ "srcdoc",
374
+ "http-equiv"
375
+ ],
376
+ "urlAttributes": [
377
+ "href",
378
+ "src",
379
+ "action",
380
+ "formaction",
381
+ "poster",
382
+ "xlink:href",
383
+ "background",
384
+ "cite",
385
+ "data",
386
+ "ping",
387
+ "codebase",
388
+ "manifest",
389
+ "lowsrc",
390
+ "dynsrc",
391
+ "longdesc",
392
+ "usemap"
393
+ ],
394
+ "urlSchemes": [
395
+ "http",
396
+ "https",
397
+ "mailto",
398
+ "tel"
399
+ ],
400
+ "settingTypes": [
401
+ "text",
402
+ "textarea",
403
+ "number",
404
+ "range",
405
+ "checkbox",
406
+ "select",
407
+ "color",
408
+ "url",
409
+ "image"
410
+ ],
411
+ "roots": [
412
+ "shop",
413
+ "settings",
414
+ "section",
415
+ "product",
416
+ "collection",
417
+ "cart",
418
+ "request",
419
+ "customer",
420
+ "layout"
421
+ ],
422
+ "paths": [
423
+ "theme.json",
424
+ "config/settings_schema.json",
425
+ "config/settings_data.json",
426
+ "templates/{template}.json",
427
+ "locales/{locale}.json",
428
+ "styles/*.scss",
429
+ "assets/**",
430
+ "sections/*.html.tmpl",
431
+ "sections/*.schema.json",
432
+ "snippets/*.html.tmpl",
433
+ "layout/theme.html.tmpl"
434
+ ],
435
+ "limits": {
436
+ "archiveBytes": 41943040,
437
+ "fileBytes": 5242880,
438
+ "totalBytes": 62914560,
439
+ "files": 600,
440
+ "styleBytes": 524288,
441
+ "jsonBytes": 1048576,
442
+ "localeBytes": 262144,
443
+ "localeEntries": 2000,
444
+ "templateBytes": 65536,
445
+ "renderBytes": 262144,
446
+ "renderMilliseconds": 50,
447
+ "listItems": 100,
448
+ "includeDepth": 8,
449
+ "instructions": 10000,
450
+ "sectionSettings": 25
451
+ }
452
+ };