layero 0.6.0 → 0.6.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/README.md +31 -0
- package/dist/api.js +12 -0
- package/dist/bin/layero.js +52 -1
- package/dist/commands/deploy.js +12 -1
- package/dist/commands/hooks.js +80 -0
- package/dist/detect.js +40 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -53,6 +53,7 @@ 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.
|
|
@@ -70,6 +71,12 @@ Run `layero <cmd> --help` for full options.
|
|
|
70
71
|
uses that explicit path. Use this for CI flows that build in the
|
|
71
72
|
pipeline, Webflow / Framer exports, or whenever you don't want the
|
|
72
73
|
platform to run install/build for you.
|
|
74
|
+
- `--root <dir>` — monorepo: tell the builder the app lives in a
|
|
75
|
+
subdirectory of the repo (e.g. `--root apps/web`). Saved on the
|
|
76
|
+
project; future GitHub-push and hook triggers use the same value.
|
|
77
|
+
CLI auto-detect honours it: framework signals are looked up inside
|
|
78
|
+
`<cwd>/<root>` so a `package.json` workspace at the repo root
|
|
79
|
+
doesn't shadow the real app's stack.
|
|
73
80
|
- `--name <name>` — project name (only on first deploy).
|
|
74
81
|
- `--project <id_or_slug>` — deploy into an existing project, ignoring
|
|
75
82
|
`./.layero/project.json` (useful for CI).
|
|
@@ -91,6 +98,7 @@ Run `layero <cmd> --help` for full options.
|
|
|
91
98
|
| `gatsby` dep | gatsby | `npm run build` | `public` |
|
|
92
99
|
| `astro` dep / `astro.config.*` | astro | `npm run build` | `dist` |
|
|
93
100
|
| `@docusaurus/core` dep / `docusaurus.config.*` | docusaurus | `npm run build` | `build` |
|
|
101
|
+
| `@storybook/*` dep / `scripts.build-storybook` / `.storybook/main.*` | storybook | `npm run build-storybook` (or `npx storybook build`) | `storybook-static` |
|
|
94
102
|
| `vitepress` dep / `.vitepress/config.*` / `docs/.vitepress/config.*` | vitepress | `npm run docs:build` (or `npx vitepress build`) | `.vitepress/dist` or `docs/.vitepress/dist` |
|
|
95
103
|
| `vite` dep / `vite.config.*` | vite | `npm run build` | `dist` |
|
|
96
104
|
| `react-scripts` dep | cra | `npm run build` | `build` |
|
|
@@ -98,6 +106,29 @@ Run `layero <cmd> --help` for full options.
|
|
|
98
106
|
| `hugo.{toml,yaml,json}` or `config.*` with Hugo markers (`baseURL`, `[markup]`, …) | hugo | `hugo --gc --minify` (no install needed) | `public` |
|
|
99
107
|
| any `.html` at root, no `package.json` | static | `true` (no-op) | `.` |
|
|
100
108
|
|
|
109
|
+
## Deploy hooks — webhook URLs that trigger builds
|
|
110
|
+
|
|
111
|
+
When something *other than you* should kick a build — a headless CMS
|
|
112
|
+
publishing content, a cron job, an external CI pipeline — create a
|
|
113
|
+
deploy hook. You get back an opaque URL; whoever POSTs to it fires a
|
|
114
|
+
deploy.
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
# Inside a linked project directory:
|
|
118
|
+
layero hooks create strapi-content # preview-target, default branch
|
|
119
|
+
layero hooks create publish --prod # production-target hook
|
|
120
|
+
layero hooks create staging --branch=dev # explicit branch
|
|
121
|
+
layero hooks list
|
|
122
|
+
layero hooks delete <id> # revoke immediately
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
The created URL looks like `https://api.layero.ru/hooks/<token>`. Paste
|
|
126
|
+
it into Strapi / Sanity / Contentful / Decap CMS / GitHub Actions / a
|
|
127
|
+
cron job — any tool that can POST to a URL. Token = credential; rotate
|
|
128
|
+
by `delete` + `create`. There is no per-token rate limit yet; rely on
|
|
129
|
+
the platform's natural in-flight-commit dedup if the same commit gets
|
|
130
|
+
fired more than once.
|
|
131
|
+
|
|
101
132
|
## Bring-your-own-build (`--prebuilt`)
|
|
102
133
|
|
|
103
134
|
If you already build your site yourself — in CI, via a desktop tool like
|
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
|
+
updateProject(projectId, input) {
|
|
72
|
+
return this.request("PATCH", `/projects/${projectId}`, input);
|
|
73
|
+
}
|
|
71
74
|
triggerDeploy(projectId, input) {
|
|
72
75
|
return this.request("POST", `/projects/${projectId}/deploy`, input);
|
|
73
76
|
}
|
|
@@ -81,6 +84,15 @@ export class ApiClient {
|
|
|
81
84
|
rollbackProject(projectId, input) {
|
|
82
85
|
return this.request("POST", `/projects/${projectId}/rollback`, input);
|
|
83
86
|
}
|
|
87
|
+
listDeployHooks(projectId) {
|
|
88
|
+
return this.request("GET", `/projects/${projectId}/deploy-hooks`);
|
|
89
|
+
}
|
|
90
|
+
createDeployHook(projectId, input) {
|
|
91
|
+
return this.request("POST", `/projects/${projectId}/deploy-hooks`, input);
|
|
92
|
+
}
|
|
93
|
+
deleteDeployHook(projectId, hookId) {
|
|
94
|
+
return this.request("DELETE", `/projects/${projectId}/deploy-hooks/${hookId}`);
|
|
95
|
+
}
|
|
84
96
|
startDeviceAuth() {
|
|
85
97
|
return this.request("POST", "/auth/cli/device");
|
|
86
98
|
}
|
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,12 +154,13 @@ 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 | vitepress | next | astro | cra | sveltekit | nuxt | gatsby | docusaurus | eleventy | hugo | static)")
|
|
157
|
+
.option("-t, --type <preset>", "framework override (vite | vitepress | next | astro | cra | sveltekit | nuxt | gatsby | docusaurus | storybook | 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)")
|
|
112
162
|
.option("--prebuilt [dir]", "ship an already-built artifact directory (default auto-pick: dist/build/public/out/_site/...)")
|
|
163
|
+
.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)")
|
|
113
164
|
.option("--prod", "deploy to production (replaces apex_hostname's active deploy). Without this flag, deploys go to the project's CLI preview pseudo-branch.")
|
|
114
165
|
.option("--branch <name>", "deploy to a specific branch's environment. Wins over --prod.")
|
|
115
166
|
.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
|
@@ -26,6 +26,7 @@ const VALID_TYPES = new Set([
|
|
|
26
26
|
"eleventy",
|
|
27
27
|
"11ty",
|
|
28
28
|
"vitepress",
|
|
29
|
+
"storybook",
|
|
29
30
|
]);
|
|
30
31
|
function dashboardOrigin(apiUrl) {
|
|
31
32
|
const override = process.env.LAYERO_DASHBOARD_URL;
|
|
@@ -201,7 +202,11 @@ async function resolvePrebuiltDir(cwd, raw) {
|
|
|
201
202
|
// Resolve the framework / build / output config to send to completeSetup.
|
|
202
203
|
// Precedence: explicit CLI args > .layero/project.json > auto-detect.
|
|
203
204
|
async function resolveSetupConfig(cwd, opts, existing) {
|
|
204
|
-
|
|
205
|
+
// Honour --root when auto-detecting: the framework signals live in the
|
|
206
|
+
// monorepo subdir, not the repo root. Without this the detector sees
|
|
207
|
+
// a bare workspace package.json and falls through to "static".
|
|
208
|
+
const detectCwd = opts.root ? path.join(cwd, opts.root) : cwd;
|
|
209
|
+
const detected = await detectProject(detectCwd);
|
|
205
210
|
emit({
|
|
206
211
|
event: "detected",
|
|
207
212
|
framework: detected.framework_hint,
|
|
@@ -298,9 +303,15 @@ export async function deployCmd(opts) {
|
|
|
298
303
|
output_dir: setup.output_dir,
|
|
299
304
|
analytics_enabled: persistedCfg.analytics_enabled ?? false,
|
|
300
305
|
env_vars: persistedCfg.env_vars ?? {},
|
|
306
|
+
root_directory: opts.root ?? null,
|
|
301
307
|
});
|
|
302
308
|
emit({ event: "setup_applied" });
|
|
303
309
|
}
|
|
310
|
+
else if (opts.root !== undefined) {
|
|
311
|
+
// Active project: --root patches the existing row so the *next*
|
|
312
|
+
// GitHub-pushed or hook-triggered build uses the new subdir.
|
|
313
|
+
project = await api.updateProject(project.id, { root_directory: opts.root });
|
|
314
|
+
}
|
|
304
315
|
let upload = null;
|
|
305
316
|
try {
|
|
306
317
|
upload = await packAndUpload(api, cwd, project, prebuiltDir);
|
|
@@ -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,46 @@ export async function detectProject(cwd) {
|
|
|
115
115
|
confident: true,
|
|
116
116
|
};
|
|
117
117
|
}
|
|
118
|
+
// Storybook before Vite/CRA: Storybook 7+ uses Vite or webpack
|
|
119
|
+
// internally, so `vite` / `react-scripts` is in deps. Without
|
|
120
|
+
// listing it first the SPA Vite path would output_dir=dist, which
|
|
121
|
+
// doesn't exist after `build-storybook`.
|
|
122
|
+
const storybookDeps = [
|
|
123
|
+
"@storybook/cli",
|
|
124
|
+
"@storybook/react",
|
|
125
|
+
"@storybook/react-vite",
|
|
126
|
+
"@storybook/react-webpack5",
|
|
127
|
+
"@storybook/vue",
|
|
128
|
+
"@storybook/vue3",
|
|
129
|
+
"@storybook/vue3-vite",
|
|
130
|
+
"@storybook/svelte",
|
|
131
|
+
"@storybook/svelte-vite",
|
|
132
|
+
"@storybook/web-components",
|
|
133
|
+
"@storybook/web-components-vite",
|
|
134
|
+
"@storybook/preact",
|
|
135
|
+
"@storybook/angular",
|
|
136
|
+
"@storybook/nextjs",
|
|
137
|
+
"@storybook/html",
|
|
138
|
+
"@storybook/html-vite",
|
|
139
|
+
"storybook",
|
|
140
|
+
];
|
|
141
|
+
const hasStorybookDep = storybookDeps.some((d) => hasDep(pkg, d));
|
|
142
|
+
const hasStorybookScript = pkg.scripts?.["build-storybook"] !== undefined
|
|
143
|
+
|| Object.values(pkg.scripts ?? {}).some((s) => typeof s === "string" && s.includes("storybook build"));
|
|
144
|
+
const hasStorybookDir = await fileExists(cwd, ".storybook/main.js", ".storybook/main.ts", ".storybook/main.cjs", ".storybook/main.mjs");
|
|
145
|
+
if (hasStorybookDep || hasStorybookScript || hasStorybookDir) {
|
|
146
|
+
const cmd = pkg.scripts?.["build-storybook"]
|
|
147
|
+
? "npm run build-storybook"
|
|
148
|
+
: (pkg.scripts?.build && pkg.scripts.build.includes("storybook"))
|
|
149
|
+
? "npm run build"
|
|
150
|
+
: "npx storybook build";
|
|
151
|
+
return {
|
|
152
|
+
framework_hint: "storybook",
|
|
153
|
+
build_cmd: cmd,
|
|
154
|
+
output_dir: "storybook-static",
|
|
155
|
+
confident: true,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
118
158
|
// VitePress before generic Vite: VitePress repos often pull `vite`
|
|
119
159
|
// transitively, and the SPA Vite path would set output_dir=dist —
|
|
120
160
|
// wrong for VitePress, which writes to `.vitepress/dist/` (or
|