@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.
- package/README.md +20 -8
- package/dist/cli.js +432 -9
- package/dist/env-hpzB56ay.js +102 -0
- package/dist/initHandler-JN92u8td.js +51 -0
- package/dist/listHandler-5L3YE52T.js +8 -0
- package/dist/{listHandler-BiwIo88i.js → listHandler-DDd6scZ8.js} +17 -12
- package/dist/{loginHandler-C8D6DgjW.js → loginHandler-BSrs0mHq.js} +49 -25
- package/dist/loginHandler-uZCrwdcR.js +7 -0
- package/dist/{logoutHandler-BBtdpxHX.js → logoutHandler-jWPGhzls.js} +1 -1
- package/dist/{pushHandler-DkCrbV7y.js → pushHandler-DG96IOVd.js} +5 -5
- package/dist/resolve-token-DizP2xRY.js +48 -0
- package/dist/{resolve-workspace-lHEoGPHe.js → resolve-workspace-EpjKI71z.js} +1 -1
- package/dist/{sdk-client-DunBmYLR.js → sdk-client-BCOB2qNU.js} +1 -1
- package/dist/{selectHandler-C2UDAWOl.js → selectHandler-CkIbO2qH.js} +4 -4
- package/dist/{statusHandler-DQDmEOGI.js → statusHandler-D6YAxhoN.js} +9 -3
- package/dist/{token-refresh-waF23pyw.js → token-refresh-BYu4XO3G.js} +1 -1
- package/package.json +1 -2
- package/dist/app-0F0Zyyt7.js +0 -350
- package/dist/bash-complete.js +0 -13
- package/dist/credentials-CfHLkU7k.js +0 -54
- package/dist/resolve-token-BL8vL_ok.js +0 -33
- /package/dist/{currentHandler-oXFeNNgd.js → currentHandler-Dzg819Np.js} +0 -0
|
@@ -1,16 +1,14 @@
|
|
|
1
|
-
import "./
|
|
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-
|
|
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
|
|
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
|
-
|
|
36
|
+
ctx.logger.info("No workspaces available.");
|
|
32
37
|
return;
|
|
33
38
|
}
|
|
34
39
|
const currentWs = loadWorkspace();
|
|
35
|
-
|
|
40
|
+
ctx.process.stdout.write("\nWorkspaces:\n\n");
|
|
36
41
|
for (const site of sites) {
|
|
37
42
|
const indicator = site.workspace_id === currentWs ? " *" : "";
|
|
38
|
-
|
|
43
|
+
ctx.process.stdout.write(` ${site.workspace_name} (${site.workspace_id}) [${site.role}]${indicator}\n`);
|
|
39
44
|
}
|
|
40
|
-
|
|
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 "./
|
|
2
|
-
import { configureClient } from "./sdk-client-
|
|
3
|
-
import "./workspace-
|
|
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
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
}
|
|
230
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
})
|
|
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 };
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { getApiUrl, getAutomationSourceIdOverride, getSourceOverride, getTeamId } from "./
|
|
2
|
-
import { client, configureClient, uploadAutomationIngest, uploadImport } from "./sdk-client-
|
|
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-
|
|
5
|
-
import "./token-refresh-
|
|
6
|
-
import { resolveToken } from "./resolve-token-
|
|
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,8 +1,8 @@
|
|
|
1
|
-
import "./
|
|
2
|
-
import { authGetSitesV1, configureClient } from "./sdk-client-
|
|
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-
|
|
5
|
-
import { resolveToken } from "./resolve-token-
|
|
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 "./
|
|
2
|
-
import { authGetProfileV1, configureClient } from "./sdk-client-
|
|
3
|
-
import { isTokenExpired } from "./token-refresh-
|
|
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 "./
|
|
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.
|
|
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": [
|