@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 +3 -3
- package/dist/cli.js +15 -6
- package/dist/commands/ai.js +29 -8
- package/dist/commands/app.js +77 -66
- package/dist/commands/codegen.js +5 -2
- package/dist/commands/schema.js +22 -6
- package/dist/commands/site.js +30 -7
- package/dist/commands/whoami.js +3 -2
- package/dist/http.js +10 -1
- package/dist/output.js +1 -1
- package/dist/precheck.js +77 -8
- package/dist/schema.js +22 -8
- package/dist/site.js +30 -7
- package/dist/zip.js +8 -2
- package/package.json +1 -1
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
|
-
|
|
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
|
-
|
|
95
|
-
const
|
|
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
|
}
|
package/dist/commands/ai.js
CHANGED
|
@@ -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
|
-
/**
|
|
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
|
-
|
|
56
|
-
|
|
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
|
-
|
|
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
|
-
|
|
138
|
-
|
|
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/`);
|
package/dist/commands/app.js
CHANGED
|
@@ -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
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
|
|
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
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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: '
|
|
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
|
-
|
|
93
|
-
|
|
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
|
-
|
|
108
|
-
|
|
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
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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).' });
|
package/dist/commands/codegen.js
CHANGED
|
@@ -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 =
|
|
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 =
|
|
33
|
+
const changed = !same;
|
|
31
34
|
if (changed) {
|
|
32
35
|
mkdirSync(dirname(outPath), { recursive: true });
|
|
33
36
|
writeFileSync(outPath, code);
|
package/dist/commands/schema.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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
|
}
|
package/dist/commands/site.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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;
|
package/dist/commands/whoami.js
CHANGED
|
@@ -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
|
-
|
|
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 &&
|
|
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())
|
|
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
|
-
|
|
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
|
|
49
|
-
/**
|
|
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) =>
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
98
|
-
* dağıtım "tekrar oynatılırdı"
|
|
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'}
|
|
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'}
|
|
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
|
-
|
|
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);
|