layero 0.1.7 → 0.2.0
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 +10 -4
- package/dist/bin/layero.js +11 -2
- package/dist/commands/deploy.js +181 -46
- package/dist/commands/link.js +8 -5
- package/dist/commands/login.js +15 -5
- package/dist/commands/token.js +4 -4
- package/dist/commands/whoami.js +1 -1
- package/dist/project-config.js +58 -1
- package/package.json +1 -1
package/dist/api.js
CHANGED
|
@@ -58,17 +58,23 @@ 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
|
}
|
|
64
70
|
pollLogs(deployId, afterId) {
|
|
65
71
|
return this.request("GET", `/deploys/${deployId}/logs?after_id=${afterId}`);
|
|
66
72
|
}
|
|
67
|
-
|
|
68
|
-
return this.request("POST", "/me/
|
|
73
|
+
setUsername(value) {
|
|
74
|
+
return this.request("POST", "/auth/me/username", { value });
|
|
69
75
|
}
|
|
70
|
-
|
|
71
|
-
return this.request("GET", `/me/
|
|
76
|
+
checkUsername(value) {
|
|
77
|
+
return this.request("GET", `/auth/me/username/check?value=${encodeURIComponent(value)}`);
|
|
72
78
|
}
|
|
73
79
|
}
|
|
74
80
|
export async function uploadArchive(init, filePath) {
|
package/dist/bin/layero.js
CHANGED
|
@@ -62,8 +62,17 @@ async function main() {
|
|
|
62
62
|
.option("-t, --type <preset>", "framework hint (vite | next | astro | cra | sveltekit | nuxt | gatsby | static)")
|
|
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
|
-
.option("-y, --yes", "non-interactive: accept defaults
|
|
66
|
-
.
|
|
65
|
+
.option("-y, --yes", "non-interactive: accept defaults and skip --prod confirmation")
|
|
66
|
+
.option("--config", "use framework/build settings + env vars from .layero/project.json (skips the browser setup wizard)")
|
|
67
|
+
.option("--prod", "deploy to production (replaces apex_hostname's active deploy). Without this flag, deploys go to the project's CLI preview pseudo-branch.")
|
|
68
|
+
.option("--branch <name>", "deploy to a specific branch's environment. Wins over --prod.")
|
|
69
|
+
.addHelpText("after", "\nExamples:\n" +
|
|
70
|
+
" $ layero deploy # preview on CLI pseudo-branch (never replaces prod)\n" +
|
|
71
|
+
" $ layero deploy --prod # production deploy (interactive confirm)\n" +
|
|
72
|
+
" $ layero deploy --prod --yes # production deploy, no prompt (CI)\n" +
|
|
73
|
+
" $ layero deploy --branch=staging # preview on a specific branch\n" +
|
|
74
|
+
" $ layero deploy --config # uses .layero/project.json end-to-end (CI-friendly)\n" +
|
|
75
|
+
" $ layero deploy --type vite")
|
|
67
76
|
.action(async (opts) => {
|
|
68
77
|
await deployCmd(opts);
|
|
69
78
|
});
|
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,26 @@ async function prompt(question, fallback) {
|
|
|
48
52
|
rl.close();
|
|
49
53
|
}
|
|
50
54
|
}
|
|
51
|
-
async function
|
|
55
|
+
async function confirm(question) {
|
|
56
|
+
const rl = readline.createInterface({
|
|
57
|
+
input: process.stdin,
|
|
58
|
+
output: process.stdout,
|
|
59
|
+
});
|
|
60
|
+
try {
|
|
61
|
+
const answer = (await rl.question(`${question} [y/N]: `)).trim().toLowerCase();
|
|
62
|
+
return answer === "y" || answer === "yes";
|
|
63
|
+
}
|
|
64
|
+
finally {
|
|
65
|
+
rl.close();
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
function deployTargeting(opts) {
|
|
69
|
+
if (opts.branch) {
|
|
70
|
+
return { target: "preview", branch: opts.branch };
|
|
71
|
+
}
|
|
72
|
+
return { target: opts.prod ? "production" : "preview" };
|
|
73
|
+
}
|
|
74
|
+
async function resolveOrCreateProject(api, cwd, opts, existing) {
|
|
52
75
|
if (opts.project) {
|
|
53
76
|
const all = await api.listProjects();
|
|
54
77
|
const match = all.find((p) => p.id === opts.project) ??
|
|
@@ -58,7 +81,6 @@ async function resolveProject(api, cwd, opts) {
|
|
|
58
81
|
}
|
|
59
82
|
return { project: match, createdNow: false };
|
|
60
83
|
}
|
|
61
|
-
const existing = await loadProjectConfig(cwd);
|
|
62
84
|
if (existing) {
|
|
63
85
|
try {
|
|
64
86
|
const project = await api.getProject(existing.project_id);
|
|
@@ -73,63 +95,175 @@ async function resolveProject(api, cwd, opts) {
|
|
|
73
95
|
throw err;
|
|
74
96
|
}
|
|
75
97
|
}
|
|
76
|
-
// No project_id locally — create one.
|
|
77
98
|
const me = await api.me();
|
|
78
|
-
if (!me.
|
|
79
|
-
throw new Error("no
|
|
99
|
+
if (!me.username) {
|
|
100
|
+
throw new Error("no username set on your account. open https://app.layero.ru/onboarding " +
|
|
80
101
|
"and pick one, then re-run.");
|
|
81
102
|
}
|
|
82
103
|
const fallbackName = path.basename(cwd);
|
|
83
|
-
const name = opts.name ?? (opts.yes ? fallbackName : await prompt("project name", fallbackName));
|
|
104
|
+
const name = opts.name ?? (opts.yes || opts.config ? fallbackName : await prompt("project name", fallbackName));
|
|
84
105
|
const project = await api.createCliProject({
|
|
85
106
|
name,
|
|
86
107
|
framework_hint: opts.type,
|
|
87
108
|
});
|
|
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
109
|
return { project, createdNow: true };
|
|
96
110
|
}
|
|
111
|
+
async function packAndUpload(api, cwd, project) {
|
|
112
|
+
console.log(chalk.cyan("packing source..."));
|
|
113
|
+
const pack = await packCwd(cwd, project.slug);
|
|
114
|
+
console.log(chalk.dim(` ${pack.fileCount} files, ${(pack.size / (1024 * 1024)).toFixed(2)} MB, sha256=${pack.sha256.slice(0, 12)}`));
|
|
115
|
+
console.log(chalk.cyan("requesting upload URL..."));
|
|
116
|
+
const init = await api.initUpload(project.id);
|
|
117
|
+
console.log(chalk.cyan("uploading archive..."));
|
|
118
|
+
await uploadArchive(init, pack.archivePath);
|
|
119
|
+
return {
|
|
120
|
+
archive_key: init.source_archive_key,
|
|
121
|
+
commit_sha: pack.sha256,
|
|
122
|
+
archivePath: pack.archivePath,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
function ensureConfigComplete(cfg) {
|
|
126
|
+
if (!cfg) {
|
|
127
|
+
throw new Error("no .layero/project.json found. run `layero deploy` once without --config " +
|
|
128
|
+
"to create the project, fill in framework_hint/build_cmd/output_dir there, " +
|
|
129
|
+
"then re-run with --config.");
|
|
130
|
+
}
|
|
131
|
+
const missing = [];
|
|
132
|
+
if (!cfg.framework_hint)
|
|
133
|
+
missing.push("framework_hint");
|
|
134
|
+
if (!cfg.build_cmd)
|
|
135
|
+
missing.push("build_cmd");
|
|
136
|
+
if (!cfg.output_dir)
|
|
137
|
+
missing.push("output_dir");
|
|
138
|
+
if (missing.length > 0) {
|
|
139
|
+
throw new Error(`.layero/project.json is missing required fields for --config: ${missing.join(", ")}`);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
97
142
|
export async function deployCmd(opts) {
|
|
98
143
|
if (opts.type && !VALID_TYPES.has(opts.type.toLowerCase())) {
|
|
99
144
|
throw new Error(`unknown --type "${opts.type}". valid: ${[...VALID_TYPES].join(", ")}`);
|
|
100
145
|
}
|
|
101
|
-
const
|
|
102
|
-
if (!
|
|
146
|
+
const cliCfg = await loadConfig();
|
|
147
|
+
if (!cliCfg.token) {
|
|
103
148
|
throw new Error("not logged in. run `layero login` first.");
|
|
104
149
|
}
|
|
105
|
-
const api = new ApiClient(
|
|
150
|
+
const api = new ApiClient(cliCfg);
|
|
106
151
|
const cwd = process.cwd();
|
|
107
|
-
const
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
//
|
|
115
|
-
|
|
152
|
+
const existing = await loadProjectConfig(cwd);
|
|
153
|
+
const { project: created, createdNow } = await resolveOrCreateProject(api, cwd, opts, existing);
|
|
154
|
+
let project = created;
|
|
155
|
+
if (project.cli_deploys_enabled === false) {
|
|
156
|
+
throw new Error(`CLI deploys are disabled on project "${project.slug}". ` +
|
|
157
|
+
"Enable them in project settings or use the dashboard's Deploy button.");
|
|
158
|
+
}
|
|
159
|
+
// Prompt before overwriting production. CLI deploys default to a per-project
|
|
160
|
+
// "cli" pseudo-branch (preview-only); --prod is the explicit opt-in to land
|
|
161
|
+
// on apex_hostname. --yes (CI) or --branch=<default_branch> bypass.
|
|
162
|
+
if (opts.prod && !opts.yes && !opts.branch) {
|
|
163
|
+
const ok = await confirm(`deploy to production (https://${project.apex_hostname})?`);
|
|
164
|
+
if (!ok) {
|
|
165
|
+
console.log(chalk.yellow("aborted."));
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
// Persist linking metadata only — never touch hand-edited config fields
|
|
170
|
+
// or unknown keys the user may have added (a --config file is the user's
|
|
171
|
+
// source of truth, not ours). `persistProjectLinking` reads the file as
|
|
172
|
+
// raw JSON, overlays just project_id/slug/organization_slug/apex_hostname,
|
|
173
|
+
// and writes it back.
|
|
174
|
+
const persistedCfg = await persistProjectLinking(cwd, {
|
|
175
|
+
project_id: project.id,
|
|
176
|
+
slug: project.slug,
|
|
177
|
+
organization_slug: project.organization.slug,
|
|
178
|
+
apex_hostname: project.apex_hostname,
|
|
179
|
+
}, opts.type ?? null);
|
|
116
180
|
if (createdNow) {
|
|
117
|
-
console.log(chalk.green(`created project ${project.slug}
|
|
181
|
+
console.log(chalk.green(`created project ${project.slug}`));
|
|
118
182
|
}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
183
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
184
|
+
// Path A: --config — fully scripted, runs the setup endpoint with
|
|
185
|
+
// fields from .layero/project.json, then deploys.
|
|
186
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
187
|
+
if (opts.config) {
|
|
188
|
+
ensureConfigComplete(persistedCfg);
|
|
189
|
+
if (project.status === "pending_setup") {
|
|
190
|
+
console.log(chalk.cyan("applying config..."));
|
|
191
|
+
project = await api.completeSetup(project.id, {
|
|
192
|
+
framework_hint: persistedCfg.framework_hint,
|
|
193
|
+
build_cmd: persistedCfg.build_cmd,
|
|
194
|
+
output_dir: persistedCfg.output_dir,
|
|
195
|
+
analytics_enabled: persistedCfg.analytics_enabled ?? false,
|
|
196
|
+
env_vars: persistedCfg.env_vars ?? {},
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
let upload = null;
|
|
200
|
+
try {
|
|
201
|
+
upload = await packAndUpload(api, cwd, project);
|
|
202
|
+
console.log(chalk.cyan("triggering deploy..."));
|
|
203
|
+
const targeting = deployTargeting(opts);
|
|
204
|
+
const deploy = await api.triggerDeploy(project.id, {
|
|
205
|
+
source_archive_key: upload.archive_key,
|
|
206
|
+
commit_sha: upload.commit_sha,
|
|
207
|
+
commit_message: "CLI deploy",
|
|
208
|
+
framework_hint: persistedCfg.framework_hint ?? undefined,
|
|
209
|
+
target: targeting.target,
|
|
210
|
+
branch: targeting.branch,
|
|
211
|
+
});
|
|
212
|
+
console.log(chalk.dim(` deploy_id=${deploy.id}`));
|
|
213
|
+
const final = await streamDeployLogs(api, deploy.id);
|
|
214
|
+
if (final.status !== "ready") {
|
|
215
|
+
console.error(chalk.red(`deploy failed (${final.status})${final.error_message ? `: ${final.error_message}` : ""}`));
|
|
216
|
+
process.exitCode = 1;
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
console.log(chalk.green(`deploy ready → ${projectUrl(cliCfg.apiUrl, project.id)}`));
|
|
220
|
+
if (opts.prod && !opts.branch) {
|
|
221
|
+
console.log(chalk.dim(` production: https://${project.apex_hostname} (CDN may take ~30-60s to propagate)`));
|
|
222
|
+
}
|
|
223
|
+
else {
|
|
224
|
+
console.log(chalk.dim(` preview deploy — open the dashboard for the exact URL`));
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
finally {
|
|
228
|
+
if (upload) {
|
|
229
|
+
await fs.unlink(upload.archivePath).catch(() => undefined);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
235
|
+
// Path B: no flag — upload the source, stash it on the project row,
|
|
236
|
+
// print the setup URL. The user finishes the flow
|
|
237
|
+
// in the browser, exactly like a GitHub import.
|
|
238
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
239
|
+
let upload = null;
|
|
122
240
|
try {
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
241
|
+
upload = await packAndUpload(api, cwd, project);
|
|
242
|
+
await api.finalizeUpload(project.id, {
|
|
243
|
+
source_archive_key: upload.archive_key,
|
|
244
|
+
commit_sha: upload.commit_sha,
|
|
245
|
+
});
|
|
246
|
+
if (project.status === "pending_setup") {
|
|
247
|
+
const url = setupUrl(cliCfg.apiUrl, project.id);
|
|
248
|
+
console.log("");
|
|
249
|
+
console.log(chalk.green("source uploaded — finish setup in the browser:"));
|
|
250
|
+
console.log(` ${chalk.bold(url)}`);
|
|
251
|
+
console.log("");
|
|
252
|
+
console.log(chalk.dim(" pick framework, build command, output dir, env vars and click Deploy."));
|
|
253
|
+
console.log(chalk.dim(" to skip the browser next time, fill .layero/project.json and run `layero deploy --config`."));
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
// Already-active project: behave like the old `layero deploy` — fire
|
|
257
|
+
// a build straight from the freshly-uploaded archive.
|
|
127
258
|
console.log(chalk.cyan("triggering deploy..."));
|
|
259
|
+
const targeting = deployTargeting(opts);
|
|
128
260
|
const deploy = await api.triggerDeploy(project.id, {
|
|
129
|
-
source_archive_key:
|
|
130
|
-
commit_sha:
|
|
261
|
+
source_archive_key: upload.archive_key,
|
|
262
|
+
commit_sha: upload.commit_sha,
|
|
131
263
|
commit_message: "CLI deploy",
|
|
132
264
|
framework_hint: opts.type,
|
|
265
|
+
target: targeting.target,
|
|
266
|
+
branch: targeting.branch,
|
|
133
267
|
});
|
|
134
268
|
console.log(chalk.dim(` deploy_id=${deploy.id}`));
|
|
135
269
|
const final = await streamDeployLogs(api, deploy.id);
|
|
@@ -138,11 +272,12 @@ export async function deployCmd(opts) {
|
|
|
138
272
|
process.exitCode = 1;
|
|
139
273
|
return;
|
|
140
274
|
}
|
|
141
|
-
console.log(chalk.green(`deploy ready → ${projectUrl}`));
|
|
275
|
+
console.log(chalk.green(`deploy ready → ${projectUrl(cliCfg.apiUrl, project.id)}`));
|
|
142
276
|
console.log(chalk.dim(` site: https://${project.apex_hostname} (CDN may take ~30-60s to propagate)`));
|
|
143
277
|
}
|
|
144
278
|
finally {
|
|
145
|
-
|
|
146
|
-
|
|
279
|
+
if (upload) {
|
|
280
|
+
await fs.unlink(upload.archivePath).catch(() => undefined);
|
|
281
|
+
}
|
|
147
282
|
}
|
|
148
283
|
}
|
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
|
+
organization_slug: proj.organization.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/commands/login.js
CHANGED
|
@@ -66,7 +66,17 @@ export async function loginCmd(opts) {
|
|
|
66
66
|
});
|
|
67
67
|
server.on("error", reject);
|
|
68
68
|
server.listen(desiredPort, "127.0.0.1", () => {
|
|
69
|
-
const
|
|
69
|
+
const addr = server.address();
|
|
70
|
+
// Defence-in-depth: confirm we actually bound to loopback before
|
|
71
|
+
// emitting `callback=…` to the browser. If listen() somehow returned
|
|
72
|
+
// a non-loopback address (Node bug, weird /etc/hosts) we'd otherwise
|
|
73
|
+
// hand the JWT redirect target out to whoever owns that interface.
|
|
74
|
+
if (!addr || addr.address !== "127.0.0.1") {
|
|
75
|
+
server.close();
|
|
76
|
+
reject(new Error(`expected to bind 127.0.0.1, got ${addr?.address ?? "null"}`));
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
const port = addr.port;
|
|
70
80
|
const callback = `http://127.0.0.1:${port}/callback`;
|
|
71
81
|
const startUrl = `${cfg.apiUrl.replace(/\/+$/, "")}/auth/cli/start` +
|
|
72
82
|
`?provider=${provider}` +
|
|
@@ -87,10 +97,10 @@ export async function loginCmd(opts) {
|
|
|
87
97
|
cfg.token = token;
|
|
88
98
|
const probe = new ApiClient(cfg);
|
|
89
99
|
const me = await probe.me();
|
|
90
|
-
cfg.user = { id: me.id,
|
|
100
|
+
cfg.user = { id: me.id, username: me.username, email: me.email };
|
|
91
101
|
await saveConfig(cfg);
|
|
92
|
-
console.log(chalk.green(`logged in as ${me.
|
|
93
|
-
if (!me.
|
|
94
|
-
console.log(chalk.yellow("no
|
|
102
|
+
console.log(chalk.green(`logged in as ${me.username ?? me.email ?? me.id}`));
|
|
103
|
+
if (!me.username) {
|
|
104
|
+
console.log(chalk.yellow("no username set — open https://app.layero.ru/onboarding to pick one."));
|
|
95
105
|
}
|
|
96
106
|
}
|
package/dist/commands/token.js
CHANGED
|
@@ -8,7 +8,7 @@ export async function tokenSetCmd(jwt) {
|
|
|
8
8
|
const probe = new ApiClient(cfg);
|
|
9
9
|
try {
|
|
10
10
|
const me = await probe.me();
|
|
11
|
-
cfg.user = { id: me.id,
|
|
11
|
+
cfg.user = { id: me.id, username: me.username, email: me.email };
|
|
12
12
|
}
|
|
13
13
|
catch (err) {
|
|
14
14
|
console.error(chalk.red(`token rejected by API: ${err.message}`));
|
|
@@ -16,9 +16,9 @@ export async function tokenSetCmd(jwt) {
|
|
|
16
16
|
return;
|
|
17
17
|
}
|
|
18
18
|
await saveConfig(cfg);
|
|
19
|
-
console.log(chalk.green(`saved token for ${cfg.user?.
|
|
20
|
-
if (!cfg.user?.
|
|
21
|
-
console.log(chalk.yellow("no
|
|
19
|
+
console.log(chalk.green(`saved token for ${cfg.user?.username ?? cfg.user?.email ?? cfg.user?.id}`));
|
|
20
|
+
if (!cfg.user?.username) {
|
|
21
|
+
console.log(chalk.yellow("no username set — open https://app.layero.ru/onboarding to pick one " +
|
|
22
22
|
"before `layero deploy`."));
|
|
23
23
|
}
|
|
24
24
|
}
|
package/dist/commands/whoami.js
CHANGED
|
@@ -11,7 +11,7 @@ export async function whoamiCmd() {
|
|
|
11
11
|
const api = new ApiClient(cfg);
|
|
12
12
|
const me = await api.me();
|
|
13
13
|
console.log(`id: ${me.id}`);
|
|
14
|
-
console.log(`
|
|
14
|
+
console.log(`username: ${me.username ?? chalk.yellow("(not set)")}`);
|
|
15
15
|
console.log(`email: ${me.email ?? "(none)"}`);
|
|
16
16
|
if (me.github_login) {
|
|
17
17
|
console.log(`github: ${me.github_login}`);
|
package/dist/project-config.js
CHANGED
|
@@ -6,7 +6,15 @@ export function projectConfigPath(cwd) {
|
|
|
6
6
|
export async function loadProjectConfig(cwd) {
|
|
7
7
|
try {
|
|
8
8
|
const raw = await fs.readFile(projectConfigPath(cwd), "utf-8");
|
|
9
|
-
|
|
9
|
+
const parsed = JSON.parse(raw);
|
|
10
|
+
// Backward-read: configs written before V050 stored the field as
|
|
11
|
+
// `owner_slug`. Promote it to `organization_slug` in-memory so the
|
|
12
|
+
// rest of the CLI doesn't have to handle both names. Disk file gets
|
|
13
|
+
// rewritten on the next persistProjectLinking() call.
|
|
14
|
+
if (!parsed.organization_slug && parsed.owner_slug) {
|
|
15
|
+
parsed.organization_slug = parsed.owner_slug;
|
|
16
|
+
}
|
|
17
|
+
return parsed;
|
|
10
18
|
}
|
|
11
19
|
catch (err) {
|
|
12
20
|
if (err?.code === "ENOENT") {
|
|
@@ -20,3 +28,52 @@ export async function saveProjectConfig(cwd, cfg) {
|
|
|
20
28
|
await fs.mkdir(dir, { recursive: true });
|
|
21
29
|
await fs.writeFile(projectConfigPath(cwd), JSON.stringify(cfg, null, 2), "utf-8");
|
|
22
30
|
}
|
|
31
|
+
/**
|
|
32
|
+
* Persist linking fields without disturbing user-managed config.
|
|
33
|
+
*
|
|
34
|
+
* `layero deploy` runs every time the user ships code, but the only thing
|
|
35
|
+
* it should ever write back to .layero/project.json is the linkage —
|
|
36
|
+
* project_id / slug / organization_slug / apex_hostname. Hand-edited
|
|
37
|
+
* fields (build_cmd, output_dir, env_vars, analytics_enabled,
|
|
38
|
+
* framework_hint) and any unknown keys the user added must be preserved
|
|
39
|
+
* verbatim, or we silently break the user's --config file on every
|
|
40
|
+
* interactive deploy.
|
|
41
|
+
*
|
|
42
|
+
* Reads the file as raw JSON, overlays the linking subset, and writes it
|
|
43
|
+
* back. Drops any legacy `owner_slug` key once the new name has been
|
|
44
|
+
* written, so future reads see only `organization_slug`. Returns the
|
|
45
|
+
* merged config the caller should treat as the new source of truth.
|
|
46
|
+
*/
|
|
47
|
+
export async function persistProjectLinking(cwd, linking, fallbackHint) {
|
|
48
|
+
const file = projectConfigPath(cwd);
|
|
49
|
+
let raw = {};
|
|
50
|
+
try {
|
|
51
|
+
const text = await fs.readFile(file, "utf-8");
|
|
52
|
+
raw = JSON.parse(text);
|
|
53
|
+
}
|
|
54
|
+
catch (err) {
|
|
55
|
+
if (err?.code !== "ENOENT") {
|
|
56
|
+
throw err;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
// Strip the legacy owner_slug if it sneaks in from an older CLI write.
|
|
60
|
+
delete raw.owner_slug;
|
|
61
|
+
const merged = {
|
|
62
|
+
...raw,
|
|
63
|
+
project_id: linking.project_id,
|
|
64
|
+
slug: linking.slug,
|
|
65
|
+
organization_slug: linking.organization_slug,
|
|
66
|
+
apex_hostname: linking.apex_hostname,
|
|
67
|
+
};
|
|
68
|
+
if (linking.api_url !== undefined)
|
|
69
|
+
merged.api_url = linking.api_url;
|
|
70
|
+
// Only seed framework_hint if the user hasn't set one yet — once it's in
|
|
71
|
+
// the file (manually or from a prior --type), leave it alone.
|
|
72
|
+
if (fallbackHint != null &&
|
|
73
|
+
(raw.framework_hint === undefined || raw.framework_hint === null)) {
|
|
74
|
+
merged.framework_hint = fallbackHint;
|
|
75
|
+
}
|
|
76
|
+
await fs.mkdir(path.join(cwd, ".layero"), { recursive: true });
|
|
77
|
+
await fs.writeFile(file, JSON.stringify(merged, null, 2), "utf-8");
|
|
78
|
+
return merged;
|
|
79
|
+
}
|