lagora-cli 1.1.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.
Files changed (52) hide show
  1. package/README.md +138 -0
  2. package/dist/help.txt +70 -0
  3. package/dist/lagora.js +342 -0
  4. package/dist/report-help.txt +5 -0
  5. package/dist/scripts/agora_playground_harness.py +263 -0
  6. package/dist/scripts/announce.js +41 -0
  7. package/dist/scripts/check-kernel-submission.py +90 -0
  8. package/dist/scripts/chunk-2EAJVB5D.js +100 -0
  9. package/dist/scripts/chunk-2KTLCUFI.js +29 -0
  10. package/dist/scripts/chunk-AZ3EEBVD.js +137 -0
  11. package/dist/scripts/chunk-NBJMYAOA.js +2128 -0
  12. package/dist/scripts/chunk-NCJMUBTG.js +125 -0
  13. package/dist/scripts/chunk-QJPQHKIO.js +23 -0
  14. package/dist/scripts/chunk-RIR5KGHC.js +33 -0
  15. package/dist/scripts/chunk-TJZVQYBL.js +8 -0
  16. package/dist/scripts/chunk-UHJXD4TG.js +18 -0
  17. package/dist/scripts/chunk-UQ6I6VTY.js +117 -0
  18. package/dist/scripts/cli-auth.js +348 -0
  19. package/dist/scripts/cli-config-IA7EOSYD.js +7 -0
  20. package/dist/scripts/install-skill.js +199 -0
  21. package/dist/scripts/issue-local-client-DZUXZOKY.js +22 -0
  22. package/dist/scripts/issue-search.js +1823 -0
  23. package/dist/scripts/issue.js +386 -0
  24. package/dist/scripts/keycloak-provision.js +986 -0
  25. package/dist/scripts/legato-fsim-runner.py +126 -0
  26. package/dist/scripts/legato-lowering-runner.py +156 -0
  27. package/dist/scripts/legato_runner_annotations.py +235 -0
  28. package/dist/scripts/legato_runner_env.py +91 -0
  29. package/dist/scripts/legato_runner_launchers.py +287 -0
  30. package/dist/scripts/legato_runner_script_wrapper.py +193 -0
  31. package/dist/scripts/notifications-EU43SIEV.js +624 -0
  32. package/dist/scripts/playground.js +408 -0
  33. package/dist/scripts/report-bundle-sync-3U7QTP4Z.js +215 -0
  34. package/dist/scripts/report.js +104 -0
  35. package/dist/scripts/resolve-sdk-package-version.py +151 -0
  36. package/dist/scripts/sdk-runtime-JE6H2PB2.js +992 -0
  37. package/dist/scripts/sdk-runtime-kubernetes-job-KOWL4ITV.js +479 -0
  38. package/dist/scripts/sdk-runtime-smoke.py +168 -0
  39. package/dist/scripts/sdk.js +256 -0
  40. package/dist/scripts/site-feedback-CAPE5MPX.js +136 -0
  41. package/dist/scripts/site-feedback-rate-limit-5BU2WSFE.js +86 -0
  42. package/dist/scripts/site-feedback.js +117 -0
  43. package/dist/scripts/storage-234FBH54.js +67 -0
  44. package/dist/scripts/submit-issue.sh +489 -0
  45. package/dist/scripts/verification-3QCY66QW.js +772 -0
  46. package/dist/scripts/verify-issue.js +144 -0
  47. package/dist/skills/legato-agora-cli/SKILL.md +556 -0
  48. package/dist/skills/legato-agora-cli/agents/openai.yaml +7 -0
  49. package/dist/skills/legato-agora-cli/reference/kernel-with-golden.py +84 -0
  50. package/dist/skills/legato-site-feedback/SKILL.md +49 -0
  51. package/dist/skills/legato-site-feedback/agents/openai.yaml +7 -0
  52. package/package.json +16 -0
@@ -0,0 +1,125 @@
1
+ // scripts/issue-local-client.ts
2
+ import { copyFile, mkdir, readFile, writeFile } from "node:fs/promises";
3
+ import path from "node:path";
4
+ async function fetchIssueFromStore(issueId, format) {
5
+ const { getIssue } = await import("./storage-234FBH54.js");
6
+ const record = await getIssue(issueId);
7
+ if (!record) throw new Error(`Issue not found: ${issueId}`);
8
+ printIssueRecord(record, format);
9
+ }
10
+ function printIssueRecord(record, format) {
11
+ if (format === "json") {
12
+ console.log(JSON.stringify(record, null, 2));
13
+ return;
14
+ }
15
+ const latestRun = record.verificationRuns.at(-1);
16
+ console.log(`# ${record.issue.title}`);
17
+ console.log(`id: ${record.issue.id}`);
18
+ console.log(`status: ${record.issue.status}`);
19
+ console.log(`reporter: ${record.issue.creator}`);
20
+ console.log(`assignee: ${record.issue.assignee?.name ?? "none"}`);
21
+ console.log(`occurrences: ${record.issue.occurrenceCount}`);
22
+ console.log(`tags: ${record.issue.tags.join(", ") || "none"}`);
23
+ console.log(`kernel: ${record.artifacts.find((artifact) => artifact.id === record.bundle.kernelArtifactId)?.filename ?? "missing"}`);
24
+ if (latestRun) {
25
+ console.log(`latestVerification: ${latestRun.kind} ${latestRun.status}${latestRun.failedStage ? ` failedStage=${latestRun.failedStage}` : ""}`);
26
+ }
27
+ console.log("");
28
+ console.log(record.issue.description || "No description supplied.");
29
+ }
30
+ async function checkoutIssueFromStore(issueId, out, author) {
31
+ const { getIssue } = await import("./storage-234FBH54.js");
32
+ const initialRecord = await getIssue(issueId);
33
+ if (initialRecord?.issue.status === "open") {
34
+ const { updateIssueStatus } = await import("./storage-234FBH54.js");
35
+ await updateIssueStatus(issueId, "investigating", author);
36
+ }
37
+ const record = await getIssue(issueId);
38
+ if (!record) throw new Error(`Issue not found: ${issueId}`);
39
+ const targetRoot = path.resolve(out?.trim() || path.join("lagora-issues", issueId));
40
+ await mkdir(targetRoot, { recursive: true });
41
+ await writeFile(path.join(targetRoot, "issue.json"), `${JSON.stringify(record, null, 2)}
42
+ `);
43
+ const issueStoreRoot = process.env.AGORA_ISSUE_STORE ? path.resolve(process.env.AGORA_ISSUE_STORE) : path.join(process.cwd(), "issue-store");
44
+ for (const artifact of record.artifacts) {
45
+ if (artifact.storage !== "issue-store" || !artifact.relativePath) continue;
46
+ await copyFile(path.join(issueStoreRoot, artifact.relativePath), path.join(targetRoot, artifact.filename));
47
+ }
48
+ console.log(`Issue ${issueId} checked out to ${targetRoot}`);
49
+ }
50
+ async function saveSuggestionToStore(input) {
51
+ const file = path.resolve(input.file);
52
+ const content = await readFile(file, "utf8");
53
+ const filename = input.filename?.trim() || path.basename(file);
54
+ const { addKernelSuggestion, getIssue, updateKernelSuggestion } = await import("./storage-234FBH54.js");
55
+ const record = await getIssue(input.issueId);
56
+ if (!record) throw new Error(`Issue not found: ${input.issueId}`);
57
+ const baseArtifactId = input.baseArtifactId?.trim() || record.bundle.kernelArtifactId;
58
+ if (input.suggestionArtifactId?.trim()) {
59
+ await updateKernelSuggestion({
60
+ issueId: input.issueId,
61
+ baseArtifactId,
62
+ suggestionArtifactId: input.suggestionArtifactId.trim(),
63
+ filename,
64
+ content,
65
+ author: input.author
66
+ });
67
+ console.log(`Suggestion updated for ${input.issueId}`);
68
+ return;
69
+ }
70
+ await addKernelSuggestion({ issueId: input.issueId, baseArtifactId, filename, content, author: input.author });
71
+ console.log(`Suggestion added to ${input.issueId}`);
72
+ }
73
+ async function assignIssueInStore(input) {
74
+ const { assignIssue, clearIssueAssignee } = await import("./storage-234FBH54.js");
75
+ if (input.assignee) {
76
+ await assignIssue({ issueId: input.issueId, assignee: input.assignee, actor: input.author });
77
+ } else {
78
+ await clearIssueAssignee(input.issueId, input.author);
79
+ }
80
+ console.log(`Assignee updated: ${input.issueId} -> ${input.assignee || "unassigned"}`);
81
+ }
82
+ async function updateIssueStatusInStore(issueId, status, author) {
83
+ const { updateIssueStatus } = await import("./storage-234FBH54.js");
84
+ await updateIssueStatus(issueId, status, author);
85
+ console.log(`Status updated: ${issueId} -> ${status}`);
86
+ }
87
+ async function addBlockerToStore(input) {
88
+ const { addIssueBlocker } = await import("./storage-234FBH54.js");
89
+ const blocker = await addIssueBlocker({
90
+ issueId: input.issueId,
91
+ repository: input.repository?.trim() || void 0,
92
+ number: input.number,
93
+ actor: input.author
94
+ });
95
+ console.log(`Blocker linked: ${input.issueId} -> ${blocker.repository}#${blocker.number} (${blocker.status})`);
96
+ }
97
+ async function syncBlockerInStore(input) {
98
+ const { syncIssueBlocker } = await import("./storage-234FBH54.js");
99
+ const blocker = await syncIssueBlocker({ issueId: input.issueId, blockerId: input.blockerId, actor: input.author });
100
+ console.log(`Blocker synced: ${input.issueId} -> ${blocker.repository}#${blocker.number} (${blocker.status})`);
101
+ }
102
+ async function updateIssueDescriptionInStore(issueId, description, author) {
103
+ const { updateIssueDescription } = await import("./storage-234FBH54.js");
104
+ try {
105
+ await updateIssueDescription({ issueId, description, actor: author });
106
+ } catch (error) {
107
+ if (error instanceof Error && error.message === "issue_description_forbidden") {
108
+ throw new Error(`Only the reporter can rewrite this issue; --author is currently "${author}"`);
109
+ }
110
+ throw error;
111
+ }
112
+ console.log(`Description updated for ${issueId}`);
113
+ }
114
+
115
+ export {
116
+ fetchIssueFromStore,
117
+ printIssueRecord,
118
+ checkoutIssueFromStore,
119
+ saveSuggestionToStore,
120
+ assignIssueInStore,
121
+ updateIssueStatusInStore,
122
+ addBlockerToStore,
123
+ syncBlockerInStore,
124
+ updateIssueDescriptionInStore
125
+ };
@@ -0,0 +1,23 @@
1
+ // lib/vendor-id.ts
2
+ var LEGACY_CREATOR_VENDOR_ID = "hyperaccel";
3
+ var vendorIdPattern = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/;
4
+ function isVendorId(value) {
5
+ return vendorIdPattern.test(value);
6
+ }
7
+ function parseVendorId(value) {
8
+ if (typeof value !== "string") return void 0;
9
+ const trimmed = value.trim();
10
+ return isVendorId(trimmed) ? trimmed : void 0;
11
+ }
12
+ function normalizeVendorId(value) {
13
+ const trimmed = value?.trim();
14
+ if (!trimmed) return LEGACY_CREATOR_VENDOR_ID;
15
+ const parsed = parseVendorId(trimmed);
16
+ if (!parsed) throw new Error(`Invalid vendor id: ${trimmed}`);
17
+ return parsed;
18
+ }
19
+
20
+ export {
21
+ parseVendorId,
22
+ normalizeVendorId
23
+ };
@@ -0,0 +1,33 @@
1
+ import {
2
+ sdkRuntimeToolchainEnv
3
+ } from "./chunk-UQ6I6VTY.js";
4
+
5
+ // lib/verification-lowering-env.ts
6
+ import path from "node:path";
7
+ function loweringWorkerEnv(runtime, baseEnv = process.env) {
8
+ const toolchainEnv = sdkRuntimeToolchainEnv(baseEnv);
9
+ return {
10
+ ...toolchainEnv,
11
+ HOME: baseEnv.HOME,
12
+ LANG: baseEnv.LANG,
13
+ LC_ALL: baseEnv.LC_ALL,
14
+ NODE_ENV: nodeEnv(baseEnv.NODE_ENV),
15
+ PATH: toolchainEnv.PATH,
16
+ AGORA_SDK_ROOT: runtime.preparedRoot,
17
+ AGORA_SDK_PREPARED_ROOT: runtime.preparedRoot,
18
+ LEGATO_KERNEL_FUNCTION: baseEnv.LEGATO_KERNEL_FUNCTION,
19
+ PYTHONPATH: [
20
+ baseEnv.AGORA_SDK_PYTHONPATH,
21
+ baseEnv.AGORA_PYTHONPATH_EXTRA,
22
+ baseEnv.PYTHONPATH
23
+ ].filter(Boolean).join(path.delimiter)
24
+ };
25
+ }
26
+ function nodeEnv(value) {
27
+ if (value === "production" || value === "test") return value;
28
+ return "development";
29
+ }
30
+
31
+ export {
32
+ loweringWorkerEnv
33
+ };
@@ -0,0 +1,8 @@
1
+ // lib/base-path.ts
2
+ var appBasePath = "/legato-dev-agora";
3
+ var publicAppUrl = `https://public.hyperaccel.net${appBasePath}`;
4
+ var internalAppUrl = `https://legato-dev-agora.hyperaccel.net${appBasePath}`;
5
+
6
+ export {
7
+ internalAppUrl
8
+ };
@@ -0,0 +1,18 @@
1
+ // lib/server/issue-store-paths.ts
2
+ import path from "node:path";
3
+ function getIssueStoreRoot() {
4
+ return process.env.AGORA_ISSUE_STORE ? path.resolve(process.env.AGORA_ISSUE_STORE) : path.resolve("issue-store");
5
+ }
6
+ function safeJoin(root, ...segments) {
7
+ const resolvedRoot = path.resolve(root);
8
+ const target = path.resolve(resolvedRoot, ...segments);
9
+ if (target !== resolvedRoot && !target.startsWith(`${resolvedRoot}${path.sep}`)) {
10
+ throw new Error(`Path escapes storage root: ${segments.join("/")}`);
11
+ }
12
+ return target;
13
+ }
14
+
15
+ export {
16
+ getIssueStoreRoot,
17
+ safeJoin
18
+ };
@@ -0,0 +1,117 @@
1
+ // lib/server/sdk-runtime-cache.ts
2
+ import { createHash } from "node:crypto";
3
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
4
+ import path from "node:path";
5
+ var CACHE_VERSION = "sdk-prepare-cache-v1";
6
+ var TOOLCHAIN_FINGERPRINT = "llvm20-openmp";
7
+ var SOURCE_FINGERPRINT_VERSION = "sdk-prepare-source-v1";
8
+ var LLVM_ROOT = "/usr/lib/llvm-20";
9
+ var LLVM_BIN = `${LLVM_ROOT}/bin`;
10
+ var LLVM_LIB = `${LLVM_ROOT}/lib`;
11
+ var SDK_PREPARE_SOURCE_PATHS = ["legato", "host_runtime", "misc", "pyproject.toml", "uv.lock", "3rdparty"];
12
+ function sdkPrepareCacheStatePath(settings) {
13
+ return path.join(settings.runtimeRoot, ".agora", "sdk-prepare-cache", `${path.basename(settings.preparedRoot)}.json`);
14
+ }
15
+ function sdkPrepareCacheFingerprint(settings) {
16
+ const input = [
17
+ CACHE_VERSION,
18
+ process.env.AGORA_SDK_TOOLCHAIN_FINGERPRINT ?? TOOLCHAIN_FINGERPRINT,
19
+ settings.prepareCommand
20
+ ].join("\n");
21
+ return createHash("sha256").update(input).digest("hex");
22
+ }
23
+ function sdkPrepareSourceFingerprint(treeListing) {
24
+ return createHash("sha256").update(`${SOURCE_FINGERPRINT_VERSION}
25
+ ${treeListing}`).digest("hex");
26
+ }
27
+ function sdkPrepareCacheEnv(settings) {
28
+ const cacheRoot = path.join(settings.runtimeRoot, ".agora", "cache");
29
+ return {
30
+ ...sdkRuntimeToolchainEnv(),
31
+ UV_CACHE_DIR: process.env.UV_CACHE_DIR ?? path.join(cacheRoot, "uv"),
32
+ // The uv-managed CPython interpreter must live on the shared runtime volume, not the
33
+ // ephemeral container filesystem. The prepared .venv symlinks bin/python at this interpreter;
34
+ // if it defaults to ~/.local/share/uv/python inside the prepare job pod, the symlink dangles
35
+ // once that pod exits and every later probe/lowering from another pod fails with ENOENT.
36
+ UV_PYTHON_INSTALL_DIR: process.env.UV_PYTHON_INSTALL_DIR ?? path.join(cacheRoot, "python"),
37
+ CCACHE_DIR: process.env.CCACHE_DIR ?? path.join(cacheRoot, "ccache"),
38
+ CCACHE_BASEDIR: process.env.CCACHE_BASEDIR ?? settings.preparedRoot,
39
+ UV_LINK_MODE: process.env.UV_LINK_MODE ?? "copy",
40
+ UV_LOCK_TIMEOUT: process.env.UV_LOCK_TIMEOUT ?? "900"
41
+ };
42
+ }
43
+ function sdkRuntimeToolchainEnv(env = process.env) {
44
+ return {
45
+ PATH: prependPath(LLVM_BIN, env.PATH),
46
+ LD_LIBRARY_PATH: prependPath(LLVM_LIB, env.LD_LIBRARY_PATH),
47
+ CC: env.CC ?? "clang-20",
48
+ CXX: env.CXX ?? "clang++-20",
49
+ CMAKE_PREFIX_PATH: env.CMAKE_PREFIX_PATH ?? LLVM_ROOT,
50
+ LLVM_DIR: env.LLVM_DIR ?? `${LLVM_LIB}/cmake/llvm`,
51
+ MLIR_DIR: env.MLIR_DIR ?? `${LLVM_LIB}/cmake/mlir`,
52
+ OpenMP_ROOT: env.OpenMP_ROOT ?? LLVM_ROOT
53
+ };
54
+ }
55
+ function prependPath(prefix, value) {
56
+ return value ? `${prefix}${path.delimiter}${value}` : prefix;
57
+ }
58
+ async function readSdkPrepareCacheState(settings) {
59
+ try {
60
+ const parsed = JSON.parse(await readFile(sdkPrepareCacheStatePath(settings), "utf8"));
61
+ return parseCacheState(parsed);
62
+ } catch {
63
+ return void 0;
64
+ }
65
+ }
66
+ function sdkPrepareCacheStateMatches(settings, state, commitSha, sourceFingerprint) {
67
+ return Boolean(state) && state?.status === "ready" && state.repoUrl === settings.repoUrl && state.branch === settings.branch && state.fingerprint === sdkPrepareCacheFingerprint(settings) && (state.sourceFingerprint ? state.sourceFingerprint === sourceFingerprint : state.commitSha === commitSha) && path.resolve(state.preparedRoot) === settings.preparedRoot;
68
+ }
69
+ async function writeSdkPrepareCacheState(settings, commitSha, sourceFingerprint, updatedAt) {
70
+ const state = {
71
+ status: "ready",
72
+ repoUrl: settings.repoUrl,
73
+ branch: settings.branch,
74
+ commitSha,
75
+ fingerprint: sdkPrepareCacheFingerprint(settings),
76
+ sourceFingerprint,
77
+ preparedRoot: settings.preparedRoot,
78
+ updatedAt
79
+ };
80
+ const statePath = sdkPrepareCacheStatePath(settings);
81
+ await mkdir(path.dirname(statePath), { recursive: true });
82
+ await writeFile(statePath, `${JSON.stringify(state, null, 2)}
83
+ `);
84
+ return state;
85
+ }
86
+ function parseCacheState(value) {
87
+ if (!isRecord(value)) return void 0;
88
+ if (value.status !== "ready") return void 0;
89
+ const repoUrl = readString(value, "repoUrl");
90
+ const branch = readString(value, "branch");
91
+ const commitSha = readString(value, "commitSha");
92
+ const fingerprint = readString(value, "fingerprint");
93
+ const sourceFingerprint = readString(value, "sourceFingerprint");
94
+ const preparedRoot = readString(value, "preparedRoot");
95
+ const updatedAt = readString(value, "updatedAt");
96
+ if (!repoUrl || !branch || !commitSha || !fingerprint || !preparedRoot || !updatedAt) return void 0;
97
+ return { status: "ready", repoUrl, branch, commitSha, fingerprint, sourceFingerprint, preparedRoot, updatedAt };
98
+ }
99
+ function readString(record, key) {
100
+ const value = record[key];
101
+ return typeof value === "string" && value.trim() ? value : void 0;
102
+ }
103
+ function isRecord(value) {
104
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
105
+ }
106
+
107
+ export {
108
+ SDK_PREPARE_SOURCE_PATHS,
109
+ sdkPrepareCacheStatePath,
110
+ sdkPrepareCacheFingerprint,
111
+ sdkPrepareSourceFingerprint,
112
+ sdkPrepareCacheEnv,
113
+ sdkRuntimeToolchainEnv,
114
+ readSdkPrepareCacheState,
115
+ sdkPrepareCacheStateMatches,
116
+ writeSdkPrepareCacheState
117
+ };
@@ -0,0 +1,348 @@
1
+ import {
2
+ LagoraCliConfigStore,
3
+ baseApiUrl,
4
+ defaultApiUrl,
5
+ requestHeaders
6
+ } from "./chunk-AZ3EEBVD.js";
7
+ import "./chunk-TJZVQYBL.js";
8
+
9
+ // scripts/cli-auth.ts
10
+ import { pathToFileURL as packageFileUrl } from "node:url";
11
+ import { execFile } from "node:child_process";
12
+ import { createHash, randomBytes } from "node:crypto";
13
+ import { createServer } from "node:http";
14
+ import { promisify } from "node:util";
15
+ var execFileAsync = promisify(execFile);
16
+ var callbackPath = "/oauth/callback";
17
+ var loginTimeoutMs = 5 * 60 * 1e3;
18
+ var logoutTimeoutMs = 3e3;
19
+ async function loginWithBrowser(apiUrl, openBrowser = openSystemBrowser) {
20
+ const normalizedApiUrl = baseApiUrl(apiUrl);
21
+ const verifier = randomBytes(32).toString("base64url");
22
+ const state = randomBytes(24).toString("base64url");
23
+ const challenge = createHash("sha256").update(verifier).digest("base64url");
24
+ const callback = await listenForCallback(state);
25
+ try {
26
+ const start = new URL(`${normalizedApiUrl}/api/auth/native/start`);
27
+ start.search = new URLSearchParams({
28
+ challenge,
29
+ state,
30
+ client: "cli",
31
+ callback: callback.url
32
+ }).toString();
33
+ console.log(`Open this URL to sign in:
34
+ ${start.toString()}`);
35
+ try {
36
+ await openBrowser(start.toString());
37
+ } catch (error) {
38
+ console.warn(`Could not open a browser automatically: ${error instanceof Error ? error.message : String(error)}`);
39
+ }
40
+ const code = await callback.code;
41
+ return await exchangeCode(normalizedApiUrl, code, verifier);
42
+ } finally {
43
+ await closeServer(callback.server);
44
+ }
45
+ }
46
+ async function validateCliSession(session) {
47
+ if (!session.token) throw new Error("Run `lagora login` first");
48
+ const target = baseApiUrl(session.apiUrl);
49
+ let response;
50
+ try {
51
+ response = await fetch(`${target}/api/auth/native/session`, {
52
+ headers: requestHeaders(session),
53
+ signal: AbortSignal.timeout(15e3)
54
+ });
55
+ } catch {
56
+ throw new Error(`CLI session validation could not reach ${target}`);
57
+ }
58
+ if (response.status === 401) throw new Error("CLI login expired; run `lagora login` again");
59
+ if (!response.ok) throw new Error(`CLI session validation failed for ${target}: HTTP ${response.status}`);
60
+ let payload;
61
+ try {
62
+ payload = await response.json();
63
+ } catch (error) {
64
+ if (error instanceof SyntaxError) {
65
+ throw new Error(`CLI session validation returned an invalid response from ${target}`);
66
+ }
67
+ throw error;
68
+ }
69
+ const parsed = readSessionPayload(payload);
70
+ if (!parsed || parsed.scope !== "cli" || !parsed.user) {
71
+ throw new Error(`CLI session validation returned an invalid response from ${target}`);
72
+ }
73
+ return parsed.user;
74
+ }
75
+ async function revokeCliSession(session) {
76
+ if (!session.token) return;
77
+ const response = await fetch(`${baseApiUrl(session.apiUrl)}/api/auth/native/session`, {
78
+ method: "DELETE",
79
+ headers: requestHeaders(session),
80
+ signal: AbortSignal.timeout(logoutTimeoutMs)
81
+ });
82
+ if (!response.ok && response.status !== 401) {
83
+ throw new Error(`CLI logout failed: HTTP ${response.status}`);
84
+ }
85
+ }
86
+ async function logoutCliSession(session, clearLocalSession, revokeRemoteSession = revokeCliSession) {
87
+ await clearLocalSession();
88
+ if (!session) return { remoteRevoked: true };
89
+ try {
90
+ await revokeRemoteSession(session);
91
+ return { remoteRevoked: true };
92
+ } catch (error) {
93
+ return {
94
+ remoteRevoked: false,
95
+ warning: error instanceof Error ? error.message : String(error)
96
+ };
97
+ }
98
+ }
99
+ async function listCliTokens(session) {
100
+ const payload = await cliTokensRequest(session, "GET", "");
101
+ const tokens = Reflect.get(payload, "tokens");
102
+ if (!Array.isArray(tokens)) throw new Error("CLI token list returned an invalid response");
103
+ return {
104
+ tokens: tokens.map(readTokenSummary).filter((token) => Boolean(token)),
105
+ currentTokenId: readString(payload, "currentTokenId")
106
+ };
107
+ }
108
+ async function revokeCliTokens(session, target) {
109
+ if (Boolean(target.all) === Boolean(target.tokenId)) {
110
+ throw new Error("Usage: lagora tokens revoke --token <token-id> | --all");
111
+ }
112
+ const query = target.all ? "?all=1" : `?tokenId=${encodeURIComponent(target.tokenId ?? "")}`;
113
+ const payload = await cliTokensRequest(session, "DELETE", query);
114
+ const revoked = Reflect.get(payload, "revoked");
115
+ if (!Array.isArray(revoked)) throw new Error("CLI token revoke returned an invalid response");
116
+ return {
117
+ revoked: revoked.filter((id) => typeof id === "string"),
118
+ currentRevoked: Reflect.get(payload, "currentRevoked") === true
119
+ };
120
+ }
121
+ function formatCliTokens(listing) {
122
+ if (listing.tokens.length === 0) return "No active CLI tokens";
123
+ return listing.tokens.map((token) => `${token.id} scope=${token.scope} created=${token.createdAt} expires=${token.expiresAt}${token.current ? " (current)" : ""}`).join("\n");
124
+ }
125
+ async function cliTokensRequest(session, method, query) {
126
+ if (!session.token) throw new Error("Run `lagora login` first");
127
+ const response = await fetch(`${baseApiUrl(session.apiUrl)}/api/auth/native/cli-tokens${query}`, {
128
+ method,
129
+ headers: requestHeaders(session),
130
+ signal: AbortSignal.timeout(15e3)
131
+ });
132
+ if (response.status === 401) throw new Error("CLI login expired; run `lagora login` again");
133
+ if (response.status === 403) throw new Error("That CLI token belongs to another account");
134
+ if (response.status === 404) throw new Error("No such active CLI token");
135
+ if (!response.ok) throw new Error(`CLI token request failed: HTTP ${response.status}`);
136
+ return response.json();
137
+ }
138
+ function readTokenSummary(value) {
139
+ if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
140
+ const id = readString(value, "id");
141
+ if (!id) return void 0;
142
+ return {
143
+ id,
144
+ scope: readString(value, "scope"),
145
+ createdAt: readString(value, "createdAt"),
146
+ expiresAt: readString(value, "expiresAt"),
147
+ current: Reflect.get(value, "current") === true
148
+ };
149
+ }
150
+ async function runTokensCommand(session, store, argv) {
151
+ const subcommand = argv[0] ?? "list";
152
+ if (subcommand === "list") {
153
+ console.log(formatCliTokens(await listCliTokens(session)));
154
+ return;
155
+ }
156
+ if (subcommand !== "revoke") throw new Error("Usage: lagora tokens list|revoke [--token <token-id>|--all]");
157
+ const tokenId = optionValue(argv, "--token");
158
+ const result = await revokeCliTokens(session, {
159
+ ...argv.includes("--all") ? { all: true } : {},
160
+ ...tokenId ? { tokenId } : {}
161
+ });
162
+ console.log(result.revoked.length === 1 ? "Revoked 1 CLI token" : `Revoked ${result.revoked.length} CLI tokens`);
163
+ if (!result.currentRevoked) return;
164
+ await store.clear();
165
+ console.log("This CLI login was revoked; run `lagora login` again");
166
+ }
167
+ var localStoreCommands = /* @__PURE__ */ new Set([
168
+ "report",
169
+ "search",
170
+ "fetch",
171
+ "comment",
172
+ "status",
173
+ "description",
174
+ "suggestion",
175
+ "assign",
176
+ "blocker",
177
+ "feedback",
178
+ "issue",
179
+ "verify"
180
+ ]);
181
+ function invocationRequiresAuth(command, argv) {
182
+ if (argv.includes("--help") || argv.includes("-h")) return false;
183
+ if (command === "skill") return false;
184
+ return !(localStoreCommands.has(command) && argv.includes("--store") && !argv.includes("--api-url"));
185
+ }
186
+ async function main() {
187
+ const command = process.argv[2] ?? "";
188
+ const store = new LagoraCliConfigStore();
189
+ if (command === "login") {
190
+ const apiUrl = optionValue(process.argv.slice(3), "--api-url") ?? process.env.LAGORA_API_URL?.trim() ?? (await store.read()).apiUrl?.trim() ?? defaultApiUrl;
191
+ console.log("Opening your browser. Sign in with Keycloak, then choose the activity profile for that browser.");
192
+ const result = await loginWithBrowser(apiUrl);
193
+ await store.write({ apiUrl: baseApiUrl(apiUrl), name: result.user.displayName, nativeToken: result.token });
194
+ console.log(`Logged in as ${result.user.displayName}`);
195
+ return;
196
+ }
197
+ if (command === "require-auth") {
198
+ const targetCommand = process.argv[3] ?? "";
199
+ if (!invocationRequiresAuth(targetCommand, process.argv.slice(4))) return;
200
+ }
201
+ const stored = await store.apiSession(void 0);
202
+ if (command === "logout") {
203
+ const result = await logoutCliSession(stored, () => store.clear());
204
+ if (result.warning) console.warn(`Local logout completed, but remote token revocation failed: ${result.warning}`);
205
+ console.log("Logged out");
206
+ return;
207
+ }
208
+ if (!stored?.token) throw new Error("Run `lagora login` first");
209
+ const explicitApiUrl = optionValue(process.argv.slice(3), "--api-url");
210
+ if (explicitApiUrl && baseApiUrl(explicitApiUrl) !== baseApiUrl(stored.apiUrl)) {
211
+ throw new Error(`Saved CLI login belongs to ${stored.apiUrl}; run \`lagora login --api-url ${baseApiUrl(explicitApiUrl)}\` first`);
212
+ }
213
+ const session = { apiUrl: stored.apiUrl, cookie: stored.cookie, token: stored.token };
214
+ if (command === "token") {
215
+ process.stdout.write(stored.token);
216
+ return;
217
+ }
218
+ if (command === "target") {
219
+ process.stdout.write(stored.apiUrl);
220
+ return;
221
+ }
222
+ if (command === "tokens") {
223
+ await runTokensCommand(session, store, process.argv.slice(3));
224
+ return;
225
+ }
226
+ const user = await validateCliSession(session);
227
+ if (command === "whoami") {
228
+ console.log(user.displayName);
229
+ return;
230
+ }
231
+ if (command === "require-auth") return;
232
+ throw new Error("Usage: lagora login|logout|whoami|tokens");
233
+ }
234
+ async function listenForCallback(state) {
235
+ let resolveCode;
236
+ let rejectCode;
237
+ const code = new Promise((resolve, reject) => {
238
+ resolveCode = resolve;
239
+ rejectCode = reject;
240
+ });
241
+ const server = createServer((request, response) => {
242
+ const url = new URL(request.url ?? "/", "http://127.0.0.1");
243
+ if (request.method !== "GET" || url.pathname !== callbackPath) {
244
+ response.writeHead(404).end();
245
+ return;
246
+ }
247
+ if (url.searchParams.get("state") !== state) {
248
+ response.writeHead(400, { "Content-Type": "text/plain; charset=utf-8" });
249
+ response.end("Invalid login state.");
250
+ return;
251
+ }
252
+ const authorizationCode = url.searchParams.get("code")?.trim();
253
+ if (!authorizationCode) {
254
+ response.writeHead(400, { "Content-Type": "text/plain; charset=utf-8" });
255
+ response.end("Missing authorization code.");
256
+ return;
257
+ }
258
+ response.writeHead(200, { "Content-Type": "text/plain; charset=utf-8" });
259
+ response.end("Lagora CLI login complete. You can close this window.");
260
+ resolveCode?.(authorizationCode);
261
+ });
262
+ server.on("error", (error) => rejectCode?.(error));
263
+ await new Promise((resolve, reject) => {
264
+ server.listen(0, "127.0.0.1", resolve);
265
+ server.once("error", reject);
266
+ });
267
+ const address = server.address();
268
+ if (!address || typeof address === "string") throw new Error("Failed to bind OAuth callback");
269
+ const timer = setTimeout(() => rejectCode?.(new Error("Browser login timed out")), loginTimeoutMs);
270
+ timer.unref();
271
+ void code.then(() => clearTimeout(timer), () => clearTimeout(timer));
272
+ return { server, url: `http://127.0.0.1:${address.port}${callbackPath}`, code };
273
+ }
274
+ async function exchangeCode(apiUrl, code, codeVerifier) {
275
+ const response = await fetch(`${apiUrl}/api/auth/native/exchange`, {
276
+ method: "POST",
277
+ headers: { "Content-Type": "application/json" },
278
+ body: JSON.stringify({ code, codeVerifier }),
279
+ signal: AbortSignal.timeout(15e3)
280
+ });
281
+ if (!response.ok) throw new Error(`CLI token exchange failed: HTTP ${response.status}`);
282
+ const payload = await response.json();
283
+ const parsed = readSessionPayload(payload);
284
+ const token = readString(payload, "token");
285
+ if (!parsed || parsed.scope !== "cli" || !parsed.user || !token) {
286
+ throw new Error("CLI token exchange returned an invalid response");
287
+ }
288
+ return { token, user: parsed.user };
289
+ }
290
+ function readSessionPayload(value) {
291
+ if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
292
+ const scope = readString(value, "scope");
293
+ const rawUser = Reflect.get(value, "user");
294
+ if (rawUser === null) return { scope, user: null };
295
+ if (!rawUser || typeof rawUser !== "object" || Array.isArray(rawUser)) return void 0;
296
+ const id = readString(rawUser, "id");
297
+ const displayName = readString(rawUser, "displayName");
298
+ return id && displayName ? { scope, user: { id, displayName } } : void 0;
299
+ }
300
+ function readString(value, key) {
301
+ if (!value || typeof value !== "object" || Array.isArray(value)) return "";
302
+ const field = Reflect.get(value, key);
303
+ return typeof field === "string" ? field.trim() : "";
304
+ }
305
+ function optionValue(argv, option) {
306
+ let value;
307
+ let seen = false;
308
+ for (let index = 0; index < argv.length; index += 1) {
309
+ if (argv[index] !== option) continue;
310
+ if (seen) throw new Error(`Pass ${option} at most once`);
311
+ seen = true;
312
+ const candidate = argv[index + 1]?.trim();
313
+ if (!candidate || candidate.startsWith("--")) throw new Error(`${option} requires a value`);
314
+ value = candidate;
315
+ }
316
+ return value;
317
+ }
318
+ async function openSystemBrowser(url) {
319
+ if (process.platform === "darwin") {
320
+ await execFileAsync("open", [url]);
321
+ return;
322
+ }
323
+ if (process.platform === "win32") {
324
+ await execFileAsync("cmd", ["/c", "start", "", url]);
325
+ return;
326
+ }
327
+ await execFileAsync("xdg-open", [url]);
328
+ }
329
+ async function closeServer(server) {
330
+ if (!server.listening) return;
331
+ await new Promise((resolve, reject) => server.close((error) => error ? reject(error) : resolve()));
332
+ }
333
+ if (import.meta.url === packageFileUrl(process.argv[1]).href) {
334
+ main().catch((error) => {
335
+ console.error(error instanceof Error ? error.message : error);
336
+ process.exit(1);
337
+ });
338
+ }
339
+ export {
340
+ formatCliTokens,
341
+ invocationRequiresAuth,
342
+ listCliTokens,
343
+ loginWithBrowser,
344
+ logoutCliSession,
345
+ revokeCliSession,
346
+ revokeCliTokens,
347
+ validateCliSession
348
+ };
@@ -0,0 +1,7 @@
1
+ import {
2
+ LagoraCliConfigStore
3
+ } from "./chunk-AZ3EEBVD.js";
4
+ import "./chunk-TJZVQYBL.js";
5
+ export {
6
+ LagoraCliConfigStore
7
+ };