@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.
- package/README.md +72 -31
- package/dist/auth.js +13 -9
- package/dist/cli.js +56 -38
- package/dist/codegen.js +6 -6
- package/dist/commands/ai.js +55 -30
- package/dist/commands/app.js +90 -73
- package/dist/commands/codegen.js +9 -6
- package/dist/commands/link.js +12 -12
- package/dist/commands/schema.js +60 -40
- package/dist/commands/site.js +51 -29
- package/dist/commands/whoami.js +20 -15
- package/dist/config.js +4 -4
- package/dist/context.js +32 -17
- package/dist/errors.js +5 -1
- package/dist/http.js +41 -5
- package/dist/output.js +1 -1
- package/dist/precheck.js +90 -21
- package/dist/schema.js +50 -34
- package/dist/site.js +75 -20
- package/dist/zip.js +11 -5
- package/package.json +2 -2
package/dist/schema.js
CHANGED
|
@@ -1,21 +1,23 @@
|
|
|
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
|
-
throw new CliError(`${path}
|
|
12
|
+
throw new CliError(`${path} is not valid JSON: ${e.message}`);
|
|
11
13
|
}
|
|
12
14
|
if (typeof m !== 'object' || m === null || Array.isArray(m))
|
|
13
|
-
throw new CliError(`${path}
|
|
15
|
+
throw new CliError(`${path} must be an object.`);
|
|
14
16
|
const mod = m.module;
|
|
15
17
|
if (!mod || typeof mod.slug !== 'string' || !mod.slug)
|
|
16
|
-
throw new CliError(`${path}: "module.slug"
|
|
18
|
+
throw new CliError(`${path}: "module.slug" is required.`);
|
|
17
19
|
if (typeof mod.version !== 'string' || !mod.version)
|
|
18
|
-
throw new CliError(`${path}: "module.version"
|
|
20
|
+
throw new CliError(`${path}: "module.version" is required.`);
|
|
19
21
|
return m;
|
|
20
22
|
}
|
|
21
23
|
/**
|
|
@@ -45,28 +47,37 @@ 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');
|
|
55
66
|
}
|
|
56
67
|
const ACTION_LABEL = {
|
|
57
|
-
create: '+
|
|
68
|
+
create: '+ add', alter: '~ change', drop: '- delete', none: '= same', kept_local: '= kept local',
|
|
58
69
|
};
|
|
59
70
|
export function renderDiff(out, result, opts = {}) {
|
|
60
71
|
const lines = [];
|
|
61
72
|
const pending = pendingChanges(result.diffs);
|
|
62
73
|
if (pending.length > 0) {
|
|
63
|
-
lines.push(out.paint('yellow', out.paint('bold', '⚠
|
|
64
|
-
lines.push(out.paint('yellow', `
|
|
74
|
+
lines.push(out.paint('yellow', out.paint('bold', '⚠ WARNING: SCHEMA CHANGES AFFECT THE ENTIRE APP')));
|
|
75
|
+
lines.push(out.paint('yellow', `This affects ALL workspaces in the ${opts.appHost ? `"${opts.appHost}" ` : ''}app, not just one workspace.`));
|
|
65
76
|
lines.push('');
|
|
66
77
|
}
|
|
67
78
|
const shown = result.diffs.filter((d) => d.action !== 'none');
|
|
68
79
|
if (shown.length === 0) {
|
|
69
|
-
lines.push('
|
|
80
|
+
lines.push('No changes: the module in the app matches the manifest.');
|
|
70
81
|
}
|
|
71
82
|
else {
|
|
72
83
|
const rows = shown.map((d) => {
|
|
@@ -74,17 +85,17 @@ export function renderDiff(out, result, opts = {}) {
|
|
|
74
85
|
const colored = d.action === 'create' ? out.paint('green', label)
|
|
75
86
|
: d.action === 'drop' ? out.paint('red', label)
|
|
76
87
|
: d.action === 'alter' ? out.paint('yellow', label) : out.paint('dim', label);
|
|
77
|
-
const detail = d.action === 'drop' && d.hasData ? '
|
|
88
|
+
const detail = d.action === 'drop' && d.hasData ? 'will be SKIPPED because it contains data' : (d.details || d.warning || '');
|
|
78
89
|
return [d.resourceType, d.slug, colored, detail];
|
|
79
90
|
});
|
|
80
|
-
lines.push(renderTable(out, ['
|
|
91
|
+
lines.push(renderTable(out, ['TYPE', 'NAME', 'ACTION', 'DETAILS'], rows));
|
|
81
92
|
const count = (a) => shown.filter((d) => d.action === a).length;
|
|
82
93
|
lines.push('');
|
|
83
|
-
lines.push(
|
|
84
|
-
(count('kept_local') ? `, ${count('kept_local')}
|
|
94
|
+
lines.push(`Summary: ${count('create')} to add, ${count('alter')} to change, ${count('drop')} to delete` +
|
|
95
|
+
(count('kept_local') ? `, ${count('kept_local')} local customizations kept` : '') + '.');
|
|
85
96
|
}
|
|
86
97
|
for (const e of result.errors ?? [])
|
|
87
|
-
lines.push(out.paint('red', `
|
|
98
|
+
lines.push(out.paint('red', `Error: ${e}`));
|
|
88
99
|
return lines.join('\n');
|
|
89
100
|
}
|
|
90
101
|
export class SchemaClient {
|
|
@@ -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(`The server response is not in the expected format: module "${slug}" has no ID.`);
|
|
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) {
|
|
@@ -163,7 +177,7 @@ export class SchemaClient {
|
|
|
163
177
|
method: 'POST', headers: this.headers({ 'Content-Type': 'application/json' }), body: manifestJson, retries: 1, timeoutMs: 120_000, fetchImpl: this.o.fetchImpl,
|
|
164
178
|
});
|
|
165
179
|
if (res.status === 404 || res.status === 405) {
|
|
166
|
-
throw new CliError('
|
|
180
|
+
throw new CliError('This app version does not support server-side validation (/api/modules/validate). Until the app is updated, run local-only checks with `schema validate --offline`.', 1, 'validate_unsupported');
|
|
167
181
|
}
|
|
168
182
|
try {
|
|
169
183
|
ensureOk(res);
|
|
@@ -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 {
|
|
@@ -211,31 +225,33 @@ function normalizeDiff(j) {
|
|
|
211
225
|
export function describeSchemaError(e, ctx) {
|
|
212
226
|
if (!(e instanceof ApiError))
|
|
213
227
|
return e;
|
|
228
|
+
if (e.status === 429)
|
|
229
|
+
return e; // the rate-limit message is already final (see rateLimitedError); cli.ts prints it
|
|
214
230
|
const body = (e.body ?? {});
|
|
215
231
|
switch (e.code) {
|
|
216
232
|
case 'manifest_requires_admin':
|
|
217
|
-
return new CliError(`
|
|
218
|
-
'
|
|
233
|
+
return new CliError(`This manifest contains sections beyond your client's role: ${(body.sections ?? []).join(', ')}.\n` +
|
|
234
|
+
'These sections (scripts, automations, permission definitions, etc.) must be applied by an administrator; remove them from the manifest or run with a client that has the administrator role.', 1, e.code, e.body);
|
|
219
235
|
case 'type_change_requires_admin':
|
|
220
|
-
return new CliError(`
|
|
236
|
+
return new CliError(`Changing a field type can transform data and is applied only by an administrator: ${(body.fields ?? []).join(', ')}.`, 1, e.code, e.body);
|
|
221
237
|
case 'breaking_change_blocked':
|
|
222
238
|
return new CliError(`${e.message}`, 1, e.code, e.body);
|
|
223
239
|
case 'module_not_found':
|
|
224
|
-
return new CliError(`"${ctx.slug ?? '
|
|
240
|
+
return new CliError(`No module with slug "${ctx.slug ?? 'module'}" is installed in this app. The first installation is done with \`enformik schema install\` (using a client with the "Schema builder" role) or by an administrator; later upgrades are done with \`schema push\`.`, 1, e.code);
|
|
225
241
|
case 'module_exists':
|
|
226
|
-
return new CliError('
|
|
242
|
+
return new CliError('A module with this slug is already installed. Use `enformik schema push` to upgrade the existing one.', 1, e.code);
|
|
227
243
|
case 'payment_required':
|
|
228
|
-
return new CliError('
|
|
244
|
+
return new CliError('The operation was blocked because of the app\'s plan/payment status (402). Check the subscription status in the console.', 1, e.code);
|
|
229
245
|
}
|
|
230
246
|
if (e.status === 401)
|
|
231
|
-
return new CliError('
|
|
247
|
+
return new CliError('Authentication rejected (401): the token is invalid or the client has been revoked.', 1, 'unauthorized');
|
|
232
248
|
if (e.status === 403) {
|
|
233
249
|
return new CliError((ctx.install
|
|
234
|
-
? '
|
|
235
|
-
: '
|
|
236
|
-
(body.required ? ` (
|
|
250
|
+
? 'This client\'s role is not sufficient to install a new module (403). Use an API client whose role in the console is "Schema builder" or "Administrator".'
|
|
251
|
+
: 'This client\'s role is not sufficient to upgrade a module (403). Use an API client whose role in the console is "Schema upgrader", "Schema builder" or "Administrator".') +
|
|
252
|
+
(body.required ? ` (required permission: ${body.required})` : ''), 1, 'forbidden');
|
|
237
253
|
}
|
|
238
254
|
if (e.status === 404)
|
|
239
|
-
return new CliError(`
|
|
240
|
-
return new CliError(`
|
|
255
|
+
return new CliError(`Not found (404): ${e.message}`, 1, 'not_found');
|
|
256
|
+
return new CliError(`Server error (HTTP ${e.status}): ${e.message}`, 1, e.code);
|
|
241
257
|
}
|
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, 'Console URL');
|
|
13
15
|
}
|
|
14
16
|
url(path) { return joinUrl(this.o.consoleUrl, `/api/deploy/v1${path}`); }
|
|
15
17
|
headers(extra = {}) {
|
|
@@ -44,6 +46,33 @@ export class DeployClient {
|
|
|
44
46
|
return (await this.call(`/deployments/${id}`, { retries: 2 })).deployment;
|
|
45
47
|
}
|
|
46
48
|
}
|
|
49
|
+
/**
|
|
50
|
+
* Finalizes a deployment. A `409 invalid_state` can mean the first finalize worked and only its reply was lost (the request was retried), or
|
|
51
|
+
* another run of the same CI job already finalized it, so the deployment's real state decides: running or finished -> carry on with that state;
|
|
52
|
+
* still waiting for its upload / finalize -> the error stands.
|
|
53
|
+
*/
|
|
54
|
+
export async function finalizeDeployment(client, dep) {
|
|
55
|
+
try {
|
|
56
|
+
await client.finalize(dep.deploymentId);
|
|
57
|
+
return { ...dep, status: 'validating' };
|
|
58
|
+
}
|
|
59
|
+
catch (e) {
|
|
60
|
+
if (!(e instanceof CliError) || e.code !== 'invalid_state')
|
|
61
|
+
throw e;
|
|
62
|
+
let cur;
|
|
63
|
+
try {
|
|
64
|
+
cur = await client.status(dep.deploymentId);
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
throw e;
|
|
68
|
+
} // the state could not be read: the original (actionable) error is the better message
|
|
69
|
+
if (cur.status === 'awaiting_upload' || cur.status === 'uploaded')
|
|
70
|
+
throw e;
|
|
71
|
+
return { ...dep, ...cur };
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
/** Üst üste bu kadar yoklama hatasından sonra bırakılır. */
|
|
75
|
+
const MAX_POLL_FAILURES = 5;
|
|
47
76
|
const TERMINAL = new Set(['live', 'failed', 'expired', 'superseded']);
|
|
48
77
|
export const isTerminal = (s) => TERMINAL.has(s);
|
|
49
78
|
/** Durum değiştikçe onStatus çağırarak terminal duruma kadar bekler. */
|
|
@@ -53,8 +82,25 @@ export async function waitForDeployment(client, id, opts) {
|
|
|
53
82
|
const start = now();
|
|
54
83
|
let last = '';
|
|
55
84
|
let interval = opts.intervalMs ?? 2000;
|
|
85
|
+
let failures = 0;
|
|
56
86
|
for (;;) {
|
|
57
|
-
|
|
87
|
+
let v;
|
|
88
|
+
try {
|
|
89
|
+
v = await client.status(id);
|
|
90
|
+
failures = 0;
|
|
91
|
+
}
|
|
92
|
+
catch (e) {
|
|
93
|
+
// Geçici hata (ağ, 5xx): sunucu dağıtımı yayınlıyor olabilir, tek bir yoklama hatası CI'ı kırmasın.
|
|
94
|
+
// Kalıcı hatalar (geçersiz anahtar, 404…) kodla gelir ve hemen iletilir.
|
|
95
|
+
// Bir 429 (kodsuz) da geçicidir: sunucu dağıtımı sürdürüyor, yoklamayı kısıtlama yüzünden bırakmayız.
|
|
96
|
+
const transient = (e instanceof CliError && (!e.code || e.code === 'deploy_unavailable')) || (e instanceof ApiError && e.status === 429 && !e.code);
|
|
97
|
+
if (!transient || ++failures >= MAX_POLL_FAILURES)
|
|
98
|
+
throw e;
|
|
99
|
+
if (now() - start > opts.timeoutMs)
|
|
100
|
+
throw e;
|
|
101
|
+
await sleep(interval);
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
58
104
|
if (v.status !== last) {
|
|
59
105
|
last = v.status;
|
|
60
106
|
opts.onStatus?.(v.status);
|
|
@@ -62,7 +108,7 @@ export async function waitForDeployment(client, id, opts) {
|
|
|
62
108
|
if (isTerminal(v.status))
|
|
63
109
|
return v;
|
|
64
110
|
if (now() - start > opts.timeoutMs)
|
|
65
|
-
throw new CliError(`
|
|
111
|
+
throw new CliError(`Timed out: the deployment did not finish within ${Math.round(opts.timeoutMs / 1000)} s (last status: ${v.status}). You can follow it on the console's "Releases" tab.`);
|
|
66
112
|
await sleep(interval);
|
|
67
113
|
interval = Math.min(interval + 500, 5000);
|
|
68
114
|
}
|
|
@@ -94,36 +140,45 @@ function gitCmd(cwd, args) {
|
|
|
94
140
|
}
|
|
95
141
|
}
|
|
96
142
|
/**
|
|
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ı"
|
|
143
|
+
* Idempotency-Key: CI'da koşu + deneme numarası + arşiv sağlama toplamının başı (yeniden çalıştırma yeni anahtar alır,
|
|
144
|
+
* 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ı
|
|
145
|
+
* paylaşmaz); yerelde her çalıştırma için rastgele.
|
|
99
146
|
*/
|
|
100
|
-
export function idempotencyKey(env, rand = randomUUID) {
|
|
147
|
+
export function idempotencyKey(env, rand = randomUUID, contentSha) {
|
|
148
|
+
const tail = contentSha ? `-${contentSha.slice(0, 12)}` : '';
|
|
149
|
+
const fit = (base) => `${base.slice(0, 64 - tail.length)}${tail}`;
|
|
101
150
|
if (env.GITHUB_RUN_ID)
|
|
102
|
-
return `gh-${env.GITHUB_RUN_ID}-${env.GITHUB_RUN_ATTEMPT ?? '1'}
|
|
151
|
+
return fit(`gh-${env.GITHUB_RUN_ID}-${env.GITHUB_RUN_ATTEMPT ?? '1'}`);
|
|
103
152
|
if (env.CI_PIPELINE_ID)
|
|
104
|
-
return `gl-${env.CI_PIPELINE_ID}-${env.CI_JOB_ID ?? '0'}
|
|
153
|
+
return fit(`gl-${env.CI_PIPELINE_ID}-${env.CI_JOB_ID ?? '0'}`);
|
|
105
154
|
return `cli-${rand()}`.slice(0, 64);
|
|
106
155
|
}
|
|
107
156
|
const HINTS = {
|
|
108
|
-
invalid_key: '
|
|
109
|
-
key_expired: '
|
|
110
|
-
rate_limited: '
|
|
111
|
-
developer_site_disabled: '
|
|
112
|
-
environment_inactive: '
|
|
113
|
-
deploy_in_progress: '
|
|
114
|
-
deploy_unavailable: '
|
|
115
|
-
deployment_expired: '
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
157
|
+
invalid_key: 'The deploy key is invalid, revoked, or does not belong to this site. Check the ENFORMIK_DEPLOY_KEY value.',
|
|
158
|
+
key_expired: 'The deploy key has expired. Create a new key in the console and update the CI secret variable.',
|
|
159
|
+
rate_limited: 'Too many invalid key attempts; wait a moment and try again.',
|
|
160
|
+
developer_site_disabled: 'The developer site is not enabled in this environment. Enable it from the "Developer site" dialog in the console.',
|
|
161
|
+
environment_inactive: 'The website environment is currently unavailable (e.g. the subscription is suspended).',
|
|
162
|
+
deploy_in_progress: 'A deployment is already in progress in this environment; wait for it to finish and try again.',
|
|
163
|
+
deploy_unavailable: 'The deployment infrastructure is not ready right now; try again shortly.',
|
|
164
|
+
deployment_expired: 'The upload window has expired; run the command again.',
|
|
165
|
+
invalid_state: 'The deployment is not in a state that allows this step (for example it was already finalized or its upload is incomplete); run the command again.',
|
|
166
|
+
quota_exceeded: 'The archive exceeds the allowed size (50 MB max).',
|
|
167
|
+
size_mismatch: 'The uploaded content does not match the declared size; run the command again.',
|
|
168
|
+
checksum_mismatch: 'The checksum of the uploaded content does not match (the upload may be corrupted); run the command again.',
|
|
119
169
|
};
|
|
120
170
|
export function describeDeployError(e) {
|
|
121
171
|
if (!(e instanceof ApiError))
|
|
122
172
|
return e;
|
|
173
|
+
// Any 429 except one with a known hint (the deploy-key limiter) keeps its rate-limit message and JSON shape (status, retryAfterSeconds).
|
|
174
|
+
if (e.status === 429 && !(e.code && HINTS[e.code]))
|
|
175
|
+
return e;
|
|
123
176
|
const hint = e.code ? HINTS[e.code] : undefined;
|
|
124
177
|
const detail = e.body?.details;
|
|
125
178
|
let msg = hint ?? e.message ?? `HTTP ${e.status}`;
|
|
126
179
|
if (e.code === 'artifact_invalid' && Array.isArray(detail))
|
|
127
|
-
msg = `
|
|
180
|
+
msg = `Invalid archive: ${detail.map((d) => d.message).join('; ')}`;
|
|
181
|
+
if (e.status === 429 && e.retryAfterSeconds !== undefined)
|
|
182
|
+
msg += ` (retry in about ${e.retryAfterSeconds} s)`;
|
|
128
183
|
return new CliError(msg, 1, e.code, e.body);
|
|
129
184
|
}
|
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(),
|
|
@@ -28,17 +28,23 @@ const MAX_U32 = 0xffffffff;
|
|
|
28
28
|
/** Girdileri sıralı (deterministik) olarak outPath'e zipler. */
|
|
29
29
|
export function writeZipFile(files, outPath) {
|
|
30
30
|
if (files.length > MAX_ENTRIES)
|
|
31
|
-
throw new Error(`
|
|
31
|
+
throw new Error(`A zip can contain at most ${MAX_ENTRIES} entries.`);
|
|
32
32
|
const sorted = [...files].sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
|
|
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);
|
|
40
46
|
if (data.length >= MAX_U32)
|
|
41
|
-
throw new Error(`
|
|
47
|
+
throw new Error(`File too large (zip64 is not supported): ${f.name}`);
|
|
42
48
|
const name = Buffer.from(f.name, 'utf8');
|
|
43
49
|
const deflated = deflateRawSync(data, { level: 6 });
|
|
44
50
|
const useDeflate = deflated.length < data.length;
|
|
@@ -90,7 +96,7 @@ export function writeZipFile(files, outPath) {
|
|
|
90
96
|
end.writeUInt32LE(cdStart, 16);
|
|
91
97
|
write(end);
|
|
92
98
|
if (offset >= MAX_U32)
|
|
93
|
-
throw new Error('
|
|
99
|
+
throw new Error('The zip exceeds the 4 GB limit.');
|
|
94
100
|
return { entries: sorted.length, bytes: offset };
|
|
95
101
|
}
|
|
96
102
|
finally {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@enformik/cli",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Enformik
|
|
3
|
+
"version": "0.6.0",
|
|
4
|
+
"description": "Enformik developer CLI: website deployment and application module (schema) upgrades.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"bin": {
|