layero 0.2.0 → 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 +7 -0
- package/dist/bin/layero.js +28 -0
- package/dist/commands/deploys.js +146 -0
- package/package.json +1 -1
package/dist/api.js
CHANGED
|
@@ -70,6 +70,13 @@ export class ApiClient {
|
|
|
70
70
|
pollLogs(deployId, afterId) {
|
|
71
71
|
return this.request("GET", `/deploys/${deployId}/logs?after_id=${afterId}`);
|
|
72
72
|
}
|
|
73
|
+
listProjectDeploys(projectId, branch) {
|
|
74
|
+
const qs = branch ? `?branch=${encodeURIComponent(branch)}` : "";
|
|
75
|
+
return this.request("GET", `/projects/${projectId}/deploys${qs}`);
|
|
76
|
+
}
|
|
77
|
+
rollbackProject(projectId, input) {
|
|
78
|
+
return this.request("POST", `/projects/${projectId}/rollback`, input);
|
|
79
|
+
}
|
|
73
80
|
setUsername(value) {
|
|
74
81
|
return this.request("POST", "/auth/me/username", { value });
|
|
75
82
|
}
|
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.")
|
|
@@ -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
|
+
}
|