layero 0.8.11 → 0.8.12

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.
@@ -17,6 +17,7 @@ import { loginCmd } from "../commands/login.js";
17
17
  import { orgsListCmd } from "../commands/orgs.js";
18
18
  import { initCmd } from "../commands/init.js";
19
19
  import { LayeroError, detectMode, emit } from "../agent.js";
20
+ import { notifyIfOutdated } from "../update-notifier.js";
20
21
  // Read version from the shipped package.json (two levels up from dist/bin/).
21
22
  const pkgPath = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..", "package.json");
22
23
  const VERSION = JSON.parse(readFileSync(pkgPath, "utf-8")).version;
@@ -194,6 +195,9 @@ async function main() {
194
195
  await deployCmd(opts);
195
196
  });
196
197
  await program.parseAsync(process.argv);
198
+ // AFTER the command, so the nag never delays real work — and on stderr, so
199
+ // `--json` stdout stays parseable. Bounded + never-throwing by construction.
200
+ await notifyIfOutdated(VERSION);
197
201
  }
198
202
  main().catch((err) => {
199
203
  const mode = detectMode();
@@ -85,7 +85,7 @@ or a \`dist/\`), ship the artifact directly and skip the server-side
85
85
  npx layero@latest deploy --prebuilt out
86
86
  \`\`\`
87
87
 
88
- Full reference: https://docs.layero.ru/cli/agents
88
+ Full reference: https://docs.layero.ru/en/cli/agents
89
89
  ${AGENT_BLOCK_MARKER_END}
90
90
  `;
91
91
  }
@@ -0,0 +1,125 @@
1
+ /**
2
+ * Tell the user when their CLI is behind the published version.
3
+ *
4
+ * WHY this exists (2026-07-27): a stale CLI silently withholds every fix we
5
+ * ship. The `project_type` auto-flip for runtime apps landed 2026-05-26, yet
6
+ * prod still logged 20 failed deploys across 16 projects over the following
7
+ * month with "this repo looks like 'ssr_next' but the project is configured as
8
+ * 'spa'" — the exact crash that fix prevents. The machine that found this was
9
+ * itself running 0.5.3 against a published 0.8.11 and had never been told.
10
+ *
11
+ * A version nag is therefore not cosmetic: it is the delivery mechanism for
12
+ * every stability fix that already exists.
13
+ *
14
+ * Constraints this respects, in order of importance:
15
+ * 1. NEVER corrupt `--json`. Agents and CI parse stdout as JSON-lines, so the
16
+ * notice goes to STDERR, always, in every mode.
17
+ * 2. NEVER slow a deploy down. One 1.5s-timeout request, at most once every
18
+ * CHECK_INTERVAL_MS, and the result is cached on disk.
19
+ * 3. NEVER fail a command. Every path swallows its errors — an offline
20
+ * machine, a proxy, a corrupt cache file must all be silent no-ops.
21
+ */
22
+ import { readFile, writeFile, mkdir } from "node:fs/promises";
23
+ import path from "node:path";
24
+ import os from "node:os";
25
+ const REGISTRY_URL = "https://registry.npmjs.org/layero/latest";
26
+ const CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000; // once a day is plenty
27
+ const FETCH_TIMEOUT_MS = 1500;
28
+ function cachePath() {
29
+ return path.join(os.homedir(), ".layero", "update-check.json");
30
+ }
31
+ /** -1 = a < b, 0 = equal, 1 = a > b. Plain numeric semver; a pre-release tag
32
+ * (1.2.3-beta.1) compares by its numeric core, which is all we need here. */
33
+ export function compareVersions(a, b) {
34
+ const core = (v) => (v.split("-")[0] ?? "").split(".").map((n) => parseInt(n, 10) || 0);
35
+ const [x, y] = [core(a), core(b)];
36
+ for (let i = 0; i < Math.max(x.length, y.length); i++) {
37
+ const d = (x[i] ?? 0) - (y[i] ?? 0);
38
+ if (d !== 0)
39
+ return d > 0 ? 1 : -1;
40
+ }
41
+ return 0;
42
+ }
43
+ async function readCache() {
44
+ try {
45
+ const raw = await readFile(cachePath(), "utf-8");
46
+ const parsed = JSON.parse(raw);
47
+ if (typeof parsed.checked_at !== "number" || typeof parsed.latest !== "string")
48
+ return null;
49
+ return parsed;
50
+ }
51
+ catch {
52
+ return null;
53
+ }
54
+ }
55
+ async function writeCache(latest) {
56
+ try {
57
+ await mkdir(path.dirname(cachePath()), { recursive: true });
58
+ await writeFile(cachePath(), JSON.stringify({ checked_at: Date.now(), latest }), "utf-8");
59
+ }
60
+ catch {
61
+ /* a read-only HOME must not break the CLI */
62
+ }
63
+ }
64
+ async function fetchLatest() {
65
+ try {
66
+ const ctl = new AbortController();
67
+ const timer = setTimeout(() => ctl.abort(), FETCH_TIMEOUT_MS);
68
+ try {
69
+ const res = await fetch(REGISTRY_URL, {
70
+ signal: ctl.signal,
71
+ headers: { accept: "application/vnd.npm.install-v1+json" },
72
+ });
73
+ if (!res.ok)
74
+ return null;
75
+ const body = (await res.json());
76
+ return typeof body.version === "string" ? body.version : null;
77
+ }
78
+ finally {
79
+ clearTimeout(timer);
80
+ }
81
+ }
82
+ catch {
83
+ return null;
84
+ }
85
+ }
86
+ /**
87
+ * Resolve the newest published version, using the on-disk cache when fresh.
88
+ * Returns null when unknown (offline, throttled, first run with no network).
89
+ */
90
+ export async function resolveLatest(now = Date.now()) {
91
+ const cached = await readCache();
92
+ if (cached && now - cached.checked_at < CHECK_INTERVAL_MS)
93
+ return cached.latest;
94
+ const latest = await fetchLatest();
95
+ if (latest)
96
+ await writeCache(latest);
97
+ return latest ?? cached?.latest ?? null;
98
+ }
99
+ /** The notice text, or null when the CLI is current. Pure — unit-testable. */
100
+ export function updateNotice(current, latest) {
101
+ if (!latest)
102
+ return null;
103
+ if (compareVersions(current, latest) >= 0)
104
+ return null;
105
+ return (`\n Доступна новая версия Layero CLI: ${current} → ${latest}\n` +
106
+ ` Обновиться: npm i -g layero@latest\n` +
107
+ ` Старые версии не получают исправлений сборки и деплоя.\n`);
108
+ }
109
+ /**
110
+ * Best-effort check + print to stderr. Awaiting this is safe: it is bounded by
111
+ * FETCH_TIMEOUT_MS and never rejects. Opt out with LAYERO_NO_UPDATE_CHECK=1.
112
+ */
113
+ export async function notifyIfOutdated(current) {
114
+ try {
115
+ if (process.env.LAYERO_NO_UPDATE_CHECK)
116
+ return;
117
+ const notice = updateNotice(current, await resolveLatest());
118
+ // stderr on purpose — stdout is the JSON-lines channel for agents.
119
+ if (notice)
120
+ process.stderr.write(notice);
121
+ }
122
+ catch {
123
+ /* a version nag must never be the reason a deploy fails */
124
+ }
125
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "layero",
3
- "version": "0.8.11",
4
- "description": "Layero CLI \u2014 publish a local site with one command. No git, no GitHub, agent-friendly (Cursor, Claude Code).",
3
+ "version": "0.8.12",
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",