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 CHANGED
@@ -208,4 +208,4 @@ always excluded: `node_modules`, `.git`, `dist`, `build`, `.next`, `.env*`,
208
208
 
209
209
  - Website: https://layero.ru
210
210
  - Docs: https://docs.layero.ru
211
- - Issues: https://github.com/layero/layero/issues
211
+ - Support: https://docs.layero.ru/contacts/
@@ -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
- return raw;
231
+ dir = raw;
224
232
  }
225
- // No explicit dir — pick the first existing common output directory.
226
- for (const candidate of PREBUILT_AUTO_DIRS) {
227
- try {
228
- const stat = await fs.stat(path.join(cwd, candidate));
229
- if (stat.isDirectory())
230
- return candidate;
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
- catch {
233
- // try next
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
- throw new LayeroError("prebuilt_no_dir", "could not auto-detect a built artifact directory", `pass it explicitly: --prebuilt ./dist (tried: ${PREBUILT_AUTO_DIRS.join(", ")})`);
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
- // Public site URL — honour the preview-first contract the dashboard uses
428
- // (probe states B/C): until the CDN apex is verified live (`cdn_ready`),
429
- // the reachable address is the off-CDN preview domain. A fresh apex
430
- // 404/000s for ~10-15min while CDN propagates, and for runtime apps the
431
- // preview domain is the *permanent* address (the CDN apex can't serve
432
- // POST/WebSocket). So only surface the canonical apex once it's warm;
433
- // before that, hand back the preview. `edge_ready` / `edge_eta_seconds`
434
- // below tell the caller the apex is on its way.
435
- const cdnWarm = probe?.cdn_ready === true;
436
- const liveUrl = cdnWarm
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
- edge_ready: probe ? probe.cdn_ready : undefined,
448
- edge_eta_seconds: probe && !probe.cdn_ready && probe.cdn_eta_seconds != null
449
- ? probe.cdn_eta_seconds
450
- : undefined,
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 { archivePath, sha256, size: stat.size, fileCount: files.length };
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.1",
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://github.com/LayeroInfra/core/issues"
9
+ "url": "https://docs.layero.ru/contacts/"
15
10
  },
16
11
  "keywords": [
17
12
  "layero",