layero 0.2.0 → 0.4.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 -0
- package/dist/bin/layero.js +37 -0
- package/dist/commands/deploy.js +40 -0
- package/dist/commands/deploys.js +146 -0
- package/dist/commands/orgs.js +24 -0
- package/package.json +1 -1
package/dist/api.js
CHANGED
|
@@ -44,6 +44,9 @@ export class ApiClient {
|
|
|
44
44
|
listProjects() {
|
|
45
45
|
return this.request("GET", "/projects");
|
|
46
46
|
}
|
|
47
|
+
listOrganizations() {
|
|
48
|
+
return this.request("GET", "/organizations");
|
|
49
|
+
}
|
|
47
50
|
getProject(idOrSlug) {
|
|
48
51
|
return this.request("GET", `/projects/${idOrSlug}`);
|
|
49
52
|
}
|
|
@@ -53,6 +56,7 @@ export class ApiClient {
|
|
|
53
56
|
slug: input.slug,
|
|
54
57
|
source_type: "cli",
|
|
55
58
|
framework_hint: input.framework_hint,
|
|
59
|
+
organization_slug: input.organization_slug,
|
|
56
60
|
});
|
|
57
61
|
}
|
|
58
62
|
initUpload(projectId) {
|
|
@@ -70,6 +74,13 @@ export class ApiClient {
|
|
|
70
74
|
pollLogs(deployId, afterId) {
|
|
71
75
|
return this.request("GET", `/deploys/${deployId}/logs?after_id=${afterId}`);
|
|
72
76
|
}
|
|
77
|
+
listProjectDeploys(projectId, branch) {
|
|
78
|
+
const qs = branch ? `?branch=${encodeURIComponent(branch)}` : "";
|
|
79
|
+
return this.request("GET", `/projects/${projectId}/deploys${qs}`);
|
|
80
|
+
}
|
|
81
|
+
rollbackProject(projectId, input) {
|
|
82
|
+
return this.request("POST", `/projects/${projectId}/rollback`, input);
|
|
83
|
+
}
|
|
73
84
|
setUsername(value) {
|
|
74
85
|
return this.request("POST", "/auth/me/username", { value });
|
|
75
86
|
}
|
package/dist/bin/layero.js
CHANGED
|
@@ -10,7 +10,9 @@ 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";
|
|
15
|
+
import { orgsListCmd } from "../commands/orgs.js";
|
|
14
16
|
// Read version from the shipped package.json (two levels up from dist/bin/).
|
|
15
17
|
const pkgPath = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..", "package.json");
|
|
16
18
|
const VERSION = JSON.parse(readFileSync(pkgPath, "utf-8")).version;
|
|
@@ -44,6 +46,40 @@ async function main() {
|
|
|
44
46
|
.command("list")
|
|
45
47
|
.description("List your projects.")
|
|
46
48
|
.action(projectsListCmd);
|
|
49
|
+
const orgs = program
|
|
50
|
+
.command("orgs")
|
|
51
|
+
.description("Layero organizations on your account (personal + teams).");
|
|
52
|
+
orgs
|
|
53
|
+
.command("list")
|
|
54
|
+
.description("Show every Layero organization you belong to.")
|
|
55
|
+
.action(orgsListCmd);
|
|
56
|
+
const deploys = program
|
|
57
|
+
.command("deploys")
|
|
58
|
+
.description("List and inspect deploys for the linked project.");
|
|
59
|
+
deploys
|
|
60
|
+
.command("list")
|
|
61
|
+
.description("List recent deploys for the project's default branch (or --branch).")
|
|
62
|
+
.option("--project <id_or_slug>", "target project (default: linked .layero/project.json)")
|
|
63
|
+
.option("--branch <name>", "branch to list deploys from (default: project's default_branch)")
|
|
64
|
+
.option("--limit <n>", "max entries to show (default 20)", (v) => Number(v))
|
|
65
|
+
.action(async (opts) => {
|
|
66
|
+
await deploysListCmd(opts);
|
|
67
|
+
});
|
|
68
|
+
program
|
|
69
|
+
.command("rollback")
|
|
70
|
+
.description("Re-activate the previous successful deploy on the project's default branch.")
|
|
71
|
+
.option("--project <id_or_slug>", "target project (default: linked .layero/project.json)")
|
|
72
|
+
.option("--branch <name>", "branch to roll back (default: project's default_branch)")
|
|
73
|
+
.option("--deploy <id_or_sha>", "explicit deploy id or commit sha prefix to roll back to")
|
|
74
|
+
.option("-y, --yes", "skip the confirmation prompt (CI)")
|
|
75
|
+
.addHelpText("after", "\nExamples:\n" +
|
|
76
|
+
" $ layero rollback # roll back default branch to previous ready deploy\n" +
|
|
77
|
+
" $ layero rollback --branch=staging # roll back the staging branch\n" +
|
|
78
|
+
" $ layero rollback --deploy=a3f9c2b # roll back to a specific commit/deploy\n" +
|
|
79
|
+
" $ layero rollback --yes # CI-friendly, no prompt")
|
|
80
|
+
.action(async (opts) => {
|
|
81
|
+
await rollbackCmd(opts);
|
|
82
|
+
});
|
|
47
83
|
program
|
|
48
84
|
.command("link <id_or_slug>")
|
|
49
85
|
.description("Link the current directory to an existing project.")
|
|
@@ -66,6 +102,7 @@ async function main() {
|
|
|
66
102
|
.option("--config", "use framework/build settings + env vars from .layero/project.json (skips the browser setup wizard)")
|
|
67
103
|
.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
104
|
.option("--branch <name>", "deploy to a specific branch's environment. Wins over --prod.")
|
|
105
|
+
.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.")
|
|
69
106
|
.addHelpText("after", "\nExamples:\n" +
|
|
70
107
|
" $ layero deploy # preview on CLI pseudo-branch (never replaces prod)\n" +
|
|
71
108
|
" $ layero deploy --prod # production deploy (interactive confirm)\n" +
|
package/dist/commands/deploy.js
CHANGED
|
@@ -100,11 +100,51 @@ async function resolveOrCreateProject(api, cwd, opts, existing) {
|
|
|
100
100
|
throw new Error("no username set on your account. open https://app.layero.ru/onboarding " +
|
|
101
101
|
"and pick one, then re-run.");
|
|
102
102
|
}
|
|
103
|
+
// Resolve target Layero organization. Three paths:
|
|
104
|
+
// * --org=slug explicit → verify membership, use it
|
|
105
|
+
// * single org → silent default (the personal account)
|
|
106
|
+
// * multiple orgs → prompt unless --yes
|
|
107
|
+
let organizationSlug = opts.org;
|
|
108
|
+
if (organizationSlug) {
|
|
109
|
+
const orgs = await api.listOrganizations();
|
|
110
|
+
if (!orgs.some((o) => o.slug === organizationSlug)) {
|
|
111
|
+
throw new Error(`you're not a member of organization "${organizationSlug}". ` +
|
|
112
|
+
`available: ${orgs.map((o) => o.slug).join(", ") || "(none)"}`);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
const orgs = await api.listOrganizations();
|
|
117
|
+
if (orgs.length === 0) {
|
|
118
|
+
throw new Error("no organization found on your account. finish onboarding at https://app.layero.ru/onboarding");
|
|
119
|
+
}
|
|
120
|
+
else if (orgs.length === 1) {
|
|
121
|
+
organizationSlug = orgs[0].slug;
|
|
122
|
+
}
|
|
123
|
+
else if (opts.yes || opts.config) {
|
|
124
|
+
// CI without --org: prefer personal, fall back to first.
|
|
125
|
+
organizationSlug =
|
|
126
|
+
orgs.find((o) => o.kind === "personal")?.slug ?? orgs[0].slug;
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
console.log(chalk.cyan("which organization?"));
|
|
130
|
+
orgs.forEach((o, i) => {
|
|
131
|
+
const tag = o.kind === "personal" ? "personal" : "team";
|
|
132
|
+
console.log(` ${i + 1}. ${o.slug} (${tag}, ${o.my_role})`);
|
|
133
|
+
});
|
|
134
|
+
const choiceRaw = await prompt("choose number", "1");
|
|
135
|
+
const idx = Number(choiceRaw) - 1;
|
|
136
|
+
if (Number.isNaN(idx) || idx < 0 || idx >= orgs.length) {
|
|
137
|
+
throw new Error(`invalid choice "${choiceRaw}"`);
|
|
138
|
+
}
|
|
139
|
+
organizationSlug = orgs[idx].slug;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
103
142
|
const fallbackName = path.basename(cwd);
|
|
104
143
|
const name = opts.name ?? (opts.yes || opts.config ? fallbackName : await prompt("project name", fallbackName));
|
|
105
144
|
const project = await api.createCliProject({
|
|
106
145
|
name,
|
|
107
146
|
framework_hint: opts.type,
|
|
147
|
+
organization_slug: organizationSlug,
|
|
108
148
|
});
|
|
109
149
|
return { project, createdNow: true };
|
|
110
150
|
}
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import { ApiClient } from "../api.js";
|
|
3
|
+
import { loadConfig } from "../config.js";
|
|
4
|
+
/** `layero orgs list` — show every Layero organization the caller belongs to.
|
|
5
|
+
*
|
|
6
|
+
* Useful before `layero deploy --org=<slug>` so the user can see which
|
|
7
|
+
* slugs to pass without leaving the terminal.
|
|
8
|
+
*/
|
|
9
|
+
export async function orgsListCmd() {
|
|
10
|
+
const cfg = await loadConfig();
|
|
11
|
+
if (!cfg.token)
|
|
12
|
+
throw new Error("not logged in. run `layero login` first.");
|
|
13
|
+
const api = new ApiClient(cfg);
|
|
14
|
+
const orgs = await api.listOrganizations();
|
|
15
|
+
if (orgs.length === 0) {
|
|
16
|
+
console.log(chalk.dim("no organizations on this account."));
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
for (const o of orgs) {
|
|
20
|
+
const kindBadge = o.kind === "personal" ? chalk.dim("personal") : chalk.cyan("team");
|
|
21
|
+
const roleBadge = chalk.dim(`(${o.my_role})`);
|
|
22
|
+
console.log(` ${chalk.bold(o.slug.padEnd(20))} ${kindBadge} ${roleBadge}`);
|
|
23
|
+
}
|
|
24
|
+
}
|