layero 0.11.3 → 0.11.5
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 +1 -1
- package/dist/agent.js +9 -1
- package/dist/api.js +5 -2
- package/dist/bin/layero.js +139 -133
- package/dist/commands/data-api.js +36 -36
- package/dist/commands/data-probe.js +21 -19
- package/dist/commands/deploy.js +7 -3
- package/dist/commands/projects.js +18 -19
- package/dist/detect.js +28 -0
- package/dist/pack.js +3 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -357,7 +357,7 @@ Full reference: <https://docs.layero.ru/cli/json-events>
|
|
|
357
357
|
|
|
358
358
|
`layero deploy` honours `.gitignore` and `.layeroignore`. Always excluded:
|
|
359
359
|
`node_modules`, `.git`, `dist`, `build`, `.next`, `.env*`, `.DS_Store`, and
|
|
360
|
-
the rule files themselves. Maximum archive size is
|
|
360
|
+
the rule files themselves. Maximum archive size is 500 MB.
|
|
361
361
|
|
|
362
362
|
## Config
|
|
363
363
|
|
package/dist/agent.js
CHANGED
|
@@ -140,6 +140,8 @@ function renderHuman(event) {
|
|
|
140
140
|
process.stdout.write(` ${event.hint}\n`);
|
|
141
141
|
if (event.next_action)
|
|
142
142
|
process.stdout.write(` → ${event.next_action}\n`);
|
|
143
|
+
for (const w of event.layero_warnings ?? [])
|
|
144
|
+
process.stdout.write(`! ${w}\n`);
|
|
143
145
|
break;
|
|
144
146
|
case "plan": {
|
|
145
147
|
const src = event.sources;
|
|
@@ -158,6 +160,8 @@ function renderHuman(event) {
|
|
|
158
160
|
process.stdout.write(` ${event.hint}\n`);
|
|
159
161
|
if (event.next_action)
|
|
160
162
|
process.stdout.write(` → ${event.next_action}\n`);
|
|
163
|
+
for (const w of event.layero_warnings ?? [])
|
|
164
|
+
process.stdout.write(`! ${w}\n`);
|
|
161
165
|
break;
|
|
162
166
|
}
|
|
163
167
|
case "prebuilt":
|
|
@@ -173,7 +177,11 @@ function renderHuman(event) {
|
|
|
173
177
|
// intentionally quiet in human mode — the next stage line takes over
|
|
174
178
|
break;
|
|
175
179
|
case "setup_applied":
|
|
176
|
-
|
|
180
|
+
// Фреймворк здесь — что увидел детект, а не записанная настройка:
|
|
181
|
+
// сборщик определяет его по репозиторию на каждой сборке.
|
|
182
|
+
process.stdout.write(event.framework
|
|
183
|
+
? `✓ Setup complete (detected: ${event.framework})\n`
|
|
184
|
+
: `✓ Setup complete\n`);
|
|
177
185
|
break;
|
|
178
186
|
case "setup_pending":
|
|
179
187
|
process.stdout.write(`· Проект ждёт настройки в панели: ${event.url}\n`);
|
package/dist/api.js
CHANGED
|
@@ -131,8 +131,11 @@ export class ApiClient {
|
|
|
131
131
|
* это право: детект ошибается, и репозиторий, который эвристика считает «не
|
|
132
132
|
* бэкендом», прекрасно работает одним сервисом.
|
|
133
133
|
*/
|
|
134
|
-
setRuntimeType(projectId, projectType, force = false
|
|
135
|
-
|
|
134
|
+
setRuntimeType(projectId, projectType, force = false,
|
|
135
|
+
// Кто назвал тип: `user` — человек (`--type`), `platform` — детект. Тип от
|
|
136
|
+
// детекта сборщик вправе уточнить по архиву; выбор человека — нет.
|
|
137
|
+
origin = "user") {
|
|
138
|
+
return this.request("POST", `/projects/${projectId}/runtime-type`, { project_type: projectType, force, origin });
|
|
136
139
|
}
|
|
137
140
|
updateProject(projectId, input) {
|
|
138
141
|
return this.request("PATCH", `/projects/${projectId}`, input);
|
package/dist/bin/layero.js
CHANGED
|
@@ -44,14 +44,14 @@ async function main() {
|
|
|
44
44
|
.option("--debug", "print a full stack trace when a command errors");
|
|
45
45
|
program
|
|
46
46
|
.command("login")
|
|
47
|
-
.description("
|
|
48
|
-
.option("--no-browser", "
|
|
49
|
-
.addHelpText("after", "\n
|
|
47
|
+
.description("Sign in through the browser (an emailed code or Yandex ID): prints a one-time URL and code.")
|
|
48
|
+
.option("--no-browser", "do not open the browser — only print the URL and code")
|
|
49
|
+
.addHelpText("after", "\nExamples:\n" +
|
|
50
50
|
" $ layero login\n" +
|
|
51
|
-
" $ layero login --no-browser # SSH,
|
|
52
|
-
"\
|
|
51
|
+
" $ layero login --no-browser # SSH, container, agent environment\n" +
|
|
52
|
+
"\nA human sign-in does not work for CI and agents — they need a long-lived token:\n" +
|
|
53
53
|
" $ layero token create ci\n" +
|
|
54
|
-
" $ LAYERO_TOKEN
|
|
54
|
+
" $ LAYERO_TOKEN=<token> npx layero@latest deploy")
|
|
55
55
|
.action(async (opts) => {
|
|
56
56
|
await loginCmd(opts);
|
|
57
57
|
});
|
|
@@ -65,14 +65,14 @@ async function main() {
|
|
|
65
65
|
.action(whoamiCmd);
|
|
66
66
|
program
|
|
67
67
|
.command("username <value>")
|
|
68
|
-
.description("
|
|
69
|
-
"
|
|
70
|
-
"
|
|
71
|
-
"
|
|
68
|
+
.description("Set the account username — it is also the address of your personal organization. " +
|
|
69
|
+
"Without it the platform has nowhere to put a project. " +
|
|
70
|
+
"In an interactive terminal `login` and `deploy` ask for it themselves; " +
|
|
71
|
+
"this command is for agents and CI, where there is nobody to ask.")
|
|
72
72
|
.addHelpText("after", "\nExamples:\n" +
|
|
73
73
|
" $ layero username alice\n" +
|
|
74
74
|
" $ layero username my-team-bot\n\n" +
|
|
75
|
-
"
|
|
75
|
+
"Lowercase Latin letters, digits and hyphens; 2–32 characters.")
|
|
76
76
|
.action(async (value) => {
|
|
77
77
|
const cfg = await loadConfig();
|
|
78
78
|
await usernameSetCmd(value, new ApiClient(cfg));
|
|
@@ -87,86 +87,92 @@ async function main() {
|
|
|
87
87
|
});
|
|
88
88
|
const projects = program
|
|
89
89
|
.command("projects")
|
|
90
|
-
.description("
|
|
90
|
+
.description("The account's projects: list, create from a repository, delete.");
|
|
91
91
|
projects
|
|
92
92
|
.command("list")
|
|
93
|
-
.description("
|
|
93
|
+
.description("List your projects.")
|
|
94
94
|
.action(projectsListCmd);
|
|
95
95
|
projects
|
|
96
96
|
.command("create")
|
|
97
|
-
.description("
|
|
98
|
-
"GitHub
|
|
99
|
-
.requiredOption("--repo <provider:owner/repo>", "
|
|
100
|
-
.option("--branch <name>", "
|
|
101
|
-
.option("--name <name>", "
|
|
102
|
-
.option("--org <slug>", "
|
|
103
|
-
.option("--no-deploy", "
|
|
104
|
-
.addHelpText("after", "\n
|
|
97
|
+
.description("Create a project from a repository of a connected Git provider — path (a): push to a branch = preview, push to main = production. " +
|
|
98
|
+
"GitHub is connected through the installed Layero GitHub App, other providers through `layero sources connect`.")
|
|
99
|
+
.requiredOption("--repo <provider:owner/repo>", "repository: github:acme/site, gitverse:acme/site, gitlab:group/sub/project")
|
|
100
|
+
.option("--branch <name>", "the project's main branch (default: the repository's default branch)")
|
|
101
|
+
.option("--name <name>", "project name (default: the repository name)")
|
|
102
|
+
.option("--org <slug>", "organization (default: your only one, otherwise your personal organization)")
|
|
103
|
+
.option("--no-deploy", "do not finish the setup or start the first build — the project stays in the setup wizard in the dashboard (app.layero.ru)")
|
|
104
|
+
.addHelpText("after", "\nExamples:\n" +
|
|
105
105
|
" $ layero projects create --repo github:acme/site\n" +
|
|
106
106
|
" $ layero projects create --repo gitverse:acme/site --branch develop --json\n" +
|
|
107
|
-
"\
|
|
108
|
-
"
|
|
109
|
-
"
|
|
110
|
-
"
|
|
111
|
-
"
|
|
107
|
+
"\nOnce the repository is linked, the command finishes the setup and starts the first\n" +
|
|
108
|
+
"build itself. The detected framework, build command and output directory are not\n" +
|
|
109
|
+
"saved in the project: the builder detects them from the repository on every build\n" +
|
|
110
|
+
"(layero.json overrides them). Saved are only the monorepo app folder and the\n" +
|
|
111
|
+
"package manager from layero.json. With --no-deploy the project stays in the setup wizard.\n" +
|
|
112
|
+
"\nEvents with --json: project_created, source_connected, webhook_installed | webhook_unavailable,\n" +
|
|
113
|
+
"then setup_applied + deploy_started | setup_pending |\n" +
|
|
114
|
+
"setup_failed (the project exists; finish the setup in the dashboard).\n" +
|
|
115
|
+
"Without a webhook a push does not start a build — add the webhook by hand\n" +
|
|
116
|
+
"using the URL from webhook_unavailable.")
|
|
112
117
|
.action(async (opts) => projectsCreateCmd({ ...opts, json: program.opts().json }));
|
|
113
118
|
projects
|
|
114
119
|
.command("delete <id_or_slug>")
|
|
115
|
-
.description("
|
|
116
|
-
.option("-y, --yes", "
|
|
120
|
+
.description("Delete a project. IRREVERSIBLE; requires a token with the admin scope (`layero token create <name> --scope admin`).")
|
|
121
|
+
.option("-y, --yes", "do not ask for confirmation (required outside a terminal)")
|
|
117
122
|
.action(async (ref, opts) => projectsDeleteCmd(ref, { ...opts, json: program.opts().json }));
|
|
118
123
|
const sources = program
|
|
119
124
|
.command("sources")
|
|
120
|
-
.description("Git
|
|
125
|
+
.description("The organization's Git providers: see which are available, connect one with a token, browse repositories.");
|
|
121
126
|
sources
|
|
122
127
|
.command("list")
|
|
123
|
-
.description("
|
|
124
|
-
.option("--org <slug>", "
|
|
128
|
+
.description("Git providers the platform supports and the organization's connections.")
|
|
129
|
+
.option("--org <slug>", "organization (default: your only one, otherwise your personal organization)")
|
|
125
130
|
.action(async (opts) => sourcesListCmd({ ...opts, json: program.opts().json }));
|
|
126
131
|
sources
|
|
127
132
|
.command("connect <provider>")
|
|
128
|
-
.description("
|
|
129
|
-
.option("--token <pat>", "
|
|
130
|
-
.option("--token-stdin", "
|
|
131
|
-
.option("--base-url <url>", "
|
|
132
|
-
.option("--name <label>", "
|
|
133
|
-
.option("--org <slug>", "
|
|
134
|
-
.addHelpText("after", "\n
|
|
133
|
+
.description("Connect a Git provider with a personal access token (PAT). The token is verified before it is saved and is never returned.")
|
|
134
|
+
.option("--token <pat>", "provider token (stays in the shell history — prefer --token-stdin)")
|
|
135
|
+
.option("--token-stdin", "read the token from stdin: echo \"$PAT\" | layero sources connect gitverse --token-stdin")
|
|
136
|
+
.option("--base-url <url>", "URL of a self-hosted instance (GitLab, GitFlic)")
|
|
137
|
+
.option("--name <label>", "connection label")
|
|
138
|
+
.option("--org <slug>", "organization (default: your only one, otherwise your personal organization)")
|
|
139
|
+
.addHelpText("after", "\nExamples:\n" +
|
|
135
140
|
" $ echo \"$GITVERSE_TOKEN\" | layero sources connect gitverse --token-stdin\n" +
|
|
136
141
|
" $ layero sources connect gitlab --token-stdin --base-url https://git.example.com < token.txt\n" +
|
|
137
|
-
"\
|
|
142
|
+
"\nProviders: `layero sources list`. GitHub is connected by installing\n" +
|
|
143
|
+
"the Layero GitHub App in the dashboard (app.layero.ru).")
|
|
138
144
|
.action(async (provider, opts) => sourcesConnectCmd(provider, { ...opts, json: program.opts().json }));
|
|
139
145
|
sources
|
|
140
146
|
.command("repos <connection_id>")
|
|
141
|
-
.description("
|
|
142
|
-
.option("--org <slug>", "
|
|
147
|
+
.description("Repositories visible to the connection's token.")
|
|
148
|
+
.option("--org <slug>", "organization (default: your only one, otherwise your personal organization)")
|
|
143
149
|
.action(async (id, opts) => sourcesReposCmd(id, { ...opts, json: program.opts().json }));
|
|
144
150
|
const envs = program
|
|
145
151
|
.command("envs")
|
|
146
|
-
.description("
|
|
152
|
+
.description("The project's environments: branches and their addresses.");
|
|
147
153
|
envs
|
|
148
154
|
.command("list")
|
|
149
|
-
.description("
|
|
150
|
-
.option("--project <id_or_slug>", "
|
|
155
|
+
.description("The project's environments with their addresses. A CLI project has one — `cli`; a project with a repository has one per branch.")
|
|
156
|
+
.option("--project <id_or_slug>", "project (default: the linked project)")
|
|
151
157
|
.action(async (opts) => envsListCmd({ ...opts, json: program.opts().json }));
|
|
152
158
|
const claim = program
|
|
153
159
|
.command("claim")
|
|
154
|
-
.description("
|
|
160
|
+
.description("Project without an account (`layero deploy --claim`): the claim status and the link to take the site over.");
|
|
155
161
|
claim
|
|
156
162
|
.command("status [code]")
|
|
157
|
-
.description("
|
|
163
|
+
.description("Claim state: alive, claimed or expired. Without a code, the code is taken from .layero/project.json.")
|
|
158
164
|
.action(async (code) => claimStatusCmd(code, { json: program.opts().json }));
|
|
159
165
|
claim
|
|
160
166
|
.command("accept [code]")
|
|
161
|
-
.description("
|
|
162
|
-
.option("--no-browser", "
|
|
167
|
+
.description("Open the claim page in the dashboard (app.layero.ru). Only a person signed in to the dashboard can accept the claim — the CLI only opens or prints the link.")
|
|
168
|
+
.option("--no-browser", "do not open the browser — only print the link")
|
|
163
169
|
.action(async (code, opts) => claimAcceptCmd(code, { ...opts, json: program.opts().json }));
|
|
164
170
|
const orgs = program
|
|
165
171
|
.command("orgs")
|
|
166
|
-
.description("
|
|
172
|
+
.description("The account's organizations: personal and team ones.");
|
|
167
173
|
orgs
|
|
168
174
|
.command("list")
|
|
169
|
-
.description("
|
|
175
|
+
.description("All organizations you are a member of.")
|
|
170
176
|
.action(orgsListCmd);
|
|
171
177
|
const deploys = program
|
|
172
178
|
.command("deploys")
|
|
@@ -174,7 +180,7 @@ async function main() {
|
|
|
174
180
|
deploys
|
|
175
181
|
.command("list")
|
|
176
182
|
.description("List recent deploys for the project's default branch (or --branch).")
|
|
177
|
-
.option("--project <id_or_slug>", "
|
|
183
|
+
.option("--project <id_or_slug>", "project (default: the one linked in .layero/project.json)")
|
|
178
184
|
.option("--branch <name>", "branch to list deploys from (default: project's default_branch)")
|
|
179
185
|
.option("--limit <n>", "max entries to show (default 20)", (v) => Number(v))
|
|
180
186
|
.action(async (opts) => {
|
|
@@ -186,7 +192,7 @@ async function main() {
|
|
|
186
192
|
hooks
|
|
187
193
|
.command("list")
|
|
188
194
|
.description("List deploy hooks for the linked project.")
|
|
189
|
-
.option("--project <id>", "
|
|
195
|
+
.option("--project <id>", "project id (default: the one linked in .layero/project.json)")
|
|
190
196
|
.action(async (opts) => {
|
|
191
197
|
await hooksListCmd(opts);
|
|
192
198
|
});
|
|
@@ -194,7 +200,7 @@ async function main() {
|
|
|
194
200
|
.command("create <name>")
|
|
195
201
|
.description("Create a new deploy hook. Prints a URL — paste it into Strapi / Sanity / "
|
|
196
202
|
+ "Contentful / GitHub Actions / cron as a POST webhook.")
|
|
197
|
-
.option("--project <id>", "
|
|
203
|
+
.option("--project <id>", "project id (default: the one linked in .layero/project.json)")
|
|
198
204
|
.option("--branch <name>", "branch to deploy when fired (default: project default_branch, evaluated at fire time)")
|
|
199
205
|
.option("--prod", "fire the hook against the production environment (default: preview)")
|
|
200
206
|
.addHelpText("after", "\nExamples:\n"
|
|
@@ -207,7 +213,7 @@ async function main() {
|
|
|
207
213
|
hooks
|
|
208
214
|
.command("delete <id>")
|
|
209
215
|
.description("Revoke a deploy hook. The URL stops working immediately.")
|
|
210
|
-
.option("--project <id>", "
|
|
216
|
+
.option("--project <id>", "project id (default: the one linked in .layero/project.json)")
|
|
211
217
|
.action(async (id, opts) => {
|
|
212
218
|
await hooksDeleteCmd(id, opts);
|
|
213
219
|
});
|
|
@@ -215,7 +221,7 @@ async function main() {
|
|
|
215
221
|
.command("promote [deploy]")
|
|
216
222
|
.description("Pin the project apex to a specific deploy (V071 production-pointer). " +
|
|
217
223
|
"Without [deploy] picks the latest ready build on --branch (defaults to the 'cli' pseudo-branch).")
|
|
218
|
-
.option("--project <id_or_slug>", "
|
|
224
|
+
.option("--project <id_or_slug>", "project (default: the one linked in .layero/project.json)")
|
|
219
225
|
.option("--branch <name>", "branch to pick latest ready deploy from (default: cli)")
|
|
220
226
|
.option("-y, --yes", "skip the confirmation prompt (CI)")
|
|
221
227
|
.addHelpText("after", "\nExamples:\n" +
|
|
@@ -232,7 +238,7 @@ async function main() {
|
|
|
232
238
|
"moves the production pointer, so the apex comes back too when it is " +
|
|
233
239
|
"served by this branch. For a SPECIFIC older deploy use " +
|
|
234
240
|
"`layero promote <commit-sha>`.")
|
|
235
|
-
.option("--project <id_or_slug>", "
|
|
241
|
+
.option("--project <id_or_slug>", "project (default: the one linked in .layero/project.json)")
|
|
236
242
|
.option("--branch <name>", "branch to roll back (default: project's default_branch)")
|
|
237
243
|
.option("--deploy <id_or_sha>", "explicit deploy id or commit sha prefix to roll back to")
|
|
238
244
|
.option("-y, --yes", "skip the confirmation prompt (CI)")
|
|
@@ -246,68 +252,68 @@ async function main() {
|
|
|
246
252
|
});
|
|
247
253
|
const env = program
|
|
248
254
|
.command("env")
|
|
249
|
-
.description("
|
|
250
|
-
const envProject = (c) => c.option("--project <id_or_slug>", "
|
|
251
|
-
envProject(env.command("list").description("
|
|
255
|
+
.description("The project's environment variables. Values are not shown — the platform does not return them.");
|
|
256
|
+
const envProject = (c) => c.option("--project <id_or_slug>", "project (default: the linked project)");
|
|
257
|
+
envProject(env.command("list").description("Variable names and value lengths."))
|
|
252
258
|
.action(async (opts) => envListCmd({ ...opts, json: program.opts().json }));
|
|
253
259
|
envProject(env
|
|
254
260
|
.command("set <pairs...>")
|
|
255
|
-
.description("
|
|
261
|
+
.description("Set variables as KEY=value. Other variables stay untouched.")).action(async (pairs, opts) => envSetCmd(pairs, { ...opts, json: program.opts().json }));
|
|
256
262
|
envProject(env
|
|
257
263
|
.command("unset <keys...>")
|
|
258
|
-
.description("
|
|
259
|
-
.option("-y, --yes", "
|
|
264
|
+
.description("Delete variables.")
|
|
265
|
+
.option("-y, --yes", "do not ask for confirmation")).action(async (keys, opts) => envUnsetCmd(keys, { ...opts, json: program.opts().json }));
|
|
260
266
|
const data = program
|
|
261
267
|
.command("data")
|
|
262
|
-
.description("Data API
|
|
268
|
+
.description("A database's Data API: keys, allowed sites, methods and access; the URL and key for the frontend.");
|
|
263
269
|
data
|
|
264
270
|
.command("env")
|
|
265
|
-
.description("
|
|
266
|
-
.option("--project <id_or_slug>", "
|
|
267
|
-
.option("-w, --write", "
|
|
268
|
-
.option("--file <path>", "
|
|
269
|
-
.addHelpText("after", "\n
|
|
270
|
-
" $ layero data env #
|
|
271
|
-
" $ layero data env --write #
|
|
272
|
-
"\
|
|
273
|
-
"
|
|
271
|
+
.description("Show the VITE_/NEXT_PUBLIC_ variables for the Data API; --write puts them in .env.local.")
|
|
272
|
+
.option("--project <id_or_slug>", "project (default: the linked project)")
|
|
273
|
+
.option("-w, --write", "write to the file instead of printing")
|
|
274
|
+
.option("--file <path>", "file name (default .env.local)")
|
|
275
|
+
.addHelpText("after", "\nExamples:\n" +
|
|
276
|
+
" $ layero data env # view\n" +
|
|
277
|
+
" $ layero data env --write # write to .env.local\n" +
|
|
278
|
+
"\nOnly the PUBLIC key is returned — the one that ends up in the bundle anyway.\n" +
|
|
279
|
+
"The platform neither stores nor returns the secret key: it is for the server.")
|
|
274
280
|
.action(async (opts) => dataEnvCmd({ ...opts, json: program.opts().json }));
|
|
275
281
|
registerDataApiCommands(data, program);
|
|
276
282
|
const db = program
|
|
277
283
|
.command("db")
|
|
278
|
-
.description("
|
|
279
|
-
const withOrg = (c) => c.option("--org <slug>", "
|
|
280
|
-
withOrg(db.command("list").description("
|
|
284
|
+
.description("The organization's databases: create, list, connect to a project, run SQL.");
|
|
285
|
+
const withOrg = (c) => c.option("--org <slug>", "organization (default: your only one, otherwise your personal organization)");
|
|
286
|
+
withOrg(db.command("list").description("The organization's databases."))
|
|
281
287
|
.action(async (opts) => dbListCmd({ ...opts, json: program.opts().json }));
|
|
282
288
|
withOrg(db
|
|
283
289
|
.command("create <name>")
|
|
284
|
-
.description("
|
|
290
|
+
.description("Create a database. The connection string is printed ONCE.")
|
|
285
291
|
// 🚨 ФЛАГ ОСТАВЛЕН РАДИ ЧЕСТНОГО ОТКАЗА, А НЕ РАДИ РАБОТЫ. Он обещал
|
|
286
292
|
// «объём платной базы в гигабайтах» и не делал НИЧЕГО: сервер поле не
|
|
287
293
|
// читает ни одним путём создания — у Shared объём задаёт тариф, у
|
|
288
294
|
// выделенного диск ступени. Убрать его совсем значило бы отвечать
|
|
289
295
|
// «неизвестный параметр» тому, кто им пользовался, и оставить человека
|
|
290
296
|
// гадать, куда делся объём. Теперь команда говорит это словами (C9).
|
|
291
|
-
.option("--gb <number>", "
|
|
297
|
+
.option("--gb <number>", "NO LONGER WORKS: the size is set by the plan or by the dedicated instance's tier", (v) => parseInt(v, 10))
|
|
292
298
|
// 🚨 ТЕ ЖЕ ФЛАГИ РАДИ ЧЕСТНОГО ОТКАЗА. Выделенный инстанс из терминала
|
|
293
299
|
// не заказать, и это осознанно: у заказа есть цена и заморозка денег, а
|
|
294
300
|
// карту в терминале не привяжешь и сумму подтвердить негде. Но человек,
|
|
295
301
|
// прочитавший про ступени в панели, попробует `--cpu 2` — и без этих
|
|
296
302
|
// флагов получит «неизвестный параметр», то есть ответ про синтаксис
|
|
297
303
|
// вместо ответа про причину.
|
|
298
|
-
.option("--cpu <number>", "
|
|
299
|
-
.option("--ram <mb>", "
|
|
300
|
-
.option("--dedicated", "
|
|
304
|
+
.option("--cpu <number>", "dedicated instance: ordered only in the dashboard", (v) => parseInt(v, 10))
|
|
305
|
+
.option("--ram <mb>", "dedicated instance: ordered only in the dashboard", (v) => parseInt(v, 10))
|
|
306
|
+
.option("--dedicated", "dedicated instance: ordered only in the dashboard")).action(async (name, opts) => dbCreateCmd(name, { ...opts, json: program.opts().json }));
|
|
301
307
|
withOrg(db
|
|
302
308
|
.command("connect <database>")
|
|
303
|
-
.description("
|
|
304
|
-
+ "
|
|
305
|
-
.option("--project <id_or_slug>", "
|
|
309
|
+
.description("Connect a project to the database: the connection string goes into the project's "
|
|
310
|
+
+ "environment variables, and the project's domains become allowed origins of the Data API.")
|
|
311
|
+
.option("--project <id_or_slug>", "project (default: the linked project)")).action(async (database, opts) => dbConnectCmd(database, { ...opts, json: program.opts().json }));
|
|
306
312
|
withOrg(db
|
|
307
313
|
.command("disconnect <database>")
|
|
308
|
-
.description("
|
|
309
|
-
+ "
|
|
310
|
-
.option("--project <id_or_slug>", "
|
|
314
|
+
.description("Disconnect a project from the database: the variable goes away with the next deploy, "
|
|
315
|
+
+ "the project's role is deleted, and the project's domains stop being allowed origins of the Data API.")
|
|
316
|
+
.option("--project <id_or_slug>", "project (default: the linked project)")).action(async (database, opts) => dbDisconnectCmd(database, { ...opts, json: program.opts().json }));
|
|
311
317
|
withOrg(db
|
|
312
318
|
// 🚨 ЗАПРОС МОЖНО ПИСАТЬ ПРОСТО СЛЕДОМ ЗА ИМЕНЕМ БАЗЫ. Раньше `-c` был
|
|
313
319
|
// ОБЯЗАТЕЛЕН, и `layero db sql моя-база "select 1"` отвечал
|
|
@@ -315,8 +321,8 @@ async function main() {
|
|
|
315
321
|
// самой очевидной форме записи. Так эта команда и записана в приёмке
|
|
316
322
|
// (C6), и так её пишет всякий, кто помнит psql.
|
|
317
323
|
.command("sql <database> [sql]")
|
|
318
|
-
.description("
|
|
319
|
-
.option("-c, --command <sql>", "
|
|
324
|
+
.description("Run SQL in a database. A script of several statements runs as one transaction.")
|
|
325
|
+
.option("-c, --command <sql>", "query or script")).action(async (database, sql, opts) => dbSqlCmd(database, {
|
|
320
326
|
...opts,
|
|
321
327
|
// Явный `-c` выигрывает: он написан намеренно, а позиционный аргумент
|
|
322
328
|
// мог прилететь из истории команд.
|
|
@@ -325,71 +331,71 @@ async function main() {
|
|
|
325
331
|
}));
|
|
326
332
|
const analytics = program
|
|
327
333
|
.command("analytics")
|
|
328
|
-
.description("
|
|
329
|
-
const withProject = (c) => c.option("--project <id_or_slug>", "
|
|
330
|
-
withProject(analytics.command("status").description("
|
|
334
|
+
.description("Yandex Metrica: connect it and see the site's statistics.");
|
|
335
|
+
const withProject = (c) => c.option("--project <id_or_slug>", "project (default: the linked project)");
|
|
336
|
+
withProject(analytics.command("status").description("Whether Yandex Metrica is connected, and to which branch."))
|
|
331
337
|
.action(async (opts) => analyticsStatusCmd({ ...opts, json: program.opts().json }));
|
|
332
338
|
withProject(analytics
|
|
333
339
|
.command("connect")
|
|
334
|
-
.description("
|
|
335
|
-
.option("--branch <name>", "
|
|
340
|
+
.description("Connect Yandex Metrica. Prints a link — a human must open it and grant access.")
|
|
341
|
+
.option("--branch <name>", "branch whose address gets the counter (default: the main branch)")).action(async (opts) => analyticsConnectCmd({ ...opts, json: program.opts().json }));
|
|
336
342
|
withProject(analytics
|
|
337
343
|
.command("stats")
|
|
338
|
-
.description("
|
|
339
|
-
.option("--period <7d|30d|90d>", "
|
|
344
|
+
.description("Traffic: totals, trend and top sources/devices/pages.")
|
|
345
|
+
.option("--period <7d|30d|90d>", "period (default 7d)")).action(async (opts) => analyticsStatsCmd({ ...opts, json: program.opts().json }));
|
|
340
346
|
withProject(analytics
|
|
341
347
|
.command("disconnect")
|
|
342
|
-
.description("
|
|
343
|
-
.option("-y, --yes", "
|
|
348
|
+
.description("Disconnect Yandex Metrica from the project.")
|
|
349
|
+
.option("-y, --yes", "do not ask for confirmation")).action(async (opts) => analyticsDisconnectCmd({ ...opts, json: program.opts().json }));
|
|
344
350
|
const perf = program
|
|
345
351
|
.command("perf")
|
|
346
|
-
.description("
|
|
352
|
+
.description("Measure site performance and compare it with the previous deploy.");
|
|
347
353
|
perf
|
|
348
354
|
.command("check")
|
|
349
|
-
.description("
|
|
350
|
-
.option("--project <id_or_slug>", "
|
|
351
|
-
.option("--wait", "
|
|
355
|
+
.description("Start a measurement of the active deploy. The run is asynchronous — with --wait the command waits for the result.")
|
|
356
|
+
.option("--project <id_or_slug>", "project (default: the linked project)")
|
|
357
|
+
.option("--wait", "wait for the result (up to 4 minutes)")
|
|
352
358
|
.action(async (opts) => perfCheckCmd({ ...opts, json: program.opts().json }));
|
|
353
359
|
perf
|
|
354
360
|
.command("show")
|
|
355
|
-
.description("
|
|
356
|
-
.option("--project <id_or_slug>", "
|
|
361
|
+
.description("Show the latest measurement and the comparison with the previous deploy.")
|
|
362
|
+
.option("--project <id_or_slug>", "project (default: the linked project)")
|
|
357
363
|
.action(async (opts) => perfShowCmd({ ...opts, json: program.opts().json }));
|
|
358
364
|
const domains = program
|
|
359
365
|
.command("domains")
|
|
360
|
-
.description("
|
|
361
|
-
const domainOpts = (c) => c.option("--project <id_or_slug>", "
|
|
362
|
-
domainOpts(domains.command("list").description("
|
|
366
|
+
.description("The project's custom domains: attach, check DNS, make primary, remove.");
|
|
367
|
+
const domainOpts = (c) => c.option("--project <id_or_slug>", "project (default: the linked project)");
|
|
368
|
+
domainOpts(domains.command("list").description("Show the project's domains."))
|
|
363
369
|
.action(async (opts) => domainsListCmd({ ...opts, json: program.opts().json }));
|
|
364
370
|
domainOpts(domains
|
|
365
371
|
.command("add <domain>")
|
|
366
|
-
.description("
|
|
367
|
-
+ "
|
|
368
|
-
domainOpts(domains.command("verify <domain>").description("
|
|
369
|
-
domainOpts(domains.command("primary <domain>").description("
|
|
372
|
+
.description("Attach a domain. Prints the DNS records to add at your registrar; "
|
|
373
|
+
+ "does NOT wait until the domain is ready — DNS propagation takes from minutes to an hour.")).action(async (domain, opts) => domainsAddCmd(domain, { ...opts, json: program.opts().json }));
|
|
374
|
+
domainOpts(domains.command("verify <domain>").description("Check DNS now instead of waiting for the background re-check.")).action(async (domain, opts) => domainsVerifyCmd(domain, { ...opts, json: program.opts().json }));
|
|
375
|
+
domainOpts(domains.command("primary <domain>").description("Make the domain primary: the platform address becomes a 301 redirect to it.")).action(async (domain, opts) => domainsPrimaryCmd(domain, { ...opts, json: program.opts().json }));
|
|
370
376
|
domainOpts(domains
|
|
371
377
|
.command("remove <domain>")
|
|
372
|
-
.description("
|
|
373
|
-
.option("-y, --yes", "
|
|
378
|
+
.description("Remove the domain from the project. Irreversible and breaks live traffic; requires a token with the admin scope.")
|
|
379
|
+
.option("-y, --yes", "do not ask for confirmation")).action(async (domain, opts) => domainsRemoveCmd(domain, { ...opts, json: program.opts().json }));
|
|
374
380
|
program
|
|
375
381
|
.command("diagnose")
|
|
376
|
-
.description("
|
|
377
|
-
+ "
|
|
378
|
-
+ "
|
|
379
|
-
.option("--project <id_or_slug>", "
|
|
380
|
-
.option("--deploy <id>", "
|
|
381
|
-
.addHelpText("after", "\n
|
|
382
|
+
.description("Explain why a deploy is in its current state: the cause in plain language, "
|
|
383
|
+
+ "the build log around the error, and the application's state. Without --deploy takes "
|
|
384
|
+
+ "the project's most recent deploy, whatever its status.")
|
|
385
|
+
.option("--project <id_or_slug>", "project (default: the one linked in .layero/project.json)")
|
|
386
|
+
.option("--deploy <id>", "a specific deploy")
|
|
387
|
+
.addHelpText("after", "\nExamples:\n $ layero diagnose\n $ layero diagnose --deploy 8da10ee6")
|
|
382
388
|
.action(async (opts) => {
|
|
383
389
|
await diagnoseCmd({ ...opts, json: program.opts().json });
|
|
384
390
|
});
|
|
385
391
|
program
|
|
386
392
|
.command("logs")
|
|
387
|
-
.description("
|
|
388
|
-
.option("--project <id_or_slug>", "
|
|
389
|
-
.option("--deploy <id>", "
|
|
390
|
-
.option("--runtime", "
|
|
391
|
-
.option("--tail <n>", "
|
|
392
|
-
.addHelpText("after", "\n
|
|
393
|
+
.description("Show a deploy's logs: the build log (default) or the application log (--runtime).")
|
|
394
|
+
.option("--project <id_or_slug>", "project (default: the linked project)")
|
|
395
|
+
.option("--deploy <id>", "a specific deploy")
|
|
396
|
+
.option("--runtime", "the running application's logs instead of build logs")
|
|
397
|
+
.option("--tail <n>", "how many recent application log lines to show (default 100)", (v) => Number(v))
|
|
398
|
+
.addHelpText("after", "\nExamples:\n $ layero logs\n $ layero logs --runtime --tail 200")
|
|
393
399
|
.action(async (opts) => {
|
|
394
400
|
await logsCmd({ ...opts, json: program.opts().json });
|
|
395
401
|
});
|
|
@@ -402,25 +408,25 @@ async function main() {
|
|
|
402
408
|
.description("Manage the auth token directly (advanced).");
|
|
403
409
|
token
|
|
404
410
|
.command("create <name>")
|
|
405
|
-
.description("
|
|
406
|
-
"
|
|
407
|
-
.option("--scope <list>", "
|
|
408
|
-
.addHelpText("after", "\n
|
|
411
|
+
.description("Issue a long-lived token for CI and agents. " +
|
|
412
|
+
"It is shown ONCE. Default scopes: read+deploy, nothing irreversible.")
|
|
413
|
+
.option("--scope <list>", "comma-separated: read, deploy, admin")
|
|
414
|
+
.addHelpText("after", "\nExamples:\n" +
|
|
409
415
|
" $ layero token create ci # read+deploy\n" +
|
|
410
|
-
" $ layero token create ci --scope read #
|
|
411
|
-
"\
|
|
416
|
+
" $ layero token create ci --scope read # read only\n" +
|
|
417
|
+
"\nIn CI: LAYERO_TOKEN=<token> npx layero@latest deploy")
|
|
412
418
|
.action(async (name, opts) => tokenCreateCmd(name, { ...opts, json: program.opts().json }));
|
|
413
419
|
token
|
|
414
420
|
.command("list")
|
|
415
|
-
.description("
|
|
421
|
+
.description("Issued tokens: name, hint, scopes, last use.")
|
|
416
422
|
.action(async () => tokenListCmd({ json: program.opts().json }));
|
|
417
423
|
token
|
|
418
424
|
.command("revoke <id>")
|
|
419
|
-
.description("
|
|
425
|
+
.description("Revoke a token. Takes effect immediately.")
|
|
420
426
|
.action(async (id) => tokenRevokeCmd(id, { json: program.opts().json }));
|
|
421
427
|
token
|
|
422
428
|
.command("set <jwt>")
|
|
423
|
-
.description("
|
|
429
|
+
.description("Save a token obtained elsewhere (for example, with `layero token create` on another machine).")
|
|
424
430
|
.action(tokenSetCmd);
|
|
425
431
|
program
|
|
426
432
|
.command("deploy")
|
|
@@ -522,57 +522,57 @@ export async function dataEnableCmd(opts) {
|
|
|
522
522
|
*/
|
|
523
523
|
export function registerDataApiCommands(data, program) {
|
|
524
524
|
const withDb = (c) => c
|
|
525
|
-
.option("--db <name>", "
|
|
526
|
-
.option("--org <slug>", "
|
|
525
|
+
.option("--db <name>", "database: name, slug or id (default: the only matching database)")
|
|
526
|
+
.option("--org <slug>", "organization (default: your only one, otherwise your personal organization)");
|
|
527
527
|
const json = (opts) => ({ ...opts, json: program.opts().json });
|
|
528
|
-
const keys = data.command("keys").description("
|
|
529
|
-
withDb(keys.command("list").description("
|
|
528
|
+
const keys = data.command("keys").description("Data API keys: list, issue, revoke.");
|
|
529
|
+
withDb(keys.command("list").description("The database's keys: prefix, expiry, last call. No key values."))
|
|
530
530
|
.action(async (opts) => dataKeysListCmd(json(opts)));
|
|
531
531
|
withDb(keys
|
|
532
532
|
.command("issue")
|
|
533
|
-
.description("
|
|
534
|
-
.option("--kind <kind>", "public —
|
|
535
|
-
.option("--label <text>", "
|
|
536
|
-
.option("--expires-in <days>", "
|
|
533
|
+
.description("Issue a key. The value is printed once.")
|
|
534
|
+
.option("--kind <kind>", "public — for the site, secret — for the server", "public")
|
|
535
|
+
.option("--label <text>", "label to recognize the key by in the list")
|
|
536
|
+
.option("--expires-in <days>", "lifetime in days: 30, 90, 365 or never", "never")).action(async (opts) => dataKeysIssueCmd(json(opts)));
|
|
537
537
|
withDb(keys
|
|
538
538
|
.command("revoke <id>")
|
|
539
|
-
.description("
|
|
540
|
-
.option("-y, --yes", "
|
|
541
|
-
const origins = data.command("origins").description("
|
|
542
|
-
withDb(origins.command("list").description("
|
|
539
|
+
.description("Revoke a key by id or prefix. Requests with it are refused immediately.")
|
|
540
|
+
.option("-y, --yes", "do not ask for confirmation")).action(async (id, opts) => dataKeysRevokeCmd(id, json(opts)));
|
|
541
|
+
const origins = data.command("origins").description("Sites allowed to call the database from the browser.");
|
|
542
|
+
withDb(origins.command("list").description("Addresses of the projects connected to the database, plus sites added by hand."))
|
|
543
543
|
.action(async (opts) => dataOriginsListCmd(json(opts)));
|
|
544
544
|
withDb(origins
|
|
545
545
|
.command("add <url>")
|
|
546
|
-
.description("
|
|
547
|
-
.option("--note <text>", "
|
|
546
|
+
.description("Allow a site. This opens no data: access levels decide that.")
|
|
547
|
+
.option("--note <text>", "why the site was added")).action(async (url, opts) => dataOriginsAddCmd(url, json(opts)));
|
|
548
548
|
withDb(origins
|
|
549
549
|
.command("remove <url>")
|
|
550
|
-
.description("
|
|
551
|
-
.option("-y, --yes", "
|
|
552
|
-
withDb(data.command("methods").description("REST
|
|
550
|
+
.description("Remove a site from the list.")
|
|
551
|
+
.option("-y, --yes", "do not ask for confirmation")).action(async (url, opts) => dataOriginsRemoveCmd(url, json(opts)));
|
|
552
|
+
withDb(data.command("methods").description("The database's REST and RPC methods with the access level of each."))
|
|
553
553
|
.action(async (opts) => dataMethodsCmd(json(opts)));
|
|
554
554
|
withDb(data
|
|
555
555
|
.command("grant <object>")
|
|
556
|
-
.description("
|
|
557
|
-
.option("--get <level>", "
|
|
558
|
-
.option("--post <level>", "
|
|
559
|
-
.option("--patch <level>", "
|
|
560
|
-
.option("--delete <level>", "
|
|
561
|
-
.option("--call <level>", "
|
|
562
|
-
.option("-y, --yes", "
|
|
563
|
-
.addHelpText("after", "\
|
|
564
|
-
"
|
|
565
|
-
"\n
|
|
566
|
-
" $ layero data grant app.products --get visitor #
|
|
567
|
-
" $ layero data grant app.orders --get user --post user #
|
|
568
|
-
" $ layero data grant api.order_create --call visitor #
|
|
569
|
-
" $ layero data grant 'api.pick(integer)' --call server #
|
|
570
|
-
"\
|
|
556
|
+
.description("Set the access level of a table's or function's methods: shows the SQL, applies it after confirmation.")
|
|
557
|
+
.option("--get <level>", "read the table")
|
|
558
|
+
.option("--post <level>", "insert rows")
|
|
559
|
+
.option("--patch <level>", "update rows")
|
|
560
|
+
.option("--delete <level>", "delete rows")
|
|
561
|
+
.option("--call <level>", "call the function")
|
|
562
|
+
.option("-y, --yes", "apply without confirmation")
|
|
563
|
+
.addHelpText("after", "\nLevels: closed — no access, visitor — any visitor, user — signed-in users, server — server only.\n" +
|
|
564
|
+
"Table methods you do not name keep their current level.\n" +
|
|
565
|
+
"\nExamples:\n" +
|
|
566
|
+
" $ layero data grant app.products --get visitor # the site can read the catalog\n" +
|
|
567
|
+
" $ layero data grant app.orders --get user --post user # orders for signed-in users\n" +
|
|
568
|
+
" $ layero data grant api.order_create --call visitor # a function for the site\n" +
|
|
569
|
+
" $ layero data grant 'api.pick(integer)' --call server # an overload: name it by argument types\n" +
|
|
570
|
+
"\nWithout --yes outside a terminal the command shows the SQL, changes nothing and exits with an error.")).action(async (object, opts) => dataGrantCmd(object, json(opts)));
|
|
571
571
|
withDb(data
|
|
572
572
|
.command("enable")
|
|
573
|
-
.description("
|
|
574
|
-
.option("--with-secret", "
|
|
575
|
-
.option("--repair", "
|
|
576
|
-
.option("-y, --yes", "
|
|
573
|
+
.description("Enable the Data API for a database: sets up the api schema and roles, issues a public key. All methods start closed.")
|
|
574
|
+
.option("--with-secret", "also issue a secret key for the server")
|
|
575
|
+
.option("--repair", "re-apply the roles and the api schema for a database whose Data API is already enabled; revokes the roles' privileges on schema public")
|
|
576
|
+
.option("-y, --yes", "do not ask for confirmation for --repair")).action(async (opts) => dataEnableCmd(json(opts)));
|
|
577
577
|
registerDataProbeCommand(data, withDb, json);
|
|
578
578
|
}
|
|
@@ -46,6 +46,8 @@ const ANSWER_HEADERS = [
|
|
|
46
46
|
];
|
|
47
47
|
/** Что откат пробы не возвращает — теми же словами, что панель. */
|
|
48
48
|
const NOT_UNDONE = "номера последовательностей, внешние вызовы из базы, сессионные блокировки и суточная квота вызовов";
|
|
49
|
+
/** Тот же список по-английски — для `--help` (справка для агентов на английском). Правите один — правьте оба. */
|
|
50
|
+
const NOT_UNDONE_HELP = "sequence numbers, outbound calls made by the database, session locks and the daily call quota";
|
|
49
51
|
/** Ручка ждёт UUID (`UUID(user_id)`). */
|
|
50
52
|
const UUID = /^[0-9a-f]{8}-?[0-9a-f]{4}-?[0-9a-f]{4}-?[0-9a-f]{4}-?[0-9a-f]{12}$/i;
|
|
51
53
|
const CONTACTS = "https://docs.layero.ru/contacts/";
|
|
@@ -515,27 +517,27 @@ export function registerDataProbeCommand(data, withDb, json) {
|
|
|
515
517
|
const collect = (value, prev = []) => [...prev, value];
|
|
516
518
|
withDb(data
|
|
517
519
|
.command("probe <method> <path>")
|
|
518
|
-
.description("
|
|
519
|
-
.option("--as <who>", "
|
|
520
|
-
.option("--user <id>", "
|
|
521
|
-
.option("--query <name=value>", "
|
|
522
|
-
.option("--body <json>", "
|
|
523
|
-
.option("--body-file <file>", "
|
|
524
|
-
.option("--schema <schema>", "
|
|
525
|
-
.option("--expect <status>", "
|
|
526
|
-
.addHelpText("after", "\
|
|
527
|
-
"
|
|
528
|
-
"\n
|
|
520
|
+
.description("Probe a method with a real request through the gateway: real grants and RLS policies apply, writes are rolled back.")
|
|
521
|
+
.option("--as <who>", "who makes the request: visitor — a site visitor (default), user — a signed-in user, server — the server")
|
|
522
|
+
.option("--user <id>", "for --as user: the app user's id (UUID)")
|
|
523
|
+
.option("--query <name=value>", "query parameter in PostgREST syntax; repeat the flag for more", collect)
|
|
524
|
+
.option("--body <json>", "body for POST and PATCH: a JSON object or array")
|
|
525
|
+
.option("--body-file <file>", "body from a JSON file")
|
|
526
|
+
.option("--schema <schema>", "table schema: api, public or app; without the flag the gateway looks in api, then public, then app")
|
|
527
|
+
.option("--expect <status>", "expected status: 200, 2xx or a comma-separated list; a mismatch is an error")
|
|
528
|
+
.addHelpText("after", "\nPaths: /rest/v1/<table>, /rest/v1/rpc/<function> (GET or POST), /whoami (GET only).\n" +
|
|
529
|
+
"Pass parameters as --query flags, not with a \"?\" in the path.\n" +
|
|
530
|
+
"\nExamples:\n" +
|
|
529
531
|
" $ layero data probe GET /rest/v1/products --query select=id,title --query limit=5\n" +
|
|
530
532
|
" $ layero data probe POST /rest/v1/cart --as user --user <id> --body '{\"qty\":1}'\n" +
|
|
531
533
|
" $ layero data probe POST /rest/v1/rpc/order_create --body-file order.json --as server\n" +
|
|
532
|
-
" $ layero data probe GET /rest/v1/orders --expect 401,403 #
|
|
534
|
+
" $ layero data probe GET /rest/v1/orders --expect 401,403 # is it closed to visitors?\n" +
|
|
533
535
|
" $ layero data probe GET /whoami\n" +
|
|
534
|
-
`\
|
|
535
|
-
"\
|
|
536
|
-
" 1.
|
|
537
|
-
" 2.
|
|
538
|
-
" 3.
|
|
539
|
-
" 4.
|
|
540
|
-
" 5.
|
|
536
|
+
`\nWrites are rolled back. Not rolled back: ${NOT_UNDONE_HELP}.\n` +
|
|
537
|
+
"\nExit code, decided in this order:\n" +
|
|
538
|
+
" 1. the write rollback was not confirmed — error data_probe_not_rolled_back;\n" +
|
|
539
|
+
" 2. the status matches --expect — 0;\n" +
|
|
540
|
+
" 3. the gateway answered 5xx — error data_probe_gateway_failed;\n" +
|
|
541
|
+
" 4. the status does not match --expect — error data_probe_unexpected_status;\n" +
|
|
542
|
+
" 5. otherwise 0, including a 4xx refusal: 401, 403, 404 are the probe's answer, not a failure.")).action(async (method, path, opts) => dataProbeCmd(method, path, json(opts)));
|
|
541
543
|
}
|
package/dist/commands/deploy.js
CHANGED
|
@@ -442,6 +442,7 @@ async function dryRun(cwd, opts, existing, cliCfg) {
|
|
|
442
442
|
...(hint ? { hint } : {}),
|
|
443
443
|
...(d.next_action ? { next_action: d.next_action } : {}),
|
|
444
444
|
...(d.candidates?.length ? { candidates: d.candidates } : {}),
|
|
445
|
+
...(d.layero_warnings?.length ? { layero_warnings: d.layero_warnings } : {}),
|
|
445
446
|
});
|
|
446
447
|
}
|
|
447
448
|
// Сколько ждём, пока живой адрес начнёт отвечать приложением. Статика
|
|
@@ -861,8 +862,11 @@ export async function deployCmd(opts) {
|
|
|
861
862
|
}
|
|
862
863
|
const dashboardUrl = projectUrl(cliCfg.apiUrl, project.id);
|
|
863
864
|
const apexUrl = `https://${project.apex_hostname}`;
|
|
864
|
-
|
|
865
|
-
|
|
865
|
+
// Сборка уже готова: сбой этих запросов не повод объявлять её упавшей
|
|
866
|
+
// (`internal` при живом сайте, T-20260918-16). Без строки деплоя — адрес
|
|
867
|
+
// апекса и никаких чужих превью.
|
|
868
|
+
const deployRow = await api.getDeploy(started.deploy_id).catch(() => null);
|
|
869
|
+
const probe = deployRow?.environment_id
|
|
866
870
|
? await resolveReachability(api, deployRow.environment_id)
|
|
867
871
|
: null;
|
|
868
872
|
const liveUrl = promoted || !opts.branch
|
|
@@ -876,7 +880,7 @@ export async function deployCmd(opts) {
|
|
|
876
880
|
// Пустой список — обычный случай и молчит. Поля может не быть вовсе, если
|
|
877
881
|
// платформа старше него: `?? []` и никаких предупреждений о том, чего не
|
|
878
882
|
// знаем.
|
|
879
|
-
const evicted = deployRow
|
|
883
|
+
const evicted = deployRow?.preview_evicted ?? [];
|
|
880
884
|
if (evicted.length > 0) {
|
|
881
885
|
emit({ event: "preview_evicted", evicted });
|
|
882
886
|
}
|
|
@@ -151,26 +151,24 @@ async function setupUrl(project) {
|
|
|
151
151
|
}
|
|
152
152
|
/** Рантайм, который панель ставит проекту ДО первой сборки; статика — как есть. */
|
|
153
153
|
const RUNTIME_KINDS = new Set(["ssr_next", "streamlit", "gradio", "flask", "python_web", "node_web"]);
|
|
154
|
-
/** Приложения, у которых `framework_hint` — рудимент: панель шлёт им `static`. */
|
|
155
|
-
const HINT_IS_STATIC = new Set(["ssr_next", "streamlit", "gradio"]);
|
|
156
154
|
const PACKAGE_MANAGERS = new Set(["npm", "yarn", "pnpm", "bun"]);
|
|
157
155
|
/**
|
|
158
|
-
* Тело `/setup`
|
|
156
|
+
* Тело `/setup` при импорте репозитория без человека.
|
|
159
157
|
*
|
|
160
|
-
*
|
|
161
|
-
*
|
|
162
|
-
*
|
|
163
|
-
*
|
|
158
|
+
* 🚨 ФРЕЙМВОРК, КОМАНДА И КАТАЛОГ ИЗ ДЕТЕКТА В ПРОЕКТ НЕ УХОДЯТ. Настройки
|
|
159
|
+
* проекта сборщик исполняет дословно на КАЖДОЙ сборке (`BUILD-CONFIG.md`,
|
|
160
|
+
* правило 3), и догадка, записанная туда, становилась «выбором владельца»:
|
|
161
|
+
* репозиторий сменит сборщик — проект продолжит собирать старым. Панель
|
|
162
|
+
* перестала так делать 25.08 (T-20260824-16), а `projects create --repo`
|
|
163
|
+
* 17.09 повторил старое поведение (T-20260918-19). Пусто = «решит сборщик по
|
|
164
|
+
* клону» — тем же детектом, но на полном дереве и на каждой сборке заново.
|
|
165
|
+
*
|
|
166
|
+
* Остаётся то, что детект не угадывает, а читает как решение: менеджер пакетов
|
|
167
|
+
* из `layero.json` (выбор владельца) и папка приложения в монорепо (без неё
|
|
168
|
+
* сборщик при нескольких кандидатах откажет).
|
|
164
169
|
*/
|
|
165
170
|
export function setupPayloadFromDetect(d) {
|
|
166
|
-
const
|
|
167
|
-
const payload = {
|
|
168
|
-
framework_hint: kind && HINT_IS_STATIC.has(kind) ? "static" : d.framework,
|
|
169
|
-
};
|
|
170
|
-
if (d.build_cmd)
|
|
171
|
-
payload.build_cmd = d.build_cmd;
|
|
172
|
-
if (d.output_dir)
|
|
173
|
-
payload.output_dir = d.output_dir;
|
|
171
|
+
const payload = {};
|
|
174
172
|
if (d.layero_found && d.package_manager && PACKAGE_MANAGERS.has(d.package_manager)) {
|
|
175
173
|
payload.package_manager = d.package_manager;
|
|
176
174
|
}
|
|
@@ -211,12 +209,13 @@ async function finishSetup(api, project, opts) {
|
|
|
211
209
|
});
|
|
212
210
|
return;
|
|
213
211
|
}
|
|
212
|
+
// Что увидел детект — для сведения: в проект эти значения не записаны.
|
|
214
213
|
emit({
|
|
215
214
|
event: "setup_applied",
|
|
216
215
|
project: project.slug,
|
|
217
|
-
framework:
|
|
218
|
-
build_cmd:
|
|
219
|
-
output_dir:
|
|
216
|
+
framework: detected.framework ?? undefined,
|
|
217
|
+
build_cmd: detected.build_cmd || null,
|
|
218
|
+
output_dir: detected.output_dir || null,
|
|
220
219
|
layero_found: detected.layero_found,
|
|
221
220
|
});
|
|
222
221
|
// Приложение, а не статика: тип — до первой сборки, иначе сборщик падает на
|
|
@@ -225,7 +224,7 @@ async function finishSetup(api, project, opts) {
|
|
|
225
224
|
const kind = detected.runtime_kind ?? null;
|
|
226
225
|
if (kind && RUNTIME_KINDS.has(kind) && project.project_type !== kind) {
|
|
227
226
|
try {
|
|
228
|
-
await api.setRuntimeType(project.id, kind);
|
|
227
|
+
await api.setRuntimeType(project.id, kind, false, "platform");
|
|
229
228
|
emit({ event: "runtime_type_applied", project_type: kind });
|
|
230
229
|
}
|
|
231
230
|
catch {
|
package/dist/detect.js
CHANGED
|
@@ -521,6 +521,33 @@ const OUTPUT_SOURCE = {
|
|
|
521
521
|
*/
|
|
522
522
|
export async function detectProject(cwd, opts = {}) {
|
|
523
523
|
const snap = await snapshotFromDir(cwd);
|
|
524
|
+
const warnings = layeroKeyWarnings(snap.layeroJson);
|
|
525
|
+
const d = await detectFromSnapshot(cwd, snap, opts);
|
|
526
|
+
return warnings.length ? { ...d, layero_warnings: warnings } : d;
|
|
527
|
+
}
|
|
528
|
+
// Значения ключа `runtime` — зеркало `layero_keys.RUNTIME_VALUES` в core.
|
|
529
|
+
const RUNTIME_VALUES = new Set(["ssr_next", "node_web", "python_web", "streamlit", "gradio", "flask"]);
|
|
530
|
+
/**
|
|
531
|
+
* Ключи layero.json, которые платформа НЕ применит, — с верным именем.
|
|
532
|
+
*
|
|
533
|
+
* 🚨 `"type": "vite"` пишут по аналогии с флагом `--type` (прогон evals 19.09,
|
|
534
|
+
* T-20260918-21). Сборщик ключ молча пропускал, и узнать об этом можно было
|
|
535
|
+
* только из строки в логе сборки. Синонимом не принимаем: `type` двусмыслен —
|
|
536
|
+
* `--type` принимает и фреймворк, и рантайм, — поэтому имя подсказываем по
|
|
537
|
+
* значению, как `layero_keys.suggest_key` у сборщика и панели.
|
|
538
|
+
*/
|
|
539
|
+
export function layeroKeyWarnings(layero) {
|
|
540
|
+
if (!layero || typeof layero !== "object" || !("type" in layero))
|
|
541
|
+
return [];
|
|
542
|
+
const value = layero.type;
|
|
543
|
+
const v = typeof value === "string" ? value.trim().toLowerCase() : "";
|
|
544
|
+
const key = RUNTIME_VALUES.has(v) ? "runtime" : "framework";
|
|
545
|
+
return [
|
|
546
|
+
`layero.json: key "type" is not applied — did you mean "${key}"? ` +
|
|
547
|
+
`Write {"${key}": ${JSON.stringify(value ?? "")}} instead.`,
|
|
548
|
+
];
|
|
549
|
+
}
|
|
550
|
+
async function detectFromSnapshot(cwd, snap, opts) {
|
|
524
551
|
const layero = snap.layeroJson;
|
|
525
552
|
const fileFramework = str(layero?.framework);
|
|
526
553
|
const outsideHint = fileFramework ? null : str(opts.frameworkHint);
|
|
@@ -682,5 +709,6 @@ export function detectedEvent(d) {
|
|
|
682
709
|
...(d.next_action ? { next_action: d.next_action } : {}),
|
|
683
710
|
...(d.candidates?.length ? { candidates: d.candidates } : {}),
|
|
684
711
|
...(d.ssr_warning ? { ssr_warning: d.ssr_warning } : {}),
|
|
712
|
+
...(d.layero_warnings?.length ? { layero_warnings: d.layero_warnings } : {}),
|
|
685
713
|
};
|
|
686
714
|
}
|
package/dist/pack.js
CHANGED
|
@@ -4,7 +4,7 @@ import { tmpdir } from "node:os";
|
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { create as tarCreate } from "tar";
|
|
6
6
|
import ignore from "ignore";
|
|
7
|
-
const MAX_BYTES =
|
|
7
|
+
const MAX_BYTES = 500 * 1024 * 1024;
|
|
8
8
|
const DEFAULT_IGNORE = [
|
|
9
9
|
"node_modules",
|
|
10
10
|
".git",
|
|
@@ -176,7 +176,7 @@ export async function packDirectory(targetDir, projectName) {
|
|
|
176
176
|
const fst = await fs.stat(archivePath);
|
|
177
177
|
if (fst.size > MAX_BYTES) {
|
|
178
178
|
await fs.unlink(archivePath).catch(() => undefined);
|
|
179
|
-
throw new Error(`archive is ${(fst.size / (1024 * 1024)).toFixed(1)}MB — over the
|
|
179
|
+
throw new Error(`archive is ${(fst.size / (1024 * 1024)).toFixed(1)}MB — over the ${MAX_BYTES / (1024 * 1024)}MB limit. ` +
|
|
180
180
|
"Trim the build output or skip large files (`.layeroignore`).");
|
|
181
181
|
}
|
|
182
182
|
const sha256 = await sha256OfFile(archivePath);
|
|
@@ -206,7 +206,7 @@ export async function packCwd(cwd, projectName) {
|
|
|
206
206
|
const stat = await fs.stat(archivePath);
|
|
207
207
|
if (stat.size > MAX_BYTES) {
|
|
208
208
|
await fs.unlink(archivePath).catch(() => undefined);
|
|
209
|
-
throw new Error(`archive is ${(stat.size / (1024 * 1024)).toFixed(1)}MB — over the
|
|
209
|
+
throw new Error(`archive is ${(stat.size / (1024 * 1024)).toFixed(1)}MB — over the ${MAX_BYTES / (1024 * 1024)}MB limit. ` +
|
|
210
210
|
"Add large directories to .layeroignore.");
|
|
211
211
|
}
|
|
212
212
|
const sha256 = await sha256OfFile(archivePath);
|
package/package.json
CHANGED