@enformik/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.
@@ -4,14 +4,14 @@ import { join, resolve } from 'node:path';
4
4
  import { CliError } from '../errors.js';
5
5
  import { formatBytes } from '../output.js';
6
6
  import { precheckSite, parseScanIgnore, DEFAULT_LIMITS } from '../precheck.js';
7
- import { collectMeta, DeployClient, idempotencyKey, isTerminal, MAX_ARCHIVE_BYTES, sha256File, waitForDeployment } from '../site.js';
7
+ import { collectMeta, DeployClient, finalizeDeployment, idempotencyKey, isTerminal, MAX_ARCHIVE_BYTES, sha256File, waitForDeployment } from '../site.js';
8
8
  import { writeZipFile } from '../zip.js';
9
9
  function client(ctx) {
10
10
  const { settings } = ctx;
11
11
  if (!settings.consoleUrl)
12
- throw new CliError('Konsol adresi yok. `enformik link --console-url https://…` çalıştırın ya da ENFORMIK_CONSOLE_URL tanımlayın.');
12
+ throw new CliError('No console URL. Run `enformik link --console-url https://…` or set ENFORMIK_CONSOLE_URL.');
13
13
  if (!settings.deployKey)
14
- throw new CliError('ENFORMIK_DEPLOY_KEY tanımlı değil (konsolda Ek ortamlar → Geliştirici sitesi → Dağıtım anahtarları).');
14
+ throw new CliError('ENFORMIK_DEPLOY_KEY is not set (in the console: Add-on environments → Developer site → Deploy keys).');
15
15
  return new DeployClient({ consoleUrl: settings.consoleUrl, deployKey: settings.deployKey, version: ctx.version, fetchImpl: ctx.fetchImpl });
16
16
  }
17
17
  /** --expect-host verilmişse hedef sitenin ana makine adı eşleşmeli; eşleşmezse hiçbir şey yüklenmeden durur. */
@@ -25,19 +25,19 @@ function assertExpectedHost(ctx, info) {
25
25
  }
26
26
  catch { /* adres bozuksa eşleşmez */ }
27
27
  if (expect.toLowerCase() !== host.toLowerCase()) {
28
- throw new CliError(`Beklenen site "${expect}" ama hedef "${host || info.url}". Yanlış siteye dağıtım yapılmaması için durduruldu.`);
28
+ throw new CliError(`Expected site "${expect}" but the target is "${host || info.url}". Stopped so that nothing is deployed to the wrong site.`);
29
29
  }
30
30
  }
31
31
  /** enformik site status */
32
32
  export async function siteStatus(ctx) {
33
33
  const info = await client(ctx).getSite();
34
34
  assertExpectedHost(ctx, info);
35
- ctx.out.info(`Web sitesi ortamı : ${info.environment.name}`);
36
- ctx.out.info(`Adres : ${info.url}`);
35
+ ctx.out.info(`Website environment : ${info.environment.name}`);
36
+ ctx.out.info(`URL : ${info.url}`);
37
37
  if (info.app)
38
- ctx.out.info(`Uygulama : ${info.app.name}${info.app.workspaceId ? ` (çalışma alanı ${info.app.workspaceId})` : ''}`);
39
- ctx.out.info(`Canlı sürüm : ${info.live ? `#${info.live.seq} (${info.live.at})` : 'henüz dağıtım yok'}`);
40
- ctx.out.info(`Anahtar bitişi : ${info.key.expiresAt.slice(0, 10)}`);
38
+ ctx.out.info(`App : ${info.app.name}${info.app.workspaceId ? ` (workspace ${info.app.workspaceId})` : ''}`);
39
+ ctx.out.info(`Live release : ${info.live ? `#${info.live.seq} (${info.live.at})` : 'no deployment yet'}`);
40
+ ctx.out.info(`Key expires : ${info.key.expiresAt.slice(0, 10)}`);
41
41
  ctx.out.result(info);
42
42
  return 0;
43
43
  }
@@ -51,7 +51,7 @@ export async function siteDeploy(ctx) {
51
51
  throw new Error();
52
52
  }
53
53
  catch {
54
- throw new CliError(`Derleme klasörü bulunamadı: ${dir} (önce siteyi derleyin ya da --dir verin).`);
54
+ throw new CliError(`Build folder not found: ${dir} (build the site first or pass --dir).`);
55
55
  }
56
56
  const timeoutSec = parseTimeout(ctx.flags.timeout);
57
57
  // Sunucunun kotası biliniyorsa ön denetimde kullan (aşımı yüklemeden görmek için).
@@ -67,7 +67,7 @@ export async function siteDeploy(ctx) {
67
67
  for (const w of pre.warnings)
68
68
  out.warn(w);
69
69
  if (pre.issues.length > 0) {
70
- out.error('Site arşivi kurallara uymuyor; yükleme yapılmadı:');
70
+ out.error('The site archive does not meet the rules; nothing was uploaded:');
71
71
  for (const i of pre.issues)
72
72
  out.error(` - ${i.path ? i.path + ': ' : ''}${i.message}`);
73
73
  out.result({ deployed: false, issues: pre.issues });
@@ -78,46 +78,45 @@ export async function siteDeploy(ctx) {
78
78
  const zipPath = join(tmp, 'site.zip');
79
79
  const z = writeZipFile(pre.files.map((f) => ({ name: f.name, path: f.path, mtime: f.mtime })), zipPath);
80
80
  if (z.bytes > MAX_ARCHIVE_BYTES)
81
- throw new CliError(`Sıkıştırılmış arşiv ${formatBytes(z.bytes)}; en çok ${formatBytes(MAX_ARCHIVE_BYTES)} olabilir.`);
82
- out.info(`${pre.files.length} dosya, ${formatBytes(pre.unpackedBytes)} → ${formatBytes(z.bytes)} (zip)`);
81
+ throw new CliError(`The compressed archive is ${formatBytes(z.bytes)}; the maximum is ${formatBytes(MAX_ARCHIVE_BYTES)}.`);
82
+ out.info(`${pre.files.length} files, ${formatBytes(pre.unpackedBytes)} → ${formatBytes(z.bytes)} (zip)`);
83
83
  const sha = sha256File(zipPath);
84
84
  const init = await api.init({ sizeBytes: z.bytes, sha256: sha, meta: collectMeta(ctx.env, ctx.cwd) }, idempotencyKey(ctx.env, undefined, sha));
85
85
  let dep = init.deployment;
86
86
  if (init.replayed)
87
- out.info(`Önceki deneme bulundu (#${dep.seq}, durum: ${dep.status}); kaldığı yerden devam ediliyor.`);
87
+ out.info(`Found a previous attempt (#${dep.seq}, status: ${dep.status}); resuming where it left off.`);
88
88
  // Durumuna göre devam: yükle → sonlandır → bekle (yeniden oynatılan kayıt için de doğru).
89
89
  if (dep.status === 'awaiting_upload') {
90
- out.info(`Yükleniyor (#${dep.seq})…`);
90
+ out.info(`Uploading (#${dep.seq})…`);
91
91
  await api.upload(dep.deploymentId, zipPath);
92
92
  dep = { ...dep, status: 'uploaded' };
93
93
  }
94
94
  if (dep.status === 'uploaded') {
95
- await api.finalize(dep.deploymentId);
96
- dep = { ...dep, status: 'validating' };
95
+ dep = await finalizeDeployment(api, dep);
97
96
  }
98
97
  // Terminal durumdaki (failed/expired/live) yeniden oynatılan kayıt --no-wait ile de gerçek sonucu vermeli.
99
98
  if (ctx.flags['no-wait'] === true && !isTerminal(dep.status)) {
100
- out.info(`Dağıtım başlatıldı (#${dep.seq}). Durum için: konsol → Geliştirici sitesi → Sürümler.`);
99
+ out.info(`Deployment started (#${dep.seq}). To follow its status: console → Developer site → Releases.`);
101
100
  out.result({ deployed: false, deploymentId: dep.deploymentId, seq: dep.seq, status: dep.status });
102
101
  return 0;
103
102
  }
104
103
  const final = isTerminal(dep.status) ? dep : await waitForDeployment(api, dep.deploymentId, {
105
104
  timeoutMs: timeoutSec * 1000,
106
- onStatus: (s) => out.info(` durum: ${s}`),
105
+ onStatus: (s) => out.info(` status: ${s}`),
107
106
  sleep: ctx.sleep,
108
107
  });
109
108
  if (final.status === 'live') {
110
- out.info(out.paint('green', `✓ Yayında: ${final.url ?? info.url} (sürüm #${final.seq})`));
109
+ out.info(out.paint('green', `✓ Live: ${final.url ?? info.url} (release #${final.seq})`));
111
110
  out.result({ deployed: true, deploymentId: final.deploymentId, seq: final.seq, url: final.url ?? info.url });
112
111
  return 0;
113
112
  }
114
113
  if (final.status === 'superseded') {
115
114
  // Daha yeni bir sürüm canlıya çıktı: istenen son durum sağlandı; CI'ı kırmayız ama açıkça uyarırız.
116
- out.warn(`Uyarı: bu dağıtım (#${final.seq}) daha yeni bir sürümle değiştirildi; canlı olan daha yeni sürümdür.`);
115
+ out.warn(`Warning: this deployment (#${final.seq}) was superseded by a newer release; the newer release is the one that is live.`);
117
116
  out.result({ deployed: false, superseded: true, status: 'superseded', deploymentId: final.deploymentId, seq: final.seq, url: info.url });
118
117
  return 0;
119
118
  }
120
- out.error(`✗ Dağıtım başarısız (#${final.seq}, ${final.status}): ${final.error ?? 'bilinmeyen neden'}`);
119
+ out.error(`✗ Deployment failed (#${final.seq}, ${final.status}): ${final.error ?? 'unknown reason'}`);
121
120
  for (const i of final.issues ?? [])
122
121
  out.error(` - ${i.path ? i.path + ': ' : ''}${i.message}`);
123
122
  out.result({ deployed: false, deploymentId: final.deploymentId, seq: final.seq, status: final.status, error: final.error, issues: final.issues });
@@ -133,6 +132,6 @@ function parseTimeout(raw) {
133
132
  return 600;
134
133
  const n = typeof raw === 'string' ? Number(raw) : NaN;
135
134
  if (!Number.isFinite(n) || n <= 0)
136
- throw new CliError(`--timeout pozitif bir saniye sayısı olmalı (verilen: ${String(raw)}).`);
135
+ throw new CliError(`--timeout must be a positive number of seconds (got: ${String(raw)}).`);
137
136
  return n;
138
137
  }
@@ -1,6 +1,6 @@
1
1
  import { discoverApp, fetchAccessToken, rolesFromClaims } from '../auth.js';
2
2
  import { DeployClient } from '../site.js';
3
- /** enformik whoami: hangi kimlikle, hangi hedefe bağlanıldığını gösterir (sır yazdırmaz). */
3
+ /** enformik whoami: shows which identity connects to which target (prints no secrets). */
4
4
  export async function whoami(ctx) {
5
5
  const { out, settings } = ctx;
6
6
  const result = {};
@@ -10,28 +10,32 @@ export async function whoami(ctx) {
10
10
  const app = await discoverApp(settings.appUrl, ctx.version, ctx.fetchImpl);
11
11
  const tok = await fetchAccessToken(app, settings.clientId, settings.clientSecret, ctx.version, ctx.fetchImpl);
12
12
  const roles = rolesFromClaims(tok.claims);
13
- out.info('Şema yükseltme (API istemcisi)');
14
- out.info(` Uygulama : ${settings.appUrl}`);
15
- out.info(` İstemci : ${settings.clientId}`);
16
- out.info(` Kimlik sağlayıcı : ${new URL(app.oidcAuthority).host}`);
17
- out.info(` Roller : ${roles.join(', ') || '(belirteçte rol yok)'}`);
18
- out.info(` Çalışma alanı : ${settings.workspaceId || app.tenantId || '(belirtilmedi)'}`);
19
- out.info(` Belirteç : geçerli${tok.expiresInSec > 0 ? ` (${Math.round(tok.expiresInSec / 60)} dk)` : ' (süre bilgisi yok)'}`);
13
+ out.info('Schema upgrades (API client)');
14
+ out.info(` App : ${settings.appUrl}`);
15
+ out.info(` Client : ${settings.clientId}`);
16
+ out.info(` Identity provider : ${new URL(app.oidcAuthority).host}`);
17
+ out.info(` Roles : ${roles.join(', ') || '(no roles in the token)'}`);
18
+ out.info(` Workspace : ${settings.workspaceId || app.tenantId || '(not specified)'}`);
19
+ out.info(` Token : valid${tok.expiresInSec > 0 ? ` (${Math.round(tok.expiresInSec / 60)} min)` : ' (no expiry info)'}`);
20
20
  result.schema = { appUrl: settings.appUrl, authorityHost: new URL(app.oidcAuthority).host, clientId: settings.clientId, roles, workspaceId: settings.workspaceId || app.tenantId || null };
21
21
  }
22
22
  if (settings.consoleUrl && settings.deployKey) {
23
23
  any = true;
24
24
  const info = await new DeployClient({ consoleUrl: settings.consoleUrl, deployKey: settings.deployKey, version: ctx.version, fetchImpl: ctx.fetchImpl }).getSite();
25
- out.info('Site dağıtımı (dağıtım anahtarı)');
26
- out.info(` Konsol : ${settings.consoleUrl}`);
27
- out.info(` Anahtar : ${settings.deployKey.slice(0, 12)}… (bitiş ${info.key.expiresAt.slice(0, 10)})`);
28
- out.info(` Ortam / adres : ${info.environment.name} → ${info.url}`);
25
+ out.info('Site deployment (deploy key)');
26
+ out.info(` Console : ${settings.consoleUrl}`);
27
+ out.info(` Key : ${settings.deployKey.slice(0, 12)}… (expires ${info.key.expiresAt.slice(0, 10)})`);
28
+ out.info(` Environment / URL : ${info.environment.name} → ${info.url}`);
29
29
  result.site = { consoleUrl: settings.consoleUrl, keyPrefix: settings.deployKey.slice(0, 12), expiresAt: info.key.expiresAt, environment: info.environment.name, url: info.url };
30
30
  }
31
31
  if (!any) {
32
- out.info('Tanımlı kimlik bilgisi yok. Gerekenler:');
33
- out.info(' Şema yükseltme : ENFORMIK_APP_URL (ya da link) + ENFORMIK_CLIENT_ID + ENFORMIK_CLIENT_SECRET');
34
- out.info(' Site dağıtımı : ENFORMIK_CONSOLE_URL (ya da link) + ENFORMIK_DEPLOY_KEY');
32
+ out.info('No credentials configured. Required:');
33
+ out.info(' Schema upgrade : ENFORMIK_APP_URL (or link) + ENFORMIK_CLIENT_ID + ENFORMIK_CLIENT_SECRET');
34
+ out.info(' Site deployment: ENFORMIK_CONSOLE_URL (or link) + ENFORMIK_DEPLOY_KEY');
35
+ out.result({
36
+ error: 'no_credentials', message: 'No credentials configured.',
37
+ required: ['ENFORMIK_APP_URL + ENFORMIK_CLIENT_ID + ENFORMIK_CLIENT_SECRET', 'ENFORMIK_CONSOLE_URL + ENFORMIK_DEPLOY_KEY'],
38
+ });
35
39
  return 1;
36
40
  }
37
41
  out.result(result);
package/dist/config.js CHANGED
@@ -25,21 +25,21 @@ export function loadConfig(cwd, explicitPath) {
25
25
  raw = readFileSync(path, 'utf8');
26
26
  }
27
27
  catch {
28
- throw new CliError(`Yapılandırma okunamadı: ${path}`);
28
+ throw new CliError(`Could not read the config: ${path}`);
29
29
  }
30
30
  let parsed;
31
31
  try {
32
32
  parsed = JSON.parse(raw);
33
33
  }
34
34
  catch {
35
- throw new CliError(`Yapılandırma geçerli JSON değil: ${path}`);
35
+ throw new CliError(`The config is not valid JSON: ${path}`);
36
36
  }
37
37
  if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed))
38
- throw new CliError(`Yapılandırma bir nesne olmalı: ${path}`);
38
+ throw new CliError(`The config must be an object: ${path}`);
39
39
  // Depoya yanlışlıkla sır yazılmasını engelle.
40
40
  const s = raw.toLowerCase();
41
41
  if (s.includes('enf_dk_') || /"(client_?secret|secret|deploy_?key|password|token)"/.test(s)) {
42
- throw new CliError(`${path} sır içeriyor gibi görünüyor. Sırlar yapılandırma dosyasına değil, ortam değişkenlerine (ENFORMIK_CLIENT_SECRET, ENFORMIK_DEPLOY_KEY) yazılır; dosyadan çıkarın.`);
42
+ throw new CliError(`${path} appears to contain a secret. Secrets belong in environment variables (ENFORMIK_CLIENT_SECRET, ENFORMIK_DEPLOY_KEY), not in the config file; remove it from the file.`);
43
43
  }
44
44
  // .enformik/config.json → proje kökü = .enformik'in üst dizini
45
45
  return { config: parsed, path, root: dirname(dirname(path)) };
package/dist/context.js CHANGED
@@ -1,25 +1,40 @@
1
1
  import { createInterface } from 'node:readline/promises';
2
- export function realPrompts() {
2
+ export function realPrompts(input = process.stdin, output = process.stderr) {
3
+ /**
4
+ * One line, or null when the input ends before an answer. A closed stdin must read as "no": left pending, the question never settles
5
+ * and the process would end silently with exit code 0 although nothing was applied.
6
+ */
7
+ const readLine = async (question) => {
8
+ const ended = input;
9
+ if (ended.readableEnded || ended.destroyed) {
10
+ output.write(`${question}\n`);
11
+ return null;
12
+ }
13
+ const rl = createInterface({ input, output });
14
+ try {
15
+ const closed = new Promise((resolve) => rl.once('close', () => resolve(null)));
16
+ const line = await Promise.race([rl.question(question), closed]);
17
+ if (line === null)
18
+ output.write('\n');
19
+ return line;
20
+ }
21
+ finally {
22
+ rl.close();
23
+ }
24
+ };
3
25
  return {
4
26
  async confirm(question) {
5
- const rl = createInterface({ input: process.stdin, output: process.stderr });
6
- try {
7
- const a = (await rl.question(`${question} [e/H]: `)).trim().toLowerCase();
8
- return a === 'e' || a === 'evet' || a === 'y' || a === 'yes';
9
- }
10
- finally {
11
- rl.close();
12
- }
27
+ const a = await readLine(`${question} [y/N]: `);
28
+ if (a === null)
29
+ return false;
30
+ const t = a.trim().toLowerCase();
31
+ return t === 'y' || t === 'yes';
13
32
  },
14
33
  async ask(question, def) {
15
- const rl = createInterface({ input: process.stdin, output: process.stderr });
16
- try {
17
- const a = (await rl.question(def ? `${question} [${def}]: ` : `${question}: `)).trim();
18
- return a || def || '';
19
- }
20
- finally {
21
- rl.close();
22
- }
34
+ const a = await readLine(def ? `${question} [${def}]: ` : `${question}: `);
35
+ if (a === null)
36
+ return def ?? '';
37
+ return a.trim() || def || '';
23
38
  },
24
39
  };
25
40
  }
package/dist/errors.js CHANGED
@@ -16,11 +16,15 @@ export class ApiError extends Error {
16
16
  status;
17
17
  code;
18
18
  body;
19
- constructor(status, message, code, body) {
19
+ retryAfterSeconds;
20
+ constructor(status, message, code, body,
21
+ /** Only for 429: seconds from the Retry-After header (clamped 0..3600), when the server sent one. */
22
+ retryAfterSeconds) {
20
23
  super(message);
21
24
  this.status = status;
22
25
  this.code = code;
23
26
  this.body = body;
27
+ this.retryAfterSeconds = retryAfterSeconds;
24
28
  this.name = 'ApiError';
25
29
  }
26
30
  }
package/dist/http.js CHANGED
@@ -7,10 +7,10 @@ export function assertSecureUrl(raw, what) {
7
7
  u = new URL(raw);
8
8
  }
9
9
  catch {
10
- throw new CliError(`${what} geçerli bir adres değil: ${raw}`);
10
+ throw new CliError(`${what} is not a valid URL: ${raw}`);
11
11
  }
12
12
  if (u.protocol !== 'https:' && !(u.protocol === 'http:' && LOCAL_HOSTS.has(u.hostname))) {
13
- throw new CliError(`${what} HTTPS olmalı (kimlik bilgileri açık metin gönderilmez): ${raw}`);
13
+ throw new CliError(`${what} must use HTTPS (credentials are never sent in plain text): ${raw}`);
14
14
  }
15
15
  return u;
16
16
  }
@@ -59,15 +59,42 @@ export async function request(url, opts = {}) {
59
59
  }
60
60
  const e = lastErr;
61
61
  if (isRedirectError(e)) {
62
- throw new CliError(`Sunucu başka bir adrese yönlendirdi (${new URL(url).host}); güvenlik gereği kimlik başlığıyla yönlendirme takip edilmez. Adresi yönlendirmenin hedefiyle (ör. https:// ya da doğru alan adı) güncelleyin.`);
62
+ throw new CliError(`The server redirected to another address (${new URL(url).host}); for security, redirects are not followed when credentials are sent. Update the URL to the redirect target (e.g. https:// or the correct domain).`);
63
63
  }
64
- const reason = e?.name === 'TimeoutError' ? 'zaman aşımı' : (e?.cause?.code ?? e?.message ?? 'bilinmeyen hata');
65
- throw new CliError(`Sunucuya ulaşılamadı (${new URL(url).host}): ${reason}`);
64
+ const reason = e?.name === 'TimeoutError' ? 'timed out' : (e?.cause?.code ?? e?.message ?? 'unknown error');
65
+ throw new CliError(`Could not reach the server (${new URL(url).host}): ${reason}`);
66
+ }
67
+ /** Retry-After (delta-seconds or HTTP-date) → seconds, clamped to 0..3600; undefined when absent or unparsable. */
68
+ export function parseRetryAfter(raw, nowMs = Date.now()) {
69
+ const v = raw?.trim();
70
+ if (!v)
71
+ return undefined;
72
+ let seconds;
73
+ if (/^\d+$/.test(v))
74
+ seconds = Number(v);
75
+ else {
76
+ if (!/[A-Za-z]/.test(v))
77
+ return undefined; // an HTTP-date has letters; Date.parse would turn "1.5" or "-5" into a (past) date
78
+ const t = Date.parse(v);
79
+ if (Number.isNaN(t))
80
+ return undefined;
81
+ seconds = Math.ceil((t - nowMs) / 1000);
82
+ }
83
+ return Math.min(3600, Math.max(0, seconds));
84
+ }
85
+ export const rateLimitMessage = (seconds) => seconds === undefined ? 'Rate limited by the server; try again shortly.' : `Rate limited by the server; retry in about ${seconds} s.`;
86
+ /** HTTP 429 as an ApiError. `code` stays the server's own code (if it sent one); there is deliberately no automatic retry. */
87
+ export function rateLimitedError(res) {
88
+ const body = res.json;
89
+ const seconds = parseRetryAfter(res.headers.get('retry-after'));
90
+ return new ApiError(429, rateLimitMessage(seconds), typeof body?.error === 'string' ? body.error : undefined, res.json ?? res.text, seconds);
66
91
  }
67
92
  /** Yanıt başarısızsa ApiError fırlatır; gövdeden {error, message} ya da düz metin okur. */
68
93
  export function ensureOk(res) {
69
94
  if (res.ok)
70
95
  return res;
96
+ if (res.status === 429)
97
+ throw rateLimitedError(res);
71
98
  const body = res.json;
72
99
  const code = typeof body?.error === 'string' ? body.error : undefined;
73
100
  // Ara katmanların (CDN/WAF) HTML hata sayfaları mesaja girmez; yalnızca "HTTP <durum>" kalır.
package/dist/precheck.js CHANGED
@@ -21,13 +21,13 @@ export function fileTypeIssue(relPath) {
21
21
  const dot = base.lastIndexOf('.');
22
22
  const ext = dot > 0 ? base.slice(dot).toLowerCase() : '';
23
23
  if (EXECUTABLE_EXTS.has(ext)) {
24
- return { code: 'executable_file', message: `Çalıştırılabilir/paket dosyası (${ext}) yüklenemez. Sitenin çalışması için gerekmeyen dosyaları klasörden çıkarın.` };
24
+ return { code: 'executable_file', message: `Executable/package files (${ext}) cannot be uploaded. Remove files the site does not need from the folder.` };
25
25
  }
26
26
  if (ext === '') {
27
- return relPath.startsWith('.well-known/') ? null : { code: 'disallowed_file_type', message: 'Uzantısız dosya yüklenemez (yalnızca .well-known/ altında izinlidir).' };
27
+ return relPath.startsWith('.well-known/') ? null : { code: 'disallowed_file_type', message: 'Files without an extension cannot be uploaded (only allowed under .well-known/).' };
28
28
  }
29
29
  if (!ALLOWED_EXTS.has(ext)) {
30
- return { code: 'disallowed_file_type', message: `${ext} dosyaları yüklenemez; yalnızca sitenin çalışması için gereken türler (sayfa, betik, stil, görsel, yazı tipi, ses, video, pdf, wasm, metin/veri) izinlidir.` };
30
+ return { code: 'disallowed_file_type', message: `${ext} files cannot be uploaded; only the types a site needs (pages, scripts, styles, images, fonts, audio, video, pdf, wasm, text/data) are allowed.` };
31
31
  }
32
32
  return null;
33
33
  }
@@ -39,10 +39,10 @@ const MAX_PATH_BYTES = 255;
39
39
  // Tarayıcı paketine sızmaması gereken sırlar. Değer ASLA çıktıya yazılmaz; yalnızca dosya yolu ve tür bildirilir.
40
40
  // Bilerek dar tutulur (yanlış alarm CI'ı kırar): dağıtım anahtarı, istemci sırrı atamaları, sabit Basic kimlik başlığı.
41
41
  const SECRET_PATTERNS = [
42
- { kind: 'dağıtım anahtarı (enf_dk_…)', re: /enf_dk_[A-Za-z0-9_-]{16,}/ },
43
- { kind: 'istemci sırrı (client_secret)', re: /client_?secret["']?\s*[:=]\s*["'`][^"'`\s]{8,}["'`]/i },
44
- { kind: 'ENFORMIK_CLIENT_SECRET değeri', re: /ENFORMIK_CLIENT_SECRET["']?\s*[:=]\s*["'`][^"'`\s]{6,}["'`]/ },
45
- { kind: 'sabit Basic kimlik başlığı', re: /Authorization["']?\s*:\s*["'`]Basic\s+[A-Za-z0-9+/=]{16,}/i },
42
+ { kind: 'deploy key (enf_dk_…)', re: /enf_dk_[A-Za-z0-9_-]{16,}/ },
43
+ { kind: 'client secret (client_secret)', re: /client_?secret["']?\s*[:=]\s*["'`][^"'`\s]{8,}["'`]/i },
44
+ { kind: 'ENFORMIK_CLIENT_SECRET value', re: /ENFORMIK_CLIENT_SECRET["']?\s*[:=]\s*["'`][^"'`\s]{6,}["'`]/ },
45
+ { kind: 'hard-coded Basic auth header', re: /Authorization["']?\s*:\s*["'`]Basic\s+[A-Za-z0-9+/=]{16,}/i },
46
46
  ];
47
47
  const SCAN_EXT = new Set(['.js', '.mjs', '.cjs', '.html', '.htm', '.json', '.map', '.css', '.txt', '.xml', '.svg', '.webmanifest', '.csv', '.md']);
48
48
  const SCAN_MAX_BYTES = 5 * 1024 * 1024;
@@ -79,7 +79,7 @@ export function scanSecrets(files, ignore = []) {
79
79
  }
80
80
  const hit = SECRET_PATTERNS.find((p) => p.re.test(text));
81
81
  if (hit)
82
- out.issues.push({ code: 'secret_in_bundle', path: f.name, message: `Pakette sır var gibi görünüyor: ${hit.kind}. Tarayıcıya giden dosyalara sır konmaz; sırrı paketten çıkarıp (ve sızdıysa iptal edip) yeniden derleyin. Yanlış alarmsa dosya yolunu proje kökündeki .enformikignore dosyasına yazın.` });
82
+ out.issues.push({ code: 'secret_in_bundle', path: f.name, message: `The bundle appears to contain a secret: ${hit.kind}. Secrets must not be placed in files served to the browser; remove the secret from the bundle (and revoke it if it leaked), then rebuild. If this is a false alarm, add the file path to .enformikignore in the project root.` });
83
83
  }
84
84
  return out;
85
85
  }
@@ -99,24 +99,24 @@ export function precheckSite(dir, limits = DEFAULT_LIMITS, opts = {}) {
99
99
  const relPath = rel ? `${rel}/${ent.name}` : ent.name;
100
100
  const absPath = join(abs, ent.name);
101
101
  if (relPath.split('/')[0] === RESERVED_DIR) {
102
- add('reserved_path', relPath, `"${RESERVED_DIR}" dizini platform için ayrılmıştır; arşivden çıkarın.`);
102
+ add('reserved_path', relPath, `The "${RESERVED_DIR}" directory is reserved for the platform; remove it from the archive.`);
103
103
  continue;
104
104
  }
105
105
  if (ent.name.startsWith('.') && ent.name !== '.well-known') {
106
- add('hidden_file', relPath, `Gizli dosya/dizin yüklenemez ("${ent.name}"). .env, .git gibi dosyaları arşive koymayın.`);
106
+ add('hidden_file', relPath, `Hidden files/directories cannot be uploaded ("${ent.name}"). Do not include files such as .env or .git in the archive.`);
107
107
  continue;
108
108
  }
109
109
  if (Buffer.byteLength(relPath) > MAX_PATH_BYTES) {
110
- add('unsafe_path', relPath, `Yol ${MAX_PATH_BYTES} karakterden uzun olamaz.`);
110
+ add('unsafe_path', relPath, `A path cannot be longer than ${MAX_PATH_BYTES} bytes.`);
111
111
  continue;
112
112
  }
113
113
  if (relPath.includes('\\') || /[\u0000-\u001f\u007f]/.test(relPath)) {
114
- add('unsafe_path', relPath, 'Yolda geçersiz karakter ya da kontrol karakteri var.');
114
+ add('unsafe_path', relPath, 'The path contains an invalid or control character.');
115
115
  continue;
116
116
  }
117
117
  const st = lstatSync(absPath);
118
118
  if (st.isSymbolicLink() || (!st.isFile() && !st.isDirectory())) {
119
- add('unsupported_entry', relPath, 'Sembolik bağ veya özel dosya desteklenmez.');
119
+ add('unsupported_entry', relPath, 'Symbolic links and special files are not supported.');
120
120
  continue;
121
121
  }
122
122
  if (st.isDirectory()) {
@@ -130,7 +130,7 @@ export function precheckSite(dir, limits = DEFAULT_LIMITS, opts = {}) {
130
130
  }
131
131
  const key = relPath.toLowerCase();
132
132
  if (seen.has(key)) {
133
- add('duplicate_path', relPath, 'Aynı yol büyük/küçük harf farkıyla birden fazla var.');
133
+ add('duplicate_path', relPath, 'The same path appears more than once, differing only in letter case.');
134
134
  continue;
135
135
  }
136
136
  seen.add(key);
@@ -140,7 +140,7 @@ export function precheckSite(dir, limits = DEFAULT_LIMITS, opts = {}) {
140
140
  if (parts.length === 2 && parts[1] === 'index.html')
141
141
  nestedIndexDirs.add(parts[0]);
142
142
  if (st.size > limits.maxFileBytes) {
143
- add('file_too_large', relPath, `Tek dosya en fazla ${Math.floor(limits.maxFileBytes / 1048576)} MB olabilir.`);
143
+ add('file_too_large', relPath, `A single file can be at most ${Math.floor(limits.maxFileBytes / 1048576)} MB.`);
144
144
  continue;
145
145
  }
146
146
  files.push({ name: relPath, path: absPath, size: st.size, mtime: st.mtime });
@@ -153,22 +153,22 @@ export function precheckSite(dir, limits = DEFAULT_LIMITS, opts = {}) {
153
153
  add(s.code, s.path, s.message);
154
154
  const warnings = [];
155
155
  if (scan.ignored.length > 0)
156
- warnings.push(`.enformikignore nedeniyle sır taraması atlanan dosya: ${scan.ignored.join(', ')}`);
156
+ warnings.push(`Files skipped by the secret scan because of .enformikignore: ${scan.ignored.join(', ')}`);
157
157
  if (scan.unscanned.length > 0)
158
- warnings.push(`${SCAN_MAX_BYTES / 1048576} MB üstü olduğu için sır taraması yapılamayan dosya: ${scan.unscanned.join(', ')}`);
158
+ warnings.push(`Files not scanned for secrets because they exceed ${SCAN_MAX_BYTES / 1048576} MB: ${scan.unscanned.join(', ')}`);
159
159
  if (files.length > limits.maxFiles) {
160
- add('too_many_files', undefined, `Arşivde en fazla ${limits.maxFiles} dosya olabilir (${files.length} bulundu).`);
160
+ add('too_many_files', undefined, `The archive can contain at most ${limits.maxFiles} files (${files.length} found).`);
161
161
  }
162
162
  if (limits.maxUnpackedBytes && total > limits.maxUnpackedBytes) {
163
- add('quota_exceeded', undefined, `Sitenin açılmış boyutu ortamınızın disk kotasını aşıyor (${total} > ${limits.maxUnpackedBytes} bayt).`);
163
+ add('quota_exceeded', undefined, `The unpacked size of the site exceeds your environment's disk quota (${total} > ${limits.maxUnpackedBytes} bytes).`);
164
164
  }
165
165
  if (!hasRootIndex && issues.length < MAX_ISSUES) {
166
166
  if (nestedIndexDirs.size === 1) {
167
167
  const d = [...nestedIndexDirs][0];
168
- add('nested_root', `${d}/index.html`, `index.html bir üst klasörün ("${d}") içinde. Klasörün kendisini değil, İÇERİĞİNİ yayınlayın (--dir ${join(dir, d)}).`);
168
+ add('nested_root', `${d}/index.html`, `index.html is inside a subfolder ("${d}"). Publish the folder's CONTENTS, not the folder itself (--dir ${join(dir, d)}).`);
169
169
  }
170
170
  else {
171
- add('missing_index', 'index.html', 'Dizinin kökünde index.html bulunmalı.');
171
+ add('missing_index', 'index.html', 'index.html must be at the root of the directory.');
172
172
  }
173
173
  }
174
174
  return { files, unpackedBytes: total, issues, warnings };
package/dist/schema.js CHANGED
@@ -9,15 +9,15 @@ export function parseManifest(text, path) {
9
9
  m = JSON.parse(stripBom(text));
10
10
  }
11
11
  catch (e) {
12
- throw new CliError(`${path} geçerli JSON değil: ${e.message}`);
12
+ throw new CliError(`${path} is not valid JSON: ${e.message}`);
13
13
  }
14
14
  if (typeof m !== 'object' || m === null || Array.isArray(m))
15
- throw new CliError(`${path} bir nesne olmalı.`);
15
+ throw new CliError(`${path} must be an object.`);
16
16
  const mod = m.module;
17
17
  if (!mod || typeof mod.slug !== 'string' || !mod.slug)
18
- throw new CliError(`${path}: "module.slug" zorunlu.`);
18
+ throw new CliError(`${path}: "module.slug" is required.`);
19
19
  if (typeof mod.version !== 'string' || !mod.version)
20
- throw new CliError(`${path}: "module.version" zorunlu.`);
20
+ throw new CliError(`${path}: "module.version" is required.`);
21
21
  return m;
22
22
  }
23
23
  /**
@@ -65,19 +65,19 @@ export function typeChanges(diffs) {
65
65
  return diffs.filter((d) => d.resourceType === 'field' && d.action === 'alter');
66
66
  }
67
67
  const ACTION_LABEL = {
68
- create: '+ ekle', alter: '~ değiştir', drop: '- sil', none: '= aynı', kept_local: '= yerel kalır',
68
+ create: '+ add', alter: '~ change', drop: '- delete', none: '= same', kept_local: '= kept local',
69
69
  };
70
70
  export function renderDiff(out, result, opts = {}) {
71
71
  const lines = [];
72
72
  const pending = pendingChanges(result.diffs);
73
73
  if (pending.length > 0) {
74
- lines.push(out.paint('yellow', out.paint('bold', '⚠ DİKKAT: ŞEMA DEĞİŞİKLİĞİ UYGULAMANIN TAMAMINI ETKİLER')));
75
- lines.push(out.paint('yellow', `Bu işlem tek bir çalışma alanını değil, ${opts.appHost ? `"${opts.appHost}" ` : ''}uygulamasındaki TÜM çalışma alanlarını etkiler.`));
74
+ lines.push(out.paint('yellow', out.paint('bold', '⚠ WARNING: SCHEMA CHANGES AFFECT THE ENTIRE APP')));
75
+ lines.push(out.paint('yellow', `This affects ALL workspaces in the ${opts.appHost ? `"${opts.appHost}" ` : ''}app, not just one workspace.`));
76
76
  lines.push('');
77
77
  }
78
78
  const shown = result.diffs.filter((d) => d.action !== 'none');
79
79
  if (shown.length === 0) {
80
- lines.push('Değişiklik yok: uygulamadaki modül manifestle aynı.');
80
+ lines.push('No changes: the module in the app matches the manifest.');
81
81
  }
82
82
  else {
83
83
  const rows = shown.map((d) => {
@@ -85,17 +85,17 @@ export function renderDiff(out, result, opts = {}) {
85
85
  const colored = d.action === 'create' ? out.paint('green', label)
86
86
  : d.action === 'drop' ? out.paint('red', label)
87
87
  : d.action === 'alter' ? out.paint('yellow', label) : out.paint('dim', label);
88
- const detail = d.action === 'drop' && d.hasData ? 'veri içerdiği için ATLANACAK' : (d.details || d.warning || '');
88
+ const detail = d.action === 'drop' && d.hasData ? 'will be SKIPPED because it contains data' : (d.details || d.warning || '');
89
89
  return [d.resourceType, d.slug, colored, detail];
90
90
  });
91
- lines.push(renderTable(out, ['TÜR', 'AD', 'İŞLEM', 'AYRINTI'], rows));
91
+ lines.push(renderTable(out, ['TYPE', 'NAME', 'ACTION', 'DETAILS'], rows));
92
92
  const count = (a) => shown.filter((d) => d.action === a).length;
93
93
  lines.push('');
94
- lines.push(`Özet: ${count('create')} eklenecek, ${count('alter')} değişecek, ${count('drop')} silinecek` +
95
- (count('kept_local') ? `, ${count('kept_local')} yerel özelleştirme korunacak` : '') + '.');
94
+ lines.push(`Summary: ${count('create')} to add, ${count('alter')} to change, ${count('drop')} to delete` +
95
+ (count('kept_local') ? `, ${count('kept_local')} local customizations kept` : '') + '.');
96
96
  }
97
97
  for (const e of result.errors ?? [])
98
- lines.push(out.paint('red', `Hata: ${e}`));
98
+ lines.push(out.paint('red', `Error: ${e}`));
99
99
  return lines.join('\n');
100
100
  }
101
101
  export class SchemaClient {
@@ -125,7 +125,7 @@ export class SchemaClient {
125
125
  const j = (res.json ?? {});
126
126
  // Beklenmeyen yanıt biçimi "/api/modules/undefined/diff" çağrısına dönüşmesin.
127
127
  if (typeof j.id !== 'string' || !j.id)
128
- throw new CliError(`Sunucu yanıtı beklenen biçimde değil: "${slug}" modülünün kimliği yok.`);
128
+ throw new CliError(`The server response is not in the expected format: module "${slug}" has no ID.`);
129
129
  return { id: j.id, name: j.name ?? '', version: j.version ?? '' };
130
130
  }
131
131
  /** slug → kurulu modül ya da null (404 module_not_found); diğer hatalar fırlatılır. */
@@ -177,7 +177,7 @@ export class SchemaClient {
177
177
  method: 'POST', headers: this.headers({ 'Content-Type': 'application/json' }), body: manifestJson, retries: 1, timeoutMs: 120_000, fetchImpl: this.o.fetchImpl,
178
178
  });
179
179
  if (res.status === 404 || res.status === 405) {
180
- throw new CliError('Bu uygulama sürümü sunucu tarafı doğrulamayı (/api/modules/validate) desteklemiyor. Uygulama güncellenene kadar `schema validate --offline` ile yalnızca yerel denetim yapın.', 1, 'validate_unsupported');
180
+ throw new CliError('This app version does not support server-side validation (/api/modules/validate). Until the app is updated, run local-only checks with `schema validate --offline`.', 1, 'validate_unsupported');
181
181
  }
182
182
  try {
183
183
  ensureOk(res);
@@ -225,31 +225,33 @@ function normalizeDiff(j) {
225
225
  export function describeSchemaError(e, ctx) {
226
226
  if (!(e instanceof ApiError))
227
227
  return e;
228
+ if (e.status === 429)
229
+ return e; // the rate-limit message is already final (see rateLimitedError); cli.ts prints it
228
230
  const body = (e.body ?? {});
229
231
  switch (e.code) {
230
232
  case 'manifest_requires_admin':
231
- return new CliError(`Bu manifest, istemcinizin rolünü aşan bölümler içeriyor: ${(body.sections ?? []).join(', ')}.\n` +
232
- 'Bu bölümleri (betik, otomasyon, yetki tanımı vb.) bir yönetici uygulamalıdır; bunları manifestten çıkarın ya da yönetici rolündeki bir istemciyle çalıştırın.', 1, e.code, e.body);
233
+ return new CliError(`This manifest contains sections beyond your client's role: ${(body.sections ?? []).join(', ')}.\n` +
234
+ 'These sections (scripts, automations, permission definitions, etc.) must be applied by an administrator; remove them from the manifest or run with a client that has the administrator role.', 1, e.code, e.body);
233
235
  case 'type_change_requires_admin':
234
- return new CliError(`Alan türü değişikliği veriyi dönüştürebilir ve yalnızca yönetici tarafından uygulanır: ${(body.fields ?? []).join(', ')}.`, 1, e.code, e.body);
236
+ return new CliError(`Changing a field type can transform data and is applied only by an administrator: ${(body.fields ?? []).join(', ')}.`, 1, e.code, e.body);
235
237
  case 'breaking_change_blocked':
236
238
  return new CliError(`${e.message}`, 1, e.code, e.body);
237
239
  case 'module_not_found':
238
- return new CliError(`"${ctx.slug ?? 'modül'}" slug'lı bir modül bu uygulamada kurulu değil. İlk kurulumu \`enformik schema install\` ile ("Şema oluşturucu" rolündeki bir istemciyle) ya da bir yönetici yapar; sonraki yükseltmeleri \`schema push\` yapar.`, 1, e.code);
240
+ return new CliError(`No module with slug "${ctx.slug ?? 'module'}" is installed in this app. The first installation is done with \`enformik schema install\` (using a client with the "Schema builder" role) or by an administrator; later upgrades are done with \`schema push\`.`, 1, e.code);
239
241
  case 'module_exists':
240
- return new CliError('Bu slug ile bir modül zaten kurulu. Var olanı yükseltmek için `enformik schema push` kullanın.', 1, e.code);
242
+ return new CliError('A module with this slug is already installed. Use `enformik schema push` to upgrade the existing one.', 1, e.code);
241
243
  case 'payment_required':
242
- return new CliError('Uygulamanın planı/ödeme durumu nedeniyle işlem engellendi (402). Konsoldan abonelik durumunu kontrol edin.', 1, e.code);
244
+ return new CliError('The operation was blocked because of the app\'s plan/payment status (402). Check the subscription status in the console.', 1, e.code);
243
245
  }
244
246
  if (e.status === 401)
245
- return new CliError('Kimlik reddedildi (401): belirteç geçersiz ya da istemci iptal edilmiş.', 1, 'unauthorized');
247
+ return new CliError('Authentication rejected (401): the token is invalid or the client has been revoked.', 1, 'unauthorized');
246
248
  if (e.status === 403) {
247
249
  return new CliError((ctx.install
248
- ? 'Bu istemcinin rolü yeni modül kurmaya yetmiyor (403). Konsolda rolü "Şema oluşturucu" ya da "Yönetici" olan bir API istemcisi kullanın.'
249
- : 'Bu istemcinin rolü modül yükseltmeye yetmiyor (403). Konsolda rolü "Şema yükseltici", "Şema oluşturucu" ya da "Yönetici" olan bir API istemcisi kullanın.') +
250
- (body.required ? ` (gereken yetki: ${body.required})` : ''), 1, 'forbidden');
250
+ ? 'This client\'s role is not sufficient to install a new module (403). Use an API client whose role in the console is "Schema builder" or "Administrator".'
251
+ : 'This client\'s role is not sufficient to upgrade a module (403). Use an API client whose role in the console is "Schema upgrader", "Schema builder" or "Administrator".') +
252
+ (body.required ? ` (required permission: ${body.required})` : ''), 1, 'forbidden');
251
253
  }
252
254
  if (e.status === 404)
253
- return new CliError(`Kayıt bulunamadı (404): ${e.message}`, 1, 'not_found');
254
- return new CliError(`Sunucu hatası (HTTP ${e.status}): ${e.message}`, 1, e.code);
255
+ return new CliError(`Not found (404): ${e.message}`, 1, 'not_found');
256
+ return new CliError(`Server error (HTTP ${e.status}): ${e.message}`, 1, e.code);
255
257
  }