layero 0.5.4 → 0.6.1
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 +58 -2
- package/dist/agent.js +3 -0
- package/dist/api.js +9 -0
- package/dist/bin/layero.js +52 -1
- package/dist/commands/deploy.js +58 -6
- package/dist/commands/hooks.js +80 -0
- package/dist/detect.js +84 -0
- package/dist/pack.js +52 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -53,14 +53,24 @@ re-run `layero deploy`, get a new preview URL each time.
|
|
|
53
53
|
| `layero deploy` | Auto-detect framework, pack cwd, build, ship. |
|
|
54
54
|
| `layero deploys list` | List recent deploys. |
|
|
55
55
|
| `layero rollback` | Re-activate the previous successful deploy. |
|
|
56
|
+
| `layero hooks list/create/delete` | Manage deploy hooks (URL tokens that trigger builds from CMS / cron / external CI). |
|
|
56
57
|
| `layero token` | Manage the auth token directly. |
|
|
57
58
|
|
|
58
59
|
Run `layero <cmd> --help` for full options.
|
|
59
60
|
|
|
60
61
|
## `layero deploy` flags
|
|
61
62
|
|
|
62
|
-
- `--type <preset>` — framework override: `vite`, `
|
|
63
|
-
`sveltekit`, `nuxt`, `gatsby`, `docusaurus`, `
|
|
63
|
+
- `--type <preset>` — framework override: `vite`, `vitepress`, `next`,
|
|
64
|
+
`astro`, `cra`, `sveltekit`, `nuxt`, `gatsby`, `docusaurus`, `eleventy`
|
|
65
|
+
(alias `11ty`), `hugo`, `static`. **Optional** — auto-detected from
|
|
66
|
+
`package.json` and config files when omitted.
|
|
67
|
+
- `--prebuilt [dir]` — ship an already-built artifact instead of building
|
|
68
|
+
remotely. Without an argument, picks the first existing of
|
|
69
|
+
`dist/`, `build/`, `public/`, `out/`, `_site/`, `.output/public/`,
|
|
70
|
+
`docs/.vitepress/dist/`, `.vitepress/dist/`. With `--prebuilt ./my-out`
|
|
71
|
+
uses that explicit path. Use this for CI flows that build in the
|
|
72
|
+
pipeline, Webflow / Framer exports, or whenever you don't want the
|
|
73
|
+
platform to run install/build for you.
|
|
64
74
|
- `--name <name>` — project name (only on first deploy).
|
|
65
75
|
- `--project <id_or_slug>` — deploy into an existing project, ignoring
|
|
66
76
|
`./.layero/project.json` (useful for CI).
|
|
@@ -82,10 +92,56 @@ Run `layero <cmd> --help` for full options.
|
|
|
82
92
|
| `gatsby` dep | gatsby | `npm run build` | `public` |
|
|
83
93
|
| `astro` dep / `astro.config.*` | astro | `npm run build` | `dist` |
|
|
84
94
|
| `@docusaurus/core` dep / `docusaurus.config.*` | docusaurus | `npm run build` | `build` |
|
|
95
|
+
| `vitepress` dep / `.vitepress/config.*` / `docs/.vitepress/config.*` | vitepress | `npm run docs:build` (or `npx vitepress build`) | `.vitepress/dist` or `docs/.vitepress/dist` |
|
|
85
96
|
| `vite` dep / `vite.config.*` | vite | `npm run build` | `dist` |
|
|
86
97
|
| `react-scripts` dep | cra | `npm run build` | `build` |
|
|
98
|
+
| `@11ty/eleventy` dep / `.eleventy.js` / `eleventy.config.*` | eleventy | `npm run build` (or `npx @11ty/eleventy`) | `_site` |
|
|
99
|
+
| `hugo.{toml,yaml,json}` or `config.*` with Hugo markers (`baseURL`, `[markup]`, …) | hugo | `hugo --gc --minify` (no install needed) | `public` |
|
|
87
100
|
| any `.html` at root, no `package.json` | static | `true` (no-op) | `.` |
|
|
88
101
|
|
|
102
|
+
## Deploy hooks — webhook URLs that trigger builds
|
|
103
|
+
|
|
104
|
+
When something *other than you* should kick a build — a headless CMS
|
|
105
|
+
publishing content, a cron job, an external CI pipeline — create a
|
|
106
|
+
deploy hook. You get back an opaque URL; whoever POSTs to it fires a
|
|
107
|
+
deploy.
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
# Inside a linked project directory:
|
|
111
|
+
layero hooks create strapi-content # preview-target, default branch
|
|
112
|
+
layero hooks create publish --prod # production-target hook
|
|
113
|
+
layero hooks create staging --branch=dev # explicit branch
|
|
114
|
+
layero hooks list
|
|
115
|
+
layero hooks delete <id> # revoke immediately
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
The created URL looks like `https://api.layero.ru/hooks/<token>`. Paste
|
|
119
|
+
it into Strapi / Sanity / Contentful / Decap CMS / GitHub Actions / a
|
|
120
|
+
cron job — any tool that can POST to a URL. Token = credential; rotate
|
|
121
|
+
by `delete` + `create`. There is no per-token rate limit yet; rely on
|
|
122
|
+
the platform's natural in-flight-commit dedup if the same commit gets
|
|
123
|
+
fired more than once.
|
|
124
|
+
|
|
125
|
+
## Bring-your-own-build (`--prebuilt`)
|
|
126
|
+
|
|
127
|
+
If you already build your site yourself — in CI, via a desktop tool like
|
|
128
|
+
Webflow/Framer, or because you want a guaranteed deterministic artifact —
|
|
129
|
+
skip the platform's install/build entirely:
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
# Auto-pick the output directory:
|
|
133
|
+
layero deploy --prebuilt
|
|
134
|
+
|
|
135
|
+
# Or point at a specific one:
|
|
136
|
+
layero deploy --prebuilt ./dist
|
|
137
|
+
layero deploy --prebuilt ./build/static
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
What changes: only the files inside the directory you point at are
|
|
141
|
+
uploaded (no source-tree filters like `.gitignore` apply). The platform
|
|
142
|
+
ships them verbatim — no detect, no install, no build. Smaller archive,
|
|
143
|
+
faster deploys, no surprises from the platform's package-manager defaults.
|
|
144
|
+
|
|
89
145
|
Override anything by editing `.layero/project.json` after the first `layero init`.
|
|
90
146
|
|
|
91
147
|
## 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/api.js
CHANGED
|
@@ -81,6 +81,15 @@ export class ApiClient {
|
|
|
81
81
|
rollbackProject(projectId, input) {
|
|
82
82
|
return this.request("POST", `/projects/${projectId}/rollback`, input);
|
|
83
83
|
}
|
|
84
|
+
listDeployHooks(projectId) {
|
|
85
|
+
return this.request("GET", `/projects/${projectId}/deploy-hooks`);
|
|
86
|
+
}
|
|
87
|
+
createDeployHook(projectId, input) {
|
|
88
|
+
return this.request("POST", `/projects/${projectId}/deploy-hooks`, input);
|
|
89
|
+
}
|
|
90
|
+
deleteDeployHook(projectId, hookId) {
|
|
91
|
+
return this.request("DELETE", `/projects/${projectId}/deploy-hooks/${hookId}`);
|
|
92
|
+
}
|
|
84
93
|
startDeviceAuth() {
|
|
85
94
|
return this.request("POST", "/auth/cli/device");
|
|
86
95
|
}
|
package/dist/bin/layero.js
CHANGED
|
@@ -11,6 +11,7 @@ import { linkCmd } from "../commands/link.js";
|
|
|
11
11
|
import { tokenSetCmd } from "../commands/token.js";
|
|
12
12
|
import { deployCmd } from "../commands/deploy.js";
|
|
13
13
|
import { deploysListCmd, rollbackCmd } from "../commands/deploys.js";
|
|
14
|
+
import { hooksCreateCmd, hooksDeleteCmd, hooksListCmd } from "../commands/hooks.js";
|
|
14
15
|
import { loginCmd } from "../commands/login.js";
|
|
15
16
|
import { orgsListCmd } from "../commands/orgs.js";
|
|
16
17
|
import { initCmd } from "../commands/init.js";
|
|
@@ -74,6 +75,55 @@ async function main() {
|
|
|
74
75
|
.action(async (opts) => {
|
|
75
76
|
await deploysListCmd(opts);
|
|
76
77
|
});
|
|
78
|
+
const hooks = program
|
|
79
|
+
.command("hooks")
|
|
80
|
+
.description("Manage deploy hooks — URL tokens that trigger builds from CMS / cron / external CI.");
|
|
81
|
+
hooks
|
|
82
|
+
.command("list")
|
|
83
|
+
.description("List deploy hooks for the linked project.")
|
|
84
|
+
.option("--project <id>", "target project id (default: linked .layero/project.json)")
|
|
85
|
+
.action(async (opts) => {
|
|
86
|
+
try {
|
|
87
|
+
await hooksListCmd(opts);
|
|
88
|
+
}
|
|
89
|
+
catch (err) {
|
|
90
|
+
console.error(String(err?.message ?? err));
|
|
91
|
+
process.exitCode = 1;
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
hooks
|
|
95
|
+
.command("create <name>")
|
|
96
|
+
.description("Create a new deploy hook. Prints a URL — paste it into Strapi / Sanity / "
|
|
97
|
+
+ "Contentful / GitHub Actions / cron as a POST webhook.")
|
|
98
|
+
.option("--project <id>", "target project id (default: linked .layero/project.json)")
|
|
99
|
+
.option("--branch <name>", "branch to deploy when fired (default: project default_branch, evaluated at fire time)")
|
|
100
|
+
.option("--prod", "fire the hook against the production environment (default: preview)")
|
|
101
|
+
.addHelpText("after", "\nExamples:\n"
|
|
102
|
+
+ " $ layero hooks create strapi-content # preview-target hook for default branch\n"
|
|
103
|
+
+ " $ layero hooks create publish --prod # production-target hook for default branch\n"
|
|
104
|
+
+ " $ layero hooks create staging --branch=dev # any branch, preview environment")
|
|
105
|
+
.action(async (name, opts) => {
|
|
106
|
+
try {
|
|
107
|
+
await hooksCreateCmd(name, opts);
|
|
108
|
+
}
|
|
109
|
+
catch (err) {
|
|
110
|
+
console.error(String(err?.message ?? err));
|
|
111
|
+
process.exitCode = 1;
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
hooks
|
|
115
|
+
.command("delete <id>")
|
|
116
|
+
.description("Revoke a deploy hook. The URL stops working immediately.")
|
|
117
|
+
.option("--project <id>", "target project id (default: linked .layero/project.json)")
|
|
118
|
+
.action(async (id, opts) => {
|
|
119
|
+
try {
|
|
120
|
+
await hooksDeleteCmd(id, opts);
|
|
121
|
+
}
|
|
122
|
+
catch (err) {
|
|
123
|
+
console.error(String(err?.message ?? err));
|
|
124
|
+
process.exitCode = 1;
|
|
125
|
+
}
|
|
126
|
+
});
|
|
77
127
|
program
|
|
78
128
|
.command("rollback")
|
|
79
129
|
.description("Re-activate the previous successful deploy on the project's default branch.")
|
|
@@ -104,11 +154,12 @@ async function main() {
|
|
|
104
154
|
program
|
|
105
155
|
.command("deploy")
|
|
106
156
|
.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)")
|
|
157
|
+
.option("-t, --type <preset>", "framework override (vite | vitepress | next | astro | cra | sveltekit | nuxt | gatsby | docusaurus | eleventy | hugo | static)")
|
|
108
158
|
.option("--name <name>", "project name (only used on first deploy)")
|
|
109
159
|
.option("--project <id_or_slug>", "deploy into an existing project, ignoring local config")
|
|
110
160
|
.option("-y, --yes", "non-interactive: accept defaults and skip --prod confirmation")
|
|
111
161
|
.option("--config", "(legacy alias of the default behaviour — auto-detect + .layero/project.json values)")
|
|
162
|
+
.option("--prebuilt [dir]", "ship an already-built artifact directory (default auto-pick: dist/build/public/out/_site/...)")
|
|
112
163
|
.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
164
|
.option("--branch <name>", "deploy to a specific branch's environment. Wins over --prod.")
|
|
114
165
|
.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);
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import { ApiClient, ApiError } from "../api.js";
|
|
3
|
+
import { loadConfig } from "../config.js";
|
|
4
|
+
import { loadProjectConfig } from "../project-config.js";
|
|
5
|
+
async function resolveProjectId(opts) {
|
|
6
|
+
if (opts.project) {
|
|
7
|
+
// Accepts an id directly; UUID format check is server-side.
|
|
8
|
+
return opts.project;
|
|
9
|
+
}
|
|
10
|
+
const linked = await loadProjectConfig(process.cwd());
|
|
11
|
+
if (linked?.project_id) {
|
|
12
|
+
return linked.project_id;
|
|
13
|
+
}
|
|
14
|
+
throw new Error("no project linked in cwd — pass --project <id> or run `layero deploy` "
|
|
15
|
+
+ "from a project directory once to link it.");
|
|
16
|
+
}
|
|
17
|
+
async function makeClient() {
|
|
18
|
+
const cfg = await loadConfig();
|
|
19
|
+
if (!cfg.token) {
|
|
20
|
+
throw new Error("not logged in. run `layero login` first.");
|
|
21
|
+
}
|
|
22
|
+
return new ApiClient(cfg);
|
|
23
|
+
}
|
|
24
|
+
export async function hooksListCmd(opts) {
|
|
25
|
+
const api = await makeClient();
|
|
26
|
+
const projectId = await resolveProjectId(opts);
|
|
27
|
+
const hooks = await api.listDeployHooks(projectId);
|
|
28
|
+
if (hooks.length === 0) {
|
|
29
|
+
console.log("no deploy hooks yet — `layero hooks create <name>` to add one.");
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
for (const h of hooks) {
|
|
33
|
+
const targetTag = h.target === "production" ? chalk.red("[prod]") : chalk.cyan("[preview]");
|
|
34
|
+
const branchTag = h.branch ? chalk.dim(`branch=${h.branch}`) : chalk.dim("branch=default");
|
|
35
|
+
const last = h.last_triggered_at
|
|
36
|
+
? `fired ${new Date(h.last_triggered_at).toISOString()}`
|
|
37
|
+
: "never fired";
|
|
38
|
+
console.log(`${targetTag} ${chalk.bold(h.name)} ${branchTag} ${chalk.dim("(" + last + ")")}`);
|
|
39
|
+
console.log(` ${chalk.dim(h.id)}`);
|
|
40
|
+
console.log(` ${h.url}`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
export async function hooksCreateCmd(name, opts) {
|
|
44
|
+
if (!name || !name.trim()) {
|
|
45
|
+
throw new Error("name is required: `layero hooks create <name>`");
|
|
46
|
+
}
|
|
47
|
+
const api = await makeClient();
|
|
48
|
+
const projectId = await resolveProjectId(opts);
|
|
49
|
+
const hook = await api.createDeployHook(projectId, {
|
|
50
|
+
name: name.trim(),
|
|
51
|
+
branch: opts.branch ?? null,
|
|
52
|
+
target: opts.prod ? "production" : "preview",
|
|
53
|
+
});
|
|
54
|
+
const targetTag = hook.target === "production" ? chalk.red("[prod]") : chalk.cyan("[preview]");
|
|
55
|
+
console.log(`${chalk.green("✓")} created ${targetTag} ${chalk.bold(hook.name)}`);
|
|
56
|
+
console.log(` ${chalk.dim(hook.id)}`);
|
|
57
|
+
console.log(` ${chalk.bold(hook.url)}`);
|
|
58
|
+
console.log(chalk.dim("\n Paste this URL into your CMS / cron / external CI as a POST webhook. "
|
|
59
|
+
+ "Anyone with the URL can fire a build — treat it like a secret. "
|
|
60
|
+
+ "Rotate by deleting and creating a new one."));
|
|
61
|
+
}
|
|
62
|
+
export async function hooksDeleteCmd(hookId, opts) {
|
|
63
|
+
if (!hookId) {
|
|
64
|
+
throw new Error("hook id is required: `layero hooks delete <id>`");
|
|
65
|
+
}
|
|
66
|
+
const api = await makeClient();
|
|
67
|
+
const projectId = await resolveProjectId(opts);
|
|
68
|
+
try {
|
|
69
|
+
await api.deleteDeployHook(projectId, hookId);
|
|
70
|
+
}
|
|
71
|
+
catch (err) {
|
|
72
|
+
if (err instanceof ApiError && err.status === 404) {
|
|
73
|
+
console.error(chalk.yellow(`no hook ${hookId} on this project (already deleted?)`));
|
|
74
|
+
process.exitCode = 1;
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
throw err;
|
|
78
|
+
}
|
|
79
|
+
console.log(`${chalk.green("✓")} hook ${chalk.dim(hookId)} revoked`);
|
|
80
|
+
}
|
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);
|