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.
- package/README.md +138 -0
- package/dist/help.txt +70 -0
- package/dist/lagora.js +342 -0
- package/dist/report-help.txt +5 -0
- package/dist/scripts/agora_playground_harness.py +263 -0
- package/dist/scripts/announce.js +41 -0
- package/dist/scripts/check-kernel-submission.py +90 -0
- package/dist/scripts/chunk-2EAJVB5D.js +100 -0
- package/dist/scripts/chunk-2KTLCUFI.js +29 -0
- package/dist/scripts/chunk-AZ3EEBVD.js +137 -0
- package/dist/scripts/chunk-NBJMYAOA.js +2128 -0
- package/dist/scripts/chunk-NCJMUBTG.js +125 -0
- package/dist/scripts/chunk-QJPQHKIO.js +23 -0
- package/dist/scripts/chunk-RIR5KGHC.js +33 -0
- package/dist/scripts/chunk-TJZVQYBL.js +8 -0
- package/dist/scripts/chunk-UHJXD4TG.js +18 -0
- package/dist/scripts/chunk-UQ6I6VTY.js +117 -0
- package/dist/scripts/cli-auth.js +348 -0
- package/dist/scripts/cli-config-IA7EOSYD.js +7 -0
- package/dist/scripts/install-skill.js +199 -0
- package/dist/scripts/issue-local-client-DZUXZOKY.js +22 -0
- package/dist/scripts/issue-search.js +1823 -0
- package/dist/scripts/issue.js +386 -0
- package/dist/scripts/keycloak-provision.js +986 -0
- package/dist/scripts/legato-fsim-runner.py +126 -0
- package/dist/scripts/legato-lowering-runner.py +156 -0
- package/dist/scripts/legato_runner_annotations.py +235 -0
- package/dist/scripts/legato_runner_env.py +91 -0
- package/dist/scripts/legato_runner_launchers.py +287 -0
- package/dist/scripts/legato_runner_script_wrapper.py +193 -0
- package/dist/scripts/notifications-EU43SIEV.js +624 -0
- package/dist/scripts/playground.js +408 -0
- package/dist/scripts/report-bundle-sync-3U7QTP4Z.js +215 -0
- package/dist/scripts/report.js +104 -0
- package/dist/scripts/resolve-sdk-package-version.py +151 -0
- package/dist/scripts/sdk-runtime-JE6H2PB2.js +992 -0
- package/dist/scripts/sdk-runtime-kubernetes-job-KOWL4ITV.js +479 -0
- package/dist/scripts/sdk-runtime-smoke.py +168 -0
- package/dist/scripts/sdk.js +256 -0
- package/dist/scripts/site-feedback-CAPE5MPX.js +136 -0
- package/dist/scripts/site-feedback-rate-limit-5BU2WSFE.js +86 -0
- package/dist/scripts/site-feedback.js +117 -0
- package/dist/scripts/storage-234FBH54.js +67 -0
- package/dist/scripts/submit-issue.sh +489 -0
- package/dist/scripts/verification-3QCY66QW.js +772 -0
- package/dist/scripts/verify-issue.js +144 -0
- package/dist/skills/legato-agora-cli/SKILL.md +556 -0
- package/dist/skills/legato-agora-cli/agents/openai.yaml +7 -0
- package/dist/skills/legato-agora-cli/reference/kernel-with-golden.py +84 -0
- package/dist/skills/legato-site-feedback/SKILL.md +49 -0
- package/dist/skills/legato-site-feedback/agents/openai.yaml +7 -0
- package/package.json +16 -0
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
// scripts/sdk-api-client.ts
|
|
2
|
+
async function readSdkSnapshot(input) {
|
|
3
|
+
const flags = [
|
|
4
|
+
...input.includeDiskUsage ? ["disk=1"] : [],
|
|
5
|
+
...input.includePublished ? ["published=1"] : []
|
|
6
|
+
];
|
|
7
|
+
const query = flags.length > 0 ? `?${flags.join("&")}` : "";
|
|
8
|
+
const payload = await requestJson(`${baseApiUrl(input.apiUrl)}/api/sdk${query}`, "GET", input.token);
|
|
9
|
+
return readSnapshot(payload);
|
|
10
|
+
}
|
|
11
|
+
async function readSdkPrepareLog(input) {
|
|
12
|
+
const payload = await requestJson(`${baseApiUrl(input.apiUrl)}/api/sdk/log`, "GET", input.token);
|
|
13
|
+
const text2 = record(payload, "SDK log").text;
|
|
14
|
+
return typeof text2 === "string" ? text2 : "";
|
|
15
|
+
}
|
|
16
|
+
async function startSdkUpdate(input) {
|
|
17
|
+
const flags = [
|
|
18
|
+
...input.force ? ["force=1"] : [],
|
|
19
|
+
...input.packageVersion ? [`version=${encodeURIComponent(input.packageVersion)}`] : []
|
|
20
|
+
];
|
|
21
|
+
const query = flags.length > 0 ? `?${flags.join("&")}` : "";
|
|
22
|
+
const response = await fetch(`${baseApiUrl(input.apiUrl)}/api/sdk/update${query}`, {
|
|
23
|
+
method: "POST",
|
|
24
|
+
headers: { authorization: `Bearer ${input.token}` }
|
|
25
|
+
});
|
|
26
|
+
const body = await response.text();
|
|
27
|
+
if (!response.ok && response.status !== 409) {
|
|
28
|
+
throw new Error(`SDK update request failed: HTTP ${response.status} ${body.slice(0, 240)}`);
|
|
29
|
+
}
|
|
30
|
+
const payload = parseJson(body, "SDK update");
|
|
31
|
+
const parsed = record(payload, "SDK update");
|
|
32
|
+
return {
|
|
33
|
+
started: parsed.started === true,
|
|
34
|
+
reason: typeof parsed.reason === "string" ? parsed.reason : void 0
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
async function requestJson(endpoint, method, token) {
|
|
38
|
+
const response = await fetch(endpoint, {
|
|
39
|
+
method,
|
|
40
|
+
headers: { authorization: `Bearer ${token}` }
|
|
41
|
+
});
|
|
42
|
+
const body = await response.text();
|
|
43
|
+
if (!response.ok) throw new Error(`SDK API request failed: HTTP ${response.status} ${body.slice(0, 240)}`);
|
|
44
|
+
return parseJson(body, "SDK API");
|
|
45
|
+
}
|
|
46
|
+
function parseJson(body, label) {
|
|
47
|
+
try {
|
|
48
|
+
return JSON.parse(body);
|
|
49
|
+
} catch {
|
|
50
|
+
throw new Error(`${label} response was not JSON: ${body.slice(0, 240)}`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
function readSnapshot(payload) {
|
|
54
|
+
const parsed = record(payload, "SDK status");
|
|
55
|
+
const prepare = record(parsed.prepare, "SDK status prepare");
|
|
56
|
+
const versions = Array.isArray(parsed.versions) ? parsed.versions : [];
|
|
57
|
+
return {
|
|
58
|
+
prepare: {
|
|
59
|
+
status: text(prepare.status) ?? "unknown",
|
|
60
|
+
percent: typeof prepare.percent === "number" ? prepare.percent : 0,
|
|
61
|
+
currentStep: text(prepare.currentStep) ?? "",
|
|
62
|
+
repoUrl: text(prepare.repoUrl) ?? "",
|
|
63
|
+
branch: text(prepare.branch) ?? "",
|
|
64
|
+
preparedRoot: text(prepare.preparedRoot) ?? "",
|
|
65
|
+
commitSha: text(prepare.commitSha),
|
|
66
|
+
runId: text(prepare.runId),
|
|
67
|
+
startedAt: text(prepare.startedAt),
|
|
68
|
+
completedAt: text(prepare.completedAt),
|
|
69
|
+
message: text(prepare.message),
|
|
70
|
+
result: readResult(prepare.result),
|
|
71
|
+
events: readEvents(prepare.events)
|
|
72
|
+
},
|
|
73
|
+
versions: versions.map(readVersion),
|
|
74
|
+
publishedVersions: Array.isArray(parsed.publishedVersions) ? parsed.publishedVersions.filter((item) => typeof item === "string") : []
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
function readResult(value) {
|
|
78
|
+
if (!isRecord(value)) return void 0;
|
|
79
|
+
return {
|
|
80
|
+
status: text(value.status),
|
|
81
|
+
exitCode: typeof value.exitCode === "number" ? value.exitCode : void 0,
|
|
82
|
+
message: text(value.message)
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
function readEvents(value) {
|
|
86
|
+
if (!Array.isArray(value)) return [];
|
|
87
|
+
return value.flatMap((item) => {
|
|
88
|
+
if (!isRecord(item)) return [];
|
|
89
|
+
const at = text(item.at);
|
|
90
|
+
const step = text(item.step);
|
|
91
|
+
const message = text(item.message);
|
|
92
|
+
return at && step && message ? [{ at, step, message }] : [];
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
function readVersion(value) {
|
|
96
|
+
const parsed = record(value, "SDK version");
|
|
97
|
+
return {
|
|
98
|
+
name: text(parsed.name) ?? "",
|
|
99
|
+
status: text(parsed.status) ?? "unknown",
|
|
100
|
+
active: parsed.active === true,
|
|
101
|
+
branch: text(parsed.branch) ?? "",
|
|
102
|
+
commitSha: text(parsed.commitSha),
|
|
103
|
+
commitSubject: text(parsed.commitSubject),
|
|
104
|
+
completedAt: text(parsed.completedAt),
|
|
105
|
+
diskUsage: text(parsed.diskUsage),
|
|
106
|
+
packageVersion: text(parsed.packageVersion)
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
function record(value, label) {
|
|
110
|
+
if (!isRecord(value)) throw new Error(`${label} response has an unsupported shape`);
|
|
111
|
+
return value;
|
|
112
|
+
}
|
|
113
|
+
function isRecord(value) {
|
|
114
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
115
|
+
}
|
|
116
|
+
function text(value) {
|
|
117
|
+
return typeof value === "string" && value.trim() ? value.trim() : void 0;
|
|
118
|
+
}
|
|
119
|
+
function baseApiUrl(apiUrl) {
|
|
120
|
+
return apiUrl.trim().replace(/\/+$/, "");
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// scripts/sdk.ts
|
|
124
|
+
var USAGE = "Usage: lagora sdk status|list|versions|log|update [--api-url url] [--token token] [--disk] [--force] [--watch] [--version published-version] [--interval seconds]";
|
|
125
|
+
var DEFAULT_WATCH_INTERVAL_SECONDS = 30;
|
|
126
|
+
var TERMINAL_STATUSES = /* @__PURE__ */ new Set(["ready", "failed"]);
|
|
127
|
+
function parseArgs(argv) {
|
|
128
|
+
const values = /* @__PURE__ */ new Map();
|
|
129
|
+
const flags = /* @__PURE__ */ new Set();
|
|
130
|
+
for (let index = 1; index < argv.length; index += 1) {
|
|
131
|
+
const option = argv[index];
|
|
132
|
+
if (!option?.startsWith("--")) continue;
|
|
133
|
+
if (option === "--force" || option === "--watch" || option === "--disk") {
|
|
134
|
+
flags.add(option);
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
const value = argv[index + 1];
|
|
138
|
+
if (!value || value.startsWith("--")) throw new Error(`${option} requires a value`);
|
|
139
|
+
values.set(option, value);
|
|
140
|
+
index += 1;
|
|
141
|
+
}
|
|
142
|
+
const interval = Number.parseInt(values.get("--interval") ?? "", 10);
|
|
143
|
+
return {
|
|
144
|
+
command: argv[0] ?? "status",
|
|
145
|
+
apiUrl: required(values.get("--api-url") ?? process.env.LAGORA_API_URL, "--api-url or LAGORA_API_URL"),
|
|
146
|
+
token: required(values.get("--token") ?? process.env.LAGORA_SDK_TOKEN, "--token or LAGORA_SDK_TOKEN"),
|
|
147
|
+
force: flags.has("--force"),
|
|
148
|
+
watch: flags.has("--watch"),
|
|
149
|
+
disk: flags.has("--disk"),
|
|
150
|
+
packageVersion: values.get("--version"),
|
|
151
|
+
intervalMs: (Number.isFinite(interval) && interval > 0 ? interval : DEFAULT_WATCH_INTERVAL_SECONDS) * 1e3
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
function required(value, name) {
|
|
155
|
+
if (!value?.trim()) throw new Error(`${name} is required`);
|
|
156
|
+
return value.trim();
|
|
157
|
+
}
|
|
158
|
+
async function main() {
|
|
159
|
+
const args = parseArgs(process.argv.slice(2));
|
|
160
|
+
const endpoint = { apiUrl: args.apiUrl, token: args.token };
|
|
161
|
+
if (args.command === "status") {
|
|
162
|
+
printPrepare(await readSdkSnapshot(endpoint));
|
|
163
|
+
if (args.watch) await watchPrepare(args);
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
if (args.command === "list") {
|
|
167
|
+
printVersions(await readSdkSnapshot({ ...endpoint, includeDiskUsage: args.disk }));
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
if (args.command === "versions") {
|
|
171
|
+
const snapshot = await readSdkSnapshot({ ...endpoint, includePublished: true });
|
|
172
|
+
if (snapshot.publishedVersions.length === 0) {
|
|
173
|
+
console.log("No published versions. The package index may be unreachable; 'lagora sdk log' shows which path a prepare took.");
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
const prepared = new Set(snapshot.versions.filter((version) => version.status === "ready").map((version) => version.packageVersion));
|
|
177
|
+
for (const version of snapshot.publishedVersions) {
|
|
178
|
+
console.log(`${prepared.has(version) ? "*" : " "} ${version}`);
|
|
179
|
+
}
|
|
180
|
+
console.log("");
|
|
181
|
+
console.log("* is already prepared. Prepare another with 'lagora sdk update --version <version>'.");
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
if (args.command === "log") {
|
|
185
|
+
const text2 = await readSdkPrepareLog(endpoint);
|
|
186
|
+
console.log(text2.trim() || "No prepare log yet.");
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
if (args.command === "update") {
|
|
190
|
+
const started = await startSdkUpdate({ ...endpoint, force: args.force, packageVersion: args.packageVersion });
|
|
191
|
+
const what = args.packageVersion ? ` (${args.packageVersion})` : args.force ? " (forced rebuild)" : "";
|
|
192
|
+
console.log(started.started ? `SDK update started${what}. Follow it with 'lagora sdk status --watch'.` : `SDK update not started: ${started.reason ?? "unknown reason"}.`);
|
|
193
|
+
if (args.watch) await watchPrepare(args);
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
throw new Error(USAGE);
|
|
197
|
+
}
|
|
198
|
+
async function watchPrepare(args) {
|
|
199
|
+
let lastLine = "";
|
|
200
|
+
for (; ; ) {
|
|
201
|
+
await sleep(args.intervalMs);
|
|
202
|
+
const snapshot = await readSdkSnapshot({ apiUrl: args.apiUrl, token: args.token });
|
|
203
|
+
const line = `${snapshot.prepare.status} ${snapshot.prepare.percent}% \xB7 ${snapshot.prepare.currentStep}`;
|
|
204
|
+
if (line !== lastLine) {
|
|
205
|
+
console.log(`[${(/* @__PURE__ */ new Date()).toISOString()}] ${line}`);
|
|
206
|
+
lastLine = line;
|
|
207
|
+
}
|
|
208
|
+
if (TERMINAL_STATUSES.has(snapshot.prepare.status)) {
|
|
209
|
+
printPrepare(snapshot);
|
|
210
|
+
if (snapshot.prepare.status === "failed") process.exitCode = 1;
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
function printPrepare(snapshot) {
|
|
216
|
+
const prepare = snapshot.prepare;
|
|
217
|
+
console.log(`prepare: ${prepare.status} ${prepare.percent}%`);
|
|
218
|
+
console.log(`step: ${prepare.currentStep || "-"}`);
|
|
219
|
+
console.log(`source: ${prepare.branch}${prepare.commitSha ? ` ${prepare.commitSha.slice(0, 12)}` : ""} \xB7 ${prepare.repoUrl}`);
|
|
220
|
+
console.log(`root: ${prepare.preparedRoot}`);
|
|
221
|
+
if (prepare.startedAt) console.log(`started: ${prepare.startedAt}${prepare.completedAt ? ` \xB7 completed ${prepare.completedAt}` : ""}`);
|
|
222
|
+
if (prepare.result?.status === "failed") {
|
|
223
|
+
console.log(`failure: ${prepare.result.message ?? "prepare failed"}${prepare.result.exitCode === void 0 ? "" : ` (exit ${prepare.result.exitCode})`}`);
|
|
224
|
+
console.log("Run 'lagora sdk log' for the full build output.");
|
|
225
|
+
}
|
|
226
|
+
const ready = snapshot.versions.filter((version) => version.status === "ready").length;
|
|
227
|
+
console.log(`versions: ${snapshot.versions.length} prepared, ${ready} ready`);
|
|
228
|
+
}
|
|
229
|
+
function printVersions(snapshot) {
|
|
230
|
+
if (snapshot.versions.length === 0) {
|
|
231
|
+
console.log("No prepared SDK runtimes.");
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
for (const version of snapshot.versions) {
|
|
235
|
+
console.log(`${version.active ? "*" : " "} ${describeVersion(version)}`);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
function describeVersion(version) {
|
|
239
|
+
const parts = [
|
|
240
|
+
version.name,
|
|
241
|
+
version.status,
|
|
242
|
+
version.packageVersion ?? version.branch,
|
|
243
|
+
version.commitSha?.slice(0, 12),
|
|
244
|
+
version.completedAt,
|
|
245
|
+
version.diskUsage,
|
|
246
|
+
version.commitSubject
|
|
247
|
+
];
|
|
248
|
+
return parts.filter(Boolean).join(" ");
|
|
249
|
+
}
|
|
250
|
+
function sleep(ms) {
|
|
251
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
252
|
+
}
|
|
253
|
+
main().catch((error) => {
|
|
254
|
+
console.error(error instanceof Error ? error.message : error);
|
|
255
|
+
process.exit(1);
|
|
256
|
+
});
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getIssueStoreRoot,
|
|
3
|
+
safeJoin
|
|
4
|
+
} from "./chunk-UHJXD4TG.js";
|
|
5
|
+
|
|
6
|
+
// lib/server/site-feedback.ts
|
|
7
|
+
import { randomUUID } from "node:crypto";
|
|
8
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
9
|
+
import path from "node:path";
|
|
10
|
+
var SITE_FEEDBACK_MESSAGE_MAX_LENGTH = 4e3;
|
|
11
|
+
var FEEDBACK_FILE = "site-feedback.json";
|
|
12
|
+
var DEFAULT_MAX_STORED = 500;
|
|
13
|
+
var DEFAULT_RATE_LIMIT = 5;
|
|
14
|
+
var DEFAULT_RATE_LIMIT_WINDOW_MS = 10 * 60 * 1e3;
|
|
15
|
+
async function listSiteFeedback() {
|
|
16
|
+
const items = await readFeedbackFile();
|
|
17
|
+
return [...items].sort((left, right) => new Date(right.createdAt).getTime() - new Date(left.createdAt).getTime());
|
|
18
|
+
}
|
|
19
|
+
async function addSiteFeedback(input, options = {}) {
|
|
20
|
+
const message = input.message.trim();
|
|
21
|
+
if (!message) return { ok: false, rejection: { code: "message_required" } };
|
|
22
|
+
if (message.length > SITE_FEEDBACK_MESSAGE_MAX_LENGTH) {
|
|
23
|
+
return {
|
|
24
|
+
ok: false,
|
|
25
|
+
rejection: { code: "message_too_long", length: message.length, maxLength: SITE_FEEDBACK_MESSAGE_MAX_LENGTH }
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
const now = options.now ?? /* @__PURE__ */ new Date();
|
|
29
|
+
const accountId = input.accountId?.trim();
|
|
30
|
+
if (accountId) {
|
|
31
|
+
const rejection = await rateLimitRejection(accountId, now.getTime());
|
|
32
|
+
if (rejection) return { ok: false, rejection };
|
|
33
|
+
}
|
|
34
|
+
const feedback = {
|
|
35
|
+
id: randomUUID(),
|
|
36
|
+
message,
|
|
37
|
+
pagePath: normalizedPagePath(input.pagePath),
|
|
38
|
+
reporter: input.reporter?.trim() || "Anonymous",
|
|
39
|
+
status: "open",
|
|
40
|
+
createdAt: now.toISOString()
|
|
41
|
+
};
|
|
42
|
+
const items = await readFeedbackFile();
|
|
43
|
+
await writeFeedbackFile(applyRetention([feedback, ...items]));
|
|
44
|
+
return { ok: true, feedback };
|
|
45
|
+
}
|
|
46
|
+
async function rateLimitRejection(accountId, nowMs) {
|
|
47
|
+
const { consumeSiteFeedbackSubmission } = await import("./site-feedback-rate-limit-5BU2WSFE.js");
|
|
48
|
+
const limit = submissionLimit();
|
|
49
|
+
const windowMs = submissionWindowMs();
|
|
50
|
+
const decision = await consumeSiteFeedbackSubmission(accountId, { limit, windowMs }, nowMs);
|
|
51
|
+
if (decision.allowed) return void 0;
|
|
52
|
+
return {
|
|
53
|
+
code: "rate_limited",
|
|
54
|
+
limit,
|
|
55
|
+
windowSeconds: Math.ceil(windowMs / 1e3),
|
|
56
|
+
retryAfterSeconds: decision.retryAfterSeconds
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
function applyRetention(items, maxStored = maxStoredFeedback()) {
|
|
60
|
+
if (items.length <= maxStored) return items;
|
|
61
|
+
const evicted = new Set(
|
|
62
|
+
[...items].sort(byEvictionPriority).slice(0, items.length - maxStored).map((item) => item.id)
|
|
63
|
+
);
|
|
64
|
+
return items.filter((item) => !evicted.has(item.id));
|
|
65
|
+
}
|
|
66
|
+
function byEvictionPriority(left, right) {
|
|
67
|
+
if (left.status !== right.status) return left.status === "resolved" ? -1 : 1;
|
|
68
|
+
return createdAtMs(left) - createdAtMs(right);
|
|
69
|
+
}
|
|
70
|
+
function createdAtMs(item) {
|
|
71
|
+
const parsed = new Date(item.createdAt).getTime();
|
|
72
|
+
return Number.isFinite(parsed) ? parsed : 0;
|
|
73
|
+
}
|
|
74
|
+
function maxStoredFeedback() {
|
|
75
|
+
return readPositiveInt(process.env.AGORA_SITE_FEEDBACK_MAX_STORED, DEFAULT_MAX_STORED);
|
|
76
|
+
}
|
|
77
|
+
function submissionLimit() {
|
|
78
|
+
return readPositiveInt(process.env.AGORA_SITE_FEEDBACK_RATE_LIMIT, DEFAULT_RATE_LIMIT);
|
|
79
|
+
}
|
|
80
|
+
function submissionWindowMs() {
|
|
81
|
+
return readPositiveInt(process.env.AGORA_SITE_FEEDBACK_RATE_LIMIT_WINDOW_MS, DEFAULT_RATE_LIMIT_WINDOW_MS);
|
|
82
|
+
}
|
|
83
|
+
function readPositiveInt(value, fallback) {
|
|
84
|
+
if (!value) return fallback;
|
|
85
|
+
const parsed = Number.parseInt(value, 10);
|
|
86
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
|
|
87
|
+
}
|
|
88
|
+
async function resolveSiteFeedback(id, actor) {
|
|
89
|
+
const items = await readFeedbackFile();
|
|
90
|
+
const target = items.find((item) => item.id === id);
|
|
91
|
+
if (!target) return void 0;
|
|
92
|
+
const resolved = {
|
|
93
|
+
...target,
|
|
94
|
+
status: "resolved",
|
|
95
|
+
resolvedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
96
|
+
resolvedBy: actor.trim() || "Language Developer"
|
|
97
|
+
};
|
|
98
|
+
await writeFeedbackFile(items.map((item) => item.id === id ? resolved : item));
|
|
99
|
+
return resolved;
|
|
100
|
+
}
|
|
101
|
+
async function readFeedbackFile() {
|
|
102
|
+
const filePath = feedbackPath();
|
|
103
|
+
try {
|
|
104
|
+
const parsed = JSON.parse(await readFile(filePath, "utf8"));
|
|
105
|
+
return Array.isArray(parsed) ? parsed.filter(isSiteFeedback) : [];
|
|
106
|
+
} catch (error) {
|
|
107
|
+
if (error instanceof Error && "code" in error && Reflect.get(error, "code") === "ENOENT") return [];
|
|
108
|
+
throw error;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
async function writeFeedbackFile(items) {
|
|
112
|
+
const filePath = feedbackPath();
|
|
113
|
+
await mkdir(path.dirname(filePath), { recursive: true });
|
|
114
|
+
await writeFile(filePath, `${JSON.stringify(items, null, 2)}
|
|
115
|
+
`);
|
|
116
|
+
}
|
|
117
|
+
function feedbackPath() {
|
|
118
|
+
return safeJoin(getIssueStoreRoot(), FEEDBACK_FILE);
|
|
119
|
+
}
|
|
120
|
+
function normalizedPagePath(value) {
|
|
121
|
+
const text = value?.trim() || "/";
|
|
122
|
+
if (!text.startsWith("/")) return "/";
|
|
123
|
+
return text.slice(0, 240);
|
|
124
|
+
}
|
|
125
|
+
function isSiteFeedback(value) {
|
|
126
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
127
|
+
const status = Reflect.get(value, "status");
|
|
128
|
+
return typeof Reflect.get(value, "id") === "string" && typeof Reflect.get(value, "message") === "string" && typeof Reflect.get(value, "pagePath") === "string" && typeof Reflect.get(value, "reporter") === "string" && (status === "open" || status === "resolved") && typeof Reflect.get(value, "createdAt") === "string";
|
|
129
|
+
}
|
|
130
|
+
export {
|
|
131
|
+
SITE_FEEDBACK_MESSAGE_MAX_LENGTH,
|
|
132
|
+
addSiteFeedback,
|
|
133
|
+
applyRetention,
|
|
134
|
+
listSiteFeedback,
|
|
135
|
+
resolveSiteFeedback
|
|
136
|
+
};
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getIssueStoreRoot,
|
|
3
|
+
safeJoin
|
|
4
|
+
} from "./chunk-UHJXD4TG.js";
|
|
5
|
+
|
|
6
|
+
// lib/server/site-feedback-rate-limit.ts
|
|
7
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
8
|
+
import path from "node:path";
|
|
9
|
+
var RATE_LIMIT_FILE = "site-feedback-rate-limit.json";
|
|
10
|
+
var DEFAULT_MAX_TRACKED_ACCOUNTS = 2e3;
|
|
11
|
+
var rateLimitMutation = Promise.resolve();
|
|
12
|
+
function evaluateSiteFeedbackSubmission(state, accountId, settings, nowMs) {
|
|
13
|
+
const pruned = pruneExpired(state, settings.windowMs, nowMs);
|
|
14
|
+
const recent = pruned[accountId] ?? [];
|
|
15
|
+
if (recent.length >= settings.limit) {
|
|
16
|
+
const oldest = recent[0] ?? nowMs;
|
|
17
|
+
const retryAfterMs = Math.max(1, oldest + settings.windowMs - nowMs);
|
|
18
|
+
return {
|
|
19
|
+
decision: { allowed: false, retryAfterSeconds: Math.ceil(retryAfterMs / 1e3) },
|
|
20
|
+
state: capTrackedAccounts(pruned, settings)
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
return {
|
|
24
|
+
decision: { allowed: true, remaining: settings.limit - recent.length - 1 },
|
|
25
|
+
state: capTrackedAccounts({ ...pruned, [accountId]: [...recent, nowMs] }, settings)
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
async function consumeSiteFeedbackSubmission(accountId, settings, nowMs) {
|
|
29
|
+
const operation = rateLimitMutation.then(async () => {
|
|
30
|
+
const evaluation = evaluateSiteFeedbackSubmission(await readRateLimitState(), accountId, settings, nowMs);
|
|
31
|
+
await writeRateLimitState(evaluation.state);
|
|
32
|
+
return evaluation.decision;
|
|
33
|
+
});
|
|
34
|
+
rateLimitMutation = operation.then(() => void 0, () => void 0);
|
|
35
|
+
return operation;
|
|
36
|
+
}
|
|
37
|
+
async function readRateLimitState() {
|
|
38
|
+
try {
|
|
39
|
+
return parseRateLimitState(JSON.parse(await readFile(rateLimitPath(), "utf8")));
|
|
40
|
+
} catch (error) {
|
|
41
|
+
if (error instanceof SyntaxError) return {};
|
|
42
|
+
if (error instanceof Error && "code" in error && Reflect.get(error, "code") === "ENOENT") return {};
|
|
43
|
+
throw error;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
async function writeRateLimitState(state) {
|
|
47
|
+
const filePath = rateLimitPath();
|
|
48
|
+
await mkdir(path.dirname(filePath), { recursive: true });
|
|
49
|
+
await writeFile(filePath, `${JSON.stringify(state, null, 2)}
|
|
50
|
+
`);
|
|
51
|
+
}
|
|
52
|
+
function pruneExpired(state, windowMs, nowMs) {
|
|
53
|
+
const cutoff = nowMs - windowMs;
|
|
54
|
+
const entries = Object.entries(state).map(([accountId, instants]) => [accountId, instants.filter((instant) => instant > cutoff).sort(ascending)]).filter(([, instants]) => instants.length > 0);
|
|
55
|
+
return Object.fromEntries(entries);
|
|
56
|
+
}
|
|
57
|
+
function capTrackedAccounts(state, settings) {
|
|
58
|
+
const max = settings.maxTrackedAccounts ?? DEFAULT_MAX_TRACKED_ACCOUNTS;
|
|
59
|
+
const entries = Object.entries(state);
|
|
60
|
+
if (entries.length <= max) return state;
|
|
61
|
+
const retained = [...entries].sort(([, left], [, right]) => lastInstant(right) - lastInstant(left)).slice(0, max);
|
|
62
|
+
return Object.fromEntries(retained);
|
|
63
|
+
}
|
|
64
|
+
function lastInstant(instants) {
|
|
65
|
+
return instants[instants.length - 1] ?? 0;
|
|
66
|
+
}
|
|
67
|
+
function ascending(left, right) {
|
|
68
|
+
return left - right;
|
|
69
|
+
}
|
|
70
|
+
function rateLimitPath() {
|
|
71
|
+
return safeJoin(getIssueStoreRoot(), RATE_LIMIT_FILE);
|
|
72
|
+
}
|
|
73
|
+
function parseRateLimitState(value) {
|
|
74
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return {};
|
|
75
|
+
const entries = Object.entries(value).map(([accountId, instants]) => [accountId, readInstants(instants)]).filter(([, instants]) => instants.length > 0);
|
|
76
|
+
return Object.fromEntries(entries);
|
|
77
|
+
}
|
|
78
|
+
function readInstants(value) {
|
|
79
|
+
if (!Array.isArray(value)) return [];
|
|
80
|
+
return value.filter((entry) => typeof entry === "number" && Number.isFinite(entry)).sort(ascending);
|
|
81
|
+
}
|
|
82
|
+
export {
|
|
83
|
+
consumeSiteFeedbackSubmission,
|
|
84
|
+
evaluateSiteFeedbackSubmission,
|
|
85
|
+
readRateLimitState
|
|
86
|
+
};
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import {
|
|
2
|
+
LagoraCliConfigStore,
|
|
3
|
+
apiSession,
|
|
4
|
+
baseApiUrl,
|
|
5
|
+
readJsonFromApi,
|
|
6
|
+
writeJsonToApi
|
|
7
|
+
} from "./chunk-AZ3EEBVD.js";
|
|
8
|
+
import "./chunk-TJZVQYBL.js";
|
|
9
|
+
|
|
10
|
+
// scripts/site-feedback.ts
|
|
11
|
+
import path from "node:path";
|
|
12
|
+
|
|
13
|
+
// scripts/site-feedback-api-client.ts
|
|
14
|
+
async function listFeedbackFromApi(session) {
|
|
15
|
+
const payload = await readJsonFromApi(`${baseApiUrl(session.apiUrl)}/api/site-feedback`, session);
|
|
16
|
+
return readFeedbackList(payload);
|
|
17
|
+
}
|
|
18
|
+
async function resolveFeedbackFromApi(input) {
|
|
19
|
+
const payload = await writeJsonToApi(`${baseApiUrl(input.session.apiUrl)}/api/site-feedback`, "PATCH", {
|
|
20
|
+
id: input.id,
|
|
21
|
+
actor: input.actor
|
|
22
|
+
}, input.session);
|
|
23
|
+
return readFeedback(payload);
|
|
24
|
+
}
|
|
25
|
+
function readFeedbackList(payload) {
|
|
26
|
+
if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
|
|
27
|
+
throw new Error("Feedback response has an unsupported shape");
|
|
28
|
+
}
|
|
29
|
+
const feedback = Reflect.get(payload, "feedback");
|
|
30
|
+
if (!Array.isArray(feedback) || !feedback.every(isSiteFeedback)) {
|
|
31
|
+
throw new Error("Feedback response has an unsupported shape");
|
|
32
|
+
}
|
|
33
|
+
return feedback;
|
|
34
|
+
}
|
|
35
|
+
function readFeedback(payload) {
|
|
36
|
+
if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
|
|
37
|
+
throw new Error("Feedback response has an unsupported shape");
|
|
38
|
+
}
|
|
39
|
+
const feedback = Reflect.get(payload, "feedback");
|
|
40
|
+
if (!isSiteFeedback(feedback)) throw new Error("Feedback response has an unsupported shape");
|
|
41
|
+
return feedback;
|
|
42
|
+
}
|
|
43
|
+
function isSiteFeedback(value) {
|
|
44
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
45
|
+
const status = Reflect.get(value, "status");
|
|
46
|
+
return typeof Reflect.get(value, "id") === "string" && typeof Reflect.get(value, "message") === "string" && typeof Reflect.get(value, "pagePath") === "string" && typeof Reflect.get(value, "reporter") === "string" && (status === "open" || status === "resolved") && typeof Reflect.get(value, "createdAt") === "string";
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// scripts/site-feedback.ts
|
|
50
|
+
function parseArgs(argv) {
|
|
51
|
+
const args = { command: argv[0] };
|
|
52
|
+
for (let index = 1; index < argv.length; index += 1) {
|
|
53
|
+
const token = argv[index];
|
|
54
|
+
const value = argv[index + 1];
|
|
55
|
+
if (!token.startsWith("--")) continue;
|
|
56
|
+
if (!value || value.startsWith("--")) throw new Error(`${token} requires a value`);
|
|
57
|
+
index += 1;
|
|
58
|
+
if (token === "--id") args.id = value;
|
|
59
|
+
if (token === "--api-url") args.apiUrl = value;
|
|
60
|
+
if (token === "--author") args.author = value;
|
|
61
|
+
if (token === "--status") args.status = value;
|
|
62
|
+
if (token === "--store") args.store = value;
|
|
63
|
+
}
|
|
64
|
+
return args;
|
|
65
|
+
}
|
|
66
|
+
async function main() {
|
|
67
|
+
const args = parseArgs(process.argv.slice(2));
|
|
68
|
+
if (args.store?.trim()) process.env.AGORA_ISSUE_STORE = path.resolve(args.store.trim());
|
|
69
|
+
const command = args.command ?? "list";
|
|
70
|
+
const config = new LagoraCliConfigStore();
|
|
71
|
+
const session = args.store?.trim() ? void 0 : await remoteSession(config, args.apiUrl);
|
|
72
|
+
if (command === "list" || command === "fetch") {
|
|
73
|
+
const feedback = session ? await listFeedbackFromApi(session) : await localFeedback();
|
|
74
|
+
printFeedback(feedback.filter((item) => !args.status || item.status === args.status));
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
if (command === "resolve") {
|
|
78
|
+
const id = requireArg(args.id, "--id");
|
|
79
|
+
const actor = await config.author(args.author);
|
|
80
|
+
const feedback = session ? await resolveFeedbackFromApi({ session, id, actor }) : await resolveLocalFeedback(id, actor);
|
|
81
|
+
if (!feedback) throw new Error(`Feedback not found: ${id}`);
|
|
82
|
+
console.log(`Feedback resolved: ${feedback.id}`);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
throw new Error("Usage: lagora feedback list|fetch|resolve [--api-url url] [--store path] [--id feedback-id] [--status open|resolved]");
|
|
86
|
+
}
|
|
87
|
+
async function remoteSession(config, explicitApiUrl) {
|
|
88
|
+
const stored = await config.apiSession(explicitApiUrl);
|
|
89
|
+
return stored ? apiSession(stored.apiUrl, { cookie: stored.cookie, token: stored.token }) : void 0;
|
|
90
|
+
}
|
|
91
|
+
async function localFeedback() {
|
|
92
|
+
const { listSiteFeedback } = await import("./site-feedback-CAPE5MPX.js");
|
|
93
|
+
return listSiteFeedback();
|
|
94
|
+
}
|
|
95
|
+
async function resolveLocalFeedback(id, actor) {
|
|
96
|
+
const { resolveSiteFeedback } = await import("./site-feedback-CAPE5MPX.js");
|
|
97
|
+
return resolveSiteFeedback(id, actor);
|
|
98
|
+
}
|
|
99
|
+
function printFeedback(feedback) {
|
|
100
|
+
if (feedback.length === 0) {
|
|
101
|
+
console.log("No site feedback.");
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
for (const item of feedback) {
|
|
105
|
+
console.log(`${item.id} [${item.status}] ${item.pagePath} \xB7 ${item.reporter} \xB7 ${item.createdAt}`);
|
|
106
|
+
console.log(item.message);
|
|
107
|
+
console.log("");
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
function requireArg(value, name) {
|
|
111
|
+
if (!value?.trim()) throw new Error(`${name} is required`);
|
|
112
|
+
return value.trim();
|
|
113
|
+
}
|
|
114
|
+
main().catch((error) => {
|
|
115
|
+
console.error(error instanceof Error ? error.message : error);
|
|
116
|
+
process.exit(1);
|
|
117
|
+
});
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import {
|
|
2
|
+
addComment,
|
|
3
|
+
addIssueBlocker,
|
|
4
|
+
addIssueTags,
|
|
5
|
+
addKernelSuggestion,
|
|
6
|
+
addTagDefinition,
|
|
7
|
+
assignIssue,
|
|
8
|
+
clearIssueAssignee,
|
|
9
|
+
closeIssue,
|
|
10
|
+
createIssue,
|
|
11
|
+
deleteComment,
|
|
12
|
+
deleteIssue,
|
|
13
|
+
deleteTagDefinition,
|
|
14
|
+
deleteVerificationRuns,
|
|
15
|
+
getArtifactTextPath,
|
|
16
|
+
getIssue,
|
|
17
|
+
hasRunningVerificationRun,
|
|
18
|
+
isAutoLoweringEnabled,
|
|
19
|
+
listIssues,
|
|
20
|
+
listTagDefinitions,
|
|
21
|
+
readArtifactText,
|
|
22
|
+
readAutomationSettings,
|
|
23
|
+
recordVerificationResult,
|
|
24
|
+
removeIssueBlocker,
|
|
25
|
+
syncIssueBlocker,
|
|
26
|
+
unassignIssue,
|
|
27
|
+
updateComment,
|
|
28
|
+
updateIssueDescription,
|
|
29
|
+
updateIssueStatus,
|
|
30
|
+
updateKernelSuggestion,
|
|
31
|
+
updateVerificationRunResult,
|
|
32
|
+
writeAutomationSettings
|
|
33
|
+
} from "./chunk-NBJMYAOA.js";
|
|
34
|
+
import "./chunk-QJPQHKIO.js";
|
|
35
|
+
export {
|
|
36
|
+
addComment,
|
|
37
|
+
addIssueBlocker,
|
|
38
|
+
addIssueTags,
|
|
39
|
+
addKernelSuggestion,
|
|
40
|
+
addTagDefinition,
|
|
41
|
+
assignIssue,
|
|
42
|
+
clearIssueAssignee,
|
|
43
|
+
closeIssue,
|
|
44
|
+
createIssue,
|
|
45
|
+
deleteComment,
|
|
46
|
+
deleteIssue,
|
|
47
|
+
deleteTagDefinition,
|
|
48
|
+
deleteVerificationRuns,
|
|
49
|
+
getArtifactTextPath,
|
|
50
|
+
getIssue,
|
|
51
|
+
hasRunningVerificationRun,
|
|
52
|
+
isAutoLoweringEnabled,
|
|
53
|
+
listIssues,
|
|
54
|
+
listTagDefinitions,
|
|
55
|
+
readArtifactText,
|
|
56
|
+
readAutomationSettings,
|
|
57
|
+
recordVerificationResult,
|
|
58
|
+
removeIssueBlocker,
|
|
59
|
+
syncIssueBlocker,
|
|
60
|
+
unassignIssue,
|
|
61
|
+
updateComment,
|
|
62
|
+
updateIssueDescription,
|
|
63
|
+
updateIssueStatus,
|
|
64
|
+
updateKernelSuggestion,
|
|
65
|
+
updateVerificationRunResult,
|
|
66
|
+
writeAutomationSettings
|
|
67
|
+
};
|