layero 0.6.0 → 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 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.
@@ -98,6 +99,29 @@ Run `layero <cmd> --help` for full options.
98
99
  | `hugo.{toml,yaml,json}` or `config.*` with Hugo markers (`baseURL`, `[markup]`, …) | hugo | `hugo --gc --minify` (no install needed) | `public` |
99
100
  | any `.html` at root, no `package.json` | static | `true` (no-op) | `.` |
100
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
+
101
125
  ## Bring-your-own-build (`--prebuilt`)
102
126
 
103
127
  If you already build your site yourself — in CI, via a desktop tool like
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
  }
@@ -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.")
@@ -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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "layero",
3
- "version": "0.6.0",
3
+ "version": "0.6.1",
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",