layero 0.1.8 → 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 +4 -4
- package/dist/bin/layero.js +8 -4
- package/dist/commands/deploy.js +49 -9
- 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,11 @@ export class ApiClient {
|
|
|
70
70
|
pollLogs(deployId, afterId) {
|
|
71
71
|
return this.request("GET", `/deploys/${deployId}/logs?after_id=${afterId}`);
|
|
72
72
|
}
|
|
73
|
-
|
|
74
|
-
return this.request("POST", "/me/
|
|
73
|
+
setUsername(value) {
|
|
74
|
+
return this.request("POST", "/auth/me/username", { value });
|
|
75
75
|
}
|
|
76
|
-
|
|
77
|
-
return this.request("GET", `/me/
|
|
76
|
+
checkUsername(value) {
|
|
77
|
+
return this.request("GET", `/auth/me/username/check?value=${encodeURIComponent(value)}`);
|
|
78
78
|
}
|
|
79
79
|
}
|
|
80
80
|
export async function uploadArchive(init, filePath) {
|
package/dist/bin/layero.js
CHANGED
|
@@ -62,13 +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
|
|
65
|
+
.option("-y, --yes", "non-interactive: accept defaults and skip --prod confirmation")
|
|
66
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.")
|
|
67
69
|
.addHelpText("after", "\nExamples:\n" +
|
|
68
|
-
" $ layero deploy #
|
|
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" +
|
|
69
74
|
" $ 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")
|
|
75
|
+
" $ layero deploy --type vite")
|
|
72
76
|
.action(async (opts) => {
|
|
73
77
|
await deployCmd(opts);
|
|
74
78
|
});
|
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);
|
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)
|