@enformik/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/dist/schema.js CHANGED
@@ -1,21 +1,23 @@
1
1
  import { ApiError, CliError } from './errors.js';
2
2
  import { ensureOk, joinUrl, request } from './http.js';
3
3
  import { renderTable } from './output.js';
4
+ /** UTF-8 BOM (Windows editörleri) JSON.parse'ı bozar; sunucuya da gitmemeli. */
5
+ export const stripBom = (text) => text.replace(/^\uFEFF/, '');
4
6
  export function parseManifest(text, path) {
5
7
  let m;
6
8
  try {
7
- m = JSON.parse(text);
9
+ m = JSON.parse(stripBom(text));
8
10
  }
9
11
  catch (e) {
10
- throw new CliError(`${path} geçerli JSON değil: ${e.message}`);
12
+ throw new CliError(`${path} is not valid JSON: ${e.message}`);
11
13
  }
12
14
  if (typeof m !== 'object' || m === null || Array.isArray(m))
13
- throw new CliError(`${path} bir nesne olmalı.`);
15
+ throw new CliError(`${path} must be an object.`);
14
16
  const mod = m.module;
15
17
  if (!mod || typeof mod.slug !== 'string' || !mod.slug)
16
- throw new CliError(`${path}: "module.slug" zorunlu.`);
18
+ throw new CliError(`${path}: "module.slug" is required.`);
17
19
  if (typeof mod.version !== 'string' || !mod.version)
18
- throw new CliError(`${path}: "module.version" zorunlu.`);
20
+ throw new CliError(`${path}: "module.version" is required.`);
19
21
  return m;
20
22
  }
21
23
  /**
@@ -45,28 +47,37 @@ export function adminOnlySections(m, opts) {
45
47
  out.push('scripts.runAs=definer');
46
48
  return out;
47
49
  }
48
- const PENDING = new Set(['create', 'alter', 'drop']);
49
- /** Uygulanacak (kalıcı) değişiklikler: none/kept_local sayılmaz; veri içeren alanın silinmesi sunucuda atlanır. */
50
+ const NOT_PENDING = new Set(['none', 'kept_local']);
51
+ /**
52
+ * Uygulanacak (kalıcı) değişiklikler: yalnızca none/kept_local sayılmaz; veri içeren alanın silinmesi sunucuda atlanır.
53
+ * Bilinmeyen eylem (sunucu yeni bir tür eklerse, ör. rename) "değişiklik yok" sayılmaz — `diff --check` sessizce 0 vermesin.
54
+ */
50
55
  export function pendingChanges(diffs) {
51
- return diffs.filter((d) => PENDING.has(d.action) && !(d.action === 'drop' && d.hasData));
56
+ return diffs.filter((d) => !NOT_PENDING.has(d.action) && !(d.action === 'drop' && d.hasData));
57
+ }
58
+ const diffKey = (d) => `${d.resourceType}:${d.slug}:${d.action}`;
59
+ /** Uygulanan değişikliklerden, onaylanan (kuru çalıştırma) listede olmayanlar: diff ile upgrade arasında uygulama değişmiş. */
60
+ export function unapprovedChanges(approved, applied) {
61
+ const ok = new Set(pendingChanges(approved).map(diffKey));
62
+ return pendingChanges(applied).filter((d) => !ok.has(diffKey(d)));
52
63
  }
53
64
  export function typeChanges(diffs) {
54
65
  return diffs.filter((d) => d.resourceType === 'field' && d.action === 'alter');
55
66
  }
56
67
  const ACTION_LABEL = {
57
- 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',
58
69
  };
59
70
  export function renderDiff(out, result, opts = {}) {
60
71
  const lines = [];
61
72
  const pending = pendingChanges(result.diffs);
62
73
  if (pending.length > 0) {
63
- lines.push(out.paint('yellow', out.paint('bold', '⚠ DİKKAT: ŞEMA DEĞİŞİKLİĞİ UYGULAMANIN TAMAMINI ETKİLER')));
64
- 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.`));
65
76
  lines.push('');
66
77
  }
67
78
  const shown = result.diffs.filter((d) => d.action !== 'none');
68
79
  if (shown.length === 0) {
69
- lines.push('Değişiklik yok: uygulamadaki modül manifestle aynı.');
80
+ lines.push('No changes: the module in the app matches the manifest.');
70
81
  }
71
82
  else {
72
83
  const rows = shown.map((d) => {
@@ -74,17 +85,17 @@ export function renderDiff(out, result, opts = {}) {
74
85
  const colored = d.action === 'create' ? out.paint('green', label)
75
86
  : d.action === 'drop' ? out.paint('red', label)
76
87
  : d.action === 'alter' ? out.paint('yellow', label) : out.paint('dim', label);
77
- 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 || '');
78
89
  return [d.resourceType, d.slug, colored, detail];
79
90
  });
80
- lines.push(renderTable(out, ['TÜR', 'AD', 'İŞLEM', 'AYRINTI'], rows));
91
+ lines.push(renderTable(out, ['TYPE', 'NAME', 'ACTION', 'DETAILS'], rows));
81
92
  const count = (a) => shown.filter((d) => d.action === a).length;
82
93
  lines.push('');
83
- lines.push(`Özet: ${count('create')} eklenecek, ${count('alter')} değişecek, ${count('drop')} silinecek` +
84
- (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` : '') + '.');
85
96
  }
86
97
  for (const e of result.errors ?? [])
87
- lines.push(out.paint('red', `Hata: ${e}`));
98
+ lines.push(out.paint('red', `Error: ${e}`));
88
99
  return lines.join('\n');
89
100
  }
90
101
  export class SchemaClient {
@@ -111,8 +122,11 @@ export class SchemaClient {
111
122
  catch (e) {
112
123
  throw describeSchemaError(e, { slug });
113
124
  }
114
- const j = res.json;
115
- return { id: j.id, name: j.name, version: j.version };
125
+ const j = (res.json ?? {});
126
+ // Beklenmeyen yanıt biçimi "/api/modules/undefined/diff" çağrısına dönüşmesin.
127
+ if (typeof j.id !== 'string' || !j.id)
128
+ throw new CliError(`The server response is not in the expected format: module "${slug}" has no ID.`);
129
+ return { id: j.id, name: j.name ?? '', version: j.version ?? '' };
116
130
  }
117
131
  /** slug → kurulu modül ya da null (404 module_not_found); diğer hatalar fırlatılır. */
118
132
  async findModule(slug) {
@@ -163,7 +177,7 @@ export class SchemaClient {
163
177
  method: 'POST', headers: this.headers({ 'Content-Type': 'application/json' }), body: manifestJson, retries: 1, timeoutMs: 120_000, fetchImpl: this.o.fetchImpl,
164
178
  });
165
179
  if (res.status === 404 || res.status === 405) {
166
- 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');
167
181
  }
168
182
  try {
169
183
  ensureOk(res);
@@ -178,7 +192,7 @@ export class SchemaClient {
178
192
  };
179
193
  }
180
194
  async diff(moduleId, manifestJson, opts) {
181
- const res = await request(joinUrl(this.o.appUrl, `/api/modules/${moduleId}/diff${opts.seedData ? '?seed_data=true' : ''}`), {
195
+ const res = await request(joinUrl(this.o.appUrl, `/api/modules/${encodeURIComponent(moduleId)}/diff${opts.seedData ? '?seed_data=true' : ''}`), {
182
196
  method: 'POST', headers: this.headers({ 'Content-Type': 'application/json' }), body: manifestJson, retries: 1, timeoutMs: 120_000, fetchImpl: this.o.fetchImpl,
183
197
  });
184
198
  try {
@@ -191,7 +205,7 @@ export class SchemaClient {
191
205
  }
192
206
  async upgrade(moduleId, manifestJson, opts) {
193
207
  // retries: 0 — uygulama tekrar edilmemeli (ağ hatasında durumu `diff` ile kontrol edin).
194
- const res = await request(joinUrl(this.o.appUrl, `/api/modules/${moduleId}/upgrade${opts.seedData ? '?seed_data=true' : ''}`), {
208
+ const res = await request(joinUrl(this.o.appUrl, `/api/modules/${encodeURIComponent(moduleId)}/upgrade${opts.seedData ? '?seed_data=true' : ''}`), {
195
209
  method: 'POST', headers: this.headers({ 'Content-Type': 'application/json' }), body: manifestJson, timeoutMs: 300_000, fetchImpl: this.o.fetchImpl,
196
210
  });
197
211
  try {
@@ -211,31 +225,33 @@ function normalizeDiff(j) {
211
225
  export function describeSchemaError(e, ctx) {
212
226
  if (!(e instanceof ApiError))
213
227
  return e;
228
+ if (e.status === 429)
229
+ return e; // the rate-limit message is already final (see rateLimitedError); cli.ts prints it
214
230
  const body = (e.body ?? {});
215
231
  switch (e.code) {
216
232
  case 'manifest_requires_admin':
217
- return new CliError(`Bu manifest, istemcinizin rolünü aşan bölümler içeriyor: ${(body.sections ?? []).join(', ')}.\n` +
218
- '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);
219
235
  case 'type_change_requires_admin':
220
- 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);
221
237
  case 'breaking_change_blocked':
222
238
  return new CliError(`${e.message}`, 1, e.code, e.body);
223
239
  case 'module_not_found':
224
- 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);
225
241
  case 'module_exists':
226
- 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);
227
243
  case 'payment_required':
228
- 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);
229
245
  }
230
246
  if (e.status === 401)
231
- 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');
232
248
  if (e.status === 403) {
233
249
  return new CliError((ctx.install
234
- ? 'Bu istemcinin rolü yeni modül kurmaya yetmiyor (403). Konsolda rolü "Şema oluşturucu" ya da "Yönetici" olan bir API istemcisi kullanın.'
235
- : '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.') +
236
- (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');
237
253
  }
238
254
  if (e.status === 404)
239
- return new CliError(`Kayıt bulunamadı (404): ${e.message}`, 1, 'not_found');
240
- 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);
241
257
  }
package/dist/site.js CHANGED
@@ -2,7 +2,7 @@ import { execFileSync } from 'node:child_process';
2
2
  import { createHash, randomUUID } from 'node:crypto';
3
3
  import { readFileSync } from 'node:fs';
4
4
  import { ApiError, CliError } from './errors.js';
5
- import { ensureOk, joinUrl, request } from './http.js';
5
+ import { assertSecureUrl, ensureOk, joinUrl, request } from './http.js';
6
6
  // Web sitesi dağıtımı — makine uçları (/api/deploy/v1) istemcisi. Akış: init → PUT content → finalize → poll.
7
7
  // Sözleşme: docs/plans/DEVELOPER_PLATFORM_DEPLOY_DESIGN.md §3.4, §4.
8
8
  export const MAX_ARCHIVE_BYTES = 50 * 1024 * 1024;
@@ -10,6 +10,8 @@ export class DeployClient {
10
10
  o;
11
11
  constructor(o) {
12
12
  this.o = o;
13
+ // Her kullanım yolunda (deploy, status, whoami, app check) dağıtım anahtarı düz metin gitmesin.
14
+ assertSecureUrl(o.consoleUrl, 'Console URL');
13
15
  }
14
16
  url(path) { return joinUrl(this.o.consoleUrl, `/api/deploy/v1${path}`); }
15
17
  headers(extra = {}) {
@@ -44,6 +46,33 @@ export class DeployClient {
44
46
  return (await this.call(`/deployments/${id}`, { retries: 2 })).deployment;
45
47
  }
46
48
  }
49
+ /**
50
+ * Finalizes a deployment. A `409 invalid_state` can mean the first finalize worked and only its reply was lost (the request was retried), or
51
+ * another run of the same CI job already finalized it, so the deployment's real state decides: running or finished -> carry on with that state;
52
+ * still waiting for its upload / finalize -> the error stands.
53
+ */
54
+ export async function finalizeDeployment(client, dep) {
55
+ try {
56
+ await client.finalize(dep.deploymentId);
57
+ return { ...dep, status: 'validating' };
58
+ }
59
+ catch (e) {
60
+ if (!(e instanceof CliError) || e.code !== 'invalid_state')
61
+ throw e;
62
+ let cur;
63
+ try {
64
+ cur = await client.status(dep.deploymentId);
65
+ }
66
+ catch {
67
+ throw e;
68
+ } // the state could not be read: the original (actionable) error is the better message
69
+ if (cur.status === 'awaiting_upload' || cur.status === 'uploaded')
70
+ throw e;
71
+ return { ...dep, ...cur };
72
+ }
73
+ }
74
+ /** Üst üste bu kadar yoklama hatasından sonra bırakılır. */
75
+ const MAX_POLL_FAILURES = 5;
47
76
  const TERMINAL = new Set(['live', 'failed', 'expired', 'superseded']);
48
77
  export const isTerminal = (s) => TERMINAL.has(s);
49
78
  /** Durum değiştikçe onStatus çağırarak terminal duruma kadar bekler. */
@@ -53,8 +82,25 @@ export async function waitForDeployment(client, id, opts) {
53
82
  const start = now();
54
83
  let last = '';
55
84
  let interval = opts.intervalMs ?? 2000;
85
+ let failures = 0;
56
86
  for (;;) {
57
- const v = await client.status(id);
87
+ let v;
88
+ try {
89
+ v = await client.status(id);
90
+ failures = 0;
91
+ }
92
+ catch (e) {
93
+ // Geçici hata (ağ, 5xx): sunucu dağıtımı yayınlıyor olabilir, tek bir yoklama hatası CI'ı kırmasın.
94
+ // Kalıcı hatalar (geçersiz anahtar, 404…) kodla gelir ve hemen iletilir.
95
+ // Bir 429 (kodsuz) da geçicidir: sunucu dağıtımı sürdürüyor, yoklamayı kısıtlama yüzünden bırakmayız.
96
+ const transient = (e instanceof CliError && (!e.code || e.code === 'deploy_unavailable')) || (e instanceof ApiError && e.status === 429 && !e.code);
97
+ if (!transient || ++failures >= MAX_POLL_FAILURES)
98
+ throw e;
99
+ if (now() - start > opts.timeoutMs)
100
+ throw e;
101
+ await sleep(interval);
102
+ continue;
103
+ }
58
104
  if (v.status !== last) {
59
105
  last = v.status;
60
106
  opts.onStatus?.(v.status);
@@ -62,7 +108,7 @@ export async function waitForDeployment(client, id, opts) {
62
108
  if (isTerminal(v.status))
63
109
  return v;
64
110
  if (now() - start > opts.timeoutMs)
65
- throw new CliError(`Zaman aşımı: dağıtım ${Math.round(opts.timeoutMs / 1000)} sn içinde tamamlanmadı (son durum: ${v.status}). Konsolun "Sürümler" sekmesinden izleyebilirsiniz.`);
111
+ throw new CliError(`Timed out: the deployment did not finish within ${Math.round(opts.timeoutMs / 1000)} s (last status: ${v.status}). You can follow it on the console's "Releases" tab.`);
66
112
  await sleep(interval);
67
113
  interval = Math.min(interval + 500, 5000);
68
114
  }
@@ -94,36 +140,45 @@ function gitCmd(cwd, args) {
94
140
  }
95
141
  }
96
142
  /**
97
- * Idempotency-Key: CI'da koşu + deneme numarası (yeniden çalıştırma yeni anahtar alır, yoksa eski — belki başarısız —
98
- * dağıtım "tekrar oynatılırdı"); yerelde her çalıştırma için rastgele.
143
+ * Idempotency-Key: CI'da koşu + deneme numarası + arşiv sağlama toplamının başı (yeniden çalıştırma yeni anahtar alır,
144
+ * yoksa eski — belki başarısız — dağıtım "tekrar oynatılırdı"; aynı işte farklı içerikli iki dağıtım da aynı anahtarı
145
+ * paylaşmaz); yerelde her çalıştırma için rastgele.
99
146
  */
100
- export function idempotencyKey(env, rand = randomUUID) {
147
+ export function idempotencyKey(env, rand = randomUUID, contentSha) {
148
+ const tail = contentSha ? `-${contentSha.slice(0, 12)}` : '';
149
+ const fit = (base) => `${base.slice(0, 64 - tail.length)}${tail}`;
101
150
  if (env.GITHUB_RUN_ID)
102
- return `gh-${env.GITHUB_RUN_ID}-${env.GITHUB_RUN_ATTEMPT ?? '1'}`.slice(0, 64);
151
+ return fit(`gh-${env.GITHUB_RUN_ID}-${env.GITHUB_RUN_ATTEMPT ?? '1'}`);
103
152
  if (env.CI_PIPELINE_ID)
104
- return `gl-${env.CI_PIPELINE_ID}-${env.CI_JOB_ID ?? '0'}`.slice(0, 64);
153
+ return fit(`gl-${env.CI_PIPELINE_ID}-${env.CI_JOB_ID ?? '0'}`);
105
154
  return `cli-${rand()}`.slice(0, 64);
106
155
  }
107
156
  const HINTS = {
108
- invalid_key: 'Dağıtım anahtarı geçersiz, iptal edilmiş ya da bu siteye ait değil. ENFORMIK_DEPLOY_KEY değerini kontrol edin.',
109
- key_expired: 'Dağıtım anahtarının süresi dolmuş. Konsolda yeni bir anahtar oluşturup CI gizli değişkenini güncelleyin.',
110
- rate_limited: 'Çok fazla geçersiz anahtar denemesi; biraz bekleyip yeniden deneyin.',
111
- developer_site_disabled: 'Bu ortamda geliştirici sitesi etkin değil. Konsolda "Geliştirici sitesi" penceresinden etkinleştirin.',
112
- environment_inactive: 'Web sitesi ortamı şu anda kullanılamıyor (ör. abonelik askıda).',
113
- deploy_in_progress: 'Bu ortamda devam eden bir dağıtım var; bitmesini bekleyip yeniden deneyin.',
114
- deploy_unavailable: 'Dağıtım altyapısı şu anda hazır değil; biraz sonra yeniden deneyin.',
115
- deployment_expired: 'Yükleme süresi dolmuş; komutu yeniden çalıştırın.',
116
- quota_exceeded: 'Arşiv izin verilen boyutu aşıyor (en çok 50 MB).',
117
- size_mismatch: 'Yüklenen içerik bildirilen boyutla uyuşmuyor; komutu yeniden çalıştırın.',
118
- checksum_mismatch: 'Yüklenen içeriğin sağlama toplamı uyuşmuyor (yükleme bozulmuş olabilir); komutu yeniden çalıştırın.',
157
+ invalid_key: 'The deploy key is invalid, revoked, or does not belong to this site. Check the ENFORMIK_DEPLOY_KEY value.',
158
+ key_expired: 'The deploy key has expired. Create a new key in the console and update the CI secret variable.',
159
+ rate_limited: 'Too many invalid key attempts; wait a moment and try again.',
160
+ developer_site_disabled: 'The developer site is not enabled in this environment. Enable it from the "Developer site" dialog in the console.',
161
+ environment_inactive: 'The website environment is currently unavailable (e.g. the subscription is suspended).',
162
+ deploy_in_progress: 'A deployment is already in progress in this environment; wait for it to finish and try again.',
163
+ deploy_unavailable: 'The deployment infrastructure is not ready right now; try again shortly.',
164
+ deployment_expired: 'The upload window has expired; run the command again.',
165
+ invalid_state: 'The deployment is not in a state that allows this step (for example it was already finalized or its upload is incomplete); run the command again.',
166
+ quota_exceeded: 'The archive exceeds the allowed size (50 MB max).',
167
+ size_mismatch: 'The uploaded content does not match the declared size; run the command again.',
168
+ checksum_mismatch: 'The checksum of the uploaded content does not match (the upload may be corrupted); run the command again.',
119
169
  };
120
170
  export function describeDeployError(e) {
121
171
  if (!(e instanceof ApiError))
122
172
  return e;
173
+ // Any 429 except one with a known hint (the deploy-key limiter) keeps its rate-limit message and JSON shape (status, retryAfterSeconds).
174
+ if (e.status === 429 && !(e.code && HINTS[e.code]))
175
+ return e;
123
176
  const hint = e.code ? HINTS[e.code] : undefined;
124
177
  const detail = e.body?.details;
125
178
  let msg = hint ?? e.message ?? `HTTP ${e.status}`;
126
179
  if (e.code === 'artifact_invalid' && Array.isArray(detail))
127
- msg = `Arşiv geçerli değil: ${detail.map((d) => d.message).join('; ')}`;
180
+ msg = `Invalid archive: ${detail.map((d) => d.message).join('; ')}`;
181
+ if (e.status === 429 && e.retryAfterSeconds !== undefined)
182
+ msg += ` (retry in about ${e.retryAfterSeconds} s)`;
128
183
  return new CliError(msg, 1, e.code, e.body);
129
184
  }
package/dist/zip.js CHANGED
@@ -17,7 +17,7 @@ export function crc32(buf) {
17
17
  return (c ^ 0xffffffff) >>> 0;
18
18
  }
19
19
  function dosDateTime(d) {
20
- const y = Math.max(1980, d.getFullYear());
20
+ const y = Math.min(2107, Math.max(1980, d.getFullYear())); // DOS tarihi yıl alanı 7 bit (1980–2107)
21
21
  return {
22
22
  time: (d.getHours() << 11) | (d.getMinutes() << 5) | Math.floor(d.getSeconds() / 2),
23
23
  date: ((y - 1980) << 9) | ((d.getMonth() + 1) << 5) | d.getDate(),
@@ -28,17 +28,23 @@ const MAX_U32 = 0xffffffff;
28
28
  /** Girdileri sıralı (deterministik) olarak outPath'e zipler. */
29
29
  export function writeZipFile(files, outPath) {
30
30
  if (files.length > MAX_ENTRIES)
31
- throw new Error(`Zip en fazla ${MAX_ENTRIES} girdi içerebilir.`);
31
+ throw new Error(`A zip can contain at most ${MAX_ENTRIES} entries.`);
32
32
  const sorted = [...files].sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
33
33
  const fd = openSync(outPath, 'w');
34
34
  let offset = 0;
35
35
  const central = [];
36
- const write = (b) => { writeSync(fd, b); offset += b.length; };
36
+ // writeSync kısmi yazabilir (disk dolu, sinyal); kesik zip geçerli sağlama toplamıyla yüklenmesin diye tamamlanana dek döner.
37
+ const write = (b) => {
38
+ let done = 0;
39
+ while (done < b.length)
40
+ done += writeSync(fd, b, done, b.length - done);
41
+ offset += b.length;
42
+ };
37
43
  try {
38
44
  for (const f of sorted) {
39
45
  const data = readFileSync(f.path);
40
46
  if (data.length >= MAX_U32)
41
- throw new Error(`Dosya çok büyük (zip64 desteklenmiyor): ${f.name}`);
47
+ throw new Error(`File too large (zip64 is not supported): ${f.name}`);
42
48
  const name = Buffer.from(f.name, 'utf8');
43
49
  const deflated = deflateRawSync(data, { level: 6 });
44
50
  const useDeflate = deflated.length < data.length;
@@ -90,7 +96,7 @@ export function writeZipFile(files, outPath) {
90
96
  end.writeUInt32LE(cdStart, 16);
91
97
  write(end);
92
98
  if (offset >= MAX_U32)
93
- throw new Error('Zip 4 GB sınırını aşıyor.');
99
+ throw new Error('The zip exceeds the 4 GB limit.');
94
100
  return { entries: sorted.length, bytes: offset };
95
101
  }
96
102
  finally {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@enformik/cli",
3
- "version": "0.4.0",
4
- "description": "Enformik geliştirici CLI'si: web sitesi dağıtımı ve uygulama modülü (şema) yükseltme.",
3
+ "version": "0.6.0",
4
+ "description": "Enformik developer CLI: website deployment and application module (schema) upgrades.",
5
5
  "license": "MIT",
6
6
  "type": "module",
7
7
  "bin": {