@levr-one/cli 0.1.1 → 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 +62 -13
- package/dist/{currentHandler-DpWIqCBm.js → currentHandler-Dzg819Np.js} +1 -1
- package/dist/env-hpzB56ay.js +102 -0
- package/dist/initHandler-JN92u8td.js +51 -0
- package/dist/listHandler-5L3YE52T.js +8 -0
- package/dist/listHandler-DDd6scZ8.js +49 -0
- package/dist/{loginHandler-BnokItPo.js → loginHandler-BSrs0mHq.js} +49 -25
- package/dist/loginHandler-uZCrwdcR.js +7 -0
- package/dist/{logoutHandler-C3X9rqLP.js → logoutHandler-jWPGhzls.js} +2 -2
- package/dist/{pushHandler-C3DteESD.js → pushHandler-DG96IOVd.js} +6 -6
- package/dist/resolve-token-DizP2xRY.js +48 -0
- package/dist/{resolve-workspace-BPMkU_SK.js → resolve-workspace-EpjKI71z.js} +2 -2
- package/dist/{sdk-client-rpUEykDw.js → sdk-client-BCOB2qNU.js} +1 -1
- package/dist/{selectHandler-BjM4LKYW.js → selectHandler-CkIbO2qH.js} +5 -5
- package/dist/{statusHandler-LfrSN4uh.js → statusHandler-D6YAxhoN.js} +9 -3
- package/dist/{token-refresh-Pl6Adngb.js → token-refresh-BYu4XO3G.js} +1 -1
- package/package.json +1 -1
- package/dist/credentials-Ciq9mA7N.js +0 -54
- package/dist/listHandler-gfnJbj60.js +0 -44
- package/dist/resolve-token-CQU55U6J.js +0 -33
- /package/dist/{workspace-store-4hfvsEHS.js → workspace-store-BcyMJAht.js} +0 -0
package/README.md
CHANGED
|
@@ -12,7 +12,19 @@ npm install -g @levr-one/cli # global install for daily use
|
|
|
12
12
|
npx @levr-one/cli --help
|
|
13
13
|
```
|
|
14
14
|
|
|
15
|
-
The package is self-contained — no peer setup required.
|
|
15
|
+
The package is self-contained — no peer setup required. A global install
|
|
16
|
+
replaces the `levr` bin from the deprecated `@levr-one/setup` package.
|
|
17
|
+
|
|
18
|
+
## Get started
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
npx @levr-one/cli init # authenticate and list your workspaces
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
`levr init` is the first-run onboarding command: it logs you in (browser
|
|
25
|
+
PKCE by default, `--device-code` for SSH/headless) and lists the workspaces
|
|
26
|
+
you can push to. Re-running it reuses your stored session — it never
|
|
27
|
+
re-opens the browser unnecessarily.
|
|
16
28
|
|
|
17
29
|
### Shell completion (optional)
|
|
18
30
|
|
|
@@ -149,13 +161,13 @@ withEnv(["LEVR_TOKEN=${LEVR_TOKEN}"]) {
|
|
|
149
161
|
|
|
150
162
|
All configuration is via environment variables. Flags take precedence.
|
|
151
163
|
|
|
152
|
-
| Variable | Description | Default
|
|
153
|
-
| --------------- | --------------------------------------------------------------------------------------- |
|
|
154
|
-
| `LEVR_TOKEN` | Personal Access Token (for CI / headless) |
|
|
155
|
-
| `LEVR_URL` | API base URL
|
|
156
|
-
| `LEVR_AUTH_URL` | Auth server URL
|
|
157
|
-
| `LEVR_TEAM_ID` | Default team ID (optional; server resolves from automation source or workspace default) |
|
|
158
|
-
| `LEVR_SOURCE` | Automation source name override (groups imports, remembers team) |
|
|
164
|
+
| Variable | Description | Default |
|
|
165
|
+
| --------------- | --------------------------------------------------------------------------------------- | ---------------------- |
|
|
166
|
+
| `LEVR_TOKEN` | Personal Access Token (for CI / headless) | |
|
|
167
|
+
| `LEVR_URL` | API base URL (`--url` flag > `LEVR_URL` > URL stored at login > default) | `https://api.levr.one` |
|
|
168
|
+
| `LEVR_AUTH_URL` | Auth server URL for the browser login page (derived from the API URL when unset) | derived |
|
|
169
|
+
| `LEVR_TEAM_ID` | Default team ID (optional; server resolves from automation source or workspace default) | |
|
|
170
|
+
| `LEVR_SOURCE` | Automation source name override (groups imports, remembers team) | |
|
|
159
171
|
|
|
160
172
|
## Troubleshooting
|
|
161
173
|
|
package/dist/cli.js
CHANGED
|
@@ -52,6 +52,45 @@ function buildContext(process$1) {
|
|
|
52
52
|
};
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
+
//#endregion
|
|
56
|
+
//#region src/commands/init.ts
|
|
57
|
+
const initCommand = buildCommand({
|
|
58
|
+
docs: {
|
|
59
|
+
brief: "First-run setup: authenticate and list your workspaces",
|
|
60
|
+
fullDescription: `Set up the Levr CLI: authenticate (if needed) and list your workspaces.
|
|
61
|
+
|
|
62
|
+
Reuses an existing session when one is stored — running init again never
|
|
63
|
+
re-opens the browser unnecessarily. For headless environments (SSH,
|
|
64
|
+
containers), use --device-code.
|
|
65
|
+
|
|
66
|
+
Examples:
|
|
67
|
+
npx @levr-one/cli init # First-run onboarding
|
|
68
|
+
levr init --device-code # Headless/SSH onboarding
|
|
69
|
+
levr init --url <api-url> # Target a non-default API server`
|
|
70
|
+
},
|
|
71
|
+
parameters: {
|
|
72
|
+
flags: {
|
|
73
|
+
"device-code": {
|
|
74
|
+
kind: "boolean",
|
|
75
|
+
default: false,
|
|
76
|
+
brief: "Use device code flow (for SSH/headless environments)"
|
|
77
|
+
},
|
|
78
|
+
url: {
|
|
79
|
+
kind: "parsed",
|
|
80
|
+
parse: String,
|
|
81
|
+
brief: "API base URL (default: https://api.levr.one)",
|
|
82
|
+
placeholder: "url",
|
|
83
|
+
optional: true
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
aliases: { d: "device-code" }
|
|
87
|
+
},
|
|
88
|
+
loader: async () => {
|
|
89
|
+
const { initHandler } = await import("./initHandler-JN92u8td.js");
|
|
90
|
+
return initHandler;
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
|
|
55
94
|
//#endregion
|
|
56
95
|
//#region src/commands/auth/login.ts
|
|
57
96
|
const loginCommand = buildCommand({
|
|
@@ -68,15 +107,24 @@ Examples:
|
|
|
68
107
|
levr auth login --device-code # Device flow for SSH/headless`
|
|
69
108
|
},
|
|
70
109
|
parameters: {
|
|
71
|
-
flags: {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
110
|
+
flags: {
|
|
111
|
+
"device-code": {
|
|
112
|
+
kind: "boolean",
|
|
113
|
+
default: false,
|
|
114
|
+
brief: "Use device code flow (for SSH/headless environments)"
|
|
115
|
+
},
|
|
116
|
+
url: {
|
|
117
|
+
kind: "parsed",
|
|
118
|
+
parse: String,
|
|
119
|
+
brief: "API base URL (default: https://api.levr.one)",
|
|
120
|
+
placeholder: "url",
|
|
121
|
+
optional: true
|
|
122
|
+
}
|
|
123
|
+
},
|
|
76
124
|
aliases: { d: "device-code" }
|
|
77
125
|
},
|
|
78
126
|
loader: async () => {
|
|
79
|
-
const { loginHandler } = await import("./loginHandler-
|
|
127
|
+
const { loginHandler } = await import("./loginHandler-uZCrwdcR.js");
|
|
80
128
|
return loginHandler;
|
|
81
129
|
}
|
|
82
130
|
});
|
|
@@ -95,7 +143,7 @@ Examples:
|
|
|
95
143
|
},
|
|
96
144
|
parameters: {},
|
|
97
145
|
loader: async () => {
|
|
98
|
-
const { logoutHandler } = await import("./logoutHandler-
|
|
146
|
+
const { logoutHandler } = await import("./logoutHandler-jWPGhzls.js");
|
|
99
147
|
return logoutHandler;
|
|
100
148
|
}
|
|
101
149
|
});
|
|
@@ -115,7 +163,7 @@ Examples:
|
|
|
115
163
|
},
|
|
116
164
|
parameters: {},
|
|
117
165
|
loader: async () => {
|
|
118
|
-
const { statusHandler } = await import("./statusHandler-
|
|
166
|
+
const { statusHandler } = await import("./statusHandler-D6YAxhoN.js");
|
|
119
167
|
return statusHandler;
|
|
120
168
|
}
|
|
121
169
|
});
|
|
@@ -226,7 +274,7 @@ Examples:
|
|
|
226
274
|
}
|
|
227
275
|
},
|
|
228
276
|
loader: async () => {
|
|
229
|
-
const { pushHandler } = await import("./pushHandler-
|
|
277
|
+
const { pushHandler } = await import("./pushHandler-DG96IOVd.js");
|
|
230
278
|
return pushHandler;
|
|
231
279
|
}
|
|
232
280
|
});
|
|
@@ -247,7 +295,7 @@ Examples:
|
|
|
247
295
|
},
|
|
248
296
|
parameters: {},
|
|
249
297
|
loader: async () => {
|
|
250
|
-
const { listHandler } = await import("./listHandler-
|
|
298
|
+
const { listHandler } = await import("./listHandler-5L3YE52T.js");
|
|
251
299
|
return listHandler;
|
|
252
300
|
}
|
|
253
301
|
});
|
|
@@ -280,7 +328,7 @@ Examples:
|
|
|
280
328
|
flags: {}
|
|
281
329
|
},
|
|
282
330
|
loader: async () => {
|
|
283
|
-
const { selectHandler } = await import("./selectHandler-
|
|
331
|
+
const { selectHandler } = await import("./selectHandler-CkIbO2qH.js");
|
|
284
332
|
return selectHandler;
|
|
285
333
|
}
|
|
286
334
|
});
|
|
@@ -297,19 +345,20 @@ Examples:
|
|
|
297
345
|
},
|
|
298
346
|
parameters: {},
|
|
299
347
|
loader: async () => {
|
|
300
|
-
const { currentHandler } = await import("./currentHandler-
|
|
348
|
+
const { currentHandler } = await import("./currentHandler-Dzg819Np.js");
|
|
301
349
|
return currentHandler;
|
|
302
350
|
}
|
|
303
351
|
});
|
|
304
352
|
|
|
305
353
|
//#endregion
|
|
306
354
|
//#region package.json
|
|
307
|
-
var version = "0.
|
|
355
|
+
var version = "0.2.0";
|
|
308
356
|
|
|
309
357
|
//#endregion
|
|
310
358
|
//#region src/app.ts
|
|
311
359
|
const routes = buildRouteMap({
|
|
312
360
|
routes: {
|
|
361
|
+
init: initCommand,
|
|
313
362
|
auth: buildRouteMap({
|
|
314
363
|
routes: {
|
|
315
364
|
login: loginCommand,
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
|
|
5
|
+
//#region src/auth/credentials.ts
|
|
6
|
+
const CREDENTIALS_PATH = join(join(homedir(), ".config", "levr"), "credentials.json");
|
|
7
|
+
function readCredentials() {
|
|
8
|
+
try {
|
|
9
|
+
const raw = readFileSync(CREDENTIALS_PATH, "utf8");
|
|
10
|
+
return JSON.parse(raw);
|
|
11
|
+
} catch {
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
function writeCredentials(creds) {
|
|
16
|
+
mkdirSync(dirname(CREDENTIALS_PATH), { recursive: true });
|
|
17
|
+
writeFileSync(CREDENTIALS_PATH, JSON.stringify(creds, null, 2), { mode: 384 });
|
|
18
|
+
}
|
|
19
|
+
function deleteCredentials() {
|
|
20
|
+
try {
|
|
21
|
+
unlinkSync(CREDENTIALS_PATH);
|
|
22
|
+
return true;
|
|
23
|
+
} catch {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
//#endregion
|
|
29
|
+
//#region src/utils/env.ts
|
|
30
|
+
const DEFAULT_API_URL = "https://api.levr.one";
|
|
31
|
+
const KNOWN_AUTH_HOSTS = {
|
|
32
|
+
"api.levr.one": "auth.levr.one",
|
|
33
|
+
"api.levr.one": "auth.levr.one"
|
|
34
|
+
};
|
|
35
|
+
/** OAuth client ID for the CLI (seeded in oauth_clients table) */
|
|
36
|
+
const CLI_CLIENT_ID = "3";
|
|
37
|
+
let sessionApiUrl;
|
|
38
|
+
function setSessionApiUrl(url) {
|
|
39
|
+
sessionApiUrl = normalizeUrl(url);
|
|
40
|
+
}
|
|
41
|
+
function normalizeUrl(url) {
|
|
42
|
+
return url.replace(/\/+$/, "");
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Resolve the API base URL: `--url` flag > `LEVR_URL` env var > the
|
|
46
|
+
* `api_url` stored in credentials at login > production default.
|
|
47
|
+
*
|
|
48
|
+
* The stored-credentials fallback keeps every later command (push, refresh,
|
|
49
|
+
* workspace list) pointed at the environment the user actually logged into,
|
|
50
|
+
* instead of silently falling back to production when the env var is no
|
|
51
|
+
* longer exported in the current shell.
|
|
52
|
+
*/
|
|
53
|
+
function getApiUrl() {
|
|
54
|
+
if (sessionApiUrl) return sessionApiUrl;
|
|
55
|
+
const env = process.env["LEVR_URL"];
|
|
56
|
+
if (env) return normalizeUrl(env);
|
|
57
|
+
if (!getPatToken()) {
|
|
58
|
+
const stored = readCredentials()?.api_url;
|
|
59
|
+
if (stored) return normalizeUrl(stored);
|
|
60
|
+
}
|
|
61
|
+
return DEFAULT_API_URL;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Resolve the auth-server base URL (browser PKCE consent page only):
|
|
65
|
+
* `LEVR_AUTH_URL` env var > derived from the resolved API URL for known
|
|
66
|
+
* Levr hosts. Unrecognized hosts (e.g. localhost dev stacks) require the
|
|
67
|
+
* explicit env var — throwing beats silently opening the production login
|
|
68
|
+
* page for a token exchange that can never succeed.
|
|
69
|
+
*/
|
|
70
|
+
function getAuthUrl() {
|
|
71
|
+
const env = process.env["LEVR_AUTH_URL"];
|
|
72
|
+
if (env) return normalizeUrl(env);
|
|
73
|
+
const apiUrl = getApiUrl();
|
|
74
|
+
const derived = deriveAuthUrl(apiUrl);
|
|
75
|
+
if (derived) return derived;
|
|
76
|
+
throw new Error(`Cannot derive the auth server URL from API URL "${apiUrl}". Set LEVR_AUTH_URL to the auth server base URL (e.g. https://auth.levr.one).`);
|
|
77
|
+
}
|
|
78
|
+
function deriveAuthUrl(apiUrl) {
|
|
79
|
+
let parsed;
|
|
80
|
+
try {
|
|
81
|
+
parsed = new URL(apiUrl);
|
|
82
|
+
} catch {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
const authHost = KNOWN_AUTH_HOSTS[parsed.host];
|
|
86
|
+
return authHost ? `${parsed.protocol}//${authHost}` : void 0;
|
|
87
|
+
}
|
|
88
|
+
function getTeamId(flagValue) {
|
|
89
|
+
return flagValue || process.env["LEVR_TEAM_ID"] || void 0;
|
|
90
|
+
}
|
|
91
|
+
function getPatToken() {
|
|
92
|
+
return process.env["LEVR_TOKEN"];
|
|
93
|
+
}
|
|
94
|
+
function getSourceOverride() {
|
|
95
|
+
return process.env["LEVR_SOURCE"];
|
|
96
|
+
}
|
|
97
|
+
function getAutomationSourceIdOverride() {
|
|
98
|
+
return process.env["LEVR_AUTOMATION_SOURCE_ID"];
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
//#endregion
|
|
102
|
+
export { CLI_CLIENT_ID, deleteCredentials, getApiUrl, getAuthUrl, getAutomationSourceIdOverride, getPatToken, getSourceOverride, getTeamId, readCredentials, setSessionApiUrl, writeCredentials };
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { performLogin } from "./loginHandler-BSrs0mHq.js";
|
|
2
|
+
import { setSessionApiUrl } from "./env-hpzB56ay.js";
|
|
3
|
+
import { authGetSitesV1, configureClient } from "./sdk-client-BCOB2qNU.js";
|
|
4
|
+
import "./workspace-store-BcyMJAht.js";
|
|
5
|
+
import "./resolve-workspace-EpjKI71z.js";
|
|
6
|
+
import "./token-refresh-BYu4XO3G.js";
|
|
7
|
+
import { CredentialsMismatchError, resolveToken } from "./resolve-token-DizP2xRY.js";
|
|
8
|
+
import { printSites } from "./listHandler-DDd6scZ8.js";
|
|
9
|
+
import chalk from "chalk";
|
|
10
|
+
|
|
11
|
+
//#region src/commands/initHandler.ts
|
|
12
|
+
/**
|
|
13
|
+
* First-run onboarding: authenticate if needed, then list workspaces.
|
|
14
|
+
* Composes the existing login flow (loginHandler's performLogin) with the
|
|
15
|
+
* workspace listing (listHandler's printSites) — absorbs the retired
|
|
16
|
+
* @levr-one/setup flow (ENG-2361).
|
|
17
|
+
*/
|
|
18
|
+
async function initHandler(flags) {
|
|
19
|
+
if (flags.url) setSessionApiUrl(flags.url);
|
|
20
|
+
let auth;
|
|
21
|
+
try {
|
|
22
|
+
auth = await resolveToken();
|
|
23
|
+
this.logger.success("Already authenticated.");
|
|
24
|
+
} catch (resolveError) {
|
|
25
|
+
if (resolveError instanceof CredentialsMismatchError) this.logger.warning(resolveError.message);
|
|
26
|
+
if (!await performLogin(this, { deviceCode: flags["device-code"] })) return;
|
|
27
|
+
try {
|
|
28
|
+
auth = await resolveToken();
|
|
29
|
+
} catch (error) {
|
|
30
|
+
this.logger.error(error instanceof Error ? error.message : "Authentication failed.");
|
|
31
|
+
this.process.exitCode = 1;
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
if (auth.type === "pat") {
|
|
36
|
+
this.logger.info("Authenticated via LEVR_TOKEN (PAT). Workspace listing requires JWT auth — run 'levr auth login' to browse workspaces.");
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
configureClient(auth);
|
|
40
|
+
const result = await authGetSitesV1();
|
|
41
|
+
if (result.error) {
|
|
42
|
+
this.logger.error("Failed to list workspaces.");
|
|
43
|
+
this.process.exitCode = 1;
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
printSites(this, result.data.sites);
|
|
47
|
+
this.process.stdout.write(`Next: ${chalk.cyan("'levr workspace select <id>'")} to pick a workspace, then ${chalk.cyan("'levr push <file>'")} to upload results.\n`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
//#endregion
|
|
51
|
+
export { initHandler };
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import "./env-hpzB56ay.js";
|
|
2
|
+
import "./sdk-client-BCOB2qNU.js";
|
|
3
|
+
import "./workspace-store-BcyMJAht.js";
|
|
4
|
+
import "./token-refresh-BYu4XO3G.js";
|
|
5
|
+
import "./resolve-token-DizP2xRY.js";
|
|
6
|
+
import { listHandler, printSites } from "./listHandler-DDd6scZ8.js";
|
|
7
|
+
|
|
8
|
+
export { listHandler };
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { authGetSitesV1, configureClient } from "./sdk-client-BCOB2qNU.js";
|
|
2
|
+
import { loadWorkspace } from "./workspace-store-BcyMJAht.js";
|
|
3
|
+
import { resolveToken } from "./resolve-token-DizP2xRY.js";
|
|
4
|
+
|
|
5
|
+
//#region src/commands/workspace/listHandler.ts
|
|
6
|
+
async function listHandler() {
|
|
7
|
+
let auth;
|
|
8
|
+
try {
|
|
9
|
+
auth = await resolveToken();
|
|
10
|
+
} catch (err) {
|
|
11
|
+
this.logger.error(err instanceof Error ? err.message : "Not authenticated. Run 'levr auth login' first.");
|
|
12
|
+
this.process.exitCode = 1;
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
if (auth.type === "pat") {
|
|
16
|
+
this.logger.error("Workspace listing requires JWT auth. Run 'levr auth login'.");
|
|
17
|
+
this.process.exitCode = 1;
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
configureClient(auth);
|
|
21
|
+
const result = await authGetSitesV1();
|
|
22
|
+
if (result.error) {
|
|
23
|
+
this.logger.error("Failed to list workspaces.");
|
|
24
|
+
this.process.exitCode = 1;
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
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) {
|
|
35
|
+
if (sites.length === 0) {
|
|
36
|
+
ctx.logger.info("No workspaces available.");
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
const currentWs = loadWorkspace();
|
|
40
|
+
ctx.process.stdout.write("\nWorkspaces:\n\n");
|
|
41
|
+
for (const site of sites) {
|
|
42
|
+
const indicator = site.workspace_id === currentWs ? " *" : "";
|
|
43
|
+
ctx.process.stdout.write(` ${site.workspace_name} (${site.workspace_id}) [${site.role}]${indicator}\n`);
|
|
44
|
+
}
|
|
45
|
+
ctx.process.stdout.write("\n");
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
//#endregion
|
|
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-BPMkU_SK.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,5 +1,5 @@
|
|
|
1
|
-
import { deleteCredentials, getPatToken } from "./
|
|
2
|
-
import { clearWorkspace } from "./workspace-store-
|
|
1
|
+
import { deleteCredentials, getPatToken } from "./env-hpzB56ay.js";
|
|
2
|
+
import { clearWorkspace } from "./workspace-store-BcyMJAht.js";
|
|
3
3
|
|
|
4
4
|
//#region src/commands/auth/logoutHandler.ts
|
|
5
5
|
function logoutHandler() {
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { getApiUrl, getAutomationSourceIdOverride, getSourceOverride, getTeamId } from "./
|
|
2
|
-
import { client, configureClient, uploadAutomationIngest, uploadImport } from "./sdk-client-
|
|
3
|
-
import "./workspace-store-
|
|
4
|
-
import { resolveWorkspace } from "./resolve-workspace-
|
|
5
|
-
import "./token-refresh-
|
|
6
|
-
import { resolveToken } from "./resolve-token-
|
|
1
|
+
import { getApiUrl, getAutomationSourceIdOverride, getSourceOverride, getTeamId } from "./env-hpzB56ay.js";
|
|
2
|
+
import { client, configureClient, uploadAutomationIngest, uploadImport } from "./sdk-client-BCOB2qNU.js";
|
|
3
|
+
import "./workspace-store-BcyMJAht.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,5 +1,5 @@
|
|
|
1
|
-
import { authGetSitesV1 } from "./sdk-client-
|
|
2
|
-
import { clearWorkspace, loadWorkspace, saveWorkspace } from "./workspace-store-
|
|
1
|
+
import { authGetSitesV1 } from "./sdk-client-BCOB2qNU.js";
|
|
2
|
+
import { clearWorkspace, loadWorkspace, saveWorkspace } from "./workspace-store-BcyMJAht.js";
|
|
3
3
|
|
|
4
4
|
//#region src/workspace/resolve-workspace.ts
|
|
5
5
|
async function fetchSites() {
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import "./
|
|
2
|
-
import { authGetSitesV1, configureClient } from "./sdk-client-
|
|
3
|
-
import { saveWorkspace } from "./workspace-store-
|
|
4
|
-
import "./token-refresh-
|
|
5
|
-
import { resolveToken } from "./resolve-token-
|
|
1
|
+
import "./env-hpzB56ay.js";
|
|
2
|
+
import { authGetSitesV1, configureClient } from "./sdk-client-BCOB2qNU.js";
|
|
3
|
+
import { saveWorkspace } from "./workspace-store-BcyMJAht.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 "./
|
|
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,54 +0,0 @@
|
|
|
1
|
-
import { mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
|
|
2
|
-
import { homedir } from "node:os";
|
|
3
|
-
import { dirname, join } from "node:path";
|
|
4
|
-
|
|
5
|
-
//#region src/utils/env.ts
|
|
6
|
-
const DEFAULT_API_URL = "https://api.levr.one";
|
|
7
|
-
const DEFAULT_AUTH_URL = "https://auth.levr.one";
|
|
8
|
-
/** OAuth client ID for the CLI (seeded in oauth_clients table) */
|
|
9
|
-
const CLI_CLIENT_ID = "3";
|
|
10
|
-
function getApiUrl() {
|
|
11
|
-
return process.env["LEVR_URL"] || DEFAULT_API_URL;
|
|
12
|
-
}
|
|
13
|
-
function getAuthUrl() {
|
|
14
|
-
return process.env["LEVR_AUTH_URL"] || DEFAULT_AUTH_URL;
|
|
15
|
-
}
|
|
16
|
-
function getTeamId(flagValue) {
|
|
17
|
-
return flagValue || process.env["LEVR_TEAM_ID"] || void 0;
|
|
18
|
-
}
|
|
19
|
-
function getPatToken() {
|
|
20
|
-
return process.env["LEVR_TOKEN"];
|
|
21
|
-
}
|
|
22
|
-
function getSourceOverride() {
|
|
23
|
-
return process.env["LEVR_SOURCE"];
|
|
24
|
-
}
|
|
25
|
-
function getAutomationSourceIdOverride() {
|
|
26
|
-
return process.env["LEVR_AUTOMATION_SOURCE_ID"];
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
//#endregion
|
|
30
|
-
//#region src/auth/credentials.ts
|
|
31
|
-
const CREDENTIALS_PATH = join(join(homedir(), ".config", "levr"), "credentials.json");
|
|
32
|
-
function readCredentials() {
|
|
33
|
-
try {
|
|
34
|
-
const raw = readFileSync(CREDENTIALS_PATH, "utf8");
|
|
35
|
-
return JSON.parse(raw);
|
|
36
|
-
} catch {
|
|
37
|
-
return null;
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
function writeCredentials(creds) {
|
|
41
|
-
mkdirSync(dirname(CREDENTIALS_PATH), { recursive: true });
|
|
42
|
-
writeFileSync(CREDENTIALS_PATH, JSON.stringify(creds, null, 2), { mode: 384 });
|
|
43
|
-
}
|
|
44
|
-
function deleteCredentials() {
|
|
45
|
-
try {
|
|
46
|
-
unlinkSync(CREDENTIALS_PATH);
|
|
47
|
-
return true;
|
|
48
|
-
} catch {
|
|
49
|
-
return false;
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
//#endregion
|
|
54
|
-
export { CLI_CLIENT_ID, deleteCredentials, getApiUrl, getAuthUrl, getAutomationSourceIdOverride, getPatToken, getSourceOverride, getTeamId, readCredentials, writeCredentials };
|
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
import "./credentials-Ciq9mA7N.js";
|
|
2
|
-
import { authGetSitesV1, configureClient } from "./sdk-client-rpUEykDw.js";
|
|
3
|
-
import { loadWorkspace } from "./workspace-store-4hfvsEHS.js";
|
|
4
|
-
import "./token-refresh-Pl6Adngb.js";
|
|
5
|
-
import { resolveToken } from "./resolve-token-CQU55U6J.js";
|
|
6
|
-
|
|
7
|
-
//#region src/commands/workspace/listHandler.ts
|
|
8
|
-
async function listHandler() {
|
|
9
|
-
let auth;
|
|
10
|
-
try {
|
|
11
|
-
auth = await resolveToken();
|
|
12
|
-
} catch {
|
|
13
|
-
this.logger.error("Not authenticated. Run 'levr auth login' first.");
|
|
14
|
-
this.process.exitCode = 1;
|
|
15
|
-
return;
|
|
16
|
-
}
|
|
17
|
-
if (auth.type === "pat") {
|
|
18
|
-
this.logger.error("Workspace listing requires JWT auth. Run 'levr auth login'.");
|
|
19
|
-
this.process.exitCode = 1;
|
|
20
|
-
return;
|
|
21
|
-
}
|
|
22
|
-
configureClient(auth);
|
|
23
|
-
const result = await authGetSitesV1();
|
|
24
|
-
if (result.error) {
|
|
25
|
-
this.logger.error("Failed to list workspaces.");
|
|
26
|
-
this.process.exitCode = 1;
|
|
27
|
-
return;
|
|
28
|
-
}
|
|
29
|
-
const sites = result.data.sites;
|
|
30
|
-
if (sites.length === 0) {
|
|
31
|
-
this.logger.info("No workspaces available.");
|
|
32
|
-
return;
|
|
33
|
-
}
|
|
34
|
-
const currentWs = loadWorkspace();
|
|
35
|
-
this.process.stdout.write("\nWorkspaces:\n\n");
|
|
36
|
-
for (const site of sites) {
|
|
37
|
-
const indicator = site.workspace_id === currentWs ? " *" : "";
|
|
38
|
-
this.process.stdout.write(` ${site.workspace_name} (${site.workspace_id}) [${site.role}]${indicator}\n`);
|
|
39
|
-
}
|
|
40
|
-
this.process.stdout.write("\n");
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
//#endregion
|
|
44
|
-
export { listHandler };
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
import { getPatToken, readCredentials } from "./credentials-Ciq9mA7N.js";
|
|
2
|
-
import { isTokenExpired, refreshToken } from "./token-refresh-Pl6Adngb.js";
|
|
3
|
-
|
|
4
|
-
//#region src/auth/resolve-token.ts
|
|
5
|
-
/**
|
|
6
|
-
* Resolve auth token in priority order:
|
|
7
|
-
* 1. LEVR_TOKEN env var (PAT) — long-lived, no refresh
|
|
8
|
-
* 2. Stored credentials (JWT) — auto-refresh if expired
|
|
9
|
-
* 3. Error — not authenticated
|
|
10
|
-
*/
|
|
11
|
-
async function resolveToken() {
|
|
12
|
-
const pat = getPatToken();
|
|
13
|
-
if (pat) return {
|
|
14
|
-
token: pat,
|
|
15
|
-
type: "pat"
|
|
16
|
-
};
|
|
17
|
-
let creds = readCredentials();
|
|
18
|
-
if (creds) {
|
|
19
|
-
if (isTokenExpired(creds)) {
|
|
20
|
-
const refreshed = await refreshToken(creds);
|
|
21
|
-
if (!refreshed) throw new Error("Token expired and refresh failed. Run 'levr auth login' to re-authenticate.");
|
|
22
|
-
creds = refreshed;
|
|
23
|
-
}
|
|
24
|
-
return {
|
|
25
|
-
token: creds.access_token,
|
|
26
|
-
type: "jwt"
|
|
27
|
-
};
|
|
28
|
-
}
|
|
29
|
-
throw new Error("Not authenticated. Run 'levr auth login' or set LEVR_TOKEN environment variable.");
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
//#endregion
|
|
33
|
-
export { resolveToken };
|
|
File without changes
|