@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.
@@ -1,7 +1,7 @@
1
1
  import { CliError } from '../errors.js';
2
2
  import { assertSecureUrl, joinUrl, request } from '../http.js';
3
3
  import { DeployClient } from '../site.js';
4
- import { prepare } from './schema.js';
4
+ import { assertExpectedAppHost, prepare } from './schema.js';
5
5
  const ICON = { ok: '✓', warn: '!', fail: '✗', skip: '-' };
6
6
  /** Web kullanıcısına (external) açık sayılan kural: rol "external" ya da özel rol; en az bir okuma eylemi. */
7
7
  function grantsWebRead(r) {
@@ -14,9 +14,9 @@ function grantsWebRead(r) {
14
14
  return null;
15
15
  }
16
16
  /**
17
- * enformik app check [--entity a,b] — web uygulamasının Enformik'le entegrasyon sağlığını SALT OKUNUR denetler.
18
- * Site tarafı (ENFORMIK_CONSOLE_URL + ENFORMIK_DEPLOY_KEY) ve uygulama tarafı (ENFORMIK_APP_URL + istemci) bağımsızdır;
19
- * kimliği olmayan taraf atlanır. Çıkış: engelleyici sorun 1, yalnız uyarı 2, hepsi tamam 0.
17
+ * enformik app check [--entity a,b] — READ-ONLY check of the web app's integration health with Enformik.
18
+ * The site side (ENFORMIK_CONSOLE_URL + ENFORMIK_DEPLOY_KEY) and the app side (ENFORMIK_APP_URL + client) are independent;
19
+ * a side without credentials is skipped. Exit: blocking problem 1, warnings only 2, all OK 0.
20
20
  */
21
21
  export async function appCheck(ctx) {
22
22
  const { settings, out } = ctx;
@@ -24,99 +24,116 @@ export async function appCheck(ctx) {
24
24
  const hasSite = !!(settings.consoleUrl && settings.deployKey);
25
25
  const hasApp = !!(settings.appUrl && settings.clientId && settings.clientSecret);
26
26
  if (!hasSite && !hasApp) {
27
- throw new CliError('Denetlenecek kimlik yok. Site için ENFORMIK_CONSOLE_URL + ENFORMIK_DEPLOY_KEY, uygulama için ENFORMIK_APP_URL + ENFORMIK_CLIENT_ID/SECRET tanımlayın.');
27
+ throw new CliError('No credentials to check. Set ENFORMIK_CONSOLE_URL + ENFORMIK_DEPLOY_KEY for the site, and ENFORMIK_APP_URL + ENFORMIK_CLIENT_ID/SECRET for the app.');
28
28
  }
29
+ // --expect-host is about the APP host (as in the schema commands); the site half ignores it. Checked first, so a mismatch stops the whole
30
+ // command (a hard error, not a check entry) before ANY request is sent, including the site half's GET /site.
31
+ if (settings.appUrl && typeof ctx.flags['expect-host'] === 'string')
32
+ assertExpectedAppHost(ctx, assertSecureUrl(settings.appUrl, 'App URL'));
29
33
  // ── Site: yayınlanmış çalışma zamanı yapılandırması ──────────────────────────────────────────
30
34
  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.' });
35
+ if (typeof ctx.flags['expect-host'] === 'string')
36
+ out.warn('Note: --expect-host checks the app host only; the site side is not checked against it.');
37
+ try {
38
+ const info = await new DeployClient({ consoleUrl: settings.consoleUrl, deployKey: settings.deployKey, version: ctx.version, fetchImpl: ctx.fetchImpl }).getSite();
39
+ if (!info.live) {
40
+ checks.push({ id: 'site.live', status: 'warn', message: 'The site has no live deployment yet; the sign-in configuration could not be checked.', hint: 'Publish it with `enformik site deploy`.' });
46
41
  }
47
42
  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.' });
43
+ assertSecureUrl(info.url, 'Site URL');
44
+ let cfg;
45
+ try {
46
+ const res = await request(joinUrl(info.url, '/_enformik/config.json'), { retries: 1, fetchImpl: ctx.fetchImpl });
47
+ if (res.ok && res.json && typeof res.json === 'object')
48
+ cfg = res.json;
49
+ }
50
+ catch { /* ulaşılamadı → aşağıda uyarı */ }
51
+ if (!cfg) {
52
+ checks.push({ id: 'site.config', status: 'warn', message: `Could not read ${info.url}/_enformik/config.json.`, hint: 'Is the site live? Try redeploying.' });
51
53
  }
52
54
  else {
53
- checks.push({ id: 'site.auth', status: 'ok', message: 'Site girişi açık; yayınlanmış yapılandırmada giriş bilgisi var.' });
55
+ const auth = cfg.auth;
56
+ if (!auth || !auth.clientId) {
57
+ checks.push({ id: 'site.auth', status: 'fail', message: 'Site sign-in is off: the published configuration has no sign-in (auth) information.', hint: 'Enable sign-in under Console → Developer site → Site sign-in, then redeploy the site.' });
58
+ }
59
+ else {
60
+ checks.push({ id: 'site.auth', status: 'ok', message: 'Site sign-in is on; the published configuration has sign-in information.' });
61
+ }
62
+ const ok = typeof cfg.appUrl === 'string' && typeof cfg.apiUrl === 'string' && !!cfg.workspaceId;
63
+ checks.push(ok ? { id: 'site.binding', status: 'ok', message: 'The site is linked to an app and a workspace.' }
64
+ : { id: 'site.binding', status: 'fail', message: 'The configuration is missing the app/workspace link.', hint: 'Check the developer site\'s app link in the console.' });
54
65
  }
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
66
  }
59
67
  }
68
+ catch (e) {
69
+ // Bir yarının hatası diğer yarıyı ve checks[] çıktısını kaybettirmesin.
70
+ checks.push({ id: 'site.error', status: 'fail', message: `The site check could not be completed: ${e.message}` });
71
+ }
60
72
  }
61
73
  else {
62
- checks.push({ id: 'site', status: 'skip', message: 'Site denetimi atlandı (ENFORMIK_CONSOLE_URL + ENFORMIK_DEPLOY_KEY yok).' });
74
+ checks.push({ id: 'site', status: 'skip', message: 'Site check skipped (ENFORMIK_CONSOLE_URL + ENFORMIK_DEPLOY_KEY not set).' });
63
75
  }
64
76
  // ── Uygulama: modül, erişim modu ve web kullanıcısı erişimi ──────────────────────────────────
65
77
  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.' });
78
+ try {
79
+ const p = await prepare(ctx, 'validate');
80
+ const slug = p.manifest.module.slug;
81
+ const mod = await p.client.findModule(slug);
82
+ if (!mod) {
83
+ checks.push({ id: 'module.installed', status: 'fail', message: `Module "${slug}" is not installed in the app.`, hint: 'Install it with `enformik schema install` (Schema builder).' });
88
84
  }
89
- else {
90
- checks.push({ id: 'access.mode', status: 'ok', message: 'Kayıt erişim kuralları etkin.' });
85
+ else if (mod.version !== p.manifest.module.version) {
86
+ checks.push({ id: 'module.version', status: 'warn', message: `The module version in the app is ${mod.version}; the manifest is ${p.manifest.module.version}.`, hint: 'Sync them with `enformik schema diff` / `schema push`.' });
91
87
  }
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).' });
88
+ else {
89
+ checks.push({ id: 'module.version', status: 'ok', message: `Module ${slug} ${mod.version} is installed and matches the manifest version.` });
106
90
  }
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.' });
91
+ const wanted = typeof ctx.flags.entity === 'string' && ctx.flags.entity !== ''
92
+ ? ctx.flags.entity.split(',').map((s) => s.trim().toLowerCase()).filter(Boolean)
93
+ : (p.manifest.entities ?? []).map((e) => (e.slug ?? '').toLowerCase()).filter(Boolean);
94
+ const summary = await p.client.accessSummary(wanted);
95
+ if (!summary) {
96
+ checks.push({ id: 'access', status: 'warn', message: 'This app version does not support the access summary endpoint; web user access could not be checked.', hint: 'Try again once the app is updated.' });
109
97
  }
110
98
  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ı).' });
99
+ if (summary.mode !== 'on') {
100
+ checks.push({ id: 'access.mode', status: 'fail', message: `Record access rules are in "${summary.mode || 'unknown'}" mode; web users do not work in this mode.`, hint: 'Open a support ticket from the Enformik management console to have it enabled.' });
101
+ }
102
+ else {
103
+ checks.push({ id: 'access.mode', status: 'ok', message: 'Record access rules are enabled.' });
104
+ }
105
+ const byName = new Map(summary.entities.map((e) => [e.entity.toLowerCase(), e.rules]));
106
+ const none = [];
107
+ const custom = [];
108
+ for (const name of wanted) {
109
+ const kinds = (byName.get(name) ?? []).map(grantsWebRead);
110
+ if (kinds.includes('external'))
111
+ continue;
112
+ if (kinds.includes('custom'))
113
+ custom.push(name);
114
+ else
115
+ none.push(name);
116
+ }
117
+ if (wanted.length === 0) {
118
+ checks.push({ id: 'access.web', status: 'skip', message: 'No entities to check (no entities in the manifest and no --entity).' });
119
+ }
120
+ else if (none.length > 0) {
121
+ checks.push({ id: 'access.web', status: 'warn', message: `Web users cannot see data in these tables (no read rule): ${none.join(', ')}.`, hint: 'An administrator must define a read rule for "Web User" (or a custom role) on the Record Access screen of those tables; a low-role client cannot apply access rules.' });
122
+ }
123
+ else {
124
+ checks.push({ id: 'access.web', status: 'ok', message: `Read rules exist for web users (${wanted.length - custom.length} tables).` });
125
+ }
126
+ if (custom.length > 0) {
127
+ checks.push({ id: 'access.custom', status: 'warn', message: `These tables have only a custom role rule: ${custom.join(', ')}.`, hint: 'Make sure the relevant custom role is assigned to web users (the default role for new users, or an administrator assignment).' });
128
+ }
115
129
  }
116
130
  }
131
+ catch (e) {
132
+ checks.push({ id: 'app.error', status: 'fail', message: `The app check could not be completed: ${e.message}` });
133
+ }
117
134
  }
118
135
  else {
119
- checks.push({ id: 'app', status: 'skip', message: 'Uygulama denetimi atlandı (ENFORMIK_APP_URL + ENFORMIK_CLIENT_ID/SECRET yok).' });
136
+ checks.push({ id: 'app', status: 'skip', message: 'App check skipped (ENFORMIK_APP_URL + ENFORMIK_CLIENT_ID/SECRET not set).' });
120
137
  }
121
138
  for (const c of checks) {
122
139
  const paint = c.status === 'ok' ? 'green' : c.status === 'fail' ? 'red' : c.status === 'warn' ? 'yellow' : 'dim';
@@ -5,7 +5,7 @@ import { CliError } from '../errors.js';
5
5
  import { parseManifest } from '../schema.js';
6
6
  /**
7
7
  * enformik codegen [--manifest module.json] [--out src/enformik.generated.ts] [--check]
8
- * Yerel manifestten TypeScript tipleri + tipli veri istemcisi üretir; ağ/kimlik gerekmez. --check: dosya güncel değilse çıkış 2.
8
+ * Generates TypeScript types + a typed data client from the local manifest; no network or credentials needed. --check: exit 2 if the file is out of date.
9
9
  */
10
10
  export async function codegen(ctx) {
11
11
  const manifestPath = resolve(ctx.cwd, ctx.settings.manifestPath);
@@ -14,26 +14,29 @@ export async function codegen(ctx) {
14
14
  text = readFileSync(manifestPath, 'utf8');
15
15
  }
16
16
  catch {
17
- throw new CliError(`Manifest okunamadı: ${manifestPath}`);
17
+ throw new CliError(`Could not read the manifest: ${manifestPath}`);
18
18
  }
19
19
  const manifest = parseManifest(text, ctx.settings.manifestPath);
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;
26
- ctx.out.info(upToDate ? `✓ ${ctx.settings.codegenOut} güncel (${slug}).` : `✗ ${ctx.settings.codegenOut} ${current === null ? 'yok' : 'manifestle uyumsuz'}; \`enformik codegen\` ile yeniden üretin.`);
28
+ const upToDate = same;
29
+ ctx.out.info(upToDate ? `✓ ${ctx.settings.codegenOut} up to date (${slug}).` : `✗ ${ctx.settings.codegenOut} ${current === null ? 'is missing' : 'does not match the manifest'}; regenerate it with \`enformik codegen\`.`);
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);
34
37
  }
35
38
  const n = (code.match(/^export interface \w+Input /gm) ?? []).length;
36
- ctx.out.info(changed ? `✓ ${ctx.settings.codegenOut} yazıldı (${slug}, ${n} entity).` : `= ${ctx.settings.codegenOut} zaten güncel (${slug}, ${n} entity).`);
39
+ ctx.out.info(changed ? `✓ ${ctx.settings.codegenOut} written (${slug}, ${n} entities).` : `= ${ctx.settings.codegenOut} already up to date (${slug}, ${n} entities).`);
37
40
  ctx.out.result({ module: slug, out: ctx.settings.codegenOut, entities: n, written: changed });
38
41
  return 0;
39
42
  }
@@ -15,10 +15,10 @@ export async function link(ctx) {
15
15
  return cur;
16
16
  return (await ctx.ask(label, cur)) || undefined;
17
17
  };
18
- next.consoleUrl = await prompt('Konsol adresi (site dağıtımı için; boş bırakılabilir)', config.consoleUrl, str('console-url'));
19
- next.appUrl = await prompt('Uygulama adresi (şema yükseltme için; boş bırakılabilir)', config.appUrl, str('app-url'));
20
- next.workspaceId = await prompt('Çalışma alanı kimliği (isteğe bağlı)', config.workspaceId, str('workspace'));
21
- const slug = await prompt('Modül slug (şema yükseltme için)', config.module?.slug, str('module'));
18
+ next.consoleUrl = await prompt('Console URL (for site deployment; may be left empty)', config.consoleUrl, str('console-url'));
19
+ next.appUrl = await prompt('App URL (for schema upgrades; may be left empty)', config.appUrl, str('app-url'));
20
+ next.workspaceId = await prompt('Workspace ID (optional)', config.workspaceId, str('workspace'));
21
+ const slug = await prompt('Module slug (for schema upgrades)', config.module?.slug, str('module'));
22
22
  const manifest = str('manifest') ?? config.module?.manifest;
23
23
  const dir = str('dir') ?? config.site?.dir;
24
24
  if (slug || manifest)
@@ -26,26 +26,26 @@ export async function link(ctx) {
26
26
  if (dir)
27
27
  next.site = { ...(config.site ?? {}), dir };
28
28
  if (!next.consoleUrl && !next.appUrl)
29
- throw new CliError('En az bir adres gerekli: --console-url (site dağıtımı) ve/ya da --app-url (şema yükseltme).');
29
+ throw new CliError('At least one URL is required: --console-url (site deployment) and/or --app-url (schema upgrades).');
30
30
  if (next.consoleUrl)
31
- assertSecureUrl(next.consoleUrl, 'Konsol adresi');
31
+ assertSecureUrl(next.consoleUrl, 'Console URL');
32
32
  if (next.appUrl) {
33
- assertSecureUrl(next.appUrl, 'Uygulama adresi');
33
+ assertSecureUrl(next.appUrl, 'App URL');
34
34
  if (flags['no-verify'] !== true) {
35
35
  await discoverApp(next.appUrl, ctx.version, ctx.fetchImpl); // erişilebilir mi / API istemcisi kabul ediyor mu
36
- out.info('✓ Uygulama erişilebilir.');
36
+ out.info('✓ The app is reachable.');
37
37
  }
38
38
  }
39
39
  for (const k of Object.keys(next))
40
40
  if (next[k] === undefined)
41
41
  delete next[k];
42
42
  const path = saveConfig(root, next);
43
- out.info(`Yapılandırma yazıldı: ${path}`);
44
- out.info('Sırlar dosyaya yazılmaz; ortam değişkeni olarak verin:');
43
+ out.info(`Config written: ${path}`);
44
+ out.info('Secrets are not written to the file; provide them as environment variables:');
45
45
  if (next.consoleUrl)
46
- out.info(' ENFORMIK_DEPLOY_KEY (site dağıtımı: dağıtım anahtarı)');
46
+ out.info(' ENFORMIK_DEPLOY_KEY (site deployment: deploy key)');
47
47
  if (next.appUrl)
48
- out.info(' ENFORMIK_CLIENT_ID / ENFORMIK_CLIENT_SECRET (şema yükseltme: API istemcisi)');
48
+ out.info(' ENFORMIK_CLIENT_ID / ENFORMIK_CLIENT_SECRET (schema upgrades: API client)');
49
49
  out.result({ linked: true, path, config: next });
50
50
  return 0;
51
51
  }
@@ -3,56 +3,64 @@ 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
+ /** --expect-host: the app's host name must match (case-insensitive); checked before any request so nothing reaches the wrong app. */
9
+ export function assertExpectedAppHost(ctx, appUrl) {
10
+ const expect = ctx.flags['expect-host'];
11
+ if (typeof expect === 'string' && expect.toLowerCase() !== appUrl.hostname.toLowerCase()) {
12
+ throw new CliError(`Expected app "${expect}" but the target is "${appUrl.hostname}". Stopped so that nothing is applied to the wrong app.`);
13
+ }
14
+ }
8
15
  /** Ortak hazırlık: manifest → uygulama keşfi → belirteç → modül kimliği. */
9
16
  export async function prepare(ctx, mode = 'existing') {
10
17
  const { settings, out } = ctx;
11
18
  if (!settings.appUrl)
12
- throw new CliError('Uygulama adresi yok. `enformik link --app-url https://…` çalıştırın ya da ENFORMIK_APP_URL tanımlayın.');
19
+ throw new CliError('No app URL. Run `enformik link --app-url https://…` or set ENFORMIK_APP_URL.');
13
20
  if (!settings.clientId || !settings.clientSecret) {
14
- throw new CliError('ENFORMIK_CLIENT_ID ve ENFORMIK_CLIENT_SECRET tanımlı olmalı (konsolda "API anahtarları" sayfasından rolü "Şema yükseltici" — ilk kurulum için "Şema oluşturucu" — olan bir istemci oluşturun).');
15
- }
16
- const appUrl = assertSecureUrl(settings.appUrl, 'Uygulama adresi');
17
- const expect = ctx.flags['expect-host'];
18
- if (typeof expect === 'string' && expect.toLowerCase() !== appUrl.hostname.toLowerCase()) {
19
- throw new CliError(`Beklenen uygulama "${expect}" ama hedef "${appUrl.hostname}". Yanlış uygulamaya işlem yapılmaması için durduruldu.`);
21
+ throw new CliError('ENFORMIK_CLIENT_ID and ENFORMIK_CLIENT_SECRET must be set (in the console, create a client with the "Schema upgrader" role — or "Schema builder" for the first installation — on the "API keys" page).');
20
22
  }
23
+ const appUrl = assertSecureUrl(settings.appUrl, 'App URL');
24
+ assertExpectedAppHost(ctx, appUrl);
21
25
  const manifestPath = resolve(ctx.cwd, settings.manifestPath);
22
26
  let text;
23
27
  try {
24
- text = readFileSync(manifestPath, 'utf8');
28
+ text = stripBom(readFileSync(manifestPath, 'utf8'));
25
29
  }
26
30
  catch {
27
- throw new CliError(`Manifest okunamadı: ${manifestPath}`);
31
+ throw new CliError(`Could not read the manifest: ${manifestPath}`);
28
32
  }
29
33
  const manifest = parseManifest(text, settings.manifestPath);
34
+ // Sunucuya dosyanın kendi metni gider (JSON.stringify yeniden serileştirmesi büyük sayıları/biçimi değiştirebilirdi).
35
+ const manifestJson = text;
30
36
  const slug = settings.moduleSlug ?? manifest.module.slug;
31
37
  if (settings.moduleSlug && settings.moduleSlug !== manifest.module.slug) {
32
- throw new CliError(`Yapılandırmadaki modül ("${settings.moduleSlug}") ile manifestteki slug ("${manifest.module.slug}") farklı.`);
38
+ throw new CliError(`The module in the config ("${settings.moduleSlug}") differs from the slug in the manifest ("${manifest.module.slug}").`);
33
39
  }
34
40
  const seedData = ctx.flags['seed-data'] === true;
35
41
  const admin = adminOnlySections(manifest, { seedData });
36
42
  if (admin.length > 0) {
37
- out.warn(`Not: manifest yönetici gerektiren bölümler içeriyor (${admin.join(', ')}). "Şema yükseltici/oluşturucu" rolündeki bir istemci bunu uygulayamaz.`);
43
+ out.warn(`Note: the manifest contains sections that require an administrator (${admin.join(', ')}). A client with the "Schema upgrader/builder" role cannot apply them.`);
38
44
  }
39
45
  const app = await discoverApp(settings.appUrl, ctx.version, ctx.fetchImpl);
40
46
  const workspaceId = settings.workspaceId || app.tenantId;
41
47
  if (!workspaceId)
42
- throw new CliError('Çalışma alanı belirlenemedi. --workspace <kimlik> verin ya da `enformik link --workspace` ile kaydedin.');
48
+ throw new CliError('Could not determine the workspace. Pass --workspace <id> or save it with `enformik link --workspace`.');
49
+ // 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.
50
+ out.info(`Identity provider: ${new URL(app.oidcAuthority).host} (the client credentials are sent here)`);
43
51
  const tok = await fetchAccessToken(app, settings.clientId, settings.clientSecret, ctx.version, ctx.fetchImpl);
44
52
  const client = new SchemaClient({ appUrl: settings.appUrl, token: tok.token, workspaceId, version: ctx.version, fetchImpl: ctx.fetchImpl });
45
53
  if (mode === 'validate') {
46
- return { client, manifest, manifestJson: JSON.stringify(manifest), moduleId: '', installedVersion: '', appHost: appUrl.hostname, seedData };
54
+ return { client, manifest, manifestJson, moduleId: '', installedVersion: '', appHost: appUrl.hostname, seedData };
47
55
  }
48
56
  if (mode === 'install') {
49
57
  const existing = await client.findModule(slug);
50
58
  if (existing)
51
- 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 };
59
+ throw new CliError(`Module "${slug}" is already installed in this app (version ${existing.version}). Use \`enformik schema push\` to upgrade it.`);
60
+ return { client, manifest, manifestJson, moduleId: '', installedVersion: '', appHost: appUrl.hostname, seedData };
53
61
  }
54
62
  const mod = await client.resolveModule(slug);
55
- return { client, manifest, manifestJson: JSON.stringify(manifest), moduleId: mod.id, installedVersion: mod.version, appHost: appUrl.hostname, seedData };
63
+ return { client, manifest, manifestJson, moduleId: mod.id, installedVersion: mod.version, appHost: appUrl.hostname, seedData };
56
64
  }
57
65
  function summary(p, r) {
58
66
  return {
@@ -69,7 +77,7 @@ function summary(p, r) {
69
77
  export async function schemaDiff(ctx) {
70
78
  const p = await prepare(ctx);
71
79
  const r = await p.client.diff(p.moduleId, p.manifestJson, { seedData: p.seedData });
72
- ctx.out.info(`Modül: ${p.manifest.module.slug} kurulu ${p.installedVersion} → manifest ${p.manifest.module.version}`);
80
+ ctx.out.info(`Module: ${p.manifest.module.slug} installed ${p.installedVersion} → manifest ${p.manifest.module.version}`);
73
81
  ctx.out.info(renderDiff(ctx.out, r, { appHost: p.appHost }));
74
82
  const s = summary(p, r);
75
83
  ctx.out.result(s);
@@ -83,7 +91,7 @@ export async function schemaDiff(ctx) {
83
91
  export async function schemaPush(ctx) {
84
92
  const p = await prepare(ctx);
85
93
  const dry = await p.client.diff(p.moduleId, p.manifestJson, { seedData: p.seedData });
86
- ctx.out.info(`Modül: ${p.manifest.module.slug} kurulu ${p.installedVersion} → manifest ${p.manifest.module.version}`);
94
+ ctx.out.info(`Module: ${p.manifest.module.slug} installed ${p.installedVersion} → manifest ${p.manifest.module.version}`);
87
95
  ctx.out.info(renderDiff(ctx.out, dry, { appHost: p.appHost }));
88
96
  if (dry.errors.length > 0) {
89
97
  ctx.out.result({ ...summary(p, dry), applied: false });
@@ -92,18 +100,18 @@ export async function schemaPush(ctx) {
92
100
  const pending = pendingChanges(dry.diffs);
93
101
  // Sürüm farkı olsa bile yapısal değişiklik yoksa uygulanacak bir şey yok.
94
102
  if (pending.length === 0) {
95
- ctx.out.info('Uygulanacak değişiklik yok.');
103
+ ctx.out.info('No changes to apply.');
96
104
  ctx.out.result({ ...summary(p, dry), applied: false });
97
105
  return 0;
98
106
  }
99
107
  const tc = typeChanges(dry.diffs);
100
108
  if (tc.length > 0)
101
- ctx.out.warn(`Alan türü değişiyor (${tc.map((d) => d.slug).join(', ')}): bu işlem yalnızca yönetici rolüyle uygulanabilir.`);
109
+ ctx.out.warn(`Field type is changing (${tc.map((d) => d.slug).join(', ')}): this can only be applied with the administrator role.`);
102
110
  if (ctx.flags.yes !== true) {
103
111
  if (!ctx.isTTY)
104
- throw new CliError('Etkileşimli terminal yok: değişiklikleri uygulamak için --yes verin (sessizce onaylanmaz).');
105
- if (!(await ctx.confirm('Değişiklikleri uygulamak istiyor musunuz?'))) {
106
- ctx.out.info('Vazgeçildi; hiçbir değişiklik uygulanmadı.');
112
+ throw new CliError('No interactive terminal: pass --yes to apply the changes (nothing is confirmed silently).');
113
+ if (!(await ctx.confirm('Do you want to apply the changes?'))) {
114
+ ctx.out.info('Cancelled; no changes were applied.');
107
115
  ctx.out.result({ ...summary(p, dry), applied: false });
108
116
  return 1;
109
117
  }
@@ -111,20 +119,27 @@ export async function schemaPush(ctx) {
111
119
  const res = await p.client.upgrade(p.moduleId, p.manifestJson, { seedData: p.seedData });
112
120
  if (res.errors.length > 0) {
113
121
  for (const e of res.errors)
114
- ctx.out.error(`Hata: ${e}`);
122
+ ctx.out.error(`Error: ${e}`);
115
123
  ctx.out.result({ ...summary(p, res), applied: false });
116
124
  return 1;
117
125
  }
118
- 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}).`));
126
+ ctx.out.info(ctx.out.paint('green', `✓ Applied: ${pendingChanges(res.diffs).length || pending.length} changes (module ${p.manifest.module.slug} ${p.manifest.module.version}).`));
127
+ // 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.
128
+ const extra = unapprovedChanges(dry.diffs, res.diffs);
129
+ if (extra.length > 0) {
130
+ ctx.out.error(`Warning: ${extra.length} change(s) that were not in the diff you approved were also applied (the app may have changed between diff and upgrade): ${extra.map((d) => `${d.resourceType} ${d.slug} (${d.action})`).join(', ')}. Verify the result with \`enformik schema diff\`.`);
131
+ ctx.out.result({ ...summary(p, res), applied: true, diverged: true, unapproved: extra });
132
+ return 1;
133
+ }
119
134
  ctx.out.result({ ...summary(p, res), applied: true });
120
135
  return 0;
121
136
  }
122
- /** enformik schema install [--yes] [--dry-run] — modülün ilk kurulumu (rol: Şema oluşturucu ya da Yönetici). */
137
+ /** enformik schema install [--yes] [--dry-run] — first installation of the module (role: Schema builder or Administrator). */
123
138
  export async function schemaInstall(ctx) {
124
139
  const p = await prepare(ctx, 'install');
125
140
  const slug = p.manifest.module.slug;
126
141
  const dry = await p.client.install(p.manifestJson, { seedData: p.seedData, dryRun: true });
127
- ctx.out.info(`Modül: ${slug} yeni kurulum → manifest ${p.manifest.module.version}`);
142
+ ctx.out.info(`Module: ${slug} new installation → manifest ${p.manifest.module.version}`);
128
143
  ctx.out.info(renderDiff(ctx.out, dry, { appHost: p.appHost }));
129
144
  const base = { module: slug, manifestVersion: p.manifest.module.version, changes: pendingChanges(dry.diffs).length, diffs: dry.diffs, errors: dry.errors };
130
145
  if (dry.errors.length > 0) {
@@ -137,9 +152,9 @@ export async function schemaInstall(ctx) {
137
152
  }
138
153
  if (ctx.flags.yes !== true) {
139
154
  if (!ctx.isTTY)
140
- throw new CliError('Etkileşimli terminal yok: modülü kurmak için --yes verin (sessizce onaylanmaz).');
141
- if (!(await ctx.confirm('Modülü kurmak istiyor musunuz?'))) {
142
- ctx.out.info('Vazgeçildi; hiçbir şey kurulmadı.');
155
+ throw new CliError('No interactive terminal: pass --yes to install the module (nothing is confirmed silently).');
156
+ if (!(await ctx.confirm('Do you want to install the module?'))) {
157
+ ctx.out.info('Cancelled; nothing was installed.');
143
158
  ctx.out.result({ ...base, installed: false });
144
159
  return 1;
145
160
  }
@@ -147,11 +162,11 @@ export async function schemaInstall(ctx) {
147
162
  const res = await p.client.install(p.manifestJson, { seedData: p.seedData, dryRun: false });
148
163
  if (res.errors.length > 0) {
149
164
  for (const e of res.errors)
150
- ctx.out.error(`Hata: ${e}`);
165
+ ctx.out.error(`Error: ${e}`);
151
166
  ctx.out.result({ ...base, diffs: res.diffs, errors: res.errors, installed: false });
152
167
  return 1;
153
168
  }
154
- ctx.out.info(ctx.out.paint('green', `✓ Kuruldu: modül ${slug} ${p.manifest.module.version} (${pendingChanges(res.diffs).length} kaynak). Sonraki değişiklikler için \`enformik schema push\` kullanın.`));
169
+ ctx.out.info(ctx.out.paint('green', `✓ Installed: module ${slug} ${p.manifest.module.version} (${pendingChanges(res.diffs).length} resources). Use \`enformik schema push\` for later changes.`));
155
170
  ctx.out.result({ ...base, moduleId: res.moduleId, diffs: res.diffs, errors: [], installed: true });
156
171
  return 0;
157
172
  }
@@ -163,7 +178,7 @@ export async function schemaInstall(ctx) {
163
178
  export async function schemaValidate(ctx) {
164
179
  const profileFlag = ctx.flags.profile;
165
180
  if (profileFlag !== undefined && profileFlag !== 'import' && profileFlag !== 'ai') {
166
- throw new CliError(`--profile "import" ya da "ai" olmalı (verilen: ${String(profileFlag)}).`);
181
+ throw new CliError(`--profile must be "import" or "ai" (got: ${String(profileFlag)}).`);
167
182
  }
168
183
  const profile = profileFlag === 'ai' ? 'ai' : 'import';
169
184
  const seedData = ctx.flags['seed-data'] === true;
@@ -171,19 +186,22 @@ export async function schemaValidate(ctx) {
171
186
  let source;
172
187
  let slug;
173
188
  if (ctx.flags.offline === true) {
189
+ // ai profilinin kuralları (isimlendirme/yapı) yalnızca sunucuda denetlenir; yerel denetimle "geçerli" demek yanlış güven verir.
190
+ if (profileFlag === 'ai')
191
+ throw new CliError('--offline cannot be combined with --profile ai: the ai profile rules are checked only on the server. Run without --offline.');
174
192
  const manifestPath = resolve(ctx.cwd, ctx.settings.manifestPath);
175
193
  let text;
176
194
  try {
177
195
  text = readFileSync(manifestPath, 'utf8');
178
196
  }
179
197
  catch {
180
- throw new CliError(`Manifest okunamadı: ${manifestPath}`);
198
+ throw new CliError(`Could not read the manifest: ${manifestPath}`);
181
199
  }
182
200
  const manifest = parseManifest(text, ctx.settings.manifestPath); // JSON + module.slug/version
183
201
  slug = manifest.module.slug;
184
202
  result = { valid: true, profile, issues: [], adminSections: adminOnlySections(manifest, { seedData }).map((section) => ({ section })) };
185
203
  source = 'local';
186
- ctx.out.warn('Yalnızca yerel denetim yapıldı (JSON, module.slug/version, yönetici bölümleri); isimlendirme ve yapı kuralları sunucuda denetlenir.');
204
+ ctx.out.warn('Only local checks were run (JSON, module.slug/version, administrator sections); naming and structure rules are checked on the server.');
187
205
  }
188
206
  else {
189
207
  const p = await prepare(ctx, 'validate');
@@ -191,14 +209,16 @@ export async function schemaValidate(ctx) {
191
209
  result = await p.client.validate(p.manifestJson, { profile, seedData });
192
210
  source = 'server';
193
211
  }
194
- ctx.out.info(`Modül: ${slug} profil: ${profile} (${source === 'server' ? 'sunucu doğrulaması' : 'yerel'})`);
212
+ ctx.out.info(`Module: ${slug} profile: ${profile} (${source === 'server' ? 'server validation' : 'local'})`);
195
213
  if (result.issues.length > 0) {
196
- ctx.out.info(renderTable(ctx.out, ['KOD', 'AÇIKLAMA'], result.issues.map((i) => [i.code, i.message])));
214
+ ctx.out.info(renderTable(ctx.out, ['CODE', 'DESCRIPTION'], result.issues.map((i) => [i.code, i.message])));
197
215
  }
198
216
  if (result.adminSections.length > 0) {
199
- ctx.out.info(ctx.out.paint('yellow', `Yönetici adımı gerekir (düşük rollü istemci uygulayamaz): ${result.adminSections.map((s) => s.section).join(', ')}`));
217
+ ctx.out.info(ctx.out.paint('yellow', `Administrator step required (a low-role client cannot apply these): ${result.adminSections.map((s) => s.section).join(', ')}`));
200
218
  }
201
- ctx.out.info(result.valid ? ctx.out.paint('green', '✓ Manifest geçerli.') : ctx.out.paint('red', `✗ ${result.issues.length} bulgu var.`));
219
+ ctx.out.info(result.valid
220
+ ? ctx.out.paint('green', source === 'local' ? '✓ Local checks passed (server rules were not checked).' : '✓ Manifest is valid.')
221
+ : ctx.out.paint('red', `✗ ${result.issues.length} issue(s) found.`));
202
222
  ctx.out.result({ module: slug, source, ...result });
203
223
  return result.valid ? 0 : 1;
204
224
  }