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,263 @@
1
+ """Running a submitted kernel and checking it against the reference it shipped with.
2
+
3
+ The harness lives here rather than inside the submitted file. A kernel is then
4
+ only a statement of what it computes -- a `@legato.compile` function and,
5
+ optionally, a `golden` reference -- and the part that drives it, compares the
6
+ result and prints the verdict cannot be edited away by whoever is working on the
7
+ kernel.
8
+
9
+ Everything the call needs is read off the kernel's own annotations, the same way
10
+ lowering already synthesizes its arguments, so an author does not repeat the
11
+ shapes a second time in a launcher.
12
+ """
13
+ from __future__ import annotations
14
+
15
+ import ast
16
+ from pathlib import Path
17
+ from typing import Any
18
+
19
+ from legato_runner_annotations import (
20
+ PreflightError,
21
+ annotation_source,
22
+ get_function_def,
23
+ dtype_name,
24
+ eval_annotation,
25
+ is_tensor_annotation,
26
+ load_module,
27
+ place_arguments,
28
+ scalar_arg,
29
+ select_kernel,
30
+ tensor_shape_from_annotation_source,
31
+ top_level_function_names,
32
+ )
33
+ from legato_runner_launchers import kernel_has_launcher, statically_decorated_kernel_names
34
+
35
+ GOLDEN_NAME = "golden"
36
+ INPUTS_NAME = "inputs"
37
+ SHAPES_NAME = "SHAPES"
38
+ OUTPUT_PARAM_NAMES = {"out", "output", "result"}
39
+
40
+ # A parameter's memory space decides how its device tensor has to be laid out.
41
+ # Only the spaces observed in working kernels are listed: an unknown one is
42
+ # refused rather than guessed, because a wrong layout surfaces as a corrupt
43
+ # result or a legalization failure well away from its cause.
44
+ DEVICE_KIND_BY_MEMORY_SPACE = {
45
+ "shared_dram": "dram_word_interleaved",
46
+ "mpu_dram": "mpu_tensor",
47
+ }
48
+
49
+
50
+ def has_playground_contract(kernel_path: Path) -> bool:
51
+ """Whether this file expects the harness to drive it.
52
+
53
+ A file that brings its own launcher keeps running itself -- every kernel
54
+ submitted before this existed does, and they must not change behaviour.
55
+ """
56
+ return bool(statically_decorated_kernel_names(kernel_path)) and not kernel_has_launcher(kernel_path)
57
+
58
+
59
+ def memory_space_of(source: str) -> str:
60
+ """The memory space argument of a tensor_type annotation."""
61
+ call = ast.parse(source, mode="eval").body
62
+ if not isinstance(call, ast.Call) or len(call.args) < 3:
63
+ raise PreflightError(f"tensor annotation is missing its memory space: {source}")
64
+ space = call.args[2]
65
+ if not isinstance(space, ast.Constant) or not isinstance(space.value, str):
66
+ raise PreflightError(f"memory space must be a string literal: {source}")
67
+ return space.value
68
+
69
+
70
+ def device_kind_for(space: str, name: str) -> str:
71
+ kind = DEVICE_KIND_BY_MEMORY_SPACE.get(space)
72
+ if not kind:
73
+ known = ", ".join(sorted(DEVICE_KIND_BY_MEMORY_SPACE))
74
+ raise PreflightError(
75
+ f"parameter '{name}' is in memory space '{space}', which the Playground harness does not know how to "
76
+ f"allocate on the device yet. Supported: {known}. Submit a kernel with its own launcher to run this."
77
+ )
78
+ return kind
79
+
80
+
81
+ def apply_declared_shapes(module: Any, plan: list[dict[str, Any]]) -> None:
82
+ """Let a submission pin the shapes to run at.
83
+
84
+ An annotation may leave a dimension dynamic, and the site then has to invent
85
+ a number -- a failure that only shows up at a particular size would never be
86
+ reproduced, and nothing would say which size was tried. Declaring SHAPES is
87
+ how a report names the one that matters without materialising tensors.
88
+ """
89
+ declared = getattr(module, SHAPES_NAME, None)
90
+ tensors = {entry["name"]: entry for entry in plan if entry["tensor"]}
91
+ if declared is not None:
92
+ if not isinstance(declared, dict):
93
+ raise PreflightError(f"{SHAPES_NAME} must be a dict of parameter name to shape")
94
+ for name, shape in declared.items():
95
+ entry = tensors.get(name)
96
+ if entry is None:
97
+ raise PreflightError(f"{SHAPES_NAME} names '{name}', which is not a tensor parameter of the kernel")
98
+ if not isinstance(shape, (list, tuple)) or not all(isinstance(dim, int) and dim > 0 for dim in shape):
99
+ raise PreflightError(f"{SHAPES_NAME}['{name}'] must be positive integers, got {shape!r}")
100
+ if len(shape) != len(entry["shape"]):
101
+ raise PreflightError(
102
+ f"{SHAPES_NAME}['{name}'] has rank {len(shape)}, but the kernel declares rank {len(entry['shape'])}"
103
+ )
104
+ entry["shape"] = list(shape)
105
+ entry["declared"] = True
106
+
107
+ # Anything still resting on a substituted dimension is said out loud, so a
108
+ # reader knows the size was the site's choice and not the report's.
109
+ for entry in tensors.values():
110
+ if not entry.get("declared") and entry.get("dynamic"):
111
+ print(
112
+ f"[LEGATO_WORKER] shape_substituted {entry['name']}={tuple(entry['shape'])} "
113
+ f"(annotation leaves a dimension dynamic; declare {SHAPES_NAME} to pin it)",
114
+ flush=True,
115
+ )
116
+
117
+
118
+ def parameter_plan(jit_function: Any) -> list[dict[str, Any]]:
119
+ """What each parameter needs: its shape, dtype, device layout and role."""
120
+ function_def = get_function_def(jit_function)
121
+ plan: list[dict[str, Any]] = []
122
+ for arg in function_def.args.args:
123
+ source = annotation_source(arg.annotation)
124
+ type_value = eval_annotation(source, jit_function)
125
+ if not is_tensor_annotation(type_value, source):
126
+ plan.append({"name": arg.arg, "tensor": False, "scalar": scalar_arg(type_value)})
127
+ continue
128
+ name = arg.arg
129
+ space = memory_space_of(source)
130
+ plan.append({
131
+ "name": name,
132
+ "tensor": True,
133
+ "shape": tensor_shape_from_annotation_source(source, jit_function),
134
+ "dtype": dtype_name(type_value, source),
135
+ "kind": device_kind_for(space, name),
136
+ "space": space,
137
+ "output": name.lower() in OUTPUT_PARAM_NAMES or name.lower().endswith(("_out", "_output")),
138
+ "dynamic": "-1" in source,
139
+ })
140
+ return plan
141
+
142
+
143
+ def host_values(module: Any, plan: list[dict[str, Any]], torch: Any) -> dict[str, Any]:
144
+ """The values the kernel and the reference both see.
145
+
146
+ A submitted `inputs()` wins. A failure often only reproduces on particular
147
+ numbers -- zeros, denormals, a specific weight matrix -- and random data
148
+ would quietly run a different case than the one being reported. Without it,
149
+ seeded random data keeps a run reproducible at least against itself.
150
+ """
151
+ wanted = [entry for entry in plan if entry["tensor"] and not entry["output"]]
152
+ supplied = getattr(module, INPUTS_NAME, None)
153
+ if not callable(supplied):
154
+ torch.manual_seed(0)
155
+ return {e["name"]: (torch.rand(*e["shape"]) * 2 - 1).to(getattr(torch, e["dtype"])) for e in wanted}
156
+
157
+ values = supplied()
158
+ if not isinstance(values, dict):
159
+ raise PreflightError(f"{INPUTS_NAME}() must return a dict of parameter name to tensor")
160
+ for entry in wanted:
161
+ if entry["name"] not in values:
162
+ raise PreflightError(f"{INPUTS_NAME}() does not provide '{entry['name']}'")
163
+ shape = tuple(values[entry["name"]].shape)
164
+ if shape != tuple(entry["shape"]):
165
+ raise PreflightError(
166
+ f"{INPUTS_NAME}()['{entry['name']}'] has shape {shape}, but the kernel declares "
167
+ f"{tuple(entry['shape'])}"
168
+ )
169
+ extra = sorted(set(values) - {entry["name"] for entry in wanted})
170
+ if extra:
171
+ raise PreflightError(f"{INPUTS_NAME}() provides {extra}, which the kernel does not take as input")
172
+ return {entry["name"]: values[entry["name"]] for entry in wanted}
173
+
174
+
175
+ def build_arguments(module: Any, jit_function: Any, plan: list[dict[str, Any]]) -> tuple[list[Any], dict[str, Any]]:
176
+ # torch must come first: it autoloads the torch_ha device backend, and
177
+ # importing torch_ha first re-enters a half-initialised torch.
178
+ import torch
179
+ import torch_ha # noqa: F401 registers torch.ops.lpu.*
180
+ host = host_values(module, plan, torch)
181
+ arguments: list[Any] = []
182
+ for entry in plan:
183
+ if not entry["tensor"]:
184
+ arguments.append(entry["scalar"])
185
+ continue
186
+ dtype = getattr(torch, entry["dtype"])
187
+ device_tensor = torch.ops.lpu.empty_with_kind(
188
+ list(entry["shape"]), entry["kind"], dtype=dtype, device="lpu:0",
189
+ )
190
+ if entry["output"]:
191
+ device_tensor.copy_(torch.zeros(*entry["shape"], dtype=dtype))
192
+ else:
193
+ device_tensor.copy_(host[entry["name"]].to(dtype))
194
+ arguments.append(device_tensor)
195
+ entry["device"] = device_tensor
196
+ return place_arguments(jit_function, arguments), host
197
+
198
+
199
+ def pin_single_core(jit_function: Any) -> None:
200
+ """A kernel that only places on core 0 must not be expanded across all cores."""
201
+ kinds = getattr(jit_function, "_param_kinds", None) or getattr(jit_function, "param_kinds", None) or {}
202
+ if any(str(kind).rstrip().removesuffix("_param") == "core" for kind in kinds.values()):
203
+ import legato.launcher
204
+
205
+ legato.launcher._enabled_core_count = lambda: 1
206
+
207
+
208
+ def report_value(module: Any, plan: list[dict[str, Any]], host: dict[str, Any]) -> None:
209
+ """Compare against the kernel's own reference, when it shipped one.
210
+
211
+ A kernel without a reference still runs; it just has nothing to be checked
212
+ against, and says so rather than claiming a pass it cannot support.
213
+ """
214
+ import torch
215
+
216
+ golden = getattr(module, GOLDEN_NAME, None)
217
+ if not callable(golden):
218
+ print(
219
+ "[LEGATO_WORKER] value=unavailable no golden() reference in the submitted kernel",
220
+ flush=True,
221
+ )
222
+ return
223
+
224
+ outputs = [entry for entry in plan if entry.get("tensor") and entry.get("output")]
225
+ if len(outputs) != 1:
226
+ print(f"[LAGORA_VALUE_DETAIL] expected exactly one output parameter, found {len(outputs)}", flush=True)
227
+ print("[LAGORA_VALUE] fail", flush=True)
228
+ return
229
+
230
+ expected = golden(**host)
231
+ actual = outputs[0]["device"].cpu().float()
232
+ expected = expected.float() if hasattr(expected, "float") else expected
233
+ rtol = float(getattr(module, "RTOL", 0.05))
234
+ atol = float(getattr(module, "ATOL", 0.2))
235
+
236
+ if tuple(actual.shape) != tuple(expected.shape):
237
+ print(f"[LAGORA_VALUE_DETAIL] shape {tuple(actual.shape)} != expected {tuple(expected.shape)}", flush=True)
238
+ print("[LAGORA_VALUE] fail", flush=True)
239
+ return
240
+ if not torch.isfinite(actual).all():
241
+ print("[LAGORA_VALUE_DETAIL] result contains non-finite values", flush=True)
242
+ print("[LAGORA_VALUE] fail", flush=True)
243
+ return
244
+ max_abs = (actual - expected).abs().max().item()
245
+ print(f"[LAGORA_VALUE_DETAIL] max_abs_diff={max_abs:.6f} rtol={rtol} atol={atol}", flush=True)
246
+ print(f"[LAGORA_VALUE] {'pass' if torch.allclose(actual, expected, rtol=rtol, atol=atol) else 'fail'}", flush=True)
247
+
248
+
249
+ def run_playground_kernel(kernel_path: Path, output_root: Path) -> None:
250
+ import legato
251
+
252
+ module = load_module(kernel_path)
253
+ _, jit_function = select_kernel(module, None, top_level_function_names(kernel_path))
254
+ plan = parameter_plan(jit_function)
255
+ apply_declared_shapes(module, plan)
256
+ arguments, host = build_arguments(module, jit_function, plan)
257
+ pin_single_core(jit_function)
258
+
259
+ print("[LAGORA_STAGE] BINARY", flush=True)
260
+ with legato.session(output_type=legato.OutputType.BINARY, output_path=str(output_root)):
261
+ jit_function(*arguments)
262
+
263
+ report_value(module, plan, host)
@@ -0,0 +1,41 @@
1
+ // scripts/announce.ts
2
+ function parseArgs(argv) {
3
+ if (argv[0] !== "create") {
4
+ throw new Error("Usage: lagora announce create --api-url <url> --title <title> --body <body> [--link <url>] [--token <token>]");
5
+ }
6
+ const values = /* @__PURE__ */ new Map();
7
+ for (let index = 1; index < argv.length; index += 2) {
8
+ const option = argv[index];
9
+ const value = argv[index + 1];
10
+ if (!option?.startsWith("--") || !value) throw new Error(`${option ?? "option"} requires a value`);
11
+ values.set(option, value);
12
+ }
13
+ return {
14
+ apiUrl: required(values.get("--api-url"), "--api-url").replace(/\/+$/, ""),
15
+ title: required(values.get("--title"), "--title"),
16
+ body: required(values.get("--body"), "--body"),
17
+ linkUrl: values.get("--link")?.trim() || void 0,
18
+ token: required(values.get("--token") ?? process.env.LAGORA_ANNOUNCEMENT_TOKEN, "--token or LAGORA_ANNOUNCEMENT_TOKEN")
19
+ };
20
+ }
21
+ function required(value, name) {
22
+ if (!value?.trim()) throw new Error(`${name} is required`);
23
+ return value.trim();
24
+ }
25
+ async function main() {
26
+ const args = parseArgs(process.argv.slice(2));
27
+ const response = await fetch(`${args.apiUrl}/api/announcements`, {
28
+ method: "POST",
29
+ headers: {
30
+ authorization: `Bearer ${args.token}`,
31
+ "content-type": "application/json"
32
+ },
33
+ body: JSON.stringify({ title: args.title, body: args.body, linkUrl: args.linkUrl })
34
+ });
35
+ const body = await response.text();
36
+ if (!response.ok) throw new Error(`Announcement failed: HTTP ${response.status} ${body.slice(0, 240)}`);
37
+ const payload = JSON.parse(body);
38
+ const delivered = payload && typeof payload === "object" ? Reflect.get(payload, "delivered") : void 0;
39
+ console.log(`Announcement published to ${typeof delivered === "number" ? delivered : "unknown"} users.`);
40
+ }
41
+ await main();
@@ -0,0 +1,90 @@
1
+ """What the site will be able to do with a kernel, reported at submission time.
2
+
3
+ Informational, never fatal. An arbitrary kernel is a perfectly good bug report
4
+ -- most of them are -- and refusing it because it cannot be value-checked would
5
+ turn a reporting tool into a formatting gate. But the reporter is the one person
6
+ who knows what the right answer was, and this is the moment to say so; asking
7
+ later means someone else reconstructing their intent.
8
+ """
9
+ from __future__ import annotations
10
+
11
+ import argparse
12
+ import ast
13
+ import sys
14
+ from pathlib import Path
15
+
16
+
17
+ def defines_golden(tree: ast.Module) -> bool:
18
+ return any(isinstance(node, ast.FunctionDef) and node.name == "golden" for node in tree.body)
19
+
20
+
21
+ def decorated_kernels(tree: ast.Module) -> list[str]:
22
+ names = []
23
+ for node in tree.body:
24
+ if not isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
25
+ continue
26
+ for decorator in node.decorator_list:
27
+ text = ast.unparse(decorator)
28
+ if "legato.compile" in text or "legato.primitive" in text:
29
+ names.append(node.name)
30
+ break
31
+ return names
32
+
33
+
34
+ def has_main_guard(tree: ast.Module) -> bool:
35
+ for node in ast.walk(tree):
36
+ if isinstance(node, ast.If) and "__main__" in ast.unparse(node.test):
37
+ return True
38
+ return False
39
+
40
+
41
+ def report(path: Path) -> list[str]:
42
+ try:
43
+ tree = ast.parse(path.read_text())
44
+ except SyntaxError as error:
45
+ return [f"error: {path.name} is not valid Python: {error}"]
46
+
47
+ kernels = decorated_kernels(tree)
48
+ notes: list[str] = []
49
+ if not kernels:
50
+ notes.append(
51
+ f"note: no @legato.compile function found in {path.name}. The site can still store and show it, "
52
+ "but it cannot lower or run it."
53
+ )
54
+ return notes
55
+
56
+ if has_main_guard(tree):
57
+ notes.append(
58
+ f"warning: {path.name} brings its own launcher, so it runs itself and the site's harness steps aside. "
59
+ "A hardcoded session output type can mislabel every requested lowering artifact. Agent-generated "
60
+ "submissions must remove the launcher and follow reference/kernel-with-golden.py."
61
+ )
62
+ return notes
63
+
64
+ if defines_golden(tree):
65
+ notes.append(f"ok: {path.name} defines golden() and {kernels[0]}(); the site can lower, run and check values.")
66
+ else:
67
+ notes.append(
68
+ f"note: {path.name} has no golden() reference, so the site will lower and run it but report the value "
69
+ "check as unavailable. Add one if the correct result is known -- see reference/kernel-with-golden.py "
70
+ "in the lagora skill."
71
+ )
72
+ return notes
73
+
74
+
75
+ def main(argv: list[str] | None = None) -> int:
76
+ parser = argparse.ArgumentParser(description=__doc__)
77
+ parser.add_argument("kernel")
78
+ args = parser.parse_args(argv)
79
+
80
+ path = Path(args.kernel)
81
+ if not path.is_file():
82
+ print(f"error: kernel not found: {path}", file=sys.stderr)
83
+ return 1
84
+ for line in report(path):
85
+ print(f"[lagora] {line}", file=sys.stderr)
86
+ return 0
87
+
88
+
89
+ if __name__ == "__main__":
90
+ raise SystemExit(main())
@@ -0,0 +1,100 @@
1
+ // lib/server/encrypted-secrets.ts
2
+ import { createCipheriv, createDecipheriv, createHash, randomBytes } from "node:crypto";
3
+ import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
4
+ import path from "node:path";
5
+ var SECRET_NAMES = ["githubToken", "sdkCliToken", "announcementToken"];
6
+ var SettingsEncryptionKeyMissingError = class extends Error {
7
+ constructor() {
8
+ super("AGORA_SETTINGS_ENCRYPTION_KEY is required before server secrets can be saved");
9
+ this.name = "SettingsEncryptionKeyMissingError";
10
+ }
11
+ };
12
+ async function readSecret(name) {
13
+ const value = (await readSecretFile())[name];
14
+ return value ? decryptText(value) : void 0;
15
+ }
16
+ async function readGithubToken() {
17
+ return await readSecret("githubToken");
18
+ }
19
+ async function readSecretFile() {
20
+ try {
21
+ const raw = await readFile(secretPath(), "utf8");
22
+ return parseSecretFile(JSON.parse(raw));
23
+ } catch (error) {
24
+ if (error instanceof Error) return {};
25
+ throw error;
26
+ }
27
+ }
28
+ function decryptText(value) {
29
+ const decipher = createDecipheriv("aes-256-gcm", encryptionKey(), Buffer.from(value.iv, "base64"));
30
+ decipher.setAuthTag(Buffer.from(value.tag, "base64"));
31
+ return Buffer.concat([
32
+ decipher.update(Buffer.from(value.ciphertext, "base64")),
33
+ decipher.final()
34
+ ]).toString("utf8");
35
+ }
36
+ function encryptionKey() {
37
+ const configured = process.env.AGORA_SETTINGS_ENCRYPTION_KEY?.trim();
38
+ if (!configured) throw new SettingsEncryptionKeyMissingError();
39
+ return createHash("sha256").update(configured).digest();
40
+ }
41
+ function parseSecretFile(value) {
42
+ if (!isRecord(value)) return {};
43
+ return Object.fromEntries(
44
+ SECRET_NAMES.flatMap((name) => {
45
+ const parsed = parseEncryptedValue(value[name]);
46
+ return parsed ? [[name, parsed]] : [];
47
+ })
48
+ );
49
+ }
50
+ function parseEncryptedValue(value) {
51
+ if (!isRecord(value)) return void 0;
52
+ if (value.algorithm !== "aes-256-gcm") return void 0;
53
+ if (typeof value.iv !== "string") return void 0;
54
+ if (typeof value.tag !== "string") return void 0;
55
+ if (typeof value.ciphertext !== "string") return void 0;
56
+ if (typeof value.updatedAt !== "string") return void 0;
57
+ return {
58
+ algorithm: "aes-256-gcm",
59
+ iv: value.iv,
60
+ tag: value.tag,
61
+ ciphertext: value.ciphertext,
62
+ updatedAt: value.updatedAt
63
+ };
64
+ }
65
+ function settingsRoot() {
66
+ return process.env.AGORA_SETTINGS_DIR ? path.resolve(process.env.AGORA_SETTINGS_DIR) : path.join(
67
+ /*turbopackIgnore: true*/
68
+ process.cwd(),
69
+ ".local-data",
70
+ "settings"
71
+ );
72
+ }
73
+ function secretPath() {
74
+ return path.join(settingsRoot(), "secrets.json");
75
+ }
76
+ function isRecord(value) {
77
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
78
+ }
79
+
80
+ // lib/server/git-auth.ts
81
+ async function configuredGithubToken() {
82
+ return await readGithubToken() ?? process.env.AGORA_GITHUB_TOKEN ?? process.env.GITHUB_TOKEN;
83
+ }
84
+ function gitEnv(token) {
85
+ if (!token) return process.env;
86
+ const encoded = Buffer.from(`x-access-token:${token}`).toString("base64");
87
+ return {
88
+ ...process.env,
89
+ GIT_CONFIG_COUNT: "2",
90
+ GIT_CONFIG_KEY_0: "http.https://github.com/.extraheader",
91
+ GIT_CONFIG_VALUE_0: `AUTHORIZATION: basic ${encoded}`,
92
+ GIT_CONFIG_KEY_1: "url.https://github.com/.insteadOf",
93
+ GIT_CONFIG_VALUE_1: "git@github.com:"
94
+ };
95
+ }
96
+
97
+ export {
98
+ configuredGithubToken,
99
+ gitEnv
100
+ };
@@ -0,0 +1,29 @@
1
+ // lib/server/sdk-package-index.ts
2
+ function sdkPackageIndex(env = process.env) {
3
+ const url = env.AGORA_SDK_INDEX_URL?.trim();
4
+ if (!url) return void 0;
5
+ return { url, host: env.AGORA_SDK_INDEX_HOST?.trim() || hostFromUrl(url) };
6
+ }
7
+ function sdkPackageIndexEnv(env = process.env, pin) {
8
+ const index = sdkPackageIndex(env);
9
+ const versions = typeof pin === "string" ? { legatoVersion: pin } : pin ?? {};
10
+ return [
11
+ { name: "AGORA_SDK_INDEX_URL", value: index?.url ?? "" },
12
+ { name: "AGORA_SDK_INDEX_HOST", value: index?.host ?? "" },
13
+ // Empty means "resolve the newest in the job" for both lines.
14
+ { name: "AGORA_LEGATO_PACKAGE_VERSION", value: versions.legatoVersion?.trim() ?? "" },
15
+ { name: "AGORA_SDK_PACKAGE_VERSION", value: versions.sdkVersion?.trim() ?? "" }
16
+ ];
17
+ }
18
+ function hostFromUrl(url) {
19
+ try {
20
+ return new URL(url).host || void 0;
21
+ } catch {
22
+ return void 0;
23
+ }
24
+ }
25
+
26
+ export {
27
+ sdkPackageIndex,
28
+ sdkPackageIndexEnv
29
+ };
@@ -0,0 +1,137 @@
1
+ import {
2
+ internalAppUrl
3
+ } from "./chunk-TJZVQYBL.js";
4
+
5
+ // scripts/cli-config.ts
6
+ import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
7
+ import os from "node:os";
8
+ import path from "node:path";
9
+
10
+ // scripts/api-session.ts
11
+ var defaultApiUrl = internalAppUrl;
12
+ function apiSession(apiUrl, credentials = {}) {
13
+ return {
14
+ apiUrl: baseApiUrl(apiUrl),
15
+ cookie: credentials.cookie?.trim() || void 0,
16
+ token: credentials.token?.trim() || void 0
17
+ };
18
+ }
19
+ async function readJsonFromApi(endpoint, session) {
20
+ const response = await fetch(endpoint, {
21
+ headers: requestHeaders(session)
22
+ });
23
+ if (!response.ok) {
24
+ const body = await response.text();
25
+ throw new Error(`API request failed: HTTP ${response.status} ${body.slice(0, 240)}`);
26
+ }
27
+ return response.json();
28
+ }
29
+ async function writeJsonToApi(endpoint, method, body, session) {
30
+ const response = await fetch(endpoint, {
31
+ method,
32
+ headers: requestHeaders(session, { "Content-Type": "application/json" }),
33
+ body: JSON.stringify(body)
34
+ });
35
+ if (!response.ok) {
36
+ const responseBody = await response.text();
37
+ throw new Error(`API request failed: HTTP ${response.status} ${responseBody.slice(0, 240)}`);
38
+ }
39
+ return response.json();
40
+ }
41
+ function baseApiUrl(apiUrl) {
42
+ const url = new URL(apiUrl.trim());
43
+ if (url.username || url.password || url.search || url.hash) {
44
+ throw new Error("Lagora API URL must not contain credentials, query parameters, or fragments");
45
+ }
46
+ const loopback = url.hostname === "localhost" || url.hostname === "127.0.0.1" || url.hostname === "[::1]";
47
+ if (url.protocol !== "https:" && !(url.protocol === "http:" && loopback)) {
48
+ throw new Error("Lagora API URL must use HTTPS except on loopback");
49
+ }
50
+ url.pathname = url.pathname.replace(/\/+$/, "");
51
+ return url.toString().replace(/\/+$/, "");
52
+ }
53
+ function requestHeaders(session, headers) {
54
+ return {
55
+ ...headers,
56
+ ...session?.cookie ? { Cookie: session.cookie } : {},
57
+ ...session?.token ? { Authorization: `Bearer ${session.token}` } : {}
58
+ };
59
+ }
60
+
61
+ // scripts/cli-config.ts
62
+ var LagoraCliConfigStore = class {
63
+ constructor(configPath = defaultConfigPath()) {
64
+ this.path = configPath;
65
+ }
66
+ async read() {
67
+ try {
68
+ const raw = await readFile(this.path, "utf8");
69
+ const parsed = JSON.parse(raw);
70
+ return isConfig(parsed) ? parsed : {};
71
+ } catch (error) {
72
+ if (isMissingFileError(error)) return {};
73
+ if (error instanceof SyntaxError) return {};
74
+ throw error;
75
+ }
76
+ }
77
+ async write(config) {
78
+ await mkdir(path.dirname(this.path), { recursive: true });
79
+ await writeFile(this.path, `${JSON.stringify(config, null, 2)}
80
+ `, { mode: 384 });
81
+ }
82
+ async clear() {
83
+ await rm(this.path, { force: true });
84
+ }
85
+ async author(explicitAuthor) {
86
+ const trimmed = explicitAuthor?.trim();
87
+ if (trimmed) return trimmed;
88
+ const config = await this.read();
89
+ return config.name?.trim() || "Language Developer";
90
+ }
91
+ /** Resolves in order: --api-url, LAGORA_API_URL, the saved login, the default. */
92
+ async apiSession(explicitApiUrl) {
93
+ const config = await this.read();
94
+ const explicit = normalizedApiUrl(explicitApiUrl ?? process.env.LAGORA_API_URL);
95
+ const configured = normalizedApiUrl(config.apiUrl);
96
+ if (explicit && configured && (config.nativeToken?.trim() || config.sessionCookie?.trim()) && explicit !== configured) {
97
+ throw new Error(`Saved CLI login belongs to ${configured}; run \`lagora login --api-url ${explicit}\` first`);
98
+ }
99
+ const apiUrl = explicit || configured || defaultApiUrl;
100
+ return {
101
+ apiUrl,
102
+ cookie: config.sessionCookie?.trim() || void 0,
103
+ token: config.nativeToken?.trim() || void 0
104
+ };
105
+ }
106
+ };
107
+ function normalizedApiUrl(value) {
108
+ return value?.trim() ? baseApiUrl(value) : void 0;
109
+ }
110
+ function defaultConfigPath() {
111
+ const configured = process.env.LAGORA_CONFIG?.trim();
112
+ if (configured) return path.resolve(configured);
113
+ const configHome = process.env.XDG_CONFIG_HOME?.trim() || path.join(os.homedir(), ".config");
114
+ return path.join(configHome, "lagora", "config.json");
115
+ }
116
+ function isConfig(value) {
117
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
118
+ const name = Reflect.get(value, "name");
119
+ const passcode = Reflect.get(value, "passcode");
120
+ const apiUrl = Reflect.get(value, "apiUrl");
121
+ const sessionCookie = Reflect.get(value, "sessionCookie");
122
+ const nativeToken = Reflect.get(value, "nativeToken");
123
+ return (name === void 0 || typeof name === "string") && (passcode === void 0 || typeof passcode === "string") && (apiUrl === void 0 || typeof apiUrl === "string") && (sessionCookie === void 0 || typeof sessionCookie === "string") && (nativeToken === void 0 || typeof nativeToken === "string");
124
+ }
125
+ function isMissingFileError(error) {
126
+ return error instanceof Error && "code" in error && Reflect.get(error, "code") === "ENOENT";
127
+ }
128
+
129
+ export {
130
+ defaultApiUrl,
131
+ apiSession,
132
+ readJsonFromApi,
133
+ writeJsonToApi,
134
+ baseApiUrl,
135
+ requestHeaders,
136
+ LagoraCliConfigStore
137
+ };