layero 0.8.1 → 0.8.3
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/commands/deploy.js +63 -27
- package/dist/pack.js +47 -1
- package/package.json +2 -7
package/README.md
CHANGED
package/dist/commands/deploy.js
CHANGED
|
@@ -196,6 +196,13 @@ async function packAndUpload(api, cwd, project, prebuiltDir) {
|
|
|
196
196
|
sha256: pack.sha256,
|
|
197
197
|
...(prebuiltDir ? { prebuilt_dir: prebuiltDir } : {}),
|
|
198
198
|
});
|
|
199
|
+
// A gitignored lockfile is force-included so the build can do a frozen
|
|
200
|
+
// install; warn (to stderr, off the --json stdout stream) that the ignore
|
|
201
|
+
// rule was overridden for it.
|
|
202
|
+
if (pack.forcedLockfiles?.length) {
|
|
203
|
+
process.stderr.write(chalk.yellow(`! including gitignored lockfile(s) so the build uses a frozen install: ` +
|
|
204
|
+
`${pack.forcedLockfiles.join(", ")}\n`));
|
|
205
|
+
}
|
|
199
206
|
const init = await api.initUpload(project.id);
|
|
200
207
|
emit({ event: "uploading" });
|
|
201
208
|
await uploadArchive(init, pack.archivePath);
|
|
@@ -219,21 +226,44 @@ const PREBUILT_AUTO_DIRS = [
|
|
|
219
226
|
async function resolvePrebuiltDir(cwd, raw) {
|
|
220
227
|
if (raw === undefined || raw === false)
|
|
221
228
|
return null;
|
|
229
|
+
let dir = null;
|
|
222
230
|
if (typeof raw === "string" && raw.length > 0) {
|
|
223
|
-
|
|
231
|
+
dir = raw;
|
|
224
232
|
}
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
233
|
+
else {
|
|
234
|
+
// No explicit dir — pick the first existing common output directory.
|
|
235
|
+
for (const candidate of PREBUILT_AUTO_DIRS) {
|
|
236
|
+
try {
|
|
237
|
+
const stat = await fs.stat(path.join(cwd, candidate));
|
|
238
|
+
if (stat.isDirectory()) {
|
|
239
|
+
dir = candidate;
|
|
240
|
+
break;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
catch {
|
|
244
|
+
// try next
|
|
245
|
+
}
|
|
231
246
|
}
|
|
232
|
-
|
|
233
|
-
|
|
247
|
+
if (dir === null) {
|
|
248
|
+
throw new LayeroError("prebuilt_no_dir", "could not auto-detect a built artifact directory", `pass it explicitly: --prebuilt ./dist (tried: ${PREBUILT_AUTO_DIRS.join(", ")})`);
|
|
234
249
|
}
|
|
235
250
|
}
|
|
236
|
-
|
|
251
|
+
// Servability check (mirrors the builder-side gate, 2026-07-01 audit): a
|
|
252
|
+
// prebuilt dir with no index.html at its root isn't a servable site — the
|
|
253
|
+
// apex "/" would 404. Fail fast here, before packing + uploading, instead of
|
|
254
|
+
// the deploy going "ready" but broken.
|
|
255
|
+
let hasIndex = false;
|
|
256
|
+
try {
|
|
257
|
+
const entries = await fs.readdir(path.join(cwd, dir));
|
|
258
|
+
hasIndex = entries.some((e) => e.toLowerCase() === "index.html");
|
|
259
|
+
}
|
|
260
|
+
catch {
|
|
261
|
+
hasIndex = false;
|
|
262
|
+
}
|
|
263
|
+
if (!hasIndex) {
|
|
264
|
+
throw new LayeroError("prebuilt_no_index", `'${dir}' has no index.html — nothing to serve`, `point --prebuilt at the folder that contains your built index.html (e.g. --prebuilt ./dist)`);
|
|
265
|
+
}
|
|
266
|
+
return dir;
|
|
237
267
|
}
|
|
238
268
|
// Resolve the framework / build / output config to send to completeSetup.
|
|
239
269
|
// Precedence: explicit CLI args > .layero/project.json > auto-detect.
|
|
@@ -424,30 +454,36 @@ export async function deployCmd(opts) {
|
|
|
424
454
|
const dashboardUrl = projectUrl(cliCfg.apiUrl, project.id);
|
|
425
455
|
const apexUrl = `https://${project.apex_hostname}`;
|
|
426
456
|
const probe = await resolveReachability(api, deploy.environment_id);
|
|
427
|
-
//
|
|
428
|
-
//
|
|
429
|
-
//
|
|
430
|
-
//
|
|
431
|
-
//
|
|
432
|
-
//
|
|
433
|
-
//
|
|
434
|
-
//
|
|
435
|
-
|
|
436
|
-
|
|
457
|
+
// Публичный адрес сайта.
|
|
458
|
+
//
|
|
459
|
+
// Раньше здесь стоял гейт `cdn_ready`: пока YC CDN не подтвердил апекс,
|
|
460
|
+
// отдавали off-CDN превью, потому что свежий апекс 10-15 минут отвечал
|
|
461
|
+
// 404, а runtime-приложения на апексе вообще не принимали POST (CDN резал
|
|
462
|
+
// не-GET). После EDGE-02 CDN перед пользовательскими зонами нет: апекс
|
|
463
|
+
// валиден с момента создания проекта (wildcard-запись + wildcard-серт), а
|
|
464
|
+
// `cdn_ready` бекенд отдаёт false НАВСЕГДА — строки в `cdn_hostnames` у
|
|
465
|
+
// новых проектов не появляется вовсе.
|
|
466
|
+
//
|
|
467
|
+
// Из-за этого условие не выполнялось никогда, и после успешной публикации
|
|
468
|
+
// CLI выдавал ссылку на дашборд вместо адреса сайта. Проверено вживую
|
|
469
|
+
// 2026-07-26: `layero deploy` вернул `url: https://app.layero.ru/projects/…`.
|
|
470
|
+
//
|
|
471
|
+
// Апекс — адрес по умолчанию; превью остаётся для деплоя ветки, которую
|
|
472
|
+
// не промоутили: там апекс ведёт на другую сборку.
|
|
473
|
+
const liveUrl = promoted || !opts.branch
|
|
437
474
|
? probe?.canonical_url ?? apexUrl
|
|
438
|
-
: probe?.preview_url ??
|
|
439
|
-
probe?.canonical_url ??
|
|
440
|
-
(promoted || !opts.branch ? apexUrl : dashboardUrl);
|
|
475
|
+
: probe?.preview_url ?? probe?.canonical_url ?? apexUrl;
|
|
441
476
|
emit({
|
|
442
477
|
event: "ready",
|
|
443
478
|
url: liveUrl,
|
|
444
479
|
deploy_id: deploy.id,
|
|
445
480
|
preview_url: probe?.preview_url ?? undefined,
|
|
446
481
|
dashboard_url: dashboardUrl,
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
482
|
+
// `available` от пробы, а НЕ `cdn_ready`: последний после EDGE-02
|
|
483
|
+
// остаётся false навсегда, и потребитель JSON-вывода ждал бы события,
|
|
484
|
+
// которого не будет. ETA пропагации убран по той же причине —
|
|
485
|
+
// распространять больше нечего.
|
|
486
|
+
edge_ready: probe ? probe.available : undefined,
|
|
451
487
|
});
|
|
452
488
|
}
|
|
453
489
|
finally {
|
package/dist/pack.js
CHANGED
|
@@ -41,11 +41,26 @@ async function readIgnoreFile(filePath) {
|
|
|
41
41
|
throw err;
|
|
42
42
|
}
|
|
43
43
|
}
|
|
44
|
+
// Lockfiles the server needs for a frozen/reproducible install. A gitignored
|
|
45
|
+
// (or .layeroignore'd) lockfile would otherwise be dropped from the upload →
|
|
46
|
+
// the builder falls back to a non-frozen install (re-resolves deps). Force them
|
|
47
|
+
// back in with negation patterns applied AFTER the ignore layers. They live at
|
|
48
|
+
// repo root, so no ignored parent dir can block the re-include.
|
|
49
|
+
const FORCE_INCLUDE_LOCKFILES = [
|
|
50
|
+
"bun.lockb",
|
|
51
|
+
"bun.lock",
|
|
52
|
+
"package-lock.json",
|
|
53
|
+
"yarn.lock",
|
|
54
|
+
"pnpm-lock.yaml",
|
|
55
|
+
"npm-shrinkwrap.json",
|
|
56
|
+
];
|
|
44
57
|
async function buildIgnore(cwd) {
|
|
45
58
|
const ig = ignore();
|
|
46
59
|
ig.add(DEFAULT_IGNORE);
|
|
47
60
|
ig.add(await readIgnoreFile(path.join(cwd, ".gitignore")));
|
|
48
61
|
ig.add(await readIgnoreFile(path.join(cwd, ".layeroignore")));
|
|
62
|
+
// Negations last so they win over any ignore rule above.
|
|
63
|
+
ig.add(FORCE_INCLUDE_LOCKFILES.map((f) => `!/${f}`));
|
|
49
64
|
return ig;
|
|
50
65
|
}
|
|
51
66
|
async function walk(cwd, ig) {
|
|
@@ -80,6 +95,30 @@ async function sha256OfFile(p) {
|
|
|
80
95
|
}
|
|
81
96
|
return hash.digest("hex");
|
|
82
97
|
}
|
|
98
|
+
/** Lockfiles present on disk that WOULD have been dropped by the ignore rules
|
|
99
|
+
* if not force-included. Built from the same ignore layers minus the lockfile
|
|
100
|
+
* negations, so it reflects exactly what the user's .gitignore/.layeroignore
|
|
101
|
+
* would have excluded. */
|
|
102
|
+
async function detectForcedLockfiles(cwd) {
|
|
103
|
+
const probe = ignore();
|
|
104
|
+
probe.add(DEFAULT_IGNORE);
|
|
105
|
+
probe.add(await readIgnoreFile(path.join(cwd, ".gitignore")));
|
|
106
|
+
probe.add(await readIgnoreFile(path.join(cwd, ".layeroignore")));
|
|
107
|
+
const forced = [];
|
|
108
|
+
for (const lf of FORCE_INCLUDE_LOCKFILES) {
|
|
109
|
+
if (!probe.ignores(lf))
|
|
110
|
+
continue; // not ignored → nothing forced
|
|
111
|
+
try {
|
|
112
|
+
const st = await fs.stat(path.join(cwd, lf));
|
|
113
|
+
if (st.isFile())
|
|
114
|
+
forced.push(lf);
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
// absent on disk — nothing to force-include
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return forced;
|
|
121
|
+
}
|
|
83
122
|
/** Pack only the contents of an already-built artifact directory.
|
|
84
123
|
*
|
|
85
124
|
* Used by `layero deploy --prebuilt`. We deliberately bypass DEFAULT_IGNORE
|
|
@@ -139,6 +178,7 @@ export async function packCwd(cwd, projectName) {
|
|
|
139
178
|
throw new Error("no files to upload after applying .gitignore/.layeroignore — " +
|
|
140
179
|
"check that you're in the right directory");
|
|
141
180
|
}
|
|
181
|
+
const forcedLockfiles = await detectForcedLockfiles(cwd);
|
|
142
182
|
const stamp = Date.now();
|
|
143
183
|
const safeName = projectName.replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
144
184
|
const archivePath = path.join(tmpdir(), `layero-${safeName}-${stamp}.tgz`);
|
|
@@ -159,5 +199,11 @@ export async function packCwd(cwd, projectName) {
|
|
|
159
199
|
"Add large directories to .layeroignore.");
|
|
160
200
|
}
|
|
161
201
|
const sha256 = await sha256OfFile(archivePath);
|
|
162
|
-
return {
|
|
202
|
+
return {
|
|
203
|
+
archivePath,
|
|
204
|
+
sha256,
|
|
205
|
+
size: stat.size,
|
|
206
|
+
fileCount: files.length,
|
|
207
|
+
...(forcedLockfiles.length ? { forcedLockfiles } : {}),
|
|
208
|
+
};
|
|
163
209
|
}
|
package/package.json
CHANGED
|
@@ -1,17 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "layero",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.3",
|
|
4
4
|
"description": "Layero CLI — publish a local site with one command. No git, no GitHub, agent-friendly (Cursor, Claude Code).",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"homepage": "https://layero.ru",
|
|
8
|
-
"repository": {
|
|
9
|
-
"type": "git",
|
|
10
|
-
"url": "https://github.com/LayeroInfra/core.git",
|
|
11
|
-
"directory": "cli"
|
|
12
|
-
},
|
|
13
8
|
"bugs": {
|
|
14
|
-
"url": "https://
|
|
9
|
+
"url": "https://docs.layero.ru/contacts/"
|
|
15
10
|
},
|
|
16
11
|
"keywords": [
|
|
17
12
|
"layero",
|