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 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`, `next`, `astro`, `cra`,
63
- `sveltekit`, `nuxt`, `gatsby`, `docusaurus`, `static`. **Optional** — auto-detected from `package.json` and config files when omitted.
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
  }
@@ -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.")
@@ -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 = await packCwd(cwd, project.slug);
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 = await resolveSetupConfig(cwd, opts, existing);
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);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "layero",
3
- "version": "0.5.4",
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",