@levr-one/cli 0.1.1 → 0.3.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 +32 -8
- package/dist/addHandler-boWTd9yY.js +688 -0
- package/dist/cli.js +105 -28
- package/dist/env-NxtzJJPk.js +102 -0
- package/dist/listHandler-EsB3AeOB.js +51 -0
- package/dist/{loginHandler-BnokItPo.js → loginHandler-BfIUQJrC.js} +48 -23
- package/dist/{logoutHandler-C3X9rqLP.js → logoutHandler-BC8laIAB.js} +1 -1
- package/dist/{pushHandler-C3DteESD.js → pushHandler-DBBa6hKz.js} +5 -5
- package/dist/resolve-token-BRPrqdG-.js +48 -0
- package/dist/{resolve-workspace-BPMkU_SK.js → resolve-workspace-Dj7jgtE3.js} +1 -1
- package/dist/{sdk-client-rpUEykDw.js → sdk-client-BeE6mXns.js} +177 -1
- package/dist/{selectHandler-BjM4LKYW.js → selectHandler-XOUHdyea.js} +4 -4
- package/dist/{statusHandler-LfrSN4uh.js → statusHandler-BaQB40t7.js} +9 -3
- package/dist/{token-refresh-Pl6Adngb.js → token-refresh-Cz-FqDtC.js} +1 -1
- package/package.json +3 -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/{currentHandler-DpWIqCBm.js → currentHandler-Bm7GFMkj.js} +0 -0
package/dist/cli.js
CHANGED
|
@@ -52,6 +52,68 @@ function buildContext(process$1) {
|
|
|
52
52
|
};
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
+
//#endregion
|
|
56
|
+
//#region src/commands/mcp/add.ts
|
|
57
|
+
const mcpAddCommand = buildCommand({
|
|
58
|
+
docs: {
|
|
59
|
+
brief: "Add the Levr MCP server to installed AI clients",
|
|
60
|
+
fullDescription: `Detect MCP-capable clients on this machine (Claude Desktop,
|
|
61
|
+
Claude Code, Cursor, Windsurf, Zed) and write the Levr MCP server into each
|
|
62
|
+
one's config. The entry is credential-free — the client opens a browser to
|
|
63
|
+
authorize with Levr the first time it connects.
|
|
64
|
+
|
|
65
|
+
Interactive by default; non-interactive when --all/--client/--yes is passed
|
|
66
|
+
or when not running in a terminal (CI). Config edits preserve existing
|
|
67
|
+
servers and comments, and re-running is a no-op.
|
|
68
|
+
|
|
69
|
+
Examples:
|
|
70
|
+
npx @levr-one/cli mcp add # detect clients and pick interactively
|
|
71
|
+
levr mcp add --all # set up every detected client
|
|
72
|
+
levr mcp add --client cursor --yes
|
|
73
|
+
levr mcp add --dry-run # preview without writing
|
|
74
|
+
levr mcp add --url <mcp-url> # target a non-default MCP server`
|
|
75
|
+
},
|
|
76
|
+
parameters: {
|
|
77
|
+
flags: {
|
|
78
|
+
client: {
|
|
79
|
+
kind: "parsed",
|
|
80
|
+
parse: String,
|
|
81
|
+
brief: "Set up these client ids (comma-separated or repeated)",
|
|
82
|
+
placeholder: "id[,id]",
|
|
83
|
+
variadic: true,
|
|
84
|
+
optional: true
|
|
85
|
+
},
|
|
86
|
+
all: {
|
|
87
|
+
kind: "boolean",
|
|
88
|
+
default: false,
|
|
89
|
+
brief: "Set up every detected, installable client"
|
|
90
|
+
},
|
|
91
|
+
yes: {
|
|
92
|
+
kind: "boolean",
|
|
93
|
+
default: false,
|
|
94
|
+
brief: "Non-interactive; auto-select detected clients"
|
|
95
|
+
},
|
|
96
|
+
"dry-run": {
|
|
97
|
+
kind: "boolean",
|
|
98
|
+
default: false,
|
|
99
|
+
brief: "Show changes without writing"
|
|
100
|
+
},
|
|
101
|
+
url: {
|
|
102
|
+
kind: "parsed",
|
|
103
|
+
parse: String,
|
|
104
|
+
brief: "MCP server URL (default derived from the API server)",
|
|
105
|
+
placeholder: "url",
|
|
106
|
+
optional: true
|
|
107
|
+
}
|
|
108
|
+
},
|
|
109
|
+
aliases: { y: "yes" }
|
|
110
|
+
},
|
|
111
|
+
loader: async () => {
|
|
112
|
+
const { mcpAddHandler } = await import("./addHandler-boWTd9yY.js");
|
|
113
|
+
return mcpAddHandler;
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
|
|
55
117
|
//#endregion
|
|
56
118
|
//#region src/commands/auth/login.ts
|
|
57
119
|
const loginCommand = buildCommand({
|
|
@@ -68,15 +130,24 @@ Examples:
|
|
|
68
130
|
levr auth login --device-code # Device flow for SSH/headless`
|
|
69
131
|
},
|
|
70
132
|
parameters: {
|
|
71
|
-
flags: {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
133
|
+
flags: {
|
|
134
|
+
"device-code": {
|
|
135
|
+
kind: "boolean",
|
|
136
|
+
default: false,
|
|
137
|
+
brief: "Use device code flow (for SSH/headless environments)"
|
|
138
|
+
},
|
|
139
|
+
url: {
|
|
140
|
+
kind: "parsed",
|
|
141
|
+
parse: String,
|
|
142
|
+
brief: "API base URL (default: https://api.levr.one)",
|
|
143
|
+
placeholder: "url",
|
|
144
|
+
optional: true
|
|
145
|
+
}
|
|
146
|
+
},
|
|
76
147
|
aliases: { d: "device-code" }
|
|
77
148
|
},
|
|
78
149
|
loader: async () => {
|
|
79
|
-
const { loginHandler } = await import("./loginHandler-
|
|
150
|
+
const { loginHandler } = await import("./loginHandler-BfIUQJrC.js");
|
|
80
151
|
return loginHandler;
|
|
81
152
|
}
|
|
82
153
|
});
|
|
@@ -95,7 +166,7 @@ Examples:
|
|
|
95
166
|
},
|
|
96
167
|
parameters: {},
|
|
97
168
|
loader: async () => {
|
|
98
|
-
const { logoutHandler } = await import("./logoutHandler-
|
|
169
|
+
const { logoutHandler } = await import("./logoutHandler-BC8laIAB.js");
|
|
99
170
|
return logoutHandler;
|
|
100
171
|
}
|
|
101
172
|
});
|
|
@@ -115,7 +186,7 @@ Examples:
|
|
|
115
186
|
},
|
|
116
187
|
parameters: {},
|
|
117
188
|
loader: async () => {
|
|
118
|
-
const { statusHandler } = await import("./statusHandler-
|
|
189
|
+
const { statusHandler } = await import("./statusHandler-BaQB40t7.js");
|
|
119
190
|
return statusHandler;
|
|
120
191
|
}
|
|
121
192
|
});
|
|
@@ -226,7 +297,7 @@ Examples:
|
|
|
226
297
|
}
|
|
227
298
|
},
|
|
228
299
|
loader: async () => {
|
|
229
|
-
const { pushHandler } = await import("./pushHandler-
|
|
300
|
+
const { pushHandler } = await import("./pushHandler-DBBa6hKz.js");
|
|
230
301
|
return pushHandler;
|
|
231
302
|
}
|
|
232
303
|
});
|
|
@@ -247,7 +318,7 @@ Examples:
|
|
|
247
318
|
},
|
|
248
319
|
parameters: {},
|
|
249
320
|
loader: async () => {
|
|
250
|
-
const { listHandler } = await import("./listHandler-
|
|
321
|
+
const { listHandler } = await import("./listHandler-EsB3AeOB.js");
|
|
251
322
|
return listHandler;
|
|
252
323
|
}
|
|
253
324
|
});
|
|
@@ -280,7 +351,7 @@ Examples:
|
|
|
280
351
|
flags: {}
|
|
281
352
|
},
|
|
282
353
|
loader: async () => {
|
|
283
|
-
const { selectHandler } = await import("./selectHandler-
|
|
354
|
+
const { selectHandler } = await import("./selectHandler-XOUHdyea.js");
|
|
284
355
|
return selectHandler;
|
|
285
356
|
}
|
|
286
357
|
});
|
|
@@ -297,35 +368,41 @@ Examples:
|
|
|
297
368
|
},
|
|
298
369
|
parameters: {},
|
|
299
370
|
loader: async () => {
|
|
300
|
-
const { currentHandler } = await import("./currentHandler-
|
|
371
|
+
const { currentHandler } = await import("./currentHandler-Bm7GFMkj.js");
|
|
301
372
|
return currentHandler;
|
|
302
373
|
}
|
|
303
374
|
});
|
|
304
375
|
|
|
305
376
|
//#endregion
|
|
306
377
|
//#region package.json
|
|
307
|
-
var version = "0.
|
|
378
|
+
var version = "0.3.0";
|
|
308
379
|
|
|
309
380
|
//#endregion
|
|
310
381
|
//#region src/app.ts
|
|
382
|
+
const authRoutes = buildRouteMap({
|
|
383
|
+
routes: {
|
|
384
|
+
login: loginCommand,
|
|
385
|
+
logout: logoutCommand,
|
|
386
|
+
status: statusCommand
|
|
387
|
+
},
|
|
388
|
+
docs: { brief: "Manage authentication" }
|
|
389
|
+
});
|
|
390
|
+
const workspaceRoutes = buildRouteMap({
|
|
391
|
+
routes: {
|
|
392
|
+
list: listCommand,
|
|
393
|
+
select: selectCommand,
|
|
394
|
+
current: currentCommand
|
|
395
|
+
},
|
|
396
|
+
docs: { brief: "Manage workspace selection" }
|
|
397
|
+
});
|
|
311
398
|
const routes = buildRouteMap({
|
|
312
399
|
routes: {
|
|
313
|
-
|
|
314
|
-
routes: {
|
|
315
|
-
|
|
316
|
-
logout: logoutCommand,
|
|
317
|
-
status: statusCommand
|
|
318
|
-
},
|
|
319
|
-
docs: { brief: "Manage authentication" }
|
|
320
|
-
}),
|
|
321
|
-
workspace: buildRouteMap({
|
|
322
|
-
routes: {
|
|
323
|
-
list: listCommand,
|
|
324
|
-
select: selectCommand,
|
|
325
|
-
current: currentCommand
|
|
326
|
-
},
|
|
327
|
-
docs: { brief: "Manage workspace selection" }
|
|
400
|
+
mcp: buildRouteMap({
|
|
401
|
+
routes: { add: mcpAddCommand },
|
|
402
|
+
docs: { brief: "Wire the Levr MCP server into installed AI clients" }
|
|
328
403
|
}),
|
|
404
|
+
auth: authRoutes,
|
|
405
|
+
workspace: workspaceRoutes,
|
|
329
406
|
push: pushCommand,
|
|
330
407
|
install: buildInstallCommand("levr", { bash: "levr __complete" }),
|
|
331
408
|
uninstall: buildUninstallCommand("levr", { bash: true })
|
|
@@ -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.now": "auth.levr.now"
|
|
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 "./env-NxtzJJPk.js";
|
|
2
|
+
import { authGetSitesV1, configureClient } from "./sdk-client-BeE6mXns.js";
|
|
3
|
+
import { loadWorkspace } from "./workspace-store-4hfvsEHS.js";
|
|
4
|
+
import "./token-refresh-Cz-FqDtC.js";
|
|
5
|
+
import { resolveToken } from "./resolve-token-BRPrqdG-.js";
|
|
6
|
+
|
|
7
|
+
//#region src/commands/workspace/listHandler.ts
|
|
8
|
+
async function listHandler() {
|
|
9
|
+
let auth;
|
|
10
|
+
try {
|
|
11
|
+
auth = await resolveToken();
|
|
12
|
+
} catch (err) {
|
|
13
|
+
this.logger.error(err instanceof Error ? err.message : "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 data = result.data;
|
|
30
|
+
printSites(this, data.sites);
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Print the workspace list with the current-workspace indicator. Shared
|
|
34
|
+
* with `levr init` (ENG-2361).
|
|
35
|
+
*/
|
|
36
|
+
function printSites(ctx, sites) {
|
|
37
|
+
if (sites.length === 0) {
|
|
38
|
+
ctx.logger.info("No workspaces available.");
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
const currentWs = loadWorkspace();
|
|
42
|
+
ctx.process.stdout.write("\nWorkspaces:\n\n");
|
|
43
|
+
for (const site of sites) {
|
|
44
|
+
const indicator = site.workspace_id === currentWs ? " *" : "";
|
|
45
|
+
ctx.process.stdout.write(` ${site.workspace_name} (${site.workspace_id}) [${site.role}]${indicator}\n`);
|
|
46
|
+
}
|
|
47
|
+
ctx.process.stdout.write("\n");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
//#endregion
|
|
51
|
+
export { listHandler };
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { CLI_CLIENT_ID, getApiUrl, getAuthUrl, writeCredentials } from "./
|
|
2
|
-
import { configureClient } from "./sdk-client-
|
|
1
|
+
import { CLI_CLIENT_ID, getApiUrl, getAuthUrl, setSessionApiUrl, writeCredentials } from "./env-NxtzJJPk.js";
|
|
2
|
+
import { configureClient } from "./sdk-client-BeE6mXns.js";
|
|
3
3
|
import "./workspace-store-4hfvsEHS.js";
|
|
4
|
-
import { autoSelectWorkspace } from "./resolve-workspace-
|
|
4
|
+
import { autoSelectWorkspace } from "./resolve-workspace-Dj7jgtE3.js";
|
|
5
5
|
import chalk from "chalk";
|
|
6
6
|
import { createHash, randomBytes } from "node:crypto";
|
|
7
7
|
import { createServer } from "node:http";
|
|
@@ -166,14 +166,19 @@ function sleep(ms) {
|
|
|
166
166
|
*/
|
|
167
167
|
async function requestDeviceCode() {
|
|
168
168
|
const apiUrl = getApiUrl();
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
169
|
+
let res;
|
|
170
|
+
try {
|
|
171
|
+
res = await fetch(`${apiUrl}/v1/oauth/device/authorize`, {
|
|
172
|
+
method: "POST",
|
|
173
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
174
|
+
body: new URLSearchParams({
|
|
175
|
+
client_id: CLI_CLIENT_ID,
|
|
176
|
+
scope: "read:own write:own"
|
|
177
|
+
})
|
|
178
|
+
});
|
|
179
|
+
} catch {
|
|
180
|
+
throw new Error(`Could not reach ${apiUrl} — check the URL and your network connection.`);
|
|
181
|
+
}
|
|
177
182
|
if (!res.ok) {
|
|
178
183
|
const body = await res.text();
|
|
179
184
|
throw new Error(`Failed to request device code (${res.status}): ${body}`);
|
|
@@ -223,15 +228,31 @@ function formatCountdown(ms) {
|
|
|
223
228
|
return `${Math.floor(totalSec / 60)}:${(totalSec % 60).toString().padStart(2, "0")}`;
|
|
224
229
|
}
|
|
225
230
|
async function loginHandler(flags) {
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
+
await performLogin(this, {
|
|
232
|
+
deviceCode: flags["device-code"],
|
|
233
|
+
url: flags.url
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* Run the login flow (PKCE or device code). Reusable outside the Stricli
|
|
238
|
+
* handler (levr init composes it). Returns true on success; on failure the
|
|
239
|
+
* error has been printed and `exitCode` set.
|
|
240
|
+
*/
|
|
241
|
+
async function performLogin(ctx, options) {
|
|
242
|
+
if (options.url) setSessionApiUrl(options.url);
|
|
243
|
+
if (options.deviceCode) return deviceCodeLogin(ctx);
|
|
244
|
+
return pkceLogin(ctx);
|
|
231
245
|
}
|
|
232
246
|
async function pkceLogin(ctx) {
|
|
233
247
|
const apiUrl = getApiUrl();
|
|
234
|
-
|
|
248
|
+
let authUrl;
|
|
249
|
+
try {
|
|
250
|
+
authUrl = getAuthUrl();
|
|
251
|
+
} catch (err) {
|
|
252
|
+
ctx.logger.error(err instanceof Error ? err.message : "Invalid auth server configuration.");
|
|
253
|
+
ctx.process.exitCode = 1;
|
|
254
|
+
return false;
|
|
255
|
+
}
|
|
235
256
|
const codeVerifier = generateCodeVerifier();
|
|
236
257
|
const codeChallenge = generateCodeChallenge(codeVerifier);
|
|
237
258
|
const state = randomBytes(16).toString("hex");
|
|
@@ -240,7 +261,7 @@ async function pkceLogin(ctx) {
|
|
|
240
261
|
expectedState: state
|
|
241
262
|
});
|
|
242
263
|
const redirectUri = `http://127.0.0.1:${await portPromise}/callback`;
|
|
243
|
-
const
|
|
264
|
+
const params = new URLSearchParams({
|
|
244
265
|
response_type: "code",
|
|
245
266
|
client_id: CLI_CLIENT_ID,
|
|
246
267
|
redirect_uri: redirectUri,
|
|
@@ -248,7 +269,8 @@ async function pkceLogin(ctx) {
|
|
|
248
269
|
code_challenge_method: "S256",
|
|
249
270
|
scope: "read:own write:own",
|
|
250
271
|
state
|
|
251
|
-
})
|
|
272
|
+
});
|
|
273
|
+
const authorizeUrl = `${authUrl}/auth/oauth/authorize?${params.toString()}`;
|
|
252
274
|
ctx.process.stdout.write("Opening browser to authenticate...\n\n");
|
|
253
275
|
ctx.process.stdout.write(` If the browser doesn't open, visit:\n ${chalk.cyan(authorizeUrl)}\n\n`);
|
|
254
276
|
try {
|
|
@@ -270,7 +292,7 @@ async function pkceLogin(ctx) {
|
|
|
270
292
|
ctx.process.stdout.write("\n");
|
|
271
293
|
ctx.logger.error(err instanceof Error ? err.message : "Authentication failed.");
|
|
272
294
|
ctx.process.exitCode = 1;
|
|
273
|
-
return;
|
|
295
|
+
return false;
|
|
274
296
|
}
|
|
275
297
|
clearInterval(countdownTimer);
|
|
276
298
|
try {
|
|
@@ -289,11 +311,12 @@ async function pkceLogin(ctx) {
|
|
|
289
311
|
const errBody = await tokenRes.text();
|
|
290
312
|
throw new Error(`Token exchange failed (${tokenRes.status}): ${errBody}`);
|
|
291
313
|
}
|
|
292
|
-
await saveTokensAndFinish(ctx, await tokenRes.json());
|
|
314
|
+
return await saveTokensAndFinish(ctx, await tokenRes.json());
|
|
293
315
|
} catch (err) {
|
|
294
316
|
ctx.process.stdout.write("\n");
|
|
295
317
|
ctx.logger.error(err instanceof Error ? err.message : "Token exchange failed.");
|
|
296
318
|
ctx.process.exitCode = 1;
|
|
319
|
+
return false;
|
|
297
320
|
}
|
|
298
321
|
}
|
|
299
322
|
async function deviceCodeLogin(ctx) {
|
|
@@ -319,11 +342,12 @@ async function deviceCodeLogin(ctx) {
|
|
|
319
342
|
throw err;
|
|
320
343
|
}
|
|
321
344
|
clearInterval(deviceTimer);
|
|
322
|
-
await saveTokensAndFinish(ctx, tokenData);
|
|
345
|
+
return await saveTokensAndFinish(ctx, tokenData);
|
|
323
346
|
} catch (err) {
|
|
324
347
|
ctx.process.stdout.write("\n");
|
|
325
348
|
ctx.logger.error(err instanceof Error ? err.message : "Device flow failed.");
|
|
326
349
|
ctx.process.exitCode = 1;
|
|
350
|
+
return false;
|
|
327
351
|
}
|
|
328
352
|
}
|
|
329
353
|
async function saveTokensAndFinish(ctx, tokenData) {
|
|
@@ -337,7 +361,7 @@ async function saveTokensAndFinish(ctx, tokenData) {
|
|
|
337
361
|
ctx.process.stdout.write("\n");
|
|
338
362
|
ctx.logger.error("Token response missing user data.");
|
|
339
363
|
ctx.process.exitCode = 1;
|
|
340
|
-
return;
|
|
364
|
+
return false;
|
|
341
365
|
}
|
|
342
366
|
writeCredentials({
|
|
343
367
|
version: 1,
|
|
@@ -357,6 +381,7 @@ async function saveTokensAndFinish(ctx, tokenData) {
|
|
|
357
381
|
const wsResult = await autoSelectWorkspace();
|
|
358
382
|
if (wsResult.kind === "single") ctx.process.stdout.write(`Workspace: ${chalk.bold(wsResult.workspaceName)}\n`);
|
|
359
383
|
else if (wsResult.kind === "multiple") ctx.process.stdout.write(`\n${wsResult.count} workspaces available. Run ${chalk.cyan("'levr workspace list'")} to see them.\n`);
|
|
384
|
+
return true;
|
|
360
385
|
}
|
|
361
386
|
|
|
362
387
|
//#endregion
|
|
@@ -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-NxtzJJPk.js";
|
|
2
|
+
import { client, configureClient, uploadAutomationIngest, uploadImport } from "./sdk-client-BeE6mXns.js";
|
|
3
3
|
import "./workspace-store-4hfvsEHS.js";
|
|
4
|
-
import { resolveWorkspace } from "./resolve-workspace-
|
|
5
|
-
import "./token-refresh-
|
|
6
|
-
import { resolveToken } from "./resolve-token-
|
|
4
|
+
import { resolveWorkspace } from "./resolve-workspace-Dj7jgtE3.js";
|
|
5
|
+
import "./token-refresh-Cz-FqDtC.js";
|
|
6
|
+
import { resolveToken } from "./resolve-token-BRPrqdG-.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-NxtzJJPk.js";
|
|
2
|
+
import { isTokenExpired, refreshToken } from "./token-refresh-Cz-FqDtC.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 { resolveToken };
|