@enformik/cli 0.0.0-stage → 0.1.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.
@@ -0,0 +1,25 @@
1
+ import { createInterface } from 'node:readline/promises';
2
+ export function realPrompts() {
3
+ return {
4
+ 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
+ }
13
+ },
14
+ 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
+ }
23
+ },
24
+ };
25
+ }
package/dist/errors.js ADDED
@@ -0,0 +1,26 @@
1
+ // CLI'nin kendi hata türü: mesaj kullanıcıya gösterilir, exitCode çıkış kodudur (0 başarı, 1 hata, 2 --check'te değişiklik var).
2
+ export class CliError extends Error {
3
+ exitCode;
4
+ code;
5
+ details;
6
+ constructor(message, exitCode = 1, code, details) {
7
+ super(message);
8
+ this.exitCode = exitCode;
9
+ this.code = code;
10
+ this.details = details;
11
+ this.name = 'CliError';
12
+ }
13
+ }
14
+ /** Sunucu yanıtından gelen hata (HTTP durumu + makine-okunur kod). */
15
+ export class ApiError extends Error {
16
+ status;
17
+ code;
18
+ body;
19
+ constructor(status, message, code, body) {
20
+ super(message);
21
+ this.status = status;
22
+ this.code = code;
23
+ this.body = body;
24
+ this.name = 'ApiError';
25
+ }
26
+ }
package/dist/http.js ADDED
@@ -0,0 +1,68 @@
1
+ import { ApiError, CliError } from './errors.js';
2
+ const LOCAL_HOSTS = new Set(['localhost', '127.0.0.1', '[::1]', '::1']);
3
+ /** Sır taşıyan istekler yalnızca HTTPS ile gider (yerel geliştirme hariç). */
4
+ export function assertSecureUrl(raw, what) {
5
+ let u;
6
+ try {
7
+ u = new URL(raw);
8
+ }
9
+ catch {
10
+ throw new CliError(`${what} geçerli bir adres değil: ${raw}`);
11
+ }
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}`);
14
+ }
15
+ return u;
16
+ }
17
+ export function joinUrl(base, path) {
18
+ return base.replace(/\/+$/, '') + (path.startsWith('/') ? path : '/' + path);
19
+ }
20
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
21
+ export async function request(url, opts = {}) {
22
+ const doFetch = opts.fetchImpl ?? fetch;
23
+ const attempts = (opts.retries ?? 0) + 1;
24
+ let lastErr;
25
+ for (let i = 0; i < attempts; i++) {
26
+ try {
27
+ const res = await doFetch(url, {
28
+ method: opts.method ?? 'GET',
29
+ headers: opts.headers,
30
+ body: opts.body,
31
+ signal: AbortSignal.timeout(opts.timeoutMs ?? 30_000),
32
+ redirect: 'error', // kimlik başlığıyla yönlendirme takip edilmez
33
+ });
34
+ const text = await res.text();
35
+ if ([502, 503, 504].includes(res.status) && i < attempts - 1) {
36
+ await sleep(400 * 2 ** i);
37
+ continue;
38
+ }
39
+ let json;
40
+ try {
41
+ json = text ? JSON.parse(text) : undefined;
42
+ }
43
+ catch {
44
+ json = undefined;
45
+ }
46
+ return { status: res.status, ok: res.ok, headers: res.headers, text, json };
47
+ }
48
+ catch (err) {
49
+ lastErr = err;
50
+ if (i < attempts - 1) {
51
+ await sleep(400 * 2 ** i);
52
+ continue;
53
+ }
54
+ }
55
+ }
56
+ const e = lastErr;
57
+ const reason = e?.name === 'TimeoutError' ? 'zaman aşımı' : (e?.cause?.code ?? e?.message ?? 'bilinmeyen hata');
58
+ throw new CliError(`Sunucuya ulaşılamadı (${new URL(url).host}): ${reason}`);
59
+ }
60
+ /** Yanıt başarısızsa ApiError fırlatır; gövdeden {error, message} ya da düz metin okur. */
61
+ export function ensureOk(res) {
62
+ if (res.ok)
63
+ return res;
64
+ const body = res.json;
65
+ const code = typeof body?.error === 'string' ? body.error : undefined;
66
+ const message = body?.message ?? (typeof body?.error === 'string' ? body.error : res.text.trim().slice(0, 300)) ?? '';
67
+ throw new ApiError(res.status, message || `HTTP ${res.status}`, code, res.json ?? res.text);
68
+ }
package/dist/output.js ADDED
@@ -0,0 +1,52 @@
1
+ // Terminal çıktısı: renk (NO_COLOR/TTY'ye saygılı), tablo, --json modu. --json iken insan okuyacağı metin stderr'e gider,
2
+ // stdout yalnızca makine-okunur JSON taşır.
3
+ const CODES = {
4
+ red: [31, 39], green: [32, 39], yellow: [33, 39], cyan: [36, 39], dim: [2, 22], bold: [1, 22],
5
+ };
6
+ export function paintWith(color, style, text) {
7
+ if (!color)
8
+ return text;
9
+ const [open, close] = CODES[style];
10
+ return `\u001b[${open}m${text}\u001b[${close}m`;
11
+ }
12
+ export function createOutput(opts = {}) {
13
+ const stdout = opts.stdout ?? process.stdout;
14
+ const stderr = opts.stderr ?? process.stderr;
15
+ const env = opts.env ?? process.env;
16
+ const json = !!opts.json;
17
+ const color = !env.NO_COLOR && env.TERM !== 'dumb' && !!stderr.isTTY && !(json && !stderr.isTTY);
18
+ const human = (s) => (json ? stderr : stdout).write(s + '\n');
19
+ return {
20
+ json,
21
+ color,
22
+ info: (m) => { human(m); },
23
+ warn: (m) => { stderr.write(paintWith(color, 'yellow', m) + '\n'); },
24
+ error: (m) => { stderr.write(paintWith(color, 'red', m) + '\n'); },
25
+ result: (obj) => { if (json)
26
+ stdout.write(JSON.stringify(obj, null, 2) + '\n'); },
27
+ paint: (style, text) => paintWith(color, style, text),
28
+ };
29
+ }
30
+ /** Sütunları hizalayan basit tablo; başlık dim. */
31
+ export function renderTable(out, headers, rows) {
32
+ const widths = headers.map((h, i) => Math.max(h.length, ...rows.map((r) => visibleLength(r[i] ?? ''))));
33
+ const line = (cells) => cells.map((c, i) => c + ' '.repeat(Math.max(0, (widths[i] ?? 0) - visibleLength(c)))).join(' ').trimEnd();
34
+ return [out.paint('dim', line(headers)), ...rows.map(line)].join('\n');
35
+ }
36
+ // eslint-disable-next-line no-control-regex
37
+ const ANSI = /\u001b\[[0-9;]*m/g;
38
+ export function visibleLength(s) {
39
+ return [...s.replace(ANSI, '')].length;
40
+ }
41
+ export function formatBytes(n) {
42
+ if (!Number.isFinite(n) || n < 0)
43
+ return '—';
44
+ const units = ['B', 'KB', 'MB', 'GB'];
45
+ let v = n;
46
+ let i = 0;
47
+ while (v >= 1024 && i < units.length - 1) {
48
+ v /= 1024;
49
+ i++;
50
+ }
51
+ return `${v.toFixed(v < 10 && i > 0 ? 1 : 0)} ${units[i]}`;
52
+ }
@@ -0,0 +1,74 @@
1
+ import { lstatSync, readdirSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ export const DEFAULT_LIMITS = { maxFiles: 10000, maxFileBytes: 100 * 1024 * 1024 };
4
+ export const RESERVED_DIR = '_enformik';
5
+ const MAX_ISSUES = 20;
6
+ export function precheckSite(dir, limits = DEFAULT_LIMITS) {
7
+ const files = [];
8
+ const issues = [];
9
+ const add = (code, path, message) => {
10
+ if (issues.length < MAX_ISSUES)
11
+ issues.push({ code, path, message });
12
+ };
13
+ const seen = new Set();
14
+ let total = 0;
15
+ let hasRootIndex = false;
16
+ const nestedIndexDirs = new Set();
17
+ const walk = (abs, rel) => {
18
+ for (const ent of readdirSync(abs, { withFileTypes: true }).sort((a, b) => (a.name < b.name ? -1 : 1))) {
19
+ const relPath = rel ? `${rel}/${ent.name}` : ent.name;
20
+ const absPath = join(abs, ent.name);
21
+ if (relPath.split('/')[0] === RESERVED_DIR) {
22
+ add('reserved_path', relPath, `"${RESERVED_DIR}" dizini platform için ayrılmıştır; arşivden çıkarın.`);
23
+ continue;
24
+ }
25
+ if (ent.name.startsWith('.') && ent.name !== '.well-known') {
26
+ add('hidden_file', relPath, `Gizli dosya/dizin yüklenemez ("${ent.name}"). .env, .git gibi dosyaları arşive koymayın.`);
27
+ continue;
28
+ }
29
+ const st = lstatSync(absPath);
30
+ if (st.isSymbolicLink() || (!st.isFile() && !st.isDirectory())) {
31
+ add('unsupported_entry', relPath, 'Sembolik bağ veya özel dosya desteklenmez.');
32
+ continue;
33
+ }
34
+ if (st.isDirectory()) {
35
+ walk(absPath, relPath);
36
+ continue;
37
+ }
38
+ const key = relPath.toLowerCase();
39
+ if (seen.has(key)) {
40
+ add('duplicate_path', relPath, 'Aynı yol büyük/küçük harf farkıyla birden fazla var.');
41
+ continue;
42
+ }
43
+ seen.add(key);
44
+ if (relPath === 'index.html')
45
+ hasRootIndex = true;
46
+ const parts = relPath.split('/');
47
+ if (parts.length === 2 && parts[1] === 'index.html')
48
+ nestedIndexDirs.add(parts[0]);
49
+ if (st.size > limits.maxFileBytes) {
50
+ add('file_too_large', relPath, `Tek dosya en fazla ${Math.floor(limits.maxFileBytes / 1048576)} MB olabilir.`);
51
+ continue;
52
+ }
53
+ files.push({ name: relPath, path: absPath, size: st.size, mtime: st.mtime });
54
+ total += st.size;
55
+ }
56
+ };
57
+ walk(dir, '');
58
+ if (files.length > limits.maxFiles) {
59
+ add('too_many_files', undefined, `Arşivde en fazla ${limits.maxFiles} dosya olabilir (${files.length} bulundu).`);
60
+ }
61
+ if (limits.maxUnpackedBytes && total > limits.maxUnpackedBytes) {
62
+ add('quota_exceeded', undefined, `Sitenin açılmış boyutu ortamınızın disk kotasını aşıyor (${total} > ${limits.maxUnpackedBytes} bayt).`);
63
+ }
64
+ if (!hasRootIndex && issues.length < MAX_ISSUES) {
65
+ if (nestedIndexDirs.size === 1) {
66
+ const d = [...nestedIndexDirs][0];
67
+ 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)}).`);
68
+ }
69
+ else {
70
+ add('missing_index', 'index.html', 'Dizinin kökünde index.html bulunmalı.');
71
+ }
72
+ }
73
+ return { files, unpackedBytes: total, issues };
74
+ }
package/dist/schema.js ADDED
@@ -0,0 +1,170 @@
1
+ import { ApiError, CliError } from './errors.js';
2
+ import { ensureOk, joinUrl, request } from './http.js';
3
+ import { renderTable } from './output.js';
4
+ export function parseManifest(text, path) {
5
+ let m;
6
+ try {
7
+ m = JSON.parse(text);
8
+ }
9
+ catch (e) {
10
+ throw new CliError(`${path} geçerli JSON değil: ${e.message}`);
11
+ }
12
+ if (typeof m !== 'object' || m === null || Array.isArray(m))
13
+ throw new CliError(`${path} bir nesne olmalı.`);
14
+ const mod = m.module;
15
+ if (!mod || typeof mod.slug !== 'string' || !mod.slug)
16
+ throw new CliError(`${path}: "module.slug" zorunlu.`);
17
+ if (typeof mod.version !== 'string' || !mod.version)
18
+ throw new CliError(`${path}: "module.version" zorunlu.`);
19
+ return m;
20
+ }
21
+ /**
22
+ * Yönetici dışı bir istemcinin (schema-deployer) uygulayamayacağı bölümler — sunucudaki manifestUpgradeNeeds'in aynası.
23
+ * Yalnızca ÖNCEDEN UYARI içindir; karar sunucudadır (admin istemci hepsini uygulayabilir).
24
+ */
25
+ export function adminOnlySections(m, opts) {
26
+ const out = [];
27
+ const has = (v) => Array.isArray(v) && v.length > 0;
28
+ if (has(m.scripts))
29
+ out.push('scripts');
30
+ if (has(m.automationRules))
31
+ out.push('automationRules');
32
+ if (has(m.dataSources))
33
+ out.push('dataSources');
34
+ if (has(m.interpages))
35
+ out.push('interpages');
36
+ if (opts.seedData && has(m.seedData))
37
+ out.push('seedData');
38
+ if (m.rbac && (has(m.rbac.capabilities) || has(m.rbac.roles)))
39
+ out.push('rbac');
40
+ if (has(m.uiExtensions))
41
+ out.push('uiExtensions');
42
+ return out;
43
+ }
44
+ const PENDING = new Set(['create', 'alter', 'drop']);
45
+ /** Uygulanacak (kalıcı) değişiklikler: none/kept_local sayılmaz; veri içeren alanın silinmesi sunucuda atlanır. */
46
+ export function pendingChanges(diffs) {
47
+ return diffs.filter((d) => PENDING.has(d.action) && !(d.action === 'drop' && d.hasData));
48
+ }
49
+ export function typeChanges(diffs) {
50
+ return diffs.filter((d) => d.resourceType === 'field' && d.action === 'alter');
51
+ }
52
+ const ACTION_LABEL = {
53
+ create: '+ ekle', alter: '~ değiştir', drop: '- sil', none: '= aynı', kept_local: '= yerel kalır',
54
+ };
55
+ export function renderDiff(out, result, opts = {}) {
56
+ const lines = [];
57
+ const pending = pendingChanges(result.diffs);
58
+ if (pending.length > 0) {
59
+ lines.push(out.paint('yellow', out.paint('bold', '⚠ DİKKAT: ŞEMA DEĞİŞİKLİĞİ UYGULAMANIN TAMAMINI ETKİLER')));
60
+ 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.`));
61
+ lines.push('');
62
+ }
63
+ const shown = result.diffs.filter((d) => d.action !== 'none');
64
+ if (shown.length === 0) {
65
+ lines.push('Değişiklik yok: uygulamadaki modül manifestle aynı.');
66
+ }
67
+ else {
68
+ const rows = shown.map((d) => {
69
+ const label = ACTION_LABEL[d.action] ?? d.action;
70
+ const colored = d.action === 'create' ? out.paint('green', label)
71
+ : d.action === 'drop' ? out.paint('red', label)
72
+ : d.action === 'alter' ? out.paint('yellow', label) : out.paint('dim', label);
73
+ const detail = d.action === 'drop' && d.hasData ? 'veri içerdiği için ATLANACAK' : (d.details || d.warning || '');
74
+ return [d.resourceType, d.slug, colored, detail];
75
+ });
76
+ lines.push(renderTable(out, ['TÜR', 'AD', 'İŞLEM', 'AYRINTI'], rows));
77
+ const count = (a) => shown.filter((d) => d.action === a).length;
78
+ lines.push('');
79
+ lines.push(`Özet: ${count('create')} eklenecek, ${count('alter')} değişecek, ${count('drop')} silinecek` +
80
+ (count('kept_local') ? `, ${count('kept_local')} yerel özelleştirme korunacak` : '') + '.');
81
+ }
82
+ for (const e of result.errors ?? [])
83
+ lines.push(out.paint('red', `Hata: ${e}`));
84
+ return lines.join('\n');
85
+ }
86
+ export class SchemaClient {
87
+ o;
88
+ constructor(o) {
89
+ this.o = o;
90
+ }
91
+ headers(extra = {}) {
92
+ return {
93
+ Authorization: `Bearer ${this.o.token}`,
94
+ 'X-Tenant-ID': this.o.workspaceId,
95
+ 'X-Enformik-CLI-Version': this.o.version,
96
+ ...extra,
97
+ };
98
+ }
99
+ /** slug → modül kimliği. 404: modül kurulu değil (ilk kurulumu bir yönetici yapar). */
100
+ async resolveModule(slug) {
101
+ const res = await request(joinUrl(this.o.appUrl, `/api/module-by-slug/${encodeURIComponent(slug)}`), {
102
+ headers: this.headers(), retries: 2, fetchImpl: this.o.fetchImpl,
103
+ });
104
+ try {
105
+ ensureOk(res);
106
+ }
107
+ catch (e) {
108
+ throw describeSchemaError(e, { slug });
109
+ }
110
+ const j = res.json;
111
+ return { id: j.id, name: j.name, version: j.version };
112
+ }
113
+ async diff(moduleId, manifestJson, opts) {
114
+ const res = await request(joinUrl(this.o.appUrl, `/api/modules/${moduleId}/diff${opts.seedData ? '?seed_data=true' : ''}`), {
115
+ method: 'POST', headers: this.headers({ 'Content-Type': 'application/json' }), body: manifestJson, retries: 1, timeoutMs: 120_000, fetchImpl: this.o.fetchImpl,
116
+ });
117
+ try {
118
+ ensureOk(res);
119
+ }
120
+ catch (e) {
121
+ throw describeSchemaError(e, {});
122
+ }
123
+ return normalizeDiff(res.json);
124
+ }
125
+ async upgrade(moduleId, manifestJson, opts) {
126
+ // retries: 0 — uygulama tekrar edilmemeli (ağ hatasında durumu `diff` ile kontrol edin).
127
+ const res = await request(joinUrl(this.o.appUrl, `/api/modules/${moduleId}/upgrade${opts.seedData ? '?seed_data=true' : ''}`), {
128
+ method: 'POST', headers: this.headers({ 'Content-Type': 'application/json' }), body: manifestJson, timeoutMs: 300_000, fetchImpl: this.o.fetchImpl,
129
+ });
130
+ try {
131
+ ensureOk(res);
132
+ }
133
+ catch (e) {
134
+ throw describeSchemaError(e, {});
135
+ }
136
+ return normalizeDiff(res.json);
137
+ }
138
+ }
139
+ function normalizeDiff(j) {
140
+ const o = (j ?? {});
141
+ return { moduleId: o.moduleId ?? '', diffs: Array.isArray(o.diffs) ? o.diffs : [], errors: Array.isArray(o.errors) ? o.errors : [] };
142
+ }
143
+ /** Sunucu hatalarını (özellikle yetki sınırlarını) kullanıcıya anlaşılır biçimde çevirir. */
144
+ export function describeSchemaError(e, ctx) {
145
+ if (!(e instanceof ApiError))
146
+ return e;
147
+ const body = (e.body ?? {});
148
+ switch (e.code) {
149
+ case 'manifest_requires_admin':
150
+ return new CliError(`Bu manifest, istemcinizin rolünü aşan bölümler içeriyor: ${(body.sections ?? []).join(', ')}.\n` +
151
+ '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);
152
+ case 'type_change_requires_admin':
153
+ 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);
154
+ case 'breaking_change_blocked':
155
+ return new CliError(`${e.message}`, 1, e.code, e.body);
156
+ case 'module_not_found':
157
+ return new CliError(`"${ctx.slug ?? 'modül'}" slug'lı bir modül bu uygulamada kurulu değil. Modülün ilk kurulumunu (içe aktarma) bir yönetici bir kez yapmalıdır; sonraki yükseltmeleri bu CLI yapar.`, 1, e.code);
158
+ case 'payment_required':
159
+ return new CliError('Uygulamanın planı/ödeme durumu nedeniyle işlem engellendi (402). Konsoldan abonelik durumunu kontrol edin.', 1, e.code);
160
+ }
161
+ if (e.status === 401)
162
+ return new CliError('Kimlik reddedildi (401): belirteç geçersiz ya da istemci iptal edilmiş.', 1, 'unauthorized');
163
+ if (e.status === 403) {
164
+ return new CliError('Bu istemcinin rolü modül yükseltmeye yetmiyor (403). Konsolda rolü "Şema yükseltici" ya da "Yönetici" olan bir API istemcisi kullanın.' +
165
+ (body.required ? ` (gereken yetki: ${body.required})` : ''), 1, 'forbidden');
166
+ }
167
+ if (e.status === 404)
168
+ return new CliError(`Kayıt bulunamadı (404): ${e.message}`, 1, 'not_found');
169
+ return new CliError(`Sunucu hatası (HTTP ${e.status}): ${e.message}`, 1, e.code);
170
+ }
package/dist/site.js ADDED
@@ -0,0 +1,129 @@
1
+ import { execFileSync } from 'node:child_process';
2
+ import { createHash, randomUUID } from 'node:crypto';
3
+ import { readFileSync } from 'node:fs';
4
+ import { ApiError, CliError } from './errors.js';
5
+ import { ensureOk, joinUrl, request } from './http.js';
6
+ // Web sitesi dağıtımı — makine uçları (/api/deploy/v1) istemcisi. Akış: init → PUT content → finalize → poll.
7
+ // Sözleşme: docs/plans/DEVELOPER_PLATFORM_DEPLOY_DESIGN.md §3.4, §4.
8
+ export const MAX_ARCHIVE_BYTES = 50 * 1024 * 1024;
9
+ export class DeployClient {
10
+ o;
11
+ constructor(o) {
12
+ this.o = o;
13
+ }
14
+ url(path) { return joinUrl(this.o.consoleUrl, `/api/deploy/v1${path}`); }
15
+ headers(extra = {}) {
16
+ return { Authorization: `Bearer ${this.o.deployKey}`, 'X-Enformik-CLI-Version': this.o.version, ...extra };
17
+ }
18
+ async call(path, init) {
19
+ const res = await request(this.url(path), { ...init, headers: this.headers(init.headers), fetchImpl: this.o.fetchImpl });
20
+ try {
21
+ ensureOk(res);
22
+ }
23
+ catch (e) {
24
+ throw describeDeployError(e);
25
+ }
26
+ return (res.json ?? {});
27
+ }
28
+ getSite() { return this.call('/site', { retries: 2 }); }
29
+ async init(body, idempotencyKey) {
30
+ return this.call('/deployments', {
31
+ method: 'POST', headers: { 'Content-Type': 'application/json', 'Idempotency-Key': idempotencyKey },
32
+ body: JSON.stringify(body), retries: 2, // Idempotency-Key ile tekrar güvenli
33
+ });
34
+ }
35
+ async upload(id, zipPath) {
36
+ // Yeniden deneme yok: kısmi yükleme sonrası tekrar 409 (invalid_state) döner; kullanıcı komutu yeniden çalıştırır.
37
+ const data = readFileSync(zipPath);
38
+ await this.call(`/deployments/${id}/content`, { method: 'PUT', headers: { 'Content-Type': 'application/zip' }, body: data, timeoutMs: 600_000 });
39
+ }
40
+ finalize(id) {
41
+ return this.call(`/deployments/${id}/finalize`, { method: 'POST', retries: 2 });
42
+ }
43
+ async status(id) {
44
+ return (await this.call(`/deployments/${id}`, { retries: 2 })).deployment;
45
+ }
46
+ }
47
+ const TERMINAL = new Set(['live', 'failed', 'expired', 'superseded']);
48
+ export const isTerminal = (s) => TERMINAL.has(s);
49
+ /** Durum değiştikçe onStatus çağırarak terminal duruma kadar bekler. */
50
+ export async function waitForDeployment(client, id, opts) {
51
+ const sleep = opts.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
52
+ const now = opts.now ?? Date.now;
53
+ const start = now();
54
+ let last = '';
55
+ let interval = opts.intervalMs ?? 2000;
56
+ for (;;) {
57
+ const v = await client.status(id);
58
+ if (v.status !== last) {
59
+ last = v.status;
60
+ opts.onStatus?.(v.status);
61
+ }
62
+ if (isTerminal(v.status))
63
+ return v;
64
+ 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.`);
66
+ await sleep(interval);
67
+ interval = Math.min(interval + 500, 5000);
68
+ }
69
+ }
70
+ export function sha256File(path) {
71
+ return createHash('sha256').update(readFileSync(path)).digest('hex');
72
+ }
73
+ /** CI'dan commit/dal/koşu bilgisi (isteğe bağlı; bulunamazsa boş). */
74
+ export function collectMeta(env, cwd, git = (a) => gitCmd(cwd, a)) {
75
+ const meta = {};
76
+ const sha = env.GITHUB_SHA || env.CI_COMMIT_SHA || env.BITBUCKET_COMMIT || env.BUILD_VCS_NUMBER || git(['rev-parse', 'HEAD']);
77
+ if (sha)
78
+ meta.gitSha = sha;
79
+ const ref = env.GITHUB_REF_NAME || env.CI_COMMIT_REF_NAME || env.BITBUCKET_BRANCH || git(['rev-parse', '--abbrev-ref', 'HEAD']);
80
+ if (ref && ref !== 'HEAD')
81
+ meta.gitRef = ref;
82
+ if (env.GITHUB_SERVER_URL && env.GITHUB_REPOSITORY && env.GITHUB_RUN_ID)
83
+ meta.runUrl = `${env.GITHUB_SERVER_URL}/${env.GITHUB_REPOSITORY}/actions/runs/${env.GITHUB_RUN_ID}`;
84
+ else if (env.CI_PIPELINE_URL)
85
+ meta.runUrl = env.CI_PIPELINE_URL;
86
+ return meta;
87
+ }
88
+ function gitCmd(cwd, args) {
89
+ try {
90
+ return execFileSync('git', args, { cwd, timeout: 3000, stdio: ['ignore', 'pipe', 'ignore'] }).toString().trim();
91
+ }
92
+ catch {
93
+ return '';
94
+ }
95
+ }
96
+ /**
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.
99
+ */
100
+ export function idempotencyKey(env, rand = randomUUID) {
101
+ if (env.GITHUB_RUN_ID)
102
+ return `gh-${env.GITHUB_RUN_ID}-${env.GITHUB_RUN_ATTEMPT ?? '1'}`.slice(0, 64);
103
+ if (env.CI_PIPELINE_ID)
104
+ return `gl-${env.CI_PIPELINE_ID}-${env.CI_JOB_ID ?? '0'}`.slice(0, 64);
105
+ return `cli-${rand()}`.slice(0, 64);
106
+ }
107
+ 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.',
119
+ };
120
+ export function describeDeployError(e) {
121
+ if (!(e instanceof ApiError))
122
+ return e;
123
+ const hint = e.code ? HINTS[e.code] : undefined;
124
+ const detail = e.body?.details;
125
+ let msg = hint ?? e.message ?? `HTTP ${e.status}`;
126
+ if (e.code === 'artifact_invalid' && Array.isArray(detail))
127
+ msg = `Arşiv geçerli değil: ${detail.map((d) => d.message).join('; ')}`;
128
+ return new CliError(msg, 1, e.code, e.body);
129
+ }
package/dist/zip.js ADDED
@@ -0,0 +1,99 @@
1
+ import { closeSync, openSync, readFileSync, writeSync } from 'node:fs';
2
+ import { deflateRawSync } from 'node:zlib';
3
+ const CRC_TABLE = (() => {
4
+ const t = new Uint32Array(256);
5
+ for (let n = 0; n < 256; n++) {
6
+ let c = n;
7
+ for (let k = 0; k < 8; k++)
8
+ c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
9
+ t[n] = c >>> 0;
10
+ }
11
+ return t;
12
+ })();
13
+ export function crc32(buf) {
14
+ let c = 0xffffffff;
15
+ for (let i = 0; i < buf.length; i++)
16
+ c = CRC_TABLE[(c ^ buf[i]) & 0xff] ^ (c >>> 8);
17
+ return (c ^ 0xffffffff) >>> 0;
18
+ }
19
+ function dosDateTime(d) {
20
+ const y = Math.max(1980, d.getFullYear());
21
+ return {
22
+ time: (d.getHours() << 11) | (d.getMinutes() << 5) | Math.floor(d.getSeconds() / 2),
23
+ date: ((y - 1980) << 9) | ((d.getMonth() + 1) << 5) | d.getDate(),
24
+ };
25
+ }
26
+ const MAX_ENTRIES = 0xffff;
27
+ const MAX_U32 = 0xffffffff;
28
+ /** Girdileri sıralı (deterministik) olarak outPath'e zipler. */
29
+ export function writeZipFile(files, outPath) {
30
+ if (files.length > MAX_ENTRIES)
31
+ throw new Error(`Zip en fazla ${MAX_ENTRIES} girdi içerebilir.`);
32
+ const sorted = [...files].sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
33
+ const fd = openSync(outPath, 'w');
34
+ let offset = 0;
35
+ const central = [];
36
+ const write = (b) => { writeSync(fd, b); offset += b.length; };
37
+ try {
38
+ for (const f of sorted) {
39
+ const data = readFileSync(f.path);
40
+ if (data.length >= MAX_U32)
41
+ throw new Error(`Dosya çok büyük (zip64 desteklenmiyor): ${f.name}`);
42
+ const name = Buffer.from(f.name, 'utf8');
43
+ const deflated = deflateRawSync(data, { level: 6 });
44
+ const useDeflate = deflated.length < data.length;
45
+ const body = useDeflate ? deflated : data;
46
+ const method = useDeflate ? 8 : 0;
47
+ const crc = crc32(data);
48
+ const { time, date } = dosDateTime(f.mtime);
49
+ const localOffset = offset;
50
+ const local = Buffer.alloc(30);
51
+ local.writeUInt32LE(0x04034b50, 0);
52
+ local.writeUInt16LE(20, 4); // gereken sürüm
53
+ local.writeUInt16LE(0x0800, 6); // bayrak: UTF-8 ad
54
+ local.writeUInt16LE(method, 8);
55
+ local.writeUInt16LE(time, 10);
56
+ local.writeUInt16LE(date, 12);
57
+ local.writeUInt32LE(crc, 14);
58
+ local.writeUInt32LE(body.length, 18);
59
+ local.writeUInt32LE(data.length, 22);
60
+ local.writeUInt16LE(name.length, 26);
61
+ local.writeUInt16LE(0, 28);
62
+ write(local);
63
+ write(name);
64
+ write(body);
65
+ const c = Buffer.alloc(46);
66
+ c.writeUInt32LE(0x02014b50, 0);
67
+ c.writeUInt16LE((3 << 8) | 20, 4); // üreten: Unix, sürüm 2.0
68
+ c.writeUInt16LE(20, 6);
69
+ c.writeUInt16LE(0x0800, 8);
70
+ c.writeUInt16LE(method, 10);
71
+ c.writeUInt16LE(time, 12);
72
+ c.writeUInt16LE(date, 14);
73
+ c.writeUInt32LE(crc, 16);
74
+ c.writeUInt32LE(body.length, 20);
75
+ c.writeUInt32LE(data.length, 24);
76
+ c.writeUInt16LE(name.length, 28);
77
+ c.writeUInt32LE(((0o100644 << 16) >>> 0), 38); // normal dosya, 0644
78
+ c.writeUInt32LE(localOffset, 42);
79
+ central.push(Buffer.concat([c, name]));
80
+ }
81
+ const cdStart = offset;
82
+ for (const c of central)
83
+ write(c);
84
+ const cdSize = offset - cdStart;
85
+ const end = Buffer.alloc(22);
86
+ end.writeUInt32LE(0x06054b50, 0);
87
+ end.writeUInt16LE(sorted.length, 8);
88
+ end.writeUInt16LE(sorted.length, 10);
89
+ end.writeUInt32LE(cdSize, 12);
90
+ end.writeUInt32LE(cdStart, 16);
91
+ write(end);
92
+ if (offset >= MAX_U32)
93
+ throw new Error('Zip 4 GB sınırını aşıyor.');
94
+ return { entries: sorted.length, bytes: offset };
95
+ }
96
+ finally {
97
+ closeSync(fd);
98
+ }
99
+ }