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 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
- setHandle(value) {
74
- return this.request("POST", "/me/handle", { value });
73
+ setUsername(value) {
74
+ return this.request("POST", "/auth/me/username", { value });
75
75
  }
76
- checkHandle(value) {
77
- return this.request("GET", `/me/handle/check?value=${encodeURIComponent(value)}`);
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) {
@@ -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, fail if anything is missing")
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 # uploads source, opens setup wizard in the browser\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" +
69
74
  " $ layero deploy --config # uses .layero/project.json end-to-end (CI-friendly)\n" +
70
- " $ layero deploy --type vite\n" +
71
- " $ layero deploy --project my-site --yes")
75
+ " $ layero deploy --type vite")
72
76
  .action(async (opts) => {
73
77
  await deployCmd(opts);
74
78
  });
@@ -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.handle) {
81
- throw new Error("no handle set on your account. open https://app.layero.ru/onboarding " +
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.source_type !== "cli") {
137
- throw new Error(`project "${project.slug}" is a GitHub-source project; ` +
138
- "use the dashboard's Deploy button or push to the linked repo.");
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/owner_slug/apex_hostname, and
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
- owner_slug: project.owner.slug,
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
- console.log(chalk.dim(` site: https://${project.apex_hostname} (CDN may take ~30-60s to propagate)`));
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);
@@ -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
- owner_slug: proj.owner.slug,
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`));
@@ -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 port = server.address().port;
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, handle: me.handle, email: me.email };
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.handle ?? me.email ?? me.id}`));
93
- if (!me.handle) {
94
- console.log(chalk.yellow("no handle set — open https://app.layero.ru/onboarding to pick one."));
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
  }
@@ -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, handle: me.handle, email: me.email };
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?.handle ?? cfg.user?.email ?? cfg.user?.id}`));
20
- if (!cfg.user?.handle) {
21
- console.log(chalk.yellow("no handle set — open https://app.layero.ru/onboarding to pick one " +
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
  }
@@ -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(`handle: ${me.handle ?? chalk.yellow("(not set)")}`);
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}`);
@@ -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
- return JSON.parse(raw);
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 / owner_slug / apex_hostname. Hand-edited fields
29
- * (build_cmd, output_dir, env_vars, analytics_enabled, framework_hint)
30
- * and any unknown keys the user added must be preserved verbatim, or we
31
- * silently break the user's --config file on every interactive deploy.
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. Returns the merged config the caller should treat as the new
35
- * source of truth (matters for first-deploy where there's no existing
36
- * file).
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
- owner_slug: linking.owner_slug,
65
+ organization_slug: linking.organization_slug,
55
66
  apex_hostname: linking.apex_hostname,
56
67
  };
57
68
  if (linking.api_url !== undefined)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "layero",
3
- "version": "0.1.8",
3
+ "version": "0.2.0",
4
4
  "description": "Layero CLI — publish a local site with one command.",
5
5
  "license": "MIT",
6
6
  "type": "module",