layero 0.5.4 → 0.6.0
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 +34 -2
- package/dist/agent.js +3 -0
- package/dist/bin/layero.js +2 -1
- package/dist/commands/deploy.js +58 -6
- package/dist/detect.js +84 -0
- package/dist/pack.js +52 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -59,8 +59,17 @@ Run `layero <cmd> --help` for full options.
|
|
|
59
59
|
|
|
60
60
|
## `layero deploy` flags
|
|
61
61
|
|
|
62
|
-
- `--type <preset>` — framework override: `vite`, `
|
|
63
|
-
`sveltekit`, `nuxt`, `gatsby`, `docusaurus`, `
|
|
62
|
+
- `--type <preset>` — framework override: `vite`, `vitepress`, `next`,
|
|
63
|
+
`astro`, `cra`, `sveltekit`, `nuxt`, `gatsby`, `docusaurus`, `eleventy`
|
|
64
|
+
(alias `11ty`), `hugo`, `static`. **Optional** — auto-detected from
|
|
65
|
+
`package.json` and config files when omitted.
|
|
66
|
+
- `--prebuilt [dir]` — ship an already-built artifact instead of building
|
|
67
|
+
remotely. Without an argument, picks the first existing of
|
|
68
|
+
`dist/`, `build/`, `public/`, `out/`, `_site/`, `.output/public/`,
|
|
69
|
+
`docs/.vitepress/dist/`, `.vitepress/dist/`. With `--prebuilt ./my-out`
|
|
70
|
+
uses that explicit path. Use this for CI flows that build in the
|
|
71
|
+
pipeline, Webflow / Framer exports, or whenever you don't want the
|
|
72
|
+
platform to run install/build for you.
|
|
64
73
|
- `--name <name>` — project name (only on first deploy).
|
|
65
74
|
- `--project <id_or_slug>` — deploy into an existing project, ignoring
|
|
66
75
|
`./.layero/project.json` (useful for CI).
|
|
@@ -82,10 +91,33 @@ Run `layero <cmd> --help` for full options.
|
|
|
82
91
|
| `gatsby` dep | gatsby | `npm run build` | `public` |
|
|
83
92
|
| `astro` dep / `astro.config.*` | astro | `npm run build` | `dist` |
|
|
84
93
|
| `@docusaurus/core` dep / `docusaurus.config.*` | docusaurus | `npm run build` | `build` |
|
|
94
|
+
| `vitepress` dep / `.vitepress/config.*` / `docs/.vitepress/config.*` | vitepress | `npm run docs:build` (or `npx vitepress build`) | `.vitepress/dist` or `docs/.vitepress/dist` |
|
|
85
95
|
| `vite` dep / `vite.config.*` | vite | `npm run build` | `dist` |
|
|
86
96
|
| `react-scripts` dep | cra | `npm run build` | `build` |
|
|
97
|
+
| `@11ty/eleventy` dep / `.eleventy.js` / `eleventy.config.*` | eleventy | `npm run build` (or `npx @11ty/eleventy`) | `_site` |
|
|
98
|
+
| `hugo.{toml,yaml,json}` or `config.*` with Hugo markers (`baseURL`, `[markup]`, …) | hugo | `hugo --gc --minify` (no install needed) | `public` |
|
|
87
99
|
| any `.html` at root, no `package.json` | static | `true` (no-op) | `.` |
|
|
88
100
|
|
|
101
|
+
## Bring-your-own-build (`--prebuilt`)
|
|
102
|
+
|
|
103
|
+
If you already build your site yourself — in CI, via a desktop tool like
|
|
104
|
+
Webflow/Framer, or because you want a guaranteed deterministic artifact —
|
|
105
|
+
skip the platform's install/build entirely:
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
# Auto-pick the output directory:
|
|
109
|
+
layero deploy --prebuilt
|
|
110
|
+
|
|
111
|
+
# Or point at a specific one:
|
|
112
|
+
layero deploy --prebuilt ./dist
|
|
113
|
+
layero deploy --prebuilt ./build/static
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
What changes: only the files inside the directory you point at are
|
|
117
|
+
uploaded (no source-tree filters like `.gitignore` apply). The platform
|
|
118
|
+
ships them verbatim — no detect, no install, no build. Smaller archive,
|
|
119
|
+
faster deploys, no surprises from the platform's package-manager defaults.
|
|
120
|
+
|
|
89
121
|
Override anything by editing `.layero/project.json` after the first `layero init`.
|
|
90
122
|
|
|
91
123
|
## Agent / JSON mode
|
package/dist/agent.js
CHANGED
|
@@ -109,6 +109,9 @@ function renderHuman(event) {
|
|
|
109
109
|
case "detected":
|
|
110
110
|
process.stdout.write(`→ Detected ${event.framework} (build: ${event.build_cmd}, output: ${event.output_dir})\n`);
|
|
111
111
|
break;
|
|
112
|
+
case "prebuilt":
|
|
113
|
+
process.stdout.write(`→ Prebuilt mode: shipping ${event.dir}\n`);
|
|
114
|
+
break;
|
|
112
115
|
case "packing":
|
|
113
116
|
process.stdout.write(`→ Packed ${event.files} files (${(event.bytes / (1024 * 1024)).toFixed(2)} MB)\n`);
|
|
114
117
|
break;
|
package/dist/bin/layero.js
CHANGED
|
@@ -104,11 +104,12 @@ async function main() {
|
|
|
104
104
|
program
|
|
105
105
|
.command("deploy")
|
|
106
106
|
.description("Pack the current directory and deploy it. Framework, build command and output directory are auto-detected.")
|
|
107
|
-
.option("-t, --type <preset>", "framework override (vite | next | astro | cra | sveltekit | nuxt | gatsby | docusaurus | static)")
|
|
107
|
+
.option("-t, --type <preset>", "framework override (vite | vitepress | next | astro | cra | sveltekit | nuxt | gatsby | docusaurus | eleventy | hugo | static)")
|
|
108
108
|
.option("--name <name>", "project name (only used on first deploy)")
|
|
109
109
|
.option("--project <id_or_slug>", "deploy into an existing project, ignoring local config")
|
|
110
110
|
.option("-y, --yes", "non-interactive: accept defaults and skip --prod confirmation")
|
|
111
111
|
.option("--config", "(legacy alias of the default behaviour — auto-detect + .layero/project.json values)")
|
|
112
|
+
.option("--prebuilt [dir]", "ship an already-built artifact directory (default auto-pick: dist/build/public/out/_site/...)")
|
|
112
113
|
.option("--prod", "deploy to production (replaces apex_hostname's active deploy). Without this flag, deploys go to the project's CLI preview pseudo-branch.")
|
|
113
114
|
.option("--branch <name>", "deploy to a specific branch's environment. Wins over --prod.")
|
|
114
115
|
.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.")
|
package/dist/commands/deploy.js
CHANGED
|
@@ -5,7 +5,7 @@ import chalk from "chalk";
|
|
|
5
5
|
import { ApiClient, ApiError, uploadArchive } from "../api.js";
|
|
6
6
|
import { loadConfig } from "../config.js";
|
|
7
7
|
import { loadProjectConfig, persistProjectLinking, projectConfigPath, } from "../project-config.js";
|
|
8
|
-
import { packCwd } from "../pack.js";
|
|
8
|
+
import { packCwd, packDirectory } from "../pack.js";
|
|
9
9
|
import { streamDeployLogs } from "../logs.js";
|
|
10
10
|
import { detectProject } from "../detect.js";
|
|
11
11
|
import { LayeroError, detectMode, emit } from "../agent.js";
|
|
@@ -22,6 +22,10 @@ const VALID_TYPES = new Set([
|
|
|
22
22
|
"static",
|
|
23
23
|
"generic",
|
|
24
24
|
"docusaurus",
|
|
25
|
+
"hugo",
|
|
26
|
+
"eleventy",
|
|
27
|
+
"11ty",
|
|
28
|
+
"vitepress",
|
|
25
29
|
]);
|
|
26
30
|
function dashboardOrigin(apiUrl) {
|
|
27
31
|
const override = process.env.LAYERO_DASHBOARD_URL;
|
|
@@ -144,13 +148,16 @@ async function resolveOrCreateProject(api, cwd, opts, existing) {
|
|
|
144
148
|
});
|
|
145
149
|
return { project, createdNow: true };
|
|
146
150
|
}
|
|
147
|
-
async function packAndUpload(api, cwd, project) {
|
|
148
|
-
const pack =
|
|
151
|
+
async function packAndUpload(api, cwd, project, prebuiltDir) {
|
|
152
|
+
const pack = prebuiltDir
|
|
153
|
+
? await packDirectory(path.resolve(cwd, prebuiltDir), project.slug)
|
|
154
|
+
: await packCwd(cwd, project.slug);
|
|
149
155
|
emit({
|
|
150
156
|
event: "packing",
|
|
151
157
|
files: pack.fileCount,
|
|
152
158
|
bytes: pack.size,
|
|
153
159
|
sha256: pack.sha256,
|
|
160
|
+
...(prebuiltDir ? { prebuilt_dir: prebuiltDir } : {}),
|
|
154
161
|
});
|
|
155
162
|
const init = await api.initUpload(project.id);
|
|
156
163
|
emit({ event: "uploading" });
|
|
@@ -162,6 +169,35 @@ async function packAndUpload(api, cwd, project) {
|
|
|
162
169
|
archivePath: pack.archivePath,
|
|
163
170
|
};
|
|
164
171
|
}
|
|
172
|
+
const PREBUILT_AUTO_DIRS = [
|
|
173
|
+
"dist",
|
|
174
|
+
"build",
|
|
175
|
+
"public",
|
|
176
|
+
"out",
|
|
177
|
+
"_site",
|
|
178
|
+
".output/public",
|
|
179
|
+
"docs/.vitepress/dist",
|
|
180
|
+
".vitepress/dist",
|
|
181
|
+
];
|
|
182
|
+
async function resolvePrebuiltDir(cwd, raw) {
|
|
183
|
+
if (raw === undefined || raw === false)
|
|
184
|
+
return null;
|
|
185
|
+
if (typeof raw === "string" && raw.length > 0) {
|
|
186
|
+
return raw;
|
|
187
|
+
}
|
|
188
|
+
// No explicit dir — pick the first existing common output directory.
|
|
189
|
+
for (const candidate of PREBUILT_AUTO_DIRS) {
|
|
190
|
+
try {
|
|
191
|
+
const stat = await fs.stat(path.join(cwd, candidate));
|
|
192
|
+
if (stat.isDirectory())
|
|
193
|
+
return candidate;
|
|
194
|
+
}
|
|
195
|
+
catch {
|
|
196
|
+
// try next
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
throw new LayeroError("prebuilt_no_dir", "could not auto-detect a built artifact directory", `pass it explicitly: --prebuilt ./dist (tried: ${PREBUILT_AUTO_DIRS.join(", ")})`);
|
|
200
|
+
}
|
|
165
201
|
// Resolve the framework / build / output config to send to completeSetup.
|
|
166
202
|
// Precedence: explicit CLI args > .layero/project.json > auto-detect.
|
|
167
203
|
async function resolveSetupConfig(cwd, opts, existing) {
|
|
@@ -213,9 +249,24 @@ export async function deployCmd(opts) {
|
|
|
213
249
|
return;
|
|
214
250
|
}
|
|
215
251
|
}
|
|
252
|
+
// Prebuilt deploys: caller has already built the artifact and points at
|
|
253
|
+
// its directory. Setup-config detection is skipped (we report a synthetic
|
|
254
|
+
// static setup so completeSetup gets *some* values and the dashboard
|
|
255
|
+
// shows the project is configured).
|
|
256
|
+
const prebuiltDir = await resolvePrebuiltDir(cwd, opts.prebuilt);
|
|
216
257
|
// Resolve setup config (auto-detect + overrides). Done eagerly so the
|
|
217
258
|
// user sees what we detected before any network I/O.
|
|
218
|
-
const setup =
|
|
259
|
+
const setup = prebuiltDir
|
|
260
|
+
? {
|
|
261
|
+
framework_hint: "static",
|
|
262
|
+
build_cmd: "true",
|
|
263
|
+
output_dir: ".",
|
|
264
|
+
source: "prebuilt",
|
|
265
|
+
}
|
|
266
|
+
: await resolveSetupConfig(cwd, opts, existing);
|
|
267
|
+
if (prebuiltDir) {
|
|
268
|
+
emit({ event: "prebuilt", dir: prebuiltDir });
|
|
269
|
+
}
|
|
219
270
|
const persistedCfg = await persistProjectLinking(cwd, {
|
|
220
271
|
project_id: project.id,
|
|
221
272
|
slug: project.slug,
|
|
@@ -252,15 +303,16 @@ export async function deployCmd(opts) {
|
|
|
252
303
|
}
|
|
253
304
|
let upload = null;
|
|
254
305
|
try {
|
|
255
|
-
upload = await packAndUpload(api, cwd, project);
|
|
306
|
+
upload = await packAndUpload(api, cwd, project, prebuiltDir);
|
|
256
307
|
const targeting = deployTargeting(opts);
|
|
257
308
|
const deploy = await api.triggerDeploy(project.id, {
|
|
258
309
|
source_archive_key: upload.archive_key,
|
|
259
310
|
commit_sha: upload.commit_sha,
|
|
260
|
-
commit_message: "CLI deploy",
|
|
311
|
+
commit_message: prebuiltDir ? `CLI prebuilt deploy (${prebuiltDir})` : "CLI deploy",
|
|
261
312
|
framework_hint: setup.framework_hint,
|
|
262
313
|
target: targeting.target,
|
|
263
314
|
branch: targeting.branch,
|
|
315
|
+
prebuilt: prebuiltDir !== null,
|
|
264
316
|
});
|
|
265
317
|
emit({ event: "deploy_started", deploy_id: deploy.id });
|
|
266
318
|
const final = await streamDeployLogs(api, deploy.id);
|
package/dist/detect.js
CHANGED
|
@@ -115,6 +115,23 @@ export async function detectProject(cwd) {
|
|
|
115
115
|
confident: true,
|
|
116
116
|
};
|
|
117
117
|
}
|
|
118
|
+
// VitePress before generic Vite: VitePress repos often pull `vite`
|
|
119
|
+
// transitively, and the SPA Vite path would set output_dir=dist —
|
|
120
|
+
// wrong for VitePress, which writes to `.vitepress/dist/` (or
|
|
121
|
+
// `docs/.vitepress/dist/` when the config lives under docs/).
|
|
122
|
+
if (hasDep(pkg, "vitepress") || (await hasVitepressConfig(cwd))) {
|
|
123
|
+
const docsLayout = await hasVitepressConfig(cwd, "docs/.vitepress");
|
|
124
|
+
const outputDir = docsLayout ? "docs/.vitepress/dist" : ".vitepress/dist";
|
|
125
|
+
const cmd = pkg.scripts?.["docs:build"]
|
|
126
|
+
? "npm run docs:build"
|
|
127
|
+
: buildCmd(pkg, "npx vitepress build");
|
|
128
|
+
return {
|
|
129
|
+
framework_hint: "vitepress",
|
|
130
|
+
build_cmd: cmd,
|
|
131
|
+
output_dir: outputDir,
|
|
132
|
+
confident: true,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
118
135
|
if (hasDep(pkg, "vite") || (await fileExists(cwd, "vite.config.ts", "vite.config.js", "vite.config.mjs"))) {
|
|
119
136
|
return {
|
|
120
137
|
framework_hint: "vite",
|
|
@@ -131,6 +148,35 @@ export async function detectProject(cwd) {
|
|
|
131
148
|
confident: true,
|
|
132
149
|
};
|
|
133
150
|
}
|
|
151
|
+
// Eleventy late in the package.json branch — `@11ty/eleventy` is
|
|
152
|
+
// unique enough not to collide with other frameworks, but Vite /
|
|
153
|
+
// CRA / etc. should win if both are present (a project that pulls
|
|
154
|
+
// both is probably using Vite as the runtime and 11ty just for one
|
|
155
|
+
// build step).
|
|
156
|
+
if (hasDep(pkg, "@11ty/eleventy") ||
|
|
157
|
+
hasDep(pkg, "eleventy") ||
|
|
158
|
+
(await fileExists(cwd, ".eleventy.js", "eleventy.config.js", "eleventy.config.mjs", "eleventy.config.cjs"))) {
|
|
159
|
+
return {
|
|
160
|
+
framework_hint: "eleventy",
|
|
161
|
+
build_cmd: buildCmd(pkg, "npx @11ty/eleventy"),
|
|
162
|
+
output_dir: "_site",
|
|
163
|
+
confident: true,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
// Non-Node SSGs (Hugo today). Recognise repos without package.json
|
|
168
|
+
// before we fall back to "static" with a no-op build command — Hugo
|
|
169
|
+
// needs `hugo --gc --minify` and writes to `public/`, the static
|
|
170
|
+
// fallback would just upload the raw .md sources.
|
|
171
|
+
if ((await fileExists(cwd, "hugo.toml", "hugo.yaml", "hugo.json")) ||
|
|
172
|
+
((await fileExists(cwd, "config.toml", "config.yaml", "config.json")) &&
|
|
173
|
+
(await hasHugoConfigMarker(cwd)))) {
|
|
174
|
+
return {
|
|
175
|
+
framework_hint: "hugo",
|
|
176
|
+
build_cmd: "hugo --gc --minify",
|
|
177
|
+
output_dir: "public",
|
|
178
|
+
confident: true,
|
|
179
|
+
};
|
|
134
180
|
}
|
|
135
181
|
// No (or unrecognised) package.json. If there's HTML on disk we treat
|
|
136
182
|
// the current directory as a static site. Otherwise we still fall back
|
|
@@ -144,3 +190,41 @@ export async function detectProject(cwd) {
|
|
|
144
190
|
confident: _staticHtml,
|
|
145
191
|
};
|
|
146
192
|
}
|
|
193
|
+
const HUGO_CONFIG_TOKENS = [
|
|
194
|
+
"baseURL",
|
|
195
|
+
"baseurl",
|
|
196
|
+
"languageCode",
|
|
197
|
+
"languagecode",
|
|
198
|
+
"[params]",
|
|
199
|
+
"[markup]",
|
|
200
|
+
"[menu",
|
|
201
|
+
"[taxonomies]",
|
|
202
|
+
"hugoVersion",
|
|
203
|
+
"minVersion",
|
|
204
|
+
"theme",
|
|
205
|
+
];
|
|
206
|
+
async function hasVitepressConfig(cwd, prefix = ".vitepress") {
|
|
207
|
+
for (const name of ["config.ts", "config.js", "config.mts", "config.mjs"]) {
|
|
208
|
+
try {
|
|
209
|
+
await fs.access(path.join(cwd, prefix, name));
|
|
210
|
+
return true;
|
|
211
|
+
}
|
|
212
|
+
catch {
|
|
213
|
+
// try next
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
return false;
|
|
217
|
+
}
|
|
218
|
+
async function hasHugoConfigMarker(cwd) {
|
|
219
|
+
for (const fn of ["config.toml", "config.yaml", "config.json"]) {
|
|
220
|
+
try {
|
|
221
|
+
const head = await fs.readFile(path.join(cwd, fn), "utf-8");
|
|
222
|
+
if (HUGO_CONFIG_TOKENS.some((t) => head.includes(t)))
|
|
223
|
+
return true;
|
|
224
|
+
}
|
|
225
|
+
catch {
|
|
226
|
+
// try next
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
return false;
|
|
230
|
+
}
|
package/dist/pack.js
CHANGED
|
@@ -80,6 +80,58 @@ async function sha256OfFile(p) {
|
|
|
80
80
|
}
|
|
81
81
|
return hash.digest("hex");
|
|
82
82
|
}
|
|
83
|
+
/** Pack only the contents of an already-built artifact directory.
|
|
84
|
+
*
|
|
85
|
+
* Used by `layero deploy --prebuilt`. We deliberately bypass DEFAULT_IGNORE
|
|
86
|
+
* (no `dist`/`build`/`.next` filtering — those names might appear *inside*
|
|
87
|
+
* a built artifact and they're now meaningful files, not source-tree
|
|
88
|
+
* leftovers). We still apply a minimal safety filter so accidental clutter
|
|
89
|
+
* doesn't blow up the archive: `.git/`, `node_modules/`, `.DS_Store`,
|
|
90
|
+
* `.env*`. The caller resolves the directory; the archive's internal layout
|
|
91
|
+
* is content-only (no source-tree prefix).
|
|
92
|
+
*/
|
|
93
|
+
export async function packDirectory(targetDir, projectName) {
|
|
94
|
+
const stat = await fs.stat(targetDir);
|
|
95
|
+
if (!stat.isDirectory()) {
|
|
96
|
+
throw new Error(`prebuilt path is not a directory: ${targetDir}`);
|
|
97
|
+
}
|
|
98
|
+
const minimalIgnore = ignore();
|
|
99
|
+
minimalIgnore.add([
|
|
100
|
+
"node_modules",
|
|
101
|
+
".git",
|
|
102
|
+
".svn",
|
|
103
|
+
".hg",
|
|
104
|
+
".env",
|
|
105
|
+
".env.*",
|
|
106
|
+
"*.log",
|
|
107
|
+
".DS_Store",
|
|
108
|
+
"Thumbs.db",
|
|
109
|
+
".layero",
|
|
110
|
+
]);
|
|
111
|
+
const files = await walk(targetDir, minimalIgnore);
|
|
112
|
+
if (files.length === 0) {
|
|
113
|
+
throw new Error(`no files to upload in ${targetDir} — check that the build produced output`);
|
|
114
|
+
}
|
|
115
|
+
const stamp = Date.now();
|
|
116
|
+
const safeName = projectName.replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
117
|
+
const archivePath = path.join(tmpdir(), `layero-${safeName}-prebuilt-${stamp}.tgz`);
|
|
118
|
+
const rootName = `layero-${safeName}-prebuilt-${stamp}`;
|
|
119
|
+
await tarCreate({
|
|
120
|
+
file: archivePath,
|
|
121
|
+
gzip: { level: 6 },
|
|
122
|
+
cwd: targetDir,
|
|
123
|
+
portable: true,
|
|
124
|
+
prefix: rootName,
|
|
125
|
+
}, files);
|
|
126
|
+
const fst = await fs.stat(archivePath);
|
|
127
|
+
if (fst.size > MAX_BYTES) {
|
|
128
|
+
await fs.unlink(archivePath).catch(() => undefined);
|
|
129
|
+
throw new Error(`archive is ${(fst.size / (1024 * 1024)).toFixed(1)}MB — over the 200MB limit. ` +
|
|
130
|
+
"Trim the build output or skip large files (`.layeroignore`).");
|
|
131
|
+
}
|
|
132
|
+
const sha256 = await sha256OfFile(archivePath);
|
|
133
|
+
return { archivePath, sha256, size: fst.size, fileCount: files.length };
|
|
134
|
+
}
|
|
83
135
|
export async function packCwd(cwd, projectName) {
|
|
84
136
|
const ig = await buildIgnore(cwd);
|
|
85
137
|
const files = await walk(cwd, ig);
|