layero 0.1.8 → 0.3.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 +11 -4
- package/dist/bin/layero.js +36 -4
- package/dist/commands/deploy.js +49 -9
- package/dist/commands/deploys.js +146 -0
- package/dist/commands/link.js +1 -1
- 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 +20 -9
- package/package.json +1 -1
package/dist/api.js
CHANGED
|
@@ -70,11 +70,18 @@ export class ApiClient {
|
|
|
70
70
|
pollLogs(deployId, afterId) {
|
|
71
71
|
return this.request("GET", `/deploys/${deployId}/logs?after_id=${afterId}`);
|
|
72
72
|
}
|
|
73
|
-
|
|
74
|
-
|
|
73
|
+
listProjectDeploys(projectId, branch) {
|
|
74
|
+
const qs = branch ? `?branch=${encodeURIComponent(branch)}` : "";
|
|
75
|
+
return this.request("GET", `/projects/${projectId}/deploys${qs}`);
|
|
75
76
|
}
|
|
76
|
-
|
|
77
|
-
return this.request("
|
|
77
|
+
rollbackProject(projectId, input) {
|
|
78
|
+
return this.request("POST", `/projects/${projectId}/rollback`, input);
|
|
79
|
+
}
|
|
80
|
+
setUsername(value) {
|
|
81
|
+
return this.request("POST", "/auth/me/username", { value });
|
|
82
|
+
}
|
|
83
|
+
checkUsername(value) {
|
|
84
|
+
return this.request("GET", `/auth/me/username/check?value=${encodeURIComponent(value)}`);
|
|
78
85
|
}
|
|
79
86
|
}
|
|
80
87
|
export async function uploadArchive(init, filePath) {
|
package/dist/bin/layero.js
CHANGED
|
@@ -10,6 +10,7 @@ import { projectsListCmd } from "../commands/projects.js";
|
|
|
10
10
|
import { linkCmd } from "../commands/link.js";
|
|
11
11
|
import { tokenSetCmd } from "../commands/token.js";
|
|
12
12
|
import { deployCmd } from "../commands/deploy.js";
|
|
13
|
+
import { deploysListCmd, rollbackCmd } from "../commands/deploys.js";
|
|
13
14
|
import { loginCmd } from "../commands/login.js";
|
|
14
15
|
// Read version from the shipped package.json (two levels up from dist/bin/).
|
|
15
16
|
const pkgPath = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..", "package.json");
|
|
@@ -44,6 +45,33 @@ async function main() {
|
|
|
44
45
|
.command("list")
|
|
45
46
|
.description("List your projects.")
|
|
46
47
|
.action(projectsListCmd);
|
|
48
|
+
const deploys = program
|
|
49
|
+
.command("deploys")
|
|
50
|
+
.description("List and inspect deploys for the linked project.");
|
|
51
|
+
deploys
|
|
52
|
+
.command("list")
|
|
53
|
+
.description("List recent deploys for the project's default branch (or --branch).")
|
|
54
|
+
.option("--project <id_or_slug>", "target project (default: linked .layero/project.json)")
|
|
55
|
+
.option("--branch <name>", "branch to list deploys from (default: project's default_branch)")
|
|
56
|
+
.option("--limit <n>", "max entries to show (default 20)", (v) => Number(v))
|
|
57
|
+
.action(async (opts) => {
|
|
58
|
+
await deploysListCmd(opts);
|
|
59
|
+
});
|
|
60
|
+
program
|
|
61
|
+
.command("rollback")
|
|
62
|
+
.description("Re-activate the previous successful deploy on the project's default branch.")
|
|
63
|
+
.option("--project <id_or_slug>", "target project (default: linked .layero/project.json)")
|
|
64
|
+
.option("--branch <name>", "branch to roll back (default: project's default_branch)")
|
|
65
|
+
.option("--deploy <id_or_sha>", "explicit deploy id or commit sha prefix to roll back to")
|
|
66
|
+
.option("-y, --yes", "skip the confirmation prompt (CI)")
|
|
67
|
+
.addHelpText("after", "\nExamples:\n" +
|
|
68
|
+
" $ layero rollback # roll back default branch to previous ready deploy\n" +
|
|
69
|
+
" $ layero rollback --branch=staging # roll back the staging branch\n" +
|
|
70
|
+
" $ layero rollback --deploy=a3f9c2b # roll back to a specific commit/deploy\n" +
|
|
71
|
+
" $ layero rollback --yes # CI-friendly, no prompt")
|
|
72
|
+
.action(async (opts) => {
|
|
73
|
+
await rollbackCmd(opts);
|
|
74
|
+
});
|
|
47
75
|
program
|
|
48
76
|
.command("link <id_or_slug>")
|
|
49
77
|
.description("Link the current directory to an existing project.")
|
|
@@ -62,13 +90,17 @@ async function main() {
|
|
|
62
90
|
.option("-t, --type <preset>", "framework hint (vite | next | astro | cra | sveltekit | nuxt | gatsby | static)")
|
|
63
91
|
.option("--name <name>", "project name (only used on first deploy)")
|
|
64
92
|
.option("--project <id_or_slug>", "deploy into an existing project, ignoring local config")
|
|
65
|
-
.option("-y, --yes", "non-interactive: accept defaults
|
|
93
|
+
.option("-y, --yes", "non-interactive: accept defaults and skip --prod confirmation")
|
|
66
94
|
.option("--config", "use framework/build settings + env vars from .layero/project.json (skips the browser setup wizard)")
|
|
95
|
+
.option("--prod", "deploy to production (replaces apex_hostname's active deploy). Without this flag, deploys go to the project's CLI preview pseudo-branch.")
|
|
96
|
+
.option("--branch <name>", "deploy to a specific branch's environment. Wins over --prod.")
|
|
67
97
|
.addHelpText("after", "\nExamples:\n" +
|
|
68
|
-
" $ layero deploy #
|
|
98
|
+
" $ layero deploy # preview on CLI pseudo-branch (never replaces prod)\n" +
|
|
99
|
+
" $ layero deploy --prod # production deploy (interactive confirm)\n" +
|
|
100
|
+
" $ layero deploy --prod --yes # production deploy, no prompt (CI)\n" +
|
|
101
|
+
" $ layero deploy --branch=staging # preview on a specific branch\n" +
|
|
69
102
|
" $ layero deploy --config # uses .layero/project.json end-to-end (CI-friendly)\n" +
|
|
70
|
-
" $ layero deploy --type vite
|
|
71
|
-
" $ layero deploy --project my-site --yes")
|
|
103
|
+
" $ layero deploy --type vite")
|
|
72
104
|
.action(async (opts) => {
|
|
73
105
|
await deployCmd(opts);
|
|
74
106
|
});
|
package/dist/commands/deploy.js
CHANGED
|
@@ -52,6 +52,25 @@ async function prompt(question, fallback) {
|
|
|
52
52
|
rl.close();
|
|
53
53
|
}
|
|
54
54
|
}
|
|
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
|
+
}
|
|
55
74
|
async function resolveOrCreateProject(api, cwd, opts, existing) {
|
|
56
75
|
if (opts.project) {
|
|
57
76
|
const all = await api.listProjects();
|
|
@@ -77,8 +96,8 @@ async function resolveOrCreateProject(api, cwd, opts, existing) {
|
|
|
77
96
|
}
|
|
78
97
|
}
|
|
79
98
|
const me = await api.me();
|
|
80
|
-
if (!me.
|
|
81
|
-
throw new Error("no
|
|
99
|
+
if (!me.username) {
|
|
100
|
+
throw new Error("no username set on your account. open https://app.layero.ru/onboarding " +
|
|
82
101
|
"and pick one, then re-run.");
|
|
83
102
|
}
|
|
84
103
|
const fallbackName = path.basename(cwd);
|
|
@@ -133,19 +152,29 @@ export async function deployCmd(opts) {
|
|
|
133
152
|
const existing = await loadProjectConfig(cwd);
|
|
134
153
|
const { project: created, createdNow } = await resolveOrCreateProject(api, cwd, opts, existing);
|
|
135
154
|
let project = created;
|
|
136
|
-
if (project.
|
|
137
|
-
throw new Error(`project "${project.slug}"
|
|
138
|
-
"use the dashboard's Deploy button
|
|
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
|
+
}
|
|
139
168
|
}
|
|
140
169
|
// Persist linking metadata only — never touch hand-edited config fields
|
|
141
170
|
// or unknown keys the user may have added (a --config file is the user's
|
|
142
171
|
// source of truth, not ours). `persistProjectLinking` reads the file as
|
|
143
|
-
// raw JSON, overlays just project_id/slug/
|
|
144
|
-
// writes it back.
|
|
172
|
+
// raw JSON, overlays just project_id/slug/organization_slug/apex_hostname,
|
|
173
|
+
// and writes it back.
|
|
145
174
|
const persistedCfg = await persistProjectLinking(cwd, {
|
|
146
175
|
project_id: project.id,
|
|
147
176
|
slug: project.slug,
|
|
148
|
-
|
|
177
|
+
organization_slug: project.organization.slug,
|
|
149
178
|
apex_hostname: project.apex_hostname,
|
|
150
179
|
}, opts.type ?? null);
|
|
151
180
|
if (createdNow) {
|
|
@@ -171,11 +200,14 @@ export async function deployCmd(opts) {
|
|
|
171
200
|
try {
|
|
172
201
|
upload = await packAndUpload(api, cwd, project);
|
|
173
202
|
console.log(chalk.cyan("triggering deploy..."));
|
|
203
|
+
const targeting = deployTargeting(opts);
|
|
174
204
|
const deploy = await api.triggerDeploy(project.id, {
|
|
175
205
|
source_archive_key: upload.archive_key,
|
|
176
206
|
commit_sha: upload.commit_sha,
|
|
177
207
|
commit_message: "CLI deploy",
|
|
178
208
|
framework_hint: persistedCfg.framework_hint ?? undefined,
|
|
209
|
+
target: targeting.target,
|
|
210
|
+
branch: targeting.branch,
|
|
179
211
|
});
|
|
180
212
|
console.log(chalk.dim(` deploy_id=${deploy.id}`));
|
|
181
213
|
const final = await streamDeployLogs(api, deploy.id);
|
|
@@ -185,7 +217,12 @@ export async function deployCmd(opts) {
|
|
|
185
217
|
return;
|
|
186
218
|
}
|
|
187
219
|
console.log(chalk.green(`deploy ready → ${projectUrl(cliCfg.apiUrl, project.id)}`));
|
|
188
|
-
|
|
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
|
+
}
|
|
189
226
|
}
|
|
190
227
|
finally {
|
|
191
228
|
if (upload) {
|
|
@@ -219,11 +256,14 @@ export async function deployCmd(opts) {
|
|
|
219
256
|
// Already-active project: behave like the old `layero deploy` — fire
|
|
220
257
|
// a build straight from the freshly-uploaded archive.
|
|
221
258
|
console.log(chalk.cyan("triggering deploy..."));
|
|
259
|
+
const targeting = deployTargeting(opts);
|
|
222
260
|
const deploy = await api.triggerDeploy(project.id, {
|
|
223
261
|
source_archive_key: upload.archive_key,
|
|
224
262
|
commit_sha: upload.commit_sha,
|
|
225
263
|
commit_message: "CLI deploy",
|
|
226
264
|
framework_hint: opts.type,
|
|
265
|
+
target: targeting.target,
|
|
266
|
+
branch: targeting.branch,
|
|
227
267
|
});
|
|
228
268
|
console.log(chalk.dim(` deploy_id=${deploy.id}`));
|
|
229
269
|
const final = await streamDeployLogs(api, deploy.id);
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import readline from "node:readline/promises";
|
|
2
|
+
import chalk from "chalk";
|
|
3
|
+
import { ApiClient, ApiError } from "../api.js";
|
|
4
|
+
import { loadConfig } from "../config.js";
|
|
5
|
+
import { loadProjectConfig } from "../project-config.js";
|
|
6
|
+
async function resolveProjectId(api, cwd, override) {
|
|
7
|
+
if (override) {
|
|
8
|
+
// Accept slug or id; resolve via list to keep consistent error UX.
|
|
9
|
+
const all = await api.listProjects();
|
|
10
|
+
const match = all.find((p) => p.id === override) ?? all.find((p) => p.slug === override);
|
|
11
|
+
if (!match)
|
|
12
|
+
throw new Error(`no project with id/slug "${override}"`);
|
|
13
|
+
return match.id;
|
|
14
|
+
}
|
|
15
|
+
const cfg = await loadProjectConfig(cwd);
|
|
16
|
+
if (!cfg) {
|
|
17
|
+
throw new Error("no .layero/project.json found. run `layero deploy` to create one, or pass --project <slug>.");
|
|
18
|
+
}
|
|
19
|
+
return cfg.project_id;
|
|
20
|
+
}
|
|
21
|
+
function shortSha(sha) {
|
|
22
|
+
return (sha ?? "").slice(0, 7);
|
|
23
|
+
}
|
|
24
|
+
function statusBadge(status) {
|
|
25
|
+
switch (status) {
|
|
26
|
+
case "ready":
|
|
27
|
+
return chalk.green("● ready");
|
|
28
|
+
case "building":
|
|
29
|
+
case "queued":
|
|
30
|
+
return chalk.cyan(`● ${status}`);
|
|
31
|
+
case "failed":
|
|
32
|
+
return chalk.red("● failed");
|
|
33
|
+
default:
|
|
34
|
+
return chalk.dim(`● ${status}`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
function fmtTime(iso) {
|
|
38
|
+
if (!iso)
|
|
39
|
+
return "—";
|
|
40
|
+
const d = new Date(iso);
|
|
41
|
+
// YYYY-MM-DD HH:mm
|
|
42
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
43
|
+
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`;
|
|
44
|
+
}
|
|
45
|
+
function fmtSource(d) {
|
|
46
|
+
const t = d.source_type ?? "github";
|
|
47
|
+
if (t === "cli")
|
|
48
|
+
return chalk.dim("(cli)");
|
|
49
|
+
if (d.triggered_by_user_id)
|
|
50
|
+
return chalk.dim("(manual)");
|
|
51
|
+
return chalk.dim("(push)");
|
|
52
|
+
}
|
|
53
|
+
export async function deploysListCmd(opts) {
|
|
54
|
+
const cliCfg = await loadConfig();
|
|
55
|
+
if (!cliCfg.token)
|
|
56
|
+
throw new Error("not logged in. run `layero login` first.");
|
|
57
|
+
const api = new ApiClient(cliCfg);
|
|
58
|
+
const projectId = await resolveProjectId(api, process.cwd(), opts.project);
|
|
59
|
+
const deploys = await api.listProjectDeploys(projectId, opts.branch);
|
|
60
|
+
const limit = opts.limit ?? 20;
|
|
61
|
+
const rows = deploys.slice(0, limit);
|
|
62
|
+
if (rows.length === 0) {
|
|
63
|
+
console.log(chalk.dim("no deploys yet."));
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
for (const d of rows) {
|
|
67
|
+
const line = [
|
|
68
|
+
statusBadge(d.status),
|
|
69
|
+
chalk.bold(shortSha(d.commit_sha).padEnd(7)),
|
|
70
|
+
fmtTime(d.created_at).padEnd(16),
|
|
71
|
+
fmtSource(d),
|
|
72
|
+
d.commit_message
|
|
73
|
+
? chalk.dim(((d.commit_message ?? "").split("\n")[0] ?? "").slice(0, 60))
|
|
74
|
+
: "",
|
|
75
|
+
].join(" ");
|
|
76
|
+
console.log(line);
|
|
77
|
+
}
|
|
78
|
+
if (deploys.length > rows.length) {
|
|
79
|
+
console.log(chalk.dim(` …${deploys.length - rows.length} more (use --limit to show)`));
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
async function confirm(question) {
|
|
83
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
84
|
+
try {
|
|
85
|
+
const answer = (await rl.question(`${question} [y/N]: `)).trim().toLowerCase();
|
|
86
|
+
return answer === "y" || answer === "yes";
|
|
87
|
+
}
|
|
88
|
+
finally {
|
|
89
|
+
rl.close();
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
export async function rollbackCmd(opts) {
|
|
93
|
+
const cliCfg = await loadConfig();
|
|
94
|
+
if (!cliCfg.token)
|
|
95
|
+
throw new Error("not logged in. run `layero login` first.");
|
|
96
|
+
const api = new ApiClient(cliCfg);
|
|
97
|
+
const cwd = process.cwd();
|
|
98
|
+
const projectId = await resolveProjectId(api, cwd, opts.project);
|
|
99
|
+
// Show what we're rolling back from/to so the user can sanity-check
|
|
100
|
+
// before confirming. The dashboard does this visually; the CLI shows
|
|
101
|
+
// the equivalent: current deploy + the candidate target.
|
|
102
|
+
const deploys = await api.listProjectDeploys(projectId, opts.branch);
|
|
103
|
+
if (deploys.length === 0) {
|
|
104
|
+
throw new Error("no deploys for this project/branch");
|
|
105
|
+
}
|
|
106
|
+
const current = deploys.find((d) => d.status === "ready");
|
|
107
|
+
let target;
|
|
108
|
+
if (opts.deploy) {
|
|
109
|
+
target = deploys.find((d) => d.id === opts.deploy || d.commit_sha.startsWith(opts.deploy));
|
|
110
|
+
if (!target)
|
|
111
|
+
throw new Error(`no deploy matching "${opts.deploy}"`);
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
// Find the latest ready deploy that isn't the current active one.
|
|
115
|
+
const ready = deploys.filter((d) => d.status === "ready");
|
|
116
|
+
target = ready.find((d) => d.id !== current?.id);
|
|
117
|
+
if (!target)
|
|
118
|
+
throw new Error("no eligible deploy to roll back to");
|
|
119
|
+
}
|
|
120
|
+
console.log(chalk.cyan("rollback plan:"));
|
|
121
|
+
if (current) {
|
|
122
|
+
console.log(` from: ${shortSha(current.commit_sha)} ${fmtTime(current.created_at)} ${chalk.dim(current.commit_message?.split("\n")[0] ?? "")}`);
|
|
123
|
+
}
|
|
124
|
+
console.log(` to: ${shortSha(target.commit_sha)} ${fmtTime(target.created_at)} ${chalk.dim(target.commit_message?.split("\n")[0] ?? "")}`);
|
|
125
|
+
if (!opts.yes) {
|
|
126
|
+
const ok = await confirm("proceed with rollback?");
|
|
127
|
+
if (!ok) {
|
|
128
|
+
console.log(chalk.yellow("aborted."));
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
try {
|
|
133
|
+
const out = await api.rollbackProject(projectId, {
|
|
134
|
+
branch: opts.branch,
|
|
135
|
+
deploy_id: target.id,
|
|
136
|
+
});
|
|
137
|
+
console.log(chalk.green(`rolled back to ${shortSha(out.commit_sha)}.`));
|
|
138
|
+
console.log(chalk.dim(" CDN cache purged; new requests serve the rolled-back artifact."));
|
|
139
|
+
}
|
|
140
|
+
catch (err) {
|
|
141
|
+
if (err instanceof ApiError) {
|
|
142
|
+
throw new Error(`rollback failed (${err.status}): ${err.body.slice(0, 200)}`);
|
|
143
|
+
}
|
|
144
|
+
throw err;
|
|
145
|
+
}
|
|
146
|
+
}
|
package/dist/commands/link.js
CHANGED
|
@@ -29,7 +29,7 @@ export async function linkCmd(idOrSlug) {
|
|
|
29
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
34
|
}, proj.framework_hint ?? null);
|
|
35
35
|
console.log(chalk.green(`linked ${proj.slug} (${proj.id}) → ./.layero/project.json`));
|
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") {
|
|
@@ -25,15 +33,16 @@ export async function saveProjectConfig(cwd, cfg) {
|
|
|
25
33
|
*
|
|
26
34
|
* `layero deploy` runs every time the user ships code, but the only thing
|
|
27
35
|
* it should ever write back to .layero/project.json is the linkage —
|
|
28
|
-
* project_id / slug /
|
|
29
|
-
* (build_cmd, output_dir, env_vars, analytics_enabled,
|
|
30
|
-
* and any unknown keys the user added must be preserved
|
|
31
|
-
* silently break the user's --config file on every
|
|
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.
|
|
32
41
|
*
|
|
33
42
|
* Reads the file as raw JSON, overlays the linking subset, and writes it
|
|
34
|
-
* back.
|
|
35
|
-
*
|
|
36
|
-
*
|
|
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.
|
|
37
46
|
*/
|
|
38
47
|
export async function persistProjectLinking(cwd, linking, fallbackHint) {
|
|
39
48
|
const file = projectConfigPath(cwd);
|
|
@@ -47,11 +56,13 @@ export async function persistProjectLinking(cwd, linking, fallbackHint) {
|
|
|
47
56
|
throw err;
|
|
48
57
|
}
|
|
49
58
|
}
|
|
59
|
+
// Strip the legacy owner_slug if it sneaks in from an older CLI write.
|
|
60
|
+
delete raw.owner_slug;
|
|
50
61
|
const merged = {
|
|
51
62
|
...raw,
|
|
52
63
|
project_id: linking.project_id,
|
|
53
64
|
slug: linking.slug,
|
|
54
|
-
|
|
65
|
+
organization_slug: linking.organization_slug,
|
|
55
66
|
apex_hostname: linking.apex_hostname,
|
|
56
67
|
};
|
|
57
68
|
if (linking.api_url !== undefined)
|