layero 0.11.4 → 0.11.6
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/detect.js +60 -3
- package/dist/logs.js +16 -1
- 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/detect.js
CHANGED
|
@@ -460,6 +460,38 @@ async function findServerEntry(cwd, pkg) {
|
|
|
460
460
|
}
|
|
461
461
|
return null;
|
|
462
462
|
}
|
|
463
|
+
/**
|
|
464
|
+
* Сервер, привязанный к localhost: снаружи контейнера до него не достучаться,
|
|
465
|
+
* и запуск оборвётся по таймауту — а текст отказа причину не называет
|
|
466
|
+
* (чистая комната 19.09.2026, T-20260919-9). Сборщик судит по команде запуска;
|
|
467
|
+
* здесь — по коду точки входа, до первого деплоя.
|
|
468
|
+
*/
|
|
469
|
+
const LOOPBACK_LISTEN_RE = /\.listen\s*\([^;\n]*?["'`](127\.0\.0\.1|localhost|::1)["'`]|host\s*[:=]\s*["'](127\.0\.0\.1|localhost)["']|--host[= ]+(127\.0\.0\.1|localhost)\b/;
|
|
470
|
+
/** Файл, который запускает команда `node|tsx|ts-node <file>`. */
|
|
471
|
+
function entryFromStart(start) {
|
|
472
|
+
const m = start?.match(/^(?:node|tsx|ts-node|bun)\s+(?:--[\w-]+\s+)*([^\s&|;]+\.(?:m?js|cjs|ts|mts))/);
|
|
473
|
+
return m?.[1] ?? null;
|
|
474
|
+
}
|
|
475
|
+
function loopbackNote(entry) {
|
|
476
|
+
return `${entry} binds the server to localhost (127.0.0.1): unreachable from outside the container, so the launch times out and the failure text does not say why. Listen on 0.0.0.0 and $PORT.`;
|
|
477
|
+
}
|
|
478
|
+
/** Точка входа, которая слушает только localhost, — или null. */
|
|
479
|
+
async function loopbackEntry(cwd, pkg, start) {
|
|
480
|
+
const names = [
|
|
481
|
+
...(entryFromStart(start) ? [entryFromStart(start)] : []),
|
|
482
|
+
...(typeof pkg?.main === "string" ? [pkg.main] : []),
|
|
483
|
+
...SERVER_ENTRY_CANDIDATES,
|
|
484
|
+
"server/index.ts", "src/index.ts", "src/server.ts", "src/main.ts", "main.py", "app.py",
|
|
485
|
+
];
|
|
486
|
+
for (const n of names) {
|
|
487
|
+
const text = await readText(path.join(cwd, n));
|
|
488
|
+
if (text !== null && LOOPBACK_LISTEN_RE.test(text.slice(0, 64 * 1024)))
|
|
489
|
+
return n;
|
|
490
|
+
}
|
|
491
|
+
if (start && /--host[= ]+(127\.0\.0\.1|localhost)\b/.test(start))
|
|
492
|
+
return "start script";
|
|
493
|
+
return null;
|
|
494
|
+
}
|
|
463
495
|
/** Первая папка с index.html, если в корне его нет, — по правилу сборщика
|
|
464
496
|
* (`discover_served_root`): мельчайшая, затем по алфавиту. */
|
|
465
497
|
async function servedSubdir(cwd, maxDepth = 3) {
|
|
@@ -587,6 +619,19 @@ async function detectFromSnapshot(cwd, snap, opts) {
|
|
|
587
619
|
}
|
|
588
620
|
// node_web / python_web / streamlit / gradio / fullstack: the container
|
|
589
621
|
// serves the app, nothing is uploaded as static files from this plan.
|
|
622
|
+
const runtimeNotes = [];
|
|
623
|
+
if (plan.projectKind === "fullstack") {
|
|
624
|
+
runtimeNotes.push("Full-stack layout from layero.json: the frontend is served as files, the backend runs in a container.");
|
|
625
|
+
}
|
|
626
|
+
{
|
|
627
|
+
// Узнанный сервер тоже падает на запуске, если слушает localhost, — а
|
|
628
|
+
// текст отказа причину не называет (T-20260919-9).
|
|
629
|
+
const pkg = snap.packageJson;
|
|
630
|
+
const start = str(layero?.start) ?? str(layero?.startCommand) ?? scriptOf(pkg, "start");
|
|
631
|
+
const loop = await loopbackEntry(cwd, pkg, start);
|
|
632
|
+
if (loop)
|
|
633
|
+
runtimeNotes.push(loopbackNote(loop));
|
|
634
|
+
}
|
|
590
635
|
return {
|
|
591
636
|
framework_hint: plan.framework,
|
|
592
637
|
build_cmd: null,
|
|
@@ -594,9 +639,7 @@ async function detectFromSnapshot(cwd, snap, opts) {
|
|
|
594
639
|
confident: true,
|
|
595
640
|
runtime_kind: plan.projectType,
|
|
596
641
|
sources,
|
|
597
|
-
...(
|
|
598
|
-
? { hint: "Full-stack layout from layero.json: the frontend is served as files, the backend runs in a container." }
|
|
599
|
-
: {}),
|
|
642
|
+
...(runtimeNotes.length ? { hint: runtimeNotes.join(" ") } : {}),
|
|
600
643
|
};
|
|
601
644
|
}
|
|
602
645
|
const framework = plan.framework;
|
|
@@ -681,6 +724,20 @@ async function detectFromSnapshot(cwd, snap, opts) {
|
|
|
681
724
|
outputDir = fileOutput;
|
|
682
725
|
outputSource = "layero.json";
|
|
683
726
|
}
|
|
727
|
+
{
|
|
728
|
+
const pkg = snap.packageJson;
|
|
729
|
+
const buildScript = scriptOf(pkg, "build");
|
|
730
|
+
// Проверка типов внутри сборки: на платформе она упадёт так же, как
|
|
731
|
+
// локально, а деплой ради этого — потраченная сборка (чистая комната 19.09).
|
|
732
|
+
if (buildScript && /(^|[\s&;])tsc\b/.test(buildScript) && !/\bnoEmit\b.*\|\|/.test(buildScript)) {
|
|
733
|
+
notes.push(`The build script runs the TypeScript checker (\`${buildScript}\`): a type error fails the build on the platform exactly as it does locally — run it locally before deploying.`);
|
|
734
|
+
}
|
|
735
|
+
const loop = await loopbackEntry(cwd, pkg, scriptOf(pkg, "start"));
|
|
736
|
+
// Форма узнана верно, поэтому `confident` не трогаем: это дефект кода,
|
|
737
|
+
// а не догадка детекта — и агенту нужен именно этот текст.
|
|
738
|
+
if (loop)
|
|
739
|
+
notes.push(loopbackNote(loop));
|
|
740
|
+
}
|
|
684
741
|
const warning = ssrWarning(snap, framework);
|
|
685
742
|
const hint = [shape?.hint, ...notes].filter(Boolean).join(" ");
|
|
686
743
|
return {
|
package/dist/logs.js
CHANGED
|
@@ -10,6 +10,7 @@ const POLL_INTERVAL_MS = 1500;
|
|
|
10
10
|
// Теперь: таймаут на запрос (api.ts), до MAX_POLL_FAILURES сбоев ПОДРЯД с
|
|
11
11
|
// растущей паузой, и свой код ошибки с подсказкой, где смотреть итог.
|
|
12
12
|
const MAX_POLL_FAILURES = 8;
|
|
13
|
+
const QUEUED_EVERY_MS = 15_000;
|
|
13
14
|
const RETRY_BASE_MS = 1500;
|
|
14
15
|
const RETRY_CAP_MS = 10_000;
|
|
15
16
|
/** Сбой, который повторять бессмысленно: сервер ответил отказом по существу. */
|
|
@@ -53,10 +54,17 @@ export async function streamDeployLogs(api, deployId) {
|
|
|
53
54
|
let lastStage = null;
|
|
54
55
|
let hiddenNoted = false;
|
|
55
56
|
let failures = 0;
|
|
57
|
+
// Этап объявляется один раз: строки прошлого этапа приходят вперемешку с
|
|
58
|
+
// началом нового, и `announce` по строке дублировал `clone`/`probe`
|
|
59
|
+
// (чистая комната 19.09.2026).
|
|
60
|
+
const announced = new Set();
|
|
61
|
+
const startedAt = Date.now();
|
|
62
|
+
let lastQueuedAt = startedAt;
|
|
56
63
|
const sleep = (ms) => new Promise((res) => setTimeout(res, retryDelayScale() * ms));
|
|
57
64
|
const announce = (name) => {
|
|
58
|
-
if (name === lastStage)
|
|
65
|
+
if (name === lastStage || announced.has(name))
|
|
59
66
|
return;
|
|
67
|
+
announced.add(name);
|
|
60
68
|
lastStage = name;
|
|
61
69
|
if (json) {
|
|
62
70
|
emit({ event: "stage", name });
|
|
@@ -112,6 +120,13 @@ export async function streamDeployLogs(api, deployId) {
|
|
|
112
120
|
}
|
|
113
121
|
if (poll.current_stage)
|
|
114
122
|
announce(poll.current_stage);
|
|
123
|
+
if (json &&
|
|
124
|
+
poll.status === "queued" &&
|
|
125
|
+
announced.size === 0 &&
|
|
126
|
+
Date.now() - lastQueuedAt >= QUEUED_EVERY_MS) {
|
|
127
|
+
lastQueuedAt = Date.now();
|
|
128
|
+
emit({ event: "queued", waited_s: Math.round((Date.now() - startedAt) / 1000) });
|
|
129
|
+
}
|
|
115
130
|
if (poll.terminal) {
|
|
116
131
|
return poll;
|
|
117
132
|
}
|
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