layero 0.7.0 → 0.7.2
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/dist/agent.js +7 -0
- package/dist/api.js +3 -0
- package/dist/commands/deploy.js +29 -1
- package/dist/detect.js +97 -2
- package/package.json +4 -2
package/dist/agent.js
CHANGED
|
@@ -124,6 +124,13 @@ function renderHuman(event) {
|
|
|
124
124
|
case "setup_applied":
|
|
125
125
|
process.stdout.write(`✓ Setup applied\n`);
|
|
126
126
|
break;
|
|
127
|
+
case "runtime_type_applied":
|
|
128
|
+
process.stdout.write(`✓ Project type set to ${event.project_type}\n`);
|
|
129
|
+
break;
|
|
130
|
+
case "runtime_type_apply_failed":
|
|
131
|
+
process.stdout.write(`! Failed to set project_type automatically: ${event.error}\n`);
|
|
132
|
+
process.stdout.write(` Build may fail at detect; accept the suggestion in the dashboard if so.\n`);
|
|
133
|
+
break;
|
|
127
134
|
case "deploy_started":
|
|
128
135
|
process.stdout.write(`→ Building...\n`);
|
|
129
136
|
break;
|
package/dist/api.js
CHANGED
|
@@ -68,6 +68,9 @@ export class ApiClient {
|
|
|
68
68
|
completeSetup(projectId, input) {
|
|
69
69
|
return this.request("POST", `/projects/${projectId}/setup`, input);
|
|
70
70
|
}
|
|
71
|
+
setRuntimeType(projectId, projectType) {
|
|
72
|
+
return this.request("POST", `/projects/${projectId}/runtime-type`, { project_type: projectType });
|
|
73
|
+
}
|
|
71
74
|
updateProject(projectId, input) {
|
|
72
75
|
return this.request("PATCH", `/projects/${projectId}`, input);
|
|
73
76
|
}
|
package/dist/commands/deploy.js
CHANGED
|
@@ -213,6 +213,8 @@ async function resolveSetupConfig(cwd, opts, existing) {
|
|
|
213
213
|
build_cmd: detected.build_cmd,
|
|
214
214
|
output_dir: detected.output_dir,
|
|
215
215
|
confident: detected.confident,
|
|
216
|
+
...(detected.runtime_kind ? { runtime_kind: detected.runtime_kind } : {}),
|
|
217
|
+
...(detected.ssr_warning ? { ssr_warning: detected.ssr_warning } : {}),
|
|
216
218
|
});
|
|
217
219
|
const framework_hint = opts.type ?? existing?.framework_hint ?? detected.framework_hint;
|
|
218
220
|
const build_cmd = existing?.build_cmd ?? detected.build_cmd;
|
|
@@ -227,7 +229,13 @@ async function resolveSetupConfig(cwd, opts, existing) {
|
|
|
227
229
|
else {
|
|
228
230
|
source = "detected";
|
|
229
231
|
}
|
|
230
|
-
return {
|
|
232
|
+
return {
|
|
233
|
+
framework_hint,
|
|
234
|
+
build_cmd,
|
|
235
|
+
output_dir,
|
|
236
|
+
source,
|
|
237
|
+
...(detected.runtime_kind ? { runtime_kind: detected.runtime_kind } : {}),
|
|
238
|
+
};
|
|
231
239
|
}
|
|
232
240
|
export async function deployCmd(opts) {
|
|
233
241
|
const mode = detectMode();
|
|
@@ -306,6 +314,26 @@ export async function deployCmd(opts) {
|
|
|
306
314
|
root_directory: opts.root ?? null,
|
|
307
315
|
});
|
|
308
316
|
emit({ event: "setup_applied" });
|
|
317
|
+
// Newly-created projects default to project_type='spa'. If detect
|
|
318
|
+
// says the repo is SSR (Next.js without `output: 'export'`), flip
|
|
319
|
+
// the type now — otherwise the first build crashes at the detect
|
|
320
|
+
// stage with "looks like ssr_next but configured as spa", forcing
|
|
321
|
+
// the user to open the dashboard and accept the suggestion. Only
|
|
322
|
+
// applied on first setup; an explicitly-configured spa project
|
|
323
|
+
// that drops a next.config later keeps user's choice.
|
|
324
|
+
if (setup.runtime_kind) {
|
|
325
|
+
try {
|
|
326
|
+
project = await api.setRuntimeType(project.id, setup.runtime_kind);
|
|
327
|
+
emit({ event: "runtime_type_applied", project_type: setup.runtime_kind });
|
|
328
|
+
}
|
|
329
|
+
catch (err) {
|
|
330
|
+
// Non-fatal: the build will fall back to the dashboard suggestion
|
|
331
|
+
// flow exactly as it did before this CLI fix. Tell the user what
|
|
332
|
+
// happened so they don't burn a deploy on a surprise crash.
|
|
333
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
334
|
+
emit({ event: "runtime_type_apply_failed", error: msg });
|
|
335
|
+
}
|
|
336
|
+
}
|
|
309
337
|
}
|
|
310
338
|
else if (opts.root !== undefined) {
|
|
311
339
|
// Active project: --root patches the existing row so the *next*
|
package/dist/detect.js
CHANGED
|
@@ -26,6 +26,41 @@ async function fileExists(cwd, ...candidates) {
|
|
|
26
26
|
}
|
|
27
27
|
return false;
|
|
28
28
|
}
|
|
29
|
+
// Mirrors `frameworks/nextjs.py:_NEXT_STATIC_EXPORT_RE` and
|
|
30
|
+
// `next_config_static_export()` so CLI and builder can't disagree on
|
|
31
|
+
// what counts as a static-export Next.js project. The class-of-bug we
|
|
32
|
+
// hit on 2026-05-22 (builder detector v72 fix) and 2026-05-26 (CLI side)
|
|
33
|
+
// was exactly this kind of dual detector drift.
|
|
34
|
+
const NEXT_STATIC_EXPORT_RE = /output\s*:\s*['"]export['"]/m;
|
|
35
|
+
// Strip JS line and block comments before regex matching. Without this,
|
|
36
|
+
// a comment like `// see output: 'export'` falsely matches as
|
|
37
|
+
// static-export — observed 2026-05-26 on the SSR smoke canary fixture.
|
|
38
|
+
// Naive (does not understand strings that contain comment-like tokens),
|
|
39
|
+
// but real next.config files don't have that shape.
|
|
40
|
+
const JS_COMMENT_RE = /\/\/[^\n]*|\/\*[\s\S]*?\*\//gm;
|
|
41
|
+
const stripJsComments = (text) => text.replace(JS_COMMENT_RE, "");
|
|
42
|
+
async function readNextConfigText(cwd) {
|
|
43
|
+
for (const name of ["next.config.mjs", "next.config.ts", "next.config.js", "next.config.cjs"]) {
|
|
44
|
+
try {
|
|
45
|
+
return await fs.readFile(path.join(cwd, name), "utf-8");
|
|
46
|
+
}
|
|
47
|
+
catch (err) {
|
|
48
|
+
if (err?.code === "ENOENT")
|
|
49
|
+
continue;
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
// True -> config has `output: 'export'` (static-export SPA).
|
|
56
|
+
// False -> config exists but no export marker (SSR Next.js).
|
|
57
|
+
// null -> no next.config file at all; caller treats as static-default.
|
|
58
|
+
async function nextConfigStaticExport(cwd) {
|
|
59
|
+
const text = await readNextConfigText(cwd);
|
|
60
|
+
if (text === null)
|
|
61
|
+
return null;
|
|
62
|
+
return NEXT_STATIC_EXPORT_RE.test(stripJsComments(text));
|
|
63
|
+
}
|
|
29
64
|
async function hasAnyHtml(cwd) {
|
|
30
65
|
// Shallow check — only top-level. Recursive walk is too expensive here
|
|
31
66
|
// and the static fallback is already permissive enough.
|
|
@@ -60,12 +95,20 @@ function buildCmd(pkg, fallback) {
|
|
|
60
95
|
export async function detectProject(cwd) {
|
|
61
96
|
const pkg = await readPkg(cwd);
|
|
62
97
|
if (pkg) {
|
|
63
|
-
if (hasDep(pkg, "next") || (await fileExists(cwd, "next.config.js", "next.config.mjs", "next.config.ts"))) {
|
|
98
|
+
if (hasDep(pkg, "next") || (await fileExists(cwd, "next.config.js", "next.config.mjs", "next.config.ts", "next.config.cjs"))) {
|
|
99
|
+
// A Next.js repo is SSR unless next.config explicitly declares
|
|
100
|
+
// `output: 'export'`. We only flag ssr_next when the config file
|
|
101
|
+
// exists AND lacks the export marker — same rule the builder uses
|
|
102
|
+
// in `runtime_detect.py:46`. Without a next.config file at all we
|
|
103
|
+
// err on the side of static (legacy `next export` workflow).
|
|
104
|
+
const exportFlag = await nextConfigStaticExport(cwd);
|
|
105
|
+
const isSsr = exportFlag === false;
|
|
64
106
|
return {
|
|
65
107
|
framework_hint: "nextjs",
|
|
66
108
|
build_cmd: buildCmd(pkg, "npx next build"),
|
|
67
|
-
output_dir: "out",
|
|
109
|
+
output_dir: isSsr ? ".next" : "out",
|
|
68
110
|
confident: true,
|
|
111
|
+
...(isSsr ? { runtime_kind: "ssr_next" } : {}),
|
|
69
112
|
};
|
|
70
113
|
}
|
|
71
114
|
if (hasDep(pkg, "nuxt") || hasDep(pkg, "nuxt3") || (await fileExists(cwd, "nuxt.config.ts", "nuxt.config.js", "nuxt.config.mjs"))) {
|
|
@@ -76,19 +119,50 @@ export async function detectProject(cwd) {
|
|
|
76
119
|
: buildScript
|
|
77
120
|
? "npm run build"
|
|
78
121
|
: "npx nuxt generate";
|
|
122
|
+
// Nuxt defaults to SSR. Layero hosts only static for Nuxt today,
|
|
123
|
+
// so a project without an explicit static signal (`nuxt generate`
|
|
124
|
+
// script, or `ssr: false` / `nitro.preset='static'` in config)
|
|
125
|
+
// will build but the resulting `.output/server/` is useless to us.
|
|
126
|
+
// Mirrors builder's `frameworks/nuxt.py` SSR hint.
|
|
127
|
+
const nuxtStatic = !!generateScript || (await nuxtConfigDeclaresStatic(cwd));
|
|
79
128
|
return {
|
|
80
129
|
framework_hint: "nuxt",
|
|
81
130
|
build_cmd: cmd,
|
|
82
131
|
output_dir: ".output/public",
|
|
83
132
|
confident: true,
|
|
133
|
+
...(nuxtStatic ? {} : {
|
|
134
|
+
ssr_warning: "Nuxt без `nuxt generate` или `ssr: false` собирается как SSR-сервер; " +
|
|
135
|
+
"Layero хостит только статику Nuxt. Добавьте `\"generate\": \"nuxt generate\"` " +
|
|
136
|
+
"в scripts или поставьте `ssr: false` в nuxt.config.",
|
|
137
|
+
}),
|
|
84
138
|
};
|
|
85
139
|
}
|
|
86
140
|
if (hasDep(pkg, "@sveltejs/kit") || (await fileExists(cwd, "svelte.config.js"))) {
|
|
141
|
+
// SvelteKit needs an adapter. adapter-static = SPA path; anything
|
|
142
|
+
// else (adapter-node, adapter-auto, …) is server-side. Mirrors
|
|
143
|
+
// builder's `frameworks/svelte.py:validate_static`.
|
|
144
|
+
const hasStaticAdapter = hasDep(pkg, "@sveltejs/adapter-static");
|
|
145
|
+
const serverAdapter = !hasStaticAdapter
|
|
146
|
+
? Object.keys({ ...(pkg.dependencies ?? {}), ...(pkg.devDependencies ?? {}) })
|
|
147
|
+
.find((d) => d.startsWith("@sveltejs/adapter-") && d !== "@sveltejs/adapter-static")
|
|
148
|
+
: undefined;
|
|
149
|
+
let ssrWarning;
|
|
150
|
+
if (serverAdapter) {
|
|
151
|
+
ssrWarning =
|
|
152
|
+
`SvelteKit использует ${serverAdapter} (серверный адаптер). ` +
|
|
153
|
+
"Layero хостит только статику — поставьте `@sveltejs/adapter-static`.";
|
|
154
|
+
}
|
|
155
|
+
else if (!hasStaticAdapter) {
|
|
156
|
+
ssrWarning =
|
|
157
|
+
"SvelteKit без явного адаптера — поставьте `@sveltejs/adapter-static` " +
|
|
158
|
+
"для статического хостинга на Layero.";
|
|
159
|
+
}
|
|
87
160
|
return {
|
|
88
161
|
framework_hint: "sveltekit",
|
|
89
162
|
build_cmd: buildCmd(pkg, "npx vite build"),
|
|
90
163
|
output_dir: "build",
|
|
91
164
|
confident: true,
|
|
165
|
+
...(ssrWarning ? { ssr_warning: ssrWarning } : {}),
|
|
92
166
|
};
|
|
93
167
|
}
|
|
94
168
|
if (hasDep(pkg, "gatsby") || (await fileExists(cwd, "gatsby-config.js", "gatsby-config.ts"))) {
|
|
@@ -243,6 +317,27 @@ const HUGO_CONFIG_TOKENS = [
|
|
|
243
317
|
"minVersion",
|
|
244
318
|
"theme",
|
|
245
319
|
];
|
|
320
|
+
// Match `ssr: false` or `nitro: { preset: 'static' }` in nuxt.config —
|
|
321
|
+
// either marker keeps the project on the SPA pipeline. Mirrors the same
|
|
322
|
+
// "string-search before AST" approach used for Next.js.
|
|
323
|
+
const NUXT_STATIC_SSR_RE = /\bssr\s*:\s*false\b/m;
|
|
324
|
+
const NUXT_STATIC_PRESET_RE = /preset\s*:\s*['"]static['"]/m;
|
|
325
|
+
async function nuxtConfigDeclaresStatic(cwd) {
|
|
326
|
+
for (const name of ["nuxt.config.mjs", "nuxt.config.ts", "nuxt.config.js"]) {
|
|
327
|
+
try {
|
|
328
|
+
const txt = stripJsComments(await fs.readFile(path.join(cwd, name), "utf-8"));
|
|
329
|
+
if (NUXT_STATIC_SSR_RE.test(txt) || NUXT_STATIC_PRESET_RE.test(txt))
|
|
330
|
+
return true;
|
|
331
|
+
return false; // config found, no static marker → SSR
|
|
332
|
+
}
|
|
333
|
+
catch (err) {
|
|
334
|
+
if (err?.code === "ENOENT")
|
|
335
|
+
continue;
|
|
336
|
+
return false;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
return false; // no config at all → defaults to SSR
|
|
340
|
+
}
|
|
246
341
|
async function hasVitepressConfig(cwd, prefix = ".vitepress") {
|
|
247
342
|
for (const name of ["config.ts", "config.js", "config.mts", "config.mjs"]) {
|
|
248
343
|
try {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "layero",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.2",
|
|
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",
|
|
@@ -41,6 +41,7 @@
|
|
|
41
41
|
"scripts": {
|
|
42
42
|
"build": "tsc -p tsconfig.json",
|
|
43
43
|
"prepublishOnly": "npm run build",
|
|
44
|
+
"test": "vitest run",
|
|
44
45
|
"dev": "tsc -p tsconfig.json --watch",
|
|
45
46
|
"postinstall": "node scripts/postinstall.cjs || true"
|
|
46
47
|
},
|
|
@@ -53,6 +54,7 @@
|
|
|
53
54
|
},
|
|
54
55
|
"devDependencies": {
|
|
55
56
|
"@types/node": "^20.14.10",
|
|
56
|
-
"typescript": "^5.5.4"
|
|
57
|
+
"typescript": "^5.5.4",
|
|
58
|
+
"vitest": "^4.1.7"
|
|
57
59
|
}
|
|
58
60
|
}
|