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