@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/README.md
CHANGED
|
@@ -1,63 +1,104 @@
|
|
|
1
1
|
# @enformik/cli
|
|
2
2
|
|
|
3
|
-
Enformik
|
|
3
|
+
The Enformik developer CLI: publishes the **website** you build and upgrades your **application module (schema)**. It is designed to run in CI/CD pipelines and has no runtime dependencies (Node.js ≥ 20).
|
|
4
4
|
|
|
5
5
|
```bash
|
|
6
6
|
npx @enformik/cli --help
|
|
7
|
-
#
|
|
8
|
-
npm install -g @enformik/cli #
|
|
7
|
+
# or install it permanently:
|
|
8
|
+
npm install -g @enformik/cli # command: enformik
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
-
## Site
|
|
11
|
+
## Site deployment
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
Requirements: in the Enformik management console, **Developer site** must be enabled on your website environment and a **deploy key** must have been created.
|
|
14
14
|
|
|
15
15
|
```bash
|
|
16
|
-
export ENFORMIK_CONSOLE_URL=https://<
|
|
17
|
-
export ENFORMIK_DEPLOY_KEY=enf_dk_… # CI
|
|
16
|
+
export ENFORMIK_CONSOLE_URL=https://<your-console-url>
|
|
17
|
+
export ENFORMIK_DEPLOY_KEY=enf_dk_… # CI secret variable; do not commit it to the repo
|
|
18
18
|
|
|
19
19
|
npx @enformik/cli site deploy --dir dist
|
|
20
20
|
```
|
|
21
21
|
|
|
22
|
-
`index.html`
|
|
22
|
+
Rules such as whether `index.html` is at the root, whether there are hidden files (`.env`, `.git`…), whether the file types are allowed (executables, `.zip`/`.iso` and files without an extension are rejected), and whether the size/quota fits are checked **before uploading**; errors are shown with the file path. If a text file in the bundle contains a secret, the upload stops; you can skip a false alarm by listing the file in `.enformikignore` in the project root (one path per line, `dir/` prefix, no wildcards). `--expect-host <site-host>` prevents deploying to the wrong site. On success the address of the new release is printed.
|
|
23
23
|
|
|
24
|
-
##
|
|
24
|
+
## Schema (module) upgrades
|
|
25
25
|
|
|
26
|
-
|
|
26
|
+
Requirements: an **API client** in your application with the **Schema upgrader** role (upgrades only), the **Schema builder** role (upgrades + first installation), or the Administrator role (console → **API Keys** page). The first installation of a module is done with `schema install` (Schema builder/Administrator); later upgrades are done with `schema push`.
|
|
27
27
|
|
|
28
28
|
```bash
|
|
29
|
-
export ENFORMIK_APP_URL=https://<
|
|
29
|
+
export ENFORMIK_APP_URL=https://<your-app-url>
|
|
30
30
|
export ENFORMIK_CLIENT_ID=apic-…
|
|
31
31
|
export ENFORMIK_CLIENT_SECRET=…
|
|
32
32
|
|
|
33
|
-
npx @enformik/cli schema install --manifest module.json --yes #
|
|
34
|
-
npx @enformik/cli schema diff --manifest module.json --check #
|
|
33
|
+
npx @enformik/cli schema install --manifest module.json --yes # first installation of the module (--dry-run: show only)
|
|
34
|
+
npx @enformik/cli schema diff --manifest module.json --check # exit code 2 if there are changes
|
|
35
35
|
npx @enformik/cli schema push --manifest module.json --yes
|
|
36
36
|
```
|
|
37
37
|
|
|
38
|
-
> **
|
|
38
|
+
> **Warning:** a module (schema) change affects **all workspaces** in the application. Apply it to a live application from CI with a client dedicated to production, not from a laptop.
|
|
39
39
|
|
|
40
|
-
"
|
|
40
|
+
The "Schema upgrader" and "Schema builder" roles cannot apply manifests that contain scripts, automations, permission definitions, data sources, interactive pages, UI extensions or seed data, nor field type changes; an administrator applies these.
|
|
41
41
|
|
|
42
|
-
##
|
|
42
|
+
## Commands
|
|
43
43
|
|
|
44
|
-
|
|
|
44
|
+
| Command | Description |
|
|
45
45
|
| --- | --- |
|
|
46
|
-
| `link` | `.enformik/config.json`
|
|
47
|
-
| `whoami` |
|
|
48
|
-
| `
|
|
49
|
-
| `
|
|
50
|
-
| `
|
|
51
|
-
| `
|
|
46
|
+
| `link` | Writes `.enformik/config.json` (URLs only; **contains no secrets**) |
|
|
47
|
+
| `whoami` | Shows which identity connects to which target |
|
|
48
|
+
| `ai init` | Downloads the AI agent guides into `.enformik/ai/` and adds an instruction block to `AGENTS.md` (`--lang en\|tr`, default `en`; `--offline`) |
|
|
49
|
+
| `app check` | Read-only check of the web app's integration health: site sign-in, module version, record access rules, web user access (`--entity`, `--expect-host`) |
|
|
50
|
+
| `codegen` | Generates TypeScript types and a typed data client from `module.json` (`--out`, `--check`) |
|
|
51
|
+
| `schema validate` | Validates the manifest with the server's rules without touching the data (`--profile ai`, `--offline`) |
|
|
52
|
+
| `schema diff` | Compares the manifest with the one in the application (`--check`) |
|
|
53
|
+
| `schema install` | Installs the module for the first time (`--dry-run`, `--yes`; role: Schema builder) |
|
|
54
|
+
| `schema push` | Applies changes (`--yes` is required in non-interactive environments) |
|
|
55
|
+
| `site deploy` | Publishes the built site (`--dir`, `--no-wait`, `--timeout`, `--expect-host`) |
|
|
56
|
+
| `site status` | Shows the target site and the live release (`--expect-host`) |
|
|
52
57
|
|
|
53
|
-
|
|
54
|
-
Çıkış kodları: `0` başarı · `1` hata · `2` (`--check`) uygulanacak değişiklik var.
|
|
55
|
-
`--expect-host <ad>` (şema komutları): hedef uygulamanın ana makine adı farklıysa işlemi durdurur (yanlış uygulamaya push'a karşı koruma).
|
|
58
|
+
Run `enformik <command> --help` for a command's usage. A mistyped command (`enformik sitee --help`) is an error, not the general help.
|
|
56
59
|
|
|
57
|
-
|
|
60
|
+
Global flags: `--json`, `--config`, `--debug`, `-h`/`--help`, `-v`/`--version`.
|
|
58
61
|
|
|
59
|
-
-
|
|
60
|
-
- Kimlik bilgileri yalnızca HTTPS ile gönderilir (`localhost` hariç); yönlendirmeler takip edilmez.
|
|
61
|
-
- Erişim belirteci yalnızca bellekte tutulur.
|
|
62
|
+
`--expect-host <host>` stops the operation if the host name differs. In the schema commands and in `app check` it is the **application's** host (in `app check` it is checked before any request is sent; the site side is not checked against it); in `site deploy` and `site status` it is the **site's** host.
|
|
62
63
|
|
|
63
|
-
|
|
64
|
+
## Exit codes
|
|
65
|
+
|
|
66
|
+
| Code | Meaning |
|
|
67
|
+
| --- | --- |
|
|
68
|
+
| `0` | Success (including no-ops, `--dry-run` and `--no-wait`) |
|
|
69
|
+
| `1` | Error: invalid input, validation, credentials, server error, failed deployment, a declined confirmation (a closed input counts as "no"), a rate limit |
|
|
70
|
+
| `2` | Not an error: `schema diff --check` found changes to apply, `codegen --check` found the file out of date, or `app check` found warnings only |
|
|
71
|
+
|
|
72
|
+
## JSON output
|
|
73
|
+
|
|
74
|
+
With `--json`, stdout carries **only** JSON (human-readable text goes to stderr), so pipelines can parse it. Errors are JSON objects with `error` (a stable code) and `message`, for example `unknown_command`, `usage`, `no_credentials` (`whoami`, with the list of what is required), `rate_limited` (with `status: 429` and, when the server sent one, `retryAfterSeconds`; the CLI does not retry a 429 by itself) or `unexpected`. Some commands report a failed outcome as their normal result instead (a declined confirmation is `applied: false`, a failed or rejected deployment is `deployed: false` with `status`, `error` or `issues`, `app check` lists its `failures`); in every case the exit code is `1`, so branch on the exit code first and read `error` second.
|
|
75
|
+
|
|
76
|
+
## Environment variables
|
|
77
|
+
|
|
78
|
+
| Variable | Used for |
|
|
79
|
+
| --- | --- |
|
|
80
|
+
| `ENFORMIK_CONSOLE_URL`, `ENFORMIK_DEPLOY_KEY` | Site deployment (`site deploy`, `site status`, `app check`, `whoami`) |
|
|
81
|
+
| `ENFORMIK_APP_URL`, `ENFORMIK_CLIENT_ID`, `ENFORMIK_CLIENT_SECRET` | Schema commands, `app check`, `whoami` |
|
|
82
|
+
| `ENFORMIK_WORKSPACE_ID` | Workspace to use (otherwise the app's default) |
|
|
83
|
+
| `ENFORMIK_DOCS_URL` | Where `ai init` downloads the guides from (default `https://help.enformik.com`) |
|
|
84
|
+
|
|
85
|
+
The URLs can also be saved with `enformik link`; precedence is flag, then environment variable, then `.enformik/config.json`. Secrets are never read from a flag or the config file.
|
|
86
|
+
|
|
87
|
+
## Upgrading from 0.5
|
|
88
|
+
|
|
89
|
+
- All output (help, errors, generated file text) is in English; it used to be Turkish. This includes the `message` strings in `--json` output, so a parser that matched Turkish text must be updated (match the stable `error` code instead).
|
|
90
|
+
- `ai init --lang` now defaults to `en` (it was `tr`); pass `--lang tr` for the Turkish guide. Files of the other language already in `.enformik/ai/` are left in place.
|
|
91
|
+
- The confirmation prompt is `[y/N]` and accepts only `y` or `yes` (it was `[e/H]` and also accepted `e` and `evet`). If the input closes before an answer, that counts as "no": the command stops with exit code `1` instead of ending silently. A CI script that piped `e` or `evet` into `schema push` now gets exit code `1` and nothing is applied; pass `--yes` instead.
|
|
92
|
+
- `whoami --json` without credentials prints a `no_credentials` JSON error instead of nothing.
|
|
93
|
+
- A rate limit (HTTP 429) is reported as `Rate limited by the server; retry in about N s.` (and as `rate_limited` in JSON); there is no automatic retry.
|
|
94
|
+
- A mistyped command with `--help` (`enformik sitee --help`) is now an `unknown_command` error with exit code `1`; it used to print the general help and exit `0`.
|
|
95
|
+
- `app check --expect-host` checks the application host before any request is sent; the site side ignores it.
|
|
96
|
+
- If the reply to a deployment's `finalize` is lost, the CLI now reads the deployment's real state and carries on instead of reporting a failure.
|
|
97
|
+
|
|
98
|
+
## Security
|
|
99
|
+
|
|
100
|
+
- Secrets are provided **only through environment variables**; there is no command-line flag or config file for them (a `.enformik/config.json` that contains a secret is rejected).
|
|
101
|
+
- Credentials are sent only over HTTPS (except `localhost`); redirects are not followed.
|
|
102
|
+
- The access token is kept in memory only.
|
|
103
|
+
|
|
104
|
+
Detailed guide: the "CLI" page in the Enformik developer documentation.
|
package/dist/auth.js
CHANGED
|
@@ -1,21 +1,23 @@
|
|
|
1
1
|
import { ApiError, CliError } from './errors.js';
|
|
2
|
-
import { assertSecureUrl, ensureOk, joinUrl, request } from './http.js';
|
|
2
|
+
import { assertSecureUrl, ensureOk, joinUrl, rateLimitedError, request } from './http.js';
|
|
3
3
|
export async function discoverApp(appUrl, version, fetchImpl) {
|
|
4
|
-
assertSecureUrl(appUrl, '
|
|
4
|
+
assertSecureUrl(appUrl, 'App URL');
|
|
5
5
|
const res = await request(joinUrl(appUrl, '/api/config'), { retries: 2, headers: { 'X-Enformik-CLI-Version': version }, fetchImpl });
|
|
6
6
|
try {
|
|
7
7
|
ensureOk(res);
|
|
8
8
|
}
|
|
9
9
|
catch (e) {
|
|
10
|
+
if (e instanceof ApiError && e.status === 429)
|
|
11
|
+
throw e;
|
|
10
12
|
if (e instanceof ApiError)
|
|
11
|
-
throw new CliError(`
|
|
13
|
+
throw new CliError(`Could not read the app configuration (HTTP ${e.status}). Is the URL correct: ${appUrl}?`);
|
|
12
14
|
throw e;
|
|
13
15
|
}
|
|
14
16
|
const j = (res.json ?? {});
|
|
15
17
|
if (!j.oidc_authority)
|
|
16
|
-
throw new CliError('
|
|
18
|
+
throw new CliError('The app configuration is not in the expected format (oidc_authority is missing).');
|
|
17
19
|
if (!j.api_audience) {
|
|
18
|
-
throw new CliError('
|
|
20
|
+
throw new CliError('This app does not accept API client tokens (api_audience is empty). Open a support ticket.');
|
|
19
21
|
}
|
|
20
22
|
return { oidcAuthority: j.oidc_authority, apiAudience: j.api_audience, tenantId: j.tenant_id ?? '' };
|
|
21
23
|
}
|
|
@@ -24,7 +26,7 @@ export function buildScope(audience) {
|
|
|
24
26
|
}
|
|
25
27
|
export async function fetchAccessToken(app, clientId, clientSecret, version, fetchImpl) {
|
|
26
28
|
const url = joinUrl(app.oidcAuthority, '/oauth/v2/token');
|
|
27
|
-
assertSecureUrl(url, '
|
|
29
|
+
assertSecureUrl(url, 'Identity provider URL');
|
|
28
30
|
const res = await request(url, {
|
|
29
31
|
method: 'POST',
|
|
30
32
|
headers: {
|
|
@@ -38,13 +40,15 @@ export async function fetchAccessToken(app, clientId, clientSecret, version, fet
|
|
|
38
40
|
});
|
|
39
41
|
if (!res.ok) {
|
|
40
42
|
if (res.status === 400 || res.status === 401) {
|
|
41
|
-
throw new CliError('
|
|
43
|
+
throw new CliError('Authentication failed: the client ID/secret is invalid or the client has been revoked. Regenerate the secret in the console.');
|
|
42
44
|
}
|
|
43
|
-
|
|
45
|
+
if (res.status === 429)
|
|
46
|
+
throw rateLimitedError(res);
|
|
47
|
+
throw new CliError(`Could not obtain a token (HTTP ${res.status}).`);
|
|
44
48
|
}
|
|
45
49
|
const j = res.json;
|
|
46
50
|
if (!j?.access_token)
|
|
47
|
-
throw new CliError('
|
|
51
|
+
throw new CliError('The identity provider did not return the expected response (access_token is missing).');
|
|
48
52
|
return { token: j.access_token, expiresInSec: j.expires_in ?? 0, claims: decodeJwtPayload(j.access_token) };
|
|
49
53
|
}
|
|
50
54
|
/** İmza DOĞRULANMAZ (yalnızca whoami'de bilgi göstermek için); yetkilendirme sunucuda yapılır. */
|
package/dist/cli.js
CHANGED
|
@@ -25,45 +25,45 @@ const OPTIONS = {
|
|
|
25
25
|
};
|
|
26
26
|
const GLOBAL = ['json', 'help', 'version', 'debug', 'config'];
|
|
27
27
|
export const COMMANDS = {
|
|
28
|
-
link: { flags: ['console-url', 'app-url', 'workspace', 'module', 'manifest', 'dir', 'no-verify'], run: link, usage: 'enformik link [--console-url U] [--app-url U] [--workspace ID] [--module SLUG] [--manifest
|
|
28
|
+
link: { flags: ['console-url', 'app-url', 'workspace', 'module', 'manifest', 'dir', 'no-verify'], run: link, usage: 'enformik link [--console-url U] [--app-url U] [--workspace ID] [--module SLUG] [--manifest PATH] [--dir DIR]' },
|
|
29
29
|
whoami: { flags: ['console-url', 'app-url', 'workspace'], run: whoami, usage: 'enformik whoami' },
|
|
30
|
-
'ai init': { flags: ['lang', 'docs-url', 'offline'], run: aiInit, usage: 'enformik ai init [--lang tr
|
|
31
|
-
'app check': { flags: ['console-url', 'app-url', 'workspace', 'module', 'manifest', 'entity', 'expect-host'], run: appCheck, usage: 'enformik app check [--entity a,b] [--manifest module.json]' },
|
|
30
|
+
'ai init': { flags: ['lang', 'docs-url', 'offline'], run: aiInit, usage: 'enformik ai init [--lang en|tr] [--docs-url URL] [--offline]' },
|
|
31
|
+
'app check': { flags: ['console-url', 'app-url', 'workspace', 'module', 'manifest', 'entity', 'expect-host'], run: appCheck, usage: 'enformik app check [--entity a,b] [--manifest module.json] [--expect-host <app-host>] (--expect-host checks the app host only)' },
|
|
32
32
|
codegen: { flags: ['manifest', 'out', 'check'], run: codegen, usage: 'enformik codegen [--manifest module.json] [--out src/enformik.generated.ts] [--check]' },
|
|
33
33
|
'schema diff': { flags: ['app-url', 'workspace', 'module', 'manifest', 'check', 'seed-data', 'expect-host'], run: schemaDiff, usage: 'enformik schema diff [--manifest module.json] [--check]' },
|
|
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', 'expect-host'], run: siteDeploy, usage: 'enformik site deploy [--dir dist] [--no-wait] [--timeout 600] [--expect-host <site-
|
|
38
|
-
'site status': { flags: ['console-url', 'expect-host'], run: siteStatus, usage: 'enformik site status [--expect-host <site-
|
|
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-host>]' },
|
|
38
|
+
'site status': { flags: ['console-url', 'expect-host'], run: siteStatus, usage: 'enformik site status [--expect-host <site-host>]' },
|
|
39
39
|
};
|
|
40
40
|
export function helpText(version) {
|
|
41
|
-
return `enformik ${version} — Enformik
|
|
41
|
+
return `enformik ${version} — the Enformik developer CLI
|
|
42
42
|
|
|
43
|
-
|
|
43
|
+
Usage: enformik <command> [flags]
|
|
44
44
|
|
|
45
|
-
|
|
46
|
-
link .enformik/config.json
|
|
47
|
-
whoami
|
|
48
|
-
ai init
|
|
49
|
-
app check
|
|
50
|
-
codegen
|
|
51
|
-
schema validate
|
|
52
|
-
schema diff
|
|
53
|
-
schema install
|
|
54
|
-
schema push
|
|
55
|
-
site deploy
|
|
56
|
-
site status
|
|
45
|
+
Commands
|
|
46
|
+
link Writes .enformik/config.json (URLs only; no secrets)
|
|
47
|
+
whoami Shows which identity connects to which target
|
|
48
|
+
ai init Downloads the AI agent guides from the server (.enformik/ai/) and adds an instruction block to AGENTS.md
|
|
49
|
+
app check Checks the web app's integration health (site sign-in, module, web user access; exit 1 = problem, 2 = warning)
|
|
50
|
+
codegen Generates TypeScript types + a typed data client from module.json (--check: exit 2 if out of date)
|
|
51
|
+
schema validate Validates the manifest without touching the DB (--profile ai: completeness; --offline: local only)
|
|
52
|
+
schema diff Compares the module manifest with the one in the app (--check: exit code 2 if there are changes)
|
|
53
|
+
schema install Installs the module into the app for the FIRST time (role: Schema builder; --dry-run only shows)
|
|
54
|
+
schema push Applies changes (--yes is required in non-interactive environments)
|
|
55
|
+
site deploy Publishes the built site (default: dist) to the website environment
|
|
56
|
+
site status Shows the target site and the live version
|
|
57
57
|
|
|
58
|
-
|
|
58
|
+
Global flags: --json (machine-readable output), --config PATH, --debug, -h/--help, -v/--version
|
|
59
59
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
Site
|
|
60
|
+
Secrets are provided only through environment variables:
|
|
61
|
+
Schema upgrade : ENFORMIK_CLIENT_ID, ENFORMIK_CLIENT_SECRET (+ ENFORMIK_APP_URL)
|
|
62
|
+
Site deployment: ENFORMIK_DEPLOY_KEY (+ ENFORMIK_CONSOLE_URL)
|
|
63
63
|
|
|
64
|
-
|
|
64
|
+
Exit codes: 0 success · 1 error · 2 changes pending (--check) or warnings only (app check)
|
|
65
65
|
|
|
66
|
-
|
|
66
|
+
Details: run "enformik <command> --help" for command help`;
|
|
67
67
|
}
|
|
68
68
|
function readVersion() {
|
|
69
69
|
try {
|
|
@@ -82,7 +82,7 @@ export async function main(d) {
|
|
|
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
84
|
const out = createOutput({ json: d.argv.includes('--json'), stderr: d.stderr, stdout: d.stdout, env: d.env });
|
|
85
|
-
out.error(`${e.message}\
|
|
85
|
+
out.error(`${e.message}\nRun "enformik --help" for usage.`);
|
|
86
86
|
out.result({ error: 'usage', message: e.message });
|
|
87
87
|
return 1;
|
|
88
88
|
}
|
|
@@ -97,37 +97,41 @@ export async function main(d) {
|
|
|
97
97
|
const find = (k) => (Object.hasOwn(COMMANDS, k) ? COMMANDS[k] : undefined);
|
|
98
98
|
const cmd = find(name) ?? find(positionals[0] ?? '');
|
|
99
99
|
const cmdName = find(name) ? name : positionals[0] ?? '';
|
|
100
|
-
|
|
100
|
+
// "enformik <typo> --help" is an error, not the general help; a command prefix ("schema --help") still shows the help.
|
|
101
|
+
const first = positionals[0];
|
|
102
|
+
const isPrefix = first !== undefined && Object.keys(COMMANDS).some((k) => k.startsWith(first + ' '));
|
|
103
|
+
const unknownWithHelp = values.help === true && first !== undefined && first !== 'help' && !cmd && (positionals.length >= 2 || !isPrefix);
|
|
104
|
+
if (!unknownWithHelp && (values.help || positionals.length === 0 || positionals[0] === 'help')) {
|
|
101
105
|
if (cmd && values.help) {
|
|
102
|
-
out.info(`
|
|
106
|
+
out.info(`Usage: ${cmd.usage}`);
|
|
103
107
|
return 0;
|
|
104
108
|
}
|
|
105
109
|
out.info(helpText(d.version));
|
|
106
110
|
return positionals.length === 0 && !values.help ? 1 : 0;
|
|
107
111
|
}
|
|
108
112
|
if (!cmd) {
|
|
109
|
-
out.error(`
|
|
110
|
-
out.result({ error: 'unknown_command', message: `
|
|
113
|
+
out.error(`Unknown command: ${positionals.join(' ')}\nRun "enformik --help" to see the commands.`);
|
|
114
|
+
out.result({ error: 'unknown_command', message: `Unknown command: ${positionals.join(' ')}` });
|
|
111
115
|
return 1;
|
|
112
116
|
}
|
|
113
117
|
const used = Object.entries(values).filter(([, v]) => v !== undefined).map(([k]) => k);
|
|
114
118
|
const bad = used.filter((k) => !GLOBAL.includes(k) && !cmd.flags.includes(k));
|
|
115
119
|
if (bad.length > 0) {
|
|
116
|
-
out.error(`"${cmdName}"
|
|
117
|
-
out.result({ error: 'usage', message: `"${cmdName}"
|
|
120
|
+
out.error(`Invalid flag for "${cmdName}": ${bad.map((b) => '--' + b).join(', ')}\nUsage: ${cmd.usage}`);
|
|
121
|
+
out.result({ error: 'usage', message: `Invalid flag for "${cmdName}": ${bad.map((b) => '--' + b).join(', ')}` });
|
|
118
122
|
return 1;
|
|
119
123
|
}
|
|
120
124
|
const extra = positionals.slice(cmdName.split(' ').length);
|
|
121
125
|
if (extra.length > 0) {
|
|
122
|
-
out.error(`
|
|
123
|
-
out.result({ error: 'usage', message: `
|
|
126
|
+
out.error(`Unexpected argument: ${extra.join(' ')}\nUsage: ${cmd.usage}`);
|
|
127
|
+
out.result({ error: 'usage', message: `Unexpected argument: ${extra.join(' ')}` });
|
|
124
128
|
return 1;
|
|
125
129
|
}
|
|
126
130
|
try {
|
|
127
131
|
const { config } = loadConfig(d.cwd, typeof values.config === 'string' ? values.config : undefined);
|
|
128
132
|
const flags = values;
|
|
129
133
|
const settings = resolveSettings(config, d.env, flags);
|
|
130
|
-
const prompts = realPrompts();
|
|
134
|
+
const prompts = realPrompts(d.stdin);
|
|
131
135
|
const ctx = {
|
|
132
136
|
out, settings, flags, cwd: d.cwd, env: d.env, version: d.version,
|
|
133
137
|
isTTY: d.isTTY ?? !!process.stdin.isTTY,
|
|
@@ -144,12 +148,17 @@ export async function main(d) {
|
|
|
144
148
|
out.result({ error: e.code ?? 'error', message: e.message });
|
|
145
149
|
return e.exitCode;
|
|
146
150
|
}
|
|
151
|
+
if (e instanceof ApiError && e.status === 429) {
|
|
152
|
+
out.error(e.message);
|
|
153
|
+
out.result({ error: e.code ?? 'rate_limited', message: e.message, status: 429, ...(e.retryAfterSeconds !== undefined ? { retryAfterSeconds: e.retryAfterSeconds } : {}) });
|
|
154
|
+
return 1;
|
|
155
|
+
}
|
|
147
156
|
if (e instanceof ApiError) {
|
|
148
|
-
out.error(`
|
|
157
|
+
out.error(`Server error (HTTP ${e.status}): ${e.message}`);
|
|
149
158
|
out.result({ error: e.code ?? 'api_error', message: e.message, status: e.status });
|
|
150
159
|
return 1;
|
|
151
160
|
}
|
|
152
|
-
out.error(`
|
|
161
|
+
out.error(`Unexpected error: ${e.message}`);
|
|
153
162
|
out.result({ error: 'unexpected', message: e.message });
|
|
154
163
|
if (values.debug)
|
|
155
164
|
out.error(e.stack ?? '');
|
|
@@ -165,5 +174,5 @@ catch {
|
|
|
165
174
|
return p;
|
|
166
175
|
} }
|
|
167
176
|
if (invoked && invoked === fileURLToPath(import.meta.url)) {
|
|
168
|
-
main({ argv: process.argv.slice(2), env: process.env, cwd: process.cwd(), version: readVersion() }).then((code) => { process.exitCode = code; }, (e) => { process.stderr.write(`
|
|
177
|
+
main({ argv: process.argv.slice(2), env: process.env, cwd: process.cwd(), version: readVersion() }).then((code) => { process.exitCode = code; }, (e) => { process.stderr.write(`Unexpected error: ${e.message}\n`); process.exitCode = 1; });
|
|
169
178
|
}
|
package/dist/codegen.js
CHANGED
|
@@ -56,15 +56,15 @@ export function generateClient(manifest) {
|
|
|
56
56
|
const m = manifest.module ?? {};
|
|
57
57
|
const entities = (manifest.entities ?? []).filter((e) => e && typeof e.slug === 'string');
|
|
58
58
|
const out = [];
|
|
59
|
-
out.push(`//
|
|
60
|
-
out.push('//
|
|
59
|
+
out.push(`// This file was GENERATED by \`enformik codegen\` from the ${m.slug ?? 'module'}@${m.version ?? '?'} manifest — do not edit by hand.`);
|
|
60
|
+
out.push('// To regenerate: npx @enformik/cli codegen (CI: codegen --check)');
|
|
61
61
|
out.push('');
|
|
62
|
-
out.push('/** @enformik/auth
|
|
62
|
+
out.push('/** The part of the @enformik/auth client that this file needs. */');
|
|
63
63
|
out.push('export interface EnformikAuthLike { fetch(path: string, init?: RequestInit): Promise<Response> }');
|
|
64
64
|
out.push('');
|
|
65
65
|
out.push('export class EnformikApiError extends Error {');
|
|
66
66
|
out.push(' constructor(public readonly status: number, public readonly body: unknown) {');
|
|
67
|
-
out.push(" super(`Enformik API
|
|
67
|
+
out.push(" super(`Enformik API error (HTTP ${status})`);");
|
|
68
68
|
out.push(" this.name = 'EnformikApiError';");
|
|
69
69
|
out.push(' }');
|
|
70
70
|
out.push('}');
|
|
@@ -73,7 +73,7 @@ export function generateClient(manifest) {
|
|
|
73
73
|
out.push('export interface ListOptions<R> {');
|
|
74
74
|
out.push(' page?: number; limit?: number; search?: string;');
|
|
75
75
|
out.push(" sort?: keyof R & string; sortDir?: 'ASC' | 'DESC';");
|
|
76
|
-
out.push(' /**
|
|
76
|
+
out.push(' /** Field name -> value equality/contains filter. */');
|
|
77
77
|
out.push(' filter?: Partial<Record<keyof R & string, string | number | boolean>>;');
|
|
78
78
|
out.push('}');
|
|
79
79
|
out.push('');
|
|
@@ -99,7 +99,7 @@ export function generateClient(manifest) {
|
|
|
99
99
|
out.push(`export type ${T}Patch = Partial<${T}Input>;`);
|
|
100
100
|
out.push('');
|
|
101
101
|
}
|
|
102
|
-
out.push('/**
|
|
102
|
+
out.push('/** Typed data client: `const api = createClient(auth); await api.xyz.list({ limit: 20 })`. */');
|
|
103
103
|
out.push('export function createClient(auth: EnformikAuthLike) {');
|
|
104
104
|
out.push(' async function call<T>(method: string, path: string, body?: unknown): Promise<{ data: T; res: Response }> {');
|
|
105
105
|
out.push(" const res = await auth.fetch(path, { method, headers: body === undefined ? undefined : { 'Content-Type': 'application/json' }, body: body === undefined ? undefined : JSON.stringify(body) });");
|
package/dist/commands/ai.js
CHANGED
|
@@ -2,7 +2,7 @@ import { createHash } from 'node:crypto';
|
|
|
2
2
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
3
3
|
import { dirname, join, resolve } from 'node:path';
|
|
4
4
|
import { CliError } from '../errors.js';
|
|
5
|
-
import { assertSecureUrl, joinUrl, request } from '../http.js';
|
|
5
|
+
import { assertSecureUrl, joinUrl, rateLimitedError, request } from '../http.js';
|
|
6
6
|
export const DEFAULT_DOCS_URL = 'https://help.enformik.com';
|
|
7
7
|
const MAX_FILE_BYTES = 2 * 1024 * 1024;
|
|
8
8
|
const SAFE_NAME = /^[A-Za-z0-9][A-Za-z0-9._-]*(\/[A-Za-z0-9][A-Za-z0-9._-]*)*$/;
|
|
@@ -20,7 +20,7 @@ export function cmpVersion(a, b) {
|
|
|
20
20
|
function parseIndex(raw) {
|
|
21
21
|
const x = raw;
|
|
22
22
|
if (!x || x.schemaVersion !== 1 || typeof x.guideVersion !== 'string' || !x.files || !x.shared || !Array.isArray(x.languages)) {
|
|
23
|
-
throw new CliError('
|
|
23
|
+
throw new CliError('The guide index (index.json) is not in the expected format; update the CLI or try again later.');
|
|
24
24
|
}
|
|
25
25
|
return x;
|
|
26
26
|
}
|
|
@@ -59,75 +59,79 @@ export function upsertBlock(existing, block) {
|
|
|
59
59
|
if (s >= 0) {
|
|
60
60
|
const e = existing.indexOf(END, s);
|
|
61
61
|
if (e < 0)
|
|
62
|
-
throw new CliError(`
|
|
62
|
+
throw new CliError(`The instruction block has a start marker but no end marker (${END}); the file looks corrupted. The file was not modified: repair the marker, or delete the block by hand and run again.`);
|
|
63
63
|
return existing.slice(0, s) + block + existing.slice(e + END.length).replace(/^\n/, '');
|
|
64
64
|
}
|
|
65
65
|
return existing.replace(/\n*$/, '\n\n') + block;
|
|
66
66
|
}
|
|
67
67
|
async function download(base, rel, sha, ctx) {
|
|
68
68
|
if (!SAFE_NAME.test(rel) || rel.includes('..'))
|
|
69
|
-
throw new CliError(`
|
|
69
|
+
throw new CliError(`Unsafe file name in the guide index: ${rel}`);
|
|
70
70
|
const res = await request(joinUrl(base, rel), { retries: 2, fetchImpl: ctx.fetchImpl, headers: { 'X-Enformik-CLI-Version': ctx.version } });
|
|
71
|
+
if (res.status === 429)
|
|
72
|
+
throw rateLimitedError(res);
|
|
71
73
|
if (!res.ok)
|
|
72
|
-
throw new CliError(`
|
|
74
|
+
throw new CliError(`Could not download guide file: ${rel} (HTTP ${res.status}).`);
|
|
73
75
|
if (Buffer.byteLength(res.text) > MAX_FILE_BYTES)
|
|
74
|
-
throw new CliError(`
|
|
76
|
+
throw new CliError(`Guide file is larger than expected: ${rel}.`);
|
|
75
77
|
if (createHash('sha256').update(res.text).digest('hex') !== sha) {
|
|
76
|
-
throw new CliError(`
|
|
78
|
+
throw new CliError(`Checksum mismatch for guide file: ${rel} (the download may be corrupted or the release incomplete; try again).`);
|
|
77
79
|
}
|
|
78
80
|
return res.text;
|
|
79
81
|
}
|
|
80
82
|
/**
|
|
81
|
-
* enformik ai init [--lang tr
|
|
82
|
-
*
|
|
83
|
-
* `.enformik/ai/`
|
|
83
|
+
* enformik ai init [--lang en|tr] [--docs-url URL] [--offline]
|
|
84
|
+
* Downloads the AI agent guides from the Enformik documentation site, verifies checksums,
|
|
85
|
+
* writes them under `.enformik/ai/` and adds a short instruction block to AGENTS.md (and CLAUDE.md if present). Writes no secrets.
|
|
84
86
|
*/
|
|
85
87
|
export async function aiInit(ctx) {
|
|
86
88
|
const { out } = ctx;
|
|
87
|
-
const lang = ctx.flags.lang === undefined ? '
|
|
89
|
+
const lang = ctx.flags.lang === undefined ? 'en' : ctx.flags.lang;
|
|
88
90
|
if (lang !== 'tr' && lang !== 'en')
|
|
89
|
-
throw new CliError(`--lang "
|
|
91
|
+
throw new CliError(`--lang must be "en" or "tr" (got: ${String(lang)}).`);
|
|
90
92
|
const root = resolve(ctx.cwd, '.enformik', 'ai');
|
|
91
93
|
let index;
|
|
92
94
|
let source = 'server';
|
|
93
95
|
if (ctx.flags.offline === true) {
|
|
94
96
|
const cached = join(root, 'index.json');
|
|
95
97
|
if (!existsSync(cached))
|
|
96
|
-
throw new CliError('
|
|
98
|
+
throw new CliError('No guide in the cache. Run `enformik ai init` online first.');
|
|
97
99
|
let raw;
|
|
98
100
|
try {
|
|
99
101
|
raw = JSON.parse(readFileSync(cached, 'utf8'));
|
|
100
102
|
}
|
|
101
103
|
catch {
|
|
102
|
-
throw new CliError('
|
|
104
|
+
throw new CliError('Could not read the cached guide index (index.json); download it again with `enformik ai init` online.');
|
|
103
105
|
}
|
|
104
106
|
index = parseIndex(raw);
|
|
105
107
|
if (cmpVersion(ctx.version, index.minCliVersion) < 0) {
|
|
106
|
-
throw new CliError(
|
|
108
|
+
throw new CliError(`The cached guide requires CLI ${index.minCliVersion} or newer (you have ${ctx.version}). Update with \`npm install -g @enformik/cli\`.`);
|
|
107
109
|
}
|
|
108
110
|
source = 'cache';
|
|
109
|
-
out.warn(
|
|
111
|
+
out.warn(`Using the cached guide (version ${index.guideVersion}); it may be out of date.`);
|
|
110
112
|
if (!existsSync(join(root, lang, 'overview.md')))
|
|
111
|
-
throw new CliError(
|
|
113
|
+
throw new CliError(`No guide in language "${lang}" in the cache.`);
|
|
112
114
|
}
|
|
113
115
|
else {
|
|
114
116
|
const base = joinUrl(ctx.settings.docsUrl, '/ai/');
|
|
115
|
-
assertSecureUrl(base, '
|
|
117
|
+
assertSecureUrl(base, 'Docs URL');
|
|
116
118
|
let res;
|
|
117
119
|
try {
|
|
118
120
|
res = await request(joinUrl(base, 'index.json'), { retries: 2, fetchImpl: ctx.fetchImpl, headers: { 'X-Enformik-CLI-Version': ctx.version } });
|
|
119
121
|
}
|
|
120
122
|
catch (e) {
|
|
121
|
-
throw new CliError(`${e.message}\
|
|
123
|
+
throw new CliError(`${e.message}\nGuides are downloaded from the server; if you are offline and have downloaded them before, use \`enformik ai init --offline\`.`);
|
|
122
124
|
}
|
|
125
|
+
if (res.status === 429)
|
|
126
|
+
throw rateLimitedError(res);
|
|
123
127
|
if (!res.ok)
|
|
124
|
-
throw new CliError(`
|
|
128
|
+
throw new CliError(`Could not fetch the guide index (HTTP ${res.status}): ${joinUrl(base, 'index.json')}`);
|
|
125
129
|
index = parseIndex(res.json);
|
|
126
130
|
if (cmpVersion(ctx.version, index.minCliVersion) < 0) {
|
|
127
|
-
throw new CliError(`
|
|
131
|
+
throw new CliError(`This guide requires CLI ${index.minCliVersion} or newer (you have ${ctx.version}). Update with \`npm install -g @enformik/cli\`.`);
|
|
128
132
|
}
|
|
129
133
|
if (!index.languages.includes(lang) || !index.files[lang])
|
|
130
|
-
throw new CliError(`
|
|
134
|
+
throw new CliError(`The guide is not available in "${lang}" (available: ${index.languages.join(', ')}).`);
|
|
131
135
|
// Önce hepsini indirip doğrula, sonra yaz: yarım rehber bırakma.
|
|
132
136
|
const contents = new Map();
|
|
133
137
|
for (const [name, sha] of Object.entries(index.files[lang]))
|
|
@@ -160,9 +164,9 @@ export async function aiInit(ctx) {
|
|
|
160
164
|
written.push(u.f);
|
|
161
165
|
}
|
|
162
166
|
const fileCount = Object.keys(index.files[lang] ?? {}).length + Object.keys(index.shared).length;
|
|
163
|
-
out.info(`✓
|
|
164
|
-
out.info(written.length > 0 ? `
|
|
165
|
-
out.info('
|
|
167
|
+
out.info(`✓ Agent guide ready (version ${index.guideVersion}, ${lang}, ${fileCount} files, source: ${source === 'server' ? 'server' : 'cache'}) → .enformik/ai/`);
|
|
168
|
+
out.info(written.length > 0 ? ` Instruction block updated: ${written.join(', ')}` : ' Instruction block already up to date.');
|
|
169
|
+
out.info(' Next step: have the agent read `.enformik/ai/' + lang + '/overview.md`.');
|
|
166
170
|
out.result({ guideVersion: index.guideVersion, lang, source, files: fileCount, agentFiles: written, dir: '.enformik/ai' });
|
|
167
171
|
return 0;
|
|
168
172
|
}
|