layero 0.8.11 → 0.8.13

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;
@@ -143,7 +144,9 @@ async function main() {
143
144
  });
144
145
  program
145
146
  .command("rollback")
146
- .description("Re-activate the previous successful deploy on the project's default branch.")
147
+ .description("Re-activate the previous successful deploy on a branch environment. " +
148
+ "DOES NOT move the production pointer — the apex keeps serving whatever " +
149
+ "it served before. To bring production back, use `layero promote <sha>`.")
147
150
  .option("--project <id_or_slug>", "target project (default: linked .layero/project.json)")
148
151
  .option("--branch <name>", "branch to roll back (default: project's default_branch)")
149
152
  .option("--deploy <id_or_sha>", "explicit deploy id or commit sha prefix to roll back to")
@@ -180,7 +183,10 @@ async function main() {
180
183
  .option("--root <dir>", "monorepo: subdirectory inside the repo that the builder treats as the app root (saved on the project; future GitHub-push and hook triggers use the same value)")
181
184
  .option("--prod", "deploy to production (replaces apex_hostname's active deploy). Without this flag, deploys go to the project's CLI preview pseudo-branch.")
182
185
  .option("--promote", "pin the project apex to this deploy after a successful build (V071). Works for any branch — e.g. `--promote` without --prod publishes a CLI preview straight to production.")
183
- .option("--branch <name>", "deploy to a specific branch's environment. Wins over --prod.")
186
+ .option("--branch <name>", "ACCEPTED BUT IGNORED for direct uploads: the backend files every " +
187
+ "archive deploy under the reserved `cli` environment. Branch " +
188
+ "environments come from pushes to a connected repository, not from " +
189
+ "this flag. Kept for backwards compatibility.")
184
190
  .option("--org <slug>", "Layero organization slug for first-time project creation. Defaults to personal; required when you're a member of multiple orgs and want a non-personal home.")
185
191
  .addHelpText("after", "\nExamples:\n" +
186
192
  " $ layero deploy # preview deploy (CLI pseudo-branch), auto-detect framework\n" +
@@ -194,6 +200,9 @@ async function main() {
194
200
  await deployCmd(opts);
195
201
  });
196
202
  await program.parseAsync(process.argv);
203
+ // AFTER the command, so the nag never delays real work — and on stderr, so
204
+ // `--json` stdout stays parseable. Bounded + never-throwing by construction.
205
+ await notifyIfOutdated(VERSION);
197
206
  }
198
207
  main().catch((err) => {
199
208
  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,6 +1,6 @@
1
1
  {
2
2
  "name": "layero",
3
- "version": "0.8.11",
3
+ "version": "0.8.13",
4
4
  "description": "Layero CLI \u2014 publish a local site with one command. No git, no GitHub, agent-friendly (Cursor, Claude Code).",
5
5
  "license": "MIT",
6
6
  "type": "module",