@enformik/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
@@ -19,7 +19,7 @@ export ENFORMIK_DEPLOY_KEY=enf_dk_… # CI gizli değişkeni; depoya yazm
19
19
  npx @enformik/cli site deploy --dir dist
20
20
  ```
21
21
 
22
- `index.html` kökte mi, gizli dosya (`.env`, `.git`…) var mı, boyut/kota uygun mu gibi kurallar **yüklemeden önce** denetlenir; hatalar dosya yoluyla gösterilir. Başarıda yeni sürümün adresi yazdırılır.
22
+ `index.html` kökte mi, gizli dosya (`.env`, `.git`…) var mı, dosya türleri izinli mi (çalıştırılabilir dosya, `.zip`/`.iso` ve uzantısız dosya reddedilir), boyut/kota uygun mu gibi kurallar **yüklemeden önce** denetlenir; hatalar dosya yoluyla gösterilir. Paketteki metin dosyalarında sır bulunursa yükleme durur; yanlış alarmı proje kökündeki `.enformikignore` dosyasına (satır başına bir yol, `dizin/` öneki, joker yok) yazarak atlayabilirsiniz. `--expect-host <site-adresi>` yanlış siteye dağıtımı engeller. Başarıda yeni sürümün adresi yazdırılır.
23
23
 
24
24
  ## Şema (modül) yükseltme
25
25
 
@@ -47,8 +47,8 @@ npx @enformik/cli schema push --manifest module.json --yes
47
47
  | `whoami` | Hangi kimlikle, hangi hedefe bağlanıldığını gösterir |
48
48
  | `schema diff` | Manifesti uygulamadakiyle karşılaştırır (`--check`) |
49
49
  | `schema push` | Değişiklikleri uygular (etkileşimsiz ortamda `--yes` zorunlu) |
50
- | `site deploy` | Derlenmiş siteyi yayınlar (`--dir`, `--no-wait`, `--timeout`) |
51
- | `site status` | Hedef site ve canlı sürüm bilgisi |
50
+ | `site deploy` | Derlenmiş siteyi yayınlar (`--dir`, `--no-wait`, `--timeout`, `--expect-host`) |
51
+ | `site status` | Hedef site ve canlı sürüm bilgisi (`--expect-host`) |
52
52
 
53
53
  Genel bayraklar: `--json` (stdout'a yalnızca JSON; insan çıktısı stderr'e), `--config`, `--debug`.
54
54
  Çıkış kodları: `0` başarı · `1` hata · `2` (`--check`) uygulanacak değişiklik var.
package/dist/cli.js CHANGED
@@ -34,8 +34,8 @@ export const COMMANDS = {
34
34
  'schema validate': { flags: ['app-url', 'workspace', 'module', 'manifest', 'seed-data', 'expect-host', 'profile', 'offline'], run: schemaValidate, usage: 'enformik schema validate [--manifest module.json] [--profile import|ai] [--offline]' },
35
35
  'schema install': { flags: ['app-url', 'workspace', 'module', 'manifest', 'yes', 'dry-run', 'seed-data', 'expect-host'], run: schemaInstall, usage: 'enformik schema install [--manifest module.json] [--yes] [--dry-run]' },
36
36
  'schema push': { flags: ['app-url', 'workspace', 'module', 'manifest', 'yes', 'seed-data', 'expect-host'], run: schemaPush, usage: 'enformik schema push [--manifest module.json] [--yes] [--seed-data]' },
37
- 'site deploy': { flags: ['console-url', 'dir', 'no-wait', 'timeout'], run: siteDeploy, usage: 'enformik site deploy [--dir dist] [--no-wait] [--timeout 600]' },
38
- 'site status': { flags: ['console-url'], run: siteStatus, usage: 'enformik site status' },
37
+ 'site deploy': { flags: ['console-url', 'dir', 'no-wait', 'timeout', 'expect-host'], run: siteDeploy, usage: 'enformik site deploy [--dir dist] [--no-wait] [--timeout 600] [--expect-host <site-adresi>]' },
38
+ 'site status': { flags: ['console-url', 'expect-host'], run: siteStatus, usage: 'enformik site status [--expect-host <site-adresi>]' },
39
39
  };
40
40
  export function helpText(version) {
41
41
  return `enformik ${version} — Enformik geliştirici CLI'si
@@ -80,8 +80,10 @@ export async function main(d) {
80
80
  parsed = parseArgs({ args: d.argv, options: OPTIONS, allowPositionals: true, strict: true });
81
81
  }
82
82
  catch (e) {
83
- const out = createOutput({ stderr: d.stderr, stdout: d.stdout, env: d.env });
83
+ // Ayrıştırma başarısız olduğu için --json değeri yok; ham argümandan anlaşılır (CAP-9: JSON kipinde stdout JSON).
84
+ const out = createOutput({ json: d.argv.includes('--json'), stderr: d.stderr, stdout: d.stdout, env: d.env });
84
85
  out.error(`${e.message}\n"enformik --help" ile kullanımı görün.`);
86
+ out.result({ error: 'usage', message: e.message });
85
87
  return 1;
86
88
  }
87
89
  const { values, positionals } = parsed;
@@ -91,8 +93,10 @@ export async function main(d) {
91
93
  return 0;
92
94
  }
93
95
  const name = positionals.slice(0, 2).join(' ');
94
- const cmd = COMMANDS[name] ?? COMMANDS[positionals[0] ?? ''];
95
- const cmdName = COMMANDS[name] ? name : positionals[0] ?? '';
96
+ // COMMANDS düz nesne: "constructor", "toString" gibi adlar prototipten çözülmesin.
97
+ const find = (k) => (Object.hasOwn(COMMANDS, k) ? COMMANDS[k] : undefined);
98
+ const cmd = find(name) ?? find(positionals[0] ?? '');
99
+ const cmdName = find(name) ? name : positionals[0] ?? '';
96
100
  if (values.help || positionals.length === 0 || positionals[0] === 'help') {
97
101
  if (cmd && values.help) {
98
102
  out.info(`Kullanım: ${cmd.usage}`);
@@ -103,17 +107,20 @@ export async function main(d) {
103
107
  }
104
108
  if (!cmd) {
105
109
  out.error(`Bilinmeyen komut: ${positionals.join(' ')}\n"enformik --help" ile komutları görün.`);
110
+ out.result({ error: 'unknown_command', message: `Bilinmeyen komut: ${positionals.join(' ')}` });
106
111
  return 1;
107
112
  }
108
113
  const used = Object.entries(values).filter(([, v]) => v !== undefined).map(([k]) => k);
109
114
  const bad = used.filter((k) => !GLOBAL.includes(k) && !cmd.flags.includes(k));
110
115
  if (bad.length > 0) {
111
116
  out.error(`"${cmdName}" komutunda geçersiz bayrak: ${bad.map((b) => '--' + b).join(', ')}\nKullanım: ${cmd.usage}`);
117
+ out.result({ error: 'usage', message: `"${cmdName}" komutunda geçersiz bayrak: ${bad.map((b) => '--' + b).join(', ')}` });
112
118
  return 1;
113
119
  }
114
120
  const extra = positionals.slice(cmdName.split(' ').length);
115
121
  if (extra.length > 0) {
116
122
  out.error(`Beklenmeyen argüman: ${extra.join(' ')}\nKullanım: ${cmd.usage}`);
123
+ out.result({ error: 'usage', message: `Beklenmeyen argüman: ${extra.join(' ')}` });
117
124
  return 1;
118
125
  }
119
126
  try {
@@ -139,9 +146,11 @@ export async function main(d) {
139
146
  }
140
147
  if (e instanceof ApiError) {
141
148
  out.error(`Sunucu hatası (HTTP ${e.status}): ${e.message}`);
149
+ out.result({ error: e.code ?? 'api_error', message: e.message, status: e.status });
142
150
  return 1;
143
151
  }
144
152
  out.error(`Beklenmeyen hata: ${e.message}`);
153
+ out.result({ error: 'unexpected', message: e.message });
145
154
  if (values.debug)
146
155
  out.error(e.stack ?? '');
147
156
  return 1;
@@ -156,5 +165,5 @@ catch {
156
165
  return p;
157
166
  } }
158
167
  if (invoked && invoked === fileURLToPath(import.meta.url)) {
159
- main({ argv: process.argv.slice(2), env: process.env, cwd: process.cwd(), version: readVersion() }).then((code) => { process.exitCode = code; });
168
+ main({ argv: process.argv.slice(2), env: process.env, cwd: process.cwd(), version: readVersion() }).then((code) => { process.exitCode = code; }, (e) => { process.stderr.write(`Beklenmeyen hata: ${e.message}\n`); process.exitCode = 1; });
160
169
  }
@@ -47,14 +47,21 @@ export function agentBlock(lang, guideVersion) {
47
47
  ];
48
48
  return `${START} (enformik ai init; guide ${guideVersion}) -->\n${body.join('\n')}\n${END}\n`;
49
49
  }
50
- /** Dosyada Enformik bloğunu ekler ya da değiştirir (blok dışı içeriğe dokunmaz). */
50
+ /**
51
+ * Dosyada Enformik bloğunu ekler ya da değiştirir (blok dışı içeriğe dokunmaz). Başlangıç işaretçisi var ama ondan
52
+ * sonra bitiş işaretçisi yoksa (elle silinmiş/bozulmuş) dosyaya DOKUNULMAZ: ikinci blok eklemek, sonraki çalıştırmada
53
+ * iki işaretçi arasındaki kullanıcı içeriğinin silinmesine yol açardı.
54
+ */
51
55
  export function upsertBlock(existing, block) {
52
56
  if (existing === null || existing.trim() === '')
53
57
  return block;
54
58
  const s = existing.indexOf(START);
55
- const e = existing.indexOf(END);
56
- if (s >= 0 && e > s)
59
+ if (s >= 0) {
60
+ const e = existing.indexOf(END, s);
61
+ if (e < 0)
62
+ throw new CliError(`Talimat bloğunun başlangıç işaretçisi var ama bitiş işaretçisi (${END}) yok; dosya bozulmuş görünüyor. Dosyaya dokunulmadı: işaretçiyi onarın ya da bloğu elle silip yeniden çalıştırın.`);
57
63
  return existing.slice(0, s) + block + existing.slice(e + END.length).replace(/^\n/, '');
64
+ }
58
65
  return existing.replace(/\n*$/, '\n\n') + block;
59
66
  }
60
67
  async function download(base, rel, sha, ctx) {
@@ -87,7 +94,17 @@ export async function aiInit(ctx) {
87
94
  const cached = join(root, 'index.json');
88
95
  if (!existsSync(cached))
89
96
  throw new CliError('Önbellekte rehber yok. Önce çevrimiçi `enformik ai init` çalıştırın.');
90
- index = parseIndex(JSON.parse(readFileSync(cached, 'utf8')));
97
+ let raw;
98
+ try {
99
+ raw = JSON.parse(readFileSync(cached, 'utf8'));
100
+ }
101
+ catch {
102
+ throw new CliError('Önbellekteki rehber dizini (index.json) okunamadı; çevrimiçi `enformik ai init` ile yeniden indirin.');
103
+ }
104
+ index = parseIndex(raw);
105
+ if (cmpVersion(ctx.version, index.minCliVersion) < 0) {
106
+ throw new CliError(`Önbellekteki rehber en az CLI ${index.minCliVersion} ister (sizde ${ctx.version}). \`npm install -g @enformik/cli\` ile güncelleyin.`);
107
+ }
91
108
  source = 'cache';
92
109
  out.warn(`Önbellekteki rehber kullanılıyor (sürüm ${index.guideVersion}); güncel olmayabilir.`);
93
110
  if (!existsSync(join(root, lang, 'overview.md')))
@@ -127,16 +144,20 @@ export async function aiInit(ctx) {
127
144
  }
128
145
  const block = agentBlock(lang, index.guideVersion);
129
146
  const written = [];
147
+ // Önce hepsini hesapla, sonra yaz: biri bozuksa hiçbir talimat dosyası yarım güncellenmesin.
148
+ const updates = [];
130
149
  for (const f of ['AGENTS.md', 'CLAUDE.md']) {
131
150
  const p = resolve(ctx.cwd, f);
132
151
  if (f === 'CLAUDE.md' && !existsSync(p))
133
152
  continue;
134
153
  const cur = existsSync(p) ? readFileSync(p, 'utf8') : null;
135
154
  const next = upsertBlock(cur, block);
136
- if (next !== cur) {
137
- writeFileSync(p, next);
138
- written.push(f);
139
- }
155
+ if (next !== cur)
156
+ updates.push({ f, p, next });
157
+ }
158
+ for (const u of updates) {
159
+ writeFileSync(u.p, u.next);
160
+ written.push(u.f);
140
161
  }
141
162
  const fileCount = Object.keys(index.files[lang] ?? {}).length + Object.keys(index.shared).length;
142
163
  out.info(`✓ Ajan rehberi hazır (sürüm ${index.guideVersion}, ${lang}, ${fileCount} dosya, kaynak: ${source === 'server' ? 'sunucu' : 'önbellek'}) → .enformik/ai/`);
@@ -28,92 +28,103 @@ export async function appCheck(ctx) {
28
28
  }
29
29
  // ── Site: yayınlanmış çalışma zamanı yapılandırması ──────────────────────────────────────────
30
30
  if (hasSite) {
31
- const info = await new DeployClient({ consoleUrl: settings.consoleUrl, deployKey: settings.deployKey, version: ctx.version, fetchImpl: ctx.fetchImpl }).getSite();
32
- if (!info.live) {
33
- checks.push({ id: 'site.live', status: 'warn', message: 'Sitede henüz canlı dağıtım yok; giriş yapılandırması denetlenemedi.', hint: '`enformik site deploy` ile yayınlayın.' });
34
- }
35
- else {
36
- assertSecureUrl(info.url, 'Site adresi');
37
- let cfg;
38
- try {
39
- const res = await request(joinUrl(info.url, '/_enformik/config.json'), { retries: 1, fetchImpl: ctx.fetchImpl });
40
- if (res.ok && res.json && typeof res.json === 'object')
41
- cfg = res.json;
42
- }
43
- catch { /* ulaşılamadı → aşağıda uyarı */ }
44
- if (!cfg) {
45
- checks.push({ id: 'site.config', status: 'warn', message: `${info.url}/_enformik/config.json okunamadı.`, hint: 'Site yayında mı? Yeniden dağıtmayı deneyin.' });
31
+ try {
32
+ const info = await new DeployClient({ consoleUrl: settings.consoleUrl, deployKey: settings.deployKey, version: ctx.version, fetchImpl: ctx.fetchImpl }).getSite();
33
+ if (!info.live) {
34
+ checks.push({ id: 'site.live', status: 'warn', message: 'Sitede henüz canlı dağıtım yok; giriş yapılandırması denetlenemedi.', hint: '`enformik site deploy` ile yayınlayın.' });
46
35
  }
47
36
  else {
48
- const auth = cfg.auth;
49
- if (!auth || !auth.clientId) {
50
- checks.push({ id: 'site.auth', status: 'fail', message: 'Site girişi kapalı: yayınlanmış yapılandırmada giriş (auth) bilgisi yok.', hint: 'Konsol → Geliştirici sitesi → Site girişi sekmesinden girişi açıp siteyi yeniden dağıtın.' });
37
+ assertSecureUrl(info.url, 'Site adresi');
38
+ let cfg;
39
+ try {
40
+ const res = await request(joinUrl(info.url, '/_enformik/config.json'), { retries: 1, fetchImpl: ctx.fetchImpl });
41
+ if (res.ok && res.json && typeof res.json === 'object')
42
+ cfg = res.json;
43
+ }
44
+ catch { /* ulaşılamadı → aşağıda uyarı */ }
45
+ if (!cfg) {
46
+ checks.push({ id: 'site.config', status: 'warn', message: `${info.url}/_enformik/config.json okunamadı.`, hint: 'Site yayında mı? Yeniden dağıtmayı deneyin.' });
51
47
  }
52
48
  else {
53
- checks.push({ id: 'site.auth', status: 'ok', message: 'Site girişi açık; yayınlanmış yapılandırmada giriş bilgisi var.' });
49
+ const auth = cfg.auth;
50
+ if (!auth || !auth.clientId) {
51
+ checks.push({ id: 'site.auth', status: 'fail', message: 'Site girişi kapalı: yayınlanmış yapılandırmada giriş (auth) bilgisi yok.', hint: 'Konsol → Geliştirici sitesi → Site girişi sekmesinden girişi açıp siteyi yeniden dağıtın.' });
52
+ }
53
+ else {
54
+ checks.push({ id: 'site.auth', status: 'ok', message: 'Site girişi açık; yayınlanmış yapılandırmada giriş bilgisi var.' });
55
+ }
56
+ const ok = typeof cfg.appUrl === 'string' && typeof cfg.apiUrl === 'string' && !!cfg.workspaceId;
57
+ checks.push(ok ? { id: 'site.binding', status: 'ok', message: 'Site bir uygulamaya ve çalışma alanına bağlı.' }
58
+ : { id: 'site.binding', status: 'fail', message: 'Yapılandırmada uygulama/çalışma alanı bağlantısı eksik.', hint: 'Konsolda geliştirici sitesinin uygulama bağlantısını kontrol edin.' });
54
59
  }
55
- const ok = typeof cfg.appUrl === 'string' && typeof cfg.apiUrl === 'string' && !!cfg.workspaceId;
56
- checks.push(ok ? { id: 'site.binding', status: 'ok', message: 'Site bir uygulamaya ve çalışma alanına bağlı.' }
57
- : { id: 'site.binding', status: 'fail', message: 'Yapılandırmada uygulama/çalışma alanı bağlantısı eksik.', hint: 'Konsolda geliştirici sitesinin uygulama bağlantısını kontrol edin.' });
58
60
  }
59
61
  }
62
+ catch (e) {
63
+ // Bir yarının hatası diğer yarıyı ve checks[] çıktısını kaybettirmesin.
64
+ checks.push({ id: 'site.error', status: 'fail', message: `Site denetimi tamamlanamadı: ${e.message}` });
65
+ }
60
66
  }
61
67
  else {
62
68
  checks.push({ id: 'site', status: 'skip', message: 'Site denetimi atlandı (ENFORMIK_CONSOLE_URL + ENFORMIK_DEPLOY_KEY yok).' });
63
69
  }
64
70
  // ── Uygulama: modül, erişim modu ve web kullanıcısı erişimi ──────────────────────────────────
65
71
  if (hasApp) {
66
- const p = await prepare(ctx, 'validate');
67
- const slug = p.manifest.module.slug;
68
- const mod = await p.client.findModule(slug);
69
- if (!mod) {
70
- checks.push({ id: 'module.installed', status: 'fail', message: `"${slug}" modülü uygulamada kurulu değil.`, hint: '`enformik schema install` (Şema oluşturucu) ile kurun.' });
71
- }
72
- else if (mod.version !== p.manifest.module.version) {
73
- checks.push({ id: 'module.version', status: 'warn', message: `Uygulamadaki modül sürümü ${mod.version}, manifest ${p.manifest.module.version}.`, hint: '`enformik schema diff` / `schema push` ile eşitleyin.' });
74
- }
75
- else {
76
- checks.push({ id: 'module.version', status: 'ok', message: `Modül ${slug} ${mod.version} kurulu ve manifestle aynı sürümde.` });
77
- }
78
- const wanted = typeof ctx.flags.entity === 'string' && ctx.flags.entity !== ''
79
- ? ctx.flags.entity.split(',').map((s) => s.trim().toLowerCase()).filter(Boolean)
80
- : (p.manifest.entities ?? []).map((e) => (e.slug ?? '').toLowerCase()).filter(Boolean);
81
- const summary = await p.client.accessSummary(wanted);
82
- if (!summary) {
83
- checks.push({ id: 'access', status: 'warn', message: 'Bu uygulama sürümü erişim özeti ucunu desteklemiyor; web kullanıcısı erişimi denetlenemedi.', hint: 'Uygulama güncellenince yeniden deneyin.' });
84
- }
85
- else {
86
- if (summary.mode !== 'on') {
87
- checks.push({ id: 'access.mode', status: 'fail', message: `Kayıt erişim kuralları "${summary.mode || 'bilinmiyor'}" modunda; web kullanıcıları bu modda çalışmaz.`, hint: 'Etkinleştirilmesi için Enformik yönetim konsolundan destek kaydı açın.' });
72
+ try {
73
+ const p = await prepare(ctx, 'validate');
74
+ const slug = p.manifest.module.slug;
75
+ const mod = await p.client.findModule(slug);
76
+ if (!mod) {
77
+ checks.push({ id: 'module.installed', status: 'fail', message: `"${slug}" modülü uygulamada kurulu değil.`, hint: '`enformik schema install` (Şema oluşturucu) ile kurun.' });
88
78
  }
89
- else {
90
- checks.push({ id: 'access.mode', status: 'ok', message: 'Kayıt erişim kuralları etkin.' });
79
+ else if (mod.version !== p.manifest.module.version) {
80
+ checks.push({ id: 'module.version', status: 'warn', message: `Uygulamadaki modül sürümü ${mod.version}, manifest ${p.manifest.module.version}.`, hint: '`enformik schema diff` / `schema push` ile eşitleyin.' });
91
81
  }
92
- const byName = new Map(summary.entities.map((e) => [e.entity.toLowerCase(), e.rules]));
93
- const none = [];
94
- const custom = [];
95
- for (const name of wanted) {
96
- const kinds = (byName.get(name) ?? []).map(grantsWebRead);
97
- if (kinds.includes('external'))
98
- continue;
99
- if (kinds.includes('custom'))
100
- custom.push(name);
101
- else
102
- none.push(name);
103
- }
104
- if (wanted.length === 0) {
105
- checks.push({ id: 'access.web', status: 'skip', message: 'Denetlenecek entity yok (manifestte entity ya da --entity yok).' });
82
+ else {
83
+ checks.push({ id: 'module.version', status: 'ok', message: `Modül ${slug} ${mod.version} kurulu ve manifestle aynı sürümde.` });
106
84
  }
107
- else if (none.length > 0) {
108
- checks.push({ id: 'access.web', status: 'warn', message: `Web kullanıcıları şu tablolarda veri göremez (okuma kuralı yok): ${none.join(', ')}.`, hint: 'Bir yönetici ilgili tabloların Kayıt Erişimi ekranında "Web Kullanıcısı" (ya da özel rol) için okuma kuralı tanımlamalı; düşük rollü istemci erişim kuralı uygulayamaz.' });
85
+ const wanted = typeof ctx.flags.entity === 'string' && ctx.flags.entity !== ''
86
+ ? ctx.flags.entity.split(',').map((s) => s.trim().toLowerCase()).filter(Boolean)
87
+ : (p.manifest.entities ?? []).map((e) => (e.slug ?? '').toLowerCase()).filter(Boolean);
88
+ const summary = await p.client.accessSummary(wanted);
89
+ if (!summary) {
90
+ checks.push({ id: 'access', status: 'warn', message: 'Bu uygulama sürümü erişim özeti ucunu desteklemiyor; web kullanıcısı erişimi denetlenemedi.', hint: 'Uygulama güncellenince yeniden deneyin.' });
109
91
  }
110
92
  else {
111
- checks.push({ id: 'access.web', status: 'ok', message: `Web kullanıcıları için okuma kuralı var (${wanted.length - custom.length} tablo).` });
112
- }
113
- if (custom.length > 0) {
114
- checks.push({ id: 'access.custom', status: 'warn', message: `Şu tablolarda yalnızca özel rol kuralı var: ${custom.join(', ')}.`, hint: 'Web kullanıcılarına ilgili özel rolün atandığından emin olun (yeni kullanıcı varsayılan rolü ya da yönetici ataması).' });
93
+ if (summary.mode !== 'on') {
94
+ checks.push({ id: 'access.mode', status: 'fail', message: `Kayıt erişim kuralları "${summary.mode || 'bilinmiyor'}" modunda; web kullanıcıları bu modda çalışmaz.`, hint: 'Etkinleştirilmesi için Enformik yönetim konsolundan destek kaydı açın.' });
95
+ }
96
+ else {
97
+ checks.push({ id: 'access.mode', status: 'ok', message: 'Kayıt erişim kuralları etkin.' });
98
+ }
99
+ const byName = new Map(summary.entities.map((e) => [e.entity.toLowerCase(), e.rules]));
100
+ const none = [];
101
+ const custom = [];
102
+ for (const name of wanted) {
103
+ const kinds = (byName.get(name) ?? []).map(grantsWebRead);
104
+ if (kinds.includes('external'))
105
+ continue;
106
+ if (kinds.includes('custom'))
107
+ custom.push(name);
108
+ else
109
+ none.push(name);
110
+ }
111
+ if (wanted.length === 0) {
112
+ checks.push({ id: 'access.web', status: 'skip', message: 'Denetlenecek entity yok (manifestte entity ya da --entity yok).' });
113
+ }
114
+ else if (none.length > 0) {
115
+ checks.push({ id: 'access.web', status: 'warn', message: `Web kullanıcıları şu tablolarda veri göremez (okuma kuralı yok): ${none.join(', ')}.`, hint: 'Bir yönetici ilgili tabloların Kayıt Erişimi ekranında "Web Kullanıcısı" (ya da özel rol) için okuma kuralı tanımlamalı; düşük rollü istemci erişim kuralı uygulayamaz.' });
116
+ }
117
+ else {
118
+ checks.push({ id: 'access.web', status: 'ok', message: `Web kullanıcıları için okuma kuralı var (${wanted.length - custom.length} tablo).` });
119
+ }
120
+ if (custom.length > 0) {
121
+ checks.push({ id: 'access.custom', status: 'warn', message: `Şu tablolarda yalnızca özel rol kuralı var: ${custom.join(', ')}.`, hint: 'Web kullanıcılarına ilgili özel rolün atandığından emin olun (yeni kullanıcı varsayılan rolü ya da yönetici ataması).' });
122
+ }
115
123
  }
116
124
  }
125
+ catch (e) {
126
+ checks.push({ id: 'app.error', status: 'fail', message: `Uygulama denetimi tamamlanamadı: ${e.message}` });
127
+ }
117
128
  }
118
129
  else {
119
130
  checks.push({ id: 'app', status: 'skip', message: 'Uygulama denetimi atlandı (ENFORMIK_APP_URL + ENFORMIK_CLIENT_ID/SECRET yok).' });
@@ -20,14 +20,17 @@ export async function codegen(ctx) {
20
20
  const code = generateClient(manifest);
21
21
  const outPath = resolve(ctx.cwd, ctx.settings.codegenOut);
22
22
  const current = existsSync(outPath) ? readFileSync(outPath, 'utf8') : null;
23
+ // Windows'ta git autocrlf dosyayı CRLF'ye çevirir; satır sonu/BOM farkı "güncel değil" sayılmasın.
24
+ const norm = (s) => s.replace(/^\uFEFF/, '').replace(/\r\n/g, '\n');
25
+ const same = current !== null && norm(current) === norm(code);
23
26
  const slug = manifest.module.slug;
24
27
  if (ctx.flags.check === true) {
25
- const upToDate = current === code;
28
+ const upToDate = same;
26
29
  ctx.out.info(upToDate ? `✓ ${ctx.settings.codegenOut} güncel (${slug}).` : `✗ ${ctx.settings.codegenOut} ${current === null ? 'yok' : 'manifestle uyumsuz'}; \`enformik codegen\` ile yeniden üretin.`);
27
30
  ctx.out.result({ module: slug, out: ctx.settings.codegenOut, upToDate });
28
31
  return upToDate ? 0 : 2;
29
32
  }
30
- const changed = current !== code;
33
+ const changed = !same;
31
34
  if (changed) {
32
35
  mkdirSync(dirname(outPath), { recursive: true });
33
36
  writeFileSync(outPath, code);
@@ -3,7 +3,7 @@ import { resolve } from 'node:path';
3
3
  import { discoverApp, fetchAccessToken } from '../auth.js';
4
4
  import { CliError } from '../errors.js';
5
5
  import { assertSecureUrl } from '../http.js';
6
- import { adminOnlySections, parseManifest, pendingChanges, renderDiff, SchemaClient, typeChanges } from '../schema.js';
6
+ import { adminOnlySections, parseManifest, pendingChanges, renderDiff, SchemaClient, stripBom, typeChanges, unapprovedChanges } from '../schema.js';
7
7
  import { renderTable } from '../output.js';
8
8
  /** Ortak hazırlık: manifest → uygulama keşfi → belirteç → modül kimliği. */
9
9
  export async function prepare(ctx, mode = 'existing') {
@@ -21,12 +21,14 @@ export async function prepare(ctx, mode = 'existing') {
21
21
  const manifestPath = resolve(ctx.cwd, settings.manifestPath);
22
22
  let text;
23
23
  try {
24
- text = readFileSync(manifestPath, 'utf8');
24
+ text = stripBom(readFileSync(manifestPath, 'utf8'));
25
25
  }
26
26
  catch {
27
27
  throw new CliError(`Manifest okunamadı: ${manifestPath}`);
28
28
  }
29
29
  const manifest = parseManifest(text, settings.manifestPath);
30
+ // Sunucuya dosyanın kendi metni gider (JSON.stringify yeniden serileştirmesi büyük sayıları/biçimi değiştirebilirdi).
31
+ const manifestJson = text;
30
32
  const slug = settings.moduleSlug ?? manifest.module.slug;
31
33
  if (settings.moduleSlug && settings.moduleSlug !== manifest.module.slug) {
32
34
  throw new CliError(`Yapılandırmadaki modül ("${settings.moduleSlug}") ile manifestteki slug ("${manifest.module.slug}") farklı.`);
@@ -40,19 +42,21 @@ export async function prepare(ctx, mode = 'existing') {
40
42
  const workspaceId = settings.workspaceId || app.tenantId;
41
43
  if (!workspaceId)
42
44
  throw new CliError('Çalışma alanı belirlenemedi. --workspace <kimlik> verin ya da `enformik link --workspace` ile kaydedin.');
45
+ // Token adresi uygulamanın /api/config yanıtından gelir; istemci sırrı oraya gider, bu yüzden hedefi görünür kıl.
46
+ out.info(`Kimlik sağlayıcı: ${new URL(app.oidcAuthority).host} (istemci kimliği buraya gönderilir)`);
43
47
  const tok = await fetchAccessToken(app, settings.clientId, settings.clientSecret, ctx.version, ctx.fetchImpl);
44
48
  const client = new SchemaClient({ appUrl: settings.appUrl, token: tok.token, workspaceId, version: ctx.version, fetchImpl: ctx.fetchImpl });
45
49
  if (mode === 'validate') {
46
- return { client, manifest, manifestJson: JSON.stringify(manifest), moduleId: '', installedVersion: '', appHost: appUrl.hostname, seedData };
50
+ return { client, manifest, manifestJson, moduleId: '', installedVersion: '', appHost: appUrl.hostname, seedData };
47
51
  }
48
52
  if (mode === 'install') {
49
53
  const existing = await client.findModule(slug);
50
54
  if (existing)
51
55
  throw new CliError(`"${slug}" modülü bu uygulamada zaten kurulu (sürüm ${existing.version}). Yükseltmek için \`enformik schema push\` kullanın.`);
52
- return { client, manifest, manifestJson: JSON.stringify(manifest), moduleId: '', installedVersion: '', appHost: appUrl.hostname, seedData };
56
+ return { client, manifest, manifestJson, moduleId: '', installedVersion: '', appHost: appUrl.hostname, seedData };
53
57
  }
54
58
  const mod = await client.resolveModule(slug);
55
- return { client, manifest, manifestJson: JSON.stringify(manifest), moduleId: mod.id, installedVersion: mod.version, appHost: appUrl.hostname, seedData };
59
+ return { client, manifest, manifestJson, moduleId: mod.id, installedVersion: mod.version, appHost: appUrl.hostname, seedData };
56
60
  }
57
61
  function summary(p, r) {
58
62
  return {
@@ -116,6 +120,13 @@ export async function schemaPush(ctx) {
116
120
  return 1;
117
121
  }
118
122
  ctx.out.info(ctx.out.paint('green', `✓ Uygulandı: ${pendingChanges(res.diffs).length || pending.length} değişiklik (modül ${p.manifest.module.slug} ${p.manifest.module.version}).`));
123
+ // Onay ile uygulama arasında uygulama başkası tarafından değişmiş olabilir: onaylanmayan bir değişiklik uygulandıysa açıkça bildir.
124
+ const extra = unapprovedChanges(dry.diffs, res.diffs);
125
+ if (extra.length > 0) {
126
+ ctx.out.error(`Uyarı: onayladığınız farkta olmayan ${extra.length} değişiklik de uygulandı (diff ile upgrade arasında uygulama değişmiş olabilir): ${extra.map((d) => `${d.resourceType} ${d.slug} (${d.action})`).join(', ')}. Sonucu \`enformik schema diff\` ile doğrulayın.`);
127
+ ctx.out.result({ ...summary(p, res), applied: true, diverged: true, unapproved: extra });
128
+ return 1;
129
+ }
119
130
  ctx.out.result({ ...summary(p, res), applied: true });
120
131
  return 0;
121
132
  }
@@ -171,6 +182,9 @@ export async function schemaValidate(ctx) {
171
182
  let source;
172
183
  let slug;
173
184
  if (ctx.flags.offline === true) {
185
+ // ai profilinin kuralları (isimlendirme/yapı) yalnızca sunucuda denetlenir; yerel denetimle "geçerli" demek yanlış güven verir.
186
+ if (profileFlag === 'ai')
187
+ throw new CliError('--offline, --profile ai ile birlikte kullanılamaz: ai profilinin kuralları yalnızca sunucuda denetlenir. --offline olmadan çalıştırın.');
174
188
  const manifestPath = resolve(ctx.cwd, ctx.settings.manifestPath);
175
189
  let text;
176
190
  try {
@@ -198,7 +212,9 @@ export async function schemaValidate(ctx) {
198
212
  if (result.adminSections.length > 0) {
199
213
  ctx.out.info(ctx.out.paint('yellow', `Yönetici adımı gerekir (düşük rollü istemci uygulayamaz): ${result.adminSections.map((s) => s.section).join(', ')}`));
200
214
  }
201
- ctx.out.info(result.valid ? ctx.out.paint('green', '✓ Manifest geçerli.') : ctx.out.paint('red', `✗ ${result.issues.length} bulgu var.`));
215
+ ctx.out.info(result.valid
216
+ ? ctx.out.paint('green', source === 'local' ? '✓ Yerel denetim geçti (sunucu kuralları denetlenmedi).' : '✓ Manifest geçerli.')
217
+ : ctx.out.paint('red', `✗ ${result.issues.length} bulgu var.`));
202
218
  ctx.out.result({ module: slug, source, ...result });
203
219
  return result.valid ? 0 : 1;
204
220
  }
@@ -1,10 +1,9 @@
1
- import { mkdtempSync, rmSync, statSync } from 'node:fs';
1
+ import { mkdtempSync, readFileSync, rmSync, statSync } from 'node:fs';
2
2
  import { tmpdir } from 'node:os';
3
3
  import { join, resolve } from 'node:path';
4
4
  import { CliError } from '../errors.js';
5
- import { assertSecureUrl } from '../http.js';
6
5
  import { formatBytes } from '../output.js';
7
- import { precheckSite, DEFAULT_LIMITS } from '../precheck.js';
6
+ import { precheckSite, parseScanIgnore, DEFAULT_LIMITS } from '../precheck.js';
8
7
  import { collectMeta, DeployClient, idempotencyKey, isTerminal, MAX_ARCHIVE_BYTES, sha256File, waitForDeployment } from '../site.js';
9
8
  import { writeZipFile } from '../zip.js';
10
9
  function client(ctx) {
@@ -13,12 +12,26 @@ function client(ctx) {
13
12
  throw new CliError('Konsol adresi yok. `enformik link --console-url https://…` çalıştırın ya da ENFORMIK_CONSOLE_URL tanımlayın.');
14
13
  if (!settings.deployKey)
15
14
  throw new CliError('ENFORMIK_DEPLOY_KEY tanımlı değil (konsolda Ek ortamlar → Geliştirici sitesi → Dağıtım anahtarları).');
16
- assertSecureUrl(settings.consoleUrl, 'Konsol adresi');
17
15
  return new DeployClient({ consoleUrl: settings.consoleUrl, deployKey: settings.deployKey, version: ctx.version, fetchImpl: ctx.fetchImpl });
18
16
  }
17
+ /** --expect-host verilmişse hedef sitenin ana makine adı eşleşmeli; eşleşmezse hiçbir şey yüklenmeden durur. */
18
+ function assertExpectedHost(ctx, info) {
19
+ const expect = ctx.flags['expect-host'];
20
+ if (typeof expect !== 'string')
21
+ return;
22
+ let host = '';
23
+ try {
24
+ host = new URL(info.url).hostname;
25
+ }
26
+ catch { /* adres bozuksa eşleşmez */ }
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.`);
29
+ }
30
+ }
19
31
  /** enformik site status */
20
32
  export async function siteStatus(ctx) {
21
33
  const info = await client(ctx).getSite();
34
+ assertExpectedHost(ctx, info);
22
35
  ctx.out.info(`Web sitesi ortamı : ${info.environment.name}`);
23
36
  ctx.out.info(`Adres : ${info.url}`);
24
37
  if (info.app)
@@ -43,7 +56,16 @@ export async function siteDeploy(ctx) {
43
56
  const timeoutSec = parseTimeout(ctx.flags.timeout);
44
57
  // Sunucunun kotası biliniyorsa ön denetimde kullan (aşımı yüklemeden görmek için).
45
58
  const info = await api.getSite();
46
- const pre = precheckSite(dir, { ...DEFAULT_LIMITS, maxFiles: info.limits.maxFiles, maxFileBytes: info.limits.maxFileBytes, maxUnpackedBytes: info.limits.maxUnpackedBytes });
59
+ assertExpectedHost(ctx, info);
60
+ // Sır taraması yanlış alarmı için proje kökündeki .enformikignore (yol listesi); yoksa boş.
61
+ let scanIgnore = [];
62
+ try {
63
+ scanIgnore = parseScanIgnore(readFileSync(join(ctx.cwd, '.enformikignore'), 'utf8'));
64
+ }
65
+ catch { /* dosya yok */ }
66
+ const pre = precheckSite(dir, { ...DEFAULT_LIMITS, maxFiles: info.limits.maxFiles, maxFileBytes: info.limits.maxFileBytes, maxUnpackedBytes: info.limits.maxUnpackedBytes }, { scanIgnore });
67
+ for (const w of pre.warnings)
68
+ out.warn(w);
47
69
  if (pre.issues.length > 0) {
48
70
  out.error('Site arşivi kurallara uymuyor; yükleme yapılmadı:');
49
71
  for (const i of pre.issues)
@@ -59,7 +81,7 @@ export async function siteDeploy(ctx) {
59
81
  throw new CliError(`Sıkıştırılmış arşiv ${formatBytes(z.bytes)}; en çok ${formatBytes(MAX_ARCHIVE_BYTES)} olabilir.`);
60
82
  out.info(`${pre.files.length} dosya, ${formatBytes(pre.unpackedBytes)} → ${formatBytes(z.bytes)} (zip)`);
61
83
  const sha = sha256File(zipPath);
62
- const init = await api.init({ sizeBytes: z.bytes, sha256: sha, meta: collectMeta(ctx.env, ctx.cwd) }, idempotencyKey(ctx.env));
84
+ const init = await api.init({ sizeBytes: z.bytes, sha256: sha, meta: collectMeta(ctx.env, ctx.cwd) }, idempotencyKey(ctx.env, undefined, sha));
63
85
  let dep = init.deployment;
64
86
  if (init.replayed)
65
87
  out.info(`Önceki deneme bulundu (#${dep.seq}, durum: ${dep.status}); kaldığı yerden devam ediliyor.`);
@@ -73,7 +95,8 @@ export async function siteDeploy(ctx) {
73
95
  await api.finalize(dep.deploymentId);
74
96
  dep = { ...dep, status: 'validating' };
75
97
  }
76
- if (ctx.flags['no-wait'] === true) {
98
+ // Terminal durumdaki (failed/expired/live) yeniden oynatılan kayıt --no-wait ile de gerçek sonucu vermeli.
99
+ if (ctx.flags['no-wait'] === true && !isTerminal(dep.status)) {
77
100
  out.info(`Dağıtım başlatıldı (#${dep.seq}). Durum için: konsol → Geliştirici sitesi → Sürümler.`);
78
101
  out.result({ deployed: false, deploymentId: dep.deploymentId, seq: dep.seq, status: dep.status });
79
102
  return 0;
@@ -13,10 +13,11 @@ export async function whoami(ctx) {
13
13
  out.info('Şema yükseltme (API istemcisi)');
14
14
  out.info(` Uygulama : ${settings.appUrl}`);
15
15
  out.info(` İstemci : ${settings.clientId}`);
16
+ out.info(` Kimlik sağlayıcı : ${new URL(app.oidcAuthority).host}`);
16
17
  out.info(` Roller : ${roles.join(', ') || '(belirteçte rol yok)'}`);
17
18
  out.info(` Çalışma alanı : ${settings.workspaceId || app.tenantId || '(belirtilmedi)'}`);
18
- out.info(` Belirteç : geçerli (${Math.round(tok.expiresInSec / 60)} dk)`);
19
- result.schema = { appUrl: settings.appUrl, clientId: settings.clientId, roles, workspaceId: settings.workspaceId || app.tenantId || null };
19
+ out.info(` Belirteç : geçerli${tok.expiresInSec > 0 ? ` (${Math.round(tok.expiresInSec / 60)} dk)` : ' (süre bilgisi yok)'}`);
20
+ result.schema = { appUrl: settings.appUrl, authorityHost: new URL(app.oidcAuthority).host, clientId: settings.clientId, roles, workspaceId: settings.workspaceId || app.tenantId || null };
20
21
  }
21
22
  if (settings.consoleUrl && settings.deployKey) {
22
23
  any = true;
package/dist/http.js CHANGED
@@ -18,6 +18,8 @@ export function joinUrl(base, path) {
18
18
  return base.replace(/\/+$/, '') + (path.startsWith('/') ? path : '/' + path);
19
19
  }
20
20
  const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
21
+ /** fetch(redirect:'error') yönlendirmede "fetch failed" + cause "unexpected redirect" ile reddeder. */
22
+ const isRedirectError = (e) => /redirect/i.test(String(e?.cause?.message ?? e?.message ?? ''));
21
23
  export async function request(url, opts = {}) {
22
24
  const doFetch = opts.fetchImpl ?? fetch;
23
25
  const attempts = (opts.retries ?? 0) + 1;
@@ -47,6 +49,8 @@ export async function request(url, opts = {}) {
47
49
  }
48
50
  catch (err) {
49
51
  lastErr = err;
52
+ if (isRedirectError(err))
53
+ break; // yönlendirme tekrar denemeyle değişmez
50
54
  if (i < attempts - 1) {
51
55
  await sleep(400 * 2 ** i);
52
56
  continue;
@@ -54,6 +58,9 @@ export async function request(url, opts = {}) {
54
58
  }
55
59
  }
56
60
  const e = lastErr;
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.`);
63
+ }
57
64
  const reason = e?.name === 'TimeoutError' ? 'zaman aşımı' : (e?.cause?.code ?? e?.message ?? 'bilinmeyen hata');
58
65
  throw new CliError(`Sunucuya ulaşılamadı (${new URL(url).host}): ${reason}`);
59
66
  }
@@ -63,6 +70,8 @@ export function ensureOk(res) {
63
70
  return res;
64
71
  const body = res.json;
65
72
  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)) ?? '';
73
+ // Ara katmanların (CDN/WAF) HTML hata sayfaları mesaja girmez; yalnızca "HTTP <durum>" kalır.
74
+ const plain = /^\s*</.test(res.text) ? '' : res.text.trim().slice(0, 300);
75
+ const message = body?.message ?? (typeof body?.error === 'string' ? body.error : plain) ?? '';
67
76
  throw new ApiError(res.status, message || `HTTP ${res.status}`, code, res.json ?? res.text);
68
77
  }
package/dist/output.js CHANGED
@@ -14,7 +14,7 @@ export function createOutput(opts = {}) {
14
14
  const stderr = opts.stderr ?? process.stderr;
15
15
  const env = opts.env ?? process.env;
16
16
  const json = !!opts.json;
17
- const color = !env.NO_COLOR && env.TERM !== 'dumb' && !!stderr.isTTY && !(json && !stderr.isTTY);
17
+ const color = !env.NO_COLOR && env.TERM !== 'dumb' && !!stderr.isTTY && (json || !!stdout.isTTY); // stdout dosyaya/boruya yönlendirilmişse ANSI kaçışı yazılmaz
18
18
  const human = (s) => (json ? stderr : stdout).write(s + '\n');
19
19
  return {
20
20
  json,
package/dist/precheck.js CHANGED
@@ -1,8 +1,41 @@
1
1
  import { lstatSync, readdirSync, readFileSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
+ /** İzin listesi: yalnızca sitenin çalışması için gereken türler (SPEC CAP-2). Worker (site_artifact.go) ile aynı kümedir;
4
+ * sapmayı temporal-worker testdata/site_file_types.json sözleşme dosyası yakalar. */
5
+ export const ALLOWED_EXTS = new Set([
6
+ '.html', '.htm', '.css', '.js', '.mjs', '.map',
7
+ '.json', '.webmanifest', '.xml', '.txt', '.csv', '.md',
8
+ '.png', '.jpg', '.jpeg', '.gif', '.webp', '.avif', '.svg', '.ico', '.bmp',
9
+ '.woff', '.woff2', '.ttf', '.otf', '.eot',
10
+ '.mp3', '.ogg', '.wav', '.m4a',
11
+ '.mp4', '.webm',
12
+ '.pdf', '.wasm',
13
+ ]);
14
+ export const EXECUTABLE_EXTS = new Set([
15
+ '.exe', '.dll', '.msi', '.bat', '.cmd', '.com', '.scr', '.ps1',
16
+ '.sh', '.apk', '.ipa', '.dmg', '.pkg', '.deb', '.rpm', '.jar', '.app',
17
+ ]);
18
+ /** İzin listesinde olmayan dosya için sorun döner; izinliyse null. Uzantısız dosya yalnızca .well-known/ altında izinli. */
19
+ export function fileTypeIssue(relPath) {
20
+ const base = relPath.slice(relPath.lastIndexOf('/') + 1);
21
+ const dot = base.lastIndexOf('.');
22
+ const ext = dot > 0 ? base.slice(dot).toLowerCase() : '';
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.` };
25
+ }
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).' };
28
+ }
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.` };
31
+ }
32
+ return null;
33
+ }
3
34
  export const DEFAULT_LIMITS = { maxFiles: 10000, maxFileBytes: 100 * 1024 * 1024 };
4
35
  export const RESERVED_DIR = '_enformik';
5
36
  const MAX_ISSUES = 20;
37
+ /** Worker (site_artifact.go maxSitePathBytes) ile aynı sınır. */
38
+ const MAX_PATH_BYTES = 255;
6
39
  // Tarayıcı paketine sızmaması gereken sırlar. Değer ASLA çıktıya yazılmaz; yalnızca dosya yolu ve tür bildirilir.
7
40
  // Bilerek dar tutulur (yanlış alarm CI'ı kırar): dağıtım anahtarı, istemci sırrı atamaları, sabit Basic kimlik başlığı.
8
41
  const SECRET_PATTERNS = [
@@ -11,15 +44,32 @@ const SECRET_PATTERNS = [
11
44
  { kind: 'ENFORMIK_CLIENT_SECRET değeri', re: /ENFORMIK_CLIENT_SECRET["']?\s*[:=]\s*["'`][^"'`\s]{6,}["'`]/ },
12
45
  { kind: 'sabit Basic kimlik başlığı', re: /Authorization["']?\s*:\s*["'`]Basic\s+[A-Za-z0-9+/=]{16,}/i },
13
46
  ];
14
- const SCAN_EXT = new Set(['.js', '.mjs', '.cjs', '.html', '.htm', '.json', '.map', '.css', '.txt', '.xml', '.svg', '.webmanifest']);
47
+ const SCAN_EXT = new Set(['.js', '.mjs', '.cjs', '.html', '.htm', '.json', '.map', '.css', '.txt', '.xml', '.svg', '.webmanifest', '.csv', '.md']);
15
48
  const SCAN_MAX_BYTES = 5 * 1024 * 1024;
49
+ /**
50
+ * `.enformikignore` içeriğini yol listesine çevirir: satır başına bir yol (dağıtım klasörüne göre), `#` yorum.
51
+ * `dir/` ile biten satır o dizinin altındaki her dosyayı kapsar; joker yoktur (bilerek dar). Yalnızca sır taramasını
52
+ * atlatır; dosya türü, boyut ve diğer denetimler yine uygulanır.
53
+ */
54
+ export function parseScanIgnore(text) {
55
+ return text.split(/\r?\n/).map((l) => l.trim().replace(/^\.\//, '')).filter((l) => l && !l.startsWith('#'));
56
+ }
57
+ const isIgnored = (name, ignore) => ignore.some((p) => (p.endsWith('/') ? name.startsWith(p) : name === p));
16
58
  /** Metin dosyalarında sır desenlerini arar (dosya başına en çok bir bulgu). */
17
- export function scanSecrets(files) {
18
- const out = [];
59
+ export function scanSecrets(files, ignore = []) {
60
+ const out = { issues: [], ignored: [], unscanned: [] };
19
61
  for (const f of files) {
20
62
  const dot = f.name.lastIndexOf('.');
21
- if (dot < 0 || !SCAN_EXT.has(f.name.slice(dot).toLowerCase()) || f.size > SCAN_MAX_BYTES)
63
+ if (dot < 0 || !SCAN_EXT.has(f.name.slice(dot).toLowerCase()))
64
+ continue;
65
+ if (isIgnored(f.name, ignore)) {
66
+ out.ignored.push(f.name);
67
+ continue;
68
+ }
69
+ if (f.size > SCAN_MAX_BYTES) {
70
+ out.unscanned.push(f.name);
22
71
  continue;
72
+ }
23
73
  let text;
24
74
  try {
25
75
  text = readFileSync(f.path, 'utf8');
@@ -29,11 +79,11 @@ export function scanSecrets(files) {
29
79
  }
30
80
  const hit = SECRET_PATTERNS.find((p) => p.re.test(text));
31
81
  if (hit)
32
- out.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.` });
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.` });
33
83
  }
34
84
  return out;
35
85
  }
36
- export function precheckSite(dir, limits = DEFAULT_LIMITS) {
86
+ export function precheckSite(dir, limits = DEFAULT_LIMITS, opts = {}) {
37
87
  const files = [];
38
88
  const issues = [];
39
89
  const add = (code, path, message) => {
@@ -56,6 +106,14 @@ export function precheckSite(dir, limits = DEFAULT_LIMITS) {
56
106
  add('hidden_file', relPath, `Gizli dosya/dizin yüklenemez ("${ent.name}"). .env, .git gibi dosyaları arşive koymayın.`);
57
107
  continue;
58
108
  }
109
+ if (Buffer.byteLength(relPath) > MAX_PATH_BYTES) {
110
+ add('unsafe_path', relPath, `Yol ${MAX_PATH_BYTES} karakterden uzun olamaz.`);
111
+ continue;
112
+ }
113
+ if (relPath.includes('\\') || /[\u0000-\u001f\u007f]/.test(relPath)) {
114
+ add('unsafe_path', relPath, 'Yolda geçersiz karakter ya da kontrol karakteri var.');
115
+ continue;
116
+ }
59
117
  const st = lstatSync(absPath);
60
118
  if (st.isSymbolicLink() || (!st.isFile() && !st.isDirectory())) {
61
119
  add('unsupported_entry', relPath, 'Sembolik bağ veya özel dosya desteklenmez.');
@@ -65,6 +123,11 @@ export function precheckSite(dir, limits = DEFAULT_LIMITS) {
65
123
  walk(absPath, relPath);
66
124
  continue;
67
125
  }
126
+ const ft = fileTypeIssue(relPath);
127
+ if (ft) {
128
+ add(ft.code, relPath, ft.message);
129
+ continue;
130
+ }
68
131
  const key = relPath.toLowerCase();
69
132
  if (seen.has(key)) {
70
133
  add('duplicate_path', relPath, 'Aynı yol büyük/küçük harf farkıyla birden fazla var.');
@@ -85,8 +148,14 @@ export function precheckSite(dir, limits = DEFAULT_LIMITS) {
85
148
  }
86
149
  };
87
150
  walk(dir, '');
88
- for (const s of scanSecrets(files))
151
+ const scan = scanSecrets(files, opts.scanIgnore);
152
+ for (const s of scan.issues)
89
153
  add(s.code, s.path, s.message);
154
+ const warnings = [];
155
+ if (scan.ignored.length > 0)
156
+ warnings.push(`.enformikignore nedeniyle sır taraması atlanan dosya: ${scan.ignored.join(', ')}`);
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(', ')}`);
90
159
  if (files.length > limits.maxFiles) {
91
160
  add('too_many_files', undefined, `Arşivde en fazla ${limits.maxFiles} dosya olabilir (${files.length} bulundu).`);
92
161
  }
@@ -102,5 +171,5 @@ export function precheckSite(dir, limits = DEFAULT_LIMITS) {
102
171
  add('missing_index', 'index.html', 'Dizinin kökünde index.html bulunmalı.');
103
172
  }
104
173
  }
105
- return { files, unpackedBytes: total, issues };
174
+ return { files, unpackedBytes: total, issues, warnings };
106
175
  }
package/dist/schema.js CHANGED
@@ -1,10 +1,12 @@
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
12
  throw new CliError(`${path} geçerli JSON değil: ${e.message}`);
@@ -45,10 +47,19 @@ 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');
@@ -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(`Sunucu yanıtı beklenen biçimde değil: "${slug}" modülünün kimliği yok.`);
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) {
@@ -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 {
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, 'Konsol adresi');
13
15
  }
14
16
  url(path) { return joinUrl(this.o.consoleUrl, `/api/deploy/v1${path}`); }
15
17
  headers(extra = {}) {
@@ -44,6 +46,8 @@ export class DeployClient {
44
46
  return (await this.call(`/deployments/${id}`, { retries: 2 })).deployment;
45
47
  }
46
48
  }
49
+ /** Üst üste bu kadar yoklama hatasından sonra bırakılır. */
50
+ const MAX_POLL_FAILURES = 5;
47
51
  const TERMINAL = new Set(['live', 'failed', 'expired', 'superseded']);
48
52
  export const isTerminal = (s) => TERMINAL.has(s);
49
53
  /** Durum değiştikçe onStatus çağırarak terminal duruma kadar bekler. */
@@ -53,8 +57,24 @@ export async function waitForDeployment(client, id, opts) {
53
57
  const start = now();
54
58
  let last = '';
55
59
  let interval = opts.intervalMs ?? 2000;
60
+ let failures = 0;
56
61
  for (;;) {
57
- const v = await client.status(id);
62
+ let v;
63
+ try {
64
+ v = await client.status(id);
65
+ failures = 0;
66
+ }
67
+ catch (e) {
68
+ // Geçici hata (ağ, 5xx): sunucu dağıtımı yayınlıyor olabilir, tek bir yoklama hatası CI'ı kırmasın.
69
+ // Kalıcı hatalar (geçersiz anahtar, 404…) kodla gelir ve hemen iletilir.
70
+ const transient = e instanceof CliError && (!e.code || e.code === 'deploy_unavailable');
71
+ if (!transient || ++failures >= MAX_POLL_FAILURES)
72
+ throw e;
73
+ if (now() - start > opts.timeoutMs)
74
+ throw e;
75
+ await sleep(interval);
76
+ continue;
77
+ }
58
78
  if (v.status !== last) {
59
79
  last = v.status;
60
80
  opts.onStatus?.(v.status);
@@ -94,14 +114,17 @@ function gitCmd(cwd, args) {
94
114
  }
95
115
  }
96
116
  /**
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.
117
+ * Idempotency-Key: CI'da koşu + deneme numarası + arşiv sağlama toplamının başı (yeniden çalıştırma yeni anahtar alır,
118
+ * 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ı
119
+ * paylaşmaz); yerelde her çalıştırma için rastgele.
99
120
  */
100
- export function idempotencyKey(env, rand = randomUUID) {
121
+ export function idempotencyKey(env, rand = randomUUID, contentSha) {
122
+ const tail = contentSha ? `-${contentSha.slice(0, 12)}` : '';
123
+ const fit = (base) => `${base.slice(0, 64 - tail.length)}${tail}`;
101
124
  if (env.GITHUB_RUN_ID)
102
- return `gh-${env.GITHUB_RUN_ID}-${env.GITHUB_RUN_ATTEMPT ?? '1'}`.slice(0, 64);
125
+ return fit(`gh-${env.GITHUB_RUN_ID}-${env.GITHUB_RUN_ATTEMPT ?? '1'}`);
103
126
  if (env.CI_PIPELINE_ID)
104
- return `gl-${env.CI_PIPELINE_ID}-${env.CI_JOB_ID ?? '0'}`.slice(0, 64);
127
+ return fit(`gl-${env.CI_PIPELINE_ID}-${env.CI_JOB_ID ?? '0'}`);
105
128
  return `cli-${rand()}`.slice(0, 64);
106
129
  }
107
130
  const HINTS = {
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(),
@@ -33,7 +33,13 @@ export function writeZipFile(files, outPath) {
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);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@enformik/cli",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "Enformik geliştirici CLI'si: web sitesi dağıtımı ve uygulama modülü (şema) yükseltme.",
5
5
  "license": "MIT",
6
6
  "type": "module",