layero 0.1.6 → 0.1.8
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/dist/api.js +6 -0
- package/dist/bin/layero.js +6 -1
- package/dist/commands/deploy.js +135 -40
- package/dist/commands/link.js +7 -4
- package/dist/project-config.js +46 -0
- package/package.json +1 -1
- package/scripts/postinstall.cjs +32 -17
package/dist/api.js
CHANGED
|
@@ -58,6 +58,12 @@ export class ApiClient {
|
|
|
58
58
|
initUpload(projectId) {
|
|
59
59
|
return this.request("POST", `/projects/${projectId}/uploads`);
|
|
60
60
|
}
|
|
61
|
+
finalizeUpload(projectId, input) {
|
|
62
|
+
return this.request("POST", `/projects/${projectId}/uploads/finalize`, input);
|
|
63
|
+
}
|
|
64
|
+
completeSetup(projectId, input) {
|
|
65
|
+
return this.request("POST", `/projects/${projectId}/setup`, input);
|
|
66
|
+
}
|
|
61
67
|
triggerDeploy(projectId, input) {
|
|
62
68
|
return this.request("POST", `/projects/${projectId}/deploy`, input);
|
|
63
69
|
}
|
package/dist/bin/layero.js
CHANGED
|
@@ -63,7 +63,12 @@ async function main() {
|
|
|
63
63
|
.option("--name <name>", "project name (only used on first deploy)")
|
|
64
64
|
.option("--project <id_or_slug>", "deploy into an existing project, ignoring local config")
|
|
65
65
|
.option("-y, --yes", "non-interactive: accept defaults, fail if anything is missing")
|
|
66
|
-
.
|
|
66
|
+
.option("--config", "use framework/build settings + env vars from .layero/project.json (skips the browser setup wizard)")
|
|
67
|
+
.addHelpText("after", "\nExamples:\n" +
|
|
68
|
+
" $ layero deploy # uploads source, opens setup wizard in the browser\n" +
|
|
69
|
+
" $ layero deploy --config # uses .layero/project.json end-to-end (CI-friendly)\n" +
|
|
70
|
+
" $ layero deploy --type vite\n" +
|
|
71
|
+
" $ layero deploy --project my-site --yes")
|
|
67
72
|
.action(async (opts) => {
|
|
68
73
|
await deployCmd(opts);
|
|
69
74
|
});
|
package/dist/commands/deploy.js
CHANGED
|
@@ -4,7 +4,7 @@ import readline from "node:readline/promises";
|
|
|
4
4
|
import chalk from "chalk";
|
|
5
5
|
import { ApiClient, ApiError, uploadArchive } from "../api.js";
|
|
6
6
|
import { loadConfig } from "../config.js";
|
|
7
|
-
import { loadProjectConfig,
|
|
7
|
+
import { loadProjectConfig, persistProjectLinking, projectConfigPath, } from "../project-config.js";
|
|
8
8
|
import { packCwd } from "../pack.js";
|
|
9
9
|
import { streamDeployLogs } from "../logs.js";
|
|
10
10
|
const VALID_TYPES = new Set([
|
|
@@ -18,23 +18,27 @@ const VALID_TYPES = new Set([
|
|
|
18
18
|
"nuxt",
|
|
19
19
|
"gatsby",
|
|
20
20
|
"static",
|
|
21
|
+
"generic",
|
|
21
22
|
]);
|
|
22
|
-
function
|
|
23
|
-
// api.layero.ru → app.layero.ru. Falls back to app.layero.ru for any
|
|
24
|
-
// unrecognized api base (incl. localhost) — the user can override via
|
|
25
|
-
// LAYERO_DASHBOARD_URL if they're pointed at a non-standard env.
|
|
23
|
+
function dashboardOrigin(apiUrl) {
|
|
26
24
|
const override = process.env.LAYERO_DASHBOARD_URL;
|
|
27
25
|
if (override)
|
|
28
|
-
return
|
|
26
|
+
return override.replace(/\/+$/, "");
|
|
29
27
|
try {
|
|
30
28
|
const u = new URL(apiUrl);
|
|
31
29
|
u.hostname = u.hostname.replace(/^api\./, "app.");
|
|
32
|
-
return
|
|
30
|
+
return u.origin;
|
|
33
31
|
}
|
|
34
32
|
catch {
|
|
35
|
-
return
|
|
33
|
+
return "https://app.layero.ru";
|
|
36
34
|
}
|
|
37
35
|
}
|
|
36
|
+
function setupUrl(apiUrl, projectId) {
|
|
37
|
+
return `${dashboardOrigin(apiUrl)}/projects/${projectId}/setup`;
|
|
38
|
+
}
|
|
39
|
+
function projectUrl(apiUrl, projectId) {
|
|
40
|
+
return `${dashboardOrigin(apiUrl)}/projects/${projectId}`;
|
|
41
|
+
}
|
|
38
42
|
async function prompt(question, fallback) {
|
|
39
43
|
const rl = readline.createInterface({
|
|
40
44
|
input: process.stdin,
|
|
@@ -48,7 +52,7 @@ async function prompt(question, fallback) {
|
|
|
48
52
|
rl.close();
|
|
49
53
|
}
|
|
50
54
|
}
|
|
51
|
-
async function
|
|
55
|
+
async function resolveOrCreateProject(api, cwd, opts, existing) {
|
|
52
56
|
if (opts.project) {
|
|
53
57
|
const all = await api.listProjects();
|
|
54
58
|
const match = all.find((p) => p.id === opts.project) ??
|
|
@@ -58,7 +62,6 @@ async function resolveProject(api, cwd, opts) {
|
|
|
58
62
|
}
|
|
59
63
|
return { project: match, createdNow: false };
|
|
60
64
|
}
|
|
61
|
-
const existing = await loadProjectConfig(cwd);
|
|
62
65
|
if (existing) {
|
|
63
66
|
try {
|
|
64
67
|
const project = await api.getProject(existing.project_id);
|
|
@@ -73,61 +76,152 @@ async function resolveProject(api, cwd, opts) {
|
|
|
73
76
|
throw err;
|
|
74
77
|
}
|
|
75
78
|
}
|
|
76
|
-
// No project_id locally — create one.
|
|
77
79
|
const me = await api.me();
|
|
78
80
|
if (!me.handle) {
|
|
79
81
|
throw new Error("no handle set on your account. open https://app.layero.ru/onboarding " +
|
|
80
82
|
"and pick one, then re-run.");
|
|
81
83
|
}
|
|
82
84
|
const fallbackName = path.basename(cwd);
|
|
83
|
-
const name = opts.name ?? (opts.yes ? fallbackName : await prompt("project name", fallbackName));
|
|
85
|
+
const name = opts.name ?? (opts.yes || opts.config ? fallbackName : await prompt("project name", fallbackName));
|
|
84
86
|
const project = await api.createCliProject({
|
|
85
87
|
name,
|
|
86
88
|
framework_hint: opts.type,
|
|
87
89
|
});
|
|
88
|
-
await saveProjectConfig(cwd, {
|
|
89
|
-
project_id: project.id,
|
|
90
|
-
slug: project.slug,
|
|
91
|
-
owner_slug: project.owner.slug,
|
|
92
|
-
apex_hostname: project.apex_hostname,
|
|
93
|
-
framework_hint: opts.type ?? null,
|
|
94
|
-
});
|
|
95
90
|
return { project, createdNow: true };
|
|
96
91
|
}
|
|
92
|
+
async function packAndUpload(api, cwd, project) {
|
|
93
|
+
console.log(chalk.cyan("packing source..."));
|
|
94
|
+
const pack = await packCwd(cwd, project.slug);
|
|
95
|
+
console.log(chalk.dim(` ${pack.fileCount} files, ${(pack.size / (1024 * 1024)).toFixed(2)} MB, sha256=${pack.sha256.slice(0, 12)}`));
|
|
96
|
+
console.log(chalk.cyan("requesting upload URL..."));
|
|
97
|
+
const init = await api.initUpload(project.id);
|
|
98
|
+
console.log(chalk.cyan("uploading archive..."));
|
|
99
|
+
await uploadArchive(init, pack.archivePath);
|
|
100
|
+
return {
|
|
101
|
+
archive_key: init.source_archive_key,
|
|
102
|
+
commit_sha: pack.sha256,
|
|
103
|
+
archivePath: pack.archivePath,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
function ensureConfigComplete(cfg) {
|
|
107
|
+
if (!cfg) {
|
|
108
|
+
throw new Error("no .layero/project.json found. run `layero deploy` once without --config " +
|
|
109
|
+
"to create the project, fill in framework_hint/build_cmd/output_dir there, " +
|
|
110
|
+
"then re-run with --config.");
|
|
111
|
+
}
|
|
112
|
+
const missing = [];
|
|
113
|
+
if (!cfg.framework_hint)
|
|
114
|
+
missing.push("framework_hint");
|
|
115
|
+
if (!cfg.build_cmd)
|
|
116
|
+
missing.push("build_cmd");
|
|
117
|
+
if (!cfg.output_dir)
|
|
118
|
+
missing.push("output_dir");
|
|
119
|
+
if (missing.length > 0) {
|
|
120
|
+
throw new Error(`.layero/project.json is missing required fields for --config: ${missing.join(", ")}`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
97
123
|
export async function deployCmd(opts) {
|
|
98
124
|
if (opts.type && !VALID_TYPES.has(opts.type.toLowerCase())) {
|
|
99
125
|
throw new Error(`unknown --type "${opts.type}". valid: ${[...VALID_TYPES].join(", ")}`);
|
|
100
126
|
}
|
|
101
|
-
const
|
|
102
|
-
if (!
|
|
127
|
+
const cliCfg = await loadConfig();
|
|
128
|
+
if (!cliCfg.token) {
|
|
103
129
|
throw new Error("not logged in. run `layero login` first.");
|
|
104
130
|
}
|
|
105
|
-
const api = new ApiClient(
|
|
131
|
+
const api = new ApiClient(cliCfg);
|
|
106
132
|
const cwd = process.cwd();
|
|
107
|
-
const
|
|
133
|
+
const existing = await loadProjectConfig(cwd);
|
|
134
|
+
const { project: created, createdNow } = await resolveOrCreateProject(api, cwd, opts, existing);
|
|
135
|
+
let project = created;
|
|
108
136
|
if (project.source_type !== "cli") {
|
|
109
137
|
throw new Error(`project "${project.slug}" is a GitHub-source project; ` +
|
|
110
138
|
"use the dashboard's Deploy button or push to the linked repo.");
|
|
111
139
|
}
|
|
112
|
-
//
|
|
113
|
-
//
|
|
114
|
-
//
|
|
115
|
-
|
|
140
|
+
// Persist linking metadata only — never touch hand-edited config fields
|
|
141
|
+
// or unknown keys the user may have added (a --config file is the user's
|
|
142
|
+
// source of truth, not ours). `persistProjectLinking` reads the file as
|
|
143
|
+
// raw JSON, overlays just project_id/slug/owner_slug/apex_hostname, and
|
|
144
|
+
// writes it back.
|
|
145
|
+
const persistedCfg = await persistProjectLinking(cwd, {
|
|
146
|
+
project_id: project.id,
|
|
147
|
+
slug: project.slug,
|
|
148
|
+
owner_slug: project.owner.slug,
|
|
149
|
+
apex_hostname: project.apex_hostname,
|
|
150
|
+
}, opts.type ?? null);
|
|
116
151
|
if (createdNow) {
|
|
117
|
-
console.log(chalk.green(`created project ${project.slug}
|
|
152
|
+
console.log(chalk.green(`created project ${project.slug}`));
|
|
118
153
|
}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
154
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
155
|
+
// Path A: --config — fully scripted, runs the setup endpoint with
|
|
156
|
+
// fields from .layero/project.json, then deploys.
|
|
157
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
158
|
+
if (opts.config) {
|
|
159
|
+
ensureConfigComplete(persistedCfg);
|
|
160
|
+
if (project.status === "pending_setup") {
|
|
161
|
+
console.log(chalk.cyan("applying config..."));
|
|
162
|
+
project = await api.completeSetup(project.id, {
|
|
163
|
+
framework_hint: persistedCfg.framework_hint,
|
|
164
|
+
build_cmd: persistedCfg.build_cmd,
|
|
165
|
+
output_dir: persistedCfg.output_dir,
|
|
166
|
+
analytics_enabled: persistedCfg.analytics_enabled ?? false,
|
|
167
|
+
env_vars: persistedCfg.env_vars ?? {},
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
let upload = null;
|
|
171
|
+
try {
|
|
172
|
+
upload = await packAndUpload(api, cwd, project);
|
|
173
|
+
console.log(chalk.cyan("triggering deploy..."));
|
|
174
|
+
const deploy = await api.triggerDeploy(project.id, {
|
|
175
|
+
source_archive_key: upload.archive_key,
|
|
176
|
+
commit_sha: upload.commit_sha,
|
|
177
|
+
commit_message: "CLI deploy",
|
|
178
|
+
framework_hint: persistedCfg.framework_hint ?? undefined,
|
|
179
|
+
});
|
|
180
|
+
console.log(chalk.dim(` deploy_id=${deploy.id}`));
|
|
181
|
+
const final = await streamDeployLogs(api, deploy.id);
|
|
182
|
+
if (final.status !== "ready") {
|
|
183
|
+
console.error(chalk.red(`deploy failed (${final.status})${final.error_message ? `: ${final.error_message}` : ""}`));
|
|
184
|
+
process.exitCode = 1;
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
console.log(chalk.green(`deploy ready → ${projectUrl(cliCfg.apiUrl, project.id)}`));
|
|
188
|
+
console.log(chalk.dim(` site: https://${project.apex_hostname} (CDN may take ~30-60s to propagate)`));
|
|
189
|
+
}
|
|
190
|
+
finally {
|
|
191
|
+
if (upload) {
|
|
192
|
+
await fs.unlink(upload.archivePath).catch(() => undefined);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
198
|
+
// Path B: no flag — upload the source, stash it on the project row,
|
|
199
|
+
// print the setup URL. The user finishes the flow
|
|
200
|
+
// in the browser, exactly like a GitHub import.
|
|
201
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
202
|
+
let upload = null;
|
|
122
203
|
try {
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
204
|
+
upload = await packAndUpload(api, cwd, project);
|
|
205
|
+
await api.finalizeUpload(project.id, {
|
|
206
|
+
source_archive_key: upload.archive_key,
|
|
207
|
+
commit_sha: upload.commit_sha,
|
|
208
|
+
});
|
|
209
|
+
if (project.status === "pending_setup") {
|
|
210
|
+
const url = setupUrl(cliCfg.apiUrl, project.id);
|
|
211
|
+
console.log("");
|
|
212
|
+
console.log(chalk.green("source uploaded — finish setup in the browser:"));
|
|
213
|
+
console.log(` ${chalk.bold(url)}`);
|
|
214
|
+
console.log("");
|
|
215
|
+
console.log(chalk.dim(" pick framework, build command, output dir, env vars and click Deploy."));
|
|
216
|
+
console.log(chalk.dim(" to skip the browser next time, fill .layero/project.json and run `layero deploy --config`."));
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
// Already-active project: behave like the old `layero deploy` — fire
|
|
220
|
+
// a build straight from the freshly-uploaded archive.
|
|
127
221
|
console.log(chalk.cyan("triggering deploy..."));
|
|
128
222
|
const deploy = await api.triggerDeploy(project.id, {
|
|
129
|
-
source_archive_key:
|
|
130
|
-
commit_sha:
|
|
223
|
+
source_archive_key: upload.archive_key,
|
|
224
|
+
commit_sha: upload.commit_sha,
|
|
131
225
|
commit_message: "CLI deploy",
|
|
132
226
|
framework_hint: opts.type,
|
|
133
227
|
});
|
|
@@ -138,11 +232,12 @@ export async function deployCmd(opts) {
|
|
|
138
232
|
process.exitCode = 1;
|
|
139
233
|
return;
|
|
140
234
|
}
|
|
141
|
-
console.log(chalk.green(`deploy ready → ${projectUrl}`));
|
|
235
|
+
console.log(chalk.green(`deploy ready → ${projectUrl(cliCfg.apiUrl, project.id)}`));
|
|
142
236
|
console.log(chalk.dim(` site: https://${project.apex_hostname} (CDN may take ~30-60s to propagate)`));
|
|
143
237
|
}
|
|
144
238
|
finally {
|
|
145
|
-
|
|
146
|
-
|
|
239
|
+
if (upload) {
|
|
240
|
+
await fs.unlink(upload.archivePath).catch(() => undefined);
|
|
241
|
+
}
|
|
147
242
|
}
|
|
148
243
|
}
|
package/dist/commands/link.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import chalk from "chalk";
|
|
2
2
|
import { ApiClient } from "../api.js";
|
|
3
3
|
import { loadConfig } from "../config.js";
|
|
4
|
-
import {
|
|
4
|
+
import { persistProjectLinking } from "../project-config.js";
|
|
5
5
|
export async function linkCmd(idOrSlug) {
|
|
6
6
|
const cfg = await loadConfig();
|
|
7
7
|
if (!cfg.token) {
|
|
@@ -26,12 +26,15 @@ export async function linkCmd(idOrSlug) {
|
|
|
26
26
|
}
|
|
27
27
|
proj = match;
|
|
28
28
|
}
|
|
29
|
-
await
|
|
29
|
+
await persistProjectLinking(process.cwd(), {
|
|
30
30
|
project_id: proj.id,
|
|
31
31
|
slug: proj.slug,
|
|
32
32
|
owner_slug: proj.owner.slug,
|
|
33
33
|
apex_hostname: proj.apex_hostname,
|
|
34
|
-
|
|
35
|
-
});
|
|
34
|
+
}, proj.framework_hint ?? null);
|
|
36
35
|
console.log(chalk.green(`linked ${proj.slug} (${proj.id}) → ./.layero/project.json`));
|
|
36
|
+
if (proj.status === "pending_setup") {
|
|
37
|
+
console.log(chalk.yellow(" note: project is in pending_setup. finish the setup wizard in the dashboard, " +
|
|
38
|
+
"or run `layero deploy` to upload source and get the wizard URL."));
|
|
39
|
+
}
|
|
37
40
|
}
|
package/dist/project-config.js
CHANGED
|
@@ -20,3 +20,49 @@ export async function saveProjectConfig(cwd, cfg) {
|
|
|
20
20
|
await fs.mkdir(dir, { recursive: true });
|
|
21
21
|
await fs.writeFile(projectConfigPath(cwd), JSON.stringify(cfg, null, 2), "utf-8");
|
|
22
22
|
}
|
|
23
|
+
/**
|
|
24
|
+
* Persist linking fields without disturbing user-managed config.
|
|
25
|
+
*
|
|
26
|
+
* `layero deploy` runs every time the user ships code, but the only thing
|
|
27
|
+
* it should ever write back to .layero/project.json is the linkage —
|
|
28
|
+
* project_id / slug / owner_slug / apex_hostname. Hand-edited fields
|
|
29
|
+
* (build_cmd, output_dir, env_vars, analytics_enabled, framework_hint)
|
|
30
|
+
* and any unknown keys the user added must be preserved verbatim, or we
|
|
31
|
+
* silently break the user's --config file on every interactive deploy.
|
|
32
|
+
*
|
|
33
|
+
* Reads the file as raw JSON, overlays the linking subset, and writes it
|
|
34
|
+
* back. Returns the merged config the caller should treat as the new
|
|
35
|
+
* source of truth (matters for first-deploy where there's no existing
|
|
36
|
+
* file).
|
|
37
|
+
*/
|
|
38
|
+
export async function persistProjectLinking(cwd, linking, fallbackHint) {
|
|
39
|
+
const file = projectConfigPath(cwd);
|
|
40
|
+
let raw = {};
|
|
41
|
+
try {
|
|
42
|
+
const text = await fs.readFile(file, "utf-8");
|
|
43
|
+
raw = JSON.parse(text);
|
|
44
|
+
}
|
|
45
|
+
catch (err) {
|
|
46
|
+
if (err?.code !== "ENOENT") {
|
|
47
|
+
throw err;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
const merged = {
|
|
51
|
+
...raw,
|
|
52
|
+
project_id: linking.project_id,
|
|
53
|
+
slug: linking.slug,
|
|
54
|
+
owner_slug: linking.owner_slug,
|
|
55
|
+
apex_hostname: linking.apex_hostname,
|
|
56
|
+
};
|
|
57
|
+
if (linking.api_url !== undefined)
|
|
58
|
+
merged.api_url = linking.api_url;
|
|
59
|
+
// Only seed framework_hint if the user hasn't set one yet — once it's in
|
|
60
|
+
// the file (manually or from a prior --type), leave it alone.
|
|
61
|
+
if (fallbackHint != null &&
|
|
62
|
+
(raw.framework_hint === undefined || raw.framework_hint === null)) {
|
|
63
|
+
merged.framework_hint = fallbackHint;
|
|
64
|
+
}
|
|
65
|
+
await fs.mkdir(path.join(cwd, ".layero"), { recursive: true });
|
|
66
|
+
await fs.writeFile(file, JSON.stringify(merged, null, 2), "utf-8");
|
|
67
|
+
return merged;
|
|
68
|
+
}
|
package/package.json
CHANGED
package/scripts/postinstall.cjs
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// Print a quick-start banner after `npm install -g layero`.
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
2
|
+
// Print a quick-start banner after `npm install -g layero`.
|
|
3
|
+
//
|
|
4
|
+
// npm 10+ buffers postinstall stdout AND stderr (foreground-scripts=false
|
|
5
|
+
// by default), so writing to either is invisible unless the script fails
|
|
6
|
+
// or the user passes --foreground-scripts. Workaround: write directly to
|
|
7
|
+
// the controlling terminal at /dev/tty, which bypasses npm's pipe.
|
|
8
|
+
//
|
|
9
|
+
// On Windows there's no /dev/tty; we fall back to stderr (which Windows
|
|
10
|
+
// npm doesn't buffer the same way) and silently skip on any error.
|
|
7
11
|
|
|
8
12
|
if (process.env.CI) return;
|
|
9
13
|
if (process.env.LAYERO_SKIP_POSTINSTALL) return;
|
|
@@ -14,17 +18,15 @@ const isGlobal = process.env.npm_config_global === "true";
|
|
|
14
18
|
const isDirect = process.env.npm_package_name === "layero";
|
|
15
19
|
if (!isGlobal && !isDirect) return;
|
|
16
20
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
const c =
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
}
|
|
27
|
-
: { reset: "", bold: "", dim: "", cyan: "", green: "" };
|
|
21
|
+
const fs = require("node:fs");
|
|
22
|
+
|
|
23
|
+
const c = {
|
|
24
|
+
reset: "\x1b[0m",
|
|
25
|
+
bold: "\x1b[1m",
|
|
26
|
+
dim: "\x1b[2m",
|
|
27
|
+
cyan: "\x1b[36m",
|
|
28
|
+
green: "\x1b[32m",
|
|
29
|
+
};
|
|
28
30
|
|
|
29
31
|
const lines = [
|
|
30
32
|
"",
|
|
@@ -37,4 +39,17 @@ const lines = [
|
|
|
37
39
|
` ${c.dim}Docs: https://layero.ru${c.reset}`,
|
|
38
40
|
"",
|
|
39
41
|
];
|
|
40
|
-
|
|
42
|
+
const banner = lines.join("\n");
|
|
43
|
+
|
|
44
|
+
try {
|
|
45
|
+
if (process.platform === "win32") {
|
|
46
|
+
process.stderr.write(banner);
|
|
47
|
+
} else {
|
|
48
|
+
// Open the controlling tty directly. This bypasses npm's stdio pipes.
|
|
49
|
+
const fd = fs.openSync("/dev/tty", "w");
|
|
50
|
+
fs.writeSync(fd, banner);
|
|
51
|
+
fs.closeSync(fd);
|
|
52
|
+
}
|
|
53
|
+
} catch {
|
|
54
|
+
// Headless install (no tty), or perms issue — give up silently.
|
|
55
|
+
}
|