@levr-one/cli 0.1.0 → 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.
@@ -1,16 +1,14 @@
1
- import "./credentials-CfHLkU7k.js";
2
- import { authGetSitesV1, configureClient } from "./sdk-client-DunBmYLR.js";
1
+ import { authGetSitesV1, configureClient } from "./sdk-client-BCOB2qNU.js";
3
2
  import { loadWorkspace } from "./workspace-store-BcyMJAht.js";
4
- import "./token-refresh-waF23pyw.js";
5
- import { resolveToken } from "./resolve-token-BL8vL_ok.js";
3
+ import { resolveToken } from "./resolve-token-DizP2xRY.js";
6
4
 
7
5
  //#region src/commands/workspace/listHandler.ts
8
6
  async function listHandler() {
9
7
  let auth;
10
8
  try {
11
9
  auth = await resolveToken();
12
- } catch {
13
- this.logger.error("Not authenticated. Run 'levr auth login' first.");
10
+ } catch (err) {
11
+ this.logger.error(err instanceof Error ? err.message : "Not authenticated. Run 'levr auth login' first.");
14
12
  this.process.exitCode = 1;
15
13
  return;
16
14
  }
@@ -26,19 +24,26 @@ async function listHandler() {
26
24
  this.process.exitCode = 1;
27
25
  return;
28
26
  }
29
- const sites = result.data.sites;
27
+ const data = result.data;
28
+ printSites(this, data.sites);
29
+ }
30
+ /**
31
+ * Print the workspace list with the current-workspace indicator. Shared
32
+ * with `levr init` (ENG-2361).
33
+ */
34
+ function printSites(ctx, sites) {
30
35
  if (sites.length === 0) {
31
- this.logger.info("No workspaces available.");
36
+ ctx.logger.info("No workspaces available.");
32
37
  return;
33
38
  }
34
39
  const currentWs = loadWorkspace();
35
- this.process.stdout.write("\nWorkspaces:\n\n");
40
+ ctx.process.stdout.write("\nWorkspaces:\n\n");
36
41
  for (const site of sites) {
37
42
  const indicator = site.workspace_id === currentWs ? " *" : "";
38
- this.process.stdout.write(` ${site.workspace_name} (${site.workspace_id}) [${site.role}]${indicator}\n`);
43
+ ctx.process.stdout.write(` ${site.workspace_name} (${site.workspace_id}) [${site.role}]${indicator}\n`);
39
44
  }
40
- this.process.stdout.write("\n");
45
+ ctx.process.stdout.write("\n");
41
46
  }
42
47
 
43
48
  //#endregion
44
- export { listHandler };
49
+ export { listHandler, printSites };
@@ -1,7 +1,6 @@
1
- import { CLI_CLIENT_ID, getApiUrl, getAuthUrl, writeCredentials } from "./credentials-CfHLkU7k.js";
2
- import { configureClient } from "./sdk-client-DunBmYLR.js";
3
- import "./workspace-store-BcyMJAht.js";
4
- import { autoSelectWorkspace } from "./resolve-workspace-lHEoGPHe.js";
1
+ import { CLI_CLIENT_ID, getApiUrl, getAuthUrl, setSessionApiUrl, writeCredentials } from "./env-hpzB56ay.js";
2
+ import { configureClient } from "./sdk-client-BCOB2qNU.js";
3
+ import { autoSelectWorkspace } from "./resolve-workspace-EpjKI71z.js";
5
4
  import chalk from "chalk";
6
5
  import { createHash, randomBytes } from "node:crypto";
7
6
  import { createServer } from "node:http";
@@ -166,14 +165,19 @@ function sleep(ms) {
166
165
  */
167
166
  async function requestDeviceCode() {
168
167
  const apiUrl = getApiUrl();
169
- const res = await fetch(`${apiUrl}/v1/oauth/device/authorize`, {
170
- method: "POST",
171
- headers: { "Content-Type": "application/x-www-form-urlencoded" },
172
- body: new URLSearchParams({
173
- client_id: CLI_CLIENT_ID,
174
- scope: "read:own write:own"
175
- })
176
- });
168
+ let res;
169
+ try {
170
+ res = await fetch(`${apiUrl}/v1/oauth/device/authorize`, {
171
+ method: "POST",
172
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
173
+ body: new URLSearchParams({
174
+ client_id: CLI_CLIENT_ID,
175
+ scope: "read:own write:own"
176
+ })
177
+ });
178
+ } catch {
179
+ throw new Error(`Could not reach ${apiUrl} — check the URL and your network connection.`);
180
+ }
177
181
  if (!res.ok) {
178
182
  const body = await res.text();
179
183
  throw new Error(`Failed to request device code (${res.status}): ${body}`);
@@ -223,15 +227,31 @@ function formatCountdown(ms) {
223
227
  return `${Math.floor(totalSec / 60)}:${(totalSec % 60).toString().padStart(2, "0")}`;
224
228
  }
225
229
  async function loginHandler(flags) {
226
- if (flags["device-code"]) {
227
- await deviceCodeLogin(this);
228
- return;
229
- }
230
- await pkceLogin(this);
230
+ await performLogin(this, {
231
+ deviceCode: flags["device-code"],
232
+ url: flags.url
233
+ });
234
+ }
235
+ /**
236
+ * Run the login flow (PKCE or device code). Reusable outside the Stricli
237
+ * handler (levr init composes it). Returns true on success; on failure the
238
+ * error has been printed and `exitCode` set.
239
+ */
240
+ async function performLogin(ctx, options) {
241
+ if (options.url) setSessionApiUrl(options.url);
242
+ if (options.deviceCode) return deviceCodeLogin(ctx);
243
+ return pkceLogin(ctx);
231
244
  }
232
245
  async function pkceLogin(ctx) {
233
246
  const apiUrl = getApiUrl();
234
- const authUrl = getAuthUrl();
247
+ let authUrl;
248
+ try {
249
+ authUrl = getAuthUrl();
250
+ } catch (err) {
251
+ ctx.logger.error(err instanceof Error ? err.message : "Invalid auth server configuration.");
252
+ ctx.process.exitCode = 1;
253
+ return false;
254
+ }
235
255
  const codeVerifier = generateCodeVerifier();
236
256
  const codeChallenge = generateCodeChallenge(codeVerifier);
237
257
  const state = randomBytes(16).toString("hex");
@@ -240,7 +260,7 @@ async function pkceLogin(ctx) {
240
260
  expectedState: state
241
261
  });
242
262
  const redirectUri = `http://127.0.0.1:${await portPromise}/callback`;
243
- const authorizeUrl = `${authUrl}/auth/oauth/authorize?${new URLSearchParams({
263
+ const params = new URLSearchParams({
244
264
  response_type: "code",
245
265
  client_id: CLI_CLIENT_ID,
246
266
  redirect_uri: redirectUri,
@@ -248,7 +268,8 @@ async function pkceLogin(ctx) {
248
268
  code_challenge_method: "S256",
249
269
  scope: "read:own write:own",
250
270
  state
251
- }).toString()}`;
271
+ });
272
+ const authorizeUrl = `${authUrl}/auth/oauth/authorize?${params.toString()}`;
252
273
  ctx.process.stdout.write("Opening browser to authenticate...\n\n");
253
274
  ctx.process.stdout.write(` If the browser doesn't open, visit:\n ${chalk.cyan(authorizeUrl)}\n\n`);
254
275
  try {
@@ -270,7 +291,7 @@ async function pkceLogin(ctx) {
270
291
  ctx.process.stdout.write("\n");
271
292
  ctx.logger.error(err instanceof Error ? err.message : "Authentication failed.");
272
293
  ctx.process.exitCode = 1;
273
- return;
294
+ return false;
274
295
  }
275
296
  clearInterval(countdownTimer);
276
297
  try {
@@ -289,11 +310,12 @@ async function pkceLogin(ctx) {
289
310
  const errBody = await tokenRes.text();
290
311
  throw new Error(`Token exchange failed (${tokenRes.status}): ${errBody}`);
291
312
  }
292
- await saveTokensAndFinish(ctx, await tokenRes.json());
313
+ return await saveTokensAndFinish(ctx, await tokenRes.json());
293
314
  } catch (err) {
294
315
  ctx.process.stdout.write("\n");
295
316
  ctx.logger.error(err instanceof Error ? err.message : "Token exchange failed.");
296
317
  ctx.process.exitCode = 1;
318
+ return false;
297
319
  }
298
320
  }
299
321
  async function deviceCodeLogin(ctx) {
@@ -319,11 +341,12 @@ async function deviceCodeLogin(ctx) {
319
341
  throw err;
320
342
  }
321
343
  clearInterval(deviceTimer);
322
- await saveTokensAndFinish(ctx, tokenData);
344
+ return await saveTokensAndFinish(ctx, tokenData);
323
345
  } catch (err) {
324
346
  ctx.process.stdout.write("\n");
325
347
  ctx.logger.error(err instanceof Error ? err.message : "Device flow failed.");
326
348
  ctx.process.exitCode = 1;
349
+ return false;
327
350
  }
328
351
  }
329
352
  async function saveTokensAndFinish(ctx, tokenData) {
@@ -337,7 +360,7 @@ async function saveTokensAndFinish(ctx, tokenData) {
337
360
  ctx.process.stdout.write("\n");
338
361
  ctx.logger.error("Token response missing user data.");
339
362
  ctx.process.exitCode = 1;
340
- return;
363
+ return false;
341
364
  }
342
365
  writeCredentials({
343
366
  version: 1,
@@ -357,7 +380,8 @@ async function saveTokensAndFinish(ctx, tokenData) {
357
380
  const wsResult = await autoSelectWorkspace();
358
381
  if (wsResult.kind === "single") ctx.process.stdout.write(`Workspace: ${chalk.bold(wsResult.workspaceName)}\n`);
359
382
  else if (wsResult.kind === "multiple") ctx.process.stdout.write(`\n${wsResult.count} workspaces available. Run ${chalk.cyan("'levr workspace list'")} to see them.\n`);
383
+ return true;
360
384
  }
361
385
 
362
386
  //#endregion
363
- export { loginHandler };
387
+ export { loginHandler, performLogin };
@@ -0,0 +1,7 @@
1
+ import { loginHandler, performLogin } from "./loginHandler-BSrs0mHq.js";
2
+ import "./env-hpzB56ay.js";
3
+ import "./sdk-client-BCOB2qNU.js";
4
+ import "./workspace-store-BcyMJAht.js";
5
+ import "./resolve-workspace-EpjKI71z.js";
6
+
7
+ export { loginHandler };
@@ -1,4 +1,4 @@
1
- import { deleteCredentials, getPatToken } from "./credentials-CfHLkU7k.js";
1
+ import { deleteCredentials, getPatToken } from "./env-hpzB56ay.js";
2
2
  import { clearWorkspace } from "./workspace-store-BcyMJAht.js";
3
3
 
4
4
  //#region src/commands/auth/logoutHandler.ts
@@ -1,9 +1,9 @@
1
- import { getApiUrl, getAutomationSourceIdOverride, getSourceOverride, getTeamId } from "./credentials-CfHLkU7k.js";
2
- import { client, configureClient, uploadAutomationIngest, uploadImport } from "./sdk-client-DunBmYLR.js";
1
+ import { getApiUrl, getAutomationSourceIdOverride, getSourceOverride, getTeamId } from "./env-hpzB56ay.js";
2
+ import { client, configureClient, uploadAutomationIngest, uploadImport } from "./sdk-client-BCOB2qNU.js";
3
3
  import "./workspace-store-BcyMJAht.js";
4
- import { resolveWorkspace } from "./resolve-workspace-lHEoGPHe.js";
5
- import "./token-refresh-waF23pyw.js";
6
- import { resolveToken } from "./resolve-token-BL8vL_ok.js";
4
+ import { resolveWorkspace } from "./resolve-workspace-EpjKI71z.js";
5
+ import "./token-refresh-BYu4XO3G.js";
6
+ import { resolveToken } from "./resolve-token-DizP2xRY.js";
7
7
  import { readFileSync, statSync } from "node:fs";
8
8
  import { basename } from "node:path";
9
9
  import ora from "ora";
@@ -0,0 +1,48 @@
1
+ import { getApiUrl, getPatToken, readCredentials } from "./env-hpzB56ay.js";
2
+ import { isTokenExpired, refreshToken } from "./token-refresh-BYu4XO3G.js";
3
+
4
+ //#region src/auth/resolve-token.ts
5
+ /**
6
+ * Stored credentials exist but were issued by a different server than the
7
+ * one this command targets (`--url`/`LEVR_URL`). Callers that can recover
8
+ * (e.g. `levr init` re-logging-in against the new target) match on this
9
+ * class instead of string-sniffing the message.
10
+ */
11
+ var CredentialsMismatchError = class extends Error {
12
+ constructor(storedUrl, activeUrl) {
13
+ super(`Stored credentials are for ${storedUrl}, but this command targets ${activeUrl}. Run 'levr auth login' to authenticate against this server.`);
14
+ this.name = "CredentialsMismatchError";
15
+ }
16
+ };
17
+ /**
18
+ * Resolve auth token in priority order:
19
+ * 1. LEVR_TOKEN env var (PAT) — long-lived, no refresh
20
+ * 2. Stored credentials (JWT) — auto-refresh if expired
21
+ * 3. Error — not authenticated
22
+ */
23
+ async function resolveToken() {
24
+ const pat = getPatToken();
25
+ if (pat) return {
26
+ token: pat,
27
+ type: "pat"
28
+ };
29
+ let creds = readCredentials();
30
+ if (creds) {
31
+ const activeUrl = getApiUrl();
32
+ const storedUrl = creds.api_url.replace(/\/+$/, "");
33
+ if (storedUrl !== activeUrl) throw new CredentialsMismatchError(storedUrl, activeUrl);
34
+ if (isTokenExpired(creds)) {
35
+ const refreshed = await refreshToken(creds);
36
+ if (!refreshed) throw new Error("Token expired and refresh failed. Run 'levr auth login' to re-authenticate.");
37
+ creds = refreshed;
38
+ }
39
+ return {
40
+ token: creds.access_token,
41
+ type: "jwt"
42
+ };
43
+ }
44
+ throw new Error("Not authenticated. Run 'levr auth login' or set LEVR_TOKEN environment variable.");
45
+ }
46
+
47
+ //#endregion
48
+ export { CredentialsMismatchError, resolveToken };
@@ -1,4 +1,4 @@
1
- import { authGetSitesV1 } from "./sdk-client-DunBmYLR.js";
1
+ import { authGetSitesV1 } from "./sdk-client-BCOB2qNU.js";
2
2
  import { clearWorkspace, loadWorkspace, saveWorkspace } from "./workspace-store-BcyMJAht.js";
3
3
 
4
4
  //#region src/workspace/resolve-workspace.ts
@@ -1,4 +1,4 @@
1
- import { getApiUrl } from "./credentials-CfHLkU7k.js";
1
+ import { getApiUrl } from "./env-hpzB56ay.js";
2
2
  import { z } from "zod/v3";
3
3
 
4
4
  //#region ../sdk/dist/gen/_common/zod.js
@@ -1,8 +1,8 @@
1
- import "./credentials-CfHLkU7k.js";
2
- import { authGetSitesV1, configureClient } from "./sdk-client-DunBmYLR.js";
1
+ import "./env-hpzB56ay.js";
2
+ import { authGetSitesV1, configureClient } from "./sdk-client-BCOB2qNU.js";
3
3
  import { saveWorkspace } from "./workspace-store-BcyMJAht.js";
4
- import "./token-refresh-waF23pyw.js";
5
- import { resolveToken } from "./resolve-token-BL8vL_ok.js";
4
+ import "./token-refresh-BYu4XO3G.js";
5
+ import { resolveToken } from "./resolve-token-DizP2xRY.js";
6
6
 
7
7
  //#region src/commands/workspace/selectHandler.ts
8
8
  async function selectHandler(_flags, workspaceId) {
@@ -1,6 +1,6 @@
1
- import { getApiUrl, getPatToken, readCredentials } from "./credentials-CfHLkU7k.js";
2
- import { authGetProfileV1, configureClient } from "./sdk-client-DunBmYLR.js";
3
- import { isTokenExpired } from "./token-refresh-waF23pyw.js";
1
+ import { getApiUrl, getPatToken, readCredentials } from "./env-hpzB56ay.js";
2
+ import { authGetProfileV1, configureClient } from "./sdk-client-BCOB2qNU.js";
3
+ import { isTokenExpired } from "./token-refresh-BYu4XO3G.js";
4
4
  import chalk from "chalk";
5
5
 
6
6
  //#region src/commands/auth/statusHandler.ts
@@ -23,6 +23,12 @@ async function statusHandler() {
23
23
  this.process.exitCode = 1;
24
24
  return;
25
25
  }
26
+ const storedUrl = creds.api_url.replace(/\/+$/, "");
27
+ if (storedUrl !== apiUrl) {
28
+ this.process.stdout.write(`${chalk.red("error")} Stored credentials are for ${storedUrl}, but the current target is ${apiUrl}. Run 'levr auth login' to authenticate against this server.\n`);
29
+ this.process.exitCode = 1;
30
+ return;
31
+ }
26
32
  if (isTokenExpired(creds)) {
27
33
  this.process.stdout.write(`${chalk.red("error")} Token expired. Run 'levr auth login' to re-authenticate.\n`);
28
34
  this.process.exitCode = 1;
@@ -1,4 +1,4 @@
1
- import { CLI_CLIENT_ID, deleteCredentials, getApiUrl, writeCredentials } from "./credentials-CfHLkU7k.js";
1
+ import { CLI_CLIENT_ID, deleteCredentials, getApiUrl, writeCredentials } from "./env-hpzB56ay.js";
2
2
 
3
3
  //#region src/auth/token-refresh.ts
4
4
  const REFRESH_BUFFER_MS = 300 * 1e3;
package/package.json CHANGED
@@ -1,12 +1,11 @@
1
1
  {
2
2
  "name": "@levr-one/cli",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "The command-line interface for Levr",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "main": "dist/cli.js",
8
8
  "bin": {
9
- "__levr_bash_complete": "dist/bash-complete.js",
10
9
  "levr": "dist/cli.js"
11
10
  },
12
11
  "files": [