uvako-cli 0.4.0 → 0.5.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 CHANGED
@@ -1,6 +1,8 @@
1
1
  # uvako-cli
2
2
 
3
- Uvako mağazaları için tema geliştirme aracı. Temayı başlatır, denetler, mağazanıza yükler ve kaydettikçe canlı önizletir.
3
+ Uvako için tema ve uygulama geliştirme aracı. Temayı başlatır, denetler, mağazanıza yükler ve kaydettikçe
4
+ canlı önizletir. Herkese açık uygulamayı başlatır, yerelde çalıştırıp webhook'ları bilgisayarınıza aktarır
5
+ ve dağıtır.
4
6
 
5
7
  **[Uvako](https://uvako.com)** — Rox Software ürünü.
6
8
 
@@ -57,6 +59,35 @@ paneline giriş yapmış olmanız gerekir. Tema, panelden yayınlanana kadar vit
57
59
  `uvako-cli` aynı komutların uzun adıdır. Hata varsa çıkış kodu 1'dir. CI için `UVAKO_STORE` ve
58
60
  `UVAKO_TOKEN` ortam değişkenleri girişin yerini tutar.
59
61
 
62
+ ## Uygulamalar
63
+
64
+ Uvako partnerleri birden çok mağazaya kurulabilen uygulama yazar. Mağaza uygulamayı panelindeki onay
65
+ ekranından kurar (OAuth); uygulama yalnızca onaylanan kapsamlarla çalışır.
66
+
67
+ ```bash
68
+ uvako login # partner hesabı: tarayıcıda partner panelinden onaylanır
69
+ uvako app init stok-senkron # örnek uygulama + uvako.app.json, Uvako'ya kaydeder
70
+ cd stok-senkron
71
+ uvako app dev --store <geliştirme mağazası>
72
+ ```
73
+
74
+ `app dev` örnek uygulamayı client id ve secret ile başlatır, geliştirme mağazanızda uygulamanın adresini
75
+ `http://localhost:3000` yapar, o mağazanın webhook'larını bilgisayarınızdaki alıcıya aktarır (imzasıyla) ve
76
+ kurulum bağlantısını yazar. Tünel gerekmez.
77
+
78
+ | Komut | Ne yapar |
79
+ |---|---|
80
+ | `uvako login` | Partner hesabıyla cihaz girişi (`--no-browser`: tarayıcıyı açmaz). |
81
+ | `uvako logout --partner` | Partner girişini kapatır (anahtar sunucuda da iptal edilir). |
82
+ | `uvako app init <ad>` | Örnek uygulama (`--dir`, `--offline`: kaydetmeden). |
83
+ | `uvako app dev [klasör]` | Yerelde çalıştırma ve webhook aktarımı (`--store`, `--port`, `--no-serve`, `--no-update`). |
84
+ | `uvako app deploy [klasör]` | `uvako.app.json`'u yeni sürüm olarak dağıtır (`--submit`). |
85
+ | `uvako app submit [klasör]` | Uvako incelemesine gönderir; onaylanınca tüm mağazalar kurabilir. |
86
+ | `uvako app list` | Uygulamalarınızı listeler (`--json`). |
87
+
88
+ Her uygulama `app/uninstalled` ve üç KVKK/GDPR webhook'unu (`privacy_webhooks`) tanımlamak zorundadır.
89
+ Client secret diske yazılmaz. CI için `UVAKO_PARTNER_TOKEN`. Ayrıntı: Uvako Admin API belgesi.
90
+
60
91
  ## Tema paketi
61
92
 
62
93
  Tema kod içermez. Bir katalog temasını (`base`) temel alır ve onun üstüne şunları getirir:
package/bin/uvako-cli.js CHANGED
@@ -1,10 +1,15 @@
1
1
  #!/usr/bin/env node
2
2
  import { main } from "../src/main.js";
3
3
 
4
- main(process.argv.slice(2)).then(
5
- (code) => process.exit(code),
6
- (error) => {
7
- console.error(`Hata: ${error?.message ?? error}`);
8
- process.exit(1);
9
- },
10
- );
4
+ // Çıkış kodu atanır ve süreç kendiliğinden kapanır: process.exit, Windows'ta kapanmakta olan fetch
5
+ // bağlantılarının üstüne gelince libuv uyarısıyla düşebiliyor. Açık kalan bir şey olursa kısa süre
6
+ // sonra yine de çıkılır.
7
+ const finish = (code) => {
8
+ process.exitCode = code;
9
+ setTimeout(() => process.exit(code), 3000).unref();
10
+ };
11
+
12
+ main(process.argv.slice(2)).then(finish, (error) => {
13
+ console.error(`Hata: ${error?.message ?? error}`);
14
+ finish(1);
15
+ });
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "uvako-cli",
3
- "version": "0.4.0",
4
- "description": "Uvako tema geliştirme aracı. Tema başlatır, denetler, mağazaya yükler ve canlı önizletir. https://uvako.com",
3
+ "version": "0.5.0",
4
+ "description": "Uvako tema ve uygulama geliştirme aracı. Tema başlatır, denetler ve canlı önizletir; herkese açık uygulama başlatır, yerelde çalıştırıp webhook aktarır ve dağıtır. https://uvako.com",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",
7
7
  "author": {
@@ -42,7 +42,8 @@
42
42
  "files": [
43
43
  "bin",
44
44
  "src",
45
- "README.md"
45
+ "README.md",
46
+ "templates"
46
47
  ],
47
48
  "engines": {
48
49
  "node": ">=22"
package/src/app.js ADDED
@@ -0,0 +1,412 @@
1
+ import { spawn } from "node:child_process";
2
+ import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
3
+ import path from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import { ApiError } from "./api.js";
6
+ import { partnerSaved } from "./config.js";
7
+ import { t, tm } from "./i18n.js";
8
+ import { explainPartner, partnerCall } from "./partner.js";
9
+
10
+ // `uvako app`: herkese açık uygulamalar. Yapılandırma uygulama klasöründeki uvako.app.json'dur;
11
+ // client secret diske yazılmaz (`app dev` yerel uygulamaya ortam değişkeniyle verir).
12
+
13
+ export const configFile = "uvako.app.json";
14
+ const templateDir = path.join(path.dirname(fileURLToPath(import.meta.url)), "..", "templates", "app");
15
+ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
16
+ const stamp = () => new Date().toLocaleTimeString("tr-TR");
17
+
18
+ export function slug(text) {
19
+ return String(text ?? "")
20
+ .toLocaleLowerCase("tr")
21
+ .replace(/ı/g, "i").replace(/ğ/g, "g").replace(/ü/g, "u").replace(/ş/g, "s").replace(/ö/g, "o").replace(/ç/g, "c")
22
+ .replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40);
23
+ }
24
+
25
+ /** Yeni uygulamanın yapılandırması. Adresler örnektir; dağıtmadan önce gerçek adresle değiştirilir. */
26
+ export function starterConfig(name) {
27
+ return {
28
+ client_id: "",
29
+ name,
30
+ application_url: "https://example.com",
31
+ redirect_urls: ["https://example.com/auth/callback"],
32
+ scopes: ["read_orders", "read_products"],
33
+ // Panelde iframe içinde açılır (@uvako/app-bridge ve oturum belirteci).
34
+ embedded: true,
35
+ webhooks: [{ topics: ["orders/create", "app/uninstalled", "app_subscriptions/update"], uri: "/webhooks" }],
36
+ // KVKK/GDPR zorunlu webhook'ları: üçü de tanımlı olmalı.
37
+ privacy_webhooks: {
38
+ customer_data_request_url: "/webhooks/customers/data_request",
39
+ customer_deletion_url: "/webhooks/customers/redact",
40
+ shop_deletion_url: "/webhooks/shop/redact",
41
+ },
42
+ listing: { tagline: "", description: "", category: "", pricing: "", website: "", support_email: "", privacy_url: "" },
43
+ dev: { command: "node server.js", port: 3000 },
44
+ };
45
+ }
46
+
47
+ export function readConfig(dir) {
48
+ const file = path.join(dir, configFile);
49
+ if (!existsSync(file)) throw new Error(t("{0} bir uygulama klasörü değil: {1} bulunamadı. Yeni uygulama için: uvako app init <ad>", dir, configFile));
50
+ let config;
51
+ try {
52
+ config = JSON.parse(readFileSync(file, "utf8"));
53
+ } catch (error) {
54
+ throw new Error(t("{0} geçerli JSON değil: {1}", configFile, error.message));
55
+ }
56
+ const problems = checkConfig(config);
57
+ if (problems.length) throw new Error(problems.map((p) => `${configFile}: ${p}`).join("\n"));
58
+ return config;
59
+ }
60
+
61
+ function writeConfig(dir, config) {
62
+ writeFileSync(path.join(dir, configFile), `${JSON.stringify(config, null, 2)}\n`);
63
+ }
64
+
65
+ /** Yerel denetim (kolaylık içindir; asıl denetimi sunucu yapar). */
66
+ export function checkConfig(config) {
67
+ const problems = [];
68
+ if (!config || typeof config !== "object" || Array.isArray(config)) return [t("yapılandırma bir nesne olmalı.")];
69
+ if (typeof config.name !== "string" || !config.name.trim() || config.name.length > 60) problems.push(t("\"name\" gerekli (en çok 60 karakter)."));
70
+ if (config.client_id !== undefined && config.client_id !== "" && !/^[0-9a-f]{32}$/.test(config.client_id)) problems.push(t("\"client_id\" 32 karakterlik bir kimlik olmalı (ya da boş)."));
71
+ const https = (value) => {
72
+ try {
73
+ const u = new URL(value);
74
+ return u.protocol === "https:" && !u.username && !u.password;
75
+ } catch {
76
+ return false;
77
+ }
78
+ };
79
+ if (!https(config.application_url)) problems.push(t("\"application_url\" https:// ile başlayan bir adres olmalı."));
80
+ if (!Array.isArray(config.redirect_urls) || config.redirect_urls.length === 0 || config.redirect_urls.some((u) => !https(u))) problems.push(t("\"redirect_urls\" https adreslerinden oluşan bir liste olmalı."));
81
+ if (!Array.isArray(config.scopes) || config.scopes.length === 0 || config.scopes.some((s) => !/^(read|write)_[a-z_]+$/.test(s))) problems.push(t("\"scopes\" kapsam listesi olmalı (ör. [\"read_products\"])."));
82
+ if (config.webhooks !== undefined && (!Array.isArray(config.webhooks) || config.webhooks.some((w) => !Array.isArray(w?.topics) || typeof w?.uri !== "string"))) problems.push(t("\"webhooks\" [{\"topics\": [...], \"uri\": \"/webhooks\"}] biçiminde olmalı."));
83
+ const privacy = config.privacy_webhooks;
84
+ for (const key of ["customer_data_request_url", "customer_deletion_url", "shop_deletion_url"]) {
85
+ const value = privacy?.[key];
86
+ if (typeof value !== "string" || !(value.startsWith("/") ? !value.startsWith("//") : https(value))) problems.push(t("\"privacy_webhooks.{0}\" zorunlu: /webhooks/... yolu ya da https adresi (KVKK/GDPR).", key));
87
+ }
88
+ if (Array.isArray(config.webhooks) && !config.webhooks.some((w) => Array.isArray(w?.topics) && w.topics.includes("app/uninstalled"))) problems.push(t("\"webhooks\" içinde zorunlu app/uninstalled konusu yok."));
89
+ if (config.dev !== undefined &&(typeof config.dev !== "object" || (config.dev.port !== undefined && !(Number.isInteger(config.dev.port) && config.dev.port > 0 && config.dev.port < 65536)))) problems.push(t("\"dev.port\" geçerli bir port olmalı."));
90
+ return problems;
91
+ }
92
+
93
+ /** Sunucuya giden yapılandırma: client_id ve dev yerel kalır. */
94
+ export function remoteConfig(config) {
95
+ const { client_id: _id, dev: _dev, ...rest } = config;
96
+ return { webhooks: [], listing: {}, ...rest };
97
+ }
98
+
99
+ export const blocksDir = "blocks";
100
+ const blockFiles = new Set(["block.json", "block.html", "block.css"]);
101
+
102
+ /**
103
+ * Vitrin blokları (theme app extension): blocks/<anahtar>/block.json (ad ve ayar şeması), block.html
104
+ * (şablon) ve isteğe bağlı block.css. Klasör varsa yapılandırmanın theme_blocks alanı buradan kurulur.
105
+ * Blokta kod çalışmaz; asıl denetimi sunucu yapar (tema paketleriyle aynı kurallar).
106
+ */
107
+ export function readBlocks(dir) {
108
+ const root = path.join(dir, blocksDir);
109
+ if (!existsSync(root)) return null;
110
+ const blocks = [];
111
+ for (const handle of readdirSync(root).sort()) {
112
+ const folder = path.join(root, handle);
113
+ if (!statSync(folder).isDirectory()) continue;
114
+ for (const name of readdirSync(folder)) {
115
+ if (!blockFiles.has(name)) throw new Error(t("{0}: bilinmeyen dosya {1}. Blokta yalnızca block.json, block.html ve block.css olabilir.", `${blocksDir}/${handle}`, name));
116
+ }
117
+ const read = (name) => (existsSync(path.join(folder, name)) ? readFileSync(path.join(folder, name), "utf8") : "");
118
+ let meta;
119
+ try {
120
+ meta = JSON.parse(read("block.json") || "{}");
121
+ } catch (error) {
122
+ throw new Error(t("{0} geçerli JSON değil: {1}", `${blocksDir}/${handle}/block.json`, error.message));
123
+ }
124
+ blocks.push({ handle, name: meta.name ?? handle, template: read("block.html"), css: read("block.css"), settings: meta.settings ?? [] });
125
+ }
126
+ return blocks;
127
+ }
128
+
129
+ /** Yerel denetim: sunucunun reddedeceği belirgin hatalar (betik, olay özniteliği, döngü). */
130
+ export function checkBlocks(blocks) {
131
+ const problems = [];
132
+ for (const b of blocks ?? []) {
133
+ const where = `${blocksDir}/${b.handle}`;
134
+ if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(b.handle)) problems.push(t("{0}: klasör adı küçük harf, rakam ve tire olmalı.", where));
135
+ if (!b.template.trim()) problems.push(t("{0}: block.html boş.", where));
136
+ if (/<script\b|\son[a-z]+\s*=|javascript:/i.test(b.template)) problems.push(t("{0}: blokta betik çalıştırılamaz (<script>, onclick=, javascript:).", where));
137
+ if (/\{\{-?\s*(range|with|define|template|block)\b|\{\{-?\s*\$/.test(b.template)) problems.push(t("{0}: şablonda yalnızca {{ .settings.ad }} ve if/else kullanılabilir.", where));
138
+ if (/url\s*\(|@import/i.test(b.css)) problems.push(t("{0}: block.css içinde url() ve @import kullanılamaz.", where));
139
+ if (!Array.isArray(b.settings)) problems.push(t("{0}: block.json \"settings\" bir liste olmalı.", where));
140
+ }
141
+ return problems;
142
+ }
143
+
144
+ /** Bloklarla birlikte yapılandırma (blocks/ klasörü varsa theme_blocks onunla değiştirilir). */
145
+ export function withBlocks(dir, config) {
146
+ const blocks = readBlocks(dir);
147
+ if (!blocks) return config;
148
+ const problems = checkBlocks(blocks);
149
+ if (problems.length) throw new Error(problems.join("\n"));
150
+ return { ...config, theme_blocks: blocks };
151
+ }
152
+
153
+ function fill(text, vars) {
154
+ return text.replace(/\{\{(\w+)\}\}/g, (_, key) => vars[key] ?? "");
155
+ }
156
+
157
+ /** `uvako app init <ad>`: örnek uygulama klasörü. Partner girişi varsa uygulama Uvako'ya da kaydedilir. */
158
+ export async function initApp(name, options = {}) {
159
+ if (!name || name === true) throw new Error(t("Uygulama adı gerekli: uvako app init <ad>"));
160
+ const handle = slug(name);
161
+ if (!handle) throw new Error(t("Uygulama adı harf ya da rakam içermeli."));
162
+ const dir = path.resolve(options.cwd ?? process.cwd(), typeof options.dir === "string" ? options.dir : handle);
163
+ if (existsSync(path.join(dir, configFile))) throw new Error(t("Klasörde zaten bir uygulama var: {0}", dir));
164
+ mkdirSync(dir, { recursive: true });
165
+ const config = starterConfig(String(name));
166
+ writeFileSync(path.join(dir, "server.js"), readFileSync(path.join(templateDir, "server.js"), "utf8"));
167
+ writeFileSync(path.join(dir, "README.md"), fill(readFileSync(path.join(templateDir, "README.md"), "utf8"), { name: config.name }));
168
+ writeFileSync(path.join(dir, "package.json"), `${JSON.stringify({ name: handle, private: true, type: "module", scripts: { dev: "uvako app dev", start: "node server.js" }, engines: { node: ">=22" } }, null, 2)}\n`);
169
+ writeFileSync(path.join(dir, ".gitignore"), ".uvako/\nnode_modules/\n.env\n");
170
+ // Örnek vitrin bloğu: tema editöründen ana sayfaya eklenir.
171
+ const example = path.join(templateDir, blocksDir, "yildiz-puani");
172
+ mkdirSync(path.join(dir, blocksDir, "yildiz-puani"), { recursive: true });
173
+ for (const name of blockFiles) {
174
+ if (existsSync(path.join(example, name))) writeFileSync(path.join(dir, blocksDir, "yildiz-puani", name), readFileSync(path.join(example, name), "utf8"));
175
+ }
176
+ writeConfig(dir, config);
177
+ let registered = false;
178
+ if (!options.offline && (partnerSaved() || process.env.UVAKO_PARTNER_TOKEN)) {
179
+ const created = await partnerCall("POST", "/apps", { body: remoteConfig(withBlocks(dir, config)) });
180
+ config.client_id = created.app.clientId;
181
+ writeConfig(dir, config);
182
+ registered = true;
183
+ }
184
+ return { dir, config, registered };
185
+ }
186
+
187
+ /** Kayıtlı değilse kaydeder, değilse yapılandırmayı yeni sürüm olarak dağıtır. */
188
+ async function syncRemote(dir, config) {
189
+ const body = remoteConfig(withBlocks(dir, config));
190
+ if (!config.client_id) {
191
+ const created = await partnerCall("POST", "/apps", { body });
192
+ config.client_id = created.app.clientId;
193
+ writeConfig(dir, config);
194
+ return { app: created.app, created: true, changed: true };
195
+ }
196
+ const before = await partnerCall("GET", `/apps/${config.client_id}`);
197
+ const app = await partnerCall("PUT", `/apps/${config.client_id}/config`, { body });
198
+ return { app, created: false, changed: app.revision !== before.revision };
199
+ }
200
+
201
+ const statusText = (app) =>
202
+ app.suspendedAt ? t("askıda") : { draft: t("taslak"), pending: t("incelemede"), approved: t("yayında"), rejected: t("reddedildi") }[app.status] ?? app.status;
203
+
204
+ export async function deployApp(target, options = {}) {
205
+ const dir = path.resolve(target ?? ".");
206
+ try {
207
+ const config = readConfig(dir);
208
+ const { app, created, changed } = await syncRemote(dir, config);
209
+ if (created) console.log(t("Uygulama kaydedildi: {0} (client_id {1})", app.config.name, app.clientId));
210
+ console.log(changed ? t("Dağıtıldı: {0} sürüm {1}", app.config.name, app.revision) : t("Değişiklik yok: {0} sürüm {1}", app.config.name, app.revision));
211
+ console.log(t("Durum: {0}{1}", statusText(app), app.publishedRevision ? t(" · mağazalardaki sürüm {0}", app.publishedRevision) : ""));
212
+ if (app.publishedRevision !== app.revision) console.log(t("Bu sürüm yalnızca kendi geliştirme mağazalarınızda geçerli. Herkese açmak için: uvako app submit"));
213
+ if (options.submit) return submitApp(target);
214
+ return 0;
215
+ } catch (error) {
216
+ if (error instanceof ApiError) return explainPartner(error);
217
+ throw error;
218
+ }
219
+ }
220
+
221
+ export async function submitApp(target) {
222
+ const dir = path.resolve(target ?? ".");
223
+ try {
224
+ const config = readConfig(dir);
225
+ if (!config.client_id) throw new Error(t("Uygulama henüz kayıtlı değil. Önce: uvako app deploy"));
226
+ const app = await partnerCall("POST", `/apps/${config.client_id}/submit`, { body: {} });
227
+ console.log(t("İncelemeye gönderildi: {0} sürüm {1}. Uvako onayladığında tüm mağazalar bu sürümü kurabilir.", app.config.name, app.revision));
228
+ return 0;
229
+ } catch (error) {
230
+ if (error instanceof ApiError) return explainPartner(error);
231
+ throw error;
232
+ }
233
+ }
234
+
235
+ export async function listApps(options = {}) {
236
+ try {
237
+ const { apps } = await partnerCall("GET", "/apps");
238
+ if (options.json) {
239
+ console.log(JSON.stringify(apps, null, 2));
240
+ return 0;
241
+ }
242
+ if (!apps.length) console.log(t("Kayıtlı uygulamanız yok. Başlamak için: uvako app init <ad>"));
243
+ for (const a of apps) console.log(` ${a.config.name} [${a.clientId}] ${t("sürüm {0}", a.revision)} · ${statusText(a)} · ${t("{0} kurulum", a.installs)}`);
244
+ return 0;
245
+ } catch (error) {
246
+ if (error instanceof ApiError) return explainPartner(error);
247
+ throw error;
248
+ }
249
+ }
250
+
251
+ /**
252
+ * Aktarılan bir webhook teslimatını yerel alıcıya gönderir. Başlıklar ve gövde Uvako'nun imzaladığı
253
+ * hâliyle gider; yol teslimat adresinin yoludur (https://uygulama.example.com/webhooks → /webhooks).
254
+ */
255
+ export async function forward(delivery, localOrigin, timeout = 5000) {
256
+ const source = new URL(delivery.address);
257
+ const target = `${localOrigin}${source.pathname}${source.search}`;
258
+ const started = Date.now();
259
+ try {
260
+ const response = await fetch(target, { method: "POST", headers: delivery.headers, body: delivery.body, redirect: "manual", signal: AbortSignal.timeout(timeout) });
261
+ const text = (await response.text()).slice(0, 2048);
262
+ return { target, outcome: { code: response.status, body: text, error: "", durationMs: Date.now() - started } };
263
+ } catch (error) {
264
+ return { target, outcome: { code: 0, body: "", error: String(error.cause?.code ?? error.message ?? error), durationMs: Date.now() - started } };
265
+ }
266
+ }
267
+
268
+ /** Bir oturumun bekleyen teslimatlarını alıp yerel alıcıya iletir; iletilen sayısını döner. */
269
+ export async function relayOnce(sessionId, localOrigin, log = console.log) {
270
+ const { deliveries } = await partnerCall("GET", `/dev/${sessionId}/deliveries`, { timeout: 20000 });
271
+ for (const d of deliveries) {
272
+ const { target, outcome } = await forward(d, localOrigin);
273
+ await partnerCall("POST", `/dev/${sessionId}/deliveries/${d.id}`, { body: outcome });
274
+ const result = outcome.code ? `${outcome.code}` : t("ulaşılamadı ({0})", outcome.error);
275
+ log(t("[{0}] {1} → {2} {3} ({4} ms)", stamp(), d.topic, new URL(target).pathname, result, outcome.durationMs));
276
+ }
277
+ return deliveries.length;
278
+ }
279
+
280
+ /** `uvako app dev`: yerel uygulamayı başlatır, geliştirme mağazasını ona bağlar ve webhook'ları aktarır. */
281
+ export async function devApp(target, options = {}) {
282
+ const dir = path.resolve(target ?? ".");
283
+ let config;
284
+ let session;
285
+ let child = null;
286
+ try {
287
+ config = readConfig(dir);
288
+ const port = Number(options.port ?? config.dev?.port ?? 3000);
289
+ if (!Number.isInteger(port) || port < 1 || port > 65535) throw new Error(t("Geçersiz port: {0}", options.port));
290
+ const local = `http://localhost:${port}`;
291
+
292
+ if (!options["no-update"]) {
293
+ const { app, created, changed } = await syncRemote(dir, config);
294
+ if (created) console.log(t("Uygulama kaydedildi: {0} (client_id {1})", app.config.name, app.clientId));
295
+ else if (changed) console.log(t("Yapılandırma dağıtıldı: sürüm {0} (yalnızca kendi mağazalarınızda)", app.revision));
296
+ } else if (!config.client_id) {
297
+ throw new Error(t("Uygulama henüz kayıtlı değil. --no-update olmadan çalıştırın ya da: uvako app deploy"));
298
+ }
299
+
300
+ let store = typeof options.store === "string" ? options.store : "";
301
+ if (!store) {
302
+ const { stores } = await partnerCall("GET", "/stores");
303
+ if (stores.length !== 1) {
304
+ console.error(stores.length ? t("Hangi mağazada geliştireceğinizi seçin: --store <mağaza>") : t("Partner hesabınızda geliştirme mağazası yok. partner.uvako.com → Mağazalar → Geliştirme mağazası aç."));
305
+ for (const s of stores) console.error(` ${s.slug} ${s.host}`);
306
+ return 1;
307
+ }
308
+ store = stores[0].slug;
309
+ }
310
+ const redirects = config.redirect_urls.map((u) => {
311
+ const parsed = new URL(u);
312
+ return `${local}${parsed.pathname}`;
313
+ });
314
+ session = await partnerCall("POST", `/apps/${config.client_id}/dev`, { body: { store, app_url: local, redirect_urls: [...new Set(redirects)] } });
315
+ const { clientSecret } = await partnerCall("POST", `/apps/${config.client_id}/secret`, { body: {} });
316
+
317
+ if (!options["no-serve"]) {
318
+ const command = String(config.dev?.command ?? "node server.js");
319
+ const adminOrigin = session.store.admin.replace(/\/admin$/, "");
320
+ child = spawn(command, {
321
+ cwd: dir,
322
+ shell: true,
323
+ stdio: "inherit",
324
+ env: {
325
+ ...process.env,
326
+ PORT: String(port),
327
+ UVAKO_CLIENT_ID: config.client_id,
328
+ UVAKO_CLIENT_SECRET: clientSecret,
329
+ UVAKO_APP_URL: local,
330
+ UVAKO_SCOPES: config.scopes.join(","),
331
+ UVAKO_API_VERSION: "2026-10",
332
+ // Yerel mağaza (http) için; gerçek mağazalarda uygulama https://<shop> kullanır.
333
+ UVAKO_ADMIN_ORIGIN: adminOrigin.startsWith("http://") ? adminOrigin : "",
334
+ // Yerelde oturum belirtecinin dest'i bu mağazadır (örnek uygulama: shopFromOrigin).
335
+ UVAKO_SHOP: session.store.host,
336
+ },
337
+ });
338
+ child.on("exit", (code) => {
339
+ if (code !== null && code !== 0) console.error(t("! Yerel uygulama {0} koduyla kapandı.", code));
340
+ });
341
+ }
342
+
343
+ console.log(t("\nGeliştirme: {0} → {1} ({2})", config.name, session.store.name, session.store.host));
344
+ console.log(t("Yerel uygulama: {0}", local));
345
+ console.log(t("Kur / aç: {0}", session.installUrl));
346
+ console.log(t("Bu mağazada uygulamanın adresi yerel adres oldu ve webhook'ları buraya aktarılıyor. Durdurmak için Ctrl+C.\n"));
347
+ } catch (error) {
348
+ child?.kill();
349
+ if (error instanceof ApiError) return explainPartner(error);
350
+ console.error(`✖ ${tm(error.message)}`);
351
+ return 1;
352
+ }
353
+
354
+ let stopping = false;
355
+ const stop = async () => {
356
+ if (stopping) return;
357
+ stopping = true;
358
+ try {
359
+ await partnerCall("DELETE", `/dev/${session.id}`);
360
+ } catch {
361
+ // Oturum 45 saniye sonra kendiliğinden düşer.
362
+ }
363
+ child?.kill();
364
+ console.log(t("\nGeliştirme durdu. Mağaza dağıtılan adresleri kullanıyor; webhook'lar yeniden internetten gidiyor."));
365
+ };
366
+ return new Promise((resolve) => {
367
+ process.on("SIGINT", () => void stop().then(() => resolve(0)));
368
+ const local = `http://localhost:${Number(options.port ?? config.dev?.port ?? 3000)}`;
369
+ (async () => {
370
+ let failures = 0;
371
+ while (!stopping) {
372
+ try {
373
+ const count = await relayOnce(session.id, local);
374
+ failures = 0;
375
+ if (count === 0) await sleep(1500);
376
+ } catch (error) {
377
+ if (stopping) break;
378
+ if (error instanceof ApiError && (error.status === 401 || error.status === 422 || error.status === 404)) {
379
+ console.error(`✖ ${tm(error.message)}`);
380
+ await stop();
381
+ resolve(1);
382
+ return;
383
+ }
384
+ failures++;
385
+ if (failures === 1 || failures % 10 === 0) console.error(t("[{0}] ! Partner paneline ulaşılamadı: {1}", stamp(), tm(error.message)));
386
+ await sleep(3000);
387
+ }
388
+ }
389
+ })();
390
+ });
391
+ }
392
+
393
+ export async function appCommand(command, target, options) {
394
+ if (command === "init") {
395
+ try {
396
+ const { dir, config, registered } = await initApp(target, options);
397
+ const rel = path.relative(process.cwd(), dir) || ".";
398
+ console.log(t("Uygulama oluşturuldu: {0}", rel));
399
+ console.log(registered ? t("Uvako'ya kaydedildi: client_id {0}", config.client_id) : t("Henüz kayıtlı değil (giriş yok ya da --offline). Kayıt için: uvako login, sonra uvako app deploy {0}", rel));
400
+ console.log(t("Sıradaki adım: cd {0} && uvako app dev --store <geliştirme mağazası>", rel));
401
+ return 0;
402
+ } catch (error) {
403
+ if (error instanceof ApiError) return explainPartner(error);
404
+ throw error;
405
+ }
406
+ }
407
+ if (command === "dev") return devApp(target, options);
408
+ if (command === "deploy") return deployApp(target, options);
409
+ if (command === "submit") return submitApp(target);
410
+ if (command === "list") return listApps(options);
411
+ return null;
412
+ }
package/src/config.js CHANGED
@@ -8,6 +8,13 @@ import { t } from "./i18n.js";
8
8
 
9
9
  export const defaultStoreApex = "uvoo.store";
10
10
  export const siteOrigin = () => process.env.UVAKO_SITE_URL || "https://uvako.com";
11
+ /** Partner paneli: `uvako login` (cihaz girişi) ve `uvako app` komutları buraya bağlanır. */
12
+ export const partnerOrigin = () => {
13
+ const raw = (process.env.UVAKO_PARTNER_URL || "https://partner.uvako.com").replace(/\/+$/, "");
14
+ const url = new URL(raw);
15
+ if (url.protocol !== "https:" && !local(url.hostname)) throw new Error(t("Partner paneline yalnızca https ile bağlanılır."));
16
+ return url.origin;
17
+ };
11
18
 
12
19
  export function configDir() {
13
20
  if (process.env.UVAKO_CONFIG_DIR) return process.env.UVAKO_CONFIG_DIR;
@@ -20,9 +27,13 @@ const file = () => path.join(configDir(), "credentials.json");
20
27
  function read() {
21
28
  try {
22
29
  const data = JSON.parse(readFileSync(file(), "utf8"));
23
- return { current: typeof data.current === "string" ? data.current : "", stores: data.stores && typeof data.stores === "object" ? data.stores : {} };
30
+ return {
31
+ current: typeof data.current === "string" ? data.current : "",
32
+ stores: data.stores && typeof data.stores === "object" ? data.stores : {},
33
+ partner: data.partner && typeof data.partner === "object" ? data.partner : null,
34
+ };
24
35
  } catch {
25
- return { current: "", stores: {} };
36
+ return { current: "", stores: {}, partner: null };
26
37
  }
27
38
  }
28
39
 
@@ -99,3 +110,34 @@ export function sessions() {
99
110
  }
100
111
 
101
112
  export const credentialsPath = () => (existsSync(file()) ? file() : "");
113
+
114
+ /**
115
+ * Partner oturumu (uvako app komutları): önce UVAKO_PARTNER_TOKEN (CI), sonra `uvako login` ile
116
+ * kaydedilen giriş. Anahtar yalnızca girişin yapıldığı partner adresine gönderilir.
117
+ */
118
+ export function partnerSession() {
119
+ const origin = partnerOrigin();
120
+ if (process.env.UVAKO_PARTNER_TOKEN) return { origin, token: process.env.UVAKO_PARTNER_TOKEN.trim(), name: "" };
121
+ const entry = read().partner;
122
+ if (!entry?.token || entry.origin !== origin) throw new Error(t("Partner girişi yapılmamış. Önce: uvako login"));
123
+ return { origin, token: entry.token, name: entry.name ?? "", email: entry.email ?? "" };
124
+ }
125
+
126
+ export function savePartner(origin, token, name, email) {
127
+ const data = read();
128
+ data.partner = { origin, token, name, email, savedAt: new Date().toISOString() };
129
+ write(data);
130
+ }
131
+
132
+ export function dropPartner() {
133
+ const data = read();
134
+ const had = Boolean(data.partner);
135
+ data.partner = null;
136
+ write(data);
137
+ return had;
138
+ }
139
+
140
+ export function partnerSaved() {
141
+ const entry = read().partner;
142
+ return entry?.token ? { origin: entry.origin, name: entry.name ?? "", email: entry.email ?? "" } : null;
143
+ }