@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.
package/README.md CHANGED
@@ -1,3 +1,62 @@
1
- # Temporary Holding Version
1
+ # @enformik/cli
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Enformik geliştirici CLI'si: kendi derlediğiniz **web sitesini** yayınlar ve **uygulama modülünüzü (şema)** yükseltir. CI/CD hatlarında çalışmak için tasarlanmıştır; çalışma zamanı bağımlılığı yoktur (Node.js ≥ 20).
4
+
5
+ ```bash
6
+ npx @enformik/cli --help
7
+ # ya da kalıcı kurulum:
8
+ npm install -g @enformik/cli # komut: enformik
9
+ ```
10
+
11
+ ## Site dağıtımı
12
+
13
+ Gerekenler: Enformik yönetim konsolunda web sitesi ortamınızda **Geliştirici sitesi** etkin ve bir **dağıtım anahtarı** oluşturulmuş olmalı.
14
+
15
+ ```bash
16
+ export ENFORMIK_CONSOLE_URL=https://<konsol-adresiniz>
17
+ export ENFORMIK_DEPLOY_KEY=enf_dk_… # CI gizli değişkeni; depoya yazmayın
18
+
19
+ npx @enformik/cli site deploy --dir dist
20
+ ```
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.
23
+
24
+ ## Şema (modül) yükseltme
25
+
26
+ Gerekenler: uygulamanızda **Şema yükseltici** (ya da Yönetici) rolünde bir **API istemcisi** (konsol → uygulamanın "API erişimi" bölümü) ve uygulamada kurulu bir modül. Modülün **ilk kurulumunu** bir yönetici yapar; sonraki yükseltmeler bu CLI ile yapılır.
27
+
28
+ ```bash
29
+ export ENFORMIK_APP_URL=https://<uygulama-adresiniz>
30
+ export ENFORMIK_CLIENT_ID=apic-…
31
+ export ENFORMIK_CLIENT_SECRET=…
32
+
33
+ npx @enformik/cli schema diff --manifest module.json --check # değişiklik varsa çıkış kodu 2
34
+ npx @enformik/cli schema push --manifest module.json --yes
35
+ ```
36
+
37
+ > **Dikkat:** modül (şema) değişikliği uygulamanın **tüm çalışma alanlarını** etkiler. Canlı uygulamaya laptop'tan değil, CI'dan ve canlıya özel bir istemci ile uygulayın.
38
+
39
+ "Şema yükseltici" rolü betik, otomasyon, yetki tanımı, veri kaynağı, etkileşimli sayfa, arayüz uzantısı ve tohum veri içeren manifestleri ve alan türü değişikliklerini uygulayamaz; bunlar bir yönetici tarafından uygulanır.
40
+
41
+ ## Komutlar
42
+
43
+ | Komut | Açıklama |
44
+ | --- | --- |
45
+ | `link` | `.enformik/config.json` yazar (adresler; **sır içermez**) |
46
+ | `whoami` | Hangi kimlikle, hangi hedefe bağlanıldığını gösterir |
47
+ | `schema diff` | Manifesti uygulamadakiyle karşılaştırır (`--check`) |
48
+ | `schema push` | Değişiklikleri uygular (etkileşimsiz ortamda `--yes` zorunlu) |
49
+ | `site deploy` | Derlenmiş siteyi yayınlar (`--dir`, `--no-wait`, `--timeout`) |
50
+ | `site status` | Hedef site ve canlı sürüm bilgisi |
51
+
52
+ Genel bayraklar: `--json` (stdout'a yalnızca JSON; insan çıktısı stderr'e), `--config`, `--debug`.
53
+ Çıkış kodları: `0` başarı · `1` hata · `2` (`--check`) uygulanacak değişiklik var.
54
+ `--expect-host <ad>` (şema komutları): hedef uygulamanın ana makine adı farklıysa işlemi durdurur (yanlış uygulamaya push'a karşı koruma).
55
+
56
+ ## Güvenlik
57
+
58
+ - Sırlar **yalnızca ortam değişkeniyle** verilir; komut satırı bayrağı veya yapılandırma dosyası yoktur (sır içeren `.enformik/config.json` reddedilir).
59
+ - Kimlik bilgileri yalnızca HTTPS ile gönderilir (`localhost` hariç); yönlendirmeler takip edilmez.
60
+ - Erişim belirteci yalnızca bellekte tutulur.
61
+
62
+ Ayrıntılı rehber: Enformik geliştirici dokümantasyonunda "CLI" sayfası.
package/dist/auth.js ADDED
@@ -0,0 +1,71 @@
1
+ import { ApiError, CliError } from './errors.js';
2
+ import { assertSecureUrl, ensureOk, joinUrl, request } from './http.js';
3
+ export async function discoverApp(appUrl, version, fetchImpl) {
4
+ assertSecureUrl(appUrl, 'Uygulama adresi');
5
+ const res = await request(joinUrl(appUrl, '/api/config'), { retries: 2, headers: { 'X-Enformik-CLI-Version': version }, fetchImpl });
6
+ try {
7
+ ensureOk(res);
8
+ }
9
+ catch (e) {
10
+ if (e instanceof ApiError)
11
+ throw new CliError(`Uygulama yapılandırması okunamadı (HTTP ${e.status}). Adres doğru mu: ${appUrl}?`);
12
+ throw e;
13
+ }
14
+ const j = (res.json ?? {});
15
+ if (!j.oidc_authority)
16
+ throw new CliError('Uygulama yapılandırması beklenen biçimde değil (oidc_authority yok).');
17
+ if (!j.api_audience) {
18
+ throw new CliError('Bu uygulama API istemcisi belirteçlerini kabul etmiyor (api_audience boş). Destek kaydı açın.');
19
+ }
20
+ return { oidcAuthority: j.oidc_authority, apiAudience: j.api_audience, tenantId: j.tenant_id ?? '' };
21
+ }
22
+ export function buildScope(audience) {
23
+ return `openid urn:zitadel:iam:org:projects:roles urn:zitadel:iam:org:project:id:${audience}:aud`;
24
+ }
25
+ export async function fetchAccessToken(app, clientId, clientSecret, version, fetchImpl) {
26
+ const url = joinUrl(app.oidcAuthority, '/oauth/v2/token');
27
+ assertSecureUrl(url, 'Kimlik sağlayıcı adresi');
28
+ const res = await request(url, {
29
+ method: 'POST',
30
+ headers: {
31
+ 'Content-Type': 'application/x-www-form-urlencoded',
32
+ Authorization: 'Basic ' + Buffer.from(`${clientId}:${clientSecret}`).toString('base64'),
33
+ 'X-Enformik-CLI-Version': version,
34
+ },
35
+ body: new URLSearchParams({ grant_type: 'client_credentials', scope: buildScope(app.apiAudience) }),
36
+ retries: 1,
37
+ fetchImpl,
38
+ });
39
+ if (!res.ok) {
40
+ if (res.status === 400 || res.status === 401) {
41
+ throw new CliError('Kimlik doğrulanamadı: istemci kimliği/sırrı geçersiz ya da istemci iptal edilmiş. Konsoldan sırrı yenileyin.');
42
+ }
43
+ throw new CliError(`Belirteç alınamadı (HTTP ${res.status}).`);
44
+ }
45
+ const j = res.json;
46
+ if (!j?.access_token)
47
+ throw new CliError('Kimlik sağlayıcı beklenen yanıtı vermedi (access_token yok).');
48
+ return { token: j.access_token, expiresInSec: j.expires_in ?? 0, claims: decodeJwtPayload(j.access_token) };
49
+ }
50
+ /** İmza DOĞRULANMAZ (yalnızca whoami'de bilgi göstermek için); yetkilendirme sunucuda yapılır. */
51
+ export function decodeJwtPayload(token) {
52
+ const part = token.split('.')[1];
53
+ if (!part)
54
+ return {};
55
+ try {
56
+ return JSON.parse(Buffer.from(part, 'base64url').toString('utf8'));
57
+ }
58
+ catch {
59
+ return {};
60
+ }
61
+ }
62
+ /** JWT'deki proje rolleri (urn:zitadel:iam:org:project:{id}:roles anahtarları) → rol adları. */
63
+ export function rolesFromClaims(claims) {
64
+ const roles = new Set();
65
+ for (const [k, v] of Object.entries(claims)) {
66
+ if (/^urn:zitadel:iam:org:project(:\d+)?:roles$/.test(k) && v && typeof v === 'object')
67
+ for (const r of Object.keys(v))
68
+ roles.add(r);
69
+ }
70
+ return [...roles].sort();
71
+ }
package/dist/cli.js ADDED
@@ -0,0 +1,146 @@
1
+ #!/usr/bin/env node
2
+ import { readFileSync, realpathSync } from 'node:fs';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { parseArgs } from 'node:util';
5
+ import { link } from './commands/link.js';
6
+ import { schemaDiff, schemaPush } from './commands/schema.js';
7
+ import { siteDeploy, siteStatus } from './commands/site.js';
8
+ import { whoami } from './commands/whoami.js';
9
+ import { loadConfig, resolveSettings } from './config.js';
10
+ import { realPrompts } from './context.js';
11
+ import { ApiError, CliError } from './errors.js';
12
+ import { createOutput } from './output.js';
13
+ // Çıkış kodları: 0 başarı · 1 hata · 2 (--check) uygulanacak değişiklik var.
14
+ const OPTIONS = {
15
+ json: { type: 'boolean' }, help: { type: 'boolean', short: 'h' }, version: { type: 'boolean', short: 'v' }, debug: { type: 'boolean' },
16
+ config: { type: 'string' },
17
+ 'console-url': { type: 'string' }, 'app-url': { type: 'string' }, workspace: { type: 'string' },
18
+ module: { type: 'string' }, manifest: { type: 'string' }, dir: { type: 'string' },
19
+ check: { type: 'boolean' }, yes: { type: 'boolean', short: 'y' }, 'seed-data': { type: 'boolean' }, 'expect-host': { type: 'string' },
20
+ 'no-wait': { type: 'boolean' }, timeout: { type: 'string' }, 'no-verify': { type: 'boolean' },
21
+ };
22
+ const GLOBAL = ['json', 'help', 'version', 'debug', 'config'];
23
+ const COMMANDS = {
24
+ link: { flags: ['console-url', 'app-url', 'workspace', 'module', 'manifest', 'dir', 'no-verify'], run: link, usage: 'enformik link [--console-url U] [--app-url U] [--workspace ID] [--module SLUG] [--manifest YOL] [--dir KLASÖR]' },
25
+ whoami: { flags: ['console-url', 'app-url', 'workspace'], run: whoami, usage: 'enformik whoami' },
26
+ 'schema diff': { flags: ['app-url', 'workspace', 'module', 'manifest', 'check', 'seed-data', 'expect-host'], run: schemaDiff, usage: 'enformik schema diff [--manifest module.json] [--check]' },
27
+ '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]' },
28
+ 'site deploy': { flags: ['console-url', 'dir', 'no-wait', 'timeout'], run: siteDeploy, usage: 'enformik site deploy [--dir dist] [--no-wait] [--timeout 600]' },
29
+ 'site status': { flags: ['console-url'], run: siteStatus, usage: 'enformik site status' },
30
+ };
31
+ export function helpText(version) {
32
+ return `enformik ${version} — Enformik geliştirici CLI'si
33
+
34
+ Kullanım: enformik <komut> [bayraklar]
35
+
36
+ Komutlar
37
+ link .enformik/config.json yazar (adresler; sır içermez)
38
+ whoami Hangi kimlikle, hangi hedefe bağlanıldığını gösterir
39
+ schema diff Modül manifestini uygulamadakiyle karşılaştırır (--check: değişiklik varsa çıkış kodu 2)
40
+ schema push Değişiklikleri uygular (etkileşimsiz ortamda --yes gerekir)
41
+ site deploy Derlenmiş siteyi (varsayılan: dist) web sitesi ortamına yayınlar
42
+ site status Hedef site ve canlı sürüm bilgisi
43
+
44
+ Genel bayraklar: --json (makine-okunur çıktı), --config YOL, --debug, -h/--help, -v/--version
45
+
46
+ Sırlar yalnızca ortam değişkeniyle verilir:
47
+ Şema yükseltme : ENFORMIK_CLIENT_ID, ENFORMIK_CLIENT_SECRET (+ ENFORMIK_APP_URL)
48
+ Site dağıtımı : ENFORMIK_DEPLOY_KEY (+ ENFORMIK_CONSOLE_URL)
49
+
50
+ Çıkış kodları: 0 başarı · 1 hata · 2 (--check) uygulanacak değişiklik var
51
+
52
+ Ayrıntılar: komut yardımı için "enformik <komut> --help"`;
53
+ }
54
+ function readVersion() {
55
+ try {
56
+ return JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version;
57
+ }
58
+ catch {
59
+ return '0.0.0';
60
+ }
61
+ }
62
+ /** Test edilebilir giriş noktası: çıkış kodunu döner, process.exit çağırmaz. */
63
+ export async function main(d) {
64
+ let parsed;
65
+ try {
66
+ parsed = parseArgs({ args: d.argv, options: OPTIONS, allowPositionals: true, strict: true });
67
+ }
68
+ catch (e) {
69
+ const out = createOutput({ stderr: d.stderr, stdout: d.stdout, env: d.env });
70
+ out.error(`${e.message}\n"enformik --help" ile kullanımı görün.`);
71
+ return 1;
72
+ }
73
+ const { values, positionals } = parsed;
74
+ const out = createOutput({ json: values.json === true, stdout: d.stdout, stderr: d.stderr, env: d.env });
75
+ if (values.version) {
76
+ out.info(d.version);
77
+ return 0;
78
+ }
79
+ const name = positionals.slice(0, 2).join(' ');
80
+ const cmd = COMMANDS[name] ?? COMMANDS[positionals[0] ?? ''];
81
+ const cmdName = COMMANDS[name] ? name : positionals[0] ?? '';
82
+ if (values.help || positionals.length === 0 || positionals[0] === 'help') {
83
+ if (cmd && values.help) {
84
+ out.info(`Kullanım: ${cmd.usage}`);
85
+ return 0;
86
+ }
87
+ out.info(helpText(d.version));
88
+ return positionals.length === 0 && !values.help ? 1 : 0;
89
+ }
90
+ if (!cmd) {
91
+ out.error(`Bilinmeyen komut: ${positionals.join(' ')}\n"enformik --help" ile komutları görün.`);
92
+ return 1;
93
+ }
94
+ const used = Object.entries(values).filter(([, v]) => v !== undefined).map(([k]) => k);
95
+ const bad = used.filter((k) => !GLOBAL.includes(k) && !cmd.flags.includes(k));
96
+ if (bad.length > 0) {
97
+ out.error(`"${cmdName}" komutunda geçersiz bayrak: ${bad.map((b) => '--' + b).join(', ')}\nKullanım: ${cmd.usage}`);
98
+ return 1;
99
+ }
100
+ const extra = positionals.slice(cmdName.split(' ').length);
101
+ if (extra.length > 0) {
102
+ out.error(`Beklenmeyen argüman: ${extra.join(' ')}\nKullanım: ${cmd.usage}`);
103
+ return 1;
104
+ }
105
+ try {
106
+ const { config } = loadConfig(d.cwd, typeof values.config === 'string' ? values.config : undefined);
107
+ const flags = values;
108
+ const settings = resolveSettings(config, d.env, flags);
109
+ const prompts = realPrompts();
110
+ const ctx = {
111
+ out, settings, flags, cwd: d.cwd, env: d.env, version: d.version,
112
+ isTTY: d.isTTY ?? !!process.stdin.isTTY,
113
+ fetchImpl: d.fetchImpl,
114
+ confirm: d.confirm ?? prompts.confirm,
115
+ ask: d.ask ?? prompts.ask,
116
+ sleep: d.sleep,
117
+ };
118
+ return await cmd.run(ctx);
119
+ }
120
+ catch (e) {
121
+ if (e instanceof CliError) {
122
+ out.error(e.message);
123
+ out.result({ error: e.code ?? 'error', message: e.message });
124
+ return e.exitCode;
125
+ }
126
+ if (e instanceof ApiError) {
127
+ out.error(`Sunucu hatası (HTTP ${e.status}): ${e.message}`);
128
+ return 1;
129
+ }
130
+ out.error(`Beklenmeyen hata: ${e.message}`);
131
+ if (values.debug)
132
+ out.error(e.stack ?? '');
133
+ return 1;
134
+ }
135
+ }
136
+ // Doğrudan çalıştırma (import edildiğinde değil).
137
+ const invoked = process.argv[1] ? safeReal(process.argv[1]) : '';
138
+ function safeReal(p) { try {
139
+ return realpathSync(p);
140
+ }
141
+ catch {
142
+ return p;
143
+ } }
144
+ if (invoked && invoked === fileURLToPath(import.meta.url)) {
145
+ main({ argv: process.argv.slice(2), env: process.env, cwd: process.cwd(), version: readVersion() }).then((code) => { process.exitCode = code; });
146
+ }
@@ -0,0 +1,51 @@
1
+ import { discoverApp } from '../auth.js';
2
+ import { loadConfig, saveConfig } from '../config.js';
3
+ import { CliError } from '../errors.js';
4
+ import { assertSecureUrl } from '../http.js';
5
+ /** enformik link: .enformik/config.json yazar (sır içermez). */
6
+ export async function link(ctx) {
7
+ const { out, flags } = ctx;
8
+ const { config, root } = loadConfig(ctx.cwd);
9
+ const next = { ...config };
10
+ const str = (k) => (typeof flags[k] === 'string' ? flags[k] : undefined);
11
+ const prompt = async (label, cur, flag) => {
12
+ if (flag !== undefined)
13
+ return flag;
14
+ if (!ctx.isTTY)
15
+ return cur;
16
+ return (await ctx.ask(label, cur)) || undefined;
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'));
22
+ const manifest = str('manifest') ?? config.module?.manifest;
23
+ const dir = str('dir') ?? config.site?.dir;
24
+ if (slug || manifest)
25
+ next.module = { ...(config.module ?? {}), ...(slug ? { slug } : {}), ...(manifest ? { manifest } : {}) };
26
+ if (dir)
27
+ next.site = { ...(config.site ?? {}), dir };
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).');
30
+ if (next.consoleUrl)
31
+ assertSecureUrl(next.consoleUrl, 'Konsol adresi');
32
+ if (next.appUrl) {
33
+ assertSecureUrl(next.appUrl, 'Uygulama adresi');
34
+ if (flags['no-verify'] !== true) {
35
+ await discoverApp(next.appUrl, ctx.version, ctx.fetchImpl); // erişilebilir mi / API istemcisi kabul ediyor mu
36
+ out.info('✓ Uygulama erişilebilir.');
37
+ }
38
+ }
39
+ for (const k of Object.keys(next))
40
+ if (next[k] === undefined)
41
+ delete next[k];
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:');
45
+ if (next.consoleUrl)
46
+ out.info(' ENFORMIK_DEPLOY_KEY (site dağıtımı: dağıtım anahtarı)');
47
+ if (next.appUrl)
48
+ out.info(' ENFORMIK_CLIENT_ID / ENFORMIK_CLIENT_SECRET (şema yükseltme: API istemcisi)');
49
+ out.result({ linked: true, path, config: next });
50
+ return 0;
51
+ }
@@ -0,0 +1,111 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { resolve } from 'node:path';
3
+ import { discoverApp, fetchAccessToken } from '../auth.js';
4
+ import { CliError } from '../errors.js';
5
+ import { assertSecureUrl } from '../http.js';
6
+ import { adminOnlySections, parseManifest, pendingChanges, renderDiff, SchemaClient, typeChanges } from '../schema.js';
7
+ /** Ortak hazırlık: manifest → uygulama keşfi → belirteç → modül kimliği. */
8
+ async function prepare(ctx) {
9
+ const { settings, out } = ctx;
10
+ if (!settings.appUrl)
11
+ throw new CliError('Uygulama adresi yok. `enformik link --app-url https://…` çalıştırın ya da ENFORMIK_APP_URL tanımlayın.');
12
+ if (!settings.clientId || !settings.clientSecret) {
13
+ throw new CliError('ENFORMIK_CLIENT_ID ve ENFORMIK_CLIENT_SECRET tanımlı olmalı (konsolda uygulamanızın "API erişimi" bölümünden rolü "Şema yükseltici" olan bir istemci oluşturun).');
14
+ }
15
+ const appUrl = assertSecureUrl(settings.appUrl, 'Uygulama adresi');
16
+ const expect = ctx.flags['expect-host'];
17
+ if (typeof expect === 'string' && expect.toLowerCase() !== appUrl.hostname.toLowerCase()) {
18
+ throw new CliError(`Beklenen uygulama "${expect}" ama hedef "${appUrl.hostname}". Yanlış uygulamaya işlem yapılmaması için durduruldu.`);
19
+ }
20
+ const manifestPath = resolve(ctx.cwd, settings.manifestPath);
21
+ let text;
22
+ try {
23
+ text = readFileSync(manifestPath, 'utf8');
24
+ }
25
+ catch {
26
+ throw new CliError(`Manifest okunamadı: ${manifestPath}`);
27
+ }
28
+ const manifest = parseManifest(text, settings.manifestPath);
29
+ const slug = settings.moduleSlug ?? manifest.module.slug;
30
+ if (settings.moduleSlug && settings.moduleSlug !== manifest.module.slug) {
31
+ throw new CliError(`Yapılandırmadaki modül ("${settings.moduleSlug}") ile manifestteki slug ("${manifest.module.slug}") farklı.`);
32
+ }
33
+ const seedData = ctx.flags['seed-data'] === true;
34
+ const admin = adminOnlySections(manifest, { seedData });
35
+ if (admin.length > 0) {
36
+ out.warn(`Not: manifest yönetici gerektiren bölümler içeriyor (${admin.join(', ')}). "Şema yükseltici" rolündeki bir istemci bunu uygulayamaz.`);
37
+ }
38
+ const app = await discoverApp(settings.appUrl, ctx.version, ctx.fetchImpl);
39
+ const workspaceId = settings.workspaceId || app.tenantId;
40
+ if (!workspaceId)
41
+ throw new CliError('Çalışma alanı belirlenemedi. --workspace <kimlik> verin ya da `enformik link --workspace` ile kaydedin.');
42
+ const tok = await fetchAccessToken(app, settings.clientId, settings.clientSecret, ctx.version, ctx.fetchImpl);
43
+ const client = new SchemaClient({ appUrl: settings.appUrl, token: tok.token, workspaceId, version: ctx.version, fetchImpl: ctx.fetchImpl });
44
+ const mod = await client.resolveModule(slug);
45
+ return { client, manifest, manifestJson: JSON.stringify(manifest), moduleId: mod.id, installedVersion: mod.version, appHost: appUrl.hostname, seedData };
46
+ }
47
+ function summary(p, r) {
48
+ return {
49
+ module: p.manifest.module.slug,
50
+ installedVersion: p.installedVersion,
51
+ manifestVersion: p.manifest.module.version,
52
+ moduleId: p.moduleId,
53
+ changes: pendingChanges(r.diffs).length,
54
+ diffs: r.diffs,
55
+ errors: r.errors,
56
+ };
57
+ }
58
+ /** enformik schema diff [--check] */
59
+ export async function schemaDiff(ctx) {
60
+ const p = await prepare(ctx);
61
+ const r = await p.client.diff(p.moduleId, p.manifestJson, { seedData: p.seedData });
62
+ ctx.out.info(`Modül: ${p.manifest.module.slug} kurulu ${p.installedVersion} → manifest ${p.manifest.module.version}`);
63
+ ctx.out.info(renderDiff(ctx.out, r, { appHost: p.appHost }));
64
+ const s = summary(p, r);
65
+ ctx.out.result(s);
66
+ if (r.errors.length > 0)
67
+ return 1;
68
+ if (ctx.flags.check === true && s.changes > 0)
69
+ return 2;
70
+ return 0;
71
+ }
72
+ /** enformik schema push [--yes] */
73
+ export async function schemaPush(ctx) {
74
+ const p = await prepare(ctx);
75
+ const dry = await p.client.diff(p.moduleId, p.manifestJson, { seedData: p.seedData });
76
+ ctx.out.info(`Modül: ${p.manifest.module.slug} kurulu ${p.installedVersion} → manifest ${p.manifest.module.version}`);
77
+ ctx.out.info(renderDiff(ctx.out, dry, { appHost: p.appHost }));
78
+ if (dry.errors.length > 0) {
79
+ ctx.out.result({ ...summary(p, dry), applied: false });
80
+ return 1;
81
+ }
82
+ const pending = pendingChanges(dry.diffs);
83
+ // Sürüm farkı olsa bile yapısal değişiklik yoksa uygulanacak bir şey yok.
84
+ if (pending.length === 0) {
85
+ ctx.out.info('Uygulanacak değişiklik yok.');
86
+ ctx.out.result({ ...summary(p, dry), applied: false });
87
+ return 0;
88
+ }
89
+ const tc = typeChanges(dry.diffs);
90
+ if (tc.length > 0)
91
+ 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.`);
92
+ if (ctx.flags.yes !== true) {
93
+ if (!ctx.isTTY)
94
+ throw new CliError('Etkileşimli terminal yok: değişiklikleri uygulamak için --yes verin (sessizce onaylanmaz).');
95
+ if (!(await ctx.confirm('Değişiklikleri uygulamak istiyor musunuz?'))) {
96
+ ctx.out.info('Vazgeçildi; hiçbir değişiklik uygulanmadı.');
97
+ ctx.out.result({ ...summary(p, dry), applied: false });
98
+ return 1;
99
+ }
100
+ }
101
+ const res = await p.client.upgrade(p.moduleId, p.manifestJson, { seedData: p.seedData });
102
+ if (res.errors.length > 0) {
103
+ for (const e of res.errors)
104
+ ctx.out.error(`Hata: ${e}`);
105
+ ctx.out.result({ ...summary(p, res), applied: false });
106
+ return 1;
107
+ }
108
+ 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}).`));
109
+ ctx.out.result({ ...summary(p, res), applied: true });
110
+ return 0;
111
+ }
@@ -0,0 +1,100 @@
1
+ import { mkdtempSync, rmSync, statSync } from 'node:fs';
2
+ import { tmpdir } from 'node:os';
3
+ import { join, resolve } from 'node:path';
4
+ import { CliError } from '../errors.js';
5
+ import { assertSecureUrl } from '../http.js';
6
+ import { formatBytes } from '../output.js';
7
+ import { precheckSite, DEFAULT_LIMITS } from '../precheck.js';
8
+ import { collectMeta, DeployClient, idempotencyKey, isTerminal, MAX_ARCHIVE_BYTES, sha256File, waitForDeployment } from '../site.js';
9
+ import { writeZipFile } from '../zip.js';
10
+ function client(ctx) {
11
+ const { settings } = ctx;
12
+ if (!settings.consoleUrl)
13
+ throw new CliError('Konsol adresi yok. `enformik link --console-url https://…` çalıştırın ya da ENFORMIK_CONSOLE_URL tanımlayın.');
14
+ if (!settings.deployKey)
15
+ 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
+ return new DeployClient({ consoleUrl: settings.consoleUrl, deployKey: settings.deployKey, version: ctx.version, fetchImpl: ctx.fetchImpl });
18
+ }
19
+ /** enformik site status */
20
+ export async function siteStatus(ctx) {
21
+ const info = await client(ctx).getSite();
22
+ ctx.out.info(`Web sitesi ortamı : ${info.environment.name}`);
23
+ ctx.out.info(`Adres : ${info.url}`);
24
+ if (info.app)
25
+ ctx.out.info(`Uygulama : ${info.app.name}${info.app.workspaceId ? ` (çalışma alanı ${info.app.workspaceId})` : ''}`);
26
+ ctx.out.info(`Canlı sürüm : ${info.live ? `#${info.live.seq} (${info.live.at})` : 'henüz dağıtım yok'}`);
27
+ ctx.out.info(`Anahtar bitişi : ${info.key.expiresAt.slice(0, 10)}`);
28
+ ctx.out.result(info);
29
+ return 0;
30
+ }
31
+ /** enformik site deploy [--dir dist] [--no-wait] [--timeout 600] */
32
+ export async function siteDeploy(ctx) {
33
+ const { out, settings } = ctx;
34
+ const api = client(ctx);
35
+ const dir = resolve(ctx.cwd, settings.siteDir);
36
+ try {
37
+ if (!statSync(dir).isDirectory())
38
+ throw new Error();
39
+ }
40
+ catch {
41
+ throw new CliError(`Derleme klasörü bulunamadı: ${dir} (önce siteyi derleyin ya da --dir verin).`);
42
+ }
43
+ // Sunucunun kotası biliniyorsa ön denetimde kullan (aşımı yüklemeden görmek için).
44
+ const info = await api.getSite();
45
+ const pre = precheckSite(dir, { ...DEFAULT_LIMITS, maxFiles: info.limits.maxFiles, maxFileBytes: info.limits.maxFileBytes, maxUnpackedBytes: info.limits.maxUnpackedBytes });
46
+ if (pre.issues.length > 0) {
47
+ out.error('Site arşivi kurallara uymuyor; yükleme yapılmadı:');
48
+ for (const i of pre.issues)
49
+ out.error(` - ${i.path ? i.path + ': ' : ''}${i.message}`);
50
+ out.result({ deployed: false, issues: pre.issues });
51
+ return 1;
52
+ }
53
+ const tmp = mkdtempSync(join(tmpdir(), 'enformik-'));
54
+ try {
55
+ const zipPath = join(tmp, 'site.zip');
56
+ const z = writeZipFile(pre.files.map((f) => ({ name: f.name, path: f.path, mtime: f.mtime })), zipPath);
57
+ if (z.bytes > MAX_ARCHIVE_BYTES)
58
+ throw new CliError(`Sıkıştırılmış arşiv ${formatBytes(z.bytes)}; en çok ${formatBytes(MAX_ARCHIVE_BYTES)} olabilir.`);
59
+ out.info(`${pre.files.length} dosya, ${formatBytes(pre.unpackedBytes)} → ${formatBytes(z.bytes)} (zip)`);
60
+ const sha = sha256File(zipPath);
61
+ const init = await api.init({ sizeBytes: z.bytes, sha256: sha, meta: collectMeta(ctx.env, ctx.cwd) }, idempotencyKey(ctx.env));
62
+ let dep = init.deployment;
63
+ if (init.replayed)
64
+ out.info(`Önceki deneme bulundu (#${dep.seq}, durum: ${dep.status}); kaldığı yerden devam ediliyor.`);
65
+ // Durumuna göre devam: yükle → sonlandır → bekle (yeniden oynatılan kayıt için de doğru).
66
+ if (dep.status === 'awaiting_upload') {
67
+ out.info(`Yükleniyor (#${dep.seq})…`);
68
+ await api.upload(dep.deploymentId, zipPath);
69
+ dep = { ...dep, status: 'uploaded' };
70
+ }
71
+ if (dep.status === 'uploaded') {
72
+ await api.finalize(dep.deploymentId);
73
+ dep = { ...dep, status: 'validating' };
74
+ }
75
+ if (ctx.flags['no-wait'] === true) {
76
+ out.info(`Dağıtım başlatıldı (#${dep.seq}). Durum için: konsol → Geliştirici sitesi → Sürümler.`);
77
+ out.result({ deployed: false, deploymentId: dep.deploymentId, seq: dep.seq, status: dep.status });
78
+ return 0;
79
+ }
80
+ const timeoutSec = Number(typeof ctx.flags.timeout === 'string' ? ctx.flags.timeout : 600);
81
+ const final = isTerminal(dep.status) ? dep : await waitForDeployment(api, dep.deploymentId, {
82
+ timeoutMs: (Number.isFinite(timeoutSec) && timeoutSec > 0 ? timeoutSec : 600) * 1000,
83
+ onStatus: (s) => out.info(` durum: ${s}`),
84
+ sleep: ctx.sleep,
85
+ });
86
+ if (final.status === 'live') {
87
+ out.info(out.paint('green', `✓ Yayında: ${final.url ?? info.url} (sürüm #${final.seq})`));
88
+ out.result({ deployed: true, deploymentId: final.deploymentId, seq: final.seq, url: final.url ?? info.url });
89
+ return 0;
90
+ }
91
+ out.error(`✗ Dağıtım başarısız (#${final.seq}, ${final.status}): ${final.error ?? 'bilinmeyen neden'}`);
92
+ for (const i of final.issues ?? [])
93
+ out.error(` - ${i.path ? i.path + ': ' : ''}${i.message}`);
94
+ out.result({ deployed: false, deploymentId: final.deploymentId, seq: final.seq, status: final.status, error: final.error, issues: final.issues });
95
+ return 1;
96
+ }
97
+ finally {
98
+ rmSync(tmp, { recursive: true, force: true });
99
+ }
100
+ }
@@ -0,0 +1,38 @@
1
+ import { discoverApp, fetchAccessToken, rolesFromClaims } from '../auth.js';
2
+ import { DeployClient } from '../site.js';
3
+ /** enformik whoami: hangi kimlikle, hangi hedefe bağlanıldığını gösterir (sır yazdırmaz). */
4
+ export async function whoami(ctx) {
5
+ const { out, settings } = ctx;
6
+ const result = {};
7
+ let any = false;
8
+ if (settings.appUrl && settings.clientId && settings.clientSecret) {
9
+ any = true;
10
+ const app = await discoverApp(settings.appUrl, ctx.version, ctx.fetchImpl);
11
+ const tok = await fetchAccessToken(app, settings.clientId, settings.clientSecret, ctx.version, ctx.fetchImpl);
12
+ const roles = rolesFromClaims(tok.claims);
13
+ out.info('Şema yükseltme (API istemcisi)');
14
+ out.info(` Uygulama : ${settings.appUrl}`);
15
+ out.info(` İstemci : ${settings.clientId}`);
16
+ out.info(` Roller : ${roles.join(', ') || '(belirteçte rol yok)'}`);
17
+ 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 };
20
+ }
21
+ if (settings.consoleUrl && settings.deployKey) {
22
+ any = true;
23
+ const info = await new DeployClient({ consoleUrl: settings.consoleUrl, deployKey: settings.deployKey, version: ctx.version, fetchImpl: ctx.fetchImpl }).getSite();
24
+ out.info('Site dağıtımı (dağıtım anahtarı)');
25
+ out.info(` Konsol : ${settings.consoleUrl}`);
26
+ out.info(` Anahtar : ${settings.deployKey.slice(0, 12)}… (bitiş ${info.key.expiresAt.slice(0, 10)})`);
27
+ out.info(` Ortam / adres : ${info.environment.name} → ${info.url}`);
28
+ result.site = { consoleUrl: settings.consoleUrl, keyPrefix: settings.deployKey.slice(0, 12), expiresAt: info.key.expiresAt, environment: info.environment.name, url: info.url };
29
+ }
30
+ if (!any) {
31
+ out.info('Tanımlı kimlik bilgisi yok. Gerekenler:');
32
+ out.info(' Şema yükseltme : ENFORMIK_APP_URL (ya da link) + ENFORMIK_CLIENT_ID + ENFORMIK_CLIENT_SECRET');
33
+ out.info(' Site dağıtımı : ENFORMIK_CONSOLE_URL (ya da link) + ENFORMIK_DEPLOY_KEY');
34
+ return 1;
35
+ }
36
+ out.result(result);
37
+ return 0;
38
+ }
package/dist/config.js ADDED
@@ -0,0 +1,68 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
2
+ import { dirname, join, resolve } from 'node:path';
3
+ import { CliError } from './errors.js';
4
+ export const CONFIG_DIR = '.enformik';
5
+ export const CONFIG_FILE = 'config.json';
6
+ /** cwd'den köke doğru .enformik/config.json arar (git gibi). */
7
+ export function findConfigPath(cwd) {
8
+ let dir = resolve(cwd);
9
+ for (;;) {
10
+ const p = join(dir, CONFIG_DIR, CONFIG_FILE);
11
+ if (existsSync(p))
12
+ return p;
13
+ const parent = dirname(dir);
14
+ if (parent === dir)
15
+ return null;
16
+ dir = parent;
17
+ }
18
+ }
19
+ export function loadConfig(cwd, explicitPath) {
20
+ const path = explicitPath ? resolve(cwd, explicitPath) : findConfigPath(cwd);
21
+ if (!path)
22
+ return { config: {}, path: null, root: resolve(cwd) };
23
+ let raw;
24
+ try {
25
+ raw = readFileSync(path, 'utf8');
26
+ }
27
+ catch {
28
+ throw new CliError(`Yapılandırma okunamadı: ${path}`);
29
+ }
30
+ let parsed;
31
+ try {
32
+ parsed = JSON.parse(raw);
33
+ }
34
+ catch {
35
+ throw new CliError(`Yapılandırma geçerli JSON değil: ${path}`);
36
+ }
37
+ if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed))
38
+ throw new CliError(`Yapılandırma bir nesne olmalı: ${path}`);
39
+ // Depoya yanlışlıkla sır yazılmasını engelle.
40
+ const s = raw.toLowerCase();
41
+ if (s.includes('enf_dk_') || /"(client_?secret|secret|deploy_?key|password|token)"/.test(s)) {
42
+ throw new CliError(`${path} sır içeriyor gibi görünüyor. Sırlar yapılandırma dosyasına değil, ortam değişkenlerine (ENFORMIK_CLIENT_SECRET, ENFORMIK_DEPLOY_KEY) yazılır; dosyadan çıkarın.`);
43
+ }
44
+ // .enformik/config.json → proje kökü = .enformik'in üst dizini
45
+ return { config: parsed, path, root: dirname(dirname(path)) };
46
+ }
47
+ export function saveConfig(root, config) {
48
+ const dir = join(root, CONFIG_DIR);
49
+ mkdirSync(dir, { recursive: true });
50
+ const p = join(dir, CONFIG_FILE);
51
+ writeFileSync(p, JSON.stringify(config, null, 2) + '\n');
52
+ return p;
53
+ }
54
+ /** Öncelik: bayrak > ortam değişkeni > yapılandırma dosyası. */
55
+ export function resolveSettings(config, env, flags) {
56
+ const pick = (...v) => v.find((x) => x !== undefined && x !== '');
57
+ return {
58
+ consoleUrl: pick(flags['console-url'], env.ENFORMIK_CONSOLE_URL, config.consoleUrl),
59
+ appUrl: pick(flags['app-url'], env.ENFORMIK_APP_URL, config.appUrl),
60
+ workspaceId: pick(flags.workspace, env.ENFORMIK_WORKSPACE_ID, config.workspaceId),
61
+ clientId: pick(env.ENFORMIK_CLIENT_ID),
62
+ clientSecret: pick(env.ENFORMIK_CLIENT_SECRET),
63
+ deployKey: pick(env.ENFORMIK_DEPLOY_KEY),
64
+ moduleSlug: pick(flags.module, config.module?.slug),
65
+ manifestPath: pick(flags.manifest, config.module?.manifest) ?? 'module.json',
66
+ siteDir: pick(flags.dir, config.site?.dir) ?? 'dist',
67
+ };
68
+ }